forked from Gitlink/gitlink-cli
486 lines
22 KiB
HTML
486 lines
22 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">
|
||
<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=>({'&':'&','<':'<','>':'>'}[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 讨论…(约 30–90s,取决于仓库数和 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>
|