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.
This commit is contained in:
Mahesh Sanikommu 2026-05-18 10:06:07 -07:00
parent da47b1d7d3
commit 14e688087d
3 changed files with 58 additions and 4 deletions

View file

@ -46,6 +46,7 @@ export const GraphLayoutView = memo(function GraphLayoutView({
maxNodes={undefined}
canvasRef={canvasRef}
onOpenDocument={onOpenDocument}
autoFetchAll
/>
</div>

View file

@ -1,7 +1,7 @@
"use client"
import { useInfiniteQuery } from "@tanstack/react-query"
import { useMemo } from "react"
import { useEffect, useMemo } from "react"
import { $fetch } from "@lib/api"
import type {
GraphApiDocument,
@ -15,6 +15,7 @@ interface UseGraphApiOptions {
containerTags?: string[]
documentIds?: string[]
enabled?: boolean
autoFetchAll?: boolean
}
interface ApiMemoryEntry {
@ -108,7 +109,12 @@ function toGraphDocument(
}
export function useGraphApi(options: UseGraphApiOptions = {}) {
const { containerTags, documentIds, enabled = true } = options
const {
containerTags,
documentIds,
enabled = true,
autoFetchAll = false,
} = options
const filteredDocumentIds = documentIds?.filter(Boolean)
const hasDocumentIds =
filteredDocumentIds != null && filteredDocumentIds.length > 0
@ -163,6 +169,19 @@ export function useGraphApi(options: UseGraphApiOptions = {}) {
enabled,
})
useEffect(() => {
if (!autoFetchAll || !enabled) return
if (!hasNextPage || isPending || isFetchingNextPage) return
fetchNextPage()
}, [
autoFetchAll,
enabled,
hasNextPage,
isPending,
isFetchingNextPage,
fetchNextPage,
])
const documents = useMemo(() => {
if (!data?.pages) return []
return data.pages.flatMap((page) =>

View file

@ -1,9 +1,11 @@
"use client"
import { useEffect, useRef, useState } from "react"
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
@ -21,6 +23,7 @@ export interface MemoryGraphWrapperProps {
onSlideshowStop?: () => void
canvasRef?: React.RefObject<HTMLCanvasElement | null>
onOpenDocument?: (documentId: string) => void
autoFetchAll?: boolean
}
export function MemoryGraph({
@ -32,6 +35,7 @@ export function MemoryGraph({
documentIds,
maxNodes,
canvasRef,
autoFetchAll = false,
...rest
}: MemoryGraphWrapperProps) {
const [containerSize, setContainerSize] = useState({ width: 0, height: 0 })
@ -60,10 +64,22 @@ export function MemoryGraph({
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="size-full [&>div]:!bg-none">
<div ref={containerRef} className="relative size-full [&>div]:!bg-none">
<MemoryGraphBase
documents={documents}
isLoading={externalIsLoading || apiIsLoading}
@ -85,6 +101,24 @@ export function MemoryGraph({
>
{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>
)
}