Spaces:
Sleeping
Sleeping
| // ββ 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 <canvas> 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 | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| ; | |
| // ββ 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(); | |