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();