aboutsummaryrefslogtreecommitdiff
path: root/apps/web/src/components
diff options
context:
space:
mode:
authoryxshv <[email protected]>2024-04-14 14:29:23 +0530
committeryxshv <[email protected]>2024-04-14 14:29:23 +0530
commitfa39265142a7aa452a273e4290d58757af2786bb (patch)
tree52e2e07d2a20009d650ed0b3ebe60aaab87d81ff /apps/web/src/components
parentfixed notes vectorize (diff)
downloadsupermemory-fa39265142a7aa452a273e4290d58757af2786bb.tar.xz
supermemory-fa39265142a7aa452a273e4290d58757af2786bb.zip
new modals
Diffstat (limited to 'apps/web/src/components')
-rw-r--r--apps/web/src/components/ChatMessage.tsx22
-rw-r--r--apps/web/src/components/Main.tsx25
-rw-r--r--apps/web/src/components/Sidebar/AddMemoryDialog.tsx3
-rw-r--r--apps/web/src/components/Sidebar/DeleteConfirmation.tsx35
-rw-r--r--apps/web/src/components/Sidebar/EditNoteDialog.tsx152
-rw-r--r--apps/web/src/components/Sidebar/FilterCombobox.tsx5
-rw-r--r--apps/web/src/components/Sidebar/MemoriesBar.tsx98
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>
);
}