mirror of
https://github.com/domfelipe/mika-agent-assist.git
synced 2026-08-07 13:56:51 +00:00
fix: wire mika runtime cron and skill actions
This commit is contained in:
parent
6c0ccff305
commit
e9f22e6ceb
16 changed files with 1562 additions and 1310 deletions
|
|
@ -11,7 +11,7 @@ import { Textarea } from "@/components/ui/textarea";
|
|||
import { Badge } from "@/components/ui/badge";
|
||||
import { invokeFunction } from "@/lib/invoke-function";
|
||||
import { syncAgentRuntime } from "@/lib/sync-agent-runtime";
|
||||
import { useCreateCronjob } from "@/hooks/use-cronjobs";
|
||||
import { markCronjobRuntimeSyncError, 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";
|
||||
|
|
@ -54,9 +54,12 @@ export function CronjobWizard({ onCreated, onCancel }: Props) {
|
|||
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 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<string, { id: string; name: string }>();
|
||||
|
|
@ -83,10 +86,10 @@ export function CronjobWizard({ onCreated, onCancel }: Props) {
|
|||
return;
|
||||
}
|
||||
setParsing(true);
|
||||
const { data, error } = await invokeFunction<ParseResult>(
|
||||
"parse-cronjob-natural-language",
|
||||
{ natural_language_input: trimmed, user_timezone: tz },
|
||||
);
|
||||
const { data, error } = await invokeFunction<ParseResult>("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.");
|
||||
|
|
@ -131,11 +134,14 @@ export function CronjobWizard({ onCreated, onCancel }: Props) {
|
|||
timezone: tz,
|
||||
next_run_at: parsed?.next_run_at ?? null,
|
||||
});
|
||||
toast.success("Automação criada!");
|
||||
|
||||
const { error: syncError } = await syncAgentRuntime(agent.id, "cronjobs");
|
||||
if (syncError) {
|
||||
toast.warning("Automação criada, mas o runtime do agente não sincronizou.");
|
||||
await markCronjobRuntimeSyncError(job.id, syncError.message);
|
||||
toast.error("Automação criada, mas não foi ativada no agente.", {
|
||||
description: "Corrija o runtime e tente sincronizar novamente pelo painel.",
|
||||
});
|
||||
} else {
|
||||
toast.success("Automação criada e sincronizada!");
|
||||
}
|
||||
|
||||
onCreated?.(job.id);
|
||||
|
|
@ -208,26 +214,29 @@ export function CronjobWizard({ onCreated, onCancel }: Props) {
|
|||
parsed.confidence === "high"
|
||||
? "success"
|
||||
: parsed.confidence === "medium"
|
||||
? "secondary"
|
||||
: "destructive";
|
||||
? "secondary"
|
||||
: "destructive";
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="rounded-xl border border-border bg-card p-6 space-y-4">
|
||||
<div className="flex items-center justify-between flex-wrap gap-2">
|
||||
<h2 className="text-lg font-semibold">Revisar interpretação</h2>
|
||||
<Badge variant={confidenceColor as "success" | "secondary" | "destructive"}>
|
||||
Confiança: {parsed.confidence === "high" ? "alta" : parsed.confidence === "medium" ? "média" : "baixa"}
|
||||
Confiança:{" "}
|
||||
{parsed.confidence === "high"
|
||||
? "alta"
|
||||
: parsed.confidence === "medium"
|
||||
? "média"
|
||||
: "baixa"}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{parsed.warnings.length > 0 && (
|
||||
<div className="rounded-md border border-amber-500/40 bg-amber-500/10 p-3 text-sm">
|
||||
<div className="rounded-md border border-warning/30 bg-warning/10 p-3 text-sm">
|
||||
<div className="flex items-start gap-2">
|
||||
<AlertTriangle className="h-4 w-4 text-amber-600 mt-0.5 shrink-0" />
|
||||
<AlertTriangle className="h-4 w-4 text-warning mt-0.5 shrink-0" />
|
||||
<div>
|
||||
<p className="font-medium text-amber-700 dark:text-amber-400">
|
||||
Suposições da IA — confira:
|
||||
</p>
|
||||
<p className="font-medium text-warning">Suposições da IA — confira:</p>
|
||||
<ul className="list-disc list-inside mt-1 text-muted-foreground">
|
||||
{parsed.warnings.map((w, i) => (
|
||||
<li key={i}>{w}</li>
|
||||
|
|
@ -317,7 +326,11 @@ export function CronjobWizard({ onCreated, onCancel }: Props) {
|
|||
variant={connected ? "success" : "destructive"}
|
||||
className="gap-1"
|
||||
>
|
||||
{connected ? <CheckCircle2 className="h-3 w-3" /> : <Plug className="h-3 w-3" />}
|
||||
{connected ? (
|
||||
<CheckCircle2 className="h-3 w-3" />
|
||||
) : (
|
||||
<Plug className="h-3 w-3" />
|
||||
)}
|
||||
{mcp?.name ?? slug}
|
||||
{!connected && " (não conectado)"}
|
||||
</Badge>
|
||||
|
|
@ -331,7 +344,9 @@ export function CronjobWizard({ onCreated, onCancel }: Props) {
|
|||
Conecte as integrações faltantes antes de criar.
|
||||
</p>
|
||||
<Button asChild size="sm" variant="outline" className="mt-2">
|
||||
<Link to="/painel/integracoes" search={{}}>Ir para Integrações</Link>
|
||||
<Link to="/painel/integracoes" search={{}}>
|
||||
Ir para Integrações
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -351,11 +366,7 @@ export function CronjobWizard({ onCreated, onCancel }: Props) {
|
|||
</div>
|
||||
|
||||
<div className="flex justify-between gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => setStep("input")}
|
||||
disabled={createMut.isPending}
|
||||
>
|
||||
<Button variant="ghost" onClick={() => setStep("input")} disabled={createMut.isPending}>
|
||||
<ArrowLeft className="h-4 w-4 mr-2" /> Voltar
|
||||
</Button>
|
||||
<Button
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
"use client";
|
||||
|
||||
import { useMemo } from "react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { MessageCircle, ExternalLink, CheckCircle2 } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
|
@ -14,6 +13,11 @@ interface Props {
|
|||
}
|
||||
|
||||
const EMOJIS = ["🎉", "✨", "⭐", "🚀", "🎉", "✨", "⭐", "🚀", "🎉", "✨", "⭐", "🚀"];
|
||||
const EMOJI_POSITIONS = EMOJIS.map((_, index) => ({
|
||||
x: ((index * 73) % 320) - 160,
|
||||
y: ((index * 47) % 240) - 120,
|
||||
rotate: ((index * 31) % 80) - 40,
|
||||
}));
|
||||
|
||||
export function StepWaiting({ agentInstanceId, botUsername, connectedAt, onFinish }: Props) {
|
||||
const { received } = useTelegramFirstMessage({
|
||||
|
|
@ -22,15 +26,7 @@ export function StepWaiting({ agentInstanceId, botUsername, connectedAt, onFinis
|
|||
enabled: true,
|
||||
});
|
||||
|
||||
const positions = useMemo(
|
||||
() =>
|
||||
EMOJIS.map(() => ({
|
||||
x: (Math.random() - 0.5) * 320,
|
||||
y: (Math.random() - 0.5) * 240,
|
||||
rotate: (Math.random() - 0.5) * 80,
|
||||
})),
|
||||
[],
|
||||
);
|
||||
const positions = EMOJI_POSITIONS;
|
||||
|
||||
return (
|
||||
<div className="px-6 py-8 max-w-xl mx-auto">
|
||||
|
|
@ -56,11 +52,7 @@ export function StepWaiting({ agentInstanceId, botUsername, connectedAt, onFinis
|
|||
</div>
|
||||
|
||||
<Button asChild size="lg" className="mt-6">
|
||||
<a
|
||||
href={`https://t.me/${botUsername}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<a href={`https://t.me/${botUsername}`} target="_blank" rel="noopener noreferrer">
|
||||
Abrir meu bot no Telegram <ExternalLink className="ml-2 h-4 w-4" />
|
||||
</a>
|
||||
</Button>
|
||||
|
|
@ -103,8 +95,8 @@ export function StepWaiting({ agentInstanceId, botUsername, connectedAt, onFinis
|
|||
</div>
|
||||
<h2 className="mt-4 text-2xl font-bold tracking-tight">🎉 Mika conectado!</h2>
|
||||
<p className="mt-2 text-muted-foreground">
|
||||
Você recebeu a primeira resposta do seu agente. Ele ainda está em modo de teste,
|
||||
mas em breve vai responder de verdade.
|
||||
Você recebeu a primeira resposta do seu agente. Ele ainda está em modo de teste, mas
|
||||
em breve vai responder de verdade.
|
||||
</p>
|
||||
|
||||
<Button size="lg" className="mt-8 min-w-56" onClick={onFinish}>
|
||||
|
|
|
|||
|
|
@ -638,10 +638,14 @@ const SidebarMenuSkeleton = React.forwardRef<
|
|||
showIcon?: boolean;
|
||||
}
|
||||
>(({ className, showIcon = false, ...props }, ref) => {
|
||||
// Random width between 50 to 90%.
|
||||
const id = React.useId();
|
||||
const width = React.useMemo(() => {
|
||||
return `${Math.floor(Math.random() * 40) + 50}%`;
|
||||
}, []);
|
||||
let hash = 0;
|
||||
for (let i = 0; i < id.length; i += 1) {
|
||||
hash = (hash * 31 + id.charCodeAt(i)) % 40;
|
||||
}
|
||||
return `${hash + 50}%`;
|
||||
}, [id]);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -142,16 +142,29 @@ export function useCreateCronjob() {
|
|||
});
|
||||
}
|
||||
|
||||
export async function markCronjobRuntimeSyncError(id: string, detail: string) {
|
||||
const message = detail.slice(0, 2000);
|
||||
const { error } = await supabase
|
||||
.from("scheduled_jobs")
|
||||
.update({
|
||||
status: "error",
|
||||
auto_paused_reason: "Falha ao sincronizar esta automação com o runtime do agente.",
|
||||
runtime_state: "error",
|
||||
runtime_last_status: "error",
|
||||
runtime_last_error: message,
|
||||
})
|
||||
.eq("id", id);
|
||||
|
||||
if (error) throw error;
|
||||
}
|
||||
|
||||
export function useUpdateCronjobStatus() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async ({ id, status }: { id: string; status: "active" | "paused" }) => {
|
||||
const update: { status: "active" | "paused"; auto_paused_reason?: null } = { status };
|
||||
if (status === "paused") update.auto_paused_reason = null;
|
||||
const { error } = await supabase
|
||||
.from("scheduled_jobs")
|
||||
.update(update)
|
||||
.eq("id", id);
|
||||
const { error } = await supabase.from("scheduled_jobs").update(update).eq("id", id);
|
||||
if (error) throw error;
|
||||
},
|
||||
onSuccess: () => {
|
||||
|
|
|
|||
|
|
@ -89,11 +89,11 @@ function IntegrationDetailPage() {
|
|||
</Link>
|
||||
</Button>
|
||||
<div className="rounded-xl border border-border bg-card p-8 text-center">
|
||||
<p className="text-muted-foreground">
|
||||
Você ainda não conectou {mcp.name}.
|
||||
</p>
|
||||
<p className="text-muted-foreground">Você ainda não conectou {mcp.name}.</p>
|
||||
<Button asChild className="mt-4">
|
||||
<Link to="/painel/integracoes" search={{}}>Conectar</Link>
|
||||
<Link to="/painel/integracoes" search={{}}>
|
||||
Conectar
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -248,9 +248,7 @@ function IntegrationDetailPage() {
|
|||
className="flex items-center justify-between text-sm border-b border-border last:border-0 pb-2 last:pb-0"
|
||||
>
|
||||
<span>{j.name}</span>
|
||||
<Badge variant={j.status === "active" ? "success" : "secondary"}>
|
||||
{j.status}
|
||||
</Badge>
|
||||
<Badge variant={j.status === "active" ? "success" : "secondary"}>{j.status}</Badge>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
|
@ -271,25 +269,20 @@ function IntegrationDetailPage() {
|
|||
{(status === "expired" || status === "revoked" || status === "error") && (
|
||||
<Button
|
||||
onClick={async () => {
|
||||
const { data, error } = await invokeFunction<{ authorize_url: string }>(
|
||||
"oauth-start",
|
||||
{ mcp_slug: mcp.slug },
|
||||
);
|
||||
if (error || !data?.authorize_url) {
|
||||
const { data, error } = await invokeFunction<{ auth_url: string }>("oauth-start", {
|
||||
mcp_slug: mcp.slug,
|
||||
});
|
||||
if (error || !data?.auth_url) {
|
||||
toast.error(error?.message ?? "Falha ao iniciar reconexão.");
|
||||
return;
|
||||
}
|
||||
window.location.href = data.authorize_url;
|
||||
window.location.href = data.auth_url;
|
||||
}}
|
||||
>
|
||||
Reconectar
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={() => setDisconnectOpen(true)}
|
||||
className="ml-auto"
|
||||
>
|
||||
<Button variant="destructive" onClick={() => setDisconnectOpen(true)} className="ml-auto">
|
||||
<Unplug className="h-4 w-4 mr-2" /> Desconectar
|
||||
</Button>
|
||||
</div>
|
||||
|
|
@ -300,14 +293,12 @@ function IntegrationDetailPage() {
|
|||
setDisconnectOpen(o);
|
||||
if (!o) {
|
||||
// se desconectou, volta para a lista
|
||||
queryClient
|
||||
.invalidateQueries({ queryKey: ["user-integrations"] })
|
||||
.then(() => {
|
||||
const stillConnected = integs.some((i) => i.id === integration.id);
|
||||
if (!stillConnected) {
|
||||
navigate({ to: "/painel/integracoes", search: {} });
|
||||
}
|
||||
});
|
||||
queryClient.invalidateQueries({ queryKey: ["user-integrations"] }).then(() => {
|
||||
const stillConnected = integs.some((i) => i.id === integration.id);
|
||||
if (!stillConnected) {
|
||||
navigate({ to: "/painel/integracoes", search: {} });
|
||||
}
|
||||
});
|
||||
}
|
||||
}}
|
||||
integrationId={integration.id}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue