Highlight likely AI-generated text using simple marker patterns
// ==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 });
})();