aboutsummaryrefslogtreecommitdiff
path: root/apps/web/src/components
diff options
context:
space:
mode:
authorDhravya <[email protected]>2024-04-08 13:00:39 -0700
committerDhravya <[email protected]>2024-04-08 13:00:39 -0700
commit514f943caadd3a42c5611b752bb30c76dc3e492a (patch)
treee84abeb795eefd4a3974880b97d9e481189bf7d1 /apps/web/src/components
parentaggregate content from same space (diff)
downloadsupermemory-514f943caadd3a42c5611b752bb30c76dc3e492a.tar.xz
supermemory-514f943caadd3a42c5611b752bb30c76dc3e492a.zip
made it functional
Diffstat (limited to 'apps/web/src/components')
-rw-r--r--apps/web/src/components/Main.tsx177
-rw-r--r--apps/web/src/components/Sidebar/index.tsx91
2 files changed, 204 insertions, 64 deletions
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={{