// Batch Processor State Management const batchState = { isProcessing: false, isPaused: false, currentBatchId: null, totalPages: 0, processedPages: 0, startTime: null, pageStatuses: {}, results: { successful: 0, failed: 0, totalImages: 0, failedPages: [] }, autoScroll: true }; // Timer for elapsed time let elapsedTimer = null; // Initialize on page load window.addEventListener('DOMContentLoaded', function() { loadPDFFiles(); setupEventListeners(); checkBatchEnvironment(); }); // Load available PDF files function loadPDFFiles() { fetch('/pdf-files') .then(response => response.json()) .then(data => { const select = document.getElementById('batch_pdf_file'); data.forEach(file => { const option = document.createElement('option'); option.value = file; option.textContent = file; select.appendChild(option); }); }) .catch(error => { console.error('Error loading PDFs:', error); addConsoleMessage('Error loading PDF files: ' + error.message, 'error'); }); } // Setup event listeners function setupEventListeners() { // PDF selection change document.getElementById('batch_pdf_file').addEventListener('change', function() { if (this.value) { fetchPDFInfo(this.value); } else { document.getElementById('batch-pdf-info').classList.add('hidden'); } }); // Page range radio buttons document.querySelectorAll('input[name="page_range"]').forEach(radio => { radio.addEventListener('change', function() { const customRange = document.getElementById('custom-range'); if (this.value === 'range') { customRange.classList.remove('hidden'); } else { customRange.classList.add('hidden'); } }); }); } // Fetch PDF information (page count) function fetchPDFInfo(pdfFile) { fetch(`/pdf-info/${encodeURIComponent(pdfFile)}`) .then(response => response.json()) .then(data => { document.getElementById('total-pages').textContent = data.pageCount; document.getElementById('batch-pdf-info').classList.remove('hidden'); // Update max values for custom range document.getElementById('start_page').max = data.pageCount; document.getElementById('end_page').max = data.pageCount; document.getElementById('end_page').value = data.pageCount; }) .catch(error => { console.error('Error fetching PDF info:', error); addConsoleMessage('Error fetching PDF info: ' + error.message, 'error'); }); } // Start batch processing function startBatchProcessing() { const pdfFile = document.getElementById('batch_pdf_file').value; if (!pdfFile) { alert('Please select a PDF file'); return; } // Get page range const pageRangeType = document.querySelector('input[name="page_range"]:checked').value; let startPage = 1; let endPage = parseInt(document.getElementById('total-pages').textContent); if (pageRangeType === 'range') { startPage = parseInt(document.getElementById('start_page').value) || 1; endPage = parseInt(document.getElementById('end_page').value) || endPage; if (startPage > endPage) { alert('Invalid page range: start page must be less than or equal to end page'); return; } } // Reset state resetBatchState(); batchState.isProcessing = true; batchState.totalPages = endPage - startPage + 1; batchState.startTime = Date.now(); // Update UI updateUIForProcessing(true); initializePageGrid(startPage, endPage); // Start elapsed timer startElapsedTimer(); // Show console document.getElementById('console-output').classList.remove('hidden'); addConsoleMessage(`Starting batch processing for ${pdfFile} (pages ${startPage}-${endPage})`, 'info'); // Prepare request data const formData = new FormData(); formData.append('pdf_file', pdfFile); formData.append('start_page', startPage); formData.append('end_page', endPage); formData.append('adjust', document.getElementById('batch_adjust').checked); formData.append('parallel', document.getElementById('parallel_processing').checked); formData.append('generate_report', document.getElementById('generate_report').checked); // Start batch processing fetch('/run-batch-processor', { method: 'POST', body: formData }) .then(response => response.json()) .then(data => { if (data.success) { batchState.currentBatchId = data.batch_id; addConsoleMessage(`Batch processing started with ID: ${data.batch_id}`, 'success'); // Start polling for updates pollBatchStatus(); } else { throw new Error(data.error || 'Failed to start batch processing'); } }) .catch(error => { console.error('Error starting batch processing:', error); addConsoleMessage('Error: ' + error.message, 'error'); finishBatchProcessing(false); }); } // Poll for batch status updates function pollBatchStatus() { if (!batchState.isProcessing || !batchState.currentBatchId) { return; } fetch(`/batch-status/${batchState.currentBatchId}`) .then(response => response.json()) .then(data => { updateBatchProgress(data); if (data.completed) { finishBatchProcessing(true); } else if (!batchState.isPaused) { // Continue polling setTimeout(pollBatchStatus, 1000); } }) .catch(error => { console.error('Error polling batch status:', error); if (batchState.isProcessing) { setTimeout(pollBatchStatus, 2000); // Retry with longer delay } }); } // Update batch progress function updateBatchProgress(data) { // Update overall progress const percentage = Math.round((data.processed / data.total) * 100); document.getElementById('overall-percentage').textContent = percentage + '%'; document.getElementById('overall-progress-fill').style.width = percentage + '%'; document.getElementById('pages-completed').textContent = data.processed; document.getElementById('pages-total').textContent = data.total; // Update stage progress if (data.stages) { updateStageProgress('analysis', data.stages.analysis); updateStageProgress('visualization', data.stages.visualization); updateStageProgress('extraction', data.stages.extraction); } // Update page statuses if (data.page_statuses) { for (const [pageNum, status] of Object.entries(data.page_statuses)) { updatePageStatus(pageNum, status); } } // Update results if (data.results) { batchState.results = data.results; updateResultsSummary(); } // Add log messages if (data.logs && data.logs.length > 0) { data.logs.forEach(log => { addConsoleMessage(log.message, log.level); }); } // Update ETA if (data.eta) { document.getElementById('eta-time').textContent = formatTime(data.eta); } } // Update stage progress function updateStageProgress(stage, data) { if (!data) return; const percentage = Math.round((data.completed / data.total) * 100); document.getElementById(`${stage}-percentage`).textContent = percentage + '%'; document.getElementById(`${stage}-progress`).style.width = percentage + '%'; } // Initialize page grid function initializePageGrid(startPage, endPage) { const grid = document.getElementById('page-grid'); grid.innerHTML = ''; document.getElementById('batch-progress-panel').classList.remove('hidden'); document.getElementById('page-status-grid').classList.remove('hidden'); for (let i = startPage; i <= endPage; i++) { const pageItem = createPageStatusItem(i); grid.appendChild(pageItem); batchState.pageStatuses[i] = 'pending'; } } // Create page status item function createPageStatusItem(pageNum) { const item = document.createElement('div'); item.className = 'page-status-item pending'; item.id = `page-status-${pageNum}`; item.innerHTML = `
No images extracted from this page
'; return; } let galleryHTML = ''; images.forEach(img => { const src = img.getAttribute('src'); galleryHTML += `No extracted images available
'; }); } // Switch modal tabs function switchModalTab(tabName) { // Update tab buttons document.querySelectorAll('.modal-tab').forEach(tab => { tab.classList.remove('active'); }); event.target.classList.add('active'); // Update content document.querySelectorAll('.modal-tab-content').forEach(content => { content.classList.remove('active'); }); document.getElementById(`modal-${tabName}-content`).classList.add('active'); } // Close modal function closeBatchImageModal() { document.getElementById('batch-image-modal').classList.remove('active'); } // Pause batch processing function pauseBatchProcessing() { if (!batchState.isProcessing || batchState.isPaused) return; batchState.isPaused = true; fetch(`/pause-batch/${batchState.currentBatchId}`, { method: 'POST' }) .then(response => response.json()) .then(data => { if (data.success) { addConsoleMessage('Batch processing paused', 'info'); document.getElementById('pause-batch-btn').classList.add('hidden'); document.getElementById('resume-batch-btn').classList.remove('hidden'); } }) .catch(error => { console.error('Error pausing batch:', error); addConsoleMessage('Error pausing batch: ' + error.message, 'error'); }); } // Resume batch processing function resumeBatchProcessing() { if (!batchState.isProcessing || !batchState.isPaused) return; batchState.isPaused = false; fetch(`/resume-batch/${batchState.currentBatchId}`, { method: 'POST' }) .then(response => response.json()) .then(data => { if (data.success) { addConsoleMessage('Batch processing resumed', 'info'); document.getElementById('resume-batch-btn').classList.add('hidden'); document.getElementById('pause-batch-btn').classList.remove('hidden'); // Resume polling pollBatchStatus(); } }) .catch(error => { console.error('Error resuming batch:', error); addConsoleMessage('Error resuming batch: ' + error.message, 'error'); }); } // Cancel batch processing function cancelBatchProcessing() { if (!batchState.isProcessing) return; if (!confirm('Are you sure you want to cancel the batch processing?')) { return; } fetch(`/cancel-batch/${batchState.currentBatchId}`, { method: 'POST' }) .then(response => response.json()) .then(data => { if (data.success) { addConsoleMessage('Batch processing cancelled', 'warning'); finishBatchProcessing(false); } }) .catch(error => { console.error('Error cancelling batch:', error); addConsoleMessage('Error cancelling batch: ' + error.message, 'error'); }); } // Finish batch processing function finishBatchProcessing(success) { batchState.isProcessing = false; stopElapsedTimer(); // Update UI updateUIForProcessing(false); // Show results document.getElementById('batch-results').classList.remove('hidden'); updateResultsSummary(); if (success) { addConsoleMessage('Batch processing completed successfully!', 'success'); } else { addConsoleMessage('Batch processing stopped', 'warning'); } // Update failed pages list if any if (batchState.results.failedPages.length > 0) { showFailedPages(); } } // Update results summary function updateResultsSummary() { document.getElementById('stat-success').textContent = batchState.results.successful; document.getElementById('stat-failed').textContent = batchState.results.failed; document.getElementById('stat-duration').textContent = formatTime(Date.now() - batchState.startTime); document.getElementById('stat-images').textContent = batchState.results.totalImages; } // Show failed pages function showFailedPages() { const failedSection = document.getElementById('failed-pages'); const failedList = document.getElementById('failed-list'); failedSection.classList.remove('hidden'); failedList.innerHTML = ''; batchState.results.failedPages.forEach(page => { const item = document.createElement('div'); item.className = 'failed-item'; item.innerHTML = ` Page ${page.pageNum} ${page.reason} `; failedList.appendChild(item); }); } // Retry failed page function retryPage(pageNum) { addConsoleMessage(`Retrying page ${pageNum}...`, 'info'); const formData = new FormData(); formData.append('pdf_file', document.getElementById('batch_pdf_file').value); formData.append('page_num', pageNum); formData.append('adjust', document.getElementById('batch_adjust').checked); // Update page status updatePageStatus(pageNum, 'processing'); fetch('/retry-page', { method: 'POST', body: formData }) .then(response => response.json()) .then(data => { if (data.success) { updatePageStatus(pageNum, 'completed'); addConsoleMessage(`Page ${pageNum} processed successfully`, 'success'); // Update results batchState.results.successful++; batchState.results.failed--; batchState.results.failedPages = batchState.results.failedPages.filter(p => p.pageNum !== pageNum); updateResultsSummary(); if (batchState.results.failedPages.length === 0) { document.getElementById('failed-pages').classList.add('hidden'); } } else { updatePageStatus(pageNum, 'failed'); addConsoleMessage(`Failed to process page ${pageNum}: ${data.error}`, 'error'); } }) .catch(error => { updatePageStatus(pageNum, 'failed'); addConsoleMessage(`Error retrying page ${pageNum}: ${error.message}`, 'error'); }); } // Download all results function downloadResults() { addConsoleMessage('Preparing results for download...', 'info'); fetch(`/download-batch-results/${batchState.currentBatchId}`) .then(response => { if (!response.ok) throw new Error('Failed to prepare download'); return response.blob(); }) .then(blob => { const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `doctags_batch_results_${new Date().toISOString().split('T')[0]}.zip`; document.body.appendChild(a); a.click(); window.URL.revokeObjectURL(url); document.body.removeChild(a); addConsoleMessage('Download started', 'success'); }) .catch(error => { addConsoleMessage('Error downloading results: ' + error.message, 'error'); }); } // View detailed report function viewReport() { window.open(`/batch-report/${batchState.currentBatchId}`, '_blank'); } // Open results folder function openResultsFolder() { addConsoleMessage('Opening results folder...', 'info'); fetch('/open-results-folder', { method: 'POST' }) .then(response => response.json()) .then(data => { if (data.success) { addConsoleMessage('Results folder opened', 'success'); } else { addConsoleMessage('Could not open results folder: ' + data.error, 'error'); } }) .catch(error => { addConsoleMessage('Error: ' + error.message, 'error'); }); } // Console functions function addConsoleMessage(message, level = 'info') { const console = document.getElementById('console-content'); const timestamp = new Date().toLocaleTimeString(); const messageDiv = document.createElement('div'); messageDiv.className = `console-message ${level}`; messageDiv.innerHTML = ` ${message}`; console.appendChild(messageDiv); if (batchState.autoScroll) { console.scrollTop = console.scrollHeight; } } function clearConsole() { document.getElementById('console-content').innerHTML = ''; } function toggleAutoScroll() { batchState.autoScroll = !batchState.autoScroll; document.getElementById('autoscroll-status').textContent = `Auto-scroll: ${batchState.autoScroll ? 'ON' : 'OFF'}`; } // Timer functions function startElapsedTimer() { elapsedTimer = setInterval(() => { const elapsed = Date.now() - batchState.startTime; document.getElementById('elapsed-time').textContent = formatTime(elapsed); }, 1000); } function stopElapsedTimer() { if (elapsedTimer) { clearInterval(elapsedTimer); elapsedTimer = null; } } // Helper functions function formatTime(milliseconds) { const seconds = Math.floor(milliseconds / 1000); const minutes = Math.floor(seconds / 60); const hours = Math.floor(minutes / 60); if (hours > 0) { return `${hours}:${String(minutes % 60).padStart(2, '0')}:${String(seconds % 60).padStart(2, '0')}`; } else { return `${minutes}:${String(seconds % 60).padStart(2, '0')}`; } } function resetBatchState() { batchState.isProcessing = false; batchState.isPaused = false; batchState.currentBatchId = null; batchState.totalPages = 0; batchState.processedPages = 0; batchState.startTime = null; batchState.pageStatuses = {}; batchState.results = { successful: 0, failed: 0, totalImages: 0, failedPages: [] }; } function updateUIForProcessing(isProcessing) { // Toggle button visibility document.getElementById('start-batch-btn').classList.toggle('hidden', isProcessing); document.getElementById('pause-batch-btn').classList.toggle('hidden', !isProcessing); document.getElementById('cancel-batch-btn').classList.toggle('hidden', !isProcessing); // Disable form inputs during processing const inputs = document.querySelectorAll('.settings-panel input, .settings-panel select'); inputs.forEach(input => { input.disabled = isProcessing; }); } // Check batch environment function checkBatchEnvironment() { fetch('/check-environment') .then(response => response.json()) .then(data => { const envDiv = document.getElementById('batch-environment-check'); const envDetails = document.getElementById('batch-env-details'); if (data.missing_scripts.length > 0 || data.pdf_files.length === 0) { envDiv.classList.remove('hidden'); let html = '