diff options
| author | yxshv <[email protected]> | 2024-04-14 14:29:23 +0530 |
|---|---|---|
| committer | yxshv <[email protected]> | 2024-04-14 14:29:23 +0530 |
| commit | fa39265142a7aa452a273e4290d58757af2786bb (patch) | |
| tree | 52e2e07d2a20009d650ed0b3ebe60aaab87d81ff /apps/web/src/components | |
| parent | fixed notes vectorize (diff) | |
| download | supermemory-fa39265142a7aa452a273e4290d58757af2786bb.tar.xz supermemory-fa39265142a7aa452a273e4290d58757af2786bb.zip | |
new modals
Diffstat (limited to 'apps/web/src/components')
| -rw-r--r-- | apps/web/src/components/ChatMessage.tsx | 22 | ||||
| -rw-r--r-- | apps/web/src/components/Main.tsx | 25 | ||||
| -rw-r--r-- | apps/web/src/components/Sidebar/AddMemoryDialog.tsx | 3 | ||||
| -rw-r--r-- | apps/web/src/components/Sidebar/DeleteConfirmation.tsx | 35 | ||||
| -rw-r--r-- | apps/web/src/components/Sidebar/EditNoteDialog.tsx | 152 | ||||
| -rw-r--r-- | apps/web/src/components/Sidebar/FilterCombobox.tsx | 5 | ||||
| -rw-r--r-- | apps/web/src/components/Sidebar/MemoriesBar.tsx | 98 |
7 files changed, 277 insertions, 63 deletions
diff --git a/apps/web/src/components/ChatMessage.tsx b/apps/web/src/components/ChatMessage.tsx index db0778c4..1567a9ac 100644 --- a/apps/web/src/components/ChatMessage.tsx +++ b/apps/web/src/components/ChatMessage.tsx @@ -1,9 +1,10 @@ import React, { useEffect } from "react"; import { motion } from "framer-motion"; -import { ArrowUpRight, Globe } from "lucide-react"; +import { ArrowUpRight, Globe, Text } from "lucide-react"; import { convertRemToPixels } from "@/lib/utils"; import { SpaceIcon } from "@/assets/Memories"; import Markdown from "react-markdown"; +import { ChatHistory } from "../../types/memory"; export function ChatAnswer({ children: message, @@ -11,7 +12,7 @@ export function ChatAnswer({ loading = false, }: { children: string; - sources?: string[]; + sources?: ChatHistory['answer']['sources']; loading?: boolean; }) { return ( @@ -29,15 +30,22 @@ export function ChatAnswer({ <SpaceIcon className="h-6 w-6 -translate-y-[2px]" /> Related Memories </h1> - <div className="animate-fade-in -mt-3 flex items-center justify-start opacity-0 [animation-duration:1s]"> - {sources?.map((source) => ( + <div className="animate-fade-in gap-1 -mt-3 flex items-center justify-start opacity-0 [animation-duration:1s]"> + {sources?.map((source) => source.isNote ? ( + <button + className="bg-rgray-3 flex items-center justify-center gap-2 rounded-full py-1 pl-2 pr-3 text-sm" + > + <Text className="w-4 h-4" /> + {source.source} + </button> + ) : ( <a className="bg-rgray-3 flex items-center justify-center gap-2 rounded-full py-1 pl-2 pr-3 text-sm" - key={source} - href={source} + key={source.source} + href={source.source} > <Globe className="h-4 w-4" /> - {cleanUrl(source)} + {cleanUrl(source.source)} </a> ))} </div> diff --git a/apps/web/src/components/Main.tsx b/apps/web/src/components/Main.tsx index bbdb630d..df6a08bf 100644 --- a/apps/web/src/components/Main.tsx +++ b/apps/web/src/components/Main.tsx @@ -13,6 +13,7 @@ import { useRouter, useSearchParams } from "next/navigation"; import { useMemory } from "@/contexts/MemoryContext"; import Image from "next/image"; +import { getMemoriesFromUrl } from "@/actions/db"; function supportsDVH() { try { @@ -185,11 +186,25 @@ export default function Main({ sidebarOpen }: { sidebarOpen: boolean }) { }, ); - const sourcesInJson = (await sourcesResponse.json()) as { - ids: string[]; - }; - console.log(sourcesInJson); + const sourcesInJson = getIdsFromSource(((await sourcesResponse.json()) as { + ids: string[] + }).ids) ?? []; + + + const notesInSources = sourcesInJson.filter( + (urls) => urls.startsWith("https://notes.supermemory.dhr.wtf/") + ) + const nonNotes = sourcesInJson.filter( + i => !notesInSources.includes(i) + ) + + const fetchedTitles = await getMemoriesFromUrl(notesInSources); + + const sources = [ + ...nonNotes.map(n => ({ isNote: false, source: n ?? "<unnamed>" })), + ...fetchedTitles.map(n => ({ isNote: true, source: n.title ?? "<unnamed>" })) + ] setIsAiLoading(false); setChatHistory((prev) => { @@ -200,7 +215,7 @@ export default function Main({ sidebarOpen }: { sidebarOpen: boolean }) { ...lastMessage, answer: { parts: lastMessage.answer.parts, - sources: getIdsFromSource(sourcesInJson.ids) ?? [], + sources }, }, ]; diff --git a/apps/web/src/components/Sidebar/AddMemoryDialog.tsx b/apps/web/src/components/Sidebar/AddMemoryDialog.tsx index 95bb3d22..f6a7224f 100644 --- a/apps/web/src/components/Sidebar/AddMemoryDialog.tsx +++ b/apps/web/src/components/Sidebar/AddMemoryDialog.tsx @@ -165,13 +165,12 @@ export function NoteAddPage({ closeDialog }: { closeDialog: () => void }) { onClick={() => { if (check()) { setLoading(true); - const randomId = Math.floor(Math.random() * 1000000); addMemory( { content, title: name, type: "note", - url: `https://notes.supermemory.dhr.wtf/${randomId}`, + url: `https://notes.supermemory.dhr.wtf/`, image: "", savedAt: new Date(), }, diff --git a/apps/web/src/components/Sidebar/DeleteConfirmation.tsx b/apps/web/src/components/Sidebar/DeleteConfirmation.tsx new file mode 100644 index 00000000..9324b147 --- /dev/null +++ b/apps/web/src/components/Sidebar/DeleteConfirmation.tsx @@ -0,0 +1,35 @@ +import { Dialog, DialogContent, DialogTrigger, DialogTitle, DialogDescription, DialogClose, DialogFooter } from "../ui/dialog"; + +export default function DeleteConfirmation({ onDelete, trigger = true, children }: { trigger?: boolean, onDelete?: () => void; children: React.ReactNode }) { + return ( + <Dialog> + {trigger ? ( + <DialogTrigger asChild> + {children} + </DialogTrigger> + ) : ( + <> + {children} + </> + )} + <DialogContent> + <DialogTitle className="text-xl">Are you sure?</DialogTitle> + <DialogDescription className="text-md"> + You will not be able to recover this it. + </DialogDescription> + <DialogFooter> + <DialogClose + type={undefined} + onClick={onDelete} + className="ml-auto flex items-center justify-center rounded-md text-red-400 bg-red-100/10 px-3 py-2 transition hover:bg-red-100/5 focus-visible:bg-red-100/5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-red-100/30" + > + Delete + </DialogClose> + <DialogClose className="focus-visible:bg-rgray-4 focus-visible:ring-rgray-7 hover:bg-rgray-4 ml-auto flex items-center justify-center rounded-md px-3 py-2 transition focus-visible:outline-none focus-visible:ring-2"> + Cancel + </DialogClose> + </DialogFooter> + </DialogContent> + </Dialog> + ) +} diff --git a/apps/web/src/components/Sidebar/EditNoteDialog.tsx b/apps/web/src/components/Sidebar/EditNoteDialog.tsx new file mode 100644 index 00000000..b7760656 --- /dev/null +++ b/apps/web/src/components/Sidebar/EditNoteDialog.tsx @@ -0,0 +1,152 @@ + +import { Editor } from "novel"; +import { + DialogClose, + DialogFooter, +} from "../ui/dialog"; +import { Input } from "../ui/input"; +import { Markdown } from "tiptap-markdown"; +import { useEffect, useRef, useState } from "react"; +import { FilterSpaces } from "./FilterCombobox"; +import { useMemory } from "@/contexts/MemoryContext"; +import { Loader, Plus, Trash, X } from "lucide-react"; +import { motion } from "framer-motion"; +import { StoredContent } from "@/server/db/schema"; +import { fetchContent } from "@/actions/db"; +import { isArraysEqual } from "@/lib/utils"; +import DeleteConfirmation from "./DeleteConfirmation"; + + +export function NoteEdit({ memory, closeDialog }: { memory: StoredContent, closeDialog: () => any }) { + const { updateMemory, deleteMemory } = useMemory(); + + const [initialSpaces, setInitialSpaces] = useState<number[]>([]) + const [selectedSpacesId, setSelectedSpacesId] = useState<number[]>([]); + + const inputRef = useRef<HTMLInputElement>(null); + const [name, setName] = useState(memory.title ?? ""); + const [content, setContent] = useState(memory.content); + const [loading, setLoading] = useState(false); + + function check(): boolean { + const data = { + name: name.trim(), + content, + }; + if (!data.name || data.name.length < 1) { + if (!inputRef.current) { + alert("Please enter a name for the note"); + return false; + } + inputRef.current.value = ""; + inputRef.current.placeholder = "Please enter a title for the note"; + inputRef.current.dataset["error"] = "true"; + setTimeout(() => { + inputRef.current!.placeholder = "Title of the note"; + inputRef.current!.dataset["error"] = "false"; + }, 500); + inputRef.current.focus(); + return false; + } + return true; + } + + useEffect(() => { + fetchContent(memory.id).then((data) => { + if (data?.spaces) { + setInitialSpaces(data.spaces) + setSelectedSpacesId(data.spaces) + } + }) + }, []) + + return ( + <div> + <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-500 focus-visible:ring-0 data-[error=true]:placeholder:text-red-400" + placeholder="Title of the note" + value={name} + disabled={loading} + onChange={(e) => setName(e.target.value)} + /> + <Editor + disableLocalStorage + defaultValue={memory.content} + 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" + /> + <DialogFooter> + <FilterSpaces + selectedSpaces={selectedSpacesId} + setSelectedSpaces={setSelectedSpacesId} + className="hover:bg-rgray-5 mr-auto bg-white/5" + name={"Spaces"} + /> + <DeleteConfirmation onDelete={() => { + deleteMemory(memory.id) + }}> + <button + type={undefined} + disabled={loading} + className="focus-visible:bg-red-100 focus-visible:text-red-400 dark:focus-visible:bg-red-100/10 hover:bg-red-100 dark:hover:bg-red-100/10 hover:text-red-400 rounded-md px-3 py-2 ring-transparent transition focus-visible:outline-none focus-visible:ring-2 disabled:cursor-not-allowed disabled:opacity-70" + > + <Trash className="w-5 h-5" /> + </button> + </DeleteConfirmation> + <button + onClick={() => { + if (check()) { + setLoading(true); + console.log( + + { + title: name === memory.title ? undefined : name, + content: content === memory.content ? undefined : content, + spaces: isArraysEqual(initialSpaces, selectedSpacesId) ? undefined : selectedSpacesId, + }, + ) + updateMemory( + memory.id, + { + title: name === memory.title ? undefined : name, + content: content === memory.content ? undefined : content, + spaces: isArraysEqual(initialSpaces, selectedSpacesId) ? undefined : selectedSpacesId, + }, + ).then(closeDialog); + } + }} + disabled={loading} + className="bg-rgray-4 hover:bg-rgray-5 focus-visible:bg-rgray-5 focus-visible:ring-rgray-7 relative rounded-md px-4 py-2 ring-transparent transition focus-visible:outline-none focus-visible:ring-2 disabled:cursor-not-allowed disabled:opacity-70" + > + <motion.div + initial={{ x: "-50%", y: "-100%" }} + animate={loading && { y: "-50%", x: "-50%", opacity: 1 }} + className="absolute left-1/2 top-1/2 -translate-x-1/2 translate-y-[-100%] opacity-0" + > + <Loader className="text-rgray-11 h-5 w-5 animate-spin" /> + </motion.div> + <motion.div + initial={{ y: "0%" }} + animate={loading && { opacity: 0, y: "30%" }} + > + Save + </motion.div> + </button> + <DialogClose + type={undefined} + disabled={loading} + 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 disabled:cursor-not-allowed disabled:opacity-70" + > + Cancel + </DialogClose> + </DialogFooter> + </div> + ); +} + diff --git a/apps/web/src/components/Sidebar/FilterCombobox.tsx b/apps/web/src/components/Sidebar/FilterCombobox.tsx index f93ae710..7625e2b6 100644 --- a/apps/web/src/components/Sidebar/FilterCombobox.tsx +++ b/apps/web/src/components/Sidebar/FilterCombobox.tsx @@ -90,6 +90,7 @@ export function FilterSpaces({ align={align} side={side} className="w-[200px] p-0" + onCloseAutoFocus={e => e.preventDefault()} > <Command filter={(val, search) => @@ -128,7 +129,7 @@ export function FilterSpaces({ className="text-rgray-11" > <SpaceIcon className="mr-2 h-4 w-4" /> - {space.name} + {space.name.length > 10 ? space.name.slice(0, 10) + "..." : space.name} {selectedSpaces.includes(space.id)} <Check data-state-on={selectedSpaces.includes(space.id)} @@ -267,7 +268,7 @@ export function FilterMemories({ } className="mr-2 h-4 w-4" /> - {m.title} + {(m.title && m.title?.length > 14) ? m.title?.slice(0, 14) + "..." : m.title} <Check data-state-on={ selected.find((i) => i.id === m.id) !== undefined diff --git a/apps/web/src/components/Sidebar/MemoriesBar.tsx b/apps/web/src/components/Sidebar/MemoriesBar.tsx index 6c640e26..1218407b 100644 --- a/apps/web/src/components/Sidebar/MemoriesBar.tsx +++ b/apps/web/src/components/Sidebar/MemoriesBar.tsx @@ -37,15 +37,15 @@ import { DialogFooter, DialogClose, } from "../ui/dialog"; -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, SpaceAddPage } from "./AddMemoryDialog"; import { ExpandedSpace } from "./ExpandedSpace"; import { StoredContent, StoredSpace } from "@/server/db/schema"; -import Image from "next/image"; import { useDebounce } from "@/hooks/useDebounce"; +import { NoteEdit } from "./EditNoteDialog"; +import DeleteConfirmation from "./DeleteConfirmation"; export function MemoriesBar() { const [parent, enableAnimations] = useAutoAnimate(); @@ -194,39 +194,58 @@ const SpaceExitVariant: Variant = { }, }; -export function MemoryItem({ id, title, image, type }: StoredContent) { +export function MemoryItem(props: StoredContent) { + + const { id, title, image, type } = props + const name = title ? title.length > 10 ? title.slice(0, 10) + "..." : title : "<no title>"; + const [isDialogOpen, setIsDialogOpen] = useState(false); + return ( - <div className="hover:bg-rgray-2 has-[[data-state='true']]:bg-rgray-2 has-[[data-space-text]:focus-visible]:bg-rgray-2 has-[[data-space-text]:focus-visible]:ring-rgray-7 [&:has-[[data-space-text]:focus-visible]>[data-more-button]]:opacity-100 relative flex select-none flex-col-reverse items-center justify-center rounded-md p-2 pb-4 text-center font-normal ring-transparent transition has-[[data-space-text]:focus-visible]:outline-none has-[[data-space-text]:focus-visible]:ring-2 md:has-[[data-state='true']]:bg-transparent [&:hover>[data-more-button]]:opacity-100"> - <button data-space-text className="focus-visible:outline-none"> - {name} - </button> + <Dialog open={type === "note" ? isDialogOpen : false} onOpenChange={setIsDialogOpen}> + <div onClick={() => setIsDialogOpen(true)} className="cursor-pointer hover:bg-rgray-2 has-[[data-state='true']]:bg-rgray-2 has-[[data-space-text]:focus-visible]:bg-rgray-2 has-[[data-space-text]:focus-visible]:ring-rgray-7 [&:has-[[data-space-text]:focus-visible]>[data-more-button]]:opacity-100 relative flex select-none flex-col-reverse items-center justify-center rounded-md p-2 pb-4 text-center font-normal ring-transparent transition has-[[data-space-text]:focus-visible]:outline-none has-[[data-space-text]:focus-visible]:ring-2 md:has-[[data-state='true']]:bg-transparent [&:hover>[data-more-button]]:opacity-100"> + { + type === "note" ? + ( + <DialogTrigger asChild> + <button data-space-text className="focus-visible:outline-none"> + {name} + </button> + </DialogTrigger> + ) : ( + <button data-space-text className="focus-visible:outline-none"> + {name} + </button> + ) + } - <div className="flex h-24 w-24 items-center justify-center"> - {type === "page" ? ( - <img - className="h-16 w-16" - id={id.toString()} - src={image!} - onError={(e) => { - (e.target as HTMLImageElement).src = - "/icons/white_without_bg.png"; - }} - /> - ) : type === "note" ? ( - <div className="bg-rgray-4 flex items-center justify-center rounded-md p-2 shadow-md"> - <Text className="h-10 w-10" /> - </div> - ) : ( - <></> - )} - </div> - </div> + <div className="flex h-24 w-24 items-center justify-center"> + {type === "page" ? ( + <img + className="h-16 w-16" + id={id.toString()} + src={image!} + onError={(e) => { + (e.target as HTMLImageElement).src = + "/icons/white_without_bg.png"; + }} + /> + ) : type === "note" ? ( + <Text className="h-16 w-16" /> + ) : ( + <></> + )} + </div> + </div> + <DialogContent className="w-max max-w-[auto]"> + <NoteEdit closeDialog={() => setIsDialogOpen(false)} memory={props} /> + </DialogContent> + </Dialog> ); } @@ -254,6 +273,9 @@ export function SpaceItem({ }, [cachedMemories]); const _name = name.length > 10 ? name.slice(0, 10) + "..." : name; + + console.log(spaceMemories) + return ( <motion.div ref={itemRef} @@ -396,7 +418,7 @@ export function SpaceMoreButton({ setIsOpen?: (open: boolean) => void; }) { return ( - <Dialog> + <DeleteConfirmation onDelete={onDelete} trigger={false}> <DropdownMenu open={isOpen} onOpenChange={setIsOpen}> <DropdownMenuTrigger asChild> <button @@ -426,25 +448,7 @@ export function SpaceMoreButton({ </DialogTrigger> </DropdownMenuContent> </DropdownMenu> - <DialogContent> - <DialogTitle className="text-xl">Are you sure?</DialogTitle> - <DialogDescription className="text-md"> - You will not be able to recover this space - </DialogDescription> - <DialogFooter> - <DialogClose - type={undefined} - onClick={onDelete} - className="ml-auto flex items-center justify-center rounded-md bg-red-500/40 px-3 py-2 transition hover:bg-red-500/60 focus-visible:bg-red-500/60 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-red-500" - > - Delete - </DialogClose> - <DialogClose className="focus-visible:bg-rgray-4 focus-visible:ring-rgray-7 hover:bg-rgray-4 ml-auto flex items-center justify-center rounded-md px-3 py-2 transition focus-visible:outline-none focus-visible:ring-2"> - Cancel - </DialogClose> - </DialogFooter> - </DialogContent> - </Dialog> + </DeleteConfirmation> ); } |