index.html 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570
  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <title>Multi-Agent 实战演示</title>
  7. <style>
  8. :root {
  9. --bg: #0a0a12;
  10. --glass: rgba(255,255,255,0.06);
  11. --glass-border: rgba(255,255,255,0.1);
  12. --glass-highlight: rgba(255,255,255,0.15);
  13. --text: #e8eaed;
  14. --text-dim: #8a8d93;
  15. --accent: #00E5A0;
  16. --accent-dim: rgba(0,229,160,0.15);
  17. --blue: #58a6ff;
  18. --purple: #bc8cff;
  19. --yellow: #d29922;
  20. --red: #f85149;
  21. --radius: 20px;
  22. --blur: 40px;
  23. }
  24. * { margin:0; padding:0; box-sizing:border-box; }
  25. body {
  26. font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'SF Pro Text', 'Helvetica Neue', sans-serif;
  27. background: var(--bg);
  28. color: var(--text);
  29. min-height: 100vh;
  30. overflow-x: hidden;
  31. -webkit-font-smoothing: antialiased;
  32. }
  33. /* ── Animated Background Orbs ── */
  34. .bg-orbs { position: fixed; inset: 0; pointer-events: none; z-index: 0; overflow: hidden; }
  35. .orb {
  36. position: absolute; border-radius: 50%;
  37. filter: blur(120px); opacity: 0.4;
  38. animation: float 20s ease-in-out infinite;
  39. }
  40. .orb-1 { width: 600px; height: 600px; background: #00E5A0; top: -15%; left: -10%; animation-delay: 0s; }
  41. .orb-2 { width: 500px; height: 500px; background: #58a6ff; bottom: -10%; right: -8%; animation-delay: -7s; }
  42. .orb-3 { width: 400px; height: 400px; background: #bc8cff; top: 50%; left: 50%; transform: translate(-50%,-50%); animation-delay: -14s; }
  43. @keyframes float {
  44. 0%, 100% { transform: translate(0, 0) scale(1); }
  45. 33% { transform: translate(30px, -40px) scale(1.05); }
  46. 66% { transform: translate(-20px, 30px) scale(0.95); }
  47. }
  48. /* ── Liquid Glass Card ── */
  49. .glass {
  50. position: relative;
  51. background: var(--glass);
  52. backdrop-filter: blur(var(--blur)) saturate(2) brightness(1.08);
  53. -webkit-backdrop-filter: blur(var(--blur)) saturate(2) brightness(1.08);
  54. border-radius: var(--radius);
  55. overflow: hidden;
  56. box-shadow:
  57. inset 0 1px 0 rgba(255,255,255,0.15),
  58. inset 0 -0.5px 0 rgba(255,255,255,0.05),
  59. 0 0 0 0.5px rgba(255,255,255,0.06),
  60. 0 8px 32px rgba(0,0,0,0.3);
  61. }
  62. .glass::before {
  63. content: '';
  64. position: absolute; inset: 0;
  65. background: linear-gradient(
  66. 160deg,
  67. rgba(255,255,255,0.12) 0%,
  68. rgba(255,255,255,0.04) 25%,
  69. transparent 50%,
  70. rgba(255,255,255,0.02) 75%,
  71. rgba(255,255,255,0.06) 100%
  72. );
  73. border-radius: inherit;
  74. pointer-events: none;
  75. z-index: 1;
  76. }
  77. /* ── Refraction Highlight (animated) ── */
  78. .glass::after {
  79. content: '';
  80. position: absolute; inset: -50%;
  81. background: conic-gradient(
  82. from 0deg,
  83. transparent 0deg,
  84. rgba(255,255,255,0.08) 60deg,
  85. transparent 120deg,
  86. rgba(0,229,160,0.04) 180deg,
  87. transparent 240deg,
  88. rgba(88,166,255,0.06) 300deg,
  89. transparent 360deg
  90. );
  91. animation: refract 12s linear infinite;
  92. pointer-events: none;
  93. z-index: 1;
  94. opacity: 0.6;
  95. }
  96. @keyframes refract { to { transform: rotate(360deg); } }
  97. .glass > * { position: relative; z-index: 2; }
  98. /* ── Layout ── */
  99. .container { max-width: 1080px; margin: 0 auto; padding: 40px 24px; position: relative; z-index: 1; }
  100. /* ── Header ── */
  101. .header { text-align: center; margin-bottom: 40px; }
  102. .header h1 {
  103. font-size: 36px; font-weight: 800; letter-spacing: -1px;
  104. background: linear-gradient(135deg, #fff 0%, #00E5A0 50%, #58a6ff 100%);
  105. -webkit-background-clip: text; -webkit-text-fill-color: transparent;
  106. background-clip: text;
  107. }
  108. .header p { color: var(--text-dim); font-size: 15px; margin-top: 8px; }
  109. /* ── Mode Tabs ── */
  110. .mode-tabs {
  111. display: flex; gap: 6px; margin-bottom: 28px;
  112. padding: 6px; border-radius: 16px;
  113. }
  114. .mode-tab {
  115. flex: 1; padding: 14px 10px; border: none; border-radius: 12px;
  116. background: transparent; color: var(--text-dim); cursor: pointer;
  117. font-size: 13px; font-weight: 600; transition: all .25s ease;
  118. display: flex; flex-direction: column; align-items: center; gap: 5px;
  119. }
  120. .mode-tab .m-icon { font-size: 22px; }
  121. .mode-tab:hover { color: var(--text); background: rgba(255,255,255,0.04); }
  122. .mode-tab.active {
  123. background: rgba(0,229,160,0.12);
  124. color: var(--accent);
  125. box-shadow: inset 0 1px 0 rgba(0,229,160,0.2), 0 0 0 0.5px rgba(0,229,160,0.15);
  126. }
  127. .mode-tab .m-desc { font-size: 10px; font-weight: 400; opacity: 0.6; }
  128. /* ── Mode Info ── */
  129. .mode-info {
  130. padding: 20px 24px; margin-bottom: 20px;
  131. display: flex; align-items: flex-start; gap: 16px; font-size: 14px; line-height: 1.7;
  132. }
  133. .mode-info .mi-icon { font-size: 36px; flex-shrink: 0; }
  134. .mode-info .mi-title { font-weight: 700; font-size: 16px; margin-bottom: 4px; }
  135. .mode-info .mi-flow {
  136. margin-top: 10px; font-family: 'SF Mono', 'Fira Code', monospace;
  137. font-size: 12px; color: var(--accent); padding: 8px 12px;
  138. background: rgba(0,0,0,0.3); border-radius: 8px;
  139. white-space: pre; overflow-x: auto;
  140. }
  141. /* ── Presets ── */
  142. .presets { display: flex; gap: 8px; margin-bottom: 16px; flex-wrap: wrap; }
  143. .preset-btn {
  144. padding: 8px 16px; border-radius: 10px; border: none;
  145. background: rgba(255,255,255,0.04); color: var(--text-dim);
  146. font-size: 12px; cursor: pointer; transition: all .2s;
  147. box-shadow: inset 0 0.5px 0 rgba(255,255,255,0.08), 0 0 0 0.5px rgba(255,255,255,0.05);
  148. }
  149. .preset-btn:hover { color: var(--accent); background: rgba(0,229,160,0.06); border-color: rgba(0,229,160,0.2); }
  150. .preset-btn.active { color: var(--accent); background: rgba(0,229,160,0.08); }
  151. /* ── Input ── */
  152. .input-area { display: flex; gap: 10px; margin-bottom: 32px; }
  153. .input-area textarea {
  154. flex: 1; padding: 16px 18px; border-radius: 14px; border: none;
  155. background: rgba(255,255,255,0.06);
  156. color: var(--text); font-size: 14px; font-family: inherit;
  157. resize: none; height: 56px; outline: none;
  158. box-shadow: inset 0 0.5px 0 rgba(255,255,255,0.1), 0 0 0 0.5px rgba(255,255,255,0.06);
  159. backdrop-filter: blur(20px);
  160. transition: all .2s;
  161. }
  162. .input-area textarea:focus {
  163. box-shadow: inset 0 0.5px 0 rgba(255,255,255,0.1), 0 0 0 2px rgba(0,229,160,0.2), 0 0 0 0.5px rgba(0,229,160,0.3);
  164. }
  165. .run-btn {
  166. padding: 0 32px; border-radius: 14px; border: none;
  167. background: linear-gradient(180deg, #00E5A0 0%, #00C98A 100%);
  168. color: #0a0a12; font-size: 14px; font-weight: 700; cursor: pointer;
  169. transition: all .2s; white-space: nowrap;
  170. box-shadow: inset 0 0.5px 0 rgba(255,255,255,0.3), 0 2px 12px rgba(0,229,160,0.3);
  171. }
  172. .run-btn:hover { transform: translateY(-1px); box-shadow: inset 0 0.5px 0 rgba(255,255,255,0.3), 0 4px 20px rgba(0,229,160,0.4); }
  173. .run-btn:disabled { opacity: 0.4; cursor: not-allowed; transform: none; }
  174. /* ── Trace Area ── */
  175. .trace-area { display: none; margin-bottom: 28px; }
  176. .trace-area.visible { display: block; animation: fadeUp .4s ease; }
  177. @keyframes fadeUp { from { opacity: 0; transform: translateY(16px); } }
  178. .trace-header {
  179. display: flex; align-items: center; gap: 8px; margin-bottom: 16px;
  180. font-size: 14px; font-weight: 700;
  181. }
  182. .trace-header .th-dot {
  183. width: 8px; height: 8px; border-radius: 50%; background: var(--accent);
  184. animation: pulse 1.5s ease infinite;
  185. }
  186. @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.3; } }
  187. /* ── Step Card ── */
  188. .step-cards { display: flex; flex-direction: column; gap: 12px; }
  189. .step {
  190. padding: 18px 20px;
  191. animation: stepIn .35s ease both;
  192. }
  193. @keyframes stepIn { from { opacity: 0; transform: translateX(-12px); } }
  194. .step-head { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; }
  195. .step-icon { font-size: 20px; }
  196. .step-agent { font-weight: 700; font-size: 14px; }
  197. .step-action { font-size: 13px; color: var(--text-dim); flex: 1; text-align: right; }
  198. .step-events { display: flex; flex-direction: column; gap: 8px; }
  199. .event {
  200. padding: 10px 14px; border-radius: 10px;
  201. background: rgba(0,0,0,0.2);
  202. font-size: 12px; line-height: 1.6;
  203. }
  204. .event-header { display: flex; align-items: center; gap: 6px; margin-bottom: 4px; }
  205. .event-tag {
  206. font-size: 10px; font-weight: 600; padding: 2px 6px; border-radius: 4px;
  207. text-transform: uppercase; letter-spacing: 0.5px;
  208. }
  209. .event-tag.llm { background: rgba(88,166,255,0.15); color: var(--blue); }
  210. .event-tag.tool { background: rgba(188,140,255,0.15); color: var(--purple); }
  211. .event-tag.answer { background: rgba(0,229,160,0.15); color: var(--accent); }
  212. .event-meta { font-size: 11px; color: var(--text-dim); margin-left: auto; }
  213. .event-body { color: var(--text-dim); font-size: 12px; max-height: 120px; overflow: hidden; position: relative; }
  214. .event-body.expanded { max-height: none; }
  215. .event-expand {
  216. display: inline-block; margin-top: 4px; font-size: 11px; color: var(--accent);
  217. cursor: pointer; background: none; border: none;
  218. }
  219. /* ── Final Answer ── */
  220. .final-answer {
  221. padding: 24px; margin-top: 20px;
  222. border: 1px solid rgba(0,229,160,0.15);
  223. }
  224. .final-answer .fa-label {
  225. font-size: 12px; font-weight: 700; color: var(--accent);
  226. text-transform: uppercase; letter-spacing: 1px; margin-bottom: 12px;
  227. }
  228. .final-answer .fa-content {
  229. font-size: 14px; line-height: 1.8; white-space: pre-wrap;
  230. }
  231. /* ── Stats Bar ── */
  232. .stats-bar {
  233. display: flex; gap: 16px; flex-wrap: wrap; margin-top: 16px; padding: 16px 20px;
  234. }
  235. .stat { text-align: center; min-width: 80px; }
  236. .stat-val { font-size: 24px; font-weight: 800; color: var(--accent); }
  237. .stat-label { font-size: 11px; color: var(--text-dim); margin-top: 2px; }
  238. /* ── Loading ── */
  239. .loading-dots { display: inline-flex; gap: 4px; align-items: center; }
  240. .loading-dots span {
  241. width: 4px; height: 4px; border-radius: 50%; background: var(--accent);
  242. animation: dotBounce 1.2s ease infinite;
  243. }
  244. .loading-dots span:nth-child(2) { animation-delay: 0.2s; }
  245. .loading-dots span:nth-child(3) { animation-delay: 0.4s; }
  246. @keyframes dotBounce { 0%,80%,100% { opacity: 0.2; } 40% { opacity: 1; } }
  247. /* ── Responsive ── */
  248. @media (max-width: 768px) {
  249. .header h1 { font-size: 26px; }
  250. .mode-tabs { flex-wrap: wrap; }
  251. .input-area { flex-direction: column; }
  252. .stats-bar { justify-content: center; }
  253. }
  254. ::-webkit-scrollbar { width: 6px; }
  255. ::-webkit-scrollbar-track { background: transparent; }
  256. ::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.08); border-radius: 3px; }
  257. </style>
  258. </head>
  259. <body>
  260. <div class="bg-orbs">
  261. <div class="orb orb-1"></div>
  262. <div class="orb orb-2"></div>
  263. <div class="orb orb-3"></div>
  264. </div>
  265. <div class="container">
  266. <div class="header">
  267. <h1>Multi-Agent 实战演示</h1>
  268. <p>DeepSeek API 真实调用 · 工具真实执行 · 全链路 trace</p>
  269. </div>
  270. <!-- Mode Tabs -->
  271. <div class="glass mode-tabs" id="modeTabs">
  272. <button class="mode-tab active" data-mode="supervisor" onclick="switchMode('supervisor')">
  273. <span class="m-icon">👔</span>Supervisor<span class="m-desc">总控 + 专家</span>
  274. </button>
  275. <button class="mode-tab" data-mode="pipeline" onclick="switchMode('pipeline')">
  276. <span class="m-icon">🔗</span>Pipeline<span class="m-desc">线性流水线</span>
  277. </button>
  278. <button class="mode-tab" data-mode="debate" onclick="switchMode('debate')">
  279. <span class="m-icon">⚖️</span>Debate<span class="m-desc">正反方辩论</span>
  280. </button>
  281. </div>
  282. <!-- Mode Info -->
  283. <div class="glass mode-info" id="modeInfo"></div>
  284. <!-- Presets -->
  285. <div class="presets" id="presets"></div>
  286. <!-- Input -->
  287. <div class="input-area">
  288. <textarea id="queryInput" placeholder="输入任务描述,或点击上方预设场景..." rows="2"></textarea>
  289. <button class="run-btn" id="runBtn" onclick="execute()">▶ 执行</button>
  290. </div>
  291. <!-- Trace -->
  292. <div class="trace-area" id="traceArea">
  293. <div class="trace-header">
  294. <div class="th-dot" id="traceDot"></div>
  295. <span id="traceTitle">执行中...</span>
  296. <span style="margin-left:auto;font-size:12px;color:var(--text-dim)" id="traceTimer"></span>
  297. </div>
  298. <div class="step-cards" id="stepCards"></div>
  299. <div id="finalSection"></div>
  300. </div>
  301. </div>
  302. <script>
  303. const MODE_INFO = {
  304. supervisor: {
  305. icon: '👔', title: 'Supervisor 模式',
  306. desc: '总控 Agent 拆解任务,路由给研究专家和代码专家,最后整合结果。职责清晰,易调试,可扩展。',
  307. flow: 'User → Supervisor(拆任务) → Researcher(搜索) → Coder(分析) → Supervisor(整合) → Answer',
  308. },
  309. pipeline: {
  310. icon: '🔗', title: 'Pipeline 管道模式',
  311. desc: '线性流水线:搜索 Agent 采集信息 → 分析 Agent 处理数据 → 报告 Agent 生成最终报告。流程固定,确定性高。',
  312. flow: 'User → 搜索Agent(web_search) → 分析Agent(python) → 报告Agent(python) → Answer',
  313. },
  314. debate: {
  315. icon: '⚖️', title: 'Debate 对抗模式',
  316. desc: '正方 Agent 和反方 Agent 各自搜索论据,经过两轮交锋后,裁判 Agent 综合评判给出结论。',
  317. flow: 'User → 正方(搜索+立论) ⟷ 反方(搜索+反驳) → 裁判(评判) → Verdict',
  318. },
  319. };
  320. let currentMode = 'supervisor';
  321. let presets = {};
  322. let isRunning = false;
  323. let timerInterval = null;
  324. async function init() {
  325. const resp = await fetch('/api/presets');
  326. presets = await resp.json();
  327. switchMode('supervisor');
  328. }
  329. function switchMode(mode) {
  330. currentMode = mode;
  331. document.querySelectorAll('.mode-tab').forEach(t => t.classList.remove('active'));
  332. document.querySelector(`.mode-tab[data-mode="${mode}"]`).classList.add('active');
  333. const info = MODE_INFO[mode];
  334. document.getElementById('modeInfo').innerHTML = `
  335. <span class="mi-icon">${info.icon}</span>
  336. <div>
  337. <div class="mi-title">${info.title}</div>
  338. <div>${info.desc}</div>
  339. <div class="mi-flow">${info.flow}</div>
  340. </div>`;
  341. // Presets
  342. const p = presets[mode];
  343. document.getElementById('presets').innerHTML = p
  344. ? `<button class="preset-btn active" onclick="fillPreset('${mode}')">${p.title}</button>`
  345. : '';
  346. if (p) document.getElementById('queryInput').value = p.query;
  347. }
  348. function fillPreset(mode) {
  349. const p = presets[mode];
  350. if (p) document.getElementById('queryInput').value = p.query;
  351. }
  352. async function execute() {
  353. const query = document.getElementById('queryInput').value.trim();
  354. if (!query || isRunning) return;
  355. isRunning = true;
  356. const btn = document.getElementById('runBtn');
  357. btn.disabled = true;
  358. btn.textContent = '⏳ 执行中...';
  359. const traceArea = document.getElementById('traceArea');
  360. const stepCards = document.getElementById('stepCards');
  361. const finalSection = document.getElementById('finalSection');
  362. const traceDot = document.getElementById('traceDot');
  363. const traceTitle = document.getElementById('traceTitle');
  364. const traceTimer = document.getElementById('traceTimer');
  365. traceArea.classList.add('visible');
  366. stepCards.innerHTML = '';
  367. finalSection.innerHTML = '';
  368. traceDot.style.background = 'var(--accent)';
  369. traceDot.style.animation = 'pulse 1.5s ease infinite';
  370. traceTitle.textContent = `${MODE_INFO[currentMode].title} — 执行中`;
  371. // Timer
  372. let startTime = Date.now();
  373. timerInterval = setInterval(() => {
  374. traceTimer.textContent = `${((Date.now() - startTime) / 1000).toFixed(1)}s`;
  375. }, 100);
  376. try {
  377. const resp = await fetch('/api/execute', {
  378. method: 'POST',
  379. headers: { 'Content-Type': 'application/json' },
  380. body: JSON.stringify({ mode: currentMode, query }),
  381. });
  382. const reader = resp.body.getReader();
  383. const decoder = new TextDecoder();
  384. let buffer = '';
  385. let currentStepEl = null;
  386. let totalLlmCalls = 0;
  387. let totalToolCalls = 0;
  388. let totalTokens = 0;
  389. while (true) {
  390. const { done, value } = await reader.read();
  391. if (done) break;
  392. buffer += decoder.decode(value, { stream: true });
  393. const lines = buffer.split('\n');
  394. buffer = lines.pop();
  395. for (const line of lines) {
  396. if (!line.startsWith('data: ')) continue;
  397. const event = JSON.parse(line.slice(6));
  398. if (event.type === 'step_start') {
  399. currentStepEl = createStepCard(event);
  400. stepCards.appendChild(currentStepEl);
  401. currentStepEl.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
  402. }
  403. else if (event.type === 'agent_event' && currentStepEl) {
  404. if (event.tool) {
  405. totalToolCalls++;
  406. appendEvent(currentStepEl, event);
  407. } else if (event.answer) {
  408. appendEvent(currentStepEl, event);
  409. } else {
  410. totalLlmCalls++;
  411. if (event.tokens) totalTokens += event.tokens;
  412. appendEvent(currentStepEl, event);
  413. }
  414. }
  415. else if (event.type === 'step_end' && currentStepEl) {
  416. // Mark step complete
  417. const head = currentStepEl.querySelector('.step-action');
  418. if (head) head.textContent = '✅ 完成';
  419. }
  420. else if (event.type === 'final') {
  421. traceDot.style.background = 'var(--accent)';
  422. traceDot.style.animation = 'none';
  423. traceTitle.textContent = `${MODE_INFO[currentMode].title} — 完成`;
  424. finalSection.innerHTML = `
  425. <div class="glass final-answer">
  426. <div class="fa-label">📋 最终结果</div>
  427. <div class="fa-content">${escHtml(event.answer)}</div>
  428. </div>
  429. <div class="glass stats-bar">
  430. <div class="stat"><div class="stat-val">${totalLlmCalls}</div><div class="stat-label">LLM 调用</div></div>
  431. <div class="stat"><div class="stat-val">${totalToolCalls}</div><div class="stat-label">工具调用</div></div>
  432. <div class="stat"><div class="stat-val">${totalTokens.toLocaleString()}</div><div class="stat-label">Tokens</div></div>
  433. <div class="stat"><div class="stat-val">${(event.total_ms / 1000).toFixed(1)}s</div><div class="stat-label">总耗时</div></div>
  434. </div>`;
  435. finalSection.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
  436. }
  437. else if (event.type === 'error') {
  438. traceDot.style.background = 'var(--red)';
  439. traceTitle.textContent = '执行出错';
  440. finalSection.innerHTML = `<div class="glass final-answer" style="border-color:rgba(248,81,73,0.3)"><div class="fa-label" style="color:var(--red)">❌ 错误</div><div class="fa-content">${escHtml(event.message)}</div></div>`;
  441. }
  442. }
  443. }
  444. } catch (e) {
  445. traceTitle.textContent = '连接失败';
  446. finalSection.innerHTML = `<div class="glass final-answer" style="border-color:rgba(248,81,73,0.3)"><div class="fa-label" style="color:var(--red)">❌ 网络错误</div><div class="fa-content">${escHtml(e.message)}</div></div>`;
  447. } finally {
  448. clearInterval(timerInterval);
  449. isRunning = false;
  450. btn.disabled = false;
  451. btn.textContent = '▶ 执行';
  452. }
  453. }
  454. function createStepCard(event) {
  455. const el = document.createElement('div');
  456. el.className = 'glass step';
  457. el.innerHTML = `
  458. <div class="step-head">
  459. <span class="step-icon">${event.icon || '🤖'}</span>
  460. <span class="step-agent">${escHtml(event.agent)}</span>
  461. <span class="step-action">${escHtml(event.action)} <span class="loading-dots"><span></span><span></span><span></span></span></span>
  462. </div>
  463. <div class="step-events"></div>`;
  464. return el;
  465. }
  466. function appendEvent(stepEl, event) {
  467. const container = stepEl.querySelector('.step-events');
  468. const el = document.createElement('div');
  469. el.className = 'event';
  470. if (event.tool) {
  471. const fullResult = escHtml(event.result || '');
  472. const hasMore = fullResult.length > 150;
  473. const shortResult = hasMore ? fullResult.slice(0, 150) + '...' : fullResult;
  474. el.innerHTML = `
  475. <div class="event-header">
  476. <span class="event-tag tool">🔧 ${escHtml(event.tool)}</span>
  477. <span class="event-meta">${event.latency_ms || 0}ms</span>
  478. </div>
  479. <div class="event-body">${shortResult}</div>
  480. ${hasMore ? '<button class="event-expand" onclick="toggleExpand(this)">展开</button>' : ''}`;
  481. if (hasMore) {
  482. el.dataset.fullResult = fullResult;
  483. }
  484. } else {
  485. el.innerHTML = `
  486. <div class="event-header">
  487. <span class="event-tag llm">🧠 ${escHtml(event.model || 'LLM')}</span>
  488. <span class="event-meta">${event.latency_ms || 0}ms · ${(event.tokens || 0).toLocaleString()} tokens</span>
  489. </div>`;
  490. }
  491. container.appendChild(el);
  492. }
  493. function toggleExpand(btn) {
  494. const body = btn.previousElementSibling;
  495. const eventEl = btn.closest('.event');
  496. if (body.classList.contains('expanded')) {
  497. body.classList.remove('expanded');
  498. body.innerHTML = body.dataset.shortResult || body.textContent.slice(0, 150) + '...';
  499. btn.textContent = '展开';
  500. } else {
  501. body.dataset.shortResult = body.innerHTML;
  502. body.innerHTML = eventEl.dataset.fullResult || body.textContent;
  503. body.classList.add('expanded');
  504. btn.textContent = '收起';
  505. }
  506. }
  507. function escHtml(s) {
  508. if (!s) return '';
  509. const d = document.createElement('div');
  510. d.textContent = s;
  511. return d.innerHTML;
  512. }
  513. init();
  514. </script>
  515. </body>
  516. </html>