📦 feat: Model & Assistants Combobox for Side Panel (#2380)

* WIP: dynamic settings

* WIP: update tests and validations

* refactor(SidePanel): use hook for Links

* WIP: dynamic settings, slider implemented

* feat(useDebouncedInput): dynamic typing with generic

* refactor(generate): add `custom` optionType to be non-conforming to conversation schema

* feat: DynamicDropdown

* refactor(DynamicSlider): custom optionType handling and useEffect for conversation updates elsewhere

* refactor(Panel): add more test cases

* chore(DynamicSlider): note

* refactor(useDebouncedInput): import defaultDebouncedDelay from ~/common`

* WIP: implement remaining ComponentTypes

* chore: add com_sidepanel_parameters

* refactor: add langCode handling for dynamic settings

* chore(useOriginNavigate): change path to '/c/'

* refactor: explicit textarea focus on new convo, share textarea idea via ~/common

* refactor: useParameterEffects: reset if convo or preset Ids change, share and maintain statefulness in side panel

* wip: combobox

* chore: minor styling for Select components

* wip: combobox select styling for side panel

* feat: complete combobox

* refactor: model select for side panel switcher

* refactor(Combobox): add portal

* chore: comment out dynamic parameters panel for future PR and delete prompt files

* refactor(Combobox): add icon field for options, change hover bg-color, add displayValue

* fix(useNewConvo): proper textarea focus with setTimeout

* refactor(AssistantSwitcher): use Combobox

* refactor(ModelSwitcher): add textarea focus on model switch
This commit is contained in:
Danny Avila 2024-04-10 14:27:22 -04:00 committed by GitHub
parent f64a2cb0b0
commit 8e5f1ad575
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
33 changed files with 2850 additions and 462 deletions

View file

@ -27,6 +27,7 @@
}, },
"homepage": "https://librechat.ai", "homepage": "https://librechat.ai",
"dependencies": { "dependencies": {
"@ariakit/react": "^0.4.5",
"@dicebear/collection": "^7.0.4", "@dicebear/collection": "^7.0.4",
"@dicebear/core": "^7.0.4", "@dicebear/core": "^7.0.4",
"@headlessui/react": "^1.7.13", "@headlessui/react": "^1.7.13",
@ -65,6 +66,7 @@
"librechat-data-provider": "*", "librechat-data-provider": "*",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"lucide-react": "^0.220.0", "lucide-react": "^0.220.0",
"match-sorter": "^6.3.4",
"rc-input-number": "^7.4.2", "rc-input-number": "^7.4.2",
"react": "^18.2.0", "react": "^18.2.0",
"react-dnd": "^16.0.1", "react-dnd": "^16.0.1",

View file

@ -2,6 +2,7 @@ import { FileSources } from 'librechat-data-provider';
import type { ColumnDef } from '@tanstack/react-table'; import type { ColumnDef } from '@tanstack/react-table';
import type { SetterOrUpdater } from 'recoil'; import type { SetterOrUpdater } from 'recoil';
import type { import type {
TSetOption as SetOption,
TConversation, TConversation,
TMessage, TMessage,
TPreset, TPreset,
@ -20,6 +21,8 @@ export type GenericSetter<T> = (value: T | ((currentValue: T) => T)) => void;
export type LastSelectedModels = Record<EModelEndpoint, string>; export type LastSelectedModels = Record<EModelEndpoint, string>;
export const mainTextareaId = 'prompt-textarea';
export enum IconContext { export enum IconContext {
landing = 'landing', landing = 'landing',
menuItem = 'menu-item', menuItem = 'menu-item',
@ -89,15 +92,16 @@ export type AssistantPanelProps = {
export type AugmentedColumnDef<TData, TValue> = ColumnDef<TData, TValue> & ColumnMeta; export type AugmentedColumnDef<TData, TValue> = ColumnDef<TData, TValue> & ColumnMeta;
export type TSetOption = ( export type TSetOption = SetOption;
param: number | string,
) => (newValue: number | string | boolean | Partial<TPreset>) => void;
export type TSetExample = ( export type TSetExample = (
i: number, i: number,
type: string, type: string,
newValue: number | string | boolean | null, newValue: number | string | boolean | null,
) => void; ) => void;
export const defaultDebouncedDelay = 450;
export enum ESide { export enum ESide {
Top = 'top', Top = 'top',
Right = 'right', Right = 'right',
@ -304,6 +308,8 @@ export type Option = Record<string, unknown> & {
value: string | number | null; value: string | number | null;
}; };
export type OptionWithIcon = Option & { icon?: React.ReactNode };
export type TOptionSettings = { export type TOptionSettings = {
showExamples?: boolean; showExamples?: boolean;
isCodeChat?: boolean; isCodeChat?: boolean;
@ -327,3 +333,8 @@ export interface ExtendedFile {
} }
export type ContextType = { navVisible: boolean; setNavVisible: (visible: boolean) => void }; export type ContextType = { navVisible: boolean; setNavVisible: (visible: boolean) => void };
export interface SwitcherProps {
endpointKeyProvided: boolean;
isCollapsed: boolean;
}

View file

@ -13,6 +13,7 @@ import { TextareaAutosize } from '~/components/ui';
import { useGetFileConfig } from '~/data-provider'; import { useGetFileConfig } from '~/data-provider';
import { cn, removeFocusOutlines } from '~/utils'; import { cn, removeFocusOutlines } from '~/utils';
import AttachFile from './Files/AttachFile'; import AttachFile from './Files/AttachFile';
import { mainTextareaId } from '~/common';
import StopButton from './StopButton'; import StopButton from './StopButton';
import SendButton from './SendButton'; import SendButton from './SendButton';
import FileRow from './Files/FileRow'; import FileRow from './Files/FileRow';
@ -119,7 +120,7 @@ const ChatForm = ({ index = 0 }) => {
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
onCompositionStart={handleCompositionStart} onCompositionStart={handleCompositionStart}
onCompositionEnd={handleCompositionEnd} onCompositionEnd={handleCompositionEnd}
id="prompt-textarea" id={mainTextareaId}
tabIndex={0} tabIndex={0}
data-testid="text-input" data-testid="text-input"
style={{ height: 44, overflowY: 'auto' }} style={{ height: 44, overflowY: 'auto' }}

View file

@ -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: (
<Icon
isCreatedByUser={false}
endpoint={EModelEndpoint.assistants}
assistantName={assistant.name ?? ''}
iconURL={(assistant.metadata?.avatar as string) ?? ''}
/>
),
};
});
}, [assistants]);
return (
<Combobox
selectedValue={currentAssistant?.id ?? ''}
displayValue={
assistants.find((assistant) => 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={
<Icon
isCreatedByUser={false}
endpoint={EModelEndpoint.assistants}
assistantName={currentAssistant?.name ?? ''}
iconURL={(currentAssistant?.metadata?.avatar as string) ?? ''}
/>
}
/>
);
}

View file

@ -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<NodeJS.Timeout>();
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 (
<Combobox
selectPlaceholder={localize('com_ui_select_model')}
searchPlaceholder={localize('com_ui_select_search_model')}
isCollapsed={isCollapsed}
ariaLabel={'model'}
selectedValue={model ?? ''}
setValue={setModel}
items={models}
SelectIcon={
<MinimalIcon
isCreatedByUser={false}
endpoint={endpoint}
// iconURL={} // for future preset icons
/>
}
/>
);
}

View file

@ -0,0 +1,97 @@
// client/src/components/SidePanel/Parameters/DynamicCheckbox.tsx
import { useMemo, useState } from 'react';
import { OptionTypes } from 'librechat-data-provider';
import type { DynamicSettingProps } from 'librechat-data-provider';
import { Label, Checkbox, HoverCard, HoverCardTrigger } from '~/components/ui';
import { useLocalize, useParameterEffects } from '~/hooks';
import { useChatContext } from '~/Providers';
import OptionHover from './OptionHover';
import { ESide } from '~/common';
function DynamicCheckbox({
label,
settingKey,
defaultValue,
description,
columnSpan,
setOption,
optionType,
readonly = false,
showDefault = true,
labelCode,
descriptionCode,
}: DynamicSettingProps) {
const localize = useLocalize();
const { conversation = { conversationId: null }, preset } = useChatContext();
const [inputValue, setInputValue] = useState<boolean>(!!(defaultValue as boolean | undefined));
const selectedValue = useMemo(() => {
if (optionType === OptionTypes.Custom) {
// TODO: custom logic, add to payload but not to conversation
return inputValue;
}
return conversation?.[settingKey] ?? defaultValue;
}, [conversation, defaultValue, optionType, settingKey, inputValue]);
const handleCheckedChange = (checked: boolean) => {
if (optionType === OptionTypes.Custom) {
// TODO: custom logic, add to payload but not to conversation
setInputValue(checked);
return;
}
setOption(settingKey)(checked);
};
useParameterEffects({
preset,
settingKey,
defaultValue,
conversation,
inputValue,
setInputValue,
preventDelayedUpdate: true,
});
return (
<div
className={`flex flex-col items-center justify-start gap-6 ${
columnSpan ? `col-span-${columnSpan}` : 'col-span-full'
}`}
>
<HoverCard openDelay={300}>
<HoverCardTrigger className="grid w-full items-center">
<div className="flex justify-start gap-4">
<Label
htmlFor={`${settingKey}-dynamic-checkbox`}
className="text-left text-sm font-medium"
>
{labelCode ? localize(label ?? '') || label : label ?? settingKey}{' '}
{showDefault && (
<small className="opacity-40">
({localize('com_endpoint_default')}:{' '}
{defaultValue ? localize('com_ui_yes') : localize('com_ui_no')})
</small>
)}
</Label>
<Checkbox
id={`${settingKey}-dynamic-checkbox`}
disabled={readonly}
checked={selectedValue}
onCheckedChange={handleCheckedChange}
className="mt-[2px] focus:ring-opacity-20 dark:border-gray-500 dark:bg-gray-700 dark:text-gray-50 dark:focus:ring-gray-600 dark:focus:ring-opacity-50 dark:focus:ring-offset-0"
/>
</div>
</HoverCardTrigger>
{description && (
<OptionHover
description={descriptionCode ? localize(description) || description : description}
side={ESide.Left}
/>
)}
</HoverCard>
</div>
);
}
export default DynamicCheckbox;

View file

@ -0,0 +1,106 @@
import { useMemo, useState } from 'react';
import { OptionTypes } from 'librechat-data-provider';
import type { DynamicSettingProps } from 'librechat-data-provider';
import { Label, HoverCard, HoverCardTrigger, SelectDropDown } from '~/components/ui';
import { useLocalize, useParameterEffects } from '~/hooks';
import { useChatContext } from '~/Providers';
import OptionHover from './OptionHover';
import { ESide } from '~/common';
import { cn } from '~/utils';
function DynamicDropdown({
label,
settingKey,
defaultValue,
description,
columnSpan,
setOption,
optionType,
options,
// type: _type,
readonly = false,
showDefault = true,
labelCode,
descriptionCode,
}: DynamicSettingProps) {
const localize = useLocalize();
const { conversation = { conversationId: null }, preset } = useChatContext();
const [inputValue, setInputValue] = useState<string | null>(null);
const selectedValue = useMemo(() => {
if (optionType === OptionTypes.Custom) {
// TODO: custom logic, add to payload but not to conversation
return inputValue;
}
return conversation?.[settingKey] ?? defaultValue;
}, [conversation, defaultValue, optionType, settingKey, inputValue]);
const handleChange = (value: string) => {
if (optionType === OptionTypes.Custom) {
// TODO: custom logic, add to payload but not to conversation
setInputValue(value);
return;
}
setOption(settingKey)(value);
};
useParameterEffects({
preset,
settingKey,
defaultValue,
conversation,
inputValue,
setInputValue,
preventDelayedUpdate: true,
});
if (!options || options.length === 0) {
return null;
}
return (
<div
className={cn(
'flex flex-col items-center justify-start gap-6',
columnSpan ? `col-span-${columnSpan}` : 'col-span-full',
)}
>
<HoverCard openDelay={300}>
<HoverCardTrigger className="grid w-full items-center gap-2">
<div className="flex w-full justify-between">
<Label
htmlFor={`${settingKey}-dynamic-dropdown`}
className="text-left text-sm font-medium"
>
{labelCode ? localize(label ?? '') || label : label ?? settingKey}
{showDefault && (
<small className="opacity-40">
({localize('com_endpoint_default')}: {defaultValue})
</small>
)}
</Label>
</div>
<SelectDropDown
showLabel={false}
emptyTitle={true}
disabled={readonly}
value={selectedValue}
setValue={handleChange}
availableValues={options}
containerClassName="w-full"
id={`${settingKey}-dynamic-dropdown`}
/>
</HoverCardTrigger>
{description && (
<OptionHover
description={descriptionCode ? localize(description) || description : description}
side={ESide.Left}
/>
)}
</HoverCard>
</div>
);
}
export default DynamicDropdown;

View file

@ -0,0 +1,93 @@
// client/src/components/SidePanel/Parameters/DynamicInput.tsx
import { OptionTypes } from 'librechat-data-provider';
import type { DynamicSettingProps } from 'librechat-data-provider';
import { useLocalize, useDebouncedInput, useParameterEffects } from '~/hooks';
import { Label, Input, HoverCard, HoverCardTrigger } from '~/components/ui';
import { cn, defaultTextProps } from '~/utils';
import { useChatContext } from '~/Providers';
import OptionHover from './OptionHover';
import { ESide } from '~/common';
function DynamicInput({
label,
settingKey,
defaultValue,
description,
columnSpan,
setOption,
optionType,
placeholder,
readonly = false,
showDefault = true,
labelCode,
descriptionCode,
placeholderCode,
}: DynamicSettingProps) {
const localize = useLocalize();
const { conversation = { conversationId: null }, preset } = useChatContext();
const [setInputValue, inputValue] = useDebouncedInput<string | null>({
optionKey: optionType !== OptionTypes.Custom ? settingKey : undefined,
initialValue:
optionType !== OptionTypes.Custom
? (conversation?.[settingKey] as string)
: (defaultValue as string),
setter: () => ({}),
setOption,
});
useParameterEffects({
preset,
settingKey,
defaultValue: typeof defaultValue === 'undefined' ? '' : defaultValue,
conversation,
inputValue,
setInputValue,
});
return (
<div
className={`flex flex-col items-center justify-start gap-6 ${
columnSpan ? `col-span-${columnSpan}` : 'col-span-full'
}`}
>
<HoverCard openDelay={300}>
<HoverCardTrigger className="grid w-full items-center gap-2">
<div className="flex w-full justify-between">
<Label
htmlFor={`${settingKey}-dynamic-input`}
className="text-left text-sm font-medium"
>
{labelCode ? localize(label ?? '') || label : label ?? settingKey}{' '}
{showDefault && (
<small className="opacity-40">
(
{typeof defaultValue === 'undefined' || !(defaultValue as string)?.length
? localize('com_endpoint_default_blank')
: `${localize('com_endpoint_default')}: ${defaultValue}`}
)
</small>
)}
</Label>
</div>
<Input
id={`${settingKey}-dynamic-input`}
disabled={readonly}
value={inputValue ?? ''}
onChange={setInputValue}
placeholder={placeholderCode ? localize(placeholder ?? '') || placeholder : placeholder}
className={cn(defaultTextProps, 'flex h-10 max-h-10 w-full resize-none px-3 py-2')}
/>
</HoverCardTrigger>
{description && (
<OptionHover
description={descriptionCode ? localize(description) || description : description}
side={ESide.Left}
/>
)}
</HoverCard>
</div>
);
}
export default DynamicInput;

View file

@ -0,0 +1,175 @@
import { useMemo, useCallback } from 'react';
import { OptionTypes } from 'librechat-data-provider';
import type { DynamicSettingProps } from 'librechat-data-provider';
import { Label, Slider, HoverCard, Input, InputNumber, HoverCardTrigger } from '~/components/ui';
import { useLocalize, useDebouncedInput, useParameterEffects } from '~/hooks';
import { cn, defaultTextProps, optionText } from '~/utils';
import { ESide, defaultDebouncedDelay } from '~/common';
import { useChatContext } from '~/Providers';
import OptionHover from './OptionHover';
function DynamicSlider({
label,
settingKey,
defaultValue,
range,
description,
columnSpan,
setOption,
optionType,
options,
readonly = false,
showDefault = true,
includeInput = true,
labelCode,
descriptionCode,
}: DynamicSettingProps) {
const localize = useLocalize();
const { conversation = { conversationId: null }, preset } = useChatContext();
const isEnum = useMemo(() => !range && options && options.length > 0, [options, range]);
const [setInputValue, inputValue] = useDebouncedInput<string | number>({
optionKey: optionType !== OptionTypes.Custom ? settingKey : undefined,
initialValue: optionType !== OptionTypes.Custom ? conversation?.[settingKey] : defaultValue,
setter: () => ({}),
setOption,
delay: isEnum ? 0 : defaultDebouncedDelay,
});
useParameterEffects({
preset,
settingKey,
defaultValue,
conversation,
inputValue,
setInputValue,
preventDelayedUpdate: isEnum,
});
const selectedValue = useMemo(() => {
if (isEnum) {
return conversation?.[settingKey] ?? defaultValue;
}
// TODO: custom logic, add to payload but not to conversation
return inputValue;
}, [conversation, defaultValue, settingKey, inputValue, isEnum]);
const enumToNumeric = useMemo(() => {
if (isEnum && options) {
return options.reduce((acc, mapping, index) => {
acc[mapping] = index;
return acc;
}, {} as Record<string, number>);
}
return {};
}, [isEnum, options]);
const valueToEnumOption = useMemo(() => {
if (isEnum && options) {
return options.reduce((acc, option, index) => {
acc[index] = option;
return acc;
}, {} as Record<number, string>);
}
return {};
}, [isEnum, options]);
const handleValueChange = useCallback(
(value: number) => {
if (isEnum) {
setInputValue(valueToEnumOption[value]);
} else {
setInputValue(value);
}
},
[isEnum, setInputValue, valueToEnumOption],
);
if (!range && !isEnum) {
return null;
}
return (
<div
className={cn(
'flex flex-col items-center justify-start gap-6',
columnSpan ? `col-span-${columnSpan}` : 'col-span-full',
)}
>
<HoverCard openDelay={300}>
<HoverCardTrigger className="grid w-full items-center gap-2">
<div className="flex justify-between">
<Label
htmlFor={`${settingKey}-dynamic-setting`}
className="text-left text-sm font-medium"
>
{labelCode ? localize(label ?? '') || label : label ?? settingKey}{' '}
{showDefault && (
<small className="opacity-40">
({localize('com_endpoint_default')}: {defaultValue})
</small>
)}
</Label>
{includeInput && !isEnum ? (
<InputNumber
id={`${settingKey}-dynamic-setting-input-number`}
disabled={readonly}
value={inputValue ?? defaultValue}
onChange={(value) => setInputValue(Number(value))}
max={range ? range.max : (options?.length ?? 0) - 1}
min={range ? range.min : 0}
step={range ? range.step ?? 1 : 1}
controls={false}
className={cn(
defaultTextProps,
cn(
optionText,
'reset-rc-number-input reset-rc-number-input-text-right h-auto w-12 border-0 group-hover/temp:border-gray-200',
),
)}
/>
) : (
<Input
id={`${settingKey}-dynamic-setting-input`}
disabled={readonly}
value={selectedValue ?? defaultValue}
onChange={() => ({})}
className={cn(
defaultTextProps,
cn(
optionText,
'reset-rc-number-input reset-rc-number-input-text-right h-auto w-12 border-0 group-hover/temp:border-gray-200',
),
)}
/>
)}
</div>
<Slider
id={`${settingKey}-dynamic-setting-slider`}
disabled={readonly}
value={[
isEnum
? enumToNumeric[(selectedValue as number) ?? '']
: (inputValue as number) ?? (defaultValue as number),
]}
onValueChange={(value) => handleValueChange(value[0])}
doubleClickHandler={() => setInputValue(defaultValue as string | number)}
max={isEnum && options ? options.length - 1 : range ? range.max : 0}
min={range ? range.min : 0}
step={range ? range.step ?? 1 : 1}
className="flex h-4 w-full"
/>
</HoverCardTrigger>
{description && (
<OptionHover
description={descriptionCode ? localize(description) || description : description}
side={ESide.Left}
/>
)}
</HoverCard>
</div>
);
}
export default DynamicSlider;

View file

@ -0,0 +1,94 @@
import { useState, useMemo } from 'react';
import { OptionTypes } from 'librechat-data-provider';
import type { DynamicSettingProps } from 'librechat-data-provider';
import { Label, Switch, HoverCard, HoverCardTrigger } from '~/components/ui';
import { useLocalize, useParameterEffects } from '~/hooks';
import { useChatContext } from '~/Providers';
import OptionHover from './OptionHover';
import { ESide } from '~/common';
function DynamicSwitch({
label,
settingKey,
defaultValue,
description,
columnSpan,
setOption,
optionType,
readonly = false,
showDefault = true,
labelCode,
descriptionCode,
}: DynamicSettingProps) {
const localize = useLocalize();
const { conversation = { conversationId: null }, preset } = useChatContext();
const [inputValue, setInputValue] = useState<boolean>(!!(defaultValue as boolean | undefined));
useParameterEffects({
preset,
settingKey,
defaultValue,
conversation,
inputValue,
setInputValue,
preventDelayedUpdate: true,
});
const selectedValue = useMemo(() => {
if (optionType === OptionTypes.Custom) {
// TODO: custom logic, add to payload but not to conversation
return inputValue;
}
return conversation?.[settingKey] ?? defaultValue;
}, [conversation, defaultValue, optionType, settingKey, inputValue]);
const handleCheckedChange = (checked: boolean) => {
if (optionType === OptionTypes.Custom) {
// TODO: custom logic, add to payload but not to conversation
setInputValue(checked);
return;
}
setOption(settingKey)(checked);
};
return (
<div
className={`flex flex-col items-center justify-start gap-6 ${
columnSpan ? `col-span-${columnSpan}` : 'col-span-full'
}`}
>
<HoverCard openDelay={300}>
<HoverCardTrigger className="grid w-full items-center gap-2">
<div className="flex justify-between">
<Label
htmlFor={`${settingKey}-dynamic-switch`}
className="text-left text-sm font-medium"
>
{labelCode ? localize(label ?? '') || label : label ?? settingKey}{' '}
{showDefault && (
<small className="opacity-40">
({localize('com_endpoint_default')}: {defaultValue ? 'com_ui_on' : 'com_ui_off'})
</small>
)}
</Label>
</div>
<Switch
id={`${settingKey}-dynamic-switch`}
checked={selectedValue}
onCheckedChange={handleCheckedChange}
disabled={readonly}
className="flex"
/>
</HoverCardTrigger>
{description && (
<OptionHover
description={descriptionCode ? localize(description) || description : description}
side={ESide.Left}
/>
)}
</HoverCard>
</div>
);
}
export default DynamicSwitch;

View file

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

View file

@ -0,0 +1,26 @@
import React from 'react';
import { HoverCardPortal, HoverCardContent } from '~/components/ui';
import { useLocalize } from '~/hooks';
import { ESide } from '~/common';
type TOptionHoverProps = {
description: string;
langCode?: boolean;
side: ESide;
};
function OptionHover({ side, description, langCode }: TOptionHoverProps) {
const localize = useLocalize();
const text = langCode ? localize(description) : description;
return (
<HoverCardPortal>
<HoverCardContent side={side} className="z-[999] w-80 dark:bg-gray-700" sideOffset={30}>
<div className="space-y-2">
<p className="text-sm text-gray-600 dark:text-gray-300">{text}</p>
</div>
</HoverCardContent>
</HoverCardPortal>
);
}
export default OptionHover;

View file

@ -0,0 +1,215 @@
import { ComponentTypes } from 'librechat-data-provider';
import type {
DynamicSettingProps,
SettingDefinition,
SettingsConfiguration,
} from 'librechat-data-provider';
import { useSetIndexOptions } from '~/hooks';
import DynamicDropdown from './DynamicDropdown';
import DynamicCheckbox from './DynamicCheckbox';
import DynamicTextarea from './DynamicTextarea';
import DynamicSlider from './DynamicSlider';
import DynamicSwitch from './DynamicSwitch';
import DynamicInput from './DynamicInput';
const settingsConfiguration: SettingsConfiguration = [
{
key: 'temperature',
label: 'com_endpoint_temperature',
labelCode: true,
description: 'com_endpoint_openai_temp',
descriptionCode: true,
type: 'number',
default: 1,
range: {
min: 0,
max: 2,
step: 0.01,
},
component: 'slider',
optionType: 'model',
// columnSpan: 2,
// includeInput: false,
},
{
key: 'top_p',
label: 'com_endpoint_top_p',
labelCode: true,
description: 'com_endpoint_openai_topp',
descriptionCode: true,
type: 'number',
default: 1,
range: {
min: 0,
max: 1,
step: 0.01,
},
component: 'slider',
optionType: 'model',
},
{
key: 'presence_penalty',
label: 'com_endpoint_presence_penalty',
labelCode: true,
description: 'com_endpoint_openai_pres',
descriptionCode: true,
type: 'number',
default: 0,
range: {
min: -2,
max: 2,
step: 0.01,
},
component: 'slider',
optionType: 'model',
},
{
key: 'frequency_penalty',
label: 'com_endpoint_frequency_penalty',
labelCode: true,
description: 'com_endpoint_openai_freq',
descriptionCode: true,
type: 'number',
default: 0,
range: {
min: -2,
max: 2,
step: 0.01,
},
component: 'slider',
optionType: 'model',
},
{
key: 'chatGptLabel',
label: 'com_endpoint_custom_name',
labelCode: true,
type: 'string',
default: '',
component: 'input',
placeholder: 'com_endpoint_openai_custom_name_placeholder',
placeholderCode: true,
optionType: 'conversation',
},
{
key: 'promptPrefix',
label: 'com_endpoint_prompt_prefix',
labelCode: true,
type: 'string',
default: '',
component: 'textarea',
placeholder: 'com_endpoint_openai_prompt_prefix_placeholder',
placeholderCode: true,
optionType: 'conversation',
// columnSpan: 2,
},
{
key: 'resendFiles',
label: 'com_endpoint_plug_resend_files',
labelCode: true,
description: 'com_endpoint_openai_resend_files',
descriptionCode: true,
type: 'boolean',
default: true,
component: 'switch',
optionType: 'conversation',
showDefault: false,
columnSpan: 2,
},
{
key: 'imageDetail',
label: 'com_endpoint_plug_image_detail',
labelCode: true,
description: 'com_endpoint_openai_detail',
descriptionCode: true,
type: 'enum',
default: 'auto',
options: ['low', 'auto', 'high'],
optionType: 'conversation',
component: 'slider',
showDefault: false,
columnSpan: 2,
},
];
const componentMapping: Record<ComponentTypes, React.ComponentType<DynamicSettingProps>> = {
[ComponentTypes.Slider]: DynamicSlider,
[ComponentTypes.Dropdown]: DynamicDropdown,
[ComponentTypes.Switch]: DynamicSwitch,
[ComponentTypes.Textarea]: DynamicTextarea,
[ComponentTypes.Input]: DynamicInput,
[ComponentTypes.Checkbox]: DynamicCheckbox,
};
export default function Parameters() {
const { setOption } = useSetIndexOptions();
const temperature = settingsConfiguration.find(
(setting) => setting.key === 'temperature',
) as SettingDefinition;
const TempComponent = componentMapping[temperature.component];
const { key: temp, default: tempDefault, ...tempSettings } = temperature;
const imageDetail = settingsConfiguration.find(
(setting) => setting.key === 'imageDetail',
) as SettingDefinition;
const DetailComponent = componentMapping[imageDetail.component];
const { key: detail, default: detailDefault, ...detailSettings } = imageDetail;
const resendFiles = settingsConfiguration.find(
(setting) => setting.key === 'resendFiles',
) as SettingDefinition;
const Switch = componentMapping[resendFiles.component];
const { key: switchKey, default: switchDefault, ...switchSettings } = resendFiles;
const promptPrefix = settingsConfiguration.find(
(setting) => setting.key === 'promptPrefix',
) as SettingDefinition;
const Textarea = componentMapping[promptPrefix.component];
const { key: textareaKey, default: textareaDefault, ...textareaSettings } = promptPrefix;
const chatGptLabel = settingsConfiguration.find(
(setting) => setting.key === 'chatGptLabel',
) as SettingDefinition;
const Input = componentMapping[chatGptLabel.component];
const { key: inputKey, default: inputDefault, ...inputSettings } = chatGptLabel;
return (
<div className="h-auto max-w-full overflow-x-hidden p-3">
<div className="grid grid-cols-4 gap-6">
{' '}
{/* This is the parent element containing all settings */}
{/* Below is an example of an applied dynamic setting, each be contained by a div with the column span specified */}
<Input
settingKey={inputKey}
defaultValue={inputDefault}
{...inputSettings}
setOption={setOption}
/>
<Textarea
settingKey={textareaKey}
defaultValue={textareaDefault}
{...textareaSettings}
setOption={setOption}
/>
<TempComponent
settingKey={temp}
defaultValue={tempDefault}
{...tempSettings}
setOption={setOption}
/>
<Switch
settingKey={switchKey}
defaultValue={switchDefault}
{...switchSettings}
setOption={setOption}
/>
<DetailComponent
settingKey={detail}
defaultValue={detailDefault}
{...detailSettings}
setOption={setOption}
/>
</div>
</div>
);
}

View file

@ -1,18 +1,15 @@
import throttle from 'lodash/throttle'; import throttle from 'lodash/throttle';
import { ArrowRightToLine } from 'lucide-react';
import { useState, useRef, useCallback, useEffect, useMemo, memo } from 'react'; import { useState, useRef, useCallback, useEffect, useMemo, memo } from 'react';
import { useGetEndpointsQuery, useUserKeyQuery } from 'librechat-data-provider/react-query'; import { useGetEndpointsQuery, useUserKeyQuery } from 'librechat-data-provider/react-query';
import type { ImperativePanelHandle } from 'react-resizable-panels'; import type { ImperativePanelHandle } from 'react-resizable-panels';
import { EModelEndpoint, type TEndpointsConfig } from 'librechat-data-provider'; import { EModelEndpoint, type TEndpointsConfig } from 'librechat-data-provider';
import type { NavLink } from '~/common';
import { ResizableHandleAlt, ResizablePanel, ResizablePanelGroup } from '~/components/ui/Resizable'; import { ResizableHandleAlt, ResizablePanel, ResizablePanelGroup } from '~/components/ui/Resizable';
import { TooltipProvider, Tooltip } from '~/components/ui/Tooltip'; import { TooltipProvider, Tooltip } from '~/components/ui/Tooltip';
import { Blocks, AttachmentIcon } from '~/components/svg'; import useSideNavLinks from '~/hooks/Nav/useSideNavLinks';
import { useMediaQuery, useLocalStorage } from '~/hooks'; import { useMediaQuery, useLocalStorage } from '~/hooks';
import { Separator } from '~/components/ui/Separator'; import { Separator } from '~/components/ui/Separator';
import NavToggle from '~/components/Nav/NavToggle'; import NavToggle from '~/components/Nav/NavToggle';
import PanelSwitch from './Builder/PanelSwitch'; import { useChatContext } from '~/Providers';
import FilesPanel from './Files/Panel';
import Switcher from './Switcher'; import Switcher from './Switcher';
import { cn } from '~/utils'; import { cn } from '~/utils';
import Nav from './Nav'; import Nav from './Nav';
@ -43,6 +40,8 @@ const SidePanel = ({
const { data: endpointsConfig = {} as TEndpointsConfig } = useGetEndpointsQuery(); const { data: endpointsConfig = {} as TEndpointsConfig } = useGetEndpointsQuery();
const { data: keyExpiry = { expiresAt: undefined } } = useUserKeyQuery(EModelEndpoint.assistants); const { data: keyExpiry = { expiresAt: undefined } } = useUserKeyQuery(EModelEndpoint.assistants);
const isSmallScreen = useMediaQuery('(max-width: 767px)'); const isSmallScreen = useMediaQuery('(max-width: 767px)');
const { conversation } = useChatContext();
const { endpoint } = conversation ?? {};
const panelRef = useRef<ImperativePanelHandle>(null); const panelRef = useRef<ImperativePanelHandle>(null);
@ -52,49 +51,25 @@ const SidePanel = ({
}, []); }, []);
const assistants = useMemo(() => endpointsConfig?.[EModelEndpoint.assistants], [endpointsConfig]); const assistants = useMemo(() => endpointsConfig?.[EModelEndpoint.assistants], [endpointsConfig]);
const userProvidesKey = useMemo(() => !!assistants?.userProvide, [assistants]); const userProvidesKey = useMemo(
() => !!endpointsConfig?.[endpoint ?? '']?.userProvide,
[endpointsConfig, endpoint],
);
const keyProvided = useMemo( const keyProvided = useMemo(
() => (userProvidesKey ? !!keyExpiry?.expiresAt : true), () => (userProvidesKey ? !!keyExpiry?.expiresAt : true),
[keyExpiry?.expiresAt, userProvidesKey], [keyExpiry?.expiresAt, userProvidesKey],
); );
const Links = useMemo(() => { const hidePanel = useCallback(() => {
const links: NavLink[] = [];
if (assistants && assistants.disableBuilder !== true && keyProvided) {
links.push({
title: 'com_sidepanel_assistant_builder',
label: '',
icon: Blocks,
id: 'assistants',
Component: PanelSwitch,
});
}
links.push({
title: 'com_sidepanel_attach_files',
label: '',
icon: AttachmentIcon,
id: 'files',
Component: FilesPanel,
});
links.push({
title: 'com_sidepanel_hide_panel',
label: '',
icon: ArrowRightToLine,
onClick: () => {
setIsCollapsed(true); setIsCollapsed(true);
setCollapsedSize(0); setCollapsedSize(0);
setMinSize(defaultMinSize); setMinSize(defaultMinSize);
setFullCollapse(true); setFullCollapse(true);
localStorage.setItem('fullPanelCollapse', 'true'); localStorage.setItem('fullPanelCollapse', 'true');
panelRef.current?.collapse(); panelRef.current?.collapse();
}, }, []);
id: 'hide-panel',
});
return links; const Links = useSideNavLinks({ hidePanel, assistants, keyProvided, endpoint });
}, [assistants, keyProvided]);
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
const throttledSaveLayout = useCallback( const throttledSaveLayout = useCallback(
@ -206,18 +181,15 @@ const SidePanel = ({
: 'opacity-100', : 'opacity-100',
)} )}
> >
{keyProvided && (
<div <div
className={cn( className={cn(
'sticky left-0 right-0 top-0 z-[100] flex h-[52px] flex-wrap items-center justify-center bg-white dark:bg-gray-850', 'sticky left-0 right-0 top-0 z-[100] flex h-[52px] flex-wrap items-center justify-center bg-white dark:bg-gray-850',
isCollapsed ? 'h-[52px]' : 'px-2', isCollapsed ? 'h-[52px]' : 'px-2',
)} )}
> >
<Switcher isCollapsed={isCollapsed} /> <Switcher isCollapsed={isCollapsed} endpointKeyProvided={keyProvided} />
<Separator className="bg-gray-100/50 dark:bg-gray-600" /> <Separator className="bg-gray-100/50 dark:bg-gray-600" />
</div> </div>
)}
<Nav <Nav
resize={panelRef.current?.resize} resize={panelRef.current?.resize}
isCollapsed={isCollapsed} isCollapsed={isCollapsed}

View file

@ -1,104 +1,20 @@
import { useEffect } from 'react'; import { EModelEndpoint } from 'librechat-data-provider';
import { import type { SwitcherProps } from '~/common';
Select, import AssistantSwitcher from './AssistantSwitcher';
SelectContent, import { useChatContext } from '~/Providers';
SelectItem, import ModelSwitcher from './ModelSwitcher';
SelectTrigger,
SelectValue,
} from '~/components/ui/Select';
import { EModelEndpoint, defaultOrderQuery } from 'librechat-data-provider';
import { useSetIndexOptions, useSelectAssistant, useLocalize } from '~/hooks';
import { useChatContext, useAssistantsMapContext } from '~/Providers';
import { useListAssistantsQuery } from '~/data-provider';
import Icon from '~/components/Endpoints/Icon';
import { cn } from '~/utils';
interface SwitcherProps { export default function Switcher(props: SwitcherProps) {
isCollapsed: boolean; const { conversation } = useChatContext();
const { endpoint } = conversation ?? {};
if (!props.endpointKeyProvided) {
return null;
} }
export default function Switcher({ isCollapsed }: SwitcherProps) { if (endpoint === EModelEndpoint.assistants) {
const localize = useLocalize(); return <AssistantSwitcher {...props} />;
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 <ModelSwitcher {...props} />;
return;
}
setOption('model')(assistant.model);
setOption('assistant_id')(assistant_id);
}
}, [index, assistants, selectedAssistant, assistantMap, endpoint, setOption]);
const currentAssistant = assistantMap?.[selectedAssistant ?? ''];
return (
<Select value={selectedAssistant as string | undefined} onValueChange={onSelect}>
<SelectTrigger
className={cn(
'flex items-center gap-2 [&>span]:line-clamp-1 [&>span]:flex [&>span]:w-full [&>span]:items-center [&>span]:gap-1 [&>span]:truncate [&_svg]:h-4 [&_svg]:w-4 [&_svg]:shrink-0',
isCollapsed
? 'flex h-9 w-9 shrink-0 items-center justify-center p-0 [&>span]:w-auto [&>svg]:hidden'
: '',
'bg-white text-black hover:bg-gray-50 dark:bg-gray-850 dark:text-white',
)}
aria-label={localize('com_sidepanel_select_assistant')}
>
<SelectValue placeholder={localize('com_sidepanel_select_assistant')}>
<div className="assistant-item flex items-center justify-center overflow-hidden rounded-full">
<Icon
isCreatedByUser={false}
endpoint={EModelEndpoint.assistants}
assistantName={currentAssistant?.name ?? ''}
iconURL={(currentAssistant?.metadata?.avatar as string) ?? ''}
/>
</div>
<span className={cn('ml-2', isCollapsed ? 'hidden' : '')} style={{ userSelect: 'none' }}>
{assistants.find((assistant) => assistant.id === selectedAssistant)?.name ??
localize('com_sidepanel_select_assistant')}
</span>
</SelectValue>
</SelectTrigger>
<SelectContent className="bg-white dark:bg-gray-800">
{assistants.map((assistant) => (
<SelectItem
key={assistant.id}
value={assistant.id}
className="hover:bg-gray-50 dark:text-white"
>
<div className="[&_svg]:text-foreground flex items-center justify-center gap-3 dark:text-white [&_svg]:h-4 [&_svg]:w-4 [&_svg]:shrink-0">
<div className="assistant-item overflow-hidden rounded-full ">
<Icon
isCreatedByUser={false}
endpoint={EModelEndpoint.assistants}
assistantName={assistant.name ?? ''}
iconURL={(assistant.metadata?.avatar as string) ?? ''}
/>
</div>
{assistant.name}
</div>
</SelectItem>
))}
</SelectContent>
</Select>
);
} }

View file

@ -0,0 +1,168 @@
import { startTransition, useMemo } from 'react';
import * as RadixSelect from '@radix-ui/react-select';
import { Search as SearchIcon } from 'lucide-react';
import { CheckIcon, ChevronDownIcon } from '@radix-ui/react-icons';
import {
Combobox,
ComboboxItem,
ComboboxList,
ComboboxProvider,
ComboboxCancel,
} from '@ariakit/react';
import type { OptionWithIcon } from '~/common';
import { SelectTrigger, SelectValue } from './Select';
import useCombobox from '~/hooks/Input/useCombobox';
import { cn } from '~/utils';
export default function ComboboxComponent({
selectedValue,
displayValue,
items,
setValue,
ariaLabel,
searchPlaceholder,
selectPlaceholder,
isCollapsed,
SelectIcon,
}: {
ariaLabel: string;
displayValue?: string;
selectedValue: string;
searchPlaceholder?: string;
selectPlaceholder?: string;
items: OptionWithIcon[] | string[];
setValue: (value: string) => void;
isCollapsed: boolean;
SelectIcon?: React.ReactNode;
}) {
const options: OptionWithIcon[] = useMemo(() => {
if (!items) {
return [];
}
return items.map((option: string | OptionWithIcon) => {
if (typeof option === 'string') {
return { label: option, value: option };
}
return option;
});
}, [items]);
const { open, setOpen, setSearchValue, matches } = useCombobox({
value: selectedValue,
options,
});
return (
<RadixSelect.Root
value={selectedValue}
onValueChange={setValue}
open={open}
onOpenChange={setOpen}
>
<ComboboxProvider
open={open}
setOpen={setOpen}
resetValueOnHide
includesBaseElement={false}
setValue={(value) => {
startTransition(() => {
setSearchValue(value);
});
}}
>
<SelectTrigger
aria-label={ariaLabel}
className={cn(
'flex items-center gap-2 [&>span]:line-clamp-1 [&>span]:flex [&>span]:w-full [&>span]:items-center [&>span]:gap-1 [&>span]:truncate [&_svg]:h-4 [&_svg]:w-4 [&_svg]:shrink-0',
isCollapsed
? 'flex h-9 w-9 shrink-0 items-center justify-center p-0 [&>span]:w-auto [&>svg]:hidden'
: '',
'bg-white text-black hover:bg-gray-50 dark:bg-gray-850 dark:text-white',
)}
>
<SelectValue placeholder={selectPlaceholder}>
<div className="assistant-item flex items-center justify-center overflow-hidden rounded-full">
{SelectIcon ? SelectIcon : <ChevronDownIcon />}
</div>
<span
className={cn('ml-2', isCollapsed ? 'hidden' : '')}
style={{ userSelect: 'none' }}
>
{selectedValue
? displayValue ?? selectedValue
: selectPlaceholder && selectPlaceholder}
</span>
</SelectValue>
</SelectTrigger>
<RadixSelect.Portal>
<RadixSelect.Content
role="dialog"
aria-label={ariaLabel + 's'}
position="popper"
sideOffset={4}
alignOffset={-16}
className={cn(
'bg-popover text-popover-foreground relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border border-gray-200 shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:border-gray-600',
'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
'bg-white dark:bg-gray-700',
)}
>
<div className="group sticky left-0 top-0 z-10 flex h-12 items-center gap-2 bg-gradient-to-b from-white from-65% to-transparent px-2 px-3 py-2 text-black transition-colors duration-300 focus:bg-gradient-to-b focus:from-white focus:to-white/50 dark:from-gray-700 dark:to-transparent dark:text-white dark:focus:from-white/10 dark:focus:to-white/20">
<SearchIcon className="h-4 w-4 text-gray-500 transition-colors duration-300 dark:group-focus-within:text-gray-300 dark:group-hover:text-gray-300" />
<Combobox
autoSelect
placeholder={searchPlaceholder}
className="flex-1 rounded-md border-none bg-transparent px-2.5 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-gray-700/10 dark:focus:ring-gray-200/10"
// Ariakit's Combobox manually triggers a blur event on virtually
// blurred items, making them work as if they had actual DOM
// focus. These blur events might happen after the corresponding
// focus events in the capture phase, leading Radix Select to
// close the popover. This happens because Radix Select relies on
// the order of these captured events to discern if the focus was
// outside the element. Since we don't have access to the
// onInteractOutside prop in the Radix SelectContent component to
// stop this behavior, we can turn off Ariakit's behavior here.
onBlurCapture={(event) => {
event.preventDefault();
event.stopPropagation();
}}
/>
<ComboboxCancel
hideWhenEmpty={true}
className="relative flex h-5 w-5 items-center justify-end text-gray-500 transition-colors duration-300 dark:group-focus-within:text-gray-300 dark:group-hover:text-gray-300"
/>
</div>
<ComboboxList className="overflow-y-auto p-1 py-2">
{matches.map(({ label, value, icon }) => (
<RadixSelect.Item key={value} value={`${value ?? ''}`} asChild>
<ComboboxItem
className={cn(
'focus:bg-accent focus:text-accent-foreground relative flex w-full cursor-pointer select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
'rounded-lg hover:bg-gray-100/50 hover:bg-gray-50 dark:text-white dark:hover:bg-gray-600',
)}
>
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
<RadixSelect.ItemIndicator>
<CheckIcon className="h-4 w-4" />
</RadixSelect.ItemIndicator>
</span>
<RadixSelect.ItemText>
<div className="[&_svg]:text-foreground flex items-center justify-center gap-3 dark:text-white [&_svg]:h-4 [&_svg]:w-4 [&_svg]:shrink-0">
<div className="assistant-item overflow-hidden rounded-full ">
{icon && icon}
</div>
{label}
</div>
</RadixSelect.ItemText>
</ComboboxItem>
</RadixSelect.Item>
))}
</ComboboxList>
</RadixSelect.Content>
</RadixSelect.Portal>
</ComboboxProvider>
</RadixSelect.Root>
);
}

View file

@ -1,8 +1,8 @@
'use client'; 'use client';
import * as React from 'react'; import * as React from 'react';
import { CaretSortIcon, CheckIcon, ChevronDownIcon, ChevronUpIcon } from '@radix-ui/react-icons';
import * as SelectPrimitive from '@radix-ui/react-select'; import * as SelectPrimitive from '@radix-ui/react-select';
import { CaretSortIcon, CheckIcon, ChevronDownIcon, ChevronUpIcon } from '@radix-ui/react-icons';
import { cn } from '~/utils'; import { cn } from '~/utils';
@ -39,7 +39,10 @@ const SelectScrollUpButton = React.forwardRef<
>(({ className = '', ...props }, ref) => ( >(({ className = '', ...props }, ref) => (
<SelectPrimitive.ScrollUpButton <SelectPrimitive.ScrollUpButton
ref={ref} ref={ref}
className={cn('flex cursor-default items-center justify-center py-1', className)} className={cn(
'flex cursor-default items-center justify-center py-1 dark:text-white',
className,
)}
{...props} {...props}
> >
<ChevronUpIcon /> <ChevronUpIcon />
@ -53,7 +56,10 @@ const SelectScrollDownButton = React.forwardRef<
>(({ className = '', ...props }, ref) => ( >(({ className = '', ...props }, ref) => (
<SelectPrimitive.ScrollDownButton <SelectPrimitive.ScrollDownButton
ref={ref} ref={ref}
className={cn('flex cursor-default items-center justify-center py-1', className)} className={cn(
'flex cursor-default items-center justify-center py-1 dark:text-white',
className,
)}
{...props} {...props}
> >
<ChevronDownIcon /> <ChevronDownIcon />

View file

@ -20,11 +20,12 @@ export * from './Templates';
export * from './Textarea'; export * from './Textarea';
export * from './TextareaAutosize'; export * from './TextareaAutosize';
export * from './Tooltip'; export * from './Tooltip';
export { default as Combobox } from './Combobox';
export { default as Dropdown } from './Dropdown'; export { default as Dropdown } from './Dropdown';
export { default as FileUpload } from './FileUpload'; export { default as FileUpload } from './FileUpload';
export { default as DelayedRender } from './DelayedRender'; export { default as DelayedRender } from './DelayedRender';
export { default as ThemeSelector } from './ThemeSelector';
export { default as SelectDropDown } from './SelectDropDown'; export { default as SelectDropDown } from './SelectDropDown';
export { default as MultiSelectPop } from './MultiSelectPop'; export { default as MultiSelectPop } from './MultiSelectPop';
export { default as SelectDropDownPop } from './SelectDropDownPop'; export { default as SelectDropDownPop } from './SelectDropDownPop';
export { default as MultiSelectDropDown } from './MultiSelectDropDown'; export { default as MultiSelectDropDown } from './MultiSelectDropDown';
export { default as ThemeSelector } from './ThemeSelector';

View file

@ -1,3 +1,4 @@
export { default as usePresets } from './usePresets'; export { default as usePresets } from './usePresets';
export { default as useGetSender } from './useGetSender'; export { default as useGetSender } from './useGetSender';
export { default as useDebouncedInput } from './useDebouncedInput'; export { default as useDebouncedInput } from './useDebouncedInput';
export { default as useParameterEffects } from './useParameterEffects';

View file

@ -2,29 +2,30 @@ import debounce from 'lodash/debounce';
import React, { useState, useCallback } from 'react'; import React, { useState, useCallback } from 'react';
import type { SetterOrUpdater } from 'recoil'; import type { SetterOrUpdater } from 'recoil';
import type { TSetOption } from '~/common'; import type { TSetOption } from '~/common';
import { defaultDebouncedDelay } from '~/common';
/** A custom hook that accepts a setOption function and an option key (e.g., 'title'). /** A custom hook that accepts a setOption function and an option key (e.g., 'title').
It manages a local state for the option value, a debounced setter function for that value, It manages a local state for the option value, a debounced setter function for that value,
and returns the local state value, its setter, and an onChange handler suitable for inputs. */ and returns the local state value, its setter, and an onChange handler suitable for inputs. */
function useDebouncedInput({ function useDebouncedInput<T = unknown>({
setOption, setOption,
setter, setter,
optionKey, optionKey,
initialValue, initialValue,
delay = 450, delay = defaultDebouncedDelay,
}: { }: {
setOption?: TSetOption; setOption?: TSetOption;
setter?: SetterOrUpdater<string>; setter?: SetterOrUpdater<T>;
optionKey?: string | number; optionKey?: string | number;
initialValue: unknown; initialValue: T;
delay?: number; delay?: number;
}): [ }): [
(e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement> | unknown) => void, (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement> | T) => void,
unknown, T,
SetterOrUpdater<string>, SetterOrUpdater<T>,
// (newValue: string) => void, // (newValue: string) => void,
] { ] {
const [value, setValue] = useState(initialValue); const [value, setValue] = useState<T>(initialValue);
/** A debounced function to call the passed setOption with the optionKey and new value. /** A debounced function to call the passed setOption with the optionKey and new value.
* *
@ -36,11 +37,12 @@ function useDebouncedInput({
/** An onChange handler that updates the local state and the debounced option */ /** An onChange handler that updates the local state and the debounced option */
const onChange = useCallback( const onChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement> | unknown) => { (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement> | T) => {
const newValue: unknown = const newValue: T =
typeof e !== 'object' typeof e !== 'object'
? e ? e
: (e as React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>)?.target.value; : ((e as React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>)?.target
.value as unknown as T);
setValue(newValue); setValue(newValue);
setDebouncedOption(newValue); setDebouncedOption(newValue);
}, },

View file

@ -0,0 +1,68 @@
import { useEffect, useRef } from 'react';
import type { DynamicSettingProps, TConversation, TPreset } from 'librechat-data-provider';
import { defaultDebouncedDelay } from '~/common';
function useParameterEffects<T = unknown>({
preset,
settingKey,
defaultValue,
conversation,
inputValue,
setInputValue,
preventDelayedUpdate = false,
}: Pick<DynamicSettingProps, 'settingKey' | 'defaultValue'> & {
preset: TPreset | null;
conversation: TConversation | { conversationId: null } | null;
inputValue: T;
setInputValue: (inputValue: T) => void;
preventDelayedUpdate?: boolean;
}) {
const idRef = useRef<string | null>(null);
const presetIdRef = useRef<string | null>(null);
/** Updates the local state inputValue if global (conversation) is updated elsewhere */
useEffect(() => {
if (preventDelayedUpdate) {
return;
}
const timeout = setTimeout(() => {
if (conversation?.[settingKey] === inputValue) {
return;
}
setInputValue(conversation?.[settingKey]);
}, defaultDebouncedDelay * 1.25);
return () => clearTimeout(timeout);
}, [setInputValue, preventDelayedUpdate, conversation, inputValue, settingKey]);
/** Resets the local state if conversationId changed */
useEffect(() => {
if (!conversation?.conversationId) {
return;
}
if (idRef.current === conversation?.conversationId) {
return;
}
idRef.current = conversation?.conversationId;
setInputValue(defaultValue as T);
}, [setInputValue, conversation?.conversationId, defaultValue]);
/** Resets the local state if presetId changed */
useEffect(() => {
if (!preset?.presetId) {
return;
}
if (presetIdRef.current === preset?.presetId) {
return;
}
presetIdRef.current = preset?.presetId;
setInputValue(defaultValue as T);
}, [setInputValue, preset?.presetId, defaultValue]);
}
export default useParameterEffects;

View file

@ -1,5 +1,6 @@
export { default as useUserKey } from './useUserKey'; export { default as useUserKey } from './useUserKey';
export { default as useDebounce } from './useDebounce'; export { default as useDebounce } from './useDebounce';
export { default as useTextarea } from './useTextarea'; export { default as useTextarea } from './useTextarea';
export { default as useCombobox } from './useCombobox';
export { default as useRequiresKey } from './useRequiresKey'; export { default as useRequiresKey } from './useRequiresKey';
export { default as useMultipleKeys } from './useMultipleKeys'; export { default as useMultipleKeys } from './useMultipleKeys';

View file

@ -0,0 +1,37 @@
import { useMemo, useState } from 'react';
import { matchSorter } from 'match-sorter';
import type { OptionWithIcon } from '~/common';
export default function useCombobox({
value,
options,
}: {
value: string;
options: OptionWithIcon[];
}) {
const [open, setOpen] = useState(false);
const [searchValue, setSearchValue] = useState('');
const matches = useMemo(() => {
if (!searchValue) {
return options;
}
const keys = ['label', 'value'];
const matches = matchSorter(options, searchValue, { keys });
// Radix Select does not work if we don't render the selected item, so we
// make sure to include it in the list of matches.
const selectedItem = options.find((currentItem) => currentItem.value === value);
if (selectedItem && !matches.includes(selectedItem)) {
matches.push(selectedItem);
}
return matches;
}, [searchValue, value, options]);
return {
open,
setOpen,
searchValue,
setSearchValue,
matches,
};
}

View file

@ -0,0 +1,71 @@
import { useMemo } from 'react';
import {
ArrowRightToLine,
// Settings2,
} from 'lucide-react';
import { EModelEndpoint } from 'librechat-data-provider';
import type { TConfig } from 'librechat-data-provider';
import type { NavLink } from '~/common';
import PanelSwitch from '~/components/SidePanel/Builder/PanelSwitch';
// import Parameters from '~/components/SidePanel/Parameters/Panel';
import FilesPanel from '~/components/SidePanel/Files/Panel';
import { Blocks, AttachmentIcon } from '~/components/svg';
export default function useSideNavLinks({
hidePanel,
assistants,
keyProvided,
endpoint,
}: {
hidePanel: () => void;
assistants?: TConfig | null;
keyProvided: boolean;
endpoint?: EModelEndpoint | null;
}) {
const Links = useMemo(() => {
const links: NavLink[] = [];
// if (endpoint !== EModelEndpoint.assistants) {
// links.push({
// title: 'com_sidepanel_parameters',
// label: '',
// icon: Settings2,
// id: 'parameters',
// Component: Parameters,
// });
// }
if (
endpoint === EModelEndpoint.assistants &&
assistants &&
assistants.disableBuilder !== true &&
keyProvided
) {
links.push({
title: 'com_sidepanel_assistant_builder',
label: '',
icon: Blocks,
id: 'assistants',
Component: PanelSwitch,
});
}
links.push({
title: 'com_sidepanel_attach_files',
label: '',
icon: AttachmentIcon,
id: 'files',
Component: FilesPanel,
});
links.push({
title: 'com_sidepanel_hide_panel',
label: '',
icon: ArrowRightToLine,
onClick: hidePanel,
id: 'hide-panel',
});
return links;
}, [assistants, keyProvided, hidePanel, endpoint]);
return Links;
}

View file

@ -1,4 +1,4 @@
import { useCallback } from 'react'; import { useCallback, useRef } from 'react';
import { EModelEndpoint, FileSources, defaultOrderQuery } from 'librechat-data-provider'; import { EModelEndpoint, FileSources, defaultOrderQuery } from 'librechat-data-provider';
import { useGetEndpointsQuery, useGetModelsQuery } from 'librechat-data-provider/react-query'; import { useGetEndpointsQuery, useGetModelsQuery } from 'librechat-data-provider/react-query';
import { import {
@ -24,6 +24,7 @@ import {
import { useDeleteFilesMutation, useListAssistantsQuery } from '~/data-provider'; import { useDeleteFilesMutation, useListAssistantsQuery } from '~/data-provider';
import useOriginNavigate from './useOriginNavigate'; import useOriginNavigate from './useOriginNavigate';
import useSetStorage from './useSetStorage'; import useSetStorage from './useSetStorage';
import { mainTextareaId } from '~/common';
import store from '~/store'; import store from '~/store';
const useNewConvo = (index = 0) => { const useNewConvo = (index = 0) => {
@ -36,6 +37,7 @@ const useNewConvo = (index = 0) => {
const resetLatestMessage = useResetRecoilState(store.latestMessageFamily(index)); const resetLatestMessage = useResetRecoilState(store.latestMessageFamily(index));
const { data: endpointsConfig = {} as TEndpointsConfig } = useGetEndpointsQuery(); const { data: endpointsConfig = {} as TEndpointsConfig } = useGetEndpointsQuery();
const modelsQuery = useGetModelsQuery(); const modelsQuery = useGetModelsQuery();
const timeoutIdRef = useRef<NodeJS.Timeout>();
const { data: assistants = [] } = useListAssistantsQuery(defaultOrderQuery, { const { data: assistants = [] } = useListAssistantsQuery(defaultOrderQuery, {
select: (res) => select: (res) =>
@ -137,6 +139,14 @@ const useNewConvo = (index = 0) => {
} }
navigate('new'); navigate('new');
} }
clearTimeout(timeoutIdRef.current);
timeoutIdRef.current = setTimeout(() => {
const textarea = document.getElementById(mainTextareaId);
if (textarea) {
textarea.focus();
}
}, 150);
}, },
[endpointsConfig, defaultPreset, assistants, modelsQuery.data], [endpointsConfig, defaultPreset, assistants, modelsQuery.data],
); );

View file

@ -9,7 +9,7 @@ const useOriginNavigate = () => {
return; return;
} }
const path = location.pathname.match(/^\/[^/]+\//); const path = location.pathname.match(/^\/[^/]+\//);
_navigate(`${path ? path[0] : '/chat/'}${url}`, opts); _navigate(`${path ? path[0] : '/c/'}${url}`, opts);
}; };
return navigate; return navigate;

View file

@ -7,6 +7,7 @@ export default {
com_files_filter: 'Filter files...', com_files_filter: 'Filter files...',
com_files_number_selected: '{0} of {1} file(s) selected', com_files_number_selected: '{0} of {1} file(s) selected',
com_sidepanel_select_assistant: 'Select an Assistant', com_sidepanel_select_assistant: 'Select an Assistant',
com_sidepanel_parameters: 'Parameters',
com_sidepanel_assistant_builder: 'Assistant Builder', com_sidepanel_assistant_builder: 'Assistant Builder',
com_sidepanel_hide_panel: 'Hide Panel', com_sidepanel_hide_panel: 'Hide Panel',
com_sidepanel_attach_files: 'Attach Files', com_sidepanel_attach_files: 'Attach Files',
@ -68,6 +69,10 @@ export default {
'May occasionally produce harmful instructions or biased content', 'May occasionally produce harmful instructions or biased content',
com_ui_limitation_limited_2021: 'Limited knowledge of world and events after 2021', com_ui_limitation_limited_2021: 'Limited knowledge of world and events after 2021',
com_ui_experimental: 'Experimental Features', com_ui_experimental: 'Experimental Features',
com_ui_on: 'On',
com_ui_off: 'Off',
com_ui_yes: 'Yes',
com_ui_no: 'No',
com_ui_ascending: 'Asc', com_ui_ascending: 'Asc',
com_ui_descending: 'Desc', com_ui_descending: 'Desc',
com_ui_show_all: 'Show All', com_ui_show_all: 'Show All',
@ -261,7 +266,7 @@ export default {
'Resend all previously attached files. Note: this will increase token cost and you may experience errors with many attachments.', 'Resend all previously attached files. Note: this will increase token cost and you may experience errors with many attachments.',
com_endpoint_openai_detail: com_endpoint_openai_detail:
'The resolution for Vision requests. "Low" is cheaper and faster, "High" is more detailed and expensive, and "Auto" will automatically choose between the two based on the image resolution.', 'The resolution for Vision requests. "Low" is cheaper and faster, "High" is more detailed and expensive, and "Auto" will automatically choose between the two based on the image resolution.',
com_endpoint_openai_custom_name_placeholder: 'Set a custom name for ChatGPT', com_endpoint_openai_custom_name_placeholder: 'Set a custom name for the AI',
com_endpoint_openai_prompt_prefix_placeholder: com_endpoint_openai_prompt_prefix_placeholder:
'Set custom instructions to include in System Message. Default: none', 'Set custom instructions to include in System Message. Default: none',
com_endpoint_anthropic_temp: com_endpoint_anthropic_temp:

View file

@ -60,3 +60,7 @@ export const optionText =
export const defaultTextPropsLabel = export const defaultTextPropsLabel =
'rounded-md border border-gray-300 bg-transparent text-sm shadow-[0_0_10px_rgba(0,0,0,0.10)] outline-none placeholder:text-gray-400 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-700 dark:bg-gray-700 dark:text-gray-50 dark:shadow-[0_0_15px_rgba(0,0,0,0.10)] dark:focus:border-gray-600 dark:focus:outline-none'; 'rounded-md border border-gray-300 bg-transparent text-sm shadow-[0_0_10px_rgba(0,0,0,0.10)] outline-none placeholder:text-gray-400 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-700 dark:bg-gray-700 dark:text-gray-50 dark:shadow-[0_0_15px_rgba(0,0,0,0.10)] dark:focus:border-gray-600 dark:focus:outline-none';
export function capitalizeFirstLetter(string: string) {
return string.charAt(0).toUpperCase() + string.slice(1);
}

413
package-lock.json generated
View file

@ -703,6 +703,7 @@
"version": "0.7.0", "version": "0.7.0",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@ariakit/react": "^0.4.5",
"@dicebear/collection": "^7.0.4", "@dicebear/collection": "^7.0.4",
"@dicebear/core": "^7.0.4", "@dicebear/core": "^7.0.4",
"@headlessui/react": "^1.7.13", "@headlessui/react": "^1.7.13",
@ -741,6 +742,7 @@
"librechat-data-provider": "*", "librechat-data-provider": "*",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"lucide-react": "^0.220.0", "lucide-react": "^0.220.0",
"match-sorter": "^6.3.4",
"rc-input-number": "^7.4.2", "rc-input-number": "^7.4.2",
"react": "^18.2.0", "react": "^18.2.0",
"react-dnd": "^16.0.1", "react-dnd": "^16.0.1",
@ -868,6 +870,41 @@
"undici-types": "~5.26.4" "undici-types": "~5.26.4"
} }
}, },
"node_modules/@ariakit/core": {
"version": "0.4.5",
"resolved": "https://registry.npmjs.org/@ariakit/core/-/core-0.4.5.tgz",
"integrity": "sha512-e294+bEcyzt/H/kO4fS5/czLAlkF7PY+Kul3q2z54VY+GGay8NlVs9UezAB7L4jUBlYRAXwp7/1Sq3R7b+MZ7w=="
},
"node_modules/@ariakit/react": {
"version": "0.4.5",
"resolved": "https://registry.npmjs.org/@ariakit/react/-/react-0.4.5.tgz",
"integrity": "sha512-GUHxaOY1JZrJUHkuV20IY4NWcgknhqTQM0qCQcVZDCi+pJiWchUjTG+UyIr/Of02hU569qnQ7yovskCf+V3tNg==",
"dependencies": {
"@ariakit/react-core": "0.4.5"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/ariakit"
},
"peerDependencies": {
"react": "^17.0.0 || ^18.0.0",
"react-dom": "^17.0.0 || ^18.0.0"
}
},
"node_modules/@ariakit/react-core": {
"version": "0.4.5",
"resolved": "https://registry.npmjs.org/@ariakit/react-core/-/react-core-0.4.5.tgz",
"integrity": "sha512-ciTYPwpj/+mdA+EstveEnoygbx5e4PXQJxfkLKy4lkTkDJJUS9GcbYhdnIFJVUta6P1YFvzkIKo+/y9mcbMKJg==",
"dependencies": {
"@ariakit/core": "0.4.5",
"@floating-ui/dom": "^1.0.0",
"use-sync-external-store": "^1.2.0"
},
"peerDependencies": {
"react": "^17.0.0 || ^18.0.0",
"react-dom": "^17.0.0 || ^18.0.0"
}
},
"node_modules/@aws-crypto/crc32": { "node_modules/@aws-crypto/crc32": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-3.0.0.tgz", "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-3.0.0.tgz",
@ -4933,9 +4970,9 @@
"integrity": "sha512-941kjlHjfI97l6NuH/AwuXV4mHuVnRooDcHNSlzi98hz+4ug3wT4gJcWjSwSZHqeGAEn90lC9sFD+8a9d5Jvxg==" "integrity": "sha512-941kjlHjfI97l6NuH/AwuXV4mHuVnRooDcHNSlzi98hz+4ug3wT4gJcWjSwSZHqeGAEn90lC9sFD+8a9d5Jvxg=="
}, },
"node_modules/@esbuild/aix-ppc64": { "node_modules/@esbuild/aix-ppc64": {
"version": "0.19.12", "version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz",
"integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==", "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
@ -4949,9 +4986,9 @@
} }
}, },
"node_modules/@esbuild/android-arm": { "node_modules/@esbuild/android-arm": {
"version": "0.19.12", "version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz",
"integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==", "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@ -4965,9 +5002,9 @@
} }
}, },
"node_modules/@esbuild/android-arm64": { "node_modules/@esbuild/android-arm64": {
"version": "0.19.12", "version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz",
"integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==", "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -4981,9 +5018,9 @@
} }
}, },
"node_modules/@esbuild/android-x64": { "node_modules/@esbuild/android-x64": {
"version": "0.19.12", "version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz",
"integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==", "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -4997,9 +5034,9 @@
} }
}, },
"node_modules/@esbuild/darwin-arm64": { "node_modules/@esbuild/darwin-arm64": {
"version": "0.19.12", "version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz",
"integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==", "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -5013,9 +5050,9 @@
} }
}, },
"node_modules/@esbuild/darwin-x64": { "node_modules/@esbuild/darwin-x64": {
"version": "0.19.12", "version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz",
"integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -5029,9 +5066,9 @@
} }
}, },
"node_modules/@esbuild/freebsd-arm64": { "node_modules/@esbuild/freebsd-arm64": {
"version": "0.19.12", "version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz",
"integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -5045,9 +5082,9 @@
} }
}, },
"node_modules/@esbuild/freebsd-x64": { "node_modules/@esbuild/freebsd-x64": {
"version": "0.19.12", "version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz",
"integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -5061,9 +5098,9 @@
} }
}, },
"node_modules/@esbuild/linux-arm": { "node_modules/@esbuild/linux-arm": {
"version": "0.19.12", "version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz",
"integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@ -5077,9 +5114,9 @@
} }
}, },
"node_modules/@esbuild/linux-arm64": { "node_modules/@esbuild/linux-arm64": {
"version": "0.19.12", "version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz",
"integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -5093,9 +5130,9 @@
} }
}, },
"node_modules/@esbuild/linux-ia32": { "node_modules/@esbuild/linux-ia32": {
"version": "0.19.12", "version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz",
"integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@ -5109,9 +5146,9 @@
} }
}, },
"node_modules/@esbuild/linux-loong64": { "node_modules/@esbuild/linux-loong64": {
"version": "0.19.12", "version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz",
"integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==",
"cpu": [ "cpu": [
"loong64" "loong64"
], ],
@ -5125,9 +5162,9 @@
} }
}, },
"node_modules/@esbuild/linux-mips64el": { "node_modules/@esbuild/linux-mips64el": {
"version": "0.19.12", "version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz",
"integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==",
"cpu": [ "cpu": [
"mips64el" "mips64el"
], ],
@ -5141,9 +5178,9 @@
} }
}, },
"node_modules/@esbuild/linux-ppc64": { "node_modules/@esbuild/linux-ppc64": {
"version": "0.19.12", "version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz",
"integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
@ -5157,9 +5194,9 @@
} }
}, },
"node_modules/@esbuild/linux-riscv64": { "node_modules/@esbuild/linux-riscv64": {
"version": "0.19.12", "version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz",
"integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==",
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
@ -5173,9 +5210,9 @@
} }
}, },
"node_modules/@esbuild/linux-s390x": { "node_modules/@esbuild/linux-s390x": {
"version": "0.19.12", "version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz",
"integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==",
"cpu": [ "cpu": [
"s390x" "s390x"
], ],
@ -5189,9 +5226,9 @@
} }
}, },
"node_modules/@esbuild/linux-x64": { "node_modules/@esbuild/linux-x64": {
"version": "0.19.12", "version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz",
"integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==", "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -5205,9 +5242,9 @@
} }
}, },
"node_modules/@esbuild/netbsd-x64": { "node_modules/@esbuild/netbsd-x64": {
"version": "0.19.12", "version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz",
"integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -5221,9 +5258,9 @@
} }
}, },
"node_modules/@esbuild/openbsd-x64": { "node_modules/@esbuild/openbsd-x64": {
"version": "0.19.12", "version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz",
"integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -5237,9 +5274,9 @@
} }
}, },
"node_modules/@esbuild/sunos-x64": { "node_modules/@esbuild/sunos-x64": {
"version": "0.19.12", "version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz",
"integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -5253,9 +5290,9 @@
} }
}, },
"node_modules/@esbuild/win32-arm64": { "node_modules/@esbuild/win32-arm64": {
"version": "0.19.12", "version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz",
"integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -5269,9 +5306,9 @@
} }
}, },
"node_modules/@esbuild/win32-ia32": { "node_modules/@esbuild/win32-ia32": {
"version": "0.19.12", "version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz",
"integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@ -5285,9 +5322,9 @@
} }
}, },
"node_modules/@esbuild/win32-x64": { "node_modules/@esbuild/win32-x64": {
"version": "0.19.12", "version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz",
"integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -8165,9 +8202,9 @@
} }
}, },
"node_modules/@rollup/rollup-android-arm-eabi": { "node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.9.6", "version": "4.14.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.9.6.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.14.1.tgz",
"integrity": "sha512-MVNXSSYN6QXOulbHpLMKYi60ppyO13W9my1qogeiAqtjb2yR4LSmfU2+POvDkLzhjYLXz9Rf9+9a3zFHW1Lecg==", "integrity": "sha512-fH8/o8nSUek8ceQnT7K4EQbSiV7jgkHq81m9lWZFIXjJ7lJzpWXbQFpT/Zh6OZYnpFykvzC3fbEvEAFZu03dPA==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@ -8178,9 +8215,9 @@
] ]
}, },
"node_modules/@rollup/rollup-android-arm64": { "node_modules/@rollup/rollup-android-arm64": {
"version": "4.9.6", "version": "4.14.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.9.6.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.14.1.tgz",
"integrity": "sha512-T14aNLpqJ5wzKNf5jEDpv5zgyIqcpn1MlwCrUXLrwoADr2RkWA0vOWP4XxbO9aiO3dvMCQICZdKeDrFl7UMClw==", "integrity": "sha512-Y/9OHLjzkunF+KGEoJr3heiD5X9OLa8sbT1lm0NYeKyaM3oMhhQFvPB0bNZYJwlq93j8Z6wSxh9+cyKQaxS7PQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -8191,9 +8228,9 @@
] ]
}, },
"node_modules/@rollup/rollup-darwin-arm64": { "node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.9.6", "version": "4.14.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.9.6.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.14.1.tgz",
"integrity": "sha512-CqNNAyhRkTbo8VVZ5R85X73H3R5NX9ONnKbXuHisGWC0qRbTTxnF1U4V9NafzJbgGM0sHZpdO83pLPzq8uOZFw==", "integrity": "sha512-+kecg3FY84WadgcuSVm6llrABOdQAEbNdnpi5X3UwWiFVhZIZvKgGrF7kmLguvxHNQy+UuRV66cLVl3S+Rkt+Q==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -8204,9 +8241,9 @@
] ]
}, },
"node_modules/@rollup/rollup-darwin-x64": { "node_modules/@rollup/rollup-darwin-x64": {
"version": "4.9.6", "version": "4.14.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.9.6.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.14.1.tgz",
"integrity": "sha512-zRDtdJuRvA1dc9Mp6BWYqAsU5oeLixdfUvkTHuiYOHwqYuQ4YgSmi6+/lPvSsqc/I0Omw3DdICx4Tfacdzmhog==", "integrity": "sha512-2pYRzEjVqq2TB/UNv47BV/8vQiXkFGVmPFwJb+1E0IFFZbIX8/jo1olxqqMbo6xCXf8kabANhp5bzCij2tFLUA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -8217,9 +8254,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-arm-gnueabihf": { "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.9.6", "version": "4.14.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.9.6.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.14.1.tgz",
"integrity": "sha512-oNk8YXDDnNyG4qlNb6is1ojTOGL/tRhbbKeE/YuccItzerEZT68Z9gHrY3ROh7axDc974+zYAPxK5SH0j/G+QQ==", "integrity": "sha512-mS6wQ6Do6/wmrF9aTFVpIJ3/IDXhg1EZcQFYHZLHqw6AzMBjTHWnCG35HxSqUNphh0EHqSM6wRTT8HsL1C0x5g==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@ -8230,9 +8267,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-arm64-gnu": { "node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.9.6", "version": "4.14.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.9.6.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.14.1.tgz",
"integrity": "sha512-Z3O60yxPtuCYobrtzjo0wlmvDdx2qZfeAWTyfOjEDqd08kthDKexLpV97KfAeUXPosENKd8uyJMRDfFMxcYkDQ==", "integrity": "sha512-p9rGKYkHdFMzhckOTFubfxgyIO1vw//7IIjBBRVzyZebWlzRLeNhqxuSaZ7kCEKVkm/kuC9fVRW9HkC/zNRG2w==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -8243,9 +8280,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-arm64-musl": { "node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.9.6", "version": "4.14.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.9.6.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.14.1.tgz",
"integrity": "sha512-gpiG0qQJNdYEVad+1iAsGAbgAnZ8j07FapmnIAQgODKcOTjLEWM9sRb+MbQyVsYCnA0Im6M6QIq6ax7liws6eQ==", "integrity": "sha512-nDY6Yz5xS/Y4M2i9JLQd3Rofh5OR8Bn8qe3Mv/qCVpHFlwtZSBYSPaU4mrGazWkXrdQ98GB//H0BirGR/SKFSw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -8255,10 +8292,23 @@
"linux" "linux"
] ]
}, },
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
"version": "4.14.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.14.1.tgz",
"integrity": "sha512-im7HE4VBL+aDswvcmfx88Mp1soqL9OBsdDBU8NqDEYtkri0qV0THhQsvZtZeNNlLeCUQ16PZyv7cqutjDF35qw==",
"cpu": [
"ppc64le"
],
"dev": true,
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": { "node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.9.6", "version": "4.14.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.9.6.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.14.1.tgz",
"integrity": "sha512-+uCOcvVmFUYvVDr27aiyun9WgZk0tXe7ThuzoUTAukZJOwS5MrGbmSlNOhx1j80GdpqbOty05XqSl5w4dQvcOA==", "integrity": "sha512-RWdiHuAxWmzPJgaHJdpvUUlDz8sdQz4P2uv367T2JocdDa98iRw2UjIJ4QxSyt077mXZT2X6pKfT2iYtVEvOFw==",
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
@ -8268,10 +8318,23 @@
"linux" "linux"
] ]
}, },
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.14.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.14.1.tgz",
"integrity": "sha512-VMgaGQ5zRX6ZqV/fas65/sUGc9cPmsntq2FiGmayW9KMNfWVG/j0BAqImvU4KTeOOgYSf1F+k6at1UfNONuNjA==",
"cpu": [
"s390x"
],
"dev": true,
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": { "node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.9.6", "version": "4.14.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.9.6.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.14.1.tgz",
"integrity": "sha512-HUNqM32dGzfBKuaDUBqFB7tP6VMN74eLZ33Q9Y1TBqRDn+qDonkAUyKWwF9BR9unV7QUzffLnz9GrnKvMqC/fw==", "integrity": "sha512-9Q7DGjZN+hTdJomaQ3Iub4m6VPu1r94bmK2z3UeWP3dGUecRC54tmVu9vKHTm1bOt3ASoYtEz6JSRLFzrysKlA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -8282,9 +8345,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-x64-musl": { "node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.9.6", "version": "4.14.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.9.6.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.14.1.tgz",
"integrity": "sha512-ch7M+9Tr5R4FK40FHQk8VnML0Szi2KRujUgHXd/HjuH9ifH72GUmw6lStZBo3c3GB82vHa0ZoUfjfcM7JiiMrQ==", "integrity": "sha512-JNEG/Ti55413SsreTguSx0LOVKX902OfXIKVg+TCXO6Gjans/k9O6ww9q3oLGjNDaTLxM+IHFMeXy/0RXL5R/g==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -8295,9 +8358,9 @@
] ]
}, },
"node_modules/@rollup/rollup-win32-arm64-msvc": { "node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.9.6", "version": "4.14.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.9.6.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.14.1.tgz",
"integrity": "sha512-VD6qnR99dhmTQ1mJhIzXsRcTBvTjbfbGGwKAHcu+52cVl15AC/kplkhxzW/uT0Xl62Y/meBKDZvoJSJN+vTeGA==", "integrity": "sha512-ryS22I9y0mumlLNwDFYZRDFLwWh3aKaC72CWjFcFvxK0U6v/mOkM5Up1bTbCRAhv3kEIwW2ajROegCIQViUCeA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -8308,9 +8371,9 @@
] ]
}, },
"node_modules/@rollup/rollup-win32-ia32-msvc": { "node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.9.6", "version": "4.14.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.9.6.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.14.1.tgz",
"integrity": "sha512-J9AFDq/xiRI58eR2NIDfyVmTYGyIZmRcvcAoJ48oDld/NTR8wyiPUu2X/v1navJ+N/FGg68LEbX3Ejd6l8B7MQ==", "integrity": "sha512-TdloItiGk+T0mTxKx7Hp279xy30LspMso+GzQvV2maYePMAWdmrzqSNZhUpPj3CGw12aGj57I026PgLCTu8CGg==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@ -8321,9 +8384,9 @@
] ]
}, },
"node_modules/@rollup/rollup-win32-x64-msvc": { "node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.9.6", "version": "4.14.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.9.6.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.14.1.tgz",
"integrity": "sha512-jqzNLhNDvIZOrt69Ce4UjGRpXJBzhUBzawMwnaDAwyHriki3XollsewxWzOzz+4yOFDkuJHtTsZFwMxhYJWmLQ==", "integrity": "sha512-wQGI+LY/Py20zdUPq+XCem7JcPOyzIJBm3dli+56DJsQOHbnXZFEwgmnC6el1TPAfC8lBT3m+z69RmLykNUbew==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -13198,9 +13261,9 @@
} }
}, },
"node_modules/esbuild": { "node_modules/esbuild": {
"version": "0.19.12", "version": "0.20.2",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz",
"integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==",
"dev": true, "dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"bin": { "bin": {
@ -13210,29 +13273,29 @@
"node": ">=12" "node": ">=12"
}, },
"optionalDependencies": { "optionalDependencies": {
"@esbuild/aix-ppc64": "0.19.12", "@esbuild/aix-ppc64": "0.20.2",
"@esbuild/android-arm": "0.19.12", "@esbuild/android-arm": "0.20.2",
"@esbuild/android-arm64": "0.19.12", "@esbuild/android-arm64": "0.20.2",
"@esbuild/android-x64": "0.19.12", "@esbuild/android-x64": "0.20.2",
"@esbuild/darwin-arm64": "0.19.12", "@esbuild/darwin-arm64": "0.20.2",
"@esbuild/darwin-x64": "0.19.12", "@esbuild/darwin-x64": "0.20.2",
"@esbuild/freebsd-arm64": "0.19.12", "@esbuild/freebsd-arm64": "0.20.2",
"@esbuild/freebsd-x64": "0.19.12", "@esbuild/freebsd-x64": "0.20.2",
"@esbuild/linux-arm": "0.19.12", "@esbuild/linux-arm": "0.20.2",
"@esbuild/linux-arm64": "0.19.12", "@esbuild/linux-arm64": "0.20.2",
"@esbuild/linux-ia32": "0.19.12", "@esbuild/linux-ia32": "0.20.2",
"@esbuild/linux-loong64": "0.19.12", "@esbuild/linux-loong64": "0.20.2",
"@esbuild/linux-mips64el": "0.19.12", "@esbuild/linux-mips64el": "0.20.2",
"@esbuild/linux-ppc64": "0.19.12", "@esbuild/linux-ppc64": "0.20.2",
"@esbuild/linux-riscv64": "0.19.12", "@esbuild/linux-riscv64": "0.20.2",
"@esbuild/linux-s390x": "0.19.12", "@esbuild/linux-s390x": "0.20.2",
"@esbuild/linux-x64": "0.19.12", "@esbuild/linux-x64": "0.20.2",
"@esbuild/netbsd-x64": "0.19.12", "@esbuild/netbsd-x64": "0.20.2",
"@esbuild/openbsd-x64": "0.19.12", "@esbuild/openbsd-x64": "0.20.2",
"@esbuild/sunos-x64": "0.19.12", "@esbuild/sunos-x64": "0.20.2",
"@esbuild/win32-arm64": "0.19.12", "@esbuild/win32-arm64": "0.20.2",
"@esbuild/win32-ia32": "0.19.12", "@esbuild/win32-ia32": "0.20.2",
"@esbuild/win32-x64": "0.19.12" "@esbuild/win32-x64": "0.20.2"
} }
}, },
"node_modules/escalade": { "node_modules/escalade": {
@ -17990,9 +18053,9 @@
} }
}, },
"node_modules/katex": { "node_modules/katex": {
"version": "0.16.9", "version": "0.16.10",
"resolved": "https://registry.npmjs.org/katex/-/katex-0.16.9.tgz", "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.10.tgz",
"integrity": "sha512-fsSYjWS0EEOwvy81j3vRA8TEAhQhKiqO+FQaKWp0m39qwOzHVBgAUBIXWj1pB+O2W3fIpNa6Y9KSKCVbfPhyAQ==", "integrity": "sha512-ZiqaC04tp2O5utMsl2TEZTXxa6WSC4yo0fv5ML++D3QZv/vx2Mct0mTlRx3O+uUkjfuAgOkzsCmq5MiUEsDDdA==",
"funding": [ "funding": [
"https://opencollective.com/katex", "https://opencollective.com/katex",
"https://github.com/sponsors/katex" "https://github.com/sponsors/katex"
@ -19037,6 +19100,20 @@
"url": "https://github.com/sponsors/wooorm" "url": "https://github.com/sponsors/wooorm"
} }
}, },
"node_modules/match-sorter": {
"version": "6.3.4",
"resolved": "https://registry.npmjs.org/match-sorter/-/match-sorter-6.3.4.tgz",
"integrity": "sha512-jfZW7cWS5y/1xswZo8VBOdudUiSd9nifYRWphc9M5D/ee4w4AoXLgBEdRbgVaxbMuagBPeUC5y2Hi8DO6o9aDg==",
"dependencies": {
"@babel/runtime": "^7.23.8",
"remove-accents": "0.5.0"
}
},
"node_modules/match-sorter/node_modules/remove-accents": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.5.0.tgz",
"integrity": "sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A=="
},
"node_modules/md5": { "node_modules/md5": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz",
@ -21896,9 +21973,9 @@
} }
}, },
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.4.35", "version": "8.4.38",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz",
"integrity": "sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==", "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==",
"funding": [ "funding": [
{ {
"type": "opencollective", "type": "opencollective",
@ -21916,7 +21993,7 @@
"dependencies": { "dependencies": {
"nanoid": "^3.3.7", "nanoid": "^3.3.7",
"picocolors": "^1.0.0", "picocolors": "^1.0.0",
"source-map-js": "^1.0.2" "source-map-js": "^1.2.0"
}, },
"engines": { "engines": {
"node": "^10 || ^12 || >=14" "node": "^10 || ^12 || >=14"
@ -25136,9 +25213,9 @@
} }
}, },
"node_modules/source-map-js": { "node_modules/source-map-js": {
"version": "1.0.2", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz",
"integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==",
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@ -27108,14 +27185,14 @@
} }
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "5.1.1", "version": "5.2.8",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.1.1.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.8.tgz",
"integrity": "sha512-wclpAgY3F1tR7t9LL5CcHC41YPkQIpKUGeIuT8MdNwNZr6OqOTLs7JX5vIHAtzqLWXts0T+GDrh9pN2arneKqg==", "integrity": "sha512-OyZR+c1CE8yeHw5V5t59aXsUPPVTHMDjEZz8MgguLL/Q7NblxhZUlTu9xSPqlsUO/y+X7dlU05jdhvyycD55DA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"esbuild": "^0.19.3", "esbuild": "^0.20.1",
"postcss": "^8.4.35", "postcss": "^8.4.38",
"rollup": "^4.2.0" "rollup": "^4.13.0"
}, },
"bin": { "bin": {
"vite": "bin/vite.js" "vite": "bin/vite.js"
@ -27252,9 +27329,9 @@
} }
}, },
"node_modules/vite/node_modules/rollup": { "node_modules/vite/node_modules/rollup": {
"version": "4.9.6", "version": "4.14.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.9.6.tgz", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.14.1.tgz",
"integrity": "sha512-05lzkCS2uASX0CiLFybYfVkwNbKZG5NFQ6Go0VWyogFTXXbR039UVsegViTntkk4OglHBdF54ccApXRRuXRbsg==", "integrity": "sha512-4LnHSdd3QK2pa1J6dFbfm1HN0D7vSK/ZuZTsdyUAlA6Rr1yTouUTL13HaDOGJVgby461AhrNGBS7sCGXXtT+SA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@types/estree": "1.0.5" "@types/estree": "1.0.5"
@ -27267,19 +27344,21 @@
"npm": ">=8.0.0" "npm": ">=8.0.0"
}, },
"optionalDependencies": { "optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.9.6", "@rollup/rollup-android-arm-eabi": "4.14.1",
"@rollup/rollup-android-arm64": "4.9.6", "@rollup/rollup-android-arm64": "4.14.1",
"@rollup/rollup-darwin-arm64": "4.9.6", "@rollup/rollup-darwin-arm64": "4.14.1",
"@rollup/rollup-darwin-x64": "4.9.6", "@rollup/rollup-darwin-x64": "4.14.1",
"@rollup/rollup-linux-arm-gnueabihf": "4.9.6", "@rollup/rollup-linux-arm-gnueabihf": "4.14.1",
"@rollup/rollup-linux-arm64-gnu": "4.9.6", "@rollup/rollup-linux-arm64-gnu": "4.14.1",
"@rollup/rollup-linux-arm64-musl": "4.9.6", "@rollup/rollup-linux-arm64-musl": "4.14.1",
"@rollup/rollup-linux-riscv64-gnu": "4.9.6", "@rollup/rollup-linux-powerpc64le-gnu": "4.14.1",
"@rollup/rollup-linux-x64-gnu": "4.9.6", "@rollup/rollup-linux-riscv64-gnu": "4.14.1",
"@rollup/rollup-linux-x64-musl": "4.9.6", "@rollup/rollup-linux-s390x-gnu": "4.14.1",
"@rollup/rollup-win32-arm64-msvc": "4.9.6", "@rollup/rollup-linux-x64-gnu": "4.14.1",
"@rollup/rollup-win32-ia32-msvc": "4.9.6", "@rollup/rollup-linux-x64-musl": "4.14.1",
"@rollup/rollup-win32-x64-msvc": "4.9.6", "@rollup/rollup-win32-arm64-msvc": "4.14.1",
"@rollup/rollup-win32-ia32-msvc": "4.14.1",
"@rollup/rollup-win32-x64-msvc": "4.14.1",
"fsevents": "~2.3.2" "fsevents": "~2.3.2"
} }
}, },
@ -28080,7 +28159,7 @@
}, },
"packages/data-provider": { "packages/data-provider": {
"name": "librechat-data-provider", "name": "librechat-data-provider",
"version": "0.5.1", "version": "0.5.2",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@types/js-yaml": "^4.0.9", "@types/js-yaml": "^4.0.9",

View file

@ -0,0 +1,525 @@
/* eslint-disable jest/no-conditional-expect */
import { ZodError, z } from 'zod';
import { generateDynamicSchema, validateSettingDefinitions } from '../src/generate';
import type { SettingsConfiguration } from '../src/generate';
describe('generateDynamicSchema', () => {
it('should generate a schema for number settings with range', () => {
const settings: SettingsConfiguration = [
{
key: 'testNumber',
description: 'A test number setting',
type: 'number',
default: 5,
range: { min: 1, max: 10, step: 1 },
component: 'slider',
optionType: 'conversation',
columnSpan: 2,
label: 'Test Number Slider',
},
];
const schema = generateDynamicSchema(settings);
const result = schema.safeParse({ testNumber: 6 });
expect(result.success).toBeTruthy();
expect(result['data']).toEqual({ testNumber: 6 });
});
it('should generate a schema for boolean settings', () => {
const settings: SettingsConfiguration = [
{
key: 'testBoolean',
description: 'A test boolean setting',
type: 'boolean',
default: true,
component: 'switch',
optionType: 'model', // Only if relevant to your application's context
columnSpan: 1,
label: 'Test Boolean Switch',
},
];
const schema = generateDynamicSchema(settings);
const result = schema.safeParse({ testBoolean: false });
expect(result.success).toBeTruthy();
expect(result['data']).toEqual({ testBoolean: false });
});
it('should generate a schema for string settings', () => {
const settings: SettingsConfiguration = [
{
key: 'testString',
description: 'A test string setting',
type: 'string',
default: 'default value',
component: 'input',
optionType: 'model', // Optional and only if relevant
columnSpan: 3,
label: 'Test String Input',
placeholder: 'Enter text here...',
minText: 0, // Optional
maxText: 100, // Optional
},
];
const schema = generateDynamicSchema(settings);
const result = schema.safeParse({ testString: 'custom value' });
expect(result.success).toBeTruthy();
expect(result['data']).toEqual({ testString: 'custom value' });
});
it('should generate a schema for enum settings', () => {
const settings: SettingsConfiguration = [
{
key: 'testEnum',
description: 'A test enum setting',
type: 'enum',
default: 'option1',
options: ['option1', 'option2', 'option3'],
enumMappings: {
option1: 'First Option',
option2: 'Second Option',
option3: 'Third Option',
},
component: 'dropdown',
columnSpan: 2,
label: 'Test Enum Dropdown',
},
];
const schema = generateDynamicSchema(settings);
const result = schema.safeParse({ testEnum: 'option2' });
expect(result.success).toBeTruthy();
expect(result['data']).toEqual({ testEnum: 'option2' });
});
it('should fail for incorrect enum value', () => {
const settings: SettingsConfiguration = [
{
key: 'testEnum',
description: 'A test enum setting',
type: 'enum',
default: 'option1',
options: ['option1', 'option2', 'option3'],
component: 'dropdown',
},
];
const schema = generateDynamicSchema(settings);
const result = schema.safeParse({ testEnum: 'option4' }); // This option does not exist
expect(result.success).toBeFalsy();
});
});
describe('validateSettingDefinitions', () => {
// Test for valid setting configurations
test('should not throw error for valid settings', () => {
const validSettings: SettingsConfiguration = [
{
key: 'themeColor',
component: 'input',
type: 'string',
default: '#ffffff',
label: 'Theme Color',
columns: 2,
columnSpan: 1,
optionType: 'model',
},
{
key: 'fontSize',
component: 'slider',
type: 'number',
range: { min: 8, max: 36 },
default: 14,
columnSpan: 2,
},
];
expect(() => validateSettingDefinitions(validSettings)).not.toThrow();
});
// Test for incorrectly configured columns
test('should throw error for invalid columns configuration', () => {
const invalidSettings: SettingsConfiguration = [
{
key: 'themeColor',
component: 'input',
type: 'string',
columns: 5,
},
];
expect(() => validateSettingDefinitions(invalidSettings)).toThrow(ZodError);
});
test('should correctly handle columnSpan defaulting based on columns', () => {
const settingsWithColumnAdjustment: SettingsConfiguration = [
{
key: 'fontSize',
component: 'slider',
type: 'number',
columns: 4,
range: { min: 8, max: 14 },
default: 11,
},
];
expect(() => validateSettingDefinitions(settingsWithColumnAdjustment)).not.toThrow();
});
// Test for label defaulting to key if not provided
test('label should default to key if not explicitly set', () => {
const settingsWithDefaultLabel: SettingsConfiguration = [
{ key: 'fontWeight', component: 'dropdown', type: 'string', options: ['normal', 'bold'] },
];
expect(() => validateSettingDefinitions(settingsWithDefaultLabel)).not.toThrow();
expect(settingsWithDefaultLabel[0].label).toBe('fontWeight');
});
// Test for minText and maxText in input/textarea component
test('should throw error for negative minText or maxText', () => {
const settingsWithNegativeTextLimits: SettingsConfiguration = [
{ key: 'biography', component: 'textarea', type: 'string', minText: -1 },
];
expect(() => validateSettingDefinitions(settingsWithNegativeTextLimits)).toThrow(ZodError);
});
// Validate optionType with tConversationSchema
test('should throw error for optionType "conversation" not matching schema', () => {
const settingsWithInvalidConversationOptionType: SettingsConfiguration = [
{ key: 'userAge', component: 'input', type: 'number', optionType: 'conversation' },
];
expect(() => validateSettingDefinitions(settingsWithInvalidConversationOptionType)).toThrow(
ZodError,
);
});
// Test for columnSpan defaulting and label defaulting to key
test('columnSpan defaults based on columns and label defaults to key if not set', () => {
const settings: SettingsConfiguration = [
{
key: 'textSize',
type: 'number',
component: 'slider',
range: { min: 10, max: 20 },
columns: 4,
},
];
validateSettingDefinitions(settings); // Perform validation which also mutates settings with default values
expect(settings[0].columnSpan).toBe(2); // Expects columnSpan to default based on columns
expect(settings[0].label).toBe('textSize'); // Expects label to default to key
});
// Test for errors thrown due to invalid columns value
test('throws error if columns value is out of range', () => {
const settings: SettingsConfiguration = [
{
key: 'themeMode',
type: 'string',
component: 'dropdown',
options: ['dark', 'light'],
columns: 5,
},
];
expect(() => validateSettingDefinitions(settings)).toThrow(ZodError);
});
// Test range validation for slider component
test('slider component range validation', () => {
const settings: SettingsConfiguration = [
{ key: 'volume', type: 'number', component: 'slider' }, // Missing range
];
expect(() => validateSettingDefinitions(settings)).toThrow(ZodError);
});
// Test options validation for enum type in slider component
test('slider component with enum type requires at least 2 options', () => {
const settings: SettingsConfiguration = [
{ key: 'color', type: 'enum', component: 'slider', options: ['red'] }, // Not enough options
];
expect(() => validateSettingDefinitions(settings)).toThrow(ZodError);
});
// Test checkbox component options validation
test('checkbox component must have 1-2 options if options are provided', () => {
const settings: SettingsConfiguration = [
{
key: 'agreeToTerms',
type: 'boolean',
component: 'checkbox',
options: ['Yes', 'No', 'Maybe'],
}, // Too many options
];
expect(() => validateSettingDefinitions(settings)).toThrow(ZodError);
});
// Test dropdown component options validation
test('dropdown component requires at least 2 options', () => {
const settings: SettingsConfiguration = [
{ key: 'country', type: 'enum', component: 'dropdown', options: ['USA'] }, // Not enough options
];
expect(() => validateSettingDefinitions(settings)).toThrow(ZodError);
});
// Validate minText and maxText constraints in input and textarea
test('validate minText and maxText constraints', () => {
const settings: SettingsConfiguration = [
{ key: 'biography', type: 'string', component: 'textarea', minText: 10, maxText: 5 }, // Incorrect minText and maxText
];
expect(() => validateSettingDefinitions(settings)).toThrow(ZodError);
});
// Validate optionType constraint with tConversationSchema
test('validate optionType constraint with tConversationSchema', () => {
const settings: SettingsConfiguration = [
{ key: 'userAge', type: 'number', component: 'input', optionType: 'conversation' }, // No corresponding schema in tConversationSchema
];
expect(() => validateSettingDefinitions(settings)).toThrow(ZodError);
});
// Validate correct handling of boolean settings with default values
test('correct handling of boolean settings with defaults', () => {
const settings: SettingsConfiguration = [
{ key: 'enableFeatureX', type: 'boolean', component: 'switch' }, // Missing default, should default to false
];
validateSettingDefinitions(settings); // This would populate default values where missing
expect(settings[0].default).toBe(false); // Expects default to be false for boolean without explicit default
});
// Validate that number slider without default uses middle of range
test('number slider without default uses middle of range', () => {
const settings: SettingsConfiguration = [
{ key: 'brightness', type: 'number', component: 'slider', range: { min: 0, max: 100 } }, // Missing default
];
validateSettingDefinitions(settings); // This would populate default values where missing
expect(settings[0].default).toBe(50); // Expects default to be midpoint of range
});
});
const settingsConfiguration: SettingsConfiguration = [
{
key: 'temperature',
description:
'Higher values = more random, while lower values = more focused and deterministic. We recommend altering this or Top P but not both.',
type: 'number',
default: 1,
range: {
min: 0,
max: 2,
step: 0.01,
},
component: 'slider',
},
{
key: 'top_p',
description:
'An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered. We recommend altering this or temperature but not both.',
type: 'number',
default: 1,
range: {
min: 0,
max: 1,
step: 0.01,
},
component: 'slider',
},
{
key: 'presence_penalty',
description:
'Number between -2.0 and 2.0. Positive values penalize new tokens based on whether they appear in the text so far, increasing the model\'s likelihood to talk about new topics.',
type: 'number',
default: 0,
range: {
min: -2,
max: 2,
step: 0.01,
},
component: 'slider',
},
{
key: 'frequency_penalty',
description:
'Number between -2.0 and 2.0. Positive values penalize new tokens based on their existing frequency in the text so far, decreasing the model\'s likelihood to repeat the same line verbatim.',
type: 'number',
default: 0,
range: {
min: -2,
max: 2,
step: 0.01,
},
component: 'slider',
},
{
key: 'resendFiles',
description:
'Resend all previously attached files. Note: this will increase token cost and you may experience errors with many attachments.',
type: 'boolean',
default: true,
component: 'switch',
},
{
key: 'imageDetail',
description:
'The resolution for Vision requests. "Low" is cheaper and faster, "High" is more detailed and expensive, and "Auto" will automatically choose between the two based on the image resolution.',
type: 'enum',
default: 'auto',
options: ['low', 'high', 'auto'],
component: 'slider',
},
{
key: 'promptPrefix',
type: 'string',
default: '',
component: 'input',
placeholder: 'Set custom instructions to include in System Message. Default: none',
},
{
key: 'chatGptLabel',
type: 'string',
default: '',
component: 'input',
placeholder: 'Set a custom name for your AI',
},
];
describe('Settings Validation and Schema Generation', () => {
// Test 1: Validate settings definitions do not throw for valid configuration
test('validateSettingDefinitions does not throw for valid configuration', () => {
expect(() => validateSettingDefinitions(settingsConfiguration)).not.toThrow();
});
test('validateSettingDefinitions throws for invalid type in settings', () => {
const settingsWithInvalidType = [
...settingsConfiguration,
{
key: 'newSetting',
description: 'A setting with an unsupported type',
type: 'unsupportedType', // Assuming 'unsupportedType' is not supported
component: 'input',
},
];
expect(() =>
validateSettingDefinitions(settingsWithInvalidType as SettingsConfiguration),
).toThrow();
});
test('validateSettingDefinitions throws for missing required fields', () => {
const settingsMissingRequiredField = [
...settingsConfiguration,
{
key: 'incompleteSetting',
type: 'number',
// Missing 'component',
},
];
expect(() =>
validateSettingDefinitions(settingsMissingRequiredField as SettingsConfiguration),
).toThrow();
});
test('validateSettingDefinitions throws for default value out of range', () => {
const settingsOutOfRange = [
...settingsConfiguration,
{
key: 'rangeTestSetting',
description: 'A setting with default value out of specified range',
type: 'number',
default: 5,
range: {
min: 0,
max: 1,
},
component: 'slider',
},
];
expect(() => validateSettingDefinitions(settingsOutOfRange as SettingsConfiguration)).toThrow();
});
test('validateSettingDefinitions throws for enum setting with incorrect default', () => {
const settingsWithIncorrectEnumDefault = [
...settingsConfiguration,
{
key: 'enumSetting',
description: 'Enum setting with a default not in options',
type: 'enum',
default: 'unlistedOption',
options: ['option1', 'option2'],
component: 'dropdown',
},
];
expect(() =>
validateSettingDefinitions(settingsWithIncorrectEnumDefault as SettingsConfiguration),
).toThrow();
});
// Test 2: Generate dynamic schema and validate correct input
test('generateDynamicSchema generates a schema that validates correct input', () => {
const schema = generateDynamicSchema(settingsConfiguration);
const validInput = {
temperature: 0.5,
top_p: 0.8,
presence_penalty: 1,
frequency_penalty: -1,
resendFiles: true,
imageDetail: 'high',
promptPrefix: 'Hello, AI.',
chatGptLabel: 'My Custom AI',
};
expect(schema.parse(validInput)).toEqual(validInput);
});
// Test 3: Generate dynamic schema and catch invalid input
test('generateDynamicSchema generates a schema that catches invalid input and provides detailed errors', async () => {
const schema = generateDynamicSchema(settingsConfiguration);
const invalidInput: z.infer<typeof schema> = {
temperature: 2.5, // Out of range
top_p: -0.5, // Out of range
presence_penalty: 3, // Out of range
frequency_penalty: -3, // Out of range
resendFiles: 'yes', // Wrong type
imageDetail: 'ultra', // Invalid option
promptPrefix: 123, // Wrong type
chatGptLabel: true, // Wrong type
};
const result = schema.safeParse(invalidInput);
expect(result.success).toBeFalsy();
if (!result.success) {
const errorPaths = result.error.issues.map((issue) => issue.path.join('.'));
expect(errorPaths).toContain('temperature');
expect(errorPaths).toContain('top_p');
expect(errorPaths).toContain('presence_penalty');
expect(errorPaths).toContain('frequency_penalty');
expect(errorPaths).toContain('resendFiles');
expect(errorPaths).toContain('imageDetail');
expect(errorPaths).toContain('promptPrefix');
expect(errorPaths).toContain('chatGptLabel');
}
});
});

View file

@ -0,0 +1,474 @@
import { z, ZodError, ZodIssueCode } from 'zod';
import { tConversationSchema, googleSettings as google, openAISettings as openAI } from './schemas';
import type { ZodIssue } from 'zod';
import type { TConversation, TSetOption } from './schemas';
export type GoogleSettings = Partial<typeof google>;
export type OpenAISettings = Partial<typeof google>;
export type ComponentType = 'input' | 'textarea' | 'slider' | 'checkbox' | 'switch' | 'dropdown';
export type OptionType = 'conversation' | 'model' | 'custom';
export enum ComponentTypes {
Input = 'input',
Textarea = 'textarea',
Slider = 'slider',
Checkbox = 'checkbox',
Switch = 'switch',
Dropdown = 'dropdown',
}
export enum OptionTypes {
Conversation = 'conversation',
Model = 'model',
Custom = 'custom',
}
export interface SettingDefinition {
key: string;
description?: string;
type: 'number' | 'boolean' | 'string' | 'enum';
default?: number | boolean | string;
showDefault?: boolean;
options?: string[];
range?: SettingRange;
enumMappings?: Record<string, number | boolean | string>;
component: ComponentType;
optionType?: OptionType;
columnSpan?: number;
columns?: number;
label?: string;
placeholder?: string;
labelCode?: boolean;
placeholderCode?: boolean;
descriptionCode?: boolean;
minText?: number;
maxText?: number;
includeInput?: boolean; // Specific to slider component
}
export type DynamicSettingProps = Partial<SettingDefinition> & {
readonly?: boolean;
settingKey: string;
setOption: TSetOption;
defaultValue?: number | boolean | string;
};
const requiredSettingFields = ['key', 'type', 'component'];
export interface SettingRange {
min: number;
max: number;
step?: number;
}
export type SettingsConfiguration = SettingDefinition[];
export function generateDynamicSchema(settings: SettingsConfiguration) {
const schemaFields: { [key: string]: z.ZodTypeAny } = {};
for (const setting of settings) {
const { key, type, default: defaultValue, range, options, minText, maxText } = setting;
if (type === 'number') {
let schema = z.number();
if (range) {
schema = schema.min(range.min);
schema = schema.max(range.max);
}
if (typeof defaultValue === 'number') {
schemaFields[key] = schema.default(defaultValue);
} else {
schemaFields[key] = schema;
}
continue;
}
if (type === 'boolean') {
const schema = z.boolean();
if (typeof defaultValue === 'boolean') {
schemaFields[key] = schema.default(defaultValue);
} else {
schemaFields[key] = schema;
}
continue;
}
if (type === 'string') {
let schema = z.string();
if (minText) {
schema = schema.min(minText);
}
if (maxText) {
schema = schema.max(maxText);
}
if (typeof defaultValue === 'string') {
schemaFields[key] = schema.default(defaultValue);
} else {
schemaFields[key] = schema;
}
continue;
}
if (type === 'enum') {
if (!options || options.length === 0) {
console.warn(`Missing or empty 'options' for enum setting '${key}'.`);
continue;
}
const schema = z.enum(options as [string, ...string[]]);
if (typeof defaultValue === 'string') {
schemaFields[key] = schema.default(defaultValue);
} else {
schemaFields[key] = schema;
}
continue;
}
console.warn(`Unsupported setting type: ${type}`);
}
return z.object(schemaFields);
}
const ZodTypeToSettingType: Record<string, string | undefined> = {
ZodString: 'string',
ZodNumber: 'number',
ZodBoolean: 'boolean',
};
const minColumns = 1;
const maxColumns = 4;
const minSliderOptions = 2;
const minDropdownOptions = 2;
/**
* Validates the provided setting using the constraints unique to each component type.
* @throws {ZodError} Throws a ZodError if any validation fails.
*/
export function validateSettingDefinitions(settings: SettingsConfiguration): void {
const errors: ZodIssue[] = [];
// Validate columns
const columnsSet = new Set<number>();
for (const setting of settings) {
if (setting.columns !== undefined) {
if (setting.columns < minColumns || setting.columns > maxColumns) {
errors.push({
code: ZodIssueCode.custom,
message: `Invalid columns value for setting ${setting.key}. Must be between ${minColumns} and ${maxColumns}.`,
path: ['columns'],
});
} else {
columnsSet.add(setting.columns);
}
}
}
const columns = columnsSet.size === 1 ? columnsSet.values().next().value : 2;
for (const setting of settings) {
for (const field of requiredSettingFields) {
if (setting[field as keyof SettingDefinition] === undefined) {
errors.push({
code: ZodIssueCode.custom,
message: `Missing required field ${field} for setting ${setting.key}.`,
path: [field],
});
}
}
// check accepted types
if (!['number', 'boolean', 'string', 'enum'].includes(setting.type)) {
errors.push({
code: ZodIssueCode.custom,
message: `Invalid type for setting ${setting.key}. Must be one of 'number', 'boolean', 'string', 'enum'.`,
path: ['type'],
});
}
// Predefined constraints based on components
if (setting.component === 'input' || setting.component === 'textarea') {
if (setting.type === 'number' && setting.component === 'textarea') {
errors.push({
code: ZodIssueCode.custom,
message: `Textarea component for setting ${setting.key} must have type string.`,
path: ['type'],
});
// continue;
}
if (
setting.minText !== undefined &&
setting.maxText !== undefined &&
setting.minText > setting.maxText
) {
errors.push({
code: ZodIssueCode.custom,
message: `For setting ${setting.key}, minText cannot be greater than maxText.`,
path: [setting.key, 'minText', 'maxText'],
});
// continue;
}
if (!setting.placeholder) {
setting.placeholder = '';
} // Default placeholder
}
if (setting.component === 'slider') {
if (setting.type === 'number' && !setting.range) {
errors.push({
code: ZodIssueCode.custom,
message: `Slider component for setting ${setting.key} must have a range if type is number.`,
path: ['range'],
});
// continue;
}
if (
setting.type === 'enum' &&
(!setting.options || setting.options.length < minSliderOptions)
) {
errors.push({
code: ZodIssueCode.custom,
message: `Slider component for setting ${setting.key} requires at least ${minSliderOptions} options for enum type.`,
path: ['options'],
});
// continue;
}
setting.includeInput = setting.type === 'number' ? setting.includeInput ?? true : false; // Default to true if type is number
}
if (setting.component === 'slider' && setting.type === 'number') {
if (setting.default === undefined && setting.range) {
// Set default to the middle of the range if unspecified
setting.default = Math.round((setting.range.min + setting.range.max) / 2);
}
}
if (setting.component === 'checkbox' || setting.component === 'switch') {
if (setting.options && setting.options.length > 2) {
errors.push({
code: ZodIssueCode.custom,
message: `Checkbox/Switch component for setting ${setting.key} must have 1-2 options.`,
path: ['options'],
});
// continue;
}
if (!setting.default && setting.type === 'boolean') {
setting.default = false; // Default to false if type is boolean
}
}
if (setting.component === 'dropdown') {
if (!setting.options || setting.options.length < minDropdownOptions) {
errors.push({
code: ZodIssueCode.custom,
message: `Dropdown component for setting ${setting.key} requires at least ${minDropdownOptions} options.`,
path: ['options'],
});
// continue;
}
if (!setting.default && setting.options && setting.options.length > 0) {
setting.default = setting.options[0]; // Default to first option if not specified
}
}
// Default columnSpan
if (!setting.columnSpan) {
setting.columnSpan = Math.floor(columns / 2);
}
// Default label to key
if (!setting.label) {
setting.label = setting.key;
}
// Validate minText and maxText for input/textarea
if (setting.component === 'input' || setting.component === 'textarea') {
if (setting.minText !== undefined && setting.minText < 0) {
errors.push({
code: ZodIssueCode.custom,
message: `Invalid minText value for setting ${setting.key}. Must be non-negative.`,
path: ['minText'],
});
}
if (setting.maxText !== undefined && setting.maxText < 0) {
errors.push({
code: ZodIssueCode.custom,
message: `Invalid maxText value for setting ${setting.key}. Must be non-negative.`,
path: ['maxText'],
});
}
}
// Validate optionType and conversation schema
if (setting.optionType !== OptionTypes.Custom) {
const conversationSchema = tConversationSchema.shape[setting.key as keyof TConversation];
if (!conversationSchema) {
errors.push({
code: ZodIssueCode.custom,
message: `Setting ${setting.key} with optionType "${setting.optionType}" must be defined in tConversationSchema.`,
path: ['optionType'],
});
} else {
const zodType = conversationSchema._def.typeName;
const settingTypeEquivalent = ZodTypeToSettingType[zodType] || null;
if (settingTypeEquivalent !== setting.type) {
errors.push({
code: ZodIssueCode.custom,
message: `Setting ${setting.key} with optionType "${setting.optionType}" must match the type defined in tConversationSchema.`,
path: ['optionType'],
});
}
}
}
/* Default value checks */
if (setting.type === 'number' && isNaN(setting.default as number)) {
errors.push({
code: ZodIssueCode.custom,
message: `Invalid default value for setting ${setting.key}. Must be a number.`,
path: ['default'],
});
}
if (setting.type === 'boolean' && typeof setting.default !== 'boolean') {
errors.push({
code: ZodIssueCode.custom,
message: `Invalid default value for setting ${setting.key}. Must be a boolean.`,
path: ['default'],
});
}
if (
(setting.type === 'string' || setting.type === 'enum') &&
typeof setting.default !== 'string'
) {
errors.push({
code: ZodIssueCode.custom,
message: `Invalid default value for setting ${setting.key}. Must be a string.`,
path: ['default'],
});
}
if (
setting.type === 'enum' &&
setting.options &&
!setting.options.includes(setting.default as string)
) {
errors.push({
code: ZodIssueCode.custom,
message: `Invalid default value for setting ${
setting.key
}. Must be one of the options: [${setting.options.join(', ')}].`,
path: ['default'],
});
}
if (
setting.type === 'number' &&
setting.range &&
typeof setting.default === 'number' &&
(setting.default < setting.range.min || setting.default > setting.range.max)
) {
errors.push({
code: ZodIssueCode.custom,
message: `Invalid default value for setting ${setting.key}. Must be within the range [${setting.range.min}, ${setting.range.max}].`,
path: ['default'],
});
}
}
if (errors.length > 0) {
throw new ZodError(errors);
}
}
export const generateOpenAISchema = (customOpenAI: OpenAISettings) => {
const defaults = { ...openAI, ...customOpenAI };
return tConversationSchema
.pick({
model: true,
chatGptLabel: true,
promptPrefix: true,
temperature: true,
top_p: true,
presence_penalty: true,
frequency_penalty: true,
resendFiles: true,
imageDetail: true,
})
.transform((obj) => ({
...obj,
model: obj.model ?? defaults.model.default,
chatGptLabel: obj.chatGptLabel ?? null,
promptPrefix: obj.promptPrefix ?? null,
temperature: obj.temperature ?? defaults.temperature.default,
top_p: obj.top_p ?? defaults.top_p.default,
presence_penalty: obj.presence_penalty ?? defaults.presence_penalty.default,
frequency_penalty: obj.frequency_penalty ?? defaults.frequency_penalty.default,
resendFiles:
typeof obj.resendFiles === 'boolean' ? obj.resendFiles : defaults.resendFiles.default,
imageDetail: obj.imageDetail ?? defaults.imageDetail.default,
}))
.catch(() => ({
model: defaults.model.default,
chatGptLabel: null,
promptPrefix: null,
temperature: defaults.temperature.default,
top_p: defaults.top_p.default,
presence_penalty: defaults.presence_penalty.default,
frequency_penalty: defaults.frequency_penalty.default,
resendFiles: defaults.resendFiles.default,
imageDetail: defaults.imageDetail.default,
}));
};
export const generateGoogleSchema = (customGoogle: GoogleSettings) => {
const defaults = { ...google, ...customGoogle };
return tConversationSchema
.pick({
model: true,
modelLabel: true,
promptPrefix: true,
examples: true,
temperature: true,
maxOutputTokens: true,
topP: true,
topK: true,
})
.transform((obj) => {
const isGeminiPro = obj?.model?.toLowerCase()?.includes('gemini-pro');
const maxOutputTokensMax = isGeminiPro
? defaults.maxOutputTokens.maxGeminiPro
: defaults.maxOutputTokens.max;
const maxOutputTokensDefault = isGeminiPro
? defaults.maxOutputTokens.defaultGeminiPro
: defaults.maxOutputTokens.default;
let maxOutputTokens = obj.maxOutputTokens ?? maxOutputTokensDefault;
maxOutputTokens = Math.min(maxOutputTokens, maxOutputTokensMax);
return {
...obj,
model: obj.model ?? defaults.model.default,
modelLabel: obj.modelLabel ?? null,
promptPrefix: obj.promptPrefix ?? null,
examples: obj.examples ?? [{ input: { content: '' }, output: { content: '' } }],
temperature: obj.temperature ?? defaults.temperature.default,
maxOutputTokens,
topP: obj.topP ?? defaults.topP.default,
topK: obj.topK ?? defaults.topK.default,
};
})
.catch(() => ({
model: defaults.model.default,
modelLabel: null,
promptPrefix: null,
examples: [{ input: { content: '' }, output: { content: '' } }],
temperature: defaults.temperature.default,
maxOutputTokens: defaults.maxOutputTokens.default,
topP: defaults.topP.default,
topK: defaults.topK.default,
}));
};

View file

@ -4,6 +4,7 @@ export * from './config';
export * from './file-config'; export * from './file-config';
/* schema helpers */ /* schema helpers */
export * from './parsers'; export * from './parsers';
export * from './generate';
/* types (exports schemas from `./types` as they contain needed in other defs) */ /* types (exports schemas from `./types` as they contain needed in other defs) */
export * from './types'; export * from './types';
export * from './types/assistants'; export * from './types/assistants';

View file

@ -17,6 +17,26 @@ export enum EModelEndpoint {
custom = 'custom', custom = 'custom',
} }
export enum ImageDetail {
low = 'low',
auto = 'auto',
high = 'high',
}
export const imageDetailNumeric = {
[ImageDetail.low]: 0,
[ImageDetail.auto]: 1,
[ImageDetail.high]: 2,
};
export const imageDetailValue = {
0: ImageDetail.low,
1: ImageDetail.auto,
2: ImageDetail.high,
};
export const eImageDetailSchema = z.nativeEnum(ImageDetail);
export const defaultAssistantFormValues = { export const defaultAssistantFormValues = {
assistant: '', assistant: '',
id: '', id: '',
@ -46,8 +66,43 @@ export const ImageVisionTool: FunctionTool = {
export const isImageVisionTool = (tool: FunctionTool | FunctionToolCall) => export const isImageVisionTool = (tool: FunctionTool | FunctionToolCall) =>
tool.type === 'function' && tool.function?.name === ImageVisionTool?.function?.name; tool.type === 'function' && tool.function?.name === ImageVisionTool?.function?.name;
export const endpointSettings = { export const openAISettings = {
[EModelEndpoint.google]: { model: {
default: 'gpt-3.5-turbo',
},
temperature: {
min: 0,
max: 1,
step: 0.01,
default: 1,
},
top_p: {
min: 0,
max: 1,
step: 0.01,
default: 1,
},
presence_penalty: {
min: 0,
max: 2,
step: 0.01,
default: 0,
},
frequency_penalty: {
min: 0,
max: 2,
step: 0.01,
default: 0,
},
resendFiles: {
default: true,
},
imageDetail: {
default: ImageDetail.auto,
},
};
export const googleSettings = {
model: { model: {
default: 'chat-bison', default: 'chat-bison',
}, },
@ -77,7 +132,11 @@ export const endpointSettings = {
step: 0.01, step: 0.01,
default: 40, default: 40,
}, },
}, };
export const endpointSettings = {
[EModelEndpoint.openAI]: openAISettings,
[EModelEndpoint.google]: googleSettings,
}; };
const google = endpointSettings[EModelEndpoint.google]; const google = endpointSettings[EModelEndpoint.google];
@ -86,26 +145,6 @@ export const eModelEndpointSchema = z.nativeEnum(EModelEndpoint);
export const extendedModelEndpointSchema = z.union([eModelEndpointSchema, z.string()]); export const extendedModelEndpointSchema = z.union([eModelEndpointSchema, z.string()]);
export enum ImageDetail {
low = 'low',
auto = 'auto',
high = 'high',
}
export const imageDetailNumeric = {
[ImageDetail.low]: 0,
[ImageDetail.auto]: 1,
[ImageDetail.high]: 2,
};
export const imageDetailValue = {
0: ImageDetail.low,
1: ImageDetail.auto,
2: ImageDetail.high,
};
export const eImageDetailSchema = z.nativeEnum(ImageDetail);
export const tPluginAuthConfigSchema = z.object({ export const tPluginAuthConfigSchema = z.object({
authField: z.string(), authField: z.string(),
label: z.string(), label: z.string(),
@ -278,12 +317,14 @@ export const tPresetUpdateSchema = tConversationSchema.merge(
export type TPreset = z.infer<typeof tPresetSchema>; export type TPreset = z.infer<typeof tPresetSchema>;
export type TSetOption = (
param: number | string,
) => (newValue: number | string | boolean | Partial<TPreset>) => void;
export type TConversation = z.infer<typeof tConversationSchema> & { export type TConversation = z.infer<typeof tConversationSchema> & {
presetOverride?: Partial<TPreset>; presetOverride?: Partial<TPreset>;
}; };
// type DefaultSchemaValues = Partial<typeof google>;
export const openAISchema = tConversationSchema export const openAISchema = tConversationSchema
.pick({ .pick({
model: true, model: true,
@ -298,26 +339,27 @@ export const openAISchema = tConversationSchema
}) })
.transform((obj) => ({ .transform((obj) => ({
...obj, ...obj,
model: obj.model ?? 'gpt-3.5-turbo', model: obj.model ?? openAISettings.model.default,
chatGptLabel: obj.chatGptLabel ?? null, chatGptLabel: obj.chatGptLabel ?? null,
promptPrefix: obj.promptPrefix ?? null, promptPrefix: obj.promptPrefix ?? null,
temperature: obj.temperature ?? 1, temperature: obj.temperature ?? openAISettings.temperature.default,
top_p: obj.top_p ?? 1, top_p: obj.top_p ?? openAISettings.top_p.default,
presence_penalty: obj.presence_penalty ?? 0, presence_penalty: obj.presence_penalty ?? openAISettings.presence_penalty.default,
frequency_penalty: obj.frequency_penalty ?? 0, frequency_penalty: obj.frequency_penalty ?? openAISettings.frequency_penalty.default,
resendFiles: typeof obj.resendFiles === 'boolean' ? obj.resendFiles : true, resendFiles:
imageDetail: obj.imageDetail ?? ImageDetail.auto, typeof obj.resendFiles === 'boolean' ? obj.resendFiles : openAISettings.resendFiles.default,
imageDetail: obj.imageDetail ?? openAISettings.imageDetail.default,
})) }))
.catch(() => ({ .catch(() => ({
model: 'gpt-3.5-turbo', model: openAISettings.model.default,
chatGptLabel: null, chatGptLabel: null,
promptPrefix: null, promptPrefix: null,
temperature: 1, temperature: openAISettings.temperature.default,
top_p: 1, top_p: openAISettings.top_p.default,
presence_penalty: 0, presence_penalty: openAISettings.presence_penalty.default,
frequency_penalty: 0, frequency_penalty: openAISettings.frequency_penalty.default,
resendFiles: true, resendFiles: openAISettings.resendFiles.default,
imageDetail: ImageDetail.auto, imageDetail: openAISettings.imageDetail.default,
})); }));
export const googleSchema = tConversationSchema export const googleSchema = tConversationSchema
@ -674,53 +716,3 @@ export const compactPluginsSchema = tConversationSchema
return removeNullishValues(newObj); return removeNullishValues(newObj);
}) })
.catch(() => ({})); .catch(() => ({}));
// const createGoogleSchema = (customGoogle: DefaultSchemaValues) => {
// const defaults = { ...google, ...customGoogle };
// return tConversationSchema
// .pick({
// model: true,
// modelLabel: true,
// promptPrefix: true,
// examples: true,
// temperature: true,
// maxOutputTokens: true,
// topP: true,
// topK: true,
// })
// .transform((obj) => {
// const isGeminiPro = obj?.model?.toLowerCase()?.includes('gemini-pro');
// const maxOutputTokensMax = isGeminiPro
// ? defaults.maxOutputTokens.maxGeminiPro
// : defaults.maxOutputTokens.max;
// const maxOutputTokensDefault = isGeminiPro
// ? defaults.maxOutputTokens.defaultGeminiPro
// : defaults.maxOutputTokens.default;
// let maxOutputTokens = obj.maxOutputTokens ?? maxOutputTokensDefault;
// maxOutputTokens = Math.min(maxOutputTokens, maxOutputTokensMax);
// return {
// ...obj,
// model: obj.model ?? defaults.model.default,
// modelLabel: obj.modelLabel ?? null,
// promptPrefix: obj.promptPrefix ?? null,
// examples: obj.examples ?? [{ input: { content: '' }, output: { content: '' } }],
// temperature: obj.temperature ?? defaults.temperature.default,
// maxOutputTokens,
// topP: obj.topP ?? defaults.topP.default,
// topK: obj.topK ?? defaults.topK.default,
// };
// })
// .catch(() => ({
// model: defaults.model.default,
// modelLabel: null,
// promptPrefix: null,
// examples: [{ input: { content: '' }, output: { content: '' } }],
// temperature: defaults.temperature.default,
// maxOutputTokens: defaults.maxOutputTokens.default,
// topP: defaults.topP.default,
// topK: defaults.topK.default,
// }));
// };