mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-20 18:30:15 +01:00
🗓️ feat: Add Special Variables for Prompts & Agents, Prompt UI Improvements (#7123)
* wip: Add Instructions component for agent configuration
* ✨ feat: Implement DropdownPopup for variable insertion in instructions
* refactor: Enhance variable handling by exporting specialVariables and updating Markdown components
* feat: Add special variable support for current date and user in Instructions component
* refactor: Update handleAddVariable to include localized label
* feat: replace special variables in instructions presets
* chore: update parameter type for user in getListAgents function
* refactor: integrate dayjs for date handling and move replaceSpecialVars function to data-provider
* feat: enhance replaceSpecialVars to include day number in current date format
* feat: integrate replaceSpecialVars for processing agent instructions
* feat: add support for current date & time in replaceSpecialVars function
* feat: add iso_datetime support in replaceSpecialVars function
* fix: enforce text parameter to be a required field in replaceSpecialVars function
* feat: add ISO datetime support in translation file
* fix: disable eslint warning for autoFocus in TextareaAutosize component
* feat: add VariablesDropdown component and integrate it into CreatePromptForm and PromptEditor; update translation for special variables
* fix: CategorySelector and related localizations
* fix: add z-index class to LanguageSTTDropdown for proper stacking context
* fix: add max-height and overflow styles to OGDialogContent in VariableDialog and PreviewPrompt components
* fix: update variable detection logic to exclude special variables and improve regex matching
* fix: improve accessibility text for actions menu in ChatGroupItem component
* fix: adjust max-width and height styles for dialog components and improve markdown rendering for light vs. dark, height/widths, etc.
* fix: remove commented-out code for better readability in PromptVariableGfm component
* fix: handle undefined input parameter in setParams function call
* fix: update variable label types to use TSpecialVarLabel for consistency
* fix: remove outdated information from special variables description in translation file
* fix: enhance unused i18next keys detection for special variable keys
* fix: update color classes for consistency/a11y in category and prompt variable components
* fix: update PromptVariableGfm component and special variable styles for consistency
* fix: improve variable highlighting logic in VariableForm component
* fix: update background color classes for consistency in VariableForm component
* fix: add missing ref parameter to Dialog component in OriginalDialog
* refactor: move navigate call for new conversation to after setConversation update
* refactor: move message query hook to client workspace; fix: handle edge case for navigation from finalHandler creating race condition for response message DB save
* chore: bump librechat-data-provider to 0.7.793
* ci: add unit tests for replaceSpecialVars function
* fix: implement getToolkitKey function for image_gen_oai toolkit filtering/including
* ci: enhance dayjs mock for consistent date/time values in tests
* fix: MCP stdio server fail to start when passing env property
* fix: use optional chaining for clientRef dereferencing in AskController and EditController
feat: add context to saveMessage call in streamResponse utility
* fix: only save error messages if the userMessageId was initialized
* refactor: add isNotAppendable check to disable inputs in ChatForm and useTextarea
* feat: enhance error handling in useEventHandlers and update conversation state in useNewConvo
* refactor: prepend underscore to conversationId in newConversation template
* feat: log aborted conversations with minimal messages and use consistent conversationId generation
---------
Co-authored-by: Olivier Schiavo <olivier.schiavo@wengo.com>
Co-authored-by: aka012 <aka012@neowiz.com>
Co-authored-by: jiasheng <jiashengguo@outlook.com>
This commit is contained in:
parent
0e8041bcac
commit
55f5f2d11a
47 changed files with 707 additions and 195 deletions
|
|
@ -28,9 +28,9 @@ const categoryColorMap: Record<string, string> = {
|
|||
code: 'text-red-500',
|
||||
misc: 'text-blue-300',
|
||||
shop: 'text-purple-400',
|
||||
idea: 'text-yellow-300',
|
||||
idea: 'text-yellow-500/90 dark:text-yellow-300 ',
|
||||
write: 'text-purple-400',
|
||||
travel: 'text-yellow-300',
|
||||
travel: 'text-yellow-500/90 dark:text-yellow-300 ',
|
||||
finance: 'text-orange-400',
|
||||
roleplay: 'text-orange-400',
|
||||
teach_or_explain: 'text-blue-300',
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import React, { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useFormContext, Controller } from 'react-hook-form';
|
||||
import { LocalStorageKeys } from 'librechat-data-provider';
|
||||
import { Dropdown } from '~/components/ui';
|
||||
|
|
@ -15,6 +17,7 @@ const CategorySelector: React.FC<CategorySelectorProps> = ({
|
|||
onValueChange,
|
||||
className = '',
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const formContext = useFormContext();
|
||||
const { categories, emptyCategory } = useCategories();
|
||||
|
||||
|
|
@ -32,13 +35,25 @@ const CategorySelector: React.FC<CategorySelectorProps> = ({
|
|||
[watchedCategory, categories, currentCategory, emptyCategory],
|
||||
);
|
||||
|
||||
const displayCategory = useMemo(() => {
|
||||
if (!categoryOption.value && !('icon' in categoryOption)) {
|
||||
return {
|
||||
...categoryOption,
|
||||
icon: (<span className="i-heroicons-tag" />) as ReactNode,
|
||||
label: categoryOption.label || t('com_ui_empty_category'),
|
||||
};
|
||||
}
|
||||
return categoryOption;
|
||||
}, [categoryOption, t]);
|
||||
|
||||
return formContext ? (
|
||||
<Controller
|
||||
name="category"
|
||||
control={control}
|
||||
render={() => (
|
||||
<Dropdown
|
||||
value={categoryOption.value ?? ''}
|
||||
value={displayCategory.value ?? ''}
|
||||
label={displayCategory.value ? undefined : t('com_ui_category')}
|
||||
onChange={(value: string) => {
|
||||
setValue('category', value, { shouldDirty: false });
|
||||
localStorage.setItem(LocalStorageKeys.LAST_PROMPT_CATEGORY, value);
|
||||
|
|
@ -48,10 +63,12 @@ const CategorySelector: React.FC<CategorySelectorProps> = ({
|
|||
ariaLabel="Prompt's category selector"
|
||||
className={className}
|
||||
options={categories || []}
|
||||
renderValue={(option) => (
|
||||
renderValue={() => (
|
||||
<div className="flex items-center space-x-2">
|
||||
{option.icon != null && <span>{option.icon as React.ReactNode}</span>}
|
||||
<span>{option.label}</span>
|
||||
{'icon' in displayCategory && displayCategory.icon != null && (
|
||||
<span>{displayCategory.icon as ReactNode}</span>
|
||||
)}
|
||||
<span>{displayCategory.label}</span>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
|
|
@ -68,10 +85,12 @@ const CategorySelector: React.FC<CategorySelectorProps> = ({
|
|||
ariaLabel="Prompt's category selector"
|
||||
className={className}
|
||||
options={categories || []}
|
||||
renderValue={(option) => (
|
||||
renderValue={() => (
|
||||
<div className="flex items-center space-x-2">
|
||||
{option.icon != null && <span>{option.icon as React.ReactNode}</span>}
|
||||
<span>{option.label}</span>
|
||||
{'icon' in displayCategory && displayCategory.icon != null && (
|
||||
<span>{displayCategory.icon as ReactNode}</span>
|
||||
)}
|
||||
<span>{displayCategory.label}</span>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ function ChatGroupItem({
|
|||
snippet={
|
||||
typeof group.oneliner === 'string' && group.oneliner.length > 0
|
||||
? group.oneliner
|
||||
: group.productionPrompt?.prompt ?? ''
|
||||
: (group.productionPrompt?.prompt ?? '')
|
||||
}
|
||||
>
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
|
|
@ -83,7 +83,11 @@ function ChatGroupItem({
|
|||
className="z-50 inline-flex h-8 w-8 items-center justify-center rounded-lg border border-border-medium bg-transparent p-0 text-sm font-medium transition-all duration-300 ease-in-out hover:border-border-heavy hover:bg-surface-hover focus:border-border-heavy focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50"
|
||||
>
|
||||
<MenuIcon className="icon-md text-text-secondary" aria-hidden="true" />
|
||||
<span className="sr-only">Open actions menu for {group.name}</span>
|
||||
<span className="sr-only">
|
||||
{localize('com_ui_sr_actions_menu', { 0: group.name }) +
|
||||
' ' +
|
||||
localize('com_ui_prompt')}
|
||||
</span>
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import PromptVariables from '~/components/Prompts/PromptVariables';
|
|||
import { Button, TextareaAutosize, Input } from '~/components/ui';
|
||||
import Description from '~/components/Prompts/Description';
|
||||
import { useLocalize, useHasAccess } from '~/hooks';
|
||||
import VariablesDropdown from '~/components/Prompts/VariablesDropdown';
|
||||
import Command from '~/components/Prompts/Command';
|
||||
import { useCreatePrompt } from '~/data-provider';
|
||||
import { cn } from '~/utils';
|
||||
|
|
@ -132,7 +133,8 @@ const CreatePromptForm = ({
|
|||
<div className="flex w-full flex-col gap-4 md:mt-[1.075rem]">
|
||||
<div>
|
||||
<h2 className="flex items-center justify-between rounded-t-lg border border-border-medium py-2 pl-4 pr-1 text-base font-semibold dark:text-gray-200">
|
||||
{localize('com_ui_prompt_text')}*
|
||||
<span>{localize('com_ui_prompt_text')}*</span>
|
||||
<VariablesDropdown fieldName="prompt" className="mr-2" />
|
||||
</h2>
|
||||
<div className="min-h-32 rounded-b-lg border border-border-medium p-4 transition-all duration-150">
|
||||
<Controller
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ const VariableDialog: React.FC<VariableDialogProps> = ({ open, onClose, group })
|
|||
|
||||
return (
|
||||
<OGDialog open={open} onOpenChange={handleOpenChange}>
|
||||
<OGDialogContent className="max-w-full bg-white dark:border-gray-700 dark:bg-gray-850 dark:text-gray-300 md:max-w-3xl">
|
||||
<OGDialogContent className="max-h-[90vh] max-w-full overflow-y-auto bg-white dark:border-gray-700 dark:bg-gray-850 dark:text-gray-300 md:max-w-[60vw]">
|
||||
<OGDialogTitle>{group.name}</OGDialogTitle>
|
||||
<VariableForm group={group} onClose={onClose} />
|
||||
</OGDialogContent>
|
||||
|
|
|
|||
|
|
@ -5,18 +5,14 @@ import supersub from 'remark-supersub';
|
|||
import rehypeKatex from 'rehype-katex';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import rehypeHighlight from 'rehype-highlight';
|
||||
import { replaceSpecialVars } from 'librechat-data-provider';
|
||||
import { useForm, useFieldArray, Controller, useWatch } from 'react-hook-form';
|
||||
import type { TPromptGroup } from 'librechat-data-provider';
|
||||
import {
|
||||
cn,
|
||||
wrapVariable,
|
||||
defaultTextProps,
|
||||
replaceSpecialVars,
|
||||
extractVariableInfo,
|
||||
} from '~/utils';
|
||||
import { cn, wrapVariable, defaultTextProps, extractVariableInfo } from '~/utils';
|
||||
import { codeNoExecution } from '~/components/Chat/Messages/Content/Markdown';
|
||||
import { TextareaAutosize, InputCombobox, Button } from '~/components/ui';
|
||||
import { useAuthContext, useLocalize, useSubmitMessage } from '~/hooks';
|
||||
import { PromptVariableGfm } from '../Markdown';
|
||||
|
||||
type FieldType = 'text' | 'select';
|
||||
|
||||
|
|
@ -115,9 +111,12 @@ export default function VariableForm({
|
|||
allVariables.forEach((variable) => {
|
||||
const placeholder = `{{${variable}}}`;
|
||||
const fieldIndex = variableIndexMap.get(variable) as string | number;
|
||||
const fieldValue = fieldValues[fieldIndex].value as string;
|
||||
const highlightText = fieldValue !== '' ? fieldValue : placeholder;
|
||||
tempText = tempText.replaceAll(placeholder, `**${highlightText}**`);
|
||||
const fieldValue = fieldValues[fieldIndex].value as string | undefined;
|
||||
if (fieldValue === placeholder || fieldValue === '' || !fieldValue) {
|
||||
return;
|
||||
}
|
||||
const highlightText = fieldValue !== '' ? `**${fieldValue}**` : placeholder;
|
||||
tempText = tempText.replaceAll(placeholder, highlightText);
|
||||
});
|
||||
return tempText;
|
||||
};
|
||||
|
|
@ -141,7 +140,7 @@ export default function VariableForm({
|
|||
return (
|
||||
<div className="mx-auto p-1 md:container">
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
||||
<div className="mb-6 max-h-screen max-w-[90vw] overflow-auto rounded-md bg-gray-100 p-4 text-text-secondary dark:bg-gray-700/50 sm:max-w-full md:max-h-80">
|
||||
<div className="mb-6 max-h-screen max-w-[90vw] overflow-auto rounded-md bg-surface-tertiary p-4 text-text-secondary dark:bg-surface-primary sm:max-w-full md:max-h-96">
|
||||
<ReactMarkdown
|
||||
/** @ts-ignore */
|
||||
remarkPlugins={[supersub, remarkGfm, [remarkMath, { singleDollarTextMath: true }]]}
|
||||
|
|
@ -152,8 +151,8 @@ export default function VariableForm({
|
|||
[rehypeHighlight, { ignoreMissing: true }],
|
||||
]}
|
||||
/** @ts-ignore */
|
||||
components={{ code: codeNoExecution }}
|
||||
className="prose dark:prose-invert light dark:text-gray-70 my-1 max-h-[50vh] break-words"
|
||||
components={{ code: codeNoExecution, p: PromptVariableGfm }}
|
||||
className="markdown prose dark:prose-invert light my-1 max-h-[50vh] max-w-full break-words dark:text-text-secondary"
|
||||
>
|
||||
{generateHighlightedMarkdown()}
|
||||
</ReactMarkdown>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
import { handleDoubleClick } from '~/utils';
|
||||
|
||||
export const CodeVariableGfm = ({ children }: { children: React.ReactNode }) => {
|
||||
export const CodeVariableGfm: React.ElementType = ({ children }: { children: React.ReactNode }) => {
|
||||
return (
|
||||
<code
|
||||
onDoubleClick={handleDoubleClick}
|
||||
|
|
@ -29,7 +29,10 @@ export const PromptVariableGfm = ({
|
|||
const parts = child.split(regex);
|
||||
return parts.map((part, index) =>
|
||||
index % 2 === 1 ? (
|
||||
<b key={index} className="rounded-md bg-yellow-100/90 p-1 text-gray-700">
|
||||
<b
|
||||
key={index}
|
||||
className="ml-[0.5] rounded-lg bg-amber-100 p-[1px] font-medium text-yellow-800 dark:border-yellow-500/50 dark:bg-transparent dark:text-yellow-500/90"
|
||||
>
|
||||
{`{{${part}}}`}
|
||||
</b>
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ const PreviewPrompt = ({
|
|||
}) => {
|
||||
return (
|
||||
<OGDialog open={open} onOpenChange={onOpenChange}>
|
||||
<OGDialogContent className="w-11/12 max-w-5xl">
|
||||
<OGDialogContent className="max-h-[90vh] w-11/12 max-w-full overflow-y-auto md:max-w-[60vw]">
|
||||
<div className="p-2">
|
||||
<PromptDetails group={group} />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -5,13 +5,13 @@ import rehypeKatex from 'rehype-katex';
|
|||
import remarkMath from 'remark-math';
|
||||
import supersub from 'remark-supersub';
|
||||
import rehypeHighlight from 'rehype-highlight';
|
||||
import { replaceSpecialVars } from 'librechat-data-provider';
|
||||
import type { TPromptGroup } from 'librechat-data-provider';
|
||||
import { codeNoExecution } from '~/components/Chat/Messages/Content/Markdown';
|
||||
import { useLocalize, useAuthContext } from '~/hooks';
|
||||
import CategoryIcon from './Groups/CategoryIcon';
|
||||
import PromptVariables from './PromptVariables';
|
||||
import { PromptVariableGfm } from './Markdown';
|
||||
import { replaceSpecialVars } from '~/utils';
|
||||
import { Label } from '~/components/ui';
|
||||
import Description from './Description';
|
||||
import Command from './Command';
|
||||
|
|
@ -46,7 +46,7 @@ const PromptDetails = ({ group }: { group?: TPromptGroup }) => {
|
|||
<div className="flex h-full max-h-screen flex-col overflow-y-auto md:flex-row">
|
||||
<div className="flex flex-1 flex-col gap-4 p-0 md:max-h-[calc(100vh-150px)] md:p-2">
|
||||
<div>
|
||||
<h2 className="flex items-center justify-between rounded-t-lg border border-border-light py-2 pl-4 text-base font-semibold text-text-primary ">
|
||||
<h2 className="flex items-center justify-between rounded-t-lg border border-border-light py-2 pl-4 text-base font-semibold text-text-primary">
|
||||
{localize('com_ui_prompt_text')}
|
||||
</h2>
|
||||
<div className="group relative min-h-32 rounded-b-lg border border-border-light p-4 transition-all duration-150">
|
||||
|
|
@ -65,7 +65,7 @@ const PromptDetails = ({ group }: { group?: TPromptGroup }) => {
|
|||
]}
|
||||
/** @ts-ignore */
|
||||
components={{ p: PromptVariableGfm, code: codeNoExecution }}
|
||||
className="prose dark:prose-invert light dark:text-gray-70 my-1"
|
||||
className="markdown prose dark:prose-invert light dark:text-gray-70 my-1 break-words"
|
||||
>
|
||||
{mainText}
|
||||
</ReactMarkdown>
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import ReactMarkdown from 'react-markdown';
|
|||
import { codeNoExecution } from '~/components/Chat/Messages/Content/Markdown';
|
||||
import AlwaysMakeProd from '~/components/Prompts/Groups/AlwaysMakeProd';
|
||||
import { SaveIcon, CrossIcon } from '~/components/svg';
|
||||
import VariablesDropdown from './VariablesDropdown';
|
||||
import { TextareaAutosize } from '~/components/ui';
|
||||
import { PromptVariableGfm } from './Markdown';
|
||||
import { PromptsEditorMode } from '~/common';
|
||||
|
|
@ -59,10 +60,11 @@ const PromptEditor: React.FC<Props> = ({ name, isEditing, setIsEditing }) => {
|
|||
<span className="max-w-[200px] truncate sm:max-w-none">
|
||||
{localize('com_ui_prompt_text')}
|
||||
</span>
|
||||
<div className="flex flex-shrink-0 flex-row gap-3 sm:gap-6">
|
||||
<div className="flex flex-shrink-0 flex-row items-center gap-3 sm:gap-6">
|
||||
{editorMode === PromptsEditorMode.ADVANCED && (
|
||||
<AlwaysMakeProd className="hidden sm:flex" />
|
||||
)}
|
||||
<VariablesDropdown fieldName={name} />
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsEditing((prev) => !prev)}
|
||||
|
|
@ -105,6 +107,7 @@ const PromptEditor: React.FC<Props> = ({ name, isEditing, setIsEditing }) => {
|
|||
isEditing ? (
|
||||
<TextareaAutosize
|
||||
{...field}
|
||||
// eslint-disable-next-line jsx-a11y/no-autofocus
|
||||
autoFocus
|
||||
className="w-full resize-none overflow-y-auto rounded bg-transparent text-sm text-text-primary focus:outline-none sm:text-base"
|
||||
minRows={3}
|
||||
|
|
@ -123,8 +126,8 @@ const PromptEditor: React.FC<Props> = ({ name, isEditing, setIsEditing }) => {
|
|||
style={{ minHeight: '4.5em', maxHeight: '21em', overflow: 'auto' }}
|
||||
>
|
||||
<ReactMarkdown
|
||||
/** @ts-ignore */
|
||||
remarkPlugins={[
|
||||
/** @ts-ignore */
|
||||
supersub,
|
||||
remarkGfm,
|
||||
[remarkMath, { singleDollarTextMath: true }],
|
||||
|
|
|
|||
|
|
@ -1,18 +1,18 @@
|
|||
import React, { useMemo } from 'react';
|
||||
import { Variable } from 'lucide-react';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import { specialVariables } from 'librechat-data-provider';
|
||||
import { cn, extractUniqueVariables } from '~/utils';
|
||||
import { CodeVariableGfm } from './Markdown';
|
||||
import { Separator } from '~/components/ui';
|
||||
import { useLocalize } from '~/hooks';
|
||||
|
||||
const specialVariables = {
|
||||
current_date: true,
|
||||
current_user: true,
|
||||
};
|
||||
|
||||
const specialVariableClasses =
|
||||
'bg-yellow-500/25 text-yellow-600 dark:border-yellow-500/50 dark:bg-transparent dark:text-yellow-500/90';
|
||||
'bg-amber-100 text-yellow-800 border-yellow-600 dark:border-yellow-500/50 dark:bg-transparent dark:text-yellow-500/90';
|
||||
|
||||
const components: {
|
||||
[nodeType: string]: React.ElementType;
|
||||
} = { code: CodeVariableGfm };
|
||||
|
||||
const PromptVariables = ({
|
||||
promptText,
|
||||
|
|
@ -52,7 +52,7 @@ const PromptVariables = ({
|
|||
</div>
|
||||
) : (
|
||||
<div className="text-sm text-text-secondary">
|
||||
<ReactMarkdown components={{ code: CodeVariableGfm }}>
|
||||
<ReactMarkdown components={components} className="markdown prose dark:prose-invert">
|
||||
{localize('com_ui_variables_info')}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
|
|
@ -65,8 +65,8 @@ const PromptVariables = ({
|
|||
{localize('com_ui_special_variables')}
|
||||
</span>
|
||||
<span className="text-sm text-text-secondary">
|
||||
<ReactMarkdown components={{ code: CodeVariableGfm }}>
|
||||
{localize('com_ui_special_variables_info')}
|
||||
<ReactMarkdown components={components} className="markdown prose dark:prose-invert">
|
||||
{localize('com_ui_special_variables_more_info')}
|
||||
</ReactMarkdown>
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -75,7 +75,7 @@ const PromptVariables = ({
|
|||
{localize('com_ui_dropdown_variables')}
|
||||
</span>
|
||||
<span className="text-sm text-text-secondary">
|
||||
<ReactMarkdown components={{ code: CodeVariableGfm }}>
|
||||
<ReactMarkdown components={components} className="markdown prose dark:prose-invert">
|
||||
{localize('com_ui_dropdown_variables_info')}
|
||||
</ReactMarkdown>
|
||||
</span>
|
||||
|
|
|
|||
75
client/src/components/Prompts/VariablesDropdown.tsx
Normal file
75
client/src/components/Prompts/VariablesDropdown.tsx
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
import { useState, useId } from 'react';
|
||||
import { PlusCircle } from 'lucide-react';
|
||||
import * as Menu from '@ariakit/react/menu';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import { specialVariables } from 'librechat-data-provider';
|
||||
import type { TSpecialVarLabel } from 'librechat-data-provider';
|
||||
import { DropdownPopup } from '~/components';
|
||||
import { useLocalize } from '~/hooks';
|
||||
|
||||
interface VariableOption {
|
||||
label: TSpecialVarLabel;
|
||||
value: string;
|
||||
}
|
||||
|
||||
const variableOptions: VariableOption[] = Object.keys(specialVariables).map((key) => ({
|
||||
label: `com_ui_special_var_${key}` as TSpecialVarLabel,
|
||||
value: `{{${key}}}`,
|
||||
}));
|
||||
|
||||
interface VariablesDropdownProps {
|
||||
fieldName?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function VariablesDropdown({
|
||||
fieldName = 'prompt',
|
||||
className = '',
|
||||
}: VariablesDropdownProps) {
|
||||
const menuId = useId();
|
||||
const localize = useLocalize();
|
||||
const methods = useFormContext();
|
||||
const { setValue, getValues } = methods;
|
||||
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
|
||||
const handleAddVariable = (label: TSpecialVarLabel, value: string) => {
|
||||
const currentText = getValues(fieldName) || '';
|
||||
const spacer = currentText.length > 0 ? '\n\n' : '';
|
||||
const prefix = localize(label);
|
||||
setValue(fieldName, currentText + spacer + prefix + ': ' + value);
|
||||
setIsMenuOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={className}
|
||||
title={`${localize('com_ui_add')} ${localize('com_ui_special_variables')}`}
|
||||
>
|
||||
<DropdownPopup
|
||||
portal={true}
|
||||
mountByState={true}
|
||||
unmountOnHide={true}
|
||||
preserveTabOrder={true}
|
||||
isOpen={isMenuOpen}
|
||||
setIsOpen={setIsMenuOpen}
|
||||
trigger={
|
||||
<Menu.MenuButton
|
||||
id="variables-menu-button"
|
||||
aria-label={`${localize('com_ui_add')} ${localize('com_ui_special_variables')}`}
|
||||
className="flex h-8 items-center gap-1 rounded-md border border-border-medium bg-surface-secondary px-2 py-0 text-sm text-text-primary transition-colors duration-200 hover:bg-surface-tertiary"
|
||||
>
|
||||
<PlusCircle className="mr-1 h-3 w-3 text-text-secondary" aria-hidden={true} />
|
||||
{localize('com_ui_special_variables')}
|
||||
</Menu.MenuButton>
|
||||
}
|
||||
items={variableOptions.map((option) => ({
|
||||
label: localize(option.label) || option.label,
|
||||
onClick: () => handleAddVariable(option.label, option.value),
|
||||
}))}
|
||||
menuId={menuId}
|
||||
className="z-30"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue