diff options
| author | Dhravya <[email protected]> | 2024-04-08 13:00:39 -0700 |
|---|---|---|
| committer | Dhravya <[email protected]> | 2024-04-08 13:00:39 -0700 |
| commit | 514f943caadd3a42c5611b752bb30c76dc3e492a (patch) | |
| tree | e84abeb795eefd4a3974880b97d9e481189bf7d1 /apps/web/src | |
| parent | aggregate content from same space (diff) | |
| download | supermemory-514f943caadd3a42c5611b752bb30c76dc3e492a.tar.xz supermemory-514f943caadd3a42c5611b752bb30c76dc3e492a.zip | |
made it functional
Diffstat (limited to 'apps/web/src')
| -rw-r--r-- | apps/web/src/app/api/query/route.ts | 5 | ||||
| -rw-r--r-- | apps/web/src/app/content.tsx | 19 | ||||
| -rw-r--r-- | apps/web/src/components/Main.tsx | 177 | ||||
| -rw-r--r-- | apps/web/src/components/Sidebar/index.tsx | 91 |
4 files changed, 219 insertions, 73 deletions
diff --git a/apps/web/src/app/api/query/route.ts b/apps/web/src/app/api/query/route.ts index 4e2f0674..28f441bc 100644 --- a/apps/web/src/app/api/query/route.ts +++ b/apps/web/src/app/api/query/route.ts @@ -21,7 +21,7 @@ export async function GET(req: NextRequest) { return NextResponse.json({ message: "Invalid Key, session not found." }, { status: 404 }); } - const session = {session: sessionData[0], user: user[0]} + const session = { session: sessionData[0], user: user[0] } const query = new URL(req.url).searchParams.get("q"); const sourcesOnly = new URL(req.url).searchParams.get("sourcesOnly") ?? "false"; @@ -36,8 +36,11 @@ export async function GET(req: NextRequest) { } }) + console.log(resp.status) + if (resp.status !== 200 || !resp.ok) { const errorData = await resp.json(); + console.log(errorData) return new Response(JSON.stringify({ message: "Error in CF function", error: errorData }), { status: resp.status }); } diff --git a/apps/web/src/app/content.tsx b/apps/web/src/app/content.tsx index 8bfebcb9..39f2948d 100644 --- a/apps/web/src/app/content.tsx +++ b/apps/web/src/app/content.tsx @@ -1,15 +1,18 @@ -"use client"; -import Main from "@/components/Main"; -import Sidebar from "@/components/Sidebar/index"; -import { useState } from "react"; +'use client'; +import Main from '@/components/Main'; +import Sidebar from '@/components/Sidebar/index'; +import { SessionProvider } from 'next-auth/react'; +import { useState } from 'react'; export default function Content() { const [selectedItem, setSelectedItem] = useState<string | null>(null); return ( - <div className="flex w-screen"> - <Sidebar selectChange={setSelectedItem} /> - <Main sidebarOpen={selectedItem !== null} /> - </div> + <SessionProvider> + <div className="flex w-screen"> + <Sidebar selectChange={setSelectedItem} /> + <Main sidebarOpen={selectedItem !== null} /> + </div> + </SessionProvider> ); } diff --git a/apps/web/src/components/Main.tsx b/apps/web/src/components/Main.tsx index 0bfd76a5..86679dcf 100644 --- a/apps/web/src/components/Main.tsx +++ b/apps/web/src/components/Main.tsx @@ -1,16 +1,17 @@ -"use client"; -import { useEffect, useRef, useState } from "react"; -import { FilterCombobox } from "./Sidebar/FilterCombobox"; -import { Textarea2 } from "./ui/textarea"; -import { ArrowRight } from "lucide-react"; -import { MemoryDrawer } from "./MemoryDrawer"; -import useViewport from "@/hooks/useViewport"; -import { motion } from "framer-motion"; -import { cn } from "@/lib/utils"; +'use client'; +import { useEffect, useRef, useState } from 'react'; +import { FilterCombobox } from './Sidebar/FilterCombobox'; +import { Textarea2 } from './ui/textarea'; +import { ArrowRight } from 'lucide-react'; +import { MemoryDrawer } from './MemoryDrawer'; +import useViewport from '@/hooks/useViewport'; +import { motion } from 'framer-motion'; +import { cn } from '@/lib/utils'; +import SearchResults from './SearchResults'; function supportsDVH() { try { - return CSS.supports("height: 100dvh"); + return CSS.supports('height: 100dvh'); } catch { return false; } @@ -18,8 +19,13 @@ function supportsDVH() { export default function Main({ sidebarOpen }: { sidebarOpen: boolean }) { const [hide, setHide] = useState(false); - const [value, setValue] = useState(""); + const [value, setValue] = useState(''); const { width } = useViewport(); + const [searchResults, setSearchResults] = useState<string[]>([]); + const [isAiLoading, setIsAiLoading] = useState(false); + + const [aiResponse, setAIResponse] = useState(''); + const [toBeParsed, setToBeParsed] = useState(''); const textArea = useRef<HTMLTextAreaElement>(null); const main = useRef<HTMLDivElement>(null); @@ -39,46 +45,145 @@ 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 = ''; + + // 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 + + // 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); + } + } 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); + } else { + // If it's the last part, it may be incomplete, so keep it + remainingData = part; + } + } + }); + + // Update the toBeParsed state to only contain the unparsed remainder + if (remainingData !== toBeParsed) { + setToBeParsed(remainingData); + } + } catch (error) { + console.error('Error parsing accumulated data: ', error); + } + }; + + // Call the parsing function if there's data to be parsed + if (toBeParsed) { + tryParseAccumulatedData(); + } + }, [toBeParsed]); + + const getSearchResults = async (e: React.FormEvent<HTMLFormElement>) => { + e.preventDefault(); + setIsAiLoading(true); + + const sourcesResponse = await fetch( + `/api/query?sourcesOnly=true&q=${value}`, + ); + + const sourcesInJson = (await sourcesResponse.json()) as { + ids: string[]; + }; + + setSearchResults(sourcesInJson.ids); + + const response = await fetch(`/api/query?q=${value}`); + + if (response.status !== 200) { + setIsAiLoading(false); + return; + } + + if (response.body) { + let reader = response.body.getReader(); + let decoder = new TextDecoder('utf-8'); + let result = ''; + + // @ts-ignore + reader.read().then(function processText({ done, value }) { + if (done) { + // setSearchResults(JSON.parse(result.replace('data: ', ''))); + // setIsAiLoading(false); + return; + } + + handleStreamData(decoder.decode(value)); + + return reader.read().then(processText); + }); + } + }; + return ( <motion.main data-sidebar-open={sidebarOpen} 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"> Ask your Second brain </h1> - <Textarea2 - ref={textArea} - className="mt-auto h-max max-h-[30em] min-h-[3em] resize-y flex-row items-start justify-center overflow-auto py-5 md:mt-0 md:h-[20vh] md:resize-none md:flex-col md:items-center md:justify-center md:p-2 md:pb-2 md:pt-2" - textAreaProps={{ - placeholder: "Ask your SuperMemory...", - className: - "h-auto overflow-auto md:h-full md:resize-none text-lg py-0 px-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), - }} - > - <div className="text-rgray-11/70 flex h-full w-fit items-center justify-center pl-0 md:w-full md:p-2"> - <FilterCombobox className="hidden md:flex" /> - <button - disabled={value.trim().length < 1} - className="text-rgray-11/70 bg-rgray-3 focus-visible:ring-rgray-8 hover:bg-rgray-4 mt-auto flex items-center justify-center rounded-full p-2 ring-2 ring-transparent focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50 md:ml-auto md:mt-0" - > - <ArrowRight className="h-5 w-5" /> - </button> - </div> - </Textarea2> + <form onSubmit={async (e) => await getSearchResults(e)}> + <Textarea2 + ref={textArea} + className="mt-auto h-max max-h-[30em] min-h-[3em] resize-y flex-row items-start justify-center overflow-auto py-5 md:mt-0 md:h-[20vh] md:resize-none md:flex-col md:items-center md:justify-center md:p-2 md:pb-2 md:pt-2" + textAreaProps={{ + placeholder: 'Ask your SuperMemory...', + className: + 'h-auto overflow-auto md:h-full md:resize-none text-lg py-0 px-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), + }} + > + <div className="text-rgray-11/70 flex h-full w-fit items-center justify-center pl-0 md:w-full md:p-2"> + <FilterCombobox className="hidden md:flex" /> + <button + type="submit" + disabled={value.trim().length < 1} + className="text-rgray-11/70 bg-rgray-3 focus-visible:ring-rgray-8 hover:bg-rgray-4 mt-auto flex items-center justify-center rounded-full p-2 ring-2 ring-transparent focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50 md:ml-auto md:mt-0" + > + <ArrowRight className="h-5 w-5" /> + </button> + </div> + </Textarea2> + </form> + {searchResults && ( + <SearchResults aiResponse={aiResponse} sources={searchResults} /> + )} {width <= 768 && <MemoryDrawer hide={hide} />} </motion.main> ); diff --git a/apps/web/src/components/Sidebar/index.tsx b/apps/web/src/components/Sidebar/index.tsx index 8effffbd..830b0f05 100644 --- a/apps/web/src/components/Sidebar/index.tsx +++ b/apps/web/src/components/Sidebar/index.tsx @@ -1,13 +1,12 @@ -"use client"; -import { StoredContent } from "@/server/db/schema"; -import { MemoryIcon } from "../../assets/Memories"; -import { Trash2, User2 } from "lucide-react"; -import React, { ElementType, useEffect, useState } from "react"; -import { MemoriesBar } from "./MemoriesBar"; -import { AnimatePresence, motion } from "framer-motion"; -import { Bin } from "@/assets/Bin"; -import { CollectedSpaces } from "../../../types/memory"; -import { useMemory } from "@/contexts/MemoryContext"; +'use client'; +import { MemoryIcon } from '../../assets/Memories'; +import { Trash2, User2 } from 'lucide-react'; +import React, { useEffect, useState } from 'react'; +import { MemoriesBar } from './MemoriesBar'; +import { AnimatePresence, motion } from 'framer-motion'; +import { Bin } from '@/assets/Bin'; +import { Avatar, AvatarFallback, AvatarImage } from '@radix-ui/react-avatar'; +import { useSession } from 'next-auth/react'; export type MenuItem = { icon: React.ReactNode | React.ReactNode[]; @@ -15,29 +14,48 @@ export type MenuItem = { content?: React.ReactNode; }; -const menuItemsBottom: Array<MenuItem> = [ - { - icon: <Trash2 strokeWidth={1.3} className="h-6 w-6" />, - label: "Trash", - }, - { - icon: <User2 strokeWidth={1.3} className="h-6 w-6" />, - label: "Profile", - }, -]; - export default function Sidebar({ selectChange, }: { selectChange?: (selectedItem: string | null) => void; }) { + const { data: session } = useSession(); const menuItemsTop: Array<MenuItem> = [ { icon: <MemoryIcon className="h-10 w-10" />, - label: "Memories", + label: 'Memories', content: <MemoriesBar />, }, ]; + + const menuItemsBottom: Array<MenuItem> = [ + { + icon: <Trash2 strokeWidth={1.3} className="h-6 w-6" />, + label: 'Trash', + }, + { + icon: ( + <div> + <Avatar> + {session?.user?.image ? ( + <AvatarImage + className="h-6 w-6 rounded-full" + src={session?.user?.image} + alt="user pfp" + /> + ) : ( + <User2 strokeWidth={1.3} className="h-6 w-6" /> + )} + <AvatarFallback> + {session?.user?.name?.split(' ').map((n) => n[0])}{' '} + </AvatarFallback> + </Avatar> + </div> + ), + label: 'Profile', + }, + ]; + const menuItems = [...menuItemsTop, ...menuItemsBottom]; const [selectedItem, setSelectedItem] = useState<string | null>(null); @@ -55,7 +73,7 @@ export default function Sidebar({ <div className="bg-rgray-2 border-r-rgray-6 relative z-[50] flex h-full w-full flex-col items-center justify-center border-r px-2 py-5 "> <MenuItem item={{ - label: "Memories", + label: 'Memories', icon: <MemoryIcon className="h-10 w-10" />, content: <MemoriesBar />, }} @@ -67,7 +85,7 @@ export default function Sidebar({ <MenuItem item={{ - label: "Trash", + label: 'Trash', icon: <Bin id="trash" className="z-[300] h-7 w-7" />, }} selectedItem={selectedItem} @@ -76,8 +94,25 @@ export default function Sidebar({ /> <MenuItem item={{ - label: "Profile", - icon: <User2 strokeWidth={1.3} className="h-7 w-7" />, + label: 'Profile', + icon: ( + <div className="mb-2"> + <Avatar> + {session?.user?.image ? ( + <AvatarImage + className="h-6 w-6 rounded-full" + src={session?.user?.image} + alt="@shadcn" + /> + ) : ( + <User2 strokeWidth={1.3} className="h-6 w-6" /> + )} + <AvatarFallback> + {session?.user?.name?.split(' ').map((n) => n[0])}{' '} + </AvatarFallback> + </Avatar> + </div> + ), }} selectedItem={selectedItem} setSelectedItem={setSelectedItem} @@ -115,11 +150,11 @@ const MenuItem = ({ export function SubSidebar({ children }: { children?: React.ReactNode }) { return ( <motion.div - initial={{ opacity: 0, x: "-100%" }} + initial={{ opacity: 0, x: '-100%' }} animate={{ opacity: 1, x: 0 }} exit={{ opacity: 0, - x: "-100%", + x: '-100%', transition: { delay: 0.2 }, }} transition={{ |