/* ═══════════════════════════════════════════════════════════════ ImgAuth AI — Frontend Logic ─ Binary verdict: Likely AI-Generated vs Likely Authentic ─ Bug fix: result section scrolls into view after analysis ═══════════════════════════════════════════════════════════════ */ // ── Theme Toggle ────────────────────────────────────────────────── (function () { const html = document.documentElement; const btn = document.querySelector('[data-theme-toggle]'); let theme = localStorage.getItem('imgauth-theme') || 'dark'; html.setAttribute('data-theme', theme); if (btn) btn.textContent = theme === 'dark' ? '🌙' : '☀️'; if (btn) { btn.addEventListener('click', () => { theme = theme === 'dark' ? 'light' : 'dark'; html.setAttribute('data-theme', theme); btn.textContent = theme === 'dark' ? '🌙' : '☀️'; localStorage.setItem('imgauth-theme', theme); }); } })(); // ── Element Refs ────────────────────────────────────────────────── const fileInput = document.getElementById('fileInput'); const dropZone = document.getElementById('dropZone'); const previewZone = document.getElementById('previewZone'); const previewImg = document.getElementById('previewImg'); const previewFilename = document.getElementById('previewFilename'); const analyzeBtn = document.getElementById('analyzeBtn'); const clearBtn = document.getElementById('clearBtn'); const loadingOverlay = document.getElementById('loadingOverlay'); const resultSection = document.getElementById('resultSection'); const newScanBtn = document.getElementById('newScanBtn'); let selectedFile = null; // ── File Handling ───────────────────────────────────────────────── fileInput.addEventListener('change', e => handleFile(e.target.files[0])); 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'); const f = e.dataTransfer.files[0]; if (f) handleFile(f); }); function handleFile(file) { if (!file || !file.type.startsWith('image/')) { showToast('Please select a valid image file (PNG, JPG, WEBP).', 'error'); return; } if (file.size > 10 * 1024 * 1024) { showToast('File too large. Maximum size is 10 MB.', 'error'); return; } selectedFile = file; const reader = new FileReader(); reader.onload = ev => { previewImg.src = ev.target.result; previewFilename.textContent = file.name; dropZone.style.display = 'none'; previewZone.style.display = 'block'; resultSection.style.display = 'none'; }; reader.readAsDataURL(file); } clearBtn.addEventListener('click', resetToUpload); // ── Analyze ─────────────────────────────────────────────────────── analyzeBtn.addEventListener('click', async () => { if (!selectedFile) return; showLoading(); try { const form = new FormData(); form.append('file', selectedFile); const res = await fetch('/api/detect', { method: 'POST', body: form }); const data = await res.json(); if (!res.ok) throw new Error(data.detail || 'Server error'); hideLoading(); showResult(data); } catch (err) { hideLoading(); showToast('Error: ' + err.message, 'error'); } }); newScanBtn.addEventListener('click', resetToUpload); function resetToUpload() { resultSection.style.display = 'none'; previewZone.style.display = 'none'; dropZone.style.display = 'block'; selectedFile = null; fileInput.value = ''; // Scroll back up to the upload section document.getElementById('uploadSection').scrollIntoView({ behavior: 'smooth', block: 'start' }); } // ── Loading Steps ───────────────────────────────────────────────── let stepTimer = null; const STEP_IDS = ['step1','step2','step3','step4','step5','step6']; function showLoading() { // Reset all steps STEP_IDS.forEach(id => { const el = document.getElementById(id); if (el) el.className = 'step'; }); loadingOverlay.style.display = 'flex'; let i = 0; stepTimer = setInterval(() => { if (i < STEP_IDS.length) { if (i > 0) { const prev = document.getElementById(STEP_IDS[i - 1]); if (prev) prev.className = 'step done'; } const cur = document.getElementById(STEP_IDS[i]); if (cur) cur.className = 'step active'; i++; } }, 700); } function hideLoading() { clearInterval(stepTimer); loadingOverlay.style.display = 'none'; } // ── Show Result ─────────────────────────────────────────────────── function showResult(data) { const aiScore = typeof data.ai_score === 'number' ? data.ai_score : 50; const realScore = typeof data.real_score === 'number' ? data.real_score : 50; // ── Populate result image document.getElementById('resultImg').src = previewImg.src; // ── Binary verdict (no "Uncertain" category) const isAI = aiScore > 50; const badge = document.getElementById('resultBadge'); const verdict = document.getElementById('resultVerdict'); if (isAI) { badge.textContent = '🔴 AI-Generated'; badge.className = 'verdict-badge is-ai'; verdict.textContent = 'Likely AI-Generated'; verdict.className = 'verdict-heading is-ai'; } else { badge.textContent = '🟢 Authentic'; badge.className = 'verdict-badge is-real'; verdict.textContent = 'Likely Authentic'; verdict.className = 'verdict-heading is-real'; } // ── Confidence label — based on distance from 50% const margin = Math.abs(aiScore - 50); let confidenceLabel; if (margin >= 35) confidenceLabel = 'Very High Confidence'; else if (margin >= 20) confidenceLabel = 'High Confidence'; else if (margin >= 10) confidenceLabel = 'Medium Confidence'; else confidenceLabel = 'Low Confidence'; document.getElementById('resultConfidenceLevel').textContent = confidenceLabel; // ── Gauge bar (animate after a tick so CSS transition fires) setTimeout(() => { document.getElementById('gaugeFill').style.width = aiScore + '%'; document.getElementById('gaugeDot').style.left = aiScore + '%'; document.getElementById('realPct').textContent = realScore.toFixed(1) + '%'; document.getElementById('aiPct').textContent = aiScore.toFixed(1) + '%'; }, 80); // ── Plain-language summary document.getElementById('resultSummary').textContent = buildSummary(aiScore); // ── "Why this result" cards buildWhyCards(data, aiScore); // ── Heatmaps buildHeatmaps(data); // ── Advanced technical section buildAdvanced(data); // ── Advanced toggle — clone to remove stale listeners const oldBtn = document.getElementById('advancedToggle'); const newBtn = oldBtn.cloneNode(true); oldBtn.parentNode.replaceChild(newBtn, oldBtn); newBtn.addEventListener('click', function () { const body = document.getElementById('advancedBody'); const isOpen = body.style.display === 'block'; body.style.display = isOpen ? 'none' : 'block'; this.setAttribute('aria-expanded', String(!isOpen)); this.querySelector('span').textContent = isOpen ? 'Show Technical Analysis' : 'Hide Technical Analysis'; }); // ── Show result section then scroll TO IT ← BUG FIX resultSection.style.display = 'block'; setTimeout(() => { resultSection.scrollIntoView({ behavior: 'smooth', block: 'start' }); }, 60); } // ── Build plain-language summary ────────────────────────────────── function buildSummary(aiScore) { if (aiScore >= 80) return 'This image shows strong patterns typically found in AI-generated content. Multiple models flagged it with high confidence.'; if (aiScore >= 60) return 'This image shows several patterns commonly associated with AI-generated images.'; if (aiScore >= 50) return 'This image leans towards AI-generated, but the signal is relatively weak.'; if (aiScore >= 35) return 'This image appears to be authentic, though some minor signals were detected.'; return 'This image shows patterns consistent with authentic, real-world photographs.'; } // ── Build "Why this result" cards ───────────────────────────────── function buildWhyCards(data, aiScore) { // Visual Patterns let visualText; if (aiScore >= 70) visualText = 'Detected unusual texture patterns and edge artifacts commonly found in AI-generated images.'; else if (aiScore >= 50) visualText = 'Some visual patterns are consistent with AI generation, though results are mixed.'; else visualText = 'Visual patterns appear natural and consistent with authentic photographs.'; document.getElementById('explainVisual').textContent = visualText; // AI Model Analysis let modelText; if (aiScore >= 70) modelText = 'Multiple AI detection models identified patterns strongly associated with AI-generated images.'; else if (aiScore >= 50) modelText = 'AI detection models show a mild lean toward generated content.'; else modelText = 'AI models detected characteristics more consistent with real, authentic photographs.'; document.getElementById('explainModels').textContent = modelText; // Metadata Check let metaText = 'No reliable camera metadata was found. This may indicate the image was generated or heavily edited.'; const mdLayer = (data.breakdown || []).find(b => b.layer === 'Metadata Analysis'); if (mdLayer && mdLayer.signals) { const sigs = mdLayer.signals.join(' '); if (sigs.includes('camera EXIF') || sigs.includes('GPS')) metaText = 'Camera metadata was found, suggesting the image may have been captured with a real device.'; else if (sigs.includes('Software = AI') || sigs.includes('PNG metadata key')) metaText = 'Metadata explicitly references AI generation tools — a strong indicator of synthetic origin.'; } document.getElementById('explainMetadata').textContent = metaText; } // ── Build Heatmaps ──────────────────────────────────────────────── function buildHeatmaps(data) { const focusAreas = document.getElementById('focusAreas'); const dfiCard = document.getElementById('dfiCardContainer'); const attImg = document.getElementById('attentionHeatmapImg'); const dfiImg = document.getElementById('dfiHeatmapImg'); let hasAny = false; const ml = data.layers && data.layers.models ? data.layers.models : {}; if (ml.attention_heatmap) { attImg.src = ml.attention_heatmap; hasAny = true; } if (ml.dfi_heatmap) { dfiImg.src = ml.dfi_heatmap; dfiCard.style.display = 'block'; hasAny = true; } else { dfiCard.style.display = 'none'; } focusAreas.style.display = hasAny ? 'block' : 'none'; } // ── Build Advanced Technical Section ───────────────────────────── function buildAdvanced(data) { const container = document.getElementById('advancedContent'); container.innerHTML = ''; const fnLayer = (data.breakdown || []).find(b => b.layer === 'Filename Analysis'); const mdLayer = (data.breakdown || []).find(b => b.layer === 'Metadata Analysis'); const mlLayer = (data.breakdown || []).find(b => b.layer === 'AI Model and Forensic Detectors'); const votes = mlLayer ? (mlLayer.votes || []) : []; const forensics = mlLayer ? (mlLayer.forensics || {}) : {}; // Card 1 — Model Results const dlVotes = votes.filter(v => v.type === 'deep_learning'); const modelItems = dlVotes.length ? dlVotes.map(v => `