| | import { useMemo } from 'react'; |
| | import remarkGfm from 'remark-gfm'; |
| | import remarkMath from 'remark-math'; |
| | 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 { TextareaAutosize, InputCombobox, Button } from '@librechat/client'; |
| | import { useForm, useFieldArray, Controller, useWatch } from 'react-hook-form'; |
| | import type { TPromptGroup } from 'librechat-data-provider'; |
| | import { codeNoExecution } from '~/components/Chat/Messages/Content/MarkdownComponents'; |
| | import { cn, wrapVariable, defaultTextProps, extractVariableInfo } from '~/utils'; |
| | import { useAuthContext, useLocalize, useSubmitMessage } from '~/hooks'; |
| | import { PromptVariableGfm } from '../Markdown'; |
| |
|
| | type FieldType = 'text' | 'select'; |
| |
|
| | type FieldConfig = { |
| | variable: string; |
| | type: FieldType; |
| | options?: string[]; |
| | }; |
| |
|
| | type FormValues = { |
| | fields: { variable: string; value: string; config: FieldConfig }[]; |
| | }; |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | const parseFieldConfig = (variable: string): FieldConfig => { |
| | const content = variable.trim(); |
| | if (content.includes(':')) { |
| | const [name, options] = content.split(':'); |
| | if (options && options.includes('|')) { |
| | return { |
| | variable: name.trim(), |
| | type: 'select', |
| | options: options.split('|').map((opt) => opt.trim()), |
| | }; |
| | } |
| | } |
| | return { variable: content, type: 'text' }; |
| | }; |
| |
|
| | export default function VariableForm({ |
| | group, |
| | onClose, |
| | }: { |
| | group: TPromptGroup; |
| | onClose: () => void; |
| | }) { |
| | const localize = useLocalize(); |
| | const { user } = useAuthContext(); |
| |
|
| | const mainText = useMemo(() => { |
| | const initialText = group.productionPrompt?.prompt ?? ''; |
| | return replaceSpecialVars({ text: initialText, user }); |
| | }, [group.productionPrompt?.prompt, user]); |
| |
|
| | const { allVariables, uniqueVariables, variableIndexMap } = useMemo( |
| | () => extractVariableInfo(mainText), |
| | [mainText], |
| | ); |
| |
|
| | const { submitPrompt } = useSubmitMessage(); |
| | const { control, handleSubmit } = useForm<FormValues>({ |
| | defaultValues: { |
| | fields: uniqueVariables.map((variable) => ({ |
| | variable: wrapVariable(variable), |
| | value: '', |
| | config: parseFieldConfig(variable), |
| | })), |
| | }, |
| | }); |
| |
|
| | const { fields } = useFieldArray({ |
| | control, |
| | name: 'fields', |
| | }); |
| |
|
| | const fieldValues = useWatch({ |
| | control, |
| | name: 'fields', |
| | }); |
| |
|
| | if (!uniqueVariables.length) { |
| | return null; |
| | } |
| |
|
| | const generateHighlightedMarkdown = () => { |
| | let tempText = mainText; |
| | allVariables.forEach((variable) => { |
| | const placeholder = `{{${variable}}}`; |
| | const fieldIndex = variableIndexMap.get(variable) as string | number; |
| | 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; |
| | }; |
| |
|
| | const onSubmit = (data: FormValues) => { |
| | let text = mainText; |
| | data.fields.forEach(({ variable, value }) => { |
| | if (!value) { |
| | return; |
| | } |
| |
|
| | const escapedVariable = variable.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&'); |
| | const regex = new RegExp(escapedVariable, 'g'); |
| | text = text.replace(regex, value); |
| | }); |
| |
|
| | submitPrompt(text); |
| | onClose(); |
| | }; |
| |
|
| | 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-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: false }]]} |
| | rehypePlugins={[ |
| | /** @ts-ignore */ |
| | [rehypeKatex], |
| | /** @ts-ignore */ |
| | [rehypeHighlight, { ignoreMissing: true }], |
| | ]} |
| | /** @ts-ignore */ |
| | 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> |
| | </div> |
| | <div className="space-y-4"> |
| | {fields.map((field, index) => ( |
| | <div key={field.id} className="flex flex-col space-y-2"> |
| | <Controller |
| | name={`fields.${index}.value`} |
| | control={control} |
| | render={({ field: { onChange, onBlur, value, ref } }) => { |
| | if (field.config.type === 'select') { |
| | return ( |
| | <InputCombobox |
| | options={field.config.options || []} |
| | placeholder={field.config.variable} |
| | className={cn( |
| | defaultTextProps, |
| | 'rounded px-3 py-2 focus:bg-surface-tertiary', |
| | )} |
| | value={value} |
| | onChange={onChange} |
| | onBlur={onBlur} |
| | /> |
| | ); |
| | } |
| | |
| | return ( |
| | <TextareaAutosize |
| | ref={ref} |
| | value={value} |
| | onChange={onChange} |
| | onBlur={onBlur} |
| | id={`fields.${index}.value`} |
| | className={cn( |
| | defaultTextProps, |
| | 'rounded px-3 py-2 focus:bg-surface-tertiary', |
| | )} |
| | placeholder={field.config.variable} |
| | maxRows={8} |
| | aria-label={field.config.variable} |
| | /> |
| | ); |
| | }} |
| | /> |
| | </div> |
| | ))} |
| | </div> |
| | <div className="flex justify-end"> |
| | <Button type="submit" variant="submit" aria-label={localize('com_ui_submit')}> |
| | {localize('com_ui_submit')} |
| | </Button> |
| | </div> |
| | </form> |
| | </div> |
| | ); |
| | } |
| |
|