@kweaver-ai/kweaver-sdk 0.5.2 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) 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 +1 -1
  17. package/dist/auth/oauth.js +64 -7
  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 +80 -32
  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/http.d.ts +3 -0
  55. package/dist/utils/http.js +37 -1
  56. package/package.json +9 -5
@@ -0,0 +1,980 @@
1
+ // ── Chat Tab ─────────────────────────────────────────────────────────────────
2
+
3
+ const chatState = {
4
+ agents: null, // array of agent objects, null = not loaded
5
+ loading: false, // agents list loading
6
+ // per-agent conversation history: { [agentId]: Array<{role, text}> }
7
+ conversations: {},
8
+ currentAgentId: null,
9
+ streaming: false,
10
+ abortController: null, // AbortController for current stream
11
+ };
12
+
13
+ // ── Chat Settings & Agents Filter ────────────────────────────────────────────
14
+
15
+ window.chatFilterAgents = function(query) {
16
+ const q = query.toLowerCase();
17
+ const items = document.querySelectorAll('.chat-agent-item');
18
+ items.forEach(el => {
19
+ const text = el.textContent.toLowerCase();
20
+ el.style.display = text.includes(q) ? 'flex' : 'none';
21
+ });
22
+ };
23
+
24
+ window.renderBubble = function(msg, agentName) {
25
+ const isUser = msg.role === "user";
26
+ const avatar = isUser ? "👤" : "🤖";
27
+ return `<div class="chat-message-row ${isUser ? 'user' : 'assistant'}">
28
+ ${!isUser ? `<div class="chat-avatar">${avatar}</div>` : ""}
29
+ <div class="chat-bubble chat-bubble-${esc(msg.role)}">
30
+ ${!isUser ? `<div class="chat-bubble-sender">${esc(agentName)}</div>` : ""}
31
+ <div class="chat-bubble-content">${chatMarkdown(msg.text)}</div>
32
+ </div>
33
+ ${isUser ? `<div class="chat-avatar">${avatar}</div>` : ""}
34
+ </div>`;
35
+ };
36
+
37
+ // ── Markdown renderer (minimal) ──────────────────────────────────────────────
38
+
39
+ function isJsonData(text) {
40
+ if (!text) return false;
41
+ var trimmed = text.trim();
42
+ if ((trimmed.startsWith('[{') || trimmed.startsWith('{"')) && trimmed.length > 200) {
43
+ try { JSON.parse(trimmed); return true; } catch(e) { return false; }
44
+ }
45
+ return false;
46
+ }
47
+
48
+ function renderJsonData(text) {
49
+ var trimmed = text.trim();
50
+ try {
51
+ var parsed = JSON.parse(trimmed);
52
+ var formatted = JSON.stringify(parsed, null, 2);
53
+ var preview = formatted.length > 150 ? formatted.substring(0, 150) + "..." : formatted;
54
+ var count = Array.isArray(parsed) ? ' (' + parsed.length + ' 条记录)' : '';
55
+ return '<details class="chat-json-data"><summary>📊 数据结果' + esc(count) + '</summary><pre><code>' + esc(formatted) + '</code></pre></details>';
56
+ } catch(e) {
57
+ return '<pre><code>' + esc(trimmed) + '</code></pre>';
58
+ }
59
+ }
60
+
61
+ function chatMarkdown(text) {
62
+ if (!text) return "";
63
+
64
+ // Detect raw JSON data and render as collapsible
65
+ if (isJsonData(text)) return renderJsonData(text);
66
+
67
+ // Detect SSE error events embedded in text (various formats)
68
+ var errMatch = text.match(/event:error[\s\S]*?data:\s*(\{[\s\S]*\})\s*$/);
69
+ if (!errMatch) errMatch = text.match(/event:error[\s\S]*?data:\s*(\{[\s\S]*\})/);
70
+ if (errMatch) {
71
+ try {
72
+ var err = JSON.parse(errMatch[1]);
73
+ var errMsg = err.description || err.details || errMatch[1];
74
+ if (err.solution && err.solution !== "无") errMsg += "\n💡 " + err.solution;
75
+ // Show detailed error info (code, details, link) in a collapsible block
76
+ var errExtra = [];
77
+ if (err.code) errExtra.push("Code: " + err.code);
78
+ if (err.details && err.details !== err.description) errExtra.push("Details: " + err.details);
79
+ if (err.link && err.link !== "无") errExtra.push("Link: " + err.link);
80
+ var detailBlock = "";
81
+ if (errExtra.length > 0) {
82
+ detailBlock = "\n\n<details><summary>详细错误信息</summary>\n" + errExtra.join("\n") + "\n</details>";
83
+ }
84
+ // Keep text before the error
85
+ var beforeErr = text.substring(0, text.indexOf("event:error")).trim();
86
+ text = (beforeErr ? beforeErr + "\n\n" : "") + "⚠️ " + errMsg + detailBlock;
87
+ } catch(e) { /* keep original */ }
88
+ }
89
+
90
+ // Extract <details> blocks before escaping so they render as HTML
91
+ var detailsBlocks = [];
92
+ text = text.replace(/<details>([\s\S]*?)<\/details>/g, function(m) {
93
+ var idx = detailsBlocks.length;
94
+ detailsBlocks.push(m);
95
+ return "%%DETAILS_" + idx + "%%";
96
+ });
97
+
98
+ let s = esc(text);
99
+
100
+ // Fenced code blocks ```...```
101
+ s = s.replace(/```[\s\S]*?```/g, (m) => {
102
+ const inner = m.slice(3, -3).replace(/^[^\n]*\n?/, ""); // strip optional lang tag
103
+ return `<pre><code>${inner}</code></pre>`;
104
+ });
105
+
106
+ // Agent thoughts
107
+ s = s.replace(/&lt;(think|thought|thinking)&gt;([\s\S]*?)&lt;\/\1&gt;/gi, function(m, p1, inner) {
108
+ return `<details class="agent-thoughts"><summary>Agent Thoughts</summary><div class="agent-thoughts-content">${inner}</div></details>`;
109
+ });
110
+
111
+ // Raw JSON array of thoughts
112
+ s = s.replace(/\[\s*(?:&quot;|")[^\]]+(?:&quot;|")\s*\]/g, (match) => {
113
+ try {
114
+ let unescaped = match.replace(/&quot;/g, '"');
115
+ let arr = JSON.parse(unescaped);
116
+ if (Array.isArray(arr) && arr.length > 0 && typeof arr[0] === 'string') {
117
+ let listStr = arr.map(step => `<li>${esc(step)}</li>`).join('');
118
+ return `<details class="agent-thoughts" open><summary>Decision Process</summary><ul class="agent-thoughts-content">${listStr}</ul></details>`;
119
+ }
120
+ } catch(e) {}
121
+ return match;
122
+ });
123
+
124
+ // Inline code `...`
125
+ s = s.replace(/`([^`]+)`/g, (_m, c) => `<code>${c}</code>`);
126
+
127
+ // Bold **...**
128
+ s = s.replace(/\*\*([^*]+)\*\*/g, (_m, c) => `<strong>${c}</strong>`);
129
+
130
+ // Italic *...*
131
+ s = s.replace(/\*([^*]+)\*/g, (_m, c) => `<em>${c}</em>`);
132
+
133
+ // Headings ### / ## / #
134
+ s = s.replace(/^(#{1,4})\s+(.+)$/gm, (_m, hashes, content) => {
135
+ const level = hashes.length;
136
+ return `<h${level} class="chat-md-heading">${content}</h${level}>`;
137
+ });
138
+
139
+ // Horizontal rule ---
140
+ s = s.replace(/^-{3,}$/gm, '<hr class="chat-md-hr">');
141
+
142
+ // Unordered lists (- item or * item), consecutive lines
143
+ s = s.replace(/(?:^[\-\*]\s+.+$\n?)+/gm, (block) => {
144
+ const items = block.trim().split("\n").map(line =>
145
+ `<li>${line.replace(/^[\-\*]\s+/, "")}</li>`
146
+ ).join("");
147
+ return `<ul class="chat-md-list">${items}</ul>`;
148
+ });
149
+
150
+ // Ordered lists (1. item), consecutive lines
151
+ s = s.replace(/(?:^\d+\.\s+.+$\n?)+/gm, (block) => {
152
+ const items = block.trim().split("\n").map(line =>
153
+ `<li>${line.replace(/^\d+\.\s+/, "")}</li>`
154
+ ).join("");
155
+ return `<ol class="chat-md-list">${items}</ol>`;
156
+ });
157
+
158
+ // Line breaks (but not after block elements)
159
+ s = s.replace(/\n/g, "<br>");
160
+ // Clean up <br> right after block elements
161
+ s = s.replace(/(<\/(?:ul|ol|li|pre|h[1-4]|hr|details|div)>)\s*<br>/g, "$1");
162
+ s = s.replace(/<br>\s*(<(?:ul|ol|pre|h[1-4]|hr|details)[\s>])/g, "$1");
163
+
164
+ // Restore <details> blocks
165
+ for (var di = 0; di < detailsBlocks.length; di++) {
166
+ // Render inner markdown of the details block
167
+ var block = detailsBlocks[di];
168
+ // Extract summary and body
169
+ var sumMatch = block.match(/<summary>([\s\S]*?)<\/summary>/);
170
+ var sumText = sumMatch ? sumMatch[1] : "";
171
+ var bodyContent = block.replace(/<\/?details>/g, "").replace(/<summary>[\s\S]*?<\/summary>/, "").trim();
172
+ // Render code blocks inside details
173
+ bodyContent = bodyContent.replace(/```json\n([\s\S]*?)\n```/g, function(_m, code) {
174
+ return '<pre class="chat-error-detail-code">' + esc(code) + '</pre>';
175
+ });
176
+ bodyContent = bodyContent.replace(/```([\s\S]*?)```/g, function(_m, code) {
177
+ return '<pre>' + esc(code) + '</pre>';
178
+ });
179
+ s = s.replace("%%DETAILS_" + di + "%%", '<details class="chat-error-detail"><summary>' + sumText + '</summary>' + bodyContent + '</details>');
180
+ }
181
+
182
+ return s;
183
+ }
184
+
185
+ // ── Agents list loader ───────────────────────────────────────────────────────
186
+
187
+ async function loadChatAgents() {
188
+ if (chatState.agents !== null) return chatState.agents;
189
+ if (chatState.loading) return null;
190
+ chatState.loading = true;
191
+ const MAX_RETRIES = 2;
192
+ const RETRY_DELAY = 1500;
193
+ try {
194
+ let lastErr;
195
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
196
+ try {
197
+ const raw = await api("GET", "/api/chat/agents");
198
+ // API returns { res: [...] } or array directly
199
+ const list = extractList(raw.res ?? raw);
200
+ chatState.agents = list;
201
+ return list;
202
+ } catch (err) {
203
+ lastErr = err;
204
+ if (attempt < MAX_RETRIES) await new Promise(r => setTimeout(r, RETRY_DELAY));
205
+ }
206
+ }
207
+ throw lastErr;
208
+ } finally {
209
+ chatState.loading = false;
210
+ }
211
+ }
212
+
213
+ // ── Trace rendering ─────────────────────────────────────────────────────────
214
+
215
+ function renderProgressSteps(items) {
216
+ if (!items || items.length === 0) return "";
217
+ const steps = items.map(function(item) {
218
+ var name = (item.skill_info && item.skill_info.name) || item.agent_name || "Step";
219
+ var skillType = (item.skill_info && item.skill_info.type) || "";
220
+ var status = (item.status || "running").toLowerCase();
221
+ var icon = status === "completed" || status === "success" ? "✅"
222
+ : status === "failed" || status === "error" ? "❌"
223
+ : '<span class="trace-spinner"></span>';
224
+ var desc = item.description || "";
225
+
226
+ // Tool call details
227
+ var detailParts = [];
228
+
229
+ // Show skill args
230
+ if (item.skill_info && item.skill_info.args && item.skill_info.args.length > 0) {
231
+ var argsHtml = item.skill_info.args.map(function(arg) {
232
+ var val = arg.value;
233
+ if (typeof val === "object" && val !== null) val = JSON.stringify(val);
234
+ return '<span class="tool-arg"><span class="tool-arg-name">' + esc(arg.name || "") + ':</span> ' + esc(String(val || "")) + '</span>';
235
+ }).join("");
236
+ detailParts.push('<div class="tool-args">' + argsHtml + '</div>');
237
+ }
238
+
239
+ // Show input_message
240
+ if (item.input_message) {
241
+ detailParts.push('<div class="tool-io"><span class="tool-io-label">输入:</span> ' + esc(item.input_message) + '</div>');
242
+ }
243
+
244
+ // Show result/answer
245
+ var resultText = item.result || "";
246
+ if (!resultText && item.answer) {
247
+ resultText = typeof item.answer === "string" ? item.answer : JSON.stringify(item.answer, null, 2);
248
+ }
249
+ if (resultText) {
250
+ var truncated = resultText.length > 200 ? resultText.substring(0, 200) + "..." : resultText;
251
+ detailParts.push(
252
+ '<div class="tool-io">' +
253
+ '<span class="tool-io-label">结果:</span>' +
254
+ '<span class="tool-io-value">' + esc(truncated) + '</span>' +
255
+ (resultText.length > 200 ? '<div class="tool-result-full" style="display:none"><pre>' + esc(resultText) + '</pre></div><span class="tool-expand" onclick="var el=this.previousElementSibling;el.style.display=el.style.display===\'none\'?\'block\':\'none\';this.textContent=el.style.display===\'none\'?\'展开\':\'收起\'">展开</span>' : '') +
256
+ '</div>'
257
+ );
258
+ }
259
+
260
+ var detailHtml = detailParts.length > 0
261
+ ? '<div class="tool-detail">' + detailParts.join("") + '</div>'
262
+ : "";
263
+
264
+ var typeLabel = skillType ? '<span class="tool-type-badge">' + esc(skillType) + '</span>' : '';
265
+
266
+ return '<div class="trace-step trace-status-' + esc(status) + '">' +
267
+ '<span class="trace-step-icon">' + icon + '</span>' +
268
+ '<span class="trace-step-name">' + esc(name) + '</span>' +
269
+ typeLabel +
270
+ (desc ? '<span class="trace-step-desc">' + esc(desc) + '</span>' : "") +
271
+ detailHtml +
272
+ '</div>';
273
+ });
274
+ return '<div class="trace-steps">' + steps.join("") + '</div>';
275
+ }
276
+
277
+ // ── Tool Detail Slide-out Panel ──────────────────────────────────────────────
278
+
279
+ function showToolDetailPanel(detail, toolName) {
280
+ // Toggle: if panel is already open for the same tool, close it
281
+ var existing = document.querySelector(".tool-detail-panel");
282
+ if (existing) {
283
+ var isSameTool = existing.getAttribute("data-panel-tool") === toolName;
284
+ existing.classList.remove("tool-detail-panel-open");
285
+ setTimeout(function() { existing.remove(); }, 250);
286
+ if (isSameTool) return;
287
+ }
288
+
289
+ var panel = document.createElement("div");
290
+ panel.className = "tool-detail-panel";
291
+ panel.setAttribute("data-panel-tool", toolName || "");
292
+
293
+ var sections = [];
294
+
295
+ // Args / Input
296
+ if (detail.args && detail.args.length > 0) {
297
+ var argsHtml = detail.args.map(function(a) {
298
+ var val = a.value;
299
+ if (typeof val === "object" && val !== null) val = JSON.stringify(val, null, 2);
300
+ return '<div class="tool-detail-kv"><span class="tool-detail-key">' + esc(a.name || "") + '</span><span class="tool-detail-val">' + esc(String(val || "")) + '</span></div>';
301
+ }).join("");
302
+ sections.push('<div class="tool-detail-section"><div class="tool-detail-section-title">参数</div>' + argsHtml + '</div>');
303
+ }
304
+ if (detail.input) {
305
+ var inputStr = typeof detail.input === "string" ? detail.input : JSON.stringify(detail.input, null, 2);
306
+ sections.push('<div class="tool-detail-section"><div class="tool-detail-section-title">输入</div><pre>' + esc(inputStr) + '</pre></div>');
307
+ }
308
+
309
+ // Output
310
+ if (detail.output) {
311
+ var outputStr = typeof detail.output === "string" ? detail.output : JSON.stringify(detail.output, null, 2);
312
+ sections.push('<div class="tool-detail-section"><div class="tool-detail-section-title">输出</div><pre>' + esc(outputStr) + '</pre></div>');
313
+ }
314
+
315
+ panel.innerHTML =
316
+ '<div class="tool-detail-panel-header">' +
317
+ '<span class="tool-detail-panel-title">🔧 ' + esc(toolName || "工具详情") + '</span>' +
318
+ '<button class="tool-detail-panel-close" onclick="this.closest(\'.tool-detail-panel\').remove()">✕</button>' +
319
+ '</div>' +
320
+ '<div class="tool-detail-panel-body">' +
321
+ (sections.length > 0 ? sections.join("") : '<div class="tool-detail-empty">暂无详细数据</div>') +
322
+ '</div>';
323
+
324
+ document.body.appendChild(panel);
325
+ // Animate in
326
+ requestAnimationFrame(function() { panel.classList.add("tool-detail-panel-open"); });
327
+ }
328
+
329
+ async function fetchAndRenderTrace(bubbleEl, agentId, conversationId, $messagesEl) {
330
+ // Trace data may not be available immediately after chat ends — retry with backoff
331
+ var TRACE_RETRY_DELAYS = [2000, 4000];
332
+ var sessions = null;
333
+
334
+ for (var attempt = 0; attempt <= TRACE_RETRY_DELAYS.length; attempt++) {
335
+ if (attempt > 0) {
336
+ await new Promise(function(r) { setTimeout(r, TRACE_RETRY_DELAYS[attempt - 1]); });
337
+ }
338
+ try {
339
+ var data = await api("GET", "/api/chat/trace?agentId=" + enc(agentId) + "&conversationId=" + enc(conversationId));
340
+ sessions = Array.isArray(data) ? data
341
+ : Array.isArray(data?.sessions) ? data.sessions
342
+ : Array.isArray(data?.results) ? data.results
343
+ : extractList(data);
344
+ if (sessions && sessions.length > 0) break;
345
+ } catch (e) {
346
+ // Trace is best-effort
347
+ }
348
+ }
349
+
350
+ try {
351
+ if (!sessions || sessions.length === 0) return;
352
+
353
+ var latestSession = sessions[sessions.length - 1];
354
+ var spans = latestSession.spans || latestSession.steps || latestSession.traces || latestSession.operations || [];
355
+ if (spans.length === 0) return;
356
+
357
+ var totalDuration = spans.reduce(function(sum, s) { return sum + (s.duration_ms || s.duration || 0); }, 0);
358
+ var totalSec = (totalDuration / 1000).toFixed(1);
359
+
360
+ var $trace = bubbleEl.querySelector(".trace-section");
361
+ if (!$trace) {
362
+ $trace = document.createElement("div");
363
+ $trace.className = "trace-section trace-expanded";
364
+ bubbleEl.appendChild($trace);
365
+ }
366
+
367
+ $trace.innerHTML =
368
+ '<div class="trace-header" onclick="this.parentElement.classList.toggle(\'trace-expanded\')">' +
369
+ '<span class="trace-toggle">▶</span> ' +
370
+ '执行过程 (' + spans.length + ' 步, ' + totalSec + 's)' +
371
+ '</div>' +
372
+ '<div class="trace-body">' +
373
+ spans.map(renderTraceSpan).join("") +
374
+ '</div>';
375
+
376
+ if ($messagesEl) $messagesEl.scrollTop = $messagesEl.scrollHeight;
377
+ } catch (e) {
378
+ // Trace is best-effort, don't break the conversation
379
+ }
380
+ }
381
+
382
+ function renderTraceSpan(span) {
383
+ var name = span.name || span.skill_name || span.operation_name || "Step";
384
+ var status = (span.status || "completed").toLowerCase();
385
+ var icon = status === "completed" || status === "success" || status === "ok" ? "✅"
386
+ : status === "failed" || status === "error" ? "❌"
387
+ : "⏳";
388
+ var durationMs = span.duration_ms || span.duration || 0;
389
+ var durationLabel = durationMs >= 1000 ? (durationMs / 1000).toFixed(1) + "s" : durationMs + "ms";
390
+
391
+ var hasDetail = span.input || span.output || span.args || span.result;
392
+ var detailHtml = "";
393
+ if (hasDetail) {
394
+ detailHtml =
395
+ '<div class="trace-detail">' +
396
+ '<div class="trace-detail-toggle" onclick="this.parentElement.classList.toggle(\'trace-detail-expanded\')">▶ 查看详情</div>' +
397
+ '<div class="trace-detail-content">' +
398
+ (span.input ? '<div class="trace-kv"><span class="trace-kv-label">输入:</span><pre>' + esc(typeof span.input === "string" ? span.input : JSON.stringify(span.input, null, 2)) + '</pre></div>' : "") +
399
+ (span.args ? '<div class="trace-kv"><span class="trace-kv-label">参数:</span><pre>' + esc(typeof span.args === "string" ? span.args : JSON.stringify(span.args, null, 2)) + '</pre></div>' : "") +
400
+ (span.output ? '<div class="trace-kv"><span class="trace-kv-label">输出:</span><pre>' + esc(typeof span.output === "string" ? span.output : JSON.stringify(span.output, null, 2)) + '</pre></div>' : "") +
401
+ (span.result ? '<div class="trace-kv"><span class="trace-kv-label">结果:</span><pre>' + esc(typeof span.result === "string" ? span.result : JSON.stringify(span.result, null, 2)) + '</pre></div>' : "") +
402
+ '</div>' +
403
+ '</div>';
404
+ }
405
+
406
+ return '<div class="trace-step trace-status-' + esc(status) + '">' +
407
+ '<span class="trace-step-icon">' + icon + '</span>' +
408
+ '<span class="trace-step-name">' + esc(name) + '</span>' +
409
+ '<span class="trace-step-duration">' + esc(durationLabel) + '</span>' +
410
+ detailHtml +
411
+ '</div>';
412
+ }
413
+
414
+ // ── Send message ─────────────────────────────────────────────────────────────
415
+
416
+ function showStopButton($sendBtn) {
417
+ $sendBtn.classList.add("chat-stop-mode");
418
+ $sendBtn.innerHTML = `
419
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" style="vertical-align: middle; margin-right: 4px;"><rect x="4" y="4" width="16" height="16" rx="2"></rect></svg>
420
+ Stop`;
421
+ $sendBtn.disabled = false;
422
+ }
423
+
424
+ function showSendButton($sendBtn) {
425
+ $sendBtn.classList.remove("chat-stop-mode");
426
+ $sendBtn.innerHTML = `
427
+ Send
428
+ <svg style="vertical-align: middle; margin-left: 4px;" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="2" x2="11" y2="13"></line><polygon points="22 2 15 22 11 13 2 9 22 2"></polygon></svg>`;
429
+ $sendBtn.disabled = false;
430
+ }
431
+
432
+ async function chatSend($messagesEl, $inputEl, $sendBtn, agentId) {
433
+ const message = $inputEl.value.trim();
434
+ if (!message || chatState.streaming) return;
435
+
436
+ $inputEl.value = "";
437
+ $inputEl.disabled = true;
438
+ chatState.streaming = true;
439
+
440
+ // Show stop button
441
+ const abortController = new AbortController();
442
+ chatState.abortController = abortController;
443
+ showStopButton($sendBtn);
444
+
445
+ // Remove welcome box if present
446
+ const welcome = $messagesEl.querySelector('.chat-welcome-box');
447
+ if (welcome) welcome.remove();
448
+
449
+ // Append user bubble
450
+ const userRow = document.createElement("div");
451
+ userRow.className = "chat-message-row user";
452
+ userRow.innerHTML = `
453
+ <div class="chat-bubble chat-bubble-user">
454
+ <div class="chat-bubble-content">${chatMarkdown(message)}</div>
455
+ </div>
456
+ <div class="chat-avatar">👤</div>
457
+ `;
458
+ $messagesEl.appendChild(userRow);
459
+
460
+ const agent = (chatState.agents ?? []).find(a => (a.id || a.agent_id) === agentId);
461
+ const agentName = agent ? (agent.name || agent.agent_name || agentId) : agentId;
462
+
463
+ // Container for the entire assistant response (interleaved: segment → tool → segment → tool → ...)
464
+ const assistantContainer = document.createElement("div");
465
+ assistantContainer.className = "chat-assistant-container";
466
+
467
+ // Current streaming bubble (always at the bottom)
468
+ const currentBubble = document.createElement("div");
469
+ currentBubble.className = "chat-bubble chat-bubble-assistant";
470
+ currentBubble.innerHTML = `<div class="chat-bubble-sender">${esc(agentName)}</div>`;
471
+
472
+ const contentSpan = document.createElement("div");
473
+ contentSpan.className = "chat-bubble-content";
474
+ contentSpan.innerHTML = '<div class="chat-thinking-pulse"><div class="dot"></div><div class="dot"></div><div class="dot"></div></div>';
475
+ currentBubble.appendChild(contentSpan);
476
+ assistantContainer.appendChild(currentBubble);
477
+
478
+ // Wrap in a message row
479
+ const assistantRow = document.createElement("div");
480
+ assistantRow.className = "chat-message-row assistant";
481
+ assistantRow.innerHTML = `<div class="chat-avatar">🤖</div>`;
482
+ assistantRow.appendChild(assistantContainer);
483
+
484
+ $messagesEl.appendChild(assistantRow);
485
+ $messagesEl.scrollTop = $messagesEl.scrollHeight;
486
+
487
+ // Smart auto-scroll: only scroll if user is near the bottom
488
+ let userScrolledUp = false;
489
+ function onUserScroll() {
490
+ var threshold = 80;
491
+ userScrolledUp = ($messagesEl.scrollHeight - $messagesEl.scrollTop - $messagesEl.clientHeight) > threshold;
492
+ }
493
+ $messagesEl.addEventListener("scroll", onUserScroll);
494
+ function autoScroll() {
495
+ if (!userScrolledUp) {
496
+ $messagesEl.scrollTop = $messagesEl.scrollHeight;
497
+ }
498
+ }
499
+
500
+ // Persist user message
501
+ if (!chatState.conversations[agentId]) chatState.conversations[agentId] = [];
502
+ chatState.conversations[agentId].push({ role: "user", text: message });
503
+
504
+ const conversationId = chatState.currentConversationId ?? undefined;
505
+ let aborted = false;
506
+ let stepCount = 0;
507
+ let receivedDone = false;
508
+
509
+ try {
510
+ const res = await fetch("/api/chat/send", {
511
+ method: "POST",
512
+ headers: { "Content-Type": "application/json" },
513
+ body: JSON.stringify({ agentId, message, conversationId }),
514
+ signal: abortController.signal,
515
+ });
516
+
517
+ if (!res.ok || !res.body) {
518
+ const errText = await res.text().catch(() => res.statusText);
519
+ // Detect auth errors and show friendly message
520
+ var isAuthError = res.status === 401 || res.status === 403;
521
+ if (!isAuthError) {
522
+ try {
523
+ var errObj = JSON.parse(errText);
524
+ isAuthError = errObj.upstream_status === 401 || errObj.upstream_status === 403
525
+ || (errObj.error && /认证|auth|token|expired|unauthorized/i.test(errObj.error));
526
+ } catch(e) {}
527
+ }
528
+ if (isAuthError) {
529
+ contentSpan.innerHTML = '<div class="chat-auth-error">' +
530
+ '<div class="chat-auth-error-icon">🔒</div>' +
531
+ '<div class="chat-auth-error-title">认证已过期</div>' +
532
+ '<div class="chat-auth-error-desc">登录凭证已失效,请在终端重新登录后刷新页面</div>' +
533
+ '<div class="chat-auth-error-cmd"><code>bkn login</code></div>' +
534
+ '</div>';
535
+ } else {
536
+ var displayErr = errText;
537
+ try {
538
+ var parsedErr = JSON.parse(errText);
539
+ displayErr = parsedErr.error || errText;
540
+ if (parsedErr.detail) {
541
+ var detailContent = parsedErr.detail;
542
+ try { detailContent = JSON.stringify(JSON.parse(parsedErr.detail), null, 2); } catch(e2) {}
543
+ contentSpan.innerHTML = '<span class="chat-error">⚠️ ' + esc(displayErr) + '</span>' +
544
+ '<details class="chat-error-detail"><summary>详细信息</summary><pre>' + esc(detailContent) + '</pre></details>';
545
+ chatState.streaming = false;
546
+ chatState.abortController = null;
547
+ $inputEl.disabled = false;
548
+ showSendButton($sendBtn);
549
+ return;
550
+ }
551
+ } catch(e3) {}
552
+ contentSpan.innerHTML = `<span class="chat-error">⚠️ Error ${res.status}: ${esc(displayErr)}</span>`;
553
+ }
554
+ chatState.streaming = false;
555
+ chatState.abortController = null;
556
+ $inputEl.disabled = false;
557
+ showSendButton($sendBtn);
558
+ return;
559
+ }
560
+
561
+ const reader = res.body.getReader();
562
+ const decoder = new TextDecoder();
563
+ let buf = "";
564
+ let lastText = "";
565
+
566
+ const gen = navGeneration;
567
+ // Stall detection: if no data arrives for 30s, show warning; 90s = abort
568
+ const STALL_WARN_MS = 30000;
569
+ const STALL_ABORT_MS = 90000;
570
+ let lastDataTime = Date.now();
571
+ let stallWarningShown = false;
572
+ const stallInterval = setInterval(() => {
573
+ const elapsed = Date.now() - lastDataTime;
574
+ if (elapsed >= STALL_ABORT_MS) {
575
+ abortController.abort();
576
+ } else if (elapsed >= STALL_WARN_MS && !stallWarningShown) {
577
+ stallWarningShown = true;
578
+ var warn = document.createElement("div");
579
+ warn.className = "chat-stall-warning";
580
+ warn.textContent = "等待响应中…如长时间无响应,可点击 Stop 重试。";
581
+ currentBubble.appendChild(warn);
582
+ }
583
+ }, 3000);
584
+
585
+ while (true) {
586
+ const { done, value } = await reader.read();
587
+ if (done) break;
588
+ if (navGeneration !== gen) { reader.cancel(); break; }
589
+ if (abortController.signal.aborted) { reader.cancel(); aborted = true; break; }
590
+ lastDataTime = Date.now();
591
+ if (stallWarningShown) {
592
+ stallWarningShown = false;
593
+ var existingWarn = currentBubble.querySelector(".chat-stall-warning");
594
+ if (existingWarn) existingWarn.remove();
595
+ }
596
+
597
+ buf += decoder.decode(value, { stream: true });
598
+ const lines = buf.split("\n");
599
+ buf = lines.pop() ?? "";
600
+
601
+ for (const line of lines) {
602
+ if (!line.startsWith("data: ")) continue;
603
+ const dataStr = line.slice(6).trim();
604
+ if (!dataStr) continue;
605
+ let evt;
606
+ try { evt = JSON.parse(dataStr); } catch { continue; }
607
+
608
+ if (evt.type === "segment") {
609
+ var segText = (evt.text || "").trim();
610
+ // Skip trivial segments: too short, just agent name, or whitespace only
611
+ if (segText.length < 5 || segText === agentName) {
612
+ contentSpan.innerHTML = '<div class="chat-thinking-pulse"><div class="dot"></div><div class="dot"></div><div class="dot"></div></div>';
613
+ } else {
614
+ stepCount++;
615
+ // Insert segment bubble before the current streaming bubble
616
+ var segBubble = document.createElement("div");
617
+ segBubble.className = "chat-bubble chat-bubble-assistant chat-segment-bubble";
618
+ segBubble.innerHTML = '<div class="chat-bubble-content">' + chatMarkdown(segText) + '</div>';
619
+ assistantContainer.insertBefore(segBubble, currentBubble);
620
+ // Reset streaming bubble for next phase
621
+ contentSpan.innerHTML = '<div class="chat-thinking-pulse"><div class="dot"></div><div class="dot"></div><div class="dot"></div></div>';
622
+ }
623
+ autoScroll();
624
+ } else if (evt.type === "text") {
625
+ lastText = evt.fullText ?? "";
626
+ var currentText = evt.currentText ?? lastText;
627
+ if (currentText) {
628
+ contentSpan.innerHTML = chatMarkdown(currentText);
629
+ contentSpan.classList.add("chat-streaming-cursor");
630
+ // Detect if text contains a terminal error — flag for auto-stop
631
+ if (/event:error[\s\S]*?"code"/.test(currentText)) {
632
+ chatState.conversations[agentId].push({ role: "assistant", text: lastText });
633
+ reader.cancel();
634
+ aborted = true;
635
+ }
636
+ }
637
+ autoScroll();
638
+ } else if (evt.type === "step_meta") {
639
+ // Tool call metadata — dedup by tool id, update in place
640
+ var meta = evt.meta || {};
641
+ if (!meta || meta === null) { /* null meta = reset, skip */ }
642
+ else {
643
+ var statusText = meta.status || "";
644
+ var toolId = meta.id || (meta.skill_info && meta.skill_info.name) || meta.agent_name || "";
645
+ var toolName = (meta.skill_info && meta.skill_info.name) || meta.agent_name || "";
646
+ if (!toolName && meta.description) toolName = meta.description;
647
+ if (!toolId) toolId = toolName;
648
+ // Skip if no meaningful info
649
+ if (toolName || statusText) {
650
+ var isRunning = statusText === "running" || statusText === "processing";
651
+ var isCompleted = statusText === "completed" || statusText === "success";
652
+ var isFailed = statusText === "failed" || statusText === "error";
653
+
654
+ // Build brief args string from skill_info.args
655
+ var argsStr = "";
656
+ if (meta.skill_info && meta.skill_info.args && meta.skill_info.args.length > 0) {
657
+ argsStr = meta.skill_info.args.map(function(a) {
658
+ var v = a.value;
659
+ if (typeof v === "string" && v.length > 30) v = v.substring(0, 30) + "…";
660
+ if (typeof v === "object" && v !== null) v = JSON.stringify(v).substring(0, 30) + "…";
661
+ return (a.name || "") + "=" + v;
662
+ }).join(", ");
663
+ } else if (meta.input_message) {
664
+ argsStr = meta.input_message.length > 60 ? meta.input_message.substring(0, 60) + "…" : meta.input_message;
665
+ }
666
+
667
+ // Format time
668
+ var timeStr = "";
669
+ var tsRaw = meta.end_time || meta.start_time;
670
+ if (tsRaw) {
671
+ var ts = parseFloat(String(tsRaw));
672
+ if (ts > 1e9 && ts < 1e12) timeStr = new Date(ts * 1000).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
673
+ else if (ts > 1e12) timeStr = new Date(ts).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
674
+ }
675
+
676
+ var icon = isRunning ? '<span class="trace-spinner"></span>' : isCompleted ? "✅" : isFailed ? "❌" : "🔧";
677
+
678
+ // Build tool card HTML
679
+ var cardHtml = '<span class="chat-tool-call-icon">' + icon + '</span>' +
680
+ '<span class="chat-tool-call-name">' + esc(toolName || statusText) + '</span>' +
681
+ (argsStr ? '<span class="chat-tool-call-args">(' + esc(argsStr) + ')</span>' : '') +
682
+ (timeStr ? '<span class="chat-tool-call-time">' + esc(timeStr) + '</span>' : '');
683
+
684
+ // Store full I/O data for detail panel
685
+ var detailData = {};
686
+ if (meta.input_message) detailData.input = meta.input_message;
687
+ if (meta.skill_info && meta.skill_info.args) detailData.args = meta.skill_info.args;
688
+ if (meta.block_answer) detailData.output = meta.block_answer;
689
+
690
+ // Dedup: find existing card for same tool id
691
+ var existingCard = assistantContainer.querySelector('.chat-tool-call[data-tool-id="' + esc(toolId) + '"]');
692
+ if (existingCard) {
693
+ // Update existing card
694
+ existingCard.innerHTML = cardHtml;
695
+ existingCard.className = "chat-tool-call" + (isRunning ? " chat-tool-call-running" : "");
696
+ if (Object.keys(detailData).length > 0) {
697
+ existingCard.setAttribute("data-tool-detail", JSON.stringify(detailData));
698
+ }
699
+ } else {
700
+ var toolCard = document.createElement("div");
701
+ toolCard.className = "chat-tool-call" + (isRunning ? " chat-tool-call-running" : "");
702
+ toolCard.setAttribute("data-tool-id", toolId);
703
+ toolCard.innerHTML = cardHtml;
704
+ if (Object.keys(detailData).length > 0) {
705
+ toolCard.setAttribute("data-tool-detail", JSON.stringify(detailData));
706
+ }
707
+ // Click to show detail panel
708
+ toolCard.addEventListener("click", function() {
709
+ var raw = this.getAttribute("data-tool-detail");
710
+ if (!raw) return;
711
+ showToolDetailPanel(JSON.parse(raw), this.querySelector(".chat-tool-call-name").textContent);
712
+ });
713
+ toolCard.style.cursor = "pointer";
714
+ toolCard.title = "点击查看详情";
715
+ assistantContainer.insertBefore(toolCard, currentBubble);
716
+ }
717
+ autoScroll();
718
+ }
719
+ }
720
+ } else if (evt.type === "progress" && Array.isArray(evt.items)) {
721
+ // Insert/update progress steps before the streaming bubble
722
+ var existingProgress = assistantContainer.querySelector(".chat-progress-inline");
723
+ if (!existingProgress) {
724
+ existingProgress = document.createElement("div");
725
+ existingProgress.className = "chat-progress-inline";
726
+ assistantContainer.insertBefore(existingProgress, currentBubble);
727
+ }
728
+ existingProgress.innerHTML = renderProgressSteps(evt.items);
729
+ autoScroll();
730
+ } else if (evt.type === "done") {
731
+ receivedDone = true;
732
+ chatState.currentConversationId = evt.conversationId || chatState.currentConversationId;
733
+ if (lastText) {
734
+ chatState.conversations[agentId].push({ role: "assistant", text: lastText });
735
+ }
736
+ // Fetch full trace after completion — append at end of container
737
+ const traceConvId = evt.conversationId || chatState.currentConversationId;
738
+ if (traceConvId) {
739
+ var traceHolder = document.createElement("div");
740
+ traceHolder.className = "chat-trace-holder";
741
+ assistantContainer.appendChild(traceHolder);
742
+ fetchAndRenderTrace(traceHolder, agentId, traceConvId, $messagesEl);
743
+ }
744
+ } else if (evt.type === "conversation_id") {
745
+ chatState.currentConversationId = evt.conversationId;
746
+ } else if (evt.type === "error") {
747
+ var errHtml = '<span class="chat-error">⚠️ ' + esc(evt.error) + '</span>';
748
+ if (evt.detail) {
749
+ var detailStr = evt.detail;
750
+ try {
751
+ detailStr = JSON.stringify(JSON.parse(evt.detail), null, 2);
752
+ } catch(e) {}
753
+ errHtml += '<details class="chat-error-detail"><summary>详细信息</summary><pre>' + esc(detailStr) + '</pre></details>';
754
+ }
755
+ contentSpan.innerHTML = errHtml;
756
+ }
757
+ }
758
+ }
759
+
760
+ clearInterval(stallInterval);
761
+ contentSpan.classList.remove("chat-streaming-cursor");
762
+ var leftoverWarn = currentBubble.querySelector(".chat-stall-warning");
763
+ if (leftoverWarn) leftoverWarn.remove();
764
+
765
+ if (aborted) {
766
+ if (lastText) {
767
+ chatState.conversations[agentId].push({ role: "assistant", text: lastText });
768
+ } else {
769
+ contentSpan.innerHTML = '<span class="chat-stopped">Response stopped.</span>';
770
+ }
771
+ } else if (!lastText) {
772
+ contentSpan.innerHTML = '<span class="chat-error">No response received.</span>';
773
+ }
774
+
775
+ // Fallback: if stream ended without a "done" event (e.g. 502, connection reset),
776
+ // still attempt to fetch trace using whatever conversationId we have
777
+ if (!receivedDone && !aborted && lastText) {
778
+ chatState.conversations[agentId].push({ role: "assistant", text: lastText });
779
+ var fallbackConvId = chatState.currentConversationId;
780
+ if (fallbackConvId) {
781
+ var traceHolder = document.createElement("div");
782
+ traceHolder.className = "chat-trace-holder";
783
+ assistantContainer.appendChild(traceHolder);
784
+ fetchAndRenderTrace(traceHolder, agentId, fallbackConvId, $messagesEl);
785
+ }
786
+ }
787
+ } catch (err) {
788
+ clearInterval(stallInterval);
789
+ contentSpan.classList.remove("chat-streaming-cursor");
790
+ if (err.name === "AbortError") {
791
+ contentSpan.innerHTML = '<span class="chat-stopped">Response stopped.</span>';
792
+ } else {
793
+ contentSpan.innerHTML = `<span class="chat-error">${esc(err.message || String(err))}</span>`;
794
+ }
795
+ }
796
+
797
+ chatState.streaming = false;
798
+ chatState.abortController = null;
799
+ $inputEl.disabled = false;
800
+ showSendButton($sendBtn);
801
+ $inputEl.focus();
802
+ $messagesEl.removeEventListener("scroll", onUserScroll);
803
+ $messagesEl.scrollTop = $messagesEl.scrollHeight;
804
+ }
805
+
806
+ // ── Render chat conversation area ────────────────────────────────────────────
807
+
808
+ function renderChatConversation($el, agentId, agentName) {
809
+ const history = chatState.conversations[agentId] ?? [];
810
+
811
+ $el.innerHTML = `
812
+ <div class="chat-pane">
813
+ <div class="chat-header">
814
+ <span class="chat-agent-name">${esc(agentName)}</span>
815
+ <button class="chat-clear-btn" onclick="chatClearConversation(${JSON.stringify(agentId)})" title="Clear Conversation">
816
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="vertical-align: middle; margin-right: 4px;"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path></svg>
817
+ Clear
818
+ </button>
819
+ </div>
820
+ <div class="chat-messages" id="chat-messages">
821
+ ${history.length === 0
822
+ ? `<div class="chat-welcome-box">
823
+ <div class="chat-welcome-icon">💭</div>
824
+ <div class="chat-welcome-text">Start a conversation with <strong>${esc(agentName)}</strong></div>
825
+ </div>`
826
+ : history.map(msg => window.renderBubble(msg, agentName)).join("")}
827
+ </div>
828
+ <div class="chat-input-bar">
829
+ <textarea id="chat-input" class="chat-input" rows="1" placeholder="Type a message…" ${chatState.streaming ? "disabled" : ""}></textarea>
830
+ <button id="chat-send-btn" class="chat-send-btn" ${chatState.streaming ? "disabled" : ""}>
831
+ Send
832
+ <svg style="vertical-align: middle; margin-left: 4px;" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="2" x2="11" y2="13"></line><polygon points="22 2 15 22 11 13 2 9 22 2"></polygon></svg>
833
+ </button>
834
+ </div>
835
+ </div>
836
+ `;
837
+
838
+ const $messages = $el.querySelector("#chat-messages");
839
+ const $input = $el.querySelector("#chat-input");
840
+ const $sendBtn = $el.querySelector("#chat-send-btn");
841
+
842
+ // Scroll to bottom
843
+ if ($messages) $messages.scrollTop = $messages.scrollHeight;
844
+
845
+ // Wire up send / stop
846
+ const doSend = () => chatSend($messages, $input, $sendBtn, agentId);
847
+ const doStop = () => {
848
+ if (chatState.abortController) {
849
+ chatState.abortController.abort();
850
+ }
851
+ };
852
+ $sendBtn.addEventListener("click", () => {
853
+ if (chatState.streaming) {
854
+ doStop();
855
+ } else {
856
+ doSend();
857
+ }
858
+ });
859
+ $input.addEventListener("keydown", (e) => {
860
+ if (e.key === "Enter" && !e.shiftKey) {
861
+ e.preventDefault();
862
+ doSend();
863
+ }
864
+ });
865
+
866
+ if (!chatState.streaming) $input.focus();
867
+ }
868
+
869
+ // ── Clear conversation ───────────────────────────────────────────────────────
870
+
871
+ function chatClearConversation(agentId) {
872
+ chatState.conversations[agentId] = [];
873
+ chatState.currentConversationId = undefined;
874
+ // Re-render conversation area
875
+ const $pane = document.getElementById("chat-conversation-pane");
876
+ if ($pane) {
877
+ const agent = (chatState.agents ?? []).find(a => (a.id || a.agent_id) === agentId);
878
+ const name = agent ? (agent.name || agent.agent_name || agentId) : agentId;
879
+ renderChatConversation($pane, agentId, name);
880
+ }
881
+ }
882
+
883
+ // ── Select agent ─────────────────────────────────────────────────────────────
884
+
885
+ function chatSelectAgent(agentId) {
886
+ if (chatState.currentAgentId === agentId) return;
887
+ chatState.currentAgentId = agentId;
888
+ // Reset conversation ID for new agent unless we have history
889
+ if (!chatState.conversations[agentId] || chatState.conversations[agentId].length === 0) {
890
+ chatState.currentConversationId = undefined;
891
+ }
892
+ location.hash = `#/chat/${enc(agentId)}`;
893
+ }
894
+
895
+ // ── Main render ──────────────────────────────────────────────────────────────
896
+
897
+ async function renderChat($el, parts, _params) {
898
+ const gen = navGeneration;
899
+
900
+ // Determine target agent from URL parts
901
+ const urlAgentId = parts && parts[0] ? decodeURIComponent(parts[0]) : null;
902
+
903
+ $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>';
904
+
905
+ let agents;
906
+ try {
907
+ agents = await loadChatAgents();
908
+ } catch (err) {
909
+ if (navGeneration !== gen) return;
910
+ $el.innerHTML = `<div class="error-banner">Failed to load agents: ${esc(err.message || String(err))}</div>`;
911
+ return;
912
+ }
913
+
914
+ if (navGeneration !== gen) return;
915
+
916
+ if (!agents || agents.length === 0) {
917
+ $el.innerHTML = '<div class="error-banner">No published decision agents found. Publish an agent in KWeaver Core first.</div>';
918
+ return;
919
+ }
920
+
921
+ // Detect test agents and sort them to the end
922
+ function isTestAgent(agent) {
923
+ const name = (agent.name || agent.agent_name || "").toLowerCase();
924
+ const key = (agent.key || "").toLowerCase();
925
+ const byName = (agent.published_by_name || "").toLowerCase();
926
+ return byName === "testbot"
927
+ || /测试智能体/.test(name)
928
+ || /test[_-]agent/.test(key)
929
+ || /^(api|chat|publish|unpublish)[_-]test/.test(key);
930
+ }
931
+
932
+ // Sort: non-test first, then test; within each group keep original order
933
+ agents.sort((a, b) => {
934
+ const aTest = isTestAgent(a) ? 1 : 0;
935
+ const bTest = isTestAgent(b) ? 1 : 0;
936
+ return aTest - bTest;
937
+ });
938
+
939
+ // Pick active agent
940
+ let activeAgentId = urlAgentId || chatState.currentAgentId;
941
+ if (!activeAgentId || !agents.find(a => (a.id || a.agent_id) === activeAgentId)) {
942
+ activeAgentId = agents[0].id || agents[0].agent_id;
943
+ }
944
+ chatState.currentAgentId = activeAgentId;
945
+
946
+ // Layout: sidebar + conversation
947
+ $el.innerHTML = `
948
+ <div class="chat-layout">
949
+ <div class="chat-sidebar" id="chat-sidebar">
950
+ <div class="chat-sidebar-header">Decision Agents</div>
951
+ <div class="chat-sidebar-search">
952
+ <input type="text" id="chat-agent-search" placeholder="Search agents..." oninput="chatFilterAgents(this.value)">
953
+ </div>
954
+ <div class="chat-agent-list" id="chat-agent-list">
955
+ ${agents.map(agent => {
956
+ const id = agent.id || agent.agent_id;
957
+ const name = agent.name || agent.agent_name || id;
958
+ const desc = agent.description || "";
959
+ const isActive = id === activeAgentId;
960
+ const testFlag = isTestAgent(agent);
961
+ return `<div class="chat-agent-item${isActive ? " active" : ""}${testFlag ? " chat-agent-test" : ""}" data-agent-id="${esc(id)}" onclick="chatSelectAgent(${esc(JSON.stringify(id))})">
962
+ <div class="chat-agent-item-icon">${testFlag ? "🧪" : "🤖"}</div>
963
+ <div class="chat-agent-item-content">
964
+ <div class="chat-agent-item-name">${esc(name)}${testFlag ? ' <span class="chat-test-badge">测试</span>' : ""}</div>
965
+ ${desc ? `<div class="chat-agent-item-desc">${esc(desc)}</div>` : ""}
966
+ </div>
967
+ </div>`;
968
+ }).join("")}
969
+ </div>
970
+ </div>
971
+ <div class="chat-conversation" id="chat-conversation-pane"></div>
972
+ </div>
973
+ `;
974
+
975
+ // Render the conversation for the active agent
976
+ const activeAgent = agents.find(a => (a.id || a.agent_id) === activeAgentId);
977
+ const activeName = activeAgent ? (activeAgent.name || activeAgent.agent_name || activeAgentId) : activeAgentId;
978
+ const $pane = $el.querySelector("#chat-conversation-pane");
979
+ renderChatConversation($pane, activeAgentId, activeName);
980
+ }