for god's sake this should work ;)

This commit is contained in:
codetorso 2024-07-20 07:17:25 +05:30
parent c511f451bb
commit 21fe55a96c
30 changed files with 2313 additions and 773 deletions

View file

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

View file

@ -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>

View file

@ -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;

View file

@ -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>
);
}

View file

@ -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>
);
}

View file

@ -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>
);
}

View file

@ -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>
);
}

View file

@ -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;

View file

@ -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.",
},
],
];

View file

@ -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"

View file

@ -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>
);
}

View file

@ -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 />

View file

@ -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>
</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 "
{/* 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>
<HomeIcon width={24} height={24} />
<p className="text-xs text-foreground-menu mt-2">Home</p>
</Link>
<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">
<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;

View file

@ -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;

View file

@ -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" />;
}
};

View file

@ -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[]
> => {

View file

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

View file

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

View file

@ -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": {}
}
}

View file

@ -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
}
]
}

View file

@ -1,53 +1,54 @@
{
"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"
]
"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"
]
}

BIN
apps/web/public/image.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

BIN
apps/web/public/image2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

BIN
apps/web/public/image3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
apps/web/public/img.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

View file

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

View file

@ -234,3 +234,10 @@ 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

@ -37,9 +37,8 @@ const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
const CommandInput = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Input>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
>(({ 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" />
>(({ className , ...props }, ref) => (
<div className="" cmdk-input-wrapper="">
<CommandPrimitive.Input
ref={ref}
className={cn(

View file

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

@ -0,0 +1,15 @@
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 }