diff --git a/lightrag_webui/src/components/graph/LayoutsControl.tsx b/lightrag_webui/src/components/graph/LayoutsControl.tsx index 4f037463..bf853dd0 100644 --- a/lightrag_webui/src/components/graph/LayoutsControl.tsx +++ b/lightrag_webui/src/components/graph/LayoutsControl.tsx @@ -7,7 +7,7 @@ import { useLayoutForce, useWorkerLayoutForce } from '@react-sigma/layout-force' import { useLayoutForceAtlas2, useWorkerLayoutForceAtlas2 } from '@react-sigma/layout-forceatlas2' import { useLayoutNoverlap, useWorkerLayoutNoverlap } from '@react-sigma/layout-noverlap' import { useLayoutRandom } from '@react-sigma/layout-random' -import { useCallback, useMemo, useState, useEffect } from 'react' +import { useCallback, useMemo, useState, useEffect, useRef } from 'react' import Button from '@/components/ui/Button' import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/Popover' @@ -26,43 +26,161 @@ type LayoutName = | 'Force Directed' | 'Force Atlas' -const WorkerLayoutControl = ({ layout, autoRunFor }: WorkerLayoutControlProps) => { +// Extend WorkerLayoutControlProps to include mainLayout +interface ExtendedWorkerLayoutControlProps extends WorkerLayoutControlProps { + mainLayout: LayoutHook; +} + +const WorkerLayoutControl = ({ layout, autoRunFor, mainLayout }: ExtendedWorkerLayoutControlProps) => { const sigma = useSigma() - const { stop, start, isRunning } = layout + // Use local state to track animation running status + const [isRunning, setIsRunning] = useState(false) + // Timer reference for animation + const animationTimerRef = useRef(null) const { t } = useTranslation() + // Function to update node positions using the layout algorithm + const updatePositions = useCallback(() => { + if (!sigma) return + + try { + const graph = sigma.getGraph() + if (!graph || graph.order === 0) return + + // Use mainLayout to get positions, similar to refreshLayout function + // console.log('Getting positions from mainLayout') + const positions = mainLayout.positions() + + // Animate nodes to new positions + // console.log('Updating node positions with layout algorithm') + animateNodes(graph, positions, { duration: 400 }) // Increase duration for smoother transitions + } catch (error) { + console.error('Error updating positions:', error) + // Stop animation if there's an error + if (animationTimerRef.current) { + window.clearInterval(animationTimerRef.current) + animationTimerRef.current = null + setIsRunning(false) + } + } + }, [sigma, mainLayout]) + + // Improved click handler that uses our own animation timer + const handleClick = useCallback(() => { + if (isRunning) { + // Stop the animation + console.log('Stopping layout animation') + if (animationTimerRef.current) { + window.clearInterval(animationTimerRef.current) + animationTimerRef.current = null + } + + // Try to kill the layout algorithm if it's running + try { + if (typeof layout.kill === 'function') { + layout.kill() + console.log('Layout algorithm killed') + } else if (typeof layout.stop === 'function') { + layout.stop() + console.log('Layout algorithm stopped') + } + } catch (error) { + console.error('Error stopping layout algorithm:', error) + } + + setIsRunning(false) + } else { + // Start the animation + console.log('Starting layout animation') + + // Initial position update + updatePositions() + + // Set up interval for continuous updates + animationTimerRef.current = window.setInterval(() => { + updatePositions() + }, 400) // Match interval with animation duration for smoother transitions + + setIsRunning(true) + + // Set a timeout to automatically stop the animation after 2 seconds + setTimeout(() => { + if (animationTimerRef.current) { + console.log('Auto-stopping layout animation after 2 seconds') + window.clearInterval(animationTimerRef.current) + animationTimerRef.current = null + setIsRunning(false) + + // Try to stop the layout algorithm + try { + if (typeof layout.kill === 'function') { + layout.kill() + } else if (typeof layout.stop === 'function') { + layout.stop() + } + } catch (error) { + console.error('Error stopping layout algorithm:', error) + } + } + }, 2000) + } + }, [isRunning, layout, updatePositions]) + /** * Init component when Sigma or component settings change. */ useEffect(() => { if (!sigma) { + console.log('No sigma instance available') return } - // we run the algo + // Auto-run if specified let timeout: number | null = null if (autoRunFor !== undefined && autoRunFor > -1 && sigma.getGraph().order > 0) { - start() - // set a timeout to stop it - timeout = - autoRunFor > 0 - ? window.setTimeout(() => { stop() }, autoRunFor) // prettier-ignore - : null - } + console.log('Auto-starting layout animation') - //cleaning - return () => { - stop() - if (timeout) { - clearTimeout(timeout) + // Initial position update + updatePositions() + + // Set up interval for continuous updates + animationTimerRef.current = window.setInterval(() => { + updatePositions() + }, 400) // Match interval with animation duration for smoother transitions + + setIsRunning(true) + + // Set a timeout to stop it if autoRunFor > 0 + if (autoRunFor > 0) { + timeout = window.setTimeout(() => { + console.log('Auto-stopping layout animation after timeout') + if (animationTimerRef.current) { + window.clearInterval(animationTimerRef.current) + animationTimerRef.current = null + } + setIsRunning(false) + }, autoRunFor) } } - }, [autoRunFor, start, stop, sigma]) + + // Cleanup function + return () => { + // console.log('Cleaning up WorkerLayoutControl') + if (animationTimerRef.current) { + window.clearInterval(animationTimerRef.current) + animationTimerRef.current = null + } + if (timeout) { + window.clearTimeout(timeout) + } + setIsRunning(false) + } + }, [autoRunFor, sigma, updatePositions]) return (
{layouts[layout] && 'worker' in layouts[layout] && ( - + )}