mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-17 08:50: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 : (
|
{presetsDisabled ? null : (
|
||||||
<Button
|
<Button
|
||||||
type="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}
|
onClick={saveAsPreset}
|
||||||
>
|
>
|
||||||
<Save className="mr-1 w-[14px]" />
|
<Save className="mr-1 w-[14px]" />
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import { useRecoilValue } from 'recoil';
|
||||||
import ReactMarkdown from 'react-markdown';
|
import ReactMarkdown from 'react-markdown';
|
||||||
import type { PluggableList } from 'unified';
|
import type { PluggableList } from 'unified';
|
||||||
import rehypeHighlight from 'rehype-highlight';
|
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 CodeBlock from '~/components/Messages/Content/CodeBlock';
|
||||||
import { useFileDownload } from '~/data-provider';
|
import { useFileDownload } from '~/data-provider';
|
||||||
import useLocalize from '~/hooks/useLocalize';
|
import useLocalize from '~/hooks/useLocalize';
|
||||||
|
|
@ -21,12 +21,16 @@ type TCodeProps = {
|
||||||
children: React.ReactNode;
|
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 match = /language-(\w+)/.exec(className ?? '');
|
||||||
const lang = match && match[1];
|
const lang = match && match[1];
|
||||||
|
|
||||||
if (inline) {
|
if (inline) {
|
||||||
return <code className={className}>{children}</code>;
|
return (
|
||||||
|
<code onDoubleClick={handleDoubleClick} className={className}>
|
||||||
|
{children}
|
||||||
|
</code>
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
return <CodeBlock lang={lang ?? 'text'} codeChildren={children} />;
|
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')}
|
placeholder={localize('com_endpoint_openai_prompt_prefix_placeholder')}
|
||||||
className={cn(
|
className={cn(
|
||||||
defaultTextProps,
|
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>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -24,14 +24,18 @@ const CodeBar: React.FC<CodeBarProps> = React.memo(({ lang, codeRef, error, plug
|
||||||
return (
|
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">
|
<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>
|
<span className="">{lang}</span>
|
||||||
{plugin ? (
|
{plugin === true ? (
|
||||||
<InfoIcon className="ml-auto flex h-4 w-4 gap-2 text-white/50" />
|
<InfoIcon className="ml-auto flex h-4 w-4 gap-2 text-white/50" />
|
||||||
) : (
|
) : (
|
||||||
<button
|
<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 () => {
|
onClick={async () => {
|
||||||
const codeString = codeRef.current?.textContent;
|
const codeString = codeRef.current?.textContent;
|
||||||
if (codeString) {
|
if (codeString != null) {
|
||||||
setIsCopied(true);
|
setIsCopied(true);
|
||||||
copy(codeString, { format: 'text/plain' });
|
copy(codeString, { format: 'text/plain' });
|
||||||
|
|
||||||
|
|
@ -44,12 +48,12 @@ const CodeBar: React.FC<CodeBarProps> = React.memo(({ lang, codeRef, error, plug
|
||||||
{isCopied ? (
|
{isCopied ? (
|
||||||
<>
|
<>
|
||||||
<CheckMark className="h-[18px] w-[18px]" />
|
<CheckMark className="h-[18px] w-[18px]" />
|
||||||
{error ? '' : localize('com_ui_copied')}
|
{error === true ? '' : localize('com_ui_copied')}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Clipboard />
|
<Clipboard />
|
||||||
{error ? '' : localize('com_ui_copy_code')}
|
{error === true ? '' : localize('com_ui_copy_code')}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ const Command = ({
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (disabled && !command) {
|
if (disabled === true && !command) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -53,10 +53,10 @@ const Command = ({
|
||||||
placeholder={localize('com_ui_command_placeholder')}
|
placeholder={localize('com_ui_command_placeholder')}
|
||||||
value={command}
|
value={command}
|
||||||
onChange={handleInputChange}
|
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 && (
|
{disabled !== true && (
|
||||||
<span className="mr-1 w-10 text-xs text-text-tertiary md:text-sm">{`${charCount}/${Constants.COMMANDS_MAX_LENGTH}`}</span>
|
<span className="mr-1 w-10 text-xs text-text-secondary md:text-sm">{`${charCount}/${Constants.COMMANDS_MAX_LENGTH}`}</span>
|
||||||
)}
|
)}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -50,10 +50,10 @@ const Description = ({
|
||||||
placeholder={localize('com_ui_description_placeholder')}
|
placeholder={localize('com_ui_description_placeholder')}
|
||||||
value={description}
|
value={description}
|
||||||
onChange={handleInputChange}
|
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 && (
|
{!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>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -111,10 +111,9 @@ const CreatePromptForm = ({
|
||||||
<Input
|
<Input
|
||||||
{...field}
|
{...field}
|
||||||
type="text"
|
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')}*`}
|
placeholder={`${localize('com_ui_prompt_name')}*`}
|
||||||
tabIndex={1}
|
tabIndex={0}
|
||||||
autoFocus={true}
|
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
|
|
@ -127,15 +126,15 @@ const CreatePromptForm = ({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<CategorySelector tabIndex={5} />
|
<CategorySelector tabIndex={0} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex w-full flex-col gap-4 md:mt-[1.075rem]">
|
<div className="flex w-full flex-col gap-4 md:mt-[1.075rem]">
|
||||||
<div>
|
<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')}*
|
{localize('com_ui_prompt_text')}*
|
||||||
</h2>
|
</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
|
<Controller
|
||||||
name="prompt"
|
name="prompt"
|
||||||
control={control}
|
control={control}
|
||||||
|
|
@ -144,9 +143,9 @@ const CreatePromptForm = ({
|
||||||
<div>
|
<div>
|
||||||
<TextareaAutosize
|
<TextareaAutosize
|
||||||
{...field}
|
{...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}
|
minRows={6}
|
||||||
tabIndex={2}
|
tabIndex={0}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className={`mt-1 text-sm text-red-500 ${
|
className={`mt-1 text-sm text-red-500 ${
|
||||||
|
|
@ -163,16 +162,11 @@ const CreatePromptForm = ({
|
||||||
<PromptVariables promptText={promptText} />
|
<PromptVariables promptText={promptText} />
|
||||||
<Description
|
<Description
|
||||||
onValueChange={(value) => methods.setValue('oneliner', value)}
|
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">
|
<div className="mt-4 flex justify-end">
|
||||||
<Button
|
<Button tabIndex={0} type="submit" disabled={!isDirty || isSubmitting || !isValid}>
|
||||||
tabIndex={6}
|
|
||||||
type="submit"
|
|
||||||
variant="default"
|
|
||||||
disabled={!isDirty || isSubmitting || !isValid}
|
|
||||||
>
|
|
||||||
{localize('com_ui_create_prompt')}
|
{localize('com_ui_create_prompt')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ const VariableDialog: React.FC<VariableDialogProps> = ({ open, onClose, group })
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<OGDialog open={open} onOpenChange={handleOpenChange}>
|
<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>
|
<OGDialogTitle>{group.name}</OGDialogTitle>
|
||||||
<VariableForm group={group} onClose={onClose} />
|
<VariableForm group={group} onClose={onClose} />
|
||||||
</OGDialogContent>
|
</OGDialogContent>
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,64 @@
|
||||||
import { useMemo } from 'react';
|
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 { useForm, useFieldArray, Controller, useWatch } from 'react-hook-form';
|
||||||
import type { TPromptGroup } from 'librechat-data-provider';
|
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 { 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 = {
|
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({
|
export default function VariableForm({
|
||||||
|
|
@ -32,7 +84,11 @@ export default function VariableForm({
|
||||||
const { submitPrompt } = useSubmitMessage();
|
const { submitPrompt } = useSubmitMessage();
|
||||||
const { control, handleSubmit } = useForm<FormValues>({
|
const { control, handleSubmit } = useForm<FormValues>({
|
||||||
defaultValues: {
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const generateHighlightedText = () => {
|
const generateHighlightedMarkdown = () => {
|
||||||
let tempText = mainText;
|
let tempText = mainText;
|
||||||
const parts: JSX.Element[] = [];
|
allVariables.forEach((variable) => {
|
||||||
|
|
||||||
allVariables.forEach((variable, index) => {
|
|
||||||
const placeholder = `{{${variable}}}`;
|
const placeholder = `{{${variable}}}`;
|
||||||
const partsBeforePlaceholder = tempText.split(placeholder);
|
|
||||||
const fieldIndex = variableIndexMap.get(variable) as string | number;
|
const fieldIndex = variableIndexMap.get(variable) as string | number;
|
||||||
const fieldValue = fieldValues[fieldIndex].value as string;
|
const fieldValue = fieldValues[fieldIndex].value as string;
|
||||||
parts.push(
|
const highlightText = fieldValue !== '' ? fieldValue : placeholder;
|
||||||
<span key={`before-${index}`}>{partsBeforePlaceholder[0]}</span>,
|
tempText = tempText.replaceAll(placeholder, `**${highlightText}**`);
|
||||||
<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);
|
|
||||||
});
|
});
|
||||||
|
return tempText;
|
||||||
parts.push(<span key="last-part">{tempText}</span>);
|
|
||||||
|
|
||||||
return parts;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSubmit = (data: FormValues) => {
|
const onSubmit = (data: FormValues) => {
|
||||||
|
|
@ -91,32 +132,53 @@ export default function VariableForm({
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto p-1">
|
<div className="mx-auto p-1 md:container">
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
|
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
||||||
<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">
|
<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">
|
||||||
<p className="text-md whitespace-pre-wrap">{generateHighlightedText()}</p>
|
<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>
|
||||||
<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) => (
|
{fields.map((field, index) => (
|
||||||
<div key={field.id} className="flex flex-col">
|
<div key={field.id} className="flex flex-col space-y-2">
|
||||||
<Controller
|
<Controller
|
||||||
name={`fields.${index}.value`}
|
name={`fields.${index}.value`}
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field }) => (
|
render={({ field: inputField }) => {
|
||||||
<Textarea
|
if (field.config.type === 'select') {
|
||||||
{...field}
|
return (
|
||||||
|
<InputWithDropdown
|
||||||
|
{...inputField}
|
||||||
id={`fields.${index}.value`}
|
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"
|
className={cn(defaultTextProps, 'focus:bg-surface-tertiary')}
|
||||||
placeholder={uniqueVariables[index]}
|
placeholder={localize('com_ui_enter_var', field.config.variable)}
|
||||||
onKeyDown={(e) => {
|
options={field.config.options || []}
|
||||||
// Submit the form on enter like you would with an Input component
|
|
||||||
if (e.key === 'Enter' && !e.shiftKey) {
|
|
||||||
e.preventDefault();
|
|
||||||
handleSubmit((data) => onSubmit(data))();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
||||||
))}
|
))}
|
||||||
|
|
@ -124,7 +186,7 @@ export default function VariableForm({
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<button
|
<button
|
||||||
type="submit"
|
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')}
|
{localize('com_ui_submit')}
|
||||||
</button>
|
</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 (
|
return (
|
||||||
<OGDialog open={open} onOpenChange={onOpenChange}>
|
<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">
|
<div className="p-2">
|
||||||
<PromptDetails group={group} />
|
<PromptDetails group={group} />
|
||||||
</div>
|
</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 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 CategoryIcon from './Groups/CategoryIcon';
|
||||||
import PromptVariables from './PromptVariables';
|
import PromptVariables from './PromptVariables';
|
||||||
|
import { PromptVariableGfm } from './Markdown';
|
||||||
|
import { replaceSpecialVars } from '~/utils';
|
||||||
import Description from './Description';
|
import Description from './Description';
|
||||||
import { useLocalize } from '~/hooks';
|
|
||||||
import Command from './Command';
|
import Command from './Command';
|
||||||
|
|
||||||
const PromptDetails = ({ group }: { group: TPromptGroup }) => {
|
const PromptDetails = ({ group }: { group?: TPromptGroup }) => {
|
||||||
const localize = useLocalize();
|
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) {
|
if (!group) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const promptText = group.productionPrompt?.prompt ?? '';
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="flex flex-col items-center justify-between px-4 dark:text-gray-200 sm:flex-row">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex h-full w-full flex-col md:flex-row">
|
<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 overflow-y-auto border-gray-300 p-0 dark:border-gray-600 md:max-h-[calc(100vh-150px)] md:p-4">
|
<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>
|
<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">
|
<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')}
|
{localize('com_ui_prompt_text')}
|
||||||
</h2>
|
</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">
|
<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">
|
||||||
<span className="block break-words px-2 py-1 dark:text-gray-200">{promptText}</span>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
<PromptVariables promptText={promptText} />
|
<PromptVariables promptText={mainText} showInfo={false} />
|
||||||
<Description initialValue={group.oneliner} disabled={true} />
|
<Description initialValue={group.oneliner} disabled={true} />
|
||||||
<Command initialValue={group.command} disabled={true} />
|
<Command initialValue={group.command} disabled={true} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,22 @@
|
||||||
import { useMemo, memo } from 'react';
|
import { useMemo, memo } from 'react';
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
import { EditIcon } from 'lucide-react';
|
import { EditIcon } from 'lucide-react';
|
||||||
|
import type { PluggableList } from 'unified';
|
||||||
|
import rehypeHighlight from 'rehype-highlight';
|
||||||
import { Controller, useFormContext, useFormState } from 'react-hook-form';
|
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 AlwaysMakeProd from '~/components/Prompts/Groups/AlwaysMakeProd';
|
||||||
|
import { code } from '~/components/Chat/Messages/Content/Markdown';
|
||||||
import { SaveIcon, CrossIcon } from '~/components/svg';
|
import { SaveIcon, CrossIcon } from '~/components/svg';
|
||||||
import { TextareaAutosize } from '~/components/ui';
|
import { TextareaAutosize } from '~/components/ui';
|
||||||
|
import { PromptVariableGfm } from './Markdown';
|
||||||
import { PromptsEditorMode } from '~/common';
|
import { PromptsEditorMode } from '~/common';
|
||||||
|
import { cn, langSubset } from '~/utils';
|
||||||
import { useLocalize } from '~/hooks';
|
import { useLocalize } from '~/hooks';
|
||||||
import { cn } from '~/utils';
|
|
||||||
import store from '~/store';
|
import store from '~/store';
|
||||||
|
|
||||||
const { promptsEditorMode } = store;
|
const { promptsEditorMode } = store;
|
||||||
|
|
@ -32,6 +41,18 @@ const PromptEditor: React.FC<Props> = ({ name, isEditing, setIsEditing }) => {
|
||||||
return isEditing ? SaveIcon : EditIcon;
|
return isEditing ? SaveIcon : EditIcon;
|
||||||
}, [isEditing, prompt]);
|
}, [isEditing, prompt]);
|
||||||
|
|
||||||
|
const rehypePlugins: PluggableList = [
|
||||||
|
[rehypeKatex, { output: 'mathml' }],
|
||||||
|
[
|
||||||
|
rehypeHighlight,
|
||||||
|
{
|
||||||
|
detect: true,
|
||||||
|
ignoreMissing: true,
|
||||||
|
subset: langSubset,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<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">
|
<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
|
<EditorIcon
|
||||||
className={cn(
|
className={cn(
|
||||||
'icon-lg',
|
'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>
|
</button>
|
||||||
|
|
@ -54,7 +75,7 @@ const PromptEditor: React.FC<Props> = ({ name, isEditing, setIsEditing }) => {
|
||||||
role="button"
|
role="button"
|
||||||
className={cn(
|
className={cn(
|
||||||
'min-h-[8rem] w-full rounded-b-lg border border-border-medium p-4 transition-all duration-150',
|
'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)}
|
onClick={() => !isEditing && setIsEditing(true)}
|
||||||
onKeyDown={(e) => {
|
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}
|
{field.value}
|
||||||
</pre>
|
</ReactMarkdown>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
import { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { Variable } from 'lucide-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 { Separator } from '~/components/ui';
|
||||||
import { useLocalize } from '~/hooks';
|
import { useLocalize } from '~/hooks';
|
||||||
|
|
||||||
|
|
@ -12,7 +14,13 @@ const specialVariables = {
|
||||||
const specialVariableClasses =
|
const specialVariableClasses =
|
||||||
'bg-yellow-500/25 text-yellow-600 dark:border-yellow-500/50 dark:bg-transparent dark:text-yellow-500/90';
|
'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 localize = useLocalize();
|
||||||
|
|
||||||
const variables = useMemo(() => {
|
const variables = useMemo(() => {
|
||||||
|
|
@ -32,25 +40,52 @@ const PromptVariables = ({ promptText }: { promptText: string }) => {
|
||||||
<label
|
<label
|
||||||
className={cn(
|
className={cn(
|
||||||
'mr-1 rounded-full border border-border-medium px-2 text-text-secondary',
|
'mr-1 rounded-full border border-border-medium px-2 text-text-secondary',
|
||||||
specialVariables[variable.toLowerCase()] ? specialVariableClasses : '',
|
specialVariables[variable.toLowerCase()] != null ? specialVariableClasses : '',
|
||||||
)}
|
)}
|
||||||
key={index}
|
key={index}
|
||||||
>
|
>
|
||||||
{specialVariables[variable.toLowerCase()] ? variable.toLowerCase() : variable}
|
{specialVariables[variable.toLowerCase()] != null
|
||||||
|
? variable.toLowerCase()
|
||||||
|
: variable}
|
||||||
</label>
|
</label>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex h-7 items-center">
|
<div className="flex h-7 items-center">
|
||||||
<span className="text-xs text-text-tertiary md:text-sm">
|
<span className="text-xs text-text-secondary md:text-sm">
|
||||||
|
<ReactMarkdown components={{ code: CodeVariableGfm }}>
|
||||||
{localize('com_ui_variables_info')}
|
{localize('com_ui_variables_info')}
|
||||||
|
</ReactMarkdown>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<Separator className="my-3 bg-border-medium" />
|
<Separator className="my-3 bg-border-medium" />
|
||||||
<span className="text-xs text-text-tertiary md:text-sm">
|
{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')}
|
{localize('com_ui_special_variables')}
|
||||||
</span>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
|
||||||
import { cn } from '../../utils';
|
import { cn } from '~/utils';
|
||||||
|
|
||||||
export type InputProps = React.InputHTMLAttributes<HTMLInputElement>;
|
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 Combobox } from './Combobox';
|
||||||
export { default as Dropdown } from './Dropdown';
|
export { default as Dropdown } from './Dropdown';
|
||||||
export { default as FileUpload } from './FileUpload';
|
export { default as FileUpload } from './FileUpload';
|
||||||
|
export { default as DropdownPopup } from './DropdownPopup';
|
||||||
export { default as DelayedRender } from './DelayedRender';
|
export { default as DelayedRender } from './DelayedRender';
|
||||||
export { default as ThemeSelector } from './ThemeSelector';
|
export { default as ThemeSelector } from './ThemeSelector';
|
||||||
export { default as SelectDropDown } from './SelectDropDown';
|
export { default as SelectDropDown } from './SelectDropDown';
|
||||||
export { default as MultiSelectPop } from './MultiSelectPop';
|
export { default as MultiSelectPop } from './MultiSelectPop';
|
||||||
|
export { default as InputWithDropdown } from './InputWithDropDown';
|
||||||
export { default as SelectDropDownPop } from './SelectDropDownPop';
|
export { default as SelectDropDownPop } from './SelectDropDownPop';
|
||||||
export { default as MultiSelectDropDown } from './MultiSelectDropDown';
|
export { default as MultiSelectDropDown } from './MultiSelectDropDown';
|
||||||
export { default as DropdownPopup } from './DropdownPopup';
|
|
||||||
|
|
|
||||||
|
|
@ -143,9 +143,13 @@ export default {
|
||||||
com_ui_manage: 'Manage',
|
com_ui_manage: 'Manage',
|
||||||
com_ui_variables: 'Variables',
|
com_ui_variables: 'Variables',
|
||||||
com_ui_variables_info:
|
com_ui_variables_info:
|
||||||
'Use double braces in your text to create variables, e.g. {{example variable}}, to later fill when using the prompt.',
|
'Use double braces in your text to create variables, e.g. `{{example variable}}`, to later fill when using the prompt.',
|
||||||
com_ui_special_variables:
|
com_ui_special_variables: 'Special variables:',
|
||||||
'Special variables: Use {{current_date}} for the current date, and {{current_user}} for your given account name.',
|
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_showing: 'Showing',
|
||||||
com_ui_of: 'of',
|
com_ui_of: 'of',
|
||||||
com_ui_entries: 'Entries',
|
com_ui_entries: 'Entries',
|
||||||
|
|
@ -239,6 +243,7 @@ export default {
|
||||||
com_ui_create_prompt: 'Create Prompt',
|
com_ui_create_prompt: 'Create Prompt',
|
||||||
com_ui_share: 'Share',
|
com_ui_share: 'Share',
|
||||||
com_ui_share_var: 'Share {0}',
|
com_ui_share_var: 'Share {0}',
|
||||||
|
com_ui_enter_var: 'Enter {0}',
|
||||||
com_ui_copy_link: 'Copy link',
|
com_ui_copy_link: 'Copy link',
|
||||||
com_ui_update_link: 'Update link',
|
com_ui_update_link: 'Update link',
|
||||||
com_ui_create_link: 'Create link',
|
com_ui_create_link: 'Create link',
|
||||||
|
|
|
||||||
|
|
@ -71,3 +71,14 @@ export const defaultTextPropsLabel =
|
||||||
export function capitalizeFirstLetter(string: string) {
|
export function capitalizeFirstLetter(string: string) {
|
||||||
return string.charAt(0).toUpperCase() + string.slice(1);
|
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