mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-10-11 03:37:56 +00:00
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.
124 lines
3.3 KiB
TypeScript
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>
|
|
)
|
|
}
|