mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-02-17 16:08:10 +01:00
📦 feat: Model & Assistants Combobox for Side Panel (#2380)
* WIP: dynamic settings * WIP: update tests and validations * refactor(SidePanel): use hook for Links * WIP: dynamic settings, slider implemented * feat(useDebouncedInput): dynamic typing with generic * refactor(generate): add `custom` optionType to be non-conforming to conversation schema * feat: DynamicDropdown * refactor(DynamicSlider): custom optionType handling and useEffect for conversation updates elsewhere * refactor(Panel): add more test cases * chore(DynamicSlider): note * refactor(useDebouncedInput): import defaultDebouncedDelay from ~/common` * WIP: implement remaining ComponentTypes * chore: add com_sidepanel_parameters * refactor: add langCode handling for dynamic settings * chore(useOriginNavigate): change path to '/c/' * refactor: explicit textarea focus on new convo, share textarea idea via ~/common * refactor: useParameterEffects: reset if convo or preset Ids change, share and maintain statefulness in side panel * wip: combobox * chore: minor styling for Select components * wip: combobox select styling for side panel * feat: complete combobox * refactor: model select for side panel switcher * refactor(Combobox): add portal * chore: comment out dynamic parameters panel for future PR and delete prompt files * refactor(Combobox): add icon field for options, change hover bg-color, add displayValue * fix(useNewConvo): proper textarea focus with setTimeout * refactor(AssistantSwitcher): use Combobox * refactor(ModelSwitcher): add textarea focus on model switch
This commit is contained in:
parent
f64a2cb0b0
commit
8e5f1ad575
33 changed files with 2850 additions and 462 deletions
|
|
@ -0,0 +1,97 @@
|
|||
// client/src/components/SidePanel/Parameters/DynamicCheckbox.tsx
|
||||
import { useMemo, useState } from 'react';
|
||||
import { OptionTypes } from 'librechat-data-provider';
|
||||
import type { DynamicSettingProps } from 'librechat-data-provider';
|
||||
import { Label, Checkbox, HoverCard, HoverCardTrigger } from '~/components/ui';
|
||||
import { useLocalize, useParameterEffects } from '~/hooks';
|
||||
import { useChatContext } from '~/Providers';
|
||||
import OptionHover from './OptionHover';
|
||||
import { ESide } from '~/common';
|
||||
|
||||
function DynamicCheckbox({
|
||||
label,
|
||||
settingKey,
|
||||
defaultValue,
|
||||
description,
|
||||
columnSpan,
|
||||
setOption,
|
||||
optionType,
|
||||
readonly = false,
|
||||
showDefault = true,
|
||||
labelCode,
|
||||
descriptionCode,
|
||||
}: DynamicSettingProps) {
|
||||
const localize = useLocalize();
|
||||
const { conversation = { conversationId: null }, preset } = useChatContext();
|
||||
const [inputValue, setInputValue] = useState<boolean>(!!(defaultValue as boolean | undefined));
|
||||
|
||||
const selectedValue = useMemo(() => {
|
||||
if (optionType === OptionTypes.Custom) {
|
||||
// TODO: custom logic, add to payload but not to conversation
|
||||
return inputValue;
|
||||
}
|
||||
|
||||
return conversation?.[settingKey] ?? defaultValue;
|
||||
}, [conversation, defaultValue, optionType, settingKey, inputValue]);
|
||||
|
||||
const handleCheckedChange = (checked: boolean) => {
|
||||
if (optionType === OptionTypes.Custom) {
|
||||
// TODO: custom logic, add to payload but not to conversation
|
||||
setInputValue(checked);
|
||||
return;
|
||||
}
|
||||
setOption(settingKey)(checked);
|
||||
};
|
||||
|
||||
useParameterEffects({
|
||||
preset,
|
||||
settingKey,
|
||||
defaultValue,
|
||||
conversation,
|
||||
inputValue,
|
||||
setInputValue,
|
||||
preventDelayedUpdate: true,
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex flex-col items-center justify-start gap-6 ${
|
||||
columnSpan ? `col-span-${columnSpan}` : 'col-span-full'
|
||||
}`}
|
||||
>
|
||||
<HoverCard openDelay={300}>
|
||||
<HoverCardTrigger className="grid w-full items-center">
|
||||
<div className="flex justify-start gap-4">
|
||||
<Label
|
||||
htmlFor={`${settingKey}-dynamic-checkbox`}
|
||||
className="text-left text-sm font-medium"
|
||||
>
|
||||
{labelCode ? localize(label ?? '') || label : label ?? settingKey}{' '}
|
||||
{showDefault && (
|
||||
<small className="opacity-40">
|
||||
({localize('com_endpoint_default')}:{' '}
|
||||
{defaultValue ? localize('com_ui_yes') : localize('com_ui_no')})
|
||||
</small>
|
||||
)}
|
||||
</Label>
|
||||
<Checkbox
|
||||
id={`${settingKey}-dynamic-checkbox`}
|
||||
disabled={readonly}
|
||||
checked={selectedValue}
|
||||
onCheckedChange={handleCheckedChange}
|
||||
className="mt-[2px] focus:ring-opacity-20 dark:border-gray-500 dark:bg-gray-700 dark:text-gray-50 dark:focus:ring-gray-600 dark:focus:ring-opacity-50 dark:focus:ring-offset-0"
|
||||
/>
|
||||
</div>
|
||||
</HoverCardTrigger>
|
||||
{description && (
|
||||
<OptionHover
|
||||
description={descriptionCode ? localize(description) || description : description}
|
||||
side={ESide.Left}
|
||||
/>
|
||||
)}
|
||||
</HoverCard>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default DynamicCheckbox;
|
||||
106
client/src/components/SidePanel/Parameters/DynamicDropdown.tsx
Normal file
106
client/src/components/SidePanel/Parameters/DynamicDropdown.tsx
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
import { useMemo, useState } from 'react';
|
||||
import { OptionTypes } from 'librechat-data-provider';
|
||||
import type { DynamicSettingProps } from 'librechat-data-provider';
|
||||
import { Label, HoverCard, HoverCardTrigger, SelectDropDown } from '~/components/ui';
|
||||
import { useLocalize, useParameterEffects } from '~/hooks';
|
||||
import { useChatContext } from '~/Providers';
|
||||
import OptionHover from './OptionHover';
|
||||
import { ESide } from '~/common';
|
||||
import { cn } from '~/utils';
|
||||
|
||||
function DynamicDropdown({
|
||||
label,
|
||||
settingKey,
|
||||
defaultValue,
|
||||
description,
|
||||
columnSpan,
|
||||
setOption,
|
||||
optionType,
|
||||
options,
|
||||
// type: _type,
|
||||
readonly = false,
|
||||
showDefault = true,
|
||||
labelCode,
|
||||
descriptionCode,
|
||||
}: DynamicSettingProps) {
|
||||
const localize = useLocalize();
|
||||
const { conversation = { conversationId: null }, preset } = useChatContext();
|
||||
const [inputValue, setInputValue] = useState<string | null>(null);
|
||||
|
||||
const selectedValue = useMemo(() => {
|
||||
if (optionType === OptionTypes.Custom) {
|
||||
// TODO: custom logic, add to payload but not to conversation
|
||||
return inputValue;
|
||||
}
|
||||
|
||||
return conversation?.[settingKey] ?? defaultValue;
|
||||
}, [conversation, defaultValue, optionType, settingKey, inputValue]);
|
||||
|
||||
const handleChange = (value: string) => {
|
||||
if (optionType === OptionTypes.Custom) {
|
||||
// TODO: custom logic, add to payload but not to conversation
|
||||
setInputValue(value);
|
||||
return;
|
||||
}
|
||||
setOption(settingKey)(value);
|
||||
};
|
||||
|
||||
useParameterEffects({
|
||||
preset,
|
||||
settingKey,
|
||||
defaultValue,
|
||||
conversation,
|
||||
inputValue,
|
||||
setInputValue,
|
||||
preventDelayedUpdate: true,
|
||||
});
|
||||
|
||||
if (!options || options.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col items-center justify-start gap-6',
|
||||
columnSpan ? `col-span-${columnSpan}` : 'col-span-full',
|
||||
)}
|
||||
>
|
||||
<HoverCard openDelay={300}>
|
||||
<HoverCardTrigger className="grid w-full items-center gap-2">
|
||||
<div className="flex w-full justify-between">
|
||||
<Label
|
||||
htmlFor={`${settingKey}-dynamic-dropdown`}
|
||||
className="text-left text-sm font-medium"
|
||||
>
|
||||
{labelCode ? localize(label ?? '') || label : label ?? settingKey}
|
||||
{showDefault && (
|
||||
<small className="opacity-40">
|
||||
({localize('com_endpoint_default')}: {defaultValue})
|
||||
</small>
|
||||
)}
|
||||
</Label>
|
||||
</div>
|
||||
<SelectDropDown
|
||||
showLabel={false}
|
||||
emptyTitle={true}
|
||||
disabled={readonly}
|
||||
value={selectedValue}
|
||||
setValue={handleChange}
|
||||
availableValues={options}
|
||||
containerClassName="w-full"
|
||||
id={`${settingKey}-dynamic-dropdown`}
|
||||
/>
|
||||
</HoverCardTrigger>
|
||||
{description && (
|
||||
<OptionHover
|
||||
description={descriptionCode ? localize(description) || description : description}
|
||||
side={ESide.Left}
|
||||
/>
|
||||
)}
|
||||
</HoverCard>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default DynamicDropdown;
|
||||
93
client/src/components/SidePanel/Parameters/DynamicInput.tsx
Normal file
93
client/src/components/SidePanel/Parameters/DynamicInput.tsx
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
// client/src/components/SidePanel/Parameters/DynamicInput.tsx
|
||||
import { OptionTypes } from 'librechat-data-provider';
|
||||
import type { DynamicSettingProps } from 'librechat-data-provider';
|
||||
import { useLocalize, useDebouncedInput, useParameterEffects } from '~/hooks';
|
||||
import { Label, Input, HoverCard, HoverCardTrigger } from '~/components/ui';
|
||||
import { cn, defaultTextProps } from '~/utils';
|
||||
import { useChatContext } from '~/Providers';
|
||||
import OptionHover from './OptionHover';
|
||||
import { ESide } from '~/common';
|
||||
|
||||
function DynamicInput({
|
||||
label,
|
||||
settingKey,
|
||||
defaultValue,
|
||||
description,
|
||||
columnSpan,
|
||||
setOption,
|
||||
optionType,
|
||||
placeholder,
|
||||
readonly = false,
|
||||
showDefault = true,
|
||||
labelCode,
|
||||
descriptionCode,
|
||||
placeholderCode,
|
||||
}: DynamicSettingProps) {
|
||||
const localize = useLocalize();
|
||||
const { conversation = { conversationId: null }, preset } = useChatContext();
|
||||
|
||||
const [setInputValue, inputValue] = useDebouncedInput<string | null>({
|
||||
optionKey: optionType !== OptionTypes.Custom ? settingKey : undefined,
|
||||
initialValue:
|
||||
optionType !== OptionTypes.Custom
|
||||
? (conversation?.[settingKey] as string)
|
||||
: (defaultValue as string),
|
||||
setter: () => ({}),
|
||||
setOption,
|
||||
});
|
||||
|
||||
useParameterEffects({
|
||||
preset,
|
||||
settingKey,
|
||||
defaultValue: typeof defaultValue === 'undefined' ? '' : defaultValue,
|
||||
conversation,
|
||||
inputValue,
|
||||
setInputValue,
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex flex-col items-center justify-start gap-6 ${
|
||||
columnSpan ? `col-span-${columnSpan}` : 'col-span-full'
|
||||
}`}
|
||||
>
|
||||
<HoverCard openDelay={300}>
|
||||
<HoverCardTrigger className="grid w-full items-center gap-2">
|
||||
<div className="flex w-full justify-between">
|
||||
<Label
|
||||
htmlFor={`${settingKey}-dynamic-input`}
|
||||
className="text-left text-sm font-medium"
|
||||
>
|
||||
{labelCode ? localize(label ?? '') || label : label ?? settingKey}{' '}
|
||||
{showDefault && (
|
||||
<small className="opacity-40">
|
||||
(
|
||||
{typeof defaultValue === 'undefined' || !(defaultValue as string)?.length
|
||||
? localize('com_endpoint_default_blank')
|
||||
: `${localize('com_endpoint_default')}: ${defaultValue}`}
|
||||
)
|
||||
</small>
|
||||
)}
|
||||
</Label>
|
||||
</div>
|
||||
<Input
|
||||
id={`${settingKey}-dynamic-input`}
|
||||
disabled={readonly}
|
||||
value={inputValue ?? ''}
|
||||
onChange={setInputValue}
|
||||
placeholder={placeholderCode ? localize(placeholder ?? '') || placeholder : placeholder}
|
||||
className={cn(defaultTextProps, 'flex h-10 max-h-10 w-full resize-none px-3 py-2')}
|
||||
/>
|
||||
</HoverCardTrigger>
|
||||
{description && (
|
||||
<OptionHover
|
||||
description={descriptionCode ? localize(description) || description : description}
|
||||
side={ESide.Left}
|
||||
/>
|
||||
)}
|
||||
</HoverCard>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default DynamicInput;
|
||||
175
client/src/components/SidePanel/Parameters/DynamicSlider.tsx
Normal file
175
client/src/components/SidePanel/Parameters/DynamicSlider.tsx
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
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 } from '~/hooks';
|
||||
import { cn, defaultTextProps, optionText } from '~/utils';
|
||||
import { ESide, defaultDebouncedDelay } from '~/common';
|
||||
import { useChatContext } from '~/Providers';
|
||||
import OptionHover from './OptionHover';
|
||||
|
||||
function DynamicSlider({
|
||||
label,
|
||||
settingKey,
|
||||
defaultValue,
|
||||
range,
|
||||
description,
|
||||
columnSpan,
|
||||
setOption,
|
||||
optionType,
|
||||
options,
|
||||
readonly = false,
|
||||
showDefault = true,
|
||||
includeInput = true,
|
||||
labelCode,
|
||||
descriptionCode,
|
||||
}: DynamicSettingProps) {
|
||||
const localize = useLocalize();
|
||||
const { conversation = { conversationId: null }, preset } = useChatContext();
|
||||
const isEnum = useMemo(() => !range && options && options.length > 0, [options, range]);
|
||||
|
||||
const [setInputValue, inputValue] = useDebouncedInput<string | number>({
|
||||
optionKey: optionType !== OptionTypes.Custom ? settingKey : undefined,
|
||||
initialValue: optionType !== OptionTypes.Custom ? conversation?.[settingKey] : defaultValue,
|
||||
setter: () => ({}),
|
||||
setOption,
|
||||
delay: isEnum ? 0 : defaultDebouncedDelay,
|
||||
});
|
||||
|
||||
useParameterEffects({
|
||||
preset,
|
||||
settingKey,
|
||||
defaultValue,
|
||||
conversation,
|
||||
inputValue,
|
||||
setInputValue,
|
||||
preventDelayedUpdate: isEnum,
|
||||
});
|
||||
|
||||
const selectedValue = useMemo(() => {
|
||||
if (isEnum) {
|
||||
return conversation?.[settingKey] ?? defaultValue;
|
||||
}
|
||||
// TODO: custom logic, add to payload but not to conversation
|
||||
|
||||
return inputValue;
|
||||
}, [conversation, defaultValue, settingKey, inputValue, isEnum]);
|
||||
|
||||
const enumToNumeric = useMemo(() => {
|
||||
if (isEnum && options) {
|
||||
return options.reduce((acc, mapping, index) => {
|
||||
acc[mapping] = index;
|
||||
return acc;
|
||||
}, {} as Record<string, number>);
|
||||
}
|
||||
return {};
|
||||
}, [isEnum, options]);
|
||||
|
||||
const valueToEnumOption = useMemo(() => {
|
||||
if (isEnum && options) {
|
||||
return options.reduce((acc, option, index) => {
|
||||
acc[index] = option;
|
||||
return acc;
|
||||
}, {} as Record<number, string>);
|
||||
}
|
||||
return {};
|
||||
}, [isEnum, options]);
|
||||
|
||||
const handleValueChange = useCallback(
|
||||
(value: number) => {
|
||||
if (isEnum) {
|
||||
setInputValue(valueToEnumOption[value]);
|
||||
} else {
|
||||
setInputValue(value);
|
||||
}
|
||||
},
|
||||
[isEnum, setInputValue, valueToEnumOption],
|
||||
);
|
||||
|
||||
if (!range && !isEnum) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col items-center justify-start gap-6',
|
||||
columnSpan ? `col-span-${columnSpan}` : 'col-span-full',
|
||||
)}
|
||||
>
|
||||
<HoverCard openDelay={300}>
|
||||
<HoverCardTrigger className="grid w-full items-center gap-2">
|
||||
<div className="flex justify-between">
|
||||
<Label
|
||||
htmlFor={`${settingKey}-dynamic-setting`}
|
||||
className="text-left text-sm font-medium"
|
||||
>
|
||||
{labelCode ? localize(label ?? '') || label : label ?? settingKey}{' '}
|
||||
{showDefault && (
|
||||
<small className="opacity-40">
|
||||
({localize('com_endpoint_default')}: {defaultValue})
|
||||
</small>
|
||||
)}
|
||||
</Label>
|
||||
{includeInput && !isEnum ? (
|
||||
<InputNumber
|
||||
id={`${settingKey}-dynamic-setting-input-number`}
|
||||
disabled={readonly}
|
||||
value={inputValue ?? defaultValue}
|
||||
onChange={(value) => setInputValue(Number(value))}
|
||||
max={range ? range.max : (options?.length ?? 0) - 1}
|
||||
min={range ? range.min : 0}
|
||||
step={range ? range.step ?? 1 : 1}
|
||||
controls={false}
|
||||
className={cn(
|
||||
defaultTextProps,
|
||||
cn(
|
||||
optionText,
|
||||
'reset-rc-number-input reset-rc-number-input-text-right h-auto w-12 border-0 group-hover/temp:border-gray-200',
|
||||
),
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<Input
|
||||
id={`${settingKey}-dynamic-setting-input`}
|
||||
disabled={readonly}
|
||||
value={selectedValue ?? defaultValue}
|
||||
onChange={() => ({})}
|
||||
className={cn(
|
||||
defaultTextProps,
|
||||
cn(
|
||||
optionText,
|
||||
'reset-rc-number-input reset-rc-number-input-text-right h-auto w-12 border-0 group-hover/temp:border-gray-200',
|
||||
),
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<Slider
|
||||
id={`${settingKey}-dynamic-setting-slider`}
|
||||
disabled={readonly}
|
||||
value={[
|
||||
isEnum
|
||||
? enumToNumeric[(selectedValue as number) ?? '']
|
||||
: (inputValue as number) ?? (defaultValue as number),
|
||||
]}
|
||||
onValueChange={(value) => handleValueChange(value[0])}
|
||||
doubleClickHandler={() => setInputValue(defaultValue as string | number)}
|
||||
max={isEnum && options ? options.length - 1 : range ? range.max : 0}
|
||||
min={range ? range.min : 0}
|
||||
step={range ? range.step ?? 1 : 1}
|
||||
className="flex h-4 w-full"
|
||||
/>
|
||||
</HoverCardTrigger>
|
||||
{description && (
|
||||
<OptionHover
|
||||
description={descriptionCode ? localize(description) || description : description}
|
||||
side={ESide.Left}
|
||||
/>
|
||||
)}
|
||||
</HoverCard>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default DynamicSlider;
|
||||
94
client/src/components/SidePanel/Parameters/DynamicSwitch.tsx
Normal file
94
client/src/components/SidePanel/Parameters/DynamicSwitch.tsx
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
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 { useLocalize, useParameterEffects } from '~/hooks';
|
||||
import { useChatContext } from '~/Providers';
|
||||
import OptionHover from './OptionHover';
|
||||
import { ESide } from '~/common';
|
||||
|
||||
function DynamicSwitch({
|
||||
label,
|
||||
settingKey,
|
||||
defaultValue,
|
||||
description,
|
||||
columnSpan,
|
||||
setOption,
|
||||
optionType,
|
||||
readonly = false,
|
||||
showDefault = true,
|
||||
labelCode,
|
||||
descriptionCode,
|
||||
}: DynamicSettingProps) {
|
||||
const localize = useLocalize();
|
||||
const { conversation = { conversationId: null }, preset } = useChatContext();
|
||||
const [inputValue, setInputValue] = useState<boolean>(!!(defaultValue as boolean | undefined));
|
||||
useParameterEffects({
|
||||
preset,
|
||||
settingKey,
|
||||
defaultValue,
|
||||
conversation,
|
||||
inputValue,
|
||||
setInputValue,
|
||||
preventDelayedUpdate: true,
|
||||
});
|
||||
|
||||
const selectedValue = useMemo(() => {
|
||||
if (optionType === OptionTypes.Custom) {
|
||||
// TODO: custom logic, add to payload but not to conversation
|
||||
return inputValue;
|
||||
}
|
||||
|
||||
return conversation?.[settingKey] ?? defaultValue;
|
||||
}, [conversation, defaultValue, optionType, settingKey, inputValue]);
|
||||
|
||||
const handleCheckedChange = (checked: boolean) => {
|
||||
if (optionType === OptionTypes.Custom) {
|
||||
// TODO: custom logic, add to payload but not to conversation
|
||||
setInputValue(checked);
|
||||
return;
|
||||
}
|
||||
setOption(settingKey)(checked);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex flex-col items-center justify-start gap-6 ${
|
||||
columnSpan ? `col-span-${columnSpan}` : 'col-span-full'
|
||||
}`}
|
||||
>
|
||||
<HoverCard openDelay={300}>
|
||||
<HoverCardTrigger className="grid w-full items-center gap-2">
|
||||
<div className="flex justify-between">
|
||||
<Label
|
||||
htmlFor={`${settingKey}-dynamic-switch`}
|
||||
className="text-left text-sm font-medium"
|
||||
>
|
||||
{labelCode ? localize(label ?? '') || label : label ?? settingKey}{' '}
|
||||
{showDefault && (
|
||||
<small className="opacity-40">
|
||||
({localize('com_endpoint_default')}: {defaultValue ? 'com_ui_on' : 'com_ui_off'})
|
||||
</small>
|
||||
)}
|
||||
</Label>
|
||||
</div>
|
||||
<Switch
|
||||
id={`${settingKey}-dynamic-switch`}
|
||||
checked={selectedValue}
|
||||
onCheckedChange={handleCheckedChange}
|
||||
disabled={readonly}
|
||||
className="flex"
|
||||
/>
|
||||
</HoverCardTrigger>
|
||||
{description && (
|
||||
<OptionHover
|
||||
description={descriptionCode ? localize(description) || description : description}
|
||||
side={ESide.Left}
|
||||
/>
|
||||
)}
|
||||
</HoverCard>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default DynamicSwitch;
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
// client/src/components/SidePanel/Parameters/DynamicTextarea.tsx
|
||||
import { OptionTypes } from 'librechat-data-provider';
|
||||
import type { DynamicSettingProps } from 'librechat-data-provider';
|
||||
import { Label, TextareaAutosize, HoverCard, HoverCardTrigger } from '~/components/ui';
|
||||
import { useLocalize, useDebouncedInput, useParameterEffects } from '~/hooks';
|
||||
import { cn, defaultTextProps } from '~/utils';
|
||||
import { useChatContext } from '~/Providers';
|
||||
import OptionHover from './OptionHover';
|
||||
import { ESide } from '~/common';
|
||||
|
||||
function DynamicTextarea({
|
||||
label,
|
||||
settingKey,
|
||||
defaultValue,
|
||||
description,
|
||||
columnSpan,
|
||||
setOption,
|
||||
optionType,
|
||||
placeholder,
|
||||
readonly = false,
|
||||
showDefault = true,
|
||||
labelCode,
|
||||
descriptionCode,
|
||||
placeholderCode,
|
||||
}: DynamicSettingProps) {
|
||||
const localize = useLocalize();
|
||||
const { conversation = { conversationId: null }, preset } = useChatContext();
|
||||
|
||||
const [setInputValue, inputValue] = useDebouncedInput<string | null>({
|
||||
optionKey: optionType !== OptionTypes.Custom ? settingKey : undefined,
|
||||
initialValue:
|
||||
optionType !== OptionTypes.Custom
|
||||
? (conversation?.[settingKey] as string)
|
||||
: (defaultValue as string),
|
||||
setter: () => ({}),
|
||||
setOption,
|
||||
});
|
||||
|
||||
useParameterEffects({
|
||||
preset,
|
||||
settingKey,
|
||||
defaultValue: typeof defaultValue === 'undefined' ? '' : defaultValue,
|
||||
conversation,
|
||||
inputValue,
|
||||
setInputValue,
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex flex-col items-center justify-start gap-6 ${
|
||||
columnSpan ? `col-span-${columnSpan}` : 'col-span-full'
|
||||
}`}
|
||||
>
|
||||
<HoverCard openDelay={300}>
|
||||
<HoverCardTrigger className="grid w-full items-center gap-2">
|
||||
<div className="flex w-full justify-between">
|
||||
<Label
|
||||
htmlFor={`${settingKey}-dynamic-textarea`}
|
||||
className="text-left text-sm font-medium"
|
||||
>
|
||||
{labelCode ? localize(label ?? '') || label : label ?? settingKey}{' '}
|
||||
{showDefault && (
|
||||
<small className="opacity-40">
|
||||
(
|
||||
{typeof defaultValue === 'undefined' || !(defaultValue as string)?.length
|
||||
? localize('com_endpoint_default_blank')
|
||||
: `${localize('com_endpoint_default')}: ${defaultValue}`}
|
||||
)
|
||||
</small>
|
||||
)}
|
||||
</Label>
|
||||
</div>
|
||||
<TextareaAutosize
|
||||
id={`${settingKey}-dynamic-textarea`}
|
||||
disabled={readonly}
|
||||
value={inputValue ?? ''}
|
||||
onChange={setInputValue}
|
||||
placeholder={placeholderCode ? localize(placeholder ?? '') || placeholder : placeholder}
|
||||
className={cn(
|
||||
defaultTextProps,
|
||||
// TODO: configurable max height
|
||||
'flex max-h-[138px] min-h-[100px] w-full resize-none px-3 py-2',
|
||||
)}
|
||||
/>
|
||||
</HoverCardTrigger>
|
||||
{description && (
|
||||
<OptionHover
|
||||
description={descriptionCode ? localize(description) || description : description}
|
||||
side={ESide.Left}
|
||||
/>
|
||||
)}
|
||||
</HoverCard>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default DynamicTextarea;
|
||||
26
client/src/components/SidePanel/Parameters/OptionHover.tsx
Normal file
26
client/src/components/SidePanel/Parameters/OptionHover.tsx
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import React from 'react';
|
||||
import { HoverCardPortal, HoverCardContent } from '~/components/ui';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import { ESide } from '~/common';
|
||||
|
||||
type TOptionHoverProps = {
|
||||
description: string;
|
||||
langCode?: boolean;
|
||||
side: ESide;
|
||||
};
|
||||
|
||||
function OptionHover({ side, description, langCode }: TOptionHoverProps) {
|
||||
const localize = useLocalize();
|
||||
const text = langCode ? localize(description) : description;
|
||||
return (
|
||||
<HoverCardPortal>
|
||||
<HoverCardContent side={side} className="z-[999] w-80 dark:bg-gray-700" sideOffset={30}>
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300">{text}</p>
|
||||
</div>
|
||||
</HoverCardContent>
|
||||
</HoverCardPortal>
|
||||
);
|
||||
}
|
||||
|
||||
export default OptionHover;
|
||||
215
client/src/components/SidePanel/Parameters/Panel.tsx
Normal file
215
client/src/components/SidePanel/Parameters/Panel.tsx
Normal file
|
|
@ -0,0 +1,215 @@
|
|||
import { ComponentTypes } from 'librechat-data-provider';
|
||||
import type {
|
||||
DynamicSettingProps,
|
||||
SettingDefinition,
|
||||
SettingsConfiguration,
|
||||
} from 'librechat-data-provider';
|
||||
import { useSetIndexOptions } from '~/hooks';
|
||||
import DynamicDropdown from './DynamicDropdown';
|
||||
import DynamicCheckbox from './DynamicCheckbox';
|
||||
import DynamicTextarea from './DynamicTextarea';
|
||||
import DynamicSlider from './DynamicSlider';
|
||||
import DynamicSwitch from './DynamicSwitch';
|
||||
import DynamicInput from './DynamicInput';
|
||||
|
||||
const settingsConfiguration: SettingsConfiguration = [
|
||||
{
|
||||
key: 'temperature',
|
||||
label: 'com_endpoint_temperature',
|
||||
labelCode: true,
|
||||
description: 'com_endpoint_openai_temp',
|
||||
descriptionCode: true,
|
||||
type: 'number',
|
||||
default: 1,
|
||||
range: {
|
||||
min: 0,
|
||||
max: 2,
|
||||
step: 0.01,
|
||||
},
|
||||
component: 'slider',
|
||||
optionType: 'model',
|
||||
// columnSpan: 2,
|
||||
// includeInput: false,
|
||||
},
|
||||
{
|
||||
key: 'top_p',
|
||||
label: 'com_endpoint_top_p',
|
||||
labelCode: true,
|
||||
description: 'com_endpoint_openai_topp',
|
||||
descriptionCode: true,
|
||||
type: 'number',
|
||||
default: 1,
|
||||
range: {
|
||||
min: 0,
|
||||
max: 1,
|
||||
step: 0.01,
|
||||
},
|
||||
component: 'slider',
|
||||
optionType: 'model',
|
||||
},
|
||||
{
|
||||
key: 'presence_penalty',
|
||||
label: 'com_endpoint_presence_penalty',
|
||||
labelCode: true,
|
||||
description: 'com_endpoint_openai_pres',
|
||||
descriptionCode: true,
|
||||
type: 'number',
|
||||
default: 0,
|
||||
range: {
|
||||
min: -2,
|
||||
max: 2,
|
||||
step: 0.01,
|
||||
},
|
||||
component: 'slider',
|
||||
optionType: 'model',
|
||||
},
|
||||
{
|
||||
key: 'frequency_penalty',
|
||||
label: 'com_endpoint_frequency_penalty',
|
||||
labelCode: true,
|
||||
description: 'com_endpoint_openai_freq',
|
||||
descriptionCode: true,
|
||||
type: 'number',
|
||||
default: 0,
|
||||
range: {
|
||||
min: -2,
|
||||
max: 2,
|
||||
step: 0.01,
|
||||
},
|
||||
component: 'slider',
|
||||
optionType: 'model',
|
||||
},
|
||||
{
|
||||
key: 'chatGptLabel',
|
||||
label: 'com_endpoint_custom_name',
|
||||
labelCode: true,
|
||||
type: 'string',
|
||||
default: '',
|
||||
component: 'input',
|
||||
placeholder: 'com_endpoint_openai_custom_name_placeholder',
|
||||
placeholderCode: true,
|
||||
optionType: 'conversation',
|
||||
},
|
||||
{
|
||||
key: 'promptPrefix',
|
||||
label: 'com_endpoint_prompt_prefix',
|
||||
labelCode: true,
|
||||
type: 'string',
|
||||
default: '',
|
||||
component: 'textarea',
|
||||
placeholder: 'com_endpoint_openai_prompt_prefix_placeholder',
|
||||
placeholderCode: true,
|
||||
optionType: 'conversation',
|
||||
// columnSpan: 2,
|
||||
},
|
||||
{
|
||||
key: 'resendFiles',
|
||||
label: 'com_endpoint_plug_resend_files',
|
||||
labelCode: true,
|
||||
description: 'com_endpoint_openai_resend_files',
|
||||
descriptionCode: true,
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
component: 'switch',
|
||||
optionType: 'conversation',
|
||||
showDefault: false,
|
||||
columnSpan: 2,
|
||||
},
|
||||
{
|
||||
key: 'imageDetail',
|
||||
label: 'com_endpoint_plug_image_detail',
|
||||
labelCode: true,
|
||||
description: 'com_endpoint_openai_detail',
|
||||
descriptionCode: true,
|
||||
type: 'enum',
|
||||
default: 'auto',
|
||||
options: ['low', 'auto', 'high'],
|
||||
optionType: 'conversation',
|
||||
component: 'slider',
|
||||
showDefault: false,
|
||||
columnSpan: 2,
|
||||
},
|
||||
];
|
||||
|
||||
const componentMapping: Record<ComponentTypes, React.ComponentType<DynamicSettingProps>> = {
|
||||
[ComponentTypes.Slider]: DynamicSlider,
|
||||
[ComponentTypes.Dropdown]: DynamicDropdown,
|
||||
[ComponentTypes.Switch]: DynamicSwitch,
|
||||
[ComponentTypes.Textarea]: DynamicTextarea,
|
||||
[ComponentTypes.Input]: DynamicInput,
|
||||
[ComponentTypes.Checkbox]: DynamicCheckbox,
|
||||
};
|
||||
|
||||
export default function Parameters() {
|
||||
const { setOption } = useSetIndexOptions();
|
||||
|
||||
const temperature = settingsConfiguration.find(
|
||||
(setting) => setting.key === 'temperature',
|
||||
) as SettingDefinition;
|
||||
const TempComponent = componentMapping[temperature.component];
|
||||
const { key: temp, default: tempDefault, ...tempSettings } = temperature;
|
||||
|
||||
const imageDetail = settingsConfiguration.find(
|
||||
(setting) => setting.key === 'imageDetail',
|
||||
) as SettingDefinition;
|
||||
const DetailComponent = componentMapping[imageDetail.component];
|
||||
const { key: detail, default: detailDefault, ...detailSettings } = imageDetail;
|
||||
|
||||
const resendFiles = settingsConfiguration.find(
|
||||
(setting) => setting.key === 'resendFiles',
|
||||
) as SettingDefinition;
|
||||
const Switch = componentMapping[resendFiles.component];
|
||||
const { key: switchKey, default: switchDefault, ...switchSettings } = resendFiles;
|
||||
|
||||
const promptPrefix = settingsConfiguration.find(
|
||||
(setting) => setting.key === 'promptPrefix',
|
||||
) as SettingDefinition;
|
||||
const Textarea = componentMapping[promptPrefix.component];
|
||||
const { key: textareaKey, default: textareaDefault, ...textareaSettings } = promptPrefix;
|
||||
|
||||
const chatGptLabel = settingsConfiguration.find(
|
||||
(setting) => setting.key === 'chatGptLabel',
|
||||
) as SettingDefinition;
|
||||
const Input = componentMapping[chatGptLabel.component];
|
||||
const { key: inputKey, default: inputDefault, ...inputSettings } = chatGptLabel;
|
||||
|
||||
return (
|
||||
<div className="h-auto max-w-full overflow-x-hidden p-3">
|
||||
<div className="grid grid-cols-4 gap-6">
|
||||
{' '}
|
||||
{/* This is the parent element containing all settings */}
|
||||
{/* Below is an example of an applied dynamic setting, each be contained by a div with the column span specified */}
|
||||
<Input
|
||||
settingKey={inputKey}
|
||||
defaultValue={inputDefault}
|
||||
{...inputSettings}
|
||||
setOption={setOption}
|
||||
/>
|
||||
<Textarea
|
||||
settingKey={textareaKey}
|
||||
defaultValue={textareaDefault}
|
||||
{...textareaSettings}
|
||||
setOption={setOption}
|
||||
/>
|
||||
<TempComponent
|
||||
settingKey={temp}
|
||||
defaultValue={tempDefault}
|
||||
{...tempSettings}
|
||||
setOption={setOption}
|
||||
/>
|
||||
<Switch
|
||||
settingKey={switchKey}
|
||||
defaultValue={switchDefault}
|
||||
{...switchSettings}
|
||||
setOption={setOption}
|
||||
/>
|
||||
<DetailComponent
|
||||
settingKey={detail}
|
||||
defaultValue={detailDefault}
|
||||
{...detailSettings}
|
||||
setOption={setOption}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue