🧪 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:
Danny Avila 2024-08-18 05:52:05 -04:00 committed by GitHub
parent bbb9324447
commit d3a20357e9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 473 additions and 110 deletions

View file

@ -111,10 +111,9 @@ const CreatePromptForm = ({
<Input
{...field}
type="text"
className="mr-2 w-full border border-gray-300 p-2 text-2xl dark:border-gray-600"
className="mr-2 w-full border border-border-medium p-2 text-2xl placeholder:text-text-tertiary dark:placeholder:text-text-secondary"
placeholder={`${localize('com_ui_prompt_name')}*`}
tabIndex={1}
autoFocus={true}
tabIndex={0}
/>
<div
className={cn(
@ -127,15 +126,15 @@ const CreatePromptForm = ({
</div>
)}
/>
<CategorySelector tabIndex={5} />
<CategorySelector tabIndex={0} />
</div>
</div>
<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-gray-300 py-2 pl-4 pr-1 text-base font-semibold dark:border-gray-600 dark:text-gray-200">
<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')}*
</h2>
<div className="min-h-32 rounded-b-lg border border-gray-300 p-4 transition-all duration-150 dark:border-gray-600">
<div className="min-h-32 rounded-b-lg border border-border-medium p-4 transition-all duration-150">
<Controller
name="prompt"
control={control}
@ -144,9 +143,9 @@ const CreatePromptForm = ({
<div>
<TextareaAutosize
{...field}
className="w-full rounded border border-gray-300 px-2 py-1 focus:outline-none dark:border-gray-600 dark:bg-transparent dark:text-gray-200"
className="w-full rounded border border-border-medium px-2 py-1 focus:outline-none dark:bg-transparent dark:text-gray-200"
minRows={6}
tabIndex={2}
tabIndex={0}
/>
<div
className={`mt-1 text-sm text-red-500 ${
@ -163,16 +162,11 @@ const CreatePromptForm = ({
<PromptVariables promptText={promptText} />
<Description
onValueChange={(value) => methods.setValue('oneliner', value)}
tabIndex={3}
tabIndex={0}
/>
<Command onValueChange={(value) => methods.setValue('command', value)} tabIndex={4} />
<Command onValueChange={(value) => methods.setValue('command', value)} tabIndex={0} />
<div className="mt-4 flex justify-end">
<Button
tabIndex={6}
type="submit"
variant="default"
disabled={!isDirty || isSubmitting || !isValid}
>
<Button tabIndex={0} type="submit" disabled={!isDirty || isSubmitting || !isValid}>
{localize('com_ui_create_prompt')}
</Button>
</div>

View file

@ -31,7 +31,7 @@ const VariableDialog: React.FC<VariableDialogProps> = ({ open, onClose, group })
return (
<OGDialog open={open} onOpenChange={handleOpenChange}>
<OGDialogContent className="max-w-3xl bg-white dark:border-gray-700 dark:bg-gray-850 dark:text-gray-300">
<OGDialogContent className="max-w-full bg-white dark:border-gray-700 dark:bg-gray-850 dark:text-gray-300 md:max-w-3xl">
<OGDialogTitle>{group.name}</OGDialogTitle>
<VariableForm group={group} onClose={onClose} />
</OGDialogContent>

View file

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