mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-19 09:50:15 +01:00
feat: implement search parameter updates
This commit is contained in:
parent
550c7cc68a
commit
4ed22aaa59
13 changed files with 567 additions and 40 deletions
|
|
@ -4,8 +4,8 @@ import { useNavigate } from 'react-router-dom';
|
|||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { QueryKeys, Constants } from 'librechat-data-provider';
|
||||
import type { TMessage, TStartupConfig } from 'librechat-data-provider';
|
||||
import { createChatSearchParams, getDefaultModelSpec, getModelSpecPreset } from '~/utils';
|
||||
import { NewChatIcon, MobileSidebar, Sidebar } from '~/components/svg';
|
||||
import { getDefaultModelSpec, getModelSpecPreset } from '~/utils';
|
||||
import { TooltipAnchor, Button } from '~/components/ui';
|
||||
import { useLocalize, useNewConvo } from '~/hooks';
|
||||
import store from '~/store';
|
||||
|
|
@ -28,6 +28,7 @@ export default function NewChat({
|
|||
const { newConversation: newConvo } = useNewConvo(index);
|
||||
const navigate = useNavigate();
|
||||
const localize = useLocalize();
|
||||
const defaultPreset = useRecoilValue(store.defaultPreset);
|
||||
const { conversation } = store.useCreateConversationAtom(index);
|
||||
|
||||
const clickHandler: React.MouseEventHandler<HTMLButtonElement> = useCallback(
|
||||
|
|
@ -40,13 +41,18 @@ export default function NewChat({
|
|||
[QueryKeys.messages, conversation?.conversationId ?? Constants.NEW_CONVO],
|
||||
[],
|
||||
);
|
||||
const startupConfig = queryClient.getQueryData<TStartupConfig>([QueryKeys.startupConfig]);
|
||||
const defaultSpec = getDefaultModelSpec(startupConfig);
|
||||
const preset = defaultSpec != null ? getModelSpecPreset(defaultSpec) : defaultPreset;
|
||||
const params = createChatSearchParams(preset ?? conversation);
|
||||
const newRoute = params.size > 0 ? `/c/new?${params.toString()}` : '/c/new';
|
||||
newConvo();
|
||||
navigate('/c/new');
|
||||
navigate(newRoute);
|
||||
if (isSmallScreen) {
|
||||
toggleNav();
|
||||
}
|
||||
},
|
||||
[queryClient, conversation, newConvo, navigate, toggleNav, isSmallScreen],
|
||||
[queryClient, conversation, newConvo, navigate, toggleNav, defaultPreset, isSmallScreen],
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -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<string | null>(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 && (
|
||||
<small className="opacity-40">
|
||||
({localize('com_endpoint_default')}: {defaultValue})
|
||||
|
|
@ -105,10 +108,14 @@ function DynamicCombobox({
|
|||
<ControlCombobox
|
||||
displayValue={selectedValue}
|
||||
selectPlaceholder={
|
||||
selectPlaceholderCode === true ? localize(selectPlaceholder as TranslationKeys) : selectPlaceholder
|
||||
selectPlaceholderCode === true
|
||||
? localize(selectPlaceholder as TranslationKeys)
|
||||
: selectPlaceholder
|
||||
}
|
||||
searchPlaceholder={
|
||||
searchPlaceholderCode === true ? localize(searchPlaceholder as TranslationKeys) : searchPlaceholder
|
||||
searchPlaceholderCode === true
|
||||
? localize(searchPlaceholder as TranslationKeys)
|
||||
: searchPlaceholder
|
||||
}
|
||||
isCollapsed={isCollapsed}
|
||||
ariaLabel={settingKey}
|
||||
|
|
@ -120,7 +127,11 @@ function DynamicCombobox({
|
|||
</HoverCardTrigger>
|
||||
{description && (
|
||||
<OptionHover
|
||||
description={descriptionCode ? localize(description as TranslationKeys) ?? description : description}
|
||||
description={
|
||||
descriptionCode
|
||||
? (localize(description as TranslationKeys) ?? description)
|
||||
: description
|
||||
}
|
||||
side={ESide.Left}
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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<string | number>({
|
||||
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 (
|
||||
|
|
|
|||
|
|
@ -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<string | number>({
|
||||
optionKey: optionType !== OptionTypes.Custom ? settingKey : undefined,
|
||||
|
|
@ -60,20 +67,26 @@ function DynamicSlider({
|
|||
|
||||
const enumToNumeric = useMemo(() => {
|
||||
if (isEnum && options) {
|
||||
return options.reduce((acc, mapping, index) => {
|
||||
return options.reduce(
|
||||
(acc, mapping, index) => {
|
||||
acc[mapping] = index;
|
||||
return acc;
|
||||
}, {} as Record<string, number>);
|
||||
},
|
||||
{} as Record<string, number>,
|
||||
);
|
||||
}
|
||||
return {};
|
||||
}, [isEnum, options]);
|
||||
|
||||
const valueToEnumOption = useMemo(() => {
|
||||
if (isEnum && options) {
|
||||
return options.reduce((acc, option, index) => {
|
||||
return options.reduce(
|
||||
(acc, option, index) => {
|
||||
acc[index] = option;
|
||||
return acc;
|
||||
}, {} as Record<number, string>);
|
||||
},
|
||||
{} as Record<number, string>,
|
||||
);
|
||||
}
|
||||
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 && (
|
||||
<small className="opacity-40">
|
||||
({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"
|
||||
/>
|
||||
</HoverCardTrigger>
|
||||
{description && (
|
||||
<OptionHover
|
||||
description={descriptionCode ? localize(description as TranslationKeys) ?? description : description}
|
||||
description={
|
||||
descriptionCode
|
||||
? (localize(description as TranslationKeys) ?? description)
|
||||
: description
|
||||
}
|
||||
side={ESide.Left}
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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<boolean>(!!(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 && (
|
||||
<small className="opacity-40">
|
||||
({localize('com_endpoint_default')}:{' '}
|
||||
|
|
@ -84,7 +86,11 @@ function DynamicSwitch({
|
|||
</HoverCardTrigger>
|
||||
{description && (
|
||||
<OptionHover
|
||||
description={descriptionCode ? localize(description as TranslationKeys) ?? description : description}
|
||||
description={
|
||||
descriptionCode
|
||||
? (localize(description as TranslationKeys) ?? description)
|
||||
: description
|
||||
}
|
||||
side={ESide.Left}
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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<HTMLInputElement>(null);
|
||||
const [tagText, setTagText] = useState<string>('');
|
||||
const [tags, setTags] = useState<string[] | undefined>(
|
||||
|
|
@ -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 && (
|
||||
<small className="opacity-40">
|
||||
(
|
||||
|
|
@ -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')}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -182,7 +189,11 @@ function DynamicTags({
|
|||
</HoverCardTrigger>
|
||||
{description && (
|
||||
<OptionHover
|
||||
description={descriptionCode ? localize(description as TranslationKeys) ?? description : description}
|
||||
description={
|
||||
descriptionCode
|
||||
? (localize(description as TranslationKeys) ?? description)
|
||||
: description
|
||||
}
|
||||
side={descriptionSide as ESide}
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import {
|
|||
getModelSpecPreset,
|
||||
getDefaultModelSpec,
|
||||
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);
|
||||
|
|
|
|||
40
client/src/hooks/useUpdateSearchParams.ts
Normal file
40
client/src/hooks/useUpdateSearchParams.ts
Normal file
|
|
@ -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<Record<string, string>>({});
|
||||
const [, setSearchParams] = useSearchParams();
|
||||
const location = useLocation();
|
||||
|
||||
const updateSearchParams = useCallback(
|
||||
(record: Record<string, string> | 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;
|
||||
310
client/src/utils/createChatSearchParams.spec.ts
Normal file
310
client/src/utils/createChatSearchParams.spec.ts
Normal file
|
|
@ -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<TConversation> = {
|
||||
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<TPreset> = {
|
||||
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<string, any> = {
|
||||
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<string, any> = {
|
||||
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<string, any> = {
|
||||
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<string, any> = {
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
93
client/src/utils/createChatSearchParams.ts
Normal file
93
client/src/utils/createChatSearchParams.ts
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
import { isAgentsEndpoint, isAssistantsEndpoint, Constants } from 'librechat-data-provider';
|
||||
import type { TConversation, TPreset } from 'librechat-data-provider';
|
||||
|
||||
export default function createChatSearchParams(
|
||||
input: TConversation | TPreset | Record<string, string> | null,
|
||||
): URLSearchParams {
|
||||
if (input == null) {
|
||||
return new URLSearchParams();
|
||||
}
|
||||
|
||||
const params = new URLSearchParams();
|
||||
|
||||
const allowedParams = [
|
||||
'endpoint',
|
||||
'model',
|
||||
'temperature',
|
||||
'presence_penalty',
|
||||
'frequency_penalty',
|
||||
'stop',
|
||||
'top_p',
|
||||
'max_tokens',
|
||||
'topP',
|
||||
'topK',
|
||||
'maxOutputTokens',
|
||||
'promptCache',
|
||||
'region',
|
||||
'maxTokens',
|
||||
'agent_id',
|
||||
'assistant_id',
|
||||
];
|
||||
|
||||
if (input && typeof input === 'object' && !('endpoint' in input) && !('model' in input)) {
|
||||
Object.entries(input as Record<string, string>).forEach(([key, value]) => {
|
||||
if (value != null && allowedParams.includes(key)) {
|
||||
params.set(key, value);
|
||||
}
|
||||
});
|
||||
return params;
|
||||
}
|
||||
|
||||
const conversation = input as TConversation | TPreset;
|
||||
const endpoint = conversation.endpoint;
|
||||
if (conversation.spec) {
|
||||
return new URLSearchParams({ spec: conversation.spec });
|
||||
}
|
||||
if (
|
||||
isAgentsEndpoint(endpoint) &&
|
||||
conversation.agent_id &&
|
||||
conversation.agent_id !== Constants.EPHEMERAL_AGENT_ID
|
||||
) {
|
||||
return new URLSearchParams({ agent_id: String(conversation.agent_id) });
|
||||
} else if (isAssistantsEndpoint(endpoint) && conversation.assistant_id) {
|
||||
return new URLSearchParams({ assistant_id: String(conversation.assistant_id) });
|
||||
} else if (isAgentsEndpoint(endpoint) && !conversation.agent_id) {
|
||||
return params;
|
||||
} else if (isAssistantsEndpoint(endpoint) && !conversation.assistant_id) {
|
||||
return params;
|
||||
}
|
||||
|
||||
if (endpoint) {
|
||||
params.set('endpoint', 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);
|
||||
}
|
||||
|
|
@ -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',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue