2026-05-22 14:38:16 -03:00
|
|
|
import { Fragment, useEffect, useMemo, useRef } from 'react';
|
2026-05-18 17:34:23 -03:00
|
|
|
|
|
|
|
|
function getMediaUrl(media) {
|
|
|
|
|
if (!media?.data || !media?.mimetype) return '';
|
|
|
|
|
return `data:${media.mimetype};base64,${media.data}`;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-19 15:29:43 -03:00
|
|
|
function parseMessageText(text) {
|
|
|
|
|
const rawText = String(text || '');
|
|
|
|
|
const match = rawText.match(/^\*(Atendente(?: virtual)?:\s*[^*]+)\*\s*\n+/i);
|
|
|
|
|
|
|
|
|
|
if (!match) {
|
|
|
|
|
return {
|
|
|
|
|
senderLabel: null,
|
|
|
|
|
body: rawText,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
senderLabel: match[1],
|
|
|
|
|
body: rawText.slice(match[0].length),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-19 16:39:01 -03:00
|
|
|
function formatMessageTime(timestamp) {
|
|
|
|
|
if (!timestamp) return '';
|
|
|
|
|
const numericTimestamp = Number(timestamp);
|
|
|
|
|
const date = new Date(numericTimestamp > 1000000000000 ? numericTimestamp : numericTimestamp * 1000);
|
|
|
|
|
return date.toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' });
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-22 14:38:16 -03:00
|
|
|
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>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-18 17:34:23 -03:00
|
|
|
function MediaRenderer({ message, contactId, onLoadMedia, isAgent }) {
|
|
|
|
|
const mediaUrl = useMemo(() => getMediaUrl(message.media), [message.media]);
|
|
|
|
|
const mimetype = message.media?.mimetype || '';
|
|
|
|
|
const filename = message.media?.filename || 'arquivo';
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!message.hasMedia || message.media?.data || message.mediaLoading || message.mediaError) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
onLoadMedia?.(contactId, message.id);
|
|
|
|
|
}, [contactId, message, onLoadMedia]);
|
|
|
|
|
|
|
|
|
|
if (!message.hasMedia && !message.media) return null;
|
|
|
|
|
|
|
|
|
|
if (message.mediaLoading || (!message.media?.data && !message.mediaError)) {
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
style={{
|
|
|
|
|
width: 260,
|
|
|
|
|
maxWidth: '100%',
|
|
|
|
|
height: 150,
|
|
|
|
|
borderRadius: 14,
|
|
|
|
|
background: isAgent ? 'rgba(255,255,255,0.18)' : 'rgba(0,49,80,0.08)',
|
|
|
|
|
display: 'grid',
|
|
|
|
|
placeItems: 'center',
|
|
|
|
|
color: isAgent ? '#fff' : 'var(--color-text-soft)',
|
|
|
|
|
fontWeight: 700,
|
|
|
|
|
}}
|
|
|
|
|
>
|
2026-05-21 15:50:55 -03:00
|
|
|
Carregando mídia...
|
2026-05-18 17:34:23 -03:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (message.mediaError) {
|
|
|
|
|
return (
|
|
|
|
|
<span style={{ color: isAgent ? '#fff' : 'var(--color-text-soft)', fontWeight: 700 }}>
|
2026-05-21 15:50:55 -03:00
|
|
|
Não foi possível carregar a mídia.
|
2026-05-18 17:34:23 -03:00
|
|
|
</span>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (mimetype.startsWith('image/')) {
|
|
|
|
|
return (
|
|
|
|
|
<a href={mediaUrl} target="_blank" rel="noreferrer" style={{ display: 'block' }}>
|
|
|
|
|
<img
|
|
|
|
|
src={mediaUrl}
|
|
|
|
|
alt={filename}
|
|
|
|
|
style={{
|
|
|
|
|
display: 'block',
|
|
|
|
|
width: 280,
|
|
|
|
|
maxWidth: '100%',
|
|
|
|
|
maxHeight: 340,
|
|
|
|
|
objectFit: 'cover',
|
|
|
|
|
borderRadius: 14,
|
|
|
|
|
boxShadow: '0 14px 30px rgba(0,0,0,0.18)',
|
|
|
|
|
transition: 'transform 160ms ease',
|
|
|
|
|
}}
|
|
|
|
|
onMouseEnter={(event) => {
|
|
|
|
|
event.currentTarget.style.transform = 'scale(1.015)';
|
|
|
|
|
}}
|
|
|
|
|
onMouseLeave={(event) => {
|
|
|
|
|
event.currentTarget.style.transform = 'scale(1)';
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
</a>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (mimetype.startsWith('video/')) {
|
|
|
|
|
return (
|
|
|
|
|
<video
|
|
|
|
|
src={mediaUrl}
|
|
|
|
|
controls
|
|
|
|
|
style={{
|
|
|
|
|
width: 320,
|
|
|
|
|
maxWidth: '100%',
|
|
|
|
|
borderRadius: 14,
|
|
|
|
|
background: '#111',
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (mimetype.startsWith('audio/') || mimetype.includes('ogg')) {
|
|
|
|
|
return <audio src={mediaUrl} controls style={{ width: 280, maxWidth: '100%' }} />;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<a
|
|
|
|
|
href={mediaUrl}
|
|
|
|
|
download={filename}
|
|
|
|
|
style={{
|
|
|
|
|
display: 'grid',
|
|
|
|
|
gridTemplateColumns: 'auto 1fr',
|
|
|
|
|
gap: '0.75rem',
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
padding: '0.85rem',
|
|
|
|
|
borderRadius: 14,
|
|
|
|
|
background: isAgent ? 'rgba(255,255,255,0.16)' : '#fff',
|
|
|
|
|
color: isAgent ? '#fff' : 'var(--color-primary)',
|
|
|
|
|
fontWeight: 700,
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<span aria-hidden="true">📄</span>
|
|
|
|
|
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{filename}</span>
|
|
|
|
|
</a>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function AttachmentPreview({ file, onRemove }) {
|
|
|
|
|
if (!file) return null;
|
|
|
|
|
const mediaUrl = getMediaUrl({ data: file.data, mimetype: file.type });
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
style={{
|
|
|
|
|
border: '1px solid var(--color-border)',
|
|
|
|
|
borderRadius: 16,
|
|
|
|
|
padding: '0.75rem',
|
|
|
|
|
display: 'grid',
|
|
|
|
|
gridTemplateColumns: 'auto 1fr auto',
|
|
|
|
|
gap: '0.75rem',
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
background: '#fff',
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{file.type?.startsWith('image/') ? (
|
|
|
|
|
<img
|
|
|
|
|
src={mediaUrl}
|
|
|
|
|
alt={file.name}
|
|
|
|
|
style={{ width: 54, height: 54, objectFit: 'cover', borderRadius: 12 }}
|
|
|
|
|
/>
|
|
|
|
|
) : (
|
|
|
|
|
<span
|
|
|
|
|
style={{
|
|
|
|
|
width: 54,
|
|
|
|
|
height: 54,
|
|
|
|
|
borderRadius: 12,
|
|
|
|
|
display: 'grid',
|
|
|
|
|
placeItems: 'center',
|
|
|
|
|
background: 'rgba(0,49,80,0.08)',
|
|
|
|
|
}}
|
|
|
|
|
aria-hidden="true"
|
|
|
|
|
>
|
|
|
|
|
📎
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
<div style={{ minWidth: 0 }}>
|
|
|
|
|
<strong style={{ display: 'block', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
|
|
|
|
{file.name}
|
|
|
|
|
</strong>
|
|
|
|
|
<span style={{ color: 'var(--color-text-soft)', fontSize: '0.86rem' }}>{file.type || 'arquivo'}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={onRemove}
|
|
|
|
|
title="Remover anexo"
|
|
|
|
|
style={{
|
|
|
|
|
border: 'none',
|
|
|
|
|
borderRadius: 12,
|
|
|
|
|
width: 36,
|
|
|
|
|
height: 36,
|
|
|
|
|
background: 'rgba(214, 40, 40, 0.1)',
|
|
|
|
|
color: '#b42318',
|
|
|
|
|
fontWeight: 900,
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
x
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-03-19 18:22:18 -03:00
|
|
|
|
2026-05-19 16:39:01 -03:00
|
|
|
function ContactActivity({ contact }) {
|
2026-05-19 15:29:43 -03:00
|
|
|
if (!contact) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-19 09:45:00 -03:00
|
|
|
const status = contact.status || 'offline';
|
2026-05-19 16:39:01 -03:00
|
|
|
const color = status === 'away' ? '#e5a22a' : '#dc2626';
|
|
|
|
|
const label = contact.lastSeen || 'Sem atividade recente';
|
2026-05-19 09:45:00 -03:00
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<span
|
|
|
|
|
style={{
|
|
|
|
|
display: 'inline-flex',
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
gap: '0.5rem',
|
|
|
|
|
color: 'var(--color-text-soft)',
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<span
|
|
|
|
|
aria-hidden="true"
|
|
|
|
|
style={{
|
|
|
|
|
width: 10,
|
|
|
|
|
height: 10,
|
|
|
|
|
borderRadius: 999,
|
|
|
|
|
background: color,
|
|
|
|
|
boxShadow: `0 0 0 3px ${color}22`,
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
{label}
|
|
|
|
|
</span>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-19 18:22:18 -03:00
|
|
|
export function ChatWindow({
|
|
|
|
|
contact,
|
|
|
|
|
messages,
|
|
|
|
|
selectedArea,
|
|
|
|
|
setSelectedArea,
|
|
|
|
|
draft,
|
|
|
|
|
setDraft,
|
2026-05-18 17:34:23 -03:00
|
|
|
attachedFile,
|
|
|
|
|
onAttachFile,
|
|
|
|
|
onRemoveAttachedFile,
|
|
|
|
|
onLoadMedia,
|
2026-03-19 18:22:18 -03:00
|
|
|
onSend,
|
|
|
|
|
onToggleTransfer,
|
2026-05-19 15:29:43 -03:00
|
|
|
onAssumeChat,
|
|
|
|
|
onReleaseChat,
|
2026-05-26 09:08:08 -03:00
|
|
|
onCloseChat,
|
2026-05-19 15:29:43 -03:00
|
|
|
canAssumeChat = false,
|
|
|
|
|
canReply = true,
|
|
|
|
|
assignmentLabel,
|
2026-05-20 11:37:29 -03:00
|
|
|
transferNote,
|
2026-03-19 18:22:18 -03:00
|
|
|
isReplying,
|
2026-05-25 14:32:41 -03:00
|
|
|
isPaused = false,
|
|
|
|
|
pauseDurationLabel = '00:00',
|
2026-03-19 18:22:18 -03:00
|
|
|
isMobile = false,
|
|
|
|
|
}) {
|
|
|
|
|
const messagesRef = useRef(null);
|
2026-05-19 15:29:43 -03:00
|
|
|
const safeContact = contact || {
|
|
|
|
|
id: '',
|
2026-05-25 14:32:41 -03:00
|
|
|
name: isPaused ? 'Atendimento pausado' : 'Nenhuma conversa ativa',
|
2026-05-19 15:29:43 -03:00
|
|
|
status: 'offline',
|
2026-05-26 09:08:08 -03:00
|
|
|
lastSeen: isPaused ? `Pausa em andamento: ${pauseDurationLabel}` : 'Aguardando fila do Agente Virtual Sothis',
|
2026-05-19 15:29:43 -03:00
|
|
|
};
|
2026-03-19 18:22:18 -03:00
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const container = messagesRef.current;
|
|
|
|
|
if (!container) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
container.scrollTo({
|
|
|
|
|
top: container.scrollHeight,
|
2026-05-20 13:56:04 -03:00
|
|
|
behavior: 'auto',
|
2026-03-19 18:22:18 -03:00
|
|
|
});
|
|
|
|
|
}, [messages, isReplying]);
|
|
|
|
|
|
2026-05-25 14:32:41 -03:00
|
|
|
if (isPaused) {
|
|
|
|
|
return (
|
|
|
|
|
<section
|
|
|
|
|
style={{
|
|
|
|
|
background: '#fff',
|
|
|
|
|
border: '1px solid var(--color-border)',
|
|
|
|
|
borderRadius: '28px',
|
|
|
|
|
overflow: 'hidden',
|
|
|
|
|
display: 'grid',
|
|
|
|
|
gridTemplateRows: 'auto minmax(0, 1fr)',
|
|
|
|
|
height: isMobile ? 'auto' : 'min(760px, calc(100vh - 190px))',
|
|
|
|
|
minHeight: isMobile ? 420 : 0,
|
|
|
|
|
minWidth: 0,
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<header
|
|
|
|
|
style={{
|
|
|
|
|
padding: '1.25rem 1.5rem',
|
|
|
|
|
borderBottom: '1px solid var(--color-border)',
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<strong style={{ display: 'block', fontSize: '1.15rem' }}>Atendimento pausado</strong>
|
|
|
|
|
<span style={{ color: 'var(--color-text-soft)' }}>Pausa em andamento: {pauseDurationLabel}</span>
|
|
|
|
|
</header>
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
style={{
|
|
|
|
|
padding: '1.5rem',
|
|
|
|
|
display: 'grid',
|
|
|
|
|
placeItems: 'center',
|
|
|
|
|
minHeight: 0,
|
|
|
|
|
background:
|
|
|
|
|
'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))',
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<div
|
|
|
|
|
style={{
|
|
|
|
|
maxWidth: 460,
|
|
|
|
|
border: '1px solid var(--color-border)',
|
|
|
|
|
borderRadius: 20,
|
|
|
|
|
padding: '1.2rem',
|
|
|
|
|
background: '#fff',
|
|
|
|
|
color: 'var(--color-text-soft)',
|
|
|
|
|
fontWeight: 700,
|
|
|
|
|
lineHeight: 1.5,
|
|
|
|
|
textAlign: 'center',
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
Voce esta em pausa ha {pauseDurationLabel}. Retome o atendimento pela Home para visualizar a fila,
|
|
|
|
|
assumir chamados e responder clientes.
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-19 18:22:18 -03:00
|
|
|
return (
|
|
|
|
|
<section
|
|
|
|
|
style={{
|
|
|
|
|
background: '#fff',
|
|
|
|
|
border: '1px solid var(--color-border)',
|
|
|
|
|
borderRadius: '28px',
|
|
|
|
|
overflow: 'hidden',
|
|
|
|
|
display: 'grid',
|
2026-05-19 09:45:00 -03:00
|
|
|
gridTemplateRows: 'auto minmax(0, 1fr) auto',
|
|
|
|
|
height: isMobile ? 'auto' : 'min(760px, calc(100vh - 190px))',
|
|
|
|
|
minHeight: isMobile ? 640 : 0,
|
|
|
|
|
minWidth: 0,
|
2026-03-19 18:22:18 -03:00
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<header
|
|
|
|
|
style={{
|
|
|
|
|
padding: '1.25rem 1.5rem',
|
|
|
|
|
borderBottom: '1px solid var(--color-border)',
|
|
|
|
|
display: 'grid',
|
|
|
|
|
gridTemplateColumns: isMobile ? '1fr' : 'minmax(0, 1fr) auto',
|
|
|
|
|
gap: '1rem',
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<div>
|
2026-05-19 15:29:43 -03:00
|
|
|
<strong style={{ display: 'block', fontSize: '1.15rem' }}>{safeContact.name}</strong>
|
2026-05-19 16:39:01 -03:00
|
|
|
<ContactActivity contact={safeContact} />
|
2026-03-19 18:22:18 -03:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
style={{
|
|
|
|
|
display: 'flex',
|
|
|
|
|
gap: '0.7rem',
|
|
|
|
|
flexWrap: 'wrap',
|
|
|
|
|
justifyContent: isMobile ? 'stretch' : 'flex-end',
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<select
|
|
|
|
|
value={selectedArea}
|
|
|
|
|
onChange={(event) => setSelectedArea(event.target.value)}
|
2026-05-19 15:29:43 -03:00
|
|
|
disabled
|
2026-03-19 18:22:18 -03:00
|
|
|
style={{
|
|
|
|
|
border: '1px solid var(--color-border)',
|
|
|
|
|
borderRadius: '14px',
|
|
|
|
|
padding: '0.8rem 0.95rem',
|
|
|
|
|
background: '#fff',
|
|
|
|
|
fontWeight: 600,
|
|
|
|
|
}}
|
|
|
|
|
>
|
2026-05-19 15:29:43 -03:00
|
|
|
<option>{selectedArea}</option>
|
2026-03-19 18:22:18 -03:00
|
|
|
<option>Suporte</option>
|
|
|
|
|
<option>Financeiro</option>
|
|
|
|
|
<option>Comercial</option>
|
|
|
|
|
</select>
|
2026-05-19 15:29:43 -03:00
|
|
|
{canAssumeChat ? (
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
2026-05-20 11:37:29 -03:00
|
|
|
onClick={() => onAssumeChat?.()}
|
2026-05-19 15:29:43 -03:00
|
|
|
style={{
|
|
|
|
|
border: 'none',
|
|
|
|
|
borderRadius: '14px',
|
|
|
|
|
padding: '0.8rem 1rem',
|
|
|
|
|
background: 'linear-gradient(135deg, var(--color-primary), #0b5a86)',
|
|
|
|
|
color: '#fff',
|
|
|
|
|
fontWeight: 700,
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
Assumir atendimento
|
|
|
|
|
</button>
|
|
|
|
|
) : null}
|
|
|
|
|
{canReply ? (
|
2026-05-26 09:08:08 -03:00
|
|
|
<>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={onReleaseChat}
|
|
|
|
|
style={{
|
|
|
|
|
border: '1px solid var(--color-border)',
|
|
|
|
|
borderRadius: '14px',
|
|
|
|
|
padding: '0.8rem 1rem',
|
|
|
|
|
background: '#fff',
|
|
|
|
|
color: 'var(--color-primary)',
|
|
|
|
|
fontWeight: 700,
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
Sair do atendimento
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={onCloseChat}
|
|
|
|
|
style={{
|
|
|
|
|
border: 'none',
|
|
|
|
|
borderRadius: '14px',
|
|
|
|
|
padding: '0.8rem 1rem',
|
|
|
|
|
background: 'rgba(181, 31, 31, 0.1)',
|
|
|
|
|
color: 'var(--color-secondary)',
|
|
|
|
|
fontWeight: 800,
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
Encerrar atendimento
|
|
|
|
|
</button>
|
|
|
|
|
</>
|
2026-05-19 15:29:43 -03:00
|
|
|
) : null}
|
2026-03-19 18:22:18 -03:00
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={onToggleTransfer}
|
2026-05-19 15:29:43 -03:00
|
|
|
disabled={!canReply}
|
2026-03-19 18:22:18 -03:00
|
|
|
style={{
|
|
|
|
|
border: 'none',
|
|
|
|
|
borderRadius: '14px',
|
|
|
|
|
padding: '0.8rem 1rem',
|
|
|
|
|
background: 'rgba(0, 49, 80, 0.08)',
|
|
|
|
|
color: 'var(--color-primary)',
|
|
|
|
|
fontWeight: 700,
|
2026-05-19 15:29:43 -03:00
|
|
|
opacity: canReply ? 1 : 0.55,
|
2026-03-19 18:22:18 -03:00
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
Transferir
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
2026-05-20 11:37:29 -03:00
|
|
|
{transferNote ? (
|
|
|
|
|
<div
|
|
|
|
|
style={{
|
|
|
|
|
gridColumn: '1 / -1',
|
|
|
|
|
border: '1px solid rgba(0, 164, 183, 0.24)',
|
|
|
|
|
borderRadius: 16,
|
|
|
|
|
padding: '0.85rem 1rem',
|
|
|
|
|
background: 'rgba(0, 164, 183, 0.07)',
|
|
|
|
|
color: 'var(--color-text)',
|
|
|
|
|
lineHeight: 1.45,
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<strong style={{ display: 'block', color: 'var(--color-primary)', marginBottom: '0.25rem' }}>
|
2026-05-21 15:50:55 -03:00
|
|
|
Observação da transferência
|
2026-05-20 11:37:29 -03:00
|
|
|
</strong>
|
|
|
|
|
{transferNote}
|
|
|
|
|
</div>
|
|
|
|
|
) : null}
|
2026-03-19 18:22:18 -03:00
|
|
|
</header>
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
ref={messagesRef}
|
|
|
|
|
style={{
|
|
|
|
|
padding: '1.5rem',
|
|
|
|
|
display: 'grid',
|
|
|
|
|
gap: '0.9rem',
|
|
|
|
|
alignContent: 'start',
|
|
|
|
|
overflowY: 'auto',
|
2026-05-19 09:45:00 -03:00
|
|
|
minHeight: 0,
|
2026-03-19 18:22:18 -03:00
|
|
|
background:
|
|
|
|
|
'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))',
|
|
|
|
|
}}
|
|
|
|
|
>
|
2026-05-22 14:38:16 -03:00
|
|
|
{messages.map((message, index) => {
|
2026-03-19 18:22:18 -03:00
|
|
|
const isAgent = message.sender === 'agent';
|
|
|
|
|
const isSystem = message.sender === 'system';
|
2026-05-19 15:29:43 -03:00
|
|
|
const parsedText = parseMessageText(message.text);
|
2026-05-19 16:39:01 -03:00
|
|
|
const messageTime = formatMessageTime(message.timestamp);
|
2026-05-22 14:38:16 -03:00
|
|
|
const dateKey = getDateKey(message.timestamp);
|
|
|
|
|
const previousDateKey = index > 0 ? getDateKey(messages[index - 1]?.timestamp) : '';
|
|
|
|
|
const shouldShowDateSeparator = dateKey && dateKey !== previousDateKey;
|
|
|
|
|
const dateSeparator = formatDateSeparator(message.timestamp);
|
2026-03-19 18:22:18 -03:00
|
|
|
|
|
|
|
|
if (isSystem) {
|
|
|
|
|
return (
|
2026-05-22 14:38:16 -03:00
|
|
|
<Fragment key={message.id}>
|
|
|
|
|
{shouldShowDateSeparator ? <DateSeparator label={dateSeparator} /> : null}
|
|
|
|
|
<div
|
|
|
|
|
style={{
|
|
|
|
|
justifySelf: 'center',
|
|
|
|
|
padding: '0.7rem 1rem',
|
|
|
|
|
borderRadius: '999px',
|
|
|
|
|
background: 'rgba(0, 49, 80, 0.08)',
|
|
|
|
|
color: 'var(--color-primary)',
|
|
|
|
|
fontSize: '0.88rem',
|
|
|
|
|
fontWeight: 600,
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{message.text}
|
|
|
|
|
</div>
|
|
|
|
|
</Fragment>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Fragment key={message.id}>
|
|
|
|
|
{shouldShowDateSeparator ? <DateSeparator label={dateSeparator} /> : null}
|
2026-03-19 18:22:18 -03:00
|
|
|
<div
|
|
|
|
|
style={{
|
2026-05-22 14:38:16 -03:00
|
|
|
justifySelf: isAgent ? 'end' : 'start',
|
|
|
|
|
maxWidth: isMobile ? '88%' : '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.65rem',
|
2026-03-19 18:22:18 -03:00
|
|
|
}}
|
|
|
|
|
>
|
2026-05-22 14:38:16 -03:00
|
|
|
<MediaRenderer
|
|
|
|
|
message={message}
|
|
|
|
|
contactId={safeContact.id}
|
|
|
|
|
onLoadMedia={onLoadMedia}
|
|
|
|
|
isAgent={isAgent}
|
|
|
|
|
/>
|
|
|
|
|
{parsedText.senderLabel ? (
|
|
|
|
|
<strong
|
|
|
|
|
style={{
|
|
|
|
|
display: 'block',
|
|
|
|
|
fontSize: '0.78rem',
|
|
|
|
|
lineHeight: 1.2,
|
|
|
|
|
letterSpacing: '0.02em',
|
|
|
|
|
textTransform: 'uppercase',
|
|
|
|
|
color: isAgent ? 'rgba(255,255,255,0.78)' : 'var(--color-primary)',
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{parsedText.senderLabel}
|
|
|
|
|
</strong>
|
|
|
|
|
) : null}
|
|
|
|
|
{parsedText.body ? (
|
|
|
|
|
<span
|
|
|
|
|
style={{
|
|
|
|
|
display: 'block',
|
|
|
|
|
whiteSpace: 'pre-wrap',
|
|
|
|
|
lineHeight: 1.45,
|
|
|
|
|
overflowWrap: 'anywhere',
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{parsedText.body}
|
|
|
|
|
</span>
|
|
|
|
|
) : null}
|
|
|
|
|
{messageTime ? (
|
|
|
|
|
<span
|
|
|
|
|
style={{
|
|
|
|
|
justifySelf: 'end',
|
|
|
|
|
fontSize: '0.72rem',
|
|
|
|
|
lineHeight: 1,
|
|
|
|
|
color: isAgent ? 'rgba(255,255,255,0.7)' : 'var(--color-text-soft)',
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{messageTime}
|
|
|
|
|
</span>
|
|
|
|
|
) : null}
|
2026-03-19 18:22:18 -03:00
|
|
|
</div>
|
2026-05-22 14:38:16 -03:00
|
|
|
</Fragment>
|
2026-03-19 18:22:18 -03:00
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
|
2026-05-25 14:32:41 -03:00
|
|
|
{messages.length === 0 ? (
|
2026-05-18 17:34:23 -03:00
|
|
|
<div
|
|
|
|
|
style={{
|
|
|
|
|
justifySelf: 'center',
|
|
|
|
|
padding: '0.8rem 1rem',
|
|
|
|
|
borderRadius: 16,
|
|
|
|
|
background: 'rgba(0,49,80,0.06)',
|
|
|
|
|
color: 'var(--color-text-soft)',
|
|
|
|
|
fontWeight: 700,
|
|
|
|
|
}}
|
|
|
|
|
>
|
2026-05-25 14:32:41 -03:00
|
|
|
{isPaused
|
|
|
|
|
? `Voce esta em pausa ha ${pauseDurationLabel}. Volte da pausa para visualizar a fila e seus atendimentos.`
|
|
|
|
|
: 'Nenhuma mensagem carregada.'}
|
2026-05-18 17:34:23 -03:00
|
|
|
</div>
|
|
|
|
|
) : null}
|
|
|
|
|
|
2026-03-19 18:22:18 -03:00
|
|
|
{isReplying ? (
|
|
|
|
|
<div
|
|
|
|
|
style={{
|
|
|
|
|
justifySelf: 'start',
|
|
|
|
|
padding: '0.8rem 0.95rem',
|
|
|
|
|
borderRadius: '18px 18px 18px 6px',
|
|
|
|
|
background: '#edf1f5',
|
|
|
|
|
color: 'var(--color-text-soft)',
|
|
|
|
|
fontWeight: 600,
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
Digitando...
|
|
|
|
|
</div>
|
|
|
|
|
) : null}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<footer
|
|
|
|
|
style={{
|
|
|
|
|
padding: '1rem 1.25rem 1.25rem',
|
|
|
|
|
borderTop: '1px solid var(--color-border)',
|
|
|
|
|
display: 'grid',
|
2026-05-18 17:34:23 -03:00
|
|
|
gridTemplateColumns: '1fr',
|
2026-03-19 18:22:18 -03:00
|
|
|
gap: '0.75rem',
|
|
|
|
|
}}
|
|
|
|
|
>
|
2026-05-18 17:34:23 -03:00
|
|
|
<AttachmentPreview file={attachedFile} onRemove={onRemoveAttachedFile} />
|
2026-05-19 15:29:43 -03:00
|
|
|
{!canReply ? (
|
|
|
|
|
<div
|
|
|
|
|
style={{
|
|
|
|
|
border: '1px solid var(--color-border)',
|
|
|
|
|
borderRadius: 16,
|
|
|
|
|
padding: '0.8rem 1rem',
|
|
|
|
|
background: 'rgba(0, 49, 80, 0.04)',
|
|
|
|
|
color: 'var(--color-text-soft)',
|
|
|
|
|
fontWeight: 700,
|
|
|
|
|
}}
|
|
|
|
|
>
|
2026-05-20 11:37:29 -03:00
|
|
|
<span style={{ display: 'block' }}>
|
2026-05-25 14:32:41 -03:00
|
|
|
{isPaused
|
|
|
|
|
? `Voce esta em pausa ha ${pauseDurationLabel}. Nenhum atendimento sera exibido ate voce voltar.`
|
|
|
|
|
: canAssumeChat
|
2026-05-21 15:50:55 -03:00
|
|
|
? 'Este atendimento está na fila. Assuma para responder ou transferir.'
|
|
|
|
|
: assignmentLabel || 'Este atendimento está atribuído a outro usuário.'}
|
2026-05-20 11:37:29 -03:00
|
|
|
</span>
|
|
|
|
|
{transferNote ? (
|
|
|
|
|
<span style={{ display: 'block', marginTop: '0.45rem', color: 'var(--color-text)' }}>
|
|
|
|
|
Obs: {transferNote}
|
|
|
|
|
</span>
|
|
|
|
|
) : null}
|
2026-05-19 15:29:43 -03:00
|
|
|
</div>
|
|
|
|
|
) : null}
|
2026-05-18 17:34:23 -03:00
|
|
|
<div
|
2026-03-19 18:22:18 -03:00
|
|
|
style={{
|
2026-05-18 17:34:23 -03:00
|
|
|
display: 'grid',
|
|
|
|
|
gridTemplateColumns: isMobile ? 'auto 1fr' : 'auto 1fr auto',
|
|
|
|
|
gap: '0.75rem',
|
|
|
|
|
alignItems: 'center',
|
2026-03-19 18:22:18 -03:00
|
|
|
}}
|
|
|
|
|
>
|
2026-05-18 17:34:23 -03:00
|
|
|
<label
|
|
|
|
|
title="Anexar arquivo"
|
|
|
|
|
style={{
|
|
|
|
|
border: '1px solid var(--color-border)',
|
|
|
|
|
borderRadius: 16,
|
|
|
|
|
width: 48,
|
|
|
|
|
height: 48,
|
|
|
|
|
display: 'grid',
|
|
|
|
|
placeItems: 'center',
|
|
|
|
|
background: '#fff',
|
|
|
|
|
cursor: 'pointer',
|
|
|
|
|
fontWeight: 900,
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
📎
|
|
|
|
|
<input
|
|
|
|
|
type="file"
|
|
|
|
|
accept="image/png,image/jpeg,image/jpg,image/webp,video/mp4,video/webm,audio/mp3,audio/mpeg,audio/ogg,audio/wav,application/pdf"
|
|
|
|
|
onChange={(event) => {
|
|
|
|
|
onAttachFile?.(event.target.files?.[0]);
|
|
|
|
|
event.target.value = '';
|
|
|
|
|
}}
|
|
|
|
|
style={{ display: 'none' }}
|
2026-05-19 15:29:43 -03:00
|
|
|
disabled={!safeContact.id}
|
2026-05-18 17:34:23 -03:00
|
|
|
/>
|
|
|
|
|
</label>
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
value={draft}
|
|
|
|
|
onChange={(event) => setDraft(event.target.value)}
|
|
|
|
|
onKeyDown={(event) => {
|
|
|
|
|
if (event.key === 'Enter') {
|
2026-05-22 14:38:16 -03:00
|
|
|
onSend?.(draft);
|
2026-05-18 17:34:23 -03:00
|
|
|
}
|
|
|
|
|
}}
|
2026-05-20 11:37:29 -03:00
|
|
|
disabled={!safeContact.id || !canReply}
|
2026-05-19 15:29:43 -03:00
|
|
|
placeholder={
|
2026-05-25 14:32:41 -03:00
|
|
|
isPaused
|
|
|
|
|
? 'Voce esta em pausa'
|
|
|
|
|
: !safeContact.id
|
2026-05-19 15:29:43 -03:00
|
|
|
? 'Aguardando conversa entrar em uma fila'
|
2026-05-20 11:37:29 -03:00
|
|
|
: canReply
|
2026-05-19 15:29:43 -03:00
|
|
|
? 'Escreva sua mensagem...'
|
2026-05-20 13:56:04 -03:00
|
|
|
: assignmentLabel?.includes('Aguardando resposta')
|
|
|
|
|
? 'Aguardando resposta do cliente'
|
2026-05-20 11:37:29 -03:00
|
|
|
: canAssumeChat
|
|
|
|
|
? 'Assuma o atendimento para responder'
|
|
|
|
|
: 'Atendimento bloqueado para resposta'
|
2026-05-19 15:29:43 -03:00
|
|
|
}
|
2026-05-18 17:34:23 -03:00
|
|
|
style={{
|
|
|
|
|
border: '1px solid var(--color-border)',
|
|
|
|
|
borderRadius: '18px',
|
|
|
|
|
padding: '0.95rem 1rem',
|
|
|
|
|
background: '#fff',
|
|
|
|
|
outline: 'none',
|
|
|
|
|
minWidth: 0,
|
2026-05-20 11:37:29 -03:00
|
|
|
opacity: safeContact.id && canReply ? 1 : 0.6,
|
2026-05-18 17:34:23 -03:00
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
2026-05-22 14:38:16 -03:00
|
|
|
onClick={() => onSend?.(draft)}
|
2026-05-20 11:37:29 -03:00
|
|
|
disabled={!safeContact.id || !canReply}
|
2026-05-18 17:34:23 -03:00
|
|
|
style={{
|
|
|
|
|
border: 'none',
|
|
|
|
|
borderRadius: '18px',
|
|
|
|
|
padding: '0.95rem 1.2rem',
|
|
|
|
|
background: 'linear-gradient(135deg, var(--color-primary), #0b5a86)',
|
|
|
|
|
color: '#fff',
|
|
|
|
|
fontWeight: 700,
|
|
|
|
|
gridColumn: isMobile ? '1 / -1' : 'auto',
|
2026-05-20 11:37:29 -03:00
|
|
|
opacity: safeContact.id && canReply ? 1 : 0.6,
|
2026-05-18 17:34:23 -03:00
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
Enviar
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
2026-03-19 18:22:18 -03:00
|
|
|
</footer>
|
|
|
|
|
</section>
|
|
|
|
|
);
|
|
|
|
|
}
|