/* eslint-disable @typescript-eslint/no-explicit-any */ "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 "node: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"); 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 searchParameters = new URLSearchParams({}); searchParameters.append("chatModel", chatModel!); searchParameters.append("chatModelProvider", chatModelProvider); if (chatModelProvider === "custom_openai") { searchParameters.append("openAIApiKey", localStorage.getItem("openAIApiKey")!); searchParameters.append("openAIBaseURL", localStorage.getItem("openAIBaseURL")!); } searchParameters.append("embeddingModel", embeddingModel!); searchParameters.append("embeddingModelProvider", embeddingModelProvider); wsURL.search = searchParameters.toString(); const ws = new WebSocket(wsURL.toString()); const timeoutId = setTimeout(() => { if (ws.readyState !== 1) { ws.close(); setError(true); toast.error("Failed to connect to the server. Please try again later."); } }, 10_000); ws.addEventListener("open", () => { console.log("[DEBUG] open"); clearTimeout(timeoutId); setError(false); setIsWSReady(true); }); // eslint-disable-next-line unicorn/prefer-add-event-listener ws.onerror = () => { clearTimeout(timeoutId); setError(true); toast.error("WebSocket connection error."); }; ws.addEventListener("close", () => { clearTimeout(timeoutId); setError(true); console.log("[DEBUG] closed"); }); setWs(ws); }; connectWs(); } return () => { ws?.close(); console.log("[DEBUG] closed"); }; }, [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((message: any) => { return { ...message, ...JSON.parse(message.metadata), }; }) as Message[]; setMessages(messages); const history = messages.map(message => { return [message.role, message.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 searchParameters = useSearchParams(); const initialMessage = searchParameters.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 }, []); const messagesReference = useRef([]); useEffect(() => { messagesReference.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; 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(previousMessages => [ ...previousMessages, { 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(previousMessages => [ ...previousMessages, { content: "", messageId: data.messageId, chatId: chatId!, role: "assistant", sources: sources, createdAt: new Date(), }, ]); added = true; } setMessageAppeared(true); } if (data.type === "message") { if (!added) { setMessages(previousMessages => [ ...previousMessages, { content: data.data, messageId: data.messageId, chatId: chatId!, role: "assistant", sources: sources, createdAt: new Date(), }, ]); added = true; } setMessages(previous => previous.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(previousHistory => [...previousHistory, ["human", message], ["assistant", recievedMessage]]); ws?.removeEventListener("message", messageHandler); setLoading(false); const lastMessage = messagesReference.current.at(-1); if ( lastMessage && lastMessage.role === "assistant" && lastMessage.sources && lastMessage.sources.length > 0 && !lastMessage.suggestions ) { const suggestions = await getSuggestions(messagesReference.current); setMessages(previous => previous.map(message_ => { if (message_.messageId === lastMessage.messageId) { return { ...message_, suggestions: suggestions }; } return message_; }), ); } } }; ws?.addEventListener("message", messageHandler); }; const rewrite = (messageId: string) => { const index = messages.findIndex(message_ => message_.messageId === messageId); if (index === -1) return; const message = messages[index - 1]; setMessages(previous => { // eslint-disable-next-line unicorn/no-useless-spread return [...previous.slice(0, messages.length > 2 ? index - 1 : 0)]; }); setChatHistory(previous => { // eslint-disable-next-line unicorn/no-useless-spread return [...previous.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;