diff --git a/background.js b/background.js index b05809f..56b0096 100644 --- a/background.js +++ b/background.js @@ -17,12 +17,8 @@ let AiClassifier; logger = await import(browser.runtime.getURL("logger.js")); logger.aiLog("background.js loaded – ready to classify", {debug: true}); try { - if (typeof ChromeUtils !== "undefined") { - ({ AiClassifier } = ChromeUtils.import("resource://aifilter/modules/AiClassifier.jsm")); - logger.aiLog("AiClassifier imported", {debug: true}); - } else { - logger.aiLog("ChromeUtils is undefined, skipping AiClassifier import", {level: 'warn'}); - } + AiClassifier = await import(browser.runtime.getURL('modules/AiClassifier.js')); + logger.aiLog("AiClassifier imported", {debug: true}); } catch (e) { logger.aiLog("failed to import AiClassifier", {level: 'error'}, e); } diff --git a/experiment/api.js b/experiment/api.js index 714d4fa..0523193 100644 --- a/experiment/api.js +++ b/experiment/api.js @@ -36,7 +36,7 @@ var aiFilter = class extends ExtensionCommon.ExtensionAPI { setDebug = loggerMod.setDebug; // Now that the resource URL is registered, import the classifier - ({ AiClassifier } = ChromeUtils.import("resource://aifilter/modules/AiClassifier.jsm")); + AiClassifier = ChromeUtils.importESModule("resource://aifilter/modules/AiClassifier.js"); aiLog("[api] onStartup()", {debug: true}); diff --git a/modules/AiClassifier.jsm b/modules/AiClassifier.js similarity index 64% rename from modules/AiClassifier.jsm rename to modules/AiClassifier.js index 204bf7a..039d244 100644 --- a/modules/AiClassifier.jsm +++ b/modules/AiClassifier.js @@ -1,10 +1,6 @@ "use strict"; -var { Services } = globalThis || ChromeUtils.importESModule("resource://gre/modules/Services.sys.mjs"); -var { NetUtil } = ChromeUtils.importESModule("resource://gre/modules/NetUtil.sys.mjs"); -var { FileUtils } = ChromeUtils.importESModule("resource://gre/modules/FileUtils.sys.mjs"); -var { aiLog, setDebug } = ChromeUtils.import("resource://aifilter/modules/logger.jsm"); - -var EXPORTED_SYMBOLS = ["AiClassifier"]; +import { aiLog, setDebug } from "../logger.js"; +const { Services } = globalThis || ChromeUtils.importESModule("resource://gre/modules/Services.sys.mjs"); const SYSTEM_PREFIX = `You are an email-classification assistant. Read the email below and the classification criterion provided by the user. @@ -41,36 +37,22 @@ let gAiParams = { let gCache = new Map(); let gCacheLoaded = false; -let gCacheFile; -function ensureCacheFile() { - if (!gCacheFile) { - gCacheFile = Services.dirsvc.get("ProfD", Ci.nsIFile); - gCacheFile.append("aifilter_cache.json"); - } -} - -function loadCache() { +async function loadCache() { if (gCacheLoaded) { return; } - ensureCacheFile(); - aiLog(`[AiClassifier] Loading cache from ${gCacheFile.path}`, {debug: true}); + aiLog(`[AiClassifier] Loading cache`, {debug: true}); try { - if (gCacheFile.exists()) { - let stream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(Ci.nsIFileInputStream); - stream.init(gCacheFile, -1, 0, 0); - let data = NetUtil.readInputStreamToString(stream, stream.available()); - stream.close(); - aiLog(`[AiClassifier] Cache file contents: ${data}`, {debug: true}); - let obj = JSON.parse(data); - for (let [k, v] of Object.entries(obj)) { + const { aiCache } = await browser.storage.local.get("aiCache"); + if (aiCache) { + for (let [k, v] of Object.entries(aiCache)) { aiLog(`[AiClassifier] ⮡ Loaded entry '${k}' → ${v}`, {debug: true}); gCache.set(k, v); } aiLog(`[AiClassifier] Loaded ${gCache.size} cache entries`, {debug: true}); } else { - aiLog(`[AiClassifier] Cache file does not exist`, {debug: true}); + aiLog(`[AiClassifier] Cache is empty`, {debug: true}); } } catch (e) { aiLog(`Failed to load cache`, {level: 'error'}, e); @@ -78,36 +60,33 @@ function loadCache() { gCacheLoaded = true; } -function saveCache(updatedKey, updatedValue) { - ensureCacheFile(); - aiLog(`[AiClassifier] Saving cache to ${gCacheFile.path}`, {debug: true}); +function loadCacheSync() { + if (!gCacheLoaded) { + let done = false; + loadCache().finally(() => { done = true; }); + Services.tm.spinEventLoopUntil(() => done); + } +} + +async function saveCache(updatedKey, updatedValue) { if (typeof updatedKey !== "undefined") { aiLog(`[AiClassifier] ⮡ Persisting entry '${updatedKey}' → ${updatedValue}`, {debug: true}); } try { - let obj = Object.fromEntries(gCache); - let data = JSON.stringify(obj); - let stream = Cc["@mozilla.org/network/file-output-stream;1"].createInstance(Ci.nsIFileOutputStream); - stream.init(gCacheFile, - FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE | FileUtils.MODE_TRUNCATE, - FileUtils.PERMS_FILE, - 0); - stream.write(data, data.length); - stream.close(); + await browser.storage.local.set({ aiCache: Object.fromEntries(gCache) }); } catch (e) { aiLog(`Failed to save cache`, {level: 'error'}, e); } } -function loadTemplate(name) { +async function loadTemplate(name) { try { - let url = `resource://aifilter/prompt_templates/${name}.txt`; - let xhr = new XMLHttpRequest(); - xhr.open("GET", url, false); - xhr.overrideMimeType("text/plain"); - xhr.send(); - if (xhr.status === 0 || xhr.status === 200) { - return xhr.responseText; + const url = typeof browser !== "undefined" && browser.runtime?.getURL + ? browser.runtime.getURL(`prompt_templates/${name}.txt`) + : `resource://aifilter/prompt_templates/${name}.txt`; + const res = await fetch(url); + if (res.ok) { + return await res.text(); } } catch (e) { aiLog(`Failed to load template '${name}':`, {level: 'error'}, e); @@ -115,6 +94,14 @@ function loadTemplate(name) { return ""; } +function loadTemplateSync(name) { + let text = ""; + let done = false; + loadTemplate(name).then(t => { text = t; }).catch(() => {}).finally(() => { done = true; }); + Services.tm.spinEventLoopUntil(() => done); + return text; +} + function setConfig(config = {}) { if (config.endpoint) { gEndpoint = config.endpoint; @@ -138,7 +125,7 @@ function setConfig(config = {}) { if (typeof config.debugLogging === "boolean") { setDebug(config.debugLogging); } - gTemplateText = gTemplateName === "custom" ? gCustomTemplate : loadTemplate(gTemplateName); + gTemplateText = gTemplateName === "custom" ? gCustomTemplate : loadTemplateSync(gTemplateName); aiLog(`[AiClassifier] Endpoint set to ${gEndpoint}`, {debug: true}); aiLog(`[AiClassifier] Template set to ${gTemplateName}`, {debug: true}); } @@ -154,12 +141,12 @@ function buildPrompt(body, criterion) { email: body, query: criterion, }; - let template = gTemplateText || loadTemplate(gTemplateName); + let template = gTemplateText || loadTemplateSync(gTemplateName); return template.replace(/{{\s*(\w+)\s*}}/g, (m, key) => data[key] || ""); } function getCachedResult(cacheKey) { - loadCache(); + loadCacheSync(); if (cacheKey && gCache.has(cacheKey)) { aiLog(`[AiClassifier] Cache hit for key: ${cacheKey}`, {debug: true}); return gCache.get(cacheKey); @@ -202,26 +189,33 @@ function classifyTextSync(text, criterion, cacheKey = null) { aiLog(`[AiClassifier] Sending classification request to ${gEndpoint}`, {debug: true}); - let matched = false; - try { - let xhr = new XMLHttpRequest(); - xhr.open("POST", gEndpoint, false); - xhr.setRequestHeader("Content-Type", "application/json"); - xhr.send(payload); - - if (xhr.status >= 200 && xhr.status < 300) { - const result = JSON.parse(xhr.responseText); - aiLog(`[AiClassifier] Received response:`, {debug: true}, result); - matched = parseMatch(result); - cacheResult(cacheKey, matched); - } else { - aiLog(`HTTP status ${xhr.status}`, {level: 'warn'}); + let result; + let done = false; + (async () => { + try { + const response = await fetch(gEndpoint, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: payload, + }); + if (response.ok) { + const json = await response.json(); + aiLog(`[AiClassifier] Received response:`, {debug: true}, json); + result = parseMatch(json); + cacheResult(cacheKey, result); + } else { + aiLog(`HTTP status ${response.status}`, {level: 'warn'}); + result = false; + } + } catch (e) { + aiLog(`HTTP request failed`, {level: 'error'}, e); + result = false; + } finally { + done = true; } - } catch (e) { - aiLog(`HTTP request failed`, {level: 'error'}, e); - } - - return matched; + })(); + Services.tm.spinEventLoopUntil(() => done); + return result; } async function classifyText(text, criterion, cacheKey = null) { @@ -257,4 +251,4 @@ async function classifyText(text, criterion, cacheKey = null) { } } -var AiClassifier = { classifyText, classifyTextSync, setConfig }; +export { classifyText, classifyTextSync, setConfig }; diff --git a/modules/ExpressionSearchFilter.jsm b/modules/ExpressionSearchFilter.jsm index c50dd59..66e19ab 100644 --- a/modules/ExpressionSearchFilter.jsm +++ b/modules/ExpressionSearchFilter.jsm @@ -3,7 +3,7 @@ var { ExtensionParent } = ChromeUtils.importESModule("resource://gre/modules/Ext var { MailServices } = ChromeUtils.importESModule("resource:///modules/MailServices.sys.mjs"); var { Services } = globalThis || ChromeUtils.importESModule("resource://gre/modules/Services.sys.mjs"); var { aiLog } = ChromeUtils.import("resource://aifilter/modules/logger.jsm"); -var { AiClassifier } = ChromeUtils.import("resource://aifilter/modules/AiClassifier.jsm"); +var AiClassifier = ChromeUtils.importESModule("resource://aifilter/modules/AiClassifier.js"); var { getPlainText } = ChromeUtils.import("resource://aifilter/modules/messageUtils.jsm"); function sha256Hex(str) { diff --git a/options/options.js b/options/options.js index f21d9ee..a1bf0e5 100644 --- a/options/options.js +++ b/options/options.js @@ -1,5 +1,6 @@ document.addEventListener('DOMContentLoaded', async () => { const logger = await import(browser.runtime.getURL('logger.js')); + const AiClassifier = await import(browser.runtime.getURL('modules/AiClassifier.js')); const defaults = await browser.storage.local.get([ 'endpoint', 'templateName', @@ -88,6 +89,7 @@ document.addEventListener('DOMContentLoaded', async () => { await browser.storage.local.set({ endpoint, templateName, customTemplate: customTemplateText, customSystemPrompt, aiParams: aiParamsSave, debugLogging }); try { await browser.aiFilter.initConfig({ endpoint, templateName, customTemplate: customTemplateText, customSystemPrompt, aiParams: aiParamsSave, debugLogging }); + AiClassifier.setConfig({ endpoint, templateName, customTemplate: customTemplateText, customSystemPrompt, aiParams: aiParamsSave, debugLogging }); logger.setDebug(debugLogging); } catch (e) { logger.aiLog('[options] failed to apply config', {level: 'error'}, e);