← back to Norma
app/api/drafts/auto-generate/route.ts
153 lines
/**
* POST /api/drafts/auto-generate
* Feature 1: Auto-Draft from Breaking News
* Takes top recent news articles, sends to Gemini, generates email draft
* with talking points and action items.
*/
import { NextRequest, NextResponse } from 'next/server';
import { requireRole } from '@/lib/require-role';
import { query } from '@/lib/db';
const GEMINI_KEY = process.env.GEMINI_API_KEY;
function getGeminiUrl(): string {
if (!GEMINI_KEY) {
throw new Error('[drafts/auto-generate] GEMINI_API_KEY env var is required');
}
return `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${GEMINI_KEY}`;
}
async function callGemini(prompt: string): Promise<string> {
const res = await fetch(getGeminiUrl(), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
contents: [{ parts: [{ text: prompt }] }],
generationConfig: { temperature: 0.7, maxOutputTokens: 2048 },
}),
});
const data = await res.json();
return data.candidates?.[0]?.content?.parts?.[0]?.text || '';
}
export async function POST(request: NextRequest) {
const auth = requireRole(request, 'admin', 'staff');
if (auth instanceof NextResponse) return auth;
try {
const body = await request.json().catch(() => ({}));
const orgId = body.org_id;
const newsCount = Math.min(body.news_count || 5, 10);
if (!orgId) {
return NextResponse.json({ error: 'org_id required' }, { status: 400 });
}
// Get org name (try nonprofit_accounts first, then organizations)
let orgResult = await query('SELECT org_name as name FROM nonprofit_accounts WHERE id = $1::uuid', [orgId]);
if (orgResult.rows.length === 0) {
orgResult = await query('SELECT name FROM organizations WHERE id = $1::uuid', [orgId]);
}
const orgName = orgResult.rows[0]?.name || 'Your Organization';
// Get top recent news for this org (or global if org has none)
let newsResult = await query(
`SELECT id, headline, outlet, summary, published_at
FROM news_items
WHERE org_id = $1::uuid
ORDER BY published_at DESC NULLS LAST
LIMIT $2`,
[orgId, newsCount]
);
// Fallback to global news if org has none
if (newsResult.rows.length === 0) {
newsResult = await query(
`SELECT id, headline, outlet, summary, published_at
FROM news_items
ORDER BY published_at DESC NULLS LAST
LIMIT $1`,
[newsCount]
);
}
if (newsResult.rows.length === 0) {
return NextResponse.json({ error: 'No news articles available to draft from' }, { status: 404 });
}
const articles = newsResult.rows;
const newsIds = articles.map((a: { id: string }) => a.id);
// Build Gemini prompt
const prompt = `You are a nonprofit communications director at ${orgName}. Generate a professional email draft based on these breaking news stories.
NEWS ARTICLES:
${articles.map((a: { headline: string; outlet: string; summary: string }, i: number) =>
`${i + 1}. "${a.headline}" (${a.outlet || 'unknown'})\n ${(a.summary || '').slice(0, 200)}`
).join('\n\n')}
Generate a JSON response with:
{
"subject": "Email subject line (compelling, action-oriented)",
"body_html": "Full HTML email body with: opening hook, key facts from the articles, 3 talking points, a clear call to action. Use <h2>, <p>, <ul><li>, <strong> tags. Professional nonprofit tone.",
"body_text": "Plain text version of the same email",
"talking_points": ["point 1", "point 2", "point 3"],
"suggested_actions": ["action 1", "action 2"]
}
Respond with JSON only, no markdown code blocks.`;
const geminiResponse = await callGemini(prompt);
// Parse response
let draft;
try {
const cleaned = geminiResponse.replace(/```json\n?/g, '').replace(/```\n?/g, '').trim();
draft = JSON.parse(cleaned);
} catch {
const match = geminiResponse.match(/\{[\s\S]*\}/);
if (match) {
draft = JSON.parse(match[0]);
} else {
return NextResponse.json({ error: 'Failed to generate draft' }, { status: 500 });
}
}
// Insert draft into DB
const insertResult = await query(
`INSERT INTO drafts (
subject, body_html, body_text, lane, lane_label, email_type,
from_name, from_email, status, org_id, source_type, source_news_ids
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
RETURNING id, subject, created_at`,
[
draft.subject,
draft.body_html,
draft.body_text,
'A', 'Breaking News',
'alert',
orgName, `info@${orgName.toLowerCase().replace(/\s+/g, '')}.org`,
'draft',
orgId,
'auto-news',
newsIds,
]
);
return NextResponse.json({
success: true,
draft: {
id: insertResult.rows[0].id,
subject: draft.subject,
talking_points: draft.talking_points,
suggested_actions: draft.suggested_actions,
source_articles: articles.length,
created_at: insertResult.rows[0].created_at,
},
});
} catch (err) {
console.error('[api/drafts/auto-generate] error:', (err as Error).message);
return NextResponse.json({ error: 'Failed to auto-generate draft' }, { status: 500 });
}
}