docclassifier-demo / index.html
akbted's picture
Upload index.html with huggingface_hub
703fe26 verified
Raw History Blame Contribute Delete
33.4 kB
<!DOCTYPE html>
<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 !important;
background-color: #f0fdf4 !important;
}
#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>