@adatechnology/conversations-ui 0.1.0-rc.4 → 0.1.0-rc.41

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 (149) hide show
  1. package/dist/ConversationSimulatorPanel--5fIzXWY.d.ts +804 -0
  2. package/dist/chunk-BJNRLLDO.js +2708 -0
  3. package/dist/chunk-DKPXKQGC.js +110 -0
  4. package/dist/{chunk-OGRRHQQW.js → chunk-WCBDXZ3X.js} +68 -4
  5. package/dist/flows/index.d.ts +422 -5
  6. package/dist/flows/index.js +2389 -678
  7. package/dist/index.d.ts +1171 -42
  8. package/dist/index.js +3755 -843
  9. package/dist/preview/index.d.ts +157 -42
  10. package/dist/preview/index.js +772 -191
  11. package/dist/styles.css +893 -0
  12. package/package.json +9 -8
  13. package/src/AudioPlayer.tsx +8 -0
  14. package/src/AudioRecorderButton.test.tsx +30 -0
  15. package/src/AudioRecorderButton.tsx +248 -0
  16. package/src/AudioTranscription.test.tsx +115 -0
  17. package/src/AudioTranscription.tsx +252 -0
  18. package/src/Avatar.tsx +14 -3
  19. package/src/ConversationContextPanel.tsx +218 -44
  20. package/src/ConversationDocumentsPanel.tsx +347 -24
  21. package/src/ConversationHeader.test.tsx +66 -0
  22. package/src/ConversationHeader.tsx +163 -45
  23. package/src/ConversationListItem.tsx +19 -2
  24. package/src/ConversationLocalesProvider.tsx +28 -0
  25. package/src/ConversationRow.tsx +31 -7
  26. package/src/DarkModeToggle.test.tsx +76 -0
  27. package/src/DarkModeToggle.tsx +92 -0
  28. package/src/DocumentsLibrary.tsx +382 -0
  29. package/src/EmojiPicker.tsx +70 -55
  30. package/src/FileIcon.test.ts +83 -0
  31. package/src/FileIcon.tsx +88 -11
  32. package/src/InteractiveMessage.test.tsx +41 -0
  33. package/src/InteractiveMessage.tsx +146 -0
  34. package/src/Lightbox.tsx +18 -3
  35. package/src/MediaRenderer.tsx +92 -16
  36. package/src/MessageBubble.test.tsx +41 -0
  37. package/src/MessageBubble.tsx +75 -6
  38. package/src/MessageComposer.test.tsx +35 -0
  39. package/src/MessageComposer.tsx +155 -19
  40. package/src/RichMessageComposer.test.tsx +113 -0
  41. package/src/RichMessageComposer.tsx +551 -0
  42. package/src/SimpleEmojiPicker.tsx +5 -3
  43. package/src/StatusTicks.tsx +1 -1
  44. package/src/Toast.tsx +4 -0
  45. package/src/Tooltip.test.ts +42 -0
  46. package/src/Tooltip.tsx +167 -0
  47. package/src/Wallpaper.test.tsx +21 -0
  48. package/src/Wallpaper.tsx +67 -7
  49. package/src/WhatsAppMessageEditor.tsx +34 -7
  50. package/src/WindowExpiredNotice.tsx +12 -4
  51. package/src/audioRecorderFormat.test.ts +67 -0
  52. package/src/buildOutput.test.ts +79 -0
  53. package/src/composer.constant.ts +33 -0
  54. package/src/conversationTranscript.test.ts +57 -0
  55. package/src/conversationTranscript.ts +29 -4
  56. package/src/conversationWindow.ts +7 -5
  57. package/src/documentTypeLabel.test.ts +57 -0
  58. package/src/documents/DocumentsWorkspace.tsx +550 -0
  59. package/src/documents/index.ts +8 -0
  60. package/src/documents/labels.ts +92 -0
  61. package/src/emojiCatalog.test.ts +35 -0
  62. package/src/emojiCatalog.ts +189 -0
  63. package/src/flows/FlowConnectionEdge.tsx +104 -0
  64. package/src/flows/FlowGroupHeader.tsx +12 -2
  65. package/src/flows/FlowLegend.tsx +125 -0
  66. package/src/flows/FlowMapCanvas.tsx +15 -12
  67. package/src/flows/FlowMapNode.tsx +4 -1
  68. package/src/flows/FlowNodeCard.tsx +219 -34
  69. package/src/flows/FlowNodePanel.tsx +153 -39
  70. package/src/flows/FlowPalette.tsx +106 -69
  71. package/src/flows/FlowPortalNode.tsx +1 -1
  72. package/src/flows/FlowWhatsAppPreview.tsx +14 -3
  73. package/src/flows/FlowsWorkspace.tsx +1219 -0
  74. package/src/flows/flowCanvasModel.test.ts +456 -0
  75. package/src/flows/flowCanvasModel.ts +378 -0
  76. package/src/flows/flowEditorOps.test.ts +276 -0
  77. package/src/flows/flowEditorOps.ts +202 -0
  78. package/src/flows/flowGraph.ts +78 -53
  79. package/src/flows/index.ts +51 -2
  80. package/src/flows/labels.ts +180 -0
  81. package/src/flows/workspaceContract.test.ts +126 -0
  82. package/src/hooks/useContainerWidth.ts +35 -0
  83. package/src/hooks/useConversationActions.ts +56 -0
  84. package/src/hooks/useConversationDocuments.ts +11 -7
  85. package/src/hooks/useConversationList.ts +15 -9
  86. package/src/hooks/useConversationMessages.ts +2 -2
  87. package/src/hooks/useConversationRealtime.ts +10 -8
  88. package/src/hooks/useScrollToLatestMessage.ts +127 -0
  89. package/src/hooks/useUrlFilterState.ts +107 -0
  90. package/src/icon.constant.ts +12 -0
  91. package/src/index.ts +129 -13
  92. package/src/lib/cn.test.ts +29 -0
  93. package/src/lib/composer-formatting.test.ts +78 -0
  94. package/src/lib/composer-formatting.ts +145 -0
  95. package/src/lib/createMediaUrlResolver.ts +33 -0
  96. package/src/lib/paginated.test.ts +33 -0
  97. package/src/lib/paginated.ts +26 -0
  98. package/src/lib/whatsapp-formatting.test.tsx +37 -0
  99. package/src/lib/whatsapp-formatting.tsx +28 -3
  100. package/src/listing/index.tsx +202 -0
  101. package/src/pagination.constant.ts +10 -0
  102. package/src/preview/ConversationPreview.tsx +225 -17
  103. package/src/preview/ConversationSimulatorClient.ts +143 -0
  104. package/src/preview/ConversationSimulatorPanel.test.tsx +55 -0
  105. package/src/preview/ConversationSimulatorPanel.tsx +131 -0
  106. package/src/preview/MediaTypesPreview.tsx +87 -0
  107. package/src/preview/conversationPreviewFailures.test.ts +64 -0
  108. package/src/preview/createMockConversationsApi.ts +175 -15
  109. package/src/preview/createPreviewBridgeClient.test.ts +92 -0
  110. package/src/preview/createPreviewBridgeClient.ts +124 -0
  111. package/src/preview/createPreviewMediaUploader.ts +82 -0
  112. package/src/preview/createPreviewWebhookClient.test.ts +96 -0
  113. package/src/preview/createPreviewWebhookClient.ts +127 -4
  114. package/src/preview/index.ts +51 -3
  115. package/src/preview/mediaTypeOf.test.ts +15 -0
  116. package/src/preview/mockDocumentsSearch.test.ts +57 -0
  117. package/src/preview/preview.test.ts +5 -3
  118. package/src/preview/previewFileSamples.test.ts +151 -0
  119. package/src/preview/previewFileSamples.ts +74 -0
  120. package/src/preview/previewFixtures.ts +288 -1
  121. package/src/preview/previewMediaSource.test.ts +62 -0
  122. package/src/preview/previewMediaSource.ts +91 -0
  123. package/src/preview/previewMediaUploader.test.ts +61 -0
  124. package/src/providers/ConversationsProvider.tsx +8 -6
  125. package/src/providers/types.ts +185 -10
  126. package/src/quickReply.test.ts +58 -0
  127. package/src/settings/MessagesWorkspace.tsx +571 -0
  128. package/src/settings/TopicsForm.tsx +2 -0
  129. package/src/settings/TranscriptionSettingsForm.test.tsx +81 -0
  130. package/src/settings/TranscriptionSettingsForm.tsx +190 -0
  131. package/src/settings/WelcomeFarewellForm.tsx +1 -0
  132. package/src/settings/WhatsAppCreateTemplateForm.tsx +4 -1
  133. package/src/settings/WhatsAppTemplateSettingsForm.tsx +5 -2
  134. package/src/settings/WhatsAppTemplatesSettings.test.tsx +61 -0
  135. package/src/settings/WhatsAppTemplatesSettings.tsx +22 -2
  136. package/src/styles.css +858 -0
  137. package/src/types.ts +64 -1
  138. package/src/useWaitingNotifications.ts +74 -29
  139. package/src/workspace/BulkTemplateModal.tsx +132 -0
  140. package/src/workspace/ConversationPane.tsx +432 -0
  141. package/src/workspace/ConversationsInboxList.tsx +194 -0
  142. package/src/workspace/ConversationsWorkspace.tsx +423 -0
  143. package/src/workspace/index.ts +17 -0
  144. package/src/workspace/labels.test.ts +17 -0
  145. package/src/workspace/labels.ts +85 -0
  146. package/src/workspace/useConversationsInbox.ts +332 -0
  147. package/dist/chunk-4R6Y43DQ.js +0 -726
  148. package/dist/chunk-NV2RZ5KT.js +0 -56
  149. package/dist/types-C0PtaO7S.d.ts +0 -207
