@adatechnology/conversations-ui 0.1.0-rc.2 → 0.1.0-rc.21

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.
Files changed (106) hide show
  1. package/dist/{chunk-ZDURDZTM.js → chunk-2AYDBWNE.js} +14 -0
  2. package/dist/chunk-TV4OQRGH.js +2187 -0
  3. package/dist/flows/index.d.ts +14 -2
  4. package/dist/flows/index.js +117 -39
  5. package/dist/index.d.ts +862 -136
  6. package/dist/index.js +1746 -1124
  7. package/dist/preview/index.d.ts +470 -0
  8. package/dist/preview/index.js +1329 -0
  9. package/dist/styles.css +228 -0
  10. package/dist/types-C6A_9edv.d.ts +456 -0
  11. package/package.json +10 -3
  12. package/src/AudioRecorderButton.test.tsx +30 -0
  13. package/src/AudioRecorderButton.tsx +248 -0
  14. package/src/AudioTranscription.test.tsx +115 -0
  15. package/src/AudioTranscription.tsx +249 -0
  16. package/src/Avatar.tsx +30 -4
  17. package/src/ChannelIcon.tsx +87 -0
  18. package/src/ConversationContextPanel.tsx +280 -0
  19. package/src/ConversationDocumentsPanel.tsx +425 -0
  20. package/src/ConversationHeader.tsx +257 -0
  21. package/src/ConversationListItem.tsx +54 -7
  22. package/src/ConversationLocalesProvider.tsx +44 -0
  23. package/src/ConversationRow.tsx +137 -0
  24. package/src/DateDivider.tsx +16 -3
  25. package/src/DocumentsLibrary.tsx +322 -0
  26. package/src/EmojiPicker.tsx +69 -55
  27. package/src/FileIcon.test.ts +83 -0
  28. package/src/FileIcon.tsx +88 -11
  29. package/src/InteractiveMessage.test.tsx +41 -0
  30. package/src/InteractiveMessage.tsx +143 -0
  31. package/src/Lightbox.tsx +18 -3
  32. package/src/MediaRenderer.tsx +96 -22
  33. package/src/MessageBubble.tsx +77 -5
  34. package/src/MessageComposer.test.tsx +35 -0
  35. package/src/MessageComposer.tsx +165 -19
  36. package/src/RichMessageComposer.test.tsx +83 -0
  37. package/src/RichMessageComposer.tsx +380 -0
  38. package/src/Wallpaper.test.tsx +21 -0
  39. package/src/Wallpaper.tsx +69 -7
  40. package/src/WhatsAppMessageEditor.tsx +28 -4
  41. package/src/WindowExpiredNotice.tsx +57 -0
  42. package/src/audioRecorderFormat.test.ts +67 -0
  43. package/src/conversationChannel.test.ts +53 -0
  44. package/src/conversationChannel.ts +146 -0
  45. package/src/conversationTranscript.test.ts +122 -0
  46. package/src/conversationTranscript.ts +89 -0
  47. package/src/conversationWindow.test.ts +90 -0
  48. package/src/conversationWindow.ts +78 -0
  49. package/src/documentTypeLabel.test.ts +57 -0
  50. package/src/emojiCatalog.test.ts +35 -0
  51. package/src/emojiCatalog.ts +189 -0
  52. package/src/flows/FlowGroupHeader.tsx +12 -2
  53. package/src/flows/FlowMapCanvas.tsx +17 -14
  54. package/src/flows/FlowMapNode.tsx +3 -1
  55. package/src/flows/FlowNodeCard.tsx +22 -4
  56. package/src/flows/FlowNodePanel.tsx +132 -35
  57. package/src/flows/FlowPalette.tsx +6 -2
  58. package/src/flows/FlowWhatsAppPreview.tsx +14 -3
  59. package/src/flows/flowGraph.ts +5 -5
  60. package/src/flows/labels.ts +5 -0
  61. package/src/hooks/useConversationActions.ts +56 -0
  62. package/src/hooks/useConversationDocuments.ts +15 -9
  63. package/src/hooks/useConversationList.ts +15 -9
  64. package/src/hooks/useConversationMessages.ts +2 -2
  65. package/src/hooks/useScrollToLatestMessage.ts +127 -0
  66. package/src/index.ts +140 -16
  67. package/src/lib/cn.test.ts +29 -0
  68. package/src/lib/cn.ts +15 -0
  69. package/src/lib/createMediaUrlResolver.ts +33 -0
  70. package/src/lib/paginated.test.ts +33 -0
  71. package/src/lib/paginated.ts +26 -0
  72. package/src/lib/phone.ts +34 -0
  73. package/src/preview/ConversationPreview.tsx +334 -0
  74. package/src/preview/MediaTypesPreview.tsx +87 -0
  75. package/src/preview/conversationPreviewFailures.test.ts +64 -0
  76. package/src/preview/createMockConversationsApi.ts +271 -0
  77. package/src/preview/createMockSSEProvider.ts +40 -0
  78. package/src/preview/createPreviewBridgeClient.test.ts +92 -0
  79. package/src/preview/createPreviewBridgeClient.ts +124 -0
  80. package/src/preview/createPreviewMediaUploader.ts +82 -0
  81. package/src/preview/createPreviewWebhookClient.test.ts +194 -0
  82. package/src/preview/createPreviewWebhookClient.ts +222 -0
  83. package/src/preview/index.ts +67 -0
  84. package/src/preview/mediaTypeOf.test.ts +15 -0
  85. package/src/preview/mockDocumentsSearch.test.ts +57 -0
  86. package/src/preview/mockEventSource.ts +53 -0
  87. package/src/preview/preview.test.ts +177 -0
  88. package/src/preview/previewFileSamples.test.ts +151 -0
  89. package/src/preview/previewFileSamples.ts +74 -0
  90. package/src/preview/previewFixtures.ts +440 -0
  91. package/src/preview/previewMediaSource.test.ts +62 -0
  92. package/src/preview/previewMediaSource.ts +91 -0
  93. package/src/preview/previewMediaUploader.test.ts +61 -0
  94. package/src/preview/previewStore.ts +193 -0
  95. package/src/preview/startPreviewScript.ts +60 -0
  96. package/src/providers/types.ts +175 -12
  97. package/src/quickReply.test.ts +58 -0
  98. package/src/settings/TranscriptionSettingsForm.test.tsx +81 -0
  99. package/src/settings/TranscriptionSettingsForm.tsx +189 -0
  100. package/src/settings/WhatsAppCreateTemplateForm.tsx +3 -1
  101. package/src/settings/WhatsAppTemplatesSettings.tsx +106 -0
  102. package/src/styles.css +173 -0
  103. package/src/types.ts +72 -1
  104. package/src/useDarkMode.ts +26 -0
  105. package/src/useIsNarrow.ts +29 -0
  106. package/src/useWaitingNotifications.ts +74 -29
