← back to Norma

app/api/member-email/route.ts

214 lines

/**
 * POST /api/member-email
 * AI-powered member email generator.
 * Generates subject line, HTML body, and CTA based on email type, topic, key points, and tone.
 *
 * Request body:
 *   {
 *     emailType: 'breaking-news' | 'issue-update' | 'education-event',
 *     topic: string,
 *     keyPoints: string,
 *     tone: 'urgent' | 'informative' | 'inspirational' | 'call-to-action',
 *     orgName?: string,
 *     orgMission?: string,
 *   }
 *
 * Response:
 *   { subject: string, bodyHtml: string }
 */

import { NextRequest, NextResponse } from 'next/server';
import { requireRole } from '@/lib/require-role';
import { checkRateLimit } from '@/lib/rate-limit';

const GEMINI_API_KEY = process.env.GEMINI_API_KEY;
const GEMINI_URL = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent`;

const EMAIL_TYPE_LABELS: Record<string, string> = {
  'breaking-news': 'Breaking News Alert',
  'issue-update': 'Issue Update',
  'education-event': 'Education Event Invitation',
};

const TONE_INSTRUCTIONS: Record<string, string> = {
  urgent: 'Use an urgent, time-sensitive tone. Convey that immediate action or attention is needed. Short, punchy sentences.',
  informative: 'Use a clear, informative tone. Focus on facts and context. Be thorough but concise.',
  inspirational: 'Use an uplifting, inspirational tone. Emphasize hope, progress, and the power of collective action.',
  'call-to-action': 'Use a strong call-to-action tone. Every paragraph should build toward getting the reader to take a specific action.',
};

const TYPE_CTA_HINTS: Record<string, string> = {
  'breaking-news': 'Include a CTA to stay informed, share the news, or take immediate protective/responsive action.',
  'issue-update': 'Include a CTA to continue supporting the campaign, share milestones, or join the next phase of advocacy.',
  'education-event': 'Include a CTA to register for the event, add it to their calendar, and share with peers.',
};

export async function POST(request: NextRequest) {
  const auth = requireRole(request, 'admin', 'staff');
  if (auth instanceof NextResponse) return auth;

  // Rate limit: 8 email generations per minute
  const rl = checkRateLimit(request, 8, 60_000, 'member-email');
  if (rl.limited) {
    return NextResponse.json(
      { error: 'Too many generation requests. Please wait a moment.' },
      { status: 429 },
    );
  }

  try {
    const body = await request.json();
    const { emailType, topic, keyPoints, tone, orgName, orgMission } = body;

    if (!emailType || !topic) {
      return NextResponse.json(
        { error: 'emailType and topic are required' },
        { status: 400 },
      );
    }

    const typeLabel = EMAIL_TYPE_LABELS[emailType] || emailType;
    const toneInstruction = TONE_INSTRUCTIONS[tone] || TONE_INSTRUCTIONS['informative'];
    const ctaHint = TYPE_CTA_HINTS[emailType] || '';

    const prompt = `You are an expert nonprofit communications director. Generate a member email for a nonprofit organization.

EMAIL TYPE: ${typeLabel}
TOPIC: ${topic}
${keyPoints ? `KEY POINTS TO INCLUDE:\n${keyPoints}` : ''}
${orgName ? `ORGANIZATION: ${orgName}` : ''}
${orgMission ? `MISSION: ${orgMission}` : ''}

TONE: ${toneInstruction}

${ctaHint}

INSTRUCTIONS:
1. Generate a compelling subject line (max 80 characters)
2. Generate the email body in clean HTML format
3. The body should have:
   - An engaging opening paragraph
   - 2-3 content paragraphs incorporating the key points
   - A clearly marked Call-to-Action section with a button-style link placeholder
   - A brief closing
4. Use semantic HTML: <h2>, <p>, <strong>, <ul>/<li> where appropriate
5. Do NOT include <html>, <head>, <body>, or <style> tags — just the inner content
6. Do NOT include email headers (From, To, Date) — just subject and body

