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

486 lines
22 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">
<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;
--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{min-height:100%}
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}
.wrap{max-width:1180px;margin:0 auto;padding:28px 36px 72px}
/* header */
.hdr{display:flex;align-items:center;gap:14px;margin-bottom:22px}
.back{display:inline-flex;align-items:center;gap:6px;font-size:13px;font-weight:600;color:var(--indigo);
text-decoration:none;padding:7px 14px;border-radius:10px;background:var(--t-blue);white-space:nowrap}
.back:hover{background:#D6DEFF}
.hdr .titles h1{font-family:var(--serif);font-weight:700;font-size:30px;line-height:1.15}
.hdr .titles .sub{font-size:13px;color:var(--mute);margin-top:3px}
/* control panel */
.ctl{background:var(--card);border:1px solid var(--line);border-radius:16px;box-shadow:var(--shadow);
padding:22px 24px;margin-bottom:22px}
.row{display:flex;align-items:center;gap:12px;flex-wrap:wrap}
.row + .row{margin-top:14px}
.row .lbl{font-size:12px;font-weight:600;color:var(--ink2);min-width:84px;letter-spacing:.02em}
.search-input{flex:1;min-width:260px;display:flex;align-items:center;gap:8px;background:#F8FAFD;
border:1px solid var(--line);border-radius:11px;padding:11px 14px}
.search-input:focus-within{border-color:var(--indigo);background:#fff;box-shadow:0 0 0 3px rgba(79,107,237,.12)}
.search-input input{border:0;outline:0;font-family:var(--mono);font-size:14px;width:100%;background:transparent;color:var(--ink)}
.ex-chips{display:flex;gap:7px;flex-wrap:wrap}
.ex-chips .ex{font-size:12px;color:var(--ink2);background:#F1F5F9;border:1px solid var(--line);
padding:5px 11px;border-radius:999px;cursor:pointer;transition:.15s}
.ex-chips .ex:hover{background:var(--t-blue);border-color:#C7D2FE;color:var(--indigo)}
.angles{display:flex;gap:9px;flex-wrap:wrap}
.ang{display:inline-flex;align-items:center;gap:7px;font-size:13px;font-weight:500;padding:8px 14px;
border-radius:11px;border:1px solid var(--line);background:#fff;cursor:pointer;transition:.15s;user-select:none}
.ang .dot{width:9px;height:9px;border-radius:50%;background:var(--mute);transition:.15s}
.ang.on{border-color:var(--indigo);background:#F5F8FF;color:var(--ink)}
.ang.on .dot{background:var(--indigo);box-shadow:0 0 0 3px rgba(79,107,237,.18)}
.ang[data-a="trending"] .dot{background:var(--coral)} .ang.on[data-a="trending"] .dot{background:var(--coral);box-shadow:0 0 0 3px rgba(242,107,94,.2)}
.ang[data-a="active"] .dot{background:#F59E0B} .ang.on[data-a="active"] .dot{background:#F59E0B;box-shadow:0 0 0 3px rgba(245,158,11,.2)}
.ang[data-a="topics"] .dot{background:#06B6D4} .ang.on[data-a="topics"] .dot{background:#06B6D4;box-shadow:0 0 0 3px rgba(6,182,212,.2)}
.ang[data-a="scholars"] .dot{background:var(--mint)} .ang.on[data-a="scholars"] .dot{background:var(--mint);box-shadow:0 0 0 3px rgba(46,196,182,.2)}
select.lim{font-family:var(--mono);font-size:13px;padding:8px 10px;border:1px solid var(--line);border-radius:10px;background:#fff;color:var(--ink)}
.btn-run{margin-left:auto;font-size:14px;font-weight:600;color:#fff;background:linear-gradient(135deg,var(--indigo),#7C8CF5);
border:0;border-radius:11px;padding:11px 22px;cursor:pointer;box-shadow:var(--shadow-sm);transition:.15s}
.btn-run:hover{filter:brightness(1.05);transform:translateY(-1px)}
.btn-run:disabled{opacity:.6;cursor:not-allowed;transform:none}
.status{font-family:var(--mono);font-size:12px;color:var(--mute);margin-top:14px;display:flex;align-items:center;gap:8px}
.status .ok{color:var(--mint)} .status .err{color:var(--coral)}
.spin{width:14px;height:14px;border-radius:50%;border:2px solid var(--t-blue);border-top-color:var(--indigo);animation:rot .8s linear infinite}
@keyframes rot{to{transform:rotate(360deg)}}
/* summary banner */
.summary{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin-bottom:18px}
.sum-tile{border:1px solid var(--line);border-radius:13px;padding:14px 16px;background:var(--card);box-shadow:var(--shadow-sm)}
.sum-tile .sl{font-size:11px;color:var(--mute);font-weight:600;letter-spacing:.02em}
.sum-tile .sv{font-family:var(--mono);font-weight:700;font-size:26px;line-height:1.1;margin-top:4px;color:var(--ink)}
.sum-tile .ss{font-size:11px;color:var(--mute);margin-top:2px}
/* sections */
.sec{background:var(--card);border:1px solid var(--line);border-radius:16px;box-shadow:var(--shadow);
padding:20px 24px;margin-top:18px}
.sec-head{display:flex;align-items:center;gap:10px;margin-bottom:16px}
.sec-head .ico{width:30px;height:30px;border-radius:9px;display:grid;place-items:center;font-size:15px}
.sec-head h2{font-family:var(--serif);font-weight:700;font-size:21px}
.sec-head .eyebrow{font-family:var(--mono);font-size:10px;letter-spacing:.16em;text-transform:uppercase;color:var(--mute);display:block;margin-top:1px}
.sec-head .cnt{font-family:var(--mono);font-size:12px;color:var(--mute);margin-left:auto}
.ic-coral{background:var(--t-coral);color:var(--coral)} .ic-amber{background:var(--t-amber);color:#B45309}
.ic-cyan{background:#E0F7FA;color:#0891B2} .ic-mint{background:var(--t-mint);color:#0E7E72}
/* trending table */
.tbl{width:100%;border-collapse:collapse;font-size:13px}
.tbl th{text-align:left;font-size:11px;color:var(--mute);font-weight:600;padding:8px 10px;border-bottom:1px solid var(--line);text-transform:uppercase;letter-spacing:.04em}
.tbl td{padding:10px;border-bottom:1px solid var(--line);vertical-align:top}
.tbl tr:hover td{background:#F8FAFD}
.tbl .repo{font-family:var(--mono);font-weight:600;color:var(--indigo)}
.tbl .num{font-family:var(--mono);font-weight:700;text-align:right}
.lang-pill{font-size:11px;padding:2px 8px;border-radius:6px;background:var(--t-blue);color:var(--indigo)}
.scorebar{height:6px;border-radius:4px;background:var(--line);overflow:hidden;margin-top:4px;min-width:70px}
.scorebar i{display:block;height:100%;background:linear-gradient(90deg,var(--coral),#FF9B8F)}
.kw-chip{display:inline-block;font-size:10px;font-weight:600;padding:2px 7px;border-radius:5px;background:var(--t-amber);color:#92400E;margin-right:3px}
/* active discussions */
.disc{display:flex;flex-direction:column;gap:10px}
.disc-item{display:flex;gap:12px;align-items:flex-start;padding:12px 14px;border:1px solid var(--line);border-radius:11px;background:#FCFDFE}
.disc-item .badge{font-size:10px;font-weight:700;padding:3px 8px;border-radius:6px;text-transform:uppercase;letter-spacing:.05em;flex-shrink:0;margin-top:1px}
.badge.issue{background:var(--t-blue);color:var(--indigo)} .badge.pr{background:var(--t-mint);color:#0E7E72}
.disc-item .main{flex:1;min-width:0}
.disc-item .ttl{font-size:13px;font-weight:500;line-height:1.4}
.disc-item .meta{font-size:11px;color:var(--mute);margin-top:3px;font-family:var(--mono)}
.disc-item .cmt{font-family:var(--mono);font-weight:700;color:#B45309;font-size:13px;flex-shrink:0}
/* topic bars */
.tlist{display:flex;flex-direction:column;gap:11px}
.trow{display:flex;align-items:center;gap:12px}
.trow .tname{width:180px;font-size:13px;font-weight:500;font-family:var(--mono);color:var(--ink)}
.trow .tbar{flex:1;height:24px;background:#F1F5F9;border-radius:7px;overflow:hidden}
.trow .tbar i{display:block;height:100%;background:linear-gradient(90deg,#06B6D4,#22D3EE);border-radius:7px}
.trow .tval{font-family:var(--mono);font-weight:700;font-size:13px;width:48px;text-align:right}
/* scholars grid */
.sgrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:12px}
.scard{border:1px solid var(--line);border-radius:11px;padding:13px 15px;background:#FCFDFE}
.scard .nm{font-family:var(--mono);font-weight:600;color:var(--ink);font-size:13px}
.scard .ct{font-size:12px;color:var(--mute);margin-top:3px}
.scard.org{border-left:3px solid var(--indigo)}
/* repos network mini-list */
.rlist{display:flex;flex-direction:column;gap:6px}
.rlist .rli{font-family:var(--mono);font-size:11px;color:var(--mute);padding:2px 0}
/* empty / loading / placeholder */
.empty{padding:50px 20px;text-align:center;color:var(--mute);font-size:14px}
.placeholder{margin-top:18px;padding:60px 20px;text-align:center;color:var(--mute);background:var(--card);
border:1px dashed var(--line);border-radius:16px;font-size:14px}
.placeholder b{color:var(--ink2)}
/* skeleton loading */
@keyframes shimmer{0%{background-position:-400px 0}100%{background-position:400px 0}}
.skel{background:linear-gradient(90deg,#F1F5F9 25%,#E6EBF2 50%,#F1F5F9 75%);background-size:800px 100%;animation:shimmer 1.5s infinite;border-radius:8px}
/* responsive */
@media(max-width:760px){
.wrap{padding:16px}
.summary{grid-template-columns:repeat(2,1fr)}
.sgrid{grid-template-columns:repeat(auto-fill,minmax(150px,1fr))}
.row .lbl{min-width:60px;font-size:11px}
}
</style>
</head>
<body>
<div class="wrap">
<div class="hdr">
<a class="back" href="index.html">← 仪表盘</a>
<div class="titles">
<h1>科研热点追踪</h1>
<div class="sub">输入领域/关键词,勾选追踪角度,扫描 GitLink 科研仓库 → 飙升项目 · 活跃讨论 · 主题热度 · 学者团队</div>
</div>
</div>
<!-- control panel -->
<div class="ctl">
<div class="row">
<span class="lbl">领域 / 关键词</span>
<label class="search-input"><span>🔍</span>
<input id="keyword" value="deep learning,机器学习" placeholder='输入研究领域或技术关键词,逗号分隔多个(如 "计算机视觉,NLP,transformer"'>
</label>
<button class="btn-run" id="runBtn">开始追踪 ▶</button>
</div>
<div class="row">
<span class="lbl">示例</span>
<div class="ex-chips" id="exChips">
<span class="ex">深度学习</span><span class="ex">computer vision</span>
<span class="ex">自然语言处理</span><span class="ex">强化学习</span>
<span class="ex">knowledge graph</span><span class="ex">联邦学习</span>
<span class="ex">LLM agent</span><span class="ex">自动驾驶</span>
</div>
</div>
<div class="row">
<span class="lbl">追踪角度</span>
<div class="angles" id="angles">
<span class="ang on" data-a="trending"><span class="dot"></span>🔥 飙升项目</span>
<span class="ang on" data-a="active"><span class="dot"></span>💬 活跃讨论</span>
<span class="ang on" data-a="topics"><span class="dot"></span>📊 热门主题</span>
<span class="ang on" data-a="scholars"><span class="dot"></span>👥 核心学者·团队</span>
</div>
<span class="lbl" style="margin-left:auto">仓库上限</span>
<select class="lim" id="lim">
<option>5</option><option>8</option><option selected>12</option><option>20</option><option>30</option>
</select>
</div>
<div class="status" id="status"><span>就绪 · 点"开始追踪"扫描 GitLink</span></div>
</div>
<!-- summary tiles (hidden until first run) -->
<div class="summary" id="summary" style="display:none"></div>
<!-- results -->
<div id="results">
<div class="placeholder">输入关键词并选择角度,点 <b>开始追踪</b> —— 服务器将通过 gitlink-cli 实时搜索并构建该领域的科研热点全景。</div>
</div>
</div>
<script>
/* ========== 工具函数 ========== */
function esc(s){return String(s||"").replace(/[&<>]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;'}[c]));}
function token(){return localStorage.getItem("demo_token")||"";}
/* ========== angle 切换 + 示例芯片 ========== */
document.querySelectorAll(".ang").forEach(a=>a.onclick=()=>a.classList.toggle("on"));
document.querySelectorAll(".ex").forEach(e=>e.onclick=()=>{
document.getElementById("keyword").value=e.textContent.trim();
});
/* ========== 状态栏 ========== */
function setStatus(html,cls){
const s=document.getElementById("status");
s.innerHTML=html;
s.className="status"+(cls?" "+cls:"");
}
/* ========== 主运行 ========== */
async function run(){
const kw=document.getElementById("keyword").value.trim();
if(!kw){setStatus("请输入关键词","err");return;}
const angles=[...document.querySelectorAll(".ang.on")].map(a=>a.dataset.a);
if(!angles.length){setStatus("请至少选择一个追踪角度","err");return;}
const btn=document.getElementById("runBtn");
btn.disabled=true;
setStatus('<span class="spin"></span> 扫描中:搜索 "'+esc(kw)+'" → 拉取仓库详情 → 批量获取 Issue/PR 讨论…(约 3090s取决于仓库数和 API 响应速度)');
document.getElementById("results").innerHTML="";
document.getElementById("summary").style.display="none";
try{
const r=await fetch("/api/run",{
method:"POST",
headers:{"Content-Type":"application/json","X-Demo-Token":token()},
body:JSON.stringify({scenario:"hotspot",keyword:kw})
});
const j=await r.json();
if(!j.ok){
const errMsg=j.error||j.stdout||"未知错误";
setStatus("✗ 运行失败: "+esc(errMsg)+" · "+(j.duration||""),"err");
btn.disabled=false;
return;
}
// 解析 JSON
let obj=null;
try{
const raw=(j.artifacts&&j.artifacts.json)||(j.stdout||"");
obj=typeof raw==="string"?JSON.parse(raw):raw;
}catch(e){
setStatus("✗ 解析 JSON 产物失败 · 可能是 Python 脚本异常: "+(e.message||""),"err");
btn.disabled=false;
return;
}
if(!obj||typeof obj!=="object"){
setStatus("✗ 返回数据为空 · 请确认目标关键词在 GitLink 上有结果","err");
btn.disabled=false;
return;
}
const meta=obj.meta||{};
setStatus(
"✓ 完成 · "+(j.duration||"")+
" · 仓库 "+meta.repo_count+" 个"+
" · 讨论 "+meta.discussion_count+" 条"+
" · 主题 "+meta.topic_count+" 个"+
" · 学者 "+meta.scholar_count+" 位",
"ok"
);
// 汇总卡片
renderSummary(meta);
// 结果
render(angles,obj);
}catch(e){
setStatus("✗ 请求失败: "+esc(e.message||e),"err");
}finally{
btn.disabled=false;
}
}
/* ========== 汇总卡片 ========== */
function renderSummary(meta){
const el=document.getElementById("summary");
el.style.display="grid";
el.innerHTML=
tile("indigo","扫描仓库",meta.repo_count||0,"个")+
tile("coral","活跃讨论",meta.discussion_count||0,"条")+
tile("cyan","热门主题",meta.topic_count||0,"个")+
tile("mint","核心学者",meta.scholar_count||0,"位");
}
function tile(c,label,val,unit){
const colors={indigo:"var(--indigo)",coral:"var(--coral)",cyan:"#06B6D4",mint:"var(--mint)"};
const bg={indigo:"var(--t-blue)",coral:"var(--t-coral)",cyan:"#E0F7FA",mint:"var(--t-mint)"};
return `<div class="sum-tile" style="border-top:3px solid ${colors[c]||colors.indigo}">
<div class="sl">${label}</div><div class="sv">${val}</div><div class="ss">${unit}</div></div>`;
}
/* ========== 渲染各个角度 ========== */
function render(angles,o){
const box=document.getElementById("results");
let h="";
const maxScore=Math.max(1,...(o.trending_repos||[]).map(r=>r.score||0));
// --- 🔥 飙升项目 ---
if(angles.includes("trending")){
const tr=o.trending_repos||[];
h+=`<div class="sec">
<div class="sec-head">
<div class="ico ic-coral">🔥</div>
<div>
<h2>飙升 / 热门项目</h2>
<span class="eyebrow">Trending repos · 热度 = Star + Fork×2 + 近期更新加成</span>
</div>
<span class="cnt">${tr.length} 个</span>
</div>`;
if(tr.length){
h+=`<table class="tbl"><thead><tr>
<th>仓库</th><th>语言</th><th class="num">★ Star</th><th class="num">⑂ Fork</th><th class="num">日均★</th><th>热度</th><th>更新</th></tr></thead><tbody>`;
tr.forEach(r=>{
const kws=(r.matched_keywords||[]).map(k=>`<span class="kw-chip">${esc(k)}</span>`).join("");
h+=`<tr>
<td><div class="repo">${esc(r.repo)}</div>
${kws?`<div style="margin-top:3px">${kws}</div>`:""}
<div style="font-size:11px;color:var(--mute);margin-top:2px">${esc(r.description||"").slice(0,100)}</div></td>
<td>${r.language?`<span class="lang-pill">${esc(r.language)}</span>`:"—"}</td>
<td class="num">${r.stars}</td>
<td class="num">${r.forks}</td>
<td class="num">${r.velocity}</td>
<td>
<div class="scorebar"><i style="width:${Math.round(r.score/maxScore*100)}%"></i></div>
<div style="font-size:11px;color:var(--mute);margin-top:2px;font-family:var(--mono)">${r.score}</div>
</td>
<td style="font-family:var(--mono);font-size:12px;color:var(--ink2)">${esc(r.updated||"—")}</td>
</tr>`;
});
h+=`</tbody></table>`;
}else{
h+=`<div class="empty">未找到匹配仓库</div>`;
}
h+=`</div>`;
}
// --- 💬 活跃讨论 ---
if(angles.includes("active")){
const ac=o.active_discussions||[];
h+=`<div class="sec">
<div class="sec-head">
<div class="ico ic-amber">💬</div>
<div>
<h2>活跃讨论</h2>
<span class="eyebrow">Active issues / PRs · 按评论数排序 · 来自所有扫描仓库</span>
</div>
<span class="cnt">${ac.length} 条</span>
</div>`;
if(ac.length){
h+=`<div class="disc">`;
ac.slice(0,30).forEach(a=>{
h+=`<div class="disc-item">
<span class="badge ${a.type}">${a.type==="pr"?"PR":"Issue"}</span>
<div class="main">
<div class="ttl">${esc(a.title||"(无标题)")}</div>
<div class="meta">${esc(a.repo)} · #${esc(a.number||"")} · ${esc(a.state||"")}</div>
</div>
<span class="cmt">${a.comments} 💬</span>
</div>`;
});
h+=`</div>`;
}else{
h+=`<div class="empty">该领域暂无活跃讨论(所有仓库的开放 Issue/PR 评论数均为 0</div>`;
}
h+=`</div>`;
}
// --- 📊 热门主题 ---
if(angles.includes("topics")){
const tp=o.topic_heat||[];
const maxT=Math.max(1,...tp.map(t=>t.count));
h+=`<div class="sec">
<div class="sec-head">
<div class="ico ic-cyan">📊</div>
<div>
<h2>热门主题</h2>
<span class="eyebrow">Topic heat · 在仓库描述/README 中命中的主题词频</span>
</div>
<span class="cnt">${tp.length} 个</span>
</div>`;
if(tp.length){
h+=`<div class="tlist">`;
tp.forEach(t=>{
h+=`<div class="trow">
<span class="tname">${esc(t.topic)}</span>
<div class="tbar"><i style="width:${Math.round(t.count/maxT*100)}%"></i></div>
<span class="tval">${t.count}</span>
</div>`;
});
h+=`</div>`;
}else{
h+=`<div class="empty">未识别到明确主题(仓库描述/README 不包含已知科研关键词)</div>`;
}
h+=`</div>`;
}
// --- 👥 核心学者·团队 ---
if(angles.includes("scholars")){
const sc=o.core_scholars||[];
const tm=o.core_teams||[];
h+=`<div class="sec">
<div class="sec-head">
<div class="ico ic-mint">👥</div>
<div>
<h2>核心学者 · 团队</h2>
<span class="eyebrow">Key scholars & teams · 按关联仓库数排列</span>
</div>
<span class="cnt">学者 ${(sc||[]).length} · 团队 ${(tm||[]).length}</span>
</div>`;
// 学者
if(sc.length){
h+=`<div style="margin-bottom:14px"><div style="font-size:12px;font-weight:600;color:var(--ink2);margin-bottom:8px">🧑 学者</div>`;
h+=`<div class="sgrid">`;
sc.slice(0,12).forEach(s=>{
const repoNames=(s.repos||[]).slice(0,5);
h+=`<div class="scard">
<div class="nm">${esc(s.login)}</div>
<div class="ct">关联 ${s.repo_count} 个仓库</div>
${repoNames.length?`<div class="rlist">${repoNames.map(rn=>`<span class="rli">${esc(rn)}</span>`).join("")}</div>`:""}
</div>`;
});
h+=`</div></div>`;
}
// 团队
if(tm.length){
h+=`<div><div style="font-size:12px;font-weight:600;color:var(--ink2);margin-bottom:8px">🏛 活跃组织/团队</div>`;
h+=`<div class="sgrid">`;
tm.slice(0,8).forEach(t=>{
h+=`<div class="scard org">
<div class="nm">${esc(t.login)}</div>
<div class="ct">${t.repo_count} 个仓库</div>
</div>`;
});
h+=`</div></div>`;
}
if(!sc.length&&!tm.length){
h+=`<div class="empty">未识别到学者/团队(仓库无贡献者数据)</div>`;
}
h+=`</div>`;
}
box.innerHTML=h||'<div class="empty" style="padding:70px 20px">无匹配角度的数据 —— 请选择至少一个追踪角度</div>';
}
/* ========== 事件绑定 ========== */
document.getElementById("runBtn").onclick=run;
document.getElementById("keyword").addEventListener("keydown",e=>{if(e.key==="Enter")run();});
/* ========== 页面加载时尝试载入上次的 hotspot 结果 ========== */
(async function init(){
try{
const r=await fetch("/api/result/hotspot",{headers:{"X-Demo-Token":token()}});
const j=await r.json();
if(j.ok&&j.artifacts&&j.artifacts.json){
const obj=JSON.parse(j.artifacts.json);
if(obj&&obj.trending_repos){
const angles=[...document.querySelectorAll(".ang.on")].map(a=>a.dataset.a);
const meta=obj.meta||{};
renderSummary(meta);
render(angles,obj);
setStatus("✓ 已载入上次扫描 · 点"开始追踪"用新关键词重新扫描","ok");
}
}
}catch(e){}
})();
</script>
</body>
</html>