forked from Gitlink/gitlink-cli
595 lines
32 KiB
HTML
595 lines
32 KiB
HTML
<!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">● 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">查看完整结果 →</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 ❯</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=>({'&':'&','<':'<','>':'>'}[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>
|