@adatechnology/conversations-ui 0.1.0-rc.26 → 0.1.0-rc.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-DXPSPUWF.js +110 -0
- package/dist/{chunk-TV4OQRGH.js → chunk-UZJBYD5O.js} +123 -45
- package/dist/{chunk-2AYDBWNE.js → chunk-WCBDXZ3X.js} +13 -3
- package/dist/flows/index.d.ts +75 -2
- package/dist/flows/index.js +176 -95
- package/dist/index.d.ts +37 -10
- package/dist/index.js +694 -297
- package/dist/preview/index.js +3 -3
- package/dist/styles.css +119 -6
- package/package.json +2 -2
- package/src/AudioPlayer.tsx +8 -0
- package/src/AudioRecorderButton.test.tsx +3 -3
- package/src/AudioRecorderButton.tsx +3 -3
- package/src/AudioTranscription.tsx +4 -1
- package/src/Avatar.tsx +1 -1
- package/src/ConversationContextPanel.tsx +2 -2
- package/src/ConversationDocumentsPanel.tsx +11 -6
- package/src/ConversationHeader.test.tsx +66 -0
- package/src/ConversationHeader.tsx +147 -47
- package/src/ConversationListItem.tsx +3 -2
- package/src/ConversationRow.tsx +31 -7
- package/src/DocumentsLibrary.tsx +9 -4
- package/src/EmojiPicker.tsx +2 -1
- package/src/InteractiveMessage.tsx +3 -0
- package/src/Lightbox.tsx +1 -1
- package/src/MediaRenderer.tsx +2 -0
- package/src/MessageBubble.test.tsx +41 -0
- package/src/MessageBubble.tsx +27 -8
- package/src/MessageComposer.tsx +11 -8
- package/src/RichMessageComposer.test.tsx +42 -12
- package/src/RichMessageComposer.tsx +216 -45
- package/src/SimpleEmojiPicker.tsx +5 -3
- package/src/StatusTicks.tsx +1 -1
- package/src/Toast.tsx +4 -0
- package/src/Tooltip.test.ts +42 -0
- package/src/Tooltip.tsx +164 -0
- package/src/WhatsAppMessageEditor.tsx +4 -4
- package/src/WindowExpiredNotice.tsx +12 -4
- package/src/buildOutput.test.ts +79 -0
- package/src/composer.constant.ts +33 -0
- package/src/conversationWindow.ts +7 -5
- package/src/documents/DocumentsWorkspace.tsx +10 -6
- package/src/flows/FlowGroupHeader.tsx +2 -2
- package/src/flows/FlowMapNode.tsx +2 -1
- package/src/flows/FlowNodeCard.tsx +3 -3
- package/src/flows/FlowNodePanel.tsx +6 -5
- package/src/flows/FlowPalette.tsx +7 -1
- package/src/flows/FlowPortalNode.tsx +1 -1
- package/src/flows/FlowsWorkspace.tsx +69 -102
- package/src/flows/flowEditorOps.test.ts +241 -0
- package/src/flows/flowEditorOps.ts +177 -0
- package/src/flows/flowGraph.ts +1 -1
- package/src/flows/index.ts +16 -0
- package/src/flows/labels.ts +4 -0
- package/src/flows/workspaceContract.test.ts +95 -0
- package/src/hooks/useContainerWidth.ts +35 -0
- package/src/hooks/useConversationRealtime.ts +10 -8
- package/src/icon.constant.ts +12 -0
- package/src/index.ts +1 -0
- package/src/lib/composer-formatting.test.ts +78 -0
- package/src/lib/composer-formatting.ts +145 -0
- package/src/lib/whatsapp-formatting.test.tsx +37 -0
- package/src/lib/whatsapp-formatting.tsx +28 -3
- package/src/listing/index.tsx +5 -1
- package/src/pagination.constant.ts +10 -0
- package/src/preview/ConversationSimulatorPanel.tsx +1 -1
- package/src/providers/ConversationsProvider.tsx +8 -6
- package/src/settings/MessagesWorkspace.tsx +3 -0
- package/src/settings/TopicsForm.tsx +2 -0
- package/src/settings/TranscriptionSettingsForm.test.tsx +1 -1
- package/src/settings/TranscriptionSettingsForm.tsx +1 -0
- package/src/settings/WelcomeFarewellForm.tsx +1 -0
- package/src/settings/WhatsAppCreateTemplateForm.tsx +1 -0
- package/src/settings/WhatsAppTemplateSettingsForm.tsx +5 -2
- package/src/settings/WhatsAppTemplatesSettings.tsx +2 -0
- package/src/styles.css +100 -4
- package/src/workspace/BulkTemplateModal.tsx +4 -2
- package/src/workspace/ConversationPane.tsx +49 -19
- package/src/workspace/ConversationsInboxList.tsx +17 -8
- package/src/workspace/ConversationsWorkspace.tsx +24 -16
- package/src/workspace/useConversationsInbox.ts +5 -2
package/src/MessageBubble.tsx
CHANGED
|
@@ -89,6 +89,18 @@ export function MessageBubble({
|
|
|
89
89
|
const isMedia = MEDIA_TYPES.has(message.type)
|
|
90
90
|
const isTemplate = message.type === 'template'
|
|
91
91
|
const isInteractive = message.type === 'interactive'
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* A legenda chega em `caption` quando o cliente manda, e em `content` quando somos nós — é lá que
|
|
95
|
+
* a API grava o texto do envio. Sem legenda a API repete o nome do arquivo em `content`, que não
|
|
96
|
+
* é conteúdo e não deve aparecer sob a mídia.
|
|
97
|
+
*/
|
|
98
|
+
const mediaCaption = isMedia
|
|
99
|
+
? [message.caption, message.content].find((text) => {
|
|
100
|
+
const trimmed = text?.trim()
|
|
101
|
+
return trimmed && trimmed !== message.filename
|
|
102
|
+
})
|
|
103
|
+
: undefined
|
|
92
104
|
const displayName = message.sender === 'agent' && senderName ? senderName : bubble[message.sender] ?? message.sender
|
|
93
105
|
|
|
94
106
|
const tooltipText = message.status === 'read' && message.readAt
|
|
@@ -104,7 +116,7 @@ export function MessageBubble({
|
|
|
104
116
|
const checkbox = (
|
|
105
117
|
<button
|
|
106
118
|
onClick={(e) => { e.stopPropagation(); onToggleSelect?.() }}
|
|
107
|
-
|
|
119
|
+
data-cv-tooltip={selection.select} aria-label={selection.select}
|
|
108
120
|
className={`
|
|
109
121
|
flex-shrink-0 self-end mb-1 w-5 h-5 rounded-full border-2 flex items-center justify-center transition-all
|
|
110
122
|
${isSelected ? 'bg-teal-600 border-teal-600' : 'bg-white/80 dark:bg-black/40 border-black/20 dark:border-white/30'}
|
|
@@ -148,7 +160,7 @@ export function MessageBubble({
|
|
|
148
160
|
{message.moderation?.isOffensive && (
|
|
149
161
|
<div
|
|
150
162
|
className="mb-1 inline-flex items-center gap-1 rounded-full bg-amber-100 px-2 py-0.5 text-xs font-medium text-amber-800 dark:bg-amber-950/60 dark:text-amber-300"
|
|
151
|
-
|
|
163
|
+
data-cv-tooltip={message.moderation.terms.length > 0 ? message.moderation.terms.join(', ') : undefined}
|
|
152
164
|
>
|
|
153
165
|
<span aria-hidden>⚠️</span>
|
|
154
166
|
<span>{bubble.moderationFlagged}</span>
|
|
@@ -156,12 +168,19 @@ export function MessageBubble({
|
|
|
156
168
|
)}
|
|
157
169
|
|
|
158
170
|
{isMedia ? (
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
171
|
+
<>
|
|
172
|
+
<MediaRenderer
|
|
173
|
+
message={message}
|
|
174
|
+
onLightbox={setLightboxSrc}
|
|
175
|
+
onResolveUrl={resolveMediaUrl}
|
|
176
|
+
{...(requestTranscription ? { onTranscribeAudio: requestTranscription } : {})}
|
|
177
|
+
/>
|
|
178
|
+
{mediaCaption ? (
|
|
179
|
+
<div className="mt-1 text-sm text-gray-900 dark:text-gray-100 whitespace-pre-wrap break-words leading-[19px]">
|
|
180
|
+
{parseWhatsAppFormatting(mediaCaption)}
|
|
181
|
+
</div>
|
|
182
|
+
) : null}
|
|
183
|
+
</>
|
|
165
184
|
) : isInteractive && message.payload ? (
|
|
166
185
|
// O texto da mensagem interativa mora dentro do payload (`body.text`), e `content` guarda
|
|
167
186
|
// só uma cópia achatada para busca — renderizar `content` aqui duplicaria o corpo.
|
package/src/MessageComposer.tsx
CHANGED
|
@@ -2,6 +2,7 @@ import { useState, useRef, useCallback, type KeyboardEvent, type ChangeEvent, ty
|
|
|
2
2
|
import { AudioRecorderButton } from './AudioRecorderButton'
|
|
3
3
|
import type { ConversationsFeatures } from './types'
|
|
4
4
|
import { cn } from './lib/cn'
|
|
5
|
+
import { COMPOSER_BAR_CLASS, QUICK_REPLY_PILL_CLASS } from './composer.constant'
|
|
5
6
|
import { EmojiPicker } from './EmojiPicker'
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -42,12 +43,14 @@ export interface MessageComposerLabels {
|
|
|
42
43
|
emoji: string
|
|
43
44
|
attach: string
|
|
44
45
|
send: string
|
|
46
|
+
removeAttachment: string
|
|
45
47
|
}
|
|
46
48
|
|
|
47
49
|
export const DEFAULT_MESSAGE_COMPOSER_LABELS: MessageComposerLabels = {
|
|
48
50
|
emoji: 'Emoji',
|
|
49
51
|
attach: 'Anexar',
|
|
50
52
|
send: 'Enviar',
|
|
53
|
+
removeAttachment: 'Remover anexo',
|
|
51
54
|
}
|
|
52
55
|
|
|
53
56
|
export interface MessageComposerProps {
|
|
@@ -123,6 +126,7 @@ export const MessageComposer = ({
|
|
|
123
126
|
const emojiLabel = labels?.emoji ?? DEFAULT_MESSAGE_COMPOSER_LABELS.emoji
|
|
124
127
|
const attachLabel = labels?.attach ?? DEFAULT_MESSAGE_COMPOSER_LABELS.attach
|
|
125
128
|
const sendLabel = labels?.send ?? DEFAULT_MESSAGE_COMPOSER_LABELS.send
|
|
129
|
+
const removeAttachmentLabel = labels?.removeAttachment ?? DEFAULT_MESSAGE_COMPOSER_LABELS.removeAttachment
|
|
126
130
|
const [internalText, setInternalText] = useState('')
|
|
127
131
|
const [showEmoji, setShowEmoji] = useState(false)
|
|
128
132
|
const [attachments, setAttachments] = useState<FilePreview[]>([])
|
|
@@ -245,7 +249,7 @@ export const MessageComposer = ({
|
|
|
245
249
|
/* A barra é a superfície (cinza, largura cheia, sem raio) e o campo dentro é que arredonda —
|
|
246
250
|
ordem do WhatsApp. Invertido, o pill arredondado ia até a borda da tela e os cantos
|
|
247
251
|
descobriam o fundo branco da página, que lia como defeito. */
|
|
248
|
-
<div className={cn(
|
|
252
|
+
<div className={cn(COMPOSER_BAR_CLASS, className)}>
|
|
249
253
|
{/* Uma linha com scroll no celular e wrap no desktop: em 375px seis chips em wrap empurrariam
|
|
250
254
|
o campo para fora da tela, e o campo é a razão de a barra existir. */}
|
|
251
255
|
{quickReplies && quickReplies.length > 0 && (
|
|
@@ -257,6 +261,7 @@ export const MessageComposer = ({
|
|
|
257
261
|
>
|
|
258
262
|
{quickReplies.map((quickReply) => (
|
|
259
263
|
<button
|
|
264
|
+
data-cv-tooltip={quickReply.label} aria-label={quickReply.label}
|
|
260
265
|
key={quickReply.key}
|
|
261
266
|
type="button"
|
|
262
267
|
// Preenche o campo em vez de enviar: mensagem pronta é ponto de partida, e quem
|
|
@@ -266,10 +271,7 @@ export const MessageComposer = ({
|
|
|
266
271
|
setText(resolveQuickReply(quickReply, quickReplyVariables))
|
|
267
272
|
textareaRef.current?.focus()
|
|
268
273
|
}}
|
|
269
|
-
className={cn(
|
|
270
|
-
'whitespace-nowrap rounded-full border border-gray-200 bg-white px-2 py-1 text-xs text-gray-600 transition-colors hover:border-teal-200 hover:bg-teal-50 hover:text-teal-700 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-400',
|
|
271
|
-
classNames?.quickReply,
|
|
272
|
-
)}
|
|
274
|
+
className={cn(QUICK_REPLY_PILL_CLASS, classNames?.quickReply)}
|
|
273
275
|
>
|
|
274
276
|
{quickReply.label}
|
|
275
277
|
</button>
|
|
@@ -288,7 +290,7 @@ export const MessageComposer = ({
|
|
|
288
290
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#9ca3af" strokeWidth="1.5"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>
|
|
289
291
|
</div>
|
|
290
292
|
)}
|
|
291
|
-
<button onClick={() => removeAttachment(i)} className="absolute -top-2 -right-2 w-5 h-5 bg-gray-600 text-white rounded-full flex items-center justify-center hover:bg-gray-800 text-xs">✕</button>
|
|
293
|
+
<button data-cv-tooltip={removeAttachmentLabel} aria-label={removeAttachmentLabel} onClick={() => removeAttachment(i)} className="absolute -top-2 -right-2 w-5 h-5 bg-gray-600 text-white rounded-full flex items-center justify-center hover:bg-gray-800 text-xs">✕</button>
|
|
292
294
|
</div>
|
|
293
295
|
))}
|
|
294
296
|
</div>
|
|
@@ -297,7 +299,7 @@ export const MessageComposer = ({
|
|
|
297
299
|
<div className={cn('flex items-end gap-1.5 rounded-xl bg-white px-3 py-2', classNames?.field)}>
|
|
298
300
|
{showEmojiButton && (
|
|
299
301
|
<div className="relative flex-shrink-0">
|
|
300
|
-
<button onClick={() => setShowEmoji(v => !v)} className="w-9 h-9 flex items-center justify-center rounded-full text-gray-500 hover:bg-gray-200 transition-colors" aria-label={emojiLabel}>
|
|
302
|
+
<button data-cv-tooltip={emojiLabel} onClick={() => setShowEmoji(v => !v)} className="w-9 h-9 flex items-center justify-center rounded-full text-gray-500 hover:bg-gray-200 transition-colors" aria-label={emojiLabel}>
|
|
301
303
|
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><circle cx="12" cy="12" r="10"/><path d="M8 14s1.5 2 4 2 4-2 4-2"/><circle cx="9" cy="9" r="0.5" fill="currentColor"/><circle cx="15" cy="9" r="0.5" fill="currentColor"/></svg>
|
|
302
304
|
</button>
|
|
303
305
|
{showEmoji && (
|
|
@@ -323,7 +325,7 @@ export const MessageComposer = ({
|
|
|
323
325
|
{showAttachButton && (
|
|
324
326
|
<>
|
|
325
327
|
<input ref={fileInputRef} type="file" multiple accept={acceptedFileTypes} onChange={handleFileChange} className="hidden" />
|
|
326
|
-
<button onClick={() => fileInputRef.current?.click()} className="w-9 h-9 flex items-center justify-center rounded-full text-gray-500 hover:bg-gray-200 flex-shrink-0 transition-colors" aria-label={attachLabel}>
|
|
328
|
+
<button data-cv-tooltip={attachLabel} onClick={() => fileInputRef.current?.click()} className="w-9 h-9 flex items-center justify-center rounded-full text-gray-500 hover:bg-gray-200 flex-shrink-0 transition-colors" aria-label={attachLabel}>
|
|
327
329
|
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"/></svg>
|
|
328
330
|
</button>
|
|
329
331
|
</>
|
|
@@ -333,6 +335,7 @@ export const MessageComposer = ({
|
|
|
333
335
|
<div className="flex-shrink-0">{effectiveIdleAction}</div>
|
|
334
336
|
) : (
|
|
335
337
|
<button
|
|
338
|
+
data-cv-tooltip={sendLabel}
|
|
336
339
|
onClick={sendMessage}
|
|
337
340
|
disabled={!canSend || disabled}
|
|
338
341
|
className={`w-10 h-10 flex items-center justify-center rounded-full flex-shrink-0 transition-all ${
|
|
@@ -20,7 +20,7 @@ describe('RichMessageComposer', () => {
|
|
|
20
20
|
)
|
|
21
21
|
|
|
22
22
|
for (const tooltip of ['Negrito', 'Itálico', 'Tachado', 'Monoespaçado', 'Anexar arquivo', 'Enviar']) {
|
|
23
|
-
expect(markup).toContain(`
|
|
23
|
+
expect(markup).toContain(`data-cv-tooltip="${tooltip}"`)
|
|
24
24
|
}
|
|
25
25
|
})
|
|
26
26
|
|
|
@@ -29,9 +29,9 @@ describe('RichMessageComposer', () => {
|
|
|
29
29
|
<RichMessageComposer value="" onChange={noop} onSend={noop} tooltips={{ bold: 'Deixar em negrito' }} />,
|
|
30
30
|
)
|
|
31
31
|
|
|
32
|
-
expect(markup).toContain('
|
|
33
|
-
expect(markup).not.toContain('
|
|
34
|
-
expect(markup).toContain('
|
|
32
|
+
expect(markup).toContain('data-cv-tooltip="Deixar em negrito"')
|
|
33
|
+
expect(markup).not.toContain('data-cv-tooltip="Negrito"')
|
|
34
|
+
expect(markup).toContain('data-cv-tooltip="Itálico"')
|
|
35
35
|
})
|
|
36
36
|
|
|
37
37
|
it('esconde só a ação que o host desligou', () => {
|
|
@@ -39,14 +39,14 @@ describe('RichMessageComposer', () => {
|
|
|
39
39
|
<RichMessageComposer value="" onChange={noop} onSend={noop} toolbar={{ monospace: false }} />,
|
|
40
40
|
)
|
|
41
41
|
|
|
42
|
-
expect(markup).not.toContain('
|
|
43
|
-
expect(markup).toContain('
|
|
42
|
+
expect(markup).not.toContain('data-cv-tooltip="Monoespaçado"')
|
|
43
|
+
expect(markup).toContain('data-cv-tooltip="Negrito"')
|
|
44
44
|
})
|
|
45
45
|
|
|
46
46
|
it('não desenha o anexo quando não há como anexar', () => {
|
|
47
47
|
const markup = renderToStaticMarkup(<RichMessageComposer value="" onChange={noop} onSend={noop} />)
|
|
48
48
|
|
|
49
|
-
expect(markup).not.toContain('
|
|
49
|
+
expect(markup).not.toContain('data-cv-tooltip="Anexar arquivo"')
|
|
50
50
|
})
|
|
51
51
|
|
|
52
52
|
it('desenha os chips de resposta rápida com o rótulo e a dica do host', () => {
|
|
@@ -55,7 +55,7 @@ describe('RichMessageComposer', () => {
|
|
|
55
55
|
)
|
|
56
56
|
|
|
57
57
|
expect(markup).toContain('Saudação')
|
|
58
|
-
expect(markup).toContain('
|
|
58
|
+
expect(markup).toContain('data-cv-tooltip="Abre o atendimento"')
|
|
59
59
|
})
|
|
60
60
|
|
|
61
61
|
it('só oferece variáveis quando o host manda alguma', () => {
|
|
@@ -64,8 +64,15 @@ describe('RichMessageComposer', () => {
|
|
|
64
64
|
<RichMessageComposer value="" onChange={noop} onSend={noop} variables={VARIABLES} />,
|
|
65
65
|
)
|
|
66
66
|
|
|
67
|
-
expect(semVariaveis).not.toContain('
|
|
68
|
-
expect(comVariaveis).toContain('
|
|
67
|
+
expect(semVariaveis).not.toContain('data-cv-tooltip="Variáveis disponíveis"')
|
|
68
|
+
expect(comVariaveis).toContain('data-cv-tooltip="Variáveis disponíveis"')
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('nasce com toda formatação apagada, e diz isso ao leitor de tela', () => {
|
|
72
|
+
const markup = renderToStaticMarkup(<RichMessageComposer value="" onChange={noop} onSend={noop} />)
|
|
73
|
+
|
|
74
|
+
expect(markup).toContain('aria-pressed="false"')
|
|
75
|
+
expect(markup).not.toContain('aria-pressed="true"')
|
|
69
76
|
})
|
|
70
77
|
|
|
71
78
|
it('cede o lugar do enviar à ação ociosa enquanto não há texto', () => {
|
|
@@ -77,7 +84,30 @@ describe('RichMessageComposer', () => {
|
|
|
77
84
|
)
|
|
78
85
|
|
|
79
86
|
expect(vazio).toContain('aria-label="Gravar áudio"')
|
|
80
|
-
expect(vazio).not.toContain('
|
|
81
|
-
expect(comTexto).toContain('
|
|
87
|
+
expect(vazio).not.toContain('data-cv-tooltip="Enviar"')
|
|
88
|
+
expect(comTexto).toContain('data-cv-tooltip="Enviar"')
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('desenha as quatro formatações abertas enquanto a barra não foi medida', () => {
|
|
92
|
+
const markup = renderToStaticMarkup(<RichMessageComposer value="" onChange={noop} onSend={noop} />)
|
|
93
|
+
|
|
94
|
+
expect(markup).toContain('data-cv-tooltip="Negrito"')
|
|
95
|
+
// O botão que recolhe as quatro só aparece com a barra medida e estreita.
|
|
96
|
+
expect(markup).not.toContain('data-cv-tooltip="Formatação"')
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('mostra o enviar com anexo na fila e nenhum texto, senão a imagem fica presa', () => {
|
|
100
|
+
const markup = renderToStaticMarkup(
|
|
101
|
+
<RichMessageComposer
|
|
102
|
+
value=""
|
|
103
|
+
onChange={noop}
|
|
104
|
+
onSend={noop}
|
|
105
|
+
hasQueuedAttachments
|
|
106
|
+
idleAction={<button aria-label="Gravar áudio" />}
|
|
107
|
+
/>,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
expect(markup).toContain('data-cv-tooltip="Enviar"')
|
|
111
|
+
expect(markup).not.toContain('aria-label="Gravar áudio"')
|
|
82
112
|
})
|
|
83
113
|
})
|