From 11d8c0c19a3c40cd04c6448be420464b77838718 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sat, 18 Apr 2026 12:03:53 +0000 Subject: [PATCH] Changes Co-authored-by: domfelipe <53182096+domfelipe@users.noreply.github.com> --- src/components/mika/cronjobs/CronjobCard.tsx | 189 +++++++++ .../mika/cronjobs/CronjobWizard.tsx | 379 ++++++++++++++++++ src/hooks/use-cronjobs.ts | 170 ++++++++ src/routeTree.gen.ts | 72 ++++ src/routes/painel.cronjobs.$id.tsx | 267 ++++++++++++ src/routes/painel.cronjobs.index.tsx | 124 ++++++ src/routes/painel.cronjobs.nova.tsx | 28 ++ 7 files changed, 1229 insertions(+) create mode 100644 src/components/mika/cronjobs/CronjobCard.tsx create mode 100644 src/components/mika/cronjobs/CronjobWizard.tsx create mode 100644 src/hooks/use-cronjobs.ts create mode 100644 src/routes/painel.cronjobs.$id.tsx create mode 100644 src/routes/painel.cronjobs.index.tsx create mode 100644 src/routes/painel.cronjobs.nova.tsx diff --git a/src/components/mika/cronjobs/CronjobCard.tsx b/src/components/mika/cronjobs/CronjobCard.tsx new file mode 100644 index 0000000..c59ba99 --- /dev/null +++ b/src/components/mika/cronjobs/CronjobCard.tsx @@ -0,0 +1,189 @@ +"use client"; + +import { Link } from "@tanstack/react-router"; +import { Pause, Play, Trash2, Clock, AlertTriangle } from "lucide-react"; +import { toast } from "sonner"; +import { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from "@/components/ui/alert-dialog"; +import { + type ScheduledJob, + useDeleteCronjob, + useUpdateCronjobStatus, +} from "@/hooks/use-cronjobs"; +import { useAvailableMcps, useUserIntegrations } from "@/hooks/use-integrations"; + +interface Props { + job: ScheduledJob; +} + +export function CronjobCard({ job }: Props) { + const updateMut = useUpdateCronjobStatus(); + const deleteMut = useDeleteCronjob(); + const { data: mcps = [] } = useAvailableMcps(); + const { data: integrations = [] } = useUserIntegrations(); + const [confirmDelete, setConfirmDelete] = useState(false); + + const connectedMcpIds = new Set( + integrations.filter((i) => i.status === "active").map((i) => i.mcp_id), + ); + const requiredMcps = job.required_mcp_slugs + .map((slug) => mcps.find((m) => m.slug === slug)) + .filter((m): m is NonNullable => !!m); + const missingMcps = requiredMcps.filter((m) => !connectedMcpIds.has(m.id)); + + const isActive = job.status === "active"; + const isAutoPaused = job.status === "auto_paused"; + + async function toggle() { + try { + await updateMut.mutateAsync({ + id: job.id, + status: isActive ? "paused" : "active", + }); + toast.success(isActive ? "Automação pausada." : "Automação ativada."); + } catch (e) { + toast.error(e instanceof Error ? e.message : "Erro ao atualizar."); + } + } + + async function remove() { + try { + await deleteMut.mutateAsync(job.id); + toast.success("Automação excluída."); + setConfirmDelete(false); + } catch (e) { + toast.error(e instanceof Error ? e.message : "Erro ao excluir."); + } + } + + return ( +
+
+
+
+

{job.name}

+ {isActive && Ativa} + {job.status === "paused" && Pausada} + {isAutoPaused && ( + + Auto-pausada + + )} +
+

+ {job.human_readable} +

+
+
+ + {isAutoPaused && job.auto_paused_reason && ( +

{job.auto_paused_reason}

+ )} + + {missingMcps.length > 0 && isActive && ( +
+ + Integrações faltando:{" "} + + {missingMcps.map((m) => m.name).join(", ")} +
+ )} + + {requiredMcps.length > 0 && ( +
+ {requiredMcps.map((m) => { + const ok = connectedMcpIds.has(m.id); + return ( + + {m.name} + + ); + })} +
+ )} + +
+ {job.cron_expression} + {job.next_run_at && ( + + + {new Date(job.next_run_at).toLocaleString("pt-BR", { + timeZone: job.timezone, + dateStyle: "short", + timeStyle: "short", + })} + + )} +
+ +
+ +
+ + + + + + + + + Excluir automação? + + Esta ação remove permanentemente a automação “{job.name}”. + + + + + Cancelar + + { + e.preventDefault(); + remove(); + }} + disabled={deleteMut.isPending} + className="bg-destructive text-destructive-foreground hover:bg-destructive/90" + > + {deleteMut.isPending ? "Excluindo..." : "Excluir"} + + + + +
+
+
+ ); +} diff --git a/src/components/mika/cronjobs/CronjobWizard.tsx b/src/components/mika/cronjobs/CronjobWizard.tsx new file mode 100644 index 0000000..94f0f06 --- /dev/null +++ b/src/components/mika/cronjobs/CronjobWizard.tsx @@ -0,0 +1,379 @@ +"use client"; + +import { useMemo, useState } from "react"; +import { Loader2, Sparkles, AlertTriangle, ArrowLeft, CheckCircle2, Plug } from "lucide-react"; +import { Link } from "@tanstack/react-router"; +import { toast } from "sonner"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Textarea } from "@/components/ui/textarea"; +import { Badge } from "@/components/ui/badge"; +import { invokeFunction } from "@/lib/invoke-function"; +import { useCreateCronjob } from "@/hooks/use-cronjobs"; +import { useAvailableMcps, useUserIntegrations } from "@/hooks/use-integrations"; +import { useAgentInstance } from "@/hooks/use-agent-instance"; +import { useProfile } from "@/hooks/use-profile"; + +interface ParseResult { + cron_expression: string; + human_readable: string; + action_description: string; + required_mcp_slugs: string[]; + warnings: string[]; + confidence: "high" | "medium" | "low"; + next_run_at: string | null; +} + +type Step = "input" | "review" | "confirm"; + +interface Props { + onCreated?: (id: string) => void; + onCancel?: () => void; +} + +export function CronjobWizard({ onCreated, onCancel }: Props) { + const [step, setStep] = useState("input"); + const [naturalInput, setNaturalInput] = useState(""); + const [parsing, setParsing] = useState(false); + const [parsed, setParsed] = useState(null); + + // Form fields editáveis na revisão + const [name, setName] = useState(""); + const [description, setDescription] = useState(""); + const [cronExpression, setCronExpression] = useState(""); + const [humanReadable, setHumanReadable] = useState(""); + const [actionPrompt, setActionPrompt] = useState(""); + const [requiredMcps, setRequiredMcps] = useState([]); + const [reviewConfirmed, setReviewConfirmed] = useState(false); + + const { data: profile } = useProfile(); + const { data: agent } = useAgentInstance(); + const { data: mcps = [] } = useAvailableMcps(); + const { data: integrations = [] } = useUserIntegrations(); + const createMut = useCreateCronjob(); + + const tz = profile && "timezone" in profile && typeof (profile as { timezone?: string }).timezone === "string" + ? (profile as { timezone: string }).timezone + : "America/Sao_Paulo"; + + const mcpsBySlug = useMemo(() => { + const m = new Map(); + for (const x of mcps) m.set(x.slug, { id: x.id, name: x.name }); + return m; + }, [mcps]); + + const connectedSlugs = useMemo(() => { + const set = new Set(); + for (const it of integrations) { + if (it.status !== "active") continue; + const mcp = mcps.find((m) => m.id === it.mcp_id); + if (mcp) set.add(mcp.slug); + } + return set; + }, [integrations, mcps]); + + const missingMcps = requiredMcps.filter((s) => !connectedSlugs.has(s)); + + async function handleParse() { + const trimmed = naturalInput.trim(); + if (trimmed.length < 5) { + toast.error("Descreva a automação com mais detalhes."); + return; + } + setParsing(true); + const { data, error } = await invokeFunction( + "parse-cronjob-natural-language", + { natural_language_input: trimmed, user_timezone: tz }, + ); + setParsing(false); + if (error || !data) { + toast.error(error?.message ?? "Não conseguimos interpretar. Tente reescrever."); + return; + } + setParsed(data); + // Pré-preenche campos + const suggestedName = trimmed.length <= 60 ? trimmed : trimmed.slice(0, 57) + "..."; + setName(suggestedName); + setDescription(""); + setCronExpression(data.cron_expression); + setHumanReadable(data.human_readable); + setActionPrompt(data.action_description || trimmed); + setRequiredMcps(data.required_mcp_slugs); + setReviewConfirmed(false); + setStep("review"); + } + + async function handleCreate() { + if (!agent) { + toast.error("Agente ainda não está pronto."); + return; + } + if (!name.trim()) { + toast.error("Dê um nome à automação."); + return; + } + if (!cronExpression.trim() || !actionPrompt.trim()) { + toast.error("Preencha o cron e a ação."); + return; + } + try { + const job = await createMut.mutateAsync({ + agent_instance_id: agent.id, + name: name.trim(), + description: description.trim() || null, + natural_language_input: naturalInput.trim(), + cron_expression: cronExpression.trim(), + human_readable: humanReadable.trim() || cronExpression.trim(), + action_prompt: actionPrompt.trim(), + required_mcp_slugs: requiredMcps, + timezone: tz, + next_run_at: parsed?.next_run_at ?? null, + }); + toast.success("Automação criada!"); + onCreated?.(job.id); + } catch (e) { + const msg = e instanceof Error ? e.message : "Erro ao criar automação"; + toast.error(msg); + } + } + + // STEP 1 — Input + if (step === "input") { + return ( +
+
+ +

+ Diga o quê, quando e quais ferramentas usar. A IA traduz para um cronjob. +

+