Revert "small commits big pr"

This commit is contained in:
CodeTorso 2024-07-20 04:47:53 +05:30 committed by GitHub
parent e47219a719
commit ae0535215a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
32 changed files with 706 additions and 2202 deletions

View file

@ -64,7 +64,7 @@ async function Signin({
action={async () => {
"use server";
await signIn("google", {
redirectTo: "/onboarding",
redirectTo: "/home?firstTime=true",
});
}}
>

View file

@ -2,7 +2,7 @@
import { AnimatePresence } from "framer-motion";
import React, { useEffect, useRef, useState } from "react";
import QueryInput from "./queryinput";
import QueryInput from "../home/queryinput";
import { cn } from "@repo/ui/lib/utils";
import { motion } from "framer-motion";
import { useRouter } from "next/navigation";

View file

@ -1,83 +0,0 @@
"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;

View file

@ -1,224 +0,0 @@
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>
);
}

View file

@ -1,50 +0,0 @@
"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>
);
}

View file

@ -1,108 +0,0 @@
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>
);
}

View file

@ -1,38 +0,0 @@
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>
);
}

View file

@ -1,50 +0,0 @@
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;

View file

@ -0,0 +1,50 @@
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.",
},
],
];

View file

@ -1,25 +1,26 @@
"use client";
import React, { useEffect, useState } from "react";
import React, { useEffect, useState } from "react";
import QueryInput from "./queryinput";
import {
getSessionAuthToken,
getSpaces,
} from "@/app/actions/fetchers";
import { getSessionAuthToken, getSpaces } from "@/app/actions/fetchers";
import { useRouter } from "next/navigation";
import { createChatThread, linkTelegramToUser } from "@/app/actions/doers";
import { toast } from "sonner";
import { Heading } from "./heading";
import History from "./history";
import { motion } from "framer-motion";
import { variants } from "./homeVariants";
import { ChromeIcon, GithubIcon, TwitterIcon } from "lucide-react";
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.");
}
const slap = {
initial: {
opacity: 0,
scale: 1.1,
},
whileInView: { opacity: 1, scale: 1 },
transition: {
duration: 0.5,
ease: "easeInOut",
},
viewport: { once: true },
};
function Page({
@ -27,24 +28,41 @@ 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 [queryPresent, setQueryPresent] = useState<boolean>(false);
const [showVariant, setShowVariant] = useState<number>(0);
useEffect(() => {
// telegram bot
const telegramUser = searchParams.extension as string;
if (telegramUser) {
linkTelegram(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();
}
if (searchParams.extension as string) {
if (extensionInstalled) {
toast.success("Extension installed successfully");
}
// fetch spaces
getSpaces().then((res) => {
if (res.success && res.data) {
setSpaces(res.data);
@ -53,18 +71,44 @@ 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 mt-[18vh] mx-auto w-full px-2 md:px-0">
<Heading queryPresent={queryPresent} />
<div className="w-full py-12">
<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">
<QueryInput
setQueryPresent={(t: boolean) => setQueryPresent(t)}
handleSubmit={async (q, spaces) => {
if (q.length === 0) {
toast.error("Query is required");
@ -83,11 +127,40 @@ function Page({
);
}}
initialSpaces={spaces}
setInitialSpaces={setSpaces}
/>
</div>
<div className="space-y-5">
<h3 className="text-lg">Recent Searches</h3>
<History />
<div className="w-full fixed bottom-0 left-0 p-4">
<div className="flex items-center justify-center gap-8">
<a
href="https://supermemory.ai/extension"
target="_blank"
rel="noreferrer"
className="flex items-center gap-2 text-muted-foreground"
>
<ChromeIcon className="w-4 h-4" />
Install extension
</a>
<a
href="https://github.com/Dhravya/supermemory/issues/new"
target="_blank"
rel="noreferrer"
className="flex items-center gap-2 text-muted-foreground"
>
<GithubIcon className="w-4 h-4" />
Bug report
</a>
<a
href="https://x.com/supermemory.ai"
target="_blank"
rel="noreferrer"
className="flex items-center gap-2 text-muted-foreground"
>
<TwitterIcon className="w-4 h-4" />
Twitter
</a>
</div>
</div>
</div>
);

View file

@ -1,48 +1,83 @@
"use client";
import React, { useState } from "react";
import { FilterSpaces } from "./filterSpaces";
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";
function QueryInput({
setQueryPresent,
initialSpaces,
initialQuery = "",
initialSpaces = [],
disabled = false,
className,
mini = false,
handleSubmit,
setInitialSpaces,
}: {
setQueryPresent: (t: boolean) => void;
initialQuery?: string;
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("");
const [q, setQ] = useState(initialQuery);
const [selectedSpaces, setSelectedSpaces] = useState<
{ id: number; name: string }[]
>([]);
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],
);
return (
<div className={`w-full`}>
<div className={`${className}`}>
<div
className={`bg-[#1F2428] overflow-hidden border-2 border-gray-700/50 shadow-md shadow-[#1d1d1dc7] rounded-3xl`}
className={`bg-secondary border-2 border-b-0 border-border ${!mini ? "rounded-t-3xl" : "rounded-3xl"}`}
>
{/* input and action button */}
<form
action={async () => {
if (q.trim().length === 0) {
return;
}
handleSubmit(q, selectedSpaces);
handleSubmit(q, preparedSpaces);
setQ("");
}}
className="flex gap-4 p-3"
>
<textarea
autoFocus
name="q"
cols={30}
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"
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"
placeholder="Ask your second brain..."
onKeyDown={(e) => {
if (e.key === "Enter" && !e.shiftKey) {
@ -50,25 +85,95 @@ function QueryInput({
if (q.trim().length === 0) {
return;
}
handleSubmit(q, selectedSpaces);
handleSubmit(q, preparedSpaces);
setQ("");
}
}}
onChange={(e) =>
setQ((prev) => {
setQueryPresent(!!e.target.value.length);
return e.target.value;
})
}
onChange={(e) => setQ(e.target.value)}
value={q}
disabled={disabled}
/>
<FilterSpaces
selectedSpaces={selectedSpaces}
setSelectedSpaces={setSelectedSpaces}
initialSpaces={initialSpaces || []}
/>
<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>
</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>
);
}

View file

@ -3,6 +3,7 @@ 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();
@ -12,15 +13,18 @@ async function Layout({ children }: { children: React.ReactNode }) {
}
return (
<main className="h-screen bg flex flex-col">
<main className="h-screen 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-[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]"
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]"
style={{ transform: "rotate(-30deg)" }}
/>
</div>
<BackgroundPlus className="absolute top-0 left-0 w-full h-full -z-50 opacity-70" />
<Menu />

View file

@ -1,174 +1,359 @@
import React from "react";
"use client";
import React, { useEffect, useMemo, useState } from "react";
import Image from "next/image";
import Link from "next/link";
import { MemoriesIcon, CanvasIcon, AddIcon } from "@repo/ui/icons";
import { DialogTrigger } from "@repo/ui/shadcn/dialog";
import { HomeIcon } from "@heroicons/react/24/solid";
import { MemoriesIcon, ExploreIcon, CanvasIcon, AddIcon } from "@repo/ui/icons";
import { Button } from "@repo/ui/shadcn/button";
import { MinusIcon, PlusCircleIcon } from "lucide-react";
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,
},
];
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 { 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";
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 */}
<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} />
<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>
</div>
</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"}`}
<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 "
>
<HomeIcon width={24} height={24} />
<p className="text-xs text-foreground-menu mt-2">Home</p>
</Link>
<DialogHeader>
<DialogTitle>Add memory</DialogTitle>
<DialogDescription className="text-[#F2F3F5]">
A "Memory" is a bookmark, something you want to remember.
</DialogDescription>
</DialogHeader>
<DialogMobileTrigger />
{menuItems.map((item) => (
<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>
{/* 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
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"
}`}
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`}
>
<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>
</>
);
}
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;

View file

@ -1,12 +0,0 @@
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;

View file

@ -1,388 +0,0 @@
"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" />;
}
};

View file

@ -1,6 +1,6 @@
"use server";
import { and, asc, desc, eq, exists, inArray, not, or, sql } from "drizzle-orm";
import { and, asc, eq, exists, inArray, not, or, sql } from "drizzle-orm";
import { db } from "../../server/db";
import {
canvas,
@ -228,34 +228,6 @@ 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[]
> => {

View file

@ -1,13 +1,13 @@
import "@repo/tailwind-config/globals.css";
import type { Metadata } from "next";
import { Poppins } from "next/font/google";
import { Inter } 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 poppins = Poppins({ subsets: ["latin"], weight: ["400", "500", "600", "700"] });
const inter = Inter({ subsets: ["latin"] });
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(
`${poppins.className} dark`,
`${inter.className} dark`,
GeistMono.variable,
GeistSans.variable,
)}

View file

@ -1,2 +0,0 @@
ALTER TABLE `chatThread` ADD `createdAt` integer;
UPDATE `chatThread` SET `createdAt` = strftime('%s', 'now') WHERE `createdAt` IS NULL;

View file

@ -1,905 +0,0 @@
{
"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": {}
}
}

View file

@ -1,20 +1,13 @@
{
"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
}
]
}
"version": "6",
"dialect": "sqlite",
"entries": [
{
"idx": 0,
"version": "6",
"when": 1720360287793,
"tag": "0000_exotic_sway",
"breakpoints": true
}
]
}

View file

@ -1,54 +1,53 @@
{
"name": "@repo/web",
"version": "1.0.0",
"private": true,
"packageManager": "yarn@1.22.21",
"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"
]
"name": "@repo/web",
"version": "1.0.0",
"private": true,
"packageManager": "yarn@1.22.21",
"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"
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 161 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

View file

@ -193,8 +193,7 @@ 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),
}),

BIN
bun.lockb

Binary file not shown.

View file

@ -234,10 +234,3 @@ body {
color: #369dfd;
background: #21303d;
}
.bg {
background-color: #171B1F;
opacity: 1;
background: radial-gradient(circle, transparent 20%, #171B1F 20%, #171B1F 80%, transparent 80%, transparent), radial-gradient(circle, transparent 20%, #171B1F 20%, #171B1F 80%, transparent 80%, transparent) 100px 100px, linear-gradient(#1a1f23 8px, transparent 8px) 0 -4px, linear-gradient(90deg, #1a1f23 8px, #171B1F 8px) -4px 0;
background-size: 100px 100px, 100px 100px, 100px 100px, 100px 100px;
}

View file

@ -1,3 +1 @@
<svg width="18" height="14" viewBox="0 0 18 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.25 13.25L16.5 7M16.5 7L10.25 0.75M16.5 7H1.5" stroke="#369DFD" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" viewBox="0 0 20 20"><path stroke="#989EA4" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M11.25 16.25L17.5 10M17.5 10L11.25 3.75M17.5 10H2.5"/></svg>

Before

Width:  |  Height:  |  Size: 247 B

After

Width:  |  Height:  |  Size: 247 B

View file

@ -37,8 +37,9 @@ const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
const CommandInput = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Input>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
>(({ className , ...props }, ref) => (
<div className="" cmdk-input-wrapper="">
>(({ className, ...props }, ref) => (
<div className="flex items-center px-3" cmdk-input-wrapper="">
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
<CommandPrimitive.Input
ref={ref}
className={cn(

View file

@ -0,0 +1,7 @@
import { cn } from "@repo/ui/lib/utils";
function Divider({ className }: { className?: string }) {
return <div className={cn("bg-[#2D343A] h-[1px] w-full", className)}></div>;
}
export default Divider;

View file

@ -1,15 +0,0 @@
import { cn } from "../lib/utils"
function Skeleton({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn("animate-pulse rounded-md bg-muted", className)}
{...props}
/>
)
}
export { Skeleton }