@@ -1,10 +1,23 @@
1
1
  import {
2
+ AudioRecorderButton,
3
+ ConversationDocumentsPanel,
4
+ ConversationPreview,
5
+ ConversationSimulatorPanel,
2
6
  ConversationWallpaper,
3
- DateDivider,
7
+ ConversationsProvider,
8
+ DEFAULT_AUDIO_RECORDER_BUTTON_LABELS,
9
+ DEFAULT_CONVERSATION_SIMULATOR_PANEL_LABELS,
10
+ DocumentsLibrary,
4
11
  MessageBubble,
5
- MessageComposer
6
- } from "../chunk-4R6Y43DQ.js";
7
- import "../chunk-OGRRHQQW.js";
12
+ SIMULATOR_FILE_MEDIA_KINDS,
13
+ acceptsMediaKind,
14
+ isConversationSimulatorClient,
15
+ mediaKindOf,
16
+ mediaTypeOf,
17
+ simulatorPanelLabelsOf,
18
+ toConversationSimulatorClient
19
+ } from "../chunk-BJNRLLDO.js";
20
+ import "../chunk-WCBDXZ3X.js";
8
21
 
9
22
  // src/preview/previewStore.ts
10
23
  var GLOBAL_CHANNEL = "global";
@@ -135,108 +148,6 @@ function createMockEventSource() {
135
148
  };
136
149
  }
137
150
 
138
- // src/preview/createMockConversationsApi.ts
139
- var PREVIEW_IMAGE_BASE64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYGD4DwABBAEAX+XyEgAAAABJRU5ErkJggg==";
140
- var DEFAULT_LATENCY_MS = 120;
141
- function createMockConversationsApi(params) {
142
- const latencyMs = params.latencyMs ?? DEFAULT_LATENCY_MS;
143
- async function withLatency(produce) {
144
- await new Promise((resolve) => setTimeout(resolve, latencyMs));
145
- return produce();
146
- }
147
- return {
148
- fetchConversations(fetchParams) {
149
- return withLatency(() => {
150
- const conversations = params.store.listConversations({
151
- waitingHuman: fetchParams?.waitingHuman,
152
- search: fetchParams?.search
153
- });
154
- const limit = fetchParams?.limit ?? conversations.length;
155
- const page = fetchParams?.page ?? 1;
156
- return conversations.slice((page - 1) * limit, page * limit);
157
- });
158
- },
159
- fetchMessages(conversationId, fetchParams) {
160
- return withLatency(() => {
161
- const messages = params.store.listMessages(conversationId);
162
- const limit = fetchParams?.limit;
163
- return limit ? messages.slice(-limit) : messages;
164
- });
165
- },
166
- sendMessage(conversationId, text) {
167
- return withLatency(
168
- () => params.store.appendMessage({ conversationId, content: text, direction: "outbound", sender: "agent" })
169
- );
170
- },
171
- sendMedia(conversationId, data) {
172
- return withLatency(
173
- () => params.store.appendMessage({
174
- conversationId,
175
- content: data.caption ?? data.filename,
176
- direction: "outbound",
177
- sender: "agent"
178
- })
179
- );
180
- },
181
- sendTemplate(conversationId, data) {
182
- return withLatency(() => {
183
- params.store.appendMessage({
184
- conversationId,
185
- content: `[template] ${data.templateName}`,
186
- direction: "outbound",
187
- sender: "agent"
188
- });
189
- });
190
- },
191
- markRead(conversationId) {
192
- return withLatency(() => params.store.markRead(conversationId));
193
- },
194
- getContext(conversationId) {
195
- return withLatency(() => {
196
- const conversation = params.store.listConversations().find((item) => item.id === conversationId);
197
- return {
198
- currentState: conversation?.currentState ?? "unknown",
199
- mode: conversation?.mode ?? "bot",
200
- preview: true
201
- };
202
- });
203
- },
204
- getDocuments() {
205
- return withLatency(() => []);
206
- },
207
- getDocumentUrl() {
208
- return withLatency(() => `data:image/png;base64,${PREVIEW_IMAGE_BASE64}`);
209
- },
210
- getMediaProxyUrl() {
211
- return withLatency(() => ({ mimeType: "image/png", data: PREVIEW_IMAGE_BASE64 }));
212
- }
213
- };
214
- }
215
-
216
- // src/preview/createMockSSEProvider.ts
217
- function createMockSSEProvider(params) {
218
- function connect(channel) {
219
- const source = createMockEventSource();
220
- const unsubscribe = params.store.subscribe(channel, (emission) => {
221
- source.emit(emission.event, emission.payload);
222
- });
223
- const close = source.close.bind(source);
224
- source.close = () => {
225
- unsubscribe();
226
- close();
227
- };
228
- return source;
229
- }
230
- return {
231
- connectConversationStream(conversationId) {
232
- return connect(conversationChannel(conversationId));
233
- },
234
- connectGlobalStream() {
235
- return connect(GLOBAL_CHANNEL);
236
- }
237
- };
238
- }
239
-
240
151
  // src/preview/previewFixtures.ts
241
152
  var BASE_DAY = "2026-07-26";
242
153
  function at(time) {
@@ -297,6 +208,24 @@ var PREVIEW_CONVERSATIONS = [
297
208
  waitingHuman: false,
298
209
  unread: 1,
299
210
  currentState: "list_import"
211
+ },
212
+ {
213
+ // Cobre TODO tipo que o composer aceita (DEFAULT_ACCEPTED_FILE_TYPES: image/*, video/*,
214
+ // audio/*, .pdf, .doc, .docx, .xls, .xlsx, .zip) mais sticker. Existe para que cada ramo do
215
+ // MediaRenderer e cada ícone/cor do FileIcon apareçam em algum lugar — ramo sem fixture é ramo
216
+ // que ninguém olha até quebrar em produção.
217
+ id: "5511944443333",
218
+ whatsappNumber: "5511944443333",
219
+ clientName: "Rita Documentos",
220
+ lastContent: "segue a planilha do pedido",
221
+ lastDirection: "inbound",
222
+ lastAt: at("15:10:00"),
223
+ lastInboundAt: at("15:10:00"),
224
+ mode: "human",
225
+ assignedUserId: "agent-1",
226
+ waitingHuman: false,
227
+ unread: 3,
228
+ currentState: "human_handling"
300
229
  }
301
230
  ];
