|
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; |
|
|
|
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> |
|
); |
|
}; |
|
|