Initial Commit

This commit is contained in:
Jordan Wages 2025-06-15 21:55:35 -05:00
commit eb02c6f3bd
13 changed files with 718 additions and 0 deletions

View file

@ -0,0 +1,8 @@
{
"classification": { "message": "AI classification" },
"matches": { "message": "matches" },
"doesntMatch": { "message": "doesn't match" },
"options.title": { "message": "AI Filter Options" },
"options.endpoint": { "message": "Endpoint" },
"options.save": { "message": "Save" }
}

64
ai-filter.sln Normal file
View file

@ -0,0 +1,64 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.12.35707.178
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{BCC6E6D2-343B-4C48-854D-5FE3BBC3CB70}"
ProjectSection(SolutionItems) = preProject
background.js = background.js
build-xpi.ps1 = build-xpi.ps1
manifest.json = manifest.json
EndProjectSection
ProjectSection(FolderGlobals) = preProject
Q_5_4Users_4Jordan_4Documents_4Gitea_4thunderbird-ai-filter_4src_4manifest_1json__JsonSchema =
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "experiment", "experiment", "{F2C8C786-FA23-4B63-934C-8CAA39D9BF95}"
ProjectSection(SolutionItems) = preProject
experiment\api.js = experiment\api.js
experiment\schema.json = experiment\schema.json
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "options", "options", "{7372FCA6-B0BC-4968-A748-7ABB17A7929A}"
ProjectSection(SolutionItems) = preProject
options\options.html = options\options.html
options\options.js = options\options.js
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "modules", "modules", "{75ED3C1E-D3C7-4546-9F2E-AC85859DDF4B}"
ProjectSection(SolutionItems) = preProject
modules\ExpressionSearchFilter.jsm = modules\ExpressionSearchFilter.jsm
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_locales", "_locales", "{D446E5C6-BDDE-4091-BD1A-EC57170003CF}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "en-US", "en-US", "{8BEA7793-3336-40ED-AB96-7FFB09FEB0F6}"
ProjectSection(SolutionItems) = preProject
_locales\en-US\messages.json = _locales\en-US\messages.json
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "DomContentScript", "DomContentScript", "{B334FFB0-4BD2-496E-BDC4-786620E019DA}"
ProjectSection(SolutionItems) = preProject
experiment\DomContentScript\implementation.js = experiment\DomContentScript\implementation.js
experiment\DomContentScript\schema.json = experiment\DomContentScript\schema.json
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "content", "content", "{028FDA4B-AC3E-4A0E-9291-978E213F9B78}"
ProjectSection(SolutionItems) = preProject
content\filterEditor.js = content\filterEditor.js
EndProjectSection
EndProject
Global
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{F2C8C786-FA23-4B63-934C-8CAA39D9BF95} = {BCC6E6D2-343B-4C48-854D-5FE3BBC3CB70}
{7372FCA6-B0BC-4968-A748-7ABB17A7929A} = {BCC6E6D2-343B-4C48-854D-5FE3BBC3CB70}
{75ED3C1E-D3C7-4546-9F2E-AC85859DDF4B} = {BCC6E6D2-343B-4C48-854D-5FE3BBC3CB70}
{D446E5C6-BDDE-4091-BD1A-EC57170003CF} = {BCC6E6D2-343B-4C48-854D-5FE3BBC3CB70}
{8BEA7793-3336-40ED-AB96-7FFB09FEB0F6} = {D446E5C6-BDDE-4091-BD1A-EC57170003CF}
{B334FFB0-4BD2-496E-BDC4-786620E019DA} = {F2C8C786-FA23-4B63-934C-8CAA39D9BF95}
{028FDA4B-AC3E-4A0E-9291-978E213F9B78} = {BCC6E6D2-343B-4C48-854D-5FE3BBC3CB70}
EndGlobalSection
EndGlobal

69
background.js Normal file
View file

