outreach: voice by-purpose (larger sample) + Tier-B Gmail draft creation (v0.1.0:71)
(1) Voice: _voice_examples now picks the sender's prior sent emails OF THE SAME PURPOSE (PURPOSE_PATTERNS keyword cues per outreach type), larger sample (8) weighted by purpose then recency — not just recent. meta carries on_topic for transparency. (2) Tier-B sending (gmail.compose now authorized in Workspace DWD). New email_integration/compose.py create_outreach_draft: mints a compose-scoped DWD token for the sender (credentials._mint/access_token_for parameterized by scope; GMAIL_COMPOSE_SCOPE), builds an RFC822 message, and POSTs gmail.drafts.create into the SENDER's mailbox — as an in-thread reply (threadId + In-Reply-To/References, recipient = matched LP address) when there's an active thread, else a fresh email. NEVER sends — the human sends from Gmail (guardrails #4, #6). Route POST /api/outreach/gmail-draft; UI "Create Gmail draft" button + "Open Gmail Drafts" link. Tests: test_compose.py (parse/reply-target/RFC822+threading). Message construction unit-verified; the live drafts.create runs on the box. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -9957,6 +9957,17 @@
|
||||
const [result, setResult] = useState(null);
|
||||
const [draftText, setDraftText] = useState('');
|
||||
const [radar, setRadar] = useState([]);
|
||||
const [lastInvestor, setLastInvestor] = useState('');
|
||||
const [gmailBusy, setGmailBusy] = useState(false);
|
||||
const [gmailResult, setGmailResult] = useState(null);
|
||||
const GMAIL_FAIL = {
|
||||
no_recipient: "No email address on file for this investor, so there's nothing to address the draft to.",
|
||||
integration_disabled: 'Gmail integration is off on the server.',
|
||||
auth_error: 'Could not authorize Gmail — check the compose scope is authorized in Workspace admin.',
|
||||
no_sender: 'Your account has no email on file to draft from.',
|
||||
empty: 'The draft is empty.',
|
||||
gmail_error: 'Gmail rejected the draft.',
|
||||
};
|
||||
const TYPES = [
|
||||
['intro', 'Intro'],
|
||||
['follow_up', 'Warm follow-up'],
|
||||
@@ -9994,6 +10005,8 @@
|
||||
try {
|
||||
setDrafting(true);
|
||||
setResult(null);
|
||||
setGmailResult(null);
|
||||
setLastInvestor(inv);
|
||||
const res = await api('/api/outreach/draft', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ investor_id: inv, outreach_type: t, guidance }),
|
||||
@@ -10015,6 +10028,26 @@
|
||||
catch (_) { onShowToast('Could not copy', 'error'); }
|
||||
};
|
||||
|
||||
const createGmailDraft = async () => {
|
||||
if (gmailBusy || !lastInvestor) return;
|
||||
try {
|
||||
setGmailBusy(true);
|
||||
setGmailResult(null);
|
||||
const res = await api('/api/outreach/gmail-draft', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ investor_id: lastInvestor, draft: draftText }),
|
||||
}, token);
|
||||
const d = res.data || res;
|
||||
setGmailResult(d);
|
||||
if (d.status === 'ok') onShowToast(d.threaded ? 'Reply draft created in your Gmail' : 'Draft created in your Gmail', 'success');
|
||||
else onShowToast(GMAIL_FAIL[d.status] || d.reason || 'Could not create the draft', 'error');
|
||||
} catch (err) {
|
||||
onShowToast(getErrorMessage(err, 'Could not create the draft'), 'error');
|
||||
} finally {
|
||||
setGmailBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
const ok = result && result.status === 'ok';
|
||||
|
||||
return (
|
||||
@@ -10092,7 +10125,15 @@
|
||||
value={draftText} onChange={(e) => setDraftText(e.target.value)} />
|
||||
<div className="index-action-buttons" style={{ marginTop: '10px' }}>
|
||||
<button onClick={copy}>Copy draft</button>
|
||||
<button onClick={createGmailDraft} disabled={gmailBusy}>
|
||||
{gmailBusy ? 'Creating…' : 'Create Gmail draft'}
|
||||
</button>
|
||||
</div>
|
||||
{gmailResult && gmailResult.status === 'ok' && (
|
||||
<div className="index-action-hint" style={{ marginTop: '8px' }}>
|
||||
{gmailResult.threaded ? 'In-thread reply draft' : 'Draft'} created in your Gmail. <a href={gmailResult.gmail_url} target="_blank" rel="noopener noreferrer">Open Gmail Drafts</a> to review and send.
|
||||
</div>
|
||||
)}
|
||||
<div className="index-action-hint" style={{ marginTop: '12px' }}>
|
||||
<strong>Voice based on:</strong>{' '}
|
||||
{result.voice_examples && result.voice_examples.length > 0
|
||||
|
||||
Reference in New Issue
Block a user