Polish mobile UI and editor workflows

This commit is contained in:
pewdiepie-archdaemon
2026-06-27 13:05:44 +00:00
parent 87e46e576a
commit 45ee5a71f4
48 changed files with 6455 additions and 1177 deletions
+131 -35
View File
@@ -212,6 +212,65 @@ async function _saveUrgentEmailSettings(prompt) {
});
}
const _EMAIL_ACCOUNT_ACTIONS = new Set([
'summarize_emails',
'draft_email_replies',
'extract_email_events',
'check_email_urgency',
]);
let _emailAccounts = null;
async function _fetchEmailAccountsForTasks() {
if (_emailAccounts) return _emailAccounts;
try {
const res = await fetch(`${API_BASE}/api/email/accounts`, { credentials: 'same-origin' });
const data = await res.json();
_emailAccounts = Array.isArray(data.accounts) ? data.accounts : [];
} catch (e) {
_emailAccounts = [];
}
return _emailAccounts;
}
function _taskPromptConfig(prompt) {
const raw = (prompt || '').trim();
if (!raw) return {};
try {
const parsed = JSON.parse(raw);
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
} catch (_) {
const cfg = {};
for (const line of raw.split(/\r?\n/)) {
const idx = line.indexOf('=');
if (idx <= 0) continue;
const key = line.slice(0, idx).trim();
const val = line.slice(idx + 1).trim();
if (key) cfg[key] = val;
}
return cfg;
}
}
async function _renderEmailActionOptions(action, existing, extra) {
if (!_EMAIL_ACCOUNT_ACTIONS.has(action)) return;
const accounts = (await _fetchEmailAccountsForTasks()).filter(a => a && a.enabled !== false);
const cfg = _taskPromptConfig(existing?.prompt || '');
const current = String(cfg.account_id || cfg.email_account_id || '');
const options = [
`<option value="" ${current ? '' : 'selected'}>All accounts</option>`,
...accounts.map(a => {
const id = String(a.id || '');
const label = a.name || a.from_address || a.imap_user || id.slice(0, 8);
const suffix = a.is_default ? ' (default)' : '';
return `<option value="${_escHtml(id)}" ${id === current ? 'selected' : ''}>${_escHtml(label + suffix)}</option>`;
}),
].join('');
extra.insertAdjacentHTML('afterbegin', `
<label class="task-form-label">Email account</label>
<select id="task-form-email-account" class="task-form-input">${options}</select>
`);
}
let _triggerEvents = null;
async function _fetchEvents() {
if (_triggerEvents) return _triggerEvents;
@@ -688,9 +747,9 @@ function _renderList() {
const titleRow = document.createElement('div');
titleRow.style.cssText = 'display:flex;align-items:center;gap:6px;cursor:pointer;';
const statusBadge = task.status === 'paused'
? `<span class="task-status-badge task-state-badge task-paused-badge" data-task-status-action="resume" title="Paused - click to resume" style="position:relative;top:4px;"><svg width="10" height="10" viewBox="0 0 24 24" fill="currentColor"><polygon points="7 4 19 12 7 20 7 4"/></svg><span class="task-state-label">paused</span></span>`
? `<span class="task-status-badge task-state-badge task-paused-badge" data-task-status-action="resume" title="Paused - click to resume" style="position:relative;top:4px;"><svg width="10" height="10" viewBox="0 0 24 24" fill="currentColor"><rect x="6" y="5" width="4" height="14" rx="1"/><rect x="14" y="5" width="4" height="14" rx="1"/></svg><span class="task-state-label">paused</span></span>`
: task.status === 'active'
? `<span class="task-status-badge task-state-badge task-active-badge" data-task-status-action="pause" title="Active - click to pause" style="position:relative;top:4px;"><svg width="10" height="10" viewBox="0 0 24 24" fill="currentColor"><rect x="6" y="5" width="4" height="14" rx="1"/><rect x="14" y="5" width="4" height="14" rx="1"/></svg><span class="task-state-label">active</span></span>`
? `<span class="task-status-badge task-state-badge task-active-badge" data-task-status-action="pause" title="Active - click to pause" style="position:relative;top:4px;"><svg width="10" height="10" viewBox="0 0 24 24" fill="currentColor"><polygon points="7 4 19 12 7 20 7 4"/></svg><span class="task-state-label">active</span></span>`
: '';
const builtinBadge = task.is_builtin
? `<span class="task-builtin-badge${task.is_modified ? ' modified' : ''}" title="${task.is_modified ? 'Built-in task — edited from its default' : 'Built-in task'}">built-in${task.is_modified ? ' · edited' : ''}</span>`
@@ -709,8 +768,8 @@ function _renderList() {
menuBtn.addEventListener('click', (e) => {
e.stopPropagation();
const items = [];
// Run now stays in the kebab too (alongside the new Run button on the
// card) for users coming from muscle-memory / mobile long-press.
// Run now stays in the kebab too for users coming from muscle-memory /
// mobile long-press. The expanded card also shows it next to Edit.
if (task.status !== 'completed') items.push({ label: 'Run now', icon: '<polyline points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/>', action: () => _doRunNow(task.id) });
items.push({ label: 'Edit', icon: '<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.12 2.12 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>', action: () => _showForm(task) });
if (task.status === 'active') items.push({ label: 'Pause', icon: '<rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/>', action: () => _doPause(task.id) });
@@ -726,17 +785,6 @@ function _renderList() {
_showTaskDropdown(menuBtn, items);
});
actionsWrap.appendChild(menuBtn);
// Run now — promoted out of the kebab onto the card itself for one-click
// manual triggering. Hidden for completed tasks (same gate as before).
if (task.status !== 'completed') {
const runBtn = document.createElement('button');
runBtn.className = 'task-status-badge task-run-now-badge task-card-run-btn';
runBtn.title = 'Run now';
runBtn.style.cssText = 'position:relative;top:1px;margin-right:4px;';
runBtn.innerHTML = '<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><polyline points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg><span>Run</span>';
runBtn.addEventListener('click', (e) => { e.stopPropagation(); _doRunNow(task.id); });
actionsWrap.insertBefore(runBtn, menuBtn);
}
titleRow.appendChild(actionsWrap);
// Content area
@@ -767,7 +815,29 @@ function _renderList() {
// Expandable detail (revealed on click) — like the library doc/chat cards:
// extra meta + last-run result + description.
const detail = document.createElement('div');
detail.style.cssText = 'display:none;margin-top:7px;padding:8px 0 2px;border-top:1px solid var(--border);';
detail.style.cssText = 'display:none;margin-top:7px;padding:8px 0 2px;border-top:1px solid var(--border);position:relative;';
const detailActions = document.createElement('div');
detailActions.style.cssText = 'display:flex;justify-content:flex-end;gap:6px;margin-top:7px;';
if (task.status !== 'completed') {
const runBtn = document.createElement('button');
runBtn.className = 'memory-toolbar-btn task-detail-run-btn';
runBtn.title = 'Run now';
runBtn.innerHTML = '<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-1px;margin-right:4px;"><polyline points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>Run';
runBtn.addEventListener('click', (e) => {
e.stopPropagation();
_doRunNow(task.id);
});
detailActions.appendChild(runBtn);
}
const editBtn = document.createElement('button');
editBtn.className = 'memory-toolbar-btn task-detail-edit-btn';
editBtn.title = 'Edit task';
editBtn.innerHTML = '<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-1px;margin-right:4px;"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.12 2.12 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>Edit';
editBtn.addEventListener('click', (e) => {
e.stopPropagation();
_showForm(task);
});
detailActions.appendChild(editBtn);
const extra = [];
if (task.last_run) extra.push('Last: ' + _relativeTime(task.last_run));
if (task.output_target && task.output_target !== 'session') extra.push('→ ' + task.output_target.replace(/^mcp__/, '').replace(/__/g, ' '));
@@ -803,6 +873,7 @@ function _renderList() {
}
detail.appendChild(desc);
}
detail.appendChild(detailActions);
content.appendChild(detail);
// Select-mode checkbox (mirrors the library's .memory-select-cb).
@@ -898,10 +969,19 @@ function _attachTaskLongPress(card, menuBtn) {
}
function _showTaskDropdown(anchor, items) {
// Remove any existing dropdown
document.querySelectorAll('.task-dropdown').forEach(d => d.remove());
const existing = document.querySelector('.task-dropdown');
if (existing && existing._anchor === anchor) {
if (typeof existing._dismiss === 'function') existing._dismiss();
else existing.remove();
return;
}
document.querySelectorAll('.task-dropdown').forEach(d => {
if (typeof d._dismiss === 'function') d._dismiss();
else d.remove();
});
const dd = document.createElement('div');
dd.className = 'task-dropdown';
dd._anchor = anchor;
dd.style.cssText = 'position:fixed;z-index:100000;background:var(--panel);border:1px solid var(--border);border-radius:6px;box-shadow:0 4px 12px rgba(0,0,0,0.3);padding:4px;min-width:120px;';
items.forEach(item => {
const btn = document.createElement('button');
@@ -933,6 +1013,10 @@ function _showTaskDropdown(anchor, items) {
if (performance.now() - openedAt < 250) return;
if (!dd.contains(e.target)) { dd.remove(); document.removeEventListener('click', close); }
};
dd._dismiss = () => {
dd.remove();
document.removeEventListener('click', close);
};
// requestAnimationFrame so the listener is registered AFTER the current
// pointer/click event cycle has finished bubbling.
requestAnimationFrame(() => document.addEventListener('click', close));
@@ -1061,10 +1145,13 @@ function _showForm(existing, initTaskType, initTriggerType) {
<option value="">None</option>
</select>
<label class="task-form-label" style="display:flex;align-items:center;gap:8px;cursor:pointer;">
<input type="checkbox" id="task-form-notif" ${existing && existing.notifications_enabled === false ? '' : 'checked'} style="margin:0;cursor:pointer;">
<span>Notifications</span>
<span style="opacity:0.55;font-weight:normal;font-size:10px;">— uncheck to silence completion notifications for this task (helpful for chatty cron jobs)</span>
<label class="task-form-notif-toggle">
<input type="checkbox" id="task-form-notif" ${existing && existing.notifications_enabled === false ? '' : 'checked'}>
<span class="task-form-notif-switch" aria-hidden="true"></span>
<span class="task-form-notif-copy">
<span>Notifications</span>
<span>Silence completion alerts for chatty cron jobs.</span>
</span>
</label>
<div class="task-form-actions">
@@ -1114,23 +1201,28 @@ function _showForm(existing, initTaskType, initTriggerType) {
const sel = document.getElementById('task-form-action');
const extra = document.getElementById('task-form-action-extra');
if (!sel || !extra) return;
if (sel.value !== 'check_email_urgency') {
const action = sel.value;
if (!_EMAIL_ACCOUNT_ACTIONS.has(action)) {
extra.innerHTML = '';
return;
}
extra.innerHTML = `
<label class="task-form-label">Email triage rules</label>
<textarea id="task-form-urgent-email-prompt" class="task-form-input task-form-textarea" rows="4" placeholder="What should count as urgent? e.g. deadlines, blockers, people waiting outside."></textarea>
<div class="memory-desc" style="font-size:11px;margin-top:4px;">Pause/resume and schedule are controlled by this task. It tags urgent, reply-soon, newsletter, marketing, and spam. Urgent/reply-soon emails use your reminder settings.</div>
`;
const settings = await _fetchUrgentEmailSettings();
const promptEl = document.getElementById('task-form-urgent-email-prompt');
if (promptEl && !promptEl.dataset.loaded) {
promptEl.value = settings.urgent_email_prompt || '';
promptEl.dataset.loaded = '1';
extra.innerHTML = '';
await _renderEmailActionOptions(action, existing, extra);
if (action === 'check_email_urgency') {
extra.insertAdjacentHTML('beforeend', `
<label class="task-form-label">Email triage rules</label>
<textarea id="task-form-urgent-email-prompt" class="task-form-input task-form-textarea" rows="4" placeholder="What should count as urgent? e.g. deadlines, blockers, people waiting outside."></textarea>
<div class="memory-desc" style="font-size:11px;margin-top:4px;">Pause/resume and schedule are controlled by this task. It tags work, personal, urgent, action-needed, finance, legal, travel, newsletter, marketing, spam, and related mail categories. Urgent/reply-soon emails use your reminder settings.</div>
`);
const settings = await _fetchUrgentEmailSettings();
const promptEl = document.getElementById('task-form-urgent-email-prompt');
if (promptEl && !promptEl.dataset.loaded) {
promptEl.value = settings.urgent_email_prompt || '';
promptEl.dataset.loaded = '1';
}
const notifEl = document.getElementById('task-form-notif');
if (notifEl && !existing?.id) notifEl.checked = false;
}
const notifEl = document.getElementById('task-form-notif');
if (notifEl && !existing?.id) notifEl.checked = false;
};
_fetchActions().then(actions => {
const sel = document.getElementById('task-form-action');
@@ -1469,6 +1561,10 @@ function _showForm(existing, initTaskType, initTriggerType) {
return;
}
payload.action = action;
if (_EMAIL_ACCOUNT_ACTIONS.has(action)) {
const accountId = document.getElementById('task-form-email-account')?.value || '';
payload.prompt = accountId ? JSON.stringify({ account_id: accountId }) : '';
}
if (action === 'check_email_urgency') {
const urgentPrompt = document.getElementById('task-form-urgent-email-prompt')?.value || '';
try {