@ -0,0 +1,69 @@
/*
* Runs in the **WebExtension (addon)** context.
* For this minimal working version we only expose an async helper
* so UI pages / devtools panels can test the classifier without
* needing Thunderbirds filter engine.
*
* Note: the filter-engine itself NEVER calls this file the
* synchronous work is all done in experiment/api.js (chrome side).
*/
"use strict";
// Startup
console.log("[ai-filter] background.js loaded ready to classify");
(async () => {
try {
const store = await browser.storage.local.get(["endpoint"]);
await browser.aiFilter.initConfig(store);
console.log("[ai-filter] configuration loaded", store);
try {
await browser.DomContentScript.registerWindow(
"chrome://messenger/content/FilterEditor.xhtml",
"resource://aifilter/content/filterEditor.js"
);
console.log("[ai-filter] registered FilterEditor content script");
} catch (e) {
console.error("[ai-filter] failed to register content script", e);
}
} catch (err) {
console.error("[ai-filter] failed to load config:", err);
}
})();
// Listen for messages from UI/devtools
browser.runtime.onMessage.addListener((msg) => {
console.log("[ai-filter] onMessage received:", msg);
if (msg?.type === "aiFilter:test") {
const { text = "", criterion = "" } = msg;
console.log("[ai-filter] aiFilter:test text:", text);
console.log("[ai-filter] aiFilter:test criterion:", criterion);
try {
console.log("[ai-filter] Calling browser.aiFilter.classify()");
const result = browser.aiFilter.classify(text, criterion);
console.log("[ai-filter] classify() returned:", result);
return { match: result };
}
catch (err) {
console.error("[ai-filter] Error in classify():", err);
// rethrow so the caller sees the failure
throw err;
}
}
else {
console.warn("[ai-filter] Unknown message type, ignoring:", msg?.type);
}
});
// Catch any unhandled rejections
window.addEventListener("unhandledrejection", ev => {
console.error("[ai-filter] Unhandled promise rejection:", ev.reason);
});
browser.runtime.onInstalled.addListener(async ({ reason }) => {
if (reason === "install") {
await browser.runtime.openOptionsPage();
}
});

75
build-xpi.ps1 Normal file
View file

