mirror of
https://github.com/domfelipe/mika-agent-assist.git
synced 2026-08-07 09:36:43 +00:00
Changes
Co-authored-by: domfelipe <53182096+domfelipe@users.noreply.github.com>
This commit is contained in:
parent
ce0108cec3
commit
93ee695c0e
4 changed files with 466 additions and 0 deletions
76
src/components/mika/telegram/DisconnectTelegramDialog.tsx
Normal file
76
src/components/mika/telegram/DisconnectTelegramDialog.tsx
Normal file
|
|
@ -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 (
|
||||
<AlertDialog open={open} onOpenChange={onOpenChange}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Desconectar Telegram?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Tem certeza? Você precisará criar um novo bot no BotFather para reconectar.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={loading}>Cancelar</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
handleDisconnect();
|
||||
}}
|
||||
disabled={loading}
|
||||
className="bg-destructive hover:bg-destructive/90 text-destructive-foreground"
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> Desconectando...
|
||||
</>
|
||||
) : (
|
||||
"Desconectar"
|
||||
)}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
}
|
||||
73
src/components/mika/telegram/TelegramConnectionBanner.tsx
Normal file
73
src/components/mika/telegram/TelegramConnectionBanner.tsx
Normal file
|
|
@ -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 (
|
||||
<div className="sticky top-16 z-30 -mx-4 sm:mx-0 sm:rounded-lg border-y sm:border border-destructive bg-destructive/10 px-4 py-3 text-sm">
|
||||
<div className="flex items-start sm:items-center gap-3 flex-wrap">
|
||||
<AlertCircle className="h-4 w-4 text-destructive shrink-0" />
|
||||
<span className="flex-1 text-destructive-foreground">
|
||||
Seu agente está suspenso. Regularize sua assinatura em Faturamento.
|
||||
</span>
|
||||
<Button asChild size="sm" variant="destructive">
|
||||
<Link to="/painel/faturamento">Ir para Faturamento</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (agent.telegram_token_invalid) {
|
||||
return (
|
||||
<div className="sticky top-16 z-30 -mx-4 sm:mx-0 sm:rounded-lg border-y sm:border border-destructive bg-destructive/10 px-4 py-3 text-sm">
|
||||
<div className="flex items-start sm:items-center gap-3 flex-wrap">
|
||||
<AlertCircle className="h-4 w-4 text-destructive shrink-0" />
|
||||
<span className="flex-1 text-destructive-foreground">
|
||||
Seu token Telegram foi revogado. Desconecte e reconecte o bot.
|
||||
</span>
|
||||
<Button asChild size="sm" variant="destructive">
|
||||
<Link to="/painel/agente">Ir para Meu Agente</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (agent.telegram_bot_username) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="sticky top-16 z-30 -mx-4 sm:mx-0 sm:rounded-lg border-y sm:border border-warning bg-warning/10 px-4 py-3 text-sm">
|
||||
<div className="flex items-start sm:items-center gap-3 flex-wrap">
|
||||
<AlertTriangle className="h-4 w-4 text-warning shrink-0" />
|
||||
<span className="flex-1">
|
||||
Conecte seu Telegram para começar a conversar com o Mika.
|
||||
</span>
|
||||
<Button size="sm" onClick={() => setOpen(true)}>
|
||||
Conectar agora
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TelegramOnboardingWizard open={open} onOpenChange={setOpen} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
212
src/components/mika/telegram/TelegramOnboardingWizard.tsx
Normal file
212
src/components/mika/telegram/TelegramOnboardingWizard.tsx
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { X } from "lucide-react";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
||||
import { useProfile } from "@/hooks/use-profile";
|
||||
import { useAgentInstance } from "@/hooks/use-agent-instance";
|
||||
import { supabase } from "@/integrations/supabase/client";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { suggestBotName, suggestBotUsername } from "@/lib/telegram-username";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { StepWelcome } from "./StepWelcome";
|
||||
import { StepCreateBot } from "./StepCreateBot";
|
||||
import { StepNaming } from "./StepNaming";
|
||||
import { StepToken, type ValidatedBot } from "./StepToken";
|
||||
import { StepConfiguring } from "./StepConfiguring";
|
||||
import { StepWaiting } from "./StepWaiting";
|
||||
|
||||
const STORAGE_KEY = "mika-onboarding-last-step";
|
||||
const TOTAL_STEPS = 6;
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
/** Step inicial (1-6). Se omitido, lê do localStorage ou começa em 1. */
|
||||
initialStep?: number;
|
||||
}
|
||||
|
||||
export function TelegramOnboardingWizard({ open, onOpenChange, initialStep }: Props) {
|
||||
const { data: profile } = useProfile();
|
||||
const { data: agent } = useAgentInstance();
|
||||
const { user } = useAuth();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const [step, setStep] = useState(1);
|
||||
const [validated, setValidated] = useState<ValidatedBot | null>(null);
|
||||
|
||||
const suggestedName = useMemo(() => suggestBotName(profile?.full_name), [profile?.full_name]);
|
||||
const suggestedUsername = useMemo(
|
||||
() => suggestBotUsername(profile?.full_name),
|
||||
[profile?.full_name],
|
||||
);
|
||||
|
||||
// Inicializa step ao abrir
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
if (initialStep) {
|
||||
setStep(Math.min(Math.max(initialStep, 1), TOTAL_STEPS));
|
||||
return;
|
||||
}
|
||||
if (typeof window !== "undefined") {
|
||||
const stored = window.localStorage.getItem(STORAGE_KEY);
|
||||
const parsed = stored ? Number(stored) : NaN;
|
||||
if (Number.isFinite(parsed) && parsed >= 1 && parsed <= TOTAL_STEPS) {
|
||||
setStep(parsed);
|
||||
return;
|
||||
}
|
||||
}
|
||||
setStep(1);
|
||||
}, [open, initialStep]);
|
||||
|
||||
// Persiste step ao mudar
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
if (typeof window !== "undefined") {
|
||||
window.localStorage.setItem(STORAGE_KEY, String(step));
|
||||
}
|
||||
}, [step, open]);
|
||||
|
||||
function handleClose() {
|
||||
onOpenChange(false);
|
||||
}
|
||||
|
||||
async function handleFinish() {
|
||||
if (!agent || !user) return handleClose();
|
||||
const { error } = await supabase
|
||||
.from("agent_instances")
|
||||
.update({ telegram_onboarding_completed: true })
|
||||
.eq("id", agent.id);
|
||||
if (error) {
|
||||
toast.error("Não foi possível salvar o status do onboarding.");
|
||||
return;
|
||||
}
|
||||
if (typeof window !== "undefined") {
|
||||
window.localStorage.removeItem(STORAGE_KEY);
|
||||
}
|
||||
await queryClient.invalidateQueries({ queryKey: ["agent-instance", user.id] });
|
||||
toast.success("Onboarding concluído!");
|
||||
onOpenChange(false);
|
||||
}
|
||||
|
||||
// Após validar token, recarrega agent_instance (já tem bot_username persistido)
|
||||
function handleValidated(bot: ValidatedBot) {
|
||||
setValidated(bot);
|
||||
if (user) {
|
||||
queryClient.invalidateQueries({ queryKey: ["agent-instance", user.id] });
|
||||
}
|
||||
}
|
||||
|
||||
function handleConfigured() {
|
||||
if (user) {
|
||||
queryClient.invalidateQueries({ queryKey: ["agent-instance", user.id] });
|
||||
}
|
||||
setStep(6);
|
||||
}
|
||||
|
||||
const botUsername = validated?.bot_username ?? agent?.telegram_bot_username ?? "";
|
||||
const connectedAt = agent?.telegram_connected_at ?? null;
|
||||
|
||||
return (
|
||||
<DialogPrimitive.Root open={open} onOpenChange={onOpenChange}>
|
||||
<DialogPrimitive.Portal>
|
||||
<DialogPrimitive.Overlay className="fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0" />
|
||||
<DialogPrimitive.Content
|
||||
className={cn(
|
||||
"fixed z-50 bg-background shadow-2xl",
|
||||
// Mobile: full-screen
|
||||
"inset-0 sm:inset-auto",
|
||||
// Desktop: dialog grande centralizado
|
||||
"sm:left-[50%] sm:top-[50%] sm:translate-x-[-50%] sm:translate-y-[-50%]",
|
||||
"sm:w-full sm:max-w-2xl sm:rounded-2xl sm:max-h-[90vh] sm:overflow-y-auto",
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out",
|
||||
)}
|
||||
>
|
||||
<DialogPrimitive.Title className="sr-only">
|
||||
Conectar Telegram ao Mika
|
||||
</DialogPrimitive.Title>
|
||||
<DialogPrimitive.Description className="sr-only">
|
||||
Wizard guiado de 6 passos para conectar seu agente Mika ao Telegram.
|
||||
</DialogPrimitive.Description>
|
||||
|
||||
{/* Header com progress + close */}
|
||||
<div className="sticky top-0 z-10 bg-background/95 backdrop-blur-sm border-b border-border">
|
||||
<div className="flex items-center justify-between px-4 sm:px-6 py-3">
|
||||
<span className="text-xs font-medium text-muted-foreground">
|
||||
Passo {step} de {TOTAL_STEPS}
|
||||
</span>
|
||||
<button
|
||||
onClick={handleClose}
|
||||
aria-label="Fechar"
|
||||
className="h-8 w-8 rounded-md hover:bg-muted flex items-center justify-center transition-colors"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="h-1 w-full bg-muted overflow-hidden">
|
||||
<motion.div
|
||||
className="h-full bg-primary"
|
||||
initial={false}
|
||||
animate={{ width: `${(step / TOTAL_STEPS) * 100}%` }}
|
||||
transition={{ duration: 0.3, ease: "easeOut" }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Conteúdo dos steps */}
|
||||
<div className="relative overflow-hidden">
|
||||
<AnimatePresence mode="wait" initial={false}>
|
||||
<motion.div
|
||||
key={step}
|
||||
initial={{ opacity: 0, x: 40 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: -40 }}
|
||||
transition={{ duration: 0.3, ease: "easeOut" }}
|
||||
>
|
||||
{step === 1 && <StepWelcome onNext={() => setStep(2)} />}
|
||||
{step === 2 && <StepCreateBot onNext={() => setStep(3)} />}
|
||||
{step === 3 && (
|
||||
<StepNaming
|
||||
suggestedName={suggestedName}
|
||||
suggestedUsername={suggestedUsername}
|
||||
onNext={() => setStep(4)}
|
||||
/>
|
||||
)}
|
||||
{step === 4 && (
|
||||
<StepToken
|
||||
validated={validated}
|
||||
onValidated={handleValidated}
|
||||
onNext={() => setStep(5)}
|
||||
/>
|
||||
)}
|
||||
{step === 5 && (
|
||||
<StepConfiguring
|
||||
onConfigured={handleConfigured}
|
||||
onSkip={() => setStep(6)}
|
||||
/>
|
||||
)}
|
||||
{step === 6 && agent && botUsername && (
|
||||
<StepWaiting
|
||||
agentInstanceId={agent.id}
|
||||
botUsername={botUsername}
|
||||
connectedAt={connectedAt}
|
||||
onFinish={handleFinish}
|
||||
/>
|
||||
)}
|
||||
{step === 6 && (!agent || !botUsername) && (
|
||||
<div className="px-6 py-12 text-center text-sm text-muted-foreground">
|
||||
Conecte o bot primeiro para receber a primeira mensagem.
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPrimitive.Portal>
|
||||
</DialogPrimitive.Root>
|
||||
);
|
||||
}
|
||||
105
src/components/mika/telegram/TelegramStatusCard.tsx
Normal file
105
src/components/mika/telegram/TelegramStatusCard.tsx
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { ExternalLink, AlertCircle } from "lucide-react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useAgentInstance } from "@/hooks/use-agent-instance";
|
||||
import { TelegramIcon } from "./TelegramIcon";
|
||||
import { TelegramOnboardingWizard } from "./TelegramOnboardingWizard";
|
||||
import { DisconnectTelegramDialog } from "./DisconnectTelegramDialog";
|
||||
|
||||
function formatPtBR(date: string | null): string | null {
|
||||
if (!date) return null;
|
||||
try {
|
||||
return new Intl.DateTimeFormat("pt-BR", {
|
||||
day: "2-digit",
|
||||
month: "2-digit",
|
||||
year: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
}).format(new Date(date));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function TelegramStatusCard() {
|
||||
const { data: agent } = useAgentInstance();
|
||||
const [wizardOpen, setWizardOpen] = useState(false);
|
||||
const [disconnectOpen, setDisconnectOpen] = useState(false);
|
||||
|
||||
const connected = !!agent?.telegram_bot_username;
|
||||
|
||||
return (
|
||||
<div className="rounded-xl border border-border bg-card p-6 shadow-soft">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<TelegramIcon className="h-5 w-5 text-primary" />
|
||||
<h3 className="font-semibold">Telegram</h3>
|
||||
{connected && (
|
||||
<Badge variant="success" className="ml-auto">
|
||||
Conectado
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!connected ? (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<TelegramIcon className="h-8 w-8 text-muted-foreground" />
|
||||
<div>
|
||||
<p className="font-medium">Telegram não conectado</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Conecte seu bot para começar a conversar com o Mika.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => setWizardOpen(true)}
|
||||
disabled={agent?.status === "suspended" || agent?.status === "error"}
|
||||
>
|
||||
Conectar agora
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div>
|
||||
<p className="font-mono text-sm">@{agent!.telegram_bot_username}</p>
|
||||
{agent!.telegram_connected_at && (
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
Conectado em {formatPtBR(agent!.telegram_connected_at)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{agent!.telegram_token_invalid && (
|
||||
<div className="flex items-start gap-2 rounded-md border border-destructive bg-destructive/10 p-3 text-sm">
|
||||
<AlertCircle className="h-4 w-4 text-destructive mt-0.5 shrink-0" />
|
||||
<p className="text-destructive-foreground">
|
||||
Token revogado — desconecte e reconecte.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button asChild variant="outline" size="sm">
|
||||
<a
|
||||
href={`https://t.me/${agent!.telegram_bot_username}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Abrir bot <ExternalLink className="ml-2 h-3 w-3" />
|
||||
</a>
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" onClick={() => setDisconnectOpen(true)}>
|
||||
Desconectar
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<TelegramOnboardingWizard open={wizardOpen} onOpenChange={setWizardOpen} />
|
||||
<DisconnectTelegramDialog open={disconnectOpen} onOpenChange={setDisconnectOpen} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue