Add Tier 2 email features: AI draft, Calendar→Email announce, delivery status, engagement badges; v1.1.24

- EmailPage: useLocation to accept pre-filled compose from Calendar; delivery status pill on sent bubbles; AI Draft button calling ANTHROPIC_API_KEY endpoint
- CalendarPage: useNavigate + announceEpisode(); ✉ Announce button in episode edit popover
- ContactsPage: emailStatus types; unreplied badge; best delivery status pill; engagement score stars
- server/routes/contact.js: POST /api/admin-contact-submissions/:id/draft-reply → Anthropic claude-haiku-4-5

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
nmemmert
2026-07-28 17:10:25 -04:00
parent e2c560a1ab
commit bf16deea92
6 changed files with 207 additions and 7 deletions
+44
View File
@@ -812,4 +812,48 @@ export function register(app) {
res.setHeader('Content-Disposition', `attachment; filename="subscribers-${new Date().toISOString().slice(0, 10)}.csv"`)
res.send(csv)
})
app.post('/api/admin-contact-submissions/:id/draft-reply', requireAdminAuth, async (req, res) => {
const apiKey = process.env.ANTHROPIC_API_KEY
if (!apiKey) { res.status(503).json({ message: 'ANTHROPIC_API_KEY is not configured on the server.' }); return }
const { id } = req.params
if (!id || typeof id !== 'string') { res.status(400).json({ message: 'Invalid submission id.' }); return }
const submission = state.contactSubmissions.find(s => s.id === id.trim())
if (!submission) { res.status(404).json({ message: 'Submission not found.' }); return }
const senderName = submission.name?.trim() || 'this listener'
const messageText = submission.message?.trim() || '(no message body)'
try {
const apiRes = await fetch('https://api.anthropic.com/v1/messages', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': apiKey,
'anthropic-version': '2023-06-01',
},
body: JSON.stringify({
model: 'claude-haiku-4-5-20251001',
max_tokens: 400,
messages: [{
role: 'user',
content: `Draft a warm, personal reply to this message from a podcast listener. Write only the reply body — no greeting line (handled separately), no sign-off (handled by a signature). Keep it under 150 words. Be genuine and specific to their message.\n\nFrom: ${senderName}\nMessage:\n${messageText.slice(0, 1500)}`,
}],
}),
})
if (!apiRes.ok) {
const err = await apiRes.json().catch(() => ({}))
res.status(502).json({ message: err?.error?.message || 'AI draft request failed.' }); return
}
const data = await apiRes.json()
const draft = String(data?.content?.[0]?.text ?? '').trim()
res.json({ ok: true, draft })
} catch {
res.status(502).json({ message: 'AI draft request failed.' })
}
})
}