From 8e5f1ad5754ee99845b84c22ff42a2ed27bd5d30 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Wed, 10 Apr 2024 14:27:22 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=93=A6=20feat:=20Model=20&=20Assistants?= =?UTF-8?q?=20Combobox=20for=20Side=20Panel=20(#2380)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * WIP: dynamic settings * WIP: update tests and validations * refactor(SidePanel): use hook for Links * WIP: dynamic settings, slider implemented * feat(useDebouncedInput): dynamic typing with generic * refactor(generate): add `custom` optionType to be non-conforming to conversation schema * feat: DynamicDropdown * refactor(DynamicSlider): custom optionType handling and useEffect for conversation updates elsewhere * refactor(Panel): add more test cases * chore(DynamicSlider): note * refactor(useDebouncedInput): import defaultDebouncedDelay from ~/common` * WIP: implement remaining ComponentTypes * chore: add com_sidepanel_parameters * refactor: add langCode handling for dynamic settings * chore(useOriginNavigate): change path to '/c/' * refactor: explicit textarea focus on new convo, share textarea idea via ~/common * refactor: useParameterEffects: reset if convo or preset Ids change, share and maintain statefulness in side panel * wip: combobox * chore: minor styling for Select components * wip: combobox select styling for side panel * feat: complete combobox * refactor: model select for side panel switcher * refactor(Combobox): add portal * chore: comment out dynamic parameters panel for future PR and delete prompt files * refactor(Combobox): add icon field for options, change hover bg-color, add displayValue * fix(useNewConvo): proper textarea focus with setTimeout * refactor(AssistantSwitcher): use Combobox * refactor(ModelSwitcher): add textarea focus on model switch --- client/package.json | 2 + client/src/common/types.ts | 17 +- client/src/components/Chat/Input/ChatForm.tsx | 3 +- .../SidePanel/AssistantSwitcher.tsx | 84 +++ .../components/SidePanel/ModelSwitcher.tsx | 54 ++ .../SidePanel/Parameters/DynamicCheckbox.tsx | 97 ++++ .../SidePanel/Parameters/DynamicDropdown.tsx | 106 ++++ .../SidePanel/Parameters/DynamicInput.tsx | 93 ++++ .../SidePanel/Parameters/DynamicSlider.tsx | 175 ++++++ .../SidePanel/Parameters/DynamicSwitch.tsx | 94 ++++ .../SidePanel/Parameters/DynamicTextarea.tsx | 97 ++++ .../SidePanel/Parameters/OptionHover.tsx | 26 + .../components/SidePanel/Parameters/Panel.tsx | 215 +++++++ client/src/components/SidePanel/SidePanel.tsx | 80 +-- client/src/components/SidePanel/Switcher.tsx | 120 +--- client/src/components/ui/Combobox.tsx | 168 ++++++ client/src/components/ui/Select.tsx | 12 +- client/src/components/ui/index.ts | 3 +- client/src/hooks/Conversations/index.ts | 1 + .../hooks/Conversations/useDebouncedInput.ts | 24 +- .../Conversations/useParameterEffects.ts | 68 +++ client/src/hooks/Input/index.ts | 1 + client/src/hooks/Input/useCombobox.ts | 37 ++ client/src/hooks/Nav/useSideNavLinks.ts | 71 +++ client/src/hooks/useNewConvo.ts | 12 +- client/src/hooks/useOriginNavigate.ts | 2 +- client/src/localization/languages/Eng.ts | 7 +- client/src/utils/index.ts | 4 + package-lock.json | 413 ++++++++------ packages/data-provider/specs/generate.spec.ts | 525 ++++++++++++++++++ packages/data-provider/src/generate.ts | 474 ++++++++++++++++ packages/data-provider/src/index.ts | 1 + packages/data-provider/src/schemas.ts | 226 ++++---- 33 files changed, 2850 insertions(+), 462 deletions(-) create mode 100644 client/src/components/SidePanel/AssistantSwitcher.tsx create mode 100644 client/src/components/SidePanel/ModelSwitcher.tsx create mode 100644 client/src/components/SidePanel/Parameters/DynamicCheckbox.tsx create mode 100644 client/src/components/SidePanel/Parameters/DynamicDropdown.tsx create mode 100644 client/src/components/SidePanel/Parameters/DynamicInput.tsx create mode 100644 client/src/components/SidePanel/Parameters/DynamicSlider.tsx create mode 100644 client/src/components/SidePanel/Parameters/DynamicSwitch.tsx create mode 100644 client/src/components/SidePanel/Parameters/DynamicTextarea.tsx create mode 100644 client/src/components/SidePanel/Parameters/OptionHover.tsx create mode 100644 client/src/components/SidePanel/Parameters/Panel.tsx create mode 100644 client/src/components/ui/Combobox.tsx create mode 100644 client/src/hooks/Conversations/useParameterEffects.ts create mode 100644 client/src/hooks/Input/useCombobox.ts create mode 100644 client/src/hooks/Nav/useSideNavLinks.ts create mode 100644 packages/data-provider/specs/generate.spec.ts create mode 100644 packages/data-provider/src/generate.ts diff --git a/client/package.json b/client/package.json index 685c320b07..ae42996cd9 100644 --- a/client/package.json +++ b/client/package.json @@ -27,6 +27,7 @@ }, "homepage": "https://librechat.ai", "dependencies": { + "@ariakit/react": "^0.4.5", "@dicebear/collection": "^7.0.4", "@dicebear/core": "^7.0.4", "@headlessui/react": "^1.7.13", @@ -65,6 +66,7 @@ "librechat-data-provider": "*", "lodash": "^4.17.21", "lucide-react": "^0.220.0", + "match-sorter": "^6.3.4", "rc-input-number": "^7.4.2", "react": "^18.2.0", "react-dnd": "^16.0.1", diff --git a/client/src/common/types.ts b/client/src/common/types.ts index 86590c3ffd..406812693b 100644 --- a/client/src/common/types.ts +++ b/client/src/common/types.ts @@ -2,6 +2,7 @@ import { FileSources } from 'librechat-data-provider'; import type { ColumnDef } from '@tanstack/react-table'; import type { SetterOrUpdater } from 'recoil'; import type { + TSetOption as SetOption, TConversation, TMessage, TPreset, @@ -20,6 +21,8 @@ export type GenericSetter = (value: T | ((currentValue: T) => T)) => void; export type LastSelectedModels = Record; +export const mainTextareaId = 'prompt-textarea'; + export enum IconContext { landing = 'landing', menuItem = 'menu-item', @@ -89,15 +92,16 @@ export type AssistantPanelProps = { export type AugmentedColumnDef = ColumnDef & ColumnMeta; -export type TSetOption = ( - param: number | string, -) => (newValue: number | string | boolean | Partial) => void; +export type TSetOption = SetOption; + export type TSetExample = ( i: number, type: string, newValue: number | string | boolean | null, ) => void; +export const defaultDebouncedDelay = 450; + export enum ESide { Top = 'top', Right = 'right', @@ -304,6 +308,8 @@ export type Option = Record & { value: string | number | null; }; +export type OptionWithIcon = Option & { icon?: React.ReactNode }; + export type TOptionSettings = { showExamples?: boolean; isCodeChat?: boolean; @@ -327,3 +333,8 @@ export interface ExtendedFile { } export type ContextType = { navVisible: boolean; setNavVisible: (visible: boolean) => void }; + +export interface SwitcherProps { + endpointKeyProvided: boolean; + isCollapsed: boolean; +} diff --git a/client/src/components/Chat/Input/ChatForm.tsx b/client/src/components/Chat/Input/ChatForm.tsx index a5623d4e8e..51e9cb5c28 100644 --- a/client/src/components/Chat/Input/ChatForm.tsx +++ b/client/src/components/Chat/Input/ChatForm.tsx @@ -13,6 +13,7 @@ import { TextareaAutosize } from '~/components/ui'; import { useGetFileConfig } from '~/data-provider'; import { cn, removeFocusOutlines } from '~/utils'; import AttachFile from './Files/AttachFile'; +import { mainTextareaId } from '~/common'; import StopButton from './StopButton'; import SendButton from './SendButton'; import FileRow from './Files/FileRow'; @@ -119,7 +120,7 @@ const ChatForm = ({ index = 0 }) => { onKeyDown={handleKeyDown} onCompositionStart={handleCompositionStart} onCompositionEnd={handleCompositionEnd} - id="prompt-textarea" + id={mainTextareaId} tabIndex={0} data-testid="text-input" style={{ height: 44, overflowY: 'auto' }} diff --git a/client/src/components/SidePanel/AssistantSwitcher.tsx b/client/src/components/SidePanel/AssistantSwitcher.tsx new file mode 100644 index 0000000000..f36d14a2e2 --- /dev/null +++ b/client/src/components/SidePanel/AssistantSwitcher.tsx @@ -0,0 +1,84 @@ +import { useEffect, useMemo } from 'react'; +import { Combobox } from '~/components/ui'; +import { EModelEndpoint, defaultOrderQuery } from 'librechat-data-provider'; +import type { SwitcherProps } from '~/common'; +import { useSetIndexOptions, useSelectAssistant, useLocalize } from '~/hooks'; +import { useChatContext, useAssistantsMapContext } from '~/Providers'; +import { useListAssistantsQuery } from '~/data-provider'; +import Icon from '~/components/Endpoints/Icon'; + +export default function AssistantSwitcher({ isCollapsed }: SwitcherProps) { + const localize = useLocalize(); + const { setOption } = useSetIndexOptions(); + const { index, conversation } = useChatContext(); + + /* `selectedAssistant` must be defined with `null` to cause re-render on update */ + const { assistant_id: selectedAssistant = null, endpoint } = conversation ?? {}; + + const { data: assistants = [] } = useListAssistantsQuery(defaultOrderQuery, { + select: (res) => res.data.map(({ id, name, metadata }) => ({ id, name, metadata })), + }); + + const assistantMap = useAssistantsMapContext(); + const { onSelect } = useSelectAssistant(); + + useEffect(() => { + if (!selectedAssistant && assistants && assistants.length && assistantMap) { + const assistant_id = + localStorage.getItem(`assistant_id__${index}`) ?? assistants[0]?.id ?? ''; + const assistant = assistantMap?.[assistant_id]; + if (!assistant) { + return; + } + + if (endpoint !== EModelEndpoint.assistants) { + return; + } + setOption('model')(assistant.model); + setOption('assistant_id')(assistant_id); + } + }, [index, assistants, selectedAssistant, assistantMap, endpoint, setOption]); + + const currentAssistant = assistantMap?.[selectedAssistant ?? '']; + + const assistantOptions = useMemo(() => { + return assistants.map((assistant) => { + return { + label: assistant.name ?? '', + value: assistant.id, + icon: ( + + ), + }; + }); + }, [assistants]); + + return ( + assistant.id === selectedAssistant)?.name ?? + localize('com_sidepanel_select_assistant') + } + selectPlaceholder={localize('com_sidepanel_select_assistant')} + searchPlaceholder={localize('com_assistants_search_name')} + isCollapsed={isCollapsed} + ariaLabel={'assistant'} + setValue={onSelect} + items={assistantOptions} + SelectIcon={ + + } + /> + ); +} diff --git a/client/src/components/SidePanel/ModelSwitcher.tsx b/client/src/components/SidePanel/ModelSwitcher.tsx new file mode 100644 index 0000000000..cff1342bec --- /dev/null +++ b/client/src/components/SidePanel/ModelSwitcher.tsx @@ -0,0 +1,54 @@ +import { useMemo, useRef, useCallback } from 'react'; +import { useGetModelsQuery } from 'librechat-data-provider/react-query'; +import MinimalIcon from '~/components/Endpoints/MinimalIcon'; +import { useSetIndexOptions, useLocalize } from '~/hooks'; +import type { SwitcherProps } from '~/common'; +import { useChatContext } from '~/Providers'; +import { Combobox } from '~/components/ui'; +import { mainTextareaId } from '~/common'; + +export default function ModelSwitcher({ isCollapsed }: SwitcherProps) { + const localize = useLocalize(); + const modelsQuery = useGetModelsQuery(); + const { conversation } = useChatContext(); + const { setOption } = useSetIndexOptions(); + const timeoutIdRef = useRef(); + + const { endpoint, model = null } = conversation ?? {}; + const models = useMemo(() => { + return modelsQuery?.data?.[endpoint ?? ''] ?? []; + }, [modelsQuery, endpoint]); + + const setModel = useCallback( + (model: string) => { + setOption('model')(model); + clearTimeout(timeoutIdRef.current); + timeoutIdRef.current = setTimeout(() => { + const textarea = document.getElementById(mainTextareaId); + if (textarea) { + textarea.focus(); + } + }, 150); + }, + [setOption], + ); + + return ( + + } + /> + ); +} diff --git a/client/src/components/SidePanel/Parameters/DynamicCheckbox.tsx b/client/src/components/SidePanel/Parameters/DynamicCheckbox.tsx new file mode 100644 index 0000000000..4b46070305 --- /dev/null +++ b/client/src/components/SidePanel/Parameters/DynamicCheckbox.tsx @@ -0,0 +1,97 @@ +// client/src/components/SidePanel/Parameters/DynamicCheckbox.tsx +import { useMemo, useState } from 'react'; +import { OptionTypes } from 'librechat-data-provider'; +import type { DynamicSettingProps } from 'librechat-data-provider'; +import { Label, Checkbox, HoverCard, HoverCardTrigger } from '~/components/ui'; +import { useLocalize, useParameterEffects } from '~/hooks'; +import { useChatContext } from '~/Providers'; +import OptionHover from './OptionHover'; +import { ESide } from '~/common'; + +function DynamicCheckbox({ + label, + settingKey, + defaultValue, + description, + columnSpan, + setOption, + optionType, + readonly = false, + showDefault = true, + labelCode, + descriptionCode, +}: DynamicSettingProps) { + const localize = useLocalize(); + const { conversation = { conversationId: null }, preset } = useChatContext(); + const [inputValue, setInputValue] = useState(!!(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 ( +
+ + +
+ + +
+
+ {description && ( + + )} +
+
+ ); +} + +export default DynamicCheckbox; diff --git a/client/src/components/SidePanel/Parameters/DynamicDropdown.tsx b/client/src/components/SidePanel/Parameters/DynamicDropdown.tsx new file mode 100644 index 0000000000..97fb957735 --- /dev/null +++ b/client/src/components/SidePanel/Parameters/DynamicDropdown.tsx @@ -0,0 +1,106 @@ +import { useMemo, useState } from 'react'; +import { OptionTypes } from 'librechat-data-provider'; +import type { DynamicSettingProps } from 'librechat-data-provider'; +import { Label, HoverCard, HoverCardTrigger, SelectDropDown } from '~/components/ui'; +import { useLocalize, useParameterEffects } from '~/hooks'; +import { useChatContext } from '~/Providers'; +import OptionHover from './OptionHover'; +import { ESide } from '~/common'; +import { cn } from '~/utils'; + +function DynamicDropdown({ + label, + settingKey, + defaultValue, + description, + columnSpan, + setOption, + optionType, + options, + // type: _type, + readonly = false, + showDefault = true, + labelCode, + descriptionCode, +}: DynamicSettingProps) { + const localize = useLocalize(); + const { conversation = { conversationId: null }, preset } = useChatContext(); + const [inputValue, setInputValue] = useState(null); + + const selectedValue = useMemo(() => { + if (optionType === OptionTypes.Custom) { + // TODO: custom logic, add to payload but not to conversation + return inputValue; + } + + return conversation?.[settingKey] ?? defaultValue; + }, [conversation, defaultValue, optionType, settingKey, inputValue]); + + const handleChange = (value: string) => { + if (optionType === OptionTypes.Custom) { + // TODO: custom logic, add to payload but not to conversation + setInputValue(value); + return; + } + setOption(settingKey)(value); + }; + + useParameterEffects({ + preset, + settingKey, + defaultValue, + conversation, + inputValue, + setInputValue, + preventDelayedUpdate: true, + }); + + if (!options || options.length === 0) { + return null; + } + + return ( +
+ + +
+ +
+ +
+ {description && ( + + )} +
+
+ ); +} + +export default DynamicDropdown; diff --git a/client/src/components/SidePanel/Parameters/DynamicInput.tsx b/client/src/components/SidePanel/Parameters/DynamicInput.tsx new file mode 100644 index 0000000000..5de5df5304 --- /dev/null +++ b/client/src/components/SidePanel/Parameters/DynamicInput.tsx @@ -0,0 +1,93 @@ +// client/src/components/SidePanel/Parameters/DynamicInput.tsx +import { OptionTypes } from 'librechat-data-provider'; +import type { DynamicSettingProps } from 'librechat-data-provider'; +import { useLocalize, useDebouncedInput, useParameterEffects } from '~/hooks'; +import { Label, Input, HoverCard, HoverCardTrigger } from '~/components/ui'; +import { cn, defaultTextProps } from '~/utils'; +import { useChatContext } from '~/Providers'; +import OptionHover from './OptionHover'; +import { ESide } from '~/common'; + +function DynamicInput({ + label, + settingKey, + defaultValue, + description, + columnSpan, + setOption, + optionType, + placeholder, + readonly = false, + showDefault = true, + labelCode, + descriptionCode, + placeholderCode, +}: DynamicSettingProps) { + const localize = useLocalize(); + const { conversation = { conversationId: null }, preset } = useChatContext(); + + const [setInputValue, inputValue] = useDebouncedInput({ + 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 ( +
+ + +
+ +
+ +
+ {description && ( + + )} +
+
+ ); +} + +export default DynamicInput; diff --git a/client/src/components/SidePanel/Parameters/DynamicSlider.tsx b/client/src/components/SidePanel/Parameters/DynamicSlider.tsx new file mode 100644 index 0000000000..275aaeffe5 --- /dev/null +++ b/client/src/components/SidePanel/Parameters/DynamicSlider.tsx @@ -0,0 +1,175 @@ +import { useMemo, useCallback } from 'react'; +import { OptionTypes } from 'librechat-data-provider'; +import type { DynamicSettingProps } from 'librechat-data-provider'; +import { Label, Slider, HoverCard, Input, InputNumber, HoverCardTrigger } from '~/components/ui'; +import { useLocalize, useDebouncedInput, useParameterEffects } from '~/hooks'; +import { cn, defaultTextProps, optionText } from '~/utils'; +import { ESide, defaultDebouncedDelay } from '~/common'; +import { useChatContext } from '~/Providers'; +import OptionHover from './OptionHover'; + +function DynamicSlider({ + label, + settingKey, + defaultValue, + range, + description, + columnSpan, + setOption, + optionType, + options, + readonly = false, + showDefault = true, + includeInput = true, + labelCode, + descriptionCode, +}: DynamicSettingProps) { + const localize = useLocalize(); + const { conversation = { conversationId: null }, preset } = useChatContext(); + const isEnum = useMemo(() => !range && options && options.length > 0, [options, range]); + + const [setInputValue, inputValue] = useDebouncedInput({ + optionKey: optionType !== OptionTypes.Custom ? settingKey : undefined, + initialValue: optionType !== OptionTypes.Custom ? conversation?.[settingKey] : defaultValue, + setter: () => ({}), + setOption, + delay: isEnum ? 0 : defaultDebouncedDelay, + }); + + useParameterEffects({ + preset, + settingKey, + defaultValue, + conversation, + inputValue, + setInputValue, + preventDelayedUpdate: isEnum, + }); + + const selectedValue = useMemo(() => { + if (isEnum) { + return conversation?.[settingKey] ?? defaultValue; + } + // TODO: custom logic, add to payload but not to conversation + + return inputValue; + }, [conversation, defaultValue, settingKey, inputValue, isEnum]); + + const enumToNumeric = useMemo(() => { + if (isEnum && options) { + return options.reduce((acc, mapping, index) => { + acc[mapping] = index; + return acc; + }, {} as Record); + } + return {}; + }, [isEnum, options]); + + const valueToEnumOption = useMemo(() => { + if (isEnum && options) { + return options.reduce((acc, option, index) => { + acc[index] = option; + return acc; + }, {} as Record); + } + return {}; + }, [isEnum, options]); + + const handleValueChange = useCallback( + (value: number) => { + if (isEnum) { + setInputValue(valueToEnumOption[value]); + } else { + setInputValue(value); + } + }, + [isEnum, setInputValue, valueToEnumOption], + ); + + if (!range && !isEnum) { + return null; + } + + return ( +
+ + +
+ + {includeInput && !isEnum ? ( + 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', + ), + )} + /> + ) : ( + ({})} + 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', + ), + )} + /> + )} +
+ 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" + /> +
+ {description && ( + + )} +
+
+ ); +} + +export default DynamicSlider; diff --git a/client/src/components/SidePanel/Parameters/DynamicSwitch.tsx b/client/src/components/SidePanel/Parameters/DynamicSwitch.tsx new file mode 100644 index 0000000000..08fb126be6 --- /dev/null +++ b/client/src/components/SidePanel/Parameters/DynamicSwitch.tsx @@ -0,0 +1,94 @@ +import { useState, useMemo } from 'react'; +import { OptionTypes } from 'librechat-data-provider'; +import type { DynamicSettingProps } from 'librechat-data-provider'; +import { Label, Switch, HoverCard, HoverCardTrigger } from '~/components/ui'; +import { useLocalize, useParameterEffects } from '~/hooks'; +import { useChatContext } from '~/Providers'; +import OptionHover from './OptionHover'; +import { ESide } from '~/common'; + +function DynamicSwitch({ + label, + settingKey, + defaultValue, + description, + columnSpan, + setOption, + optionType, + readonly = false, + showDefault = true, + labelCode, + descriptionCode, +}: DynamicSettingProps) { + const localize = useLocalize(); + const { conversation = { conversationId: null }, preset } = useChatContext(); + const [inputValue, setInputValue] = useState(!!(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 ( +
+ + +
+ +
+ +
+ {description && ( + + )} +
+
+ ); +} + +export default DynamicSwitch; diff --git a/client/src/components/SidePanel/Parameters/DynamicTextarea.tsx b/client/src/components/SidePanel/Parameters/DynamicTextarea.tsx new file mode 100644 index 0000000000..f2e1b70b80 --- /dev/null +++ b/client/src/components/SidePanel/Parameters/DynamicTextarea.tsx @@ -0,0 +1,97 @@ +// client/src/components/SidePanel/Parameters/DynamicTextarea.tsx +import { OptionTypes } from 'librechat-data-provider'; +import type { DynamicSettingProps } from 'librechat-data-provider'; +import { Label, TextareaAutosize, HoverCard, HoverCardTrigger } from '~/components/ui'; +import { useLocalize, useDebouncedInput, useParameterEffects } from '~/hooks'; +import { cn, defaultTextProps } from '~/utils'; +import { useChatContext } from '~/Providers'; +import OptionHover from './OptionHover'; +import { ESide } from '~/common'; + +function DynamicTextarea({ + label, + settingKey, + defaultValue, + description, + columnSpan, + setOption, + optionType, + placeholder, + readonly = false, + showDefault = true, + labelCode, + descriptionCode, + placeholderCode, +}: DynamicSettingProps) { + const localize = useLocalize(); + const { conversation = { conversationId: null }, preset } = useChatContext(); + + const [setInputValue, inputValue] = useDebouncedInput({ + 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 ( +
+ + +
+ +
+ +
+ {description && ( + + )} +
+
+ ); +} + +export default DynamicTextarea; diff --git a/client/src/components/SidePanel/Parameters/OptionHover.tsx b/client/src/components/SidePanel/Parameters/OptionHover.tsx new file mode 100644 index 0000000000..a53c68844c --- /dev/null +++ b/client/src/components/SidePanel/Parameters/OptionHover.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import { HoverCardPortal, HoverCardContent } from '~/components/ui'; +import { useLocalize } from '~/hooks'; +import { ESide } from '~/common'; + +type TOptionHoverProps = { + description: string; + langCode?: boolean; + side: ESide; +}; + +function OptionHover({ side, description, langCode }: TOptionHoverProps) { + const localize = useLocalize(); + const text = langCode ? localize(description) : description; + return ( + + +
+

{text}

+
+
+
+ ); +} + +export default OptionHover; diff --git a/client/src/components/SidePanel/Parameters/Panel.tsx b/client/src/components/SidePanel/Parameters/Panel.tsx new file mode 100644 index 0000000000..bb22f1fea3 --- /dev/null +++ b/client/src/components/SidePanel/Parameters/Panel.tsx @@ -0,0 +1,215 @@ +import { ComponentTypes } from 'librechat-data-provider'; +import type { + DynamicSettingProps, + SettingDefinition, + SettingsConfiguration, +} from 'librechat-data-provider'; +import { useSetIndexOptions } from '~/hooks'; +import DynamicDropdown from './DynamicDropdown'; +import DynamicCheckbox from './DynamicCheckbox'; +import DynamicTextarea from './DynamicTextarea'; +import DynamicSlider from './DynamicSlider'; +import DynamicSwitch from './DynamicSwitch'; +import DynamicInput from './DynamicInput'; + +const settingsConfiguration: SettingsConfiguration = [ + { + key: 'temperature', + label: 'com_endpoint_temperature', + labelCode: true, + description: 'com_endpoint_openai_temp', + descriptionCode: true, + type: 'number', + default: 1, + range: { + min: 0, + max: 2, + step: 0.01, + }, + component: 'slider', + optionType: 'model', + // columnSpan: 2, + // includeInput: false, + }, + { + key: 'top_p', + label: 'com_endpoint_top_p', + labelCode: true, + description: 'com_endpoint_openai_topp', + descriptionCode: true, + type: 'number', + default: 1, + range: { + min: 0, + max: 1, + step: 0.01, + }, + component: 'slider', + optionType: 'model', + }, + { + key: 'presence_penalty', + label: 'com_endpoint_presence_penalty', + labelCode: true, + description: 'com_endpoint_openai_pres', + descriptionCode: true, + type: 'number', + default: 0, + range: { + min: -2, + max: 2, + step: 0.01, + }, + component: 'slider', + optionType: 'model', + }, + { + key: 'frequency_penalty', + label: 'com_endpoint_frequency_penalty', + labelCode: true, + description: 'com_endpoint_openai_freq', + descriptionCode: true, + type: 'number', + default: 0, + range: { + min: -2, + max: 2, + step: 0.01, + }, + component: 'slider', + optionType: 'model', + }, + { + key: 'chatGptLabel', + label: 'com_endpoint_custom_name', + labelCode: true, + type: 'string', + default: '', + component: 'input', + placeholder: 'com_endpoint_openai_custom_name_placeholder', + placeholderCode: true, + optionType: 'conversation', + }, + { + key: 'promptPrefix', + label: 'com_endpoint_prompt_prefix', + labelCode: true, + type: 'string', + default: '', + component: 'textarea', + placeholder: 'com_endpoint_openai_prompt_prefix_placeholder', + placeholderCode: true, + optionType: 'conversation', + // columnSpan: 2, + }, + { + key: 'resendFiles', + label: 'com_endpoint_plug_resend_files', + labelCode: true, + description: 'com_endpoint_openai_resend_files', + descriptionCode: true, + type: 'boolean', + default: true, + component: 'switch', + optionType: 'conversation', + showDefault: false, + columnSpan: 2, + }, + { + key: 'imageDetail', + label: 'com_endpoint_plug_image_detail', + labelCode: true, + description: 'com_endpoint_openai_detail', + descriptionCode: true, + type: 'enum', + default: 'auto', + options: ['low', 'auto', 'high'], + optionType: 'conversation', + component: 'slider', + showDefault: false, + columnSpan: 2, + }, +]; + +const componentMapping: Record> = { + [ComponentTypes.Slider]: DynamicSlider, + [ComponentTypes.Dropdown]: DynamicDropdown, + [ComponentTypes.Switch]: DynamicSwitch, + [ComponentTypes.Textarea]: DynamicTextarea, + [ComponentTypes.Input]: DynamicInput, + [ComponentTypes.Checkbox]: DynamicCheckbox, +}; + +export default function Parameters() { + const { setOption } = useSetIndexOptions(); + + const temperature = settingsConfiguration.find( + (setting) => setting.key === 'temperature', + ) as SettingDefinition; + const TempComponent = componentMapping[temperature.component]; + const { key: temp, default: tempDefault, ...tempSettings } = temperature; + + const imageDetail = settingsConfiguration.find( + (setting) => setting.key === 'imageDetail', + ) as SettingDefinition; + const DetailComponent = componentMapping[imageDetail.component]; + const { key: detail, default: detailDefault, ...detailSettings } = imageDetail; + + const resendFiles = settingsConfiguration.find( + (setting) => setting.key === 'resendFiles', + ) as SettingDefinition; + const Switch = componentMapping[resendFiles.component]; + const { key: switchKey, default: switchDefault, ...switchSettings } = resendFiles; + + const promptPrefix = settingsConfiguration.find( + (setting) => setting.key === 'promptPrefix', + ) as SettingDefinition; + const Textarea = componentMapping[promptPrefix.component]; + const { key: textareaKey, default: textareaDefault, ...textareaSettings } = promptPrefix; + + const chatGptLabel = settingsConfiguration.find( + (setting) => setting.key === 'chatGptLabel', + ) as SettingDefinition; + const Input = componentMapping[chatGptLabel.component]; + const { key: inputKey, default: inputDefault, ...inputSettings } = chatGptLabel; + + return ( +
+
+ {' '} + {/* 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 */} + +