"use client"; import { AnimatePresence } from "framer-motion"; import React, { useEffect, useRef, useState } from "react"; import QueryInput from "../home/queryinput"; 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"; import { SendIcon } from "lucide-react"; function ChatWindow({ q, spaces, initialChat = [ { question: q, answer: { parts: [], sources: [], }, }, ], threadId, }: { q: string; spaces: { id: string; name: string }[]; initialChat?: ChatHistory[]; threadId: string; }) { const [layout, setLayout] = useState<"chat" | "initial">( initialChat.length > 1 ? "chat" : "initial", ); 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[]) => { const sourcesFetch = await fetch( `/api/chat?q=${query}&spaces=${spaces}&sourcesOnly=true&threadId=${threadId}`, { 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", }); 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, })); resolve(newChatHistory); return newChatHistory; }); }); // Step 2: Fetch data from the API const resp = await fetch( `/api/chat?q=${query}&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}`), ); } } else { router.push("/home"); } }, []); return (
{layout === "initial" ? (
{}} initialQuery={q} initialSpaces={[]} disabled />
) : (
{chatHistory.map((chat, idx) => (

{chat.question}

{/* Related memories */}
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}
))}
{/* Summary */}
Summary
{/* 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 */}
{/* Justification */} {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: [], }, }, ]; }); await getAnswer( q, spaces.map((s) => `${s.id}`), ); }} />
)}
); } export default ChatWindow;