"use client"; import { AnimatePresence } from "framer-motion"; import React, { useEffect, useRef, useState } from "react"; import QueryInput from "./chatQueryInput"; import { cn } from "@repo/ui/lib/utils"; import { motion } from "framer-motion"; import { useRouter } from "next/navigation"; import { ChatHistory, sourcesZod } from "@repo/shared-types"; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, } from "@repo/ui/shadcn/accordion"; import Markdown from "react-markdown"; import remarkGfm from "remark-gfm"; import remarkMath from "remark-math"; import rehypeKatex from "rehype-katex"; import rehypeHighlight from "rehype-highlight"; import { code, p } from "./markdownRenderHelpers"; import { codeLanguageSubset } from "@/lib/constants"; import { toast } from "sonner"; import Link from "next/link"; import { createChatObject } from "@/app/actions/doers"; import { ClipboardIcon } from "@heroicons/react/24/outline"; function ChatWindow({ q, spaces, initialChat = [ { question: q, answer: { parts: [], sources: [], }, proModeProcessing: { queries: [], }, }, ], threadId, proMode, }: { q: string; spaces: { id: number; name: string }[]; initialChat?: ChatHistory[]; threadId: string; proMode: boolean; }) { const [layout, setLayout] = useState<"chat" | "initial">("chat"); const [chatHistory, setChatHistory] = useState(initialChat); const removeJustificationFromText = (text: string) => { // remove everything after the first "" word const justificationLine = text.indexOf(""); if (justificationLine !== -1) { // Add that justification to the last chat message const lastChatMessage = chatHistory[chatHistory.length - 1]; if (lastChatMessage) { lastChatMessage.answer.justification = text.slice(justificationLine); } return text.slice(0, justificationLine); } return text; }; const router = useRouter(); const getAnswer = async ( query: string, spaces: string[], proMode: boolean = false, ) => { if (query.trim() === "from_loading" || query.trim().length === 0) { return; } const sourcesFetch = await fetch( `/api/chat?q=${query}&spaces=${spaces}&sourcesOnly=true&threadId=${threadId}&proMode=${proMode}`, { method: "POST", body: JSON.stringify({ chatHistory }), }, ); // TODO: handle this properly const sources = await sourcesFetch.json(); const sourcesParsed = sourcesZod.safeParse(sources); if (!sourcesParsed.success) { console.error(sourcesParsed.error); toast.error("Something went wrong while getting the sources"); return; } window.scrollTo({ top: document.documentElement.scrollHeight, behavior: "smooth", }); let proModeListedQueries: string[] = []; const updateChatHistoryAndFetch = async () => { // Step 1: Update chat history with the assistant's response await new Promise((resolve) => { setChatHistory((prevChatHistory) => { const newChatHistory = [...prevChatHistory]; const lastAnswer = newChatHistory[newChatHistory.length - 1]; if (!lastAnswer) { resolve(undefined); return prevChatHistory; } const filteredSourceUrls = new Set( sourcesParsed.data.metadata.map((source) => source.url), ); const uniqueSources = sourcesParsed.data.metadata.filter((source) => { if (filteredSourceUrls.has(source.url)) { filteredSourceUrls.delete(source.url); return true; } return false; }); lastAnswer.answer.sources = uniqueSources.map((source) => ({ title: source.title ?? "Untitled", type: source.type ?? "page", source: source.url ?? "https://supermemory.ai", content: source.description ?? "No content available", numChunks: sourcesParsed.data.metadata.filter( (f) => f.url === source.url, ).length, })); lastAnswer.proModeProcessing.queries = sourcesParsed.data.proModeListedQueries ?? []; proModeListedQueries = lastAnswer.proModeProcessing.queries; resolve(newChatHistory); return newChatHistory; }); }); // Step 2: Fetch data from the API const resp = await fetch( `/api/chat?q=${(query += proModeListedQueries.join(" "))}&spaces=${spaces}&threadId=${threadId}`, { method: "POST", body: JSON.stringify({ chatHistory, sources: sourcesParsed.data }), }, ); // Step 3: Read the response stream and update the chat history const reader = resp.body?.getReader(); let done = false; while (!done && reader) { const { value, done: d } = await reader.read(); if (d) { setChatHistory((prevChatHistory) => { createChatObject(threadId, prevChatHistory); return prevChatHistory; }); } done = d; const txt = new TextDecoder().decode(value); setChatHistory((prevChatHistory) => { const newChatHistory = [...prevChatHistory]; const lastAnswer = newChatHistory[newChatHistory.length - 1]; if (!lastAnswer) return prevChatHistory; window.scrollTo({ top: document.documentElement.scrollHeight, behavior: "smooth", }); lastAnswer.answer.parts.push({ text: txt }); return newChatHistory; }); } }; updateChatHistoryAndFetch(); }; useEffect(() => { if (q.trim().length > 0 || chatHistory.length > 0) { setLayout("chat"); const lastChat = chatHistory.length > 0 ? chatHistory.length - 1 : 0; const startGenerating = chatHistory[lastChat]?.answer.parts[0]?.text ? false : true; if (startGenerating) { getAnswer( q, spaces.map((s) => `${s.id}`), proMode, ); } } else { router.push("/home"); } }, []); return (
{layout === "initial" ? (
{}} initialSpaces={[]} initialQuery={q} />
) : (
{chatHistory.map((chat, idx) => (

{chat.question}

{chat.proModeProcessing?.queries?.length > 0 && (
Pro Mode
{chat.proModeProcessing.queries.map( (query, idx) => (
{query}
), )}
)}
Answer
{/* Loading state */} {(chat.answer.parts.length === 0 || chat.answer.parts.join("").length === 0) && (
)} {removeJustificationFromText( chat.answer.parts .map((part) => part.text) .join(""), )}
{/* TODO: speak response */} {/* */} {/* copy response */}
0 || chat.answer.parts.length === 0 ? "flex" : "hidden"}`} > Related Memories {/* TODO: fade out content on the right side, the fade goes away when the user scrolls */}
{/* Loading state */} {chat.answer.sources.length > 0 || (chat.answer.parts.length === 0 && ( <> {[1, 2, 3, 4].map((_, idx) => (
))} ))} {chat.answer.sources.map((source, idx) => (
{source.type} {source.numChunks > 1 && ( {source.numChunks} chunks )}
{source.title}
{source.content.length > 100 ? source.content.slice(0, 100) + "..." : source.content}
))}
{chat.answer.justification && chat.answer.justification.length && (
0 ? "flex" : "hidden"}`} > Justification {chat.answer.justification.length > 0 ? chat.answer.justification .replaceAll( "", "", ) .replaceAll( "", "", ) : "No justification provided."}
)}
))}
{ setChatHistory((prevChatHistory) => { return [ ...prevChatHistory, { question: q, answer: { parts: [], sources: [], }, proModeProcessing: { queries: [], }, }, ]; }); await getAnswer( q, spaces.map((s) => `${s.id}`), ); }} />
)}
); } export default ChatWindow;