From bebe095d6ee710fc0307e229f6fd1264c565221e 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:00:56 +0000 Subject: [PATCH 1/8] Changes Co-authored-by: domfelipe <53182096+domfelipe@users.noreply.github.com> --- src/hooks/use-agent-instance.ts | 12 ++- src/hooks/use-telegram-first-message.ts | 103 ++++++++++++++++++++++++ src/lib/invoke-function.ts | 50 ++++++++++++ src/lib/telegram-username.ts | 25 ++++++ 4 files changed, 188 insertions(+), 2 deletions(-) create mode 100644 src/hooks/use-telegram-first-message.ts create mode 100644 src/lib/invoke-function.ts create mode 100644 src/lib/telegram-username.ts diff --git a/src/hooks/use-agent-instance.ts b/src/hooks/use-agent-instance.ts index 0ba6fcd..0de6cc4 100644 --- a/src/hooks/use-agent-instance.ts +++ b/src/hooks/use-agent-instance.ts @@ -7,8 +7,14 @@ import { useAuth } from "@/hooks/use-auth"; export interface AgentInstance { id: string; user_id: string; + uuid_tenant: string; status: string; telegram_bot_username: string | null; + telegram_webhook_configured: boolean; + telegram_token_invalid: boolean; + telegram_first_message_received_at: string | null; + telegram_connected_at: string | null; + telegram_onboarding_completed: boolean; created_at: string; } @@ -20,11 +26,13 @@ export function useAgentInstance() { queryFn: async (): Promise => { const { data, error } = await supabase .from("agent_instances") - .select("id, user_id, status, telegram_bot_username, created_at") + .select( + "id, user_id, uuid_tenant, status, telegram_bot_username, telegram_webhook_configured, telegram_token_invalid, telegram_first_message_received_at, telegram_connected_at, telegram_onboarding_completed, created_at", + ) .eq("user_id", user!.id) .maybeSingle(); if (error) throw error; - return data; + return data as AgentInstance | null; }, }); } diff --git a/src/hooks/use-telegram-first-message.ts b/src/hooks/use-telegram-first-message.ts new file mode 100644 index 0000000..b477f3a --- /dev/null +++ b/src/hooks/use-telegram-first-message.ts @@ -0,0 +1,103 @@ +"use client"; + +import { useEffect, useRef, useState } from "react"; +import { supabase } from "@/integrations/supabase/client"; + +interface FirstMessage { + id: string; + created_at: string; + message_text: string | null; + message_type: string; +} + +/** + * Aguarda a primeira mensagem incoming do Telegram para o agent_instance dado. + * - Subscribe via Realtime com filter explícito. + * - Fallback de polling a cada 5s caso o Realtime não conecte em 10s. + * - Considera apenas mensagens com created_at >= since (telegram_connected_at). + */ +export function useTelegramFirstMessage(opts: { + agentInstanceId: string | null | undefined; + since: string | null | undefined; + enabled: boolean; +}): { received: FirstMessage | null; reset: () => void } { + const { agentInstanceId, since, enabled } = opts; + const [received, setReceived] = useState(null); + const realtimeConnected = useRef(false); + const pollingRef = useRef(null); + + function reset() { + setReceived(null); + } + + useEffect(() => { + if (!enabled || !agentInstanceId) return; + + let cancelled = false; + realtimeConnected.current = false; + + const channel = supabase + .channel(`telegram-messages-${agentInstanceId}`) + .on( + "postgres_changes", + { + event: "INSERT", + schema: "public", + table: "telegram_messages_log", + filter: `agent_instance_id=eq.${agentInstanceId}`, + }, + (payload) => { + // deno-lint-ignore no-explicit-any + const row = payload.new as any; + if (row?.direction !== "incoming") return; + if (since && new Date(row.created_at) < new Date(since)) return; + if (cancelled) return; + setReceived({ + id: row.id, + created_at: row.created_at, + message_text: row.message_text ?? null, + message_type: row.message_type ?? "text", + }); + }, + ) + .subscribe((status) => { + if (status === "SUBSCRIBED") { + realtimeConnected.current = true; + } + }); + + // Fallback de polling: começa em 10s caso Realtime ainda não tenha conectado + const fallbackTimer = window.setTimeout(() => { + if (realtimeConnected.current || cancelled) return; + pollingRef.current = window.setInterval(async () => { + if (cancelled) return; + let q = supabase + .from("telegram_messages_log") + .select("id, created_at, message_text, message_type, direction") + .eq("agent_instance_id", agentInstanceId) + .eq("direction", "incoming") + .order("created_at", { ascending: false }) + .limit(1); + if (since) q = q.gte("created_at", since); + const { data } = await q.maybeSingle(); + if (data && !cancelled) { + setReceived({ + id: data.id as string, + created_at: data.created_at as string, + message_text: (data.message_text as string | null) ?? null, + message_type: (data.message_type as string) ?? "text", + }); + } + }, 5000); + }, 10_000); + + return () => { + cancelled = true; + window.clearTimeout(fallbackTimer); + if (pollingRef.current) window.clearInterval(pollingRef.current); + supabase.removeChannel(channel); + }; + }, [agentInstanceId, since, enabled]); + + return { received, reset }; +} diff --git a/src/lib/invoke-function.ts b/src/lib/invoke-function.ts new file mode 100644 index 0000000..ea77c29 --- /dev/null +++ b/src/lib/invoke-function.ts @@ -0,0 +1,50 @@ +"use client"; + +import { supabase } from "@/integrations/supabase/client"; + +interface InvokeResult { + data: T | null; + error: { message: string; status?: number } | null; +} + +/** + * Wrapper para supabase.functions.invoke que normaliza erros de função + * (FunctionsHttpError vem com Response no .context que precisa ser lido). + */ +export async function invokeFunction( + name: string, + body?: Record, +): Promise> { + try { + const { data, error } = await supabase.functions.invoke(name, { + body: body ?? {}, + }); + + if (error) { + // Tenta extrair mensagem do response real + let msg = error.message ?? "Erro inesperado"; + let status: number | undefined; + // deno-lint-ignore no-explicit-any + const ctx = (error as any).context as Response | undefined; + if (ctx && typeof ctx.json === "function") { + try { + status = ctx.status; + const parsed = await ctx.json(); + if (parsed?.error) msg = parsed.error; + } catch { + // ignora + } + } + return { data: null, error: { message: msg, status } }; + } + + return { data: (data ?? null) as T | null, error: null }; + } catch (err) { + return { + data: null, + error: { + message: err instanceof Error ? err.message : "Erro inesperado", + }, + }; + } +} diff --git a/src/lib/telegram-username.ts b/src/lib/telegram-username.ts new file mode 100644 index 0000000..2f802a8 --- /dev/null +++ b/src/lib/telegram-username.ts @@ -0,0 +1,25 @@ +"use client"; + +/** + * Sanitiza um primeiro nome para uso em sugestões de username do Telegram. + * - remove acentos via NFD + * - remove caracteres não-alfanuméricos + * - lowercase + */ +export function sanitizeForUsername(name: string): string { + return (name || "") + .normalize("NFD") + .replace(/[\u0300-\u036f]/g, "") + .replace(/[^a-zA-Z0-9]/g, "") + .toLowerCase(); +} + +export function suggestBotName(fullName: string | null | undefined): string { + const first = (fullName || "").trim().split(/\s+/)[0] || "Você"; + return `Mika de ${first}`; +} + +export function suggestBotUsername(fullName: string | null | undefined): string { + const first = sanitizeForUsername((fullName || "").trim().split(/\s+/)[0] || "voce"); + return `mika_${first || "voce"}_bot`; +} From ce0108cec3c59c6cdb9e91ace2f15d5b580348f3 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:01:59 +0000 Subject: [PATCH 2/8] Changes Co-authored-by: domfelipe <53182096+domfelipe@users.noreply.github.com> --- .../mika/telegram/StepConfiguring.tsx | 67 ++++++++++ .../mika/telegram/StepCreateBot.tsx | 47 +++++++ src/components/mika/telegram/StepNaming.tsx | 71 +++++++++++ src/components/mika/telegram/StepToken.tsx | 111 ++++++++++++++++ src/components/mika/telegram/StepWaiting.tsx | 119 ++++++++++++++++++ src/components/mika/telegram/StepWelcome.tsx | 48 +++++++ src/components/mika/telegram/TelegramIcon.tsx | 12 ++ 7 files changed, 475 insertions(+) create mode 100644 src/components/mika/telegram/StepConfiguring.tsx create mode 100644 src/components/mika/telegram/StepCreateBot.tsx create mode 100644 src/components/mika/telegram/StepNaming.tsx create mode 100644 src/components/mika/telegram/StepToken.tsx create mode 100644 src/components/mika/telegram/StepWaiting.tsx create mode 100644 src/components/mika/telegram/StepWelcome.tsx create mode 100644 src/components/mika/telegram/TelegramIcon.tsx 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/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