Fix rule handling and add priority UI

This commit is contained in:
Jordan Wages 2025-06-26 01:57:24 -05:00
commit dd831e89b4
3 changed files with 65 additions and 8 deletions

View file

@ -20,6 +20,7 @@ message meets a specified criterion.
- **Advanced parameters** tune generation settings like temperature, topp and more from the options page. - **Advanced parameters** tune generation settings like temperature, topp 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. - **Automatic rules** create rules that tag or move new messages based on AI classification.
- **Rule ordering** drag rules to prioritize them and optionally stop processing after a match.
- **Context menu** apply AI rules from the message list or the message display action button. - **Context menu** apply AI rules from the message list or the message display action button.
- **Status icons** toolbar icons indicate when messages are queued or being classified. - **Status icons** toolbar icons indicate when messages are queued or being classified.
- **Packaging script** `build-xpi.ps1` builds an XPI ready for installation. - **Packaging script** `build-xpi.ps1` builds an XPI ready for installation.
@ -60,7 +61,8 @@ Sortana is implemented entirely with standard WebExtension scripts—no custom e
1. Open the add-on's options and set the URL of your classification service. 1. Open the add-on's options and set the URL of your classification service.
2. Use the **Classification Rules** section to add a criterion and optional 2. Use the **Classification Rules** section to add a criterion and optional
actions such as tagging or moving a message when it matches. actions such as tagging or moving a message when it matches. Drag rules to
reorder them and check *Stop after match* to halt further processing.
3. Save your settings. New mail will be evaluated automatically using the 3. Save your settings. New mail will be evaluated automatically using the
configured rules. configured rules.

View file

