@kweaver-ai/kweaver-sdk 0.5.2 → 0.6.1

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 (57) hide show
  1. package/README.md +19 -1
  2. package/README.zh.md +19 -1
  3. package/dist/api/agent-chat.d.ts +7 -1
  4. package/dist/api/agent-chat.js +146 -40
  5. package/dist/api/agent-list.js +13 -13
  6. package/dist/api/business-domains.js +9 -5
  7. package/dist/api/context-loader.js +4 -1
  8. package/dist/api/conversations.js +4 -9
  9. package/dist/api/dataflow2.d.ts +95 -0
  10. package/dist/api/dataflow2.js +80 -0
  11. package/dist/api/headers.d.ts +2 -0
  12. package/dist/api/headers.js +7 -2
  13. package/dist/api/skills.js +2 -10
  14. package/dist/api/vega.d.ts +0 -16
  15. package/dist/api/vega.js +0 -33
  16. package/dist/auth/oauth.d.ts +7 -6
  17. package/dist/auth/oauth.js +170 -80
  18. package/dist/cli.js +21 -1
  19. package/dist/client.d.ts +9 -0
  20. package/dist/client.js +48 -8
  21. package/dist/commands/auth.js +103 -42
  22. package/dist/commands/bkn-schema.js +22 -0
  23. package/dist/commands/call.js +8 -5
  24. package/dist/commands/dataflow.d.ts +1 -0
  25. package/dist/commands/dataflow.js +251 -0
  26. package/dist/commands/explore-bkn.d.ts +79 -0
  27. package/dist/commands/explore-bkn.js +273 -0
  28. package/dist/commands/explore-chat.d.ts +3 -0
  29. package/dist/commands/explore-chat.js +193 -0
  30. package/dist/commands/explore-vega.d.ts +3 -0
  31. package/dist/commands/explore-vega.js +71 -0
  32. package/dist/commands/explore.d.ts +9 -0
  33. package/dist/commands/explore.js +258 -0
  34. package/dist/commands/vega.js +2 -104
  35. package/dist/config/no-auth.d.ts +3 -0
  36. package/dist/config/no-auth.js +5 -0
  37. package/dist/config/store.d.ts +8 -0
  38. package/dist/config/store.js +22 -0
  39. package/dist/index.d.ts +1 -1
  40. package/dist/index.js +1 -1
  41. package/dist/kweaver.d.ts +5 -0
  42. package/dist/kweaver.js +32 -2
  43. package/dist/resources/bkn.js +2 -3
  44. package/dist/resources/knowledge-networks.js +3 -8
  45. package/dist/resources/vega.d.ts +0 -6
  46. package/dist/resources/vega.js +1 -10
  47. package/dist/templates/explorer/app.js +136 -0
  48. package/dist/templates/explorer/bkn.js +747 -0
  49. package/dist/templates/explorer/chat.js +980 -0
  50. package/dist/templates/explorer/dashboard.js +82 -0
  51. package/dist/templates/explorer/index.html +35 -0
  52. package/dist/templates/explorer/style.css +2440 -0
  53. package/dist/templates/explorer/vega.js +291 -0
  54. package/dist/utils/browser.js +33 -10
  55. package/dist/utils/http.d.ts +3 -0
  56. package/dist/utils/http.js +37 -1
  57. package/package.json +9 -5
