mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-08-28 05:25:33 +00:00
small UX fixes and multi space save actually works now
This commit is contained in:
parent
dff1bdd295
commit
1fd0aae0be
8 changed files with 173 additions and 203 deletions
|
|
@ -7,9 +7,4 @@ module.exports = {
|
|||
parserOptions: {
|
||||
project: true,
|
||||
},
|
||||
// ignore some rules
|
||||
rules: {
|
||||
"@": "off",
|
||||
"import/no-unresolved": "off",
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -284,7 +284,10 @@ function LinkComponent({
|
|||
<div className="flex items-center gap-2 text-xs">
|
||||
<PaperclipIcon className="w-3 h-3" /> Page
|
||||
</div>
|
||||
<div className="text-lg text-[#fff] mt-4 line-clamp-2">{title}</div>
|
||||
{/* remove `<---chunkId: ${vector.id}\n${content}\n---->` pattern from title */}
|
||||
<div className="text-lg text-[#fff] mt-4 line-clamp-2">
|
||||
{title.replace(/(<---chunkId: .*?\n.*?\n---->)/g, "")}
|
||||
</div>
|
||||
<div>
|
||||
{url.replace("https://supermemory.ai", "").split("#")[0] ?? "/"}
|
||||
</div>
|
||||
|
|
@ -294,7 +297,9 @@ function LinkComponent({
|
|||
<div className="flex items-center gap-2 text-xs">
|
||||
<NotebookIcon className="w-3 h-3" /> Note
|
||||
</div>
|
||||
<div className="text-lg text-[#fff] mt-4 line-clamp-2">{title}</div>
|
||||
<div className="text-lg text-[#fff] mt-4 line-clamp-2">
|
||||
{title.replace(/(<---chunkId: .*?\n.*?\n---->)/g, "")}
|
||||
</div>
|
||||
<div className="line-clamp-3 mt-2">
|
||||
{content.replace(title, "")}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -17,34 +17,15 @@ import {
|
|||
} from "@repo/ui/shadcn/dialog";
|
||||
import { Label } from "@repo/ui/shadcn/label";
|
||||
import { Textarea } from "@repo/ui/shadcn/textarea";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@repo/ui/shadcn/select";
|
||||
import { toast } from "sonner";
|
||||
import { getSpaces } from "../actions/fetchers";
|
||||
import { Space } from "../actions/types";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@repo/ui/shadcn/tooltip";
|
||||
import { InformationCircleIcon } from "@heroicons/react/24/outline";
|
||||
import { HomeIcon } from "@heroicons/react/24/solid";
|
||||
import { createMemory, createSpace } from "../actions/doers";
|
||||
import { Input } from "@repo/ui/shadcn/input";
|
||||
import ComboboxWithCreate from "@repo/ui/shadcn/combobox";
|
||||
import { StoredSpace } from "@/server/db/schema";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import useMeasure from "react-use-measure";
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
function Menu() {
|
||||
const [ref, bounds] = useMeasure();
|
||||
const [spaces, setSpaces] = useState<StoredSpace[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -109,7 +90,7 @@ function Menu() {
|
|||
[spaces],
|
||||
);
|
||||
|
||||
const handleSubmit = async (content?: string, space?: string) => {
|
||||
const handleSubmit = async (content?: string, spaces?: number[]) => {
|
||||
setDialogOpen(false);
|
||||
|
||||
toast.info("Creating memory...", {
|
||||
|
|
@ -122,12 +103,15 @@ function Menu() {
|
|||
return;
|
||||
}
|
||||
|
||||
console.log(spaces);
|
||||
|
||||
const cont = await createMemory({
|
||||
content: content,
|
||||
spaces: space ? [space] : undefined,
|
||||
spaces: spaces ?? undefined,
|
||||
});
|
||||
|
||||
setContent("");
|
||||
setSelectedSpaces([]);
|
||||
|
||||
if (cont.success) {
|
||||
toast.success("Memory created", {
|
||||
|
|
@ -187,148 +171,138 @@ function Menu() {
|
|||
</div>
|
||||
|
||||
<DialogContent className="sm:max-w-[475px] text-[#F2F3F5] rounded-2xl bg-background z-[39] backdrop-blur-md">
|
||||
<motion.div
|
||||
className="overflow-hidden max-h-[70vh]"
|
||||
animate={{ height: bounds.height + 10 }}
|
||||
>
|
||||
<form
|
||||
ref={ref}
|
||||
action={async (e: FormData) => {
|
||||
const content = e.get("content")?.toString();
|
||||
const space = e.get("space")?.toString();
|
||||
<form
|
||||
action={async (e: FormData) => {
|
||||
const content = e.get("content")?.toString();
|
||||
|
||||
await handleSubmit(content, space);
|
||||
}}
|
||||
className="flex flex-col gap-4 "
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Add memory</DialogTitle>
|
||||
<DialogDescription className="text-[#F2F3F5]">
|
||||
A "Memory" is a bookmark, something you want to remember.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
await handleSubmit(content, selectedSpaces);
|
||||
}}
|
||||
className="flex flex-col gap-4 "
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Add memory</DialogTitle>
|
||||
<DialogDescription className="text-[#F2F3F5]">
|
||||
A "Memory" is a bookmark, something you want to remember.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="name">Resource (URL or content)</Label>
|
||||
<Textarea
|
||||
className={`bg-[#2F353C] text-[#DBDEE1] max-h-[35vh] overflow-auto focus-visible:ring-0 border-none focus-visible:ring-offset-0 mt-2 ${/^https?:\/\/\S+$/i.test(content) && "text-[#1D9BF0] underline underline-offset-2"}`}
|
||||
id="content"
|
||||
name="content"
|
||||
rows={8}
|
||||
placeholder="Start typing a note or paste a URL here. I'll remember it."
|
||||
value={content}
|
||||
onChange={(e) => setContent(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleSubmit(content, selectedSpaces);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label className="space-y-1" htmlFor="space">
|
||||
<h3 className="font-semibold text-lg tracking-tight">
|
||||
Spaces (Optional)
|
||||
</h3>
|
||||
<p className="leading-normal text-[#F2F3F5] text-sm">
|
||||
A space is a collection of memories. It's a way to organise
|
||||
your memories.
|
||||
</p>
|
||||
</Label>
|
||||
|
||||
<ComboboxWithCreate
|
||||
options={spaces.map((x) => ({
|
||||
label: x.name,
|
||||
value: x.id.toString(),
|
||||
}))}
|
||||
onSelect={(v) =>
|
||||
setSelectedSpaces((prev) => {
|
||||
if (v === "") {
|
||||
return [];
|
||||
}
|
||||
return [...prev, parseInt(v)];
|
||||
})
|
||||
}
|
||||
onSubmit={async (spaceName) => {
|
||||
const space = options.find((x) => x.label === spaceName);
|
||||
toast.info("Creating space...");
|
||||
|
||||
if (space) {
|
||||
toast.error("A space with that name already exists.");
|
||||
}
|
||||
|
||||
const creationTask = await createSpace(spaceName);
|
||||
if (creationTask.success && creationTask.data) {
|
||||
toast.success("Space created " + creationTask.data);
|
||||
setSpaces?.((prev) => [
|
||||
...prev,
|
||||
{
|
||||
name: spaceName,
|
||||
id: creationTask.data!,
|
||||
createdAt: new Date(),
|
||||
user: null,
|
||||
numItems: 0,
|
||||
},
|
||||
]);
|
||||
setSelectedSpaces((prev) => [...prev, creationTask.data!]);
|
||||
} else {
|
||||
toast.error(
|
||||
"Space creation failed: " + creationTask.error ??
|
||||
"Unknown error",
|
||||
);
|
||||
}
|
||||
}}
|
||||
placeholder="Select or create a new space."
|
||||
className="bg-[#2F353C] h-min rounded-md mt-4 mb-4"
|
||||
/>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="name">Resource (URL or content)</Label>
|
||||
<Textarea
|
||||
className={`bg-[#2F353C] text-[#DBDEE1] max-h-[35vh] overflow-auto focus-visible:ring-0 border-none focus-visible:ring-offset-0 mt-2 ${/^https?:\/\/\S+$/i.test(content) && "text-[#1D9BF0] underline underline-offset-2"}`}
|
||||
id="content"
|
||||
name="content"
|
||||
rows={8}
|
||||
placeholder="Start typing a note or paste a URL here. I'll remember it."
|
||||
value={content}
|
||||
onChange={(e) => setContent(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleSubmit(content);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{autoDetectedType != "none" && (
|
||||
<div>
|
||||
<Label className="space-y-2" htmlFor="space">
|
||||
<h3 className="font-bold text-lg">Spaces (Optional)</h3>
|
||||
<p className="leading-normal">
|
||||
A space is a collection of memories. It's a way to
|
||||
organise your memories.
|
||||
</p>
|
||||
</Label>
|
||||
|
||||
<ComboboxWithCreate
|
||||
options={spaces.map((x) => ({
|
||||
label: x.name,
|
||||
value: x.id.toString(),
|
||||
}))}
|
||||
onSelect={(v) =>
|
||||
setSelectedSpaces((prev) => {
|
||||
if (v === "") {
|
||||
return [];
|
||||
{selectedSpaces.length > 0 && (
|
||||
<div className="flex flex-row flex-wrap gap-0.5 h-min">
|
||||
{selectedSpaces.map((x, idx) => (
|
||||
<button
|
||||
key={x}
|
||||
type="button"
|
||||
onClick={() =>
|
||||
setSelectedSpaces((prev) =>
|
||||
prev.filter((y) => y !== x),
|
||||
)
|
||||
}
|
||||
return [...prev, parseInt(v)];
|
||||
})
|
||||
}
|
||||
onSubmit={async (spaceName) => {
|
||||
const space = options.find((x) => x.label === spaceName);
|
||||
toast.info("Creating space...");
|
||||
|
||||
if (space) {
|
||||
toast.error("A space with that name already exists.");
|
||||
}
|
||||
|
||||
const creationTask = await createSpace(spaceName);
|
||||
if (creationTask.success && creationTask.data) {
|
||||
toast.success("Space created " + creationTask.data);
|
||||
setSpaces?.((prev) => [
|
||||
...prev,
|
||||
{
|
||||
name: spaceName,
|
||||
id: creationTask.data!,
|
||||
createdAt: new Date(),
|
||||
user: null,
|
||||
numItems: 0,
|
||||
},
|
||||
]);
|
||||
setSelectedSpaces((prev) => [
|
||||
...prev,
|
||||
creationTask.data!,
|
||||
]);
|
||||
} else {
|
||||
toast.error(
|
||||
"Space creation failed: " + creationTask.error ??
|
||||
"Unknown error",
|
||||
);
|
||||
}
|
||||
}}
|
||||
placeholder="select or create a new space."
|
||||
className="bg-[#2F353C] h-min rounded-md mt-4 mb-4"
|
||||
/>
|
||||
|
||||
<div>
|
||||
{selectedSpaces.length > 0 && (
|
||||
<div className="flex flex-row flex-wrap gap-0.5 h-min">
|
||||
{selectedSpaces.map((x, idx) => (
|
||||
<button
|
||||
key={x}
|
||||
type="button"
|
||||
onClick={() =>
|
||||
setSelectedSpaces((prev) =>
|
||||
prev.filter((y) => y !== x),
|
||||
)
|
||||
}
|
||||
className={`relative group p-2 py-3 bg-[#3C464D] max-w-32 ${
|
||||
idx === selectedSpaces.length - 1
|
||||
? "rounded-br-xl"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
<p className="line-clamp-1">
|
||||
{spaces.find((y) => y.id === x)?.name}
|
||||
</p>
|
||||
<div className="absolute h-full right-0 top-0 p-1 opacity-0 group-hover:opacity-100 items-center">
|
||||
<MinusIcon className="w-6 h-6 rounded-full bg-secondary" />
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
className={`relative group p-2 py-3 bg-[#3C464D] max-w-32 ${
|
||||
idx === selectedSpaces.length - 1
|
||||
? "rounded-br-xl"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
<p className="line-clamp-1">
|
||||
{spaces.find((y) => y.id === x)?.name}
|
||||
</p>
|
||||
<div className="absolute h-full right-0 top-0 p-1 opacity-0 group-hover:opacity-100 items-center">
|
||||
<MinusIcon className="w-6 h-6 rounded-full bg-secondary" />
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
disabled={autoDetectedType === "none"}
|
||||
variant={"secondary"}
|
||||
type="submit"
|
||||
>
|
||||
Save {autoDetectedType != "none" && autoDetectedType}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</motion.div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
disabled={autoDetectedType === "none"}
|
||||
variant={"secondary"}
|
||||
type="submit"
|
||||
>
|
||||
Save {autoDetectedType != "none" && autoDetectedType}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
|
||||
{/* Mobile Menu */}
|
||||
|
|
|
|||
|
|
@ -158,7 +158,7 @@ const getTweetData = async (tweetID: string) => {
|
|||
|
||||
export const createMemory = async (input: {
|
||||
content: string;
|
||||
spaces?: string[];
|
||||
spaces?: number[];
|
||||
}): ServerActionReturnType<number> => {
|
||||
const data = await auth();
|
||||
|
||||
|
|
@ -239,7 +239,7 @@ export const createMemory = async (input: {
|
|||
title: metadata.title,
|
||||
description: metadata.description,
|
||||
url: metadata.baseUrl,
|
||||
spaces: storeToSpaces,
|
||||
spaces: storeToSpaces.map((spaceId) => spaceId.toString()),
|
||||
user: data.user.id,
|
||||
type,
|
||||
}),
|
||||
|
|
@ -302,7 +302,7 @@ export const createMemory = async (input: {
|
|||
const insertResponse = await db
|
||||
.insert(storedContent)
|
||||
.values({
|
||||
content: response.chunkedInput,
|
||||
content: pageContent,
|
||||
title: metadata.title,
|
||||
description: metadata.description,
|
||||
url: saveToDbUrl,
|
||||
|
|
@ -355,13 +355,7 @@ export const createMemory = async (input: {
|
|||
.select()
|
||||
.from(space)
|
||||
.where(
|
||||
and(
|
||||
inArray(
|
||||
space.id,
|
||||
storeToSpaces.map((s) => parseInt(s)),
|
||||
),
|
||||
eq(space.user, data.user.id),
|
||||
),
|
||||
and(inArray(space.id, storeToSpaces), eq(space.user, data.user.id)),
|
||||
)
|
||||
.all();
|
||||
|
||||
|
|
@ -371,7 +365,7 @@ export const createMemory = async (input: {
|
|||
.insert(contentToSpace)
|
||||
.values({ contentId: contentId, spaceId: s.id });
|
||||
|
||||
db.update(space).set({ numItems: s.numItems + 1 });
|
||||
await db.update(space).set({ numItems: s.numItems + 1 });
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import s from "./tweet-header.module.css";
|
|||
import { VerifiedBadge } from "./verified-badge";
|
||||
|
||||
type Props = {
|
||||
tweet: Tweet;
|
||||
tweet: Tweet | { error: string };
|
||||
components?: TwitterComponents;
|
||||
};
|
||||
|
||||
|
|
@ -101,6 +101,10 @@ const TweetHeader = ({
|
|||
};
|
||||
|
||||
export const MyTweet = ({ tweet: t, components }: Props) => {
|
||||
if ("error" in t) {
|
||||
return <div>{t.error}</div>;
|
||||
}
|
||||
|
||||
const tweet = enrichTweet(t);
|
||||
return (
|
||||
<TweetContainer className="bg-transparent !m-0 !p-0 !z-0">
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@
|
|||
"eslint-plugin-next-on-pages": "^1.11.3",
|
||||
"lint-staged": "^15.2.5",
|
||||
"postcss": "^8.4.38",
|
||||
"prettier": "^3.2.5",
|
||||
"prettier": "^3.3.3",
|
||||
"readline-sync": "^1.4.10",
|
||||
"tailwindcss": "^3.4.3",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
|
|
@ -79,6 +79,7 @@
|
|||
"cheerio": "^1.0.0-rc.12",
|
||||
"compromise": "^14.13.0",
|
||||
"drizzle-orm": "0.30.0",
|
||||
"eslint-config-turbo": "^2.0.6",
|
||||
"framer-motion": "^11.2.6",
|
||||
"geist": "^1.3.0",
|
||||
"grammy": "^1.25.1",
|
||||
|
|
|
|||
|
|
@ -12,15 +12,12 @@ export const tweetToMd = (tweet: Tweet) => {
|
|||
|
||||
export const getRawTweet = (tweet: string) => {
|
||||
// Get the content inside the last <raw> tag, there can any number of <raw> tags in the tweet (or just one)
|
||||
const rawTag = /<raw>(.*)<\/raw>/gs; // Use 's' flag to match across multiple lines
|
||||
const rawTag = /<raw>(.*)<\/raw>/g;
|
||||
const match = rawTag.exec(tweet);
|
||||
if (match && match.length > 1) {
|
||||
// Remove the specified item pattern
|
||||
const cleanedContent = match[1]?.replace(
|
||||
/<---chunkId:.*?\n.*?\n---->/gs,
|
||||
"",
|
||||
);
|
||||
return cleanedContent;
|
||||
if (match) {
|
||||
return match[1];
|
||||
}
|
||||
return tweet;
|
||||
return `{
|
||||
"error": "No <raw> tag found"
|
||||
}`;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -52,23 +52,6 @@ const ComboboxWithCreate: React.FC<ComboboxWithCreateProps> = ({
|
|||
value={inputValue}
|
||||
/>
|
||||
<CommandList className="z-10 translate-y-12 translate-x-5 opacity-0 absolute group-focus-within:opacity-100 bg-secondary p-2 rounded-b-xl max-w-64">
|
||||
<CommandEmpty>
|
||||
<Button
|
||||
className="px-1"
|
||||
type="button"
|
||||
onClick={async () => onSubmit(inputValue)}
|
||||
variant="link"
|
||||
disabled={inputValue.length === 0}
|
||||
>
|
||||
{inputValue.length > 0 ? (
|
||||
<>
|
||||
{createNewMessage} "{inputValue}"
|
||||
</>
|
||||
) : (
|
||||
<>Create a new space</>
|
||||
)}
|
||||
</Button>
|
||||
</CommandEmpty>
|
||||
<CommandGroup className="hidden group-focus-within:block">
|
||||
{options.map((option, idx) => (
|
||||
<CommandItem
|
||||
|
|
@ -78,6 +61,23 @@ const ComboboxWithCreate: React.FC<ComboboxWithCreateProps> = ({
|
|||
{option.label}
|
||||
</CommandItem>
|
||||
))}
|
||||
{!options.map((opts) => opts.label).includes(inputValue) && (
|
||||
<Button
|
||||
className="px-1"
|
||||
type="button"
|
||||
onClick={async () => onSubmit(inputValue)}
|
||||
variant="link"
|
||||
disabled={inputValue.length === 0}
|
||||
>
|
||||
{inputValue.length > 0 ? (
|
||||
<>
|
||||
{createNewMessage} "{inputValue}"
|
||||
</>
|
||||
) : (
|
||||
<>Type to create a new space</>
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue