feat,fix: document observation, popup refinement

* Implement document recheck on text content change
* Review extension config
* Split text/image popup sections into tabs
* Fix popup setting not being properly reactive
This commit is contained in:
masters 2026-03-12 02:16:17 +05:00 • committed by spushk
commit b84eaaac82
Signed by: spushk
GPG key ID: 7797B761E105E8AA
7 changed files with 366 additions and 284 deletions

View file

@ -24,14 +24,14 @@
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/vite": "^4.2.1", "@tailwindcss/vite": "^4.2.1",
"@tsconfig/svelte": "^5.0.7", "@tsconfig/svelte": "^5.0.8",
"@wxt-dev/module-svelte": "^2.0.4", "@wxt-dev/module-svelte": "^2.0.4",
"daisyui": "^5.5.19", "daisyui": "^5.5.19",
"svelte": "^5.50.0", "svelte": "^5.53.10",
"svelte-check": "^4.3.6", "svelte-check": "^4.4.5",
"tailwindcss": "^4.2.1", "tailwindcss": "^4.2.1",
"tslib": "^2.8.1", "tslib": "^2.8.1",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"wxt": "^0.20.14" "wxt": "^0.20.18"
} }
} }

View file

@ -106,36 +106,34 @@ function setup_inference_ai_text() {
} }
async function configure_ai_text(config: ExtensionConfigAiText) { async function configure_ai_text(config: ExtensionConfigAiText) {
if (config.enabled) { // if (config.enabled) {
const loader = config.lite ? inference_ai_text_lite : inference_ai_text; // const loader = config.lite ? inference_ai_text_lite : inference_ai_text;
const handler = config.lite ? AiTextLite : AiText; // const handler = config.lite ? AiTextLite : AiText;
const capability = config.lite ? AiTextLiteCapability : AiTextCapability; // const capability = config.lite ? AiTextLiteCapability : AiTextCapability;
const have = config.lite ? AI_TEXT_STATE.have_lite_model : AI_TEXT_STATE.have_normal_model; // const have = config.lite ? AI_TEXT_STATE.have_lite_model : AI_TEXT_STATE.have_normal_model;
const set_have = config.lite // const set_have = config.lite
? () => (AI_TEXT_STATE.have_lite_model = true) // ? () => (AI_TEXT_STATE.have_lite_model = true)
: () => (AI_TEXT_STATE.have_normal_model = true); // : () => (AI_TEXT_STATE.have_normal_model = true);
// if (!have) {
if (!have) { // await loader();
await loader(); // if (await guess_has_webgpu()) {
if (await guess_has_webgpu()) { // console.warn(
console.warn( // "WebGPU detected, but use is not currently possible due to burn APIs being sync. Check the issue progress in burn and burn-onnx repos. In the mean time will use NdArray anyway.",
"WebGPU detected, but use is not currently possible due to burn APIs being sync. Check the issue progress in burn and burn-onnx repos. In the mean time will use NdArray anyway.", // );
); // //await capability.assert_ndarray();
//await capability.assert_ndarray(); // await capability.assert_webgpu();
await capability.assert_webgpu(); // } else {
} else { // await capability.assert_ndarray();
await capability.assert_ndarray(); // }
} // set_have();
set_have(); // }
} // AI_TEXT_STATE.handle = new handler();
// } else {
AI_TEXT_STATE.handle = new handler(); // AI_TEXT_STATE.handle = null;
} else { // if (AI_TEXT_STATE.queue_inference_timer) {
AI_TEXT_STATE.handle = null; // clearTimeout(AI_TEXT_STATE.queue_inference_timer);
if (AI_TEXT_STATE.queue_inference_timer) { // }
clearTimeout(AI_TEXT_STATE.queue_inference_timer); // }
}
}
} }
async function guess_has_webgpu() { async function guess_has_webgpu() {

View file

@ -1,21 +1,37 @@
import { EXTENSION_CONFIG, ExtensionConfigAiImage, ExtensionConfigAiText } from "@/utils/config"; import { EXTENSION_CONFIG, ExtensionConfigAiImage, ExtensionConfigAiText } from "@/utils/config";
import { traverse_text } from "@/utils/domtraverse"; import { traverse_text } from "@/utils/domtraverse";
const console = ExtConsole("content");
export default defineContentScript({ export default defineContentScript({
matches: ["http://*/*", "https://*/*"], matches: ["http://*/*", "https://*/*"],
main() { main() {
const trigger = setTimeout(() => { DOM_OBSERVATORY.observer = new MutationObserver((_) => {
text_ai_traverse(); if (DOM_OBSERVATORY.timer) {
}, 1000); clearTimeout(DOM_OBSERVATORY.timer);
}
document.addEventListener("load", () => { DOM_OBSERVATORY.timer = setTimeout(() => {
clearTimeout(trigger); console.debug("retraverse, cause: mutation");
text_ai_traverse();
text_ai_traverse(); }, 1000);
}); });
setTimeout(() => {
text_ai_traverse();
DOM_OBSERVATORY.observer?.observe(document.body, {
subtree: true,
characterData: true,
});
}, 1000);
}, },
}); });
const DOM_OBSERVATORY = {
observer: null as MutationObserver | null,
timer: null as number | null,
};
const TRAVERSE_VIEWPORT_OBSERVER = new IntersectionObserver( const TRAVERSE_VIEWPORT_OBSERVER = new IntersectionObserver(
(entries, observer) => { (entries, observer) => {
for (const entry of entries) { for (const entry of entries) {
@ -23,27 +39,25 @@ const TRAVERSE_VIEWPORT_OBSERVER = new IntersectionObserver(
const stack = TRAVERSE_PARENT_TO_STACK.get(entry.target); const stack = TRAVERSE_PARENT_TO_STACK.get(entry.target);
if (stack) { if (stack) {
sendMessage("inference_aiText_classify", { text: stack.text }) sendMessage("inference_aiText_classify", { text: stack.text }).then((result) => {
.then((result) => { observer.unobserve(entry.target);
observer.unobserve(entry.target);
if ( if (
EXTENSION_CONFIG.ai_text.show_ai && EXTENSION_CONFIG.ai_text.show_ai &&
result.prob_ai >= EXTENSION_CONFIG.ai_text.threshold && result.prob_ai >= EXTENSION_CONFIG.ai_text.threshold &&
result.prob_ai >= result.prob_human result.prob_ai >= result.prob_human
) { ) {
ai_set_badge(entry.target, result.prob_ai, undefined); ai_set_badge(entry.target, result.prob_ai, undefined);
} else if ( } else if (
EXTENSION_CONFIG.ai_text.show_human && EXTENSION_CONFIG.ai_text.show_human &&
result.prob_human >= EXTENSION_CONFIG.ai_text.threshold && result.prob_human >= EXTENSION_CONFIG.ai_text.threshold &&
result.prob_human >= result.prob_ai result.prob_human >= result.prob_ai
) { ) {
ai_set_badge(entry.target, undefined, result.prob_human); ai_set_badge(entry.target, undefined, result.prob_human);
} else { } else {
ai_clear_badge(entry.target); ai_clear_badge(entry.target);
} }
}) });
.catch((e) => {});
} }
} }
} }
@ -63,6 +77,7 @@ interface WeakStack {
} }
const TRAVERSE_PARENT_TO_STACK: WeakMap<Element, WeakStack> = new WeakMap(); const TRAVERSE_PARENT_TO_STACK: WeakMap<Element, WeakStack> = new WeakMap();
function text_ai_traverse() { function text_ai_traverse() {
console.debug("text_ai_traverse: traverse DOM");
for (const stack of traverse_text(600, 20)) { for (const stack of traverse_text(600, 20)) {
let target = stack.first_node.parentElement || stack.root.parentElement; let target = stack.first_node.parentElement || stack.root.parentElement;
if (target) { if (target) {
@ -77,6 +92,7 @@ const SET_BADGES: WeakMap<Node, Element> = new WeakMap();
function ai_set_badge(node: Node, ai?: number, human?: number) { function ai_set_badge(node: Node, ai?: number, human?: number) {
const badge = document.createElement("span"); const badge = document.createElement("span");
badge.setAttribute(TRAVERSE_BEHAVOUR_ATTR, TRAVERSE_BEHAVOUR_IGNORE_MAGIC);
badge.style.display = "inline"; badge.style.display = "inline";
badge.style.borderRadius = "4px"; badge.style.borderRadius = "4px";
badge.style.textAlign = "center"; badge.style.textAlign = "center";
@ -84,20 +100,14 @@ function ai_set_badge(node: Node, ai?: number, human?: number) {
badge.style.fontSize = "12px"; badge.style.fontSize = "12px";
badge.style.padding = "1px"; badge.style.padding = "1px";
badge.style.marginRight = "4px"; badge.style.marginRight = "4px";
if (ai || human) { if (ai) {
if (ai) { badge.style.backgroundColor = "#ffc36d";
badge.style.backgroundColor = "#ffc36d"; badge.style.color = "#fff";
badge.style.color = "#fff"; badge.textContent = `🤖: ${Math.trunc(ai * 100)}%`;
badge.textContent = `🤖: ${Math.trunc(ai * 100)}%`; } else if (human) {
} else if (human) { badge.style.backgroundColor = "#8fe1a1";
badge.style.backgroundColor = "#8fe1a1"; badge.style.color = "#fff";
badge.style.color = "#fff"; badge.textContent = `👨‍💼: ${Math.trunc(human * 100)}%`;
badge.textContent = `👨‍💼: ${Math.trunc(human * 100)}%`;
} else {
badge.style.backgroundColor = "#ddd";
badge.style.color = "#fff";
badge.textContent = "🦊: confused";
}
} else { } else {
badge.style.backgroundColor = "#ddd"; badge.style.backgroundColor = "#ddd";
badge.style.color = "#fff"; badge.style.color = "#fff";

View file

@ -5,71 +5,65 @@
class Config<Key extends keyof typeof extensionConfig> { class Config<Key extends keyof typeof extensionConfig> {
key: Key; key: Key;
value: (typeof extensionConfig)[Key]["fallback"]; value: any;
insync = false;
constructor(key: Key) { constructor(key: Key) {
this.key = key; this.key = key;
this.value = $state(extensionConfig[key].fallback); this.value = $state((extensionConfig[key] as any).fallback);
this.load(); this.load();
$effect(() => { $effect(() => {
console.log("RESYNC"); const value = $state.snapshot(this.value);
extensionConfig[key].setValue(this.value as any); const timeout = setTimeout(() => {
if (!this.insync || this.value == value) return;
extensionConfig[this.key].setValue(value).catch((e) => {
console.error("Failed to write state back to storage", e);
});
}, 20);
return () => clearTimeout(timeout);
}); });
} }
async load() { async load() {
const config = await extensionConfig[this.key].getValue(); const config = await extensionConfig[this.key].getValue().catch((e) => {
console.error("Failed to sync state from storage", e);
});
this.value = config; this.value = config;
this.insync = true;
extensionConfig[this.key].watch((value) => { (extensionConfig[this.key] as any).watch((value: any) => {
if (value != this.value) this.value = value; if (value != this.value) this.value = value;
}); });
} }
} }
const config_ai_text = new Config("ai_text"); const config_ai_text = new Config("ai_text");
$inspect(config_ai_text.value);
const config_ai_image = new Config("ai_image"); const config_ai_image = new Config("ai_image");
$inspect(config_ai_image.value); const domain_whitelist = new Config("domain_whitelist");
const config_ai = new Config("ai");
$inspect(config_ai.value);
</script> </script>
<main class="min-w-115"> <main class="min-w-120 max-w-120 w-120 min-h-50 max-h-130">
<div class="tabs tabs-box min-h-50 max-h-max"> <div class="tabs tabs-box rounded-none">
<div class="tab pr-4">🦊</div> <div class="tab pr-4">🦊</div>
<input type="radio" name="cfgtabs0" class="tab" aria-label="General" checked={true} /> <input type="radio" name="cfgtabs0" class="tab" aria-label="General" checked={true} />
<div class="tab-content bg-base-100 border-base-300 p-2"></div> <div class="tab-content bg-base-100 border-base-300 p-2"></div>
<input type="radio" name="cfgtabs0" class="tab" aria-label="AI detector" /> <input type="radio" name="cfgtabs0" class="tab" aria-label="AI text" />
<div class="tab-content bg-base-100 border-base-300 p-2"> <div class="tab-content bg-base-100 border-base-300 p-2">
<fieldset class="fieldset border-base-300 border rounded-box p-2"> <fieldset class="fieldset border-base-300 border rounded-box p-2">
<legend class="fieldset-legend">AI Text detection</legend> <legend class="fieldset-legend">General</legend>
<label class="label"> <label class="label">
<input <input
type="checkbox" type="checkbox"
class="checkbox checkbox-sm" class="radio checked:radio-success radio-error"
bind:checked={config_ai_text.value.enabled} bind:checked={config_ai_text.value.enabled}
/> />
Enabled Enabled
</label> </label>
<label class="label"> <label class="label">
<input <input type="checkbox" class="radio" bind:checked={config_ai_text.value.show_ai} />
type="checkbox"
class="checkbox checkbox-sm"
bind:checked={config_ai_text.value.lite}
/>
Lite version (faster, less accurate)
</label>
<label class="label">
<input
type="checkbox"
class="checkbox checkbox-sm"
bind:checked={config_ai_text.value.show_ai}
/>
Show "AI generated" warnings Show "AI generated" warnings
<span <span
style="border-radius:4px;text-align:center;font-family:sans;font-size:10px;background-color:#ffc36d;color:#fff" style="border-radius:4px;text-align:center;font-family:sans;font-size:10px;background-color:#ffc36d;color:#fff"
@ -78,11 +72,7 @@
</span> </span>
</label> </label>
<label class="label"> <label class="label">
<input <input type="checkbox" class="radio" bind:checked={config_ai_text.value.show_human} />
type="checkbox"
class="checkbox checkbox-sm"
bind:checked={config_ai_text.value.show_human}
/>
Show "Human produced" labels Show "Human produced" labels
<span <span
style="border-radius:4px;text-align:center;font-family:sans;font-size:10px;background-color:#8fe1a1;color:#fff" style="border-radius:4px;text-align:center;font-family:sans;font-size:10px;background-color:#8fe1a1;color:#fff"
@ -90,11 +80,47 @@
👨‍💼: 99% 👨‍💼: 99%
</span> </span>
</label> </label>
<label class="label font-bold" for="textaimodelconf"> Report: </label> </fieldset>
<fieldset class="fieldset border-base-300 border rounded-box p-2">
<legend class="fieldset-legend">Mode</legend>
<label class="label"> <label class="label">
Any suspicion
<input <input
id="textaimodelconf01" type="radio"
class="radio"
name="config_ai_text.mode"
value="lite"
bind:group={config_ai_text.value.mode}
/>
Lite (the fastest, low RAM usage)
</label>
<label class="label">
<input
type="radio"
class="radio"
name="config_ai_text.mode"
value="mixed"
bind:group={config_ai_text.value.mode}
/>
Mixed (more accurate, pretty fast, highest RAM usage)
</label>
<label class="label">
<input
type="radio"
class="radio"
name="config_ai_text.mode"
value="hard"
bind:group={config_ai_text.value.mode}
/>
Hard (most accurate, slow, medium RAM usage)
</label>
</fieldset>
<fieldset class="fieldset border-base-300 border rounded-box p-2">
<legend class="fieldset-legend">Misc</legend>
<label class="label font-bold" for="config_ai_text-value-threshold">Show results: </label>
<label class="label">
Always
<input
id="config_ai_text-value-threshold"
type="range" type="range"
class="range range-xs" class="range range-xs"
min="0.1" min="0.1"
@ -105,30 +131,21 @@
When confident When confident
</label> </label>
</fieldset> </fieldset>
</div>
<input type="radio" name="cfgtabs0" class="tab" aria-label="AI images" />
<div class="tab-content bg-base-100 border-base-300 p-2">
<fieldset class="fieldset border-base-300 border rounded-box p-2"> <fieldset class="fieldset border-base-300 border rounded-box p-2">
<legend class="fieldset-legend">AI Image detection</legend> <legend class="fieldset-legend">General</legend>
<label class="label"> <label class="label">
<input <input
type="checkbox" type="checkbox"
class="checkbox checkbox-sm" class="radio checked:radio-success radio-error"
bind:checked={config_ai_image.value.enabled} bind:checked={config_ai_image.value.enabled}
/> />
Enabled Enabled
</label> </label>
<label class="label"> <label class="label">
<input <input type="checkbox" class="radio" bind:checked={config_ai_image.value.show_ai} />
type="checkbox"
class="checkbox checkbox-sm"
bind:checked={config_ai_image.value.lite}
/>
Lite version (faster, less accurate)
</label>
<label class="label">
<input
type="checkbox"
class="checkbox checkbox-sm"
bind:checked={config_ai_image.value.show_ai}
/>
Show "AI generated" warnings Show "AI generated" warnings
<span <span
style="border-radius:4px;text-align:center;font-family:sans;font-size:10px;background-color:#ffc36d;color:#fff" style="border-radius:4px;text-align:center;font-family:sans;font-size:10px;background-color:#ffc36d;color:#fff"
@ -137,11 +154,7 @@
</span> </span>
</label> </label>
<label class="label"> <label class="label">
<input <input type="checkbox" class="radio" bind:checked={config_ai_image.value.show_human} />
type="checkbox"
class="checkbox checkbox-sm"
bind:checked={config_ai_image.value.show_human}
/>
Show "Human produced" labels Show "Human produced" labels
<span <span
style="border-radius:4px;text-align:center;font-family:sans;font-size:10px;background-color:#8fe1a1;color:#fff" style="border-radius:4px;text-align:center;font-family:sans;font-size:10px;background-color:#8fe1a1;color:#fff"
@ -149,11 +162,47 @@
👨‍💼: 99% 👨‍💼: 99%
</span> </span>
</label> </label>
<label class="label font-bold" for="textaimodelconf"> Report: </label> </fieldset>
<fieldset class="fieldset border-base-300 border rounded-box p-2">
<legend class="fieldset-legend">Mode</legend>
<label class="label"> <label class="label">
Any suspicion
<input <input
id="textaimodelconf01" type="radio"
class="radio"
name="config_ai_image.mode"
value="lite"
bind:group={config_ai_image.value.mode}
/>
Lite (the fastest, low RAM usage)
</label>
<label class="label">
<input
type="radio"
class="radio"
name="config_ai_image.mode"
value="mixed"
bind:group={config_ai_image.value.mode}
/>
Mixed (more accurate, pretty fast, highest RAM usage)
</label>
<label class="label">
<input
type="radio"
class="radio"
name="config_ai_image.mode"
value="hard"
bind:group={config_ai_image.value.mode}
/>
Hard (most accurate, slow, medium RAM usage)
</label>
</fieldset>
<fieldset class="fieldset border-base-300 border rounded-box p-2">
<legend class="fieldset-legend">Misc</legend>
<label class="label font-bold" for="config_ai_image.threshold">Show results: </label>
<label class="label">
Always
<input
name="config_ai_image.threshold"
type="range" type="range"
class="range range-xs" class="range range-xs"
min="0.1" min="0.1"
@ -164,29 +213,34 @@
When confident When confident
</label> </label>
</fieldset> </fieldset>
<fieldset class="fieldset border-base-300 border rounded-box p-2"> <fieldset class="fieldset border-base-300 border rounded-box p-2">
<legend class="fieldset-legend"> Do not check on these sites: </legend> <legend class="fieldset-legend"> Do not check on these sites: </legend>
<div class="overflow-y-auto max-h-30 w-full"> <div class="flex gap-2 flex-wrap overflow-y-auto max-h-30 w-full">
{#each Object.entries(config_ai.value.domain_blacklist) as domain (domain[0])} {#each domain_whitelist.value as domain}
<div class="badge"> <div class="badge">
{domain[0]} {domain}
</div> </div>
{/each} {/each}
</div> </div>
</fieldset> </fieldset>
</div> </div>
<input type="radio" name="cfgtabs0" class="tab" aria-label="License, citations" /> <input type="radio" name="cfgtabs0" class="tab" aria-label="🛈" />
<div class="tab-content bg-base-100 border-base-300 p-2"> <div class="tab-content bg-base-100 border-base-300 p-2">
<p> <p>
The source code of this extension is available <a Firaid-moz webextension v{browser.runtime.getManifest().version}
class="link" by Spushk 🦊
href={browser.runtime.getManifest().homepage_url}>here</a </p>
<br />
<p>
The source code is available <a class="link" href={browser.runtime.getManifest().homepage_url}
>here</a
> under terms of Apache-2.0 license > under terms of Apache-2.0 license
</p> </p>
<br /> <br />
<p> <p>
The following people/organizations made this fun extention possible to create by training and The following people/organizations made it possible to create this by training and providing
providing ML models on <a class="link" href="https://huggingface.co">huggingface</a>: ML models on <a class="link" href="https://huggingface.co">huggingface</a>:
</p> </p>
<br /> <br />
<div class="csl-bib-body"> <div class="csl-bib-body">

View file

@ -1,2 +1,70 @@
@import "tailwindcss"; @import "tailwindcss";
@plugin "daisyui"; @plugin "daisyui";
@plugin "daisyui/theme" {
name: "spushk-dim";
default: false;
prefersdark: true;
color-scheme: "dark";
--color-base-100: #1f273e;
--color-base-200: #455474;
--color-base-300: #4f658a;
--color-base-content: oklch(96% 0.001 286.375);
--color-primary: #fe9735;
--color-primary-content: oklch(17.226% 0.028 139.549);
--color-secondary: oklch(83% 0.128 66.29);
--color-secondary-content: oklch(14.675% 0.033 35.353);
--color-accent: oklch(74.229% 0.133 311.379);
--color-accent-content: oklch(14.845% 0.026 311.379);
--color-neutral: oklch(24.731% 0.02 264.094);
--color-neutral-content: oklch(87% 0.006 286.286);
--color-info: oklch(86.078% 0.142 206.182);
--color-info-content: oklch(17.215% 0.028 206.182);
--color-success: oklch(86.171% 0.142 166.534);
--color-success-content: oklch(17.234% 0.028 166.534);
--color-warning: oklch(92% 0.12 95.746);
--color-warning-content: oklch(17.232% 0.028 94.818);
--color-error: oklch(89% 0.058 10.001);
--color-error-content: oklch(16.483% 0.019 33.756);
--radius-selector: 0.5rem;
--radius-field: 0.5rem;
--radius-box: 0.5rem;
--size-selector: 0.1875rem;
--size-field: 0.1875rem;
--border: 0.5px;
--depth: 0;
--noise: 0;
}
@plugin "daisyui/theme" {
name: "spushk-pastel";
default: true;
prefersdark: false;
color-scheme: "light";
--color-base-100: oklch(100% 0 0);
--color-base-200: oklch(98.462% 0.001 247.838);
--color-base-300: oklch(92.462% 0.001 247.838);
--color-base-content: oklch(20% 0 0);
--color-primary: #fe9735;
--color-primary-content: oklch(14% 0 0);
--color-secondary: oklch(83% 0.128 66.29);
--color-secondary-content: oklch(14% 0 0);
--color-accent: #c792e9;
--color-accent-content: oklch(14% 0 0);
--color-neutral: #455474;
--color-neutral-content: oklch(98% 0.002 247.839);
--color-info: oklch(86% 0.127 207.078);
--color-info-content: oklch(25% 0.09 281.288);
--color-success: oklch(87% 0.15 154.449);
--color-success-content: oklch(26% 0.065 152.934);
--color-warning: oklch(79% 0.184 86.047);
--color-warning-content: oklch(28% 0.066 53.813);
--color-error: oklch(51% 0.222 16.935);
--color-error-content: oklch(94% 0.03 12.58);
--radius-selector: 0.5rem;
--radius-field: 0.5rem;
--radius-box: 0.5rem;
--size-selector: 0.1875rem;
--size-field: 0.1875rem;
--border: 0.5px;
--depth: 0;
--noise: 0;
}

View file

@ -1,9 +1,6 @@
import { ExtensionConfigSchema } from "#imports";
import { defineExtensionStorage, ExtensionStorage } from "@webext-core/storage";
export interface ExtensionConfigAiText { export interface ExtensionConfigAiText {
enabled: boolean; enabled: boolean;
lite: boolean; mode: "lite" | "mixed" | "hard";
show_human: boolean; show_human: boolean;
show_ai: boolean; show_ai: boolean;
threshold: number; threshold: number;
@ -11,53 +8,30 @@ export interface ExtensionConfigAiText {
export interface ExtensionConfigAiImage { export interface ExtensionConfigAiImage {
enabled: boolean; enabled: boolean;
lite: boolean; mode: "lite" | "mixed" | "hard";
show_human: boolean; show_human: boolean;
show_ai: boolean; show_ai: boolean;
threshold: number; threshold: number;
} }
export interface ExtensionConfigAi {
/// shall contain domain qualified name, separated by dots
/// indexed by domain name
/// e.g. {
/// "github.com": ["github", "com"]
/// }
domain_blacklist: {
[domain: string]: string[];
};
}
// export interface ExtensionConfigSchema {
// version: string;
// ai_text: ExtensionConfigAiText;
// ai_image: ExtensionConfigAiImage;
// }
//export const extensionConfig = defineExtensionStorage<ExtensionConfigSchema>(browser.storage.local);
export const extensionConfig = { export const extensionConfig = {
ai: storage.defineItem<ExtensionConfigAi>("local:config:ai", { domain_whitelist: storage.defineItem("local:config:domain_whitelist", {
fallback: { fallback: [],
domain_blacklist: {}, init: () => ["asd.asd", "lkj.asd", "oiua.asd", "oi1u2e.aoisdu", "did0.asdiho"],
},
init: () => ({
domain_blacklist: {},
}),
version: 1, version: 1,
migrations: {}, migrations: {},
}), }),
ai_text: storage.defineItem<ExtensionConfigAiText>("local:config:ai_text", { ai_text: storage.defineItem<ExtensionConfigAiText>("local:config:ai_text", {
fallback: { fallback: {
enabled: true, enabled: false,
lite: true, mode: "lite",
show_ai: true, show_ai: true,
show_human: false, show_human: false,
threshold: 0.6, threshold: 0.6,
}, },
init: () => ({ init: () => ({
enabled: true, enabled: true,
lite: true, mode: "lite",
show_ai: true, show_ai: true,
show_human: false, show_human: false,
threshold: 0.6, threshold: 0.6,
@ -67,39 +41,39 @@ export const extensionConfig = {
}), }),
ai_image: storage.defineItem<ExtensionConfigAiImage>("local:config:ai_image", { ai_image: storage.defineItem<ExtensionConfigAiImage>("local:config:ai_image", {
fallback: { fallback: {
enabled: true, enabled: false,
lite: true, mode: "lite",
show_ai: true, show_ai: true,
show_human: false, show_human: false,
threshold: 0.6, threshold: 0.6,
}, },
init: () => ({ init: () => ({
enabled: true, enabled: true,
lite: true, mode: "lite",
show_ai: true, show_ai: true,
show_human: true, show_human: false,
threshold: 0.6, threshold: 0.6,
}), }),
version: 1, version: 1,
migrations: {}, migrations: {},
}), }),
} as const; };
export const EXTENSION_CONFIG: { export const EXTENSION_CONFIG: {
ai: ExtensionConfigAi; domain_whitelist: Set<string>;
ai_text: ExtensionConfigAiText; ai_text: ExtensionConfigAiText;
ai_image: ExtensionConfigAiImage; ai_image: ExtensionConfigAiImage;
} = { } = {
ai: extensionConfig.ai.fallback, domain_whitelist: new Set(extensionConfig.domain_whitelist.fallback),
ai_text: extensionConfig.ai_text.fallback, ai_text: extensionConfig.ai_text.fallback,
ai_image: extensionConfig.ai_image.fallback, ai_image: extensionConfig.ai_image.fallback,
}; };
extensionConfig.ai.getValue().then((config) => { extensionConfig.domain_whitelist.getValue().then((config) => {
EXTENSION_CONFIG.ai = config; EXTENSION_CONFIG.domain_whitelist = new Set(config);
extensionConfig.ai.watch((config) => { extensionConfig.domain_whitelist.watch((config) => {
EXTENSION_CONFIG.ai = config; EXTENSION_CONFIG.domain_whitelist = new Set(config);
}); });
}); });
@ -118,41 +92,3 @@ extensionConfig.ai_image.getValue().then((config) => {
EXTENSION_CONFIG.ai_image = config; EXTENSION_CONFIG.ai_image = config;
}); });
}); });
// extensionConfig.getItem("version").then(async (v) => {
// const current_version = browser.runtime.getManifest().version;
// if (v != current_version) {
// switch (v) {
// default:
// await config_migrate_fresh();
// }
// await extensionConfig.setItem("version", current_version);
// }
// });
// async function config_migrate_fresh() {
// await extensionConfig.clear();
// await extensionConfig.setItem("ai_text", {
// enabled: true,
// lite: true,
// show_ai: true,
// show_human: false,
// threshold: 0.6,
// });
// await extensionConfig.setItem("ai_image", {
// enabled: true,
// lite: true,
// show_ai: true,
// show_human: false,
// threshold: 0.6,
// });
// await extensionConfig.setItem("fun_githief", {
// enabled: false,
// });
// await extensionConfig.setItem("fun_microslop", {
// enabled: false,
// });
// await extensionConfig.setItem("fun_huyandex", {
// enabled: false,
// });
// }

View file

@ -1,19 +1,8 @@
/// Extracts meaningful portions of text from DOM, yielding the top matched nodes
///
/// e.g.
///
/// ```html
/// <div>
/// my hair are NOT
/// <span>
/// blue!
/// </span>
/// </div>
/// ```
///
/// Shall yield a reference to the `div` with text "my hair are NOT blue!"
const IS_DEV = import.meta.env.MODE == "development"; const IS_DEV = import.meta.env.MODE == "development";
const console = ExtConsole("domtraverse");
export const TRAVERSE_BEHAVOUR_IGNORE_MAGIC = Math.floor(Math.random() * 1000000).toString();
export const TRAVERSE_BEHAVOUR_ATTR = "data-firaid-moz-traverse-behavour";
const TRAVERSE_STACK_BANS: Set<string | null | undefined> = new Set([ const TRAVERSE_STACK_BANS: Set<string | null | undefined> = new Set([
"IMG", "IMG",
@ -50,28 +39,56 @@ const TRAVERSE_ALLOWED_ELEMENT_TAGS: Set<string | null | undefined> = new Set([
"I", "I",
"STRONG", "STRONG",
"EM", "EM",
"DEL",
]); ]);
const KNOWN_STACK_ROOTS: WeakMap<
Node,
{
csum: number;
}
> = new WeakMap();
export function* traverse_text(soft_max_words: number, hard_min_words: number) { export function* traverse_text(soft_max_words: number, hard_min_words: number) {
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, { const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, {
acceptNode: (node) => { acceptNode: (node) => {
// sanity check // sanity check
if (!node.isConnected) { if (node.parentNode?.nodeType != Node.ELEMENT_NODE || !node.parentElement) {
if (IS_DEV) {
node.parentElement?.setAttribute("data-firaid-moz-traverse", "REJECT-!IS_CONNECTED");
}
return NodeFilter.FILTER_REJECT; return NodeFilter.FILTER_REJECT;
} }
if (node.parentNode?.nodeType != Node.ELEMENT_NODE) { if (!node.isConnected) {
if (IS_DEV) { if (IS_DEV) {
node.parentElement?.setAttribute("data-firaid-moz-traverse", "REJECT-!ELEMENT_NODE"); node.parentElement.setAttribute("data-firaid-moz-traverse", "REJECT:disconnected");
} }
return NodeFilter.FILTER_REJECT; return NodeFilter.FILTER_REJECT;
} }
// filter elements // filter elements
if (!TRAVERSE_ALLOWED_ELEMENT_TAGS.has(node.parentElement?.tagName)) { if (!TRAVERSE_ALLOWED_ELEMENT_TAGS.has(node.parentElement.tagName)) {
if (IS_DEV) { if (IS_DEV) {
node.parentElement?.setAttribute("data-firaid-moz-traverse", "REJECT-!ALLOWED_TAG"); node.parentElement.setAttribute("data-firaid-moz-traverse", "REJECT:tag_not_allowed");
}
return NodeFilter.FILTER_REJECT;
}
// ignored elements
if (node.parentElement.getAttribute(TRAVERSE_BEHAVOUR_ATTR) == TRAVERSE_BEHAVOUR_IGNORE_MAGIC) {
if (IS_DEV) {
node.parentElement.setAttribute("data-firaid-moz-traverse", "REJECT:behavour_magic");
}
return NodeFilter.FILTER_REJECT;
}
// check visibility (my best effort, still mistakes sometimes)
if (
!node.parentElement.checkVisibility({
visibilityProperty: true,
}) ||
!node.parentElement.offsetParent ||
!node.parentElement.getBoundingClientRect().height
) {
// check visibility
if (IS_DEV) {
node.parentElement.setAttribute("data-firaid-moz-traverse", "REJECT:invisible");
} }
return NodeFilter.FILTER_REJECT; return NodeFilter.FILTER_REJECT;
} }
@ -80,17 +97,14 @@ export function* traverse_text(soft_max_words: number, hard_min_words: number) {
// Ignore anything inside of banned elements. // Ignore anything inside of banned elements.
// //
// For now I'll just limit the depth to 16 // For now I'll just limit the depth to 16
let checking = node.parentElement; let checking: Element | null = node.parentElement;
for (let i = 0; i < 16; i++) { for (let i = 0; i < 16; i++) {
if (!checking || checking.tagName == "BODY") { if (!checking || checking.tagName == "BODY") {
break; break;
} }
if (TRAVERSE_STACK_BANS.has(checking.tagName)) { if (TRAVERSE_STACK_BANS.has(checking.tagName)) {
if (IS_DEV) { if (IS_DEV) {
node.parentElement?.setAttribute( node.parentElement.setAttribute("data-firaid-moz-traverse", "REJECT:banned_ancestor");
"data-firaid-moz-traverse",
"REJECT-BANNED_ANCESTOR",
);
} }
return NodeFilter.FILTER_REJECT; return NodeFilter.FILTER_REJECT;
} }
@ -98,16 +112,16 @@ export function* traverse_text(soft_max_words: number, hard_min_words: number) {
} }
// drop with nonsense content // drop with nonsense content
if ((node.textContent?.trim()?.length || 0) < 8) { // if (!node.textContent?.trim()) {
if (IS_DEV) { // if (IS_DEV) {
node.parentElement?.setAttribute("data-firaid-moz-traverse", "REJECT-SHORT_TEXT"); // node.parentElement?.setAttribute("data-firaid-moz-traverse", "REJECT-NO_TEXT");
} // }
return NodeFilter.FILTER_REJECT; // return NodeFilter.FILTER_REJECT;
} // }
// finally good ones // finally good ones
if (IS_DEV) { if (IS_DEV) {
node.parentElement?.setAttribute("data-firaid-moz-traverse", "ACCEPT"); node.parentElement.setAttribute("data-firaid-moz-traverse", "ACCEPT");
} }
return NodeFilter.FILTER_ACCEPT; return NodeFilter.FILTER_ACCEPT;
}, },
@ -127,25 +141,15 @@ export function* traverse_text(soft_max_words: number, hard_min_words: number) {
rootable_parent = rootable_parent.parentElement; rootable_parent = rootable_parent.parentElement;
} }
if (!rootable_parent) { if (!rootable_parent) {
if (IS_DEV) {
node.parentElement?.setAttribute("data-firaid-moz-stack", "REJECTED-!ROOTABLE_PARENT");
}
continue; continue;
} }
let nodeText = node.textContent?.trim(); let nodeText = node.textContent?.trim();
if (!nodeText) { if (!nodeText) {
if (IS_DEV) {
node.parentElement?.setAttribute("data-firaid-moz-stack", "REJECTED-!TEXT");
}
continue; continue;
} }
let nodeTextWords = count_words_naive(nodeText); let nodeTextWords = count_words_naive(nodeText);
if (IS_DEV) {
node.parentElement?.setAttribute("data-firaid-moz-stack", "VISITED");
}
if (!stack) { if (!stack) {
stack = { stack = {
root: rootable_parent, root: rootable_parent,
@ -155,20 +159,19 @@ export function* traverse_text(soft_max_words: number, hard_min_words: number) {
words: nodeTextWords, words: nodeTextWords,
}; };
} else if (stack.root.contains(rootable_parent)) { } else if (stack.root.contains(rootable_parent)) {
if (IS_DEV) {
node.parentElement?.setAttribute("data-firaid-moz-stack", "MEMBER");
}
stack.text += " " + nodeText; stack.text += " " + nodeText;
stack.words += nodeTextWords; stack.words += nodeTextWords;
stack.last_node = node; stack.last_node = node;
} else { } else {
if (stack.words >= hard_min_words) { if (stack.words >= hard_min_words) {
yield stack; const known = KNOWN_STACK_ROOTS.get(stack.root);
if (IS_DEV) { const csum = text_csum(stack.text);
stack.root.parentElement?.setAttribute("data-firaid-moz-stack", "ROOT"); if (csum !== known?.csum) {
KNOWN_STACK_ROOTS.set(stack.root, { csum, ...known });
yield stack;
} else {
console.debug("Refuse to yield stack as matching csum exists");
} }
} else if (IS_DEV) {
stack.root.parentElement?.setAttribute("data-firaid-moz-stack", "SKIP-SHORT_TEXT");
} }
stack = { stack = {
root: rootable_parent, root: rootable_parent,
@ -180,13 +183,16 @@ export function* traverse_text(soft_max_words: number, hard_min_words: number) {
continue; continue;
} }
if (stack.words > soft_max_words) { if (stack && stack.words > soft_max_words) {
if (stack.words >= hard_min_words) { if (stack.words >= hard_min_words) {
if (IS_DEV) { const known = KNOWN_STACK_ROOTS.get(stack.root);
stack.root.parentElement?.setAttribute("data-firaid-moz-stack", "ROOT"); const csum = text_csum(stack.text);
if (csum !== known?.csum) {
KNOWN_STACK_ROOTS.set(stack.root, { csum, ...known });
yield stack;
} else {
console.debug("Refuse to yield stack as matching csum exists");
} }
} else if (IS_DEV) {
stack.root.parentElement?.setAttribute("data-firaid-moz-stack", "SKIP-SHORT_TEXT");
} }
stack = null; stack = null;
} }
@ -209,3 +215,13 @@ function count_words_naive(text: string): number {
return count; return count;
} }
function text_csum(text: string): number {
let csum = 5381;
for (let i = 0; i < text.length; i++) {
csum += (csum << 5) + text.charCodeAt(i);
csum &= csum;
}
return csum >>> 0;
}