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 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 // dynamicId: key for sigmaGraph dynamicId: string } export class RawGraph { nodes: RawNodeType[] = [] edges: RawEdgeType[] = [] // nodeIDMap: map node id to index in nodes array (SigmaGraph has nodeId as key) nodeIdMap: Record = {} // edgeIDMap: map edge id to index in edges array (SigmaGraph not use id as key) edgeIdMap: Record = {} // edgeDynamicIdMap: map edge dynamic id to index in edges array (SigmaGraph has DynamicId as key) edgeDynamicIdMap: Record = {} 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 // 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 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 } const useGraphStoreBase = create()((set) => ({ 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(), 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) => 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 })), })) const useGraphStore = createSelectors(useGraphStoreBase) export { useGraphStore }