mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-10-07 02:58:11 +00:00
279 lines
9.9 KiB
TypeScript
279 lines
9.9 KiB
TypeScript
"use client";
|
|
|
|
import { AnimatePresence } from "framer-motion";
|
|
import React, { useEffect, 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 } 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,
|
|
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.trim().length > 0) {
|
|
getAnswer(
|
|
q,
|
|
spaces.map((s) => s.id),
|
|
);
|
|
setTimeout(() => {
|
|
setLayout("chat");
|
|
}, 300);
|
|
} else {
|
|
router.push("/home");
|
|
}
|
|
}, []);
|
|
|
|
return (
|
|
<div className="h-full">
|
|
<AnimatePresence mode="popLayout">
|
|
{layout === "initial" ? (
|
|
<motion.div
|
|
exit={{ opacity: 0 }}
|
|
key="initial"
|
|
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 />
|
|
</div>
|
|
</motion.div>
|
|
) : (
|
|
<div
|
|
className="max-w-3xl flex mx-auto w-full flex-col mt-24"
|
|
key="chat"
|
|
>
|
|
{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>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default ChatWindow;
|