arcadiasuite/client/src/components/DevHistory.tsx

489 lines
20 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { ScrollArea } from "@/components/ui/scroll-area";
import {
CheckCircle,
XCircle,
Clock,
Loader2,
ChevronDown,
ChevronRight,
FileText,
Code,
Brain,
Wrench,
Shield,
Rocket,
TrendingUp,
AlertCircle,
RefreshCw,
Filter,
BarChart3,
GitBranch,
Eye,
Layers,
Play,
} from "lucide-react";
interface TimelineEntry {
id: number;
agent: string;
action: string;
thought: string;
observation?: string;
createdAt: string;
}
interface SubtaskEntry {
id: number;
title: string;
status: string;
assignedAgent: string;
startedAt?: string;
completedAt?: string;
}
interface ArtifactEntry {
id: number;
type: string;
name: string;
createdBy: string;
createdAt: string;
}
interface HistoryTask {
id: number;
type: string;
title: string;
description: string;
status: string;
priority: number;
assignedAgent?: string;
userId?: string;
result?: any;
errorMessage?: string;
createdAt: string;
updatedAt?: string;
startedAt?: string;
completedAt?: string;
subtaskCount: number;
artifactCount: number;
logCount: number;
subtasks: SubtaskEntry[];
artifacts: ArtifactEntry[];
timeline: TimelineEntry[];
}
const AGENT_CONFIG: Record<string, { label: string; color: string; icon: any }> = {
architect: { label: "Arquiteto", color: "bg-blue-100 text-blue-700 border-blue-200", icon: Brain },
generator: { label: "Gerador", color: "bg-purple-100 text-purple-700 border-purple-200", icon: Code },
validator: { label: "Validador", color: "bg-amber-100 text-amber-700 border-amber-200", icon: Shield },
executor: { label: "Executor", color: "bg-green-100 text-green-700 border-green-200", icon: Rocket },
evolution: { label: "Evolução", color: "bg-cyan-100 text-cyan-700 border-cyan-200", icon: TrendingUp },
dispatcher: { label: "Dispatcher", color: "bg-slate-100 text-slate-700 border-slate-200", icon: Layers },
};
const STATUS_CONFIG: Record<string, { label: string; color: string; icon: any }> = {
pending: { label: "Pendente", color: "bg-slate-100 text-slate-600", icon: Clock },
in_progress: { label: "Em Progresso", color: "bg-blue-100 text-blue-600", icon: Loader2 },
completed: { label: "Concluída", color: "bg-green-100 text-green-600", icon: CheckCircle },
failed: { label: "Falhou", color: "bg-red-100 text-red-600", icon: XCircle },
blocked: { label: "Bloqueada", color: "bg-orange-100 text-orange-600", icon: AlertCircle },
};
const ARTIFACT_ICONS: Record<string, any> = {
spec: FileText,
code: Code,
test: Shield,
doc: FileText,
config: Wrench,
analysis: BarChart3,
};
function formatDate(dateStr?: string) {
if (!dateStr) return "—";
const d = new Date(dateStr);
return d.toLocaleDateString("pt-BR", { day: "2-digit", month: "2-digit", year: "2-digit" }) +
" " + d.toLocaleTimeString("pt-BR", { hour: "2-digit", minute: "2-digit" });
}
function formatDuration(start?: string, end?: string) {
if (!start || !end) return null;
const ms = new Date(end).getTime() - new Date(start).getTime();
if (ms < 1000) return `${ms}ms`;
if (ms < 60000) return `${Math.round(ms / 1000)}s`;
return `${Math.round(ms / 60000)}min`;
}
function TaskCard({ task, onContinueTask }: { task: HistoryTask; onContinueTask?: (title: string) => void }) {
const [expanded, setExpanded] = useState(false);
const [activeSection, setActiveSection] = useState<"timeline" | "subtasks" | "artifacts">("timeline");
const statusCfg = STATUS_CONFIG[task.status] || STATUS_CONFIG.pending;
const StatusIcon = statusCfg.icon;
const duration = formatDuration(task.startedAt || task.createdAt, task.completedAt);
return (
<Card className="overflow-hidden transition-shadow hover:shadow-md" data-testid={`history-task-${task.id}`}>
<div
className="flex items-center gap-3 px-4 py-3 cursor-pointer hover:bg-muted/30 transition-colors"
onClick={() => setExpanded(!expanded)}
>
<div className="shrink-0">
{expanded ? <ChevronDown className="h-4 w-4 text-muted-foreground" /> : <ChevronRight className="h-4 w-4 text-muted-foreground" />}
</div>
<div className="flex items-center gap-2 shrink-0">
<div className={`w-8 h-8 rounded-lg flex items-center justify-center ${statusCfg.color}`}>
<StatusIcon className="h-4 w-4" />
</div>
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="font-medium text-sm truncate">{task.title}</span>
<Badge variant="outline" className="text-[10px] shrink-0">#{task.id}</Badge>
</div>
<p className="text-xs text-muted-foreground truncate">{task.description}</p>
</div>
<div className="flex items-center gap-3 shrink-0 text-xs text-muted-foreground">
{duration && (
<span className="flex items-center gap-1">
<Clock className="h-3 w-3" /> {duration}
</span>
)}
<span className="flex items-center gap-1">
<GitBranch className="h-3 w-3" /> {task.subtaskCount}
</span>
<span className="flex items-center gap-1">
<FileText className="h-3 w-3" /> {task.artifactCount}
</span>
<span className="text-[10px]">{formatDate(task.createdAt)}</span>
{onContinueTask && (
<Button
variant="outline"
size="sm"
className="h-6 text-[10px] px-2 ml-1 border-purple-300 text-purple-600 hover:bg-purple-50"
onClick={(e) => {
e.stopPropagation();
onContinueTask(task.title);
}}
data-testid={`btn-continue-task-${task.id}`}
>
<Play className="h-3 w-3 mr-1" /> Continuar
</Button>
)}
</div>
</div>
{expanded && (
<div className="border-t">
{task.errorMessage && (
<div className="mx-4 mt-3 p-3 bg-red-50 border border-red-200 rounded-lg text-sm text-red-700">
<strong>Erro:</strong> {task.errorMessage}
</div>
)}
<div className="flex border-b mx-4 mt-2">
{[
{ key: "timeline" as const, label: "Timeline", count: task.logCount },
{ key: "subtasks" as const, label: "Subtarefas", count: task.subtaskCount },
{ key: "artifacts" as const, label: "Artefatos", count: task.artifactCount },
].map(tab => (
<button
key={tab.key}
className={`px-3 py-2 text-xs font-medium border-b-2 transition-colors ${
activeSection === tab.key
? "border-primary text-primary"
: "border-transparent text-muted-foreground hover:text-foreground"
}`}
onClick={() => setActiveSection(tab.key)}
data-testid={`tab-${tab.key}-${task.id}`}
>
{tab.label} ({tab.count})
</button>
))}
</div>
<div className="p-4">
{activeSection === "timeline" && (
<div className="space-y-1">
{task.timeline.length === 0 ? (
<p className="text-sm text-muted-foreground text-center py-4">Nenhum registro na timeline</p>
) : (
<div className="relative">
<div className="absolute left-4 top-2 bottom-2 w-px bg-border" />
{task.timeline.map((entry, idx) => {
const agentCfg = AGENT_CONFIG[entry.agent] || AGENT_CONFIG.dispatcher;
const AgentIcon = agentCfg.icon;
return (
<div key={entry.id || idx} className="relative pl-10 pb-3">
<div className={`absolute left-2 top-1 w-5 h-5 rounded-full flex items-center justify-center border ${agentCfg.color}`}>
<AgentIcon className="h-2.5 w-2.5" />
</div>
<div className="bg-muted/30 rounded-lg px-3 py-2">
<div className="flex items-center justify-between gap-2 mb-0.5">
<div className="flex items-center gap-2">
<Badge variant="outline" className={`text-[10px] ${agentCfg.color}`}>
{agentCfg.label}
</Badge>
<span className="text-[10px] font-medium text-muted-foreground uppercase">{entry.action}</span>
</div>
<span className="text-[10px] text-muted-foreground">{formatDate(entry.createdAt)}</span>
</div>
<p className="text-xs text-foreground">{entry.thought}</p>
{entry.observation && (
<p className="text-[11px] text-muted-foreground mt-1 italic border-l-2 border-muted pl-2">
{entry.observation.length > 200 ? entry.observation.slice(0, 200) + "…" : entry.observation}
</p>
)}
</div>
</div>
);
})}
</div>
)}
</div>
)}
{activeSection === "subtasks" && (
<div className="space-y-2">
{task.subtasks.length === 0 ? (
<p className="text-sm text-muted-foreground text-center py-4">Nenhuma subtarefa</p>
) : (
task.subtasks.map(sub => {
const subStatus = STATUS_CONFIG[sub.status] || STATUS_CONFIG.pending;
const SubIcon = subStatus.icon;
const agentCfg = AGENT_CONFIG[sub.assignedAgent] || AGENT_CONFIG.dispatcher;
const AgentIcon = agentCfg.icon;
const subDuration = formatDuration(sub.startedAt, sub.completedAt);
return (
<div key={sub.id} className="flex items-center gap-3 p-3 border rounded-lg">
<div className={`w-7 h-7 rounded flex items-center justify-center ${subStatus.color}`}>
<SubIcon className="h-3.5 w-3.5" />
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium truncate">{sub.title}</p>
<div className="flex items-center gap-2 mt-0.5">
<Badge variant="outline" className={`text-[10px] ${agentCfg.color}`}>
<AgentIcon className="h-2.5 w-2.5 mr-1" />
{agentCfg.label}
</Badge>
{subDuration && (
<span className="text-[10px] text-muted-foreground flex items-center gap-0.5">
<Clock className="h-2.5 w-2.5" /> {subDuration}
</span>
)}
</div>
</div>
<Badge className={`text-[10px] ${subStatus.color}`}>{subStatus.label}</Badge>
</div>
);
})
)}
</div>
)}
{activeSection === "artifacts" && (
<div className="space-y-2">
{task.artifacts.length === 0 ? (
<p className="text-sm text-muted-foreground text-center py-4">Nenhum artefato gerado</p>
) : (
task.artifacts.map(art => {
const ArtIcon = ARTIFACT_ICONS[art.type] || FileText;
const agentCfg = AGENT_CONFIG[art.createdBy] || AGENT_CONFIG.dispatcher;
return (
<div key={art.id} className="flex items-center gap-3 p-3 border rounded-lg">
<div className="w-8 h-8 rounded-lg bg-muted flex items-center justify-center">
<ArtIcon className="h-4 w-4 text-muted-foreground" />
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium truncate">{art.name}</p>
<div className="flex items-center gap-2 mt-0.5">
<Badge variant="outline" className="text-[10px]">{art.type}</Badge>
<Badge variant="outline" className={`text-[10px] ${agentCfg.color}`}>
{agentCfg.label}
</Badge>
<span className="text-[10px] text-muted-foreground">{formatDate(art.createdAt)}</span>
</div>
</div>
</div>
);
})
)}
</div>
)}
</div>
</div>
)}
</Card>
);
}
interface DevHistoryProps {
embedded?: boolean;
onContinueTask?: (taskTitle: string) => void;
}
export default function DevHistory({ embedded, onContinueTask }: DevHistoryProps) {
const [statusFilter, setStatusFilter] = useState<string>("all");
const [page, setPage] = useState(0);
const pageSize = 20;
const { data: historyData, isLoading, refetch } = useQuery({
queryKey: ["/api/blackboard/history", statusFilter, page],
queryFn: async () => {
const params = new URLSearchParams();
params.set("limit", pageSize.toString());
params.set("offset", (page * pageSize).toString());
if (statusFilter !== "all") params.set("status", statusFilter);
const res = await fetch(`/api/blackboard/history?${params}`, { credentials: "include" });
if (!res.ok) throw new Error("Falha ao carregar histórico");
return res.json();
},
refetchInterval: 15000,
});
const { data: statsData } = useQuery({
queryKey: ["/api/blackboard/stats"],
queryFn: async () => {
const res = await fetch("/api/blackboard/stats", { credentials: "include" });
if (!res.ok) throw new Error("Falha ao carregar stats");
return res.json();
},
refetchInterval: 15000,
});
const tasks: HistoryTask[] = historyData?.tasks || [];
const stats = statsData?.stats;
const agents = statsData?.agents || [];
return (
<div className={`space-y-4 ${embedded ? "" : "p-4"}`} data-testid="dev-history">
{/* Stats Cards */}
<div className="grid grid-cols-2 md:grid-cols-5 gap-3">
{[
{ label: "Total", value: stats?.totalTasks || 0, color: "text-foreground", bgColor: "bg-muted/50" },
{ label: "Pendentes", value: stats?.pendingTasks || 0, color: "text-amber-600", bgColor: "bg-amber-50" },
{ label: "Concluídas", value: stats?.completedTasks || 0, color: "text-green-600", bgColor: "bg-green-50" },
{ label: "Falhas", value: stats?.failedTasks || 0, color: "text-red-600", bgColor: "bg-red-50" },
{ label: "Artefatos", value: stats?.artifactsCount || 0, color: "text-blue-600", bgColor: "bg-blue-50" },
].map(stat => (
<Card key={stat.label} className={`${stat.bgColor}`}>
<CardContent className="p-3 text-center">
<p className={`text-2xl font-bold ${stat.color}`}>{stat.value}</p>
<p className="text-xs text-muted-foreground">{stat.label}</p>
</CardContent>
</Card>
))}
</div>
{/* Agentes Status */}
{agents.length > 0 && (
<div className="flex items-center gap-2 flex-wrap">
<span className="text-xs font-medium text-muted-foreground">Agentes:</span>
{agents.map((agent: any) => {
const cfg = AGENT_CONFIG[agent.name] || AGENT_CONFIG.dispatcher;
return (
<Badge key={agent.name} variant="outline" className={`text-[10px] ${cfg.color}`}>
<div className={`w-1.5 h-1.5 rounded-full mr-1.5 ${agent.running ? "bg-green-500" : "bg-slate-400"}`} />
{cfg.label}
{agent.tasksProcessed > 0 && ` (${agent.tasksProcessed})`}
</Badge>
);
})}
</div>
)}
{/* Filtros e ações */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-1">
<Filter className="h-4 w-4 text-muted-foreground" />
{[
{ key: "all", label: "Todas" },
{ key: "completed", label: "Concluídas" },
{ key: "failed", label: "Falhas" },
{ key: "in_progress", label: "Em Progresso" },
{ key: "pending", label: "Pendentes" },
].map(f => (
<Button
key={f.key}
variant={statusFilter === f.key ? "default" : "ghost"}
size="sm"
className="h-7 text-xs"
onClick={() => { setStatusFilter(f.key); setPage(0); }}
data-testid={`filter-${f.key}`}
>
{f.label}
</Button>
))}
</div>
<Button variant="ghost" size="sm" onClick={() => refetch()} data-testid="btn-refresh-history">
<RefreshCw className="h-4 w-4" />
</Button>
</div>
{/* Lista de Tarefas */}
<ScrollArea className={embedded ? "h-[calc(100vh-380px)]" : "h-[calc(100vh-300px)]"}>
{isLoading ? (
<div className="flex items-center justify-center py-16">
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
<span className="ml-2 text-sm text-muted-foreground">Carregando histórico...</span>
</div>
) : tasks.length === 0 ? (
<Card className="border-dashed">
<CardContent className="flex flex-col items-center justify-center py-16">
<Eye className="h-12 w-12 text-muted-foreground mb-3" />
<h3 className="font-semibold text-lg mb-1">Nenhuma tarefa encontrada</h3>
<p className="text-sm text-muted-foreground text-center max-w-md">
As tarefas executadas pelos agentes autônomos aparecerão aqui com todos os detalhes.
</p>
</CardContent>
</Card>
) : (
<div className="space-y-2 pr-2">
{tasks.map(task => (
<TaskCard key={task.id} task={task} onContinueTask={onContinueTask} />
))}
</div>
)}
</ScrollArea>
{/* Pagination */}
{(historyData?.total || 0) > pageSize && (
<div className="flex items-center justify-between pt-2 border-t">
<span className="text-xs text-muted-foreground">
{page * pageSize + 1}{Math.min((page + 1) * pageSize, historyData?.total || 0)} de {historyData?.total || 0}
</span>
<div className="flex gap-1">
<Button
variant="ghost"
size="sm"
className="h-7 text-xs"
disabled={page === 0}
onClick={() => setPage(p => Math.max(0, p - 1))}
data-testid="btn-prev-page"
>
Anterior
</Button>
<Button
variant="ghost"
size="sm"
className="h-7 text-xs"
disabled={(page + 1) * pageSize >= (historyData?.total || 0)}
onClick={() => setPage(p => p + 1)}
data-testid="btn-next-page"
>
Próximo
</Button>
</div>
</div>
)}
</div>
);
}