From 2a78d14a7471a3c9fa182a7ae6f8ad378be732f6 Mon Sep 17 00:00:00 2001 From: esdrasrenan Date: Sun, 7 Dec 2025 01:24:33 -0300 Subject: [PATCH] Redesenha widget de chat com visual moderno MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Layout estilo messenger com baloes de mensagem - Avatares para agente (headphones) e usuario (user) - Cores distintas: preto para agente, branco para cliente - Header com status online/offline da maquina - Input com Enter para enviar - Scroll automatico para novas mensagens 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- src/components/tickets/ticket-chat-panel.tsx | 265 +++++++++++-------- 1 file changed, 155 insertions(+), 110 deletions(-) diff --git a/src/components/tickets/ticket-chat-panel.tsx b/src/components/tickets/ticket-chat-panel.tsx index 8d67301..20413d5 100644 --- a/src/components/tickets/ticket-chat-panel.tsx +++ b/src/components/tickets/ticket-chat-panel.tsx @@ -6,14 +6,13 @@ 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 { Textarea } from "@/components/ui/textarea" -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +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, MonitorSmartphone, WifiOff, X } from "lucide-react" +import { MessageCircle, Send, WifiOff, X, User, Headphones } from "lucide-react" const MAX_MESSAGE_LENGTH = 4000 @@ -25,6 +24,13 @@ function formatRelative(timestamp: number) { } } +function formatTime(timestamp: number) { + return new Date(timestamp).toLocaleTimeString("pt-BR", { + hour: "2-digit", + minute: "2-digit", + }) +} + type TicketChatPanelProps = { ticketId: string } @@ -42,7 +48,7 @@ export function TicketChatPanel({ ticketId }: TicketChatPanelProps) { status: string canPost: boolean reopenDeadline: number | null - liveChat: { + liveChat?: { hasMachine: boolean machineOnline: boolean machineHostname: string | null @@ -73,7 +79,9 @@ export function TicketChatPanel({ ticketId }: TicketChatPanelProps) { 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) @@ -82,6 +90,8 @@ export function TicketChatPanel({ ticketId }: TicketChatPanelProps) { 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 @@ -109,7 +119,7 @@ export function TicketChatPanel({ ticketId }: TicketChatPanelProps) { const disabledReason = useMemo(() => { if (!chatEnabled) return "Chat desativado para este ticket" - if (!canPost) return "Você não tem permissão para enviar mensagens" + if (!canPost) return "Voce nao tem permissao para enviar mensagens" return null }, [canPost, chatEnabled]) @@ -160,69 +170,81 @@ export function TicketChatPanel({ ticketId }: TicketChatPanelProps) { return } setIsSending(true) - toast.dismiss("ticket-chat") - toast.loading("Enviando mensagem...", { id: "ticket-chat" }) try { await postChatMessage({ ticketId: ticketId as Id<"tickets">, actorId: viewerId as Id<"users">, - body: draft, + body: draft.trim(), }) setDraft("") - toast.success("Mensagem enviada!", { id: "ticket-chat" }) + inputRef.current?.focus() } catch (error) { console.error(error) - toast.error("Nao foi possivel enviar a mensagem.", { id: "ticket-chat" }) + toast.error("Nao foi possivel enviar a mensagem.") } finally { setIsSending(false) } } + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault() + handleSend() + } + } + if (!viewerId) { return null } - const liveChat = chat?.liveChat - const hasActiveSession = Boolean(liveChat?.activeSession) - return ( - - -
- Chat do atendimento - {liveChat?.hasMachine && ( -
- {liveChat.machineOnline ? ( + + {/* Header */} + +
+
+ +
+
+

Chat do Atendimento

+
+ {liveChat?.hasMachine && ( + <> + {liveChat.machineOnline ? ( + + + {liveChat.machineHostname ?? "Maquina"} online + + ) : ( + + + {liveChat.machineHostname ?? "Maquina"} offline + + )} + + )} + {hasActiveSession && ( - - Online - - ) : ( - - - Offline + Chat ativo )}
- )} +
- {!chatEnabled ? ( - Chat desativado - ) : null} {liveChat?.hasMachine && ( <> {hasActiveSession ? ( ) : (