eval_web.js 8.15 KB
    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 = `<div class="label">${key}</div><div class="value">${value}</div>`;
        root.appendChild(card);
      });
    }
    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 = `
          <div><span class="badge ${label}">${label}</span><div class="muted" style="margin-top:8px">${showRank ? `#${item.rank || '-'}` : (item.rerank_score != null ? `rerank=${item.rerank_score.toFixed ? item.rerank_score.toFixed(4) : item.rerank_score}` : 'not recalled')}</div></div>
          <img class="thumb" src="${item.image_url || ''}" alt="" />
          <div>
            <div class="title">${item.title || ''}</div>
            ${item.title_zh ? `<div class="title-zh">${item.title_zh}</div>` : ''}
            <div class="options">
              <div>${(item.option_values || [])[0] || ''}</div>
              <div>${(item.option_values || [])[1] || ''}</div>
              <div>${(item.option_values || [])[2] || ''}</div>
            </div>
          </div>`;
        mount.appendChild(box);
      });
      if (!(results || []).length) {
        mount.innerHTML = '<div class="muted">None.</div>';
      }
    }
    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 Exact: ${stats.missing_exact_count || 0}. Missed Partial: ${stats.missing_partial_count || 0}.`);
      root.innerHTML = tips.map(text => `<div class="tip">${text}</div>`).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(`<span>Queries</span> ${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(`<span>P@10</span> ${p10}`);
      if (p52) parts.push(`<span>P@5_2_3</span> ${p52}`);
      if (map3) parts.push(`<span>MAP_3</span> ${map3}`);
      if (!parts.length) return '';
      return `<div class="hstats">${parts.join(' · ')}</div>`;
    }
    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 = '<span class="muted">No history yet.</span>';
        return;
      }
      root.innerHTML = `<div class="history-list"></div>`;
      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 = `<div class="hid">${item.batch_id}</div>
          <div class="hmeta">${item.created_at} · tenant ${item.tenant_id}</div>${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 = '<div class="tip">Batch evaluation uses cached labels only unless force refresh is requested via CLI/API.</div>';
      loadHistory();
    }
    loadQueries();
    loadHistory();