mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-05-19 16:13:19 +00:00
added slideshow mode to automatically cycle through nodes with smooth animations and physics
This commit is contained in:
parent
acc9181d77
commit
97599c42ef
5 changed files with 212 additions and 16 deletions
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue