Co-authored-by: domfelipe <53182096+domfelipe@users.noreply.github.com>
This commit is contained in:
gpt-engineer-app[bot] 2026-04-17 20:01:59 +00:00
parent bebe095d6e
commit ce0108cec3
7 changed files with 475 additions and 0 deletions

View file

@ -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<string | null>(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 (
<div className="px-6 py-12 flex flex-col items-center justify-center min-h-[320px]">
{state === "loading" && (
<>
<Loader2 className="h-10 w-10 text-primary animate-spin" />
<p className="mt-4 text-sm text-muted-foreground">
Configurando recebimento de mensagens...
</p>
</>
)}
{state === "error" && (
<div className="w-full max-w-md rounded-lg border border-destructive bg-destructive/5 p-6 text-center">
<AlertCircle className="mx-auto h-8 w-8 text-destructive" />
<h3 className="mt-3 font-semibold">Falha ao configurar webhook</h3>
{error && <p className="mt-2 text-sm text-muted-foreground">{error}</p>}
<div className="mt-5 flex flex-col sm:flex-row gap-2 justify-center">
<Button onClick={() => setAttempt((a) => a + 1)}>Tentar novamente</Button>
<Button variant="ghost" onClick={onSkip}>
Pular (configurar depois)
</Button>
</div>
</div>
)}
</div>
);
}

View file

@ -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 (
<div className="px-6 py-8 max-w-2xl mx-auto">
<h2 className="text-2xl font-bold tracking-tight">Crie seu bot no BotFather</h2>
<p className="mt-2 text-muted-foreground">
O BotFather é o bot oficial do Telegram para criar outros bots. É gratuito e leva 1 minuto.
</p>
<Button asChild size="lg" className="mt-6 w-full sm:w-auto">
<a href="https://t.me/BotFather?start" target="_blank" rel="noopener noreferrer">
Abrir BotFather <ExternalLink className="ml-2 h-4 w-4" />
</a>
</Button>
<ol className="mt-8 space-y-3">
{STEPS.map((step, i) => (
<li
key={step}
className="flex items-start gap-3 rounded-lg border border-border bg-card p-4"
>
<span className="h-7 w-7 rounded-full bg-primary text-primary-foreground flex items-center justify-center font-bold text-sm shrink-0">
{i + 1}
</span>
<span className="text-sm pt-1">{step}</span>
</li>
))}
</ol>
<div className="mt-8 flex justify-end">
<Button variant="secondary" onClick={onNext}>
criei meu bot
</Button>
</div>
</div>
);
}

View file

@ -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 (
<div className="px-6 py-8 max-w-2xl mx-auto">
<h2 className="text-2xl font-bold tracking-tight">Escolha os nomes</h2>
<p className="mt-2 text-muted-foreground">
O BotFather vai pedir dois nomes. Use nossas sugestões se quiser.
</p>
<div className="mt-6 space-y-4">
<CopyField
label="Nome do bot"
value={suggestedName}
help="Esse é o nome que aparece no topo da conversa."
/>
<CopyField
label="Username (termina em bot)"
value={suggestedUsername}
help="Se o username já estiver em uso, tente adicionar um número no final."
/>
</div>
<div className="mt-8 flex justify-end">
<Button onClick={onNext}>Próximo: colar o token</Button>
</div>
</div>
);
}
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 (
<div className="rounded-lg border border-border bg-card p-4">
<label className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
{label}
</label>
<div className="mt-2 flex gap-2">
<Input value={value} readOnly className="font-mono text-sm" />
<Button type="button" variant="outline" size="icon" onClick={copy} aria-label="Copiar">
{copied ? <Check className="h-4 w-4 text-success" /> : <Copy className="h-4 w-4" />}
</Button>
</div>
{help && <p className="mt-2 text-xs text-muted-foreground">{help}</p>}
</div>
);
}

View file

@ -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<State>(validated ? "success" : "idle");
const [errorMsg, setErrorMsg] = useState<string | null>(null);
async function handleValidate() {
setState("validating");
setErrorMsg(null);
const { data, error } = await invokeFunction<ValidatedBot>("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 (
<div className="px-6 py-8 max-w-2xl mx-auto">
<h2 className="text-2xl font-bold tracking-tight">Cole o token do seu bot</h2>
<p className="mt-2 text-muted-foreground">
No final da conversa, o BotFather te enviou um token parecido com{" "}
<span className="font-mono text-xs">8234567890:ABC-DEF...</span>. Cole ele abaixo.
</p>
<div
className={cn(
"mt-6 rounded-lg border bg-card p-4 transition-colors",
state === "success" && "border-success bg-success/5",
state === "error" && "border-destructive bg-destructive/5",
)}
>
<Input
type="text"
placeholder="123456789:ABC-DEF..."
value={token}
onChange={(e) => {
setToken(e.target.value);
if (state === "error") setState("idle");
}}
disabled={state === "validating" || state === "success"}
className="font-mono text-sm"
/>
<Button
className="mt-3 w-full"
onClick={handleValidate}
disabled={!token.trim() || state === "validating" || state === "success"}
>
{state === "validating" && (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> Validando...
</>
)}
{state === "success" && (
<>
<Check className="mr-2 h-4 w-4" /> Bot validado!
</>
)}
{(state === "idle" || state === "error") && "Validar conexão"}
</Button>
{state === "error" && errorMsg && (
<div className="mt-3 flex items-start gap-2 text-sm text-destructive">
<AlertCircle className="h-4 w-4 mt-0.5 shrink-0" />
<p>{errorMsg}</p>
</div>
)}
{state === "success" && validated && (
<div className="mt-4 rounded-md bg-success/10 border border-success/30 p-3">
<p className="text-sm font-semibold text-success-foreground">{validated.bot_name}</p>
<p className="text-xs text-muted-foreground">@{validated.bot_username}</p>
</div>
)}
</div>
<div className="mt-6 flex justify-end">
<Button onClick={onNext} disabled={state !== "success"}>
Próximo
</Button>
</div>
</div>
);
}

