Prompts (#229)
This commit is contained in:
parent
2269403806
commit
34c79c0d66
|
@ -1,14 +1,20 @@
|
||||||
import {
|
import { Conversation, Message } from '@/types/chat';
|
||||||
Conversation,
|
import { KeyValuePair } from '@/types/data';
|
||||||
ErrorMessage,
|
import { ErrorMessage } from '@/types/error';
|
||||||
KeyValuePair,
|
import { OpenAIModel } from '@/types/openai';
|
||||||
Message,
|
import { Prompt } from '@/types/prompt';
|
||||||
OpenAIModel,
|
|
||||||
} from '@/types';
|
|
||||||
import { throttle } from '@/utils';
|
import { throttle } from '@/utils';
|
||||||
import { IconClearAll, IconKey, IconSettings } from '@tabler/icons-react';
|
import { IconClearAll, IconKey, IconSettings } from '@tabler/icons-react';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import { FC, memo, MutableRefObject, useEffect, useRef, useState } from 'react';
|
import {
|
||||||
|
FC,
|
||||||
|
memo,
|
||||||
|
MutableRefObject,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
import { Spinner } from '../Global/Spinner';
|
import { Spinner } from '../Global/Spinner';
|
||||||
import { ChatInput } from './ChatInput';
|
import { ChatInput } from './ChatInput';
|
||||||
import { ChatLoader } from './ChatLoader';
|
import { ChatLoader } from './ChatLoader';
|
||||||
|
@ -24,8 +30,8 @@ interface Props {
|
||||||
serverSideApiKeyIsSet: boolean;
|
serverSideApiKeyIsSet: boolean;
|
||||||
messageIsStreaming: boolean;
|
messageIsStreaming: boolean;
|
||||||
modelError: ErrorMessage | null;
|
modelError: ErrorMessage | null;
|
||||||
messageError: boolean;
|
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
|
prompts: Prompt[];
|
||||||
onSend: (message: Message, deleteCount?: number) => void;
|
onSend: (message: Message, deleteCount?: number) => void;
|
||||||
onUpdateConversation: (
|
onUpdateConversation: (
|
||||||
conversation: Conversation,
|
conversation: Conversation,
|
||||||
|
@ -43,8 +49,8 @@ export const Chat: FC<Props> = memo(
|
||||||
serverSideApiKeyIsSet,
|
serverSideApiKeyIsSet,
|
||||||
messageIsStreaming,
|
messageIsStreaming,
|
||||||
modelError,
|
modelError,
|
||||||
messageError,
|
|
||||||
loading,
|
loading,
|
||||||
|
prompts,
|
||||||
onSend,
|
onSend,
|
||||||
onUpdateConversation,
|
onUpdateConversation,
|
||||||
onEditMessage,
|
onEditMessage,
|
||||||
|
@ -59,6 +65,27 @@ export const Chat: FC<Props> = memo(
|
||||||
const chatContainerRef = useRef<HTMLDivElement>(null);
|
const chatContainerRef = useRef<HTMLDivElement>(null);
|
||||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
|
|
||||||
|
const scrollToBottom = useCallback(() => {
|
||||||
|
if (autoScrollEnabled) {
|
||||||
|
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||||
|
textareaRef.current?.focus();
|
||||||
|
}
|
||||||
|
}, [autoScrollEnabled]);
|
||||||
|
|
||||||
|
const handleScroll = () => {
|
||||||
|
if (chatContainerRef.current) {
|
||||||
|
const { scrollTop, scrollHeight, clientHeight } =
|
||||||
|
chatContainerRef.current;
|
||||||
|
const bottomTolerance = 30;
|
||||||
|
|
||||||
|
if (scrollTop + clientHeight < scrollHeight - bottomTolerance) {
|
||||||
|
setAutoScrollEnabled(false);
|
||||||
|
} else {
|
||||||
|
setAutoScrollEnabled(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleSettings = () => {
|
const handleSettings = () => {
|
||||||
setShowSettings(!showSettings);
|
setShowSettings(!showSettings);
|
||||||
};
|
};
|
||||||
|
@ -174,6 +201,7 @@ export const Chat: FC<Props> = memo(
|
||||||
|
|
||||||
<SystemPrompt
|
<SystemPrompt
|
||||||
conversation={conversation}
|
conversation={conversation}
|
||||||
|
prompts={prompts}
|
||||||
onChangePrompt={(prompt) =>
|
onChangePrompt={(prompt) =>
|
||||||
onUpdateConversation(conversation, {
|
onUpdateConversation(conversation, {
|
||||||
key: 'prompt',
|
key: 'prompt',
|
||||||
|
@ -201,8 +229,8 @@ export const Chat: FC<Props> = memo(
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{showSettings && (
|
{showSettings && (
|
||||||
<div className="flex flex-col space-y-10 md:max-w-xl md:gap-6 md:py-3 md:pt-6 md:mx-auto lg:max-w-2xl lg:px-0 xl:max-w-3xl">
|
<div className="flex flex-col space-y-10 md:mx-auto md:max-w-xl md:gap-6 md:py-3 md:pt-6 lg:max-w-2xl lg:px-0 xl:max-w-3xl">
|
||||||
<div className="flex h-full flex-col space-y-4 border-b md:rounded-lg md:border border-neutral-200 p-4 dark:border-neutral-600">
|
<div className="flex h-full flex-col space-y-4 border-b border-neutral-200 p-4 dark:border-neutral-600 md:rounded-lg md:border">
|
||||||
<ModelSelect
|
<ModelSelect
|
||||||
model={conversation.model}
|
model={conversation.model}
|
||||||
models={models}
|
models={models}
|
||||||
|
@ -241,7 +269,9 @@ export const Chat: FC<Props> = memo(
|
||||||
textareaRef={textareaRef}
|
textareaRef={textareaRef}
|
||||||
messageIsStreaming={messageIsStreaming}
|
messageIsStreaming={messageIsStreaming}
|
||||||
conversationIsEmpty={conversation.messages.length === 0}
|
conversationIsEmpty={conversation.messages.length === 0}
|
||||||
|
messages={conversation.messages}
|
||||||
model={conversation.model}
|
model={conversation.model}
|
||||||
|
prompts={prompts}
|
||||||
onSend={(message) => {
|
onSend={(message) => {
|
||||||
setCurrentMessage(message);
|
setCurrentMessage(message);
|
||||||
onSend(message);
|
onSend(message);
|
||||||
|
|
|
@ -1,18 +1,26 @@
|
||||||
import { Message, OpenAIModel, OpenAIModelID } from '@/types';
|
import { Message } from '@/types/chat';
|
||||||
|
import { OpenAIModel, OpenAIModelID } from '@/types/openai';
|
||||||
|
import { Prompt } from '@/types/prompt';
|
||||||
import { IconPlayerStop, IconRepeat, IconSend } from '@tabler/icons-react';
|
import { IconPlayerStop, IconRepeat, IconSend } from '@tabler/icons-react';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import {
|
import {
|
||||||
FC,
|
FC,
|
||||||
KeyboardEvent,
|
KeyboardEvent,
|
||||||
MutableRefObject,
|
MutableRefObject,
|
||||||
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
import { PromptList } from './PromptList';
|
||||||
|
import { VariableModal } from './VariableModal';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
messageIsStreaming: boolean;
|
messageIsStreaming: boolean;
|
||||||
model: OpenAIModel;
|
model: OpenAIModel;
|
||||||
conversationIsEmpty: boolean;
|
conversationIsEmpty: boolean;
|
||||||
|
messages: Message[];
|
||||||
|
prompts: Prompt[];
|
||||||
onSend: (message: Message) => void;
|
onSend: (message: Message) => void;
|
||||||
onRegenerate: () => void;
|
onRegenerate: () => void;
|
||||||
stopConversationRef: MutableRefObject<boolean>;
|
stopConversationRef: MutableRefObject<boolean>;
|
||||||
|
@ -23,14 +31,28 @@ export const ChatInput: FC<Props> = ({
|
||||||
messageIsStreaming,
|
messageIsStreaming,
|
||||||
model,
|
model,
|
||||||
conversationIsEmpty,
|
conversationIsEmpty,
|
||||||
|
messages,
|
||||||
|
prompts,
|
||||||
onSend,
|
onSend,
|
||||||
onRegenerate,
|
onRegenerate,
|
||||||
stopConversationRef,
|
stopConversationRef,
|
||||||
textareaRef,
|
textareaRef,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation('chat');
|
const { t } = useTranslation('chat');
|
||||||
|
|
||||||
const [content, setContent] = useState<string>();
|
const [content, setContent] = useState<string>();
|
||||||
const [isTyping, setIsTyping] = useState<boolean>(false);
|
const [isTyping, setIsTyping] = useState<boolean>(false);
|
||||||
|
const [showPromptList, setShowPromptList] = useState(false);
|
||||||
|
const [activePromptIndex, setActivePromptIndex] = useState(0);
|
||||||
|
const [promptInputValue, setPromptInputValue] = useState('');
|
||||||
|
const [variables, setVariables] = useState<string[]>([]);
|
||||||
|
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||||
|
|
||||||
|
const promptListRef = useRef<HTMLUListElement | null>(null);
|
||||||
|
|
||||||
|
const filteredPrompts = prompts.filter((prompt) =>
|
||||||
|
prompt.name.toLowerCase().includes(promptInputValue.toLowerCase()),
|
||||||
|
);
|
||||||
|
|
||||||
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
const value = e.target.value;
|
const value = e.target.value;
|
||||||
|
@ -47,6 +69,7 @@ export const ChatInput: FC<Props> = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
setContent(value);
|
setContent(value);
|
||||||
|
updatePromptListVisibility(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSend = () => {
|
const handleSend = () => {
|
||||||
|
@ -67,6 +90,13 @@ export const ChatInput: FC<Props> = ({
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleStopConversation = () => {
|
||||||
|
stopConversationRef.current = true;
|
||||||
|
setTimeout(() => {
|
||||||
|
stopConversationRef.current = false;
|
||||||
|
}, 1000);
|
||||||
|
};
|
||||||
|
|
||||||
const isMobile = () => {
|
const isMobile = () => {
|
||||||
const userAgent =
|
const userAgent =
|
||||||
typeof window.navigator === 'undefined' ? '' : navigator.userAgent;
|
typeof window.navigator === 'undefined' ? '' : navigator.userAgent;
|
||||||
|
@ -75,15 +105,106 @@ export const ChatInput: FC<Props> = ({
|
||||||
return mobileRegex.test(userAgent);
|
return mobileRegex.test(userAgent);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleInitModal = () => {
|
||||||
|
const selectedPrompt = filteredPrompts[activePromptIndex];
|
||||||
|
setContent((prevContent) => {
|
||||||
|
const newContent = prevContent?.replace(/\/\w*$/, selectedPrompt.content);
|
||||||
|
return newContent;
|
||||||
|
});
|
||||||
|
handlePromptSelect(selectedPrompt);
|
||||||
|
setShowPromptList(false);
|
||||||
|
};
|
||||||
|
|
||||||
const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
|
const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
|
||||||
if (!isTyping) {
|
if (showPromptList) {
|
||||||
if (e.key === 'Enter' && !e.shiftKey && !isMobile()) {
|
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);
|
||||||
|
}
|
||||||
|
} else if (e.key === 'Enter' && !isMobile() && !e.shiftKey) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
handleSend();
|
handleSend();
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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 {
|
||||||
|
setContent((prevContent) => {
|
||||||
|
const updatedContent = prevContent?.replace(/\/\w*$/, prompt.content);
|
||||||
|
return updatedContent;
|
||||||
|
});
|
||||||
|
updatePromptListVisibility(prompt.content);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSubmit = (updatedVariables: string[]) => {
|
||||||
|
const newContent = content?.replace(/{{(.*?)}}/g, (match, variable) => {
|
||||||
|
const index = variables.indexOf(variable);
|
||||||
|
return updatedVariables[index];
|
||||||
|
});
|
||||||
|
|
||||||
|
setContent(newContent);
|
||||||
|
|
||||||
|
if (textareaRef && textareaRef.current) {
|
||||||
|
textareaRef.current.focus();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (promptListRef.current) {
|
||||||
|
promptListRef.current.scrollTop = activePromptIndex * 30;
|
||||||
|
}
|
||||||
|
}, [activePromptIndex]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (textareaRef && textareaRef.current) {
|
if (textareaRef && textareaRef.current) {
|
||||||
textareaRef.current.style.height = 'inherit';
|
textareaRef.current.style.height = 'inherit';
|
||||||
|
@ -94,19 +215,29 @@ export const ChatInput: FC<Props> = ({
|
||||||
}
|
}
|
||||||
}, [content]);
|
}, [content]);
|
||||||
|
|
||||||
function handleStopConversation() {
|
useEffect(() => {
|
||||||
stopConversationRef.current = true;
|
const handleOutsideClick = (e: MouseEvent) => {
|
||||||
setTimeout(() => {
|
if (
|
||||||
stopConversationRef.current = false;
|
promptListRef.current &&
|
||||||
}, 1000);
|
!promptListRef.current.contains(e.target as Node)
|
||||||
|
) {
|
||||||
|
setShowPromptList(false);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('click', handleOutsideClick);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('click', handleOutsideClick);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="absolute bottom-0 left-0 w-full border-transparent bg-gradient-to-b from-transparent via-white to-white pt-6 dark:border-white/20 dark:via-[#343541] dark:to-[#343541] md:pt-2">
|
<div className="absolute bottom-0 left-0 w-full border-transparent bg-gradient-to-b from-transparent via-white to-white pt-6 dark:border-white/20 dark:via-[#343541] dark:to-[#343541] md:pt-2">
|
||||||
<div className="stretch mx-2 mt-4 flex flex-row gap-3 last:mb-2 md:mx-4 md:mt-[52px] md:last:mb-6 lg:mx-auto lg:max-w-3xl">
|
<div className="stretch mx-2 mt-4 flex flex-row gap-3 last:mb-2 md:mx-4 md:mt-[52px] md:last:mb-6 lg:mx-auto lg:max-w-3xl">
|
||||||
{messageIsStreaming && (
|
{messageIsStreaming && (
|
||||||
<button
|
<button
|
||||||
className="absolute -top-2 left-0 right-0 mx-auto w-fit rounded border border-neutral-200 bg-white py-2 px-4 text-black dark:border-neutral-600 dark:bg-[#343541] dark:text-white md:top-0"
|
className="absolute top-2 left-0 right-0 mx-auto mt-2 w-fit rounded border border-neutral-200 bg-white py-2 px-4 text-black hover:opacity-50 dark:border-neutral-600 dark:bg-[#343541] dark:text-white md:top-0"
|
||||||
onClick={handleStopConversation}
|
onClick={handleStopConversation}
|
||||||
>
|
>
|
||||||
<IconPlayerStop size={16} className="mb-[2px] inline-block" />{' '}
|
<IconPlayerStop size={16} className="mb-[2px] inline-block" />{' '}
|
||||||
|
@ -116,7 +247,7 @@ export const ChatInput: FC<Props> = ({
|
||||||
|
|
||||||
{!messageIsStreaming && !conversationIsEmpty && (
|
{!messageIsStreaming && !conversationIsEmpty && (
|
||||||
<button
|
<button
|
||||||
className="absolute -top-2 left-0 right-0 mx-auto w-fit rounded border border-neutral-200 bg-white py-2 px-4 text-black dark:border-neutral-600 dark:bg-[#343541] dark:text-white md:top-0"
|
className="absolute left-0 right-0 mx-auto mt-2 w-fit rounded border border-neutral-200 bg-white py-2 px-4 text-black hover:opacity-50 dark:border-neutral-600 dark:bg-[#343541] dark:text-white md:top-0"
|
||||||
onClick={onRegenerate}
|
onClick={onRegenerate}
|
||||||
>
|
>
|
||||||
<IconRepeat size={16} className="mb-[2px] inline-block" />{' '}
|
<IconRepeat size={16} className="mb-[2px] inline-block" />{' '}
|
||||||
|
@ -124,10 +255,10 @@ export const ChatInput: FC<Props> = ({
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="relative flex w-full flex-grow flex-col rounded-md border border-black/10 bg-white py-2 shadow-[0_0_10px_rgba(0,0,0,0.10)] dark:border-gray-900/50 dark:bg-[#40414F] dark:text-white dark:shadow-[0_0_15px_rgba(0,0,0,0.10)] md:py-3 md:pl-4">
|
<div className="relative mx-2 flex w-full flex-grow flex-col rounded-md border border-black/10 bg-white py-2 shadow-[0_0_10px_rgba(0,0,0,0.10)] dark:border-gray-900/50 dark:bg-[#40414F] dark:text-white dark:shadow-[0_0_15px_rgba(0,0,0,0.10)] sm:mx-4 md:py-3 md:pl-4">
|
||||||
<textarea
|
<textarea
|
||||||
ref={textareaRef}
|
ref={textareaRef}
|
||||||
className="m-0 w-full resize-none border-0 bg-transparent p-0 pr-7 pl-2 text-black outline-none focus:ring-0 focus-visible:ring-0 dark:bg-transparent dark:text-white md:pl-0"
|
className="m-0 w-full resize-none border-0 bg-transparent p-0 pr-8 pl-2 text-black outline-none focus:ring-0 focus-visible:ring-0 dark:bg-transparent dark:text-white md:pl-0"
|
||||||
style={{
|
style={{
|
||||||
resize: 'none',
|
resize: 'none',
|
||||||
bottom: `${textareaRef?.current?.scrollHeight}px`,
|
bottom: `${textareaRef?.current?.scrollHeight}px`,
|
||||||
|
@ -138,7 +269,9 @@ export const ChatInput: FC<Props> = ({
|
||||||
: 'hidden'
|
: 'hidden'
|
||||||
}`,
|
}`,
|
||||||
}}
|
}}
|
||||||
placeholder={t('Type a message...') || ''}
|
placeholder={
|
||||||
|
t('Type a message or type "/" to select a prompt...') || ''
|
||||||
|
}
|
||||||
value={content}
|
value={content}
|
||||||
rows={1}
|
rows={1}
|
||||||
onCompositionStart={() => setIsTyping(true)}
|
onCompositionStart={() => setIsTyping(true)}
|
||||||
|
@ -153,9 +286,30 @@ export const ChatInput: FC<Props> = ({
|
||||||
>
|
>
|
||||||
<IconSend size={16} className="opacity-60" />
|
<IconSend size={16} className="opacity-60" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{showPromptList && prompts.length > 0 && (
|
||||||
|
<div className="absolute bottom-12 w-full">
|
||||||
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="px-3 pt-2 pb-3 text-center text-xs text-black/50 dark:text-white/50 md:px-4 md:pt-3 md:pb-6">
|
<div className="px-3 pt-2 pb-3 text-center text-[12px] text-black/50 dark:text-white/50 md:px-4 md:pt-3 md:pb-6">
|
||||||
<a
|
<a
|
||||||
href="https://github.com/mckaywrigley/chatbot-ui"
|
href="https://github.com/mckaywrigley/chatbot-ui"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { Message } from '@/types';
|
import { Message } from '@/types/chat';
|
||||||
import { IconEdit } from '@tabler/icons-react';
|
import { IconEdit } from '@tabler/icons-react';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import { FC, useEffect, useRef, useState, memo } from 'react';
|
import { FC, memo, useEffect, useRef, useState } from 'react';
|
||||||
import rehypeMathjax from 'rehype-mathjax';
|
import rehypeMathjax from 'rehype-mathjax';
|
||||||
import remarkGfm from 'remark-gfm';
|
import remarkGfm from 'remark-gfm';
|
||||||
import remarkMath from 'remark-math';
|
import remarkMath from 'remark-math';
|
||||||
|
@ -73,7 +73,7 @@ export const ChatMessage: FC<Props> = memo(
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`group ${
|
className={`group px-4 ${
|
||||||
message.role === 'assistant'
|
message.role === 'assistant'
|
||||||
? 'border-b border-black/10 bg-gray-50 text-gray-800 dark:border-gray-900/50 dark:bg-[#444654] dark:text-gray-100'
|
? 'border-b border-black/10 bg-gray-50 text-gray-800 dark:border-gray-900/50 dark:bg-[#444654] dark:text-gray-100'
|
||||||
: 'border-b border-black/10 bg-white text-gray-800 dark:border-gray-900/50 dark:bg-[#343541] dark:text-gray-100'
|
: 'border-b border-black/10 bg-white text-gray-800 dark:border-gray-900/50 dark:bg-[#343541] dark:text-gray-100'
|
||||||
|
@ -138,7 +138,7 @@ export const ChatMessage: FC<Props> = memo(
|
||||||
className={`absolute ${
|
className={`absolute ${
|
||||||
window.innerWidth < 640
|
window.innerWidth < 640
|
||||||
? 'right-3 bottom-1'
|
? 'right-3 bottom-1'
|
||||||
: 'right-[-20px] top-[26px]'
|
: 'right-0 top-[26px]'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<IconEdit
|
<IconEdit
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { IconCheck, IconCopy } from "@tabler/icons-react";
|
import { IconCheck, IconCopy } from '@tabler/icons-react';
|
||||||
import { FC } from "react";
|
import { FC } from 'react';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
messagedCopied: boolean;
|
messagedCopied: boolean;
|
||||||
|
@ -7,16 +7,17 @@ type Props = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CopyButton: FC<Props> = ({ messagedCopied, copyOnClick }) => (
|
export const CopyButton: FC<Props> = ({ messagedCopied, copyOnClick }) => (
|
||||||
<button className={`absolute ${window.innerWidth < 640 ? "right-3 bottom-1" : "right-[-20px] top-[26px] m-0"}`}>
|
<button
|
||||||
|
className={`absolute ${
|
||||||
|
window.innerWidth < 640 ? 'right-3 bottom-1' : 'right-0 top-[26px] m-0'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
{messagedCopied ? (
|
{messagedCopied ? (
|
||||||
<IconCheck
|
<IconCheck size={20} className="text-green-500 dark:text-green-400" />
|
||||||
size={20}
|
|
||||||
className="text-green-500 dark:text-green-400"
|
|
||||||
/>
|
|
||||||
) : (
|
) : (
|
||||||
<IconCopy
|
<IconCopy
|
||||||
size={20}
|
size={20}
|
||||||
className="text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300"
|
className="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300"
|
||||||
onClick={copyOnClick}
|
onClick={copyOnClick}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { ErrorMessage } from '@/types';
|
import { ErrorMessage } from '@/types/error';
|
||||||
import { IconCircleX } from '@tabler/icons-react';
|
import { IconCircleX } from '@tabler/icons-react';
|
||||||
import { FC } from 'react';
|
import { FC } from 'react';
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@ export const ErrorMessageDiv: FC<Props> = ({ error }) => {
|
||||||
{line}{' '}
|
{line}{' '}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<div className="text-xs dark:text-red-400 opacity-50 mt-4">
|
<div className="mt-4 text-xs opacity-50 dark:text-red-400">
|
||||||
{error.code ? <i>Code: {error.code}</i> : ''}
|
{error.code ? <i>Code: {error.code}</i> : ''}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { OpenAIModel } from '@/types';
|
import { OpenAIModel } from '@/types/openai';
|
||||||
import { FC } from 'react';
|
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
|
import { FC } from 'react';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
model: OpenAIModel;
|
model: OpenAIModel;
|
||||||
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
import { Prompt } from '@/types/prompt';
|
||||||
|
import { FC, MutableRefObject } from 'react';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
prompts: Prompt[];
|
||||||
|
activePromptIndex: number;
|
||||||
|
onSelect: () => void;
|
||||||
|
onMouseOver: (index: number) => void;
|
||||||
|
promptListRef: MutableRefObject<HTMLUListElement | null>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PromptList: FC<Props> = ({
|
||||||
|
prompts,
|
||||||
|
activePromptIndex,
|
||||||
|
onSelect,
|
||||||
|
onMouseOver,
|
||||||
|
promptListRef,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<ul
|
||||||
|
ref={promptListRef}
|
||||||
|
className="z-10 w-full rounded border border-black/10 bg-white shadow-[0_0_10px_rgba(0,0,0,0.10)] dark:border-neutral-500 dark:bg-[#343541] dark:text-white dark:shadow-[0_0_15px_rgba(0,0,0,0.10)]"
|
||||||
|
style={{
|
||||||
|
width: 'calc(100% - 48px)',
|
||||||
|
bottom: '100%',
|
||||||
|
marginBottom: '4px',
|
||||||
|
maxHeight: '200px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{prompts.map((prompt, index) => (
|
||||||
|
<li
|
||||||
|
key={prompt.id}
|
||||||
|
className={`${
|
||||||
|
index === activePromptIndex
|
||||||
|
? 'bg-gray-200 dark:bg-[#202123] dark:text-black'
|
||||||
|
: ''
|
||||||
|
} cursor-pointer px-3 py-2 text-sm text-black dark:text-white`}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
onSelect();
|
||||||
|
}}
|
||||||
|
onMouseEnter={() => onMouseOver(index)}
|
||||||
|
>
|
||||||
|
{prompt.name}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,6 +1,6 @@
|
||||||
import { IconRefresh } from '@tabler/icons-react';
|
import { IconRefresh } from '@tabler/icons-react';
|
||||||
import { FC } from 'react';
|
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
|
import { FC } from 'react';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onRegenerate: () => void;
|
onRegenerate: () => void;
|
||||||
|
|
|
@ -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 { DEFAULT_SYSTEM_PROMPT } from '@/utils/app/const';
|
||||||
import { FC, useEffect, useRef, useState } from 'react';
|
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
|
import {
|
||||||
|
FC,
|
||||||
|
KeyboardEvent,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
import { PromptList } from './PromptList';
|
||||||
|
import { VariableModal } from './VariableModal';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
conversation: Conversation;
|
conversation: Conversation;
|
||||||
|
prompts: Prompt[];
|
||||||
onChangePrompt: (prompt: string) => void;
|
onChangePrompt: (prompt: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SystemPrompt: FC<Props> = ({ conversation, onChangePrompt }) => {
|
export const SystemPrompt: FC<Props> = ({
|
||||||
|
conversation,
|
||||||
|
prompts,
|
||||||
|
onChangePrompt,
|
||||||
|
}) => {
|
||||||
const { t } = useTranslation('chat');
|
const { t } = useTranslation('chat');
|
||||||
|
|
||||||
const [value, setValue] = useState<string>('');
|
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 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 handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
const value = e.target.value;
|
const value = e.target.value;
|
||||||
const maxLength = 4000;
|
const maxLength =
|
||||||
|
conversation.model.id === OpenAIModelID.GPT_3_5 ? 12000 : 24000;
|
||||||
|
|
||||||
if (value.length > maxLength) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setValue(value);
|
setValue(value);
|
||||||
|
updatePromptListVisibility(value);
|
||||||
|
|
||||||
if (value.length > 0) {
|
if (value.length > 0) {
|
||||||
onChangePrompt(value);
|
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(() => {
|
useEffect(() => {
|
||||||
if (textareaRef && textareaRef.current) {
|
if (textareaRef && textareaRef.current) {
|
||||||
textareaRef.current.style.height = 'inherit';
|
textareaRef.current.style.height = 'inherit';
|
||||||
|
@ -45,6 +172,23 @@ export const SystemPrompt: FC<Props> = ({ conversation, onChangePrompt }) => {
|
||||||
}
|
}
|
||||||
}, [conversation]);
|
}, [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 (
|
return (
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<label className="mb-2 text-left text-neutral-700 dark:text-neutral-400">
|
<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>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
ref={textareaRef}
|
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={{
|
style={{
|
||||||
resize: 'none',
|
resize: 'none',
|
||||||
bottom: `${textareaRef?.current?.scrollHeight}px`,
|
bottom: `${textareaRef?.current?.scrollHeight}px`,
|
||||||
|
@ -63,11 +207,35 @@ export const SystemPrompt: FC<Props> = ({ conversation, onChangePrompt }) => {
|
||||||
: 'hidden'
|
: 'hidden'
|
||||||
}`,
|
}`,
|
||||||
}}
|
}}
|
||||||
placeholder={t('Enter a prompt') || ''}
|
placeholder={
|
||||||
|
t(`Enter a prompt or type "/" to select a prompt...`) || ''
|
||||||
|
}
|
||||||
value={t(value) || ''}
|
value={t(value) || ''}
|
||||||
rows={1}
|
rows={1}
|
||||||
onChange={handleChange}
|
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>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,123 @@
|
||||||
|
import { Prompt } from '@/types/prompt';
|
||||||
|
import { FC, KeyboardEvent, useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
prompt: Prompt;
|
||||||
|
variables: string[];
|
||||||
|
onSubmit: (updatedVariables: string[]) => void;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const VariableModal: FC<Props> = ({
|
||||||
|
prompt,
|
||||||
|
variables,
|
||||||
|
onSubmit,
|
||||||
|
onClose,
|
||||||
|
}) => {
|
||||||
|
const [updatedVariables, setUpdatedVariables] = useState<
|
||||||
|
{ key: string; value: string }[]
|
||||||
|
>(
|
||||||
|
variables
|
||||||
|
.map((variable) => ({ key: variable, value: '' }))
|
||||||
|
.filter(
|
||||||
|
(item, index, array) =>
|
||||||
|
array.findIndex((t) => t.key === item.key) === index,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const modalRef = useRef<HTMLDivElement>(null);
|
||||||
|
const nameInputRef = useRef<HTMLTextAreaElement>(null);
|
||||||
|
|
||||||
|
const handleChange = (index: number, value: string) => {
|
||||||
|
setUpdatedVariables((prev) => {
|
||||||
|
const updated = [...prev];
|
||||||
|
updated[index].value = value;
|
||||||
|
return updated;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
if (updatedVariables.some((variable) => variable.value === '')) {
|
||||||
|
alert('Please fill out all variables');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
onSubmit(updatedVariables.map((variable) => variable.value));
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {
|
||||||
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
handleSubmit();
|
||||||
|
} else if (e.key === 'Escape') {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleOutsideClick = (e: MouseEvent) => {
|
||||||
|
if (modalRef.current && !modalRef.current.contains(e.target as Node)) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('click', handleOutsideClick);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('click', handleOutsideClick);
|
||||||
|
};
|
||||||
|
}, [onClose]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (nameInputRef.current) {
|
||||||
|
nameInputRef.current.focus();
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50"
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
ref={modalRef}
|
||||||
|
className="dark:border-netural-400 inline-block max-h-[400px] transform overflow-hidden overflow-y-auto rounded-lg border border-gray-300 bg-white px-4 pt-5 pb-4 text-left align-bottom shadow-xl transition-all dark:bg-[#202123] sm:my-8 sm:max-h-[600px] sm:w-full sm:max-w-lg sm:p-6 sm:align-middle"
|
||||||
|
role="dialog"
|
||||||
|
>
|
||||||
|
<div className="mb-4 text-xl font-bold text-black dark:text-neutral-200">
|
||||||
|
{prompt.name}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-4 text-sm italic text-black dark:text-neutral-200">
|
||||||
|
{prompt.description}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{updatedVariables.map((variable, index) => (
|
||||||
|
<div className="mb-4" key={index}>
|
||||||
|
<div className="mb-2 text-sm font-bold text-neutral-200">
|
||||||
|
{variable.key}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<textarea
|
||||||
|
ref={index === 0 ? nameInputRef : undefined}
|
||||||
|
className="mt-1 w-full rounded-lg border border-neutral-500 px-4 py-2 text-neutral-900 shadow focus:outline-none dark:border-neutral-800 dark:border-opacity-50 dark:bg-[#40414F] dark:text-neutral-100"
|
||||||
|
style={{ resize: 'none' }}
|
||||||
|
placeholder={`Enter a value for ${variable.key}...`}
|
||||||
|
value={variable.value}
|
||||||
|
onChange={(e) => handleChange(index, e.target.value)}
|
||||||
|
rows={3}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<button
|
||||||
|
className="mt-6 w-full rounded-lg border border-neutral-500 px-4 py-2 text-neutral-900 shadow hover:bg-neutral-100 focus:outline-none dark:border-neutral-800 dark:border-opacity-50 dark:bg-white dark:text-black dark:hover:bg-neutral-300"
|
||||||
|
onClick={handleSubmit}
|
||||||
|
>
|
||||||
|
Submit
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,16 +1,18 @@
|
||||||
import { ChatFolder, Conversation, KeyValuePair } from '@/types';
|
import { Conversation } from '@/types/chat';
|
||||||
|
import { KeyValuePair } from '@/types/data';
|
||||||
|
import { Folder } from '@/types/folder';
|
||||||
import {
|
import {
|
||||||
IconArrowBarLeft,
|
IconArrowBarLeft,
|
||||||
IconFolderPlus,
|
IconFolderPlus,
|
||||||
IconMessagesOff,
|
IconMessagesOff,
|
||||||
IconPlus,
|
IconPlus,
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
import { FC, useEffect, useState } from 'react';
|
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
|
import { FC, useEffect, useState } from 'react';
|
||||||
|
import { ChatFolders } from '../Folders/Chat/ChatFolders';
|
||||||
|
import { Search } from '../Sidebar/Search';
|
||||||
|
import { ChatbarSettings } from './ChatbarSettings';
|
||||||
import { Conversations } from './Conversations';
|
import { Conversations } from './Conversations';
|
||||||
import { Folders } from './Folders';
|
|
||||||
import { Search } from './Search';
|
|
||||||
import { SidebarSettings } from './SidebarSettings';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
|
@ -18,10 +20,10 @@ interface Props {
|
||||||
lightMode: 'light' | 'dark';
|
lightMode: 'light' | 'dark';
|
||||||
selectedConversation: Conversation;
|
selectedConversation: Conversation;
|
||||||
apiKey: string;
|
apiKey: string;
|
||||||
folders: ChatFolder[];
|
folders: Folder[];
|
||||||
onCreateFolder: (name: string) => void;
|
onCreateFolder: (name: string) => void;
|
||||||
onDeleteFolder: (folderId: number) => void;
|
onDeleteFolder: (folderId: string) => void;
|
||||||
onUpdateFolder: (folderId: number, name: string) => void;
|
onUpdateFolder: (folderId: string, name: string) => void;
|
||||||
onNewConversation: () => void;
|
onNewConversation: () => void;
|
||||||
onToggleLightMode: (mode: 'light' | 'dark') => void;
|
onToggleLightMode: (mode: 'light' | 'dark') => void;
|
||||||
onSelectConversation: (conversation: Conversation) => void;
|
onSelectConversation: (conversation: Conversation) => void;
|
||||||
|
@ -36,11 +38,11 @@ interface Props {
|
||||||
onExportConversations: () => void;
|
onExportConversations: () => void;
|
||||||
onImportConversations: (data: {
|
onImportConversations: (data: {
|
||||||
conversations: Conversation[];
|
conversations: Conversation[];
|
||||||
folders: ChatFolder[];
|
folders: Folder[];
|
||||||
}) => void;
|
}) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Sidebar: FC<Props> = ({
|
export const Chatbar: FC<Props> = ({
|
||||||
loading,
|
loading,
|
||||||
conversations,
|
conversations,
|
||||||
lightMode,
|
lightMode,
|
||||||
|
@ -117,12 +119,12 @@ export const Sidebar: FC<Props> = ({
|
||||||
}, [searchTerm, conversations]);
|
}, [searchTerm, conversations]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside
|
<div
|
||||||
className={`fixed top-0 bottom-0 z-50 flex h-full w-[260px] flex-none flex-col space-y-2 bg-[#202123] p-2 transition-all sm:relative sm:top-0`}
|
className={`fixed top-0 bottom-0 z-50 flex h-full w-[260px] flex-none flex-col space-y-2 bg-[#202123] p-2 transition-all sm:relative sm:top-0`}
|
||||||
>
|
>
|
||||||
<header className="flex items-center">
|
<div className="flex items-center">
|
||||||
<button
|
<button
|
||||||
className="flex w-[190px] flex-shrink-0 cursor-pointer items-center gap-3 rounded-md border border-white/20 p-3 text-[12.5px] leading-3 text-white transition-colors duration-200 select-none hover:bg-gray-500/10"
|
className="flex w-[190px] flex-shrink-0 cursor-pointer select-none items-center gap-3 rounded-md border border-white/20 p-3 text-[14px] leading-normal text-white transition-colors duration-200 hover:bg-gray-500/10"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onNewConversation();
|
onNewConversation();
|
||||||
setSearchTerm('');
|
setSearchTerm('');
|
||||||
|
@ -133,7 +135,7 @@ export const Sidebar: FC<Props> = ({
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
className="ml-2 flex flex-shrink-0 cursor-pointer items-center gap-3 rounded-md border border-white/20 p-3 text-[12.5px] leading-3 text-white transition-colors duration-200 hover:bg-gray-500/10"
|
className="ml-2 flex flex-shrink-0 cursor-pointer items-center gap-3 rounded-md border border-white/20 p-3 text-[14px] leading-normal text-white transition-colors duration-200 hover:bg-gray-500/10"
|
||||||
onClick={() => onCreateFolder(t('New folder'))}
|
onClick={() => onCreateFolder(t('New folder'))}
|
||||||
>
|
>
|
||||||
<IconFolderPlus size={18} />
|
<IconFolderPlus size={18} />
|
||||||
|
@ -144,21 +146,25 @@ export const Sidebar: FC<Props> = ({
|
||||||
size={32}
|
size={32}
|
||||||
onClick={onToggleSidebar}
|
onClick={onToggleSidebar}
|
||||||
/>
|
/>
|
||||||
</header>
|
</div>
|
||||||
|
|
||||||
{conversations.length > 1 && (
|
{conversations.length > 1 && (
|
||||||
<Search searchTerm={searchTerm} onSearch={setSearchTerm} />
|
<Search
|
||||||
|
placeholder="Search conversations..."
|
||||||
|
searchTerm={searchTerm}
|
||||||
|
onSearch={setSearchTerm}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex-grow overflow-y-auto overflow-x-clip">
|
<div className="flex-grow overflow-auto">
|
||||||
{folders.length > 0 && (
|
{folders.length > 0 && (
|
||||||
<div className="flex border-b border-white/20 pb-2">
|
<div className="flex border-b border-white/20 pb-2">
|
||||||
<Folders
|
<ChatFolders
|
||||||
searchTerm={searchTerm}
|
searchTerm={searchTerm}
|
||||||
conversations={filteredConversations.filter(
|
conversations={filteredConversations.filter(
|
||||||
(conversation) => conversation.folderId !== 0,
|
(conversation) => conversation.folderId,
|
||||||
)}
|
)}
|
||||||
folders={folders}
|
folders={folders.filter((folder) => folder.type === 'chat')}
|
||||||
onDeleteFolder={onDeleteFolder}
|
onDeleteFolder={onDeleteFolder}
|
||||||
onUpdateFolder={onUpdateFolder}
|
onUpdateFolder={onUpdateFolder}
|
||||||
selectedConversation={selectedConversation}
|
selectedConversation={selectedConversation}
|
||||||
|
@ -181,9 +187,7 @@ export const Sidebar: FC<Props> = ({
|
||||||
<Conversations
|
<Conversations
|
||||||
loading={loading}
|
loading={loading}
|
||||||
conversations={filteredConversations.filter(
|
conversations={filteredConversations.filter(
|
||||||
(conversation) =>
|
(conversation) => !conversation.folderId,
|
||||||
conversation.folderId === 0 ||
|
|
||||||
!folders[conversation.folderId - 1],
|
|
||||||
)}
|
)}
|
||||||
selectedConversation={selectedConversation}
|
selectedConversation={selectedConversation}
|
||||||
onSelectConversation={onSelectConversation}
|
onSelectConversation={onSelectConversation}
|
||||||
|
@ -192,14 +196,16 @@ export const Sidebar: FC<Props> = ({
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="mt-8 text-white text-center opacity-50 select-none">
|
<div className="mt-8 select-none text-center text-white opacity-50">
|
||||||
<IconMessagesOff className='mx-auto mb-3'/>
|
<IconMessagesOff className="mx-auto mb-3" />
|
||||||
<span className='text-[12.5px] leading-3'>{t('No conversations.')}</span>
|
<span className="text-[14px] leading-normal">
|
||||||
|
{t('No conversations.')}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SidebarSettings
|
<ChatbarSettings
|
||||||
lightMode={lightMode}
|
lightMode={lightMode}
|
||||||
apiKey={apiKey}
|
apiKey={apiKey}
|
||||||
conversationsCount={conversations.length}
|
conversationsCount={conversations.length}
|
||||||
|
@ -209,6 +215,6 @@ export const Sidebar: FC<Props> = ({
|
||||||
onExportConversations={onExportConversations}
|
onExportConversations={onExportConversations}
|
||||||
onImportConversations={onImportConversations}
|
onImportConversations={onImportConversations}
|
||||||
/>
|
/>
|
||||||
</aside>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
|
@ -1,11 +1,12 @@
|
||||||
import { ChatFolder, Conversation } from '@/types';
|
import { Conversation } from '@/types/chat';
|
||||||
|
import { Folder } from '@/types/folder';
|
||||||
import { IconFileExport, IconMoon, IconSun } from '@tabler/icons-react';
|
import { IconFileExport, IconMoon, IconSun } from '@tabler/icons-react';
|
||||||
import { FC } from 'react';
|
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
|
import { FC } from 'react';
|
||||||
|
import { Import } from '../Settings/Import';
|
||||||
|
import { Key } from '../Settings/Key';
|
||||||
|
import { SidebarButton } from '../Sidebar/SidebarButton';
|
||||||
import { ClearConversations } from './ClearConversations';
|
import { ClearConversations } from './ClearConversations';
|
||||||
import { Import } from './Import';
|
|
||||||
import { Key } from './Key';
|
|
||||||
import { SidebarButton } from './SidebarButton';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
lightMode: 'light' | 'dark';
|
lightMode: 'light' | 'dark';
|
||||||
|
@ -17,11 +18,11 @@ interface Props {
|
||||||
onExportConversations: () => void;
|
onExportConversations: () => void;
|
||||||
onImportConversations: (data: {
|
onImportConversations: (data: {
|
||||||
conversations: Conversation[];
|
conversations: Conversation[];
|
||||||
folders: ChatFolder[];
|
folders: Folder[];
|
||||||
}) => void;
|
}) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SidebarSettings: FC<Props> = ({
|
export const ChatbarSettings: FC<Props> = ({
|
||||||
lightMode,
|
lightMode,
|
||||||
apiKey,
|
apiKey,
|
||||||
conversationsCount,
|
conversationsCount,
|
|
@ -1,7 +1,7 @@
|
||||||
import { IconCheck, IconTrash, IconX } from '@tabler/icons-react';
|
import { IconCheck, IconTrash, IconX } from '@tabler/icons-react';
|
||||||
import { FC, useState } from 'react';
|
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import { SidebarButton } from './SidebarButton';
|
import { FC, useState } from 'react';
|
||||||
|
import { SidebarButton } from '../Sidebar/SidebarButton';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onClearConversations: () => void;
|
onClearConversations: () => void;
|
|
@ -1,4 +1,5 @@
|
||||||
import { Conversation, KeyValuePair } from '@/types';
|
import { Conversation } from '@/types/chat';
|
||||||
|
import { KeyValuePair } from '@/types/data';
|
||||||
import {
|
import {
|
||||||
IconCheck,
|
IconCheck,
|
||||||
IconMessage,
|
IconMessage,
|
||||||
|
@ -67,10 +68,10 @@ export const ConversationComponent: FC<Props> = ({
|
||||||
return (
|
return (
|
||||||
<div className="relative flex items-center">
|
<div className="relative flex items-center">
|
||||||
{isRenaming && selectedConversation.id === conversation.id ? (
|
{isRenaming && selectedConversation.id === conversation.id ? (
|
||||||
<div className="flex w-full items-center gap-3 rounded-lg bg-gray-500/10 p-3">
|
<div className="flex w-full items-center gap-3 bg-[#343541]/90 p-3">
|
||||||
<IconMessage size={18} />
|
<IconMessage size={18} />
|
||||||
<input
|
<input
|
||||||
className="mr-12 flex-1 overflow-hidden overflow-ellipsis border-neutral-400 bg-transparent text-left text-[12.5px] leading-4 text-white outline-none focus:border-neutral-100"
|
className="mr-12 flex-1 overflow-hidden overflow-ellipsis border-neutral-400 bg-transparent text-left text-[12.5px] leading-3 text-white outline-none focus:border-neutral-100"
|
||||||
type="text"
|
type="text"
|
||||||
value={renameValue}
|
value={renameValue}
|
||||||
onChange={(e) => setRenameValue(e.target.value)}
|
onChange={(e) => setRenameValue(e.target.value)}
|
||||||
|
@ -80,10 +81,10 @@ export const ConversationComponent: FC<Props> = ({
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<button
|
<button
|
||||||
className={`flex w-full cursor-pointer items-center gap-3 rounded-lg p-3 text-sm transition-colors duration-200 hover:bg-gray-500/10 ${
|
className={`flex w-full cursor-pointer items-center gap-3 rounded-lg p-3 text-sm transition-colors duration-200 hover:bg-[#343541]/90 ${
|
||||||
loading ? 'disabled:cursor-not-allowed' : ''
|
loading ? 'disabled:cursor-not-allowed' : ''
|
||||||
} ${
|
} ${
|
||||||
selectedConversation.id === conversation.id ? 'bg-gray-500/10' : ''
|
selectedConversation.id === conversation.id ? 'bg-[#343541]/90' : ''
|
||||||
}`}
|
}`}
|
||||||
onClick={() => onSelectConversation(conversation)}
|
onClick={() => onSelectConversation(conversation)}
|
||||||
disabled={loading}
|
disabled={loading}
|
|
@ -1,4 +1,5 @@
|
||||||
import { Conversation, KeyValuePair } from '@/types';
|
import { Conversation } from '@/types/chat';
|
||||||
|
import { KeyValuePair } from '@/types/data';
|
||||||
import { FC } from 'react';
|
import { FC } from 'react';
|
||||||
import { ConversationComponent } from './Conversation';
|
import { ConversationComponent } from './Conversation';
|
||||||
|
|
||||||
|
@ -23,8 +24,11 @@ export const Conversations: FC<Props> = ({
|
||||||
onUpdateConversation,
|
onUpdateConversation,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex w-full flex-col gap-1 pt-2">
|
<div className="flex w-full flex-col gap-1">
|
||||||
{conversations.slice().reverse().map((conversation, index) => (
|
{conversations
|
||||||
|
.slice()
|
||||||
|
.reverse()
|
||||||
|
.map((conversation, index) => (
|
||||||
<ConversationComponent
|
<ConversationComponent
|
||||||
key={index}
|
key={index}
|
||||||
selectedConversation={selectedConversation}
|
selectedConversation={selectedConversation}
|
|
@ -1,4 +1,6 @@
|
||||||
import { ChatFolder, Conversation, KeyValuePair } from '@/types';
|
import { Conversation } from '@/types/chat';
|
||||||
|
import { KeyValuePair } from '@/types/data';
|
||||||
|
import { Folder } from '@/types/folder';
|
||||||
import {
|
import {
|
||||||
IconCaretDown,
|
IconCaretDown,
|
||||||
IconCaretRight,
|
IconCaretRight,
|
||||||
|
@ -8,14 +10,14 @@ import {
|
||||||
IconX,
|
IconX,
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
import { FC, KeyboardEvent, useEffect, useState } from 'react';
|
import { FC, KeyboardEvent, useEffect, useState } from 'react';
|
||||||
import { ConversationComponent } from './Conversation';
|
import { ConversationComponent } from '../../Chatbar/Conversation';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
searchTerm: string;
|
searchTerm: string;
|
||||||
conversations: Conversation[];
|
conversations: Conversation[];
|
||||||
currentFolder: ChatFolder;
|
currentFolder: Folder;
|
||||||
onDeleteFolder: (folder: number) => void;
|
onDeleteFolder: (folder: string) => void;
|
||||||
onUpdateFolder: (folder: number, name: string) => void;
|
onUpdateFolder: (folder: string, name: string) => void;
|
||||||
// conversation props
|
// conversation props
|
||||||
selectedConversation: Conversation;
|
selectedConversation: Conversation;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
|
@ -27,7 +29,7 @@ interface Props {
|
||||||
) => void;
|
) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Folder: FC<Props> = ({
|
export const ChatFolder: FC<Props> = ({
|
||||||
searchTerm,
|
searchTerm,
|
||||||
conversations,
|
conversations,
|
||||||
currentFolder,
|
currentFolder,
|
||||||
|
@ -58,7 +60,7 @@ export const Folder: FC<Props> = ({
|
||||||
setIsRenaming(false);
|
setIsRenaming(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDrop = (e: any, folder: ChatFolder) => {
|
const handleDrop = (e: any, folder: Folder) => {
|
||||||
if (e.dataTransfer) {
|
if (e.dataTransfer) {
|
||||||
setIsOpen(true);
|
setIsOpen(true);
|
||||||
|
|
||||||
|
@ -100,7 +102,7 @@ export const Folder: FC<Props> = ({
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
className={`mb-1 flex cursor-pointer items-center gap-3 rounded-lg px-3 py-2 text-[12px] leading-normal transition-colors duration-200 hover:bg-[#343541]/90`}
|
className={`mb-1 flex cursor-pointer items-center gap-3 rounded-lg px-3 py-2 text-[14px] leading-normal transition-colors duration-200 hover:bg-[#343541]/90`}
|
||||||
onClick={() => setIsOpen(!isOpen)}
|
onClick={() => setIsOpen(!isOpen)}
|
||||||
onDrop={(e) => handleDrop(e, currentFolder)}
|
onDrop={(e) => handleDrop(e, currentFolder)}
|
||||||
onDragOver={allowDrop}
|
onDragOver={allowDrop}
|
|
@ -1,13 +1,15 @@
|
||||||
import { ChatFolder, Conversation, KeyValuePair } from '@/types';
|
import { Conversation } from '@/types/chat';
|
||||||
|
import { KeyValuePair } from '@/types/data';
|
||||||
|
import { Folder } from '@/types/folder';
|
||||||
import { FC } from 'react';
|
import { FC } from 'react';
|
||||||
import { Folder } from './Folder';
|
import { ChatFolder } from './ChatFolder';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
searchTerm: string;
|
searchTerm: string;
|
||||||
conversations: Conversation[];
|
conversations: Conversation[];
|
||||||
folders: ChatFolder[];
|
folders: Folder[];
|
||||||
onDeleteFolder: (folder: number) => void;
|
onDeleteFolder: (folder: string) => void;
|
||||||
onUpdateFolder: (folder: number, name: string) => void;
|
onUpdateFolder: (folder: string, name: string) => void;
|
||||||
// conversation props
|
// conversation props
|
||||||
selectedConversation: Conversation;
|
selectedConversation: Conversation;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
|
@ -19,7 +21,7 @@ interface Props {
|
||||||
) => void;
|
) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Folders: FC<Props> = ({
|
export const ChatFolders: FC<Props> = ({
|
||||||
searchTerm,
|
searchTerm,
|
||||||
conversations,
|
conversations,
|
||||||
folders,
|
folders,
|
||||||
|
@ -35,7 +37,7 @@ export const Folders: FC<Props> = ({
|
||||||
return (
|
return (
|
||||||
<div className="flex w-full flex-col gap-1 pt-2">
|
<div className="flex w-full flex-col gap-1 pt-2">
|
||||||
{folders.map((folder, index) => (
|
{folders.map((folder, index) => (
|
||||||
<Folder
|
<ChatFolder
|
||||||
key={index}
|
key={index}
|
||||||
searchTerm={searchTerm}
|
searchTerm={searchTerm}
|
||||||
conversations={conversations.filter((c) => c.folderId)}
|
conversations={conversations.filter((c) => c.folderId)}
|
|
@ -0,0 +1,197 @@
|
||||||
|
import { PromptComponent } from '@/components/Promptbar/Prompt';
|
||||||
|
import { Folder } from '@/types/folder';
|
||||||
|
import { Prompt } from '@/types/prompt';
|
||||||
|
import {
|
||||||
|
IconCaretDown,
|
||||||
|
IconCaretRight,
|
||||||
|
IconCheck,
|
||||||
|
IconPencil,
|
||||||
|
IconTrash,
|
||||||
|
IconX,
|
||||||
|
} from '@tabler/icons-react';
|
||||||
|
import { FC, KeyboardEvent, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
searchTerm: string;
|
||||||
|
prompts: Prompt[];
|
||||||
|
currentFolder: Folder;
|
||||||
|
onDeleteFolder: (folder: string) => void;
|
||||||
|
onUpdateFolder: (folder: string, name: string) => void;
|
||||||
|
// prompt props
|
||||||
|
onDeletePrompt: (prompt: Prompt) => void;
|
||||||
|
onUpdatePrompt: (prompt: Prompt) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PromptFolder: FC<Props> = ({
|
||||||
|
searchTerm,
|
||||||
|
prompts,
|
||||||
|
currentFolder,
|
||||||
|
onDeleteFolder,
|
||||||
|
onUpdateFolder,
|
||||||
|
// prompt props
|
||||||
|
onDeletePrompt,
|
||||||
|
onUpdatePrompt,
|
||||||
|
}) => {
|
||||||
|
const [isDeleting, setIsDeleting] = useState(false);
|
||||||
|
const [isRenaming, setIsRenaming] = useState(false);
|
||||||
|
const [renameValue, setRenameValue] = useState('');
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
|
const handleEnterDown = (e: KeyboardEvent<HTMLDivElement>) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
handleRename();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRename = () => {
|
||||||
|
onUpdateFolder(currentFolder.id, renameValue);
|
||||||
|
setRenameValue('');
|
||||||
|
setIsRenaming(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDrop = (e: any, folder: Folder) => {
|
||||||
|
if (e.dataTransfer) {
|
||||||
|
setIsOpen(true);
|
||||||
|
|
||||||
|
const prompt = JSON.parse(e.dataTransfer.getData('prompt'));
|
||||||
|
|
||||||
|
const updatedPrompt = {
|
||||||
|
...prompt,
|
||||||
|
folderId: folder.id,
|
||||||
|
};
|
||||||
|
|
||||||
|
onUpdatePrompt(updatedPrompt);
|
||||||
|
|
||||||
|
e.target.style.background = 'none';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const allowDrop = (e: any) => {
|
||||||
|
e.preventDefault();
|
||||||
|
};
|
||||||
|
|
||||||
|
const highlightDrop = (e: any) => {
|
||||||
|
e.target.style.background = '#343541';
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeHighlight = (e: any) => {
|
||||||
|
e.target.style.background = 'none';
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isRenaming) {
|
||||||
|
setIsDeleting(false);
|
||||||
|
} else if (isDeleting) {
|
||||||
|
setIsRenaming(false);
|
||||||
|
}
|
||||||
|
}, [isRenaming, isDeleting]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (searchTerm) {
|
||||||
|
setIsOpen(true);
|
||||||
|
} else {
|
||||||
|
setIsOpen(false);
|
||||||
|
}
|
||||||
|
}, [searchTerm]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
className={`mb-1 flex cursor-pointer items-center gap-3 rounded-lg px-3 py-2 text-[14px] leading-normal transition-colors duration-200 hover:bg-[#343541]/90`}
|
||||||
|
onClick={() => setIsOpen(!isOpen)}
|
||||||
|
onDrop={(e) => handleDrop(e, currentFolder)}
|
||||||
|
onDragOver={allowDrop}
|
||||||
|
onDragEnter={highlightDrop}
|
||||||
|
onDragLeave={removeHighlight}
|
||||||
|
>
|
||||||
|
{isOpen ? <IconCaretDown size={16} /> : <IconCaretRight size={16} />}
|
||||||
|
|
||||||
|
{isRenaming ? (
|
||||||
|
<input
|
||||||
|
className="flex-1 overflow-hidden overflow-ellipsis border-b border-neutral-400 bg-transparent pr-1 text-left text-white outline-none focus:border-neutral-100"
|
||||||
|
type="text"
|
||||||
|
value={renameValue}
|
||||||
|
onChange={(e) => setRenameValue(e.target.value)}
|
||||||
|
onKeyDown={handleEnterDown}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="flex-1 overflow-hidden overflow-ellipsis whitespace-nowrap pr-1 text-left">
|
||||||
|
{currentFolder.name}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{(isDeleting || isRenaming) && (
|
||||||
|
<div className="-ml-2 flex gap-1">
|
||||||
|
<IconCheck
|
||||||
|
className="min-w-[20px] text-neutral-400 hover:text-neutral-100"
|
||||||
|
size={16}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
if (isDeleting) {
|
||||||
|
onDeleteFolder(currentFolder.id);
|
||||||
|
} else if (isRenaming) {
|
||||||
|
handleRename();
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsDeleting(false);
|
||||||
|
setIsRenaming(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<IconX
|
||||||
|
className="min-w-[20px] text-neutral-400 hover:text-neutral-100"
|
||||||
|
size={16}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setIsDeleting(false);
|
||||||
|
setIsRenaming(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isDeleting && !isRenaming && (
|
||||||
|
<div className="ml-2 flex gap-1">
|
||||||
|
<IconPencil
|
||||||
|
className="min-w-[20px] text-neutral-400 hover:text-neutral-100"
|
||||||
|
size={18}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setIsRenaming(true);
|
||||||
|
setRenameValue(currentFolder.name);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<IconTrash
|
||||||
|
className=" min-w-[20px] text-neutral-400 hover:text-neutral-100"
|
||||||
|
size={18}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setIsDeleting(true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isOpen
|
||||||
|
? prompts.map((prompt, index) => {
|
||||||
|
if (prompt.folderId === currentFolder.id) {
|
||||||
|
return (
|
||||||
|
<div key={index} className="ml-5 gap-2 border-l pl-2 pt-2">
|
||||||
|
<PromptComponent
|
||||||
|
prompt={prompt}
|
||||||
|
onDeletePrompt={onDeletePrompt}
|
||||||
|
onUpdatePrompt={onUpdatePrompt}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
: null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,44 @@
|
||||||
|
import { Folder } from '@/types/folder';
|
||||||
|
import { Prompt } from '@/types/prompt';
|
||||||
|
import { FC } from 'react';
|
||||||
|
import { PromptFolder } from './PromptFolder';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
searchTerm: string;
|
||||||
|
prompts: Prompt[];
|
||||||
|
folders: Folder[];
|
||||||
|
onDeleteFolder: (folder: string) => void;
|
||||||
|
onUpdateFolder: (folder: string, name: string) => void;
|
||||||
|
// prompt props
|
||||||
|
onDeletePrompt: (prompt: Prompt) => void;
|
||||||
|
onUpdatePrompt: (prompt: Prompt) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PromptFolders: FC<Props> = ({
|
||||||
|
searchTerm,
|
||||||
|
prompts,
|
||||||
|
folders,
|
||||||
|
onDeleteFolder,
|
||||||
|
onUpdateFolder,
|
||||||
|
// prompt props
|
||||||
|
onDeletePrompt,
|
||||||
|
onUpdatePrompt,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className="flex w-full flex-col gap-1 pt-2">
|
||||||
|
{folders.map((folder, index) => (
|
||||||
|
<PromptFolder
|
||||||
|
key={index}
|
||||||
|
searchTerm={searchTerm}
|
||||||
|
prompts={prompts.filter((p) => p.folderId)}
|
||||||
|
currentFolder={folder}
|
||||||
|
onDeleteFolder={onDeleteFolder}
|
||||||
|
onUpdateFolder={onUpdateFolder}
|
||||||
|
// prompt props
|
||||||
|
onDeletePrompt={onDeletePrompt}
|
||||||
|
onUpdatePrompt={onUpdatePrompt}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -3,8 +3,8 @@ import {
|
||||||
programmingLanguages,
|
programmingLanguages,
|
||||||
} from '@/utils/app/codeblock';
|
} from '@/utils/app/codeblock';
|
||||||
import { IconCheck, IconClipboard, IconDownload } from '@tabler/icons-react';
|
import { IconCheck, IconClipboard, IconDownload } from '@tabler/icons-react';
|
||||||
import { FC, useState, memo } from 'react';
|
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
|
import { FC, memo, useState } from 'react';
|
||||||
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
||||||
import { oneDark } from 'react-syntax-highlighter/dist/cjs/styles/prism';
|
import { oneDark } from 'react-syntax-highlighter/dist/cjs/styles/prism';
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Conversation } from '@/types';
|
import { Conversation } from '@/types/chat';
|
||||||
import { IconPlus } from '@tabler/icons-react';
|
import { IconPlus } from '@tabler/icons-react';
|
||||||
import { FC } from 'react';
|
import { FC } from 'react';
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,123 @@
|
||||||
|
import { Prompt } from '@/types/prompt';
|
||||||
|
import {
|
||||||
|
IconBulbFilled,
|
||||||
|
IconCheck,
|
||||||
|
IconTrash,
|
||||||
|
IconX,
|
||||||
|
} from '@tabler/icons-react';
|
||||||
|
import { DragEvent, FC, useEffect, useState } from 'react';
|
||||||
|
import { PromptModal } from './PromptModal';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
prompt: Prompt;
|
||||||
|
onUpdatePrompt: (prompt: Prompt) => void;
|
||||||
|
onDeletePrompt: (prompt: Prompt) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PromptComponent: FC<Props> = ({
|
||||||
|
prompt,
|
||||||
|
onUpdatePrompt,
|
||||||
|
onDeletePrompt,
|
||||||
|
}) => {
|
||||||
|
const [showModal, setShowModal] = useState<boolean>(false);
|
||||||
|
const [isDeleting, setIsDeleting] = useState(false);
|
||||||
|
const [isRenaming, setIsRenaming] = useState(false);
|
||||||
|
const [renameValue, setRenameValue] = useState('');
|
||||||
|
|
||||||
|
const handleDragStart = (e: DragEvent<HTMLButtonElement>, prompt: Prompt) => {
|
||||||
|
if (e.dataTransfer) {
|
||||||
|
e.dataTransfer.setData('prompt', JSON.stringify(prompt));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isRenaming) {
|
||||||
|
setIsDeleting(false);
|
||||||
|
} else if (isDeleting) {
|
||||||
|
setIsRenaming(false);
|
||||||
|
}
|
||||||
|
}, [isRenaming, isDeleting]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
className="text-sidebar flex w-full cursor-pointer items-center gap-3 rounded-lg p-3 text-[14px] transition-colors duration-200 hover:bg-[#343541]/90"
|
||||||
|
draggable="true"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setShowModal(true);
|
||||||
|
}}
|
||||||
|
onDragStart={(e) => handleDragStart(e, prompt)}
|
||||||
|
onMouseLeave={() => {
|
||||||
|
setIsDeleting(false);
|
||||||
|
setIsRenaming(false);
|
||||||
|
setRenameValue('');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconBulbFilled size={16} />
|
||||||
|
|
||||||
|
{isRenaming ? (
|
||||||
|
<input
|
||||||
|
className="flex-1 overflow-hidden overflow-ellipsis border-b border-neutral-400 bg-transparent pr-1 text-left text-white outline-none focus:border-neutral-100"
|
||||||
|
type="text"
|
||||||
|
value={renameValue}
|
||||||
|
onChange={(e) => setRenameValue(e.target.value)}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="flex-1 overflow-hidden overflow-ellipsis whitespace-nowrap pr-1 text-left">
|
||||||
|
{prompt.name}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{(isDeleting || isRenaming) && (
|
||||||
|
<div className="-ml-2 flex gap-1">
|
||||||
|
<IconCheck
|
||||||
|
className="min-w-[20px] text-neutral-400 hover:text-neutral-100"
|
||||||
|
size={16}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
if (isDeleting) {
|
||||||
|
onDeletePrompt(prompt);
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsDeleting(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<IconX
|
||||||
|
className="min-w-[20px] text-neutral-400 hover:text-neutral-100"
|
||||||
|
size={16}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setIsDeleting(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isDeleting && !isRenaming && (
|
||||||
|
<div className="-ml-2 flex gap-1">
|
||||||
|
<IconTrash
|
||||||
|
className=" min-w-[20px] text-neutral-400 hover:text-neutral-100"
|
||||||
|
size={18}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setIsDeleting(true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{showModal && (
|
||||||
|
<PromptModal
|
||||||
|
prompt={prompt}
|
||||||
|
onClose={() => setShowModal(false)}
|
||||||
|
onUpdatePrompt={onUpdatePrompt}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,117 @@
|
||||||
|
import { Prompt } from '@/types/prompt';
|
||||||
|
import { FC, KeyboardEvent, useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
prompt: Prompt;
|
||||||
|
onClose: () => void;
|
||||||
|
onUpdatePrompt: (prompt: Prompt) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PromptModal: FC<Props> = ({ prompt, onClose, onUpdatePrompt }) => {
|
||||||
|
const [name, setName] = useState(prompt.name);
|
||||||
|
const [description, setDescription] = useState(prompt.description);
|
||||||
|
const [content, setContent] = useState(prompt.content);
|
||||||
|
|
||||||
|
const modalRef = useRef<HTMLDivElement>(null);
|
||||||
|
const nameInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
const handleEnter = (e: KeyboardEvent<HTMLDivElement>) => {
|
||||||
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
|
onUpdatePrompt({ ...prompt, name, description, content: content.trim() });
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleOutsideClick = (e: MouseEvent) => {
|
||||||
|
if (modalRef.current && !modalRef.current.contains(e.target as Node)) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('click', handleOutsideClick);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('click', handleOutsideClick);
|
||||||
|
};
|
||||||
|
}, [onClose]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
nameInputRef.current?.focus();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="z-100 fixed inset-0 flex items-center justify-center bg-black bg-opacity-50"
|
||||||
|
onKeyDown={handleEnter}
|
||||||
|
>
|
||||||
|
<div className="fixed inset-0 z-10 overflow-y-auto">
|
||||||
|
<div className="flex min-h-screen items-center justify-center px-4 pt-4 pb-20 text-center sm:block sm:p-0">
|
||||||
|
<div
|
||||||
|
className="hidden sm:inline-block sm:h-screen sm:align-middle"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
ref={modalRef}
|
||||||
|
className="dark:border-netural-400 inline-block max-h-[400px] transform overflow-hidden rounded-lg border border-gray-300 bg-white px-4 pt-5 pb-4 text-left align-bottom shadow-xl transition-all dark:bg-[#202123] sm:my-8 sm:max-h-[600px] sm:w-full sm:max-w-lg sm:p-6 sm:align-middle"
|
||||||
|
role="dialog"
|
||||||
|
>
|
||||||
|
<div className="text-sm font-bold text-black dark:text-neutral-200">
|
||||||
|
Name
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
ref={nameInputRef}
|
||||||
|
className="mt-2 w-full rounded-lg border border-neutral-500 px-4 py-2 text-neutral-900 shadow focus:outline-none dark:border-neutral-800 dark:border-opacity-50 dark:bg-[#40414F] dark:text-neutral-100"
|
||||||
|
placeholder="A name for your prompt."
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => setName(e.target.value)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="mt-6 text-sm font-bold text-black dark:text-neutral-200">
|
||||||
|
Description
|
||||||
|
</div>
|
||||||
|
<textarea
|
||||||
|
className="mt-2 w-full rounded-lg border border-neutral-500 px-4 py-2 text-neutral-900 shadow focus:outline-none dark:border-neutral-800 dark:border-opacity-50 dark:bg-[#40414F] dark:text-neutral-100"
|
||||||
|
style={{ resize: 'none' }}
|
||||||
|
placeholder="A description for your prompt."
|
||||||
|
value={description}
|
||||||
|
onChange={(e) => setDescription(e.target.value)}
|
||||||
|
rows={3}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="mt-6 text-sm font-bold text-black dark:text-neutral-200">
|
||||||
|
Prompt
|
||||||
|
</div>
|
||||||
|
<textarea
|
||||||
|
className="mt-2 w-full rounded-lg border border-neutral-500 px-4 py-2 text-neutral-900 shadow focus:outline-none dark:border-neutral-800 dark:border-opacity-50 dark:bg-[#40414F] dark:text-neutral-100"
|
||||||
|
style={{ resize: 'none' }}
|
||||||
|
placeholder="Prompt content. Use {{}} to denote a variable. Ex: {{name}} is a {{adjective}} {{noun}}"
|
||||||
|
value={content}
|
||||||
|
onChange={(e) => setContent(e.target.value)}
|
||||||
|
rows={10}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="mt-6 w-full rounded-lg border border-neutral-500 px-4 py-2 text-neutral-900 shadow hover:bg-neutral-100 focus:outline-none dark:border-neutral-800 dark:border-opacity-50 dark:bg-white dark:text-black dark:hover:bg-neutral-300"
|
||||||
|
onClick={() => {
|
||||||
|
const updatedPrompt = {
|
||||||
|
...prompt,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
content: content.trim(),
|
||||||
|
};
|
||||||
|
|
||||||
|
onUpdatePrompt(updatedPrompt);
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,175 @@
|
||||||
|
import { Folder } from '@/types/folder';
|
||||||
|
import { Prompt } from '@/types/prompt';
|
||||||
|
import {
|
||||||
|
IconArrowBarRight,
|
||||||
|
IconFolderPlus,
|
||||||
|
IconPlus,
|
||||||
|
} from '@tabler/icons-react';
|
||||||
|
import { FC, useEffect, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { PromptFolders } from '../Folders/Prompt/PromptFolders';
|
||||||
|
import { Search } from '../Sidebar/Search';
|
||||||
|
import { PromptbarSettings } from './PromptbarSettings';
|
||||||
|
import { Prompts } from './Prompts';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
prompts: Prompt[];
|
||||||
|
folders: Folder[];
|
||||||
|
onCreateFolder: (name: string) => void;
|
||||||
|
onDeleteFolder: (folderId: string) => void;
|
||||||
|
onUpdateFolder: (folderId: string, name: string) => void;
|
||||||
|
onToggleSidebar: () => void;
|
||||||
|
onCreatePrompt: () => void;
|
||||||
|
onUpdatePrompt: (prompt: Prompt) => void;
|
||||||
|
onDeletePrompt: (prompt: Prompt) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Promptbar: FC<Props> = ({
|
||||||
|
folders,
|
||||||
|
prompts,
|
||||||
|
onCreateFolder,
|
||||||
|
onDeleteFolder,
|
||||||
|
onUpdateFolder,
|
||||||
|
onCreatePrompt,
|
||||||
|
onUpdatePrompt,
|
||||||
|
onDeletePrompt,
|
||||||
|
onToggleSidebar,
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation('promptbar');
|
||||||
|
const [searchTerm, setSearchTerm] = useState<string>('');
|
||||||
|
const [filteredPrompts, setFilteredPrompts] = useState<Prompt[]>(prompts);
|
||||||
|
|
||||||
|
const handleUpdatePrompt = (prompt: Prompt) => {
|
||||||
|
onUpdatePrompt(prompt);
|
||||||
|
setSearchTerm('');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeletePrompt = (prompt: Prompt) => {
|
||||||
|
onDeletePrompt(prompt);
|
||||||
|
setSearchTerm('');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDrop = (e: any) => {
|
||||||
|
if (e.dataTransfer) {
|
||||||
|
const prompt = JSON.parse(e.dataTransfer.getData('prompt'));
|
||||||
|
|
||||||
|
const updatedPrompt = {
|
||||||
|
...prompt,
|
||||||
|
folderId: e.target.dataset.folderId,
|
||||||
|
};
|
||||||
|
|
||||||
|
onUpdatePrompt(updatedPrompt);
|
||||||
|
|
||||||
|
e.target.style.background = 'none';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const allowDrop = (e: any) => {
|
||||||
|
e.preventDefault();
|
||||||
|
};
|
||||||
|
|
||||||
|
const highlightDrop = (e: any) => {
|
||||||
|
e.target.style.background = '#343541';
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeHighlight = (e: any) => {
|
||||||
|
e.target.style.background = 'none';
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (searchTerm) {
|
||||||
|
setFilteredPrompts(
|
||||||
|
prompts.filter((prompt) => {
|
||||||
|
const searchable =
|
||||||
|
prompt.name.toLowerCase() +
|
||||||
|
' ' +
|
||||||
|
prompt.description.toLowerCase() +
|
||||||
|
' ' +
|
||||||
|
prompt.content.toLowerCase();
|
||||||
|
return searchable.includes(searchTerm.toLowerCase());
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
setFilteredPrompts(prompts);
|
||||||
|
}
|
||||||
|
}, [searchTerm, prompts]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`fixed top-0 bottom-0 z-50 flex h-full w-[260px] flex-none flex-col space-y-2 bg-[#202123] p-2 text-[14px] transition-all sm:relative sm:top-0`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<button
|
||||||
|
className="text-sidebar flex w-[190px] flex-shrink-0 cursor-pointer select-none items-center gap-3 rounded-md border border-white/20 p-3 text-white transition-colors duration-200 hover:bg-gray-500/10"
|
||||||
|
onClick={() => {
|
||||||
|
onCreatePrompt();
|
||||||
|
setSearchTerm('');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconPlus size={16} />
|
||||||
|
{t('New prompt')}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className="ml-2 flex flex-shrink-0 cursor-pointer items-center gap-3 rounded-md border border-white/20 p-3 text-sm text-white transition-colors duration-200 hover:bg-gray-500/10"
|
||||||
|
onClick={() => onCreateFolder(t('New folder'))}
|
||||||
|
>
|
||||||
|
<IconFolderPlus size={16} />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<IconArrowBarRight
|
||||||
|
className="ml-1 hidden cursor-pointer p-1 text-neutral-300 hover:text-neutral-400 sm:flex"
|
||||||
|
size={32}
|
||||||
|
onClick={onToggleSidebar}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{prompts.length > 1 && (
|
||||||
|
<Search
|
||||||
|
placeholder="Search prompts..."
|
||||||
|
searchTerm={searchTerm}
|
||||||
|
onSearch={setSearchTerm}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex-grow overflow-auto">
|
||||||
|
{folders.length > 0 && (
|
||||||
|
<div className="flex border-b border-white/20 pb-2">
|
||||||
|
<PromptFolders
|
||||||
|
searchTerm={searchTerm}
|
||||||
|
prompts={filteredPrompts}
|
||||||
|
folders={folders.filter((folder) => folder.type === 'prompt')}
|
||||||
|
onUpdateFolder={onUpdateFolder}
|
||||||
|
onDeleteFolder={onDeleteFolder}
|
||||||
|
// prompt props
|
||||||
|
onDeletePrompt={handleDeletePrompt}
|
||||||
|
onUpdatePrompt={handleUpdatePrompt}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{prompts.length > 0 ? (
|
||||||
|
<div
|
||||||
|
className="h-full pt-2"
|
||||||
|
onDrop={(e) => handleDrop(e)}
|
||||||
|
onDragOver={allowDrop}
|
||||||
|
onDragEnter={highlightDrop}
|
||||||
|
onDragLeave={removeHighlight}
|
||||||
|
>
|
||||||
|
<Prompts
|
||||||
|
prompts={filteredPrompts.filter((prompt) => !prompt.folderId)}
|
||||||
|
onUpdatePrompt={handleUpdatePrompt}
|
||||||
|
onDeletePrompt={handleDeletePrompt}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="mt-4 text-center text-white">
|
||||||
|
<div>{t('No prompts.')}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<PromptbarSettings />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { FC } from "react";
|
||||||
|
|
||||||
|
interface Props {}
|
||||||
|
|
||||||
|
export const PromptbarSettings: FC<Props> = () => {
|
||||||
|
return <div></div>;
|
||||||
|
};
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { Prompt } from '@/types/prompt';
|
||||||
|
import { FC } from 'react';
|
||||||
|
import { PromptComponent } from './Prompt';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
prompts: Prompt[];
|
||||||
|
onUpdatePrompt: (prompt: Prompt) => void;
|
||||||
|
onDeletePrompt: (prompt: Prompt) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Prompts: FC<Props> = ({
|
||||||
|
prompts,
|
||||||
|
onUpdatePrompt,
|
||||||
|
onDeletePrompt,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className="flex w-full flex-col gap-1">
|
||||||
|
{prompts
|
||||||
|
.slice()
|
||||||
|
.reverse()
|
||||||
|
.map((prompt, index) => (
|
||||||
|
<PromptComponent
|
||||||
|
key={index}
|
||||||
|
prompt={prompt}
|
||||||
|
onUpdatePrompt={onUpdatePrompt}
|
||||||
|
onDeletePrompt={onDeletePrompt}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,14 +1,15 @@
|
||||||
import { ChatFolder, Conversation } from '@/types';
|
import { Conversation } from '@/types/chat';
|
||||||
|
import { Folder } from '@/types/folder';
|
||||||
import { cleanConversationHistory } from '@/utils/app/clean';
|
import { cleanConversationHistory } from '@/utils/app/clean';
|
||||||
import { IconFileImport } from '@tabler/icons-react';
|
import { IconFileImport } from '@tabler/icons-react';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import { FC } from 'react';
|
import { FC } from 'react';
|
||||||
import { SidebarButton } from './SidebarButton';
|
import { SidebarButton } from '../Sidebar/SidebarButton';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onImport: (data: {
|
onImport: (data: {
|
||||||
conversations: Conversation[];
|
conversations: Conversation[];
|
||||||
folders: ChatFolder[];
|
folders: Folder[];
|
||||||
}) => void;
|
}) => void;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { IconCheck, IconKey, IconX } from '@tabler/icons-react';
|
import { IconCheck, IconKey, IconX } from '@tabler/icons-react';
|
||||||
import { FC, KeyboardEvent, useState } from 'react';
|
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import { SidebarButton } from './SidebarButton';
|
import { FC, KeyboardEvent, useState } from 'react';
|
||||||
|
import { SidebarButton } from '../Sidebar/SidebarButton';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
apiKey: string;
|
apiKey: string;
|
|
@ -1,13 +1,14 @@
|
||||||
import { IconX } from '@tabler/icons-react';
|
import { IconX } from '@tabler/icons-react';
|
||||||
import { FC } from 'react';
|
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
|
import { FC } from 'react';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
placeholder: string;
|
||||||
searchTerm: string;
|
searchTerm: string;
|
||||||
onSearch: (searchTerm: string) => void;
|
onSearch: (searchTerm: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Search: FC<Props> = ({ searchTerm, onSearch }) => {
|
export const Search: FC<Props> = ({ placeholder, searchTerm, onSearch }) => {
|
||||||
const { t } = useTranslation('sidebar');
|
const { t } = useTranslation('sidebar');
|
||||||
|
|
||||||
const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
@ -21,9 +22,9 @@ export const Search: FC<Props> = ({ searchTerm, onSearch }) => {
|
||||||
return (
|
return (
|
||||||
<div className="relative flex items-center">
|
<div className="relative flex items-center">
|
||||||
<input
|
<input
|
||||||
className="w-full flex-1 rounded-md border border-neutral-600 bg-[#202123] px-4 py-3 pr-10 text-[12px] leading-3 text-white"
|
className="w-full flex-1 rounded-md border border-neutral-600 bg-[#202123] px-4 py-3 pr-10 text-[14px] leading-3 text-white"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={t('Search conversations...') || ''}
|
placeholder={t(placeholder) || ''}
|
||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
onChange={handleSearchChange}
|
onChange={handleSearchChange}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -9,7 +9,7 @@ interface Props {
|
||||||
export const SidebarButton: FC<Props> = ({ text, icon, onClick }) => {
|
export const SidebarButton: FC<Props> = ({ text, icon, onClick }) => {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className="flex w-full cursor-pointer select-none items-center gap-3 rounded-md py-3 px-3 text-[12.5px] leading-3 text-white transition-colors duration-200 hover:bg-gray-500/10"
|
className="flex w-full cursor-pointer select-none items-center gap-3 rounded-md py-3 px-3 text-[14px] leading-3 text-white transition-colors duration-200 hover:bg-gray-500/10"
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
>
|
>
|
||||||
<div>{icon}</div>
|
<div>{icon}</div>
|
||||||
|
|
|
@ -22,7 +22,8 @@
|
||||||
"react-syntax-highlighter": "^15.5.0",
|
"react-syntax-highlighter": "^15.5.0",
|
||||||
"rehype-mathjax": "^4.0.2",
|
"rehype-mathjax": "^4.0.2",
|
||||||
"remark-gfm": "^3.0.1",
|
"remark-gfm": "^3.0.1",
|
||||||
"remark-math": "^5.1.1"
|
"remark-math": "^5.1.1",
|
||||||
|
"uuid": "^9.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/typography": "^0.5.9",
|
"@tailwindcss/typography": "^0.5.9",
|
||||||
|
@ -30,6 +31,7 @@
|
||||||
"@types/react": "18.0.28",
|
"@types/react": "18.0.28",
|
||||||
"@types/react-dom": "18.0.11",
|
"@types/react-dom": "18.0.11",
|
||||||
"@types/react-syntax-highlighter": "^15.5.6",
|
"@types/react-syntax-highlighter": "^15.5.6",
|
||||||
|
"@types/uuid": "^9.0.1",
|
||||||
"autoprefixer": "^10.4.14",
|
"autoprefixer": "^10.4.14",
|
||||||
"eslint": "8.36.0",
|
"eslint": "8.36.0",
|
||||||
"eslint-config-next": "13.2.4",
|
"eslint-config-next": "13.2.4",
|
||||||
|
@ -574,6 +576,12 @@
|
||||||
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.6.tgz",
|
||||||
"integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ=="
|
"integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ=="
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/uuid": {
|
||||||
|
"version": "9.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.1.tgz",
|
||||||
|
"integrity": "sha512-rFT3ak0/2trgvp4yYZo5iKFEPsET7vKydKF+VRCxlQ9bpheehyAJH89dAkaLEq/j/RZXJIqcgsmPJKUP1Z28HA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/@types/web": {
|
"node_modules/@types/web": {
|
||||||
"version": "0.0.46",
|
"version": "0.0.46",
|
||||||
"resolved": "https://registry.npmjs.org/@types/web/-/web-0.0.46.tgz",
|
"resolved": "https://registry.npmjs.org/@types/web/-/web-0.0.46.tgz",
|
||||||
|
@ -6441,6 +6449,14 @@
|
||||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/uuid": {
|
||||||
|
"version": "9.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz",
|
||||||
|
"integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==",
|
||||||
|
"bin": {
|
||||||
|
"uuid": "dist/bin/uuid"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/uvu": {
|
"node_modules/uvu": {
|
||||||
"version": "0.5.6",
|
"version": "0.5.6",
|
||||||
"resolved": "https://registry.npmjs.org/uvu/-/uvu-0.5.6.tgz",
|
"resolved": "https://registry.npmjs.org/uvu/-/uvu-0.5.6.tgz",
|
||||||
|
@ -7088,6 +7104,12 @@
|
||||||
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.6.tgz",
|
||||||
"integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ=="
|
"integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ=="
|
||||||
},
|
},
|
||||||
|
"@types/uuid": {
|
||||||
|
"version": "9.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.1.tgz",
|
||||||
|
"integrity": "sha512-rFT3ak0/2trgvp4yYZo5iKFEPsET7vKydKF+VRCxlQ9bpheehyAJH89dAkaLEq/j/RZXJIqcgsmPJKUP1Z28HA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"@types/web": {
|
"@types/web": {
|
||||||
"version": "0.0.46",
|
"version": "0.0.46",
|
||||||
"resolved": "https://registry.npmjs.org/@types/web/-/web-0.0.46.tgz",
|
"resolved": "https://registry.npmjs.org/@types/web/-/web-0.0.46.tgz",
|
||||||
|
@ -11086,6 +11108,11 @@
|
||||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"uuid": {
|
||||||
|
"version": "9.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz",
|
||||||
|
"integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg=="
|
||||||
|
},
|
||||||
"uvu": {
|
"uvu": {
|
||||||
"version": "0.5.6",
|
"version": "0.5.6",
|
||||||
"resolved": "https://registry.npmjs.org/uvu/-/uvu-0.5.6.tgz",
|
"resolved": "https://registry.npmjs.org/uvu/-/uvu-0.5.6.tgz",
|
||||||
|
|
|
@ -24,7 +24,8 @@
|
||||||
"react-syntax-highlighter": "^15.5.0",
|
"react-syntax-highlighter": "^15.5.0",
|
||||||
"rehype-mathjax": "^4.0.2",
|
"rehype-mathjax": "^4.0.2",
|
||||||
"remark-gfm": "^3.0.1",
|
"remark-gfm": "^3.0.1",
|
||||||
"remark-math": "^5.1.1"
|
"remark-math": "^5.1.1",
|
||||||
|
"uuid": "^9.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/typography": "^0.5.9",
|
"@tailwindcss/typography": "^0.5.9",
|
||||||
|
@ -32,6 +33,7 @@
|
||||||
"@types/react": "18.0.28",
|
"@types/react": "18.0.28",
|
||||||
"@types/react-dom": "18.0.11",
|
"@types/react-dom": "18.0.11",
|
||||||
"@types/react-syntax-highlighter": "^15.5.6",
|
"@types/react-syntax-highlighter": "^15.5.6",
|
||||||
|
"@types/uuid": "^9.0.1",
|
||||||
"autoprefixer": "^10.4.14",
|
"autoprefixer": "^10.4.14",
|
||||||
"eslint": "8.36.0",
|
"eslint": "8.36.0",
|
||||||
"eslint-config-next": "13.2.4",
|
"eslint-config-next": "13.2.4",
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { ChatBody, Message, OpenAIModelID } from '@/types';
|
import { ChatBody, Message } from '@/types/chat';
|
||||||
|
import { OpenAIModelID } from '@/types/openai';
|
||||||
import { DEFAULT_SYSTEM_PROMPT } from '@/utils/app/const';
|
import { DEFAULT_SYSTEM_PROMPT } from '@/utils/app/const';
|
||||||
import { OpenAIStream } from '@/utils/server';
|
import { OpenAIStream } from '@/utils/server';
|
||||||
import tiktokenModel from '@dqbd/tiktoken/encoders/cl100k_base.json';
|
import tiktokenModel from '@dqbd/tiktoken/encoders/cl100k_base.json';
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { OpenAIModel, OpenAIModelID, OpenAIModels } from '@/types';
|
import { OpenAIModel, OpenAIModelID, OpenAIModels } from '@/types/openai';
|
||||||
import { OPENAI_API_HOST } from '@/utils/app/const';
|
import { OPENAI_API_HOST } from '@/utils/app/const';
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
|
|
256
pages/index.tsx
256
pages/index.tsx
|
@ -1,17 +1,13 @@
|
||||||
import { Chat } from '@/components/Chat/Chat';
|
import { Chat } from '@/components/Chat/Chat';
|
||||||
|
import { Chatbar } from '@/components/Chatbar/Chatbar';
|
||||||
import { Navbar } from '@/components/Mobile/Navbar';
|
import { Navbar } from '@/components/Mobile/Navbar';
|
||||||
import { Sidebar } from '@/components/Sidebar/Sidebar';
|
import { Promptbar } from '@/components/Promptbar/Promptbar';
|
||||||
import {
|
import { ChatBody, Conversation, Message } from '@/types/chat';
|
||||||
ChatBody,
|
import { KeyValuePair } from '@/types/data';
|
||||||
ChatFolder,
|
import { ErrorMessage } from '@/types/error';
|
||||||
Conversation,
|
import { Folder, FolderType } from '@/types/folder';
|
||||||
ErrorMessage,
|
import { OpenAIModel, OpenAIModelID, OpenAIModels } from '@/types/openai';
|
||||||
KeyValuePair,
|
import { Prompt } from '@/types/prompt';
|
||||||
Message,
|
|
||||||
OpenAIModel,
|
|
||||||
OpenAIModelID,
|
|
||||||
OpenAIModels,
|
|
||||||
} from '@/types';
|
|
||||||
import {
|
import {
|
||||||
cleanConversationHistory,
|
cleanConversationHistory,
|
||||||
cleanSelectedConversation,
|
cleanSelectedConversation,
|
||||||
|
@ -24,12 +20,14 @@ import {
|
||||||
} from '@/utils/app/conversation';
|
} from '@/utils/app/conversation';
|
||||||
import { saveFolders } from '@/utils/app/folders';
|
import { saveFolders } from '@/utils/app/folders';
|
||||||
import { exportData, importData } from '@/utils/app/importExport';
|
import { exportData, importData } from '@/utils/app/importExport';
|
||||||
|
import { savePrompts } from '@/utils/app/prompts';
|
||||||
import { IconArrowBarLeft, IconArrowBarRight } from '@tabler/icons-react';
|
import { IconArrowBarLeft, IconArrowBarRight } from '@tabler/icons-react';
|
||||||
import { GetServerSideProps } from 'next';
|
import { GetServerSideProps } from 'next';
|
||||||
|
import { useTranslation } from 'next-i18next';
|
||||||
|
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import { useTranslation } from 'next-i18next';
|
|
||||||
|
|
||||||
interface HomeProps {
|
interface HomeProps {
|
||||||
serverSideApiKeyIsSet: boolean;
|
serverSideApiKeyIsSet: boolean;
|
||||||
|
@ -37,22 +35,36 @@ interface HomeProps {
|
||||||
|
|
||||||
const Home: React.FC<HomeProps> = ({ serverSideApiKeyIsSet }) => {
|
const Home: React.FC<HomeProps> = ({ serverSideApiKeyIsSet }) => {
|
||||||
const { t } = useTranslation('chat');
|
const { t } = useTranslation('chat');
|
||||||
const [folders, setFolders] = useState<ChatFolder[]>([]);
|
|
||||||
|
// STATE ----------------------------------------------
|
||||||
|
|
||||||
|
const [apiKey, setApiKey] = useState<string>('');
|
||||||
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
|
const [lightMode, setLightMode] = useState<'dark' | 'light'>('dark');
|
||||||
|
const [messageIsStreaming, setMessageIsStreaming] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const [modelError, setModelError] = useState<ErrorMessage | null>(null);
|
||||||
|
|
||||||
|
const [models, setModels] = useState<OpenAIModel[]>([]);
|
||||||
|
|
||||||
|
const [folders, setFolders] = useState<Folder[]>([]);
|
||||||
|
|
||||||
const [conversations, setConversations] = useState<Conversation[]>([]);
|
const [conversations, setConversations] = useState<Conversation[]>([]);
|
||||||
const [selectedConversation, setSelectedConversation] =
|
const [selectedConversation, setSelectedConversation] =
|
||||||
useState<Conversation>();
|
useState<Conversation>();
|
||||||
const [loading, setLoading] = useState<boolean>(false);
|
|
||||||
const [models, setModels] = useState<OpenAIModel[]>([]);
|
|
||||||
const [lightMode, setLightMode] = useState<'dark' | 'light'>('dark');
|
|
||||||
const [messageIsStreaming, setMessageIsStreaming] = useState<boolean>(false);
|
|
||||||
const [showSidebar, setShowSidebar] = useState<boolean>(true);
|
|
||||||
const [apiKey, setApiKey] = useState<string>('');
|
|
||||||
const [messageError, setMessageError] = useState<boolean>(false);
|
|
||||||
const [modelError, setModelError] = useState<ErrorMessage | null>(null);
|
|
||||||
const [currentMessage, setCurrentMessage] = useState<Message>();
|
const [currentMessage, setCurrentMessage] = useState<Message>();
|
||||||
|
|
||||||
|
const [showSidebar, setShowSidebar] = useState<boolean>(true);
|
||||||
|
|
||||||
|
const [prompts, setPrompts] = useState<Prompt[]>([]);
|
||||||
|
const [showPromptbar, setShowPromptbar] = useState<boolean>(true);
|
||||||
|
|
||||||
|
// REFS ----------------------------------------------
|
||||||
|
|
||||||
const stopConversationRef = useRef<boolean>(false);
|
const stopConversationRef = useRef<boolean>(false);
|
||||||
|
|
||||||
|
// FETCH RESPONSE ----------------------------------------------
|
||||||
|
|
||||||
const handleSend = async (message: Message, deleteCount = 0) => {
|
const handleSend = async (message: Message, deleteCount = 0) => {
|
||||||
if (selectedConversation) {
|
if (selectedConversation) {
|
||||||
let updatedConversation: Conversation;
|
let updatedConversation: Conversation;
|
||||||
|
@ -77,7 +89,6 @@ const Home: React.FC<HomeProps> = ({ serverSideApiKeyIsSet }) => {
|
||||||
setSelectedConversation(updatedConversation);
|
setSelectedConversation(updatedConversation);
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setMessageIsStreaming(true);
|
setMessageIsStreaming(true);
|
||||||
setMessageError(false);
|
|
||||||
|
|
||||||
const chatBody: ChatBody = {
|
const chatBody: ChatBody = {
|
||||||
model: updatedConversation.model,
|
model: updatedConversation.model,
|
||||||
|
@ -99,7 +110,6 @@ const Home: React.FC<HomeProps> = ({ serverSideApiKeyIsSet }) => {
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setMessageIsStreaming(false);
|
setMessageIsStreaming(false);
|
||||||
setMessageError(true);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -108,8 +118,6 @@ const Home: React.FC<HomeProps> = ({ serverSideApiKeyIsSet }) => {
|
||||||
if (!data) {
|
if (!data) {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setMessageIsStreaming(false);
|
setMessageIsStreaming(false);
|
||||||
setMessageError(true);
|
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -204,6 +212,8 @@ const Home: React.FC<HomeProps> = ({ serverSideApiKeyIsSet }) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// FETCH MODELS ----------------------------------------------
|
||||||
|
|
||||||
const fetchModels = async (key: string) => {
|
const fetchModels = async (key: string) => {
|
||||||
const error = {
|
const error = {
|
||||||
title: t('Error fetching models.'),
|
title: t('Error fetching models.'),
|
||||||
|
@ -249,6 +259,8 @@ const Home: React.FC<HomeProps> = ({ serverSideApiKeyIsSet }) => {
|
||||||
setModelError(null);
|
setModelError(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// BASIC HANDLERS --------------------------------------------
|
||||||
|
|
||||||
const handleLightMode = (mode: 'dark' | 'light') => {
|
const handleLightMode = (mode: 'dark' | 'light') => {
|
||||||
setLightMode(mode);
|
setLightMode(mode);
|
||||||
localStorage.setItem('theme', mode);
|
localStorage.setItem('theme', mode);
|
||||||
|
@ -259,18 +271,33 @@ const Home: React.FC<HomeProps> = ({ serverSideApiKeyIsSet }) => {
|
||||||
localStorage.setItem('apiKey', apiKey);
|
localStorage.setItem('apiKey', apiKey);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleToggleChatbar = () => {
|
||||||
|
setShowSidebar(!showSidebar);
|
||||||
|
localStorage.setItem('showChatbar', JSON.stringify(!showSidebar));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTogglePromptbar = () => {
|
||||||
|
setShowPromptbar(!showPromptbar);
|
||||||
|
localStorage.setItem('showPromptbar', JSON.stringify(!showPromptbar));
|
||||||
|
};
|
||||||
|
|
||||||
const handleExportData = () => {
|
const handleExportData = () => {
|
||||||
exportData();
|
exportData();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleImportConversations = (data: {
|
const handleImportConversations = (data: {
|
||||||
conversations: Conversation[];
|
conversations: Conversation[];
|
||||||
folders: ChatFolder[];
|
folders: Folder[];
|
||||||
}) => {
|
}) => {
|
||||||
importData(data.conversations, data.folders);
|
const updatedConversations = [...conversations, ...data.conversations];
|
||||||
setConversations(data.conversations);
|
const updatedFolders = [...folders, ...data.folders];
|
||||||
setSelectedConversation(data.conversations[data.conversations.length - 1]);
|
|
||||||
setFolders(data.folders);
|
importData(updatedConversations, updatedFolders);
|
||||||
|
setConversations(updatedConversations);
|
||||||
|
setSelectedConversation(
|
||||||
|
updatedConversations[updatedConversations.length - 1],
|
||||||
|
);
|
||||||
|
setFolders(updatedFolders);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSelectConversation = (conversation: Conversation) => {
|
const handleSelectConversation = (conversation: Conversation) => {
|
||||||
|
@ -278,12 +305,13 @@ const Home: React.FC<HomeProps> = ({ serverSideApiKeyIsSet }) => {
|
||||||
saveConversation(conversation);
|
saveConversation(conversation);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCreateFolder = (name: string) => {
|
// FOLDER OPERATIONS --------------------------------------------
|
||||||
const lastFolder = folders[folders.length - 1];
|
|
||||||
|
|
||||||
const newFolder: ChatFolder = {
|
const handleCreateFolder = (name: string, type: FolderType) => {
|
||||||
id: lastFolder ? lastFolder.id + 1 : 1,
|
const newFolder: Folder = {
|
||||||
|
id: uuidv4(),
|
||||||
name,
|
name,
|
||||||
|
type,
|
||||||
};
|
};
|
||||||
|
|
||||||
const updatedFolders = [...folders, newFolder];
|
const updatedFolders = [...folders, newFolder];
|
||||||
|
@ -292,7 +320,7 @@ const Home: React.FC<HomeProps> = ({ serverSideApiKeyIsSet }) => {
|
||||||
saveFolders(updatedFolders);
|
saveFolders(updatedFolders);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteFolder = (folderId: number) => {
|
const handleDeleteFolder = (folderId: string) => {
|
||||||
const updatedFolders = folders.filter((f) => f.id !== folderId);
|
const updatedFolders = folders.filter((f) => f.id !== folderId);
|
||||||
setFolders(updatedFolders);
|
setFolders(updatedFolders);
|
||||||
saveFolders(updatedFolders);
|
saveFolders(updatedFolders);
|
||||||
|
@ -301,7 +329,7 @@ const Home: React.FC<HomeProps> = ({ serverSideApiKeyIsSet }) => {
|
||||||
if (c.folderId === folderId) {
|
if (c.folderId === folderId) {
|
||||||
return {
|
return {
|
||||||
...c,
|
...c,
|
||||||
folderId: 0,
|
folderId: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -309,9 +337,22 @@ const Home: React.FC<HomeProps> = ({ serverSideApiKeyIsSet }) => {
|
||||||
});
|
});
|
||||||
setConversations(updatedConversations);
|
setConversations(updatedConversations);
|
||||||
saveConversations(updatedConversations);
|
saveConversations(updatedConversations);
|
||||||
|
|
||||||
|
const updatedPrompts: Prompt[] = prompts.map((p) => {
|
||||||
|
if (p.folderId === folderId) {
|
||||||
|
return {
|
||||||
|
...p,
|
||||||
|
folderId: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return p;
|
||||||
|
});
|
||||||
|
setPrompts(updatedPrompts);
|
||||||
|
savePrompts(updatedPrompts);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleUpdateFolder = (folderId: number, name: string) => {
|
const handleUpdateFolder = (folderId: string, name: string) => {
|
||||||
const updatedFolders = folders.map((f) => {
|
const updatedFolders = folders.map((f) => {
|
||||||
if (f.id === folderId) {
|
if (f.id === folderId) {
|
||||||
return {
|
return {
|
||||||
|
@ -327,18 +368,18 @@ const Home: React.FC<HomeProps> = ({ serverSideApiKeyIsSet }) => {
|
||||||
saveFolders(updatedFolders);
|
saveFolders(updatedFolders);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// CONVERSATION OPERATIONS --------------------------------------------
|
||||||
|
|
||||||
const handleNewConversation = () => {
|
const handleNewConversation = () => {
|
||||||
const lastConversation = conversations[conversations.length - 1];
|
const lastConversation = conversations[conversations.length - 1];
|
||||||
|
|
||||||
const newConversation: Conversation = {
|
const newConversation: Conversation = {
|
||||||
id: lastConversation ? lastConversation.id + 1 : 1,
|
id: uuidv4(),
|
||||||
name: `${t('Conversation')} ${
|
name: `${t('New Conversation')}`,
|
||||||
lastConversation ? lastConversation.id + 1 : 1
|
|
||||||
}`,
|
|
||||||
messages: [],
|
messages: [],
|
||||||
model: OpenAIModels[OpenAIModelID.GPT_3_5],
|
model: OpenAIModels[OpenAIModelID.GPT_3_5],
|
||||||
prompt: DEFAULT_SYSTEM_PROMPT,
|
prompt: DEFAULT_SYSTEM_PROMPT,
|
||||||
folderId: 0,
|
folderId: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
const updatedConversations = [...conversations, newConversation];
|
const updatedConversations = [...conversations, newConversation];
|
||||||
|
@ -366,12 +407,12 @@ const Home: React.FC<HomeProps> = ({ serverSideApiKeyIsSet }) => {
|
||||||
saveConversation(updatedConversations[updatedConversations.length - 1]);
|
saveConversation(updatedConversations[updatedConversations.length - 1]);
|
||||||
} else {
|
} else {
|
||||||
setSelectedConversation({
|
setSelectedConversation({
|
||||||
id: 1,
|
id: uuidv4(),
|
||||||
name: 'New conversation',
|
name: 'New conversation',
|
||||||
messages: [],
|
messages: [],
|
||||||
model: OpenAIModels[OpenAIModelID.GPT_3_5],
|
model: OpenAIModels[OpenAIModelID.GPT_3_5],
|
||||||
prompt: DEFAULT_SYSTEM_PROMPT,
|
prompt: DEFAULT_SYSTEM_PROMPT,
|
||||||
folderId: 0,
|
folderId: null,
|
||||||
});
|
});
|
||||||
localStorage.removeItem('selectedConversation');
|
localStorage.removeItem('selectedConversation');
|
||||||
}
|
}
|
||||||
|
@ -400,17 +441,18 @@ const Home: React.FC<HomeProps> = ({ serverSideApiKeyIsSet }) => {
|
||||||
localStorage.removeItem('conversationHistory');
|
localStorage.removeItem('conversationHistory');
|
||||||
|
|
||||||
setSelectedConversation({
|
setSelectedConversation({
|
||||||
id: 1,
|
id: uuidv4(),
|
||||||
name: 'New conversation',
|
name: 'New conversation',
|
||||||
messages: [],
|
messages: [],
|
||||||
model: OpenAIModels[OpenAIModelID.GPT_3_5],
|
model: OpenAIModels[OpenAIModelID.GPT_3_5],
|
||||||
prompt: DEFAULT_SYSTEM_PROMPT,
|
prompt: DEFAULT_SYSTEM_PROMPT,
|
||||||
folderId: 0,
|
folderId: null,
|
||||||
});
|
});
|
||||||
localStorage.removeItem('selectedConversation');
|
localStorage.removeItem('selectedConversation');
|
||||||
|
|
||||||
setFolders([]);
|
const updatedFolders = folders.filter((f) => f.type !== 'chat');
|
||||||
localStorage.removeItem('folders');
|
setFolders(updatedFolders);
|
||||||
|
saveFolders(updatedFolders);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEditMessage = (message: Message, messageIndex: number) => {
|
const handleEditMessage = (message: Message, messageIndex: number) => {
|
||||||
|
@ -440,6 +482,49 @@ const Home: React.FC<HomeProps> = ({ serverSideApiKeyIsSet }) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// PROMPT OPERATIONS --------------------------------------------
|
||||||
|
|
||||||
|
const handleCreatePrompt = () => {
|
||||||
|
const lastPrompt = prompts[prompts.length - 1];
|
||||||
|
|
||||||
|
const newPrompt: Prompt = {
|
||||||
|
id: uuidv4(),
|
||||||
|
name: `Prompt ${prompts.length + 1}`,
|
||||||
|
description: '',
|
||||||
|
content: '',
|
||||||
|
model: OpenAIModels[OpenAIModelID.GPT_3_5],
|
||||||
|
folderId: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const updatedPrompts = [...prompts, newPrompt];
|
||||||
|
|
||||||
|
setPrompts(updatedPrompts);
|
||||||
|
savePrompts(updatedPrompts);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdatePrompt = (prompt: Prompt) => {
|
||||||
|
const updatedPrompts = prompts.map((p) => {
|
||||||
|
if (p.id === prompt.id) {
|
||||||
|
return prompt;
|
||||||
|
}
|
||||||
|
|
||||||
|
return p;
|
||||||
|
});
|
||||||
|
|
||||||
|
setPrompts(updatedPrompts);
|
||||||
|
savePrompts(updatedPrompts);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeletePrompt = (prompt: Prompt) => {
|
||||||
|
const updatedPrompts = prompts.filter((p) => p.id !== prompt.id);
|
||||||
|
setPrompts(updatedPrompts);
|
||||||
|
savePrompts(updatedPrompts);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreatePromptFolder = (name: string) => {};
|
||||||
|
|
||||||
|
// EFFECTS --------------------------------------------
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (currentMessage) {
|
if (currentMessage) {
|
||||||
handleSend(currentMessage);
|
handleSend(currentMessage);
|
||||||
|
@ -459,6 +544,8 @@ const Home: React.FC<HomeProps> = ({ serverSideApiKeyIsSet }) => {
|
||||||
}
|
}
|
||||||
}, [apiKey]);
|
}, [apiKey]);
|
||||||
|
|
||||||
|
// ON LOAD --------------------------------------------
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const theme = localStorage.getItem('theme');
|
const theme = localStorage.getItem('theme');
|
||||||
if (theme) {
|
if (theme) {
|
||||||
|
@ -477,11 +564,26 @@ const Home: React.FC<HomeProps> = ({ serverSideApiKeyIsSet }) => {
|
||||||
setShowSidebar(false);
|
setShowSidebar(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const showChatbar = localStorage.getItem('showChatbar');
|
||||||
|
if (showChatbar) {
|
||||||
|
setShowSidebar(showChatbar === 'true');
|
||||||
|
}
|
||||||
|
|
||||||
|
const showPromptbar = localStorage.getItem('showPromptbar');
|
||||||
|
if (showPromptbar) {
|
||||||
|
setShowPromptbar(showPromptbar === 'true');
|
||||||
|
}
|
||||||
|
|
||||||
const folders = localStorage.getItem('folders');
|
const folders = localStorage.getItem('folders');
|
||||||
if (folders) {
|
if (folders) {
|
||||||
setFolders(JSON.parse(folders));
|
setFolders(JSON.parse(folders));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const prompts = localStorage.getItem('prompts');
|
||||||
|
if (prompts) {
|
||||||
|
setPrompts(JSON.parse(prompts));
|
||||||
|
}
|
||||||
|
|
||||||
const conversationHistory = localStorage.getItem('conversationHistory');
|
const conversationHistory = localStorage.getItem('conversationHistory');
|
||||||
if (conversationHistory) {
|
if (conversationHistory) {
|
||||||
const parsedConversationHistory: Conversation[] =
|
const parsedConversationHistory: Conversation[] =
|
||||||
|
@ -502,12 +604,12 @@ const Home: React.FC<HomeProps> = ({ serverSideApiKeyIsSet }) => {
|
||||||
setSelectedConversation(cleanedSelectedConversation);
|
setSelectedConversation(cleanedSelectedConversation);
|
||||||
} else {
|
} else {
|
||||||
setSelectedConversation({
|
setSelectedConversation({
|
||||||
id: 1,
|
id: uuidv4(),
|
||||||
name: 'New conversation',
|
name: 'New conversation',
|
||||||
messages: [],
|
messages: [],
|
||||||
model: OpenAIModels[OpenAIModelID.GPT_3_5],
|
model: OpenAIModels[OpenAIModelID.GPT_3_5],
|
||||||
prompt: DEFAULT_SYSTEM_PROMPT,
|
prompt: DEFAULT_SYSTEM_PROMPT,
|
||||||
folderId: 0,
|
folderId: null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [serverSideApiKeyIsSet]);
|
}, [serverSideApiKeyIsSet]);
|
||||||
|
@ -517,7 +619,10 @@ const Home: React.FC<HomeProps> = ({ serverSideApiKeyIsSet }) => {
|
||||||
<Head>
|
<Head>
|
||||||
<title>Chatbot UI</title>
|
<title>Chatbot UI</title>
|
||||||
<meta name="description" content="ChatGPT but better." />
|
<meta name="description" content="ChatGPT but better." />
|
||||||
<meta name="viewport" content="height=device-height ,width=device-width, initial-scale=1, user-scalable=no" />
|
<meta
|
||||||
|
name="viewport"
|
||||||
|
content="height=device-height ,width=device-width, initial-scale=1, user-scalable=no"
|
||||||
|
/>
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
</Head>
|
</Head>
|
||||||
{selectedConversation && (
|
{selectedConversation && (
|
||||||
|
@ -534,7 +639,7 @@ const Home: React.FC<HomeProps> = ({ serverSideApiKeyIsSet }) => {
|
||||||
<div className="flex h-full w-full pt-[48px] sm:pt-0">
|
<div className="flex h-full w-full pt-[48px] sm:pt-0">
|
||||||
{showSidebar ? (
|
{showSidebar ? (
|
||||||
<div>
|
<div>
|
||||||
<Sidebar
|
<Chatbar
|
||||||
loading={messageIsStreaming}
|
loading={messageIsStreaming}
|
||||||
conversations={conversations}
|
conversations={conversations}
|
||||||
lightMode={lightMode}
|
lightMode={lightMode}
|
||||||
|
@ -542,13 +647,13 @@ const Home: React.FC<HomeProps> = ({ serverSideApiKeyIsSet }) => {
|
||||||
apiKey={apiKey}
|
apiKey={apiKey}
|
||||||
folders={folders}
|
folders={folders}
|
||||||
onToggleLightMode={handleLightMode}
|
onToggleLightMode={handleLightMode}
|
||||||
onCreateFolder={handleCreateFolder}
|
onCreateFolder={(name) => handleCreateFolder(name, 'chat')}
|
||||||
onDeleteFolder={handleDeleteFolder}
|
onDeleteFolder={handleDeleteFolder}
|
||||||
onUpdateFolder={handleUpdateFolder}
|
onUpdateFolder={handleUpdateFolder}
|
||||||
onNewConversation={handleNewConversation}
|
onNewConversation={handleNewConversation}
|
||||||
onSelectConversation={handleSelectConversation}
|
onSelectConversation={handleSelectConversation}
|
||||||
onDeleteConversation={handleDeleteConversation}
|
onDeleteConversation={handleDeleteConversation}
|
||||||
onToggleSidebar={() => setShowSidebar(!showSidebar)}
|
onToggleSidebar={handleToggleChatbar}
|
||||||
onUpdateConversation={handleUpdateConversation}
|
onUpdateConversation={handleUpdateConversation}
|
||||||
onApiKeyChange={handleApiKeyChange}
|
onApiKeyChange={handleApiKeyChange}
|
||||||
onClearConversations={handleClearConversations}
|
onClearConversations={handleClearConversations}
|
||||||
|
@ -558,36 +663,67 @@ const Home: React.FC<HomeProps> = ({ serverSideApiKeyIsSet }) => {
|
||||||
|
|
||||||
<IconArrowBarLeft
|
<IconArrowBarLeft
|
||||||
className="fixed top-5 left-[270px] z-50 h-7 w-7 cursor-pointer hover:text-gray-400 dark:text-white dark:hover:text-gray-300 sm:top-0.5 sm:left-[270px] sm:h-8 sm:w-8 sm:text-neutral-700"
|
className="fixed top-5 left-[270px] z-50 h-7 w-7 cursor-pointer hover:text-gray-400 dark:text-white dark:hover:text-gray-300 sm:top-0.5 sm:left-[270px] sm:h-8 sm:w-8 sm:text-neutral-700"
|
||||||
onClick={() => setShowSidebar(!showSidebar)}
|
onClick={handleToggleChatbar}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
onClick={() => setShowSidebar(!showSidebar)}
|
onClick={handleToggleChatbar}
|
||||||
className="absolute top-0 left-0 z-10 h-full w-full bg-black opacity-70 sm:hidden"
|
className="absolute top-0 left-0 z-10 h-full w-full bg-black opacity-70 sm:hidden"
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<IconArrowBarRight
|
<IconArrowBarRight
|
||||||
className="fixed top-2.5 left-4 z-50 h-7 w-7 cursor-pointer text-white hover:text-gray-400 dark:text-white dark:hover:text-gray-300 sm:top-0.5 sm:left-4 sm:h-8 sm:w-8 sm:text-neutral-700"
|
className="fixed top-2.5 left-4 z-50 h-7 w-7 cursor-pointer text-white hover:text-gray-400 dark:text-white dark:hover:text-gray-300 sm:top-0.5 sm:left-4 sm:h-8 sm:w-8 sm:text-neutral-700"
|
||||||
onClick={() => setShowSidebar(!showSidebar)}
|
onClick={handleToggleChatbar}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<div className="flex flex-1">
|
||||||
<Chat
|
<Chat
|
||||||
conversation={selectedConversation}
|
conversation={selectedConversation}
|
||||||
messageIsStreaming={messageIsStreaming}
|
messageIsStreaming={messageIsStreaming}
|
||||||
apiKey={apiKey}
|
apiKey={apiKey}
|
||||||
serverSideApiKeyIsSet={serverSideApiKeyIsSet}
|
serverSideApiKeyIsSet={serverSideApiKeyIsSet}
|
||||||
modelError={modelError}
|
modelError={modelError}
|
||||||
messageError={messageError}
|
|
||||||
models={models}
|
models={models}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
|
prompts={prompts}
|
||||||
onSend={handleSend}
|
onSend={handleSend}
|
||||||
onUpdateConversation={handleUpdateConversation}
|
onUpdateConversation={handleUpdateConversation}
|
||||||
onEditMessage={handleEditMessage}
|
onEditMessage={handleEditMessage}
|
||||||
stopConversationRef={stopConversationRef}
|
stopConversationRef={stopConversationRef}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{showPromptbar ? (
|
||||||
|
<div>
|
||||||
|
<Promptbar
|
||||||
|
prompts={prompts}
|
||||||
|
folders={folders}
|
||||||
|
onToggleSidebar={handleTogglePromptbar}
|
||||||
|
onCreatePrompt={handleCreatePrompt}
|
||||||
|
onUpdatePrompt={handleUpdatePrompt}
|
||||||
|
onDeletePrompt={handleDeletePrompt}
|
||||||
|
onCreateFolder={(name) => handleCreateFolder(name, 'prompt')}
|
||||||
|
onDeleteFolder={handleDeleteFolder}
|
||||||
|
onUpdateFolder={handleUpdateFolder}
|
||||||
|
/>
|
||||||
|
<IconArrowBarRight
|
||||||
|
className="fixed top-5 right-[270px] z-50 h-7 w-7 cursor-pointer hover:text-gray-400 dark:text-white dark:hover:text-gray-300 sm:top-0.5 sm:right-[270px] sm:h-8 sm:w-8 sm:text-neutral-700"
|
||||||
|
onClick={handleTogglePromptbar}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
onClick={handleTogglePromptbar}
|
||||||
|
className="absolute top-0 left-0 z-10 h-full w-full bg-black opacity-70 sm:hidden"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<IconArrowBarLeft
|
||||||
|
className="fixed top-2.5 right-4 z-50 h-7 w-7 cursor-pointer text-white hover:text-gray-400 dark:text-white dark:hover:text-gray-300 sm:top-0.5 sm:right-4 sm:h-8 sm:w-8 sm:text-neutral-700"
|
||||||
|
onClick={handleTogglePromptbar}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { OpenAIModel } from './openai';
|
||||||
|
|
||||||
|
export interface Message {
|
||||||
|
role: Role;
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Role = 'assistant' | 'user';
|
||||||
|
|
||||||
|
export interface ChatBody {
|
||||||
|
model: OpenAIModel;
|
||||||
|
messages: Message[];
|
||||||
|
key: string;
|
||||||
|
prompt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Conversation {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
messages: Message[];
|
||||||
|
model: OpenAIModel;
|
||||||
|
prompt: string;
|
||||||
|
folderId: string | null;
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
export interface KeyValuePair {
|
||||||
|
key: string;
|
||||||
|
value: any;
|
||||||
|
}
|
|
@ -1,6 +0,0 @@
|
||||||
namespace NodeJS {
|
|
||||||
interface ProcessEnv {
|
|
||||||
OPENAI_API_KEY: string;
|
|
||||||
OPENAI_API_HOST?: string;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
export interface ProcessEnv {
|
||||||
|
OPENAI_API_KEY: string;
|
||||||
|
OPENAI_API_HOST?: string;
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
export interface ErrorMessage {
|
||||||
|
code: String | null;
|
||||||
|
title: String;
|
||||||
|
messageLines: String[];
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
export interface Folder {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
type: FolderType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FolderType = 'chat' | 'prompt';
|
|
@ -1,69 +1 @@
|
||||||
export interface OpenAIModel {
|
export {};
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum OpenAIModelID {
|
|
||||||
GPT_3_5 = 'gpt-3.5-turbo',
|
|
||||||
GPT_4 = 'gpt-4',
|
|
||||||
}
|
|
||||||
|
|
||||||
export const OpenAIModels: Record<OpenAIModelID, OpenAIModel> = {
|
|
||||||
[OpenAIModelID.GPT_3_5]: {
|
|
||||||
id: OpenAIModelID.GPT_3_5,
|
|
||||||
name: 'Default (GPT-3.5)',
|
|
||||||
},
|
|
||||||
[OpenAIModelID.GPT_4]: {
|
|
||||||
id: OpenAIModelID.GPT_4,
|
|
||||||
name: 'GPT-4',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface Message {
|
|
||||||
role: Role;
|
|
||||||
content: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Role = 'assistant' | 'user';
|
|
||||||
|
|
||||||
export interface ChatFolder {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Conversation {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
messages: Message[];
|
|
||||||
model: OpenAIModel;
|
|
||||||
prompt: string;
|
|
||||||
folderId: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ChatBody {
|
|
||||||
model: OpenAIModel;
|
|
||||||
messages: Message[];
|
|
||||||
key: string;
|
|
||||||
prompt: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface KeyValuePair {
|
|
||||||
key: string;
|
|
||||||
value: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
// keep track of local storage schema
|
|
||||||
export interface LocalStorage {
|
|
||||||
apiKey: string;
|
|
||||||
conversationHistory: Conversation[];
|
|
||||||
selectedConversation: Conversation;
|
|
||||||
theme: 'light' | 'dark';
|
|
||||||
// added folders (3/23/23)
|
|
||||||
folders: ChatFolder[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ErrorMessage {
|
|
||||||
code: String | null;
|
|
||||||
title: String;
|
|
||||||
messageLines: String[];
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
export interface OpenAIModel {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum OpenAIModelID {
|
||||||
|
GPT_3_5 = 'gpt-3.5-turbo',
|
||||||
|
GPT_4 = 'gpt-4',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const OpenAIModels: Record<OpenAIModelID, OpenAIModel> = {
|
||||||
|
[OpenAIModelID.GPT_3_5]: {
|
||||||
|
id: OpenAIModelID.GPT_3_5,
|
||||||
|
name: 'Default (GPT-3.5)',
|
||||||
|
},
|
||||||
|
[OpenAIModelID.GPT_4]: {
|
||||||
|
id: OpenAIModelID.GPT_4,
|
||||||
|
name: 'GPT-4',
|
||||||
|
},
|
||||||
|
};
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { OpenAIModel } from './openai';
|
||||||
|
|
||||||
|
export interface Prompt {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
content: string;
|
||||||
|
model: OpenAIModel;
|
||||||
|
folderId: string | null;
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { Conversation } from './chat';
|
||||||
|
import { Folder } from './folder';
|
||||||
|
import { Prompt } from './prompt';
|
||||||
|
|
||||||
|
// keep track of local storage schema
|
||||||
|
export interface LocalStorage {
|
||||||
|
apiKey: string;
|
||||||
|
conversationHistory: Conversation[];
|
||||||
|
selectedConversation: Conversation;
|
||||||
|
theme: 'light' | 'dark';
|
||||||
|
// added folders (3/23/23)
|
||||||
|
folders: Folder[];
|
||||||
|
// added prompts (3/26/23)
|
||||||
|
prompts: Prompt[];
|
||||||
|
// added showChatbar and showPromptbar (3/26/23)
|
||||||
|
showChatbar: boolean;
|
||||||
|
showPromptbar: boolean;
|
||||||
|
}
|
|
@ -1,10 +1,12 @@
|
||||||
import { Conversation, OpenAIModelID, OpenAIModels } from '@/types';
|
import { Conversation } from '@/types/chat';
|
||||||
|
import { OpenAIModelID, OpenAIModels } from '@/types/openai';
|
||||||
import { DEFAULT_SYSTEM_PROMPT } from './const';
|
import { DEFAULT_SYSTEM_PROMPT } from './const';
|
||||||
|
|
||||||
export const cleanSelectedConversation = (conversation: Conversation) => {
|
export const cleanSelectedConversation = (conversation: Conversation) => {
|
||||||
// added model for each conversation (3/20/23)
|
// added model for each conversation (3/20/23)
|
||||||
// added system prompt for each conversation (3/21/23)
|
// added system prompt for each conversation (3/21/23)
|
||||||
// added folders (3/23/23)
|
// added folders (3/23/23)
|
||||||
|
// added prompts (3/26/23)
|
||||||
|
|
||||||
let updatedConversation = conversation;
|
let updatedConversation = conversation;
|
||||||
|
|
||||||
|
@ -27,7 +29,7 @@ export const cleanSelectedConversation = (conversation: Conversation) => {
|
||||||
if (!updatedConversation.folderId) {
|
if (!updatedConversation.folderId) {
|
||||||
updatedConversation = {
|
updatedConversation = {
|
||||||
...updatedConversation,
|
...updatedConversation,
|
||||||
folderId: updatedConversation.folderId || 0,
|
folderId: updatedConversation.folderId || null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,6 +40,7 @@ export const cleanConversationHistory = (history: Conversation[]) => {
|
||||||
// added model for each conversation (3/20/23)
|
// added model for each conversation (3/20/23)
|
||||||
// added system prompt for each conversation (3/21/23)
|
// added system prompt for each conversation (3/21/23)
|
||||||
// added folders (3/23/23)
|
// added folders (3/23/23)
|
||||||
|
// added prompts (3/26/23)
|
||||||
|
|
||||||
return history.reduce((acc: Conversation[], conversation) => {
|
return history.reduce((acc: Conversation[], conversation) => {
|
||||||
try {
|
try {
|
||||||
|
@ -50,7 +53,7 @@ export const cleanConversationHistory = (history: Conversation[]) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!conversation.folderId) {
|
if (!conversation.folderId) {
|
||||||
conversation.folderId = 0;
|
conversation.folderId = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
acc.push(conversation);
|
acc.push(conversation);
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Conversation } from '@/types';
|
import { Conversation } from '@/types/chat';
|
||||||
|
|
||||||
export const updateConversation = (
|
export const updateConversation = (
|
||||||
updatedConversation: Conversation,
|
updatedConversation: Conversation,
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { ChatFolder } from '@/types';
|
import { Folder } from '@/types/folder';
|
||||||
|
|
||||||
export const saveFolders = (folders: ChatFolder[]) => {
|
export const saveFolders = (folders: Folder[]) => {
|
||||||
localStorage.setItem('folders', JSON.stringify(folders));
|
localStorage.setItem('folders', JSON.stringify(folders));
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { ChatFolder, Conversation } from '@/types';
|
import { Conversation } from '@/types/chat';
|
||||||
|
import { Folder } from '@/types/folder';
|
||||||
|
|
||||||
function currentDate() {
|
function currentDate() {
|
||||||
const date = new Date();
|
const date = new Date();
|
||||||
|
@ -40,7 +41,7 @@ export const exportData = () => {
|
||||||
|
|
||||||
export const importData = (
|
export const importData = (
|
||||||
conversations: Conversation[],
|
conversations: Conversation[],
|
||||||
folders: ChatFolder[],
|
folders: Folder[],
|
||||||
) => {
|
) => {
|
||||||
localStorage.setItem('conversationHistory', JSON.stringify(conversations));
|
localStorage.setItem('conversationHistory', JSON.stringify(conversations));
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { Prompt } from '@/types/prompt';
|
||||||
|
|
||||||
|
export const updatePrompt = (updatedPrompt: Prompt, allPrompts: Prompt[]) => {
|
||||||
|
const updatedPrompts = allPrompts.map((c) => {
|
||||||
|
if (c.id === updatedPrompt.id) {
|
||||||
|
return updatedPrompt;
|
||||||
|
}
|
||||||
|
|
||||||
|
return c;
|
||||||
|
});
|
||||||
|
|
||||||
|
savePrompts(updatedPrompts);
|
||||||
|
|
||||||
|
return {
|
||||||
|
single: updatedPrompt,
|
||||||
|
all: updatedPrompts,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const savePrompts = (prompts: Prompt[]) => {
|
||||||
|
localStorage.setItem('prompts', JSON.stringify(prompts));
|
||||||
|
};
|
|
@ -1,4 +1,5 @@
|
||||||
import { Message, OpenAIModel } from '@/types';
|
import { Message } from '@/types/chat';
|
||||||
|
import { OpenAIModel } from '@/types/openai';
|
||||||
import {
|
import {
|
||||||
createParser,
|
createParser,
|
||||||
ParsedEvent,
|
ParsedEvent,
|
||||||
|
|
Loading…
Reference in New Issue