mvp
This commit is contained in:
		
							parent
							
								
									e87136c4ec
								
							
						
					
					
						commit
						758a1215c2
					
				|  | @ -0,0 +1,39 @@ | ||||||
|  | import { Conversation } from "@/types"; | ||||||
|  | import { IconMessage, IconTrash } from "@tabler/icons-react"; | ||||||
|  | import { FC } from "react"; | ||||||
|  | 
 | ||||||
|  | interface Props { | ||||||
|  |   conversations: Conversation[]; | ||||||
|  |   selectedConversation: Conversation; | ||||||
|  |   onSelectConversation: (conversation: Conversation) => void; | ||||||
|  |   onDeleteConversation: (conversation: Conversation) => void; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export const Conversations: FC<Props> = ({ conversations, selectedConversation, onSelectConversation, onDeleteConversation }) => { | ||||||
|  |   return ( | ||||||
|  |     <div className="flex flex-col space-y-2"> | ||||||
|  |       {conversations.map((conversation, index) => ( | ||||||
|  |         <div | ||||||
|  |           key={index} | ||||||
|  |           className={`flex items-center justify-start w-[240px] h-[40px] px-2 text-sm rounded-lg hover:bg-neutral-700 cursor-pointer ${selectedConversation.id === conversation.id ? "bg-slate-600" : ""}`} | ||||||
|  |           onClick={() => onSelectConversation(conversation)} | ||||||
|  |         > | ||||||
|  |           <IconMessage | ||||||
|  |             className="mr-2 min-w-[20px]" | ||||||
|  |             size={18} | ||||||
|  |           /> | ||||||
|  |           <div className="overflow-hidden whitespace-nowrap overflow-ellipsis pr-1">{conversation.messages[0] ? conversation.messages[0].content : "Empty conversation"}</div> | ||||||
|  | 
 | ||||||
|  |           <IconTrash | ||||||
|  |             className="ml-auto min-w-[20px] text-neutral-400 hover:text-neutral-100" | ||||||
|  |             size={18} | ||||||
|  |             onClick={(e) => { | ||||||
|  |               e.stopPropagation(); | ||||||
|  |               onDeleteConversation(conversation); | ||||||
|  |             }} | ||||||
|  |           /> | ||||||
|  |         </div> | ||||||
|  |       ))} | ||||||
|  |     </div> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
|  | @ -11,7 +11,7 @@ export const ModelSelect: FC<Props> = ({ model, onSelect }) => { | ||||||
|     <div className="flex flex-col"> |     <div className="flex flex-col"> | ||||||
|       <label className="text-left mb-2 dark:text-neutral-400 text-neutral-700">Model</label> |       <label className="text-left mb-2 dark:text-neutral-400 text-neutral-700">Model</label> | ||||||
|       <select |       <select | ||||||
|         className="w-[300px] p-3 dark:text-white dark:bg-[#343541] border border-neutral-500 rounded-lg appearance-none focus:shadow-outline text-neutral-900" |         className="w-[300px] p-3 dark:text-white dark:bg-[#343541] border border-neutral-500 rounded-lg appearance-none focus:shadow-outline text-neutral-900 cursor-pointer" | ||||||
|         placeholder="Select a model" |         placeholder="Select a model" | ||||||
|         value={model} |         value={model} | ||||||
|         onChange={(e) => onSelect(e.target.value as OpenAIModel)} |         onChange={(e) => onSelect(e.target.value as OpenAIModel)} | ||||||
|  |  | ||||||
|  | @ -1,17 +1,27 @@ | ||||||
|  | import { Conversation } from "@/types"; | ||||||
| import { IconPlus } from "@tabler/icons-react"; | import { IconPlus } from "@tabler/icons-react"; | ||||||
| import { FC } from "react"; | import { FC } from "react"; | ||||||
|  | import { Conversations } from "../Conversations"; | ||||||
| import { SidebarSettings } from "./SidebarSettings"; | import { SidebarSettings } from "./SidebarSettings"; | ||||||
| 
 | 
 | ||||||
| interface Props { | interface Props { | ||||||
|  |   conversations: Conversation[]; | ||||||
|   lightMode: "light" | "dark"; |   lightMode: "light" | "dark"; | ||||||
|  |   selectedConversation: Conversation; | ||||||
|  |   onNewConversation: () => void; | ||||||
|   onToggleLightMode: (mode: "light" | "dark") => void; |   onToggleLightMode: (mode: "light" | "dark") => void; | ||||||
|  |   onSelectConversation: (conversation: Conversation) => void; | ||||||
|  |   onDeleteConversation: (conversation: Conversation) => void; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export const Sidebar: FC<Props> = ({ lightMode, onToggleLightMode }) => { | export const Sidebar: FC<Props> = ({ conversations, lightMode, selectedConversation, onNewConversation, onToggleLightMode, onSelectConversation, onDeleteConversation }) => { | ||||||
|   return ( |   return ( | ||||||
|     <div className="flex flex-col bg-[#202123] min-w-[260px]"> |     <div className="flex flex-col bg-[#202123] min-w-[260px]"> | ||||||
|       <div className="flex items-center justify-center h-[60px]"> |       <div className="flex items-center justify-center h-[60px]"> | ||||||
|         <button className="flex items-center w-[240px] h-[40px] rounded-lg bg-[#202123] border border-neutral-600 text-sm hover:bg-neutral-700"> |         <button | ||||||
|  |           className="flex items-center w-[240px] h-[40px] rounded-lg bg-[#202123] border border-neutral-600 text-sm hover:bg-neutral-700" | ||||||
|  |           onClick={onNewConversation} | ||||||
|  |         > | ||||||
|           <IconPlus |           <IconPlus | ||||||
|             className="ml-4 mr-3" |             className="ml-4 mr-3" | ||||||
|             size={16} |             size={16} | ||||||
|  | @ -20,7 +30,14 @@ export const Sidebar: FC<Props> = ({ lightMode, onToggleLightMode }) => { | ||||||
|         </button> |         </button> | ||||||
|       </div> |       </div> | ||||||
| 
 | 
 | ||||||
|       <div className="flex-1"></div> |       <div className="flex-1 mx-auto pb-2 overflow-auto"> | ||||||
|  |         <Conversations | ||||||
|  |           conversations={conversations} | ||||||
|  |           selectedConversation={selectedConversation} | ||||||
|  |           onSelectConversation={onSelectConversation} | ||||||
|  |           onDeleteConversation={onDeleteConversation} | ||||||
|  |         /> | ||||||
|  |       </div> | ||||||
| 
 | 
 | ||||||
|       <SidebarSettings |       <SidebarSettings | ||||||
|         lightMode={lightMode} |         lightMode={lightMode} | ||||||
|  |  | ||||||
							
								
								
									
										243
									
								
								pages/index.tsx
								
								
								
								
							
							
						
						
									
										243
									
								
								pages/index.tsx
								
								
								
								
							|  | @ -1,80 +1,112 @@ | ||||||
| import { Chat } from "@/components/Chat/Chat"; | import { Chat } from "@/components/Chat/Chat"; | ||||||
| import { Sidebar } from "@/components/Sidebar/Sidebar"; | import { Sidebar } from "@/components/Sidebar/Sidebar"; | ||||||
| import { Message, OpenAIModel } from "@/types"; | import { Conversation, Message, OpenAIModel } from "@/types"; | ||||||
| import Head from "next/head"; | import Head from "next/head"; | ||||||
| import { useEffect, useState } from "react"; | import { useEffect, useState } from "react"; | ||||||
| 
 | 
 | ||||||
| export default function Home() { | export default function Home() { | ||||||
|   const [messages, setMessages] = useState<Message[]>([]); |   const [conversations, setConversations] = useState<Conversation[]>([]); | ||||||
|  |   const [selectedConversation, setSelectedConversation] = useState<Conversation>(); | ||||||
|   const [loading, setLoading] = useState<boolean>(false); |   const [loading, setLoading] = useState<boolean>(false); | ||||||
|   const [model, setModel] = useState<OpenAIModel>(OpenAIModel.GPT_3_5); |   const [model, setModel] = useState<OpenAIModel>(OpenAIModel.GPT_3_5); | ||||||
|   const [lightMode, setLightMode] = useState<"dark" | "light">("dark"); |   const [lightMode, setLightMode] = useState<"dark" | "light">("dark"); | ||||||
| 
 | 
 | ||||||
|   const handleSend = async (message: Message) => { |   const handleSend = async (message: Message) => { | ||||||
|     const updatedMessages = [...messages, message]; |     if (selectedConversation) { | ||||||
|  |       let updatedConversation: Conversation = { | ||||||
|  |         ...selectedConversation, | ||||||
|  |         messages: [...selectedConversation.messages, message] | ||||||
|  |       }; | ||||||
| 
 | 
 | ||||||
|     setMessages(updatedMessages); |       setSelectedConversation(updatedConversation); | ||||||
|     setLoading(true); |       setLoading(true); | ||||||
| 
 | 
 | ||||||
|     const response = await fetch("/api/chat", { |       const response = await fetch("/api/chat", { | ||||||
|       method: "POST", |         method: "POST", | ||||||
|       headers: { |         headers: { | ||||||
|         "Content-Type": "application/json" |           "Content-Type": "application/json" | ||||||
|       }, |         }, | ||||||
|       body: JSON.stringify({ |         body: JSON.stringify({ | ||||||
|         model, |           model, | ||||||
|         messages: updatedMessages |           messages: updatedConversation.messages | ||||||
|       }) |         }) | ||||||
|     }); |       }); | ||||||
| 
 | 
 | ||||||
|     if (!response.ok) { |       if (!response.ok) { | ||||||
|       setLoading(false); |         setLoading(false); | ||||||
|       throw new Error(response.statusText); |         throw new Error(response.statusText); | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     const data = response.body; |  | ||||||
| 
 |  | ||||||
|     if (!data) { |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     setLoading(false); |  | ||||||
| 
 |  | ||||||
|     const reader = data.getReader(); |  | ||||||
|     const decoder = new TextDecoder(); |  | ||||||
|     let done = false; |  | ||||||
|     let isFirst = true; |  | ||||||
|     let text = ""; |  | ||||||
| 
 |  | ||||||
|     while (!done) { |  | ||||||
|       const { value, done: doneReading } = await reader.read(); |  | ||||||
|       done = doneReading; |  | ||||||
|       const chunkValue = decoder.decode(value); |  | ||||||
| 
 |  | ||||||
|       text += chunkValue; |  | ||||||
| 
 |  | ||||||
|       if (isFirst) { |  | ||||||
|         isFirst = false; |  | ||||||
|         setMessages((messages) => [ |  | ||||||
|           ...messages, |  | ||||||
|           { |  | ||||||
|             role: "assistant", |  | ||||||
|             content: chunkValue |  | ||||||
|           } |  | ||||||
|         ]); |  | ||||||
|       } else { |  | ||||||
|         setMessages((messages) => { |  | ||||||
|           const lastMessage = messages[messages.length - 1]; |  | ||||||
|           const updatedMessage = { |  | ||||||
|             ...lastMessage, |  | ||||||
|             content: lastMessage.content + chunkValue |  | ||||||
|           }; |  | ||||||
|           return [...messages.slice(0, -1), updatedMessage]; |  | ||||||
|         }); |  | ||||||
|       } |       } | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|     localStorage.setItem("messageHistory", JSON.stringify([...updatedMessages, { role: "assistant", content: text }])); |       const data = response.body; | ||||||
|  | 
 | ||||||
|  |       if (!data) { | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       setLoading(false); | ||||||
|  | 
 | ||||||
|  |       const reader = data.getReader(); | ||||||
|  |       const decoder = new TextDecoder(); | ||||||
|  |       let done = false; | ||||||
|  |       let isFirst = true; | ||||||
|  |       let text = ""; | ||||||
|  | 
 | ||||||
|  |       while (!done) { | ||||||
|  |         const { value, done: doneReading } = await reader.read(); | ||||||
|  |         done = doneReading; | ||||||
|  |         const chunkValue = decoder.decode(value); | ||||||
|  | 
 | ||||||
|  |         text += chunkValue; | ||||||
|  | 
 | ||||||
|  |         if (isFirst) { | ||||||
|  |           isFirst = false; | ||||||
|  |           const updatedMessages: Message[] = [...updatedConversation.messages, { role: "assistant", content: chunkValue }]; | ||||||
|  | 
 | ||||||
|  |           updatedConversation = { | ||||||
|  |             ...updatedConversation, | ||||||
|  |             messages: updatedMessages | ||||||
|  |           }; | ||||||
|  | 
 | ||||||
|  |           setSelectedConversation(updatedConversation); | ||||||
|  |         } else { | ||||||
|  |           const updatedMessages: Message[] = updatedConversation.messages.map((message, index) => { | ||||||
|  |             if (index === updatedConversation.messages.length - 1) { | ||||||
|  |               return { | ||||||
|  |                 ...message, | ||||||
|  |                 content: text | ||||||
|  |               }; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             return message; | ||||||
|  |           }); | ||||||
|  | 
 | ||||||
|  |           updatedConversation = { | ||||||
|  |             ...updatedConversation, | ||||||
|  |             messages: updatedMessages | ||||||
|  |           }; | ||||||
|  | 
 | ||||||
|  |           setSelectedConversation(updatedConversation); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       localStorage.setItem("selectedConversation", JSON.stringify(updatedConversation)); | ||||||
|  | 
 | ||||||
|  |       const updatedConversations: Conversation[] = conversations.map((conversation) => { | ||||||
|  |         if (conversation.id === selectedConversation.id) { | ||||||
|  |           return updatedConversation; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return conversation; | ||||||
|  |       }); | ||||||
|  | 
 | ||||||
|  |       if (updatedConversations.length === 0) { | ||||||
|  |         updatedConversations.push(updatedConversation); | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       setConversations(updatedConversations); | ||||||
|  | 
 | ||||||
|  |       localStorage.setItem("conversationHistory", JSON.stringify(updatedConversations)); | ||||||
|  |     } | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   const handleLightMode = (mode: "dark" | "light") => { |   const handleLightMode = (mode: "dark" | "light") => { | ||||||
|  | @ -82,18 +114,61 @@ export default function Home() { | ||||||
|     localStorage.setItem("theme", mode); |     localStorage.setItem("theme", mode); | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|  |   const handleNewConversation = () => { | ||||||
|  |     const newConversation: Conversation = { | ||||||
|  |       id: conversations.length + 1, | ||||||
|  |       name: "", | ||||||
|  |       messages: [] | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     const updatedConversations = [...conversations, newConversation]; | ||||||
|  |     setConversations(updatedConversations); | ||||||
|  |     localStorage.setItem("conversationHistory", JSON.stringify(updatedConversations)); | ||||||
|  | 
 | ||||||
|  |     setSelectedConversation(newConversation); | ||||||
|  |     localStorage.setItem("selectedConversation", JSON.stringify(newConversation)); | ||||||
|  | 
 | ||||||
|  |     setModel(OpenAIModel.GPT_3_5); | ||||||
|  |     setLoading(false); | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   const handleSelectConversation = (conversation: Conversation) => { | ||||||
|  |     setSelectedConversation(conversation); | ||||||
|  |     localStorage.setItem("selectedConversation", JSON.stringify(conversation)); | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   const handleDeleteConversation = (conversation: Conversation) => { | ||||||
|  |     const updatedConversations = conversations.filter((c) => c.id !== conversation.id); | ||||||
|  |     setConversations(updatedConversations); | ||||||
|  |     localStorage.setItem("conversationHistory", JSON.stringify(updatedConversations)); | ||||||
|  | 
 | ||||||
|  |     if (selectedConversation && selectedConversation.id === conversation.id) { | ||||||
|  |       setSelectedConversation(undefined); | ||||||
|  |       localStorage.removeItem("selectedConversation"); | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|   useEffect(() => { |   useEffect(() => { | ||||||
|     const theme = localStorage.getItem("theme"); |     const theme = localStorage.getItem("theme"); | ||||||
| 
 |  | ||||||
|     if (theme) { |     if (theme) { | ||||||
|       setLightMode(theme as "dark" | "light"); |       setLightMode(theme as "dark" | "light"); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     const messageHistory = localStorage.getItem("messageHistory"); |     const conversationHistory = localStorage.getItem("conversationHistory"); | ||||||
|     console.log(messageHistory); |  | ||||||
| 
 | 
 | ||||||
|     if (messageHistory) { |     if (conversationHistory) { | ||||||
|       setMessages(JSON.parse(messageHistory)); |       setConversations(JSON.parse(conversationHistory)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     const selectedConversation = localStorage.getItem("selectedConversation"); | ||||||
|  |     if (selectedConversation) { | ||||||
|  |       setSelectedConversation(JSON.parse(selectedConversation)); | ||||||
|  |     } else { | ||||||
|  |       setSelectedConversation({ | ||||||
|  |         id: 1, | ||||||
|  |         name: "", | ||||||
|  |         messages: [] | ||||||
|  |       }); | ||||||
|     } |     } | ||||||
|   }, []); |   }, []); | ||||||
| 
 | 
 | ||||||
|  | @ -114,23 +189,29 @@ export default function Home() { | ||||||
|           href="/favicon.ico" |           href="/favicon.ico" | ||||||
|         /> |         /> | ||||||
|       </Head> |       </Head> | ||||||
| 
 |       {selectedConversation && ( | ||||||
|       <div className={`flex h-screen text-white ${lightMode}`}> |         <div className={`flex h-screen text-white ${lightMode}`}> | ||||||
|         <Sidebar |           <Sidebar | ||||||
|           lightMode={lightMode} |             conversations={conversations} | ||||||
|           onToggleLightMode={handleLightMode} |             lightMode={lightMode} | ||||||
|         /> |             selectedConversation={selectedConversation} | ||||||
| 
 |             onToggleLightMode={handleLightMode} | ||||||
|         <div className="flex flex-col w-full h-full dark:bg-[#343541]"> |             onNewConversation={handleNewConversation} | ||||||
|           <Chat |             onSelectConversation={handleSelectConversation} | ||||||
|             model={model} |             onDeleteConversation={handleDeleteConversation} | ||||||
|             messages={messages} |  | ||||||
|             loading={loading} |  | ||||||
|             onSend={handleSend} |  | ||||||
|             onSelect={setModel} |  | ||||||
|           /> |           /> | ||||||
|  | 
 | ||||||
|  |           <div className="flex flex-col w-full h-full dark:bg-[#343541]"> | ||||||
|  |             <Chat | ||||||
|  |               model={model} | ||||||
|  |               messages={selectedConversation.messages} | ||||||
|  |               loading={loading} | ||||||
|  |               onSend={handleSend} | ||||||
|  |               onSelect={setModel} | ||||||
|  |             /> | ||||||
|  |           </div> | ||||||
|         </div> |         </div> | ||||||
|       </div> |       )} | ||||||
|     </> |     </> | ||||||
|   ); |   ); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -16,3 +16,9 @@ export interface Message { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export type Role = "assistant" | "user"; | export type Role = "assistant" | "user"; | ||||||
|  | 
 | ||||||
|  | export interface Conversation { | ||||||
|  |   id: number; | ||||||
|  |   name: string; | ||||||
|  |   messages: Message[]; | ||||||
|  | } | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue