Daniel.y commited on
Commit
a71ca10
·
unverified ·
2 Parent(s): 6d16de8 4fe2432

Merge pull request #1374 from danielaskdd/fix-neo4j-graph-edit

Browse files
lightrag/api/__init__.py CHANGED
@@ -1 +1 @@
1
- __api_version__ = "0150"
 
1
+ __api_version__ = "0151"
lightrag/api/webui/assets/{index-1fU28mRC.js → index-CK7D4HV6.js} RENAMED
Binary files a/lightrag/api/webui/assets/index-1fU28mRC.js and b/lightrag/api/webui/assets/index-CK7D4HV6.js differ
 
lightrag/api/webui/index.html CHANGED
Binary files a/lightrag/api/webui/index.html and b/lightrag/api/webui/index.html differ
 
lightrag_webui/src/components/graph/EditablePropertyRow.tsx CHANGED
@@ -2,7 +2,7 @@ import { useState, useEffect } from 'react'
2
  import { useTranslation } from 'react-i18next'
3
  import { toast } from 'sonner'
4
  import { updateEntity, updateRelation, checkEntityNameExists } from '@/api/lightrag'
5
- import { updateGraphNode, updateGraphEdge } from '@/utils/graphOperations'
6
  import { PropertyName, EditIcon, PropertyValue } from './PropertyRowComponents'
7
  import PropertyEditDialog from './PropertyEditDialog'
8
 
