🗓️ 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:
Danny Avila 2025-04-29 03:49:02 -04:00 committed by GitHub
parent 0e8041bcac
commit 55f5f2d11a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
47 changed files with 707 additions and 195 deletions

View file

@ -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',

View file

@ -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>
)}
/>

View file

@ -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

View file

@ -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

View file

@ -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>

View file

@ -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>