Spaces:
Sleeping
Sleeping
| const dropzone = document.getElementById('dropzone'); | |
| const fileInput = document.getElementById('fileInput'); | |
| const scanStage = document.getElementById('scanStage'); | |
| const previewImg = document.getElementById('previewImg'); | |
| const scanLabel = document.getElementById('scanLabel'); | |
| const rescanBtn = document.getElementById('rescanBtn'); | |
| const result = document.getElementById('result'); | |
| const errorBox = document.getElementById('errorBox'); | |
| const verdictIcon = document.getElementById('verdictIcon'); | |
| const verdictLabel = document.getElementById('verdictLabel'); | |
| const verdictSub = document.getElementById('verdictSub'); | |
| const realPct = document.getElementById('realPct'); | |
| const screenPct = document.getElementById('screenPct'); | |
| const meterFillReal = document.getElementById('meterFillReal'); | |
| const meterFillScreen = document.getElementById('meterFillScreen'); | |
| const MIN_SCAN_MS = 1700; | |
| const SCAN_MESSAGES = ['Reading pixels…', 'Checking bezel shadows…', 'Measuring color temperature…', 'Scoring saturation profile…']; | |
| dropzone.addEventListener('click', () => fileInput.click()); | |
| dropzone.addEventListener('dragover', (e) => { e.preventDefault(); dropzone.classList.add('drag-over'); }); | |
| dropzone.addEventListener('dragleave', () => dropzone.classList.remove('drag-over')); | |
| dropzone.addEventListener('drop', (e) => { | |
| e.preventDefault(); | |
| dropzone.classList.remove('drag-over'); | |
| if (e.dataTransfer.files.length) handleFile(e.dataTransfer.files[0]); | |
| }); | |
| fileInput.addEventListener('change', () => { | |
| if (fileInput.files.length) handleFile(fileInput.files[0]); | |
| }); | |
| rescanBtn.addEventListener('click', resetUI); | |
| function resetUI() { | |
| fileInput.value = ''; | |
| dropzone.classList.remove('hidden'); | |
| scanStage.classList.add('hidden'); | |
| scanStage.classList.remove('scanning'); | |
| rescanBtn.classList.add('hidden'); | |
| result.classList.add('hidden'); | |
| errorBox.classList.add('hidden'); | |
| meterFillReal.style.width = '0%'; | |
| meterFillScreen.style.width = '0%'; | |
| } | |
| function handleFile(file) { | |
| if (!file.type.startsWith('image/')) { | |
| showError('That file does not look like an image.'); | |
| return; | |
| } | |
| const url = URL.createObjectURL(file); | |
| previewImg.src = url; | |
| dropzone.classList.add('hidden'); | |
| scanStage.classList.remove('hidden'); | |
| scanStage.classList.add('scanning'); | |
| rescanBtn.classList.add('hidden'); | |
| result.classList.add('hidden'); | |
| errorBox.classList.add('hidden'); | |
| let msgIdx = 0; | |
| scanLabel.textContent = SCAN_MESSAGES[0]; | |
| const msgTimer = setInterval(() => { | |
| msgIdx = (msgIdx + 1) % SCAN_MESSAGES.length; | |
| scanLabel.textContent = SCAN_MESSAGES[msgIdx]; | |
| }, 600); | |
| const started = Date.now(); | |
| const formData = new FormData(); | |
| formData.append('file', file); | |
| fetch('/api/predict', { method: 'POST', body: formData }) | |
| .then(async (res) => { | |
| const body = await res.json().catch(() => ({})); | |
| if (!res.ok) throw new Error(body.detail || 'Prediction failed.'); | |
| return body; | |
| }) | |
| .then((data) => { | |
| const elapsed = Date.now() - started; | |
| const wait = Math.max(0, MIN_SCAN_MS - elapsed); | |
| setTimeout(() => { | |
| clearInterval(msgTimer); | |
| finishScan(); | |
| showResult(data); | |
| }, wait); | |
| }) | |
| .catch((err) => { | |
| clearInterval(msgTimer); | |
| const elapsed = Date.now() - started; | |
| const wait = Math.max(0, 600 - elapsed); | |
| setTimeout(() => { | |
| finishScan(); | |
| showError(err.message || 'Something went wrong while analyzing the image.'); | |
| }, wait); | |
| }); | |
| } | |
| function finishScan() { | |
| scanStage.classList.remove('scanning'); | |
| rescanBtn.classList.remove('hidden'); | |
| } | |
| function showResult(data) { | |
| result.classList.remove('hidden'); | |
| const isScreen = data.label === 'screen_recapture'; | |
| verdictIcon.className = 'verdict-icon ' + (isScreen ? 'screen' : 'real'); | |
| verdictIcon.textContent = isScreen ? '⚠' : '✓'; | |
| verdictLabel.className = 'verdict-label ' + (isScreen ? 'screen' : 'real'); | |
| verdictLabel.textContent = isScreen ? 'Screen Recapture' : 'Real Photo'; | |
| verdictSub.textContent = `${data.confidence}% confidence`; | |
| realPct.textContent = `${data.real_pct}%`; | |
| screenPct.textContent = `${data.screen_pct}%`; | |
| requestAnimationFrame(() => { | |
| meterFillReal.style.width = `${data.real_pct}%`; | |
| meterFillScreen.style.width = `${data.screen_pct}%`; | |
| }); | |
| } | |
| function showError(message) { | |
| result.classList.remove('hidden'); | |
| errorBox.textContent = message; | |
| errorBox.classList.remove('hidden'); | |
| verdictIcon.className = 'verdict-icon screen'; | |
| verdictIcon.textContent = '!'; | |
| verdictLabel.className = 'verdict-label screen'; | |
| verdictLabel.textContent = 'Could not analyze image'; | |
| verdictSub.textContent = ''; | |
| realPct.textContent = '0%'; | |
| screenPct.textContent = '0%'; | |
| meterFillReal.style.width = '0%'; | |
| meterFillScreen.style.width = '0%'; | |
| } | |