@adatechnology/conversations-ui 0.1.0-rc.25 → 0.1.0-rc.27
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 +3 -1
- package/dist/flows/index.js +61 -18
- 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 +1 -1
- 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/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 +16 -5
- package/src/flows/labels.ts +4 -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
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
type TranscriptionSettingsFormLabels,
|
|
34
34
|
} from './TranscriptionSettingsForm'
|
|
35
35
|
import type { TranscriptionMode } from '../types'
|
|
36
|
+
import { TooltipLayer } from '../Tooltip'
|
|
36
37
|
|
|
37
38
|
export interface BotMessages {
|
|
38
39
|
welcomeMessage: string
|
|
@@ -345,6 +346,7 @@ export function MessagesWorkspace({
|
|
|
345
346
|
|
|
346
347
|
return (
|
|
347
348
|
<div className={`space-y-6 p-4 lg:p-0 ${className ?? ''}`}>
|
|
349
|
+
<TooltipLayer />
|
|
348
350
|
<header>
|
|
349
351
|
<h1 className="text-xl font-semibold text-gray-900 dark:text-gray-100">{labels.title}</h1>
|
|
350
352
|
<p className="text-sm text-gray-500 dark:text-gray-400">{labels.subtitle}</p>
|
|
@@ -355,6 +357,7 @@ export function MessagesWorkspace({
|
|
|
355
357
|
<nav className="flex gap-1 border-b border-gray-200 dark:border-gray-700" role="tablist">
|
|
356
358
|
{tabs.map((item) => (
|
|
357
359
|
<button
|
|
360
|
+
data-cv-tooltip={item.label} aria-label={item.label}
|
|
358
361
|
key={item.id}
|
|
359
362
|
type="button"
|
|
360
363
|
role="tab"
|
|
@@ -66,6 +66,7 @@ export function TopicsForm({
|
|
|
66
66
|
<p className="text-sm font-medium text-gray-900 dark:text-gray-100">{topic.label}</p>
|
|
67
67
|
</div>
|
|
68
68
|
<button
|
|
69
|
+
data-cv-tooltip={topic.label} aria-label={topic.label}
|
|
69
70
|
type="button"
|
|
70
71
|
onClick={() => onToggle(topic.key)}
|
|
71
72
|
className={`relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none ${
|
|
@@ -94,6 +95,7 @@ export function TopicsForm({
|
|
|
94
95
|
|
|
95
96
|
<div className="flex items-center gap-3">
|
|
96
97
|
<button
|
|
98
|
+
data-cv-tooltip={labels.saveButton} aria-label={labels.saveButton}
|
|
97
99
|
type="submit"
|
|
98
100
|
disabled={saving}
|
|
99
101
|
className="inline-flex items-center gap-2 rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700 disabled:opacity-50"
|
|
@@ -76,6 +76,6 @@ describe('TranscriptionSettingsForm', () => {
|
|
|
76
76
|
const markup = render({ saving: true })
|
|
77
77
|
|
|
78
78
|
expect(markup).toContain('Salvando...')
|
|
79
|
-
expect(findTag(markup, /<button
|
|
79
|
+
expect(findTag(markup, /<button[^>]*>/g, 'type="submit"')).toContain('disabled')
|
|
80
80
|
})
|
|
81
81
|
})
|
|
@@ -132,6 +132,7 @@ export function TranscriptionSettingsForm({
|
|
|
132
132
|
|
|
133
133
|
<div className="flex justify-end">
|
|
134
134
|
<button
|
|
135
|
+
data-cv-tooltip={saving ? labels.saving : labels.save} aria-label={saving ? labels.saving : labels.save}
|
|
135
136
|
type="submit"
|
|
136
137
|
disabled={saving || !isAvailable}
|
|
137
138
|
className="flex items-center gap-2 bg-blue-600 hover:bg-blue-700 disabled:bg-blue-400 text-white font-semibold px-5 py-2.5 rounded-xl transition-colors text-sm"
|
|
@@ -104,6 +104,7 @@ export function WelcomeFarewellForm({
|
|
|
104
104
|
|
|
105
105
|
<div className="flex justify-end">
|
|
106
106
|
<button
|
|
107
|
+
data-cv-tooltip={saving ? labels.saving : labels.save} aria-label={saving ? labels.saving : labels.save}
|
|
107
108
|
type="submit"
|
|
108
109
|
disabled={saving}
|
|
109
110
|
className="flex items-center gap-2 bg-blue-600 hover:bg-blue-700 disabled:bg-blue-400 text-white font-semibold px-5 py-2.5 rounded-xl transition-colors text-sm"
|
|
@@ -297,6 +297,7 @@ export function WhatsAppCreateTemplateForm({
|
|
|
297
297
|
|
|
298
298
|
<div className="flex justify-end">
|
|
299
299
|
<button
|
|
300
|
+
data-cv-tooltip={submitting ? labels.sending : labels.sendForApproval} aria-label={submitting ? labels.sending : labels.sendForApproval}
|
|
300
301
|
type="submit"
|
|
301
302
|
disabled={submitting || !value.name || !value.bodyText}
|
|
302
303
|
className="flex items-center gap-2 bg-emerald-600 hover:bg-emerald-700 disabled:bg-emerald-400 text-white font-semibold px-5 py-2.5 rounded-xl transition-colors text-sm"
|
|
@@ -132,6 +132,7 @@ export function WhatsAppTemplateSettingsForm({
|
|
|
132
132
|
<div className="flex items-center justify-between mb-1.5">
|
|
133
133
|
<label className="text-sm font-medium text-gray-700 dark:text-gray-300">{labels.templateLabel}</label>
|
|
134
134
|
<button
|
|
135
|
+
data-cv-tooltip={labels.refreshList} aria-label={labels.refreshList}
|
|
135
136
|
type="button"
|
|
136
137
|
onClick={onRefreshTemplates}
|
|
137
138
|
disabled={loadingTemplates}
|
|
@@ -180,6 +181,7 @@ export function WhatsAppTemplateSettingsForm({
|
|
|
180
181
|
<div className="flex items-center justify-between mb-1.5">
|
|
181
182
|
<label className="text-sm font-medium text-gray-700 dark:text-gray-300">{labels.variablesLabel}</label>
|
|
182
183
|
<button
|
|
184
|
+
data-cv-tooltip={labels.addVariable} aria-label={labels.addVariable}
|
|
183
185
|
type="button"
|
|
184
186
|
onClick={() => onVariablesChange([...variables, ''])}
|
|
185
187
|
className="flex items-center gap-1 text-xs text-blue-600 dark:text-blue-400 hover:underline"
|
|
@@ -198,7 +200,7 @@ export function WhatsAppTemplateSettingsForm({
|
|
|
198
200
|
key={available.token}
|
|
199
201
|
type="button"
|
|
200
202
|
onClick={() => insertVariable(available.token)}
|
|
201
|
-
|
|
203
|
+
data-cv-tooltip={`Inserir ${available.token}`} aria-label={`Inserir ${available.token}`}
|
|
202
204
|
className="inline-flex items-center gap-1 rounded-md border border-gray-200 dark:border-gray-600 bg-white dark:bg-gray-800 px-2 py-1 text-xs hover:border-blue-400 dark:hover:border-blue-500 transition-colors"
|
|
203
205
|
>
|
|
204
206
|
<code className="text-blue-600 dark:text-blue-300">{available.token}</code>
|
|
@@ -233,7 +235,7 @@ export function WhatsAppTemplateSettingsForm({
|
|
|
233
235
|
type="button"
|
|
234
236
|
onClick={() => removeVariable(index)}
|
|
235
237
|
className="text-gray-400 hover:text-red-500 dark:hover:text-red-400 transition-colors flex-shrink-0"
|
|
236
|
-
|
|
238
|
+
data-cv-tooltip={labels.removeVariable} aria-label={labels.removeVariable}
|
|
237
239
|
>
|
|
238
240
|
<Trash2 size={14} />
|
|
239
241
|
</button>
|
|
@@ -250,6 +252,7 @@ export function WhatsAppTemplateSettingsForm({
|
|
|
250
252
|
|
|
251
253
|
<div className="flex justify-end">
|
|
252
254
|
<button
|
|
255
|
+
data-cv-tooltip={saving ? labels.saving : labels.save} aria-label={saving ? labels.saving : labels.save}
|
|
253
256
|
type="submit"
|
|
254
257
|
disabled={saving || !selectedTemplateName.trim()}
|
|
255
258
|
className="flex items-center gap-2 bg-blue-600 hover:bg-blue-700 disabled:bg-blue-400 text-white font-semibold px-5 py-2.5 rounded-xl transition-colors text-sm"
|
|
@@ -78,6 +78,7 @@ export function WhatsAppTemplatesSettings({
|
|
|
78
78
|
<div className="space-y-4">
|
|
79
79
|
<nav className="flex gap-1 border-b" role="tablist">
|
|
80
80
|
<button
|
|
81
|
+
data-cv-tooltip={labels.selectTab} aria-label={labels.selectTab}
|
|
81
82
|
type="button"
|
|
82
83
|
role="tab"
|
|
83
84
|
aria-selected={tab === TEMPLATE_SETTINGS_TAB.SELECT}
|
|
@@ -91,6 +92,7 @@ export function WhatsAppTemplatesSettings({
|
|
|
91
92
|
que não leva a nada. */}
|
|
92
93
|
{create ? (
|
|
93
94
|
<button
|
|
95
|
+
data-cv-tooltip={labels.createTab} aria-label={labels.createTab}
|
|
94
96
|
type="button"
|
|
95
97
|
role="tab"
|
|
96
98
|
aria-selected={tab === TEMPLATE_SETTINGS_TAB.CREATE}
|
package/src/styles.css
CHANGED
|
@@ -22,7 +22,12 @@
|
|
|
22
22
|
|
|
23
23
|
/* Botão de ação do cabeçalho de conversa e do aviso de janela. Vive no stylesheet, como o
|
|
24
24
|
.cv-wallpaper, para não depender da configuração de Tailwind do consumidor. */
|
|
25
|
+
/* `inline-flex` com gap em todo controle com ícone: o ícone é SVG e, como elemento inline, assentava
|
|
26
|
+
na baseline do texto — o traço ficava um fio abaixo do rótulo em cada botão. */
|
|
25
27
|
.cv-header-action {
|
|
28
|
+
display: inline-flex;
|
|
29
|
+
align-items: center;
|
|
30
|
+
gap: 0.375rem;
|
|
26
31
|
border: 1px solid rgb(203 213 225);
|
|
27
32
|
border-radius: 0.375rem;
|
|
28
33
|
padding: 0.375rem 0.75rem;
|
|
@@ -34,6 +39,9 @@
|
|
|
34
39
|
.dark .cv-header-action { border-color: rgb(71 85 105); color: rgb(226 232 240); }
|
|
35
40
|
|
|
36
41
|
.cv-header-icon {
|
|
42
|
+
display: inline-flex;
|
|
43
|
+
align-items: center;
|
|
44
|
+
justify-content: center;
|
|
37
45
|
border: 1px solid rgb(203 213 225);
|
|
38
46
|
border-radius: 0.5rem;
|
|
39
47
|
padding: 0.375rem 0.5rem;
|
|
@@ -100,6 +108,9 @@
|
|
|
100
108
|
|
|
101
109
|
/* Voltar do master/detail: existe só onde lista e conversa não cabem lado a lado. */
|
|
102
110
|
.cv-back {
|
|
111
|
+
display: inline-flex;
|
|
112
|
+
align-items: center;
|
|
113
|
+
justify-content: center;
|
|
103
114
|
border: 1px solid rgb(203 213 225);
|
|
104
115
|
border-radius: 0.5rem;
|
|
105
116
|
padding: 0.25rem 0.5rem;
|
|
@@ -110,6 +121,13 @@
|
|
|
110
121
|
@media (min-width: 1024px) { .cv-back { display: none; } }
|
|
111
122
|
.dark .cv-back { border-color: rgb(71 85 105); color: rgb(226 232 240); }
|
|
112
123
|
|
|
124
|
+
/* Contador do cabeçalho da inbox: ícone e número na mesma linha de base ótica. */
|
|
125
|
+
.cv-stat {
|
|
126
|
+
display: inline-flex;
|
|
127
|
+
align-items: center;
|
|
128
|
+
gap: 0.25rem;
|
|
129
|
+
}
|
|
130
|
+
|
|
113
131
|
/* Utilitários de densidade: em tela estreita o texto das ações vira ícone, porque rótulo longo
|
|
114
132
|
empilha e come a altura útil da conversa. */
|
|
115
133
|
.cv-only-wide { display: none; }
|
|
@@ -328,6 +346,9 @@
|
|
|
328
346
|
}
|
|
329
347
|
|
|
330
348
|
.cv-workspace-toggle {
|
|
349
|
+
display: inline-flex;
|
|
350
|
+
align-items: center;
|
|
351
|
+
gap: 0.375rem;
|
|
331
352
|
border: 1px solid rgb(229 231 235);
|
|
332
353
|
border-radius: 0.375rem;
|
|
333
354
|
padding: 0.5rem 0.75rem;
|
|
@@ -495,24 +516,48 @@
|
|
|
495
516
|
}
|
|
496
517
|
.dark .cv-workspace-search { border-color: rgb(71 85 105); color: rgb(226 232 240); }
|
|
497
518
|
|
|
519
|
+
/* Linha única com rolagem, e não wrap: quebrando, a fila de janelas empurrava a categoria e a
|
|
520
|
+
lista para baixo toda vez que a coluna estreitava. */
|
|
498
521
|
.cv-workspace-chips {
|
|
499
522
|
display: flex;
|
|
500
|
-
flex-wrap:
|
|
523
|
+
flex-wrap: nowrap;
|
|
501
524
|
align-items: center;
|
|
502
|
-
gap: 0.
|
|
525
|
+
gap: 0.375rem;
|
|
503
526
|
font-size: 0.75rem;
|
|
527
|
+
overflow-x: auto;
|
|
528
|
+
scrollbar-width: none;
|
|
504
529
|
}
|
|
505
|
-
.cv-workspace-
|
|
530
|
+
.cv-workspace-chips::-webkit-scrollbar { display: none; }
|
|
531
|
+
.cv-workspace-chips__legend { color: rgb(107 114 128); flex-shrink: 0; }
|
|
506
532
|
.cv-workspace-chip {
|
|
507
533
|
display: flex;
|
|
534
|
+
flex-shrink: 0;
|
|
508
535
|
align-items: center;
|
|
509
536
|
gap: 0.25rem;
|
|
537
|
+
white-space: nowrap;
|
|
510
538
|
border-radius: 0.375rem;
|
|
511
539
|
padding: 0.25rem 0.5rem;
|
|
512
540
|
}
|
|
513
541
|
.cv-workspace-chip--on { background: rgb(229 231 235); }
|
|
514
542
|
.dark .cv-workspace-chip--on { background: rgb(51 65 85); }
|
|
515
|
-
|
|
543
|
+
|
|
544
|
+
/* A cor do próprio rótulo é o estado da janela; selecionado, o mesmo tom vira o fundo. */
|
|
545
|
+
.cv-workspace-chip--fresh { color: rgb(22 163 74); }
|
|
546
|
+
.cv-workspace-chip--warning { color: rgb(202 138 4); }
|
|
547
|
+
.cv-workspace-chip--critical { color: rgb(220 38 38); }
|
|
548
|
+
.cv-workspace-chip--expired { color: rgb(107 114 128); }
|
|
549
|
+
.cv-workspace-chip--fresh.cv-workspace-chip--on { background: rgb(220 252 231); }
|
|
550
|
+
.cv-workspace-chip--warning.cv-workspace-chip--on { background: rgb(254 249 195); }
|
|
551
|
+
.cv-workspace-chip--critical.cv-workspace-chip--on { background: rgb(254 226 226); }
|
|
552
|
+
.cv-workspace-chip--expired.cv-workspace-chip--on { background: rgb(229 231 235); }
|
|
553
|
+
.dark .cv-workspace-chip--fresh { color: rgb(74 222 128); }
|
|
554
|
+
.dark .cv-workspace-chip--warning { color: rgb(250 204 21); }
|
|
555
|
+
.dark .cv-workspace-chip--critical { color: rgb(248 113 113); }
|
|
556
|
+
.dark .cv-workspace-chip--expired { color: rgb(148 163 184); }
|
|
557
|
+
.dark .cv-workspace-chip--fresh.cv-workspace-chip--on { background: rgb(20 83 45); }
|
|
558
|
+
.dark .cv-workspace-chip--warning.cv-workspace-chip--on { background: rgb(113 63 18); }
|
|
559
|
+
.dark .cv-workspace-chip--critical.cv-workspace-chip--on { background: rgb(127 29 29); }
|
|
560
|
+
.dark .cv-workspace-chip--expired.cv-workspace-chip--on { background: rgb(51 65 85); }
|
|
516
561
|
|
|
517
562
|
.cv-workspace-selectall {
|
|
518
563
|
display: flex;
|
|
@@ -842,3 +887,54 @@
|
|
|
842
887
|
font-size: 0.75rem;
|
|
843
888
|
}
|
|
844
889
|
.dark .cv-listing-perpage { border-color: rgb(71 85 105); }
|
|
890
|
+
|
|
891
|
+
/* Dica de ação (ver `Tooltip.tsx`). Impressa em `document.body`, então a posição é da janela e o
|
|
892
|
+
`z-index` fica acima de qualquer painel do produto. */
|
|
893
|
+
.cv-tooltip {
|
|
894
|
+
position: fixed;
|
|
895
|
+
z-index: 9999;
|
|
896
|
+
max-width: min(16rem, 90vw);
|
|
897
|
+
transform: translate(-50%, -100%);
|
|
898
|
+
border-radius: 0.375rem;
|
|
899
|
+
background: rgb(17 24 39 / 95%);
|
|
900
|
+
padding: 0.3125rem 0.5rem;
|
|
901
|
+
font-size: 0.75rem;
|
|
902
|
+
line-height: 1.2;
|
|
903
|
+
color: #fff;
|
|
904
|
+
text-align: center;
|
|
905
|
+
box-shadow: 0 4px 12px rgb(0 0 0 / 18%);
|
|
906
|
+
pointer-events: none;
|
|
907
|
+
animation: cv-tooltip-in 90ms ease-out;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
.cv-tooltip--bottom {
|
|
911
|
+
transform: translate(-50%, 0);
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
.dark .cv-tooltip {
|
|
915
|
+
background: rgb(243 244 246 / 96%);
|
|
916
|
+
color: rgb(17 24 39);
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
@keyframes cv-tooltip-in {
|
|
920
|
+
from { opacity: 0; }
|
|
921
|
+
to { opacity: 1; }
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
@media (prefers-reduced-motion: reduce) {
|
|
925
|
+
.cv-tooltip { animation: none; }
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
/* Barra de composição: fundo neutro para separar da conversa e para os atalhos e o campo de texto
|
|
929
|
+
terem contraste próprio. Sobre branco, régua branca e campo cinza-claro se confundiam num bloco
|
|
930
|
+
só, sem hierarquia. */
|
|
931
|
+
.cv-composer-bar {
|
|
932
|
+
border-top: 1px solid rgb(229 231 235);
|
|
933
|
+
background: rgb(240 242 245);
|
|
934
|
+
padding: 0.625rem 0.75rem;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
.dark .cv-composer-bar {
|
|
938
|
+
border-top-color: rgb(51 65 85);
|
|
939
|
+
background: rgb(17 24 39);
|
|
940
|
+
}
|
|
@@ -70,7 +70,7 @@ export function BulkTemplateModal({
|
|
|
70
70
|
<h3>{labels.templateModalTitle}</h3>
|
|
71
71
|
<p>{labels.templateModalAvailable(templates.length)}</p>
|
|
72
72
|
</div>
|
|
73
|
-
<button type="button" onClick={onClose} aria-label={labels.templateModalCancel}>
|
|
73
|
+
<button data-cv-tooltip={labels.templateModalCancel} type="button" onClick={onClose} aria-label={labels.templateModalCancel}>
|
|
74
74
|
✕
|
|
75
75
|
</button>
|
|
76
76
|
</header>
|
|
@@ -89,6 +89,7 @@ export function BulkTemplateModal({
|
|
|
89
89
|
{filtered.length === 0 ? <p className="cv-workspace-empty">{labels.templateModalEmpty}</p> : null}
|
|
90
90
|
{filtered.map((template) => (
|
|
91
91
|
<button
|
|
92
|
+
data-cv-tooltip={template.name} aria-label={template.name}
|
|
92
93
|
key={template.name}
|
|
93
94
|
type="button"
|
|
94
95
|
// Reclicar desmarca: sem isso não havia como voltar ao template padrão depois de
|
|
@@ -113,10 +114,11 @@ export function BulkTemplateModal({
|
|
|
113
114
|
) : null}
|
|
114
115
|
|
|
115
116
|
<footer className="cv-workspace-modal__footer">
|
|
116
|
-
<button type="button" onClick={onClose}>
|
|
117
|
+
<button data-cv-tooltip={labels.templateModalCancel} aria-label={labels.templateModalCancel} type="button" onClick={onClose}>
|
|
117
118
|
{labels.templateModalCancel}
|
|
118
119
|
</button>
|
|
119
120
|
<button
|
|
121
|
+
data-cv-tooltip={sending ? labels.templateModalSending : labels.templateModalSend(expiredCount)} aria-label={sending ? labels.templateModalSending : labels.templateModalSend(expiredCount)}
|
|
120
122
|
type="button"
|
|
121
123
|
disabled={sending || expiredCount === 0}
|
|
122
124
|
onClick={() => onSend(selected || undefined)}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* última mensagem, outro engolia falha de anexo, outro não abria a biblioteca de arquivos.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { useEffect, useState, type ReactNode } from 'react'
|
|
8
|
+
import { useEffect, useRef, useState, type ReactNode } from 'react'
|
|
9
9
|
|
|
10
10
|
import { AudioRecorderButton } from '../AudioRecorderButton'
|
|
11
11
|
import { ConversationContextPanel, type ConversationContextEntry } from '../ConversationContextPanel'
|
|
@@ -36,7 +36,8 @@ export interface ConversationPaneProps {
|
|
|
36
36
|
readonly now: number
|
|
37
37
|
readonly busy: boolean
|
|
38
38
|
readonly labels: ConversationsWorkspaceLabels
|
|
39
|
-
|
|
39
|
+
/** Devolve a promessa quando o host a tem: o envio de template espera a tomada antes de sair. */
|
|
40
|
+
readonly onTakeover?: (() => void | Promise<void>) | undefined
|
|
40
41
|
readonly onReturnToBot?: (() => void) | undefined
|
|
41
42
|
readonly onFinish?: (() => void) | undefined
|
|
42
43
|
readonly onBack: () => void
|
|
@@ -139,6 +140,9 @@ export function ConversationPane({
|
|
|
139
140
|
const [selectedMessageIds, setSelectedMessageIds] = useState<ReadonlySet<string>>(new Set())
|
|
140
141
|
const [draft, setDraft] = useState(initialComposerText ?? '')
|
|
141
142
|
const [queuedFiles, setQueuedFiles] = useState<readonly File[]>([])
|
|
143
|
+
const [isSendingDraft, setIsSendingDraft] = useState(false)
|
|
144
|
+
/** Ref, não estado: entre dois cliques seguidos o React ainda não teria repintado a trava. */
|
|
145
|
+
const sendInFlightRef = useRef(false)
|
|
142
146
|
|
|
143
147
|
// Trocar de conversa zera as duas coisas: seleção de mensagem e rascunho pertencem à thread, e
|
|
144
148
|
// levá-los adiante faria copiar o trecho errado ou responder ao cliente errado.
|
|
@@ -187,18 +191,31 @@ export function ConversationPane({
|
|
|
187
191
|
* Falha vira aviso na tela em vez de exceção silenciosa: o atendente escreveu ou gravou, achou
|
|
188
192
|
* que mandou, e sem retorno não teria como saber que o cliente não recebeu nada.
|
|
189
193
|
*/
|
|
190
|
-
|
|
194
|
+
/** Devolve se o envio passou: limpar o rascunho depois de uma falha apagaria o texto do operador. */
|
|
195
|
+
async function runSend(action: () => Promise<unknown>, fallback: string): Promise<boolean> {
|
|
191
196
|
setSendFailure(undefined)
|
|
192
197
|
try {
|
|
193
198
|
await action()
|
|
194
199
|
await refetch()
|
|
200
|
+
return true
|
|
195
201
|
} catch (error: unknown) {
|
|
196
202
|
setSendFailure(error instanceof Error ? error.message : fallback)
|
|
203
|
+
return false
|
|
197
204
|
}
|
|
198
205
|
}
|
|
199
206
|
|
|
200
|
-
async function handleSend(text: string): Promise<
|
|
201
|
-
|
|
207
|
+
async function handleSend(text: string): Promise<boolean> {
|
|
208
|
+
return runSend(() => api.sendMessage(conversation.id, text), labels.sendFailure)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Reabrir a janela é ato de atendente: quem manda o template quer conversar. A tomada vai antes do
|
|
213
|
+
* envio — se ficasse depois, a resposta do cliente chegaria com a conversa ainda no bot e o fluxo
|
|
214
|
+
* automático responderia por cima de quem acabou de reabri-la.
|
|
215
|
+
*/
|
|
216
|
+
async function handleSendTemplate(): Promise<void> {
|
|
217
|
+
if (conversation.mode !== 'human' && onTakeover) await onTakeover()
|
|
218
|
+
await runSend(() => api.sendTemplate(conversation.id, {}), labels.sendFailure)
|
|
202
219
|
}
|
|
203
220
|
|
|
204
221
|
/**
|
|
@@ -206,17 +223,28 @@ export function ConversationPane({
|
|
|
206
223
|
* frase embaixo, e mandar as duas separadas invertia a ordem quando a mídia demorava a subir.
|
|
207
224
|
*/
|
|
208
225
|
async function handleRichSend(): Promise<void> {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
226
|
+
// O upload da mídia demora e não dá retorno na tela; sem esta trava o segundo clique — ou o
|
|
227
|
+
// Enter impaciente — mandava o mesmo arquivo outra vez.
|
|
228
|
+
if (sendInFlightRef.current) return
|
|
229
|
+
sendInFlightRef.current = true
|
|
230
|
+
setIsSendingDraft(true)
|
|
231
|
+
try {
|
|
232
|
+
if (onSendAttachments && queuedFiles.length > 0) {
|
|
233
|
+
const files = queuedFiles
|
|
234
|
+
const caption = draft
|
|
235
|
+
const didSend = await runSend(() => onSendAttachments(files, caption), labels.attachFailure)
|
|
236
|
+
if (didSend) {
|
|
237
|
+
setQueuedFiles([])
|
|
238
|
+
setDraft('')
|
|
239
|
+
}
|
|
240
|
+
return
|
|
241
|
+
}
|
|
242
|
+
if (!draft.trim()) return
|
|
243
|
+
if (await handleSend(draft)) setDraft('')
|
|
244
|
+
} finally {
|
|
245
|
+
sendInFlightRef.current = false
|
|
246
|
+
setIsSendingDraft(false)
|
|
216
247
|
}
|
|
217
|
-
if (!draft.trim()) return
|
|
218
|
-
await handleSend(draft)
|
|
219
|
-
setDraft('')
|
|
220
248
|
}
|
|
221
249
|
|
|
222
250
|
async function handleAttach(file: File): Promise<void> {
|
|
@@ -309,10 +337,10 @@ export function ConversationPane({
|
|
|
309
337
|
<div className="cv-workspace-selection">
|
|
310
338
|
<span>{labels.messagesSelected(selectedMessageIds.size)}</span>
|
|
311
339
|
<div className="cv-workspace-selection__actions">
|
|
312
|
-
<button type="button" onClick={() => setSelectedMessageIds(new Set())}>
|
|
340
|
+
<button data-cv-tooltip={labels.bulkClear} aria-label={labels.bulkClear} type="button" onClick={() => setSelectedMessageIds(new Set())}>
|
|
313
341
|
{labels.bulkClear}
|
|
314
342
|
</button>
|
|
315
|
-
<button type="button" onClick={copySelectedMessages}>
|
|
343
|
+
<button data-cv-tooltip={labels.copySelected} aria-label={labels.copySelected} type="button" onClick={copySelectedMessages}>
|
|
316
344
|
{labels.copySelected}
|
|
317
345
|
</button>
|
|
318
346
|
</div>
|
|
@@ -328,7 +356,7 @@ export function ConversationPane({
|
|
|
328
356
|
{blocked ? (
|
|
329
357
|
<WindowExpiredNotice
|
|
330
358
|
disabled={busy}
|
|
331
|
-
onSendTemplate={() => void
|
|
359
|
+
onSendTemplate={() => void handleSendTemplate()}
|
|
332
360
|
/>
|
|
333
361
|
) : botOwnsConversation ? (
|
|
334
362
|
// Responder com a conversa no bot atropelaria o fluxo automático no meio de uma pergunta.
|
|
@@ -348,7 +376,8 @@ export function ConversationPane({
|
|
|
348
376
|
}
|
|
349
377
|
: {})}
|
|
350
378
|
placeholder={labels.composerPlaceholder}
|
|
351
|
-
isSending={busy}
|
|
379
|
+
isSending={busy || isSendingDraft}
|
|
380
|
+
hasQueuedAttachments={queuedFiles.length > 0}
|
|
352
381
|
{...(onRecordAudio
|
|
353
382
|
? {
|
|
354
383
|
idleAction: (
|
|
@@ -367,6 +396,7 @@ export function ConversationPane({
|
|
|
367
396
|
<li key={`${file.name}-${index}`}>
|
|
368
397
|
<span>{file.name}</span>
|
|
369
398
|
<button
|
|
399
|
+
data-cv-tooltip={labels.attachmentRemove}
|
|
370
400
|
type="button"
|
|
371
401
|
aria-label={labels.attachmentRemove}
|
|
372
402
|
onClick={() =>
|
|
@@ -14,6 +14,7 @@ import { WINDOW_FILTERS } from '../conversationWindow'
|
|
|
14
14
|
import type { ConversationSummary } from '../providers/types'
|
|
15
15
|
import type { ConversationsWorkspaceLabels } from './labels'
|
|
16
16
|
import type { UseConversationsInboxResult } from './useConversationsInbox'
|
|
17
|
+
import { PAGINATION_LABELS } from '../pagination.constant'
|
|
17
18
|
|
|
18
19
|
export interface ConversationsInboxListProps {
|
|
19
20
|
readonly inbox: UseConversationsInboxResult
|
|
@@ -44,16 +45,17 @@ export function ConversationsInboxList({
|
|
|
44
45
|
<div className="cv-workspace-bulk">
|
|
45
46
|
<span className="cv-workspace-bulk__count">{labels.bulkSelected(inbox.selectedIds.size)}</span>
|
|
46
47
|
<div className="cv-workspace-bulk__actions">
|
|
47
|
-
<button type="button" onClick={inbox.clearBulkSelection}>
|
|
48
|
+
<button data-cv-tooltip={labels.bulkClear} aria-label={labels.bulkClear} type="button" onClick={inbox.clearBulkSelection}>
|
|
48
49
|
{labels.bulkClear}
|
|
49
50
|
</button>
|
|
50
51
|
{onSendTemplateToSelected ? (
|
|
51
|
-
<button type="button" onClick={onSendTemplateToSelected} disabled={inbox.busy}>
|
|
52
|
+
<button data-cv-tooltip={labels.bulkTemplate} aria-label={labels.bulkTemplate} type="button" onClick={onSendTemplateToSelected} disabled={inbox.busy}>
|
|
52
53
|
{labels.bulkTemplate}
|
|
53
54
|
</button>
|
|
54
55
|
) : null}
|
|
55
56
|
{inbox.canFinalize ? (
|
|
56
57
|
<button
|
|
58
|
+
data-cv-tooltip={labels.bulkFinalize} aria-label={labels.bulkFinalize}
|
|
57
59
|
type="button"
|
|
58
60
|
disabled={inbox.busy}
|
|
59
61
|
onClick={() => {
|
|
@@ -89,9 +91,15 @@ export function ConversationsInboxList({
|
|
|
89
91
|
type="button"
|
|
90
92
|
onClick={() => inbox.setWindowFilter(filter.value)}
|
|
91
93
|
aria-pressed={inbox.windowFilter === filter.value}
|
|
92
|
-
|
|
94
|
+
data-cv-tooltip={filter.label} aria-label={filter.label}
|
|
95
|
+
className={[
|
|
96
|
+
'cv-workspace-chip',
|
|
97
|
+
filter.tone ? `cv-workspace-chip--${filter.tone}` : '',
|
|
98
|
+
inbox.windowFilter === filter.value ? 'cv-workspace-chip--on' : '',
|
|
99
|
+
]
|
|
100
|
+
.filter(Boolean)
|
|
101
|
+
.join(' ')}
|
|
93
102
|
>
|
|
94
|
-
{filter.dotClass ? <span className={`cv-workspace-dot ${filter.dotClass}`} /> : null}
|
|
95
103
|
{filter.label}
|
|
96
104
|
</button>
|
|
97
105
|
))}
|
|
@@ -103,6 +111,7 @@ export function ConversationsInboxList({
|
|
|
103
111
|
<span className="cv-workspace-chips__legend">{labels.channelLegend}</span>
|
|
104
112
|
{inbox.channelFilters.map((filter) => (
|
|
105
113
|
<button
|
|
114
|
+
data-cv-tooltip={filter.label} aria-label={filter.label}
|
|
106
115
|
key={filter.value}
|
|
107
116
|
type="button"
|
|
108
117
|
onClick={() => inbox.setChannelFilter(filter.value)}
|
|
@@ -150,14 +159,14 @@ export function ConversationsInboxList({
|
|
|
150
159
|
<div className="cv-workspace-pager">
|
|
151
160
|
<span>{labels.rangeOf(inbox.firstOnPage, inbox.lastOnPage, inbox.filteredCount)}</span>
|
|
152
161
|
<div className="cv-workspace-pager__buttons">
|
|
153
|
-
<button type="button" onClick={() => inbox.goToPage(1)} disabled={inbox.page === 1} aria-label=
|
|
162
|
+
<button type="button" onClick={() => inbox.goToPage(1)} disabled={inbox.page === 1} aria-label={PAGINATION_LABELS.first} data-cv-tooltip={PAGINATION_LABELS.first}>
|
|
154
163
|
«
|
|
155
164
|
</button>
|
|
156
165
|
<button
|
|
157
166
|
type="button"
|
|
158
167
|
onClick={() => inbox.goToPage(inbox.page - 1)}
|
|
159
168
|
disabled={inbox.page === 1}
|
|
160
|
-
aria-label=
|
|
169
|
+
aria-label={PAGINATION_LABELS.previous} data-cv-tooltip={PAGINATION_LABELS.previous}
|
|
161
170
|
>
|
|
162
171
|
‹
|
|
163
172
|
</button>
|
|
@@ -166,7 +175,7 @@ export function ConversationsInboxList({
|
|
|
166
175
|
type="button"
|
|
167
176
|
onClick={() => inbox.goToPage(inbox.page + 1)}
|
|
168
177
|
disabled={inbox.page === inbox.pageCount}
|
|
169
|
-
aria-label=
|
|
178
|
+
aria-label={PAGINATION_LABELS.next} data-cv-tooltip={PAGINATION_LABELS.next}
|
|
170
179
|
>
|
|
171
180
|
›
|
|
172
181
|
</button>
|
|
@@ -174,7 +183,7 @@ export function ConversationsInboxList({
|
|
|
174
183
|
type="button"
|
|
175
184
|
onClick={() => inbox.goToPage(inbox.pageCount)}
|
|
176
185
|
disabled={inbox.page === inbox.pageCount}
|
|
177
|
-
aria-label=
|
|
186
|
+
aria-label={PAGINATION_LABELS.last} data-cv-tooltip={PAGINATION_LABELS.last}
|
|
178
187
|
>
|
|
179
188
|
»
|
|
180
189
|
</button>
|