| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570 |
- <!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>
|