if (!window.__AI_DETECTOR_LOADED__) {
window.__AI_DETECTOR_LOADED__ = true;
const OVERLAY_ATTR = "data-ai-detector-overlay";
const TIP_ATTR = "data-ai-detector-tip";
let activeOverlays = [];
// ── Stop-scan flag ──────────────────────────────────────────────────────
// Reset on every new SCAN_IMAGES request; set to true by STOP_SCAN.
let stopRequested = false;
// ── API Configuration ───────────────────────────────────────────────────
const API_URL = "https://ragecoder2006-radai-api.hf.space/predict";
// const API_URL = "http://127.0.0.1:5000/predict"; // Uncomment to use local backend
/* ── Inject shared tooltip styles once ── */
if (!document.getElementById("__ai_detector_styles__")) {
const link = document.createElement("link");
link.rel = "stylesheet";
link.href = "https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&family=Inter:wght@400;500;600;700&display=swap";
document.head.appendChild(link);
const style = document.createElement("style");
style.id = "__ai_detector_styles__";
style.textContent = `
[${TIP_ATTR}="1"] {
--radai-mono: "IBM Plex Mono", monospace, sans-serif;
--radai-sans: "Inter", -apple-system, system-ui, sans-serif;
position: fixed;
pointer-events: none;
z-index: 2147483647;
background: #ffffff;
border: 1px solid #d4d4d4;
border-radius: 4px;
padding: 14px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
font-family: var(--radai-sans);
color: #111111;
min-width: 200px;
max-width: 320px;
opacity: 0;
transform: translateY(4px);
transition: opacity 0.2s ease, transform 0.2s ease;
line-height: 1.5;
}
[${TIP_ATTR}="1"].visible {
opacity: 1;
transform: translateY(0);
}
[${TIP_ATTR}="1"] .tip-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
margin-bottom: 10px;
gap: 12px;
}
[${TIP_ATTR}="1"] .tip-label {
font-size: 18px;
font-weight: 700;
letter-spacing: -0.5px;
line-height: 1;
}
[${TIP_ATTR}="1"] .tip-badge {
font-size: 18px;
font-weight: 700;
font-family: var(--radai-mono);
letter-spacing: -0.5px;
line-height: 1;
}
[${TIP_ATTR}="1"] .tip-divider {
height: 1px;
background: #e5e5e5;
margin-bottom: 10px;
}
[${TIP_ATTR}="1"] .tip-text {
font-size: 12px;
color: #525252;
font-weight: 400;
line-height: 1.5;
}
`;
document.head.appendChild(style);
}
/* ── Clear all overlays ── */
function clearBoxes() {
activeOverlays.forEach(o => o.box.remove());
activeOverlays = [];
document.querySelectorAll(`[${TIP_ATTR}="1"]`).forEach(el => el.remove());
}
/* ── Get a muted color per label ── */
function labelColor(label) {
return label === "AI" ? "#e53e3e" : "#38a169";
}
/* ── Strip HTML ── */
function stripHtml(htmlStr) {
if (!htmlStr) return "Analysis complete.";
const tmp = document.createElement("div");
tmp.innerHTML = htmlStr;
return tmp.textContent || tmp.innerText || "";
}
/* ── Build the tooltip element ── */
function buildTooltip(data) {
const rawExp = data.explanation || "";
const fullExp = stripHtml(rawExp);
const color = labelColor(data.label);
const pct = Math.round(data.confidence * 100);
const tip = document.createElement("div");
tip.setAttribute(TIP_ATTR, "1");
tip.innerHTML = `
${fullExp}
`;
return tip;
}
/* ── Position tooltip near image, keeping it in viewport ── */
function positionTooltip(tip, rect) {
const TIP_W = 172;
const TIP_H = 118;
const PAD = 10;
let left = rect.left;
let top = rect.top - TIP_H - 10;
// Flip below if not enough room above
if (top < PAD) top = rect.bottom + 10;
// Clamp horizontally
if (left + TIP_W > window.innerWidth - PAD) {
left = window.innerWidth - TIP_W - PAD;
}
if (left < PAD) left = PAD;
tip.style.left = `${left}px`;
tip.style.top = `${top}px`;
}
/* ── Draw border box + attach hover tooltip ── */
function drawBox(img, data) {
const rect = img.getBoundingClientRect();
const color = labelColor(data.label);
// Border overlay box
const box = document.createElement("div");
box.setAttribute(OVERLAY_ATTR, "1");
box.style.cssText = `
position: absolute;
left: ${rect.left + window.scrollX}px;
top: ${rect.top + window.scrollY}px;
width: ${rect.width}px;
height: ${rect.height}px;
border: 3px solid ${color};
border-radius: 10px;
pointer-events: none;
z-index: 999998;
opacity: 0;
transition: opacity 0.25s ease;
box-shadow: 0 0 0 1px rgba(255,255,255,0.15), 0 4px 20px ${color}33;
`;
// Small inline label tag
const tag = document.createElement("div");
tag.style.cssText = `
position: absolute;
top: -1px;
left: -1px;
padding: 3px 8px;
font-size: 11px;
font-weight: 600;
font-family: -apple-system, BlinkMacSystemFont, "Inter", sans-serif;
color: #ffffff;
background: ${color};
border-radius: 9px 0 9px 0;
letter-spacing: 0.2px;
line-height: 1.6;
`;
tag.textContent = `${data.label} · ${Math.round(data.confidence * 100)}%`;
box.appendChild(tag);
document.body.appendChild(box);
activeOverlays.push({ img, box });
// Fade in
requestAnimationFrame(() => { box.style.opacity = "1"; });
// Hover tooltip (attached to the img element)
if (!img.dataset.aiDetectorBound) {
img.dataset.aiDetectorBound = "1";
let activeTip = null;
img.addEventListener("mouseenter", () => {
// Remove any existing tip
document.querySelectorAll(`[${TIP_ATTR}="1"]`).forEach(el => el.remove());
const r = img.getBoundingClientRect();
const tip = buildTooltip(data);
document.body.appendChild(tip);
positionTooltip(tip, r);
activeTip = tip;
// Trigger fade-in on next frame
requestAnimationFrame(() => { tip.classList.add("visible"); });
});
img.addEventListener("mouseleave", () => {
if (activeTip) {
activeTip.classList.remove("visible");
const dying = activeTip;
activeTip = null;
setTimeout(() => dying.remove(), 220);
}
});
// Re-position if user scrolls while hovering
window.addEventListener("scroll", () => {
if (activeTip) {
const r = img.getBoundingClientRect();
positionTooltip(activeTip, r);
}
}, { passive: true });
}
}
/* ── Recalculate Box Positions on Resize ── */
let resizeTimeout;
window.addEventListener("resize", () => {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(() => {
activeOverlays.forEach(({ img, box }) => {
if (!img.isConnected) {
box.style.display = "none";
return;
}
const rect = img.getBoundingClientRect();
// Hide box if image becomes invisible or too small
if (rect.width === 0 || rect.height === 0) {
box.style.display = "none";
return;
}
box.style.display = "block";
box.style.left = `${rect.left + window.scrollX}px`;
box.style.top = `${rect.top + window.scrollY}px`;
box.style.width = `${rect.width}px`;
box.style.height = `${rect.height}px`;
});
}, 150); // slight debounce ensures sidebar finishes dragging/animating
}, { passive: true });
/* ── Helpers ── */
function loadImage(src) {
return new Promise((resolve, reject) => {
const image = new Image();
image.onload = () => resolve(image);
image.onerror = reject;
image.src = src;
});
}
async function cropVisibleImageFromScreenshot(screenshotDataUrl, rect) {
const shot = await loadImage(screenshotDataUrl);
const scaleX = shot.width / window.innerWidth;
const scaleY = shot.height / window.innerHeight;
const sx = Math.max(0, rect.left * scaleX);
const sy = Math.max(0, rect.top * scaleY);
const sw = Math.max(1, rect.width * scaleX);
const sh = Math.max(1, rect.height * scaleY);
const canvas = document.createElement("canvas");
canvas.width = Math.max(1, Math.round(sw));
canvas.height = Math.max(1, Math.round(sh));
const ctx = canvas.getContext("2d");
ctx.drawImage(shot, sx, sy, sw, sh, 0, 0, canvas.width, canvas.height);
return canvas.toDataURL("image/png");
}
async function predictWithUrlOrFallback(img, screenshotDataUrl) {
const url = img.currentSrc || img.src;
if (url && !url.startsWith("blob:")) {
try {
const res = await fetch(API_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ image_url: url })
});
if (res.ok) return await res.json();
} catch (err) {
console.log("URL path failed, falling back to screenshot crop:", err);
}
}
const rect = img.getBoundingClientRect();
const cropDataUrl = await cropVisibleImageFromScreenshot(screenshotDataUrl, rect);
const res = await fetch(API_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ image: cropDataUrl })
});
if (!res.ok) throw new Error(`Prediction failed with status ${res.status}`);
return await res.json();
}
async function scanImages(screenshotDataUrl) {
clearBoxes();
stopRequested = false;
const images = Array.from(document.querySelectorAll("img")).filter(img => {
const rect = img.getBoundingClientRect();
return (
img.complete &&
img.naturalWidth > 0 &&
rect.width >= 80 &&
rect.height >= 80
);
});
let count = 0;
let aiCount = 0;
let lastExplanation = null;
let topImages = [];
for (const img of images) {
// Check stop flag before processing each image
if (stopRequested) {
return { count, aiCount, risk: count > 0 ? Math.round((aiCount / count) * 100) : 0, explanation: lastExplanation, stopped: true, total: images.length };
}
try {
const data = await predictWithUrlOrFallback(img, screenshotDataUrl);
if (stopRequested) {
return { count, aiCount, risk: count > 0 ? Math.round((aiCount / count) * 100) : 0, explanation: lastExplanation, stopped: true, total: images.length };
}
if (data.label === "AI") {
aiCount++;
} else if (data.label === "Real") {
data.confidence = data.confidence.toString();
}
topImages.push({
src: img.src,
confidence: data.confidence,
label: data.label
});
// Save the last explanation regardless of label to provide some feedback
if (data.explanation) lastExplanation = data.explanation;
drawBox(img, data);
count++;
} catch (err) {
console.log("Skipping image:", err);
}
}
const risk = count > 0 ? Math.round((aiCount / count) * 100) : 0;
// Sort by suspiciousness (confidence) and take top 3
topImages.sort((a, b) => b.confidence - a.confidence);
const suspiciousTop3 = topImages.filter(i => i.label === "AI").slice(0, 3);
return { count, aiCount, risk, explanation: lastExplanation, stopped: false, topImages: suspiciousTop3 };
}
chrome.runtime.onMessage.addListener((req, sender, sendResponse) => {
if (req.action === "SCAN_IMAGES") {
scanImages(req.screenshot)
.then(result => {
if (result.stopped) {
sendResponse({ done: false, stopped: true, count: result.count, aiCount: result.aiCount, total: result.total, risk: result.risk, explanation: result.explanation });
} else {
sendResponse({
done: true,
count: result.count,
risk: result.risk,
explanation: result.explanation,
topImages: result.topImages
});
}
})
.catch(err => {
console.error(err);
sendResponse({ done: false, error: String(err) });
});
return true; // keep message channel open
}
if (req.action === "STOP_SCAN") {
stopRequested = true;
// No sendResponse needed; popup will receive the stopped flag via SCAN_IMAGES response
return false;
}
});
}