Spaces:
Running
Running
Download index.html from akbted/docclassifier-demo: direct link, hf CLI and curl.
- Browser
- Download file 33.4 kB
-
https://huggingface.co/spaces/akbted/docclassifier-demo/resolve/main/index.html
- Command line
-
hf download hf://spaces/akbted/docclassifier-demo/index.html
-
curl -L -o index.html https://huggingface.co/spaces/akbted/docclassifier-demo/resolve/main/index.html
33.4 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Healthcare Document Classifier</title> | |
| <!-- Tailwind CSS --> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <!-- Lucide Icons --> | |
| <script src="https://unpkg.com/lucide@latest"></script> | |
| <!-- PDF.js: renders PDFs to canvas (works inside sandboxed iframes) --> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script> | |
| <!-- Google Fonts: Inter --> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet"> | |
| <script> | |
| tailwind.config = { | |
| theme: { | |
| extend: { | |
| fontFamily: { | |
| sans: ['Inter', 'sans-serif'], | |
| }, | |
| colors: { | |
| primary: { | |
| 50: '#f0fdf4', | |
| 100: '#dcfce7', | |
| 500: '#22c55e', | |
| 600: '#16a34a', | |
| 700: '#15803d', | |
| } | |
| } | |
| } | |
| } | |
| } | |
| </script> | |
| <style> | |
| /* Custom Scrollbar */ | |
| ::-webkit-scrollbar { | |
| width: 8px; | |
| height: 8px; | |
| } | |
| ::-webkit-scrollbar-track { | |
| background: #f1f5f9; | |
| } | |
| ::-webkit-scrollbar-thumb { | |
| background: #cbd5e1; | |
| border-radius: 4px; | |
| } | |
| ::-webkit-scrollbar-thumb:hover { | |
| background: #94a3b8; | |
| } | |
| .drag-active { | |
| border-color: #22c55e ; | |
| background-color: #f0fdf4 ; | |
| } | |
| #preview-container { | |
| position: absolute; | |
| inset: 0; | |
| z-index: 5; | |
| background: #e2e8f0; | |
| } | |
| #preview-container img { | |
| max-width: 100%; | |
| max-height: 100%; | |
| object-fit: contain; | |
| } | |
| #preview-container iframe { | |
| width: 100%; | |
| height: 100%; | |
| border: none; | |
| border-radius: 0.5rem; | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-slate-50 text-slate-800 h-screen flex flex-col font-sans overflow-hidden"> | |
| <!-- Header --> | |
| <header class="bg-white border-b border-slate-200 px-6 py-4 flex items-center justify-between shrink-0 z-10 shadow-sm"> | |
| <div class="flex items-center gap-3"> | |
| <div class="bg-primary-100 p-2 rounded-lg text-primary-600"> | |
| <i data-lucide="file-check-2" class="w-6 h-6"></i> | |
| </div> | |
| <div> | |
| <h1 class="font-semibold text-lg leading-tight">Med Doc Classifier</h1> | |
| <p class="text-xs text-slate-500">Advanced Classification Module</p> | |
| </div> | |
| </div> | |
| <div class="flex items-center gap-4"> | |
| <div class="flex items-center gap-2 text-sm text-slate-600 bg-slate-100 px-3 py-1.5 rounded-md border border-slate-200"> | |
| <i data-lucide="server" class="w-4 h-4"></i> | |
| <input type="text" id="api-url" value="https://78c9-103-64-129-250.ngrok-free.app/classifier/api/v1" class="bg-transparent border-none outline-none w-48 text-slate-700 font-mono text-xs" title="API Base URL"> | |
| </div> | |
| <div class="flex items-center gap-2 text-sm text-slate-600 bg-slate-100 px-3 py-1.5 rounded-md border border-slate-200"> | |
| <i data-lucide="key-round" class="w-4 h-4"></i> | |
| <input type="password" id="api-key" value="dc_aa4bc757ae2869a9bdd91b316b8cf748a86a3bf66e6954b854c185ae12cad4e2" class="bg-transparent border-none outline-none w-40 text-slate-700 font-mono text-xs" title="API Key (masked)"> | |
| </div> | |
| <label class="flex items-center gap-2 cursor-pointer text-sm"> | |
| <input type="checkbox" id="mock-mode" class="rounded text-primary-600 focus:ring-primary-500" checked> | |
| <span class="text-slate-600">Test Mode (Mock API)</span> | |
| </label> | |
| </div> | |
| </header> | |
| <!-- Navigation Tabs --> | |
| <div class="bg-white border-b border-slate-200 px-6 flex items-center gap-2 shrink-0"> | |
| <button id="tab-single" onclick="setAppMode('single')" class="px-4 py-3 text-sm font-semibold border-b-2 border-primary-600 text-primary-600 flex items-center gap-2 transition-all"> | |
| <i data-lucide="file-text" class="w-4.5 h-4.5"></i> | |
| Single File | |
| </button> | |
| <button id="tab-batch" onclick="setAppMode('batch')" class="px-4 py-3 text-sm font-semibold border-b-2 border-transparent text-slate-500 hover:text-slate-800 flex items-center gap-2 transition-all"> | |
| <i data-lucide="files" class="w-4.5 h-4.5"></i> | |
| Batch Processing | |
| <span id="batch-count-badge" class="hidden bg-slate-100 border border-slate-200 text-slate-700 text-[10px] font-bold px-1.5 py-0.5 rounded-full">0</span> | |
| </button> | |
| </div> | |
| <!-- Main Content Split View --> | |
| <main class="flex-grow flex flex-col lg:flex-row overflow-hidden"> | |
| <!-- Left Pane: Document Viewer --> | |
| <section class="lg:w-3/5 w-full border-r border-slate-200 bg-slate-100 flex flex-col relative"> | |
| <!-- Upload Overlay (hidden when file loaded) --> | |
| <div id="upload-zone" class="absolute inset-0 z-10 bg-slate-50 p-6 flex flex-col items-center justify-center transition-colors border-2 border-dashed border-slate-300 m-4 rounded-xl"> | |
| <div class="bg-white p-4 rounded-full shadow-sm mb-4 text-primary-500"> | |
| <i data-lucide="upload-cloud" class="w-8 h-8"></i> | |
| </div> | |
| <h3 id="upload-title" class="text-lg font-medium text-slate-700 mb-1">Upload Document</h3> | |
| <p id="upload-description" class="text-sm text-slate-500 mb-6 text-center max-w-sm">Drag and drop your PDF or Image file here, or click to browse.</p> | |
| <input type="file" id="file-input" class="hidden" accept=".pdf,image/png,image/jpeg,image/jpg"> | |
| <button onclick="document.getElementById('file-input').click()" class="bg-white border border-slate-200 shadow-sm text-slate-700 px-6 py-2.5 rounded-lg font-medium hover:bg-slate-50 transition-colors flex items-center gap-2"> | |
| <i data-lucide="folder-search" class="w-4 h-4"></i> | |
| Browse Files | |
| </button> | |
| <p class="text-xs text-slate-400 mt-4">Supported formats: PDF, PNG, JPEG</p> | |
| </div> | |
| <!-- Header above viewer (shown when file loaded) --> | |
| <div id="viewer-header" class="hidden bg-white/80 backdrop-blur border-b border-slate-200 p-3 flex justify-between items-center absolute top-0 w-full z-20"> | |
| <div class="flex items-center gap-2 truncate max-w-md"> | |
| <i data-lucide="file-text" class="w-4 h-4 text-slate-400 shrink-0"></i> | |
| <span id="file-name-display" class="text-sm font-medium text-slate-700 truncate">document.pdf</span> | |
| <span id="file-size-display" class="text-xs text-slate-400 ml-2 shrink-0"></span> | |
| </div> | |
| <button onclick="resetApp()" class="text-xs text-red-600 hover:text-red-700 bg-red-50 hover:bg-red-100 px-3 py-1.5 rounded-md font-medium transition-colors flex items-center gap-1"> | |
| <i data-lucide="x" class="w-3 h-3"></i> Clear | |
| </button> | |
| </div> | |
| <!-- Multi-file Tabs (shows up below the viewer header only when 2+ files are uploaded) --> | |
| <div id="multi-file-tabs" class="hidden absolute top-[52px] left-0 right-0 bg-slate-50/90 backdrop-blur border-b border-slate-200 px-3 py-2 flex gap-2 overflow-x-auto z-10 shadow-sm"> | |
| <!-- Tab items injected dynamically --> | |
| </div> | |
| <!-- Actual Preview Container --> | |
| <div id="preview-container" class="flex-grow w-full h-full p-4 pt-16 overflow-auto flex flex-col items-center hidden"> | |
| <!-- Image or iframe injected here --> | |
| </div> | |
| </section> | |
| <!-- Right Pane: Controls & Results --> | |
| <section class="lg:w-2/5 w-full bg-white flex flex-col overflow-hidden shadow-[-4px_0_15px_-3px_rgba(0,0,0,0.05)] z-20 relative"> | |
| <div class="p-6 border-b border-slate-100 shrink-0"> | |
| <h2 id="action-title" class="text-lg font-semibold text-slate-800 mb-1">Analysis Action</h2> | |
| <p id="action-desc" class="text-sm text-slate-500 mb-5">Run the classifier to identify the document type.</p> | |
| <button id="analyze-btn" disabled class="w-full bg-primary-600 hover:bg-primary-700 disabled:bg-slate-200 disabled:text-slate-400 disabled:cursor-not-allowed text-white font-medium py-3 px-4 rounded-lg shadow-sm transition-all flex justify-center items-center gap-2"> | |
| <i data-lucide="scan" class="w-5 h-5"></i> | |
| <span id="analyze-btn-text">Classify Document</span> | |
| </button> | |
| </div> | |
| <div class="flex-grow overflow-y-auto p-6 bg-slate-50/50"> | |
| <!-- Empty State --> | |
| <div id="results-empty" class="h-full flex flex-col items-center justify-center text-slate-400"> | |
| <i data-lucide="layout-template" class="w-12 h-12 mb-3 opacity-50"></i> | |
| <p class="text-sm">Results will appear here</p> | |
| </div> | |
| <!-- Loading State --> | |
| <div id="results-loading" class="hidden h-full flex flex-col items-center justify-center text-primary-600"> | |
| <i data-lucide="loader-2" class="w-10 h-10 mb-4 animate-spin"></i> | |
| <p class="text-sm font-medium text-slate-600 animate-pulse">Running LayoutLMv3 + OCR inference...</p> | |
| </div> | |
| <!-- Results Content --> | |
| <div id="results-content" class="hidden flex flex-col gap-4"> | |
| <div class="flex items-center justify-between mb-2"> | |
| <h3 class="font-semibold text-slate-700 flex items-center gap-2"> | |
| <i data-lucide="check-circle-2" class="w-4 h-4 text-primary-500"></i> | |
| Classification Results | |
| </h3> | |
| </div> | |
| <div id="predictions-list" class="flex flex-col gap-3"> | |
| <!-- Result Cards Injected Here --> | |
| </div> | |
| <!-- Raw JSON Viewer Accordion --> | |
| <div class="mt-6 border border-slate-200 rounded-lg bg-white overflow-hidden"> | |
| <button onclick="toggleJson()" class="w-full px-4 py-3 bg-slate-50 flex justify-between items-center text-sm font-medium text-slate-600 hover:bg-slate-100 transition-colors"> | |
| <span class="flex items-center gap-2"><i data-lucide="code" class="w-4 h-4"></i> Raw API Response</span> | |
| <i data-lucide="chevron-down" id="json-chevron" class="w-4 h-4 transition-transform"></i> | |
| </button> | |
| <div id="json-container" class="hidden border-t border-slate-200"> | |
| <pre id="json-output" class="p-4 text-xs font-mono text-slate-700 overflow-x-auto bg-slate-900 !text-green-400 max-h-64 overflow-y-auto"></pre> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Error State --> | |
| <div id="error-state" class="hidden p-4 bg-red-50 border border-red-200 rounded-lg text-red-700 flex items-start gap-3 mt-4"> | |
| <i data-lucide="alert-triangle" class="w-5 h-5 shrink-0 mt-0.5 text-red-500"></i> | |
| <div> | |
| <h4 class="font-medium text-sm mb-1">Classification Failed</h4> | |
| <p id="error-message" class="text-xs opacity-90">Cannot connect to the server.</p> | |
| </div> | |
| </div> | |
| </div> | |
| </section> | |
| </main> | |
| <!-- Footer --> | |
| <footer class="bg-white border-t border-slate-200 p-3 text-center shrink-0 z-20"> | |
| <p class="text-xs text-slate-500 font-medium flex items-center justify-center gap-1.5"> | |
| Developed by <span class="font-bold text-slate-800">Arca Ai</span> | |
| </p> | |
| <p class="text-[10px] text-slate-400 mt-1">Demo API token is shared and capped at 30 requests/minute; it expires or may be rotated. Contact ArcaAI for your own token.</p> | |
| </footer> | |
| <!-- Application Logic --> | |
| <script> | |
| // Initialize Icons (non-fatal if the CDN is unreachable) | |
| function initIcons(root) { | |
| try { | |
| if (typeof lucide !== 'undefined' && lucide.createIcons) { | |
| lucide.createIcons(root ? { root } : undefined); | |
| } | |
| } catch (e) { | |
| console.warn('lucide icons unavailable:', e); | |
| } | |
| } | |
| initIcons(); | |
| // Configure PDF.js worker | |
| if (typeof pdfjsLib !== 'undefined') { | |
| pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js'; | |
| } | |
| // Mode Navigation Tabs | |
| const tabSingle = document.getElementById('tab-single'); | |
| const tabBatch = document.getElementById('tab-batch'); | |
| const batchCountBadge = document.getElementById('batch-count-badge'); | |
| // DOM Elements | |
| const uploadZone = document.getElementById('upload-zone'); | |
| const uploadTitle = document.getElementById('upload-title'); | |
| const uploadDescription = document.getElementById('upload-description'); | |
| const fileInput = document.getElementById('file-input'); | |
| const analyzeBtn = document.getElementById('analyze-btn'); | |
| const analyzeBtnText = document.getElementById('analyze-btn-text'); | |
| const actionTitle = document.getElementById('action-title'); | |
| const actionDesc = document.getElementById('action-desc'); | |
| const previewContainer = document.getElementById('preview-container'); | |
| const viewerHeader = document.getElementById('viewer-header'); | |
| const multiFileTabs = document.getElementById('multi-file-tabs'); | |
| // Display elements | |
| const fileNameDisplay = document.getElementById('file-name-display'); | |
| const fileSizeDisplay = document.getElementById('file-size-display'); | |
| // Result views | |
| const resultsEmpty = document.getElementById('results-empty'); | |
| const resultsLoading = document.getElementById('results-loading'); | |
| const resultsContent = document.getElementById('results-content'); | |
| const errorState = document.getElementById('error-state'); | |
| const errorMessage = document.getElementById('error-message'); | |
| const predictionsList = document.getElementById('predictions-list'); | |
| const jsonOutput = document.getElementById('json-output'); | |
| // State | |
| let currentMode = 'single'; // 'single' or 'batch' | |
| let currentFiles = []; // array of File | |
| let objectUrl = null; | |
| let activeFileIndex = 0; | |
| // Configuration | |
| const MOCK_LABELS = ['bill', 'discharge_summary', 'echs_card', 'lab_report', 'prescription_slip', 'referral_letter_by_polyclinic']; | |
| // Mode Switching | |
| function setAppMode(mode) { | |
| currentMode = mode; | |
| // UI Visual Tab Toggle | |
| if (mode === 'single') { | |
| tabSingle.className = "px-4 py-3 text-sm font-semibold border-b-2 border-primary-600 text-primary-600 flex items-center gap-2 transition-all"; | |
| tabBatch.className = "px-4 py-3 text-sm font-semibold border-b-2 border-transparent text-slate-500 hover:text-slate-800 flex items-center gap-2 transition-all"; | |
| // Adjust File Input Type | |
| fileInput.removeAttribute('multiple'); | |
| // Adjust Upload UI instructions | |
| uploadTitle.textContent = "Upload Document"; | |
| uploadDescription.textContent = "Drag and drop your PDF or Image file here, or click to browse."; | |
| actionTitle.textContent = "Analysis Action"; | |
| actionDesc.textContent = "Run the classifier to identify the document type."; | |
| } else { | |
| tabSingle.className = "px-4 py-3 text-sm font-semibold border-b-2 border-transparent text-slate-500 hover:text-slate-800 flex items-center gap-2 transition-all"; | |
| tabBatch.className = "px-4 py-3 text-sm font-semibold border-b-2 border-primary-600 text-primary-600 flex items-center gap-2 transition-all"; | |
| // Adjust File Input Type | |
| fileInput.setAttribute('multiple', 'multiple'); | |
| // Adjust Upload UI instructions | |
| uploadTitle.textContent = "Upload Documents (Batch)"; | |
| uploadDescription.textContent = "Drag and drop one or multiple PDFs / Images here, or click to browse."; | |
| actionTitle.textContent = "Batch Action"; | |
| actionDesc.textContent = "Submit all files currently in the queue to LayoutLMv3 in parallel."; | |
| } | |
| // Always reset current inputs when switching modes to avoid configuration mismatch | |
| resetApp(); | |
| } | |
| // Format file size | |
| function formatBytes(bytes, decimals = 2) { | |
| if (bytes === 0) return '0 Bytes'; | |
| const k = 1024; | |
| const dm = decimals < 0 ? 0 : decimals; | |
| const sizes = ['Bytes', 'KB', 'MB', 'GB']; | |
| const i = Math.floor(Math.log(bytes) / Math.log(k)); | |
| return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; | |
| } | |
| // Drag and Drop Handlers | |
| ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { | |
| uploadZone.addEventListener(eventName, preventDefaults, false); | |
| }); | |
| function preventDefaults(e) { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| } | |
| ['dragenter', 'dragover'].forEach(eventName => { | |
| uploadZone.addEventListener(eventName, () => uploadZone.classList.add('drag-active'), false); | |
| }); | |
| ['dragleave', 'drop'].forEach(eventName => { | |
| uploadZone.addEventListener(eventName, () => uploadZone.classList.remove('drag-active'), false); | |
| }); | |
| uploadZone.addEventListener('drop', (e) => { | |
| preventDefaults(e); | |
| const dt = e.dataTransfer; | |
| if (!dt.files.length) return; | |
| handleFiles(dt.files); | |
| }); | |
| fileInput.addEventListener('change', (e) => { | |
| if (!e.target.files.length) return; | |
| handleFiles(e.target.files); | |
| }, false); | |
| // handle one or multiple files (File or FileList) | |
| function handleFiles(filesInput) { | |
| let files = filesInput instanceof FileList ? Array.from(filesInput) : [filesInput]; | |
| if (!files.length) return; | |
| // Enforcement of single file mode constraints | |
| if (currentMode === 'single') { | |
| files = [files[0]]; | |
| } | |
| const validTypes = ['application/pdf', 'image/jpeg', 'image/png', 'image/jpg']; | |
| const invalid = files.find(f => !validTypes.includes(f.type)); | |
| if (invalid) { | |
| alert("Please upload only PDF or Image (PNG/JPG) files."); | |
| return; | |
| } | |
| currentFiles = files; | |
| activeFileIndex = 0; | |
| uploadZone.classList.add('hidden'); | |
| viewerHeader.classList.remove('hidden'); | |
| previewContainer.classList.remove('hidden'); | |
| analyzeBtn.disabled = false; | |
| updateViewerHeader(); | |
| renderSelectedFiles(files); | |
| previewFile(activeFileIndex); | |
| showState('empty'); | |
| } | |
| function updateViewerHeader() { | |
| if (currentFiles.length === 1) { | |
| fileNameDisplay.textContent = currentFiles[0].name; | |
| fileSizeDisplay.textContent = formatBytes(currentFiles[0].size); | |
| multiFileTabs.classList.add('hidden'); | |
| previewContainer.classList.remove('pt-28'); | |
| previewContainer.classList.add('pt-16'); | |
| analyzeBtnText.textContent = "Classify Document"; | |
| batchCountBadge.classList.add('hidden'); | |
| } else { | |
| fileNameDisplay.textContent = `${currentFiles.length} files selected`; | |
| fileSizeDisplay.textContent = `Batch Processing`; | |
| multiFileTabs.classList.remove('hidden'); | |
| previewContainer.classList.remove('pt-16'); | |
| previewContainer.classList.add('pt-28'); | |
| analyzeBtnText.textContent = `Process Batch (${currentFiles.length})`; | |
| batchCountBadge.textContent = currentFiles.length; | |
| batchCountBadge.classList.remove('hidden'); | |
| } | |
| } | |
| function renderSelectedFiles(files) { | |
| multiFileTabs.innerHTML = ''; | |
| if (files.length <= 1) return; | |
| files.forEach((file, index) => { | |
| const isActive = index === activeFileIndex; | |
| const tab = document.createElement('button'); | |
| tab.className = `px-3 py-1.5 rounded-lg text-xs font-semibold transition-all flex items-center gap-1.5 shrink-0 border ${ | |
| isActive | |
| ? 'bg-primary-50 text-primary-700 border-primary-200 ring-1 ring-primary-200' | |
| : 'bg-white text-slate-600 border-slate-200 hover:bg-slate-50 shadow-sm' | |
| }`; | |
| const iconName = file.type === 'application/pdf' ? 'file-text' : 'image'; | |
| tab.innerHTML = `<i data-lucide="${iconName}" class="w-3.5 h-3.5"></i> <span class="truncate max-w-[120px]">${file.name}</span>`; | |
| tab.addEventListener('click', () => { | |
| activeFileIndex = index; | |
| previewFile(index); | |
| renderSelectedFiles(files); | |
| }); | |
| multiFileTabs.appendChild(tab); | |
| }); | |
| initIcons(multiFileTabs); | |
| } | |
| function previewFile(index) { | |
| const file = currentFiles[index]; | |
| if (!file) return; | |
| if (objectUrl) URL.revokeObjectURL(objectUrl); | |
| objectUrl = URL.createObjectURL(file); | |
| previewContainer.innerHTML = ''; | |
| if (file.type.startsWith('image/')) { | |
| const img = document.createElement('img'); | |
| img.src = objectUrl; | |
| img.alt = file.name; | |
| img.className = 'max-w-full max-h-full object-contain rounded-lg shadow-sm border border-slate-200 bg-white'; | |
| previewContainer.appendChild(img); | |
| } else if (file.type === 'application/pdf') { | |
| previewPdf(file); | |
| } | |
| } | |
| async function previewPdf(file) { | |
| try { | |
| if (typeof pdfjsLib === 'undefined') { | |
| throw new Error('PDF.js failed to load'); | |
| } | |
| const data = await file.arrayBuffer(); | |
| const pdf = await pdfjsLib.getDocument({ data }).promise; | |
| previewContainer.innerHTML = ''; | |
| const scale = 1.5; | |
| for (let i = 1; i <= pdf.numPages; i++) { | |
| const page = await pdf.getPage(i); | |
| const viewport = page.getViewport({ scale }); | |
| const canvas = document.createElement('canvas'); | |
| canvas.width = viewport.width; | |
| canvas.height = viewport.height; | |
| canvas.className = 'w-full h-auto rounded-lg shadow-sm border border-slate-200 bg-white mb-3'; | |
| const ctx = canvas.getContext('2d'); | |
| await page.render({ canvasContext: ctx, viewport }).promise; | |
| previewContainer.appendChild(canvas); | |
| } | |
| } catch (err) { | |
| console.error('PDF preview error:', err); | |
| previewContainer.innerHTML = | |
| `<p class="text-sm text-slate-500 italic p-4">PDF preview failed: ${err.message}</p>`; | |
| } | |
| } | |
| function resetApp() { | |
| currentFiles = []; | |
| activeFileIndex = 0; | |
| if (objectUrl) URL.revokeObjectURL(objectUrl); | |
| fileInput.value = ''; | |
| uploadZone.classList.remove('hidden'); | |
| viewerHeader.classList.add('hidden'); | |
| multiFileTabs.classList.add('hidden'); | |
| previewContainer.classList.add('hidden'); | |
| analyzeBtn.disabled = true; | |
| batchCountBadge.classList.add('hidden'); | |
| if (currentMode === 'single') { | |
| analyzeBtnText.textContent = "Classify Document"; | |
| } else { | |
| analyzeBtnText.textContent = "Process Batch (0)"; | |
| } | |
| showState('empty'); | |
| } | |
| function showState(state) { | |
| resultsEmpty.classList.add('hidden'); | |
| resultsLoading.classList.add('hidden'); | |
| resultsContent.classList.add('hidden'); | |
| errorState.classList.add('hidden'); | |
| if (state === 'empty') resultsEmpty.classList.remove('hidden'); | |
| if (state === 'loading') resultsLoading.classList.remove('hidden'); | |
| if (state === 'content') resultsContent.classList.remove('hidden'); | |
| if (state === 'error') errorState.classList.remove('hidden'); | |
| } | |
| function toggleJson() { | |
| const container = document.getElementById('json-container'); | |
| const chevron = document.getElementById('json-chevron'); | |
| container.classList.toggle('hidden'); | |
| chevron.style.transform = container.classList.contains('hidden') ? 'rotate(0deg)' : 'rotate(180deg)'; | |
| } | |
| // Label Color mapping helper | |
| function getLabelColorClass(label) { | |
| const l = label.toLowerCase(); | |
| if (l.includes('discharge')) return 'bg-blue-50 text-blue-700 border-blue-200 ring-blue-500/20'; | |
| if (l.includes('lab')) return 'bg-purple-50 text-purple-700 border-purple-200 ring-purple-500/20'; | |
| if (l.includes('bill')) return 'bg-emerald-50 text-emerald-700 border-emerald-200 ring-emerald-500/20'; | |
| if (l.includes('prescription')) return 'bg-amber-50 text-amber-700 border-amber-200 ring-amber-500/20'; | |
| return 'bg-slate-100 text-slate-700 border-slate-200 ring-slate-500/20'; | |
| } | |
| // API Interaction | |
| analyzeBtn.addEventListener('click', async () => { | |
| if (!currentFiles || currentFiles.length === 0) return; | |
| showState('loading'); | |
| analyzeBtn.disabled = true; | |
| const isMockMode = document.getElementById('mock-mode').checked; | |
| const baseUrl = document.getElementById('api-url').value.replace(/\/$/, ''); | |
| const apiKey = document.getElementById('api-key').value.trim(); | |
| try { | |
| let data; | |
| if (isMockMode) { | |
| await new Promise(r => setTimeout(r, 1500)); | |
| const mockResults = []; | |
| for (const file of currentFiles) { | |
| const numPages = file.type === 'application/pdf' ? Math.floor(Math.random() * 2) + 1 : 1; | |
| const docType = MOCK_LABELS[Math.floor(Math.random() * MOCK_LABELS.length)]; | |
| for (let i = 1; i <= numPages; i++) { | |
| mockResults.push({ | |
| file_path: `/temp/uploads/${file.name}`, | |
| page: i, | |
| label: docType, | |
| confidence: 0.85 + Math.random() * 0.14 | |
| }); | |
| } | |
| } | |
| data = mockResults; | |
| } else { | |
| const formData = new FormData(); | |
| const headers = { 'X-API-Key': apiKey }; | |
| if (currentFiles.length === 1) { | |
| formData.append('file', currentFiles[0]); | |
| const resp = await fetch(`${baseUrl}/predict/upload`, { method: 'POST', headers, body: formData }); | |
| if (!resp.ok) throw new Error(`HTTP ${resp.status}: ${resp.statusText}`); | |
| data = await resp.json(); | |
| } else { | |
| currentFiles.forEach(f => formData.append('files', f)); | |
| const resp = await fetch(`${baseUrl}/predict/upload/batch`, { method: 'POST', headers, body: formData }); | |
| if (!resp.ok) throw new Error(`HTTP ${resp.status}: ${resp.statusText}`); | |
| data = await resp.json(); | |
| } | |
| } | |
| renderResults(data); | |
| showState('content'); | |
| } catch (err) { | |
| console.error("Classification error:", err); | |
| errorMessage.textContent = err.message || "Failed to connect to the classification service."; | |
| showState('error'); | |
| } finally { | |
| analyzeBtn.disabled = false; | |
| } | |
| }); | |
| function renderResults(data) { | |
| predictionsList.innerHTML = ''; | |
| jsonOutput.textContent = JSON.stringify(data, null, 2); | |
| if (!Array.isArray(data) || data.length === 0) { | |
| predictionsList.innerHTML = '<p class="text-sm text-slate-500 italic">No predictions returned.</p>'; | |
| return; | |
| } | |
| data.forEach((pred, index) => { | |
| const confPercent = (pred.confidence * 100).toFixed(1); | |
| const colorClasses = getLabelColorClass(pred.label); | |
| // Construct card for each page prediction | |
| const card = document.createElement('div'); | |
| card.className = `p-4 rounded-xl border ${colorClasses.split(' ')[2]} bg-white shadow-sm ring-1 ${colorClasses.split(' ')[3]} relative overflow-hidden`; | |
| // subtle left border highlight | |
| const indicatorColor = colorClasses.split(' ')[1].replace('text-', 'bg-'); | |
| card.innerHTML = ` | |
| <div class="absolute left-0 top-0 bottom-0 w-1 ${indicatorColor}"></div> | |
| <div class="pl-2"> | |
| <div class="flex justify-between items-start mb-2"> | |
| <div> | |
| <span class="text-[10px] font-bold uppercase tracking-wider text-slate-400 mb-1 block">Predicted Class</span> | |
| <h4 class="text-lg font-bold ${colorClasses.split(' ')[1]} leading-tight">${pred.label}</h4> | |
| </div> | |
| <div class="flex flex-col items-end"> | |
| <span class="text-[10px] font-bold uppercase tracking-wider text-slate-400 mb-1">Confidence</span> | |
| <div class="bg-slate-100 px-2 py-1 rounded text-sm font-bold text-slate-700 flex items-center gap-1"> | |
| ${confPercent}% | |
| </div> | |
| </div> | |
| </div> | |
| <div class="flex items-center gap-4 mt-3 text-xs text-slate-500 border-t border-slate-100 pt-3"> | |
| <div class="flex items-center gap-1" title="Page Number"> | |
| <i data-lucide="file" class="w-3.5 h-3.5"></i> Page ${pred.page} | |
| </div> | |
| <div class="flex items-center gap-1 truncate" title="File Path"> | |
| <i data-lucide="hard-drive" class="w-3.5 h-3.5"></i> | |
| <span class="truncate max-w-[150px]">${pred.file_path.split('/').pop()}</span> | |
| </div> | |
| </div> | |
| <!-- Confidence Bar --> | |
| <div class="w-full bg-slate-100 rounded-full h-1.5 mt-3 overflow-hidden"> | |
| <div class="${indicatorColor} h-1.5 rounded-full transition-all duration-1000" style="width: 0%"></div> | |
| </div> | |
| </div> | |
| `; | |
| predictionsList.appendChild(card); | |
| // Re-init lucide icons for new elements | |
| initIcons(card); | |
| // Animate progress bar slightly after insertion | |
| setTimeout(() => { | |
| const bar = card.querySelector('.transition-all'); | |
| if(bar) bar.style.width = `${confPercent}%`; | |
| }, 50); | |
| }); | |
| } | |
| </script> | |
| </body> | |
| </html> |