Skip to content

Commit

Permalink
🧪 feat: Prompt Dropdown Variable; style: Add Markdown Support (danny-…
Browse files Browse the repository at this point in the history
…avila#3681)

* feat: Add extended inputs for promts library variables

* feat: Add maxRows prop to VariableForm input field

* 📩 feat: invite user (danny-avila#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>
  • Loading branch information
4 people authored Aug 18, 2024
1 parent 74ce382 commit fe1eed2
Show file tree
Hide file tree
Showing 19 changed files with 473 additions and 110 deletions.
2 changes: 1 addition & 1 deletion client/src/components/Chat/Input/OptionsPopover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export default function OptionsPopover({
{presetsDisabled ? null : (
<Button
type="button"
className="h-auto w-[150px] justify-start rounded-md border border-gray-300/50 bg-transparent px-2 py-1 text-xs font-normal text-black hover:bg-gray-100 hover:text-black focus:ring-1 focus:ring-ring-primary dark:border-gray-500/50 dark:bg-transparent dark:text-white dark:hover:bg-gray-600 dark:focus:ring-white"
className="h-auto w-[150px] justify-start rounded-md border border-gray-300/50 bg-transparent px-2 py-1 text-xs font-normal text-black hover:bg-gray-100 hover:text-black focus-visible:ring-1 focus-visible:ring-ring-primary dark:border-gray-600 dark:bg-transparent dark:text-white dark:hover:bg-gray-600 dark:focus-visible:ring-white"
onClick={saveAsPreset}
>
<Save className="mr-1 w-[14px]" />
Expand Down
10 changes: 7 additions & 3 deletions client/src/components/Chat/Messages/Content/Markdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { useRecoilValue } from 'recoil';
import ReactMarkdown from 'react-markdown';
import type { PluggableList } from 'unified';
import rehypeHighlight from 'rehype-highlight';
import { cn, langSubset, validateIframe, processLaTeX } from '~/utils';
import { cn, langSubset, validateIframe, processLaTeX, handleDoubleClick } from '~/utils';
import CodeBlock from '~/components/Messages/Content/CodeBlock';
import { useFileDownload } from '~/data-provider';
import useLocalize from '~/hooks/useLocalize';
Expand All @@ -21,12 +21,16 @@ type TCodeProps = {
children: React.ReactNode;
};

export const code = memo(({ inline, className, children }: TCodeProps) => {
export const code: React.ElementType = memo(({ inline, className, children }: TCodeProps) => {
const match = /language-(\w+)/.exec(className ?? '');
const lang = match && match[1];

if (inline) {
return <code className={className}>{children}</code>;
return (
<code onDoubleClick={handleDoubleClick} className={className}>
{children}
</code>
);
} else {
return <CodeBlock lang={lang ?? 'text'} codeChildren={children} />;
}
Expand Down
2 changes: 1 addition & 1 deletion client/src/components/Endpoints/Settings/OpenAI.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ export default function Settings({ conversation, setOption, models, readonly }:
placeholder={localize('com_endpoint_openai_prompt_prefix_placeholder')}
className={cn(
defaultTextProps,
'flex max-h-[138px] min-h-[100px] w-full resize-none px-3 py-2 ',
'flex max-h-[138px] min-h-[100px] w-full resize-none px-3 py-2 transition-colors focus:outline-none',
)}
/>
</div>
Expand Down
14 changes: 9 additions & 5 deletions client/src/components/Messages/Content/CodeBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,18 @@ const CodeBar: React.FC<CodeBarProps> = React.memo(({ lang, codeRef, error, plug
return (
<div className="relative flex items-center rounded-tl-md rounded-tr-md bg-gray-700 px-4 py-2 font-sans text-xs text-gray-200 dark:bg-gray-700">
<span className="">{lang}</span>
{plugin ? (
{plugin === true ? (
<InfoIcon className="ml-auto flex h-4 w-4 gap-2 text-white/50" />
) : (
<button
className={cn('ml-auto flex gap-2', error ? 'h-4 w-4 items-start text-white/50' : '')}
type="button"
className={cn(
'ml-auto flex gap-2',
error === true ? 'h-4 w-4 items-start text-white/50' : '',
)}
onClick={async () => {
const codeString = codeRef.current?.textContent;
if (codeString) {
if (codeString != null) {
setIsCopied(true);
copy(codeString, { format: 'text/plain' });

Expand All @@ -44,12 +48,12 @@ const CodeBar: React.FC<CodeBarProps> = React.memo(({ lang, codeRef, error, plug
{isCopied ? (
<>
<CheckMark className="h-[18px] w-[18px]" />
{error ? '' : localize('com_ui_copied')}
{error === true ? '' : localize('com_ui_copied')}
</>
) : (
<>
<Clipboard />
{error ? '' : localize('com_ui_copy_code')}
{error === true ? '' : localize('com_ui_copy_code')}
</>
)}
</button>
Expand Down
8 changes: 4 additions & 4 deletions client/src/components/Prompts/Command.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ const Command = ({
}
};

if (disabled && !command) {
if (disabled === true && !command) {
return null;
}

Expand All @@ -53,10 +53,10 @@ const Command = ({
placeholder={localize('com_ui_command_placeholder')}
value={command}
onChange={handleInputChange}
className="w-full rounded-lg border-none bg-surface-tertiary p-1 text-text-primary placeholder:text-text-secondary-alt focus:bg-surface-tertiary focus:outline-none focus:ring-0 md:w-96"
className="w-full rounded-lg border-none bg-surface-tertiary p-1 text-text-primary placeholder:text-text-secondary focus:bg-surface-tertiary focus:outline-none focus:ring-1 focus:ring-inset focus:ring-ring-primary md:w-96"
/>
{!disabled && (
<span className="mr-1 w-10 text-xs text-text-tertiary md:text-sm">{`${charCount}/${Constants.COMMANDS_MAX_LENGTH}`}</span>
{disabled !== true && (
<span className="mr-1 w-10 text-xs text-text-secondary md:text-sm">{`${charCount}/${Constants.COMMANDS_MAX_LENGTH}`}</span>
)}
</h3>
</div>
Expand Down
4 changes: 2 additions & 2 deletions client/src/components/Prompts/Description.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,10 @@ const Description = ({
placeholder={localize('com_ui_description_placeholder')}
value={description}
onChange={handleInputChange}
className="w-full rounded-lg border-none bg-surface-tertiary p-1 text-text-primary placeholder:text-text-secondary-alt focus:bg-surface-tertiary focus:outline-none focus:ring-0 md:w-96"
className="w-full rounded-lg border-none bg-surface-tertiary p-1 text-text-primary placeholder:text-text-secondary focus:bg-surface-tertiary focus:outline-none focus:ring-1 focus:ring-inset focus:ring-ring-primary md:w-96"
/>
{!disabled && (
<span className="mr-1 w-10 text-xs text-text-tertiary md:text-sm">{`${charCount}/${MAX_LENGTH}`}</span>
<span className="mr-1 w-10 text-xs text-text-secondary md:text-sm">{`${charCount}/${MAX_LENGTH}`}</span>
)}
</h3>
</div>
Expand Down
26 changes: 10 additions & 16 deletions client/src/components/Prompts/Groups/CreatePromptForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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}
Expand All @@ -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 ${
Expand All @@ -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>
Expand Down
2 changes: 1 addition & 1 deletion client/src/components/Prompts/Groups/VariableDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
154 changes: 108 additions & 46 deletions client/src/components/Prompts/Groups/VariableForm.tsx
Original file line number Diff line number Diff line change
@@ -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({
Expand All @@ -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),
})),
},
});

Expand All @@ -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) => {
Expand All @@ -91,40 +132,61 @@ 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>
))}
</div>
<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>
Expand Down
Loading

0 comments on commit fe1eed2

Please sign in to comment.