diff --git a/apps/web/app/new/page.tsx b/apps/web/app/new/page.tsx index ee953abb..3e1042e0 100644 --- a/apps/web/app/new/page.tsx +++ b/apps/web/app/new/page.tsx @@ -24,4 +24,4 @@ export default function NewPage() { ) -} \ No newline at end of file +} diff --git a/apps/web/components/new/chat-sidebar.tsx b/apps/web/components/new/chat-sidebar.tsx index 974469fa..c211e644 100644 --- a/apps/web/components/new/chat-sidebar.tsx +++ b/apps/web/components/new/chat-sidebar.tsx @@ -4,7 +4,12 @@ import { useState } from "react" import { motion, AnimatePresence } from "motion/react" import NovaOrb from "@/components/nova/nova-orb" import { Button } from "@ui/components/button" -import { HistoryIcon, PanelRightCloseIcon, SendIcon, SquarePenIcon } from "lucide-react" +import { + HistoryIcon, + PanelRightCloseIcon, + SendIcon, + SquarePenIcon, +} from "lucide-react" import { cn } from "@lib/utils" import { dmSansClassName } from "@/utils/fonts" @@ -77,26 +82,38 @@ export function ChatSidebar() { exit={{ x: "100px", opacity: 0 }} transition={{ duration: 0.3, ease: "easeOut", bounce: 0 }} > -
+

Chat Title

- - - - - - + + + + +
-
+
{messages.map((msg, i) => (
) } - diff --git a/apps/web/components/new/document-cards/file-preview.tsx b/apps/web/components/new/document-cards/file-preview.tsx index cb2ce4f4..447c4f30 100644 --- a/apps/web/components/new/document-cards/file-preview.tsx +++ b/apps/web/components/new/document-cards/file-preview.tsx @@ -24,7 +24,7 @@ function getFileTypeInfo(document: DocumentWithMemories): { return { icon: , extension: ".pdf", - color: "#DC2626", + color: "#DC2626", } } if (mimeType.startsWith("image/")) { @@ -49,7 +49,7 @@ function getFileTypeInfo(document: DocumentWithMemories): { return { icon: , extension: ".pdf", - color: "#DC2626", + color: "#DC2626", } case "image": return { @@ -72,10 +72,13 @@ function getFileTypeInfo(document: DocumentWithMemories): { export function FilePreview({ document }: { document: DocumentWithMemories }) { const [imageError, setImageError] = useState(false) const { icon, extension, color } = getFileTypeInfo(document) - + const type = document.type?.toLowerCase() const mimeType = document.metadata?.mimeType as string | undefined - const isImage = (mimeType?.startsWith("image/") || type === "image") && document.url && !imageError + const isImage = + (mimeType?.startsWith("image/") || type === "image") && + document.url && + !imageError return (
@@ -87,25 +90,25 @@ export function FilePreview({ document }: { document: DocumentWithMemories }) { }} /> )} - {isImage ? ( - <> -
- {document.title setImageError(true)} - loading="lazy" - /> -
- {document.content && ( -
-

- {document.content} -

-
- )} - + {isImage && document.url ? ( +
+
+
+ {document.title setImageError(true)} + loading="lazy" + /> +
) : (
@@ -127,4 +130,3 @@ export function FilePreview({ document }: { document: DocumentWithMemories }) {
) } - diff --git a/apps/web/components/new/document-cards/google-docs-preview.tsx b/apps/web/components/new/document-cards/google-docs-preview.tsx index 843955e3..ee3e6c72 100644 --- a/apps/web/components/new/document-cards/google-docs-preview.tsx +++ b/apps/web/components/new/document-cards/google-docs-preview.tsx @@ -8,7 +8,11 @@ import { cn } from "@lib/utils" type DocumentsResponse = z.infer type DocumentWithMemories = DocumentsResponse["documents"][0] -export function GoogleDocsPreview({ document }: { document: DocumentWithMemories }) { +export function GoogleDocsPreview({ + document, +}: { + document: DocumentWithMemories +}) { return (
@@ -56,4 +60,3 @@ export function GoogleDocsPreview({ document }: { document: DocumentWithMemories
) } - diff --git a/apps/web/components/new/document-cards/mcp-preview.tsx b/apps/web/components/new/document-cards/mcp-preview.tsx new file mode 100644 index 00000000..47dd8d14 --- /dev/null +++ b/apps/web/components/new/document-cards/mcp-preview.tsx @@ -0,0 +1,36 @@ +"use client" + +import type { DocumentsWithMemoriesResponseSchema } from "@repo/validation/api" +import type { z } from "zod" +import { dmSansClassName } from "@/utils/fonts" +import { cn } from "@lib/utils" +import { ClaudeDesktopIcon, MCPIcon } from "@ui/assets/icons" + +type DocumentsResponse = z.infer +type DocumentWithMemories = DocumentsResponse["documents"][0] + +export function McpPreview({ document }: { document: DocumentWithMemories }) { + return ( +
+
+

+ + Claude Desktop +

+ +
+
+ {document.title && ( +

+ {document.title} +

+ )} + {document.content && ( +

+ {document.content} +

+ )} +
+
+ ) +} diff --git a/apps/web/components/new/document-cards/note-preview.tsx b/apps/web/components/new/document-cards/note-preview.tsx index ef4a0030..4f443e90 100644 --- a/apps/web/components/new/document-cards/note-preview.tsx +++ b/apps/web/components/new/document-cards/note-preview.tsx @@ -17,6 +17,7 @@ function NoteIcon() { fill="none" xmlns="http://www.w3.org/2000/svg" > + Note Icon
- - { - document.title && ( + {document.title && (

{document.title}

- ) - } - {document.content && ( -

- {document.content} -

- )} + )} + {document.content && ( +

+ {document.content} +

+ )}
) } - diff --git a/apps/web/components/new/document-cards/tweet-preview.tsx b/apps/web/components/new/document-cards/tweet-preview.tsx index 9f0b2728..51f97bce 100644 --- a/apps/web/components/new/document-cards/tweet-preview.tsx +++ b/apps/web/components/new/document-cards/tweet-preview.tsx @@ -36,4 +36,3 @@ export function TweetPreview({ data }: { data: Tweet }) {
) } - diff --git a/apps/web/components/new/document-cards/website-preview.tsx b/apps/web/components/new/document-cards/website-preview.tsx index 7740da60..d61b1ed6 100644 --- a/apps/web/components/new/document-cards/website-preview.tsx +++ b/apps/web/components/new/document-cards/website-preview.tsx @@ -9,9 +9,14 @@ import { cn } from "@lib/utils" type DocumentsResponse = z.infer type DocumentWithMemories = DocumentsResponse["documents"][0] -export function WebsitePreview({ document }: { document: DocumentWithMemories }) { +export function WebsitePreview({ + document, +}: { + document: DocumentWithMemories +}) { const [imageError, setImageError] = useState(false) - const ogImage = (document as DocumentWithMemories & { ogImage?: string }).ogImage + const ogImage = (document as DocumentWithMemories & { ogImage?: string }) + .ogImage return (
@@ -45,4 +50,3 @@ export function WebsitePreview({ document }: { document: DocumentWithMemories })
) } - diff --git a/apps/web/components/new/document-cards/youtube-preview.tsx b/apps/web/components/new/document-cards/youtube-preview.tsx index 698a8faf..101376f8 100644 --- a/apps/web/components/new/document-cards/youtube-preview.tsx +++ b/apps/web/components/new/document-cards/youtube-preview.tsx @@ -4,12 +4,16 @@ import type { DocumentsWithMemoriesResponseSchema } from "@repo/validation/api" import type { z } from "zod" import { dmSansClassName } from "@/utils/fonts" import { cn } from "@lib/utils" -import { extractYouTubeVideoId } from "./youtube-utils" +import { extractYouTubeVideoId } from "../utils" type DocumentsResponse = z.infer type DocumentWithMemories = DocumentsResponse["documents"][0] -export function YoutubePreview({ document }: { document: DocumentWithMemories }) { +export function YoutubePreview({ + document, +}: { + document: DocumentWithMemories +}) { const videoId = extractYouTubeVideoId(document.url) if (!videoId) { @@ -46,4 +50,3 @@ export function YoutubePreview({ document }: { document: DocumentWithMemories })
) } - diff --git a/apps/web/components/new/header.tsx b/apps/web/components/new/header.tsx index 64444c5a..6730e96c 100644 --- a/apps/web/components/new/header.tsx +++ b/apps/web/components/new/header.tsx @@ -4,71 +4,195 @@ import { Logo } from "@ui/assets/Logo" import { Avatar, AvatarFallback, AvatarImage } from "@ui/components/avatar" import { useAuth } from "@lib/auth-context" import { useEffect, useState } from "react" -import { ChevronsLeftRight, LayoutGridIcon, Plus, SearchIcon } from "lucide-react" +import { + ChevronsLeftRight, + LayoutGridIcon, + Plus, + SearchIcon, + FolderIcon, +} from "lucide-react" import { Button } from "@ui/components/button" import { cn } from "@lib/utils" import { dmSansClassName } from "@/utils/fonts" import { Tabs, TabsList, TabsTrigger } from "@ui/components/tabs" +import { useProjectName } from "@/hooks/use-project-name" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@ui/components/dropdown-menu" +import { useQuery } from "@tanstack/react-query" +import { $fetch } from "@repo/lib/api" +import { DEFAULT_PROJECT_ID } from "@repo/lib/constants" +import { useProjectMutations } from "@/hooks/use-project-mutations" +import { useProject } from "@/stores" + +interface Project { + id: string + name: string + containerTag: string + createdAt: string + updatedAt: string + isExperimental?: boolean +} export function Header() { - const { user } = useAuth() - const [name, setName] = useState("") + const { user } = useAuth() + const [name, setName] = useState("") + const projectName = useProjectName() + const { selectedProject } = useProject() + const { switchProject } = useProjectMutations() - useEffect(() => { - const storedName = - localStorage.getItem("username") || localStorage.getItem("userName") || "" - setName(storedName) - }, []) + const { data: projects = [] } = useQuery({ + queryKey: ["projects"], + queryFn: async () => { + const response = await $fetch("@get/projects") + + if (response.error) { + throw new Error(response.error?.message || "Failed to load projects") + } + + return response.data?.projects || [] + }, + staleTime: 30 * 1000, + }) + + useEffect(() => { + const storedName = + localStorage.getItem("username") || localStorage.getItem("userName") || "" + setName(storedName) + }, []) return (
-
-
- - {name && ( -
-

{name}'s

-

- supermemory -

-
- )} -
-
-
-

📁 My Space

- -
-
- - - Grid - Graph - - -
- - - {user && ( - - - {user?.name?.charAt(0)} - - )} -
+
+
+ + {name && ( +
+

{name}'s

+

+ supermemory +

+
+ )} +
+
+
+

📁 {projectName}

+ + + + + + switchProject(DEFAULT_PROJECT_ID)} + className={cn( + "cursor-pointer", + selectedProject === DEFAULT_PROJECT_ID && "bg-accent", + )} + > + + Default Project + + {projects + .filter((p: Project) => p.containerTag !== DEFAULT_PROJECT_ID) + .map((project: Project) => ( + switchProject(project.containerTag)} + className={cn( + "cursor-pointer", + selectedProject === project.containerTag && "bg-accent", + )} + > + + {project.name} + + ))} + + +
+
+ + + + + Grid + + + + Graph + + + +
+ + + {user && ( + + + {user?.name?.charAt(0)} + + )} +
) } diff --git a/apps/web/components/new/memories-grid.tsx b/apps/web/components/new/memories-grid.tsx index 19b11feb..10de8916 100644 --- a/apps/web/components/new/memories-grid.tsx +++ b/apps/web/components/new/memories-grid.tsx @@ -20,10 +20,9 @@ import { GoogleDocsPreview } from "./document-cards/google-docs-preview" import { FilePreview } from "./document-cards/file-preview" import { NotePreview } from "./document-cards/note-preview" import { YoutubePreview } from "./document-cards/youtube-preview" -import { - isYouTubeUrl, - useYouTubeChannelName, -} from "./document-cards/youtube-utils" +import { getAbsoluteUrl, isYouTubeUrl, useYouTubeChannelName } from "./utils" +import { SyncLogoIcon } from "@ui/assets/icons" +import { McpPreview } from "./document-cards/mcp-preview" type DocumentsResponse = z.infer type DocumentWithMemories = DocumentsResponse["documents"][0] @@ -83,7 +82,9 @@ export function MemoriesGrid() { }) const documents = useMemo(() => { - return data?.pages.flatMap((p: DocumentsResponse) => p.documents ?? []) ?? [] + return ( + data?.pages.flatMap((p: DocumentsResponse) => p.documents ?? []) ?? [] + ) }, [data]) const hasMore = hasNextPage @@ -119,13 +120,7 @@ export function MemoriesGrid() { index: number data: DocumentWithMemories width: number - }) => ( - - ), + }) => , [], ) @@ -234,54 +229,71 @@ const DocumentCard = memo( width: number }) => { return ( -
+
-
- {document.url && !document.url.includes("x.com") && !document.url.includes("twitter.com") && !document.url.includes("files.supermemory.ai") && ( -
-

+ {document.url && + !document.url.includes("x.com") && + !document.url.includes("twitter.com") && + !document.url.includes("files.supermemory.ai") && ( +

+

+ {document.title} +

+ + +
)} - > - {document.title} -

- - +
+

+ + {document.memoryEntries.length} memories +

+

+ {new Date(document.createdAt).toLocaleDateString("en-US", { + month: "short", + day: "numeric", + year: "numeric", + })} +

+
- )} -
-

- - {document.memoryEntries.length} memories -

-

- {new Date(document.createdAt).toLocaleDateString("en-US", { - month: "short", - day: "numeric", - year: "numeric", - })} -

-
-
+ )}
) }, @@ -289,72 +301,7 @@ const DocumentCard = memo( DocumentCard.displayName = "DocumentCard" -function getAbsoluteUrl(url: string): string { - try { - const urlObj = new URL(url) - return urlObj.host.replace(/^www\./, "") - } catch { - const match = url.match(/^https?:\/\/([^\/]+)/) - const host = match?.[1] ?? url.replace(/^https?:\/\//, "") - return host.replace(/^www\./, "") - } -} - -function SyncLogoIcon({ className }: { className?: string }) { - return ( - - - - - - - - - - - - - - - - - - - - - - ) -} - function ContentPreview({ document }: { document: DocumentWithMemories }) { - // Check for Google Docs if ( document.url?.includes("https://docs.googleapis.com/v1/documents") || document.url?.includes("docs.google.com/document") || @@ -363,7 +310,6 @@ function ContentPreview({ document }: { document: DocumentWithMemories }) { return } - // Check for Twitter if ( document.url?.includes("x.com/") && document.metadata?.sm_internal_twitter_metadata @@ -377,6 +323,10 @@ function ContentPreview({ document }: { document: DocumentWithMemories }) { ) } + if (document.source === "mcp") { + return + } + if (isYouTubeUrl(document.url)) { return } @@ -390,7 +340,6 @@ function ContentPreview({ document }: { document: DocumentWithMemories }) { return } - // Check for Website if (document.url?.includes("https://")) { return } diff --git a/apps/web/components/new/document-cards/youtube-utils.ts b/apps/web/components/new/utils.ts similarity index 76% rename from apps/web/components/new/document-cards/youtube-utils.ts rename to apps/web/components/new/utils.ts index 9ac1e508..b830a2a3 100644 --- a/apps/web/components/new/document-cards/youtube-utils.ts +++ b/apps/web/components/new/utils.ts @@ -11,7 +11,9 @@ export function isYouTubeUrl(url: string | undefined | null): boolean { ) } -export function extractYouTubeVideoId(url: string | undefined | null): string | null { +export function extractYouTubeVideoId( + url: string | undefined | null, +): string | null { if (!url) return null // Handle youtu.be format @@ -27,7 +29,9 @@ export function extractYouTubeVideoId(url: string | undefined | null): string | if (embedMatch?.[1]) return embedMatch[1] // Handle m.youtube.com format - const mobileMatch = url.match(/(?:m\.youtube\.com\/watch\?v=)([a-zA-Z0-9_-]{11})/) + const mobileMatch = url.match( + /(?:m\.youtube\.com\/watch\?v=)([a-zA-Z0-9_-]{11})/, + ) if (mobileMatch?.[1]) return mobileMatch[1] return null @@ -61,3 +65,14 @@ export function useYouTubeChannelName(url: string | undefined | null) { }) } + +export function getAbsoluteUrl(url: string): string { + try { + const urlObj = new URL(url) + return urlObj.host.replace(/^www\./, "") + } catch { + const match = url.match(/^https?:\/\/([^\/]+)/) + const host = match?.[1] ?? url.replace(/^https?:\/\//, "") + return host.replace(/^www\./, "") + } +} diff --git a/biome.json b/biome.json index 0c9043b5..ea4876ba 100644 --- a/biome.json +++ b/biome.json @@ -81,13 +81,6 @@ "useNumberNamespace": "error", "useSelfClosingElements": "error", "useSingleVarDeclarator": "error" - }, - "suspicious": { - "noUnknownAtRules": { - "options": { - "ignore": ["source", "theme", "apply"] - } - } } } }, diff --git a/packages/ui/assets/icons.tsx b/packages/ui/assets/icons.tsx index ef24567a..c8c8dcc5 100644 --- a/packages/ui/assets/icons.tsx +++ b/packages/ui/assets/icons.tsx @@ -206,3 +206,159 @@ export const PDF = ({ className }: { className?: string }) => ( /> ); + +export const SyncLogoIcon = ({ className }: { className?: string }) => { + return ( + + Sync Logo + + + + + + + + + + + + + + + + + + + + + ); +}; + +export const MCPIcon = ({ className }: { className?: string }) => { + return ( + + Model Context Protocol + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export const ClaudeDesktopIcon = ({ className }: { className?: string }) => { + return ( + + Claude Desktop + + + + + + + + + ); +};