aboutsummaryrefslogtreecommitdiff
path: root/packages/memory-graph/src/components/memory-graph.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'packages/memory-graph/src/components/memory-graph.tsx')
-rw-r--r--packages/memory-graph/src/components/memory-graph.tsx284
1 files changed, 159 insertions, 125 deletions
diff --git a/packages/memory-graph/src/components/memory-graph.tsx b/packages/memory-graph/src/components/memory-graph.tsx
index 21d4a08f..8f356d2f 100644
--- a/packages/memory-graph/src/components/memory-graph.tsx
+++ b/packages/memory-graph/src/components/memory-graph.tsx
@@ -1,21 +1,21 @@
-"use client";
-
-import { GlassMenuEffect } from "@/ui/glass-effect";
-import { AnimatePresence } from "motion/react";
-import { useCallback, useEffect, useMemo, useRef, useState } from "react";
-import { GraphCanvas } from "./graph-canvas";
-import { useGraphData } from "@/hooks/use-graph-data";
-import { useGraphInteractions } from "@/hooks/use-graph-interactions";
-import { injectStyles } from "@/lib/inject-styles";
-import { Legend } from "./legend";
-import { LoadingIndicator } from "./loading-indicator";
-import { NavigationControls } from "./navigation-controls";
-import { NodeDetailPanel } from "./node-detail-panel";
-import { SpacesDropdown } from "./spaces-dropdown";
-import * as styles from "./memory-graph.css";
-import { defaultTheme } from "@/styles/theme.css";
-
-import type { MemoryGraphProps } from "@/types";
+"use client"
+
+import { GlassMenuEffect } from "@/ui/glass-effect"
+import { AnimatePresence } from "motion/react"
+import { useCallback, useEffect, useMemo, useRef, useState } from "react"
+import { GraphCanvas } from "./graph-canvas"
+import { useGraphData } from "@/hooks/use-graph-data"
+import { useGraphInteractions } from "@/hooks/use-graph-interactions"
+import { injectStyles } from "@/lib/inject-styles"
+import { Legend } from "./legend"
+import { LoadingIndicator } from "./loading-indicator"
+import { NavigationControls } from "./navigation-controls"
+import { NodeDetailPanel } from "./node-detail-panel"
+import { SpacesDropdown } from "./spaces-dropdown"
+import * as styles from "./memory-graph.css"
+import { defaultTheme } from "@/styles/theme.css"
+
+import type { MemoryGraphProps } from "@/types"
export const MemoryGraph = ({
children,
@@ -34,23 +34,45 @@ export const MemoryGraph = ({
occludedRightPx = 0,
autoLoadOnViewport = true,
themeClassName,
+ selectedSpace: externalSelectedSpace,
+ onSpaceChange: externalOnSpaceChange,
+ memoryLimit,
+ isExperimental,
}: MemoryGraphProps) => {
// Inject styles on first render (client-side only)
useEffect(() => {
- injectStyles();
- }, []);
+ injectStyles()
+ }, [])
// Derive totalLoaded from documents if not provided
- const effectiveTotalLoaded = totalLoaded ?? documents.length;
+ const effectiveTotalLoaded = totalLoaded ?? documents.length
// No-op for loadMoreDocuments if not provided
- const effectiveLoadMoreDocuments = loadMoreDocuments ?? (async () => {});
+ const effectiveLoadMoreDocuments = loadMoreDocuments ?? (async () => {})
// Derive showSpacesSelector from variant if not explicitly provided
// console variant shows spaces selector, consumer variant hides it
- const finalShowSpacesSelector = showSpacesSelector ?? (variant === "console");
+ const finalShowSpacesSelector = showSpacesSelector ?? variant === "console"
- const [selectedSpace, setSelectedSpace] = useState<string>("all");
- const [containerSize, setContainerSize] = useState({ width: 0, height: 0 });
- const containerRef = useRef<HTMLDivElement>(null);
+ // Internal state for controlled/uncontrolled pattern
+ const [internalSelectedSpace, setInternalSelectedSpace] =
+ useState<string>("all")
+
+ const [containerSize, setContainerSize] = useState({ width: 0, height: 0 })
+ const containerRef = useRef<HTMLDivElement>(null)
+
+ // Use external state if provided, otherwise use internal state
+ const selectedSpace = externalSelectedSpace ?? internalSelectedSpace
+
+ // Handle space change
+ const handleSpaceChange = useCallback(
+ (spaceId: string) => {
+ if (externalOnSpaceChange) {
+ externalOnSpaceChange(spaceId)
+ } else {
+ setInternalSelectedSpace(spaceId)
+ }
+ },
+ [externalOnSpaceChange],
+ )
// Create data object with pagination to satisfy type requirements
const data = useMemo(() => {
@@ -64,8 +86,8 @@ export const MemoryGraph = ({
totalPages: 1,
},
}
- : null;
- }, [documents]);
+ : null
+ }, [documents])
// Graph interactions with variant-specific settings
const {
@@ -95,7 +117,7 @@ export const MemoryGraph = ({
centerViewportOn,
zoomIn,
zoomOut,
- } = useGraphInteractions(variant);
+ } = useGraphInteractions(variant)
// Graph data
const { nodes, edges } = useGraphData(
@@ -103,14 +125,13 @@ export const MemoryGraph = ({
selectedSpace,
nodePositions,
draggingNodeId,
- );
+ memoryLimit,
+ )
// Auto-fit once per unique highlight set to show the full graph for context
- const lastFittedHighlightKeyRef = useRef<string>("");
+ const lastFittedHighlightKeyRef = useRef<string>("")
useEffect(() => {
- const highlightKey = highlightsVisible
- ? highlightDocumentIds.join("|")
- : "";
+ const highlightKey = highlightsVisible ? highlightDocumentIds.join("|") : ""
if (
highlightKey &&
highlightKey !== lastFittedHighlightKeyRef.current &&
@@ -121,8 +142,8 @@ export const MemoryGraph = ({
autoFitToViewport(nodes, containerSize.width, containerSize.height, {
occludedRightPx,
animate: true,
- });
- lastFittedHighlightKeyRef.current = highlightKey;
+ })
+ lastFittedHighlightKeyRef.current = highlightKey
}
}, [
highlightsVisible,
@@ -132,10 +153,10 @@ export const MemoryGraph = ({
nodes.length,
occludedRightPx,
autoFitToViewport,
- ]);
+ ])
// Auto-fit graph when component mounts or nodes change significantly
- const hasAutoFittedRef = useRef(false);
+ const hasAutoFittedRef = useRef(false)
useEffect(() => {
// Only auto-fit once when we have nodes and container size
if (
@@ -147,90 +168,85 @@ export const MemoryGraph = ({
// Auto-fit to show all content for both variants
// Add a small delay to ensure the canvas is fully initialized
const timer = setTimeout(() => {
- autoFitToViewport(nodes, containerSize.width, containerSize.height);
- hasAutoFittedRef.current = true;
- }, 100);
-
- return () => clearTimeout(timer);
+ autoFitToViewport(nodes, containerSize.width, containerSize.height)
+ hasAutoFittedRef.current = true
+ }, 100)
+
+ return () => clearTimeout(timer)
}
- }, [
- nodes,
- containerSize.width,
- containerSize.height,
- autoFitToViewport,
- ]);
+ }, [nodes, containerSize.width, containerSize.height, autoFitToViewport])
// Reset auto-fit flag when nodes array becomes empty (switching views)
useEffect(() => {
if (nodes.length === 0) {
- hasAutoFittedRef.current = false;
+ hasAutoFittedRef.current = false
}
- }, [nodes.length]);
+ }, [nodes.length])
// Extract unique spaces from memories and calculate counts
const { availableSpaces, spaceMemoryCounts } = useMemo(() => {
- if (!data?.documents) return { availableSpaces: [], spaceMemoryCounts: {} };
+ if (!data?.documents) return { availableSpaces: [], spaceMemoryCounts: {} }
- const spaceSet = new Set<string>();
- const counts: Record<string, number> = {};
+ const spaceSet = new Set<string>()
+ const counts: Record<string, number> = {}
data.documents.forEach((doc) => {
doc.memoryEntries.forEach((memory) => {
- const spaceId = memory.spaceContainerTag || memory.spaceId || "default";
- spaceSet.add(spaceId);
- counts[spaceId] = (counts[spaceId] || 0) + 1;
- });
- });
+ const spaceId = memory.spaceContainerTag || memory.spaceId || "default"
+ spaceSet.add(spaceId)
+ counts[spaceId] = (counts[spaceId] || 0) + 1
+ })
+ })
return {
availableSpaces: Array.from(spaceSet).sort(),
spaceMemoryCounts: counts,
- };
- }, [data]);
+ }
+ }, [data])
// Handle container resize
useEffect(() => {
const updateSize = () => {
if (containerRef.current) {
- const newWidth = containerRef.current.clientWidth;
- const newHeight = containerRef.current.clientHeight;
-
+ const newWidth = containerRef.current.clientWidth
+ const newHeight = containerRef.current.clientHeight
+
// Only update if size actually changed and is valid
setContainerSize((prev) => {
if (prev.width !== newWidth || prev.height !== newHeight) {
- return { width: newWidth, height: newHeight };
+ return { width: newWidth, height: newHeight }
}
- return prev;
- });
+ return prev
+ })
}
- };
+ }
// Use a slight delay to ensure DOM is fully rendered
- const timer = setTimeout(updateSize, 0);
- updateSize(); // Also call immediately
-
- window.addEventListener("resize", updateSize);
-
+ const timer = setTimeout(updateSize, 0)
+ updateSize() // Also call immediately
+
+ window.addEventListener("resize", updateSize)
+
// Use ResizeObserver for more accurate container size detection
- const resizeObserver = new ResizeObserver(updateSize);
+ const resizeObserver = new ResizeObserver(updateSize)
if (containerRef.current) {
- resizeObserver.observe(containerRef.current);
+ resizeObserver.observe(containerRef.current)
}
-
+
return () => {
- clearTimeout(timer);
- window.removeEventListener("resize", updateSize);
- resizeObserver.disconnect();
- };
- }, []);
+ clearTimeout(timer)
+ window.removeEventListener("resize", updateSize)
+ resizeObserver.disconnect()
+ }
+ }, [])
// Enhanced node drag start that includes nodes data
const handleNodeDragStartWithNodes = useCallback(
(nodeId: string, e: React.MouseEvent) => {
- handleNodeDragStart(nodeId, e, nodes);
+ handleNodeDragStart(nodeId, e, nodes)
},
[handleNodeDragStart, nodes],
- );
+ )
// Navigation callbacks
const handleCenter = useCallback(() => {
@@ -239,35 +255,50 @@ export const MemoryGraph = ({
let sumX = 0
let sumY = 0
let count = 0
-
+
nodes.forEach((node) => {
sumX += node.x
sumY += node.y
count++
})
-
+
if (count > 0) {
const centerX = sumX / count
const centerY = sumY / count
- centerViewportOn(centerX, centerY, containerSize.width, containerSize.height)
+ centerViewportOn(
+ centerX,
+ centerY,
+ containerSize.width,
+ containerSize.height,
+ )
}
}
}, [nodes, centerViewportOn, containerSize.width, containerSize.height])
const handleAutoFit = useCallback(() => {
- if (nodes.length > 0 && containerSize.width > 0 && containerSize.height > 0) {
+ if (
+ nodes.length > 0 &&
+ containerSize.width > 0 &&
+ containerSize.height > 0
+ ) {
autoFitToViewport(nodes, containerSize.width, containerSize.height, {
occludedRightPx,
animate: true,
})
}
- }, [nodes, containerSize.width, containerSize.height, occludedRightPx, autoFitToViewport])
+ }, [
+ nodes,
+ containerSize.width,
+ containerSize.height,
+ occludedRightPx,
+ autoFitToViewport,
+ ])
// Get selected node data
const selectedNodeData = useMemo(() => {
- if (!selectedNode) return null;
- return nodes.find((n) => n.id === selectedNode) || null;
- }, [selectedNode, nodes]);
+ if (!selectedNode) return null
+ return nodes.find((n) => n.id === selectedNode) || null
+ }, [selectedNode, nodes])
// Viewport-based loading: load more when most documents are visible (optional)
const checkAndLoadMore = useCallback(() => {
@@ -277,7 +308,7 @@ export const MemoryGraph = ({
!data?.documents ||
data.documents.length === 0
)
- return;
+ return
// Calculate viewport bounds
const viewportBounds = {
@@ -285,26 +316,26 @@ export const MemoryGraph = ({
right: (-panX + containerSize.width) / zoom + 200,
top: -panY / zoom - 200,
bottom: (-panY + containerSize.height) / zoom + 200,
- };
+ }
// Count visible documents
const visibleDocuments = data.documents.filter((doc) => {
const docNodes = nodes.filter(
(node) => node.type === "document" && node.data.id === doc.id,
- );
+ )
return docNodes.some(
(node) =>
node.x >= viewportBounds.left &&
node.x <= viewportBounds.right &&
node.y >= viewportBounds.top &&
node.y <= viewportBounds.bottom,
- );
- });
+ )
+ })
// If 80% or more of documents are visible, load more
- const visibilityRatio = visibleDocuments.length / data.documents.length;
+ const visibilityRatio = visibleDocuments.length / data.documents.length
if (visibilityRatio >= 0.8) {
- effectiveLoadMoreDocuments();
+ effectiveLoadMoreDocuments()
}
}, [
isLoadingMore,
@@ -317,35 +348,35 @@ export const MemoryGraph = ({
containerSize.height,
nodes,
effectiveLoadMoreDocuments,
- ]);
+ ])
// Throttled version to avoid excessive checks
- const lastLoadCheckRef = useRef(0);
+ const lastLoadCheckRef = useRef(0)
const throttledCheckAndLoadMore = useCallback(() => {
- const now = Date.now();
+ const now = Date.now()
if (now - lastLoadCheckRef.current > 1000) {
// Check at most once per second
- lastLoadCheckRef.current = now;
- checkAndLoadMore();
+ lastLoadCheckRef.current = now
+ checkAndLoadMore()
}
- }, [checkAndLoadMore]);
+ }, [checkAndLoadMore])
// Monitor viewport changes to trigger loading
useEffect(() => {
- if (!autoLoadOnViewport) return;
- throttledCheckAndLoadMore();
- }, [throttledCheckAndLoadMore, autoLoadOnViewport]);
+ if (!autoLoadOnViewport) return
+ throttledCheckAndLoadMore()
+ }, [throttledCheckAndLoadMore, autoLoadOnViewport])
// Initial load trigger when graph is first rendered
useEffect(() => {
- if (!autoLoadOnViewport) return;
+ if (!autoLoadOnViewport) return
if (data?.documents && data.documents.length > 0 && hasMore) {
// Start loading more documents after initial render
setTimeout(() => {
- throttledCheckAndLoadMore();
- }, 500); // Small delay to allow initial layout
+ throttledCheckAndLoadMore()
+ }, 500) // Small delay to allow initial layout
}
- }, [data, hasMore, throttledCheckAndLoadMore, autoLoadOnViewport]);
+ }, [data, hasMore, throttledCheckAndLoadMore, autoLoadOnViewport])
if (error) {
return (
@@ -359,17 +390,19 @@ export const MemoryGraph = ({
</div>
</div>
</div>
- );
+ )
}
return (
- <div className={`${themeClassName ?? defaultTheme} ${styles.mainContainer}`}>
- {/* Spaces selector - only shown for console */}
- {finalShowSpacesSelector && availableSpaces.length > 0 && (
+ <div
+ className={`${themeClassName ?? defaultTheme} ${styles.mainContainer}`}
+ >
+ {/* Spaces selector - only shown for console variant */}
+ {variant === "console" && availableSpaces.length > 0 && (
<div className={styles.spacesSelectorContainer}>
<SpacesDropdown
availableSpaces={availableSpaces}
- onSpaceChange={setSelectedSpace}
+ onSpaceChange={handleSpaceChange}
selectedSpace={selectedSpace}
spaceMemoryCounts={spaceMemoryCounts}
/>
@@ -411,11 +444,8 @@ export const MemoryGraph = ({
)}
{/* Graph container */}
- <div
- className={styles.graphContainer}
- ref={containerRef}
- >
- {(containerSize.width > 0 && containerSize.height > 0) && (
+ <div className={styles.graphContainer} ref={containerRef}>
+ {containerSize.width > 0 && containerSize.height > 0 && (
<GraphCanvas
draggingNodeId={draggingNodeId}
edges={edges}
@@ -446,8 +476,12 @@ export const MemoryGraph = ({
{containerSize.width > 0 && (
<NavigationControls
onCenter={handleCenter}
- onZoomIn={() => zoomIn(containerSize.width / 2, containerSize.height / 2)}
- onZoomOut={() => zoomOut(containerSize.width / 2, containerSize.height / 2)}
+ onZoomIn={() =>
+ zoomIn(containerSize.width / 2, containerSize.height / 2)
+ }
+ onZoomOut={() =>
+ zoomOut(containerSize.width / 2, containerSize.height / 2)
+ }
onAutoFit={handleAutoFit}
nodes={nodes}
className={styles.navControlsContainer}
@@ -455,5 +489,5 @@ export const MemoryGraph = ({
)}
</div>
</div>
- );
-};
+ )
+}