← back to Norma

app/api/library/generate/route.ts

83 lines

import { NextRequest, NextResponse } from 'next/server';
import { requireRole } from '@/lib/require-role';
import { getBrand } from '@/lib/brand';
import { getOrgId } from '@/lib/orgId';

const GEMINI_KEY = process.env.GEMINI_API_KEY || '';
const GEMINI_URL = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${GEMINI_KEY}`;

/**
 * POST /api/library/generate
 * Use Gemini AI to generate a library template or snippet.
 * Body: { topic, item_type, email_type, voice_mode }
 */
export async function POST(request: NextRequest) {
  const auth = requireRole(request, 'admin', 'staff');
  if (auth instanceof NextResponse) return auth;

  try {
    const body = await request.json();
    const orgId = auth.role === 'admin' ? getOrgId(request) : auth.orgId;
    const brand = await getBrand(orgId);
    const focusDesc = brand.focusAreas?.length > 0 ? brand.focusAreas.join(', ') : 'advocacy';

    const {
      topic = focusDesc,
      item_type = 'template',
      email_type = 'action',
      voice_mode = 'passionate',
    } = body;

    const prompt = `Generate an email ${item_type} for ${brand.name}, a nonprofit organization focused on ${focusDesc}, about "${topic}".

Requirements:
- Email type: ${email_type} (action = call-to-action, policy = policy update, resources = helpful resources)
- Voice/tone: ${voice_mode} (formal, passionate, urgent, or conversational)
- ${item_type === 'template' ? 'This should be a full email template with placeholders like [BORROWER_NAME], [ACTION_LINK], etc.' : 'This should be a reusable content snippet/block that can be inserted into any email.'}

Return a JSON object with:
- title: short descriptive title for this ${item_type}
- description: 1-2 sentence description of what this ${item_type} is for
- content_html: the full HTML content with basic formatting (p tags, strong, ul/li, a tags)
- content_text: plain text version
- suggested_tags: array of 3-5 relevant tags

Return ONLY the JSON object, no markdown fences or other text.`;

    const geminiRes = await fetch(GEMINI_URL, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        contents: [{ parts: [{ text: prompt }] }],
        generationConfig: { temperature: 0.8, maxOutputTokens: 4096 },
      }),
    });

    if (!geminiRes.ok) {
      return NextResponse.json({ error: 'AI generation failed' }, { status: 502 });
    }

    const geminiData = await geminiRes.json();
    const rawText = geminiData.candidates?.[0]?.content?.parts?.[0]?.text || '{}';
    const cleaned = rawText.replace(/```(?:json)?\s*/g, '').replace(/```\s*/g, '').trim();

    let generated;
    try {
      generated = JSON.parse(cleaned);
    } catch {
      return NextResponse.json({ error: 'Failed to parse AI response' }, { status: 502 });
    }

    return NextResponse.json({
      title: generated.title || `Generated ${item_type}`,
      description: generated.description || '',
      content_html: generated.content_html || '',
      content_text: generated.content_text || '',
      suggested_tags: generated.suggested_tags || [],
    });
  } catch (err) {
    console.error('[api/library/generate] POST error:', (err as Error).message);
    return NextResponse.json({ error: 'Failed to generate content' }, { status: 500 });
  }
}