Spaces:
Paused
Paused
| /** | |
| * βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| * β AGENT PROCESS TRACKER β | |
| * βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| * β Real-time visualization of agent task execution β | |
| * β Part of the Liquid UI Arsenal β | |
| * βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| */ | |
| import { useState, useEffect } from 'react'; | |
| import { Badge } from '@/components/ui/badge'; | |
| import { Progress } from '@/components/ui/progress'; | |
| import { cn } from '@/lib/utils'; | |
| import { | |
| Bot, CheckCircle, Clock, AlertTriangle, | |
| Loader2, ChevronRight, Terminal, Sparkles, | |
| Cpu, Zap | |
| } from 'lucide-react'; | |
| export interface ProcessStep { | |
| id: string; | |
| name: string; | |
| status: 'pending' | 'running' | 'completed' | 'failed' | 'skipped'; | |
| duration_ms?: number; | |
| output?: string; | |
| error?: string; | |
| metadata?: Record<string, unknown>; | |
| } | |
| export interface AgentProcess { | |
| id: string; | |
| agent: string; | |
| task: string; | |
| status: 'queued' | 'running' | 'completed' | 'failed'; | |
| progress: number; | |
| steps: ProcessStep[]; | |
| started_at?: string; | |
| completed_at?: string; | |
| result?: unknown; | |
| } | |
| export interface AgentProcessTrackerProps { | |
| process: AgentProcess; | |
| showSteps?: boolean; | |
| showOutput?: boolean; | |
| compact?: boolean; | |
| } | |
| const agentColors: Record<string, string> = { | |
| claude: 'text-orange-400 bg-orange-500/20', | |
| gemini: 'text-blue-400 bg-blue-500/20', | |
| deepseek: 'text-purple-400 bg-purple-500/20', | |
| sentinel: 'text-cyan-400 bg-cyan-500/20', | |
| muse: 'text-pink-400 bg-pink-500/20', | |
| default: 'text-gray-400 bg-gray-500/20', | |
| }; | |
| const statusIcons = { | |
| pending: Clock, | |
| running: Loader2, | |
| completed: CheckCircle, | |
| failed: AlertTriangle, | |
| skipped: ChevronRight, | |
| queued: Clock, | |
| }; | |
| const statusColors = { | |
| pending: 'text-gray-400', | |
| queued: 'text-gray-400', | |
| running: 'text-yellow-400', | |
| completed: 'text-green-400', | |
| failed: 'text-red-400', | |
| skipped: 'text-gray-500', | |
| }; | |
| export function AgentProcessTracker({ | |
| process, | |
| showSteps = true, | |
| showOutput = true, | |
| compact = false, | |
| }: AgentProcessTrackerProps) { | |
| const [currentTime, setCurrentTime] = useState(Date.now()); | |
| // Update time for running processes | |
| useEffect(() => { | |
| if (process.status === 'running') { | |
| const interval = setInterval(() => setCurrentTime(Date.now()), 100); | |
| return () => clearInterval(interval); | |
| } | |
| }, [process.status]); | |
| const agentColor = agentColors[process.agent.toLowerCase()] || agentColors.default; | |
| const StatusIcon = statusIcons[process.status]; | |
| // Calculate elapsed time | |
| const getElapsed = () => { | |
| if (!process.started_at) return null; | |
| const start = new Date(process.started_at).getTime(); | |
| const end = process.completed_at ? new Date(process.completed_at).getTime() : currentTime; | |
| const ms = end - start; | |
| if (ms < 1000) return `${ms}ms`; | |
| if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`; | |
| return `${(ms / 60000).toFixed(1)}m`; | |
| }; | |
| // Get current step | |
| const currentStep = process.steps.find(s => s.status === 'running'); | |
| const completedSteps = process.steps.filter(s => s.status === 'completed').length; | |
| if (compact) { | |
| return ( | |
| <div className="flex items-center gap-3 p-3 rounded-lg border border-border/30 bg-background/50"> | |
| <div className={cn('p-2 rounded-lg', agentColor)}> | |
| <Bot className="w-4 h-4" /> | |
| </div> | |
| <div className="flex-1 min-w-0"> | |
| <div className="flex items-center gap-2"> | |
| <span className="text-sm font-medium truncate">{process.task}</span> | |
| <StatusIcon className={cn( | |
| 'w-3 h-3', | |
| statusColors[process.status], | |
| process.status === 'running' && 'animate-spin' | |
| )} /> | |
| </div> | |
| <Progress value={process.progress} className="h-1 mt-1" /> | |
| </div> | |
| <span className="text-[10px] text-muted-foreground font-mono"> | |
| {getElapsed()} | |
| </span> | |
| </div> | |
| ); | |
| } | |
| return ( | |
| <div className="rounded-lg border border-border/30 bg-background/50 overflow-hidden"> | |
| {/* Header */} | |
| <div className="flex items-center justify-between p-4 bg-muted/30"> | |
| <div className="flex items-center gap-3"> | |
| <div className={cn('p-2 rounded-lg', agentColor)}> | |
| <Bot className="w-5 h-5" /> | |
| </div> | |
| <div> | |
| <div className="flex items-center gap-2"> | |
| <span className="text-sm font-medium">{process.agent}</span> | |
| <Badge variant="outline" className="text-[9px] font-mono"> | |
| {process.id.slice(0, 8)} | |
| </Badge> | |
| </div> | |
| <p className="text-xs text-muted-foreground">{process.task}</p> | |
| </div> | |
| </div> | |
| <div className="flex items-center gap-3"> | |
| <div className="text-right"> | |
| <div className="flex items-center gap-1"> | |
| <StatusIcon className={cn( | |
| 'w-4 h-4', | |
| statusColors[process.status], | |
| process.status === 'running' && 'animate-spin' | |
| )} /> | |
| <span className={cn('text-xs font-medium', statusColors[process.status])}> | |
| {process.status.toUpperCase()} | |
| </span> | |
| </div> | |
| {getElapsed() && ( | |
| <span className="text-[10px] text-muted-foreground font-mono"> | |
| {getElapsed()} | |
| </span> | |
| )} | |
| </div> | |
| </div> | |
| </div> | |
| {/* Progress bar */} | |
| <div className="px-4 py-2 bg-muted/10"> | |
| <div className="flex items-center justify-between text-[10px] text-muted-foreground mb-1"> | |
| <span>Progress</span> | |
| <span>{process.progress}% ({completedSteps}/{process.steps.length} steps)</span> | |
| </div> | |
| <Progress value={process.progress} className="h-2" /> | |
| </div> | |
| {/* Current step indicator */} | |
| {currentStep && ( | |
| <div className="px-4 py-2 flex items-center gap-2 bg-yellow-500/10 border-y border-yellow-500/20"> | |
| <Cpu className="w-3 h-3 text-yellow-400 animate-pulse" /> | |
| <span className="text-xs text-yellow-400">{currentStep.name}</span> | |
| {currentStep.duration_ms !== undefined && ( | |
| <span className="text-[10px] text-muted-foreground ml-auto font-mono"> | |
| {currentStep.duration_ms}ms | |
| </span> | |
| )} | |
| </div> | |
| )} | |
| {/* Steps list */} | |
| {showSteps && process.steps.length > 0 && ( | |
| <div className="px-4 py-3"> | |
| <span className="text-[10px] text-muted-foreground uppercase tracking-wider"> | |
| Execution Steps | |
| </span> | |
| <div className="mt-2 space-y-1"> | |
| {process.steps.map((step, index) => { | |
| const StepIcon = statusIcons[step.status]; | |
| return ( | |
| <div | |
| key={step.id} | |
| className={cn( | |
| 'flex items-center gap-2 px-2 py-1.5 rounded text-xs', | |
| step.status === 'running' && 'bg-yellow-500/10', | |
| step.status === 'completed' && 'bg-green-500/5', | |
| step.status === 'failed' && 'bg-red-500/10' | |
| )} | |
| > | |
| <span className="text-muted-foreground w-4">{index + 1}.</span> | |
| <StepIcon className={cn( | |
| 'w-3 h-3', | |
| statusColors[step.status], | |
| step.status === 'running' && 'animate-spin' | |
| )} /> | |
| <span className={cn( | |
| 'flex-1', | |
| step.status === 'skipped' && 'text-muted-foreground line-through' | |
| )}> | |
| {step.name} | |
| </span> | |
| {step.duration_ms !== undefined && step.status === 'completed' && ( | |
| <span className="text-[10px] text-muted-foreground font-mono"> | |
| {step.duration_ms}ms | |
| </span> | |
| )} | |
| </div> | |
| ); | |
| })} | |
| </div> | |
| </div> | |
| )} | |
| {/* Output/Result */} | |
| {showOutput && (process.result || process.steps.some(s => s.output || s.error)) && ( | |
| <div className="px-4 py-3 border-t border-border/30"> | |
| <span className="text-[10px] text-muted-foreground uppercase tracking-wider flex items-center gap-1"> | |
| <Terminal className="w-3 h-3" /> | |
| Output | |
| </span> | |
| <div className="mt-2 p-2 bg-black/30 rounded font-mono text-[11px] max-h-32 overflow-auto"> | |
| {process.result && ( | |
| <pre className="text-green-400 whitespace-pre-wrap"> | |
| {typeof process.result === 'string' ? process.result : JSON.stringify(process.result, null, 2)} | |
| </pre> | |
| )} | |
| {process.steps.filter(s => s.error).map(step => ( | |
| <div key={step.id} className="text-red-400 mt-1"> | |
| [{step.name}] ERROR: {step.error} | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| )} | |
| {/* Footer */} | |
| <div className="px-4 py-2 bg-muted/20 border-t border-border/30 flex items-center justify-between text-[10px] text-muted-foreground"> | |
| <span className="font-mono">Started: {process.started_at || 'Queued'}</span> | |
| {process.status === 'completed' && ( | |
| <div className="flex items-center gap-1 text-green-400"> | |
| <Sparkles className="w-3 h-3" /> | |
| Task Complete | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| ); | |
| } | |
| export default AgentProcessTracker; | |