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