Aniruddha7/QueryLens-Text2SQL_DocVQA-V2
0
1const askBtn = document.getElementById('ask');2const questionEl = document.getElementById('question');3const answerEl = document.getElementById('answer');4const sqlEl = document.getElementById('sql');5const memoryEl = document.getElementById('memory');6const querycraftEl = document.getElementById('querycraft');7const presenterEl = document.getElementById('presenter');8const uploadInput = document.getElementById('upload');9const uploadBtn = document.getElementById('uploadBtn');10const uploadStatus = document.getElementById('uploadStatus');11const uploadControls = document.getElementById('uploadControls');12const modeToggle = document.getElementById('modeToggle');13let currentDocId = null;14let currentMode = 'text2sql';15const statusEl = document.getElementById('status');16const timeEl = document.getElementById('time');17const recalcBtn = document.getElementById('recalc');18const spinner = document.getElementById('spinner');19const spinnerText = document.getElementById('spinnerText');20const askBtnEl = askBtn;21 22// Mode toggle logic23modeToggle.addEventListener('change', function() {24 currentMode = modeToggle.value;25 console.log('[DEBUG] Mode changed to:', currentMode); // Debug log26 if(currentMode === 'ocr_qa') {27 uploadControls.classList.remove('hidden');28 console.log('[DEBUG] Upload controls shown'); // Debug log29 } else {30 uploadControls.classList.add('hidden');31 currentDocId = null;32 uploadStatus.textContent = 'No upload';33 uploadStatus.className = 'status-text';34 console.log('[DEBUG] Upload controls hidden'); // Debug log35 }36});37 38// Initialize UI state39console.log('[DEBUG] Initial mode:', currentMode); // Debug log40if(currentMode === 'ocr_qa') {41 uploadControls.classList.remove('hidden');42} else {43 uploadControls.classList.add('hidden');44}45 46// Helper to programmatically switch to OCR Q&A mode47function switchToOcrQaMode() {48 if (modeToggle.value !== 'ocr_qa') {49 modeToggle.value = 'ocr_qa';50 currentMode = 'ocr_qa';51 uploadControls.classList.remove('hidden');52 // Fire a change event so any listeners update state consistently53 const ev = new Event('change', { bubbles: true });54 modeToggle.dispatchEvent(ev);55 console.log('[DEBUG] Switched to OCR Q&A mode');56 }57}58 59// Extract mode change logic into a function so it can be called directly60function onModeChange() {61 currentMode = modeToggle.value;62 console.log('[DEBUG] onModeChange called, mode:', currentMode); // Debug log63 if(currentMode === 'ocr_qa') {64 uploadControls.classList.remove('hidden');65 console.log('[DEBUG] Upload controls shown via onModeChange'); // Debug log66 } else {67 uploadControls.classList.add('hidden');68 currentDocId = null;69 uploadStatus.textContent = 'No upload';70 uploadStatus.className = 'status-text';71 console.log('[DEBUG] Upload controls hidden via onModeChange'); // Debug log72 }73}74 75// Mode toggle logic76modeToggle.addEventListener('change', onModeChange);77 78// Upload handler: create a temporary file input on demand to avoid issues with a permanently hidden input.79if (uploadBtn) {80 uploadBtn.addEventListener('click', () => {81 // If not in OCR Q&A mode, switch and highlight, then open file picker after a delay82 if (modeToggle.value !== 'ocr_qa') {83 switchToOcrQaMode();84 // Visually highlight the dropdown to show the switch85 modeToggle.style.boxShadow = '0 0 0 3px rgba(15, 98, 254, 0.2)';86 modeToggle.style.transition = 'box-shadow 0.2s';87 setTimeout(() => {88 modeToggle.style.boxShadow = '';89 // Now open the file picker90 const tmp = document.createElement('input');91 tmp.type = 'file';92 tmp.accept = 'image/*';93 tmp.style.position = 'absolute';94 tmp.style.left = '-9999px';95 document.body.appendChild(tmp);96 97 tmp.addEventListener('change', async () => {98 const files = tmp.files;99 if (!files || files.length === 0) {100 uploadStatus.textContent = 'No file selected';101 document.body.removeChild(tmp);102 return;103 }104 switchToOcrQaMode();105 uploadStatus.textContent = 'Uploading...';106 uploadStatus.className = 'status-text text-warning';107 const fd = new FormData();108 fd.append('file', files[0]);109 try {110 const r = await fetch('/upload-image', { method: 'POST', body: fd });111 if (!r.ok) {112 const j = await r.json().catch(()=>({}));113 uploadStatus.textContent = 'Upload failed: ' + (j.detail || r.statusText);114 uploadStatus.className = 'status-text text-error';115 document.body.removeChild(tmp);116 return;117 }118 const j = await r.json();119 currentDocId = j.doc_id;120 uploadStatus.textContent = `Uploaded (doc_id=${currentDocId})`;121 uploadStatus.className = 'status-text text-success';122 } catch (e) {123 uploadStatus.textContent = 'Upload error: ' + String(e);124 uploadStatus.className = 'status-text text-error';125 }126 document.body.removeChild(tmp);127 });128 tmp.click();129 }, 350); // 350ms to allow UI to update and user to see the switch130 return;131 }132 // Already in OCR Q&A mode: open file picker immediately133 const tmp = document.createElement('input');134 tmp.type = 'file';135 tmp.accept = 'image/*';136 tmp.style.position = 'absolute';137 tmp.style.left = '-9999px';138 document.body.appendChild(tmp);139 140 tmp.addEventListener('change', async () => {141 const files = tmp.files;142 if (!files || files.length === 0) {143 uploadStatus.textContent = 'No file selected';144 uploadStatus.className = 'status-text';145 document.body.removeChild(tmp);146 return;147 }148 switchToOcrQaMode();149 uploadStatus.textContent = 'Uploading...';150 uploadStatus.className = 'status-text text-warning';151 const fd = new FormData();152 fd.append('file', files[0]);153 try {154 const r = await fetch('/upload-image', { method: 'POST', body: fd });155 if (!r.ok) {156 const j = await r.json().catch(()=>({}));157 uploadStatus.textContent = 'Upload failed: ' + (j.detail || r.statusText);158 uploadStatus.className = 'status-text text-error';159 document.body.removeChild(tmp);160 return;161 }162 const j = await r.json();163 currentDocId = j.doc_id;164 uploadStatus.textContent = `Uploaded (doc_id=${currentDocId})`;165 uploadStatus.className = 'status-text text-success';166 } catch (e) {167 uploadStatus.textContent = 'Upload error: ' + String(e);168 uploadStatus.className = 'status-text text-error';169 }170 document.body.removeChild(tmp);171 });172 tmp.click();173 });174}175 176function setRunning(running){177 if(running){178 askBtnEl.disabled = true;179 recalcBtn.disabled = true;180 questionEl.disabled = true;181 spinner.classList.remove('hidden');182 spinnerText.classList.remove('hidden');183 spinnerText.textContent = 'Processing...';184 // hide the small status text while spinner is active185 statusEl.textContent = '';186 console.log('[DEBUG] Loading state: ON, spinner should be visible'); // Debug log187 } else {188 askBtnEl.disabled = false;189 recalcBtn.disabled = false;190 questionEl.disabled = false;191 spinner.classList.add('hidden');192 spinnerText.classList.add('hidden');193 // restore default status194 statusEl.textContent = 'Ready';195 console.log('[DEBUG] Loading state: OFF, spinner should be hidden'); // Debug log196 }197}198 199function renderPresenterTrace(text){200 if(!text) return '—';201 // Split traces into chunks separated by blank lines and render as collapsible202 const parts = String(text).split(/\n\s*\n/).filter(Boolean);203 const container = document.createElement('div');204 parts.forEach((p, i) => {205 const d = document.createElement('details');206 const summary = document.createElement('summary');207 summary.textContent = p.split('\n')[0].slice(0,80);208 summary.style.cursor = 'pointer';209 summary.style.padding = '0.5rem';210 summary.style.borderRadius = '0.375rem';211 summary.style.marginBottom = '0.5rem';212 summary.style.backgroundColor = 'var(--bg-tertiary)';213 const pre = document.createElement('pre');214 pre.style.whiteSpace = 'pre-wrap';215 pre.style.fontSize = '0.75rem';216 pre.style.padding = '0.75rem';217 pre.style.backgroundColor = 'var(--bg-secondary)';218 pre.style.borderRadius = '0.375rem';219 pre.style.margin = '0';220 pre.textContent = p;221 d.appendChild(summary);222 d.appendChild(pre);223 if(i===0) d.open = true;224 container.appendChild(d);225 });226 return container;227}228 229// small helper to escape HTML230const esc = s => String(s || '').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');231 232// Detect if text contains a pipe/markdown table and return true233function isMarkdownTable(text){234 if(!text) return false;235 const lines = String(text).split(/\r?\n/).map(l=>l.trim()).filter(Boolean);236 if(lines.length < 2) return false;237 // Count lines that contain pipe separators238 const pipeLines = lines.filter(l => l.includes('|'));239 if(pipeLines.length < 2) return false;240 // If the second line is a markdown separator like | --- | --- | or ---|---241 const second = lines[1];242 if(/^\s*\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)+\|?\s*$/.test(second)) return true;243 // Otherwise, if majority of non-empty lines contain pipes, treat as table244 return (pipeLines.length / lines.length) >= 0.5;245}246 247// Parse a simple markdown/pipe table into a DOM <table>248function parseMarkdownTable(text){249 const lines = String(text).split(/\r?\n/).map(l=>l.trim()).filter(Boolean);250 // find the first contiguous block of pipe lines251 let start = -1, end = -1;252 for(let i=0;i<lines.length;i++){253 if(lines[i].includes('|')){ start = i; break; }254 }255 if(start === -1) return null;256 for(let i=start;i<lines.length;i++){257 if(!lines[i].includes('|')){ end = i; break; }258 }259 if(end === -1) end = lines.length;260 const block = lines.slice(start, end);261 const rows = block.map(r => {262 let s = r;263 if(s.startsWith('|')) s = s.slice(1);264 if(s.endsWith('|')) s = s.slice(0,-1);265 return s.split('|').map(c => c.trim());266 });267 268 // If second row is separator, treat first row as header269 let header = null;270 if(rows.length >= 2 && rows[1].every(c => /^:?-{3,}:?$/.test(c))) {271 header = rows[0];272 rows.splice(0,2);273 }274 275 const table = document.createElement('table');276 table.className = 'answer-table';277 const thead = document.createElement('thead');278 const tbody = document.createElement('tbody');279 280 if(header){281 const tr = document.createElement('tr');282 header.forEach(h => { const th = document.createElement('th'); th.textContent = h || ''; tr.appendChild(th); });283 thead.appendChild(tr);284 } else if(rows.length>0){285 // use first row as header if no separator present286 const tr = document.createElement('tr');287 rows[0].forEach((_,i) => { const th = document.createElement('th'); th.textContent = `col ${i+1}`; tr.appendChild(th); });288 thead.appendChild(tr);289 }290 291 rows.forEach(r => {292 const tr = document.createElement('tr');293 r.forEach(c => { const td = document.createElement('td'); td.textContent = c; tr.appendChild(td); });294 tbody.appendChild(tr);295 });296 297 if(thead.childElementCount) table.appendChild(thead);298 table.appendChild(tbody);299 300 // Create enhanced table wrapper with controls301 const wrapper = document.createElement('div');302 wrapper.className = 'table-wrapper';303 304 // Create table controls305 const controls = document.createElement('div');306 controls.className = 'table-controls';307 308 const controlsLeft = document.createElement('div');309 controlsLeft.className = 'table-controls-left';310 311 const tableInfo = document.createElement('div');312 tableInfo.className = 'table-info';313 const rowCount = tbody.children.length;314 const colCount = header ? header.length : (rows.length > 0 ? rows[0].length : 0);315 tableInfo.textContent = `${rowCount} rows × ${colCount} columns`;316 317 controlsLeft.appendChild(tableInfo);318 319 const controlsRight = document.createElement('div');320 controlsRight.className = 'table-controls-right';321 322 const resizeControls = document.createElement('div');323 resizeControls.className = 'table-resize-controls';324 325 // Size control buttons326 const sizes = [327 { name: 'Compact', class: 'compact' },328 { name: 'Comfortable', class: 'comfortable' },329 { name: 'Spacious', class: 'spacious' }330 ];331 332 sizes.forEach((size, index) => {333 const btn = document.createElement('button');334 btn.textContent = size.name;335 btn.className = 'table-resize-btn';336 if (index === 1) btn.classList.add('active'); // Default to comfortable337 338 btn.addEventListener('click', () => {339 // Remove all size classes340 table.classList.remove('compact', 'comfortable', 'spacious');341 // Add selected size class342 table.classList.add(size.class);343 344 // Update active button345 resizeControls.querySelectorAll('.table-resize-btn').forEach(b => b.classList.remove('active'));346 btn.classList.add('active');347 });348 349 resizeControls.appendChild(btn);350 });351 352 controlsRight.appendChild(resizeControls);353 354 controls.appendChild(controlsLeft);355 controls.appendChild(controlsRight);356 357 // Create table container358 const container = document.createElement('div');359 container.className = 'table-container';360 container.appendChild(table);361 362 wrapper.appendChild(controls);363 wrapper.appendChild(container);364 365 // Set default size366 table.classList.add('comfortable');367 368 return wrapper;369}370 371// heuristics: split logs into agent sections372function splitLogs(logs) {373 const sections = {memory: '', querycraft: '', presenter: ''};374 if (!logs) {375 console.log('[DEBUG] splitLogs: No logs provided');376 return sections;377 }378 379 console.log('[DEBUG] splitLogs: Input logs length:', logs.length);380 console.log('[DEBUG] splitLogs: First 500 chars:', logs.substring(0, 500));381 console.log('[DEBUG] splitLogs: Last 500 chars:', logs.substring(Math.max(0, logs.length - 500)));382 383 // Normalize line endings384 const lines = logs.replace(/\r/g,'').split('\n');385 console.log('[DEBUG] splitLogs: Total lines:', lines.length);386 387 // Look for explicit markers first388 let mode = null;389 let memoryMarkerFound = false;390 let querycraftMarkerFound = false;391 let presenterMarkerFound = false;392 393 for (let i = 0; i < lines.length; i++) {394 const line = lines[i];395 const l = line.trim();396 397 // Match the exact agent section headers from agentic_workflow.py398 if (/=== MEMORY AGENT ===/i.test(l) || /INITIAL MEMORY/i.test(l)) { 399 mode = 'memory'; 400 memoryMarkerFound = true;401 console.log(`[DEBUG] splitLogs: Found MEMORY marker at line ${i}: "${l}"`);402 continue; 403 }404 if (/=== QUERY CRAFT AGENT ===/i.test(l) || /QUERY_CRAFT/i.test(l) || /MICRO-HINT/i.test(l)) { 405 mode = 'querycraft'; 406 querycraftMarkerFound = true;407 console.log(`[DEBUG] splitLogs: Found QUERYCRAFT marker at line ${i}: "${l}"`);408 continue; 409 }410 if (/=== RESULT PRESENTER AGENT ===/i.test(l) || /PRESENTER/i.test(l)) { 411 mode = 'presenter'; 412 presenterMarkerFound = true;413 console.log(`[DEBUG] splitLogs: Found PRESENTER marker at line ${i}: "${l}"`);414 continue; 415 }416 // Additional patterns to catch query craft related logs417 if (/\[FAST\]/i.test(l) || /\[GEN\]/i.test(l) || /\[MICRO-HINT\]/i.test(l)) { 418 mode = 'querycraft'; 419 console.log(`[DEBUG] splitLogs: Found QUERYCRAFT pattern at line ${i}: "${l}"`);420 }421 // Heuristic: SQL lines go to querycraft422 if (/^(SELECT|WITH|INSERT|UPDATE|DELETE)\b/i.test(l)) { 423 mode = 'querycraft'; 424 sections.querycraft += l + '\n'; 425 console.log(`[DEBUG] splitLogs: Found SQL pattern at line ${i}, mode set to querycraft`);426 continue; 427 }428 if (!mode) {429 // Seed with memory until a clearer marker appears430 sections.memory += line + '\n';431 } else {432 sections[mode] += line + '\n';433 }434 }435 436 console.log('[DEBUG] splitLogs: Markers found - Memory:', memoryMarkerFound, 'QueryCraft:', querycraftMarkerFound, 'Presenter:', presenterMarkerFound);437 console.log('[DEBUG] splitLogs: Section lengths - Memory:', sections.memory.length, 'QueryCraft:', sections.querycraft.length, 'Presenter:', sections.presenter.length);438 console.log('[DEBUG] splitLogs: Memory section preview:', sections.memory.substring(0, 200));439 console.log('[DEBUG] splitLogs: QueryCraft section preview:', sections.querycraft.substring(0, 200));440 console.log('[DEBUG] splitLogs: Presenter section preview:', sections.presenter.substring(0, 200));441 442 return sections;443}444 445function findSQL(logs) {446 if (!logs) return '';447 // Primary: explicit markers448 const markerRe = /<<<SQL_REVISED_START>>>([\s\S]*?)<<<SQL_REVISED_END>>>/m;449 let m = logs.match(markerRe);450 if (m) return m[1].trim();451 const markerRe2 = /<<<SQL_START>>>([\s\S]*?)<<<SQL_END>>>/m;452 m = logs.match(markerRe2);453 if (m) return m[1].trim();454 // Fallback: first standalone SQL-ish block up to semicolon455 const fallback = logs.match(/^(?:.*?)(SELECT|WITH)[^;]{0,4000};/im);456 if (fallback) {457 const startIdx = fallback.index;458 if (startIdx != null) {459 const slice = logs.slice(startIdx).match(/([\s\S]*?;)/);460 if (slice) return slice[1].trim();461 }462 }463 return '';464}465 466async function runQuery(q){467 if (!q) return alert('Please enter a question');468 answerEl.textContent = 'Thinking...';469 setRunning(true);470 timeEl.textContent = '';471 sqlEl.textContent = '-- SQL will appear here --';472 memoryEl.textContent = querycraftEl.textContent = presenterEl.textContent = '—';473 474 const startTime = Date.now();475 476 try {477 const body = { question: q, mode: currentMode };478 if (currentMode === 'ocr_qa' && currentDocId) body.doc_id = currentDocId;479 const res = await fetch('/api/query', {480 method: 'POST', headers: { 'Content-Type': 'application/json' },481 body: JSON.stringify(body)482 });483 let j = null;484 if (!res.ok) {485 try { j = await res.json(); } catch(err){ const t = await res.text(); answerEl.textContent = 'Error: ' + t; setRunning(false); return; }486 if (j.error === 'worker_timeout') { answerEl.textContent = 'Worker timeout: ' + (j.message || 'The worker did not respond in time.'); setRunning(false); return; }487 answerEl.textContent = j.error || j.message || JSON.stringify(j);488 if (j.trace) presenterEl.textContent = j.trace;489 setRunning(false);490 return;491 }492 j = await res.json();493 494 const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);495 496 if (j.answer) {497 // Render markdown/pipe tables as HTML tables for readability498 if(isMarkdownTable(j.answer)){499 const node = parseMarkdownTable(j.answer);500 answerEl.innerHTML = '';501 if(node) answerEl.appendChild(node);502 else answerEl.textContent = j.answer;503 } else {504 // Non-table answers: preserve plain text505 answerEl.textContent = j.answer;506 }507 } else answerEl.textContent = JSON.stringify(j, null, 2);508 509 // Prefer direct sql field from worker, else derive from logs510 if (j.sql) {511 const codeEl = document.getElementById('sql');512 codeEl.textContent = j.sql;513 if(window.Prism && Prism.highlightElement) Prism.highlightElement(codeEl);514 }515 516 if (j.logs) {517 console.log('[DEBUG] runQuery: Processing logs of length:', j.logs.length);518 const sections = splitLogs(j.logs);519 console.log('[DEBUG] runQuery: Sections received:', {520 memory: sections.memory.length,521 querycraft: sections.querycraft.length,522 presenter: sections.presenter.length523 });524 525 const memoryContent = sections.memory.trim() || '—';526 const querycraftContent = sections.querycraft.trim() || '—';527 const presenterContent = sections.presenter.trim() || '';528 529 console.log('[DEBUG] runQuery: Setting memory content length:', memoryContent.length);530 console.log('[DEBUG] runQuery: Setting querycraft content length:', querycraftContent.length);531 console.log('[DEBUG] runQuery: Setting presenter content length:', presenterContent.length);532 533 memoryEl.textContent = memoryContent;534 querycraftEl.textContent = querycraftContent;535 536 if(presenterContent){ 537 console.log('[DEBUG] runQuery: Rendering presenter trace');538 const node = renderPresenterTrace(presenterContent); 539 presenterEl.innerHTML = ''; 540 presenterEl.appendChild(node); 541 } else { 542 console.log('[DEBUG] runQuery: No presenter content, setting dash');543 presenterEl.textContent = '—'; 544 }545 if (!j.sql) {546 const sql = findSQL(j.logs) || '';547 if (sql) { 548 const codeEl = document.getElementById('sql'); 549 codeEl.textContent = sql; 550 if(window.Prism && Prism.highlightElement) Prism.highlightElement(codeEl); 551 }552 }553 } else {554 console.log('[DEBUG] runQuery: No logs in response');555 }556 557 statusEl.textContent = 'Done';558 timeEl.textContent = `${elapsed}s`;559 setRunning(false);560 } catch (e) {561 answerEl.textContent = 'Error: ' + String(e);562 setRunning(false);563 }564}565 566askBtn.onclick = () => runQuery(questionEl.value.trim());567 568recalcBtn.onclick = () => {569 const q = questionEl.value.trim();570 if (!q) return alert('Please enter a question');571 const normalized = q.replace(/^(?:re-execute\s+)+/i, '');572 const prefixed = 're-execute ' + normalized;573 // do not modify the visible textarea; send the prefixed query directly574 runQuery(prefixed);575};576 577// Enter key support578questionEl.addEventListener('keydown', (e) => {579 if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {580 e.preventDefault();581 runQuery(questionEl.value.trim());582 }583});584 585 