docling-studio/frontend/static/batch-processor.js
2025-05-27 11:20:13 +02:00

718 lines
No EOL
24 KiB
JavaScript

// 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 = `
<div class="page-number">Page ${pageNum}</div>
<div class="page-status-icon">⏳</div>
<div class="page-actions hidden">
<button class="mini-btn" onclick="viewPageResults(${pageNum})" title="View results">
👁️
</button>
</div>
`;
item.onclick = () => viewPageResults(pageNum);
return item;
}
// Update page status
function updatePageStatus(pageNum, status) {
const item = document.getElementById(`page-status-${pageNum}`);
if (!item) return;
// Remove all status classes
item.classList.remove('pending', 'processing', 'completed', 'failed');
// Add new status class
item.classList.add(status.toLowerCase());
// Update icon
const icons = {
'pending': '⏳',
'processing': '🔄',
'completed': '✅',
'failed': '❌'
};
const iconElement = item.querySelector('.page-status-icon');
iconElement.textContent = icons[status.toLowerCase()] || '❓';
// Show actions for completed pages
if (status.toLowerCase() === 'completed') {
item.querySelector('.page-actions').classList.remove('hidden');
}
batchState.pageStatuses[pageNum] = status.toLowerCase();
}
// View page results
function viewPageResults(pageNum) {
// Check if we have a current batch ID
if (!batchState.currentBatchId) {
addConsoleMessage('No batch ID available', 'error');
return;
}
// Open modal with page visualization and extracted images
const modal = document.getElementById('batch-image-modal');
modal.classList.add('active');
// Load visualization from the batch report directory
const vizImage = document.getElementById('modal-visualization-image');
vizImage.src = `/batch-report-image/${batchState.currentBatchId}/visualization_page_${pageNum}.png?t=${Date.now()}`;
// Handle loading errors
vizImage.onerror = function() {
// Try the regular results directory as fallback
this.src = `/results/visualization_page_${pageNum}.png?t=${Date.now()}`;
this.onerror = function() {
this.alt = 'Visualization not found';
console.error('Failed to load visualization');
};
};
// Load extracted images
loadExtractedImagesForPage(pageNum);
// Update page info
document.getElementById('modal-page-info').textContent = `Page ${pageNum} Results`;
}
// Load extracted images for a specific page
function loadExtractedImagesForPage(pageNum) {
const gallery = document.getElementById('modal-extracted-gallery');
gallery.innerHTML = '<div class="loader"></div> Loading extracted images...';
// Fetch extracted images for this page
fetch(`/results/pictures_page_${pageNum}/index.html`)
.then(response => {
if (!response.ok) throw new Error('No extracted images');
return response.text();
})
.then(html => {
// Parse and display images
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const images = doc.querySelectorAll('.picture-card img');
if (images.length === 0) {
gallery.innerHTML = '<p class="no-images">No images extracted from this page</p>';
return;
}
let galleryHTML = '';
images.forEach(img => {
const src = img.getAttribute('src');
galleryHTML += `
<div class="modal-gallery-item">
<img src="/results/pictures_page_${pageNum}/${src}"
alt="Extracted image"
onclick="window.open('/results/pictures_page_${pageNum}/${src}', '_blank')">
</div>
`;
});
gallery.innerHTML = galleryHTML;
})
.catch(error => {
gallery.innerHTML = '<p class="no-images">No extracted images available</p>';
});
}
// 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 = `
<span>Page ${page.pageNum}</span>
<span class="failed-reason">${page.reason}</span>
<button class="retry-btn" onclick="retryPage(${page.pageNum})">Retry</button>
`;
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 = `<span class="timestamp">[${timestamp}]</span> ${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 = '<ul>';
if (data.missing_scripts.length > 0) {
html += '<li class="env-error">Missing scripts: ' + data.missing_scripts.join(', ') + '</li>';
}
if (data.pdf_files.length === 0) {
html += '<li class="env-error">No PDF files found in working directory</li>';
}
html += '</ul>';
envDetails.innerHTML = html;
}
})
.catch(error => {
console.error('Error checking environment:', error);
});
}