← 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 });
  }
}