← back to Norma

components/agents/PetitionBuilder.tsx

588 lines

'use client';

import { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import {
  ScrollText, Sparkles, Search, Eye, Users, Copy, CheckCircle2,
  ChevronDown, AlertCircle, Building2, Target, FileText, Hash,
  ExternalLink, Radio, Rocket,
} from 'lucide-react';
import { useToast } from '../ToastProvider';
import { useDebounce } from '@/hooks/useDebounce';

interface Organization {
  id: string;
  name: string;
  city: string | null;
  state: string | null;
}

interface Politician {
  id: string;
  name: string;
  title: string | null;
  party: string | null;
  state: string | null;
  district: string | null;
  office_level: string | null;
}

const TARGET_TYPES = [
  { value: 'congress', label: 'Congress Member' },
  { value: 'state_legislature', label: 'State Legislature' },
  { value: 'federal_agency', label: 'Federal Agency' },
  { value: 'institution', label: 'Institution' },
];

const ISSUE_CATEGORIES = [
  'Education Access', 'Education Funding', 'Workforce Development',
  'Healthcare Access', 'Housing Affordability', 'Economic Justice',
  'Racial Equity', 'Climate Action', 'Immigration Reform',
  'Voting Rights', 'Criminal Justice Reform', 'Food Security',
];

type PetitionPlatform = 'pulse_of_america' | 'moveon';

const PLATFORM_OPTIONS: { value: PetitionPlatform; label: string; description: string; color: string }[] = [
  {
    value: 'pulse_of_america',
    label: 'Pulse of America',
    description: 'Publish locally -- petition is saved to our database and visible on the Pulse petitions page',
    color: '#1B4332',
  },
  {
    value: 'moveon',
    label: 'MoveOn.org',
    description: 'Copy petition text and open MoveOn.org to publish externally',
    color: '#E40000',
  },
];

interface PetitionBuilderProps {
  selectedOrgId: string;
  onOrgChange: (id: string) => void;
}

export default function PetitionBuilder({ selectedOrgId, onOrgChange }: PetitionBuilderProps) {
  const { addToast } = useToast();

  // Data
  const [orgs, setOrgs] = useState<Organization[]>([]);
  const [politicians, setPoliticians] = useState<Politician[]>([]);
  const [polSearch, setPolSearch] = useState('');
  const debouncedPolSearch = useDebounce(polSearch, 300);

  // Form
  const [targetType, setTargetType] = useState('congress');
  const [targetId, setTargetId] = useState('');
  const [title, setTitle] = useState('');
  const [issueCategory, setIssueCategory] = useState('');
  const [signatureGoal, setSignatureGoal] = useState('');
  const [platform, setPlatform] = useState<PetitionPlatform>('pulse_of_america');

  // Generated
  const [generating, setGenerating] = useState(false);
  const [petitionText, setPetitionText] = useState('');
  const [talkingPoints, setTalkingPoints] = useState<string[]>([]);
  const [suggestedGoal, setSuggestedGoal] = useState(0);
  const [showPreview, setShowPreview] = useState(false);
  const [publishing, setPublishing] = useState(false);
  const [published, setPublished] = useState(false);
  const [publishedPetitionId, setPublishedPetitionId] = useState<string | null>(null);
  const [copied, setCopied] = useState(false);

  // Load organizations
  useEffect(() => {
    (async () => {
      try {
        const res = await fetch('/api/orgs?limit=200&sort=name&sort_dir=asc');
        if (res.ok) {
          const data = await res.json();
          setOrgs(data.rows || []);
        }
      } catch {}
    })();
  }, []);

  // Load politicians when searching
  useEffect(() => {
    if (targetType !== 'congress' && targetType !== 'state_legislature') return;
    (async () => {
      try {
        const params = new URLSearchParams({ limit: '50' });
        if (debouncedPolSearch) params.set('q', debouncedPolSearch);
        const res = await fetch(`/api/congress?${params}`);
        if (res.ok) {
          const data = await res.json();
          setPoliticians(data.rows || data.politicians || []);
        }
      } catch {}
    })();
  }, [debouncedPolSearch, targetType]);

  async function handleGenerate() {
    if (!selectedOrgId) {
      addToast('Please select an organization first', 'error');
      return;
    }
    if (!title.trim()) {
      addToast('Please enter a petition title', 'error');
      return;
    }

    setGenerating(true);
    setPetitionText('');
    setTalkingPoints([]);

    try {
      const res = await fetch('/api/agents/petition/generate', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          org_id: selectedOrgId,
          target_type: targetType,
          target_id: targetId || null,
          title: title.trim(),
          issue_category: issueCategory || null,
        }),
      });

      if (!res.ok) {
        const data = await res.json();
        throw new Error(data.error || 'Generation failed');
      }

      const data = await res.json();
      setPetitionText(data.petition_text || '');
      setTalkingPoints(data.talking_points || []);
      setSuggestedGoal(data.suggested_signatures_goal || 5000);
      if (!signatureGoal) setSignatureGoal(String(data.suggested_signatures_goal || 5000));
      addToast('Petition draft generated', 'success');
    } catch (err) {
      addToast((err as Error).message || 'Failed to generate petition', 'error');
    }
    setGenerating(false);
  }

  async function handlePublish() {
    if (!petitionText) {
      addToast('Generate a petition draft first', 'error');
      return;
    }
    if (!title.trim()) {
      addToast('Petition title is required', 'error');
      return;
    }

    if (platform === 'moveon') {
      // MoveOn: copy body to clipboard and open MoveOn petition creation page
      try {
        await navigator.clipboard.writeText(petitionText);
        addToast('Petition body copied! Paste into MoveOn form.', 'success');
      } catch {
        addToast('Failed to copy -- please copy the text manually', 'error');
      }
      window.open('https://sign.moveon.org/petition/start/', '_blank');
      return;
    }

    // Pulse of America: save locally to petitions table via /api/petitions
    setPublishing(true);
    try {
      const res = await fetch('/api/petitions', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          title: title.trim(),
          body: petitionText,
          description: petitionText,
          target: targetId || targetType,
          category: issueCategory || 'other',
          signature_goal: parseInt(signatureGoal) || suggestedGoal || 5000,
          platform: 'custom',
          status: 'active',
          tags: issueCategory ? [issueCategory.toLowerCase().replace(/\s+/g, '_')] : [],
          talking_points: talkingPoints,
        }),
      });
      if (!res.ok) {
        const data = await res.json().catch(() => ({}));
        throw new Error(data.error || 'Failed to publish');
      }
      const data = await res.json();
      setPublishedPetitionId(data.petition?.id || null);
      setPublished(true);
      addToast('Petition published to Pulse of America', 'success');
    } catch (err) {
      addToast((err as Error).message || 'Failed to publish petition', 'error');
    }
    setPublishing(false);
  }

  function handleCopy() {
    navigator.clipboard.writeText(petitionText);
    setCopied(true);
    addToast('Copied to clipboard', 'success');
    setTimeout(() => setCopied(false), 2000);
  }

  return (
    <div style={{ padding: 24, maxWidth: 1100, margin: '0 auto' }}>
      {/* Header */}
      <div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 24 }}>
        <div style={{
          width: 40, height: 40, borderRadius: 12,
          backgroundColor: 'rgba(99,102,241,0.12)',
          display: 'flex', alignItems: 'center', justifyContent: 'center',
        }}>
          <ScrollText size={20} style={{ color: '#6366f1' }} />
        </div>
        <div>
          <h1 style={{ fontSize: 22, fontWeight: 700, color: 'var(--color-text)', margin: 0 }}>
            AI Petition Builder
          </h1>
          <p style={{ fontSize: 13, color: 'var(--color-text-muted)', margin: 0 }}>
            Generate compelling petitions powered by AI
          </p>
        </div>
      </div>

      <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 20 }}>
        {/* Left: Configuration */}
        <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>

          {/* Organization */}
          <div className="card" style={{ padding: 16 }}>
            <label style={{ fontSize: 12, fontWeight: 600, color: 'var(--color-text-secondary)', display: 'flex', alignItems: 'center', gap: 6, marginBottom: 8 }}>
              <Building2 size={14} /> Organization
            </label>
            <select
              className="input"
              value={selectedOrgId}
              onChange={e => onOrgChange(e.target.value)}
            >
              <option value="">-- Select Organization --</option>
              {orgs.map(o => <option key={o.id} value={o.id}>{o.name}</option>)}
            </select>
          </div>

          {/* Target */}
          <div className="card" style={{ padding: 16 }}>
            <label style={{ fontSize: 12, fontWeight: 600, color: 'var(--color-text-secondary)', display: 'flex', alignItems: 'center', gap: 6, marginBottom: 8 }}>
              <Target size={14} /> Target
            </label>
            <select
              className="input"
              value={targetType}
              onChange={e => { setTargetType(e.target.value); setTargetId(''); }}
              style={{ marginBottom: 10 }}
            >
              {TARGET_TYPES.map(t => <option key={t.value} value={t.value}>{t.label}</option>)}
            </select>

            {(targetType === 'congress' || targetType === 'state_legislature') && (
              <>
                <div style={{ position: 'relative', marginBottom: 8 }}>
                  <Search size={14} style={{
                    position: 'absolute', left: 10, top: '50%', transform: 'translateY(-50%)',
                    color: 'var(--color-text-muted)', pointerEvents: 'none',
                  }} />
                  <input
                    className="input"
                    style={{ paddingLeft: 32 }}
                    value={polSearch}
                    onChange={e => setPolSearch(e.target.value)}
                    placeholder="Search congress members..."
                  />
                </div>
                <select
                  className="input"
                  value={targetId}
                  onChange={e => setTargetId(e.target.value)}
                  size={Math.min(politicians.length + 1, 6)}
                  style={{ minHeight: 100 }}
                >
                  <option value="">-- Select Member --</option>
                  {politicians.map(p => (
                    <option key={p.id} value={p.id}>
                      {p.title ? `${p.title} ` : ''}{p.name} ({p.party || '?'}-{p.state || '?'})
                      {p.district ? ` Dist. ${p.district}` : ''}
                    </option>
                  ))}
                </select>
              </>
            )}
          </div>

          {/* Petition Details */}
          <div className="card" style={{ padding: 16 }}>
            <label style={{ fontSize: 12, fontWeight: 600, color: 'var(--color-text-secondary)', display: 'flex', alignItems: 'center', gap: 6, marginBottom: 8 }}>
              <FileText size={14} /> Petition Details
            </label>
            <input
              className="input"
              value={title}
              onChange={e => setTitle(e.target.value)}
              placeholder="Petition title..."
              style={{ marginBottom: 10 }}
            />
            <select
              className="input"
              value={issueCategory}
              onChange={e => setIssueCategory(e.target.value)}
              style={{ marginBottom: 10 }}
            >
              <option value="">-- Issue Category --</option>
              {ISSUE_CATEGORIES.map(c => <option key={c} value={c}>{c}</option>)}
            </select>
            <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
              <Hash size={14} style={{ color: 'var(--color-text-muted)', flexShrink: 0 }} />
              <input
                className="input"
                type="number"
                value={signatureGoal}
                onChange={e => setSignatureGoal(e.target.value)}
                placeholder="Signature goal (auto-suggested)"
                min={100}
              />
            </div>
          </div>

          {/* Platform */}
          <div className="card" style={{ padding: 16 }}>
            <label style={{ fontSize: 12, fontWeight: 600, color: 'var(--color-text-secondary)', display: 'flex', alignItems: 'center', gap: 6, marginBottom: 10 }}>
              <Radio size={14} /> Platform
            </label>
            <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
              {PLATFORM_OPTIONS.map(opt => {
                const isSelected = platform === opt.value;
                return (
                  <button
                    key={opt.value}
                    type="button"
                    onClick={() => setPlatform(opt.value)}
                    style={{
                      display: 'flex', alignItems: 'flex-start', gap: 10,
                      padding: '10px 12px', borderRadius: 8, cursor: 'pointer',
                      border: isSelected ? `2px solid ${opt.color}` : '2px solid var(--color-border)',
                      backgroundColor: isSelected ? `${opt.color}14` : 'transparent',
                      textAlign: 'left', transition: 'var(--transition)',
                    }}
                  >
                    <span style={{
                      width: 18, height: 18, borderRadius: '50%', flexShrink: 0, marginTop: 1,
                      border: isSelected ? `5px solid ${opt.color}` : '2px solid var(--color-text-muted)',
                      backgroundColor: isSelected ? '#fff' : 'transparent',
                      transition: 'var(--transition)',
                    }} />
                    <div>
                      <span style={{ fontSize: 13, fontWeight: 600, color: isSelected ? opt.color : 'var(--color-text)', display: 'block' }}>
                        {opt.label}
                      </span>
                      <span style={{ fontSize: 11, color: 'var(--color-text-muted)', lineHeight: 1.4, display: 'block', marginTop: 2 }}>
                        {opt.description}
                      </span>
                    </div>
                  </button>
                );
              })}
            </div>
          </div>

          {/* Generate Button */}
          <button
            className="btn btn-primary btn-lg"
            onClick={handleGenerate}
            disabled={generating || !selectedOrgId || !title.trim()}
            style={{ width: '100%' }}
          >
            {generating ? (
              <>
                <span className="spinner" style={{ width: 16, height: 16 }} />
                Generating Draft...
              </>
            ) : (
              <>
                <Sparkles size={16} />
                Generate Draft
              </>
            )}
          </button>
        </div>

        {/* Right: Generated Content */}
        <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
          <AnimatePresence mode="wait">
            {petitionText ? (
              <motion.div
                key="content"
                initial={{ opacity: 0, y: 10 }}
                animate={{ opacity: 1, y: 0 }}
                exit={{ opacity: 0 }}
                style={{ display: 'flex', flexDirection: 'column', gap: 16 }}
              >
                {/* Petition Text */}
                <div className="card" style={{ padding: 16 }}>
                  <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 12 }}>
                    <h3 style={{ fontSize: 14, fontWeight: 600, color: 'var(--color-text)', margin: 0 }}>
                      Petition Draft
                    </h3>
                    <div style={{ display: 'flex', gap: 6 }}>
                      <button className="btn btn-ghost btn-sm" onClick={handleCopy}>
                        {copied ? <CheckCircle2 size={14} style={{ color: '#22c55e' }} /> : <Copy size={14} />}
                        {copied ? 'Copied' : 'Copy'}
                      </button>
                      <button className="btn btn-ghost btn-sm" onClick={() => setShowPreview(!showPreview)}>
                        <Eye size={14} />
                        {showPreview ? 'Edit' : 'Preview'}
                      </button>
                    </div>
                  </div>

                  {showPreview ? (
                    <div style={{
                      padding: 16, borderRadius: 8,
                      backgroundColor: 'var(--color-surface-el)',
                      border: '1px solid var(--color-border)',
                      fontSize: 14, color: 'var(--color-text)', lineHeight: 1.7,
                      whiteSpace: 'pre-wrap',
                    }}>
                      <h2 style={{ fontSize: 18, fontWeight: 700, margin: '0 0 12px', color: 'var(--color-text)' }}>
                        {title}
                      </h2>
                      {petitionText}
                      <div style={{ marginTop: 20, padding: '12px 0', borderTop: '1px solid var(--color-border)' }}>
                        <div style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 13, color: 'var(--color-text-secondary)' }}>
                          <Users size={16} />
                          Goal: {parseInt(signatureGoal) || suggestedGoal || 5000} signatures
                        </div>
                      </div>
                    </div>
                  ) : (
                    <textarea
                      className="input"
                      value={petitionText}
                      onChange={e => setPetitionText(e.target.value)}
                      rows={14}
                      style={{ resize: 'vertical', fontFamily: 'inherit', lineHeight: 1.6 }}
                    />
                  )}
                </div>

                {/* Talking Points */}
                {talkingPoints.length > 0 && (
                  <div className="card" style={{ padding: 16 }}>
                    <h3 style={{ fontSize: 14, fontWeight: 600, color: 'var(--color-text)', margin: '0 0 12px' }}>
                      Talking Points
                    </h3>
                    <ul style={{ margin: 0, padding: '0 0 0 20px', display: 'flex', flexDirection: 'column', gap: 6 }}>
                      {talkingPoints.map((point, i) => (
                        <li key={i} style={{ fontSize: 13, color: 'var(--color-text-secondary)', lineHeight: 1.5 }}>
                          {point}
                        </li>
                      ))}
                    </ul>
                  </div>
                )}

                {/* Publish */}
                <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
                  <div style={{ display: 'flex', gap: 10 }}>
                    <button
                      className="btn btn-primary btn-lg"
                      onClick={handlePublish}
                      disabled={publishing || published}
                      style={{
                        flex: 1,
                        backgroundColor: platform === 'moveon' ? '#E40000' : undefined,
                        borderColor: platform === 'moveon' ? '#E40000' : undefined,
                      }}
                    >
                      {published ? (
                        <>
                          <CheckCircle2 size={16} />
                          Published to Pulse of America
                        </>
                      ) : publishing ? (
                        <>
                          <span className="spinner" style={{ width: 16, height: 16 }} />
                          Publishing...
                        </>
                      ) : platform === 'moveon' ? (
                        <>
                          <ExternalLink size={16} />
                          Copy & Open MoveOn.org
                        </>
                      ) : (
                        <>
                          <Rocket size={16} />
                          Publish to Pulse of America
                        </>
                      )}
                    </button>
                    <button
                      className="btn btn-ghost btn-lg"
                      onClick={handleGenerate}
                      disabled={generating}
                    >
                      <Sparkles size={16} />
                      Regenerate
                    </button>
                  </div>

                  {/* View on Pulse link after publishing */}
                  {published && publishedPetitionId && platform === 'pulse_of_america' && (
                    <a
                      href={`http://45.61.58.125:7400/pulse/petitions`}
                      target="_blank"
                      rel="noopener noreferrer"
                      className="btn btn-secondary"
                      style={{
                        display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
                        gap: 6, width: '100%',
                      }}
                    >
                      <ExternalLink size={14} />
                      View on Pulse of America Petitions
                    </a>
                  )}
                </div>
              </motion.div>
            ) : (
              <motion.div
                key="empty"
                initial={{ opacity: 0 }}
                animate={{ opacity: 1 }}
              >
                <div className="card" style={{
                  padding: 48, textAlign: 'center',
                  display: 'flex', flexDirection: 'column', alignItems: 'center',
                }}>
                  <div style={{
                    width: 56, height: 56, borderRadius: 16, marginBottom: 16,
                    backgroundColor: 'rgba(99,102,241,0.1)',
                    display: 'flex', alignItems: 'center', justifyContent: 'center',
                  }}>
                    <ScrollText size={26} style={{ color: '#6366f1', opacity: 0.6 }} />
                  </div>
                  <h3 style={{ fontSize: 16, fontWeight: 600, color: 'var(--color-text)', margin: '0 0 8px' }}>
                    {generating ? 'Generating your petition...' : 'Ready to Generate'}
                  </h3>
                  <p style={{ fontSize: 13, color: 'var(--color-text-muted)', maxWidth: 320, margin: 0 }}>
                    {generating
                      ? 'AI is crafting a compelling petition based on your inputs...'
                      : 'Select an organization, choose a target, and enter your petition title to begin.'}
                  </p>
                  {generating && (
                    <div className="spinner" style={{ marginTop: 20, width: 28, height: 28 }} />
                  )}
                </div>
              </motion.div>
            )}
          </AnimatePresence>
        </div>
      </div>
    </div>
  );
}