Merge branch 'feat-node-expand' into webui-node-expansion

This commit is contained in:
yangdx
2025-03-17 20:06:03 +08:00
10 changed files with 309 additions and 185 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -8,8 +8,8 @@
<link rel="icon" type="image/svg+xml" href="./logo.png" /> <link rel="icon" type="image/svg+xml" href="./logo.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Lightrag</title> <title>Lightrag</title>
<script type="module" crossorigin src="./assets/index-nzv8EoUv.js"></script> <script type="module" crossorigin src="./assets/index-BpDMZCmZ.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-TPDyec81.css"> <link rel="stylesheet" crossorigin href="./assets/index-BEGlBF11.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

View File

@@ -123,21 +123,18 @@ async def openai_complete_if_cache(
async def inner(): async def inner():
try: try:
_content = ""
async for chunk in response: async for chunk in response:
content = chunk.choices[0].delta.content content = chunk.choices[0].delta.content
if content is None: if content is None:
continue continue
if r"\u" in content: if r"\u" in content:
content = safe_unicode_decode(content.encode("utf-8")) content = safe_unicode_decode(content.encode("utf-8"))
_content += content yield content
return _content
except Exception as e: except Exception as e:
logger.error(f"Error in stream response: {str(e)}") logger.error(f"Error in stream response: {str(e)}")
raise raise
response_content = await inner() return inner()
return response_content
else: else:
if ( if (

View File

@@ -7,7 +7,7 @@ import { useLayoutForce, useWorkerLayoutForce } from '@react-sigma/layout-force'
import { useLayoutForceAtlas2, useWorkerLayoutForceAtlas2 } from '@react-sigma/layout-forceatlas2' import { useLayoutForceAtlas2, useWorkerLayoutForceAtlas2 } from '@react-sigma/layout-forceatlas2'
import { useLayoutNoverlap, useWorkerLayoutNoverlap } from '@react-sigma/layout-noverlap' import { useLayoutNoverlap, useWorkerLayoutNoverlap } from '@react-sigma/layout-noverlap'
import { useLayoutRandom } from '@react-sigma/layout-random' 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 Button from '@/components/ui/Button'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/Popover' import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/Popover'
@@ -15,7 +15,7 @@ import { Command, CommandGroup, CommandItem, CommandList } from '@/components/ui
import { controlButtonVariant } from '@/lib/constants' import { controlButtonVariant } from '@/lib/constants'
import { useSettingsStore } from '@/stores/settings' import { useSettingsStore } from '@/stores/settings'
import { GripIcon, PlayIcon, PauseIcon, RefreshCwIcon } from 'lucide-react' import { GripIcon, PlayIcon, PauseIcon } from 'lucide-react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
type LayoutName = type LayoutName =
@@ -26,43 +26,161 @@ type LayoutName =
| 'Force Directed' | 'Force Directed'
| 'Force Atlas' | '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 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<number | null>(null)
const { t } = useTranslation() 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: 300 }) // Reduced duration for more frequent updates
} 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()
}, 200) // Reduced interval to create overlapping animations for smoother transitions
setIsRunning(true)
// Set a timeout to automatically stop the animation after 3 seconds
setTimeout(() => {
if (animationTimerRef.current) {
console.log('Auto-stopping layout animation after 3 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)
}
}
}, 3000)
}
}, [isRunning, layout, updatePositions])
/** /**
* Init component when Sigma or component settings change. * Init component when Sigma or component settings change.
*/ */
useEffect(() => { useEffect(() => {
if (!sigma) { if (!sigma) {
console.log('No sigma instance available')
return return
} }
// we run the algo // Auto-run if specified
let timeout: number | null = null let timeout: number | null = null
if (autoRunFor !== undefined && autoRunFor > -1 && sigma.getGraph().order > 0) { if (autoRunFor !== undefined && autoRunFor > -1 && sigma.getGraph().order > 0) {
start() console.log('Auto-starting layout animation')
// set a timeout to stop it
timeout = // Initial position update
autoRunFor > 0 updatePositions()
? window.setTimeout(() => { stop() }, autoRunFor) // prettier-ignore
: null // Set up interval for continuous updates
animationTimerRef.current = window.setInterval(() => {
updatePositions()
}, 200) // Reduced interval to create overlapping animations 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)
}
} }
//cleaning // Cleanup function
return () => { return () => {
stop() // console.log('Cleaning up WorkerLayoutControl')
if (animationTimerRef.current) {
window.clearInterval(animationTimerRef.current)
animationTimerRef.current = null
}
if (timeout) { if (timeout) {
clearTimeout(timeout) window.clearTimeout(timeout)
} }
setIsRunning(false)
} }
}, [autoRunFor, start, stop, sigma]) }, [autoRunFor, sigma, updatePositions])
return ( return (
<Button <Button
size="icon" size="icon"
onClick={() => (isRunning ? stop() : start())} onClick={handleClick}
tooltip={isRunning ? t('graphPanel.sideBar.layoutsControl.stopAnimation') : t('graphPanel.sideBar.layoutsControl.startAnimation')} tooltip={isRunning ? t('graphPanel.sideBar.layoutsControl.stopAnimation') : t('graphPanel.sideBar.layoutsControl.startAnimation')}
variant={controlButtonVariant} variant={controlButtonVariant}
> >
@@ -86,7 +204,17 @@ const LayoutsControl = () => {
const layoutCirclepack = useLayoutCirclepack() const layoutCirclepack = useLayoutCirclepack()
const layoutRandom = useLayoutRandom() const layoutRandom = useLayoutRandom()
const layoutNoverlap = useLayoutNoverlap({ settings: { margin: 1 } }) const layoutNoverlap = useLayoutNoverlap({ settings: { margin: 1 } })
const layoutForce = useLayoutForce({ maxIterations: maxIterations }) // Add parameters for Force Directed layout to improve convergence
const layoutForce = useLayoutForce({
maxIterations: maxIterations * 3, // Triple the iterations for better convergence
settings: {
attraction: 0.0003, // Lower attraction force to reduce oscillation
repulsion: 0.05, // Lower repulsion force to reduce oscillation
gravity: 0.01, // Increase gravity to make nodes converge to center faster
inertia: 0.4, // Lower inertia to add damping effect
maxMove: 100 // Limit maximum movement per step to prevent large jumps
}
})
const layoutForceAtlas2 = useLayoutForceAtlas2({ iterations: maxIterations }) const layoutForceAtlas2 = useLayoutForceAtlas2({ iterations: maxIterations })
const workerNoverlap = useWorkerLayoutNoverlap() const workerNoverlap = useWorkerLayoutNoverlap()
const workerForce = useWorkerLayoutForce() const workerForce = useWorkerLayoutForce()
@@ -130,34 +258,35 @@ const LayoutsControl = () => {
const runLayout = useCallback( const runLayout = useCallback(
(newLayout: LayoutName) => { (newLayout: LayoutName) => {
console.debug(newLayout) console.debug('Running layout:', newLayout)
const { positions } = layouts[newLayout].layout const { positions } = layouts[newLayout].layout
animateNodes(sigma.getGraph(), positions(), { duration: 500 })
try {
const graph = sigma.getGraph()
if (!graph) {
console.error('No graph available')
return
}
const pos = positions()
console.log('Positions calculated, animating nodes')
animateNodes(graph, pos, { duration: 400 })
setLayout(newLayout) setLayout(newLayout)
} catch (error) {
console.error('Error running layout:', error)
}
}, },
[layouts, sigma] [layouts, sigma]
) )
const refreshLayout = useCallback(() => {
if (!sigma) return
const graph = sigma.getGraph()
const positions = layoutForceAtlas2.positions()
animateNodes(graph, positions, { duration: 500 })
}, [sigma, layoutForceAtlas2])
return ( return (
<> <>
<Button
variant={controlButtonVariant}
tooltip={t('graphPanel.sideBar.settings.refreshLayout')}
size="icon"
onClick={refreshLayout}
>
<RefreshCwIcon />
</Button>
<div> <div>
{layouts[layout] && 'worker' in layouts[layout] && ( {layouts[layout] && 'worker' in layouts[layout] && (
<WorkerLayoutControl layout={layouts[layout].worker!} /> <WorkerLayoutControl
layout={layouts[layout].worker!}
mainLayout={layouts[layout].layout}
/>
)} )}
</div> </div>
<div> <div>

View File

@@ -11,7 +11,6 @@ const PopoverContent = React.forwardRef<
React.ComponentRef<typeof PopoverPrimitive.Content>, React.ComponentRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content> React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = 'center', sideOffset = 4, ...props }, ref) => ( >(({ className, align = 'center', sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content <PopoverPrimitive.Content
ref={ref} ref={ref}
align={align} align={align}
@@ -22,7 +21,6 @@ const PopoverContent = React.forwardRef<
)} )}
{...props} {...props}
/> />
</PopoverPrimitive.Portal>
)) ))
PopoverContent.displayName = PopoverPrimitive.Content.displayName PopoverContent.displayName = PopoverPrimitive.Content.displayName

