aboutsummaryrefslogtreecommitdiff
path: root/apps
diff options
context:
space:
mode:
authorDhravya <[email protected]>2024-06-29 00:11:28 -0500
committerDhravya <[email protected]>2024-06-29 00:11:28 -0500
commitdfcc1ea7f7db341843d9d60c5c70f682f08dc4a8 (patch)
tree2157ea4afea117dcfd19228e4f202d9cd1172e31 /apps
parentallow pointer events (diff)
downloadsupermemory-dfcc1ea7f7db341843d9d60c5c70f682f08dc4a8.tar.xz
supermemory-dfcc1ea7f7db341843d9d60c5c70f682f08dc4a8.zip
new filter
Diffstat (limited to 'apps')
-rw-r--r--apps/web/app/(dash)/home/page.tsx10
-rw-r--r--apps/web/app/(dash)/home/queryinput.tsx53
-rw-r--r--apps/web/app/(dash)/menu.tsx315
-rw-r--r--apps/web/app/actions/doers.ts12
4 files changed, 322 insertions, 68 deletions
diff --git a/apps/web/app/(dash)/home/page.tsx b/apps/web/app/(dash)/home/page.tsx
index a78301fb..17d52529 100644
--- a/apps/web/app/(dash)/home/page.tsx
+++ b/apps/web/app/(dash)/home/page.tsx
@@ -57,8 +57,18 @@ function Page({
<div className="w-full pb-20">
<QueryInput
handleSubmit={async (q, spaces) => {
+ if (q.length === 0) {
+ toast.error("Query is required");
+ return;
+ }
+
const threadid = await createChatThread(q);
+ if (!threadid.success || !threadid.data) {
+ toast.error("Failed to create chat thread");
+ return;
+ }
+
push(
`/chat/${threadid.data}?spaces=${JSON.stringify(spaces)}&q=${q}`,
);
diff --git a/apps/web/app/(dash)/home/queryinput.tsx b/apps/web/app/(dash)/home/queryinput.tsx
index 99476e40..868f93f9 100644
--- a/apps/web/app/(dash)/home/queryinput.tsx
+++ b/apps/web/app/(dash)/home/queryinput.tsx
@@ -4,9 +4,10 @@ import { ArrowRightIcon } from "@repo/ui/icons";
import Image from "next/image";
import React, { useEffect, useMemo, useState } from "react";
import Divider from "@repo/ui/shadcn/divider";
-import { MultipleSelector, Option } from "@repo/ui/shadcn/combobox";
import { useRouter } from "next/navigation";
import { getSpaces } from "@/app/actions/fetchers";
+import Combobox from "@repo/ui/shadcn/combobox";
+import { MinusIcon } from "lucide-react";
function QueryInput({
initialQuery = "",
@@ -53,9 +54,9 @@ function QueryInput({
);
return (
- <div className={className}>
+ <div className={`${className}`}>
<div
- className={`bg-secondary ${!mini ? "rounded-t-3xl" : "rounded-3xl"}`}
+ className={`bg-secondary border-2 border-b-0 border-border ${!mini ? "rounded-t-3xl" : "rounded-3xl"}`}
>
{/* input and action button */}
<form
@@ -69,7 +70,7 @@ function QueryInput({
name="q"
cols={30}
rows={mini ? 2 : 4}
- className="bg-transparent pt-2.5 text-base placeholder:text-[#5D6165] text-[#9DA0A4] focus:text-gray-200 duration-200 tracking-[3%] outline-none resize-none w-full p-4"
+ className="bg-transparent pt-2.5 text-base placeholder:text-[#9B9B9B] focus:text-gray-200 duration-200 tracking-[3%] outline-none resize-none w-full p-4"
placeholder="Ask your second brain..."
onKeyDown={(e) => {
if (e.key === "Enter") {
@@ -90,7 +91,7 @@ function QueryInput({
handleSubmit(q, preparedSpaces);
}}
disabled={disabled}
- className="h-12 w-12 rounded-[14px] bg-[#21303D] all-center shrink-0 hover:brightness-125 duration-200 outline-none focus:outline focus:outline-primary active:scale-90"
+ className="h-12 w-12 rounded-[14px] bg-border all-center shrink-0 hover:brightness-125 duration-200 outline-none focus:outline focus:outline-primary active:scale-90"
>
<Image src={ArrowRightIcon} alt="Right arrow icon" />
</button>
@@ -100,21 +101,37 @@ function QueryInput({
{!mini && (
<>
<Divider />
- <div className="flex items-center gap-6 p-2 h-auto bg-secondary rounded-b-3xl">
- <MultipleSelector
- key={options.length}
- disabled={disabled}
- defaultOptions={options}
- onChange={(e) =>
- setSelectedSpaces(e.map((x) => parseInt(x.value)))
- }
- placeholder="Focus on specific spaces..."
- emptyIndicator={
- <p className="text-center text-lg leading-10 text-gray-600 dark:text-gray-400">
- no results found.
- </p>
+ <div className="flex justify-between items-center gap-6 h-auto bg-secondary rounded-b-3xl border-2 border-border">
+ <Combobox
+ options={options}
+ onSelect={(v) =>
+ setSelectedSpaces((prev) => {
+ if (v === "") {
+ return [];
+ }
+ return [...prev, parseInt(v)];
+ })
}
+ onSubmit={() => {}}
+ placeholder="Filter spaces..."
/>
+
+ <div className="flex flex-row gap-0.5 h-full">
+ {preparedSpaces.map((x, idx) => (
+ <button
+ key={x.id}
+ onClick={() =>
+ setSelectedSpaces((prev) => prev.filter((y) => y !== x.id))
+ }
+ className={`relative group p-2 py-3 bg-[#3C464D] max-w-32 ${idx === preparedSpaces.length - 1 ? "rounded-br-xl" : ""}`}
+ >
+ <p className="line-clamp-1">{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>
</>
)}
diff --git a/apps/web/app/(dash)/menu.tsx b/apps/web/app/(dash)/menu.tsx
index b7ea6c1c..29d25574 100644
--- a/apps/web/app/(dash)/menu.tsx
+++ b/apps/web/app/(dash)/menu.tsx
@@ -1,11 +1,60 @@
"use client";
-import React from "react";
+import React, { useEffect, useMemo, useState } from "react";
import Image from "next/image";
import Link from "next/link";
-import { MemoriesIcon, ExploreIcon, CanvasIcon } from "@repo/ui/icons";
+import { MemoriesIcon, ExploreIcon, CanvasIcon, AddIcon } from "@repo/ui/icons";
+import { Button } from "@repo/ui/shadcn/button";
+import { PlusCircleIcon } from "lucide-react";
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} 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 { createMemory } from "../actions/doers";
+import { Input } from "@repo/ui/shadcn/input";
function Menu() {
+ const [spaces, setSpaces] = useState<Space[]>([]);
+
+ useEffect(() => {
+ (async () => {
+ let spaces = await getSpaces();
+
+ if (!spaces.success || !spaces.data) {
+ toast.warning("Unable to get spaces", {
+ richColors: true,
+ });
+ setSpaces([]);
+ return;
+ }
+ setSpaces(spaces.data);
+ })();
+ }, []);
+
const menuItems = [
{
icon: MemoriesIcon,
@@ -27,63 +76,231 @@ function Menu() {
},
];
+ const [content, setContent] = useState("");
+ const [selectedSpace, setSelectedSpace] = useState<string | null>(null);
+
+ const autoDetectedType = useMemo(() => {
+ if (content.length === 0) {
+ return "none";
+ }
+
+ if (content.match(/https?:\/\/[\w\.]+\/[\w]+\/[\w]+\/[\d]+/)) {
+ return "tweet";
+ } else if (content.match(/https?:\/\/[\w\.]+/)) {
+ return "page";
+ } else if (content.match(/https?:\/\/www\.[\w\.]+/)) {
+ return "page";
+ } else {
+ return "note";
+ }
+ }, [content]);
+
+ const [dialogOpen, setDialogOpen] = useState(false);
+
+ const handleSubmit = async (content?: string, space?: string) => {
+ setDialogOpen(false);
+
+ toast.info("Creating memory...");
+
+ if (!content || content.length === 0) {
+ toast.error("Content is required");
+ return;
+ }
+
+ const cont = await createMemory({
+ content: content,
+ spaces: space ? [space] : undefined,
+ });
+
+ setContent("");
+
+ if (cont.success) {
+ toast.success("Memory created");
+ } else {
+ toast.error(`Memory creation failed: ${cont.error}`);
+ }
+ };
+
return (
<>
{/* Desktop Menu */}
- <div className="hidden lg:flex fixed h-screen pb-20 w-full p-4 items-center justify-start top-0 left-0 pointer-events-none">
- <div className="pointer-events-auto group flex w-14 text-foreground-menu text-[15px] font-medium flex-col items-start gap-6 overflow-hidden rounded-[28px] bg-secondary px-3 py-4 duration-200 hover:w-40">
- {menuItems.map((item) => (
- <Link
- aria-disabled={item.disabled}
- href={item.disabled ? "#" : item.url}
- key={item.url}
- className={`flex w-full ${
- item.disabled
- ? "cursor-not-allowed opacity-50"
- : "text-[#777E87] brightness-75 hover:brightness-125 cursor-pointer"
- } items-center gap-3 px-1 duration-200 hover:scale-105 active:scale-90 justify-start`}
- >
- <Image
- src={item.icon}
- alt={`${item.text} icon`}
- width={24}
- height={24}
- className="hover:brightness-125 duration-200"
- />
- <p className="opacity-0 duration-200 group-hover:opacity-100">
- {item.text}
- </p>
- </Link>
- ))}
+ <Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
+ <div className="hidden lg:flex fixed h-screen pb-20 w-full p-4 items-center justify-start top-0 left-0 pointer-events-none">
+ <div className="pointer-events-auto group flex w-14 text-foreground-menu text-[15px] font-medium flex-col items-start gap-6 overflow-hidden rounded-[28px] border-2 border-border bg-secondary px-3 py-4 duration-200 hover:w-40">
+ <div className="border-b border-border pb-4 w-full">
+ <DialogTrigger
+ className={`flex w-full text-white brightness-75 hover:brightness-125 focus:brightness-125 cursor-pointer items-center gap-3 px-1 duration-200 justify-start`}
+ >
+ <Image
+ src={AddIcon}
+ alt="Logo"
+ width={24}
+ height={24}
+ className="hover:brightness-125 focus:brightness-125 duration-200 text-white"
+ />
+ <p className="opacity-0 duration-200 group-hover:opacity-100 group-focus-within:opacity-100">
+ Add
+ </p>
+ </DialogTrigger>
+ </div>
+ {menuItems.map((item) => (
+ <Link
+ aria-disabled={item.disabled}
+ href={item.disabled ? "#" : item.url}
+ key={item.url}
+ className={`flex w-full ${
+ item.disabled
+ ? "cursor-not-allowed opacity-30"
+ : "text-white brightness-75 hover:brightness-125 cursor-pointer"
+ } items-center gap-3 px-1 duration-200 hover:scale-105 active:scale-90 justify-start`}
+ >
+ <Image
+ src={item.icon}
+ alt={`${item.text} icon`}
+ width={24}
+ height={24}
+ className="hover:brightness-125 duration-200"
+ />
+ <p className="opacity-0 duration-200 group-hover:opacity-100 group-focus-within:opacity-100">
+ {item.text}
+ </p>
+ </Link>
+ ))}
+ </div>
</div>
- </div>
-
- {/* Mobile Menu */}
- <div className="lg:hidden fixed bottom-0 left-0 w-full p-4 bg-secondary">
- <div className="flex justify-around items-center">
- {menuItems.map((item) => (
- <Link
- aria-disabled={item.disabled}
- href={item.disabled ? "#" : item.url}
- key={item.url}
- className={`flex flex-col items-center ${
- item.disabled
- ? "opacity-50 cursor-not-allowed"
- : "cursor-pointer"
- }`}
- onClick={(e) => item.disabled && e.preventDefault()}
+
+ <DialogContent className="sm:max-w-[425px]">
+ <form
+ action={async (e: FormData) => {
+ const content = e.get("content")?.toString();
+ const space = e.get("space")?.toString();
+
+ await handleSubmit(content, space);
+ }}
+ className="flex flex-col gap-4"
+ >
+ <DialogHeader>
+ <DialogTitle>Add memory</DialogTitle>
+ <DialogDescription>
+ A "Memory" is a bookmark, something you want to remember.
+ </DialogDescription>
+ </DialogHeader>
+
+ <div>
+ <Label className="text-[#858B92]" htmlFor="name">
+ Resource (URL or content)
+ </Label>
+ <Textarea
+ className="bg-[#2B3237] focus-visible:ring-0 border-none focus-visible:ring-offset-0 mt-2"
+ id="content"
+ name="content"
+ 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.preventDefault();
+ handleSubmit(content);
+ }
+ }}
+ />
+ </div>
+
+ {autoDetectedType != "none" && (
+ <div>
+ <Label
+ className="text-[#858B92] flex items-center gap-1 duration-200 transform transition-transform"
+ htmlFor="space"
+ >
+ Space
+ <TooltipProvider>
+ <Tooltip>
+ <TooltipTrigger>
+ <InformationCircleIcon className="w-4 h-4" />
+ </TooltipTrigger>
+ <TooltipContent>
+ <p>
+ A space is a collection of memories. You can create a
+ space and then chat/write/ideate with it.
+ </p>
+ </TooltipContent>
+ </Tooltip>
+ </TooltipProvider>
+ </Label>
+ <Select
+ onValueChange={(value) => setSelectedSpace(value)}
+ value={selectedSpace ?? "none"}
+ defaultValue="none"
+ name="space"
+ >
+ <SelectTrigger className="mt-2">
+ <SelectValue placeholder="None" />
+ </SelectTrigger>
+ <SelectContent className="bg-secondary text-white">
+ <SelectItem defaultChecked value="none">
+ None
+ </SelectItem>
+ {spaces.map((space) => (
+ <SelectItem key={space.id} value={space.id.toString()}>
+ {space.name}
+ </SelectItem>
+ ))}
+ </SelectContent>
+ </Select>
+ </div>
+ )}
+
+ <DialogFooter>
+ <Button
+ disabled={autoDetectedType === "none"}
+ variant={"secondary"}
+ type="submit"
+ >
+ Save {autoDetectedType != "none" && autoDetectedType}
+ </Button>
+ </DialogFooter>
+ </form>
+ </DialogContent>
+
+ {/* Mobile Menu */}
+ <div className="lg:hidden fixed bottom-0 left-0 w-full p-4 bg-secondary">
+ <div className="flex justify-around items-center">
+ <DialogTrigger
+ className={`flex flex-col items-center cursor-pointer text-white`}
>
<Image
- src={item.icon}
- alt={`${item.text} icon`}
+ src={AddIcon}
+ alt="Logo"
width={24}
height={24}
+ className="hover:brightness-125 focus:brightness-125 duration-200 stroke-white"
/>
- <p className="text-xs text-foreground-menu mt-2">{item.text}</p>
- </Link>
- ))}
+ <p className="text-xs text-foreground-menu mt-2">Add</p>
+ </DialogTrigger>
+ {menuItems.map((item) => (
+ <Link
+ aria-disabled={item.disabled}
+ href={item.disabled ? "#" : item.url}
+ key={item.url}
+ className={`flex flex-col items-center ${
+ item.disabled
+ ? "opacity-50 cursor-not-allowed"
+ : "cursor-pointer"
+ }`}
+ onClick={(e) => item.disabled && e.preventDefault()}
+ >
+ <Image
+ src={item.icon}
+ alt={`${item.text} icon`}
+ width={24}
+ height={24}
+ />
+ <p className="text-xs text-foreground-menu mt-2">{item.text}</p>
+ </Link>
+ ))}
+ </div>
</div>
- </div>
+ </Dialog>
</>
);
}
diff --git a/apps/web/app/actions/doers.ts b/apps/web/app/actions/doers.ts
index 99a1b719..a1de7b54 100644
--- a/apps/web/app/actions/doers.ts
+++ b/apps/web/app/actions/doers.ts
@@ -65,6 +65,8 @@ const typeDecider = (content: string) => {
return "tweet";
} else if (content.match(/https?:\/\/[\w\.]+/)) {
return "page";
+ } else if (content.match(/https?:\/\/www\.[\w\.]+/)) {
+ return "page";
} else {
return "note";
}
@@ -138,7 +140,15 @@ export const createMemory = async (input: {
},
});
pageContent = await response.text();
- metadata = await getMetaData(input.content);
+
+ try {
+ metadata = await getMetaData(input.content);
+ } catch (e) {
+ return {
+ success: false,
+ error: "Failed to fetch metadata for the page. Please try again later.",
+ };
+ }
} else if (type === "tweet") {
const tweet = await getTweetData(input.content.split("/").pop() as string);
pageContent = JSON.stringify(tweet);