File size: 12,965 Bytes
376f172
 
 
dcae38a
a5f80ff
376f172
 
f4c3104
 
376f172
 
 
 
 
 
 
 
 
 
 
 
 
f4c3104
 
376f172
 
 
858f35e
376f172
f4c3104
376f172
 
 
b6a03c6
 
 
 
 
 
 
 
 
376f172
 
 
f4c3104
376f172
f4c3104
376f172
f4c3104
376f172
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e41c826
dcae38a
376f172
a5f80ff
 
376f172
3ab8571
f57fcd2
 
2e179f3
563cdd1
 
5611aed
 
 
376f172
e41c826
376f172
 
 
 
 
 
 
 
f57fcd2
 
376f172
 
 
dcae38a
 
3ab8571
2e179f3
a5f80ff
 
 
 
5611aed
 
 
37bba66
b8f3341
 
 
37bba66
b8f3341
 
 
fa48cea
 
 
 
2ea23b2
b6a03c6
 
 
376f172
 
b6a03c6
376f172
 
 
 
 
 
3ab8571
f57fcd2
 
2e179f3
5611aed
 
 
376f172
 
 
e41c826
dcae38a
376f172
563cdd1
 
a5f80ff
 
f57fcd2
 
 
f566759
3ab8571
376f172
 
 
 
 
 
 
 
 
 
 
 
d3a437a
376f172
 
 
 
 
 
88fc1cb
dc332df
f57fcd2
 
d3a437a
 
376f172
 
 
 
 
 
d3a437a
9defefd
d3a437a
 
201c775
dcae38a
 
 
 
5611aed
dcae38a
 
5611aed
dcae38a
 
 
5611aed
dcae38a
 
 
5611aed
169a4c0
e41c826
2e179f3
563cdd1
 
a5f80ff
 
 
5611aed
 
b8f3341
37bba66
b8f3341
 
 
37bba66
b8f3341
 
 
37bba66
fa48cea
 
 
 
b6a03c6
 
 
 
 
2ea23b2
b6a03c6
 
 
 
2ea23b2
b6a03c6
 
2ea23b2
b6a03c6
2ea23b2
b6a03c6
 
 
 
2ea23b2
b6a03c6
2ea23b2
b6a03c6
 
 
 
2ea23b2
b6a03c6
 
 
2ea23b2
b6a03c6
 
 
 
 
 
2ea23b2
b6a03c6
 
 
 
 
 
 
 
2ea23b2
b6a03c6
 
 
2ea23b2
b6a03c6
 
2ea23b2
b6a03c6
 
 
 
 
 
 
 
 
2ea23b2
b6a03c6
 
 
 
 
 
 
 
 
 
2ea23b2
b6a03c6
 
 
 
 
 
2ea23b2
b6a03c6
 
 
 
 
 
 
 
 
 
 
 
2ea23b2
b6a03c6
 
 
 
 
 
 
 
2ea23b2
b6a03c6
 
 
 
2ea23b2
b6a03c6
 
 
 
2ea23b2
b6a03c6
 
 
 
 
 
 
 
2ea23b2
b6a03c6
 
2ea23b2
b6a03c6
 
 
 
 
 
 
376f172
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
import { create } from 'zustand'
import { createSelectors } from '@/lib/utils'
import { DirectedGraph } from 'graphology'
import { getGraphLabels } from '@/api/lightrag'
import MiniSearch from 'minisearch'

export type RawNodeType = {
  // for NetworkX: id is identical to properties['entity_id']
  // for Neo4j: id is unique identifier for each node
  id: string
  labels: string[]
  properties: Record<string, any>

  size: number
  x: number
  y: number
  color: string

  degree: number
}

export type RawEdgeType = {
  // for NetworkX: id is "source-target"
  // for Neo4j: id is unique identifier for each edge
  id: string
  source: string
  target: string
  type?: string
  properties: Record<string, any>
  // dynamicId: key for sigmaGraph
  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[] = []
  // nodeIDMap: map node id to index in nodes array (SigmaGraph has nodeId as key)
  nodeIdMap: Record<string, number> = {}
  // edgeIDMap: map edge id to index in edges array (SigmaGraph not use id as key)
  edgeIdMap: Record<string, number> = {}
  // edgeDynamicIdMap: map edge dynamic id to index in edges array (SigmaGraph has DynamicId as key)
  edgeDynamicIdMap: Record<string, number> = {}

  getNode = (nodeId: string) => {
    const nodeIndex = this.nodeIdMap[nodeId]
    if (nodeIndex !== undefined) {
      return this.nodes[nodeIndex]
    }
    return undefined
  }

  getEdge = (edgeId: string, dynamicId: boolean = true) => {
    const edgeIndex = dynamicId ? this.edgeDynamicIdMap[edgeId] : this.edgeIdMap[edgeId]
    if (edgeIndex !== undefined) {
      return this.edges[edgeIndex]
    }
    return undefined
  }

  buildDynamicMap = () => {
    this.edgeDynamicIdMap = {}
    for (let i = 0; i < this.edges.length; i++) {
      const edge = this.edges[i]
      this.edgeDynamicIdMap[edge.dynamicId] = i
    }
  }
}

interface GraphState {
  selectedNode: string | null
  focusedNode: string | null
  selectedEdge: string | null
  focusedEdge: string | null

  rawGraph: RawGraph | null
  sigmaGraph: DirectedGraph | null
  sigmaInstance: any | null
  allDatabaseLabels: string[]

  searchEngine: MiniSearch | null

