Polish email reply and task controls

This commit is contained in:
pewdiepie-archdaemon
2026-06-01 23:02:25 +09:00
parent 033852ab14
commit 7711e14f90
4 changed files with 162 additions and 48 deletions

View File

@@ -639,7 +639,8 @@ function _createEmailItem(em) {
}
async function _openEmail(em, itemEl, preloadedData = null, mode = 'reply') {
const wantsAiReply = mode === 'ai-reply';
const aiReplyMode = mode === 'ai-reply-fast' ? 'fast' : (mode === 'ai-reply-full' ? 'full' : '');
const wantsAiReply = mode === 'ai-reply' || !!aiReplyMode;
let aiSuggestedBody = null;
if (wantsAiReply) {
// Fall through to reply-all (not plain reply) so the generated AI
@@ -696,7 +697,7 @@ async function _openEmail(em, itemEl, preloadedData = null, mode = 'reply') {
message_id: data.message_id || '',
uid: String(em.uid || ''),
folder: _currentFolder,
fast: _shouldUseFastAiReply(data),
fast: aiReplyMode ? aiReplyMode === 'fast' : _shouldUseFastAiReply(data),
}),
});
const result = await res.json();

View File

@@ -115,6 +115,24 @@ function _syncReminderClearButton() {
document.getElementById('email-reminders-clear-btn')?.classList.toggle('hidden', state._libFilter !== 'reminders');
}
function _renderAccountsLoading() {
const strip = document.getElementById('email-lib-accounts');
if (!strip) return;
strip.style.display = 'flex';
strip.innerHTML = '';
try {
const wp = spinnerModule.createWhirlpool(14);
wp.element.classList.add('email-accounts-loading-whirlpool');
const label = document.createElement('span');
label.className = 'email-accounts-loading-label';
label.textContent = 'Accounts';
strip.appendChild(wp.element);
strip.appendChild(label);
} catch (_) {
strip.textContent = 'Accounts...';
}
}
function _syncEmailReminderBellVisibility(enabled) {
const btn = document.getElementById('email-reminder-btn');
const wrap = document.querySelector('#email-lib-modal .email-search-wrap');
@@ -437,7 +455,7 @@ function _resetEmailListForFreshLoad() {
state._libTotal = 0;
_libLoadSeq += 1;
const grid = document.getElementById('email-lib-grid');
if (grid) grid.innerHTML = '';
if (grid) _renderEmailLoading(grid);
const stats = document.getElementById('email-lib-stats');
if (stats) stats.textContent = 'Loading...';
}
@@ -1063,6 +1081,7 @@ export function openEmailLibrary(opts = {}) {
};
document.addEventListener('keydown', state._libEscHandler, true);
_renderAccountsLoading();
_loadAccounts();
_loadFolders();
_loadEmailReminderBellVisibility();
@@ -1296,9 +1315,7 @@ async function _doSearch() {
}
const grid = document.getElementById('email-lib-grid');
if (!grid) return;
grid.innerHTML = '';
const sp = spinnerModule.createWhirlpool(28);
grid.appendChild(sp.element);
const sp = _renderEmailLoading(grid);
try {
const res = await fetch(`${API_BASE}/api/email/search?folder=${encodeURIComponent(state._libFolder)}${_acct()}&q=${encodeURIComponent(q)}&limit=100`);
@@ -1317,6 +1334,24 @@ async function _doSearch() {
}
}
function _renderEmailLoading(grid) {
if (!grid) return null;
grid.innerHTML = '';
const wrap = document.createElement('div');
wrap.className = 'email-loading email-loading-with-label';
let sp = null;
try {
sp = spinnerModule.createWhirlpool(28);
wrap.appendChild(sp.element);
} catch (_) {}
const label = document.createElement('div');
label.className = 'email-loading-label';
label.textContent = 'Loading emails';
wrap.appendChild(label);
grid.appendChild(wrap);
return sp;
}
// Refreshes the small accent-pill in the modal title with the unread count
// for the current folder. When the inbox is currently filtered to unread, the
// pill flips to show the total-emails count + "all" label, because clicking
@@ -1401,9 +1436,7 @@ async function _loadEmails({ force = false, useCache = true } = {}) {
const stats = document.getElementById('email-lib-stats');
if (stats) stats.textContent = `${state._libTotal} emails`;
} else {
grid.innerHTML = '';
sp = spinnerModule.createWhirlpool(28);
grid.appendChild(sp.element);
sp = _renderEmailLoading(grid);
}
try {
@@ -2015,8 +2048,8 @@ async function _toggleCardPreview(card, em) {
<button class="memory-toolbar-btn reader-icon-btn" data-act="forward" title="Forward"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 17 20 12 15 7"/><path d="M4 18v-2a4 4 0 0 1 4-4h12"/></svg><span class="reader-btn-label">Forward</span></button>
</div>
<div class="email-reader-actions-row email-reader-actions-row-secondary">
<button class="memory-toolbar-btn reader-icon-btn" data-act="ai-reply" title="AI Reply (suggest a draft)"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 17 4 12 9 7"/><path d="M20 18v-2a4 4 0 0 0-4-4H4"/><path d="M14 4l1 2 2 1-2 1-1 2-1-2-2-1 2-1z" fill="var(--accent-primary, var(--red))" stroke="none" transform="translate(2 0)"/></svg><span class="reader-btn-label">AI reply</span></button>
<button class="memory-toolbar-btn reader-icon-btn" data-act="summarize" title="Summarize"><svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><path d="M12 0L14.59 8.41L23 12L14.59 15.59L12 24L9.41 15.59L1 12L9.41 8.41Z"/></svg><span class="reader-btn-label">Summary</span></button>
<button class="memory-toolbar-btn reader-icon-btn" data-act="ai-reply" title="${data.cached_ai_reply ? 'AI Reply (cached draft ready)' : 'AI Reply (suggest a draft)'}">${_aiReplyIcon(data)}<span class="reader-btn-label">AI reply</span></button>
<button class="memory-toolbar-btn reader-icon-btn" data-act="summarize" title="Summarize">${_summaryIcon(data)}<span class="reader-btn-label">Summary</span></button>
<button class="memory-toolbar-btn reader-icon-btn" data-act="from-sender" title="Search text in this thread"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="7"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg><span class="reader-btn-label">Search</span></button>
<div class="email-reader-more-wrap" style="position:relative">
<button class="memory-toolbar-btn reader-icon-btn" data-act="more" title="More actions"><svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><circle cx="12" cy="5" r="2"/><circle cx="12" cy="12" r="2"/><circle cx="12" cy="19" r="2"/></svg><span class="reader-btn-label">More</span></button>
@@ -2067,28 +2100,7 @@ async function _toggleCardPreview(card, em) {
_snapEmailModalToLeftSidebar(ev.currentTarget.closest('.modal'));
if (state._onEmailClick) await state._onEmailClick({ email: em, emailData: data, mode: 'reply-all' });
});
reader.querySelector('[data-act="ai-reply"]')?.addEventListener('click', async (ev) => {
ev.stopPropagation();
_snapEmailModalToLeftSidebar(ev.currentTarget.closest('.modal'));
const btn = ev.currentTarget;
btn.disabled = true;
const orig = btn.innerHTML;
// Use the app-wide whirlpool spinner for consistency.
let _wp = null;
try {
_wp = spinnerModule.createWhirlpool(14);
_wp.element.style.cssText = 'width:14px;height:14px;display:inline-block;vertical-align:middle;position:relative;top:-2px;';
btn.innerHTML = '';
btn.appendChild(_wp.element);
} catch (_) {}
try {
if (state._onEmailClick) await state._onEmailClick({ email: em, emailData: data, mode: 'ai-reply' });
} finally {
try { _wp && _wp.stop(); } catch (_) {}
btn.disabled = false;
btn.innerHTML = orig;
}
});
reader.querySelector('[data-act="ai-reply"]')?.addEventListener('click', (ev) => _handleAiReplyButton(ev, em, data));
reader.querySelector('[data-act="forward"]')?.addEventListener('click', async (ev) => {
ev.stopPropagation();
if (state._onEmailClick) await state._onEmailClick({ email: em, emailData: data, mode: 'forward' });
@@ -3730,8 +3742,8 @@ async function _openEmailAsTab(em, folder) {
<button class="memory-toolbar-btn reader-icon-btn" data-act="forward" title="Forward"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 17 20 12 15 7"/><path d="M4 18v-2a4 4 0 0 1 4-4h12"/></svg><span class="reader-btn-label">Forward</span></button>
</div>
<div class="email-reader-actions-row email-reader-actions-row-secondary">
<button class="memory-toolbar-btn reader-icon-btn" data-act="ai-reply" title="AI Reply"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 17 4 12 9 7"/><path d="M20 18v-2a4 4 0 0 0-4-4H4"/><path d="M14 4l1 2 2 1-2 1-1 2-1-2-2-1 2-1z" fill="var(--accent-primary, var(--red))" stroke="none" transform="translate(2 0)"/></svg><span class="reader-btn-label">AI reply</span></button>
<button class="memory-toolbar-btn reader-icon-btn" data-act="summarize" title="Summarize"><svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><path d="M12 0L14.59 8.41L23 12L14.59 15.59L12 24L9.41 15.59L1 12L9.41 8.41Z"/></svg><span class="reader-btn-label">Summary</span></button>
<button class="memory-toolbar-btn reader-icon-btn" data-act="ai-reply" title="${data.cached_ai_reply ? 'AI Reply (cached draft ready)' : 'AI Reply'}">${_aiReplyIcon(data)}<span class="reader-btn-label">AI reply</span></button>
<button class="memory-toolbar-btn reader-icon-btn" data-act="summarize" title="Summarize">${_summaryIcon(data)}<span class="reader-btn-label">Summary</span></button>
<button class="memory-toolbar-btn reader-icon-btn" data-act="from-sender" title="Search text in this thread"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="7"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg><span class="reader-btn-label">Search</span></button>
<div class="email-reader-more-wrap" style="position:relative">
<button class="memory-toolbar-btn reader-icon-btn" data-act="more" title="More actions"><svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><circle cx="12" cy="5" r="2"/><circle cx="12" cy="12" r="2"/><circle cx="12" cy="19" r="2"/></svg><span class="reader-btn-label">More</span></button>
@@ -3758,11 +3770,7 @@ async function _openEmailAsTab(em, folder) {
_snapEmailModalToLeftSidebar(ev.currentTarget.closest('.modal'));
if (state._onEmailClick) await state._onEmailClick({ email: em, emailData: data, mode: 'reply-all' });
});
reader.querySelector('[data-act="ai-reply"]')?.addEventListener('click', async (ev) => {
ev.stopPropagation();
_snapEmailModalToLeftSidebar(ev.currentTarget.closest('.modal'));
if (state._onEmailClick) await state._onEmailClick({ email: em, emailData: data, mode: 'ai-reply' });
});
reader.querySelector('[data-act="ai-reply"]')?.addEventListener('click', (ev) => _handleAiReplyButton(ev, em, data));
reader.querySelector('[data-act="forward"]')?.addEventListener('click', async (ev) => {
ev.stopPropagation();
if (state._onEmailClick) await state._onEmailClick({ email: em, emailData: data, mode: 'forward' });
@@ -3885,8 +3893,8 @@ async function _openEmailWindow(em, folder) {
<button class="memory-toolbar-btn reader-icon-btn" data-act="forward" title="Forward"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 17 20 12 15 7"/><path d="M4 18v-2a4 4 0 0 1 4-4h12"/></svg><span class="reader-btn-label">Forward</span></button>
</div>
<div class="email-reader-actions-row email-reader-actions-row-secondary">
<button class="memory-toolbar-btn reader-icon-btn" data-act="ai-reply" title="AI Reply (suggest a draft)"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 17 4 12 9 7"/><path d="M20 18v-2a4 4 0 0 0-4-4H4"/><path d="M14 4l1 2 2 1-2 1-1 2-1-2-2-1 2-1z" fill="var(--accent-primary, var(--red))" stroke="none" transform="translate(2 0)"/></svg><span class="reader-btn-label">AI reply</span></button>
<button class="memory-toolbar-btn reader-icon-btn" data-act="summarize" title="Summarize"><svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><path d="M12 0L14.59 8.41L23 12L14.59 15.59L12 24L9.41 15.59L1 12L9.41 8.41Z"/></svg><span class="reader-btn-label">Summary</span></button>
<button class="memory-toolbar-btn reader-icon-btn" data-act="ai-reply" title="${data.cached_ai_reply ? 'AI Reply (cached draft ready)' : 'AI Reply (suggest a draft)'}">${_aiReplyIcon(data)}<span class="reader-btn-label">AI reply</span></button>
<button class="memory-toolbar-btn reader-icon-btn" data-act="summarize" title="Summarize">${_summaryIcon(data)}<span class="reader-btn-label">Summary</span></button>
<button class="memory-toolbar-btn reader-icon-btn" data-act="from-sender" title="Search text in this thread"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="7"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg><span class="reader-btn-label">Search</span></button>
<div class="email-reader-more-wrap" style="position:relative">
<button class="memory-toolbar-btn reader-icon-btn" data-act="more" title="More actions"><svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><circle cx="12" cy="5" r="2"/><circle cx="12" cy="12" r="2"/><circle cx="12" cy="19" r="2"/></svg><span class="reader-btn-label">More</span></button>
@@ -3914,11 +3922,7 @@ async function _openEmailWindow(em, folder) {
_snapEmailModalToLeftSidebar(ev.currentTarget.closest('.modal'));
if (state._onEmailClick) await state._onEmailClick({ email: em, emailData: data, mode: 'reply-all' });
});
bodyEl.querySelector('[data-act="ai-reply"]')?.addEventListener('click', async (ev) => {
ev.stopPropagation();
_snapEmailModalToLeftSidebar(ev.currentTarget.closest('.modal'));
if (state._onEmailClick) await state._onEmailClick({ email: em, emailData: data, mode: 'ai-reply' });
});
bodyEl.querySelector('[data-act="ai-reply"]')?.addEventListener('click', (ev) => _handleAiReplyButton(ev, em, data));
bodyEl.querySelector('[data-act="forward"]')?.addEventListener('click', async (ev) => {
ev.stopPropagation();
if (state._onEmailClick) await state._onEmailClick({ email: em, emailData: data, mode: 'forward' });
@@ -4666,6 +4670,88 @@ async function _bulkAction(action) {
// _extractName lives in ./emailLibrary/utils.js
function _aiReplyIcon(data) {
const cachedSpark = data?.cached_ai_reply
? '<path d="M14 4l1 2 2 1-2 1-1 2-1-2-2-1 2-1z" fill="var(--accent-primary, var(--red))" stroke="none" transform="translate(2 0)"/>'
: '';
return `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 17 4 12 9 7"/><path d="M20 18v-2a4 4 0 0 0-4-4H4"/>${cachedSpark}</svg>`;
}
function _summaryIcon(data) {
const fill = data?.cached_summary ? 'var(--accent-primary, var(--red))' : 'currentColor';
return `<svg width="14" height="14" viewBox="0 0 24 24" fill="${fill}"><path d="M12 0L14.59 8.41L23 12L14.59 15.59L12 24L9.41 15.59L1 12L9.41 8.41Z"/></svg>`;
}
async function _runAiReplyFromButton(btn, em, data, mode) {
_snapEmailModalToLeftSidebar(btn.closest('.modal'));
btn.disabled = true;
const orig = btn.innerHTML;
let wp = null;
try {
wp = spinnerModule.createWhirlpool(14);
wp.element.style.cssText = 'width:14px;height:14px;display:inline-block;vertical-align:middle;position:relative;top:-2px;';
btn.innerHTML = '';
btn.appendChild(wp.element);
} catch (_) {}
try {
if (state._onEmailClick) await state._onEmailClick({ email: em, emailData: data, mode });
} finally {
try { wp && wp.stop(); } catch (_) {}
btn.disabled = false;
btn.innerHTML = orig;
}
}
function _closeAiReplyChoice() {
document.querySelectorAll('.email-ai-reply-choice').forEach(el => el.remove());
document.removeEventListener('click', _closeAiReplyChoice, true);
}
function _showAiReplyChoice(btn, em, data) {
_closeAiReplyChoice();
const rect = btn.getBoundingClientRect();
const menu = document.createElement('div');
menu.className = 'email-ai-reply-choice';
menu.style.cssText = [
'position:fixed',
`left:${Math.max(8, Math.min(rect.left, window.innerWidth - 190))}px`,
`top:${Math.min(window.innerHeight - 96, rect.bottom + 6)}px`,
'z-index:10060',
'display:flex',
'gap:6px',
'padding:6px',
'background:var(--bg,#111)',
'border:1px solid var(--border,#333)',
'border-radius:7px',
'box-shadow:0 8px 24px rgba(0,0,0,.28)',
].join(';');
menu.innerHTML = `
<button class="memory-toolbar-btn" data-mode="ai-reply-fast" title="Shorter, faster draft">Fast</button>
<button class="memory-toolbar-btn" data-mode="ai-reply-full" title="Uses the fuller reply context">Full</button>
`;
menu.addEventListener('click', async (ev) => {
const choice = ev.target.closest('[data-mode]');
if (!choice) return;
ev.preventDefault();
ev.stopPropagation();
const mode = choice.getAttribute('data-mode') || 'ai-reply';
_closeAiReplyChoice();
await _runAiReplyFromButton(btn, em, data, mode);
});
document.body.appendChild(menu);
setTimeout(() => document.addEventListener('click', _closeAiReplyChoice, true), 0);
}
function _handleAiReplyButton(ev, em, data) {
ev.stopPropagation();
const btn = ev.currentTarget;
if (data?.cached_ai_reply) {
_runAiReplyFromButton(btn, em, data, 'ai-reply');
return;
}
_showAiReplyChoice(btn, em, data);
}
function _hasMultipleRecipients(data) {
// Count distinct addresses in To + Cc (minus the current user). Empty
// fallback when the user's address isn't yet known — no exclusion.

View File

@@ -700,7 +700,7 @@ function _renderList() {
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.style.cssText = 'position:relative;top:2px;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 now</span>';
runBtn.addEventListener('click', (e) => { e.stopPropagation(); _doRunNow(task.id); });
actionsWrap.insertBefore(runBtn, menuBtn);

View File

@@ -32102,6 +32102,33 @@ button.cal-add-btn.cal-add-btn-text.cal-add-btn-sm:hover .cal-add-label {
inside #email-lib-accounts pack to the left as normal flex items. */
.email-accounts-row > .memory-toolbar-btn { flex-shrink: 0; margin-left: auto; }
#email-lib-accounts { justify-content: flex-start; }
.email-accounts-loading-whirlpool {
width: 14px;
height: 14px;
margin: 3px 4px 0 1px;
display: inline-flex;
flex: 0 0 auto;
}
.email-accounts-loading-label {
font-size: 10px;
opacity: 0.55;
position: relative;
top: 2px;
white-space: nowrap;
}
.email-loading-with-label {
min-height: 180px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8px;
text-align: center;
}
.email-loading-label {
font-size: 11px;
opacity: 0.6;
}
/* Refresh button now lives top-right in the modal header next to the close X.
Borderless (matches the close X), and a fixed square box so the spin and the