"use client"; import { createFileRoute, Link, useNavigate } from "@tanstack/react-router"; import { useEffect, useMemo, useState } from "react"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { toast } from "sonner"; import { ArrowLeft, ExternalLink, Loader2, Rocket, Save, ShieldAlert, Sparkles, } from "lucide-react"; import { supabase } from "@/integrations/supabase/client"; import { useAuth } from "@/hooks/use-auth"; import { invokeFunction } from "@/lib/invoke-function"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Skeleton } from "@/components/ui/skeleton"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; export const Route = createFileRoute("/admin/agente/$id")({ component: AgentDetailPage, }); const MODEL_OPTIONS = [ { value: "openrouter/google/gemma-4-27b-a4b-it", label: "Gemma 4 27B — Rápido e gratuito (Basic/Starter)", plans: ["basic", "starter"], }, { value: "openrouter/google/gemma-4-31b-it", label: "Gemma 4 31B — Mais capaz (Professional)", plans: ["professional", "enterprise"], }, ]; interface AgentDetail { id: string; user_id: string; uuid_tenant: string; status: string; telegram_bot_username: string | null; telegram_user_chat_id: number | null; railway_service_id: string | null; vps_pool_id: string | null; provisioned_at: string | null; created_at: string; model_config: Record | null; agent_name: string | null; vps_pool: { railway_project_id: string | null; railway_environment_id: string | null; } | null; profile: { full_name: string | null; phone: string | null; onboarding_completed: boolean; } | null; user_email: string | null; subscription: { plans: { slug: string; name: string } | null } | null; } function AgentDetailPage() { const { id } = Route.useParams(); const { user, loading: authLoading } = useAuth(); const navigate = useNavigate(); const queryClient = useQueryClient(); const { data: isAdmin, isLoading: roleLoading } = useQuery({ queryKey: ["is-admin", user?.id], enabled: !authLoading && !!user, queryFn: async () => { const { data, error } = await supabase.rpc("has_role", { _user_id: user!.id, _role: "admin", }); if (error) throw error; return data === true; }, }); const { data: agent, isLoading: agentLoading, error: agentError } = useQuery({ queryKey: ["agent-detail", id], enabled: !authLoading && !!user && isAdmin === true, refetchInterval: 10_000, queryFn: async () => { console.log("[admin.agente] fetching agent detail", { id }); const { data, error } = await supabase .from("agent_instances") .select( "id, user_id, uuid_tenant, status, telegram_bot_username, telegram_user_chat_id, railway_service_id, vps_pool_id, provisioned_at, created_at, model_config, agent_name", ) .eq("id", id) .maybeSingle(); if (error) { console.error("[admin.agente] agent query error", error); throw error; } if (!data) { console.warn("[admin.agente] agent not found", { id }); return null; } // Buscar profile separadamente (evita join com possíveis problemas de FK) const { data: profileData } = await supabase .from("profiles") .select("full_name, phone, onboarding_completed") .eq("id", data.user_id) .maybeSingle(); // Buscar vps_pool separadamente se houver let vpsPool: { railway_project_id: string | null; railway_environment_id: string | null } | null = null; if (data.vps_pool_id) { const { data: poolData } = await supabase .from("vps_pool") .select("railway_project_id, railway_environment_id") .eq("id", data.vps_pool_id) .maybeSingle(); vpsPool = poolData ?? null; } const { data: subscriptionData, error: subscriptionError } = await supabase .from("subscriptions") .select("status, plans(slug, name)") .eq("user_id", data.user_id) .order("created_at", { ascending: false }) .limit(1) .maybeSingle(); if (subscriptionError) throw subscriptionError; const subscription = subscriptionData ? { ...subscriptionData, plans: Array.isArray(subscriptionData.plans) ? subscriptionData.plans[0] ?? null : subscriptionData.plans, } : null; const result = { ...data, vps_pool: vpsPool, profile: profileData ?? null, subscription, user_email: null as string | null, } as AgentDetail; console.log("[admin.agente] agent loaded", { id: result.id, status: result.status }); return result; }, }); const { data: jobs } = useQuery({ queryKey: ["provisioning-jobs", id], enabled: !!isAdmin, refetchInterval: 10_000, queryFn: async () => { const { data, error } = await supabase .from("provisioning_jobs") .select("id, status, created_at, error_message, attempt") .eq("agent_instance_id", id) .order("created_at", { ascending: false }) .limit(5); if (error) throw error; return data; }, }); // Backfill: se o agente não tem telegram_user_chat_id mas já recebeu mensagens, // pega a primeira mensagem incoming e popula automaticamente. useEffect(() => { if (!agent || !isAdmin) return; if (agent.telegram_user_chat_id) return; let cancelled = false; (async () => { const { data: msg } = await supabase .from("telegram_messages_log") .select("telegram_chat_id") .eq("agent_instance_id", id) .eq("direction", "incoming") .order("created_at", { ascending: true }) .limit(1) .maybeSingle(); if (cancelled || !msg?.telegram_chat_id) return; const { error: updErr } = await supabase .from("agent_instances") .update({ telegram_user_chat_id: msg.telegram_chat_id }) .eq("id", id); if (!updErr) { queryClient.invalidateQueries({ queryKey: ["agent-detail", id] }); } })(); return () => { cancelled = true; }; }, [agent, isAdmin, id, queryClient]); // ===== Estado do formulário ===== const fullName = agent?.profile?.full_name?.trim() || "Usuário"; const firstName = fullName.split(" ")[0] || "Usuário"; const planSlug = agent?.subscription?.plans?.slug ?? "basic"; const isPro = ["professional", "enterprise"].includes(planSlug); const defaultModel = isPro ? "openrouter/google/gemma-4-31b-it" : "openrouter/google/gemma-4-27b-a4b-it"; const cfg = (agent?.model_config ?? {}) as Record; const defaultAgentName = agent?.agent_name?.trim() || cfg.agent_name || `Mika de ${firstName}`; const defaultSoul = useMemo( () => `Você se chama ${defaultAgentName}. Você é um assistente pessoal de IA criado pela DOMCO para ${fullName}. Você é proativo, direto e fala sempre em português brasileiro. Você ajuda ${firstName} a ser mais produtivo — gerenciando emails, agenda, tarefas e automatizando o que puder. Seja conciso nas respostas via Telegram. Nunca se identifique como Hermes ou como produto da Nous Research — você é Mika.`, [defaultAgentName, fullName, firstName], ); const [agentName, setAgentName] = useState(""); const [soul, setSoul] = useState(""); const [model, setModel] = useState(""); const [stt, setStt] = useState("local"); const [tts, setTts] = useState("disabled"); const [busy, setBusy] = useState(false); const [initialized, setInitialized] = useState(false); useEffect(() => { if (!agent || initialized) return; setAgentName(defaultAgentName); setSoul(defaultSoul); setModel(cfg.provider || defaultModel); setStt(cfg.stt || "local"); setTts(cfg.tts || "disabled"); setInitialized(true); }, [agent, initialized, defaultAgentName, defaultSoul, cfg.provider, cfg.stt, cfg.tts, defaultModel]); // ===== Auth guard ===== useEffect(() => { if (!authLoading && !user) { navigate({ to: "/login", search: { redirect: `/admin/agente/${id}` } }); } }, [authLoading, user, navigate, id]); useEffect(() => { if (!roleLoading && isAdmin === false) { navigate({ to: "/painel" }); } }, [roleLoading, isAdmin, navigate]); console.log("[admin.agente] render", { id, authLoading, roleLoading, agentLoading, isAdmin, hasUser: !!user, hasAgent: !!agent, agentError, }); if (authLoading || (user && roleLoading) || (isAdmin === true && agentLoading)) { return (
); } if (!user) { return null; } if (isAdmin === false) { return (

Acesso negado

); } if (agentError) { return (

Erro ao carregar agente

{agentError.message}

); } if (!agent) { return (

Agente não encontrado

); } const needsProvision = !agent.railway_service_id && agent.status !== "active"; async function handleProvision() { if (soul.length < 100) { toast.error("A personalidade precisa ter pelo menos 100 caracteres."); return; } setBusy(true); const { data, error } = await invokeFunction<{ success?: boolean; error?: string }>( "provision-agent", { agent_instance_id: id, agent_name: agentName, soul_content: soul, model, stt_provider: stt, tts_provider: tts, }, ); setBusy(false); if (error || data?.error) { toast.error(`Falha ao provisionar: ${error?.message || data?.error}`); return; } toast.success("Agente provisionado com sucesso! O cliente será notificado."); queryClient.invalidateQueries({ queryKey: ["agent-detail", id] }); queryClient.invalidateQueries({ queryKey: ["agents-admin"] }); queryClient.invalidateQueries({ queryKey: ["provisioning-jobs", id] }); } async function handleUpdate() { if (soul.length < 100) { toast.error("A personalidade precisa ter pelo menos 100 caracteres."); return; } setBusy(true); const { data, error } = await invokeFunction<{ success?: boolean; error?: string }>( "update-agent-config", { agent_instance_id: id, agent_name: agentName, soul_content: soul, model, stt_provider: stt, tts_provider: tts, }, ); setBusy(false); if (error || data?.error) { toast.error(`Falha ao atualizar: ${error?.message || data?.error}`); return; } toast.success("Configuração atualizada! Redeploy disparado no Railway."); queryClient.invalidateQueries({ queryKey: ["agent-detail", id] }); queryClient.invalidateQueries({ queryKey: ["agents-admin"] }); } return (

Configurar agente

{agent.uuid_tenant.slice(0, 8)} · {agent.profile?.full_name || "Sem nome"}

{/* ===== Coluna esquerda — Formulário ===== */}
{/* Cliente */}

Informações do cliente

Plano
Bot Telegram
{agent.telegram_bot_username ? ( @{agent.telegram_bot_username} ) : ( Não conectado )}
{/* Configuração */}

Configuração do agente

setAgentName(e.target.value.slice(0, 50))} maxLength={50} placeholder={`Mika de ${firstName}`} />

{agentName.length}/50