Add rule management UI and automatic classification
This commit is contained in:
parent
5fc66de82c
commit
c364238f54
4 changed files with 107 additions and 9 deletions
|
@ -19,6 +19,7 @@ message meets a specified criterion.
|
||||||
- **Persistent result caching** – classification results are saved to disk so messages aren't re-evaluated across restarts.
|
- **Persistent result caching** – classification results are saved to disk so messages aren't re-evaluated across restarts.
|
||||||
- **Advanced parameters** – tune generation settings like temperature, top‑p and more from the options page.
|
- **Advanced parameters** – tune generation settings like temperature, top‑p and more from the options page.
|
||||||
- **Debug logging** – optional colorized logs help troubleshoot interactions with the AI service.
|
- **Debug logging** – optional colorized logs help troubleshoot interactions with the AI service.
|
||||||
|
- **Automatic rules** – create rules that tag or move new messages based on AI classification.
|
||||||
- **Packaging script** – `build-xpi.ps1` builds an XPI ready for installation.
|
- **Packaging script** – `build-xpi.ps1` builds an XPI ready for installation.
|
||||||
|
|
||||||
## Architecture Overview
|
## Architecture Overview
|
||||||
|
@ -45,7 +46,7 @@ APIs:
|
||||||
| `modules/ExpressionSearchFilter.jsm` | Custom filter term and AI request logic. |
|
| `modules/ExpressionSearchFilter.jsm` | Custom filter term and AI request logic. |
|
||||||
| `experiment/api.js` | Bridges WebExtension code with privileged APIs.|
|
| `experiment/api.js` | Bridges WebExtension code with privileged APIs.|
|
||||||
| `content/filterEditor.js` | Patches the filter editor interface. |
|
| `content/filterEditor.js` | Patches the filter editor interface. |
|
||||||
| `options/options.html` and `options.js` | Endpoint configuration UI. |
|
| `options/options.html` and `options.js` | Endpoint and rule configuration UI. |
|
||||||
| `logger.js` and `modules/logger.jsm` | Colorized logging with optional debug mode. |
|
| `logger.js` and `modules/logger.jsm` | Colorized logging with optional debug mode. |
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
|
|
|
@ -12,6 +12,12 @@
|
||||||
|
|
||||||
let logger;
|
let logger;
|
||||||
let AiClassifier;
|
let AiClassifier;
|
||||||
|
let aiRules = [];
|
||||||
|
|
||||||
|
async function sha256Hex(str) {
|
||||||
|
const buf = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(str));
|
||||||
|
return Array.from(new Uint8Array(buf), b => b.toString(16).padStart(2, '0')).join('');
|
||||||
|
}
|
||||||
// Startup
|
// Startup
|
||||||
(async () => {
|
(async () => {
|
||||||
logger = await import(browser.runtime.getURL("logger.js"));
|
logger = await import(browser.runtime.getURL("logger.js"));
|
||||||
|
@ -23,9 +29,10 @@ let AiClassifier;
|
||||||
logger.aiLog("failed to import AiClassifier", {level: 'error'}, e);
|
logger.aiLog("failed to import AiClassifier", {level: 'error'}, e);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const store = await browser.storage.local.get(["endpoint", "templateName", "customTemplate", "customSystemPrompt", "aiParams", "debugLogging"]);
|
const store = await browser.storage.local.get(["endpoint", "templateName", "customTemplate", "customSystemPrompt", "aiParams", "debugLogging", "aiRules"]);
|
||||||
logger.setDebug(store.debugLogging);
|
logger.setDebug(store.debugLogging);
|
||||||
AiClassifier.setConfig(store);
|
AiClassifier.setConfig(store);
|
||||||
|
aiRules = Array.isArray(store.aiRules) ? store.aiRules : [];
|
||||||
logger.aiLog("configuration loaded", {debug: true}, store);
|
logger.aiLog("configuration loaded", {debug: true}, store);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.aiLog("failed to load config", {level: 'error'}, err);
|
logger.aiLog("failed to load config", {level: 'error'}, err);
|
||||||
|
@ -62,15 +69,26 @@ browser.runtime.onMessage.addListener(async (msg) => {
|
||||||
if (typeof messenger !== "undefined" && messenger.messages?.onNewMailReceived) {
|
if (typeof messenger !== "undefined" && messenger.messages?.onNewMailReceived) {
|
||||||
messenger.messages.onNewMailReceived.addListener(async (folder, messages) => {
|
messenger.messages.onNewMailReceived.addListener(async (folder, messages) => {
|
||||||
logger.aiLog("onNewMailReceived", {debug: true}, messages);
|
logger.aiLog("onNewMailReceived", {debug: true}, messages);
|
||||||
|
if (!aiRules.length) {
|
||||||
|
const { aiRules: stored } = await browser.storage.local.get("aiRules");
|
||||||
|
aiRules = Array.isArray(stored) ? stored : [];
|
||||||
|
}
|
||||||
for (const msg of (messages?.messages || messages || [])) {
|
for (const msg of (messages?.messages || messages || [])) {
|
||||||
const id = msg.id ?? msg;
|
const id = msg.id ?? msg;
|
||||||
try {
|
try {
|
||||||
const full = await messenger.messages.getFull(id);
|
const full = await messenger.messages.getFull(id);
|
||||||
const text = full?.parts?.[0]?.body || "";
|
const text = full?.parts?.[0]?.body || "";
|
||||||
const criterion = (await browser.storage.local.get("autoCriterion")).autoCriterion || "";
|
for (const rule of aiRules) {
|
||||||
const matched = await AiClassifier.classifyText(text, criterion);
|
const cacheKey = await sha256Hex(`${id}|${rule.criterion}`);
|
||||||
if (matched) {
|
const matched = await AiClassifier.classifyText(text, rule.criterion, cacheKey);
|
||||||
await messenger.messages.update(id, {tags: ["$label1"]});
|
if (matched) {
|
||||||
|
if (rule.tag) {
|
||||||
|
await messenger.messages.update(id, {tags: [rule.tag]});
|
||||||
|
}
|
||||||
|
if (rule.moveTo) {
|
||||||
|
await messenger.messages.move([id], rule.moveTo);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.aiLog("failed to classify new mail", {level: 'error'}, e);
|
logger.aiLog("failed to classify new mail", {level: 'error'}, e);
|
||||||
|
|
|
@ -78,6 +78,21 @@
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#rules-container {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule {
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
padding: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-actions {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
padding: 10px 20px;
|
padding: 10px 20px;
|
||||||
border: none;
|
border: none;
|
||||||
|
@ -197,8 +212,13 @@
|
||||||
<input type="number" step="0.01" id="tfs">
|
<input type="number" step="0.01" id="tfs">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
<h2>Classification Rules</h2>
|
||||||
|
<div id="rules-container"></div>
|
||||||
|
<button id="add-rule" type="button">Add Rule</button>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<script src="options.js"></script>
|
<script src="options.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -7,7 +7,8 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
'customTemplate',
|
'customTemplate',
|
||||||
'customSystemPrompt',
|
'customSystemPrompt',
|
||||||
'aiParams',
|
'aiParams',
|
||||||
'debugLogging'
|
'debugLogging',
|
||||||
|
'aiRules'
|
||||||
]);
|
]);
|
||||||
logger.setDebug(defaults.debugLogging === true);
|
logger.setDebug(defaults.debugLogging === true);
|
||||||
const DEFAULT_AI_PARAMS = {
|
const DEFAULT_AI_PARAMS = {
|
||||||
|
@ -72,6 +73,59 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
systemBox.value = DEFAULT_SYSTEM;
|
systemBox.value = DEFAULT_SYSTEM;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const rulesContainer = document.getElementById('rules-container');
|
||||||
|
const addRuleBtn = document.getElementById('add-rule');
|
||||||
|
|
||||||
|
function renderRules(rules = []) {
|
||||||
|
rulesContainer.innerHTML = '';
|
||||||
|
for (const rule of rules) {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = 'rule';
|
||||||
|
|
||||||
|
const critInput = document.createElement('input');
|
||||||
|
critInput.type = 'text';
|
||||||
|
critInput.placeholder = 'Criterion';
|
||||||
|
critInput.value = rule.criterion || '';
|
||||||
|
|
||||||
|
const tagInput = document.createElement('input');
|
||||||
|
tagInput.type = 'text';
|
||||||
|
tagInput.placeholder = 'Tag (e.g. $label1)';
|
||||||
|
tagInput.value = rule.tag || '';
|
||||||
|
|
||||||
|
const moveInput = document.createElement('input');
|
||||||
|
moveInput.type = 'text';
|
||||||
|
moveInput.placeholder = 'Folder URL';
|
||||||
|
moveInput.value = rule.moveTo || '';
|
||||||
|
|
||||||
|
const actionsDiv = document.createElement('div');
|
||||||
|
actionsDiv.className = 'rule-actions';
|
||||||
|
|
||||||
|
const delBtn = document.createElement('button');
|
||||||
|
delBtn.textContent = 'Delete';
|
||||||
|
delBtn.type = 'button';
|
||||||
|
delBtn.addEventListener('click', () => div.remove());
|
||||||
|
|
||||||
|
actionsDiv.appendChild(delBtn);
|
||||||
|
|
||||||
|
div.appendChild(critInput);
|
||||||
|
div.appendChild(tagInput);
|
||||||
|
div.appendChild(moveInput);
|
||||||
|
div.appendChild(actionsDiv);
|
||||||
|
|
||||||
|
rulesContainer.appendChild(div);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addRuleBtn.addEventListener('click', () => {
|
||||||
|
renderRules([...rulesContainer.querySelectorAll('.rule')].map(el => ({
|
||||||
|
criterion: el.children[0].value,
|
||||||
|
tag: el.children[1].value,
|
||||||
|
moveTo: el.children[2].value
|
||||||
|
})).concat([{ criterion: '', tag: '', moveTo: '' }]));
|
||||||
|
});
|
||||||
|
|
||||||
|
renderRules(defaults.aiRules || []);
|
||||||
|
|
||||||
document.getElementById('save').addEventListener('click', async () => {
|
document.getElementById('save').addEventListener('click', async () => {
|
||||||
const endpoint = document.getElementById('endpoint').value;
|
const endpoint = document.getElementById('endpoint').value;
|
||||||
const templateName = templateSelect.value;
|
const templateName = templateSelect.value;
|
||||||
|
@ -86,7 +140,12 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const debugLogging = debugToggle.checked;
|
const debugLogging = debugToggle.checked;
|
||||||
await browser.storage.local.set({ endpoint, templateName, customTemplate: customTemplateText, customSystemPrompt, aiParams: aiParamsSave, debugLogging });
|
const rules = [...rulesContainer.querySelectorAll('.rule')].map(el => ({
|
||||||
|
criterion: el.children[0].value,
|
||||||
|
tag: el.children[1].value,
|
||||||
|
moveTo: el.children[2].value
|
||||||
|
})).filter(r => r.criterion);
|
||||||
|
await browser.storage.local.set({ endpoint, templateName, customTemplate: customTemplateText, customSystemPrompt, aiParams: aiParamsSave, debugLogging, aiRules: rules });
|
||||||
try {
|
try {
|
||||||
AiClassifier.setConfig({ endpoint, templateName, customTemplate: customTemplateText, customSystemPrompt, aiParams: aiParamsSave, debugLogging });
|
AiClassifier.setConfig({ endpoint, templateName, customTemplate: customTemplateText, customSystemPrompt, aiParams: aiParamsSave, debugLogging });
|
||||||
logger.setDebug(debugLogging);
|
logger.setDebug(debugLogging);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue