"use client" import { useEffect, useMemo, useRef, useState } from "react" import { useMutation, useQuery } from "convex/react" import type { Id } from "@/convex/_generated/dataModel" import { api } from "@/convex/_generated/api" import { useAuth } from "@/lib/auth-client" import { Button } from "@/components/ui/button" import { Card, CardContent, CardHeader } from "@/components/ui/card" import { Spinner } from "@/components/ui/spinner" import { cn } from "@/lib/utils" import { toast } from "sonner" import { formatDistanceToNowStrict } from "date-fns" import { ptBR } from "date-fns/locale" import { MessageCircle, Send, WifiOff, X, User, Headphones } from "lucide-react" const MAX_MESSAGE_LENGTH = 4000 function formatRelative(timestamp: number) { try { return formatDistanceToNowStrict(timestamp, { locale: ptBR, addSuffix: true }) } catch { return new Date(timestamp).toLocaleString("pt-BR") } } function formatTime(timestamp: number) { return new Date(timestamp).toLocaleTimeString("pt-BR", { hour: "2-digit", minute: "2-digit", }) } type TicketChatPanelProps = { ticketId: string } export function TicketChatPanel({ ticketId }: TicketChatPanelProps) { const { convexUserId } = useAuth() const viewerId = convexUserId ?? null const chat = useQuery( api.tickets.listChatMessages, viewerId ? { ticketId: ticketId as Id<"tickets">, viewerId: viewerId as Id<"users"> } : "skip" ) as | { ticketId: string chatEnabled: boolean status: string canPost: boolean reopenDeadline: number | null liveChat?: { hasMachine: boolean machineOnline: boolean machineHostname: string | null activeSession: { sessionId: Id<"liveChatSessions"> agentId: Id<"users"> agentName: string | null startedAt: number unreadByAgent: number } | null } messages: Array<{ id: Id<"ticketChatMessages"> body: string createdAt: number updatedAt: number authorId: string authorName: string | null authorEmail: string | null attachments: Array<{ storageId: Id<"_storage">; name: string; size: number | null; type: string | null }> readBy: Array<{ userId: string; readAt: number }> }> } | null | undefined const markChatRead = useMutation(api.tickets.markChatRead) const postChatMessage = useMutation(api.tickets.postChatMessage) const startLiveChat = useMutation(api.liveChat.startSession) const endLiveChat = useMutation(api.liveChat.endSession) const messagesEndRef = useRef(null) const inputRef = useRef(null) const [draft, setDraft] = useState("") const [isSending, setIsSending] = useState(false) const [isStartingChat, setIsStartingChat] = useState(false) const [isEndingChat, setIsEndingChat] = useState(false) const messages = chat?.messages ?? [] const canPost = Boolean(chat?.canPost && viewerId) const chatEnabled = Boolean(chat?.chatEnabled) const liveChat = chat?.liveChat const hasActiveSession = Boolean(liveChat?.activeSession) useEffect(() => { if (!viewerId || !chat || !Array.isArray(chat.messages) || chat.messages.length === 0) return const unreadIds = chat.messages .filter((message) => { const alreadyRead = (message.readBy ?? []).some((entry) => entry.userId === viewerId) return !alreadyRead }) .map((message) => message.id) if (unreadIds.length === 0) return void markChatRead({ ticketId: ticketId as Id<"tickets">, actorId: viewerId as Id<"users">, messageIds: unreadIds, }).catch((error) => { console.error("Failed to mark chat messages as read", error) }) }, [markChatRead, chat, ticketId, viewerId]) useEffect(() => { if (messagesEndRef.current) { messagesEndRef.current.scrollIntoView({ behavior: "smooth" }) } }, [messages.length]) const disabledReason = useMemo(() => { if (!chatEnabled) return "Chat desativado para este ticket" if (!canPost) return "Voce nao tem permissao para enviar mensagens" return null }, [canPost, chatEnabled]) const handleStartLiveChat = async () => { if (!viewerId) return setIsStartingChat(true) toast.dismiss("live-chat") try { const result = await startLiveChat({ ticketId: ticketId as Id<"tickets">, actorId: viewerId as Id<"users">, }) if (result.isNew) { toast.success("Chat ao vivo iniciado", { id: "live-chat" }) } else { toast.info("Já existe uma sessão de chat ativa", { id: "live-chat" }) } } catch (error: unknown) { const message = error instanceof Error ? error.message : "Não foi possível iniciar o chat" toast.error(message, { id: "live-chat" }) } finally { setIsStartingChat(false) } } const handleEndLiveChat = async () => { if (!viewerId || !chat?.liveChat?.activeSession) return setIsEndingChat(true) toast.dismiss("live-chat") try { await endLiveChat({ sessionId: chat.liveChat.activeSession.sessionId, actorId: viewerId as Id<"users">, }) toast.success("Chat ao vivo encerrado.", { id: "live-chat" }) } catch (error: unknown) { const message = error instanceof Error ? error.message : "Não foi possível encerrar o chat" toast.error(message, { id: "live-chat" }) } finally { setIsEndingChat(false) } } const handleSend = async () => { if (!viewerId || !canPost || draft.trim().length === 0) return if (draft.length > MAX_MESSAGE_LENGTH) { toast.error(`Mensagem muito longa (max. ${MAX_MESSAGE_LENGTH} caracteres).`) return } setIsSending(true) try { await postChatMessage({ ticketId: ticketId as Id<"tickets">, actorId: viewerId as Id<"users">, body: draft.trim(), }) setDraft("") inputRef.current?.focus() } catch (error) { console.error(error) toast.error("Não foi possível enviar a mensagem.") } finally { setIsSending(false) } } const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault() handleSend() } } if (!viewerId) { return null } return ( {/* Header */}

Chat do Atendimento

{liveChat?.hasMachine && ( <> {liveChat.machineOnline ? ( {liveChat.machineHostname ?? "Maquina"} online ) : ( {liveChat.machineHostname ?? "Maquina"} offline )} )} {hasActiveSession && ( Chat ativo )}
{liveChat?.hasMachine && ( <> {hasActiveSession ? ( ) : ( )} )}
{/* Messages */}
{chat === undefined ? (

Carregando mensagens...

) : messages.length === 0 ? (

Nenhuma mensagem ainda

{hasActiveSession ? "Envie uma mensagem para iniciar a conversa" : "Inicie um chat para conversar com o cliente"}

) : (
{messages.map((message) => { const isOwn = String(message.authorId) === String(viewerId) return (
{/* Avatar */}
{isOwn ? : }
{/* Bubble */}
{!isOwn && (

{message.authorName ?? "Usuario"}

)}

{message.body}

{formatTime(message.createdAt)}

) })}
)}
{/* Input */}
{!chatEnabled ? (

Chat desativado para este ticket

) : !canPost ? (

Voce nao pode enviar mensagens neste chat

) : (