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 = `
${data.label === "AI" ? "AI Generated" : "Likely Real"} ${pct}%
${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; } }); }