326 lines
13 KiB
JavaScript
326 lines
13 KiB
JavaScript
document.addEventListener('DOMContentLoaded', async () => {
|
|
const storage = (globalThis.messenger ?? browser).storage;
|
|
const logger = await import(browser.runtime.getURL('logger.js'));
|
|
const AiClassifier = await import(browser.runtime.getURL('modules/AiClassifier.js'));
|
|
const defaults = await storage.local.get([
|
|
'endpoint',
|
|
'templateName',
|
|
'customTemplate',
|
|
'customSystemPrompt',
|
|
'aiParams',
|
|
'debugLogging',
|
|
'aiRules'
|
|
]);
|
|
const tabButtons = document.querySelectorAll('#main-tabs li');
|
|
const tabs = document.querySelectorAll('.tab-content');
|
|
tabButtons.forEach(btn => btn.addEventListener('click', () => {
|
|
tabButtons.forEach(b => b.classList.remove('is-active'));
|
|
btn.classList.add('is-active');
|
|
tabs.forEach(tab => {
|
|
tab.classList.toggle('is-hidden', tab.id !== `${btn.dataset.tab}-tab`);
|
|
});
|
|
}));
|
|
tabButtons[0]?.click();
|
|
|
|
const saveBtn = document.getElementById('save');
|
|
let initialized = false;
|
|
let dragRule = null;
|
|
function markDirty() {
|
|
if (initialized) saveBtn.disabled = false;
|
|
}
|
|
document.addEventListener('input', markDirty, true);
|
|
document.addEventListener('change', markDirty, true);
|
|
logger.setDebug(defaults.debugLogging === true);
|
|
const DEFAULT_AI_PARAMS = {
|
|
max_tokens: 4096,
|
|
temperature: 0.6,
|
|
top_p: 0.95,
|
|
seed: -1,
|
|
repetition_penalty: 1.0,
|
|
top_k: 20,
|
|
min_p: 0,
|
|
presence_penalty: 0,
|
|
frequency_penalty: 0,
|
|
typical_p: 1,
|
|
tfs: 1
|
|
};
|
|
document.getElementById('endpoint').value = defaults.endpoint || 'http://127.0.0.1:5000/v1/classify';
|
|
|
|
const templates = {
|
|
openai: browser.i18n.getMessage('template.openai'),
|
|
qwen: browser.i18n.getMessage('template.qwen'),
|
|
mistral: browser.i18n.getMessage('template.mistral'),
|
|
custom: browser.i18n.getMessage('template.custom')
|
|
};
|
|
const templateSelect = document.getElementById('template');
|
|
for (const [value, label] of Object.entries(templates)) {
|
|
const opt = document.createElement('option');
|
|
opt.value = value;
|
|
opt.textContent = label;
|
|
templateSelect.appendChild(opt);
|
|
}
|
|
templateSelect.value = defaults.templateName || 'openai';
|
|
|
|
const customBox = document.getElementById('custom-template-container');
|
|
const customTemplate = document.getElementById('custom-template');
|
|
customTemplate.value = defaults.customTemplate || '';
|
|
|
|
function updateVisibility() {
|
|
customBox.classList.toggle('is-hidden', templateSelect.value !== 'custom');
|
|
}
|
|
templateSelect.addEventListener('change', updateVisibility);
|
|
updateVisibility();
|
|
|
|
const advancedBox = document.getElementById('advanced-options');
|
|
const advancedBtn = document.getElementById('toggle-advanced');
|
|
advancedBtn.addEventListener('click', () => {
|
|
advancedBox.classList.toggle('is-hidden');
|
|
});
|
|
|
|
const debugToggle = document.getElementById('debug-logging');
|
|
debugToggle.checked = defaults.debugLogging === true;
|
|
|
|
const aiParams = Object.assign({}, DEFAULT_AI_PARAMS, defaults.aiParams || {});
|
|
for (const [key, val] of Object.entries(aiParams)) {
|
|
const el = document.getElementById(key);
|
|
if (el) el.value = val;
|
|
}
|
|
|
|
let tagList = [];
|
|
let folderList = [];
|
|
try {
|
|
tagList = await messenger.messages.tags.list();
|
|
} catch (e) {
|
|
logger.aiLog('failed to list tags', {level:'error'}, e);
|
|
}
|
|
try {
|
|
const accounts = await messenger.accounts.list(true);
|
|
const collect = (f, prefix='') => {
|
|
folderList.push({ id: f.id ?? f.path, name: prefix + f.name });
|
|
(f.subFolders || []).forEach(sf => collect(sf, prefix + f.name + '/'));
|
|
};
|
|
for (const acct of accounts) {
|
|
(acct.folders || []).forEach(f => collect(f, `${acct.name}/`));
|
|
}
|
|
} catch (e) {
|
|
logger.aiLog('failed to list folders', {level:'error'}, e);
|
|
}
|
|
|
|
const DEFAULT_SYSTEM = 'Determine whether the email satisfies the user\'s criterion.';
|
|
const systemBox = document.getElementById('system-instructions');
|
|
systemBox.value = defaults.customSystemPrompt || DEFAULT_SYSTEM;
|
|
document.getElementById('reset-system').addEventListener('click', () => {
|
|
systemBox.value = DEFAULT_SYSTEM;
|
|
});
|
|
|
|
const rulesContainer = document.getElementById('rules-container');
|
|
const addRuleBtn = document.getElementById('add-rule');
|
|
|
|
function createActionRow(action = {type: 'tag'}) {
|
|
const row = document.createElement('div');
|
|
row.className = 'action-row field is-grouped';
|
|
|
|
const typeSelect = document.createElement('select');
|
|
['tag','move','junk'].forEach(t => {
|
|
const opt = document.createElement('option');
|
|
opt.value = t;
|
|
opt.textContent = t;
|
|
typeSelect.appendChild(opt);
|
|
});
|
|
typeSelect.value = action.type;
|
|
|
|
const paramSpan = document.createElement('span');
|
|
|
|
function updateParams() {
|
|
paramSpan.innerHTML = '';
|
|
if (typeSelect.value === 'tag') {
|
|
const sel = document.createElement('select');
|
|
sel.className = 'tag-select';
|
|
for (const t of tagList) {
|
|
const opt = document.createElement('option');
|
|
opt.value = t.key;
|
|
opt.textContent = t.tag;
|
|
sel.appendChild(opt);
|
|
}
|
|
sel.value = action.tagKey || '';
|
|
paramSpan.appendChild(sel);
|
|
} else if (typeSelect.value === 'move') {
|
|
const sel = document.createElement('select');
|
|
sel.className = 'folder-select';
|
|
for (const f of folderList) {
|
|
const opt = document.createElement('option');
|
|
opt.value = f.id;
|
|
opt.textContent = f.name;
|
|
sel.appendChild(opt);
|
|
}
|
|
sel.value = action.folder || '';
|
|
paramSpan.appendChild(sel);
|
|
} else if (typeSelect.value === 'junk') {
|
|
const sel = document.createElement('select');
|
|
sel.className = 'junk-select';
|
|
sel.appendChild(new Option('mark junk','true'));
|
|
sel.appendChild(new Option('mark not junk','false'));
|
|
sel.value = String(action.junk ?? true);
|
|
paramSpan.appendChild(sel);
|
|
}
|
|
}
|
|
|
|
typeSelect.addEventListener('change', updateParams);
|
|
updateParams();
|
|
|
|
const removeBtn = document.createElement('button');
|
|
removeBtn.textContent = 'Remove';
|
|
removeBtn.type = 'button';
|
|
removeBtn.className = 'button is-small is-danger is-light';
|
|
removeBtn.addEventListener('click', () => row.remove());
|
|
|
|
row.appendChild(typeSelect);
|
|
row.appendChild(paramSpan);
|
|
row.appendChild(removeBtn);
|
|
|
|
return row;
|
|
}
|
|
|
|
function renderRules(rules = []) {
|
|
rulesContainer.innerHTML = '';
|
|
for (const rule of rules) {
|
|
const div = document.createElement('div');
|
|
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');
|
|
critInput.type = 'text';
|
|
critInput.placeholder = 'Criterion';
|
|
critInput.value = rule.criterion || '';
|
|
critInput.className = 'criterion';
|
|
|
|
const actionsContainer = document.createElement('div');
|
|
actionsContainer.className = 'rule-actions';
|
|
|
|
for (const act of (rule.actions || [])) {
|
|
actionsContainer.appendChild(createActionRow(act));
|
|
}
|
|
|
|
const addAction = document.createElement('button');
|
|
addAction.textContent = 'Add Action';
|
|
addAction.type = 'button';
|
|
addAction.className = 'button is-small';
|
|
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');
|
|
delBtn.textContent = 'Delete Rule';
|
|
delBtn.type = 'button';
|
|
delBtn.className = 'button is-small is-danger';
|
|
delBtn.addEventListener('click', () => div.remove());
|
|
|
|
div.appendChild(critInput);
|
|
div.appendChild(actionsContainer);
|
|
div.appendChild(addAction);
|
|
div.appendChild(stopLabel);
|
|
div.appendChild(delBtn);
|
|
|
|
rulesContainer.appendChild(div);
|
|
}
|
|
}
|
|
|
|
addRuleBtn.addEventListener('click', () => {
|
|
const data = [...rulesContainer.querySelectorAll('.rule')].map(ruleEl => {
|
|
const criterion = ruleEl.querySelector('.criterion').value;
|
|
const actions = [...ruleEl.querySelectorAll('.action-row')].map(row => {
|
|
const type = row.querySelector('select').value;
|
|
if (type === 'tag') {
|
|
return { type, tagKey: row.querySelector('.tag-select').value };
|
|
}
|
|
if (type === 'move') {
|
|
return { type, folder: row.querySelector('.folder-select').value };
|
|
}
|
|
if (type === 'junk') {
|
|
return { type, junk: row.querySelector('.junk-select').value === 'true' };
|
|
}
|
|
return { type };
|
|
});
|
|
const stopProcessing = ruleEl.querySelector('.stop-processing')?.checked;
|
|
return { criterion, actions, stopProcessing };
|
|
});
|
|
data.push({ criterion: '', actions: [], stopProcessing: false });
|
|
renderRules(data);
|
|
});
|
|
|
|
renderRules((defaults.aiRules || []).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;
|
|
}));
|
|
initialized = true;
|
|
|
|
document.getElementById('save').addEventListener('click', async () => {
|
|
const endpoint = document.getElementById('endpoint').value;
|
|
const templateName = templateSelect.value;
|
|
const customTemplateText = customTemplate.value;
|
|
const customSystemPrompt = systemBox.value;
|
|
const aiParamsSave = {};
|
|
for (const key of Object.keys(DEFAULT_AI_PARAMS)) {
|
|
const el = document.getElementById(key);
|
|
if (el) {
|
|
const num = parseFloat(el.value);
|
|
aiParamsSave[key] = isNaN(num) ? DEFAULT_AI_PARAMS[key] : num;
|
|
}
|
|
}
|
|
const debugLogging = debugToggle.checked;
|
|
const rules = [...rulesContainer.querySelectorAll('.rule')].map(ruleEl => {
|
|
const criterion = ruleEl.querySelector('.criterion').value;
|
|
const actions = [...ruleEl.querySelectorAll('.action-row')].map(row => {
|
|
const type = row.querySelector('select').value;
|
|
if (type === 'tag') {
|
|
return { type, tagKey: row.querySelector('.tag-select').value };
|
|
}
|
|
if (type === 'move') {
|
|
return { type, folder: row.querySelector('.folder-select').value };
|
|
}
|
|
if (type === 'junk') {
|
|
return { type, junk: row.querySelector('.junk-select').value === 'true' };
|
|
}
|
|
return { type };
|
|
});
|
|
const stopProcessing = ruleEl.querySelector('.stop-processing')?.checked;
|
|
return { criterion, actions, stopProcessing };
|
|
}).filter(r => r.criterion);
|
|
await storage.local.set({ endpoint, templateName, customTemplate: customTemplateText, customSystemPrompt, aiParams: aiParamsSave, debugLogging, aiRules: rules });
|
|
try {
|
|
await 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);
|
|
}
|
|
saveBtn.disabled = true;
|
|
});
|
|
});
|