diff --git a/client/package.json b/client/package.json
index 6046baf8fb..9b2477ed79 100644
--- a/client/package.json
+++ b/client/package.json
@@ -28,7 +28,7 @@
},
"homepage": "https://librechat.ai",
"dependencies": {
- "@ariakit/react": "^0.4.5",
+ "@ariakit/react": "^0.4.8",
"@dicebear/collection": "^7.0.4",
"@dicebear/core": "^7.0.4",
"@headlessui/react": "^2.1.2",
diff --git a/client/src/components/SidePanel/AssistantSwitcher.tsx b/client/src/components/SidePanel/AssistantSwitcher.tsx
index 1348f6feff..26f253f76e 100644
--- a/client/src/components/SidePanel/AssistantSwitcher.tsx
+++ b/client/src/components/SidePanel/AssistantSwitcher.tsx
@@ -1,10 +1,10 @@
import { useEffect, useMemo } from 'react';
-import { Combobox } from '~/components/ui';
import { isAssistantsEndpoint, LocalStorageKeys } from 'librechat-data-provider';
import type { AssistantsEndpoint } from 'librechat-data-provider';
import type { SwitcherProps, AssistantListItem } from '~/common';
import { useSetIndexOptions, useSelectAssistant, useLocalize, useAssistantListMap } from '~/hooks';
import { useChatContext, useAssistantsMapContext } from '~/Providers';
+import ControlCombobox from '~/components/ui/ControlCombobox';
import Icon from '~/components/Endpoints/Icon';
export default function AssistantSwitcher({ isCollapsed }: SwitcherProps) {
@@ -31,7 +31,7 @@ export default function AssistantSwitcher({ isCollapsed }: SwitcherProps) {
localStorage.getItem(`${LocalStorageKeys.ASST_ID_PREFIX}${index}${endpoint}`) ??
assistants[0]?.id ??
'';
- const assistant = assistantMap?.[endpoint ?? '']?.[assistant_id];
+ const assistant = assistantMap[endpoint ?? ''][assistant_id];
if (!assistant) {
return;
@@ -51,14 +51,14 @@ export default function AssistantSwitcher({ isCollapsed }: SwitcherProps) {
const assistantOptions = useMemo(() => {
return assistants.map((assistant) => {
return {
- label: assistant.name ?? '',
+ label: (assistant.name as string | null) ?? '',
value: assistant.id,
icon: (
),
};
@@ -66,7 +66,7 @@ export default function AssistantSwitcher({ isCollapsed }: SwitcherProps) {
}, [assistants, endpoint]);
return (
- assistant.id === selectedAssistant)?.name ??
@@ -83,7 +83,7 @@ export default function AssistantSwitcher({ isCollapsed }: SwitcherProps) {
isCreatedByUser={false}
endpoint={endpoint}
assistantName={currentAssistant?.name ?? ''}
- iconURL={(currentAssistant?.metadata?.avatar as string) ?? ''}
+ iconURL={currentAssistant?.metadata?.avatar ?? ''}
/>
}
/>
diff --git a/client/src/components/SidePanel/ModelSwitcher.tsx b/client/src/components/SidePanel/ModelSwitcher.tsx
index cff1342bec..6c8d79a38e 100644
--- a/client/src/components/SidePanel/ModelSwitcher.tsx
+++ b/client/src/components/SidePanel/ModelSwitcher.tsx
@@ -1,10 +1,10 @@
import { useMemo, useRef, useCallback } from 'react';
import { useGetModelsQuery } from 'librechat-data-provider/react-query';
+import type { SwitcherProps } from '~/common';
+import ControlCombobox from '~/components/ui/ControlCombobox';
import MinimalIcon from '~/components/Endpoints/MinimalIcon';
import { useSetIndexOptions, useLocalize } from '~/hooks';
-import type { SwitcherProps } from '~/common';
import { useChatContext } from '~/Providers';
-import { Combobox } from '~/components/ui';
import { mainTextareaId } from '~/common';
export default function ModelSwitcher({ isCollapsed }: SwitcherProps) {
@@ -16,7 +16,10 @@ export default function ModelSwitcher({ isCollapsed }: SwitcherProps) {
const { endpoint, model = null } = conversation ?? {};
const models = useMemo(() => {
- return modelsQuery?.data?.[endpoint ?? ''] ?? [];
+ return (modelsQuery.data?.[endpoint ?? ''] ?? []).map((model) => ({
+ label: model,
+ value: model,
+ }));
}, [modelsQuery, endpoint]);
const setModel = useCallback(
@@ -34,7 +37,8 @@ export default function ModelSwitcher({ isCollapsed }: SwitcherProps) {
);
return (
-
-
+
>
);
} else if (isAssistantsEndpoint(props.endpoint)) {
@@ -19,7 +19,7 @@ export default function Switcher(props: SwitcherProps) {
return (
<>
-
+
>
);
}
diff --git a/client/src/components/ui/Combobox.tsx b/client/src/components/ui/Combobox.tsx
index e8843284e1..0bc1cc8ecd 100644
--- a/client/src/components/ui/Combobox.tsx
+++ b/client/src/components/ui/Combobox.tsx
@@ -1,4 +1,4 @@
-import { startTransition, useMemo } from 'react';
+import { startTransition } from 'react';
import { Search as SearchIcon } from 'lucide-react';
import * as RadixSelect from '@radix-ui/react-select';
import { CheckIcon, ChevronDownIcon } from '@radix-ui/react-icons';
@@ -52,7 +52,16 @@ export default function ComboboxComponent({
value={selectedValue}
onValueChange={setValue}
open={open}
- onOpenChange={setOpen}
+ /** Hacky fix for radix-ui Android issue: https://github.com/radix-ui/primitives/issues/1658 */
+ onOpenChange={() => {
+ if (open === true) {
+ setOpen(false);
+ return;
+ }
+ setTimeout(() => {
+ setOpen(!open);
+ }, 75);
+ }}
>
{
+ setValue(`${value ?? ''}`);
+ setOpen(false);
+ }}
>
diff --git a/client/src/components/ui/ControlCombobox.tsx b/client/src/components/ui/ControlCombobox.tsx
new file mode 100644
index 0000000000..abd8d69700
--- /dev/null
+++ b/client/src/components/ui/ControlCombobox.tsx
@@ -0,0 +1,123 @@
+import * as Ariakit from '@ariakit/react';
+import { matchSorter } from 'match-sorter';
+import { startTransition, useMemo, useState, useEffect, useRef } from 'react';
+import { cn } from '~/utils';
+import type { OptionWithIcon } from '~/common';
+import { Search } from 'lucide-react';
+
+interface ControlComboboxProps {
+ selectedValue: string;
+ displayValue?: string;
+ items: OptionWithIcon[];
+ setValue: (value: string) => void;
+ ariaLabel: string;
+ searchPlaceholder?: string;
+ selectPlaceholder?: string;
+ isCollapsed: boolean;
+ SelectIcon?: React.ReactNode;
+}
+
+export default function ControlCombobox({
+ selectedValue,
+ displayValue,
+ items,
+ setValue,
+ ariaLabel,
+ searchPlaceholder,
+ selectPlaceholder,
+ isCollapsed,
+ SelectIcon,
+}: ControlComboboxProps) {
+ const [searchValue, setSearchValue] = useState('');
+ const buttonRef = useRef(null);
+ const [buttonWidth, setButtonWidth] = useState(null);
+
+ const matches = useMemo(() => {
+ return matchSorter(items, searchValue, {
+ keys: ['value', 'label'],
+ baseSort: (a, b) => (a.index < b.index ? -1 : 1),
+ });
+ }, [searchValue, items]);
+
+ useEffect(() => {
+ if (buttonRef.current && !isCollapsed) {
+ setButtonWidth(buttonRef.current.offsetWidth);
+ }
+ }, [isCollapsed]);
+
+ return (
+
+
{
+ startTransition(() => {
+ setSearchValue(value);
+ });
+ }}
+ >
+
+ {ariaLabel}
+
+ {SelectIcon != null && (
+
+ {SelectIcon}
+
+ )}
+ {!isCollapsed && (
+
+ {displayValue ?? selectPlaceholder}
+
+ )}
+
+
+
+
+ {matches.map((item) => (
+ }
+ >
+ {item.icon != null && (
+
+ {item.icon}
+
+ )}
+ {item.label}
+
+ ))}
+
+
+
+
+
+ );
+}
diff --git a/client/src/hooks/SSE/useEventHandlers.ts b/client/src/hooks/SSE/useEventHandlers.ts
index 4cc9309370..395ff9693d 100644
--- a/client/src/hooks/SSE/useEventHandlers.ts
+++ b/client/src/hooks/SSE/useEventHandlers.ts
@@ -1,7 +1,8 @@
import { v4 } from 'uuid';
+import { useCallback } from 'react';
+import { useSetRecoilState } from 'recoil';
import { useParams } from 'react-router-dom';
import { useQueryClient } from '@tanstack/react-query';
-import { useCallback } from 'react';
import {
QueryKeys,
Constants,
@@ -29,6 +30,7 @@ import useContentHandler from '~/hooks/SSE/useContentHandler';
import type { TGenTitleMutation } from '~/data-provider';
import { useAuthContext } from '~/hooks/AuthContext';
import { useLiveAnnouncer } from '~/Providers';
+import store from '~/store';
type TSyncData = {
sync: boolean;
@@ -65,6 +67,7 @@ export default function useEventHandlers({
resetLatestMessage,
}: EventHandlerParams) {
const queryClient = useQueryClient();
+ const setAbortScroll = useSetRecoilState(store.abortScroll);
const { announcePolite, announceAssertive } = useLiveAnnouncer();
const { conversationId: paramId } = useParams();
@@ -306,15 +309,16 @@ export default function useEventHandlers({
resetLatestMessage();
}
- scrollToEnd();
+ scrollToEnd(() => setAbortScroll(false));
},
[
setMessages,
- setConversation,
queryClient,
+ setAbortScroll,
isAddedRequest,
- resetLatestMessage,
+ setConversation,
announceAssertive,
+ resetLatestMessage,
],
);
diff --git a/client/src/utils/messages.ts b/client/src/utils/messages.ts
index 88ab105866..b5131c00e1 100644
--- a/client/src/utils/messages.ts
+++ b/client/src/utils/messages.ts
@@ -44,9 +44,12 @@ export const getTextKey = (message?: TMessage | null, convoId?: string | null) =
}${message.conversationId ?? convoId}`;
};
-export const scrollToEnd = () => {
+export const scrollToEnd = (callback?: () => void) => {
const messagesEndElement = document.getElementById('messages-end');
if (messagesEndElement) {
messagesEndElement.scrollIntoView({ behavior: 'instant' });
+ if (callback) {
+ callback();
+ }
}
};
diff --git a/package-lock.json b/package-lock.json
index 7110c93f78..037ab4f114 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1117,7 +1117,7 @@
"version": "0.7.4",
"license": "ISC",
"dependencies": {
- "@ariakit/react": "^0.4.5",
+ "@ariakit/react": "^0.4.8",
"@dicebear/collection": "^7.0.4",
"@dicebear/core": "^7.0.4",
"@headlessui/react": "^2.1.2",
@@ -1289,38 +1289,38 @@
}
},
"node_modules/@ariakit/core": {
- "version": "0.4.5",
- "resolved": "https://registry.npmjs.org/@ariakit/core/-/core-0.4.5.tgz",
- "integrity": "sha512-e294+bEcyzt/H/kO4fS5/czLAlkF7PY+Kul3q2z54VY+GGay8NlVs9UezAB7L4jUBlYRAXwp7/1Sq3R7b+MZ7w=="
+ "version": "0.4.8",
+ "resolved": "https://registry.npmjs.org/@ariakit/core/-/core-0.4.8.tgz",
+ "integrity": "sha512-HQS+9CI7pMqqVlAt5bPGenT0/e65UxXY+PKtgU7Y+0UToBDBRolO5S9+UUSDm8OmJHSnq24owEGm1Mv28l5XCQ=="
},
"node_modules/@ariakit/react": {
- "version": "0.4.5",
- "resolved": "https://registry.npmjs.org/@ariakit/react/-/react-0.4.5.tgz",
- "integrity": "sha512-GUHxaOY1JZrJUHkuV20IY4NWcgknhqTQM0qCQcVZDCi+pJiWchUjTG+UyIr/Of02hU569qnQ7yovskCf+V3tNg==",
+ "version": "0.4.8",
+ "resolved": "https://registry.npmjs.org/@ariakit/react/-/react-0.4.8.tgz",
+ "integrity": "sha512-Bb1vOrp0X52hxi1wE9TEHjjZ/Y08tVq2ZH+RFDwRQB3g04uVwrrhnTccHepC6rsObrDpAOV3/YlJCi4k/lSUaQ==",
"dependencies": {
- "@ariakit/react-core": "0.4.5"
+ "@ariakit/react-core": "0.4.8"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/ariakit"
},
"peerDependencies": {
- "react": "^17.0.0 || ^18.0.0",
- "react-dom": "^17.0.0 || ^18.0.0"
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/@ariakit/react-core": {
- "version": "0.4.5",
- "resolved": "https://registry.npmjs.org/@ariakit/react-core/-/react-core-0.4.5.tgz",
- "integrity": "sha512-ciTYPwpj/+mdA+EstveEnoygbx5e4PXQJxfkLKy4lkTkDJJUS9GcbYhdnIFJVUta6P1YFvzkIKo+/y9mcbMKJg==",
+ "version": "0.4.8",
+ "resolved": "https://registry.npmjs.org/@ariakit/react-core/-/react-core-0.4.8.tgz",
+ "integrity": "sha512-TzsddUWQwWYhrEVWHA/Gf7KCGx8rwFohAHfuljjqidKeZi2kUmuRAImCTG9oga34FWHFf4AdXQbBKclMNt0nrQ==",
"dependencies": {
- "@ariakit/core": "0.4.5",
+ "@ariakit/core": "0.4.8",
"@floating-ui/dom": "^1.0.0",
"use-sync-external-store": "^1.2.0"
},
"peerDependencies": {
- "react": "^17.0.0 || ^18.0.0",
- "react-dom": "^17.0.0 || ^18.0.0"
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/@aws-crypto/crc32": {