← back to Freddy
app/api/causes/discover/route.ts
116 lines
import { NextRequest, NextResponse } from 'next/server';
import { query } from '@/lib/db';
import { verifyAuth } from '@/lib/auth';
import { auditLog } from '@/lib/audit';
const GEMINI_URL = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=' + (process.env.GEMINI_API_KEY || '');
export async function POST(request: NextRequest) {
const user = verifyAuth(request);
if (!user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
try {
const prompt = `You are a social impact analyst. Identify 8-10 trending social causes that need urgent attention right now.
For each cause, provide:
- title: A concise, compelling name (max 60 chars)
- description: A 2-3 sentence explanation of why this cause matters now
- category: One of: education, health, environment, poverty, housing, legal_aid, civil_rights, veterans, youth, elderly, disability, arts, other
- urgency_score: A float between 0.0 and 1.0 (1.0 = most urgent)
- tags: An array of 2-4 relevant tags (lowercase, no spaces)
- petition_velocity: Estimated number of related petitions/actions per month (integer)
- news_mentions: Estimated monthly news mentions (integer)
Return ONLY valid JSON array, no markdown, no explanation. Example format:
[{"title":"...", "description":"...", "category":"...", "urgency_score":0.85, "tags":["tag1","tag2"], "petition_velocity": 500, "news_mentions": 1200}]`;
const geminiRes = await fetch(GEMINI_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
contents: [{ parts: [{ text: prompt }] }],
generationConfig: {
temperature: 0.7,
maxOutputTokens: 4096,
},
}),
});
if (!geminiRes.ok) {
const errText = await geminiRes.text();
console.error('[causes/discover] Gemini error:', errText);
return NextResponse.json({ error: 'AI service error' }, { status: 502 });
}
const geminiData = await geminiRes.json();
const rawText = geminiData?.candidates?.[0]?.content?.parts?.[0]?.text || '';
// Extract JSON from response (handle markdown code blocks)
let jsonStr = rawText.trim();
if (jsonStr.startsWith('```')) {
jsonStr = jsonStr.replace(/^```(?:json)?\n?/, '').replace(/\n?```$/, '');
}
let causes: Array<{
title: string;
description: string;
category: string;
urgency_score: number;
tags: string[];
petition_velocity: number;
news_mentions: number;
}>;
try {
causes = JSON.parse(jsonStr);
} catch {
console.error('[causes/discover] Failed to parse Gemini response:', jsonStr.slice(0, 300));
return NextResponse.json({ error: 'Failed to parse AI response' }, { status: 500 });
}
// Insert each discovered cause
const inserted = [];
for (const cause of causes) {
try {
const result = await query(
`INSERT INTO causes (title, description, category, urgency_score, tags, source, petition_velocity, news_mentions)
VALUES ($1, $2, $3, $4, $5, 'ai', $6, $7)
ON CONFLICT DO NOTHING
RETURNING *`,
[
cause.title,
cause.description,
cause.category || 'other',
Math.min(1, Math.max(0, cause.urgency_score || 0.5)),
cause.tags || [],
cause.petition_velocity || 0,
cause.news_mentions || 0,
],
);
if (result.rows.length > 0) {
inserted.push(result.rows[0]);
}
} catch (insertErr) {
console.error('[causes/discover] Insert error for:', cause.title, (insertErr as Error).message);
}
}
await auditLog('causes.discovered', 'cause', null, {
user,
discovered: causes.length,
inserted: inserted.length,
});
return NextResponse.json({
discovered: causes.length,
inserted: inserted.length,
causes: inserted,
});
} catch (err) {
console.error('[causes/discover] Error:', (err as Error).message);
return NextResponse.json({ error: 'Failed to discover causes' }, { status: 500 });
}
}