@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.
- package/dist/api/agent-chat.d.ts +10 -2
- package/dist/api/agent-chat.js +19 -5
- package/dist/api/datasources.d.ts +14 -0
- package/dist/api/datasources.js +14 -0
- package/dist/cli.js +2 -14
- package/dist/client.d.ts +7 -1
- package/dist/client.js +7 -1
- package/dist/commands/bkn-ops.d.ts +1 -1
- package/dist/commands/bkn-ops.js +42 -21
- package/dist/commands/bkn.js +6 -3
- package/dist/commands/ds.d.ts +0 -31
- package/dist/commands/ds.js +18 -448
- package/dist/commands/explore-bkn.d.ts +7 -1
- package/dist/commands/explore-bkn.js +32 -3
- package/dist/resources/datasources.d.ts +7 -0
- package/dist/resources/datasources.js +7 -0
- package/dist/templates/explorer/bkn.js +860 -9
- package/dist/templates/explorer/index.html +1 -0
- package/dist/templates/explorer/style.css +225 -0
- package/dist/templates/explorer/vendor/g6.min.js +68 -0
- package/dist/trace-ai/eval-set/schemas.d.ts +1 -0
- package/dist/trace-ai/eval-set/schemas.js +4 -0
- package/dist/trace-ai/eval-set/types.d.ts +2 -0
- package/dist/trace-ai/exp/capture-fingerprint.d.ts +10 -0
- package/dist/trace-ai/exp/capture-fingerprint.js +12 -0
- package/dist/trace-ai/exp/context/context-assembler.d.ts +18 -0
- package/dist/trace-ai/exp/context/context-assembler.js +42 -0
- package/dist/trace-ai/exp/context/failure-analyzer.d.ts +22 -0
- package/dist/trace-ai/exp/context/failure-analyzer.js +59 -0
- package/dist/trace-ai/exp/context/kn-data-prober.d.ts +13 -0
- package/dist/trace-ai/exp/context/kn-data-prober.js +38 -0
- package/dist/trace-ai/exp/context/kn-schema-client.d.ts +14 -0
- package/dist/trace-ai/exp/context/kn-schema-client.js +41 -0
- package/dist/trace-ai/exp/context/retrieval-health.d.ts +32 -0
- package/dist/trace-ai/exp/context/retrieval-health.js +138 -0
- package/dist/trace-ai/exp/context/vega-catalog-client.d.ts +14 -0
- package/dist/trace-ai/exp/context/vega-catalog-client.js +15 -0
- package/dist/trace-ai/exp/coordinator.d.ts +34 -21
- package/dist/trace-ai/exp/coordinator.js +246 -24
- package/dist/trace-ai/exp/eval-runner.js +4 -2
- package/dist/trace-ai/exp/exp-store/events-jsonl.d.ts +1 -0
- package/dist/trace-ai/exp/exp-store/events-jsonl.js +18 -0
- package/dist/trace-ai/exp/exp-store/expected-fingerprint.d.ts +3 -0
- package/dist/trace-ai/exp/exp-store/expected-fingerprint.js +31 -0
- package/dist/trace-ai/exp/exp-store/index.d.ts +63 -2
- package/dist/trace-ai/exp/exp-store/index.js +2 -1
- package/dist/trace-ai/exp/exp-store/rollback-yaml.d.ts +12 -0
- package/dist/trace-ai/exp/exp-store/rollback-yaml.js +29 -0
- package/dist/trace-ai/exp/index.d.ts +2 -0
- package/dist/trace-ai/exp/index.js +68 -3
- package/dist/trace-ai/exp/info.js +1 -1
- package/dist/trace-ai/exp/patch/index.d.ts +13 -2
- package/dist/trace-ai/exp/patch/index.js +65 -10
- package/dist/trace-ai/exp/patch/kn-api-client.d.ts +40 -0
- package/dist/trace-ai/exp/patch/kn-api-client.js +14 -0
- package/dist/trace-ai/exp/patch/kn.d.ts +8 -0
- package/dist/trace-ai/exp/patch/kn.js +36 -0
- package/dist/trace-ai/exp/patch/skill-api-client.d.ts +17 -0
- package/dist/trace-ai/exp/patch/skill-api-client.js +14 -0
- package/dist/trace-ai/exp/patch/skill-content.d.ts +9 -0
- package/dist/trace-ai/exp/patch/skill-content.js +12 -0
- package/dist/trace-ai/exp/preflight.d.ts +77 -0
- package/dist/trace-ai/exp/preflight.js +148 -0
- package/dist/trace-ai/exp/providers/synthesizer-client.d.ts +3 -14
- package/dist/trace-ai/exp/providers/synthesizer-client.js +53 -35
- package/dist/trace-ai/exp/providers/triage-client.d.ts +15 -2
- package/dist/trace-ai/exp/providers/triage-client.js +143 -28
- package/dist/trace-ai/exp/run-preflight.d.ts +19 -0
- package/dist/trace-ai/exp/run-preflight.js +56 -0
- package/dist/trace-ai/exp/schemas.d.ts +402 -44
- package/dist/trace-ai/exp/schemas.js +131 -18
- package/dist/utils/deprecation.d.ts +1 -0
- package/dist/utils/deprecation.js +18 -0
- 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
|
-
|
|
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) {
|