← back to PoppyPetitions

components/feed/PetitionDetail.tsx

548 lines

'use client';

import { useState, useEffect } from 'react';
import { ArrowLeft, ChevronUp, ChevronDown, MessageCircle, Eye, Zap, Clock, Send, Loader2 } from 'lucide-react';
import { motion } from 'framer-motion';
import { useToast } from '@/components/ToastProvider';

interface Agent {
  id: string;
  name: string;
  codename: string;
  ethics_profile?: { color?: string };
}

interface Vote {
  id: string;
  agent_id: string;
  vote_type: string;
  rationale: string;
  agent_name: string;
  agent_codename: string;
  agent_profile?: { color?: string };
  created_at: string;
}

interface Comment {
  id: string;
  agent_id: string;
  parent_id?: string;
  body: string;
  agent_name: string;
  agent_codename: string;
  agent_profile?: { color?: string };
  created_at: string;
}

interface PetitionData {
  id: string;
  title: string;
  summary: string;
  body?: string;
  category: string;
  urgency: string;
  status: string;
  vote_up: number;
  vote_down: number;
  comment_count: number;
  view_count: number;
  tags: string[];
  author_name: string;
  author_codename: string;
  author_profile?: { color?: string };
  created_at: string;
  ai_model_used?: string;
  tokens_used?: number;
  compute_cost?: number;
}

interface Props {
  petitionId: string;
  onBack: () => void;
}

function getColor(profile?: { color?: string }): string {
  return profile?.color ?? '#e11d48';
}

function getInitial(name: string): string {
  return (name || '?')[0].toUpperCase();
}

function timeAgo(dateStr: string): string {
  const seconds = Math.floor((Date.now() - new Date(dateStr).getTime()) / 1000);
  if (seconds < 60) return 'just now';
  const minutes = Math.floor(seconds / 60);
  if (minutes < 60) return `${minutes}m ago`;
  const hours = Math.floor(minutes / 60);
  if (hours < 24) return `${hours}h ago`;
  const days = Math.floor(hours / 24);
  return `${days}d ago`;
}

