Merge branch 'improve-property-tooltip' into feat-node-expand
This commit is contained in:
@@ -423,12 +423,24 @@ def create_app(args):
|
||||
"update_status": update_status,
|
||||
}
|
||||
|
||||
# Custom StaticFiles class to prevent caching of HTML files
|
||||
class NoCacheStaticFiles(StaticFiles):
|
||||
async def get_response(self, path: str, scope):
|
||||
response = await super().get_response(path, scope)
|
||||
if path.endswith(".html"):
|
||||
response.headers["Cache-Control"] = (
|
||||
"no-cache, no-store, must-revalidate"
|
||||
)
|
||||
response.headers["Pragma"] = "no-cache"
|
||||
response.headers["Expires"] = "0"
|
||||
return response
|
||||
|
||||
# Webui mount webui/index.html
|
||||
static_dir = Path(__file__).parent / "webui"
|
||||
static_dir.mkdir(exist_ok=True)
|
||||
app.mount(
|
||||
"/webui",
|
||||
StaticFiles(directory=static_dir, html=True, check_dir=True),
|
||||
NoCacheStaticFiles(directory=static_dir, html=True, check_dir=True),
|
||||
name="webui",
|
||||
)
|
||||
|
||||
|
@@ -99,6 +99,37 @@ class DocsStatusesResponse(BaseModel):
|
||||
statuses: Dict[DocStatus, List[DocStatusResponse]] = {}
|
||||
|
||||
|
||||
class PipelineStatusResponse(BaseModel):
|
||||
"""Response model for pipeline status
|
||||
|
||||
Attributes:
|
||||
autoscanned: Whether auto-scan has started
|
||||
busy: Whether the pipeline is currently busy
|
||||
job_name: Current job name (e.g., indexing files/indexing texts)
|
||||
job_start: Job start time as ISO format string (optional)
|
||||
docs: Total number of documents to be indexed
|
||||
batchs: Number of batches for processing documents
|
||||
cur_batch: Current processing batch
|
||||
request_pending: Flag for pending request for processing
|
||||
latest_message: Latest message from pipeline processing
|
||||
history_messages: List of history messages
|
||||
"""
|
||||
|
||||
autoscanned: bool = False
|
||||
busy: bool = False
|
||||
job_name: str = "Default Job"
|
||||
job_start: Optional[str] = None
|
||||
docs: int = 0
|
||||
batchs: int = 0
|
||||
cur_batch: int = 0
|
||||
request_pending: bool = False
|
||||
latest_message: str = ""
|
||||
history_messages: Optional[List[str]] = None
|
||||
|
||||
class Config:
|
||||
extra = "allow" # Allow additional fields from the pipeline status
|
||||
|
||||
|
||||
class DocumentManager:
|
||||
def __init__(
|
||||
self,
|
||||
@@ -247,7 +278,7 @@ async def pipeline_enqueue_file(rag: LightRAG, file_path: Path) -> bool:
|
||||
if global_args["main_args"].document_loading_engine == "DOCLING":
|
||||
if not pm.is_installed("docling"): # type: ignore
|
||||
pm.install("docling")
|
||||
from docling.document_converter import DocumentConverter
|
||||
from docling.document_converter import DocumentConverter # type: ignore
|
||||
|
||||
converter = DocumentConverter()
|
||||
result = converter.convert(file_path)
|
||||
@@ -266,7 +297,7 @@ async def pipeline_enqueue_file(rag: LightRAG, file_path: Path) -> bool:
|
||||
if global_args["main_args"].document_loading_engine == "DOCLING":
|
||||
if not pm.is_installed("docling"): # type: ignore
|
||||
pm.install("docling")
|
||||
from docling.document_converter import DocumentConverter
|
||||
from docling.document_converter import DocumentConverter # type: ignore
|
||||
|
||||
converter = DocumentConverter()
|
||||
result = converter.convert(file_path)
|
||||
@@ -286,7 +317,7 @@ async def pipeline_enqueue_file(rag: LightRAG, file_path: Path) -> bool:
|
||||
if global_args["main_args"].document_loading_engine == "DOCLING":
|
||||
if not pm.is_installed("docling"): # type: ignore
|
||||
pm.install("docling")
|
||||
from docling.document_converter import DocumentConverter
|
||||
from docling.document_converter import DocumentConverter # type: ignore
|
||||
|
||||
converter = DocumentConverter()
|
||||
result = converter.convert(file_path)
|
||||
@@ -307,7 +338,7 @@ async def pipeline_enqueue_file(rag: LightRAG, file_path: Path) -> bool:
|
||||
if global_args["main_args"].document_loading_engine == "DOCLING":
|
||||
if not pm.is_installed("docling"): # type: ignore
|
||||
pm.install("docling")
|
||||
from docling.document_converter import DocumentConverter
|
||||
from docling.document_converter import DocumentConverter # type: ignore
|
||||
|
||||
converter = DocumentConverter()
|
||||
result = converter.convert(file_path)
|
||||
@@ -718,17 +749,33 @@ def create_document_routes(
|
||||
logger.error(traceback.format_exc())
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.get("/pipeline_status", dependencies=[Depends(optional_api_key)])
|
||||
async def get_pipeline_status():
|
||||
@router.get(
|
||||
"/pipeline_status",
|
||||
dependencies=[Depends(optional_api_key)],
|
||||
response_model=PipelineStatusResponse,
|
||||
)
|
||||
async def get_pipeline_status() -> PipelineStatusResponse:
|
||||
"""
|
||||
Get the current status of the document indexing pipeline.
|
||||
|
||||
This endpoint returns information about the current state of the document processing pipeline,
|
||||
including whether it's busy, the current job name, when it started, how many documents
|
||||
are being processed, how many batches there are, and which batch is currently being processed.
|
||||
including the processing status, progress information, and history messages.
|
||||
|
||||
Returns:
|
||||
dict: A dictionary containing the pipeline status information
|
||||
PipelineStatusResponse: A response object containing:
|
||||
- autoscanned (bool): Whether auto-scan has started
|
||||
- busy (bool): Whether the pipeline is currently busy
|
||||
- job_name (str): Current job name (e.g., indexing files/indexing texts)
|
||||
- job_start (str, optional): Job start time as ISO format string
|
||||
- docs (int): Total number of documents to be indexed
|
||||
- batchs (int): Number of batches for processing documents
|
||||
- cur_batch (int): Current processing batch
|
||||
- request_pending (bool): Flag for pending request for processing
|
||||
- latest_message (str): Latest message from pipeline processing
|
||||
- history_messages (List[str], optional): List of history messages
|
||||
|
||||
Raises:
|
||||
HTTPException: If an error occurs while retrieving pipeline status (500)
|
||||
"""
|
||||
try:
|
||||
from lightrag.kg.shared_storage import get_namespace_data
|
||||
@@ -746,7 +793,7 @@ def create_document_routes(
|
||||
if status_dict.get("job_start"):
|
||||
status_dict["job_start"] = str(status_dict["job_start"])
|
||||
|
||||
return status_dict
|
||||
return PipelineStatusResponse(**status_dict)
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting pipeline status: {str(e)}")
|
||||
logger.error(traceback.format_exc())
|
||||
|
File diff suppressed because one or more lines are too long
@@ -2,10 +2,13 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
|
||||
<meta http-equiv="Pragma" content="no-cache" />
|
||||
<meta http-equiv="Expires" content="0" />
|
||||
<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-B9TRs-Wk.js"></script>
|
||||
<script type="module" crossorigin src="./assets/index-Cs3ZbEon.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-DRGuXfZw.css">
|
||||
</head>
|
||||
<body>
|
||||
|
@@ -661,7 +661,7 @@ class Neo4JStorage(BaseGraphStorage):
|
||||
WITH collect({node: n}) AS filtered_nodes
|
||||
UNWIND filtered_nodes AS node_info
|
||||
WITH collect(node_info.node) AS kept_nodes, filtered_nodes
|
||||
MATCH (a)-[r]-(b)
|
||||
OPTIONAL MATCH (a)-[r]-(b)
|
||||
WHERE a IN kept_nodes AND b IN kept_nodes
|
||||
RETURN filtered_nodes AS node_info,
|
||||
collect(DISTINCT r) AS relationships
|
||||
@@ -704,7 +704,7 @@ class Neo4JStorage(BaseGraphStorage):
|
||||
WITH collect({node: node}) AS filtered_nodes
|
||||
UNWIND filtered_nodes AS node_info
|
||||
WITH collect(node_info.node) AS kept_nodes, filtered_nodes
|
||||
MATCH (a)-[r]-(b)
|
||||
OPTIONAL MATCH (a)-[r]-(b)
|
||||
WHERE a IN kept_nodes AND b IN kept_nodes
|
||||
RETURN filtered_nodes AS node_info,
|
||||
collect(DISTINCT r) AS relationships
|
||||
|
@@ -2,6 +2,9 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
|
||||
<meta http-equiv="Pragma" content="no-cache" />
|
||||
<meta http-equiv="Expires" content="0" />
|
||||
<link rel="icon" type="image/svg+xml" href="/logo.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Lightrag</title>
|
||||
|
@@ -9,10 +9,10 @@ import { useTranslation } from 'react-i18next'
|
||||
export default function AppSettings() {
|
||||
const [opened, setOpened] = useState<boolean>(false)
|
||||
const { t } = useTranslation()
|
||||
|
||||
|
||||
const language = useSettingsStore.use.language()
|
||||
const setLanguage = useSettingsStore.use.setLanguage()
|
||||
|
||||
|
||||
const theme = useSettingsStore.use.theme()
|
||||
const setTheme = useSettingsStore.use.setTheme()
|
||||
|
||||
|
@@ -34,10 +34,10 @@ export default function ThemeProvider({ children, ...props }: ThemeProviderProps
|
||||
root.classList.remove('light', 'dark')
|
||||
root.classList.add(e.matches ? 'dark' : 'light')
|
||||
}
|
||||
|
||||
|
||||
root.classList.add(mediaQuery.matches ? 'dark' : 'light')
|
||||
mediaQuery.addEventListener('change', handleChange)
|
||||
|
||||
|
||||
return () => mediaQuery.removeEventListener('change', handleChange)
|
||||
} else {
|
||||
root.classList.add(theme)
|
||||
|
@@ -13,15 +13,24 @@ const FocusOnNode = ({ node, move }: { node: string | null; move?: boolean }) =>
|
||||
* When the selected item changes, highlighted the node and center the camera on it.
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!node) return
|
||||
sigma.getGraph().setNodeAttribute(node, 'highlighted', true)
|
||||
if (move) {
|
||||
gotoNode(node)
|
||||
if (node) {
|
||||
sigma.getGraph().setNodeAttribute(node, 'highlighted', true)
|
||||
gotoNode(node)
|
||||
} else {
|
||||
// If no node is selected but move is true, reset to default view
|
||||
sigma.setCustomBBox(null)
|
||||
sigma.getCamera().animate({ x: 0.5, y: 0.5, ratio: 1 }, { duration: 0 })
|
||||
}
|
||||
useGraphStore.getState().setMoveToSelectedNode(false)
|
||||
} else if (node) {
|
||||
sigma.getGraph().setNodeAttribute(node, 'highlighted', true)
|
||||
}
|
||||
|
||||
return () => {
|
||||
sigma.getGraph().setNodeAttribute(node, 'highlighted', false)
|
||||
if (node) {
|
||||
sigma.getGraph().setNodeAttribute(node, 'highlighted', false)
|
||||
}
|
||||
}
|
||||
}, [node, move, sigma, gotoNode])
|
||||
|
||||
|
@@ -1,10 +1,11 @@
|
||||
import { useLoadGraph, useRegisterEvents, useSetSettings, useSigma } from '@react-sigma/core'
|
||||
import Graph from 'graphology'
|
||||
// import { useLayoutCircular } from '@react-sigma/layout-circular'
|
||||
import { useLayoutForceAtlas2 } from '@react-sigma/layout-forceatlas2'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
// import useRandomGraph, { EdgeType, NodeType } from '@/hooks/useRandomGraph'
|
||||
import useLightragGraph, { EdgeType, NodeType } from '@/hooks/useLightragGraph'
|
||||
import { EdgeType, NodeType } from '@/hooks/useLightragGraph'
|
||||
import useTheme from '@/hooks/useTheme'
|
||||
import * as Constants from '@/lib/constants'
|
||||
|
||||
@@ -21,7 +22,6 @@ const isButtonPressed = (ev: MouseEvent | TouchEvent) => {
|
||||
}
|
||||
|
||||
const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean }) => {
|
||||
const { lightrageGraph } = useLightragGraph()
|
||||
const sigma = useSigma<NodeType, EdgeType>()
|
||||
const registerEvents = useRegisterEvents<NodeType, EdgeType>()
|
||||
const setSettings = useSetSettings<NodeType, EdgeType>()
|
||||
@@ -38,17 +38,18 @@ const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean })
|
||||
const focusedNode = useGraphStore.use.focusedNode()
|
||||
const selectedEdge = useGraphStore.use.selectedEdge()
|
||||
const focusedEdge = useGraphStore.use.focusedEdge()
|
||||
const sigmaGraph = useGraphStore.use.sigmaGraph()
|
||||
|
||||
/**
|
||||
* When component mount or maxIterations changes
|
||||
* => load the graph and apply layout
|
||||
*/
|
||||
useEffect(() => {
|
||||
// Create & load the graph
|
||||
const graph = lightrageGraph()
|
||||
loadGraph(graph)
|
||||
assignLayout()
|
||||
}, [assignLayout, loadGraph, lightrageGraph, maxIterations])
|
||||
if (sigmaGraph) {
|
||||
loadGraph(sigmaGraph as unknown as Graph<NodeType, EdgeType>)
|
||||
assignLayout()
|
||||
}
|
||||
}, [assignLayout, loadGraph, sigmaGraph, maxIterations])
|
||||
|
||||
/**
|
||||
* When component mount
|
||||
|
@@ -9,7 +9,7 @@ import { useTranslation } from 'react-i18next'
|
||||
const GraphLabels = () => {
|
||||
const { t } = useTranslation()
|
||||
const label = useSettingsStore.use.queryLabel()
|
||||
const graphLabels = useGraphStore.use.graphLabels()
|
||||
const allDatabaseLabels = useGraphStore.use.allDatabaseLabels()
|
||||
|
||||
const getSearchEngine = useCallback(() => {
|
||||
// Create search engine
|
||||
@@ -26,14 +26,14 @@ const GraphLabels = () => {
|
||||
})
|
||||
|
||||
// Add documents
|
||||
const documents = graphLabels.map((str, index) => ({ id: index, value: str }))
|
||||
const documents = allDatabaseLabels.map((str, index) => ({ id: index, value: str }))
|
||||
searchEngine.addAll(documents)
|
||||
|
||||
return {
|
||||
labels: graphLabels,
|
||||
labels: allDatabaseLabels,
|
||||
searchEngine
|
||||
}
|
||||
}, [graphLabels])
|
||||
}, [allDatabaseLabels])
|
||||
|
||||
const fetchData = useCallback(
|
||||
async (query?: string): Promise<string[]> => {
|
||||
@@ -47,24 +47,11 @@ const GraphLabels = () => {
|
||||
|
||||
return result.length <= labelListLimit
|
||||
? result
|
||||
: [...result.slice(0, labelListLimit), t('graphLabels.andOthers', { count: result.length - labelListLimit })]
|
||||
: [...result.slice(0, labelListLimit), '...']
|
||||
},
|
||||
[getSearchEngine]
|
||||
)
|
||||
|
||||
const setQueryLabel = useCallback((newLabel: string) => {
|
||||
if (newLabel.startsWith('And ') && newLabel.endsWith(' others')) return
|
||||
|
||||
const currentLabel = useSettingsStore.getState().queryLabel
|
||||
|
||||
// When selecting the same label (except '*'), switch to '*'
|
||||
if (newLabel === currentLabel && newLabel !== '*') {
|
||||
useSettingsStore.getState().setQueryLabel('*')
|
||||
} else {
|
||||
useSettingsStore.getState().setQueryLabel(newLabel)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<AsyncSelect<string>
|
||||
className="ml-2"
|
||||
@@ -78,8 +65,20 @@ const GraphLabels = () => {
|
||||
notFound={<div className="py-6 text-center text-sm">No labels found</div>}
|
||||
label={t('graphPanel.graphLabels.label')}
|
||||
placeholder={t('graphPanel.graphLabels.placeholder')}
|
||||
value={label !== null ? label : ''}
|
||||
onChange={setQueryLabel}
|
||||
value={label !== null ? label : '*'}
|
||||
onChange={(newLabel) => {
|
||||
const currentLabel = useSettingsStore.getState().queryLabel
|
||||
|
||||
if (newLabel === '...') {
|
||||
newLabel = '*'
|
||||
}
|
||||
if (newLabel === currentLabel && newLabel !== '*') {
|
||||
// 选择相同标签时切换到'*'
|
||||
useSettingsStore.getState().setQueryLabel('*')
|
||||
} else {
|
||||
useSettingsStore.getState().setQueryLabel(newLabel)
|
||||
}
|
||||
}}
|
||||
clearable={false} // Prevent clearing value on reselect
|
||||
/>
|
||||
)
|
||||
|
@@ -85,8 +85,19 @@ export const GraphSearchInput = ({
|
||||
const loadOptions = useCallback(
|
||||
async (query?: string): Promise<OptionItem[]> => {
|
||||
if (onFocus) onFocus(null)
|
||||
if (!query || !searchEngine) return []
|
||||
const result: OptionItem[] = searchEngine.search(query).map((r) => ({
|
||||
if (!graph || !searchEngine) return []
|
||||
|
||||
// If no query, return first searchResultLimit nodes
|
||||
if (!query) {
|
||||
const nodeIds = graph.nodes().slice(0, searchResultLimit)
|
||||
return nodeIds.map(id => ({
|
||||
id,
|
||||
type: 'nodes'
|
||||
}))
|
||||
}
|
||||
|
||||
// If has query, search nodes
|
||||
const result: OptionItem[] = searchEngine.search(query).map((r: { id: string }) => ({
|
||||
id: r.id,
|
||||
type: 'nodes'
|
||||
}))
|
||||
@@ -103,7 +114,7 @@ export const GraphSearchInput = ({
|
||||
}
|
||||
]
|
||||
},
|
||||
[searchEngine, onFocus]
|
||||
[graph, searchEngine, onFocus, t]
|
||||
)
|
||||
|
||||
return (
|
||||
|
@@ -193,7 +193,7 @@ export function AsyncSearch<T>({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<CommandList hidden={!open || debouncedSearchTerm.length === 0}>
|
||||
<CommandList hidden={!open}>
|
||||
{error && <div className="text-destructive p-4 text-center">{error}</div>}
|
||||
{loading && options.length === 0 && (loadingSkeleton || <DefaultLoadingSkeleton />)}
|
||||
{!loading &&
|
||||
|
@@ -25,7 +25,7 @@ const TooltipContent = React.forwardRef<
|
||||
}
|
||||
>(({ className, side = 'left', align = 'start', children, ...props }, ref) => {
|
||||
const contentRef = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
|
||||
React.useEffect(() => {
|
||||
if (contentRef.current) {
|
||||
contentRef.current.scrollTop = 0;
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState, useCallback, useMemo } from 'react'
|
||||
import { useEffect, useState, useCallback, useMemo, useRef } from 'react'
|
||||
// import { MiniMap } from '@react-sigma/minimap'
|
||||
import { SigmaContainer, useRegisterEvents, useSigma } from '@react-sigma/core'
|
||||
import { Settings as SigmaSettings } from 'sigma/settings'
|
||||
@@ -91,8 +91,12 @@ const GraphEvents = () => {
|
||||
}
|
||||
},
|
||||
// Disable the autoscale at the first down interaction
|
||||
mousedown: () => {
|
||||
if (!sigma.getCustomBBox()) sigma.setCustomBBox(sigma.getBBox())
|
||||
mousedown: (e) => {
|
||||
// Only set custom BBox if it's a drag operation (mouse button is pressed)
|
||||
const mouseEvent = e.original as MouseEvent;
|
||||
if (mouseEvent.buttons !== 0 && !sigma.getCustomBBox()) {
|
||||
sigma.setCustomBBox(sigma.getBBox())
|
||||
}
|
||||
}
|
||||
})
|
||||
}, [registerEvents, sigma, draggedNode])
|
||||
@@ -102,6 +106,7 @@ const GraphEvents = () => {
|
||||
|
||||
const GraphViewer = () => {
|
||||
const [sigmaSettings, setSigmaSettings] = useState(defaultSigmaSettings)
|
||||
const sigmaRef = useRef<any>(null)
|
||||
|
||||
const selectedNode = useGraphStore.use.selectedNode()
|
||||
const focusedNode = useGraphStore.use.focusedNode()
|
||||
@@ -144,7 +149,11 @@ const GraphViewer = () => {
|
||||
)
|
||||
|
||||
return (
|
||||
<SigmaContainer settings={sigmaSettings} className="!bg-background !size-full overflow-hidden">
|
||||
<SigmaContainer
|
||||
settings={sigmaSettings}
|
||||
className="!bg-background !size-full overflow-hidden"
|
||||
ref={sigmaRef}
|
||||
>
|
||||
<GraphControl />
|
||||
|
||||
{enableNodeDrag && <GraphEvents />}
|
||||
|
@@ -162,13 +162,37 @@ const createSigmaGraph = (rawGraph: RawGraph | null) => {
|
||||
}
|
||||
|
||||
const useLightrangeGraph = () => {
|
||||
// Use useRef to maintain lastQueryLabel state between renders
|
||||
const lastQueryLabelRef = useRef({ label: '', maxQueryDepth: 0, minDegree: 0 })
|
||||
const queryLabel = useSettingsStore.use.queryLabel()
|
||||
const rawGraph = useGraphStore.use.rawGraph()
|
||||
const sigmaGraph = useGraphStore.use.sigmaGraph()
|
||||
const maxQueryDepth = useSettingsStore.use.graphQueryMaxDepth()
|
||||
const minDegree = useSettingsStore.use.graphMinDegree()
|
||||
const isFetching = useGraphStore.use.isFetching()
|
||||
|
||||
// Fetch all database labels on mount
|
||||
useEffect(() => {
|
||||
useGraphStore.getState().fetchAllDatabaseLabels()
|
||||
}, [])
|
||||
|
||||
// Use ref to track fetch status
|
||||
const fetchStatusRef = useRef<Record<string, boolean>>({});
|
||||
|
||||
// Track previous parameters to detect actual changes
|
||||
const prevParamsRef = useRef({ queryLabel, maxQueryDepth, minDegree });
|
||||
|
||||
// Reset fetch status only when parameters actually change
|
||||
useEffect(() => {
|
||||
const prevParams = prevParamsRef.current;
|
||||
if (prevParams.queryLabel !== queryLabel ||
|
||||
prevParams.maxQueryDepth !== maxQueryDepth ||
|
||||
prevParams.minDegree !== minDegree) {
|
||||
useGraphStore.getState().setIsFetching(false);
|
||||
// Reset fetch status for new parameters
|
||||
fetchStatusRef.current = {};
|
||||
// Update previous parameters
|
||||
prevParamsRef.current = { queryLabel, maxQueryDepth, minDegree };
|
||||
}
|
||||
}, [queryLabel, maxQueryDepth, minDegree, isFetching])
|
||||
|
||||
const getNode = useCallback(
|
||||
(nodeId: string) => {
|
||||
@@ -186,29 +210,33 @@ const useLightrangeGraph = () => {
|
||||
|
||||
useEffect(() => {
|
||||
if (queryLabel) {
|
||||
// Always fetch data for "*" label
|
||||
// For other labels, only fetch when parameters change
|
||||
const shouldUpdate = true;
|
||||
const fetchKey = `${queryLabel}-${maxQueryDepth}-${minDegree}`;
|
||||
|
||||
if (shouldUpdate) {
|
||||
lastQueryLabelRef.current = {
|
||||
label: queryLabel,
|
||||
maxQueryDepth,
|
||||
minDegree
|
||||
// Only fetch if we haven't fetched this combination in the current component lifecycle
|
||||
if (!isFetching && !fetchStatusRef.current[fetchKey]) {
|
||||
const state = useGraphStore.getState();
|
||||
// Clear selection and highlighted nodes before fetching new graph
|
||||
state.clearSelection();
|
||||
if (state.sigmaGraph) {
|
||||
state.sigmaGraph.forEachNode((node) => {
|
||||
state.sigmaGraph?.setNodeAttribute(node, 'highlighted', false);
|
||||
});
|
||||
}
|
||||
|
||||
state.setIsFetching(true);
|
||||
fetchStatusRef.current[fetchKey] = true;
|
||||
fetchGraph(queryLabel, maxQueryDepth, minDegree).then((data) => {
|
||||
const state = useGraphStore.getState()
|
||||
const newSigmaGraph = createSigmaGraph(data)
|
||||
data?.buildDynamicMap()
|
||||
|
||||
|
||||
// Update all graph data at once to minimize UI flicker
|
||||
state.clearSelection()
|
||||
state.setMoveToSelectedNode(false)
|
||||
state.setSigmaGraph(newSigmaGraph)
|
||||
state.setRawGraph(data)
|
||||
|
||||
// Extract labels from graph data
|
||||
|
||||
// Extract labels from current graph data
|
||||
if (data) {
|
||||
const labelSet = new Set<string>();
|
||||
for (const node of data.nodes) {
|
||||
@@ -227,6 +255,21 @@ const useLightrangeGraph = () => {
|
||||
// Ensure * is there eventhough there is no graph data
|
||||
state.setGraphLabels(['*']);
|
||||
}
|
||||
|
||||
// Fetch all database labels after graph update
|
||||
state.fetchAllDatabaseLabels();
|
||||
if (!data) {
|
||||
// If data is invalid, remove the fetch flag to allow retry
|
||||
delete fetchStatusRef.current[fetchKey];
|
||||
}
|
||||
// Reset fetching state after all updates are complete
|
||||
// Reset camera view by triggering FocusOnNode component
|
||||
state.setMoveToSelectedNode(true);
|
||||
state.setIsFetching(false);
|
||||
}).catch(() => {
|
||||
// Reset fetching state and remove flag in case of error
|
||||
useGraphStore.getState().setIsFetching(false);
|
||||
delete fetchStatusRef.current[fetchKey];
|
||||
})
|
||||
}
|
||||
} else {
|
||||
@@ -234,7 +277,7 @@ const useLightrangeGraph = () => {
|
||||
state.reset()
|
||||
state.setSigmaGraph(new DirectedGraph())
|
||||
}
|
||||
}, [queryLabel, maxQueryDepth, minDegree])
|
||||
}, [queryLabel, maxQueryDepth, minDegree, isFetching])
|
||||
|
||||
const lightrageGraph = useCallback(() => {
|
||||
if (sigmaGraph) {
|
||||
|
@@ -15,8 +15,8 @@ export const edgeColorDarkTheme = '#969696'
|
||||
export const edgeColorSelected = '#F57F17'
|
||||
export const edgeColorHighlighted = '#B2EBF2'
|
||||
|
||||
export const searchResultLimit = 20
|
||||
export const labelListLimit = 40
|
||||
export const searchResultLimit = 50
|
||||
export const labelListLimit = 100
|
||||
|
||||
export const minNodeSize = 4
|
||||
export const maxNodeSize = 20
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import { create } from 'zustand'
|
||||
import { createSelectors } from '@/lib/utils'
|
||||
import { DirectedGraph } from 'graphology'
|
||||
import { getGraphLabels } from '@/api/lightrag'
|
||||
|
||||
export type RawNodeType = {
|
||||
id: string
|
||||
@@ -66,8 +67,10 @@ interface GraphState {
|
||||
rawGraph: RawGraph | null
|
||||
sigmaGraph: DirectedGraph | null
|
||||
graphLabels: string[]
|
||||
allDatabaseLabels: string[]
|
||||
|
||||
moveToSelectedNode: boolean
|
||||
isFetching: boolean
|
||||
|
||||
setSelectedNode: (nodeId: string | null, moveToSelectedNode?: boolean) => void
|
||||
setFocusedNode: (nodeId: string | null) => void
|
||||
@@ -81,6 +84,9 @@ interface GraphState {
|
||||
setRawGraph: (rawGraph: RawGraph | null) => void
|
||||
setSigmaGraph: (sigmaGraph: DirectedGraph | null) => void
|
||||
setGraphLabels: (labels: string[]) => void
|
||||
setAllDatabaseLabels: (labels: string[]) => void
|
||||
fetchAllDatabaseLabels: () => Promise<void>
|
||||
setIsFetching: (isFetching: boolean) => void
|
||||
}
|
||||
|
||||
const useGraphStoreBase = create<GraphState>()((set) => ({
|
||||
@@ -90,11 +96,14 @@ const useGraphStoreBase = create<GraphState>()((set) => ({
|
||||
focusedEdge: null,
|
||||
|
||||
moveToSelectedNode: false,
|
||||
isFetching: false,
|
||||
|
||||
rawGraph: null,
|
||||
sigmaGraph: null,
|
||||
graphLabels: ['*'],
|
||||
allDatabaseLabels: ['*'],
|
||||
|
||||
setIsFetching: (isFetching: boolean) => set({ isFetching }),
|
||||
setSelectedNode: (nodeId: string | null, moveToSelectedNode?: boolean) =>
|
||||
set({ selectedNode: nodeId, moveToSelectedNode }),
|
||||
setFocusedNode: (nodeId: string | null) => set({ focusedNode: nodeId }),
|
||||
@@ -125,9 +134,21 @@ const useGraphStoreBase = create<GraphState>()((set) => ({
|
||||
}),
|
||||
|
||||
setSigmaGraph: (sigmaGraph: DirectedGraph | null) => set({ sigmaGraph }),
|
||||
|
||||
|
||||
setGraphLabels: (labels: string[]) => set({ graphLabels: labels }),
|
||||
|
||||
setAllDatabaseLabels: (labels: string[]) => set({ allDatabaseLabels: labels }),
|
||||
|
||||
fetchAllDatabaseLabels: async () => {
|
||||
try {
|
||||
const labels = await getGraphLabels();
|
||||
set({ allDatabaseLabels: ['*', ...labels] });
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch all database labels:', error);
|
||||
set({ allDatabaseLabels: ['*'] });
|
||||
}
|
||||
},
|
||||
|
||||
setMoveToSelectedNode: (moveToSelectedNode?: boolean) => set({ moveToSelectedNode })
|
||||
}))
|
||||
|
||||
|
@@ -171,7 +171,7 @@ const useSettingsStoreBase = create<SettingsState>()(
|
||||
{
|
||||
name: 'settings-storage',
|
||||
storage: createJSONStorage(() => localStorage),
|
||||
version: 7,
|
||||
version: 8,
|
||||
migrate: (state: any, version: number) => {
|
||||
if (version < 2) {
|
||||
state.showEdgeLabel = false
|
||||
@@ -208,7 +208,13 @@ const useSettingsStoreBase = create<SettingsState>()(
|
||||
}
|
||||
if (version < 7) {
|
||||
state.graphQueryMaxDepth = 3
|
||||
state.graphLayoutMaxIterations = 10
|
||||
state.graphLayoutMaxIterations = 15
|
||||
}
|
||||
if (version < 8) {
|
||||
state.enableNodeDrag = true
|
||||
state.enableHideUnselectedEdges = true
|
||||
state.enableEdgeEvents = false
|
||||
state.graphMinDegree = 0
|
||||
}
|
||||
return state
|
||||
}
|
||||
|
Reference in New Issue
Block a user