/* Upload handling — drag-drop, file validation, check flow */ let currentJobId = null; let pollInterval = null; let pollCount = 0; function initUpload() { const uploadArea = document.getElementById('uploadArea'); const fileInput = document.getElementById('fileInput'); uploadArea.addEventListener('click', () => fileInput.click()); uploadArea.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); fileInput.click(); } }); uploadArea.addEventListener('dragover', (e) => { e.preventDefault(); uploadArea.classList.add('dragover'); }); uploadArea.addEventListener('dragleave', () => { uploadArea.classList.remove('dragover'); }); uploadArea.addEventListener('drop', (e) => { e.preventDefault(); uploadArea.classList.remove('dragover'); if (e.dataTransfer.files.length > 0) handleFile(e.dataTransfer.files[0]); }); fileInput.addEventListener('change', (e) => { if (e.target.files.length > 0) handleFile(e.target.files[0]); }); } async function handleFile(file) { if (!file.name.toLowerCase().endsWith('.pdf')) { alert('Please select a PDF file'); return; } if (file.size > 50 * 1024 * 1024) { alert('File too large. Maximum size is 50MB.'); return; } clearLog(); document.getElementById('progressContainer').style.display = 'block'; updateProgress(0, 'Preparing upload...'); addLog('File selected: ' + file.name + ' (' + (file.size / 1024 / 1024).toFixed(2) + ' MB)', 'info'); try { updateProgress(10, 'Uploading file...'); addLog('Uploading to server...', 'info'); const result = await uploadFile(file); if (result.success) { currentJobId = result.data.job_id; updateProgress(20, 'Upload complete'); addLog('Upload successful — Job ID: ' + currentJobId, 'success'); document.getElementById('progressContainer').style.display = 'none'; showReadyState(file); } else { addLog('Upload failed: ' + result.error, 'error'); alert('Upload failed: ' + result.error); document.getElementById('progressContainer').style.display = 'none'; } } catch (error) { addLog('Upload error: ' + error.message, 'error'); alert('Upload failed: ' + error.message); document.getElementById('progressContainer').style.display = 'none'; } } function showReadyState(file) { const readyDiv = document.getElementById('uploadReadyState'); if (!readyDiv) return; document.getElementById('readyFilename').textContent = file.name; document.getElementById('readyFilesize').textContent = (file.size / 1024 / 1024).toFixed(2) + ' MB'; readyDiv.style.display = 'block'; document.getElementById('singleUploadArea').querySelector('.upload-area').style.display = 'none'; } function removeFile() { currentJobId = null; const readyDiv = document.getElementById('uploadReadyState'); if (readyDiv) readyDiv.style.display = 'none'; document.getElementById('singleUploadArea').querySelector('.upload-area').style.display = ''; document.getElementById('fileInput').value = ''; clearLog(); } async function beginCheck() { // Hide ready state, show progress const readyDiv = document.getElementById('uploadReadyState'); if (readyDiv) readyDiv.style.display = 'none'; document.getElementById('progressContainer').style.display = 'block'; updateProgress(25, 'Initializing accessibility check...'); addLog('Preparing accessibility analysis...', 'info'); const quickMode = document.getElementById('quickMode').checked; if (quickMode) addLog('Quick mode enabled — skipping expensive checks', 'info'); // Animate progress while Cloud Run processes synchronously (can take 2-5 min) const progressStages = [ { pct: 35, msg: 'Loading PDF structure...', log: 'Reading PDF metadata and tagging' }, { pct: 45, msg: 'Checking document structure...', log: 'Validating PDF tags and structure tree' }, { pct: 55, msg: 'Analyzing images with AI...', log: 'Running AI vision analysis on images' }, { pct: 65, msg: 'Checking color contrast...', log: 'Calculating WCAG contrast ratios' }, { pct: 72, msg: 'Analyzing readability...', log: 'Computing Flesch reading scores' }, { pct: 80, msg: 'Checking headings & links...', log: 'Heading hierarchy, tab order, role mapping' }, { pct: 88, msg: 'Running PDF/UA validation...', log: 'veraPDF structure validation' }, { pct: 94, msg: 'Compiling results...', log: 'Generating accessibility report' }, ]; let stageIdx = 0; const progressTimer = setInterval(() => { if (stageIdx < progressStages.length) { const s = progressStages[stageIdx++]; updateProgress(s.pct, s.msg); addLog(s.log, 'info'); } }, 18000); // advance every 18s → covers ~2.5 min of processing updateProgress(30, 'Analyzing PDF (this may take a few minutes)...'); addLog('Sent to Cloud Run for processing...', 'info'); try { const result = await startCheck(currentJobId, quickMode); clearInterval(progressTimer); if (result.success) { if (result.data && result.data.status === 'completed') { // Synchronous Cloud Run response — results are ready updateProgress(98, 'Loading results...'); addLog('Analysis complete!', 'success'); loadResults(); } else { // Async/local mode fallback — poll for status updateProgress(35, 'Analysis started'); addLog('Job processing...', 'success'); pollJobStatus(); } } else { addLog('Check failed: ' + result.error, 'error'); alert('Check failed: ' + result.error); document.getElementById('progressContainer').style.display = 'none'; } } catch (error) { clearInterval(progressTimer); addLog('Check error: ' + error.message, 'error'); alert('Check failed: ' + error.message); document.getElementById('progressContainer').style.display = 'none'; } } async function pollJobStatus() { pollCount = 0; const simStages = [ { percent: 40, message: 'Loading PDF...', log: 'Reading PDF structure and metadata' }, { percent: 50, message: 'Analyzing document structure...', log: 'Checking PDF tagging and structure' }, { percent: 60, message: 'Analyzing images...', log: 'Processing images with AI' }, { percent: 70, message: 'Checking color contrast...', log: 'Calculating WCAG contrast ratios' }, { percent: 80, message: 'Analyzing readability...', log: 'Computing readability scores' }, { percent: 90, message: 'Running final checks...', log: 'Font embedding, bookmarks, headings, tab order' }, { percent: 95, message: 'Compiling results...', log: 'Generating accessibility report' } ]; let stageIdx = 0; const tick = async () => { pollCount++; try { const result = await checkStatus(currentJobId); if (result.success) { const data = result.data; // Use real progress from Redis if available if (data.progress && data.progress > 0) { updateProgress(data.progress, data.status_message || data.status); } else if (stageIdx < simStages.length && pollCount % 3 === 0) { const s = simStages[stageIdx]; updateProgress(s.percent, s.message); addLog(s.log, 'info'); stageIdx++; } if (data.status === 'completed') { clearInterval(pollInterval); updateProgress(98, 'Loading results...'); addLog('Analysis complete! Loading results...', 'success'); loadResults(); } else if (data.status === 'failed' || data.status === 'error') { clearInterval(pollInterval); addLog('Analysis failed', 'error'); if (data.error_log) addLog('Error: ' + data.error_log.substring(0, 500), 'error'); document.getElementById('progressContainer').style.display = 'none'; alert('Analysis failed. Check the error log for details.'); } else if (pollCount > 450) { clearInterval(pollInterval); addLog('Analysis timed out after 15 minutes', 'error'); addLog('Try using Quick Mode for faster results', 'info'); document.getElementById('progressContainer').style.display = 'none'; } } } catch (error) { console.error('Status check failed:', error); addLog('Status check error (retrying...): ' + error.message, 'warning'); } }; tick(); pollInterval = setInterval(tick, 2000); } async function loadResults() { updateProgress(100, 'Complete!'); addLog('Fetching results from server...', 'info'); try { const result = await getResult(currentJobId); if (result.success) { addLog('Results loaded — Score: ' + result.data.accessibility_score + '/100', 'success'); await new Promise(r => setTimeout(r, 800)); displayResults(result.data); } else { addLog('Failed to load results: ' + result.error, 'error'); } } catch (error) { addLog('Error loading results: ' + error.message, 'error'); } } function resetCheck() { if (pollInterval) { clearInterval(pollInterval); pollInterval = null; } if (batchPollInterval) { clearInterval(batchPollInterval); batchPollInterval = null; } pollCount = 0; document.getElementById('uploadSection').style.display = 'block'; document.getElementById('resultsSection').style.display = 'none'; document.getElementById('progressContainer').style.display = 'none'; document.getElementById('pageViewerCard').style.display = 'none'; document.getElementById('fileInput').value = ''; var readyDiv = document.getElementById('uploadReadyState'); if (readyDiv) readyDiv.style.display = 'none'; var uploadArea = document.getElementById('singleUploadArea') && document.getElementById('singleUploadArea').querySelector('.upload-area'); if (uploadArea) uploadArea.style.display = ''; var remCard = document.getElementById('remediationCard'); if (remCard) remCard.style.display = 'none'; currentJobId = null; clearLog(); }