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" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Lightrag</title>
<script type="module" crossorigin src="./assets/index-nzv8EoUv.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-TPDyec81.css">
<script type="module" crossorigin src="./assets/index-BpDMZCmZ.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-BEGlBF11.css">
</head>
<body>
<div id="root"></div>

View File

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

View File

@@ -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'
@@ -15,7 +15,7 @@ import { Command, CommandGroup, CommandItem, CommandList } from '@/components/ui
import { controlButtonVariant } from '@/lib/constants'
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'
type LayoutName =
@@ -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<number | null>(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: 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.
*/
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()
}, 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)
}
}
}, [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 (
<Button
size="icon"
onClick={() => (isRunning ? stop() : start())}
onClick={handleClick}
tooltip={isRunning ? t('graphPanel.sideBar.layoutsControl.stopAnimation') : t('graphPanel.sideBar.layoutsControl.startAnimation')}
variant={controlButtonVariant}
>
@@ -86,7 +204,17 @@ const LayoutsControl = () => {
const layoutCirclepack = useLayoutCirclepack()
const layoutRandom = useLayoutRandom()
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 workerNoverlap = useWorkerLayoutNoverlap()
const workerForce = useWorkerLayoutForce()
@@ -130,34 +258,35 @@ const LayoutsControl = () => {
const runLayout = useCallback(
(newLayout: LayoutName) => {
console.debug(newLayout)
console.debug('Running layout:', newLayout)
const { positions } = layouts[newLayout].layout
animateNodes(sigma.getGraph(), positions(), { duration: 500 })
setLayout(newLayout)
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)
} catch (error) {
console.error('Error running layout:', error)
}
},
[layouts, sigma]
)
const refreshLayout = useCallback(() => {
if (!sigma) return
const graph = sigma.getGraph()
const positions = layoutForceAtlas2.positions()
animateNodes(graph, positions, { duration: 500 })
}, [sigma, layoutForceAtlas2])
return (
<>
<Button
variant={controlButtonVariant}
tooltip={t('graphPanel.sideBar.settings.refreshLayout')}
size="icon"
onClick={refreshLayout}
>
<RefreshCwIcon />
</Button>
<div>
{layouts[layout] && 'worker' in layouts[layout] && (
<WorkerLayoutControl layout={layouts[layout].worker!} />
<WorkerLayoutControl
layout={layouts[layout].worker!}
mainLayout={layouts[layout].layout}
/>
)}
</div>
<div>

View File

@@ -11,18 +11,16 @@ const PopoverContent = React.forwardRef<
React.ComponentRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = 'center', sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-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 z-50 rounded-md border p-4 shadow-md outline-none',
className
)}
{...props}
/>
</PopoverPrimitive.Portal>
<PopoverPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-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 z-50 rounded-md border p-4 shadow-md outline-none',
className
)}
{...props}
/>
))
PopoverContent.displayName = PopoverPrimitive.Content.displayName

View File

@@ -38,7 +38,7 @@ const TooltipContent = React.forwardRef<
side={side}
align={align}
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
)}
{...props}

View File

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

View File

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