refactor: dynamic form elements using react-hook-form Controllers

This commit is contained in:
Danny Avila 2024-09-03 11:38:55 -04:00
parent fac2acd4cf
commit 2150c4815d
No known key found for this signature in database
GPG key ID: 2DD9CC89B9B50364
10 changed files with 376 additions and 619 deletions

View file

@ -1,63 +1,30 @@
// client/src/components/SidePanel/Parameters/DynamicCheckbox.tsx // client/src/components/SidePanel/Parameters/DynamicCheckbox.tsx
import { useMemo, useState } from 'react'; import React from 'react';
import { OptionTypes } from 'librechat-data-provider'; import { Controller, useFormContext } from 'react-hook-form';
import type { DynamicSettingProps } from 'librechat-data-provider';
import { Label, Checkbox, HoverCard, HoverCardTrigger } from '~/components/ui'; import { Label, Checkbox, HoverCard, HoverCardTrigger } from '~/components/ui';
import { useLocalize, useParameterEffects } from '~/hooks'; import { useLocalize } from '~/hooks';
import { useChatContext } from '~/Providers';
import OptionHover from './OptionHover'; import OptionHover from './OptionHover';
import { ESide } from '~/common'; import { ESide } from '~/common';
import type { DynamicSettingProps } from 'librechat-data-provider';
function DynamicCheckbox({ function DynamicCheckbox({
label, label = '',
settingKey, settingKey,
defaultValue, defaultValue,
description, description = '',
columnSpan, columnSpan,
setOption,
optionType,
readonly = false, readonly = false,
showDefault = true, showDefault = true,
labelCode, labelCode,
descriptionCode, descriptionCode,
conversation,
}: DynamicSettingProps) { }: DynamicSettingProps) {
const localize = useLocalize(); const localize = useLocalize();
const { preset } = useChatContext(); const { control } = useFormContext();
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 ( return (
<div <div
className={`flex flex-col items-center justify-start gap-6 ${ className={`flex flex-col items-center justify-start gap-6 ${
columnSpan ? `col-span-${columnSpan}` : 'col-span-full' columnSpan != null ? `col-span-${columnSpan}` : 'col-span-full'
}`} }`}
> >
<HoverCard openDelay={300}> <HoverCard openDelay={300}>
@ -67,26 +34,35 @@ function DynamicCheckbox({
htmlFor={`${settingKey}-dynamic-checkbox`} htmlFor={`${settingKey}-dynamic-checkbox`}
className="text-left text-sm font-medium" className="text-left text-sm font-medium"
> >
{labelCode ? localize(label ?? '') || label : label ?? settingKey}{' '} {labelCode === true ? localize(label) ?? label : label || settingKey}{' '}
{showDefault && ( {showDefault && (
<small className="opacity-40"> <small className="opacity-40">
({localize('com_endpoint_default')}:{' '} ({localize('com_endpoint_default')}:{' '}
{defaultValue ? localize('com_ui_yes') : localize('com_ui_no')}) {defaultValue != null ? localize('com_ui_yes') : localize('com_ui_no')})
</small> </small>
)} )}
</Label> </Label>
<Controller
name={settingKey}
control={control}
defaultValue={defaultValue as boolean}
render={({ field }) => (
<Checkbox <Checkbox
id={`${settingKey}-dynamic-checkbox`} id={`${settingKey}-dynamic-checkbox`}
disabled={readonly} disabled={readonly}
checked={selectedValue} checked={field.value}
onCheckedChange={handleCheckedChange} onCheckedChange={field.onChange}
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" 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> </div>
</HoverCardTrigger> </HoverCardTrigger>
{description && ( {description && (
<OptionHover <OptionHover
description={descriptionCode ? localize(description) || description : description} description={
descriptionCode === true ? localize(description) ?? description : description
}
side={ESide.Left} side={ESide.Left}
/> />
)} )}

View file

@ -1,60 +1,27 @@
import { useMemo, useState } from 'react'; // client/src/components/SidePanel/Parameters/DynamicDropdown.tsx
import { OptionTypes } from 'librechat-data-provider'; import React from 'react';
import type { DynamicSettingProps } from 'librechat-data-provider'; import { Controller, useFormContext } from 'react-hook-form';
import { Label, HoverCard, HoverCardTrigger, SelectDropDown } from '~/components/ui'; import { Label, HoverCard, HoverCardTrigger, SelectDropDown } from '~/components/ui';
import { useLocalize, useParameterEffects } from '~/hooks'; import { useLocalize } from '~/hooks';
import { useChatContext } from '~/Providers';
import OptionHover from './OptionHover'; import OptionHover from './OptionHover';
import { ESide } from '~/common'; import { ESide } from '~/common';
import { cn } from '~/utils'; import { cn } from '~/utils';
import type { DynamicSettingProps } from 'librechat-data-provider';
function DynamicDropdown({ function DynamicDropdown({
label, label = '',
settingKey, settingKey,
defaultValue, defaultValue,
description, description = '',
columnSpan, columnSpan,
setOption,
optionType,
options, options,
// type: _type,
readonly = false, readonly = false,
showDefault = true, showDefault = true,
labelCode, labelCode,
descriptionCode, descriptionCode,
conversation,
}: DynamicSettingProps) { }: DynamicSettingProps) {
const localize = useLocalize(); const localize = useLocalize();
const { preset } = useChatContext(); const { control } = useFormContext();
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) { if (!options || options.length === 0) {
return null; return null;
@ -64,7 +31,7 @@ function DynamicDropdown({
<div <div
className={cn( className={cn(
'flex flex-col items-center justify-start gap-6', 'flex flex-col items-center justify-start gap-6',
columnSpan ? `col-span-${columnSpan}` : 'col-span-full', columnSpan != null ? `col-span-${columnSpan}` : 'col-span-full',
)} )}
> >
<HoverCard openDelay={300}> <HoverCard openDelay={300}>
@ -74,7 +41,7 @@ function DynamicDropdown({
htmlFor={`${settingKey}-dynamic-dropdown`} htmlFor={`${settingKey}-dynamic-dropdown`}
className="text-left text-sm font-medium" className="text-left text-sm font-medium"
> >
{labelCode ? localize(label ?? '') || label : label ?? settingKey} {labelCode === true ? localize(label) ?? label : label || settingKey}
{showDefault && ( {showDefault && (
<small className="opacity-40"> <small className="opacity-40">
({localize('com_endpoint_default')}: {defaultValue}) ({localize('com_endpoint_default')}: {defaultValue})
@ -82,20 +49,29 @@ function DynamicDropdown({
)} )}
</Label> </Label>
</div> </div>
<Controller
name={settingKey}
control={control}
defaultValue={defaultValue as string}
render={({ field }) => (
<SelectDropDown <SelectDropDown
showLabel={false} showLabel={false}
emptyTitle={true} emptyTitle={true}
disabled={readonly} disabled={readonly}
value={selectedValue} value={field.value}
setValue={handleChange} setValue={field.onChange}
availableValues={options} availableValues={options}
containerClassName="w-full" containerClassName="w-full"
id={`${settingKey}-dynamic-dropdown`} id={`${settingKey}-dynamic-dropdown`}
/> />
)}
/>
</HoverCardTrigger> </HoverCardTrigger>
{description && ( {description && (
<OptionHover <OptionHover
description={descriptionCode ? localize(description) || description : description} description={
descriptionCode === true ? localize(description) ?? description : description
}
side={ESide.Left} side={ESide.Left}
/> />
)} )}

View file

@ -1,55 +1,33 @@
// client/src/components/SidePanel/Parameters/DynamicInput.tsx // client/src/components/SidePanel/Parameters/DynamicInput.tsx
import { OptionTypes } from 'librechat-data-provider'; import React from 'react';
import type { DynamicSettingProps } from 'librechat-data-provider'; import { Controller, useFormContext } from 'react-hook-form';
import { useLocalize, useDebouncedInput, useParameterEffects } from '~/hooks'; import { useLocalize } from '~/hooks';
import { Label, Input, HoverCard, HoverCardTrigger } from '~/components/ui'; import { Label, Input, HoverCard, HoverCardTrigger } from '~/components/ui';
import { cn, defaultTextProps } from '~/utils'; import { cn, defaultTextProps } from '~/utils';
import { useChatContext } from '~/Providers';
import OptionHover from './OptionHover'; import OptionHover from './OptionHover';
import { ESide } from '~/common'; import { ESide } from '~/common';
import type { DynamicSettingProps } from 'librechat-data-provider';
function DynamicInput({ function DynamicInput({
label, label = '',
settingKey, settingKey,
defaultValue, defaultValue,
description, description = '',
columnSpan, columnSpan,
setOption, placeholder = '',
optionType,
placeholder,
readonly = false, readonly = false,
showDefault = true, showDefault = true,
labelCode, labelCode,
descriptionCode, descriptionCode,
placeholderCode, placeholderCode,
conversation,
}: DynamicSettingProps) { }: DynamicSettingProps) {
const localize = useLocalize(); const localize = useLocalize();
const { preset } = useChatContext(); const { control } = useFormContext();
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 ( return (
<div <div
className={`flex flex-col items-center justify-start gap-6 ${ className={`flex flex-col items-center justify-start gap-6 ${
columnSpan ? `col-span-${columnSpan}` : 'col-span-full' columnSpan != null ? `col-span-${columnSpan}` : 'col-span-full'
}`} }`}
> >
<HoverCard openDelay={300}> <HoverCard openDelay={300}>
@ -59,30 +37,40 @@ function DynamicInput({
htmlFor={`${settingKey}-dynamic-input`} htmlFor={`${settingKey}-dynamic-input`}
className="text-left text-sm font-medium" className="text-left text-sm font-medium"
> >
{labelCode ? localize(label ?? '') || label : label ?? settingKey}{' '} {labelCode === true ? localize(label) ?? label : label || settingKey}{' '}
{showDefault && ( {showDefault && (
<small className="opacity-40"> <small className="opacity-40">
( {typeof defaultValue === 'undefined' ||
{typeof defaultValue === 'undefined' || !(defaultValue as string)?.length !((defaultValue as string | undefined)?.length ?? 0)
? localize('com_endpoint_default_blank') ? localize('com_endpoint_default_blank')
: `${localize('com_endpoint_default')}: ${defaultValue}`} : `${localize('com_endpoint_default')}: ${defaultValue}`}
)
</small> </small>
)} )}
</Label> </Label>
</div> </div>
<Controller
name={settingKey}
control={control}
defaultValue={defaultValue as string}
render={({ field }) => (
<Input <Input
id={`${settingKey}-dynamic-input`} id={`${settingKey}-dynamic-input`}
disabled={readonly} disabled={readonly}
value={inputValue ?? ''} value={field.value ?? ''}
onChange={setInputValue} onChange={(e) => field.onChange(e.target.value)}
placeholder={placeholderCode ? localize(placeholder ?? '') || placeholder : placeholder} placeholder={
placeholderCode === true ? localize(placeholder) ?? placeholder : placeholder
}
className={cn(defaultTextProps, 'flex h-10 max-h-10 w-full resize-none px-3 py-2')} className={cn(defaultTextProps, 'flex h-10 max-h-10 w-full resize-none px-3 py-2')}
/> />
)}
/>
</HoverCardTrigger> </HoverCardTrigger>
{description && ( {description && (
<OptionHover <OptionHover
description={descriptionCode ? localize(description) || description : description} description={
descriptionCode === true ? localize(description) ?? description : description
}
side={ESide.Left} side={ESide.Left}
/> />
)} )}

View file

@ -1,59 +1,37 @@
import { OptionTypes } from 'librechat-data-provider'; // client/src/components/SidePanel/Parameters/DynamicInputNumber.tsx
import type { DynamicSettingProps } from 'librechat-data-provider'; import React from 'react';
import type { ValueType } from '@rc-component/mini-decimal'; import { Controller, useFormContext } from 'react-hook-form';
import { Label, HoverCard, InputNumber, HoverCardTrigger } from '~/components/ui'; import { Label, HoverCard, InputNumber, HoverCardTrigger } from '~/components/ui';
import { useLocalize, useDebouncedInput, useParameterEffects } from '~/hooks'; import { useLocalize } from '~/hooks';
import { cn, defaultTextProps, optionText } from '~/utils'; import { cn, defaultTextProps, optionText } from '~/utils';
import { ESide } from '~/common'; import { ESide } from '~/common';
import { useChatContext } from '~/Providers';
import OptionHover from './OptionHover'; import OptionHover from './OptionHover';
import type { DynamicSettingProps } from 'librechat-data-provider';
function DynamicInputNumber({ function DynamicInputNumber({
label, label = '',
settingKey, settingKey,
defaultValue, defaultValue,
description, description = '',
columnSpan, columnSpan,
setOption,
optionType,
readonly = false, readonly = false,
showDefault = true, showDefault = true,
labelCode, labelCode,
descriptionCode, descriptionCode,
placeholderCode, placeholderCode,
placeholder, placeholder = '',
conversation,
range, range,
className = '', className = '',
inputClassName = '', inputClassName = '',
}: DynamicSettingProps) { }: DynamicSettingProps) {
const localize = useLocalize(); const localize = useLocalize();
const { preset } = useChatContext(); const { control } = useFormContext();
const [setInputValue, inputValue] = useDebouncedInput<ValueType | null>({
optionKey: optionType !== OptionTypes.Custom ? settingKey : undefined,
initialValue:
optionType !== OptionTypes.Custom
? (conversation?.[settingKey] as number)
: (defaultValue as number),
setter: () => ({}),
setOption,
});
useParameterEffects({
preset,
settingKey,
defaultValue: typeof defaultValue === 'undefined' ? '' : defaultValue,
conversation,
inputValue,
setInputValue,
});
return ( return (
<div <div
className={cn( className={cn(
'flex flex-col items-center justify-start gap-6', 'flex flex-col items-center justify-start gap-6',
columnSpan ? `col-span-${columnSpan}` : 'col-span-full', columnSpan != null ? `col-span-${columnSpan}` : 'col-span-full',
className, className,
)} )}
> >
@ -64,39 +42,46 @@ function DynamicInputNumber({
htmlFor={`${settingKey}-dynamic-setting`} htmlFor={`${settingKey}-dynamic-setting`}
className="text-left text-sm font-medium" className="text-left text-sm font-medium"
> >
{labelCode ? localize(label ?? '') || label : label ?? settingKey}{' '} {labelCode === true ? localize(label) ?? label : label || settingKey}{' '}
{showDefault && ( {showDefault && (
<small className="opacity-40"> <small className="opacity-40">
({localize('com_endpoint_default')}: {defaultValue}) ({localize('com_endpoint_default')}: {defaultValue})
</small> </small>
)} )}
</Label> </Label>
<Controller
name={settingKey}
control={control}
defaultValue={defaultValue as number}
render={({ field }) => (
<InputNumber <InputNumber
id={`${settingKey}-dynamic-setting-input-number`} id={`${settingKey}-dynamic-setting-input-number`}
disabled={readonly} disabled={readonly}
value={inputValue} value={field.value}
onChange={setInputValue} onChange={(value) => field.onChange(value)}
min={range?.min} min={range?.min}
max={range?.max} max={range?.max}
step={range?.step} step={range?.step}
placeholder={ placeholder={
placeholderCode ? localize(placeholder ?? '') || placeholder : placeholder placeholderCode === true ? localize(placeholder) ?? placeholder : placeholder
} }
controls={false} controls={false}
className={cn( className={cn(
defaultTextProps, defaultTextProps,
cn(
optionText, optionText,
'reset-rc-number-input reset-rc-number-input-text-right h-auto w-12 border-0 group-hover/temp:border-gray-200', 'reset-rc-number-input reset-rc-number-input-text-right h-auto w-12 border-0 group-hover/temp:border-gray-200',
),
inputClassName, inputClassName,
)} )}
/> />
)}
/>
</div> </div>
</HoverCardTrigger> </HoverCardTrigger>
{description && ( {description && (
<OptionHover <OptionHover
description={descriptionCode ? localize(description) || description : description} description={
descriptionCode === true ? localize(description) ?? description : description
}
side={ESide.Left} side={ESide.Left}
/> />
)} )}

View file

@ -1,67 +1,41 @@
import { useMemo, useCallback } from 'react'; // client/src/components/SidePanel/Parameters/DynamicSlider.tsx
import { OptionTypes } from 'librechat-data-provider'; import React, { useMemo } from 'react';
import type { DynamicSettingProps } from 'librechat-data-provider'; import { Controller, useFormContext } from 'react-hook-form';
import { Label, Slider, HoverCard, Input, InputNumber, HoverCardTrigger } from '~/components/ui'; import { Label, Slider, HoverCard, Input, InputNumber, HoverCardTrigger } from '~/components/ui';
import { useLocalize, useDebouncedInput, useParameterEffects } from '~/hooks'; import { useLocalize } from '~/hooks';
import { cn, defaultTextProps, optionText } from '~/utils'; import { cn, defaultTextProps, optionText } from '~/utils';
import { ESide, defaultDebouncedDelay } from '~/common'; import { ESide } from '~/common';
import { useChatContext } from '~/Providers';
import OptionHover from './OptionHover'; import OptionHover from './OptionHover';
import type { DynamicSettingProps } from 'librechat-data-provider';
function DynamicSlider({ function DynamicSlider({
label, label = '',
settingKey, settingKey,
defaultValue, defaultValue,
range, range,
description, description = '',
columnSpan, columnSpan,
setOption,
optionType,
options, options,
readonly = false, readonly = false,
showDefault = true, showDefault = true,
includeInput = true, includeInput = true,
labelCode, labelCode,
descriptionCode, descriptionCode,
conversation,
}: DynamicSettingProps) { }: DynamicSettingProps) {
const localize = useLocalize(); const localize = useLocalize();
const { preset } = useChatContext(); const { control } = useFormContext();
const isEnum = useMemo(() => !range && options && options.length > 0, [options, range]);
const [setInputValue, inputValue] = useDebouncedInput<string | number>({ const isEnum = useMemo(
optionKey: optionType !== OptionTypes.Custom ? settingKey : undefined, () => (!range && options && options.length > 0) ?? false,
initialValue: optionType !== OptionTypes.Custom ? conversation?.[settingKey] : defaultValue, [options, range],
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(() => { const enumToNumeric = useMemo(() => {
if (isEnum && options) { if (isEnum && options) {
return options.reduce((acc, mapping, index) => { return options.reduce((acc, mapping, index) => {
acc[mapping] = index; acc[mapping] = index;
return acc; return acc;
}, {} as Record<string, number>); }, {} as Record<string, number | undefined>);
} }
return {}; return {};
}, [isEnum, options]); }, [isEnum, options]);
@ -76,16 +50,15 @@ function DynamicSlider({
return {}; return {};
}, [isEnum, options]); }, [isEnum, options]);
const handleValueChange = useCallback( const max = useMemo(() => {
(value: number) => { if (isEnum && options) {
if (isEnum) { return options.length - 1;
setInputValue(valueToEnumOption[value]); } else if (range) {
return range.max;
} else { } else {
setInputValue(value); return 0;
} }
}, }, [isEnum, options, range]);
[isEnum, setInputValue, valueToEnumOption],
);
if (!range && !isEnum) { if (!range && !isEnum) {
return null; return null;
@ -95,7 +68,7 @@ function DynamicSlider({
<div <div
className={cn( className={cn(
'flex flex-col items-center justify-start gap-6', 'flex flex-col items-center justify-start gap-6',
columnSpan ? `col-span-${columnSpan}` : 'col-span-full', columnSpan != null ? `col-span-${columnSpan}` : 'col-span-full',
)} )}
> >
<HoverCard openDelay={300}> <HoverCard openDelay={300}>
@ -105,66 +78,72 @@ function DynamicSlider({
htmlFor={`${settingKey}-dynamic-setting`} htmlFor={`${settingKey}-dynamic-setting`}
className="text-left text-sm font-medium" className="text-left text-sm font-medium"
> >
{labelCode ? localize(label ?? '') || label : label ?? settingKey}{' '} {labelCode === true ? localize(label) ?? label : label || settingKey}{' '}
{showDefault && ( {showDefault && (
<small className="opacity-40"> <small className="opacity-40">
({localize('com_endpoint_default')}: {defaultValue}) ({localize('com_endpoint_default')}: {defaultValue})
</small> </small>
)} )}
</Label> </Label>
<Controller
name={settingKey}
control={control}
defaultValue={defaultValue as number | string}
render={({ field }) => (
<>
{includeInput && !isEnum ? ( {includeInput && !isEnum ? (
<InputNumber <InputNumber
id={`${settingKey}-dynamic-setting-input-number`} id={`${settingKey}-dynamic-setting-input-number`}
disabled={readonly} disabled={readonly}
value={inputValue ?? defaultValue} value={field.value as number}
onChange={(value) => setInputValue(Number(value))} onChange={(value) => field.onChange(Number(value))}
max={range ? range.max : (options?.length ?? 0) - 1} max={range ? range.max : (options?.length ?? 0) - 1}
min={range ? range.min : 0} min={range ? range.min : 0}
step={range ? range.step ?? 1 : 1} step={range ? range.step ?? 1 : 1}
controls={false} controls={false}
className={cn( className={cn(
defaultTextProps, defaultTextProps,
cn(
optionText, optionText,
'reset-rc-number-input reset-rc-number-input-text-right h-auto w-12 border-0 group-hover/temp:border-gray-200', 'reset-rc-number-input reset-rc-number-input-text-right h-auto w-12 border-0 group-hover/temp:border-gray-200',
),
)} )}
/> />
) : ( ) : (
<Input <Input
id={`${settingKey}-dynamic-setting-input`} id={`${settingKey}-dynamic-setting-input`}
disabled={readonly} disabled={readonly}
value={selectedValue ?? defaultValue} value={field.value as string}
onChange={() => ({})} onChange={() => ({})}
className={cn( className={cn(
defaultTextProps, defaultTextProps,
cn(
optionText, optionText,
'reset-rc-number-input reset-rc-number-input-text-right h-auto w-12 border-0 group-hover/temp:border-gray-200', 'reset-rc-number-input reset-rc-number-input-text-right h-auto w-12 border-0 group-hover/temp:border-gray-200',
),
)} )}
/> />
)} )}
</div>
<Slider <Slider
id={`${settingKey}-dynamic-setting-slider`} id={`${settingKey}-dynamic-setting-slider`}
disabled={readonly} disabled={readonly}
value={[ value={[
isEnum isEnum ? enumToNumeric[field.value as string] ?? 0 : (field.value as number),
? enumToNumeric[(selectedValue as number) ?? '']
: (inputValue as number) ?? (defaultValue as number),
]} ]}
onValueChange={(value) => handleValueChange(value[0])} onValueChange={(value) =>
doubleClickHandler={() => setInputValue(defaultValue as string | number)} field.onChange(isEnum ? valueToEnumOption[value[0]] : value[0])
max={isEnum && options ? options.length - 1 : range ? range.max : 0} }
max={max}
min={range ? range.min : 0} min={range ? range.min : 0}
step={range ? range.step ?? 1 : 1} step={range ? range.step ?? 1 : 1}
className="flex h-4 w-full" className="flex h-4 w-full"
/> />
</>
)}
/>
</div>
</HoverCardTrigger> </HoverCardTrigger>
{description && ( {description && (
<OptionHover <OptionHover
description={descriptionCode ? localize(description) || description : description} description={
descriptionCode === true ? localize(description) ?? description : description
}
side={ESide.Left} side={ESide.Left}
/> />
)} )}

View file

@ -1,61 +1,30 @@
import { useState, useMemo } from 'react'; // client/src/components/SidePanel/Parameters/DynamicSwitch.tsx
import { OptionTypes } from 'librechat-data-provider'; import React from 'react';
import type { DynamicSettingProps } from 'librechat-data-provider'; import { Controller, useFormContext } from 'react-hook-form';
import { Label, Switch, HoverCard, HoverCardTrigger } from '~/components/ui'; import { Label, Switch, HoverCard, HoverCardTrigger } from '~/components/ui';
import { useLocalize, useParameterEffects } from '~/hooks'; import { useLocalize } from '~/hooks';
import { useChatContext } from '~/Providers';
import OptionHover from './OptionHover'; import OptionHover from './OptionHover';
import { ESide } from '~/common'; import { ESide } from '~/common';
import type { DynamicSettingProps } from 'librechat-data-provider';
function DynamicSwitch({ function DynamicSwitch({
label, label = '',
settingKey, settingKey,
defaultValue, defaultValue,
description, description = '',
columnSpan, columnSpan,
setOption,
optionType,
readonly = false, readonly = false,
showDefault = true, showDefault = true,
labelCode, labelCode,
descriptionCode, descriptionCode,
conversation,
}: DynamicSettingProps) { }: DynamicSettingProps) {
const localize = useLocalize(); const localize = useLocalize();
const { preset } = useChatContext(); const { control } = useFormContext();
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 ( return (
<div <div
className={`flex flex-col items-center justify-start gap-6 ${ className={`flex flex-col items-center justify-start gap-6 ${
columnSpan ? `col-span-${columnSpan}` : 'col-span-full' columnSpan != null ? `col-span-${columnSpan}` : 'col-span-full'
}`} }`}
> >
<HoverCard openDelay={300}> <HoverCard openDelay={300}>
@ -65,25 +34,35 @@ function DynamicSwitch({
htmlFor={`${settingKey}-dynamic-switch`} htmlFor={`${settingKey}-dynamic-switch`}
className="text-left text-sm font-medium" className="text-left text-sm font-medium"
> >
{labelCode ? localize(label ?? '') || label : label ?? settingKey}{' '} {labelCode === true ? localize(label) ?? label : label || settingKey}{' '}
{showDefault && ( {showDefault && (
<small className="opacity-40"> <small className="opacity-40">
({localize('com_endpoint_default')}: {defaultValue ? 'com_ui_on' : 'com_ui_off'}) ({localize('com_endpoint_default')}:{' '}
{defaultValue != null ? localize('com_ui_on') : localize('com_ui_off')})
</small> </small>
)} )}
</Label> </Label>
</div> </div>
<Controller
name={settingKey}
control={control}
defaultValue={defaultValue as boolean}
render={({ field }) => (
<Switch <Switch
id={`${settingKey}-dynamic-switch`} id={`${settingKey}-dynamic-switch`}
checked={selectedValue} checked={field.value}
onCheckedChange={handleCheckedChange} onCheckedChange={field.onChange}
disabled={readonly} disabled={readonly}
className="flex" className="flex"
/> />
)}
/>
</HoverCardTrigger> </HoverCardTrigger>
{description && ( {description && (
<OptionHover <OptionHover
description={descriptionCode ? localize(description) || description : description} description={
descriptionCode === true ? localize(description) ?? description : description
}
side={ESide.Left} side={ESide.Left}
/> />
)} )}

View file

@ -1,53 +1,35 @@
// client/src/components/SidePanel/Parameters/DynamicTags.tsx // client/src/components/SidePanel/Parameters/DynamicTags.tsx
import { useState, useMemo, useCallback, useRef } from 'react'; import React, { useState, useCallback, useRef } from 'react';
import { OptionTypes } from 'librechat-data-provider'; import { Controller, useFormContext } from 'react-hook-form';
import type { DynamicSettingProps } from 'librechat-data-provider';
import { Label, Input, HoverCard, HoverCardTrigger, Tag } from '~/components/ui'; import { Label, Input, HoverCard, HoverCardTrigger, Tag } from '~/components/ui';
import { useChatContext, useToastContext } from '~/Providers'; import { useToastContext } from '~/Providers';
import { useLocalize, useParameterEffects } from '~/hooks'; import { useLocalize } from '~/hooks';
import { cn, defaultTextProps } from '~/utils'; import { cn, defaultTextProps } from '~/utils';
import OptionHover from './OptionHover'; import OptionHover from './OptionHover';
import { ESide } from '~/common'; import { ESide } from '~/common';
import type { DynamicSettingProps } from 'librechat-data-provider';
function DynamicTags({ function DynamicTags({
label, label = '',
settingKey, settingKey,
defaultValue = [], defaultValue = [],
description, description = '',
columnSpan, columnSpan,
setOption, placeholder = '',
optionType,
placeholder,
readonly = false, readonly = false,
showDefault = true, showDefault = true,
labelCode, labelCode,
descriptionCode, descriptionCode,
placeholderCode, placeholderCode,
descriptionSide = ESide.Left, descriptionSide = ESide.Left,
conversation,
minTags, minTags,
maxTags, maxTags,
}: DynamicSettingProps) { }: DynamicSettingProps) {
const localize = useLocalize(); const localize = useLocalize();
const { preset } = useChatContext();
const { showToast } = useToastContext(); const { showToast } = useToastContext();
const { control } = useFormContext();
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
const [tagText, setTagText] = useState<string>(''); const [tagText, setTagText] = useState<string>('');
const [tags, setTags] = useState<string[] | undefined>(
(defaultValue as string[] | undefined) ?? [],
);
const updateState = useCallback(
(update: string[]) => {
if (optionType === OptionTypes.Custom) {
// TODO: custom logic, add to payload but not to conversation
setTags(update);
return;
}
setOption(settingKey)(update);
},
[optionType, setOption, settingKey],
);
const onTagClick = useCallback(() => { const onTagClick = useCallback(() => {
if (inputRef.current) { if (inputRef.current) {
@ -55,69 +37,10 @@ function DynamicTags({
} }
}, [inputRef]); }, [inputRef]);
const currentTags: string[] | undefined = useMemo(() => {
if (optionType === OptionTypes.Custom) {
// TODO: custom logic, add to payload but not to conversation
return tags;
}
if (!conversation?.[settingKey]) {
return defaultValue ?? [];
}
return conversation?.[settingKey];
}, [conversation, defaultValue, optionType, settingKey, tags]);
const onTagRemove = useCallback(
(indexToRemove: number) => {
if (!currentTags) {
return;
}
if (minTags && currentTags.length <= minTags) {
showToast({
message: localize('com_ui_min_tags', minTags + ''),
status: 'warning',
});
return;
}
const update = currentTags.filter((_, index) => index !== indexToRemove);
updateState(update);
},
[localize, minTags, currentTags, showToast, updateState],
);
const onTagAdd = useCallback(() => {
if (!tagText) {
return;
}
let update = [...(currentTags ?? []), tagText];
if (maxTags && update.length > maxTags) {
showToast({
message: localize('com_ui_max_tags', maxTags + ''),
status: 'warning',
});
update = update.slice(-maxTags);
}
updateState(update);
setTagText('');
}, [tagText, currentTags, updateState, maxTags, showToast, localize]);
useParameterEffects({
preset,
settingKey,
defaultValue: typeof defaultValue === 'undefined' ? [] : defaultValue,
inputValue: tags,
setInputValue: setTags,
preventDelayedUpdate: true,
conversation,
});
return ( return (
<div <div
className={`flex flex-col items-center justify-start gap-6 ${ className={`flex flex-col items-center justify-start gap-6 ${
columnSpan ? `col-span-${columnSpan}` : 'col-span-full' columnSpan != null ? `col-span-${columnSpan}` : 'col-span-full'
}`} }`}
> >
<HoverCard openDelay={300}> <HoverCard openDelay={300}>
@ -127,27 +50,41 @@ function DynamicTags({
htmlFor={`${settingKey}-dynamic-input`} htmlFor={`${settingKey}-dynamic-input`}
className="text-left text-sm font-medium" className="text-left text-sm font-medium"
> >
{labelCode ? localize(label ?? '') || label : label ?? settingKey}{' '} {labelCode === true ? localize(label) ?? label : label || settingKey}{' '}
{showDefault && ( {showDefault && (
<small className="opacity-40"> <small className="opacity-40">
( {typeof defaultValue === 'undefined' ||
{typeof defaultValue === 'undefined' || !(defaultValue as string)?.length !((defaultValue as string[] | undefined)?.length ?? 0)
? localize('com_endpoint_default_blank') ? localize('com_endpoint_default_blank')
: `${localize('com_endpoint_default')}: ${defaultValue}`} : `${localize('com_endpoint_default')}: ${(defaultValue as string[]).join(
) ', ',
)}`}
</small> </small>
)} )}
</Label> </Label>
</div> </div>
<Controller
name={settingKey}
control={control}
defaultValue={defaultValue as string[]}
render={({ field }) => (
<div> <div>
<div className="bg-muted mb-2 flex flex-wrap gap-1 break-all rounded-lg"> <div className="bg-muted mb-2 flex flex-wrap gap-1 break-all rounded-lg">
{currentTags?.map((tag: string, index: number) => ( {field.value?.map((tag: string, index: number) => (
<Tag <Tag
key={`${tag}-${index}`} key={`${tag}-${index}`}
label={tag} label={tag}
onClick={onTagClick} onClick={onTagClick}
onRemove={() => { onRemove={() => {
onTagRemove(index); if (minTags != null && field.value.length <= minTags) {
showToast({
message: localize('com_ui_min_tags', minTags + ''),
status: 'warning',
});
return;
}
const newTags = field.value.filter((_, i) => i !== index);
field.onChange(newTags);
if (inputRef.current) { if (inputRef.current) {
inputRef.current.focus(); inputRef.current.focus();
} }
@ -160,28 +97,39 @@ function DynamicTags({
disabled={readonly} disabled={readonly}
value={tagText} value={tagText}
onKeyDown={(e) => { onKeyDown={(e) => {
if (!currentTags) { if (e.key === 'Backspace' && !tagText && field.value.length > 0) {
const newTags = field.value.slice(0, -1);
field.onChange(newTags);
}
if (e.key === 'Enter' && tagText) {
const newTags = [...field.value, tagText];
if (maxTags != null && newTags.length > maxTags) {
showToast({
message: localize('com_ui_max_tags', maxTags + ''),
status: 'warning',
});
return; return;
} }
if (e.key === 'Backspace' && !tagText) { field.onChange(newTags);
onTagRemove(currentTags.length - 1); setTagText('');
}
if (e.key === 'Enter') {
onTagAdd();
} }
}} }}
onChange={(e) => setTagText(e.target.value)} onChange={(e) => setTagText(e.target.value)}
placeholder={ placeholder={
placeholderCode ? localize(placeholder ?? '') || placeholder : placeholder placeholderCode === true ? localize(placeholder) ?? placeholder : placeholder
} }
className={cn(defaultTextProps, 'flex h-10 max-h-10 px-3 py-2')} className={cn(defaultTextProps, 'flex h-10 max-h-10 px-3 py-2')}
/> />
</div> </div>
</div> </div>
)}
/>
</HoverCardTrigger> </HoverCardTrigger>
{description && ( {description && (
<OptionHover <OptionHover
description={descriptionCode ? localize(description) || description : description} description={
descriptionCode === true ? localize(description) ?? description : description
}
side={descriptionSide as ESide} side={descriptionSide as ESide}
/> />
)} )}

View file

@ -1,55 +1,33 @@
// client/src/components/SidePanel/Parameters/DynamicTextarea.tsx // client/src/components/SidePanel/Parameters/DynamicTextarea.tsx
import { OptionTypes } from 'librechat-data-provider'; import React from 'react';
import type { DynamicSettingProps } from 'librechat-data-provider'; import { Controller, useFormContext } from 'react-hook-form';
import { Label, TextareaAutosize, HoverCard, HoverCardTrigger } from '~/components/ui'; import { Label, TextareaAutosize, HoverCard, HoverCardTrigger } from '~/components/ui';
import { useLocalize, useDebouncedInput, useParameterEffects } from '~/hooks'; import { useLocalize } from '~/hooks';
import { cn, defaultTextProps } from '~/utils'; import { cn, defaultTextProps } from '~/utils';
import { useChatContext } from '~/Providers';
import OptionHover from './OptionHover'; import OptionHover from './OptionHover';
import { ESide } from '~/common'; import { ESide } from '~/common';
import type { DynamicSettingProps } from 'librechat-data-provider';
function DynamicTextarea({ function DynamicTextarea({
label, label = '',
settingKey, settingKey,
defaultValue, defaultValue,
description, description = '',
columnSpan, columnSpan,
setOption, placeholder = '',
optionType,
placeholder,
readonly = false, readonly = false,
showDefault = true, showDefault = true,
labelCode, labelCode,
descriptionCode, descriptionCode,
placeholderCode, placeholderCode,
conversation,
}: DynamicSettingProps) { }: DynamicSettingProps) {
const localize = useLocalize(); const localize = useLocalize();
const { preset } = useChatContext(); const { control } = useFormContext();
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 ( return (
<div <div
className={`flex flex-col items-center justify-start gap-6 ${ className={`flex flex-col items-center justify-start gap-6 ${
columnSpan ? `col-span-${columnSpan}` : 'col-span-full' columnSpan != null ? `col-span-${columnSpan}` : 'col-span-full'
}`} }`}
> >
<HoverCard openDelay={300}> <HoverCard openDelay={300}>
@ -59,34 +37,43 @@ function DynamicTextarea({
htmlFor={`${settingKey}-dynamic-textarea`} htmlFor={`${settingKey}-dynamic-textarea`}
className="text-left text-sm font-medium" className="text-left text-sm font-medium"
> >
{labelCode ? localize(label ?? '') || label : label ?? settingKey}{' '} {labelCode === true ? localize(label) ?? label : label || settingKey}{' '}
{showDefault && ( {showDefault && (
<small className="opacity-40"> <small className="opacity-40">
( {typeof defaultValue === 'undefined' ||
{typeof defaultValue === 'undefined' || !(defaultValue as string)?.length !((defaultValue as string | undefined)?.length ?? 0)
? localize('com_endpoint_default_blank') ? localize('com_endpoint_default_blank')
: `${localize('com_endpoint_default')}: ${defaultValue}`} : `${localize('com_endpoint_default')}: ${defaultValue}`}
)
</small> </small>
)} )}
</Label> </Label>
</div> </div>
<Controller
name={settingKey}
control={control}
defaultValue={defaultValue as string}
render={({ field }) => (
<TextareaAutosize <TextareaAutosize
id={`${settingKey}-dynamic-textarea`} id={`${settingKey}-dynamic-textarea`}
disabled={readonly} disabled={readonly}
value={inputValue ?? ''} value={field.value ?? ''}
onChange={setInputValue} onChange={(e) => field.onChange(e.target.value)}
placeholder={placeholderCode ? localize(placeholder ?? '') || placeholder : placeholder} placeholder={
placeholderCode === true ? localize(placeholder) ?? placeholder : placeholder
}
className={cn( className={cn(
defaultTextProps, defaultTextProps,
// TODO: configurable max height
'flex max-h-[138px] min-h-[100px] w-full resize-none px-3 py-2', 'flex max-h-[138px] min-h-[100px] w-full resize-none px-3 py-2',
)} )}
/> />
)}
/>
</HoverCardTrigger> </HoverCardTrigger>
{description && ( {description && (
<OptionHover <OptionHover
description={descriptionCode ? localize(description) || description : description} description={
descriptionCode === true ? localize(description) ?? description : description
}
side={ESide.Left} side={ESide.Left}
/> />
)} )}

View file

@ -1,11 +1,7 @@
import React from 'react';
import { useForm, FormProvider } from 'react-hook-form';
import { ComponentTypes } from 'librechat-data-provider'; import { ComponentTypes } from 'librechat-data-provider';
import type { import type { DynamicSettingProps, SettingsConfiguration } from 'librechat-data-provider';
DynamicSettingProps,
SettingDefinition,
SettingsConfiguration,
} from 'librechat-data-provider';
import { useSetIndexOptions } from '~/hooks';
import { useChatContext } from '~/Providers';
import { import {
DynamicDropdown, DynamicDropdown,
DynamicCheckbox, DynamicCheckbox,
@ -162,92 +158,37 @@ const componentMapping: Record<ComponentTypes, React.ComponentType<DynamicSettin
}; };
export default function Parameters() { export default function Parameters() {
const { conversation } = useChatContext(); const methods = useForm({
const { setOption } = useSetIndexOptions(); defaultValues: settingsConfiguration.reduce((acc, setting) => {
acc[setting.key] = setting.default;
return acc;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
}, {} as Record<string, any>),
});
const temperature = settingsConfiguration.find( // eslint-disable-next-line @typescript-eslint/no-explicit-any
(setting) => setting.key === 'temperature', const onSubmit = (data: Record<string, any>) => {
) as SettingDefinition; console.log('Form data:', data);
const TempComponent = componentMapping[temperature.component]; // Here you can handle the form submission, e.g., send the data to an API
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;
const stop = settingsConfiguration.find((setting) => setting.key === 'stop') as SettingDefinition;
const Tags = componentMapping[stop.component];
const { key: stopKey, default: stopDefault, ...stopSettings } = stop;
return ( return (
<FormProvider {...methods}>
<form onSubmit={methods.handleSubmit(onSubmit)}>
<div className="h-auto max-w-full overflow-x-hidden p-3"> <div className="h-auto max-w-full overflow-x-hidden p-3">
<div className="grid grid-cols-4 gap-6"> <div className="grid grid-cols-4 gap-6">
{' '} {settingsConfiguration.map((setting) => {
{/* This is the parent element containing all settings */} const Component = componentMapping[setting.component];
{/* Below is an example of an applied dynamic setting, each be contained by a div with the column span specified */} const { key, default: defaultValue, ...rest } = setting;
<Input
settingKey={inputKey} return <Component key={key} settingKey={key} defaultValue={defaultValue} {...rest} />;
defaultValue={inputDefault} })}
{...inputSettings}
setOption={setOption}
conversation={conversation}
/>
<Textarea
settingKey={textareaKey}
defaultValue={textareaDefault}
{...textareaSettings}
setOption={setOption}
conversation={conversation}
/>
<TempComponent
settingKey={temp}
defaultValue={tempDefault}
{...tempSettings}
setOption={setOption}
conversation={conversation}
/>
<Switch
settingKey={switchKey}
defaultValue={switchDefault}
{...switchSettings}
setOption={setOption}
conversation={conversation}
/>
<DetailComponent
settingKey={detail}
defaultValue={detailDefault}
{...detailSettings}
setOption={setOption}
conversation={conversation}
/>
<Tags
settingKey={stopKey}
defaultValue={stopDefault}
{...stopSettings}
setOption={setOption}
conversation={conversation}
/>
</div> </div>
</div> </div>
<button type="submit" className="mt-4 rounded bg-blue-500 px-4 py-2 text-white">
Submit
</button>
</form>
</FormProvider>
); );
} }

View file

@ -69,8 +69,6 @@ export interface SettingDefinition {
export type DynamicSettingProps = Partial<SettingDefinition> & { export type DynamicSettingProps = Partial<SettingDefinition> & {
readonly?: boolean; readonly?: boolean;
settingKey: string; settingKey: string;
setOption: TSetOption;
conversation: TConversation | TPreset | null;
defaultValue?: number | boolean | string | string[]; defaultValue?: number | boolean | string | string[];
className?: string; className?: string;
inputClassName?: string; inputClassName?: string;