gitlink-cli/cmd/server/static/index.html

595 lines
32 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>GitLink Research Atlas · 科研代码图谱智能体</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@500;600;700&family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@500;700&family=Noto+Serif+SC:wght@600;700&family=Noto+Sans+SC:wght@400;500;700&display=swap" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script>
<style>
:root{
--bg:#F7F9FC; --card:#FFFFFF; --ink:#1E2A44; --ink2:#475569; --mute:#94A3B8;
--line:#E6EBF2; --indigo:#4F6BED; --coral:#F26B5E; --mint:#2EC4B6;
--t-blue:#E8EEFF; --t-coral:#FFEDEA; --t-mint:#E6F7F4; --t-amber:#FEF3C7;
--drawer:#111A2E;
--serif:"Cormorant Garamond","Noto Serif SC",Georgia,serif;
--sans:"Inter","Noto Sans SC",-apple-system,"PingFang SC","Microsoft YaHei",sans-serif;
--mono:"JetBrains Mono",ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
--shadow:0 1px 2px rgba(30,42,68,.04),0 6px 18px rgba(30,42,68,.06);
--shadow-sm:0 1px 2px rgba(30,42,68,.06);
}
*{box-sizing:border-box;margin:0;padding:0}
html,body{height:100%;margin:0;overflow:hidden}
body{font-family:var(--sans);color:var(--ink);background:var(--bg);
background-image:radial-gradient(1200px 480px at 80% -8%,#EAF0FF 0%,rgba(234,240,255,0) 60%),
radial-gradient(900px 420px at 0% 0%,#FFF1EE 0%,rgba(255,241,238,0) 55%);
-webkit-font-smoothing:antialiased}
.app{display:flex;flex-direction:column;height:100vh;overflow:hidden}
/* ========== HEADER ========== */
header.bar{
display:flex;align-items:center;gap:18px;padding:0 22px;
min-height:62px;height:auto;
background:rgba(255,255,255,.86);backdrop-filter:blur(8px);
border-bottom:1px solid var(--line);position:relative;z-index:5;
overflow:visible;flex-shrink:0;
}
.brand{display:flex;align-items:center;gap:10px;min-width:0;flex-shrink:0}
.brand .mark{width:30px;height:30px;border-radius:9px;background:linear-gradient(135deg,var(--indigo),#7C8CF5 60%,var(--coral));
display:grid;place-items:center;color:#fff;font-family:var(--serif);font-weight:700;font-size:18px;box-shadow:var(--shadow-sm)}
.brand .t1{font-family:var(--serif);font-weight:700;font-size:20px;line-height:1.1;letter-spacing:.2px}
.brand .t2{font-size:10px;color:var(--mute);letter-spacing:.12em;text-transform:uppercase;line-height:1.2}
.search{display:flex;align-items:center;gap:8px;margin-left:8px;flex:1;max-width:640px}
.field{display:flex;align-items:center;gap:7px;background:var(--card);border:1px solid var(--line);
border-radius:10px;padding:7px 11px;box-shadow:var(--shadow-sm);flex:1}
.field .k{font-family:var(--mono);font-size:11px;color:var(--mute)}
.field input{border:0;outline:0;font-family:var(--mono);font-size:13px;color:var(--ink);width:100%;background:transparent}
.chip{display:inline-flex;align-items:center;gap:6px;font-size:12px;font-weight:600;padding:6px 11px;border-radius:999px;white-space:nowrap}
.chip.agent{background:var(--t-mint);color:#0E7E72;border:1px solid #B6EBE3}
.chip.agent::before{content:"";width:7px;height:7px;border-radius:50%;background:var(--mint);box-shadow:0 0 0 3px rgba(46,196,182,.18);animation:pulse 1.8s infinite}
.btn-login{font-size:12px;font-weight:600;color:#fff;background:var(--indigo);border:0;border-radius:10px;padding:8px 15px;cursor:pointer;box-shadow:var(--shadow-sm)}
.btn-hot{font-size:12px;font-weight:600;color:#fff;background:linear-gradient(135deg,var(--coral),#FF8A7A);text-decoration:none;border:0;border-radius:10px;padding:8px 14px;cursor:pointer;box-shadow:var(--shadow-sm);display:inline-flex;align-items:center;gap:5px;white-space:nowrap}
.btn-hot:hover{filter:brightness(1.05)}
@keyframes pulse{50%{opacity:.4}}
/* ========== MAIN GRID ========== */
main.grid{
display:grid;grid-template-columns:262px 1fr 332px;
gap:16px;padding:16px;flex:1;min-height:0;overflow:hidden;
}
aside.left,aside.right{min-height:0;overflow:auto;display:flex;flex-direction:column;gap:12px;padding-right:2px}
section.center{min-height:0;display:flex;flex-direction:column;gap:14px;overflow:hidden}
.panel{background:var(--card);border:1px solid var(--line);border-radius:14px;box-shadow:var(--shadow)}
.panel .ph{padding:14px 16px 6px}
.panel .ph h3{margin:0;font-size:13px;font-weight:700;letter-spacing:.02em}
.panel .ph .sub{font-size:11px;color:var(--mute);margin-top:2px}
.eyebrow{font-family:var(--mono);font-size:10px;letter-spacing:.16em;text-transform:uppercase;color:var(--mute)}
/* ========== SCENARIO CARDS ========== */
.scn{display:flex;gap:11px;align-items:flex-start;padding:11px 12px;border:1px solid var(--line);border-radius:12px;
background:var(--card);cursor:pointer;transition:.15s;position:relative;box-shadow:var(--shadow-sm)}
.scn:hover{border-color:#C7D2FE;transform:translateY(-1px)}
.scn.active{border-color:var(--indigo);background:linear-gradient(180deg,#F5F8FF,#fff)}
.scn.active::after{content:"";position:absolute;top:10px;right:10px;width:7px;height:7px;border-radius:50%;background:var(--coral)}
.scn.running{pointer-events:none;opacity:.7}
.scn .ico{width:30px;height:30px;border-radius:9px;display:grid;place-items:center;flex:0 0 auto;font-size:15px}
.scn .t{min-width:0}
.scn .t b{font-size:13px;font-weight:600;display:block}
.scn .t small{font-size:11px;color:var(--mute);line-height:1.35;display:block;margin-top:1px}
.tint-blue .ico{background:var(--t-blue);color:var(--indigo)}
.tint-coral .ico{background:var(--t-coral);color:var(--coral)}
.tint-mint .ico{background:var(--t-mint);color:#0E7E72}
.tint-amber .ico{background:var(--t-amber);color:#B45309}
/* ========== CENTER HERO / GRAPH ========== */
.hero{flex:1;min-height:0;display:flex;flex-direction:column;overflow:hidden}
.hero .ph{display:flex;align-items:center;justify-content:space-between;flex-shrink:0}
.hero .ph .live{font-family:var(--mono);font-size:11px;color:var(--indigo);background:var(--t-blue);padding:4px 9px;border-radius:999px}
.graph-wrap{flex:1;min-height:0;position:relative;margin:0 4px 4px;border-radius:10px;
background:radial-gradient(420px 320px at 50% 42%,#F2F6FF 0%,rgba(247,249,252,0) 70%);overflow:hidden}
svg.graph{width:100%;height:100%;display:block}
.g-edge{stroke:#9DB4F0;stroke-opacity:.55;stroke-width:1}
.g-node circle{stroke:#fff;stroke-width:2;transition:.2s}
.g-node:hover circle{filter:brightness(1.08)}
.g-node text{font-family:var(--sans);font-size:10px;font-weight:600;fill:var(--ink);pointer-events:none;text-anchor:middle}
.glow{filter:drop-shadow(0 0 6px rgba(242,107,94,.55))}
/* hero-overlay: ONLY visible during loading/empty, hidden after graph renders */
.hero-overlay{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;flex-direction:column;gap:8px;pointer-events:none;
transition:opacity .3s}
.hero-overlay.hidden{opacity:0;pointer-events:none}
.hero-overlay .hint{font-family:var(--mono);font-size:12px;color:var(--mute);background:rgba(255,255,255,.8);padding:6px 12px;border-radius:999px}
.spin{width:26px;height:26px;border-radius:50%;border:3px solid var(--t-blue);border-top-color:var(--indigo);animation:rot .8s linear infinite}
@keyframes rot{to{transform:rotate(360deg)}}
.center-extra{flex:0 0 auto;max-height:46%;display:flex;flex-direction:column;gap:10px;overflow:auto}
.center-extra:empty{display:none}
iframe.plot{width:100%;height:420px;border:0;border-radius:10px;background:#fff}
.mmd-box{background:#fff;padding:8px;border-radius:10px;overflow:auto;max-height:420px}
/* Graph legend bar */
.graph-legend{display:flex;align-items:center;gap:14px;padding:6px 16px 10px;flex-wrap:wrap;flex-shrink:0}
.graph-legend .leg-item{display:flex;align-items:center;gap:5px;font-size:11px;color:var(--ink2)}
.graph-legend .leg-dot{width:10px;height:10px;border-radius:50%;flex-shrink:0}
.graph-legend .leg-note{font-size:11px;color:var(--mute);margin-left:auto}
/* ========== RIGHT COLUMN ========== */
.metrics{display:grid;grid-template-columns:1fr 1fr;gap:10px;padding:6px 14px 14px}
.tile{border:1px solid var(--line);border-radius:12px;padding:12px;background:#FCFDFE;box-shadow:var(--shadow-sm)}
.tile .lab{display:flex;align-items:center;gap:6px;font-size:11px;color:var(--ink2);font-weight:500}
.tile .lab .dot{width:8px;height:8px;border-radius:50%}
.tile .val{font-family:var(--mono);font-weight:700;font-size:24px;line-height:1.1;margin-top:6px;color:var(--ink)}
.tile .of{font-size:11px;color:var(--mute);margin-top:3px}
.d-indigo{background:var(--indigo)} .d-mint{background:var(--mint)} .d-coral{background:var(--coral)} .d-amber{background:#F59E0B}
.findings{padding:4px 14px 14px;display:flex;flex-direction:column;gap:9px;max-height:330px;overflow:auto}
.find{border-left:3px solid var(--indigo);padding:8px 11px;background:#F8FAFF;border-radius:0 9px 9px 0}
.find.critical{border-color:var(--coral);background:#FFF5F3}
.find.warning{border-color:#F59E0B;background:#FFFBEB}
.find.success{border-color:var(--mint);background:#F0FBF9}
.find .ft{font-size:11px;font-weight:700;color:var(--ink2);display:flex;justify-content:space-between;gap:8px}
.find .fb{font-size:12px;color:var(--ink);margin-top:3px;line-height:1.45}
.find .fs{font-size:11px;color:var(--mute);margin-top:3px}
.bar{height:6px;border-radius:4px;background:var(--line);overflow:hidden;margin-top:6px}
.bar > i{display:block;height:100%;background:linear-gradient(90deg,var(--indigo),#7C8CF5)}
.empty{font-size:12px;color:var(--mute);padding:18px;text-align:center}
.loading-brief{font-size:12px;color:var(--mute);padding:18px;text-align:center;display:flex;flex-direction:column;align-items:center;gap:8px}
.loading-brief .spin{width:20px;height:20px;border-width:2px}
/* CTA button to view full result */
.view-result-btn{display:block;margin:8px 14px 14px;padding:10px 18px;border:0;border-radius:12px;
background:linear-gradient(135deg,var(--indigo),#7C8CF5);color:#fff;font-family:var(--sans);
font-size:13px;font-weight:600;cursor:pointer;text-align:center;box-shadow:var(--shadow);
transition:.15s}
.view-result-btn:hover{transform:translateY(-1px);box-shadow:0 4px 14px rgba(79,107,237,.25)}
/* ========== COMMAND DRAWER ========== */
footer.drawer{display:flex;align-items:center;gap:14px;padding:0 18px;background:var(--drawer);color:#CBD5E1;font-family:var(--mono);font-size:12px;
flex-shrink:0;min-height:52px;height:52px}
.dots{display:flex;gap:6px}
.dots i{width:11px;height:11px;border-radius:50%;display:block}
.drawer .prompt{color:var(--mint)}
.drawer .cmd{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:#E2E8F0}
.drawer .st{color:var(--mute);font-size:11px;white-space:nowrap}
.drawer .st.running{color:var(--indigo);animation:pulse 1s infinite}
/* ========== SCROLLBARS ========== */
aside::-webkit-scrollbar,.findings::-webkit-scrollbar,.center-extra::-webkit-scrollbar{width:8px;height:8px}
::-webkit-scrollbar-thumb{background:#D7DEEA;border-radius:8px}
::-webkit-scrollbar-track{background:transparent}
/* ========== RESPONSIVE ========== */
@media (max-width:1180px){
main.grid{grid-template-columns:230px 1fr;grid-template-rows:auto auto}
aside.right{grid-column:1 / span 2;flex-direction:row;flex-wrap:wrap}
aside.right .panel{flex:1;min-width:280px}
}
@media (max-width:760px){
main.grid{grid-template-columns:1fr;overflow:auto}
aside.left,aside.right{grid-column:auto}
.search{display:none}
}
</style>
</head>
<body>
<div class="app">
<!-- ============ TOP BAR ============ -->
<header class="bar">
<div class="brand">
<div class="mark">G</div>
<div>
<div class="t1">GitLink Research Atlas</div>
<div class="t2">科研代码图谱智能体</div>
</div>
</div>
<div class="search">
<label class="field"><span class="k">repo</span>
<input id="owner" value="mindspore-Ecosystem" placeholder="owner">
<span class="k" style="color:#CBD5E1">/</span>
<input id="repo" value="mindspore" placeholder="repo" style="max-width:150px">
</label>
<label class="field"><span class="k">keyword</span>
<input id="keyword" value="deep learning,机器学习" placeholder="S2 用,逗号分隔">
</label>
</div>
<a class="btn-hot" href="hotspot.html">🔥 热点追踪</a>
<span class="chip agent">AI Agent online</span>
<button class="btn-login" id="loginBtn">Login</button>
</header>
<!-- ============ MAIN ============ -->
<main class="grid">
<!-- LEFT: scenarios -->
<aside class="left">
<div class="panel">
<div class="ph"><div class="eyebrow">Research Scenarios</div>
<h3>科研场景</h3><div class="sub">点选任一场景,服务器终端即运行 gitlink-cli + Python 算法</div>
</div>
<div id="scenarios" style="padding:8px 12px 14px;display:flex;flex-direction:column;gap:9px"></div>
</div>
</aside>
<!-- CENTER: graph / viz -->
<section class="center">
<div class="panel hero">
<div class="ph">
<div><div class="eyebrow">Knowledge Graph</div><h3 id="heroTitle">科研知识图谱</h3></div>
<span class="live" id="heroLive" style="display:none">&#9679; live build</span>
</div>
<div class="graph-wrap">
<svg class="graph" id="graph" preserveAspectRatio="xMidYMid meet"></svg>
<div class="hero-overlay hidden" id="heroOverlay">
<div class="hint">选择左侧科研场景以启动分析</div>
</div>
</div>
</div>
<div class="graph-legend" id="graphLegend"></div>
<div class="center-extra" id="centerExtra"></div>
</section>
<!-- RIGHT: metrics + report -->
<aside class="right">
<div class="panel">
<div class="ph"><div class="eyebrow">Live Metrics</div><h3>科研指标</h3></div>
<div class="metrics" id="metrics"></div>
</div>
<div class="panel" style="flex:1;min-height:0;display:flex;flex-direction:column">
<div class="ph"><div class="eyebrow">AI Research Brief</div><h3 id="briefTitle">研究简报</h3></div>
<div class="findings" id="findings"><div class="empty">运行场景后,这里展示创新点 / 风险预警 / 协作推荐等结构化结论</div></div>
<button class="view-result-btn" id="viewResultBtn" style="display:none" target="_blank">查看完整结果 &#8594;</button>
</div>
</aside>
</main>
<!-- ============ DRAWER ============ -->
<footer class="drawer">
<div class="dots"><i style="background:#FF5F57"></i><i style="background:#FEBC2E"></i><i style="background:#28C840"></i></div>
<span class="prompt">atlas &#10095;</span>
<span class="cmd" id="cmd">gitlink-cli research --repo mindspore-Ecosystem/mindspore --agent atlas</span>
<span class="st" id="cmdStatus">ready</span>
</footer>
</div>
<script>
mermaid.initialize({startOnLoad:false, theme:"base", securityLevel:"loose",
themeVariables:{primaryColor:"#E8EEFF",primaryTextColor:"#1E2A44",primaryBorderColor:"#4F6BED",lineColor:"#9DB4F0",fontSize:"12px"}});
let SCENARIOS=[], ACTIVE=null, LAST_RESULT_KEY=null;
/* ---------- node color palette ---------- */
const NODE_COLOR = {
repo:"#4F6BED", scholar:"#3B82F6", team:"#6366F1", topic:"#06B6D4", method:"#6366F1",
paper:"#8B5CF6", dataset:"#2EC4B6", model:"#4F6BED", experiment:"#F26B5E",
reproduce:"#2EC4B6", license:"#64748B", trend:"#F26B5E", issue:"#F59E0B", pr:"#10B981",
default:"#94A3B8"
};
const NODE_LABEL = {
repo:"仓库", scholar:"学者", topic:"主题", paper:"论文", dataset:"数据集",
model:"模型", experiment:"实验", reproduce:"复现", license:"许可证",
trend:"趋势", issue:"Issue", pr:"PR"
};
/* Default decorative graph — clean, ~12 nodes showing the research world */
const DEMO = {
nodes:[
{id:"repo",type:"repo",label:"GitLink Repo",imp:true},
{id:"scholar",type:"scholar",label:"Scholar"},
{id:"topic",type:"topic",label:"Topic"},
{id:"paper",type:"paper",label:"Paper"},
{id:"dataset",type:"dataset",label:"Dataset"},
{id:"model",type:"model",label:"Model"},
{id:"exp",type:"experiment",label:"Experiment",imp:true},
{id:"repr",type:"reproduce",label:"Reproduce"},
{id:"lic",type:"license",label:"License"},
{id:"trend",type:"trend",label:"Trend"},
{id:"issue",type:"issue",label:"Issue Signals"},
{id:"pr",type:"pr",label:"Pull Request"}
],
edges:[["repo","scholar"],["repo","topic"],["repo","paper"],["repo","dataset"],
["repo","model"],["repo","exp"],["repo","repr"],["repo","lic"],
["scholar","paper"],["topic","trend"],["exp","repr"],["issue","pr"],["pr","repo"],["model","dataset"]]
};
/* ---------- build legend HTML ---------- */
function buildLegend(){
const el=document.getElementById("graphLegend");
const types=new Set();
// gather types from current graph data
const src=ACTIVE&&ACTIVE._graphData?ACTIVE._graphData:DEMO;
(src.nodes||[]).forEach(n=>types.add(n.type||"default"));
let h="";
for(const t of [...types].sort()){
h+=`<span class="leg-item"><span class="leg-dot" style="background:${NODE_COLOR[t]||NODE_COLOR.default}"></span>${NODE_LABEL[t]||t}</span>`;
}
h+=`<span class="leg-note">示例:科研仓库与学者/主题/数据集的关联 — 选择 S2 生成真实领域图谱</span>`;
el.innerHTML=h;
}
/* ---------- force-directed layout ---------- */
function forceLayout(nodes, edges, W, H, iters){
const cx=W/2, cy=H/2;
nodes.forEach(n=>{ if(n.x==null){n.x=cx+(Math.random()-.5)*W*.5; n.y=cy+(Math.random()-.5)*H*.5;} });
for(let it=0; it<iters; it++){
for(const n of nodes){ n._fx=0; n._fy=0; }
for(let i=0;i<nodes.length;i++){
for(let j=i+1;j<nodes.length;j++){
let dx=nodes[i].x-nodes[j].x, dy=nodes[i].y-nodes[j].y;
let d2=dx*dx+dy*dy+0.02, d=Math.sqrt(d2), f=2600/d2;
let fx=dx/d*f, fy=dy/d*f;
nodes[i]._fx+=fx; nodes[i]._fy+=fy; nodes[j]._fx-=fx; nodes[j]._fy-=fy;
}
}
for(const e of edges){
const a=nodes.find(n=>n.id===e[0]), b=nodes.find(n=>n.id===e[1]); if(!a||!b) continue;
let dx=b.x-a.x, dy=b.y-a.y, d=Math.sqrt(dx*dx+dy*dy)+.02, L=a.imp||b.imp?120:78, k=.04;
let f=(d-L)*k, fx=dx/d*f, fy=dy/d*f;
a._fx+=fx; a._fy+=fy; b._fx-=fx; b._fy-=fy;
}
for(const n of nodes){
n._fx+=(cx-n.x)*.012; n._fy+=(cy-n.y)*.012;
n.x+=Math.max(-14,Math.min(14,n._fx)); n.y+=Math.max(-14,Math.min(14,n._fy));
n.x=Math.max(40,Math.min(W-40,n.x)); n.y=Math.max(34,Math.min(H-30,n.y));
}
}
}
function drawGraph(data){
const svg=document.getElementById("graph");
const W=svg.clientWidth||760, H=svg.clientHeight||460;
svg.setAttribute("viewBox",`0 0 ${W} ${H}`);
let nodes=(data.nodes||[]).map(n=>Object.assign({},n));
let rawEdges=(data.edges||[]);
// normalize edges to [source,target] pairs
let edges=rawEdges.map(e=>Array.isArray(e)?e:[e.source||e.from,e.target||e.to]);
// cap to ~40 for performance
if(nodes.length>40){
const deg=new Map(nodes.map(n=>[n.id,0]));
edges.forEach(e=>{deg.set(e[0],(deg.get(e[0])||0)+1);deg.set(e[1],(deg.get(e[1])||0)+1);});
nodes.sort((a,b)=>(deg.get(b.id)||0)-(deg.get(a.id)||0));
const keep=new Set(nodes.slice(0,40).map(n=>n.id));
nodes=nodes.filter(n=>keep.has(n.id));
edges=edges.filter(e=>keep.has(e[0])&&keep.has(e[1]));
}
forceLayout(nodes, edges, W, H, nodes.length>20?220:320);
let s=`<defs>
<filter id="glow"><feGaussianBlur stdDeviation="4" result="blur"/><feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
</defs>`;
for(const e of edges){
const a=nodes.find(n=>n.id===e[0]), b=nodes.find(n=>n.id===e[1]); if(!a||!b) continue;
s+=`<line class="g-edge" x1="${a.x.toFixed(1)}" y1="${a.y.toFixed(1)}" x2="${b.x.toFixed(1)}" y2="${b.y.toFixed(1)}"/>`;
}
for(const n of nodes){
const col=NODE_COLOR[n.type]||NODE_COLOR.default;
const r=n.imp?11:7;
const glowAttr=n.imp?` filter="url(#glow)"`:"";
s+=`<g class="g-node" transform="translate(${n.x.toFixed(1)},${n.y.toFixed(1)})">
<circle r="${r}" fill="${col}"${glowAttr}/><text y="${r+12}">${esc(n.label||n.id).slice(0,16)}</text></g>`;
}
svg.innerHTML=s;
// BUG FIX #1: after graph renders, immediately hide the overlay
hideOverlay();
buildLegend();
}
/* ---------- overlay helpers ---------- */
const overlay=document.getElementById("heroOverlay");
function showOverlay(html){
overlay.innerHTML=html||'<div class="hint">选择左侧科研场景以启动分析</div>';
overlay.classList.remove("hidden");
}
function showLoading(msg){
overlay.innerHTML=`<div class="spin"></div><div class="hint">${esc(msg||"加载中...")}</div>`;
overlay.classList.remove("hidden");
}
function hideOverlay(){
overlay.classList.add("hidden");
}
/* ---------- init scenarios ---------- */
async function init(){
try{
const r=await fetch("/api/scenarios",{headers:{"X-Demo-Token":token()}});
const j=await r.json(); SCENARIOS=j.scenarios||[];
}catch(e){ SCENARIOS=[]; }
const tints=["tint-blue","tint-coral","tint-mint","tint-amber","tint-blue","tint-mint"];
const icons=["","Ⅱ","Ⅲ","Ⅳ","","Ⅵ"];
const box=document.getElementById("scenarios");
box.innerHTML=SCENARIOS.map((s,i)=>`
<div class="scn ${tints[i%tints.length]}" data-key="${s.key}">
<div class="ico">${icons[i]||"•"}</div>
<div class="t"><b>${esc(s.label)}</b><small>${esc(s.desc)}<br><span style="color:#A5B4CF">需: ${(s.needs||[]).join(" · ")}</span></small></div>
</div>`).join("");
box.querySelectorAll(".scn").forEach(el=>el.onclick=()=>run(el.dataset.key, el));
// Draw default graph
drawGraph(DEMO);
setMetrics(defaultMetrics());
buildLegend();
// Login button
document.getElementById("loginBtn").onclick=()=>{
const t=prompt("粘贴演示 token留空表示无鉴权", localStorage.getItem("demo_token")||"");
if(t!=null){ localStorage.setItem("demo_token",t.trim()); alert(t.trim()?"已保存":"已清除"); }
};
// View result button
document.getElementById("viewResultBtn").onclick=()=>{
if(LAST_RESULT_KEY) window.open("result.html?scenario="+LAST_RESULT_KEY,"_blank");
};
}
/* ---------- run scenario ---------- */
async function run(key, el){
const s=SCENARIOS.find(x=>x.key===key); if(!s) return;
ACTIVE=s;
// Mark card active + running
document.querySelectorAll(".scn").forEach(e=>{e.classList.remove("active");e.classList.remove("running");});
if(el) el.classList.add("active","running");
document.getElementById("heroTitle").textContent=s.label;
// BUG FIX #3: immediate loading feedback everywhere
showLoading("服务器终端执行中:"+s.script+" …");
document.getElementById("heroLive").style.display="none";
document.getElementById("cmd").textContent=cmdText(s);
document.getElementById("cmdStatus").textContent="running…";
document.getElementById("cmdStatus").className="st running";
document.getElementById("centerExtra").innerHTML="";
document.getElementById("viewResultBtn").style.display="none";
LAST_RESULT_KEY=null;
// BUG FIX #3: set brief + metrics to loading state
setFindings('<div class="loading-brief"><div class="spin"></div><div>正在分析…</div></div>');
setMetrics(tile("indigo","status","…","计算中")+tile("mint","status","…","")+tile("coral","status","…","")+tile("amber","status","…",""));
const body={scenario:key, owner:val("owner"), repo:val("repo"), keyword:val("keyword")};
try{
const r=await fetch("/api/run",{method:"POST",headers:{"Content-Type":"application/json","X-Demo-Token":token()},body:JSON.stringify(body)});
const j=await r.json();
// Clear running state
document.querySelectorAll(".scn").forEach(e=>e.classList.remove("running"));
document.getElementById("cmdStatus").textContent=(j.ok?"✓ done":"✗ failed")+" · "+(j.duration||"");
document.getElementById("cmdStatus").className="st";
if(!j.ok){
document.getElementById("heroOverlay").innerHTML='<div class="hint" style="color:#F26B5E">运行失败:'+esc(j.error||"未知错误")+'</div>';
document.getElementById("heroOverlay").classList.remove("hidden");
return;
}
renderResult(s, j);
}catch(e){
document.querySelectorAll(".scn").forEach(e=>e.classList.remove("running"));
document.getElementById("cmdStatus").textContent="✗ 请求失败";
document.getElementById("cmdStatus").className="st";
document.getElementById("heroOverlay").innerHTML='<div class="hint" style="color:#F26B5E">请求失败:'+esc(e)+'</div>';
document.getElementById("heroOverlay").classList.remove("hidden");
}
}
function cmdText(s){
const o=val("owner"), r=val("repo"), k=val("keyword");
if(s.key==="s2") return `gitlink-cli research --graph -k "${k}"`;
return `gitlink-cli research ${s.script.replace(".py","")} --repo ${o}/${r}`;
}
function val(id){return document.getElementById(id).value.trim();}
function token(){return localStorage.getItem("demo_token")||"";}
/* ---------- render result ---------- */
function parseJson(j){
if(!j.artifacts||!j.artifacts.json) return null;
try{return JSON.parse(j.artifacts.json);}catch(e){return null;}
}
function renderResult(s, j){
const obj=parseJson(j);
const extra=document.getElementById("centerExtra");
// S2: real knowledge graph
if(obj && obj.nodes && obj.edges){
const graphData={nodes:obj.nodes.map(n=>({id:n.id,type:n.type||"default",label:n.label||n.id,imp:false})), edges:obj.edges.map(e=>[e.source||e.from,e.target||e.to])};
drawGraph(graphData);
ACTIVE._graphData=graphData;
document.getElementById("heroLive").style.display="";
document.getElementById("heroLive").textContent="● live — "+(obj.nodes.length)+" nodes, "+(obj.edges.length)+" edges";
}
// S6: plotly iframe
else if(j.artifacts && j.artifacts.html){
const blob=URL.createObjectURL(new Blob([j.artifacts.html],{type:"text/html"}));
extra.innerHTML='<div class="panel" style="padding:10px"><div class="eyebrow" style="padding:4px 6px">S6 Visualization · plotly</div>'+
'<iframe class="plot" src="'+blob+'"></iframe></div>';
// restore demo graph if no real graph
drawGraph(DEMO);
ACTIVE._graphData=null;
}
// S1/S4: mermaid
else if(j.artifacts && j.artifacts.mermaid){
const id="mmd_"+Date.now();
extra.innerHTML='<div class="panel" style="padding:10px"><div class="eyebrow" style="padding:4px 6px">关系图 · Mermaid</div><div class="mmd-box"><div class="mermaid" id="'+id+'">'+j.artifacts.mermaid.replace(/```mermaid|```/g,"")+'</div></div></div>';
setTimeout(()=>mermaid.run({nodes:["#"+id]}).catch(()=>{}),30);
drawGraph(DEMO);
ACTIVE._graphData=null;
}
// S3/S5 or others: keep demo graph, hide overlay
else{
drawGraph(DEMO);
ACTIVE._graphData=null;
}
// Metrics + findings
if(obj){
setMetrics(metricsFor(s.key, obj));
setFindings(findingsFor(s.key, obj));
} else {
setMetrics(defaultMetrics());
setFindings('<div class="empty">'+(j.stdout?esc(j.stdout.slice(-400)):"(无可解析 JSON 产物)")+'</div>');
}
// Show "view full result" CTA
LAST_RESULT_KEY=s.key;
const btn=document.getElementById("viewResultBtn");
btn.style.display="block";
btn.textContent="查看完整结果 → "+s.label;
}
/* ---------- metrics per scenario ---------- */
function tile(c,label,val,of){return `<div class="tile"><div class="lab"><span class="dot d-${c}"></span>${label}</div><div class="val">${val}</div><div class="of">${of||""}</div></div>`;}
function defaultMetrics(){
return tile("indigo","commits","1,248","+18% this month")+
tile("mint","PR merge","72.6%","healthy collab")+
tile("indigo","contributors","36","8 core scholars")+
tile("coral","compliance","88","示例数据 · 待运行");
}
function metricsFor(key,o){
const M=o.meta||{};
switch(key){
case "s1": return tile("indigo","commits",M.commit_count||0,"采样提交")+tile("mint","merged PR",M.merged_pr_count||0,"已合并")+tile("amber","docs",M.doc_count||0,"文档文件")+tile("coral","创新点",(o.innovation_points||[]).length,"高影响合并");
case "s2": return tile("indigo","repos",M.repo_count||(o.nodes||[]).length,"仓库节点")+tile("coral","scholars",M.scholar_count||0,"学者")+tile("mint","topics",M.topic_count||0,"主题方向")+tile("amber","edges",M.edge_count||(o.edges||[]).length,"关系边");
case "s3": return tile("mint","repro",(o.repro_score||0)+"/10","复现性")+tile("indigo","compliance",(o.compliance_score||0)+"/10","合规")+tile("coral","risks",(o.risks||o.risk_items||[]).length,"风险项")+tile("amber","license",esc(o.license?(typeof o.license==="string"?o.license:(o.license.id||o.license.name||o.license.spdx_id||"—")):(o.license_id||"—")),"识别许可证");
case "s4": return tile("indigo","gap topics",(o.gap_topics||[]).length,"缺口主题")+tile("mint","candidates",(o.candidates||[]).length,"推荐候选")+tile("coral","top score",(o.candidates&&o.candidates[0]?o.candidates[0].score:0),"最高匹配")+tile("amber","pool",o.meta&&o.meta.pool_size||0,"候选池");
case "s5": {const tw=(o.week_stats&&o.week_stats.this_week)||{}; return tile("indigo","commits/周",tw.commits||0,"本周")+tile("coral","risks",(o.risk_warnings||[]).length,"预警")+tile("mint","PR 合并",tw.prs_merged||0,"本周")+tile("amber","trend",(o.trend&&o.trend.activity_level)||"—","活跃度");}
case "s6": {const tl=o.timeline||{}; const sm=a=>(a||[]).reduce((x,y)=>x+(y||0),0); return tile("indigo","weeks",(tl.labels||[]).length,"时间线周数")+tile("coral","commits",sm(tl.commits),"累计")+tile("mint","PRs",sm(tl.prs),"累计")+tile("amber","issues",sm(tl.issues),"累计");}
}
return defaultMetrics();
}
function setMetrics(html){document.getElementById("metrics").innerHTML=html;}
/* ---------- findings per scenario ---------- */
function find(cls,title,body,sub){return `<div class="find ${cls}"><div class="ft"><span>${title}</span></div><div class="fb">${body}</div>${sub?`<div class="fs">${sub}</div>`:""}</div>`;}
function rank(c){return `<div class="bar"><i style="width:${Math.min(100,Math.max(4,c))}%"></i></div>`;}
function findingsFor(key,o){
let out="";
switch(key){
case "s1": (o.innovation_points||[]).slice(0,6).forEach(i=>{out+=find("warning","["+i.category+"] 创新点",esc(i.description),esc(i.evidence));}); break;
case "s2": (o.topic_heat||[]).slice(0,4).forEach(h=>{out+=find("success","主题热度 · "+h.topic,"覆盖 "+h.count+" 个仓库",rank(h.count*15));});
(o.core_scholars||[]).slice(0,3).forEach(s=>{out+=find("",esc(s.login),"关联 "+s.repo_count+" 个仓库");}); break;
case "s3": (o.risk_items||o.risks||[]).slice(0,6).forEach(r=>{const lv=(r.level||"").includes("high")||r.severity==="P0"?"critical":"warning"; out+=find(lv,esc((r.level||r.severity||"")+" · "+(r.category||r.name||"")),esc(r.detail||r.evidence||r.name||""),esc(r.file?r.file:""));});
out+=find("success","复现性 "+(o.repro_score||0)+"/10 · 合规 "+(o.compliance_score||0)+"/10","许可证: "+esc(o.license?(typeof o.license==="string"?o.license:(o.license.id||"—")):"—")); break;
case "s4": (o.candidates||[]).slice(0,5).forEach((c,i)=>{out+=find(i===0?"success":"", "#"+(i+1)+" "+esc(c.login), (c.reasons||[]).join("")||"—", rank(c.score)+` <span style="float:right">${c.score}分</span>`);}); break;
case "s5": (o.risk_warnings||[]).slice(0,6).forEach(w=>{out+=find(w.level==="critical"?"critical":"warning",esc(w.level+" · "+w.type),esc(w.message),esc(w.suggestion||""));}); break;
case "s6": out+=find("success","可视化产物","时间线 "+((o.timeline&&o.timeline.labels||[]).length)+" 周 · 语言 "+((o.language_pie&&o.language_pie.length)||0)+" 类","已生成可交互 plotly 图表"); break;
}
return out||'<div class="empty">(该场景无结构化简报,详见完整结果页)</div>';
}
function setFindings(html){document.getElementById("findings").innerHTML=html;}
/* ---------- utilities ---------- */
function esc(s){return String(s).replace(/[&<>]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;'}[c]));}
/* ---------- resize handler ---------- */
let resizeTimer;
window.addEventListener("resize",()=>{
clearTimeout(resizeTimer);
resizeTimer=setTimeout(()=>{
const src=ACTIVE&&ACTIVE._graphData?ACTIVE._graphData:DEMO;
drawGraph(src);
},200);
});
/* go */
init();
</script>
</body>
</html>