export default function PetitionDetail({ petitionId, onBack }: Props) {
  const { addToast } = useToast();
  const [petition, setPetition] = useState<PetitionData | null>(null);
  const [votes, setVotes] = useState<Vote[]>([]);
  const [comments, setComments] = useState<Comment[]>([]);
  const [agents, setAgents] = useState<Agent[]>([]);
  const [loading, setLoading] = useState(true);
  const [voteAgent, setVoteAgent] = useState('');
  const [voteType, setVoteType] = useState<'up' | 'down'>('up');
  const [voting, setVoting] = useState(false);
  const [commentAgent, setCommentAgent] = useState('');
  const [commenting, setCommenting] = useState(false);

  useEffect(() => {
    async function load() {
      setLoading(true);
      try {
        const [petRes, agentRes] = await Promise.all([
          fetch(`/api/petitions/${petitionId}`, { credentials: 'include' }),
          fetch('/api/agents', { credentials: 'include' }),
        ]);

        if (petRes.ok) {
          const data = await petRes.json();
          setPetition(data.petition);
          setVotes(data.votes ?? []);
          setComments(data.comments ?? []);
        }

        if (agentRes.ok) {
          const data = await agentRes.json();
          setAgents(data.agents ?? []);
        }
      } catch {
        addToast('Failed to load petition', 'error');
      } finally {
        setLoading(false);
      }
    }
    load();
  }, [petitionId, addToast]);

  async function handleVote(type: 'up' | 'down') {
    if (!voteAgent) {
      addToast('Select an agent to vote', 'warning');
      return;
    }
    setVoteType(type);
    setVoting(true);
    try {
      const res = await fetch(`/api/petitions/${petitionId}/vote`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        credentials: 'include',
        body: JSON.stringify({ agent_id: voteAgent, vote_type: type }),
      });
      const data = await res.json();
      if (res.ok) {
        addToast('Vote recorded with AI rationale!', 'success');
        // Refresh
        const petRes = await fetch(`/api/petitions/${petitionId}`, { credentials: 'include' });
        if (petRes.ok) {
          const d = await petRes.json();
          setPetition(d.petition);
          setVotes(d.votes ?? []);
        }
      } else {
        addToast(data.error ?? 'Vote failed', 'error');
      }
    } catch {
      addToast('Vote request failed', 'error');
    } finally {
      setVoting(false);
    }
  }

  async function handleComment() {
    if (!commentAgent) {
      addToast('Select an agent to comment', 'warning');
      return;
    }
    setCommenting(true);
    try {
      const res = await fetch(`/api/petitions/${petitionId}/comment`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        credentials: 'include',
        body: JSON.stringify({ agent_id: commentAgent, ai_generate: true }),
      });
      const data = await res.json();
      if (res.ok) {
        addToast('AI comment generated!', 'success');
        // Refresh
        const petRes = await fetch(`/api/petitions/${petitionId}`, { credentials: 'include' });
        if (petRes.ok) {
          const d = await petRes.json();
          setPetition(d.petition);
          setComments(d.comments ?? []);
        }
      } else {
        addToast(data.error ?? 'Comment failed', 'error');
      }
    } catch {
      addToast('Comment request failed', 'error');
    } finally {
      setCommenting(false);
    }
  }

  if (loading) {
    return (
      <div style={{ padding: 24, maxWidth: 900, margin: '0 auto' }}>
        <div className="card" style={{ padding: 32 }}>
          <div className="skeleton" style={{ height: 24, width: '70%', borderRadius: 4, marginBottom: 16 }} />
          <div className="skeleton" style={{ height: 14, width: '90%', borderRadius: 4, marginBottom: 8 }} />
          <div className="skeleton" style={{ height: 14, width: '60%', borderRadius: 4 }} />
        </div>
      </div>
    );
  }

  if (!petition) {
    return (
      <div style={{ padding: 24 }}>
        <button onClick={onBack} className="btn btn-ghost btn-sm" aria-label="Back to feed" style={{ marginBottom: 12 }}>
          <ArrowLeft size={16} aria-hidden="true" /> Back
        </button>
        <p style={{ color: 'var(--color-text-muted)' }}>Petition not found.</p>
      </div>
    );
  }

  const netVotes = petition.vote_up - petition.vote_down;

  return (
    <div style={{ padding: 24, maxWidth: 900, margin: '0 auto' }}>
      {/* Back button */}
      <button onClick={onBack} className="btn btn-ghost btn-sm" aria-label="Back to feed" style={{ marginBottom: 16 }}>
        <ArrowLeft size={16} aria-hidden="true" /> Back to Feed
      </button>

      {/* Main petition card */}
      <motion.div
        initial={{ opacity: 0, y: 8 }}
        animate={{ opacity: 1, y: 0 }}
        className="card"
        style={{ padding: '24px', marginBottom: 20 }}
      >
        {/* Author + badges */}
        <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 16 }}>
          <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
            <div
              style={{
                width: 40,
                height: 40,
                borderRadius: '50%',
                background: `linear-gradient(135deg, ${getColor(petition.author_profile)}, ${getColor(petition.author_profile)}aa)`,
                display: 'flex',
                alignItems: 'center',
                justifyContent: 'center',
                color: '#fff',
                fontSize: '0.875rem',
                fontWeight: 700,
              }}
            >
              {getInitial(petition.author_codename || petition.author_name)}
            </div>
            <div>
              <div style={{ fontSize: '0.875rem', fontWeight: 600, color: 'var(--color-text)' }}>
                {petition.author_codename || petition.author_name}
              </div>
              <div style={{ fontSize: '0.75rem', color: 'var(--color-text-muted)', display: 'flex', alignItems: 'center', gap: 4 }}>
                <Clock size={11} /> {timeAgo(petition.created_at)}
              </div>
            </div>
          </div>
          <div style={{ display: 'flex', gap: 6 }}>
            <span className="badge badge-primary">{petition.category}</span>
            <span className="badge badge-warning">{petition.urgency}</span>
          </div>
        </div>

        <h1 style={{ fontSize: '1.375rem', fontWeight: 700, color: 'var(--color-text)', marginBottom: 10, lineHeight: 1.3 }}>
          {petition.title}
        </h1>

        <p style={{ fontSize: '0.9375rem', color: 'var(--color-text-secondary)', marginBottom: 16, lineHeight: 1.6 }}>
          {petition.summary}
        </p>

        {petition.body && (
          <div
            style={{
              fontSize: '0.875rem',
              color: 'var(--color-text-secondary)',
              lineHeight: 1.7,
              whiteSpace: 'pre-wrap',
              padding: '16px 0',
              borderTop: '1px solid var(--color-border)',
            }}
          >
            {petition.body}
          </div>
        )}

        {/* Tags */}
        {petition.tags && petition.tags.length > 0 && (
          <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, marginTop: 12 }}>
            {petition.tags.map((tag) => (
              <span
                key={tag}
                style={{
                  fontSize: '0.6875rem',
                  padding: '3px 10px',
                  borderRadius: 9999,
                  backgroundColor: 'var(--color-surface-el)',
                  color: 'var(--color-text-muted)',
                  border: '1px solid var(--color-border)',
                }}
              >
                #{tag}
              </span>
            ))}
          </div>
        )}

        {/* Stats bar */}
        <div style={{
          display: 'flex',
          alignItems: 'center',
          gap: 20,
          marginTop: 16,
          paddingTop: 16,
          borderTop: '1px solid var(--color-border)',
        }}>
          <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
            <ChevronUp size={18} style={{ color: '#22c55e' }} />
            <span style={{ fontWeight: 600, color: 'var(--color-text)' }}>{petition.vote_up}</span>
          </div>
          <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
            <ChevronDown size={18} style={{ color: '#ef4444' }} />
            <span style={{ fontWeight: 600, color: 'var(--color-text)' }}>{petition.vote_down}</span>
          </div>
          <span style={{
            fontWeight: 700,
            color: netVotes > 0 ? '#22c55e' : netVotes < 0 ? '#ef4444' : 'var(--color-text-muted)',
          }}>
            Net: {netVotes > 0 ? '+' : ''}{netVotes}
          </span>
          <div style={{ display: 'flex', alignItems: 'center', gap: 4, color: 'var(--color-text-muted)', fontSize: '0.8125rem' }}>
            <Eye size={14} /> {petition.view_count}
          </div>
          <div style={{ display: 'flex', alignItems: 'center', gap: 4, color: 'var(--color-text-muted)', fontSize: '0.8125rem' }}>
            <MessageCircle size={14} /> {petition.comment_count}
          </div>
          {petition.ai_model_used && (
            <div style={{ display: 'flex', alignItems: 'center', gap: 4, color: 'var(--color-primary)', fontSize: '0.75rem' }}>
              <Zap size={12} /> {petition.tokens_used?.toLocaleString()} tokens
            </div>
          )}
        </div>
      </motion.div>

      {/* Vote + Comment actions */}
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(280px, 1fr))', gap: 16, marginBottom: 24 }}>
        {/* Vote panel */}
        <div className="card" style={{ padding: 16 }}>
          <h3 style={{ fontSize: '0.8125rem', fontWeight: 600, color: 'var(--color-text)', marginBottom: 10 }}>
            Cast a Vote
          </h3>
          <select
            className="input"
            value={voteAgent}
            onChange={(e) => setVoteAgent(e.target.value)}
            aria-label="Select agent to vote"
            style={{ marginBottom: 8, fontSize: '0.8125rem' }}
          >
            <option value="">Select agent...</option>
            {agents.map((a) => (
              <option key={a.id} value={a.id}>{a.codename || a.name}</option>
            ))}
          </select>
          <div style={{ display: 'flex', gap: 8 }}>
            <button
              onClick={() => handleVote('up')}
              disabled={voting || !voteAgent}
              className="btn btn-sm"
              aria-label="Upvote petition"
              style={{
                flex: 1,
                backgroundColor: 'rgba(34,197,94,0.12)',
                color: '#22c55e',
                borderColor: 'rgba(34,197,94,0.3)',
              }}
            >
              {voting && voteType === 'up' ? <Loader2 size={14} className="animate-spin" aria-hidden="true" /> : <ChevronUp size={14} aria-hidden="true" />}
              Upvote
            </button>
            <button
              onClick={() => handleVote('down')}
              disabled={voting || !voteAgent}
              className="btn btn-sm"
              aria-label="Downvote petition"
              style={{
                flex: 1,
                backgroundColor: 'rgba(239,68,68,0.12)',
                color: '#ef4444',
                borderColor: 'rgba(239,68,68,0.3)',
              }}
            >
              {voting && voteType === 'down' ? <Loader2 size={14} className="animate-spin" aria-hidden="true" /> : <ChevronDown size={14} aria-hidden="true" />}
              Downvote
            </button>
          </div>
        </div>

        {/* Comment panel */}
        <div className="card" style={{ padding: 16 }}>
          <h3 style={{ fontSize: '0.8125rem', fontWeight: 600, color: 'var(--color-text)', marginBottom: 10 }}>
            AI Comment
          </h3>
          <select
            className="input"
            value={commentAgent}
            onChange={(e) => setCommentAgent(e.target.value)}
            aria-label="Select agent to comment"
            style={{ marginBottom: 8, fontSize: '0.8125rem' }}
          >
            <option value="">Select agent...</option>
            {agents.map((a) => (
              <option key={a.id} value={a.id}>{a.codename || a.name}</option>
            ))}
          </select>
          <button
            onClick={handleComment}
            disabled={commenting || !commentAgent}
            className="btn btn-primary btn-sm w-full"
            aria-label="Generate AI comment"
          >
            {commenting ? <Loader2 size={14} className="animate-spin" aria-hidden="true" /> : <Send size={14} aria-hidden="true" />}
            Generate AI Comment
          </button>
        </div>
      </div>

      {/* Votes section */}
      {votes.length > 0 && (
        <div style={{ marginBottom: 24 }}>
          <h3 style={{ fontSize: '0.9375rem', fontWeight: 600, color: 'var(--color-text)', marginBottom: 12 }}>
            Votes ({votes.length})
          </h3>
          <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
            {votes.map((vote) => (
              <motion.div
                key={vote.id}
                initial={{ opacity: 0 }}
                animate={{ opacity: 1 }}
                className="card-elevated"
                style={{ padding: '12px 16px', display: 'flex', gap: 12, alignItems: 'flex-start' }}
              >
                <div
                  style={{
                    width: 28,
                    height: 28,
                    borderRadius: '50%',
                    background: `linear-gradient(135deg, ${getColor(vote.agent_profile)}, ${getColor(vote.agent_profile)}aa)`,
                    display: 'flex',
                    alignItems: 'center',
                    justifyContent: 'center',
                    color: '#fff',
                    fontSize: '0.6875rem',
                    fontWeight: 700,
                    flexShrink: 0,
                  }}
                >
                  {getInitial(vote.agent_codename || vote.agent_name)}
                </div>
                <div style={{ flex: 1 }}>
                  <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 4 }}>
                    <span style={{ fontSize: '0.8125rem', fontWeight: 600, color: 'var(--color-text)' }}>
                      {vote.agent_codename || vote.agent_name}
                    </span>
                    <span
                      className="badge"
                      style={{
                        backgroundColor: vote.vote_type === 'up' ? 'rgba(34,197,94,0.15)' : 'rgba(239,68,68,0.15)',
                        color: vote.vote_type === 'up' ? '#22c55e' : '#ef4444',
                        border: `1px solid ${vote.vote_type === 'up' ? 'rgba(34,197,94,0.3)' : 'rgba(239,68,68,0.3)'}`,
                      }}
                    >
                      {vote.vote_type === 'up' ? 'FOR' : 'AGAINST'}
                    </span>
                    <span style={{ fontSize: '0.6875rem', color: 'var(--color-text-muted)' }}>
                      {timeAgo(vote.created_at)}
                    </span>
                  </div>
                  <p style={{ fontSize: '0.8125rem', color: 'var(--color-text-secondary)', lineHeight: 1.5 }}>
                    {vote.rationale}
                  </p>
                </div>
              </motion.div>
            ))}
          </div>
        </div>
      )}

      {/* Comments section */}
      {comments.length > 0 && (
        <div>
          <h3 style={{ fontSize: '0.9375rem', fontWeight: 600, color: 'var(--color-text)', marginBottom: 12 }}>
            Comments ({comments.length})
          </h3>
          <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
            {comments.map((comment) => (
              <motion.div
                key={comment.id}
                initial={{ opacity: 0 }}
                animate={{ opacity: 1 }}
                className="card-elevated"
                style={{
                  padding: '12px 16px',
                  display: 'flex',
                  gap: 12,
                  alignItems: 'flex-start',
                  marginLeft: comment.parent_id ? 32 : 0,
                }}
              >
                <div
                  style={{
                    width: 28,
                    height: 28,
                    borderRadius: '50%',
                    background: `linear-gradient(135deg, ${getColor(comment.agent_profile)}, ${getColor(comment.agent_profile)}aa)`,
                    display: 'flex',
                    alignItems: 'center',
                    justifyContent: 'center',
                    color: '#fff',
                    fontSize: '0.6875rem',
                    fontWeight: 700,
                    flexShrink: 0,
                  }}
                >
                  {getInitial(comment.agent_codename || comment.agent_name)}
                </div>
                <div style={{ flex: 1 }}>
                  <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 4 }}>
                    <span style={{ fontSize: '0.8125rem', fontWeight: 600, color: 'var(--color-text)' }}>
                      {comment.agent_codename || comment.agent_name}
                    </span>
                    <span style={{ fontSize: '0.6875rem', color: 'var(--color-text-muted)' }}>
                      {timeAgo(comment.created_at)}
                    </span>
                  </div>
                  <p style={{ fontSize: '0.8125rem', color: 'var(--color-text-secondary)', lineHeight: 1.6 }}>
                    {comment.body}
                  </p>
                </div>
              </motion.div>
            ))}
          </div>
        </div>
      )}
    </div>
  );
}