Skip to content

feat: Mind map ↔ knowledge graph bidirectional sync via shared Zustand store #286

@d-oit

Description

@d-oit

Problem

src/features/mindmap/ (using mind-elixir) and src/features/graph/ (using graphology) operate as completely independent silos. Changes in one are not reflected in the other. A user creating a topic in the mind map cannot have it automatically become a node in the knowledge graph, and vice versa.

Current State

src/features/mindmap/     # mind-elixir based, independent state
src/features/graph/       # graphology based, independent state  

No shared state, no event bus, no synchronization mechanism.

Proposed Implementation

1. Shared graph node type

// src/store/graph-sync-types.ts
export interface SharedNode {
  id: string;
  label: string;
  type?: string;
  metadata?: Record<string, unknown>;
}

export interface SharedEdge {
  id: string;
  from: string;
  to: string;
  label?: string;
}

export interface GraphSyncEvent {
  type: 'node:add' | 'node:update' | 'node:remove' | 'edge:add' | 'edge:remove';
  source: 'mindmap' | 'graph';
  payload: SharedNode | SharedEdge;
}

2. Zustand sync slice

// src/store/graph-sync-store.ts
import { create } from 'zustand';

interface GraphSyncStore {
  syncEnabled: boolean;
  setSyncEnabled: (v: boolean) => void;
  pendingEvents: GraphSyncEvent[];
  emitEvent: (event: GraphSyncEvent) => void;
  consumeEvents: (target: 'mindmap' | 'graph') => GraphSyncEvent[];
}

export const useGraphSyncStore = create<GraphSyncStore>((set, get) => ({
  syncEnabled: false,
  setSyncEnabled: (v) => set({ syncEnabled: v }),
  pendingEvents: [],
  emitEvent: (event) =>
    set(s => ({ pendingEvents: [...s.pendingEvents, event] })),
  consumeEvents: (target) => {
    const events = get().pendingEvents.filter(e => e.source !== target);
    set({ pendingEvents: get().pendingEvents.filter(e => e.source === target) });
    return events;
  },
}));

3. Mind-elixir adapter

// src/features/mindmap/sync-adapter.ts
export function setupMindMapSyncListeners(
  mindMap: MindElixirInstance,
  store: GraphSyncStore
) {
  mindMap.bus.addListener('operation', (op) => {
    if (!store.syncEnabled) return;
    
    if (op.name === 'addChild' || op.name === 'addSibling') {
      store.emitEvent({
        type: 'node:add',
        source: 'mindmap',
        payload: { id: op.obj.id, label: op.obj.topic },
      });
    }
    if (op.name === 'removeNode') {
      store.emitEvent({ type: 'node:remove', source: 'mindmap', payload: { id: op.obj.id } });
    }
  });
}

4. Graphology adapter

// src/features/graph/sync-adapter.ts  
export function setupGraphSyncListeners(
  graph: Graph,
  store: GraphSyncStore
) {
  graph.on('nodeAdded', ({ key, attributes }) => {
    if (!store.syncEnabled) return;
    store.emitEvent({
      type: 'node:add',
      source: 'graph',
      payload: { id: key, label: attributes.label },
    });
  });
  graph.on('nodeDropped', ({ key }) => {
    if (!store.syncEnabled) return;
    store.emitEvent({ type: 'node:remove', source: 'graph', payload: { id: key, label: '' } });
  });
}

5. UI toggle

Add a "Sync" toggle button to both the MindMap and Graph toolbars:

<SyncToggle
  enabled={syncEnabled}
  onToggle={setSyncEnabled}
  tooltip="Sync mind map with knowledge graph"
/>

When enabled, show a visual indicator (e.g. pulsing dot) on both views.

Conflict Resolution

  • Use id as the shared key between mind-elixir nodes and graphology nodes
  • On conflict (same id, different label), last-writer wins
  • Log conflicts to browser console for debugging

Acceptance Criteria

  • GraphSyncStore Zustand slice implemented
  • Mind-elixir sync adapter emitting events on node add/remove/update
  • Graphology sync adapter consuming events and applying them
  • Bidirectional: graph changes also sync back to mind map
  • Sync on/off toggle in both UI toolbars
  • Infinite loop prevention (source filter in consumeEvents)
  • Unit tests for sync event emission and consumption
  • Integration test: add node in mindmap, verify it appears in graph

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions