diff --git a/lightrag_webui/src/components/graph/PropertiesView.tsx b/lightrag_webui/src/components/graph/PropertiesView.tsx index 39bd3bb4..24a7829b 100644 --- a/lightrag_webui/src/components/graph/PropertiesView.tsx +++ b/lightrag_webui/src/components/graph/PropertiesView.tsx @@ -16,10 +16,12 @@ const PropertiesView = () => { const focusedNode = useGraphStore.use.focusedNode() const selectedEdge = useGraphStore.use.selectedEdge() const focusedEdge = useGraphStore.use.focusedEdge() + const graphDataVersion = useGraphStore.use.graphDataVersion() const [currentElement, setCurrentElement] = useState(null) const [currentType, setCurrentType] = useState<'node' | 'edge' | null>(null) + // This effect will run when selection changes or when graph data is updated useEffect(() => { let type: 'node' | 'edge' | null = null let element: RawNodeType | RawEdgeType | null = null @@ -53,6 +55,7 @@ const PropertiesView = () => { selectedNode, focusedEdge, selectedEdge, + graphDataVersion, // Add dependency on graphDataVersion to refresh when data changes setCurrentElement, setCurrentType, getNode, diff --git a/lightrag_webui/src/stores/graph.ts b/lightrag_webui/src/stores/graph.ts index 37a48545..c4c6a285 100644 --- a/lightrag_webui/src/stores/graph.ts +++ b/lightrag_webui/src/stores/graph.ts @@ -31,6 +31,15 @@ export type RawEdgeType = { dynamicId: string } +/** + * Interface for tracking edges that need updating when a node ID changes + */ +interface EdgeToUpdate { + originalDynamicId: string + newEdgeId: string + edgeIndex: number +} + export class RawGraph { nodes: RawNodeType[] = [] edges: RawEdgeType[] = [] @@ -127,9 +136,13 @@ interface GraphState { // Version counter to trigger data refresh graphDataVersion: number incrementGraphDataVersion: () => void + + // Methods for updating graph elements and UI state together + updateNodeAndSelect: (nodeId: string, entityId: string, propertyName: string, newValue: string) => Promise + updateEdgeAndSelect: (edgeId: string, dynamicId: string, sourceId: string, targetId: string, propertyName: string, newValue: string) => Promise } -const useGraphStoreBase = create()((set) => ({ +const useGraphStoreBase = create()((set, get) => ({ selectedNode: null, focusedNode: null, selectedEdge: null, @@ -234,6 +247,140 @@ const useGraphStoreBase = create()((set) => ({ graphDataVersion: 0, incrementGraphDataVersion: () => set((state) => ({ graphDataVersion: state.graphDataVersion + 1 })), + // Methods for updating graph elements and UI state together + updateNodeAndSelect: async (nodeId: string, entityId: string, propertyName: string, newValue: string) => { + // Get current state + const state = get() + const { sigmaGraph, rawGraph } = state + + // Validate graph state + if (!sigmaGraph || !rawGraph || !sigmaGraph.hasNode(nodeId)) { + return + } + + try { + const nodeAttributes = sigmaGraph.getNodeAttributes(nodeId) + + console.log('updateNodeAndSelect', nodeId, entityId, propertyName, newValue) + + // For entity_id changes (node renaming) with NetworkX graph storage + if ((nodeId === entityId) && (propertyName === 'entity_id')) { + // Create new node with updated ID but same attributes + sigmaGraph.addNode(newValue, { ...nodeAttributes, label: newValue }) + + const edgesToUpdate: EdgeToUpdate[] = [] + + // Process all edges connected to this node + sigmaGraph.forEachEdge(nodeId, (edge, attributes, source, target) => { + const otherNode = source === nodeId ? target : source + const isOutgoing = source === nodeId + + // Get original edge dynamic ID for later reference + const originalEdgeDynamicId = edge + const edgeIndexInRawGraph = rawGraph.edgeDynamicIdMap[originalEdgeDynamicId] + + // Create new edge with updated node reference + const newEdgeId = sigmaGraph.addEdge( + isOutgoing ? newValue : otherNode, + isOutgoing ? otherNode : newValue, + attributes + ) + + // Track edges that need updating in the raw graph + if (edgeIndexInRawGraph !== undefined) { + edgesToUpdate.push({ + originalDynamicId: originalEdgeDynamicId, + newEdgeId: newEdgeId, + edgeIndex: edgeIndexInRawGraph + }) + } + + // Remove the old edge + sigmaGraph.dropEdge(edge) + }) + + // Remove the old node after all edges are processed + sigmaGraph.dropNode(nodeId) + + // Update node reference in raw graph data + const nodeIndex = rawGraph.nodeIdMap[nodeId] + if (nodeIndex !== undefined) { + rawGraph.nodes[nodeIndex].id = newValue + rawGraph.nodes[nodeIndex].labels = [newValue] + rawGraph.nodes[nodeIndex].properties.entity_id = newValue + delete rawGraph.nodeIdMap[nodeId] + rawGraph.nodeIdMap[newValue] = nodeIndex + } + + // Update all edge references in raw graph data + edgesToUpdate.forEach(({ originalDynamicId, newEdgeId, edgeIndex }) => { + if (rawGraph.edges[edgeIndex]) { + // Update source/target references + if (rawGraph.edges[edgeIndex].source === nodeId) { + rawGraph.edges[edgeIndex].source = newValue + } + if (rawGraph.edges[edgeIndex].target === nodeId) { + rawGraph.edges[edgeIndex].target = newValue + } + + // Update dynamic ID mappings + rawGraph.edges[edgeIndex].dynamicId = newEdgeId + delete rawGraph.edgeDynamicIdMap[originalDynamicId] + rawGraph.edgeDynamicIdMap[newEdgeId] = edgeIndex + } + }) + + // Update selected node in store + set({ selectedNode: newValue, moveToSelectedNode: true }) + } else { + // For non-NetworkX nodes or non-entity_id changes + const nodeIndex = rawGraph.nodeIdMap[String(nodeId)] + if (nodeIndex !== undefined) { + rawGraph.nodes[nodeIndex].properties[propertyName] = newValue + if (propertyName === 'entity_id') { + rawGraph.nodes[nodeIndex].labels = [newValue] + sigmaGraph.setNodeAttribute(String(nodeId), 'label', newValue) + } + } + + // Trigger a re-render by incrementing the version counter + set((state) => ({ graphDataVersion: state.graphDataVersion + 1 })) + } + } catch (error) { + console.error('Error updating node in graph:', error) + throw new Error('Failed to update node in graph') + } + }, + + updateEdgeAndSelect: async (edgeId: string, dynamicId: string, sourceId: string, targetId: string, propertyName: string, newValue: string) => { + // Get current state + const state = get() + const { sigmaGraph, rawGraph } = state + + // Validate graph state + if (!sigmaGraph || !rawGraph) { + return + } + + try { + const edgeIndex = rawGraph.edgeIdMap[String(edgeId)] + if (edgeIndex !== undefined && rawGraph.edges[edgeIndex]) { + rawGraph.edges[edgeIndex].properties[propertyName] = newValue + if(dynamicId !== undefined && propertyName === 'keywords') { + sigmaGraph.setEdgeAttribute(dynamicId, 'label', newValue) + } + } + + // Trigger a re-render by incrementing the version counter + set((state) => ({ graphDataVersion: state.graphDataVersion + 1 })) + + // Update selected edge in store to ensure UI reflects changes + set({ selectedEdge: dynamicId }) + } catch (error) { + console.error(`Error updating edge ${sourceId}->${targetId} in graph:`, error) + throw new Error('Failed to update edge in graph') + } + } })) const useGraphStore = createSelectors(useGraphStoreBase) diff --git a/lightrag_webui/src/utils/graphOperations.ts b/lightrag_webui/src/utils/graphOperations.ts index d36b312f..5bc9c5ba 100644 --- a/lightrag_webui/src/utils/graphOperations.ts +++ b/lightrag_webui/src/utils/graphOperations.ts @@ -1,117 +1,18 @@ import { useGraphStore } from '@/stores/graph' -/** - * Interface for tracking edges that need updating when a node ID changes - */ -interface EdgeToUpdate { - originalDynamicId: string - newEdgeId: string - edgeIndex: number -} - /** * Update node in the graph visualization - * Handles both property updates and entity ID changes - * + * This function is now a wrapper around the store's updateNodeAndSelect method + * * @param nodeId - ID of the node to update + * @param entityId - ID of the entity * @param propertyName - Name of the property being updated * @param newValue - New value for the property */ -export const updateGraphNode = async (nodeId: string, entityId:string, propertyName: string, newValue: string) => { - // Get graph state from store - const sigmaGraph = useGraphStore.getState().sigmaGraph - const rawGraph = useGraphStore.getState().rawGraph - - // Validate graph state - if (!sigmaGraph || !rawGraph || !sigmaGraph.hasNode(nodeId)) { - return - } - +export const updateGraphNode = async (nodeId: string, entityId: string, propertyName: string, newValue: string) => { try { - const nodeAttributes = sigmaGraph.getNodeAttributes(nodeId) - - console.log('updateGraphNode', nodeId, entityId, propertyName, newValue) - - // For entity_id changes (node renaming) with NetworkX graph storage - if ((nodeId === entityId) && (propertyName === 'entity_id')) { - // Create new node with updated ID but same attributes - sigmaGraph.addNode(newValue, { ...nodeAttributes, label: newValue }) - - const edgesToUpdate: EdgeToUpdate[] = [] - - // Process all edges connected to this node - sigmaGraph.forEachEdge(nodeId, (edge, attributes, source, target) => { - const otherNode = source === nodeId ? target : source - const isOutgoing = source === nodeId - - // Get original edge dynamic ID for later reference - const originalEdgeDynamicId = edge - const edgeIndexInRawGraph = rawGraph.edgeDynamicIdMap[originalEdgeDynamicId] - - // Create new edge with updated node reference - const newEdgeId = sigmaGraph.addEdge( - isOutgoing ? newValue : otherNode, - isOutgoing ? otherNode : newValue, - attributes - ) - - // Track edges that need updating in the raw graph - if (edgeIndexInRawGraph !== undefined) { - edgesToUpdate.push({ - originalDynamicId: originalEdgeDynamicId, - newEdgeId: newEdgeId, - edgeIndex: edgeIndexInRawGraph - }) - } - - // Remove the old edge - sigmaGraph.dropEdge(edge) - }) - - // Remove the old node after all edges are processed - sigmaGraph.dropNode(nodeId) - - // Update node reference in raw graph data - const nodeIndex = rawGraph.nodeIdMap[nodeId] - if (nodeIndex !== undefined) { - rawGraph.nodes[nodeIndex].id = newValue - rawGraph.nodes[nodeIndex].labels = [newValue] - rawGraph.nodes[nodeIndex].properties.entity_id = newValue - delete rawGraph.nodeIdMap[nodeId] - rawGraph.nodeIdMap[newValue] = nodeIndex - } - - // Update all edge references in raw graph data - edgesToUpdate.forEach(({ originalDynamicId, newEdgeId, edgeIndex }) => { - if (rawGraph.edges[edgeIndex]) { - // Update source/target references - if (rawGraph.edges[edgeIndex].source === nodeId) { - rawGraph.edges[edgeIndex].source = newValue - } - if (rawGraph.edges[edgeIndex].target === nodeId) { - rawGraph.edges[edgeIndex].target = newValue - } - - // Update dynamic ID mappings - rawGraph.edges[edgeIndex].dynamicId = newEdgeId - delete rawGraph.edgeDynamicIdMap[originalDynamicId] - rawGraph.edgeDynamicIdMap[newEdgeId] = edgeIndex - } - }) - - // Update selected node in store - useGraphStore.getState().setSelectedNode(newValue) - } else { - // for none NetworkX nodes or none entity_id changes - const nodeIndex = rawGraph.nodeIdMap[String(nodeId)] - if (nodeIndex !== undefined) { - rawGraph.nodes[nodeIndex].properties[propertyName] = newValue - if (propertyName === 'entity_id') { - rawGraph.nodes[nodeIndex].labels = [newValue] - sigmaGraph.setNodeAttribute(String(nodeId), 'label', newValue) - } - } - } + // Call the store method that handles both data update and UI state + await useGraphStore.getState().updateNodeAndSelect(nodeId, entityId, propertyName, newValue) } catch (error) { console.error('Error updating node in graph:', error) throw new Error('Failed to update node in graph') @@ -120,32 +21,19 @@ export const updateGraphNode = async (nodeId: string, entityId:string, propertyN /** * Update edge in the graph visualization + * This function is now a wrapper around the store's updateEdgeAndSelect method * + * @param edgeId - ID of the edge + * @param dynamicId - Dynamic ID of the edge in sigma graph * @param sourceId - ID of the source node * @param targetId - ID of the target node * @param propertyName - Name of the property being updated * @param newValue - New value for the property */ export const updateGraphEdge = async (edgeId: string, dynamicId: string, sourceId: string, targetId: string, propertyName: string, newValue: string) => { - // Get graph state from store - const sigmaGraph = useGraphStore.getState().sigmaGraph - const rawGraph = useGraphStore.getState().rawGraph - - // Validate graph state - if (!sigmaGraph || !rawGraph) { - return - } - try { - - const edgeIndex = rawGraph.edgeIdMap[String(edgeId)] - if (edgeIndex !== undefined && rawGraph.edges[edgeIndex]) { - rawGraph.edges[edgeIndex].properties[propertyName] = newValue - if(dynamicId !== undefined && propertyName === 'keywords') { - sigmaGraph.setEdgeAttribute(dynamicId, 'label', newValue) - } - } - + // Call the store method that handles both data update and UI state + await useGraphStore.getState().updateEdgeAndSelect(edgeId, dynamicId, sourceId, targetId, propertyName, newValue) } catch (error) { console.error(`Error updating edge ${sourceId}->${targetId} in graph:`, error) throw new Error('Failed to update edge in graph')