Merge branch 'improve-property-tooltip' into feat-node-expand

This commit is contained in:
yangdx
2025-03-13 02:37:09 +08:00
19 changed files with 340 additions and 176 deletions

View File

@@ -423,12 +423,24 @@ def create_app(args):
"update_status": update_status, "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 # Webui mount webui/index.html
static_dir = Path(__file__).parent / "webui" static_dir = Path(__file__).parent / "webui"
static_dir.mkdir(exist_ok=True) static_dir.mkdir(exist_ok=True)
app.mount( app.mount(
"/webui", "/webui",
StaticFiles(directory=static_dir, html=True, check_dir=True), NoCacheStaticFiles(directory=static_dir, html=True, check_dir=True),
name="webui", name="webui",
) )

View File

@@ -99,6 +99,37 @@ class DocsStatusesResponse(BaseModel):
statuses: Dict[DocStatus, List[DocStatusResponse]] = {} 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: class DocumentManager:
def __init__( def __init__(
self, 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 global_args["main_args"].document_loading_engine == "DOCLING":
if not pm.is_installed("docling"): # type: ignore if not pm.is_installed("docling"): # type: ignore
pm.install("docling") pm.install("docling")
from docling.document_converter import DocumentConverter from docling.document_converter import DocumentConverter # type: ignore
converter = DocumentConverter() converter = DocumentConverter()
result = converter.convert(file_path) 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 global_args["main_args"].document_loading_engine == "DOCLING":
if not pm.is_installed("docling"): # type: ignore if not pm.is_installed("docling"): # type: ignore
pm.install("docling") pm.install("docling")
from docling.document_converter import DocumentConverter from docling.document_converter import DocumentConverter # type: ignore
converter = DocumentConverter() converter = DocumentConverter()
result = converter.convert(file_path) 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 global_args["main_args"].document_loading_engine == "DOCLING":
if not pm.is_installed("docling"): # type: ignore if not pm.is_installed("docling"): # type: ignore
pm.install("docling") pm.install("docling")
from docling.document_converter import DocumentConverter from docling.document_converter import DocumentConverter # type: ignore
converter = DocumentConverter() converter = DocumentConverter()
result = converter.convert(file_path) 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 global_args["main_args"].document_loading_engine == "DOCLING":
if not pm.is_installed("docling"): # type: ignore if not pm.is_installed("docling"): # type: ignore
pm.install("docling") pm.install("docling")
from docling.document_converter import DocumentConverter from docling.document_converter import DocumentConverter # type: ignore
converter = DocumentConverter() converter = DocumentConverter()
result = converter.convert(file_path) result = converter.convert(file_path)
@@ -718,17 +749,33 @@ def create_document_routes(
logger.error(traceback.format_exc()) logger.error(traceback.format_exc())
raise HTTPException(status_code=500, detail=str(e)) raise HTTPException(status_code=500, detail=str(e))
@router.get("/pipeline_status", dependencies=[Depends(optional_api_key)]) @router.get(
async def get_pipeline_status(): "/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. Get the current status of the document indexing pipeline.
This endpoint returns information about the current state of the document processing 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 including the processing status, progress information, and history messages.
are being processed, how many batches there are, and which batch is currently being processed.
Returns: 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: try:
from lightrag.kg.shared_storage import get_namespace_data from lightrag.kg.shared_storage import get_namespace_data
@@ -746,7 +793,7 @@ def create_document_routes(
if status_dict.get("job_start"): if status_dict.get("job_start"):
status_dict["job_start"] = str(status_dict["job_start"]) status_dict["job_start"] = str(status_dict["job_start"])
return status_dict return PipelineStatusResponse(**status_dict)
except Exception as e: except Exception as e:
logger.error(f"Error getting pipeline status: {str(e)}") logger.error(f"Error getting pipeline status: {str(e)}")
logger.error(traceback.format_exc()) logger.error(traceback.format_exc())

File diff suppressed because one or more lines are too long

View File

@@ -2,10 +2,13 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <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" /> <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-B9TRs-Wk.js"></script> <script type="module" crossorigin src="./assets/index-Cs3ZbEon.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-DRGuXfZw.css"> <link rel="stylesheet" crossorigin href="./assets/index-DRGuXfZw.css">
</head> </head>
<body> <body>

View File

@@ -661,7 +661,7 @@ class Neo4JStorage(BaseGraphStorage):
WITH collect({node: n}) AS filtered_nodes WITH collect({node: n}) AS filtered_nodes
UNWIND filtered_nodes AS node_info UNWIND filtered_nodes AS node_info
WITH collect(node_info.node) AS kept_nodes, filtered_nodes 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 WHERE a IN kept_nodes AND b IN kept_nodes
RETURN filtered_nodes AS node_info, RETURN filtered_nodes AS node_info,
collect(DISTINCT r) AS relationships collect(DISTINCT r) AS relationships
@@ -704,7 +704,7 @@ class Neo4JStorage(BaseGraphStorage):
WITH collect({node: node}) AS filtered_nodes WITH collect({node: node}) AS filtered_nodes
UNWIND filtered_nodes AS node_info UNWIND filtered_nodes AS node_info
WITH collect(node_info.node) AS kept_nodes, filtered_nodes 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 WHERE a IN kept_nodes AND b IN kept_nodes
RETURN filtered_nodes AS node_info, RETURN filtered_nodes AS node_info,
collect(DISTINCT r) AS relationships collect(DISTINCT r) AS relationships

View File

@@ -2,6 +2,9 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <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" /> <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>

View File

@@ -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. * When the selected item changes, highlighted the node and center the camera on it.
*/ */
useEffect(() => { useEffect(() => {
if (!node) return
sigma.getGraph().setNodeAttribute(node, 'highlighted', true)
if (move) { 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) useGraphStore.getState().setMoveToSelectedNode(false)
} else if (node) {
sigma.getGraph().setNodeAttribute(node, 'highlighted', true)
} }
return () => { return () => {
sigma.getGraph().setNodeAttribute(node, 'highlighted', false) if (node) {
sigma.getGraph().setNodeAttribute(node, 'highlighted', false)
}
} }
}, [node, move, sigma, gotoNode]) }, [node, move, sigma, gotoNode])

View File

@@ -1,10 +1,11 @@
import { useLoadGraph, useRegisterEvents, useSetSettings, useSigma } from '@react-sigma/core' import { useLoadGraph, useRegisterEvents, useSetSettings, useSigma } from '@react-sigma/core'
import Graph from 'graphology'
// import { useLayoutCircular } from '@react-sigma/layout-circular' // import { useLayoutCircular } from '@react-sigma/layout-circular'
import { useLayoutForceAtlas2 } from '@react-sigma/layout-forceatlas2' import { useLayoutForceAtlas2 } from '@react-sigma/layout-forceatlas2'
import { useEffect } from 'react' import { useEffect } from 'react'
// import useRandomGraph, { EdgeType, NodeType } from '@/hooks/useRandomGraph' // 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 useTheme from '@/hooks/useTheme'
import * as Constants from '@/lib/constants' import * as Constants from '@/lib/constants'
@@ -21,7 +22,6 @@ const isButtonPressed = (ev: MouseEvent | TouchEvent) => {
} }
const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean }) => { const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean }) => {
const { lightrageGraph } = useLightragGraph()
const sigma = useSigma<NodeType, EdgeType>() const sigma = useSigma<NodeType, EdgeType>()
const registerEvents = useRegisterEvents<NodeType, EdgeType>() const registerEvents = useRegisterEvents<NodeType, EdgeType>()
const setSettings = useSetSettings<NodeType, EdgeType>() const setSettings = useSetSettings<NodeType, EdgeType>()
@@ -38,17 +38,18 @@ const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean })
const focusedNode = useGraphStore.use.focusedNode() const focusedNode = useGraphStore.use.focusedNode()
const selectedEdge = useGraphStore.use.selectedEdge() const selectedEdge = useGraphStore.use.selectedEdge()
const focusedEdge = useGraphStore.use.focusedEdge() const focusedEdge = useGraphStore.use.focusedEdge()
const sigmaGraph = useGraphStore.use.sigmaGraph()
/** /**
* When component mount or maxIterations changes * When component mount or maxIterations changes
* => load the graph and apply layout * => load the graph and apply layout
*/ */
useEffect(() => { useEffect(() => {
// Create & load the graph if (sigmaGraph) {
const graph = lightrageGraph() loadGraph(sigmaGraph as unknown as Graph<NodeType, EdgeType>)
loadGraph(graph) assignLayout()
assignLayout() }
}, [assignLayout, loadGraph, lightrageGraph, maxIterations]) }, [assignLayout, loadGraph, sigmaGraph, maxIterations])
/** /**
* When component mount * When component mount

View File

@@ -9,7 +9,7 @@ import { useTranslation } from 'react-i18next'
const GraphLabels = () => { const GraphLabels = () => {
const { t } = useTranslation() const { t } = useTranslation()
const label = useSettingsStore.use.queryLabel() const label = useSettingsStore.use.queryLabel()
const graphLabels = useGraphStore.use.graphLabels() const allDatabaseLabels = useGraphStore.use.allDatabaseLabels()
const getSearchEngine = useCallback(() => { const getSearchEngine = useCallback(() => {
// Create search engine // Create search engine
@@ -26,14 +26,14 @@ const GraphLabels = () => {
}) })
// Add documents // 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) searchEngine.addAll(documents)
return { return {
labels: graphLabels, labels: allDatabaseLabels,
searchEngine searchEngine
} }
}, [graphLabels]) }, [allDatabaseLabels])
const fetchData = useCallback( const fetchData = useCallback(
async (query?: string): Promise<string[]> => { async (query?: string): Promise<string[]> => {
@@ -47,24 +47,11 @@ const GraphLabels = () => {
return result.length <= labelListLimit return result.length <= labelListLimit
? result ? result
: [...result.slice(0, labelListLimit), t('graphLabels.andOthers', { count: result.length - labelListLimit })] : [...result.slice(0, labelListLimit), '...']
}, },
[getSearchEngine] [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 ( return (
<AsyncSelect<string> <AsyncSelect<string>
className="ml-2" className="ml-2"
@@ -78,8 +65,20 @@ const GraphLabels = () => {
notFound={<div className="py-6 text-center text-sm">No labels found</div>} notFound={<div className="py-6 text-center text-sm">No labels found</div>}
label={t('graphPanel.graphLabels.label')} label={t('graphPanel.graphLabels.label')}
placeholder={t('graphPanel.graphLabels.placeholder')} placeholder={t('graphPanel.graphLabels.placeholder')}
value={label !== null ? label : ''} value={label !== null ? label : '*'}
onChange={setQueryLabel} 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 clearable={false} // Prevent clearing value on reselect
/> />
) )

View File

@@ -85,8 +85,19 @@ export const GraphSearchInput = ({
const loadOptions = useCallback( const loadOptions = useCallback(
async (query?: string): Promise<OptionItem[]> => { async (query?: string): Promise<OptionItem[]> => {
if (onFocus) onFocus(null) if (onFocus) onFocus(null)
if (!query || !searchEngine) return [] if (!graph || !searchEngine) return []
const result: OptionItem[] = searchEngine.search(query).map((r) => ({
// 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, id: r.id,
type: 'nodes' type: 'nodes'
})) }))
@@ -103,7 +114,7 @@ export const GraphSearchInput = ({
} }
] ]
}, },
[searchEngine, onFocus] [graph, searchEngine, onFocus, t]
) )
return ( return (

View File

@@ -193,7 +193,7 @@ export function AsyncSearch<T>({
</div> </div>
)} )}
</div> </div>
<CommandList hidden={!open || debouncedSearchTerm.length === 0}> <CommandList hidden={!open}>
{error && <div className="text-destructive p-4 text-center">{error}</div>} {error && <div className="text-destructive p-4 text-center">{error}</div>}
{loading && options.length === 0 && (loadingSkeleton || <DefaultLoadingSkeleton />)} {loading && options.length === 0 && (loadingSkeleton || <DefaultLoadingSkeleton />)}
{!loading && {!loading &&

View File

@@ -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 { MiniMap } from '@react-sigma/minimap'
import { SigmaContainer, useRegisterEvents, useSigma } from '@react-sigma/core' import { SigmaContainer, useRegisterEvents, useSigma } from '@react-sigma/core'
import { Settings as SigmaSettings } from 'sigma/settings' import { Settings as SigmaSettings } from 'sigma/settings'
@@ -91,8 +91,12 @@ const GraphEvents = () => {
} }
}, },
// Disable the autoscale at the first down interaction // Disable the autoscale at the first down interaction
mousedown: () => { mousedown: (e) => {
if (!sigma.getCustomBBox()) sigma.setCustomBBox(sigma.getBBox()) // 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]) }, [registerEvents, sigma, draggedNode])
@@ -102,6 +106,7 @@ const GraphEvents = () => {
const GraphViewer = () => { const GraphViewer = () => {
const [sigmaSettings, setSigmaSettings] = useState(defaultSigmaSettings) const [sigmaSettings, setSigmaSettings] = useState(defaultSigmaSettings)
const sigmaRef = useRef<any>(null)
const selectedNode = useGraphStore.use.selectedNode() const selectedNode = useGraphStore.use.selectedNode()
const focusedNode = useGraphStore.use.focusedNode() const focusedNode = useGraphStore.use.focusedNode()
@@ -144,7 +149,11 @@ const GraphViewer = () => {
) )
return ( return (
<SigmaContainer settings={sigmaSettings} className="!bg-background !size-full overflow-hidden"> <SigmaContainer
settings={sigmaSettings}
className="!bg-background !size-full overflow-hidden"
ref={sigmaRef}
>
<GraphControl /> <GraphControl />
{enableNodeDrag && <GraphEvents />} {enableNodeDrag && <GraphEvents />}

View File

@@ -162,13 +162,37 @@ const createSigmaGraph = (rawGraph: RawGraph | null) => {
} }
const useLightrangeGraph = () => { const useLightrangeGraph = () => {
// Use useRef to maintain lastQueryLabel state between renders
const lastQueryLabelRef = useRef({ label: '', maxQueryDepth: 0, minDegree: 0 })
const queryLabel = useSettingsStore.use.queryLabel() const queryLabel = useSettingsStore.use.queryLabel()
const rawGraph = useGraphStore.use.rawGraph() const rawGraph = useGraphStore.use.rawGraph()
const sigmaGraph = useGraphStore.use.sigmaGraph() const sigmaGraph = useGraphStore.use.sigmaGraph()
const maxQueryDepth = useSettingsStore.use.graphQueryMaxDepth() const maxQueryDepth = useSettingsStore.use.graphQueryMaxDepth()
const minDegree = useSettingsStore.use.graphMinDegree() 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( const getNode = useCallback(
(nodeId: string) => { (nodeId: string) => {
@@ -186,17 +210,21 @@ const useLightrangeGraph = () => {
useEffect(() => { useEffect(() => {
if (queryLabel) { if (queryLabel) {
// Always fetch data for "*" label const fetchKey = `${queryLabel}-${maxQueryDepth}-${minDegree}`;
// For other labels, only fetch when parameters change
const shouldUpdate = true;
if (shouldUpdate) { // Only fetch if we haven't fetched this combination in the current component lifecycle
lastQueryLabelRef.current = { if (!isFetching && !fetchStatusRef.current[fetchKey]) {
label: queryLabel, const state = useGraphStore.getState();
maxQueryDepth, // Clear selection and highlighted nodes before fetching new graph
minDegree 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) => { fetchGraph(queryLabel, maxQueryDepth, minDegree).then((data) => {
const state = useGraphStore.getState() const state = useGraphStore.getState()
const newSigmaGraph = createSigmaGraph(data) const newSigmaGraph = createSigmaGraph(data)
@@ -208,7 +236,7 @@ const useLightrangeGraph = () => {
state.setSigmaGraph(newSigmaGraph) state.setSigmaGraph(newSigmaGraph)
state.setRawGraph(data) state.setRawGraph(data)
// Extract labels from graph data // Extract labels from current graph data
if (data) { if (data) {
const labelSet = new Set<string>(); const labelSet = new Set<string>();
for (const node of data.nodes) { for (const node of data.nodes) {
@@ -227,6 +255,21 @@ const useLightrangeGraph = () => {
// Ensure * is there eventhough there is no graph data // Ensure * is there eventhough there is no graph data
state.setGraphLabels(['*']); 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 { } else {
@@ -234,7 +277,7 @@ const useLightrangeGraph = () => {
state.reset() state.reset()
state.setSigmaGraph(new DirectedGraph()) state.setSigmaGraph(new DirectedGraph())
} }
}, [queryLabel, maxQueryDepth, minDegree]) }, [queryLabel, maxQueryDepth, minDegree, isFetching])
const lightrageGraph = useCallback(() => { const lightrageGraph = useCallback(() => {
if (sigmaGraph) { if (sigmaGraph) {

View File

@@ -15,8 +15,8 @@ export const edgeColorDarkTheme = '#969696'
export const edgeColorSelected = '#F57F17' export const edgeColorSelected = '#F57F17'
export const edgeColorHighlighted = '#B2EBF2' export const edgeColorHighlighted = '#B2EBF2'
export const searchResultLimit = 20 export const searchResultLimit = 50
export const labelListLimit = 40 export const labelListLimit = 100
export const minNodeSize = 4 export const minNodeSize = 4
export const maxNodeSize = 20 export const maxNodeSize = 20

View File

@@ -1,6 +1,7 @@
import { create } from 'zustand' import { create } from 'zustand'
import { createSelectors } from '@/lib/utils' import { createSelectors } from '@/lib/utils'
import { DirectedGraph } from 'graphology' import { DirectedGraph } from 'graphology'
import { getGraphLabels } from '@/api/lightrag'
export type RawNodeType = { export type RawNodeType = {
id: string id: string
@@ -66,8 +67,10 @@ interface GraphState {
rawGraph: RawGraph | null rawGraph: RawGraph | null
sigmaGraph: DirectedGraph | null sigmaGraph: DirectedGraph | null
graphLabels: string[] graphLabels: string[]
allDatabaseLabels: string[]
moveToSelectedNode: boolean moveToSelectedNode: boolean
isFetching: boolean
setSelectedNode: (nodeId: string | null, moveToSelectedNode?: boolean) => void setSelectedNode: (nodeId: string | null, moveToSelectedNode?: boolean) => void
setFocusedNode: (nodeId: string | null) => void setFocusedNode: (nodeId: string | null) => void
@@ -81,6 +84,9 @@ interface GraphState {
setRawGraph: (rawGraph: RawGraph | null) => void setRawGraph: (rawGraph: RawGraph | null) => void
setSigmaGraph: (sigmaGraph: DirectedGraph | null) => void setSigmaGraph: (sigmaGraph: DirectedGraph | null) => void
setGraphLabels: (labels: string[]) => void setGraphLabels: (labels: string[]) => void
setAllDatabaseLabels: (labels: string[]) => void
fetchAllDatabaseLabels: () => Promise<void>
setIsFetching: (isFetching: boolean) => void
} }
const useGraphStoreBase = create<GraphState>()((set) => ({ const useGraphStoreBase = create<GraphState>()((set) => ({
@@ -90,11 +96,14 @@ const useGraphStoreBase = create<GraphState>()((set) => ({
focusedEdge: null, focusedEdge: null,
moveToSelectedNode: false, moveToSelectedNode: false,
isFetching: false,
rawGraph: null, rawGraph: null,
sigmaGraph: null, sigmaGraph: null,
graphLabels: ['*'], graphLabels: ['*'],
allDatabaseLabels: ['*'],
setIsFetching: (isFetching: boolean) => set({ isFetching }),
setSelectedNode: (nodeId: string | null, moveToSelectedNode?: boolean) => setSelectedNode: (nodeId: string | null, moveToSelectedNode?: boolean) =>
set({ selectedNode: nodeId, moveToSelectedNode }), set({ selectedNode: nodeId, moveToSelectedNode }),
setFocusedNode: (nodeId: string | null) => set({ focusedNode: nodeId }), setFocusedNode: (nodeId: string | null) => set({ focusedNode: nodeId }),
@@ -128,6 +137,18 @@ const useGraphStoreBase = create<GraphState>()((set) => ({
setGraphLabels: (labels: string[]) => set({ graphLabels: labels }), 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 }) setMoveToSelectedNode: (moveToSelectedNode?: boolean) => set({ moveToSelectedNode })
})) }))

View File

@@ -171,7 +171,7 @@ const useSettingsStoreBase = create<SettingsState>()(
{ {
name: 'settings-storage', name: 'settings-storage',
storage: createJSONStorage(() => localStorage), storage: createJSONStorage(() => localStorage),
version: 7, version: 8,
migrate: (state: any, version: number) => { migrate: (state: any, version: number) => {
if (version < 2) { if (version < 2) {
state.showEdgeLabel = false state.showEdgeLabel = false
@@ -208,7 +208,13 @@ const useSettingsStoreBase = create<SettingsState>()(
} }
if (version < 7) { if (version < 7) {
state.graphQueryMaxDepth = 3 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 return state
} }