From db38398c21fd582aaf3fee9e86e1434c1cc5e236 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Thu, 23 Apr 2026 20:57:43 +0000 Subject: [PATCH 1/7] Changes Co-authored-by: domfelipe <53182096+domfelipe@users.noreply.github.com> --- src/integrations/supabase/types.ts | 3 + supabase/functions/_shared/railway.ts | 30 ++++ .../functions/update-agent-config/index.ts | 155 ++++++++++++++++++ ...9_e0832773-c162-4f85-9753-aa544c1f671c.sql | 2 + 4 files changed, 190 insertions(+) create mode 100644 supabase/functions/update-agent-config/index.ts create mode 100644 supabase/migrations/20260423205719_e0832773-c162-4f85-9753-aa544c1f671c.sql diff --git a/src/integrations/supabase/types.ts b/src/integrations/supabase/types.ts index 356c9b7..919f517 100644 --- a/src/integrations/supabase/types.ts +++ b/src/integrations/supabase/types.ts @@ -20,6 +20,7 @@ export type Database = { created_at: string id: string last_health_check_at: string | null + model_config: Json provisioned_at: string | null railway_service_id: string | null status: string @@ -43,6 +44,7 @@ export type Database = { created_at?: string id?: string last_health_check_at?: string | null + model_config?: Json provisioned_at?: string | null railway_service_id?: string | null status?: string @@ -66,6 +68,7 @@ export type Database = { created_at?: string id?: string last_health_check_at?: string | null + model_config?: Json provisioned_at?: string | null railway_service_id?: string | null status?: string diff --git a/supabase/functions/_shared/railway.ts b/supabase/functions/_shared/railway.ts index abfff3b..9db5bc4 100644 --- a/supabase/functions/_shared/railway.ts +++ b/supabase/functions/_shared/railway.ts @@ -135,6 +135,36 @@ export async function deployRailwayService(opts: { } } +/** + * Upsert de várias variáveis de uma vez no serviço Railway. + * Mais eficiente que múltiplos variableUpsert sequenciais. + */ +export async function upsertRailwayVariableCollection(opts: { + token: string; + serviceId: string; + environmentId: string; + projectId: string; + variables: Record; + replace?: boolean; +}): Promise { + const mutation = ` + mutation VariableCollectionUpsert($input: VariableCollectionUpsertInput!) { + variableCollectionUpsert(input: $input) + } + `; + const input = { + projectId: opts.projectId, + environmentId: opts.environmentId, + serviceId: opts.serviceId, + variables: opts.variables, + replace: opts.replace ?? false, + }; + const res = await railwayQuery(mutation, { input }, opts.token); + if (res.errors?.length) { + throw new Error(`variableCollectionUpsert failed: ${JSON.stringify(res.errors)}`); + } +} + /** * Upsert de uma única variável de ambiente no serviço Railway. * Para "remover" o efeito de uma variável boolean, passe value="" (string vazia). diff --git a/supabase/functions/update-agent-config/index.ts b/supabase/functions/update-agent-config/index.ts new file mode 100644 index 0000000..f0637dc --- /dev/null +++ b/supabase/functions/update-agent-config/index.ts @@ -0,0 +1,155 @@ +// update-agent-config +// Atualiza variáveis de ambiente (SOUL, modelo, STT, TTS) de um agente já provisionado +// e dispara redeploy. Apenas admins podem chamar. +// verify_jwt = true: precisa de JWT válido + check de role admin. + +import { createClient } from "https://esm.sh/@supabase/supabase-js@2.45.4"; +import { corsHeaders } from "../_shared/cors.ts"; +import { + deployRailwayService, + getServiceContext, + upsertRailwayVariableCollection, +} from "../_shared/railway.ts"; + +interface RequestBody { + agent_instance_id: string; + agent_name?: string; + soul_content: string; + model: string; + stt_provider: string; + tts_provider: string; +} + +const SUPABASE_URL = Deno.env.get("SUPABASE_URL")!; +const SUPABASE_SERVICE_ROLE_KEY = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!; +const SUPABASE_ANON_KEY = Deno.env.get("SUPABASE_ANON_KEY")!; +const RAILWAY_API_TOKEN = Deno.env.get("RAILWAY_API_TOKEN"); + +Deno.serve(async (req) => { + if (req.method === "OPTIONS") return new Response(null, { headers: corsHeaders }); + + if (!RAILWAY_API_TOKEN) { + return jsonResponse(500, { error: "RAILWAY_API_TOKEN not configured" }); + } + + // 1) Auth: extrair JWT e validar admin + const authHeader = req.headers.get("Authorization") ?? ""; + const jwt = authHeader.replace(/^Bearer\s+/i, ""); + if (!jwt) return jsonResponse(401, { error: "missing authorization" }); + + const userClient = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, { + global: { headers: { Authorization: `Bearer ${jwt}` } }, + auth: { persistSession: false, autoRefreshToken: false }, + }); + const { data: userData, error: userErr } = await userClient.auth.getUser(); + if (userErr || !userData?.user) { + return jsonResponse(401, { error: "invalid token" }); + } + + const supabase = createClient(SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, { + auth: { persistSession: false, autoRefreshToken: false }, + }); + + const { data: isAdmin, error: roleErr } = await supabase.rpc("has_role", { + _user_id: userData.user.id, + _role: "admin", + }); + if (roleErr || !isAdmin) { + return jsonResponse(403, { error: "admin role required" }); + } + + // 2) Validar body + let body: RequestBody; + try { + body = await req.json(); + } catch { + return jsonResponse(400, { error: "invalid json body" }); + } + + if (!body.agent_instance_id) return jsonResponse(400, { error: "agent_instance_id required" }); + if (!body.soul_content || body.soul_content.length < 50) { + return jsonResponse(400, { error: "soul_content too short (min 50 chars)" }); + } + if (!body.model) return jsonResponse(400, { error: "model required" }); + + // 3) Carregar agent + pool + const { data: agent, error: agentErr } = await supabase + .from("agent_instances") + .select("id, railway_service_id, vps_pool_id, vps_pool:vps_pool_id(railway_project_id, railway_environment_id)") + .eq("id", body.agent_instance_id) + .maybeSingle(); + + if (agentErr || !agent) { + return jsonResponse(404, { error: "agent_instance not found" }); + } + if (!agent.railway_service_id) { + return jsonResponse(409, { error: "agent has no railway_service_id — provision first" }); + } + + // deno-lint-ignore no-explicit-any + const pool = (agent as any).vps_pool; + let projectId: string | null = pool?.railway_project_id ?? null; + let environmentId: string | null = pool?.railway_environment_id ?? null; + + if (!projectId || !environmentId) { + const ctx = await getServiceContext({ token: RAILWAY_API_TOKEN, serviceId: agent.railway_service_id }); + projectId = projectId ?? ctx.projectId; + environmentId = environmentId ?? ctx.environmentId; + } + + if (!projectId || !environmentId) { + return jsonResponse(500, { error: "failed to resolve railway project/environment" }); + } + + // 4) Upsert variáveis + const variables: Record = { + HERMES_SOUL_OVERRIDE: body.soul_content, + HERMES_MODEL: body.model, + HERMES_FALLBACK_MODEL: "openrouter/google/gemma-4-31b-it", + HERMES_STT_PROVIDER: body.stt_provider || "local", + HERMES_TTS_PROVIDER: body.tts_provider || "disabled", + }; + + try { + await upsertRailwayVariableCollection({ + token: RAILWAY_API_TOKEN, + serviceId: agent.railway_service_id, + environmentId, + projectId, + variables, + }); + + await deployRailwayService({ + token: RAILWAY_API_TOKEN, + serviceId: agent.railway_service_id, + environmentId, + }); + } catch (e) { + const msg = e instanceof Error ? e.message : String(e); + console.error("railway update failed:", msg); + return jsonResponse(500, { error: "railway update failed", detail: msg }); + } + + // 5) Persistir model_config + await supabase + .from("agent_instances") + .update({ + model_config: { + provider: body.model, + stt: body.stt_provider || "local", + tts: body.tts_provider || "disabled", + agent_name: body.agent_name ?? null, + }, + updated_at: new Date().toISOString(), + }) + .eq("id", agent.id); + + return jsonResponse(200, { success: true, redeploying: true }); +}); + +function jsonResponse(status: number, body: unknown) { + return new Response(JSON.stringify(body), { + status, + headers: { ...corsHeaders, "Content-Type": "application/json" }, + }); +} diff --git a/supabase/migrations/20260423205719_e0832773-c162-4f85-9753-aa544c1f671c.sql b/supabase/migrations/20260423205719_e0832773-c162-4f85-9753-aa544c1f671c.sql new file mode 100644 index 0000000..af1b1b9 --- /dev/null +++ b/supabase/migrations/20260423205719_e0832773-c162-4f85-9753-aa544c1f671c.sql @@ -0,0 +1,2 @@ +ALTER TABLE public.agent_instances + ADD COLUMN IF NOT EXISTS model_config jsonb NOT NULL DEFAULT '{}'::jsonb; \ No newline at end of file From ff481ef92a88a244fd565c710dfcf1a2482ce111 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Thu, 23 Apr 2026 20:57:54 +0000 Subject: [PATCH 2/7] Changes Co-authored-by: domfelipe <53182096+domfelipe@users.noreply.github.com> --- supabase/functions/provision-agent/index.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/supabase/functions/provision-agent/index.ts b/supabase/functions/provision-agent/index.ts index 69bca68..ab64e3c 100644 --- a/supabase/functions/provision-agent/index.ts +++ b/supabase/functions/provision-agent/index.ts @@ -15,6 +15,11 @@ import { interface RequestBody { agent_instance_id: string; + agent_name?: string; + soul_content?: string; + model?: string; + stt_provider?: string; + tts_provider?: string; } const SUPABASE_URL = Deno.env.get("SUPABASE_URL")!; From 245582d3186fe329030a9991b352549779404ad5 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Thu, 23 Apr 2026 20:58:14 +0000 Subject: [PATCH 3/7] Changes Co-authored-by: domfelipe <53182096+domfelipe@users.noreply.github.com> --- supabase/functions/provision-agent/index.ts | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/supabase/functions/provision-agent/index.ts b/supabase/functions/provision-agent/index.ts index ab64e3c..ae5d28e 100644 --- a/supabase/functions/provision-agent/index.ts +++ b/supabase/functions/provision-agent/index.ts @@ -83,7 +83,7 @@ Deno.serve(async (req) => { const fullName = (profile?.full_name?.trim() || "Usuário").toString(); const firstName = fullName.split(" ")[0] || "Usuário"; - const agentName = `Mika de ${firstName}`; + const agentName = body.agent_name?.trim() || `Mika de ${firstName}`; // 1c) Carregar subscription ativa (para definir modelo Pro vs Basic) const { data: subscription } = await supabase @@ -162,7 +162,15 @@ Deno.serve(async (req) => { // 6) Montar variáveis de ambiente do container const hasChatId = !!agent.telegram_user_chat_id; - const soulContent = `Você se chama ${agentName}. 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.`; + const defaultSoul = `Você se chama ${agentName}. 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.`; + const soulContent = body.soul_content?.trim() || defaultSoul; + + const defaultModel = isPro + ? "openrouter/google/gemma-4-31b-it" + : "openrouter/google/gemma-4-27b-a4b-it"; + const model = body.model || defaultModel; + const sttProvider = body.stt_provider || "local"; + const ttsProvider = body.tts_provider || "disabled"; const envVars: Record = { TELEGRAM_BOT_TOKEN: telegramBotToken, @@ -170,12 +178,11 @@ Deno.serve(async (req) => { TELEGRAM_HOME_CHANNEL: hasChatId ? String(agent.telegram_user_chat_id) : "", GATEWAY_ALLOW_ALL_USERS: hasChatId ? "false" : "true", HERMES_SOUL_MD: soulContent, - HERMES_TTS_PROVIDER: "disabled", - HERMES_STT_PROVIDER: "local", + HERMES_SOUL_OVERRIDE: soulContent, + HERMES_TTS_PROVIDER: ttsProvider, + HERMES_STT_PROVIDER: sttProvider, OPENROUTER_API_KEY, - HERMES_MODEL: isPro - ? "openrouter/google/gemma-4-31b-it" - : "openrouter/google/gemma-4-27b-a4b-it", + HERMES_MODEL: model, HERMES_FALLBACK_MODEL: "openrouter/google/gemma-4-31b-it", API_SERVER_ENABLED: "false", HERMES_HOME: "/opt/data", From 885b2ebe1f6088b8cd3f846e74713578ce2ea0f5 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Thu, 23 Apr 2026 20:58:24 +0000 Subject: [PATCH 4/7] Changes Co-authored-by: domfelipe <53182096+domfelipe@users.noreply.github.com> --- supabase/functions/provision-agent/index.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/supabase/functions/provision-agent/index.ts b/supabase/functions/provision-agent/index.ts index ae5d28e..2595e2e 100644 --- a/supabase/functions/provision-agent/index.ts +++ b/supabase/functions/provision-agent/index.ts @@ -223,7 +223,16 @@ Deno.serve(async (req) => { // 8) Persistir railway_service_id no agent_instance e no job (status='running') await supabase .from("agent_instances") - .update({ railway_service_id: railwayServiceId, vps_pool_id: pool.id }) + .update({ + railway_service_id: railwayServiceId, + vps_pool_id: pool.id, + model_config: { + provider: model, + stt: sttProvider, + tts: ttsProvider, + agent_name: agentName, + }, + }) .eq("id", agent.id); await supabase From 9e6c8a482e3e710fa6406fbdc231d07cc7ff4e0d Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Thu, 23 Apr 2026 20:59:03 +0000 Subject: [PATCH 5/7] Changes Co-authored-by: domfelipe <53182096+domfelipe@users.noreply.github.com> --- src/routes/admin.tsx | 88 +++++++++++++++++++++++--------------------- 1 file changed, 47 insertions(+), 41 deletions(-) diff --git a/src/routes/admin.tsx b/src/routes/admin.tsx index 72edeb2..df5a5d0 100644 --- a/src/routes/admin.tsx +++ b/src/routes/admin.tsx @@ -9,8 +9,8 @@ import { Loader2, PlayCircle, PauseCircle, - RotateCw, Server, + Settings, ShieldAlert, } from "lucide-react"; import { supabase } from "@/integrations/supabase/client"; @@ -43,6 +43,8 @@ interface AgentRow { vps_pool_id: string | null; created_at: string; provisioned_at: string | null; + profile: { full_name: string | null } | null; + subscription: { plans: { slug: string; name: string } | null } | null; } function AdminPage() { @@ -65,18 +67,30 @@ function AdminPage() { }); const { data: agents, isLoading: agentsLoading } = useQuery({ - queryKey: ["admin-agents"], + queryKey: ["agents-admin"], enabled: !!isAdmin, + refetchInterval: 15000, queryFn: async () => { const { data, error } = await supabase .from("agent_instances") .select( - "id, user_id, status, uuid_tenant, telegram_bot_username, telegram_bot_token_vault_id, railway_service_id, vps_pool_id, created_at, provisioned_at", + `id, user_id, status, uuid_tenant, telegram_bot_username, telegram_bot_token_vault_id, + railway_service_id, vps_pool_id, created_at, provisioned_at, + profile:profiles!agent_instances_user_id_fkey(full_name), + subscription:subscriptions!subscriptions_user_id_fkey(plans(slug, name))`, ) .order("created_at", { ascending: false }) .limit(100); if (error) throw error; - return data as AgentRow[]; + // Pegar apenas a subscription ativa (primeira) — o relacionamento retorna array + // deno-lint-ignore no-explicit-any + return (data as any[]).map((a) => ({ + ...a, + profile: Array.isArray(a.profile) ? a.profile[0] ?? null : a.profile, + subscription: Array.isArray(a.subscription) + ? a.subscription.find((s: { plans: unknown }) => s.plans) ?? a.subscription[0] ?? null + : a.subscription, + })) as AgentRow[]; }, }); @@ -115,10 +129,7 @@ function AdminPage() { ); } - async function action( - fn: "provision-agent" | "suspend-agent" | "resume-agent", - agentId: string, - ) { + async function action(fn: "suspend-agent" | "resume-agent", agentId: string) { setBusy(agentId + fn); const { data, error } = await invokeFunction<{ ok?: boolean; error?: string }>(fn, { agent_instance_id: agentId, @@ -130,20 +141,20 @@ function AdminPage() { toast.error(`${fn}: ${data.error}`); } else { toast.success(`${fn} executado com sucesso`); - queryClient.invalidateQueries({ queryKey: ["admin-agents"] }); + queryClient.invalidateQueries({ queryKey: ["agents-admin"] }); } } return (
-
+

Admin · Mika

- Gerencie agentes provisionados, suspenda e reative containers Railway. + Configure e gerencie agentes provisionados.

- )} + {a.status === "active" && ( )} {a.status === "suspended" && ( @@ -244,7 +241,7 @@ function AdminPage() { ) : ( )} - Reativar + Reativar )} @@ -267,3 +264,12 @@ function StatusBadge({ status }: { status: string }) { if (status === "error") return Erro; return {status}; } + +function PlanBadge({ slug }: { slug: string | null }) { + if (!slug) return Sem plano; + if (slug === "professional" || slug === "enterprise") + return {slug}; + if (slug === "starter") return {slug}; + return {slug}; +} + From 08a69cd37825bd141e69f500af551b7f4ea55e60 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Thu, 23 Apr 2026 21:00:27 +0000 Subject: [PATCH 6/7] Changes Co-authored-by: domfelipe <53182096+domfelipe@users.noreply.github.com> --- src/routes/admin.agente.$id.tsx | 632 ++++++++++++++++++++++++++++++++ 1 file changed, 632 insertions(+) create mode 100644 src/routes/admin.agente.$id.tsx diff --git a/src/routes/admin.agente.$id.tsx b/src/routes/admin.agente.$id.tsx new file mode 100644 index 0000000..2f268cd --- /dev/null +++ b/src/routes/admin.agente.$id.tsx @@ -0,0 +1,632 @@ +"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; + + // Buscar email via auth (admin) + let userEmail: string | null = null; + try { + const { data: u } = await supabase.auth.admin.getUserById(d.user_id); + userEmail = u?.user?.email ?? null; + } catch { + // Sem permissão admin no client — ignorar + } + + 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

+
+ +
+ +