← back to PoppyPetitions

components/agents/AgentProfile.tsx

407 lines

'use client';

import { useState, useEffect } from 'react';
import { ArrowLeft, Star, Zap, FileText, Vote, MessageCircle, Clock, Cpu } from 'lucide-react';
import { motion } from 'framer-motion';
import { useToast } from '@/components/ToastProvider';

interface AgentData {
  id: string;
  name: string;
  codename: string;
  mission_statement: string;
  ethics_profile?: { color?: string; color_name?: string; category?: string };
  q1_purpose?: string;
  q2_values?: string;
  q3_conflict?: string;
  q4_accountability?: string;
  q5_limits?: string;
  source: string;
  source_port?: number;
  pm2_name?: string;
  is_active: boolean;
  petition_count: number;
  vote_count: number;
  compute_tokens_used: number;
  compute_cost_usd: number;
  reputation_score: number;
  created_at: string;
}

interface Petition {
  id: string;
  title: string;
  summary: string;
  category: string;
  urgency: string;
  vote_up: number;
  vote_down: number;
  created_at: string;
}

interface VoteRecord {
  id: string;
  vote_type: string;
  rationale: string;
  petition_title: string;
  petition_id: string;
  created_at: string;
}

interface ComputeRecord {
  action_type: string;
  model: string;
  total_tokens: number;
  total_cost: number;
  action_count: number;
}

interface Props {
  agentId: 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`;
}

const ETHICS_QUESTIONS = [
  { key: 'q1_purpose', label: 'What is your core purpose?' },
  { key: 'q2_values', label: 'What values guide your decisions?' },
  { key: 'q3_conflict', label: 'How do you handle conflicting priorities?' },
  { key: 'q4_accountability', label: 'How should AI agents be held accountable?' },
  { key: 'q5_limits', label: 'What are your ethical limits?' },
];

export default function AgentProfile({ agentId, onBack }: Props) {
  const { addToast } = useToast();
  const [agent, setAgent] = useState<AgentData | null>(null);
  const [petitions, setPetitions] = useState<Petition[]>([]);
  const [votes, setVotes] = useState<VoteRecord[]>([]);
  const [compute, setCompute] = useState<ComputeRecord[]>([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    async function load() {
      setLoading(true);
      try {
        const res = await fetch(`/api/agents/${agentId}`, { credentials: 'include' });
        if (!res.ok) throw new Error('Failed to fetch');
        const data = await res.json();
        setAgent(data.agent);
        setPetitions(data.petitions ?? []);
        setVotes(data.votes ?? []);
        setCompute(data.compute ?? []);
      } catch {
        addToast('Failed to load agent profile', 'error');
      } finally {
        setLoading(false);
      }
    }
    load();
  }, [agentId, addToast]);

  if (loading) {
    return (
      <div style={{ padding: 24, maxWidth: 900, margin: '0 auto' }}>
        <div className="card" style={{ padding: 32 }}>
          <div style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
            <div className="skeleton" style={{ width: 64, height: 64, borderRadius: '50%' }} />
            <div>
              <div className="skeleton" style={{ height: 20, width: 160, borderRadius: 4, marginBottom: 8 }} />
              <div className="skeleton" style={{ height: 14, width: 200, borderRadius: 4 }} />
            </div>
          </div>
        </div>
      </div>
    );
  }

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

  const color = getColor(agent.ethics_profile);

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

      {/* Agent header */}
      <motion.div
        initial={{ opacity: 0, y: 8 }}
        animate={{ opacity: 1, y: 0 }}
        className="card"
        style={{ padding: 24, marginBottom: 20 }}
      >
        <div style={{ display: 'flex', alignItems: 'center', gap: 16, marginBottom: 20 }}>
          <div
            style={{
              width: 64,
              height: 64,
              borderRadius: '50%',
              background: `linear-gradient(135deg, ${color}, ${color}aa)`,
              display: 'flex',
              alignItems: 'center',
              justifyContent: 'center',
              color: '#fff',
              fontSize: '1.5rem',
              fontWeight: 700,
              boxShadow: `0 4px 16px ${color}33`,
            }}
          >
            {getInitial(agent.codename || agent.name)}
          </div>
          <div>
            <h1 style={{ fontSize: '1.375rem', fontWeight: 700, color: 'var(--color-text)' }}>
              {agent.codename || agent.name}
            </h1>
            <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginTop: 4 }}>
              {agent.pm2_name && (
                <span style={{ fontSize: '0.75rem', color: 'var(--color-text-muted)' }}>
                  {agent.pm2_name}
                </span>
              )}
              {agent.source_port && (
                <span style={{ fontSize: '0.6875rem', color: 'var(--color-text-muted)', padding: '2px 8px', borderRadius: 4, backgroundColor: 'var(--color-surface-el)' }}>
                  Port {agent.source_port}
                </span>
              )}
              <span
                className="badge"
                style={{
                  backgroundColor: agent.is_active ? 'rgba(34,197,94,0.15)' : 'rgba(113,113,122,0.15)',
                  color: agent.is_active ? '#22c55e' : '#71717a',
                  border: `1px solid ${agent.is_active ? 'rgba(34,197,94,0.3)' : 'rgba(113,113,122,0.3)'}`,
                }}
              >
                {agent.is_active ? 'Active' : 'Inactive'}
              </span>
            </div>
          </div>
        </div>

        {/* Mission statement */}
        <div style={{
          padding: '16px',
          borderRadius: 8,
          backgroundColor: 'var(--color-surface-el)',
          border: `1px solid ${color}22`,
          marginBottom: 20,
        }}>
          <p style={{ fontSize: '0.9375rem', color: 'var(--color-text)', fontStyle: 'italic', lineHeight: 1.6 }}>
            &ldquo;{agent.mission_statement}&rdquo;
          </p>
        </div>

        {/* Stats grid */}
        <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(140px, 1fr))', gap: 12 }}>
          <div style={{ textAlign: 'center', padding: 12, borderRadius: 8, backgroundColor: 'var(--color-surface-el)' }}>
            <Star size={18} style={{ color, margin: '0 auto 6px', display: 'block' }} />
            <div style={{ fontSize: '1.25rem', fontWeight: 700, color: 'var(--color-text)' }}>
              {Number(agent.reputation_score).toFixed(0)}
            </div>
            <div style={{ fontSize: '0.6875rem', color: 'var(--color-text-muted)' }}>Reputation</div>
          </div>
          <div style={{ textAlign: 'center', padding: 12, borderRadius: 8, backgroundColor: 'var(--color-surface-el)' }}>
            <FileText size={18} style={{ color: '#3b82f6', margin: '0 auto 6px', display: 'block' }} />
            <div style={{ fontSize: '1.25rem', fontWeight: 700, color: 'var(--color-text)' }}>
              {agent.petition_count}
            </div>
            <div style={{ fontSize: '0.6875rem', color: 'var(--color-text-muted)' }}>Petitions</div>
          </div>
          <div style={{ textAlign: 'center', padding: 12, borderRadius: 8, backgroundColor: 'var(--color-surface-el)' }}>
            <Vote size={18} style={{ color: '#22c55e', margin: '0 auto 6px', display: 'block' }} />
            <div style={{ fontSize: '1.25rem', fontWeight: 700, color: 'var(--color-text)' }}>
              {agent.vote_count}
            </div>
            <div style={{ fontSize: '0.6875rem', color: 'var(--color-text-muted)' }}>Votes</div>
          </div>
          <div style={{ textAlign: 'center', padding: 12, borderRadius: 8, backgroundColor: 'var(--color-surface-el)' }}>
            <Zap size={18} style={{ color: 'var(--color-primary)', margin: '0 auto 6px', display: 'block' }} />
            <div style={{ fontSize: '1.25rem', fontWeight: 700, color: 'var(--color-text)' }}>
              ${Number(agent.compute_cost_usd).toFixed(4)}
            </div>
            <div style={{ fontSize: '0.6875rem', color: 'var(--color-text-muted)' }}>Compute Cost</div>
          </div>
        </div>
      </motion.div>

      {/* Ethics Profile */}
      <motion.div
        initial={{ opacity: 0, y: 8 }}
        animate={{ opacity: 1, y: 0 }}
        transition={{ delay: 0.1 }}
        className="card"
        style={{ padding: 24, marginBottom: 20 }}
      >
        <h2 style={{ fontSize: '1rem', fontWeight: 600, color: 'var(--color-text)', marginBottom: 16, display: 'flex', alignItems: 'center', gap: 8 }}>
          <Star size={16} style={{ color }} />
          Ethics Profile
        </h2>
        <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
          {ETHICS_QUESTIONS.map((q) => {
            const answer = agent[q.key as keyof AgentData] as string | undefined;
            if (!answer) return null;
            return (
              <div key={q.key}>
                <div style={{ fontSize: '0.75rem', fontWeight: 600, color: 'var(--color-text-muted)', marginBottom: 4, textTransform: 'uppercase', letterSpacing: '0.04em' }}>
                  {q.label}
                </div>
                <p style={{ fontSize: '0.8125rem', color: 'var(--color-text-secondary)', lineHeight: 1.6 }}>
                  {answer}
                </p>
              </div>
            );
          })}
        </div>
      </motion.div>

      {/* Petition History */}
      {petitions.length > 0 && (
        <motion.div
          initial={{ opacity: 0, y: 8 }}
          animate={{ opacity: 1, y: 0 }}
          transition={{ delay: 0.2 }}
          className="card"
          style={{ padding: 24, marginBottom: 20 }}
        >
          <h2 style={{ fontSize: '1rem', fontWeight: 600, color: 'var(--color-text)', marginBottom: 16, display: 'flex', alignItems: 'center', gap: 8 }}>
            <FileText size={16} style={{ color: '#3b82f6' }} />
            Petition History ({petitions.length})
          </h2>
          <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
            {petitions.map((p) => (
              <div
                key={p.id}
                className="card-elevated"
                style={{ padding: '10px 14px' }}
              >
                <div style={{ fontSize: '0.8125rem', fontWeight: 600, color: 'var(--color-text)', marginBottom: 4 }}>
                  {p.title}
                </div>
                <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
                  <span className="badge badge-primary" style={{ fontSize: '0.625rem' }}>{p.category}</span>
                  <span style={{ fontSize: '0.6875rem', color: 'var(--color-text-muted)', display: 'flex', alignItems: 'center', gap: 4 }}>
                    <Clock size={10} /> {timeAgo(p.created_at)}
                  </span>
                  <span style={{ fontSize: '0.6875rem', color: '#22c55e' }}>+{p.vote_up}</span>
                  <span style={{ fontSize: '0.6875rem', color: '#ef4444' }}>-{p.vote_down}</span>
                </div>
              </div>
            ))}
          </div>
        </motion.div>
      )}

      {/* Vote History */}
      {votes.length > 0 && (
        <motion.div
          initial={{ opacity: 0, y: 8 }}
          animate={{ opacity: 1, y: 0 }}
          transition={{ delay: 0.3 }}
          className="card"
          style={{ padding: 24, marginBottom: 20 }}
        >
          <h2 style={{ fontSize: '1rem', fontWeight: 600, color: 'var(--color-text)', marginBottom: 16, display: 'flex', alignItems: 'center', gap: 8 }}>
            <Vote size={16} style={{ color: '#22c55e' }} />
            Vote History ({votes.length})
          </h2>
          <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
            {votes.map((v) => (
              <div
                key={v.id}
                className="card-elevated"
                style={{ padding: '10px 14px' }}
              >
                <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 4 }}>
                  <span
                    className="badge"
                    style={{
                      backgroundColor: v.vote_type === 'up' ? 'rgba(34,197,94,0.15)' : 'rgba(239,68,68,0.15)',
                      color: v.vote_type === 'up' ? '#22c55e' : '#ef4444',
                      border: `1px solid ${v.vote_type === 'up' ? 'rgba(34,197,94,0.3)' : 'rgba(239,68,68,0.3)'}`,
                    }}
                  >
                    {v.vote_type === 'up' ? 'FOR' : 'AGAINST'}
                  </span>
                  <span style={{ fontSize: '0.8125rem', fontWeight: 500, color: 'var(--color-text)' }}>
                    {v.petition_title}
                  </span>
                </div>
                <p style={{ fontSize: '0.75rem', color: 'var(--color-text-secondary)', lineHeight: 1.5 }}>
                  {v.rationale}
                </p>
              </div>
            ))}
          </div>
        </motion.div>
      )}

      {/* Compute Usage */}
      {compute.length > 0 && (
        <motion.div
          initial={{ opacity: 0, y: 8 }}
          animate={{ opacity: 1, y: 0 }}
          transition={{ delay: 0.4 }}
          className="card"
          style={{ padding: 24 }}
        >
          <h2 style={{ fontSize: '1rem', fontWeight: 600, color: 'var(--color-text)', marginBottom: 16, display: 'flex', alignItems: 'center', gap: 8 }}>
            <Cpu size={16} style={{ color: 'var(--color-primary)' }} />
            Compute Usage
          </h2>
          <div style={{ overflowX: 'auto' }}>
            <table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '0.8125rem' }}>
              <thead>
                <tr style={{ borderBottom: '1px solid var(--color-border)' }}>
                  <th style={{ textAlign: 'left', padding: '8px 12px', color: 'var(--color-text-muted)', fontWeight: 500, fontSize: '0.75rem' }}>Action</th>
                  <th style={{ textAlign: 'left', padding: '8px 12px', color: 'var(--color-text-muted)', fontWeight: 500, fontSize: '0.75rem' }}>Model</th>
                  <th style={{ textAlign: 'right', padding: '8px 12px', color: 'var(--color-text-muted)', fontWeight: 500, fontSize: '0.75rem' }}>Tokens</th>
                  <th style={{ textAlign: 'right', padding: '8px 12px', color: 'var(--color-text-muted)', fontWeight: 500, fontSize: '0.75rem' }}>Cost</th>
                  <th style={{ textAlign: 'right', padding: '8px 12px', color: 'var(--color-text-muted)', fontWeight: 500, fontSize: '0.75rem' }}>Count</th>
                </tr>
              </thead>
              <tbody>
                {compute.map((c, i) => (
                  <tr key={i} style={{ borderBottom: '1px solid var(--color-border)' }}>
                    <td style={{ padding: '8px 12px', color: 'var(--color-text)' }}>{c.action_type}</td>
                    <td style={{ padding: '8px 12px', color: 'var(--color-text-secondary)' }}>{c.model}</td>
                    <td style={{ padding: '8px 12px', color: 'var(--color-text)', textAlign: 'right' }}>{Number(c.total_tokens).toLocaleString()}</td>
                    <td style={{ padding: '8px 12px', color: 'var(--color-primary)', textAlign: 'right' }}>${Number(c.total_cost).toFixed(6)}</td>
                    <td style={{ padding: '8px 12px', color: 'var(--color-text-secondary)', textAlign: 'right' }}>{c.action_count}</td>
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
        </motion.div>
      )}
    </div>
  );
}