'use client'; import { useEffect, useRef, useState } from 'react'; import { Document } from '@langchain/core/documents'; import Navbar from './Navbar'; import Chat from './Chat'; import EmptyChat from './EmptyChat'; import crypto from 'crypto'; import { toast } from 'sonner'; import { useSearchParams } from 'next/navigation'; import { getSuggestions } from '@/lib/actions'; import Error from 'next/error'; export type Message = { messageId: string; chatId: string; createdAt: Date; content: string; role: 'user' | 'assistant'; suggestions?: string[]; sources?: Document[]; }; const fetchSettings = async () => { try { const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/settings`); const settings = await res.json(); if (settings) { localStorage.setItem('chatModelProvider', settings.chatModelProvider); localStorage.setItem('chatModel', settings.chatModel); localStorage.setItem( 'embeddingModelProvider', settings.embeddingModelProvider, ); localStorage.setItem('embeddingModel', settings.embeddingModel); localStorage.setItem('openAIApiKey', settings.openAIApiKey); localStorage.setItem('openAIBaseURL', settings.openAIBaseURL); } } catch (err) { console.error('Failed to fetch settings:', err); } }; export interface File { fileName: string; fileExtension: string; fileId: string; } const useSocket = ( url: string, setIsWSReady: (ready: boolean) => void, setError: (error: boolean) => void, hasError: boolean, ) => { const [ws, setWs] = useState(null); const reconnectTimeout = useRef(0); const reconnectAttempts = useRef(0); useEffect(() => { if (!ws) { const connectWs = async () => { await fetchSettings(); let chatModel = localStorage.getItem('chatModel'); let chatModelProvider = localStorage.getItem('chatModelProvider'); let embeddingModel = localStorage.getItem('embeddingModel'); let embeddingModelProvider = localStorage.getItem( 'embeddingModelProvider', ); const providers = await fetch( `${process.env.NEXT_PUBLIC_API_URL}/models`, { headers: { 'Content-Type': 'application/json', }, }, ).then(async (res) => await res.json()); if ( !chatModel || !chatModelProvider || !embeddingModel || !embeddingModelProvider ) { if (!chatModel || !chatModelProvider) { const chatModelProviders = providers.chatModelProviders; chatModelProvider = Object.keys(chatModelProviders)[0]; if (chatModelProvider === 'custom_openai') { toast.error( 'Seems like you are using the custom OpenAI provider, please open the settings and configure the API key and base URL', ); setError(true); return; } else { chatModel = Object.keys(chatModelProviders[chatModelProvider])[0]; if ( !chatModelProviders || Object.keys(chatModelProviders).length === 0 ) return toast.error('No chat models available'); } } if (!embeddingModel || !embeddingModelProvider) { const embeddingModelProviders = providers.embeddingModelProviders; if ( !embeddingModelProviders || Object.keys(embeddingModelProviders).length === 0 ) return toast.error('No embedding models available'); 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, ); } else { const chatModelProviders = providers.chatModelProviders; const embeddingModelProviders = providers.embeddingModelProviders; if ( Object.keys(chatModelProviders).length > 0 && !chatModelProviders[chatModelProvider] ) { chatModelProvider = Object.keys(chatModelProviders)[0]; localStorage.setItem('chatModelProvider', chatModelProvider); } if ( chatModelProvider && chatModelProvider != 'custom_openai' && !chatModelProviders[chatModelProvider][chatModel] ) { chatModel = Object.keys(chatModelProviders[chatModelProvider])[0]; localStorage.setItem('chatModel', chatModel); } if ( Object.keys(embeddingModelProviders).length > 0 && !embeddingModelProviders[embeddingModelProvider] ) { embeddingModelProvider = Object.keys(embeddingModelProviders)[0]; localStorage.setItem( 'embeddingModelProvider', embeddingModelProvider, ); } if ( embeddingModelProvider && !embeddingModelProviders[embeddingModelProvider][embeddingModel] ) { embeddingModel = Object.keys( embeddingModelProviders[embeddingModelProvider], )[0]; localStorage.setItem('embeddingModel', embeddingModel); } } 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()); const timeoutId = setTimeout(() => { if (ws.readyState !== 1) { toast.error( 'Failed to connect to the server. Please try again later.', ); } }, 10000); ws.addEventListener('message', (e) => { const data = JSON.parse(e.data); if (data.type === 'signal' && data.data === 'open') { const interval = setInterval(() => { if (ws.readyState === 1) { reconnectTimeout.current = 0; setIsWSReady(true); clearInterval(interval); } }, 5); clearTimeout(timeoutId); reconnectTimeout.current = 0; reconnectAttempts.current = 0; console.log('[DEBUG] opened'); } if (data.type === 'error') { toast.error(data.data); } }); ws.onerror = () => { clearTimeout(timeoutId); setError(true); toast.error('WebSocket connection error.'); }; ws.onclose = () => { clearTimeout(timeoutId); if (!hasError && reconnectAttempts.current < 3) { setWs(null); // forces websocket to reopen when needed. } console.log('[DEBUG] closed'); }; setWs(ws); }; if (reconnectAttempts.current < 3) { console.log( `[DEBUG] Attempting to reconnect (${reconnectAttempts.current + 1}/3)`, ); setTimeout(connectWs, reconnectTimeout.current); reconnectTimeout.current = reconnectTimeout.current > 0 ? reconnectTimeout.current * 2 : 1000; reconnectAttempts.current += 1; } else { console.log('[DEBUG] WebSocket reconnect failure after 3 retries'); setError(true); } } }, [ws, url, setIsWSReady, setError]); return ws; }; const loadMessages = async ( chatId: string, setMessages: (messages: Message[]) => void, setIsMessagesLoaded: (loaded: boolean) => void, setChatHistory: (history: [string, string][]) => void, setFocusMode: (mode: string) => void, setNotFound: (notFound: boolean) => void, setFiles: (files: File[]) => void, setFileIds: (fileIds: string[]) => void, ) => { const res = await fetch( `${process.env.NEXT_PUBLIC_API_URL}/chats/${chatId}`, { method: 'GET', headers: { 'Content-Type': 'application/json', }, }, ); if (res.status === 404) { setNotFound(true); setIsMessagesLoaded(true); return; } const data = await res.json(); const messages = data.messages.map((msg: any) => { return { ...msg, ...JSON.parse(msg.metadata), }; }) as Message[]; setMessages(messages); const history = messages.map((msg) => { return [msg.role, msg.content]; }) as [string, string][]; console.log('[DEBUG] messages loaded'); document.title = messages[0].content; const files = data.chat.files.map((file: any) => { return { fileName: file.name, fileExtension: file.name.split('.').pop(), fileId: file.fileId, }; }); setFiles(files); setFileIds(files.map((file: File) => file.fileId)); setChatHistory(history); setFocusMode(data.chat.focusMode); setIsMessagesLoaded(true); }; const ChatWindow = ({ id }: { id?: string }) => { const searchParams = useSearchParams(); const initialMessage = searchParams.get('q'); const [chatId, setChatId] = useState(id); const [newChatCreated, setNewChatCreated] = useState(false); const [hasError, setHasError] = useState(false); const [isReady, setIsReady] = useState(false); const [isWSReady, setIsWSReady] = useState(false); const ws = useSocket( process.env.NEXT_PUBLIC_WS_URL!, setIsWSReady, setHasError, hasError, ); const [loading, setLoading] = useState(false); const [messageAppeared, setMessageAppeared] = useState(false); const [chatHistory, setChatHistory] = useState<[string, string][]>([]); const [messages, setMessages] = useState([]); const [files, setFiles] = useState([]); const [fileIds, setFileIds] = useState([]); const [focusMode, setFocusMode] = useState('webSearch'); const [optimizationMode, setOptimizationMode] = useState('speed'); const [isMessagesLoaded, setIsMessagesLoaded] = useState(false); const [notFound, setNotFound] = useState(false); useEffect(() => { if ( chatId && !newChatCreated && !isMessagesLoaded && messages.length === 0 ) { loadMessages( chatId, setMessages, setIsMessagesLoaded, setChatHistory, setFocusMode, setNotFound, setFiles, setFileIds, ); } else if (!chatId) { setNewChatCreated(true); setIsMessagesLoaded(true); setChatId(crypto.randomBytes(20).toString('hex')); } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); useEffect(() => { return () => { if (ws?.readyState === 1) { ws.close(); console.log('[DEBUG] closed'); } }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const messagesRef = useRef([]); useEffect(() => { fetchSettings(); }, []); useEffect(() => { messagesRef.current = messages; }, [messages]); useEffect(() => { if (isMessagesLoaded && isWSReady) { setIsReady(true); console.log('[DEBUG] ready'); } }, [isMessagesLoaded, isWSReady]); const sendMessage = async (message: string, messageId?: string) => { if (loading) return; setLoading(true); setMessageAppeared(false); let sources: Document[] | undefined = undefined; let recievedMessage = ''; let added = false; messageId = messageId ?? crypto.randomBytes(7).toString('hex'); ws?.send( JSON.stringify({ type: 'message', message: { messageId: messageId, chatId: chatId!, content: message, }, files: fileIds, focusMode: focusMode, optimizationMode: optimizationMode, history: [...chatHistory, ['human', message]], }), ); setMessages((prevMessages) => [ ...prevMessages, { content: message, messageId: messageId, chatId: chatId!, role: 'user', createdAt: new Date(), }, ]); const messageHandler = async (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: '', messageId: data.messageId, chatId: chatId!, role: 'assistant', sources: sources, createdAt: new Date(), }, ]); added = true; } setMessageAppeared(true); } if (data.type === 'message') { if (!added) { setMessages((prevMessages) => [ ...prevMessages, { content: data.data, messageId: data.messageId, chatId: chatId!, role: 'assistant', sources: sources, createdAt: new Date(), }, ]); added = true; } setMessages((prev) => prev.map((message) => { if (message.messageId === 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); const lastMsg = messagesRef.current[messagesRef.current.length - 1]; if ( lastMsg.role === 'assistant' && lastMsg.sources && lastMsg.sources.length > 0 && !lastMsg.suggestions ) { const suggestions = await getSuggestions(messagesRef.current); setMessages((prev) => prev.map((msg) => { if (msg.messageId === lastMsg.messageId) { return { ...msg, suggestions: suggestions }; } return msg; }), ); } } }; ws?.addEventListener('message', messageHandler); }; const rewrite = (messageId: string) => { const index = messages.findIndex((msg) => msg.messageId === 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, message.messageId); }; useEffect(() => { if (isReady && initialMessage && ws?.readyState === 1) { sendMessage(initialMessage); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [ws?.readyState, isReady, initialMessage, isWSReady]); if (hasError) { return (

Failed to connect to the server. Please try again later.

); } return isReady ? ( notFound ? ( ) : (
{messages.length > 0 ? ( <> ) : ( )}
) ) : (
); }; export default ChatWindow;