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