AI Detector

Highlight likely AI-generated text using simple marker patterns

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

You will need to install an extension such as Tampermonkey to install this script.

Tendrás que instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Tendrás que instalar una extensión como Tampermonkey antes de poder instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Advertisement:

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

Advertisement:

// ==UserScript==
// @name         AI Detector
// @namespace    http://tampermonkey.net/
// @version      2.1.0
// @description  Highlight likely AI-generated text using simple marker patterns
// @author       anrinion
// @license      MIT
// @match        *://*/*
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_addStyle
// @grant        GM_registerMenuCommand
// @grant        GM_notification
// @run-at       document-idle
// ==/UserScript==

(function () {
  "use strict";

  // ---------- PERSISTENT EXCLUDED DOMAINS ----------
  function loadExcludedDomains() {
    try {
      const saved = GM_getValue("aiHighlighterExcludedDomains", "[]");
      return JSON.parse(saved);
    } catch (e) {
      return [];
    }
  }

  function saveExcludedDomains(domains) {
    GM_setValue("aiHighlighterExcludedDomains", JSON.stringify(domains));
  }

  let excludedDomains = loadExcludedDomains();

  function isDomainExcluded() {
    const host = location.hostname;
    return excludedDomains.some((pattern) => {
      const regexPattern = pattern.replace(/\./g, "\\.").replace(/\*/g, ".*");
      return new RegExp(`^${regexPattern}$`, "i").test(host);
    });
  }

  // ----------  AI MARKERS (with labels) ----------
  const AI_MARKERS = [
    {
      pattern:
        /\b(delve|tapestry|testament|realm|pivotal|vibrant|unleash|robust|seamless)\b/i,
      label: "high-risk vocabulary",
    },
    {
      pattern:
        /\b(operational excellence|strategic alignment|in today's digital age|brilliant)\b/i,
      label: "high-risk phrase",
    },
    { pattern: /as an ai language model/i, label: "AI disclaimer" },
    {
      pattern: /as of my last knowledge update/i,
      label: "AI knowledge cutoff phrase",
    },
    { pattern: /it is important to note that/i, label: "hedging phrase" },
    {
      pattern: /I hope this (?:email )?finds you well/i,
      label: "generic email opening",
    },
    {
      pattern: /if you have any (?:further )?questions/i,
      label: "customer service template",
    },
    {
      pattern:
        /\b(a recent study|many people|experts say|research shows|it is widely believed|some argue)\b/i,
      label: "vague attribution",
    },
    { pattern: /—/, label: "em dash" },
    { pattern: /```/, label: "triple backticks (code block)" },
    { pattern: /[«»]/, label: "guillemets" },
  ];

  // Only these emojis are considered “human”; everything else is an AI marker.
  const HUMAN_EMOJIS = new Set([
    "😅",
    "😭",
    "💀",
    "😊",
    "👌",
    "👍",
    "😢",
    "🤦‍♂️",
    "😎",
    "😐",
    "🙂",
    "😑",
    "😶",
    "🙄",
    "😝",
    "🫠",
    "😱",
    "🥳",
    "🤮",
    "🤭",
    "🧐",
    "❤️",
    "👀",
    "🤗",
    "🤣",
    "😮",
    "🥱",
    "😞",
    "🙈",
  ]);

  // Returns true if every word starts with an uppercase letter (any language)
  // and the text contains a colon.
  function isTitleCaseWithColon(text) {
    if (!text.includes(":")) return false;
    // Split into tokens that contain at least one letter (Unicode-aware)
    const tokens = text.split(/\s+/).filter((t) => /\p{L}/u.test(t));
    if (tokens.length === 0) return false;
    return tokens.every((token) => {
      // Find first letter in the token
      const match = token.match(/\p{L}/u);
      if (!match) return false;
      const firstLetter = match[0];
      // Check if that letter is uppercase
      return firstLetter === firstLetter.toLocaleUpperCase();
    });
  }

  // Returns the reason (string) if an AI marker is found, otherwise null.
  function getAIMarkerReason(text) {
    // Check each text-based pattern
    for (const { pattern, label } of AI_MARKERS) {
      if (pattern.test(text)) {
        const match = text.match(pattern);
        const sample = match ? match[0] : "";
        return sample ? `${label}: “${escapeHtml(sample)}”` : label;
      }
    }

    // Inverted emoji check: any emoji NOT in the human set is suspected AI.
    const emojiList = text.match(/\p{Emoji_Presentation}/gu);
    if (emojiList) {
      for (const emoji of emojiList) {
        if (!HUMAN_EMOJIS.has(emoji)) {
          return `non-human emoji (${emoji})`;
        }
      }
    }

    // Title-case heading with colon (general)
    if (isTitleCaseWithColon(text)) {
      return `title-case heading with colon: “${escapeHtml(text)}”`;
    }

    return null;
  }

  function escapeHtml(str) {
    const div = document.createElement("div");
    div.textContent = str;
    return div.innerHTML;
  }

  // ---------- BLOCK PROCESSING ----------
  function getBlockElements() {
    const selectors =
      "p, div, li, blockquote, h1, h2, h3, h4, h5, h6, section, article, td, th";
    const elements = document.querySelectorAll(selectors);
    const candidates = Array.from(elements).filter((el) => {
      const text = el.innerText?.trim() || "";
      if (text.length < 30) return false;
      if (el.closest(".ai-highlighter-ui, .ai-highlight")) return false;
      return true;
    });
    // Keep only leaf blocks (those that don't contain other candidates)
    return candidates.filter(
      (el) => !candidates.some((c) => c !== el && el.contains(c)),
    );
  }

  function clearHighlights() {
    document.querySelectorAll(".ai-highlight").forEach((el) => {
      const parent = el.parentNode;
      while (el.firstChild) {
        parent.insertBefore(el.firstChild, el);
      }
      parent.removeChild(el);
      parent.normalize();
    });
  }

  let applying = false;
  function applyHighlights() {
    if (isDomainExcluded() || applying) return;
    applying = true;
    try {
      const blocks = getBlockElements();
      blocks.forEach((block) => {
        const reason = getAIMarkerReason(block.innerText);
        if (reason) {
          const wrapper = document.createElement("span");
          wrapper.className = "ai-highlight";
          wrapper.setAttribute("data-ai-reason", reason);
          while (block.firstChild) {
            wrapper.appendChild(block.firstChild);
          }
          block.appendChild(wrapper);
        }
      });
    } finally {
      applying = false;
    }
  }

  // ---------- TOOLTIP WITH REASON ----------
  function initTooltip() {
    const tooltip = document.createElement("div");
    tooltip.className = "ai-highlighter-tooltip";
    tooltip.style.cssText = `
            position: fixed;
            background: #333;
            color: #fff;
            padding: 6px 10px;
            border-radius: 4px;
            font-size: 13px;
            max-width: 350px;
            z-index: 2147483647;
            pointer-events: none;
            display: none;
            box-shadow: 0 2px 8px rgba(0,0,0,0.3);
            line-height: 1.3;
        `;
    document.body.appendChild(tooltip);

    document.addEventListener("mouseover", (e) => {
      const target = e.target.closest(".ai-highlight");
      if (!target) {
        tooltip.style.display = "none";
        return;
      }
      const reason = target.getAttribute("data-ai-reason") || "";
      tooltip.innerHTML = `<strong> Likely AI-generated</strong><br><span style="font-size:11px;color:#ccc;">${escapeHtml(reason)}</span>`;
      tooltip.style.display = "block";
    });
    document.addEventListener("mousemove", (e) => {
      if (tooltip.style.display === "block") {
        tooltip.style.left = e.clientX + 12 + "px";
        tooltip.style.top = e.clientY + 12 + "px";
      }
    });
    document.addEventListener("mouseout", (e) => {
      if (e.target.closest(".ai-highlight")) tooltip.style.display = "none";
    });
  }

  // ---------- MUTATION OBSERVER ----------
  let scanTimer = null;
  function scheduleRescan() {
    clearTimeout(scanTimer);
    scanTimer = setTimeout(() => {
      if (isDomainExcluded() || applying) return;
      clearHighlights();
      applyHighlights();
    }, 2000);
  }

  function startObserver() {
    const observer = new MutationObserver((mutations) => {
      if (applying) return;
      const shouldRescan = mutations.some((m) => {
        if (m.type !== "childList") return false;
        if (m.target.closest?.(".ai-highlighter-ui, .ai-highlight"))
          return false;
        for (let node of m.addedNodes) {
          if (node.nodeType === Node.ELEMENT_NODE) {
            if (node.closest?.(".ai-highlighter-ui, .ai-highlight")) continue;
            if (node.querySelector) {
              const hasText = node.querySelector(
                "p, div, li, blockquote, h1, h2, h3, h4, h5, h6",
              );
              if (hasText) return true;
            }
          }
        }
        return false;
      });
      if (shouldRescan) scheduleRescan();
    });
    observer.observe(document.body, { childList: true, subtree: true });
  }

  // ---------- MENU COMMANDS ----------
  GM_registerMenuCommand("Rescan page", () => {
    clearHighlights();
    applyHighlights();
    GM_notification({ text: "AI Highlighter: Rescan complete", timeout: 2000 });
  });

  GM_registerMenuCommand("Exclude this site", () => {
    const host = location.hostname;
    if (!excludedDomains.includes(host)) {
      excludedDomains.push(host);
      saveExcludedDomains(excludedDomains);
      clearHighlights();
      GM_notification({
        text: `AI Highlighter: Excluded ${host}`,
        timeout: 2500,
      });
    } else {
      GM_notification({ text: `${host} is already excluded`, timeout: 2000 });
    }
  });

  // ---------- INITIALIZATION ----------
  GM_addStyle(`
        .ai-highlight {
            background-color: #fff9c4 !important;
            color: #1e1e1e !important;
            cursor: help !important;
            border-radius: 2px;
            transition: background-color 0.2s;
        }
        .ai-highlight:hover {
            background-color: #fff176 !important;
        }
        .ai-highlighter-tooltip {
            font-family: system-ui, sans-serif !important;
        }
    `);

  if (!isDomainExcluded()) {
    setTimeout(() => {
      applyHighlights();
      initTooltip();
      startObserver();
    }, 800);
  }

  // Handle SPA navigation
  let lastUrl = location.href;
  new MutationObserver(() => {
    if (location.href !== lastUrl) {
      lastUrl = location.href;
      setTimeout(() => {
        if (!isDomainExcluded()) {
          clearHighlights();
          applyHighlights();
        }
      }, 800);
    }
  }).observe(document, { subtree: true, childList: true });
})();