// ── RADAI · sidepanel.js ────────────────────────────────────────────── // // Architecture: // • Loads latest scan result from background via GET_SCAN_RESULT message // • Also reads the stored screenshot from chrome.storage.local for heatmap // • Heatmap: patch-based pixel analysis (frequency variance, luminance // deviation, edge density per cell) rendered to a element // • Re-loads on window focus (user ran a new scan) // • No state held in globals that could stale; every render call is fresh // // Bug fixes vs. previous version: // • Background GET_SCAN_RESULT now has a 5s timeout guard // • Canvas is cleared before each render (no ghost overlays) // • All DOM refs validated before use // • rAF used for canvas draw to avoid blocking the main thread // ───────────────────────────────────────────────────────────────────────── "use strict"; // ── DOM refs ────────────────────────────────────────────────────────────── const $ = (id) => document.getElementById(id); const statusChip = $("statusChip"); const loadingBar = $("loadingBar"); const emptyState = $("emptyState"); const dataContainer = $("dataContainer"); const stoppedBanner = $("stoppedBanner"); const verdictLabel = $("verdictLabel"); const verdictDesc = $("verdictDesc"); const riskBarFill = $("riskBarFill"); const riskBarLabel = $("riskBarLabel"); const statusDot = $("statusDot"); const statusText = $("statusText"); const footerLeft = $("footerLeft"); const footerRight = $("footerRight"); const themeBtn = $("themeBtn"); const scanBtn = $("scanBtn"); const stopBtn = $("stopBtn"); // ── UI state functions ──────────────────────────────────────────────────── function showLoading() { loadingBar.style.display = "block"; statusChip.textContent = "LOADING"; statusChip.style.color = ""; } function hideLoading() { loadingBar.style.display = "none"; } function showEmpty() { hideLoading(); emptyState.classList.remove("hidden"); dataContainer.classList.remove("visible"); statusChip.textContent = "READY"; statusChip.style.color = ""; } function showStopped(result = {}) { hideLoading(); emptyState.classList.add("hidden"); dataContainer.classList.add("visible"); stoppedBanner.style.display = "block"; statusChip.textContent = "STOPPED"; statusChip.style.color = "var(--warn)"; const scanned = result.count || 0; const total = result.total || 0; const flagged = result.aiCount || 0; const risk = result.risk || 0; verdictLabel.textContent = "Stopped (incomplete scan)"; verdictLabel.style.color = "var(--warn)"; verdictDesc.textContent = `${scanned} / ${total} images scanned\n${flagged} flagged as AI\n~${risk}% of scanned images appear AI-generated`; riskBarFill.style.width = `${risk}%`; riskBarFill.style.background = "var(--warn)"; riskBarLabel.textContent = `${risk}%`; statusDot.style.background = "var(--warn)"; statusText.textContent = "SCAN STOPPED BY USER"; footerRight.textContent = "Partial scan"; } function showResult(result) { hideLoading(); emptyState.classList.add("hidden"); dataContainer.classList.add("visible"); stoppedBanner.style.display = "none"; const risk = typeof result.risk === "number" ? result.risk : 0; const count = typeof result.count === "number" ? result.count : 0; const confidence = result.rawConfidence || (risk / 100); const timestamp = result.timestamp; // Colour swatch for risk const riskColor = risk > 60 ? "var(--danger)" : risk > 30 ? "var(--warn)" : "var(--ok)"; // Status chip statusChip.textContent = risk > 60 ? "HIGH RISK" : risk > 30 ? "MODERATE" : "CLEAN"; statusChip.style.color = riskColor; if (count === 0) { verdictLabel.textContent = "No Images"; verdictLabel.style.color = riskColor; verdictDesc.textContent = "No images found on this page."; } else { // Verdict verdictLabel.textContent = risk > 60 ? "AI-Generated" : risk > 30 ? "Suspicious" : "Likely Real"; verdictLabel.style.color = riskColor; const flagged = Math.round((risk / 100) * count); verdictDesc.textContent = `${count} images scanned • ${flagged} flagged as AI\n${risk}% of images appear AI-generated`; } // Risk bar riskBarFill.style.width = `${risk}%`; riskBarFill.style.background = riskColor; riskBarLabel.textContent = `${risk}%`; // Status row statusDot.style.background = riskColor; statusText.textContent = risk > 60 ? `HIGH CONFIDENCE AI · ${count} image${count !== 1 ? "s" : ""} processed` : risk > 30 ? `MODERATE SIGNAL · ${count} image${count !== 1 ? "s" : ""} processed` : `CLEAN · ${count} image${count !== 1 ? "s" : ""} processed`; // Footer if (timestamp) { const d = new Date(timestamp); footerRight.textContent = `Scanned at ${d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}`; } else { footerRight.textContent = `${count} image${count !== 1 ? "s" : ""} analysed`; } } // ── Load data (now just initialized on empty) ────────────────────────────── async function loadResult() { showEmpty(); } // ── Re-load on focus ────────────────────────────────────────────────────── window.addEventListener("focus", () => { loadResult(); }); // ── Theme ───────────────────────────────────────────────────────────────── function applyTheme(theme) { document.documentElement.setAttribute("data-theme", theme); themeBtn.textContent = theme === "dark" ? "LIGHT" : "DARK"; } function initTheme() { try { chrome.storage.local.get("theme", (res) => { void chrome.runtime.lastError; const t = res?.theme || (window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"); applyTheme(t); }); } catch { applyTheme(window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"); } } themeBtn.addEventListener("click", () => { const cur = document.documentElement.getAttribute("data-theme") || "light"; const next = cur === "dark" ? "light" : "dark"; applyTheme(next); try { chrome.storage.local.set({ theme: next }); } catch { /* ignore */ } }); // ── Global scan coordination ──────────────────────────────────────────────── let isScanRunning = false; function setScanButtons(scanning) { if (!scanBtn) return; scanBtn.disabled = scanning; scanBtn.textContent = scanning ? "SCANNING..." : "SCAN IMAGES"; stopBtn.disabled = !scanning; } async function startScan() { if (isScanRunning) return; console.log("SCAN CLICKED"); const tabs = await chrome.tabs.query({ active: true, currentWindow: true }); const tab = tabs[0]; if (!tab) return; if (!tab.url || tab.url.startsWith("chrome://") || tab.url.startsWith("chrome-extension://")) { showEmpty(); // Not allowed return; } isScanRunning = true; setScanButtons(true); // Reset UI before scan showLoading(); statusChip.textContent = "SCANNING..."; verdictLabel.textContent = "—"; verdictLabel.style.color = "inherit"; verdictDesc.textContent = "—"; riskBarFill.style.width = "0%"; riskBarLabel.textContent = "—"; statusText.textContent = "—"; let screenshot = null; try { screenshot = await chrome.tabs.captureVisibleTab(null, { format: "png" }); } catch (e) { console.log("Screenshot failed (could be context limit)", e); } console.log("Sending action: SCAN_IMAGES to content.js on tab:", tab.id); chrome.tabs.sendMessage(tab.id, { action: "SCAN_IMAGES", screenshot }, (response) => { isScanRunning = false; setScanButtons(false); if (chrome.runtime.lastError) { console.warn("No response from content script. Error:", chrome.runtime.lastError.message); showStopped(); return; } if (response) { console.log("Response:", response); if (response.stopped) { showStopped(); } else if (response.done) { showResult(response); } else { showEmpty(); } } else { showEmpty(); } }); } async function stopScan() { console.log("STOP CLICKED"); const tabs = await chrome.tabs.query({ active: true, currentWindow: true }); const tab = tabs[0]; if (tab) { chrome.tabs.sendMessage(tab.id, { action: "STOP_SCAN" }); } } if (scanBtn) scanBtn.addEventListener("click", startScan); if (stopBtn) stopBtn.addEventListener("click", stopScan); // ── Boot ────────────────────────────────────────────────────────────────── initTheme(); loadResult();