From 68d374dd6b8c33701838362e91732211f45f693c Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Mon, 27 Apr 2026 12:27:34 +0000 Subject: [PATCH 1/2] Changes Co-authored-by: domfelipe <53182096+domfelipe@users.noreply.github.com> --- .../mika/telegram/BotFatherWizard.tsx | 447 ++++++++++++++++++ 1 file changed, 447 insertions(+) create mode 100644 src/components/mika/telegram/BotFatherWizard.tsx diff --git a/src/components/mika/telegram/BotFatherWizard.tsx b/src/components/mika/telegram/BotFatherWizard.tsx new file mode 100644 index 0000000..294356f --- /dev/null +++ b/src/components/mika/telegram/BotFatherWizard.tsx @@ -0,0 +1,447 @@ +"use client"; + +import { useEffect, useMemo, useState } from "react"; +import { motion, AnimatePresence } from "framer-motion"; +import { + ArrowRight, + Check, + Copy, + ExternalLink, + Loader2, + AlertCircle, +} from "lucide-react"; +import { toast } from "sonner"; + +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { invokeFunction } from "@/lib/invoke-function"; +import { cn } from "@/lib/utils"; +import { + sanitizeForUsername, + suggestBotUsername, +} from "@/lib/telegram-username"; + +const TOKEN_REGEX = /^\d+:[A-Za-z0-9_-]{35}$/; + +interface Props { + agentName: string; + fullName: string; + onActivated: (bot: { bot_username: string; bot_name: string; bot_id: number }) => void; + onSkip: () => void; +} + +export function BotFatherWizard({ agentName, fullName, onActivated, onSkip }: Props) { + const [step1Done, setStep1Done] = useState(false); + const [step2Done, setStep2Done] = useState(false); + const [step3Done, setStep3Done] = useState(false); + const [token, setToken] = useState(""); + const [submitting, setSubmitting] = useState(false); + const [errorMsg, setErrorMsg] = useState(null); + + const suggestedUsername = useMemo(() => { + // base do agent_name; cai pro firstName se não der + const base = sanitizeForUsername(agentName).replace(/^mikade/, "mika"); + if (base.length >= 5) { + const trimmed = base.slice(0, 28); + return trimmed.endsWith("bot") ? trimmed : `${trimmed}bot`; + } + return suggestBotUsername(fullName); + }, [agentName, fullName]); + + const tokenValid = TOKEN_REGEX.test(token.trim()); + + function handleOpenBotFather() { + window.open("https://t.me/BotFather?start=newbot", "_blank", "noopener,noreferrer"); + setStep1Done(true); + } + + async function copyToClipboard(value: string, onDone: () => void) { + try { + await navigator.clipboard.writeText(value); + toast.success("Copiado!"); + onDone(); + } catch { + toast.error("Não foi possível copiar."); + } + } + + async function handleActivate() { + if (!tokenValid) return; + setSubmitting(true); + setErrorMsg(null); + const { data, error } = await invokeFunction<{ + bot_username: string; + bot_name: string; + bot_id: number; + }>("validate-telegram-bot", { token: token.trim() }); + + if (error || !data?.bot_username) { + setSubmitting(false); + setErrorMsg( + error?.message ?? "Token inválido. Verifique e tente novamente.", + ); + return; + } + onActivated(data); + } + + return ( +
+ {/* COLUNA ESQUERDA — passos */} +
+

+ Crie seu bot em 2 minutos +

+

+ Siga os passos abaixo. Cada um leva alguns segundos. +

+ +
    + + + + + + setStep2Done(true)} + /> + + + + setStep3Done(true)} + /> + + + +
    + { + setToken(e.target.value); + if (errorMsg) setErrorMsg(null); + }} + placeholder="1234567890:ABCdefGHIjklMNOpqrsTUVwxyz" + className={cn( + "h-11 font-mono text-sm bg-white/5 border-white/20 text-white placeholder:text-white/30", + tokenValid && + "border-emerald-400/60 focus-visible:ring-emerald-400/40", + )} + disabled={submitting} + /> + {tokenValid && ( +

    + Token válido! +

    + )} + {errorMsg && ( +

    + + {errorMsg} +

    + )} +
    +
    +
