| import { create } from 'zustand' |
| import { createSelectors } from '@/lib/utils' |
| import { DirectedGraph } from 'graphology' |
| import MiniSearch from 'minisearch' |
| import { resolveNodeColor, DEFAULT_NODE_COLOR } from '@/utils/graphColor' |
|
|
| const createErrorWithCause = (message: string, cause: unknown): Error => { |
| const error = new Error(message) as Error & { cause?: unknown } |
| error.cause = cause |
| return error |
| } |
|
|
| export type RawNodeType = { |
| |
| |
| id: string |
| labels: string[] |
| properties: Record<string, any> |
|
|
| size: number |
| x: number |
| y: number |
| color: string |
|
|
| degree: number |
| } |
|
|
| export type RawEdgeType = { |
| |
| |
| id: string |
| source: string |
| target: string |
| type?: string |
| properties: Record<string, any> |
| |
| dynamicId: string |
| } |
|
|
| |
| |
| |
| interface EdgeToUpdate { |
| originalDynamicId: string |
| newEdgeId: string |
| edgeIndex: number |
| } |
|
|
| export class RawGraph { |
| nodes: RawNodeType[] = [] |
| edges: RawEdgeType[] = [] |
| |
| nodeIdMap: Record<string, number> = {} |
| |
| edgeIdMap: Record<string, number> = {} |
| |
| 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 |
|
|
| searchEngine: MiniSearch | null |
|
|
| moveToSelectedNode: boolean |
| isFetching: boolean |
| graphIsEmpty: boolean |
| lastSuccessfulQueryLabel: string |
|
|
| typeColorMap: Map<string, string> |
|
|
| |
| 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 |
| setIsFetching: (isFetching: boolean) => void |
|
|
| |
| setTypeColorMap: (typeColorMap: Map<string, string>) => void |
|
|
| |
| setSearchEngine: (engine: MiniSearch | null) => void |
| resetSearchEngine: () => void |
|
|
| |
| setGraphDataFetchAttempted: (attempted: boolean) => void |
| setLabelsFetchAttempted: (attempted: boolean) => void |
|
|
| |
| triggerNodeExpand: (nodeId: string | null) => void |
| triggerNodePrune: (nodeId: string | null) => void |
|
|
| |
| nodeToExpand: string | null |
| nodeToPrune: string | null |
|
|
| |
| graphDataVersion: number |
| incrementGraphDataVersion: () => void |
|
|
| |
| 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: '', |
|
|
| |
| graphDataFetchAttempted: false, |
| labelsFetchAttempted: false, |
|
|
| rawGraph: null, |
| sigmaGraph: null, |
| sigmaInstance: null, |
|
|
| 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, |
| searchEngine: null, |
| moveToSelectedNode: false, |
| graphIsEmpty: false |
| }); |
| }, |
|
|
| setRawGraph: (rawGraph: RawGraph | null) => |
| set({ |
| rawGraph |
| }), |
|
|
| setSigmaGraph: (sigmaGraph: DirectedGraph | null) => { |
| |
| set({ sigmaGraph }); |
| }, |
|
|
| 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 }), |
|
|
| |
| setGraphDataFetchAttempted: (attempted: boolean) => set({ graphDataFetchAttempted: attempted }), |
| setLabelsFetchAttempted: (attempted: boolean) => set({ labelsFetchAttempted: attempted }), |
|
|
| |
| nodeToExpand: null, |
| nodeToPrune: null, |
|
|
| |
| triggerNodeExpand: (nodeId: string | null) => set({ nodeToExpand: nodeId }), |
| triggerNodePrune: (nodeId: string | null) => set({ nodeToPrune: nodeId }), |
|
|
| |
| graphDataVersion: 0, |
| incrementGraphDataVersion: () => set((state) => ({ graphDataVersion: state.graphDataVersion + 1 })), |
|
|
| |
| updateNodeAndSelect: async (nodeId: string, entityId: string, propertyName: string, newValue: string) => { |
| |
| const state = get() |
| const { sigmaGraph, rawGraph } = state |
|
|
| |
| if (!sigmaGraph || !rawGraph || !sigmaGraph.hasNode(nodeId)) { |
| return |
| } |
|
|
| try { |
| const nodeAttributes = sigmaGraph.getNodeAttributes(nodeId) |
|
|
| console.log('updateNodeAndSelect', nodeId, entityId, propertyName, newValue) |
|
|
| |
| if ((nodeId === entityId) && (propertyName === 'entity_id')) { |
| |
| sigmaGraph.addNode(newValue, { ...nodeAttributes, label: newValue }) |
|
|
| const edgesToUpdate: EdgeToUpdate[] = [] |
|
|
| |
| sigmaGraph.forEachEdge(nodeId, (edge, attributes, source, target) => { |
| const otherNode = source === nodeId ? target : source |
| const isOutgoing = source === nodeId |
|
|
| |
| const originalEdgeDynamicId = edge |
| const edgeIndexInRawGraph = rawGraph.edgeDynamicIdMap[originalEdgeDynamicId] |
|
|
| |
| const newEdgeId = sigmaGraph.addEdge( |
| isOutgoing ? newValue : otherNode, |
| isOutgoing ? otherNode : newValue, |
| attributes |
| ) |
|
|
| |
| if (edgeIndexInRawGraph !== undefined) { |
| edgesToUpdate.push({ |
| originalDynamicId: originalEdgeDynamicId, |
| newEdgeId: newEdgeId, |
| edgeIndex: edgeIndexInRawGraph |
| }) |
| } |
|
|
| |
| sigmaGraph.dropEdge(edge) |
| }) |
|
|
| |
| sigmaGraph.dropNode(nodeId) |
|
|
| |
| 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 |
| } |
|
|
| |
| edgesToUpdate.forEach(({ originalDynamicId, newEdgeId, edgeIndex }) => { |
| if (rawGraph.edges[edgeIndex]) { |
| |
| if (rawGraph.edges[edgeIndex].source === nodeId) { |
| rawGraph.edges[edgeIndex].source = newValue |
| } |
| if (rawGraph.edges[edgeIndex].target === nodeId) { |
| rawGraph.edges[edgeIndex].target = newValue |
| } |
|
|
| |
| rawGraph.edges[edgeIndex].dynamicId = newEdgeId |
| delete rawGraph.edgeDynamicIdMap[originalDynamicId] |
| rawGraph.edgeDynamicIdMap[newEdgeId] = edgeIndex |
| } |
| }) |
|
|
| |
| set({ selectedNode: newValue, moveToSelectedNode: true }) |
| } else { |
| |
| const nodeIndex = rawGraph.nodeIdMap[String(nodeId)] |
| if (nodeIndex !== undefined) { |
| const nodeRef = rawGraph.nodes[nodeIndex] |
| nodeRef.properties[propertyName] = newValue |
| if (propertyName === 'entity_id') { |
| nodeRef.labels = [newValue] |
| sigmaGraph.setNodeAttribute(String(nodeId), 'label', newValue) |
| } |
| if (propertyName === 'entity_type') { |
| const { color, map, updated } = resolveNodeColor(newValue, state.typeColorMap) |
| const resolvedColor = color || DEFAULT_NODE_COLOR |
| nodeRef.color = resolvedColor |
| sigmaGraph.setNodeAttribute(String(nodeId), 'color', resolvedColor) |
| if (updated) { |
| set({ typeColorMap: map }) |
| } |
| } |
| } |
|
|
| |
| set((state) => ({ graphDataVersion: state.graphDataVersion + 1 })) |
| } |
| } catch (error) { |
| console.error('Error updating node in graph:', error) |
| throw createErrorWithCause('Failed to update node in graph', error) |
| } |
| }, |
|
|
| updateEdgeAndSelect: async (edgeId: string, dynamicId: string, sourceId: string, targetId: string, propertyName: string, newValue: string) => { |
| |
| const state = get() |
| const { sigmaGraph, rawGraph } = 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) |
| } |
| } |
|
|
| |
| set((state) => ({ graphDataVersion: state.graphDataVersion + 1 })) |
|
|
| |
| set({ selectedEdge: dynamicId }) |
| } catch (error) { |
| console.error(`Error updating edge ${sourceId}->${targetId} in graph:`, error) |
| throw createErrorWithCause('Failed to update edge in graph', error) |
| } |
| } |
| })) |
|
|
| const useGraphStore = createSelectors(useGraphStoreBase) |
|
|
| export { useGraphStore } |
|
|