mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-21 02:40:14 +01:00
🧪 feat: Prompt Dropdown Variable; style: Add Markdown Support (#3681)
* feat: Add extended inputs for promts library variables * feat: Add maxRows prop to VariableForm input field * 📩 feat: invite user (#3012) * feat: basic invite-user script * feat: add invite user functionality and registration validation middleware * fix: invite user fixes * refactor: consolidate direct model access to a central place of functions * style(Registration): add spinner to continue button * refactor: import ordrer * feat: improve invite user script and error handling * fix: merge conflict * refactor: remove `console.log` and use `logger` * fix: token operation and checkinvite issues * bring back comment and remove console log * fix: return invalid token when token is not found * fix: getInvite fix * refactor: Update Token.js to use async/await syntax for update and delete operations * feat: Refactor Token.js to use async/await syntax for createToken and findToken functions * refactor(inviteUser): define functions outside of module.exports * Update AuthService.js --------- Co-authored-by: Danny Avila <danny@librechat.ai> * style: improve OpenAI.tsx input field focus styling * refactor: update import statement in Input.tsx * refactor: remove multi-line * refactor: update placeholder text to use localization * style: new dropdown variable info and markdown styling for info * Add ReactMarkdown * chore: styling, import order * refactor: update ReactMarkdown usage in VariableForm * style: remove markdown class * refactor: update mobile styling and use code renderer * style(InputWithDropDown): update focus trigger style * style(OptionsPopover): update Save As Preset `focus` and `dark:bg` --------- Co-authored-by: Konstantin Meshcheryakov <kmeshcheryakov@klika-tech.com> Co-authored-by: Marco Beretta <81851188+berry-13@users.noreply.github.com> Co-authored-by: bsu3338 <bsu3338@users.noreply.github.com>
This commit is contained in:
parent
bbb9324447
commit
d3a20357e9
19 changed files with 473 additions and 110 deletions
|
|
@ -1,12 +1,64 @@
|
|||
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 { useForm, useFieldArray, Controller, useWatch } from 'react-hook-form';
|
||||
import type { TPromptGroup } from 'librechat-data-provider';
|
||||
import { extractVariableInfo, wrapVariable, replaceSpecialVars } from '~/utils';
|
||||
import {
|
||||
cn,
|
||||
wrapVariable,
|
||||
defaultTextProps,
|
||||
replaceSpecialVars,
|
||||
extractVariableInfo,
|
||||
} from '~/utils';
|
||||
import { useAuthContext, useLocalize, useSubmitMessage } from '~/hooks';
|
||||
import { Textarea } from '~/components/ui';
|
||||
import { TextareaAutosize, InputWithDropdown } from '~/components/ui';
|
||||
import { code } from '~/components/Chat/Messages/Content/Markdown';
|
||||
|
||||
type FieldType = 'text' | 'select';
|
||||
|
||||
type FieldConfig = {
|
||||
variable: string;
|
||||
type: FieldType;
|
||||
options?: string[];
|
||||
};
|
||||
|
||||
type FormValues = {
|
||||
fields: { variable: string; value: string }[];
|
||||
fields: { variable: string; value: string; config: FieldConfig }[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Variable Format Guide:
|
||||
*
|
||||
* Variables in prompts should be enclosed in double curly braces: {{variable}}
|
||||
*
|
||||
* Simple text input:
|
||||
* {{variable_name}}
|
||||
*
|
||||
* Dropdown select with predefined options:
|
||||
* {{variable_name:option1|option2|option3}}
|
||||
*
|
||||
* All dropdown selects allow custom input in addition to predefined options.
|
||||
*
|
||||
* Examples:
|
||||
* {{name}} - Simple text input for a name
|
||||
* {{tone:formal|casual|business casual}} - Dropdown for tone selection with custom input option
|
||||
*
|
||||
* Note: The order of variables in the prompt will be preserved in the input form.
|
||||
*/
|
||||
|
||||
const parseFieldConfig = (variable: string): FieldConfig => {
|
||||
const content = variable;
|
||||
if (content.includes(':')) {
|
||||
const [name, options] = content.split(':');
|
||||
if (options && options.includes('|')) {
|
||||
return { variable: name, type: 'select', options: options.split('|') };
|
||||
}
|
||||
}
|
||||
return { variable: content, type: 'text' };
|
||||
};
|
||||
|
||||
export default function VariableForm({
|
||||
|
|
@ -32,7 +84,11 @@ export default function VariableForm({
|
|||
const { submitPrompt } = useSubmitMessage();
|
||||
const { control, handleSubmit } = useForm<FormValues>({
|
||||
defaultValues: {
|
||||
fields: uniqueVariables.map((variable) => ({ variable: wrapVariable(variable), value: '' })),
|
||||
fields: uniqueVariables.map((variable) => ({
|
||||
variable: wrapVariable(variable),
|
||||
value: '',
|
||||
config: parseFieldConfig(variable),
|
||||
})),
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -50,31 +106,16 @@ export default function VariableForm({
|
|||
return null;
|
||||
}
|
||||
|
||||
const generateHighlightedText = () => {
|
||||
const generateHighlightedMarkdown = () => {
|
||||
let tempText = mainText;
|
||||
const parts: JSX.Element[] = [];
|
||||
|
||||
allVariables.forEach((variable, index) => {
|
||||
allVariables.forEach((variable) => {
|
||||
const placeholder = `{{${variable}}}`;
|
||||
const partsBeforePlaceholder = tempText.split(placeholder);
|
||||
const fieldIndex = variableIndexMap.get(variable) as string | number;
|
||||
const fieldValue = fieldValues[fieldIndex].value as string;
|
||||
parts.push(
|
||||
<span key={`before-${index}`}>{partsBeforePlaceholder[0]}</span>,
|
||||
<span
|
||||
key={`highlight-${index}`}
|
||||
className="rounded bg-yellow-100 p-1 font-medium dark:text-gray-800"
|
||||
>
|
||||
{fieldValue !== '' ? fieldValue : placeholder}
|
||||
</span>,
|
||||
);
|
||||
|
||||
tempText = partsBeforePlaceholder.slice(1).join(placeholder);
|
||||
const highlightText = fieldValue !== '' ? fieldValue : placeholder;
|
||||
tempText = tempText.replaceAll(placeholder, `**${highlightText}**`);
|
||||
});
|
||||
|
||||
parts.push(<span key="last-part">{tempText}</span>);
|
||||
|
||||
return parts;
|
||||
return tempText;
|
||||
};
|
||||
|
||||
const onSubmit = (data: FormValues) => {
|
||||
|
|
@ -91,32 +132,53 @@ export default function VariableForm({
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="container mx-auto p-1">
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
|
||||
<div className="mb-6 max-h-screen overflow-auto rounded-md bg-gray-100 p-4 dark:bg-gray-700/50 dark:text-gray-300 md:max-h-80">
|
||||
<p className="text-md whitespace-pre-wrap">{generateHighlightedText()}</p>
|
||||
<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">
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[supersub, remarkGfm, [remarkMath, { singleDollarTextMath: true }]]}
|
||||
rehypePlugins={[
|
||||
[rehypeKatex, { output: 'mathml' }],
|
||||
[rehypeHighlight, { ignoreMissing: true }],
|
||||
]}
|
||||
components={{ code }}
|
||||
className="prose dark:prose-invert light dark:text-gray-70 my-1 max-h-[50vh] break-words"
|
||||
>
|
||||
{generateHighlightedMarkdown()}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-4">
|
||||
<div className="space-y-4">
|
||||
{fields.map((field, index) => (
|
||||
<div key={field.id} className="flex flex-col">
|
||||
<div key={field.id} className="flex flex-col space-y-2">
|
||||
<Controller
|
||||
name={`fields.${index}.value`}
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Textarea
|
||||
{...field}
|
||||
id={`fields.${index}.value`}
|
||||
className="input text-grey-darker h-10 rounded border px-3 py-2 focus:bg-white dark:border-gray-500 dark:focus:bg-gray-700"
|
||||
placeholder={uniqueVariables[index]}
|
||||
onKeyDown={(e) => {
|
||||
// Submit the form on enter like you would with an Input component
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleSubmit((data) => onSubmit(data))();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
render={({ field: inputField }) => {
|
||||
if (field.config.type === 'select') {
|
||||
return (
|
||||
<InputWithDropdown
|
||||
{...inputField}
|
||||
id={`fields.${index}.value`}
|
||||
className={cn(defaultTextProps, 'focus:bg-surface-tertiary')}
|
||||
placeholder={localize('com_ui_enter_var', field.config.variable)}
|
||||
options={field.config.options || []}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TextareaAutosize
|
||||
{...inputField}
|
||||
id={`fields.${index}.value`}
|
||||
className={cn(
|
||||
defaultTextProps,
|
||||
'rounded px-3 py-2 focus:bg-surface-tertiary',
|
||||
)}
|
||||
placeholder={localize('com_ui_enter_var', field.config.variable)}
|
||||
maxRows={8}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
|
|
@ -124,7 +186,7 @@ export default function VariableForm({
|
|||
<div className="flex justify-end">
|
||||
<button
|
||||
type="submit"
|
||||
className="btn rounded bg-green-500 font-bold text-white transition-all hover:bg-green-600"
|
||||
className="btn rounded bg-green-500 px-4 py-2 font-bold text-white transition-all hover:bg-green-600"
|
||||
>
|
||||
{localize('com_ui_submit')}
|
||||
</button>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue