@kweaver-ai/kweaver-sdk 0.8.3 → 0.8.4

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 (74) hide show
  1. package/dist/api/agent-chat.d.ts +10 -2
  2. package/dist/api/agent-chat.js +19 -5
  3. package/dist/api/datasources.d.ts +14 -0
  4. package/dist/api/datasources.js +14 -0
  5. package/dist/cli.js +2 -14
  6. package/dist/client.d.ts +7 -1
  7. package/dist/client.js +7 -1
  8. package/dist/commands/bkn-ops.d.ts +1 -1
  9. package/dist/commands/bkn-ops.js +42 -21
  10. package/dist/commands/bkn.js +6 -3
  11. package/dist/commands/ds.d.ts +0 -31
  12. package/dist/commands/ds.js +18 -448
  13. package/dist/commands/explore-bkn.d.ts +7 -1
  14. package/dist/commands/explore-bkn.js +32 -3
  15. package/dist/resources/datasources.d.ts +7 -0
  16. package/dist/resources/datasources.js +7 -0
  17. package/dist/templates/explorer/bkn.js +860 -9
  18. package/dist/templates/explorer/index.html +1 -0
  19. package/dist/templates/explorer/style.css +225 -0
  20. package/dist/templates/explorer/vendor/g6.min.js +68 -0
  21. package/dist/trace-ai/eval-set/schemas.d.ts +1 -0
  22. package/dist/trace-ai/eval-set/schemas.js +4 -0
  23. package/dist/trace-ai/eval-set/types.d.ts +2 -0
  24. package/dist/trace-ai/exp/capture-fingerprint.d.ts +10 -0
  25. package/dist/trace-ai/exp/capture-fingerprint.js +12 -0
  26. package/dist/trace-ai/exp/context/context-assembler.d.ts +18 -0
  27. package/dist/trace-ai/exp/context/context-assembler.js +42 -0
  28. package/dist/trace-ai/exp/context/failure-analyzer.d.ts +22 -0
  29. package/dist/trace-ai/exp/context/failure-analyzer.js +59 -0
  30. package/dist/trace-ai/exp/context/kn-data-prober.d.ts +13 -0
  31. package/dist/trace-ai/exp/context/kn-data-prober.js +38 -0
  32. package/dist/trace-ai/exp/context/kn-schema-client.d.ts +14 -0
  33. package/dist/trace-ai/exp/context/kn-schema-client.js +41 -0
  34. package/dist/trace-ai/exp/context/retrieval-health.d.ts +32 -0
  35. package/dist/trace-ai/exp/context/retrieval-health.js +138 -0
  36. package/dist/trace-ai/exp/context/vega-catalog-client.d.ts +14 -0
  37. package/dist/trace-ai/exp/context/vega-catalog-client.js +15 -0
  38. package/dist/trace-ai/exp/coordinator.d.ts +34 -21
  39. package/dist/trace-ai/exp/coordinator.js +246 -24
  40. package/dist/trace-ai/exp/eval-runner.js +4 -2
  41. package/dist/trace-ai/exp/exp-store/events-jsonl.d.ts +1 -0
  42. package/dist/trace-ai/exp/exp-store/events-jsonl.js +18 -0
  43. package/dist/trace-ai/exp/exp-store/expected-fingerprint.d.ts +3 -0
  44. package/dist/trace-ai/exp/exp-store/expected-fingerprint.js +31 -0
  45. package/dist/trace-ai/exp/exp-store/index.d.ts +63 -2
  46. package/dist/trace-ai/exp/exp-store/index.js +2 -1
  47. package/dist/trace-ai/exp/exp-store/rollback-yaml.d.ts +12 -0
  48. package/dist/trace-ai/exp/exp-store/rollback-yaml.js +29 -0
  49. package/dist/trace-ai/exp/index.d.ts +2 -0
  50. package/dist/trace-ai/exp/index.js +68 -3
  51. package/dist/trace-ai/exp/info.js +1 -1
  52. package/dist/trace-ai/exp/patch/index.d.ts +13 -2
  53. package/dist/trace-ai/exp/patch/index.js +65 -10
  54. package/dist/trace-ai/exp/patch/kn-api-client.d.ts +40 -0
  55. package/dist/trace-ai/exp/patch/kn-api-client.js +14 -0
  56. package/dist/trace-ai/exp/patch/kn.d.ts +8 -0
  57. package/dist/trace-ai/exp/patch/kn.js +36 -0
  58. package/dist/trace-ai/exp/patch/skill-api-client.d.ts +17 -0
  59. package/dist/trace-ai/exp/patch/skill-api-client.js +14 -0
  60. package/dist/trace-ai/exp/patch/skill-content.d.ts +9 -0
  61. package/dist/trace-ai/exp/patch/skill-content.js +12 -0
  62. package/dist/trace-ai/exp/preflight.d.ts +77 -0
  63. package/dist/trace-ai/exp/preflight.js +148 -0
  64. package/dist/trace-ai/exp/providers/synthesizer-client.d.ts +3 -14
  65. package/dist/trace-ai/exp/providers/synthesizer-client.js +53 -35
  66. package/dist/trace-ai/exp/providers/triage-client.d.ts +15 -2
  67. package/dist/trace-ai/exp/providers/triage-client.js +143 -28
  68. package/dist/trace-ai/exp/run-preflight.d.ts +19 -0
  69. package/dist/trace-ai/exp/run-preflight.js +56 -0
  70. package/dist/trace-ai/exp/schemas.d.ts +402 -44
  71. package/dist/trace-ai/exp/schemas.js +131 -18
  72. package/dist/utils/deprecation.d.ts +1 -0
  73. package/dist/utils/deprecation.js +18 -0
  74. package/package.json +2 -1
