From 9e1e392a2b923c9d00e12c0fd27586d9d5b29daf Mon Sep 17 00:00:00 2001 From: Mahesh Sanikommu Date: Thu, 21 Aug 2025 08:40:44 -0700 Subject: [PATCH] Enhanced Text Editor and format fixes with biome (#371) --- apps/web/app/(auth)/login/page.tsx | 4 +- apps/web/app/layout.tsx | 38 +- apps/web/app/manifest.ts | 38 +- apps/web/app/not-found.tsx | 20 +- apps/web/app/page.tsx | 536 ++++--- apps/web/app/ref/[code]/page.tsx | 128 +- apps/web/app/ref/page.tsx | 41 +- apps/web/app/waitlist/page.tsx | 117 +- apps/web/biome.json | 11 +- apps/web/button.tsx | 18 +- apps/web/components/connect-ai-modal.tsx | 135 +- apps/web/components/create-project-dialog.tsx | 63 +- apps/web/components/glass-menu-effect.tsx | 8 +- apps/web/components/install-prompt.tsx | 244 +-- apps/web/components/memory-list-view.tsx | 428 ++--- apps/web/components/menu.tsx | 373 ++--- apps/web/components/project-selector.tsx | 212 +-- .../web/components/referral-upgrade-modal.tsx | 122 +- apps/web/components/spinner.tsx | 6 +- apps/web/components/text-shimmer.tsx | 92 +- apps/web/components/tour.tsx | 219 +-- apps/web/components/views/add-memory.tsx | 1425 ----------------- .../views/add-memory/action-buttons.tsx | 67 + .../web/components/views/add-memory/index.tsx | 1205 ++++++++++++++ .../views/add-memory/memory-usage-ring.tsx | 53 + .../views/add-memory/project-selection.tsx | 94 ++ .../views/add-memory/tab-button.tsx | 28 + .../views/add-memory/text-editor.tsx | 545 +++++++ apps/web/components/views/billing.tsx | 72 +- .../components/views/chat/chat-messages.tsx | 704 ++++---- apps/web/components/views/chat/index.tsx | 262 +-- .../views/connections-tab-content.tsx | 618 +++---- apps/web/components/views/mcp/index.tsx | 116 +- .../views/mcp/installation-dialog-content.tsx | 28 +- apps/web/components/views/profile.tsx | 112 +- apps/web/components/views/projects.tsx | 161 +- apps/web/globals.css | 4 +- apps/web/hooks/use-project-mutations.ts | 66 +- apps/web/hooks/use-project-name.ts | 22 +- apps/web/hooks/use-resize-observer.ts | 30 +- apps/web/instrumentation-client.ts | 6 +- apps/web/lib/analytics.ts | 24 +- apps/web/lib/mobile-panel-context.tsx | 22 +- apps/web/lib/tour-constants.ts | 4 +- apps/web/lib/view-mode-context.tsx | 80 +- apps/web/middleware.ts | 42 +- apps/web/next.config.ts | 15 +- apps/web/open-next.config.ts | 4 +- apps/web/package.json | 184 +-- apps/web/postcss.config.mjs | 4 +- apps/web/stores/chat.ts | 326 ++-- apps/web/stores/highlights.ts | 53 +- apps/web/stores/index.ts | 108 +- biome.json | 75 + bun.lockb | Bin 740848 -> 746024 bytes package.json | 126 +- packages/eslint-config/base.js | 50 +- packages/eslint-config/next.js | 84 +- packages/eslint-config/package.json | 44 +- packages/eslint-config/react-internal.js | 66 +- packages/hooks/package.json | 8 +- packages/lib/api.ts | 16 +- packages/lib/package.json | 20 +- packages/typescript-config/base.json | 34 +- packages/typescript-config/nextjs.json | 20 +- packages/typescript-config/package.json | 14 +- packages/typescript-config/react-library.json | 10 +- packages/ui/assets/Logo.tsx | 16 +- packages/ui/assets/icons.tsx | 24 +- packages/ui/biome.json | 10 +- packages/ui/button/external-auth.tsx | 10 +- packages/ui/components/accordion.tsx | 20 +- packages/ui/components/alert-dialog.tsx | 34 +- packages/ui/components/avatar.tsx | 16 +- packages/ui/components/badge.tsx | 16 +- packages/ui/components/breadcrumb.tsx | 28 +- packages/ui/components/button.tsx | 18 +- packages/ui/components/card.tsx | 20 +- packages/ui/components/carousel.tsx | 124 +- packages/ui/components/chart.tsx | 130 +- packages/ui/components/checkbox.tsx | 14 +- packages/ui/components/collapsible.tsx | 12 +- packages/ui/components/combobox.tsx | 78 +- packages/ui/components/command.tsx | 40 +- packages/ui/components/dialog.tsx | 34 +- packages/ui/components/drawer.tsx | 30 +- packages/ui/components/dropdown-menu.tsx | 50 +- packages/ui/components/input.tsx | 8 +- packages/ui/components/label.tsx | 12 +- packages/ui/components/popover.tsx | 18 +- packages/ui/components/progress.tsx | 12 +- packages/ui/components/scroll-area.tsx | 14 +- packages/ui/components/select.tsx | 34 +- packages/ui/components/separator.tsx | 12 +- packages/ui/components/shadcn-io/dropzone.tsx | 122 +- packages/ui/components/sheet.tsx | 34 +- packages/ui/components/sidebar.tsx | 216 +-- packages/ui/components/skeleton.tsx | 6 +- packages/ui/components/sonner.tsx | 14 +- packages/ui/components/table.tsx | 24 +- packages/ui/components/tabs.tsx | 18 +- packages/ui/components/text-separator.tsx | 6 +- packages/ui/components/textarea.tsx | 8 +- packages/ui/components/toggle-group.tsx | 22 +- packages/ui/components/toggle.tsx | 16 +- packages/ui/components/tooltip.tsx | 18 +- packages/ui/copy-button.tsx | 32 +- packages/ui/copyable-cell.tsx | 36 +- packages/ui/input/labeled-input.tsx | 18 +- packages/ui/memory-graph/constants.ts | 8 +- packages/ui/memory-graph/controls.tsx | 28 +- packages/ui/memory-graph/graph-canvas.tsx | 588 +++---- .../ui/memory-graph/graph-webgl-canvas.tsx | 627 ++++---- .../ui/memory-graph/hooks/use-graph-data.ts | 207 +-- .../hooks/use-graph-interactions.ts | 262 +-- packages/ui/memory-graph/legend.tsx | 137 +- .../ui/memory-graph/loading-indicator.tsx | 24 +- packages/ui/memory-graph/memory-graph.tsx | 179 ++- .../ui/memory-graph/node-detail-panel.tsx | 94 +- packages/ui/memory-graph/spaces-dropdown.tsx | 43 +- packages/ui/memory-graph/types.ts | 166 +- packages/ui/other/anonymous-auth.tsx | 90 +- packages/ui/other/glass-effect.tsx | 6 +- packages/ui/package.json | 70 +- packages/ui/pages/login.tsx | 122 +- packages/ui/text/heading/heading-h1-bold.tsx | 8 +- .../ui/text/heading/heading-h1-medium.tsx | 8 +- packages/ui/text/heading/heading-h2-bold.tsx | 8 +- .../ui/text/heading/heading-h2-medium.tsx | 8 +- packages/ui/text/heading/heading-h3-bold.tsx | 8 +- .../ui/text/heading/heading-h3-medium.tsx | 8 +- packages/ui/text/heading/heading-h4-bold.tsx | 8 +- .../ui/text/heading/heading-h4-medium.tsx | 8 +- packages/ui/text/label/label-1-medium.tsx | 8 +- packages/ui/text/label/label-1-regular.tsx | 8 +- packages/ui/text/label/label-2-medium.tsx | 8 +- packages/ui/text/label/label-2-regular.tsx | 8 +- packages/ui/text/label/label-3-medium.tsx | 8 +- packages/ui/text/label/label-3-regular.tsx | 8 +- packages/ui/text/title/title-1-bold.tsx | 8 +- packages/ui/text/title/title-1-medium.tsx | 8 +- packages/ui/text/title/title-2-bold.tsx | 8 +- packages/ui/text/title/title-2-medium.tsx | 8 +- packages/ui/text/title/title-3-bold.tsx | 8 +- packages/ui/text/title/title-3-medium.tsx | 8 +- packages/validation/api.ts | 274 ++-- packages/validation/package.json | 10 +- packages/validation/schemas.ts | 83 +- turbo.json | 38 +- 149 files changed, 7812 insertions(+), 6732 deletions(-) delete mode 100644 apps/web/components/views/add-memory.tsx create mode 100644 apps/web/components/views/add-memory/action-buttons.tsx create mode 100644 apps/web/components/views/add-memory/index.tsx create mode 100644 apps/web/components/views/add-memory/memory-usage-ring.tsx create mode 100644 apps/web/components/views/add-memory/project-selection.tsx create mode 100644 apps/web/components/views/add-memory/tab-button.tsx create mode 100644 apps/web/components/views/add-memory/text-editor.tsx create mode 100644 biome.json diff --git a/apps/web/app/(auth)/login/page.tsx b/apps/web/app/(auth)/login/page.tsx index e2f72129..df052a3d 100644 --- a/apps/web/app/(auth)/login/page.tsx +++ b/apps/web/app/(auth)/login/page.tsx @@ -1,4 +1,4 @@ -import { LoginPage } from "@repo/ui/pages/login" +import { LoginPage } from "@repo/ui/pages/login"; export default function Page() { return ( @@ -8,5 +8,5 @@ export default function Page() { "Private, secure, and reliable.", ]} /> - ) + ); } diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx index 095763b3..e6d9094d 100644 --- a/apps/web/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -1,39 +1,39 @@ -import type { Metadata } from "next" -import { JetBrains_Mono, Inter } from "next/font/google" -import "../globals.css" -import "@ui/globals.css" -import { AuthProvider } from "@lib/auth-context" -import { ErrorTrackingProvider } from "@lib/error-tracking" -import { PostHogProvider } from "@lib/posthog" -import { QueryProvider } from "@lib/query-client" -import { AutumnProvider } from "autumn-js/react" -import { Suspense } from "react" -import { Toaster } from "sonner" -import { TourProvider } from "@/components/tour" -import { MobilePanelProvider } from "@/lib/mobile-panel-context" +import type { Metadata } from "next"; +import { Inter, JetBrains_Mono } from "next/font/google"; +import "../globals.css"; +import "@ui/globals.css"; +import { AuthProvider } from "@lib/auth-context"; +import { ErrorTrackingProvider } from "@lib/error-tracking"; +import { PostHogProvider } from "@lib/posthog"; +import { QueryProvider } from "@lib/query-client"; +import { AutumnProvider } from "autumn-js/react"; +import { Suspense } from "react"; +import { Toaster } from "sonner"; +import { TourProvider } from "@/components/tour"; +import { MobilePanelProvider } from "@/lib/mobile-panel-context"; -import { ViewModeProvider } from "@/lib/view-mode-context" +import { ViewModeProvider } from "@/lib/view-mode-context"; const sans = Inter({ subsets: ["latin"], variable: "--font-sans", -}) +}); const mono = JetBrains_Mono({ subsets: ["latin"], variable: "--font-mono", -}) +}); export const metadata: Metadata = { metadataBase: new URL("https://app.supermemory.ai"), description: "Your memories, wherever you are", title: "supermemory app", -} +}; export default function RootLayout({ children, }: Readonly<{ - children: React.ReactNode + children: React.ReactNode; }>) { return ( @@ -65,5 +65,5 @@ export default function RootLayout({ - ) + ); } diff --git a/apps/web/app/manifest.ts b/apps/web/app/manifest.ts index 17611deb..9077d287 100644 --- a/apps/web/app/manifest.ts +++ b/apps/web/app/manifest.ts @@ -1,20 +1,20 @@ -import type { MetadataRoute } from 'next' - +import type { MetadataRoute } from "next"; + export default function manifest(): MetadataRoute.Manifest { - return { - name: 'Supermemory', - short_name: 'supermemory', - description: 'Your memories, wherever you are', - start_url: '/', - display: 'standalone', - background_color: '#ffffff', - theme_color: '#000000', - icons: [ - { - src: '/images/logo.png', - sizes: '192x192', - type: 'image/png', - } - ], - } -} \ No newline at end of file + return { + name: "Supermemory", + short_name: "supermemory", + description: "Your memories, wherever you are", + start_url: "/", + display: "standalone", + background_color: "#ffffff", + theme_color: "#000000", + icons: [ + { + src: "/images/logo.png", + sizes: "192x192", + type: "image/png", + }, + ], + }; +} diff --git a/apps/web/app/not-found.tsx b/apps/web/app/not-found.tsx index 98cf6b58..d37d1e7c 100644 --- a/apps/web/app/not-found.tsx +++ b/apps/web/app/not-found.tsx @@ -1,20 +1,20 @@ -"use client" // Error boundaries must be Client Components +"use client"; // Error boundaries must be Client Components -import { Button } from "@ui/components/button" -import { Title1Bold } from "@ui/text/title/title-1-bold" -import { useRouter } from "next/navigation" -import { useEffect } from "react" +import { Button } from "@ui/components/button"; +import { Title1Bold } from "@ui/text/title/title-1-bold"; +import { useRouter } from "next/navigation"; +import { useEffect } from "react"; export default function NotFound({ error, }: { - error: Error & { digest?: string } + error: Error & { digest?: string }; }) { - const router = useRouter() + const router = useRouter(); useEffect(() => { // Log the error to an error reporting service - console.error(error) - }, [error]) + console.error(error); + }, [error]); return ( @@ -23,5 +23,5 @@ export default function NotFound({ - ) + ); } diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx index 05b32a15..335d79e3 100644 --- a/apps/web/app/page.tsx +++ b/apps/web/app/page.tsx @@ -1,4 +1,4 @@ -"use client" +"use client"; import { useIsMobile } from "@hooks/use-mobile"; import { useAuth } from "@lib/auth-context"; @@ -7,32 +7,38 @@ import { MemoryGraph } from "@repo/ui/memory-graph"; import type { DocumentsWithMemoriesResponseSchema } from "@repo/validation/api"; import { useInfiniteQuery, useQuery } from "@tanstack/react-query"; import { Logo, LogoFull } from "@ui/assets/Logo"; -import { GlassMenuEffect } from "@ui/other/glass-effect"; import { Button } from "@ui/components/button"; -import { Gift, LayoutGrid, List, LoaderIcon, MessageSquare, Unplug } from "lucide-react"; +import { GlassMenuEffect } from "@ui/other/glass-effect"; +import { + Gift, + LayoutGrid, + List, + LoaderIcon, + MessageSquare, + Unplug, +} from "lucide-react"; import { AnimatePresence, motion } from "motion/react"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { useCallback, useEffect, useMemo, useState } from "react"; import type { z } from "zod"; -import { MemoryListView } from "@/components/memory-list-view"; -import Menu from "@/components/menu"; -import type { TourStep } from "@/components/tour"; -import { TourAlertDialog, useTour } from "@/components/tour"; -import { useProject } from "@/stores"; -import { TOUR_STEP_IDS, TOUR_STORAGE_KEY } from "@/lib/tour-constants"; -import { useViewMode } from "@/lib/view-mode-context"; -import { useChatOpen } from "@/stores"; -import { ChatRewrite } from "@/components/views/chat"; -import { useGraphHighlights } from "@/stores/highlights"; -import { ProjectSelector } from "@/components/project-selector"; -import { AddMemoryView } from "@/components/views/add-memory"; -import { ReferralUpgradeModal } from "@/components/referral-upgrade-modal"; import { ConnectAIModal } from "@/components/connect-ai-modal"; import { InstallPrompt } from "@/components/install-prompt"; +import { MemoryListView } from "@/components/memory-list-view"; +import Menu from "@/components/menu"; +import { ProjectSelector } from "@/components/project-selector"; +import { ReferralUpgradeModal } from "@/components/referral-upgrade-modal"; +import type { TourStep } from "@/components/tour"; +import { TourAlertDialog, useTour } from "@/components/tour"; +import { AddMemoryView } from "@/components/views/add-memory"; +import { ChatRewrite } from "@/components/views/chat"; +import { TOUR_STEP_IDS, TOUR_STORAGE_KEY } from "@/lib/tour-constants"; +import { useViewMode } from "@/lib/view-mode-context"; +import { useChatOpen, useProject } from "@/stores"; +import { useGraphHighlights } from "@/stores/highlights"; -type DocumentsResponse = z.infer -type DocumentWithMemories = DocumentsResponse["documents"][0] +type DocumentsResponse = z.infer; +type DocumentWithMemories = DocumentsResponse["documents"][0]; const MemoryGraphPage = () => { const { documentIds: allHighlightDocumentIds } = useGraphHighlights(); @@ -41,26 +47,26 @@ const MemoryGraphPage = () => { const { selectedProject } = useProject(); const { setSteps, isTourCompleted } = useTour(); const { isOpen, setIsOpen } = useChatOpen(); - const [injectedDocs, setInjectedDocs] = useState([]) - const [showAddMemoryView, setShowAddMemoryView] = useState(false) - const [showReferralModal, setShowReferralModal] = useState(false) + const [injectedDocs, setInjectedDocs] = useState([]); + const [showAddMemoryView, setShowAddMemoryView] = useState(false); + const [showReferralModal, setShowReferralModal] = useState(false); // Fetch projects meta to detect experimental flag const { data: projectsMeta = [] } = useQuery({ queryKey: ["projects"], queryFn: async () => { - const response = await $fetch("@get/projects") - return response.data?.projects ?? [] + const response = await $fetch("@get/projects"); + return response.data?.projects ?? []; }, staleTime: 5 * 60 * 1000, - }) + }); const isCurrentProjectExperimental = !!projectsMeta.find( (p: any) => p.containerTag === selectedProject, - )?.isExperimental + )?.isExperimental; // Tour state - const [showTourDialog, setShowTourDialog] = useState(false) + const [showTourDialog, setShowTourDialog] = useState(false); // Define tour steps with useMemo to prevent recreation const tourSteps: TourStep[] = useMemo(() => { @@ -193,36 +199,36 @@ const MemoryGraphPage = () => { selectorId: TOUR_STEP_IDS.FLOATING_CHAT, position: "left", }, - ] - }, []) + ]; + }, []); // Check if tour has been completed before useEffect(() => { - const hasCompletedTour = localStorage.getItem(TOUR_STORAGE_KEY) === "true" + const hasCompletedTour = localStorage.getItem(TOUR_STORAGE_KEY) === "true"; if (!hasCompletedTour && !isTourCompleted) { const timer = setTimeout(() => { - setShowTourDialog(true) - }, 1000) // Show after 1 second - return () => clearTimeout(timer) + setShowTourDialog(true); + }, 1000); // Show after 1 second + return () => clearTimeout(timer); } - }, [isTourCompleted]) + }, [isTourCompleted]); // Set up tour steps useEffect(() => { - setSteps(tourSteps) - }, [setSteps, tourSteps]) + setSteps(tourSteps); + }, [setSteps, tourSteps]); // Save tour completion to localStorage useEffect(() => { if (isTourCompleted) { - localStorage.setItem(TOUR_STORAGE_KEY, "true") + localStorage.setItem(TOUR_STORAGE_KEY, "true"); } - }, [isTourCompleted]) + }, [isTourCompleted]); // Progressive loading via useInfiniteQuery - const IS_DEV = process.env.NODE_ENV === "development" - const PAGE_SIZE = IS_DEV ? 3 : 100 - const MAX_TOTAL = 1000 + const IS_DEV = process.env.NODE_ENV === "development"; + const PAGE_SIZE = IS_DEV ? 3 : 100; + const MAX_TOTAL = 1000; const { data, @@ -244,69 +250,76 @@ const MemoryGraphPage = () => { containerTags: selectedProject ? [selectedProject] : undefined, }, disableValidation: true, - }) + }); if (response.error) { - throw new Error(response.error?.message || "Failed to fetch documents") + throw new Error(response.error?.message || "Failed to fetch documents"); } - return response.data + return response.data; }, getNextPageParam: (lastPage, allPages) => { const loaded = allPages.reduce( (acc, p) => acc + (p.documents?.length ?? 0), 0, - ) - if (loaded >= MAX_TOTAL) return undefined + ); + if (loaded >= MAX_TOTAL) return undefined; - const { currentPage, totalPages } = lastPage.pagination + const { currentPage, totalPages } = lastPage.pagination; if (currentPage < totalPages) { - return currentPage + 1 + return currentPage + 1; } - return undefined + return undefined; }, staleTime: 5 * 60 * 1000, - }) + }); const baseDocuments = useMemo(() => { - return data?.pages.flatMap((p: DocumentsResponse) => p.documents ?? []) ?? [] - }, [data]) + return ( + data?.pages.flatMap((p: DocumentsResponse) => p.documents ?? []) ?? [] + ); + }, [data]); const allDocuments = useMemo(() => { - if (injectedDocs.length === 0) return baseDocuments - const byId = new Map() - for (const d of injectedDocs) byId.set(d.id, d) - for (const d of baseDocuments) if (!byId.has(d.id)) byId.set(d.id, d) - return Array.from(byId.values()) - }, [baseDocuments, injectedDocs]) + if (injectedDocs.length === 0) return baseDocuments; + const byId = new Map(); + for (const d of injectedDocs) byId.set(d.id, d); + for (const d of baseDocuments) if (!byId.has(d.id)) byId.set(d.id, d); + return Array.from(byId.values()); + }, [baseDocuments, injectedDocs]); - const totalLoaded = allDocuments.length - const hasMore = hasNextPage - const isLoadingMore = isFetchingNextPage + const totalLoaded = allDocuments.length; + const hasMore = hasNextPage; + const isLoadingMore = isFetchingNextPage; const loadMoreDocuments = useCallback(async (): Promise => { if (hasNextPage && !isFetchingNextPage) { - await fetchNextPage() - return + await fetchNextPage(); + return; } - return - }, [hasNextPage, isFetchingNextPage, fetchNextPage]) + return; + }, [hasNextPage, isFetchingNextPage, fetchNextPage]); // Reset injected docs when project changes - useEffect(() => { setInjectedDocs([]) }, [selectedProject]) + useEffect(() => { + setInjectedDocs([]); + }, [selectedProject]); // Surgical fetch of missing highlighted documents (customId-based IDs from search) useEffect(() => { - if (!isOpen) return - if (!allHighlightDocumentIds || allHighlightDocumentIds.length === 0) return - const present = new Set() + if (!isOpen) return; + if (!allHighlightDocumentIds || allHighlightDocumentIds.length === 0) + return; + const present = new Set(); for (const d of [...baseDocuments, ...injectedDocs]) { - if (d.id) present.add(d.id) - if ((d as any).customId) present.add((d as any).customId as string) + if (d.id) present.add(d.id); + if ((d as any).customId) present.add((d as any).customId as string); } - const missing = allHighlightDocumentIds.filter((id: string) => !present.has(id)) - if (missing.length === 0) return - let cancelled = false + const missing = allHighlightDocumentIds.filter( + (id: string) => !present.has(id), + ); + if (missing.length === 0) return; + let cancelled = false; const run = async () => { try { const resp = await $fetch("@post/memories/documents/by-ids", { @@ -316,49 +329,63 @@ const MemoryGraphPage = () => { containerTags: selectedProject ? [selectedProject] : undefined, }, disableValidation: true, - }) - if (cancelled || (resp as any)?.error) return - const extraDocs = (resp as any)?.data?.documents as DocumentWithMemories[] | undefined - if (!extraDocs || extraDocs.length === 0) return + }); + if (cancelled || (resp as any)?.error) return; + const extraDocs = (resp as any)?.data?.documents as + | DocumentWithMemories[] + | undefined; + if (!extraDocs || extraDocs.length === 0) return; setInjectedDocs((prev) => { - const seen = new Set([...prev.map((d) => d.id), ...baseDocuments.map((d) => d.id)]) - const merged = [...prev] + const seen = new Set([ + ...prev.map((d) => d.id), + ...baseDocuments.map((d) => d.id), + ]); + const merged = [...prev]; for (const doc of extraDocs) { if (!seen.has(doc.id)) { - merged.push(doc) - seen.add(doc.id) + merged.push(doc); + seen.add(doc.id); } } - return merged - }) - } catch { } - } - void run() - return () => { cancelled = true } - }, [isOpen, allHighlightDocumentIds.join('|'), baseDocuments, injectedDocs, selectedProject, $fetch]) + return merged; + }); + } catch {} + }; + void run(); + return () => { + cancelled = true; + }; + }, [ + isOpen, + allHighlightDocumentIds.join("|"), + baseDocuments, + injectedDocs, + selectedProject, + $fetch, + ]); // Handle view mode change const handleViewModeChange = useCallback( (mode: "graph" | "list") => { - setViewMode(mode) + setViewMode(mode); }, [setViewMode], - ) + ); // Prevent body scrolling useEffect(() => { - document.body.style.overflow = 'hidden' - document.body.style.height = '100vh' - document.documentElement.style.overflow = 'hidden' - document.documentElement.style.height = '100vh' - + document.body.style.overflow = "hidden"; + document.body.style.height = "100vh"; + document.documentElement.style.overflow = "hidden"; + document.documentElement.style.height = "100vh"; + return () => { - document.body.style.overflow = '' - document.body.style.height = '' - document.documentElement.style.overflow = '' - document.documentElement.style.height = '' - } - }, []) + document.body.style.overflow = ""; + document.body.style.height = ""; + document.documentElement.style.overflow = ""; + document.documentElement.style.height = ""; + }; + }, []); return (
@@ -409,52 +436,52 @@ const MemoryGraphPage = () => { - handleViewModeChange("list")} - transition={{ duration: 0.2 }} - whileHover={{ scale: 1.02 }} - whileTap={{ scale: 0.98 }} - > - {viewMode === "list" && ( - - )} - - - List - - -
- + handleViewModeChange("list")} + transition={{ duration: 0.2 }} + whileHover={{ scale: 1.02 }} + whileTap={{ scale: 0.98 }} + > + {viewMode === "list" && ( + + )} + + + List + + + + - {/* Animated content switching */} - - {viewMode === "graph" ? ( - - + {viewMode === "graph" ? ( + + { occludedRightPx={isOpen && !isMobile ? 600 : 0} autoLoadOnViewport={false} isExperimental={isCurrentProjectExperimental} - > -
-
-
-

No Memories to Visualize

- + > +
+
+
+

+ No Memories to Visualize +

+ +
-
- - - ) : ( - - + + ) : ( + -
-
-
-

No Memories to Visualize

- + +
+
+
+

+ No Memories to Visualize +

+ +
-
- - - )} - + + + )} + - {/* Top Bar */} -
-
- - - - + {/* Top Bar */} +
+
+ + + + -
- +
+ +
+ + + +
- - - +
+ +
-
- -
-
- {/* Floating Open Chat Button */} {!isOpen && !isMobile && ( { id={TOUR_STEP_IDS.FLOATING_CHAT} > { type: "spring", stiffness: 500, damping: 40, - }}> + }} + > @@ -623,18 +660,18 @@ const MemoryGraphPage = () => { {/* Referral/Upgrade Modal */} - setShowReferralModal(false)} + setShowReferralModal(false)} />
- ) -} + ); +}; // Wrapper component to handle auth and waitlist checks export default function Page() { - const router = useRouter() - const { user } = useAuth() + const router = useRouter(); + const { user } = useAuth(); // Check waitlist status const { @@ -645,24 +682,24 @@ export default function Page() { queryKey: ["waitlist-status", user?.id], queryFn: async () => { try { - const response = await $fetch("@get/waitlist/status") - return response.data + const response = await $fetch("@get/waitlist/status"); + return response.data; } catch (error) { - console.error("Error checking waitlist status:", error) + console.error("Error checking waitlist status:", error); // Return null to indicate error, will handle in useEffect - return null + return null; } }, enabled: !!user && !user.isAnonymous, staleTime: 5 * 60 * 1000, // 5 minutes retry: 1, // Only retry once on failure - }) + }); useEffect(() => { if (waitlistStatus && !waitlistStatus.accessGranted) { - router.push("/waitlist") + router.push("/waitlist"); } - }, []) + }, []); // Show loading state while checking authentication and waitlist status if (!user || isCheckingWaitlist) { @@ -673,9 +710,14 @@ export default function Page() {

Loading...

- ) + ); } // If we have a user and they have access, show the main component - return <>; + return ( + <> + + + + ); } diff --git a/apps/web/app/ref/[code]/page.tsx b/apps/web/app/ref/[code]/page.tsx index d1456152..3acbe657 100644 --- a/apps/web/app/ref/[code]/page.tsx +++ b/apps/web/app/ref/[code]/page.tsx @@ -1,41 +1,41 @@ -"use client" +"use client"; -import { $fetch } from "@lib/api" -import { Button } from "@ui/components/button" +import { $fetch } from "@lib/api"; +import { Button } from "@ui/components/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, -} from "@ui/components/card" -import { CheckIcon, CopyIcon, LoaderIcon, ShareIcon } from "lucide-react" -import Link from "next/link" -import { useRouter, useParams } from "next/navigation" -import { useEffect, useState } from "react" -import { toast } from "sonner" +} from "@ui/components/card"; +import { CheckIcon, CopyIcon, LoaderIcon, ShareIcon } from "lucide-react"; +import Link from "next/link"; +import { useParams, useRouter } from "next/navigation"; +import { useEffect, useState } from "react"; +import { toast } from "sonner"; export default function ReferralPage() { - const router = useRouter() - const params = useParams() - const referralCode = params.code as string - - const [isJoiningWaitlist, setIsJoiningWaitlist] = useState(false) - const [isLoading, setIsLoading] = useState(true) - const [referralData, setReferralData] = useState<{ - referrerName?: string - valid: boolean - } | null>(null) - const [copiedLink, setCopiedLink] = useState(false) + const router = useRouter(); + const params = useParams(); + const referralCode = params.code as string; - const referralLink = `https://supermemory.ai/ref/${referralCode}` + const [isJoiningWaitlist, setIsJoiningWaitlist] = useState(false); + const [isLoading, setIsLoading] = useState(true); + const [referralData, setReferralData] = useState<{ + referrerName?: string; + valid: boolean; + } | null>(null); + const [copiedLink, setCopiedLink] = useState(false); + + const referralLink = `https://supermemory.ai/ref/${referralCode}`; // Verify referral code and get referrer info useEffect(() => { async function checkReferral() { if (!referralCode) { - setIsLoading(false) - return + setIsLoading(false); + return; } try { @@ -43,42 +43,42 @@ export default function ReferralPage() { // For now, we'll assume it's valid - in the future this should call an API setReferralData({ valid: true, - referrerName: "A supermemory user" // Placeholder - should come from API - }) + referrerName: "A supermemory user", // Placeholder - should come from API + }); } catch (error) { - console.error("Error checking referral:", error) - setReferralData({ valid: false }) + console.error("Error checking referral:", error); + setReferralData({ valid: false }); } finally { - setIsLoading(false) + setIsLoading(false); } } - checkReferral() - }, [referralCode]) + checkReferral(); + }, [referralCode]); const handleJoinWaitlist = async () => { - setIsJoiningWaitlist(true) + setIsJoiningWaitlist(true); try { // Redirect to waitlist signup with referral code - router.push(`/waitlist?ref=${referralCode}`) + router.push(`/waitlist?ref=${referralCode}`); } catch (error) { - console.error("Error joining waitlist:", error) - toast.error("Failed to join waitlist. Please try again.") + console.error("Error joining waitlist:", error); + toast.error("Failed to join waitlist. Please try again."); } finally { - setIsJoiningWaitlist(false) + setIsJoiningWaitlist(false); } - } + }; const handleCopyLink = async () => { try { - await navigator.clipboard.writeText(referralLink) - setCopiedLink(true) - toast.success("Referral link copied!") - setTimeout(() => setCopiedLink(false), 2000) + await navigator.clipboard.writeText(referralLink); + setCopiedLink(true); + toast.success("Referral link copied!"); + setTimeout(() => setCopiedLink(false), 2000); } catch (error) { - toast.error("Failed to copy link") + toast.error("Failed to copy link"); } - } + }; const handleShare = () => { if (navigator.share) { @@ -86,11 +86,11 @@ export default function ReferralPage() { title: "Join supermemory", text: "I'm excited about supermemory - it's going to change how we store and interact with our memories!", url: referralLink, - }) + }); } else { - handleCopyLink() + handleCopyLink(); } - } + }; if (isLoading) { return ( @@ -100,7 +100,7 @@ export default function ReferralPage() {

Checking invitation...

- ) + ); } if (!referralData?.valid) { @@ -118,15 +118,13 @@ export default function ReferralPage() {
- ) + ); } return ( @@ -142,21 +140,25 @@ export default function ReferralPage() { You're invited to supermemory! - {referralData.referrerName} invited you to join the future of memory management. + {referralData.referrerName} invited you to join the future of + memory management.
-

What is supermemory?

+

+ What is supermemory? +

- supermemory is an AI-powered personal knowledge base that helps you store, - organize, and interact with all your digital memories - from documents - and links to conversations and ideas. + supermemory is an AI-powered personal knowledge base that + helps you store, organize, and interact with all your digital + memories - from documents and links to conversations and + ideas.

- -
- ) -} \ No newline at end of file + ); +} diff --git a/apps/web/app/ref/page.tsx b/apps/web/app/ref/page.tsx index 0a33ebba..38be47b0 100644 --- a/apps/web/app/ref/page.tsx +++ b/apps/web/app/ref/page.tsx @@ -1,15 +1,15 @@ -"use client" +"use client"; -import { Button } from "@ui/components/button" +import { Button } from "@ui/components/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, -} from "@ui/components/card" -import { ShareIcon } from "lucide-react" -import Link from "next/link" +} from "@ui/components/card"; +import { ShareIcon } from "lucide-react"; +import Link from "next/link"; export default function ReferralHomePage() { return ( @@ -23,28 +23,33 @@ export default function ReferralHomePage() { Missing Referral Code - It looks like you're missing a referral code. Get one from a friend or join directly! + It looks like you're missing a referral code. Get one from a friend + or join directly!
-

What is supermemory?

+

+ What is supermemory? +

- supermemory is an AI-powered personal knowledge base that helps you store, - organize, and interact with all your digital memories. + supermemory is an AI-powered personal knowledge base that helps + you store, organize, and interact with all your digital + memories.

- -
- Learn more about supermemory @@ -54,5 +59,5 @@ export default function ReferralHomePage() {
- ) -} \ No newline at end of file + ); +} diff --git a/apps/web/app/waitlist/page.tsx b/apps/web/app/waitlist/page.tsx index b493c813..5f591f21 100644 --- a/apps/web/app/waitlist/page.tsx +++ b/apps/web/app/waitlist/page.tsx @@ -1,115 +1,115 @@ -"use client" +"use client"; -import { $fetch } from "@lib/api" -import { authClient } from "@lib/auth" -import { useAuth } from "@lib/auth-context" -import { fetchConsumerProProduct } from "@lib/queries" -import { Button } from "@ui/components/button" +import { $fetch } from "@lib/api"; +import { authClient } from "@lib/auth"; +import { useAuth } from "@lib/auth-context"; +import { fetchConsumerProProduct } from "@lib/queries"; +import { Button } from "@ui/components/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, -} from "@ui/components/card" -import { useCustomer } from "autumn-js/react" -import { Clock, LoaderIcon, SkipForwardIcon, LogOut } from "lucide-react" -import Link from "next/link" -import { useRouter, useSearchParams } from "next/navigation" -import { useEffect, useState } from "react" -import { toast } from "sonner" +} from "@ui/components/card"; +import { useCustomer } from "autumn-js/react"; +import { Clock, LoaderIcon, LogOut, SkipForwardIcon } from "lucide-react"; +import Link from "next/link"; +import { useRouter, useSearchParams } from "next/navigation"; +import { useEffect, useState } from "react"; +import { toast } from "sonner"; export default function WaitlistPage() { - const router = useRouter() - const searchParams = useSearchParams() - const referralCode = searchParams.get('ref') - const { user } = useAuth() - const [isChecking, setIsChecking] = useState(true) - const [isSkippingWaitlist, setIsSkippingWaitlist] = useState(false) + const router = useRouter(); + const searchParams = useSearchParams(); + const referralCode = searchParams.get("ref"); + const { user } = useAuth(); + const [isChecking, setIsChecking] = useState(true); + const [isSkippingWaitlist, setIsSkippingWaitlist] = useState(false); const [waitlistStatus, setWaitlistStatus] = useState<{ - inWaitlist: boolean - accessGranted: boolean - createdAt: string - } | null>(null) - const autumn = useCustomer() + inWaitlist: boolean; + accessGranted: boolean; + createdAt: string; + } | null>(null); + const autumn = useCustomer(); - // @ts-ignore - const { data: earlyAccess } = fetchConsumerProProduct(autumn) + // @ts-expect-error + const { data: earlyAccess } = fetchConsumerProProduct(autumn); const handleSkipWaitlist = async () => { - setIsSkippingWaitlist(true) + setIsSkippingWaitlist(true); try { const res = await autumn.attach({ productId: "consumer_pro", forceCheckout: true, successUrl: "https://app.supermemory.ai/", - }) + }); if (res.data && "checkout_url" in res.data && res.data.checkout_url) { - router.push(res.data.checkout_url) + router.push(res.data.checkout_url); } } catch (error) { - console.error("Error skipping waitlist:", error) + console.error("Error skipping waitlist:", error); } finally { - setIsSkippingWaitlist(false) + setIsSkippingWaitlist(false); } - } + }; const handleLogout = async () => { try { - await authClient.signOut() - router.push("/") + await authClient.signOut(); + router.push("/"); } catch (error) { - console.error("Error signing out:", error) - toast.error("Failed to sign out") + console.error("Error signing out:", error); + toast.error("Failed to sign out"); } - } + }; useEffect(() => { async function checkAccess() { if (!user) { - router.push("/") - return + router.push("/"); + return; } // Anonymous users should sign in first if (user.isAnonymous) { - authClient.signOut() - router.push("/") - return + authClient.signOut(); + router.push("/"); + return; } try { // Check waitlist status using the new endpoint - const response = await $fetch("@get/waitlist/status") + const response = await $fetch("@get/waitlist/status"); if (response.data) { - setWaitlistStatus(response.data) + setWaitlistStatus(response.data); if (!response.data.inWaitlist) { - authClient.signOut() - router.push("/login") + authClient.signOut(); + router.push("/login"); } // If user has access, redirect to home if (response.data.accessGranted) { - router.push("/") + router.push("/"); } } } catch (error) { - console.error("Error checking waitlist status:", error) + console.error("Error checking waitlist status:", error); // If there's an error, assume user is on waitlist setWaitlistStatus({ inWaitlist: true, accessGranted: false, createdAt: new Date().toISOString(), - }) + }); } finally { - setIsChecking(false) + setIsChecking(false); } } - checkAccess() - }, [user, router]) + checkAccess(); + }, [user, router]); if (isChecking) { return ( @@ -119,7 +119,7 @@ export default function WaitlistPage() {

Checking access...

