125 lines
4.4 KiB
TypeScript
125 lines
4.4 KiB
TypeScript
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<typeof setTimeout> | 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<Element, { text: string }>;
|
|
badge: WeakMap<Node, Element>;
|
|
} = {
|
|
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);
|
|
}
|