feat: upgrade search to live dropdown with paper-level results + debounce

This commit is contained in:
2026-06-09 15:52:47 +00:00
parent 3d869fd1fd
commit 7bbc0f912f
3 changed files with 115 additions and 9 deletions

View File

@@ -35,9 +35,15 @@ async function init() {
document.addEventListener('keydown', e => {
if (e.key === 'Escape') {
if ($('#pdfOverlay')?.classList.contains('open')) closePdf();
else if ($('#searchResults')?.style.display === 'block') closeSearchResults();
else if ($('#overlay').classList.contains('open')) closeModal();
}
});
// Close search results on outside click
document.addEventListener('click', e => {
if (!e.target.closest('.search-wrap')) closeSearchResults();
});
}
// ═══════════════ STATUS BAR ════════════════════════════
@@ -260,16 +266,97 @@ function closePdf() {
}
// ═══════════════ SEARCH ═══════════════════════════════
let searchTimer = null;
function searchPapers(q) {
q = (q||'').toLowerCase().trim();
if (q.length<2) { $$('.card').forEach(c=>c.style.outline=''); return; }
fetch(`${API}/papers?q=${encodeURIComponent(q)}&limit=10`).then(r=>r.json()).then(ps=>{
const matched = new Set(ps.map(p=>p.module_id));
$$('.card').forEach(c=>{
const m=[...c.classList].find(x=>x.startsWith('card-'))?.replace('card-','');
c.style.outline=matched.has(m)?'2px solid var(--green)':'';
q = (q||'').trim();
if (q.length < 2) {
closeSearchResults();
$$('.card').forEach(c => c.style.outline = '');
return;
}
clearTimeout(searchTimer);
searchTimer = setTimeout(() => {
fetch(`${API}/papers?q=${encodeURIComponent(q)}&limit=15`)
.then(r => r.json())
.then(ps => {
// Highlight matching module cards
const matched = new Set(ps.map(p => p.module_id));
$$('.card').forEach(c => {
const m = [...c.classList].find(x => x.startsWith('card-'))?.replace('card-', '');
c.style.outline = matched.has(m) ? '2px solid var(--green)' : '';
});
// Show paper-level results dropdown
renderSearchResults(ps, q);
});
}, 200); // 200ms debounce
}
function renderSearchResults(papers, query) {
let panel = $('#searchResults');
if (!panel) {
panel = document.createElement('div');
panel.id = 'searchResults';
panel.className = 'search-results';
$('.search-wrap').appendChild(panel);
panel.addEventListener('click', e => {
const item = e.target.closest('.search-result-item');
if (item && item.dataset.mid) {
closeSearchResults();
openModule(item.dataset.mid);
// After modal opens, switch to the right tab
setTimeout(() => {
const areaIdx = parseInt(item.dataset.aidx || '0');
switchArea(areaIdx);
}, 300);
}
});
}
if (papers.length === 0) {
panel.innerHTML = '<div class="search-result-empty">未找到匹配论文</div>';
panel.style.display = 'block';
return;
}
// Group by module for clean display
const modColors = {};
$$('.card').forEach(c => {
const m = [...c.classList].find(x => x.startsWith('card-'))?.replace('card-', '');
if (m) modColors[m] = `var(--${m === 'arch' ? 'blue' : m === 'multi' ? 'cyan' : m === 'data' ? 'green' : m === 'pretrain' ? 'red' : m === 'post' ? 'purple' : m === 'compress' ? 'orange' : m === 'deploy' ? 'teal' : m === 'agent' ? 'pink' : 'yellow'})`;
});
panel.innerHTML = papers.map(p => {
const mid = p.module_id;
const areaIdx = moduleData[mid]?.areas?.findIndex(a => a.id === p.area_id) ?? 0;
const color = modColors[mid] || 'var(--blue)';
const tags = (p.tags || []).map(t => `<span class="paper-tag ${TAG_CLASS[t] || 'tag-branch'}" style="font-size:0.65em">${t}</span>`).join('');
return `<div class="search-result-item" data-mid="${mid}" data-aidx="${areaIdx}">
<div class="search-result-title">${highlightMatch(p.title, query)}</div>
<div class="search-result-meta">
<span style="color:${color};font-weight:600">${p.module_name} / ${p.area_name}</span>
<span>${p.year || ''}</span>
${tags}
</div>
</div>`;
}).join('') + `<div class="search-result-footer">共 ${papers.length} 篇匹配 · 点击跳转</div>`;
panel.style.display = 'block';
}
function highlightMatch(text, query) {
const idx = text.toLowerCase().indexOf(query.toLowerCase());
if (idx < 0) return text;
const before = text.slice(0, idx);
const match = text.slice(idx, idx + query.length);
const after = text.slice(idx + query.length);
return `${before}<mark>${match}</mark>${after}`;
}
function closeSearchResults() {
const panel = $('#searchResults');
if (panel) { panel.style.display = 'none'; panel.innerHTML = ''; }
}
// ═══════════════ EVENTS ═══════════════════════════════