Spaces:
Sleeping
Sleeping
zofiasmolenasana
Auth: Set-Cookie session + credentials include (HF strips Authorization on GET)
3195abb unverified | <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 } | |
| .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">▶</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> | |