diff --git a/.planning/STATE.md b/.planning/STATE.md index 7d181be..ee04b20 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -1,6 +1,6 @@ # State -## Current Phase: 5 — Automation Fabric +## Current Phase: 6 — Dev Center Completo ## Completed - Phase 1: submodules, tabelas, Neo4j, ReferenceParser @@ -8,9 +8,10 @@ - Phase 3: miroflow_service.py, bridge TS + KG logging, MiroFlowControl.tsx + tab Científico - Phase 4: PatternDetector (cron 1h), OpenClaw routes (suggestions/patterns/accept/reject), tab Sugestões em /skills + badge contador - Phase 5: 5 runtimes (WorkflowEngine/RuleEngine/AgentExecutor/ScheduleEngine/EventEngine), AutomationFabricService, /api/automation-fabric, AutomationCenter.tsx em /automations-center +- Phase 6: arcadia_agent_defs (schema+migration+API CRUD+assemble/deploy/run/fork), 4 tabs no DevCenter (Design/Montar/Deploy/Galeria) ## In Progress -- (nenhum — Phase 5 concluída, iniciar Phase 6) +- (nenhum — Phase 6 concluída, iniciar Phase 7) ## Roadmap Evolution - Phase 7 added: Skill Fabric Expandido (compiladores, sandbox, 3 editor modes, validation pipeline) diff --git a/client/src/pages/DevCenter.tsx b/client/src/pages/DevCenter.tsx index 4d60174..cf81299 100644 --- a/client/src/pages/DevCenter.tsx +++ b/client/src/pages/DevCenter.tsx @@ -61,13 +61,615 @@ import { CheckSquare, Square, Fingerprint, - PanelLeft + PanelLeft, + Save } from "lucide-react"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; import { apiRequest } from "@/lib/queryClient"; import { useToast } from "@/hooks/use-toast"; import DevHistory from "@/components/DevHistory"; +// ============================================================================ +// PHASE 6: Agent Factory Tabs (Design, Assemble, Deploy, Galeria) +// ============================================================================ + +type AgentDef = { + id: number; + name: string; + description: string | null; + spec: { mode: "markdown" | "typescript" | "visual"; content: string } | null; + status: "draft" | "assembling" | "ready" | "deployed"; + version: number; + lastTaskId: number | null; + tenantId: string | null; + userId: string | null; + createdAt: string; + updatedAt: string; +}; + +function AgentFactoryTabs() { + const { toast } = useToast(); + const queryClient = useQueryClient(); + + // ---- Design tab state ---- + const [selectedDefId, setSelectedDefId] = useState(null); + const [editorMode, setEditorMode] = useState<"markdown" | "typescript" | "visual">("markdown"); + const [editorContent, setEditorContent] = useState(""); + const [defName, setDefName] = useState(""); + const [defDescription, setDefDescription] = useState(""); + const [isCreatingNew, setIsCreatingNew] = useState(false); + + // ---- Assemble tab state ---- + const [assemblePollingId, setAssemblePollingId] = useState(null); + const [assembleTaskStatus, setAssembleTaskStatus] = useState>({}); + + // ---- Galeria filter ---- + const [galFilter, setGalFilter] = useState(""); + + const { data: defsData, isLoading: defsLoading } = useQuery<{ success: boolean; data: AgentDef[] }>({ + queryKey: ["/api/agent-defs"], + refetchInterval: assemblePollingId ? 3000 : false, + }); + const defs = defsData?.data || []; + + // Poll task status for assembling defs + useEffect(() => { + const assembling = defs.filter(d => d.status === "assembling" && d.lastTaskId); + if (assembling.length === 0) return; + + const interval = setInterval(async () => { + for (const def of assembling) { + if (!def.lastTaskId) continue; + try { + const res = await fetch(`/api/blackboard/task/${def.lastTaskId}`, { credentials: "include" }); + const data = await res.json(); + const taskStatus = data?.task?.status; + if (taskStatus === "completed" || taskStatus === "failed") { + // Mark agent as ready or draft + await fetch(`/api/agent-defs/${def.id}`, { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + credentials: "include", + body: JSON.stringify({ status: taskStatus === "completed" ? "ready" : "draft" }), + }); + queryClient.invalidateQueries({ queryKey: ["/api/agent-defs"] }); + } + setAssembleTaskStatus(prev => ({ ...prev, [def.id]: taskStatus || "unknown" })); + } catch { /* ignore */ } + } + }, 3000); + return () => clearInterval(interval); + }, [defs, queryClient]); + + const createMutation = useMutation({ + mutationFn: async () => { + const res = await apiRequest("POST", "/api/agent-defs", { + name: defName, + description: defDescription, + spec: { mode: editorMode, content: editorContent }, + }); + return res.json(); + }, + onSuccess: (data) => { + queryClient.invalidateQueries({ queryKey: ["/api/agent-defs"] }); + setSelectedDefId(data.data.id); + setIsCreatingNew(false); + toast({ title: "Agente criado", description: data.data.name }); + }, + onError: (e: any) => toast({ title: "Erro", description: e.message, variant: "destructive" }), + }); + + const saveMutation = useMutation({ + mutationFn: async (id: number) => { + const res = await apiRequest("PATCH", `/api/agent-defs/${id}`, { + name: defName, + description: defDescription, + spec: { mode: editorMode, content: editorContent }, + }); + return res.json(); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["/api/agent-defs"] }); + toast({ title: "Salvo" }); + }, + onError: (e: any) => toast({ title: "Erro ao salvar", description: e.message, variant: "destructive" }), + }); + + const assembleMutation = useMutation({ + mutationFn: async (id: number) => { + const res = await apiRequest("POST", `/api/agent-defs/${id}/assemble`, {}); + return res.json(); + }, + onSuccess: (data, id) => { + queryClient.invalidateQueries({ queryKey: ["/api/agent-defs"] }); + setAssemblePollingId(id); + toast({ title: "Montagem iniciada", description: `Task #${data.taskId}` }); + }, + onError: (e: any) => toast({ title: "Erro", description: e.message, variant: "destructive" }), + }); + + const deployMutation = useMutation({ + mutationFn: async (id: number) => { + const res = await apiRequest("POST", `/api/agent-defs/${id}/deploy`, {}); + return res.json(); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["/api/agent-defs"] }); + toast({ title: "Deploy realizado!" }); + }, + onError: (e: any) => toast({ title: "Erro no deploy", description: e.message, variant: "destructive" }), + }); + + const redraftMutation = useMutation({ + mutationFn: async (id: number) => { + const res = await apiRequest("POST", `/api/agent-defs/${id}/redraft`, {}); + return res.json(); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["/api/agent-defs"] }); + toast({ title: "Volta para rascunho" }); + }, + onError: (e: any) => toast({ title: "Erro", description: e.message, variant: "destructive" }), + }); + + const runMutation = useMutation({ + mutationFn: async (id: number) => { + const res = await apiRequest("POST", `/api/agent-defs/${id}/run`, {}); + return res.json(); + }, + onSuccess: (data) => { + toast({ title: "Execução iniciada", description: `Task #${data.taskId}` }); + }, + onError: (e: any) => toast({ title: "Erro ao executar", description: e.message, variant: "destructive" }), + }); + + const forkMutation = useMutation({ + mutationFn: async (id: number) => { + const res = await apiRequest("POST", `/api/agent-defs/${id}/fork`, {}); + return res.json(); + }, + onSuccess: (data) => { + queryClient.invalidateQueries({ queryKey: ["/api/agent-defs"] }); + toast({ title: "Fork criado", description: data.data.name }); + }, + onError: (e: any) => toast({ title: "Erro ao fazer fork", description: e.message, variant: "destructive" }), + }); + + const deleteMutation = useMutation({ + mutationFn: async (id: number) => { + const res = await apiRequest("DELETE", `/api/agent-defs/${id}`, undefined); + return res.json(); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["/api/agent-defs"] }); + setSelectedDefId(null); + toast({ title: "Excluído" }); + }, + onError: (e: any) => toast({ title: "Erro", description: e.message, variant: "destructive" }), + }); + + // Quando seleciona um def existente, carrega os campos + const selectDef = (def: AgentDef) => { + setSelectedDefId(def.id); + setDefName(def.name); + setDefDescription(def.description || ""); + const spec = def.spec || { mode: "markdown", content: "" }; + setEditorMode(spec.mode || "markdown"); + setEditorContent(spec.content || ""); + setIsCreatingNew(false); + }; + + const startNew = () => { + setSelectedDefId(null); + setDefName(""); + setDefDescription(""); + setEditorMode("markdown"); + setEditorContent(""); + setIsCreatingNew(true); + }; + + const statusColor: Record = { + draft: "bg-zinc-500", + assembling: "bg-blue-500", + ready: "bg-green-500", + deployed: "bg-purple-500", + }; + + const statusLabel: Record = { + draft: "Rascunho", + assembling: "Montando...", + ready: "Pronto", + deployed: "Implantado", + }; + + const galDefs = defs.filter(d => + d.status === "deployed" && + (d.name.toLowerCase().includes(galFilter.toLowerCase()) || + (d.description || "").toLowerCase().includes(galFilter.toLowerCase())) + ); + + return ( + <> + {/* ---- TAB: DESIGN ---- */} + +
+ {/* Lista de defs */} + + +
+ + Agentes + + +
+
+ + {defsLoading ? ( +
+ +
+ ) : defs.length === 0 ? ( +

Nenhum agente ainda.
Clique em "+ Novo" para criar.

+ ) : ( +
+ {defs.map(def => ( + + ))} +
+ )} +
+
+ + {/* Editor */} + + {(selectedDefId || isCreatingNew) ? ( + <> + +
+
+ setDefName(e.target.value)} + placeholder="Nome do agente" + className="h-7 text-sm font-medium" + /> + setDefDescription(e.target.value)} + placeholder="Descrição (opcional)" + className="h-6 text-xs text-zinc-500" + /> +
+
+ {(["markdown", "typescript", "visual"] as const).map(m => ( + + ))} +
+
+ {isCreatingNew ? ( + + ) : ( + <> + + + + )} +
+
+
+ +
+ {editorMode === "visual" ? ( + +

Defina os nós do agente em JSON (name, type, connections):

+