Prompts (#229)
This commit is contained in:
@@ -1,35 +1,162 @@
|
||||
import { Conversation } from '@/types';
|
||||
import { Conversation } from '@/types/chat';
|
||||
import { OpenAIModelID } from '@/types/openai';
|
||||
import { Prompt } from '@/types/prompt';
|
||||
import { DEFAULT_SYSTEM_PROMPT } from '@/utils/app/const';
|
||||
import { FC, useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import {
|
||||
FC,
|
||||
KeyboardEvent,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { PromptList } from './PromptList';
|
||||
import { VariableModal } from './VariableModal';
|
||||
|
||||
interface Props {
|
||||
conversation: Conversation;
|
||||
prompts: Prompt[];
|
||||
onChangePrompt: (prompt: string) => void;
|
||||
}
|
||||
|
||||
export const SystemPrompt: FC<Props> = ({ conversation, onChangePrompt }) => {
|
||||
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 = 4000;
|
||||
const maxLength =
|
||||
conversation.model.id === OpenAIModelID.GPT_3_5 ? 12000 : 24000;
|
||||
|
||||
if (value.length > maxLength) {
|
||||
alert(t(`Prompt limit is {{maxLength}} characters`, { 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';
|
||||
@@ -45,6 +172,23 @@ export const SystemPrompt: FC<Props> = ({ conversation, onChangePrompt }) => {
|
||||
}
|
||||
}, [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">
|
||||
@@ -52,7 +196,7 @@ export const SystemPrompt: FC<Props> = ({ conversation, onChangePrompt }) => {
|
||||
</label>
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
className="w-full rounded-lg border border-neutral-200 px-4 py-3 text-neutral-900 focus:outline-none bg-transparent dark:border-neutral-600 dark:text-neutral-100"
|
||||
className="w-full rounded-lg border border-neutral-200 bg-transparent px-4 py-3 text-neutral-900 focus:outline-none dark:border-neutral-600 dark:text-neutral-100"
|
||||
style={{
|
||||
resize: 'none',
|
||||
bottom: `${textareaRef?.current?.scrollHeight}px`,
|
||||
@@ -63,11 +207,35 @@ export const SystemPrompt: FC<Props> = ({ conversation, onChangePrompt }) => {
|
||||
: 'hidden'
|
||||
}`,
|
||||
}}
|
||||
placeholder={t('Enter a prompt') || ''}
|
||||
placeholder={
|
||||
t(`Enter a prompt or type "/" to select a prompt...`) || ''
|
||||
}
|
||||
value={t(value) || ''}
|
||||
rows={1}
|
||||
onChange={handleChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
|
||||
{showPromptList && prompts.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>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user