package/dist/styles.css CHANGED
@@ -9,3 +9,231 @@
9
9
  background-color: #0b141a;
10
10
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100' height='100' viewBox='0 0 100 100'%3E%3Cg fill='none' stroke='%2319232a' stroke-width='1.2' opacity='0.9'%3E%3Ccircle cx='15' cy='15' r='3'/%3E%3Cpath d='M40 10 q5 8 0 16 q-5 -8 0 -16z'/%3E%3Ccircle cx='70' cy='28' r='2'/%3E%3Cpath d='M18 55 l6 6 m-6 0 l6 -6'/%3E%3Ccircle cx='55' cy='68' r='2.5'/%3E%3Cpath d='M85 58 q6 6 0 12 q-6 -6 0 -12z'/%3E%3Ccircle cx='8' cy='85' r='2'/%3E%3Cpath d='M65 90 l5 5 m-5 0 l5 -5'/%3E%3C/g%3E%3C/svg%3E");
11
11
  }
12
+ .cv-header-action {
13
+ border: 1px solid rgb(203 213 225);
14
+ border-radius: 0.375rem;
15
+ padding: 0.375rem 0.75rem;
16
+ font-size: 0.8125rem;
17
+ background: transparent;
18
+ cursor: pointer;
19
+ }
20
+ .cv-header-action:disabled {
21
+ opacity: 0.5;
22
+ cursor: not-allowed;
23
+ }
24
+ .dark .cv-header-action {
25
+ border-color: rgb(71 85 105);
26
+ color: rgb(226 232 240);
27
+ }
28
+ .cv-header-icon {
29
+ border: 1px solid rgb(203 213 225);
30
+ border-radius: 0.5rem;
31
+ padding: 0.375rem 0.5rem;
32
+ background: transparent;
33
+ cursor: pointer;
34
+ line-height: 1;
35
+ }
36
+ .cv-header-icon--active {
37
+ background: rgb(241 245 249);
38
+ border-color: rgb(148 163 184);
39
+ }
40
+ .cv-header-icon:disabled {
41
+ opacity: 0.5;
42
+ cursor: not-allowed;
43
+ }
44
+ .dark .cv-header-icon {
45
+ border-color: rgb(71 85 105);
46
+ }
47
+ .dark .cv-header-icon--active {
48
+ background: rgb(51 65 85);
49
+ }
50
+ .cv-header-action--primary {
51
+ background: rgb(37 99 235);
52
+ border-color: rgb(37 99 235);
53
+ color: white;
54
+ font-weight: 500;
55
+ }
56
+ .cv-header-action--danger {
57
+ background: rgb(239 68 68);
58
+ border-color: rgb(239 68 68);
59
+ color: white;
60
+ font-weight: 500;
61
+ }
62
+ .cv-subtab {
63
+ padding: 0.5rem 0.75rem;
64
+ font-size: 0.8125rem;
65
+ color: rgb(100 116 139);
66
+ background: transparent;
67
+ border: 0;
68
+ border-bottom: 2px solid transparent;
69
+ cursor: pointer;
70
+ }
71
+ .cv-subtab--active {
72
+ color: inherit;
73
+ font-weight: 500;
74
+ border-bottom-color: rgb(37 99 235);
75
+ }
76
+ .cv-pill {
77
+ display: inline-flex;
78
+ align-items: center;
79
+ gap: 0.25rem;
80
+ font-size: 0.6875rem;
81
+ font-weight: 500;
82
+ line-height: 1;
83
+ padding: 0.25rem 0.5rem;
84
+ border-radius: 9999px;
85
+ background: rgb(241 245 249);
86
+ color: rgb(71 85 105);
87
+ }
88
+ .cv-pill--success {
89
+ background: rgb(220 252 231);
90
+ color: rgb(21 128 61);
91
+ }
92
+ .cv-pill--warning {
93
+ background: rgb(254 243 199);
94
+ color: rgb(146 64 14);
95
+ }
96
+ .cv-pill--danger {
97
+ background: rgb(255 237 213);
98
+ color: rgb(194 65 12);
99
+ }
100
+ .dark .cv-pill {
101
+ background: rgb(51 65 85);
102
+ color: rgb(203 213 225);
103
+ }
104
+ .dark .cv-pill--success {
105
+ background: rgb(20 83 45);
106
+ color: rgb(187 247 208);
107
+ }
108
+ .dark .cv-pill--warning {
109
+ background: rgb(120 53 15);
110
+ color: rgb(253 230 138);
111
+ }
112
+ .dark .cv-pill--danger {
113
+ background: rgb(124 45 18);
114
+ color: rgb(254 215 170);
115
+ }
116
+ .cv-row:hover {
117
+ background: #f0f2f5;
118
+ }
119
+ .cv-row--active,
120
+ .cv-row--active:hover {
121
+ background: #f0f2f5;
122
+ }
123
+ .dark .cv-row:hover {
124
+ background: rgb(30 41 59);
125
+ }
126
+ .dark .cv-row--active,
127
+ .dark .cv-row--active:hover {
128
+ background: rgb(30 41 59);
129
+ }
130
+ .cv-back {
131
+ border: 1px solid rgb(203 213 225);
132
+ border-radius: 0.5rem;
133
+ padding: 0.25rem 0.5rem;
134
+ background: transparent;
135
+ cursor: pointer;
136
+ line-height: 1;
137
+ }
138
+ @media (min-width: 1024px) {
139
+ .cv-back {
140
+ display: none;
141
+ }
142
+ }
143
+ .dark .cv-back {
144
+ border-color: rgb(71 85 105);
145
+ color: rgb(226 232 240);
146
+ }
147
+ .cv-only-wide {
148
+ display: none;
149
+ }
150
+ @media (min-width: 1024px) {
151
+ .cv-only-wide {
152
+ display: inline;
153
+ }
154
+ }
155
+ .cv-only-narrow {
156
+ display: inline-flex;
157
+ }
158
+ @media (min-width: 1024px) {
159
+ .cv-only-narrow {
160
+ display: none;
161
+ }
162
+ }
163
+ .cv-menu {
164
+ position: relative;
165
+ }
166
+ .cv-menu-panel {
167
+ position: absolute;
168
+ right: 0;
169
+ top: calc(100% + 0.25rem);
170
+ z-index: 20;
171
+ min-width: 12rem;
172
+ display: flex;
173
+ flex-direction: column;
174
+ gap: 0.25rem;
175
+ padding: 0.375rem;
176
+ border: 1px solid rgb(203 213 225);
177
+ border-radius: 0.5rem;
178
+ background: #fff;
179
+ box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1);
180
+ }
181
+ .dark .cv-menu-panel {
182
+ background: rgb(30 41 59);
183
+ border-color: rgb(71 85 105);
184
+ }
185
+ .cv-menu-panel .cv-header-action {
186
+ text-align: left;
187
+ }
188
+ @media (max-width: 1023px) {
189
+ .cv-touch {
190
+ min-width: 44px;
191
+ min-height: 44px;
192
+ }
193
+ .cv-back {
194
+ min-width: 44px;
195
+ min-height: 44px;
196
+ }
197
+ .cv-menu-panel .cv-touch {
198
+ min-height: 44px;
199
+ }
200
+ }
201
+ @media (max-width: 1023px) {
202
+ .cv-menu-panel {
203
+ position: fixed;
204
+ right: 0.75rem;
205
+ left: auto;
206
+ top: auto;
207
+ max-width: calc(100vw - 1.5rem);
208
+ }
209
+ }
210
+ .cv-scrollbar-thin {
211
+ scrollbar-width: thin;
212
+ scrollbar-color: rgba(0, 0, 0, 0.15) transparent;
213
+ }
214
+ .cv-scrollbar-thin::-webkit-scrollbar {
215
+ width: 4px;
216
+ height: 4px;
217
+ }
218
+ .cv-scrollbar-thin::-webkit-scrollbar-track {
219
+ background: transparent;
220
+ }
221
+ .cv-scrollbar-thin::-webkit-scrollbar-thumb {
222
+ background: rgba(0, 0, 0, 0.15);
223
+ border-radius: 2px;
224
+ }
225
+ .cv-scrollbar-thin::-webkit-scrollbar-thumb:hover {
226
+ background: rgba(0, 0, 0, 0.3);
227
+ }
228
+ .dark .cv-scrollbar-thin {
229
+ scrollbar-color: rgba(255, 255, 255, 0.15) transparent;
230
+ }
231
+ .dark .cv-scrollbar-thin::-webkit-scrollbar-thumb {
232
+ background: rgba(255, 255, 255, 0.15);
233
+ }
234
+ .dark .cv-scrollbar-thin::-webkit-scrollbar-thumb:hover {
235
+ background: rgba(255, 255, 255, 0.3);
236
+ }
237
+ :where(button:not(:disabled), [role=button]:not(:disabled), summary) {
238
+ cursor: pointer;
239
+ }
@@ -0,0 +1,456 @@
1
+ import * as react from 'react';
2
+
3
+ interface ConversationsUIConfig {
4
+ apiBaseUrl: string;
5
+ theme?: ConversationsTheme;
6
+ features?: ConversationsFeatures;
7
+ }
8
+ interface ConversationsTheme {
9
+ primaryColor?: string;
10
+ backgroundColor?: string;
11
+ bubbleSent?: string;
12
+ bubbleReceived?: string;
13
+ textPrimary?: string;
14
+ textSecondary?: string;
15
+ }
16
+ interface ConversationsFeatures {
17
+ audio?: boolean;
18
+ documents?: boolean;
19
+ emoji?: boolean;
20
+ darkMode?: boolean;
21
+ }
22
+ /**
23
+ * Recorte do bloco `interactive` da Meta que a UI precisa para desenhar o menu. Fica solto (e não
24
+ * espelhando o contrato inteiro) porque o que chega do banco é o payload cru já enviado ao
25
+ * WhatsApp: qualquer campo que a UI não conheça é ignorado, nunca causa erro de render.
26
+ */
27
+ interface InteractiveOption {
28
+ id: string;
29
+ title: string;
30
+ description?: string;
31
+ }
32
+ interface InteractiveSection {
33
+ title?: string;
34
+ rows?: InteractiveOption[];
35
+ }
36
+ interface InteractivePayload {
37
+ type?: 'button' | 'list' | string;
38
+ header?: {
39
+ text?: string;
40
+ };
41
+ body?: {
42
+ text?: string;
43
+ };
44
+ footer?: {
45
+ text?: string;
46
+ };
47
+ action?: {
48
+ /** Rótulo do botão que abre a lista — só existe em `type: 'list'`. */
49
+ button?: string;
50
+ sections?: InteractiveSection[];
51
+ buttons?: {
52
+ reply?: InteractiveOption;
53
+ }[];
54
+ };
55
+ }
56
+ /** Como o cliente respondeu a um menu: por botão ou por item de lista. */
57
+ type InteractiveSelection = {
58
+ readonly kind: 'button' | 'list';
59
+ readonly option: InteractiveOption;
60
+ };
61
+ interface MessagePayload {
62
+ id: string;
63
+ type: 'text' | 'image' | 'video' | 'audio' | 'document' | 'sticker' | 'template' | 'interactive';
64
+ /** Payload cru da mensagem. Em `type: 'interactive'`, carrega o menu que o cliente vê. */
65
+ payload?: InteractivePayload | null;
66
+ content?: string;
67
+ caption?: string;
68
+ mediaUrl?: string;
69
+ base64?: string;
70
+ uploadId?: string;
71
+ mediaId?: string;
72
+ mimeType?: string;
73
+ filename?: string;
74
+ sizeBytes?: number;
75
+ direction: 'inbound' | 'outbound';
76
+ sender: 'bot' | 'customer' | 'agent';
77
+ timestamp: string;
78
+ status?: 'sent' | 'delivered' | 'read' | 'failed';
79
+ readAt?: string;
80
+ agentName?: string | null;
81
+ templateName?: string;
82
+ /**
83
+ * Veredito de moderação vindo do backend — a UI só exibe, nunca calcula. Dicionário no browser
84
+ * seria peso morto e daria veredito diferente por versão de cliente.
85
+ *
86
+ * `null`/ausente = não avaliado (moderação desligada, ou mensagem anterior ao recurso), que é
87
+ * diferente de avaliado e limpo.
88
+ */
89
+ moderation?: {
90
+ isOffensive: boolean;
91
+ terms: string[];
92
+ } | null;
93
+ /**
94
+ * Transcrição do áudio, vinda do backend — a UI só exibe, nunca transcreve. Rodar STT no browser
95
+ * exigiria baixar modelo por aba e daria resultado diferente por versão de cliente.
96
+ *
97
+ * `null`/ausente = não avaliado, que é diferente de `'done'` com texto vazio (áudio em silêncio,
98
+ * já processado). É essa distinção que decide se o balão oferece "transcrever" ou "sem fala
99
+ * detectada".
100
+ */
101
+ transcription?: MessageTranscription | null;
102
+ isFirstInGroup?: boolean;
103
+ isLastInGroup?: boolean;
104
+ }
105
+ type TranscriptionStatus = 'pending' | 'done' | 'failed' | 'unsupported';
106
+ /**
107
+ * Quando transcrever, escolhido nas configurações da empresa. Espelha o
108
+ * `TranscriptionMode` de `@adatechnology/meta-whatsapp-contracts`; declarado aqui para o pacote de
109
+ * UI não obrigar quem só desenha telas a instalar os contratos do backend.
110
+ */
111
+ type TranscriptionMode = 'auto' | 'onDemand';
112
+ interface MessageTranscription {
113
+ status: TranscriptionStatus;
114
+ text?: string | null;
115
+ /** ISO 639-1 ou nome do idioma, conforme o engine. Exibido como dica, não interpretado. */
116
+ language?: string | null;
117
+ engine?: string | null;
118
+ }
119
+
120
+ type ResolveMediaUrl = (message: MessagePayload) => Promise<string | null>;
121
+ interface MediaRendererProps {
122
+ message: MessagePayload;
123
+ onLightbox: (src: string) => void;
124
+ onResolveUrl?: ResolveMediaUrl;
125
+ /**
126
+ * Pede ao backend a transcrição do áudio desta mensagem. Ausente, o bloco de transcrição só exibe
127
+ * o que já veio pronto — sem oferecer um botão que o host não sabe atender.
128
+ *
129
+ * O que devolver é exibido na hora, sem esperar refetch da lista.
130
+ */
131
+ onTranscribeAudio?: () => Promise<MessageTranscription | void>;
132
+ /** Aplicado no wrapper de cada tipo de mídia — imagem, vídeo, áudio e documento. */
133
+ className?: string;
134
+ }
135
+ declare function MediaRenderer({ message, onLightbox, onResolveUrl, onTranscribeAudio, className, }: MediaRendererProps): react.JSX.Element | null;
136
+
137
+ /**
138
+ * Gravação de áudio no simulador, pelo microfone do próprio navegador.
139
+ *
140
+ * Existe porque áudio é o formato que mais chega de cliente real e o que mais quebra fluxo: sem
141
+ * poder gravar aqui, testar o caminho de transcrição exigia mandar mensagem do celular de alguém.
142
+ *
143
+ * O arquivo gravado sai daqui como `File` e segue exatamente o mesmo caminho de um anexo — quem
144
+ * hospeda e devolve o `mediaId` é o host, via `uploadMedia`.
145
+ */
146
+ interface AudioRecorderButtonLabels {
147
+ start: string;
148
+ stop: string;
149
+ unsupported: string;
150
+ denied: string;
151
+ review: string;
152
+ send: string;
153
+ discard: string;
154
+ empty: string;
155
+ }
156
+ declare const DEFAULT_AUDIO_RECORDER_BUTTON_LABELS: AudioRecorderButtonLabels;
157
+ interface AudioRecorderButtonProps {
158
+ onRecorded: (file: File) => void | Promise<void>;
159
+ onFailure?: (message: string) => void;
160
+ /**
161
+ * Avisa quando a gravação começa e termina. O botão é um interruptor — o segundo toque é que
162
+ * envia — e sem um aviso fora dele o operador grava, não vê nada acontecer e desiste achando
163
+ * que o microfone está quebrado.
164
+ */
165
+ onRecordingChange?: (isRecording: boolean) => void;
166
+ /**
167
+ * Abre uma etapa de revisão quando a gravação para: o áudio toca ali mesmo e só sai depois de
168
+ * confirmado. Ligado por padrão — voz é o único anexo que quem envia não viu antes de mandar, e
169
+ * sem ouvir não há como saber se o microfone captou alguma coisa. Desligar volta ao envio direto.
170
+ */
171
+ reviewBeforeSend?: boolean;
172
+ /**
173
+ * Teto de duração da gravação, em milissegundos. Passado o tempo, o gravador para e envia o que
174
+ * tem. Produto com limite próprio sobrescreve.
175
+ */
176
+ maxDurationMilliseconds?: number;
177
+ labels?: Partial<AudioRecorderButtonLabels>;
178
+ disabled?: boolean;
179
+ }
180
+ /**
181
+ * Cinco minutos: com o codec de voz do WhatsApp isso dá menos de 3MB, folgado dentro do teto de
182
+ * 16MB que a Meta impõe a áudio, e é mais do que qualquer recado de cliente. O corte automático
183
+ * existe porque gravação esquecida aberta só se descobre no envio, com o arquivo inteiro perdido.
184
+ */
185
+ declare const DEFAULT_MAX_RECORDING_MILLISECONDS: number;
186
+ declare function AudioRecorderButton({ onRecorded, onFailure, onRecordingChange, reviewBeforeSend, maxDurationMilliseconds, labels, disabled, }: AudioRecorderButtonProps): react.JSX.Element;
187
+
188
+ /**
189
+ * Canal de origem da conversa e o que cada um permite.
190
+ *
191
+ * Existe porque as regras que a inbox precisa respeitar não são do WhatsApp, são **de cada canal**:
192
+ * a janela de sessão, o jeito de reabrir a conversa e o formato do identificador do contato mudam
193
+ * entre WhatsApp, Messenger, Instagram e chat de site. Tratar a regra do WhatsApp como universal
194
+ * faria a UI bloquear o composer num chat de site, onde janela nenhuma existe.
195
+ *
196
+ * `whatsapp` é o padrão em todo lugar: instalações que ainda não informam canal continuam
197
+ * funcionando exatamente como antes.
198
+ */
199
+ declare const CONVERSATION_CHANNEL: {
200
+ readonly WHATSAPP: "whatsapp";
201
+ readonly MESSENGER: "messenger";
202
+ readonly INSTAGRAM: "instagram";
203
+ readonly WEBCHAT: "webchat";
204
+ };
205
+ type ConversationChannel = (typeof CONVERSATION_CHANNEL)[keyof typeof CONVERSATION_CHANNEL];
206
+ declare const DEFAULT_CONVERSATION_CHANNEL: ConversationChannel;
207
+ /** Como o canal reabre uma conversa fora da janela de sessão. */
208
+ declare const REOPEN_MECHANISM: {
209
+ readonly TEMPLATE: "template";
210
+ readonly TAG: "tag";
211
+ readonly NONE: "none";
212
+ };
213
+ type ReopenMechanism = (typeof REOPEN_MECHANISM)[keyof typeof REOPEN_MECHANISM];
214
+ /** Natureza do identificador do contato — decide como exibi-lo. */
215
+ declare const HANDLE_KIND: {
216
+ readonly PHONE: "phone";
217
+ readonly USERNAME: "username";
218
+ readonly SESSION: "session";
219
+ };
220
+ type HandleKind = (typeof HANDLE_KIND)[keyof typeof HANDLE_KIND];
221
+ type ChannelCapabilities = {
222
+ readonly label: string;
223
+ readonly icon: string;
224
+ readonly hasSessionWindow: boolean;
225
+ readonly windowHours: number;
226
+ readonly reopenMechanism: ReopenMechanism;
227
+ readonly handleKind: HandleKind;
228
+ };
229
+ declare const CHANNEL_CAPABILITIES: Readonly<Record<ConversationChannel, ChannelCapabilities>>;
230
+ declare function capabilitiesOf(channel: ConversationChannel | undefined): ChannelCapabilities;
231
+ declare const CHANNEL_FILTER_ALL = "all";
232
+ type ChannelFilter = ConversationChannel | typeof CHANNEL_FILTER_ALL;
233
+ type ChannelFilterOption = {
234
+ readonly value: ChannelFilter;
235
+ readonly label: string;
236
+ };
237
+ /**
238
+ * Opções derivadas do que existe na lista, não do catálogo inteiro: oferecer Instagram numa conta
239
+ * que só tem WhatsApp promete um recorte que nunca traz resultado.
240
+ *
241
+ * Devolve vazio com menos de dois canais — um filtro de opção única não filtra nada, e a barra só
242
+ * ocuparia espaço. O host usa isso para esconder a seção.
243
+ */
244
+ declare function channelFiltersFor(conversations: readonly {
245
+ readonly channel?: ConversationChannel | undefined;
246
+ }[]): ChannelFilterOption[];
247
+ type FormatContactHandleParams = {
248
+ readonly handle: string;
249
+ readonly channel?: ConversationChannel | undefined;
250
+ };
251
+ /**
252
+ * Exibição do identificador conforme a natureza dele. Formatar tudo como telefone — o que a UI
253
+ * fazia — transforma um `@perfil` do Instagram em dígitos sem sentido.
254
+ */
255
+ declare function formatContactHandle(params: FormatContactHandleParams): string;
256
+ /** Bandeira só faz sentido quando o identificador é telefone. */
257
+ declare function contactFlag(params: FormatContactHandleParams): string;
258
+
259
+ interface ListConversationsParams {
260
+ page?: number;
261
+ limit?: number;
262
+ waitingHuman?: boolean;
263
+ search?: string;
264
+ /**
265
+ * Recortes que só o produto conhece (tipo de financiamento, carteira, campanha) repassados
266
+ * crus ao backend dele. É o que evita o vocabulário de uma vertical virar campo fixo aqui:
267
+ * o pacote transporta o filtro sem saber o que ele significa.
268
+ */
269
+ filters?: Record<string, string | undefined>;
270
+ }
271
+ /**
272
+ * Página com o total, para a UI conseguir desenhar controles de paginação.
273
+ *
274
+ * `fetchConversations` devolve isto **ou** o array puro de antes: implementações existentes
275
+ * continuam válidas sem mudar uma linha, e quem precisa paginar passa a ter o total. Sem a união
276
+ * seria impossível saber se um retorno curto é a última página ou uma página cheia por acaso.
277
+ */
278
+ interface ConversationPage {
279
+ conversations: ConversationSummary[];
280
+ total: number;
281
+ }
282
+ interface ListDocumentsParams {
283
+ search?: string;
284
+ page?: number;
285
+ /** Tamanho da página. Sem ele, `page` sozinho não define fatia nenhuma. */
286
+ limit?: number;
287
+ /** Origem do arquivo (`customer`, `agent`, `bot`…). O vocabulário é do host. */
288
+ source?: string;
289
+ sortDirection?: 'asc' | 'desc';
290
+ }
291
+ /** Arquivo na biblioteca da empresa: o mesmo da conversa, mais de qual conversa veio. */
292
+ interface CompanyDocument extends ConversationDocument {
293
+ conversationId: string;
294
+ }
295
+ interface CompanyDocumentPage {
296
+ documents: CompanyDocument[];
297
+ total: number;
298
+ }
299
+ interface ConversationDocumentPage {
300
+ documents: ConversationDocument[];
301
+ total: number;
302
+ }
303
+ /**
304
+ * Template disponível para envio a partir da inbox. Distinto do `WhatsAppTemplateSummary` de
305
+ * `settings/`, e de propósito: aquele serve ao formulário que **edita** template e carrega o que
306
+ * a edição precisa (`shortId`, `variableCount`); este serve a quem só vai **escolher um para
307
+ * enviar**, e pedir os campos de edição obrigaria todo host a produzi-los sem uso.
308
+ */
309
+ interface ConversationTemplate {
310
+ name: string;
311
+ language: string;
312
+ status: string;
313
+ category?: string;
314
+ bodyText?: string | null;
315
+ }
316
+ interface ConversationsApi {
317
+ fetchMessages(conversationId: string, params?: {
318
+ limit?: number;
319
+ before?: string;
320
+ }): Promise<MessagePayload[]>;
321
+ fetchConversations(params?: ListConversationsParams): Promise<ConversationSummary[] | ConversationPage>;
322
+ sendMessage(conversationId: string, text: string): Promise<MessagePayload>;
323
+ sendMedia(conversationId: string, data: {
324
+ base64: string;
325
+ mimeType: string;
326
+ filename: string;
327
+ caption?: string;
328
+ }): Promise<MessagePayload>;
329
+ /**
330
+ * `templateName` é opcional porque reabrir a janela é a operação, e escolher *qual* template a
331
+ * usa nem sempre é decisão da UI: backends que guardam um template padrão configurado só
332
+ * precisam do "reabra". Exigir o nome obrigaria toda inbox a listar templates antes de poder
333
+ * mandar o primeiro — e a listagem é `listTemplates?`, opcional.
334
+ */
335
+ sendTemplate(conversationId: string, data: {
336
+ templateName?: string;
337
+ languageCode?: string;
338
+ bodyParams?: string[];
339
+ }): Promise<void>;
340
+ markRead(conversationId: string): Promise<void>;
341
+ getContext(conversationId: string): Promise<Record<string, unknown>>;
342
+ getDocuments(conversationId: string, params?: ListDocumentsParams): Promise<ConversationDocument[] | ConversationDocumentPage>;
343
+ /**
344
+ * `disposition` decide entre abrir no navegador e baixar. É o backend que assina a URL e grava
345
+ * o `Content-Disposition` nela, então a escolha precisa viajar na chamada — depois de assinada
346
+ * não há como o cliente mudá-la. Ausente = o padrão do host.
347
+ */
348
+ getDocumentUrl(uploadId: string, disposition?: 'inline' | 'attachment'): Promise<string>;
349
+ /**
350
+ * Baixa vários arquivos num zip único.
351
+ *
352
+ * **Opcional por capacidade:** montar zip exige o host LER os bytes do storage, o que nem toda
353
+ * instalação faz — as que só assinam URL não conseguem. Ausente, o painel esconde a seleção em
354
+ * lote em vez de oferecer um botão que falha.
355
+ */
356
+ downloadDocumentsArchive?(conversationId: string, uploadIds: readonly string[]): Promise<Blob>;
357
+ /**
358
+ * Biblioteca de TODAS as conversas, para uma tela de Documentos fora do atendimento.
359
+ *
360
+ * Opcional por capacidade: host que só expõe anexo dentro da conversa não implementa, e o
361
+ * componente de biblioteca simplesmente não é usável — melhor que uma tela que sempre erra.
362
+ */
363
+ getAllDocuments?(params?: ListDocumentsParams): Promise<CompanyDocumentPage>;
364
+ getMediaProxyUrl(mediaId: string): Promise<{
365
+ mimeType: string;
366
+ data: string;
367
+ }>;
368
+ /**
369
+ * Operações de atendimento humano. **Opcionais por capacidade, não por descuido:** nem toda
370
+ * inbox tem fila humana — um canal só-bot, ou um chat de site sem operador, não sabe o que é
371
+ * assumir conversa. Quem não implementa não ganha o botão, em vez de ganhar um botão que
372
+ * estoura no clique. Os hooks devolvem `undefined` para a ação ausente, e é isso que a UI
373
+ * consulta para decidir se desenha a afordância.
374
+ */
375
+ takeover?(conversationId: string): Promise<void>;
376
+ release?(conversationId: string): Promise<void>;
377
+ /** Encerra o atendimento. Despedida, se houver, é decisão do host — o pacote não a inventa. */
378
+ finalize?(conversationId: string): Promise<void>;
379
+ markAllRead?(): Promise<void>;
380
+ listTemplates?(): Promise<ConversationTemplate[]>;
381
+ /**
382
+ * Transcrição completa gerada pelo servidor. Existe ao lado de `buildTranscriptText`, que monta
383
+ * a partir das mensagens já em memória: a tela costuma ter só a última página carregada, e
384
+ * exportar dali entregaria um recorte parcial com cara de histórico inteiro. Opcional porque
385
+ * nem todo backend expõe a rota — quem não tem continua usando o builder local.
386
+ */
387
+ exportTranscript?(conversationId: string): Promise<{
388
+ transcript: string;
389
+ filename: string;
390
+ }>;
391
+ /**
392
+ * Transcreve o áudio de uma mensagem e devolve o resultado.
393
+ *
394
+ * **Opcional por capacidade.** Um host em modo automático transcreve na ingestão e não expõe rota
395
+ * nenhuma; um host sem engine configurado não transcreve de jeito algum. Nos dois casos o balão
396
+ * simplesmente não desenha o botão, em vez de oferecer uma ação que estoura no clique.
397
+ *
398
+ * `messageId` e não `conversationId`: transcrição é por áudio, e uma conversa tem vários.
399
+ */
400
+ transcribeAudio?(messageId: string): Promise<MessageTranscription>;
401
+ }
402
+ /**
403
+ * Superfície mínima de stream que o pacote consome — exatamente o que `useConversationRealtime`
404
+ * usa: assinar 'message', desassinar e fechar. Deliberadamente estrutural em vez de
405
+ * `EventSource`: sem servidor HTTP não existe `EventSource`, e é isso que impediria alimentar a
406
+ * inbox com dados mockados em desenvolvimento. Um `EventSource` nativo satisfaz este tipo, então
407
+ * quem já implementa `SSEProvider` continua válido sem mudança.
408
+ */
409
+ interface ConversationEventSource {
410
+ addEventListener(type: string, listener: (event: MessageEvent) => void): void;
411
+ removeEventListener(type: string, listener: (event: MessageEvent) => void): void;
412
+ close(): void;
413
+ }
414
+ interface SSEProvider {
415
+ connectConversationStream(conversationId: string): ConversationEventSource;
416
+ connectGlobalStream(): ConversationEventSource;
417
+ }
418
+ interface ConversationSummary {
419
+ id: string;
420
+ /**
421
+ * @deprecated Use `contactId` com `channel`. Mantido obrigatório para não quebrar quem já
422
+ * consome; some quando o segundo canal entrar em produção.
423
+ */
424
+ whatsappNumber: string;
425
+ /** Identificador neutro do contato. Ausente = usa `whatsappNumber`. */
426
+ contactId?: string;
427
+ /** Ausente = `whatsapp`, o comportamento de antes desta mudança. */
428
+ channel?: ConversationChannel;
429
+ clientName?: string;
430
+ lastContent?: string;
431
+ lastDirection?: 'inbound' | 'outbound';
432
+ lastAt: string;
433
+ lastInboundAt: string | null;
434
+ mode: 'bot' | 'human';
435
+ assignedUserId: string | null;
436
+ waitingHuman: boolean;
437
+ unread: number;
438
+ currentState: string;
439
+ /**
440
+ * Atributos que só o produto conhece e desenha (tipo de financiamento, carteira, campanha). É a
441
+ * contraparte de leitura do `filters` de `ListConversationsParams`: o pacote transporta e nunca
442
+ * interpreta. Sem isto, exibir um selo próprio na linha exigiria o host manter uma segunda
443
+ * consulta paralela à mesma listagem — a implementação duplicada que o pacote existe para evitar.
444
+ */
445
+ attributes?: Record<string, string | undefined>;
446
+ }
447
+ interface ConversationDocument {
448
+ id: string;
449
+ filename: string;
450
+ mimeType: string;
451
+ sizeBytes: number;
452
+ source: string;
453
+ linkedAt: string;
454
+ }
455
+
456
+ export { AudioRecorderButton as A, type ListDocumentsParams as B, CHANNEL_CAPABILITIES as C, DEFAULT_AUDIO_RECORDER_BUTTON_LABELS as D, type MediaRendererProps as E, type FormatContactHandleParams as F, type MessagePayload as G, HANDLE_KIND as H, type InteractiveOption as I, type MessageTranscription as J, type ReopenMechanism as K, type ListConversationsParams as L, MediaRenderer as M, type ResolveMediaUrl as N, type TranscriptionStatus as O, capabilitiesOf as P, channelFiltersFor as Q, REOPEN_MECHANISM as R, type SSEProvider as S, type TranscriptionMode as T, contactFlag as U, formatContactHandle as V, type AudioRecorderButtonLabels as a, type AudioRecorderButtonProps as b, CHANNEL_FILTER_ALL as c, CONVERSATION_CHANNEL as d, type ChannelCapabilities as e, type ChannelFilter as f, type ChannelFilterOption as g, type CompanyDocument as h, type CompanyDocumentPage as i, type ConversationChannel as j, type ConversationDocument as k, type ConversationDocumentPage as l, type ConversationEventSource as m, type ConversationPage as n, type ConversationSummary as o, type ConversationTemplate as p, type ConversationsApi as q, type ConversationsFeatures as r, type ConversationsTheme as s, type ConversationsUIConfig as t, DEFAULT_CONVERSATION_CHANNEL as u, DEFAULT_MAX_RECORDING_MILLISECONDS as v, type HandleKind as w, type InteractivePayload as x, type InteractiveSection as y, type InteractiveSelection as z };