AI Reply menu: '...' kebab opens a note input to steer the draft

The Fast/Full popover now has a kebab (three-dot) button alongside the
two preset choices. Clicking it expands a textarea below with a
'Draft with note' send button. The textarea is for the user to tell
the AI how to reply ('confirm Tuesday at 2', 'decline politely', 'say
we'll need an extra week') instead of accepting a generic draft.

Plumbing:
- emailLibrary.js: kebab button + note panel inside .email-ai-reply-choice
  menu. Submitting calls _runAiReplyFromButton with mode='ai-reply-full'
  and a noteHint string.
- _runAiReplyFromButton signature gains noteHint; passes it through
  state._onEmailClick as opts.noteHint.
- emailInbox.js consumer: forwards opts.noteHint into _openEmail's new
  5th arg, which puts it in the /api/email/ai-reply POST body as
  user_hint.
- routes/email_routes.py /ai-reply: reads user_hint, appends a
  'User's instructions for THIS reply' section to the user message
  (priority over default tone/length). Also skips the per-message
  AI-reply cache when a hint is set — the cached generic draft would
  silently override the instructions otherwise.
This commit is contained in:
pewdiepie-archdaemon
2026-06-11 18:41:11 +09:00
parent 6a392542f3
commit e1585aa4aa
2 changed files with 14 additions and 4 deletions
+11 -2
View File
@@ -2657,11 +2657,15 @@ def setup_email_routes():
source_uid = (data.get("uid") or "").strip() source_uid = (data.get("uid") or "").strip()
source_folder = (data.get("folder") or "INBOX").strip() source_folder = (data.get("folder") or "INBOX").strip()
fast_reply = bool(data.get("fast", False)) fast_reply = bool(data.get("fast", False))
user_hint = (data.get("user_hint") or "").strip()
if not original_body: if not original_body:
return {"success": False, "error": "No email body provided"} return {"success": False, "error": "No email body provided"}
if message_id: # Skip cache lookup when the caller supplied a user_hint — the
# cached generic reply doesn't reflect the instructions and
# would silently override them.
if message_id and not user_hint:
try: try:
_c = _sql3.connect(SCHEDULED_DB) _c = _sql3.connect(SCHEDULED_DB)
owner_clause, owner_params = _email_cache_owner_clause(owner) owner_clause, owner_params = _email_cache_owner_clause(owner)
@@ -2801,8 +2805,13 @@ def setup_email_routes():
user_msg = ( user_msg = (
f"Recipient: {to}\nSubject: {subject}\n\n" f"Recipient: {to}\nSubject: {subject}\n\n"
f"Original email and any current draft:\n{original_body[:6000]}\n\n" f"Original email and any current draft:\n{original_body[:6000]}\n\n"
f"Draft a reply. Return only the reply body text."
) )
if user_hint:
user_msg += (
f"User's instructions for THIS reply (follow these — they override "
f"defaults like length/tone):\n{user_hint[:2000]}\n\n"
)
user_msg += "Draft a reply. Return only the reply body text."
# Build a candidate chain so a stale session-stored API key # Build a candidate chain so a stale session-stored API key
# (the most common cause of "authentication failed" here) # (the most common cause of "authentication failed" here)
+3 -2
View File
@@ -118,7 +118,7 @@ export function init(documentModule) {
} catch (_) {} } catch (_) {}
if (opts.compose) { _composeNew(); return; } if (opts.compose) { _composeNew(); return; }
if (opts.email) { if (opts.email) {
await _openEmail(opts.email, null, opts.emailData, opts.mode || 'reply'); await _openEmail(opts.email, null, opts.emailData, opts.mode || 'reply', opts.noteHint || '');
} }
}, },
}); });
@@ -630,7 +630,7 @@ function _createEmailItem(em) {
return item; return item;
} }
async function _openEmail(em, itemEl, preloadedData = null, mode = 'reply') { async function _openEmail(em, itemEl, preloadedData = null, mode = 'reply', noteHint = '') {
const aiReplyMode = mode === 'ai-reply-fast' ? 'fast' : (mode === 'ai-reply-full' ? 'full' : ''); const aiReplyMode = mode === 'ai-reply-fast' ? 'fast' : (mode === 'ai-reply-full' ? 'full' : '');
const wantsAiReply = mode === 'ai-reply' || !!aiReplyMode; const wantsAiReply = mode === 'ai-reply' || !!aiReplyMode;
let aiSuggestedBody = null; let aiSuggestedBody = null;
@@ -690,6 +690,7 @@ async function _openEmail(em, itemEl, preloadedData = null, mode = 'reply') {
uid: String(em.uid || ''), uid: String(em.uid || ''),
folder: _currentFolder, folder: _currentFolder,
fast: aiReplyMode ? aiReplyMode === 'fast' : _shouldUseFastAiReply(data), fast: aiReplyMode ? aiReplyMode === 'fast' : _shouldUseFastAiReply(data),
user_hint: (noteHint || '').trim() || undefined,
}), }),
}); });
const result = await res.json(); const result = await res.json();