mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-05-11 13:11:15 +00:00
352 lines
12 KiB
TypeScript
352 lines
12 KiB
TypeScript
import { Editor } from "novel";
|
|
import {
|
|
DialogClose,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from "../ui/dialog";
|
|
import { Input } from "../ui/input";
|
|
import { Label } from "../ui/label";
|
|
import { Markdown } from "tiptap-markdown";
|
|
import { useEffect, useRef, useState } from "react";
|
|
import { FilterMemories, FilterSpaces } from "./FilterCombobox";
|
|
import { useMemory } from "@/contexts/MemoryContext";
|
|
import { Loader, Plus, X } from "lucide-react";
|
|
import { StoredContent } from "@/server/db/schema";
|
|
import { cleanUrl } from "@/lib/utils";
|
|
import { motion } from "framer-motion";
|
|
import { getMetaData } from "@/server/helpers";
|
|
|
|
export function AddMemoryPage({ closeDialog }: { closeDialog: () => void }) {
|
|
const { addMemory } = useMemory();
|
|
|
|
const [loading, setLoading] = useState(false);
|
|
const [url, setUrl] = useState("");
|
|
const [selectedSpacesId, setSelectedSpacesId] = useState<number[]>([]);
|
|
|
|
return (
|
|
<div className="md:w-[40vw]">
|
|
<DialogHeader>
|
|
<DialogTitle>Add a web page to memory</DialogTitle>
|
|
<DialogDescription>
|
|
This will take you the web page you are trying to add to memory, where
|
|
the extension will save the page to memory
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<Label className="mt-5 block">URL</Label>
|
|
<Input
|
|
placeholder="Enter the URL of the page"
|
|
type="url"
|
|
data-modal-autofocus
|
|
className="bg-rgray-4 mt-2 w-full disabled:cursor-not-allowed disabled:opacity-70"
|
|
value={url}
|
|
onChange={(e) => setUrl(e.target.value)}
|
|
disabled={loading}
|
|
/>
|
|
<DialogFooter>
|
|
<FilterSpaces
|
|
selectedSpaces={selectedSpacesId}
|
|
setSelectedSpaces={setSelectedSpacesId}
|
|
className="hover:bg-rgray-5 mr-auto bg-white/5 disabled:cursor-not-allowed disabled:opacity-70"
|
|
name={"Spaces"}
|
|
disabled={loading}
|
|
/>
|
|
<button
|
|
type={"submit"}
|
|
disabled={loading}
|
|
onClick={async () => {
|
|
setLoading(true);
|
|
const metadata = await getMetaData(url);
|
|
await addMemory(
|
|
{
|
|
title: metadata.title,
|
|
description: metadata.description,
|
|
content: "",
|
|
type: "page",
|
|
url: url,
|
|
image: metadata.image,
|
|
savedAt: new Date(),
|
|
},
|
|
selectedSpacesId,
|
|
);
|
|
closeDialog();
|
|
}}
|
|
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%" }}
|
|
>
|
|
Add
|
|
</motion.div>
|
|
</button>
|
|
<DialogClose
|
|
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>
|
|
);
|
|
}
|
|
|
|
export function NoteAddPage({ closeDialog }: { closeDialog: () => void }) {
|
|
const { addMemory } = useMemory();
|
|
|
|
const [selectedSpacesId, setSelectedSpacesId] = useState<number[]>([]);
|
|
|
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
const [name, setName] = useState("");
|
|
const [content, setContent] = useState("");
|
|
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;
|
|
}
|
|
|
|
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"
|
|
data-modal-autofocus
|
|
value={name}
|
|
disabled={loading}
|
|
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"
|
|
/>
|
|
<DialogFooter>
|
|
<FilterSpaces
|
|
selectedSpaces={selectedSpacesId}
|
|
setSelectedSpaces={setSelectedSpacesId}
|
|
className="hover:bg-rgray-5 mr-auto bg-white/5"
|
|
name={"Spaces"}
|
|
/>
|
|
<button
|
|
onClick={() => {
|
|
if (check()) {
|
|
setLoading(true);
|
|
addMemory(
|
|
{
|
|
content,
|
|
title: name,
|
|
type: "note",
|
|
url: `https://notes.supermemory.dhr.wtf/`,
|
|
image: "",
|
|
savedAt: new Date(),
|
|
},
|
|
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%" }}
|
|
>
|
|
Add
|
|
</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>
|
|
);
|
|
}
|
|
|
|
export function SpaceAddPage({ closeDialog }: { closeDialog: () => void }) {
|
|
const { addSpace } = useMemory();
|
|
|
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
const [name, setName] = useState("");
|
|
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
const [selected, setSelected] = useState<StoredContent[]>([]);
|
|
|
|
function check(): boolean {
|
|
const data = {
|
|
name: name.trim(),
|
|
};
|
|
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 space";
|
|
inputRef.current.dataset["error"] = "true";
|
|
setTimeout(() => {
|
|
inputRef.current!.placeholder = "Enter the name of the space";
|
|
inputRef.current!.dataset["error"] = "false";
|
|
}, 500);
|
|
inputRef.current.focus();
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
return (
|
|
<div className="md:w-[40vw]">
|
|
<DialogHeader>
|
|
<DialogTitle>Add a space</DialogTitle>
|
|
</DialogHeader>
|
|
<Label className="mt-5 block">Name</Label>
|
|
<Input
|
|
ref={inputRef}
|
|
placeholder="Enter the name of the space"
|
|
type="url"
|
|
data-modal-autofocus
|
|
value={name}
|
|
disabled={loading}
|
|
onChange={(e) => setName(e.target.value)}
|
|
className="bg-rgray-4 mt-2 w-full placeholder:transition placeholder:duration-500 data-[error=true]:placeholder:text-red-400 focus-visible:data-[error=true]:ring-red-500/10"
|
|
/>
|
|
{selected.length > 0 && (
|
|
<>
|
|
<Label className="mt-5 block">Add Memories</Label>
|
|
<div className="flex min-h-5 flex-col items-center justify-center py-2">
|
|
{selected.map((i) => (
|
|
<MemorySelectedItem
|
|
key={i.id}
|
|
onRemove={() =>
|
|
setSelected((prev) => prev.filter((p) => p.id !== i.id))
|
|
}
|
|
{...i}
|
|
/>
|
|
))}
|
|
</div>
|
|
</>
|
|
)}
|
|
<DialogFooter>
|
|
<FilterMemories
|
|
selected={selected}
|
|
setSelected={setSelected}
|
|
disabled={loading}
|
|
className="hover:bg-rgray-4 focus-visible:bg-rgray-4 mr-auto bg-white/5 disabled:cursor-not-allowed disabled:opacity-70"
|
|
>
|
|
<Plus className="h-5 w-5" />
|
|
Memory
|
|
</FilterMemories>
|
|
<button
|
|
type={undefined}
|
|
onClick={() => {
|
|
if (check()) {
|
|
setLoading(true);
|
|
addSpace(
|
|
name,
|
|
selected.map((s) => s.id),
|
|
).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%" }}
|
|
>
|
|
Add
|
|
</motion.div>
|
|
</button>
|
|
<DialogClose
|
|
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>
|
|
);
|
|
}
|
|
|
|
export function MemorySelectedItem({
|
|
id,
|
|
title,
|
|
url,
|
|
type,
|
|
image,
|
|
onRemove,
|
|
}: StoredContent & { onRemove: () => void }) {
|
|
return (
|
|
<div className="hover:bg-rgray-4 focus-within-bg-rgray-4 flex w-full items-center justify-start gap-2 rounded-md p-1 px-2 text-sm [&:hover>[data-icon]]:block [&:hover>img]:hidden">
|
|
<img
|
|
src={
|
|
type === "note" ? "/note.svg" : image ?? "/icons/logo_without_bg.png"
|
|
}
|
|
className="h-5 w-5"
|
|
/>
|
|
<button
|
|
onClick={onRemove}
|
|
data-icon
|
|
className="m-0 hidden h-5 w-5 p-0 focus-visible:outline-none"
|
|
>
|
|
<X className="h-5 w-5 scale-90" />
|
|
</button>
|
|
<span>{title}</span>
|
|
<span className="ml-auto block opacity-50">
|
|
{type === "note" ? "Note" : cleanUrl(url)}
|
|
</span>
|
|
</div>
|
|
);
|
|
}
|