- ) + ); } return ( @@ -133,10 +133,9 @@ export default function WaitlistPage() { You're on the waitlist! - {referralCode + {referralCode ? "Thanks for joining through a friend's invitation! You've been added to the waitlist with priority access." - : "Thanks for your interest in supermemory. We'll notify you as soon as we're ready for you." - } + : "Thanks for your interest in supermemory. We'll notify you as soon as we're ready for you."} {referralCode && (
@@ -199,8 +198,8 @@ export default function WaitlistPage() {

Signed in as {user.email}

-
- ) + ); } diff --git a/apps/web/biome.json b/apps/web/biome.json index 79f38fb5..ea994ee6 100644 --- a/apps/web/biome.json +++ b/apps/web/biome.json @@ -1,10 +1,11 @@ { - "$schema": "https://biomejs.dev/schemas/2.1.2/schema.json", - "extends": "//", + "root": false, + "$schema": "https://biomejs.dev/schemas/2.2.0/schema.json", "linter": { - "domains": { - "next": "recommended", - "react": "recommended" + "rules": { + "nursery": { + "recommended": true + } } } } diff --git a/apps/web/button.tsx b/apps/web/button.tsx index 99d10e10..3a671e03 100644 --- a/apps/web/button.tsx +++ b/apps/web/button.tsx @@ -1,7 +1,7 @@ -import { cn } from "@lib/utils" -import { Slot } from "@radix-ui/react-slot" -import { cva, type VariantProps } from "class-variance-authority" -import type * as React from "react" +import { cn } from "@lib/utils"; +import { Slot } from "@radix-ui/react-slot"; +import { cva, type VariantProps } from "class-variance-authority"; +import type * as React from "react"; const buttonVariants = cva( "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-2 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", @@ -32,7 +32,7 @@ const buttonVariants = cva( size: "default", }, }, -) +); function Button({ className, @@ -42,9 +42,9 @@ function Button({ ...props }: React.ComponentProps<"button"> & VariantProps & { - asChild?: boolean + asChild?: boolean; }) { - const Comp = asChild ? Slot : "button" + const Comp = asChild ? Slot : "button"; return ( - ) + ); } -export { Button, buttonVariants } +export { Button, buttonVariants }; diff --git a/apps/web/components/connect-ai-modal.tsx b/apps/web/components/connect-ai-modal.tsx index ca5240cd..ff5f1064 100644 --- a/apps/web/components/connect-ai-modal.tsx +++ b/apps/web/components/connect-ai-modal.tsx @@ -1,7 +1,7 @@ -"use client" +"use client"; -import { useIsMobile } from "@hooks/use-mobile" -import { Button } from "@ui/components/button" +import { useIsMobile } from "@hooks/use-mobile"; +import { Button } from "@ui/components/button"; import { Dialog, DialogContent, @@ -9,20 +9,20 @@ import { DialogHeader, DialogTitle, DialogTrigger, -} from "@ui/components/dialog" -import { Input } from "@ui/components/input" +} from "@ui/components/dialog"; +import { Input } from "@ui/components/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, -} from "@ui/components/select" -import { CopyableCell } from "@ui/copyable-cell" -import { CopyIcon, ExternalLink } from "lucide-react" -import Image from "next/image" -import { useState } from "react" -import { toast } from "sonner" +} from "@ui/components/select"; +import { CopyableCell } from "@ui/copyable-cell"; +import { CopyIcon, ExternalLink } from "lucide-react"; +import Image from "next/image"; +import { useState } from "react"; +import { toast } from "sonner"; const clients = { cursor: "Cursor", @@ -34,23 +34,23 @@ const clients = { "roo-cline": "Roo Cline", witsy: "Witsy", enconvo: "Enconvo", -} as const +} as const; interface ConnectAIModalProps { - children: React.ReactNode + children: React.ReactNode; } export function ConnectAIModal({ children }: ConnectAIModalProps) { - const [client, setClient] = useState("cursor") - const [isOpen, setIsOpen] = useState(false) - const [showAllTools, setShowAllTools] = useState(false) - const isMobile = useIsMobile() - const installCommand = `npx -y install-mcp@latest https://api.supermemory.ai/mcp --client ${client} --oauth=yes` + const [client, setClient] = useState("cursor"); + const [isOpen, setIsOpen] = useState(false); + const [showAllTools, setShowAllTools] = useState(false); + const isMobile = useIsMobile(); + const installCommand = `npx -y install-mcp@latest https://api.supermemory.ai/mcp --client ${client} --oauth=yes`; const copyToClipboard = () => { - navigator.clipboard.writeText(installCommand) - toast.success("Copied to clipboard!") - } + navigator.clipboard.writeText(installCommand); + toast.success("Copied to clipboard!"); + }; return ( @@ -59,12 +59,13 @@ export function ConnectAIModal({ children }: ConnectAIModalProps) { Connect Supermemory to Your AI - Connect supermemory to your favorite AI tools using the Model Context Protocol (MCP). - This allows your AI assistant to create, search, and access your memories directly. + Connect supermemory to your favorite AI tools using the Model + Context Protocol (MCP). This allows your AI assistant to create, + search, and access your memories directly. -
+

- Click URL to copy to clipboard. Use this URL to configure supermemory in your AI assistant. + Click URL to copy to clipboard. Use this URL to configure + supermemory in your AI assistant.

- +

Supported AI Tools

@@ -102,12 +104,18 @@ export function ConnectAIModal({ children }: ConnectAIModalProps) { className={"rounded object-contain"} onError={(e) => { const target = e.target as HTMLImageElement; - target.style.display = 'none'; + target.style.display = "none"; const parent = target.parentElement; - if (parent && !parent.querySelector('.fallback-text')) { - const fallback = document.createElement('span'); - fallback.className = 'fallback-text text-xs font-bold text-muted-foreground'; - fallback.textContent = clientName.substring(0, 2).toUpperCase(); + if ( + parent && + !parent.querySelector(".fallback-text") + ) { + const fallback = document.createElement("span"); + fallback.className = + "fallback-text text-xs font-bold text-muted-foreground"; + fallback.textContent = clientName + .substring(0, 2) + .toUpperCase(); parent.appendChild(fallback); } }} @@ -136,7 +144,9 @@ export function ConnectAIModal({ children }: ConnectAIModalProps) {
- -
- - - -
+ +
+ + + +
@@ -203,17 +219,20 @@ export function ConnectAIModal({ children }: ConnectAIModalProps) {
- +
- ) + ); } diff --git a/apps/web/components/create-project-dialog.tsx b/apps/web/components/create-project-dialog.tsx index 904c3f2d..1672a689 100644 --- a/apps/web/components/create-project-dialog.tsx +++ b/apps/web/components/create-project-dialog.tsx @@ -1,6 +1,6 @@ -"use client" +"use client"; -import { Button } from "@repo/ui/components/button" +import { Button } from "@repo/ui/components/button"; import { Dialog, DialogContent, @@ -8,37 +8,40 @@ import { DialogFooter, DialogHeader, DialogTitle, -} from "@repo/ui/components/dialog" -import { Input } from "@repo/ui/components/input" -import { Label } from "@repo/ui/components/label" -import { Loader2 } from "lucide-react" -import { motion, AnimatePresence } from "motion/react" -import { useState } from "react" -import { useProjectMutations } from "@/hooks/use-project-mutations" +} from "@repo/ui/components/dialog"; +import { Input } from "@repo/ui/components/input"; +import { Label } from "@repo/ui/components/label"; +import { Loader2 } from "lucide-react"; +import { AnimatePresence, motion } from "motion/react"; +import { useState } from "react"; +import { useProjectMutations } from "@/hooks/use-project-mutations"; interface CreateProjectDialogProps { - open: boolean - onOpenChange: (open: boolean) => void + open: boolean; + onOpenChange: (open: boolean) => void; } -export function CreateProjectDialog({ open, onOpenChange }: CreateProjectDialogProps) { - const [projectName, setProjectName] = useState("") - const { createProjectMutation } = useProjectMutations() +export function CreateProjectDialog({ + open, + onOpenChange, +}: CreateProjectDialogProps) { + const [projectName, setProjectName] = useState(""); + const { createProjectMutation } = useProjectMutations(); const handleClose = () => { - onOpenChange(false) - setProjectName("") - } + onOpenChange(false); + setProjectName(""); + }; const handleCreate = () => { if (projectName.trim()) { createProjectMutation.mutate(projectName, { onSuccess: () => { - handleClose() - } - }) + handleClose(); + }, + }); } - } + }; return ( @@ -72,7 +75,7 @@ export function CreateProjectDialog({ open, onOpenChange }: CreateProjectDialogP onChange={(e) => setProjectName(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter" && projectName.trim()) { - handleCreate() + handleCreate(); } }} /> @@ -82,7 +85,10 @@ export function CreateProjectDialog({ open, onOpenChange }: CreateProjectDialogP
- + - - -

- Add Supermemory to your home screen for quick access and a better experience. -

- - {isIOS ? ( -
-

- 1. Tap the Share button in Safari -

-

- 2. Select "Add to Home Screen" ➕ -

- -
- ) : ( - - )} - -
-
- ) + const [isIOS, setIsIOS] = useState(false); + const [showPrompt, setShowPrompt] = useState(false); + const [deferredPrompt, setDeferredPrompt] = useState(null); + + useEffect(() => { + const isIOSDevice = + /iPad|iPhone|iPod/.test(navigator.userAgent) && !(window as any).MSStream; + const isInStandaloneMode = window.matchMedia( + "(display-mode: standalone)", + ).matches; + const hasSeenPrompt = + localStorage.getItem("install-prompt-dismissed") === "true"; + + setIsIOS(isIOSDevice); + + const isDevelopment = process.env.NODE_ENV === "development"; + setShowPrompt( + !hasSeenPrompt && + (isDevelopment || + (!isInStandaloneMode && + (isIOSDevice || "serviceWorker" in navigator))), + ); + + const handleBeforeInstallPrompt = (e: Event) => { + e.preventDefault(); + setDeferredPrompt(e); + if (!hasSeenPrompt) { + setShowPrompt(true); + } + }; + + window.addEventListener("beforeinstallprompt", handleBeforeInstallPrompt); + + return () => { + window.removeEventListener( + "beforeinstallprompt", + handleBeforeInstallPrompt, + ); + }; + }, []); + + const handleInstall = async () => { + if (deferredPrompt) { + deferredPrompt.prompt(); + const { outcome } = await deferredPrompt.userChoice; + if (outcome === "accepted") { + localStorage.setItem("install-prompt-dismissed", "true"); + setShowPrompt(false); + } + setDeferredPrompt(null); + } + }; + + const handleDismiss = () => { + localStorage.setItem("install-prompt-dismissed", "true"); + setShowPrompt(false); + }; + + if (!showPrompt) { + return null; + } + + return ( + + +
+
+
+
+ +
+

Install Supermemory

+
+ +
+ +

+ Add Supermemory to your home screen for quick access and a better + experience. +

+ + {isIOS ? ( +
+

+ 1. Tap the Share button in + Safari +

+

+ 2. Select "Add to Home Screen" ➕ +

+ +
+ ) : ( + + )} +
+
+
+ ); } diff --git a/apps/web/components/memory-list-view.tsx b/apps/web/components/memory-list-view.tsx index 654a3ad1..8269562a 100644 --- a/apps/web/components/memory-list-view.tsx +++ b/apps/web/components/memory-list-view.tsx @@ -1,5 +1,7 @@ -"use client" +"use client"; +import { useIsMobile } from "@hooks/use-mobile"; +import { cn } from "@lib/utils"; import { GoogleDocs, GoogleDrive, @@ -12,165 +14,178 @@ import { NotionDoc, OneDrive, PDF, -} from "@repo/ui/assets/icons" -import { Badge } from "@repo/ui/components/badge" -import { Card, CardContent, CardHeader } from "@repo/ui/components/card" - -import { - Sheet, - SheetContent, - SheetHeader, - SheetTitle, -} from "@repo/ui/components/sheet" +} from "@repo/ui/assets/icons"; +import { Badge } from "@repo/ui/components/badge"; +import { Card, CardContent, CardHeader } from "@repo/ui/components/card"; import { Drawer, DrawerContent, DrawerHeader, DrawerTitle, -} from "@repo/ui/components/drawer" -import { colors } from "@repo/ui/memory-graph/constants" -import type { DocumentsWithMemoriesResponseSchema } from "@repo/validation/api" -import { Label1Regular } from "@ui/text/label/label-1-regular" -import { Brain, Calendar, ExternalLink, FileText, Sparkles } from "lucide-react" -import { useVirtualizer } from "@tanstack/react-virtual" -import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react" -import type { z } from "zod" -import { analytics } from "@/lib/analytics" -import useResizeObserver from "@/hooks/use-resize-observer" -import { useIsMobile } from "@hooks/use-mobile" -import { cn } from "@lib/utils" +} from "@repo/ui/components/drawer"; +import { + Sheet, + SheetContent, + SheetHeader, + SheetTitle, +} from "@repo/ui/components/sheet"; +import { colors } from "@repo/ui/memory-graph/constants"; +import type { DocumentsWithMemoriesResponseSchema } from "@repo/validation/api"; +import { useVirtualizer } from "@tanstack/react-virtual"; +import { Label1Regular } from "@ui/text/label/label-1-regular"; +import { + Brain, + Calendar, + ExternalLink, + FileText, + Sparkles, +} from "lucide-react"; +import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"; +import type { z } from "zod"; +import useResizeObserver from "@/hooks/use-resize-observer"; +import { analytics } from "@/lib/analytics"; -type DocumentsResponse = z.infer -type DocumentWithMemories = DocumentsResponse["documents"][0] -type MemoryEntry = DocumentWithMemories["memoryEntries"][0] +type DocumentsResponse = z.infer; +type DocumentWithMemories = DocumentsResponse["documents"][0]; +type MemoryEntry = DocumentWithMemories["memoryEntries"][0]; interface MemoryListViewProps { - children?: React.ReactNode - documents: DocumentWithMemories[] - isLoading: boolean - isLoadingMore: boolean - error: Error | null - totalLoaded: number - hasMore: boolean - loadMoreDocuments: () => Promise + children?: React.ReactNode; + documents: DocumentWithMemories[]; + isLoading: boolean; + isLoadingMore: boolean; + error: Error | null; + totalLoaded: number; + hasMore: boolean; + loadMoreDocuments: () => Promise; } const GreetingMessage = memo(() => { const getGreeting = () => { - const hour = new Date().getHours() - if (hour < 12) return "Good morning" - if (hour < 17) return "Good afternoon" - return "Good evening" - } + const hour = new Date().getHours(); + if (hour < 12) return "Good morning"; + if (hour < 17) return "Good afternoon"; + return "Good evening"; + }; return (
-

{getGreeting()}!

-

+

Welcome back to your memory collection

- ) -}) + ); +}); const formatDate = (date: string | Date) => { - const dateObj = new Date(date) - const now = new Date() - const currentYear = now.getFullYear() - const dateYear = dateObj.getFullYear() - - const monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] - const month = monthNames[dateObj.getMonth()] - const day = dateObj.getDate() - + const dateObj = new Date(date); + const now = new Date(); + const currentYear = now.getFullYear(); + const dateYear = dateObj.getFullYear(); + + const monthNames = [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", + ]; + const month = monthNames[dateObj.getMonth()]; + const day = dateObj.getDate(); + const getOrdinalSuffix = (n: number) => { - const s = ["th", "st", "nd", "rd"] - const v = n % 100 - return n + (s[(v - 20) % 10] || s[v] || s[0]!) - } - - const formattedDay = getOrdinalSuffix(day) - + const s = ["th", "st", "nd", "rd"]; + const v = n % 100; + return n + (s[(v - 20) % 10] || s[v] || s[0]!); + }; + + const formattedDay = getOrdinalSuffix(day); + if (dateYear !== currentYear) { - return `${month} ${formattedDay}, ${dateYear}` + return `${month} ${formattedDay}, ${dateYear}`; } - - return `${month} ${formattedDay}` -} + + return `${month} ${formattedDay}`; +}; const formatDocumentType = (type: string) => { // Special case for PDF - if (type.toLowerCase() === "pdf") return "PDF" + if (type.toLowerCase() === "pdf") return "PDF"; // Replace underscores with spaces and capitalize each word return type .split("_") .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) - .join(" ") -} + .join(" "); +}; const getDocumentIcon = (type: string, className: string) => { const iconProps = { className, style: { color: colors.text.muted }, - } + }; switch (type) { case "google_doc": - return + return ; case "google_sheet": - return + return ; case "google_slide": - return + return ; case "google_drive": - return + return ; case "notion": case "notion_doc": - return + return ; case "word": case "microsoft_word": - return + return ; case "excel": case "microsoft_excel": - return + return ; case "powerpoint": case "microsoft_powerpoint": - return + return ; case "onenote": case "microsoft_onenote": - return + return ; case "onedrive": - return + return ; case "pdf": - return + return ; default: - return + return ; } -} +}; const getSourceUrl = (document: DocumentWithMemories) => { if (document.type === "google_doc" && document.customId) { - return `https://docs.google.com/document/d/${document.customId}` + return `https://docs.google.com/document/d/${document.customId}`; } if (document.type === "google_sheet" && document.customId) { - return `https://docs.google.com/spreadsheets/d/${document.customId}` + return `https://docs.google.com/spreadsheets/d/${document.customId}`; } if (document.type === "google_slide" && document.customId) { - return `https://docs.google.com/presentation/d/${document.customId}` + return `https://docs.google.com/presentation/d/${document.customId}`; } // Fallback to existing URL for all other document types - return document.url -} + return document.url; +}; const MemoryDetailItem = memo(({ memory }: { memory: MemoryEntry }) => { return ( @@ -199,8 +214,9 @@ const MemoryDetailItem = memo(({ memory }: { memory: MemoryEntry }) => { }} >
@@ -282,29 +298,29 @@ const MemoryDetailItem = memo(({ memory }: { memory: MemoryEntry }) => {
- ) -}) + ); +}); const DocumentCard = memo( ({ document, onOpenDetails, }: { - document: DocumentWithMemories - onOpenDetails: (document: DocumentWithMemories) => void + document: DocumentWithMemories; + onOpenDetails: (document: DocumentWithMemories) => void; }) => { - const activeMemories = document.memoryEntries.filter((m) => !m.isForgotten) + const activeMemories = document.memoryEntries.filter((m) => !m.isForgotten); const forgottenMemories = document.memoryEntries.filter( (m) => m.isForgotten, - ) + ); return ( { - analytics.documentCardClicked() - onOpenDetails(document) - }} + analytics.documentCardClicked(); + onOpenDetails(document); + }} style={{ backgroundColor: colors.document.primary, }} @@ -313,15 +329,22 @@ const DocumentCard = memo(
{getDocumentIcon(document.type, "w-4 h-4 flex-shrink-0")} -

{document.title || "Untitled Document"}

+

+ {document.title || "Untitled Document"} +

{document.url && (
- ) + ); }, -) +); const DocumentDetailSheet = memo( ({ @@ -386,20 +409,24 @@ const DocumentDetailSheet = memo( onClose, isMobile, }: { - document: DocumentWithMemories | null - isOpen: boolean - onClose: () => void - isMobile: boolean + document: DocumentWithMemories | null; + isOpen: boolean; + onClose: () => void; + isMobile: boolean; }) => { - if (!document) return null + if (!document) return null; - const [isSummaryExpanded, setIsSummaryExpanded] = useState(false) - const activeMemories = document.memoryEntries.filter((m) => !m.isForgotten) + const [isSummaryExpanded, setIsSummaryExpanded] = useState(false); + const activeMemories = document.memoryEntries.filter((m) => !m.isForgotten); const forgottenMemories = document.memoryEntries.filter( (m) => m.isForgotten, - ) + ); - const HeaderContent = ({ TitleComponent }: { TitleComponent: typeof SheetTitle | typeof DrawerTitle }) => ( + const HeaderContent = ({ + TitleComponent, + }: { + TitleComponent: typeof SheetTitle | typeof DrawerTitle; + }) => (
{formatDocumentType(document.type)} - - {formatDate(document.createdAt)} - + {formatDate(document.createdAt)} {document.url && ( <>
- ) + ); const SummarySection = () => { - if (!document.summary) return null + if (!document.summary) return null; - const shouldShowToggle = document.summary.length > 200 // Show toggle for longer summaries + const shouldShowToggle = document.summary.length > 200; // Show toggle for longer summaries return (
-

{document.summary} @@ -473,12 +498,12 @@ const DocumentDetailSheet = memo( style={{ color: colors.accent.primary }} type="button" > - {isSummaryExpanded ? 'Show less' : 'Show more'} + {isSummaryExpanded ? "Show less" : "Show more"} )}

- ) - } + ); + }; const MemoryContent = () => (
@@ -529,26 +554,25 @@ const DocumentDetailSheet = memo(
)} - {activeMemories.length === 0 && - forgottenMemories.length === 0 && ( -
- -

- No memories found for this document -

-
- )} + {activeMemories.length === 0 && forgottenMemories.length === 0 && ( +
+ +

+ No memories found for this document +

+
+ )} - ) + ); if (isMobile) { return ( @@ -582,7 +606,7 @@ const DocumentDetailSheet = memo( - ) + ); } return ( @@ -616,9 +640,9 @@ const DocumentDetailSheet = memo( - ) + ); }, -) +); export const MemoryListView = ({ children, @@ -629,26 +653,29 @@ export const MemoryListView = ({ hasMore, loadMoreDocuments, }: MemoryListViewProps) => { - const [selectedSpace, _] = useState("all") + const [selectedSpace, _] = useState("all"); const [selectedDocument, setSelectedDocument] = - useState(null) - const [isDetailOpen, setIsDetailOpen] = useState(false) - const parentRef = useRef(null) - const containerRef = useRef(null) - const isMobile = useIsMobile() + useState(null); + const [isDetailOpen, setIsDetailOpen] = useState(false); + const parentRef = useRef(null); + const containerRef = useRef(null); + const isMobile = useIsMobile(); + + const gap = 14; - const gap = 14 - const { width: containerWidth } = useResizeObserver(containerRef); - const columnWidth = isMobile ? containerWidth : 320 - const columns = Math.max(1, Math.floor((containerWidth + gap) / (columnWidth + gap))) + const columnWidth = isMobile ? containerWidth : 320; + const columns = Math.max( + 1, + Math.floor((containerWidth + gap) / (columnWidth + gap)), + ); // Filter documents based on selected space const filteredDocuments = useMemo(() => { - if (!documents) return [] + if (!documents) return []; if (selectedSpace === "all") { - return documents + return documents; } return documents @@ -659,46 +686,52 @@ export const MemoryListView = ({ (memory.spaceContainerTag ?? memory.spaceId) === selectedSpace, ), })) - .filter((doc) => doc.memoryEntries.length > 0) - }, [documents, selectedSpace]) + .filter((doc) => doc.memoryEntries.length > 0); + }, [documents, selectedSpace]); const handleOpenDetails = useCallback((document: DocumentWithMemories) => { - analytics.memoryDetailOpened() - setSelectedDocument(document) - setIsDetailOpen(true) - }, []) + analytics.memoryDetailOpened(); + setSelectedDocument(document); + setIsDetailOpen(true); + }, []); const handleCloseDetails = useCallback(() => { - setIsDetailOpen(false) - setTimeout(() => setSelectedDocument(null), 300) - }, []) - + setIsDetailOpen(false); + setTimeout(() => setSelectedDocument(null), 300); + }, []); + const virtualItems = useMemo(() => { - const items = [] + const items = []; for (let i = 0; i < filteredDocuments.length; i += columns) { - items.push(filteredDocuments.slice(i, i + columns)) + items.push(filteredDocuments.slice(i, i + columns)); } - return items - }, [filteredDocuments, columns]) + return items; + }, [filteredDocuments, columns]); const virtualizer = useVirtualizer({ count: virtualItems.length, getScrollElement: () => parentRef.current, overscan: 5, estimateSize: () => 200, - }) + }); useEffect(() => { - const [lastItem] = [...virtualizer.getVirtualItems()].reverse() - + const [lastItem] = [...virtualizer.getVirtualItems()].reverse(); + if (!lastItem || !hasMore || isLoadingMore) { - return + return; } if (lastItem.index >= virtualItems.length - 1) { - loadMoreDocuments() + loadMoreDocuments(); } - }, [hasMore, isLoadingMore, loadMoreDocuments, virtualizer.getVirtualItems(), virtualItems.length]) + }, [ + hasMore, + isLoadingMore, + loadMoreDocuments, + virtualizer.getVirtualItems(), + virtualItems.length, + ]); // Always render with consistent structure return ( @@ -738,19 +771,21 @@ export const MemoryListView = ({ {children} ) : ( -
- +
{virtualizer.getVirtualItems().map((virtualRow) => { - const rowItems = virtualItems[virtualRow.index] - if (!rowItems) return null + const rowItems = virtualItems[virtualRow.index]; + if (!rowItems) return null; return (
- {rowItems.map((document) => ( + {rowItems.map((document, columnIndex) => ( ))}
- ) + ); })}
- + {isLoadingMore && (
@@ -798,5 +838,5 @@ export const MemoryListView = ({ isMobile={isMobile} /> - ) -} + ); +}; diff --git a/apps/web/components/menu.tsx b/apps/web/components/menu.tsx index 622b94b1..0501603f 100644 --- a/apps/web/components/menu.tsx +++ b/apps/web/components/menu.tsx @@ -1,90 +1,85 @@ -"use client" +"use client"; -import { useIsMobile } from "@hooks/use-mobile" +import { useIsMobile } from "@hooks/use-mobile"; import { fetchConsumerProProduct, fetchMemoriesFeature, -} from "@repo/lib/queries" -import { Button } from "@repo/ui/components/button" -import { HeadingH2Bold } from "@repo/ui/text/heading/heading-h2-bold" -import { GlassMenuEffect } from "@ui/other/glass-effect" -import { useCustomer } from "autumn-js/react" -import { - MessageSquareMore, - Plus, - User, - X, -} from "lucide-react" -import { AnimatePresence, LayoutGroup, motion } from "motion/react" -import { useEffect, useState } from "react" -import { useMobilePanel } from "@/lib/mobile-panel-context" -import { TOUR_STEP_IDS } from "@/lib/tour-constants" -import { ProjectSelector } from "./project-selector" -import { useTour } from "./tour" -import { AddMemoryExpandedView, AddMemoryView } from "./views/add-memory" -import { MCPView } from "./views/mcp" -import { ProfileView } from "./views/profile" -import { useChatOpen } from "@/stores" -import { Drawer } from "vaul" +} from "@repo/lib/queries"; +import { Button } from "@repo/ui/components/button"; +import { HeadingH2Bold } from "@repo/ui/text/heading/heading-h2-bold"; +import { GlassMenuEffect } from "@ui/other/glass-effect"; +import { useCustomer } from "autumn-js/react"; +import { MessageSquareMore, Plus, User, X } from "lucide-react"; +import { AnimatePresence, LayoutGroup, motion } from "motion/react"; +import { useEffect, useState } from "react"; +import { Drawer } from "vaul"; +import { useMobilePanel } from "@/lib/mobile-panel-context"; +import { TOUR_STEP_IDS } from "@/lib/tour-constants"; +import { useChatOpen } from "@/stores"; +import { ProjectSelector } from "./project-selector"; +import { useTour } from "./tour"; +import { AddMemoryExpandedView, AddMemoryView } from "./views/add-memory"; +import { MCPView } from "./views/mcp"; +import { ProfileView } from "./views/profile"; const MCPIcon = ({ className }: { className?: string }) => { return ( - ModelContextProtocol - ) -} + ); +}; function Menu({ id }: { id?: string }) { - const [isHovered, setIsHovered] = useState(false) + const [isHovered, setIsHovered] = useState(false); const [expandedView, setExpandedView] = useState< "addUrl" | "mcp" | "projects" | "profile" | null - >(null) - const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false) - const [isCollapsing, setIsCollapsing] = useState(false) - const [showAddMemoryView, setShowAddMemoryView] = useState(false) - const isMobile = useIsMobile() - const { activePanel, setActivePanel } = useMobilePanel() - const { setMenuExpanded } = useTour() - const autumn = useCustomer() - const { setIsOpen } = useChatOpen() + >(null); + const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); + const [isCollapsing, setIsCollapsing] = useState(false); + const [showAddMemoryView, setShowAddMemoryView] = useState(false); + const isMobile = useIsMobile(); + const { activePanel, setActivePanel } = useMobilePanel(); + const { setMenuExpanded } = useTour(); + const autumn = useCustomer(); + const { setIsOpen } = useChatOpen(); - const { data: memoriesCheck } = fetchMemoriesFeature(autumn as any) + const { data: memoriesCheck } = fetchMemoriesFeature(autumn as any); - const memoriesUsed = memoriesCheck?.usage ?? 0 - const memoriesLimit = memoriesCheck?.included_usage ?? 0 + const memoriesUsed = memoriesCheck?.usage ?? 0; + const memoriesLimit = memoriesCheck?.included_usage ?? 0; - const { data: proCheck } = fetchConsumerProProduct(autumn as any) + const { data: proCheck } = fetchConsumerProProduct(autumn as any); useEffect(() => { if (memoriesCheck) { - console.log({ memoriesCheck }) + console.log({ memoriesCheck }); } if (proCheck) { - console.log({ proCheck }) + console.log({ proCheck }); } - }, [memoriesCheck, proCheck]) + }, [memoriesCheck, proCheck]); - const isProUser = proCheck?.allowed ?? false + const isProUser = proCheck?.allowed ?? false; const shouldShowLimitWarning = - !isProUser && memoriesUsed >= memoriesLimit * 0.8 + !isProUser && memoriesUsed >= memoriesLimit * 0.8; // Map menu item keys to tour IDs const menuItemTourIds: Record = { addUrl: TOUR_STEP_IDS.MENU_ADD_MEMORY, projects: TOUR_STEP_IDS.MENU_PROJECTS, mcp: TOUR_STEP_IDS.MENU_MCP, - } + }; const menuItems = [ { @@ -111,55 +106,56 @@ function Menu({ id }: { id?: string }) { key: "profile" as const, disabled: false, }, - ] + ]; const handleMenuItemClick = ( key: "chat" | "addUrl" | "mcp" | "projects" | "profile", ) => { if (key === "chat") { - setIsOpen(true) - setIsMobileMenuOpen(false) + setIsOpen(true); + setIsMobileMenuOpen(false); if (isMobile) { - setActivePanel("chat") + setActivePanel("chat"); } } else { if (expandedView === key) { - setIsCollapsing(true) - setExpandedView(null) + setIsCollapsing(true); + setExpandedView(null); } else if (key === "addUrl") { - setShowAddMemoryView(true) - setExpandedView(null) + setShowAddMemoryView(true); + setExpandedView(null); } else { - setExpandedView(key) + setExpandedView(key); } if (isMobile) { - setActivePanel("menu") + setActivePanel("menu"); } } - } + }; // Watch for active panel changes on mobile useEffect(() => { if (isMobile && activePanel !== "menu" && activePanel !== null) { // Another panel became active, close the menu - setIsMobileMenuOpen(false) - setExpandedView(null) + setIsMobileMenuOpen(false); + setExpandedView(null); } - }, [isMobile, activePanel]) + }, [isMobile, activePanel]); // Notify tour provider about expansion state changes useEffect(() => { const isExpanded = isMobile ? isMobileMenuOpen || !!expandedView - : isHovered || !!expandedView - setMenuExpanded(isExpanded) - }, [isMobile, isMobileMenuOpen, isHovered, expandedView, setMenuExpanded]) + : isHovered || !!expandedView; + setMenuExpanded(isExpanded); + }, [isMobile, isMobileMenuOpen, isHovered, expandedView, setMenuExpanded]); // Calculate width based on state - const menuWidth = expandedView || isCollapsing ? 600 : isHovered ? 160 : 56 + const menuWidth = expandedView || isCollapsing ? 600 : isHovered ? 160 : 56; // Dynamic z-index for mobile based on active panel - const mobileZIndex = isMobile && activePanel === "menu" ? "z-[70]" : "z-[100]" + const mobileZIndex = + isMobile && activePanel === "menu" ? "z-[70]" : "z-[100]"; return ( <> @@ -293,11 +289,11 @@ function Menu({ id }: { id?: string }) { {item.text} {/* Show warning indicator for Add Memory when limits approached */} {shouldShowLimitWarning && - item.key === "addUrl" && ( - - {memoriesLimit - memoriesUsed} left - - )} + item.key === "addUrl" && ( + + {memoriesLimit - memoriesUsed} left + + )} {index === 0 && ( @@ -374,8 +370,8 @@ function Menu({ id }: { id?: string }) { - -
- {expandedView === "addUrl" && ( - - )} - {expandedView === "mcp" && } - {expandedView === "profile" && } + type="button" + whileHover={{ scale: 1.05 }} + whileTap={{ scale: 0.95 }} + > + + + {item.text} + + {/* Show warning indicator for Add Memory when limits approached */} + {shouldShowLimitWarning && + item.key === "addUrl" && ( + + {memoriesLimit - memoriesUsed} left + + )} + + {/* Add horizontal line after first item */} + {index === 0 && ( + + )} +
+ ))}
-
- - )} - + + ) : ( + +
+ + + {expandedView === "addUrl" && "Add Memory"} + {expandedView === "mcp" && + "Model Context Protocol"} + {expandedView === "profile" && "Profile"} + + + +
+ {expandedView === "addUrl" && ( + + )} + {expandedView === "mcp" && } + {expandedView === "profile" && } +
+
+
+ )} +
@@ -612,7 +615,7 @@ function Menu({ id }: { id?: string }) { /> )} - ) + ); } -export default Menu +export default Menu; diff --git a/apps/web/components/project-selector.tsx b/apps/web/components/project-selector.tsx index bb62d8ca..a592c474 100644 --- a/apps/web/components/project-selector.tsx +++ b/apps/web/components/project-selector.tsx @@ -1,21 +1,8 @@ -"use client" +"use client"; -import { $fetch } from "@repo/lib/api" -import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query" -import { ChevronDown, FolderIcon, Plus, Trash2, Loader2, MoreVertical, MoreHorizontal } from "lucide-react" -import { motion, AnimatePresence } from "motion/react" -import { useState } from "react" -import { toast } from "sonner" -import { CreateProjectDialog } from "./create-project-dialog" -import { useProject } from "@/stores" -import { useProjectName } from "@/hooks/use-project-name" -import { useProjectMutations } from "@/hooks/use-project-mutations" -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from "@repo/ui/components/dropdown-menu" +import { $fetch } from "@repo/lib/api"; +import { DEFAULT_PROJECT_ID } from "@repo/lib/constants"; +import { Button } from "@repo/ui/components/button"; import { Dialog, DialogContent, @@ -23,96 +10,121 @@ import { DialogFooter, DialogHeader, DialogTitle, -} from "@repo/ui/components/dialog" -import { Button } from "@repo/ui/components/button" -import { Label } from "@repo/ui/components/label" +} from "@repo/ui/components/dialog"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@repo/ui/components/dropdown-menu"; +import { Label } from "@repo/ui/components/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, -} from "@repo/ui/components/select" -import { DEFAULT_PROJECT_ID } from "@repo/lib/constants" +} from "@repo/ui/components/select"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { + ChevronDown, + FolderIcon, + Loader2, + MoreHorizontal, + MoreVertical, + Plus, + Trash2, +} from "lucide-react"; +import { AnimatePresence, motion } from "motion/react"; +import { useState } from "react"; +import { toast } from "sonner"; +import { useProjectMutations } from "@/hooks/use-project-mutations"; +import { useProjectName } from "@/hooks/use-project-name"; +import { useProject } from "@/stores"; +import { CreateProjectDialog } from "./create-project-dialog"; interface Project { - id: string - name: string - containerTag: string - createdAt: string - updatedAt: string - isExperimental?: boolean + id: string; + name: string; + containerTag: string; + createdAt: string; + updatedAt: string; + isExperimental?: boolean; } export function ProjectSelector() { - const queryClient = useQueryClient() - const [isOpen, setIsOpen] = useState(false) - const [showCreateDialog, setShowCreateDialog] = useState(false) - const { selectedProject } = useProject() - const projectName = useProjectName() - const { switchProject, deleteProjectMutation } = useProjectMutations() + const queryClient = useQueryClient(); + const [isOpen, setIsOpen] = useState(false); + const [showCreateDialog, setShowCreateDialog] = useState(false); + const { selectedProject } = useProject(); + const projectName = useProjectName(); + const { switchProject, deleteProjectMutation } = useProjectMutations(); const [deleteDialog, setDeleteDialog] = useState<{ - open: boolean - project: null | { id: string; name: string; containerTag: string } - action: "move" | "delete" - targetProjectId: string + open: boolean; + project: null | { id: string; name: string; containerTag: string }; + action: "move" | "delete"; + targetProjectId: string; }>({ open: false, project: null, action: "move", targetProjectId: DEFAULT_PROJECT_ID, - }) + }); const [expDialog, setExpDialog] = useState<{ - open: boolean - projectId: string + open: boolean; + projectId: string; }>({ open: false, projectId: "", - }) + }); const { data: projects = [], isLoading } = useQuery({ queryKey: ["projects"], queryFn: async () => { - const response = await $fetch("@get/projects") + const response = await $fetch("@get/projects"); if (response.error) { - throw new Error(response.error?.message || "Failed to load projects") + throw new Error(response.error?.message || "Failed to load projects"); } - return response.data?.projects || [] + return response.data?.projects || []; }, staleTime: 30 * 1000, - }) + }); const enableExperimentalMutation = useMutation({ mutationFn: async (projectId: string) => { - const response = await $fetch(`@post/projects/${projectId}/enable-experimental`) + const response = await $fetch( + `@post/projects/${projectId}/enable-experimental`, + ); if (response.error) { - throw new Error(response.error?.message || "Failed to enable experimental mode") + throw new Error( + response.error?.message || "Failed to enable experimental mode", + ); } - return response.data + return response.data; }, onSuccess: () => { - toast.success("Experimental mode enabled for project") - queryClient.invalidateQueries({ queryKey: ["projects"] }) - setExpDialog({ open: false, projectId: "" }) + toast.success("Experimental mode enabled for project"); + queryClient.invalidateQueries({ queryKey: ["projects"] }); + setExpDialog({ open: false, projectId: "" }); }, onError: (error) => { toast.error("Failed to enable experimental mode", { description: error instanceof Error ? error.message : "Unknown error", - }) + }); }, - }) + }); const handleProjectSelect = (containerTag: string) => { - switchProject(containerTag) - setIsOpen(false) - } + switchProject(containerTag); + setIsOpen(false); + }; const handleCreateNewProject = () => { - setIsOpen(false) - setShowCreateDialog(true) - } + setIsOpen(false); + setShowCreateDialog(true); + }; return (
@@ -164,7 +176,9 @@ export function ProjectSelector() { >
- Default + + Default +
@@ -183,9 +197,11 @@ export function ProjectSelector() { animate={{ opacity: 1, x: 0 }} transition={{ delay: index * 0.03 }} > -
handleProjectSelect(project.containerTag)} + onClick={() => + handleProjectSelect(project.containerTag) + } > @@ -214,12 +230,12 @@ export function ProjectSelector() { { - e.stopPropagation() + e.stopPropagation(); setExpDialog({ open: true, projectId: project.id, - }) - setIsOpen(false) + }); + setIsOpen(false); }} >
@@ -238,7 +254,7 @@ export function ProjectSelector() { { - e.stopPropagation() + e.stopPropagation(); setDeleteDialog({ open: true, project: { @@ -248,8 +264,8 @@ export function ProjectSelector() { }, action: "move", targetProjectId: "", - }) - setIsOpen(false) + }); + setIsOpen(false); }} > @@ -270,7 +286,9 @@ export function ProjectSelector() { transition={{ delay: (projects.length + 1) * 0.03 }} > - New Project + + New Project +
@@ -278,9 +296,9 @@ export function ProjectSelector() { )} - {/* Delete Project Dialog */} @@ -301,8 +319,8 @@ export function ProjectSelector() { Delete Project - Are you sure you want to delete "{deleteDialog.project.name}"? - Choose what to do with the documents in this project. + Are you sure you want to delete "{deleteDialog.project.name} + "? Choose what to do with the documents in this project.
@@ -432,10 +450,11 @@ export function ProjectSelector() { whileTap={{ scale: 0.95 }} >
- ) + ); } diff --git a/apps/web/components/referral-upgrade-modal.tsx b/apps/web/components/referral-upgrade-modal.tsx index 2ad1b18a..029bd2ae 100644 --- a/apps/web/components/referral-upgrade-modal.tsx +++ b/apps/web/components/referral-upgrade-modal.tsx @@ -1,23 +1,25 @@ -"use client" +"use client"; -import { useAuth } from "@lib/auth-context" -import { - fetchMemoriesFeature, - fetchSubscriptionStatus, -} from "@lib/queries" -import { Button } from "@repo/ui/components/button" +import { useAuth } from "@lib/auth-context"; +import { fetchMemoriesFeature, fetchSubscriptionStatus } from "@lib/queries"; +import { Button } from "@repo/ui/components/button"; import { Dialog, DialogContent, DialogDescription, DialogHeader, - DialogTitle -} from "@repo/ui/components/dialog" -import { Input } from "@repo/ui/components/input" -import { Label } from "@repo/ui/components/label" -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@repo/ui/components/tabs" -import { HeadingH3Bold } from "@repo/ui/text/heading/heading-h3-bold" -import { useCustomer } from "autumn-js/react" + DialogTitle, +} from "@repo/ui/components/dialog"; +import { Input } from "@repo/ui/components/input"; +import { Label } from "@repo/ui/components/label"; +import { + Tabs, + TabsContent, + TabsList, + TabsTrigger, +} from "@repo/ui/components/tabs"; +import { HeadingH3Bold } from "@repo/ui/text/heading/heading-h3-bold"; +import { useCustomer } from "autumn-js/react"; import { CheckCircle, Copy, @@ -25,26 +27,29 @@ import { Gift, LoaderIcon, Share2, - Users -} from "lucide-react" -import { motion } from "motion/react" -import Link from "next/link" -import { useState } from "react" + Users, +} from "lucide-react"; +import { motion } from "motion/react"; +import Link from "next/link"; +import { useState } from "react"; interface ReferralUpgradeModalProps { - isOpen: boolean - onClose: () => void + isOpen: boolean; + onClose: () => void; } -export function ReferralUpgradeModal({ isOpen, onClose }: ReferralUpgradeModalProps) { - const { user } = useAuth() - const autumn = useCustomer() - const [isLoading, setIsLoading] = useState(false) - const [copied, setCopied] = useState(false) +export function ReferralUpgradeModal({ + isOpen, + onClose, +}: ReferralUpgradeModalProps) { + const { user } = useAuth(); + const autumn = useCustomer(); + const [isLoading, setIsLoading] = useState(false); + const [copied, setCopied] = useState(false); - const { data: memoriesCheck } = fetchMemoriesFeature(autumn as any) - const memoriesUsed = memoriesCheck?.usage ?? 0 - const memoriesLimit = memoriesCheck?.included_usage ?? 0 + const { data: memoriesCheck } = fetchMemoriesFeature(autumn as any); + const memoriesUsed = memoriesCheck?.usage ?? 0; + const memoriesLimit = memoriesCheck?.included_usage ?? 0; // Fetch subscription status const { @@ -52,53 +57,53 @@ export function ReferralUpgradeModal({ isOpen, onClose }: ReferralUpgradeModalPr consumer_pro: null, }, isLoading: isCheckingStatus, - } = fetchSubscriptionStatus(autumn as any) + } = fetchSubscriptionStatus(autumn as any); - const isPro = status.consumer_pro + const isPro = status.consumer_pro; // Handle upgrade const handleUpgrade = async () => { - setIsLoading(true) + setIsLoading(true); try { await autumn.attach({ productId: "consumer_pro", successUrl: "https://app.supermemory.ai/", - }) - window.location.reload() + }); + window.location.reload(); } catch (error) { - console.error(error) - setIsLoading(false) + console.error(error); + setIsLoading(false); } - } + }; // Generate referral link (you'll need to implement this based on your referral system) - const referralLink = `https://app.supermemory.ai/ref/${user?.id || 'user'}` + const referralLink = `https://app.supermemory.ai/ref/${user?.id || "user"}`; const handleCopyReferralLink = async () => { try { - await navigator.clipboard.writeText(referralLink) - setCopied(true) - setTimeout(() => setCopied(false), 2000) + await navigator.clipboard.writeText(referralLink); + setCopied(true); + setTimeout(() => setCopied(false), 2000); } catch (error) { - console.error('Failed to copy:', error) + console.error("Failed to copy:", error); } - } + }; const handleShare = async () => { if (navigator.share) { try { await navigator.share({ - title: 'Join Supermemory', - text: 'Check out Supermemory - the best way to organize and search your digital memories!', + title: "Join Supermemory", + text: "Check out Supermemory - the best way to organize and search your digital memories!", url: referralLink, - }) + }); } catch (error) { - console.error('Error sharing:', error) + console.error("Error sharing:", error); } } else { - handleCopyReferralLink() + handleCopyReferralLink(); } - } + }; if (user?.isAnonymous) { return ( @@ -127,7 +132,7 @@ export function ReferralUpgradeModal({ isOpen, onClose }: ReferralUpgradeModalPr - ) + ); } return ( @@ -149,14 +154,20 @@ export function ReferralUpgradeModal({ isOpen, onClose }: ReferralUpgradeModalPr
Current Usage - = memoriesLimit ? "text-red-400" : "text-white/90"}`}> + = memoriesLimit ? "text-red-400" : "text-white/90"}`} + > {memoriesUsed} / {memoriesLimit} memories
= memoriesLimit ? "bg-red-500" : isPro ? "bg-green-500" : "bg-blue-500" + memoriesUsed >= memoriesLimit + ? "bg-red-500" + : isPro + ? "bg-green-500" + : "bg-blue-500" }`} style={{ width: `${Math.min((memoriesUsed / memoriesLimit) * 100, 100)}%`, @@ -173,7 +184,10 @@ export function ReferralUpgradeModal({ isOpen, onClose }: ReferralUpgradeModalPr Refer Friends {!isPro && ( - + Upgrade Plan @@ -286,5 +300,5 @@ export function ReferralUpgradeModal({ isOpen, onClose }: ReferralUpgradeModalPr - ) + ); } diff --git a/apps/web/components/spinner.tsx b/apps/web/components/spinner.tsx index aad15e33..6e14eb5b 100644 --- a/apps/web/components/spinner.tsx +++ b/apps/web/components/spinner.tsx @@ -2,7 +2,5 @@ import { cn } from "@lib/utils"; import { Loader2 } from "lucide-react"; export function Spinner({ className }: { className?: string }) { - return ( - - ) -} \ No newline at end of file + return ; +} diff --git a/apps/web/components/text-shimmer.tsx b/apps/web/components/text-shimmer.tsx index bef0e7a9..1825d08c 100644 --- a/apps/web/components/text-shimmer.tsx +++ b/apps/web/components/text-shimmer.tsx @@ -1,57 +1,57 @@ -'use client'; -import React, { useMemo, type JSX } from 'react'; -import { motion } from 'motion/react'; -import { cn } from '@lib/utils'; +"use client"; +import { cn } from "@lib/utils"; +import { motion } from "motion/react"; +import React, { type JSX, useMemo } from "react"; export type TextShimmerProps = { - children: string; - as?: React.ElementType; - className?: string; - duration?: number; - spread?: number; + children: string; + as?: React.ElementType; + className?: string; + duration?: number; + spread?: number; }; function TextShimmerComponent({ - children, - as: Component = 'p', - className, - duration = 2, - spread = 2, + children, + as: Component = "p", + className, + duration = 2, + spread = 2, }: TextShimmerProps) { - const MotionComponent = motion.create( - Component as keyof JSX.IntrinsicElements - ); + const MotionComponent = motion.create( + Component as keyof JSX.IntrinsicElements, + ); - const dynamicSpread = useMemo(() => { - return children.length * spread; - }, [children, spread]); + const dynamicSpread = useMemo(() => { + return children.length * spread; + }, [children, spread]); - return ( - - {children} - - ); + return ( + + {children} + + ); } export const TextShimmer = React.memo(TextShimmerComponent); diff --git a/apps/web/components/tour.tsx b/apps/web/components/tour.tsx index 981effbc..33919efe 100644 --- a/apps/web/components/tour.tsx +++ b/apps/web/components/tour.tsx @@ -1,54 +1,54 @@ -"use client" +"use client"; -import { Button } from "@repo/ui/components/button" -import { GlassMenuEffect } from "@repo/ui/other/glass-effect" -import { AnimatePresence, motion } from "motion/react" -import * as React from "react" -import { analytics } from "@/lib/analytics" +import { Button } from "@repo/ui/components/button"; +import { GlassMenuEffect } from "@repo/ui/other/glass-effect"; +import { AnimatePresence, motion } from "motion/react"; +import * as React from "react"; +import { analytics } from "@/lib/analytics"; // Types export interface TourStep { - content: React.ReactNode - selectorId: string - position?: "top" | "bottom" | "left" | "right" | "center" - onClickWithinArea?: () => void + content: React.ReactNode; + selectorId: string; + position?: "top" | "bottom" | "left" | "right" | "center"; + onClickWithinArea?: () => void; } interface TourContextType { - currentStep: number - totalSteps: number - nextStep: () => void - previousStep: () => void - endTour: () => void - isActive: boolean - isPaused: boolean - startTour: () => void - setSteps: (steps: TourStep[]) => void - steps: TourStep[] - isTourCompleted: boolean - setIsTourCompleted: (completed: boolean) => void + currentStep: number; + totalSteps: number; + nextStep: () => void; + previousStep: () => void; + endTour: () => void; + isActive: boolean; + isPaused: boolean; + startTour: () => void; + setSteps: (steps: TourStep[]) => void; + steps: TourStep[]; + isTourCompleted: boolean; + setIsTourCompleted: (completed: boolean) => void; // Expansion state tracking - setMenuExpanded: (expanded: boolean) => void - setChatExpanded: (expanded: boolean) => void + setMenuExpanded: (expanded: boolean) => void; + setChatExpanded: (expanded: boolean) => void; } // Context -const TourContext = React.createContext(undefined) +const TourContext = React.createContext(undefined); export function useTour() { - const context = React.useContext(TourContext) + const context = React.useContext(TourContext); if (!context) { - throw new Error("useTour must be used within a TourProvider") + throw new Error("useTour must be used within a TourProvider"); } - return context + return context; } // Provider interface TourProviderProps { - children: React.ReactNode - onComplete?: () => void - className?: string - isTourCompleted?: boolean + children: React.ReactNode; + onComplete?: () => void; + className?: string; + isTourCompleted?: boolean; } export function TourProvider({ @@ -57,60 +57,61 @@ export function TourProvider({ className, isTourCompleted: initialCompleted = false, }: TourProviderProps) { - const [currentStep, setCurrentStep] = React.useState(-1) - const [steps, setSteps] = React.useState([]) - const [isActive, setIsActive] = React.useState(false) - const [isTourCompleted, setIsTourCompleted] = React.useState(initialCompleted) + const [currentStep, setCurrentStep] = React.useState(-1); + const [steps, setSteps] = React.useState([]); + const [isActive, setIsActive] = React.useState(false); + const [isTourCompleted, setIsTourCompleted] = + React.useState(initialCompleted); // Track expansion states - const [isMenuExpanded, setIsMenuExpanded] = React.useState(false) - const [isChatExpanded, setIsChatExpanded] = React.useState(false) + const [isMenuExpanded, setIsMenuExpanded] = React.useState(false); + const [isChatExpanded, setIsChatExpanded] = React.useState(false); // Calculate if tour should be paused const isPaused = React.useMemo(() => { - return isActive && (isMenuExpanded || isChatExpanded) - }, [isActive, isMenuExpanded, isChatExpanded]) + return isActive && (isMenuExpanded || isChatExpanded); + }, [isActive, isMenuExpanded, isChatExpanded]); const startTour = React.useCallback(() => { - console.debug("Starting tour with", steps.length, "steps") - analytics.tourStarted() - setCurrentStep(0) - setIsActive(true) - }, [steps]) + console.debug("Starting tour with", steps.length, "steps"); + analytics.tourStarted(); + setCurrentStep(0); + setIsActive(true); + }, [steps]); const endTour = React.useCallback(() => { - setCurrentStep(-1) - setIsActive(false) - setIsTourCompleted(true) // Mark tour as completed when ended/skipped - analytics.tourSkipped() + setCurrentStep(-1); + setIsActive(false); + setIsTourCompleted(true); // Mark tour as completed when ended/skipped + analytics.tourSkipped(); if (onComplete) { - onComplete() + onComplete(); } - }, [onComplete]) + }, [onComplete]); const nextStep = React.useCallback(() => { if (currentStep < steps.length - 1) { - setCurrentStep(currentStep + 1) + setCurrentStep(currentStep + 1); } else { - analytics.tourCompleted() - endTour() - setIsTourCompleted(true) + analytics.tourCompleted(); + endTour(); + setIsTourCompleted(true); } - }, [currentStep, steps.length, endTour]) + }, [currentStep, steps.length, endTour]); const previousStep = React.useCallback(() => { if (currentStep > 0) { - setCurrentStep(currentStep - 1) + setCurrentStep(currentStep - 1); } - }, [currentStep]) + }, [currentStep]); const setMenuExpanded = React.useCallback((expanded: boolean) => { - setIsMenuExpanded(expanded) - }, []) + setIsMenuExpanded(expanded); + }, []); const setChatExpanded = React.useCallback((expanded: boolean) => { - setIsChatExpanded(expanded) - }, []) + setIsChatExpanded(expanded); + }, []); const value = React.useMemo( () => ({ @@ -142,7 +143,7 @@ export function TourProvider({ setMenuExpanded, setChatExpanded, ], - ) + ); return ( @@ -164,7 +165,7 @@ export function TourProvider({ )} - ) + ); } // Tour Highlight Component @@ -173,33 +174,33 @@ function TourHighlight({ steps, className, }: { - currentStepIndex: number - steps: TourStep[] - className?: string + currentStepIndex: number; + steps: TourStep[]; + className?: string; }) { - const { nextStep, previousStep, endTour } = useTour() - const [elementRect, setElementRect] = React.useState(null) + const { nextStep, previousStep, endTour } = useTour(); + const [elementRect, setElementRect] = React.useState(null); // Get current step safely const step = currentStepIndex >= 0 && currentStepIndex < steps.length ? steps[currentStepIndex] - : null + : null; React.useEffect(() => { - if (!step) return + if (!step) return; // Use requestAnimationFrame to ensure DOM is ready const rafId = requestAnimationFrame(() => { - const element = document.getElementById(step.selectorId) + const element = document.getElementById(step.selectorId); console.debug( "Looking for element with ID:", step.selectorId, "Found:", !!element, - ) + ); if (element) { - const rect = element.getBoundingClientRect() + const rect = element.getBoundingClientRect(); console.debug("Element rect:", { id: step.selectorId, top: rect.top, @@ -208,34 +209,34 @@ function TourHighlight({ height: rect.height, bottom: rect.bottom, right: rect.right, - }) - setElementRect(rect) + }); + setElementRect(rect); } - }) + }); // Add click listener for onClickWithinArea - let clickHandler: ((e: MouseEvent) => void) | null = null + let clickHandler: ((e: MouseEvent) => void) | null = null; if (step.onClickWithinArea) { - const element = document.getElementById(step.selectorId) + const element = document.getElementById(step.selectorId); if (element) { clickHandler = (e: MouseEvent) => { if (element.contains(e.target as Node)) { - step.onClickWithinArea?.() + step.onClickWithinArea?.(); } - } - document.addEventListener("click", clickHandler) + }; + document.addEventListener("click", clickHandler); } } return () => { - cancelAnimationFrame(rafId) + cancelAnimationFrame(rafId); if (clickHandler) { - document.removeEventListener("click", clickHandler) + document.removeEventListener("click", clickHandler); } - } - }, [step]) + }; + }, [step]); - if (!step) return null + if (!step) return null; // Keep the wrapper mounted but animate the content return ( @@ -277,12 +278,12 @@ function TourHighlight({ ? elementRect.bottom + 8 : step.position === "top" ? elementRect.top - 200 - : elementRect.top + elementRect.height / 2 - 100 + : elementRect.top + elementRect.height / 2 - 100; // Ensure tooltip stays within viewport - const maxTop = window.innerHeight - 250 // Leave space for tooltip height - const minTop = 10 - return Math.max(minTop, Math.min(baseTop, maxTop)) + const maxTop = window.innerHeight - 250; // Leave space for tooltip height + const minTop = 10; + return Math.max(minTop, Math.min(baseTop, maxTop)); })(), left: (() => { const baseLeft = @@ -290,12 +291,12 @@ function TourHighlight({ ? elementRect.right + 8 : step.position === "left" ? elementRect.left - 300 - : elementRect.left + elementRect.width / 2 - 150 + : elementRect.left + elementRect.width / 2 - 150; // Ensure tooltip stays within viewport - const maxLeft = window.innerWidth - 300 // Tooltip width - const minLeft = 10 - return Math.max(minLeft, Math.min(baseLeft, maxLeft)) + const maxLeft = window.innerWidth - 300; // Tooltip width + const minLeft = 10; + return Math.max(minLeft, Math.min(baseLeft, maxLeft)); })(), }} > @@ -342,31 +343,31 @@ function TourHighlight({ )} - ) + ); } // Tour Alert Dialog interface TourAlertDialogProps { - open: boolean - onOpenChange: (open: boolean) => void + open: boolean; + onOpenChange: (open: boolean) => void; } export function TourAlertDialog({ open, onOpenChange }: TourAlertDialogProps) { - const { startTour, setIsTourCompleted } = useTour() + const { startTour, setIsTourCompleted } = useTour(); const handleStart = () => { - console.debug("TourAlertDialog: Starting tour") - onOpenChange(false) - startTour() - } + console.debug("TourAlertDialog: Starting tour"); + onOpenChange(false); + startTour(); + }; const handleSkip = () => { - analytics.tourSkipped() - setIsTourCompleted(true) // Mark tour as completed when skipped - onOpenChange(false) - } + analytics.tourSkipped(); + setIsTourCompleted(true); // Mark tour as completed when skipped + onOpenChange(false); + }; - if (!open) return null + if (!open) return null; return ( @@ -409,5 +410,5 @@ export function TourAlertDialog({ open, onOpenChange }: TourAlertDialogProps) {
- ) + ); } diff --git a/apps/web/components/views/add-memory.tsx b/apps/web/components/views/add-memory.tsx deleted file mode 100644 index 744faa35..00000000 --- a/apps/web/components/views/add-memory.tsx +++ /dev/null @@ -1,1425 +0,0 @@ -import { $fetch } from "@lib/api" -import { - fetchConsumerProProduct, - fetchMemoriesFeature, -} from "@repo/lib/queries" -import { Button } from "@repo/ui/components/button" -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from "@repo/ui/components/dialog" -import { Input } from "@repo/ui/components/input" -import { Label } from "@repo/ui/components/label" -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@repo/ui/components/select" -import { - Tabs, - TabsContent, - TabsList, - TabsTrigger, -} from "@repo/ui/components/tabs" -import { Textarea } from "@repo/ui/components/textarea" -import { useForm } from "@tanstack/react-form" -import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" -import { - Dropzone, - DropzoneContent, - DropzoneEmptyState, -} from "@ui/components/shadcn-io/dropzone" -import { useCustomer } from "autumn-js/react" -import { - Brain, - FileIcon, - Link as LinkIcon, - Loader2, - PlugIcon, - Plus, - UploadIcon, -} from "lucide-react" -import { AnimatePresence, motion } from "motion/react" -import Link from "next/link" -import { useEffect, useState } from "react" -import { toast } from "sonner" -import { z } from "zod" -import { useProject } from "@/stores" -import { ConnectionsTabContent } from "./connections-tab-content" -import { analytics } from "@/lib/analytics" - -// // Processing status component -// function ProcessingStatus({ status }: { status: string }) { -// const statusConfig = { -// queued: { color: "text-yellow-400", label: "Queued", icon: "⏳" }, -// extracting: { color: "text-blue-400", label: "Extracting", icon: "📤" }, -// chunking: { color: "text-indigo-400", label: "Chunking", icon: "✂️" }, -// embedding: { color: "text-purple-400", label: "Embedding", icon: "🧠" }, -// indexing: { color: "text-pink-400", label: "Indexing", icon: "📝" }, -// unknown: { color: "text-gray-400", label: "Processing", icon: "⚙️" }, -// } - -// const config = -// statusConfig[status as keyof typeof statusConfig] || statusConfig.unknown - -// return ( -//
-// {config.icon} -// {config.label} -//
-// ) -// } - -export function AddMemoryView({ - onClose, - initialTab = "note", -}: { - onClose?: () => void - initialTab?: "note" | "link" | "file" | "connect" -}) { - const queryClient = useQueryClient() - const { selectedProject, setSelectedProject } = useProject() - const [showAddDialog, setShowAddDialog] = useState(true) - const [selectedFiles, setSelectedFiles] = useState([]) - const [activeTab, setActiveTab] = useState< - "note" | "link" | "file" | "connect" - >(initialTab) - const autumn = useCustomer() - const [showCreateProjectDialog, setShowCreateProjectDialog] = useState(false) - const [newProjectName, setNewProjectName] = useState("") - - // Check memory limits - const { data: memoriesCheck } = fetchMemoriesFeature(autumn as any) - - const memoriesUsed = memoriesCheck?.usage ?? 0 - const memoriesLimit = memoriesCheck?.included_usage ?? 0 - - // Check if user is pro - const { data: proCheck } = fetchConsumerProProduct(autumn as any) - const isProUser = proCheck?.allowed ?? false - - const canAddMemory = memoriesUsed < memoriesLimit - - // Fetch projects for the dropdown - const { data: projects = [], isLoading: isLoadingProjects } = 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, - }) - - // Create project mutation - const createProjectMutation = useMutation({ - mutationFn: async (name: string) => { - const response = await $fetch("@post/projects", { - body: { name }, - }) - - if (response.error) { - throw new Error(response.error?.message || "Failed to create project") - } - - return response.data - }, - onSuccess: (data) => { - analytics.projectCreated() - toast.success("Project created successfully!") - setShowCreateProjectDialog(false) - setNewProjectName("") - queryClient.invalidateQueries({ queryKey: ["projects"] }) - // Set the newly created project as selected - if (data?.containerTag) { - setSelectedProject(data.containerTag) - // Update form values - addContentForm.setFieldValue("project", data.containerTag) - fileUploadForm.setFieldValue("project", data.containerTag) - } - }, - onError: (error) => { - toast.error("Failed to create project", { - description: error instanceof Error ? error.message : "Unknown error", - }) - }, - }) - - const addContentForm = useForm({ - defaultValues: { - content: "", - project: selectedProject || "sm_project_default", - }, - onSubmit: async ({ value, formApi }) => { - addContentMutation.mutate({ - content: value.content, - project: value.project, - contentType: activeTab as "note" | "link", - }) - formApi.reset() - }, - validators: { - onChange: z.object({ - content: z.string().min(1, "Content is required"), - project: z.string(), - }), - }, - }) - - // Re-validate content field when tab changes between note/link - // biome-ignore lint/correctness/useExhaustiveDependencies: It is what it is - useEffect(() => { - // Trigger validation of the content field when switching between note/link - if (activeTab === "note" || activeTab === "link") { - const currentValue = addContentForm.getFieldValue("content") - if (currentValue) { - addContentForm.validateField("content", "change") - } - } - }, [activeTab]) - - // Form for file upload metadata - const fileUploadForm = useForm({ - defaultValues: { - title: "", - description: "", - project: selectedProject || "sm_project_default", - }, - onSubmit: async ({ value, formApi }) => { - if (selectedFiles.length === 0) { - toast.error("Please select a file to upload") - return - } - - for (const file of selectedFiles) { - fileUploadMutation.mutate({ - file, - title: value.title || undefined, - description: value.description || undefined, - project: value.project, - }) - } - - formApi.reset() - setSelectedFiles([]) - }, - }) - - const handleUpgrade = async () => { - try { - await autumn.attach({ - productId: "consumer_pro", - successUrl: "https://app.supermemory.ai/", - }) - window.location.reload() - } catch (error) { - console.error(error) - } - } - - const addContentMutation = useMutation({ - mutationFn: async ({ - content, - project, - contentType, - }: { - content: string - project: string - contentType: "note" | "link" - }) => { - // close the modal - onClose?.() - - const processingPromise = (async () => { - // First, create the memory - const response = await $fetch("@post/memories", { - body: { - content: content, - containerTags: [project], - metadata: { - sm_source: "consumer", // Use "consumer" source to bypass limits - }, - }, - }) - - if (response.error) { - throw new Error( - response.error?.message || `Failed to add ${contentType}`, - ) - } - - const memoryId = response.data.id - - // Polling function to check status - const pollForCompletion = async (): Promise => { - let attempts = 0 - const maxAttempts = 60 // Maximum 5 minutes (60 attempts * 5 seconds) - - while (attempts < maxAttempts) { - try { - const memory = await $fetch<{ status: string; content: string }>( - "@get/memories/" + memoryId, - ) - - if (memory.error) { - throw new Error( - memory.error?.message || "Failed to fetch memory status", - ) - } - - // Check if processing is complete - // Adjust this condition based on your API response structure - if ( - memory.data?.status === "done" || - // Sometimes the memory might be ready when it has content and no processing status - memory.data?.content - ) { - return memory.data - } - - // If still processing, wait and try again - await new Promise((resolve) => setTimeout(resolve, 5000)) // Wait 5 seconds - attempts++ - } catch (error) { - console.error("Error polling memory status:", error) - // Don't throw immediately, retry a few times - if (attempts >= 3) { - throw new Error("Failed to check processing status") - } - await new Promise((resolve) => setTimeout(resolve, 5000)) - attempts++ - } - } - - // If we've exceeded max attempts, throw an error - throw new Error("Memory processing timed out. Please check back later.") - } - - // Wait for completion - const completedMemory = await pollForCompletion() - return completedMemory - })() - - toast.promise(processingPromise, { - loading: "Processing...", - success: `${contentType === "link" ? "Link" : "Note"} created successfully!`, - error: (err) => `Failed to add ${contentType}: ${err instanceof Error ? err.message : "Unknown error"}`, - }) - - return processingPromise - }, - onMutate: async ({ content, project, contentType }) => { - console.log("🚀 onMutate starting...") - - // Cancel any outgoing refetches - await queryClient.cancelQueries({ queryKey: ["documents-with-memories", project] }) - console.log("✅ Cancelled queries") - - // Snapshot the previous value - const previousMemories = queryClient.getQueryData([ - "documents-with-memories", - project, - ]) - console.log("📸 Previous memories:", previousMemories) - - // Create optimistic memory - const optimisticMemory = { - id: `temp-${Date.now()}`, - content: contentType === "link" ? "" : content, - url: contentType === "link" ? content : null, - title: - contentType === "link" ? "Processing..." : content.substring(0, 100), - description: contentType === "link" ? "Extracting content..." : "Processing content...", - containerTags: [project], - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), - status: "queued", - type: contentType, - metadata: { - processingStage: "queued", - processingMessage: "Added to processing queue" - }, - memoryEntries: [], - isOptimistic: true, - } - console.log("🎯 Created optimistic memory:", optimisticMemory) - - // Optimistically update to include the new memory - queryClient.setQueryData(["documents-with-memories", project], (old: any) => { - console.log("🔄 Old data:", old) - const newData = old - ? { - ...old, - documents: [optimisticMemory, ...(old.documents || [])], - totalCount: (old.totalCount || 0) + 1, - } - : { documents: [optimisticMemory], totalCount: 1 } - console.log("✨ New data:", newData) - return newData - }) - - console.log("✅ onMutate completed") - return { previousMemories, optimisticId: optimisticMemory.id } - }, - // If the mutation fails, roll back to the previous value - onError: (error, variables, context) => { - if (context?.previousMemories) { - queryClient.setQueryData( - ["documents-with-memories", variables.project], - context.previousMemories, - ) - } - }, - onSuccess: (_data, variables) => { - analytics.memoryAdded({ - type: variables.contentType === "link" ? "link" : "note", - project_id: variables.project, - content_length: variables.content.length, - }) - - queryClient.invalidateQueries({ queryKey: ["documents-with-memories", variables.project] }) - - setTimeout(() => { - queryClient.invalidateQueries({ queryKey: ["documents-with-memories", variables.project] }) - }, 30000) // 30 seconds - - setTimeout(() => { - queryClient.invalidateQueries({ queryKey: ["documents-with-memories", variables.project] }) - }, 120000) // 2 minutes - - setShowAddDialog(false) - onClose?.() - }, - }) - - const fileUploadMutation = useMutation({ - mutationFn: async ({ - file, - title, - description, - project, - }: { - file: File - title?: string - description?: string - project: string - }) => { - // TEMPORARILY DISABLED: Limit check disabled - // Check if user can add more memories - // if (!canAddMemory && !isProUser) { - // throw new Error( - // `Free plan limit reached (${memoriesLimit} memories). Upgrade to Pro for up to 500 memories.`, - // ); - // } - - const formData = new FormData() - formData.append("file", file) - formData.append("containerTags", JSON.stringify([project])) - - const response = await fetch( - `${process.env.NEXT_PUBLIC_BACKEND_URL}/v3/memories/file`, - { - method: "POST", - body: formData, - credentials: "include", - }, - ) - - if (!response.ok) { - const error = await response.json() - throw new Error(error.error || "Failed to upload file") - } - - const data = await response.json() - - // If we have metadata, we can update the document after creation - if (title || description) { - await $fetch(`@patch/memories/${data.id}`, { - body: { - metadata: { - ...(title && { title }), - ...(description && { description }), - sm_source: "consumer", // Use "consumer" source to bypass limits - }, - }, - }) - } - - return data - }, - // Optimistic update - onMutate: async ({ file, title, description, project }) => { - // Cancel any outgoing refetches - await queryClient.cancelQueries({ queryKey: ["documents-with-memories", project] }) - - // Snapshot the previous value - const previousMemories = queryClient.getQueryData([ - "documents-with-memories", - project, - ]) - - // Create optimistic memory for the file - const optimisticMemory = { - id: `temp-file-${Date.now()}`, - content: "", - url: null, - title: title || file.name, - description: description || `Uploading ${file.name}...`, - containerTags: [project], - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), - status: "processing", - type: "file", - metadata: { - fileName: file.name, - fileSize: file.size, - mimeType: file.type, - }, - memoryEntries: [], - } - - // Optimistically update to include the new memory - queryClient.setQueryData(["documents-with-memories", project], (old: any) => { - if (!old) return { documents: [optimisticMemory], totalCount: 1 } - return { - ...old, - documents: [optimisticMemory, ...(old.documents || [])], - totalCount: (old.totalCount || 0) + 1, - } - }) - - // Return a context object with the snapshotted value - return { previousMemories } - }, - // If the mutation fails, roll back to the previous value - onError: (error, variables, context) => { - if (context?.previousMemories) { - queryClient.setQueryData( - ["documents-with-memories", variables.project], - context.previousMemories, - ) - } - toast.error("Failed to upload file", { - description: error instanceof Error ? error.message : "Unknown error", - }) - }, - onSuccess: (_data, variables) => { - analytics.memoryAdded({ - type: "file", - project_id: variables.project, - file_size: variables.file.size, - file_type: variables.file.type, - }) - toast.success("File uploaded successfully!", { - description: "Your file is being processed", - }) - setShowAddDialog(false) - onClose?.() - }, - // Always refetch after error or success - onSettled: () => { - queryClient.invalidateQueries({ queryKey: ["documents-with-memories"] }) - }, - }) - - return ( - - {showAddDialog && ( - { - setShowAddDialog(open) - if (!open) onClose?.() - }} - open={showAddDialog} - > - - - - Add to Memory - - Save any webpage, article, or file to your memory - - { - -
- {memoriesUsed} of {memoriesLimit} memories used - {!isProUser && memoriesUsed >= memoriesLimit * 0.8 && ( - - • {memoriesLimit - memoriesUsed} remaining - - )} -
- {!canAddMemory && !isProUser && ( - -

- You've reached the free plan limit. - - for up to 5000 memories. -

-
- )} -
- } -
- - - setActiveTab(v as "note" | "link" | "file" | "connect") - } - value={activeTab} - > - - - - Note - - - - Link - - - - File - - - - Connect - - - - -
{ - e.preventDefault() - e.stopPropagation() - addContentForm.handleSubmit() - }} - > -
- {/* Note Input */} - - - { - if (!value || value.trim() === "") { - return "Note is required" - } - return undefined - }, - }} - > - {({ state, handleChange, handleBlur }) => ( - <> -