document.addEventListener('DOMContentLoaded', () => { const API = window.location.origin; // ── Toast ──────────────────────────────────────────────── function toast(msg, type = 'info') { const el = document.createElement('div'); el.className = `toast ${type}`; el.textContent = msg; document.getElementById('toasts').appendChild(el); setTimeout(() => el.remove(), 3500); } // ── Backend status ─────────────────────────────────────── const dot = document.getElementById('connection-badge'); function checkStatus() { fetch(`${API}/api/status`) .then(() => { dot.className = 'status-dot online'; }) .catch(() => { dot.className = 'status-dot offline'; }); } checkStatus(); setInterval(checkStatus, 20000); // ── PWA install ────────────────────────────────────────── let installPrompt; const installBtn = document.getElementById('pwa-install-btn'); window.addEventListener('beforeinstallprompt', e => { e.preventDefault(); installPrompt = e; installBtn.classList.remove('hidden'); }); installBtn.addEventListener('click', async () => { if (!installPrompt) return; installPrompt.prompt(); const { outcome } = await installPrompt.userChoice; if (outcome === 'accepted') toast('App installed!', 'success'); installPrompt = null; installBtn.classList.add('hidden'); }); // ── Mode switching ─────────────────────────────────────── let currentMode = 'dataset'; let chassisBlob = null; let barcodeBlob = null; document.querySelectorAll('.seg-btn').forEach(btn => { btn.addEventListener('click', () => { document.querySelectorAll('.seg-btn').forEach(b => b.classList.remove('active')); btn.classList.add('active'); currentMode = btn.dataset.mode; ['dataset', 'upload', 'camera'].forEach(m => document.getElementById(`mode-${m}`).classList.add('hidden') ); document.getElementById(`mode-${currentMode}`).classList.remove('hidden'); stopAllCameras(); validate(); }); }); // ── Dataset pairs ──────────────────────────────────────── const pairSelect = document.getElementById('dataset-pair-select'); const barcodeValInput = document.getElementById('barcode-val'); fetch(`${API}/api/test-pairs`) .then(r => r.json()) .then(pairs => { pairSelect.innerHTML = ''; pairs.forEach(p => { const o = document.createElement('option'); o.value = o.textContent = p; pairSelect.appendChild(o); }); }) .catch(() => toast('Could not load dataset pairs', 'error')); pairSelect.addEventListener('change', () => { const key = pairSelect.value; if (!key) { barcodeValInput.value = ''; validate(); return; } barcodeValInput.value = 'Decoding…'; barcodeValInput.disabled = true; fetch(`${API}/api/scan-barcode/${encodeURIComponent(key)}`) .then(r => r.json()) .then(d => { barcodeValInput.value = d.success ? d.barcode : ''; if (!d.success) toast('Barcode not decoded. Enter manually.', 'info'); }) .catch(() => { barcodeValInput.value = ''; toast('Barcode scan failed', 'error'); }) .finally(() => { barcodeValInput.disabled = false; validate(); }); }); // ── Upload dropzones ───────────────────────────────────── function setupDropzone(dzId, inputId, nameId, isBarcode) { const dz = document.getElementById(dzId); const inp = document.getElementById(inputId); const nm = document.getElementById(nameId); dz.addEventListener('click', () => inp.click()); dz.addEventListener('dragover', e => { e.preventDefault(); dz.classList.add('active'); }); dz.addEventListener('dragleave', () => dz.classList.remove('active')); dz.addEventListener('drop', e => { e.preventDefault(); dz.classList.remove('active'); if (e.dataTransfer.files[0]) handleFile(e.dataTransfer.files[0], nm, isBarcode); }); inp.addEventListener('change', () => { if (inp.files[0]) handleFile(inp.files[0], nm, isBarcode); }); } function handleFile(file, nameEl, isBarcode) { nameEl.textContent = file.name; if (isBarcode) { barcodeBlob = file; barcodeValInput.value = 'Decoding…'; barcodeValInput.disabled = true; const fd = new FormData(); fd.append('file', file); fetch(`${API}/api/scan-barcode`, { method: 'POST', body: fd }) .then(r => r.json()) .then(d => { barcodeValInput.value = d.success ? d.barcode : ''; if (d.success) toast('Barcode decoded', 'success'); else toast('Could not decode barcode. Enter manually.', 'info'); }) .catch(() => { barcodeValInput.value = ''; toast('Barcode scan error', 'error'); }) .finally(() => { barcodeValInput.disabled = false; validate(); }); } else { chassisBlob = file; validate(); } } setupDropzone('barcode-dropzone', 'barcode-file', 'barcode-file-name', true); setupDropzone('chassis-dropzone', 'chassis-file', 'chassis-file-name', false); // ── Camera — barcode ───────────────────────────────────── let qr = null; const startBarcodeCam = document.getElementById('start-barcode-cam'); const camStep2 = document.getElementById('cam-step-2'); startBarcodeCam.addEventListener('click', () => { if (qr) { qr.stop().finally(() => { qr = null; startBarcodeCam.textContent = 'Start Camera'; }); return; } startBarcodeCam.textContent = 'Stop'; qr = new Html5Qrcode('barcode-reader'); qr.start( { facingMode: 'environment' }, { fps: 10, qrbox: { width: 240, height: 100 } }, decoded => { barcodeValInput.value = decoded; toast(`Barcode: ${decoded}`, 'success'); qr.stop().finally(() => { qr = null; startBarcodeCam.textContent = 'Start Camera'; camStep2.classList.remove('disabled'); document.getElementById('start-chassis-cam').disabled = false; validate(); }); }, () => { } ).catch(e => { toast(`Camera error: ${e}`, 'error'); startBarcodeCam.textContent = 'Start Camera'; qr = null; }); }); // ── Camera — chassis ───────────────────────────────────── let chassisStream = null; const chassisVideo = document.getElementById('chassis-video'); const chassisCanvas = document.getElementById('chassis-canvas'); const snapContainer = document.getElementById('chassis-snap-container'); const snapImg = document.getElementById('chassis-snap-img'); const startChassisCam = document.getElementById('start-chassis-cam'); const captureBtn = document.getElementById('capture-chassis'); const retakeBtn = document.getElementById('reset-chassis-snap'); const guideBox = document.getElementById('chassis-guide-box'); // Guide box region as fractions of the video frame (matches CSS: 70% wide, 22% tall, centered) const GUIDE_BOX_FRACTION = { widthFrac: 0.70, heightFrac: 0.22, xFrac: 0.15, yFrac: 0.39 }; startChassisCam.addEventListener('click', async () => { if (chassisStream) { stopChassisCamera(); return; } try { chassisStream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment', width: { ideal: 1280 } }, audio: false }); chassisVideo.srcObject = chassisStream; chassisVideo.classList.remove('hidden'); guideBox.classList.remove('hidden'); captureBtn.classList.remove('hidden'); snapContainer.classList.add('hidden'); retakeBtn.classList.add('hidden'); startChassisCam.textContent = 'Stop'; } catch (e) { toast(`Camera error: ${e.message}`, 'error'); } }); captureBtn.addEventListener('click', () => { const vw = chassisVideo.videoWidth; const vh = chassisVideo.videoHeight; // Crop to just the guide box region so OCR only sees the framed chassis strip const cropX = vw * GUIDE_BOX_FRACTION.xFrac; const cropY = vh * GUIDE_BOX_FRACTION.yFrac; const cropW = vw * GUIDE_BOX_FRACTION.widthFrac; const cropH = vh * GUIDE_BOX_FRACTION.heightFrac; chassisCanvas.width = cropW; chassisCanvas.height = cropH; chassisCanvas.getContext('2d').drawImage( chassisVideo, cropX, cropY, cropW, cropH, // source rect (the guide box area) 0, 0, cropW, cropH // destination rect (full canvas) ); chassisCanvas.toBlob(blob => { chassisBlob = blob; snapImg.src = URL.createObjectURL(blob); chassisVideo.classList.add('hidden'); guideBox.classList.add('hidden'); captureBtn.classList.add('hidden'); snapContainer.classList.remove('hidden'); retakeBtn.classList.remove('hidden'); stopChassisCamera(); validate(); }, 'image/jpeg'); }); retakeBtn.addEventListener('click', () => { chassisBlob = null; snapContainer.classList.add('hidden'); retakeBtn.classList.add('hidden'); startChassisCam.click(); validate(); }); function stopChassisCamera() { if (chassisStream) { chassisStream.getTracks().forEach(t => t.stop()); chassisStream = null; } chassisVideo.srcObject = null; chassisVideo.classList.add('hidden'); guideBox.classList.add('hidden'); captureBtn.classList.add('hidden'); startChassisCam.textContent = 'Start Camera'; } function stopAllCameras() { if (qr) { qr.stop().catch(() => { }); qr = null; startBarcodeCam.textContent = 'Start Camera'; } stopChassisCamera(); } // ── Validation ─────────────────────────────────────────── const runBtn = document.getElementById('run-ocr-btn'); barcodeValInput.addEventListener('input', validate); function validate() { const hasBarcode = barcodeValInput.value.trim() !== '' && barcodeValInput.value !== 'Decoding…'; let hasChassis = false; if (currentMode === 'dataset') hasChassis = pairSelect.value !== ''; else hasChassis = chassisBlob !== null; runBtn.disabled = !(hasBarcode && hasChassis); } // ── Run OCR ────────────────────────────────────────────── let lastResult = null; runBtn.addEventListener('click', () => { runBtn.disabled = true; runBtn.querySelector('.spinner').classList.remove('hidden'); const fd = new FormData(); fd.append('barcode_val', barcodeValInput.value.trim().toUpperCase()); if (currentMode === 'dataset') { fd.append('chassis_key', pairSelect.value); } else { fd.append('chassis_file', chassisBlob, currentMode === 'camera' ? 'chassis.jpg' : chassisBlob.name); } fetch(`${API}/api/match`, { method: 'POST', body: fd }) .then(r => { if (!r.ok) return r.json().then(e => { throw new Error(e.detail); }); return r.json(); }) .then(data => { lastResult = data; renderResult(data); toast('Scan complete', 'success'); }) .catch(e => toast(e.message || 'OCR failed', 'error')) .finally(() => { runBtn.disabled = false; runBtn.querySelector('.spinner').classList.add('hidden'); }); }); function renderResult(d) { document.getElementById('result-empty').classList.add('hidden'); document.getElementById('result-content').classList.remove('hidden'); // Banner const banner = document.getElementById('result-banner'); banner.className = `result-banner ${d.status}`; const icons = { EXACT: '✓', CORRECTED: '~', FAILED: '✗' }; const subs = { EXACT: 'Raw OCR matched the barcode exactly.', CORRECTED: 'Match achieved after error-correction.', FAILED: 'OCR output could not be matched.' }; document.getElementById('banner-icon').textContent = icons[d.status]; document.getElementById('banner-title').textContent = d.status === 'EXACT' ? 'Exact Match' : d.status === 'CORRECTED' ? 'Corrected Match' : 'No Match'; document.getElementById('banner-sub').textContent = subs[d.status]; // Table document.getElementById('res-expected').textContent = d.barcode_val; document.getElementById('res-raw').textContent = d.raw_ocr || '—'; document.getElementById('res-corrected').textContent = d.corrected_ocr || '—'; document.getElementById('res-conf').textContent = `${Math.round(d.confidence * 100)}%`; document.getElementById('res-filter').textContent = d.winning_label || '—'; // Preprocessing carousel const tabs = document.querySelectorAll('.pre-tab'); tabs.forEach(t => t.replaceWith(t.cloneNode(true))); const newTabs = document.querySelectorAll('.pre-tab'); function setPreview(type) { const img = document.getElementById('pre-img'); const lbl = document.getElementById('pre-ocr-text'); if (type === 'original') { img.src = `data:image/jpeg;base64,${d.original}`; lbl.textContent = d.raw_ocr || '—'; } else { const v = d.variations.find(x => x.label.toLowerCase() === type); const det = d.variation_details.find(x => x.label.toLowerCase() === type); if (v) img.src = `data:image/jpeg;base64,${v.base64}`; lbl.textContent = det ? (det.text || '—') : '—'; } } newTabs.forEach(tab => { tab.addEventListener('click', () => { newTabs.forEach(t => t.classList.remove('active')); tab.classList.add('active'); setPreview(tab.dataset.pre); }); }); // Set initial to original document.querySelector('.pre-tab[data-pre="original"]').classList.add('active'); setPreview('original'); } // ── Modal ──────────────────────────────────────────────── const modal = document.getElementById('modal'); const modalImg = document.getElementById('modal-img'); const modalTitle = document.getElementById('modal-title'); function openModal(url, title) { modalTitle.textContent = title; modalImg.src = `${url}?t=${Date.now()}`; modal.classList.remove('hidden'); } document.getElementById('modal-close').addEventListener('click', () => modal.classList.add('hidden')); modal.addEventListener('click', e => { if (e.target === modal) modal.classList.add('hidden'); }); // ── Service Worker ─────────────────────────────────────── if ('serviceWorker' in navigator) { navigator.serviceWorker.register('/sw.js').catch(() => { }); } });