302
231
  var PREVIEW_MESSAGES = {
@@ -375,104 +304,596 @@ var PREVIEW_MESSAGES = {
375
304
  sender: "customer",
376
305
  timestamp: at("13:31:00")
377
306
  }
307
+ ],
308
+ // Um tipo por mensagem, na ordem em que o MediaRenderer os trata.
309
+ "5511944443333": [
310
+ {
311
+ id: "fixture-doc-image",
312
+ type: "image",
313
+ mediaId: "preview-image-1",
314
+ mimeType: "image/png",
315
+ caption: "foto da prateleira",
316
+ direction: "inbound",
317
+ sender: "customer",
318
+ timestamp: at("15:00:00")
319
+ },
320
+ {
321
+ id: "fixture-doc-video",
322
+ type: "video",
323
+ mediaId: "preview-video-1",
324
+ mimeType: "video/mp4",
325
+ direction: "inbound",
326
+ sender: "customer",
327
+ timestamp: at("15:01:00")
328
+ },
329
+ {
330
+ id: "fixture-doc-audio",
331
+ type: "audio",
332
+ mediaId: "preview-audio-2",
333
+ mimeType: "audio/ogg",
334
+ direction: "inbound",
335
+ sender: "customer",
336
+ timestamp: at("15:02:00")
337
+ },
338
+ {
339
+ id: "fixture-doc-sticker",
340
+ type: "sticker",
341
+ mediaId: "preview-sticker-1",
342
+ mimeType: "image/webp",
343
+ direction: "inbound",
344
+ sender: "customer",
345
+ timestamp: at("15:03:00")
346
+ },
347
+ // Os cinco ramos do FileIcon: pdf, doc, xls, zip e o genérico do fallback.
348
+ {
349
+ id: "fixture-doc-pdf",
350
+ type: "document",
351
+ uploadId: "preview/documentos/nota-fiscal.pdf",
352
+ filename: "nota-fiscal.pdf",
353
+ mimeType: "application/pdf",
354
+ sizeBytes: 184320,
355
+ direction: "inbound",
356
+ sender: "customer",
357
+ timestamp: at("15:04:00")
358
+ },
359
+ {
360
+ id: "fixture-doc-docx",
361
+ type: "document",
362
+ uploadId: "preview/documentos/contrato.docx",
363
+ filename: "contrato.docx",
364
+ mimeType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
365
+ sizeBytes: 42112,
366
+ direction: "inbound",
367
+ sender: "customer",
368
+ timestamp: at("15:05:00")
369
+ },
370
+ {
371
+ id: "fixture-doc-doc",
372
+ type: "document",
373
+ uploadId: "preview/documentos/procuracao.doc",
374
+ filename: "procuracao.doc",
375
+ mimeType: "application/msword",
376
+ sizeBytes: 31744,
377
+ direction: "inbound",
378
+ sender: "customer",
379
+ timestamp: at("15:06:00")
380
+ },
381
+ {
382
+ id: "fixture-doc-xlsx",
383
+ type: "document",
384
+ uploadId: "preview/documentos/pedido.xlsx",
385
+ filename: "pedido.xlsx",
386
+ mimeType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
387
+ sizeBytes: 15872,
388
+ direction: "inbound",
389
+ sender: "customer",
390
+ timestamp: at("15:07:00")
391
+ },
392
+ {
393
+ id: "fixture-doc-xls",
394
+ type: "document",
395
+ uploadId: "preview/documentos/tabela-antiga.xls",
396
+ filename: "tabela-antiga.xls",
397
+ mimeType: "application/vnd.ms-excel",
398
+ sizeBytes: 9216,
399
+ direction: "inbound",
400
+ sender: "customer",
401
+ timestamp: at("15:08:00")
402
+ },
403
+ {
404
+ id: "fixture-doc-zip",
405
+ type: "document",
406
+ uploadId: "preview/documentos/comprovantes.zip",
407
+ filename: "comprovantes.zip",
408
+ mimeType: "application/zip",
409
+ sizeBytes: 2355200,
410
+ direction: "inbound",
411
+ sender: "customer",
412
+ timestamp: at("15:09:00")
413
+ },
414
+ {
415
+ id: "fixture-doc-txt",
416
+ type: "document",
417
+ uploadId: "preview/documentos/lista-compras.txt",
418
+ filename: "lista-compras.txt",
419
+ mimeType: "text/plain",
420
+ sizeBytes: 1024,
421
+ direction: "inbound",
422
+ sender: "customer",
423
+ timestamp: at("15:10:00")
424
+ },
425
+ {
426
+ id: "fixture-doc-csv",
427
+ type: "document",
428
+ uploadId: "preview/documentos/planilha-do-pedido.csv",
429
+ filename: "planilha-do-pedido.csv",
430
+ mimeType: "text/csv",
431
+ sizeBytes: 2048,
432
+ direction: "inbound",
433
+ sender: "customer",
434
+ timestamp: at("15:11:00")
435
+ },
436
+ {
437
+ id: "fixture-doc-pptx",
438
+ type: "document",
439
+ uploadId: "preview/documentos/apresentacao.pptx",
440
+ filename: "apresentacao.pptx",
441
+ mimeType: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
442
+ sizeBytes: 512e3,
443
+ direction: "inbound",
444
+ sender: "customer",
445
+ timestamp: at("15:12:00")
446
+ },
447
+ {
448
+ id: "fixture-doc-ppt",
449
+ type: "document",
450
+ uploadId: "preview/documentos/antiga.ppt",
451
+ filename: "antiga.ppt",
452
+ mimeType: "application/vnd.ms-powerpoint",
453
+ sizeBytes: 348160,
454
+ direction: "inbound",
455
+ sender: "customer",
456
+ timestamp: at("15:13:00")
457
+ },
458
+ // Mídia enviada COMO arquivo: aí a Meta manda o nome, então é onde a extensão de música e de
459
+ // vídeo aparece na lista. Mandada como `image`/`audio`/`video` (acima e abaixo) ela chega sem
460
+ // nome nenhum, e o rótulo vira o id da mídia — como em produção.
461
+ {
462
+ id: "fixture-doc-mp3",
463
+ type: "document",
464
+ uploadId: "preview/documentos/musica-do-anuncio.mp3",
465
+ filename: "musica-do-anuncio.mp3",
466
+ mimeType: "audio/mpeg",
467
+ sizeBytes: 3145728,
468
+ direction: "inbound",
469
+ sender: "customer",
470
+ timestamp: at("15:14:00")
471
+ },
472
+ {
473
+ id: "fixture-doc-m4a",
474
+ type: "document",
475
+ uploadId: "preview/documentos/recado.m4a",
476
+ filename: "recado.m4a",
477
+ mimeType: "audio/mp4",
478
+ sizeBytes: 486400,
479
+ direction: "inbound",
480
+ sender: "customer",
481
+ timestamp: at("15:15:00")
482
+ },
483
+ {
484
+ id: "fixture-doc-mp4-file",
485
+ type: "document",
486
+ uploadId: "preview/documentos/video-do-produto.mp4",
487
+ filename: "video-do-produto.mp4",
488
+ mimeType: "video/mp4",
489
+ sizeBytes: 8388608,
490
+ direction: "inbound",
491
+ sender: "customer",
492
+ timestamp: at("15:16:00")
493
+ },
494
+ {
495
+ id: "fixture-doc-jpg",
496
+ type: "document",
497
+ uploadId: "preview/documentos/foto-do-recibo.jpg",
498
+ filename: "foto-do-recibo.jpg",
499
+ mimeType: "image/jpeg",
500
+ sizeBytes: 262144,
501
+ direction: "inbound",
502
+ sender: "customer",
503
+ timestamp: at("15:17:00")
504
+ },
505
+ // As espécies restantes que a Meta aceita, sem nome de arquivo — fecham a lista de tipos
506
+ // aceitos e exercitam o fallback de ícone pela família do mimeType.
507
+ {
508
+ id: "fixture-media-jpeg",
509
+ type: "image",
510
+ mediaId: "preview-image-jpeg",
511
+ mimeType: "image/jpeg",
512
+ sizeBytes: 138240,
513
+ direction: "inbound",
514
+ sender: "customer",
515
+ timestamp: at("15:18:00")
516
+ },
517
+ {
518
+ id: "fixture-media-3gp",
519
+ type: "video",
520
+ mediaId: "preview-video-3gp",
521
+ mimeType: "video/3gp",
522
+ sizeBytes: 921600,
523
+ direction: "inbound",
524
+ sender: "customer",
525
+ timestamp: at("15:19:00")
526
+ },
527
+ {
528
+ id: "fixture-media-aac",
529
+ type: "audio",
530
+ mediaId: "preview-audio-aac",
531
+ mimeType: "audio/aac",
532
+ sizeBytes: 76800,
533
+ direction: "inbound",
534
+ sender: "customer",
535
+ timestamp: at("15:20:00")
536
+ },
537
+ {
538
+ id: "fixture-media-amr",
539
+ type: "audio",
540
+ mediaId: "preview-audio-amr",
541
+ mimeType: "audio/amr",
542
+ sizeBytes: 24576,
543
+ direction: "inbound",
544
+ sender: "customer",
545
+ timestamp: at("15:21:00")
546
+ }
378
547
  ]
379
548
  };
549
+ var LIBRARY_MEDIA_TYPES = /* @__PURE__ */ new Set(["document", "image", "video", "audio", "sticker"]);
550
+ var PREVIEW_DOCUMENTS = {
551
+ "5511944443333": (PREVIEW_MESSAGES["5511944443333"] ?? []).filter((message) => LIBRARY_MEDIA_TYPES.has(message.type)).map((message) => ({
552
+ // Mídia não tem `uploadId` na mensagem: o backend deriva a key do id da mídia na Meta, e o
553
+ // caminho abaixo imita esse formato para o download assinado receber algo do mesmo feitio.
554
+ id: message.uploadId ?? (message.mediaId ? `preview/inbound/${message.mediaId}` : message.id),
555
+ // Só documento chega nomeado. Para os outros o backend salva o id da mídia como rótulo —
556
+ // manter isso aqui é o que faz o ícone por família do mimeType ser exercitado de verdade.
557
+ filename: message.filename ?? message.mediaId ?? message.id,
558
+ mimeType: message.mimeType ?? "application/octet-stream",
559
+ sizeBytes: message.sizeBytes ?? 0,
560
+ source: message.sender,
561
+ linkedAt: message.timestamp
562
+ }))
563
+ };
380
564
 
