diff --git a/client/src/components/Chat/Landing.tsx b/client/src/components/Chat/Landing.tsx
index 032c59f538..c4eb4a904a 100644
--- a/client/src/components/Chat/Landing.tsx
+++ b/client/src/components/Chat/Landing.tsx
@@ -9,7 +9,7 @@ import { useLocalize, useAuthContext } from '~/hooks';
import { getIconEndpoint, getEntity } from '~/utils';
const containerClassName =
- 'shadow-stroke relative flex h-full items-center justify-center rounded-full bg-white text-black';
+ 'shadow-stroke relative flex h-full items-center justify-center rounded-full bg-white dark:bg-presentation dark:text-white text-black dark:after:shadow-none ';
function getTextSizeClass(text: string | undefined | null) {
if (!text) {
diff --git a/client/src/components/Nav/NewChat.tsx b/client/src/components/Nav/NewChat.tsx
index a891fe9386..c43ff933d8 100644
--- a/client/src/components/Nav/NewChat.tsx
+++ b/client/src/components/Nav/NewChat.tsx
@@ -5,7 +5,7 @@ import { useNavigate } from 'react-router-dom';
import { useQueryClient } from '@tanstack/react-query';
import { QueryKeys, Constants } from 'librechat-data-provider';
import type { TConversation, TMessage } from 'librechat-data-provider';
-import { getEndpointField, getIconEndpoint, getIconKey } from '~/utils';
+import { createChatSearchParams, getEndpointField, getIconEndpoint, getIconKey } from '~/utils';
import ConvoIconURL from '~/components/Endpoints/ConvoIconURL';
import { useGetEndpointsQuery } from '~/data-provider';
import { useLocalize, useNewConvo } from '~/hooks';
@@ -57,7 +57,7 @@ const NewChatButtonIcon = React.memo(({ conversation }: { conversation: TConvers
context="nav"
/>
) : (
-
+
{endpoint && Icon && (
) => {
- if (event.button === 0 && !(event.ctrlKey || event.metaKey)) {
- event.preventDefault();
- queryClient.setQueryData(
- [QueryKeys.messages, conversation?.conversationId ?? Constants.NEW_CONVO],
- [],
- );
- newConvo();
- navigate('/c/new');
- toggleNav();
- }
- },
- [queryClient, conversation, newConvo, navigate, toggleNav],
- );
+ const clickHandler = useCallback(() => {
+ queryClient.setQueryData(
+ [QueryKeys.messages, conversation?.conversationId ?? Constants.NEW_CONVO],
+ [],
+ );
+ const params = createChatSearchParams(defaultPreset ?? conversation);
+ const newRoute = params.size > 0 ? `/c/new?${params.toString()}` : '/c/new';
+
+ newConvo();
+ navigate(newRoute);
+ toggleNav();
+ }, [queryClient, conversation, newConvo, navigate, toggleNav, defaultPreset]);
return (
{subHeaders != null ? subHeaders : null}
diff --git a/client/src/components/SidePanel/Parameters/DynamicCombobox.tsx b/client/src/components/SidePanel/Parameters/DynamicCombobox.tsx
index c0aca06149..d2875034e7 100644
--- a/client/src/components/SidePanel/Parameters/DynamicCombobox.tsx
+++ b/client/src/components/SidePanel/Parameters/DynamicCombobox.tsx
@@ -3,7 +3,7 @@ import { OptionTypes } from 'librechat-data-provider';
import type { DynamicSettingProps } from 'librechat-data-provider';
import { Label, HoverCard, HoverCardTrigger } from '~/components/ui';
import ControlCombobox from '~/components/ui/ControlCombobox';
-import { TranslationKeys, useLocalize, useParameterEffects } from '~/hooks';
+import { TranslationKeys, useLocalize, useParameterEffects, useUpdateSearchParams } from '~/hooks';
import { useChatContext } from '~/Providers';
import OptionHover from './OptionHover';
import { ESide } from '~/common';
@@ -33,6 +33,7 @@ function DynamicCombobox({
}: DynamicSettingProps & { isCollapsed?: boolean; SelectIcon?: React.ReactNode }) {
const localize = useLocalize();
const { preset } = useChatContext();
+ const updateSearchParams = useUpdateSearchParams();
const [inputValue, setInputValue] = useState
(null);
const selectedValue = useMemo(() => {
@@ -59,8 +60,10 @@ function DynamicCombobox({
} else {
setOption(settingKey)(value);
}
+
+ updateSearchParams({ [settingKey]: value });
},
- [optionType, setOption, settingKey],
+ [optionType, setOption, settingKey, updateSearchParams],
);
useParameterEffects({
@@ -93,7 +96,7 @@ function DynamicCombobox({
htmlFor={`${settingKey}-dynamic-combobox`}
className="text-left text-sm font-medium"
>
- {labelCode ? localize(label as TranslationKeys) ?? label : label || settingKey}
+ {labelCode ? (localize(label as TranslationKeys) ?? label) : label || settingKey}
{showDefault && (
({localize('com_endpoint_default')}: {defaultValue})
@@ -105,10 +108,14 @@ function DynamicCombobox({
{description && (
)}
diff --git a/client/src/components/SidePanel/Parameters/DynamicInput.tsx b/client/src/components/SidePanel/Parameters/DynamicInput.tsx
index e2c49a5d4a..79e8a5d43c 100644
--- a/client/src/components/SidePanel/Parameters/DynamicInput.tsx
+++ b/client/src/components/SidePanel/Parameters/DynamicInput.tsx
@@ -1,6 +1,12 @@
import { OptionTypes } from 'librechat-data-provider';
import type { DynamicSettingProps } from 'librechat-data-provider';
-import { useLocalize, useDebouncedInput, useParameterEffects, TranslationKeys } from '~/hooks';
+import {
+ useLocalize,
+ useDebouncedInput,
+ useParameterEffects,
+ TranslationKeys,
+ useUpdateSearchParams,
+} from '~/hooks';
import { Label, Input, HoverCard, HoverCardTrigger } from '~/components/ui';
import { useChatContext } from '~/Providers';
import OptionHover from './OptionHover';
@@ -26,6 +32,7 @@ function DynamicInput({
}: DynamicSettingProps) {
const localize = useLocalize();
const { preset } = useChatContext();
+ const updateSearchParams = useUpdateSearchParams();
const [setInputValue, inputValue, setLocalValue] = useDebouncedInput({
optionKey: optionType !== OptionTypes.Custom ? settingKey : undefined,
@@ -47,6 +54,7 @@ function DynamicInput({
const value = e.target.value;
if (type !== 'number') {
setInputValue(e);
+ updateSearchParams({ [settingKey]: value });
return;
}
@@ -55,6 +63,7 @@ function DynamicInput({
} else if (!isNaN(Number(value))) {
setInputValue(e, true);
}
+ updateSearchParams({ [settingKey]: value });
};
return (
diff --git a/client/src/components/SidePanel/Parameters/DynamicSlider.tsx b/client/src/components/SidePanel/Parameters/DynamicSlider.tsx
index 92ab0a91f6..e440f62953 100644
--- a/client/src/components/SidePanel/Parameters/DynamicSlider.tsx
+++ b/client/src/components/SidePanel/Parameters/DynamicSlider.tsx
@@ -2,7 +2,13 @@ import { useMemo, useCallback } from 'react';
import { OptionTypes } from 'librechat-data-provider';
import type { DynamicSettingProps } from 'librechat-data-provider';
import { Label, Slider, HoverCard, Input, InputNumber, HoverCardTrigger } from '~/components/ui';
-import { useLocalize, useDebouncedInput, useParameterEffects, TranslationKeys } from '~/hooks';
+import {
+ useLocalize,
+ useDebouncedInput,
+ useParameterEffects,
+ TranslationKeys,
+ useUpdateSearchParams,
+} from '~/hooks';
import { cn, defaultTextProps, optionText } from '~/utils';
import { ESide, defaultDebouncedDelay } from '~/common';
import { useChatContext } from '~/Providers';
@@ -31,6 +37,7 @@ function DynamicSlider({
() => (!range && options && options.length > 0) ?? false,
[options, range],
);
+ const updateSearchParams = useUpdateSearchParams();
const [setInputValue, inputValue, setLocalValue] = useDebouncedInput({
optionKey: optionType !== OptionTypes.Custom ? settingKey : undefined,
@@ -60,20 +67,26 @@ function DynamicSlider({
const enumToNumeric = useMemo(() => {
if (isEnum && options) {
- return options.reduce((acc, mapping, index) => {
- acc[mapping] = index;
- return acc;
- }, {} as Record);
+ return options.reduce(
+ (acc, mapping, index) => {
+ acc[mapping] = index;
+ return acc;
+ },
+ {} as Record,
+ );
}
return {};
}, [isEnum, options]);
const valueToEnumOption = useMemo(() => {
if (isEnum && options) {
- return options.reduce((acc, option, index) => {
- acc[index] = option;
- return acc;
- }, {} as Record);
+ return options.reduce(
+ (acc, option, index) => {
+ acc[index] = option;
+ return acc;
+ },
+ {} as Record,
+ );
}
return {};
}, [isEnum, options]);
@@ -85,8 +98,10 @@ function DynamicSlider({
} else {
setInputValue(value);
}
+
+ updateSearchParams({ [settingKey]: value.toString() });
},
- [isEnum, setInputValue, valueToEnumOption],
+ [isEnum, setInputValue, valueToEnumOption, updateSearchParams, settingKey],
);
const max = useMemo(() => {
@@ -117,7 +132,7 @@ function DynamicSlider({
htmlFor={`${settingKey}-dynamic-setting`}
className="text-left text-sm font-medium"
>
- {labelCode ? localize(label as TranslationKeys) ?? label : label || settingKey}{' '}
+ {labelCode ? (localize(label as TranslationKeys) ?? label) : label || settingKey}{' '}
{showDefault && (
({localize('com_endpoint_default')}: {defaultValue})
@@ -132,7 +147,7 @@ function DynamicSlider({
onChange={(value) => setInputValue(Number(value))}
max={range ? range.max : (options?.length ?? 0) - 1}
min={range ? range.min : 0}
- step={range ? range.step ?? 1 : 1}
+ step={range ? (range.step ?? 1) : 1}
controls={false}
className={cn(
defaultTextProps,
@@ -164,19 +179,23 @@ function DynamicSlider({
value={[
isEnum
? enumToNumeric[(selectedValue as number) ?? '']
- : (inputValue as number) ?? (defaultValue as number),
+ : ((inputValue as number) ?? (defaultValue as number)),
]}
onValueChange={(value) => handleValueChange(value[0])}
onDoubleClick={() => setInputValue(defaultValue as string | number)}
max={max}
min={range ? range.min : 0}
- step={range ? range.step ?? 1 : 1}
+ step={range ? (range.step ?? 1) : 1}
className="flex h-4 w-full"
/>
{description && (
)}
diff --git a/client/src/components/SidePanel/Parameters/DynamicSwitch.tsx b/client/src/components/SidePanel/Parameters/DynamicSwitch.tsx
index f7a67fe71e..e92fff7a63 100644
--- a/client/src/components/SidePanel/Parameters/DynamicSwitch.tsx
+++ b/client/src/components/SidePanel/Parameters/DynamicSwitch.tsx
@@ -2,7 +2,7 @@ import { useState, useMemo } from 'react';
import { OptionTypes } from 'librechat-data-provider';
import type { DynamicSettingProps } from 'librechat-data-provider';
import { Label, Switch, HoverCard, HoverCardTrigger } from '~/components/ui';
-import { TranslationKeys, useLocalize, useParameterEffects } from '~/hooks';
+import { TranslationKeys, useLocalize, useParameterEffects, useUpdateSearchParams } from '~/hooks';
import { useChatContext } from '~/Providers';
import OptionHover from './OptionHover';
import { ESide } from '~/common';
@@ -23,6 +23,7 @@ function DynamicSwitch({
}: DynamicSettingProps) {
const localize = useLocalize();
const { preset } = useChatContext();
+ const updateSearchParams = useUpdateSearchParams();
const [inputValue, setInputValue] = useState(!!(defaultValue as boolean | undefined));
useParameterEffects({
preset,
@@ -47,6 +48,7 @@ function DynamicSwitch({
if (optionType === OptionTypes.Custom) {
// TODO: custom logic, add to payload but not to conversation
setInputValue(checked);
+ updateSearchParams({ [settingKey]: checked.toString() });
return;
}
setOption(settingKey)(checked);
@@ -65,7 +67,7 @@ function DynamicSwitch({
htmlFor={`${settingKey}-dynamic-switch`}
className="text-left text-sm font-medium"
>
- {labelCode ? localize(label as TranslationKeys) ?? label : label || settingKey}{' '}
+ {labelCode ? (localize(label as TranslationKeys) ?? label) : label || settingKey}{' '}
{showDefault && (
({localize('com_endpoint_default')}:{' '}
@@ -84,7 +86,11 @@ function DynamicSwitch({
{description && (
)}
diff --git a/client/src/components/SidePanel/Parameters/DynamicTags.tsx b/client/src/components/SidePanel/Parameters/DynamicTags.tsx
index 5fb6f43a42..cb6f108a85 100644
--- a/client/src/components/SidePanel/Parameters/DynamicTags.tsx
+++ b/client/src/components/SidePanel/Parameters/DynamicTags.tsx
@@ -3,8 +3,8 @@ import { OptionTypes } from 'librechat-data-provider';
import type { DynamicSettingProps } from 'librechat-data-provider';
import { Label, Input, HoverCard, HoverCardTrigger, Tag } from '~/components/ui';
import { useChatContext, useToastContext } from '~/Providers';
-import { TranslationKeys, useLocalize, useParameterEffects } from '~/hooks';
-import { cn, defaultTextProps } from '~/utils';
+import { TranslationKeys, useLocalize, useParameterEffects, useUpdateSearchParams } from '~/hooks';
+import { cn } from '~/utils';
import OptionHover from './OptionHover';
import { ESide } from '~/common';
@@ -30,6 +30,7 @@ function DynamicTags({
const localize = useLocalize();
const { preset } = useChatContext();
const { showToast } = useToastContext();
+ const updateSearchParams = useUpdateSearchParams();
const inputRef = useRef(null);
const [tagText, setTagText] = useState('');
const [tags, setTags] = useState(
@@ -41,11 +42,13 @@ function DynamicTags({
if (optionType === OptionTypes.Custom) {
// TODO: custom logic, add to payload but not to conversation
setTags(update);
+ updateSearchParams({ [settingKey]: update.join(',') });
return;
}
setOption(settingKey)(update);
+ updateSearchParams({ [settingKey]: update.join(',') });
},
- [optionType, setOption, settingKey],
+ [optionType, setOption, settingKey, updateSearchParams],
);
const onTagClick = useCallback(() => {
@@ -75,7 +78,7 @@ function DynamicTags({
if (minTags != null && currentTags.length <= minTags) {
showToast({
- message: localize('com_ui_min_tags',{ 0: minTags + '' }),
+ message: localize('com_ui_min_tags', { 0: minTags + '' }),
status: 'warning',
});
return;
@@ -126,7 +129,7 @@ function DynamicTags({
htmlFor={`${settingKey}-dynamic-input`}
className="text-left text-sm font-medium"
>
- {labelCode ? localize(label as TranslationKeys) ?? label : label || settingKey}{' '}
+ {labelCode ? (localize(label as TranslationKeys) ?? label) : label || settingKey}{' '}
{showDefault && (
(
@@ -174,7 +177,11 @@ function DynamicTags({
}
}}
onChange={(e) => setTagText(e.target.value)}
- placeholder={placeholderCode ? localize(placeholder as TranslationKeys) ?? placeholder : placeholder}
+ placeholder={
+ placeholderCode
+ ? (localize(placeholder as TranslationKeys) ?? placeholder)
+ : placeholder
+ }
className={cn('flex h-10 max-h-10 border-none bg-surface-secondary px-3 py-2')}
/>
@@ -182,7 +189,11 @@ function DynamicTags({
{description && (
)}
diff --git a/client/src/hooks/Chat/useChatFunctions.ts b/client/src/hooks/Chat/useChatFunctions.ts
index 770c8127d8..b75d97d6a2 100644
--- a/client/src/hooks/Chat/useChatFunctions.ts
+++ b/client/src/hooks/Chat/useChatFunctions.ts
@@ -25,6 +25,7 @@ import store, { useGetEphemeralAgent } from '~/store';
import { getArtifactsMode } from '~/utils/artifacts';
import { getEndpointField, logger } from '~/utils';
import useUserKey from '~/hooks/Input/useUserKey';
+import { useNavigate } from 'react-router-dom';
const logChatRequest = (request: Record) => {
logger.log('=====================================\nAsk function called with:');
@@ -69,6 +70,7 @@ export default function useChatFunctions({
const codeArtifacts = useRecoilValue(store.codeArtifacts);
const includeShadcnui = useRecoilValue(store.includeShadcnui);
const customPromptMode = useRecoilValue(store.customPromptMode);
+ const navigate = useNavigate();
const resetLatestMultiMessage = useResetRecoilState(store.latestMessageFamily(index + 1));
const setShowStopButton = useSetRecoilState(store.showStopButtonByIndex(index));
const setFilesToDelete = useSetFilesToDelete();
@@ -146,6 +148,7 @@ export default function useChatFunctions({
parentMessageId = Constants.NO_PARENT;
currentMessages = [];
conversationId = null;
+ navigate('/c/new');
}
const targetParentMessageId = isRegenerate ? messageId : latestMessage?.parentMessageId;
diff --git a/client/src/hooks/Input/useQueryParams.ts b/client/src/hooks/Input/useQueryParams.ts
index a2f7344f0d..123314ce8c 100644
--- a/client/src/hooks/Input/useQueryParams.ts
+++ b/client/src/hooks/Input/useQueryParams.ts
@@ -73,7 +73,7 @@ export default function useQueryParams({
const attemptsRef = useRef(0);
const processedRef = useRef(false);
const methods = useChatFormContext();
- const [searchParams] = useSearchParams();
+ const [searchParams, setSearchParams] = useSearchParams();
const getDefaultConversation = useDefaultConvo();
const modularChat = useRecoilValue(store.modularChat);
const availableTools = useRecoilValue(store.availableTools);
@@ -222,8 +222,12 @@ export default function useQueryParams({
/** Clean up URL parameters after successful processing */
const success = () => {
- const newUrl = window.location.pathname;
- window.history.replaceState({}, '', newUrl);
+ const currentParams = new URLSearchParams(searchParams.toString());
+ currentParams.delete('prompt');
+ currentParams.delete('q');
+ currentParams.delete('submit');
+
+ setSearchParams(currentParams, { replace: true });
processedRef.current = true;
console.log('Parameters processed successfully');
clearInterval(intervalId);
@@ -254,5 +258,14 @@ export default function useQueryParams({
return () => {
clearInterval(intervalId);
};
- }, [searchParams, methods, textAreaRef, newQueryConvo, newConversation, submitMessage]);
+ }, [
+ searchParams,
+ methods,
+ textAreaRef,
+ newQueryConvo,
+ newConversation,
+ submitMessage,
+ setSearchParams,
+ queryClient,
+ ]);
}
diff --git a/client/src/hooks/SSE/useEventHandlers.ts b/client/src/hooks/SSE/useEventHandlers.ts
index 257331e485..047f3592a5 100644
--- a/client/src/hooks/SSE/useEventHandlers.ts
+++ b/client/src/hooks/SSE/useEventHandlers.ts
@@ -1,7 +1,7 @@
import { v4 } from 'uuid';
import { useCallback, useRef } from 'react';
import { useSetRecoilState } from 'recoil';
-import { useParams } from 'react-router-dom';
+import { useParams, useNavigate, useLocation } from 'react-router-dom';
import { useQueryClient } from '@tanstack/react-query';
import {
QueryKeys,
@@ -172,6 +172,8 @@ export default function useEventHandlers({
const { announcePolite } = useLiveAnnouncer();
const applyAgentTemplate = useApplyNewAgentTemplate();
const setAbortScroll = useSetRecoilState(store.abortScroll);
+ const navigate = useNavigate();
+ const location = useLocation();
const lastAnnouncementTimeRef = useRef(Date.now());
const { conversationId: paramId } = useParams();
@@ -421,6 +423,7 @@ export default function useEventHandlers({
announcePolite,
setConversation,
resetLatestMessage,
+ applyAgentTemplate,
],
);
@@ -476,8 +479,8 @@ export default function useEventHandlers({
}
if (setConversation && isAddedRequest !== true) {
- if (window.location.pathname === '/c/new') {
- window.history.pushState({}, '', '/c/' + conversation.conversationId);
+ if (location.pathname === '/c/new') {
+ navigate(`/c/${conversation.conversationId}`, { replace: true });
}
setConversation((prevState) => {
@@ -502,16 +505,18 @@ export default function useEventHandlers({
setIsSubmitting(false);
},
[
- genTitle,
- queryClient,
- getMessages,
- setMessages,
- setCompleted,
- isAddedRequest,
- announcePolite,
- setConversation,
- setIsSubmitting,
setShowStopButton,
+ setCompleted,
+ getMessages,
+ announcePolite,
+ genTitle,
+ setConversation,
+ isAddedRequest,
+ setIsSubmitting,
+ setMessages,
+ queryClient,
+ location.pathname,
+ navigate,
],
);
@@ -599,7 +604,7 @@ export default function useEventHandlers({
setIsSubmitting(false);
return;
},
- [setMessages, paramId, setIsSubmitting, setCompleted, newConversation],
+ [setCompleted, setMessages, paramId, newConversation, setIsSubmitting, getMessages],
);
const abortConversation = useCallback(
@@ -698,7 +703,15 @@ export default function useEventHandlers({
setIsSubmitting(false);
}
},
- [token, setIsSubmitting, finalHandler, cancelHandler, setMessages, newConversation],
+ [
+ finalHandler,
+ newConversation,
+ setIsSubmitting,
+ token,
+ cancelHandler,
+ getMessages,
+ setMessages,
+ ],
);
return {
diff --git a/client/src/hooks/index.ts b/client/src/hooks/index.ts
index 83cdb7fbc4..9f05f5a2f4 100644
--- a/client/src/hooks/index.ts
+++ b/client/src/hooks/index.ts
@@ -35,3 +35,4 @@ export { default as useOnClickOutside } from './useOnClickOutside';
export { default as useSpeechToText } from './Input/useSpeechToText';
export { default as useTextToSpeech } from './Input/useTextToSpeech';
export { default as useGenerationsByLatest } from './useGenerationsByLatest';
+export { default as useUpdateSearchParams } from './useUpdateSearchParams';
diff --git a/client/src/hooks/useNewConvo.ts b/client/src/hooks/useNewConvo.ts
index f5933cc547..8e1e4f8e5f 100644
--- a/client/src/hooks/useNewConvo.ts
+++ b/client/src/hooks/useNewConvo.ts
@@ -25,6 +25,7 @@ import {
getDefaultModelSpec,
getModelSpecIconURL,
updateLastSelectedModel,
+ createChatSearchParams,
} from '~/utils';
import { useDeleteFilesMutation, useGetEndpointsQuery, useGetStartupConfig } from '~/data-provider';
import useAssistantListMap from './Assistants/useAssistantListMap';
@@ -164,7 +165,13 @@ const useNewConvo = (index = 0) => {
if (appTitle) {
document.title = appTitle;
}
- navigate(`/c/${Constants.NEW_CONVO}`);
+ const params = createChatSearchParams(conversation);
+ const newRoute =
+ params.size > 0
+ ? `/c/${Constants.NEW_CONVO}?${params.toString()}`
+ : `/c/${Constants.NEW_CONVO}`;
+
+ navigate(newRoute);
}
clearTimeout(timeoutIdRef.current);
diff --git a/client/src/hooks/useUpdateSearchParams.ts b/client/src/hooks/useUpdateSearchParams.ts
new file mode 100644
index 0000000000..e3e34bd374
--- /dev/null
+++ b/client/src/hooks/useUpdateSearchParams.ts
@@ -0,0 +1,40 @@
+import { useCallback, useRef } from 'react';
+import { useSearchParams, useLocation } from 'react-router-dom';
+import { createChatSearchParams } from '~/utils';
+
+export const useUpdateSearchParams = () => {
+ const lastParamsRef = useRef>({});
+ const [, setSearchParams] = useSearchParams();
+ const location = useLocation();
+
+ const updateSearchParams = useCallback(
+ (record: Record | null) => {
+ if (record == null || location.pathname !== '/c/new') {
+ return;
+ }
+
+ setSearchParams(
+ (params) => {
+ const currentParams = Object.fromEntries(params.entries());
+ const newSearchParams = createChatSearchParams(record);
+ const newParams = Object.fromEntries(newSearchParams.entries());
+ const mergedParams = { ...currentParams, ...newParams };
+
+ // If the new params are the same as the last params, don't update the search params
+ if (JSON.stringify(lastParamsRef.current) === JSON.stringify(newParams)) {
+ return currentParams;
+ }
+
+ lastParamsRef.current = { ...newParams };
+ return mergedParams;
+ },
+ { replace: true },
+ );
+ },
+ [setSearchParams, location.pathname],
+ );
+
+ return updateSearchParams;
+};
+
+export default useUpdateSearchParams;
diff --git a/client/src/utils/createChatSearchParams.spec.ts b/client/src/utils/createChatSearchParams.spec.ts
new file mode 100644
index 0000000000..19b6812774
--- /dev/null
+++ b/client/src/utils/createChatSearchParams.spec.ts
@@ -0,0 +1,310 @@
+import { EModelEndpoint } from 'librechat-data-provider';
+import type { TConversation, TPreset } from 'librechat-data-provider';
+import createChatSearchParams from './createChatSearchParams';
+
+describe('createChatSearchParams', () => {
+ describe('conversation inputs', () => {
+ it('handles basic conversation properties', () => {
+ const conversation: Partial = {
+ endpoint: EModelEndpoint.openAI,
+ model: 'gpt-4',
+ temperature: 0.7,
+ };
+
+ const result = createChatSearchParams(conversation as TConversation);
+ expect(result.get('endpoint')).toBe(EModelEndpoint.openAI);
+ expect(result.get('model')).toBe('gpt-4');
+ expect(result.get('temperature')).toBe('0.7');
+ });
+
+ it('applies only the endpoint property when other conversation fields are absent', () => {
+ const endpointOnly = createChatSearchParams({
+ endpoint: EModelEndpoint.openAI,
+ } as TConversation);
+ expect(endpointOnly.get('endpoint')).toBe(EModelEndpoint.openAI);
+ expect(endpointOnly.has('model')).toBe(false);
+ expect(endpointOnly.has('endpoint')).toBe(true);
+ });
+
+ it('applies only the model property when other conversation fields are absent', () => {
+ const modelOnly = createChatSearchParams({ model: 'gpt-4' } as TConversation);
+ expect(modelOnly.has('endpoint')).toBe(false);
+ expect(modelOnly.get('model')).toBe('gpt-4');
+ expect(modelOnly.has('model')).toBe(true);
+ });
+
+ it('used to include endpoint and model with assistant_id but now only includes assistant_id', () => {
+ const withAssistantId = createChatSearchParams({
+ endpoint: EModelEndpoint.openAI,
+ model: 'gpt-4',
+ assistant_id: 'asst_123',
+ temperature: 0.7,
+ } as TConversation);
+
+ expect(withAssistantId.get('assistant_id')).toBe('asst_123');
+ expect(withAssistantId.has('endpoint')).toBe(false);
+ expect(withAssistantId.has('model')).toBe(false);
+ expect(withAssistantId.has('temperature')).toBe(false);
+ });
+
+ it('used to include endpoint and model with agent_id but now only includes agent_id', () => {
+ const withAgentId = createChatSearchParams({
+ endpoint: EModelEndpoint.openAI,
+ model: 'gpt-4',
+ agent_id: 'agent_123',
+ temperature: 0.7,
+ } as TConversation);
+
+ expect(withAgentId.get('agent_id')).toBe('agent_123');
+ expect(withAgentId.has('endpoint')).toBe(false);
+ expect(withAgentId.has('model')).toBe(false);
+ expect(withAgentId.has('temperature')).toBe(false);
+ });
+
+ it('when assistant_id is present, only include that param (excluding endpoint, model, etc)', () => {
+ const withAssistantId = createChatSearchParams({
+ endpoint: EModelEndpoint.openAI,
+ model: 'gpt-4',
+ assistant_id: 'asst_123',
+ temperature: 0.7,
+ } as TConversation);
+
+ expect(withAssistantId.get('assistant_id')).toBe('asst_123');
+ expect(withAssistantId.has('endpoint')).toBe(false);
+ expect(withAssistantId.has('model')).toBe(false);
+ expect(withAssistantId.has('temperature')).toBe(false);
+ expect([...withAssistantId.entries()].length).toBe(1);
+ });
+
+ it('when agent_id is present, only include that param (excluding endpoint, model, etc)', () => {
+ const withAgentId = createChatSearchParams({
+ endpoint: EModelEndpoint.openAI,
+ model: 'gpt-4',
+ agent_id: 'agent_123',
+ temperature: 0.7,
+ } as TConversation);
+
+ expect(withAgentId.get('agent_id')).toBe('agent_123');
+ expect(withAgentId.has('endpoint')).toBe(false);
+ expect(withAgentId.has('model')).toBe(false);
+ expect(withAgentId.has('temperature')).toBe(false);
+ expect([...withAgentId.entries()].length).toBe(1);
+ });
+
+ it('handles stop arrays correctly by joining with commas', () => {
+ const withStopArray = createChatSearchParams({
+ endpoint: EModelEndpoint.openAI,
+ model: 'gpt-4',
+ stop: ['stop1', 'stop2'],
+ } as TConversation);
+
+ expect(withStopArray.get('endpoint')).toBe(EModelEndpoint.openAI);
+ expect(withStopArray.get('model')).toBe('gpt-4');
+ expect(withStopArray.get('stop')).toBe('stop1,stop2');
+ });
+
+ it('filters out non-supported array properties', () => {
+ const withOtherArray = createChatSearchParams({
+ endpoint: EModelEndpoint.openAI,
+ model: 'gpt-4',
+ otherArrayProp: ['value1', 'value2'],
+ } as any);
+
+ expect(withOtherArray.get('endpoint')).toBe(EModelEndpoint.openAI);
+ expect(withOtherArray.get('model')).toBe('gpt-4');
+ expect(withOtherArray.has('otherArrayProp')).toBe(false);
+ });
+
+ it('includes empty arrays in output params', () => {
+ const result = createChatSearchParams({
+ endpoint: EModelEndpoint.openAI,
+ stop: [],
+ });
+
+ expect(result.get('endpoint')).toBe(EModelEndpoint.openAI);
+ expect(result.has('stop')).toBe(true);
+ expect(result.get('stop')).toBe('');
+ });
+
+ it('handles non-stop arrays correctly in paramMap', () => {
+ const conversation: any = {
+ endpoint: EModelEndpoint.openAI,
+ model: 'gpt-4',
+ top_p: ['0.7', '0.8'],
+ };
+
+ const result = createChatSearchParams(conversation);
+
+ const expectedJson = JSON.stringify(['0.7', '0.8']);
+ expect(result.get('top_p')).toBe(expectedJson);
+ expect(result.get('endpoint')).toBe(EModelEndpoint.openAI);
+ expect(result.get('model')).toBe('gpt-4');
+ });
+
+ it('includes empty non-stop arrays as serialized empty arrays', () => {
+ const result = createChatSearchParams({
+ endpoint: EModelEndpoint.openAI,
+ model: 'gpt-4',
+ temperature: 0.7,
+ top_p: [],
+ } as any);
+
+ expect(result.get('endpoint')).toBe(EModelEndpoint.openAI);
+ expect(result.get('model')).toBe('gpt-4');
+ expect(result.get('temperature')).toBe('0.7');
+ expect(result.has('top_p')).toBe(true);
+ expect(result.get('top_p')).toBe('[]');
+ });
+
+ it('excludes parameters with null or undefined values from the output', () => {
+ const result = createChatSearchParams({
+ endpoint: EModelEndpoint.openAI,
+ model: 'gpt-4',
+ temperature: 0.7,
+ top_p: undefined,
+ presence_penalty: undefined,
+ frequency_penalty: null,
+ } as any);
+
+ expect(result.get('endpoint')).toBe(EModelEndpoint.openAI);
+ expect(result.get('model')).toBe('gpt-4');
+ expect(result.get('temperature')).toBe('0.7');
+ expect(result.has('top_p')).toBe(false);
+ expect(result.has('presence_penalty')).toBe(false);
+ expect(result.has('frequency_penalty')).toBe(false);
+ expect(result).toBeDefined();
+ });
+
+ it('handles float parameter values correctly', () => {
+ const result = createChatSearchParams({
+ endpoint: EModelEndpoint.google,
+ model: 'gemini-pro',
+ frequency_penalty: 0.25,
+ temperature: 0.75,
+ });
+
+ expect(result.get('endpoint')).toBe(EModelEndpoint.google);
+ expect(result.get('model')).toBe('gemini-pro');
+ expect(result.get('frequency_penalty')).toBe('0.25');
+ expect(result.get('temperature')).toBe('0.75');
+ });
+
+ it('handles integer parameter values correctly', () => {
+ const result = createChatSearchParams({
+ endpoint: EModelEndpoint.google,
+ model: 'gemini-pro',
+ topK: 40,
+ maxOutputTokens: 2048,
+ });
+
+ expect(result.get('endpoint')).toBe(EModelEndpoint.google);
+ expect(result.get('model')).toBe('gemini-pro');
+ expect(result.get('topK')).toBe('40');
+ expect(result.get('maxOutputTokens')).toBe('2048');
+ });
+ });
+
+ describe('preset inputs', () => {
+ it('handles preset objects correctly', () => {
+ const preset: Partial = {
+ endpoint: EModelEndpoint.google,
+ model: 'gemini-pro',
+ temperature: 0.5,
+ topP: 0.8,
+ };
+
+ const result = createChatSearchParams(preset as TPreset);
+ expect(result.get('endpoint')).toBe(EModelEndpoint.google);
+ expect(result.get('model')).toBe('gemini-pro');
+ expect(result.get('temperature')).toBe('0.5');
+ expect(result.get('topP')).toBe('0.8');
+ });
+ });
+
+ describe('record inputs', () => {
+ it('includes allowed parameters from Record inputs', () => {
+ const record: Record = {
+ endpoint: EModelEndpoint.anthropic,
+ model: 'claude-2',
+ temperature: '0.8',
+ top_p: '0.95',
+ extraParam: 'should-not-be-included',
+ invalidParam1: 'value1',
+ invalidParam2: 'value2',
+ };
+
+ const result = createChatSearchParams(record);
+ expect(result.get('endpoint')).toBe(EModelEndpoint.anthropic);
+ expect(result.get('model')).toBe('claude-2');
+ expect(result.get('temperature')).toBe('0.8');
+ expect(result.get('top_p')).toBe('0.95');
+ });
+
+ it('excludes disallowed parameters from Record inputs', () => {
+ const record: Record = {
+ endpoint: EModelEndpoint.anthropic,
+ model: 'claude-2',
+ extraParam: 'should-not-be-included',
+ invalidParam1: 'value1',
+ invalidParam2: 'value2',
+ };
+
+ const result = createChatSearchParams(record);
+ expect(result.has('extraParam')).toBe(false);
+ expect(result.has('invalidParam1')).toBe(false);
+ expect(result.has('invalidParam2')).toBe(false);
+ expect(result.toString().includes('invalidParam')).toBe(false);
+ expect(result.toString().includes('extraParam')).toBe(false);
+ });
+
+ it('includes valid values from Record inputs', () => {
+ const record: Record = {
+ temperature: '0.7',
+ top_p: null,
+ frequency_penalty: undefined,
+ };
+
+ const result = createChatSearchParams(record);
+ expect(result.get('temperature')).toBe('0.7');
+ });
+
+ it('excludes null or undefined values from Record inputs', () => {
+ const record: Record = {
+ temperature: '0.7',
+ top_p: null,
+ frequency_penalty: undefined,
+ };
+
+ const result = createChatSearchParams(record);
+ expect(result.has('top_p')).toBe(false);
+ expect(result.has('frequency_penalty')).toBe(false);
+ });
+
+ it('handles generic object without endpoint or model properties', () => {
+ const customObject = {
+ temperature: '0.5',
+ top_p: '0.7',
+ customProperty: 'value',
+ };
+
+ const result = createChatSearchParams(customObject);
+ expect(result.get('temperature')).toBe('0.5');
+ expect(result.get('top_p')).toBe('0.7');
+ expect(result.has('customProperty')).toBe(false);
+ });
+ });
+
+ describe('edge cases', () => {
+ it('returns an empty URLSearchParams instance when input is null', () => {
+ const result = createChatSearchParams(null);
+ expect(result.toString()).toBe('');
+ expect(result instanceof URLSearchParams).toBe(true);
+ });
+
+ it('returns an empty URLSearchParams instance for an empty object input', () => {
+ const result = createChatSearchParams({});
+ expect(result.toString()).toBe('');
+ expect(result instanceof URLSearchParams).toBe(true);
+ });
+ });
+});
diff --git a/client/src/utils/createChatSearchParams.ts b/client/src/utils/createChatSearchParams.ts
new file mode 100644
index 0000000000..17a5847e12
--- /dev/null
+++ b/client/src/utils/createChatSearchParams.ts
@@ -0,0 +1,85 @@
+import type { TConversation, TPreset } from 'librechat-data-provider';
+
+export default function createChatSearchParams(
+ input: TConversation | TPreset | Record | null,
+): URLSearchParams {
+ if (input == null) {
+ return new URLSearchParams();
+ }
+
+ const params = new URLSearchParams();
+
+ // Define the allowable parameters
+ const allowedParams = [
+ 'endpoint',
+ 'model',
+ 'temperature',
+ 'presence_penalty',
+ 'frequency_penalty',
+ 'stop',
+ 'top_p',
+ 'max_tokens',
+ 'topP',
+ 'topK',
+ 'maxOutputTokens',
+ 'promptCache',
+ 'region',
+ 'maxTokens',
+ 'agent_id',
+ 'assistant_id',
+ ];
+
+ // Handle Record directly
+ if (input && typeof input === 'object' && !('endpoint' in input) && !('model' in input)) {
+ Object.entries(input as Record).forEach(([key, value]) => {
+ if (value != null && allowedParams.includes(key)) {
+ params.set(key, value);
+ }
+ });
+ return params;
+ }
+
+ const conversation = input as TConversation | TPreset;
+
+ // If agent_id or assistant_id are present, they take precedence over all other params
+ if (conversation.agent_id) {
+ return new URLSearchParams({ agent_id: String(conversation.agent_id) });
+ } else if (conversation.assistant_id) {
+ return new URLSearchParams({ assistant_id: String(conversation.assistant_id) });
+ }
+
+ // Otherwise, set regular params
+ if (conversation.endpoint) {
+ params.set('endpoint', conversation.endpoint);
+ }
+ if (conversation.model) {
+ params.set('model', conversation.model);
+ }
+
+ const paramMap = {
+ temperature: conversation.temperature,
+ presence_penalty: conversation.presence_penalty,
+ frequency_penalty: conversation.frequency_penalty,
+ stop: conversation.stop,
+ top_p: conversation.top_p,
+ max_tokens: conversation.max_tokens,
+ topP: conversation.topP,
+ topK: conversation.topK,
+ maxOutputTokens: conversation.maxOutputTokens,
+ promptCache: conversation.promptCache,
+ region: conversation.region,
+ maxTokens: conversation.maxTokens,
+ };
+
+ return Object.entries(paramMap).reduce((params, [key, value]) => {
+ if (value != null) {
+ if (Array.isArray(value)) {
+ params.set(key, key === 'stop' ? value.join(',') : JSON.stringify(value));
+ } else {
+ params.set(key, String(value));
+ }
+ }
+
+ return params;
+ }, params);
+}
diff --git a/client/src/utils/index.ts b/client/src/utils/index.ts
index 1a295837f4..7518aeeb11 100644
--- a/client/src/utils/index.ts
+++ b/client/src/utils/index.ts
@@ -22,6 +22,7 @@ export { default as getLoginError } from './getLoginError';
export { default as cleanupPreset } from './cleanupPreset';
export { default as buildDefaultConvo } from './buildDefaultConvo';
export { default as getDefaultEndpoint } from './getDefaultEndpoint';
+export { default as createChatSearchParams } from './createChatSearchParams';
export const languages = [
'java',