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"> | ||||
|       <label className="text-left mb-2 dark:text-neutral-400 text-neutral-700">Model</label> | ||||
|       <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" | ||||
|         value={model} | ||||
|         onChange={(e) => onSelect(e.target.value as OpenAIModel)} | ||||
|  |  | |||
|  | @ -1,17 +1,27 @@ | |||
| import { Conversation } from "@/types"; | ||||
| import { IconPlus } from "@tabler/icons-react"; | ||||
| import { FC } from "react"; | ||||
| import { Conversations } from "../Conversations"; | ||||
| import { SidebarSettings } from "./SidebarSettings"; | ||||
| 
 | ||||
| interface Props { | ||||
|   conversations: Conversation[]; | ||||
|   lightMode: "light" | "dark"; | ||||
|   selectedConversation: Conversation; | ||||
|   onNewConversation: () => 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 ( | ||||
|     <div className="flex flex-col bg-[#202123] min-w-[260px]"> | ||||
|       <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 | ||||
|             className="ml-4 mr-3" | ||||
|             size={16} | ||||
|  | @ -20,7 +30,14 @@ export const Sidebar: FC<Props> = ({ lightMode, onToggleLightMode }) => { | |||
|         </button> | ||||
|       </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 | ||||
|         lightMode={lightMode} | ||||
|  |  | |||
							
								
								
									
										243
									
								
								pages/index.tsx
								
								
								
								
							
							
						
						
									
										243
									
								
								pages/index.tsx
								
								
								
								
							|  | @ -1,80 +1,112 @@ | |||
| import { Chat } from "@/components/Chat/Chat"; | ||||
| import { Sidebar } from "@/components/Sidebar/Sidebar"; | ||||
| import { Message, OpenAIModel } from "@/types"; | ||||
| import { Conversation, Message, OpenAIModel } from "@/types"; | ||||
| import Head from "next/head"; | ||||
| import { useEffect, useState } from "react"; | ||||
| 
 | ||||
| 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 [model, setModel] = useState<OpenAIModel>(OpenAIModel.GPT_3_5); | ||||
|   const [lightMode, setLightMode] = useState<"dark" | "light">("dark"); | ||||
| 
 | ||||
|   const handleSend = async (message: Message) => { | ||||
|     const updatedMessages = [...messages, message]; | ||||
|     if (selectedConversation) { | ||||
|       let updatedConversation: Conversation = { | ||||
|         ...selectedConversation, | ||||
|         messages: [...selectedConversation.messages, message] | ||||
|       }; | ||||
| 
 | ||||
|     setMessages(updatedMessages); | ||||
|     setLoading(true); | ||||
|       setSelectedConversation(updatedConversation); | ||||
|       setLoading(true); | ||||
| 
 | ||||
|     const response = await fetch("/api/chat", { | ||||
|       method: "POST", | ||||
|       headers: { | ||||
|         "Content-Type": "application/json" | ||||
|       }, | ||||
|       body: JSON.stringify({ | ||||
|         model, | ||||
|         messages: updatedMessages | ||||
|       }) | ||||
|     }); | ||||
|       const response = await fetch("/api/chat", { | ||||
|         method: "POST", | ||||
|         headers: { | ||||
|           "Content-Type": "application/json" | ||||
|         }, | ||||
|         body: JSON.stringify({ | ||||
|           model, | ||||
|           messages: updatedConversation.messages | ||||
|         }) | ||||
|       }); | ||||
| 
 | ||||
|     if (!response.ok) { | ||||
|       setLoading(false); | ||||
|       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]; | ||||
|         }); | ||||
|       if (!response.ok) { | ||||
|         setLoading(false); | ||||
|         throw new Error(response.statusText); | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     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") => { | ||||
|  | @ -82,18 +114,61 @@ export default function Home() { | |||
|     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(() => { | ||||
|     const theme = localStorage.getItem("theme"); | ||||
| 
 | ||||
|     if (theme) { | ||||
|       setLightMode(theme as "dark" | "light"); | ||||
|     } | ||||
| 
 | ||||
|     const messageHistory = localStorage.getItem("messageHistory"); | ||||
|     console.log(messageHistory); | ||||
|     const conversationHistory = localStorage.getItem("conversationHistory"); | ||||
| 
 | ||||
|     if (messageHistory) { | ||||
|       setMessages(JSON.parse(messageHistory)); | ||||
|     if (conversationHistory) { | ||||
|       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" | ||||
|         /> | ||||
|       </Head> | ||||
| 
 | ||||
|       <div className={`flex h-screen text-white ${lightMode}`}> | ||||
|         <Sidebar | ||||
|           lightMode={lightMode} | ||||
|           onToggleLightMode={handleLightMode} | ||||
|         /> | ||||
| 
 | ||||
|         <div className="flex flex-col w-full h-full dark:bg-[#343541]"> | ||||
|           <Chat | ||||
|             model={model} | ||||
|             messages={messages} | ||||
|             loading={loading} | ||||
|             onSend={handleSend} | ||||
|             onSelect={setModel} | ||||
|       {selectedConversation && ( | ||||
|         <div className={`flex h-screen text-white ${lightMode}`}> | ||||
|           <Sidebar | ||||
|             conversations={conversations} | ||||
|             lightMode={lightMode} | ||||
|             selectedConversation={selectedConversation} | ||||
|             onToggleLightMode={handleLightMode} | ||||
|             onNewConversation={handleNewConversation} | ||||
|             onSelectConversation={handleSelectConversation} | ||||
|             onDeleteConversation={handleDeleteConversation} | ||||
|           /> | ||||
| 
 | ||||
|           <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> | ||||
|       )} | ||||
|     </> | ||||
|   ); | ||||
| } | ||||
|  |  | |||
|  | @ -16,3 +16,9 @@ export interface Message { | |||
| } | ||||
| 
 | ||||
| export type Role = "assistant" | "user"; | ||||
| 
 | ||||
| export interface Conversation { | ||||
|   id: number; | ||||
|   name: string; | ||||
|   messages: Message[]; | ||||
| } | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue