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
Problem
src/features/mindmap/(usingmind-elixir) andsrc/features/graph/(usinggraphology) 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
No shared state, no event bus, no synchronization mechanism.
Proposed Implementation
1. Shared graph node type
2. Zustand sync slice
3. Mind-elixir adapter
4. Graphology adapter
5. UI toggle
Add a "Sync" toggle button to both the MindMap and Graph toolbars:
When enabled, show a visual indicator (e.g. pulsing dot) on both views.
Conflict Resolution
idas the shared key between mind-elixir nodes and graphology nodesAcceptance Criteria
GraphSyncStoreZustand slice implementedconsumeEvents)