"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 { 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 { 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); // 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); }, 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 ? 3 : 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/memories/documents", { body: { page: pageParam as number, limit: (pageParam as number) === 1 ? (IS_DEV ? 3 : 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], ); // 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' ? (

No Memories to Visualize

) : (

No Memories to Visualize

)}
{/* 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 router = useRouter(); const { user } = useAuth(); // Check waitlist status const { data: waitlistStatus, isLoading: isCheckingWaitlist, error: waitlistError, } = useQuery({ queryKey: ["waitlist-status", user?.id], queryFn: async () => { try { const response = await $fetch("@get/waitlist/status"); return response.data; } catch (error) { console.error("Error checking waitlist status:", error); // Return null to indicate error, will handle in useEffect 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"); } }, []); // Show loading state while checking authentication and waitlist status if (!user || isCheckingWaitlist) { return (

Loading...

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