Fix rule handling and add priority UI
This commit is contained in:
parent
2032ad9fa7
commit
dd831e89b4
3 changed files with 65 additions and 8 deletions
|
@ -20,6 +20,7 @@ message meets a specified criterion.
|
||||||
- **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.
|
- **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.
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue