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