mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-10-07 02:58:11 +00:00
new-ui WORKS AND DONE AND DUSTED
This commit is contained in:
parent
531fd5c1ce
commit
d72f902579
2 changed files with 91 additions and 103 deletions
|
|
@ -1,32 +1,19 @@
|
|||
"use client";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { FilterCombobox } from "./Sidebar/FilterCombobox";
|
||||
import { Textarea2 } from "./ui/textarea";
|
||||
import { ArrowRight, ArrowUp } from "lucide-react";
|
||||
import { MemoryDrawer } from "./MemoryDrawer";
|
||||
import useViewport from "@/hooks/useViewport";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { cn, countLines } from "@/lib/utils";
|
||||
import { ChatHistory } from "../../types/memory";
|
||||
import { ChatAnswer, ChatMessage, ChatQuestion } from "./ChatMessage";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
|
||||
const dummyChatHistory: ChatHistory = {
|
||||
question: "who is dhravya?",
|
||||
answer: {
|
||||
parts: [
|
||||
{
|
||||
text: "Dhravya Shah is an 18-year-old full-stack developer based in Arizona, USA. He is a passionate developer who focuses on creating products that people love. Dhravya has a background in entrepreneurship, having been a 2x acquired founder and a participant in various hackathons. He is also involved in open-source contributions, content creation to inspire others in coding, and has a growing community of developers. Dhravya's work spans from creating AI-powered note-taking apps to personalized music companions and educational tools. Additionally, he is a guitarist, student, and active in sharing his experiences as a developer and entrepreneur",
|
||||
},
|
||||
],
|
||||
sources: ["dhravya.dev"],
|
||||
},
|
||||
};
|
||||
'use client';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { FilterCombobox } from './Sidebar/FilterCombobox';
|
||||
import { Textarea2 } from './ui/textarea';
|
||||
import { ArrowRight, ArrowUp } from 'lucide-react';
|
||||
import { MemoryDrawer } from './MemoryDrawer';
|
||||
import useViewport from '@/hooks/useViewport';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { cn, countLines } from '@/lib/utils';
|
||||
import { ChatHistory } from '../../types/memory';
|
||||
import { ChatAnswer, ChatMessage, ChatQuestion } from './ChatMessage';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
|
||||
function supportsDVH() {
|
||||
try {
|
||||
return CSS.supports("height: 100dvh");
|
||||
return CSS.supports('height: 100dvh');
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
|
|
@ -37,37 +24,25 @@ export default function Main({ sidebarOpen }: { sidebarOpen: boolean }) {
|
|||
const router = useRouter();
|
||||
|
||||
const [hide, setHide] = useState(false);
|
||||
const [layout, setLayout] = useState<"chat" | "initial">("initial");
|
||||
const [value, setValue] = useState("");
|
||||
const [layout, setLayout] = useState<'chat' | 'initial'>('initial');
|
||||
const [value, setValue] = useState('');
|
||||
const { width } = useViewport();
|
||||
const [searchResults, setSearchResults] = useState<string[]>([]);
|
||||
const [isAiLoading, setIsAiLoading] = useState(false);
|
||||
|
||||
const { data: session } = useSession();
|
||||
|
||||
// Variable to keep track of the chat history in this session
|
||||
const [chatHistory, setChatHistory] = useState<ChatHistory[]>([]);
|
||||
|
||||
// TEMPORARY solution: Basically this is to just keep track of the sources used for each chat message
|
||||
// Not a great solution
|
||||
const [chatTextSourceDict, setChatTextSourceDict] = useState<
|
||||
Record<string, string[]>
|
||||
>({});
|
||||
|
||||
// This is the streamed AI response we get from the server.
|
||||
const [aiResponse, setAIResponse] = useState("");
|
||||
|
||||
const [toBeParsed, setToBeParsed] = useState("");
|
||||
const [toBeParsed, setToBeParsed] = useState('');
|
||||
|
||||
const textArea = useRef<HTMLDivElement>(null);
|
||||
const main = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const search = searchParams.get("q");
|
||||
const search = searchParams.get('q');
|
||||
if (search && search.trim().length > 0) {
|
||||
setValue(search);
|
||||
onSend();
|
||||
router.push("/");
|
||||
router.push('/');
|
||||
}
|
||||
}, []);
|
||||
|
||||
|
|
@ -86,47 +61,38 @@ export default function Main({ sidebarOpen }: { sidebarOpen: boolean }) {
|
|||
}
|
||||
}
|
||||
|
||||
window.visualViewport?.addEventListener("resize", onResize);
|
||||
window.visualViewport?.addEventListener('resize', onResize);
|
||||
return () => {
|
||||
window.visualViewport?.removeEventListener("resize", onResize);
|
||||
window.visualViewport?.removeEventListener('resize', onResize);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleStreamData = (newChunk: string) => {
|
||||
// Append the new chunk to the existing data to be parsed
|
||||
setToBeParsed((prev) => prev + newChunk);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// Define a function to try parsing the accumulated data
|
||||
const tryParseAccumulatedData = () => {
|
||||
// Attempt to parse the "toBeParsed" state as JSON
|
||||
try {
|
||||
// Split the accumulated data by the known delimiter "\n\n"
|
||||
const parts = toBeParsed.split("\n\n");
|
||||
let remainingData = "";
|
||||
const parts = toBeParsed.split('\n\n');
|
||||
let remainingData = '';
|
||||
|
||||
// Process each part to extract JSON objects
|
||||
parts.forEach((part, index) => {
|
||||
try {
|
||||
const parsedPart = JSON.parse(part.replace("data: ", "")); // Try to parse the part as JSON
|
||||
const parsedPart = JSON.parse(part.replace('data: ', '')); // Try to parse the part as JSON
|
||||
|
||||
// If the part is the last one and couldn't be parsed, keep it to accumulate more data
|
||||
if (index === parts.length - 1 && !parsedPart) {
|
||||
remainingData = part;
|
||||
} else if (parsedPart && parsedPart.response) {
|
||||
// If the part is parsable and has the "response" field, update the AI response state
|
||||
// setAIResponse((prev) => prev + parsedPart.response);
|
||||
// appendToChatHistory('model', parsedPart.response);
|
||||
|
||||
// Append to chat history in this way:
|
||||
// If the last message was from the model, append to that message
|
||||
// Otherwise, Start a new message from the model and append to that
|
||||
if (chatHistory.length > 0) {
|
||||
setChatHistory((prev: any) => {
|
||||
setChatHistory((prev: ChatHistory[]) => {
|
||||
const lastMessage = prev[prev.length - 1];
|
||||
const newParts = [
|
||||
...lastMessage.parts,
|
||||
...lastMessage.answer.parts,
|
||||
{ text: parsedPart.response },
|
||||
];
|
||||
return [
|
||||
|
|
@ -146,7 +112,7 @@ export default function Main({ sidebarOpen }: { sidebarOpen: boolean }) {
|
|||
} catch (error) {
|
||||
// If parsing fails and it's not the last part, it's a malformed JSON
|
||||
if (index !== parts.length - 1) {
|
||||
console.error("Malformed JSON part: ", part);
|
||||
console.error('Malformed JSON part: ', part);
|
||||
} else {
|
||||
// If it's the last part, it may be incomplete, so keep it
|
||||
remainingData = part;
|
||||
|
|
@ -159,7 +125,7 @@ export default function Main({ sidebarOpen }: { sidebarOpen: boolean }) {
|
|||
setToBeParsed(remainingData);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error parsing accumulated data: ", error);
|
||||
console.error('Error parsing accumulated data: ', error);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -169,23 +135,45 @@ export default function Main({ sidebarOpen }: { sidebarOpen: boolean }) {
|
|||
}
|
||||
}, [toBeParsed]);
|
||||
|
||||
const modifyChatHistory = useCallback((old: ChatHistory[]) => {
|
||||
const final: { role: 'user' | 'model'; parts: { text: string }[] }[] = [];
|
||||
old.forEach((chat) => {
|
||||
final.push({
|
||||
role: 'user',
|
||||
parts: [{ text: chat.question }],
|
||||
});
|
||||
final.push({
|
||||
role: 'model',
|
||||
parts: chat.answer.parts.map((part) => ({ text: part.text })),
|
||||
});
|
||||
});
|
||||
|
||||
return final;
|
||||
}, []);
|
||||
|
||||
const getSearchResults = async () => {
|
||||
setIsAiLoading(true);
|
||||
|
||||
const _value = value.trim();
|
||||
setValue("");
|
||||
setValue('');
|
||||
|
||||
// @dhravya, this is using temporary dummy data remove this before testing
|
||||
setChatHistory((prev) => [...prev, dummyChatHistory]);
|
||||
setTimeout(() => setIsAiLoading(false), 5000);
|
||||
return;
|
||||
setChatHistory((prev) => [
|
||||
...prev,
|
||||
{
|
||||
question: _value,
|
||||
answer: {
|
||||
parts: [],
|
||||
sources: [],
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const sourcesResponse = await fetch(
|
||||
`/api/chat?sourcesOnly=true&q=${_value}`,
|
||||
{
|
||||
method: "POST",
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
chatHistory,
|
||||
chatHistory: modifyChatHistory(chatHistory),
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
|
@ -194,15 +182,26 @@ export default function Main({ sidebarOpen }: { sidebarOpen: boolean }) {
|
|||
ids: string[];
|
||||
};
|
||||
|
||||
setSearchResults((prev) =>
|
||||
Array.from(new Set([...prev, ...(sourcesInJson.ids ?? [])])),
|
||||
);
|
||||
setIsAiLoading(false)
|
||||
setChatHistory((prev) => {
|
||||
const lastMessage = prev[prev.length - 1];
|
||||
return [
|
||||
...prev.slice(0, prev.length - 1),
|
||||
{
|
||||
...lastMessage,
|
||||
answer: {
|
||||
parts: lastMessage.answer.parts,
|
||||
sources: sourcesInJson.ids ?? [],
|
||||
},
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
// TODO: PASS THE `SPACE` TO THE API
|
||||
const response = await fetch(`/api/chat?q=${_value}`, {
|
||||
method: "POST",
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
chatHistory,
|
||||
chatHistory: modifyChatHistory(chatHistory),
|
||||
}),
|
||||
});
|
||||
|
||||
|
|
@ -211,33 +210,21 @@ export default function Main({ sidebarOpen }: { sidebarOpen: boolean }) {
|
|||
return;
|
||||
}
|
||||
|
||||
setChatHistory((prev) => [
|
||||
...prev,
|
||||
{
|
||||
question: _value,
|
||||
answer: {
|
||||
parts: [],
|
||||
sources: sourcesInJson.ids ?? [],
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
if (response.body) {
|
||||
let reader = response.body?.getReader();
|
||||
let decoder = new TextDecoder("utf-8");
|
||||
let result = "";
|
||||
let decoder = new TextDecoder('utf-8');
|
||||
let result = '';
|
||||
|
||||
// @ts-ignore
|
||||
reader.read().then(function processText({ done, value }) {
|
||||
if (done) {
|
||||
setIsAiLoading(false);
|
||||
setToBeParsed("");
|
||||
setValue("");
|
||||
setToBeParsed('');
|
||||
setValue('');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
handleStreamData(decoder.decode(value));
|
||||
setToBeParsed((prev) => prev + decoder.decode(value));
|
||||
|
||||
return reader?.read().then(processText);
|
||||
});
|
||||
|
|
@ -245,14 +232,14 @@ export default function Main({ sidebarOpen }: { sidebarOpen: boolean }) {
|
|||
};
|
||||
|
||||
const onSend = async () => {
|
||||
setLayout("chat");
|
||||
setLayout('chat');
|
||||
await getSearchResults();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<AnimatePresence mode="wait">
|
||||
{layout === "chat" ? (
|
||||
{layout === 'chat' ? (
|
||||
<Chat
|
||||
key="chat"
|
||||
isLoading={isAiLoading}
|
||||
|
|
@ -268,7 +255,7 @@ export default function Main({ sidebarOpen }: { sidebarOpen: boolean }) {
|
|||
ref={main}
|
||||
className={cn(
|
||||
"sidebar flex w-full flex-col items-end justify-center gap-5 px-5 pt-5 transition-[padding-left,padding-top,padding-right] delay-200 duration-200 md:items-center md:gap-10 md:px-72 [&[data-sidebar-open='true']]:pr-10 [&[data-sidebar-open='true']]:delay-0 md:[&[data-sidebar-open='true']]:pl-[calc(2.5rem+30vw)]",
|
||||
hide ? "" : "main-hidden",
|
||||
hide ? '' : 'main-hidden',
|
||||
)}
|
||||
>
|
||||
<h1 className="text-rgray-11 mt-auto w-full text-center text-3xl md:mt-0">
|
||||
|
|
@ -282,19 +269,19 @@ export default function Main({ sidebarOpen }: { sidebarOpen: boolean }) {
|
|||
y: 50,
|
||||
}}
|
||||
transition={{
|
||||
type: "tween",
|
||||
type: 'tween',
|
||||
duration: 0.2,
|
||||
}}
|
||||
textAreaProps={{
|
||||
placeholder: "Ask your SuperMemory...",
|
||||
placeholder: 'Ask your SuperMemory...',
|
||||
className:
|
||||
"h-auto overflow-auto md:h-full md:resize-none text-lg py-0 px-2 pt-2 md:py-0 md:p-5 resize-y text-rgray-11 w-full min-h-[1em]",
|
||||
'h-auto overflow-auto md:h-full md:resize-none text-lg py-0 px-2 pt-2 md:py-0 md:p-5 resize-y text-rgray-11 w-full min-h-[1em]',
|
||||
value,
|
||||
autoFocus: true,
|
||||
onChange: (e) => setValue(e.target.value),
|
||||
onKeyDown: (e) => {
|
||||
console.log(e.key, e.ctrlKey, e.metaKey);
|
||||
if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) {
|
||||
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
|
||||
onSend();
|
||||
}
|
||||
},
|
||||
|
|
@ -303,7 +290,7 @@ export default function Main({ sidebarOpen }: { sidebarOpen: boolean }) {
|
|||
<div className="text-rgray-11/70 flex h-full w-fit items-center justify-center pl-0 md:w-full md:p-2">
|
||||
<FilterCombobox
|
||||
onClose={() => {
|
||||
textArea.current?.querySelector("textarea")?.focus();
|
||||
textArea.current?.querySelector('textarea')?.focus();
|
||||
}}
|
||||
className="hidden md:flex"
|
||||
/>
|
||||
|
|
@ -363,7 +350,7 @@ export function Chat({
|
|||
loading={i === chatHistory.length - 1 ? isLoading : false}
|
||||
sources={msg.answer.sources}
|
||||
>
|
||||
{msg.answer.parts.map((part) => part.text).join(" ")}
|
||||
{msg.answer.parts.map((part) => part.text).join(' ')}
|
||||
</ChatAnswer>
|
||||
</ChatMessage>
|
||||
))}
|
||||
|
|
@ -376,7 +363,7 @@ export function Chat({
|
|||
<div className="animate-from-top fixed bottom-10 mt-auto flex w-[50%] flex-col items-start justify-center gap-2">
|
||||
<FilterCombobox
|
||||
onClose={() => {
|
||||
textArea.current?.querySelector("textarea")?.focus();
|
||||
textArea.current?.querySelector('textarea')?.focus();
|
||||
}}
|
||||
side="top"
|
||||
align="start"
|
||||
|
|
@ -386,15 +373,15 @@ export function Chat({
|
|||
ref={textArea}
|
||||
className="bg-rgray-2 h-auto w-full flex-row items-start justify-center overflow-auto px-3 md:items-center md:justify-center"
|
||||
textAreaProps={{
|
||||
placeholder: "Ask your SuperMemory...",
|
||||
placeholder: 'Ask your SuperMemory...',
|
||||
className:
|
||||
"overflow-auto h-auto p-3 md:resize-none text-lg w-auto resize-y text-rgray-11 w-full",
|
||||
'overflow-auto h-auto p-3 md:resize-none text-lg w-auto resize-y text-rgray-11 w-full',
|
||||
value,
|
||||
rows: 1,
|
||||
autoFocus: true,
|
||||
onChange: onValueChange,
|
||||
onKeyDown: (e) => {
|
||||
if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) {
|
||||
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
|
||||
askQuestion();
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@
|
|||
"nuqs": "^1.17.1",
|
||||
"react-markdown": "^9.0.1",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"tailwind-scrollbar": "^3.1.0",
|
||||
"tailwindcss-animate": "^1.0.7"
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue