| import { |
| FC, |
| KeyboardEvent, |
| useCallback, |
| useEffect, |
| useRef, |
| useState, |
| } from 'react'; |
|
|
| import { useTranslation } from 'next-i18next'; |
|
|
| import { DEFAULT_SYSTEM_PROMPT } from '@/utils/app/const'; |
|
|
| import { Conversation } from '@/types/chat'; |
| import { Prompt } from '@/types/prompt'; |
|
|
| import { PromptList } from './PromptList'; |
| import { VariableModal } from './VariableModal'; |
|
|
| interface Props { |
| conversation: Conversation; |
| prompts: Prompt[]; |
| onChangePrompt: (prompt: string) => void; |
| } |
|
|
| export const SystemPrompt: FC<Props> = ({ |
| conversation, |
| prompts, |
| onChangePrompt, |
| }) => { |
| const { t } = useTranslation('chat'); |
|
|
| const [value, setValue] = useState<string>(''); |
| const [activePromptIndex, setActivePromptIndex] = useState(0); |
| const [showPromptList, setShowPromptList] = useState(false); |
| const [promptInputValue, setPromptInputValue] = useState(''); |
| const [variables, setVariables] = useState<string[]>([]); |
| const [isModalVisible, setIsModalVisible] = useState(false); |
|
|
| const textareaRef = useRef<HTMLTextAreaElement>(null); |
| const promptListRef = useRef<HTMLUListElement | null>(null); |
|
|
| const filteredPrompts = prompts.filter((prompt) => |
| prompt.name.toLowerCase().includes(promptInputValue.toLowerCase()), |
| ); |
|
|
| const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => { |
| const value = e.target.value; |
| const maxLength = conversation.model.maxLength; |
|
|
| if (value.length > maxLength) { |
| alert( |
| t( |
| `Prompt limit is {{maxLength}} characters. You have entered {{valueLength}} characters.`, |
| { maxLength, valueLength: value.length }, |
| ), |
| ); |
| return; |
| } |
|
|
| setValue(value); |
| updatePromptListVisibility(value); |
|
|
| if (value.length > 0) { |
| onChangePrompt(value); |
| } |
| }; |
|
|
| const handleInitModal = () => { |
| const selectedPrompt = filteredPrompts[activePromptIndex]; |
| setValue((prevVal) => { |
| const newContent = prevVal?.replace(/\/\w*$/, selectedPrompt.content); |
| return newContent; |
| }); |
| handlePromptSelect(selectedPrompt); |
| setShowPromptList(false); |
| }; |
|
|
| const parseVariables = (content: string) => { |
| const regex = /{{(.*?)}}/g; |
| const foundVariables = []; |
| let match; |
|
|
| while ((match = regex.exec(content)) !== null) { |
| foundVariables.push(match[1]); |
| } |
|
|
| return foundVariables; |
| }; |
|
|
| const updatePromptListVisibility = useCallback((text: string) => { |
| const match = text.match(/\/\w*$/); |
|
|
| if (match) { |
| setShowPromptList(true); |
| setPromptInputValue(match[0].slice(1)); |
| } else { |
| setShowPromptList(false); |
| setPromptInputValue(''); |
| } |
| }, []); |
|
|
| const handlePromptSelect = (prompt: Prompt) => { |
| const parsedVariables = parseVariables(prompt.content); |
| setVariables(parsedVariables); |
|
|
| if (parsedVariables.length > 0) { |
| setIsModalVisible(true); |
| } else { |
| const updatedContent = value?.replace(/\/\w*$/, prompt.content); |
|
|
| setValue(updatedContent); |
| onChangePrompt(updatedContent); |
|
|
| updatePromptListVisibility(prompt.content); |
| } |
| }; |
|
|
| const handleSubmit = (updatedVariables: string[]) => { |
| const newContent = value?.replace(/{{(.*?)}}/g, (match, variable) => { |
| const index = variables.indexOf(variable); |
| return updatedVariables[index]; |
| }); |
|
|
| setValue(newContent); |
| onChangePrompt(newContent); |
|
|
| if (textareaRef && textareaRef.current) { |
| textareaRef.current.focus(); |
| } |
| }; |
|
|
| const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => { |
| if (showPromptList) { |
| if (e.key === 'ArrowDown') { |
| e.preventDefault(); |
| setActivePromptIndex((prevIndex) => |
| prevIndex < prompts.length - 1 ? prevIndex + 1 : prevIndex, |
| ); |
| } else if (e.key === 'ArrowUp') { |
| e.preventDefault(); |
| setActivePromptIndex((prevIndex) => |
| prevIndex > 0 ? prevIndex - 1 : prevIndex, |
| ); |
| } else if (e.key === 'Tab') { |
| e.preventDefault(); |
| setActivePromptIndex((prevIndex) => |
| prevIndex < prompts.length - 1 ? prevIndex + 1 : 0, |
| ); |
| } else if (e.key === 'Enter') { |
| e.preventDefault(); |
| handleInitModal(); |
| } else if (e.key === 'Escape') { |
| e.preventDefault(); |
| setShowPromptList(false); |
| } else { |
| setActivePromptIndex(0); |
| } |
| } |
| }; |
|
|
| useEffect(() => { |
| if (textareaRef && textareaRef.current) { |
| textareaRef.current.style.height = 'inherit'; |
| textareaRef.current.style.height = `${textareaRef.current?.scrollHeight}px`; |
| } |
| }, [value]); |
|
|
| useEffect(() => { |
| if (conversation.prompt) { |
| setValue(conversation.prompt); |
| } else { |
| setValue(DEFAULT_SYSTEM_PROMPT); |
| } |
| }, [conversation]); |
|
|
| useEffect(() => { |
| const handleOutsideClick = (e: MouseEvent) => { |
| if ( |
| promptListRef.current && |
| !promptListRef.current.contains(e.target as Node) |
| ) { |
| setShowPromptList(false); |
| } |
| }; |
|
|
| window.addEventListener('click', handleOutsideClick); |
|
|
| return () => { |
| window.removeEventListener('click', handleOutsideClick); |
| }; |
| }, []); |
|
|
| return ( |
| <div className="flex flex-col"> |
| <label className="mb-2 text-left text-neutral-700 dark:text-neutral-400"> |
| {t('System Prompt')} |
| </label> |
| <textarea |
| ref={textareaRef} |
| className="w-full rounded-lg border border-neutral-200 bg-transparent px-4 py-3 text-neutral-900 dark:border-neutral-600 dark:text-neutral-100" |
| style={{ |
| resize: 'none', |
| bottom: `${textareaRef?.current?.scrollHeight}px`, |
| maxHeight: '300px', |
| overflow: `${ |
| textareaRef.current && textareaRef.current.scrollHeight > 400 |
| ? 'auto' |
| : 'hidden' |
| }`, |
| }} |
| placeholder={ |
| t(`Enter a prompt or type "/" to select a prompt...`) || '' |
| } |
| value={t(value) || ''} |
| rows={1} |
| onChange={handleChange} |
| onKeyDown={handleKeyDown} |
| /> |
| |
| {showPromptList && filteredPrompts.length > 0 && ( |
| <div> |
| <PromptList |
| activePromptIndex={activePromptIndex} |
| prompts={filteredPrompts} |
| onSelect={handleInitModal} |
| onMouseOver={setActivePromptIndex} |
| promptListRef={promptListRef} |
| /> |
| </div> |
| )} |
| |
| {isModalVisible && ( |
| <VariableModal |
| prompt={prompts[activePromptIndex]} |
| variables={variables} |
| onSubmit={handleSubmit} |
| onClose={() => setIsModalVisible(false)} |
| /> |
| )} |
| </div> |
| ); |
| }; |
|
|