Files
projectAIpopular/gateway/static/index.html
T

167 lines
16 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>端云协同 LLM 协作系统</title>
<style>
:root{--bg:#0f1115;--panel:#171a21;--panel2:#1d212b;--line:#2a2f3a;--txt:#e6e9ef;--muted:#8b93a3;--acc:#4f8cff;--acc2:#23c46d;--warn:#f5a623;--bad:#ff5c5c;--mono:Consolas,"Courier New",monospace}
*{box-sizing:border-box;margin:0;padding:0}
body{background:var(--bg);color:var(--txt);font:14px/1.6 -apple-system,"Segoe UI","Microsoft YaHei",sans-serif;height:100vh;display:flex;flex-direction:column}
header{display:flex;align-items:center;gap:16px;padding:12px 20px;background:var(--panel);border-bottom:1px solid var(--line)}
header h1{font-size:16px;font-weight:600}
header .sub{color:var(--muted);font-size:12px}
.pill{font-size:11px;padding:2px 8px;border-radius:10px;border:1px solid var(--line);color:var(--muted)}
nav{display:flex;gap:4px;padding:8px 20px;background:var(--panel);border-bottom:1px solid var(--line)}
nav button{background:none;border:1px solid transparent;color:var(--muted);padding:6px 14px;border-radius:8px;cursor:pointer;font-size:13px}
nav button.active{color:var(--txt);background:var(--panel2);border-color:var(--line)}
nav button:hover{color:var(--txt)}
main{flex:1;overflow:auto;padding:16px 20px}
.panel{display:none;max-width:1200px;margin:0 auto}
.panel.active{display:block}
.chatrow{display:flex;gap:16px;align-items:flex-start}
.chatbox{flex:1}
.prompt{display:flex;gap:8px;margin-bottom:12px}
textarea{flex:1;resize:vertical;min-height:70px;background:var(--panel2);border:1px solid var(--line);color:var(--txt);border-radius:10px;padding:10px;font-size:14px;font-family:inherit}
textarea:focus{outline:none;border-color:var(--acc)}
button.primary{background:var(--acc);color:#fff;border:none;border-radius:10px;padding:10px 20px;font-size:14px;cursor:pointer}
button.primary:disabled{opacity:.5;cursor:not-allowed}
.card{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:16px;margin-bottom:14px}
.card h3{font-size:13px;color:var(--muted);margin-bottom:10px;font-weight:600;letter-spacing:.5px}
.resp{white-space:pre-wrap;word-break:break-word;font-size:14px}
.badges{display:flex;flex-wrap:wrap;gap:6px;margin:10px 0}
.b{font-size:11px;padding:2px 8px;border-radius:10px}
.b.ok{background:rgba(35,196,109,.15);color:var(--acc2);border:1px solid rgba(35,196,109,.4)}
.b.fast{background:rgba(79,140,255,.15);color:var(--acc);border:1px solid rgba(79,140,255,.4)}
.b.warn{background:rgba(245,166,35,.15);color:var(--warn);border:1px solid rgba(245,166,35,.4)}
.b.err{background:rgba(255,92,92,.15);color:var(--bad);border:1px solid rgba(255,92,92,.4)}
.b.plain{background:var(--panel2);color:var(--muted);border:1px solid var(--line)}
.chips{display:flex;flex-wrap:wrap;gap:4px;margin-top:8px}
.chip{font-size:11px;font-family:var(--mono);background:var(--panel2);color:var(--muted);padding:2px 7px;border-radius:6px;border:1px solid var(--line)}
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:12px}
.stat{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:14px}
.stat .v{font-size:22px;font-weight:700}
.stat .k{font-size:12px;color:var(--muted);margin-top:2px}
.step{display:flex;gap:8px;align-items:baseline;padding:6px 0;border-bottom:1px dashed var(--line)}
.step:last-child{border:none}
.step .id{font-family:var(--mono);color:var(--acc);font-weight:600}
.step .st{font-size:11px;margin-left:auto}
.done{color:var(--acc2)}.fail{color:var(--bad)}.block{color:var(--warn)}
details{background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:10px 12px;margin-bottom:8px}
summary{cursor:pointer;font-weight:600;font-size:13px}
pre.trace{background:var(--bg);border:1px solid var(--line);border-radius:8px;padding:10px;font-family:var(--mono);font-size:12px;overflow:auto;color:var(--muted)}
.kw{font-family:var(--mono);color:var(--acc)}
.row-btns{display:flex;gap:8px;margin-top:8px;flex-wrap:wrap}
button.small{background:var(--panel2);border:1px solid var(--line);color:var(--txt);border-radius:8px;padding:6px 12px;font-size:12px;cursor:pointer}
button.small:hover{border-color:var(--acc)}
input,select{background:var(--panel2);border:1px solid var(--line);color:var(--txt);border-radius:8px;padding:6px 10px;font-size:13px}
.muted{color:var(--muted)}
.history{max-height:220px;overflow:auto;margin-top:8px}
table{width:100%;border-collapse:collapse;font-size:13px}
th,td{text-align:left;padding:8px;border-bottom:1px solid var(--line)}
th{color:var(--muted);font-weight:600}
footer{padding:8px 20px;color:var(--muted);font-size:11px;border-top:1px solid var(--line);background:var(--panel)}
</style>
</head>
<body>
<header>
<h1>⚙️ 端云协同 LLM 协作系统</h1>
<span class="sub">Architect(大模型 API)+ Worker(本地小模型)+ 交流文本协议</span>
<span class="pill" id="healthPill"></span>
</header>
<nav>
<button data-tab="chat" class="active">💬 对话</button>
<button data-tab="work">🔗 协作过程</button>
<button data-tab="review">🧑‍💻 人工检验</button>
<button data-tab="metrics">📊 指标</button>
</nav>
<main>
<!-- 对话 -->
<section class="panel active" id="panel-chat">
<div class="chatrow">
<div class="chatbox">
<div class="prompt">
<textarea id="query" placeholder="输入你的问题,例如:用 Python 写一个快速排序并分析复杂度…"></textarea>
<button class="primary" id="sendBtn">发送</button>
</div>
<div class="row-btns">
<button class="small" id="legacyBtn">🔁 与 v1 legacy 对照</button>
<button class="small" id="sampleBtn">🎲 示例</button>
</div>
<div id="chatResult"></div>
<div id="legacyResult" style="margin-top:12px"></div>
</div>
<div id="workspacePanel" style="flex:1;max-width:480px"></div>
</div>
</section>
<!-- 协作过程 -->
<section class="panel" id="panel-work">
<div class="row-btns" style="margin-bottom:10px">
<input id="workRid" placeholder="输入 request_id,留空用最近一次" style="width:300px">
<button class="small" id="loadWorkBtn">加载交流文本</button>
</div>
<div id="workResult"></div>
</section>
<!-- 人工检验 -->
<section class="panel" id="panel-review">
<div class="row-btns" style="margin-bottom:10px">
<select id="reviewStatus"><option value="">全部</option><option value="pending">待审</option><option value="reviewed">已审</option></select>
<button class="small" id="refreshReview">刷新</button>
</div>
<div id="reviewResult"></div>
</section>
<!-- 指标 -->
<section class="panel" id="panel-metrics">
<div class="row-btns" style="margin-bottom:10px"><button class="small" id="refreshMetrics">刷新</button></div>
<div id="metricsResult"></div>
</section>
</main>
<footer>端云协同 LLM 协作系统 · v2 · 交流文本协议 · 北极星:token 下降 ≥80%</footer>
<script>
const $=id=>document.getElementById(id);
let lastRequestId=null;
const esc=s=>{const d=document.createElement("div");d.textContent=s==null?"":String(s);return d.innerHTML;};
function el(tag,cls,txt){const n=document.createElement(tag);if(cls)n.className=cls;if(txt!=null)n.textContent=txt;return n;}
function setTab(t){document.querySelectorAll("nav button").forEach(b=>b.classList.toggle("active",b.dataset.tab===t));document.querySelectorAll(".panel").forEach(p=>p.classList.toggle("active",p.id==="panel-"+t));}
document.querySelectorAll("nav button").forEach(b=>b.onclick=()=>{setTab(b.dataset.tab);if(b.dataset.tab==="review")loadReview();if(b.dataset.tab==="metrics")loadMetrics();});
// ---- 健康状态 ----
async function health(){try{const r=await fetch("/health");const d=await r.json();$("healthPill").textContent="健康 · "+d.domains.length+" 领域";}catch(e){$("healthPill").textContent="离线";}}
// ---- 对话 ----
async function sendChat(){const q=$("query").value.trim();if(!q)return;const btn=$("sendBtn");btn.disabled=true;btn.textContent="运行中…";$("chatResult").innerHTML="";$("legacyResult").innerHTML="";$("workspacePanel").innerHTML="";try{const r=await fetch("/chat",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({query:q})});const d=await r.json();lastRequestId=d.request_id||null;renderChatResult(d);if(d.request_id)loadWorkspace(d.request_id);}catch(e){$("chatResult").appendChild(el("div","resp","请求失败:"+e.message));}finally{btn.disabled=false;btn.textContent="发送";}}
function renderChatResult(d){const c=$("chatResult");c.innerHTML="";const card=el("div","card");card.appendChild(el("h3","","响应"));const b=$("badges0")||el("div","badges");b.innerHTML="";const st=(d.status||"done");b.appendChild(chip(st==="fast_path"?"⚡ 快路径":"🤝 协作",st==="fast_path"?"b fast":(st==="done"?"b ok":"b warn")));b.appendChild(chip("status: "+esc(st),"b plain"));b.appendChild(chip("model: "+esc(d.model_used),"b plain"));b.appendChild(chip("输入 "+d.api_input_tokens+" / 输出 "+d.api_output_tokens+" token","b plain"));b.appendChild(chip("耗时 "+d.latency_ms+" ms","b plain"));b.appendChild(chip("rounds "+d.rounds_used,"b plain"));b.id="badges0";card.appendChild(b);const resp=el("div","resp",d.response||"(空)");card.appendChild(resp);if(d.error)card.appendChild(el("div","resp muted","⚠️ "+d.error));const route=el("div","chips");(d.route||[]).forEach(r=>route.appendChild(el("span","chip",r)));card.appendChild(el("h3","","路由轨迹"));card.appendChild(route);card.appendChild(el("div","muted","request_id: "+(d.request_id||"-")+"(见右侧协作过程)"));c.appendChild(card);}
function chip(txt,cls){const s=el("span","b "+cls,txt);return s;}
// ---- 协作过程 / 交流文本 ----
async function loadWorkspace(rid){try{const r=await fetch("/runs/"+rid+"/workspace");if(!r.ok)throw new Error("无 workspace");const ws=await r.json();renderWorkspace($("workspacePanel"),ws);if($("workRid"))$("workRid").value=rid;renderWorkTab(ws);}catch(e){$("workspacePanel").appendChild(el("div","muted","(该请求无交流文本:"+e.message+""));}}
function renderWorkspace(host,ws){host.innerHTML="";const card=el("div","card");card.appendChild(el("h3","","🤝 交流文本 · "+(ws.request_id||"")));const b=ws.brief||{};card.appendChild(el("div","resp","🎯 "+(b.goal||"")));const plan=el("div","");(b.plan||[]).forEach(p=>{const row=el("div","step");row.appendChild(el("span","id",p.id));row.appendChild(el("span","",p.task||""));plan.appendChild(row);});card.appendChild(el("h3","","计划 (plan)"));card.appendChild(plan);card.appendChild(el("h3","","进度 (progress)"));const pg=el("div","");(ws.progress||[]).forEach(p=>{const row=el("div","step");row.appendChild(el("span","id",p.step));row.appendChild(el("span","",p.summary||""));const st=el("span","st "+(p.status==="done"?"done":p.status==="failed"?"fail":"block"),p.status);row.appendChild(st);pg.appendChild(row);});card.appendChild(pg);if((ws.issues||[]).length){card.appendChild(el("h3","","问题 (issues)"));(ws.issues||[]).forEach(i=>{const dt=el("details","");dt.appendChild(el("summary","","#"+(i.id||"")+" · step "+(i.step||"")));dt.appendChild(el("div","resp","observed: "+(i.observed||"")+"\nask: "+(i.ask||"")));card.appendChild(dt);});}if((ws.decisions||[]).length){card.appendChild(el("h3","","裁决 (decisions)"));(ws.decisions||[]).forEach(dc=>{const dt=el("details","");dt.appendChild(el("summary","","#"+esc(dc.ref)+" · 裁决"));dt.appendChild(el("div","resp",dc.reply||""));card.appendChild(dt);});}if((ws.archive||[]).length){card.appendChild(el("h3","","归档 (archive)"));const ar=el("div","resp");(ws.archive||[]).forEach(a=>ar.appendChild(el("div","muted","▸ "+a)));card.appendChild(ar);}host.appendChild(card);}
function renderWorkTab(ws){const host=$("workResult");if(!host)return;host.innerHTML="";renderWorkspace(host,ws);}
async function loadWorkByRid(){const rid=$("workRid").value.trim()||lastRequestId;if(!rid){$("workResult").innerHTML="<div class=muted>没有 request_id</div>";return;}await loadWorkspace(rid);}
// ---- 人工检验 ----
async function loadReview(){const st=$("reviewStatus").value;const r=await fetch("/review/queue?limit=50&status="+st);const rows=await r.json();const host=$("reviewResult");host.innerHTML="";if(!rows.length){host.appendChild(el("div","muted","暂无记录"));return;}rows.forEach(x=>{const card=el("div","card");card.appendChild(el("h3","","#"+(x.id||"")+" · "+(x.request_id||"")+" · "+(x.status||"")));card.appendChild(el("div","resp",x.query||""));card.appendChild(el("div","muted","tags: "+(x.tags||[]).join(", ")+" · reason: "+(x.reason||"")));if(x.verdict)card.appendChild(el("div","muted","verdict: "+x.verdict+(x.correction?" · correction: "+x.correction:"")));if(x.status==="pending"){const btns=el("div","row-btns");["approve","edit","reject"].forEach(v=>{const b=el("button","small",v);b.onclick=()=>submitReview(x.id,v);btns.appendChild(b);});card.appendChild(btns);}host.appendChild(card);});}
async function submitReview(id,verdict){let correction=null;if(verdict==="edit"){correction=prompt("修正内容:");if(correction==null)return;}const r=await fetch("/review/"+id+"?verdict="+verdict+(correction?"&correction="+encodeURIComponent(correction):""),{method:"POST"});const d=await r.json();if(d.ok)loadReview();else alert(d.detail||"提交失败");}
// ---- 指标 ----
async function loadMetrics(){const r=await fetch("/metrics");const d=await r.json();const host=$("metricsResult");host.innerHTML="";const v2=d.v2||{};const grid=el("div","grid");stat(grid,"总请求",v2.total_requests||0);stat(grid,"快路径命中率",((v2.fast_path_rate||0)*100).toFixed(1)+"%");stat(grid,"熔断次数",v2.breach_count||0);stat(grid,"API 输入 token",v2.api_tokens?v2.api_tokens.input:0);stat(grid,"API 输出 token",v2.api_tokens?v2.api_tokens.output:0);stat(grid,"回合数均值",v2.rounds_used?v2.rounds_used.avg:0);host.appendChild(grid);const card=el("div","card");card.appendChild(el("h3","","状态分布"));const tb=el("table","");const tr=el("tr","");["status","count"].forEach(h=>tr.appendChild(el("th","",h)));tb.appendChild(tr);Object.entries(v2.status_distribution||{}).forEach(([k,v])=>{const r=el("tr","");r.appendChild(el("td","",k));r.appendChild(el("td","",v));tb.appendChild(r);});card.appendChild(tb);host.appendChild(card);const rc=el("div","card");rc.appendChild(el("h3","","回合数分布"));const tb2=el("table","");const tr2=el("tr","");["rounds","count"].forEach(h=>tr2.appendChild(el("th","",h)));tb2.appendChild(tr2);Object.entries((v2.rounds_used||{}).distribution||{}).forEach(([k,v])=>{const r=el("tr","");r.appendChild(el("td","",k));r.appendChild(el("td","",v));tb2.appendChild(r);});rc.appendChild(tb2);host.appendChild(rc);}
function stat(grid,v,k){const s=el("div","stat");s.appendChild(el("div","v",v));s.appendChild(el("div","k",k));grid.appendChild(s);}
// ---- v1 legacy 对照 ----
async function sendLegacy(){const q=$("query").value.trim();if(!q)return;const host=$("legacyResult");host.innerHTML="";try{const r=await fetch("/chat/legacy",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({query:q})});const d=await r.json();const card=el("div","card");card.appendChild(el("h3","","🔁 v1 legacy(规则路由)"));card.appendChild(el("div","resp",d.response||""));const chips=el("div","chips");(d.route||[]).forEach(x=>chips.appendChild(el("span","chip",x)));card.appendChild(chips);card.appendChild(el("div","muted","domain: "+(d.domain||"")+" · conf "+(d.confidence||"")+" · latency "+(d.latency_ms||0)+" ms"));host.appendChild(card);}catch(e){host.appendChild(el("div","muted","legacy 调用失败:"+e.message));}}
// ---- 示例 ----
const SAMPLES=["用 Python 实现快速排序,并分析时间与空间复杂度","求解方程 x^2 - 5x + 6 = 0","高血压患者日常饮食需要注意什么","解释一下深度学习中的注意力机制"];
function sample(){$("query").value=SAMPLES[Math.floor(Math.random()*SAMPLES.length)];}
$("sendBtn").onclick=sendChat;
$("legacyBtn").onclick=sendLegacy;
$("sampleBtn").onclick=sample;
$("loadWorkBtn").onclick=loadWorkByRid;
$("refreshReview").onclick=loadReview;
$("refreshMetrics").onclick=loadMetrics;
$("query").addEventListener("keydown",e=>{if(e.key==="Enter"&&(e.ctrlKey||e.metaKey))sendChat();});
health();loadMetrics();
</script>
</body>
</html>