aboutsummaryrefslogtreecommitdiff
path: root/apps
diff options
context:
space:
mode:
Diffstat (limited to 'apps')
-rw-r--r--apps/web/app/(auth)/signin/page.tsx2
-rw-r--r--apps/web/app/(dash)/chat/chatWindow.tsx218
-rw-r--r--apps/web/app/(dash)/chat/queryinput.tsx83
-rw-r--r--apps/web/app/(dash)/dialogContentContainer.tsx224
-rw-r--r--apps/web/app/(dash)/dialogTriggerWrapper.tsx50
-rw-r--r--apps/web/app/(dash)/home/filterSpaces.tsx108
-rw-r--r--apps/web/app/(dash)/home/heading.tsx38
-rw-r--r--apps/web/app/(dash)/home/history.tsx50
-rw-r--r--apps/web/app/(dash)/home/homeVariants.ts50
-rw-r--r--apps/web/app/(dash)/home/page.tsx97
-rw-r--r--apps/web/app/(dash)/home/queryinput.tsx163
-rw-r--r--apps/web/app/(dash)/layout.tsx8
-rw-r--r--apps/web/app/(dash)/menu.tsx483
-rw-r--r--apps/web/app/(onboarding)/layout.tsx12
-rw-r--r--apps/web/app/(onboarding)/onboarding/page.tsx388
-rw-r--r--apps/web/app/actions/fetchers.ts30
-rw-r--r--apps/web/app/layout.tsx6
-rw-r--r--apps/web/migrations/0001_nervous_longshot.sql2
-rw-r--r--apps/web/migrations/meta/0001_snapshot.json905
-rw-r--r--apps/web/migrations/meta/_journal.json31
-rw-r--r--apps/web/package.json103
-rw-r--r--apps/web/public/image.pngbin0 -> 164761 bytes
-rw-r--r--apps/web/public/image2.pngbin0 -> 8360 bytes
-rw-r--r--apps/web/public/image3.pngbin0 -> 10298 bytes
-rw-r--r--apps/web/public/img.pngbin0 -> 68722 bytes
-rw-r--r--apps/web/server/db/schema.ts3
26 files changed, 2290 insertions, 764 deletions
diff --git a/apps/web/app/(auth)/signin/page.tsx b/apps/web/app/(auth)/signin/page.tsx
index 3db512ae..ab965325 100644
--- a/apps/web/app/(auth)/signin/page.tsx
+++ b/apps/web/app/(auth)/signin/page.tsx
@@ -64,7 +64,7 @@ async function Signin({
action={async () => {
"use server";
await signIn("google", {
- redirectTo: "/home?firstTime=true",
+ redirectTo: "/onboarding",
});
}}
>
diff --git a/apps/web/app/(dash)/chat/chatWindow.tsx b/apps/web/app/(dash)/chat/chatWindow.tsx
index f0827a3d..066e7d20 100644
--- a/apps/web/app/(dash)/chat/chatWindow.tsx
+++ b/apps/web/app/(dash)/chat/chatWindow.tsx
@@ -2,7 +2,7 @@
import { AnimatePresence } from "framer-motion";
import React, { useEffect, useRef, useState } from "react";
-import QueryInput from "../home/queryinput";
+import QueryInput from "./queryinput";
import { cn } from "@repo/ui/lib/utils";
import { motion } from "framer-motion";
import { useRouter } from "next/navigation";
@@ -224,9 +224,73 @@ function ChatWindow({
{chat.question}
</h2>
- <div className="flex flex-col mt-2">
+ <div className="flex flex-col">
+ {/* Related memories */}
+ <div
+ className={`space-y-4 ${chat.answer.sources.length > 0 || chat.answer.parts.length === 0 ? "flex" : "hidden"}`}
+ >
+ <Accordion
+ defaultValue={
+ idx === chatHistory.length - 1 ? "memories" : ""
+ }
+ type="single"
+ collapsible
+ >
+ <AccordionItem value="memories">
+ <AccordionTrigger className="text-foreground-menu">
+ Related Memories
+ </AccordionTrigger>
+ {/* TODO: fade out content on the right side, the fade goes away when the user scrolls */}
+ <AccordionContent
+ className="flex items-center no-scrollbar overflow-auto gap-4 relative max-w-3xl no-scrollbar"
+ defaultChecked
+ >
+ {/* Loading state */}
+ {chat.answer.sources.length > 0 ||
+ (chat.answer.parts.length === 0 && (
+ <>
+ {[1, 2, 3, 4].map((_, idx) => (
+ <div
+ key={`loadingState-${idx}`}
+ className="w-[350px] shrink-0 p-4 gap-2 rounded-2xl flex flex-col bg-secondary animate-pulse"
+ >
+ <div className="bg-slate-700 h-2 rounded-full w-1/2"></div>
+ <div className="bg-slate-700 h-2 rounded-full w-full"></div>
+ </div>
+ ))}
+ </>
+ ))}
+ {chat.answer.sources.map((source, idx) => (
+ <Link
+ href={source.source}
+ key={idx}
+ className="w-[350px] shrink-0 p-4 gap-2 rounded-2xl flex flex-col bg-secondary"
+ >
+ <div className="flex justify-between text-foreground-menu text-sm">
+ <span>{source.type}</span>
+
+ {source.numChunks > 1 && (
+ <span>{source.numChunks} chunks</span>
+ )}
+ </div>
+ <div className="text-base">
+ {source.title}
+ </div>
+ <div className="text-xs line-clamp-2">
+ {source.content.length > 100
+ ? source.content.slice(0, 100) + "..."
+ : source.content}
+ </div>
+ </Link>
+ ))}
+ </AccordionContent>
+ </AccordionItem>
+ </Accordion>
+ </div>
+
+ {/* Summary */}
<div>
- <div className="text-foreground-menu py-2">Answer</div>
+ <div className="text-foreground-menu py-2">Summary</div>
<div className="text-base">
{/* Loading state */}
{(chat.answer.parts.length === 0 ||
@@ -283,108 +347,60 @@ function ChatWindow({
>
<ClipboardIcon className="size-[18px] group-hover:text-primary" />
</button>
+ <button
+ onClick={async () => {
+ const isWebShareSupported =
+ navigator.share !== undefined;
+ if (isWebShareSupported) {
+ try {
+ await navigator.share({
+ title: "Your Share Title",
+ text: "Your share text or description",
+ url: "https://your-url-to-share.com",
+ });
+ } catch (e) {
+ console.error("Error sharing:", e);
+ }
+ } else {
+ console.error("web share is not supported!");
+ }
+ }}
+ className="group h-8 w-8 flex justify-center items-center active:scale-75 duration-200"
+ >
+ <SendIcon className="size-[18px] group-hover:text-primary" />
+ </button>
</div>
</div>
</div>
-
- <div
- className={`space-y-4 ${chat.answer.sources.length > 0 || chat.answer.parts.length === 0 ? "flex" : "hidden"}`}
- >
- <Accordion
- defaultValue={
- idx === chatHistory.length - 1 ? "memories" : ""
- }
- type="single"
- collapsible
- >
- <AccordionItem value="memories">
- <AccordionTrigger className="text-foreground-menu">
- Related Memories
- </AccordionTrigger>
- {/* TODO: fade out content on the right side, the fade goes away when the user scrolls */}
- <AccordionContent
- className="flex flex-col no-scrollbar overflow-auto gap-4 relative max-w-3xl no-scrollbar"
- defaultChecked
+ {/* Justification */}
+ {chat.answer.justification &&
+ chat.answer.justification.length && (
+ <div
+ className={`${chat.answer.justification && chat.answer.justification.length > 0 ? "flex" : "hidden"}`}
+ >
+ <Accordion
+ defaultValue={""}
+ type="single"
+ collapsible
>
- <div className="w-full no-scrollbar flex gap-4">
- {/* Loading state */}
- {chat.answer.sources.length > 0 ||
- (chat.answer.parts.length === 0 && (
- <>
- {[1, 2, 3, 4].map((_, idx) => (
- <div
- key={`loadingState-${idx}`}
- className="w-[350px] shrink-0 p-4 gap-2 rounded-2xl flex flex-col bg-secondary animate-pulse"
- >
- <div className="bg-slate-700 h-2 rounded-full w-1/2"></div>
- <div className="bg-slate-700 h-2 rounded-full w-full"></div>
- </div>
- ))}
- </>
- ))}
- {chat.answer.sources.map((source, idx) => (
- <Link
- href={source.source}
- key={idx}
- className="w-[350px] shrink-0 p-4 gap-2 rounded-2xl flex flex-col bg-secondary"
- >
- <div className="flex justify-between text-foreground-menu text-sm">
- <span>{source.type}</span>
-
- {source.numChunks > 1 && (
- <span>{source.numChunks} chunks</span>
- )}
- </div>
- <div className="text-base">
- {source.title}
- </div>
- <div className="text-xs line-clamp-2">
- {source.content.length > 100
- ? source.content.slice(0, 100) + "..."
- : source.content}
- </div>
- </Link>
- ))}
- </div>
-
- {chat.answer.justification &&
- chat.answer.justification.length && (
- <div
- className={`${chat.answer.justification && chat.answer.justification.length > 0 ? "flex" : "hidden"}`}
- >
- <Accordion
- defaultValue={""}
- type="single"
- collapsible
- >
- <AccordionItem value="justification">
- <AccordionTrigger className="text-foreground-menu">
- Justification
- </AccordionTrigger>
- <AccordionContent
- className="relative flex gap-2 max-w-3xl overflow-auto no-scrollbar"
- defaultChecked
- >
- {chat.answer.justification.length > 0
- ? chat.answer.justification
- .replaceAll(
- "<justification>",
- "",
- )
- .replaceAll(
- "</justification>",
- "",
- )
- : "No justification provided."}
- </AccordionContent>
- </AccordionItem>
- </Accordion>
- </div>
- )}
- </AccordionContent>
- </AccordionItem>
- </Accordion>
- </div>
+ <AccordionItem value="justification">
+ <AccordionTrigger className="text-foreground-menu">
+ Justification
+ </AccordionTrigger>
+ <AccordionContent
+ className="relative flex gap-2 max-w-3xl overflow-auto no-scrollbar"
+ defaultChecked
+ >
+ {chat.answer.justification.length > 0
+ ? chat.answer.justification
+ .replaceAll("<justification>", "")
+ .replaceAll("</justification>", "")
+ : "No justification provided."}
+ </AccordionContent>
+ </AccordionItem>
+ </Accordion>
+ </div>
+ )}
</div>
</div>
</div>
diff --git a/apps/web/app/(dash)/chat/queryinput.tsx b/apps/web/app/(dash)/chat/queryinput.tsx
new file mode 100644
index 00000000..99f55986
--- /dev/null
+++ b/apps/web/app/(dash)/chat/queryinput.tsx
@@ -0,0 +1,83 @@
+"use client";
+
+import { ArrowRightIcon } from "@repo/ui/icons";
+import Image from "next/image";
+import React, { useState } from "react";
+
+function QueryInput({
+ initialSpaces,
+ initialQuery = "",
+ disabled = false,
+ className,
+ mini = false,
+ handleSubmit,
+}: {
+ initialQuery?: string;
+ initialSpaces?: {
+ id: number;
+ name: string;
+ }[];
+ disabled?: boolean;
+ className?: string;
+ mini?: boolean;
+ handleSubmit: (q: string, spaces: { id: number; name: string }[]) => void;
+}) {
+ const [q, setQ] = useState(initialQuery);
+
+ const [selectedSpaces, setSelectedSpaces] = useState<
+ { id: number; name: string }[]
+ >([]);
+
+ return (
+ <div className={`${className}`}>
+ <div
+ className={`bg-[#1F2428] overflow-hidden border-2 border-gray-700/50 shadow-md shadow-[#1d1d1dc7] rounded-3xl`}
+ >
+ {/* input and action button */}
+ <form
+ action={async () => {
+ if (q.trim().length === 0) {
+ return;
+ }
+ handleSubmit(q, selectedSpaces);
+ setQ("");
+ }}
+ className="flex gap-4 p-3"
+ >
+ <textarea
+ autoFocus
+ name="q"
+ cols={30}
+ rows={mini ? 2 : 4}
+ className="bg-transparent pt-2.5 text-lg 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" && !e.shiftKey) {
+ e.preventDefault();
+ if (q.trim().length === 0) {
+ return;
+ }
+ handleSubmit(q, selectedSpaces);
+ setQ("");
+ }
+ }}
+ onChange={(e) => setQ(e.target.value)}
+ value={q}
+ disabled={disabled}
+ />
+
+ <button
+ type="submit"
+ disabled={disabled}
+ 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>
+ </form>{" "}
+ </div>
+ {/* selected sources */}
+ </div>
+ );
+}
+
+export default QueryInput;
diff --git a/apps/web/app/(dash)/dialogContentContainer.tsx b/apps/web/app/(dash)/dialogContentContainer.tsx
new file mode 100644
index 00000000..7ad68f17
--- /dev/null
+++ b/apps/web/app/(dash)/dialogContentContainer.tsx
@@ -0,0 +1,224 @@
+import { StoredSpace } from "@/server/db/schema";
+import { useEffect, useMemo, useState } from "react";
+import { createMemory, createSpace } from "../actions/doers";
+import ComboboxWithCreate from "@repo/ui/shadcn/combobox";
+import { toast } from "sonner";
+import { getSpaces } from "../actions/fetchers";
+import { MinusIcon, PlusCircleIcon } from "lucide-react";
+import { DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@repo/ui/shadcn/dialog";
+import { Label } from "@repo/ui/shadcn/label";
+import { Textarea } from "@repo/ui/shadcn/textarea";
+import { Button } from "@repo/ui/shadcn/button";
+
+export function DialogContentContainer({DialogClose}: {DialogClose: ()=> void}) {
+ const [spaces, setSpaces] = useState<StoredSpace[]>([]);
+const [content, setContent] = useState("");
+const [selectedSpaces, setSelectedSpaces] = useState<number[]>([]);
+
+const options = useMemo(
+ () =>
+ spaces.map((x) => ({
+ label: x.name,
+ value: x.id.toString(),
+ })),
+ [spaces],
+);
+
+const autoDetectedType = useMemo(() => {
+ if (content.length === 0) {
+ return "none";
+ }
+
+ if (
+ content.match(/https?:\/\/(x\.com|twitter\.com)\/[\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 handleSubmit = async (content?: string, spaces?: number[]) => {
+ DialogClose();
+
+ toast.info("Creating memory...", {
+ icon: <PlusCircleIcon className="w-4 h-4 text-white animate-spin" />,
+ duration: 7500,
+ });
+
+ if (!content || content.length === 0) {
+ toast.error("Content is required");
+ return;
+ }
+
+ console.log(spaces);
+
+ const cont = await createMemory({
+ content: content,
+ spaces: spaces ?? undefined,
+ });
+
+ setContent("");
+ setSelectedSpaces([]);
+
+ if (cont.success) {
+ toast.success("Memory created", {
+ richColors: true,
+ });
+ } else {
+ toast.error(`Memory creation failed: ${cont.error}`);
+ }
+};
+
+useEffect(() => {
+ (async () => {
+ let spaces = await getSpaces();
+
+ if (!spaces.success || !spaces.data) {
+ toast.warning("Unable to get spaces", {
+ richColors: true,
+ });
+ setSpaces([]);
+ return;
+ }
+ setSpaces(spaces.data);
+ })();
+}, []);
+
+return (
+ <DialogContent className="sm:max-w-[475px] text-[#F2F3F5] rounded-2xl bg-background z-[39] backdrop-blur-md">
+ <form
+ action={async (e: FormData) => {
+ const content = e.get("content")?.toString();
+
+ 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>
+ {selectedSpaces.length > 0 && (
+ <div className="flex flex-row flex-wrap gap-0.5 h-min">
+ {[...new Set(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>
+ )}
+ </div>
+ </div>
+
+ <DialogFooter>
+ <Button
+ disabled={autoDetectedType === "none"}
+ variant={"secondary"}
+ type="submit"
+ >
+ Save {autoDetectedType != "none" && autoDetectedType}
+ </Button>
+ </DialogFooter>
+ </form>
+ </DialogContent>
+);
+} \ No newline at end of file
diff --git a/apps/web/app/(dash)/dialogTriggerWrapper.tsx b/apps/web/app/(dash)/dialogTriggerWrapper.tsx
new file mode 100644
index 00000000..7dcfc355
--- /dev/null
+++ b/apps/web/app/(dash)/dialogTriggerWrapper.tsx
@@ -0,0 +1,50 @@
+"use client";
+
+import { Dialog, DialogTrigger } from "@repo/ui/shadcn/dialog";
+import { useState } from "react";
+import { DialogContentContainer } from "./dialogContentContainer";
+import { PlusIcon } from "@heroicons/react/24/solid";
+
+export function DialogDesktopTrigger() {
+ return (
+ <DialogTriggerWrapper>
+ <div className="border-gray-700/50 border-[1px] space-y-4 group relative bg-[#1F2428] shadow-md shadow-[#1d1d1dc7] rounded-xl flex justify-center">
+ <button className="cursor-pointer p-2 hover:scale-105 hover:text-[#bfc4c9] active:scale-90">
+ <PlusIcon className="h-6 w-6" />
+ </button>
+ <div className="opacity-0 group-hover:opacity-100 scale-x-50 group-hover:scale-x-100 origin-left transition-all absolute whitespace-nowrap pointer-events-none border-gray-700/50 border-[1px] bg-[#1F2428] shadow-md shadow-[#1d1d1dc7] rounded-xl px-2 py-1 left-[120%] -top-2">
+ Add Memories
+ </div>
+ </div>
+ </DialogTriggerWrapper>
+ );
+}
+
+export function DialogMobileTrigger() {
+ return (
+ <DialogTriggerWrapper>
+ <div className={`flex flex-col items-center cursor-pointer text-white`}>
+ <PlusIcon className="h-6 w-6 hover:brightness-125 focus:brightness-125 duration-200 stroke-white" />
+ <p className="text-xs text-foreground-menu mt-2">Add</p>
+ </div>
+ </DialogTriggerWrapper>
+ );
+}
+export default function DialogTriggerWrapper({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ const [dialogOpen, setDialogOpen] = useState(false);
+
+ return (
+ <Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
+ <DialogTrigger>{children}</DialogTrigger>
+ <DialogContentContainer
+ DialogClose={() => {
+ setDialogOpen(false);
+ }}
+ />
+ </Dialog>
+ );
+}
diff --git a/apps/web/app/(dash)/home/filterSpaces.tsx b/apps/web/app/(dash)/home/filterSpaces.tsx
new file mode 100644
index 00000000..8818791a
--- /dev/null
+++ b/apps/web/app/(dash)/home/filterSpaces.tsx
@@ -0,0 +1,108 @@
+import { ChevronUpDownIcon } from "@heroicons/react/24/outline";
+import { ArrowRightIcon } from "@repo/ui/icons";
+import {
+ Command,
+ CommandGroup,
+ CommandInput,
+ CommandItem,
+ CommandList,
+} from "@repo/ui/shadcn/command";
+import { Check } from "lucide-react";
+import Image from "next/image";
+import React, { useState } from "react";
+
+type space = {
+ id: number;
+ name: string;
+};
+
+export function FilterSpaces({
+ initialSpaces,
+ selectedSpaces,
+ setSelectedSpaces,
+}: {
+ initialSpaces: space[];
+ selectedSpaces: space[];
+ setSelectedSpaces: React.Dispatch<React.SetStateAction<space[]>>;
+}) {
+ const [input, setInput] = useState<string>("");
+
+ const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
+ if (e.key === "Backspace" && input === "") {
+ setSelectedSpaces((prevValue) => prevValue.slice(0, -1));
+ }
+ };
+
+ const handleSelect = (selectedSpace: space) => {
+ setSelectedSpaces((current) =>
+ current.some((space) => space.id === selectedSpace.id)
+ ? current.filter((space) => space.id !== selectedSpace.id)
+ : [...current, selectedSpace],
+ );
+ };
+
+ return (
+ <div className="flex p-2 px-3 w-full items-center justify-between rounded-xl overflow-hidden">
+ <div className="flex bg-[#2C3338] rounded-xl overflow-hidden pl-1">
+ <div className="flex rounded-lg items-center">
+ {selectedSpaces.map((v) => (
+ <button
+ key={v.id}
+ onClick={() => handleSelect(v)}
+ className="bg-[#3a4248] text-white max-w-32 truncate-wor truncate whitespace-nowrap py-1 rounded-md px-2 mx-1 aria-selected:outline"
+ >
+ {v.name}
+ </button>
+ ))}
+ </div>
+ <Command
+ className={`group transition-all border-0 bg-[#2c3338] text-white outline-0 ${
+ selectedSpaces.length ? "w-5 hover:w-24 focus-within:w-20" : "w-44"
+ }`}
+ >
+ <div className="relative">
+ <CommandInput
+ placeholder={selectedSpaces.length ? "" : "Search in Spaces"}
+ onKeyDown={handleKeyDown}
+ className="text-white peer placeholder:text-white pl-2"
+ onChangeCapture={(e) => setInput(e.currentTarget.value)}
+ value={input}
+ />
+ <ChevronUpDownIcon
+ className={`h-6 w-6 text-[#858B92] pointer-events-none absolute top-1/2 -translate-y-1/2 right-2 ${
+ selectedSpaces.length && "opacity-0"
+ }`}
+ />
+ </div>
+ <CommandList className="z-10 translate-y-12 translate-x-5 opacity-0 absolute group-focus-within:opacity-100 transition-opacity p-2 rounded-lg max-w-64 bg-[#2C3338]">
+ <CommandGroup className="pointer-events-none opacity-0 group-focus-within:opacity-100 scale-50 scale-y-50 group-focus-within:scale-y-100 group-focus-within:scale-100 group-focus-within:pointer-events-auto transition-all origin-top">
+ {initialSpaces.map((space) => {
+ if (!selectedSpaces.some((v) => v.id === space.id)) {
+ return (
+ <CommandItem
+ className="text-[#eaeaea] data-[disabled]:opacity-90"
+ value={space.name}
+ key={space.id}
+ onSelect={() => handleSelect(space)}
+ >
+ <Check
+ className={`mr-2 h-4 w-4 ${selectedSpaces.some((v) => v.id === space.id) ? "opacity-100" : "opacity-0"}`}
+ />
+ {space.name}
+ </CommandItem>
+ );
+ }
+ })}
+ </CommandGroup>
+ </CommandList>
+ </Command>
+ </div>
+ <button
+ type="submit"
+ className="rounded-lg bg-[#369DFD1A] p-3"
+ >
+ <Image src={ArrowRightIcon} alt="Enter" />
+ </button>
+ </div>
+ );
+}
diff --git a/apps/web/app/(dash)/home/heading.tsx b/apps/web/app/(dash)/home/heading.tsx
new file mode 100644
index 00000000..dc5b8799
--- /dev/null
+++ b/apps/web/app/(dash)/home/heading.tsx
@@ -0,0 +1,38 @@
+import { useEffect, useState } from "react";
+import { AnimatePresence, motion } from "framer-motion";
+import { Inter } from "next/font/google";
+
+const poppins = Inter({ subsets: ["latin"], weight: ["600"] });
+
+const headings = [
+ "Unlock your digital brain",
+ "Save everything.",
+ " Connect anything.",
+ "Turn your bookmarks into insights.",
+ "The smart way to use your digital treasure.",
+];
+
+export function Heading({ queryPresent }: { queryPresent: boolean }) {
+ const [showHeading, setShowHeading] = useState<number>(0);
+ useEffect(() => {
+ setShowHeading(Math.floor(Math.random() * headings.length));
+ }, [queryPresent]);
+ return (
+ <div className="h-[7rem] flex items-end justify-center overflow-hidden text-white">
+ <AnimatePresence mode="popLayout">
+ {!queryPresent && (
+ <motion.h1
+ initial={{ opacity: 0, y: "20%" }}
+ animate={{ opacity: 1, y: "0%" }}
+ exit={{ opacity: 0, y: "20%", whiteSpace: "nowrap" }}
+ className={`text-[2.45rem] font-semibold ${
+ queryPresent ? "pointer-events-none" : "pointer-events-auto"
+ } transition-opacity text-center ${poppins.className}`}
+ >
+ {headings[showHeading]}
+ </motion.h1>
+ )}
+ </AnimatePresence>
+ </div>
+ );
+}
diff --git a/apps/web/app/(dash)/home/history.tsx b/apps/web/app/(dash)/home/history.tsx
new file mode 100644
index 00000000..9c6757e5
--- /dev/null
+++ b/apps/web/app/(dash)/home/history.tsx
@@ -0,0 +1,50 @@
+import { getRecentChats } from "@/app/actions/fetchers";
+import { ArrowLongRightIcon } from "@heroicons/react/24/outline";
+import { Skeleton } from "@repo/ui/shadcn/skeleton";
+import Link from "next/link";
+import { memo, useEffect, useState } from "react";
+import { motion } from "framer-motion";
+
+const History = memo(() => {
+ const [chatThreads, setChatThreads] = useState(null);
+
+ useEffect(() => {
+ (async () => {
+ const chatThreads = await getRecentChats();
+
+ // @ts-ignore
+ setChatThreads(chatThreads);
+ })();
+ }, []);
+
+ if (!chatThreads) {
+ return (
+ <>
+ <Skeleton className="w-[80%] h-4 bg-[#3b444b] "></Skeleton>
+ <Skeleton className="w-[40%] h-4 bg-[#3b444b] "></Skeleton>
+ <Skeleton className="w-[60%] h-4 bg-[#3b444b] "></Skeleton>
+ </>
+ );
+ }
+
+ // @ts-ignore, time wastage
+ if (!chatThreads.success || !chatThreads.data) {
+ return <div>Error fetching chat threads</div>;
+ }
+
+ return (
+ <ul className="text-base list-none space-y-3 text-[#b9b9b9]">
+ {/* @ts-ignore */}
+ {chatThreads.data.map((thread) => (
+ <motion.li initial={{opacity: 0, filter: "blur(1px)"}} animate={{opacity: 1, filter: "blur(0px)"}} className="flex items-center gap-2 truncate">
+ <ArrowLongRightIcon className="h-5" />{" "}
+ <Link prefetch={false} href={`/chat/${thread.id}`}>
+ {thread.firstMessage}
+ </Link>
+ </motion.li>
+ ))}
+ </ul>
+ );
+});
+
+export default History; \ No newline at end of file
diff --git a/apps/web/app/(dash)/home/homeVariants.ts b/apps/web/app/(dash)/home/homeVariants.ts
deleted file mode 100644
index 1b44bab9..00000000
--- a/apps/web/app/(dash)/home/homeVariants.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-export const variants = [
- [
- {
- type: "text",
- content: "Unlock your",
- },
- {
- type: "highlighted",
- content: " digital brain",
- },
- ],
- [
- {
- type: "text",
- content: "Save",
- },
- {
- type: "highlighted",
- content: " everything.",
- },
- {
- type: "text",
- content: " Connect",
- },
- {
- type: "highlighted",
- content: " anything.",
- },
- ],
- [
- {
- type: "text",
- content: "Turn your bookmarks into",
- },
- {
- type: "highlighted",
- content: " insights.",
- },
- ],
- [
- {
- type: "text",
- content: "The smart way to use your",
- },
- {
- type: "highlighted",
- content: " digital treasure.",
- },
- ],
-];
diff --git a/apps/web/app/(dash)/home/page.tsx b/apps/web/app/(dash)/home/page.tsx
index 378acdf8..630c4306 100644
--- a/apps/web/app/(dash)/home/page.tsx
+++ b/apps/web/app/(dash)/home/page.tsx
@@ -6,21 +6,18 @@ import { getSessionAuthToken, getSpaces } from "@/app/actions/fetchers";
import { useRouter } from "next/navigation";
import { createChatThread, linkTelegramToUser } from "@/app/actions/doers";
import { toast } from "sonner";
-import { motion } from "framer-motion";
-import { variants } from "./homeVariants";
+import { Heading } from "./heading";
+import History from "./history";
import { ChromeIcon, GithubIcon, TwitterIcon } from "lucide-react";
-const slap = {
- initial: {
- opacity: 0,
- scale: 1.1,
- },
- whileInView: { opacity: 1, scale: 1 },
- transition: {
- duration: 0.5,
- ease: "easeInOut",
- },
- viewport: { once: true },
+const linkTelegram = async (telegramUser: string) => {
+ const response = await linkTelegramToUser(telegramUser);
+
+ if (response.success) {
+ toast.success("Your telegram has been linked successfully.");
+ } else {
+ toast.error("Failed to link telegram. Please try again.");
+ }
};
function Page({
@@ -28,41 +25,24 @@ function Page({
}: {
searchParams: Record<string, string | string[] | undefined>;
}) {
- // TODO: use this to show a welcome page/modal
- // const { firstTime } = homeSearchParamsCache.parse(searchParams);
-
- const [telegramUser, setTelegramUser] = useState<string | undefined>(
- searchParams.telegramUser as string,
- );
- const [extensionInstalled, setExtensionInstalled] = useState<
- string | undefined
- >(searchParams.extension as string);
-
const { push } = useRouter();
const [spaces, setSpaces] = useState<{ id: number; name: string }[]>([]);
- const [showVariant, setShowVariant] = useState<number>(0);
+ const [queryPresent, setQueryPresent] = useState<boolean>(false);
useEffect(() => {
+ // telegram bot
+ const telegramUser = searchParams.extension as string;
if (telegramUser) {
- const linkTelegram = async () => {
- const response = await linkTelegramToUser(telegramUser);
-
- if (response.success) {
- toast.success("Your telegram has been linked successfully.");
- } else {
- toast.error("Failed to link telegram. Please try again.");
- }
- };
-
- linkTelegram();
+ linkTelegram(telegramUser);
}
- if (extensionInstalled) {
+ if (searchParams.extension as string) {
toast.success("Extension installed successfully");
}
+ // fetch spaces
getSpaces().then((res) => {
if (res.success && res.data) {
setSpaces(res.data);
@@ -71,44 +51,18 @@ function Page({
// TODO: HANDLE ERROR
});
- setShowVariant(Math.floor(Math.random() * variants.length));
-
getSessionAuthToken().then((token) => {
if (typeof window === "undefined") return;
window.postMessage({ token: token.data }, "*");
});
- }, [telegramUser]);
+ }, []);
return (
- <div className="max-w-3xl h-full justify-center flex mx-auto w-full flex-col px-2 md:px-0">
- {/* all content goes here */}
- {/* <div className="">hi {firstTime ? 'first time' : ''}</div> */}
-
- <motion.h1
- {...{
- ...slap,
- transition: { ...slap.transition, delay: 0.2 },
- }}
- className="text-center mx-auto bg-[linear-gradient(180deg,_#FFF_0%,_rgba(255,_255,_255,_0.00)_202.08%)] bg-clip-text text-4xl tracking-tighter text-transparent md:text-5xl"
- >
- {variants[showVariant]!.map((v, i) => {
- return (
- <span
- key={i}
- className={
- v.type === "highlighted"
- ? "bg-gradient-to-r to-blue-200 from-zinc-300 text-transparent bg-clip-text"
- : ""
- }
- >
- {v.content}
- </span>
- );
- })}
- </motion.h1>
-
- <div className="w-full pb-20 mt-12">
+ <div className="max-w-3xl mt-[18vh] mx-auto w-full px-2 md:px-0">
+ <Heading queryPresent={queryPresent} />
+ <div className="w-full py-12">
<QueryInput
+ setQueryPresent={(t: boolean) => setQueryPresent(t)}
handleSubmit={async (q, spaces) => {
if (q.length === 0) {
toast.error("Query is required");
@@ -127,9 +81,12 @@ function Page({
);
}}
initialSpaces={spaces}
- setInitialSpaces={setSpaces}
/>
</div>
+ <div className="space-y-5">
+ <h3 className="text-lg">Recent Searches</h3>
+ <History />
+ </div>
<div className="w-full fixed bottom-0 left-0 p-4">
<div className="flex items-center justify-center gap-8">
@@ -143,7 +100,7 @@ function Page({
Install extension
</a>
<a
- href="https://github.com/supermemoryai/supermemory/issues/new"
+ href="https://github.com/Dhravya/supermemory/issues/new"
target="_blank"
rel="noreferrer"
className="flex items-center gap-2 text-muted-foreground"
@@ -152,7 +109,7 @@ function Page({
Bug report
</a>
<a
- href="https://x.com/supermemoryai"
+ href="https://x.com/supermemory.ai"
target="_blank"
rel="noreferrer"
className="flex items-center gap-2 text-muted-foreground"
diff --git a/apps/web/app/(dash)/home/queryinput.tsx b/apps/web/app/(dash)/home/queryinput.tsx
index c7267298..f15a712b 100644
--- a/apps/web/app/(dash)/home/queryinput.tsx
+++ b/apps/web/app/(dash)/home/queryinput.tsx
@@ -1,83 +1,48 @@
"use client";
-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 { useRouter } from "next/navigation";
-import { getSpaces } from "@/app/actions/fetchers";
-import Combobox from "@repo/ui/shadcn/combobox";
-import { MinusIcon } from "lucide-react";
-import { toast } from "sonner";
-import { createSpace } from "@/app/actions/doers";
+import React, { useState } from "react";
+import { FilterSpaces } from "./filterSpaces";
function QueryInput({
- initialQuery = "",
- initialSpaces = [],
- disabled = false,
- className,
- mini = false,
+ setQueryPresent,
+ initialSpaces,
handleSubmit,
- setInitialSpaces,
}: {
- initialQuery?: string;
+ setQueryPresent: (t: boolean) => void;
initialSpaces?: {
id: number;
name: string;
}[];
- disabled?: boolean;
- className?: string;
mini?: boolean;
handleSubmit: (q: string, spaces: { id: number; name: string }[]) => void;
- setInitialSpaces?: React.Dispatch<
- React.SetStateAction<{ id: number; name: string }[]>
- >;
}) {
- const [q, setQ] = useState(initialQuery);
+ const [q, setQ] = useState("");
- const [selectedSpaces, setSelectedSpaces] = useState<number[]>([]);
-
- const options = useMemo(
- () =>
- initialSpaces.map((x) => ({
- label: x.name,
- value: x.id.toString(),
- })),
- [initialSpaces],
- );
-
- const preparedSpaces = useMemo(
- () =>
- initialSpaces
- .filter((x) => selectedSpaces.includes(x.id))
- .map((x) => {
- return {
- id: x.id,
- name: x.name,
- };
- }),
- [selectedSpaces, initialSpaces],
- );
+ const [selectedSpaces, setSelectedSpaces] = useState<
+ { id: number; name: string }[]
+ >([]);
return (
- <div className={`${className}`}>
+ <div className={`w-full`}>
<div
- className={`bg-secondary border-2 border-b-0 border-border ${!mini ? "rounded-t-3xl" : "rounded-3xl"}`}
+ className={`bg-[#1F2428] overflow-hidden border-2 border-gray-700/50 shadow-md shadow-[#1d1d1dc7] rounded-3xl`}
>
{/* input and action button */}
<form
action={async () => {
- handleSubmit(q, preparedSpaces);
+ if (q.trim().length === 0) {
+ return;
+ }
+ handleSubmit(q, selectedSpaces);
setQ("");
}}
- className="flex gap-4 p-3"
>
<textarea
autoFocus
name="q"
cols={30}
- rows={mini ? 2 : 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"
+ rows={4}
+ className="bg-transparent pt-2.5 text-lg placeholder:text-[#9B9B9B] text-gray-200 tracking-[3%] outline-none resize-none w-full p-4"
placeholder="Ask your second brain..."
onKeyDown={(e) => {
if (e.key === "Enter" && !e.shiftKey) {
@@ -85,95 +50,25 @@ function QueryInput({
if (q.trim().length === 0) {
return;
}
- handleSubmit(q, preparedSpaces);
+ handleSubmit(q, selectedSpaces);
setQ("");
}
}}
- onChange={(e) => setQ(e.target.value)}
+ onChange={(e) =>
+ setQ((prev) => {
+ setQueryPresent(!!e.target.value.length);
+ return e.target.value;
+ })
+ }
value={q}
- disabled={disabled}
/>
-
- <button
- type="submit"
- onClick={(e) => {
- e.preventDefault();
- if (q.trim().length === 0) {
- return;
- }
- handleSubmit(q, preparedSpaces);
- }}
- disabled={disabled}
- 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>
+ <FilterSpaces
+ selectedSpaces={selectedSpaces}
+ setSelectedSpaces={setSelectedSpaces}
+ initialSpaces={initialSpaces || []}
+ />
</form>
</div>
- {/* selected sources */}
- {!mini && (
- <>
- <Divider />
- <div className="flex justify-between items-center gap-6 h-auto bg-secondary rounded-b-3xl border-2 border-border">
- <Combobox
- options={options}
- className="rounded-bl-3xl bg-[#3C464D] w-44"
- 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);
- setInitialSpaces?.((prev) => [
- ...prev,
- {
- name: spaceName,
- id: creationTask.data!,
- },
- ]);
- setSelectedSpaces((prev) => [...prev, creationTask.data!]);
- } else {
- toast.error(
- "Space creation failed: " + creationTask.error ??
- "Unknown error",
- );
- }
- }}
- placeholder="Chat with a space..."
- />
-
- <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>
- </>
- )}
</div>
);
}
diff --git a/apps/web/app/(dash)/layout.tsx b/apps/web/app/(dash)/layout.tsx
index b2b27a4f..24857fb1 100644
--- a/apps/web/app/(dash)/layout.tsx
+++ b/apps/web/app/(dash)/layout.tsx
@@ -3,7 +3,6 @@ import Menu from "./menu";
import { redirect } from "next/navigation";
import { auth } from "../../server/auth";
import { Toaster } from "@repo/ui/shadcn/sonner";
-import BackgroundPlus from "../(landing)/GridPatterns/PlusGrid";
async function Layout({ children }: { children: React.ReactNode }) {
const info = await auth();
@@ -13,18 +12,15 @@ async function Layout({ children }: { children: React.ReactNode }) {
}
return (
- <main className="h-screen flex flex-col">
+ <main className="h-screen bg flex flex-col">
<div className="fixed top-0 left-0 w-full z-40">
<Header />
</div>
- <div className="relative flex justify-center z-40 pointer-events-none">
<div
- className="absolute -z-10 left-0 top-[10%] h-32 w-[90%] overflow-x-hidden bg-[rgb(54,157,253)] bg-opacity-100 md:bg-opacity-70 blur-[337.4px]"
+ className="absolute z-[100] left-0 top-[10%] h-32 w-[90%] overflow-x-hidden bg-[rgb(54,157,253)] bg-opacity-100 pointer-events-none md:bg-opacity-70 blur-[337.4px]"
style={{ transform: "rotate(-30deg)" }}
/>
- </div>
- <BackgroundPlus className="absolute top-0 left-0 w-full h-full -z-50 opacity-70" />
<Menu />
diff --git a/apps/web/app/(dash)/menu.tsx b/apps/web/app/(dash)/menu.tsx
index 711b081c..3cb79309 100644
--- a/apps/web/app/(dash)/menu.tsx
+++ b/apps/web/app/(dash)/menu.tsx
@@ -1,359 +1,174 @@
-"use client";
-
-import React, { useEffect, useMemo, useState } from "react";
+import React from "react";
import Image from "next/image";
import Link from "next/link";
-import { MemoriesIcon, ExploreIcon, CanvasIcon, AddIcon } from "@repo/ui/icons";
-import { Button } from "@repo/ui/shadcn/button";
-import { MinusIcon, 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 { toast } from "sonner";
-import { getSpaces } from "../actions/fetchers";
+import { MemoriesIcon, CanvasIcon, AddIcon } from "@repo/ui/icons";
+import { DialogTrigger } from "@repo/ui/shadcn/dialog";
+
import { HomeIcon } from "@heroicons/react/24/solid";
-import { createMemory, createSpace } from "../actions/doers";
-import ComboboxWithCreate from "@repo/ui/shadcn/combobox";
-import { StoredSpace } from "@/server/db/schema";
-import useMeasure from "react-use-measure";
+import {
+ PencilSquareIcon,
+ PlusIcon,
+ PresentationChartLineIcon,
+ RectangleStackIcon,
+} from "@heroicons/react/24/solid";
+import DialogTriggerWrapper, {
+ DialogDesktopTrigger,
+ DialogMobileTrigger,
+} from "./dialogTriggerWrapper";
+
+const menuItems = [
+ {
+ icon: MemoriesIcon,
+ text: "Memories",
+ url: "/memories",
+ disabled: false,
+ },
+ {
+ icon: CanvasIcon,
+ text: "Canvas",
+ url: "/canvas",
+ disabled: true,
+ },
+];
+
+const items = [
+ {
+ icon: <HomeIcon className="h-6 w-6" />,
+ name: "home",
+ url: "/home",
+ disabled: false,
+ },
+ {
+ icon: <RectangleStackIcon className="h-6 w-6" />,
+ name: "memories",
+ url: "/memories",
+ disabled: false,
+ },
+ {
+ icon: <PencilSquareIcon className="h-6 w-6" />,
+ name: "editor",
+ url: "/#",
+ disabled: true,
+ },
+ {
+ icon: <PresentationChartLineIcon className="h-6 w-6" />,
+ name: "thinkpad",
+ url: "/#",
+ disabled: true,
+ },
+];
function Menu() {
- const [spaces, setSpaces] = useState<StoredSpace[]>([]);
-
- 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,
- text: "Memories",
- url: "/memories",
- disabled: false,
- },
- {
- icon: CanvasIcon,
- text: "Canvas",
- url: "/canvas",
- disabled: true,
- },
- ];
-
- const [content, setContent] = useState("");
- const [selectedSpaces, setSelectedSpaces] = useState<number[]>([]);
-
- const autoDetectedType = useMemo(() => {
- if (content.length === 0) {
- return "none";
- }
-
- if (
- content.match(/https?:\/\/(x\.com|twitter\.com)\/[\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 options = useMemo(
- () =>
- spaces.map((x) => ({
- label: x.name,
- value: x.id.toString(),
- })),
- [spaces],
- );
-
- const handleSubmit = async (content?: string, spaces?: number[]) => {
- setDialogOpen(false);
-
- toast.info("Creating memory...", {
- icon: <PlusCircleIcon className="w-4 h-4 text-white animate-spin" />,
- duration: 7500,
- });
-
- if (!content || content.length === 0) {
- toast.error("Content is required");
- return;
- }
-
- console.log(spaces);
-
- const cont = await createMemory({
- content: content,
- spaces: spaces ?? undefined,
- });
-
- setContent("");
- setSelectedSpaces([]);
-
- if (cont.success) {
- toast.success("Memory created", {
- richColors: true,
- });
- } else {
- toast.error(`Memory creation failed: ${cont.error}`);
- }
- };
-
return (
<>
{/* Desktop Menu */}
- <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 z-[39]">
- <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 z-[99999]">
- <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">
- 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">
- {item.text}
- </p>
- </Link>
+ <div className="hidden lg:flex items-center pointer-events-none z-[39] fixed left-0 top-0 h-screen flex-col justify-center px-2">
+ <div className="pointer-events-none z-10 absolute top-1/2 h-1/3 w-full -translate-y-1/2 bg-blue-500/20 blur-[300px] "></div>
+ <div className="pointer-events-auto flex flex-col gap-2">
+ <DialogDesktopTrigger />
+ <div className="inline-flex w-14 flex-col items-start gap-6 rounded-2xl border-[1px] border-gray-700/50 bg-[#1F2428] px-3 py-4 text-[#b9b9b9] shadow-md shadow-[#1d1d1dc7]">
+ {items.map((v) => (
+ <NavItem {...v} />
))}
</div>
</div>
-
- <DialogContent className="sm:max-w-[475px] text-[#F2F3F5] rounded-2xl bg-background z-[39] backdrop-blur-md">
- <form
- action={async (e: FormData) => {
- const content = e.get("content")?.toString();
-
- await handleSubmit(content, selectedSpaces);
- }}
- className="flex flex-col gap-4 "
+ </div>
+
+ {/* Mobile Menu */}
+ <div className="lg:hidden fixed bottom-0 left-0 w-full p-4 bg-secondary z-50 border-t-2 border-border">
+ <div className="flex justify-around items-center">
+ <Link
+ href={"/"}
+ className={`flex flex-col items-center text-white ${"cursor-pointer"}`}
>
- <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...");
+ <HomeIcon width={24} height={24} />
+ <p className="text-xs text-foreground-menu mt-2">Home</p>
+ </Link>
- 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">
- {[...new Set(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>
- )}
- </div>
- </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 z-50 border-t-2 border-border">
- <div className="flex justify-around items-center">
+ <DialogMobileTrigger />
+ {menuItems.map((item) => (
<Link
- href={"/"}
- className={`flex flex-col items-center text-white ${"cursor-pointer"}`}
- >
- <HomeIcon width={24} height={24} />
- <p className="text-xs text-foreground-menu mt-2">Home</p>
- </Link>
-
- <DialogTrigger
- className={`flex flex-col items-center cursor-pointer text-white`}
+ aria-disabled={item.disabled}
+ href={item.disabled ? "#" : item.url}
+ key={item.url}
+ className={`flex flex-col items-center ${
+ item.disabled
+ ? "opacity-50 pointer-events-none"
+ : "cursor-pointer"
+ }`}
>
<Image
- src={AddIcon}
- alt="Logo"
+ src={item.icon}
+ alt={`${item.text} icon`}
width={24}
height={24}
- className="hover:brightness-125 focus:brightness-125 duration-200 stroke-white"
/>
- <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>
+ <p className="text-xs text-foreground-menu mt-2">{item.text}</p>
+ </Link>
+ ))}
</div>
- </Dialog>
+ </div>
</>
);
}
+export function Navbar() {
+ return (
+ <div className="pointer-events-none fixed left-0 top-0 flex h-screen flex-col justify-center px-2">
+ <div className="pointer-events-none absolute top-1/2 h-1/3 w-full -translate-y-1/2 bg-blue-500/20 blur-[300px] "></div>
+ <div className="pointer-events-auto">
+ <div className="inline-flex w-14 flex-col items-start gap-6 rounded-2xl border-[1px] border-gray-700/50 bg-[#1f24289b] px-3 py-4 text-[#b9b9b9] shadow-md shadow-[#1d1d1dc7]">
+ <Top />
+ {items.map((v) => (
+ <NavItem {...v} />
+ ))}
+ </div>
+ </div>
+ </div>
+ );
+}
+
+function Top() {
+ return (
+ <DialogTriggerWrapper>
+ <DialogTrigger>
+ <div className="space-y-4 group relative">
+ <button className="cursor-pointer px-1 hover:scale-105 hover:text-[#bfc4c9] active:scale-90">
+ <PlusIcon className="h-6 w-6" />
+ </button>
+ <div className="h-[1px] w-full bg-[#323b41]"></div>
+ <div className="opacity-0 group-hover:opacity-100 scale-x-50 group-hover:scale-x-100 origin-left transition-all absolute whitespace-nowrap -top-1 -translate-y-1/2 left-[150%] pointer-events-none border-gray-700/50 border-[1px] bg-[#1F2428] shadow-md shadow-[#1d1d1dc7] rounded-xl px-2 py-1">
+ Add Memories
+ </div>
+ </div>
+ </DialogTrigger>
+ </DialogTriggerWrapper>
+ );
+}
+
+function NavItem({
+ disabled,
+ icon,
+ url,
+ name,
+}: {
+ disabled: boolean;
+ icon: React.JSX.Element;
+ name: string;
+ url: string;
+}) {
+ return (
+ <div className="relative group">
+ <Link aria-disabled={disabled} href={disabled ? "#" : url}>
+ <div className={`cursor-pointer px-1 hover:scale-105 hover:text-[#bfc4c9] active:scale-90 ${disabled && "opacity-50"}`}>
+ {icon}
+ </div>
+ </Link>
+ <div className="opacity-0 group-hover:opacity-100 scale-x-50 group-hover:scale-x-100 origin-left transition-all absolute whitespace-nowrap top-1/2 -translate-y-1/2 left-[150%] pointer-events-none border-gray-700/50 border-[1px] bg-[#1F2428] shadow-md shadow-[#1d1d1dc7] rounded-xl px-2 py-1">
+ {name}
+ </div>
+ </div>
+ );
+}
+
export default Menu;
diff --git a/apps/web/app/(onboarding)/layout.tsx b/apps/web/app/(onboarding)/layout.tsx
new file mode 100644
index 00000000..10a9a3ec
--- /dev/null
+++ b/apps/web/app/(onboarding)/layout.tsx
@@ -0,0 +1,12 @@
+import React from "react";
+import { Toaster } from "@repo/ui/shadcn/sonner";
+
+function layout({ children }: { children: React.ReactNode }) {
+ return (
+ <div>
+ {children} <Toaster />
+ </div>
+ );
+}
+
+export default layout;
diff --git a/apps/web/app/(onboarding)/onboarding/page.tsx b/apps/web/app/(onboarding)/onboarding/page.tsx
new file mode 100644
index 00000000..294efb33
--- /dev/null
+++ b/apps/web/app/(onboarding)/onboarding/page.tsx
@@ -0,0 +1,388 @@
+"use client";
+
+import Link from "next/link";
+import {
+ ChevronLeftIcon,
+ ChevronRightIcon,
+ QuestionMarkCircleIcon,
+ ArrowTurnDownLeftIcon,
+} from "@heroicons/react/24/solid";
+import { CheckIcon, PlusCircleIcon } from "@heroicons/react/24/outline";
+import { motion } from "framer-motion";
+import { useState } from "react";
+import { toast } from "sonner";
+import { createMemory } from "@/app/actions/doers";
+import { useRouter } from "next/navigation";
+
+export default function Home() {
+ const [currStep, setCurrStep] = useState(0);
+
+ return (
+ <main className="min-h-screen text-sm bg text-[#B8C4C6]">
+ {/* Navbar */}
+ <Navbar />
+
+ {/* main-content */}
+ <div className="w-full max-w-3xl p-4 mt-24 sm:mt-32 mx-auto">
+ {currStep === 0 && (
+ <div className="text-white space-y-3">
+ <h1 className="text-2xl">
+ We are so excited to have you.. but but first let's set up
+ everything
+ </h1>
+ </div>
+ )}
+ {currStep >= 1 && <StepOne currStep={currStep} />}
+ {currStep >= 2 && <StepTwo currStep={currStep} />}
+ {currStep >= 3 && <StepThree currStep={currStep} />}
+ </div>
+ <div className="absolute flex justify-center w-full bottom-0 left-0 mb-4 bg-[#171B1F]">
+ <StepIndicator
+ setCurrStep={(v) => setCurrStep(v)}
+ currStep={currStep}
+ />
+ </div>
+ </main>
+ );
+}
+
+function StepOne({ currStep }: { currStep: number }) {
+ return (
+ <motion.div
+ initial={{ opacity: 0, x: "50%" }}
+ animate={{ opacity: 1, x: 0 }}
+ transition={{ duration: 0.2, type: "spring", bounce: 0.1, delay: 0.15 }}
+ >
+ <motion.div
+ animate={{
+ y: currStep > 1 ? (currStep > 2 ? -40 : -20) : 0,
+ }}
+ transition={{ duration: 0.2, type: "spring", bounce: 0.1 }}
+ >
+ <div
+ className={`flex items-center justify-between transition-colors w-full p-4 rounded-2xl ${
+ currStep > 1
+ ? "bg-[#26D987]/10 text-[#26D987]"
+ : "bg-[#1F2428] text-white"
+ } `}
+ >
+ <div className="flex items-center gap-4">
+ <div>
+ <svg
+ width="24"
+ height="24"
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ d="M22.46 6C21.69 6.35 20.86 6.58 20 6.69C20.88 6.16 21.56 5.32 21.88 4.31C21.05 4.81 20.13 5.16 19.16 5.36C18.37 4.5 17.26 4 16 4C13.65 4 11.73 5.92 11.73 8.29C11.73 8.63 11.77 8.96 11.84 9.27C8.28004 9.09 5.11004 7.38 3.00004 4.79C2.63004 5.42 2.42004 6.16 2.42004 6.94C2.42004 8.43 3.17004 9.75 4.33004 10.5C3.62004 10.5 2.96004 10.3 2.38004 10V10.03C2.38004 12.11 3.86004 13.85 5.82004 14.24C5.19077 14.4122 4.53013 14.4362 3.89004 14.31C4.16165 15.1625 4.69358 15.9084 5.41106 16.4429C6.12854 16.9775 6.99549 17.2737 7.89004 17.29C6.37367 18.4904 4.49404 19.1393 2.56004 19.13C2.22004 19.13 1.88004 19.11 1.54004 19.07C3.44004 20.29 5.70004 21 8.12004 21C16 21 20.33 14.46 20.33 8.79C20.33 8.6 20.33 8.42 20.32 8.23C21.16 7.63 21.88 6.87 22.46 6Z"
+ fill="currentColor"
+ />
+ </svg>
+ </div>
+ <div>
+ <p className="text-base">Import twitter bookmarks</p>
+ <p className="opacity-70">
+ Directly import all your bookmarks from twitter in single click
+ </p>
+ </div>
+ </div>
+ <div>{getStatusIcon({ completed: currStep > 1 })}</div>
+ </div>
+ {currStep === 1 && (
+ <div className="my-4 bg-[#1F2428] rounded-2xl p-4">
+ <ol className="text-lg space-y-3">
+ <li>
+ First Download our Extension from{" "}
+ <a
+ className="underline underline-offset-2"
+ href="https://chromewebstore.google.com/detail/supermemory/afpgkkipfdpeaflnpoaffkcankadgjfc"
+ >
+ here
+ </a>
+ </li>
+ <li>
+ Once downloaded, it will try to authenticate you by visiting
+ supermemory website, coordinate it with it.
+ </li>
+ <li>
+ After successful authentication, visit x.com and click on the
+ bookmark icon and done, all your bookmarks are in supermemory.
+ </li>
+ <li>
+ <span className="bg-gray-600/30 py-1 px-2 text-white ">
+ hover over the bottom left area of the browser window to see
+ the extension
+ </span>
+ </li>
+ </ol>
+ <img
+ className="mx-auto mt-8 mb-4 rounded-xl"
+ src="/image3.png"
+ alt=""
+ />
+ </div>
+ )}
+ </motion.div>
+ </motion.div>
+ );
+}
+
+function StepIndicator({
+ currStep,
+ setCurrStep,
+}: {
+ currStep: number;
+ setCurrStep: (v: number) => void;
+}) {
+ return (
+ <div className="flex flex-col items-center gap-3 p-4 select-none">
+ <div className="flex items-center w-full justify-between">
+ <ChevronLeftIcon
+ className={`h-6 ${currStep >= 2 ? "opacity-100" : "opacity-0"}`}
+ onClick={() => currStep >= 2 && setCurrStep(currStep - 1)}
+ />
+ <p>Step: {currStep}/3</p>
+ <ChevronRightIcon
+ className="h-6"
+ onClick={() => currStep <= 3 && setCurrStep(currStep + 1)}
+ />
+ </div>
+
+ <div className="flex items-center gap-3">
+ {Array.from({ length: 3 }).map((_, i) => (
+ <div
+ className={`w-16 h-2 ${
+ currStep > i + 1 ? "bg-[#26D987]" : "bg-white/10"
+ } rounded-full overflow-hidden`}
+ >
+ {i === currStep - 1 && (
+ <motion.div
+ initial={{ scaleX: 0 }}
+ transition={{ duration: 0.8, ease: "linear" }}
+ animate={{ scaleX: 1 }}
+ className="bg-[#26D987] w-full h-full origin-left"
+ ></motion.div>
+ )}
+ </div>
+ ))}
+ </div>
+ </div>
+ );
+}
+
+function StepThree({ currStep }: { currStep: number }) {
+ const { push } = useRouter();
+
+ return (
+ <motion.div
+ initial={{ opacity: 0, x: "50%" }}
+ animate={{ opacity: 1, x: 0 }}
+ transition={{ duration: 0.2, type: "spring", bounce: 0 }}
+ >
+ <div
+ className={`flex items-center justify-between w-full p-4 rounded-2xl ${
+ currStep > 3
+ ? "bg-[#26D987]/10 text-[#26D987]"
+ : "bg-[#1F2428] text-white"
+ } `}
+ >
+ {/* info */}
+ <div className="flex items-center gap-4">
+ {/* icon */}
+ <div>
+ {/* custom twitter icon */}
+ <svg
+ width="24"
+ height="24"
+ viewBox="0 0 24 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ d="M22.46 6C21.69 6.35 20.86 6.58 20 6.69C20.88 6.16 21.56 5.32 21.88 4.31C21.05 4.81 20.13 5.16 19.16 5.36C18.37 4.5 17.26 4 16 4C13.65 4 11.73 5.92 11.73 8.29C11.73 8.63 11.77 8.96 11.84 9.27C8.28004 9.09 5.11004 7.38 3.00004 4.79C2.63004 5.42 2.42004 6.16 2.42004 6.94C2.42004 8.43 3.17004 9.75 4.33004 10.5C3.62004 10.5 2.96004 10.3 2.38004 10V10.03C2.38004 12.11 3.86004 13.85 5.82004 14.24C5.19077 14.4122 4.53013 14.4362 3.89004 14.31C4.16165 15.1625 4.69358 15.9084 5.41106 16.4429C6.12854 16.9775 6.99549 17.2737 7.89004 17.29C6.37367 18.4904 4.49404 19.1393 2.56004 19.13C2.22004 19.13 1.88004 19.11 1.54004 19.07C3.44004 20.29 5.70004 21 8.12004 21C16 21 20.33 14.46 20.33 8.79C20.33 8.6 20.33 8.42 20.32 8.23C21.16 7.63 21.88 6.87 22.46 6Z"
+ fill="currentColor"
+ />
+ </svg>
+ </div>
+ <div>
+ <p className="text-base">Ask a question</p>
+ <p className="opacity-70">
+ Let's use the true power of supermemory!
+ </p>
+ </div>
+ </div>
+
+ {/* status */}
+ <div>{getStatusIcon({ completed: currStep > 3 })}</div>
+ </div>
+
+ {currStep === 3 && (
+ <div className="my-4 bg-[#1F2428] rounded-2xl p-4">
+ <ol className="text-lg space-y-5">
+ <li>
+ Let's start off by adding some content! We have added
+ supermemory's link for you press enter
+ </li>
+ <li>
+ <span className="bg-gray-600/30 py-1 px-2 text-white ">
+ Once saved, you'll be redirected to home from there you can
+ search supermemory.ai or whatever you saved, and woilla, the
+ result is there!
+ </span>
+ </li>
+ <li className="relative">
+ <form
+ action={async (formData) => {
+ toast.info("Creating memory...", {
+ icon: (
+ <PlusCircleIcon className="w-4 h-4 text-white animate-spin" />
+ ),
+ duration: 7500,
+ });
+
+ const cont = await createMemory({
+ content: (formData.get("cont") as string) || "",
+ spaces: undefined,
+ });
+
+ if (cont.success) {
+ toast.success("Memory created", {
+ richColors: true,
+ });
+ } else {
+ toast.error(`Memory creation failed: ${cont.error}`);
+ }
+
+ push(`/home`);
+ }}
+ >
+ <textarea
+ name="cont"
+ defaultValue="https://supermemory.ai"
+ rows={3}
+ placeholder="paste any link or text here.. supermemory will save it"
+ className="w-full bg-[#24292e] px-2 py-1 outline-none border-2 border-[#2e353b] shadow-md resize-none"
+ />
+ <button
+ type="submit"
+ className="rounded-lg bg-[#369DFD1A] p-3 absolute bottom-4 right-2"
+ >
+ <ArrowTurnDownLeftIcon className="w-4 h-4 text-[#369DFD]" />
+ </button>
+ </form>
+ </li>
+ </ol>
+ </div>
+ )}
+ </motion.div>
+ );
+}
+
+function StepTwo({ currStep }: { currStep: number }) {
+ return (
+ <motion.div
+ initial={{ opacity: 0, x: "50%" }}
+ animate={{ opacity: 1, x: 0 }}
+ transition={{ duration: 0.2, type: "spring", bounce: 0, delay: 0.15 }}
+ >
+ <motion.div
+ animate={{ y: currStep > 2 ? -20 : 0 }}
+ transition={{ duration: 0.2, type: "spring", bounce: 0 }}
+ >
+ <div
+ className={`flex items-center justify-between w-full p-4 rounded-2xl ${
+ currStep > 2
+ ? "bg-[#26D987]/10 text-[#26D987]"
+ : "bg-[#1F2428] text-white"
+ } `}
+ >
+ {/* info */}
+ <div className="flex items-center gap-4">
+ {/* icon */}
+ <div>
+ {/* custom twitter icon */}
+ <QuestionMarkCircleIcon className="w-6 h-6" />
+ </div>
+ <div>
+ <p className="text-base">Adding Content into supermemory</p>
+ <p className="opacity-70">one click method to save your time.</p>
+ </div>
+ </div>
+
+ {/* status */}
+ <div>{getStatusIcon({ completed: currStep > 2 })}</div>
+ </div>
+
+ {currStep === 2 && (
+ <div className="my-4 bg-[#1F2428] rounded-2xl p-4">
+ <ol className="text-lg space-y-3">
+ <li>
+ You can either add content from the home or from the extension.
+ </li>
+ </ol>
+ <img
+ className="mx-auto mt-8 mb-4 rounded-xl w-[60%]"
+ src="/image.png"
+ alt=""
+ />
+ <img
+ className="mx-auto mt-8 mb-4 rounded-xl w-[40%]"
+ src="/img.png"
+ alt=""
+ />
+ </div>
+ )}
+ </motion.div>
+ </motion.div>
+ );
+}
+
+function Navbar() {
+ return (
+ <div className="flex items-center justify-between p-4">
+ {/* logo */}
+ <svg
+ width="28"
+ height="32"
+ viewBox="0 0 28 32"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ d="M11.5 0C13.4312 0 14 1.56875 14 3.5V10.25H17.25V7.65685C17.25 6.39708 17.7504 5.18889 18.6412 4.2981L19.1583 3.78107C19.0564 3.54111 19 3.27714 19 3C19 1.89543 19.8954 1 21 1C22.1046 1 23 1.89543 23 3C23 4.10457 22.1046 5 21 5C20.7229 5 20.4589 4.94363 20.2189 4.84173L19.7019 5.35876C19.0924 5.96825 18.75 6.7949 18.75 7.65685V10.25H24.1454C24.4421 9.51704 25.1607 9 26 9C27.1046 9 28 9.89543 28 11C28 12.1046 27.1046 13 26 13C25.1607 13 24.4421 12.483 24.1454 11.75H14V16.25H19.1454C19.4421 15.517 20.1607 15 21 15C22.1046 15 23 15.8954 23 17C23 18.1046 22.1046 19 21 19C20.1607 19 19.4421 18.483 19.1454 17.75H14V22.25H18.3431C19.6029 22.25 20.8111 22.7504 21.7019 23.6412L24.2189 26.1583C24.4589 26.0564 24.7229 26 25 26C26.1046 26 27 26.8954 27 28C27 29.1046 26.1046 30 25 30C23.8954 30 23 29.1046 23 28C23 27.7229 23.0564 27.4589 23.1583 27.2189L20.6412 24.7019C20.0317 24.0924 19.2051 23.75 18.3431 23.75H14V28.5C14 30.4312 13.4312 32 11.5 32C9.69375 32 8.20625 30.6313 8.01875 28.8687C7.69375 28.9562 7.35 29 7 29C4.79375 29 3 27.2062 3 25C3 24.5375 3.08125 24.0875 3.225 23.675C1.3375 22.9625 0 21.1375 0 19C0 17.0063 1.16875 15.2812 2.8625 14.4812C2.31875 13.8 2 12.9375 2 12C2 10.0813 3.35 8.48125 5.15 8.0875C5.05 7.74375 5 7.375 5 7C5 5.13125 6.2875 3.55625 8.01875 3.11875C8.20625 1.36875 9.69375 0 11.5 0Z"
+ fill="#545B62"
+ />
+ </svg>
+
+ <Link href="/home">
+ <button className="text-sm">Skip</button>
+ </Link>
+ </div>
+ );
+}
+
+const getStatusIcon = ({ completed }: { completed: boolean }) => {
+ if (!completed) {
+ return (
+ <svg
+ width="20"
+ height="20"
+ viewBox="0 0 20 20"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ d="M8.41615 1.81781C9.462 1.61542 10.537 1.61542 11.5828 1.81781M11.5828 18.1811C10.537 18.3835 9.462 18.3835 8.41615 18.1811M14.6736 3.10031C15.5582 3.69967 16.3192 4.46354 16.9153 5.35032M1.81781 11.5828C1.61542 10.537 1.61542 9.462 1.81781 8.41615M16.8986 14.6736C16.2993 15.5582 15.5354 16.3192 14.6486 16.9153M18.1811 8.41615C18.3835 9.462 18.3835 10.537 18.1811 11.5828M3.10031 5.32531C3.69967 4.44076 4.46354 3.67971 5.35032 3.08365M5.32531 16.8986C4.44076 16.2993 3.67971 15.5354 3.08365 14.6486"
+ stroke="currentColor"
+ stroke-width="2"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ );
+ } else {
+ return <CheckIcon className="w-6 h-6" />;
+ }
+};
diff --git a/apps/web/app/actions/fetchers.ts b/apps/web/app/actions/fetchers.ts
index 1838ee1c..74df0b04 100644
--- a/apps/web/app/actions/fetchers.ts
+++ b/apps/web/app/actions/fetchers.ts
@@ -1,6 +1,6 @@
"use server";
-import { and, asc, eq, exists, inArray, not, or, sql } from "drizzle-orm";
+import { and, asc, desc, eq, exists, inArray, not, or, sql } from "drizzle-orm";
import { db } from "../../server/db";
import {
canvas,
@@ -228,6 +228,34 @@ export const getFullChatThread = async (
};
};
+export const getRecentChats = async (): ServerActionReturnType<ChatThread[]> => {
+ const data = await auth();
+
+ if (!data || !data.user || !data.user.id) {
+ redirect("/signin");
+ return { error: "Not authenticated", success: false };
+ }
+
+ try {
+ const chatHistorys = await db.query.chatThreads.findMany({
+ where: eq(chatThreads.userId, data.user.id),
+ orderBy: desc(chatThreads.createdAt),
+ limit: 4,
+ });
+
+ return {
+ success: true,
+ data: chatHistorys,
+ };
+ } catch (e) {
+ return {
+ success: false,
+ error: (e as Error).message,
+ };
+ }
+};
+
+
export const getChatHistory = async (): ServerActionReturnType<
ChatThread[]
> => {
diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx
index cf6e9b0f..2d612eef 100644
--- a/apps/web/app/layout.tsx
+++ b/apps/web/app/layout.tsx
@@ -1,13 +1,13 @@
import "@repo/tailwind-config/globals.css";
import type { Metadata } from "next";
-import { Inter } from "next/font/google";
+import { Poppins } from "next/font/google";
import { GeistSans } from "geist/font/sans";
import { GeistMono } from "geist/font/mono";
import { cn } from "@repo/ui/lib/utils";
import { Toaster } from "@repo/ui/shadcn/toaster";
-const inter = Inter({ subsets: ["latin"] });
+const poppins = Poppins({ subsets: ["latin"], weight: ["400", "500", "600", "700"] });
export const runtime = "edge";
@@ -72,7 +72,7 @@ export default function RootLayout({
{/* TODO: when lightmode support is added, remove the 'dark' class from the body tag */}
<body
className={cn(
- `${inter.className} dark`,
+ `${poppins.className} dark`,
GeistMono.variable,
GeistSans.variable,
)}
diff --git a/apps/web/migrations/0001_nervous_longshot.sql b/apps/web/migrations/0001_nervous_longshot.sql
new file mode 100644
index 00000000..a15917c6
--- /dev/null
+++ b/apps/web/migrations/0001_nervous_longshot.sql
@@ -0,0 +1,2 @@
+ALTER TABLE `chatThread` ADD `createdAt` integer;
+UPDATE `chatThread` SET `createdAt` = strftime('%s', 'now') WHERE `createdAt` IS NULL;
diff --git a/apps/web/migrations/meta/0001_snapshot.json b/apps/web/migrations/meta/0001_snapshot.json
new file mode 100644
index 00000000..91c50c13
--- /dev/null
+++ b/apps/web/migrations/meta/0001_snapshot.json
@@ -0,0 +1,905 @@
+{
+ "version": "6",
+ "dialect": "sqlite",
+ "id": "39768c98-95d2-4d42-bae0-43e3e511be7e",
+ "prevId": "ab91d972-05ff-4916-84b7-1cfaab4c3879",
+ "tables": {
+ "account": {
+ "name": "account",
+ "columns": {
+ "userId": {
+ "name": "userId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "provider": {
+ "name": "provider",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "providerAccountId": {
+ "name": "providerAccountId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "refresh_token": {
+ "name": "refresh_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "access_token": {
+ "name": "access_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "token_type": {
+ "name": "token_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "scope": {
+ "name": "scope",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "id_token": {
+ "name": "id_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "session_state": {
+ "name": "session_state",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "account_userId_user_id_fk": {
+ "name": "account_userId_user_id_fk",
+ "tableFrom": "account",
+ "tableTo": "user",
+ "columnsFrom": [
+ "userId"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "account_provider_providerAccountId_pk": {
+ "columns": [
+ "provider",
+ "providerAccountId"
+ ],
+ "name": "account_provider_providerAccountId_pk"
+ }
+ },
+ "uniqueConstraints": {}
+ },
+ "authenticator": {
+ "name": "authenticator",
+ "columns": {
+ "credentialID": {
+ "name": "credentialID",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "userId": {
+ "name": "userId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "providerAccountId": {
+ "name": "providerAccountId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "credentialPublicKey": {
+ "name": "credentialPublicKey",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "counter": {
+ "name": "counter",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "credentialDeviceType": {
+ "name": "credentialDeviceType",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "credentialBackedUp": {
+ "name": "credentialBackedUp",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "transports": {
+ "name": "transports",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "authenticator_credentialID_unique": {
+ "name": "authenticator_credentialID_unique",
+ "columns": [
+ "credentialID"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {
+ "authenticator_userId_user_id_fk": {
+ "name": "authenticator_userId_user_id_fk",
+ "tableFrom": "authenticator",
+ "tableTo": "user",
+ "columnsFrom": [
+ "userId"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "authenticator_userId_credentialID_pk": {
+ "columns": [
+ "credentialID",
+ "userId"
+ ],
+ "name": "authenticator_userId_credentialID_pk"
+ }
+ },
+ "uniqueConstraints": {}
+ },
+ "canvas": {
+ "name": "canvas",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'Untitled'"
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'Untitled'"
+ },
+ "url": {
+ "name": "url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "''"
+ },
+ "userId": {
+ "name": "userId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "canvas_user_userId": {
+ "name": "canvas_user_userId",
+ "columns": [
+ "userId"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "canvas_userId_user_id_fk": {
+ "name": "canvas_userId_user_id_fk",
+ "tableFrom": "canvas",
+ "tableTo": "user",
+ "columnsFrom": [
+ "userId"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "chatHistory": {
+ "name": "chatHistory",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "threadId": {
+ "name": "threadId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "question": {
+ "name": "question",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "answerParts": {
+ "name": "answerParts",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "answerSources": {
+ "name": "answerSources",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "answerJustification": {
+ "name": "answerJustification",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "chatHistory_thread_idx": {
+ "name": "chatHistory_thread_idx",
+ "columns": [
+ "threadId"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "chatHistory_threadId_chatThread_id_fk": {
+ "name": "chatHistory_threadId_chatThread_id_fk",
+ "tableFrom": "chatHistory",
+ "tableTo": "chatThread",
+ "columnsFrom": [
+ "threadId"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "chatThread": {
+ "name": "chatThread",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "firstMessage": {
+ "name": "firstMessage",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "userId": {
+ "name": "userId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "chatThread_user_idx": {
+ "name": "chatThread_user_idx",
+ "columns": [
+ "userId"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "chatThread_userId_user_id_fk": {
+ "name": "chatThread_userId_user_id_fk",
+ "tableFrom": "chatThread",
+ "tableTo": "user",
+ "columnsFrom": [
+ "userId"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "contentToSpace": {
+ "name": "contentToSpace",
+ "columns": {
+ "contentId": {
+ "name": "contentId",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "spaceId": {
+ "name": "spaceId",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "contentToSpace_contentId_storedContent_id_fk": {
+ "name": "contentToSpace_contentId_storedContent_id_fk",
+ "tableFrom": "contentToSpace",
+ "tableTo": "storedContent",
+ "columnsFrom": [
+ "contentId"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "contentToSpace_spaceId_space_id_fk": {
+ "name": "contentToSpace_spaceId_space_id_fk",
+ "tableFrom": "contentToSpace",
+ "tableTo": "space",
+ "columnsFrom": [
+ "spaceId"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "contentToSpace_contentId_spaceId_pk": {
+ "columns": [
+ "contentId",
+ "spaceId"
+ ],
+ "name": "contentToSpace_contentId_spaceId_pk"
+ }
+ },
+ "uniqueConstraints": {}
+ },
+ "session": {
+ "name": "session",
+ "columns": {
+ "sessionToken": {
+ "name": "sessionToken",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "userId": {
+ "name": "userId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "expires": {
+ "name": "expires",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "session_userId_user_id_fk": {
+ "name": "session_userId_user_id_fk",
+ "tableFrom": "session",
+ "tableTo": "user",
+ "columnsFrom": [
+ "userId"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "space": {
+ "name": "space",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'none'"
+ },
+ "user": {
+ "name": "user",
+ "type": "text(255)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "numItems": {
+ "name": "numItems",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": 0
+ }
+ },
+ "indexes": {
+ "space_name_unique": {
+ "name": "space_name_unique",
+ "columns": [
+ "name"
+ ],
+ "isUnique": true
+ },
+ "spaces_name_idx": {
+ "name": "spaces_name_idx",
+ "columns": [
+ "name"
+ ],
+ "isUnique": false
+ },
+ "spaces_user_idx": {
+ "name": "spaces_user_idx",
+ "columns": [
+ "user"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "space_user_user_id_fk": {
+ "name": "space_user_user_id_fk",
+ "tableFrom": "space",
+ "tableTo": "user",
+ "columnsFrom": [
+ "user"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "spacesAccess": {
+ "name": "spacesAccess",
+ "columns": {
+ "spaceId": {
+ "name": "spaceId",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "userEmail": {
+ "name": "userEmail",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "spacesAccess_spaceId_space_id_fk": {
+ "name": "spacesAccess_spaceId_space_id_fk",
+ "tableFrom": "spacesAccess",
+ "tableTo": "space",
+ "columnsFrom": [
+ "spaceId"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "spacesAccess_spaceId_userEmail_pk": {
+ "columns": [
+ "spaceId",
+ "userEmail"
+ ],
+ "name": "spacesAccess_spaceId_userEmail_pk"
+ }
+ },
+ "uniqueConstraints": {}
+ },
+ "storedContent": {
+ "name": "storedContent",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "content": {
+ "name": "content",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "title": {
+ "name": "title",
+ "type": "text(255)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "description": {
+ "name": "description",
+ "type": "text(255)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "url": {
+ "name": "url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "savedAt": {
+ "name": "savedAt",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "baseUrl": {
+ "name": "baseUrl",
+ "type": "text(255)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "ogImage": {
+ "name": "ogImage",
+ "type": "text(255)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'page'"
+ },
+ "image": {
+ "name": "image",
+ "type": "text(255)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "user": {
+ "name": "user",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "noteId": {
+ "name": "noteId",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "storedContent_baseUrl_unique": {
+ "name": "storedContent_baseUrl_unique",
+ "columns": [
+ "baseUrl"
+ ],
+ "isUnique": true
+ },
+ "storedContent_url_idx": {
+ "name": "storedContent_url_idx",
+ "columns": [
+ "url"
+ ],
+ "isUnique": false
+ },
+ "storedContent_savedAt_idx": {
+ "name": "storedContent_savedAt_idx",
+ "columns": [
+ "savedAt"
+ ],
+ "isUnique": false
+ },
+ "storedContent_title_idx": {
+ "name": "storedContent_title_idx",
+ "columns": [
+ "title"
+ ],
+ "isUnique": false
+ },
+ "storedContent_user_idx": {
+ "name": "storedContent_user_idx",
+ "columns": [
+ "user"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "storedContent_user_user_id_fk": {
+ "name": "storedContent_user_user_id_fk",
+ "tableFrom": "storedContent",
+ "tableTo": "user",
+ "columnsFrom": [
+ "user"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "user": {
+ "name": "user",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "emailVerified": {
+ "name": "emailVerified",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "image": {
+ "name": "image",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "telegramId": {
+ "name": "telegramId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "users_email_idx": {
+ "name": "users_email_idx",
+ "columns": [
+ "email"
+ ],
+ "isUnique": false
+ },
+ "users_telegram_idx": {
+ "name": "users_telegram_idx",
+ "columns": [
+ "telegramId"
+ ],
+ "isUnique": false
+ },
+ "users_id_idx": {
+ "name": "users_id_idx",
+ "columns": [
+ "id"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "verificationToken": {
+ "name": "verificationToken",
+ "columns": {
+ "identifier": {
+ "name": "identifier",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "token": {
+ "name": "token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "expires": {
+ "name": "expires",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "verificationToken_identifier_token_pk": {
+ "columns": [
+ "identifier",
+ "token"
+ ],
+ "name": "verificationToken_identifier_token_pk"
+ }
+ },
+ "uniqueConstraints": {}
+ }
+ },
+ "enums": {},
+ "_meta": {
+ "schemas": {},
+ "tables": {},
+ "columns": {}
+ },
+ "internal": {
+ "indexes": {}
+ }
+} \ No newline at end of file
diff --git a/apps/web/migrations/meta/_journal.json b/apps/web/migrations/meta/_journal.json
index c7ab51e1..7af2b032 100644
--- a/apps/web/migrations/meta/_journal.json
+++ b/apps/web/migrations/meta/_journal.json
@@ -1,13 +1,20 @@
{
- "version": "6",
- "dialect": "sqlite",
- "entries": [
- {
- "idx": 0,
- "version": "6",
- "when": 1720360287793,
- "tag": "0000_exotic_sway",
- "breakpoints": true
- }
- ]
-}
+ "version": "6",
+ "dialect": "sqlite",
+ "entries": [
+ {
+ "idx": 0,
+ "version": "6",
+ "when": 1720360287793,
+ "tag": "0000_exotic_sway",
+ "breakpoints": true
+ },
+ {
+ "idx": 1,
+ "version": "6",
+ "when": 1721329604476,
+ "tag": "0001_nervous_longshot",
+ "breakpoints": true
+ }
+ ]
+} \ No newline at end of file
diff --git a/apps/web/package.json b/apps/web/package.json
index 7d97fe7c..f157ad87 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -1,53 +1,54 @@
{
- "name": "@repo/web",
- "version": "1.0.0",
- "private": true,
- "packageManager": "[email protected]",
- "scripts": {
- "dev": "next dev",
- "build": "next build",
- "start": "next start",
- "lint": "eslint . --max-warnings 0",
- "cf-typegen": "wrangler types --env-interface CloudflareEnv env.d.ts",
- "pages:build": "npx @cloudflare/next-on-pages",
- "preview": "npm run pages:build && wrangler pages dev",
- "deploy": "npm run pages:build && wrangler pages deploy --branch main",
- "schema-update": "bunx drizzle-kit generate sqlite",
- "update-local-db": "bunx wrangler d1 execute dev-d1-anycontext --local",
- "update-prod-db": "bunx wrangler d1 execute prod-d1-supermemory --remote"
- },
- "dependencies": {
- "@radix-ui/react-dialog": "^1.0.5",
- "@radix-ui/react-popover": "^1.0.7",
- "@radix-ui/react-slot": "^1.1.0",
- "@sentry/nextjs": "^8",
- "cmdk": "^1.0.0",
- "lowlight": "^3.1.0",
- "million": "^3.1.6",
- "next": "^14.1.1",
- "novel": "^0.4.2",
- "nuqs": "^1.17.4",
- "react": "^18.2.0",
- "react-dom": "^18.2.0",
- "react-resizable-panels": "^2.0.19",
- "use-debounce": "^10.0.1"
- },
- "devDependencies": {
- "@next/eslint-plugin-next": "^14.1.1",
- "@repo/eslint-config": "*",
- "@repo/typescript-config": "*",
- "@repo/tailwind-config": "*",
- "@repo/shared-types": "*",
- "@types/eslint": "^8.56.5",
- "@types/node": "^20.11.24",
- "@types/react": "^18.2.61",
- "@types/react-dom": "^18.2.19",
- "eslint": "^8.57.0",
- "typescript": "^5.3.3"
- },
- "trustedDependencies": [
- "esbuild",
- "workerd",
- "xycolors"
- ]
+ "name": "@repo/web",
+ "version": "1.0.0",
+ "private": true,
+ "packageManager": "[email protected]",
+ "scripts": {
+ "dev": "next dev",
+ "build": "next build",
+ "start": "next start",
+ "lint": "eslint . --max-warnings 0",
+ "cf-typegen": "wrangler types --env-interface CloudflareEnv env.d.ts",
+ "pages:build": "npx @cloudflare/next-on-pages",
+ "preview": "npm run pages:build && wrangler pages dev",
+ "deploy": "npm run pages:build && wrangler pages deploy --branch main",
+ "schema-update": "bunx drizzle-kit generate sqlite",
+ "update-local-db": "bunx wrangler d1 execute dev-d1-anycontext --local",
+ "update-prod-db": "bunx wrangler d1 execute prod-d1-supermemory --remote"
+ },
+ "dependencies": {
+ "@radix-ui/react-dialog": "^1.0.5",
+ "@radix-ui/react-popover": "^1.0.7",
+ "@radix-ui/react-slot": "^1.1.0",
+ "@sentry/nextjs": "^8",
+ "cmdk": "^1.0.0",
+ "lowlight": "^3.1.0",
+ "million": "^3.1.6",
+ "next": "^14.1.1",
+ "novel": "^0.4.2",
+ "nuqs": "^1.17.4",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "react-resizable-panels": "^2.0.19",
+ "react-use-measure": "^2.1.1",
+ "use-debounce": "^10.0.1"
+ },
+ "devDependencies": {
+ "@next/eslint-plugin-next": "^14.1.1",
+ "@repo/eslint-config": "*",
+ "@repo/typescript-config": "*",
+ "@repo/tailwind-config": "*",
+ "@repo/shared-types": "*",
+ "@types/eslint": "^8.56.5",
+ "@types/node": "^20.11.24",
+ "@types/react": "^18.2.61",
+ "@types/react-dom": "^18.2.19",
+ "eslint": "^8.57.0",
+ "typescript": "^5.3.3"
+ },
+ "trustedDependencies": [
+ "esbuild",
+ "workerd",
+ "xycolors"
+ ]
}
diff --git a/apps/web/public/image.png b/apps/web/public/image.png
new file mode 100644
index 00000000..515b96f4
--- /dev/null
+++ b/apps/web/public/image.png
Binary files differ
diff --git a/apps/web/public/image2.png b/apps/web/public/image2.png
new file mode 100644
index 00000000..10f57a47
--- /dev/null
+++ b/apps/web/public/image2.png
Binary files differ
diff --git a/apps/web/public/image3.png b/apps/web/public/image3.png
new file mode 100644
index 00000000..581d65b2
--- /dev/null
+++ b/apps/web/public/image3.png
Binary files differ
diff --git a/apps/web/public/img.png b/apps/web/public/img.png
new file mode 100644
index 00000000..28b983e7
--- /dev/null
+++ b/apps/web/public/img.png
Binary files differ
diff --git a/apps/web/server/db/schema.ts b/apps/web/server/db/schema.ts
index ae293a91..8d6157e9 100644
--- a/apps/web/server/db/schema.ts
+++ b/apps/web/server/db/schema.ts
@@ -193,7 +193,8 @@ export const chatThreads = createTable(
userId: text("userId")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
- },
+ createdAt: integer("createdAt").notNull().$defaultFn(() => Math.floor(Date.now() / 1000)), // Use Unix timestamp
+ },
(thread) => ({
userIdx: index("chatThread_user_idx").on(thread.userId),
}),