| import React, { useState, useEffect } from 'react'; |
| import { supabase } from '../services/supabase'; |
| import { MessageSquare, Play, CheckCircle2, AlertCircle } from 'lucide-react'; |
| import { motion } from 'framer-motion'; |
| import { getApiUrl } from '../services/runtimeConfig'; |
|
|
| interface DebateAgent { |
| id: string; |
| name: string; |
| model: string; |
| } |
|
|
| interface DebateTask { |
| id: string; |
| title: string; |
| status: string; |
| } |
|
|
| const renderContent = (content: any) => { |
| if (!content) return null; |
| if (typeof content === 'string') return content; |
| |
| if (Array.isArray(content) && content.length > 0 && typeof content[0] === 'object' && !Array.isArray(content[0])) { |
| const keys = Object.keys(content[0]); |
| const isTableCandidate = content.every(item => |
| item && typeof item === 'object' && |
| Object.keys(item).length === keys.length && |
| keys.every(k => Object.keys(item).includes(k)) |
| ); |
|
|
| if (isTableCandidate && keys.length <= 6) { |
| return ( |
| <div style={{ overflowX: 'auto', marginBottom: '16px', borderRadius: 'var(--radius-md)', border: '1px solid rgba(255,255,255,0.05)' }}> |
| <table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '0.8rem' }}> |
| <thead> |
| <tr style={{ background: 'rgba(110, 89, 255, 0.1)', borderBottom: '2px solid rgba(110, 89, 255, 0.2)' }}> |
| {keys.map(k => ( |
| <th key={k} style={{ textAlign: 'left', padding: '12px 8px', color: 'var(--accent)', textTransform: 'uppercase', letterSpacing: '1px', fontWeight: 700 }}> |
| {k.replace(/_/g, ' ')} |
| </th> |
| ))} |
| </tr> |
| </thead> |
| <tbody> |
| {content.map((item, i) => ( |
| <tr key={i} style={{ borderBottom: '1px solid rgba(255,255,255,0.05)', background: i % 2 === 0 ? 'transparent' : 'rgba(255,255,255,0.02)' }}> |
| {keys.map(k => ( |
| <td key={k} style={{ padding: '10px 8px', color: 'rgba(255,255,255,0.9)' }}> |
| {typeof item[k] === 'object' ? JSON.stringify(item[k]) : String(item[k])} |
| </td> |
| ))} |
| </tr> |
| ))} |
| </tbody> |
| </table> |
| </div> |
| ); |
| } |
| } |
|
|
| if (Array.isArray(content)) { |
| return ( |
| <ul style={{ paddingLeft: '20px', margin: 0 }}> |
| {content.map((item, i) => ( |
| <li key={i} style={{ marginBottom: '8px' }}> |
| {typeof item === 'object' ? renderContent(item) : String(item)} |
| </li> |
| ))} |
| </ul> |
| ); |
| } |
|
|
| if (typeof content === 'object') { |
| return ( |
| <div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}> |
| {Object.entries(content).map(([key, value]) => ( |
| <div key={key}> |
| <div style={{ |
| fontWeight: 700, |
| color: 'var(--accent)', |
| fontSize: '0.7rem', |
| textTransform: 'uppercase', |
| letterSpacing: '1px', |
| marginBottom: '6px', |
| opacity: 0.8 |
| }}> |
| {key.replace(/_/g, ' ').replace(/-/g, ' ')} |
| </div> |
| <div style={{ |
| paddingLeft: '12px', |
| borderLeft: '2px solid rgba(110, 89, 255, 0.3)', |
| color: 'rgba(255,255,255,0.9)', |
| lineHeight: '1.5' |
| }}> |
| {typeof value === 'object' ? renderContent(value) : String(value)} |
| </div> |
| </div> |
| ))} |
| </div> |
| ); |
| } |
| return String(content); |
| }; |
|
|
| const DebateView: React.FC = () => { |
| const [agents, setAgents] = useState<DebateAgent[]>([]); |
| const [tasks, setTasks] = useState<DebateTask[]>([]); |
| const [selectedTask, setSelectedTask] = useState(''); |
| const [agentA, setAgentA] = useState(''); |
| const [agentB, setAgentB] = useState(''); |
| const [loading, setLoading] = useState(false); |
| const [status, setStatus] = useState<string | null>(null); |
| const [debateResult, setDebateResult] = useState<any>(null); |
|
|
| useEffect(() => { |
| const fetchData = async () => { |
| const { data: agentsData } = await supabase.from('agents').select('id,name,model'); |
| const { data: tasksData } = await supabase.from('tasks') |
| .select('id,title,status') |
| .in('status', ['todo', 'awaiting_approval']); |
| if (agentsData) setAgents(agentsData); |
| if (tasksData) setTasks(tasksData); |
| }; |
| fetchData(); |
| }, []); |
|
|
| useEffect(() => { |
| let interval: number; |
| if (loading && selectedTask) { |
| interval = window.setInterval(async () => { |
| const { data } = await supabase.from('tasks').select('status, output_data').eq('id', selectedTask).single(); |
| if (data && data.status !== 'in_progress') { |
| setLoading(false); |
| setStatus(data.status === 'awaiting_approval' ? 'Debate completed successfully!' : `Debate finished with status: ${data.status}`); |
| |
| if (data.status === 'awaiting_approval' && data.output_data?.debate_history) { |
| setDebateResult(data.output_data.debate_history); |
| } |
| |
| window.clearInterval(interval); |
| } |
| }, 3000); |
| } |
| return () => window.clearInterval(interval); |
| }, [loading, selectedTask]); |
|
|
| const handleStartDebate = async () => { |
| if (!selectedTask || !agentA || !agentB) { |
| alert('Please select a task and two different agents.'); |
| return; |
| } |
| if (agentA === agentB) { |
| alert('Agents must be different for a debate.'); |
| return; |
| } |
|
|
| setDebateResult(null); |
| setLoading(true); |
| setStatus('Initializing debate flow...'); |
| |
| try { |
| const response = await fetch(`${getApiUrl()}/orchestrator/debate`, { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ |
| task_id: selectedTask, |
| agent_a_id: agentA, |
| agent_b_id: agentB |
| }) |
| }); |
|
|
| if (response.ok) { |
| setStatus('Debate started! Monitor the agent console for progress.'); |
| |
| } else { |
| setStatus('Failed to start debate.'); |
| setLoading(false); |
| } |
| } catch { |
| setStatus('Error connecting to backend.'); |
| setLoading(false); |
| } |
| }; |
|
|
| return ( |
| <motion.div |
| initial={{ opacity: 0, y: 20 }} |
| animate={{ opacity: 1, y: 0 }} |
| className="glass-panel" |
| style={{ |
| width: '100%', |
| maxWidth: debateResult ? '1000px' : '600px', |
| margin: '0 auto', |
| padding: 'var(--space-xl)', |
| transition: 'max-width 0.5s ease-in-out' |
| }} |
| > |
| <div className="panel-heading"> |
| <MessageSquare size={32} color="var(--accent)" /> |
| <div> |
| <h2 style={{ fontSize: '1.5rem' }}>Multi-Agent Debate</h2> |
| <p style={{ color: 'var(--text-dim)', fontSize: '0.9rem' }}>Two agents collaborate to refine a task's output.</p> |
| </div> |
| </div> |
| |
| <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--space-md)' }}> |
| <div> |
| <label style={{ display: 'block', marginBottom: 'var(--space-xs)', fontWeight: 600 }}>Select Task</label> |
| <select |
| value={selectedTask} |
| onChange={(e) => setSelectedTask(e.target.value)} |
| style={{ width: '100%', padding: '0.8rem', background: 'rgba(255,255,255,0.05)', border: '1px solid var(--glass-border)', borderRadius: 'var(--radius-md)', color: 'white' }} |
| > |
| <option value="">-- Choose a pending task --</option> |
| {tasks.map(t => <option key={t.id} value={t.id}>{t.title} ({t.status.replace('_', ' ')})</option>)} |
| </select> |
| </div> |
| |
| <div className="responsive-two-col"> |
| <div> |
| <label style={{ display: 'block', marginBottom: 'var(--space-xs)', fontWeight: 600 }}>Agent A (Generator)</label> |
| <select |
| value={agentA} |
| onChange={(e) => setAgentA(e.target.value)} |
| style={{ width: '100%', padding: '0.8rem', background: 'rgba(255,255,255,0.05)', border: '1px solid var(--glass-border)', borderRadius: 'var(--radius-md)', color: 'white' }} |
| > |
| <option value="">-- Select Agent --</option> |
| {agents.map(a => <option key={a.id} value={a.id}>{a.name} ({a.model})</option>)} |
| </select> |
| </div> |
| <div> |
| <label style={{ display: 'block', marginBottom: 'var(--space-xs)', fontWeight: 600 }}>Agent B (Critique)</label> |
| <select |
| value={agentB} |
| onChange={(e) => setAgentB(e.target.value)} |
| style={{ width: '100%', padding: '0.8rem', background: 'rgba(255,255,255,0.05)', border: '1px solid var(--glass-border)', borderRadius: 'var(--radius-md)', color: 'white' }} |
| > |
| <option value="">-- Select Agent --</option> |
| {agents.map(a => <option key={a.id} value={a.id}>{a.name} ({a.model})</option>)} |
| </select> |
| </div> |
| </div> |
| |
| {status && ( |
| <div style={{ display: 'flex', alignItems: 'center', gap: 'var(--space-sm)', padding: 'var(--space-sm)', background: 'rgba(255,255,255,0.05)', borderRadius: 'var(--radius-sm)', fontSize: '0.9rem' }}> |
| {status.includes('Error') || status.includes('Failed') ? <AlertCircle size={16} color="var(--danger)" /> : <CheckCircle2 size={16} color="var(--success)" />} |
| <span>{status}</span> |
| </div> |
| )} |
| |
| <button |
| className="btn btn-primary" |
| onClick={handleStartDebate} |
| disabled={loading} |
| style={{ width: '100%', padding: '1rem', marginTop: 'var(--space-md)' }} |
| > |
| <Play size={18} fill="white" /> |
| {loading ? 'Processing Debate...' : 'Execute Debate Flow'} |
| </button> |
| |
| {debateResult && ( |
| <motion.div |
| initial={{ opacity: 0, height: 0 }} |
| animate={{ opacity: 1, height: 'auto' }} |
| style={{ marginTop: 'var(--space-xl)', borderTop: '1px solid var(--glass-border)', paddingTop: 'var(--space-lg)' }} |
| > |
| <div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: 'var(--space-md)' }}> |
| <CheckCircle2 size={20} color="var(--success)" /> |
| <h3 style={{ fontSize: '1.1rem', margin: 0 }}>Debate Results: Before & After</h3> |
| </div> |
| |
| <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 'var(--space-lg)' }}> |
| <div className="glass-panel" style={{ padding: 'var(--space-md)', background: 'rgba(255,255,255,0.02)' }}> |
| <div style={{ fontSize: '0.7rem', color: 'var(--text-dim)', textTransform: 'uppercase', marginBottom: '12px', letterSpacing: '1px' }}>Initial Proposal</div> |
| <div style={{ fontSize: '0.9rem', color: 'rgba(255,255,255,0.8)', maxHeight: '500px', overflowY: 'auto' }}> |
| {renderContent(debateResult.initial)} |
| </div> |
| </div> |
| |
| <div className="glass-panel" style={{ padding: 'var(--space-md)', background: 'rgba(110, 89, 255, 0.05)', border: '1px solid rgba(110, 89, 255, 0.2)' }}> |
| <div style={{ fontSize: '0.7rem', color: 'var(--accent)', textTransform: 'uppercase', marginBottom: '12px', letterSpacing: '1px' }}>Refined Final Result</div> |
| <div style={{ fontSize: '0.9rem', color: 'white', maxHeight: '500px', overflowY: 'auto' }}> |
| {renderContent(debateResult.final)} |
| </div> |
| </div> |
| </div> |
| |
| {debateResult.critique && ( |
| <div style={{ marginTop: 'var(--space-md)', padding: 'var(--space-md)', background: 'rgba(255, 107, 107, 0.05)', borderRadius: 'var(--radius-md)', border: '1px solid rgba(255, 107, 107, 0.1)' }}> |
| <div style={{ fontSize: '0.7rem', color: 'var(--danger)', textTransform: 'uppercase', marginBottom: '8px', letterSpacing: '1px' }}>Critique Context</div> |
| <div style={{ fontSize: '0.85rem', color: 'var(--text-dim)', fontStyle: 'italic' }}> |
| {renderContent(debateResult.critique)} |
| </div> |
| </div> |
| )} |
| </motion.div> |
| )} |
| </div> |
| </motion.div> |
| ); |
| }; |
|
|
| export default DebateView; |
|
|