| document.addEventListener('DOMContentLoaded', () => { |
| const API = window.location.origin; |
|
|
| |
| 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); |
| } |
|
|
| |
| 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); |
|
|
| |
| 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'); |
| }); |
|
|
| |
| 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(); |
| }); |
| }); |
|
|
| |
| 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 = '<option value="">β Choose a pair β</option>'; |
| 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(); }); |
| }); |
|
|
| |
| 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); |
|
|
| |
| 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; }); |
| }); |
|
|
| |
| 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'); |
|
|
| |
| 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; |
|
|
| |
| 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, |
| 0, 0, cropW, cropH |
| ); |
|
|
| 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(); |
| } |
|
|
| |
| 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); |
| } |
|
|
| |
| 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'); |
|
|
| |
| 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]; |
|
|
| |
| 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 || 'β'; |
|
|
| |
| 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); |
| }); |
| }); |
|
|
| |
| document.querySelector('.pre-tab[data-pre="original"]').classList.add('active'); |
| setPreview('original'); |
| } |
|
|
| |
| 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'); }); |
|
|
| |
| if ('serviceWorker' in navigator) { |
| navigator.serviceWorker.register('/sw.js').catch(() => { }); |
| } |
| }); |