RESPOND IN EXACTLY THIS FORMAT (no markdown code fences):
SUBJECT: [your subject line here]
---BODY---
[your HTML body here]`;

    // Try Gemini AI first
    if (GEMINI_API_KEY) {
      try {
        const res = await fetch(`${GEMINI_URL}?key=${GEMINI_API_KEY}`, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({
            contents: [{ parts: [{ text: prompt }] }],
            generationConfig: { temperature: 0.7, maxOutputTokens: 4096 },
          }),
        });

        if (res.ok) {
          const data = await res.json();
          const rawText = data?.candidates?.[0]?.content?.parts?.[0]?.text || '';

          // Parse the structured response
          const subjectMatch = rawText.match(/SUBJECT:\s*(.+?)(?:\n|---)/);
          const bodyMatch = rawText.split('---BODY---');

          if (subjectMatch && bodyMatch.length > 1) {
            const subject = subjectMatch[1].trim();
            let bodyHtml = bodyMatch[1].trim();
            // Clean any residual markdown code fences
            bodyHtml = bodyHtml.replace(/^```html?\s*\n?/i, '').replace(/\n?```\s*$/i, '').trim();

            return NextResponse.json({ subject, bodyHtml });
          }

          // Fallback: try to extract something usable
          const lines = rawText.split('\n');
          const subject = lines[0]?.replace(/^(SUBJECT:|Subject:)\s*/i, '').trim() || `${typeLabel}: ${topic}`;
          const bodyHtml = lines.slice(1).join('\n').replace(/---BODY---/g, '').trim();

          return NextResponse.json({ subject, bodyHtml: bodyHtml || generateFallbackHtml(typeLabel, topic, keyPoints, tone) });
        }
      } catch (aiErr) {
        console.error('[member-email] Gemini error, falling back to template:', (aiErr as Error).message);
      }
    }

    // Fallback: template-based generation
    const subject = generateFallbackSubject(emailType, topic);
    const bodyHtml = generateFallbackHtml(typeLabel, topic, keyPoints, tone);

    return NextResponse.json({ subject, bodyHtml });
  } catch (err) {
    console.error('[member-email] Error:', (err as Error).message);
    return NextResponse.json(
      { error: 'Email generation failed' },
      { status: 500 },
    );
  }
}

/* ── Fallback template generators ──────────────────────────────────────── */

function generateFallbackSubject(emailType: string, topic: string): string {
  const prefix: Record<string, string> = {
    'breaking-news': 'BREAKING',
    'issue-update': 'Update',
    'education-event': 'You\'re Invited',
  };
  return `${prefix[emailType] || 'Important'}: ${topic}`;
}

function generateFallbackHtml(typeLabel: string, topic: string, keyPoints: string, tone: string): string {
  const points = keyPoints
    ? keyPoints.split('\n').filter(Boolean).map(p => `<li>${p.replace(/^[-*]\s*/, '')}</li>`).join('\n      ')
    : '';

  const toneOpener: Record<string, string> = {
    urgent: 'This requires your immediate attention.',
    informative: 'We want to keep you informed about an important development.',
    inspirational: 'Together, we are making a real difference.',
    'call-to-action': 'Your voice matters now more than ever.',
  };

  const toneCTA: Record<string, string> = {
    urgent: 'Act Now',
    informative: 'Learn More',
    inspirational: 'Join the Movement',
    'call-to-action': 'Take Action Today',
  };

  return `<h2 style="color: #10b981; margin: 0 0 16px;">${typeLabel}</h2>

<p style="font-size: 16px; line-height: 1.6; color: #e0e0e8;">
  ${toneOpener[tone] || toneOpener['informative']}
</p>

<p style="font-size: 16px; line-height: 1.6; color: #e0e0e8;">
  <strong>${topic}</strong> &mdash; This is a critical moment for our community and the issues we care about.
  We are reaching out to ensure you have the latest information and know how you can make a difference.
</p>

${points ? `<h3 style="color: #f0f0f5; margin: 20px 0 12px;">Key Points</h3>
<ul style="color: #c0c0d0; line-height: 1.8; padding-left: 20px;">
      ${points}
</ul>` : ''}

<div style="margin: 28px 0; text-align: center;">
  <a href="#" style="display: inline-block; padding: 14px 32px; background-color: #10b981; color: #ffffff; text-decoration: none; border-radius: 8px; font-weight: 700; font-size: 16px;">
    ${toneCTA[tone] || 'Take Action'}
  </a>
</div>

<p style="font-size: 14px; line-height: 1.6; color: #a1a1b5;">
  Thank you for being part of our community. Together, we can create meaningful change.
</p>`;
}