supermemory/apps/web/components/memory-graph/memory-graph-wrapper.tsx
Mahesh Sanikommu 14e688087d feat(graph): auto-paginate consumer graph + loading progress chip
Accounts with more than 100 documents previously only saw the newest
100 in the memory graph until they manually zoomed out (and even then,
only one extra page per zoom). For users with thousands of documents
this made the graph appear nearly empty.

Adds an opt-in `autoFetchAll` flag to `useGraphApi` that chains
`fetchNextPage()` until every page is loaded, and renders a top-right
progress chip ("X memories - Y/Z documents") with a spinner while pages
stream in. The chip self-hides on completion. Only the consumer
GraphLayoutView opts in; modal and chat-rail variants remain scoped to
their existing single-page behaviour.

This is a stopgap for high-memory users. A proper server-side
sampling/clustering endpoint is needed to scale past tens of thousands
of nodes and will be tracked separately.
2026-05-18 10:06:07 -07:00

124 lines
3.3 KiB
TypeScript

"use client"
import { useEffect, useMemo, useRef, useState } from "react"
import { Loader2 } from "lucide-react"
import { MemoryGraph as MemoryGraphBase } from "@supermemory/memory-graph"
import type { GraphThemeColors } from "@supermemory/memory-graph"
import { useGraphApi } from "./hooks/use-graph-api"
import { cn } from "@lib/utils"
export interface MemoryGraphWrapperProps {
children?: React.ReactNode
isLoading?: boolean
error?: Error | null
variant?: "console" | "consumer"
legendId?: string
highlightDocumentIds?: string[]
highlightsVisible?: boolean
containerTags?: string[]
documentIds?: string[]
maxNodes?: number
isSlideshowActive?: boolean
onSlideshowNodeChange?: (nodeId: string | null) => void
onSlideshowStop?: () => void
canvasRef?: React.RefObject<HTMLCanvasElement | null>
onOpenDocument?: (documentId: string) => void
autoFetchAll?: boolean
}
export function MemoryGraph({
children,
isLoading: externalIsLoading = false,
error: externalError = null,
variant = "console",
containerTags,
documentIds,
maxNodes,
canvasRef,
autoFetchAll = false,
...rest
}: MemoryGraphWrapperProps) {
const [containerSize, setContainerSize] = useState({ width: 0, height: 0 })
const containerRef = useRef<HTMLDivElement>(null)
useEffect(() => {
const el = containerRef.current
if (!el) return
const ro = new ResizeObserver(() => {
setContainerSize({ width: el.clientWidth, height: el.clientHeight })
})
ro.observe(el)
setContainerSize({ width: el.clientWidth, height: el.clientHeight })
return () => ro.disconnect()
}, [])
const {
documents,
isLoading: apiIsLoading,
isLoadingMore,
error: apiError,
hasMore,
loadMore,
totalCount,
} = useGraphApi({
containerTags,
documentIds,
enabled: containerSize.width > 0 && containerSize.height > 0,
autoFetchAll,
})
const loadedDocCount = documents.length
const loadedMemoryCount = useMemo(
() => documents.reduce((sum, doc) => sum + doc.memories.length, 0),
[documents],
)
const showProgress =
autoFetchAll &&
!apiIsLoading &&
totalCount > 0 &&
(hasMore || isLoadingMore || loadedDocCount < totalCount)
return (
<div ref={containerRef} className="relative size-full [&>div]:!bg-none">
<MemoryGraphBase
documents={documents}
isLoading={externalIsLoading || apiIsLoading}
isLoadingMore={isLoadingMore}
onLoadMore={hasMore ? () => loadMore() : undefined}
hasMore={hasMore}
error={externalError || apiError}
variant={variant}
maxNodes={maxNodes}
canvasRef={canvasRef}
totalCount={totalCount}
colors={
{
bg: "transparent",
edgeDerives: "#9ca3af",
} satisfies Partial<GraphThemeColors>
}
{...rest}
>
{children}
</MemoryGraphBase>
{showProgress && (
<div
className={cn(
"absolute right-3 top-3 z-15 md:right-4 md:top-4",
"flex items-center gap-2 rounded-full",
"bg-background/85 px-3 py-1.5 text-xs backdrop-blur-md",
"border border-border/60 shadow-sm",
)}
aria-live="polite"
>
<Loader2 className="size-3 shrink-0 animate-spin text-muted-foreground" />
<span className="tabular-nums">
{loadedMemoryCount.toLocaleString()} memories ·{" "}
{loadedDocCount.toLocaleString()}/{totalCount.toLocaleString()}{" "}
documents
</span>
</div>
)}
</div>
)
}