|
|
@@ -0,0 +1,570 @@
|
|
|
+<!DOCTYPE html>
|
|
|
+<html lang="zh-CN">
|
|
|
+<head>
|
|
|
+<meta charset="UTF-8">
|
|
|
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
+<title>Multi-Agent 实战演示</title>
|
|
|
+<style>
|
|
|
+:root {
|
|
|
+ --bg: #0a0a12;
|
|
|
+ --glass: rgba(255,255,255,0.06);
|
|
|
+ --glass-border: rgba(255,255,255,0.1);
|
|
|
+ --glass-highlight: rgba(255,255,255,0.15);
|
|
|
+ --text: #e8eaed;
|
|
|
+ --text-dim: #8a8d93;
|
|
|
+ --accent: #00E5A0;
|
|
|
+ --accent-dim: rgba(0,229,160,0.15);
|
|
|
+ --blue: #58a6ff;
|
|
|
+ --purple: #bc8cff;
|
|
|
+ --yellow: #d29922;
|
|
|
+ --red: #f85149;
|
|
|
+ --radius: 20px;
|
|
|
+ --blur: 40px;
|
|
|
+}
|
|
|
+
|
|
|
+* { margin:0; padding:0; box-sizing:border-box; }
|
|
|
+body {
|
|
|
+ font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'SF Pro Text', 'Helvetica Neue', sans-serif;
|
|
|
+ background: var(--bg);
|
|
|
+ color: var(--text);
|
|
|
+ min-height: 100vh;
|
|
|
+ overflow-x: hidden;
|
|
|
+ -webkit-font-smoothing: antialiased;
|
|
|
+}
|
|
|
+
|
|
|
+/* ── Animated Background Orbs ── */
|
|
|
+.bg-orbs { position: fixed; inset: 0; pointer-events: none; z-index: 0; overflow: hidden; }
|
|
|
+.orb {
|
|
|
+ position: absolute; border-radius: 50%;
|
|
|
+ filter: blur(120px); opacity: 0.4;
|
|
|
+ animation: float 20s ease-in-out infinite;
|
|
|
+}
|
|
|
+.orb-1 { width: 600px; height: 600px; background: #00E5A0; top: -15%; left: -10%; animation-delay: 0s; }
|
|
|
+.orb-2 { width: 500px; height: 500px; background: #58a6ff; bottom: -10%; right: -8%; animation-delay: -7s; }
|
|
|
+.orb-3 { width: 400px; height: 400px; background: #bc8cff; top: 50%; left: 50%; transform: translate(-50%,-50%); animation-delay: -14s; }
|
|
|
+@keyframes float {
|
|
|
+ 0%, 100% { transform: translate(0, 0) scale(1); }
|
|
|
+ 33% { transform: translate(30px, -40px) scale(1.05); }
|
|
|
+ 66% { transform: translate(-20px, 30px) scale(0.95); }
|
|
|
+}
|
|
|
+
|
|
|
+/* ── Liquid Glass Card ── */
|
|
|
+.glass {
|
|
|
+ position: relative;
|
|
|
+ background: var(--glass);
|
|
|
+ backdrop-filter: blur(var(--blur)) saturate(2) brightness(1.08);
|
|
|
+ -webkit-backdrop-filter: blur(var(--blur)) saturate(2) brightness(1.08);
|
|
|
+ border-radius: var(--radius);
|
|
|
+ overflow: hidden;
|
|
|
+ box-shadow:
|
|
|
+ inset 0 1px 0 rgba(255,255,255,0.15),
|
|
|
+ inset 0 -0.5px 0 rgba(255,255,255,0.05),
|
|
|
+ 0 0 0 0.5px rgba(255,255,255,0.06),
|
|
|
+ 0 8px 32px rgba(0,0,0,0.3);
|
|
|
+}
|
|
|
+.glass::before {
|
|
|
+ content: '';
|
|
|
+ position: absolute; inset: 0;
|
|
|
+ background: linear-gradient(
|
|
|
+ 160deg,
|
|
|
+ rgba(255,255,255,0.12) 0%,
|
|
|
+ rgba(255,255,255,0.04) 25%,
|
|
|
+ transparent 50%,
|
|
|
+ rgba(255,255,255,0.02) 75%,
|
|
|
+ rgba(255,255,255,0.06) 100%
|
|
|
+ );
|
|
|
+ border-radius: inherit;
|
|
|
+ pointer-events: none;
|
|
|
+ z-index: 1;
|
|
|
+}
|
|
|
+
|
|
|
+/* ── Refraction Highlight (animated) ── */
|
|
|
+.glass::after {
|
|
|
+ content: '';
|
|
|
+ position: absolute; inset: -50%;
|
|
|
+ background: conic-gradient(
|
|
|
+ from 0deg,
|
|
|
+ transparent 0deg,
|
|
|
+ rgba(255,255,255,0.08) 60deg,
|
|
|
+ transparent 120deg,
|
|
|
+ rgba(0,229,160,0.04) 180deg,
|
|
|
+ transparent 240deg,
|
|
|
+ rgba(88,166,255,0.06) 300deg,
|
|
|
+ transparent 360deg
|
|
|
+ );
|
|
|
+ animation: refract 12s linear infinite;
|
|
|
+ pointer-events: none;
|
|
|
+ z-index: 1;
|
|
|
+ opacity: 0.6;
|
|
|
+}
|
|
|
+@keyframes refract { to { transform: rotate(360deg); } }
|
|
|
+
|
|
|
+.glass > * { position: relative; z-index: 2; }
|
|
|
+
|
|
|
+/* ── Layout ── */
|
|
|
+.container { max-width: 1080px; margin: 0 auto; padding: 40px 24px; position: relative; z-index: 1; }
|
|
|
+
|
|
|
+/* ── Header ── */
|
|
|
+.header { text-align: center; margin-bottom: 40px; }
|
|
|
+.header h1 {
|
|
|
+ font-size: 36px; font-weight: 800; letter-spacing: -1px;
|
|
|
+ background: linear-gradient(135deg, #fff 0%, #00E5A0 50%, #58a6ff 100%);
|
|
|
+ -webkit-background-clip: text; -webkit-text-fill-color: transparent;
|
|
|
+ background-clip: text;
|
|
|
+}
|
|
|
+.header p { color: var(--text-dim); font-size: 15px; margin-top: 8px; }
|
|
|
+
|
|
|
+/* ── Mode Tabs ── */
|
|
|
+.mode-tabs {
|
|
|
+ display: flex; gap: 6px; margin-bottom: 28px;
|
|
|
+ padding: 6px; border-radius: 16px;
|
|
|
+}
|
|
|
+.mode-tab {
|
|
|
+ flex: 1; padding: 14px 10px; border: none; border-radius: 12px;
|
|
|
+ background: transparent; color: var(--text-dim); cursor: pointer;
|
|
|
+ font-size: 13px; font-weight: 600; transition: all .25s ease;
|
|
|
+ display: flex; flex-direction: column; align-items: center; gap: 5px;
|
|
|
+}
|
|
|
+.mode-tab .m-icon { font-size: 22px; }
|
|
|
+.mode-tab:hover { color: var(--text); background: rgba(255,255,255,0.04); }
|
|
|
+.mode-tab.active {
|
|
|
+ background: rgba(0,229,160,0.12);
|
|
|
+ color: var(--accent);
|
|
|
+ box-shadow: inset 0 1px 0 rgba(0,229,160,0.2), 0 0 0 0.5px rgba(0,229,160,0.15);
|
|
|
+}
|
|
|
+.mode-tab .m-desc { font-size: 10px; font-weight: 400; opacity: 0.6; }
|
|
|
+
|
|
|
+/* ── Mode Info ── */
|
|
|
+.mode-info {
|
|
|
+ padding: 20px 24px; margin-bottom: 20px;
|
|
|
+ display: flex; align-items: flex-start; gap: 16px; font-size: 14px; line-height: 1.7;
|
|
|
+}
|
|
|
+.mode-info .mi-icon { font-size: 36px; flex-shrink: 0; }
|
|
|
+.mode-info .mi-title { font-weight: 700; font-size: 16px; margin-bottom: 4px; }
|
|
|
+.mode-info .mi-flow {
|
|
|
+ margin-top: 10px; font-family: 'SF Mono', 'Fira Code', monospace;
|
|
|
+ font-size: 12px; color: var(--accent); padding: 8px 12px;
|
|
|
+ background: rgba(0,0,0,0.3); border-radius: 8px;
|
|
|
+ white-space: pre; overflow-x: auto;
|
|
|
+}
|
|
|
+
|
|
|
+/* ── Presets ── */
|
|
|
+.presets { display: flex; gap: 8px; margin-bottom: 16px; flex-wrap: wrap; }
|
|
|
+.preset-btn {
|
|
|
+ padding: 8px 16px; border-radius: 10px; border: none;
|
|
|
+ background: rgba(255,255,255,0.04); color: var(--text-dim);
|
|
|
+ font-size: 12px; cursor: pointer; transition: all .2s;
|
|
|
+ box-shadow: inset 0 0.5px 0 rgba(255,255,255,0.08), 0 0 0 0.5px rgba(255,255,255,0.05);
|
|
|
+}
|
|
|
+.preset-btn:hover { color: var(--accent); background: rgba(0,229,160,0.06); border-color: rgba(0,229,160,0.2); }
|
|
|
+.preset-btn.active { color: var(--accent); background: rgba(0,229,160,0.08); }
|
|
|
+
|
|
|
+/* ── Input ── */
|
|
|
+.input-area { display: flex; gap: 10px; margin-bottom: 32px; }
|
|
|
+.input-area textarea {
|
|
|
+ flex: 1; padding: 16px 18px; border-radius: 14px; border: none;
|
|
|
+ background: rgba(255,255,255,0.06);
|
|
|
+ color: var(--text); font-size: 14px; font-family: inherit;
|
|
|
+ resize: none; height: 56px; outline: none;
|
|
|
+ box-shadow: inset 0 0.5px 0 rgba(255,255,255,0.1), 0 0 0 0.5px rgba(255,255,255,0.06);
|
|
|
+ backdrop-filter: blur(20px);
|
|
|
+ transition: all .2s;
|
|
|
+}
|
|
|
+.input-area textarea:focus {
|
|
|
+ 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);
|
|
|
+}
|
|
|
+.run-btn {
|
|
|
+ padding: 0 32px; border-radius: 14px; border: none;
|
|
|
+ background: linear-gradient(180deg, #00E5A0 0%, #00C98A 100%);
|
|
|
+ color: #0a0a12; font-size: 14px; font-weight: 700; cursor: pointer;
|
|
|
+ transition: all .2s; white-space: nowrap;
|
|
|
+ box-shadow: inset 0 0.5px 0 rgba(255,255,255,0.3), 0 2px 12px rgba(0,229,160,0.3);
|
|
|
+}
|
|
|
+.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); }
|
|
|
+.run-btn:disabled { opacity: 0.4; cursor: not-allowed; transform: none; }
|
|
|
+
|
|
|
+/* ── Trace Area ── */
|
|
|
+.trace-area { display: none; margin-bottom: 28px; }
|
|
|
+.trace-area.visible { display: block; animation: fadeUp .4s ease; }
|
|
|
+@keyframes fadeUp { from { opacity: 0; transform: translateY(16px); } }
|
|
|
+
|
|
|
+.trace-header {
|
|
|
+ display: flex; align-items: center; gap: 8px; margin-bottom: 16px;
|
|
|
+ font-size: 14px; font-weight: 700;
|
|
|
+}
|
|
|
+.trace-header .th-dot {
|
|
|
+ width: 8px; height: 8px; border-radius: 50%; background: var(--accent);
|
|
|
+ animation: pulse 1.5s ease infinite;
|
|
|
+}
|
|
|
+@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.3; } }
|
|
|
+
|
|
|
+/* ── Step Card ── */
|
|
|
+.step-cards { display: flex; flex-direction: column; gap: 12px; }
|
|
|
+
|
|
|
+.step {
|
|
|
+ padding: 18px 20px;
|
|
|
+ animation: stepIn .35s ease both;
|
|
|
+}
|
|
|
+@keyframes stepIn { from { opacity: 0; transform: translateX(-12px); } }
|
|
|
+
|
|
|
+.step-head { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; }
|
|
|
+.step-icon { font-size: 20px; }
|
|
|
+.step-agent { font-weight: 700; font-size: 14px; }
|
|
|
+.step-action { font-size: 13px; color: var(--text-dim); flex: 1; text-align: right; }
|
|
|
+
|
|
|
+.step-events { display: flex; flex-direction: column; gap: 8px; }
|
|
|
+
|
|
|
+.event {
|
|
|
+ padding: 10px 14px; border-radius: 10px;
|
|
|
+ background: rgba(0,0,0,0.2);
|
|
|
+ font-size: 12px; line-height: 1.6;
|
|
|
+}
|
|
|
+.event-header { display: flex; align-items: center; gap: 6px; margin-bottom: 4px; }
|
|
|
+.event-tag {
|
|
|
+ font-size: 10px; font-weight: 600; padding: 2px 6px; border-radius: 4px;
|
|
|
+ text-transform: uppercase; letter-spacing: 0.5px;
|
|
|
+}
|
|
|
+.event-tag.llm { background: rgba(88,166,255,0.15); color: var(--blue); }
|
|
|
+.event-tag.tool { background: rgba(188,140,255,0.15); color: var(--purple); }
|
|
|
+.event-tag.answer { background: rgba(0,229,160,0.15); color: var(--accent); }
|
|
|
+.event-meta { font-size: 11px; color: var(--text-dim); margin-left: auto; }
|
|
|
+.event-body { color: var(--text-dim); font-size: 12px; max-height: 120px; overflow: hidden; position: relative; }
|
|
|
+.event-body.expanded { max-height: none; }
|
|
|
+.event-expand {
|
|
|
+ display: inline-block; margin-top: 4px; font-size: 11px; color: var(--accent);
|
|
|
+ cursor: pointer; background: none; border: none;
|
|
|
+}
|
|
|
+
|
|
|
+/* ── Final Answer ── */
|
|
|
+.final-answer {
|
|
|
+ padding: 24px; margin-top: 20px;
|
|
|
+ border: 1px solid rgba(0,229,160,0.15);
|
|
|
+}
|
|
|
+.final-answer .fa-label {
|
|
|
+ font-size: 12px; font-weight: 700; color: var(--accent);
|
|
|
+ text-transform: uppercase; letter-spacing: 1px; margin-bottom: 12px;
|
|
|
+}
|
|
|
+.final-answer .fa-content {
|
|
|
+ font-size: 14px; line-height: 1.8; white-space: pre-wrap;
|
|
|
+}
|
|
|
+
|
|
|
+/* ── Stats Bar ── */
|
|
|
+.stats-bar {
|
|
|
+ display: flex; gap: 16px; flex-wrap: wrap; margin-top: 16px; padding: 16px 20px;
|
|
|
+}
|
|
|
+.stat { text-align: center; min-width: 80px; }
|
|
|
+.stat-val { font-size: 24px; font-weight: 800; color: var(--accent); }
|
|
|
+.stat-label { font-size: 11px; color: var(--text-dim); margin-top: 2px; }
|
|
|
+
|
|
|
+/* ── Loading ── */
|
|
|
+.loading-dots { display: inline-flex; gap: 4px; align-items: center; }
|
|
|
+.loading-dots span {
|
|
|
+ width: 4px; height: 4px; border-radius: 50%; background: var(--accent);
|
|
|
+ animation: dotBounce 1.2s ease infinite;
|
|
|
+}
|
|
|
+.loading-dots span:nth-child(2) { animation-delay: 0.2s; }
|
|
|
+.loading-dots span:nth-child(3) { animation-delay: 0.4s; }
|
|
|
+@keyframes dotBounce { 0%,80%,100% { opacity: 0.2; } 40% { opacity: 1; } }
|
|
|
+
|
|
|
+/* ── Responsive ── */
|
|
|
+@media (max-width: 768px) {
|
|
|
+ .header h1 { font-size: 26px; }
|
|
|
+ .mode-tabs { flex-wrap: wrap; }
|
|
|
+ .input-area { flex-direction: column; }
|
|
|
+ .stats-bar { justify-content: center; }
|
|
|
+}
|
|
|
+
|
|
|
+::-webkit-scrollbar { width: 6px; }
|
|
|
+::-webkit-scrollbar-track { background: transparent; }
|
|
|
+::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.08); border-radius: 3px; }
|
|
|
+</style>
|
|
|
+</head>
|
|
|
+<body>
|
|
|
+
|
|
|
+<div class="bg-orbs">
|
|
|
+ <div class="orb orb-1"></div>
|
|
|
+ <div class="orb orb-2"></div>
|
|
|
+ <div class="orb orb-3"></div>
|
|
|
+</div>
|
|
|
+
|
|
|
+<div class="container">
|
|
|
+ <div class="header">
|
|
|
+ <h1>Multi-Agent 实战演示</h1>
|
|
|
+ <p>DeepSeek API 真实调用 · 工具真实执行 · 全链路 trace</p>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Mode Tabs -->
|
|
|
+ <div class="glass mode-tabs" id="modeTabs">
|
|
|
+ <button class="mode-tab active" data-mode="supervisor" onclick="switchMode('supervisor')">
|
|
|
+ <span class="m-icon">👔</span>Supervisor<span class="m-desc">总控 + 专家</span>
|
|
|
+ </button>
|
|
|
+ <button class="mode-tab" data-mode="pipeline" onclick="switchMode('pipeline')">
|
|
|
+ <span class="m-icon">🔗</span>Pipeline<span class="m-desc">线性流水线</span>
|
|
|
+ </button>
|
|
|
+ <button class="mode-tab" data-mode="debate" onclick="switchMode('debate')">
|
|
|
+ <span class="m-icon">⚖️</span>Debate<span class="m-desc">正反方辩论</span>
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Mode Info -->
|
|
|
+ <div class="glass mode-info" id="modeInfo"></div>
|
|
|
+
|
|
|
+ <!-- Presets -->
|
|
|
+ <div class="presets" id="presets"></div>
|
|
|
+
|
|
|
+ <!-- Input -->
|
|
|
+ <div class="input-area">
|
|
|
+ <textarea id="queryInput" placeholder="输入任务描述,或点击上方预设场景..." rows="2"></textarea>
|
|
|
+ <button class="run-btn" id="runBtn" onclick="execute()">▶ 执行</button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Trace -->
|
|
|
+ <div class="trace-area" id="traceArea">
|
|
|
+ <div class="trace-header">
|
|
|
+ <div class="th-dot" id="traceDot"></div>
|
|
|
+ <span id="traceTitle">执行中...</span>
|
|
|
+ <span style="margin-left:auto;font-size:12px;color:var(--text-dim)" id="traceTimer"></span>
|
|
|
+ </div>
|
|
|
+ <div class="step-cards" id="stepCards"></div>
|
|
|
+ <div id="finalSection"></div>
|
|
|
+ </div>
|
|
|
+</div>
|
|
|
+
|
|
|
+<script>
|
|
|
+const MODE_INFO = {
|
|
|
+ supervisor: {
|
|
|
+ icon: '👔', title: 'Supervisor 模式',
|
|
|
+ desc: '总控 Agent 拆解任务,路由给研究专家和代码专家,最后整合结果。职责清晰,易调试,可扩展。',
|
|
|
+ flow: 'User → Supervisor(拆任务) → Researcher(搜索) → Coder(分析) → Supervisor(整合) → Answer',
|
|
|
+ },
|
|
|
+ pipeline: {
|
|
|
+ icon: '🔗', title: 'Pipeline 管道模式',
|
|
|
+ desc: '线性流水线:搜索 Agent 采集信息 → 分析 Agent 处理数据 → 报告 Agent 生成最终报告。流程固定,确定性高。',
|
|
|
+ flow: 'User → 搜索Agent(web_search) → 分析Agent(python) → 报告Agent(python) → Answer',
|
|
|
+ },
|
|
|
+ debate: {
|
|
|
+ icon: '⚖️', title: 'Debate 对抗模式',
|
|
|
+ desc: '正方 Agent 和反方 Agent 各自搜索论据,经过两轮交锋后,裁判 Agent 综合评判给出结论。',
|
|
|
+ flow: 'User → 正方(搜索+立论) ⟷ 反方(搜索+反驳) → 裁判(评判) → Verdict',
|
|
|
+ },
|
|
|
+};
|
|
|
+
|
|
|
+let currentMode = 'supervisor';
|
|
|
+let presets = {};
|
|
|
+let isRunning = false;
|
|
|
+let timerInterval = null;
|
|
|
+
|
|
|
+async function init() {
|
|
|
+ const resp = await fetch('/api/presets');
|
|
|
+ presets = await resp.json();
|
|
|
+ switchMode('supervisor');
|
|
|
+}
|
|
|
+
|
|
|
+function switchMode(mode) {
|
|
|
+ currentMode = mode;
|
|
|
+ document.querySelectorAll('.mode-tab').forEach(t => t.classList.remove('active'));
|
|
|
+ document.querySelector(`.mode-tab[data-mode="${mode}"]`).classList.add('active');
|
|
|
+
|
|
|
+ const info = MODE_INFO[mode];
|
|
|
+ document.getElementById('modeInfo').innerHTML = `
|
|
|
+ <span class="mi-icon">${info.icon}</span>
|
|
|
+ <div>
|
|
|
+ <div class="mi-title">${info.title}</div>
|
|
|
+ <div>${info.desc}</div>
|
|
|
+ <div class="mi-flow">${info.flow}</div>
|
|
|
+ </div>`;
|
|
|
+
|
|
|
+ // Presets
|
|
|
+ const p = presets[mode];
|
|
|
+ document.getElementById('presets').innerHTML = p
|
|
|
+ ? `<button class="preset-btn active" onclick="fillPreset('${mode}')">${p.title}</button>`
|
|
|
+ : '';
|
|
|
+ if (p) document.getElementById('queryInput').value = p.query;
|
|
|
+}
|
|
|
+
|
|
|
+function fillPreset(mode) {
|
|
|
+ const p = presets[mode];
|
|
|
+ if (p) document.getElementById('queryInput').value = p.query;
|
|
|
+}
|
|
|
+
|
|
|
+async function execute() {
|
|
|
+ const query = document.getElementById('queryInput').value.trim();
|
|
|
+ if (!query || isRunning) return;
|
|
|
+
|
|
|
+ isRunning = true;
|
|
|
+ const btn = document.getElementById('runBtn');
|
|
|
+ btn.disabled = true;
|
|
|
+ btn.textContent = '⏳ 执行中...';
|
|
|
+
|
|
|
+ const traceArea = document.getElementById('traceArea');
|
|
|
+ const stepCards = document.getElementById('stepCards');
|
|
|
+ const finalSection = document.getElementById('finalSection');
|
|
|
+ const traceDot = document.getElementById('traceDot');
|
|
|
+ const traceTitle = document.getElementById('traceTitle');
|
|
|
+ const traceTimer = document.getElementById('traceTimer');
|
|
|
+
|
|
|
+ traceArea.classList.add('visible');
|
|
|
+ stepCards.innerHTML = '';
|
|
|
+ finalSection.innerHTML = '';
|
|
|
+ traceDot.style.background = 'var(--accent)';
|
|
|
+ traceDot.style.animation = 'pulse 1.5s ease infinite';
|
|
|
+ traceTitle.textContent = `${MODE_INFO[currentMode].title} — 执行中`;
|
|
|
+
|
|
|
+ // Timer
|
|
|
+ let startTime = Date.now();
|
|
|
+ timerInterval = setInterval(() => {
|
|
|
+ traceTimer.textContent = `${((Date.now() - startTime) / 1000).toFixed(1)}s`;
|
|
|
+ }, 100);
|
|
|
+
|
|
|
+ try {
|
|
|
+ const resp = await fetch('/api/execute', {
|
|
|
+ method: 'POST',
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
+ body: JSON.stringify({ mode: currentMode, query }),
|
|
|
+ });
|
|
|
+
|
|
|
+ const reader = resp.body.getReader();
|
|
|
+ const decoder = new TextDecoder();
|
|
|
+ let buffer = '';
|
|
|
+ let currentStepEl = null;
|
|
|
+ let totalLlmCalls = 0;
|
|
|
+ let totalToolCalls = 0;
|
|
|
+ let totalTokens = 0;
|
|
|
+
|
|
|
+ while (true) {
|
|
|
+ const { done, value } = await reader.read();
|
|
|
+ if (done) break;
|
|
|
+ buffer += decoder.decode(value, { stream: true });
|
|
|
+ const lines = buffer.split('\n');
|
|
|
+ buffer = lines.pop();
|
|
|
+
|
|
|
+ for (const line of lines) {
|
|
|
+ if (!line.startsWith('data: ')) continue;
|
|
|
+ const event = JSON.parse(line.slice(6));
|
|
|
+
|
|
|
+ if (event.type === 'step_start') {
|
|
|
+ currentStepEl = createStepCard(event);
|
|
|
+ stepCards.appendChild(currentStepEl);
|
|
|
+ currentStepEl.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
|
|
+ }
|
|
|
+ else if (event.type === 'agent_event' && currentStepEl) {
|
|
|
+ if (event.tool) {
|
|
|
+ totalToolCalls++;
|
|
|
+ appendEvent(currentStepEl, event);
|
|
|
+ } else if (event.answer) {
|
|
|
+ appendEvent(currentStepEl, event);
|
|
|
+ } else {
|
|
|
+ totalLlmCalls++;
|
|
|
+ if (event.tokens) totalTokens += event.tokens;
|
|
|
+ appendEvent(currentStepEl, event);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ else if (event.type === 'step_end' && currentStepEl) {
|
|
|
+ // Mark step complete
|
|
|
+ const head = currentStepEl.querySelector('.step-action');
|
|
|
+ if (head) head.textContent = '✅ 完成';
|
|
|
+ }
|
|
|
+ else if (event.type === 'final') {
|
|
|
+ traceDot.style.background = 'var(--accent)';
|
|
|
+ traceDot.style.animation = 'none';
|
|
|
+ traceTitle.textContent = `${MODE_INFO[currentMode].title} — 完成`;
|
|
|
+
|
|
|
+ finalSection.innerHTML = `
|
|
|
+ <div class="glass final-answer">
|
|
|
+ <div class="fa-label">📋 最终结果</div>
|
|
|
+ <div class="fa-content">${escHtml(event.answer)}</div>
|
|
|
+ </div>
|
|
|
+ <div class="glass stats-bar">
|
|
|
+ <div class="stat"><div class="stat-val">${totalLlmCalls}</div><div class="stat-label">LLM 调用</div></div>
|
|
|
+ <div class="stat"><div class="stat-val">${totalToolCalls}</div><div class="stat-label">工具调用</div></div>
|
|
|
+ <div class="stat"><div class="stat-val">${totalTokens.toLocaleString()}</div><div class="stat-label">Tokens</div></div>
|
|
|
+ <div class="stat"><div class="stat-val">${(event.total_ms / 1000).toFixed(1)}s</div><div class="stat-label">总耗时</div></div>
|
|
|
+ </div>`;
|
|
|
+ finalSection.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
|
|
+ }
|
|
|
+ else if (event.type === 'error') {
|
|
|
+ traceDot.style.background = 'var(--red)';
|
|
|
+ traceTitle.textContent = '执行出错';
|
|
|
+ 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>`;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ traceTitle.textContent = '连接失败';
|
|
|
+ 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>`;
|
|
|
+ } finally {
|
|
|
+ clearInterval(timerInterval);
|
|
|
+ isRunning = false;
|
|
|
+ btn.disabled = false;
|
|
|
+ btn.textContent = '▶ 执行';
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function createStepCard(event) {
|
|
|
+ const el = document.createElement('div');
|
|
|
+ el.className = 'glass step';
|
|
|
+ el.innerHTML = `
|
|
|
+ <div class="step-head">
|
|
|
+ <span class="step-icon">${event.icon || '🤖'}</span>
|
|
|
+ <span class="step-agent">${escHtml(event.agent)}</span>
|
|
|
+ <span class="step-action">${escHtml(event.action)} <span class="loading-dots"><span></span><span></span><span></span></span></span>
|
|
|
+ </div>
|
|
|
+ <div class="step-events"></div>`;
|
|
|
+ return el;
|
|
|
+}
|
|
|
+
|
|
|
+function appendEvent(stepEl, event) {
|
|
|
+ const container = stepEl.querySelector('.step-events');
|
|
|
+ const el = document.createElement('div');
|
|
|
+ el.className = 'event';
|
|
|
+
|
|
|
+ if (event.tool) {
|
|
|
+ const fullResult = escHtml(event.result || '');
|
|
|
+ const hasMore = fullResult.length > 150;
|
|
|
+ const shortResult = hasMore ? fullResult.slice(0, 150) + '...' : fullResult;
|
|
|
+ el.innerHTML = `
|
|
|
+ <div class="event-header">
|
|
|
+ <span class="event-tag tool">🔧 ${escHtml(event.tool)}</span>
|
|
|
+ <span class="event-meta">${event.latency_ms || 0}ms</span>
|
|
|
+ </div>
|
|
|
+ <div class="event-body">${shortResult}</div>
|
|
|
+ ${hasMore ? '<button class="event-expand" onclick="toggleExpand(this)">展开</button>' : ''}`;
|
|
|
+ if (hasMore) {
|
|
|
+ el.dataset.fullResult = fullResult;
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ el.innerHTML = `
|
|
|
+ <div class="event-header">
|
|
|
+ <span class="event-tag llm">🧠 ${escHtml(event.model || 'LLM')}</span>
|
|
|
+ <span class="event-meta">${event.latency_ms || 0}ms · ${(event.tokens || 0).toLocaleString()} tokens</span>
|
|
|
+ </div>`;
|
|
|
+ }
|
|
|
+ container.appendChild(el);
|
|
|
+}
|
|
|
+
|
|
|
+function toggleExpand(btn) {
|
|
|
+ const body = btn.previousElementSibling;
|
|
|
+ const eventEl = btn.closest('.event');
|
|
|
+ if (body.classList.contains('expanded')) {
|
|
|
+ body.classList.remove('expanded');
|
|
|
+ body.innerHTML = body.dataset.shortResult || body.textContent.slice(0, 150) + '...';
|
|
|
+ btn.textContent = '展开';
|
|
|
+ } else {
|
|
|
+ body.dataset.shortResult = body.innerHTML;
|
|
|
+ body.innerHTML = eventEl.dataset.fullResult || body.textContent;
|
|
|
+ body.classList.add('expanded');
|
|
|
+ btn.textContent = '收起';
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function escHtml(s) {
|
|
|
+ if (!s) return '';
|
|
|
+ const d = document.createElement('div');
|
|
|
+ d.textContent = s;
|
|
|
+ return d.innerHTML;
|
|
|
+}
|
|
|
+
|
|
|
+init();
|
|
|
+</script>
|
|
|
+</body>
|
|
|
+</html>
|