spreadsheetlabeller / static /rag_eval.html
zofiasmolenasana
Auth: Set-Cookie session + credentials include (HF strips Authorization on GET)
3195abb unverified
Raw
History Blame Contribute Delete
35.3 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>RAG Evaluation — Unlabelled Sheets</title>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:'Segoe UI',system-ui,-apple-system,sans-serif;background:#f5f5f5;color:#222;display:flex;height:100vh;overflow:hidden}
/* Sidebar */
.sidebar{width:340px;min-width:280px;background:#fff;border-right:1px solid #ddd;display:flex;flex-direction:column;overflow:hidden}
.sidebar-header{padding:16px 18px 12px;border-bottom:1px solid #eee}
.sidebar-header h1{font-size:17px;font-weight:700;margin-bottom:4px}
.sidebar-header p{font-size:12px;color:#888}
.sidebar-search{padding:8px 14px;border-bottom:1px solid #eee}
.sidebar-search input{width:100%;padding:6px 10px;border:1px solid #ccc;border-radius:6px;font-size:13px}
.sidebar-filter{padding:6px 14px;border-bottom:1px solid #eee;display:flex;gap:6px;align-items:center}
.sidebar-filter label{font-size:11px;color:#666;display:flex;align-items:center;gap:3px;cursor:pointer}
.sheet-list{flex:1;overflow-y:auto}
/* Workbook group */
.wb-group{border-bottom:1px solid #e8e8e8}
.wb-header{padding:8px 14px;cursor:pointer;transition:background .1s;display:flex;align-items:center;gap:8px;user-select:none}
.wb-header:hover{background:#f8f9ff}
.wb-header .wb-arrow{font-size:10px;color:#999;transition:transform .15s;width:12px;flex-shrink:0}
.wb-header.open .wb-arrow{transform:rotate(90deg)}
.wb-header .wb-name{font-size:13px;font-weight:600;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.wb-header .wb-badges{display:flex;gap:4px;flex-shrink:0;align-items:center}
.wb-header .badge{font-size:9px;font-weight:700;padding:1px 5px;border-radius:8px;color:#fff}
.wb-header .wb-tabs-count{font-size:10px;color:#999;font-weight:500}
.wb-holdout{width:14px;height:14px;cursor:pointer;flex-shrink:0}
.wb-tabs{display:none;background:#fafafa;border-top:1px solid #f0f0f0}
.wb-group.open .wb-tabs{display:block}
.tab-item{padding:6px 14px 6px 34px;cursor:pointer;transition:background .1s;display:flex;align-items:center;gap:8px;border-bottom:1px solid #f0f0f0}
.tab-item:last-child{border-bottom:none}
.tab-item:hover{background:#eff6ff}
.tab-item.active{background:#dbeafe;border-left:3px solid #2563eb;padding-left:31px}
.tab-item .tab-name{font-size:12px;font-weight:500;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.tab-item .tab-meta{font-size:10px;color:#999}
.badge-qa{background:#2563eb}
.badge-holdout{background:#f59e0b}
.badge-rag{background:#059669}
/* Main area */
.main{flex:1;display:flex;flex-direction:column;overflow:hidden}
.main-header{padding:12px 20px;background:#fff;border-bottom:1px solid #ddd;display:flex;align-items:center;gap:14px;flex-wrap:wrap;box-shadow:0 1px 3px rgba(0,0,0,.06)}
.main-header h2{font-size:16px;font-weight:600;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.main-header .subtitle{font-size:12px;color:#888;font-weight:400;margin-left:6px}
.holdout-toggle{display:flex;align-items:center;gap:6px;font-size:13px;font-weight:500}
.holdout-toggle input[type=checkbox]{width:16px;height:16px;cursor:pointer}
.nav-links{display:flex;gap:8px}
.nav-links a{font-size:12px;color:#2563eb;text-decoration:none;padding:4px 10px;border:1px solid #ddd;border-radius:5px}
.nav-links a:hover{background:#eff6ff}
.tab-bar{display:flex;border-bottom:1px solid #ddd;background:#fff}
.tab-btn{padding:8px 20px;font-size:13px;font-weight:600;border:none;background:none;cursor:pointer;
border-bottom:2px solid transparent;color:#888;transition:all .15s}
.tab-btn.active{color:#2563eb;border-bottom-color:#2563eb}
.tab-btn:hover{color:#333}
/* Grid */
.grid-wrap{flex:1;overflow:auto;padding:12px 20px}
.grid-wrap table{border-collapse:collapse;background:#fff;font-size:12px}
.grid-wrap td{border:1px solid #d0d0d0;padding:3px 5px;min-width:40px;max-width:250px;
overflow:hidden;text-overflow:ellipsis;white-space:nowrap;cursor:cell;user-select:none;position:relative}
.grid-wrap td.source-selected{outline:3px solid #f59e0b;outline-offset:-2px;z-index:1;background:#fef3c7 !important}
.grid-wrap td.label-value{background:#c6efce}
.grid-wrap td.label-attribute{background:#a8e6cf}
.grid-wrap td.label-aggregation{background:#d5a6e6}
.grid-wrap td.label-metadata{background:#bdd7ee}
.grid-wrap td.label-comment{background:#fde68a}
.grid-wrap td.label-empty{background:#f5f5f5;color:#bbb}
.grid-wrap td.label-junk{background:#f8d7da;color:#aaa;text-decoration:line-through}
.grid-wrap td.label-col_header{background:#ffd6a5}
.grid-wrap td.label-row_header{background:#ffadad}
.grid-wrap td .tid{position:absolute;top:1px;right:1px;font-size:7px;font-weight:700;color:#fff;
padding:0 3px;border-radius:3px;line-height:12px;opacity:.85;pointer-events:none}
/* QA panel */
.qa-panel{background:#fff;border-top:1px solid #ddd;max-height:50vh;overflow-y:auto}
.qa-form{padding:14px 20px;border-bottom:1px solid #eee;display:flex;gap:10px;align-items:flex-end;flex-wrap:wrap}
.qa-form .field{display:flex;flex-direction:column;gap:3px}
.qa-form .field label{font-size:11px;font-weight:600;color:#555}
.qa-form .field input,.qa-form .field textarea{padding:6px 10px;border:1px solid #ccc;border-radius:6px;font-size:13px;font-family:inherit}
.qa-form .field input{width:280px}
.qa-form .field textarea{width:280px;height:50px;resize:vertical}
.source-display{font-size:12px;color:#333;padding:6px 10px;border:1px solid #f59e0b;border-radius:6px;
background:#fffbeb;min-width:120px;min-height:32px}
.source-display .src-cell{display:inline-block;padding:1px 6px;margin:1px 3px;background:#fef3c7;
border:1px solid #f59e0b;border-radius:4px;font-size:11px;font-weight:600;cursor:pointer}
.source-display .src-cell:hover{background:#fde68a}
.qa-form button{padding:8px 20px;font-size:13px;font-weight:600;border:none;border-radius:6px;cursor:pointer;transition:background .15s}
.qa-form .btn-add{background:#2563eb;color:#fff}
.qa-form .btn-add:hover{background:#1d4ed8}
.qa-form .btn-add:disabled{background:#94a3b8;cursor:not-allowed}
.qa-form .btn-clear{background:#fff;color:#888;border:1px solid #ccc}
.qa-form .btn-clear:hover{background:#f5f5f5}
.qa-list{padding:0 20px 12px}
.qa-list h3{font-size:13px;font-weight:600;color:#555;padding:10px 0 6px}
.qa-entry{padding:8px 12px;border:1px solid #eee;border-radius:8px;margin-bottom:6px;display:flex;gap:12px;align-items:flex-start}
.qa-entry .qa-body{flex:1;font-size:12px}
.qa-entry .qa-q{font-weight:600;color:#222}
.qa-entry .qa-a{color:#555;margin-top:2px}
.qa-entry .qa-src{color:#b45309;margin-top:2px;font-size:11px}
.qa-entry .qa-actions{display:flex;gap:4px;flex-shrink:0}
.qa-entry .qa-actions button{padding:3px 8px;font-size:11px;border:1px solid #ccc;border-radius:4px;
background:#fff;cursor:pointer;transition:all .1s}
.qa-entry .qa-actions .btn-del{color:#dc2626;border-color:#fca5a5}
.qa-entry .qa-actions .btn-del:hover{background:#fef2f2}
.qa-entry .qa-badges{display:flex;gap:4px;margin-top:3px}
.diff-badge{display:inline-flex;align-items:center;gap:2px;font-size:10px;font-weight:600;padding:1px 6px;
border-radius:4px;white-space:nowrap}
.diff-badge.qd{background:#ede9fe;color:#6d28d9}
.diff-badge.sc{background:#ecfdf5;color:#047857}
.rating-group{display:flex;flex-direction:column;gap:3px}
.rating-group .rating-label{font-size:11px;font-weight:600;color:#555}
.rating-btns{display:flex;gap:0}
.rating-btns button{padding:4px 10px;font-size:12px;font-weight:600;border:1px solid #ccc;background:#fff;
cursor:pointer;transition:all .1s;color:#555;min-width:28px}
.rating-btns button:first-child{border-radius:5px 0 0 5px}
.rating-btns button:last-child{border-radius:0 5px 5px 0}
.rating-btns button:not(:first-child){border-left:none}
.rating-btns button.active{background:#6d28d9;color:#fff;border-color:#6d28d9}
.rating-btns.sc-btns button.active{background:#047857;color:#fff;border-color:#047857}
.rating-btns button:hover:not(.active){background:#f5f3ff}
.rating-btns.sc-btns button:hover:not(.active){background:#ecfdf5}
.rating-hint{font-size:10px;color:#999;max-width:180px;line-height:1.3;margin-top:1px}
.empty-state{text-align:center;padding:60px 20px;color:#888;font-size:15px}
/* Results tab */
.results-wrap{flex:1;overflow:auto;padding:20px}
.results-table{width:100%;border-collapse:collapse;font-size:13px;background:#fff;border-radius:8px;overflow:hidden}
.results-table th,.results-table td{border:1px solid #e0e0e0;padding:8px 12px;text-align:left}
.results-table th{background:#f5f5f5;font-weight:600;font-size:12px;color:#555}
.results-table td{font-variant-numeric:tabular-nums}
.results-table tr:hover td{background:#f8f9ff}
.run-bar{padding:12px 20px;background:#fff;border-bottom:1px solid #ddd;display:flex;gap:10px;align-items:center}
.run-bar button{padding:8px 20px;font-size:13px;font-weight:600;background:#059669;color:#fff;
border:none;border-radius:6px;cursor:pointer}
.run-bar button:hover{background:#047857}
.run-bar button:disabled{background:#94a3b8;cursor:not-allowed}
.run-bar .run-status{font-size:12px;color:#555;flex:1}
.run-bar select{padding:6px 10px;border:1px solid #ccc;border-radius:6px;font-size:12px}
.run-log{padding:10px 20px;background:#f8f8f8;font-size:11px;color:#555;max-height:120px;overflow-y:auto;border-bottom:1px solid #eee;font-family:monospace;white-space:pre-wrap}
/* Login */
.login-overlay{position:fixed;inset:0;z-index:999;background:#f5f5f5;display:flex;align-items:center;justify-content:center}
.login-box{background:#fff;padding:40px 36px;border-radius:12px;box-shadow:0 4px 24px rgba(0,0,0,.12);text-align:center;max-width:360px;width:100%}
.login-box h2{margin-bottom:8px;font-size:20px}
.login-box p{color:#666;font-size:13px;margin-bottom:20px}
.login-box input{width:100%;padding:10px 14px;border:1px solid #ccc;border-radius:8px;font-size:15px;margin-bottom:12px}
.login-box button{width:100%;padding:10px;font-size:15px;font-weight:600;background:#2563eb;color:#fff;border:none;border-radius:8px;cursor:pointer}
.login-box button:hover{background:#1d4ed8}
.login-error{color:#dc2626;font-size:13px;margin-top:8px;display:none}
</style>
</head>
<body>
<div class="login-overlay" id="login-overlay">
<div class="login-box">
<h2>RAG Evaluation</h2>
<p>Enter password to continue</p>
<input type="password" id="login-pw" placeholder="Password" onkeydown="if(event.key==='Enter')doLogin()">
<button onclick="doLogin()">Sign in</button>
<div class="login-error" id="login-error">Wrong password</div>
</div>
</div>
<div class="sidebar">
<div class="sidebar-header">
<h1>RAG Eval — Unlabelled</h1>
<p id="sidebar-stats">Loading...</p>
</div>
<div class="sidebar-search">
<input type="text" id="search-input" placeholder="Search spreadsheets..." oninput="filterSheets()">
</div>
<div class="sidebar-filter">
<label><input type="checkbox" id="filter-holdout" onchange="filterSheets()"> Reserved for RAG</label>
<label><input type="checkbox" id="filter-with-qa" onchange="filterSheets()"> With Q&As</label>
</div>
<div class="sheet-list" id="sheet-list"></div>
</div>
<div class="main">
<div class="main-header">
<h2 id="main-title">Select a spreadsheet from the left panel</h2>
<div class="holdout-toggle" id="holdout-area" style="display:none">
<input type="checkbox" id="holdout-cb" onchange="toggleHoldout()">
<label for="holdout-cb">Reserve workbook for RAG eval (block labeling)</label>
</div>
<div class="nav-links">
<a href="/">Labeler</a>
<a href="/train">Training</a>
</div>
</div>
<div class="tab-bar">
<button class="tab-btn active" data-tab="annotate" onclick="switchTab('annotate')">Annotate</button>
<button class="tab-btn" data-tab="results" onclick="switchTab('results')">Results</button>
</div>
<div id="tab-annotate" style="display:flex;flex-direction:column;flex:1;overflow:hidden">
<div class="grid-wrap" id="grid-wrap">
<div class="empty-state" id="grid-empty">Select a spreadsheet and tab to view the grid and create Q&A pairs</div>
</div>
<div class="qa-panel" id="qa-panel" style="display:none">
<div class="qa-form">
<div class="field">
<label>Question</label>
<textarea id="qa-question" placeholder="e.g. What was the total revenue in Q3 2024?"></textarea>
</div>
<div class="field">
<label>Expected Answer</label>
<input type="text" id="qa-answer" placeholder="e.g. $1.2M">
</div>
<div class="field">
<label>Source Cell(s) <span style="font-weight:400;color:#999">(click grid)</span></label>
<div class="source-display" id="source-display">No cells selected</div>
</div>
<div class="rating-group">
<span class="rating-label">Query difficulty</span>
<div class="rating-btns qd-btns" id="qd-btns">
<button type="button" data-val="1" onclick="setRating('qd',1)" title="Single lookup: one value retrieved directly">1</button>
<button type="button" data-val="2" onclick="setRating('qd',2)" title="Multi-lookup: several values retrieved, no reasoning">2</button>
<button type="button" data-val="3" onclick="setRating('qd',3)" title="Reasoning: multiple values retrieved and reasoned over">3</button>
</div>
<div class="rating-hint" id="qd-hint"></div>
</div>
<div class="rating-group">
<span class="rating-label">Sheet complexity</span>
<div class="rating-btns sc-btns" id="sc-btns">
<button type="button" data-val="1" onclick="setRating('sc',1)" title="Simple: single table, clear headers, flat layout">1</button>
<button type="button" data-val="2" onclick="setRating('sc',2)" title="Moderate: multiple sections, merged cells, or multi-level headers">2</button>
<button type="button" data-val="3" onclick="setRating('sc',3)" title="Complex: multiple tables, nested layout, heavy merges or formulas">3</button>
<button type="button" data-val="4" onclick="setRating('sc',4)" title="Hostile: barely readable even for a human; broken layout, ambiguous structure">4</button>
</div>
<div class="rating-hint" id="sc-hint"></div>
</div>
<button class="btn-add" id="btn-add-qa" onclick="addQuery()" disabled>Add Q&A</button>
<button class="btn-clear" onclick="clearForm()">Clear</button>
</div>
<div class="qa-list" id="qa-list"></div>
</div>
</div>
<div id="tab-results" style="display:none;flex-direction:column;flex:1;overflow:hidden">
<div class="run-bar">
<button id="btn-run" onclick="runEval()">Run Evaluation</button>
<select id="methods-select" multiple>
<option value="graph" selected>Graph chunks</option>
<option value="graph_row">Graph row (per trained model)</option>
<option value="graph_kg">Graph KG</option>
<option value="naive" selected>Naive row</option>
<option value="single_cell" selected>Single cell</option>
<option value="pandas" selected>pandas</option>
<option value="beautifulsoup" selected>BeautifulSoup</option>
<option value="lxml" selected>lxml</option>
<option value="unstructured">unstructured</option>
<option value="llamasheets">LlamaSheets</option>
<option value="calamari">Calamari OCR</option>
</select>
<div class="run-status" id="run-status">Ready</div>
</div>
<div class="run-log" id="run-log" style="display:none"></div>
<div class="results-wrap" id="results-wrap">
<div class="empty-state" id="results-empty">No evaluation results yet. Create Q&A entries and run the evaluation.</div>
<table class="results-table" id="results-table" style="display:none">
<thead><tr id="results-header"></tr></thead>
<tbody id="results-body"></tbody>
</table>
</div>
</div>
</div>
<script>
let TOKEN = localStorage.getItem('auth_token') || '';
let allSheets = [];
let workbooks = {}; // drive_file_id -> {tabs: [...], qaTotal, allReserved, ...}
let currentSheet = null; // the selected tab
let currentWbId = null; // the selected workbook's drive_file_id
let currentCells = [];
let currentQueries = [];
let selectedSources = [];
let editingQueryId = null;
let openWorkbooks = new Set();
let currentQD = null; // query difficulty 1-3
let currentSC = null; // sheet complexity 1-3
const QD_HINTS = {
1: 'Single lookup — one value retrieved directly',
2: 'Multi-lookup — several values retrieved, no reasoning',
3: 'Reasoning — multiple values retrieved and reasoned over',
};
const SC_HINTS = {
1: 'Simple — single table, clear headers, flat layout',
2: 'Moderate — multiple sections, merged cells, or multi-level headers',
3: 'Complex — multiple tables, nested layout, heavy merges or formulas',
4: 'Hostile — barely readable even for a human; broken layout, ambiguous structure',
};
const TABLE_COLORS = [
'#e74c3c','#2ecc71','#3498db','#9b59b6','#e67e22',
'#1abc9c','#f39c12','#e84393','#00cec9','#6c5ce7',
];
async function api(path, opts={}) {
const headers = opts.headers || {};
if (TOKEN) headers['Authorization'] = `Bearer ${TOKEN}`;
if (opts.body && typeof opts.body === 'object' && !(opts.body instanceof FormData)) {
headers['Content-Type'] = 'application/json';
opts.body = JSON.stringify(opts.body);
}
const res = await fetch(path, {...opts, headers, credentials: "include"});
if (res.status === 401) { showLogin(); throw new Error('Auth required'); }
if (!res.ok) {
const err = await res.json().catch(() => ({detail: res.statusText}));
throw new Error(err.detail || `HTTP ${res.status}`);
}
return res.json();
}
function showLogin() { document.getElementById('login-overlay').style.display = 'flex'; }
function hideLogin() { document.getElementById('login-overlay').style.display = 'none'; }
async function doLogin() {
const pw = document.getElementById('login-pw').value;
try {
const r = await fetch('/api/auth', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({password:pw}), credentials:'include'});
const d = await r.json();
if (d.ok) { TOKEN = d.token; localStorage.setItem('auth_token', TOKEN); hideLogin(); loadSheets(); }
else { document.getElementById('login-error').style.display='block'; }
} catch { document.getElementById('login-error').style.display='block'; }
}
async function checkAuth() {
try {
const r = await api('/api/auth/check');
if (r.authenticated) { hideLogin(); loadSheets(); }
else { TOKEN = ''; localStorage.removeItem('auth_token'); showLogin(); }
} catch { showLogin(); }
}
function buildWorkbooks() {
workbooks = {};
for (const s of allSheets) {
const wbId = s.drive_file_id || s.spreadsheet_id || s.sheet_id;
if (!workbooks[wbId]) {
workbooks[wbId] = {
id: wbId,
fileName: s.file_name || wbId,
tabs: [], qaTotal: 0,
allReserved: true, anyReserved: false,
};
}
const wb = workbooks[wbId];
wb.tabs.push(s);
wb.qaTotal += s.qa_count;
if (!s.held_out) wb.allReserved = false;
if (s.held_out) wb.anyReserved = true;
}
for (const wb of Object.values(workbooks)) {
if (wb.tabs.length === 0) wb.allReserved = false;
}
}
async function loadSheets() {
try {
const data = await api('/api/rag_eval/sheets');
allSheets = data.sheets || [];
buildWorkbooks();
const wbCount = Object.keys(workbooks).length;
const reserved = Object.values(workbooks).filter(w => w.allReserved).length;
const qas = allSheets.reduce((s,x) => s + x.qa_count, 0);
document.getElementById('sidebar-stats').textContent =
`${wbCount} workbooks (${allSheets.length} tabs) · ${reserved} reserved · ${qas} Q&As`;
renderSheetList();
loadResults();
} catch(e) {
console.error('Failed to load sheets:', e);
document.getElementById('sidebar-stats').textContent = 'Error loading sheets — check Google API connection';
document.getElementById('sidebar-stats').style.color = '#dc2626';
document.getElementById('sheet-list').innerHTML =
'<div style="padding:20px;text-align:center;color:#888;font-size:13px">' +
'<p style="margin-bottom:8px">Could not load sheet list from Google Sheets API.</p>' +
'<button onclick="loadSheets()" style="padding:6px 16px;border:1px solid #ccc;border-radius:6px;cursor:pointer;font-size:13px">Retry</button></div>';
}
}
function renderSheetList() {
const search = document.getElementById('search-input').value.toLowerCase();
const reservedOnly = document.getElementById('filter-holdout').checked;
const withQaOnly = document.getElementById('filter-with-qa').checked;
const el = document.getElementById('sheet-list');
let html = '';
const sortedWbs = Object.values(workbooks).sort((a, b) => {
if (a.qaTotal !== b.qaTotal) return b.qaTotal - a.qaTotal;
if (a.allReserved !== b.allReserved) return a.allReserved ? -1 : 1;
return (a.fileName || a.id).localeCompare(b.fileName || b.id);
});
for (const wb of sortedWbs) {
const name = wb.fileName || wb.id;
const matchesSearch = !search || name.toLowerCase().includes(search) ||
wb.id.toLowerCase().includes(search) ||
wb.tabs.some(t => (t.sheet_name || '').toLowerCase().includes(search));
if (!matchesSearch) continue;
if (reservedOnly && !wb.allReserved) continue;
if (withQaOnly && wb.qaTotal === 0) continue;
const isOpen = openWorkbooks.has(wb.id);
let badges = '';
if (wb.qaTotal > 0) badges += `<span class="badge badge-qa">${wb.qaTotal} Q&A</span>`;
if (wb.allReserved) badges += `<span class="badge badge-rag">reserved</span>`;
const displayName = name.length > 30 ? name.slice(0, 14) + '…' + name.slice(-12) : name;
html += `<div class="wb-group ${isOpen ? 'open' : ''}">
<div class="wb-header ${isOpen ? 'open' : ''}" onclick="toggleWorkbook('${wb.id}')">
<span class="wb-arrow">&#9654;</span>
<span class="wb-name" title="${esc(name)}">${esc(displayName)}</span>
<span class="wb-tabs-count">${wb.tabs.length} tab${wb.tabs.length > 1 ? 's' : ''}</span>
<span class="wb-badges">${badges}</span>
</div>
<div class="wb-tabs">`;
for (const tab of wb.tabs) {
const isActive = currentSheet && currentSheet.sheet_id === tab.sheet_id;
let tabBadges = '';
if (tab.qa_count > 0) tabBadges += `<span class="badge badge-qa" style="font-size:8px;padding:0 4px">${tab.qa_count}</span>`;
if (tab.status === 'rag_eval') tabBadges += `<span class="badge badge-rag" style="font-size:8px;padding:0 4px">RAG</span>`;
html += `<div class="tab-item ${isActive ? 'active' : ''}" onclick="event.stopPropagation();selectSheet('${esc(tab.sheet_id)}','${esc(wb.id)}')">
<span class="tab-name" title="${esc(tab.sheet_name)}">${esc(tab.sheet_name)}</span>
${tabBadges}
</div>`;
}
html += `</div></div>`;
}
el.innerHTML = html;
}
function toggleWorkbook(wbId) {
if (openWorkbooks.has(wbId)) openWorkbooks.delete(wbId);
else openWorkbooks.add(wbId);
renderSheetList();
}
function filterSheets() { renderSheetList(); }
async function selectSheet(sheetId, wbId) {
currentSheet = allSheets.find(s => s.sheet_id === sheetId);
currentWbId = wbId || (currentSheet && currentSheet.drive_file_id) || null;
openWorkbooks.add(currentWbId);
const wb = workbooks[currentWbId];
const tabName = currentSheet?.sheet_name || sheetId;
const fileName = wb?.fileName || currentWbId;
const fileDisplay = fileName && fileName.length > 30
? fileName.slice(0, 14) + '…' + fileName.slice(-12) : fileName;
document.getElementById('main-title').innerHTML =
`${esc(tabName)} <span class="subtitle">in ${esc(fileDisplay)}</span>`;
document.getElementById('holdout-area').style.display = 'flex';
document.getElementById('holdout-cb').checked = wb ? wb.allReserved : false;
document.getElementById('grid-wrap').innerHTML = '<div class="empty-state">Loading from Drive…</div>';
const driveFid = currentSheet?.drive_file_id || currentWbId;
const sName = currentSheet?.sheet_name || '';
const data = await api(`/api/rag_eval/sheet?drive_file_id=${encodeURIComponent(driveFid)}&sheet_name=${encodeURIComponent(sName)}`);
currentCells = data.cells || [];
currentQueries = data.queries || [];
selectedSources = [];
editingQueryId = null;
renderGrid();
renderQueries();
document.getElementById('qa-panel').style.display = 'block';
document.getElementById('grid-empty').style.display = 'none';
clearForm();
renderSheetList();
}
function renderGrid() {
const wrap = document.getElementById('grid-wrap');
if (!currentCells.length) { wrap.innerHTML = '<div class="empty-state">No cells in this sheet</div>'; return; }
let maxR = 0, maxC = 0;
const cellMap = {};
for (const c of currentCells) {
const key = `${c.row},${c.col}`;
cellMap[key] = c;
if (c.row > maxR) maxR = c.row;
if (c.col > maxC) maxC = c.col;
}
let html = '<table>';
for (let r = 0; r <= maxR; r++) {
html += '<tr>';
for (let col = 0; col <= maxC; col++) {
const key = `${r},${col}`;
const c = cellMap[key];
const val = c ? (c.value || '') : '';
const isSrc = selectedSources.some(s => s.row === r && s.col === col);
let cls = isSrc ? 'source-selected' : '';
if (!val) cls += ' label-empty';
const display = val.length > 40 ? val.slice(0, 37) + '…' : val;
html += `<td class="${cls}" onclick="toggleSource(${r},${col})" title="(${r},${col}) ${esc(val)}">${esc(display)}</td>`;
}
html += '</tr>';
}
html += '</table>';
wrap.innerHTML = html;
}
function toggleSource(row, col) {
const idx = selectedSources.findIndex(s => s.row === row && s.col === col);
if (idx >= 0) selectedSources.splice(idx, 1);
else selectedSources.push({row, col});
renderGrid();
updateSourceDisplay();
validateForm();
}
function updateSourceDisplay() {
const el = document.getElementById('source-display');
if (!selectedSources.length) { el.innerHTML = 'No cells selected'; return; }
el.innerHTML = selectedSources.map(s => {
const c = currentCells.find(x => x.row === s.row && x.col === s.col);
const val = c ? (c.value || '').slice(0, 20) : '';
return `<span class="src-cell" onclick="event.stopPropagation();toggleSource(${s.row},${s.col})" title="Click to remove">(${s.row},${s.col}) ${esc(val)}</span>`;
}).join('');
}
function setRating(type, val) {
if (type === 'qd') {
currentQD = currentQD === val ? null : val;
document.querySelectorAll('#qd-btns button').forEach(b => b.classList.toggle('active', +b.dataset.val === currentQD));
document.getElementById('qd-hint').textContent = currentQD ? QD_HINTS[currentQD] : '';
} else {
currentSC = currentSC === val ? null : val;
document.querySelectorAll('#sc-btns button').forEach(b => b.classList.toggle('active', +b.dataset.val === currentSC));
document.getElementById('sc-hint').textContent = currentSC ? SC_HINTS[currentSC] : '';
}
validateForm();
}
function validateForm() {
const q = document.getElementById('qa-question').value.trim();
const a = document.getElementById('qa-answer').value.trim();
document.getElementById('btn-add-qa').disabled = !(q && a && selectedSources.length);
}
function clearForm() {
document.getElementById('qa-question').value = '';
document.getElementById('qa-answer').value = '';
selectedSources = [];
editingQueryId = null;
currentQD = null;
currentSC = null;
document.querySelectorAll('#qd-btns button, #sc-btns button').forEach(b => b.classList.remove('active'));
document.getElementById('qd-hint').textContent = '';
document.getElementById('sc-hint').textContent = '';
document.getElementById('btn-add-qa').textContent = 'Add Q&A';
updateSourceDisplay();
renderGrid();
validateForm();
}
async function addQuery() {
const q = document.getElementById('qa-question').value.trim();
const a = document.getElementById('qa-answer').value.trim();
if (!q || !a || !selectedSources.length || !currentSheet) return;
const payload = {
sheet_id: currentSheet.sheet_id,
drive_file_id: currentSheet.drive_file_id || currentWbId,
sheet_name: currentSheet.sheet_name,
question: q,
expected_answer: a,
source_cells: selectedSources,
query_difficulty: currentQD,
sheet_complexity: currentSC,
};
if (editingQueryId) payload.query_id = editingQueryId;
await api('/api/rag_eval/query', {method:'POST', body: payload});
clearForm();
await loadSheets();
if (currentSheet) await selectSheet(currentSheet.sheet_id, currentWbId);
}
async function deleteQuery(queryId) {
if (!confirm('Delete this Q&A entry?')) return;
await api(`/api/rag_eval/query/${queryId}`, {method:'DELETE'});
await loadSheets();
if (currentSheet) await selectSheet(currentSheet.sheet_id, currentWbId);
}
function editQuery(queryId) {
const q = currentQueries.find(x => x.id === queryId);
if (!q) return;
document.getElementById('qa-question').value = q.question;
document.getElementById('qa-answer').value = q.expected_answer;
selectedSources = (q.source_cells || []).map(s => ({row: s.row, col: s.col}));
editingQueryId = queryId;
currentQD = q.query_difficulty || null;
currentSC = q.sheet_complexity || null;
document.querySelectorAll('#qd-btns button').forEach(b => b.classList.toggle('active', +b.dataset.val === currentQD));
document.querySelectorAll('#sc-btns button').forEach(b => b.classList.toggle('active', +b.dataset.val === currentSC));
document.getElementById('qd-hint').textContent = currentQD ? QD_HINTS[currentQD] : '';
document.getElementById('sc-hint').textContent = currentSC ? SC_HINTS[currentSC] : '';
document.getElementById('btn-add-qa').textContent = 'Update Q&A';
renderGrid();
updateSourceDisplay();
validateForm();
}
function renderQueries() {
const list = document.getElementById('qa-list');
if (!currentQueries.length) {
list.innerHTML = '<h3>No Q&A entries yet for this tab</h3><p style="font-size:12px;color:#999;padding-bottom:10px">Click cells in the grid to select source(s), then type a question and expected answer.</p>';
return;
}
let html = `<h3>${currentQueries.length} Q&A entr${currentQueries.length===1?'y':'ies'}</h3>`;
for (const q of currentQueries) {
const srcs = (q.source_cells || []).map(s => `(${s.row},${s.col})`).join(', ');
let badges = '';
if (q.query_difficulty) badges += `<span class="diff-badge qd" title="${esc(QD_HINTS[q.query_difficulty])}">QD:${q.query_difficulty}</span>`;
if (q.sheet_complexity) badges += `<span class="diff-badge sc" title="${esc(SC_HINTS[q.sheet_complexity])}">SC:${q.sheet_complexity}</span>`;
html += `<div class="qa-entry">
<div class="qa-body">
<div class="qa-q">Q: ${esc(q.question)}</div>
<div class="qa-a">A: ${esc(q.expected_answer)}</div>
<div class="qa-src">Source: ${srcs}</div>
${badges ? `<div class="qa-badges">${badges}</div>` : ''}
</div>
<div class="qa-actions">
<button onclick="editQuery('${q.id}')">Edit</button>
<button class="btn-del" onclick="deleteQuery('${q.id}')">Delete</button>
</div>
</div>`;
}
list.innerHTML = html;
}
function esc(s) { if (!s) return ''; const d=document.createElement('div'); d.textContent=s; return d.innerHTML; }
async function toggleHoldout() {
if (!currentWbId) return;
const held = document.getElementById('holdout-cb').checked;
await api('/api/rag_eval/toggle_holdout', {method:'POST', body:{drive_file_id: currentWbId, held_out: held}});
await loadSheets();
}
function switchTab(tab) {
document.querySelectorAll('.tab-btn').forEach(b => b.classList.toggle('active', b.dataset.tab === tab));
document.getElementById('tab-annotate').style.display = tab === 'annotate' ? 'flex' : 'none';
document.getElementById('tab-results').style.display = tab === 'results' ? 'flex' : 'none';
if (tab === 'results') loadResults();
}
async function loadResults() {
try {
const data = await api('/api/rag_eval/results');
if (data.exists && data.results) renderResults(data.results);
} catch {}
}
function renderResults(results) {
const table = document.getElementById('results-table');
const empty = document.getElementById('results-empty');
if (!results || !Object.keys(results).length) { table.style.display='none'; empty.style.display='block'; return; }
table.style.display = 'table';
empty.style.display = 'none';
const metrics = ['n_chunks','recall@1','recall@5','recall@10','mrr','source_recall@1','source_recall@5','source_mrr','judge_score','judge_binary_acc'];
const methods = Object.keys(results);
let headerHtml = '<th>Method</th>';
for (const m of metrics) {
if (methods.some(method => results[method][m] !== undefined))
headerHtml += `<th>${m}</th>`;
}
document.getElementById('results-header').innerHTML = headerHtml;
let bodyHtml = '';
for (const method of methods) {
const r = results[method];
bodyHtml += '<tr>';
bodyHtml += `<td style="font-weight:600">${method}</td>`;
for (const m of metrics) {
if (!methods.some(mt => results[mt][m] !== undefined)) continue;
const v = r[m];
bodyHtml += `<td>${v !== undefined ? (typeof v === 'number' ? v.toFixed(4) : v) : '-'}</td>`;
}
bodyHtml += '</tr>';
}
document.getElementById('results-body').innerHTML = bodyHtml;
}
async function runEval() {
const sel = document.getElementById('methods-select');
const methods = Array.from(sel.selectedOptions).map(o => o.value).join(',');
if (!methods) return alert('Select at least one method');
document.getElementById('btn-run').disabled = true;
document.getElementById('run-status').textContent = 'Running...';
document.getElementById('run-log').style.display = 'block';
document.getElementById('run-log').textContent = '';
try {
await api(`/api/rag_eval/run?methods=${encodeURIComponent(methods)}`, {method:'POST'});
pollEvalStatus();
} catch(e) {
document.getElementById('run-status').textContent = 'Error: ' + e.message;
document.getElementById('btn-run').disabled = false;
}
}
async function pollEvalStatus() {
try {
const s = await api('/api/rag_eval/status');
const logEl = document.getElementById('run-log');
logEl.textContent = (s.log || []).join('\n');
logEl.scrollTop = logEl.scrollHeight;
if (s.running) {
document.getElementById('run-status').textContent = 'Running...';
setTimeout(pollEvalStatus, 2000);
} else {
document.getElementById('btn-run').disabled = false;
document.getElementById('run-status').textContent = s.result?.error ? `Error: ${s.result.error}` : 'Completed';
if (!s.result?.error) loadResults();
}
} catch {
setTimeout(pollEvalStatus, 3000);
}
}
document.getElementById('qa-question').addEventListener('input', validateForm);
document.getElementById('qa-answer').addEventListener('input', validateForm);
checkAuth();
</script>
</body>
</html>