@jungjaehoon/mama-os 0.8.3 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/agent/agent-loop.d.ts +1 -8
  3. package/dist/agent/agent-loop.d.ts.map +1 -1
  4. package/dist/agent/agent-loop.js +44 -159
  5. package/dist/agent/agent-loop.js.map +1 -1
  6. package/dist/agent/claude-cli-wrapper.d.ts +6 -0
  7. package/dist/agent/claude-cli-wrapper.d.ts.map +1 -1
  8. package/dist/agent/claude-cli-wrapper.js +6 -0
  9. package/dist/agent/claude-cli-wrapper.js.map +1 -1
  10. package/dist/agent/codex-mcp-process.d.ts +85 -0
  11. package/dist/agent/codex-mcp-process.d.ts.map +1 -0
  12. package/dist/agent/codex-mcp-process.js +357 -0
  13. package/dist/agent/codex-mcp-process.js.map +1 -0
  14. package/dist/agent/session-pool.d.ts +17 -2
  15. package/dist/agent/session-pool.d.ts.map +1 -1
  16. package/dist/agent/session-pool.js +51 -26
  17. package/dist/agent/session-pool.js.map +1 -1
  18. package/dist/agent/types.d.ts +9 -24
  19. package/dist/agent/types.d.ts.map +1 -1
  20. package/dist/agent/types.js.map +1 -1
  21. package/dist/api/graph-api.d.ts.map +1 -1
  22. package/dist/api/graph-api.js +133 -45
  23. package/dist/api/graph-api.js.map +1 -1
  24. package/dist/cli/commands/init.d.ts +1 -1
  25. package/dist/cli/commands/init.d.ts.map +1 -1
  26. package/dist/cli/commands/init.js +14 -25
  27. package/dist/cli/commands/init.js.map +1 -1
  28. package/dist/cli/commands/run.d.ts.map +1 -1
  29. package/dist/cli/commands/run.js +3 -10
  30. package/dist/cli/commands/run.js.map +1 -1
  31. package/dist/cli/commands/start.d.ts.map +1 -1
  32. package/dist/cli/commands/start.js +143 -54
  33. package/dist/cli/commands/start.js.map +1 -1
  34. package/dist/cli/commands/status.d.ts.map +1 -1
  35. package/dist/cli/commands/status.js +2 -7
  36. package/dist/cli/commands/status.js.map +1 -1
  37. package/dist/cli/config/config-manager.d.ts.map +1 -1
  38. package/dist/cli/config/config-manager.js +9 -17
  39. package/dist/cli/config/config-manager.js.map +1 -1
  40. package/dist/cli/config/types.d.ts +19 -25
  41. package/dist/cli/config/types.d.ts.map +1 -1
  42. package/dist/cli/config/types.js.map +1 -1
  43. package/dist/cli/index.js +2 -2
  44. package/dist/cli/index.js.map +1 -1
  45. package/dist/gateways/context-injector.d.ts.map +1 -1
  46. package/dist/gateways/context-injector.js +6 -3
  47. package/dist/gateways/context-injector.js.map +1 -1
  48. package/dist/gateways/discord.d.ts +4 -0
  49. package/dist/gateways/discord.d.ts.map +1 -1
  50. package/dist/gateways/discord.js +39 -16
  51. package/dist/gateways/discord.js.map +1 -1
  52. package/dist/gateways/message-router.d.ts +6 -1
  53. package/dist/gateways/message-router.d.ts.map +1 -1
  54. package/dist/gateways/message-router.js +92 -7
  55. package/dist/gateways/message-router.js.map +1 -1
  56. package/dist/multi-agent/agent-process-manager.d.ts.map +1 -1
  57. package/dist/multi-agent/agent-process-manager.js +36 -9
  58. package/dist/multi-agent/agent-process-manager.js.map +1 -1
  59. package/dist/multi-agent/runtime-process.d.ts +4 -4
  60. package/dist/multi-agent/runtime-process.d.ts.map +1 -1
  61. package/dist/multi-agent/runtime-process.js +9 -20
  62. package/dist/multi-agent/runtime-process.js.map +1 -1
  63. package/dist/multi-agent/types.d.ts +13 -8
  64. package/dist/multi-agent/types.d.ts.map +1 -1
  65. package/dist/multi-agent/types.js.map +1 -1
  66. package/dist/setup/setup-prompt.d.ts +1 -1
  67. package/dist/setup/setup-prompt.d.ts.map +1 -1
  68. package/dist/setup/setup-prompt.js +19 -0
  69. package/dist/setup/setup-prompt.js.map +1 -1
  70. package/dist/setup/setup-server.d.ts.map +1 -1
  71. package/dist/setup/setup-server.js +39 -16
  72. package/dist/setup/setup-server.js.map +1 -1
  73. package/dist/skills/skill-registry.d.ts.map +1 -1
  74. package/dist/skills/skill-registry.js +5 -2
  75. package/dist/skills/skill-registry.js.map +1 -1
  76. package/package.json +5 -3
  77. package/public/setup.html +12 -1
  78. package/public/viewer/js/modules/chat.js +1760 -1976
  79. package/public/viewer/js/modules/dashboard.js +613 -695
  80. package/public/viewer/js/modules/graph.js +857 -970
  81. package/public/viewer/js/modules/memory.js +357 -312
  82. package/public/viewer/js/modules/settings.js +1009 -1026
  83. package/public/viewer/js/modules/skills.js +336 -355
  84. package/public/viewer/js/utils/api.js +255 -255
  85. package/public/viewer/js/utils/debug-logger.js +20 -26
  86. package/public/viewer/js/utils/dom.js +73 -60
  87. package/public/viewer/js/utils/format.js +182 -228
  88. package/public/viewer/js/utils/markdown.js +40 -0
  89. package/public/viewer/src/modules/chat.ts +2258 -0
  90. package/public/viewer/src/modules/dashboard.ts +1052 -0
  91. package/public/viewer/src/modules/graph.ts +1080 -0
  92. package/public/viewer/src/modules/memory.ts +453 -0
  93. package/public/viewer/src/modules/settings.ts +1398 -0
  94. package/public/viewer/src/modules/skills.ts +457 -0
  95. package/public/viewer/src/types/global.d.ts +168 -0
  96. package/public/viewer/src/utils/api.ts +650 -0
  97. package/public/viewer/src/utils/debug-logger.ts +36 -0
  98. package/public/viewer/src/utils/dom.ts +138 -0
  99. package/public/viewer/src/utils/format.ts +331 -0
  100. package/public/viewer/src/utils/markdown.ts +46 -0
  101. package/public/viewer/tsconfig.viewer.json +18 -0
  102. package/public/viewer/viewer.html +214 -311
  103. package/dist/agent/codex-cli-wrapper.d.ts +0 -85
  104. package/dist/agent/codex-cli-wrapper.d.ts.map +0 -1
  105. package/dist/agent/codex-cli-wrapper.js +0 -295
  106. package/dist/agent/codex-cli-wrapper.js.map +0 -1
@@ -1,4 +1,3 @@
1
- /* global marked */
2
1
  /**
3
2
  * Graph Module - Decision Graph Visualization
4
3
  * @module modules/graph
@@ -11,997 +10,885 @@
11
10
  * - Detail panel for node information
12
11
  * - BFS traversal for connected nodes
13
12
  */
14
-
15
13
  /* eslint-env browser */
16
14
  /* global vis */
17
-
18
- import { escapeHtml, debounce, showToast } from '../utils/dom.js';
15
+ import { escapeHtml, escapeAttr, debounce, showToast, getElementByIdOrNull, getErrorMessage, } from '../utils/dom.js';
19
16
  import { DebugLogger } from '../utils/debug-logger.js';
20
17
  import { API } from '../utils/api.js';
21
-
18
+ import { renderSafeMarkdown } from '../utils/markdown.js';
22
19
  const logger = new DebugLogger('Graph');
23
-
24
20
  /**
25
21
  * Graph Module Class
26
22
  */
27
23
  export class GraphModule {
28
- constructor() {
29
- // Network state
30
- this.network = null;
31
- this.graphData = { nodes: [], edges: [], meta: {} };
32
- this.currentNodeId = null;
33
- this.adjacencyList = new Map();
34
-
35
- // Search state
36
- this.searchMatches = [];
37
- this.currentSearchIndex = 0;
38
- this.debouncedSearch = debounce(() => this.search(), 300);
39
-
40
- // Color management - MAMA Brand Palette
41
- this.topicColors = {};
42
- this.colorPalette = [
43
- '#FFCE00', // mama yellow (primary)
44
- '#E6B800', // mama yellow-hover
45
- '#FF9999', // mama blush
46
- '#D4C4E0', // mama lavender-dark
47
- '#22c55e', // success green
48
- '#f97316', // warning orange
49
- '#06b6d4', // info cyan
50
- '#8b5cf6', // purple accent
51
- '#ec4899', // pink accent
52
- '#f59e0b', // amber
53
- '#10b981', // teal
54
- '#0ea5e9', // sky blue
55
- ];
56
- this.colorIndex = 0;
57
-
58
- // Edge styles - High contrast for lavender background
59
- this.edgeStyles = {
60
- supersedes: { color: '#666666', dashes: false, width: 2 },
61
- builds_on: { color: '#B8860B', dashes: [5, 5], width: 2.5 }, // dark goldenrod
62
- debates: { color: '#DC143C', dashes: [5, 5], width: 2.5 }, // crimson
63
- synthesizes: { color: '#6B4C9A', width: 3, dashes: false }, // dark purple
64
- };
65
- }
66
-
67
- // =============================================
68
- // Data Loading
69
- // =============================================
70
-
71
- /**
72
- * Fetch graph data from API
73
- */
74
- async fetchData() {
75
- try {
76
- this.graphData = await API.getGraph();
77
- logger.info('Graph data loaded:', this.graphData.meta);
78
- return this.graphData;
79
- } catch (error) {
80
- logger.error('Failed to fetch graph:', error);
81
- throw error;
82
- }
83
- }
84
-
85
- // =============================================
86
- // Graph Initialization
87
- // =============================================
88
-
89
- /**
90
- * Initialize vis-network
91
- */
92
- init(data) {
93
- const container = document.getElementById('graph-canvas');
94
- if (!container) {
95
- logger.error('graph-canvas element not found');
96
- return;
97
- }
98
-
99
- // Ensure container has dimensions for vis-network
100
- if (container.offsetHeight === 0) {
101
- container.style.minHeight = '400px';
102
- }
103
-
104
- logger.debug('Graph canvas dimensions:', container.offsetWidth, 'x', container.offsetHeight);
105
-
106
- this.graphData = data;
107
-
108
- // Build adjacency list for BFS
109
- this.buildAdjacencyList(data.edges);
110
-
111
- // Calculate connection counts for sizing
112
- const connectionCounts = this.calculateConnectionCounts(data.nodes, data.edges);
113
-
114
- // Map nodes to vis-network format
115
- const nodes = data.nodes.map((n) => ({
116
- id: n.id,
117
- label: n.topic || n.id.substring(0, 20),
118
- title: this.createNodeTooltip(n),
119
- color: {
120
- background: this.getTopicColor(n.topic),
121
- border: this.getOutcomeBorderColor(n.outcome),
122
- highlight: { background: this.getTopicColor(n.topic), border: '#fff' },
123
- },
124
- size: this.getNodeSize(connectionCounts[n.id] || 0),
125
- font: { color: '#131313', size: 12 },
126
- borderWidth: 3,
127
- data: n,
128
- }));
129
-
130
- // Map edges to vis-network format
131
- const edges = data.edges.map((e) => {
132
- const style = this.getEdgeStyle(e.relationship);
133
- return {
134
- from: e.from,
135
- to: e.to,
136
- arrows: { to: { enabled: true, scaleFactor: 0.5 } },
137
- color: style.color,
138
- dashes: style.dashes,
139
- width: style.width || 2,
140
- title: e.relationship,
141
- };
142
- });
143
-
144
- const networkData = {
145
- nodes: new vis.DataSet(nodes),
146
- edges: new vis.DataSet(edges),
24
+ network = null;
25
+ graphData = { nodes: [], edges: [], meta: {} };
26
+ currentNodeId = null;
27
+ adjacencyList = new Map();
28
+ searchMatches = [];
29
+ currentSearchIndex = 0;
30
+ debouncedSearch = debounce(() => this.search(), 300);
31
+ similarDecisionClickHandler = (event) => {
32
+ const target = event.target;
33
+ const btn = target.closest('.similar-decision-btn');
34
+ if (!btn || !btn.dataset.nodeId) {
35
+ return;
36
+ }
37
+ this.navigateToNode(btn.dataset.nodeId);
147
38
  };
148
-
149
- const options = {
150
- nodes: {
151
- shape: 'dot',
152
- scaling: { min: 10, max: 30 },
153
- },
154
- edges: {
155
- smooth: { type: 'continuous', roundness: 0.5 },
156
- width: 2,
157
- },
158
- physics: {
159
- enabled: true,
160
- barnesHut: {
161
- gravitationalConstant: -8000,
162
- centralGravity: 0.3,
163
- springLength: 150,
164
- springConstant: 0.04,
165
- damping: 0.09,
166
- avoidOverlap: 0.5,
167
- },
168
- stabilization: {
169
- enabled: true,
170
- iterations: 200,
171
- updateInterval: 50,
172
- },
173
- },
174
- interaction: {
175
- hover: true,
176
- tooltipDelay: 200,
177
- zoomView: true,
178
- dragView: true,
179
- },
39
+ topicColors = {};
40
+ colorPalette = [
41
+ '#FFCE00', // mama yellow (primary)
42
+ '#E6B800', // mama yellow-hover
43
+ '#FF9999', // mama blush
44
+ '#D4C4E0', // mama lavender-dark
45
+ '#22c55e', // success green
46
+ '#f97316', // warning orange
47
+ '#06b6d4', // info cyan
48
+ '#8b5cf6', // purple accent
49
+ '#ec4899', // pink accent
50
+ '#f59e0b', // amber
51
+ '#10b981', // teal
52
+ '#0ea5e9', // sky blue
53
+ ];
54
+ colorIndex = 0;
55
+ edgeStyles = {
56
+ supersedes: { color: '#666666', dashes: false, width: 2 },
57
+ builds_on: { color: '#B8860B', dashes: [5, 5], width: 2.5 }, // dark goldenrod
58
+ debates: { color: '#DC143C', dashes: [5, 5], width: 2.5 }, // crimson
59
+ synthesizes: { color: '#6B4C9A', width: 3, dashes: false }, // dark purple
180
60
  };
181
-
182
- this.network = new vis.Network(container, networkData, options);
183
-
184
- // Event handlers
185
- this.network.on('click', (params) => {
186
- try {
187
- if (params.nodes.length > 0) {
188
- const nodeId = params.nodes[0];
189
- const node = data.nodes.find((n) => n.id === nodeId);
190
- if (node) {
191
- logger.debug('Node clicked:', nodeId, node);
192
- this.showDetail(node);
193
- this.highlightConnectedNodes(nodeId);
194
- }
195
- } else {
196
- this.closeDetail();
197
- this.resetNodeHighlight();
198
- }
199
- } catch (error) {
200
- logger.error('Error handling click:', error);
201
- logger.error('Error stack:', error.stack);
202
- }
203
- });
204
-
205
- this.network.on('stabilized', () => {
206
- const loadingEl = document.getElementById('graph-loading');
207
- if (loadingEl) {
208
- loadingEl.style.display = 'none';
209
- }
210
- logger.info('Graph stabilized');
211
- });
212
-
213
- // Backup: hide loading after 3 seconds even if stabilization doesn't complete
214
- setTimeout(() => {
215
- const loadingEl = document.getElementById('graph-loading');
216
- if (loadingEl && loadingEl.style.display !== 'none') {
217
- loadingEl.style.display = 'none';
218
- logger.warn('Graph loading hidden by timeout');
219
- }
220
- }, 3000);
221
-
222
- // Populate topic filter
223
- const topics = [...new Set(data.nodes.map((n) => n.topic))].sort();
224
- this.populateTopicFilter(topics);
225
-
226
- logger.info('Graph initialized with', nodes.length, 'nodes and', edges.length, 'edges');
227
- }
228
-
229
- // =============================================
230
- // Styling Utilities
231
- // =============================================
232
-
233
- /**
234
- * Get color for topic
235
- */
236
- getTopicColor(topic) {
237
- if (!this.topicColors[topic]) {
238
- this.topicColors[topic] = this.colorPalette[this.colorIndex % this.colorPalette.length];
239
- this.colorIndex++;
240
- }
241
- return this.topicColors[topic];
242
- }
243
-
244
- /**
245
- * Get border color based on outcome
246
- */
247
- getOutcomeBorderColor(outcome) {
248
- switch (outcome?.toLowerCase()) {
249
- case 'success':
250
- return '#22c55e';
251
- case 'failed':
252
- return '#ef4444';
253
- case 'partial':
254
- return '#f59e0b';
255
- default:
256
- return '#4a4a6a';
257
- }
258
- }
259
-
260
- /**
261
- * Get edge style by relationship type
262
- */
263
- getEdgeStyle(relationship) {
264
- return this.edgeStyles[relationship] || { color: '#4a4a6a', dashes: false };
265
- }
266
-
267
- /**
268
- * Get node size based on connection count
269
- */
270
- getNodeSize(connectionCount) {
271
- if (connectionCount <= 2) {
272
- return 12;
273
- }
274
- if (connectionCount <= 5) {
275
- return 18;
276
- }
277
- if (connectionCount <= 10) {
278
- return 24;
279
- }
280
- return 30;
281
- }
282
-
283
- /**
284
- * Create node tooltip
285
- */
286
- createNodeTooltip(node) {
287
- return `
61
+ constructor() {
62
+ // Network state
63
+ }
64
+ // =============================================
65
+ // Data Loading
66
+ // =============================================
67
+ /**
68
+ * Fetch graph data from API
69
+ */
70
+ async fetchData() {
71
+ try {
72
+ this.graphData = await API.getGraph();
73
+ logger.info('Graph data loaded:', this.graphData.meta);
74
+ return this.graphData;
75
+ }
76
+ catch (error) {
77
+ logger.error('Failed to fetch graph:', error);
78
+ throw error;
79
+ }
80
+ }
81
+ // =============================================
82
+ // Graph Initialization
83
+ // =============================================
84
+ /**
85
+ * Initialize vis-network
86
+ */
87
+ init(data) {
88
+ const container = getElementByIdOrNull('graph-canvas');
89
+ if (!container) {
90
+ logger.error('graph-canvas element not found');
91
+ return;
92
+ }
93
+ // Ensure container has dimensions for vis-network
94
+ if (container.offsetHeight === 0) {
95
+ container.style.minHeight = '400px';
96
+ }
97
+ logger.debug('Graph canvas dimensions:', container.offsetWidth, 'x', container.offsetHeight);
98
+ this.graphData = data;
99
+ // Build adjacency list for BFS
100
+ this.buildAdjacencyList(data.edges);
101
+ // Calculate connection counts for sizing
102
+ const connectionCounts = this.calculateConnectionCounts(data.nodes, data.edges);
103
+ // Map nodes to vis-network format
104
+ const nodes = data.nodes.map((n) => ({
105
+ id: n.id,
106
+ label: n.topic || String(n.id).substring(0, 20),
107
+ title: this.createNodeTooltip(n),
108
+ color: {
109
+ background: this.getTopicColor(n.topic),
110
+ border: this.getOutcomeBorderColor(n.outcome),
111
+ highlight: { background: this.getTopicColor(n.topic), border: '#fff' },
112
+ },
113
+ size: this.getNodeSize(connectionCounts[String(n.id)] || 0),
114
+ font: { color: '#131313', size: 12 },
115
+ borderWidth: 3,
116
+ data: n,
117
+ }));
118
+ // Map edges to vis-network format
119
+ const edges = data.edges.map((e) => {
120
+ const style = this.getEdgeStyle(e.relationship);
121
+ return {
122
+ from: e.from,
123
+ to: e.to,
124
+ arrows: { to: { enabled: true, scaleFactor: 0.5 } },
125
+ color: style.color,
126
+ dashes: style.dashes,
127
+ width: style.width || 2,
128
+ title: e.relationship,
129
+ };
130
+ });
131
+ const networkData = {
132
+ nodes: new vis.DataSet(nodes),
133
+ edges: new vis.DataSet(edges),
134
+ };
135
+ const options = {
136
+ nodes: {
137
+ shape: 'dot',
138
+ scaling: { min: 10, max: 30 },
139
+ },
140
+ edges: {
141
+ smooth: { type: 'continuous', roundness: 0.5 },
142
+ width: 2,
143
+ },
144
+ physics: {
145
+ enabled: true,
146
+ barnesHut: {
147
+ gravitationalConstant: -8000,
148
+ centralGravity: 0.3,
149
+ springLength: 150,
150
+ springConstant: 0.04,
151
+ damping: 0.09,
152
+ avoidOverlap: 0.5,
153
+ },
154
+ stabilization: {
155
+ enabled: true,
156
+ iterations: 200,
157
+ updateInterval: 50,
158
+ },
159
+ },
160
+ interaction: {
161
+ hover: true,
162
+ tooltipDelay: 200,
163
+ zoomView: true,
164
+ dragView: true,
165
+ },
166
+ };
167
+ this.network = new vis.Network(container, networkData, options);
168
+ // Event handlers
169
+ this.network.on('click', (params) => {
170
+ try {
171
+ if (params.nodes.length > 0) {
172
+ const nodeId = params.nodes[0];
173
+ const targetId = String(nodeId);
174
+ const node = data.nodes.find((n) => String(n.id) === targetId);
175
+ if (node) {
176
+ logger.debug('Node clicked:', nodeId, node);
177
+ this.showDetail(node);
178
+ this.highlightConnectedNodes(targetId);
179
+ }
180
+ }
181
+ else {
182
+ this.closeDetail();
183
+ this.resetNodeHighlight();
184
+ }
185
+ }
186
+ catch (error) {
187
+ logger.error('Error handling click:', error);
188
+ if (error instanceof Error && error.stack) {
189
+ logger.error('Error stack:', error.stack);
190
+ }
191
+ }
192
+ });
193
+ this.network.on('stabilized', () => {
194
+ const loadingEl = getElementByIdOrNull('graph-loading');
195
+ if (loadingEl) {
196
+ loadingEl.style.display = 'none';
197
+ }
198
+ logger.info('Graph stabilized');
199
+ });
200
+ // Backup: hide loading after 3 seconds even if stabilization doesn't complete
201
+ setTimeout(() => {
202
+ const loadingEl = getElementByIdOrNull('graph-loading');
203
+ if (loadingEl && loadingEl.style.display !== 'none') {
204
+ loadingEl.style.display = 'none';
205
+ logger.warn('Graph loading hidden by timeout');
206
+ }
207
+ }, 3000);
208
+ // Populate topic filter
209
+ const topics = [...new Set(data.nodes.map((n) => n.topic || ''))].sort();
210
+ this.populateTopicFilter(topics);
211
+ logger.info('Graph initialized with', nodes.length, 'nodes and', edges.length, 'edges');
212
+ }
213
+ // =============================================
214
+ // Styling Utilities
215
+ // =============================================
216
+ /**
217
+ * Get color for topic
218
+ */
219
+ getTopicColor(topic = '') {
220
+ if (!this.topicColors[topic]) {
221
+ this.topicColors[topic] = this.colorPalette[this.colorIndex % this.colorPalette.length];
222
+ this.colorIndex++;
223
+ }
224
+ return this.topicColors[topic];
225
+ }
226
+ /**
227
+ * Get border color based on outcome
228
+ */
229
+ getOutcomeBorderColor(outcome) {
230
+ switch (outcome?.toLowerCase()) {
231
+ case 'success':
232
+ return '#22c55e';
233
+ case 'failed':
234
+ return '#ef4444';
235
+ case 'partial':
236
+ return '#f59e0b';
237
+ default:
238
+ return '#4a4a6a';
239
+ }
240
+ }
241
+ /**
242
+ * Get edge style by relationship type
243
+ */
244
+ getEdgeStyle(relationship) {
245
+ return (this.edgeStyles[relationship || 'default'] || { color: '#4a4a6a', dashes: false, width: 2 });
246
+ }
247
+ /**
248
+ * Get node size based on connection count
249
+ */
250
+ getNodeSize(connectionCount) {
251
+ if (connectionCount <= 2) {
252
+ return 12;
253
+ }
254
+ if (connectionCount <= 5) {
255
+ return 18;
256
+ }
257
+ if (connectionCount <= 10) {
258
+ return 24;
259
+ }
260
+ return 30;
261
+ }
262
+ /**
263
+ * Create node tooltip
264
+ */
265
+ createNodeTooltip(node) {
266
+ return `
288
267
  <strong>${escapeHtml(node.topic || 'Unknown')}</strong><br>
289
268
  Decision: ${escapeHtml((node.decision || '').substring(0, 100))}...<br>
290
- Outcome: ${node.outcome || 'PENDING'}<br>
269
+ Outcome: ${escapeHtml(node.outcome || 'PENDING')}<br>
291
270
  Confidence: ${Math.round((node.confidence || 0) * 100)}%
292
271
  `;
293
- }
294
-
295
- // =============================================
296
- // Data Processing
297
- // =============================================
298
-
299
- /**
300
- * Build adjacency list for BFS
301
- */
302
- buildAdjacencyList(edges) {
303
- this.adjacencyList = new Map();
304
-
305
- edges.forEach((edge) => {
306
- if (!this.adjacencyList.has(edge.from)) {
307
- this.adjacencyList.set(edge.from, []);
308
- }
309
- if (!this.adjacencyList.has(edge.to)) {
310
- this.adjacencyList.set(edge.to, []);
311
- }
312
- this.adjacencyList.get(edge.from).push(edge.to);
313
- this.adjacencyList.get(edge.to).push(edge.from);
314
- });
315
-
316
- logger.debug('Adjacency list built with', this.adjacencyList.size, 'nodes');
317
- }
318
-
319
- /**
320
- * Calculate connection count for each node
321
- */
322
- calculateConnectionCounts(nodes, edges) {
323
- const counts = {};
324
- nodes.forEach((n) => (counts[n.id] = 0));
325
-
326
- edges.forEach((edge) => {
327
- if (counts[edge.from] !== undefined) {
328
- counts[edge.from]++;
329
- }
330
- if (counts[edge.to] !== undefined) {
331
- counts[edge.to]++;
332
- }
333
- });
334
-
335
- return counts;
336
- }
337
-
338
- // =============================================
339
- // Graph Traversal & Highlighting
340
- // =============================================
341
-
342
- /**
343
- * Get connected node IDs using BFS
344
- */
345
- getConnectedNodeIds(nodeId, maxDepth = 3) {
346
- const visited = new Set();
347
- const queue = [{ id: nodeId, depth: 0 }];
348
- visited.add(nodeId);
349
-
350
- while (queue.length > 0) {
351
- const { id, depth } = queue.shift();
352
-
353
- if (depth >= maxDepth) {
354
- continue;
355
- }
356
-
357
- const neighbors = this.adjacencyList.get(id) || [];
358
- neighbors.forEach((neighborId) => {
359
- if (!visited.has(neighborId)) {
360
- visited.add(neighborId);
361
- queue.push({ id: neighborId, depth: depth + 1 });
362
- }
363
- });
364
- }
365
-
366
- return Array.from(visited);
367
- }
368
-
369
- /**
370
- * Highlight connected nodes
371
- */
372
- highlightConnectedNodes(nodeId) {
373
- if (!this.network) {
374
- return;
375
- }
376
-
377
- const connectedIds = this.getConnectedNodeIds(nodeId, 3);
378
- const allNodes = this.network.body.data.nodes.get();
379
-
380
- allNodes.forEach((node) => {
381
- const isConnected = connectedIds.includes(node.id);
382
- const opacity = isConnected ? 1.0 : 0.2;
383
-
384
- this.network.body.data.nodes.update({
385
- id: node.id,
386
- opacity: opacity,
387
- font: { ...node.font, color: isConnected ? '#131313' : '#999' },
388
- });
389
- });
390
-
391
- const allEdges = this.network.body.data.edges.get();
392
- allEdges.forEach((edge) => {
393
- const isConnected = connectedIds.includes(edge.from) && connectedIds.includes(edge.to);
394
- this.network.body.data.edges.update({
395
- id: edge.id,
396
- opacity: isConnected ? 1.0 : 0.1,
397
- });
398
- });
399
- }
400
-
401
- /**
402
- * Reset node highlight
403
- */
404
- resetNodeHighlight() {
405
- if (!this.network) {
406
- return;
407
- }
408
-
409
- const allNodes = this.network.body.data.nodes.get();
410
- allNodes.forEach((node) => {
411
- this.network.body.data.nodes.update({
412
- id: node.id,
413
- opacity: 1.0,
414
- font: { ...node.font, color: '#131313' },
415
- });
416
- });
417
-
418
- const allEdges = this.network.body.data.edges.get();
419
- allEdges.forEach((edge) => {
420
- this.network.body.data.edges.update({
421
- id: edge.id,
422
- opacity: 1.0,
423
- });
424
- });
425
- }
426
-
427
- // =============================================
428
- // Detail Panel
429
- // =============================================
430
-
431
- /**
432
- * Show node detail panel
433
- */
434
- async showDetail(node) {
435
- try {
436
- logger.debug('showDetail called with node:', node);
437
- this.currentNodeId = node.id;
438
- const panel = document.getElementById('decision-detail-modal');
439
-
440
- if (!panel) {
441
- logger.error('decision-detail-modal element not found');
442
- return;
443
- }
444
-
445
- // Update existing DOM elements with markdown rendering
446
- document.getElementById('detail-topic').textContent = node.topic || 'Unknown Topic';
447
- const decisionEl = document.getElementById('detail-decision');
448
- const reasoningEl = document.getElementById('detail-reasoning');
449
-
450
- // Use marked for markdown if available (textContent to avoid XSS)
451
- if (typeof marked !== 'undefined' && marked.parse) {
272
+ }
273
+ // =============================================
274
+ // Data Processing
275
+ // =============================================
276
+ /**
277
+ * Build adjacency list for BFS
278
+ */
279
+ buildAdjacencyList(edges) {
280
+ this.adjacencyList = new Map();
281
+ edges.forEach((edge) => {
282
+ const from = String(edge.from);
283
+ const to = String(edge.to);
284
+ if (!this.adjacencyList.has(from)) {
285
+ this.adjacencyList.set(from, []);
286
+ }
287
+ if (!this.adjacencyList.has(to)) {
288
+ this.adjacencyList.set(to, []);
289
+ }
290
+ this.adjacencyList.get(from).push(to);
291
+ this.adjacencyList.get(to).push(from);
292
+ });
293
+ logger.debug('Adjacency list built with', this.adjacencyList.size, 'nodes');
294
+ }
295
+ /**
296
+ * Calculate connection count for each node
297
+ */
298
+ calculateConnectionCounts(nodes, edges) {
299
+ const counts = {};
300
+ nodes.forEach((n) => {
301
+ counts[String(n.id)] = 0;
302
+ });
303
+ edges.forEach((edge) => {
304
+ const from = String(edge.from);
305
+ const to = String(edge.to);
306
+ if (counts[from] !== undefined) {
307
+ counts[from]++;
308
+ }
309
+ if (counts[to] !== undefined) {
310
+ counts[to]++;
311
+ }
312
+ });
313
+ return counts;
314
+ }
315
+ // =============================================
316
+ // Graph Traversal & Highlighting
317
+ // =============================================
318
+ /**
319
+ * Get connected node IDs using BFS
320
+ */
321
+ getConnectedNodeIds(nodeId, maxDepth = 3) {
322
+ const visited = new Set();
323
+ const queue = [{ id: nodeId, depth: 0 }];
324
+ visited.add(nodeId);
325
+ while (queue.length > 0) {
326
+ const next = queue.shift();
327
+ if (!next) {
328
+ continue;
329
+ }
330
+ const { id, depth } = next;
331
+ if (depth >= maxDepth) {
332
+ continue;
333
+ }
334
+ const neighbors = this.adjacencyList.get(id) || [];
335
+ neighbors.forEach((neighborId) => {
336
+ if (!visited.has(neighborId)) {
337
+ visited.add(neighborId);
338
+ queue.push({ id: neighborId, depth: depth + 1 });
339
+ }
340
+ });
341
+ }
342
+ return Array.from(visited);
343
+ }
344
+ /**
345
+ * Highlight connected nodes
346
+ */
347
+ highlightConnectedNodes(nodeId) {
348
+ const targetId = String(nodeId);
349
+ if (!this.network) {
350
+ return;
351
+ }
352
+ const connectedIds = this.getConnectedNodeIds(targetId, 3);
353
+ const allNodes = this.network.body.data.nodes.get();
354
+ allNodes.forEach((node) => {
355
+ const isConnected = connectedIds.includes(String(node.id));
356
+ const opacity = isConnected ? 1.0 : 0.2;
357
+ this.network.body.data.nodes.update({
358
+ id: node.id,
359
+ opacity: opacity,
360
+ font: { ...node.font, color: isConnected ? '#131313' : '#999' },
361
+ });
362
+ });
363
+ const allEdges = this.network.body.data.edges.get();
364
+ allEdges.forEach((edge) => {
365
+ const isConnected = connectedIds.includes(String(edge.from)) && connectedIds.includes(String(edge.to));
366
+ this.network.body.data.edges.update({
367
+ id: edge.id,
368
+ opacity: isConnected ? 1.0 : 0.1,
369
+ });
370
+ });
371
+ }
372
+ /**
373
+ * Reset node highlight
374
+ */
375
+ resetNodeHighlight() {
376
+ if (!this.network) {
377
+ return;
378
+ }
379
+ const allNodes = this.network.body.data.nodes.get();
380
+ allNodes.forEach((node) => {
381
+ this.network.body.data.nodes.update({
382
+ id: node.id,
383
+ opacity: 1.0,
384
+ font: { ...node.font, color: '#131313' },
385
+ });
386
+ });
387
+ const allEdges = this.network.body.data.edges.get();
388
+ allEdges.forEach((edge) => {
389
+ this.network.body.data.edges.update({
390
+ id: edge.id,
391
+ opacity: 1.0,
392
+ });
393
+ });
394
+ }
395
+ // =============================================
396
+ // Detail Panel
397
+ // =============================================
398
+ /**
399
+ * Show node detail panel
400
+ */
401
+ async showDetail(node) {
452
402
  try {
453
- const renderedDecision = marked.parse(node.decision || '-', {
454
- mangle: false,
455
- headerIds: false,
456
- });
457
- const renderedReasoning = marked.parse(node.reasoning || '-', {
458
- mangle: false,
459
- headerIds: false,
460
- });
461
- decisionEl.textContent = renderedDecision;
462
- reasoningEl.textContent = renderedReasoning;
463
- } catch (e) {
464
- const message = e instanceof Error ? e.message : String(e);
465
- showToast(`Markdown render failed: ${message}`);
466
- logger.warn('[Graph] Markdown parse failed:', e);
467
- decisionEl.textContent = node.decision || '-';
468
- reasoningEl.textContent = node.reasoning || '-';
469
- }
470
- } else {
471
- decisionEl.textContent = node.decision || '-';
472
- reasoningEl.textContent = node.reasoning || '-';
473
- }
474
-
475
- const outcomeSelect = document.getElementById('detail-outcome-select');
476
- if (outcomeSelect) {
477
- outcomeSelect.value = (node.outcome || 'PENDING').toUpperCase();
478
- }
479
-
480
- // Clear outcome status
481
- const outcomeStatus = document.getElementById('outcome-status');
482
- if (outcomeStatus) {
483
- outcomeStatus.textContent = '';
484
- outcomeStatus.className = '';
485
- }
486
-
487
- document.getElementById('detail-confidence').textContent = node.confidence
488
- ? `${(node.confidence * 100).toFixed(0)}%`
489
- : '-';
490
-
491
- const createdEl = document.getElementById('detail-created');
492
- if (createdEl) {
493
- createdEl.textContent = node.created_at ? new Date(node.created_at).toLocaleString() : '-';
494
- }
495
-
496
- // Reset reasoning toggle
497
- const reasoningArrow = document.getElementById('reasoning-arrow');
498
- if (reasoningArrow) {
499
- reasoningArrow.textContent = '▶';
500
- }
501
- document.getElementById('detail-reasoning').classList.add('hidden');
502
-
503
- // Show loading state for similar decisions
504
- const similarEl = document.getElementById('detail-similar');
505
- if (similarEl) {
506
- similarEl.innerHTML = '<span class="loading-similar">Searching...</span>';
507
- }
508
-
509
- // Show panel
510
- panel.classList.add('visible');
511
-
512
- // Fetch similar decisions
513
- logger.info('Fetching similar decisions...');
514
- this.fetchSimilarDecisions(node.id);
515
- logger.debug('showDetail completed successfully');
516
- } catch (error) {
517
- logger.error('Error in showDetail:', error);
518
- logger.error('Error stack:', error.stack);
519
- logger.error('Node data:', node);
520
- }
521
- }
522
-
523
- /**
524
- * Get outcome icon name
525
- */
526
- getOutcomeIcon(outcome) {
527
- const outcomeMap = {
528
- pending: 'clock',
529
- success: 'check-circle',
530
- failed: 'x-circle',
531
- partial: 'alert-circle',
532
- };
533
- return outcomeMap[(outcome || 'pending').toLowerCase()] || 'clock';
534
- }
535
-
536
- /**
537
- * Toggle reasoning full text
538
- */
539
- toggleReasoning() {
540
- const arrow = document.getElementById('reasoning-arrow');
541
- const content = document.getElementById('detail-reasoning');
542
- if (arrow && content) {
543
- const isHidden = content.classList.contains('hidden');
544
- content.classList.toggle('hidden');
545
- arrow.textContent = isHidden ? '' : '▶';
546
- }
547
- }
548
-
549
- /**
550
- * Close detail panel
551
- */
552
- closeDetail() {
553
- const panel = document.getElementById('decision-detail-modal');
554
- if (panel) {
555
- panel.classList.remove('visible');
556
- }
557
- this.currentNodeId = null;
558
- this.resetNodeHighlight();
559
- }
560
-
561
- /**
562
- * Fetch similar decisions
563
- */
564
- async fetchSimilarDecisions(nodeId) {
565
- logger.debug('fetchSimilarDecisions called for node:', nodeId);
566
- const container = document.getElementById('detail-similar');
567
-
568
- if (!container) {
569
- logger.warn('detail-similar element not found');
570
- return;
571
- }
572
-
573
- try {
574
- logger.info('Calling API.getSimilarDecisions...');
575
- const data = await API.getSimilarDecisions(nodeId);
576
- logger.debug('Similar decisions received:', data);
577
-
578
- if (data.error) {
579
- container.innerHTML = `<span style="color:#666">${escapeHtml(data.message || 'Search failed')}</span>`;
580
- return;
581
- }
582
-
583
- const similar = data.similar || [];
584
-
585
- if (similar.length === 0) {
586
- console.log('[MAMA] No similar decisions found');
587
- container.innerHTML = '<span style="color:#666">No similar decisions found</span>';
588
- return;
589
- }
590
-
591
- console.log('[MAMA] Building similar decisions HTML for', similar.length, 'items');
592
- const html = similar
593
- .map(
594
- (s) => `
595
- <button class="w-full text-left p-2 mb-2 bg-gray-100 dark:bg-gray-800 hover:bg-indigo-100 dark:hover:bg-indigo-900/30 border border-gray-200 dark:border-gray-700 rounded-lg transition-colors" onclick="window.graphModule.navigateToNode('${s.id}')">
596
- <div class="text-xs font-semibold text-indigo-600 dark:text-indigo-400">${escapeHtml(s.topic)}</div>
597
- <div class="text-xs text-gray-600 dark:text-gray-400 mt-1 line-clamp-2">${escapeHtml((s.decision || '').substring(0, 80))}...</div>
403
+ logger.debug('showDetail called with node:', node);
404
+ this.currentNodeId = String(node.id);
405
+ const panel = getElementByIdOrNull('decision-detail-modal');
406
+ if (!panel) {
407
+ logger.error('decision-detail-modal element not found');
408
+ return;
409
+ }
410
+ // Update existing DOM elements with markdown rendering
411
+ const topicEl = getElementByIdOrNull('detail-topic');
412
+ const decisionEl = getElementByIdOrNull('detail-decision');
413
+ const reasoningEl = getElementByIdOrNull('detail-reasoning');
414
+ if (!decisionEl || !reasoningEl) {
415
+ logger.error('Required detail elements missing');
416
+ return;
417
+ }
418
+ if (topicEl) {
419
+ topicEl.textContent = node.topic || 'Unknown Topic';
420
+ }
421
+ decisionEl.innerHTML = renderSafeMarkdown(node.decision || '-');
422
+ reasoningEl.innerHTML = renderSafeMarkdown(node.reasoning || '-');
423
+ const outcomeSelect = getElementByIdOrNull('detail-outcome-select');
424
+ if (outcomeSelect) {
425
+ outcomeSelect.value = (node.outcome || 'PENDING').toUpperCase();
426
+ }
427
+ // Clear outcome status
428
+ const outcomeStatus = getElementByIdOrNull('outcome-status');
429
+ if (outcomeStatus) {
430
+ outcomeStatus.textContent = '';
431
+ outcomeStatus.className = '';
432
+ }
433
+ const confidenceEl = getElementByIdOrNull('detail-confidence');
434
+ if (confidenceEl) {
435
+ confidenceEl.textContent = node.confidence ? `${(node.confidence * 100).toFixed(0)}%` : '-';
436
+ }
437
+ const createdEl = getElementByIdOrNull('detail-created');
438
+ if (createdEl) {
439
+ createdEl.textContent = node.created_at ? new Date(node.created_at).toLocaleString() : '-';
440
+ }
441
+ // Reset reasoning toggle
442
+ const reasoningArrow = getElementByIdOrNull('reasoning-arrow');
443
+ if (reasoningArrow) {
444
+ reasoningArrow.textContent = '▶';
445
+ }
446
+ reasoningEl.classList.add('hidden');
447
+ // Show loading state for similar decisions
448
+ const similarEl = getElementByIdOrNull('detail-similar');
449
+ if (similarEl) {
450
+ similarEl.innerHTML = '<span class="loading-similar">Searching...</span>';
451
+ }
452
+ // Show panel
453
+ panel.classList.add('visible');
454
+ // Fetch similar decisions
455
+ logger.info('Fetching similar decisions...');
456
+ await this.fetchSimilarDecisions(String(node.id));
457
+ logger.debug('showDetail completed successfully');
458
+ }
459
+ catch (error) {
460
+ logger.error('Error in showDetail:', error);
461
+ if (error instanceof Error && error.stack) {
462
+ logger.error('Error stack:', error.stack);
463
+ }
464
+ logger.error('Node data:', node);
465
+ }
466
+ }
467
+ /**
468
+ * Get outcome icon name
469
+ */
470
+ getOutcomeIcon(outcome) {
471
+ const outcomeMap = {
472
+ pending: 'clock',
473
+ success: 'check-circle',
474
+ failed: 'x-circle',
475
+ partial: 'alert-circle',
476
+ };
477
+ return outcomeMap[(outcome || 'pending').toLowerCase()] || 'clock';
478
+ }
479
+ /**
480
+ * Toggle reasoning full text
481
+ */
482
+ toggleReasoning() {
483
+ const arrow = getElementByIdOrNull('reasoning-arrow');
484
+ const content = getElementByIdOrNull('detail-reasoning');
485
+ if (arrow && content) {
486
+ const isHidden = content.classList.contains('hidden');
487
+ content.classList.toggle('hidden');
488
+ arrow.textContent = isHidden ? '▼' : '▶';
489
+ }
490
+ }
491
+ /**
492
+ * Close detail panel
493
+ */
494
+ closeDetail() {
495
+ const panel = getElementByIdOrNull('decision-detail-modal');
496
+ if (panel) {
497
+ panel.classList.remove('visible');
498
+ }
499
+ this.currentNodeId = null;
500
+ this.resetNodeHighlight();
501
+ }
502
+ /**
503
+ * Fetch similar decisions
504
+ */
505
+ async fetchSimilarDecisions(nodeId) {
506
+ logger.debug('fetchSimilarDecisions called for node:', nodeId);
507
+ const container = getElementByIdOrNull('detail-similar');
508
+ if (!container) {
509
+ logger.warn('detail-similar element not found');
510
+ return;
511
+ }
512
+ try {
513
+ logger.info('Calling API.getSimilarDecisions...');
514
+ const data = await API.getSimilarDecisions(nodeId);
515
+ logger.debug('Similar decisions received:', data);
516
+ const similar = (data.similar || []);
517
+ if (similar.length === 0) {
518
+ logger.debug('[MAMA] No similar decisions found');
519
+ container.innerHTML = '<span style="color:#666">No similar decisions found</span>';
520
+ return;
521
+ }
522
+ logger.debug('[MAMA] Building similar decisions HTML for', similar.length, 'items');
523
+ const html = similar
524
+ .map((s) => `
525
+ <button class="similar-decision-btn w-full text-left p-2 mb-2 bg-gray-100 dark:bg-gray-800 hover:bg-indigo-100 dark:hover:bg-indigo-900/30 border border-gray-200 dark:border-gray-700 rounded-lg transition-colors" data-node-id="${escapeAttr(String(s.id))}">
526
+ <div class="text-xs font-semibold text-indigo-600 dark:text-indigo-400">${escapeHtml(String(s.topic || ''))}</div>
527
+ <div class="text-xs text-gray-600 dark:text-gray-400 mt-1 line-clamp-2">${escapeHtml(String(s.decision || '').substring(0, 80))}...</div>
598
528
  <div class="text-xs text-gray-500 mt-1">${Math.round((s.similarity || 0) * 100)}% match</div>
599
529
  </button>
600
- `
601
- )
602
- .join('');
603
-
604
- console.log('[MAMA] Setting similar decisions HTML...');
605
- container.innerHTML = html;
606
- console.log('[MAMA] fetchSimilarDecisions completed');
607
- } catch (error) {
608
- console.error('[MAMA] Failed to fetch similar decisions:', error);
609
- logger.error('Error stack:', error.stack);
610
- container.innerHTML = '<span style="color:#f66">Failed to load</span>';
611
- }
612
- }
613
-
614
- /**
615
- * Save outcome for current node
616
- */
617
- async saveOutcome() {
618
- const select = document.getElementById('detail-outcome-select');
619
- const newOutcome = select.value;
620
-
621
- if (!this.currentNodeId || !newOutcome) {
622
- return;
623
- }
624
-
625
- try {
626
- await API.updateOutcome(this.currentNodeId, newOutcome);
627
-
628
- // Update local data
629
- const node = this.graphData.nodes.find((n) => n.id === this.currentNodeId);
630
- if (node) {
631
- node.outcome = newOutcome.toUpperCase();
632
-
633
- // Update visualization
634
- this.network.body.data.nodes.update({
635
- id: this.currentNodeId,
636
- color: {
637
- border: this.getOutcomeBorderColor(newOutcome),
638
- },
530
+ `)
531
+ .join('');
532
+ logger.debug('[MAMA] Setting similar decisions HTML');
533
+ container.innerHTML = html;
534
+ // Bind click handler once to avoid duplicate listeners.
535
+ container.removeEventListener('click', this.similarDecisionClickHandler);
536
+ container.addEventListener('click', this.similarDecisionClickHandler);
537
+ logger.debug('[MAMA] fetchSimilarDecisions completed');
538
+ }
539
+ catch (error) {
540
+ const message = getErrorMessage(error);
541
+ logger.error('[MAMA] Failed to fetch similar decisions:', message, error);
542
+ if (error instanceof Error && error.stack) {
543
+ logger.error('Error stack:', error.stack);
544
+ }
545
+ container.innerHTML = '<span style="color:#f66">Failed to load</span>';
546
+ }
547
+ }
548
+ /**
549
+ * Save outcome for current node
550
+ */
551
+ async saveOutcome() {
552
+ const select = getElementByIdOrNull('detail-outcome-select');
553
+ if (!select) {
554
+ return;
555
+ }
556
+ const newOutcome = select.value;
557
+ if (!this.currentNodeId || !newOutcome) {
558
+ return;
559
+ }
560
+ try {
561
+ await API.updateOutcome(this.currentNodeId, newOutcome);
562
+ // Update local data
563
+ const node = this.graphData.nodes.find((n) => String(n.id) === this.currentNodeId);
564
+ if (node) {
565
+ node.outcome = newOutcome.toUpperCase();
566
+ // Update visualization
567
+ this.network.body.data.nodes.update({
568
+ id: this.currentNodeId,
569
+ color: {
570
+ border: this.getOutcomeBorderColor(newOutcome),
571
+ },
572
+ });
573
+ // Refresh detail panel
574
+ this.showDetail(node);
575
+ }
576
+ logger.debug('[MAMA] Outcome updated:', this.currentNodeId, newOutcome);
577
+ }
578
+ catch (error) {
579
+ const message = getErrorMessage(error);
580
+ logger.error('[MAMA] Failed to update outcome:', message);
581
+ alert(`Failed to update outcome: ${message}`);
582
+ }
583
+ }
584
+ // =============================================
585
+ // Navigation
586
+ // =============================================
587
+ /**
588
+ * Navigate to specific node
589
+ */
590
+ async navigateToNode(nodeId) {
591
+ if (!this.network) {
592
+ return;
593
+ }
594
+ let nodeIdString = String(nodeId);
595
+ // Try exact match first
596
+ let node = this.graphData.nodes.find((n) => String(n.id) === nodeIdString);
597
+ // If not found, try partial match (for short IDs from checkpoints)
598
+ if (!node) {
599
+ logger.debug('[MAMA] Exact match not found, trying partial match for:', nodeIdString);
600
+ node = this.graphData.nodes.find((n) => String(n.id).startsWith(nodeIdString));
601
+ if (node) {
602
+ logger.debug('[MAMA] Found node via partial match:', node.id);
603
+ nodeIdString = String(node.id); // Update to the full ID
604
+ }
605
+ }
606
+ // If node not in current graph, reload without filters
607
+ if (!node) {
608
+ logger.warn('[MAMA] Node not in current graph, reloading all nodes...');
609
+ // Reset topic filter
610
+ const topicFilter = getElementByIdOrNull('topic-filter');
611
+ if (topicFilter) {
612
+ topicFilter.value = '';
613
+ }
614
+ // Reload graph: fetch fresh data and reinitialize
615
+ await this.fetchData();
616
+ this.init(this.graphData);
617
+ // Try exact match again
618
+ node = this.graphData.nodes.find((n) => String(n.id) === nodeIdString);
619
+ // If still not found, try partial match
620
+ if (!node) {
621
+ node = this.graphData.nodes.find((n) => String(n.id).startsWith(nodeIdString));
622
+ if (node) {
623
+ logger.debug('[MAMA] Found node via partial match after reload:', node.id);
624
+ nodeIdString = String(node.id);
625
+ }
626
+ }
627
+ if (!node) {
628
+ logger.warn('[MAMA] Node not found even after reload:', nodeIdString);
629
+ showToast('⚠️ Decision not found in graph');
630
+ return;
631
+ }
632
+ }
633
+ // Focus on node (use resolved nodeIdString, not original nodeId)
634
+ this.network.focus(nodeIdString, {
635
+ scale: 1.5,
636
+ animation: { duration: 500, easingFunction: 'easeInOutQuad' },
639
637
  });
640
-
641
- // Refresh detail panel
638
+ // Select node (triggers click event)
639
+ this.network.selectNodes([nodeIdString]);
640
+ // Show detail
642
641
  this.showDetail(node);
643
- }
644
-
645
- console.log('[MAMA] Outcome updated:', this.currentNodeId, newOutcome);
646
- } catch (error) {
647
- console.error('[MAMA] Failed to update outcome:', error);
648
- alert('Failed to update outcome: ' + error.message);
649
- }
650
- }
651
-
652
- // =============================================
653
- // Navigation
654
- // =============================================
655
-
656
- /**
657
- * Navigate to specific node
658
- */
659
- async navigateToNode(nodeId) {
660
- if (!this.network) {
661
- return;
662
- }
663
-
664
- // Try exact match first
665
- let node = this.graphData.nodes.find((n) => n.id === nodeId);
666
-
667
- // If not found, try partial match (for short IDs from checkpoints)
668
- if (!node) {
669
- console.log('[MAMA] Exact match not found, trying partial match for:', nodeId);
670
- node = this.graphData.nodes.find((n) => n.id.startsWith(nodeId));
671
-
672
- if (node) {
673
- console.log('[MAMA] Found node via partial match:', node.id);
674
- nodeId = node.id; // Update nodeId to the full ID
675
- }
676
- }
677
-
678
- // If node not in current graph, reload without filters
679
- if (!node) {
680
- console.log('[MAMA] Node not in current graph, reloading all nodes...');
681
-
682
- // Reset topic filter
683
- const topicFilter = document.getElementById('topic-filter');
684
- if (topicFilter) {
685
- topicFilter.value = '';
686
- }
687
-
688
- // Reload graph: fetch fresh data and reinitialize
689
- await this.fetchData();
690
- this.init(this.graphData);
691
-
692
- // Try exact match again
693
- node = this.graphData.nodes.find((n) => n.id === nodeId);
694
-
695
- // If still not found, try partial match
696
- if (!node) {
697
- node = this.graphData.nodes.find((n) => n.id.startsWith(nodeId));
698
- if (node) {
699
- console.log('[MAMA] Found node via partial match after reload:', node.id);
700
- nodeId = node.id;
701
- }
702
- }
703
-
704
- if (!node) {
705
- console.warn('[MAMA] Node not found even after reload:', nodeId);
706
- showToast('⚠️ Decision not found in graph');
707
- return;
708
- }
709
- }
710
-
711
- // Focus on node
712
- this.network.focus(nodeId, {
713
- scale: 1.5,
714
- animation: { duration: 500, easingFunction: 'easeInOutQuad' },
715
- });
716
-
717
- // Select node (triggers click event)
718
- this.network.selectNodes([nodeId]);
719
-
720
- // Show detail
721
- this.showDetail(node);
722
- this.highlightConnectedNodes(nodeId);
723
- }
724
-
725
- /**
726
- * Get connected edge types
727
- */
728
- getConnectedEdges(nodeId) {
729
- const edges = this.graphData.edges.filter((e) => e.from === nodeId || e.to === nodeId);
730
-
731
- const outgoing = edges.filter((e) => e.from === nodeId);
732
- const incoming = edges.filter((e) => e.to === nodeId);
733
-
734
- return { outgoing, incoming, all: edges };
735
- }
736
-
737
- // =============================================
738
- // Filtering
739
- // =============================================
740
-
741
- /**
742
- * Populate topic filter dropdown
743
- */
744
- populateTopicFilter(topics) {
745
- const select = document.getElementById('topic-filter');
746
- if (!select) {
747
- return;
748
- }
749
-
750
- select.innerHTML = '<option value="">All Topics</option>';
751
- topics.forEach((topic) => {
752
- const option = document.createElement('option');
753
- option.value = topic;
754
- option.textContent = topic;
755
- select.appendChild(option);
756
- });
757
- }
758
-
759
- /**
760
- * Filter by topic
761
- */
762
- filterByTopic(topic) {
763
- if (!this.network) {
764
- return;
765
- }
766
-
767
- const allNodes = this.network.body.data.nodes.get();
768
-
769
- if (!topic) {
770
- // Show all
771
- allNodes.forEach((node) => {
772
- this.network.body.data.nodes.update({
773
- id: node.id,
774
- hidden: false,
642
+ this.highlightConnectedNodes(nodeIdString);
643
+ }
644
+ /**
645
+ * Get connected edge types
646
+ */
647
+ getConnectedEdges(nodeId) {
648
+ const edges = this.graphData.edges.filter((e) => String(e.from) === nodeId || String(e.to) === nodeId);
649
+ const outgoing = edges.filter((e) => String(e.from) === nodeId);
650
+ const incoming = edges.filter((e) => String(e.to) === nodeId);
651
+ return { outgoing, incoming, all: edges };
652
+ }
653
+ // =============================================
654
+ // Filtering
655
+ // =============================================
656
+ /**
657
+ * Populate topic filter dropdown
658
+ */
659
+ populateTopicFilter(topics) {
660
+ const select = getElementByIdOrNull('topic-filter');
661
+ if (!select) {
662
+ return;
663
+ }
664
+ select.innerHTML = '<option value="">All Topics</option>';
665
+ topics.forEach((topic) => {
666
+ const option = document.createElement('option');
667
+ option.value = topic;
668
+ option.textContent = topic;
669
+ select.appendChild(option);
775
670
  });
776
- });
777
- } else {
778
- // Filter
779
- allNodes.forEach((node) => {
780
- const nodeData = this.graphData.nodes.find((n) => n.id === node.id);
781
- this.network.body.data.nodes.update({
782
- id: node.id,
783
- hidden: nodeData?.topic !== topic,
671
+ }
672
+ /**
673
+ * Filter by topic
674
+ */
675
+ filterByTopic(topic) {
676
+ if (!this.network) {
677
+ return;
678
+ }
679
+ const allNodes = this.network.body.data.nodes.get();
680
+ if (!topic) {
681
+ // Show all
682
+ allNodes.forEach((node) => {
683
+ this.network.body.data.nodes.update({
684
+ id: node.id,
685
+ hidden: false,
686
+ });
687
+ });
688
+ }
689
+ else {
690
+ // Filter
691
+ allNodes.forEach((node) => {
692
+ const nodeData = this.graphData.nodes.find((n) => String(n.id) === String(node.id));
693
+ this.network.body.data.nodes.update({
694
+ id: node.id,
695
+ hidden: nodeData?.topic !== topic,
696
+ });
697
+ });
698
+ }
699
+ logger.debug('[MAMA] Filtered by topic:', topic || 'all');
700
+ }
701
+ /**
702
+ * Filter by outcome
703
+ */
704
+ filterByOutcome(outcome) {
705
+ if (!this.network) {
706
+ return;
707
+ }
708
+ const allNodes = this.network.body.data.nodes.get();
709
+ if (!outcome) {
710
+ // Show all
711
+ allNodes.forEach((node) => {
712
+ this.network.body.data.nodes.update({
713
+ id: node.id,
714
+ hidden: false,
715
+ });
716
+ });
717
+ }
718
+ else {
719
+ // Filter by outcome
720
+ allNodes.forEach((node) => {
721
+ const nodeData = this.graphData.nodes.find((n) => String(n.id) === String(node.id));
722
+ const nodeOutcome = (nodeData?.outcome || 'pending').toLowerCase();
723
+ this.network.body.data.nodes.update({
724
+ id: node.id,
725
+ hidden: nodeOutcome !== outcome.toLowerCase(),
726
+ });
727
+ });
728
+ }
729
+ logger.debug('[MAMA] Filtered by outcome:', outcome || 'all');
730
+ }
731
+ /**
732
+ * Clear all filters
733
+ */
734
+ clearFilters() {
735
+ if (!this.network) {
736
+ return;
737
+ }
738
+ // Show all nodes
739
+ const allNodes = this.network.body.data.nodes.get();
740
+ allNodes.forEach((node) => {
741
+ this.network.body.data.nodes.update({
742
+ id: node.id,
743
+ hidden: false,
744
+ opacity: 1.0,
745
+ font: { ...node.font, color: '#131313' },
746
+ });
784
747
  });
785
- });
786
- }
787
-
788
- console.log('[MAMA] Filtered by topic:', topic || 'all');
789
- }
790
-
791
- /**
792
- * Filter by outcome
793
- */
794
- filterByOutcome(outcome) {
795
- if (!this.network) {
796
- return;
797
- }
798
-
799
- const allNodes = this.network.body.data.nodes.get();
800
-
801
- if (!outcome) {
802
- // Show all
803
- allNodes.forEach((node) => {
804
- this.network.body.data.nodes.update({
805
- id: node.id,
806
- hidden: false,
748
+ // Show all edges
749
+ const allEdges = this.network.body.data.edges.get();
750
+ allEdges.forEach((edge) => {
751
+ this.network.body.data.edges.update({
752
+ id: edge.id,
753
+ opacity: 1.0,
754
+ });
807
755
  });
808
- });
809
- } else {
810
- // Filter by outcome
811
- allNodes.forEach((node) => {
812
- const nodeData = this.graphData.nodes.find((n) => n.id === node.id);
813
- const nodeOutcome = (nodeData?.outcome || 'pending').toLowerCase();
814
- this.network.body.data.nodes.update({
815
- id: node.id,
816
- hidden: nodeOutcome !== outcome.toLowerCase(),
756
+ // Clear search state
757
+ this.searchMatches = [];
758
+ this.currentSearchIndex = 0;
759
+ const countEl = getElementByIdOrNull('search-count');
760
+ if (countEl) {
761
+ countEl.style.display = 'none';
762
+ }
763
+ logger.debug('[MAMA] All filters cleared');
764
+ }
765
+ // =============================================
766
+ // Search
767
+ // =============================================
768
+ /**
769
+ * Perform search
770
+ */
771
+ search() {
772
+ const queryInput = getElementByIdOrNull('search-input');
773
+ const query = queryInput ? queryInput.value.trim().toLowerCase() : '';
774
+ if (!query) {
775
+ this.clearSearch();
776
+ return;
777
+ }
778
+ // Search in topic, decision, and reasoning
779
+ this.searchMatches = this.graphData.nodes.filter((node) => (node.topic || '').toLowerCase().includes(query) ||
780
+ (node.decision || '').toLowerCase().includes(query) ||
781
+ (node.reasoning || '').toLowerCase().includes(query));
782
+ this.currentSearchIndex = 0;
783
+ this.updateSearchResults();
784
+ if (this.searchMatches.length > 0) {
785
+ this.highlightSearchResults();
786
+ this.navigateToNode(String(this.searchMatches[0].id));
787
+ }
788
+ logger.debug('[MAMA] Search:', query, '- Found', this.searchMatches.length, 'matches');
789
+ }
790
+ /**
791
+ * Handle search input
792
+ */
793
+ handleSearchInput(event) {
794
+ if (event.key === 'Enter' && this.searchMatches.length > 0) {
795
+ this.nextSearchResult();
796
+ }
797
+ else {
798
+ this.debouncedSearch();
799
+ }
800
+ }
801
+ /**
802
+ * Navigate to next search result
803
+ */
804
+ nextSearchResult() {
805
+ if (this.searchMatches.length === 0) {
806
+ return;
807
+ }
808
+ this.currentSearchIndex = (this.currentSearchIndex + 1) % this.searchMatches.length;
809
+ this.navigateToNode(this.searchMatches[this.currentSearchIndex].id);
810
+ this.updateSearchResults();
811
+ }
812
+ /**
813
+ * Navigate to previous search result
814
+ */
815
+ prevSearchResult() {
816
+ if (this.searchMatches.length === 0) {
817
+ return;
818
+ }
819
+ this.currentSearchIndex =
820
+ (this.currentSearchIndex - 1 + this.searchMatches.length) % this.searchMatches.length;
821
+ this.navigateToNode(this.searchMatches[this.currentSearchIndex].id);
822
+ this.updateSearchResults();
823
+ }
824
+ /**
825
+ * Update search count display
826
+ */
827
+ updateSearchResults() {
828
+ const countEl = getElementByIdOrNull('search-count');
829
+ if (!countEl) {
830
+ return;
831
+ }
832
+ if (this.searchMatches.length > 0) {
833
+ countEl.textContent = `${this.currentSearchIndex + 1} / ${this.searchMatches.length}`;
834
+ countEl.style.display = 'inline';
835
+ }
836
+ else {
837
+ countEl.textContent = 'No results';
838
+ countEl.style.display = 'inline';
839
+ }
840
+ }
841
+ /**
842
+ * Highlight search results
843
+ */
844
+ highlightSearchResults() {
845
+ if (!this.network) {
846
+ return;
847
+ }
848
+ const matchIds = this.searchMatches.map((n) => n.id);
849
+ const allNodes = this.network.body.data.nodes.get();
850
+ allNodes.forEach((node) => {
851
+ const isMatch = matchIds.includes(String(node.id));
852
+ this.network.body.data.nodes.update({
853
+ id: node.id,
854
+ opacity: isMatch ? 1.0 : 0.2,
855
+ font: { ...node.font, color: isMatch ? '#131313' : '#999' },
856
+ });
817
857
  });
818
- });
819
- }
820
-
821
- console.log('[MAMA] Filtered by outcome:', outcome || 'all');
822
- }
823
-
824
- /**
825
- * Clear all filters
826
- */
827
- clearFilters() {
828
- if (!this.network) {
829
- return;
830
- }
831
-
832
- // Show all nodes
833
- const allNodes = this.network.body.data.nodes.get();
834
- allNodes.forEach((node) => {
835
- this.network.body.data.nodes.update({
836
- id: node.id,
837
- hidden: false,
838
- opacity: 1.0,
839
- font: { ...node.font, color: '#131313' },
840
- });
841
- });
842
-
843
- // Show all edges
844
- const allEdges = this.network.body.data.edges.get();
845
- allEdges.forEach((edge) => {
846
- this.network.body.data.edges.update({
847
- id: edge.id,
848
- opacity: 1.0,
849
- });
850
- });
851
-
852
- // Clear search state
853
- this.searchMatches = [];
854
- this.currentSearchIndex = 0;
855
-
856
- const countEl = document.getElementById('search-count');
857
- if (countEl) {
858
- countEl.style.display = 'none';
859
- }
860
-
861
- console.log('[MAMA] All filters cleared');
862
- }
863
-
864
- // =============================================
865
- // Search
866
- // =============================================
867
-
868
- /**
869
- * Perform search
870
- */
871
- search() {
872
- const query = document.getElementById('search-input').value.trim().toLowerCase();
873
-
874
- if (!query) {
875
- this.clearSearch();
876
- return;
877
- }
878
-
879
- // Search in topic, decision, and reasoning
880
- this.searchMatches = this.graphData.nodes.filter(
881
- (node) =>
882
- (node.topic || '').toLowerCase().includes(query) ||
883
- (node.decision || '').toLowerCase().includes(query) ||
884
- (node.reasoning || '').toLowerCase().includes(query)
885
- );
886
-
887
- this.currentSearchIndex = 0;
888
- this.updateSearchResults();
889
-
890
- if (this.searchMatches.length > 0) {
891
- this.highlightSearchResults();
892
- this.navigateToNode(this.searchMatches[0].id);
893
- }
894
-
895
- console.log('[MAMA] Search:', query, '- Found', this.searchMatches.length, 'matches');
896
- }
897
-
898
- /**
899
- * Handle search input
900
- */
901
- handleSearchInput(event) {
902
- if (event.key === 'Enter' && this.searchMatches.length > 0) {
903
- this.nextSearchResult();
904
- } else {
905
- this.debouncedSearch();
906
- }
907
- }
908
-
909
- /**
910
- * Navigate to next search result
911
- */
912
- nextSearchResult() {
913
- if (this.searchMatches.length === 0) {
914
- return;
915
- }
916
-
917
- this.currentSearchIndex = (this.currentSearchIndex + 1) % this.searchMatches.length;
918
- this.navigateToNode(this.searchMatches[this.currentSearchIndex].id);
919
- this.updateSearchResults();
920
- }
921
-
922
- /**
923
- * Navigate to previous search result
924
- */
925
- prevSearchResult() {
926
- if (this.searchMatches.length === 0) {
927
- return;
928
- }
929
-
930
- this.currentSearchIndex =
931
- (this.currentSearchIndex - 1 + this.searchMatches.length) % this.searchMatches.length;
932
- this.navigateToNode(this.searchMatches[this.currentSearchIndex].id);
933
- this.updateSearchResults();
934
- }
935
-
936
- /**
937
- * Update search count display
938
- */
939
- updateSearchResults() {
940
- const countEl = document.getElementById('search-count');
941
- if (!countEl) {
942
- return;
943
- }
944
-
945
- if (this.searchMatches.length > 0) {
946
- countEl.textContent = `${this.currentSearchIndex + 1} / ${this.searchMatches.length}`;
947
- countEl.style.display = 'inline';
948
- } else {
949
- countEl.textContent = 'No results';
950
- countEl.style.display = 'inline';
951
- }
952
- }
953
-
954
- /**
955
- * Highlight search results
956
- */
957
- highlightSearchResults() {
958
- if (!this.network) {
959
- return;
960
- }
961
-
962
- const matchIds = this.searchMatches.map((n) => n.id);
963
- const allNodes = this.network.body.data.nodes.get();
964
-
965
- allNodes.forEach((node) => {
966
- const isMatch = matchIds.includes(node.id);
967
- this.network.body.data.nodes.update({
968
- id: node.id,
969
- opacity: isMatch ? 1.0 : 0.2,
970
- font: { ...node.font, color: isMatch ? '#131313' : '#999' },
971
- });
972
- });
973
- }
974
-
975
- /**
976
- * Clear search
977
- */
978
- clearSearch() {
979
- this.searchMatches = [];
980
- this.currentSearchIndex = 0;
981
- this.resetNodeHighlight();
982
-
983
- const countEl = document.getElementById('search-count');
984
- if (countEl) {
985
- countEl.style.display = 'none';
986
- }
987
- }
988
-
989
- /**
990
- * Open search panel
991
- */
992
- openSearch() {
993
- const searchContainer = document.getElementById('search-container');
994
- const searchInput = document.getElementById('search-input');
995
-
996
- searchContainer.style.display = 'flex';
997
- searchInput.focus();
998
- }
999
-
1000
- /**
1001
- * Close search panel
1002
- */
1003
- closeSearch() {
1004
- document.getElementById('search-container').style.display = 'none';
1005
- this.clearSearch();
1006
- }
858
+ }
859
+ /**
860
+ * Clear search
861
+ */
862
+ clearSearch() {
863
+ this.searchMatches = [];
864
+ this.currentSearchIndex = 0;
865
+ this.resetNodeHighlight();
866
+ const countEl = getElementByIdOrNull('search-count');
867
+ if (countEl) {
868
+ countEl.style.display = 'none';
869
+ }
870
+ }
871
+ /**
872
+ * Open search panel
873
+ */
874
+ openSearch() {
875
+ const searchContainer = getElementByIdOrNull('search-container');
876
+ const searchInput = getElementByIdOrNull('search-input');
877
+ if (!searchContainer || !searchInput) {
878
+ return;
879
+ }
880
+ searchContainer.style.display = 'flex';
881
+ searchInput.focus();
882
+ }
883
+ /**
884
+ * Close search panel
885
+ */
886
+ closeSearch() {
887
+ const searchContainer = getElementByIdOrNull('search-container');
888
+ if (!searchContainer) {
889
+ return;
890
+ }
891
+ searchContainer.style.display = 'none';
892
+ this.clearSearch();
893
+ }
1007
894
  }