@@ -13,7 +13,10 @@ interface EditablePropertyRowProps {
13
  name: string // Property name to display and edit
14
  value: any // Initial value of the property
15
  onClick?: () => void // Optional click handler for the property value
 
16
  entityId?: string // ID of the entity (for node type)
 
 
17
  entityType?: 'node' | 'edge' // Type of graph entity
18
  sourceId?: string // Source node ID (for edge type)
19
  targetId?: string // Target node ID (for edge type)
@@ -30,7 +33,10 @@ const EditablePropertyRow = ({
30
  name,
31
  value: initialValue,
32
  onClick,
 
 
33
  entityId,
 
34
  entityType,
35
  sourceId,
36
  targetId,
@@ -66,7 +72,7 @@ const EditablePropertyRow = ({
66
  setIsSubmitting(true)
67
 
68
  try {
69
- if (entityType === 'node' && entityId) {
70
  let updatedData = { [name]: value }
71
 
72
  if (name === 'entity_id') {
@@ -79,12 +85,22 @@ const EditablePropertyRow = ({
79
  }
80
 
81
  await updateEntity(entityId, updatedData, true)
82
- await updateGraphNode(entityId, name, value)
 
 
 
 
 
83
  toast.success(t('graphPanel.propertiesView.success.entityUpdated'))
84
- } else if (entityType === 'edge' && sourceId && targetId) {
85
  const updatedData = { [name]: value }
86
  await updateRelation(sourceId, targetId, updatedData)
87
- await updateGraphEdge(sourceId, targetId, name, value)
 
 
 
 
 
88
  toast.success(t('graphPanel.propertiesView.success.relationUpdated'))
89
  }
90
 
 
2
  import { useTranslation } from 'react-i18next'
3
  import { toast } from 'sonner'
4
  import { updateEntity, updateRelation, checkEntityNameExists } from '@/api/lightrag'
5
+ import { useGraphStore } from '@/stores/graph'
6
  import { PropertyName, EditIcon, PropertyValue } from './PropertyRowComponents'
7
  import PropertyEditDialog from './PropertyEditDialog'
8
 
 
13
  name: string // Property name to display and edit
14
  value: any // Initial value of the property
15
  onClick?: () => void // Optional click handler for the property value
16
+ nodeId?: string // ID of the node (for node type)
17
  entityId?: string // ID of the entity (for node type)
18
+ edgeId?: string // ID of the edge (for edge type)
19
+ dynamicId?: string
20
  entityType?: 'node' | 'edge' // Type of graph entity
21
  sourceId?: string // Source node ID (for edge type)
22
  targetId?: string // Target node ID (for edge type)
 
33
  name,
34
  value: initialValue,
35
  onClick,
36
+ nodeId,
37
+ edgeId,
38
  entityId,
39
+ dynamicId,
40
  entityType,
41
  sourceId,
42
  targetId,
 
72
  setIsSubmitting(true)
73
 
74
  try {
75
+ if (entityType === 'node' && entityId && nodeId) {
76
  let updatedData = { [name]: value }
77
 
78
  if (name === 'entity_id') {
 
85
  }
86
 
87
  await updateEntity(entityId, updatedData, true)
88
+ try {
89
+ await useGraphStore.getState().updateNodeAndSelect(nodeId, entityId, name, value)
90
+ } catch (error) {
91
+ console.error('Error updating node in graph:', error)
92
+ throw new Error('Failed to update node in graph')
93
+ }
94
  toast.success(t('graphPanel.propertiesView.success.entityUpdated'))
95
+ } else if (entityType === 'edge' && sourceId && targetId && edgeId && dynamicId) {
96
  const updatedData = { [name]: value }
97
  await updateRelation(sourceId, targetId, updatedData)
98
+ try {
99
+ await useGraphStore.getState().updateEdgeAndSelect(edgeId, dynamicId, sourceId, targetId, name, value)
100
+ } catch (error) {
101
+ console.error(`Error updating edge ${sourceId}->${targetId} in graph:`, error)
102
+ throw new Error('Failed to update edge in graph')
103
+ }
104
  toast.success(t('graphPanel.propertiesView.success.relationUpdated'))
105
  }
106
 
lightrag_webui/src/components/graph/PropertiesView.tsx CHANGED
@@ -16,10 +16,12 @@ const PropertiesView = () => {
16
  const focusedNode = useGraphStore.use.focusedNode()
17
  const selectedEdge = useGraphStore.use.selectedEdge()
18
  const focusedEdge = useGraphStore.use.focusedEdge()
 
19
 
20
  const [currentElement, setCurrentElement] = useState<NodeType | EdgeType | null>(null)
21
  const [currentType, setCurrentType] = useState<'node' | 'edge' | null>(null)
22
 
 
23
  useEffect(() => {
24
  let type: 'node' | 'edge' | null = null
25
  let element: RawNodeType | RawEdgeType | null = null
@@ -53,6 +55,7 @@ const PropertiesView = () => {
53
  selectedNode,
54
  focusedEdge,
55
  selectedEdge,
 
56
  setCurrentElement,
57
  setCurrentType,
58
  getNode,
@@ -93,6 +96,7 @@ const refineNodeProperties = (node: RawNodeType): NodeType => {
93
  if (state.sigmaGraph && state.rawGraph) {
94
  try {
95
  if (!state.sigmaGraph.hasNode(node.id)) {
 
96
  return {
97
  ...node,
98
  relationships: []
@@ -139,7 +143,8 @@ const refineEdgeProperties = (edge: RawEdgeType): EdgeType => {
139
 
140
  if (state.sigmaGraph && state.rawGraph) {
141
  try {
142
- if (!state.sigmaGraph.hasEdge(edge.id)) {
 
143
  return {
144
  ...edge,
145
  sourceNode: undefined,
@@ -171,6 +176,9 @@ const PropertyRow = ({
171
  value,
172
  onClick,
173
  tooltip,
 
 
 
174
  entityId,
175
  entityType,
176
  sourceId,
@@ -181,7 +189,10 @@ const PropertyRow = ({
181
  value: any
182
  onClick?: () => void
183
  tooltip?: string
 
184
  entityId?: string
 
 
185
  entityType?: 'node' | 'edge'
186
  sourceId?: string
187
  targetId?: string
@@ -202,7 +213,10 @@ const PropertyRow = ({
202
  name={name}
203
  value={value}
204
  onClick={onClick}
 
205
  entityId={entityId}
 
 
206
  entityType={entityType}
207
  sourceId={sourceId}
208
  targetId={targetId}
@@ -265,7 +279,7 @@ const NodePropertiesView = ({ node }: { node: NodeType }) => {
265
  </div>
266
  </div>
267
  <div className="bg-primary/5 max-h-96 overflow-auto rounded p-1">
268
- <PropertyRow name={t('graphPanel.propertiesView.node.id')} value={node.id} />
269
  <PropertyRow
270
  name={t('graphPanel.propertiesView.node.labels')}
271
  value={node.labels.join(', ')}
@@ -285,7 +299,8 @@ const NodePropertiesView = ({ node }: { node: NodeType }) => {
285
  key={name}
286
  name={name}
287
  value={node.properties[name]}
288
- entityId={node.properties['entity_id'] || node.id}
 
289
  entityType="node"
290
  isEditable={name === 'description' || name === 'entity_id'}
291
  />
@@ -350,10 +365,11 @@ const EdgePropertiesView = ({ edge }: { edge: EdgeType }) => {
350
  key={name}
351
  name={name}
352
  value={edge.properties[name]}
353
- entityId={edge.id}
 
354
  entityType="edge"
355
- sourceId={edge.source}
356
- targetId={edge.target}
357
  isEditable={name === 'description' || name === 'keywords'}
358
  />
359
  )
 
16
  const focusedNode = useGraphStore.use.focusedNode()
17
  const selectedEdge = useGraphStore.use.selectedEdge()
18
  const focusedEdge = useGraphStore.use.focusedEdge()
19
+ const graphDataVersion = useGraphStore.use.graphDataVersion()
20
 
21
  const [currentElement, setCurrentElement] = useState<NodeType | EdgeType | null>(null)
22
  const [currentType, setCurrentType] = useState<'node' | 'edge' | null>(null)
23
 
24
+ // This effect will run when selection changes or when graph data is updated
25
  useEffect(() => {
26
  let type: 'node' | 'edge' | null = null
27
  let element: RawNodeType | RawEdgeType | null = null
 
55
  selectedNode,
56
  focusedEdge,
57
  selectedEdge,
58
+ graphDataVersion, // Add dependency on graphDataVersion to refresh when data changes
59
  setCurrentElement,
60
  setCurrentType,
61
  getNode,
 
96
  if (state.sigmaGraph && state.rawGraph) {
97
  try {
98
  if (!state.sigmaGraph.hasNode(node.id)) {
99
+ console.warn('Node not found in sigmaGraph:', node.id)
100
  return {
101
  ...node,
102
  relationships: []
 
143
 
144
  if (state.sigmaGraph && state.rawGraph) {
145
  try {
146
+ if (!state.sigmaGraph.hasEdge(edge.dynamicId)) {
147
+ console.warn('Edge not found in sigmaGraph:', edge.id, 'dynamicId:', edge.dynamicId)
148
  return {
149
  ...edge,
150
  sourceNode: undefined,
 
176
  value,
177
  onClick,
178
  tooltip,
179
+ nodeId,
180
+ edgeId,
181
+ dynamicId,
182
  entityId,
183
  entityType,
184
  sourceId,
 
189
  value: any
190
  onClick?: () => void
191
  tooltip?: string
192
+ nodeId?: string
193
  entityId?: string
194
+ edgeId?: string
195
+ dynamicId?: string
196
  entityType?: 'node' | 'edge'
197
  sourceId?: string
198
  targetId?: string
 
213
  name={name}
214
  value={value}
215
  onClick={onClick}
216
+ nodeId={nodeId}
217
  entityId={entityId}
218
+ edgeId={edgeId}
219
+ dynamicId={dynamicId}
220
  entityType={entityType}
221
  sourceId={sourceId}
222
  targetId={targetId}
 
279
  </div>
280
  </div>
281
  <div className="bg-primary/5 max-h-96 overflow-auto rounded p-1">
282
+ <PropertyRow name={t('graphPanel.propertiesView.node.id')} value={String(node.id)} />
283
  <PropertyRow
284
  name={t('graphPanel.propertiesView.node.labels')}
285
  value={node.labels.join(', ')}
 
299
  key={name}
300
  name={name}
301
  value={node.properties[name]}
302
+ nodeId={String(node.id)}
303
+ entityId={node.properties['entity_id']}
304
  entityType="node"
305
  isEditable={name === 'description' || name === 'entity_id'}
306
  />
 
365
  key={name}
366
  name={name}
367
  value={edge.properties[name]}
368
+ edgeId={String(edge.id)}
369
+ dynamicId={String(edge.dynamicId)}
370
  entityType="edge"
371
+ sourceId={edge.sourceNode?.properties['entity_id'] || edge.source}
372
+ targetId={edge.targetNode?.properties['entity_id'] || edge.target}
373
  isEditable={name === 'description' || name === 'keywords'}
374
  />
375
  )
lightrag_webui/src/stores/graph.ts CHANGED
@@ -5,6 +5,8 @@ import { getGraphLabels } from '@/api/lightrag'
5
  import MiniSearch from 'minisearch'
6
 
7
  export type RawNodeType = {
 
 
8
  id: string
9
  labels: string[]
10
  properties: Record<string, any>
@@ -18,20 +20,34 @@ export type RawNodeType = {
18
  }
19
 
20
  export type RawEdgeType = {
 
 
21
  id: string
22
  source: string
23
  target: string
24
  type?: string
25
  properties: Record<string, any>
26
-
27
  dynamicId: string
28
  }
29
 
 
 
 
 
 
 
 
 
 
30
  export class RawGraph {
31
  nodes: RawNodeType[] = []
32
  edges: RawEdgeType[] = []
 
33
  nodeIdMap: Record<string, number> = {}
 
34
  edgeIdMap: Record<string, number> = {}
 
35
  edgeDynamicIdMap: Record<string, number> = {}
36
 
37
  getNode = (nodeId: string) => {
@@ -120,9 +136,13 @@ interface GraphState {
120
  // Version counter to trigger data refresh
121
  graphDataVersion: number
122
  incrementGraphDataVersion: () => void
 
 
 
 
123
  }
124
 
125
- const useGraphStoreBase = create<GraphState>()((set) => ({
126
  selectedNode: null,
127
  focusedNode: null,
128
  selectedEdge: null,
@@ -227,6 +247,140 @@ const useGraphStoreBase = create<GraphState>()((set) => ({
227
  graphDataVersion: 0,
228
  incrementGraphDataVersion: () => set((state) => ({ graphDataVersion: state.graphDataVersion + 1 })),
229
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
230
  }))
231
 
232
  const useGraphStore = createSelectors(useGraphStoreBase)
 
5
  import MiniSearch from 'minisearch'
6
 
7
  export type RawNodeType = {
8
+ // for NetworkX: id is identical to properties['entity_id']
9
+ // for Neo4j: id is unique identifier for each node
10
  id: string
11
  labels: string[]
12
  properties: Record<string, any>
 
20
  }
21
 
22
  export type RawEdgeType = {
23
+ // for NetworkX: id is "source-target"
24
+ // for Neo4j: id is unique identifier for each edge
25
  id: string
26
  source: string
27
  target: string
28
  type?: string
29
  properties: Record<string, any>
30
+ // dynamicId: key for sigmaGraph
31
  dynamicId: string
32
  }
33
 
34
+ /**
35
+ * Interface for tracking edges that need updating when a node ID changes
36
+ */
37
+ interface EdgeToUpdate {
38
+ originalDynamicId: string
39
+ newEdgeId: string
40
+ edgeIndex: number
41
+ }
42
+
43
  export class RawGraph {
44
  nodes: RawNodeType[] = []
45
  edges: RawEdgeType[] = []
46
+ // nodeIDMap: map node id to index in nodes array (SigmaGraph has nodeId as key)
47
  nodeIdMap: Record<string, number> = {}
48
+ // edgeIDMap: map edge id to index in edges array (SigmaGraph not use id as key)
49
  edgeIdMap: Record<string, number> = {}
50
+ // edgeDynamicIdMap: map edge dynamic id to index in edges array (SigmaGraph has DynamicId as key)
51
  edgeDynamicIdMap: Record<string, number> = {}
52
 
53
  getNode = (nodeId: string) => {
 
136
  // Version counter to trigger data refresh
137
  graphDataVersion: number
138
  incrementGraphDataVersion: () => void
139
+
140
+ // Methods for updating graph elements and UI state together
141
+ updateNodeAndSelect: (nodeId: string, entityId: string, propertyName: string, newValue: string) => Promise<void>
142
+ updateEdgeAndSelect: (edgeId: string, dynamicId: string, sourceId: string, targetId: string, propertyName: string, newValue: string) => Promise<void>
143
  }
144
 
145
+ const useGraphStoreBase = create<GraphState>()((set, get) => ({
146
  selectedNode: null,
147
  focusedNode: null,
148
  selectedEdge: null,
 
247
  graphDataVersion: 0,
248
  incrementGraphDataVersion: () => set((state) => ({ graphDataVersion: state.graphDataVersion + 1 })),
249
 
250
+ // Methods for updating graph elements and UI state together
251
+ updateNodeAndSelect: async (nodeId: string, entityId: string, propertyName: string, newValue: string) => {
252
+ // Get current state
253
+ const state = get()
254
+ const { sigmaGraph, rawGraph } = state
255
+
256
+ // Validate graph state
257
+ if (!sigmaGraph || !rawGraph || !sigmaGraph.hasNode(nodeId)) {
258
+ return
259
+ }
260
+
261
+ try {
262
+ const nodeAttributes = sigmaGraph.getNodeAttributes(nodeId)
263
+
264
+ console.log('updateNodeAndSelect', nodeId, entityId, propertyName, newValue)
265
+
266
+ // For entity_id changes (node renaming) with NetworkX graph storage
267
+ if ((nodeId === entityId) && (propertyName === 'entity_id')) {
268
+ // Create new node with updated ID but same attributes
269
+ sigmaGraph.addNode(newValue, { ...nodeAttributes, label: newValue })
270
+
271
+ const edgesToUpdate: EdgeToUpdate[] = []
272
+
273
+ // Process all edges connected to this node
274
+ sigmaGraph.forEachEdge(nodeId, (edge, attributes, source, target) => {
275
+ const otherNode = source === nodeId ? target : source
276
+ const isOutgoing = source === nodeId
277
+
278
+ // Get original edge dynamic ID for later reference
279
+ const originalEdgeDynamicId = edge
280
+ const edgeIndexInRawGraph = rawGraph.edgeDynamicIdMap[originalEdgeDynamicId]
281
+
282
+ // Create new edge with updated node reference
283
+ const newEdgeId = sigmaGraph.addEdge(
284
+ isOutgoing ? newValue : otherNode,
285
+ isOutgoing ? otherNode : newValue,
286
+ attributes
287
+ )
288
+
289
+ // Track edges that need updating in the raw graph
290
+ if (edgeIndexInRawGraph !== undefined) {
291
+ edgesToUpdate.push({
292
+ originalDynamicId: originalEdgeDynamicId,
293
+ newEdgeId: newEdgeId,
294
+ edgeIndex: edgeIndexInRawGraph
295
+ })
296
+ }
297
+
298
+ // Remove the old edge
299
+ sigmaGraph.dropEdge(edge)
300
+ })
301
+
302
+ // Remove the old node after all edges are processed
303
+ sigmaGraph.dropNode(nodeId)
304
+
305
+ // Update node reference in raw graph data
306
+ const nodeIndex = rawGraph.nodeIdMap[nodeId]
307
+ if (nodeIndex !== undefined) {
308
+ rawGraph.nodes[nodeIndex].id = newValue
309
+ rawGraph.nodes[nodeIndex].labels = [newValue]
310
+ rawGraph.nodes[nodeIndex].properties.entity_id = newValue
311
+ delete rawGraph.nodeIdMap[nodeId]
312
+ rawGraph.nodeIdMap[newValue] = nodeIndex
313
+ }
314
+
315
+ // Update all edge references in raw graph data
316
+ edgesToUpdate.forEach(({ originalDynamicId, newEdgeId, edgeIndex }) => {
317
+ if (rawGraph.edges[edgeIndex]) {
318
+ // Update source/target references
319
+ if (rawGraph.edges[edgeIndex].source === nodeId) {
320
+ rawGraph.edges[edgeIndex].source = newValue
321
+ }
322
+ if (rawGraph.edges[edgeIndex].target === nodeId) {
323
+ rawGraph.edges[edgeIndex].target = newValue
324
+ }
325
+
326
+ // Update dynamic ID mappings
327
+ rawGraph.edges[edgeIndex].dynamicId = newEdgeId
328
+ delete rawGraph.edgeDynamicIdMap[originalDynamicId]
329
+ rawGraph.edgeDynamicIdMap[newEdgeId] = edgeIndex
330
+ }
331
+ })
332
+
333
+ // Update selected node in store
334
+ set({ selectedNode: newValue, moveToSelectedNode: true })
335
+ } else {
336
+ // For non-NetworkX nodes or non-entity_id changes
337
+ const nodeIndex = rawGraph.nodeIdMap[String(nodeId)]
338
+ if (nodeIndex !== undefined) {
339
+ rawGraph.nodes[nodeIndex].properties[propertyName] = newValue
340
+ if (propertyName === 'entity_id') {
341
+ rawGraph.nodes[nodeIndex].labels = [newValue]
342
+ sigmaGraph.setNodeAttribute(String(nodeId), 'label', newValue)
343
+ }
344
+ }
345
+
346
+ // Trigger a re-render by incrementing the version counter
347
+ set((state) => ({ graphDataVersion: state.graphDataVersion + 1 }))
348
+ }
349
+ } catch (error) {
350
+ console.error('Error updating node in graph:', error)
351
+ throw new Error('Failed to update node in graph')
352
+ }
353
+ },
354
+
355
+ updateEdgeAndSelect: async (edgeId: string, dynamicId: string, sourceId: string, targetId: string, propertyName: string, newValue: string) => {
356
+ // Get current state
357
+ const state = get()
358
+ const { sigmaGraph, rawGraph } = state
359
+
360
+ // Validate graph state
361
+ if (!sigmaGraph || !rawGraph) {
362
+ return
363
+ }
364
+
365
+ try {
366
+ const edgeIndex = rawGraph.edgeIdMap[String(edgeId)]
367
+ if (edgeIndex !== undefined && rawGraph.edges[edgeIndex]) {
368
+ rawGraph.edges[edgeIndex].properties[propertyName] = newValue
369
+ if(dynamicId !== undefined && propertyName === 'keywords') {
370
+ sigmaGraph.setEdgeAttribute(dynamicId, 'label', newValue)
371
+ }
372
+ }
373
+
374
+ // Trigger a re-render by incrementing the version counter
375
+ set((state) => ({ graphDataVersion: state.graphDataVersion + 1 }))
376
+
377
+ // Update selected edge in store to ensure UI reflects changes
378
+ set({ selectedEdge: dynamicId })
379
+ } catch (error) {
380
+ console.error(`Error updating edge ${sourceId}->${targetId} in graph:`, error)
381
+ throw new Error('Failed to update edge in graph')
382
+ }
383
+ }
384
  }))
385
 
386
  const useGraphStore = createSelectors(useGraphStoreBase)
lightrag_webui/src/utils/graphOperations.ts DELETED
@@ -1,175 +0,0 @@
1
- import { useGraphStore } from '@/stores/graph'
2
-
3
- /**
4
- * Interface for tracking edges that need updating when a node ID changes
5
- */
6
- interface EdgeToUpdate {
7
- originalDynamicId: string
8
- newEdgeId: string
9
- edgeIndex: number
10
- }
11
-
12
- /**
13
- * Update node in the graph visualization
14
- * Handles both property updates and entity ID changes
15
- *
16
- * @param nodeId - ID of the node to update
17
- * @param propertyName - Name of the property being updated
18
- * @param newValue - New value for the property
19
- */
20
- export const updateGraphNode = async (nodeId: string, propertyName: string, newValue: string) => {
21
- // Get graph state from store
22
- const sigmaGraph = useGraphStore.getState().sigmaGraph
23
- const rawGraph = useGraphStore.getState().rawGraph
24
-
25
- // Validate graph state
26
- if (!sigmaGraph || !rawGraph || !sigmaGraph.hasNode(String(nodeId))) {
27
- return
28
- }
29
-
30
- try {
31
- const nodeAttributes = sigmaGraph.getNodeAttributes(String(nodeId))
32
-
33
- // Special handling for entity_id changes (node renaming)
34
- if (propertyName === 'entity_id') {
35
- // Create new node with updated ID but same attributes
36
- sigmaGraph.addNode(newValue, { ...nodeAttributes, label: newValue })
37
-
38
- const edgesToUpdate: EdgeToUpdate[] = []
39
-
40
- // Process all edges connected to this node
41
- sigmaGraph.forEachEdge(String(nodeId), (edge, attributes, source, target) => {
42
- const otherNode = source === String(nodeId) ? target : source
43
- const isOutgoing = source === String(nodeId)
44
-
45
- // Get original edge dynamic ID for later reference
46
- const originalEdgeDynamicId = edge
47
- const edgeIndexInRawGraph = rawGraph.edgeDynamicIdMap[originalEdgeDynamicId]
48
-
49
- // Create new edge with updated node reference
50
- const newEdgeId = sigmaGraph.addEdge(
51
- isOutgoing ? newValue : otherNode,
52
- isOutgoing ? otherNode : newValue,
53
- attributes
54
- )
55
-
56
- // Track edges that need updating in the raw graph
57
- if (edgeIndexInRawGraph !== undefined) {
58
- edgesToUpdate.push({
59
- originalDynamicId: originalEdgeDynamicId,
60
- newEdgeId: newEdgeId,
61
- edgeIndex: edgeIndexInRawGraph
62
- })
63
- }
64
-
65
- // Remove the old edge
66
- sigmaGraph.dropEdge(edge)
67
- })
68
-
69
- // Remove the old node after all edges are processed
70
- sigmaGraph.dropNode(String(nodeId))
71
-
72
- // Update node reference in raw graph data
73
- const nodeIndex = rawGraph.nodeIdMap[String(nodeId)]
74
- if (nodeIndex !== undefined) {
75
- rawGraph.nodes[nodeIndex].id = newValue
76
- rawGraph.nodes[nodeIndex].properties.entity_id = newValue
77
- delete rawGraph.nodeIdMap[String(nodeId)]
78
- rawGraph.nodeIdMap[newValue] = nodeIndex
79
- }
80
-
81
- // Update all edge references in raw graph data
82
- edgesToUpdate.forEach(({ originalDynamicId, newEdgeId, edgeIndex }) => {
83
- if (rawGraph.edges[edgeIndex]) {
84
- // Update source/target references
85
- if (rawGraph.edges[edgeIndex].source === String(nodeId)) {
86
- rawGraph.edges[edgeIndex].source = newValue
87
- }
88
- if (rawGraph.edges[edgeIndex].target === String(nodeId)) {
89
- rawGraph.edges[edgeIndex].target = newValue
90
- }
91
-
92
- // Update dynamic ID mappings
93
- rawGraph.edges[edgeIndex].dynamicId = newEdgeId
94
- delete rawGraph.edgeDynamicIdMap[originalDynamicId]
95
- rawGraph.edgeDynamicIdMap[newEdgeId] = edgeIndex
96
- }
97
- })
98
-
99
- // Update selected node in store
100
- useGraphStore.getState().setSelectedNode(newValue)
101
- } else {
102
- // For other properties, just update the property in raw graph
103
- const nodeIndex = rawGraph.nodeIdMap[String(nodeId)]
104
- if (nodeIndex !== undefined) {
105
- rawGraph.nodes[nodeIndex].properties[propertyName] = newValue
106
- }
107
- }
108
- } catch (error) {
109
- console.error('Error updating node in graph:', error)
110
- throw new Error('Failed to update node in graph')
111
- }
112
- }
113
-
114
- /**
115
- * Update edge in the graph visualization
116
- *
117
- * @param sourceId - ID of the source node
118
- * @param targetId - ID of the target node
119
- * @param propertyName - Name of the property being updated
120
- * @param newValue - New value for the property
121
- */
122
- export const updateGraphEdge = async (sourceId: string, targetId: string, propertyName: string, newValue: string) => {
123
- // Get graph state from store
124
- const sigmaGraph = useGraphStore.getState().sigmaGraph
125
- const rawGraph = useGraphStore.getState().rawGraph
126
-
127
- // Validate graph state
128
- if (!sigmaGraph || !rawGraph) {
129
- return
130
- }
131
-
132
- try {
133
- // Find the edge between source and target nodes
134
- const allEdges = sigmaGraph.edges()
135
- let keyToUse = null
136
-
137
- for (const edge of allEdges) {
138
- const edgeSource = sigmaGraph.source(edge)
139
- const edgeTarget = sigmaGraph.target(edge)
140
-
141
- // Match edge in either direction (undirected graph support)
142
- if ((edgeSource === sourceId && edgeTarget === targetId) ||
143
- (edgeSource === targetId && edgeTarget === sourceId)) {
144
- keyToUse = edge
145
- break
146
- }
147
- }
148
-
149
- if (keyToUse !== null) {
150
- // Special handling for keywords property (updates edge label)
151
- if(propertyName === 'keywords') {
152
- sigmaGraph.setEdgeAttribute(keyToUse, 'label', newValue)
153
- } else {
154
- sigmaGraph.setEdgeAttribute(keyToUse, propertyName, newValue)
155
- }
156
-
157
- // Update edge in raw graph data using dynamic ID mapping
158
- if (keyToUse && rawGraph.edgeDynamicIdMap[keyToUse] !== undefined) {
159
- const edgeIndex = rawGraph.edgeDynamicIdMap[keyToUse]
160
- if (rawGraph.edges[edgeIndex]) {
161
- rawGraph.edges[edgeIndex].properties[propertyName] = newValue
162
- }
163
- } else if (keyToUse !== null) {
164
- // Fallback: try to find edge by key in edge ID map
165
- const edgeIndexByKey = rawGraph.edgeIdMap[keyToUse]
166
- if (edgeIndexByKey !== undefined && rawGraph.edges[edgeIndexByKey]) {
167
- rawGraph.edges[edgeIndexByKey].properties[propertyName] = newValue
168
- }
169
- }
170
- }
171
- } catch (error) {
172
- console.error(`Error updating edge ${sourceId}->${targetId} in graph:`, error)
173
- throw new Error('Failed to update edge in graph')
174
- }
175
- }