added slideshow mode to automatically cycle through nodes with smooth animations and physics

This commit is contained in:
Vidya Rupak 2025-12-22 15:15:06 -07:00
parent acc9181d77
commit 97599c42ef
5 changed files with 212 additions and 16 deletions

View file

@ -29,6 +29,10 @@ export default function Home() {
// State for controlled space selection
const [selectedSpace, setSelectedSpace] = useState<string>("all")
// State for slideshow
const [isSlideshowActive, setIsSlideshowActive] = useState(false)
const [currentSlideshowNode, setCurrentSlideshowNode] = useState<string | null>(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 (
<div className="flex flex-col h-screen bg-zinc-950">
{/* Header */}
@ -158,12 +174,50 @@ export default function Home() {
</span>
</div>
</div>
<button
onClick={handleReset}
className="rounded-lg border border-zinc-700 px-3 py-1 text-xs font-medium text-zinc-300 transition-colors hover:bg-zinc-800"
>
Reset Filters
</button>
<div className="flex items-center gap-3">
<button
onClick={handleToggleSlideshow}
className={`rounded-lg px-3 py-1.5 text-xs font-medium transition-colors flex items-center gap-1.5 ${
isSlideshowActive
? "bg-blue-600 text-white hover:bg-blue-700"
: "border border-zinc-700 text-zinc-300 hover:bg-zinc-800"
}`}
>
{isSlideshowActive ? (
<>
<svg
width="12"
height="12"
viewBox="0 0 24 24"
fill="currentColor"
>
<rect x="6" y="4" width="4" height="16" />
<rect x="14" y="4" width="4" height="16" />
</svg>
Stop Slideshow
</>
) : (
<>
<svg
width="12"
height="12"
viewBox="0 0 24 24"
fill="currentColor"
>
<path d="M8 5v14l11-7z" />
</svg>
Start Slideshow
</>
)}
</button>
<div className="h-6 w-px bg-zinc-700" />
<button
onClick={handleReset}
className="rounded-lg border border-zinc-700 px-3 py-1.5 text-xs font-medium text-zinc-300 transition-colors hover:bg-zinc-800"
>
Reset Filters
</button>
</div>
</div>
</div>
)}
@ -225,6 +279,9 @@ export default function Home() {
// Controlled space selection
selectedSpace={selectedSpace}
onSpaceChange={handleSpaceChange}
// Slideshow control
isSlideshowActive={isSlideshowActive}
onSlideshowNodeChange={handleSlideshowNodeChange}
>
<div className="flex h-full items-center justify-center">
<p className="text-zinc-400">

View file

@ -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

View file

@ -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<NodeJS.Timeout | null>(null)
const lastSelectedIndexRef = useRef<number>(-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 (
<div className={styles.errorContainer}>
@ -586,6 +692,7 @@ export const MemoryGraph = ({
x={popoverPosition.x}
y={popoverPosition.y}
onClose={() => setSelectedNode(null)}
containerBounds={containerRef.current?.getBoundingClientRect()}
/>
)}

View file

@ -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<NodePopoverProps>(function NodePopover({
@ -15,6 +16,7 @@ export const NodePopover = memo<NodePopoverProps>(function NodePopover({
x,
y,
onClose,
containerBounds,
}) {
// Handle Escape key to close popover
useEffect(() => {
@ -28,19 +30,30 @@ export const NodePopover = memo<NodePopoverProps>(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 */}
<div
onClick={onClose}
style={{
position: "fixed",
inset: 0,
zIndex: 999,
pointerEvents: "auto",
backgroundColor: "transparent",
}}
/>
<div onClick={onClose} style={backdropStyle} />
{/* Popover content */}
<div

View file

@ -133,6 +133,12 @@ export interface MemoryGraphProps {
// Feature flags
/** Enable experimental features */
isExperimental?: boolean
// Slideshow control
/** Whether slideshow mode is currently active */
isSlideshowActive?: boolean
/** Callback when slideshow selects a new node (provides node ID) */
onSlideshowNodeChange?: (nodeId: string | null) => void
}
export interface LegendProps {