@ -50,7 +50,9 @@ async function applyAiRules(idsInput) {
const actions = []; const actions = [];
if (r.tag) actions.push({ type: 'tag', tagKey: r.tag }); if (r.tag) actions.push({ type: 'tag', tagKey: r.tag });
if (r.moveTo) actions.push({ type: 'move', folder: r.moveTo }); if (r.moveTo) actions.push({ type: 'move', folder: r.moveTo });
return { criterion: r.criterion, actions }; const rule = { criterion: r.criterion, actions };
if (r.stopProcessing) rule.stopProcessing = true;
return rule;
}) : []; }) : [];
} }
@ -78,6 +80,9 @@ async function applyAiRules(idsInput) {
await messenger.messages.update(id, { junk: !!act.junk }); await messenger.messages.update(id, { junk: !!act.junk });
} }
} }
if (rule.stopProcessing) {
break;
}
} }
} }
} catch (e) { } catch (e) {
@ -111,9 +116,26 @@ async function applyAiRules(idsInput) {
const actions = []; const actions = [];
if (r.tag) actions.push({ type: 'tag', tagKey: r.tag }); if (r.tag) actions.push({ type: 'tag', tagKey: r.tag });
if (r.moveTo) actions.push({ type: 'move', folder: r.moveTo }); if (r.moveTo) actions.push({ type: 'move', folder: r.moveTo });
return { criterion: r.criterion, actions }; const rule = { criterion: r.criterion, actions };
if (r.stopProcessing) rule.stopProcessing = true;
return rule;
}) : []; }) : [];
logger.aiLog("configuration loaded", {debug: true}, store); logger.aiLog("configuration loaded", {debug: true}, store);
storage.onChanged.addListener(async changes => {
if (changes.aiRules) {
const newRules = changes.aiRules.newValue || [];
aiRules = newRules.map(r => {
if (r.actions) return r;
const actions = [];
if (r.tag) actions.push({ type: 'tag', tagKey: r.tag });
if (r.moveTo) actions.push({ type: 'move', folder: r.moveTo });
const rule = { criterion: r.criterion, actions };
if (r.stopProcessing) rule.stopProcessing = true;
return rule;
});
logger.aiLog("aiRules updated from storage change", {debug: true}, aiRules);
}
});
} catch (err) { } catch (err) {
logger.aiLog("failed to load config", {level: 'error'}, err); logger.aiLog("failed to load config", {level: 'error'}, err);
} }
@ -145,7 +167,8 @@ async function applyAiRules(idsInput) {
browser.menus.onClicked.addListener(async info => { browser.menus.onClicked.addListener(async info => {
if (info.menuItemId === "apply-ai-rules-list" || info.menuItemId === "apply-ai-rules-display") { if (info.menuItemId === "apply-ai-rules-list" || info.menuItemId === "apply-ai-rules-display") {
const ids = info.selectedMessages?.ids || (info.messageId ? [info.messageId] : []); const ids = info.selectedMessages?.messages?.map(m => m.id) ||
(info.messageId ? [info.messageId] : []);
await applyAiRules(ids); await applyAiRules(ids);
} }
}); });

View file

@ -24,6 +24,7 @@ document.addEventListener('DOMContentLoaded', async () => {
const saveBtn = document.getElementById('save'); const saveBtn = document.getElementById('save');
let initialized = false; let initialized = false;
let dragRule = null;
function markDirty() { function markDirty() {
if (initialized) saveBtn.disabled = false; if (initialized) saveBtn.disabled = false;
} }
@ -185,6 +186,23 @@ document.addEventListener('DOMContentLoaded', async () => {
for (const rule of rules) { for (const rule of rules) {
const div = document.createElement('div'); const div = document.createElement('div');
div.className = 'rule box'; div.className = 'rule box';
div.draggable = true;
div.addEventListener('dragstart', ev => { dragRule = div; ev.dataTransfer.setData('text/plain', ''); });
div.addEventListener('dragover', ev => ev.preventDefault());
div.addEventListener('drop', ev => {
ev.preventDefault();
if (dragRule && dragRule !== div) {
const children = Array.from(rulesContainer.children);
const dragIndex = children.indexOf(dragRule);
const dropIndex = children.indexOf(div);
if (dragIndex < dropIndex) {
rulesContainer.insertBefore(dragRule, div.nextSibling);
} else {
rulesContainer.insertBefore(dragRule, div);
}
markDirty();
}
});
const critInput = document.createElement('input'); const critInput = document.createElement('input');
critInput.type = 'text'; critInput.type = 'text';
@ -205,6 +223,15 @@ document.addEventListener('DOMContentLoaded', async () => {
addAction.className = 'button is-small'; addAction.className = 'button is-small';
addAction.addEventListener('click', () => actionsContainer.appendChild(createActionRow())); addAction.addEventListener('click', () => actionsContainer.appendChild(createActionRow()));
const stopLabel = document.createElement('label');
stopLabel.className = 'checkbox ml-2';
const stopCheck = document.createElement('input');
stopCheck.type = 'checkbox';
stopCheck.className = 'stop-processing';
stopCheck.checked = rule.stopProcessing === true;
stopLabel.appendChild(stopCheck);
stopLabel.append(' Stop after match');
const delBtn = document.createElement('button'); const delBtn = document.createElement('button');
delBtn.textContent = 'Delete Rule'; delBtn.textContent = 'Delete Rule';
delBtn.type = 'button'; delBtn.type = 'button';
@ -214,6 +241,7 @@ document.addEventListener('DOMContentLoaded', async () => {
div.appendChild(critInput); div.appendChild(critInput);
div.appendChild(actionsContainer); div.appendChild(actionsContainer);
div.appendChild(addAction); div.appendChild(addAction);
div.appendChild(stopLabel);
div.appendChild(delBtn); div.appendChild(delBtn);
rulesContainer.appendChild(div); rulesContainer.appendChild(div);
@ -236,9 +264,10 @@ document.addEventListener('DOMContentLoaded', async () => {
} }
return { type }; return { type };
}); });
return { criterion, actions }; const stopProcessing = ruleEl.querySelector('.stop-processing')?.checked;
return { criterion, actions, stopProcessing };
}); });
data.push({ criterion: '', actions: [] }); data.push({ criterion: '', actions: [], stopProcessing: false });
renderRules(data); renderRules(data);
}); });
@ -247,7 +276,9 @@ document.addEventListener('DOMContentLoaded', async () => {
const actions = []; const actions = [];
if (r.tag) actions.push({ type: 'tag', tagKey: r.tag }); if (r.tag) actions.push({ type: 'tag', tagKey: r.tag });
if (r.moveTo) actions.push({ type: 'move', folder: r.moveTo }); if (r.moveTo) actions.push({ type: 'move', folder: r.moveTo });
return { criterion: r.criterion, actions }; const rule = { criterion: r.criterion, actions };
if (r.stopProcessing) rule.stopProcessing = true;
return rule;
})); }));
initialized = true; initialized = true;
@ -280,7 +311,8 @@ document.addEventListener('DOMContentLoaded', async () => {
} }
return { type }; return { type };
}); });
return { criterion, actions }; const stopProcessing = ruleEl.querySelector('.stop-processing')?.checked;
return { criterion, actions, stopProcessing };
}).filter(r => r.criterion); }).filter(r => r.criterion);
await storage.local.set({ endpoint, templateName, customTemplate: customTemplateText, customSystemPrompt, aiParams: aiParamsSave, debugLogging, aiRules: rules }); await storage.local.set({ endpoint, templateName, customTemplate: customTemplateText, customSystemPrompt, aiParams: aiParamsSave, debugLogging, aiRules: rules });
try { try {