@@ -197,15 +197,7 @@ function renderBknHome($el, knId) {
197
197
  '<button id="bkn-search-btn">Search</button>' +
198
198
  '</div>' +
199
199
 
200
- '<h2 style="font-size:18px; margin-bottom:16px;">Object Types</h2>' +
201
- '<div class="ot-grid">' +
202
- m.objectTypes.map(function(ot) {
203
- return '<a href="#/bkn/' + enc(knId) + '/ot/' + enc(ot.id) + '" class="ot-card" style="text-decoration:none;color:inherit;">' +
204
- '<h3>' + esc(ot.name) + '</h3>' +
205
- '<div class="meta">' + ot.propertyCount + ' properties</div>' +
206
- '</a>';
207
- }).join("") +
208
- '</div>' +
200
+ renderOtSection(knId, m) +
209
201
 
210
202
  (rtCount > 0 ? (
211
203
  '<h2 style="font-size:18px; margin:24px 0 16px;">Relation Types</h2>' +
@@ -219,6 +211,8 @@ function renderBknHome($el, knId) {
219
211
  '</div>'
220
212
  ) : '');
221
213
 
214
+ mountOtView(knId, m);
215
+
222
216
  // Bind search
223
217
  var searchInput = document.getElementById("bkn-search-input");
224
218
  var searchBtn = document.getElementById("bkn-search-btn");
@@ -232,6 +226,863 @@ function renderBknHome($el, knId) {
232
226
  }
233
227
  }
234
228
 
229
+ // \u2500\u2500 Object Types section (multi-view: card / list / graph) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
230
+
231
+ var OT_VIEW_KEY = "kweaver.bkn.otView";
232
+ var OT_LIST_SORT = { key: "name", dir: "asc" };
233
+ var __g6Instance = null;
234
+
235
+ function getOtView() {
236
+ try { return localStorage.getItem(OT_VIEW_KEY) || "card"; }
237
+ catch (_) { return "card"; }
238
+ }
239
+ function setOtView(v) {
240
+ try { localStorage.setItem(OT_VIEW_KEY, v); } catch (_) {}
241
+ }
242
+
243
+ var VIEW_ICONS = {
244
+ card: '<svg viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><rect x="1" y="1" width="6" height="6" rx="1"/><rect x="9" y="1" width="6" height="6" rx="1"/><rect x="1" y="9" width="6" height="6" rx="1"/><rect x="9" y="9" width="6" height="6" rx="1"/></svg>',
245
+ list: '<svg viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><rect x="1" y="2" width="14" height="2" rx="0.5"/><rect x="1" y="7" width="14" height="2" rx="0.5"/><rect x="1" y="12" width="14" height="2" rx="0.5"/></svg>',
246
+ graph: '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><circle cx="3" cy="4" r="2"/><circle cx="13" cy="4" r="2"/><circle cx="8" cy="13" r="2"/><line x1="3" y1="4" x2="13" y2="4"/><line x1="3" y1="4" x2="8" y2="13"/><line x1="13" y1="4" x2="8" y2="13"/></svg>',
247
+ };
248
+
249
+ function enrichObjectTypes(m) {
250
+ var rtCounts = {};
251
+ m.relationTypes.forEach(function(rt) {
252
+ rtCounts[rt.sourceOtId] = (rtCounts[rt.sourceOtId] || 0) + 1;
253
+ if (rt.targetOtId !== rt.sourceOtId) {
254
+ rtCounts[rt.targetOtId] = (rtCounts[rt.targetOtId] || 0) + 1;
255
+ }
256
+ });
257
+ return m.objectTypes.map(function(ot) {
258
+ return {
259
+ id: ot.id,
260
+ name: ot.name,
261
+ propertyCount: ot.propertyCount,
262
+ relCount: rtCounts[ot.id] || 0,
263
+ dsType: (ot.dataSource && ot.dataSource.type) || "\u2014",
264
+ dsName: (ot.dataSource && ot.dataSource.name) || "",
265
+ };
266
+ });
267
+ }
268
+
269
+ function renderViewSwitcher(current) {
270
+ return '<div class="view-switcher" role="tablist" aria-label="Object Types view mode">' +
271
+ ["card", "list", "graph"].map(function(v) {
272
+ var sel = v === current;
273
+ var label = v.charAt(0).toUpperCase() + v.slice(1);
274
+ return '<button type="button" role="tab" aria-selected="' + sel + '" ' +
275
+ 'data-ot-view="' + v + '" title="' + label + ' view">' +
276
+ VIEW_ICONS[v] + '<span>' + label + '</span></button>';
277
+ }).join("") +
278
+ '</div>';
279
+ }
280
+
281
+ function renderOtSection(knId, m) {
282
+ var view = getOtView();
283
+ return '<div class="section-header">' +
284
+ '<h2>Object Types</h2>' +
285
+ '<div class="header-tools">' + renderViewSwitcher(view) + '</div>' +
286
+ '</div>' +
287
+ '<div id="ot-view-host"></div>';
288
+ }
289
+
290
+ function mountOtView(knId, m) {
291
+ var view = getOtView();
292
+ var ots = enrichObjectTypes(m);
293
+ var host = document.getElementById("ot-view-host");
294
+ if (!host) return;
295
+
296
+ // Destroy any prior G6 instance when leaving graph view
297
+ if (__g6Instance && view !== "graph") {
298
+ try { __g6Instance.destroy(); } catch (_) {}
299
+ __g6Instance = null;
300
+ }
301
+
302
+ if (view === "card") {
303
+ host.innerHTML = renderOtCardView(knId, ots);
304
+ } else if (view === "list") {
305
+ host.innerHTML = renderOtListView(knId, ots);
306
+ bindOtListEvents(knId, m);
307
+ } else if (view === "graph") {
308
+ host.innerHTML = renderOtGraphShell();
309
+ renderOtGraphView(knId, ots, m.relationTypes, m.conceptGroups || []);
310
+ }
311
+
312
+ // Bind switcher (idempotent re-bind)
313
+ var buttons = document.querySelectorAll(".view-switcher [data-ot-view]");
314
+ buttons.forEach(function(btn) {
315
+ btn.onclick = function() {
316
+ var v = btn.getAttribute("data-ot-view");
317
+ if (v === getOtView()) return;
318
+ setOtView(v);
319
+ buttons.forEach(function(b) {
320
+ b.setAttribute("aria-selected", b === btn ? "true" : "false");
321
+ });
322
+ mountOtView(knId, m);
323
+ };
324
+ btn.onkeydown = function(e) {
325
+ if (e.key !== "ArrowLeft" && e.key !== "ArrowRight") return;
326
+ e.preventDefault();
327
+ var order = ["card", "list", "graph"];
328
+ var idx = order.indexOf(getOtView());
329
+ var next = e.key === "ArrowRight" ? (idx + 1) % 3 : (idx + 2) % 3;
330
+ setOtView(order[next]);
331
+ buttons.forEach(function(b) {
332
+ b.setAttribute("aria-selected", b.getAttribute("data-ot-view") === order[next] ? "true" : "false");
333
+ });
334
+ var nextBtn = document.querySelector('[data-ot-view="' + order[next] + '"]');
335
+ if (nextBtn) nextBtn.focus();
336
+ mountOtView(knId, m);
337
+ };
338
+ });
339
+ }
340
+
341
+ // \u2500\u2500 Card view \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
342
+ function renderOtCardView(knId, ots) {
343
+ return '<div class="ot-grid">' +
344
+ ots.map(function(ot) {
345
+ var badge = ot.relCount > 0
346
+ ? '<span class="ot-badge" title="' + ot.relCount + ' relations">' + ot.relCount + '</span>'
347
+ : '';
348
+ var chipCls = ot.dsType === "resource" ? "chip chip-resource" : "chip";
349
+ return '<a href="#/bkn/' + enc(knId) + '/ot/' + enc(ot.id) + '" class="ot-card" style="text-decoration:none;color:inherit;">' +
350
+ badge +
351
+ '<h3>' + esc(ot.name) + '</h3>' +
352
+ '<div class="meta">' + ot.propertyCount + ' properties</div>' +
353
+ '<div class="ot-chips"><span class="' + chipCls + '">' + esc(ot.dsType) + '</span></div>' +
354
+ '</a>';
355
+ }).join("") +
356
+ '</div>';
357
+ }
358
+
359
+ // \u2500\u2500 List view \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
360
+ function renderOtListView(knId, ots) {
361
+ var sorted = ots.slice().sort(function(a, b) {
362
+ var k = OT_LIST_SORT.key, va = a[k], vb = b[k];
363
+ var cmp;
364
+ if (typeof va === "string") cmp = va.localeCompare(vb);
365
+ else cmp = (va || 0) - (vb || 0);
366
+ return OT_LIST_SORT.dir === "asc" ? cmp : -cmp;
367
+ });
368
+
369
+ function th(label, key, cls) {
370
+ var sort = OT_LIST_SORT.key === key
371
+ ? (OT_LIST_SORT.dir === "asc" ? "ascending" : "descending")
372
+ : "none";
373
+ return '<th data-sort-key="' + key + '" aria-sort="' + sort + '"' +
374
+ (cls ? ' class="' + cls + '"' : '') + '>' + label + '</th>';
375
+ }
376
+
377
+ return '<div class="ot-list-wrap"><table class="ot-list-table">' +
378
+ '<thead><tr>' +
379
+ th("Name", "name") +
380
+ th("Properties", "propertyCount", "num") +
381
+ th("Data Source", "dsType") +
382
+ th("Relations", "relCount", "num") +
383
+ '<th class="row-arrow" aria-hidden="true"></th>' +
384
+ '</tr></thead>' +
385
+ '<tbody>' +
386
+ sorted.map(function(ot) {
387
+ var chipCls = ot.dsType === "resource" ? "chip chip-resource" : "chip";
388
+ var dsDisplay = ot.dsName ? esc(ot.dsType) + ' \u00b7 ' + esc(ot.dsName) : esc(ot.dsType);
389
+ return '<tr data-href="/bkn/' + enc(knId) + '/ot/' + enc(ot.id) + '" tabindex="0">' +
390
+ '<td class="name-cell">' + esc(ot.name) + '</td>' +
391
+ '<td class="num">' + ot.propertyCount + '</td>' +
392
+ '<td><span class="' + chipCls + '">' + esc(ot.dsType) + '</span>' +
393
+ (ot.dsName ? ' <span style="color:var(--text-secondary); font-size:12px;">' + esc(ot.dsName) + '</span>' : '') +
394
+ '</td>' +
395
+ '<td class="num">' + (ot.relCount || "\u2014") + '</td>' +
396
+ '<td class="row-arrow">\u2192</td>' +
397
+ '</tr>';
398
+ }).join("") +
399
+ '</tbody>' +
400
+ '</table></div>';
401
+ }
402
+
403
+ function bindOtListEvents(knId, m) {
404
+ // Row click / Enter \u2192 navigate
405
+ document.querySelectorAll(".ot-list-table tbody tr").forEach(function(tr) {
406
+ var go = function() { location.hash = tr.getAttribute("data-href"); };
407
+ tr.addEventListener("click", go);
408
+ tr.addEventListener("keydown", function(e) { if (e.key === "Enter") go(); });
409
+ });
410
+ // Header click \u2192 sort
411
+ document.querySelectorAll(".ot-list-table thead th[data-sort-key]").forEach(function(th) {
412
+ th.addEventListener("click", function() {
413
+ var k = th.getAttribute("data-sort-key");
414
+ if (OT_LIST_SORT.key === k) {
415
+ OT_LIST_SORT.dir = OT_LIST_SORT.dir === "asc" ? "desc" : "asc";
416
+ } else {
417
+ OT_LIST_SORT.key = k;
418
+ OT_LIST_SORT.dir = "asc";
419
+ }
420
+ mountOtView(knId, m);
421
+ });
422
+ });
423
+ }
424
+
425
+ // \u2500\u2500 Graph view (AntV G6 v5) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
426
+ function renderOtGraphShell() {
427
+ var current = getOtGraphLayout();
428
+ var grouped = getOtGraphGroup();
429
+ var currentPal = getOtGraphPalette();
430
+ var layoutOpts = [
431
+ { v: "d3-force", l: "Force" },
432
+ { v: "radial", l: "Radial" },
433
+ { v: "concentric", l: "Concentric" },
434
+ { v: "circular", l: "Circular" },
435
+ { v: "dagre", l: "Dagre (hierarchy)" },
436
+ { v: "grid", l: "Grid" },
437
+ ];
438
+ var palOpts = [
439
+ { v: "ocean", l: "Ocean (blue · default)" },
440
+ { v: "forest", l: "Forest (green)" },
441
+ { v: "sunset", l: "Sunset (warm)" },
442
+ { v: "pastel", l: "Pastel (soft)" },
443
+ { v: "bold", l: "Bold (saturated)" },
444
+ { v: "mono", l: "Mono (slate)" },
445
+ ];
446
+ return '<div class="ot-graph-wrap">' +
447
+ '<div class="ot-graph-toolbar">' +
448
+ '<label style="display:inline-flex;align-items:center;gap:6px;font-size:12px;color:var(--text-secondary);cursor:pointer;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-sm);padding:6px 10px;box-shadow:var(--shadow-sm);" title="Group by concept-groups defined in the BKN schema">' +
449
+ '<input type="checkbox" id="ot-graph-group"' + (grouped ? " checked" : "") + ' style="margin:0;cursor:pointer;"> Group' +
450
+ '</label>' +
451
+ '<select id="ot-graph-palette" title="Color palette (applies to whole graph)">' +
452
+ palOpts.map(function(o) {
453
+ return '<option value="' + o.v + '"' + (o.v === currentPal ? " selected" : "") + '>' + o.l + '</option>';
454
+ }).join("") +
455
+ '</select>' +
456
+ '<select id="ot-graph-layout" title="Layout algorithm (disabled when Group is on)"' + (grouped ? " disabled" : "") + '>' +
457
+ layoutOpts.map(function(o) {
458
+ return '<option value="' + o.v + '"' + (o.v === current ? " selected" : "") + '>' + o.l + '</option>';
459
+ }).join("") +
460
+ '</select>' +
461
+ '<button type="button" data-graph-action="fit" title="Fit view">Fit</button>' +
462
+ '<button type="button" data-graph-action="export" title="Export PNG (3x for slides)">Export PNG</button>' +
463
+ '</div>' +
464
+ '<div id="ot-graph-host" class="ot-graph-host"></div>' +
465
+ '</div>';
466
+ }
467
+
468
+ var OT_GRAPH_LAYOUT_KEY = "kweaver.bkn.otGraphLayout";
469
+ var OT_GRAPH_GROUP_KEY = "kweaver.bkn.otGraphGroup";
470
+ function getOtGraphLayout() {
471
+ try { return localStorage.getItem(OT_GRAPH_LAYOUT_KEY) || "d3-force"; }
472
+ catch (_) { return "d3-force"; }
473
+ }
474
+ function setOtGraphLayout(v) {
475
+ try { localStorage.setItem(OT_GRAPH_LAYOUT_KEY, v); } catch (_) {}
476
+ }
477
+ function getOtGraphGroup() {
478
+ try { return localStorage.getItem(OT_GRAPH_GROUP_KEY) !== "off"; }
479
+ catch (_) { return true; }
480
+ }
481
+ function setOtGraphGroup(on) {
482
+ try { localStorage.setItem(OT_GRAPH_GROUP_KEY, on ? "on" : "off"); } catch (_) {}
483
+ }
484
+
485
+ // Named palettes — picked via Palette dropdown. Each entry has 8 hues.
486
+ // First entry is the primary/accent that drives the whole graph theme.
487
+ var GROUP_PALETTES = {
488
+ ocean: [
489
+ { strong: "#0ea5e9", light: "#f0f9ff", darkLight: "rgba(14,165,233,0.20)" }, // sky
490
+ { strong: "#06b6d4", light: "#ecfeff", darkLight: "rgba(6,182,212,0.20)" }, // cyan
491
+ { strong: "#3b82f6", light: "#eff6ff", darkLight: "rgba(59,130,246,0.20)" }, // blue
492
+ { strong: "#6366f1", light: "#eef2ff", darkLight: "rgba(99,102,241,0.20)" }, // indigo
493
+ { strong: "#14b8a6", light: "#f0fdfa", darkLight: "rgba(20,184,166,0.20)" }, // teal
494
+ { strong: "#0d9488", light: "#ccfbf1", darkLight: "rgba(13,148,136,0.20)" }, // dark teal
495
+ { strong: "#1e40af", light: "#dbeafe", darkLight: "rgba(30,64,175,0.20)" }, // dark blue
496
+ { strong: "#4f46e5", light: "#e0e7ff", darkLight: "rgba(79,70,229,0.20)" }, // dark indigo
497
+ ],
498
+ forest: [
499
+ { strong: "#16a34a", light: "#f0fdf4", darkLight: "rgba(22,163,74,0.20)" }, // green
500
+ { strong: "#65a30d", light: "#f7fee7", darkLight: "rgba(101,163,13,0.20)" }, // lime
501
+ { strong: "#15803d", light: "#dcfce7", darkLight: "rgba(21,128,61,0.20)" }, // dark green
502
+ { strong: "#0d9488", light: "#ccfbf1", darkLight: "rgba(13,148,136,0.20)" }, // teal
503
+ { strong: "#84cc16", light: "#ecfccb", darkLight: "rgba(132,204,22,0.20)" }, // light lime
504
+ { strong: "#059669", light: "#d1fae5", darkLight: "rgba(5,150,105,0.20)" }, // emerald
505
+ { strong: "#365314", light: "#ecfccb", darkLight: "rgba(54,83,20,0.20)" }, // dark lime
506
+ { strong: "#047857", light: "#a7f3d0", darkLight: "rgba(4,120,87,0.20)" }, // dark emerald
507
+ ],
508
+ sunset: [
509
+ { strong: "#f97316", light: "#fff7ed", darkLight: "rgba(249,115,22,0.20)" }, // orange
510
+ { strong: "#ef4444", light: "#fef2f2", darkLight: "rgba(239,68,68,0.20)" }, // red
511
+ { strong: "#f59e0b", light: "#fffbeb", darkLight: "rgba(245,158,11,0.20)" }, // amber
512
+ { strong: "#ec4899", light: "#fdf2f8", darkLight: "rgba(236,72,153,0.20)" }, // pink
513
+ { strong: "#dc2626", light: "#fee2e2", darkLight: "rgba(220,38,38,0.20)" }, // dark red
514
+ { strong: "#ea580c", light: "#ffedd5", darkLight: "rgba(234,88,12,0.20)" }, // dark orange
515
+ { strong: "#be123c", light: "#ffe4e6", darkLight: "rgba(190,18,67,0.20)" }, // rose
516
+ { strong: "#b91c1c", light: "#fee2e2", darkLight: "rgba(185,28,28,0.20)" }, // crimson
517
+ ],
518
+ pastel: [
519
+ { strong: "#f9a8d4", light: "#fdf2f8", darkLight: "rgba(249,168,212,0.22)" }, // pink
520
+ { strong: "#a5b4fc", light: "#eef2ff", darkLight: "rgba(165,180,252,0.22)" }, // indigo
521
+ { strong: "#86efac", light: "#f0fdf4", darkLight: "rgba(134,239,172,0.22)" }, // green
522
+ { strong: "#fcd34d", light: "#fffbeb", darkLight: "rgba(252,211,77,0.22)" }, // amber
523
+ { strong: "#c4b5fd", light: "#f5f3ff", darkLight: "rgba(196,181,253,0.22)" }, // violet
524
+ { strong: "#7dd3fc", light: "#f0f9ff", darkLight: "rgba(125,211,252,0.22)" }, // sky
525
+ { strong: "#fca5a5", light: "#fef2f2", darkLight: "rgba(252,165,165,0.22)" }, // red
526
+ { strong: "#5eead4", light: "#f0fdfa", darkLight: "rgba(94,234,212,0.22)" }, // teal
527
+ ],
528
+ bold: [
529
+ { strong: "#6d28d9", light: "#ede9fe", darkLight: "rgba(109,40,217,0.25)" }, // violet
530
+ { strong: "#1d4ed8", light: "#dbeafe", darkLight: "rgba(29,78,216,0.25)" }, // blue
531
+ { strong: "#047857", light: "#d1fae5", darkLight: "rgba(4,120,87,0.25)" }, // emerald
532
+ { strong: "#b45309", light: "#fef3c7", darkLight: "rgba(180,83,9,0.25)" }, // amber
533
+ { strong: "#be185d", light: "#fce7f3", darkLight: "rgba(190,24,93,0.25)" }, // pink
534
+ { strong: "#0f766e", light: "#ccfbf1", darkLight: "rgba(15,118,110,0.25)" }, // teal
535
+ { strong: "#b91c1c", light: "#fee2e2", darkLight: "rgba(185,28,28,0.25)" }, // red
536
+ { strong: "#4338ca", light: "#e0e7ff", darkLight: "rgba(67,56,202,0.25)" }, // indigo
537
+ ],
538
+ mono: [
539
+ { strong: "#475569", light: "#f1f5f9", darkLight: "rgba(71,85,105,0.22)" }, // slate
540
+ { strong: "#64748b", light: "#f8fafc", darkLight: "rgba(100,116,139,0.22)" },
541
+ { strong: "#94a3b8", light: "#f1f5f9", darkLight: "rgba(148,163,184,0.22)" },
542
+ { strong: "#475569", light: "#e2e8f0", darkLight: "rgba(71,85,105,0.22)" },
543
+ { strong: "#334155", light: "#cbd5e1", darkLight: "rgba(51,65,85,0.22)" },
544
+ { strong: "#1e293b", light: "#f1f5f9", darkLight: "rgba(30,41,59,0.22)" },
545
+ { strong: "#0f172a", light: "#e2e8f0", darkLight: "rgba(15,23,42,0.22)" },
546
+ { strong: "#64748b", light: "#f1f5f9", darkLight: "rgba(100,116,139,0.22)" },
547
+ ],
548
+ };
549
+
550
+ var OT_GRAPH_PALETTE_KEY = "kweaver.bkn.otGraphPalette";
551
+ function getOtGraphPalette() {
552
+ try { return localStorage.getItem(OT_GRAPH_PALETTE_KEY) || "ocean"; }
553
+ catch (_) { return "ocean"; }
554
+ }
555
+ function setOtGraphPalette(v) {
556
+ try { localStorage.setItem(OT_GRAPH_PALETTE_KEY, v); } catch (_) {}
557
+ }
558
+ function getActivePalette() {
559
+ return GROUP_PALETTES[getOtGraphPalette()] || GROUP_PALETTES.ocean;
560
+ }
561
+
562
+ // Normalize a backend-supplied color (e.g. "#ff8800") into our paired format.
563
+ function normalizePaletteEntry(hex) {
564
+ // Convert hex → rgba light + a darkLight variant
565
+ if (!hex || !/^#[0-9a-f]{6}$/i.test(hex)) return null;
566
+ var r = parseInt(hex.slice(1, 3), 16);
567
+ var g = parseInt(hex.slice(3, 5), 16);
568
+ var b = parseInt(hex.slice(5, 7), 16);
569
+ return {
570
+ strong: hex,
571
+ light: "rgba(" + r + "," + g + "," + b + ",0.08)",
572
+ darkLight: "rgba(" + r + "," + g + "," + b + ",0.22)",
573
+ };
574
+ }
575
+
576
+ // Group OTs by BKN concept-groups (the business-level concept defined in the schema).
577
+ // Returns null if the BKN has no concept-groups defined.
578
+ function buildConceptGroupCombos(ots, conceptGroups) {
579
+ if (!conceptGroups || !conceptGroups.length) return null;
580
+
581
+ var otIds = {};
582
+ ots.forEach(function(ot) { otIds[ot.id] = true; });
583
+
584
+ var ownerByNode = {};
585
+ conceptGroups.forEach(function(cg) {
586
+ (cg.objectTypeIds || []).forEach(function(otId) {
587
+ if (otIds[otId] && !(otId in ownerByNode)) {
588
+ // First-wins: if OT belongs to multiple groups, pick first
589
+ ownerByNode[otId] = cg.id;
590
+ }
591
+ });
592
+ });
593
+
594
+ // Build combo list — only include groups that own at least one OT in this BKN
595
+ var members = {};
596
+ Object.keys(ownerByNode).forEach(function(id) {
597
+ var g = ownerByNode[id];
598
+ members[g] = (members[g] || 0) + 1;
599
+ });
600
+ var palette = getActivePalette();
601
+ var visibleGroups = conceptGroups.filter(function(cg) { return members[cg.id] > 0; });
602
+ var combos = visibleGroups.map(function(cg, idx) {
603
+ // User palette choice always wins; backend color ignored
604
+ var pal = palette[idx % palette.length];
605
+ return {
606
+ id: "combo-" + cg.id,
607
+ data: {
608
+ label: cg.name + " · " + members[cg.id],
609
+ color: pal.strong,
610
+ fill: pal.light,
611
+ fillDark: pal.darkLight,
612
+ },
613
+ };
614
+ });
615
+
616
+ // Orphan OTs (not in any concept-group): collect into a synthetic combo
617
+ var orphans = ots.filter(function(ot) { return !(ot.id in ownerByNode); });
618
+ if (orphans.length > 0) {
619
+ var orphanId = "__ungrouped__";
620
+ orphans.forEach(function(ot) { ownerByNode[ot.id] = orphanId; });
621
+ combos.push({
622
+ id: "combo-" + orphanId,
623
+ data: {
624
+ label: "未分组 · " + orphans.length,
625
+ color: "#94a3b8",
626
+ fill: "rgba(148,163,184,0.05)",
627
+ fillDark: "rgba(148,163,184,0.12)",
628
+ },
629
+ });
630
+ }
631
+
632
+ // Build node → color map for downstream node styling
633
+ var nodeColorById = {};
634
+ Object.keys(ownerByNode).forEach(function(otId) {
635
+ var comboId = "combo-" + ownerByNode[otId];
636
+ var combo = combos.find(function(c) { return c.id === comboId; });
637
+ if (combo) nodeColorById[otId] = combo.data;
638
+ });
639
+
640
+ return { ownerByNode: ownerByNode, combos: combos, nodeColorById: nodeColorById };
641
+ }
642
+
643
+ function buildLayoutConfig(name, width, height) {
644
+ var reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
645
+ var animation = !reducedMotion;
646
+ if (name === "radial") {
647
+ return {
648
+ type: "radial",
649
+ unitRadius: 110,
650
+ preventOverlap: true,
651
+ nodeSize: 80,
652
+ strictRadial: false,
653
+ animation: animation,
654
+ };
655
+ }
656
+ if (name === "concentric") {
657
+ return {
658
+ type: "concentric",
659
+ preventOverlap: true,
660
+ nodeSize: 80,
661
+ minNodeSpacing: 30,
662
+ sortBy: "degree",
663
+ animation: animation,
664
+ };
665
+ }
666
+ if (name === "circular") {
667
+ return { type: "circular", radius: Math.min(width, height) / 2.5, animation: animation };
668
+ }
669
+ if (name === "dagre") {
670
+ return {
671
+ type: "dagre",
672
+ rankdir: "LR",
673
+ nodesep: 30,
674
+ ranksep: 80,
675
+ animation: animation,
676
+ };
677
+ }
678
+ if (name === "grid") {
679
+ return { type: "grid", preventOverlap: true, nodeSize: 80, animation: animation };
680
+ }
681
+ // default: d3-force — animation OFF to avoid continuous-tick jank
682
+ return {
683
+ type: "d3-force",
684
+ preventOverlap: true,
685
+ nodeSize: 80,
686
+ link: { distance: 220, strength: 0.4 },
687
+ manyBody: { strength: -800 },
688
+ collide: { radius: 55 },
689
+ center: { x: width / 2, y: height / 2 },
690
+ animation: false,
691
+ };
692
+ }
693
+
694
+ function renderOtGraphView(knId, ots, rts, conceptGroups) {
695
+ var host = document.getElementById("ot-graph-host");
696
+ if (!host) return;
697
+
698
+ if (!ots.length) {
699
+ host.innerHTML = '<div class="ot-graph-empty">No object types to display.</div>';
700
+ return;
701
+ }
702
+ if (typeof G6 === "undefined" || !G6.Graph) {
703
+ host.innerHTML = '<div class="ot-graph-empty">Graph library failed to load (vendor/g6.min.js missing). Refresh or rebuild.</div>';
704
+ return;
705
+ }
706
+
707
+ var nodeById = {};
708
+ ots.forEach(function(ot) { nodeById[ot.id] = true; });
709
+
710
+ // Compute combos from BKN concept-groups (business-level grouping)
711
+ var grouping = getOtGraphGroup() ? buildConceptGroupCombos(ots, conceptGroups) : null;
712
+
713
+ // Estimate rect width per node based on label length (Chinese chars ~14px each)
714
+ function estimateRectSize(name, propertyCount) {
715
+ var chars = (name || "").length;
716
+ var width = Math.max(90, Math.min(180, 24 + chars * 14));
717
+ var height = 40;
718
+ return [width, height];
719
+ }
720
+
721
+ var nodes = ots.map(function(ot) {
722
+ var size = estimateRectSize(ot.name, ot.propertyCount);
723
+ var groupColors = grouping && grouping.nodeColorById && grouping.nodeColorById[ot.id];
724
+ var node = {
725
+ id: ot.id,
726
+ data: {
727
+ name: ot.name,
728
+ propertyCount: ot.propertyCount,
729
+ relCount: ot.relCount,
730
+ groupColor: groupColors ? groupColors.color : null,
731
+ groupFill: groupColors ? groupColors.fill : null,
732
+ groupFillDark: groupColors ? groupColors.fillDark : null,
733
+ },
734
+ style: {
735
+ labelText: ot.name,
736
+ size: size,
737
+ },
738
+ };
739
+ if (grouping && grouping.ownerByNode[ot.id]) {
740
+ node.combo = "combo-" + grouping.ownerByNode[ot.id];
741
+ }
742
+ return node;
743
+ });
744
+ var edges = rts
745
+ .filter(function(rt) { return nodeById[rt.sourceOtId] && nodeById[rt.targetOtId]; })
746
+ .map(function(rt, i) {
747
+ return {
748
+ id: rt.id || ("edge-" + i),
749
+ source: rt.sourceOtId,
750
+ target: rt.targetOtId,
751
+ data: { name: rt.name || "" },
752
+ style: { labelText: rt.name || "" },
753
+ };
754
+ });
755
+ var combos = grouping ? grouping.combos : [];
756
+
757
+ // Detect dark mode for theming
758
+ var isDark = document.documentElement.getAttribute("data-theme") === "dark";
759
+ // Palette[0] is the global "accent" — drives node/edge/selection color even when Group is off
760
+ var activePal = getActivePalette();
761
+ var primary = activePal[0];
762
+ var accentBlue = primary.strong;
763
+ var accentHover = primary.strong;
764
+ var accentLight = isDark ? primary.darkLight : primary.light;
765
+ var textColor = isDark ? "#f8fafc" : "#0f172a";
766
+ var edgeColor = isDark ? "#475569" : "#cbd5e1";
767
+ var bgColor = isDark ? "#1e293b" : "#ffffff";
768
+
769
+ // Node fills must be OPAQUE so edges (drawn underneath) never bleed through
770
+ // the rectangle. Dark-mode palette tints are rgba(...,~0.2) — composite them
771
+ // over the canvas bg to get an equivalent solid color, preserving the hue.
772
+ function toRgb(c) {
773
+ if (!c) return null;
774
+ var m = c.match(/rgba?\(([^)]+)\)/);
775
+ if (m) {
776
+ var p = m[1].split(",").map(function(x) { return parseFloat(x); });
777
+ return { r: p[0], g: p[1], b: p[2], a: p.length > 3 ? p[3] : 1 };
778
+ }
779
+ var h = c.replace("#", "");
780
+ if (h.length === 3) h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2];
781
+ if (h.length === 6) {
782
+ return { r: parseInt(h.slice(0, 2), 16), g: parseInt(h.slice(2, 4), 16), b: parseInt(h.slice(4, 6), 16), a: 1 };
783
+ }
784
+ return null;
785
+ }
786
+ function opaqueFill(color) {
787
+ var fg = toRgb(color);
788
+ if (!fg) return color;
789
+ if (fg.a >= 1) return color; // already opaque
790
+ var bg = toRgb(bgColor) || { r: 255, g: 255, b: 255 };
791
+ var r = Math.round(fg.r * fg.a + bg.r * (1 - fg.a));
792
+ var g = Math.round(fg.g * fg.a + bg.g * (1 - fg.a));
793
+ var b = Math.round(fg.b * fg.a + bg.b * (1 - fg.a));
794
+ return "rgb(" + r + "," + g + "," + b + ")";
795
+ }
796
+
797
+ // Destroy prior instance if re-rendering
798
+ if (__g6Instance) {
799
+ try { __g6Instance.destroy(); } catch (_) {}
800
+ __g6Instance = null;
801
+ }
802
+
803
+ var width = host.clientWidth || 800;
804
+ var height = host.clientHeight || 600;
805
+ var layoutName = getOtGraphLayout();
806
+ var reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
807
+
808
+ function buildOuterLayout(name) {
809
+ if (name === "dagre") {
810
+ return { type: "dagre", rankdir: "LR", nodesep: 60, ranksep: 120, animation: !reducedMotion };
811
+ }
812
+ if (name === "grid") {
813
+ return { type: "grid", preventOverlap: true, nodeSize: 140, animation: !reducedMotion };
814
+ }
815
+ if (name === "radial") {
816
+ return { type: "radial", unitRadius: 220, preventOverlap: true, nodeSize: 140, animation: !reducedMotion };
817
+ }
818
+ if (name === "concentric") {
819
+ return { type: "concentric", preventOverlap: true, nodeSize: 140, minNodeSpacing: 80, sortBy: "degree", animation: !reducedMotion };
820
+ }
821
+ if (name === "circular") {
822
+ return { type: "circular", radius: Math.min(width, height) / 2.5, animation: !reducedMotion };
823
+ }
824
+ // d3-force default — animation OFF: live force ticking every frame is the
825
+ // main jank source. Settle synchronously, render once.
826
+ return {
827
+ type: "d3-force",
828
+ preventOverlap: true,
829
+ nodeSize: 140,
830
+ link: { distance: 320, strength: 0.3 },
831
+ manyBody: { strength: -1800 },
832
+ collide: { radius: 90 },
833
+ center: { x: width / 2, y: height / 2 },
834
+ animation: false,
835
+ };
836
+ }
837
+
838
+ // When Group is ON, outer is fixed to force; layout dropdown disabled in UI.
839
+ var layoutCfg = (grouping && combos.length > 1)
840
+ ? {
841
+ type: "combo-combined",
842
+ spacing: 120,
843
+ comboPadding: 36,
844
+ outerLayout: {
845
+ type: "force",
846
+ linkDistance: 700,
847
+ nodeStrength: -6000,
848
+ edgeStrength: 0.05,
849
+ preventOverlap: true,
850
+ nodeSize: 320,
851
+ collideStrength: 1.0,
852
+ animation: false,
853
+ },
854
+ innerLayout: {
855
+ // Concentric packs nodes densely in rings — keeps combo bbox compact
856
+ type: "concentric",
857
+ preventOverlap: true,
858
+ minNodeSpacing: 14,
859
+ nodeSize: 110,
860
+ sortBy: "degree",
861
+ animation: false,
862
+ },
863
+ }
864
+ : buildLayoutConfig(layoutName, width, height);
865
+
866
+ var graph = new G6.Graph({
867
+ container: host,
868
+ background: bgColor,
869
+ width: width,
870
+ height: height,
871
+ padding: 40,
872
+ autoResize: true,
873
+ data: { nodes: nodes, edges: edges, combos: combos },
874
+ combo: combos.length ? {
875
+ type: "rect",
876
+ style: {
877
+ fill: function(d) {
878
+ var data = d.data || {};
879
+ return isDark ? (data.fillDark || "rgba(59,130,246,0.12)") : (data.fill || "rgba(59,130,246,0.04)");
880
+ },
881
+ stroke: function(d) { return (d.data && d.data.color) || (isDark ? "#94a3b8" : "#94a3b8"); },
882
+ lineWidth: 1.5,
883
+ lineDash: [6, 4],
884
+ strokeOpacity: 0.7,
885
+ radius: 16,
886
+ labelText: function(d) { return d.data && d.data.label; },
887
+ labelFill: function(d) { return (d.data && d.data.color) || (isDark ? "#cbd5e1" : "#475569"); },
888
+ labelFontSize: 13,
889
+ labelFontWeight: 700,
890
+ labelPlacement: "top",
891
+ labelOffsetY: -10,
892
+ labelBackground: true,
893
+ labelBackgroundFill: bgColor,
894
+ labelBackgroundFillOpacity: 0.98,
895
+ labelPadding: [4, 10],
896
+ labelBackgroundRadius: 6,
897
+ padding: 32,
898
+ zIndex: -1,
899
+ },
900
+ } : undefined,
901
+ node: {
902
+ type: "rect",
903
+ style: {
904
+ fill: function(d) {
905
+ var data = d.data || {};
906
+ if (data.groupColor) {
907
+ return opaqueFill(isDark ? (data.groupFillDark || accentLight) : (data.groupFill || accentLight));
908
+ }
909
+ return opaqueFill(accentLight);
910
+ },
911
+ stroke: function(d) {
912
+ var data = d.data || {};
913
+ return data.groupColor || accentBlue;
914
+ },
915
+ lineWidth: 1.5,
916
+ radius: 6,
917
+ labelText: function(d) { return d.data && d.data.name; },
918
+ labelFill: textColor,
919
+ labelFontSize: 13,
920
+ labelFontWeight: 600,
921
+ labelPlacement: "center",
922
+ labelMaxWidth: function(d) { return (d.style && d.style.size && d.style.size[0]) ? d.style.size[0] - 12 : 100; },
923
+ labelTextOverflow: "ellipsis",
924
+ cursor: "pointer",
925
+ zIndex: 2,
926
+ },
927
+ state: {
928
+ hover: {
929
+ fill: function(d) { return (d.data && d.data.groupColor) || accentBlue; },
930
+ stroke: function(d) { return (d.data && d.data.groupColor) || accentHover; },
931
+ lineWidth: 2.5,
932
+ labelFill: "#ffffff",
933
+ labelFontWeight: 700,
934
+ },
935
+ selected: {
936
+ fill: function(d) { return (d.data && d.data.groupColor) || accentHover; },
937
+ stroke: function(d) { return (d.data && d.data.groupColor) || accentHover; },
938
+ lineWidth: 3,
939
+ labelFill: "#ffffff",
940
+ },
941
+ },
942
+ },
943
+ edge: {
944
+ type: "line",
945
+ style: {
946
+ stroke: accentBlue,
947
+ lineWidth: 1.5,
948
+ strokeOpacity: 0.4,
949
+ endArrow: true,
950
+ endArrowType: "vee",
951
+ endArrowSize: 9,
952
+ endArrowFill: accentBlue,
953
+ // Labels hidden by default — show on hover only (cuts clutter)
954
+ labelFill: isDark ? "#cbd5e1" : "#475569",
955
+ labelFontSize: 11,
956
+ labelOpacity: 0,
957
+ labelBackground: true,
958
+ labelBackgroundFill: bgColor,
959
+ labelBackgroundFillOpacity: 0.95,
960
+ labelBackgroundRadius: 3,
961
+ labelPadding: [2, 6],
962
+ labelMaxWidth: 120,
963
+ labelTextOverflow: "ellipsis",
964
+ zIndex: 1,
965
+ },
966
+ state: {
967
+ hover: {
968
+ stroke: accentBlue,
969
+ lineWidth: 3,
970
+ strokeOpacity: 1,
971
+ labelOpacity: 1,
972
+ labelFontWeight: 700,
973
+ endArrowFill: accentBlue,
974
+ },
975
+ active: {
976
+ stroke: accentBlue,
977
+ lineWidth: 2.5,
978
+ strokeOpacity: 1,
979
+ labelOpacity: 1,
980
+ },
981
+ },
982
+ },
983
+ layout: layoutCfg,
984
+ behaviors: [
985
+ { type: "drag-canvas", key: "drag-canvas" },
986
+ { type: "zoom-canvas", key: "zoom-canvas", sensitivity: 1.2 },
987
+ // drag-element works for both node and combo; drag-element-force
988
+ // only works with d3-force layout (per G6 v5 bundle constraint)
989
+ { type: "drag-element", key: "drag-element", enableTransient: true },
990
+ { type: "hover-activate", key: "hover-activate" },
991
+ ],
992
+ animation: false,
993
+ });
994
+
995
+ // Fit + center after layout finishes (handles async layouts like dagre / force)
996
+ function fitAndCenter() {
997
+ try {
998
+ graph.fitView({ padding: [60, 40, 40, 40], when: "always" });
999
+ } catch (_) {
1000
+ try { graph.fitCenter(); } catch (_) {}
1001
+ }
1002
+ }
1003
+ graph.on("afterlayout", fitAndCenter);
1004
+ graph.on("afterrender", function() { setTimeout(fitAndCenter, 100); });
1005
+
1006
+ graph.render().then(function() {
1007
+ setTimeout(fitAndCenter, 300);
1008
+ setTimeout(fitAndCenter, 800); // fallback for slow layouts
1009
+
1010
+ graph.on("node:click", function(evt) {
1011
+ var id = evt.target && evt.target.id;
1012
+ if (id) location.hash = "/bkn/" + enc(knId) + "/ot/" + enc(id);
1013
+ });
1014
+ }).catch(function(err) {
1015
+ host.innerHTML = '<div class="ot-graph-empty">Graph render error: ' + esc(String(err && err.message || err)) + '</div>';
1016
+ });
1017
+
1018
+ __g6Instance = graph;
1019
+
1020
+ // Toolbar actions
1021
+ var wrap = host.closest(".ot-graph-wrap");
1022
+ if (!wrap) return;
1023
+ wrap.querySelectorAll("[data-graph-action]").forEach(function(btn) {
1024
+ btn.addEventListener("click", function() {
1025
+ var action = btn.getAttribute("data-graph-action");
1026
+ if (action === "fit") {
1027
+ graph.fitView({ padding: 40 });
1028
+ } else if (action === "export") {
1029
+ Promise.resolve(graph.toDataURL("image/png", bgColor)).then(function(dataUrl) {
1030
+ var a = document.createElement("a");
1031
+ a.href = dataUrl;
1032
+ a.download = "bkn-graph-" + Date.now() + ".png";
1033
+ a.click();
1034
+ }).catch(function(err) {
1035
+ alert("Export failed: " + (err && err.message || err));
1036
+ });
1037
+ }
1038
+ });
1039
+ });
1040
+
1041
+ // Layout switcher
1042
+ var layoutSel = wrap.querySelector("#ot-graph-layout");
1043
+ if (layoutSel) {
1044
+ layoutSel.addEventListener("change", function() {
1045
+ var newLayout = layoutSel.value;
1046
+ setOtGraphLayout(newLayout);
1047
+ // Manual layouts only apply when grouping is OFF
1048
+ if (getOtGraphGroup() && combos.length > 1) {
1049
+ return; // grouped graph uses combo-combined, layout dropdown is no-op
1050
+ }
1051
+ var cfg = buildLayoutConfig(newLayout, host.clientWidth, host.clientHeight);
1052
+ try {
1053
+ graph.setLayout(cfg);
1054
+ graph.layout().then(function() {
1055
+ setTimeout(function() {
1056
+ try { graph.fitView({ padding: 40 }); } catch (_) {}
1057
+ }, 150);
1058
+ });
1059
+ } catch (err) {
1060
+ console.error("Layout switch failed:", err);
1061
+ }
1062
+ });
1063
+ }
1064
+
1065
+ // Group toggle — re-renders the graph host (toolbar persists, so sync the
1066
+ // layout dropdown's disabled state here; it's a no-op while grouping is on).
1067
+ var groupChk = wrap.querySelector("#ot-graph-group");
1068
+ if (groupChk) {
1069
+ groupChk.addEventListener("change", function() {
1070
+ setOtGraphGroup(groupChk.checked);
1071
+ if (layoutSel) layoutSel.disabled = groupChk.checked;
1072
+ renderOtGraphView(knId, ots, rts, conceptGroups);
1073
+ });
1074
+ }
1075
+
1076
+ // Palette switcher — re-renders entire graph
1077
+ var palSel = wrap.querySelector("#ot-graph-palette");
1078
+ if (palSel) {
1079
+ palSel.addEventListener("change", function() {
1080
+ setOtGraphPalette(palSel.value);
1081
+ renderOtGraphView(knId, ots, rts, conceptGroups);
1082
+ });
1083
+ }
1084
+ }
1085
+
235
1086
  // ── Object type instance list ───────────────────────────────────────────────
236
1087
 
237
1088
  async function renderBknOtList($el, knId, otId, gen) {