"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; 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: !!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 } = useQuery({ queryKey: ["agent-detail", id], enabled: !!isAdmin, refetchInterval: 10_000, queryFn: async () => { 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, vps_pool:vps_pool_id(railway_project_id, railway_environment_id), profile:profiles!agent_instances_user_id_fkey(full_name, phone, onboarding_completed), subscription:subscriptions!subscriptions_user_id_fkey(plans(slug, name))`, ) .eq("id", id) .maybeSingle(); if (error) throw error; if (!data) return null; // Normalizar arrays vindos do PostgREST // deno-lint-ignore no-explicit-any const d = data as any; const profile = Array.isArray(d.profile) ? d.profile[0] ?? null : d.profile; const subscription = Array.isArray(d.subscription) ? d.subscription.find((s: { plans: unknown }) => s.plans) ?? d.subscription[0] ?? null : d.subscription; // Email não é acessível via client (RLS) — admin pode ver no Railway/Telegram const userEmail: string | null = null; return { ...d, profile, subscription, user_email: userEmail } as AgentDetail; }, }); 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; }, }); // ===== 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 = 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]); if (authLoading || roleLoading || agentLoading) { return (
); } if (!isAdmin) { return (

Acesso negado

); } 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