diff options
| author | Dhravya <[email protected]> | 2024-04-09 19:10:22 -0700 |
|---|---|---|
| committer | Dhravya <[email protected]> | 2024-04-09 19:10:22 -0700 |
| commit | d72f902579815b14360d7cea756616bb981545f0 (patch) | |
| tree | b1672a97c13d049bff319817a74130f6cff921c3 /apps/web/src/components | |
| parent | have a initial search query (diff) | |
| download | archived-supermemory-d72f902579815b14360d7cea756616bb981545f0.tar.xz archived-supermemory-d72f902579815b14360d7cea756616bb981545f0.zip | |
new-ui WORKS AND DONE AND DUSTED
Diffstat (limited to 'apps/web/src/components')
| -rw-r--r-- | apps/web/src/components/Main.tsx | 193 |
1 files changed, 90 insertions, 103 deletions
diff --git a/apps/web/src/components/Main.tsx b/apps/web/src/components/Main.tsx index 9efa9a81..3d0fc18a 100644 --- a/apps/web/src/components/Main.tsx +++ b/apps/web/src/components/Main.tsx @@ -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(); } }, |