@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
@@ -0,0 +1,1329 @@
1
+ import {
2
+ AudioRecorderButton,
3
+ ConversationDocumentsPanel,
4
+ ConversationWallpaper,
5
+ ConversationsProvider,
6
+ DEFAULT_AUDIO_RECORDER_BUTTON_LABELS,
7
+ DateDivider,
8
+ DocumentsLibrary,
9
+ MessageBubble,
10
+ MessageComposer
11
+ } from "../chunk-TV4OQRGH.js";
12
+ import "../chunk-2AYDBWNE.js";
13
+
14
+ // src/preview/previewStore.ts
15
+ var GLOBAL_CHANNEL = "global";
16
+ function conversationChannel(conversationId) {
17
+ return `conv:${conversationId}`;
18
+ }
19
+ function createPreviewStore(params) {
20
+ const conversations = params.conversations.map((conversation) => ({ ...conversation }));
21
+ const messages = new Map(
22
+ Object.entries(params.messages).map(([conversationId, list]) => [conversationId, [...list]])
23
+ );
24
+ const listeners = /* @__PURE__ */ new Map();
25
+ const now = params.now ?? (() => /* @__PURE__ */ new Date());
26
+ let messageSequence = 0;
27
+ function emit(emission) {
28
+ for (const listener of listeners.get(emission.channel) ?? []) listener(emission);
29
+ }
30
+ function emitDataChanged() {
31
+ emit({ channel: GLOBAL_CHANNEL, event: "data-changed", payload: {} });
32
+ }
33
+ function findConversation(conversationId) {
34
+ return conversations.find((conversation) => conversation.id === conversationId);
35
+ }
36
+ return {
37
+ listConversations(filters) {
38
+ return conversations.filter((conversation) => filters?.waitingHuman ? conversation.waitingHuman : true).filter((conversation) => {
39
+ const search = filters?.search?.toLowerCase();
40
+ if (!search) return true;
41
+ return conversation.whatsappNumber.includes(search) || (conversation.clientName?.toLowerCase().includes(search) ?? false);
42
+ }).map((conversation) => ({ ...conversation })).sort((left, right) => right.lastAt.localeCompare(left.lastAt));
43
+ },
44
+ listMessages(conversationId) {
45
+ return [...messages.get(conversationId) ?? []];
46
+ },
47
+ appendMessage(appendParams) {
48
+ messageSequence += 1;
49
+ const timestamp = now().toISOString();
50
+ const message = {
51
+ id: `preview-${messageSequence}`,
52
+ type: "text",
53
+ content: appendParams.content,
54
+ direction: appendParams.direction,
55
+ sender: appendParams.sender,
56
+ timestamp,
57
+ status: appendParams.direction === "outbound" ? "sent" : void 0
58
+ };
59
+ const conversationMessages = messages.get(appendParams.conversationId) ?? [];
60
+ messages.set(appendParams.conversationId, [...conversationMessages, message]);
61
+ const conversation = findConversation(appendParams.conversationId);
62
+ if (conversation) {
63
+ conversation.lastContent = appendParams.content;
64
+ conversation.lastDirection = appendParams.direction;
65
+ conversation.lastAt = timestamp;
66
+ if (appendParams.direction === "inbound") {
67
+ conversation.lastInboundAt = timestamp;
68
+ conversation.unread += 1;
69
+ }
70
+ }
71
+ emit({
72
+ channel: conversationChannel(appendParams.conversationId),
73
+ event: "message",
74
+ payload: { direction: message.direction, sender: message.sender }
75
+ });
76
+ emitDataChanged();
77
+ return message;
78
+ },
79
+ setMode(modeParams) {
80
+ const conversation = findConversation(modeParams.conversationId);
81
+ if (!conversation) return;
82
+ conversation.mode = modeParams.mode;
83
+ conversation.assignedUserId = modeParams.assignedUserId ?? null;
84
+ if (modeParams.mode === "human") conversation.waitingHuman = false;
85
+ emit({
86
+ channel: conversationChannel(modeParams.conversationId),
87
+ event: "mode-changed",
88
+ payload: { mode: conversation.mode, assignedUserId: conversation.assignedUserId }
89
+ });
90
+ emitDataChanged();
91
+ },
92
+ requestHuman(conversationId) {
93
+ const conversation = findConversation(conversationId);
94
+ if (!conversation) return;
95
+ conversation.waitingHuman = true;
96
+ emitDataChanged();
97
+ },
98
+ markRead(conversationId) {
99
+ const conversation = findConversation(conversationId);
100
+ if (!conversation) return;
101
+ conversation.unread = 0;
102
+ emitDataChanged();
103
+ },
104
+ subscribe(channel, listener) {
105
+ const channelListeners = listeners.get(channel) ?? /* @__PURE__ */ new Set();
106
+ channelListeners.add(listener);
107
+ listeners.set(channel, channelListeners);
108
+ return () => {
109
+ channelListeners.delete(listener);
110
+ };
111
+ }
112
+ };
113
+ }
114
+
115
+ // src/preview/mockEventSource.ts
116
+ function createMockEventSource() {
117
+ const listeners = /* @__PURE__ */ new Map();
118
+ let closed = false;
119
+ return {
120
+ get closed() {
121
+ return closed;
122
+ },
123
+ addEventListener(type, listener) {
124
+ const typeListeners = listeners.get(type) ?? /* @__PURE__ */ new Set();
125
+ typeListeners.add(listener);
126
+ listeners.set(type, typeListeners);
127
+ },
128
+ removeEventListener(type, listener) {
129
+ listeners.get(type)?.delete(listener);
130
+ },
131
+ close() {
132
+ closed = true;
133
+ listeners.clear();
134
+ },
135
+ emit(event, payload) {
136
+ if (closed) return;
137
+ const messageEvent = new MessageEvent(event, { data: JSON.stringify(payload) });
138
+ for (const listener of listeners.get(event) ?? []) listener(messageEvent);
139
+ }
140
+ };
141
+ }
142
+
143
+ // src/preview/previewFixtures.ts
144
+ var BASE_DAY = "2026-07-26";
145
+ function at(time) {
146
+ return `${BASE_DAY}T${time}.000Z`;
147
+ }
148
+ var PREVIEW_CONVERSATIONS = [
149
+ {
150
+ id: "5511988887777",
151
+ whatsappNumber: "5511988887777",
152
+ clientName: "Marina Alves",
153
+ lastContent: "quero 2kg de arroz e um \xF3leo",
154
+ lastDirection: "inbound",
155
+ lastAt: at("14:32:00"),
156
+ lastInboundAt: at("14:32:00"),
157
+ mode: "bot",
158
+ assignedUserId: null,
159
+ waitingHuman: false,
160
+ unread: 2,
161
+ currentState: "list_review"
162
+ },
163
+ {
164
+ id: "5511977776666",
165
+ whatsappNumber: "5511977776666",
166
+ clientName: "Diego Prado",
167
+ lastContent: "preciso falar com algu\xE9m",
168
+ lastDirection: "inbound",
169
+ lastAt: at("14:20:00"),
170
+ lastInboundAt: at("14:20:00"),
171
+ mode: "bot",
172
+ assignedUserId: null,
173
+ waitingHuman: true,
174
+ unread: 1,
175
+ currentState: "awaiting_human"
176
+ },
177
+ {
178
+ id: "5511966665555",
179
+ whatsappNumber: "5511966665555",
180
+ clientName: "Sofia Nakamura",
181
+ lastContent: "j\xE1 separei seu pedido, confere?",
182
+ lastDirection: "outbound",
183
+ lastAt: at("13:58:00"),
184
+ lastInboundAt: at("13:50:00"),
185
+ mode: "human",
186
+ assignedUserId: "agent-1",
187
+ waitingHuman: false,
188
+ unread: 0,
189
+ currentState: "human_handling"
190
+ },
191
+ {
192
+ id: "5511955554444",
193
+ whatsappNumber: "5511955554444",
194
+ lastContent: "\xC1udio",
195
+ lastDirection: "inbound",
196
+ lastAt: at("13:31:00"),
197
+ lastInboundAt: at("13:31:00"),
198
+ mode: "bot",
199
+ assignedUserId: null,
200
+ waitingHuman: false,
201
+ unread: 1,
202
+ currentState: "list_import"
203
+ },
204
+ {
205
+ // Cobre TODO tipo que o composer aceita (DEFAULT_ACCEPTED_FILE_TYPES: image/*, video/*,
206
+ // audio/*, .pdf, .doc, .docx, .xls, .xlsx, .zip) mais sticker. Existe para que cada ramo do
207
+ // MediaRenderer e cada ícone/cor do FileIcon apareçam em algum lugar — ramo sem fixture é ramo
208
+ // que ninguém olha até quebrar em produção.
209
+ id: "5511944443333",
210
+ whatsappNumber: "5511944443333",
211
+ clientName: "Rita Documentos",
212
+ lastContent: "segue a planilha do pedido",
213
+ lastDirection: "inbound",
214
+ lastAt: at("15:10:00"),
215
+ lastInboundAt: at("15:10:00"),
216
+ mode: "human",
217
+ assignedUserId: "agent-1",
218
+ waitingHuman: false,
219
+ unread: 3,
220
+ currentState: "human_handling"
221
+ }
222
+ ];
223
+ var PREVIEW_MESSAGES = {
224
+ "5511988887777": [
225
+ {
226
+ id: "fixture-1",
227
+ type: "text",
228
+ content: "oi, boa tarde",
229
+ direction: "inbound",
230
+ sender: "customer",
231
+ timestamp: at("14:30:00")
232
+ },
233
+ {
234
+ id: "fixture-2",
235
+ type: "text",
236
+ content: "Boa tarde! Me manda sua lista de compras que eu monto o carrinho.",
237
+ direction: "outbound",
238
+ sender: "bot",
239
+ timestamp: at("14:30:30"),
240
+ status: "read"
241
+ },
242
+ {
243
+ id: "fixture-3",
244
+ type: "text",
245
+ content: "quero 2kg de arroz e um \xF3leo",
246
+ direction: "inbound",
247
+ sender: "customer",
248
+ timestamp: at("14:32:00")
249
+ }
250
+ ],
251
+ "5511977776666": [
252
+ {
253
+ id: "fixture-4",
254
+ type: "text",
255
+ content: "esse valor do frete est\xE1 certo?",
256
+ direction: "inbound",
257
+ sender: "customer",
258
+ timestamp: at("14:19:00")
259
+ },
260
+ {
261
+ id: "fixture-5",
262
+ type: "text",
263
+ content: "preciso falar com algu\xE9m",
264
+ direction: "inbound",
265
+ sender: "customer",
266
+ timestamp: at("14:20:00")
267
+ }
268
+ ],
269
+ "5511966665555": [
270
+ {
271
+ id: "fixture-6",
272
+ type: "text",
273
+ content: "consegue trocar o leite integral por desnatado?",
274
+ direction: "inbound",
275
+ sender: "customer",
276
+ timestamp: at("13:50:00")
277
+ },
278
+ {
279
+ id: "fixture-7",
280
+ type: "text",
281
+ content: "j\xE1 separei seu pedido, confere?",
282
+ direction: "outbound",
283
+ sender: "agent",
284
+ timestamp: at("13:58:00"),
285
+ status: "delivered",
286
+ agentName: "Ana"
287
+ }
288
+ ],
289
+ "5511955554444": [
290
+ {
291
+ id: "fixture-8",
292
+ type: "audio",
293
+ mediaId: "preview-audio-1",
294
+ mimeType: "audio/ogg",
295
+ direction: "inbound",
296
+ sender: "customer",
297
+ timestamp: at("13:31:00")
298
+ }
299
+ ],
300
+ // Um tipo por mensagem, na ordem em que o MediaRenderer os trata.
301
+ "5511944443333": [
302
+ {
303
+ id: "fixture-doc-image",
304
+ type: "image",
305
+ mediaId: "preview-image-1",
306
+ mimeType: "image/png",
307
+ caption: "foto da prateleira",
308
+ direction: "inbound",
309
+ sender: "customer",
310
+ timestamp: at("15:00:00")
311
+ },
312
+ {
313
+ id: "fixture-doc-video",
314
+ type: "video",
315
+ mediaId: "preview-video-1",
316
+ mimeType: "video/mp4",
317
+ direction: "inbound",
318
+ sender: "customer",
319
+ timestamp: at("15:01:00")
320
+ },
321
+ {
322
+ id: "fixture-doc-audio",
323
+ type: "audio",
324
+ mediaId: "preview-audio-2",
325
+ mimeType: "audio/ogg",
326
+ direction: "inbound",
327
+ sender: "customer",
328
+ timestamp: at("15:02:00")
329
+ },
330
+ {
331
+ id: "fixture-doc-sticker",
332
+ type: "sticker",
333
+ mediaId: "preview-sticker-1",
334
+ mimeType: "image/webp",
335
+ direction: "inbound",
336
+ sender: "customer",
337
+ timestamp: at("15:03:00")
338
+ },
339
+ // Os cinco ramos do FileIcon: pdf, doc, xls, zip e o genérico do fallback.
340
+ {
341
+ id: "fixture-doc-pdf",
342
+ type: "document",
343
+ uploadId: "preview/documentos/nota-fiscal.pdf",
344
+ filename: "nota-fiscal.pdf",
345
+ mimeType: "application/pdf",
346
+ sizeBytes: 184320,
347
+ direction: "inbound",
348
+ sender: "customer",
349
+ timestamp: at("15:04:00")
350
+ },
351
+ {
352
+ id: "fixture-doc-docx",
353
+ type: "document",
354
+ uploadId: "preview/documentos/contrato.docx",
355
+ filename: "contrato.docx",
356
+ mimeType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
357
+ sizeBytes: 42112,
358
+ direction: "inbound",
359
+ sender: "customer",
360
+ timestamp: at("15:05:00")
361
+ },
362
+ {
363
+ id: "fixture-doc-doc",
364
+ type: "document",
365
+ uploadId: "preview/documentos/procuracao.doc",
366
+ filename: "procuracao.doc",
367
+ mimeType: "application/msword",
368
+ sizeBytes: 31744,
369
+ direction: "inbound",
370
+ sender: "customer",
371
+ timestamp: at("15:06:00")
372
+ },
373
+ {
374
+ id: "fixture-doc-xlsx",
375
+ type: "document",
376
+ uploadId: "preview/documentos/pedido.xlsx",
377
+ filename: "pedido.xlsx",
378
+ mimeType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
379
+ sizeBytes: 15872,
380
+ direction: "inbound",
381
+ sender: "customer",
382
+ timestamp: at("15:07:00")
383
+ },
384
+ {
385
+ id: "fixture-doc-xls",
386
+ type: "document",
387
+ uploadId: "preview/documentos/tabela-antiga.xls",
388
+ filename: "tabela-antiga.xls",
389
+ mimeType: "application/vnd.ms-excel",
390
+ sizeBytes: 9216,
391
+ direction: "inbound",
392
+ sender: "customer",
393
+ timestamp: at("15:08:00")
394
+ },
395
+ {
396
+ id: "fixture-doc-zip",
397
+ type: "document",
398
+ uploadId: "preview/documentos/comprovantes.zip",
399
+ filename: "comprovantes.zip",
400
+ mimeType: "application/zip",
401
+ sizeBytes: 2355200,
402
+ direction: "inbound",
403
+ sender: "customer",
404
+ timestamp: at("15:09:00")
405
+ },
406
+ {
407
+ id: "fixture-doc-txt",
408
+ type: "document",
409
+ uploadId: "preview/documentos/lista-compras.txt",
410
+ filename: "lista-compras.txt",
411
+ mimeType: "text/plain",
412
+ sizeBytes: 1024,
413
+ direction: "inbound",
414
+ sender: "customer",
415
+ timestamp: at("15:10:00")
416
+ },
417
+ {
418
+ id: "fixture-doc-csv",
419
+ type: "document",
420
+ uploadId: "preview/documentos/planilha-do-pedido.csv",
421
+ filename: "planilha-do-pedido.csv",
422
+ mimeType: "text/csv",
423
+ sizeBytes: 2048,
424
+ direction: "inbound",
425
+ sender: "customer",
426
+ timestamp: at("15:11:00")
427
+ },
428
+ {
429
+ id: "fixture-doc-pptx",
430
+ type: "document",
431
+ uploadId: "preview/documentos/apresentacao.pptx",
432
+ filename: "apresentacao.pptx",
433
+ mimeType: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
434
+ sizeBytes: 512e3,
435
+ direction: "inbound",
436
+ sender: "customer",
437
+ timestamp: at("15:12:00")
438
+ },
439
+ {
440
+ id: "fixture-doc-ppt",
441
+ type: "document",
442
+ uploadId: "preview/documentos/antiga.ppt",
443
+ filename: "antiga.ppt",
444
+ mimeType: "application/vnd.ms-powerpoint",
445
+ sizeBytes: 348160,
446
+ direction: "inbound",
447
+ sender: "customer",
448
+ timestamp: at("15:13:00")
449
+ },
450
+ // Mídia enviada COMO arquivo: aí a Meta manda o nome, então é onde a extensão de música e de
451
+ // vídeo aparece na lista. Mandada como `image`/`audio`/`video` (acima e abaixo) ela chega sem
452
+ // nome nenhum, e o rótulo vira o id da mídia — como em produção.
453
+ {
454
+ id: "fixture-doc-mp3",
455
+ type: "document",
456
+ uploadId: "preview/documentos/musica-do-anuncio.mp3",
457
+ filename: "musica-do-anuncio.mp3",
458
+ mimeType: "audio/mpeg",
459
+ sizeBytes: 3145728,
460
+ direction: "inbound",
461
+ sender: "customer",
462
+ timestamp: at("15:14:00")
463
+ },
464
+ {
465
+ id: "fixture-doc-m4a",
466
+ type: "document",
467
+ uploadId: "preview/documentos/recado.m4a",
468
+ filename: "recado.m4a",
469
+ mimeType: "audio/mp4",
470
+ sizeBytes: 486400,
471
+ direction: "inbound",
472
+ sender: "customer",
473
+ timestamp: at("15:15:00")
474
+ },
475
+ {
476
+ id: "fixture-doc-mp4-file",
477
+ type: "document",
478
+ uploadId: "preview/documentos/video-do-produto.mp4",
479
+ filename: "video-do-produto.mp4",
480
+ mimeType: "video/mp4",
481
+ sizeBytes: 8388608,
482
+ direction: "inbound",
483
+ sender: "customer",
484
+ timestamp: at("15:16:00")
485
+ },
486
+ {
487
+ id: "fixture-doc-jpg",
488
+ type: "document",
489
+ uploadId: "preview/documentos/foto-do-recibo.jpg",
490
+ filename: "foto-do-recibo.jpg",
491
+ mimeType: "image/jpeg",
492
+ sizeBytes: 262144,
493
+ direction: "inbound",
494
+ sender: "customer",
495
+ timestamp: at("15:17:00")
496
+ },
497
+ // As espécies restantes que a Meta aceita, sem nome de arquivo — fecham a lista de tipos
498
+ // aceitos e exercitam o fallback de ícone pela família do mimeType.
499
+ {
500
+ id: "fixture-media-jpeg",
501
+ type: "image",
502
+ mediaId: "preview-image-jpeg",
503
+ mimeType: "image/jpeg",
504
+ sizeBytes: 138240,
505
+ direction: "inbound",
506
+ sender: "customer",
507
+ timestamp: at("15:18:00")
508
+ },
509
+ {
510
+ id: "fixture-media-3gp",
511
+ type: "video",
512
+ mediaId: "preview-video-3gp",
513
+ mimeType: "video/3gp",
514
+ sizeBytes: 921600,
515
+ direction: "inbound",
516
+ sender: "customer",
517
+ timestamp: at("15:19:00")
518
+ },
519
+ {
520
+ id: "fixture-media-aac",
521
+ type: "audio",
522
+ mediaId: "preview-audio-aac",
523
+ mimeType: "audio/aac",
524
+ sizeBytes: 76800,
525
+ direction: "inbound",
526
+ sender: "customer",
527
+ timestamp: at("15:20:00")
528
+ },
529
+ {
530
+ id: "fixture-media-amr",
531
+ type: "audio",
532
+ mediaId: "preview-audio-amr",
533
+ mimeType: "audio/amr",
534
+ sizeBytes: 24576,
535
+ direction: "inbound",
536
+ sender: "customer",
537
+ timestamp: at("15:21:00")
538
+ }
539
+ ]
540
+ };
541
+ var LIBRARY_MEDIA_TYPES = /* @__PURE__ */ new Set(["document", "image", "video", "audio", "sticker"]);
542
+ var PREVIEW_DOCUMENTS = {
543
+ "5511944443333": (PREVIEW_MESSAGES["5511944443333"] ?? []).filter((message) => LIBRARY_MEDIA_TYPES.has(message.type)).map((message) => ({
544
+ // Mídia não tem `uploadId` na mensagem: o backend deriva a key do id da mídia na Meta, e o
545
+ // caminho abaixo imita esse formato para o download assinado receber algo do mesmo feitio.
546
+ id: message.uploadId ?? (message.mediaId ? `preview/inbound/${message.mediaId}` : message.id),
547
+ // Só documento chega nomeado. Para os outros o backend salva o id da mídia como rótulo —
548
+ // manter isso aqui é o que faz o ícone por família do mimeType ser exercitado de verdade.
549
+ filename: message.filename ?? message.mediaId ?? message.id,
550
+ mimeType: message.mimeType ?? "application/octet-stream",
551
+ sizeBytes: message.sizeBytes ?? 0,
552
+ source: message.sender,
553
+ linkedAt: message.timestamp
554
+ }))
555
+ };
556
+
557
+ // src/preview/previewFileSamples.ts
558
+ var PREVIEW_FILE_SAMPLES = {
559
+ "application/pdf": "data:application/pdf;base64,JVBERi0xLjQKMSAwIG9iajw8L1R5cGUvQ2F0YWxvZy9QYWdlcyAyIDAgUj4+ZW5kb2JqCjIgMCBvYmo8PC9UeXBlL1BhZ2VzL0tpZHNbMyAwIFJdL0NvdW50IDE+PmVuZG9iagozIDAgb2JqPDwvVHlwZS9QYWdlL1BhcmVudCAyIDAgUi9NZWRpYUJveFswIDAgMjYwIDExMF0vQ29udGVudHMgNCAwIFIvUmVzb3VyY2VzPDwvRm9udDw8L0YxIDUgMCBSPj4+Pj4+ZW5kb2JqCjQgMCBvYmo8PC9MZW5ndGggNjI+PnN0cmVhbQpCVCAvRjEgMTMgVGYgMTggNTUgVGQgKERvY3VtZW50byBkZSBleGVtcGxvIGRvIHByZXZpZXcpIFRqIEVUCmVuZHN0cmVhbWVuZG9iago1IDAgb2JqPDwvVHlwZS9Gb250L1N1YnR5cGUvVHlwZTEvQmFzZUZvbnQvSGVsdmV0aWNhPj5lbmRvYmoKeHJlZgowIDYKMDAwMDAwMDAwMCA2NTUzNSBmIAowMDAwMDAwMDA5IDAwMDAwIG4gCjAwMDAwMDAwNTIgMDAwMDAgbiAKMDAwMDAwMDEwMSAwMDAwMCBuIAowMDAwMDAwMjExIDAwMDAwIG4gCjAwMDAwMDAzMTcgMDAwMDAgbiAKdHJhaWxlcjw8L1NpemUgNi9Sb290IDEgMCBSPj4Kc3RhcnR4cmVmCjM3OAolJUVPRgo=",
560
+ "image/png": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAIAAADYYG7QAAAACXBIWXMAAAABAAAAAQBPJcTWAAAATElEQVR4nO3OMRHAIAAAMbhDWo2hCYPdu/xIh0RB5rPP+JN1O/AlVISKUBEqQkWoCBWhIlSEilARKkJFqAgVoSJUhIpQESpCRagIlRcgDwI25HmPQAAAAABJRU5ErkJggg==",
561
+ "image/jpeg": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAgAAAQABAAD//gAQTGF2YzYyLjExLjEwMAD/2wBDAAgEBAQEBAUFBQUFBQYGBgYGBgYGBgYGBgYHBwcICAgHBwcGBgcHCAgICAkJCQgICAgJCQoKCgwMCwsODg4RERT/xABMAAEBAAAAAAAAAAAAAAAAAAAABAEBAQAAAAAAAAAAAAAAAAAAAAYQAQAAAAAAAAAAAAAAAAAAAAARAQAAAAAAAAAAAAAAAAAAAAD/wAARCAAwADADASIAAhEAAxEA/9oADAMBAAIRAxEAPwCUBRpwAAAAAAAAAAAAAAAB/9k=",
562
+ "image/webp": "data:image/webp;base64,UklGRhoAAABXRUJQVlA4TA0AAAAvAAAAEAcQERGIiP4HAA==",
563
+ "video/mp4": "data:video/mp4;base64,AAAAIGZ0eXBpc29tAAACAGlzb21pc28yYXZjMW1wNDEAAANdbW9vdgAAAGxtdmhkAAAAAAAAAAAAAAAAAAAD6AAAASwAAQAAAQAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAoh0cmFrAAAAXHRraGQAAAADAAAAAAAAAAAAAAABAAAAAAAAASwAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAABAAAAAAEAAAABAAAAAAAAkZWR0cwAAABxlbHN0AAAAAAAAAAEAAAEsAAAIAAABAAAAAAIAbWRpYQAAACBtZGhkAAAAAAAAAAAAAAAAAAAoAAAAEABVxAAAAAAALWhkbHIAAAAAAAAAAHZpZGUAAAAAAAAAAAAAAABWaWRlb0hhbmRsZXIAAAABq21pbmYAAAAUdm1oZAAAAAEAAAAAAAAAAAAAACRkaW5mAAAAHGRyZWYAAAAAAAAAAQAAAAx1cmwgAAAAAQAAAWtzdGJsAAAAv3N0c2QAAAAAAAAAAQAAAK9hdmMxAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAEAAQABIAAAASAAAAAAAAAABFUxhdmM2Mi4xMS4xMDAgbGlieDI2NAAAAAAAAAAAAAAAGP//AAAANWF2Y0MBZAAK/+EAGGdkAAqs2UQmwEQAAAMABAAAAwBQPEiWWAEABmjr48siwP34+AAAAAAQcGFzcAAAAAEAAAABAAAAFGJ0cnQAAAAAAABO2gAAAAAAAAAYc3R0cwAAAAAAAAABAAAAAwAABAAAAAAUc3RzcwAAAAAAAAABAAAAAQAAAChjdHRzAAAAAAAAAAMAAAABAAAIAAAAAAEAAAwAAAAAAQAABAAAAAAcc3RzYwAAAAAAAAABAAAAAQAAAAMAAAABAAAAIHN0c3oAAAAAAAAAAAAAAAMAAALbAAAADgAAAAwAAAAUc3RjbwAAAAAAAAABAAADjQAAAGF1ZHRhAAAAWW1ldGEAAAAAAAAAIWhkbHIAAAAAAAAAAG1kaXJhcHBsAAAAAAAAAAAAAAAALGlsc3QAAAAkqXRvbwAAABxkYXRhAAAAAQAAAABMYXZmNjIuMy4xMDAAAAAIZnJlZQAAAv1tZGF0AAACrgYF//+q3EXpvebZSLeWLNgg2SPu73gyNjQgLSBjb3JlIDE2NSByMzIyMiBiMzU2MDVhIC0gSC4yNjQvTVBFRy00IEFWQyBjb2RlYyAtIENvcHlsZWZ0IDIwMDMtMjAyNSAtIGh0dHA6Ly93d3cudmlkZW9sYW4ub3JnL3gyNjQuaHRtbCAtIG9wdGlvbnM6IGNhYmFjPTEgcmVmPTMgZGVibG9jaz0xOjA6MCBhbmFseXNlPTB4MzoweDExMyBtZT1oZXggc3VibWU9NyBwc3k9MSBwc3lfcmQ9MS4wMDowLjAwIG1peGVkX3JlZj0xIG1lX3JhbmdlPTE2IGNocm9tYV9tZT0xIHRyZWxsaXM9MSA4eDhkY3Q9MSBjcW09MCBkZWFkem9uZT0yMSwxMSBmYXN0X3Bza2lwPTEgY2hyb21hX3FwX29mZnNldD0tMiB0aHJlYWRzPTIgbG9va2FoZWFkX3RocmVhZHM9MSBzbGljZWRfdGhyZWFkcz0wIG5yPTAgZGVjaW1hdGU9MSBpbnRlcmxhY2VkPTAgYmx1cmF5X2NvbXBhdD0wIGNvbnN0cmFpbmVkX2ludHJhPTAgYmZyYW1lcz0zIGJfcHlyYW1pZD0yIGJfYWRhcHQ9MSBiX2JpYXM9MCBkaXJlY3Q9MSB3ZWlnaHRiPTEgb3Blbl9nb3A9MCB3ZWlnaHRwPTIga2V5aW50PTI1MCBrZXlpbnRfbWluPTEwIHNjZW5lY3V0PTQwIGludHJhX3JlZnJlc2g9MCByY19sb29rYWhlYWQ9NDAgcmM9Y3JmIG1idHJlZT0xIGNyZj0yMy4wIHFjb21wPTAuNjAgcXBtaW49MCBxcG1heD02OSBxcHN0ZXA9NCBpcF9yYXRpbz0xLjQwIGFxPTE6MS4wMACAAAAAJWWIhAA///7mdfgUoKykhh+DdK6xVynnAWznTQ4fVSgMZ1FwY28AAAAKQZoibEN//qePiAAAAAgBnkF5Df8HfQ==",
564
+ "audio/mpeg": "data:audio/mpeg;base64,SUQzBAAAAAAAIlRTU0UAAAAOAAADTGF2ZjYyLjMuMTAwAAAAAAAAAAAAAAD/40jAAAAAAAAAAAAASW5mbwAAAA8AAAAFAAAGwABVVVVVVVVVVVVVVVVVVVVVVVVVgICAgICAgICAgICAgICAgICAgICqqqqqqqqqqqqqqqqqqqqqqqqqqtXV1dXV1dXV1dXV1dXV1dXV1dXV//////////////////////////8AAAAATGF2YzYyLjExAAAAAAAAAAAAAAAAJALAAAAAAAAABsDR4nNFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/40jEAAAAA0gAAAAATEFNRTMuMTAwVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/40jEAAAAA0gAAAAATEFNRTMuMTAwVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/40jEAAAAA0gAAAAATEFNRTMuMTAwVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/40jEAAAAA0gAAAAATEFNRTMuMTAwVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/40jEAAAAA0gAAAAATEFNRTMuMTAwVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVU=",
565
+ "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=",
566
+ "audio/mp4": "data:audio/mp4;base64,AAAAHGZ0eXBNNEEgAAACAE00QSBpc29taXNvMgAAAAhmcmVlAAAAQW1kYXTeAgBMYXZjNjIuMTEuMTAwAAIwQA4BGCAHARggBwEYIAcBGCAHARggBwEYIAcBGCAHARggBwEYIAcAAAMibW9vdgAAAGxtdmhkAAAAAAAAAAAAAAAAAAAD6AAAAMgAAQAAAQAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAk10cmFrAAAAXHRraGQAAAADAAAAAAAAAAAAAAABAAAAAAAAAMgAAAAAAAAAAAAAAAEBAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAkZWR0cwAAABxlbHN0AAAAAAAAAAEAAADIAAAEAAABAAAAAAHFbWRpYQAAACBtZGhkAAAAAAAAAAAAAAAAAACsRAAAJnRVxAAAAAAALWhkbHIAAAAAAAAAAHNvdW4AAAAAAAAAAAAAAABTb3VuZEhhbmRsZXIAAAABcG1pbmYAAAAQc21oZAAAAAAAAAAAAAAAJGRpbmYAAAAcZHJlZgAAAAAAAAABAAAADHVybCAAAAABAAABNHN0YmwAAABqc3RzZAAAAAAAAAABAAAAWm1wNGEAAAAAAAAAAQAAAAAAAAAAAAEAEAAAAACsRAAAAAAANmVzZHMAAAAAA4CAgCUAAQAEgICAF0AVAAAAAAB9AAAAB/oFgICABRIIVuUABoCAgAECAAAAIHN0dHMAAAAAAAAAAgAAAAkAAAQAAAAAAQAAAnQAAAAcc3RzYwAAAAAAAAABAAAAAQAAAAoAAAABAAAAPHN0c3oAAAAAAAAAAAAAAAoAAAAVAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAAFHN0Y28AAAAAAAAAAQAAACwAAAAac2dwZAEAAAByb2xsAAAAAgAAAAH//wAAABxzYmdwAAAAAHJvbGwAAAABAAAACgAAAAEAAABhdWR0YQAAAFltZXRhAAAAAAAAACFoZGxyAAAAAAAAAABtZGlyYXBwbAAAAAAAAAAAAAAAACxpbHN0AAAAJKl0b28AAAAcZGF0YQAAAAEAAAAATGF2ZjYyLjMuMTAw",
567
+ "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==",
568
+ "text/plain": "data:text/plain;base64,TGlzdGEgZGUgY29tcHJhcwotIGFycm96IDJrZwotIG9sZW8gMXVuCg==",
569
+ "text/csv": "data:text/csv;base64,cHJvZHV0byxxdWFudGlkYWRlLHByZWNvCmFycm96LDIsMjQuOTAKb2xlbywxLDcuNTAK",
570
+ "application/zip": "data:application/zip;base64,UEsDBBQAAAAIANFO/FwtmGOxGwAAABkAAAARAAAAY29tcHJvdmFudGUtMS50eHRLzs8tKMovS8wrSVVISVUoSExPzE3NK8nnAgBQSwMEFAAAAAgA0U78XCftV8YWAAAAFAAAABEAAABjb21wcm92YW50ZS0yLnR4dCtOTS/NS8lXSM7PLSjKL0vMK0nlAgBQSwECFAMUAAAACADRTvxcLZhjsRsAAAAZAAAAEQAAAAAAAAAAAAAAgAEAAAAAY29tcHJvdmFudGUtMS50eHRQSwECFAMUAAAACADRTvxcJ+1XxhYAAAAUAAAAEQAAAAAAAAAAAAAAgAFKAAAAY29tcHJvdmFudGUtMi50eHRQSwUGAAAAAAIAAgB+AAAAjwAAAAAA",
571
+ "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==",
572
+ "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=="
573
+ };
574
+ var FAMILY_FALLBACK = {
575
+ image: "image/png",
576
+ video: "video/mp4",
577
+ audio: "audio/mpeg",
578
+ text: "text/plain"
579
+ };
580
+ function resolvePreviewFileSample(mimeType, filename) {
581
+ const normalized = (mimeType ?? "").split(";")[0].trim().toLowerCase();
582
+ const exact = PREVIEW_FILE_SAMPLES[normalized];
583
+ if (exact) return exact;
584
+ const family = FAMILY_FALLBACK[normalized.split("/")[0] ?? ""];
585
+ const byFamily = family ? PREVIEW_FILE_SAMPLES[family] : void 0;
586
+ if (byFamily) return byFamily;
587
+ const note = `Preview do SDK: nao existe amostra binaria valida para ${normalized || "tipo desconhecido"}${filename ? ` (${filename})` : ""}. Na tela real este arquivo vem do storage.`;
588
+ return `data:text/plain;charset=utf-8,${encodeURIComponent(note)}`;
589
+ }
590
+
591
+ // src/preview/previewMediaSource.ts
592
+ var blobUrlCache = /* @__PURE__ */ new Map();
593
+ function toBlob(dataUrl) {
594
+ const [head, payload] = dataUrl.split(",");
595
+ const mimeType = head.replace(/^data:/, "").replace(/;base64$/, "").replace(/;charset=.*$/, "");
596
+ if (!head.endsWith(";base64")) {
597
+ return new Blob([decodeURIComponent(payload)], { type: mimeType || "text/plain" });
598
+ }
599
+ const binary = atob(payload);
600
+ const bytes = new Uint8Array(binary.length);
601
+ for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index);
602
+ return new Blob([bytes], { type: mimeType });
603
+ }
604
+ function previewFileUrl(mimeType, filename) {
605
+ const dataUrl = resolvePreviewFileSample(mimeType, filename);
606
+ if (typeof URL === "undefined" || typeof URL.createObjectURL !== "function") return dataUrl;
607
+ const cached = blobUrlCache.get(dataUrl);
608
+ if (cached) return cached;
609
+ const blobUrl = URL.createObjectURL(toBlob(dataUrl));
610
+ blobUrlCache.set(dataUrl, blobUrl);
611
+ return blobUrl;
612
+ }
613
+ function previewFileBase64(mimeType, filename) {
614
+ const dataUrl = resolvePreviewFileSample(mimeType, filename);
615
+ const [head, payload] = dataUrl.split(",");
616
+ const declared = head.replace(/^data:/, "").replace(/;base64$/, "").replace(/;charset=.*$/, "");
617
+ if (head.endsWith(";base64")) return { mimeType: declared, data: payload };
618
+ return { mimeType: declared, data: btoa(decodeURIComponent(payload)) };
619
+ }
620
+ function createPreviewMediaResolver() {
621
+ const byId = /* @__PURE__ */ new Map();
622
+ for (const documents of Object.values(PREVIEW_DOCUMENTS)) {
623
+ for (const document of documents) {
624
+ byId.set(document.id, { mimeType: document.mimeType, filename: document.filename });
625
+ }
626
+ }
627
+ return async (message) => {
628
+ const reference = message.uploadId ?? (message.mediaId ? `preview/inbound/${message.mediaId}` : void 0);
629
+ if (!reference) return null;
630
+ const known = byId.get(reference);
631
+ return previewFileUrl(known?.mimeType ?? message.mimeType, known?.filename ?? message.filename);
632
+ };
633
+ }
634
+
635
+ // src/preview/createMockConversationsApi.ts
636
+ var DEFAULT_LATENCY_MS = 120;
637
+ var PREVIEW_AGENT_ID = "preview-agent";
638
+ var PREVIEW_TEMPLATES = [
639
+ { name: "retomada_atendimento", language: "pt_BR", status: "APPROVED", category: "UTILITY" },
640
+ { name: "lembrete_documentos", language: "pt_BR", status: "APPROVED", category: "UTILITY" },
641
+ { name: "promocao_taxa", language: "pt_BR", status: "PENDING", category: "MARKETING" }
642
+ ];
643
+ function createMockConversationsApi(params) {
644
+ const latencyMs = params.latencyMs ?? DEFAULT_LATENCY_MS;
645
+ async function withLatency(produce) {
646
+ await new Promise((resolve) => setTimeout(resolve, latencyMs));
647
+ return produce();
648
+ }
649
+ return {
650
+ // Devolve a forma paginada, não o array puro: é a que o contrato passou a oferecer e a que
651
+ // permite o preview desenhar controles de página. O total é contado ANTES do corte — depois
652
+ // dele seria sempre o tamanho da página, e a paginação nunca sairia da primeira.
653
+ fetchConversations(fetchParams) {
654
+ return withLatency(() => {
655
+ const conversations = params.store.listConversations({
656
+ waitingHuman: fetchParams?.waitingHuman,
657
+ search: fetchParams?.search
658
+ });
659
+ const limit = fetchParams?.limit ?? conversations.length;
660
+ const page = fetchParams?.page ?? 1;
661
+ return {
662
+ conversations: conversations.slice((page - 1) * limit, page * limit),
663
+ total: conversations.length
664
+ };
665
+ });
666
+ },
667
+ fetchMessages(conversationId, fetchParams) {
668
+ return withLatency(() => {
669
+ const messages = params.store.listMessages(conversationId);
670
+ const limit = fetchParams?.limit;
671
+ return limit ? messages.slice(-limit) : messages;
672
+ });
673
+ },
674
+ sendMessage(conversationId, text) {
675
+ return withLatency(
676
+ () => params.store.appendMessage({ conversationId, content: text, direction: "outbound", sender: "agent" })
677
+ );
678
+ },
679
+ sendMedia(conversationId, data) {
680
+ return withLatency(
681
+ () => params.store.appendMessage({
682
+ conversationId,
683
+ content: data.caption ?? data.filename,
684
+ direction: "outbound",
685
+ sender: "agent"
686
+ })
687
+ );
688
+ },
689
+ sendTemplate(conversationId, data) {
690
+ return withLatency(() => {
691
+ params.store.appendMessage({
692
+ conversationId,
693
+ // Sem nome, o host está pedindo o template padrão do backend — o mock representa isso
694
+ // pelo que o atendente veria, não por um nome inventado.
695
+ content: `[template] ${data.templateName ?? PREVIEW_TEMPLATES[0]?.name ?? "padrao"}`,
696
+ direction: "outbound",
697
+ sender: "agent"
698
+ });
699
+ });
700
+ },
701
+ markRead(conversationId) {
702
+ return withLatency(() => params.store.markRead(conversationId));
703
+ },
704
+ getContext(conversationId) {
705
+ return withLatency(() => {
706
+ const conversation = params.store.listConversations().find((item) => item.id === conversationId);
707
+ return {
708
+ currentState: conversation?.currentState ?? "unknown",
709
+ mode: conversation?.mode ?? "bot",
710
+ preview: true
711
+ };
712
+ });
713
+ },
714
+ /**
715
+ * Espelha o backend em busca, filtro de origem, ordenação E paginação. Mock que ignora params
716
+ * faz o painel parecer quebrado aqui e, pior, esconde o caso em que o backend também os ignora
717
+ * — foi exatamente assim que o filtro de origem passou a existir só no contrato.
718
+ */
719
+ getDocuments(conversationId, documentParams) {
720
+ return withLatency(() => {
721
+ let documents = [...PREVIEW_DOCUMENTS[conversationId] ?? []];
722
+ const search = documentParams?.search?.trim().toLowerCase();
723
+ if (search) {
724
+ documents = documents.filter((document) => document.filename.toLowerCase().includes(search));
725
+ }
726
+ const source = documentParams?.source;
727
+ if (source === "team") {
728
+ documents = documents.filter((document) => document.source === "agent" || document.source === "bot");
729
+ } else if (source) {
730
+ documents = documents.filter((document) => document.source === source);
731
+ }
732
+ documents.sort(
733
+ (left, right) => documentParams?.sortDirection === "asc" ? left.linkedAt.localeCompare(right.linkedAt) : right.linkedAt.localeCompare(left.linkedAt)
734
+ );
735
+ const total = documents.length;
736
+ const limit = documentParams?.limit ?? total;
737
+ const page = documentParams?.page ?? 1;
738
+ return { documents: documents.slice((page - 1) * limit, page * limit), total };
739
+ });
740
+ },
741
+ /**
742
+ * Zip de mentira: um texto listando o que entraria. Basta para exercitar seleção, botão e o
743
+ * caminho de download no preview, sem arrastar uma lib de compactação para o pacote.
744
+ */
745
+ downloadDocumentsArchive(conversationId, uploadIds) {
746
+ return withLatency(() => {
747
+ const known = PREVIEW_DOCUMENTS[conversationId] ?? [];
748
+ const names = uploadIds.map((id) => known.find((document) => document.id === id)?.filename ?? id);
749
+ return new Blob([`preview: ${names.length} arquivo(s)
750
+ ${names.join("\n")}`], { type: "application/zip" });
751
+ });
752
+ },
753
+ /** Junta as bibliotecas de todas as conversas do fixture, com a origem de cada arquivo. */
754
+ getAllDocuments(documentParams) {
755
+ return withLatency(() => {
756
+ let all = Object.entries(PREVIEW_DOCUMENTS).flatMap(
757
+ ([conversationId, docs]) => docs.map((document) => ({ ...document, conversationId }))
758
+ );
759
+ const search = documentParams?.search?.trim().toLowerCase();
760
+ if (search) {
761
+ const digits = search.replace(/\D/g, "");
762
+ all = all.filter(
763
+ (document) => document.filename.toLowerCase().includes(search) || digits !== "" && document.conversationId.includes(digits)
764
+ );
765
+ }
766
+ const source = documentParams?.source;
767
+ if (source === "team") {
768
+ all = all.filter((document) => document.source === "agent" || document.source === "bot");
769
+ } else if (source) {
770
+ all = all.filter((document) => document.source === source);
771
+ }
772
+ all.sort(
773
+ (left, right) => documentParams?.sortDirection === "asc" ? left.linkedAt.localeCompare(right.linkedAt) : right.linkedAt.localeCompare(left.linkedAt)
774
+ );
775
+ const total = all.length;
776
+ const limit = documentParams?.limit ?? total;
777
+ const page = documentParams?.page ?? 1;
778
+ return { documents: all.slice((page - 1) * limit, page * limit), total };
779
+ });
780
+ },
781
+ // Devolve os bytes DO TIPO do documento, não uma imagem para tudo: antes, abrir um PDF entregava
782
+ // um PNG rotulado `application/pdf` e o leitor recusava o arquivo. O `uploadId` é a única pista
783
+ // que o contrato dá, então o tipo vem da própria biblioteca.
784
+ getDocumentUrl(uploadId) {
785
+ return withLatency(() => {
786
+ const found = Object.values(PREVIEW_DOCUMENTS).flat().find((document) => document.id === uploadId);
787
+ return previewFileUrl(found?.mimeType, found?.filename);
788
+ });
789
+ },
790
+ // Caminho da mídia ainda não ingerida: o backend busca na Meta e devolve base64. Resolve pelo
791
+ // id para a bolha receber os bytes DO TIPO dela — devolvendo um PNG para todo id, vídeo e áudio
792
+ // apareciam quebrados na thread mesmo havendo amostra válida do formato.
793
+ getMediaProxyUrl(mediaId) {
794
+ return withLatency(() => {
795
+ const found = Object.values(PREVIEW_DOCUMENTS).flat().find((document) => document.id === `preview/inbound/${mediaId}`);
796
+ return previewFileBase64(found?.mimeType, found?.filename);
797
+ });
798
+ },
799
+ takeover(conversationId) {
800
+ return withLatency(
801
+ () => params.store.setMode({ conversationId, mode: "human", assignedUserId: PREVIEW_AGENT_ID })
802
+ );
803
+ },
804
+ release(conversationId) {
805
+ return withLatency(() => params.store.setMode({ conversationId, mode: "bot" }));
806
+ },
807
+ // Encerrar devolve ao bot como o release, e é de propósito: a diferença entre os dois é a
808
+ // despedida, que o host manda antes de chamar aqui. O mock não a inventa.
809
+ finalize(conversationId) {
810
+ return withLatency(() => params.store.setMode({ conversationId, mode: "bot" }));
811
+ },
812
+ markAllRead() {
813
+ return withLatency(() => {
814
+ for (const conversation of params.store.listConversations()) {
815
+ params.store.markRead(conversation.id);
816
+ }
817
+ });
818
+ },
819
+ listTemplates() {
820
+ return withLatency(() => [...PREVIEW_TEMPLATES]);
821
+ }
822
+ };
823
+ }
824
+
825
+ // src/preview/createMockSSEProvider.ts
826
+ function createMockSSEProvider(params) {
827
+ function connect(channel) {
828
+ const source = createMockEventSource();
829
+ const unsubscribe = params.store.subscribe(channel, (emission) => {
830
+ source.emit(emission.event, emission.payload);
831
+ });
832
+ const close = source.close.bind(source);
833
+ source.close = () => {
834
+ unsubscribe();
835
+ close();
836
+ };
837
+ return source;
838
+ }
839
+ return {
840
+ connectConversationStream(conversationId) {
841
+ return connect(conversationChannel(conversationId));
842
+ },
843
+ connectGlobalStream() {
844
+ return connect(GLOBAL_CHANNEL);
845
+ }
846
+ };
847
+ }
848
+
849
+ // src/preview/ConversationPreview.tsx
850
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
851
+ import { jsx, jsxs } from "react/jsx-runtime";
852
+ function mediaTypeOf(mimeType) {
853
+ if (mimeType.startsWith("image/")) return "image";
854
+ if (mimeType.startsWith("video/")) return "video";
855
+ if (mimeType.startsWith("audio/")) return "audio";
856
+ return "document";
857
+ }
858
+ var GROUPING_WINDOW_MS = 5 * 60 * 1e3;
859
+ var FOLLOW_UP_REFRESH_MS = [400, 1200, 3e3];
860
+ function decorate(messages) {
861
+ return messages.map((message, index) => {
862
+ const previous = index > 0 ? messages[index - 1] : void 0;
863
+ const currentTime = new Date(message.timestamp).getTime();
864
+ const previousTime = previous ? new Date(previous.timestamp).getTime() : 0;
865
+ return {
866
+ message,
867
+ isFirstInGroup: !previous || previous.sender !== message.sender || currentTime - previousTime > GROUPING_WINDOW_MS,
868
+ showDateDivider: !previous || new Date(message.timestamp).toDateString() !== new Date(previous.timestamp).toDateString()
869
+ };
870
+ });
871
+ }
872
+ function statusOf(error) {
873
+ if (typeof error !== "object" || error === null) return void 0;
874
+ const candidate = error;
875
+ const value = candidate.status ?? candidate.statusCode;
876
+ return typeof value === "number" ? value : void 0;
877
+ }
878
+ function isNotFound(error) {
879
+ return statusOf(error) === 404;
880
+ }
881
+ function describeLoadFailure(error) {
882
+ const status = statusOf(error);
883
+ if (status === 401 || status === 403) {
884
+ return "Sem sess\xE3o de administrador nesta aba: a mensagem \xE9 entregue no webhook, mas o transcript n\xE3o pode ser lido. Entre no painel nesta mesma aba e reabra o simulador.";
885
+ }
886
+ if (error instanceof Error && error.message) return `N\xE3o foi poss\xEDvel ler o transcript: ${error.message}`;
887
+ return "N\xE3o foi poss\xEDvel ler o transcript da conversa.";
888
+ }
889
+ function ConversationPreview({
890
+ client,
891
+ sse,
892
+ conversationId,
893
+ loadMessages,
894
+ placeholder,
895
+ pollIntervalMs,
896
+ uploadMedia
897
+ }) {
898
+ const [messages, setMessages] = useState([]);
899
+ const [failure, setFailure] = useState(void 0);
900
+ const [loadFailure, setLoadFailure] = useState(void 0);
901
+ const [isRecording, setIsRecording] = useState(false);
902
+ const [pendingLocal, setPendingLocal] = useState([]);
903
+ const loadMessagesRef = useRef(loadMessages);
904
+ const bottomRef = useRef(null);
905
+ loadMessagesRef.current = loadMessages;
906
+ const refresh = useCallback(async () => {
907
+ try {
908
+ const loaded = await loadMessagesRef.current(conversationId);
909
+ setMessages(loaded);
910
+ setLoadFailure(void 0);
911
+ if (loaded.length > 0) setPendingLocal([]);
912
+ } catch (error) {
913
+ if (isNotFound(error)) {
914
+ setMessages([]);
915
+ setLoadFailure(void 0);
916
+ return;
917
+ }
918
+ setLoadFailure(describeLoadFailure(error));
919
+ }
920
+ }, [conversationId]);
921
+ useEffect(() => {
922
+ void refresh();
923
+ }, [refresh]);
924
+ useEffect(() => {
925
+ const source = sse.connectConversationStream(conversationId);
926
+ const handler = () => {
927
+ void refresh();
928
+ };
929
+ source.addEventListener("message", handler);
930
+ return () => {
931
+ source.removeEventListener("message", handler);
932
+ source.close();
933
+ };
934
+ }, [sse, conversationId, refresh]);
935
+ useEffect(() => {
936
+ if (!pollIntervalMs) return;
937
+ const timer = setInterval(() => void refresh(), pollIntervalMs);
938
+ return () => clearInterval(timer);
939
+ }, [pollIntervalMs, refresh]);
940
+ useEffect(() => {
941
+ bottomRef.current?.scrollIntoView({ behavior: "smooth", block: "nearest" });
942
+ }, [messages]);
943
+ const rendered = useMemo(() => decorate([...messages, ...pendingLocal]), [messages, pendingLocal]);
944
+ async function refreshWithFollowUps() {
945
+ await refresh();
946
+ for (const atraso of FOLLOW_UP_REFRESH_MS) {
947
+ setTimeout(() => void refresh(), atraso);
948
+ }
949
+ }
950
+ async function handleSend(text) {
951
+ setFailure(void 0);
952
+ try {
953
+ await client.sendText(text);
954
+ setPendingLocal((current) => [
955
+ ...current,
956
+ {
957
+ id: `local-${current.length}-${text.length}`,
958
+ type: "text",
959
+ content: text,
960
+ // Do ponto de vista do servidor, mensagem do cliente é inbound — é assim que ela aparece
961
+ // como "minha" nesta visão.
962
+ direction: "inbound",
963
+ sender: "customer",
964
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
965
+ status: "sent"
966
+ }
967
+ ]);
968
+ await refreshWithFollowUps();
969
+ } catch (error) {
970
+ setFailure(error instanceof Error ? error.message : "Falha ao entregar a mensagem no webhook.");
971
+ }
972
+ }
973
+ async function handleInteractiveSelect(selection) {
974
+ setFailure(void 0);
975
+ const reply = { id: selection.option.id, title: selection.option.title };
976
+ try {
977
+ await (selection.kind === "button" ? client.sendButtonReply(reply) : client.sendListReply(reply));
978
+ await refreshWithFollowUps();
979
+ } catch (error) {
980
+ setFailure(error instanceof Error ? error.message : "Falha ao entregar a resposta no webhook.");
981
+ }
982
+ }
983
+ const uploadFile = uploadMedia ?? client.uploadMedia;
984
+ async function handleAttach(file) {
985
+ if (!uploadFile) return;
986
+ setFailure(void 0);
987
+ try {
988
+ const uploaded = await uploadFile(file);
989
+ await client.sendMedia({
990
+ mediaType: mediaTypeOf(uploaded.mimeType ?? file.type),
991
+ mediaId: uploaded.mediaId,
992
+ mimeType: uploaded.mimeType ?? file.type,
993
+ filename: uploaded.filename ?? file.name
994
+ });
995
+ await refreshWithFollowUps();
996
+ } catch (error) {
997
+ setFailure(error instanceof Error ? error.message : "Falha ao enviar o arquivo.");
998
+ }
999
+ }
1000
+ return /* @__PURE__ */ jsxs("div", { className: "flex h-full min-h-0 flex-col", children: [
1001
+ /* @__PURE__ */ jsxs(ConversationWallpaper, { className: "flex-1 min-h-0 overflow-y-auto px-4 py-3", children: [
1002
+ rendered.map(({ message, isFirstInGroup, showDateDivider }) => /* @__PURE__ */ jsxs("div", { children: [
1003
+ showDateDivider ? /* @__PURE__ */ jsx(DateDivider, { iso: message.timestamp }) : null,
1004
+ /* @__PURE__ */ jsx(
1005
+ MessageBubble,
1006
+ {
1007
+ message,
1008
+ isMine: message.direction === "inbound",
1009
+ isFirstInGroup,
1010
+ onInteractiveSelect: message.direction === "outbound" ? (selection) => void handleInteractiveSelect(selection) : void 0
1011
+ }
1012
+ )
1013
+ ] }, message.id)),
1014
+ /* @__PURE__ */ jsx("div", { ref: bottomRef })
1015
+ ] }),
1016
+ failure ? /* @__PURE__ */ jsx("p", { role: "alert", className: "px-4 py-2 text-sm text-red-600 dark:text-red-400", children: failure }) : null,
1017
+ loadFailure ? /* @__PURE__ */ jsx("p", { role: "status", className: "px-4 py-2 text-sm text-amber-700 dark:text-amber-400", children: loadFailure }) : null,
1018
+ /* @__PURE__ */ jsx(
1019
+ MessageComposer,
1020
+ {
1021
+ onSend: (text) => void handleSend(text),
1022
+ onAttach: uploadFile ? (file) => void handleAttach(file) : void 0,
1023
+ placeholder: isRecording ? "Gravando\u2026 toque no quadrado para ouvir" : placeholder ?? "Escreva como o cliente\u2026",
1024
+ idleAction: uploadFile ? /* @__PURE__ */ jsx(
1025
+ AudioRecorderButton,
1026
+ {
1027
+ onRecorded: (file) => void handleAttach(file),
1028
+ onFailure: (message) => setFailure(message),
1029
+ onRecordingChange: setIsRecording
1030
+ }
1031
+ ) : void 0
1032
+ }
1033
+ )
1034
+ ] });
1035
+ }
1036
+
1037
+ // src/preview/createPreviewWebhookClient.ts
1038
+ import {
1039
+ buildInboundAudioPayload,
1040
+ buildInboundInteractivePayload,
1041
+ buildInboundMediaPayload,
1042
+ buildInboundTextPayload,
1043
+ serializeWebhookPayload
1044
+ } from "@adatechnology/meta-whatsapp-contracts/testing";
1045
+
1046
+ // src/preview/createPreviewMediaUploader.ts
1047
+ import { PREVIEW_MEDIA_ID_PREFIX } from "@adatechnology/meta-whatsapp-contracts";
1048
+ import { toPreviewMediaId } from "@adatechnology/meta-whatsapp-contracts";
1049
+ var CHUNK_SIZE = 8192;
1050
+ async function fileToBase64(file) {
1051
+ const bytes = new Uint8Array(await file.arrayBuffer());
1052
+ let binary = "";
1053
+ for (let offset = 0; offset < bytes.length; offset += CHUNK_SIZE) {
1054
+ binary += String.fromCharCode(...bytes.subarray(offset, offset + CHUNK_SIZE));
1055
+ }
1056
+ return btoa(binary);
1057
+ }
1058
+ function createPreviewMediaUploader(params) {
1059
+ const fallbackMimeType = params.fallbackMimeType ?? "audio/ogg";
1060
+ const fallbackFilename = params.fallbackFilename ?? "audio.ogg";
1061
+ return async function uploadPreviewMedia(file) {
1062
+ const mimeType = file.type || fallbackMimeType;
1063
+ const filename = file.name || fallbackFilename;
1064
+ const { uploadId } = await params.upload({ base64: await fileToBase64(file), mimeType, filename });
1065
+ return { mediaId: toPreviewMediaId(uploadId), mimeType, filename };
1066
+ };
1067
+ }
1068
+
1069
+ // src/preview/createPreviewWebhookClient.ts
1070
+ var PreviewMediaUploadRejectedError = class extends Error {
1071
+ constructor(status) {
1072
+ super(`A rota de m\xEDdia do simulador recusou o upload (HTTP ${status}).`);
1073
+ this.status = status;
1074
+ this.name = "PreviewMediaUploadRejectedError";
1075
+ }
1076
+ };
1077
+ var PreviewInProductionError = class extends Error {
1078
+ constructor() {
1079
+ super("O preview de conversa carrega um app secret e n\xE3o pode ser montado em produ\xE7\xE3o.");
1080
+ this.name = "PreviewInProductionError";
1081
+ }
1082
+ };
1083
+ var PreviewWebhookRejectedError = class extends Error {
1084
+ constructor(status) {
1085
+ super(`O webhook recusou a entrega do preview (HTTP ${status}).`);
1086
+ this.status = status;
1087
+ this.name = "PreviewWebhookRejectedError";
1088
+ }
1089
+ };
1090
+ function assertPreviewEnvironment(isProduction) {
1091
+ if (isProduction) throw new PreviewInProductionError();
1092
+ }
1093
+ async function signPreviewPayload(params) {
1094
+ const encoder = new TextEncoder();
1095
+ const key = await globalThis.crypto.subtle.importKey(
1096
+ "raw",
1097
+ encoder.encode(params.appSecret),
1098
+ { name: "HMAC", hash: "SHA-256" },
1099
+ false,
1100
+ ["sign"]
1101
+ );
1102
+ const signature = await globalThis.crypto.subtle.sign("HMAC", key, encoder.encode(params.rawBody));
1103
+ return `sha256=${[...new Uint8Array(signature)].map((byte) => byte.toString(16).padStart(2, "0")).join("")}`;
1104
+ }
1105
+ var signWithWebCrypto = signPreviewPayload;
1106
+ var DEFAULT_MEDIA_UPLOAD_PATH = "/v1/preview/media";
1107
+ function defaultMediaUploadUrl(webhookUrl) {
1108
+ try {
1109
+ return new URL(DEFAULT_MEDIA_UPLOAD_PATH, webhookUrl).toString();
1110
+ } catch {
1111
+ return DEFAULT_MEDIA_UPLOAD_PATH;
1112
+ }
1113
+ }
1114
+ function createPreviewMediaPoster(params) {
1115
+ return createPreviewMediaUploader({
1116
+ upload: async (request) => {
1117
+ const performRequest = params.fetchImplementation ?? fetch;
1118
+ const extraHeaders = await params.headers?.(request.mimeType) ?? {};
1119
+ const response = await performRequest(params.url, {
1120
+ method: "POST",
1121
+ headers: { "content-type": "application/json", ...extraHeaders },
1122
+ body: JSON.stringify(request)
1123
+ });
1124
+ if (!response.ok) throw new PreviewMediaUploadRejectedError(response.status);
1125
+ const body = await response.json();
1126
+ return { uploadId: body.data.uploadId };
1127
+ }
1128
+ });
1129
+ }
1130
+ function createPreviewWebhookClient(params) {
1131
+ const sendPayload = async (payload) => {
1132
+ const rawBody = serializeWebhookPayload(payload);
1133
+ const signature = await signWithWebCrypto({ rawBody, appSecret: params.appSecret });
1134
+ const performRequest = params.fetchImplementation ?? fetch;
1135
+ const response = await performRequest(params.webhookUrl, {
1136
+ method: "POST",
1137
+ headers: { "content-type": "application/json", "x-hub-signature-256": signature },
1138
+ body: rawBody
1139
+ });
1140
+ if (!response.ok) throw new PreviewWebhookRejectedError(response.status);
1141
+ };
1142
+ const uploadMedia = createPreviewMediaPoster({
1143
+ url: params.mediaUploadUrl ?? defaultMediaUploadUrl(params.webhookUrl),
1144
+ headers: async (mimeType) => ({
1145
+ "x-preview-signature": await signWithWebCrypto({ rawBody: mimeType, appSecret: params.appSecret })
1146
+ }),
1147
+ ...params.fetchImplementation ? { fetchImplementation: params.fetchImplementation } : {}
1148
+ });
1149
+ const envelope = { from: params.from, phoneNumberId: params.phoneNumberId };
1150
+ return {
1151
+ sendText: (text) => sendPayload(buildInboundTextPayload({ ...envelope, text })),
1152
+ sendButtonReply: (reply) => sendPayload(buildInboundInteractivePayload({ ...envelope, buttonReply: reply })),
1153
+ sendListReply: (reply) => sendPayload(buildInboundInteractivePayload({ ...envelope, listReply: reply })),
1154
+ sendAudio: (mediaId) => sendPayload(buildInboundAudioPayload({ ...envelope, mediaId })),
1155
+ sendMedia: (media) => sendPayload(buildInboundMediaPayload({ ...envelope, ...media })),
1156
+ uploadMedia
1157
+ };
1158
+ }
1159
+
1160
+ // src/preview/createPreviewBridgeClient.ts
1161
+ var PreviewBridgeRejectedError = class extends Error {
1162
+ constructor(status) {
1163
+ super(`A rota de preview do host recusou a entrega (HTTP ${status}).`);
1164
+ this.status = status;
1165
+ this.name = "PreviewBridgeRejectedError";
1166
+ }
1167
+ };
1168
+ function buildFetchSender(params) {
1169
+ const endpointUrl = params.endpointUrl;
1170
+ if (!endpointUrl) {
1171
+ throw new Error("createPreviewBridgeClient exige `sendCommand` ou `endpointUrl`.");
1172
+ }
1173
+ return async (command) => {
1174
+ const performRequest = params.fetchImplementation ?? fetch;
1175
+ const response = await performRequest(endpointUrl, {
1176
+ method: "POST",
1177
+ // `credentials` fica com o host via `headers`/`fetchImplementation`: sessão por cookie e por
1178
+ // bearer não convivem numa escolha default sem quebrar um dos dois.
1179
+ headers: { "content-type": "application/json", ...params.headers },
1180
+ body: JSON.stringify(command)
1181
+ });
1182
+ if (!response.ok) throw new PreviewBridgeRejectedError(response.status);
1183
+ };
1184
+ }
1185
+ function resolveBridgeUpload(params) {
1186
+ if (params.uploadMedia) return params.uploadMedia;
1187
+ const url = params.mediaUploadUrl ?? (params.endpointUrl ? defaultMediaUploadUrl(params.endpointUrl) : void 0);
1188
+ if (!url) return void 0;
1189
+ return createPreviewMediaPoster({
1190
+ url,
1191
+ ...params.headers ? { headers: async () => params.headers ?? {} } : {},
1192
+ ...params.fetchImplementation ? { fetchImplementation: params.fetchImplementation } : {}
1193
+ });
1194
+ }
1195
+ function createPreviewBridgeClient(params) {
1196
+ const send = params.sendCommand ?? buildFetchSender(params);
1197
+ const from = params.from;
1198
+ const uploadMedia = resolveBridgeUpload(params);
1199
+ return {
1200
+ sendText: (text) => send({ kind: "text", from, text }),
1201
+ sendButtonReply: (reply) => send({ kind: "buttonReply", from, reply }),
1202
+ sendListReply: (reply) => send({ kind: "listReply", from, reply }),
1203
+ sendAudio: (mediaId) => send({ kind: "audio", from, mediaId }),
1204
+ sendMedia: (media) => send({ kind: "media", from, ...media }),
1205
+ ...uploadMedia ? { uploadMedia } : {}
1206
+ };
1207
+ }
1208
+
1209
+ // src/preview/startPreviewScript.ts
1210
+ var DEFAULT_PREVIEW_SCRIPT = [
1211
+ (store) => store.appendMessage({
1212
+ conversationId: "5511988887777",
1213
+ content: "pode trocar o \xF3leo por azeite?",
1214
+ direction: "inbound",
1215
+ sender: "customer"
1216
+ }),
1217
+ (store) => store.requestHuman("5511988887777"),
1218
+ (store) => store.appendMessage({
1219
+ conversationId: "5511955554444",
1220
+ content: "e o troco?",
1221
+ direction: "inbound",
1222
+ sender: "customer"
1223
+ }),
1224
+ (store) => store.setMode({ conversationId: "5511977776666", mode: "human", assignedUserId: "agent-2" }),
1225
+ (store) => store.appendMessage({
1226
+ conversationId: "5511977776666",
1227
+ content: "Oi Diego, sou a Ana. J\xE1 vi seu pedido.",
1228
+ direction: "outbound",
1229
+ sender: "agent"
1230
+ }),
1231
+ (store) => store.setMode({ conversationId: "5511966665555", mode: "bot" })
1232
+ ];
1233
+ var DEFAULT_INTERVAL_MS = 4e3;
1234
+ function startPreviewScript(params) {
1235
+ const steps = params.steps ?? DEFAULT_PREVIEW_SCRIPT;
1236
+ const intervalMs = params.intervalMs ?? DEFAULT_INTERVAL_MS;
1237
+ let index = 0;
1238
+ const timer = setInterval(() => {
1239
+ steps[index % steps.length]?.(params.store);
1240
+ index += 1;
1241
+ }, intervalMs);
1242
+ return () => clearInterval(timer);
1243
+ }
1244
+
1245
+ // src/preview/MediaTypesPreview.tsx
1246
+ import { useMemo as useMemo2 } from "react";
1247
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
1248
+ var MEDIA_TYPES_CONVERSATION_ID = "5511944443333";
1249
+ function MediaTypesPreview({
1250
+ conversationId = MEDIA_TYPES_CONVERSATION_ID,
1251
+ className
1252
+ }) {
1253
+ const store = useMemo2(
1254
+ () => createPreviewStore({ conversations: PREVIEW_CONVERSATIONS, messages: PREVIEW_MESSAGES }),
1255
+ []
1256
+ );
1257
+ const api = useMemo2(() => createMockConversationsApi({ store }), [store]);
1258
+ const sse = useMemo2(() => createMockSSEProvider({ store }), [store]);
1259
+ const messages = PREVIEW_MESSAGES[conversationId] ?? [];
1260
+ const documents = PREVIEW_DOCUMENTS[conversationId] ?? [];
1261
+ const mimeTypes = [...new Set(documents.map((document) => document.mimeType))];
1262
+ return /* @__PURE__ */ jsx2(ConversationsProvider, { api, sse, children: /* @__PURE__ */ jsxs2("div", { className, children: [
1263
+ /* @__PURE__ */ jsxs2("header", { className: "border-b px-4 py-3 dark:border-gray-700", children: [
1264
+ /* @__PURE__ */ jsx2("h1", { className: "text-lg font-semibold", children: "Teste manual de m\xEDdia" }),
1265
+ /* @__PURE__ */ jsxs2("p", { className: "text-sm text-gray-500", children: [
1266
+ documents.length,
1267
+ " arquivos, ",
1268
+ mimeTypes.length,
1269
+ " 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."
1270
+ ] })
1271
+ ] }),
1272
+ /* @__PURE__ */ jsxs2("div", { className: "grid gap-4 p-4 lg:grid-cols-2", children: [
1273
+ /* @__PURE__ */ jsxs2("section", { className: "space-y-3", children: [
1274
+ /* @__PURE__ */ jsx2("h2", { className: "text-sm font-semibold uppercase tracking-wide text-gray-500", children: "Biblioteca da empresa" }),
1275
+ /* @__PURE__ */ jsx2(DocumentsLibrary, { perPage: documents.length || 20 })
1276
+ ] }),
1277
+ /* @__PURE__ */ jsxs2("section", { className: "space-y-3", children: [
1278
+ /* @__PURE__ */ jsx2("h2", { className: "text-sm font-semibold uppercase tracking-wide text-gray-500", children: "Painel da conversa" }),
1279
+ /* @__PURE__ */ jsx2(ConversationDocumentsPanel, { conversationId, open: true, perPage: documents.length || 20 }),
1280
+ /* @__PURE__ */ jsx2("h2", { className: "text-sm font-semibold uppercase tracking-wide text-gray-500", children: "Bolhas na thread" }),
1281
+ /* @__PURE__ */ jsx2(ConversationWallpaper, { className: "max-h-[70vh] overflow-y-auto rounded-lg px-3 py-2", children: messages.map((message, index) => /* @__PURE__ */ jsx2(
1282
+ MessageBubble,
1283
+ {
1284
+ message,
1285
+ isMine: message.direction === "outbound",
1286
+ isFirstInGroup: index === 0 || messages[index - 1]?.sender !== message.sender
1287
+ },
1288
+ message.id
1289
+ )) })
1290
+ ] })
1291
+ ] })
1292
+ ] }) });
1293
+ }
1294
+ export {
1295
+ AudioRecorderButton,
1296
+ ConversationPreview,
1297
+ DEFAULT_AUDIO_RECORDER_BUTTON_LABELS,
1298
+ DEFAULT_MEDIA_UPLOAD_PATH,
1299
+ DEFAULT_PREVIEW_SCRIPT,
1300
+ GLOBAL_CHANNEL,
1301
+ MEDIA_TYPES_CONVERSATION_ID,
1302
+ MediaTypesPreview,
1303
+ PREVIEW_CONVERSATIONS,
1304
+ PREVIEW_DOCUMENTS,
1305
+ PREVIEW_FILE_SAMPLES,
1306
+ PREVIEW_MEDIA_ID_PREFIX,
1307
+ PREVIEW_MESSAGES,
1308
+ PreviewBridgeRejectedError,
1309
+ PreviewInProductionError,
1310
+ PreviewMediaUploadRejectedError,
1311
+ PreviewWebhookRejectedError,
1312
+ assertPreviewEnvironment,
1313
+ conversationChannel,
1314
+ createMockConversationsApi,
1315
+ createMockEventSource,
1316
+ createMockSSEProvider,
1317
+ createPreviewBridgeClient,
1318
+ createPreviewMediaPoster,
1319
+ createPreviewMediaResolver,
1320
+ createPreviewMediaUploader,
1321
+ createPreviewStore,
1322
+ createPreviewWebhookClient,
1323
+ mediaTypeOf,
1324
+ previewFileBase64,
1325
+ previewFileUrl,
1326
+ resolvePreviewFileSample,
1327
+ signPreviewPayload,
1328
+ startPreviewScript
1329
+ };