@ -0,0 +1,75 @@
<#
.SYNOPSIS
Bullet-proof packager: uses .NET ZipFile to preserve folders.
.DESCRIPTION
Reads version from manifest.json (no comments allowed)
Gathers all files under the project (excludes .sln, .ps1, release/, .vs/, .git/)
Creates a .zip with each entrys name set to its relative path
Renames .zip .xpi
#>
# 1) Locate
$ScriptDir = Split-Path $MyInvocation.MyCommand.Path
$ReleaseDir = Join-Path $ScriptDir 'release'
$Manifest = Join-Path $ScriptDir 'manifest.json'
# 2) Prep release folder
if (-not (Test-Path $ReleaseDir)) {
New-Item -ItemType Directory -Path $ReleaseDir | Out-Null
}
# 3) Read manifest.json (must be pure JSON)
$version = (Get-Content $Manifest -Raw | ConvertFrom-Json).version
if (-not $version) {
Write-Error "No version found in manifest.json"; exit 1
}
# 4) Define output names & clean up
$xpiName = "ai-filter-$version.xpi"
$zipPath = Join-Path $ReleaseDir "ai-filter-$version.zip"
$xpiPath = Join-Path $ReleaseDir $xpiName
Remove-Item -Path $zipPath,$xpiPath -Force -ErrorAction SilentlyContinue
# 5) Collect files to include
$allFiles = Get-ChildItem -Path $ScriptDir -Recurse -File |
Where-Object {
$_.Extension -notin '.sln','.ps1' -and
$_.FullName -notmatch '\\release\\' -and
$_.FullName -notmatch '\\.vs\\' -and
$_.FullName -notmatch '\\.git\\'
}
foreach ($file in $allFiles) {
$size = (Get-Item $file.FullName).Length
Write-Host "Zipping: $entryName$($file.FullName) ($size bytes)"
}
if ($allFiles.Count -eq 0) {
Write-Warning "No files found to package."; exit 0
}
# 6) Load .NET ZipFile
Add-Type -AssemblyName System.IO.Compression.FileSystem
# 7) Create zip and add each file with its relative path
$zip = [System.IO.Compression.ZipFile]::Open($zipPath, 'Create')
foreach ($file in $allFiles) {
# Compute entry name (relative, forward-slashed)
$rel = $file.FullName.Substring($ScriptDir.Length + 1).TrimStart('\')
$entryName = $rel.Replace('\', '/')
[System.IO.Compression.ZipFileExtensions]::CreateEntryFromFile(
$zip,
$file.FullName,
$entryName,
[System.IO.Compression.CompressionLevel]::Optimal
)
}
$zip.Dispose()
# 8) Rename zip → xpi
Rename-Item -Path $zipPath -NewName $xpiName -Force
Write-Host "✅ Built XPI at: $xpiPath"

50
content/filterEditor.js Normal file
View file

@ -0,0 +1,50 @@
(function() {
function patch(container) {
if (!container || container.getAttribute("ai-filter-patched") === "true") {
return;
}
while (container.firstChild) {
container.firstChild.remove();
}
let frag = window.MozXULElement.parseXULToFragment(
`<html:input class="search-value-textbox flexinput ai-filter-textbox" inherits="disabled"
onchange="this.parentNode.setAttribute('value', this.value); this.parentNode.value=this.value;">
</html:input>`
);
container.appendChild(frag);
if (container.hasAttribute("value")) {
container.firstChild.value = container.getAttribute("value");
}
container.classList.add("flexelementcontainer");
container.setAttribute("ai-filter-patched", "true");
}
function check(node) {
if (!(node instanceof Element)) {
return;
}
if (
node.classList.contains("search-value-custom") &&
node.getAttribute("searchAttribute") === "aifilter#classification"
) {
patch(node);
}
node
.querySelectorAll('.search-value-custom[searchAttribute="aifilter#classification"]')
.forEach(patch);
}
const observer = new MutationObserver(mutations => {
for (let mutation of mutations) {
if (mutation.type === "childList") {
mutation.addedNodes.forEach(check);
} else if (mutation.type === "attributes") {
check(mutation.target);
}
}
});
const termList = document.getElementById("searchTermList") || document;
observer.observe(termList, { childList: true, attributes: true, subtree: true });
check(termList);
})();

View file

@ -0,0 +1,80 @@
var { AppConstants } = ChromeUtils.importESModule("resource://gre/modules/AppConstants.sys.mjs");
var DomContent_ESM = parseInt(AppConstants.MOZ_APP_VERSION, 10) >= 128;
var { ExtensionCommon } = ChromeUtils.importESModule(
"resource://gre/modules/ExtensionCommon.sys.mjs"
);
var { ExtensionUtils } = DomContent_ESM
? ChromeUtils.importESModule("resource://gre/modules/ExtensionUtils.sys.mjs")
: ChromeUtils.import("resource://gre/modules/ExtensionUtils.jsm");
var { ExtensionError } = ExtensionUtils;
var registeredWindows = new Map();
var DomContentScript = class extends ExtensionCommon.ExtensionAPI {
constructor(extension) {
super(extension);
this._windowListener = {
// nsIWindowMediatorListener functions
onOpenWindow(appWindow) {
// A new window has opened.
let domWindow = appWindow.docShell.domWindow;
/**
* Set up listeners to run the callbacks on the given window.
*
* @param aWindow {nsIDOMWindow} The window to set up.
* @param aID {String} Optional. ID of the new caller that has registered right now.
*/
domWindow.addEventListener(
"DOMContentLoaded",
function() {
// do stuff
let windowChromeURL = domWindow.document.location.href;
if (registeredWindows.has(windowChromeURL)) {
let jsPath = registeredWindows.get(windowChromeURL);
Services.scriptloader.loadSubScript(jsPath, domWindow, "UTF-8");
}
},
{ once: true }
);
},
onCloseWindow(appWindow) {
// One of the windows has closed.
let domWindow = appWindow.docShell.domWindow; // we don't need to do anything (script only loads once)
},
};
Services.wm.addListener(this._windowListener);
}
onShutdown(isAppShutdown) {
if (isAppShutdown) {
return; // the application gets unloaded anyway
}
Services.wm.removeListener(this._windowListener);
}
getAPI(context) {
/** API IMPLEMENTATION **/
return {
DomContentScript: {
// only returns something, if a user pref value is set
registerWindow: async function (windowUrl,jsPath) {
registeredWindows.set(windowUrl,jsPath);
}
},
};
}
};

View file

@ -0,0 +1,25 @@
[
{
"namespace": "DomContentScript",
"functions": [
{
"name": "registerWindow",
"type": "function",
"async": true,
"description": "Register a script for onDOMContentLoaded",
"parameters": [
{
"name": "windowUrl",
"type": "string",
"description": "chrome URL of the window "
},
{
"name": "jsPath",
"type": "string",
"description": "chrome URL of the script"
}
]
}
]
}
]

89
experiment/api.js Normal file
View file

@ -0,0 +1,89 @@
var { ExtensionCommon } = ChromeUtils.importESModule("resource://gre/modules/ExtensionCommon.sys.mjs");
var { Services } = globalThis || ChromeUtils.importESModule("resource://gre/modules/Services.sys.mjs");
var { MailServices } = ChromeUtils.importESModule("resource:///modules/MailServices.sys.mjs");
console.log("[ai-filter][api] Experiment API module loaded");
var resProto = Cc["@mozilla.org/network/protocol;1?name=resource"]
.getService(Ci.nsISubstitutingProtocolHandler);
function registerResourceUrl(extension, namespace) {
console.log(`[ai-filter][api] registerResourceUrl called for namespace="${namespace}"`);
if (resProto.hasSubstitution(namespace)) {
console.log(`[ai-filter][api] namespace="${namespace}" already registered, skipping`);
return;
}
let uri = Services.io.newURI(".", null, extension.rootURI);
console.log(`[ai-filter][api] setting substitution for "${namespace}" → ${uri.spec}`);
resProto.setSubstitutionWithFlags(namespace, uri, resProto.ALLOW_CONTENT_ACCESS);
}
var gTerm;
var AIFilterMod;
var aiFilter = class extends ExtensionCommon.ExtensionAPI {
async onStartup() {
console.log("[ai-filter][api] onStartup()");
let { extension } = this;
registerResourceUrl(extension, "aifilter");
try {
console.log("[ai-filter][api] importing ExpressionSearchFilter.jsm");
AIFilterMod = ChromeUtils.import("resource://aifilter/modules/ExpressionSearchFilter.jsm");
console.log("[ai-filter][api] ExpressionSearchFilter.jsm import succeeded");
}
catch (err) {
console.error("[ai-filter][api] failed to import ExpressionSearchFilter.jsm:", err);
}
}
onShutdown(isAppShutdown) {
console.log("[ai-filter][api] onShutdown(), isAppShutdown =", isAppShutdown);
if (!isAppShutdown && resProto.hasSubstitution("aifilter")) {
console.log("[ai-filter][api] removing substitution for namespace='aifilter'");
resProto.setSubstitution("aifilter", null);
}
}
getAPI(context) {
console.log("[ai-filter][api] getAPI()");
return {
aiFilter: {
initConfig: async (config) => {
try {
if (AIFilterMod?.AIFilter?.setConfig) {
AIFilterMod.AIFilter.setConfig(config);
console.log("[ai-filter][api] configuration applied", config);
}
} catch (err) {
console.error("[ai-filter][api] failed to apply config:", err);
}
},
classify: (msg) => {
console.log("[ai-filter][api] classify() called with msg:", msg);
try {
if (!gTerm) {
console.log("[ai-filter][api] instantiating new ClassificationTerm");
let mod = AIFilterMod || ChromeUtils.import("resource://aifilter/modules/ExpressionSearchFilter.jsm");
gTerm = new mod.ClassificationTerm();
}
console.log("[ai-filter][api] calling gTerm.match()");
let matchResult = gTerm.match(
msg.msgHdr,
msg.value,
Ci.nsMsgSearchOp.Contains
);
console.log("[ai-filter][api] gTerm.match() returned:", matchResult);
return matchResult;
}
catch (err) {
console.error("[ai-filter][api] error in classify():", err);
throw err;
}
}
}
};
}
};

25
experiment/schema.json Normal file
View file

@ -0,0 +1,25 @@
[
{
"namespace": "aiFilter",
"functions": [
{
"name": "initConfig",
"type": "function",
"async": true,
"parameters": [
{ "name": "config", "type": "any" }
]
},
{
"name": "classify",
"type": "function",
"parameters": [
{
"name": "msg",
"type": "any"
}
]
}
]
}
]

32
manifest.json Normal file
View file

@ -0,0 +1,32 @@
{
"manifest_version": 2,
"name": "AI Filter",
"version": "0.1",
"default_locale": "en-US",
"applications": { "gecko": { "id": "ai-filter@example" } },
"background": { "scripts": [ "background.js" ] },
"experiment_apis": {
"aiFilter": {
"schema": "experiment/schema.json",
"parent": {
"scopes": [ "addon_parent" ],
"paths": [ [ "aiFilter" ] ],
"script": "experiment/api.js",
"events": [ "startup" ]
}
},
"DomContentScript": {
"schema": "experiment/DomContentScript/schema.json",
"parent": {
"scopes": [ "addon_parent" ],
"paths": [ [ "DomContentScript" ] ],
"script": "experiment/DomContentScript/implementation.js"
}
}
},
"options_ui": {
"page": "options/options.html",
"open_in_tab": false
},
"permissions": [ "storage" ]
}

View file

@ -0,0 +1,175 @@
"use strict";
var { ExtensionParent } = ChromeUtils.importESModule("resource://gre/modules/ExtensionParent.sys.mjs");
var { MailServices } = ChromeUtils.importESModule("resource:///modules/MailServices.sys.mjs");
var { Services } = globalThis || ChromeUtils.importESModule("resource://gre/modules/Services.sys.mjs");
var { NetUtil } = ChromeUtils.importESModule("resource://gre/modules/NetUtil.sys.mjs");
var { MimeParser } = ChromeUtils.importESModule("resource:///modules/mimeParser.sys.mjs");
var EXPORTED_SYMBOLS = ["AIFilter", "ClassificationTerm"];
class CustomerTermBase {
constructor(nameId, operators) {
this.extension = ExtensionParent.GlobalManager.getExtension("ai-filter@example");
this.id = "aifilter#" + nameId;
this.name = this.extension.localeData.localizeMessage(nameId);
this.operators = operators;
this.cache = new Map();
console.log(`[ai-filter][ExpressionSearchFilter] Initialized term base "${this.id}"`);
}
getEnabled() {
console.log(`[ai-filter][ExpressionSearchFilter] getEnabled() called on "${this.id}"`);
return true;
}
getAvailable() {
console.log(`[ai-filter][ExpressionSearchFilter] getAvailable() called on "${this.id}"`);
return true;
}
getAvailableOperators() {
console.log(`[ai-filter][ExpressionSearchFilter] getAvailableOperators() called on "${this.id}"`);
return this.operators;
}
getAvailableValues() {
console.log(`[ai-filter][ExpressionSearchFilter] getAvailableValues() called on "${this.id}"`);
return null;
}
get attrib() {
console.log(`[ai-filter][ExpressionSearchFilter] attrib getter called for "${this.id}"`);
//return Ci.nsMsgSearchAttrib.Custom;
}
}
function getPlainText(msgHdr) {
console.log(`[ai-filter][ExpressionSearchFilter] Extracting plain text for message ID ${msgHdr.messageId}`);
let folder = msgHdr.folder;
if (!folder.getMsgInputStream) return "";
let reusable = {};
let stream = folder.getMsgInputStream(msgHdr, reusable);
let data = NetUtil.readInputStreamToString(stream, msgHdr.messageSize);
if (!reusable.value) stream.close();
let parser = Cc["@mozilla.org/parserutils;1"].getService(Ci.nsIParserUtils);
return parser.convertToPlainText(data,
Ci.nsIDocumentEncoder.OutputLFLineBreak |
Ci.nsIDocumentEncoder.OutputNoScriptContent |
Ci.nsIDocumentEncoder.OutputNoFramesContent |
Ci.nsIDocumentEncoder.OutputBodyOnly, 0);
}
let gEndpoint = "http://127.0.0.1:5000/v1/classify";
function setConfig(config = {}) {
if (config.endpoint) {
gEndpoint = config.endpoint;
}
console.log(`[ai-filter][ExpressionSearchFilter] Endpoint set to ${gEndpoint}`);
}
function buildPrompt(body, criterion) {
console.log(`[ai-filter][ExpressionSearchFilter] Building prompt with criterion: "${criterion}"`);
return `<|im_start|>system
You are an email-classification assistant.
Read the email below and the classification criterion provided by the user.
Return ONLY a JSON object on a single line of the form:
{"match": true} - if the email satisfies the criterion
{"match": false} - otherwise
Do not add any other keys, text, or formatting.<|im_end|>
<|im_start|>user
**Email Contents**
\`\`\`
${body}
\`\`\`
Classification Criteria: ${criterion}<|im_end|>
<|im_start|>assistant`;
}
class ClassificationTerm extends CustomerTermBase {
constructor() {
super("classification", [Ci.nsMsgSearchOp.Matches, Ci.nsMsgSearchOp.DoesntMatch]);
console.log(`[ai-filter][ExpressionSearchFilter] ClassificationTerm constructed`);
}
needsBody() { return true; }
match(msgHdr, value, op) {
const opName = op === Ci.nsMsgSearchOp.Matches ? "matches" :
op === Ci.nsMsgSearchOp.DoesntMatch ? "doesn't match" : `unknown (${op})`;
console.log(`[ai-filter][ExpressionSearchFilter] Matching message ${msgHdr.messageId} using op "${opName}" and value "${value}"`);
let key = msgHdr.messageId + "|" + op + "|" + value;
if (this.cache.has(key)) {
console.log(`[ai-filter][ExpressionSearchFilter] Cache hit for key: ${key}`);
return this.cache.get(key);
}
let body = getPlainText(msgHdr);
let payload = JSON.stringify({
prompt: buildPrompt(body, value),
max_tokens: 4096,
temperature: 1.31,
top_p: 1,
seed: -1,
repetition_penalty: 1.0,
top_k: 0,
min_p: 0.2,
presence_penalty: 0,
frequency_penalty: 0,
typical_p: 1,
tfs: 1
});
console.log(`[ai-filter][ExpressionSearchFilter] Sending classification request to ${gEndpoint}`);
let matched = false;
try {
let xhr = new XMLHttpRequest();
xhr.open("POST", gEndpoint, false); // synchronous request
xhr.setRequestHeader("Content-Type", "application/json");
xhr.send(payload);
if (xhr.status < 200 || xhr.status >= 300) {
console.warn(`[ai-filter][ExpressionSearchFilter] HTTP status ${xhr.status}`);
} else {
const result = JSON.parse(xhr.responseText);
const rawText = result.choices?.[0]?.text || "";
const cleanedText = rawText.replace(/<think>[\s\S]*?<\/think>/gi, "").trim();
const obj = JSON.parse(cleanedText);
matched = obj.matched === true || obj.match === true;
console.log(`[ai-filter][ExpressionSearchFilter] Received response:`, result);
console.log(`[ai-filter][ExpressionSearchFilter] Caching:`, key);
this.cache.set(key, matched);
}
} catch (e) {
console.error(`[ai-filter][ExpressionSearchFilter] HTTP request failed:`, e);
}
if (op === Ci.nsMsgSearchOp.DoesntMatch) {
matched = !matched;
console.log(`[ai-filter][ExpressionSearchFilter] Operator is "doesn't match" → inverting to ${matched}`);
}
console.log(`[ai-filter][ExpressionSearchFilter] Final match result: ${matched}`);
return matched;
}
}
(function register() {
console.log(`[ai-filter][ExpressionSearchFilter] Registering custom filter term...`);
let term = new ClassificationTerm();
if (!MailServices.filters.getCustomTerm(term.id)) {
MailServices.filters.addCustomTerm(term);
console.log(`[ai-filter][ExpressionSearchFilter] Registered term: ${term.id}`);
} else {
console.log(`[ai-filter][ExpressionSearchFilter] Term already registered: ${term.id}`);
}
})();
var AIFilter = { setConfig };

12
options/options.html Normal file
View file

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>AI Filter Options</title>
</head>
<body>
<label>Endpoint: <input id="endpoint" type="text"></label><br>
<button id="save">Save</button>
<script src="options.js"></script>
</body>
</html>

14
options/options.js Normal file
View file

@ -0,0 +1,14 @@
document.addEventListener('DOMContentLoaded', async () => {
let { endpoint = 'http://127.0.0.1:5000/v1/classify' } = await browser.storage.local.get(['endpoint']);
document.getElementById('endpoint').value = endpoint;
});
document.getElementById('save').addEventListener('click', async () => {
const endpoint = document.getElementById('endpoint').value;
await browser.storage.local.set({ endpoint });
try {
await browser.aiFilter.initConfig({ endpoint });
} catch (e) {
console.error('[ai-filter][options] failed to apply config', e);
}
});