CoolFace
Apppublic

Aniruddha7/QueryLens-Text2SQL_DocVQA-V2

sourceHugging Faceupdated 3mo agoView on Hugging Face
0likes
app.js585 linesDownload Raw Back to static
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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');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