mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-09-07 08:26:15 +00:00
added multi-turn conversations
This commit is contained in:
parent
e4c6c1aab0
commit
074ea24565
3 changed files with 141 additions and 72 deletions
|
|
@ -1,7 +1,7 @@
|
|||
"use client";
|
||||
|
||||
import { AnimatePresence } from "framer-motion";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import QueryInput from "../home/queryinput";
|
||||
import { cn } from "@repo/ui/lib/utils";
|
||||
import { motion } from "framer-motion";
|
||||
|
|
@ -36,15 +36,12 @@ function ChatWindow({
|
|||
{
|
||||
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.`,
|
||||
// },
|
||||
],
|
||||
parts: [],
|
||||
sources: [],
|
||||
},
|
||||
},
|
||||
]);
|
||||
const [isAutoScroll, setIsAutoScroll] = useState(true);
|
||||
|
||||
const removeJustificationFromText = (text: string) => {
|
||||
// remove everything after the first "<justification>" word
|
||||
|
|
@ -68,7 +65,7 @@ function ChatWindow({
|
|||
{
|
||||
method: "POST",
|
||||
body: JSON.stringify({ chatHistory }),
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// TODO: handle this properly
|
||||
|
|
@ -89,11 +86,15 @@ function ChatWindow({
|
|||
}
|
||||
|
||||
setChatHistory((prevChatHistory) => {
|
||||
window.scrollTo({
|
||||
top: document.documentElement.scrollHeight,
|
||||
behavior: "smooth",
|
||||
});
|
||||
const newChatHistory = [...prevChatHistory];
|
||||
const lastAnswer = newChatHistory[newChatHistory.length - 1];
|
||||
if (!lastAnswer) return prevChatHistory;
|
||||
const filteredSourceUrls = new Set(
|
||||
sourcesParsed.data.metadata.map((source) => source.url)
|
||||
sourcesParsed.data.metadata.map((source) => source.url),
|
||||
);
|
||||
const uniqueSources = sourcesParsed.data.metadata.filter((source) => {
|
||||
if (filteredSourceUrls.has(source.url)) {
|
||||
|
|
@ -106,9 +107,9 @@ function ChatWindow({
|
|||
title: source.title ?? "Untitled",
|
||||
type: source.type ?? "page",
|
||||
source: source.url ?? "https://supermemory.ai",
|
||||
content: source.content ?? "No content available",
|
||||
content: source.description ?? "No content available",
|
||||
numChunks: sourcesParsed.data.metadata.filter(
|
||||
(f) => f.url === source.url
|
||||
(f) => f.url === source.url,
|
||||
).length,
|
||||
}));
|
||||
return newChatHistory;
|
||||
|
|
@ -129,7 +130,16 @@ function ChatWindow({
|
|||
const newChatHistory = [...prevChatHistory];
|
||||
const lastAnswer = newChatHistory[newChatHistory.length - 1];
|
||||
if (!lastAnswer) return prevChatHistory;
|
||||
lastAnswer.answer.parts.push({ text: new TextDecoder().decode(value) });
|
||||
const txt = new TextDecoder().decode(value);
|
||||
|
||||
if (isAutoScroll) {
|
||||
window.scrollTo({
|
||||
top: document.documentElement.scrollHeight,
|
||||
behavior: "smooth",
|
||||
});
|
||||
}
|
||||
|
||||
lastAnswer.answer.parts.push({ text: txt });
|
||||
return newChatHistory;
|
||||
});
|
||||
}
|
||||
|
|
@ -137,13 +147,11 @@ function ChatWindow({
|
|||
|
||||
useEffect(() => {
|
||||
if (q.trim().length > 0) {
|
||||
setLayout("chat");
|
||||
getAnswer(
|
||||
q,
|
||||
spaces.map((s) => s.id)
|
||||
spaces.map((s) => s.id),
|
||||
);
|
||||
setTimeout(() => {
|
||||
setLayout("chat");
|
||||
}, 300);
|
||||
} else {
|
||||
router.push("/home");
|
||||
}
|
||||
|
|
@ -159,22 +167,27 @@ function ChatWindow({
|
|||
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 />
|
||||
<QueryInput
|
||||
handleSubmit={() => {}}
|
||||
initialQuery={q}
|
||||
initialSpaces={[]}
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
</motion.div>
|
||||
) : (
|
||||
<div
|
||||
className="max-w-3xl flex mx-auto w-full flex-col mt-24"
|
||||
className="max-w-3xl relative flex mx-auto w-full flex-col mt-24 pb-32"
|
||||
key="chat"
|
||||
>
|
||||
{chatHistory.map((chat, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className={`mt-8 ${idx != chatHistory.length - 1 ? "pb-2 border-b" : ""}`}
|
||||
className={`mt-8 ${idx != chatHistory.length - 1 ? "pb-2 border-b border-b-gray-400" : ""}`}
|
||||
>
|
||||
<h2
|
||||
className={cn(
|
||||
"text-white transition-all transform translate-y-0 opacity-100 duration-500 ease-in-out font-semibold text-2xl"
|
||||
"text-white transition-all transform translate-y-0 opacity-100 duration-500 ease-in-out font-semibold text-2xl",
|
||||
)}
|
||||
>
|
||||
{chat.question}
|
||||
|
|
@ -273,7 +286,7 @@ function ChatWindow({
|
|||
className="flex flex-col gap-2"
|
||||
>
|
||||
{removeJustificationFromText(
|
||||
chat.answer.parts.map((part) => part.text).join("")
|
||||
chat.answer.parts.map((part) => part.text).join(""),
|
||||
)}
|
||||
</Markdown>
|
||||
</div>
|
||||
|
|
@ -307,6 +320,33 @@ function ChatWindow({
|
|||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="fixed bottom-0 w-full max-w-3xl pb-4">
|
||||
<QueryInput
|
||||
mini
|
||||
className="w-full shadow-md"
|
||||
initialQuery={""}
|
||||
initialSpaces={[]}
|
||||
handleSubmit={async (q, spaces) => {
|
||||
setChatHistory((prevChatHistory) => {
|
||||
return [
|
||||
...prevChatHistory,
|
||||
{
|
||||
question: q,
|
||||
answer: {
|
||||
parts: [],
|
||||
sources: [],
|
||||
},
|
||||
},
|
||||
];
|
||||
});
|
||||
await getAnswer(
|
||||
q,
|
||||
spaces.map((s) => `${s.id}`),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
import React from "react";
|
||||
import Menu from "../menu";
|
||||
import Header from "../header";
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
import QueryInput from "./queryinput";
|
||||
import { homeSearchParamsCache } from "@/lib/searchParams";
|
||||
import { getSpaces } from "@/app/actions/fetchers";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
async function Page({
|
||||
function Page({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: Record<string, string | string[] | undefined>;
|
||||
|
|
@ -13,12 +14,18 @@ async function Page({
|
|||
// TODO: use this to show a welcome page/modal
|
||||
const { firstTime } = homeSearchParamsCache.parse(searchParams);
|
||||
|
||||
let spaces = await getSpaces();
|
||||
const [spaces, setSpaces] = useState<{ id: number; name: string }[]>([]);
|
||||
|
||||
if (!spaces.success) {
|
||||
// TODO: handle this error properly.
|
||||
spaces.data = [];
|
||||
}
|
||||
useEffect(() => {
|
||||
getSpaces().then((res) => {
|
||||
if (res.success && res.data) {
|
||||
setSpaces(res.data);
|
||||
}
|
||||
// TODO: HANDLE ERROR
|
||||
});
|
||||
}, []);
|
||||
|
||||
const { push } = useRouter();
|
||||
|
||||
return (
|
||||
<div className="max-w-3xl h-full justify-center flex mx-auto w-full flex-col">
|
||||
|
|
@ -26,7 +33,17 @@ async function Page({
|
|||
{/* <div className="">hi {firstTime ? 'first time' : ''}</div> */}
|
||||
|
||||
<div className="w-full h-96">
|
||||
<QueryInput initialSpaces={spaces.data} />
|
||||
<QueryInput
|
||||
handleSubmit={(q, spaces) => {
|
||||
const newQ =
|
||||
"/chat?q=" +
|
||||
encodeURI(q) +
|
||||
(spaces ? "&spaces=" + JSON.stringify(spaces) : "");
|
||||
|
||||
push(newQ);
|
||||
}}
|
||||
initialSpaces={spaces}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -12,6 +12,9 @@ function QueryInput({
|
|||
initialQuery = "",
|
||||
initialSpaces = [],
|
||||
disabled = false,
|
||||
className,
|
||||
mini = false,
|
||||
handleSubmit,
|
||||
}: {
|
||||
initialQuery?: string;
|
||||
initialSpaces?: {
|
||||
|
|
@ -19,32 +22,14 @@ function QueryInput({
|
|||
name: string;
|
||||
}[];
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
mini?: boolean;
|
||||
handleSubmit: (q: string, spaces: { id: number; name: string }[]) => void;
|
||||
}) {
|
||||
const [q, setQ] = useState(initialQuery);
|
||||
|
||||
const [selectedSpaces, setSelectedSpaces] = useState<number[]>([]);
|
||||
|
||||
const { push } = useRouter();
|
||||
|
||||
const parseQ = () => {
|
||||
// preparedSpaces is list of spaces selected by user, with id and name
|
||||
const preparedSpaces = initialSpaces
|
||||
.filter((x) => selectedSpaces.includes(x.id))
|
||||
.map((x) => {
|
||||
return {
|
||||
id: x.id,
|
||||
name: x.name,
|
||||
};
|
||||
});
|
||||
|
||||
const newQ =
|
||||
"/chat?q=" +
|
||||
encodeURI(q) +
|
||||
(selectedSpaces ? "&spaces=" + JSON.stringify(preparedSpaces) : "");
|
||||
|
||||
return newQ;
|
||||
};
|
||||
|
||||
const options = useMemo(
|
||||
() =>
|
||||
initialSpaces.map((x) => ({
|
||||
|
|
@ -54,21 +39,43 @@ function QueryInput({
|
|||
[initialSpaces],
|
||||
);
|
||||
|
||||
const preparedSpaces = useMemo(
|
||||
() =>
|
||||
initialSpaces
|
||||
.filter((x) => selectedSpaces.includes(x.id))
|
||||
.map((x) => {
|
||||
return {
|
||||
id: x.id,
|
||||
name: x.name,
|
||||
};
|
||||
}),
|
||||
[selectedSpaces, initialSpaces],
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="bg-secondary rounded-t-[24px]">
|
||||
<div className={className}>
|
||||
<div
|
||||
className={`bg-secondary ${!mini ? "rounded-t-3xl" : "rounded-3xl"}`}
|
||||
>
|
||||
{/* input and action button */}
|
||||
<form action={async () => push(parseQ())} className="flex gap-4 p-3">
|
||||
<form
|
||||
action={async () => {
|
||||
handleSubmit(q, preparedSpaces);
|
||||
setQ("");
|
||||
}}
|
||||
className="flex gap-4 p-3"
|
||||
>
|
||||
<textarea
|
||||
name="q"
|
||||
cols={30}
|
||||
rows={4}
|
||||
rows={mini ? 2 : 4}
|
||||
className="bg-transparent pt-2.5 text-base text-[#989EA4] focus:text-foreground duration-200 tracking-[3%] outline-none resize-none w-full p-4"
|
||||
placeholder="Ask your second brain..."
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
if (!e.shiftKey) push(parseQ());
|
||||
handleSubmit(q, preparedSpaces);
|
||||
setQ("");
|
||||
}
|
||||
}}
|
||||
onChange={(e) => setQ(e.target.value)}
|
||||
|
|
@ -84,24 +91,29 @@ function QueryInput({
|
|||
<Image src={ArrowRightIcon} alt="Right arrow icon" />
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<Divider />
|
||||
</div>
|
||||
{/* selected sources */}
|
||||
<div className="flex items-center gap-6 p-2 h-auto bg-secondary rounded-b-[24px]">
|
||||
<MultipleSelector
|
||||
key={options.length}
|
||||
disabled={disabled}
|
||||
defaultOptions={options}
|
||||
onChange={(e) => setSelectedSpaces(e.map((x) => parseInt(x.value)))}
|
||||
placeholder="Focus on specific spaces..."
|
||||
emptyIndicator={
|
||||
<p className="text-center text-lg leading-10 text-gray-600 dark:text-gray-400">
|
||||
no results found.
|
||||
</p>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
{!mini && (
|
||||
<>
|
||||
<Divider />
|
||||
<div className="flex items-center gap-6 p-2 h-auto bg-secondary rounded-b-3xl">
|
||||
<MultipleSelector
|
||||
key={options.length}
|
||||
disabled={disabled}
|
||||
defaultOptions={options}
|
||||
onChange={(e) =>
|
||||
setSelectedSpaces(e.map((x) => parseInt(x.value)))
|
||||
}
|
||||
placeholder="Focus on specific spaces..."
|
||||
emptyIndicator={
|
||||
<p className="text-center text-lg leading-10 text-gray-600 dark:text-gray-400">
|
||||
no results found.
|
||||
</p>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue