boom
This commit is contained in:
		
							parent
							
								
									a6503fb498
								
							
						
					
					
						commit
						ce331a1bbd
					
				|  | @ -1,34 +1,59 @@ | |||
| import { Message } from "@/types"; | ||||
| import { FC } from "react"; | ||||
| import { Message, OpenAIModel, OpenAIModelNames } from "@/types"; | ||||
| import { FC, useEffect, useRef } from "react"; | ||||
| import { ModelSelect } from "../ModelSelect"; | ||||
| import { ChatInput } from "./ChatInput"; | ||||
| import { ChatLoader } from "./ChatLoader"; | ||||
| import { ChatMessage } from "./ChatMessage"; | ||||
| 
 | ||||
| interface Props { | ||||
|   model: OpenAIModel; | ||||
|   messages: Message[]; | ||||
|   loading: boolean; | ||||
|   onSend: (message: Message) => void; | ||||
|   onSelect: (model: OpenAIModel) => void; | ||||
| } | ||||
| 
 | ||||
| export const Chat: FC<Props> = ({ messages, loading, onSend }) => { | ||||
|   return ( | ||||
|     <div className="flex flex-col rounded-lg px-2 sm:p-4 sm:border border-neutral-300"> | ||||
|       {messages.map((message, index) => ( | ||||
|         <div | ||||
|           key={index} | ||||
|           className="my-1 sm:my-1.5" | ||||
|         > | ||||
|           <ChatMessage message={message} /> | ||||
|         </div> | ||||
|       ))} | ||||
| export const Chat: FC<Props> = ({ model, messages, loading, onSend, onSelect }) => { | ||||
|   const messagesEndRef = useRef<HTMLDivElement>(null); | ||||
| 
 | ||||
|       {loading && ( | ||||
|         <div className="my-1 sm:my-1.5"> | ||||
|           <ChatLoader /> | ||||
|         </div> | ||||
|   const scrollToBottom = () => { | ||||
|     messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); | ||||
|   }; | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     scrollToBottom(); | ||||
|   }, [messages]); | ||||
| 
 | ||||
|   return ( | ||||
|     <div className="h-full flex flex-col"> | ||||
|       {messages.length === 0 ? ( | ||||
|         <> | ||||
|           <div className="flex justify-center pt-8"> | ||||
|             <ModelSelect | ||||
|               model={model} | ||||
|               onSelect={onSelect} | ||||
|             /> | ||||
|           </div> | ||||
| 
 | ||||
|           <div className="flex-1 text-4xl text-center text-neutral-300 pt-[280px]">Chatbot UI Pro</div> | ||||
|         </> | ||||
|       ) : ( | ||||
|         <> | ||||
|           <div className="flex-1 overflow-auto"> | ||||
|             <div className="text-center py-3 dark:bg-[#434654] dark:text-neutral-300 text-neutral-500 text-sm border border-b-neutral-300 dark:border-none">Model: {OpenAIModelNames[model]}</div> | ||||
| 
 | ||||
|             {messages.map((message, index) => ( | ||||
|               <div key={index}> | ||||
|                 <ChatMessage message={message} /> | ||||
|               </div> | ||||
|             ))} | ||||
|             {loading && <ChatLoader />} | ||||
|             <div ref={messagesEndRef} /> | ||||
|           </div> | ||||
|         </> | ||||
|       )} | ||||
| 
 | ||||
|       <div className="mt-4 sm:mt-8 bottom-[56px] left-0 w-full"> | ||||
|       <div className="h-[140px] w-[800px] mx-auto"> | ||||
|         <ChatInput onSend={onSend} /> | ||||
|       </div> | ||||
|     </div> | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| import { Message } from "@/types"; | ||||
| import { IconArrowUp } from "@tabler/icons-react"; | ||||
| import { IconSend } from "@tabler/icons-react"; | ||||
| import { FC, KeyboardEvent, useEffect, useRef, useState } from "react"; | ||||
| 
 | ||||
| interface Props { | ||||
|  | @ -46,20 +46,24 @@ export const ChatInput: FC<Props> = ({ onSend }) => { | |||
| 
 | ||||
|   return ( | ||||
|     <div className="relative"> | ||||
|       <textarea | ||||
|         ref={textareaRef} | ||||
|         className="min-h-[44px] rounded-lg pl-4 pr-12 py-2 w-full focus:outline-none focus:ring-1 focus:ring-neutral-300 border-2 border-neutral-200" | ||||
|         style={{ resize: "none" }} | ||||
|         placeholder="Type a message..." | ||||
|         value={content} | ||||
|         rows={1} | ||||
|         onChange={handleChange} | ||||
|         onKeyDown={handleKeyDown} | ||||
|       /> | ||||
| 
 | ||||
|       <button onClick={() => handleSend()}> | ||||
|         <IconArrowUp className="absolute right-2 bottom-3 h-8 w-8 hover:cursor-pointer rounded-full p-1 bg-blue-500 text-white hover:opacity-80" /> | ||||
|       </button> | ||||
|       <div className="absolute bottom-[-120px] w-full"> | ||||
|         <textarea | ||||
|           ref={textareaRef} | ||||
|           className="rounded-lg pl-4 pr-8 py-3 w-full focus:outline-none max-h-[280px] dark:bg-[#40414F] dark:border-opacity-50 dark:border-neutral-800 dark:text-neutral-100 border border-neutral-300 shadow text-neutral-900" | ||||
|           style={{ resize: "none", bottom: `${textareaRef?.current?.scrollHeight}px` }} | ||||
|           placeholder="Type a message..." | ||||
|           value={content} | ||||
|           rows={1} | ||||
|           onChange={handleChange} | ||||
|           onKeyDown={handleKeyDown} | ||||
|         /> | ||||
|         <button | ||||
|           className="absolute right-2 bottom-[14px] text-neutral-400 p-2 hover:dark:bg-neutral-800 hover:bg-neutral-400 hover:text-white rounded-md" | ||||
|           onClick={handleSend} | ||||
|         > | ||||
|           <IconSend size={18} /> | ||||
|         </button> | ||||
|       </div> | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
|  |  | |||
|  | @ -5,11 +5,12 @@ interface Props {} | |||
| 
 | ||||
| export const ChatLoader: FC<Props> = () => { | ||||
|   return ( | ||||
|     <div className="flex flex-col flex-start"> | ||||
|       <div | ||||
|         className={`flex items-center bg-neutral-200 text-neutral-900 rounded-2xl px-4 py-2 w-fit`} | ||||
|         style={{ overflowWrap: "anywhere" }} | ||||
|       > | ||||
|     <div | ||||
|       className={`flex justify-center px-[120px] py-[30px] whitespace-pre-wrap dark:bg-[#434654] dark:text-neutral-100 bg-neutral-100 text-neutral-900 dark:border-none"`} | ||||
|       style={{ overflowWrap: "anywhere" }} | ||||
|     > | ||||
|       <div className="w-[650px] flex"> | ||||
|         <div className="mr-4 font-bold min-w-[30px]">AI:</div> | ||||
|         <IconDots className="animate-pulse" /> | ||||
|       </div> | ||||
|     </div> | ||||
|  |  | |||
|  | @ -7,12 +7,14 @@ interface Props { | |||
| 
 | ||||
| export const ChatMessage: FC<Props> = ({ message }) => { | ||||
|   return ( | ||||
|     <div className={`flex flex-col ${message.role === "assistant" ? "items-start" : "items-end"}`}> | ||||
|       <div | ||||
|         className={`flex items-center ${message.role === "assistant" ? "bg-neutral-200 text-neutral-900" : "bg-blue-500 text-white"} rounded-2xl px-3 py-2 max-w-[67%] whitespace-pre-wrap`} | ||||
|         style={{ overflowWrap: "anywhere" }} | ||||
|       > | ||||
|         {message.content} | ||||
|     <div | ||||
|       className={`flex justify-center px-[120px] py-[30px] whitespace-pre-wrap] ${message.role === "assistant" ? "dark:bg-[#434654] dark:text-neutral-100 bg-neutral-100 text-neutral-900 border border-neutral-300 dark:border-none" : "dark:bg-[#343541] dark:text-white text-neutral-900"}`} | ||||
|       style={{ overflowWrap: "anywhere" }} | ||||
|     > | ||||
|       <div className="w-[650px] flex"> | ||||
|         <div className="mr-4 font-bold min-w-[40px]">{message.role === "assistant" ? "AI:" : "You:"}</div> | ||||
| 
 | ||||
|         <div className="whitespace-pre-wrap">{message.content}</div> | ||||
|       </div> | ||||
|     </div> | ||||
|   ); | ||||
|  |  | |||
|  | @ -0,0 +1,30 @@ | |||
| import { OpenAIModel, OpenAIModelNames } from "@/types"; | ||||
| import { FC } from "react"; | ||||
| 
 | ||||
| interface Props { | ||||
|   model: OpenAIModel; | ||||
|   onSelect: (model: OpenAIModel) => void; | ||||
| } | ||||
| 
 | ||||
| export const ModelSelect: FC<Props> = ({ model, onSelect }) => { | ||||
|   return ( | ||||
|     <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" | ||||
|         placeholder="Select a model" | ||||
|         value={model} | ||||
|         onChange={(e) => onSelect(e.target.value as OpenAIModel)} | ||||
|       > | ||||
|         {Object.entries(OpenAIModelNames).map(([value, name]) => ( | ||||
|           <option | ||||
|             key={value} | ||||
|             value={value} | ||||
|           > | ||||
|             {name} | ||||
|           </option> | ||||
|         ))} | ||||
|       </select> | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
|  | @ -0,0 +1,31 @@ | |||
| import { IconPlus } from "@tabler/icons-react"; | ||||
| import { FC } from "react"; | ||||
| import { SidebarSettings } from "./SidebarSettings"; | ||||
| 
 | ||||
| interface Props { | ||||
|   lightMode: "light" | "dark"; | ||||
|   onToggleLightMode: (mode: "light" | "dark") => void; | ||||
| } | ||||
| 
 | ||||
| export const Sidebar: FC<Props> = ({ lightMode, onToggleLightMode }) => { | ||||
|   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"> | ||||
|           <IconPlus | ||||
|             className="ml-4 mr-3" | ||||
|             size={16} | ||||
|           /> | ||||
|           New chat | ||||
|         </button> | ||||
|       </div> | ||||
| 
 | ||||
|       <div className="flex-1"></div> | ||||
| 
 | ||||
|       <SidebarSettings | ||||
|         lightMode={lightMode} | ||||
|         onToggleLightMode={onToggleLightMode} | ||||
|       /> | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
|  | @ -0,0 +1,19 @@ | |||
| import { FC } from "react"; | ||||
| 
 | ||||
| interface Props { | ||||
|   text: string; | ||||
|   icon: JSX.Element; | ||||
|   onClick: () => void; | ||||
| } | ||||
| 
 | ||||
| export const SidebarButton: FC<Props> = ({ text, icon, onClick }) => { | ||||
|   return ( | ||||
|     <div | ||||
|       className="flex hover:bg-[#343541] py-2 px-4 rounded-md cursor-pointer w-[200px] items-center" | ||||
|       onClick={onClick} | ||||
|     > | ||||
|       <div className="mr-3">{icon}</div> | ||||
|       <div>{text}</div> | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
|  | @ -0,0 +1,20 @@ | |||
| import { IconMoon, IconSun } from "@tabler/icons-react"; | ||||
| import { FC } from "react"; | ||||
| import { SidebarButton } from "./SidebarButton"; | ||||
| 
 | ||||
| interface Props { | ||||
|   lightMode: "light" | "dark"; | ||||
|   onToggleLightMode: (mode: "light" | "dark") => void; | ||||
| } | ||||
| 
 | ||||
| export const SidebarSettings: FC<Props> = ({ lightMode, onToggleLightMode }) => { | ||||
|   return ( | ||||
|     <div className="flex flex-col items-center border-t border-neutral-500 py-4"> | ||||
|       <SidebarButton | ||||
|         text={lightMode === "light" ? "Dark mode" : "Light mode"} | ||||
|         icon={lightMode === "light" ? <IconMoon /> : <IconSun />} | ||||
|         onClick={() => onToggleLightMode(lightMode === "light" ? "dark" : "light")} | ||||
|       /> | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
|  | @ -1,4 +1,4 @@ | |||
| import { Message } from "@/types"; | ||||
| import { Message, OpenAIModel } from "@/types"; | ||||
| import { OpenAIStream } from "@/utils"; | ||||
| 
 | ||||
| export const config = { | ||||
|  | @ -7,7 +7,8 @@ export const config = { | |||
| 
 | ||||
| const handler = async (req: Request): Promise<Response> => { | ||||
|   try { | ||||
|     const { messages } = (await req.json()) as { | ||||
|     const { model, messages } = (await req.json()) as { | ||||
|       model: OpenAIModel; | ||||
|       messages: Message[]; | ||||
|     }; | ||||
| 
 | ||||
|  | @ -24,7 +25,7 @@ const handler = async (req: Request): Promise<Response> => { | |||
|       messagesToSend.push(message); | ||||
|     } | ||||
| 
 | ||||
|     const stream = await OpenAIStream(messagesToSend); | ||||
|     const stream = await OpenAIStream(model, messagesToSend); | ||||
| 
 | ||||
|     return new Response(stream); | ||||
|   } catch (error) { | ||||
|  |  | |||
|  | @ -1,19 +1,14 @@ | |||
| import { Chat } from "@/components/Chat/Chat"; | ||||
| import { Footer } from "@/components/Layout/Footer"; | ||||
| import { Navbar } from "@/components/Layout/Navbar"; | ||||
| import { Message } from "@/types"; | ||||
| import { Sidebar } from "@/components/Sidebar/Sidebar"; | ||||
| import { Message, OpenAIModel } from "@/types"; | ||||
| import Head from "next/head"; | ||||
| import { useEffect, useRef, useState } from "react"; | ||||
| import { useEffect, useState } from "react"; | ||||
| 
 | ||||
| export default function Home() { | ||||
|   const [messages, setMessages] = useState<Message[]>([]); | ||||
|   const [loading, setLoading] = useState<boolean>(false); | ||||
| 
 | ||||
|   const messagesEndRef = useRef<HTMLDivElement>(null); | ||||
| 
 | ||||
|   const scrollToBottom = () => { | ||||
|     messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); | ||||
|   }; | ||||
|   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]; | ||||
|  | @ -27,6 +22,7 @@ export default function Home() { | |||
|         "Content-Type": "application/json" | ||||
|       }, | ||||
|       body: JSON.stringify({ | ||||
|         model, | ||||
|         messages: updatedMessages | ||||
|       }) | ||||
|     }); | ||||
|  | @ -76,18 +72,7 @@ export default function Home() { | |||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     scrollToBottom(); | ||||
|   }, [messages]); | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     setMessages([ | ||||
|       { | ||||
|         role: "assistant", | ||||
|         content: `Hi there! I'm Chatbot UI, an AI assistant. I can help you with things like answering questions, providing information, and helping with tasks. How can I help you?` | ||||
|       } | ||||
|     ]); | ||||
|   }, []); | ||||
|   useEffect(() => {}, []); | ||||
| 
 | ||||
|   return ( | ||||
|     <> | ||||
|  | @ -107,20 +92,21 @@ export default function Home() { | |||
|         /> | ||||
|       </Head> | ||||
| 
 | ||||
|       <div className="flex flex-col h-screen"> | ||||
|         <Navbar /> | ||||
|       <div className={`flex h-screen text-white ${lightMode}`}> | ||||
|         <Sidebar | ||||
|           lightMode={lightMode} | ||||
|           onToggleLightMode={setLightMode} | ||||
|         /> | ||||
| 
 | ||||
|         <div className="flex-1 overflow-auto sm:px-10 pb-4 sm:pb-10"> | ||||
|           <div className="max-w-[800px] mx-auto mt-4 sm:mt-12"> | ||||
|             <Chat | ||||
|               messages={messages} | ||||
|               loading={loading} | ||||
|               onSend={handleSend} | ||||
|             /> | ||||
|             <div ref={messagesEndRef} /> | ||||
|           </div> | ||||
|         <div className="flex flex-col w-full h-full dark:bg-[#343541]"> | ||||
|           <Chat | ||||
|             model={model} | ||||
|             messages={messages} | ||||
|             loading={loading} | ||||
|             onSend={handleSend} | ||||
|             onSelect={setModel} | ||||
|           /> | ||||
|         </div> | ||||
|         <Footer /> | ||||
|       </div> | ||||
|     </> | ||||
|   ); | ||||
|  |  | |||
|  | @ -1,6 +1,7 @@ | |||
| /** @type {import('tailwindcss').Config} */ | ||||
| module.exports = { | ||||
|   content: ["./app/**/*.{js,ts,jsx,tsx}", "./pages/**/*.{js,ts,jsx,tsx}", "./components/**/*.{js,ts,jsx,tsx}"], | ||||
|   darkMode: "class", | ||||
|   theme: { | ||||
|     extend: {} | ||||
|   }, | ||||
|  |  | |||
|  | @ -1,7 +1,15 @@ | |||
| export enum OpenAIModel { | ||||
|   DAVINCI_TURBO = "gpt-3.5-turbo" | ||||
|   GPT_3_5 = "gpt-3.5-turbo", | ||||
|   GPT_3_5_LEGACY = "gpt-3.5-turbo-0301" | ||||
|   // GPT_4 = "gpt-4"
 | ||||
| } | ||||
| 
 | ||||
| export const OpenAIModelNames: Record<OpenAIModel, string> = { | ||||
|   [OpenAIModel.GPT_3_5]: "Default (GPT-3.5)", | ||||
|   [OpenAIModel.GPT_3_5_LEGACY]: "Legacy (GPT-3.5)" | ||||
|   // [OpenAIModel.GPT_4]: "GPT-4"
 | ||||
| }; | ||||
| 
 | ||||
| export interface Message { | ||||
|   role: Role; | ||||
|   content: string; | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| import { Message, OpenAIModel } from "@/types"; | ||||
| import { createParser, ParsedEvent, ReconnectInterval } from "eventsource-parser"; | ||||
| 
 | ||||
| export const OpenAIStream = async (messages: Message[]) => { | ||||
| export const OpenAIStream = async (model: OpenAIModel, messages: Message[]) => { | ||||
|   const encoder = new TextEncoder(); | ||||
|   const decoder = new TextDecoder(); | ||||
| 
 | ||||
|  | @ -12,7 +12,7 @@ export const OpenAIStream = async (messages: Message[]) => { | |||
|     }, | ||||
|     method: "POST", | ||||
|     body: JSON.stringify({ | ||||
|       model: OpenAIModel.DAVINCI_TURBO, | ||||
|       model, | ||||
|       messages: [ | ||||
|         { | ||||
|           role: "system", | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue