From 93ee695c0e052784dda2535449781615a5303ae9 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Fri, 17 Apr 2026 20:03:05 +0000 Subject: [PATCH] Changes Co-authored-by: domfelipe <53182096+domfelipe@users.noreply.github.com> --- .../telegram/DisconnectTelegramDialog.tsx | 76 +++++++ .../telegram/TelegramConnectionBanner.tsx | 73 ++++++ .../telegram/TelegramOnboardingWizard.tsx | 212 ++++++++++++++++++ .../mika/telegram/TelegramStatusCard.tsx | 105 +++++++++ 4 files changed, 466 insertions(+) create mode 100644 src/components/mika/telegram/DisconnectTelegramDialog.tsx create mode 100644 src/components/mika/telegram/TelegramConnectionBanner.tsx create mode 100644 src/components/mika/telegram/TelegramOnboardingWizard.tsx create mode 100644 src/components/mika/telegram/TelegramStatusCard.tsx diff --git a/src/components/mika/telegram/DisconnectTelegramDialog.tsx b/src/components/mika/telegram/DisconnectTelegramDialog.tsx new file mode 100644 index 0000000..3db7f78 --- /dev/null +++ b/src/components/mika/telegram/DisconnectTelegramDialog.tsx @@ -0,0 +1,76 @@ +"use client"; + +import { useState } from "react"; +import { Loader2 } from "lucide-react"; +import { useQueryClient } from "@tanstack/react-query"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog"; +import { useAuth } from "@/hooks/use-auth"; +import { invokeFunction } from "@/lib/invoke-function"; +import { toast } from "sonner"; + +interface Props { + open: boolean; + onOpenChange: (open: boolean) => void; +} + +export function DisconnectTelegramDialog({ open, onOpenChange }: Props) { + const { user } = useAuth(); + const queryClient = useQueryClient(); + const [loading, setLoading] = useState(false); + + async function handleDisconnect() { + setLoading(true); + const { error } = await invokeFunction("disconnect-telegram"); + setLoading(false); + if (error) { + toast.error(error.message ?? "Falha ao desconectar."); + return; + } + if (user) { + await queryClient.invalidateQueries({ queryKey: ["agent-instance", user.id] }); + } + toast.success("Telegram desconectado."); + onOpenChange(false); + } + + return ( + + + + Desconectar Telegram? + + Tem certeza? Você precisará criar um novo bot no BotFather para reconectar. + + + + Cancelar + { + e.preventDefault(); + handleDisconnect(); + }} + disabled={loading} + className="bg-destructive hover:bg-destructive/90 text-destructive-foreground" + > + {loading ? ( + <> + Desconectando... + + ) : ( + "Desconectar" + )} + + + + + ); +} diff --git a/src/components/mika/telegram/TelegramConnectionBanner.tsx b/src/components/mika/telegram/TelegramConnectionBanner.tsx new file mode 100644 index 0000000..573e0ef --- /dev/null +++ b/src/components/mika/telegram/TelegramConnectionBanner.tsx @@ -0,0 +1,73 @@ +"use client"; + +import { useState } from "react"; +import { Link } from "@tanstack/react-router"; +import { AlertCircle, AlertTriangle } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { useAgentInstance } from "@/hooks/use-agent-instance"; +import { TelegramOnboardingWizard } from "./TelegramOnboardingWizard"; + +/** + * Banner sticky no topo do painel. + * - Suspended/error → vermelho com link p/ faturamento + * - token revogado → vermelho com link p/ Meu Agente + * - bot ainda não conectado → amber com CTA para abrir o wizard + */ +export function TelegramConnectionBanner() { + const { data: agent } = useAgentInstance(); + const [open, setOpen] = useState(false); + + if (!agent) return null; + + if (agent.status === "suspended" || agent.status === "error") { + return ( +
+
+ + + Seu agente está suspenso. Regularize sua assinatura em Faturamento. + + +
+
+ ); + } + + if (agent.telegram_token_invalid) { + return ( +
+
+ + + Seu token Telegram foi revogado. Desconecte e reconecte o bot. + + +
+
+ ); + } + + if (agent.telegram_bot_username) return null; + + return ( + <> +
+
+ + + Conecte seu Telegram para começar a conversar com o Mika. + + +
+
+ + + + ); +} diff --git a/src/components/mika/telegram/TelegramOnboardingWizard.tsx b/src/components/mika/telegram/TelegramOnboardingWizard.tsx new file mode 100644 index 0000000..9a1e2c8 --- /dev/null +++ b/src/components/mika/telegram/TelegramOnboardingWizard.tsx @@ -0,0 +1,212 @@ +"use client"; + +import { useEffect, useMemo, useState } from "react"; +import { motion, AnimatePresence } from "framer-motion"; +import { X } from "lucide-react"; +import { useQueryClient } from "@tanstack/react-query"; +import * as DialogPrimitive from "@radix-ui/react-dialog"; +import { useProfile } from "@/hooks/use-profile"; +import { useAgentInstance } from "@/hooks/use-agent-instance"; +import { supabase } from "@/integrations/supabase/client"; +import { useAuth } from "@/hooks/use-auth"; +import { suggestBotName, suggestBotUsername } from "@/lib/telegram-username"; +import { cn } from "@/lib/utils"; +import { toast } from "sonner"; + +import { StepWelcome } from "./StepWelcome"; +import { StepCreateBot } from "./StepCreateBot"; +import { StepNaming } from "./StepNaming"; +import { StepToken, type ValidatedBot } from "./StepToken"; +import { StepConfiguring } from "./StepConfiguring"; +import { StepWaiting } from "./StepWaiting"; + +const STORAGE_KEY = "mika-onboarding-last-step"; +const TOTAL_STEPS = 6; + +interface Props { + open: boolean; + onOpenChange: (open: boolean) => void; + /** Step inicial (1-6). Se omitido, lê do localStorage ou começa em 1. */ + initialStep?: number; +} + +export function TelegramOnboardingWizard({ open, onOpenChange, initialStep }: Props) { + const { data: profile } = useProfile(); + const { data: agent } = useAgentInstance(); + const { user } = useAuth(); + const queryClient = useQueryClient(); + + const [step, setStep] = useState(1); + const [validated, setValidated] = useState(null); + + const suggestedName = useMemo(() => suggestBotName(profile?.full_name), [profile?.full_name]); + const suggestedUsername = useMemo( + () => suggestBotUsername(profile?.full_name), + [profile?.full_name], + ); + + // Inicializa step ao abrir + useEffect(() => { + if (!open) return; + if (initialStep) { + setStep(Math.min(Math.max(initialStep, 1), TOTAL_STEPS)); + return; + } + if (typeof window !== "undefined") { + const stored = window.localStorage.getItem(STORAGE_KEY); + const parsed = stored ? Number(stored) : NaN; + if (Number.isFinite(parsed) && parsed >= 1 && parsed <= TOTAL_STEPS) { + setStep(parsed); + return; + } + } + setStep(1); + }, [open, initialStep]); + + // Persiste step ao mudar + useEffect(() => { + if (!open) return; + if (typeof window !== "undefined") { + window.localStorage.setItem(STORAGE_KEY, String(step)); + } + }, [step, open]); + + function handleClose() { + onOpenChange(false); + } + + async function handleFinish() { + if (!agent || !user) return handleClose(); + const { error } = await supabase + .from("agent_instances") + .update({ telegram_onboarding_completed: true }) + .eq("id", agent.id); + if (error) { + toast.error("Não foi possível salvar o status do onboarding."); + return; + } + if (typeof window !== "undefined") { + window.localStorage.removeItem(STORAGE_KEY); + } + await queryClient.invalidateQueries({ queryKey: ["agent-instance", user.id] }); + toast.success("Onboarding concluído!"); + onOpenChange(false); + } + + // Após validar token, recarrega agent_instance (já tem bot_username persistido) + function handleValidated(bot: ValidatedBot) { + setValidated(bot); + if (user) { + queryClient.invalidateQueries({ queryKey: ["agent-instance", user.id] }); + } + } + + function handleConfigured() { + if (user) { + queryClient.invalidateQueries({ queryKey: ["agent-instance", user.id] }); + } + setStep(6); + } + + const botUsername = validated?.bot_username ?? agent?.telegram_bot_username ?? ""; + const connectedAt = agent?.telegram_connected_at ?? null; + + return ( + + + + + + Conectar Telegram ao Mika + + + Wizard guiado de 6 passos para conectar seu agente Mika ao Telegram. + + + {/* Header com progress + close */} +
+
+ + Passo {step} de {TOTAL_STEPS} + + +
+
+ +
+
+ + {/* Conteúdo dos steps */} +
+ + + {step === 1 && setStep(2)} />} + {step === 2 && setStep(3)} />} + {step === 3 && ( + setStep(4)} + /> + )} + {step === 4 && ( + setStep(5)} + /> + )} + {step === 5 && ( + setStep(6)} + /> + )} + {step === 6 && agent && botUsername && ( + + )} + {step === 6 && (!agent || !botUsername) && ( +
+ Conecte o bot primeiro para receber a primeira mensagem. +
+ )} +
+
+
+
+
+
+ ); +} diff --git a/src/components/mika/telegram/TelegramStatusCard.tsx b/src/components/mika/telegram/TelegramStatusCard.tsx new file mode 100644 index 0000000..74e05d6 --- /dev/null +++ b/src/components/mika/telegram/TelegramStatusCard.tsx @@ -0,0 +1,105 @@ +"use client"; + +import { useState } from "react"; +import { ExternalLink, AlertCircle } from "lucide-react"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { useAgentInstance } from "@/hooks/use-agent-instance"; +import { TelegramIcon } from "./TelegramIcon"; +import { TelegramOnboardingWizard } from "./TelegramOnboardingWizard"; +import { DisconnectTelegramDialog } from "./DisconnectTelegramDialog"; + +function formatPtBR(date: string | null): string | null { + if (!date) return null; + try { + return new Intl.DateTimeFormat("pt-BR", { + day: "2-digit", + month: "2-digit", + year: "numeric", + hour: "2-digit", + minute: "2-digit", + }).format(new Date(date)); + } catch { + return null; + } +} + +export function TelegramStatusCard() { + const { data: agent } = useAgentInstance(); + const [wizardOpen, setWizardOpen] = useState(false); + const [disconnectOpen, setDisconnectOpen] = useState(false); + + const connected = !!agent?.telegram_bot_username; + + return ( +
+
+ +

Telegram

+ {connected && ( + + Conectado + + )} +
+ + {!connected ? ( +
+
+ +
+

Telegram não conectado

+

+ Conecte seu bot para começar a conversar com o Mika. +

+
+
+ +
+ ) : ( +
+
+

@{agent!.telegram_bot_username}

+ {agent!.telegram_connected_at && ( +

+ Conectado em {formatPtBR(agent!.telegram_connected_at)} +

+ )} +
+ + {agent!.telegram_token_invalid && ( +
+ +

+ Token revogado — desconecte e reconecte. +

+
+ )} + +
+ + +
+
+ )} + + + +
+ ); +}