PDF-accessibility-saas/js/upload.js

254 lines
11 KiB
JavaScript

/* 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();
}