async function fetchJSON(url, options) { const res = await fetch(url, options); if (!res.ok) throw new Error(await res.text()); return await res.json(); } function renderMetrics(metrics) { const root = document.getElementById('metrics'); root.innerHTML = ''; Object.entries(metrics || {}).forEach(([key, value]) => { const card = document.createElement('div'); card.className = 'metric'; card.innerHTML = `
${key}
${value}
`; root.appendChild(card); }); } function labelBadgeClass(label) { if (!label || label === 'Unknown') return 'badge-unknown'; return 'label-' + String(label).toLowerCase().replace(/\s+/g, '-'); } function renderResults(results, rootId='results', showRank=true) { const mount = document.getElementById(rootId); mount.innerHTML = ''; (results || []).forEach(item => { const label = item.label || 'Unknown'; const box = document.createElement('div'); box.className = 'result'; box.innerHTML = `
${label}
${showRank ? `#${item.rank || '-'}` : (item.rerank_score != null ? `rerank=${item.rerank_score.toFixed ? item.rerank_score.toFixed(4) : item.rerank_score}` : 'not recalled')}
${item.title || ''}
${item.title_zh ? `
${item.title_zh}
` : ''}
${(item.option_values || [])[0] || ''}
${(item.option_values || [])[1] || ''}
${(item.option_values || [])[2] || ''}
`; mount.appendChild(box); }); if (!(results || []).length) { mount.innerHTML = '
None.
'; } } function renderTips(data) { const root = document.getElementById('tips'); const tips = [...(data.tips || [])]; const stats = data.label_stats || {}; tips.unshift(`Cached labels for query: ${stats.total || 0}. Recalled hits: ${stats.recalled_hits || 0}. Missed (non-irrelevant): ${stats.missing_relevant_count || 0} — Exact: ${stats.missing_exact_count || 0}, High: ${stats.missing_high_count || 0}, Low: ${stats.missing_low_count || 0}.`); root.innerHTML = tips.map(text => `
${text}
`).join(''); } async function loadQueries() { const data = await fetchJSON('/api/queries'); const root = document.getElementById('queryList'); root.innerHTML = ''; data.queries.forEach(query => { const btn = document.createElement('button'); btn.className = 'query-item'; btn.textContent = query; btn.onclick = () => { document.getElementById('queryInput').value = query; runSingle(); }; root.appendChild(btn); }); } function fmtMetric(m, key, digits) { const v = m && m[key]; if (v == null || Number.isNaN(Number(v))) return null; const n = Number(v); return n.toFixed(digits); } function historySummaryHtml(meta) { const m = meta && meta.aggregate_metrics; const nq = (meta && meta.queries && meta.queries.length) || (meta && meta.per_query && meta.per_query.length) || null; const parts = []; if (nq != null) parts.push(`Queries ${nq}`); const p10 = fmtMetric(m, 'P@10', 3); const p52 = fmtMetric(m, 'P@5_2_3', 3); const map3 = fmtMetric(m, 'MAP_3', 3); if (p10) parts.push(`P@10 ${p10}`); if (p52) parts.push(`P@5_2_3 ${p52}`); if (map3) parts.push(`MAP_3 ${map3}`); if (!parts.length) return ''; return `
${parts.join(' · ')}
`; } async function loadHistory() { const data = await fetchJSON('/api/history'); const root = document.getElementById('history'); root.classList.remove('muted'); const items = data.history || []; if (!items.length) { root.innerHTML = 'No history yet.'; return; } root.innerHTML = `
`; const list = root.querySelector('.history-list'); items.forEach(item => { const btn = document.createElement('button'); btn.type = 'button'; btn.className = 'history-item'; btn.setAttribute('aria-label', `Open report ${item.batch_id}`); const sum = historySummaryHtml(item.metadata); btn.innerHTML = `
${item.batch_id}
${item.created_at} · tenant ${item.tenant_id}
${sum}`; btn.onclick = () => openBatchReport(item.batch_id); list.appendChild(btn); }); } let _lastReportPath = ''; function closeReportModal() { const el = document.getElementById('reportModal'); el.classList.remove('is-open'); el.setAttribute('aria-hidden', 'true'); document.getElementById('reportModalBody').innerHTML = ''; document.getElementById('reportModalMeta').textContent = ''; } async function openBatchReport(batchId) { const el = document.getElementById('reportModal'); const body = document.getElementById('reportModalBody'); const metaEl = document.getElementById('reportModalMeta'); const titleEl = document.getElementById('reportModalTitle'); el.classList.add('is-open'); el.setAttribute('aria-hidden', 'false'); titleEl.textContent = batchId; metaEl.textContent = ''; body.className = 'report-modal-body batch-report-md report-modal-loading'; body.textContent = 'Loading report…'; try { const rep = await fetchJSON('/api/history/' + encodeURIComponent(batchId) + '/report'); _lastReportPath = rep.report_markdown_path || ''; metaEl.textContent = rep.report_markdown_path || ''; const raw = marked.parse(rep.markdown || '', { gfm: true }); const safe = DOMPurify.sanitize(raw, { USE_PROFILES: { html: true } }); body.className = 'report-modal-body batch-report-md'; body.innerHTML = safe; } catch (e) { body.className = 'report-modal-body report-modal-error'; body.textContent = (e && e.message) ? e.message : String(e); } } document.getElementById('reportModal').addEventListener('click', (ev) => { if (ev.target && ev.target.getAttribute('data-close-report') === '1') closeReportModal(); }); document.addEventListener('keydown', (ev) => { if (ev.key === 'Escape') closeReportModal(); }); document.getElementById('reportCopyPath').addEventListener('click', async () => { if (!_lastReportPath) return; try { await navigator.clipboard.writeText(_lastReportPath); } catch (_) {} }); async function runSingle() { const query = document.getElementById('queryInput').value.trim(); if (!query) return; document.getElementById('status').textContent = `Evaluating "${query}"...`; const data = await fetchJSON('/api/search-eval', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({query, top_k: 100, auto_annotate: false}) }); document.getElementById('status').textContent = `Done. total=${data.total}`; renderMetrics(data.metrics); renderResults(data.results, 'results', true); renderResults(data.missing_relevant, 'missingRelevant', false); renderTips(data); loadHistory(); } async function runBatch() { document.getElementById('status').textContent = 'Running batch evaluation...'; const data = await fetchJSON('/api/batch-eval', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({top_k: 100, auto_annotate: false}) }); document.getElementById('status').textContent = `Batch done. report=${data.batch_id}`; renderMetrics(data.aggregate_metrics); renderResults([], 'results', true); renderResults([], 'missingRelevant', false); document.getElementById('tips').innerHTML = '
Batch evaluation uses cached labels only unless force refresh is requested via CLI/API.
'; loadHistory(); } loadQueries(); loadHistory();