+ +
+ + +
+
+ + {/* COLUNA DIREITA — preview animado (apenas desktop) */} + +
+ ); +} + +function Step({ + number, + emoji, + title, + description, + done, + highlight, + children, +}: { + number: number; + emoji: string; + title: string; + description: string; + done: boolean; + highlight: boolean; + children?: React.ReactNode; +}) { + return ( +
  • +
    +
    + {done ? : number} +
    +
    +

    + {emoji} + {title} +

    +

    {description}

    + {children} +
    +
    +
  • + ); +} + +function CopyChip({ + value, + onCopied, +}: { + value: string; + onCopied: () => void; +}) { + const [copied, setCopied] = useState(false); + async function handleCopy() { + try { + await navigator.clipboard.writeText(value); + setCopied(true); + toast.success("Copiado!"); + onCopied(); + setTimeout(() => setCopied(false), 1500); + } catch { + toast.error("Não foi possível copiar."); + } + } + return ( + + ); +} + +function ChatPreview({ + agentName, + firstName, +}: { + agentName: string; + firstName: string; +}) { + const safeName = (agentName || "Mika").trim(); + const safeFirst = (firstName || "você").trim(); + const reply = `Olá, ${safeFirst}! 👋 Sou a Mika, sua assistente pessoal. Como posso ajudar você hoje?`; + + // Cycle: 0 = só "Oi!", 1 = typing, 2 = resposta digitando, 3 = completa, depois reseta + const [cycle, setCycle] = useState(0); + const [typed, setTyped] = useState(""); + + useEffect(() => { + if (cycle === 0) { + const t = setTimeout(() => setCycle(1), 900); + return () => clearTimeout(t); + } + if (cycle === 1) { + const t = setTimeout(() => setCycle(2), 1100); + return () => clearTimeout(t); + } + if (cycle === 2) { + // typewriter + let i = 0; + setTyped(""); + const interval = setInterval(() => { + i += 1; + setTyped(reply.slice(0, i)); + if (i >= reply.length) { + clearInterval(interval); + setTimeout(() => setCycle(3), 1500); + } + }, 25); + return () => clearInterval(interval); + } + if (cycle === 3) { + const t = setTimeout(() => { + setTyped(""); + setCycle(0); + }, 2500); + return () => clearTimeout(t); + } + }, [cycle, reply]); + + return ( +
    +

    + Prévia do seu bot +

    + +
    +
    + 🤖 +
    +
    +

    {safeName}

    +

    online

    +
    +
    + +
    + {/* Mensagem do usuário */} +
    +
    + Oi! +
    +
    + + {/* Typing indicator */} + + {cycle === 1 && ( + +
    + {[0, 1, 2].map((i) => ( + + ))} +
    +
    + )} +
    + + {/* Resposta do bot */} + {(cycle === 2 || cycle === 3) && ( + +
    + {cycle === 2 ? typed : reply} + {cycle === 2 && ( + + )} +
    +
    + )} +
    +
    + ); +} From 66308d4a1f198d7fb748b19b67a911553730e67e Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Mon, 27 Apr 2026 12:28:25 +0000 Subject: [PATCH 2/2] Changes Co-authored-by: domfelipe <53182096+domfelipe@users.noreply.github.com> --- src/routes/bem-vindo.tsx | 279 ++++----------------------------------- 1 file changed, 29 insertions(+), 250 deletions(-) diff --git a/src/routes/bem-vindo.tsx b/src/routes/bem-vindo.tsx index 90e1abb..c8fd153 100644 --- a/src/routes/bem-vindo.tsx +++ b/src/routes/bem-vindo.tsx @@ -3,14 +3,7 @@ import { createFileRoute, useNavigate } from "@tanstack/react-router"; import { useEffect, useMemo, useRef, useState } from "react"; import { motion, AnimatePresence } from "framer-motion"; -import { - ArrowRight, - CheckCircle2, - ExternalLink, - Loader2, - RefreshCcw, - Sparkles, -} from "lucide-react"; +import { ArrowRight, Loader2, Sparkles } from "lucide-react"; import { useQueryClient } from "@tanstack/react-query"; import { toast } from "sonner"; @@ -21,8 +14,7 @@ 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 { BotFatherWizard } from "@/components/mika/telegram/BotFatherWizard"; import { cn } from "@/lib/utils"; const WELCOME_DONE_KEY = "mika-welcome-done"; @@ -41,14 +33,6 @@ function WelcomePage() { const [step, setStep] = useState<1 | 2 | 3>(1); const [agentName, setAgentName] = useState(""); const [saving, setSaving] = useState(false); - const [wizardOpen, setWizardOpen] = useState(false); - - // Managed bot state - const [creatingBot, setCreatingBot] = useState(false); - const [waitingConfirm, setWaitingConfirm] = useState(false); - const [waitTimedOut, setWaitTimedOut] = useState(false); - const [previewUsername, setPreviewUsername] = useState(null); - const waitStartedAt = useRef(null); const fullName = (profile?.full_name || "").trim(); const firstName = useMemo( @@ -141,95 +125,28 @@ function WelcomePage() { } } - async function handleOpenManualWizard() { - await markWelcomeDone(); - setWaitingConfirm(false); - setWizardOpen(true); - } - async function handleSkip() { await markWelcomeDone(); navigate({ to: "/painel", search: {} }); } - // Sugere username localmente (apenas preview visual antes do clique) - useEffect(() => { - if (step !== 3) return; - if (agent?.managed_bot_suggested_username) { - setPreviewUsername(agent.managed_bot_suggested_username); - return; + async function handleActivated() { + await markWelcomeDone(); + if (agent) { + await supabase + .from("agent_instances") + .update({ telegram_onboarding_completed: true }) + .eq("id", agent.id); } - const base = (agentName || "mika") - .toLowerCase() - .normalize("NFD") - .replace(/[\u0300-\u036f]/g, "") - .replace(/[^a-z0-9]/g, "") - .substring(0, 28); - const safe = base.length >= 3 ? base : `mika${base}`; - setPreviewUsername(`${safe}bot`); - }, [step, agentName, agent?.managed_bot_suggested_username]); - - async function handleCreateManagedBot() { - if (!agent) { - toast.error("Aguarde, ainda estamos preparando seu agente…"); - return; - } - setCreatingBot(true); - setWaitTimedOut(false); - try { - const { data, error } = await supabase.functions.invoke<{ - url: string; - suggested_username: string; - manager_username: string; - }>("create-managed-bot", { - body: { - agent_instance_id: agent.id, - agent_name: agentName.trim(), - }, - }); - if (error || !data?.url) { - throw error ?? new Error("Resposta inválida"); - } - setPreviewUsername(data.suggested_username); - window.open(data.url, "_blank", "noopener,noreferrer"); - await markWelcomeDone(); - waitStartedAt.current = Date.now(); - setWaitingConfirm(true); - } catch (err) { - console.error(err); - toast.error( - "Não foi possível iniciar a criação do bot. Tente novamente ou use o modo manual.", - ); - } finally { - setCreatingBot(false); + if (user) { + await queryClient.invalidateQueries({ queryKey: ["agent-instance", user.id] }); } + toast.success( + "🎉 Perfeito! Seu agente está sendo ativado. Em alguns minutos você receberá uma mensagem no Telegram!", + ); + navigate({ to: "/painel", search: {} }); } - // Polling: enquanto aguardamos confirmação, useAgentInstance já refetch a cada 10s. - // Aceleramos o refetch a cada 3s e detectamos sucesso. - useEffect(() => { - if (!waitingConfirm) return; - if (!user) return; - const interval = setInterval(() => { - queryClient.invalidateQueries({ queryKey: ["agent-instance", user.id] }); - if (waitStartedAt.current && Date.now() - waitStartedAt.current > 5 * 60_000) { - setWaitingConfirm(false); - setWaitTimedOut(true); - } - }, 3000); - return () => clearInterval(interval); - }, [waitingConfirm, user, queryClient]); - - // Detecta sucesso da confirmação via webhook - useEffect(() => { - if (!waitingConfirm) return; - if (agent?.telegram_onboarding_completed && !agent.managed_bot_pending) { - setWaitingConfirm(false); - toast.success("🎉 Bot criado! Seu agente está sendo ativado."); - navigate({ to: "/painel", search: {} }); - } - }, [waitingConfirm, agent, navigate]); - if (authLoading || !user) { return (
    @@ -252,7 +169,7 @@ function WelcomePage() {
    -
    +
    {step === 1 && ( -

    - Crie seu bot em 1 toque 🚀 -

    -

    - Preparamos tudo para você. Basta confirmar no Telegram. -

    - - {/* Card de preview do bot */} -
    -
    -
    - -
    -
    -

    - {agentName || "Seu agente"} -

    -

    - @{previewUsername || "carregando…"} -

    -
    -
    +
    +

    + Conecte seu Telegram +

    +

    + Falta pouco! Vamos colocar a {agentName || "Mika"} para conversar com você. +

    - {/* Estado: aguardando confirmação */} - {waitingConfirm && ( -
    -
    - - - Aguardando confirmação no Telegram… - -
    -

    - Confirme as informações na conversa com @mika_managerbot. -

    -
    - )} - - {/* Estado: timeout */} - {waitTimedOut && ( -
    -

    - Não recebemos a confirmação ainda. Tente novamente ou faça - o processo manualmente. -

    -
    - )} - - {/* Ícones explicativos — só antes de iniciar */} - {!waitingConfirm && !waitTimedOut && ( -
    - - - -
    - )} - - {/* Ações */} -
    - {!waitingConfirm && !waitTimedOut && ( - - )} - - {waitTimedOut && ( - - )} - - - -
    + )}
    - - { - setWizardOpen(open); - if (!open) { - // Se o token foi salvo (vault_id presente), considera sucesso - if (agent?.telegram_bot_token_vault_id) { - toast.success( - "Perfeito! Seu agente está sendo ativado. Você receberá uma mensagem no Telegram quando estiver pronto! 🎉", - ); - } - navigate({ to: "/painel", search: {} }); - } - }} - /> -
    - ); -} - -function StepRow({ - number, - title, - hint, - extra, -}: { - number: number; - title: string; - hint?: string; - extra?: React.ReactNode; -}) { - return ( -
  • - - {number} - -
    -

    {title}

    - {hint &&

    {hint}

    } - {extra} -
    -
  • - ); -} - -function ExplainIcon({ emoji, label }: { emoji: string; label: string }) { - return ( -
    -
    {emoji}
    -

    {label}

    ); }