diff options
| author | Samrat Malisetti <[email protected]> | 2024-07-19 11:19:50 -0700 |
|---|---|---|
| committer | GitHub <[email protected]> | 2024-07-19 11:19:50 -0700 |
| commit | 581ce2780ea8b8b931539adcbddacf1d322fd027 (patch) | |
| tree | 4813158cd7aa3abb44c7e3884cfd4415d8b5148c /apps/web/app/(dash) | |
| parent | fixed mbile layout (diff) | |
| parent | Disabled sentry for now because of unreasonably large bundle size (diff) | |
| download | supermemory-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.tsx | 673 | ||||
| -rw-r--r-- | apps/web/app/(dash)/(memories)/memories/page.tsx | 6 | ||||
| -rw-r--r-- | apps/web/app/(dash)/(memories)/space/[spaceid]/page.tsx | 28 | ||||
| -rw-r--r-- | apps/web/app/(dash)/chat/CodeBlock.tsx | 158 | ||||
| -rw-r--r-- | apps/web/app/(dash)/chat/[chatid]/page.tsx | 48 | ||||
| -rw-r--r-- | apps/web/app/(dash)/chat/chatWindow.tsx | 774 | ||||
| -rw-r--r-- | apps/web/app/(dash)/chat/markdownRenderHelpers.tsx | 30 | ||||
| -rw-r--r-- | apps/web/app/(dash)/header/autoBreadCrumbs.tsx | 68 | ||||
| -rw-r--r-- | apps/web/app/(dash)/header/header.tsx | 96 | ||||
| -rw-r--r-- | apps/web/app/(dash)/header/newChatButton.tsx | 26 | ||||
| -rw-r--r-- | apps/web/app/(dash)/home/homeVariants.ts | 96 | ||||
| -rw-r--r-- | apps/web/app/(dash)/home/page.tsx | 232 | ||||
| -rw-r--r-- | apps/web/app/(dash)/home/queryinput.tsx | 308 | ||||
| -rw-r--r-- | apps/web/app/(dash)/layout.tsx | 42 | ||||
| -rw-r--r-- | apps/web/app/(dash)/menu.tsx | 616 | ||||
| -rw-r--r-- | apps/web/app/(dash)/note/[noteid]/page.tsx | 30 |
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; |