radai-api / extension /sidepanel.js
RageCoder2006's picture
Clean deployment for Hugging Face
879d39c
Raw
History Blame Contribute Delete
9.67 kB
// ── 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
// ─────────────────────────────────────────────────────────────────────────
"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();