← back to Patty

components/topics/TopicsTab.tsx

416 lines

'use client';

import { useState, useEffect } from 'react';
import {
  Loader2, Sparkles, ChevronDown, ChevronUp,
  Target, MapPin, FileText, ExternalLink, AlertTriangle,
  DollarSign,
} from 'lucide-react';
import { Grant, formatAmount, STATUS_COLORS } from '../grants/types';

interface Topic {
  id: string;
  title: string;
  description: string;
  category: string;
  urgency: string;
  article_count: number;
  avg_sentiment: number;
  geo_states: string[];
  petition_suggestion: string | null;
  petition_target: string | null;
  petition_tone: string | null;
  is_ai_suggested: boolean;
  created_at: string;
}

interface GeneratedPetition {
  title: string;
  summary: string;
  body_html: string;
  suggested_tags: string[];
  suggested_goal: number;
}

const URGENCY_COLORS: Record<string, string> = {
  critical: '#ef4444', high: '#f59e0b', medium: '#3b82f6', low: '#6b7280',
};

export default function TopicsTab({ onViewPetition, onViewGrant }: { onViewPetition?: (id: number) => void; onViewGrant?: (id: string) => void }) {
  const [topics, setTopics] = useState<Topic[]>([]);
  const [loading, setLoading] = useState(true);
  const [generating, setGenerating] = useState<string | null>(null);
  const [generated, setGenerated] = useState<Record<string, { petition: GeneratedPetition; petitionId: number }>>({});
  const [expanded, setExpanded] = useState<string | null>(null);
  const [discoveredGrants, setDiscoveredGrants] = useState<Record<string, Grant[]>>({});
  const [findingGrants, setFindingGrants] = useState<string | null>(null);

  useEffect(() => {
    (async () => {
      try {
        const res = await fetch('/api/pulse/topics');
        if (res.ok) {
          const data = await res.json();
          setTopics(data.topics || []);
        }
      } catch (err) { console.error('Topics error:', err); }
      finally { setLoading(false); }
    })();
  }, []);

  async function generatePetition(topic: Topic) {
    setGenerating(topic.id);
    try {
      // Step 1: Generate petition via Gemini
      const genRes = await fetch('/api/petitions/generate', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          topic: topic.title,
          target: topic.petition_target || 'Elected Officials',
          tone: topic.petition_tone || 'formal',
          category: topic.category || 'policy',
        }),
      });
      if (!genRes.ok) throw new Error('Generation failed');
      const petition = await genRes.json();

      // Step 2: Save as draft
      const saveRes = await fetch('/api/petitions', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          title: petition.title,
          summary: petition.summary,
          body_html: petition.body_html,
          body_text: petition.body_text || '',
          target: petition.suggested_target || topic.petition_target,
          category: topic.category,
          tags: petition.suggested_tags || [],
          signature_goal: petition.suggested_goal || 1000,
          status: 'draft',
        }),
      });
      if (!saveRes.ok) throw new Error('Save failed');
      const saved = await saveRes.json();

      setGenerated(prev => ({
        ...prev,
        [topic.id]: { petition, petitionId: saved.id || saved.petition?.id },
      }));
      setExpanded(topic.id);

      // Step 3: Auto-discover related grants
      try {
        const grantRes = await fetch('/api/grants-proxy/grants/discover', { method: 'POST' });
        if (grantRes.ok) {
          const grantData = await grantRes.json();
          if (grantData.grants?.length) {
            setDiscoveredGrants(prev => ({ ...prev, [topic.id]: grantData.grants }));
          }
        }
      } catch { /* grant discovery is optional */ }
    } catch (err) {
      console.error('Generate petition error:', err);
    } finally {
      setGenerating(null);
    }
  }

  async function findGrantsForTopic(topic: Topic) {
    setFindingGrants(topic.id);
    try {
      const res = await fetch('/api/grants-proxy/grants/discover', { method: 'POST' });
      if (res.ok) {
        const data = await res.json();
        setDiscoveredGrants(prev => ({ ...prev, [topic.id]: data.grants || [] }));
        setExpanded(topic.id);
      }
    } catch (err) {
      console.error('Find grants error:', err);
    } finally {
      setFindingGrants(null);
    }
  }

  if (loading) {
    return (
      <div style={{ padding: 32, color: 'var(--color-text-muted)', display: 'flex', alignItems: 'center', gap: 8 }}>
        <Loader2 size={18} style={{ animation: 'spin 1s linear infinite' }} /> Loading topics...
      </div>
    );
  }

  return (
    <div style={{ padding: 24, maxWidth: 1000 }}>
      <div style={{ marginBottom: 20 }}>
        <h2 style={{ fontSize: 20, fontWeight: 700, color: 'var(--color-text)', margin: 0 }}>
          Topics
          <span style={{ fontSize: 13, fontWeight: 400, color: 'var(--color-text-muted)', marginLeft: 10 }}>
            {topics.length} AI-suggested topics
          </span>
        </h2>
      </div>

      <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
        {topics.map(topic => {
          const isGenerating = generating === topic.id;
          const gen = generated[topic.id];
          const isExpanded = expanded === topic.id;

          return (
            <div key={topic.id} style={{
              borderRadius: 12, border: '1px solid var(--color-border)',
              backgroundColor: 'var(--color-surface)', overflow: 'hidden',
            }}>
              {/* Topic Header */}
              <div style={{ padding: '16px 20px' }}>
                <div style={{ display: 'flex', alignItems: 'flex-start', gap: 12 }}>
                  <div style={{ flex: 1 }}>
                    <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 6 }}>
                      <h3 style={{ fontSize: 15, fontWeight: 600, color: 'var(--color-text)', margin: 0, lineHeight: 1.3 }}>
                        {topic.title}
                      </h3>
                    </div>
                    <p style={{ fontSize: 13, color: 'var(--color-text-muted)', margin: 0, lineHeight: 1.5 }}>
                      {topic.description}
                    </p>

                    {/* Meta row */}
                    <div style={{ display: 'flex', flexWrap: 'wrap', gap: 10, marginTop: 10 }}>
                      {/* Urgency badge */}
                      <span style={{
                        fontSize: 10, fontWeight: 700, padding: '2px 8px', borderRadius: 10,
                        backgroundColor: (URGENCY_COLORS[topic.urgency] || '#6b7280') + '22',
                        color: URGENCY_COLORS[topic.urgency] || '#6b7280',
                        textTransform: 'uppercase', letterSpacing: '0.04em',
                      }}>
                        {topic.urgency}
                      </span>

                      {/* Category */}
                      <span style={{
                        fontSize: 10, fontWeight: 600, padding: '2px 8px', borderRadius: 10,
                        backgroundColor: 'var(--color-bg)', color: 'var(--color-text-muted)',
                      }}>
                        {topic.category}
                      </span>

                      {/* Article count */}
                      <span style={{ fontSize: 11, color: 'var(--color-text-muted)', display: 'flex', alignItems: 'center', gap: 3 }}>
                        <FileText size={12} /> {topic.article_count} article{topic.article_count !== 1 ? 's' : ''}
                      </span>

                      {/* Geo states */}
                      {topic.geo_states?.length > 0 && (
                        <span style={{ fontSize: 11, color: 'var(--color-text-muted)', display: 'flex', alignItems: 'center', gap: 3 }}>
                          <MapPin size={12} /> {topic.geo_states.join(', ')}
                        </span>
                      )}

                      {/* Sentiment */}
                      <span style={{
                        fontSize: 11,
                        color: topic.avg_sentiment < -0.3 ? '#ef4444' : topic.avg_sentiment > 0.3 ? '#22c55e' : 'var(--color-text-muted)',
                      }}>
                        Sentiment: {topic.avg_sentiment?.toFixed(2)}
                      </span>
                    </div>
                  </div>

                  {/* Action Buttons */}
                  <div style={{ flexShrink: 0, display: 'flex', flexDirection: 'column', gap: 6 }}>
                    {gen ? (
                      <div style={{ display: 'flex', gap: 6 }}>
                        <button
                          onClick={() => setExpanded(isExpanded ? null : topic.id)}
                          style={{
                            display: 'flex', alignItems: 'center', gap: 4, padding: '7px 12px',
                            borderRadius: 8, border: '1px solid var(--color-border)',
                            backgroundColor: 'var(--color-surface)', color: 'var(--color-text)',
                            cursor: 'pointer', fontSize: 12,
                          }}
                        >
                          {isExpanded ? <ChevronUp size={13} /> : <ChevronDown size={13} />}
                          Preview
                        </button>
                        <button
                          onClick={() => onViewPetition?.(gen.petitionId)}
                          style={{
                            display: 'flex', alignItems: 'center', gap: 4, padding: '7px 12px',
                            borderRadius: 8, border: 'none',
                            backgroundColor: '#22c55e', color: '#fff',
                            cursor: 'pointer', fontSize: 12, fontWeight: 600,
                          }}
                        >
                          Edit in Petitions <ExternalLink size={12} />
                        </button>
                      </div>
                    ) : (
                      <button
                        onClick={() => generatePetition(topic)}
                        disabled={isGenerating || !!generating}
                        style={{
                          display: 'flex', alignItems: 'center', gap: 6, padding: '8px 16px',
                          borderRadius: 8, border: 'none',
                          background: isGenerating ? '#6b7280' : 'linear-gradient(135deg, #7c3aed, #6d28d9)',
                          color: '#fff', cursor: isGenerating ? 'wait' : 'pointer',
                          fontSize: 13, fontWeight: 600, opacity: generating && !isGenerating ? 0.5 : 1,
                        }}
                      >
                        {isGenerating ? (
                          <><Loader2 size={14} style={{ animation: 'spin 1s linear infinite' }} /> Generating...</>
                        ) : (
                          <><Sparkles size={14} /> Generate Petition</>
                        )}
                      </button>
                    )}
                    {/* Find Grants button */}
                    <button
                      onClick={() => findGrantsForTopic(topic)}
                      disabled={findingGrants === topic.id}
                      style={{
                        display: 'flex', alignItems: 'center', gap: 5, padding: '6px 14px',
                        borderRadius: 8, border: '1px solid #059669',
                        backgroundColor: 'transparent', color: '#10b981',
                        cursor: findingGrants === topic.id ? 'wait' : 'pointer',
                        fontSize: 12, fontWeight: 600,
                        opacity: findingGrants === topic.id ? 0.7 : 1,
                      }}
                    >
                      {findingGrants === topic.id
                        ? <><Loader2 size={12} style={{ animation: 'spin 1s linear infinite' }} /> Finding…</>
                        : <><DollarSign size={12} /> Find Grants</>
                      }
                    </button>
                  </div>
                </div>
              </div>

              {/* Expanded Grants Only (no petition yet) */}
              {!gen && isExpanded && discoveredGrants[topic.id]?.length > 0 && (
                <div style={{
                  padding: '16px 20px', borderTop: '1px solid var(--color-border)',
                  backgroundColor: 'var(--color-bg)',
                }}>
                  <div style={{ fontSize: 11, fontWeight: 600, color: '#10b981', textTransform: 'uppercase', letterSpacing: '0.04em', marginBottom: 6 }}>
                    Discovered Grants ({discoveredGrants[topic.id].length})
                  </div>
                  <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
                    {discoveredGrants[topic.id].slice(0, 5).map(g => (
                      <div key={g.id}
                        onClick={() => onViewGrant?.(g.id)}
                        style={{
                          display: 'flex', alignItems: 'center', gap: 10, padding: '8px 12px',
                          borderRadius: 8, backgroundColor: 'var(--color-surface)',
                          border: '1px solid var(--color-border)', cursor: 'pointer',
                        }}
                        onMouseEnter={e => { e.currentTarget.style.borderColor = '#059669'; }}
                        onMouseLeave={e => { e.currentTarget.style.borderColor = 'var(--color-border)'; }}
                      >
                        <DollarSign size={14} style={{ color: '#10b981', flexShrink: 0 }} />
                        <div style={{ flex: 1, minWidth: 0 }}>
                          <div style={{ fontSize: 12, fontWeight: 600, color: 'var(--color-text)', overflow: 'hidden', whiteSpace: 'nowrap', textOverflow: 'ellipsis' }}>
                            {g.title}
                          </div>
                          <div style={{ fontSize: 11, color: 'var(--color-text-muted)' }}>
                            {g.funder} · {formatAmount(g.amount_min, g.amount_max)}
                          </div>
                        </div>
                        {g.ai_fit_score != null && (
                          <span style={{ fontSize: 11, fontWeight: 600, color: '#f59e0b', flexShrink: 0 }}>
                            {Math.round(g.ai_fit_score * 100)}%
                          </span>
                        )}
                        <ExternalLink size={12} style={{ color: 'var(--color-text-muted)', flexShrink: 0 }} />
                      </div>
                    ))}
                  </div>
                </div>
              )}

              {/* Expanded Petition Preview */}
              {gen && isExpanded && (
                <div style={{
                  padding: '16px 20px', borderTop: '1px solid var(--color-border)',
                  backgroundColor: 'var(--color-bg)',
                }}>
                  <div style={{ fontSize: 11, fontWeight: 600, color: '#22c55e', textTransform: 'uppercase', letterSpacing: '0.04em', marginBottom: 8 }}>
                    Generated Petition — Saved as Draft
                  </div>
                  <h4 style={{ fontSize: 16, fontWeight: 700, color: 'var(--color-text)', margin: '0 0 8px' }}>
                    {gen.petition.title}
                  </h4>
                  <p style={{ fontSize: 13, color: 'var(--color-text-secondary)', margin: '0 0 12px', lineHeight: 1.6 }}>
                    {gen.petition.summary}
                  </p>
                  {gen.petition.suggested_tags?.length > 0 && (
                    <div style={{ display: 'flex', flexWrap: 'wrap', gap: 4, marginBottom: 12 }}>
                      {gen.petition.suggested_tags.map((tag, i) => (
                        <span key={i} style={{
                          fontSize: 10, padding: '2px 8px', borderRadius: 10,
                          backgroundColor: '#7c3aed22', color: '#a78bfa',
                        }}>{tag}</span>
                      ))}
                    </div>
                  )}

                  {/* Discovered Grants inline */}
                  {discoveredGrants[topic.id]?.length > 0 && (
                    <div>
                      <div style={{ fontSize: 11, fontWeight: 600, color: '#10b981', textTransform: 'uppercase', letterSpacing: '0.04em', marginBottom: 6 }}>
                        Related Grants Found ({discoveredGrants[topic.id].length})
                      </div>
                      <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
                        {discoveredGrants[topic.id].slice(0, 5).map(g => (
                          <div key={g.id}
                            onClick={() => onViewGrant?.(g.id)}
                            style={{
                              display: 'flex', alignItems: 'center', gap: 10, padding: '8px 12px',
                              borderRadius: 8, backgroundColor: 'var(--color-surface)',
                              border: '1px solid var(--color-border)', cursor: 'pointer',
                              transition: 'border-color 0.15s',
                            }}
                            onMouseEnter={e => { e.currentTarget.style.borderColor = '#059669'; }}
                            onMouseLeave={e => { e.currentTarget.style.borderColor = 'var(--color-border)'; }}
                          >
                            <DollarSign size={14} style={{ color: '#10b981', flexShrink: 0 }} />
                            <div style={{ flex: 1, minWidth: 0 }}>
                              <div style={{ fontSize: 12, fontWeight: 600, color: 'var(--color-text)', overflow: 'hidden', whiteSpace: 'nowrap', textOverflow: 'ellipsis' }}>
                                {g.title}
                              </div>
                              <div style={{ fontSize: 11, color: 'var(--color-text-muted)' }}>
                                {g.funder} · {formatAmount(g.amount_min, g.amount_max)}
                              </div>
                            </div>
                            {g.ai_fit_score != null && (
                              <span style={{ fontSize: 11, fontWeight: 600, color: '#f59e0b', flexShrink: 0 }}>
                                {Math.round(g.ai_fit_score * 100)}%
                              </span>
                            )}
                            <ExternalLink size={12} style={{ color: 'var(--color-text-muted)', flexShrink: 0 }} />
                          </div>
                        ))}
                      </div>
                    </div>
                  )}
                </div>
              )}
            </div>
          );
        })}
      </div>

      {topics.length === 0 && (
        <div style={{
          padding: 40, textAlign: 'center',
          color: 'var(--color-text-muted)', fontSize: 14,
        }}>
          <AlertTriangle size={24} style={{ marginBottom: 8, opacity: 0.5 }} />
          <div>No topics found. Topics are generated when news is fetched.</div>
        </div>
      )}
    </div>
  );
}