| import { useMemo, memo } from 'react'; |
| import remarkGfm from 'remark-gfm'; |
| import remarkMath from 'remark-math'; |
| import rehypeKatex from 'rehype-katex'; |
| import supersub from 'remark-supersub'; |
| import { useRecoilValue } from 'recoil'; |
| import { EditIcon } from 'lucide-react'; |
| import ReactMarkdown from 'react-markdown'; |
| import rehypeHighlight from 'rehype-highlight'; |
| import { SaveIcon, CrossIcon, TextareaAutosize } from '@librechat/client'; |
| import { Controller, useFormContext, useFormState } from 'react-hook-form'; |
| import type { PluggableList } from 'unified'; |
| import { codeNoExecution } from '~/components/Chat/Messages/Content/MarkdownComponents'; |
| import AlwaysMakeProd from '~/components/Prompts/Groups/AlwaysMakeProd'; |
| import VariablesDropdown from './VariablesDropdown'; |
| import { PromptVariableGfm } from './Markdown'; |
| import { PromptsEditorMode } from '~/common'; |
| import { cn, langSubset } from '~/utils'; |
| import { useLocalize } from '~/hooks'; |
| import store from '~/store'; |
|
|
| const { promptsEditorMode } = store; |
|
|
| type Props = { |
| name: string; |
| isEditing: boolean; |
| setIsEditing: React.Dispatch<React.SetStateAction<boolean>>; |
| }; |
|
|
| const PromptEditor: React.FC<Props> = ({ name, isEditing, setIsEditing }) => { |
| const localize = useLocalize(); |
| const { control } = useFormContext(); |
| const editorMode = useRecoilValue(promptsEditorMode); |
| const { dirtyFields } = useFormState({ control: control }); |
| const { prompt } = dirtyFields as { prompt?: string }; |
|
|
| const EditorIcon = useMemo(() => { |
| if (isEditing && prompt?.length == null) { |
| return CrossIcon; |
| } |
| return isEditing ? SaveIcon : EditIcon; |
| }, [isEditing, prompt]); |
|
|
| const rehypePlugins: PluggableList = [ |
| [rehypeKatex], |
| [ |
| rehypeHighlight, |
| { |
| detect: true, |
| ignoreMissing: true, |
| subset: langSubset, |
| }, |
| ], |
| ]; |
|
|
| return ( |
| <div className="flex max-h-[85vh] flex-col sm:max-h-[85vh]"> |
| <h2 className="flex items-center justify-between rounded-t-xl border border-border-light py-1.5 pl-3 text-sm font-semibold text-text-primary sm:py-2 sm:pl-4 sm:text-base"> |
| <span className="max-w-[200px] truncate sm:max-w-none"> |
| {localize('com_ui_prompt_text')} |
| </span> |
| <div className="flex flex-shrink-0 flex-row items-center gap-3 sm:gap-6"> |
| {editorMode === PromptsEditorMode.ADVANCED && ( |
| <AlwaysMakeProd className="hidden sm:flex" /> |
| )} |
| <VariablesDropdown fieldName={name} /> |
| <button |
| type="button" |
| onClick={() => setIsEditing((prev) => !prev)} |
| aria-label={isEditing ? localize('com_ui_save') : localize('com_ui_edit')} |
| className="mr-1 rounded-lg p-1.5 sm:mr-2 sm:p-1" |
| > |
| <EditorIcon |
| className={cn( |
| 'h-5 w-5 sm:h-6 sm:w-6', |
| isEditing ? 'p-[0.05rem]' : 'text-secondary-alt hover:text-text-primary', |
| )} |
| /> |
| </button> |
| </div> |
| </h2> |
| <div |
| role="button" |
| className={cn( |
| 'w-full flex-1 overflow-auto rounded-b-xl border border-border-light p-2 shadow-md transition-all duration-150 sm:p-4', |
| { |
| 'cursor-pointer bg-surface-primary hover:bg-surface-secondary active:bg-surface-tertiary': |
| !isEditing, |
| }, |
| )} |
| onClick={() => !isEditing && setIsEditing(true)} |
| onKeyDown={(e) => { |
| if (e.key === 'Enter' || e.key === ' ') { |
| !isEditing && setIsEditing(true); |
| } |
| }} |
| tabIndex={0} |
| > |
| {!isEditing && ( |
| <EditIcon className="icon-xl absolute inset-0 m-auto hidden h-6 w-6 text-text-primary opacity-25 group-hover:block sm:h-8 sm:w-8" /> |
| )} |
| <Controller |
| name={name} |
| control={control} |
| render={({ field }) => |
| isEditing ? ( |
| <TextareaAutosize |
| {...field} |
| // eslint-disable-next-line jsx-a11y/no-autofocus |
| autoFocus |
| className="w-full resize-none overflow-y-auto rounded bg-transparent text-sm text-text-primary focus:outline-none sm:text-base" |
| minRows={3} |
| maxRows={14} |
| onBlur={() => setIsEditing(false)} |
| onKeyDown={(e) => { |
| if (e.key === 'Escape') { |
| e.preventDefault(); |
| setIsEditing(false); |
| } |
| }} |
| aria-label={localize('com_ui_prompt_input')} |
| /> |
| ) : ( |
| <div |
| className={cn('overflow-y-auto text-sm sm:text-base')} |
| style={{ minHeight: '4.5em', maxHeight: '21em', overflow: 'auto' }} |
| > |
| <ReactMarkdown |
| remarkPlugins={[ |
| /** @ts-ignore */ |
| supersub, |
| remarkGfm, |
| [remarkMath, { singleDollarTextMath: false }], |
| ]} |
| /** @ts-ignore */ |
| rehypePlugins={rehypePlugins} |
| /** @ts-ignore */ |
| components={{ p: PromptVariableGfm, code: codeNoExecution }} |
| className="markdown prose dark:prose-invert light my-1 w-full break-words text-text-primary" |
| > |
| {field.value} |
| </ReactMarkdown> |
| </div> |
| ) |
| } |
| /> |
| </div> |
| </div> |
| ); |
| }; |
|
|
| export default memo(PromptEditor); |
|
|