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/StepConfiguring.tsx b/src/components/mika/telegram/StepConfiguring.tsx new file mode 100644 index 0000000..049812a --- /dev/null +++ b/src/components/mika/telegram/StepConfiguring.tsx @@ -0,0 +1,67 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { Loader2, AlertCircle } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { invokeFunction } from "@/lib/invoke-function"; + +interface Props { + onConfigured: () => void; + onSkip: () => void; +} + +export function StepConfiguring({ onConfigured, onSkip }: Props) { + const [state, setState] = useState<"loading" | "error">("loading"); + const [error, setError] = useState(null); + const [attempt, setAttempt] = useState(0); + + useEffect(() => { + let cancelled = false; + setState("loading"); + setError(null); + + (async () => { + const { error: err } = await invokeFunction("configure-telegram-webhook"); + if (cancelled) return; + if (err) { + setError(err.message); + setState("error"); + return; + } + window.setTimeout(() => { + if (!cancelled) onConfigured(); + }, 800); + })(); + + return () => { + cancelled = true; + }; + }, [attempt, onConfigured]); + + return ( +
+ {state === "loading" && ( + <> + +

+ Configurando recebimento de mensagens... +

+ + )} + + {state === "error" && ( +
+ +

Falha ao configurar webhook

+ {error &&

{error}

} +
+ + +
+
+ )} +
+ ); +} diff --git a/src/components/mika/telegram/StepCreateBot.tsx b/src/components/mika/telegram/StepCreateBot.tsx new file mode 100644 index 0000000..b27ace7 --- /dev/null +++ b/src/components/mika/telegram/StepCreateBot.tsx @@ -0,0 +1,47 @@ +"use client"; + +import { ExternalLink } from "lucide-react"; +import { Button } from "@/components/ui/button"; + +const STEPS = [ + 'Clique em "Abrir BotFather" acima', + "Envie /newbot", + "Siga as instruções do BotFather", +]; + +export function StepCreateBot({ onNext }: { onNext: () => void }) { + return ( +
+

Crie seu bot no BotFather

+

+ O BotFather é o bot oficial do Telegram para criar outros bots. É gratuito e leva 1 minuto. +

+ + + +
    + {STEPS.map((step, i) => ( +
  1. + + {i + 1} + + {step} +
  2. + ))} +
+ +
+ +
+
+ ); +} diff --git a/src/components/mika/telegram/StepNaming.tsx b/src/components/mika/telegram/StepNaming.tsx new file mode 100644 index 0000000..e85ab35 --- /dev/null +++ b/src/components/mika/telegram/StepNaming.tsx @@ -0,0 +1,71 @@ +"use client"; + +import { useState } from "react"; +import { Copy, Check } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { toast } from "sonner"; + +interface Props { + suggestedName: string; + suggestedUsername: string; + onNext: () => void; +} + +export function StepNaming({ suggestedName, suggestedUsername, onNext }: Props) { + return ( +
+

Escolha os nomes

+

+ O BotFather vai pedir dois nomes. Use nossas sugestões se quiser. +

+ +
+ + +
+ +
+ +
+
+ ); +} + +function CopyField({ label, value, help }: { label: string; value: string; help?: string }) { + const [copied, setCopied] = useState(false); + + async function copy() { + try { + await navigator.clipboard.writeText(value); + setCopied(true); + toast.success("Copiado para a área de transferência"); + window.setTimeout(() => setCopied(false), 1500); + } catch { + toast.error("Não foi possível copiar"); + } + } + + return ( +
+ +
+ + +
+ {help &&

{help}

} +
+ ); +} diff --git a/src/components/mika/telegram/StepToken.tsx b/src/components/mika/telegram/StepToken.tsx new file mode 100644 index 0000000..930545f --- /dev/null +++ b/src/components/mika/telegram/StepToken.tsx @@ -0,0 +1,111 @@ +"use client"; + +import { useState } from "react"; +import { Loader2, Check, AlertCircle } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { invokeFunction } from "@/lib/invoke-function"; +import { cn } from "@/lib/utils"; + +export interface ValidatedBot { + bot_username: string; + bot_name: string; + bot_id: number; +} + +interface Props { + onValidated: (bot: ValidatedBot) => void; + onNext: () => void; + validated: ValidatedBot | null; +} + +type State = "idle" | "validating" | "success" | "error"; + +export function StepToken({ onValidated, onNext, validated }: Props) { + const [token, setToken] = useState(""); + const [state, setState] = useState(validated ? "success" : "idle"); + const [errorMsg, setErrorMsg] = useState(null); + + async function handleValidate() { + setState("validating"); + setErrorMsg(null); + const { data, error } = await invokeFunction("validate-telegram-bot", { + token: token.trim(), + }); + if (error || !data?.bot_username) { + setState("error"); + setErrorMsg(error?.message ?? "Não foi possível validar o token."); + return; + } + onValidated(data); + setState("success"); + } + + return ( +
+

Cole o token do seu bot

+

+ No final da conversa, o BotFather te enviou um token parecido com{" "} + 8234567890:ABC-DEF.... Cole ele abaixo. +

+ +
+ { + setToken(e.target.value); + if (state === "error") setState("idle"); + }} + disabled={state === "validating" || state === "success"} + className="font-mono text-sm" + /> + + + + {state === "error" && errorMsg && ( +
+ +

{errorMsg}

+
+ )} + + {state === "success" && validated && ( +
+

{validated.bot_name}

+

@{validated.bot_username}

+
+ )} +
+ +
+ +
+
+ ); +} diff --git a/src/components/mika/telegram/StepWaiting.tsx b/src/components/mika/telegram/StepWaiting.tsx new file mode 100644 index 0000000..cf03e9a --- /dev/null +++ b/src/components/mika/telegram/StepWaiting.tsx @@ -0,0 +1,119 @@ +"use client"; + +import { useMemo } from "react"; +import { motion, AnimatePresence } from "framer-motion"; +import { MessageCircle, ExternalLink, CheckCircle2 } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { useTelegramFirstMessage } from "@/hooks/use-telegram-first-message"; + +interface Props { + agentInstanceId: string; + botUsername: string; + connectedAt: string | null; + onFinish: () => void; +} + +const EMOJIS = ["🎉", "✨", "⭐", "🚀", "🎉", "✨", "⭐", "🚀", "🎉", "✨", "⭐", "🚀"]; + +export function StepWaiting({ agentInstanceId, botUsername, connectedAt, onFinish }: Props) { + const { received } = useTelegramFirstMessage({ + agentInstanceId, + since: connectedAt, + enabled: true, + }); + + const positions = useMemo( + () => + EMOJIS.map(() => ({ + x: (Math.random() - 0.5) * 320, + y: (Math.random() - 0.5) * 240, + rotate: (Math.random() - 0.5) * 80, + })), + [], + ); + + return ( +
+ + {!received ? ( + +

+ Mande qualquer mensagem para seu Mika agora +

+

+ Estou aguardando sua primeira mensagem. Pode ser "oi". 😊 +

+ +
+ +

Aguardando sua primeira mensagem...

+
+ + +
+ ) : ( + + {/* emojis flutuantes */} +
+ {EMOJIS.map((emoji, i) => ( + + {emoji} + + ))} +
+ +
+
+ +
+

🎉 Mika conectado!

+

+ Você recebeu a primeira resposta do seu agente. Ele ainda está em modo de teste, + mas em breve vai responder de verdade. +

+ + +
+
+ )} +
+
+ ); +} diff --git a/src/components/mika/telegram/StepWelcome.tsx b/src/components/mika/telegram/StepWelcome.tsx new file mode 100644 index 0000000..61ce457 --- /dev/null +++ b/src/components/mika/telegram/StepWelcome.tsx @@ -0,0 +1,48 @@ +"use client"; + +import { Sparkles, Shield, Smartphone } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { TelegramIcon } from "./TelegramIcon"; + +export function StepWelcome({ onNext }: { onNext: () => void }) { + return ( +
+
+ +
+

+ Vamos conectar seu Telegram em 30 segundos +

+

+ Seu agente Mika ficará disponível diretamente no Telegram — onde você já passa seu dia. +

+ +
    + }> + Criação gratuita via BotFather + + }> + Nenhum dado sensível compartilhado conosco + + }> + Funciona no celular, desktop e web + +
+ + +
+ ); +} + +function Bullet({ icon, children }: { icon: React.ReactNode; children: React.ReactNode }) { + return ( +
  • + + {icon} + + {children} +
  • + ); +} 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/TelegramIcon.tsx b/src/components/mika/telegram/TelegramIcon.tsx new file mode 100644 index 0000000..a57c9e8 --- /dev/null +++ b/src/components/mika/telegram/TelegramIcon.tsx @@ -0,0 +1,12 @@ +"use client"; + +import { Send } from "lucide-react"; + +interface Props { + className?: string; +} + +/** Ícone do Telegram (paper plane) — usa apenas tokens, herda cor via currentColor. */ +export function TelegramIcon({ className }: Props) { + return