mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-17 17:00:15 +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
|
|
@ -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]" />
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
@ -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} />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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' });
|
||||
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ const Command = ({
|
|||
}
|
||||
};
|
||||
|
||||
if (disabled && !command) {
|
||||
if (disabled === true && !command) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
42
client/src/components/Prompts/Markdown.tsx
Normal file
42
client/src/components/Prompts/Markdown.tsx
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import React from 'react';
|
||||
import { handleDoubleClick } from '~/utils';
|
||||
|
||||
export const CodeVariableGfm = ({ children }: { children: React.ReactNode }) => {
|
||||
return (
|
||||
<code
|
||||
onDoubleClick={handleDoubleClick}
|
||||
className="rounded-md bg-surface-primary-alt p-1 text-xs text-text-secondary md:text-sm"
|
||||
>
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
};
|
||||
|
||||
const regex = /{{(.*?)}}/g;
|
||||
export const PromptVariableGfm = ({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode & React.ReactNode[];
|
||||
}) => {
|
||||
const renderContent = (child: React.ReactNode) => {
|
||||
if (typeof child === 'object' && child !== null) {
|
||||
return child;
|
||||
}
|
||||
if (typeof child !== 'string') {
|
||||
return child;
|
||||
}
|
||||
|
||||
const parts = child.split(regex);
|
||||
return parts.map((part, index) =>
|
||||
index % 2 === 1 ? (
|
||||
<b key={index} className="rounded-md bg-yellow-100/90 p-1 text-gray-700">
|
||||
{`{{${part}}}`}
|
||||
</b>
|
||||
) : (
|
||||
part
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
return <p>{React.Children.map(children, (child) => renderContent(child))}</p>;
|
||||
};
|
||||
|
|
@ -13,7 +13,7 @@ const PreviewPrompt = ({
|
|||
}) => {
|
||||
return (
|
||||
<OGDialog open={open} onOpenChange={onOpenChange}>
|
||||
<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">
|
||||
<div className="p-2">
|
||||
<PromptDetails group={group} />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,18 +1,33 @@
|
|||
import React, { useMemo } from 'react';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import rehypeKatex from 'rehype-katex';
|
||||
import remarkMath from 'remark-math';
|
||||
import supersub from 'remark-supersub';
|
||||
import rehypeHighlight from 'rehype-highlight';
|
||||
import type { TPromptGroup } from 'librechat-data-provider';
|
||||
import { code } from '~/components/Chat/Messages/Content/Markdown';
|
||||
import { useLocalize, useAuthContext } from '~/hooks';
|
||||
import CategoryIcon from './Groups/CategoryIcon';
|
||||
import PromptVariables from './PromptVariables';
|
||||
import { PromptVariableGfm } from './Markdown';
|
||||
import { replaceSpecialVars } from '~/utils';
|
||||
import Description from './Description';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import Command from './Command';
|
||||
|
||||
const PromptDetails = ({ group }: { group: TPromptGroup }) => {
|
||||
const PromptDetails = ({ group }: { group?: TPromptGroup }) => {
|
||||
const localize = useLocalize();
|
||||
const { user } = useAuthContext();
|
||||
|
||||
const mainText = useMemo(() => {
|
||||
const initialText = group?.productionPrompt?.prompt ?? '';
|
||||
return replaceSpecialVars({ text: initialText, user });
|
||||
}, [group?.productionPrompt?.prompt, user]);
|
||||
|
||||
if (!group) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const promptText = group.productionPrompt?.prompt ?? '';
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex flex-col items-center justify-between px-4 dark:text-gray-200 sm:flex-row">
|
||||
|
|
@ -27,17 +42,27 @@ const PromptDetails = ({ group }: { group: TPromptGroup }) => {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex h-full w-full flex-col md:flex-row">
|
||||
<div className="flex flex-1 flex-col gap-4 overflow-y-auto border-gray-300 p-0 dark:border-gray-600 md:max-h-[calc(100vh-150px)] md:p-4">
|
||||
<div className="flex h-full max-h-screen w-full max-w-[90vw] flex-col overflow-y-auto md:flex-row">
|
||||
<div className="flex flex-1 flex-col gap-4 border-gray-300 p-0 dark:border-gray-600 md:max-h-[calc(100vh-150px)] md:p-2">
|
||||
<div>
|
||||
<h2 className="flex items-center justify-between rounded-t-lg border border-gray-300 py-2 pl-4 text-base font-semibold dark:border-gray-600 dark:text-gray-200">
|
||||
{localize('com_ui_prompt_text')}
|
||||
</h2>
|
||||
<div className="group relative min-h-32 rounded-b-lg border border-gray-300 p-4 transition-all duration-150 dark:border-gray-600">
|
||||
<span className="block break-words px-2 py-1 dark:text-gray-200">{promptText}</span>
|
||||
<div className="group relative min-h-32 rounded-b-lg border border-gray-300 p-4 transition-all duration-150 dark:border-gray-600 sm:max-w-full">
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[supersub, remarkGfm, [remarkMath, { singleDollarTextMath: true }]]}
|
||||
rehypePlugins={[
|
||||
[rehypeKatex, { output: 'mathml' }],
|
||||
[rehypeHighlight, { ignoreMissing: true }],
|
||||
]}
|
||||
components={{ p: PromptVariableGfm, code }}
|
||||
className="prose dark:prose-invert light dark:text-gray-70 my-1"
|
||||
>
|
||||
{mainText}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
</div>
|
||||
<PromptVariables promptText={promptText} />
|
||||
<PromptVariables promptText={mainText} showInfo={false} />
|
||||
<Description initialValue={group.oneliner} disabled={true} />
|
||||
<Command initialValue={group.command} disabled={true} />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,13 +1,22 @@
|
|||
import { useMemo, memo } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { EditIcon } from 'lucide-react';
|
||||
import type { PluggableList } from 'unified';
|
||||
import rehypeHighlight from 'rehype-highlight';
|
||||
import { Controller, useFormContext, useFormState } from 'react-hook-form';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import rehypeKatex from 'rehype-katex';
|
||||
import remarkMath from 'remark-math';
|
||||
import supersub from 'remark-supersub';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import AlwaysMakeProd from '~/components/Prompts/Groups/AlwaysMakeProd';
|
||||
import { code } from '~/components/Chat/Messages/Content/Markdown';
|
||||
import { SaveIcon, CrossIcon } from '~/components/svg';
|
||||
import { TextareaAutosize } from '~/components/ui';
|
||||
import { PromptVariableGfm } from './Markdown';
|
||||
import { PromptsEditorMode } from '~/common';
|
||||
import { cn, langSubset } from '~/utils';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import { cn } from '~/utils';
|
||||
import store from '~/store';
|
||||
|
||||
const { promptsEditorMode } = store;
|
||||
|
|
@ -32,6 +41,18 @@ const PromptEditor: React.FC<Props> = ({ name, isEditing, setIsEditing }) => {
|
|||
return isEditing ? SaveIcon : EditIcon;
|
||||
}, [isEditing, prompt]);
|
||||
|
||||
const rehypePlugins: PluggableList = [
|
||||
[rehypeKatex, { output: 'mathml' }],
|
||||
[
|
||||
rehypeHighlight,
|
||||
{
|
||||
detect: true,
|
||||
ignoreMissing: true,
|
||||
subset: langSubset,
|
||||
},
|
||||
],
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 className="flex items-center justify-between rounded-t-lg border border-border-medium py-2 pl-4 text-base font-semibold text-text-primary">
|
||||
|
|
@ -44,7 +65,7 @@ const PromptEditor: React.FC<Props> = ({ name, isEditing, setIsEditing }) => {
|
|||
<EditorIcon
|
||||
className={cn(
|
||||
'icon-lg',
|
||||
isEditing ? 'p-[0.05rem]' : 'text-gray-400 hover:text-gray-600',
|
||||
isEditing ? 'p-[0.05rem]' : 'text-secondary-alt hover:text-text-primary',
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
|
|
@ -54,7 +75,7 @@ const PromptEditor: React.FC<Props> = ({ name, isEditing, setIsEditing }) => {
|
|||
role="button"
|
||||
className={cn(
|
||||
'min-h-[8rem] w-full rounded-b-lg border border-border-medium p-4 transition-all duration-150',
|
||||
{ 'cursor-pointer hover:bg-gray-100/50 dark:hover:bg-gray-100/10': !isEditing },
|
||||
{ 'bg-surface-secondary-alt cursor-pointer hover:bg-surface-tertiary': !isEditing },
|
||||
)}
|
||||
onClick={() => !isEditing && setIsEditing(true)}
|
||||
onKeyDown={(e) => {
|
||||
|
|
@ -85,9 +106,14 @@ const PromptEditor: React.FC<Props> = ({ name, isEditing, setIsEditing }) => {
|
|||
}}
|
||||
/>
|
||||
) : (
|
||||
<pre className="block h-full w-full whitespace-pre-wrap break-words px-2 py-1 text-left text-text-primary">
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[supersub, remarkGfm, [remarkMath, { singleDollarTextMath: true }]]}
|
||||
rehypePlugins={rehypePlugins}
|
||||
components={{ p: PromptVariableGfm, code }}
|
||||
className="markdown prose dark:prose-invert light my-1 w-full break-words text-text-primary"
|
||||
>
|
||||
{field.value}
|
||||
</pre>
|
||||
</ReactMarkdown>
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import { useMemo } from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import { Variable } from 'lucide-react';
|
||||
import { extractUniqueVariables, cn } from '~/utils';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import { cn, extractUniqueVariables } from '~/utils';
|
||||
import { CodeVariableGfm } from './Markdown';
|
||||
import { Separator } from '~/components/ui';
|
||||
import { useLocalize } from '~/hooks';
|
||||
|
||||
|
|
@ -12,7 +14,13 @@ const specialVariables = {
|
|||
const specialVariableClasses =
|
||||
'bg-yellow-500/25 text-yellow-600 dark:border-yellow-500/50 dark:bg-transparent dark:text-yellow-500/90';
|
||||
|
||||
const PromptVariables = ({ promptText }: { promptText: string }) => {
|
||||
const PromptVariables = ({
|
||||
promptText,
|
||||
showInfo = true,
|
||||
}: {
|
||||
promptText: string;
|
||||
showInfo?: boolean;
|
||||
}) => {
|
||||
const localize = useLocalize();
|
||||
|
||||
const variables = useMemo(() => {
|
||||
|
|
@ -32,25 +40,52 @@ const PromptVariables = ({ promptText }: { promptText: string }) => {
|
|||
<label
|
||||
className={cn(
|
||||
'mr-1 rounded-full border border-border-medium px-2 text-text-secondary',
|
||||
specialVariables[variable.toLowerCase()] ? specialVariableClasses : '',
|
||||
specialVariables[variable.toLowerCase()] != null ? specialVariableClasses : '',
|
||||
)}
|
||||
key={index}
|
||||
>
|
||||
{specialVariables[variable.toLowerCase()] ? variable.toLowerCase() : variable}
|
||||
{specialVariables[variable.toLowerCase()] != null
|
||||
? variable.toLowerCase()
|
||||
: variable}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex h-7 items-center">
|
||||
<span className="text-xs text-text-tertiary md:text-sm">
|
||||
{localize('com_ui_variables_info')}
|
||||
<span className="text-xs text-text-secondary md:text-sm">
|
||||
<ReactMarkdown components={{ code: CodeVariableGfm }}>
|
||||
{localize('com_ui_variables_info')}
|
||||
</ReactMarkdown>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<Separator className="my-3 bg-border-medium" />
|
||||
<span className="text-xs text-text-tertiary md:text-sm">
|
||||
{localize('com_ui_special_variables')}
|
||||
</span>
|
||||
{showInfo && (
|
||||
<div className="flex flex-col space-y-4">
|
||||
<div>
|
||||
<span className="text-xs font-medium text-text-secondary md:text-sm">
|
||||
{localize('com_ui_special_variables')}
|
||||
</span>
|
||||
{'\u00A0'}
|
||||
<span className="text-xs text-text-secondary md:text-sm">
|
||||
<ReactMarkdown components={{ code: CodeVariableGfm }}>
|
||||
{localize('com_ui_special_variables_info')}
|
||||
</ReactMarkdown>
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-xs font-medium text-text-secondary md:text-sm">
|
||||
{localize('com_ui_dropdown_variables')}
|
||||
</span>
|
||||
{'\u00A0'}
|
||||
<span className="text-xs text-text-secondary md:text-sm">
|
||||
<ReactMarkdown components={{ code: CodeVariableGfm }}>
|
||||
{localize('com_ui_dropdown_variables_info')}
|
||||
</ReactMarkdown>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import * as React from 'react';
|
||||
|
||||
import { cn } from '../../utils';
|
||||
import { cn } from '~/utils';
|
||||
|
||||
export type InputProps = React.InputHTMLAttributes<HTMLInputElement>;
|
||||
|
||||
|
|
|
|||
154
client/src/components/ui/InputWithDropDown.tsx
Normal file
154
client/src/components/ui/InputWithDropDown.tsx
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
import * as React from 'react';
|
||||
import { Input } from '~/components/ui/Input';
|
||||
import { cn } from '~/utils';
|
||||
|
||||
export type InputWithDropdownProps = React.InputHTMLAttributes<HTMLInputElement> & {
|
||||
options: string[];
|
||||
onSelect?: (value: string) => void;
|
||||
};
|
||||
|
||||
const InputWithDropdown = React.forwardRef<HTMLInputElement, InputWithDropdownProps>(
|
||||
({ className, options, onSelect, ...props }, ref) => {
|
||||
const [isOpen, setIsOpen] = React.useState(false);
|
||||
const [inputValue, setInputValue] = React.useState((props.value as string) || '');
|
||||
const [highlightedIndex, setHighlightedIndex] = React.useState(-1);
|
||||
const inputRef = React.useRef<HTMLInputElement>(null);
|
||||
|
||||
const handleSelect = (value: string) => {
|
||||
setInputValue(value);
|
||||
setIsOpen(false);
|
||||
setHighlightedIndex(-1);
|
||||
if (onSelect) {
|
||||
onSelect(value);
|
||||
}
|
||||
if (props.onChange) {
|
||||
props.onChange({ target: { value } } as React.ChangeEvent<HTMLInputElement>);
|
||||
}
|
||||
};
|
||||
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setInputValue(e.target.value);
|
||||
if (props.onChange) {
|
||||
props.onChange(e);
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
switch (e.key) {
|
||||
case 'ArrowDown':
|
||||
e.preventDefault();
|
||||
if (!isOpen) {
|
||||
setIsOpen(true);
|
||||
} else {
|
||||
setHighlightedIndex((prevIndex) =>
|
||||
prevIndex < options.length - 1 ? prevIndex + 1 : prevIndex,
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
e.preventDefault();
|
||||
setHighlightedIndex((prevIndex) => (prevIndex > 0 ? prevIndex - 1 : 0));
|
||||
break;
|
||||
case 'Enter':
|
||||
e.preventDefault();
|
||||
if (isOpen && highlightedIndex !== -1) {
|
||||
handleSelect(options[highlightedIndex]);
|
||||
}
|
||||
setIsOpen(false);
|
||||
break;
|
||||
case 'Escape':
|
||||
setIsOpen(false);
|
||||
setHighlightedIndex(-1);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (inputRef.current && !inputRef.current.contains(event.target as Node)) {
|
||||
setIsOpen(false);
|
||||
setHighlightedIndex(-1);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="relative" ref={inputRef}>
|
||||
<div className="relative">
|
||||
<Input
|
||||
{...props}
|
||||
value={inputValue}
|
||||
onChange={handleInputChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
aria-haspopup="listbox"
|
||||
aria-controls="dropdown-list"
|
||||
className={cn('bg-surface-secondary', className ?? '')}
|
||||
ref={ref}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="text-tertiary hover:text-secondary absolute inset-y-0 right-0 flex items-center rounded-md px-2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring-primary"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
aria-label={isOpen ? 'Close dropdown' : 'Open dropdown'}
|
||||
>
|
||||
<svg
|
||||
className="h-5 w-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M19 9l-7 7-7-7"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
{isOpen && (
|
||||
<ul
|
||||
id="dropdown-list"
|
||||
role="listbox"
|
||||
className="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md border border-border-medium bg-surface-secondary shadow-lg focus:ring-1 focus:ring-inset focus:ring-ring-primary"
|
||||
>
|
||||
{options.map((option, index) => (
|
||||
<li
|
||||
key={index}
|
||||
role="option"
|
||||
aria-selected={index === highlightedIndex}
|
||||
className={cn(
|
||||
'cursor-pointer rounded-md px-3 py-2',
|
||||
'focus:bg-surface-tertiary focus:outline-none focus:ring-1 focus:ring-inset focus:ring-ring-primary',
|
||||
index === highlightedIndex
|
||||
? 'text-primary bg-surface-active'
|
||||
: 'text-secondary hover:bg-surface-tertiary',
|
||||
)}
|
||||
onClick={() => handleSelect(option)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
handleSelect(option);
|
||||
}
|
||||
}}
|
||||
tabIndex={0}
|
||||
>
|
||||
{option}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
InputWithDropdown.displayName = 'InputWithDropdown';
|
||||
|
||||
export default InputWithDropdown;
|
||||
|
|
@ -25,10 +25,11 @@ export * from './Tooltip';
|
|||
export { default as Combobox } from './Combobox';
|
||||
export { default as Dropdown } from './Dropdown';
|
||||
export { default as FileUpload } from './FileUpload';
|
||||
export { default as DropdownPopup } from './DropdownPopup';
|
||||
export { default as DelayedRender } from './DelayedRender';
|
||||
export { default as ThemeSelector } from './ThemeSelector';
|
||||
export { default as SelectDropDown } from './SelectDropDown';
|
||||
export { default as MultiSelectPop } from './MultiSelectPop';
|
||||
export { default as InputWithDropdown } from './InputWithDropDown';
|
||||
export { default as SelectDropDownPop } from './SelectDropDownPop';
|
||||
export { default as MultiSelectDropDown } from './MultiSelectDropDown';
|
||||
export { default as DropdownPopup } from './DropdownPopup';
|
||||
|
|
|
|||
|
|
@ -143,9 +143,13 @@ export default {
|
|||
com_ui_manage: 'Manage',
|
||||
com_ui_variables: 'Variables',
|
||||
com_ui_variables_info:
|
||||
'Use double braces in your text to create variables, e.g. {{example variable}}, to later fill when using the prompt.',
|
||||
com_ui_special_variables:
|
||||
'Special variables: Use {{current_date}} for the current date, and {{current_user}} for your given account name.',
|
||||
'Use double braces in your text to create variables, e.g. `{{example variable}}`, to later fill when using the prompt.',
|
||||
com_ui_special_variables: 'Special variables:',
|
||||
com_ui_special_variables_info:
|
||||
'Use `{{current_date}}` for the current date, and `{{current_user}}` for your given account name.',
|
||||
com_ui_dropdown_variables: 'Dropdown variables:',
|
||||
com_ui_dropdown_variables_info:
|
||||
'Create custom dropdown menus for your prompts: `{{variable_name:option1|option2|option3}}`',
|
||||
com_ui_showing: 'Showing',
|
||||
com_ui_of: 'of',
|
||||
com_ui_entries: 'Entries',
|
||||
|
|
@ -239,6 +243,7 @@ export default {
|
|||
com_ui_create_prompt: 'Create Prompt',
|
||||
com_ui_share: 'Share',
|
||||
com_ui_share_var: 'Share {0}',
|
||||
com_ui_enter_var: 'Enter {0}',
|
||||
com_ui_copy_link: 'Copy link',
|
||||
com_ui_update_link: 'Update link',
|
||||
com_ui_create_link: 'Create link',
|
||||
|
|
|
|||
|
|
@ -71,3 +71,14 @@ export const defaultTextPropsLabel =
|
|||
export function capitalizeFirstLetter(string: string) {
|
||||
return string.charAt(0).toUpperCase() + string.slice(1);
|
||||
}
|
||||
|
||||
export const handleDoubleClick: React.MouseEventHandler<HTMLElement> = (event) => {
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(event.target as Node);
|
||||
const selection = window.getSelection();
|
||||
if (!selection) {
|
||||
return;
|
||||
}
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue