laminark 2.21.6

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 (40) hide show
  1. package/.claude-plugin/marketplace.json +15 -0
  2. package/README.md +182 -0
  3. package/package.json +63 -0
  4. package/plugin/.claude-plugin/plugin.json +13 -0
  5. package/plugin/.mcp.json +12 -0
  6. package/plugin/dist/analysis/worker.d.ts +1 -0
  7. package/plugin/dist/analysis/worker.js +233 -0
  8. package/plugin/dist/analysis/worker.js.map +1 -0
  9. package/plugin/dist/config-t8LZeB-u.mjs +90 -0
  10. package/plugin/dist/config-t8LZeB-u.mjs.map +1 -0
  11. package/plugin/dist/hooks/handler.d.ts +284 -0
  12. package/plugin/dist/hooks/handler.d.ts.map +1 -0
  13. package/plugin/dist/hooks/handler.js +2125 -0
  14. package/plugin/dist/hooks/handler.js.map +1 -0
  15. package/plugin/dist/index.d.ts +445 -0
  16. package/plugin/dist/index.d.ts.map +1 -0
  17. package/plugin/dist/index.js +5831 -0
  18. package/plugin/dist/index.js.map +1 -0
  19. package/plugin/dist/observations-Ch0nc47i.d.mts +170 -0
  20. package/plugin/dist/observations-Ch0nc47i.d.mts.map +1 -0
  21. package/plugin/dist/tool-registry-CZ3mJ4iR.mjs +2655 -0
  22. package/plugin/dist/tool-registry-CZ3mJ4iR.mjs.map +1 -0
  23. package/plugin/hooks/hooks.json +78 -0
  24. package/plugin/scripts/README.md +47 -0
  25. package/plugin/scripts/bump-version.sh +44 -0
  26. package/plugin/scripts/ensure-deps.sh +12 -0
  27. package/plugin/scripts/install.sh +63 -0
  28. package/plugin/scripts/local-install.sh +103 -0
  29. package/plugin/scripts/setup-tmpdir.sh +65 -0
  30. package/plugin/scripts/uninstall.sh +95 -0
  31. package/plugin/scripts/update.sh +88 -0
  32. package/plugin/scripts/verify-install.sh +43 -0
  33. package/plugin/ui/activity.js +185 -0
  34. package/plugin/ui/app.js +1642 -0
  35. package/plugin/ui/graph.js +2333 -0
  36. package/plugin/ui/help.js +228 -0
  37. package/plugin/ui/index.html +492 -0
  38. package/plugin/ui/settings.js +650 -0
  39. package/plugin/ui/styles.css +2910 -0
  40. package/plugin/ui/timeline.js +652 -0
@@ -0,0 +1,650 @@
1
+ /**
2
+ * Laminark Settings tab — database statistics, config sections, and reset operations.
3
+ */
4
+
5
+ (function () {
6
+ var currentStats = null;
7
+
8
+ // Preset-to-multiplier mapping
9
+ var PRESET_MULTIPLIERS = { sensitive: 1.0, balanced: 1.5, relaxed: 2.5 };
10
+
11
+ function getProjectHash() {
12
+ return window.laminarkState.currentProject || null;
13
+ }
14
+
15
+ // =========================================================================
16
+ // Stats
17
+ // =========================================================================
18
+
19
+ async function fetchStats(projectHash) {
20
+ var params = new URLSearchParams();
21
+ if (projectHash) params.set('project', projectHash);
22
+ var url = '/api/admin/stats' + (params.toString() ? '?' + params.toString() : '');
23
+ try {
24
+ var res = await fetch(url);
25
+ if (!res.ok) throw new Error('HTTP ' + res.status);
26
+ return await res.json();
27
+ } catch (err) {
28
+ console.error('[laminark] Failed to fetch stats:', err);
29
+ return null;
30
+ }
31
+ }
32
+
33
+ function renderStats(stats) {
34
+ currentStats = stats;
35
+ var grid = document.getElementById('db-stats-grid');
36
+ if (!grid || !stats) return;
37
+
38
+ var cards = [
39
+ { label: 'Observations', value: stats.observations, id: 'stat-observations' },
40
+ { label: 'Embeddings', value: stats.observationEmbeddings, id: 'stat-embeddings' },
41
+ { label: 'Staleness Flags', value: stats.stalenessFlags || 0, id: 'stat-staleness' },
42
+ { label: 'Graph Nodes', value: stats.graphNodes, id: 'stat-nodes' },
43
+ { label: 'Graph Edges', value: stats.graphEdges, id: 'stat-edges' },
44
+ { label: 'Sessions', value: stats.sessions, id: 'stat-sessions' },
45
+ { label: 'Context Stashes', value: stats.contextStashes, id: 'stat-stashes' },
46
+ { label: 'Shift Decisions', value: stats.shiftDecisions, id: 'stat-shifts' },
47
+ { label: 'Notifications', value: stats.pendingNotifications || 0, id: 'stat-notifications' },
48
+ { label: 'Projects', value: stats.projects, id: 'stat-projects' },
49
+ ];
50
+
51
+ grid.innerHTML = '';
52
+ cards.forEach(function (card) {
53
+ var el = document.createElement('div');
54
+ el.className = 'stat-card';
55
+ el.id = card.id;
56
+
57
+ var valueEl = document.createElement('div');
58
+ valueEl.className = 'stat-value';
59
+ valueEl.textContent = card.value.toLocaleString();
60
+
61
+ var labelEl = document.createElement('div');
62
+ labelEl.className = 'stat-label';
63
+ labelEl.textContent = card.label;
64
+
65
+ el.appendChild(valueEl);
66
+ el.appendChild(labelEl);
67
+ grid.appendChild(el);
68
+ });
69
+ }
70
+
71
+ // =========================================================================
72
+ // Reset
73
+ // =========================================================================
74
+
75
+ async function resetData(type, scope) {
76
+ var projectHash = scope === 'current' ? getProjectHash() : undefined;
77
+ try {
78
+ var res = await fetch('/api/admin/reset', {
79
+ method: 'POST',
80
+ headers: { 'Content-Type': 'application/json' },
81
+ body: JSON.stringify({ type: type, scope: scope, projectHash: projectHash }),
82
+ });
83
+ if (!res.ok) throw new Error('HTTP ' + res.status);
84
+ return await res.json();
85
+ } catch (err) {
86
+ console.error('[laminark] Reset failed:', err);
87
+ return null;
88
+ }
89
+ }
90
+
91
+ function getResetDescription(type) {
92
+ switch (type) {
93
+ case 'observations':
94
+ return 'This will permanently delete all observations, full-text search indexes, and vector embeddings.';
95
+ case 'graph':
96
+ return 'This will permanently delete all knowledge graph nodes and edges.';
97
+ case 'sessions':
98
+ return 'This will permanently delete all sessions, context stashes, threshold history, and shift decisions.';
99
+ case 'all':
100
+ return 'This will permanently delete ALL data: observations, graph, sessions, and intelligence data.';
101
+ default:
102
+ return '';
103
+ }
104
+ }
105
+
106
+ function getAffectedCount(type) {
107
+ if (!currentStats) return 0;
108
+ var s = currentStats;
109
+ switch (type) {
110
+ case 'observations':
111
+ return s.observations + s.observationEmbeddings + (s.stalenessFlags || 0);
112
+ case 'graph':
113
+ return s.graphNodes + s.graphEdges;
114
+ case 'sessions':
115
+ return s.sessions + s.contextStashes + s.shiftDecisions + s.thresholdHistory + (s.pendingNotifications || 0);
116
+ case 'all':
117
+ return s.observations + s.observationEmbeddings + (s.stalenessFlags || 0) +
118
+ s.graphNodes + s.graphEdges +
119
+ s.sessions + s.contextStashes +
120
+ s.shiftDecisions + s.thresholdHistory + (s.pendingNotifications || 0) +
121
+ s.projects;
122
+ default:
123
+ return 0;
124
+ }
125
+ }
126
+
127
+ function showConfirmDialog(type) {
128
+ var overlay = document.getElementById('confirm-overlay');
129
+ if (!overlay) return;
130
+
131
+ var scope = getSelectedScope();
132
+ var count = getAffectedCount(type);
133
+ var scopeLabel = scope === 'current' ? 'current project' : 'ALL projects';
134
+
135
+ var title = document.getElementById('confirm-title');
136
+ var desc = document.getElementById('confirm-desc');
137
+ var countEl = document.getElementById('confirm-count');
138
+ var input = document.getElementById('confirm-input');
139
+ var confirmBtn = document.getElementById('confirm-btn');
140
+
141
+ if (title) title.textContent = 'Reset ' + type;
142
+ if (desc) desc.textContent = getResetDescription(type) + ' Scope: ' + scopeLabel + '.';
143
+ if (countEl) countEl.textContent = count.toLocaleString() + ' rows will be deleted';
144
+ if (input) {
145
+ input.value = '';
146
+ input.placeholder = 'Type DELETE to confirm';
147
+ }
148
+ if (confirmBtn) {
149
+ confirmBtn.disabled = true;
150
+ confirmBtn.onclick = async function () {
151
+ confirmBtn.disabled = true;
152
+ confirmBtn.textContent = 'Resetting...';
153
+ var result = await resetData(type, scope);
154
+ hideConfirmDialog();
155
+ if (result && result.ok) {
156
+ showSuccessMessage('Successfully reset ' + type + ' data.');
157
+ await refreshStats();
158
+ if (window.laminarkGraph && window.laminarkState.graphInitialized) {
159
+ window.laminarkGraph.loadGraphData();
160
+ }
161
+ if (window.laminarkTimeline && window.laminarkState.timelineInitialized) {
162
+ window.laminarkTimeline.loadTimelineData();
163
+ }
164
+ }
165
+ confirmBtn.textContent = 'Reset';
166
+ };
167
+ }
168
+
169
+ if (input && confirmBtn) {
170
+ input.oninput = function () {
171
+ confirmBtn.disabled = input.value !== 'DELETE';
172
+ };
173
+ }
174
+
175
+ overlay.classList.remove('hidden');
176
+ if (input) input.focus();
177
+ }
178
+
179
+ function hideConfirmDialog() {
180
+ var overlay = document.getElementById('confirm-overlay');
181
+ if (overlay) overlay.classList.add('hidden');
182
+ }
183
+
184
+ function showSuccessMessage(text) {
185
+ var msg = document.getElementById('settings-success');
186
+ if (!msg) return;
187
+ msg.textContent = text;
188
+ msg.classList.remove('hidden');
189
+ setTimeout(function () {
190
+ msg.classList.add('hidden');
191
+ }, 4000);
192
+ }
193
+
194
+ function getSelectedScope() {
195
+ var radio = document.querySelector('input[name="reset-scope"]:checked');
196
+ return radio ? radio.value : 'current';
197
+ }
198
+
199
+ async function refreshStats() {
200
+ var scope = getSelectedScope();
201
+ var projectHash = scope === 'current' ? getProjectHash() : null;
202
+ var stats = await fetchStats(projectHash);
203
+ if (stats) renderStats(stats);
204
+ }
205
+
206
+ // =========================================================================
207
+ // Config helpers
208
+ // =========================================================================
209
+
210
+ function bindSlider(sliderId, valueId) {
211
+ var slider = document.getElementById(sliderId);
212
+ var label = document.getElementById(valueId);
213
+ if (!slider || !label) return;
214
+ slider.addEventListener('input', function () {
215
+ label.textContent = parseFloat(slider.value).toFixed(2);
216
+ });
217
+ }
218
+
219
+ function updateStatusBadge(badgeId, enabled) {
220
+ var badge = document.getElementById(badgeId);
221
+ if (!badge) return;
222
+ badge.textContent = enabled ? 'Enabled' : 'Disabled';
223
+ badge.className = 'config-section-status ' + (enabled ? 'enabled' : 'disabled');
224
+ }
225
+
226
+ function setFieldsDisabled(containerId, disabled) {
227
+ var el = document.getElementById(containerId);
228
+ if (!el) return;
229
+ if (disabled) {
230
+ el.classList.add('disabled-fields');
231
+ } else {
232
+ el.classList.remove('disabled-fields');
233
+ }
234
+ }
235
+
236
+ // =========================================================================
237
+ // Topic Detection Config
238
+ // =========================================================================
239
+
240
+ function populateTopicDetection(config) {
241
+ var enabled = document.getElementById('td-enabled');
242
+ if (enabled) enabled.checked = config.enabled;
243
+ updateStatusBadge('td-status', config.enabled);
244
+ setFieldsDisabled('td-fields', !config.enabled);
245
+
246
+ // Preset radio
247
+ var presetBtns = document.querySelectorAll('#td-preset .config-radio');
248
+ presetBtns.forEach(function (btn) {
249
+ btn.classList.toggle('active', btn.getAttribute('data-value') === config.sensitivityPreset);
250
+ });
251
+
252
+ var multiplier = document.getElementById('td-multiplier');
253
+ if (multiplier) multiplier.value = config.sensitivityMultiplier;
254
+
255
+ var manualEnabled = document.getElementById('td-manual-enabled');
256
+ var manualValue = document.getElementById('td-manual-value');
257
+ if (manualEnabled) manualEnabled.checked = config.manualThreshold !== null;
258
+ if (manualValue) {
259
+ manualValue.disabled = config.manualThreshold === null;
260
+ manualValue.value = config.manualThreshold !== null ? config.manualThreshold : 0.3;
261
+ }
262
+
263
+ var ewma = document.getElementById('td-ewma');
264
+ var ewmaVal = document.getElementById('td-ewma-val');
265
+ if (ewma) ewma.value = config.ewmaAlpha;
266
+ if (ewmaVal) ewmaVal.textContent = config.ewmaAlpha.toFixed(2);
267
+
268
+ var boundsMin = document.getElementById('td-bounds-min');
269
+ var boundsMax = document.getElementById('td-bounds-max');
270
+ if (boundsMin) boundsMin.value = config.thresholdBounds.min;
271
+ if (boundsMax) boundsMax.value = config.thresholdBounds.max;
272
+ }
273
+
274
+ function gatherTopicDetection() {
275
+ var manualEnabled = document.getElementById('td-manual-enabled');
276
+ var manualValue = document.getElementById('td-manual-value');
277
+ var activePreset = document.querySelector('#td-preset .config-radio.active');
278
+
279
+ return {
280
+ enabled: document.getElementById('td-enabled').checked,
281
+ sensitivityPreset: activePreset ? activePreset.getAttribute('data-value') : 'balanced',
282
+ sensitivityMultiplier: parseFloat(document.getElementById('td-multiplier').value) || 1.5,
283
+ manualThreshold: manualEnabled && manualEnabled.checked ? (parseFloat(manualValue.value) || 0.3) : null,
284
+ ewmaAlpha: parseFloat(document.getElementById('td-ewma').value) || 0.3,
285
+ thresholdBounds: {
286
+ min: parseFloat(document.getElementById('td-bounds-min').value) || 0.15,
287
+ max: parseFloat(document.getElementById('td-bounds-max').value) || 0.6,
288
+ },
289
+ };
290
+ }
291
+
292
+ async function loadTopicDetectionConfig() {
293
+ try {
294
+ var res = await fetch('/api/admin/config/topic-detection');
295
+ if (!res.ok) throw new Error('HTTP ' + res.status);
296
+ var config = await res.json();
297
+ populateTopicDetection(config);
298
+ } catch (err) {
299
+ console.error('[laminark] Failed to load topic detection config:', err);
300
+ }
301
+ }
302
+
303
+ async function saveTopicDetectionConfig(data) {
304
+ try {
305
+ var res = await fetch('/api/admin/config/topic-detection', {
306
+ method: 'PUT',
307
+ headers: { 'Content-Type': 'application/json' },
308
+ body: JSON.stringify(data),
309
+ });
310
+ if (!res.ok) throw new Error('HTTP ' + res.status);
311
+ var config = await res.json();
312
+ populateTopicDetection(config);
313
+ return config;
314
+ } catch (err) {
315
+ console.error('[laminark] Failed to save topic detection config:', err);
316
+ return null;
317
+ }
318
+ }
319
+
320
+ function initTopicDetection() {
321
+ // Collapsible section
322
+ var header = document.querySelector('[data-config-toggle="topic-detection"]');
323
+ if (header) {
324
+ header.addEventListener('click', function () {
325
+ document.getElementById('topic-detection-section').classList.toggle('collapsed');
326
+ });
327
+ }
328
+
329
+ // Enabled toggle
330
+ var enabled = document.getElementById('td-enabled');
331
+ if (enabled) {
332
+ enabled.addEventListener('change', function () {
333
+ updateStatusBadge('td-status', enabled.checked);
334
+ setFieldsDisabled('td-fields', !enabled.checked);
335
+ });
336
+ }
337
+
338
+ // Preset buttons
339
+ var presetBtns = document.querySelectorAll('#td-preset .config-radio');
340
+ presetBtns.forEach(function (btn) {
341
+ btn.addEventListener('click', function () {
342
+ presetBtns.forEach(function (b) { b.classList.remove('active'); });
343
+ btn.classList.add('active');
344
+ var preset = btn.getAttribute('data-value');
345
+ var multiplier = document.getElementById('td-multiplier');
346
+ if (multiplier && PRESET_MULTIPLIERS[preset] !== undefined) {
347
+ multiplier.value = PRESET_MULTIPLIERS[preset];
348
+ }
349
+ });
350
+ });
351
+
352
+ // Manual threshold toggle
353
+ var manualEnabled = document.getElementById('td-manual-enabled');
354
+ var manualValue = document.getElementById('td-manual-value');
355
+ if (manualEnabled && manualValue) {
356
+ manualEnabled.addEventListener('change', function () {
357
+ manualValue.disabled = !manualEnabled.checked;
358
+ });
359
+ }
360
+
361
+ // EWMA slider
362
+ bindSlider('td-ewma', 'td-ewma-val');
363
+
364
+ // Save button
365
+ var saveBtn = document.getElementById('td-save');
366
+ if (saveBtn) {
367
+ saveBtn.addEventListener('click', async function () {
368
+ var data = gatherTopicDetection();
369
+ var result = await saveTopicDetectionConfig(data);
370
+ if (result) showSuccessMessage('Topic detection settings saved.');
371
+ });
372
+ }
373
+
374
+ // Reset to defaults
375
+ var defaultsBtn = document.getElementById('td-defaults');
376
+ if (defaultsBtn) {
377
+ var tdResetTimer = null;
378
+ defaultsBtn.addEventListener('click', async function () {
379
+ if (defaultsBtn.classList.contains('confirming')) {
380
+ clearTimeout(tdResetTimer);
381
+ defaultsBtn.classList.remove('confirming');
382
+ defaultsBtn.textContent = 'Reset to Defaults';
383
+ var result = await saveTopicDetectionConfig({ __reset: true });
384
+ if (result) showSuccessMessage('Topic detection reset to defaults.');
385
+ } else {
386
+ defaultsBtn.classList.add('confirming');
387
+ defaultsBtn.textContent = 'Confirm?';
388
+ tdResetTimer = setTimeout(function () {
389
+ defaultsBtn.classList.remove('confirming');
390
+ defaultsBtn.textContent = 'Reset to Defaults';
391
+ }, 3000);
392
+ }
393
+ });
394
+ }
395
+
396
+ loadTopicDetectionConfig();
397
+ }
398
+
399
+ // =========================================================================
400
+ // Graph Extraction Config
401
+ // =========================================================================
402
+
403
+ function populateGraphExtraction(config) {
404
+ var enabled = document.getElementById('ge-enabled');
405
+ if (enabled) enabled.checked = config.enabled;
406
+ updateStatusBadge('ge-status', config.enabled);
407
+ setFieldsDisabled('ge-fields', !config.enabled);
408
+
409
+ // Temporal decay
410
+ setVal('ge-halflife', config.temporalDecay.halfLifeDays);
411
+ setSlider('ge-minfloor', 'ge-minfloor-val', config.temporalDecay.minFloor);
412
+ setSlider('ge-delthresh', 'ge-delthresh-val', config.temporalDecay.deletionThreshold);
413
+ setVal('ge-maxage', config.temporalDecay.maxAgeDays);
414
+
415
+ // Fuzzy dedup
416
+ setVal('ge-levenshtein', config.fuzzyDedup.maxLevenshteinDistance);
417
+ setSlider('ge-jaccard', 'ge-jaccard-val', config.fuzzyDedup.jaccardThreshold);
418
+
419
+ // Quality gate
420
+ setVal('ge-maxfiles', config.qualityGate.maxFilesPerObservation);
421
+ setSlider('ge-filenonchange', 'ge-filenonchange-val', config.qualityGate.fileNonChangeMultiplier);
422
+ setVal('ge-minname', config.qualityGate.minNameLength);
423
+ setVal('ge-maxname', config.qualityGate.maxNameLength);
424
+
425
+ // Type confidence thresholds
426
+ var thresholds = config.qualityGate.typeConfidenceThresholds || {};
427
+ var grid = document.getElementById('ge-thresholds');
428
+ if (grid) {
429
+ var rows = grid.querySelectorAll('.config-threshold-row');
430
+ rows.forEach(function (row) {
431
+ var slider = row.querySelector('.config-slider');
432
+ var label = row.querySelector('.config-slider-value');
433
+ var type = slider.getAttribute('data-type');
434
+ if (type && thresholds[type] !== undefined) {
435
+ slider.value = thresholds[type];
436
+ if (label) label.textContent = thresholds[type].toFixed(2);
437
+ }
438
+ });
439
+ }
440
+
441
+ // Relationship detector
442
+ setSlider('ge-minedge', 'ge-minedge-val', config.relationshipDetector.minEdgeConfidence);
443
+
444
+ // Signal classifier
445
+ setVal('ge-mincontent', config.signalClassifier.minContentLength);
446
+ }
447
+
448
+ function setVal(id, value) {
449
+ var el = document.getElementById(id);
450
+ if (el) el.value = value;
451
+ }
452
+
453
+ function setSlider(sliderId, labelId, value) {
454
+ var slider = document.getElementById(sliderId);
455
+ var label = document.getElementById(labelId);
456
+ if (slider) slider.value = value;
457
+ if (label) label.textContent = parseFloat(value).toFixed(2);
458
+ }
459
+
460
+ function gatherGraphExtraction() {
461
+ var thresholds = {};
462
+ var grid = document.getElementById('ge-thresholds');
463
+ if (grid) {
464
+ var sliders = grid.querySelectorAll('.config-slider');
465
+ sliders.forEach(function (slider) {
466
+ var type = slider.getAttribute('data-type');
467
+ if (type) thresholds[type] = parseFloat(slider.value);
468
+ });
469
+ }
470
+
471
+ return {
472
+ enabled: document.getElementById('ge-enabled').checked,
473
+ temporalDecay: {
474
+ halfLifeDays: parseInt(document.getElementById('ge-halflife').value, 10) || 30,
475
+ minFloor: parseFloat(document.getElementById('ge-minfloor').value) || 0.05,
476
+ deletionThreshold: parseFloat(document.getElementById('ge-delthresh').value) || 0.08,
477
+ maxAgeDays: parseInt(document.getElementById('ge-maxage').value, 10) || 180,
478
+ },
479
+ fuzzyDedup: {
480
+ maxLevenshteinDistance: parseInt(document.getElementById('ge-levenshtein').value, 10) || 2,
481
+ jaccardThreshold: parseFloat(document.getElementById('ge-jaccard').value) || 0.7,
482
+ },
483
+ qualityGate: {
484
+ maxFilesPerObservation: parseInt(document.getElementById('ge-maxfiles').value, 10) || 5,
485
+ typeConfidenceThresholds: thresholds,
486
+ fileNonChangeMultiplier: parseFloat(document.getElementById('ge-filenonchange').value) || 0.74,
487
+ minNameLength: parseInt(document.getElementById('ge-minname').value, 10) || 3,
488
+ maxNameLength: parseInt(document.getElementById('ge-maxname').value, 10) || 200,
489
+ },
490
+ relationshipDetector: {
491
+ minEdgeConfidence: parseFloat(document.getElementById('ge-minedge').value) || 0.45,
492
+ },
493
+ signalClassifier: {
494
+ minContentLength: parseInt(document.getElementById('ge-mincontent').value, 10) || 30,
495
+ },
496
+ };
497
+ }
498
+
499
+ async function loadGraphExtractionConfig() {
500
+ try {
501
+ var res = await fetch('/api/admin/config/graph-extraction');
502
+ if (!res.ok) throw new Error('HTTP ' + res.status);
503
+ var config = await res.json();
504
+ populateGraphExtraction(config);
505
+ } catch (err) {
506
+ console.error('[laminark] Failed to load graph extraction config:', err);
507
+ }
508
+ }
509
+
510
+ async function saveGraphExtractionConfig(data) {
511
+ try {
512
+ var res = await fetch('/api/admin/config/graph-extraction', {
513
+ method: 'PUT',
514
+ headers: { 'Content-Type': 'application/json' },
515
+ body: JSON.stringify(data),
516
+ });
517
+ if (!res.ok) throw new Error('HTTP ' + res.status);
518
+ var config = await res.json();
519
+ populateGraphExtraction(config);
520
+ return config;
521
+ } catch (err) {
522
+ console.error('[laminark] Failed to save graph extraction config:', err);
523
+ return null;
524
+ }
525
+ }
526
+
527
+ function initGraphExtraction() {
528
+ // Collapsible section
529
+ var header = document.querySelector('[data-config-toggle="graph-extraction"]');
530
+ if (header) {
531
+ header.addEventListener('click', function () {
532
+ document.getElementById('graph-extraction-section').classList.toggle('collapsed');
533
+ });
534
+ }
535
+
536
+ // Enabled toggle
537
+ var enabled = document.getElementById('ge-enabled');
538
+ if (enabled) {
539
+ enabled.addEventListener('change', function () {
540
+ updateStatusBadge('ge-status', enabled.checked);
541
+ setFieldsDisabled('ge-fields', !enabled.checked);
542
+ });
543
+ }
544
+
545
+ // Bind all sliders
546
+ bindSlider('ge-minfloor', 'ge-minfloor-val');
547
+ bindSlider('ge-delthresh', 'ge-delthresh-val');
548
+ bindSlider('ge-jaccard', 'ge-jaccard-val');
549
+ bindSlider('ge-filenonchange', 'ge-filenonchange-val');
550
+ bindSlider('ge-minedge', 'ge-minedge-val');
551
+
552
+ // Threshold grid sliders
553
+ var grid = document.getElementById('ge-thresholds');
554
+ if (grid) {
555
+ var rows = grid.querySelectorAll('.config-threshold-row');
556
+ rows.forEach(function (row) {
557
+ var slider = row.querySelector('.config-slider');
558
+ var label = row.querySelector('.config-slider-value');
559
+ if (slider && label) {
560
+ slider.addEventListener('input', function () {
561
+ label.textContent = parseFloat(slider.value).toFixed(2);
562
+ });
563
+ }
564
+ });
565
+ }
566
+
567
+ // Save button
568
+ var saveBtn = document.getElementById('ge-save');
569
+ if (saveBtn) {
570
+ saveBtn.addEventListener('click', async function () {
571
+ var data = gatherGraphExtraction();
572
+ var result = await saveGraphExtractionConfig(data);
573
+ if (result) showSuccessMessage('Graph extraction settings saved.');
574
+ });
575
+ }
576
+
577
+ // Reset to defaults
578
+ var defaultsBtn = document.getElementById('ge-defaults');
579
+ if (defaultsBtn) {
580
+ var geResetTimer = null;
581
+ defaultsBtn.addEventListener('click', async function () {
582
+ if (defaultsBtn.classList.contains('confirming')) {
583
+ clearTimeout(geResetTimer);
584
+ defaultsBtn.classList.remove('confirming');
585
+ defaultsBtn.textContent = 'Reset to Defaults';
586
+ var result = await saveGraphExtractionConfig({ __reset: true });
587
+ if (result) showSuccessMessage('Graph extraction reset to defaults.');
588
+ } else {
589
+ defaultsBtn.classList.add('confirming');
590
+ defaultsBtn.textContent = 'Confirm?';
591
+ geResetTimer = setTimeout(function () {
592
+ defaultsBtn.classList.remove('confirming');
593
+ defaultsBtn.textContent = 'Reset to Defaults';
594
+ }, 3000);
595
+ }
596
+ });
597
+ }
598
+
599
+ loadGraphExtractionConfig();
600
+ }
601
+
602
+ // =========================================================================
603
+ // Init
604
+ // =========================================================================
605
+
606
+ function initSettings() {
607
+ // Reset action buttons
608
+ var resetBtns = document.querySelectorAll('.reset-action-btn');
609
+ resetBtns.forEach(function (btn) {
610
+ btn.addEventListener('click', function () {
611
+ var type = btn.getAttribute('data-reset-type');
612
+ if (type) showConfirmDialog(type);
613
+ });
614
+ });
615
+
616
+ // Cancel button in confirm dialog
617
+ var cancelBtn = document.getElementById('confirm-cancel');
618
+ if (cancelBtn) {
619
+ cancelBtn.addEventListener('click', hideConfirmDialog);
620
+ }
621
+
622
+ // Close on overlay click
623
+ var overlay = document.getElementById('confirm-overlay');
624
+ if (overlay) {
625
+ overlay.addEventListener('click', function (e) {
626
+ if (e.target === overlay) hideConfirmDialog();
627
+ });
628
+ }
629
+
630
+ // Escape key closes dialog
631
+ document.addEventListener('keydown', function (e) {
632
+ if (e.key === 'Escape') hideConfirmDialog();
633
+ });
634
+
635
+ // Scope radio change refreshes stats
636
+ var radios = document.querySelectorAll('input[name="reset-scope"]');
637
+ radios.forEach(function (radio) {
638
+ radio.addEventListener('change', refreshStats);
639
+ });
640
+
641
+ // Config sections
642
+ initTopicDetection();
643
+ initGraphExtraction();
644
+ }
645
+
646
+ window.laminarkSettings = {
647
+ initSettings: initSettings,
648
+ refreshStats: refreshStats,
649
+ };
650
+ })();