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