'use strict'; const $ = (id) => document.getElementById(id); let pollTimer = null; // ---------- 启动:轮询索引状态(服务器开机自动建索引) ---------- window.addEventListener('DOMContentLoaded', pollStatus); function pollStatus() { clearInterval(pollTimer); tick(); pollTimer = setInterval(tick, 800); } async function tick() { let s; try { s = await (await fetch('/api/status')).json(); } catch { $('status').textContent = '连接服务失败,确认服务器已启动。'; return; } // BM25 就绪即可搜(向量还在编码也能先看关键词路) if (s.bm25_ready) { $('q').disabled = false; $('search').disabled = false; } const bar = $('buildBar'); if (s.state === 'building') { bar.classList.remove('hidden'); if (s.phase === 'loadmodel') { $('status').textContent = `图片 ${s.n_images} 张 / 名称 ${s.total} 个 · 正在加载向量模型…`; setBar('加载本地向量模型 BGE-M3(首次需下载约 2.3G)…', 8); } else if (s.phase === 'embedding') { const pct = s.emb_total ? Math.round((s.emb_done / s.emb_total) * 100) : 0; const dev = s.device ? ` · ${s.device}` : ''; $('status').textContent = `图片 ${s.n_images} 张 / 名称 ${s.total} 个 · 向量编码中(BM25 已就绪,可先搜)`; setBar(`向量编码中${dev} ${s.emb_done}/${s.emb_total}`, pct); } else { $('status').textContent = 'BM25 分词建索引中…'; setBar('BM25 分词建索引中…', 4); } } else if (s.state === 'ready') { clearInterval(pollTimer); const dev = s.device ? ` (${s.device})` : ''; const note = s.failed ? `,${s.failed} 条失败` : ''; $('status').innerHTML = `✓ 就绪:${s.total} 个名称 / ${s.n_images} 张图${dev}${note} —— 直接搜索吧。`; bar.classList.add('hidden'); } else if (s.state === 'error') { clearInterval(pollTimer); $('status').textContent = '❌ ' + (s.message || '建索引失败'); bar.classList.add('hidden'); } } function setBar(t, pct) { $('buildText').textContent = t; $('barFill').style.width = (pct || 0) + '%'; } // ---------- 搜索 ---------- $('search').addEventListener('click', doSearch); $('q').addEventListener('keydown', (e) => { if (e.key === 'Enter') doSearch(); }); async function doSearch() { const q = $('q').value.trim(); if (!q) return; const params = new URLSearchParams({ q, topk: $('topk').value || 20, rrf_k: $('rrfk').value || 60, w_bm25: $('wbm').value || 1, w_vec: $('wvec').value || 1, }); let d; try { d = await (await fetch('/api/search?' + params)).json(); } catch (err) { alert('搜索失败:' + err.message); return; } renderCol('bm25', d.bm25); renderCol('vector', d.vector); renderCol('fusion', d.fusion); } function renderCol(kind, items) { $('cnt-' + kind).textContent = items.length ? `共 ${items.length}` : ''; const box = $('col-' + kind); box.innerHTML = ''; if (!items.length) { box.innerHTML = '
无结果
'; return; } items.forEach((it, i) => box.appendChild(card(kind, it, i + 1))); } function card(kind, it, rank) { const el = document.createElement('div'); el.className = 'card'; const primary = kind === 'bm25' ? `BM25 ${fmt(it.bm25)}` : kind === 'vector' ? `余弦 ${fmt(it.vec)}` : `RRF ${fmt(it.score)}`; const badges = []; if (kind !== 'bm25') badges.push(rankBadge('BM25', it.bm25_rank)); if (kind !== 'vector') badges.push(rankBadge('向量', it.vec_rank)); const src = imgURL(it.files && it.files[0]); el.innerHTML = `
${rank}
${src ? `` : '
'}
${esc(it.name)}
${primary}
${badges.join('')}
`; return el; } function rankBadge(label, rank) { return rank ? `${label}#${rank}` : `${label}未命中`; } function imgURL(file) { return file ? '/images/' + encodeURIComponent(file) : ''; } const fmt = (v) => (v === null || v === undefined ? '—' : v); const esc = (s) => String(s).replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]));