mirror of
https://github.com/domfelipe/mika-agent-assist.git
synced 2026-08-07 11:16:46 +00:00
Reescreveu etapa 3 do Bem-Vindo
X-Lovable-Edit-ID: edt-c8403ce8-f189-47d7-8108-15f6556aaf30 Co-authored-by: domfelipe <53182096+domfelipe@users.noreply.github.com>
This commit is contained in:
commit
8ac80f09a6
2 changed files with 476 additions and 250 deletions
447
src/components/mika/telegram/BotFatherWizard.tsx
Normal file
447
src/components/mika/telegram/BotFatherWizard.tsx
Normal file
|
|
@ -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<string | null>(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 (
|
||||||
|
<div className="grid gap-8 lg:grid-cols-2 lg:gap-10 items-start">
|
||||||
|
{/* COLUNA ESQUERDA — passos */}
|
||||||
|
<div className="text-left">
|
||||||
|
<h2 className="text-2xl font-bold text-white">
|
||||||
|
Crie seu bot em 2 minutos
|
||||||
|
</h2>
|
||||||
|
<p className="mt-2 text-sm text-white/60">
|
||||||
|
Siga os passos abaixo. Cada um leva alguns segundos.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ol className="mt-6 space-y-4">
|
||||||
|
<Step
|
||||||
|
number={1}
|
||||||
|
emoji="📱"
|
||||||
|
title="Abra o BotFather"
|
||||||
|
description="O BotFather é o bot oficial do Telegram para criar bots."
|
||||||
|
done={step1Done}
|
||||||
|
highlight={!step1Done}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
onClick={handleOpenBotFather}
|
||||||
|
className="mt-3"
|
||||||
|
>
|
||||||
|
Abrir BotFather
|
||||||
|
<ExternalLink className="ml-2 h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step
|
||||||
|
number={2}
|
||||||
|
emoji="🤖"
|
||||||
|
title="Escolha um nome"
|
||||||
|
description="Quando o BotFather perguntar o nome, use:"
|
||||||
|
done={step2Done}
|
||||||
|
highlight={step1Done && !step2Done}
|
||||||
|
>
|
||||||
|
<CopyChip
|
||||||
|
value={agentName || "Mika"}
|
||||||
|
onCopied={() => setStep2Done(true)}
|
||||||
|
/>
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step
|
||||||
|
number={3}
|
||||||
|
emoji="@"
|
||||||
|
title="Escolha um username"
|
||||||
|
description="Quando pedir o username (deve terminar em 'bot'), use:"
|
||||||
|
done={step3Done}
|
||||||
|
highlight={step2Done && !step3Done}
|
||||||
|
>
|
||||||
|
<CopyChip
|
||||||
|
value={suggestedUsername}
|
||||||
|
onCopied={() => setStep3Done(true)}
|
||||||
|
/>
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step
|
||||||
|
number={4}
|
||||||
|
emoji="🔑"
|
||||||
|
title="Cole o token aqui"
|
||||||
|
description="O BotFather vai te enviar um token. Cole ele abaixo:"
|
||||||
|
done={tokenValid}
|
||||||
|
highlight={step3Done && !tokenValid}
|
||||||
|
>
|
||||||
|
<div className="mt-3 space-y-2">
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
value={token}
|
||||||
|
onChange={(e) => {
|
||||||
|
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 && (
|
||||||
|
<p className="flex items-center gap-1.5 text-xs font-medium text-emerald-400">
|
||||||
|
<Check className="h-3.5 w-3.5" /> Token válido!
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{errorMsg && (
|
||||||
|
<p className="flex items-start gap-1.5 text-xs text-red-300">
|
||||||
|
<AlertCircle className="mt-0.5 h-3.5 w-3.5 shrink-0" />
|
||||||
|
{errorMsg}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Step>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<div className="mt-8 flex flex-col items-center gap-3">
|
||||||
|
<Button
|
||||||
|
size="lg"
|
||||||
|
className="w-full sm:min-w-64 sm:w-auto"
|
||||||
|
disabled={!tokenValid || submitting}
|
||||||
|
onClick={handleActivate}
|
||||||
|
>
|
||||||
|
{submitting ? (
|
||||||
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
|
) : null}
|
||||||
|
Ativar meu agente
|
||||||
|
<ArrowRight className="ml-2 h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onSkip}
|
||||||
|
className="text-xs text-white/50 hover:text-white/80 underline-offset-4 hover:underline"
|
||||||
|
>
|
||||||
|
Fazer isso depois
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* COLUNA DIREITA — preview animado (apenas desktop) */}
|
||||||
|
<aside className="hidden lg:block">
|
||||||
|
<ChatPreview agentName={agentName} firstName={fullName.split(" ")[0] || "você"} />
|
||||||
|
<ul className="mt-6 space-y-2 text-sm text-white/80">
|
||||||
|
{[
|
||||||
|
"Responder perguntas e pesquisar",
|
||||||
|
"Gerenciar sua agenda e lembretes",
|
||||||
|
"Resumir emails importantes",
|
||||||
|
"Criar automações personalizadas",
|
||||||
|
"Memória persistente entre conversas",
|
||||||
|
].map((item) => (
|
||||||
|
<li key={item} className="flex items-start gap-2">
|
||||||
|
<span className="text-emerald-400 mt-0.5">✅</span>
|
||||||
|
<span>{item}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<li
|
||||||
|
className={cn(
|
||||||
|
"rounded-xl border p-4 transition-all",
|
||||||
|
done
|
||||||
|
? "border-emerald-500/30 bg-emerald-500/5"
|
||||||
|
: highlight
|
||||||
|
? "border-primary/50 bg-primary/5"
|
||||||
|
: "border-white/10 bg-white/5",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"h-8 w-8 shrink-0 rounded-full flex items-center justify-center font-bold text-sm",
|
||||||
|
done
|
||||||
|
? "bg-emerald-500 text-white"
|
||||||
|
: "bg-primary text-primary-foreground",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{done ? <Check className="h-4 w-4" /> : number}
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<p className="text-sm font-semibold text-white">
|
||||||
|
<span className="mr-1.5">{emoji}</span>
|
||||||
|
{title}
|
||||||
|
</p>
|
||||||
|
<p className="mt-0.5 text-xs text-white/60">{description}</p>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleCopy}
|
||||||
|
className={cn(
|
||||||
|
"mt-3 group flex w-full items-center justify-between gap-3 rounded-lg border px-3 py-2.5 text-left transition-colors",
|
||||||
|
"border-white/15 bg-white/5 hover:bg-white/10",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span className="font-mono text-sm text-white truncate">{value}</span>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"flex items-center gap-1.5 text-xs font-medium shrink-0",
|
||||||
|
copied ? "text-emerald-400" : "text-white/60 group-hover:text-white",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{copied ? (
|
||||||
|
<>
|
||||||
|
<Check className="h-3.5 w-3.5" /> Copiado
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Copy className="h-3.5 w-3.5" /> Copiar
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div className="rounded-2xl border border-white/10 bg-[oklch(0.18_0.03_265)] p-5 shadow-2xl">
|
||||||
|
<p className="text-xs font-medium uppercase tracking-wide text-white/40">
|
||||||
|
Prévia do seu bot
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="mt-3 flex items-center gap-3 border-b border-white/10 pb-3">
|
||||||
|
<div className="h-10 w-10 rounded-full bg-primary/20 flex items-center justify-center text-lg">
|
||||||
|
🤖
|
||||||
|
</div>
|
||||||
|
<div className="min-w-0">
|
||||||
|
<p className="text-sm font-semibold text-white truncate">{safeName}</p>
|
||||||
|
<p className="text-xs text-emerald-400">online</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-4 space-y-2 min-h-[180px]">
|
||||||
|
{/* Mensagem do usuário */}
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<div className="rounded-2xl rounded-br-sm bg-primary px-3 py-1.5 text-sm text-primary-foreground">
|
||||||
|
Oi!
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Typing indicator */}
|
||||||
|
<AnimatePresence>
|
||||||
|
{cycle === 1 && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 6 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
className="flex justify-start"
|
||||||
|
>
|
||||||
|
<div className="rounded-2xl rounded-bl-sm bg-white/10 px-3 py-2 flex gap-1">
|
||||||
|
{[0, 1, 2].map((i) => (
|
||||||
|
<motion.span
|
||||||
|
key={i}
|
||||||
|
className="h-1.5 w-1.5 rounded-full bg-white/60"
|
||||||
|
animate={{ opacity: [0.3, 1, 0.3] }}
|
||||||
|
transition={{
|
||||||
|
duration: 1,
|
||||||
|
repeat: Infinity,
|
||||||
|
delay: i * 0.2,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
|
||||||
|
{/* Resposta do bot */}
|
||||||
|
{(cycle === 2 || cycle === 3) && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 6 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
className="flex justify-start"
|
||||||
|
>
|
||||||
|
<div className="max-w-[85%] rounded-2xl rounded-bl-sm bg-white/10 px-3 py-2 text-sm text-white">
|
||||||
|
{cycle === 2 ? typed : reply}
|
||||||
|
{cycle === 2 && (
|
||||||
|
<span className="ml-0.5 inline-block h-3.5 w-px bg-white/80 align-middle animate-pulse" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -3,14 +3,7 @@
|
||||||
import { createFileRoute, useNavigate } from "@tanstack/react-router";
|
import { createFileRoute, useNavigate } from "@tanstack/react-router";
|
||||||
import { useEffect, useMemo, useRef, useState } from "react";
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { motion, AnimatePresence } from "framer-motion";
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
import {
|
import { ArrowRight, Loader2, Sparkles } from "lucide-react";
|
||||||
ArrowRight,
|
|
||||||
CheckCircle2,
|
|
||||||
ExternalLink,
|
|
||||||
Loader2,
|
|
||||||
RefreshCcw,
|
|
||||||
Sparkles,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
|
@ -21,8 +14,7 @@ import { supabase } from "@/integrations/supabase/client";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Logo } from "@/components/mika/Logo";
|
import { Logo } from "@/components/mika/Logo";
|
||||||
import { TelegramIcon } from "@/components/mika/telegram/TelegramIcon";
|
import { BotFatherWizard } from "@/components/mika/telegram/BotFatherWizard";
|
||||||
import { TelegramOnboardingWizard } from "@/components/mika/telegram/TelegramOnboardingWizard";
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const WELCOME_DONE_KEY = "mika-welcome-done";
|
const WELCOME_DONE_KEY = "mika-welcome-done";
|
||||||
|
|
@ -41,14 +33,6 @@ function WelcomePage() {
|
||||||
const [step, setStep] = useState<1 | 2 | 3>(1);
|
const [step, setStep] = useState<1 | 2 | 3>(1);
|
||||||
const [agentName, setAgentName] = useState("");
|
const [agentName, setAgentName] = useState("");
|
||||||
const [saving, setSaving] = useState(false);
|
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<string | null>(null);
|
|
||||||
const waitStartedAt = useRef<number | null>(null);
|
|
||||||
|
|
||||||
const fullName = (profile?.full_name || "").trim();
|
const fullName = (profile?.full_name || "").trim();
|
||||||
const firstName = useMemo(
|
const firstName = useMemo(
|
||||||
|
|
@ -141,94 +125,27 @@ function WelcomePage() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleOpenManualWizard() {
|
|
||||||
await markWelcomeDone();
|
|
||||||
setWaitingConfirm(false);
|
|
||||||
setWizardOpen(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleSkip() {
|
async function handleSkip() {
|
||||||
await markWelcomeDone();
|
await markWelcomeDone();
|
||||||
navigate({ to: "/painel", search: {} });
|
navigate({ to: "/painel", search: {} });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sugere username localmente (apenas preview visual antes do clique)
|
async function handleActivated() {
|
||||||
useEffect(() => {
|
|
||||||
if (step !== 3) return;
|
|
||||||
if (agent?.managed_bot_suggested_username) {
|
|
||||||
setPreviewUsername(agent.managed_bot_suggested_username);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
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();
|
await markWelcomeDone();
|
||||||
waitStartedAt.current = Date.now();
|
if (agent) {
|
||||||
setWaitingConfirm(true);
|
await supabase
|
||||||
} catch (err) {
|
.from("agent_instances")
|
||||||
console.error(err);
|
.update({ telegram_onboarding_completed: true })
|
||||||
toast.error(
|
.eq("id", agent.id);
|
||||||
"Não foi possível iniciar a criação do bot. Tente novamente ou use o modo manual.",
|
}
|
||||||
|
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!",
|
||||||
);
|
);
|
||||||
} finally {
|
|
||||||
setCreatingBot(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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: {} });
|
navigate({ to: "/painel", search: {} });
|
||||||
}
|
}
|
||||||
}, [waitingConfirm, agent, navigate]);
|
|
||||||
|
|
||||||
if (authLoading || !user) {
|
if (authLoading || !user) {
|
||||||
return (
|
return (
|
||||||
|
|
@ -252,7 +169,7 @@ function WelcomePage() {
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main className="flex-1 flex items-center justify-center px-4 py-8">
|
<main className="flex-1 flex items-center justify-center px-4 py-8">
|
||||||
<div className="w-full max-w-2xl">
|
<div className={cn("w-full", step === 3 ? "max-w-5xl" : "max-w-2xl")}>
|
||||||
<AnimatePresence mode="wait" initial={false}>
|
<AnimatePresence mode="wait" initial={false}>
|
||||||
{step === 1 && (
|
{step === 1 && (
|
||||||
<motion.section
|
<motion.section
|
||||||
|
|
@ -378,165 +295,27 @@ function WelcomePage() {
|
||||||
animate={{ opacity: 1, x: 0 }}
|
animate={{ opacity: 1, x: 0 }}
|
||||||
exit={{ opacity: 0, x: -60 }}
|
exit={{ opacity: 0, x: -60 }}
|
||||||
transition={{ duration: 0.3, ease: "easeOut" }}
|
transition={{ duration: 0.3, ease: "easeOut" }}
|
||||||
className="text-center"
|
|
||||||
>
|
>
|
||||||
<h2 className="text-3xl font-bold text-white">
|
<div className="text-center mb-8">
|
||||||
Crie seu bot em 1 toque 🚀
|
<h1 className="text-3xl sm:text-4xl font-bold text-white">
|
||||||
</h2>
|
Conecte seu Telegram
|
||||||
<p className="mt-3 text-white/70 max-w-lg mx-auto">
|
</h1>
|
||||||
Preparamos tudo para você. Basta confirmar no Telegram.
|
<p className="mt-2 text-white/70">
|
||||||
</p>
|
Falta pouco! Vamos colocar a {agentName || "Mika"} para conversar com você.
|
||||||
|
|
||||||
{/* Card de preview do bot */}
|
|
||||||
<div className="mx-auto mt-8 max-w-md rounded-2xl border border-white/10 bg-white/5 p-5 text-left">
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<div className="h-14 w-14 rounded-full bg-primary/20 flex items-center justify-center shrink-0">
|
|
||||||
<TelegramIcon className="h-7 w-7 text-primary" />
|
|
||||||
</div>
|
|
||||||
<div className="min-w-0 flex-1">
|
|
||||||
<p className="text-base font-semibold text-white truncate">
|
|
||||||
{agentName || "Seu agente"}
|
|
||||||
</p>
|
|
||||||
<p className="text-sm text-white/60 truncate">
|
|
||||||
@{previewUsername || "carregando…"}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Estado: aguardando confirmação */}
|
<BotFatherWizard
|
||||||
{waitingConfirm && (
|
agentName={agentName}
|
||||||
<div className="mt-8 max-w-md mx-auto">
|
fullName={fullName}
|
||||||
<div className="flex items-center justify-center gap-3 text-white">
|
onActivated={handleActivated}
|
||||||
<Loader2 className="h-5 w-5 animate-spin text-primary" />
|
onSkip={handleSkip}
|
||||||
<span className="text-sm font-medium">
|
/>
|
||||||
Aguardando confirmação no Telegram…
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<p className="mt-2 text-xs text-white/50">
|
|
||||||
Confirme as informações na conversa com @mika_managerbot.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Estado: timeout */}
|
|
||||||
{waitTimedOut && (
|
|
||||||
<div className="mt-8 max-w-md mx-auto rounded-lg border border-amber-500/30 bg-amber-500/10 p-4 text-left">
|
|
||||||
<p className="text-sm text-amber-100">
|
|
||||||
Não recebemos a confirmação ainda. Tente novamente ou faça
|
|
||||||
o processo manualmente.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Ícones explicativos — só antes de iniciar */}
|
|
||||||
{!waitingConfirm && !waitTimedOut && (
|
|
||||||
<div className="mt-8 grid grid-cols-3 gap-3 max-w-md mx-auto">
|
|
||||||
<ExplainIcon emoji="📱" label="Abre no Telegram" />
|
|
||||||
<ExplainIcon emoji="✅" label="Confirma as informações" />
|
|
||||||
<ExplainIcon emoji="🎉" label="Bot criado automaticamente" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Ações */}
|
|
||||||
<div className="mt-10 flex flex-col items-center gap-3">
|
|
||||||
{!waitingConfirm && !waitTimedOut && (
|
|
||||||
<Button
|
|
||||||
size="lg"
|
|
||||||
className="min-w-64"
|
|
||||||
onClick={handleCreateManagedBot}
|
|
||||||
disabled={creatingBot || !agent}
|
|
||||||
>
|
|
||||||
{creatingBot ? (
|
|
||||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
||||||
) : null}
|
|
||||||
Criar meu bot no Telegram
|
|
||||||
<ArrowRight className="ml-2 h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{waitTimedOut && (
|
|
||||||
<Button
|
|
||||||
size="lg"
|
|
||||||
className="min-w-64"
|
|
||||||
onClick={handleCreateManagedBot}
|
|
||||||
disabled={creatingBot}
|
|
||||||
>
|
|
||||||
<RefreshCcw className="mr-2 h-4 w-4" />
|
|
||||||
Tentar novamente
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={handleOpenManualWizard}
|
|
||||||
className="text-sm text-white/70 hover:text-white underline-offset-4 hover:underline"
|
|
||||||
>
|
|
||||||
Prefiro configurar manualmente
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={handleSkip}
|
|
||||||
className="text-xs text-white/40 hover:text-white/70 underline-offset-4 hover:underline"
|
|
||||||
>
|
|
||||||
Fazer isso depois
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</motion.section>
|
</motion.section>
|
||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<TelegramOnboardingWizard
|
|
||||||
open={wizardOpen}
|
|
||||||
onOpenChange={(open) => {
|
|
||||||
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: {} });
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function StepRow({
|
|
||||||
number,
|
|
||||||
title,
|
|
||||||
hint,
|
|
||||||
extra,
|
|
||||||
}: {
|
|
||||||
number: number;
|
|
||||||
title: string;
|
|
||||||
hint?: string;
|
|
||||||
extra?: React.ReactNode;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<li className="flex items-start gap-3 rounded-lg border border-white/10 bg-white/5 p-4">
|
|
||||||
<span className="h-7 w-7 shrink-0 rounded-full bg-primary text-primary-foreground flex items-center justify-center font-bold text-sm">
|
|
||||||
{number}
|
|
||||||
</span>
|
|
||||||
<div className="flex-1">
|
|
||||||
<p className="text-sm font-medium text-white">{title}</p>
|
|
||||||
{hint && <p className="mt-1 text-xs text-white/60">{hint}</p>}
|
|
||||||
{extra}
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ExplainIcon({ emoji, label }: { emoji: string; label: string }) {
|
|
||||||
return (
|
|
||||||
<div className="rounded-lg border border-white/10 bg-white/5 p-3 text-center">
|
|
||||||
<div className="text-2xl">{emoji}</div>
|
|
||||||
<p className="mt-1 text-[11px] leading-tight text-white/70">{label}</p>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue