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:
@@ -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.' })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user