Files
ClaudeMarketing/components/pipeline-progress.tsx
T
Trey T 352fb59e7c feat(pipeline-progress): expandable agent rows with output and error details
Click an agent row to toggle its expanded view; expanded rows surface
outputSummary and any error text. Adds chevron icons, hover affordance,
and a shrink-0 guard on status icons so they keep their size in flex layouts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 20:37:19 -05:00

107 lines
3.7 KiB
TypeScript

"use client";
import { useState } from "react";
import { CheckCircle2, ChevronDown, ChevronRight, Circle, Loader2, XCircle } from "lucide-react";
import type { AgentStatus } from "@/hooks/use-pipeline-progress";
const AGENT_LABELS: Record<string, string> = {
"trend-scout": "Trend Scout",
"marketing-research-agent": "Research Agent",
"script-writer": "Script Writer",
"ad-creative-designer": "Ad Creative Designer",
"video-ad-producer": "Video Ad Producer",
"copywriter-agent": "Copywriter",
"distribution-agent": "Distribution Agent",
};
function formatDuration(ms?: number) {
if (!ms) return "";
if (ms < 1000) return `${ms}ms`;
if (ms < 60000) return `${Math.round(ms / 1000)}s`;
return `${Math.round(ms / 60000)}m ${Math.round((ms % 60000) / 1000)}s`;
}
function StatusIcon({ status }: { status: AgentStatus["status"] }) {
switch (status) {
case "completed":
return <CheckCircle2 className="h-5 w-5 text-green-500 shrink-0" />;
case "running":
return <Loader2 className="h-5 w-5 animate-spin text-blue-500 shrink-0" />;
case "failed":
return <XCircle className="h-5 w-5 text-red-500 shrink-0" />;
default:
return <Circle className="h-5 w-5 text-muted-foreground shrink-0" />;
}
}
function AgentRow({ agent }: { agent: AgentStatus }) {
const [expanded, setExpanded] = useState(false);
const hasDetails = agent.outputSummary || agent.error;
return (
<div
className={`rounded-lg border transition-colors ${hasDetails ? "cursor-pointer hover:bg-muted/50" : ""}`}
onClick={() => hasDetails && setExpanded(!expanded)}
>
<div className="flex items-center gap-3 p-3">
<StatusIcon status={agent.status} />
<div className="flex-1 min-w-0">
<div className="font-medium">
{AGENT_LABELS[agent.agentName] || agent.agentName}
</div>
{!expanded && agent.outputSummary && (
<p className="text-sm text-muted-foreground line-clamp-1">
{agent.outputSummary}
</p>
)}
{!expanded && agent.error && (
<p className="text-sm text-red-500 line-clamp-1">{agent.error}</p>
)}
</div>
{agent.durationMs && (
<span className="text-sm text-muted-foreground shrink-0">
{formatDuration(agent.durationMs)}
</span>
)}
{hasDetails && (
expanded
? <ChevronDown className="h-4 w-4 text-muted-foreground shrink-0" />
: <ChevronRight className="h-4 w-4 text-muted-foreground shrink-0" />
)}
</div>
{expanded && (
<div className="border-t px-3 py-3 space-y-2">
{agent.error && (
<div>
<div className="text-xs font-medium text-red-500 uppercase mb-1">Error</div>
<p className="text-sm text-red-500 whitespace-pre-wrap">{agent.error}</p>
</div>
)}
{agent.outputSummary && (
<div>
<div className="text-xs font-medium text-muted-foreground uppercase mb-1">Output</div>
<p className="text-sm text-muted-foreground whitespace-pre-wrap">{agent.outputSummary}</p>
</div>
)}
{agent.durationMs && (
<div>
<div className="text-xs font-medium text-muted-foreground uppercase mb-1">Duration</div>
<p className="text-sm text-muted-foreground">{formatDuration(agent.durationMs)}</p>
</div>
)}
</div>
)}
</div>
);
}
export function PipelineProgress({ agents }: { agents: AgentStatus[] }) {
return (
<div className="space-y-3">
{agents.map((agent) => (
<AgentRow key={agent.agentName} agent={agent} />
))}
</div>
);
}