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