381
- // src/preview/ConversationPreview.tsx
382
- import { useCallback, useEffect, useMemo, useRef, useState } from "react";
383
- import { jsx, jsxs } from "react/jsx-runtime";
384
- var GROUPING_WINDOW_MS = 5 * 60 * 1e3;
385
- function decorate(messages) {
386
- return messages.map((message, index) => {
387
- const previous = index > 0 ? messages[index - 1] : void 0;
388
- const currentTime = new Date(message.timestamp).getTime();
389
- const previousTime = previous ? new Date(previous.timestamp).getTime() : 0;
390
- return {
391
- message,
392
- isFirstInGroup: !previous || previous.sender !== message.sender || currentTime - previousTime > GROUPING_WINDOW_MS,
393
- showDateDivider: !previous || new Date(message.timestamp).toDateString() !== new Date(previous.timestamp).toDateString()
394
- };
395
- });
565
+ // src/preview/previewFileSamples.ts
566
+ var PREVIEW_FILE_SAMPLES = {
567
+ "application/pdf": "data:application/pdf;base64,JVBERi0xLjQKMSAwIG9iajw8L1R5cGUvQ2F0YWxvZy9QYWdlcyAyIDAgUj4+ZW5kb2JqCjIgMCBvYmo8PC9UeXBlL1BhZ2VzL0tpZHNbMyAwIFJdL0NvdW50IDE+PmVuZG9iagozIDAgb2JqPDwvVHlwZS9QYWdlL1BhcmVudCAyIDAgUi9NZWRpYUJveFswIDAgMjYwIDExMF0vQ29udGVudHMgNCAwIFIvUmVzb3VyY2VzPDwvRm9udDw8L0YxIDUgMCBSPj4+Pj4+ZW5kb2JqCjQgMCBvYmo8PC9MZW5ndGggNjI+PnN0cmVhbQpCVCAvRjEgMTMgVGYgMTggNTUgVGQgKERvY3VtZW50byBkZSBleGVtcGxvIGRvIHByZXZpZXcpIFRqIEVUCmVuZHN0cmVhbWVuZG9iago1IDAgb2JqPDwvVHlwZS9Gb250L1N1YnR5cGUvVHlwZTEvQmFzZUZvbnQvSGVsdmV0aWNhPj5lbmRvYmoKeHJlZgowIDYKMDAwMDAwMDAwMCA2NTUzNSBmIAowMDAwMDAwMDA5IDAwMDAwIG4gCjAwMDAwMDAwNTIgMDAwMDAgbiAKMDAwMDAwMDEwMSAwMDAwMCBuIAowMDAwMDAwMjExIDAwMDAwIG4gCjAwMDAwMDAzMTcgMDAwMDAgbiAKdHJhaWxlcjw8L1NpemUgNi9Sb290IDEgMCBSPj4Kc3RhcnR4cmVmCjM3OAolJUVPRgo=",
568
+ "image/png": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAIAAADYYG7QAAAACXBIWXMAAAABAAAAAQBPJcTWAAAATElEQVR4nO3OMRHAIAAAMbhDWo2hCYPdu/xIh0RB5rPP+JN1O/AlVISKUBEqQkWoCBWhIlSEilARKkJFqAgVoSJUhIpQESpCRagIlRcgDwI25HmPQAAAAABJRU5ErkJggg==",
569
+ "image/jpeg": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAgAAAQABAAD//gAQTGF2YzYyLjExLjEwMAD/2wBDAAgEBAQEBAUFBQUFBQYGBgYGBgYGBgYGBgYHBwcICAgHBwcGBgcHCAgICAkJCQgICAgJCQoKCgwMCwsODg4RERT/xABMAAEBAAAAAAAAAAAAAAAAAAAABAEBAQAAAAAAAAAAAAAAAAAAAAYQAQAAAAAAAAAAAAAAAAAAAAARAQAAAAAAAAAAAAAAAAAAAAD/wAARCAAwADADASIAAhEAAxEA/9oADAMBAAIRAxEAPwCUBRpwAAAAAAAAAAAAAAAB/9k=",
570
+ "image/webp": "data:image/webp;base64,UklGRhoAAABXRUJQVlA4TA0AAAAvAAAAEAcQERGIiP4HAA==",
571
+ "video/mp4": "data:video/mp4;base64,AAAAIGZ0eXBpc29tAAACAGlzb21pc28yYXZjMW1wNDEAAANdbW9vdgAAAGxtdmhkAAAAAAAAAAAAAAAAAAAD6AAAASwAAQAAAQAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAoh0cmFrAAAAXHRraGQAAAADAAAAAAAAAAAAAAABAAAAAAAAASwAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAABAAAAAAEAAAABAAAAAAAAkZWR0cwAAABxlbHN0AAAAAAAAAAEAAAEsAAAIAAABAAAAAAIAbWRpYQAAACBtZGhkAAAAAAAAAAAAAAAAAAAoAAAAEABVxAAAAAAALWhkbHIAAAAAAAAAAHZpZGUAAAAAAAAAAAAAAABWaWRlb0hhbmRsZXIAAAABq21pbmYAAAAUdm1oZAAAAAEAAAAAAAAAAAAAACRkaW5mAAAAHGRyZWYAAAAAAAAAAQAAAAx1cmwgAAAAAQAAAWtzdGJsAAAAv3N0c2QAAAAAAAAAAQAAAK9hdmMxAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAEAAQABIAAAASAAAAAAAAAABFUxhdmM2Mi4xMS4xMDAgbGlieDI2NAAAAAAAAAAAAAAAGP//AAAANWF2Y0MBZAAK/+EAGGdkAAqs2UQmwEQAAAMABAAAAwBQPEiWWAEABmjr48siwP34+AAAAAAQcGFzcAAAAAEAAAABAAAAFGJ0cnQAAAAAAABO2gAAAAAAAAAYc3R0cwAAAAAAAAABAAAAAwAABAAAAAAUc3RzcwAAAAAAAAABAAAAAQAAAChjdHRzAAAAAAAAAAMAAAABAAAIAAAAAAEAAAwAAAAAAQAABAAAAAAcc3RzYwAAAAAAAAABAAAAAQAAAAMAAAABAAAAIHN0c3oAAAAAAAAAAAAAAAMAAALbAAAADgAAAAwAAAAUc3RjbwAAAAAAAAABAAADjQAAAGF1ZHRhAAAAWW1ldGEAAAAAAAAAIWhkbHIAAAAAAAAAAG1kaXJhcHBsAAAAAAAAAAAAAAAALGlsc3QAAAAkqXRvbwAAABxkYXRhAAAAAQAAAABMYXZmNjIuMy4xMDAAAAAIZnJlZQAAAv1tZGF0AAACrgYF//+q3EXpvebZSLeWLNgg2SPu73gyNjQgLSBjb3JlIDE2NSByMzIyMiBiMzU2MDVhIC0gSC4yNjQvTVBFRy00IEFWQyBjb2RlYyAtIENvcHlsZWZ0IDIwMDMtMjAyNSAtIGh0dHA6Ly93d3cudmlkZW9sYW4ub3JnL3gyNjQuaHRtbCAtIG9wdGlvbnM6IGNhYmFjPTEgcmVmPTMgZGVibG9jaz0xOjA6MCBhbmFseXNlPTB4MzoweDExMyBtZT1oZXggc3VibWU9NyBwc3k9MSBwc3lfcmQ9MS4wMDowLjAwIG1peGVkX3JlZj0xIG1lX3JhbmdlPTE2IGNocm9tYV9tZT0xIHRyZWxsaXM9MSA4eDhkY3Q9MSBjcW09MCBkZWFkem9uZT0yMSwxMSBmYXN0X3Bza2lwPTEgY2hyb21hX3FwX29mZnNldD0tMiB0aHJlYWRzPTIgbG9va2FoZWFkX3RocmVhZHM9MSBzbGljZWRfdGhyZWFkcz0wIG5yPTAgZGVjaW1hdGU9MSBpbnRlcmxhY2VkPTAgYmx1cmF5X2NvbXBhdD0wIGNvbnN0cmFpbmVkX2ludHJhPTAgYmZyYW1lcz0zIGJfcHlyYW1pZD0yIGJfYWRhcHQ9MSBiX2JpYXM9MCBkaXJlY3Q9MSB3ZWlnaHRiPTEgb3Blbl9nb3A9MCB3ZWlnaHRwPTIga2V5aW50PTI1MCBrZXlpbnRfbWluPTEwIHNjZW5lY3V0PTQwIGludHJhX3JlZnJlc2g9MCByY19sb29rYWhlYWQ9NDAgcmM9Y3JmIG1idHJlZT0xIGNyZj0yMy4wIHFjb21wPTAuNjAgcXBtaW49MCBxcG1heD02OSBxcHN0ZXA9NCBpcF9yYXRpbz0xLjQwIGFxPTE6MS4wMACAAAAAJWWIhAA///7mdfgUoKykhh+DdK6xVynnAWznTQ4fVSgMZ1FwY28AAAAKQZoibEN//qePiAAAAAgBnkF5Df8HfQ==",
572
+ "audio/mpeg": "data:audio/mpeg;base64,SUQzBAAAAAAAIlRTU0UAAAAOAAADTGF2ZjYyLjMuMTAwAAAAAAAAAAAAAAD/40jAAAAAAAAAAAAASW5mbwAAAA8AAAAFAAAGwABVVVVVVVVVVVVVVVVVVVVVVVVVgICAgICAgICAgICAgICAgICAgICqqqqqqqqqqqqqqqqqqqqqqqqqqtXV1dXV1dXV1dXV1dXV1dXV1dXV//////////////////////////8AAAAATGF2YzYyLjExAAAAAAAAAAAAAAAAJALAAAAAAAAABsDR4nNFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/40jEAAAAA0gAAAAATEFNRTMuMTAwVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/40jEAAAAA0gAAAAATEFNRTMuMTAwVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/40jEAAAAA0gAAAAATEFNRTMuMTAwVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/40jEAAAAA0gAAAAATEFNRTMuMTAwVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/40jEAAAAA0gAAAAATEFNRTMuMTAwVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVU=",
573
+ "audio/ogg": "data:audio/ogg;base64,T2dnUwACAAAAAAAAAABXDTmmAAAAAKMnDKEBE09wdXNIZWFkAQE4AYC7AAAAAABPZ2dTAAAAAAAAAAAAAFcNOaYBAAAAZlPuKwE9T3B1c1RhZ3MMAAAATGF2ZjYyLjMuMTAwAQAAAB0AAABlbmNvZGVyPUxhdmM2Mi4xMS4xMDAgbGlib3B1c09nZ1MABLgmAAAAAAAAVw05pgIAAAA3uL6lCw8REhISEhISEhISeAvkwTbsxY2MSUaZpCaweAfJcifhROpV8fDA1d3pUmB4B8l5yMlXwKISI/rvZ/NkwOB4B8l5yMlXwKISI/rvZ/NkwOB4B8l5yMlXwKISI/rvZ/NkwOB4B8l5yMlXwKISI/rvZ/NkwOB4B8l5yMlXwKISI/rvZ/NkwOB4B8l5yMlXwKISI/rvZ/NkwOB4B8l5yMlXwKISI/rvZ/NkwOB4B8l5yMlXwKISI/rvZ/NkwOB4B8l5yMlXwKISI/rvZ/NkwOA=",
574
+ "audio/mp4": "data:audio/mp4;base64,AAAAHGZ0eXBNNEEgAAACAE00QSBpc29taXNvMgAAAAhmcmVlAAAAQW1kYXTeAgBMYXZjNjIuMTEuMTAwAAIwQA4BGCAHARggBwEYIAcBGCAHARggBwEYIAcBGCAHARggBwEYIAcAAAMibW9vdgAAAGxtdmhkAAAAAAAAAAAAAAAAAAAD6AAAAMgAAQAAAQAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAk10cmFrAAAAXHRraGQAAAADAAAAAAAAAAAAAAABAAAAAAAAAMgAAAAAAAAAAAAAAAEBAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAkZWR0cwAAABxlbHN0AAAAAAAAAAEAAADIAAAEAAABAAAAAAHFbWRpYQAAACBtZGhkAAAAAAAAAAAAAAAAAACsRAAAJnRVxAAAAAAALWhkbHIAAAAAAAAAAHNvdW4AAAAAAAAAAAAAAABTb3VuZEhhbmRsZXIAAAABcG1pbmYAAAAQc21oZAAAAAAAAAAAAAAAJGRpbmYAAAAcZHJlZgAAAAAAAAABAAAADHVybCAAAAABAAABNHN0YmwAAABqc3RzZAAAAAAAAAABAAAAWm1wNGEAAAAAAAAAAQAAAAAAAAAAAAEAEAAAAACsRAAAAAAANmVzZHMAAAAAA4CAgCUAAQAEgICAF0AVAAAAAAB9AAAAB/oFgICABRIIVuUABoCAgAECAAAAIHN0dHMAAAAAAAAAAgAAAAkAAAQAAAAAAQAAAnQAAAAcc3RzYwAAAAAAAAABAAAAAQAAAAoAAAABAAAAPHN0c3oAAAAAAAAAAAAAAAoAAAAVAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAAFHN0Y28AAAAAAAAAAQAAACwAAAAac2dwZAEAAAByb2xsAAAAAgAAAAH//wAAABxzYmdwAAAAAHJvbGwAAAABAAAACgAAAAEAAABhdWR0YQAAAFltZXRhAAAAAAAAACFoZGxyAAAAAAAAAABtZGlyYXBwbAAAAAAAAAAAAAAAACxpbHN0AAAAJKl0b28AAAAcZGF0YQAAAAEAAAAATGF2ZjYyLjMuMTAw",
575
+ "audio/aac": "data:audio/aac;base64,//FQQAOf/N4CAExhdmM2Mi4xMS4xMDAAAjBADv/xUEABf/wBGCAH//FQQAF//AEYIAf/8VBAAX/8ARggB//xUEABf/wBGCAH//FQQAF//AEYIAf/8VBAAX/8ARggB//xUEABf/wBGCAH//FQQAF//AEYIAf/8VBAAX/8ARggBw==",
576
+ "text/plain": "data:text/plain;base64,TGlzdGEgZGUgY29tcHJhcwotIGFycm96IDJrZwotIG9sZW8gMXVuCg==",
577
+ "text/csv": "data:text/csv;base64,cHJvZHV0byxxdWFudGlkYWRlLHByZWNvCmFycm96LDIsMjQuOTAKb2xlbywxLDcuNTAK",
578
+ "application/zip": "data:application/zip;base64,UEsDBBQAAAAIANFO/FwtmGOxGwAAABkAAAARAAAAY29tcHJvdmFudGUtMS50eHRLzs8tKMovS8wrSVVISVUoSExPzE3NK8nnAgBQSwMEFAAAAAgA0U78XCftV8YWAAAAFAAAABEAAABjb21wcm92YW50ZS0yLnR4dCtOTS/NS8lXSM7PLSjKL0vMK0nlAgBQSwECFAMUAAAACADRTvxcLZhjsRsAAAAZAAAAEQAAAAAAAAAAAAAAgAEAAAAAY29tcHJvdmFudGUtMS50eHRQSwECFAMUAAAACADRTvxcJ+1XxhYAAAAUAAAAEQAAAAAAAAAAAAAAgAFKAAAAY29tcHJvdmFudGUtMi50eHRQSwUGAAAAAAIAAgB+AAAAjwAAAAAA",
579
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document": "data:application/vnd.openxmlformats-officedocument.wordprocessingml.document;base64,UEsDBBQAAAAIANFO/Fx5bjPX6AAAAK0BAAATAAAAW0NvbnRlbnRfVHlwZXNdLnhtbH1QyU7DMBD9FWuuKHHggBCK0wPLETiUDxjZk8SqN3nc0v49Tlt6QIXjzFv1+tXeO7GjzDYGBbdtB4KCjsaGScHn+rV5AMEFg0EXAyk4EMNq6NeHRCyqNrCCuZT0KCXrmTxyGxOFiowxeyz1zJNMqDc4kbzrunupYygUSlMWDxj6Zxpx64p42df3qUcmxyCeTsQlSwGm5KzGUnG5C+ZXSnNOaKvyyOHZJr6pBJBXExbk74Cz7r0Ok60h8YG5vKGvLPkVs5Em6q2vyvZ/mys94zhaTRf94pZy1MRcF/euvSAebfjpL49zD99QSwMEFAAAAAgA0U78XJv9N+qtAAAAKQEAAAsAAABfcmVscy8ucmVsc43POw7CMAwG4KtE3mlaBoRQ0y4IqSsqB7ASN61oHkrCo7cnAwNFDIy2f3+W6/ZpZnanECdnBVRFCYysdGqyWsClP232wGJCq3B2lgQsFKFt6jPNmPJKHCcfWTZsFDCm5A+cRzmSwVg4TzZPBhcMplwGzT3KK2ri27Lc8fBpwNpknRIQOlUB6xdP/9huGCZJRydvhmz6ceIrkWUMmpKAhwuKq3e7yCzwpuarF5sXUEsDBBQAAAAIANFO/FyraIuoqgAAAOcAAAARAAAAd29yZC9kb2N1bWVudC54bWxFjksOwjAMBa8SZU9TWCBUNWWBxAngAKExbaTGjpzQz+1JyoLNPFt+Grm9rn4SM3B0hFoeq1oKwJ6sw0HL5+N+uEgRk0FrJkLQcoMor127NJb6jwdMIgswNouWY0qhUSr2I3gTKwqA+fYm9ibllQe1ENvA1EOM2e8ndarrs/LGoSzKF9mtZCjggtTdCBObRMKCgBV8mPJIIjDMDpZWlU4h7ww7fx71/7H7AlBLAQIUAxQAAAAIANFO/Fx5bjPX6AAAAK0BAAATAAAAAAAAAAAAAACAAQAAAABbQ29udGVudF9UeXBlc10ueG1sUEsBAhQDFAAAAAgA0U78XJv9N+qtAAAAKQEAAAsAAAAAAAAAAAAAAIABGQEAAF9yZWxzLy5yZWxzUEsBAhQDFAAAAAgA0U78XKtoi6iqAAAA5wAAABEAAAAAAAAAAAAAAIAB7wEAAHdvcmQvZG9jdW1lbnQueG1sUEsFBgAAAAADAAMAuQAAAMgCAAAAAA==",
580
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": "data:application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;base64,UEsDBBQAAAAIANFO/FxuYbgN/gAAAC0CAAATAAAAW0NvbnRlbnRfVHlwZXNdLnhtbK2RzU7DMBCEX8XytYqdckAIJe2BnyNwKA+w2JvEiv/kdUv69jhp4YAKXDit7JnZb2Q328lZdsBEJviWr0XNGXoVtPF9y193j9UNZ5TBa7DBY8uPSHy7aXbHiMRK1lPLh5zjrZSkBnRAIkT0RelCcpDLMfUyghqhR3lV19dSBZ/R5yrPO/imuccO9jazh6lcn3oktMTZ3ck4s1oOMVqjIBddHrz+RqnOBFGSi4cGE2lVDFxeJMzKz4Bz7rk8TDIa2Quk/ASuuORk5XtI41sIo/h9yYWWoeuMQh3U3pWIoJgQNA2I2VmxTOHA+NXf/MVMchnrfy7ytf+zh1y+e/MBUEsDBBQAAAAIANFO/FyY2uuLrgAAACcBAAALAAAAX3JlbHMvLnJlbHONz8EOgjAMBuBXWXqXgQdjDIOLMeFq8AHmVgYB1mWbCm/vjmI8eGz69/vTsl7miT3Rh4GsgCLLgaFVpAdrBNzay+4ILERptZzIooAVA9RVecVJxnQS+sEFlgwbBPQxuhPnQfU4y5CRQ5s2HflZxjR6w51UozTI93l+4P7TgK3JGi3AN7oA1q4O/7Gp6waFZ1KPGW38UfGVSLL0BqOAZeIv8uOdaMwSCrwq+ebB6g1QSwMEFAAAAAgA0U78XEkKfby5AAAAGwEAAA8AAAB4bC93b3JrYm9vay54bWyNj7sOwjAMRX8l8g4pDAhVbVkQEhsDfEBoXBrR2JUdXn9PeO1Mfuke31ut7nEwVxQNTDXMpgUYpJZ9oFMNh/1msgSjyZF3AxPW8ECFVVPdWM5H5rPJctIa+pTG0lpte4xOpzwi5UvHEl3Ko5ysjoLOa4+Y4mDnRbGw0QWCD6GUfxjcdaHFNbeXiJQ+EMHBpWxe+zAqNNX7g36rIRez6R364DkHee22PucEI2XIjWz9DGxT2Z/M/pI1T1BLAwQUAAAACADRTvxcWv2Ca7EAAAAoAQAAGgAAAHhsL19yZWxzL3dvcmtib29rLnhtbC5yZWxzjc/JCsJADAbgVxlyt2k9iEinXkToVeoDDNN0oZ2Fybj07R08iAUPnkLyky+kPD7NLO4UeHRWQpHlIMhq1462l3Btzps9CI7Ktmp2liQsxHCsygvNKqYVHkbPIhmWJQwx+gMi64GM4sx5sinpXDAqpjb06JWeVE+4zfMdhm8D1qaoWwmhbgsQzeLpH9t13ajp5PTNkI0/TuDDhYkHophQFXqKEj4jxncpsqQCViWuPqxeUEsDBBQAAAAIANFO/FwcNOTnuQAAAP8AAAAYAAAAeGwvd29ya3NoZWV0cy9zaGVldDEueG1sTY5RTsMwDIavEvmdudsDmlCSaQhxAdgBotasEY1T2VYHO/3SgRAvlv1Zn/37w1eZ3EKiuXKA7aYDR9zXIfM5wOn99WEPTi3xkKbKFOCbFA7RX6p86khkrvmsAUaz+QlR+5FK0k2didvmo0pJ1kY5o85CabhLZcJd1z1iSZkh+jt7SZail3px0nI02q/NcQvOAmSeMtObSeNZo7eYROrVo0WPK8D+V3he1SXuPC4/FNvJVv/9wL/w8QZQSwECFAMUAAAACADRTvxcbmG4Df4AAAAtAgAAEwAAAAAAAAAAAAAAgAEAAAAAW0NvbnRlbnRfVHlwZXNdLnhtbFBLAQIUAxQAAAAIANFO/FyY2uuLrgAAACcBAAALAAAAAAAAAAAAAACAAS8BAABfcmVscy8ucmVsc1BLAQIUAxQAAAAIANFO/FxJCn28uQAAABsBAAAPAAAAAAAAAAAAAACAAQYCAAB4bC93b3JrYm9vay54bWxQSwECFAMUAAAACADRTvxcWv2Ca7EAAAAoAQAAGgAAAAAAAAAAAAAAgAHsAgAAeGwvX3JlbHMvd29ya2Jvb2sueG1sLnJlbHNQSwECFAMUAAAACADRTvxcHDTk57kAAAD/AAAAGAAAAAAAAAAAAAAAgAHVAwAAeGwvd29ya3NoZWV0cy9zaGVldDEueG1sUEsFBgAAAAAFAAUARQEAAMQEAAAAAA=="
581
+ };
582
+ var FAMILY_FALLBACK = {
583
+ image: "image/png",
584
+ video: "video/mp4",
585
+ audio: "audio/mpeg",
586
+ text: "text/plain"
587
+ };
588
+ function resolvePreviewFileSample(mimeType, filename) {
589
+ const normalized = (mimeType ?? "").split(";")[0].trim().toLowerCase();
590
+ const exact = PREVIEW_FILE_SAMPLES[normalized];
591
+ if (exact) return exact;
592
+ const family = FAMILY_FALLBACK[normalized.split("/")[0] ?? ""];
593
+ const byFamily = family ? PREVIEW_FILE_SAMPLES[family] : void 0;
594
+ if (byFamily) return byFamily;
595
+ const note = `Preview do SDK: nao existe amostra binaria valida para ${normalized || "tipo desconhecido"}${filename ? ` (${filename})` : ""}. Na tela real este arquivo vem do storage.`;
596
+ return `data:text/plain;charset=utf-8,${encodeURIComponent(note)}`;
396
597
  }
397
- function ConversationPreview({
398
- client,
399
- sse,
400
- conversationId,
401
- loadMessages,
402
- placeholder
403
- }) {
404
- const [messages, setMessages] = useState([]);
405
- const [failure, setFailure] = useState(void 0);
406
- const loadMessagesRef = useRef(loadMessages);
407
- const bottomRef = useRef(null);
408
- loadMessagesRef.current = loadMessages;
409
- const refresh = useCallback(async () => {
410
- try {
411
- setMessages(await loadMessagesRef.current(conversationId));
412
- } catch {
413
- setMessages([]);
598
+
599
+ // src/preview/previewMediaSource.ts
600
+ var blobUrlCache = /* @__PURE__ */ new Map();
601
+ function toBlob(dataUrl) {
602
+ const [head, payload] = dataUrl.split(",");
603
+ const mimeType = head.replace(/^data:/, "").replace(/;base64$/, "").replace(/;charset=.*$/, "");
604
+ if (!head.endsWith(";base64")) {
605
+ return new Blob([decodeURIComponent(payload)], { type: mimeType || "text/plain" });
606
+ }
607
+ const binary = atob(payload);
608
+ const bytes = new Uint8Array(binary.length);
609
+ for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index);
610
+ return new Blob([bytes], { type: mimeType });
611
+ }
612
+ function previewFileUrl(mimeType, filename) {
613
+ const dataUrl = resolvePreviewFileSample(mimeType, filename);
614
+ if (typeof URL === "undefined" || typeof URL.createObjectURL !== "function") return dataUrl;
615
+ const cached = blobUrlCache.get(dataUrl);
616
+ if (cached) return cached;
617
+ const blobUrl = URL.createObjectURL(toBlob(dataUrl));
618
+ blobUrlCache.set(dataUrl, blobUrl);
619
+ return blobUrl;
620
+ }
621
+ function previewFileBase64(mimeType, filename) {
622
+ const dataUrl = resolvePreviewFileSample(mimeType, filename);
623
+ const [head, payload] = dataUrl.split(",");
624
+ const declared = head.replace(/^data:/, "").replace(/;base64$/, "").replace(/;charset=.*$/, "");
625
+ if (head.endsWith(";base64")) return { mimeType: declared, data: payload };
626
+ return { mimeType: declared, data: btoa(decodeURIComponent(payload)) };
627
+ }
628
+ function createPreviewMediaResolver() {
629
+ const byId = /* @__PURE__ */ new Map();
630
+ for (const documents of Object.values(PREVIEW_DOCUMENTS)) {
631
+ for (const document of documents) {
632
+ byId.set(document.id, { mimeType: document.mimeType, filename: document.filename });
414
633
  }
415
- }, [conversationId]);
416
- useEffect(() => {
417
- void refresh();
418
- }, [refresh]);
419
- useEffect(() => {
420
- const source = sse.connectConversationStream(conversationId);
421
- const handler = () => {
422
- void refresh();
423
- };
424
- source.addEventListener("message", handler);
425
- return () => {
426
- source.removeEventListener("message", handler);
427
- source.close();
428
- };
429
- }, [sse, conversationId, refresh]);
430
- useEffect(() => {
431
- bottomRef.current?.scrollIntoView({ behavior: "smooth" });
432
- }, [messages]);
433
- const rendered = useMemo(() => decorate(messages), [messages]);
434
- async function handleSend(text) {
435
- setFailure(void 0);
436
- try {
437
- await client.sendText(text);
438
- await refresh();
439
- } catch (error) {
440
- setFailure(error instanceof Error ? error.message : "Falha ao entregar a mensagem no webhook.");
634
+ }
635
+ return async (message) => {
636
+ const reference = message.uploadId ?? (message.mediaId ? `preview/inbound/${message.mediaId}` : void 0);
637
+ if (!reference) return null;
638
+ const known = byId.get(reference);
639
+ return previewFileUrl(known?.mimeType ?? message.mimeType, known?.filename ?? message.filename);
640
+ };
641
+ }
642
+
643
+ // src/preview/createMockConversationsApi.ts
644
+ var DEFAULT_LATENCY_MS = 120;
645
+ var PREVIEW_AGENT_ID = "preview-agent";
646
+ var PREVIEW_TEMPLATES = [
647
+ { name: "retomada_atendimento", language: "pt_BR", status: "APPROVED", category: "UTILITY" },
648
+ { name: "lembrete_documentos", language: "pt_BR", status: "APPROVED", category: "UTILITY" },
649
+ { name: "promocao_taxa", language: "pt_BR", status: "PENDING", category: "MARKETING" }
650
+ ];
651
+ function createMockConversationsApi(params) {
652
+ const latencyMs = params.latencyMs ?? DEFAULT_LATENCY_MS;
653
+ async function withLatency(produce) {
654
+ await new Promise((resolve) => setTimeout(resolve, latencyMs));
655
+ return produce();
656
+ }
657
+ return {
658
+ // Devolve a forma paginada, não o array puro: é a que o contrato passou a oferecer e a que
659
+ // permite o preview desenhar controles de página. O total é contado ANTES do corte — depois
660
+ // dele seria sempre o tamanho da página, e a paginação nunca sairia da primeira.
661
+ fetchConversations(fetchParams) {
662
+ return withLatency(() => {
663
+ const conversations = params.store.listConversations({
664
+ waitingHuman: fetchParams?.waitingHuman,
665
+ search: fetchParams?.search
666
+ });
667
+ const limit = fetchParams?.limit ?? conversations.length;
668
+ const page = fetchParams?.page ?? 1;
669
+ return {
670
+ conversations: conversations.slice((page - 1) * limit, page * limit),
671
+ total: conversations.length
672
+ };
673
+ });
674
+ },
675
+ fetchMessages(conversationId, fetchParams) {
676
+ return withLatency(() => {
677
+ const messages = params.store.listMessages(conversationId);
678
+ const limit = fetchParams?.limit;
679
+ return limit ? messages.slice(-limit) : messages;
680
+ });
681
+ },
682
+ sendMessage(conversationId, text) {
683
+ return withLatency(
684
+ () => params.store.appendMessage({ conversationId, content: text, direction: "outbound", sender: "agent" })
685
+ );
686
+ },
687
+ sendMedia(conversationId, data) {
688
+ return withLatency(
689
+ () => params.store.appendMessage({
690
+ conversationId,
691
+ content: data.caption ?? data.filename,
692
+ direction: "outbound",
693
+ sender: "agent"
694
+ })
695
+ );
696
+ },
697
+ sendTemplate(conversationId, data) {
698
+ return withLatency(() => {
699
+ params.store.appendMessage({
700
+ conversationId,
701
+ // Sem nome, o host está pedindo o template padrão do backend — o mock representa isso
702
+ // pelo que o atendente veria, não por um nome inventado.
703
+ content: `[template] ${data.templateName ?? PREVIEW_TEMPLATES[0]?.name ?? "padrao"}`,
704
+ direction: "outbound",
705
+ sender: "agent"
706
+ });
707
+ });
708
+ },
709
+ markRead(conversationId) {
710
+ return withLatency(() => params.store.markRead(conversationId));
711
+ },
712
+ getContext(conversationId) {
713
+ return withLatency(() => {
714
+ const conversation = params.store.listConversations().find((item) => item.id === conversationId);
715
+ return {
716
+ currentState: conversation?.currentState ?? "unknown",
717
+ mode: conversation?.mode ?? "bot",
718
+ preview: true
719
+ };
720
+ });
721
+ },
722
+ /**
723
+ * Espelha o backend em busca, filtro de origem, ordenação E paginação. Mock que ignora params
724
+ * faz o painel parecer quebrado aqui e, pior, esconde o caso em que o backend também os ignora
725
+ * — foi exatamente assim que o filtro de origem passou a existir só no contrato.
726
+ */
727
+ getDocuments(conversationId, documentParams) {
728
+ return withLatency(() => {
729
+ let documents = [...PREVIEW_DOCUMENTS[conversationId] ?? []];
730
+ const search = documentParams?.search?.trim().toLowerCase();
731
+ if (search) {
732
+ documents = documents.filter((document) => document.filename.toLowerCase().includes(search));
733
+ }
734
+ const source = documentParams?.source;
735
+ if (source === "team") {
736
+ documents = documents.filter((document) => document.source === "agent" || document.source === "bot");
737
+ } else if (source) {
738
+ documents = documents.filter((document) => document.source === source);
739
+ }
740
+ documents.sort(
741
+ (left, right) => documentParams?.sortDirection === "asc" ? left.linkedAt.localeCompare(right.linkedAt) : right.linkedAt.localeCompare(left.linkedAt)
742
+ );
743
+ const total = documents.length;
744
+ const limit = documentParams?.limit ?? total;
745
+ const page = documentParams?.page ?? 1;
746
+ return { documents: documents.slice((page - 1) * limit, page * limit), total };
747
+ });
748
+ },
749
+ /**
750
+ * Zip de mentira: um texto listando o que entraria. Basta para exercitar seleção, botão e o
751
+ * caminho de download no preview, sem arrastar uma lib de compactação para o pacote.
752
+ */
753
+ downloadDocumentsArchive(conversationId, uploadIds) {
754
+ return withLatency(() => {
755
+ const known = PREVIEW_DOCUMENTS[conversationId] ?? [];
756
+ const names = uploadIds.map((id) => known.find((document) => document.id === id)?.filename ?? id);
757
+ return new Blob([`preview: ${names.length} arquivo(s)
758
+ ${names.join("\n")}`], { type: "application/zip" });
759
+ });
760
+ },
761
+ /** Junta as bibliotecas de todas as conversas do fixture, com a origem de cada arquivo. */
762
+ getAllDocuments(documentParams) {
763
+ return withLatency(() => {
764
+ let all = Object.entries(PREVIEW_DOCUMENTS).flatMap(
765
+ ([conversationId, docs]) => docs.map((document) => ({ ...document, conversationId }))
766
+ );
767
+ const search = documentParams?.search?.trim().toLowerCase();
768
+ if (search) {
769
+ const digits = search.replace(/\D/g, "");
770
+ all = all.filter(
771
+ (document) => document.filename.toLowerCase().includes(search) || digits !== "" && document.conversationId.includes(digits)
772
+ );
773
+ }
774
+ const source = documentParams?.source;
775
+ if (source === "team") {
776
+ all = all.filter((document) => document.source === "agent" || document.source === "bot");
777
+ } else if (source) {
778
+ all = all.filter((document) => document.source === source);
779
+ }
780
+ all.sort(
781
+ (left, right) => documentParams?.sortDirection === "asc" ? left.linkedAt.localeCompare(right.linkedAt) : right.linkedAt.localeCompare(left.linkedAt)
782
+ );
783
+ const total = all.length;
784
+ const limit = documentParams?.limit ?? total;
785
+ const page = documentParams?.page ?? 1;
786
+ return { documents: all.slice((page - 1) * limit, page * limit), total };
787
+ });
788
+ },
789
+ // Devolve os bytes DO TIPO do documento, não uma imagem para tudo: antes, abrir um PDF entregava
790
+ // um PNG rotulado `application/pdf` e o leitor recusava o arquivo. O `uploadId` é a única pista
791
+ // que o contrato dá, então o tipo vem da própria biblioteca.
792
+ getDocumentUrl(uploadId) {
793
+ return withLatency(() => {
794
+ const found = Object.values(PREVIEW_DOCUMENTS).flat().find((document) => document.id === uploadId);
795
+ return previewFileUrl(found?.mimeType, found?.filename);
796
+ });
797
+ },
798
+ // Caminho da mídia ainda não ingerida: o backend busca na Meta e devolve base64. Resolve pelo
799
+ // id para a bolha receber os bytes DO TIPO dela — devolvendo um PNG para todo id, vídeo e áudio
800
+ // apareciam quebrados na thread mesmo havendo amostra válida do formato.
801
+ getMediaProxyUrl(mediaId) {
802
+ return withLatency(() => {
803
+ const found = Object.values(PREVIEW_DOCUMENTS).flat().find((document) => document.id === `preview/inbound/${mediaId}`);
804
+ return previewFileBase64(found?.mimeType, found?.filename);
805
+ });
806
+ },
807
+ takeover(conversationId) {
808
+ return withLatency(
809
+ () => params.store.setMode({ conversationId, mode: "human", assignedUserId: PREVIEW_AGENT_ID })
810
+ );
811
+ },
812
+ release(conversationId) {
813
+ return withLatency(() => params.store.setMode({ conversationId, mode: "bot" }));
814
+ },
815
+ // Encerrar devolve ao bot como o release, e é de propósito: a diferença entre os dois é a
816
+ // despedida, que o host manda antes de chamar aqui. O mock não a inventa.
817
+ finalize(conversationId) {
818
+ return withLatency(() => params.store.setMode({ conversationId, mode: "bot" }));
819
+ },
820
+ markAllRead() {
821
+ return withLatency(() => {
822
+ for (const conversation of params.store.listConversations()) {
823
+ params.store.markRead(conversation.id);
824
+ }
825
+ });
826
+ },
827
+ listTemplates() {
828
+ return withLatency(() => [...PREVIEW_TEMPLATES]);
441
829
  }
830
+ };
831
+ }
832
+
833
+ // src/preview/createMockSSEProvider.ts
834
+ function createMockSSEProvider(params) {
835
+ function connect(channel) {
836
+ const source = createMockEventSource();
837
+ const unsubscribe = params.store.subscribe(channel, (emission) => {
838
+ source.emit(emission.event, emission.payload);
839
+ });
840
+ const close = source.close.bind(source);
841
+ source.close = () => {
842
+ unsubscribe();
843
+ close();
844
+ };
845
+ return source;
442
846
  }
443
- return /* @__PURE__ */ jsxs("div", { className: "flex h-full min-h-0 flex-col", children: [
444
- /* @__PURE__ */ jsxs(ConversationWallpaper, { className: "flex-1 min-h-0 overflow-y-auto px-4 py-3", children: [
445
- rendered.map(({ message, isFirstInGroup, showDateDivider }) => /* @__PURE__ */ jsxs("div", { children: [
446
- showDateDivider ? /* @__PURE__ */ jsx(DateDivider, { iso: message.timestamp }) : null,
447
- /* @__PURE__ */ jsx(
448
- MessageBubble,
449
- {
450
- message,
451
- isMine: message.direction === "inbound",
452
- isFirstInGroup
453
- }
454
- )
455
- ] }, message.id)),
456
- /* @__PURE__ */ jsx("div", { ref: bottomRef })
457
- ] }),
458
- failure ? /* @__PURE__ */ jsx("p", { role: "alert", className: "px-4 py-2 text-sm text-red-600 dark:text-red-400", children: failure }) : null,
459
- /* @__PURE__ */ jsx(
460
- MessageComposer,
461
- {
462
- onSend: (text) => void handleSend(text),
463
- placeholder: placeholder ?? "Escreva como o cliente\u2026"
464
- }
465
- )
466
- ] });
847
+ return {
848
+ connectConversationStream(conversationId) {
849
+ return connect(conversationChannel(conversationId));
850
+ },
851
+ connectGlobalStream() {
852
+ return connect(GLOBAL_CHANNEL);
853
+ }
854
+ };
467
855
  }
468
856
 
469
857
  // src/preview/createPreviewWebhookClient.ts
470
858
  import {
471
859
  buildInboundAudioPayload,
472
860
  buildInboundInteractivePayload,
861
+ buildInboundMediaPayload,
473
862
  buildInboundTextPayload,
474
863
  serializeWebhookPayload
475
864
  } from "@adatechnology/meta-whatsapp-contracts/testing";
865
+
866
+ // src/preview/createPreviewMediaUploader.ts
867
+ import { PREVIEW_MEDIA_ID_PREFIX } from "@adatechnology/meta-whatsapp-contracts";
868
+ import { toPreviewMediaId } from "@adatechnology/meta-whatsapp-contracts";
869
+ var CHUNK_SIZE = 8192;
870
+ async function fileToBase64(file) {
871
+ const bytes = new Uint8Array(await file.arrayBuffer());
872
+ let binary = "";
873
+ for (let offset = 0; offset < bytes.length; offset += CHUNK_SIZE) {
874
+ binary += String.fromCharCode(...bytes.subarray(offset, offset + CHUNK_SIZE));
875
+ }
876
+ return btoa(binary);
877
+ }
878
+ function createPreviewMediaUploader(params) {
879
+ const fallbackMimeType = params.fallbackMimeType ?? "audio/ogg";
880
+ const fallbackFilename = params.fallbackFilename ?? "audio.ogg";
881
+ return async function uploadPreviewMedia(file) {
882
+ const mimeType = file.type || fallbackMimeType;
883
+ const filename = file.name || fallbackFilename;
884
+ const { uploadId } = await params.upload({ base64: await fileToBase64(file), mimeType, filename });
885
+ return { mediaId: toPreviewMediaId(uploadId), mimeType, filename };
886
+ };
887
+ }
888
+
889
+ // src/preview/createPreviewWebhookClient.ts
890
+ var PreviewMediaUploadRejectedError = class extends Error {
891
+ constructor(status) {
892
+ super(`A rota de m\xEDdia do simulador recusou o upload (HTTP ${status}).`);
893
+ this.status = status;
894
+ this.name = "PreviewMediaUploadRejectedError";
895
+ }
896
+ };
476
897
  var PreviewInProductionError = class extends Error {
477
898
  constructor() {
478
899
  super("O preview de conversa carrega um app secret e n\xE3o pode ser montado em produ\xE7\xE3o.");
@@ -489,7 +910,7 @@ var PreviewWebhookRejectedError = class extends Error {
489
910
  function assertPreviewEnvironment(isProduction) {
490
911
  if (isProduction) throw new PreviewInProductionError();
491
912
  }
492
- async function signWithWebCrypto(params) {
913
+ async function signPreviewPayload(params) {
493
914
  const encoder = new TextEncoder();
494
915
  const key = await globalThis.crypto.subtle.importKey(
495
916
  "raw",
@@ -501,6 +922,31 @@ async function signWithWebCrypto(params) {
501
922
  const signature = await globalThis.crypto.subtle.sign("HMAC", key, encoder.encode(params.rawBody));
502
923
  return `sha256=${[...new Uint8Array(signature)].map((byte) => byte.toString(16).padStart(2, "0")).join("")}`;
503
924
  }
925
+ var signWithWebCrypto = signPreviewPayload;
926
+ var DEFAULT_MEDIA_UPLOAD_PATH = "/v1/preview/media";
927
+ function defaultMediaUploadUrl(webhookUrl) {
928
+ try {
929
+ return new URL(DEFAULT_MEDIA_UPLOAD_PATH, webhookUrl).toString();
930
+ } catch {
931
+ return DEFAULT_MEDIA_UPLOAD_PATH;
932
+ }
933
+ }
934
+ function createPreviewMediaPoster(params) {
935
+ return createPreviewMediaUploader({
936
+ upload: async (request) => {
937
+ const performRequest = params.fetchImplementation ?? fetch;
938
+ const extraHeaders = await params.headers?.(request.mimeType) ?? {};
939
+ const response = await performRequest(params.url, {
940
+ method: "POST",
941
+ headers: { "content-type": "application/json", ...extraHeaders },
942
+ body: JSON.stringify(request)
943
+ });
944
+ if (!response.ok) throw new PreviewMediaUploadRejectedError(response.status);
945
+ const body = await response.json();
946
+ return { uploadId: body.data.uploadId };
947
+ }
948
+ });
949
+ }
504
950
  function createPreviewWebhookClient(params) {
505
951
  const sendPayload = async (payload) => {
506
952
  const rawBody = serializeWebhookPayload(payload);
@@ -513,12 +959,70 @@ function createPreviewWebhookClient(params) {
513
959
  });
514
960
  if (!response.ok) throw new PreviewWebhookRejectedError(response.status);
515
961
  };
962
+ const uploadMedia = createPreviewMediaPoster({
963
+ url: params.mediaUploadUrl ?? defaultMediaUploadUrl(params.webhookUrl),
964
+ headers: async (mimeType) => ({
965
+ "x-preview-signature": await signWithWebCrypto({ rawBody: mimeType, appSecret: params.appSecret })
966
+ }),
967
+ ...params.fetchImplementation ? { fetchImplementation: params.fetchImplementation } : {}
968
+ });
516
969
  const envelope = { from: params.from, phoneNumberId: params.phoneNumberId };
517
970
  return {
518
971
  sendText: (text) => sendPayload(buildInboundTextPayload({ ...envelope, text })),
519
972
  sendButtonReply: (reply) => sendPayload(buildInboundInteractivePayload({ ...envelope, buttonReply: reply })),
520
973
  sendListReply: (reply) => sendPayload(buildInboundInteractivePayload({ ...envelope, listReply: reply })),
521
- sendAudio: (mediaId) => sendPayload(buildInboundAudioPayload({ ...envelope, mediaId }))
974
+ sendAudio: (mediaId) => sendPayload(buildInboundAudioPayload({ ...envelope, mediaId })),
975
+ sendMedia: (media) => sendPayload(buildInboundMediaPayload({ ...envelope, ...media })),
976
+ uploadMedia
977
+ };
978
+ }
979
+
980
+ // src/preview/createPreviewBridgeClient.ts
981
+ var PreviewBridgeRejectedError = class extends Error {
982
+ constructor(status) {
983
+ super(`A rota de preview do host recusou a entrega (HTTP ${status}).`);
984
+ this.status = status;
985
+ this.name = "PreviewBridgeRejectedError";
986
+ }
987
+ };
988
+ function buildFetchSender(params) {
989
+ const endpointUrl = params.endpointUrl;
990
+ if (!endpointUrl) {
991
+ throw new Error("createPreviewBridgeClient exige `sendCommand` ou `endpointUrl`.");
992
+ }
993
+ return async (command) => {
994
+ const performRequest = params.fetchImplementation ?? fetch;
995
+ const response = await performRequest(endpointUrl, {
996
+ method: "POST",
997
+ // `credentials` fica com o host via `headers`/`fetchImplementation`: sessão por cookie e por
998
+ // bearer não convivem numa escolha default sem quebrar um dos dois.
999
+ headers: { "content-type": "application/json", ...params.headers },
1000
+ body: JSON.stringify(command)
1001
+ });
1002
+ if (!response.ok) throw new PreviewBridgeRejectedError(response.status);
1003
+ };
1004
+ }
1005
+ function resolveBridgeUpload(params) {
1006
+ if (params.uploadMedia) return params.uploadMedia;
1007
+ const url = params.mediaUploadUrl ?? (params.endpointUrl ? defaultMediaUploadUrl(params.endpointUrl) : void 0);
1008
+ if (!url) return void 0;
1009
+ return createPreviewMediaPoster({
1010
+ url,
1011
+ ...params.headers ? { headers: async () => params.headers ?? {} } : {},
1012
+ ...params.fetchImplementation ? { fetchImplementation: params.fetchImplementation } : {}
1013
+ });
1014
+ }
1015
+ function createPreviewBridgeClient(params) {
1016
+ const send = params.sendCommand ?? buildFetchSender(params);
1017
+ const from = params.from;
1018
+ const uploadMedia = resolveBridgeUpload(params);
1019
+ return {
1020
+ sendText: (text) => send({ kind: "text", from, text }),
1021
+ sendButtonReply: (reply) => send({ kind: "buttonReply", from, reply }),
1022
+ sendListReply: (reply) => send({ kind: "listReply", from, reply }),
1023
+ sendAudio: (mediaId) => send({ kind: "audio", from, mediaId }),
1024
+ sendMedia: (media) => send({ kind: "media", from, ...media }),
1025
+ ...uploadMedia ? { uploadMedia } : {}
522
1026
  };
523
1027
  }
524
1028
 
@@ -557,20 +1061,97 @@ function startPreviewScript(params) {
557
1061
  }, intervalMs);
558
1062
  return () => clearInterval(timer);
559
1063
  }
1064
+
1065
+ // src/preview/MediaTypesPreview.tsx
1066
+ import { useMemo } from "react";
1067
+ import { jsx, jsxs } from "react/jsx-runtime";
1068
+ var MEDIA_TYPES_CONVERSATION_ID = "5511944443333";
1069
+ function MediaTypesPreview({
1070
+ conversationId = MEDIA_TYPES_CONVERSATION_ID,
1071
+ className
1072
+ }) {
1073
+ const store = useMemo(
1074
+ () => createPreviewStore({ conversations: PREVIEW_CONVERSATIONS, messages: PREVIEW_MESSAGES }),
1075
+ []
1076
+ );
1077
+ const api = useMemo(() => createMockConversationsApi({ store }), [store]);
1078
+ const sse = useMemo(() => createMockSSEProvider({ store }), [store]);
1079
+ const messages = PREVIEW_MESSAGES[conversationId] ?? [];
1080
+ const documents = PREVIEW_DOCUMENTS[conversationId] ?? [];
1081
+ const mimeTypes = [...new Set(documents.map((document) => document.mimeType))];
1082
+ return /* @__PURE__ */ jsx(ConversationsProvider, { api, sse, children: /* @__PURE__ */ jsxs("div", { className, children: [
1083
+ /* @__PURE__ */ jsxs("header", { className: "border-b px-4 py-3 dark:border-gray-700", children: [
1084
+ /* @__PURE__ */ jsx("h1", { className: "text-lg font-semibold", children: "Teste manual de m\xEDdia" }),
1085
+ /* @__PURE__ */ jsxs("p", { className: "text-sm text-gray-500", children: [
1086
+ documents.length,
1087
+ " arquivos, ",
1088
+ mimeTypes.length,
1089
+ " tipos. Clique no olho para abrir em aba nova e no bot\xE3o da bolha para carregar a m\xEDdia na thread \u2014 \xE9 o que teste automatizado n\xE3o v\xEA."
1090
+ ] })
1091
+ ] }),
1092
+ /* @__PURE__ */ jsxs("div", { className: "grid gap-4 p-4 lg:grid-cols-2", children: [
1093
+ /* @__PURE__ */ jsxs("section", { className: "space-y-3", children: [
1094
+ /* @__PURE__ */ jsx("h2", { className: "text-sm font-semibold uppercase tracking-wide text-gray-500", children: "Biblioteca da empresa" }),
1095
+ /* @__PURE__ */ jsx(DocumentsLibrary, { perPage: documents.length || 20 })
1096
+ ] }),
1097
+ /* @__PURE__ */ jsxs("section", { className: "space-y-3", children: [
1098
+ /* @__PURE__ */ jsx("h2", { className: "text-sm font-semibold uppercase tracking-wide text-gray-500", children: "Painel da conversa" }),
1099
+ /* @__PURE__ */ jsx(ConversationDocumentsPanel, { conversationId, open: true, perPage: documents.length || 20 }),
1100
+ /* @__PURE__ */ jsx("h2", { className: "text-sm font-semibold uppercase tracking-wide text-gray-500", children: "Bolhas na thread" }),
1101
+ /* @__PURE__ */ jsx(ConversationWallpaper, { className: "max-h-[70vh] overflow-y-auto rounded-lg px-3 py-2", children: messages.map((message, index) => /* @__PURE__ */ jsx(
1102
+ MessageBubble,
1103
+ {
1104
+ message,
1105
+ isMine: message.direction === "outbound",
1106
+ isFirstInGroup: index === 0 || messages[index - 1]?.sender !== message.sender
1107
+ },
1108
+ message.id
1109
+ )) })
1110
+ ] })
1111
+ ] })
1112
+ ] }) });
1113
+ }
560
1114
  export {
1115
+ AudioRecorderButton,
561
1116
  ConversationPreview,
1117
+ ConversationSimulatorPanel,
1118
+ DEFAULT_AUDIO_RECORDER_BUTTON_LABELS,
1119
+ DEFAULT_CONVERSATION_SIMULATOR_PANEL_LABELS,
1120
+ DEFAULT_MEDIA_UPLOAD_PATH,
562
1121
  DEFAULT_PREVIEW_SCRIPT,
563
1122
  GLOBAL_CHANNEL,
1123
+ MEDIA_TYPES_CONVERSATION_ID,
1124
+ MediaTypesPreview,
564
1125
  PREVIEW_CONVERSATIONS,
1126
+ PREVIEW_DOCUMENTS,
1127
+ PREVIEW_FILE_SAMPLES,
1128
+ PREVIEW_MEDIA_ID_PREFIX,
565
1129
  PREVIEW_MESSAGES,
1130
+ PreviewBridgeRejectedError,
566
1131
  PreviewInProductionError,
1132
+ PreviewMediaUploadRejectedError,
567
1133
  PreviewWebhookRejectedError,
1134
+ SIMULATOR_FILE_MEDIA_KINDS,
1135
+ acceptsMediaKind,
568
1136
  assertPreviewEnvironment,
569
1137
  conversationChannel,
570
1138
  createMockConversationsApi,
571
1139
  createMockEventSource,
572
1140
  createMockSSEProvider,
1141
+ createPreviewBridgeClient,
1142
+ createPreviewMediaPoster,
1143
+ createPreviewMediaResolver,
1144
+ createPreviewMediaUploader,
573
1145
  createPreviewStore,
574
1146
  createPreviewWebhookClient,
575
- startPreviewScript
1147
+ isConversationSimulatorClient,
1148
+ mediaKindOf,
1149
+ mediaTypeOf,
1150
+ previewFileBase64,
1151
+ previewFileUrl,
1152
+ resolvePreviewFileSample,
1153
+ signPreviewPayload,
1154
+ simulatorPanelLabelsOf,
1155
+ startPreviewScript,
1156
+ toConversationSimulatorClient
576
1157
  };