'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 useSocket = ( url: string, setIsWSReady: (ready: boolean) => void, setError: (error: 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', ) 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.onopen = () => { console.log('[DEBUG] open') clearTimeout(timeoutId) setIsWSReady(true) } ws.onerror = () => { clearTimeout(timeoutId) setError(true) toast.error('WebSocket connection error.') } ws.onclose = () => { clearTimeout(timeoutId) setError(true) console.log('[DEBUG] closed') } ws.addEventListener('message', e => { const data = JSON.parse(e.data) if (data.type === 'error') { toast.error(data.data) } }) setWs(ws) } connectWs() } }, [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, ) => { 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 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, ) const [loading, setLoading] = useState(false) const [messageAppeared, setMessageAppeared] = useState(false) const [chatHistory, setChatHistory] = useState<[string, string][]>([]) const [messages, setMessages] = useState([]) const [focusMode, setFocusMode] = useState('webSearch') 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, ) } 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') } } }, []) const messagesRef = useRef([]) useEffect(() => { messagesRef.current = messages }, [messages]) useEffect(() => { if (isMessagesLoaded && isWSReady) { setIsReady(true) } }, [isMessagesLoaded, isWSReady]) const sendMessage = async (message: string) => { if (loading) return setLoading(true) setMessageAppeared(false) let sources: Document[] | undefined = undefined let recievedMessage = '' let added = false const messageId = crypto.randomBytes(7).toString('hex') ws?.send( JSON.stringify({ type: 'message', message: { chatId: chatId!, content: message, }, focusMode: focusMode, 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) } useEffect(() => { if (isReady && initialMessage) { sendMessage(initialMessage) } // eslint-disable-next-line react-hooks/exhaustive-deps }, [isReady, initialMessage]) if (hasError) { return (

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

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