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

View file

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

View file

@ -1,55 +1,33 @@
// 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 React from 'react';
import { Controller, useFormContext } from 'react-hook-form';
import { useLocalize } 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';
import type { DynamicSettingProps } from 'librechat-data-provider';
function DynamicInput({
label,
label = '',
settingKey,
defaultValue,
description,
description = '',
columnSpan,
setOption,
optionType,
placeholder,
placeholder = '',
readonly = false,
showDefault = true,
labelCode,
descriptionCode,
placeholderCode,
conversation,
}: DynamicSettingProps) {
const localize = useLocalize();
const { 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,
});
const { control } = useFormContext();
return (
<div
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}>
@ -59,30 +37,40 @@ function DynamicInput({
htmlFor={`${settingKey}-dynamic-input`}
className="text-left text-sm font-medium"
>
{labelCode ? localize(label ?? '') || label : label ?? settingKey}{' '}
{labelCode === true ? localize(label) ?? label : label || settingKey}{' '}
{showDefault && (
<small className="opacity-40">
(
{typeof defaultValue === 'undefined' || !(defaultValue as string)?.length
{typeof defaultValue === 'undefined' ||
!((defaultValue as string | undefined)?.length ?? 0)
? 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')}
<Controller
name={settingKey}
control={control}
defaultValue={defaultValue as string}
render={({ field }) => (
<Input
id={`${settingKey}-dynamic-input`}
disabled={readonly}
value={field.value ?? ''}
onChange={(e) => field.onChange(e.target.value)}
placeholder={
placeholderCode === true ? 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}
description={
descriptionCode === true ? localize(description) ?? description : description
}
side={ESide.Left}
/>
)}

View file

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

View file

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

View file

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

View file

@ -1,53 +1,35 @@
// client/src/components/SidePanel/Parameters/DynamicTags.tsx
import { useState, useMemo, useCallback, useRef } from 'react';
import { OptionTypes } from 'librechat-data-provider';
import type { DynamicSettingProps } from 'librechat-data-provider';
import React, { useState, useCallback, useRef } from 'react';
import { Controller, useFormContext } from 'react-hook-form';
import { Label, Input, HoverCard, HoverCardTrigger, Tag } from '~/components/ui';
import { useChatContext, useToastContext } from '~/Providers';
import { useLocalize, useParameterEffects } from '~/hooks';
import { useToastContext } from '~/Providers';
import { useLocalize } from '~/hooks';
import { cn, defaultTextProps } from '~/utils';
import OptionHover from './OptionHover';
import { ESide } from '~/common';
import type { DynamicSettingProps } from 'librechat-data-provider';
function DynamicTags({
label,
label = '',
settingKey,
defaultValue = [],
description,
description = '',
columnSpan,
setOption,
optionType,
placeholder,
placeholder = '',
readonly = false,
showDefault = true,
labelCode,
descriptionCode,
placeholderCode,
descriptionSide = ESide.Left,
conversation,
minTags,
maxTags,
}: DynamicSettingProps) {
const localize = useLocalize();
const { preset } = useChatContext();
const { showToast } = useToastContext();
const { control } = useFormContext();
const inputRef = useRef<HTMLInputElement>(null);
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(() => {
if (inputRef.current) {
@ -55,69 +37,10 @@ function DynamicTags({
}
}, [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 (
<div
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}>
@ -127,61 +50,86 @@ function DynamicTags({
htmlFor={`${settingKey}-dynamic-input`}
className="text-left text-sm font-medium"
>
{labelCode ? localize(label ?? '') || label : label ?? settingKey}{' '}
{labelCode === true ? localize(label) ?? label : label || settingKey}{' '}
{showDefault && (
<small className="opacity-40">
(
{typeof defaultValue === 'undefined' || !(defaultValue as string)?.length
{typeof defaultValue === 'undefined' ||
!((defaultValue as string[] | undefined)?.length ?? 0)
? localize('com_endpoint_default_blank')
: `${localize('com_endpoint_default')}: ${defaultValue}`}
)
: `${localize('com_endpoint_default')}: ${(defaultValue as string[]).join(
', ',
)}`}
</small>
)}
</Label>
</div>
<div>
<div className="bg-muted mb-2 flex flex-wrap gap-1 break-all rounded-lg">
{currentTags?.map((tag: string, index: number) => (
<Tag
key={`${tag}-${index}`}
label={tag}
onClick={onTagClick}
onRemove={() => {
onTagRemove(index);
if (inputRef.current) {
inputRef.current.focus();
<Controller
name={settingKey}
control={control}
defaultValue={defaultValue as string[]}
render={({ field }) => (
<div>
<div className="bg-muted mb-2 flex flex-wrap gap-1 break-all rounded-lg">
{field.value?.map((tag: string, index: number) => (
<Tag
key={`${tag}-${index}`}
label={tag}
onClick={onTagClick}
onRemove={() => {
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) {
inputRef.current.focus();
}
}}
/>
))}
<Input
ref={inputRef}
id={`${settingKey}-dynamic-input`}
disabled={readonly}
value={tagText}
onKeyDown={(e) => {
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;
}
field.onChange(newTags);
setTagText('');
}
}}
onChange={(e) => setTagText(e.target.value)}
placeholder={
placeholderCode === true ? localize(placeholder) ?? placeholder : placeholder
}
}}
/>
))}
<Input
ref={inputRef}
id={`${settingKey}-dynamic-input`}
disabled={readonly}
value={tagText}
onKeyDown={(e) => {
if (!currentTags) {
return;
}
if (e.key === 'Backspace' && !tagText) {
onTagRemove(currentTags.length - 1);
}
if (e.key === 'Enter') {
onTagAdd();
}
}}
onChange={(e) => setTagText(e.target.value)}
placeholder={
placeholderCode ? localize(placeholder ?? '') || placeholder : placeholder
}
className={cn(defaultTextProps, 'flex h-10 max-h-10 px-3 py-2')}
/>
</div>
</div>
className={cn(defaultTextProps, 'flex h-10 max-h-10 px-3 py-2')}
/>
</div>
</div>
)}
/>
</HoverCardTrigger>
{description && (
<OptionHover
description={descriptionCode ? localize(description) || description : description}
description={
descriptionCode === true ? localize(description) ?? description : description
}
side={descriptionSide as ESide}
/>
)}

View file

@ -1,55 +1,33 @@
// client/src/components/SidePanel/Parameters/DynamicTextarea.tsx
import { OptionTypes } from 'librechat-data-provider';
import type { DynamicSettingProps } from 'librechat-data-provider';
import React from 'react';
import { Controller, useFormContext } from 'react-hook-form';
import { Label, TextareaAutosize, HoverCard, HoverCardTrigger } from '~/components/ui';
import { useLocalize, useDebouncedInput, useParameterEffects } from '~/hooks';
import { useLocalize } from '~/hooks';
import { cn, defaultTextProps } from '~/utils';
import { useChatContext } from '~/Providers';
import OptionHover from './OptionHover';
import { ESide } from '~/common';
import type { DynamicSettingProps } from 'librechat-data-provider';
function DynamicTextarea({
label,
label = '',
settingKey,
defaultValue,
description,
description = '',
columnSpan,
setOption,
optionType,
placeholder,
placeholder = '',
readonly = false,
showDefault = true,
labelCode,
descriptionCode,
placeholderCode,
conversation,
}: DynamicSettingProps) {
const localize = useLocalize();
const { 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,
});
const { control } = useFormContext();
return (
<div
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}>
@ -59,34 +37,43 @@ function DynamicTextarea({
htmlFor={`${settingKey}-dynamic-textarea`}
className="text-left text-sm font-medium"
>
{labelCode ? localize(label ?? '') || label : label ?? settingKey}{' '}
{labelCode === true ? localize(label) ?? label : label || settingKey}{' '}
{showDefault && (
<small className="opacity-40">
(
{typeof defaultValue === 'undefined' || !(defaultValue as string)?.length
{typeof defaultValue === 'undefined' ||
!((defaultValue as string | undefined)?.length ?? 0)
? 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',
<Controller
name={settingKey}
control={control}
defaultValue={defaultValue as string}
render={({ field }) => (
<TextareaAutosize
id={`${settingKey}-dynamic-textarea`}
disabled={readonly}
value={field.value ?? ''}
onChange={(e) => field.onChange(e.target.value)}
placeholder={
placeholderCode === true ? localize(placeholder) ?? placeholder : placeholder
}
className={cn(
defaultTextProps,
'flex max-h-[138px] min-h-[100px] w-full resize-none px-3 py-2',
)}
/>
)}
/>
</HoverCardTrigger>
{description && (
<OptionHover
description={descriptionCode ? localize(description) || description : description}
description={
descriptionCode === true ? localize(description) ?? description : description
}
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 type {
DynamicSettingProps,
SettingDefinition,
SettingsConfiguration,
} from 'librechat-data-provider';
import { useSetIndexOptions } from '~/hooks';
import { useChatContext } from '~/Providers';
import type { DynamicSettingProps, SettingsConfiguration } from 'librechat-data-provider';
import {
DynamicDropdown,
DynamicCheckbox,
@ -162,92 +158,37 @@ const componentMapping: Record<ComponentTypes, React.ComponentType<DynamicSettin
};
export default function Parameters() {
const { conversation } = useChatContext();
const { setOption } = useSetIndexOptions();
const methods = useForm({
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(
(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;
const stop = settingsConfiguration.find((setting) => setting.key === 'stop') as SettingDefinition;
const Tags = componentMapping[stop.component];
const { key: stopKey, default: stopDefault, ...stopSettings } = stop;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const onSubmit = (data: Record<string, any>) => {
console.log('Form data:', data);
// Here you can handle the form submission, e.g., send the data to an API
};
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}
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>
<FormProvider {...methods}>
<form onSubmit={methods.handleSubmit(onSubmit)}>
<div className="h-auto max-w-full overflow-x-hidden p-3">
<div className="grid grid-cols-4 gap-6">
{settingsConfiguration.map((setting) => {
const Component = componentMapping[setting.component];
const { key, default: defaultValue, ...rest } = setting;
return <Component key={key} settingKey={key} defaultValue={defaultValue} {...rest} />;
})}
</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> & {
readonly?: boolean;
settingKey: string;
setOption: TSetOption;
conversation: TConversation | TPreset | null;
defaultValue?: number | boolean | string | string[];
className?: string;
inputClassName?: string;