  moveToSelectedNode: boolean
  isFetching: boolean
  graphIsEmpty: boolean
  lastSuccessfulQueryLabel: string

  typeColorMap: Map<string, string>

  // Global flags to track data fetching attempts
  graphDataFetchAttempted: boolean
  labelsFetchAttempted: boolean

  setSigmaInstance: (instance: any) => void
  setSelectedNode: (nodeId: string | null, moveToSelectedNode?: boolean) => void
  setFocusedNode: (nodeId: string | null) => void
  setSelectedEdge: (edgeId: string | null) => void
  setFocusedEdge: (edgeId: string | null) => void
  clearSelection: () => void
  reset: () => void

  setMoveToSelectedNode: (moveToSelectedNode: boolean) => void
  setGraphIsEmpty: (isEmpty: boolean) => void
  setLastSuccessfulQueryLabel: (label: string) => void

  setRawGraph: (rawGraph: RawGraph | null) => void
  setSigmaGraph: (sigmaGraph: DirectedGraph | null) => void
  setAllDatabaseLabels: (labels: string[]) => void
  fetchAllDatabaseLabels: () => Promise<void>
  setIsFetching: (isFetching: boolean) => void

  // 搜索引擎方法
  setSearchEngine: (engine: MiniSearch | null) => void
  resetSearchEngine: () => void

  // Methods to set global flags
  setGraphDataFetchAttempted: (attempted: boolean) => void
  setLabelsFetchAttempted: (attempted: boolean) => void

  // Event trigger methods for node operations
  triggerNodeExpand: (nodeId: string | null) => void
  triggerNodePrune: (nodeId: string | null) => void

  // Node operation state
  nodeToExpand: string | null
  nodeToPrune: string | null

  // 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<void>
  updateEdgeAndSelect: (edgeId: string, dynamicId: string, sourceId: string, targetId: string, propertyName: string, newValue: string) => Promise<void>
}

const useGraphStoreBase = create<GraphState>()((set, get) => ({
  selectedNode: null,
  focusedNode: null,
  selectedEdge: null,
  focusedEdge: null,

  moveToSelectedNode: false,
  isFetching: false,
  graphIsEmpty: false,
  lastSuccessfulQueryLabel: '', // Initialize as empty to ensure fetchAllDatabaseLabels runs on first query

  // Initialize global flags
  graphDataFetchAttempted: false,
  labelsFetchAttempted: false,

  rawGraph: null,
  sigmaGraph: null,
  sigmaInstance: null,
  allDatabaseLabels: ['*'],

  typeColorMap: new Map<string, string>(),

  searchEngine: null,

  setGraphIsEmpty: (isEmpty: boolean) => set({ graphIsEmpty: isEmpty }),
  setLastSuccessfulQueryLabel: (label: string) => set({ lastSuccessfulQueryLabel: label }),


  setIsFetching: (isFetching: boolean) => set({ isFetching }),
  setSelectedNode: (nodeId: string | null, moveToSelectedNode?: boolean) =>
    set({ selectedNode: nodeId, moveToSelectedNode }),
  setFocusedNode: (nodeId: string | null) => set({ focusedNode: nodeId }),
  setSelectedEdge: (edgeId: string | null) => set({ selectedEdge: edgeId }),
  setFocusedEdge: (edgeId: string | null) => set({ focusedEdge: edgeId }),
  clearSelection: () =>
    set({
      selectedNode: null,
      focusedNode: null,
      selectedEdge: null,
      focusedEdge: null
    }),
  reset: () => {
    set({
      selectedNode: null,
      focusedNode: null,
      selectedEdge: null,
      focusedEdge: null,
      rawGraph: null,
      sigmaGraph: null,  // to avoid other components from acccessing graph objects
      searchEngine: null,
      moveToSelectedNode: false,
      graphIsEmpty: false
    });
  },

  setRawGraph: (rawGraph: RawGraph | null) =>
    set({
      rawGraph
    }),

  setSigmaGraph: (sigmaGraph: DirectedGraph | null) => {
    // Replace graph instance, no need to keep WebGL context
    set({ sigmaGraph });
  },

  setAllDatabaseLabels: (labels: string[]) => set({ allDatabaseLabels: labels }),

  fetchAllDatabaseLabels: async () => {
    try {
      console.log('Fetching all database labels...');
      const labels = await getGraphLabels();
      set({ allDatabaseLabels: ['*', ...labels] });
      return;
    } catch (error) {
      console.error('Failed to fetch all database labels:', error);
      set({ allDatabaseLabels: ['*'] });
      throw error;
    }
  },

  setMoveToSelectedNode: (moveToSelectedNode?: boolean) => set({ moveToSelectedNode }),

  setSigmaInstance: (instance: any) => set({ sigmaInstance: instance }),

  setTypeColorMap: (typeColorMap: Map<string, string>) => set({ typeColorMap }),

  setSearchEngine: (engine: MiniSearch | null) => set({ searchEngine: engine }),
  resetSearchEngine: () => set({ searchEngine: null }),

  // Methods to set global flags
  setGraphDataFetchAttempted: (attempted: boolean) => set({ graphDataFetchAttempted: attempted }),
  setLabelsFetchAttempted: (attempted: boolean) => set({ labelsFetchAttempted: attempted }),

  // Node operation state
  nodeToExpand: null,
  nodeToPrune: null,

  // Event trigger methods for node operations
  triggerNodeExpand: (nodeId: string | null) => set({ nodeToExpand: nodeId }),
  triggerNodePrune: (nodeId: string | null) => set({ nodeToPrune: nodeId }),

  // Version counter implementation
  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)

export { useGraphStore }