mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-17 08:50:15 +01:00
refactor: dynamic form elements using react-hook-form Controllers
This commit is contained in:
parent
fac2acd4cf
commit
2150c4815d
10 changed files with 376 additions and 619 deletions
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue