Kraft102's picture
fix: sql.js Docker/Alpine compatibility layer for PatternMemory and FailureMemory
5a81b95
/**
* ╔═══════════════════════════════════════════════════════════════════════════╗
* β•‘ 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;