parent
1a4b4401ee
commit
f5118e3037
|
@ -11,6 +11,7 @@ interface Props {
|
||||||
conversation: Conversation;
|
conversation: Conversation;
|
||||||
models: OpenAIModel[];
|
models: OpenAIModel[];
|
||||||
apiKey: string;
|
apiKey: string;
|
||||||
|
isUsingEnv: boolean;
|
||||||
messageIsStreaming: boolean;
|
messageIsStreaming: boolean;
|
||||||
modelError: boolean;
|
modelError: boolean;
|
||||||
messageError: boolean;
|
messageError: boolean;
|
||||||
|
@ -18,10 +19,11 @@ interface Props {
|
||||||
lightMode: "light" | "dark";
|
lightMode: "light" | "dark";
|
||||||
onSend: (message: Message, isResend: boolean) => void;
|
onSend: (message: Message, isResend: boolean) => void;
|
||||||
onUpdateConversation: (conversation: Conversation, data: KeyValuePair) => void;
|
onUpdateConversation: (conversation: Conversation, data: KeyValuePair) => void;
|
||||||
|
onAcceptEnv: (accept: boolean) => void;
|
||||||
stopConversationRef: MutableRefObject<boolean>;
|
stopConversationRef: MutableRefObject<boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Chat: FC<Props> = ({ conversation, models, apiKey, messageIsStreaming, modelError, messageError, loading, lightMode, onSend, onUpdateConversation, stopConversationRef }) => {
|
export const Chat: FC<Props> = ({ conversation, models, apiKey, isUsingEnv, messageIsStreaming, modelError, messageError, loading, lightMode, onSend, onUpdateConversation, onAcceptEnv, stopConversationRef }) => {
|
||||||
const [currentMessage, setCurrentMessage] = useState<Message>();
|
const [currentMessage, setCurrentMessage] = useState<Message>();
|
||||||
const [autoScrollEnabled, setAutoScrollEnabled] = useState(true);
|
const [autoScrollEnabled, setAutoScrollEnabled] = useState(true);
|
||||||
|
|
||||||
|
@ -29,7 +31,6 @@ export const Chat: FC<Props> = ({ conversation, models, apiKey, messageIsStreami
|
||||||
const chatContainerRef = useRef<HTMLDivElement>(null);
|
const chatContainerRef = useRef<HTMLDivElement>(null);
|
||||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
|
|
||||||
|
|
||||||
const scrollToBottom = () => {
|
const scrollToBottom = () => {
|
||||||
if (autoScrollEnabled) {
|
if (autoScrollEnabled) {
|
||||||
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||||
|
@ -51,8 +52,7 @@ export const Chat: FC<Props> = ({ conversation, models, apiKey, messageIsStreami
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
textareaRef.current?.focus()
|
textareaRef.current?.focus();
|
||||||
|
|
||||||
}, [conversation.messages]);
|
}, [conversation.messages]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -69,10 +69,17 @@ export const Chat: FC<Props> = ({ conversation, models, apiKey, messageIsStreami
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative flex-1 overflow-none dark:bg-[#343541] bg-white">
|
<div className="relative flex-1 overflow-none dark:bg-[#343541] bg-white">
|
||||||
{!apiKey ? (
|
{!apiKey && !isUsingEnv ? (
|
||||||
<div className="flex flex-col justify-center mx-auto h-full w-[300px] sm:w-[500px] space-y-6">
|
<div className="flex flex-col justify-center mx-auto h-full w-[300px] sm:w-[500px] space-y-6">
|
||||||
<div className="text-2xl font-semibold text-center text-gray-800 dark:text-gray-100">OpenAI API Key Required</div>
|
<div className="text-2xl font-semibold text-center text-gray-800 dark:text-gray-100">OpenAI API Key Required</div>
|
||||||
<div className="text-center text-gray-500 dark:text-gray-400">Please set your OpenAI API key in the bottom left of the sidebar.</div>
|
<div className="text-center text-gray-500 dark:text-gray-400">Please set your OpenAI API key in the bottom left of the sidebar.</div>
|
||||||
|
<div className="text-center text-gray-500 dark:text-gray-400">- OR -</div>
|
||||||
|
<button
|
||||||
|
className="flex items-center justify-center mx-auto px-4 py-2 border border-transparent text-xs rounded-md text-white bg-gray-600 hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500"
|
||||||
|
onClick={() => onAcceptEnv(true)}
|
||||||
|
>
|
||||||
|
click if using a .env.local file
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
) : modelError ? (
|
) : modelError ? (
|
||||||
<div className="flex flex-col justify-center mx-auto h-full w-[300px] sm:w-[500px] space-y-6">
|
<div className="flex flex-col justify-center mx-auto h-full w-[300px] sm:w-[500px] space-y-6">
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { generateRandomString, programmingLanguages } from "@/utils/app/data";
|
import { generateRandomString, programmingLanguages } from "@/utils/app/codeblock";
|
||||||
import { IconDownload } from "@tabler/icons-react";
|
import { IconDownload } from "@tabler/icons-react";
|
||||||
import { FC, useState } from "react";
|
import { FC, useState } from "react";
|
||||||
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
|
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
|
||||||
|
|
|
@ -0,0 +1,124 @@
|
||||||
|
import { Conversation, KeyValuePair } from "@/types";
|
||||||
|
import { IconCheck, IconMessage, IconPencil, IconTrash, IconX } from "@tabler/icons-react";
|
||||||
|
import { DragEvent, FC, KeyboardEvent, useEffect, useState } from "react";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
selectedConversation: Conversation;
|
||||||
|
conversation: Conversation;
|
||||||
|
loading: boolean;
|
||||||
|
onSelectConversation: (conversation: Conversation) => void;
|
||||||
|
onDeleteConversation: (conversation: Conversation) => void;
|
||||||
|
onUpdateConversation: (conversation: Conversation, data: KeyValuePair) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ConversationComponent: FC<Props> = ({ selectedConversation, conversation, loading, onSelectConversation, onDeleteConversation, onUpdateConversation }) => {
|
||||||
|
const [isDeleting, setIsDeleting] = useState(false);
|
||||||
|
const [isRenaming, setIsRenaming] = useState(false);
|
||||||
|
const [renameValue, setRenameValue] = useState("");
|
||||||
|
|
||||||
|
const handleEnterDown = (e: KeyboardEvent<HTMLDivElement>) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
e.preventDefault();
|
||||||
|
handleRename(selectedConversation);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDragStart = (e: DragEvent<HTMLButtonElement>, conversation: Conversation) => {
|
||||||
|
if (e.dataTransfer) {
|
||||||
|
e.dataTransfer.setData("conversation", JSON.stringify(conversation));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRename = (conversation: Conversation) => {
|
||||||
|
onUpdateConversation(conversation, { key: "name", value: renameValue });
|
||||||
|
setRenameValue("");
|
||||||
|
setIsRenaming(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isRenaming) {
|
||||||
|
setIsDeleting(false);
|
||||||
|
} else if (isDeleting) {
|
||||||
|
setIsRenaming(false);
|
||||||
|
}
|
||||||
|
}, [isRenaming, isDeleting]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className={`flex w-full gap-3 items-center p-3 text-sm rounded-lg hover:bg-[#343541]/90 transition-colors duration-200 cursor-pointer ${loading ? "disabled:cursor-not-allowed" : ""} ${selectedConversation.id === conversation.id ? "bg-[#343541]/90" : ""}`}
|
||||||
|
onClick={() => onSelectConversation(conversation)}
|
||||||
|
disabled={loading}
|
||||||
|
draggable="true"
|
||||||
|
onDragStart={(e) => handleDragStart(e, conversation)}
|
||||||
|
>
|
||||||
|
<IconMessage size={16} />
|
||||||
|
|
||||||
|
{isRenaming && selectedConversation.id === conversation.id ? (
|
||||||
|
<input
|
||||||
|
className="flex-1 bg-transparent border-b border-neutral-400 focus:border-neutral-100 text-left overflow-hidden overflow-ellipsis pr-1 outline-none text-white"
|
||||||
|
type="text"
|
||||||
|
value={renameValue}
|
||||||
|
onChange={(e) => setRenameValue(e.target.value)}
|
||||||
|
onKeyDown={handleEnterDown}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="overflow-hidden whitespace-nowrap overflow-ellipsis pr-1 flex-1 text-left">{conversation.name}</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{(isDeleting || isRenaming) && selectedConversation.id === conversation.id && (
|
||||||
|
<div className="flex gap-1 -ml-2">
|
||||||
|
<IconCheck
|
||||||
|
className="min-w-[20px] text-neutral-400 hover:text-neutral-100"
|
||||||
|
size={16}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
if (isDeleting) {
|
||||||
|
onDeleteConversation(conversation);
|
||||||
|
} else if (isRenaming) {
|
||||||
|
handleRename(conversation);
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{selectedConversation.id === conversation.id && !isDeleting && !isRenaming && (
|
||||||
|
<div className="flex gap-1 -ml-2">
|
||||||
|
<IconPencil
|
||||||
|
className="min-w-[20px] text-neutral-400 hover:text-neutral-100"
|
||||||
|
size={18}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setIsRenaming(true);
|
||||||
|
setRenameValue(selectedConversation.name);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<IconTrash
|
||||||
|
className=" min-w-[20px] text-neutral-400 hover:text-neutral-100"
|
||||||
|
size={18}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setIsDeleting(true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,6 +1,6 @@
|
||||||
import { Conversation } from "@/types";
|
import { Conversation, KeyValuePair } from "@/types";
|
||||||
import { IconCheck, IconMessage, IconPencil, IconTrash, IconX } from "@tabler/icons-react";
|
import { FC } from "react";
|
||||||
import { FC, KeyboardEvent, useEffect, useState } from "react";
|
import { ConversationComponent } from "./Conversation";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
|
@ -8,116 +8,22 @@ interface Props {
|
||||||
selectedConversation: Conversation;
|
selectedConversation: Conversation;
|
||||||
onSelectConversation: (conversation: Conversation) => void;
|
onSelectConversation: (conversation: Conversation) => void;
|
||||||
onDeleteConversation: (conversation: Conversation) => void;
|
onDeleteConversation: (conversation: Conversation) => void;
|
||||||
onRenameConversation: (conversation: Conversation, name: string) => void;
|
onUpdateConversation: (conversation: Conversation, data: KeyValuePair) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Conversations: FC<Props> = ({ loading, conversations, selectedConversation, onSelectConversation, onDeleteConversation, onRenameConversation }) => {
|
export const Conversations: FC<Props> = ({ loading, conversations, selectedConversation, onSelectConversation, onDeleteConversation, onUpdateConversation }) => {
|
||||||
const [isDeleting, setIsDeleting] = useState(false);
|
|
||||||
const [isRenaming, setIsRenaming] = useState(false);
|
|
||||||
const [renameValue, setRenameValue] = useState("");
|
|
||||||
|
|
||||||
const handleEnterDown = (e: KeyboardEvent<HTMLDivElement>) => {
|
|
||||||
if (e.key === "Enter") {
|
|
||||||
e.preventDefault();
|
|
||||||
handleRename(selectedConversation);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRename = (conversation: Conversation) => {
|
|
||||||
onRenameConversation(conversation, renameValue);
|
|
||||||
setRenameValue("");
|
|
||||||
setIsRenaming(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isRenaming) {
|
|
||||||
setIsDeleting(false);
|
|
||||||
} else if (isDeleting) {
|
|
||||||
setIsRenaming(false);
|
|
||||||
}
|
|
||||||
}, [isRenaming, isDeleting]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col-reverse gap-1 w-full pt-2">
|
<div className="flex flex-col-reverse gap-1 w-full pt-2">
|
||||||
{conversations.map((conversation, index) => (
|
{conversations.map((conversation, index) => (
|
||||||
<button
|
<ConversationComponent
|
||||||
key={index}
|
key={index}
|
||||||
className={`flex gap-3 items-center p-3 text-sm rounded-lg hover:bg-[#343541]/90 transition-colors duration-200 cursor-pointer ${loading ? "disabled:cursor-not-allowed" : ""} ${selectedConversation.id === conversation.id ? "bg-[#343541]/90" : ""}`}
|
selectedConversation={selectedConversation}
|
||||||
onClick={() => onSelectConversation(conversation)}
|
conversation={conversation}
|
||||||
disabled={loading}
|
loading={loading}
|
||||||
>
|
onSelectConversation={onSelectConversation}
|
||||||
<IconMessage
|
onDeleteConversation={onDeleteConversation}
|
||||||
className=""
|
onUpdateConversation={onUpdateConversation}
|
||||||
size={16}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{isRenaming && selectedConversation.id === conversation.id ? (
|
|
||||||
<input
|
|
||||||
className="flex-1 bg-transparent border-b border-neutral-400 focus:border-neutral-100 text-left overflow-hidden overflow-ellipsis pr-1 outline-none text-white"
|
|
||||||
type="text"
|
|
||||||
value={renameValue}
|
|
||||||
onChange={(e) => setRenameValue(e.target.value)}
|
|
||||||
onKeyDown={handleEnterDown}
|
|
||||||
autoFocus
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className="overflow-hidden whitespace-nowrap overflow-ellipsis pr-1 flex-1 text-left">{conversation.name}</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{(isDeleting || isRenaming) && selectedConversation.id === conversation.id && (
|
|
||||||
<div className="flex gap-1 -ml-2">
|
|
||||||
<IconCheck
|
|
||||||
className="min-w-[20px] text-neutral-400 hover:text-neutral-100"
|
|
||||||
size={16}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
if (isDeleting) {
|
|
||||||
onDeleteConversation(conversation);
|
|
||||||
} else if (isRenaming) {
|
|
||||||
handleRename(conversation);
|
|
||||||
}
|
|
||||||
|
|
||||||
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>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{selectedConversation.id === conversation.id && !isDeleting && !isRenaming && (
|
|
||||||
<div className="flex gap-1 -ml-2">
|
|
||||||
<IconPencil
|
|
||||||
className="min-w-[20px] text-neutral-400 hover:text-neutral-100"
|
|
||||||
size={18}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
setIsRenaming(true);
|
|
||||||
setRenameValue(selectedConversation.name);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<IconTrash
|
|
||||||
className=" min-w-[20px] text-neutral-400 hover:text-neutral-100"
|
|
||||||
size={18}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
setIsDeleting(true);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -0,0 +1,193 @@
|
||||||
|
import { ChatFolder, Conversation, KeyValuePair } from "@/types";
|
||||||
|
import { IconCaretDown, IconCaretRight, IconCheck, IconPencil, IconTrash, IconX } from "@tabler/icons-react";
|
||||||
|
import { FC, KeyboardEvent, useEffect, useState } from "react";
|
||||||
|
import { ConversationComponent } from "./Conversation";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
searchTerm: string;
|
||||||
|
conversations: Conversation[];
|
||||||
|
currentFolder: ChatFolder;
|
||||||
|
onDeleteFolder: (folder: number) => void;
|
||||||
|
onUpdateFolder: (folder: number, name: string) => void;
|
||||||
|
// conversation props
|
||||||
|
selectedConversation: Conversation;
|
||||||
|
loading: boolean;
|
||||||
|
onSelectConversation: (conversation: Conversation) => void;
|
||||||
|
onDeleteConversation: (conversation: Conversation) => void;
|
||||||
|
onUpdateConversation: (conversation: Conversation, data: KeyValuePair) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Folder: FC<Props> = ({
|
||||||
|
searchTerm,
|
||||||
|
conversations,
|
||||||
|
currentFolder,
|
||||||
|
onDeleteFolder,
|
||||||
|
onUpdateFolder,
|
||||||
|
// conversation props
|
||||||
|
selectedConversation,
|
||||||
|
loading,
|
||||||
|
onSelectConversation,
|
||||||
|
onDeleteConversation,
|
||||||
|
onUpdateConversation
|
||||||
|
}) => {
|
||||||
|
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: ChatFolder) => {
|
||||||
|
if (e.dataTransfer) {
|
||||||
|
setIsOpen(true);
|
||||||
|
|
||||||
|
const conversation = JSON.parse(e.dataTransfer.getData("conversation"));
|
||||||
|
onUpdateConversation(conversation, { key: "folderId", value: folder.id });
|
||||||
|
|
||||||
|
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 gap-3 items-center px-3 py-2 text-sm rounded-lg hover:bg-[#343541]/90 transition-colors duration-200 cursor-pointer`}
|
||||||
|
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 bg-transparent border-b border-neutral-400 focus:border-neutral-100 text-left overflow-hidden overflow-ellipsis pr-1 outline-none text-white"
|
||||||
|
type="text"
|
||||||
|
value={renameValue}
|
||||||
|
onChange={(e) => setRenameValue(e.target.value)}
|
||||||
|
onKeyDown={handleEnterDown}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="overflow-hidden whitespace-nowrap overflow-ellipsis pr-1 flex-1 text-left">{currentFolder.name}</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{(isDeleting || isRenaming) && (
|
||||||
|
<div className="flex gap-1 -ml-2">
|
||||||
|
<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="flex gap-1 ml-2">
|
||||||
|
<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
|
||||||
|
? conversations.map((conversation, index) => {
|
||||||
|
if (conversation.folderId === currentFolder.id) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="ml-5 pl-2 border-l gap-2 pt-2"
|
||||||
|
>
|
||||||
|
<ConversationComponent
|
||||||
|
selectedConversation={selectedConversation}
|
||||||
|
conversation={conversation}
|
||||||
|
loading={loading}
|
||||||
|
onSelectConversation={onSelectConversation}
|
||||||
|
onDeleteConversation={onDeleteConversation}
|
||||||
|
onUpdateConversation={onUpdateConversation}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
: null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,52 @@
|
||||||
|
import { ChatFolder, Conversation, KeyValuePair } from "@/types";
|
||||||
|
import { FC } from "react";
|
||||||
|
import { Folder } from "./Folder";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
searchTerm: string;
|
||||||
|
conversations: Conversation[];
|
||||||
|
folders: ChatFolder[];
|
||||||
|
onDeleteFolder: (folder: number) => void;
|
||||||
|
onUpdateFolder: (folder: number, name: string) => void;
|
||||||
|
// conversation props
|
||||||
|
selectedConversation: Conversation;
|
||||||
|
loading: boolean;
|
||||||
|
onSelectConversation: (conversation: Conversation) => void;
|
||||||
|
onDeleteConversation: (conversation: Conversation) => void;
|
||||||
|
onUpdateConversation: (conversation: Conversation, data: KeyValuePair) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Folders: FC<Props> = ({
|
||||||
|
searchTerm,
|
||||||
|
conversations,
|
||||||
|
folders,
|
||||||
|
onDeleteFolder,
|
||||||
|
onUpdateFolder,
|
||||||
|
// conversation props
|
||||||
|
selectedConversation,
|
||||||
|
loading,
|
||||||
|
onSelectConversation,
|
||||||
|
onDeleteConversation,
|
||||||
|
onUpdateConversation
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-1 w-full pt-2">
|
||||||
|
{folders.map((folder, index) => (
|
||||||
|
<Folder
|
||||||
|
key={index}
|
||||||
|
searchTerm={searchTerm}
|
||||||
|
conversations={conversations.filter((c) => c.folderId)}
|
||||||
|
currentFolder={folder}
|
||||||
|
onDeleteFolder={onDeleteFolder}
|
||||||
|
onUpdateFolder={onUpdateFolder}
|
||||||
|
// conversation props
|
||||||
|
selectedConversation={selectedConversation}
|
||||||
|
loading={loading}
|
||||||
|
onSelectConversation={onSelectConversation}
|
||||||
|
onDeleteConversation={onDeleteConversation}
|
||||||
|
onUpdateConversation={onUpdateConversation}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,7 +1,8 @@
|
||||||
import { Conversation, KeyValuePair } from "@/types";
|
import { ChatFolder, Conversation, KeyValuePair } from "@/types";
|
||||||
import { IconArrowBarLeft, IconPlus } from "@tabler/icons-react";
|
import { IconArrowBarLeft, IconFolderPlus, IconPlus } from "@tabler/icons-react";
|
||||||
import { FC, useEffect, useState } from "react";
|
import { FC, useEffect, useState } from "react";
|
||||||
import { Conversations } from "./Conversations";
|
import { Conversations } from "./Conversations";
|
||||||
|
import { Folders } from "./Folders";
|
||||||
import { Search } from "./Search";
|
import { Search } from "./Search";
|
||||||
import { SidebarSettings } from "./SidebarSettings";
|
import { SidebarSettings } from "./SidebarSettings";
|
||||||
|
|
||||||
|
@ -11,6 +12,10 @@ interface Props {
|
||||||
lightMode: "light" | "dark";
|
lightMode: "light" | "dark";
|
||||||
selectedConversation: Conversation;
|
selectedConversation: Conversation;
|
||||||
apiKey: string;
|
apiKey: string;
|
||||||
|
folders: ChatFolder[];
|
||||||
|
onCreateFolder: (name: string) => void;
|
||||||
|
onDeleteFolder: (folderId: number) => void;
|
||||||
|
onUpdateFolder: (folderId: number, 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;
|
||||||
|
@ -23,17 +28,49 @@ interface Props {
|
||||||
onImportConversations: (conversations: Conversation[]) => void;
|
onImportConversations: (conversations: Conversation[]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Sidebar: FC<Props> = ({ loading, conversations, lightMode, selectedConversation, apiKey, onNewConversation, onToggleLightMode, onSelectConversation, onDeleteConversation, onToggleSidebar, onUpdateConversation, onApiKeyChange, onClearConversations, onExportConversations, onImportConversations }) => {
|
export const Sidebar: FC<Props> = ({ loading, conversations, lightMode, selectedConversation, apiKey, folders, onCreateFolder, onDeleteFolder, onUpdateFolder, onNewConversation, onToggleLightMode, onSelectConversation, onDeleteConversation, onToggleSidebar, onUpdateConversation, onApiKeyChange, onClearConversations, onExportConversations, onImportConversations }) => {
|
||||||
const [searchTerm, setSearchTerm] = useState<string>("");
|
const [searchTerm, setSearchTerm] = useState<string>("");
|
||||||
const [filteredConversations, setFilteredConversations] = useState<Conversation[]>(conversations);
|
const [filteredConversations, setFilteredConversations] = useState<Conversation[]>(conversations);
|
||||||
|
|
||||||
|
const handleUpdateConversation = (conversation: Conversation, data: KeyValuePair) => {
|
||||||
|
onUpdateConversation(conversation, data);
|
||||||
|
setSearchTerm("");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteConversation = (conversation: Conversation) => {
|
||||||
|
onDeleteConversation(conversation);
|
||||||
|
setSearchTerm("");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDrop = (e: any) => {
|
||||||
|
if (e.dataTransfer) {
|
||||||
|
const conversation = JSON.parse(e.dataTransfer.getData("conversation"));
|
||||||
|
onUpdateConversation(conversation, { key: "folderId", value: 0 });
|
||||||
|
|
||||||
|
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(() => {
|
useEffect(() => {
|
||||||
if (searchTerm) {
|
if (searchTerm) {
|
||||||
setFilteredConversations(conversations.filter((conversation) => {
|
setFilteredConversations(
|
||||||
const searchable = conversation.name.toLocaleLowerCase() + ' ' + conversation.messages.map((message) => message.content).join(" ");
|
conversations.filter((conversation) => {
|
||||||
|
const searchable = conversation.name.toLocaleLowerCase() + " " + conversation.messages.map((message) => message.content).join(" ");
|
||||||
return searchable.toLowerCase().includes(searchTerm.toLowerCase());
|
return searchable.toLowerCase().includes(searchTerm.toLowerCase());
|
||||||
}
|
})
|
||||||
));
|
);
|
||||||
} else {
|
} else {
|
||||||
setFilteredConversations(conversations);
|
setFilteredConversations(conversations);
|
||||||
}
|
}
|
||||||
|
@ -43,19 +80,23 @@ export const Sidebar: FC<Props> = ({ loading, conversations, lightMode, selected
|
||||||
<aside className={`h-full flex flex-none space-y-2 p-2 flex-col bg-[#202123] w-[260px] z-10 sm:relative sm:top-0 absolute top-12 bottom-0`}>
|
<aside className={`h-full flex flex-none space-y-2 p-2 flex-col bg-[#202123] w-[260px] z-10 sm:relative sm:top-0 absolute top-12 bottom-0`}>
|
||||||
<header className="flex items-center">
|
<header className="flex items-center">
|
||||||
<button
|
<button
|
||||||
className="flex gap-3 p-3 items-center w-full sm:w-[200px] rounded-md hover:bg-gray-500/10 transition-colors duration-200 text-white cursor-pointer text-sm flex-shrink-0 border border-white/20"
|
className="flex gap-3 p-3 items-center w-[190px] rounded-md hover:bg-gray-500/10 transition-colors duration-200 text-white cursor-pointer text-sm flex-shrink-0 border border-white/20"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onNewConversation();
|
onNewConversation();
|
||||||
setSearchTerm("");
|
setSearchTerm("");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<IconPlus
|
<IconPlus size={16} />
|
||||||
className=""
|
|
||||||
size={16}
|
|
||||||
/>
|
|
||||||
New chat
|
New chat
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className="ml-2 flex gap-3 p-3 items-center rounded-md hover:bg-gray-500/10 transition-colors duration-200 text-white cursor-pointer text-sm flex-shrink-0 border border-white/20"
|
||||||
|
onClick={() => onCreateFolder("New folder")}
|
||||||
|
>
|
||||||
|
<IconFolderPlus size={16} />
|
||||||
|
</button>
|
||||||
|
|
||||||
<IconArrowBarLeft
|
<IconArrowBarLeft
|
||||||
className="ml-1 p-1 text-neutral-300 cursor-pointer hover:text-neutral-400 hidden sm:flex"
|
className="ml-1 p-1 text-neutral-300 cursor-pointer hover:text-neutral-400 hidden sm:flex"
|
||||||
size={32}
|
size={32}
|
||||||
|
@ -71,21 +112,46 @@ export const Sidebar: FC<Props> = ({ loading, conversations, lightMode, selected
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex-grow overflow-auto">
|
<div className="flex-grow overflow-auto">
|
||||||
|
{folders.length > 0 && (
|
||||||
|
<div className="flex border-b border-white/20 pb-2">
|
||||||
|
<Folders
|
||||||
|
searchTerm={searchTerm}
|
||||||
|
conversations={filteredConversations.filter((conversation) => conversation.folderId !== 0)}
|
||||||
|
folders={folders}
|
||||||
|
onDeleteFolder={onDeleteFolder}
|
||||||
|
onUpdateFolder={onUpdateFolder}
|
||||||
|
selectedConversation={selectedConversation}
|
||||||
|
loading={loading}
|
||||||
|
onSelectConversation={onSelectConversation}
|
||||||
|
onDeleteConversation={handleDeleteConversation}
|
||||||
|
onUpdateConversation={handleUpdateConversation}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{conversations.length > 0 ? (
|
||||||
|
<div
|
||||||
|
className="pt-2 h-full"
|
||||||
|
onDrop={(e) => handleDrop(e)}
|
||||||
|
onDragOver={allowDrop}
|
||||||
|
onDragEnter={highlightDrop}
|
||||||
|
onDragLeave={removeHighlight}
|
||||||
|
>
|
||||||
<Conversations
|
<Conversations
|
||||||
loading={loading}
|
loading={loading}
|
||||||
conversations={filteredConversations}
|
conversations={filteredConversations.filter((conversation) => conversation.folderId === 0)}
|
||||||
selectedConversation={selectedConversation}
|
selectedConversation={selectedConversation}
|
||||||
onSelectConversation={onSelectConversation}
|
onSelectConversation={onSelectConversation}
|
||||||
onDeleteConversation={(conversation) => {
|
onDeleteConversation={handleDeleteConversation}
|
||||||
onDeleteConversation(conversation);
|
onUpdateConversation={handleUpdateConversation}
|
||||||
setSearchTerm("");
|
|
||||||
}}
|
|
||||||
onRenameConversation={(conversation, name) => {
|
|
||||||
onUpdateConversation(conversation, { key: "name", value: name });
|
|
||||||
setSearchTerm("");
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="mt-4 text-white text-center">
|
||||||
|
<div>No conversations.</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<SidebarSettings
|
<SidebarSettings
|
||||||
lightMode={lightMode}
|
lightMode={lightMode}
|
||||||
|
|
|
@ -1,16 +1,18 @@
|
||||||
import { Chat } from "@/components/Chat/Chat";
|
import { Chat } from "@/components/Chat/Chat";
|
||||||
import { Navbar } from "@/components/Mobile/Navbar";
|
import { Navbar } from "@/components/Mobile/Navbar";
|
||||||
import { Sidebar } from "@/components/Sidebar/Sidebar";
|
import { Sidebar } from "@/components/Sidebar/Sidebar";
|
||||||
import { ChatBody, Conversation, KeyValuePair, Message, OpenAIModel, OpenAIModelID, OpenAIModels } from "@/types";
|
import { ChatBody, ChatFolder, Conversation, KeyValuePair, Message, OpenAIModel, OpenAIModelID, OpenAIModels } from "@/types";
|
||||||
import { cleanConversationHistory, cleanSelectedConversation } from "@/utils/app/clean";
|
import { cleanConversationHistory, cleanSelectedConversation } from "@/utils/app/clean";
|
||||||
import { DEFAULT_SYSTEM_PROMPT } from "@/utils/app/const";
|
import { DEFAULT_SYSTEM_PROMPT } from "@/utils/app/const";
|
||||||
import { saveConversation, saveConversations, updateConversation } from "@/utils/app/conversation";
|
import { saveConversation, saveConversations, updateConversation } from "@/utils/app/conversation";
|
||||||
import { exportConversations, importConversations } from "@/utils/app/data";
|
import { saveFolders } from "@/utils/app/folders";
|
||||||
|
import { exportConversations, importConversations } from "@/utils/app/importExport";
|
||||||
import { IconArrowBarLeft, IconArrowBarRight } from "@tabler/icons-react";
|
import { IconArrowBarLeft, IconArrowBarRight } from "@tabler/icons-react";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
|
const [folders, setFolders] = useState<ChatFolder[]>([]);
|
||||||
const [conversations, setConversations] = useState<Conversation[]>([]);
|
const [conversations, setConversations] = useState<Conversation[]>([]);
|
||||||
const [selectedConversation, setSelectedConversation] = useState<Conversation>();
|
const [selectedConversation, setSelectedConversation] = useState<Conversation>();
|
||||||
const [loading, setLoading] = useState<boolean>(false);
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
|
@ -21,6 +23,8 @@ export default function Home() {
|
||||||
const [apiKey, setApiKey] = useState<string>("");
|
const [apiKey, setApiKey] = useState<string>("");
|
||||||
const [messageError, setMessageError] = useState<boolean>(false);
|
const [messageError, setMessageError] = useState<boolean>(false);
|
||||||
const [modelError, setModelError] = useState<boolean>(false);
|
const [modelError, setModelError] = useState<boolean>(false);
|
||||||
|
const [isUsingEnv, setIsUsingEnv] = useState<boolean>(false);
|
||||||
|
|
||||||
const stopConversationRef = useRef<boolean>(false);
|
const stopConversationRef = useRef<boolean>(false);
|
||||||
|
|
||||||
const handleSend = async (message: Message, isResend: boolean) => {
|
const handleSend = async (message: Message, isResend: boolean) => {
|
||||||
|
@ -201,6 +205,11 @@ export default function Home() {
|
||||||
localStorage.setItem("apiKey", apiKey);
|
localStorage.setItem("apiKey", apiKey);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleEnvChange = (isUsingEnv: boolean) => {
|
||||||
|
setIsUsingEnv(isUsingEnv);
|
||||||
|
localStorage.setItem("isUsingEnv", isUsingEnv.toString());
|
||||||
|
};
|
||||||
|
|
||||||
const handleExportConversations = () => {
|
const handleExportConversations = () => {
|
||||||
exportConversations();
|
exportConversations();
|
||||||
};
|
};
|
||||||
|
@ -216,6 +225,55 @@ export default function Home() {
|
||||||
saveConversation(conversation);
|
saveConversation(conversation);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleCreateFolder = (name: string) => {
|
||||||
|
const lastFolder = folders[folders.length - 1];
|
||||||
|
|
||||||
|
const newFolder: ChatFolder = {
|
||||||
|
id: lastFolder ? lastFolder.id + 1 : 1,
|
||||||
|
name
|
||||||
|
};
|
||||||
|
|
||||||
|
const updatedFolders = [...folders, newFolder];
|
||||||
|
|
||||||
|
setFolders(updatedFolders);
|
||||||
|
saveFolders(updatedFolders);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteFolder = (folderId: number) => {
|
||||||
|
const updatedFolders = folders.filter((f) => f.id !== folderId);
|
||||||
|
setFolders(updatedFolders);
|
||||||
|
saveFolders(updatedFolders);
|
||||||
|
|
||||||
|
const updatedConversations: Conversation[] = conversations.map((c) => {
|
||||||
|
if (c.folderId === folderId) {
|
||||||
|
return {
|
||||||
|
...c,
|
||||||
|
folderId: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return c;
|
||||||
|
});
|
||||||
|
setConversations(updatedConversations);
|
||||||
|
saveConversations(updatedConversations);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdateFolder = (folderId: number, name: string) => {
|
||||||
|
const updatedFolders = folders.map((f) => {
|
||||||
|
if (f.id === folderId) {
|
||||||
|
return {
|
||||||
|
...f,
|
||||||
|
name
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return f;
|
||||||
|
});
|
||||||
|
|
||||||
|
setFolders(updatedFolders);
|
||||||
|
saveFolders(updatedFolders);
|
||||||
|
};
|
||||||
|
|
||||||
const handleNewConversation = () => {
|
const handleNewConversation = () => {
|
||||||
const lastConversation = conversations[conversations.length - 1];
|
const lastConversation = conversations[conversations.length - 1];
|
||||||
|
|
||||||
|
@ -224,7 +282,8 @@ export default function Home() {
|
||||||
name: `Conversation ${lastConversation ? lastConversation.id + 1 : 1}`,
|
name: `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
|
||||||
};
|
};
|
||||||
|
|
||||||
const updatedConversations = [...conversations, newConversation];
|
const updatedConversations = [...conversations, newConversation];
|
||||||
|
@ -252,7 +311,8 @@ export default function Home() {
|
||||||
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
|
||||||
});
|
});
|
||||||
localStorage.removeItem("selectedConversation");
|
localStorage.removeItem("selectedConversation");
|
||||||
}
|
}
|
||||||
|
@ -279,9 +339,16 @@ export default function Home() {
|
||||||
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
|
||||||
});
|
});
|
||||||
localStorage.removeItem("selectedConversation");
|
localStorage.removeItem("selectedConversation");
|
||||||
|
|
||||||
|
setFolders([]);
|
||||||
|
localStorage.removeItem("folders");
|
||||||
|
|
||||||
|
setIsUsingEnv(false);
|
||||||
|
localStorage.removeItem("isUsingEnv");
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -308,10 +375,21 @@ export default function Home() {
|
||||||
fetchModels(apiKey);
|
fetchModels(apiKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const usingEnv = localStorage.getItem("isUsingEnv");
|
||||||
|
if (usingEnv) {
|
||||||
|
setIsUsingEnv(usingEnv === "true");
|
||||||
|
fetchModels("");
|
||||||
|
}
|
||||||
|
|
||||||
if (window.innerWidth < 640) {
|
if (window.innerWidth < 640) {
|
||||||
setShowSidebar(false);
|
setShowSidebar(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const folders = localStorage.getItem("folders");
|
||||||
|
if (folders) {
|
||||||
|
setFolders(JSON.parse(folders));
|
||||||
|
}
|
||||||
|
|
||||||
const conversationHistory = localStorage.getItem("conversationHistory");
|
const conversationHistory = localStorage.getItem("conversationHistory");
|
||||||
if (conversationHistory) {
|
if (conversationHistory) {
|
||||||
const parsedConversationHistory: Conversation[] = JSON.parse(conversationHistory);
|
const parsedConversationHistory: Conversation[] = JSON.parse(conversationHistory);
|
||||||
|
@ -330,7 +408,8 @@ export default function Home() {
|
||||||
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
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
@ -370,7 +449,11 @@ export default function Home() {
|
||||||
lightMode={lightMode}
|
lightMode={lightMode}
|
||||||
selectedConversation={selectedConversation}
|
selectedConversation={selectedConversation}
|
||||||
apiKey={apiKey}
|
apiKey={apiKey}
|
||||||
|
folders={folders}
|
||||||
onToggleLightMode={handleLightMode}
|
onToggleLightMode={handleLightMode}
|
||||||
|
onCreateFolder={handleCreateFolder}
|
||||||
|
onDeleteFolder={handleDeleteFolder}
|
||||||
|
onUpdateFolder={handleUpdateFolder}
|
||||||
onNewConversation={handleNewConversation}
|
onNewConversation={handleNewConversation}
|
||||||
onSelectConversation={handleSelectConversation}
|
onSelectConversation={handleSelectConversation}
|
||||||
onDeleteConversation={handleDeleteConversation}
|
onDeleteConversation={handleDeleteConversation}
|
||||||
|
@ -398,6 +481,7 @@ export default function Home() {
|
||||||
conversation={selectedConversation}
|
conversation={selectedConversation}
|
||||||
messageIsStreaming={messageIsStreaming}
|
messageIsStreaming={messageIsStreaming}
|
||||||
apiKey={apiKey}
|
apiKey={apiKey}
|
||||||
|
isUsingEnv={isUsingEnv}
|
||||||
modelError={modelError}
|
modelError={modelError}
|
||||||
messageError={messageError}
|
messageError={messageError}
|
||||||
models={models}
|
models={models}
|
||||||
|
@ -405,6 +489,7 @@ export default function Home() {
|
||||||
lightMode={lightMode}
|
lightMode={lightMode}
|
||||||
onSend={handleSend}
|
onSend={handleSend}
|
||||||
onUpdateConversation={handleUpdateConversation}
|
onUpdateConversation={handleUpdateConversation}
|
||||||
|
onAcceptEnv={handleEnvChange}
|
||||||
stopConversationRef={stopConversationRef}
|
stopConversationRef={stopConversationRef}
|
||||||
/>
|
/>
|
||||||
</article>
|
</article>
|
||||||
|
|
|
@ -26,12 +26,18 @@ export interface Message {
|
||||||
|
|
||||||
export type Role = "assistant" | "user";
|
export type Role = "assistant" | "user";
|
||||||
|
|
||||||
|
export interface ChatFolder {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Conversation {
|
export interface Conversation {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
messages: Message[];
|
messages: Message[];
|
||||||
model: OpenAIModel;
|
model: OpenAIModel;
|
||||||
prompt: string;
|
prompt: string;
|
||||||
|
folderId: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ChatBody {
|
export interface ChatBody {
|
||||||
|
@ -52,4 +58,6 @@ export interface LocalStorage {
|
||||||
conversationHistory: Conversation[];
|
conversationHistory: Conversation[];
|
||||||
selectedConversation: Conversation;
|
selectedConversation: Conversation;
|
||||||
theme: "light" | "dark";
|
theme: "light" | "dark";
|
||||||
|
// added folders (3/23/23)
|
||||||
|
folders: ChatFolder[];
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ 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)
|
||||||
|
|
||||||
let updatedConversation = conversation;
|
let updatedConversation = conversation;
|
||||||
|
|
||||||
|
@ -11,7 +12,7 @@ export const cleanSelectedConversation = (conversation: Conversation) => {
|
||||||
if (!updatedConversation.model) {
|
if (!updatedConversation.model) {
|
||||||
updatedConversation = {
|
updatedConversation = {
|
||||||
...updatedConversation,
|
...updatedConversation,
|
||||||
model: OpenAIModels[OpenAIModelID.GPT_3_5]
|
model: updatedConversation.model || OpenAIModels[OpenAIModelID.GPT_3_5]
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,7 +20,14 @@ export const cleanSelectedConversation = (conversation: Conversation) => {
|
||||||
if (!updatedConversation.prompt) {
|
if (!updatedConversation.prompt) {
|
||||||
updatedConversation = {
|
updatedConversation = {
|
||||||
...updatedConversation,
|
...updatedConversation,
|
||||||
prompt: DEFAULT_SYSTEM_PROMPT
|
prompt: updatedConversation.prompt || DEFAULT_SYSTEM_PROMPT
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!updatedConversation.folderId) {
|
||||||
|
updatedConversation = {
|
||||||
|
...updatedConversation,
|
||||||
|
folderId: updatedConversation.folderId || 0
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,24 +37,23 @@ export const cleanSelectedConversation = (conversation: Conversation) => {
|
||||||
export const cleanConversationHistory = (history: Conversation[]) => {
|
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)
|
||||||
|
|
||||||
let updatedHistory = [...history];
|
let updatedHistory = [...history];
|
||||||
|
|
||||||
// check for model on each conversation
|
updatedHistory.forEach((conversation) => {
|
||||||
if (!updatedHistory.every((conversation) => conversation.model)) {
|
if (!conversation.model) {
|
||||||
updatedHistory = updatedHistory.map((conversation) => ({
|
conversation.model = OpenAIModels[OpenAIModelID.GPT_3_5];
|
||||||
...conversation,
|
|
||||||
model: OpenAIModels[OpenAIModelID.GPT_3_5]
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// check for system prompt on each conversation
|
if (!conversation.prompt) {
|
||||||
if (!updatedHistory.every((conversation) => conversation.prompt)) {
|
conversation.prompt = DEFAULT_SYSTEM_PROMPT;
|
||||||
updatedHistory = updatedHistory.map((conversation) => ({
|
|
||||||
...conversation,
|
|
||||||
systemPrompt: DEFAULT_SYSTEM_PROMPT
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!conversation.folderId) {
|
||||||
|
conversation.folderId = 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return updatedHistory;
|
return updatedHistory;
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
interface languageMap {
|
||||||
|
[key: string]: string | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const programmingLanguages: languageMap = {
|
||||||
|
javascript: ".js",
|
||||||
|
python: ".py",
|
||||||
|
java: ".java",
|
||||||
|
c: ".c",
|
||||||
|
cpp: ".cpp",
|
||||||
|
"c++": ".cpp",
|
||||||
|
"c#": ".cs",
|
||||||
|
ruby: ".rb",
|
||||||
|
php: ".php",
|
||||||
|
swift: ".swift",
|
||||||
|
"objective-c": ".m",
|
||||||
|
kotlin: ".kt",
|
||||||
|
typescript: ".ts",
|
||||||
|
go: ".go",
|
||||||
|
perl: ".pl",
|
||||||
|
rust: ".rs",
|
||||||
|
scala: ".scala",
|
||||||
|
haskell: ".hs",
|
||||||
|
lua: ".lua",
|
||||||
|
shell: ".sh",
|
||||||
|
sql: ".sql",
|
||||||
|
html: ".html",
|
||||||
|
css: ".css"
|
||||||
|
// add more file extensions here, make sure the key is same as language prop in CodeBlock.tsx component
|
||||||
|
};
|
||||||
|
|
||||||
|
export const generateRandomString = (length: Number, lowercase = false) => {
|
||||||
|
const chars = "ABCDEFGHJKLMNPQRSTUVWXY3456789"; // excluding similar looking characters like Z, 2, I, 1, O, 0
|
||||||
|
let result = "";
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||||
|
}
|
||||||
|
return lowercase ? result.toLowerCase() : result;
|
||||||
|
};
|
|
@ -1,64 +0,0 @@
|
||||||
import { Conversation } from "@/types";
|
|
||||||
|
|
||||||
export const exportConversations = () => {
|
|
||||||
const history = localStorage.getItem("conversationHistory");
|
|
||||||
|
|
||||||
if (!history) return;
|
|
||||||
|
|
||||||
const blob = new Blob([history], { type: "application/json" });
|
|
||||||
const url = URL.createObjectURL(blob);
|
|
||||||
const link = document.createElement("a");
|
|
||||||
link.download = "chatbot_ui_history.json";
|
|
||||||
link.href = url;
|
|
||||||
link.style.display = "none";
|
|
||||||
document.body.appendChild(link);
|
|
||||||
link.click();
|
|
||||||
document.body.removeChild(link);
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const importConversations = (conversations: Conversation[]) => {
|
|
||||||
localStorage.setItem("conversationHistory", JSON.stringify(conversations));
|
|
||||||
localStorage.setItem("selectedConversation", JSON.stringify(conversations[conversations.length - 1]));
|
|
||||||
};
|
|
||||||
|
|
||||||
interface languageMap {
|
|
||||||
[key: string]: string | undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
export const programmingLanguages: languageMap = {
|
|
||||||
'javascript': '.js',
|
|
||||||
'python': '.py',
|
|
||||||
'java': '.java',
|
|
||||||
'c': '.c',
|
|
||||||
'cpp': '.cpp',
|
|
||||||
'c++': '.cpp',
|
|
||||||
'c#': '.cs',
|
|
||||||
'ruby': '.rb',
|
|
||||||
'php': '.php',
|
|
||||||
'swift': '.swift',
|
|
||||||
'objective-c': '.m',
|
|
||||||
'kotlin': '.kt',
|
|
||||||
'typescript': '.ts',
|
|
||||||
'go': '.go',
|
|
||||||
'perl': '.pl',
|
|
||||||
'rust': '.rs',
|
|
||||||
'scala': '.scala',
|
|
||||||
'haskell': '.hs',
|
|
||||||
'lua': '.lua',
|
|
||||||
'shell': '.sh',
|
|
||||||
'sql': '.sql',
|
|
||||||
'html': '.html',
|
|
||||||
'css': '.css'
|
|
||||||
// add more file extensions here, make sure the key is same as language prop in CodeBlock.tsx component
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
export const generateRandomString = (length: Number, lowercase=false) => {
|
|
||||||
const chars = "ABCDEFGHJKLMNPQRSTUVWXY3456789"; // excluding similar looking characters like Z, 2, I, 1, O, 0
|
|
||||||
let result = "";
|
|
||||||
for (let i = 0; i < length; i++) {
|
|
||||||
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
||||||
}
|
|
||||||
return lowercase ? result.toLowerCase() : result;
|
|
||||||
}
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { ChatFolder } from "@/types";
|
||||||
|
|
||||||
|
export const saveFolders = (folders: ChatFolder[]) => {
|
||||||
|
localStorage.setItem("folders", JSON.stringify(folders));
|
||||||
|
};
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { Conversation } from "@/types";
|
||||||
|
|
||||||
|
export const exportConversations = () => {
|
||||||
|
const history = localStorage.getItem("conversationHistory");
|
||||||
|
|
||||||
|
if (!history) return;
|
||||||
|
|
||||||
|
const blob = new Blob([history], { type: "application/json" });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.download = "chatbot_ui_history.json";
|
||||||
|
link.href = url;
|
||||||
|
link.style.display = "none";
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const importConversations = (conversations: Conversation[]) => {
|
||||||
|
localStorage.setItem("conversationHistory", JSON.stringify(conversations));
|
||||||
|
localStorage.setItem("selectedConversation", JSON.stringify(conversations[conversations.length - 1]));
|
||||||
|
};
|
Loading…
Reference in New Issue