mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-23 11:50:14 +01:00
✋ feat: Stop Sequences for Conversations & Presets (#2536)
* feat: `stop` conversation parameter * feat: Tag primitive * feat: dynamic tags * refactor: update tag styling * feat: add stop sequences to OpenAI settings * fix(Presentation): prevent `SidePanel` re-renders that flicker side panel * refactor: use stop placeholder * feat: type and schema update for `stop` and `TPreset` in generation param related types * refactor: pass conversation to dynamic settings * refactor(OpenAIClient): remove default handling for `modelOptions.stop` * docs: fix Google AI Setup formatting * feat: current_model * docs: WIP update * fix(ChatRoute): prevent default preset override before `hasSetConversation.current` becomes true by including latest conversation state as template * docs: update docs with more info on `stop` * chore: bump config_version * refactor: CURRENT_MODEL handling
This commit is contained in:
parent
4121818124
commit
099aa9dead
29 changed files with 690 additions and 93 deletions
|
|
@ -20,9 +20,10 @@ function DynamicCheckbox({
|
|||
showDefault = true,
|
||||
labelCode,
|
||||
descriptionCode,
|
||||
conversation,
|
||||
}: DynamicSettingProps) {
|
||||
const localize = useLocalize();
|
||||
const { conversation = { conversationId: null }, preset } = useChatContext();
|
||||
const { preset } = useChatContext();
|
||||
const [inputValue, setInputValue] = useState<boolean>(!!(defaultValue as boolean | undefined));
|
||||
|
||||
const selectedValue = useMemo(() => {
|
||||
|
|
|
|||
|
|
@ -22,9 +22,10 @@ function DynamicDropdown({
|
|||
showDefault = true,
|
||||
labelCode,
|
||||
descriptionCode,
|
||||
conversation,
|
||||
}: DynamicSettingProps) {
|
||||
const localize = useLocalize();
|
||||
const { conversation = { conversationId: null }, preset } = useChatContext();
|
||||
const { preset } = useChatContext();
|
||||
const [inputValue, setInputValue] = useState<string | null>(null);
|
||||
|
||||
const selectedValue = useMemo(() => {
|
||||
|
|
|
|||
|
|
@ -22,9 +22,10 @@ function DynamicInput({
|
|||
labelCode,
|
||||
descriptionCode,
|
||||
placeholderCode,
|
||||
conversation,
|
||||
}: DynamicSettingProps) {
|
||||
const localize = useLocalize();
|
||||
const { conversation = { conversationId: null }, preset } = useChatContext();
|
||||
const { preset } = useChatContext();
|
||||
|
||||
const [setInputValue, inputValue] = useDebouncedInput<string | null>({
|
||||
optionKey: optionType !== OptionTypes.Custom ? settingKey : undefined,
|
||||
|
|
|
|||
|
|
@ -23,9 +23,10 @@ function DynamicSlider({
|
|||
includeInput = true,
|
||||
labelCode,
|
||||
descriptionCode,
|
||||
conversation,
|
||||
}: DynamicSettingProps) {
|
||||
const localize = useLocalize();
|
||||
const { conversation = { conversationId: null }, preset } = useChatContext();
|
||||
const { preset } = useChatContext();
|
||||
const isEnum = useMemo(() => !range && options && options.length > 0, [options, range]);
|
||||
|
||||
const [setInputValue, inputValue] = useDebouncedInput<string | number>({
|
||||
|
|
|
|||
|
|
@ -19,9 +19,10 @@ function DynamicSwitch({
|
|||
showDefault = true,
|
||||
labelCode,
|
||||
descriptionCode,
|
||||
conversation,
|
||||
}: DynamicSettingProps) {
|
||||
const localize = useLocalize();
|
||||
const { conversation = { conversationId: null }, preset } = useChatContext();
|
||||
const { preset } = useChatContext();
|
||||
const [inputValue, setInputValue] = useState<boolean>(!!(defaultValue as boolean | undefined));
|
||||
useParameterEffects({
|
||||
preset,
|
||||
|
|
|
|||
193
client/src/components/SidePanel/Parameters/DynamicTags.tsx
Normal file
193
client/src/components/SidePanel/Parameters/DynamicTags.tsx
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
// client/src/components/SidePanel/Parameters/DynamicTags.tsx
|
||||
import { useState, useMemo, useCallback, useRef } from 'react';
|
||||
import { OptionTypes } from 'librechat-data-provider';
|
||||
import type { DynamicSettingProps } from 'librechat-data-provider';
|
||||
import { Label, Input, HoverCard, HoverCardTrigger, Tag } from '~/components/ui';
|
||||
import { useChatContext, useToastContext } from '~/Providers';
|
||||
import { useLocalize, useParameterEffects } from '~/hooks';
|
||||
import { cn, defaultTextProps } from '~/utils';
|
||||
import OptionHover from './OptionHover';
|
||||
import { ESide } from '~/common';
|
||||
|
||||
function DynamicTags({
|
||||
label,
|
||||
settingKey,
|
||||
defaultValue = [],
|
||||
description,
|
||||
columnSpan,
|
||||
setOption,
|
||||
optionType,
|
||||
placeholder,
|
||||
readonly = false,
|
||||
showDefault = true,
|
||||
labelCode,
|
||||
descriptionCode,
|
||||
placeholderCode,
|
||||
descriptionSide = ESide.Left,
|
||||
conversation,
|
||||
minTags,
|
||||
maxTags,
|
||||
}: DynamicSettingProps) {
|
||||
const localize = useLocalize();
|
||||
const { preset } = useChatContext();
|
||||
const { showToast } = useToastContext();
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [tagText, setTagText] = useState<string>('');
|
||||
const [tags, setTags] = useState<string[] | undefined>(
|
||||
(defaultValue as string[] | undefined) ?? [],
|
||||
);
|
||||
|
||||
const updateState = useCallback(
|
||||
(update: string[]) => {
|
||||
if (optionType === OptionTypes.Custom) {
|
||||
// TODO: custom logic, add to payload but not to conversation
|
||||
setTags(update);
|
||||
return;
|
||||
}
|
||||
setOption(settingKey)(update);
|
||||
},
|
||||
[optionType, setOption, settingKey],
|
||||
);
|
||||
|
||||
const onTagClick = useCallback(() => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, [inputRef]);
|
||||
|
||||
const currentTags: string[] | undefined = useMemo(() => {
|
||||
if (optionType === OptionTypes.Custom) {
|
||||
// TODO: custom logic, add to payload but not to conversation
|
||||
return tags;
|
||||
}
|
||||
|
||||
if (!conversation?.[settingKey]) {
|
||||
return defaultValue ?? [];
|
||||
}
|
||||
|
||||
return conversation?.[settingKey];
|
||||
}, [conversation, defaultValue, optionType, settingKey, tags]);
|
||||
|
||||
const onTagRemove = useCallback(
|
||||
(indexToRemove: number) => {
|
||||
if (!currentTags) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (minTags && currentTags.length <= minTags) {
|
||||
showToast({
|
||||
message: localize('com_ui_min_tags', minTags + ''),
|
||||
status: 'warning',
|
||||
});
|
||||
return;
|
||||
}
|
||||
const update = currentTags.filter((_, index) => index !== indexToRemove);
|
||||
updateState(update);
|
||||
},
|
||||
[localize, minTags, currentTags, showToast, updateState],
|
||||
);
|
||||
|
||||
const onTagAdd = useCallback(() => {
|
||||
if (!tagText) {
|
||||
return;
|
||||
}
|
||||
|
||||
let update = [...(currentTags ?? []), tagText];
|
||||
if (maxTags && update.length > maxTags) {
|
||||
showToast({
|
||||
message: localize('com_ui_max_tags', maxTags + ''),
|
||||
status: 'warning',
|
||||
});
|
||||
update = update.slice(-maxTags);
|
||||
}
|
||||
updateState(update);
|
||||
setTagText('');
|
||||
}, [tagText, currentTags, updateState, maxTags, showToast, localize]);
|
||||
|
||||
useParameterEffects({
|
||||
preset,
|
||||
settingKey,
|
||||
defaultValue: typeof defaultValue === 'undefined' ? [] : defaultValue,
|
||||
inputValue: tags,
|
||||
setInputValue: setTags,
|
||||
preventDelayedUpdate: true,
|
||||
conversation,
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex flex-col items-center justify-start gap-6 ${
|
||||
columnSpan ? `col-span-${columnSpan}` : 'col-span-full'
|
||||
}`}
|
||||
>
|
||||
<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>
|
||||
<div>
|
||||
<div className="bg-muted mb-2 flex flex-wrap gap-1 break-all rounded-lg">
|
||||
{currentTags?.map((tag: string, index: number) => (
|
||||
<Tag
|
||||
key={`${tag}-${index}`}
|
||||
label={tag}
|
||||
onClick={onTagClick}
|
||||
onRemove={() => {
|
||||
onTagRemove(index);
|
||||
if (inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
<Input
|
||||
ref={inputRef}
|
||||
id={`${settingKey}-dynamic-input`}
|
||||
disabled={readonly}
|
||||
value={tagText}
|
||||
onKeyDown={(e) => {
|
||||
if (!currentTags) {
|
||||
return;
|
||||
}
|
||||
if (e.key === 'Backspace' && !tagText) {
|
||||
onTagRemove(currentTags.length - 1);
|
||||
}
|
||||
if (e.key === 'Enter') {
|
||||
onTagAdd();
|
||||
}
|
||||
}}
|
||||
onChange={(e) => setTagText(e.target.value)}
|
||||
placeholder={
|
||||
placeholderCode ? localize(placeholder ?? '') || placeholder : placeholder
|
||||
}
|
||||
className={cn(defaultTextProps, 'flex h-10 max-h-10 px-3 py-2')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</HoverCardTrigger>
|
||||
{description && (
|
||||
<OptionHover
|
||||
description={descriptionCode ? localize(description) || description : description}
|
||||
side={descriptionSide as ESide}
|
||||
/>
|
||||
)}
|
||||
</HoverCard>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default DynamicTags;
|
||||
|
|
@ -22,9 +22,10 @@ function DynamicTextarea({
|
|||
labelCode,
|
||||
descriptionCode,
|
||||
placeholderCode,
|
||||
conversation,
|
||||
}: DynamicSettingProps) {
|
||||
const localize = useLocalize();
|
||||
const { conversation = { conversationId: null }, preset } = useChatContext();
|
||||
const { preset } = useChatContext();
|
||||
|
||||
const [setInputValue, inputValue] = useDebouncedInput<string | null>({
|
||||
optionKey: optionType !== OptionTypes.Custom ? settingKey : undefined,
|
||||
|
|
|
|||
|
|
@ -5,12 +5,16 @@ import type {
|
|||
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';
|
||||
import { useChatContext } from '~/Providers';
|
||||
import {
|
||||
DynamicDropdown,
|
||||
DynamicCheckbox,
|
||||
DynamicTextarea,
|
||||
DynamicSlider,
|
||||
DynamicSwitch,
|
||||
DynamicInput,
|
||||
DynamicTags,
|
||||
} from './';
|
||||
|
||||
const settingsConfiguration: SettingsConfiguration = [
|
||||
{
|
||||
|
|
@ -129,6 +133,22 @@ const settingsConfiguration: SettingsConfiguration = [
|
|||
showDefault: false,
|
||||
columnSpan: 2,
|
||||
},
|
||||
{
|
||||
key: 'stop',
|
||||
label: 'com_endpoint_stop',
|
||||
labelCode: true,
|
||||
description: 'com_endpoint_openai_stop',
|
||||
descriptionCode: true,
|
||||
placeholder: 'com_endpoint_stop_placeholder',
|
||||
placeholderCode: true,
|
||||
type: 'array',
|
||||
default: [],
|
||||
component: 'tags',
|
||||
optionType: 'conversation',
|
||||
columnSpan: 4,
|
||||
minTags: 1,
|
||||
maxTags: 4,
|
||||
},
|
||||
];
|
||||
|
||||
const componentMapping: Record<ComponentTypes, React.ComponentType<DynamicSettingProps>> = {
|
||||
|
|
@ -138,9 +158,11 @@ const componentMapping: Record<ComponentTypes, React.ComponentType<DynamicSettin
|
|||
[ComponentTypes.Textarea]: DynamicTextarea,
|
||||
[ComponentTypes.Input]: DynamicInput,
|
||||
[ComponentTypes.Checkbox]: DynamicCheckbox,
|
||||
[ComponentTypes.Tags]: DynamicTags,
|
||||
};
|
||||
|
||||
export default function Parameters() {
|
||||
const { conversation } = useChatContext();
|
||||
const { setOption } = useSetIndexOptions();
|
||||
|
||||
const temperature = settingsConfiguration.find(
|
||||
|
|
@ -173,6 +195,10 @@ export default function Parameters() {
|
|||
const Input = componentMapping[chatGptLabel.component];
|
||||
const { key: inputKey, default: inputDefault, ...inputSettings } = chatGptLabel;
|
||||
|
||||
const stop = settingsConfiguration.find((setting) => setting.key === 'stop') as SettingDefinition;
|
||||
const Tags = componentMapping[stop.component];
|
||||
const { key: stopKey, default: stopDefault, ...stopSettings } = stop;
|
||||
|
||||
return (
|
||||
<div className="h-auto max-w-full overflow-x-hidden p-3">
|
||||
<div className="grid grid-cols-4 gap-6">
|
||||
|
|
@ -184,30 +210,42 @@ export default function Parameters() {
|
|||
defaultValue={inputDefault}
|
||||
{...inputSettings}
|
||||
setOption={setOption}
|
||||
conversation={conversation}
|
||||
/>
|
||||
<Textarea
|
||||
settingKey={textareaKey}
|
||||
defaultValue={textareaDefault}
|
||||
{...textareaSettings}
|
||||
setOption={setOption}
|
||||
conversation={conversation}
|
||||
/>
|
||||
<TempComponent
|
||||
settingKey={temp}
|
||||
defaultValue={tempDefault}
|
||||
{...tempSettings}
|
||||
setOption={setOption}
|
||||
conversation={conversation}
|
||||
/>
|
||||
<Switch
|
||||
settingKey={switchKey}
|
||||
defaultValue={switchDefault}
|
||||
{...switchSettings}
|
||||
setOption={setOption}
|
||||
conversation={conversation}
|
||||
/>
|
||||
<DetailComponent
|
||||
settingKey={detail}
|
||||
defaultValue={detailDefault}
|
||||
{...detailSettings}
|
||||
setOption={setOption}
|
||||
conversation={conversation}
|
||||
/>
|
||||
<Tags
|
||||
settingKey={stopKey}
|
||||
defaultValue={stopDefault}
|
||||
{...stopSettings}
|
||||
setOption={setOption}
|
||||
conversation={conversation}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
7
client/src/components/SidePanel/Parameters/index.ts
Normal file
7
client/src/components/SidePanel/Parameters/index.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
export { default as DynamicDropdown } from './DynamicDropdown';
|
||||
export { default as DynamicCheckbox } from './DynamicCheckbox';
|
||||
export { default as DynamicTextarea } from './DynamicTextarea';
|
||||
export { default as DynamicSlider } from './DynamicSlider';
|
||||
export { default as DynamicSwitch } from './DynamicSwitch';
|
||||
export { default as DynamicInput } from './DynamicInput';
|
||||
export { default as DynamicTags } from './DynamicTags';
|
||||
Loading…
Add table
Add a link
Reference in a new issue