From 97599c42ef7259b59f205e7ed025223662096445 Mon Sep 17 00:00:00 2001 From: Vidya Rupak Date: Mon, 22 Dec 2025 15:15:06 -0700 Subject: [PATCH] added slideshow mode to automatically cycle through nodes with smooth animations and physics --- apps/memory-graph-playground/src/app/page.tsx | 69 ++++++++++- packages/memory-graph/CHANGELOG.md | 13 +++ .../src/components/memory-graph.tsx | 107 ++++++++++++++++++ .../src/components/node-popover.tsx | 33 ++++-- packages/memory-graph/src/types.ts | 6 + 5 files changed, 212 insertions(+), 16 deletions(-) diff --git a/apps/memory-graph-playground/src/app/page.tsx b/apps/memory-graph-playground/src/app/page.tsx index 7192c4c2..eefd7217 100644 --- a/apps/memory-graph-playground/src/app/page.tsx +++ b/apps/memory-graph-playground/src/app/page.tsx @@ -29,6 +29,10 @@ export default function Home() { // State for controlled space selection const [selectedSpace, setSelectedSpace] = useState("all") + // State for slideshow + const [isSlideshowActive, setIsSlideshowActive] = useState(false) + const [currentSlideshowNode, setCurrentSlideshowNode] = useState(null) + const PAGE_SIZE = 500 const fetchDocuments = useCallback( @@ -109,6 +113,18 @@ export default function Home() { setSelectedSpace("all") } + // Toggle slideshow + const handleToggleSlideshow = () => { + setIsSlideshowActive((prev) => !prev) + } + + // Handle slideshow node change + const handleSlideshowNodeChange = useCallback((nodeId: string | null) => { + // Track which node is being shown in slideshow + setCurrentSlideshowNode(nodeId) + console.log("Slideshow showing node:", nodeId) + }, []) + return (
{/* Header */} @@ -158,12 +174,50 @@ export default function Home() {
- +
+ +
+ +
)} @@ -225,6 +279,9 @@ export default function Home() { // Controlled space selection selectedSpace={selectedSpace} onSpaceChange={handleSpaceChange} + // Slideshow control + isSlideshowActive={isSlideshowActive} + onSlideshowNodeChange={handleSlideshowNodeChange} >

diff --git a/packages/memory-graph/CHANGELOG.md b/packages/memory-graph/CHANGELOG.md index 7e52aef0..66a449c5 100644 --- a/packages/memory-graph/CHANGELOG.md +++ b/packages/memory-graph/CHANGELOG.md @@ -14,6 +14,19 @@ > > Then open http://localhost:3000 in your browser. +## Slideshow Feature (2025-12-22) + +**Feature:** Added slideshow mode to automatically cycle through nodes with smooth animations and physics. + +**Implementation:** +- `isSlideshowActive` and `onSlideshowNodeChange` props for slideshow control +- Random node selection every 3.5 seconds (avoids consecutive duplicates) +- Smooth pan-to-node animation with automatic popover display +- Physics simulation triggers briefly (1s) on each selection for natural movement +- Background dimming animation on each node selection +- Popover backdrop scoped to graph container via `containerBounds` prop +- Single-click stop with automatic popover cleanup + ## Visual & Layout Improvements (2025-12-22) ### Background Dimming When Popover is Open diff --git a/packages/memory-graph/src/components/memory-graph.tsx b/packages/memory-graph/src/components/memory-graph.tsx index d98fb42b..eb2b24fd 100644 --- a/packages/memory-graph/src/components/memory-graph.tsx +++ b/packages/memory-graph/src/components/memory-graph.tsx @@ -40,6 +40,9 @@ export const MemoryGraph = ({ onSpaceChange: externalOnSpaceChange, memoryLimit, isExperimental, + // Slideshow control + isSlideshowActive = false, + onSlideshowNodeChange, }: MemoryGraphProps) => { // Inject styles on first render (client-side only) useEffect(() => { @@ -531,6 +534,109 @@ export const MemoryGraph = ({ } }, [data, hasMore, throttledCheckAndLoadMore, autoLoadOnViewport]) + // Slideshow logic - simulate actual node clicks with physics + const slideshowIntervalRef = useRef(null) + const lastSelectedIndexRef = useRef(-1) + const isSlideshowActiveRef = useRef(isSlideshowActive) + + // Update slideshow active ref + useEffect(() => { + isSlideshowActiveRef.current = isSlideshowActive + }, [isSlideshowActive]) + + // Use refs to store current values without triggering re-renders + const nodesRef = useRef(nodes) + const handleNodeClickRef = useRef(handleNodeClick) + const centerViewportOnRef = useRef(centerViewportOn) + const containerSizeRef = useRef(containerSize) + const onSlideshowNodeChangeRef = useRef(onSlideshowNodeChange) + const forceSimulationRef = useRef(forceSimulation) + + // Update refs when values change + useEffect(() => { + nodesRef.current = nodes + handleNodeClickRef.current = handleNodeClick + centerViewportOnRef.current = centerViewportOn + containerSizeRef.current = containerSize + onSlideshowNodeChangeRef.current = onSlideshowNodeChange + forceSimulationRef.current = forceSimulation + }, [nodes, handleNodeClick, centerViewportOn, containerSize, onSlideshowNodeChange, forceSimulation]) + + useEffect(() => { + // Clear any existing interval when isSlideshowActive changes + if (slideshowIntervalRef.current) { + clearInterval(slideshowIntervalRef.current) + slideshowIntervalRef.current = null + } + + if (!isSlideshowActive) { + // Close the popover when stopping slideshow + setSelectedNode(null) + return + } + + // Select a random node (avoid selecting the same one twice in a row) + const selectRandomNode = () => { + // Double-check slideshow is still active + if (!isSlideshowActiveRef.current) return + + const currentNodes = nodesRef.current + if (currentNodes.length === 0) return + + let randomIndex: number + // If we have more than one node, avoid selecting the same one + if (currentNodes.length > 1) { + do { + randomIndex = Math.floor(Math.random() * currentNodes.length) + } while (randomIndex === lastSelectedIndexRef.current) + } else { + randomIndex = 0 + } + + lastSelectedIndexRef.current = randomIndex + const randomNode = currentNodes[randomIndex] + + if (randomNode) { + // Smoothly pan to the node first + centerViewportOnRef.current( + randomNode.x, + randomNode.y, + containerSizeRef.current.width, + containerSizeRef.current.height, + ) + + // Simulate the actual node click (triggers dimming and popover) + handleNodeClickRef.current(randomNode.id) + + // Trigger physics animation briefly + forceSimulationRef.current.reheat() + + // Cool down physics after 1 second + setTimeout(() => { + forceSimulationRef.current.coolDown() + }, 1000) + + // Notify parent component + onSlideshowNodeChangeRef.current?.(randomNode.id) + } + } + + // Start immediately + selectRandomNode() + + // Set interval for subsequent selections (3.5 seconds) + slideshowIntervalRef.current = setInterval(() => { + selectRandomNode() + }, 3500) + + return () => { + if (slideshowIntervalRef.current) { + clearInterval(slideshowIntervalRef.current) + slideshowIntervalRef.current = null + } + } + }, [isSlideshowActive]) // Only depend on isSlideshowActive + if (error) { return (

@@ -586,6 +692,7 @@ export const MemoryGraph = ({ x={popoverPosition.x} y={popoverPosition.y} onClose={() => setSelectedNode(null)} + containerBounds={containerRef.current?.getBoundingClientRect()} /> )} diff --git a/packages/memory-graph/src/components/node-popover.tsx b/packages/memory-graph/src/components/node-popover.tsx index 2657d10c..7ae758de 100644 --- a/packages/memory-graph/src/components/node-popover.tsx +++ b/packages/memory-graph/src/components/node-popover.tsx @@ -8,6 +8,7 @@ export interface NodePopoverProps { x: number // Screen X position y: number // Screen Y position onClose: () => void + containerBounds?: DOMRect // Optional container bounds to limit backdrop } export const NodePopover = memo(function NodePopover({ @@ -15,6 +16,7 @@ export const NodePopover = memo(function NodePopover({ x, y, onClose, + containerBounds, }) { // Handle Escape key to close popover useEffect(() => { @@ -28,19 +30,30 @@ export const NodePopover = memo(function NodePopover({ return () => window.removeEventListener("keydown", handleKeyDown) }, [onClose]) + // Calculate backdrop bounds - use container bounds if provided, otherwise full viewport + const backdropStyle = containerBounds + ? { + position: "fixed" as const, + left: `${containerBounds.left}px`, + top: `${containerBounds.top}px`, + width: `${containerBounds.width}px`, + height: `${containerBounds.height}px`, + zIndex: 999, + pointerEvents: "auto" as const, + backgroundColor: "transparent", + } + : { + position: "fixed" as const, + inset: 0, + zIndex: 999, + pointerEvents: "auto" as const, + backgroundColor: "transparent", + } + return ( <> {/* Invisible backdrop to catch clicks outside */} -
+
{/* Popover content */}
void } export interface LegendProps {