import { useState, useEffect, useContext } from "react"; import ChatHistory from "./ChatHistory"; import { CLEAR_ATTACHMENTS_EVENT, DndUploaderContext } from "./DnDWrapper"; import PromptInput, { PROMPT_INPUT_EVENT, PROMPT_INPUT_ID, } from "./PromptInput"; import Workspace from "@/models/workspace"; import handleChat, { ABORT_STREAM_EVENT } from "@/utils/chat"; import { isMobile } from "react-device-detect"; import { SidebarMobileHeader } from "../../Sidebar"; import { useParams } from "react-router-dom"; import { v4 } from "uuid"; import handleSocketResponse, { websocketURI, AGENT_SESSION_END, AGENT_SESSION_START, } from "@/utils/chat/agent"; import DnDFileUploaderWrapper from "./DnDWrapper"; import SpeechRecognition, { useSpeechRecognition, } from "react-speech-recognition"; import { ChatTooltips } from "./ChatTooltips"; import { MetricsProvider } from "./ChatHistory/HistoricalMessage/Actions/RenderMetrics"; export default function ChatContainer({ workspace, knownHistory = [] }) { const { threadSlug = null } = useParams(); const [message, setMessage] = useState(""); const [loadingResponse, setLoadingResponse] = useState(false); const [chatHistory, setChatHistory] = useState(knownHistory); const [socketId, setSocketId] = useState(null); const [websocket, setWebsocket] = useState(null); const { files, parseAttachments } = useContext(DndUploaderContext); // Maintain state of message from whatever is in PromptInput const handleMessageChange = (event) => { setMessage(event.target.value); }; const { listening, resetTranscript } = useSpeechRecognition({ clearTranscriptOnListen: true, }); /** * Emit an update to the state of the prompt input without directly * passing a prop in so that it does not re-render constantly. * @param {string} messageContent - The message content to set * @param {'replace' | 'append'} writeMode - Replace current text or append to existing text (default: replace) */ function setMessageEmit(messageContent = "", writeMode = "replace") { if (writeMode === "append") setMessage((prev) => prev + messageContent); else setMessage(messageContent ?? ""); // Push the update to the PromptInput component (same logic as above to keep in sync) window.dispatchEvent( new CustomEvent(PROMPT_INPUT_EVENT, { detail: { messageContent, writeMode }, }) ); } const handleSubmit = async (event) => { event.preventDefault(); if (!message || message === "") return false; const prevChatHistory = [ ...chatHistory, { content: message, role: "user", attachments: parseAttachments(), }, { content: "", role: "assistant", pending: true, userMessage: message, animate: true, }, ]; if (listening) { // Stop the mic if the send button is clicked endSTTSession(); } setChatHistory(prevChatHistory); setMessageEmit(""); setLoadingResponse(true); }; function endSTTSession() { SpeechRecognition.stopListening(); resetTranscript(); } const regenerateAssistantMessage = (chatId) => { const updatedHistory = chatHistory.slice(0, -1); const lastUserMessage = updatedHistory.slice(-1)[0]; Workspace.deleteChats(workspace.slug, [chatId]) .then(() => sendCommand({ text: lastUserMessage.content, autoSubmit: true, history: updatedHistory, attachments: lastUserMessage?.attachments, }) ) .catch((e) => console.error(e)); }; /** * Send a command to the LLM prompt input. * @param {Object} options - Arguments to send to the LLM * @param {string} options.text - The text to send to the LLM * @param {boolean} options.autoSubmit - Determines if the text should be sent immediately or if it should be added to the message state (default: false) * @param {Object[]} options.history - The history of the chat prior to this message for overriding the current chat history * @param {Object[import("./DnDWrapper").Attachment]} options.attachments - The attachments to send to the LLM for this message * @param {'replace' | 'append'} options.writeMode - Replace current text or append to existing text (default: replace) * @returns {void} */ const sendCommand = async ({ text = "", autoSubmit = false, history = [], attachments = [], writeMode = "replace", } = {}) => { // If we are not auto-submitting, we can just emit the text to the prompt input. if (!autoSubmit) { setMessageEmit(text, writeMode); return; } // If we are auto-submitting in append mode // than we need to update text with whatever is in the prompt input + the text we are sending. // @note: `message` will not work here since it is not updated yet. // If text is still empty, after this, then we should just return. if (writeMode === "append") { const currentText = document.getElementById(PROMPT_INPUT_ID)?.value; text = currentText + text; } if (!text || text === "") return false; // If we are auto-submitting // Then we can replace the current text since this is not accumulating. let prevChatHistory; if (history.length > 0) { // use pre-determined history chain. prevChatHistory = [ ...history, { content: "", role: "assistant", pending: true, userMessage: text, attachments, animate: true, }, ]; } else { prevChatHistory = [ ...chatHistory, { content: text, role: "user", attachments, }, { content: "", role: "assistant", pending: true, userMessage: text, animate: true, }, ]; } setChatHistory(prevChatHistory); setMessageEmit(""); setLoadingResponse(true); }; useEffect(() => { async function fetchReply() { const promptMessage = chatHistory.length > 0 ? chatHistory[chatHistory.length - 1] : null; const remHistory = chatHistory.length > 0 ? chatHistory.slice(0, -1) : []; var _chatHistory = [...remHistory]; // Override hook for new messages to now go to agents until the connection closes if (!!websocket) { if (!promptMessage || !promptMessage?.userMessage) return false; window.dispatchEvent(new CustomEvent(CLEAR_ATTACHMENTS_EVENT)); websocket.send( JSON.stringify({ type: "awaitingFeedback", feedback: promptMessage?.userMessage, }) ); return; } if (!promptMessage || !promptMessage?.userMessage) return false; // If running and edit or regeneration, this history will already have attachments // so no need to parse the current state. const attachments = promptMessage?.attachments ?? parseAttachments(); window.dispatchEvent(new CustomEvent(CLEAR_ATTACHMENTS_EVENT)); await Workspace.multiplexStream({ workspaceSlug: workspace.slug, threadSlug, prompt: promptMessage.userMessage, chatHandler: (chatResult) => handleChat( chatResult, setLoadingResponse, setChatHistory, remHistory, _chatHistory, setSocketId ), attachments, }); return; } loadingResponse === true && fetchReply(); }, [loadingResponse, chatHistory, workspace]); // TODO: Simplify this WSS stuff useEffect(() => { function handleWSS() { try { if (!socketId || !!websocket) return; const socket = new WebSocket( `${websocketURI()}/api/agent-invocation/${socketId}` ); window.addEventListener(ABORT_STREAM_EVENT, () => { window.dispatchEvent(new CustomEvent(AGENT_SESSION_END)); websocket.close(); }); socket.addEventListener("message", (event) => { setLoadingResponse(true); try { handleSocketResponse(event, setChatHistory); } catch (e) { console.error("Failed to parse data"); window.dispatchEvent(new CustomEvent(AGENT_SESSION_END)); socket.close(); } setLoadingResponse(false); }); socket.addEventListener("close", (_event) => { window.dispatchEvent(new CustomEvent(AGENT_SESSION_END)); setChatHistory((prev) => [ ...prev.filter((msg) => !!msg.content), { uuid: v4(), type: "statusResponse", content: "Agent session complete.", role: "assistant", sources: [], closed: true, error: null, animate: false, pending: false, }, ]); setLoadingResponse(false); setWebsocket(null); setSocketId(null); }); setWebsocket(socket); window.dispatchEvent(new CustomEvent(AGENT_SESSION_START)); window.dispatchEvent(new CustomEvent(CLEAR_ATTACHMENTS_EVENT)); } catch (e) { setChatHistory((prev) => [ ...prev.filter((msg) => !!msg.content), { uuid: v4(), type: "abort", content: e.message, role: "assistant", sources: [], closed: true, error: e.message, animate: false, pending: false, }, ]); setLoadingResponse(false); setWebsocket(null); setSocketId(null); } } handleWSS(); }, [socketId]); return (