Spaces:
Sleeping
Sleeping
| 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 = ` | |
| <div class="tip-header"> | |
| <span class="tip-label" style="color:${color}">${data.label === "AI" ? "AI Generated" : "Likely Real"}</span> | |
| <span class="tip-badge" style="color:${color};">${pct}%</span> | |
| </div> | |
| <div class="tip-divider"></div> | |
| <div class="tip-text">${fullExp}</div> | |
| `; | |
| 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; | |
| } | |
| }); | |
| } |