aboutsummaryrefslogtreecommitdiff
path: root/apps/web/app/(dash)/chat/chatWindow.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'apps/web/app/(dash)/chat/chatWindow.tsx')
-rw-r--r--apps/web/app/(dash)/chat/chatWindow.tsx249
1 files changed, 236 insertions, 13 deletions
diff --git a/apps/web/app/(dash)/chat/chatWindow.tsx b/apps/web/app/(dash)/chat/chatWindow.tsx
index 43c337ee..77c1f32b 100644
--- a/apps/web/app/(dash)/chat/chatWindow.tsx
+++ b/apps/web/app/(dash)/chat/chatWindow.tsx
@@ -6,29 +6,147 @@ import QueryInput from "../home/queryinput";
import { cn } from "@repo/ui/lib/utils";
import { motion } from "framer-motion";
import { useRouter } from "next/navigation";
+import { ChatHistory } 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 { z } from "zod";
+import { toast } from "sonner";
+import Link from "next/link";
+import { sources } from "next/dist/compiled/webpack/webpack";
-function ChatWindow({ q }: { q: string }) {
+function ChatWindow({
+ q,
+ spaces,
+}: {
+ q: string;
+ spaces: { id: string; name: string }[];
+}) {
const [layout, setLayout] = useState<"chat" | "initial">("initial");
+ const [chatHistory, setChatHistory] = useState<ChatHistory[]>([
+ {
+ question: q,
+ answer: {
+ parts: [
+ // {
+ // text: `It seems like there might be a typo in your question. Could you please clarify or provide more context? If you meant "interesting," please let me know what specific information or topic you find interesting, and I can help you with that.`,
+ // },
+ ],
+ sources: [],
+ },
+ },
+ ]);
const router = useRouter();
+ const getAnswer = async (query: string, spaces: string[]) => {
+ const sourcesFetch = await fetch(
+ `/api/chat?q=${query}&spaces=${spaces}&sourcesOnly=true`,
+ {
+ method: "POST",
+ body: JSON.stringify({ chatHistory }),
+ },
+ );
+
+ // TODO: handle this properly
+ const sources = await sourcesFetch.json();
+
+ const sourcesZod = z.object({
+ ids: z.array(z.string()),
+ metadata: z.array(z.any()),
+ });
+
+ const sourcesParsed = sourcesZod.safeParse(sources);
+
+ if (!sourcesParsed.success) {
+ console.log(sources);
+ console.error(sourcesParsed.error);
+ toast.error("Something went wrong while getting the sources");
+ return;
+ }
+
+ setChatHistory((prevChatHistory) => {
+ const newChatHistory = [...prevChatHistory];
+ const lastAnswer = newChatHistory[newChatHistory.length - 1];
+ if (!lastAnswer) 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.content ?? "No content available",
+ numChunks: sourcesParsed.data.metadata.filter(
+ (f) => f.url === source.url,
+ ).length,
+ }));
+ return newChatHistory;
+ });
+
+ const resp = await fetch(`/api/chat?q=${query}&spaces=${spaces}`, {
+ method: "POST",
+ body: JSON.stringify({ chatHistory }),
+ });
+
+ const reader = resp.body?.getReader();
+ let done = false;
+ let result = "";
+ while (!done && reader) {
+ const { value, done: d } = await reader.read();
+ done = d;
+
+ setChatHistory((prevChatHistory) => {
+ const newChatHistory = [...prevChatHistory];
+ const lastAnswer = newChatHistory[newChatHistory.length - 1];
+ if (!lastAnswer) return prevChatHistory;
+ lastAnswer.answer.parts.push({ text: new TextDecoder().decode(value) });
+ return newChatHistory;
+ });
+ }
+
+ console.log(result);
+ };
+
useEffect(() => {
- if (q !== "") {
+ if (q.trim().length > 0) {
+ getAnswer(
+ q,
+ spaces.map((s) => s.id),
+ );
setTimeout(() => {
setLayout("chat");
}, 300);
} else {
router.push("/home");
}
- }, [q]);
+ }, []);
+
return (
- <div>
+ <div className="h-full">
<AnimatePresence mode="popLayout">
{layout === "initial" ? (
<motion.div
exit={{ opacity: 0 }}
key="initial"
- className="max-w-3xl flex mx-auto w-full flex-col"
+ className="max-w-3xl h-full justify-center items-center flex mx-auto w-full flex-col"
>
<div className="w-full h-96">
<QueryInput initialQuery={q} initialSpaces={[]} disabled />
@@ -36,16 +154,121 @@ function ChatWindow({ q }: { q: string }) {
</motion.div>
) : (
<div
- className="max-w-3xl flex mx-auto w-full flex-col mt-8"
+ className="max-w-3xl flex mx-auto w-full flex-col mt-24"
key="chat"
>
- <h2
- className={cn(
- "transition-all transform translate-y-0 opacity-100 duration-500 ease-in-out font-semibold text-2xl",
- )}
- >
- {q}
- </h2>
+ {chatHistory.map((chat, idx) => (
+ <div
+ key={idx}
+ className={`mt-8 ${idx != chatHistory.length - 1 ? "pb-2 border-b" : ""}`}
+ >
+ <h2
+ className={cn(
+ "text-white transition-all transform translate-y-0 opacity-100 duration-500 ease-in-out font-semibold text-2xl",
+ )}
+ >
+ {chat.question}
+ </h2>
+
+ <div className="flex flex-col gap-2 mt-2">
+ <div
+ className={`${chat.answer.sources.length > 0 || chat.answer.parts.length === 0 ? "flex" : "hidden"}`}
+ >
+ <Accordion
+ defaultValue={
+ idx === chatHistory.length - 1 ? "memories" : ""
+ }
+ type="single"
+ collapsible
+ >
+ <AccordionItem value="memories">
+ <AccordionTrigger className="text-foreground-menu">
+ Related Memories
+ </AccordionTrigger>
+ {/* TODO: fade out content on the right side, the fade goes away when the user scrolls */}
+ <AccordionContent
+ className="relative flex gap-2 max-w-3xl overflow-auto no-scrollbar"
+ defaultChecked
+ >
+ {/* Loading state */}
+ {chat.answer.sources.length > 0 ||
+ (chat.answer.parts.length === 0 && (
+ <>
+ {[1, 2, 3, 4].map((_, idx) => (
+ <div
+ key={`loadingState-${idx}`}
+ className="rounded-xl bg-secondary p-4 flex flex-col gap-2 min-w-72 animate-pulse"
+ >
+ <div className="bg-slate-700 h-2 rounded-full w-1/2"></div>
+ <div className="bg-slate-700 h-2 rounded-full w-full"></div>
+ </div>
+ ))}
+ </>
+ ))}
+ {chat.answer.sources.map((source, idx) => (
+ <Link
+ href={source.source}
+ key={idx}
+ className="rounded-xl bg-secondary p-4 flex flex-col gap-2 min-w-72"
+ >
+ <div className="flex justify-between text-foreground-menu text-sm">
+ <span>{source.type}</span>
+
+ {source.numChunks > 1 && (
+ <span>{source.numChunks} chunks</span>
+ )}
+ </div>
+ <div className="text-base">{source.title}</div>
+ <div className="text-xs">
+ {source.content.length > 100
+ ? source.content.slice(0, 100) + "..."
+ : source.content}
+ </div>
+ </Link>
+ ))}
+ </AccordionContent>
+ </AccordionItem>
+ </Accordion>
+ </div>
+
+ {/* Summary */}
+ <div>
+ <div className="text-foreground-menu py-2">Summary</div>
+ <div className="text-base">
+ {chat.answer.parts.length === 0 && (
+ <div className="animate-pulse flex space-x-4">
+ <div className="flex-1 space-y-3 py-1">
+ <div className="h-2 bg-slate-700 rounded"></div>
+ <div className="h-2 bg-slate-700 rounded"></div>
+ </div>
+ </div>
+ )}
+ <Markdown
+ remarkPlugins={[remarkGfm, [remarkMath]]}
+ rehypePlugins={[
+ rehypeKatex,
+ [
+ rehypeHighlight,
+ {
+ detect: true,
+ ignoreMissing: true,
+ subset: codeLanguageSubset,
+ },
+ ],
+ ]}
+ components={{
+ code: code as any,
+ p: p as any,
+ }}
+ className="flex flex-col gap-2"
+ >
+ {chat.answer.parts.map((part) => part.text).join("")}
+ </Markdown>
+ </div>
+ </div>
+ </div>
+ </div>
+ ))}
</div>
)}
</AnimatePresence>