FEAT: Refatorado componentes dos chats, removido mock e adicionado marcador de data

This commit is contained in:
Rafael Alves Lopes 2026-05-22 14:38:16 -03:00
parent fbdbca7f20
commit 3343a12548
5 changed files with 306 additions and 223 deletions

View File

@ -110,7 +110,7 @@ function UnreadBadge({ count }) {
); );
} }
function SavedContactLabel({ contact }) { function SavedContactIcon({ contact }) {
const profile = contact.contactProfile; const profile = contact.contactProfile;
const hasSavedContact = Boolean(profile?.created_at || profile?.name || profile?.company || profile?.note); const hasSavedContact = Boolean(profile?.created_at || profile?.name || profile?.company || profile?.note);
if (!hasSavedContact) return null; if (!hasSavedContact) return null;
@ -118,12 +118,23 @@ function SavedContactLabel({ contact }) {
return ( return (
<span <span
title="Contato salvo na agenda" title="Contato salvo na agenda"
aria-label="Contato salvo na agenda"
style={{ style={{
width: 24,
height: 24,
borderRadius: '50%',
border: '1px solid rgba(183, 121, 31, 0.28)',
background: 'rgba(183, 121, 31, 0.1)',
backgroundImage:
"url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23b7791f' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M16 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2'/%3E%3Ccircle cx='10' cy='7' r='4'/%3E%3Cpath d='m17 11 2 2 4-4'/%3E%3C/svg%3E\")",
backgroundPosition: 'center',
backgroundRepeat: 'no-repeat',
backgroundSize: 14,
color: '#b7791f', color: '#b7791f',
flex: '0 0 auto', flex: '0 0 auto',
fontSize: '0.72rem', display: 'inline-grid',
fontWeight: 800, placeItems: 'center',
lineHeight: 1, fontSize: 0,
}} }}
> >
Salvo Salvo
@ -213,9 +224,9 @@ export function ChatConversationList({
</div> </div>
<div style={{ display: 'flex', justifyContent: 'space-between', gap: '0.75rem' }}> <div style={{ display: 'flex', justifyContent: 'space-between', gap: '0.75rem' }}>
<span style={{ display: 'inline-flex', alignItems: 'center', gap: '0.45rem', minWidth: 0 }}> <span style={{ display: 'inline-flex', alignItems: 'center', gap: '0.45rem', minWidth: 0 }}>
<SavedContactIcon contact={contact} />
<ChannelBadge channel={contact.channel} /> <ChannelBadge channel={contact.channel} />
<SpecialtyBadge contact={contact} /> <SpecialtyBadge contact={contact} />
<SavedContactLabel contact={contact} />
</span> </span>
<UnreadBadge count={contact.unread} /> <UnreadBadge count={contact.unread} />
</div> </div>

View File

@ -1,4 +1,4 @@
import { useEffect, useMemo, useRef } from 'react'; import { Fragment, useEffect, useMemo, useRef } from 'react';
function getMediaUrl(media) { function getMediaUrl(media) {
if (!media?.data || !media?.mimetype) return ''; if (!media?.data || !media?.mimetype) return '';
@ -29,6 +29,68 @@ function formatMessageTime(timestamp) {
return date.toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' }); return date.toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' });
} }
function getMessageDate(timestamp) {
if (!timestamp) return null;
const numericTimestamp = Number(timestamp);
const date = new Date(numericTimestamp > 1000000000000 ? numericTimestamp : numericTimestamp * 1000);
if (Number.isNaN(date.getTime())) return null;
return date;
}
function getDateKey(timestamp) {
const date = getMessageDate(timestamp);
if (!date) return '';
return date.toISOString().slice(0, 10);
}
function formatDateSeparator(timestamp) {
const date = getMessageDate(timestamp);
if (!date) return '';
const today = new Date();
const isToday =
date.getFullYear() === today.getFullYear() &&
date.getMonth() === today.getMonth() &&
date.getDate() === today.getDate();
if (isToday) return 'Hoje';
return date.toLocaleDateString('pt-BR');
}
function DateSeparator({ label }) {
if (!label) return null;
return (
<div
style={{
width: '100%',
display: 'grid',
gridTemplateColumns: '1fr auto 1fr',
gap: '0.75rem',
alignItems: 'center',
color: 'var(--color-text-soft)',
fontSize: '0.78rem',
fontWeight: 800,
textTransform: 'uppercase',
}}
>
<span style={{ height: 1, background: 'var(--color-border)' }} />
<span
style={{
border: '1px solid var(--color-border)',
borderRadius: 999,
padding: '0.28rem 0.7rem',
background: 'rgba(255,255,255,0.88)',
}}
>
{label}
</span>
<span style={{ height: 1, background: 'var(--color-border)' }} />
</div>
);
}
function MediaRenderer({ message, contactId, onLoadMedia, isAgent }) { function MediaRenderer({ message, contactId, onLoadMedia, isAgent }) {
const mediaUrl = useMemo(() => getMediaUrl(message.media), [message.media]); const mediaUrl = useMemo(() => getMediaUrl(message.media), [message.media]);
const mimetype = message.media?.mimetype || ''; const mimetype = message.media?.mimetype || '';
@ -414,91 +476,99 @@ export function ChatWindow({
'radial-gradient(circle at top left, rgba(0, 164, 183, 0.06), transparent 22%), linear-gradient(180deg, rgba(245, 248, 251, 0.8), rgba(255, 255, 255, 0.95))', 'radial-gradient(circle at top left, rgba(0, 164, 183, 0.06), transparent 22%), linear-gradient(180deg, rgba(245, 248, 251, 0.8), rgba(255, 255, 255, 0.95))',
}} }}
> >
{messages.map((message) => { {messages.map((message, index) => {
const isAgent = message.sender === 'agent'; const isAgent = message.sender === 'agent';
const isSystem = message.sender === 'system'; const isSystem = message.sender === 'system';
const parsedText = parseMessageText(message.text); const parsedText = parseMessageText(message.text);
const messageTime = formatMessageTime(message.timestamp); const messageTime = formatMessageTime(message.timestamp);
const dateKey = getDateKey(message.timestamp);
const previousDateKey = index > 0 ? getDateKey(messages[index - 1]?.timestamp) : '';
const shouldShowDateSeparator = dateKey && dateKey !== previousDateKey;
const dateSeparator = formatDateSeparator(message.timestamp);
if (isSystem) { if (isSystem) {
return ( return (
<div <Fragment key={message.id}>
key={message.id} {shouldShowDateSeparator ? <DateSeparator label={dateSeparator} /> : null}
style={{ <div
justifySelf: 'center', style={{
padding: '0.7rem 1rem', justifySelf: 'center',
borderRadius: '999px', padding: '0.7rem 1rem',
background: 'rgba(0, 49, 80, 0.08)', borderRadius: '999px',
color: 'var(--color-primary)', background: 'rgba(0, 49, 80, 0.08)',
fontSize: '0.88rem', color: 'var(--color-primary)',
fontWeight: 600, fontSize: '0.88rem',
}} fontWeight: 600,
> }}
{message.text} >
</div> {message.text}
</div>
</Fragment>
); );
} }
return ( return (
<div <Fragment key={message.id}>
key={message.id} {shouldShowDateSeparator ? <DateSeparator label={dateSeparator} /> : null}
style={{ <div
justifySelf: isAgent ? 'end' : 'start', style={{
maxWidth: isMobile ? '88%' : '72%', justifySelf: isAgent ? 'end' : 'start',
padding: '0.95rem 1rem', maxWidth: isMobile ? '88%' : '72%',
borderRadius: isAgent ? '18px 18px 6px 18px' : '18px 18px 18px 6px', padding: '0.95rem 1rem',
background: isAgent ? 'var(--color-primary)' : '#edf1f5', borderRadius: isAgent ? '18px 18px 6px 18px' : '18px 18px 18px 6px',
color: isAgent ? '#fff' : 'var(--color-text)', background: isAgent ? 'var(--color-primary)' : '#edf1f5',
boxShadow: 'var(--shadow-md)', color: isAgent ? '#fff' : 'var(--color-text)',
display: 'grid', boxShadow: 'var(--shadow-md)',
gap: '0.65rem', display: 'grid',
}} gap: '0.65rem',
> }}
<MediaRenderer >
message={message} <MediaRenderer
contactId={safeContact.id} message={message}
onLoadMedia={onLoadMedia} contactId={safeContact.id}
isAgent={isAgent} onLoadMedia={onLoadMedia}
/> isAgent={isAgent}
{parsedText.senderLabel ? ( />
<strong {parsedText.senderLabel ? (
style={{ <strong
display: 'block', style={{
fontSize: '0.78rem', display: 'block',
lineHeight: 1.2, fontSize: '0.78rem',
letterSpacing: '0.02em', lineHeight: 1.2,
textTransform: 'uppercase', letterSpacing: '0.02em',
color: isAgent ? 'rgba(255,255,255,0.78)' : 'var(--color-primary)', textTransform: 'uppercase',
}} color: isAgent ? 'rgba(255,255,255,0.78)' : 'var(--color-primary)',
> }}
{parsedText.senderLabel} >
</strong> {parsedText.senderLabel}
) : null} </strong>
{parsedText.body ? ( ) : null}
<span {parsedText.body ? (
style={{ <span
display: 'block', style={{
whiteSpace: 'pre-wrap', display: 'block',
lineHeight: 1.45, whiteSpace: 'pre-wrap',
overflowWrap: 'anywhere', lineHeight: 1.45,
}} overflowWrap: 'anywhere',
> }}
{parsedText.body} >
</span> {parsedText.body}
) : null} </span>
{messageTime ? ( ) : null}
<span {messageTime ? (
style={{ <span
justifySelf: 'end', style={{
fontSize: '0.72rem', justifySelf: 'end',
lineHeight: 1, fontSize: '0.72rem',
color: isAgent ? 'rgba(255,255,255,0.7)' : 'var(--color-text-soft)', lineHeight: 1,
}} color: isAgent ? 'rgba(255,255,255,0.7)' : 'var(--color-text-soft)',
> }}
{messageTime} >
</span> {messageTime}
) : null} </span>
</div> ) : null}
</div>
</Fragment>
); );
})} })}
@ -606,7 +676,7 @@ export function ChatWindow({
onChange={(event) => setDraft(event.target.value)} onChange={(event) => setDraft(event.target.value)}
onKeyDown={(event) => { onKeyDown={(event) => {
if (event.key === 'Enter') { if (event.key === 'Enter') {
onSend(); onSend?.(draft);
} }
}} }}
disabled={!safeContact.id || !canReply} disabled={!safeContact.id || !canReply}
@ -633,7 +703,7 @@ export function ChatWindow({
/> />
<button <button
type="button" type="button"
onClick={onSend} onClick={() => onSend?.(draft)}
disabled={!safeContact.id || !canReply} disabled={!safeContact.id || !canReply}
style={{ style={{
border: 'none', border: 'none',

View File

@ -3,14 +3,9 @@ import { useWhatsappSocket } from '../../../shared/hooks/useWhatsappSocket';
import { API_BASE_URL } from '../../../shared/services/apiConfig'; import { API_BASE_URL } from '../../../shared/services/apiConfig';
import { getAccessOptions, getAccessUsers } from '../../management/services/adminAccessService'; import { getAccessOptions, getAccessUsers } from '../../management/services/adminAccessService';
import { getCurrentUser, getCurrentUserProfile } from '../../auth/services/sessionService'; import { getCurrentUser, getCurrentUserProfile } from '../../auth/services/sessionService';
import { chatContacts, transferAreas as fallbackTransferAreas } from '../services/chatMocks'; import { transferAreas as fallbackTransferAreas } from '../services/chatMocks';
function buildInitialMessages() { const MAX_ATTACHMENT_SIZE_BYTES = 15 * 1024 * 1024;
return chatContacts.reduce((acc, contact) => {
acc[contact.id] = contact.messages;
return acc;
}, {});
}
function getLastMessageFromMe(messages = []) { function getLastMessageFromMe(messages = []) {
const lastMessage = [...messages].reverse().find(isDisplayableMessage); const lastMessage = [...messages].reverse().find(isDisplayableMessage);
@ -145,15 +140,6 @@ function fileToBase64(file) {
}); });
} }
function buildFallbackContacts() {
return chatContacts.map((contact) => ({
...contact,
assignment: null,
areaId: null,
lastMessageFromMe: getLastMessageFromMe(contact.messages),
}));
}
function normalizeComparableContact(contact) { function normalizeComparableContact(contact) {
return { return {
id: contact.id, id: contact.id,
@ -217,14 +203,14 @@ export function useChat() {
const currentUserAreas = getUserAreas(currentUser); const currentUserAreas = getUserAreas(currentUser);
const isAdminUser = currentUserProfile === 'admin'; const isAdminUser = currentUserProfile === 'admin';
const { status: whatsappStatus, incomingMessage, clearIncomingMessage } = useWhatsappSocket(); const { status: whatsappStatus, incomingMessage, clearIncomingMessage } = useWhatsappSocket();
const [contacts, setContacts] = useState(buildFallbackContacts); const [contacts, setContacts] = useState([]);
const [activeContactId, setActiveContactId] = useState(chatContacts[0].id); const [activeContactId, setActiveContactId] = useState('');
const [messagesByContact, setMessagesByContact] = useState(buildInitialMessages); const [messagesByContact, setMessagesByContact] = useState({});
const [draft, setDraft] = useState(''); const [draft, setDraft] = useState('');
const [attachedFile, setAttachedFile] = useState(null); const [attachedFile, setAttachedFile] = useState(null);
const [areaOptions, setAreaOptions] = useState([]); const [areaOptions, setAreaOptions] = useState([]);
const [accessUsers, setAccessUsers] = useState([]); const [accessUsers, setAccessUsers] = useState([]);
const [selectedArea, setSelectedArea] = useState(chatContacts[0].area); const [selectedArea, setSelectedArea] = useState('Sem fila');
const [isTransferOpen, setIsTransferOpen] = useState(false); const [isTransferOpen, setIsTransferOpen] = useState(false);
const [transferArea, setTransferArea] = useState(currentUserAreas[0] || 'Suporte'); const [transferArea, setTransferArea] = useState(currentUserAreas[0] || 'Suporte');
const [transferAttendant, setTransferAttendant] = useState(''); const [transferAttendant, setTransferAttendant] = useState('');
@ -329,11 +315,9 @@ export function useChat() {
async function loadChats({ showLoading = false } = {}) { async function loadChats({ showLoading = false } = {}) {
if (whatsappStatus !== 'CONNECTED') { if (whatsappStatus !== 'CONNECTED') {
const fallbackContacts = buildFallbackContacts(); setContacts((current) => (current.length ? [] : current));
setContacts((current) => (areContactListsEqual(current, fallbackContacts) ? current : fallbackContacts)); setActiveContactId('');
setActiveContactId((current) => setMessagesByContact({});
fallbackContacts.some((contact) => contact.id === current) ? current : fallbackContacts[0]?.id,
);
setIsLoadingChats(false); setIsLoadingChats(false);
return; return;
} }
@ -502,12 +486,18 @@ export function useChat() {
async function attachFile(file) { async function attachFile(file) {
if (!file) return; if (!file) return;
if (file.size > MAX_ATTACHMENT_SIZE_BYTES) {
setApiError('Arquivo muito grande. Envie uma mídia de até 15 MB.');
return;
}
const data = await fileToBase64(file); const data = await fileToBase64(file);
setAttachedFile({ setAttachedFile({
name: file.name, name: file.name,
type: file.type || 'application/octet-stream', type: file.type || 'application/octet-stream',
data, data,
}); });
setApiError(null);
} }
function removeAttachedFile() { function removeAttachedFile() {
@ -597,7 +587,8 @@ export function useChat() {
} }
async function sendMessage(messageText = draft, contactId = activeContactId) { async function sendMessage(messageText = draft, contactId = activeContactId) {
const trimmed = String(messageText || '').trim(); const rawMessage = typeof messageText === 'string' ? messageText : draft;
const trimmed = rawMessage.trim();
if (!trimmed && !attachedFile) return; if (!trimmed && !attachedFile) return;
const targetContact = contacts.find((contact) => contact.id === contactId) || activeContact; const targetContact = contacts.find((contact) => contact.id === contactId) || activeContact;
@ -650,7 +641,7 @@ export function useChat() {
if (!contactId.includes('@')) return; if (!contactId.includes('@')) return;
try { try {
await fetch(`${API_BASE_URL}/whatsapp/send`, { const response = await fetch(`${API_BASE_URL}/whatsapp/send`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({
@ -660,6 +651,15 @@ export function useChat() {
media, media,
}), }),
}); });
if (!response.ok) {
const message =
response.status === 413
? 'Arquivo muito grande para envio. Tente uma mídia menor.'
: 'Não foi possível enviar a mensagem.';
throw new Error(message);
}
setApiError(null); setApiError(null);
updateContact(contactId, (contact) => ({ updateContact(contactId, (contact) => ({
...contact, ...contact,

View File

@ -1,74 +1,7 @@
export const chatContacts = [
{
id: 'maria-souza',
name: 'Maria Souza',
channel: 'WhatsApp',
status: 'away',
area: 'Suporte',
lastSeen: 'Última atividade as 09:42',
preview: 'Preciso atualizar o cadastro do meu pedido.',
time: '09:42',
unread: 2,
messages: [
{ id: 1, sender: 'customer', text: 'Oi, bom dia! Preciso de ajuda com meu pedido.' },
{ id: 2, sender: 'agent', text: 'Bom dia, Maria! Claro, me conta o que aconteceu.' },
{ id: 3, sender: 'customer', text: 'Quero confirmar se o endereço foi alterado.' },
{ id: 4, sender: 'agent', text: 'Estou verificando aqui e te atualizo em instantes.' },
],
},
{
id: 'joao-pedro',
name: 'João Pedro',
channel: 'SMS',
status: 'offline',
area: 'Financeiro',
lastSeen: 'Última atividade as 08:15',
preview: 'Pode me ligar em 10 minutos?',
time: '08:15',
unread: 1,
messages: [
{ id: 1, sender: 'customer', text: 'Recebi a cobrança em duplicidade.' },
{ id: 2, sender: 'agent', text: 'Vou analisar isso agora para você.' },
{ id: 3, sender: 'customer', text: 'Pode me ligar em 10 minutos?' },
],
},
{
id: 'empresa-alpha',
name: 'Empresa Alpha',
channel: 'Email',
status: 'offline',
area: 'Comercial',
lastSeen: 'Visto ontem',
preview: 'Aguardando retorno sobre a proposta comercial.',
time: 'Ontem',
unread: 0,
messages: [
{ id: 1, sender: 'customer', text: 'Precisamos rever os valores da ultima proposta.' },
{ id: 2, sender: 'agent', text: 'Perfeito, vou encaminhar para o time comercial.' },
],
},
];
export const transferAreas = ['Suporte', 'Financeiro', 'Comercial']; export const transferAreas = ['Suporte', 'Financeiro', 'Comercial'];
export const attendantsByArea = {
Suporte: ['Ana Camolesi', 'Rafael Lopes', 'Romero Britto'],
Financeiro: ['Roberto Pêra', 'Monica Limoeira', 'Edson Arantes'],
Comercial: ['Natasha Homanoff', 'Helena Pêra', 'Pedro Parque'],
};
export const quickReplies = [ export const quickReplies = [
'Recebi sua mensagem e já vou verificar.', 'Recebi sua mensagem e ja vou verificar.',
'Consegue me confirmar o número do protocolo?', 'Consegue me confirmar o numero do protocolo?',
'Posso seguir com essa atualização por aqui.', 'Posso seguir com essa atualizacao por aqui.',
]; ];
export function getMockReply(contactName) {
const replies = [
`Perfeito, obrigado pelo retorno, ${contactName.split(' ')[0]}.`,
'Tudo bem, fico no aguardo dessa confirmação.',
'Entendi. Se precisar, posso encaminhar para a especialidade responsável.',
];
return replies[Math.floor(Math.random() * replies.length)];
}

View File

@ -1,4 +1,4 @@
import { useEffect, useMemo, useState } from 'react'; import { Fragment, useEffect, useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { createAgentNote, deleteAgentNote, listAgentNotes } from '../services/agentNotesService'; import { createAgentNote, deleteAgentNote, listAgentNotes } from '../services/agentNotesService';
import { getCurrentUser } from '../../auth/services/sessionService'; import { getCurrentUser } from '../../auth/services/sessionService';
@ -123,6 +123,68 @@ function formatMessageTime(timestamp) {
return date.toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' }); return date.toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' });
} }
function getMessageDate(timestamp) {
if (!timestamp) return null;
const numericTimestamp = Number(timestamp);
const date = new Date(numericTimestamp > 1000000000000 ? numericTimestamp : numericTimestamp * 1000);
if (Number.isNaN(date.getTime())) return null;
return date;
}
function getDateKey(timestamp) {
const date = getMessageDate(timestamp);
if (!date) return '';
return date.toISOString().slice(0, 10);
}
function formatDateSeparator(timestamp) {
const date = getMessageDate(timestamp);
if (!date) return '';
const today = new Date();
const isToday =
date.getFullYear() === today.getFullYear() &&
date.getMonth() === today.getMonth() &&
date.getDate() === today.getDate();
if (isToday) return 'Hoje';
return date.toLocaleDateString('pt-BR');
}
function DateSeparator({ label }) {
if (!label) return null;
return (
<div
style={{
width: '100%',
display: 'grid',
gridTemplateColumns: '1fr auto 1fr',
gap: '0.7rem',
alignItems: 'center',
color: 'var(--color-text-soft)',
fontSize: '0.74rem',
fontWeight: 800,
textTransform: 'uppercase',
}}
>
<span style={{ height: 1, background: 'var(--color-border)' }} />
<span
style={{
border: '1px solid var(--color-border)',
borderRadius: 999,
padding: '0.24rem 0.62rem',
background: 'rgba(255,255,255,0.9)',
}}
>
{label}
</span>
<span style={{ height: 1, background: 'var(--color-border)' }} />
</div>
);
}
function getUserId(user) { function getUserId(user) {
const value = user?.databaseId || user?.id; const value = user?.databaseId || user?.id;
const numeric = Number(value); const numeric = Number(value);
@ -408,62 +470,69 @@ export function MessagesWorkspace({
'linear-gradient(180deg, rgba(245, 248, 251, 0.45), rgba(255, 255, 255, 0.9))', 'linear-gradient(180deg, rgba(245, 248, 251, 0.45), rgba(255, 255, 255, 0.9))',
}} }}
> >
{safeActiveConversation.messages.map((message) => { {safeActiveConversation.messages.map((message, index) => {
const isAgent = message.from === 'agent'; const isAgent = message.from === 'agent';
const parsedText = parseMessageText(message.text); const parsedText = parseMessageText(message.text);
const messageTime = formatMessageTime(message.timestamp); const messageTime = formatMessageTime(message.timestamp);
const dateKey = getDateKey(message.timestamp);
const previousDateKey =
index > 0 ? getDateKey(safeActiveConversation.messages[index - 1]?.timestamp) : '';
const shouldShowDateSeparator = dateKey && dateKey !== previousDateKey;
const dateSeparator = formatDateSeparator(message.timestamp);
return ( return (
<div <Fragment key={message.id}>
key={message.id} {shouldShowDateSeparator ? <DateSeparator label={dateSeparator} /> : null}
style={{ <div
justifySelf: isAgent ? 'end' : 'start',
maxWidth: '72%',
padding: '0.95rem 1rem',
borderRadius: isAgent ? '18px 18px 6px 18px' : '18px 18px 18px 6px',
background: isAgent ? 'var(--color-primary)' : '#edf1f5',
color: isAgent ? '#fff' : 'var(--color-text)',
boxShadow: 'var(--shadow-md)',
display: 'grid',
gap: '0.55rem',
}}
>
{parsedText.senderLabel ? (
<strong
style={{
display: 'block',
fontSize: '0.76rem',
lineHeight: 1.2,
letterSpacing: '0.02em',
textTransform: 'uppercase',
color: isAgent ? 'rgba(255,255,255,0.78)' : 'var(--color-primary)',
}}
>
{parsedText.senderLabel}
</strong>
) : null}
<span
style={{ style={{
whiteSpace: 'pre-wrap', justifySelf: isAgent ? 'end' : 'start',
lineHeight: 1.45, maxWidth: '72%',
overflowWrap: 'anywhere', padding: '0.95rem 1rem',
borderRadius: isAgent ? '18px 18px 6px 18px' : '18px 18px 18px 6px',
background: isAgent ? 'var(--color-primary)' : '#edf1f5',
color: isAgent ? '#fff' : 'var(--color-text)',
boxShadow: 'var(--shadow-md)',
display: 'grid',
gap: '0.55rem',
}} }}
> >
{parsedText.body} {parsedText.senderLabel ? (
</span> <strong
{messageTime ? ( style={{
display: 'block',
fontSize: '0.76rem',
lineHeight: 1.2,
letterSpacing: '0.02em',
textTransform: 'uppercase',
color: isAgent ? 'rgba(255,255,255,0.78)' : 'var(--color-primary)',
}}
>
{parsedText.senderLabel}
</strong>
) : null}
<span <span
style={{ style={{
justifySelf: 'end', whiteSpace: 'pre-wrap',
fontSize: '0.72rem', lineHeight: 1.45,
lineHeight: 1, overflowWrap: 'anywhere',
color: isAgent ? 'rgba(255,255,255,0.7)' : 'var(--color-text-soft)',
}} }}
> >
{messageTime} {parsedText.body}
</span> </span>
) : null} {messageTime ? (
</div> <span
style={{
justifySelf: 'end',
fontSize: '0.72rem',
lineHeight: 1,
color: isAgent ? 'rgba(255,255,255,0.7)' : 'var(--color-text-soft)',
}}
>
{messageTime}
</span>
) : null}
</div>
</Fragment>
); );
})} })}
</div> </div>