@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.
- package/README.md +19 -1
- package/README.zh.md +19 -1
- package/dist/api/agent-chat.d.ts +7 -1
- package/dist/api/agent-chat.js +146 -40
- package/dist/api/agent-list.js +13 -13
- package/dist/api/business-domains.js +9 -5
- package/dist/api/context-loader.js +4 -1
- package/dist/api/conversations.js +4 -9
- package/dist/api/dataflow2.d.ts +95 -0
- package/dist/api/dataflow2.js +80 -0
- package/dist/api/headers.d.ts +2 -0
- package/dist/api/headers.js +7 -2
- package/dist/api/skills.js +2 -10
- package/dist/api/vega.d.ts +0 -16
- package/dist/api/vega.js +0 -33
- package/dist/auth/oauth.d.ts +1 -1
- package/dist/auth/oauth.js +64 -7
- package/dist/cli.js +21 -1
- package/dist/client.d.ts +9 -0
- package/dist/client.js +48 -8
- package/dist/commands/auth.js +80 -32
- package/dist/commands/bkn-schema.js +22 -0
- package/dist/commands/call.js +8 -5
- package/dist/commands/dataflow.d.ts +1 -0
- package/dist/commands/dataflow.js +251 -0
- package/dist/commands/explore-bkn.d.ts +79 -0
- package/dist/commands/explore-bkn.js +273 -0
- package/dist/commands/explore-chat.d.ts +3 -0
- package/dist/commands/explore-chat.js +193 -0
- package/dist/commands/explore-vega.d.ts +3 -0
- package/dist/commands/explore-vega.js +71 -0
- package/dist/commands/explore.d.ts +9 -0
- package/dist/commands/explore.js +258 -0
- package/dist/commands/vega.js +2 -104
- package/dist/config/no-auth.d.ts +3 -0
- package/dist/config/no-auth.js +5 -0
- package/dist/config/store.d.ts +8 -0
- package/dist/config/store.js +22 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/kweaver.d.ts +5 -0
- package/dist/kweaver.js +32 -2
- package/dist/resources/bkn.js +2 -3
- package/dist/resources/knowledge-networks.js +3 -8
- package/dist/resources/vega.d.ts +0 -6
- package/dist/resources/vega.js +1 -10
- package/dist/templates/explorer/app.js +136 -0
- package/dist/templates/explorer/bkn.js +747 -0
- package/dist/templates/explorer/chat.js +980 -0
- package/dist/templates/explorer/dashboard.js +82 -0
- package/dist/templates/explorer/index.html +35 -0
- package/dist/templates/explorer/style.css +2440 -0
- package/dist/templates/explorer/vega.js +291 -0
- package/dist/utils/http.d.ts +3 -0
- package/dist/utils/http.js +37 -1
- 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(/<(think|thought|thinking)>([\s\S]*?)<\/\1>/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*(?:"|")[^\]]+(?:"|")\s*\]/g, (match) => {
|
|
113
|
+
try {
|
|
114
|
+
let unescaped = match.replace(/"/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
|
+
}
|