"use client" import { useIsMobile } from "@hooks/use-mobile" import { useAuth } from "@lib/auth-context" import { $fetch } from "@repo/lib/api" 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 { Button } from "@ui/components/button" import { GlassMenuEffect } from "@ui/other/glass-effect" import { HelpCircle, LayoutGrid, List, LoaderIcon, MessageSquare, Unplug, } from "lucide-react" import { AnimatePresence, motion } from "motion/react" import Link from "next/link" import { useCallback, useEffect, useMemo, useState } from "react" import type { z } from "zod" 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] const MemoryGraphPage = () => { const { documentIds: allHighlightDocumentIds } = useGraphHighlights() const isMobile = useIsMobile() const { viewMode, setViewMode, isInitialized } = useViewMode() 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 [showConnectAIModal, setShowConnectAIModal] = useState(false) const [isHelpHovered, setIsHelpHovered] = 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 ?? [] }, staleTime: 5 * 60 * 1000, }) const isCurrentProjectExperimental = !!projectsMeta.find( (p: any) => p.containerTag === selectedProject, )?.isExperimental // Tour state const [showTourDialog, setShowTourDialog] = useState(false) // Define tour steps with useMemo to prevent recreation const tourSteps: TourStep[] = useMemo(() => { return [ { content: (

Memories Overview

This is your memory graph. Each node represents a memory, and connections show relationships between them.

), selectorId: TOUR_STEP_IDS.MEMORY_GRAPH, position: "center", }, { content: (

Add Memories

Click here to add new memories to your knowledge base. You can add text, links, or connect external sources.

), selectorId: TOUR_STEP_IDS.MENU_ADD_MEMORY, position: "right", }, { content: (

Connections

Connect your external accounts like Google Drive, Notion, or OneDrive to automatically sync and organize your content.

), selectorId: TOUR_STEP_IDS.MENU_CONNECTIONS, position: "right", }, { content: (

Projects

Organize your memories into projects. Switch between different contexts easily.

), selectorId: TOUR_STEP_IDS.MENU_PROJECTS, position: "right", }, { content: (

MCP Servers

Access Model Context Protocol servers to give AI tools access to your memories securely.

), selectorId: TOUR_STEP_IDS.MENU_MCP, position: "right", }, { content: (

Billing

Manage your subscription and billing information.

), selectorId: TOUR_STEP_IDS.MENU_BILLING, position: "right", }, { content: (

View Toggle

Switch between graph view and list view to see your memories in different ways.

), selectorId: TOUR_STEP_IDS.VIEW_TOGGLE, position: "left", }, { content: (

Legend

Understand the different types of nodes and connections in your memory graph.

), selectorId: TOUR_STEP_IDS.LEGEND, position: "left", }, { content: (

Chat Assistant

Ask questions or add new memories using our AI-powered chat interface.

), selectorId: TOUR_STEP_IDS.FLOATING_CHAT, position: "left", }, ] }, []) // Check if tour has been completed before useEffect(() => { const hasCompletedTour = localStorage.getItem(TOUR_STORAGE_KEY) === "true" if (!hasCompletedTour && !isTourCompleted) { const timer = setTimeout(() => { setShowTourDialog(true) setShowConnectAIModal(false) }, 1000) // Show after 1 second return () => clearTimeout(timer) } }, [isTourCompleted]) // Set up tour steps useEffect(() => { setSteps(tourSteps) }, [setSteps, tourSteps]) // Save tour completion to localStorage useEffect(() => { if (isTourCompleted) { localStorage.setItem(TOUR_STORAGE_KEY, "true") } }, [isTourCompleted]) // Progressive loading via useInfiniteQuery const IS_DEV = process.env.NODE_ENV === "development" const PAGE_SIZE = IS_DEV ? 100 : 100 const MAX_TOTAL = 1000 const { data, error, isPending, isFetchingNextPage, hasNextPage, fetchNextPage, } = useInfiniteQuery({ queryKey: ["documents-with-memories", selectedProject], initialPageParam: 1, queryFn: async ({ pageParam }) => { const response = await $fetch("@post/documents/documents", { body: { page: pageParam as number, limit: (pageParam as number) === 1 ? (IS_DEV ? 500 : 500) : PAGE_SIZE, sort: "createdAt", order: "desc", containerTags: selectedProject ? [selectedProject] : undefined, }, disableValidation: true, }) if (response.error) { throw new Error(response.error?.message || "Failed to fetch documents") } return response.data }, getNextPageParam: (lastPage, allPages) => { const loaded = allPages.reduce( (acc, p) => acc + (p.documents?.length ?? 0), 0, ) if (loaded >= MAX_TOTAL) return undefined const { currentPage, totalPages } = lastPage.pagination if (currentPage < totalPages) { return currentPage + 1 } return undefined }, staleTime: 5 * 60 * 1000, }) const baseDocuments = useMemo(() => { 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]) const totalLoaded = allDocuments.length const hasMore = hasNextPage const isLoadingMore = isFetchingNextPage const loadMoreDocuments = useCallback(async (): Promise => { if (hasNextPage && !isFetchingNextPage) { await fetchNextPage() return } return }, [hasNextPage, isFetchingNextPage, fetchNextPage]) // Reset injected docs when project changes 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() 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) } 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", { body: { ids: missing, by: "customId", 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 setInjectedDocs((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) } } 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], ) useEffect(() => { const hasCompletedTour = localStorage.getItem(TOUR_STORAGE_KEY) === "true" if (hasCompletedTour && allDocuments.length === 0 && !showTourDialog) { setShowConnectAIModal(true) } else if (showTourDialog) { setShowConnectAIModal(false) } }, [allDocuments.length, showTourDialog]) // Prevent body scrolling useEffect(() => { 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 = "" } }, []) return (
{/* Main content area */}
handleViewModeChange("graph")} transition={{ duration: 0.2 }} whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }} > {viewMode === "graph" && ( )} Graph handleViewModeChange("list")} transition={{ duration: 0.2 }} whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }} > {viewMode === "list" && ( )} List
{/* Animated content switching */} {viewMode === "graph" ? (

Get Started with supermemory

Click here to set up your AI connection

or

) : (

Get Started with supermemory

Click here to set up your AI connection

or

)}
{/* Top Bar */}
{/* Floating Open Chat Button */} {!isOpen && !isMobile && ( )}
{/* Chat panel - positioned absolutely */} {showAddMemoryView && ( setShowAddMemoryView(false)} /> )} {/* Tour Alert Dialog */} {/* Referral/Upgrade Modal */} setShowReferralModal(false)} />
) } // Wrapper component to handle auth and waitlist checks export default function Page() { const { user, session } = useAuth() useEffect(() => { const url = new URL(window.location.href) const authenticateChromeExtension = url.searchParams.get( "extension-auth-success", ) if (authenticateChromeExtension) { const sessionToken = session?.token const userData = { email: user?.email, name: user?.name, userId: user?.id, } if (sessionToken && userData?.email) { const encodedToken = encodeURIComponent(sessionToken) window.postMessage({ token: encodedToken, userData }, "*") url.searchParams.delete("extension-auth-success") window.history.replaceState({}, "", url.toString()) } } }, [user, session]) // Show loading state while checking authentication and waitlist status if (!user) { return (

Loading...

) } // If we have a user and they have access, show the main component return ( <> ) }