← back to Norma
app/api/petitions/ideas/route.ts
194 lines
import { NextRequest, NextResponse } from 'next/server';
import { query } from '@/lib/db';
import { requireRole } from '@/lib/require-role';
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}`;
interface PetitionIdea {
title: string;
description: string;
target: string;
category: string;
urgency: string;
unlikely_factor: string;
connection_to_mission: string;
potential_signatures: number;
}
// ─── GET /api/petitions/ideas ────────────────────────────────────────────────
// Returns existing petition ideas + top trending petitions on student debt
export async function GET(request: NextRequest) {
const auth = requireRole(request, 'admin', 'staff', 'pulse');
if (auth instanceof NextResponse) return auth;
try {
// Fetch top student debt petitions
const petitions = await query(
`SELECT id, title, platform, category, signature_count, growth_rate,
is_climber, target, url, description
FROM trending_petitions
WHERE LOWER(title) LIKE '%student%'
OR LOWER(title) LIKE '%loan%'
OR LOWER(title) LIKE '%debt%'
OR LOWER(title) LIKE '%tuition%'
OR LOWER(title) LIKE '%college%'
OR LOWER(title) LIKE '%university%'
OR LOWER(title) LIKE '%fafsa%'
OR LOWER(category) LIKE '%education%'
OR LOWER(category) LIKE '%student%'
ORDER BY signature_count DESC NULLS LAST
LIMIT 30`,
);
// Fetch existing suggestions
const suggestions = await query(
`SELECT id, title, category, urgency, viability_score, reasoning,
source_topics, source_petitions, status, created_at
FROM petition_suggestions
WHERE status IN ('suggested', 'approved')
ORDER BY viability_score DESC NULLS LAST
LIMIT 20`,
);
// Fetch trending topics related to student debt
const topics = await query(
`SELECT id, topic, score, mentions, category
FROM trending_topics
WHERE LOWER(topic) LIKE '%student%'
OR LOWER(topic) LIKE '%loan%'
OR LOWER(topic) LIKE '%debt%'
OR LOWER(topic) LIKE '%tuition%'
OR LOWER(topic) LIKE '%college%'
OR LOWER(topic) LIKE '%forgiveness%'
OR LOWER(topic) LIKE '%borrower%'
OR LOWER(category) LIKE '%education%'
ORDER BY score DESC
LIMIT 20`,
);
return NextResponse.json({
petitions: petitions.rows,
suggestions: suggestions.rows,
related_topics: topics.rows,
stats: {
petition_count: petitions.rowCount,
suggestion_count: suggestions.rowCount,
topic_count: topics.rowCount,
},
});
} catch (err) {
console.error('[petitions/ideas] GET Error:', (err as Error).message);
return NextResponse.json({ error: (err as Error).message }, { status: 500 });
}
}
// ─── POST /api/petitions/ideas ───────────────────────────────────────────────
// Generate new unlikely petition ideas using Gemini AI
export async function POST(request: NextRequest) {
const auth = requireRole(request, 'admin', 'staff');
if (auth instanceof NextResponse) return auth;
try {
// Gather context: current petitions + topics
const [petResult, topicResult] = await Promise.all([
query(
`SELECT title, signature_count, category FROM trending_petitions
WHERE LOWER(title) LIKE '%student%' OR LOWER(title) LIKE '%loan%' OR LOWER(title) LIKE '%debt%'
ORDER BY signature_count DESC LIMIT 10`,
),
query(
`SELECT topic, score FROM trending_topics
WHERE LOWER(topic) LIKE '%student%' OR LOWER(topic) LIKE '%loan%' OR LOWER(topic) LIKE '%debt%'
ORDER BY score DESC LIMIT 10`,
),
]);
const existingPetitions = petResult.rows.map(p => p.title).join('\n- ');
const trendingTopics = topicResult.rows.map(t => t.topic).join(', ');
const prompt = `You are a creative strategist for a nonprofit advocacy organization.
Based on current trending advocacy topics: ${trendingTopics}
And existing petitions:
- ${existingPetitions}
Generate 5 UNLIKELY but potentially impactful petition ideas that connect the organization's focus areas to unexpected areas. These should be creative, attention-grabbing, and make surprising but logical connections.
For each idea, think about:
- Cross-sector connections (tech, sports, entertainment, environment, healthcare)
- Unlikely allies or targets
- Novel angles that could go viral
Return ONLY valid JSON (no markdown, no code fences):
[
{
"title": "catchy petition title",
"description": "2-3 sentence description of why this petition makes sense",
"target": "who the petition is directed at",
"category": "education|policy|cross_sector|advocacy",
"urgency": "high|medium|low",
"unlikely_factor": "what makes this unexpected/creative",
"connection_to_mission": "how this connects back to the organization's mission",
"potential_signatures": estimated_number
}
]`;
const res = await fetch(GEMINI_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
contents: [{ parts: [{ text: prompt }] }],
generationConfig: { temperature: 0.8, maxOutputTokens: 2000 },
}),
});
if (!res.ok) {
return NextResponse.json({ error: 'Gemini API failed' }, { status: 502 });
}
const data = await res.json();
let text = data?.candidates?.[0]?.content?.parts?.[0]?.text ?? '';
text = text.replace(/```json\s*/gi, '').replace(/```\s*/gi, '').trim();
const ideas: PetitionIdea[] = JSON.parse(text);
// Save to petition_suggestions
let saved = 0;
for (const idea of ideas) {
try {
await query(
`INSERT INTO petition_suggestions (title, category, urgency, viability_score, reasoning, status)
VALUES ($1, $2, $3, $4, $5, 'suggested')
ON CONFLICT DO NOTHING`,
[
idea.title,
idea.category || 'cross_sector',
idea.urgency || 'medium',
idea.potential_signatures > 50000 ? 0.8 : idea.potential_signatures > 10000 ? 0.6 : 0.4,
`${idea.description}\n\nUnlikely factor: ${idea.unlikely_factor}\nConnection: ${idea.connection_to_mission}`,
],
);
saved++;
} catch {
// Skip duplicates
}
}
return NextResponse.json({
ideas,
saved,
stats: {
generated: ideas.length,
saved,
},
});
} catch (err) {
console.error('[petitions/ideas] POST Error:', (err as Error).message);
return NextResponse.json({ error: (err as Error).message }, { status: 500 });
}
}