'use client'; import { useEffect, useState } from 'react'; import { Document } from '@langchain/core/documents'; import Navbar from './Navbar'; import Chat from './Chat'; import EmptyChat from './EmptyChat'; import { toast } from 'sonner'; import { useSearchParams } from 'next/navigation'; export type Message = { id: string; createdAt: Date; content: string; role: 'user' | 'assistant'; sources?: Document[]; }; const useSocket = (url: string, setIsReady: (ready: boolean) => void) => { const [ws, setWs] = useState(null); useEffect(() => { if (!ws) { const connectWs = async () => { let chatModel = localStorage.getItem('chatModel'); let chatModelProvider = localStorage.getItem('chatModelProvider'); let embeddingModel = localStorage.getItem('embeddingModel'); let embeddingModelProvider = localStorage.getItem( 'embeddingModelProvider', ); if ( !chatModel || !chatModelProvider || !embeddingModel || !embeddingModelProvider ) { const providers = await fetch( `${process.env.NEXT_PUBLIC_API_URL}/models`, { headers: { 'Content-Type': 'application/json', }, }, ).then(async (res) => await res.json()); const chatModelProviders = providers.chatModelProviders; const embeddingModelProviders = providers.embeddingModelProviders; if ( !chatModelProviders || Object.keys(chatModelProviders).length === 0 ) return toast.error('No chat models available'); if ( !embeddingModelProviders || Object.keys(embeddingModelProviders).length === 0 ) return toast.error('No embedding models available'); chatModelProvider = Object.keys(chatModelProviders)[0]; chatModel = Object.keys(chatModelProviders[chatModelProvider])[0]; embeddingModelProvider = Object.keys(embeddingModelProviders)[0]; embeddingModel = Object.keys( embeddingModelProviders[embeddingModelProvider], )[0]; localStorage.setItem('chatModel', chatModel!); localStorage.setItem('chatModelProvider', chatModelProvider); localStorage.setItem('embeddingModel', embeddingModel!); localStorage.setItem( 'embeddingModelProvider', embeddingModelProvider, ); } const wsURL = new URL(url); const searchParams = new URLSearchParams({}); searchParams.append('chatModel', chatModel!); searchParams.append('chatModelProvider', chatModelProvider); if (chatModelProvider === 'custom_openai') { searchParams.append( 'openAIApiKey', localStorage.getItem('openAIApiKey')!, ); searchParams.append( 'openAIBaseURL', localStorage.getItem('openAIBaseURL')!, ); } searchParams.append('embeddingModel', embeddingModel!); searchParams.append('embeddingModelProvider', embeddingModelProvider); wsURL.search = searchParams.toString(); const ws = new WebSocket(wsURL.toString()); ws.onopen = () => { console.log('[DEBUG] open'); }; const stateCheckInterval = setInterval(() => { if (ws.readyState === 1) { setIsReady(true); clearInterval(stateCheckInterval); } }, 100); setWs(ws); ws.onmessage = (e) => { const parsedData = JSON.parse(e.data); if (parsedData.type === 'error') { toast.error(parsedData.data); if (parsedData.key === 'INVALID_MODEL_SELECTED') { localStorage.clear(); } } }; }; connectWs(); } return () => { ws?.close(); console.log('[DEBUG] closed'); }; }, [ws, url, setIsReady]); return ws; }; const ChatWindow = () => { const searchParams = useSearchParams(); const initialMessage = searchParams.get('q'); const [isReady, setIsReady] = useState(false); const ws = useSocket(process.env.NEXT_PUBLIC_WS_URL!, setIsReady); const [chatHistory, setChatHistory] = useState<[string, string][]>([]); const [messages, setMessages] = useState([]); const [loading, setLoading] = useState(false); const [messageAppeared, setMessageAppeared] = useState(false); const [focusMode, setFocusMode] = useState('webSearch'); const sendMessage = async (message: string) => { if (loading) return; setLoading(true); setMessageAppeared(false); let sources: Document[] | undefined = undefined; let recievedMessage = ''; let added = false; ws?.send( JSON.stringify({ type: 'message', content: message, focusMode: focusMode, history: [...chatHistory, ['human', message]], }), ); setMessages((prevMessages) => [ ...prevMessages, { content: message, id: Math.random().toString(36).substring(7), role: 'user', createdAt: new Date(), }, ]); const messageHandler = (e: MessageEvent) => { const data = JSON.parse(e.data); if (data.type === 'error') { toast.error(data.data); setLoading(false); return; } if (data.type === 'sources') { sources = data.data; if (!added) { setMessages((prevMessages) => [ ...prevMessages, { content: '', id: data.messageId, role: 'assistant', sources: sources, createdAt: new Date(), }, ]); added = true; } setMessageAppeared(true); } if (data.type === 'message') { if (!added) { setMessages((prevMessages) => [ ...prevMessages, { content: data.data, id: data.messageId, role: 'assistant', sources: sources, createdAt: new Date(), }, ]); added = true; } setMessages((prev) => prev.map((message) => { if (message.id === data.messageId) { return { ...message, content: message.content + data.data }; } return message; }), ); recievedMessage += data.data; setMessageAppeared(true); } if (data.type === 'messageEnd') { setChatHistory((prevHistory) => [ ...prevHistory, ['human', message], ['assistant', recievedMessage], ]); ws?.removeEventListener('message', messageHandler); setLoading(false); } }; ws?.addEventListener('message', messageHandler); }; const rewrite = (messageId: string) => { const index = messages.findIndex((msg) => msg.id === messageId); if (index === -1) return; const message = messages[index - 1]; setMessages((prev) => { return [...prev.slice(0, messages.length > 2 ? index - 1 : 0)]; }); setChatHistory((prev) => { return [...prev.slice(0, messages.length > 2 ? index - 1 : 0)]; }); sendMessage(message.content); }; useEffect(() => { if (isReady && initialMessage) { sendMessage(initialMessage); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [isReady, initialMessage]); return isReady ? (
{messages.length > 0 ? ( <> ) : ( )}
) : (
); }; export default ChatWindow;