mirror of
https://github.com/domfelipe/mika-agent-assist.git
synced 2026-08-07 07:36:41 +00:00
Criou /bem-vindo e migrou DB
X-Lovable-Edit-ID: edt-73315edb-9a59-4b3d-b193-cc9361d33393 Co-authored-by: domfelipe <53182096+domfelipe@users.noreply.github.com>
This commit is contained in:
commit
eed4edcd1c
10 changed files with 552 additions and 17 deletions
|
|
@ -16,6 +16,8 @@ export interface AgentInstance {
|
||||||
telegram_first_message_received_at: string | null;
|
telegram_first_message_received_at: string | null;
|
||||||
telegram_connected_at: string | null;
|
telegram_connected_at: string | null;
|
||||||
telegram_onboarding_completed: boolean;
|
telegram_onboarding_completed: boolean;
|
||||||
|
onboarding_completed: boolean;
|
||||||
|
agent_name: string | null;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -31,7 +33,7 @@ export function useAgentInstance() {
|
||||||
const { data, error } = await supabase
|
const { data, error } = await supabase
|
||||||
.from("agent_instances")
|
.from("agent_instances")
|
||||||
.select(
|
.select(
|
||||||
"id, user_id, uuid_tenant, status, telegram_bot_username, telegram_bot_token_vault_id, telegram_webhook_configured, telegram_token_invalid, telegram_first_message_received_at, telegram_connected_at, telegram_onboarding_completed, created_at",
|
"id, user_id, uuid_tenant, status, telegram_bot_username, telegram_bot_token_vault_id, telegram_webhook_configured, telegram_token_invalid, telegram_first_message_received_at, telegram_connected_at, telegram_onboarding_completed, onboarding_completed, agent_name, created_at",
|
||||||
)
|
)
|
||||||
.eq("user_id", user!.id)
|
.eq("user_id", user!.id)
|
||||||
.maybeSingle();
|
.maybeSingle();
|
||||||
|
|
|
||||||
|
|
@ -16,11 +16,13 @@ export type Database = {
|
||||||
Tables: {
|
Tables: {
|
||||||
agent_instances: {
|
agent_instances: {
|
||||||
Row: {
|
Row: {
|
||||||
|
agent_name: string | null
|
||||||
container_name: string | null
|
container_name: string | null
|
||||||
created_at: string
|
created_at: string
|
||||||
id: string
|
id: string
|
||||||
last_health_check_at: string | null
|
last_health_check_at: string | null
|
||||||
model_config: Json
|
model_config: Json
|
||||||
|
onboarding_completed: boolean
|
||||||
provisioned_at: string | null
|
provisioned_at: string | null
|
||||||
railway_service_id: string | null
|
railway_service_id: string | null
|
||||||
status: string
|
status: string
|
||||||
|
|
@ -38,13 +40,16 @@ export type Database = {
|
||||||
uuid_tenant: string
|
uuid_tenant: string
|
||||||
vps_host: string | null
|
vps_host: string | null
|
||||||
vps_pool_id: string | null
|
vps_pool_id: string | null
|
||||||
|
welcome_message_sent_at: string | null
|
||||||
}
|
}
|
||||||
Insert: {
|
Insert: {
|
||||||
|
agent_name?: string | null
|
||||||
container_name?: string | null
|
container_name?: string | null
|
||||||
created_at?: string
|
created_at?: string
|
||||||
id?: string
|
id?: string
|
||||||
last_health_check_at?: string | null
|
last_health_check_at?: string | null
|
||||||
model_config?: Json
|
model_config?: Json
|
||||||
|
onboarding_completed?: boolean
|
||||||
provisioned_at?: string | null
|
provisioned_at?: string | null
|
||||||
railway_service_id?: string | null
|
railway_service_id?: string | null
|
||||||
status?: string
|
status?: string
|
||||||
|
|
@ -62,13 +67,16 @@ export type Database = {
|
||||||
uuid_tenant?: string
|
uuid_tenant?: string
|
||||||
vps_host?: string | null
|
vps_host?: string | null
|
||||||
vps_pool_id?: string | null
|
vps_pool_id?: string | null
|
||||||
|
welcome_message_sent_at?: string | null
|
||||||
}
|
}
|
||||||
Update: {
|
Update: {
|
||||||
|
agent_name?: string | null
|
||||||
container_name?: string | null
|
container_name?: string | null
|
||||||
created_at?: string
|
created_at?: string
|
||||||
id?: string
|
id?: string
|
||||||
last_health_check_at?: string | null
|
last_health_check_at?: string | null
|
||||||
model_config?: Json
|
model_config?: Json
|
||||||
|
onboarding_completed?: boolean
|
||||||
provisioned_at?: string | null
|
provisioned_at?: string | null
|
||||||
railway_service_id?: string | null
|
railway_service_id?: string | null
|
||||||
status?: string
|
status?: string
|
||||||
|
|
@ -86,6 +94,7 @@ export type Database = {
|
||||||
uuid_tenant?: string
|
uuid_tenant?: string
|
||||||
vps_host?: string | null
|
vps_host?: string | null
|
||||||
vps_pool_id?: string | null
|
vps_pool_id?: string | null
|
||||||
|
welcome_message_sent_at?: string | null
|
||||||
}
|
}
|
||||||
Relationships: [
|
Relationships: [
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import { Route as RedefinirSenhaRouteImport } from './routes/redefinir-senha'
|
||||||
import { Route as RecuperarSenhaRouteImport } from './routes/recuperar-senha'
|
import { Route as RecuperarSenhaRouteImport } from './routes/recuperar-senha'
|
||||||
import { Route as PainelRouteImport } from './routes/painel'
|
import { Route as PainelRouteImport } from './routes/painel'
|
||||||
import { Route as LoginRouteImport } from './routes/login'
|
import { Route as LoginRouteImport } from './routes/login'
|
||||||
|
import { Route as BemVindoRouteImport } from './routes/bem-vindo'
|
||||||
import { Route as AdminRouteImport } from './routes/admin'
|
import { Route as AdminRouteImport } from './routes/admin'
|
||||||
import { Route as IndexRouteImport } from './routes/index'
|
import { Route as IndexRouteImport } from './routes/index'
|
||||||
import { Route as PainelIndexRouteImport } from './routes/painel.index'
|
import { Route as PainelIndexRouteImport } from './routes/painel.index'
|
||||||
|
|
@ -59,6 +60,11 @@ const LoginRoute = LoginRouteImport.update({
|
||||||
path: '/login',
|
path: '/login',
|
||||||
getParentRoute: () => rootRouteImport,
|
getParentRoute: () => rootRouteImport,
|
||||||
} as any)
|
} as any)
|
||||||
|
const BemVindoRoute = BemVindoRouteImport.update({
|
||||||
|
id: '/bem-vindo',
|
||||||
|
path: '/bem-vindo',
|
||||||
|
getParentRoute: () => rootRouteImport,
|
||||||
|
} as any)
|
||||||
const AdminRoute = AdminRouteImport.update({
|
const AdminRoute = AdminRouteImport.update({
|
||||||
id: '/admin',
|
id: '/admin',
|
||||||
path: '/admin',
|
path: '/admin',
|
||||||
|
|
@ -158,6 +164,7 @@ const AdminAgenteIdRoute = AdminAgenteIdRouteImport.update({
|
||||||
export interface FileRoutesByFullPath {
|
export interface FileRoutesByFullPath {
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
'/admin': typeof AdminRouteWithChildren
|
'/admin': typeof AdminRouteWithChildren
|
||||||
|
'/bem-vindo': typeof BemVindoRoute
|
||||||
'/login': typeof LoginRoute
|
'/login': typeof LoginRoute
|
||||||
'/painel': typeof PainelRouteWithChildren
|
'/painel': typeof PainelRouteWithChildren
|
||||||
'/recuperar-senha': typeof RecuperarSenhaRoute
|
'/recuperar-senha': typeof RecuperarSenhaRoute
|
||||||
|
|
@ -183,6 +190,7 @@ export interface FileRoutesByFullPath {
|
||||||
}
|
}
|
||||||
export interface FileRoutesByTo {
|
export interface FileRoutesByTo {
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
|
'/bem-vindo': typeof BemVindoRoute
|
||||||
'/login': typeof LoginRoute
|
'/login': typeof LoginRoute
|
||||||
'/recuperar-senha': typeof RecuperarSenhaRoute
|
'/recuperar-senha': typeof RecuperarSenhaRoute
|
||||||
'/redefinir-senha': typeof RedefinirSenhaRoute
|
'/redefinir-senha': typeof RedefinirSenhaRoute
|
||||||
|
|
@ -208,6 +216,7 @@ export interface FileRoutesById {
|
||||||
__root__: typeof rootRouteImport
|
__root__: typeof rootRouteImport
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
'/admin': typeof AdminRouteWithChildren
|
'/admin': typeof AdminRouteWithChildren
|
||||||
|
'/bem-vindo': typeof BemVindoRoute
|
||||||
'/login': typeof LoginRoute
|
'/login': typeof LoginRoute
|
||||||
'/painel': typeof PainelRouteWithChildren
|
'/painel': typeof PainelRouteWithChildren
|
||||||
'/recuperar-senha': typeof RecuperarSenhaRoute
|
'/recuperar-senha': typeof RecuperarSenhaRoute
|
||||||
|
|
@ -236,6 +245,7 @@ export interface FileRouteTypes {
|
||||||
fullPaths:
|
fullPaths:
|
||||||
| '/'
|
| '/'
|
||||||
| '/admin'
|
| '/admin'
|
||||||
|
| '/bem-vindo'
|
||||||
| '/login'
|
| '/login'
|
||||||
| '/painel'
|
| '/painel'
|
||||||
| '/recuperar-senha'
|
| '/recuperar-senha'
|
||||||
|
|
@ -261,6 +271,7 @@ export interface FileRouteTypes {
|
||||||
fileRoutesByTo: FileRoutesByTo
|
fileRoutesByTo: FileRoutesByTo
|
||||||
to:
|
to:
|
||||||
| '/'
|
| '/'
|
||||||
|
| '/bem-vindo'
|
||||||
| '/login'
|
| '/login'
|
||||||
| '/recuperar-senha'
|
| '/recuperar-senha'
|
||||||
| '/redefinir-senha'
|
| '/redefinir-senha'
|
||||||
|
|
@ -285,6 +296,7 @@ export interface FileRouteTypes {
|
||||||
| '__root__'
|
| '__root__'
|
||||||
| '/'
|
| '/'
|
||||||
| '/admin'
|
| '/admin'
|
||||||
|
| '/bem-vindo'
|
||||||
| '/login'
|
| '/login'
|
||||||
| '/painel'
|
| '/painel'
|
||||||
| '/recuperar-senha'
|
| '/recuperar-senha'
|
||||||
|
|
@ -312,6 +324,7 @@ export interface FileRouteTypes {
|
||||||
export interface RootRouteChildren {
|
export interface RootRouteChildren {
|
||||||
IndexRoute: typeof IndexRoute
|
IndexRoute: typeof IndexRoute
|
||||||
AdminRoute: typeof AdminRouteWithChildren
|
AdminRoute: typeof AdminRouteWithChildren
|
||||||
|
BemVindoRoute: typeof BemVindoRoute
|
||||||
LoginRoute: typeof LoginRoute
|
LoginRoute: typeof LoginRoute
|
||||||
PainelRoute: typeof PainelRouteWithChildren
|
PainelRoute: typeof PainelRouteWithChildren
|
||||||
RecuperarSenhaRoute: typeof RecuperarSenhaRoute
|
RecuperarSenhaRoute: typeof RecuperarSenhaRoute
|
||||||
|
|
@ -357,6 +370,13 @@ declare module '@tanstack/react-router' {
|
||||||
preLoaderRoute: typeof LoginRouteImport
|
preLoaderRoute: typeof LoginRouteImport
|
||||||
parentRoute: typeof rootRouteImport
|
parentRoute: typeof rootRouteImport
|
||||||
}
|
}
|
||||||
|
'/bem-vindo': {
|
||||||
|
id: '/bem-vindo'
|
||||||
|
path: '/bem-vindo'
|
||||||
|
fullPath: '/bem-vindo'
|
||||||
|
preLoaderRoute: typeof BemVindoRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
'/admin': {
|
'/admin': {
|
||||||
id: '/admin'
|
id: '/admin'
|
||||||
path: '/admin'
|
path: '/admin'
|
||||||
|
|
@ -555,6 +575,7 @@ const PainelRouteWithChildren =
|
||||||
const rootRouteChildren: RootRouteChildren = {
|
const rootRouteChildren: RootRouteChildren = {
|
||||||
IndexRoute: IndexRoute,
|
IndexRoute: IndexRoute,
|
||||||
AdminRoute: AdminRouteWithChildren,
|
AdminRoute: AdminRouteWithChildren,
|
||||||
|
BemVindoRoute: BemVindoRoute,
|
||||||
LoginRoute: LoginRoute,
|
LoginRoute: LoginRoute,
|
||||||
PainelRoute: PainelRouteWithChildren,
|
PainelRoute: PainelRouteWithChildren,
|
||||||
RecuperarSenhaRoute: RecuperarSenhaRoute,
|
RecuperarSenhaRoute: RecuperarSenhaRoute,
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,7 @@ interface AgentDetail {
|
||||||
provisioned_at: string | null;
|
provisioned_at: string | null;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
model_config: Record<string, unknown> | null;
|
model_config: Record<string, unknown> | null;
|
||||||
|
agent_name: string | null;
|
||||||
vps_pool: {
|
vps_pool: {
|
||||||
railway_project_id: string | null;
|
railway_project_id: string | null;
|
||||||
railway_environment_id: string | null;
|
railway_environment_id: string | null;
|
||||||
|
|
@ -100,7 +101,7 @@ function AgentDetailPage() {
|
||||||
const { data, error } = await supabase
|
const { data, error } = await supabase
|
||||||
.from("agent_instances")
|
.from("agent_instances")
|
||||||
.select(
|
.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",
|
"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)
|
.eq("id", id)
|
||||||
.maybeSingle();
|
.maybeSingle();
|
||||||
|
|
@ -216,7 +217,7 @@ function AgentDetailPage() {
|
||||||
: "openrouter/google/gemma-4-27b-a4b-it";
|
: "openrouter/google/gemma-4-27b-a4b-it";
|
||||||
|
|
||||||
const cfg = (agent?.model_config ?? {}) as Record<string, string | undefined>;
|
const cfg = (agent?.model_config ?? {}) as Record<string, string | undefined>;
|
||||||
const defaultAgentName = cfg.agent_name || `Mika de ${firstName}`;
|
const defaultAgentName = agent?.agent_name?.trim() || cfg.agent_name || `Mika de ${firstName}`;
|
||||||
const defaultSoul = useMemo(
|
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.`,
|
`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.`,
|
||||||
|
|
@ -404,6 +405,10 @@ function AgentDetailPage() {
|
||||||
<h2 className="font-semibold text-lg">Informações do cliente</h2>
|
<h2 className="font-semibold text-lg">Informações do cliente</h2>
|
||||||
<dl className="grid grid-cols-1 sm:grid-cols-2 gap-3 text-sm">
|
<dl className="grid grid-cols-1 sm:grid-cols-2 gap-3 text-sm">
|
||||||
<Field label="Nome" value={agent.profile?.full_name || "—"} />
|
<Field label="Nome" value={agent.profile?.full_name || "—"} />
|
||||||
|
<Field
|
||||||
|
label="Nome do agente (escolhido pelo cliente)"
|
||||||
|
value={agent.agent_name || "—"}
|
||||||
|
/>
|
||||||
<Field label="Email" value={agent.user_email || "—"} />
|
<Field label="Email" value={agent.user_email || "—"} />
|
||||||
<Field label="Telefone" value={agent.profile?.phone || "—"} />
|
<Field label="Telefone" value={agent.profile?.phone || "—"} />
|
||||||
<div>
|
<div>
|
||||||
|
|
|
||||||
401
src/routes/bem-vindo.tsx
Normal file
401
src/routes/bem-vindo.tsx
Normal file
|
|
@ -0,0 +1,401 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { createFileRoute, useNavigate } from "@tanstack/react-router";
|
||||||
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
|
import {
|
||||||
|
ArrowRight,
|
||||||
|
CheckCircle2,
|
||||||
|
ExternalLink,
|
||||||
|
Loader2,
|
||||||
|
Sparkles,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
import { useAuth } from "@/hooks/use-auth";
|
||||||
|
import { useProfile } from "@/hooks/use-profile";
|
||||||
|
import { useAgentInstance } from "@/hooks/use-agent-instance";
|
||||||
|
import { supabase } from "@/integrations/supabase/client";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Logo } from "@/components/mika/Logo";
|
||||||
|
import { TelegramIcon } from "@/components/mika/telegram/TelegramIcon";
|
||||||
|
import { TelegramOnboardingWizard } from "@/components/mika/telegram/TelegramOnboardingWizard";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const WELCOME_DONE_KEY = "mika-welcome-done";
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/bem-vindo")({
|
||||||
|
component: WelcomePage,
|
||||||
|
});
|
||||||
|
|
||||||
|
function WelcomePage() {
|
||||||
|
const { user, loading: authLoading } = useAuth();
|
||||||
|
const { data: profile } = useProfile();
|
||||||
|
const { data: agent } = useAgentInstance();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const [step, setStep] = useState<1 | 2 | 3>(1);
|
||||||
|
const [agentName, setAgentName] = useState("");
|
||||||
|
const [saving, setSaving] = useState(false);
|
||||||
|
const [wizardOpen, setWizardOpen] = useState(false);
|
||||||
|
|
||||||
|
const fullName = (profile?.full_name || "").trim();
|
||||||
|
const firstName = useMemo(
|
||||||
|
() => (fullName.split(" ")[0] || "você").trim(),
|
||||||
|
[fullName],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Auth guard
|
||||||
|
useEffect(() => {
|
||||||
|
if (!authLoading && !user) {
|
||||||
|
navigate({ to: "/login", search: { redirect: "/bem-vindo" } });
|
||||||
|
}
|
||||||
|
}, [authLoading, user, navigate]);
|
||||||
|
|
||||||
|
// Pré-preenche o input com o default
|
||||||
|
useEffect(() => {
|
||||||
|
if (agentName) return;
|
||||||
|
if (agent?.agent_name) {
|
||||||
|
setAgentName(agent.agent_name);
|
||||||
|
} else if (firstName && firstName !== "você") {
|
||||||
|
setAgentName(`Mika de ${firstName}`);
|
||||||
|
}
|
||||||
|
}, [agent?.agent_name, firstName, agentName]);
|
||||||
|
|
||||||
|
// Etapa 1 → 2 automático em 3s
|
||||||
|
useEffect(() => {
|
||||||
|
if (step !== 1) return;
|
||||||
|
const t = setTimeout(() => setStep(2), 3000);
|
||||||
|
return () => clearTimeout(t);
|
||||||
|
}, [step]);
|
||||||
|
|
||||||
|
// Se já completou o onboarding, vai direto para /painel
|
||||||
|
useEffect(() => {
|
||||||
|
if (!agent) return;
|
||||||
|
if (agent.onboarding_completed) {
|
||||||
|
navigate({ to: "/painel", search: {} });
|
||||||
|
}
|
||||||
|
}, [agent, navigate]);
|
||||||
|
|
||||||
|
async function handleSaveName() {
|
||||||
|
const trimmed = agentName.trim();
|
||||||
|
if (trimmed.length < 2) {
|
||||||
|
toast.error("O nome precisa ter pelo menos 2 caracteres.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!agent) {
|
||||||
|
toast.error("Aguarde, ainda estamos preparando seu agente…");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setSaving(true);
|
||||||
|
const { error } = await supabase
|
||||||
|
.from("agent_instances")
|
||||||
|
.update({ agent_name: trimmed })
|
||||||
|
.eq("id", agent.id);
|
||||||
|
setSaving(false);
|
||||||
|
if (error) {
|
||||||
|
toast.error("Não foi possível salvar o nome. Tente novamente.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (user) {
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ["agent-instance", user.id] });
|
||||||
|
}
|
||||||
|
setStep(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function markWelcomeDone() {
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
window.localStorage.setItem(WELCOME_DONE_KEY, "1");
|
||||||
|
}
|
||||||
|
if (agent) {
|
||||||
|
await supabase
|
||||||
|
.from("agent_instances")
|
||||||
|
.update({ onboarding_completed: true })
|
||||||
|
.eq("id", agent.id);
|
||||||
|
if (user) {
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ["agent-instance", user.id] });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleConnectTelegram() {
|
||||||
|
await markWelcomeDone();
|
||||||
|
setWizardOpen(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSkip() {
|
||||||
|
await markWelcomeDone();
|
||||||
|
navigate({ to: "/painel", search: {} });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authLoading || !user) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex items-center justify-center bg-[oklch(0.21_0.04_265)]">
|
||||||
|
<Loader2 className="h-6 w-6 animate-spin text-primary" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="min-h-screen flex flex-col"
|
||||||
|
style={{
|
||||||
|
background:
|
||||||
|
"linear-gradient(180deg, oklch(0.21 0.04 265) 0%, oklch(0.27 0.04 265) 100%)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Header */}
|
||||||
|
<header className="flex items-center justify-center pt-8 pb-4 px-4">
|
||||||
|
<Logo size="lg" className="text-white" />
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main className="flex-1 flex items-center justify-center px-4 py-8">
|
||||||
|
<div className="w-full max-w-2xl">
|
||||||
|
<AnimatePresence mode="wait" initial={false}>
|
||||||
|
{step === 1 && (
|
||||||
|
<motion.section
|
||||||
|
key="step-1"
|
||||||
|
initial={{ opacity: 0, x: 60 }}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
exit={{ opacity: 0, x: -60 }}
|
||||||
|
transition={{ duration: 0.3, ease: "easeOut" }}
|
||||||
|
className="text-center"
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
animate={{ scale: [1, 1.08, 1] }}
|
||||||
|
transition={{ duration: 2, repeat: Infinity, ease: "easeInOut" }}
|
||||||
|
className="mx-auto mb-8 h-24 w-24 rounded-full bg-primary/20 flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<Sparkles className="h-12 w-12 text-primary" />
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<h1 className="text-4xl font-bold text-white">
|
||||||
|
Bem-vindo à Mika! 🎉
|
||||||
|
</h1>
|
||||||
|
<p className="mt-4 text-lg text-white/70">
|
||||||
|
Seu assistente pessoal de IA está sendo preparado.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ul className="mt-10 space-y-3 max-w-sm mx-auto text-left">
|
||||||
|
{[
|
||||||
|
{ icon: "✅", text: "Pagamento confirmado" },
|
||||||
|
{ icon: "✅", text: "Sua conta está ativa" },
|
||||||
|
{ icon: "⏳", text: "Configurando seu agente..." },
|
||||||
|
].map((item, i) => (
|
||||||
|
<motion.li
|
||||||
|
key={item.text}
|
||||||
|
initial={{ opacity: 0, y: 10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: 0.5 + i * 0.5, duration: 0.4 }}
|
||||||
|
className="flex items-center gap-3 rounded-lg bg-white/5 border border-white/10 px-4 py-3 text-white"
|
||||||
|
>
|
||||||
|
<span className="text-xl">{item.icon}</span>
|
||||||
|
<span className="text-sm font-medium">{item.text}</span>
|
||||||
|
</motion.li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</motion.section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{step === 2 && (
|
||||||
|
<motion.section
|
||||||
|
key="step-2"
|
||||||
|
initial={{ opacity: 0, x: 60 }}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
exit={{ opacity: 0, x: -60 }}
|
||||||
|
transition={{ duration: 0.3, ease: "easeOut" }}
|
||||||
|
className="text-center"
|
||||||
|
>
|
||||||
|
<h2 className="text-3xl font-bold text-white">
|
||||||
|
Como você quer chamar seu assistente?
|
||||||
|
</h2>
|
||||||
|
<p className="mt-3 text-white/70">
|
||||||
|
Este será o nome que aparecerá nas conversas.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="mt-8 mx-auto max-w-md space-y-3">
|
||||||
|
<Input
|
||||||
|
autoFocus
|
||||||
|
value={agentName}
|
||||||
|
maxLength={40}
|
||||||
|
onChange={(e) => setAgentName(e.target.value)}
|
||||||
|
placeholder="Ex: Mika de João, Maya, Assistente..."
|
||||||
|
className="h-12 text-center text-lg bg-white/5 border-white/20 text-white placeholder:text-white/40 focus-visible:ring-primary"
|
||||||
|
/>
|
||||||
|
<div className="flex justify-between text-xs text-white/50 px-1">
|
||||||
|
<span>
|
||||||
|
{agentName.trim().length < 2
|
||||||
|
? "Mínimo 2 caracteres"
|
||||||
|
: "\u00A0"}
|
||||||
|
</span>
|
||||||
|
<span>{agentName.length}/40</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-6 flex flex-wrap justify-center gap-2 max-w-md mx-auto">
|
||||||
|
{[
|
||||||
|
`Mika de ${firstName}`,
|
||||||
|
"Maya",
|
||||||
|
"Alex",
|
||||||
|
"Assistente",
|
||||||
|
].map((sugg) => (
|
||||||
|
<button
|
||||||
|
key={sugg}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setAgentName(sugg.slice(0, 40))}
|
||||||
|
className={cn(
|
||||||
|
"px-3 py-1.5 rounded-full text-xs font-medium border transition-colors",
|
||||||
|
agentName === sugg
|
||||||
|
? "bg-primary text-primary-foreground border-primary"
|
||||||
|
: "bg-white/5 text-white/80 border-white/15 hover:bg-white/10",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{sugg}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
size="lg"
|
||||||
|
className="mt-8 min-w-56"
|
||||||
|
disabled={agentName.trim().length < 2 || saving}
|
||||||
|
onClick={handleSaveName}
|
||||||
|
>
|
||||||
|
{saving ? (
|
||||||
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
|
) : null}
|
||||||
|
Continuar <ArrowRight className="ml-2 h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</motion.section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{step === 3 && (
|
||||||
|
<motion.section
|
||||||
|
key="step-3"
|
||||||
|
initial={{ opacity: 0, x: 60 }}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
exit={{ opacity: 0, x: -60 }}
|
||||||
|
transition={{ duration: 0.3, ease: "easeOut" }}
|
||||||
|
className="text-center"
|
||||||
|
>
|
||||||
|
<h2 className="text-3xl font-bold text-white">
|
||||||
|
Quase lá! Conecte seu Telegram.
|
||||||
|
</h2>
|
||||||
|
<p className="mt-3 text-white/70 max-w-lg mx-auto">
|
||||||
|
Para conversar com{" "}
|
||||||
|
<span className="font-semibold text-white">
|
||||||
|
{agentName || "seu agente"}
|
||||||
|
</span>
|
||||||
|
, você precisa conectar seu Telegram. Leva menos de 2 minutos.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
animate={{ scale: [1, 1.06, 1] }}
|
||||||
|
transition={{ duration: 2.4, repeat: Infinity, ease: "easeInOut" }}
|
||||||
|
className="mx-auto mt-8 mb-8 h-24 w-24 rounded-full bg-primary/20 flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<TelegramIcon className="h-12 w-12 text-primary" />
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<ol className="space-y-3 max-w-md mx-auto text-left">
|
||||||
|
<StepRow
|
||||||
|
number={1}
|
||||||
|
title="Abra o BotFather no Telegram"
|
||||||
|
extra={
|
||||||
|
<Button
|
||||||
|
asChild
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
className="mt-2 border-white/20 bg-white/5 text-white hover:bg-white/10 hover:text-white"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="https://t.me/BotFather?start"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
Abrir BotFather <ExternalLink className="ml-2 h-3.5 w-3.5" />
|
||||||
|
</a>
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<StepRow
|
||||||
|
number={2}
|
||||||
|
title="Digite /newbot e siga as instruções"
|
||||||
|
hint={`Use "${agentName || "Mika"}Bot" como username sugerido`}
|
||||||
|
/>
|
||||||
|
<StepRow
|
||||||
|
number={3}
|
||||||
|
title="Cole o token aqui na próxima tela"
|
||||||
|
/>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<div className="mt-10 flex flex-col items-center gap-3">
|
||||||
|
<Button
|
||||||
|
size="lg"
|
||||||
|
className="min-w-64"
|
||||||
|
onClick={handleConnectTelegram}
|
||||||
|
>
|
||||||
|
Conectar meu Telegram <ArrowRight className="ml-2 h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleSkip}
|
||||||
|
className="text-sm text-white/60 hover:text-white underline-offset-4 hover:underline"
|
||||||
|
>
|
||||||
|
Fazer isso depois
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</motion.section>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<TelegramOnboardingWizard
|
||||||
|
open={wizardOpen}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
setWizardOpen(open);
|
||||||
|
// Quando fechar o wizard, leva ao painel
|
||||||
|
if (!open) {
|
||||||
|
navigate({ to: "/painel", search: {} });
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function StepRow({
|
||||||
|
number,
|
||||||
|
title,
|
||||||
|
hint,
|
||||||
|
extra,
|
||||||
|
}: {
|
||||||
|
number: number;
|
||||||
|
title: string;
|
||||||
|
hint?: string;
|
||||||
|
extra?: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<li className="flex items-start gap-3 rounded-lg border border-white/10 bg-white/5 p-4">
|
||||||
|
<span className="h-7 w-7 shrink-0 rounded-full bg-primary text-primary-foreground flex items-center justify-center font-bold text-sm">
|
||||||
|
{number}
|
||||||
|
</span>
|
||||||
|
<div className="flex-1">
|
||||||
|
<p className="text-sm font-medium text-white">{title}</p>
|
||||||
|
{hint && <p className="mt-1 text-xs text-white/60">{hint}</p>}
|
||||||
|
{extra}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marker no componente para cumprir contrato de "marcar como visitada"
|
||||||
|
// (também usado pelo redirect do /painel)
|
||||||
|
export function isWelcomeDone(): boolean {
|
||||||
|
if (typeof window === "undefined") return false;
|
||||||
|
return window.localStorage.getItem(WELCOME_DONE_KEY) === "1";
|
||||||
|
}
|
||||||
|
|
@ -15,11 +15,10 @@ function CheckoutSuccessPage() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Invalida queries de assinatura para refletir o novo estado quando o webhook chegar
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["subscription"] });
|
queryClient.invalidateQueries({ queryKey: ["subscription"] });
|
||||||
const t = setTimeout(() => {
|
const t = setTimeout(() => {
|
||||||
navigate({ to: "/painel", search: {} });
|
navigate({ to: "/bem-vindo" });
|
||||||
}, 6000);
|
}, 4000);
|
||||||
return () => clearTimeout(t);
|
return () => clearTimeout(t);
|
||||||
}, [navigate, queryClient]);
|
}, [navigate, queryClient]);
|
||||||
|
|
||||||
|
|
@ -32,15 +31,12 @@ function CheckoutSuccessPage() {
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<h1 className="text-3xl font-bold tracking-tight">Assinatura confirmada!</h1>
|
<h1 className="text-3xl font-bold tracking-tight">Assinatura confirmada!</h1>
|
||||||
<p className="text-muted-foreground">
|
<p className="text-muted-foreground">
|
||||||
Estamos provisionando seu acesso. Em alguns segundos seu plano estará ativo.
|
Em instantes vamos preparar seu agente Mika.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col sm:flex-row gap-3 justify-center">
|
<div className="flex flex-col sm:flex-row gap-3 justify-center">
|
||||||
<Button asChild className="rounded-lg bg-primary hover:bg-primary-dark text-primary-foreground">
|
<Button asChild className="rounded-lg bg-primary hover:bg-primary-dark text-primary-foreground">
|
||||||
<Link to="/painel" search={{}}>Ir para o painel</Link>
|
<Link to="/bem-vindo">Continuar</Link>
|
||||||
</Button>
|
|
||||||
<Button asChild variant="outline" className="rounded-lg">
|
|
||||||
<Link to="/painel/faturamento">Ver faturamento</Link>
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-muted-foreground">Você será redirecionado em instantes…</p>
|
<p className="text-xs text-muted-foreground">Você será redirecionado em instantes…</p>
|
||||||
|
|
|
||||||
|
|
@ -35,13 +35,17 @@ function DashboardPage() {
|
||||||
const [wizardOpen, setWizardOpen] = useState(false);
|
const [wizardOpen, setWizardOpen] = useState(false);
|
||||||
const previousStatusRef = useRef<string | null>(null);
|
const previousStatusRef = useRef<string | null>(null);
|
||||||
|
|
||||||
// Auto-open do wizard ao voltar com ?status=success
|
// Redireciona para /bem-vindo se cliente acabou de pagar e ainda não completou onboarding
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (search.status !== "success" || !agent) return;
|
if (search.status !== "success" || !agent) return;
|
||||||
if (agent.status === "suspended" || agent.status === "error") {
|
if (agent.status === "suspended" || agent.status === "error") {
|
||||||
navigate({ search: { status: undefined }, replace: true });
|
navigate({ search: { status: undefined }, replace: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!agent.onboarding_completed) {
|
||||||
|
navigate({ to: "/bem-vindo", replace: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!agent.telegram_bot_username) setWizardOpen(true);
|
if (!agent.telegram_bot_username) setWizardOpen(true);
|
||||||
navigate({ search: { status: undefined }, replace: true });
|
navigate({ search: { status: undefined }, replace: true });
|
||||||
}, [search.status, agent, navigate]);
|
}, [search.status, agent, navigate]);
|
||||||
|
|
@ -74,6 +78,17 @@ function DashboardPage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const firstName = (profile?.full_name || "").split(" ")[0] || "por aqui";
|
const firstName = (profile?.full_name || "").split(" ")[0] || "por aqui";
|
||||||
|
const agentName = agent?.agent_name?.trim() || "Mika";
|
||||||
|
const statusLabel =
|
||||||
|
agent?.status === "active"
|
||||||
|
? "ativo"
|
||||||
|
: agent?.status === "provisioning"
|
||||||
|
? "sendo preparado"
|
||||||
|
: agent?.status === "suspended"
|
||||||
|
? "pausado"
|
||||||
|
: agent?.status === "error"
|
||||||
|
? "com erro"
|
||||||
|
: "aguardando configuração";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
|
|
@ -83,7 +98,9 @@ function DashboardPage() {
|
||||||
<h1 className="text-3xl font-bold tracking-tight">Olá, {firstName} 👋</h1>
|
<h1 className="text-3xl font-bold tracking-tight">Olá, {firstName} 👋</h1>
|
||||||
<p className="mt-1 text-muted-foreground">
|
<p className="mt-1 text-muted-foreground">
|
||||||
{subscription
|
{subscription
|
||||||
? "Acompanhe abaixo o status do seu agente Mika."
|
? agent
|
||||||
|
? <>Seu agente <span className="font-semibold text-foreground">{agentName}</span> está {statusLabel}.</>
|
||||||
|
: "Acompanhe abaixo o status do seu agente."
|
||||||
: "Vamos colocar seu agente Mika no ar."}
|
: "Vamos colocar seu agente Mika no ar."}
|
||||||
</p>
|
</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,7 @@ Deno.serve(async (req) => {
|
||||||
const { data: agent, error: agentErr } = await supabase
|
const { data: agent, error: agentErr } = await supabase
|
||||||
.from("agent_instances")
|
.from("agent_instances")
|
||||||
.select(
|
.select(
|
||||||
"id, user_id, uuid_tenant, status, telegram_bot_token_vault_id, telegram_bot_username, telegram_user_chat_id, railway_service_id",
|
"id, user_id, uuid_tenant, status, telegram_bot_token_vault_id, telegram_bot_username, telegram_user_chat_id, railway_service_id, agent_name",
|
||||||
)
|
)
|
||||||
.eq("id", body.agent_instance_id)
|
.eq("id", body.agent_instance_id)
|
||||||
.maybeSingle();
|
.maybeSingle();
|
||||||
|
|
@ -108,7 +108,11 @@ Deno.serve(async (req) => {
|
||||||
|
|
||||||
const fullName = (profile?.full_name?.trim() || "Usuário").toString();
|
const fullName = (profile?.full_name?.trim() || "Usuário").toString();
|
||||||
const firstName = fullName.split(" ")[0] || "Usuário";
|
const firstName = fullName.split(" ")[0] || "Usuário";
|
||||||
const agentName = body.agent_name?.trim() || `Mika de ${firstName}`;
|
// Prioridade: body > coluna agent_name no DB > default "Mika de {firstName}"
|
||||||
|
const agentName =
|
||||||
|
body.agent_name?.trim() ||
|
||||||
|
(agent.agent_name?.trim() ?? "") ||
|
||||||
|
`Mika de ${firstName}`;
|
||||||
console.log(`[provision-agent] profile carregado: ${fullName} → agent_name=${agentName}`);
|
console.log(`[provision-agent] profile carregado: ${fullName} → agent_name=${agentName}`);
|
||||||
|
|
||||||
// 1c) Carregar subscription ativa (para definir modelo Pro vs Basic)
|
// 1c) Carregar subscription ativa (para definir modelo Pro vs Basic)
|
||||||
|
|
@ -280,6 +284,7 @@ Deno.serve(async (req) => {
|
||||||
.update({
|
.update({
|
||||||
railway_service_id: railwayServiceId,
|
railway_service_id: railwayServiceId,
|
||||||
vps_pool_id: pool.id,
|
vps_pool_id: pool.id,
|
||||||
|
agent_name: agentNameFinal,
|
||||||
model_config: {
|
model_config: {
|
||||||
provider: modelFinal,
|
provider: modelFinal,
|
||||||
stt: sttProvider,
|
stt: sttProvider,
|
||||||
|
|
@ -389,7 +394,10 @@ async function handleUpdateExistingService(
|
||||||
|
|
||||||
const fullName = (profile?.full_name?.trim() || "Usuário").toString();
|
const fullName = (profile?.full_name?.trim() || "Usuário").toString();
|
||||||
const firstName = fullName.split(" ")[0] || "Usuário";
|
const firstName = fullName.split(" ")[0] || "Usuário";
|
||||||
const agentName = body.agent_name?.trim() || `Mika de ${firstName}`;
|
const agentName =
|
||||||
|
body.agent_name?.trim() ||
|
||||||
|
(agent.agent_name?.trim() ?? "") ||
|
||||||
|
`Mika de ${firstName}`;
|
||||||
|
|
||||||
const { data: subscription } = await supabase
|
const { data: subscription } = await supabase
|
||||||
.from("subscriptions")
|
.from("subscriptions")
|
||||||
|
|
|
||||||
|
|
@ -81,7 +81,9 @@ Deno.serve(async (req) => {
|
||||||
|
|
||||||
const { data: agent } = await supabase
|
const { data: agent } = await supabase
|
||||||
.from("agent_instances")
|
.from("agent_instances")
|
||||||
.select("id, status, user_id, telegram_bot_username, railway_service_id")
|
.select(
|
||||||
|
"id, status, user_id, telegram_bot_username, railway_service_id, telegram_user_chat_id, telegram_bot_token_vault_id, agent_name, welcome_message_sent_at",
|
||||||
|
)
|
||||||
.eq("railway_service_id", serviceId)
|
.eq("railway_service_id", serviceId)
|
||||||
.maybeSingle();
|
.maybeSingle();
|
||||||
|
|
||||||
|
|
@ -123,6 +125,19 @@ Deno.serve(async (req) => {
|
||||||
|
|
||||||
console.log(`railway-webhook: agent ${agent.id} marcado como active (status=${upper})`);
|
console.log(`railway-webhook: agent ${agent.id} marcado como active (status=${upper})`);
|
||||||
|
|
||||||
|
// Envia mensagem de boas-vindas via Telegram (apenas na primeira ativação)
|
||||||
|
if (
|
||||||
|
!agent.welcome_message_sent_at &&
|
||||||
|
agent.telegram_user_chat_id &&
|
||||||
|
agent.telegram_bot_token_vault_id
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
await sendWelcomeMessage(supabase, agent);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("railway-webhook: falha ao enviar welcome message:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Notifica admin somente se era um auto-provisionamento (status anterior=provisioning)
|
// Notifica admin somente se era um auto-provisionamento (status anterior=provisioning)
|
||||||
if (wasProvisioning) {
|
if (wasProvisioning) {
|
||||||
const fullName = await loadFullName();
|
const fullName = await loadFullName();
|
||||||
|
|
@ -181,3 +196,60 @@ function jsonResponse(status: number, body: unknown) {
|
||||||
headers: { ...corsHeaders, "Content-Type": "application/json" },
|
headers: { ...corsHeaders, "Content-Type": "application/json" },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// deno-lint-ignore no-explicit-any
|
||||||
|
async function sendWelcomeMessage(supabase: any, agent: any): Promise<void> {
|
||||||
|
// Decifra o token do bot
|
||||||
|
const { data: secret } = await supabase.rpc("vault_decrypt_secret", {
|
||||||
|
secret_id: agent.telegram_bot_token_vault_id,
|
||||||
|
});
|
||||||
|
const token: string = secret?.[0]?.decrypted_secret ?? "";
|
||||||
|
if (!token) {
|
||||||
|
console.warn("sendWelcomeMessage: token vazio, abortando");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Carrega first name
|
||||||
|
const { data: profile } = await supabase
|
||||||
|
.from("profiles")
|
||||||
|
.select("full_name")
|
||||||
|
.eq("id", agent.user_id)
|
||||||
|
.maybeSingle();
|
||||||
|
|
||||||
|
const fullName = (profile?.full_name as string | undefined)?.trim() || "";
|
||||||
|
const firstName = fullName.split(" ")[0] || "você";
|
||||||
|
const agentName = (agent.agent_name as string | undefined)?.trim() || "Mika";
|
||||||
|
|
||||||
|
const text =
|
||||||
|
`Olá, ${firstName}! 👋\n\n` +
|
||||||
|
`Sou ${agentName}, sua assistente pessoal de IA criada pela DomCo.\n\n` +
|
||||||
|
`Estou pronta para começar! Aqui estão algumas coisas que posso fazer por você:\n\n` +
|
||||||
|
`📧 Resumir seus e-mails importantes\n` +
|
||||||
|
`📅 Gerenciar sua agenda\n` +
|
||||||
|
`✅ Organizar suas tarefas\n` +
|
||||||
|
`🔍 Pesquisar qualquer coisa\n` +
|
||||||
|
`⚡ Criar automações personalizadas\n\n` +
|
||||||
|
`Pode me mandar uma mensagem quando quiser. Estou aqui! 🚀`;
|
||||||
|
|
||||||
|
const res = await fetch(`https://api.telegram.org/bot${token}/sendMessage`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
chat_id: agent.telegram_user_chat_id,
|
||||||
|
text,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const err = await res.text().catch(() => "");
|
||||||
|
console.error(`sendWelcomeMessage: Telegram API ${res.status}: ${err}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await supabase
|
||||||
|
.from("agent_instances")
|
||||||
|
.update({ welcome_message_sent_at: new Date().toISOString() })
|
||||||
|
.eq("id", agent.id);
|
||||||
|
|
||||||
|
console.log(`sendWelcomeMessage: enviada para agent ${agent.id}`);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
ALTER TABLE public.agent_instances
|
||||||
|
ADD COLUMN IF NOT EXISTS agent_name text,
|
||||||
|
ADD COLUMN IF NOT EXISTS welcome_message_sent_at timestamp with time zone,
|
||||||
|
ADD COLUMN IF NOT EXISTS onboarding_completed boolean NOT NULL DEFAULT false;
|
||||||
Loading…
Add table
Add a link
Reference in a new issue