starchat-ui / components /Chat /SystemPrompt.tsx
matthoffner's picture
Duplicate from matthoffner/chatbot
13095e0
raw
history blame
6.66 kB
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>
);
};