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:
parent
f2b8bdca98
commit
b84eaaac82
7 changed files with 366 additions and 284 deletions
|
|
@ -24,14 +24,14 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "^4.2.1",
|
||||
"@tsconfig/svelte": "^5.0.7",
|
||||
"@tsconfig/svelte": "^5.0.8",
|
||||
"@wxt-dev/module-svelte": "^2.0.4",
|
||||
"daisyui": "^5.5.19",
|
||||
"svelte": "^5.50.0",
|
||||
"svelte-check": "^4.3.6",
|
||||
"svelte": "^5.53.10",
|
||||
"svelte-check": "^4.4.5",
|
||||
"tailwindcss": "^4.2.1",
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.9.3",
|
||||
"wxt": "^0.20.14"
|
||||
"wxt": "^0.20.18"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -106,36 +106,34 @@ function setup_inference_ai_text() {
|
|||
}
|
||||
|
||||
async function configure_ai_text(config: ExtensionConfigAiText) {
|
||||
if (config.enabled) {
|
||||
const loader = config.lite ? inference_ai_text_lite : inference_ai_text;
|
||||
const handler = config.lite ? AiTextLite : AiText;
|
||||
const capability = config.lite ? AiTextLiteCapability : AiTextCapability;
|
||||
const have = config.lite ? AI_TEXT_STATE.have_lite_model : AI_TEXT_STATE.have_normal_model;
|
||||
const set_have = config.lite
|
||||
? () => (AI_TEXT_STATE.have_lite_model = true)
|
||||
: () => (AI_TEXT_STATE.have_normal_model = true);
|
||||
|
||||
if (!have) {
|
||||
await loader();
|
||||
if (await guess_has_webgpu()) {
|
||||
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.",
|
||||
);
|
||||
// if (config.enabled) {
|
||||
// const loader = config.lite ? inference_ai_text_lite : inference_ai_text;
|
||||
// const handler = config.lite ? AiTextLite : AiText;
|
||||
// const capability = config.lite ? AiTextLiteCapability : AiTextCapability;
|
||||
// const have = config.lite ? AI_TEXT_STATE.have_lite_model : AI_TEXT_STATE.have_normal_model;
|
||||
// const set_have = config.lite
|
||||
// ? () => (AI_TEXT_STATE.have_lite_model = true)
|
||||
// : () => (AI_TEXT_STATE.have_normal_model = true);
|
||||
// if (!have) {
|
||||
// await loader();
|
||||
// if (await guess_has_webgpu()) {
|
||||
// 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.",
|
||||
// );
|
||||
// //await capability.assert_ndarray();
|
||||
// await capability.assert_webgpu();
|
||||
// } else {
|
||||
// await capability.assert_ndarray();
|
||||
await capability.assert_webgpu();
|
||||
} else {
|
||||
await capability.assert_ndarray();
|
||||
}
|
||||
set_have();
|
||||
}
|
||||
|
||||
AI_TEXT_STATE.handle = new handler();
|
||||
} else {
|
||||
AI_TEXT_STATE.handle = null;
|
||||
if (AI_TEXT_STATE.queue_inference_timer) {
|
||||
clearTimeout(AI_TEXT_STATE.queue_inference_timer);
|
||||
}
|
||||
}
|
||||
// }
|
||||
// set_have();
|
||||
// }
|
||||
// AI_TEXT_STATE.handle = new handler();
|
||||
// } else {
|
||||
// AI_TEXT_STATE.handle = null;
|
||||
// if (AI_TEXT_STATE.queue_inference_timer) {
|
||||
// clearTimeout(AI_TEXT_STATE.queue_inference_timer);
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
async function guess_has_webgpu() {
|
||||
|
|
|
|||
|
|
@ -1,21 +1,37 @@
|
|||
import { EXTENSION_CONFIG, ExtensionConfigAiImage, ExtensionConfigAiText } from "@/utils/config";
|
||||
import { traverse_text } from "@/utils/domtraverse";
|
||||
|
||||
const console = ExtConsole("content");
|
||||
|
||||
export default defineContentScript({
|
||||
matches: ["http://*/*", "https://*/*"],
|
||||
main() {
|
||||
const trigger = setTimeout(() => {
|
||||
DOM_OBSERVATORY.observer = new MutationObserver((_) => {
|
||||
if (DOM_OBSERVATORY.timer) {
|
||||
clearTimeout(DOM_OBSERVATORY.timer);
|
||||
}
|
||||
DOM_OBSERVATORY.timer = setTimeout(() => {
|
||||
console.debug("retraverse, cause: mutation");
|
||||
text_ai_traverse();
|
||||
}, 1000);
|
||||
|
||||
document.addEventListener("load", () => {
|
||||
clearTimeout(trigger);
|
||||
|
||||
text_ai_traverse();
|
||||
});
|
||||
|
||||
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(
|
||||
(entries, observer) => {
|
||||
for (const entry of entries) {
|
||||
|
|
@ -23,8 +39,7 @@ const TRAVERSE_VIEWPORT_OBSERVER = new IntersectionObserver(
|
|||
const stack = TRAVERSE_PARENT_TO_STACK.get(entry.target);
|
||||
|
||||
if (stack) {
|
||||
sendMessage("inference_aiText_classify", { text: stack.text })
|
||||
.then((result) => {
|
||||
sendMessage("inference_aiText_classify", { text: stack.text }).then((result) => {
|
||||
observer.unobserve(entry.target);
|
||||
|
||||
if (
|
||||
|
|
@ -42,8 +57,7 @@ const TRAVERSE_VIEWPORT_OBSERVER = new IntersectionObserver(
|
|||
} else {
|
||||
ai_clear_badge(entry.target);
|
||||
}
|
||||
})
|
||||
.catch((e) => {});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -63,6 +77,7 @@ interface WeakStack {
|
|||
}
|
||||
const TRAVERSE_PARENT_TO_STACK: WeakMap<Element, WeakStack> = new WeakMap();
|
||||
function text_ai_traverse() {
|
||||
console.debug("text_ai_traverse: traverse DOM");
|
||||
for (const stack of traverse_text(600, 20)) {
|
||||
let target = stack.first_node.parentElement || stack.root.parentElement;
|
||||
if (target) {
|
||||
|
|
@ -77,6 +92,7 @@ const SET_BADGES: WeakMap<Node, Element> = new WeakMap();
|
|||
function ai_set_badge(node: Node, ai?: number, human?: number) {
|
||||
const badge = document.createElement("span");
|
||||
|
||||
badge.setAttribute(TRAVERSE_BEHAVOUR_ATTR, TRAVERSE_BEHAVOUR_IGNORE_MAGIC);
|
||||
badge.style.display = "inline";
|
||||
badge.style.borderRadius = "4px";
|
||||
badge.style.textAlign = "center";
|
||||
|
|
@ -84,7 +100,6 @@ function ai_set_badge(node: Node, ai?: number, human?: number) {
|
|||
badge.style.fontSize = "12px";
|
||||
badge.style.padding = "1px";
|
||||
badge.style.marginRight = "4px";
|
||||
if (ai || human) {
|
||||
if (ai) {
|
||||
badge.style.backgroundColor = "#ffc36d";
|
||||
badge.style.color = "#fff";
|
||||
|
|
@ -93,11 +108,6 @@ function ai_set_badge(node: Node, ai?: number, human?: number) {
|
|||
badge.style.backgroundColor = "#8fe1a1";
|
||||
badge.style.color = "#fff";
|
||||
badge.textContent = `👨💼: ${Math.trunc(human * 100)}%`;
|
||||
} else {
|
||||
badge.style.backgroundColor = "#ddd";
|
||||
badge.style.color = "#fff";
|
||||
badge.textContent = "🦊: confused";
|
||||
}
|
||||
} else {
|
||||
badge.style.backgroundColor = "#ddd";
|
||||
badge.style.color = "#fff";
|
||||
|
|
|
|||
|
|
@ -5,71 +5,65 @@
|
|||
|
||||
class Config<Key extends keyof typeof extensionConfig> {
|
||||
key: Key;
|
||||
value: (typeof extensionConfig)[Key]["fallback"];
|
||||
value: any;
|
||||
insync = false;
|
||||
|
||||
constructor(key: Key) {
|
||||
this.key = key;
|
||||
this.value = $state(extensionConfig[key].fallback);
|
||||
this.value = $state((extensionConfig[key] as any).fallback);
|
||||
this.load();
|
||||
|
||||
$effect(() => {
|
||||
console.log("RESYNC");
|
||||
extensionConfig[key].setValue(this.value as any);
|
||||
const value = $state.snapshot(this.value);
|
||||
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() {
|
||||
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.insync = true;
|
||||
|
||||
extensionConfig[this.key].watch((value) => {
|
||||
(extensionConfig[this.key] as any).watch((value: any) => {
|
||||
if (value != this.value) this.value = value;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const config_ai_text = new Config("ai_text");
|
||||
$inspect(config_ai_text.value);
|
||||
|
||||
const config_ai_image = new Config("ai_image");
|
||||
$inspect(config_ai_image.value);
|
||||
|
||||
const config_ai = new Config("ai");
|
||||
$inspect(config_ai.value);
|
||||
const domain_whitelist = new Config("domain_whitelist");
|
||||
</script>
|
||||
|
||||
<main class="min-w-115">
|
||||
<div class="tabs tabs-box min-h-50 max-h-max">
|
||||
<main class="min-w-120 max-w-120 w-120 min-h-50 max-h-130">
|
||||
<div class="tabs tabs-box rounded-none">
|
||||
<div class="tab pr-4">🦊</div>
|
||||
<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>
|
||||
<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">
|
||||
<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">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="checkbox checkbox-sm"
|
||||
class="radio checked:radio-success radio-error"
|
||||
bind:checked={config_ai_text.value.enabled}
|
||||
/>
|
||||
Enabled
|
||||
</label>
|
||||
<label class="label">
|
||||
<input
|
||||
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}
|
||||
/>
|
||||
<input type="checkbox" class="radio" bind:checked={config_ai_text.value.show_ai} />
|
||||
Show "AI generated" warnings
|
||||
<span
|
||||
style="border-radius:4px;text-align:center;font-family:sans;font-size:10px;background-color:#ffc36d;color:#fff"
|
||||
|
|
@ -78,11 +72,7 @@
|
|||
</span>
|
||||
</label>
|
||||
<label class="label">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="checkbox checkbox-sm"
|
||||
bind:checked={config_ai_text.value.show_human}
|
||||
/>
|
||||
<input type="checkbox" class="radio" bind:checked={config_ai_text.value.show_human} />
|
||||
Show "Human produced" labels
|
||||
<span
|
||||
style="border-radius:4px;text-align:center;font-family:sans;font-size:10px;background-color:#8fe1a1;color:#fff"
|
||||
|
|
@ -90,11 +80,47 @@
|
|||
👨💼: 99%
|
||||
</span>
|
||||
</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">
|
||||
Any suspicion
|
||||
<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"
|
||||
class="range range-xs"
|
||||
min="0.1"
|
||||
|
|
@ -105,30 +131,21 @@
|
|||
When confident
|
||||
</label>
|
||||
</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">
|
||||
<legend class="fieldset-legend">AI Image detection</legend>
|
||||
<legend class="fieldset-legend">General</legend>
|
||||
<label class="label">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="checkbox checkbox-sm"
|
||||
class="radio checked:radio-success radio-error"
|
||||
bind:checked={config_ai_image.value.enabled}
|
||||
/>
|
||||
Enabled
|
||||
</label>
|
||||
<label class="label">
|
||||
<input
|
||||
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}
|
||||
/>
|
||||
<input type="checkbox" class="radio" bind:checked={config_ai_image.value.show_ai} />
|
||||
Show "AI generated" warnings
|
||||
<span
|
||||
style="border-radius:4px;text-align:center;font-family:sans;font-size:10px;background-color:#ffc36d;color:#fff"
|
||||
|
|
@ -137,11 +154,7 @@
|
|||
</span>
|
||||
</label>
|
||||
<label class="label">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="checkbox checkbox-sm"
|
||||
bind:checked={config_ai_image.value.show_human}
|
||||
/>
|
||||
<input type="checkbox" class="radio" bind:checked={config_ai_image.value.show_human} />
|
||||
Show "Human produced" labels
|
||||
<span
|
||||
style="border-radius:4px;text-align:center;font-family:sans;font-size:10px;background-color:#8fe1a1;color:#fff"
|
||||
|
|
@ -149,11 +162,47 @@
|
|||
👨💼: 99%
|
||||
</span>
|
||||
</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">
|
||||
Any suspicion
|
||||
<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"
|
||||
class="range range-xs"
|
||||
min="0.1"
|
||||
|
|
@ -164,29 +213,34 @@
|
|||
When confident
|
||||
</label>
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="fieldset border-base-300 border rounded-box p-2">
|
||||
<legend class="fieldset-legend"> Do not check on these sites: </legend>
|
||||
<div class="overflow-y-auto max-h-30 w-full">
|
||||
{#each Object.entries(config_ai.value.domain_blacklist) as domain (domain[0])}
|
||||
<div class="flex gap-2 flex-wrap overflow-y-auto max-h-30 w-full">
|
||||
{#each domain_whitelist.value as domain}
|
||||
<div class="badge">
|
||||
{domain[0]}
|
||||
{domain}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</fieldset>
|
||||
</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">
|
||||
<p>
|
||||
The source code of this extension is available <a
|
||||
class="link"
|
||||
href={browser.runtime.getManifest().homepage_url}>here</a
|
||||
Firaid-moz webextension v{browser.runtime.getManifest().version}
|
||||
by Spushk 🦊
|
||||
</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
|
||||
</p>
|
||||
<br />
|
||||
<p>
|
||||
The following people/organizations made this fun extention possible to create by training and
|
||||
providing ML models on <a class="link" href="https://huggingface.co">huggingface</a>:
|
||||
The following people/organizations made it possible to create this by training and providing
|
||||
ML models on <a class="link" href="https://huggingface.co">huggingface</a>:
|
||||
</p>
|
||||
<br />
|
||||
<div class="csl-bib-body">
|
||||
|
|
|
|||
|
|
@ -1,2 +1,70 @@
|
|||
@import "tailwindcss";
|
||||
@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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
import { ExtensionConfigSchema } from "#imports";
|
||||
import { defineExtensionStorage, ExtensionStorage } from "@webext-core/storage";
|
||||
|
||||
export interface ExtensionConfigAiText {
|
||||
enabled: boolean;
|
||||
lite: boolean;
|
||||
mode: "lite" | "mixed" | "hard";
|
||||
show_human: boolean;
|
||||
show_ai: boolean;
|
||||
threshold: number;
|
||||
|
|
@ -11,53 +8,30 @@ export interface ExtensionConfigAiText {
|
|||
|
||||
export interface ExtensionConfigAiImage {
|
||||
enabled: boolean;
|
||||
lite: boolean;
|
||||
mode: "lite" | "mixed" | "hard";
|
||||
show_human: boolean;
|
||||
show_ai: boolean;
|
||||
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 = {
|
||||
ai: storage.defineItem<ExtensionConfigAi>("local:config:ai", {
|
||||
fallback: {
|
||||
domain_blacklist: {},
|
||||
},
|
||||
init: () => ({
|
||||
domain_blacklist: {},
|
||||
}),
|
||||
domain_whitelist: storage.defineItem("local:config:domain_whitelist", {
|
||||
fallback: [],
|
||||
init: () => ["asd.asd", "lkj.asd", "oiua.asd", "oi1u2e.aoisdu", "did0.asdiho"],
|
||||
version: 1,
|
||||
migrations: {},
|
||||
}),
|
||||
ai_text: storage.defineItem<ExtensionConfigAiText>("local:config:ai_text", {
|
||||
fallback: {
|
||||
enabled: true,
|
||||
lite: true,
|
||||
enabled: false,
|
||||
mode: "lite",
|
||||
show_ai: true,
|
||||
show_human: false,
|
||||
threshold: 0.6,
|
||||
},
|
||||
init: () => ({
|
||||
enabled: true,
|
||||
lite: true,
|
||||
mode: "lite",
|
||||
show_ai: true,
|
||||
show_human: false,
|
||||
threshold: 0.6,
|
||||
|
|
@ -67,39 +41,39 @@ export const extensionConfig = {
|
|||
}),
|
||||
ai_image: storage.defineItem<ExtensionConfigAiImage>("local:config:ai_image", {
|
||||
fallback: {
|
||||
enabled: true,
|
||||
lite: true,
|
||||
enabled: false,
|
||||
mode: "lite",
|
||||
show_ai: true,
|
||||
show_human: false,
|
||||
threshold: 0.6,
|
||||
},
|
||||
init: () => ({
|
||||
enabled: true,
|
||||
lite: true,
|
||||
mode: "lite",
|
||||
show_ai: true,
|
||||
show_human: true,
|
||||
show_human: false,
|
||||
threshold: 0.6,
|
||||
}),
|
||||
version: 1,
|
||||
migrations: {},
|
||||
}),
|
||||
} as const;
|
||||
};
|
||||
|
||||
export const EXTENSION_CONFIG: {
|
||||
ai: ExtensionConfigAi;
|
||||
domain_whitelist: Set<string>;
|
||||
ai_text: ExtensionConfigAiText;
|
||||
ai_image: ExtensionConfigAiImage;
|
||||
} = {
|
||||
ai: extensionConfig.ai.fallback,
|
||||
domain_whitelist: new Set(extensionConfig.domain_whitelist.fallback),
|
||||
ai_text: extensionConfig.ai_text.fallback,
|
||||
ai_image: extensionConfig.ai_image.fallback,
|
||||
};
|
||||
|
||||
extensionConfig.ai.getValue().then((config) => {
|
||||
EXTENSION_CONFIG.ai = config;
|
||||
extensionConfig.domain_whitelist.getValue().then((config) => {
|
||||
EXTENSION_CONFIG.domain_whitelist = new Set(config);
|
||||
|
||||
extensionConfig.ai.watch((config) => {
|
||||
EXTENSION_CONFIG.ai = config;
|
||||
extensionConfig.domain_whitelist.watch((config) => {
|
||||
EXTENSION_CONFIG.domain_whitelist = new Set(config);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -118,41 +92,3 @@ extensionConfig.ai_image.getValue().then((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,
|
||||
// });
|
||||
// }
|
||||
|
|
|
|||
|
|
@ -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 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([
|
||||
"IMG",
|
||||
|
|
@ -50,28 +39,56 @@ const TRAVERSE_ALLOWED_ELEMENT_TAGS: Set<string | null | undefined> = new Set([
|
|||
"I",
|
||||
"STRONG",
|
||||
"EM",
|
||||
"DEL",
|
||||
]);
|
||||
const KNOWN_STACK_ROOTS: WeakMap<
|
||||
Node,
|
||||
{
|
||||
csum: number;
|
||||
}
|
||||
> = new WeakMap();
|
||||
|
||||
export function* traverse_text(soft_max_words: number, hard_min_words: number) {
|
||||
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, {
|
||||
acceptNode: (node) => {
|
||||
// sanity check
|
||||
if (!node.isConnected) {
|
||||
if (IS_DEV) {
|
||||
node.parentElement?.setAttribute("data-firaid-moz-traverse", "REJECT-!IS_CONNECTED");
|
||||
}
|
||||
if (node.parentNode?.nodeType != Node.ELEMENT_NODE || !node.parentElement) {
|
||||
return NodeFilter.FILTER_REJECT;
|
||||
}
|
||||
if (node.parentNode?.nodeType != Node.ELEMENT_NODE) {
|
||||
if (!node.isConnected) {
|
||||
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;
|
||||
}
|
||||
|
||||
// filter elements
|
||||
if (!TRAVERSE_ALLOWED_ELEMENT_TAGS.has(node.parentElement?.tagName)) {
|
||||
if (!TRAVERSE_ALLOWED_ELEMENT_TAGS.has(node.parentElement.tagName)) {
|
||||
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;
|
||||
}
|
||||
|
|
@ -80,17 +97,14 @@ export function* traverse_text(soft_max_words: number, hard_min_words: number) {
|
|||
// Ignore anything inside of banned elements.
|
||||
//
|
||||
// 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++) {
|
||||
if (!checking || checking.tagName == "BODY") {
|
||||
break;
|
||||
}
|
||||
if (TRAVERSE_STACK_BANS.has(checking.tagName)) {
|
||||
if (IS_DEV) {
|
||||
node.parentElement?.setAttribute(
|
||||
"data-firaid-moz-traverse",
|
||||
"REJECT-BANNED_ANCESTOR",
|
||||
);
|
||||
node.parentElement.setAttribute("data-firaid-moz-traverse", "REJECT:banned_ancestor");
|
||||
}
|
||||
return NodeFilter.FILTER_REJECT;
|
||||
}
|
||||
|
|
@ -98,16 +112,16 @@ export function* traverse_text(soft_max_words: number, hard_min_words: number) {
|
|||
}
|
||||
|
||||
// drop with nonsense content
|
||||
if ((node.textContent?.trim()?.length || 0) < 8) {
|
||||
if (IS_DEV) {
|
||||
node.parentElement?.setAttribute("data-firaid-moz-traverse", "REJECT-SHORT_TEXT");
|
||||
}
|
||||
return NodeFilter.FILTER_REJECT;
|
||||
}
|
||||
// if (!node.textContent?.trim()) {
|
||||
// if (IS_DEV) {
|
||||
// node.parentElement?.setAttribute("data-firaid-moz-traverse", "REJECT-NO_TEXT");
|
||||
// }
|
||||
// return NodeFilter.FILTER_REJECT;
|
||||
// }
|
||||
|
||||
// finally good ones
|
||||
if (IS_DEV) {
|
||||
node.parentElement?.setAttribute("data-firaid-moz-traverse", "ACCEPT");
|
||||
node.parentElement.setAttribute("data-firaid-moz-traverse", "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;
|
||||
}
|
||||
if (!rootable_parent) {
|
||||
if (IS_DEV) {
|
||||
node.parentElement?.setAttribute("data-firaid-moz-stack", "REJECTED-!ROOTABLE_PARENT");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
let nodeText = node.textContent?.trim();
|
||||
if (!nodeText) {
|
||||
if (IS_DEV) {
|
||||
node.parentElement?.setAttribute("data-firaid-moz-stack", "REJECTED-!TEXT");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
let nodeTextWords = count_words_naive(nodeText);
|
||||
|
||||
if (IS_DEV) {
|
||||
node.parentElement?.setAttribute("data-firaid-moz-stack", "VISITED");
|
||||
}
|
||||
|
||||
if (!stack) {
|
||||
stack = {
|
||||
root: rootable_parent,
|
||||
|
|
@ -155,20 +159,19 @@ export function* traverse_text(soft_max_words: number, hard_min_words: number) {
|
|||
words: nodeTextWords,
|
||||
};
|
||||
} else if (stack.root.contains(rootable_parent)) {
|
||||
if (IS_DEV) {
|
||||
node.parentElement?.setAttribute("data-firaid-moz-stack", "MEMBER");
|
||||
}
|
||||
stack.text += " " + nodeText;
|
||||
stack.words += nodeTextWords;
|
||||
stack.last_node = node;
|
||||
} else {
|
||||
if (stack.words >= hard_min_words) {
|
||||
const known = KNOWN_STACK_ROOTS.get(stack.root);
|
||||
const csum = text_csum(stack.text);
|
||||
if (csum !== known?.csum) {
|
||||
KNOWN_STACK_ROOTS.set(stack.root, { csum, ...known });
|
||||
yield stack;
|
||||
if (IS_DEV) {
|
||||
stack.root.parentElement?.setAttribute("data-firaid-moz-stack", "ROOT");
|
||||
} 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 = {
|
||||
root: rootable_parent,
|
||||
|
|
@ -180,13 +183,16 @@ export function* traverse_text(soft_max_words: number, hard_min_words: number) {
|
|||
continue;
|
||||
}
|
||||
|
||||
if (stack.words > soft_max_words) {
|
||||
if (stack && stack.words > soft_max_words) {
|
||||
if (stack.words >= hard_min_words) {
|
||||
if (IS_DEV) {
|
||||
stack.root.parentElement?.setAttribute("data-firaid-moz-stack", "ROOT");
|
||||
const known = KNOWN_STACK_ROOTS.get(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;
|
||||
}
|
||||
|
|
@ -209,3 +215,13 @@ function count_words_naive(text: string): number {
|
|||
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue