diff options
| author | Yash <[email protected]> | 2024-04-11 11:02:24 +0000 |
|---|---|---|
| committer | Yash <[email protected]> | 2024-04-11 11:02:24 +0000 |
| commit | bf99ee97f7c4d7580829d074816ebe0d32316d92 (patch) | |
| tree | 6bf59543316a15399ae3cc001b7cdd0fc2c00d39 /apps/web/src | |
| parent | schema update (diff) | |
| download | supermemory-bf99ee97f7c4d7580829d074816ebe0d32316d92.tar.xz supermemory-bf99ee97f7c4d7580829d074816ebe0d32316d92.zip | |
ok
Diffstat (limited to 'apps/web/src')
| -rw-r--r-- | apps/web/src/actions/db.ts | 52 | ||||
| -rw-r--r-- | apps/web/src/components/Main.tsx | 6 | ||||
| -rw-r--r-- | apps/web/src/components/Sidebar/AddMemoryDialog.tsx | 100 | ||||
| -rw-r--r-- | apps/web/src/components/Sidebar/FilterCombobox.tsx | 119 | ||||
| -rw-r--r-- | apps/web/src/components/Sidebar/MemoriesBar.tsx | 18 | ||||
| -rw-r--r-- | apps/web/src/contexts/MemoryContext.tsx | 48 |
6 files changed, 282 insertions, 61 deletions
diff --git a/apps/web/src/actions/db.ts b/apps/web/src/actions/db.ts new file mode 100644 index 00000000..3b640c96 --- /dev/null +++ b/apps/web/src/actions/db.ts @@ -0,0 +1,52 @@ +"use server"; +import { db } from "@/server/db"; +import { + contentToSpace, + StoredContent, + storedContent, +} from "@/server/db/schema"; +import { like, eq, and } from "drizzle-orm"; +import { auth as authOptions } from "@/server/auth"; +import { getSession } from "next-auth/react"; + +export async function getMemory(title: string) { + const session = await getSession(); + + console.log(session?.user?.name); + + if (!session || !session.user) { + return null; + } + + return await db + .select() + .from(storedContent) + .where( + and( + eq(storedContent.user, session.user.id!), + like(storedContent.title, `%${title}%`), + ), + ); +} + +export async function addMemory( + content: typeof storedContent.$inferInsert, + spaces: number[], +) { + const session = await getSession(); + + if (!session || !session.user) { + return null; + } + content.user = session.user.id; + + const _content = ( + await db.insert(storedContent).values(content).returning() + )[0]; + await Promise.all( + spaces.map((spaceId) => + db.insert(contentToSpace).values({ contentId: _content.id, spaceId }), + ), + ); + return _content; +} diff --git a/apps/web/src/components/Main.tsx b/apps/web/src/components/Main.tsx index 0235d608..c7bb3f1e 100644 --- a/apps/web/src/components/Main.tsx +++ b/apps/web/src/components/Main.tsx @@ -1,6 +1,6 @@ "use client"; import { useCallback, useEffect, useRef, useState } from "react"; -import { FilterCombobox } from "./Sidebar/FilterCombobox"; +import { FilterSpaces } from "./Sidebar/FilterCombobox"; import { Textarea2 } from "./ui/textarea"; import { ArrowRight, ArrowUp } from "lucide-react"; import { MemoryDrawer } from "./MemoryDrawer"; @@ -307,7 +307,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 + <FilterSpaces name={"Filter"} onClose={() => { textArea.current?.querySelector("textarea")?.focus(); @@ -383,7 +383,7 @@ export function Chat({ className="absolute flex w-full items-center justify-center" > <div className="animate-from-top fixed bottom-10 mt-auto flex w-[50%] flex-col items-start justify-center gap-2"> - <FilterCombobox + <FilterSpaces name={"Filter"} onClose={() => { textArea.current?.querySelector("textarea")?.focus(); diff --git a/apps/web/src/components/Sidebar/AddMemoryDialog.tsx b/apps/web/src/components/Sidebar/AddMemoryDialog.tsx index aa86966f..886507ff 100644 --- a/apps/web/src/components/Sidebar/AddMemoryDialog.tsx +++ b/apps/web/src/components/Sidebar/AddMemoryDialog.tsx @@ -10,13 +10,17 @@ import { Input } from "../ui/input"; import { Label } from "../ui/label"; import { Markdown } from "tiptap-markdown"; import { useEffect, useRef, useState } from "react"; -import { FilterCombobox } from "./FilterCombobox"; +import { FilterSpaces } from "./FilterCombobox"; +import { useMemory } from "@/contexts/MemoryContext"; export function AddMemoryPage() { + const { addMemory } = useMemory(); + + const [url, setUrl] = useState(""); const [selectedSpacesId, setSelectedSpacesId] = useState<number[]>([]); return ( - <div className="md:w-[40vw]"> + <form className="md:w-[40vw]"> <DialogHeader> <DialogTitle>Add a web page to memory</DialogTitle> <DialogDescription> @@ -30,25 +34,41 @@ export function AddMemoryPage() { type="url" data-modal-autofocus className="bg-rgray-4 mt-2 w-full" + value={url} + onChange={(e) => setUrl(e.target.value)} /> <DialogFooter> - <FilterCombobox + <FilterSpaces selectedSpaces={selectedSpacesId} setSelectedSpaces={setSelectedSpacesId} className="hover:bg-rgray-5 mr-auto bg-white/5" name={"Spaces"} /> - <DialogClose - type={undefined} + <button + type={"submit"} + onClick={async () => { + // @Dhravya this is adding a memory with insufficient information fix pls + await addMemory( + { + title: url, + content: "", + type: "page", + url: url, + image: "/icons/logo_without_bg.png", + savedAt: new Date(), + }, + selectedSpacesId, + ); + }} className="bg-rgray-4 hover:bg-rgray-5 focus-visible:bg-rgray-5 focus-visible:ring-rgray-7 rounded-md px-4 py-2 ring-transparent transition focus-visible:outline-none focus-visible:ring-2" > Add - </DialogClose> + </button> <DialogClose className="hover:bg-rgray-4 focus-visible:bg-rgray-4 focus-visible:ring-rgray-7 rounded-md px-3 py-2 ring-transparent transition focus-visible:outline-none focus-visible:ring-2"> Cancel </DialogClose> </DialogFooter> - </div> + </form> ); } @@ -60,16 +80,15 @@ export function NoteAddPage({ closeDialog }: { closeDialog: () => void }) { const [content, setContent] = useState(""); const [loading, setLoading] = useState(false); - function check() { + function check(): boolean { const data = { name: name.trim(), content, }; - console.log(name); if (!data.name || data.name.length < 1) { if (!inputRef.current) { alert("Please enter a name for the note"); - return; + return false; } inputRef.current.value = ""; inputRef.current.placeholder = "Please enter a title for the note"; @@ -79,7 +98,9 @@ export function NoteAddPage({ closeDialog }: { closeDialog: () => void }) { inputRef.current!.dataset["error"] = "false"; }, 500); inputRef.current.focus(); + return false; } + return true; } return ( @@ -87,7 +108,7 @@ export function NoteAddPage({ closeDialog }: { closeDialog: () => void }) { <Input ref={inputRef} data-error="false" - className="w-full border-none p-0 text-xl ring-0 placeholder:text-white/30 placeholder:transition placeholder:duration-200 focus-visible:ring-0 data-[error=true]:placeholder:text-red-400" + className="w-full border-none p-0 text-xl ring-0 placeholder:text-white/30 placeholder:transition placeholder:duration-500 focus-visible:ring-0 data-[error=true]:placeholder:text-red-400" placeholder="Title of the note" data-modal-autofocus value={name} @@ -104,7 +125,7 @@ export function NoteAddPage({ closeDialog }: { closeDialog: () => void }) { className="novel-editor bg-rgray-4 border-rgray-7 dark mt-5 max-h-[60vh] min-h-[40vh] w-[50vw] overflow-y-auto rounded-lg border [&>div>div]:p-5" /> <DialogFooter> - <FilterCombobox + <FilterSpaces selectedSpaces={selectedSpacesId} setSelectedSpaces={setSelectedSpacesId} className="hover:bg-rgray-5 mr-auto bg-white/5" @@ -112,7 +133,9 @@ export function NoteAddPage({ closeDialog }: { closeDialog: () => void }) { /> <button onClick={() => { - check(); + if (check()) { + closeDialog(); + } }} className="bg-rgray-4 hover:bg-rgray-5 focus-visible:bg-rgray-5 focus-visible:ring-rgray-7 rounded-md px-4 py-2 ring-transparent transition focus-visible:outline-none focus-visible:ring-2" > @@ -137,7 +160,7 @@ export function SpaceAddPage({ closeDialog }: { closeDialog: () => void }) { const [content, setContent] = useState(""); const [loading, setLoading] = useState(false); - function check() { + function check(): boolean { const data = { name: name.trim(), content, @@ -146,7 +169,7 @@ export function SpaceAddPage({ closeDialog }: { closeDialog: () => void }) { if (!data.name || data.name.length < 1) { if (!inputRef.current) { alert("Please enter a name for the note"); - return; + return false; } inputRef.current.value = ""; inputRef.current.placeholder = "Please enter a title for the note"; @@ -156,49 +179,32 @@ export function SpaceAddPage({ closeDialog }: { closeDialog: () => void }) { inputRef.current!.dataset["error"] = "false"; }, 500); inputRef.current.focus(); + return false; } + return true; } return ( - <div> + <div className="md:w-[40vw]"> + <DialogHeader> + <DialogTitle>Add a space</DialogTitle> + </DialogHeader> + <Label className="mt-5 block">Name</Label> <Input - ref={inputRef} - data-error="false" - className="w-full border-none p-0 text-xl ring-0 placeholder:text-white/30 placeholder:transition placeholder:duration-200 focus-visible:ring-0 data-[error=true]:placeholder:text-red-400" - placeholder="Title of the note" + placeholder="Enter the name of the space" + type="url" data-modal-autofocus - value={name} - onChange={(e) => setName(e.target.value)} - /> - <Editor - disableLocalStorage - defaultValue={""} - onUpdate={(editor) => { - if (!editor) return; - setContent(editor.storage.markdown.getMarkdown()); - }} - extensions={[Markdown]} - className="novel-editor bg-rgray-4 border-rgray-7 dark mt-5 max-h-[60vh] min-h-[40vh] w-[50vw] overflow-y-auto rounded-lg border [&>div>div]:p-5" + className="bg-rgray-4 mt-2 w-full" /> + <Label className="mt-5 block">Memories</Label> <DialogFooter> - <FilterCombobox - selectedSpaces={selectedSpacesId} - setSelectedSpaces={setSelectedSpacesId} - className="hover:bg-rgray-5 mr-auto bg-white/5" - name={"Spaces"} - /> - <button - onClick={() => { - check(); - }} - className="bg-rgray-4 hover:bg-rgray-5 focus-visible:bg-rgray-5 focus-visible:ring-rgray-7 rounded-md px-4 py-2 ring-transparent transition focus-visible:outline-none focus-visible:ring-2" - > - Add - </button> <DialogClose type={undefined} - className="hover:bg-rgray-4 focus-visible:bg-rgray-4 focus-visible:ring-rgray-7 rounded-md px-3 py-2 ring-transparent transition focus-visible:outline-none focus-visible:ring-2" + className="bg-rgray-4 hover:bg-rgray-5 focus-visible:bg-rgray-5 focus-visible:ring-rgray-7 rounded-md px-4 py-2 ring-transparent transition focus-visible:outline-none focus-visible:ring-2" > + Add + </DialogClose> + <DialogClose className="hover:bg-rgray-4 focus-visible:bg-rgray-4 focus-visible:ring-rgray-7 rounded-md px-3 py-2 ring-transparent transition focus-visible:outline-none focus-visible:ring-2"> Cancel </DialogClose> </DialogFooter> diff --git a/apps/web/src/components/Sidebar/FilterCombobox.tsx b/apps/web/src/components/Sidebar/FilterCombobox.tsx index 04ff0324..0a93ee55 100644 --- a/apps/web/src/components/Sidebar/FilterCombobox.tsx +++ b/apps/web/src/components/Sidebar/FilterCombobox.tsx @@ -33,7 +33,124 @@ export interface Props extends React.ButtonHTMLAttributes<HTMLButtonElement> { name: string; } -export function FilterCombobox({ +export function FilterSpaces({ + className, + side = "bottom", + align = "center", + onClose, + selectedSpaces, + setSelectedSpaces, + name, + ...props +}: Props) { + const { spaces } = useMemory(); + const [open, setOpen] = React.useState(false); + + const sortedSpaces = spaces.sort(({ id: a }, { id: b }) => + selectedSpaces.includes(a) && !selectedSpaces.includes(b) + ? -1 + : selectedSpaces.includes(b) && !selectedSpaces.includes(a) + ? 1 + : 0, + ); + + React.useEffect(() => { + if (!open) { + onClose?.(); + } + }, [open]); + + return ( + <AnimatePresence mode="popLayout"> + <LayoutGroup> + <Popover open={open} onOpenChange={setOpen}> + <PopoverTrigger asChild> + <button + type={undefined} + data-state-on={open} + className={cn( + "text-rgray-11/70 on:bg-rgray-3 focus-visible:ring-rgray-8 hover:bg-rgray-3 relative flex items-center justify-center gap-1 rounded-md px-3 py-1.5 ring-2 ring-transparent focus-visible:outline-none", + className, + )} + {...props} + > + <SpaceIcon className="mr-1 h-5 w-5" /> + {name} + <ChevronsUpDown className="h-4 w-4" /> + <div + data-state-on={selectedSpaces.length > 0} + className="on:flex text-rgray-11 border-rgray-6 bg-rgray-2 absolute left-0 top-0 hidden aspect-[1] h-4 w-4 -translate-x-1/3 -translate-y-1/3 items-center justify-center rounded-full border text-center text-[9px]" + > + {selectedSpaces.length} + </div> + </button> + </PopoverTrigger> + <PopoverContent + onCloseAutoFocus={(e) => e.preventDefault()} + align={align} + side={side} + className="w-[200px] p-0" + > + <Command + filter={(val, search) => + spaces + .find((s) => s.id.toString() === val) + ?.title.toLowerCase() + .includes(search.toLowerCase().trim()) + ? 1 + : 0 + } + > + <CommandInput placeholder="Filter spaces..." /> + <CommandList asChild> + <motion.div layoutScroll> + <CommandEmpty>Nothing found</CommandEmpty> + <CommandGroup> + {sortedSpaces.map((space) => ( + <CommandItem + key={space.id} + value={space.id.toString()} + onSelect={(val) => { + setSelectedSpaces((prev: number[]) => + prev.includes(parseInt(val)) + ? prev.filter((v) => v !== parseInt(val)) + : [...prev, parseInt(val)], + ); + }} + asChild + > + <motion.div + initial={{ opacity: 0 }} + animate={{ opacity: 1, transition: { delay: 0.05 } }} + transition={{ duration: 0.15 }} + layout + layoutId={`space-combobox-${space.id}`} + className="text-rgray-11" + > + <SpaceIcon className="mr-2 h-4 w-4" /> + {space.title} + {selectedSpaces.includes(space.id)} + <Check + data-state-on={selectedSpaces.includes(space.id)} + className={cn( + "on:opacity-100 ml-auto h-4 w-4 opacity-0", + )} + /> + </motion.div> + </CommandItem> + ))} + </CommandGroup> + </motion.div> + </CommandList> + </Command> + </PopoverContent> + </Popover> + </LayoutGroup> + </AnimatePresence> + ); +} + +export function FilterMemories({ className, side = "bottom", align = "center", diff --git a/apps/web/src/components/Sidebar/MemoriesBar.tsx b/apps/web/src/components/Sidebar/MemoriesBar.tsx index 3d7bd8b9..66c3138b 100644 --- a/apps/web/src/components/Sidebar/MemoriesBar.tsx +++ b/apps/web/src/components/Sidebar/MemoriesBar.tsx @@ -40,7 +40,7 @@ import { Label } from "../ui/label"; import useViewport from "@/hooks/useViewport"; import useTouchHold from "@/hooks/useTouchHold"; import { DialogTrigger } from "@radix-ui/react-dialog"; -import { AddMemoryPage, NoteAddPage } from "./AddMemoryDialog"; +import { AddMemoryPage, NoteAddPage, SpaceAddPage } from "./AddMemoryDialog"; export function MemoriesBar() { const [parent, enableAnimations] = useAutoAnimate(); @@ -91,10 +91,16 @@ export function MemoriesBar() { Note </DropdownMenuItem> </DialogTrigger> - <DropdownMenuItem> - <SpaceIcon className="mr-2 h-4 w-4" /> - Space - </DropdownMenuItem> + <DialogTrigger className="block w-full"> + <DropdownMenuItem + onClick={() => { + setAddMemoryState("space"); + }} + > + <SpaceIcon className="mr-2 h-4 w-4" /> + Space + </DropdownMenuItem> + </DialogTrigger> </DropdownMenuContent> </DropdownMenu> </AddMemoryModal> @@ -343,6 +349,8 @@ export function AddMemoryModal({ <AddMemoryPage /> ) : type === "note" ? ( <NoteAddPage closeDialog={() => setIsDialogOpen(false)} /> + ) : type === "space" ? ( + <SpaceAddPage closeDialog={() => setIsDialogOpen(false)} /> ) : ( <></> )} diff --git a/apps/web/src/contexts/MemoryContext.tsx b/apps/web/src/contexts/MemoryContext.tsx index eab1e4fe..68a22434 100644 --- a/apps/web/src/contexts/MemoryContext.tsx +++ b/apps/web/src/contexts/MemoryContext.tsx @@ -1,22 +1,37 @@ "use client"; import React, { useCallback } from "react"; import { CollectedSpaces } from "../../types/memory"; +import { StoredContent, storedContent } from "@/server/db/schema"; +import { useSession } from "next-auth/react"; +import { addMemory } from "@/actions/db"; // temperory (will change) export const MemoryContext = React.createContext<{ spaces: CollectedSpaces[]; deleteSpace: (id: number) => Promise<void>; + freeMemories: StoredContent[]; addSpace: (space: CollectedSpaces) => Promise<void>; + addMemory: ( + memory: typeof storedContent.$inferInsert, + spaces?: number[], + ) => Promise<void>; }>({ spaces: [], - addSpace: async (space) => {}, - deleteSpace: async (id) => {}, + freeMemories: [], + addMemory: async () => {}, + addSpace: async () => {}, + deleteSpace: async () => {}, }); export const MemoryProvider: React.FC< - { spaces: CollectedSpaces[] } & React.PropsWithChildren -> = ({ children, spaces: initalSpaces }) => { + { + spaces: CollectedSpaces[]; + freeMemories: StoredContent[]; + } & React.PropsWithChildren +> = ({ children, spaces: initalSpaces, freeMemories: initialFreeMemories }) => { const [spaces, setSpaces] = React.useState<CollectedSpaces[]>(initalSpaces); + const [freeMemories, setFreeMemories] = + React.useState<StoredContent[]>(initialFreeMemories); const addSpace = useCallback( async (space: CollectedSpaces) => { @@ -31,8 +46,31 @@ export const MemoryProvider: React.FC< [spaces], ); + // const fetchMemories = useCallback(async (query: string) => { + // const response = await fetch(`/api/memories?${query}`); + // }, []); + + const _addMemory = useCallback( + async ( + memory: typeof storedContent.$inferInsert, + spaces: number[] = [], + ) => { + const content = await addMemory(memory, spaces); + console.log(content); + }, + [freeMemories, spaces], + ); + return ( - <MemoryContext.Provider value={{ spaces, addSpace, deleteSpace }}> + <MemoryContext.Provider + value={{ + spaces, + addSpace, + deleteSpace, + freeMemories, + addMemory: _addMemory, + }} + > {children} </MemoryContext.Provider> ); |