'use client'; import React, { useEffect, useState, useRef } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import type { AgentOutput, AgentStatus } from '../../lib/types'; interface AgentCardProps { output: AgentOutput; index: number; } const AGENT_CONFIG = { architect: { num: '01', title: 'ARCHITECT', subtitle: 'System Design Agent', color: '#00D4FF', glow: 'rgba(0,212,255,0.3)', subtle: 'rgba(0,212,255,0.06)', icon: '⬡', persona: 'Senior Solutions Architect', }, critic: { num: '02', title: 'CRITIC', subtitle: 'SRE Review Agent', color: '#FFB800', glow: 'rgba(255,184,0,0.3)', subtle: 'rgba(255,184,0,0.06)', icon: '◈', persona: 'Principal Site Reliability Engineer', }, refiner: { num: '03', title: 'REFINER', subtitle: 'Self-Correction Agent', color: '#00FF9C', glow: 'rgba(0,255,156,0.3)', subtle: 'rgba(0,255,156,0.06)', icon: '◆', persona: 'Staff Platform Engineer', }, }; const STATUS_LABELS: Record = { idle: 'STANDBY', thinking: 'PROCESSING', complete: 'COMPLETE', error: 'ERROR', }; // ─── Inline markdown renderer ──────────────────────────────────────────────── function renderInline(text: string, color: string): React.ReactNode { const parts = text.split(/(\*\*[^*]+\*\*|`[^`]+`)/g); return parts.map((part, i) => { if (part.startsWith('**') && part.endsWith('**')) { return {part.slice(2, -2)}; } if (part.startsWith('`') && part.endsWith('`')) { return ( {part.slice(1, -1)} ); } return part; }); } function MarkdownDisplay({ content, color }: { content: string; color: string }) { return (
{content.split('\n').map((line, i) => { if (line.startsWith('# ')) { return (
0 ? '1rem' : 0, letterSpacing: '0.05em' }}> {line.replace(/^# /, '')}
); } if (line.startsWith('## ')) { return (
{line.replace(/^## /, '')}
); } if (line.startsWith('### ')) { return (
{line.replace(/^### /, '')}
); } if (line.startsWith('- ') || line.startsWith('* ')) { return (
{renderInline(line.replace(/^[-*] /, ''), color)}
); } if (line === '') return
; return
{renderInline(line, color)}
; })}
); } function ThinkingIndicator({ color }: { color: string }) { return (
{[0, 1, 2, 3, 4].map((i) => ( ))} Agent reasoning...
{[70, 55, 85, 45, 65].map((w, i) => ( ))}
); } // ─── Main component ─────────────────────────────────────────────────────────── export default function AgentCard({ output, index }: AgentCardProps) { const config = AGENT_CONFIG[output.agent]; const [displayedContent, setDisplayedContent] = useState(''); const streamRef = useRef(null); useEffect(() => { if (output.status !== 'complete' || !output.content) { setDisplayedContent(''); return; } // Reset and start streaming setDisplayedContent(''); let char = 0; const full = output.content; streamRef.current = setInterval(() => { char += 8; setDisplayedContent(full.slice(0, char)); if (char >= full.length) { setDisplayedContent(full); if (streamRef.current) clearInterval(streamRef.current); } }, 10); return () => { if (streamRef.current) clearInterval(streamRef.current); }; }, [output.status, output.content]); const isThinking = output.status === 'thinking'; const isComplete = output.status === 'complete'; return ( {/* Scan line when thinking */} {isThinking && ( )} {/* Header */}
{config.num} {config.icon}
{config.title}
{config.persona}
{/* Status badge */}
{output.duration && isComplete && ( {(output.duration / 1000).toFixed(1)}s )}
{STATUS_LABELS[output.status]}
{/* Body */}
{isThinking && ( )} {isComplete && ( )} {output.status === 'idle' && (
{config.icon}
AWAITING_ACTIVATION
)} {output.status === 'error' && (
ERROR
{output.content}
)}
); }