import { Chat } from "@/components/Chat/Chat"; import { Navbar } from "@/components/Mobile/Navbar"; import { Sidebar } from "@/components/Sidebar/Sidebar"; import { ChatBody, ChatFolder, Conversation, ErrorMessage, KeyValuePair, Message, OpenAIModel, OpenAIModelID, OpenAIModels } from "@/types"; import { cleanConversationHistory, cleanSelectedConversation } from "@/utils/app/clean"; import { DEFAULT_SYSTEM_PROMPT } from "@/utils/app/const"; import { saveConversation, saveConversations, updateConversation } from "@/utils/app/conversation"; import { saveFolders } from "@/utils/app/folders"; import { exportData, importData } from "@/utils/app/importExport"; import { IconArrowBarLeft, IconArrowBarRight } from "@tabler/icons-react"; import { GetServerSideProps } from "next"; import Head from "next/head"; import { useEffect, useRef, useState } from "react"; import { serverSideTranslations } from 'next-i18next/serverSideTranslations' import { useTranslation } from "next-i18next"; interface HomeProps { serverSideApiKeyIsSet: boolean; } const Home: React.FC = ({ serverSideApiKeyIsSet }) => { const { t } = useTranslation('chat') const [folders, setFolders] = useState([]); const [conversations, setConversations] = useState([]); const [selectedConversation, setSelectedConversation] = useState(); const [loading, setLoading] = useState(false); const [models, setModels] = useState([]); const [lightMode, setLightMode] = useState<"dark" | "light">("dark"); const [messageIsStreaming, setMessageIsStreaming] = useState(false); const [showSidebar, setShowSidebar] = useState(true); const [apiKey, setApiKey] = useState(""); const [messageError, setMessageError] = useState(false); const [modelError, setModelError] = useState(null); const [currentMessage, setCurrentMessage] = useState(); const stopConversationRef = useRef(false); const handleSend = async (message: Message, deleteCount = 0) => { if (selectedConversation) { let updatedConversation: Conversation; if (deleteCount) { const updatedMessages = [...selectedConversation.messages]; for (let i = 0; i < deleteCount; i++) { updatedMessages.pop(); } updatedConversation = { ...selectedConversation, messages: [...updatedMessages, message] }; } else { updatedConversation = { ...selectedConversation, messages: [...selectedConversation.messages, message] }; } setSelectedConversation(updatedConversation); setLoading(true); setMessageIsStreaming(true); setMessageError(false); const chatBody: ChatBody = { model: updatedConversation.model, messages: updatedConversation.messages, key: apiKey, prompt: updatedConversation.prompt }; const controller = new AbortController(); const response = await fetch("/api/chat", { method: "POST", headers: { "Content-Type": "application/json" }, signal: controller.signal, body: JSON.stringify(chatBody) }); if (!response.ok) { setLoading(false); setMessageIsStreaming(false); setMessageError(true); return; } const data = response.body; if (!data) { setLoading(false); setMessageIsStreaming(false); setMessageError(true); return; } if (updatedConversation.messages.length === 1) { const { content } = message; const customName = content.length > 30 ? content.substring(0, 30) + "..." : content; updatedConversation = { ...updatedConversation, name: customName }; } setLoading(false); const reader = data.getReader(); const decoder = new TextDecoder(); let done = false; let isFirst = true; let text = ""; while (!done) { if (stopConversationRef.current === true) { controller.abort(); done = true; break; } 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); } } saveConversation(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); saveConversations(updatedConversations); setMessageIsStreaming(false); } }; const fetchModels = async (key: string) => { const error = { title: t('Error fetching models.'), code: null, messageLines: [ t('Make sure your OpenAI API key is set in the bottom left of the sidebar.'), t('If you completed this step, OpenAI may be experiencing issues.') ] } as ErrorMessage; const response = await fetch("/api/models", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ key }) }); if (!response.ok) { try { const data = await response.json(); Object.assign(error, { code: data.error?.code, messageLines: [data.error?.message] }) } catch (e) { } setModelError(error); return; } const data = await response.json(); if (!data) { setModelError(error); return; } setModels(data); setModelError(null); }; const handleLightMode = (mode: "dark" | "light") => { setLightMode(mode); localStorage.setItem("theme", mode); }; const handleApiKeyChange = (apiKey: string) => { setApiKey(apiKey); localStorage.setItem("apiKey", apiKey); }; const handleExportData = () => { exportData(); }; const handleImportConversations = (data: { conversations: Conversation[]; folders: ChatFolder[] }) => { importData(data.conversations, data.folders); setConversations(data.conversations); setSelectedConversation(data.conversations[data.conversations.length - 1]); setFolders(data.folders); }; const handleSelectConversation = (conversation: Conversation) => { setSelectedConversation(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 lastConversation = conversations[conversations.length - 1]; const newConversation: Conversation = { id: lastConversation ? lastConversation.id + 1 : 1, name: `${t('Conversation')} ${lastConversation ? lastConversation.id + 1 : 1}`, messages: [], model: OpenAIModels[OpenAIModelID.GPT_3_5], prompt: DEFAULT_SYSTEM_PROMPT, folderId: 0 }; const updatedConversations = [...conversations, newConversation]; setSelectedConversation(newConversation); setConversations(updatedConversations); saveConversation(newConversation); saveConversations(updatedConversations); setLoading(false); }; const handleDeleteConversation = (conversation: Conversation) => { const updatedConversations = conversations.filter((c) => c.id !== conversation.id); setConversations(updatedConversations); saveConversations(updatedConversations); if (updatedConversations.length > 0) { setSelectedConversation(updatedConversations[updatedConversations.length - 1]); saveConversation(updatedConversations[updatedConversations.length - 1]); } else { setSelectedConversation({ id: 1, name: "New conversation", messages: [], model: OpenAIModels[OpenAIModelID.GPT_3_5], prompt: DEFAULT_SYSTEM_PROMPT, folderId: 0 }); localStorage.removeItem("selectedConversation"); } }; const handleUpdateConversation = (conversation: Conversation, data: KeyValuePair) => { const updatedConversation = { ...conversation, [data.key]: data.value }; const { single, all } = updateConversation(updatedConversation, conversations); setSelectedConversation(single); setConversations(all); }; const handleClearConversations = () => { setConversations([]); localStorage.removeItem("conversationHistory"); setSelectedConversation({ id: 1, name: "New conversation", messages: [], model: OpenAIModels[OpenAIModelID.GPT_3_5], prompt: DEFAULT_SYSTEM_PROMPT, folderId: 0 }); localStorage.removeItem("selectedConversation"); setFolders([]); localStorage.removeItem("folders"); }; const handleEditMessage = (message: Message, messageIndex: number) => { if (selectedConversation) { const updatedMessages = selectedConversation.messages .map((m, i) => { if (i < messageIndex) { return m; } }) .filter((m) => m) as Message[]; const updatedConversation = { ...selectedConversation, messages: updatedMessages }; const { single, all } = updateConversation(updatedConversation, conversations); setSelectedConversation(single); setConversations(all); setCurrentMessage(message); } }; useEffect(() => { if (currentMessage) { handleSend(currentMessage); setCurrentMessage(undefined); } }, [currentMessage]); useEffect(() => { if (window.innerWidth < 640) { setShowSidebar(false); } }, [selectedConversation]); useEffect(() => { if (apiKey) { fetchModels(apiKey); } }, [apiKey]); useEffect(() => { const theme = localStorage.getItem("theme"); if (theme) { setLightMode(theme as "dark" | "light"); } const apiKey = localStorage.getItem("apiKey"); if (apiKey) { setApiKey(apiKey); fetchModels(apiKey); } else if (serverSideApiKeyIsSet) { fetchModels(""); } if (window.innerWidth < 640) { setShowSidebar(false); } const folders = localStorage.getItem("folders"); if (folders) { setFolders(JSON.parse(folders)); } const conversationHistory = localStorage.getItem("conversationHistory"); if (conversationHistory) { const parsedConversationHistory: Conversation[] = JSON.parse(conversationHistory); const cleanedConversationHistory = cleanConversationHistory(parsedConversationHistory); setConversations(cleanedConversationHistory); } const selectedConversation = localStorage.getItem("selectedConversation"); if (selectedConversation) { const parsedSelectedConversation: Conversation = JSON.parse(selectedConversation); const cleanedSelectedConversation = cleanSelectedConversation(parsedSelectedConversation); setSelectedConversation(cleanedSelectedConversation); } else { setSelectedConversation({ id: 1, name: "New conversation", messages: [], model: OpenAIModels[OpenAIModelID.GPT_3_5], prompt: DEFAULT_SYSTEM_PROMPT, folderId: 0 }); } }, [serverSideApiKeyIsSet]); return ( <> Chatbot UI {selectedConversation && (
{showSidebar ? (
setShowSidebar(!showSidebar)} onUpdateConversation={handleUpdateConversation} onApiKeyChange={handleApiKeyChange} onClearConversations={handleClearConversations} onExportConversations={handleExportData} onImportConversations={handleImportConversations} /> setShowSidebar(!showSidebar)} />
setShowSidebar(!showSidebar)} className="sm:hidden bg-black opacity-70 z-10 absolute top-0 left-0 h-full w-full" >
) : ( setShowSidebar(!showSidebar)} /> )}
)} ); }; export default Home; export const getServerSideProps: GetServerSideProps = async ({ locale }) => { return { props: { serverSideApiKeyIsSet: !!process.env.OPENAI_API_KEY, ...(await serverSideTranslations(locale ?? 'en', [ 'common', 'chat', 'sidebar', 'markdown', ])), } }; };