View file

@ -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 (
<div className="px-6 py-8 max-w-xl mx-auto">
<AnimatePresence mode="wait">
{!received ? (
<motion.div
key="waiting"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0 }}
className="text-center"
>
<h2 className="text-2xl font-bold tracking-tight">
Mande qualquer mensagem para seu Mika agora
</h2>
<p className="mt-2 text-muted-foreground">
Estou aguardando sua primeira mensagem. Pode ser "oi". 😊
</p>
<div className="mt-8 rounded-2xl border-2 border-primary bg-primary/5 p-8 animate-pulse">
<MessageCircle className="mx-auto h-12 w-12 text-primary" />
<p className="mt-4 text-sm font-medium">Aguardando sua primeira mensagem...</p>
</div>
<Button asChild size="lg" className="mt-6">
<a
href={`https://t.me/${botUsername}`}
target="_blank"
rel="noopener noreferrer"
>
Abrir meu bot no Telegram <ExternalLink className="ml-2 h-4 w-4" />
</a>
</Button>
</motion.div>
) : (
<motion.div
key="success"
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
className="relative text-center"
>
{/* emojis flutuantes */}
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
{EMOJIS.map((emoji, i) => (
<motion.span
key={i}
initial={{ opacity: 0, scale: 0.4, x: 0, y: 0, rotate: 0 }}
animate={{
opacity: [0, 1, 0],
scale: [0.4, 1.2, 0.8],
x: positions[i].x,
y: positions[i].y,
rotate: positions[i].rotate,
}}
transition={{
duration: 1.5,
delay: i * 0.05,
ease: "easeOut",
}}
className="absolute text-3xl"
>
{emoji}
</motion.span>
))}
</div>
<div className="relative">
<div className="mx-auto h-16 w-16 rounded-full bg-success/10 flex items-center justify-center">
<CheckCircle2 className="h-9 w-9 text-success" />
</div>
<h2 className="mt-4 text-2xl font-bold tracking-tight">🎉 Mika conectado!</h2>
<p className="mt-2 text-muted-foreground">
Você recebeu a primeira resposta do seu agente. Ele ainda está em modo de teste,
mas em breve vai responder de verdade.
</p>
<Button size="lg" className="mt-8 min-w-56" onClick={onFinish}>
Finalizar onboarding
</Button>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
);
}

View file

@ -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 (
<div className="flex flex-col items-center text-center px-6 py-8">
<div className="h-20 w-20 rounded-full bg-primary/10 flex items-center justify-center mb-6">
<TelegramIcon className="h-10 w-10 text-primary" />
</div>
<h2 className="text-2xl sm:text-3xl font-bold tracking-tight">
Vamos conectar seu Telegram em 30 segundos
</h2>
<p className="mt-3 text-muted-foreground max-w-md">
Seu agente Mika ficará disponível diretamente no Telegram onde você passa seu dia.
</p>
<ul className="mt-8 space-y-3 text-left max-w-sm w-full">
<Bullet icon={<Sparkles className="h-4 w-4" />}>
Criação gratuita via BotFather
</Bullet>
<Bullet icon={<Shield className="h-4 w-4" />}>
Nenhum dado sensível compartilhado conosco
</Bullet>
<Bullet icon={<Smartphone className="h-4 w-4" />}>
Funciona no celular, desktop e web
</Bullet>
</ul>
<Button size="lg" className="mt-8 min-w-48" onClick={onNext}>
Vamos começar
</Button>
</div>
);
}
function Bullet({ icon, children }: { icon: React.ReactNode; children: React.ReactNode }) {
return (
<li className="flex items-center gap-3">
<span className="h-8 w-8 rounded-full bg-primary/10 text-primary flex items-center justify-center shrink-0">
{icon}
</span>
<span className="text-sm text-foreground">{children}</span>
</li>
);
}

View file

@ -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 <Send className={className} aria-hidden="true" />;
}