View File

@@ -38,7 +38,7 @@ const TooltipContent = React.forwardRef<
side={side} side={side}
align={align} align={align}
className={cn( className={cn(
'bg-popover text-popover-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 max-h-[60vh] overflow-y-auto whitespace-pre-wrap break-words rounded-md border px-3 py-2 text-sm shadow-md', 'bg-popover text-popover-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 max-h-[60vh] overflow-y-auto whitespace-pre-wrap break-words rounded-md border px-3 py-2 text-sm shadow-md z-60',
className className
)} )}
{...props} {...props}

View File

@@ -101,8 +101,8 @@
}, },
"layoutsControl": { "layoutsControl": {
"startAnimation": "Start the layout animation", "startAnimation": "Continue layout animation",
"stopAnimation": "Stop the layout animation", "stopAnimation": "Stop layout animation",
"layoutGraph": "Layout Graph", "layoutGraph": "Layout Graph",
"layouts": { "layouts": {
"Circular": "Circular", "Circular": "Circular",

View File

@@ -99,7 +99,7 @@
"resetZoom": "重置缩放" "resetZoom": "重置缩放"
}, },
"layoutsControl": { "layoutsControl": {
"startAnimation": "开始布局动画", "startAnimation": "继续布局动画",
"stopAnimation": "停止布局动画", "stopAnimation": "停止布局动画",
"layoutGraph": "图布局", "layoutGraph": "图布局",
"layouts": { "layouts": {
@@ -108,7 +108,7 @@
"Random": "随机", "Random": "随机",
"Noverlaps": "无重叠", "Noverlaps": "无重叠",
"Force Directed": "力导向", "Force Directed": "力导向",
"Force Atlas": "力图" "Force Atlas": "力图"
} }
}, },
"fullScreenControl": { "fullScreenControl": {