aboutsummaryrefslogtreecommitdiff
path: root/apps/web/app/(dash)
diff options
context:
space:
mode:
authorSamrat Malisetti <[email protected]>2024-07-19 11:19:50 -0700
committerGitHub <[email protected]>2024-07-19 11:19:50 -0700
commit581ce2780ea8b8b931539adcbddacf1d322fd027 (patch)
tree4813158cd7aa3abb44c7e3884cfd4415d8b5148c /apps/web/app/(dash)
parentfixed mbile layout (diff)
parentDisabled sentry for now because of unreasonably large bundle size (diff)
downloadsupermemory-581ce2780ea8b8b931539adcbddacf1d322fd027.tar.xz
supermemory-581ce2780ea8b8b931539adcbddacf1d322fd027.zip
Merge branch 'main' into fix/mobile-layout
Diffstat (limited to 'apps/web/app/(dash)')
-rw-r--r--apps/web/app/(dash)/(memories)/content.tsx673
-rw-r--r--apps/web/app/(dash)/(memories)/memories/page.tsx6
-rw-r--r--apps/web/app/(dash)/(memories)/space/[spaceid]/page.tsx28
-rw-r--r--apps/web/app/(dash)/chat/CodeBlock.tsx158
-rw-r--r--apps/web/app/(dash)/chat/[chatid]/page.tsx48
-rw-r--r--apps/web/app/(dash)/chat/chatWindow.tsx774
-rw-r--r--apps/web/app/(dash)/chat/markdownRenderHelpers.tsx30
-rw-r--r--apps/web/app/(dash)/header/autoBreadCrumbs.tsx68
-rw-r--r--apps/web/app/(dash)/header/header.tsx96
-rw-r--r--apps/web/app/(dash)/header/newChatButton.tsx26
-rw-r--r--apps/web/app/(dash)/home/homeVariants.ts96
-rw-r--r--apps/web/app/(dash)/home/page.tsx232
-rw-r--r--apps/web/app/(dash)/home/queryinput.tsx308
-rw-r--r--apps/web/app/(dash)/layout.tsx42
-rw-r--r--apps/web/app/(dash)/menu.tsx616
-rw-r--r--apps/web/app/(dash)/note/[noteid]/page.tsx30
16 files changed, 1615 insertions, 1616 deletions
diff --git a/apps/web/app/(dash)/(memories)/content.tsx b/apps/web/app/(dash)/(memories)/content.tsx
index 879c0502..fea4477a 100644
--- a/apps/web/app/(dash)/(memories)/content.tsx
+++ b/apps/web/app/(dash)/(memories)/content.tsx
@@ -1,15 +1,14 @@
"use client";
-import { getAllUserMemoriesAndSpaces } from "@/app/actions/fetchers";
import { Content, StoredSpace } from "@/server/db/schema";
import { MemoriesIcon, NextIcon, SearchIcon, UrlIcon } from "@repo/ui/icons";
import {
- ArrowLeftIcon,
- MenuIcon,
- MoveIcon,
- NotebookIcon,
- PaperclipIcon,
- TrashIcon,
+ ArrowLeftIcon,
+ MenuIcon,
+ MoveIcon,
+ NotebookIcon,
+ PaperclipIcon,
+ TrashIcon,
} from "lucide-react";
import Image from "next/image";
import Link from "next/link";
@@ -18,16 +17,16 @@ import Masonry from "react-layout-masonry";
import { getRawTweet } from "@repo/shared-types/utils";
import { MyTweet } from "../../../components/twitter/render-tweet";
import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuLabel,
- DropdownMenuPortal,
- DropdownMenuSeparator,
- DropdownMenuSub,
- DropdownMenuSubContent,
- DropdownMenuSubTrigger,
- DropdownMenuTrigger,
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuLabel,
+ DropdownMenuPortal,
+ DropdownMenuSeparator,
+ DropdownMenuSub,
+ DropdownMenuSubContent,
+ DropdownMenuSubTrigger,
+ DropdownMenuTrigger,
} from "@repo/ui/shadcn/dropdown-menu";
import { Button } from "@repo/ui/shadcn/button";
import { addUserToSpace, deleteItem, moveItem } from "@/app/actions/doers";
@@ -36,360 +35,360 @@ import { Input } from "@repo/ui/shadcn/input";
import { motion } from "framer-motion";
export function MemoriesPage({
- memoriesAndSpaces,
- title = "Your Memories",
- currentSpace,
- usersWithAccess,
+ memoriesAndSpaces,
+ title = "Your Memories",
+ currentSpace,
+ usersWithAccess,
}: {
- memoriesAndSpaces: { memories: Content[]; spaces: StoredSpace[] };
- title?: string;
- currentSpace?: StoredSpace;
- usersWithAccess?: string[];
+ memoriesAndSpaces: { memories: Content[]; spaces: StoredSpace[] };
+ title?: string;
+ currentSpace?: StoredSpace;
+ usersWithAccess?: string[];
}) {
- const [filter, setFilter] = useState("All");
+ const [filter, setFilter] = useState("All");
- // Sort Both memories and spaces by their savedAt and createdAt dates respectfully.
- // The output should be just one single list of items
- // And it will look something like { item: "memory" | "space", date: Date, data: Content | StoredSpace }
- const sortedItems = useMemo(() => {
- // Merge the lists
- const unifiedItems = [
- ...memoriesAndSpaces.memories.map((memory) => ({
- item: "memory",
- date: new Date(memory.savedAt), // Assuming savedAt is a string date
- data: memory,
- })),
- ...memoriesAndSpaces.spaces.map((space) => ({
- item: "space",
- date: new Date(space.createdAt), // Assuming createdAt is a string date
- data: space,
- })),
- ].map((item) => ({
- ...item,
- date: Number(item.date), // Convert the date to a number
- }));
+ // Sort Both memories and spaces by their savedAt and createdAt dates respectfully.
+ // The output should be just one single list of items
+ // And it will look something like { item: "memory" | "space", date: Date, data: Content | StoredSpace }
+ const sortedItems = useMemo(() => {
+ // Merge the lists
+ const unifiedItems = [
+ ...memoriesAndSpaces.memories.map((memory) => ({
+ item: "memory",
+ date: new Date(memory.savedAt), // Assuming savedAt is a string date
+ data: memory,
+ })),
+ ...memoriesAndSpaces.spaces.map((space) => ({
+ item: "space",
+ date: new Date(space.createdAt), // Assuming createdAt is a string date
+ data: space,
+ })),
+ ].map((item) => ({
+ ...item,
+ date: Number(item.date), // Convert the date to a number
+ }));
- // Sort the merged list
- return unifiedItems
- .filter((item) => {
- if (filter === "All") return true;
- if (filter === "Spaces" && item.item === "space") {
- return true;
- }
- if (filter === "Pages")
- return (
- item.item === "memory" && (item.data as Content).type === "page"
- );
- if (filter === "Notes")
- return (
- item.item === "memory" && (item.data as Content).type === "note"
- );
- if (filter === "Tweet")
- return (
- item.item === "memory" && (item.data as Content).type === "tweet"
- );
- return false;
- })
- .sort((a, b) => b.date - a.date);
- }, [memoriesAndSpaces.memories, memoriesAndSpaces.spaces, filter]);
+ // Sort the merged list
+ return unifiedItems
+ .filter((item) => {
+ if (filter === "All") return true;
+ if (filter === "Spaces" && item.item === "space") {
+ return true;
+ }
+ if (filter === "Pages")
+ return (
+ item.item === "memory" && (item.data as Content).type === "page"
+ );
+ if (filter === "Notes")
+ return (
+ item.item === "memory" && (item.data as Content).type === "note"
+ );
+ if (filter === "Tweet")
+ return (
+ item.item === "memory" && (item.data as Content).type === "tweet"
+ );
+ return false;
+ })
+ .sort((a, b) => b.date - a.date);
+ }, [memoriesAndSpaces.memories, memoriesAndSpaces.spaces, filter]);
- return (
- <div
- key={`${memoriesAndSpaces.memories.length + memoriesAndSpaces.spaces.length}`}
- className="px-2 md:px-32 py-36 h-full flex mx-auto w-full flex-col gap-6"
- >
- {currentSpace && (
- <Link href={"/memories"} className="flex gap-2 items-center">
- <ArrowLeftIcon className="w-3 h-3" /> Back to all memories
- </Link>
- )}
+ return (
+ <div
+ key={`${memoriesAndSpaces.memories.length + memoriesAndSpaces.spaces.length}`}
+ className="px-2 md:px-32 py-36 h-full flex mx-auto w-full flex-col gap-6"
+ >
+ {currentSpace && (
+ <Link href={"/memories"} className="flex gap-2 items-center">
+ <ArrowLeftIcon className="w-3 h-3" /> Back to all memories
+ </Link>
+ )}
- <h2 className="text-white w-full text-3xl text-left font-semibold">
- {title}
- </h2>
- {currentSpace && (
- <div className="flex flex-col gap-2">
- <div className="flex gap-4 items-center">
- Space
- <div className="flex items-center gap-2 bg-secondary p-2 rounded-xl">
- <Image src={MemoriesIcon} alt="Spaces icon" className="w-3 h-3" />
- <span className="text-[#fff]">{currentSpace.name}</span>
- </div>
- </div>
+ <h2 className="text-white w-full text-3xl text-left font-semibold">
+ {title}
+ </h2>
+ {currentSpace && (
+ <div className="flex flex-col gap-2">
+ <div className="flex gap-4 items-center">
+ Space
+ <div className="flex items-center gap-2 bg-secondary p-2 rounded-xl">
+ <Image src={MemoriesIcon} alt="Spaces icon" className="w-3 h-3" />
+ <span className="text-[#fff]">{currentSpace.name}</span>
+ </div>
+ </div>
- {usersWithAccess && usersWithAccess.length > 0 && (
- <div className="flex gap-4 items-center">
- Users with access
- <div className="flex gap-2">
- {usersWithAccess.map((user) => (
- <div className="flex items-center gap-2 bg-secondary p-2 rounded-xl">
- <Image
- src={UrlIcon}
- alt="Spaces icon"
- className="w-3 h-3"
- />
- <span className="text-[#fff]">{user}</span>
- </div>
- ))}
- </div>
- </div>
- )}
+ {usersWithAccess && usersWithAccess.length > 0 && (
+ <div className="flex gap-4 items-center">
+ Users with access
+ <div className="flex gap-2">
+ {usersWithAccess.map((user) => (
+ <div className="flex items-center gap-2 bg-secondary p-2 rounded-xl">
+ <Image
+ src={UrlIcon}
+ alt="Spaces icon"
+ className="w-3 h-3"
+ />
+ <span className="text-[#fff]">{user}</span>
+ </div>
+ ))}
+ </div>
+ </div>
+ )}
- <form
- action={async (e: FormData) => {
- const email = e.get("email")?.toString();
+ <form
+ action={async (e: FormData) => {
+ const email = e.get("email")?.toString();
- if (!email) {
- toast.error("Please enter an email");
- return;
- }
+ if (!email) {
+ toast.error("Please enter an email");
+ return;
+ }
- const resp = await addUserToSpace(email, currentSpace.id);
+ const resp = await addUserToSpace(email, currentSpace.id);
- if (resp.success) {
- toast.success("User added to space");
- } else {
- toast.error("Failed to add user to space");
- }
- }}
- className="flex gap-2 max-w-xl mt-2"
- >
- <Input name="email" placeholder="Add user by email" />
- <Button variant="secondary">Add</Button>
- </form>
- </div>
- )}
+ if (resp.success) {
+ toast.success("User added to space");
+ } else {
+ toast.error("Failed to add user to space");
+ }
+ }}
+ className="flex gap-2 max-w-xl mt-2"
+ >
+ <Input name="email" placeholder="Add user by email" />
+ <Button variant="secondary">Add</Button>
+ </form>
+ </div>
+ )}
- <Filters
- setFilter={setFilter}
- filter={filter}
- filterMethods={
- currentSpace ? SpaceFilterMethods : MemoriesFilterMethods
- }
- />
+ <Filters
+ setFilter={setFilter}
+ filter={filter}
+ filterMethods={
+ currentSpace ? SpaceFilterMethods : MemoriesFilterMethods
+ }
+ />
- <Masonry
- className="mt-6 relative"
- columns={{ 640: 1, 768: 2, 1024: 3 }}
- gap={16}
- columnProps={{
- className: "min-w-[calc(33.3333%-16px)] w-full",
- }}
- >
- {sortedItems.map((item) => {
- if (item.item === "memory") {
- return (
- <LinkComponent
- type={(item.data as Content).type ?? "note"}
- content={(item.data as Content).content}
- title={(item.data as Content).title ?? "Untitled"}
- url={
- (item.data as Content).baseUrl ?? (item.data as Content).url
- }
- image={
- (item.data as Content).ogImage ??
- (item.data as Content).image ??
- "/placeholder-image.svg" // TODO: add this placeholder
- }
- description={(item.data as Content).description ?? ""}
- spaces={memoriesAndSpaces.spaces}
- id={(item.data as Content).id}
- />
- );
- }
+ <Masonry
+ className="mt-6 relative"
+ columns={{ 640: 1, 768: 2, 1024: 3 }}
+ gap={16}
+ columnProps={{
+ className: "min-w-[calc(33.3333%-16px)] w-full",
+ }}
+ >
+ {sortedItems.map((item) => {
+ if (item.item === "memory") {
+ return (
+ <LinkComponent
+ type={(item.data as Content).type ?? "note"}
+ content={(item.data as Content).content}
+ title={(item.data as Content).title ?? "Untitled"}
+ url={
+ (item.data as Content).baseUrl ?? (item.data as Content).url
+ }
+ image={
+ (item.data as Content).ogImage ??
+ (item.data as Content).image ??
+ "/placeholder-image.svg" // TODO: add this placeholder
+ }
+ description={(item.data as Content).description ?? ""}
+ spaces={memoriesAndSpaces.spaces}
+ id={(item.data as Content).id}
+ />
+ );
+ }
- if (item.item === "space") {
- return (
- <TabComponent
- title={(item.data as StoredSpace).name}
- description={`${(item.data as StoredSpace).numItems} memories`}
- id={(item.data as StoredSpace).id}
- />
- );
- }
+ if (item.item === "space") {
+ return (
+ <TabComponent
+ title={(item.data as StoredSpace).name}
+ description={`${(item.data as StoredSpace).numItems} memories`}
+ id={(item.data as StoredSpace).id}
+ />
+ );
+ }
- return null;
- })}
- </Masonry>
- </div>
- );
+ return null;
+ })}
+ </Masonry>
+ </div>
+ );
}
function TabComponent({
- title,
- description,
- id,
+ title,
+ description,
+ id,
}: {
- title: string;
- description: string;
- id: number;
+ title: string;
+ description: string;
+ id: number;
}) {
- return (
- <Link
- href={`/space/${id}`}
- className="flex flex-col gap-4 bg-[#161f2a]/30 backdrop-blur-md border-2 border-border w-full rounded-xl p-4"
- >
- <div className="flex items-center gap-2 text-xs">
- <Image alt="Spaces icon" src={MemoriesIcon} className="size-3" /> Space
- </div>
- <div className="flex items-center">
- <div>
- <div className="h-12 w-12 flex justify-center items-center rounded-md">
- {title.slice(0, 2).toUpperCase()} {id}
- </div>
- </div>
- <div className="grow px-4">
- <div className="text-lg text-[#fff] line-clamp-2">{title}</div>
- <div>{description}</div>
- </div>
- <div>
- <Image src={NextIcon} alt="Search icon" />
- </div>
- </div>
- </Link>
- );
+ return (
+ <Link
+ href={`/space/${id}`}
+ className="flex flex-col gap-4 bg-[#161f2a]/30 backdrop-blur-md border-2 border-border w-full rounded-xl p-4"
+ >
+ <div className="flex items-center gap-2 text-xs">
+ <Image alt="Spaces icon" src={MemoriesIcon} className="size-3" /> Space
+ </div>
+ <div className="flex items-center">
+ <div>
+ <div className="h-12 w-12 flex justify-center items-center rounded-md">
+ {title.slice(0, 2).toUpperCase()} {id}
+ </div>
+ </div>
+ <div className="grow px-4">
+ <div className="text-lg text-[#fff] line-clamp-2">{title}</div>
+ <div>{description}</div>
+ </div>
+ <div>
+ <Image src={NextIcon} alt="Search icon" />
+ </div>
+ </div>
+ </Link>
+ );
}
function LinkComponent({
- type,
- content,
- title,
- url,
- image,
- description,
- spaces,
- id,
+ type,
+ content,
+ title,
+ url,
+ image,
+ description,
+ spaces,
+ id,
}: {
- type: string;
- content: string;
- title: string;
- url: string;
- image?: string;
- description: string;
- spaces: StoredSpace[];
- id: number;
+ type: string;
+ content: string;
+ title: string;
+ url: string;
+ image?: string;
+ description: string;
+ spaces: StoredSpace[];
+ id: number;
}) {
- return (
- <motion.div
- initial={{ opacity: 0 }}
- animate={{ opacity: 1 }}
- className={`bg-secondary group relative border-2 border-border rounded-xl ${type === "tweet" ? "" : "p-4"} hover:scale-105 transition duration-200`}
- >
- <Link
- href={url.replace("https://supermemory.ai", "").split("#")[0] ?? "/"}
- target="_blank"
- >
- {type === "page" ? (
- <>
- <div className="flex items-center gap-2 text-xs">
- <PaperclipIcon className="w-3 h-3" /> Page
- </div>
- {/* remove `<---chunkId: ${vector.id}\n${content}\n---->` pattern from title */}
- <div className="text-lg text-[#fff] mt-4 line-clamp-2">
- {title.replace(/(<---chunkId: .*?\n.*?\n---->)/g, "")}
- </div>
- <div>
- {url.replace("https://supermemory.ai", "").split("#")[0] ?? "/"}
- </div>
- </>
- ) : type === "note" ? (
- <>
- <div className="flex items-center gap-2 text-xs">
- <NotebookIcon className="w-3 h-3" /> Note
- </div>
- <div className="text-lg text-[#fff] mt-4 line-clamp-2">
- {title.replace(/(<---chunkId: .*?\n.*?\n---->)/g, "")}
- </div>
- <div className="line-clamp-3 mt-2">
- {content.replace(title, "")}
- </div>
- </>
- ) : type === "tweet" ? (
- <MyTweet tweet={JSON.parse(getRawTweet(content) ?? "{}")} />
- ) : null}
- </Link>
- <DropdownMenu modal={false}>
- <DropdownMenuTrigger className="top-5 right-5 absolute opacity-0 group-focus:opacity-100 group-hover:opacity-100 transition duration-200">
- <MenuIcon />
- </DropdownMenuTrigger>
- <DropdownMenuContent>
- {spaces.length > 0 && (
- <DropdownMenuSub>
- <DropdownMenuSubTrigger>
- <MoveIcon className="mr-2 h-4 w-4" />
- <span>Add to space</span>
- </DropdownMenuSubTrigger>
- <DropdownMenuPortal>
- <DropdownMenuSubContent>
- {spaces.map((space) => (
- <DropdownMenuItem>
- <button
- className="w-full h-full"
- onClick={async () => {
- toast.info("Adding to space...");
+ return (
+ <motion.div
+ initial={{ opacity: 0 }}
+ animate={{ opacity: 1 }}
+ className={`bg-secondary group relative border-2 border-border rounded-xl ${type === "tweet" ? "" : "p-4"} hover:scale-105 transition duration-200`}
+ >
+ <Link
+ href={url.replace("https://supermemory.ai", "").split("#")[0] ?? "/"}
+ target="_blank"
+ >
+ {type === "page" ? (
+ <>
+ <div className="flex items-center gap-2 text-xs">
+ <PaperclipIcon className="w-3 h-3" /> Page
+ </div>
+ {/* remove `<---chunkId: ${vector.id}\n${content}\n---->` pattern from title */}
+ <div className="text-lg text-[#fff] mt-4 line-clamp-2">
+ {title.replace(/(<---chunkId: .*?\n.*?\n---->)/g, "")}
+ </div>
+ <div>
+ {url.replace("https://supermemory.ai", "").split("#")[0] ?? "/"}
+ </div>
+ </>
+ ) : type === "note" ? (
+ <>
+ <div className="flex items-center gap-2 text-xs">
+ <NotebookIcon className="w-3 h-3" /> Note
+ </div>
+ <div className="text-lg text-[#fff] mt-4 line-clamp-2">
+ {title.replace(/(<---chunkId: .*?\n.*?\n---->)/g, "")}
+ </div>
+ <div className="line-clamp-3 mt-2">
+ {content.replace(title, "")}
+ </div>
+ </>
+ ) : type === "tweet" ? (
+ <MyTweet tweet={JSON.parse(getRawTweet(content) ?? "{}")} />
+ ) : null}
+ </Link>
+ <DropdownMenu modal={false}>
+ <DropdownMenuTrigger className="top-5 right-5 absolute opacity-0 group-focus:opacity-100 group-hover:opacity-100 transition duration-200">
+ <MenuIcon />
+ </DropdownMenuTrigger>
+ <DropdownMenuContent>
+ {spaces.length > 0 && (
+ <DropdownMenuSub>
+ <DropdownMenuSubTrigger>
+ <MoveIcon className="mr-2 h-4 w-4" />
+ <span>Add to space</span>
+ </DropdownMenuSubTrigger>
+ <DropdownMenuPortal>
+ <DropdownMenuSubContent>
+ {spaces.map((space) => (
+ <DropdownMenuItem>
+ <button
+ className="w-full h-full"
+ onClick={async () => {
+ toast.info("Adding to space...");
- const response = await moveItem(id, [space.id]);
+ const response = await moveItem(id, [space.id]);
- if (response.success) {
- toast.success("Moved to space");
- console.log("Moved to space");
- } else {
- toast.error("Failed to move to space");
- console.error("Failed to move to space");
- }
- }}
- >
- {space.name}
- </button>
- </DropdownMenuItem>
- ))}
- </DropdownMenuSubContent>
- </DropdownMenuPortal>
- </DropdownMenuSub>
- )}
- <DropdownMenuItem asChild>
- <Button
- onClick={async () => {
- await deleteItem(id);
- }}
- variant="destructive"
- className="w-full"
- >
- <TrashIcon className="mr-2 h-4 w-4" />
- Delete
- </Button>
- </DropdownMenuItem>
- </DropdownMenuContent>
- </DropdownMenu>
- </motion.div>
- );
+ if (response.success) {
+ toast.success("Moved to space");
+ console.log("Moved to space");
+ } else {
+ toast.error("Failed to move to space");
+ console.error("Failed to move to space");
+ }
+ }}
+ >
+ {space.name}
+ </button>
+ </DropdownMenuItem>
+ ))}
+ </DropdownMenuSubContent>
+ </DropdownMenuPortal>
+ </DropdownMenuSub>
+ )}
+ <DropdownMenuItem asChild>
+ <Button
+ onClick={async () => {
+ await deleteItem(id);
+ }}
+ variant="destructive"
+ className="w-full"
+ >
+ <TrashIcon className="mr-2 h-4 w-4" />
+ Delete
+ </Button>
+ </DropdownMenuItem>
+ </DropdownMenuContent>
+ </DropdownMenu>
+ </motion.div>
+ );
}
const MemoriesFilterMethods = ["All", "Spaces", "Pages", "Notes", "Tweet"];
const SpaceFilterMethods = ["All", "Pages", "Notes", "Tweet"];
function Filters({
- setFilter,
- filter,
- filterMethods,
+ setFilter,
+ filter,
+ filterMethods,
}: {
- setFilter: (i: string) => void;
- filter: string;
- filterMethods: string[];
+ setFilter: (i: string) => void;
+ filter: string;
+ filterMethods: string[];
}) {
- return (
- <div className="flex gap-4 flex-wrap">
- {filterMethods.map((i) => {
- return (
- <button
- onClick={() => setFilter(i)}
- className={`transition px-6 py-2 rounded-xl bg-border ${i === filter ? " text-[#369DFD]" : "text-[#B3BCC5] bg-secondary hover:bg-secondary hover:text-[#76a3cc]"}`}
- >
- {i}
- </button>
- );
- })}
- </div>
- );
+ return (
+ <div className="flex gap-4 flex-wrap">
+ {filterMethods.map((i) => {
+ return (
+ <button
+ onClick={() => setFilter(i)}
+ className={`transition px-6 py-2 rounded-xl bg-border ${i === filter ? " text-[#369DFD]" : "text-[#B3BCC5] bg-secondary hover:bg-secondary hover:text-[#76a3cc]"}`}
+ >
+ {i}
+ </button>
+ );
+ })}
+ </div>
+ );
}
export default MemoriesPage;
diff --git a/apps/web/app/(dash)/(memories)/memories/page.tsx b/apps/web/app/(dash)/(memories)/memories/page.tsx
index d1aa999a..d0f555f2 100644
--- a/apps/web/app/(dash)/(memories)/memories/page.tsx
+++ b/apps/web/app/(dash)/(memories)/memories/page.tsx
@@ -3,9 +3,9 @@ import { redirect } from "next/navigation";
import MemoriesPage from "../content";
async function Page() {
- const { success, data } = await getAllUserMemoriesAndSpaces();
- if (!success ?? !data) return redirect("/home");
- return <MemoriesPage memoriesAndSpaces={data} />;
+ const { success, data } = await getAllUserMemoriesAndSpaces();
+ if (!success ?? !data) return redirect("/home");
+ return <MemoriesPage memoriesAndSpaces={data} />;
}
export default Page;
diff --git a/apps/web/app/(dash)/(memories)/space/[spaceid]/page.tsx b/apps/web/app/(dash)/(memories)/space/[spaceid]/page.tsx
index 0bf33896..ed1ea1cc 100644
--- a/apps/web/app/(dash)/(memories)/space/[spaceid]/page.tsx
+++ b/apps/web/app/(dash)/(memories)/space/[spaceid]/page.tsx
@@ -7,23 +7,23 @@ import { spacesAccess } from "@/server/db/schema";
import { auth } from "@/server/auth";
async function Page({ params: { spaceid } }: { params: { spaceid: number } }) {
- const user = await auth();
+ const user = await auth();
- const { success, data } = await getMemoriesInsideSpace(spaceid);
- if (!success ?? !data) return redirect("/home");
+ const { success, data } = await getMemoriesInsideSpace(spaceid);
+ if (!success ?? !data) return redirect("/home");
- const hasAccess = await db.query.spacesAccess.findMany({
- where: and(eq(spacesAccess.spaceId, spaceid)),
- });
+ const hasAccess = await db.query.spacesAccess.findMany({
+ where: and(eq(spacesAccess.spaceId, spaceid)),
+ });
- return (
- <MemoriesPage
- memoriesAndSpaces={{ memories: data.memories, spaces: [] }}
- title={data.spaces[0]?.name}
- currentSpace={data.spaces[0]}
- usersWithAccess={hasAccess.map((x) => x.userEmail) ?? []}
- />
- );
+ return (
+ <MemoriesPage
+ memoriesAndSpaces={{ memories: data.memories, spaces: [] }}
+ title={data.spaces[0]?.name}
+ currentSpace={data.spaces[0]}
+ usersWithAccess={hasAccess.map((x) => x.userEmail) ?? []}
+ />
+ );
}
export default Page;
diff --git a/apps/web/app/(dash)/chat/CodeBlock.tsx b/apps/web/app/(dash)/chat/CodeBlock.tsx
index 0bb6a19d..22b2570a 100644
--- a/apps/web/app/(dash)/chat/CodeBlock.tsx
+++ b/apps/web/app/(dash)/chat/CodeBlock.tsx
@@ -1,90 +1,90 @@
import React, { useRef, useState } from "react";
const CodeBlock = ({
- lang,
- codeChildren,
+ lang,
+ codeChildren,
}: {
- lang: string;
- codeChildren: React.ReactNode & React.ReactNode[];
+ lang: string;
+ codeChildren: React.ReactNode & React.ReactNode[];
}) => {
- const codeRef = useRef<HTMLElement>(null);
+ const codeRef = useRef<HTMLElement>(null);
- return (
- <div className="bg-black rounded-md">
- <CodeBar lang={lang} codeRef={codeRef} />
- <div className="p-4 overflow-y-auto">
- <code ref={codeRef} className={`!whitespace-pre hljs language-${lang}`}>
- {codeChildren}
- </code>
- </div>
- </div>
- );
+ return (
+ <div className="bg-black rounded-md">
+ <CodeBar lang={lang} codeRef={codeRef} />
+ <div className="p-4 overflow-y-auto">
+ <code ref={codeRef} className={`!whitespace-pre hljs language-${lang}`}>
+ {codeChildren}
+ </code>
+ </div>
+ </div>
+ );
};
const CodeBar = React.memo(
- ({
- lang,
- codeRef,
- }: {
- lang: string;
- codeRef: React.RefObject<HTMLElement>;
- }) => {
- const [isCopied, setIsCopied] = useState<boolean>(false);
- return (
- <div className="flex items-center relative text-gray-200 bg-gray-800 px-4 py-2 text-xs font-sans">
- <span className="">{lang}</span>
- <button
- className="flex ml-auto gap-2"
- aria-label="copy codeblock"
- onClick={async () => {
- const codeString = codeRef.current?.textContent;
- if (codeString)
- navigator.clipboard.writeText(codeString).then(() => {
- setIsCopied(true);
- setTimeout(() => setIsCopied(false), 3000);
- });
- }}
- >
- {isCopied ? (
- <>
- <svg
- xmlns="http://www.w3.org/2000/svg"
- fill="none"
- viewBox="0 0 24 24"
- strokeWidth={1.5}
- stroke="currentColor"
- className="size-4"
- >
- <path
- strokeLinecap="round"
- strokeLinejoin="round"
- d="M11.35 3.836c-.065.21-.1.433-.1.664 0 .414.336.75.75.75h4.5a.75.75 0 0 0 .75-.75 2.25 2.25 0 0 0-.1-.664m-5.8 0A2.251 2.251 0 0 1 13.5 2.25H15c1.012 0 1.867.668 2.15 1.586m-5.8 0c-.376.023-.75.05-1.124.08C9.095 4.01 8.25 4.973 8.25 6.108V8.25m8.9-4.414c.376.023.75.05 1.124.08 1.131.094 1.976 1.057 1.976 2.192V16.5A2.25 2.25 0 0 1 18 18.75h-2.25m-7.5-10.5H4.875c-.621 0-1.125.504-1.125 1.125v11.25c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V18.75m-7.5-10.5h6.375c.621 0 1.125.504 1.125 1.125v9.375m-8.25-3 1.5 1.5 3-3.75"
- />
- </svg>
- Copied!
- </>
- ) : (
- <>
- <svg
- xmlns="http://www.w3.org/2000/svg"
- fill="none"
- viewBox="0 0 24 24"
- strokeWidth={1.5}
- stroke="currentColor"
- className="size-4"
- >
- <path
- strokeLinecap="round"
- strokeLinejoin="round"
- d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 0 1-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 0 1 1.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 0 0-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 0 1-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 0 0-3.375-3.375h-1.5a1.125 1.125 0 0 1-1.125-1.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H9.75"
- />
- </svg>
- Copy code
- </>
- )}
- </button>
- </div>
- );
- },
+ ({
+ lang,
+ codeRef,
+ }: {
+ lang: string;
+ codeRef: React.RefObject<HTMLElement>;
+ }) => {
+ const [isCopied, setIsCopied] = useState<boolean>(false);
+ return (
+ <div className="flex items-center relative text-gray-200 bg-gray-800 px-4 py-2 text-xs font-sans">
+ <span className="">{lang}</span>
+ <button
+ className="flex ml-auto gap-2"
+ aria-label="copy codeblock"
+ onClick={async () => {
+ const codeString = codeRef.current?.textContent;
+ if (codeString)
+ navigator.clipboard.writeText(codeString).then(() => {
+ setIsCopied(true);
+ setTimeout(() => setIsCopied(false), 3000);
+ });
+ }}
+ >
+ {isCopied ? (
+ <>
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ fill="none"
+ viewBox="0 0 24 24"
+ strokeWidth={1.5}
+ stroke="currentColor"
+ className="size-4"
+ >
+ <path
+ strokeLinecap="round"
+ strokeLinejoin="round"
+ d="M11.35 3.836c-.065.21-.1.433-.1.664 0 .414.336.75.75.75h4.5a.75.75 0 0 0 .75-.75 2.25 2.25 0 0 0-.1-.664m-5.8 0A2.251 2.251 0 0 1 13.5 2.25H15c1.012 0 1.867.668 2.15 1.586m-5.8 0c-.376.023-.75.05-1.124.08C9.095 4.01 8.25 4.973 8.25 6.108V8.25m8.9-4.414c.376.023.75.05 1.124.08 1.131.094 1.976 1.057 1.976 2.192V16.5A2.25 2.25 0 0 1 18 18.75h-2.25m-7.5-10.5H4.875c-.621 0-1.125.504-1.125 1.125v11.25c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V18.75m-7.5-10.5h6.375c.621 0 1.125.504 1.125 1.125v9.375m-8.25-3 1.5 1.5 3-3.75"
+ />
+ </svg>
+ Copied!
+ </>
+ ) : (
+ <>
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ fill="none"
+ viewBox="0 0 24 24"
+ strokeWidth={1.5}
+ stroke="currentColor"
+ className="size-4"
+ >
+ <path
+ strokeLinecap="round"
+ strokeLinejoin="round"
+ d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 0 1-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 0 1 1.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 0 0-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 0 1-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 0 0-3.375-3.375h-1.5a1.125 1.125 0 0 1-1.125-1.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H9.75"
+ />
+ </svg>
+ Copy code
+ </>
+ )}
+ </button>
+ </div>
+ );
+ },
);
export default CodeBlock;
diff --git a/apps/web/app/(dash)/chat/[chatid]/page.tsx b/apps/web/app/(dash)/chat/[chatid]/page.tsx
index 96e96020..87fd0b19 100644
--- a/apps/web/app/(dash)/chat/[chatid]/page.tsx
+++ b/apps/web/app/(dash)/chat/[chatid]/page.tsx
@@ -3,36 +3,36 @@ import { chatSearchParamsCache } from "@/lib/searchParams";
import ChatWindow from "../chatWindow";
async function Page({
- params,
- searchParams,
+ params,
+ searchParams,
}: {
- params: { chatid: string };
- searchParams: Record<string, string | string[] | undefined>;
+ params: { chatid: string };
+ searchParams: Record<string, string | string[] | undefined>;
}) {
- const { firstTime, q, spaces } = chatSearchParamsCache.parse(searchParams);
+ const { firstTime, q, spaces } = chatSearchParamsCache.parse(searchParams);
- let chat: Awaited<ReturnType<typeof getFullChatThread>>;
+ let chat: Awaited<ReturnType<typeof getFullChatThread>>;
- try {
- chat = await getFullChatThread(params.chatid);
- } catch (e) {
- const error = e as Error;
- return <div>This page errored out: {error.message}</div>;
- }
+ try {
+ chat = await getFullChatThread(params.chatid);
+ } catch (e) {
+ const error = e as Error;
+ return <div>This page errored out: {error.message}</div>;
+ }
- if (!chat.success || !chat.data) {
- console.error(chat.error);
- return <div>Chat not found. Check the console for more details.</div>;
- }
+ if (!chat.success || !chat.data) {
+ console.error(chat.error);
+ return <div>Chat not found. Check the console for more details.</div>;
+ }
- return (
- <ChatWindow
- q={q}
- spaces={spaces ?? []}
- initialChat={chat.data.length > 0 ? chat.data : undefined}
- threadId={params.chatid}
- />
- );
+ return (
+ <ChatWindow
+ q={q}
+ spaces={spaces ?? []}
+ initialChat={chat.data.length > 0 ? chat.data : undefined}
+ threadId={params.chatid}
+ />
+ );
}
export default Page;
diff --git a/apps/web/app/(dash)/chat/chatWindow.tsx b/apps/web/app/(dash)/chat/chatWindow.tsx
index e610057d..3bc9fec6 100644
--- a/apps/web/app/(dash)/chat/chatWindow.tsx
+++ b/apps/web/app/(dash)/chat/chatWindow.tsx
@@ -8,10 +8,10 @@ import { motion } from "framer-motion";
import { useRouter } from "next/navigation";
import { ChatHistory, sourcesZod } from "@repo/shared-types";
import {
- Accordion,
- AccordionContent,
- AccordionItem,
- AccordionTrigger,
+ Accordion,
+ AccordionContent,
+ AccordionItem,
+ AccordionTrigger,
} from "@repo/ui/shadcn/accordion";
import Markdown from "react-markdown";
import remarkGfm from "remark-gfm";
@@ -27,417 +27,417 @@ import { ClipboardIcon } from "@heroicons/react/24/outline";
import { SendIcon } from "lucide-react";
function ChatWindow({
- q,
- spaces,
- initialChat = [
- {
- question: q,
- answer: {
- parts: [],
- sources: [],
- },
- },
- ],
- threadId,
+ q,
+ spaces,
+ initialChat = [
+ {
+ question: q,
+ answer: {
+ parts: [],
+ sources: [],
+ },
+ },
+ ],
+ threadId,
}: {
- q: string;
- spaces: { id: number; name: string }[];
- initialChat?: ChatHistory[];
- threadId: string;
+ q: string;
+ spaces: { id: number; name: string }[];
+ initialChat?: ChatHistory[];
+ threadId: string;
}) {
- const [layout, setLayout] = useState<"chat" | "initial">(
- initialChat.length > 1 ? "chat" : "initial",
- );
- const [chatHistory, setChatHistory] = useState<ChatHistory[]>(initialChat);
+ const [layout, setLayout] = useState<"chat" | "initial">(
+ initialChat.length > 1 ? "chat" : "initial",
+ );
+ const [chatHistory, setChatHistory] = useState<ChatHistory[]>(initialChat);
- const removeJustificationFromText = (text: string) => {
- // remove everything after the first "<justification>" word
- const justificationLine = text.indexOf("<justification>");
- if (justificationLine !== -1) {
- // Add that justification to the last chat message
- const lastChatMessage = chatHistory[chatHistory.length - 1];
- if (lastChatMessage) {
- lastChatMessage.answer.justification = text.slice(justificationLine);
- }
- return text.slice(0, justificationLine);
- }
- return text;
- };
+ const removeJustificationFromText = (text: string) => {
+ // remove everything after the first "<justification>" word
+ const justificationLine = text.indexOf("<justification>");
+ if (justificationLine !== -1) {
+ // Add that justification to the last chat message
+ const lastChatMessage = chatHistory[chatHistory.length - 1];
+ if (lastChatMessage) {
+ lastChatMessage.answer.justification = text.slice(justificationLine);
+ }
+ return text.slice(0, justificationLine);
+ }
+ return text;
+ };
- const router = useRouter();
+ const router = useRouter();
- const getAnswer = async (query: string, spaces: string[]) => {
- const sourcesFetch = await fetch(
- `/api/chat?q=${query}&spaces=${spaces}&sourcesOnly=true&threadId=${threadId}`,
- {
- method: "POST",
- body: JSON.stringify({ chatHistory }),
- },
- );
+ const getAnswer = async (query: string, spaces: string[]) => {
+ const sourcesFetch = await fetch(
+ `/api/chat?q=${query}&spaces=${spaces}&sourcesOnly=true&threadId=${threadId}`,
+ {
+ method: "POST",
+ body: JSON.stringify({ chatHistory }),
+ },
+ );
- // TODO: handle this properly
- const sources = await sourcesFetch.json();
+ // TODO: handle this properly
+ const sources = await sourcesFetch.json();
- const sourcesParsed = sourcesZod.safeParse(sources);
+ const sourcesParsed = sourcesZod.safeParse(sources);
- if (!sourcesParsed.success) {
- console.error(sourcesParsed.error);
- toast.error("Something went wrong while getting the sources");
- return;
- }
- window.scrollTo({
- top: document.documentElement.scrollHeight,
- behavior: "smooth",
- });
+ if (!sourcesParsed.success) {
+ console.error(sourcesParsed.error);
+ toast.error("Something went wrong while getting the sources");
+ return;
+ }
+ window.scrollTo({
+ top: document.documentElement.scrollHeight,
+ behavior: "smooth",
+ });
- const updateChatHistoryAndFetch = async () => {
- // Step 1: Update chat history with the assistant's response
- await new Promise((resolve) => {
- setChatHistory((prevChatHistory) => {
- const newChatHistory = [...prevChatHistory];
- const lastAnswer = newChatHistory[newChatHistory.length - 1];
- if (!lastAnswer) {
- resolve(undefined);
- return prevChatHistory;
- }
+ const updateChatHistoryAndFetch = async () => {
+ // Step 1: Update chat history with the assistant's response
+ await new Promise((resolve) => {
+ setChatHistory((prevChatHistory) => {
+ const newChatHistory = [...prevChatHistory];
+ const lastAnswer = newChatHistory[newChatHistory.length - 1];
+ if (!lastAnswer) {
+ resolve(undefined);
+ return prevChatHistory;
+ }
- const filteredSourceUrls = new Set(
- sourcesParsed.data.metadata.map((source) => source.url),
- );
- const uniqueSources = sourcesParsed.data.metadata.filter((source) => {
- if (filteredSourceUrls.has(source.url)) {
- filteredSourceUrls.delete(source.url);
- return true;
- }
- return false;
- });
+ const filteredSourceUrls = new Set(
+ sourcesParsed.data.metadata.map((source) => source.url),
+ );
+ const uniqueSources = sourcesParsed.data.metadata.filter((source) => {
+ if (filteredSourceUrls.has(source.url)) {
+ filteredSourceUrls.delete(source.url);
+ return true;
+ }
+ return false;
+ });
- lastAnswer.answer.sources = uniqueSources.map((source) => ({
- title: source.title ?? "Untitled",
- type: source.type ?? "page",
- source: source.url ?? "https://supermemory.ai",
- content: source.description ?? "No content available",
- numChunks: sourcesParsed.data.metadata.filter(
- (f) => f.url === source.url,
- ).length,
- }));
+ lastAnswer.answer.sources = uniqueSources.map((source) => ({
+ title: source.title ?? "Untitled",
+ type: source.type ?? "page",
+ source: source.url ?? "https://supermemory.ai",
+ content: source.description ?? "No content available",
+ numChunks: sourcesParsed.data.metadata.filter(
+ (f) => f.url === source.url,
+ ).length,
+ }));
- resolve(newChatHistory);
- return newChatHistory;
- });
- });
+ resolve(newChatHistory);
+ return newChatHistory;
+ });
+ });
- // Step 2: Fetch data from the API
- const resp = await fetch(
- `/api/chat?q=${query}&spaces=${spaces}&threadId=${threadId}`,
- {
- method: "POST",
- body: JSON.stringify({ chatHistory, sources: sourcesParsed.data }),
- },
- );
+ // Step 2: Fetch data from the API
+ const resp = await fetch(
+ `/api/chat?q=${query}&spaces=${spaces}&threadId=${threadId}`,
+ {
+ method: "POST",
+ body: JSON.stringify({ chatHistory, sources: sourcesParsed.data }),
+ },
+ );
- // Step 3: Read the response stream and update the chat history
- const reader = resp.body?.getReader();
- let done = false;
- while (!done && reader) {
- const { value, done: d } = await reader.read();
- if (d) {
- setChatHistory((prevChatHistory) => {
- createChatObject(threadId, prevChatHistory);
- return prevChatHistory;
- });
- }
- done = d;
+ // Step 3: Read the response stream and update the chat history
+ const reader = resp.body?.getReader();
+ let done = false;
+ while (!done && reader) {
+ const { value, done: d } = await reader.read();
+ if (d) {
+ setChatHistory((prevChatHistory) => {
+ createChatObject(threadId, prevChatHistory);
+ return prevChatHistory;
+ });
+ }
+ done = d;
- const txt = new TextDecoder().decode(value);
- setChatHistory((prevChatHistory) => {
- const newChatHistory = [...prevChatHistory];
- const lastAnswer = newChatHistory[newChatHistory.length - 1];
- if (!lastAnswer) return prevChatHistory;
+ const txt = new TextDecoder().decode(value);
+ setChatHistory((prevChatHistory) => {
+ const newChatHistory = [...prevChatHistory];
+ const lastAnswer = newChatHistory[newChatHistory.length - 1];
+ if (!lastAnswer) return prevChatHistory;
- window.scrollTo({
- top: document.documentElement.scrollHeight,
- behavior: "smooth",
- });
+ window.scrollTo({
+ top: document.documentElement.scrollHeight,
+ behavior: "smooth",
+ });
- lastAnswer.answer.parts.push({ text: txt });
- return newChatHistory;
- });
- }
- };
+ lastAnswer.answer.parts.push({ text: txt });
+ return newChatHistory;
+ });
+ }
+ };
- updateChatHistoryAndFetch();
- };
+ updateChatHistoryAndFetch();
+ };
- useEffect(() => {
- if (q.trim().length > 0 || chatHistory.length > 0) {
- setLayout("chat");
- const lastChat = chatHistory.length > 0 ? chatHistory.length - 1 : 0;
- const startGenerating = chatHistory[lastChat]?.answer.parts[0]?.text
- ? false
- : true;
- if (startGenerating) {
- getAnswer(
- q,
- spaces.map((s) => `${s.id}`),
- );
- }
- } else {
- router.push("/home");
- }
- }, []);
+ useEffect(() => {
+ if (q.trim().length > 0 || chatHistory.length > 0) {
+ setLayout("chat");
+ const lastChat = chatHistory.length > 0 ? chatHistory.length - 1 : 0;
+ const startGenerating = chatHistory[lastChat]?.answer.parts[0]?.text
+ ? false
+ : true;
+ if (startGenerating) {
+ getAnswer(
+ q,
+ spaces.map((s) => `${s.id}`),
+ );
+ }
+ } else {
+ router.push("/home");
+ }
+ }, []);
- return (
- <div className="h-full">
- <AnimatePresence mode="popLayout">
- {layout === "initial" ? (
- <motion.div
- exit={{ opacity: 0 }}
- key="initial"
- className="max-w-3xl h-full justify-center items-center flex mx-auto w-full flex-col"
- >
- <div className="w-full h-96">
- <QueryInput
- handleSubmit={() => {}}
- initialQuery={q}
- initialSpaces={[]}
- disabled
- />
- </div>
- </motion.div>
- ) : (
- <div
- className="max-w-3xl z-10 mx-auto relative h-full overflow-y-auto scrollbar-none"
- key="chat"
- >
- <div className="w-full pt-24 mb-40 px-4 md:px-0">
- {chatHistory.map((chat, idx) => (
- <div key={idx} className="space-y-16">
- <div
- className={`mt-8 ${idx != chatHistory.length - 1 ? "pb-2 border-b border-b-gray-400" : ""}`}
- >
- <h2
- className={cn(
- "text-white transition-all transform translate-y-0 opacity-100 duration-500 ease-in-out font-semibold text-xl",
- )}
- >
- {chat.question}
- </h2>
+ return (
+ <div className="h-full">
+ <AnimatePresence mode="popLayout">
+ {layout === "initial" ? (
+ <motion.div
+ exit={{ opacity: 0 }}
+ key="initial"
+ className="max-w-3xl h-full justify-center items-center flex mx-auto w-full flex-col"
+ >
+ <div className="w-full h-96">
+ <QueryInput
+ handleSubmit={() => {}}
+ initialQuery={q}
+ initialSpaces={[]}
+ disabled
+ />
+ </div>
+ </motion.div>
+ ) : (
+ <div
+ className="max-w-3xl z-10 mx-auto relative h-full overflow-y-auto scrollbar-none"
+ key="chat"
+ >
+ <div className="w-full pt-24 mb-40 px-4 md:px-0">
+ {chatHistory.map((chat, idx) => (
+ <div key={idx} className="space-y-16">
+ <div
+ className={`mt-8 ${idx != chatHistory.length - 1 ? "pb-2 border-b border-b-gray-400" : ""}`}
+ >
+ <h2
+ className={cn(
+ "text-white transition-all transform translate-y-0 opacity-100 duration-500 ease-in-out font-semibold text-xl",
+ )}
+ >
+ {chat.question}
+ </h2>
- <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>
+ <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>
+ {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">Summary</div>
- <div className="text-base">
- {/* Loading state */}
- {(chat.answer.parts.length === 0 ||
- chat.answer.parts.join("").length === 0) && (
- <div className="animate-pulse flex space-x-4">
- <div className="flex-1 space-y-3 py-1">
- <div className="h-2 bg-slate-700 rounded"></div>
- <div className="h-2 bg-slate-700 rounded"></div>
- </div>
- </div>
- )}
+ {/* Summary */}
+ <div>
+ <div className="text-foreground-menu py-2">Summary</div>
+ <div className="text-base">
+ {/* Loading state */}
+ {(chat.answer.parts.length === 0 ||
+ chat.answer.parts.join("").length === 0) && (
+ <div className="animate-pulse flex space-x-4">
+ <div className="flex-1 space-y-3 py-1">
+ <div className="h-2 bg-slate-700 rounded"></div>
+ <div className="h-2 bg-slate-700 rounded"></div>
+ </div>
+ </div>
+ )}
- <Markdown
- remarkPlugins={[remarkGfm, [remarkMath]]}
- rehypePlugins={[
- rehypeKatex,
- [
- rehypeHighlight,
- {
- detect: true,
- ignoreMissing: true,
- subset: codeLanguageSubset,
- },
- ],
- ]}
- components={{
- code: code as any,
- p: p as any,
- }}
- className="flex flex-col gap-2 text-base"
- >
- {removeJustificationFromText(
- chat.answer.parts
- .map((part) => part.text)
- .join(""),
- )}
- </Markdown>
+ <Markdown
+ remarkPlugins={[remarkGfm, [remarkMath]]}
+ rehypePlugins={[
+ rehypeKatex,
+ [
+ rehypeHighlight,
+ {
+ detect: true,
+ ignoreMissing: true,
+ subset: codeLanguageSubset,
+ },
+ ],
+ ]}
+ components={{
+ code: code as any,
+ p: p as any,
+ }}
+ className="flex flex-col gap-2 text-base"
+ >
+ {removeJustificationFromText(
+ chat.answer.parts
+ .map((part) => part.text)
+ .join(""),
+ )}
+ </Markdown>
- <div className="mt-3 relative -left-2 flex items-center gap-1">
- {/* TODO: speak response */}
- {/* <button className="group h-8 w-8 flex justify-center items-center active:scale-75 duration-200">
+ <div className="mt-3 relative -left-2 flex items-center gap-1">
+ {/* TODO: speak response */}
+ {/* <button className="group h-8 w-8 flex justify-center items-center active:scale-75 duration-200">
<SpeakerWaveIcon className="size-[18px] group-hover:text-primary" />
</button> */}
- {/* copy response */}
- <button
- onClick={() =>
- navigator.clipboard.writeText(
- chat.answer.parts
- .map((part) => part.text)
- .join(""),
- )
- }
- className="group h-8 w-8 flex justify-center items-center active:scale-75 duration-200"
- >
- <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>
- {/* 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
- >
- <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>
- ))}
- </div>
+ {/* copy response */}
+ <button
+ onClick={() =>
+ navigator.clipboard.writeText(
+ chat.answer.parts
+ .map((part) => part.text)
+ .join(""),
+ )
+ }
+ className="group h-8 w-8 flex justify-center items-center active:scale-75 duration-200"
+ >
+ <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>
+ {/* 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
+ >
+ <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>
+ ))}
+ </div>
- <div className="fixed bottom-24 md:bottom-4 w-full max-w-3xl">
- <QueryInput
- mini
- className="w-full shadow-md"
- initialQuery={""}
- initialSpaces={spaces}
- handleSubmit={async (q, spaces) => {
- setChatHistory((prevChatHistory) => {
- return [
- ...prevChatHistory,
- {
- question: q,
- answer: {
- parts: [],
- sources: [],
- },
- },
- ];
- });
- await getAnswer(
- q,
- spaces.map((s) => `${s.id}`),
- );
- }}
- />
- </div>
- </div>
- )}
- </AnimatePresence>
- </div>
- );
+ <div className="fixed bottom-24 md:bottom-4 w-full max-w-3xl">
+ <QueryInput
+ mini
+ className="w-full shadow-md"
+ initialQuery={""}
+ initialSpaces={spaces}
+ handleSubmit={async (q, spaces) => {
+ setChatHistory((prevChatHistory) => {
+ return [
+ ...prevChatHistory,
+ {
+ question: q,
+ answer: {
+ parts: [],
+ sources: [],
+ },
+ },
+ ];
+ });
+ await getAnswer(
+ q,
+ spaces.map((s) => `${s.id}`),
+ );
+ }}
+ />
+ </div>
+ </div>
+ )}
+ </AnimatePresence>
+ </div>
+ );
}
export default ChatWindow;
diff --git a/apps/web/app/(dash)/chat/markdownRenderHelpers.tsx b/apps/web/app/(dash)/chat/markdownRenderHelpers.tsx
index 747d4fca..71d3b889 100644
--- a/apps/web/app/(dash)/chat/markdownRenderHelpers.tsx
+++ b/apps/web/app/(dash)/chat/markdownRenderHelpers.tsx
@@ -3,23 +3,23 @@ import { ExtraProps } from "react-markdown";
import CodeBlock from "./CodeBlock";
export const code = memo((props: JSX.IntrinsicElements["code"]) => {
- const { className, children } = props;
- const match = /language-(\w+)/.exec(className || "");
- const lang = match && match[1];
+ const { className, children } = props;
+ const match = /language-(\w+)/.exec(className || "");
+ const lang = match && match[1];
- return <CodeBlock lang={lang || "text"} codeChildren={children as any} />;
+ return <CodeBlock lang={lang || "text"} codeChildren={children as any} />;
});
export const p = memo(
- (
- props?: Omit<
- DetailedHTMLProps<
- HTMLAttributes<HTMLParagraphElement>,
- HTMLParagraphElement
- >,
- "ref"
- >,
- ) => {
- return <p className="whitespace-pre-wrap">{props?.children}</p>;
- },
+ (
+ props?: Omit<
+ DetailedHTMLProps<
+ HTMLAttributes<HTMLParagraphElement>,
+ HTMLParagraphElement
+ >,
+ "ref"
+ >,
+ ) => {
+ return <p className="whitespace-pre-wrap">{props?.children}</p>;
+ },
);
diff --git a/apps/web/app/(dash)/header/autoBreadCrumbs.tsx b/apps/web/app/(dash)/header/autoBreadCrumbs.tsx
index 5767ca6f..a823671c 100644
--- a/apps/web/app/(dash)/header/autoBreadCrumbs.tsx
+++ b/apps/web/app/(dash)/header/autoBreadCrumbs.tsx
@@ -1,47 +1,47 @@
"use client";
import {
- Breadcrumb,
- BreadcrumbItem,
- BreadcrumbLink,
- BreadcrumbList,
- BreadcrumbSeparator,
+ Breadcrumb,
+ BreadcrumbItem,
+ BreadcrumbLink,
+ BreadcrumbList,
+ BreadcrumbSeparator,
} from "@repo/ui/shadcn/breadcrumb";
import { usePathname } from "next/navigation";
import React from "react";
function AutoBreadCrumbs() {
- const pathname = usePathname();
+ const pathname = usePathname();
- console.log(pathname.split("/").filter(Boolean));
+ console.log(pathname.split("/").filter(Boolean));
- return (
- <Breadcrumb className="hidden md:block">
- <BreadcrumbList>
- {!pathname.startsWith("/home") && (
- <>
- <BreadcrumbItem>
- <BreadcrumbLink href="/">Home</BreadcrumbLink>
- </BreadcrumbItem>
- <BreadcrumbSeparator hidden={pathname.split("/").length === 1} />
- </>
- )}
- {pathname
- .split("/")
- .filter(Boolean)
- .map((path, idx, paths) => (
- <>
- <BreadcrumbItem key={path}>
- <BreadcrumbLink href={`/${paths.slice(0, idx + 1).join("/")}`}>
- {path.charAt(0).toUpperCase() + path.slice(1)}
- </BreadcrumbLink>
- </BreadcrumbItem>
- <BreadcrumbSeparator hidden={idx === paths.length - 1} />
- </>
- ))}
- </BreadcrumbList>
- </Breadcrumb>
- );
+ return (
+ <Breadcrumb className="hidden md:block">
+ <BreadcrumbList>
+ {!pathname.startsWith("/home") && (
+ <>
+ <BreadcrumbItem>
+ <BreadcrumbLink href="/">Home</BreadcrumbLink>
+ </BreadcrumbItem>
+ <BreadcrumbSeparator hidden={pathname.split("/").length === 1} />
+ </>
+ )}
+ {pathname
+ .split("/")
+ .filter(Boolean)
+ .map((path, idx, paths) => (
+ <>
+ <BreadcrumbItem key={path}>
+ <BreadcrumbLink href={`/${paths.slice(0, idx + 1).join("/")}`}>
+ {path.charAt(0).toUpperCase() + path.slice(1)}
+ </BreadcrumbLink>
+ </BreadcrumbItem>
+ <BreadcrumbSeparator hidden={idx === paths.length - 1} />
+ </>
+ ))}
+ </BreadcrumbList>
+ </Breadcrumb>
+ );
}
export default AutoBreadCrumbs;
diff --git a/apps/web/app/(dash)/header/header.tsx b/apps/web/app/(dash)/header/header.tsx
index 0fc28227..b9d400c9 100644
--- a/apps/web/app/(dash)/header/header.tsx
+++ b/apps/web/app/(dash)/header/header.tsx
@@ -8,54 +8,54 @@ import NewChatButton from "./newChatButton";
import AutoBreadCrumbs from "./autoBreadCrumbs";
async function Header() {
- const chatThreads = await getChatHistory();
-
- if (!chatThreads.success || !chatThreads.data) {
- return <div>Error fetching chat threads</div>;
- }
-
- return (
- <div className="p-4 relative z-30 h-16 flex items-center">
- <div className="w-full flex items-center justify-between">
- <div className="flex items-center gap-4">
- <Link className="" href="/home">
- <Image
- src={Logo}
- alt="SuperMemory logo"
- className="hover:brightness-125 duration-200 w-full h-full"
- />
- </Link>
-
- <AutoBreadCrumbs />
- </div>
-
- <div className="flex items-center gap-2">
- <NewChatButton />
-
- <div className="relative group">
- <button className="flex duration-200 items-center text-[#7D8994] hover:bg-[#1F2429] text-[13px] gap-2 px-3 py-2 rounded-xl">
- History
- </button>
-
- <div className="absolute p-4 hidden group-hover:block right-0 w-full md:w-[400px] max-h-[70vh] overflow-auto">
- <div className="bg-[#1F2429] rounded-xl p-2 flex flex-col shadow-lg">
- {chatThreads.data.map((thread) => (
- <Link
- prefetch={false}
- href={`/chat/${thread.id}`}
- key={thread.id}
- className="p-2 rounded-md hover:bg-secondary"
- >
- {thread.firstMessage}
- </Link>
- ))}
- </div>
- </div>
- </div>
- </div>
- </div>
- </div>
- );
+ const chatThreads = await getChatHistory();
+
+ if (!chatThreads.success || !chatThreads.data) {
+ return <div>Error fetching chat threads</div>;
+ }
+
+ return (
+ <div className="p-4 relative z-30 h-16 flex items-center">
+ <div className="w-full flex items-center justify-between">
+ <div className="flex items-center gap-4">
+ <Link className="" href="/home">
+ <Image
+ src={Logo}
+ alt="SuperMemory logo"
+ className="hover:brightness-125 duration-200 w-full h-full"
+ />
+ </Link>
+
+ <AutoBreadCrumbs />
+ </div>
+
+ <div className="flex items-center gap-2">
+ <NewChatButton />
+
+ <div className="relative group">
+ <button className="flex duration-200 items-center text-[#7D8994] hover:bg-[#1F2429] text-[13px] gap-2 px-3 py-2 rounded-xl">
+ History
+ </button>
+
+ <div className="absolute p-4 hidden group-hover:block right-0 w-full md:w-[400px] max-h-[70vh] overflow-auto">
+ <div className="bg-[#1F2429] rounded-xl p-2 flex flex-col shadow-lg">
+ {chatThreads.data.map((thread) => (
+ <Link
+ prefetch={false}
+ href={`/chat/${thread.id}`}
+ key={thread.id}
+ className="p-2 rounded-md hover:bg-secondary"
+ >
+ {thread.firstMessage}
+ </Link>
+ ))}
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ );
}
export default Header;
diff --git a/apps/web/app/(dash)/header/newChatButton.tsx b/apps/web/app/(dash)/header/newChatButton.tsx
index 0e9e1c5a..a634ab41 100644
--- a/apps/web/app/(dash)/header/newChatButton.tsx
+++ b/apps/web/app/(dash)/header/newChatButton.tsx
@@ -7,21 +7,21 @@ import { usePathname } from "next/navigation";
import React from "react";
function NewChatButton() {
- const path = usePathname();
+ const path = usePathname();
- if (path.startsWith("/chat")) {
- return (
- <Link
- href="/home"
- className="flex duration-200 items-center text-[#7D8994] hover:bg-[#1F2429] text-[13px] gap-2 px-3 py-2 rounded-xl"
- >
- <Image src={ChatIcon} alt="Chat icon" className="w-5" />
- Start new chat
- </Link>
- );
- }
+ if (path.startsWith("/chat")) {
+ return (
+ <Link
+ href="/home"
+ className="flex duration-200 items-center text-[#7D8994] hover:bg-[#1F2429] text-[13px] gap-2 px-3 py-2 rounded-xl"
+ >
+ <Image src={ChatIcon} alt="Chat icon" className="w-5" />
+ Start new chat
+ </Link>
+ );
+ }
- return null;
+ return null;
}
export default NewChatButton;
diff --git a/apps/web/app/(dash)/home/homeVariants.ts b/apps/web/app/(dash)/home/homeVariants.ts
index cc533fc4..1b44bab9 100644
--- a/apps/web/app/(dash)/home/homeVariants.ts
+++ b/apps/web/app/(dash)/home/homeVariants.ts
@@ -1,50 +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.",
- },
- ],
+ [
+ {
+ type: "text",
+ content: "Unlock your",
+ },
+ {
+ type: "highlighted",
+ content: " digital brain",
+ },
+ ],
+ [
+ {
+ type: "text",
+ content: "Save",
+ },
+ {
+ type: "highlighted",
+ content: " everything.",
+ },
+ {
+ type: "text",
+ content: " Connect",
+ },
+ {
+ type: "highlighted",
+ content: " anything.",
+ },
+ ],
+ [
+ {
+ type: "text",
+ content: "Turn your bookmarks into",
+ },
+ {
+ type: "highlighted",
+ content: " insights.",
+ },
+ ],
+ [
+ {
+ type: "text",
+ content: "The smart way to use your",
+ },
+ {
+ type: "highlighted",
+ content: " digital treasure.",
+ },
+ ],
];
diff --git a/apps/web/app/(dash)/home/page.tsx b/apps/web/app/(dash)/home/page.tsx
index a3a3b946..7ef8e65d 100644
--- a/apps/web/app/(dash)/home/page.tsx
+++ b/apps/web/app/(dash)/home/page.tsx
@@ -10,127 +10,127 @@ import { motion } from "framer-motion";
import { variants } from "./homeVariants";
const slap = {
- initial: {
- opacity: 0,
- scale: 1.1,
- },
- whileInView: { opacity: 1, scale: 1 },
- transition: {
- duration: 0.5,
- ease: "easeInOut",
- },
- viewport: { once: true },
+ initial: {
+ opacity: 0,
+ scale: 1.1,
+ },
+ whileInView: { opacity: 1, scale: 1 },
+ transition: {
+ duration: 0.5,
+ ease: "easeInOut",
+ },
+ viewport: { once: true },
};
function Page({
- searchParams,
+ searchParams,
}: {
- searchParams: Record<string, string | string[] | undefined>;
+ 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);
-
- useEffect(() => {
- 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();
- }
-
- if (extensionInstalled) {
- toast.success("Extension installed successfully");
- }
-
- getSpaces().then((res) => {
- if (res.success && res.data) {
- setSpaces(res.data);
- return;
- }
- // 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">
- <QueryInput
- handleSubmit={async (q, spaces) => {
- if (q.length === 0) {
- toast.error("Query is required");
- return;
- }
-
- const threadid = await createChatThread(q);
-
- if (!threadid.success || !threadid.data) {
- toast.error("Failed to create chat thread");
- return;
- }
-
- push(
- `/chat/${threadid.data}?spaces=${JSON.stringify(spaces)}&q=${q}`,
- );
- }}
- initialSpaces={spaces}
- setInitialSpaces={setSpaces}
- />
- </div>
- </div>
- );
+ // 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);
+
+ useEffect(() => {
+ 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();
+ }
+
+ if (extensionInstalled) {
+ toast.success("Extension installed successfully");
+ }
+
+ getSpaces().then((res) => {
+ if (res.success && res.data) {
+ setSpaces(res.data);
+ return;
+ }
+ // 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">
+ <QueryInput
+ handleSubmit={async (q, spaces) => {
+ if (q.length === 0) {
+ toast.error("Query is required");
+ return;
+ }
+
+ const threadid = await createChatThread(q);
+
+ if (!threadid.success || !threadid.data) {
+ toast.error("Failed to create chat thread");
+ return;
+ }
+
+ push(
+ `/chat/${threadid.data}?spaces=${JSON.stringify(spaces)}&q=${q}`,
+ );
+ }}
+ initialSpaces={spaces}
+ setInitialSpaces={setSpaces}
+ />
+ </div>
+ </div>
+ );
}
export default Page;
diff --git a/apps/web/app/(dash)/home/queryinput.tsx b/apps/web/app/(dash)/home/queryinput.tsx
index d622b8b0..c7267298 100644
--- a/apps/web/app/(dash)/home/queryinput.tsx
+++ b/apps/web/app/(dash)/home/queryinput.tsx
@@ -12,170 +12,170 @@ import { toast } from "sonner";
import { createSpace } from "@/app/actions/doers";
function QueryInput({
- initialQuery = "",
- initialSpaces = [],
- disabled = false,
- className,
- mini = false,
- handleSubmit,
- setInitialSpaces,
+ initialQuery = "",
+ initialSpaces = [],
+ disabled = false,
+ className,
+ mini = false,
+ handleSubmit,
+ setInitialSpaces,
}: {
- 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 }[]>
- >;
+ 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(initialQuery);
+ const [q, setQ] = useState(initialQuery);
- const [selectedSpaces, setSelectedSpaces] = useState<number[]>([]);
+ const [selectedSpaces, setSelectedSpaces] = useState<number[]>([]);
- const options = useMemo(
- () =>
- initialSpaces.map((x) => ({
- label: x.name,
- value: x.id.toString(),
- })),
- [initialSpaces],
- );
+ 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 preparedSpaces = useMemo(
+ () =>
+ initialSpaces
+ .filter((x) => selectedSpaces.includes(x.id))
+ .map((x) => {
+ return {
+ id: x.id,
+ name: x.name,
+ };
+ }),
+ [selectedSpaces, initialSpaces],
+ );
- return (
- <div className={`${className}`}>
- <div
- className={`bg-secondary border-2 border-b-0 border-border ${!mini ? "rounded-t-3xl" : "rounded-3xl"}`}
- >
- {/* input and action button */}
- <form
- action={async () => {
- handleSubmit(q, preparedSpaces);
- 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"
- placeholder="Ask your second brain..."
- onKeyDown={(e) => {
- if (e.key === "Enter" && !e.shiftKey) {
- e.preventDefault();
- if (q.trim().length === 0) {
- return;
- }
- handleSubmit(q, preparedSpaces);
- setQ("");
- }
- }}
- onChange={(e) => setQ(e.target.value)}
- value={q}
- disabled={disabled}
- />
+ return (
+ <div className={`${className}`}>
+ <div
+ className={`bg-secondary border-2 border-b-0 border-border ${!mini ? "rounded-t-3xl" : "rounded-3xl"}`}
+ >
+ {/* input and action button */}
+ <form
+ action={async () => {
+ handleSubmit(q, preparedSpaces);
+ 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"
+ placeholder="Ask your second brain..."
+ onKeyDown={(e) => {
+ if (e.key === "Enter" && !e.shiftKey) {
+ e.preventDefault();
+ if (q.trim().length === 0) {
+ return;
+ }
+ handleSubmit(q, preparedSpaces);
+ setQ("");
+ }
+ }}
+ onChange={(e) => setQ(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>
- </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...");
+ <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.");
- }
+ 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..."
- />
+ 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>
- );
+ <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>
+ );
}
export default QueryInput;
diff --git a/apps/web/app/(dash)/layout.tsx b/apps/web/app/(dash)/layout.tsx
index e161992f..b2b27a4f 100644
--- a/apps/web/app/(dash)/layout.tsx
+++ b/apps/web/app/(dash)/layout.tsx
@@ -6,33 +6,33 @@ import { Toaster } from "@repo/ui/shadcn/sonner";
import BackgroundPlus from "../(landing)/GridPatterns/PlusGrid";
async function Layout({ children }: { children: React.ReactNode }) {
- const info = await auth();
+ const info = await auth();
- if (!info) {
- return redirect("/signin");
- }
+ if (!info) {
+ return redirect("/signin");
+ }
- return (
- <main className="h-screen flex flex-col">
- <div className="fixed top-0 left-0 w-full z-40">
- <Header />
- </div>
+ return (
+ <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-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" />
+ <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]"
+ style={{ transform: "rotate(-30deg)" }}
+ />
+ </div>
+ <BackgroundPlus className="absolute top-0 left-0 w-full h-full -z-50 opacity-70" />
- <Menu />
+ <Menu />
- <div className="w-full h-full">{children}</div>
+ <div className="w-full h-full">{children}</div>
- <Toaster />
- </main>
- );
+ <Toaster />
+ </main>
+ );
}
export default Layout;
diff --git a/apps/web/app/(dash)/menu.tsx b/apps/web/app/(dash)/menu.tsx
index 5e05d358..711b081c 100644
--- a/apps/web/app/(dash)/menu.tsx
+++ b/apps/web/app/(dash)/menu.tsx
@@ -7,13 +7,13 @@ 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,
+ 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";
@@ -26,334 +26,334 @@ import { StoredSpace } from "@/server/db/schema";
import useMeasure from "react-use-measure";
function Menu() {
- const [spaces, setSpaces] = useState<StoredSpace[]>([]);
+ const [spaces, setSpaces] = useState<StoredSpace[]>([]);
- useEffect(() => {
- (async () => {
- let spaces = await getSpaces();
+ useEffect(() => {
+ (async () => {
+ let spaces = await getSpaces();
- if (!spaces.success || !spaces.data) {
- toast.warning("Unable to get spaces", {
- richColors: true,
- });
- setSpaces([]);
- return;
- }
- setSpaces(spaces.data);
- })();
- }, []);
+ 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 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 [content, setContent] = useState("");
+ const [selectedSpaces, setSelectedSpaces] = useState<number[]>([]);
- const autoDetectedType = useMemo(() => {
- if (content.length === 0) {
- return "none";
- }
+ 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]);
+ 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 [dialogOpen, setDialogOpen] = useState(false);
- const options = useMemo(
- () =>
- spaces.map((x) => ({
- label: x.name,
- value: x.id.toString(),
- })),
- [spaces],
- );
+ const options = useMemo(
+ () =>
+ spaces.map((x) => ({
+ label: x.name,
+ value: x.id.toString(),
+ })),
+ [spaces],
+ );
- const handleSubmit = async (content?: string, spaces?: number[]) => {
- setDialogOpen(false);
+ 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,
- });
+ 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;
- }
+ if (!content || content.length === 0) {
+ toast.error("Content is required");
+ return;
+ }
- console.log(spaces);
+ console.log(spaces);
- const cont = await createMemory({
- content: content,
- spaces: spaces ?? undefined,
- });
+ const cont = await createMemory({
+ content: content,
+ spaces: spaces ?? undefined,
+ });
- setContent("");
- setSelectedSpaces([]);
+ setContent("");
+ setSelectedSpaces([]);
- if (cont.success) {
- toast.success("Memory created", {
- richColors: true,
- });
- } else {
- toast.error(`Memory creation failed: ${cont.error}`);
- }
- };
+ 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>
- </div>
+ 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>
+ </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();
+ <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>
+ 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 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>
+ <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...");
+ <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.");
- }
+ 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"
- />
+ 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>
+ <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>
+ <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
- 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>
+ {/* 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"}`}
+ >
+ <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={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">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>
- </Dialog>
- </>
- );
+ <DialogTrigger
+ className={`flex flex-col items-center cursor-pointer text-white`}
+ >
+ <Image
+ 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">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>
+ </Dialog>
+ </>
+ );
}
export default Menu;
diff --git a/apps/web/app/(dash)/note/[noteid]/page.tsx b/apps/web/app/(dash)/note/[noteid]/page.tsx
index 76fed275..40fe6a9d 100644
--- a/apps/web/app/(dash)/note/[noteid]/page.tsx
+++ b/apps/web/app/(dash)/note/[noteid]/page.tsx
@@ -2,23 +2,23 @@ import { getNoteFromId } from "@/app/actions/fetchers";
import { NotebookIcon } from "lucide-react";
async function Page({ params }: { params: { noteid: string } }) {
- const note = await getNoteFromId(params.noteid as string);
+ const note = await getNoteFromId(params.noteid as string);
- if (!note.success) {
- return <div>Failed to load note</div>;
- }
+ if (!note.success) {
+ return <div>Failed to load note</div>;
+ }
- return (
- <div className="max-w-3xl mt-16 md:mt-32 flex mx-auto w-full flex-col">
- <div className="flex items-center gap-2 text-xs">
- <NotebookIcon className="w-3 h-3" /> Note
- </div>
- <h1 className="text-white w-full font-medium text-2xl text-left mt-2">
- {note.data?.title}
- </h1>
- <div className="w-full pb-20 mt-12">{note.data?.content}</div>
- </div>
- );
+ return (
+ <div className="max-w-3xl mt-16 md:mt-32 flex mx-auto w-full flex-col">
+ <div className="flex items-center gap-2 text-xs">
+ <NotebookIcon className="w-3 h-3" /> Note
+ </div>
+ <h1 className="text-white w-full font-medium text-2xl text-left mt-2">
+ {note.data?.title}
+ </h1>
+ <div className="w-full pb-20 mt-12">{note.data?.content}</div>
+ </div>
+ );
}
export default Page;