Fix chat widget UI and allow attachment-only messages

- Allow sending messages with only attachments (no text required)
- Change "Chat Ativo" header to just "Chat"
- Replace Headphones icon with MessageCircle for own messages
- Replace PhoneOff icon with XCircle for end chat button

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
esdrasrenan 2025-12-07 03:09:51 -03:00
parent 60e98dd47c
commit f45ee91804
2 changed files with 18 additions and 15 deletions

View file

@ -3178,14 +3178,17 @@ export const postChatMessage = mutation({
} }
const trimmedBody = body.replace(/\r\n/g, "\n").trim() const trimmedBody = body.replace(/\r\n/g, "\n").trim()
if (trimmedBody.length === 0) { const files = attachments ?? []
throw new ConvexError("Digite uma mensagem para enviar no chat")
// Validar que há pelo menos texto ou anexo
if (trimmedBody.length === 0 && files.length === 0) {
throw new ConvexError("Digite uma mensagem ou anexe um arquivo")
} }
if (trimmedBody.length > 4000) { if (trimmedBody.length > 4000) {
throw new ConvexError("Mensagem muito longa (máx. 4000 caracteres)") throw new ConvexError("Mensagem muito longa (máx. 4000 caracteres)")
} }
const files = attachments ?? [] // Validar anexos
if (files.length > 5) { if (files.length > 5) {
throw new ConvexError("Envie até 5 arquivos por mensagem") throw new ConvexError("Envie até 5 arquivos por mensagem")
} }
@ -3196,13 +3199,14 @@ export const postChatMessage = mutation({
} }
} }
const normalizedBody = await normalizeTicketMentions(ctx, trimmedBody, { user: participant.user, role: participant.role ?? "" }, ticketDoc.tenantId) // Normalizar corpo apenas se houver texto
const plainLength = plainTextLength(normalizedBody) let normalizedBody = ""
if (plainLength === 0) { if (trimmedBody.length > 0) {
throw new ConvexError("A mensagem está vazia após a formatação") normalizedBody = await normalizeTicketMentions(ctx, trimmedBody, { user: participant.user, role: participant.role ?? "" }, ticketDoc.tenantId)
} const plainLength = plainTextLength(normalizedBody)
if (plainLength > 4000) { if (plainLength > 4000) {
throw new ConvexError("Mensagem muito longa (máx. 4000 caracteres)") throw new ConvexError("Mensagem muito longa (máx. 4000 caracteres)")
}
} }
const authorSnapshot: CommentAuthorSnapshot = { const authorSnapshot: CommentAuthorSnapshot = {

View file

@ -22,10 +22,9 @@ import {
X, X,
Minimize2, Minimize2,
User, User,
Headphones,
ChevronDown, ChevronDown,
WifiOff, WifiOff,
PhoneOff, XCircle,
Paperclip, Paperclip,
FileText, FileText,
Image as ImageIcon, Image as ImageIcon,
@ -428,7 +427,7 @@ export function ChatWidget() {
</div> </div>
<div> <div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<p className="text-sm font-semibold text-slate-900">Chat Ativo</p> <p className="text-sm font-semibold text-slate-900">Chat</p>
{/* Indicador online/offline */} {/* Indicador online/offline */}
{liveChat?.hasMachine && ( {liveChat?.hasMachine && (
machineOnline ? ( machineOnline ? (
@ -464,7 +463,7 @@ export function ChatWidget() {
{isEndingChat ? ( {isEndingChat ? (
<Spinner className="size-3.5" /> <Spinner className="size-3.5" />
) : ( ) : (
<PhoneOff className="size-3.5" /> <XCircle className="size-3.5" />
)} )}
<span className="hidden sm:inline">Encerrar</span> <span className="hidden sm:inline">Encerrar</span>
</Button> </Button>
@ -548,7 +547,7 @@ export function ChatWidget() {
isOwn ? "bg-black text-white" : "bg-slate-200 text-slate-600" isOwn ? "bg-black text-white" : "bg-slate-200 text-slate-600"
)} )}
> >
{isOwn ? <Headphones className="size-3.5" /> : <User className="size-3.5" />} {isOwn ? <MessageCircle className="size-3.5" /> : <User className="size-3.5" />}
</div> </div>
<div <div
className={cn( className={cn(