import { TRAVERSE_BEHAVOUR_ATTR, TRAVERSE_BEHAVOUR_IGNORE_MAGIC, traverse_text } from "@/utils/domtraverse"; import { SyncingExtensionConfig } from "@/utils/config"; import { ExtConsole } from "@/utils/logging"; import { sendMessage } from "@/utils/messaging"; const console = ExtConsole("contentscript::text_class"); const CONFIG = new SyncingExtensionConfig("ai_text"); const DOM_OBSERVATORY: { observer: MutationObserver; timer: ReturnType | null } = { observer: new MutationObserver((_) => { if (DOM_OBSERVATORY.timer) clearTimeout(DOM_OBSERVATORY.timer); DOM_OBSERVATORY.timer = setTimeout(() => { console.debug("observatory trigger"); traverse(); }, 1000); }), timer: null, }; const VIEWPORT_OBSERVATORY: { observer: IntersectionObserver } = { observer: new IntersectionObserver((entries, observer) => { for (const entry of entries) { if (entry.isIntersecting) { const stack = STATE.stack.get(entry.target); if (stack) { sendMessage("inference_aiText_classify", { text: stack.text }) .then((resp) => { console.debug("received inf response", resp); observer.unobserve(entry.target); apply_class(entry.target, resp.prob_ai, resp.prob_human); }) .catch((e) => { console.warn("inference failed", e, "\nentry will be rescheduled by observer"); }); } else { console.warn("viewport_observatory triggered on node, but the stack is absent"); observer.unobserve(entry.target); } } } }), }; const STATE: { stack: WeakMap; badge: WeakMap; } = { stack: new WeakMap(), badge: new WeakMap(), }; export default function setup() { DOM_OBSERVATORY.observer.observe(document.body, { subtree: true, characterData: true, }); } export function traverse() { CONFIG.sync.then(() => { if (!CONFIG.value.enabled) { console.debug("disabled"); return; } console.debug("traverse start"); for (const stack of traverse_text(600, 20)) { const target = stack.first_node.parentElement || stack.root.parentElement; if (target) { set_badge(target); STATE.stack.set(target, { text: stack.text }); VIEWPORT_OBSERVATORY.observer.observe(target); } else { console.warn("Stack didn't have target"); } } }); } function apply_class(target: Node, prob_ai: number, prob_human: number) { CONFIG.sync.then(() => { if (CONFIG.value.show_ai && prob_ai >= CONFIG.value.threshold && prob_ai >= prob_human) { set_badge(target, { type: "ai", prob: prob_ai }); } else if (CONFIG.value.show_human && prob_human >= CONFIG.value.threshold && prob_human >= prob_ai) { set_badge(target, { type: "human", prob: prob_human }); } else { clear_badge(target); } }); } function set_badge(target: Node, data?: { type: "ai" | "human"; prob: number }) { if (STATE.badge.has(target)) STATE.badge.delete(target); let badge = document.createElement("mark"); badge.setAttribute(TRAVERSE_BEHAVOUR_ATTR, TRAVERSE_BEHAVOUR_IGNORE_MAGIC); badge.style.display = "inline"; badge.style.borderRadius = "4px"; badge.style.textAlign = "center"; badge.style.fontFamily = "sans"; badge.style.fontSize = "12px"; badge.style.padding = "1px"; badge.style.marginRight = "4px"; if (data?.type === "ai") { badge.style.backgroundColor = "#ffc36d"; badge.style.color = "#fff"; badge.textContent = `🤖: ${Math.trunc(data.prob * 100)}%`; } else if (data?.type === "human") { badge.style.backgroundColor = "#8fe1a1"; badge.style.color = "#fff"; badge.textContent = `👨‍💼: ${Math.trunc(data.prob * 100)}%`; } else { badge.style.backgroundColor = "#ddd"; badge.style.color = "#fff"; badge.textContent = "🦊 •••• "; } badge = target.insertBefore(badge, target.firstChild); STATE.badge.set(target, badge); } function clear_badge(target: Node) { STATE.badge.delete(target); }