@@ -0,0 +1,747 @@
1
+ // ── BKN Tab ─────────────────────────────────────────────────────────────────
2
+
3
+ // BKN-specific state
4
+ let bknMeta = null;
5
+ let bknCurrentKnId = null;
6
+ const PAGE_SIZE = 30;
7
+
8
+ // Caches
9
+ const instanceListCache = {};
10
+ const instanceDetailCache = {};
11
+ const subgraphCache = {};
12
+ const bknSearchCache = {};
13
+ const rtDetailCache = {};
14
+
15
+ function bknCacheKey(...parts) { return parts.join("::"); }
16
+ function bknIsFresh(entry) { return entry && (Date.now() - entry.timestamp < CACHE_TTL); }
17
+
18
+ function clearBknCaches() {
19
+ for (const k in instanceListCache) delete instanceListCache[k];
20
+ for (const k in instanceDetailCache) delete instanceDetailCache[k];
21
+ for (const k in subgraphCache) delete subgraphCache[k];
22
+ for (const k in bknSearchCache) delete bknSearchCache[k];
23
+ for (const k in rtDetailCache) delete rtDetailCache[k];
24
+ bknMeta = null;
25
+ bknCurrentKnId = null;
26
+ }
27
+
28
+ // Per-OT user-chosen subtitle fields; null = use auto-detected
29
+ const userSubtitleFields = {};
30
+ let currentPageState = null;
31
+
32
+ // ── API wrappers ────────────────────────────────────────────────────────────
33
+
34
+ async function loadBknForKn(knId) {
35
+ if (bknCurrentKnId === knId && bknMeta) return bknMeta;
36
+ // Tell server to load this KN's data
37
+ try {
38
+ await api("POST", "/api/bkn/load", { knId });
39
+ } catch (e) {
40
+ // 404 means the endpoint isn't deployed yet — fall through to meta
41
+ if (e.status !== 404) throw e;
42
+ }
43
+ bknMeta = await api("GET", "/api/bkn/meta");
44
+ bknCurrentKnId = knId;
45
+ return bknMeta;
46
+ }
47
+
48
+ async function bknQueryInstances(otId, opts) {
49
+ if (!opts) opts = {};
50
+ const body = { otId, limit: opts.limit ?? PAGE_SIZE };
51
+ if (opts.searchAfter) body.search_after = opts.searchAfter;
52
+ if (opts.condition) body.condition = opts.condition;
53
+ if (opts._instance_identities) body._instance_identities = opts._instance_identities;
54
+ return api("POST", "/api/bkn/instances", body);
55
+ }
56
+
57
+ async function bknQueryInstancesCached(otId, opts) {
58
+ if (!opts) opts = {};
59
+ const key = bknCacheKey(otId, JSON.stringify(opts.searchAfter ?? "first"));
60
+ if (bknIsFresh(instanceListCache[key])) return instanceListCache[key].data;
61
+ const data = await bknQueryInstances(otId, opts);
62
+ instanceListCache[key] = { data, timestamp: Date.now() };
63
+
64
+ // Also cache each instance for detail view reuse
65
+ const items = data.datas ?? data.entries ?? [];
66
+ for (const item of items) {
67
+ const identity = item._instance_identity ?? {};
68
+ const pk = Object.entries(identity).map(([k, v]) => `${k}=${v}`).join("&");
69
+ if (pk) instanceDetailCache[bknCacheKey(otId, pk)] = { instance: item, timestamp: Date.now() };
70
+ }
71
+
72
+ return data;
73
+ }
74
+
75
+ async function bknQuerySubgraph(body) {
76
+ return api("POST", "/api/bkn/subgraph", body);
77
+ }
78
+
79
+ async function bknQuerySubgraphCached(body) {
80
+ const key = JSON.stringify(body);
81
+ if (bknIsFresh(subgraphCache[key])) return subgraphCache[key].result;
82
+ const result = await bknQuerySubgraph(body);
83
+ subgraphCache[key] = { result, timestamp: Date.now() };
84
+ return result;
85
+ }
86
+
87
+ async function bknSearch(query) {
88
+ return api("POST", "/api/bkn/search", { query, maxConcepts: 30 });
89
+ }
90
+
91
+ async function bknSearchCached(query) {
92
+ if (bknIsFresh(bknSearchCache[query])) return bknSearchCache[query].data;
93
+ const data = await bknSearch(query);
94
+ bknSearchCache[query] = { data, timestamp: Date.now() };
95
+ return data;
96
+ }
97
+
98
+ // ── Main dispatcher ─────────────────────────────────────────────────────────
99
+
100
+ async function renderBkn($el, parts, params) {
101
+ const gen = navGeneration;
102
+
103
+ if (parts.length === 0) {
104
+ // KN selection list
105
+ return renderBknKnList($el, gen);
106
+ }
107
+
108
+ const knId = parts[0];
109
+ const subParts = parts.slice(1);
110
+
111
+ // Load BKN meta for this KN
112
+ try {
113
+ $el.innerHTML = '<div class="loading-skeleton"><div class="skeleton skeleton-title"></div><div class="loading-skeleton grid"><div class="skeleton skeleton-card"></div><div class="skeleton skeleton-card"></div></div></div>';
114
+ await loadBknForKn(knId);
115
+ } catch (err) {
116
+ $el.innerHTML = '<div class="error-banner">Failed to load KN: ' + esc(String(err.message || err)) + '</div>';
117
+ return;
118
+ }
119
+ if (navGeneration !== gen) return;
120
+
121
+ if (subParts.length === 0) {
122
+ renderBknHome($el, knId);
123
+ } else if (subParts[0] === "ot" && subParts[1]) {
124
+ await renderBknOtList($el, knId, decodeURIComponent(subParts[1]), gen);
125
+ } else if (subParts[0] === "instance" && subParts[1] && subParts[2]) {
126
+ await renderBknInstance($el, knId, decodeURIComponent(subParts[1]), decodeURIComponent(subParts[2]), gen);
127
+ } else if (subParts[0] === "search") {
128
+ await renderBknSearchView($el, knId, params.get("q") || "", gen);
129
+ } else if (subParts[0] === "rt" && subParts[1]) {
130
+ await renderBknRtDetail($el, knId, decodeURIComponent(subParts[1]), gen);
131
+ } else {
132
+ $el.innerHTML = '<div class="error-banner">Unknown BKN route</div>';
133
+ }
134
+ }
135
+
136
+ // ── KN list view (NEW) ─────────────────────────────────────────────────────
137
+
138
+ async function renderBknKnList($el, gen) {
139
+ $el.innerHTML = '<div class="loading-skeleton"><div class="skeleton skeleton-title"></div><div class="loading-skeleton grid"><div class="skeleton skeleton-card"></div><div class="skeleton skeleton-card"></div></div></div>';
140
+
141
+ let data;
142
+ try {
143
+ data = await cachedFetch(bknSearchCache, "__kn_list__", () => api("GET", "/api/dashboard"));
144
+ } catch (err) {
145
+ $el.innerHTML = '<div class="error-banner">Failed to load knowledge networks. <a href="#/bkn" onclick="location.reload()">Retry</a></div>';
146
+ return;
147
+ }
148
+ if (navGeneration !== gen) return;
149
+
150
+ const knList = extractList(data.kn);
151
+
152
+ if (knList.length === 0) {
153
+ $el.innerHTML = '<div class="breadcrumb"><a href="#/">Home</a> / BKN</div>' +
154
+ '<h1 class="page-title">Knowledge Networks</h1>' +
155
+ '<p style="padding:20px;color:#666;">No knowledge networks found.</p>';
156
+ return;
157
+ }
158
+
159
+ $el.innerHTML = '<div class="breadcrumb"><a href="#/">Home</a> / BKN</div>' +
160
+ '<h1 class="page-title">Knowledge Networks</h1>' +
161
+ '<p class="page-subtitle">' + knList.length + ' knowledge network(s) available</p>' +
162
+ '<div class="ot-grid">' +
163
+ knList.map(function(kn) {
164
+ var id = kn.id || kn.kg_id;
165
+ var name = kn.name || kn.kg_name || id;
166
+ var desc = kn.description || "";
167
+ return '<a href="#/bkn/' + enc(id) + '" class="ot-card" style="text-decoration:none;color:inherit;">' +
168
+ '<h3>' + esc(name) + '</h3>' +
169
+ (desc ? '<div class="meta">' + esc(desc) + '</div>' : '') +
170
+ '</a>';
171
+ }).join("") +
172
+ '</div>';
173
+ }
174
+
175
+ // ── BKN Home (schema overview for a KN) ─────────────────────────────────────
176
+
177
+ function renderBknHome($el, knId) {
178
+ var m = bknMeta;
179
+ var otCount = m.objectTypes.length;
180
+ var rtCount = m.relationTypes.length;
181
+ var knName = m.bkn.name || knId;
182
+
183
+ $el.innerHTML =
184
+ '<div class="breadcrumb"><a href="#/bkn">BKN</a> / ' + esc(knName) + '</div>' +
185
+ '<h1 class="page-title">' + esc(knName) + '</h1>' +
186
+ '<p class="page-subtitle">Knowledge Network Explorer</p>' +
187
+
188
+ '<div class="stats-row">' +
189
+ '<div class="stat-card"><div class="number">' + otCount + '</div><div class="label">Object Types</div></div>' +
190
+ '<div class="stat-card"><div class="number">' + (m.statistics.object_count || "\u2014") + '</div><div class="label">Instances</div></div>' +
191
+ '<div class="stat-card"><div class="number">' + rtCount + '</div><div class="label">Relation Types</div></div>' +
192
+ '<div class="stat-card"><div class="number">' + (m.statistics.relation_count || "\u2014") + '</div><div class="label">Relations</div></div>' +
193
+ '</div>' +
194
+
195
+ '<div class="search-box" style="margin-bottom:24px;">' +
196
+ '<input type="text" id="bkn-search-input" placeholder="Semantic search..." />' +
197
+ '<button id="bkn-search-btn">Search</button>' +
198
+ '</div>' +
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>' +
209
+
210
+ (rtCount > 0 ? (
211
+ '<h2 style="font-size:18px; margin:24px 0 16px;">Relation Types</h2>' +
212
+ '<div class="ot-grid">' +
213
+ m.relationTypes.map(function(rt) {
214
+ return '<a href="#/bkn/' + enc(knId) + '/rt/' + enc(rt.id) + '" class="ot-card" style="text-decoration:none;color:inherit;">' +
215
+ '<h3>' + esc(rt.name) + '</h3>' +
216
+ '<div class="meta">' + esc(rt.sourceOtName || rt.sourceOtId) + ' \u2192 ' + esc(rt.targetOtName || rt.targetOtId) + '</div>' +
217
+ '</a>';
218
+ }).join("") +
219
+ '</div>'
220
+ ) : '');
221
+
222
+ // Bind search
223
+ var searchInput = document.getElementById("bkn-search-input");
224
+ var searchBtn = document.getElementById("bkn-search-btn");
225
+ if (searchInput && searchBtn) {
226
+ function doSearch() {
227
+ var q = searchInput.value.trim();
228
+ if (q) location.hash = "/bkn/" + enc(knId) + "/search?q=" + encodeURIComponent(q);
229
+ }
230
+ searchBtn.addEventListener("click", doSearch);
231
+ searchInput.addEventListener("keydown", function(e) { if (e.key === "Enter") doSearch(); });
232
+ }
233
+ }
234
+
235
+ // ── Object type instance list ───────────────────────────────────────────────
236
+
237
+ async function renderBknOtList($el, knId, otId, gen) {
238
+ var ot = bknMeta.objectTypes.find(function(o) { return o.id === otId; });
239
+ if (!ot) { $el.innerHTML = "<p>Object type not found</p>"; return; }
240
+ var knName = bknMeta.bkn.name || knId;
241
+
242
+ $el.innerHTML =
243
+ '<div class="breadcrumb"><a href="#/bkn">BKN</a> / <a href="#/bkn/' + enc(knId) + '">' + esc(knName) + '</a> / ' + esc(ot.name) + '</div>' +
244
+ '<h1 class="page-title">' + esc(ot.name) + '</h1>' +
245
+ '<p class="page-subtitle">Display key: ' + esc(ot.displayKey) + ' \u00B7 ' + ot.propertyCount + ' properties</p>' +
246
+ '<div id="field-picker"></div>' +
247
+ '<div id="instance-container"><div class="loading-skeleton"><div class="skeleton skeleton-list-item"></div><div class="skeleton skeleton-list-item"></div><div class="skeleton skeleton-list-item"></div></div></div>' +
248
+ '<div id="pagination-container"></div>';
249
+
250
+ await bknLoadInstances(knId, otId, ot.displayKey, gen);
251
+ }
252
+
253
+ async function bknLoadInstances(knId, otId, displayKey, gen, searchAfter) {
254
+ var data;
255
+ try {
256
+ // Timeout wrapper: abort loading after 15 seconds
257
+ data = await Promise.race([
258
+ bknQueryInstancesCached(otId, { searchAfter: searchAfter }),
259
+ new Promise(function(_, reject) {
260
+ setTimeout(function() { reject(new Error("Request timed out")); }, 15000);
261
+ }),
262
+ ]);
263
+ } catch (err) {
264
+ if (navGeneration !== gen) return;
265
+ var errContainer = document.getElementById("instance-container");
266
+ if (errContainer) {
267
+ errContainer.innerHTML =
268
+ '<div class="error-banner">' +
269
+ 'Failed to load instances: ' + esc(err.message || String(err)) +
270
+ ' <a href="javascript:void(0)" id="retry-instances" style="margin-left:12px;color:var(--accent);">Retry</a>' +
271
+ '</div>';
272
+ var retryBtn = document.getElementById("retry-instances");
273
+ if (retryBtn) retryBtn.onclick = function() { bknLoadInstances(knId, otId, displayKey, gen, searchAfter); };
274
+ }
275
+ return;
276
+ }
277
+ if (navGeneration !== gen) return;
278
+
279
+ var container = document.getElementById("instance-container");
280
+ if (!container) return;
281
+ var items = data.datas ?? data.entries ?? [];
282
+
283
+ if (items.length === 0) {
284
+ container.innerHTML = '<p style="padding:20px;color:#666;">No instances</p>';
285
+ return;
286
+ }
287
+
288
+ currentPageState = { knId: knId, otId: otId, displayKey: displayKey, items: items, searchAfter: data.search_after };
289
+
290
+ // All available fields (non-internal, non-displayKey, with at least one non-empty value)
291
+ var allFields = bknGetAllFields(items, displayKey);
292
+ var autoFields = bknPickSubtitleFields(items, displayKey);
293
+ var activeFields = userSubtitleFields[otId] ?? autoFields;
294
+
295
+ bknRenderFieldPicker(knId, otId, allFields, activeFields, autoFields);
296
+ bknRenderInstanceList(items, displayKey, activeFields, knId, otId);
297
+
298
+ var pag = document.getElementById("pagination-container");
299
+ if (items.length >= PAGE_SIZE && data.search_after) {
300
+ pag.innerHTML = '<div class="pagination"><button id="next-page">Next page</button></div>';
301
+ document.getElementById("next-page").onclick = function() {
302
+ bknLoadInstances(knId, otId, displayKey, gen, data.search_after);
303
+ };
304
+ } else {
305
+ pag.innerHTML = "";
306
+ }
307
+ }
308
+
309
+ function bknRenderInstanceList(items, displayKey, subtitleFields, knId, otId) {
310
+ var container = document.getElementById("instance-container");
311
+ container.innerHTML = '<div class="instance-list">' +
312
+ items.map(function(item) {
313
+ var identity = item._instance_identity ?? {};
314
+ var pk = Object.entries(identity).map(function(e) { return e[0] + "=" + e[1]; }).join("&");
315
+ var name = item[displayKey] ?? Object.values(identity)[0] ?? "\u2014";
316
+ var subtitle = bknBuildSubtitle(item, subtitleFields);
317
+ return '<a href="#/bkn/' + enc(knId) + '/instance/' + enc(otId) + '/' + enc(pk) + '" class="instance-item" style="display:block;text-decoration:none;color:inherit;">' +
318
+ '<div class="name">' + esc(String(name)) + '</div>' +
319
+ (subtitle ? '<div class="instance-subtitle">' + esc(subtitle) + '</div>' : '') +
320
+ '</a>';
321
+ }).join("") +
322
+ '</div>';
323
+ }
324
+
325
+ // ── Field picker ────────────────────────────────────────────────────────────
326
+
327
+ function bknGetAllFields(items, displayKey) {
328
+ var skip = new Set(["_instance_identity", "_object_type_id", "_score", "_instance_id", "_display", "id", displayKey]);
329
+ var fields = new Set();
330
+ for (var i = 0; i < items.length; i++) {
331
+ var item = items[i];
332
+ for (var _i = 0, _a = Object.entries(item); _i < _a.length; _i++) {
333
+ var k = _a[_i][0], v = _a[_i][1];
334
+ if (k.startsWith("_") || skip.has(k) || v == null || v === "") continue;
335
+ fields.add(k);
336
+ }
337
+ }
338
+ return [...fields];
339
+ }
340
+
341
+ function bknRenderFieldPicker(knId, otId, allFields, activeFields, autoFields) {
342
+ var picker = document.getElementById("field-picker");
343
+ if (allFields.length === 0) { picker.innerHTML = ""; return; }
344
+
345
+ var activeSet = new Set(activeFields);
346
+ picker.innerHTML =
347
+ '<div class="field-picker">' +
348
+ '<span class="field-picker-label">Subtitle fields:</span>' +
349
+ allFields.map(function(f) {
350
+ return '<button class="field-chip ' + (activeSet.has(f) ? "active" : "") + '" data-field="' + esc(f) + '">' + esc(f) + '</button>';
351
+ }).join("") +
352
+ '<button class="field-chip field-chip-auto" title="Reset to auto-detected">Auto</button>' +
353
+ '</div>';
354
+
355
+ picker.querySelectorAll(".field-chip[data-field]").forEach(function(btn) {
356
+ btn.addEventListener("click", function() {
357
+ var field = btn.dataset.field;
358
+ var selected = userSubtitleFields[otId] ? [...userSubtitleFields[otId]] : [...activeFields];
359
+ if (selected.includes(field)) {
360
+ selected = selected.filter(function(f) { return f !== field; });
361
+ } else {
362
+ selected.push(field);
363
+ }
364
+ userSubtitleFields[otId] = selected;
365
+ bknRenderFieldPicker(knId, otId, allFields, selected, autoFields);
366
+ bknRenderInstanceList(currentPageState.items, currentPageState.displayKey, selected, knId, otId);
367
+ });
368
+ });
369
+
370
+ picker.querySelector(".field-chip-auto").addEventListener("click", function() {
371
+ delete userSubtitleFields[otId];
372
+ bknRenderFieldPicker(knId, otId, allFields, autoFields, autoFields);
373
+ bknRenderInstanceList(currentPageState.items, currentPageState.displayKey, autoFields, knId, otId);
374
+ });
375
+ }
376
+
377
+ // ── Instance detail ─────────────────────────────────────────────────────────
378
+
379
+ async function renderBknInstance($el, knId, otId, instanceId, gen) {
380
+ var ot = bknMeta.objectTypes.find(function(o) { return o.id === otId; });
381
+ if (!ot) { $el.innerHTML = "<p>Object type not found</p>"; return; }
382
+ var knName = bknMeta.bkn.name || knId;
383
+
384
+ var identity = {};
385
+ instanceId.split("&").forEach(function(pair) {
386
+ var parts = pair.split("=");
387
+ var k = parts[0];
388
+ var raw = decodeURIComponent(parts.slice(1).join("="));
389
+ // Preserve numeric types for identity values
390
+ var num = Number(raw);
391
+ identity[decodeURIComponent(k)] = (raw !== "" && !isNaN(num)) ? num : raw;
392
+ });
393
+
394
+ // Try cache first (populated from list view)
395
+ var pk = instanceId;
396
+ var detailKey = bknCacheKey(otId, pk);
397
+ var instance = null;
398
+ if (bknIsFresh(instanceDetailCache[detailKey])) {
399
+ instance = instanceDetailCache[detailKey].instance;
400
+ } else {
401
+ var data = await bknQueryInstances(otId, { _instance_identities: [identity], limit: 1 });
402
+ if (navGeneration !== gen) return;
403
+ var items = data.datas ?? data.entries ?? [];
404
+ instance = items[0];
405
+ if (instance) {
406
+ instanceDetailCache[detailKey] = { instance: instance, timestamp: Date.now() };
407
+ }
408
+ }
409
+
410
+ if (!instance) {
411
+ $el.innerHTML =
412
+ '<div class="breadcrumb"><a href="#/bkn">BKN</a> / <a href="#/bkn/' + enc(knId) + '">' + esc(knName) + '</a> / <a href="#/bkn/' + enc(knId) + '/ot/' + enc(otId) + '">' + esc(ot.name) + '</a></div>' +
413
+ '<p>Instance not found</p>';
414
+ return;
415
+ }
416
+
417
+ var displayName = instance[ot.displayKey] ?? Object.values(identity)[0] ?? "\u2014";
418
+ var props = Object.entries(instance).filter(function(e) { return !e[0].startsWith("_"); });
419
+ var propsHtml = props.map(function(e) {
420
+ return '<tr><td>' + esc(e[0]) + '</td><td>' + formatValue(e[1]) + '</td></tr>';
421
+ }).join("");
422
+
423
+ $el.innerHTML =
424
+ '<div class="breadcrumb"><a href="#/bkn">BKN</a> / <a href="#/bkn/' + enc(knId) + '">' + esc(knName) + '</a> / <a href="#/bkn/' + enc(knId) + '/ot/' + enc(otId) + '">' + esc(ot.name) + '</a> / ' + esc(String(displayName)) + '</div>' +
425
+ '<h1 class="page-title">' + esc(String(displayName)) + '</h1>' +
426
+ '<p class="page-subtitle">' + esc(ot.name) + '</p>' +
427
+
428
+ '<div class="detail-section">' +
429
+ '<h2>Properties</h2>' +
430
+ '<table class="props-table">' + propsHtml + '</table>' +
431
+ '</div>' +
432
+
433
+ '<div class="detail-section" id="relations-section">' +
434
+ '<h2>Relations</h2>' +
435
+ '<div id="relations-loading"><div class="loading-skeleton"><div class="skeleton skeleton-list-item"></div><div class="skeleton skeleton-list-item"></div></div></div>' +
436
+ '</div>';
437
+
438
+ bknLoadRelations(knId, otId, instance, gen);
439
+ }
440
+
441
+ async function bknLoadRelations(knId, otId, instance, gen) {
442
+ var container = document.getElementById("relations-loading");
443
+ if (!container) return;
444
+ var relatedRts = bknMeta.relationTypes.filter(function(rt) {
445
+ return rt.sourceOtId === otId || rt.targetOtId === otId;
446
+ });
447
+
448
+ if (relatedRts.length === 0) {
449
+ container.innerHTML = '<p style="color:#666;">No relations</p>';
450
+ return;
451
+ }
452
+
453
+ var html = "";
454
+ for (var i = 0; i < relatedRts.length; i++) {
455
+ var rt = relatedRts[i];
456
+ var isSource = rt.sourceOtId === otId;
457
+ var targetOtId = isSource ? rt.targetOtId : rt.sourceOtId;
458
+ var targetOt = bknMeta.objectTypes.find(function(o) { return o.id === targetOtId; });
459
+ if (!targetOt) continue;
460
+
461
+ try {
462
+ var body = {
463
+ relation_type_paths: [{
464
+ object_types: [
465
+ { id: otId, condition: bknBuildInstanceCondition(instance), limit: 1 },
466
+ { id: targetOtId, limit: 10 },
467
+ ],
468
+ relation_types: [{
469
+ relation_type_id: rt.id,
470
+ source_object_type_id: rt.sourceOtId,
471
+ target_object_type_id: rt.targetOtId,
472
+ }],
473
+ }],
474
+ };
475
+
476
+ var result = await bknQuerySubgraphCached(body);
477
+ if (navGeneration !== gen) return;
478
+ var entries = result.entries ?? result.datas ?? [];
479
+
480
+ if (entries.length === 0) continue;
481
+
482
+ var links = bknExtractLinkedInstances(entries, targetOtId, targetOt.displayKey);
483
+ if (links.length === 0) continue;
484
+
485
+ html += '<div class="relation-group">' +
486
+ '<h4>' + esc(rt.name) + ' \u2192 ' + esc(targetOt.name) + '</h4>' +
487
+ '<div class="link-list">' +
488
+ links.map(function(link) {
489
+ return '<a href="#/bkn/' + enc(knId) + '/instance/' + enc(targetOtId) + '/' + enc(link.pk) + '" class="link-tag">' + esc(link.name) + '</a>';
490
+ }).join("") +
491
+ '</div>' +
492
+ '</div>';
493
+ } catch (e) {
494
+ // Skip failed relation queries
495
+ }
496
+ }
497
+
498
+ if (navGeneration !== gen) return;
499
+ container.innerHTML = html || '<p style="color:#666;">No related instances</p>';
500
+ }
501
+
502
+ // ── Search ──────────────────────────────────────────────────────────────────
503
+
504
+ async function renderBknSearchView($el, knId, query, gen) {
505
+ var knName = bknMeta.bkn.name || knId;
506
+
507
+ $el.innerHTML =
508
+ '<div class="breadcrumb"><a href="#/bkn">BKN</a> / <a href="#/bkn/' + enc(knId) + '">' + esc(knName) + '</a> / Search</div>' +
509
+ '<h1 class="page-title">Search: ' + esc(query) + '</h1>' +
510
+ '<div id="search-results"><div class="loading-skeleton"><div class="skeleton skeleton-list-item"></div><div class="skeleton skeleton-list-item"></div></div></div>';
511
+
512
+ if (!query) {
513
+ document.getElementById("search-results").innerHTML = '<p style="color:#666;">Enter a search query.</p>';
514
+ return;
515
+ }
516
+
517
+ try {
518
+ var data = await bknSearchCached(query);
519
+ if (navGeneration !== gen) return;
520
+ var concepts = data.concepts ?? [];
521
+ var container = document.getElementById("search-results");
522
+
523
+ if (concepts.length === 0) {
524
+ container.innerHTML = '<p style="color:#666;">No results found</p>';
525
+ return;
526
+ }
527
+
528
+ container.innerHTML = concepts.map(function(c) {
529
+ var ot = bknMeta.objectTypes.find(function(o) { return o.id === c.concept_type || o.name === c.concept_type; });
530
+ var otId = ot ? ot.id : c.concept_type;
531
+ var otName = ot ? ot.name : c.concept_type;
532
+ var pk = c.concept_id ? "id=" + c.concept_id : "";
533
+ var href = pk
534
+ ? "#/bkn/" + enc(knId) + "/instance/" + enc(otId) + "/" + enc(pk)
535
+ : "#/bkn/" + enc(knId) + "/ot/" + enc(otId);
536
+ var score = (c.rerank_score ?? c.match_score ?? 0).toFixed(3);
537
+
538
+ return '<a href="' + href + '" class="search-result" style="display:block;text-decoration:none;color:inherit;">' +
539
+ '<span class="score">' + score + '</span>' +
540
+ '<span class="type-badge">' + esc(otName) + '</span>' +
541
+ '<strong>' + esc(c.concept_name) + '</strong>' +
542
+ '</a>';
543
+ }).join("");
544
+ } catch (err) {
545
+ if (navGeneration !== gen) return;
546
+ document.getElementById("search-results").innerHTML = '<p>Search error: ' + esc(err.message) + '</p>';
547
+ }
548
+ }
549
+
550
+ // ── Relation type detail ────────────────────────────────────────────────────
551
+
552
+ async function renderBknRtDetail($el, knId, rtId, gen) {
553
+ var rt = bknMeta.relationTypes.find(function(r) { return r.id === rtId; });
554
+ if (!rt) { $el.innerHTML = "<p>Relation type not found</p>"; return; }
555
+ var knName = bknMeta.bkn.name || knId;
556
+
557
+ var sourceOt = bknMeta.objectTypes.find(function(o) { return o.id === rt.sourceOtId; });
558
+ var targetOt = bknMeta.objectTypes.find(function(o) { return o.id === rt.targetOtId; });
559
+ var srcName = sourceOt ? sourceOt.name : rt.sourceOtId;
560
+ var tgtName = targetOt ? targetOt.name : rt.targetOtId;
561
+ var displayKey = sourceOt ? sourceOt.displayKey : undefined;
562
+
563
+ $el.innerHTML =
564
+ '<div class="breadcrumb"><a href="#/bkn">BKN</a> / <a href="#/bkn/' + enc(knId) + '">' + esc(knName) + '</a> / ' + esc(rt.name) + '</div>' +
565
+ '<h1 class="page-title">' + esc(rt.name) + '</h1>' +
566
+ '<p class="page-subtitle">' +
567
+ '<a href="#/bkn/' + enc(knId) + '/ot/' + enc(rt.sourceOtId) + '" style="color:var(--accent);text-decoration:none;">' + esc(srcName) + '</a>' +
568
+ ' \u2192 <a href="#/bkn/' + enc(knId) + '/ot/' + enc(rt.targetOtId) + '" style="color:var(--accent);text-decoration:none;">' + esc(tgtName) + '</a>' +
569
+ '</p>' +
570
+ '<div id="rt-instances"><div class="loading-skeleton"><div class="skeleton skeleton-list-item"></div><div class="skeleton skeleton-list-item"></div></div></div>';
571
+
572
+ // Use cached RT results if available
573
+ var results;
574
+ if (bknIsFresh(rtDetailCache[rtId])) {
575
+ results = rtDetailCache[rtId].results;
576
+ } else {
577
+ // Load source OT instances
578
+ var data = await bknQueryInstancesCached(rt.sourceOtId, {});
579
+ if (navGeneration !== gen) return;
580
+ var items = data.datas ?? data.entries ?? [];
581
+
582
+ if (items.length === 0) {
583
+ var c = document.getElementById("rt-instances");
584
+ if (c) c.innerHTML = '<p style="padding:20px;color:#666;">No instances</p>';
585
+ return;
586
+ }
587
+
588
+ // Batch-query relations (3 concurrent, with cancellation)
589
+ results = [];
590
+ var BATCH = 3;
591
+ for (var i = 0; i < items.length; i += BATCH) {
592
+ if (navGeneration !== gen) return; // user navigated away, stop querying
593
+ var batch = items.slice(i, i + BATCH);
594
+ var promises = batch.map(function(item) {
595
+ var body = {
596
+ relation_type_paths: [{
597
+ object_types: [
598
+ { id: rt.sourceOtId, condition: bknBuildInstanceCondition(item), limit: 1 },
599
+ { id: rt.targetOtId, limit: 20 },
600
+ ],
601
+ relation_types: [{
602
+ relation_type_id: rt.id,
603
+ source_object_type_id: rt.sourceOtId,
604
+ target_object_type_id: rt.targetOtId,
605
+ }],
606
+ }],
607
+ };
608
+ return bknQuerySubgraphCached(body).then(function(result) {
609
+ var entries = result.entries ?? result.datas ?? [];
610
+ var links = bknExtractLinkedInstances(entries, rt.targetOtId, targetOt ? targetOt.displayKey : undefined);
611
+ return { sourceItem: item, links: links };
612
+ }).catch(function() {
613
+ return { sourceItem: item, links: [] };
614
+ });
615
+ });
616
+ var batchResults = await Promise.all(promises);
617
+ results.push.apply(results, batchResults);
618
+ }
619
+
620
+ if (navGeneration !== gen) return;
621
+ rtDetailCache[rtId] = { results: results, timestamp: Date.now() };
622
+ }
623
+
624
+ // Render
625
+ var container = document.getElementById("rt-instances");
626
+ if (!container) return;
627
+
628
+ var withLinks = results.filter(function(r) { return r.links.length > 0; });
629
+ var withoutLinks = results.filter(function(r) { return r.links.length === 0; });
630
+
631
+ var html = "";
632
+
633
+ if (withLinks.length > 0) {
634
+ html += '<div class="instance-list">' +
635
+ withLinks.map(function(r) {
636
+ var identity = r.sourceItem._instance_identity ?? {};
637
+ var pk = Object.entries(identity).map(function(e) { return e[0] + "=" + e[1]; }).join("&");
638
+ var name = (displayKey && r.sourceItem[displayKey]) ?? Object.values(identity)[0] ?? "\u2014";
639
+ return '<div class="rt-source-item">' +
640
+ '<div class="rt-source-header">' +
641
+ '<a href="#/bkn/' + enc(knId) + '/instance/' + enc(rt.sourceOtId) + '/' + enc(pk) + '" class="name">' + esc(String(name)) + '</a>' +
642
+ '<span class="rt-link-count">' + r.links.length + ' relation(s)</span>' +
643
+ '</div>' +
644
+ '<div class="rt-targets" style="display:block;">' +
645
+ '<div class="link-list">' +
646
+ r.links.map(function(link) {
647
+ return '<a href="#/bkn/' + enc(knId) + '/instance/' + enc(rt.targetOtId) + '/' + enc(link.pk) + '" class="link-tag">' + esc(link.name) + '</a>';
648
+ }).join("") +
649
+ '</div>' +
650
+ '</div>' +
651
+ '</div>';
652
+ }).join("") +
653
+ '</div>';
654
+ } else {
655
+ html += '<p style="padding:20px;color:#666;">No relation instances found</p>';
656
+ }
657
+
658
+ if (withoutLinks.length > 0) {
659
+ html += '<details class="rt-no-links-section">' +
660
+ '<summary>' + withoutLinks.length + ' ' + esc(srcName) + ' instance(s) with no relations</summary>' +
661
+ '<div class="rt-no-links-list">' +
662
+ withoutLinks.map(function(r) {
663
+ var identity = r.sourceItem._instance_identity ?? {};
664
+ var pk = Object.entries(identity).map(function(e) { return e[0] + "=" + e[1]; }).join("&");
665
+ var name = (displayKey && r.sourceItem[displayKey]) ?? Object.values(identity)[0] ?? "\u2014";
666
+ return '<a href="#/bkn/' + enc(knId) + '/instance/' + enc(rt.sourceOtId) + '/' + enc(pk) + '" class="rt-no-link-item">' + esc(String(name)) + '</a>';
667
+ }).join("") +
668
+ '</div>' +
669
+ '</details>';
670
+ }
671
+
672
+ container.innerHTML = html;
673
+ }
674
+
675
+ // ── Condition builder for subgraph queries ──────────────────────────────────
676
+
677
+ function bknBuildInstanceCondition(instance) {
678
+ var identity = instance._instance_identity ?? {};
679
+ return {
680
+ operation: "and",
681
+ sub_conditions: Object.entries(identity).map(function(e) {
682
+ return { field: e[0], operation: "==", value: e[1] };
683
+ }),
684
+ };
685
+ }
686
+
687
+ // ── Extract linked instances from subgraph results ──────────────────────────
688
+
689
+ function bknExtractLinkedInstances(entries, targetOtId, displayKey) {
690
+ var results = [];
691
+ var seen = new Set();
692
+
693
+ function walk(obj) {
694
+ if (!obj || typeof obj !== "object") return;
695
+ if (Array.isArray(obj)) { obj.forEach(walk); return; }
696
+
697
+ if (obj._instance_identity) {
698
+ var identity = obj._instance_identity;
699
+ var pk = Object.entries(identity).map(function(e) { return e[0] + "=" + e[1]; }).join("&");
700
+ if (!seen.has(pk)) {
701
+ seen.add(pk);
702
+ var name = (displayKey && obj[displayKey]) ?? Object.values(identity)[0] ?? "\u2014";
703
+ results.push({ pk: pk, name: String(name) });
704
+ }
705
+ }
706
+
707
+ var values = Object.values(obj);
708
+ for (var i = 0; i < values.length; i++) {
709
+ walk(values[i]);
710
+ }
711
+ }
712
+
713
+ walk(entries);
714
+ return results;
715
+ }
716
+
717
+ // ── Subtitle helpers ────────────────────────────────────────────────────────
718
+
719
+ function bknPickSubtitleFields(items, displayKey, topN) {
720
+ if (!topN) topN = 3;
721
+ var skip = new Set(["_instance_identity", "_object_type_id", "_score", "_instance_id", "_display", "id", displayKey]);
722
+ // Collect unique values per field across all items
723
+ var fieldValues = {}; // field -> Set of values
724
+ for (var i = 0; i < items.length; i++) {
725
+ var item = items[i];
726
+ for (var _i = 0, _a = Object.entries(item); _i < _a.length; _i++) {
727
+ var k = _a[_i][0], v = _a[_i][1];
728
+ if (k.startsWith("_") || skip.has(k) || v == null || v === "") continue;
729
+ var str = formatValue(v);
730
+ if (str.length > 50) continue;
731
+ if (!fieldValues[k]) fieldValues[k] = new Set();
732
+ fieldValues[k].add(str);
733
+ }
734
+ }
735
+ // Sort by number of unique values (descending) — more unique = more distinguishing
736
+ return Object.entries(fieldValues)
737
+ .sort(function(a, b) { return b[1].size - a[1].size; })
738
+ .slice(0, topN)
739
+ .map(function(e) { return e[0]; });
740
+ }
741
+
742
+ function bknBuildSubtitle(item, subtitleFields) {
743
+ return subtitleFields
744
+ .filter(function(k) { return item[k] != null && item[k] !== ""; })
745
+ .map(function(k) { return k + ": " + formatValue(item[k]); })
746
+ .join(" \u00B7 ");
747
+ }