add import/export (#71)
This commit is contained in:
		
							parent
							
								
									972a5aff23
								
							
						
					
					
						commit
						f0c575b40d
					
				|  | @ -0,0 +1,34 @@ | ||||||
|  | import { Conversation } from "@/types"; | ||||||
|  | import { IconFileImport } from "@tabler/icons-react"; | ||||||
|  | import { FC } from "react"; | ||||||
|  | 
 | ||||||
|  | interface Props { | ||||||
|  |   onImport: (conversations: Conversation[]) => void; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export const Import: FC<Props> = ({ onImport }) => { | ||||||
|  |   return ( | ||||||
|  |     <div className="flex py-3 px-3 gap-3 rounded-md hover:bg-gray-500/10 transition-colors duration-200 text-white cursor-pointer w-full items-center"> | ||||||
|  |       <input | ||||||
|  |         className="opacity-0 absolute w-[200px] cursor-pointer" | ||||||
|  |         type="file" | ||||||
|  |         accept=".json" | ||||||
|  |         onChange={(e) => { | ||||||
|  |           if (!e.target.files?.length) return; | ||||||
|  | 
 | ||||||
|  |           const file = e.target.files[0]; | ||||||
|  |           const reader = new FileReader(); | ||||||
|  |           reader.onload = (e) => { | ||||||
|  |             const conversations: Conversation[] = JSON.parse(e.target?.result as string); | ||||||
|  |             onImport(conversations); | ||||||
|  |           }; | ||||||
|  |           reader.readAsText(file); | ||||||
|  |         }} | ||||||
|  |       /> | ||||||
|  |       <div className="flex items-center gap-3 text-left"> | ||||||
|  |         <IconFileImport size={16} /> | ||||||
|  |         <div>Import conversations</div> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
|  | @ -19,9 +19,11 @@ interface Props { | ||||||
|   onUpdateConversation: (conversation: Conversation, data: KeyValuePair) => void; |   onUpdateConversation: (conversation: Conversation, data: KeyValuePair) => void; | ||||||
|   onApiKeyChange: (apiKey: string) => void; |   onApiKeyChange: (apiKey: string) => void; | ||||||
|   onClearConversations: () => void; |   onClearConversations: () => void; | ||||||
|  |   onExportConversations: () => void; | ||||||
|  |   onImportConversations: (conversations: Conversation[]) => void; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export const Sidebar: FC<Props> = ({ loading, conversations, lightMode, selectedConversation, apiKey, onNewConversation, onToggleLightMode, onSelectConversation, onDeleteConversation, onToggleSidebar, onUpdateConversation, onApiKeyChange, onClearConversations }) => { | export const Sidebar: FC<Props> = ({ loading, conversations, lightMode, selectedConversation, apiKey, 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); | ||||||
| 
 | 
 | ||||||
|  | @ -34,7 +36,7 @@ export const Sidebar: FC<Props> = ({ loading, conversations, lightMode, selected | ||||||
|   }, [searchTerm, conversations]); |   }, [searchTerm, conversations]); | ||||||
| 
 | 
 | ||||||
|   return ( |   return ( | ||||||
|     <div className={`h-full flex flex-col 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`}> |     <div 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`}> | ||||||
|       <div className="flex items-center"> |       <div 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-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" | ||||||
|  | @ -87,6 +89,8 @@ export const Sidebar: FC<Props> = ({ loading, conversations, lightMode, selected | ||||||
|         onToggleLightMode={onToggleLightMode} |         onToggleLightMode={onToggleLightMode} | ||||||
|         onApiKeyChange={onApiKeyChange} |         onApiKeyChange={onApiKeyChange} | ||||||
|         onClearConversations={onClearConversations} |         onClearConversations={onClearConversations} | ||||||
|  |         onExportConversations={onExportConversations} | ||||||
|  |         onImportConversations={onImportConversations} | ||||||
|       /> |       /> | ||||||
|     </div> |     </div> | ||||||
|   ); |   ); | ||||||
|  |  | ||||||
|  | @ -12,7 +12,7 @@ export const SidebarButton: FC<Props> = ({ text, icon, onClick }) => { | ||||||
|       className="flex py-3 px-3 gap-3 rounded-md hover:bg-gray-500/10 transition-colors duration-200 text-white cursor-pointer w-full items-center" |       className="flex py-3 px-3 gap-3 rounded-md hover:bg-gray-500/10 transition-colors duration-200 text-white cursor-pointer w-full items-center" | ||||||
|       onClick={onClick} |       onClick={onClick} | ||||||
|     > |     > | ||||||
|       <div className="">{icon}</div> |       <div>{icon}</div> | ||||||
|       <div>{text}</div> |       <div>{text}</div> | ||||||
|     </div> |     </div> | ||||||
|   ); |   ); | ||||||
|  |  | ||||||
|  | @ -1,6 +1,8 @@ | ||||||
| import { IconMoon, IconSun } from "@tabler/icons-react"; | import { Conversation } from "@/types"; | ||||||
|  | import { IconFileExport, IconMoon, IconSun } from "@tabler/icons-react"; | ||||||
| import { FC } from "react"; | import { FC } from "react"; | ||||||
| import { ClearConversations } from "./ClearConversations"; | import { ClearConversations } from "./ClearConversations"; | ||||||
|  | import { Import } from "./Import"; | ||||||
| import { Key } from "./Key"; | import { Key } from "./Key"; | ||||||
| import { SidebarButton } from "./SidebarButton"; | import { SidebarButton } from "./SidebarButton"; | ||||||
| 
 | 
 | ||||||
|  | @ -10,18 +12,29 @@ interface Props { | ||||||
|   onToggleLightMode: (mode: "light" | "dark") => void; |   onToggleLightMode: (mode: "light" | "dark") => void; | ||||||
|   onApiKeyChange: (apiKey: string) => void; |   onApiKeyChange: (apiKey: string) => void; | ||||||
|   onClearConversations: () => void; |   onClearConversations: () => void; | ||||||
|  |   onExportConversations: () => void; | ||||||
|  |   onImportConversations: (conversations: Conversation[]) => void; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export const SidebarSettings: FC<Props> = ({ lightMode, apiKey, onToggleLightMode, onApiKeyChange, onClearConversations }) => { | export const SidebarSettings: FC<Props> = ({ lightMode, apiKey, onToggleLightMode, onApiKeyChange, onClearConversations, onExportConversations, onImportConversations }) => { | ||||||
|   return ( |   return ( | ||||||
|     <div className="flex flex-col pt-1 items-center border-t border-white/20 text-sm space-y-1"> |     <div className="flex flex-col pt-1 items-center border-t border-white/20 text-sm space-y-1"> | ||||||
|       <ClearConversations onClearConversations={onClearConversations} /> |       <ClearConversations onClearConversations={onClearConversations} /> | ||||||
| 
 | 
 | ||||||
|  |       <Import onImport={onImportConversations} /> | ||||||
|  | 
 | ||||||
|  |       <SidebarButton | ||||||
|  |         text="Export conversations" | ||||||
|  |         icon={<IconFileExport size={16} />} | ||||||
|  |         onClick={() => onExportConversations()} | ||||||
|  |       /> | ||||||
|  | 
 | ||||||
|       <SidebarButton |       <SidebarButton | ||||||
|         text={lightMode === "light" ? "Dark mode" : "Light mode"} |         text={lightMode === "light" ? "Dark mode" : "Light mode"} | ||||||
|         icon={lightMode === "light" ? <IconMoon size={16} /> : <IconSun size={16} />} |         icon={lightMode === "light" ? <IconMoon size={16} /> : <IconSun size={16} />} | ||||||
|         onClick={() => onToggleLightMode(lightMode === "light" ? "dark" : "light")} |         onClick={() => onToggleLightMode(lightMode === "light" ? "dark" : "light")} | ||||||
|       /> |       /> | ||||||
|  | 
 | ||||||
|       <Key |       <Key | ||||||
|         apiKey={apiKey} |         apiKey={apiKey} | ||||||
|         onApiKeyChange={onApiKeyChange} |         onApiKeyChange={onApiKeyChange} | ||||||
|  |  | ||||||
|  | @ -5,6 +5,7 @@ import { ChatBody, Conversation, KeyValuePair, Message, OpenAIModel, OpenAIModel | ||||||
| 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 { 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, useState } from "react"; | import { useEffect, useState } from "react"; | ||||||
|  | @ -181,6 +182,16 @@ export default function Home() { | ||||||
|     localStorage.setItem("apiKey", apiKey); |     localStorage.setItem("apiKey", apiKey); | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|  |   const handleExportConversations = () => { | ||||||
|  |     exportConversations(); | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   const handleImportConversations = (conversations: Conversation[]) => { | ||||||
|  |     importConversations(conversations); | ||||||
|  |     setConversations(conversations); | ||||||
|  |     setSelectedConversation(conversations[conversations.length - 1]); | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|   const handleSelectConversation = (conversation: Conversation) => { |   const handleSelectConversation = (conversation: Conversation) => { | ||||||
|     setSelectedConversation(conversation); |     setSelectedConversation(conversation); | ||||||
|     saveConversation(conversation); |     saveConversation(conversation); | ||||||
|  | @ -343,6 +354,8 @@ export default function Home() { | ||||||
|                   onUpdateConversation={handleUpdateConversation} |                   onUpdateConversation={handleUpdateConversation} | ||||||
|                   onApiKeyChange={handleApiKeyChange} |                   onApiKeyChange={handleApiKeyChange} | ||||||
|                   onClearConversations={handleClearConversations} |                   onClearConversations={handleClearConversations} | ||||||
|  |                   onExportConversations={handleExportConversations} | ||||||
|  |                   onImportConversations={handleImportConversations} | ||||||
|                 /> |                 /> | ||||||
| 
 | 
 | ||||||
|                 <IconArrowBarLeft |                 <IconArrowBarLeft | ||||||
|  |  | ||||||
|  | @ -0,0 +1,22 @@ | ||||||
|  | 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)); | ||||||
|  | }; | ||||||
		Loading…
	
		Reference in New Issue