parent
1a4b4401ee
commit
f5118e3037
|
@ -11,6 +11,7 @@ interface Props {
|
|||
conversation: Conversation;
|
||||
models: OpenAIModel[];
|
||||
apiKey: string;
|
||||
isUsingEnv: boolean;
|
||||
messageIsStreaming: boolean;
|
||||
modelError: boolean;
|
||||
messageError: boolean;
|
||||
|
@ -18,10 +19,11 @@ interface Props {
|
|||
lightMode: "light" | "dark";
|
||||
onSend: (message: Message, isResend: boolean) => void;
|
||||
onUpdateConversation: (conversation: Conversation, data: KeyValuePair) => void;
|
||||
onAcceptEnv: (accept: boolean) => void;
|
||||
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 [autoScrollEnabled, setAutoScrollEnabled] = useState(true);
|
||||
|
||||
|
@ -29,7 +31,6 @@ export const Chat: FC<Props> = ({ conversation, models, apiKey, messageIsStreami
|
|||
const chatContainerRef = useRef<HTMLDivElement>(null);
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
|
||||
const scrollToBottom = () => {
|
||||
if (autoScrollEnabled) {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||
|
@ -51,8 +52,7 @@ export const Chat: FC<Props> = ({ conversation, models, apiKey, messageIsStreami
|
|||
|
||||
useEffect(() => {
|
||||
scrollToBottom();
|
||||
textareaRef.current?.focus()
|
||||
|
||||
textareaRef.current?.focus();
|
||||
}, [conversation.messages]);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -69,10 +69,17 @@ export const Chat: FC<Props> = ({ conversation, models, apiKey, messageIsStreami
|
|||
|
||||
return (
|
||||
<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="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">- 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>
|
||||
) : modelError ? (
|
||||
<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 { FC, useState } from "react";
|
||||
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 { IconCheck, IconMessage, IconPencil, IconTrash, IconX } from "@tabler/icons-react";
|
||||
import { FC, KeyboardEvent, useEffect, useState } from "react";
|
||||
import { Conversation, KeyValuePair } from "@/types";
|
||||
import { FC } from "react";
|
||||
import { ConversationComponent } from "./Conversation";
|
||||
|
||||
interface Props {
|
||||
loading: boolean;
|
||||
|
@ -8,116 +8,22 @@ interface Props {
|
|||
selectedConversation: Conversation;
|
||||
onSelectConversation: (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 }) => {
|
||||
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]);
|
||||
|
||||
export const Conversations: FC<Props> = ({ loading, conversations, selectedConversation, onSelectConversation, onDeleteConversation, onUpdateConversation }) => {
|
||||
return (
|
||||
<div className="flex flex-col-reverse gap-1 w-full pt-2">
|
||||
{conversations.map((conversation, index) => (
|
||||
<button
|
||||
<ConversationComponent
|
||||
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" : ""}`}
|
||||
onClick={() => onSelectConversation(conversation)}
|
||||
disabled={loading}
|
||||
>
|
||||
<IconMessage
|
||||
className=""
|
||||
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>
|
||||
selectedConversation={selectedConversation}
|
||||
conversation={conversation}
|
||||
loading={loading}
|
||||
onSelectConversation={onSelectConversation}
|
||||
onDeleteConversation={onDeleteConversation}
|
||||
onUpdateConversation={onUpdateConversation}
|
||||
/>
|
||||
))}
|
||||
</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 { IconArrowBarLeft, IconPlus } from "@tabler/icons-react";
|
||||
import { ChatFolder, Conversation, KeyValuePair } from "@/types";
|
||||
import { IconArrowBarLeft, IconFolderPlus, IconPlus } from "@tabler/icons-react";
|
||||
import { FC, useEffect, useState } from "react";
|
||||
import { Conversations } from "./Conversations";
|
||||
import { Folders } from "./Folders";
|
||||
import { Search } from "./Search";
|
||||
import { SidebarSettings } from "./SidebarSettings";
|
||||
|
||||
|
@ -11,6 +12,10 @@ interface Props {
|
|||
lightMode: "light" | "dark";
|
||||
selectedConversation: Conversation;
|
||||
apiKey: string;
|
||||
folders: ChatFolder[];
|
||||
onCreateFolder: (name: string) => void;
|
||||
onDeleteFolder: (folderId: number) => void;
|
||||
onUpdateFolder: (folderId: number, name: string) => void;
|
||||
onNewConversation: () => void;
|
||||
onToggleLightMode: (mode: "light" | "dark") => void;
|
||||
onSelectConversation: (conversation: Conversation) => void;
|
||||
|
@ -23,17 +28,49 @@ interface Props {
|
|||
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 [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(() => {
|
||||
if (searchTerm) {
|
||||
setFilteredConversations(conversations.filter((conversation) => {
|
||||
const searchable = conversation.name.toLocaleLowerCase() + ' ' + conversation.messages.map((message) => message.content).join(" ");
|
||||
return searchable.toLowerCase().includes(searchTerm.toLowerCase());
|
||||
}
|
||||
));
|
||||
setFilteredConversations(
|
||||
conversations.filter((conversation) => {
|
||||
const searchable = conversation.name.toLocaleLowerCase() + " " + conversation.messages.map((message) => message.content).join(" ");
|
||||
return searchable.toLowerCase().includes(searchTerm.toLowerCase());
|
||||
})
|
||||
);
|
||||
} else {
|
||||
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`}>
|
||||
<header className="flex items-center">
|
||||
<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={() => {
|
||||
onNewConversation();
|
||||
setSearchTerm("");
|
||||
}}
|
||||
>
|
||||
<IconPlus
|
||||
className=""
|
||||
size={16}
|
||||
/>
|
||||
<IconPlus size={16} />
|
||||
New chat
|
||||
</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
|
||||
className="ml-1 p-1 text-neutral-300 cursor-pointer hover:text-neutral-400 hidden sm:flex"
|
||||
size={32}
|
||||
|
@ -71,20 +112,45 @@ export const Sidebar: FC<Props> = ({ loading, conversations, lightMode, selected
|
|||
)}
|
||||
|
||||
<div className="flex-grow overflow-auto">
|
||||
<Conversations
|
||||
loading={loading}
|
||||
conversations={filteredConversations}
|
||||
selectedConversation={selectedConversation}
|
||||
onSelectConversation={onSelectConversation}
|
||||
onDeleteConversation={(conversation) => {
|
||||
onDeleteConversation(conversation);
|
||||
setSearchTerm("");
|
||||
}}
|
||||
onRenameConversation={(conversation, name) => {
|
||||
onUpdateConversation(conversation, { key: "name", value: name });
|
||||
setSearchTerm("");
|
||||
}}
|
||||
/>
|
||||
{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
|
||||
loading={loading}
|
||||
conversations={filteredConversations.filter((conversation) => conversation.folderId === 0)}
|
||||
selectedConversation={selectedConversation}
|
||||
onSelectConversation={onSelectConversation}
|
||||
onDeleteConversation={handleDeleteConversation}
|
||||
onUpdateConversation={handleUpdateConversation}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-4 text-white text-center">
|
||||
<div>No conversations.</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<SidebarSettings
|
||||
|
|
|
@ -1,16 +1,18 @@
|
|||
import { Chat } from "@/components/Chat/Chat";
|
||||
import { Navbar } from "@/components/Mobile/Navbar";
|
||||
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 { DEFAULT_SYSTEM_PROMPT } from "@/utils/app/const";
|
||||
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 Head from "next/head";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
export default function Home() {
|
||||
const [folders, setFolders] = useState<ChatFolder[]>([]);
|
||||
const [conversations, setConversations] = useState<Conversation[]>([]);
|
||||
const [selectedConversation, setSelectedConversation] = useState<Conversation>();
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
|
@ -21,6 +23,8 @@ export default function Home() {
|
|||
const [apiKey, setApiKey] = useState<string>("");
|
||||
const [messageError, setMessageError] = useState<boolean>(false);
|
||||
const [modelError, setModelError] = useState<boolean>(false);
|
||||
const [isUsingEnv, setIsUsingEnv] = useState<boolean>(false);
|
||||
|
||||
const stopConversationRef = useRef<boolean>(false);
|
||||
|
||||
const handleSend = async (message: Message, isResend: boolean) => {
|
||||
|
@ -201,6 +205,11 @@ export default function Home() {
|
|||
localStorage.setItem("apiKey", apiKey);
|
||||
};
|
||||
|
||||
const handleEnvChange = (isUsingEnv: boolean) => {
|
||||
setIsUsingEnv(isUsingEnv);
|
||||
localStorage.setItem("isUsingEnv", isUsingEnv.toString());
|
||||
};
|
||||
|
||||
const handleExportConversations = () => {
|
||||
exportConversations();
|
||||
};
|
||||
|
@ -216,6 +225,55 @@ export default function Home() {
|
|||
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 lastConversation = conversations[conversations.length - 1];
|
||||
|
||||
|
@ -224,7 +282,8 @@ export default function Home() {
|
|||
name: `Conversation ${lastConversation ? lastConversation.id + 1 : 1}`,
|
||||
messages: [],
|
||||
model: OpenAIModels[OpenAIModelID.GPT_3_5],
|
||||
prompt: DEFAULT_SYSTEM_PROMPT
|
||||
prompt: DEFAULT_SYSTEM_PROMPT,
|
||||
folderId: 0
|
||||
};
|
||||
|
||||
const updatedConversations = [...conversations, newConversation];
|
||||
|
@ -252,7 +311,8 @@ export default function Home() {
|
|||
name: "New conversation",
|
||||
messages: [],
|
||||
model: OpenAIModels[OpenAIModelID.GPT_3_5],
|
||||
prompt: DEFAULT_SYSTEM_PROMPT
|
||||
prompt: DEFAULT_SYSTEM_PROMPT,
|
||||
folderId: 0
|
||||
});
|
||||
localStorage.removeItem("selectedConversation");
|
||||
}
|
||||
|
@ -279,9 +339,16 @@ export default function Home() {
|
|||
name: "New conversation",
|
||||
messages: [],
|
||||
model: OpenAIModels[OpenAIModelID.GPT_3_5],
|
||||
prompt: DEFAULT_SYSTEM_PROMPT
|
||||
prompt: DEFAULT_SYSTEM_PROMPT,
|
||||
folderId: 0
|
||||
});
|
||||
localStorage.removeItem("selectedConversation");
|
||||
|
||||
setFolders([]);
|
||||
localStorage.removeItem("folders");
|
||||
|
||||
setIsUsingEnv(false);
|
||||
localStorage.removeItem("isUsingEnv");
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -308,10 +375,21 @@ export default function Home() {
|
|||
fetchModels(apiKey);
|
||||
}
|
||||
|
||||
const usingEnv = localStorage.getItem("isUsingEnv");
|
||||
if (usingEnv) {
|
||||
setIsUsingEnv(usingEnv === "true");
|
||||
fetchModels("");
|
||||
}
|
||||
|
||||
if (window.innerWidth < 640) {
|
||||
setShowSidebar(false);
|
||||
}
|
||||
|
||||
const folders = localStorage.getItem("folders");
|
||||
if (folders) {
|
||||
setFolders(JSON.parse(folders));
|
||||
}
|
||||
|
||||
const conversationHistory = localStorage.getItem("conversationHistory");
|
||||
if (conversationHistory) {
|
||||
const parsedConversationHistory: Conversation[] = JSON.parse(conversationHistory);
|
||||
|
@ -330,7 +408,8 @@ export default function Home() {
|
|||
name: "New conversation",
|
||||
messages: [],
|
||||
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}
|
||||
selectedConversation={selectedConversation}
|
||||
apiKey={apiKey}
|
||||
folders={folders}
|
||||
onToggleLightMode={handleLightMode}
|
||||
onCreateFolder={handleCreateFolder}
|
||||
onDeleteFolder={handleDeleteFolder}
|
||||
onUpdateFolder={handleUpdateFolder}
|
||||
onNewConversation={handleNewConversation}
|
||||
onSelectConversation={handleSelectConversation}
|
||||
onDeleteConversation={handleDeleteConversation}
|
||||
|
@ -398,6 +481,7 @@ export default function Home() {
|
|||
conversation={selectedConversation}
|
||||
messageIsStreaming={messageIsStreaming}
|
||||
apiKey={apiKey}
|
||||
isUsingEnv={isUsingEnv}
|
||||
modelError={modelError}
|
||||
messageError={messageError}
|
||||
models={models}
|
||||
|
@ -405,6 +489,7 @@ export default function Home() {
|
|||
lightMode={lightMode}
|
||||
onSend={handleSend}
|
||||
onUpdateConversation={handleUpdateConversation}
|
||||
onAcceptEnv={handleEnvChange}
|
||||
stopConversationRef={stopConversationRef}
|
||||
/>
|
||||
</article>
|
||||
|
|
|
@ -26,12 +26,18 @@ export interface Message {
|
|||
|
||||
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 {
|
||||
|
@ -52,4 +58,6 @@ export interface LocalStorage {
|
|||
conversationHistory: Conversation[];
|
||||
selectedConversation: Conversation;
|
||||
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) => {
|
||||
// added model for each conversation (3/20/23)
|
||||
// added system prompt for each conversation (3/21/23)
|
||||
// added folders (3/23/23)
|
||||
|
||||
let updatedConversation = conversation;
|
||||
|
||||
|
@ -11,7 +12,7 @@ export const cleanSelectedConversation = (conversation: Conversation) => {
|
|||
if (!updatedConversation.model) {
|
||||
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) {
|
||||
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[]) => {
|
||||
// added model for each conversation (3/20/23)
|
||||
// added system prompt for each conversation (3/21/23)
|
||||
// added folders (3/23/23)
|
||||
|
||||
let updatedHistory = [...history];
|
||||
|
||||
// check for model on each conversation
|
||||
if (!updatedHistory.every((conversation) => conversation.model)) {
|
||||
updatedHistory = updatedHistory.map((conversation) => ({
|
||||
...conversation,
|
||||
model: OpenAIModels[OpenAIModelID.GPT_3_5]
|
||||
}));
|
||||
}
|
||||
updatedHistory.forEach((conversation) => {
|
||||
if (!conversation.model) {
|
||||
conversation.model = OpenAIModels[OpenAIModelID.GPT_3_5];
|
||||
}
|
||||
|
||||
// check for system prompt on each conversation
|
||||
if (!updatedHistory.every((conversation) => conversation.prompt)) {
|
||||
updatedHistory = updatedHistory.map((conversation) => ({
|
||||
...conversation,
|
||||
systemPrompt: DEFAULT_SYSTEM_PROMPT
|
||||
}));
|
||||
}
|
||||
if (!conversation.prompt) {
|
||||
conversation.prompt = DEFAULT_SYSTEM_PROMPT;
|
||||
}
|
||||
|
||||
if (!conversation.folderId) {
|
||||
conversation.folderId = 0;
|
||||
}
|
||||
});
|
||||
|
||||
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