Skip to main content
Glama
app.js18.9 kB
/** * OCR-MCP Web Interface JavaScript * Handles frontend interactions with the FastAPI backend */ // Global state let currentJobId = null; let selectedFiles = []; let currentResult = null; let statusCheckInterval = null; // DOM elements const uploadArea = document.getElementById('upload-area'); const fileInput = document.getElementById('file-input'); const batchFileInput = document.getElementById('batch-file-input'); const batchFileList = document.getElementById('batch-file-list'); const processingStatus = document.getElementById('processing-status'); const resultsSection = document.getElementById('results-section'); // Initialize the application document.addEventListener('DOMContentLoaded', function() { initializeEventListeners(); checkHealth(); }); // Initialize event listeners function initializeEventListeners() { // Tab switching document.querySelectorAll('.tab-button').forEach(button => { button.addEventListener('click', function() { const tabName = this.getAttribute('onclick').match(/'([^']+)'/)[1]; showTab(tabName); }); }); // File upload drag and drop ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { uploadArea.addEventListener(eventName, preventDefaults, false); }); ['dragenter', 'dragover'].forEach(eventName => { uploadArea.addEventListener(eventName, highlight, false); }); ['dragleave', 'drop'].forEach(eventName => { uploadArea.addEventListener(eventName, unhighlight, false); }); uploadArea.addEventListener('drop', handleDrop, false); fileInput.addEventListener('change', handleFileSelect, false); // Batch file handling batchFileInput.addEventListener('change', handleBatchFileSelect, false); // Results tabs document.querySelectorAll('.results-tab').forEach(tab => { tab.addEventListener('click', function() { const tabType = this.getAttribute('onclick').match(/'([^']+)'/)[1]; showResultTab(tabType); }); }); } // Tab switching functions function showTab(tabName) { // Hide all tabs document.querySelectorAll('.tab-content').forEach(tab => { tab.classList.remove('active'); }); document.querySelectorAll('.tab-button').forEach(button => { button.classList.remove('active'); }); // Show selected tab document.getElementById(tabName + '-tab').classList.add('active'); event.target.classList.add('active'); } function showResultTab(tabType) { // Hide all result panels document.querySelectorAll('.result-panel').forEach(panel => { panel.classList.remove('active'); }); document.querySelectorAll('.results-tab').forEach(tab => { tab.classList.remove('active'); }); // Show selected result panel document.getElementById(tabType + '-results').classList.add('active'); event.target.classList.add('active'); } // File upload handling function preventDefaults(e) { e.preventDefault(); e.stopPropagation(); } function highlight(e) { uploadArea.classList.add('dragover'); } function unhighlight(e) { uploadArea.classList.remove('dragover'); } function handleDrop(e) { const dt = e.dataTransfer; const files = dt.files; handleFiles(files); } function handleFileSelect(e) { const files = e.target.files; handleFiles(files); } function handleFiles(files) { if (files.length > 0) { const file = files[0]; displaySelectedFile(file); } } function displaySelectedFile(file) { const uploadContent = uploadArea.querySelector('.upload-content'); uploadContent.innerHTML = ` <i class="fas fa-file-alt upload-icon" style="color: var(--success-color);"></i> <p><strong>${file.name}</strong></p> <p class="upload-subtitle">${formatFileSize(file.size)} • ${file.type || 'Unknown type'}</p> <button class="btn btn-secondary" onclick="clearFile()"> <i class="fas fa-times"></i> Clear </button> `; // Store the file for processing window.selectedFile = file; } // Batch file handling function handleBatchFileSelect(e) { const files = Array.from(e.target.files); selectedFiles = files; displayBatchFiles(); } function displayBatchFiles() { batchFileList.innerHTML = ''; if (selectedFiles.length === 0) { batchFileList.innerHTML = '<p class="text-center mt-20">No files selected</p>'; return; } selectedFiles.forEach((file, index) => { const fileItem = document.createElement('div'); fileItem.className = 'file-item'; fileItem.innerHTML = ` <div> <span class="file-name">${file.name}</span> <span class="file-size">${formatFileSize(file.size)}</span> </div> <button class="remove-btn" onclick="removeBatchFile(${index})"> <i class="fas fa-times"></i> </button> `; batchFileList.appendChild(fileItem); }); } function removeBatchFile(index) { selectedFiles.splice(index, 1); displayBatchFiles(); } function clearFile() { window.selectedFile = null; const uploadContent = uploadArea.querySelector('.upload-content'); uploadContent.innerHTML = ` <i class="fas fa-cloud-upload-alt upload-icon"></i> <p>Drag & drop files here or click to browse</p> <p class="upload-subtitle">Supports PDF, PNG, JPG, CBZ, and other image formats</p> <button class="btn btn-primary" onclick="document.getElementById('file-input').click()"> <i class="fas fa-folder-open"></i> Choose Files </button> `; } // File processing async function processFile() { if (!window.selectedFile) { showAlert('Please select a file first', 'danger'); return; } const ocrMode = document.getElementById('ocr-mode').value; const backend = document.getElementById('backend').value; const formData = new FormData(); formData.append('file', window.selectedFile); formData.append('ocr_mode', ocrMode); formData.append('backend', backend); try { showProcessingStatus(window.selectedFile.name); document.getElementById('process-btn').disabled = true; document.getElementById('process-btn').innerHTML = '<i class="fas fa-spinner fa-spin"></i> Processing...'; const response = await fetch('/api/upload', { method: 'POST', body: formData }); const result = await response.json(); if (response.ok) { currentJobId = result.job_id; monitorJobStatus(); } else { throw new Error(result.detail || 'Upload failed'); } } catch (error) { console.error('Upload error:', error); showAlert('Upload failed: ' + error.message, 'danger'); hideProcessingStatus(); document.getElementById('process-btn').disabled = false; document.getElementById('process-btn').innerHTML = '<i class="fas fa-play"></i> Process Document'; } } async function processBatch() { if (selectedFiles.length === 0) { showAlert('Please select files first', 'danger'); return; } const ocrMode = document.getElementById('batch-ocr-mode').value; const backend = document.getElementById('batch-backend').value; const formData = new FormData(); selectedFiles.forEach(file => { formData.append('files', file); }); formData.append('ocr_mode', ocrMode); formData.append('backend', backend); try { showProcessingStatus(`${selectedFiles.length} files`); document.getElementById('batch-process-btn').disabled = true; document.getElementById('batch-process-btn').innerHTML = '<i class="fas fa-spinner fa-spin"></i> Processing...'; const response = await fetch('/api/process_batch', { method: 'POST', body: formData }); const result = await response.json(); if (response.ok) { currentJobId = result.job_id; monitorJobStatus(); } else { throw new Error(result.detail || 'Batch processing failed'); } } catch (error) { console.error('Batch processing error:', error); showAlert('Batch processing failed: ' + error.message, 'danger'); hideProcessingStatus(); document.getElementById('batch-process-btn').disabled = false; document.getElementById('batch-process-btn').innerHTML = '<i class="fas fa-play-circle"></i> Process Batch'; } } // Job monitoring function monitorJobStatus() { if (statusCheckInterval) { clearInterval(statusCheckInterval); } statusCheckInterval = setInterval(async () => { try { const response = await fetch(`/api/job/${currentJobId}`); const job = await response.json(); updateProcessingStatus(job); if (job.status === 'completed') { clearInterval(statusCheckInterval); showResults(job.result); hideProcessingStatus(); resetButtons(); } else if (job.status === 'failed') { clearInterval(statusCheckInterval); showAlert('Processing failed: ' + (job.error || 'Unknown error'), 'danger'); hideProcessingStatus(); resetButtons(); } } catch (error) { console.error('Status check error:', error); } }, 2000); } function updateProcessingStatus(job) { const statusBadge = document.getElementById('status-badge'); const progressFill = document.getElementById('progress-fill'); const statusDetails = document.getElementById('status-details'); statusBadge.textContent = job.status.charAt(0).toUpperCase() + job.status.slice(1); statusBadge.className = 'status-badge ' + job.status; // Simulate progress (in a real app, this would come from the backend) let progress = 0; if (job.status === 'processing') { progress = 60; } else if (job.status === 'completed') { progress = 100; } progressFill.style.width = progress + '%'; statusDetails.innerHTML = ` <p><strong>Status:</strong> ${job.status}</p> <p><strong>File:</strong> ${job.filename || 'Processing...'}</p> ${job.result ? '<p><strong>Backend:</strong> ' + (job.result.backend || 'Unknown') + '</p>' : ''} `; } function showProcessingStatus(filename) { document.getElementById('status-filename').textContent = filename; processingStatus.style.display = 'block'; resultsSection.style.display = 'none'; } function hideProcessingStatus() { processingStatus.style.display = 'none'; } function showResults(result) { currentResult = result; resultsSection.style.display = 'block'; // Populate text results document.getElementById('text-content').textContent = result.text || 'No text extracted'; // Populate JSON results document.getElementById('json-content').textContent = JSON.stringify(result, null, 2); // Populate HTML results document.getElementById('html-content').innerHTML = result.html || '<p>No HTML content available</p>'; } function resetButtons() { document.getElementById('process-btn').disabled = false; document.getElementById('process-btn').innerHTML = '<i class="fas fa-play"></i> Process Document'; document.getElementById('batch-process-btn').disabled = false; document.getElementById('batch-process-btn').innerHTML = '<i class="fas fa-play-circle"></i> Process Batch'; } // Scanner functions async function discoverScanners() { try { const response = await fetch('/api/scanners'); const data = await response.json(); const scannerList = document.getElementById('scanner-list'); scannerList.innerHTML = ''; if (data.length === 0) { scannerList.innerHTML = '<p>No scanners found. Make sure your scanner is connected and powered on.</p>'; return; } data.forEach(scanner => { const scannerItem = document.createElement('div'); scannerItem.className = 'scanner-item'; scannerItem.innerHTML = ` <div> <h4>${scanner.name || scanner.id}</h4> <p>ID: ${scanner.id}</p> </div> <button class="btn btn-primary" onclick="selectScanner('${scanner.id}')"> <i class="fas fa-check"></i> Select </button> `; scannerList.appendChild(scannerItem); }); } catch (error) { console.error('Scanner discovery error:', error); showAlert('Failed to discover scanners: ' + error.message, 'danger'); } } function selectScanner(scannerId) { window.selectedScanner = scannerId; document.getElementById('scanner-controls').style.display = 'block'; showAlert('Scanner selected: ' + scannerId, 'success'); } async function scanDocument() { if (!window.selectedScanner) { showAlert('Please select a scanner first', 'danger'); return; } const dpi = document.getElementById('scan-dpi').value; const colorMode = document.getElementById('scan-color').value; const paperSize = document.getElementById('scan-size').value; try { document.getElementById('scan-btn').disabled = true; document.getElementById('scan-btn').innerHTML = '<i class="fas fa-spinner fa-spin"></i> Scanning...'; const response = await fetch('/api/scan', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: new URLSearchParams({ device_id: window.selectedScanner, dpi: dpi, color_mode: colorMode, paper_size: paperSize }) }); const result = await response.json(); if (response.ok) { showAlert('Scan completed successfully', 'success'); // The scanned file would be available for processing console.log('Scanned file:', result); } else { throw new Error(result.detail || 'Scan failed'); } } catch (error) { console.error('Scan error:', error); showAlert('Scan failed: ' + error.message, 'danger'); } finally { document.getElementById('scan-btn').disabled = false; document.getElementById('scan-btn').innerHTML = '<i class="fas fa-camera"></i> Scan Document'; } } // Health and diagnostics async function checkHealth() { try { const response = await fetch('/api/health'); const data = await response.json(); const statusDiv = document.getElementById('system-status'); statusDiv.innerHTML = ` <p><strong>Server:</strong> ${data.status === 'healthy' ? '✅ Running' : '❌ Issues'}</p> <p><strong>MCP Connection:</strong> ${data.mcp_connected ? '✅ Connected' : '❌ Disconnected'}</p> <p><strong>Version:</strong> ${data.version}</p> `; } catch (error) { console.error('Health check error:', error); document.getElementById('system-status').innerHTML = '<p>❌ Unable to connect to server</p>'; } } async function checkBackends() { try { const response = await fetch('/api/backends'); const data = await response.json(); const backendDiv = document.getElementById('backend-status'); backendDiv.innerHTML = '<h4>Available OCR Backends:</h4>'; data.forEach(backend => { const status = backend.available ? '✅ Available' : '❌ Unavailable'; backendDiv.innerHTML += `<p><strong>${backend.name}:</strong> ${status}</p>`; }); } catch (error) { console.error('Backend check error:', error); document.getElementById('backend-status').innerHTML = '<p>❌ Unable to check backends</p>'; } } // Utility functions function formatFileSize(bytes) { if (bytes === 0) return '0 Bytes'; const k = 1024; const sizes = ['Bytes', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; } function downloadResult(format) { if (!currentResult) return; let content, filename, mimeType; switch (format) { case 'text': content = currentResult.text || ''; filename = 'ocr_result.txt'; mimeType = 'text/plain'; break; case 'json': content = JSON.stringify(currentResult, null, 2); filename = 'ocr_result.json'; mimeType = 'application/json'; break; default: return; } const blob = new Blob([content], { type: mimeType }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } function copyToClipboard() { if (!currentResult || !currentResult.text) return; navigator.clipboard.writeText(currentResult.text).then(() => { showAlert('Text copied to clipboard!', 'success'); }).catch(() => { // Fallback for older browsers const textArea = document.createElement('textarea'); textArea.value = currentResult.text; document.body.appendChild(textArea); textArea.select(); document.execCommand('copy'); document.body.removeChild(textArea); showAlert('Text copied to clipboard!', 'success'); }); } function showAlert(message, type) { // Create alert element const alert = document.createElement('div'); alert.className = `alert alert-${type}`; alert.style.cssText = ` position: fixed; top: 20px; right: 20px; padding: 15px 20px; border-radius: 6px; color: white; font-weight: 500; z-index: 1000; max-width: 400px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); `; // Set colors based on type const colors = { success: '#059669', danger: '#dc2626', warning: '#d97706', info: '#2563eb' }; alert.style.backgroundColor = colors[type] || colors.info; alert.textContent = message; document.body.appendChild(alert); // Remove after 5 seconds setTimeout(() => { if (alert.parentNode) { alert.parentNode.removeChild(alert); } }, 5000); } // Initialize on page load checkHealth(); checkBackends();

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/sandraschi/ocr-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server