@adatechnology/conversations-ui 0.1.0-rc.5 → 0.1.0-rc.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-LITPZCWW.js +1465 -0
- package/dist/index.d.ts +135 -18
- package/dist/index.js +228 -589
- package/dist/preview/index.d.ts +82 -7
- package/dist/preview/index.js +314 -11
- package/dist/{types-C2Yexi8A.d.ts → types-CeixG2Z9.d.ts} +28 -1
- package/package.json +1 -1
- package/src/Avatar.tsx +13 -2
- package/src/ConversationListItem.tsx +18 -2
- package/src/DocumentsLibrary.tsx +322 -0
- package/src/FileIcon.test.ts +46 -1
- package/src/FileIcon.tsx +76 -10
- package/src/Lightbox.tsx +18 -3
- package/src/MessageBubble.tsx +18 -2
- package/src/MessageComposer.tsx +20 -3
- package/src/WhatsAppMessageEditor.tsx +28 -4
- package/src/index.ts +17 -10
- package/src/lib/createMediaUrlResolver.ts +33 -0
- package/src/preview/MediaTypesPreview.tsx +87 -0
- package/src/preview/createMockConversationsApi.ts +62 -8
- package/src/preview/index.ts +5 -0
- package/src/preview/mockDocumentsSearch.test.ts +57 -0
- package/src/preview/previewFileSamples.test.ts +151 -0
- package/src/preview/previewFileSamples.ts +74 -0
- package/src/preview/previewFixtures.ts +140 -8
- package/src/preview/previewMediaSource.test.ts +62 -0
- package/src/preview/previewMediaSource.ts +91 -0
- package/src/providers/types.ts +17 -0
- package/src/settings/WhatsAppCreateTemplateForm.tsx +3 -1
- package/dist/chunk-YWITIIHD.js +0 -728
package/dist/preview/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { v as MessagePayload, m as ConversationSummary, k as ConversationEventSource, o as ConversationsApi, L as ListConversationsParams, l as ConversationPage, S as SSEProvider, i as ConversationDocument, x as ResolveMediaUrl } from '../types-CeixG2Z9.js';
|
|
2
2
|
import * as react from 'react';
|
|
3
3
|
import { InteractiveReplyOption } from '@adatechnology/meta-whatsapp-contracts/testing';
|
|
4
4
|
|
|
@@ -109,11 +109,6 @@ declare function createMockSSEProvider(params: CreateMockSSEProviderParams): SSE
|
|
|
109
109
|
|
|
110
110
|
declare const PREVIEW_CONVERSATIONS: readonly ConversationSummary[];
|
|
111
111
|
declare const PREVIEW_MESSAGES: Readonly<Record<string, readonly MessagePayload[]>>;
|
|
112
|
-
/**
|
|
113
|
-
* A biblioteca de arquivos da conversa, como o backend a devolveria. Deriva das mensagens de
|
|
114
|
-
* documento acima em vez de repetir os dados: fixture duplicada divergiria na primeira edição, e o
|
|
115
|
-
* painel passaria a mostrar arquivo que a thread não tem.
|
|
116
|
-
*/
|
|
117
112
|
declare const PREVIEW_DOCUMENTS: Readonly<Record<string, readonly ConversationDocument[]>>;
|
|
118
113
|
|
|
119
114
|
/**
|
|
@@ -183,4 +178,84 @@ type StartPreviewScriptParams = {
|
|
|
183
178
|
};
|
|
184
179
|
declare function startPreviewScript(params: StartPreviewScriptParams): () => void;
|
|
185
180
|
|
|
186
|
-
|
|
181
|
+
/**
|
|
182
|
+
* Bytes de verdade para o preview abrir.
|
|
183
|
+
*
|
|
184
|
+
* O mock antes devolvia a MESMA imagem PNG para qualquer documento, então clicar em "visualizar" num
|
|
185
|
+
* PDF entregava um PNG rotulado `application/pdf` e o leitor dizia que o arquivo era inválido. Cada
|
|
186
|
+
* amostra aqui é um arquivo mínimo porém legítimo do seu tipo: as de mídia foram geradas com ffmpeg
|
|
187
|
+
* e conferidas com ffprobe, o PDF tem xref/startxref/%%EOF, e os pacotes Office são zips com as
|
|
188
|
+
* partes obrigatórias (`[Content_Types].xml`, `_rels/.rels`, o documento).
|
|
189
|
+
*
|
|
190
|
+
* Não há amostra de `audio/amr`, `video/3gp` nem dos formatos binários legados do Office
|
|
191
|
+
* (`application/msword`, `application/vnd.ms-excel`, `application/vnd.ms-powerpoint`): não há
|
|
192
|
+
* encoder para eles nesta máquina e forjar OLE2 à mão daria arquivo corrompido — exatamente o que
|
|
193
|
+
* este módulo existe para evitar. Esses caem no texto explicativo de `resolvePreviewFileSample`,
|
|
194
|
+
* que abre sem erro e diz o que é.
|
|
195
|
+
*
|
|
196
|
+
* GERADO — não editar à mão.
|
|
197
|
+
*/
|
|
198
|
+
declare const PREVIEW_FILE_SAMPLES: Readonly<Record<string, string>>;
|
|
199
|
+
/**
|
|
200
|
+
* A data URL que o preview abre para um documento.
|
|
201
|
+
*
|
|
202
|
+
* Ordem: tipo exato → família (`audio/amr` toca como mp3, `video/3gp` como mp4) → nota em texto.
|
|
203
|
+
* A nota fecha o caso dos tipos sem amostra: abre num leitor de texto e explica, em vez de baixar
|
|
204
|
+
* um binário quebrado com extensão de Word.
|
|
205
|
+
*/
|
|
206
|
+
declare function resolvePreviewFileSample(mimeType: string | undefined, filename?: string): string;
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Como o preview entrega mídia para a UI abrir.
|
|
210
|
+
*
|
|
211
|
+
* Separado de `previewFileSamples` (que é gerado) porque aqui está a parte que depende do navegador:
|
|
212
|
+
* **blob URL, não data URL**. O Chrome bloqueia navegação de topo para `data:` desde a v60, então
|
|
213
|
+
* `window.open(dataUrl)` — que é o que os botões "visualizar" e "baixar" fazem — abre aba em branco
|
|
214
|
+
* mesmo com bytes perfeitamente válidos. Em `src` de `<img>`/`<video>` a data URL funcionaria; no
|
|
215
|
+
* `window.open` não, e o mesmo `getDocumentUrl` alimenta os dois.
|
|
216
|
+
*/
|
|
217
|
+
|
|
218
|
+
/** URL que o preview pode abrir em aba nova. Cai na data URL fora do navegador (teste, SSR). */
|
|
219
|
+
declare function previewFileUrl(mimeType: string | undefined, filename?: string): string;
|
|
220
|
+
/**
|
|
221
|
+
* A mesma amostra em base64 cru, como o proxy de mídia do backend a devolveria.
|
|
222
|
+
*
|
|
223
|
+
* Existe porque `getMediaProxyUrl` é o caminho da mídia AINDA NÃO ingerida (só existe o id na Meta),
|
|
224
|
+
* e o contrato pede `{ mimeType, data }` — não URL. Sem isto o mock devolvia o PNG 1x1 para
|
|
225
|
+
* qualquer id, e vídeo e áudio da thread apareciam quebrados apesar de haver amostra válida.
|
|
226
|
+
*/
|
|
227
|
+
declare function previewFileBase64(mimeType: string | undefined, filename?: string): {
|
|
228
|
+
mimeType: string;
|
|
229
|
+
data: string;
|
|
230
|
+
};
|
|
231
|
+
/**
|
|
232
|
+
* O `onResolveMediaUrl` que o `MessageBubble` espera.
|
|
233
|
+
*
|
|
234
|
+
* Sem ele, foto, vídeo e áudio da thread ficam parados no placeholder para sempre — o
|
|
235
|
+
* `MediaRenderer` só resolve `uploadId`/`mediaId` por esta porta, de propósito, para o pacote nunca
|
|
236
|
+
* chamar endpoint fixo. O preview não tinha resolvedor nenhum, então nenhuma mídia carregava.
|
|
237
|
+
*/
|
|
238
|
+
declare function createPreviewMediaResolver(): ResolveMediaUrl;
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Bancada de teste manual de mídia — montável em uma linha por qualquer projeto que adote o SDK.
|
|
242
|
+
*
|
|
243
|
+
* Existe porque o defeito que ela pega não é pegável por teste automatizado: "o PDF abre?" depende
|
|
244
|
+
* do leitor do navegador, "o vídeo toca?" do decodificador, e "a aba abre?" da política do Chrome
|
|
245
|
+
* sobre `data:` URL. Teste unitário confere bytes; só o olho confere que o arquivo abre. Sem uma
|
|
246
|
+
* superfície pronta no pacote, cada projeto teria de montar a sua — e, na prática, nenhum montava.
|
|
247
|
+
*
|
|
248
|
+
* Traz o próprio store, o próprio mock e o próprio provider: o host não injeta nada. E não passa
|
|
249
|
+
* `onResolveMediaUrl` em lugar nenhum de propósito — é o `MessageBubble` resolvendo mídia pelo
|
|
250
|
+
* `ConversationsApi` do contexto, então se essa resolução automática quebrar, esta tela mostra.
|
|
251
|
+
*/
|
|
252
|
+
/** A conversa do fixture que carrega uma mensagem de cada tipo aceito. */
|
|
253
|
+
declare const MEDIA_TYPES_CONVERSATION_ID = "5511944443333";
|
|
254
|
+
type MediaTypesPreviewProps = {
|
|
255
|
+
/** Outra conversa do fixture, se o projeto tiver acrescentado a sua. */
|
|
256
|
+
conversationId?: string;
|
|
257
|
+
className?: string;
|
|
258
|
+
};
|
|
259
|
+
declare function MediaTypesPreview({ conversationId, className, }: MediaTypesPreviewProps): react.JSX.Element;
|
|
260
|
+
|
|
261
|
+
export { type AppendMessageParams, ConversationPreview, type ConversationPreviewProps, type CreateMockConversationsApiParams, type CreateMockSSEProviderParams, type CreatePreviewStoreParams, type CreatePreviewWebhookClientParams, DEFAULT_PREVIEW_SCRIPT, GLOBAL_CHANNEL, type ListConversationsFilters, MEDIA_TYPES_CONVERSATION_ID, MediaTypesPreview, type MediaTypesPreviewProps, type MockEventSource, PREVIEW_CONVERSATIONS, PREVIEW_DOCUMENTS, PREVIEW_FILE_SAMPLES, PREVIEW_MESSAGES, type PreviewEmission, PreviewInProductionError, type PreviewScriptStep, type PreviewStore, type PreviewStoreListener, type PreviewWebhookClient, PreviewWebhookRejectedError, type SetModeParams, type StartPreviewScriptParams, assertPreviewEnvironment, conversationChannel, createMockConversationsApi, createMockEventSource, createMockSSEProvider, createPreviewMediaResolver, createPreviewStore, createPreviewWebhookClient, previewFileBase64, previewFileUrl, resolvePreviewFileSample, startPreviewScript };
|
package/dist/preview/index.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
|
+
ConversationDocumentsPanel,
|
|
2
3
|
ConversationWallpaper,
|
|
4
|
+
ConversationsProvider,
|
|
3
5
|
DateDivider,
|
|
6
|
+
DocumentsLibrary,
|
|
4
7
|
MessageBubble,
|
|
5
8
|
MessageComposer
|
|
6
|
-
} from "../chunk-
|
|
9
|
+
} from "../chunk-LITPZCWW.js";
|
|
7
10
|
import "../chunk-OGRRHQQW.js";
|
|
8
11
|
|
|
9
12
|
// src/preview/previewStore.ts
|
|
@@ -399,8 +402,7 @@ var PREVIEW_MESSAGES = {
|
|
|
399
402
|
timestamp: at("15:09:00")
|
|
400
403
|
},
|
|
401
404
|
{
|
|
402
|
-
|
|
403
|
-
id: "fixture-doc-generic",
|
|
405
|
+
id: "fixture-doc-txt",
|
|
404
406
|
type: "document",
|
|
405
407
|
uploadId: "preview/documentos/lista-compras.txt",
|
|
406
408
|
filename: "lista-compras.txt",
|
|
@@ -409,13 +411,140 @@ var PREVIEW_MESSAGES = {
|
|
|
409
411
|
direction: "inbound",
|
|
410
412
|
sender: "customer",
|
|
411
413
|
timestamp: at("15:10:00")
|
|
414
|
+
},
|
|
415
|
+
{
|
|
416
|
+
id: "fixture-doc-csv",
|
|
417
|
+
type: "document",
|
|
418
|
+
uploadId: "preview/documentos/planilha-do-pedido.csv",
|
|
419
|
+
filename: "planilha-do-pedido.csv",
|
|
420
|
+
mimeType: "text/csv",
|
|
421
|
+
sizeBytes: 2048,
|
|
422
|
+
direction: "inbound",
|
|
423
|
+
sender: "customer",
|
|
424
|
+
timestamp: at("15:11:00")
|
|
425
|
+
},
|
|
426
|
+
{
|
|
427
|
+
id: "fixture-doc-pptx",
|
|
428
|
+
type: "document",
|
|
429
|
+
uploadId: "preview/documentos/apresentacao.pptx",
|
|
430
|
+
filename: "apresentacao.pptx",
|
|
431
|
+
mimeType: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
432
|
+
sizeBytes: 512e3,
|
|
433
|
+
direction: "inbound",
|
|
434
|
+
sender: "customer",
|
|
435
|
+
timestamp: at("15:12:00")
|
|
436
|
+
},
|
|
437
|
+
{
|
|
438
|
+
id: "fixture-doc-ppt",
|
|
439
|
+
type: "document",
|
|
440
|
+
uploadId: "preview/documentos/antiga.ppt",
|
|
441
|
+
filename: "antiga.ppt",
|
|
442
|
+
mimeType: "application/vnd.ms-powerpoint",
|
|
443
|
+
sizeBytes: 348160,
|
|
444
|
+
direction: "inbound",
|
|
445
|
+
sender: "customer",
|
|
446
|
+
timestamp: at("15:13:00")
|
|
447
|
+
},
|
|
448
|
+
// Mídia enviada COMO arquivo: aí a Meta manda o nome, então é onde a extensão de música e de
|
|
449
|
+
// vídeo aparece na lista. Mandada como `image`/`audio`/`video` (acima e abaixo) ela chega sem
|
|
450
|
+
// nome nenhum, e o rótulo vira o id da mídia — como em produção.
|
|
451
|
+
{
|
|
452
|
+
id: "fixture-doc-mp3",
|
|
453
|
+
type: "document",
|
|
454
|
+
uploadId: "preview/documentos/musica-do-anuncio.mp3",
|
|
455
|
+
filename: "musica-do-anuncio.mp3",
|
|
456
|
+
mimeType: "audio/mpeg",
|
|
457
|
+
sizeBytes: 3145728,
|
|
458
|
+
direction: "inbound",
|
|
459
|
+
sender: "customer",
|
|
460
|
+
timestamp: at("15:14:00")
|
|
461
|
+
},
|
|
462
|
+
{
|
|
463
|
+
id: "fixture-doc-m4a",
|
|
464
|
+
type: "document",
|
|
465
|
+
uploadId: "preview/documentos/recado.m4a",
|
|
466
|
+
filename: "recado.m4a",
|
|
467
|
+
mimeType: "audio/mp4",
|
|
468
|
+
sizeBytes: 486400,
|
|
469
|
+
direction: "inbound",
|
|
470
|
+
sender: "customer",
|
|
471
|
+
timestamp: at("15:15:00")
|
|
472
|
+
},
|
|
473
|
+
{
|
|
474
|
+
id: "fixture-doc-mp4-file",
|
|
475
|
+
type: "document",
|
|
476
|
+
uploadId: "preview/documentos/video-do-produto.mp4",
|
|
477
|
+
filename: "video-do-produto.mp4",
|
|
478
|
+
mimeType: "video/mp4",
|
|
479
|
+
sizeBytes: 8388608,
|
|
480
|
+
direction: "inbound",
|
|
481
|
+
sender: "customer",
|
|
482
|
+
timestamp: at("15:16:00")
|
|
483
|
+
},
|
|
484
|
+
{
|
|
485
|
+
id: "fixture-doc-jpg",
|
|
486
|
+
type: "document",
|
|
487
|
+
uploadId: "preview/documentos/foto-do-recibo.jpg",
|
|
488
|
+
filename: "foto-do-recibo.jpg",
|
|
489
|
+
mimeType: "image/jpeg",
|
|
490
|
+
sizeBytes: 262144,
|
|
491
|
+
direction: "inbound",
|
|
492
|
+
sender: "customer",
|
|
493
|
+
timestamp: at("15:17:00")
|
|
494
|
+
},
|
|
495
|
+
// As espécies restantes que a Meta aceita, sem nome de arquivo — fecham a lista de tipos
|
|
496
|
+
// aceitos e exercitam o fallback de ícone pela família do mimeType.
|
|
497
|
+
{
|
|
498
|
+
id: "fixture-media-jpeg",
|
|
499
|
+
type: "image",
|
|
500
|
+
mediaId: "preview-image-jpeg",
|
|
501
|
+
mimeType: "image/jpeg",
|
|
502
|
+
sizeBytes: 138240,
|
|
503
|
+
direction: "inbound",
|
|
504
|
+
sender: "customer",
|
|
505
|
+
timestamp: at("15:18:00")
|
|
506
|
+
},
|
|
507
|
+
{
|
|
508
|
+
id: "fixture-media-3gp",
|
|
509
|
+
type: "video",
|
|
510
|
+
mediaId: "preview-video-3gp",
|
|
511
|
+
mimeType: "video/3gp",
|
|
512
|
+
sizeBytes: 921600,
|
|
513
|
+
direction: "inbound",
|
|
514
|
+
sender: "customer",
|
|
515
|
+
timestamp: at("15:19:00")
|
|
516
|
+
},
|
|
517
|
+
{
|
|
518
|
+
id: "fixture-media-aac",
|
|
519
|
+
type: "audio",
|
|
520
|
+
mediaId: "preview-audio-aac",
|
|
521
|
+
mimeType: "audio/aac",
|
|
522
|
+
sizeBytes: 76800,
|
|
523
|
+
direction: "inbound",
|
|
524
|
+
sender: "customer",
|
|
525
|
+
timestamp: at("15:20:00")
|
|
526
|
+
},
|
|
527
|
+
{
|
|
528
|
+
id: "fixture-media-amr",
|
|
529
|
+
type: "audio",
|
|
530
|
+
mediaId: "preview-audio-amr",
|
|
531
|
+
mimeType: "audio/amr",
|
|
532
|
+
sizeBytes: 24576,
|
|
533
|
+
direction: "inbound",
|
|
534
|
+
sender: "customer",
|
|
535
|
+
timestamp: at("15:21:00")
|
|
412
536
|
}
|
|
413
537
|
]
|
|
414
538
|
};
|
|
539
|
+
var LIBRARY_MEDIA_TYPES = /* @__PURE__ */ new Set(["document", "image", "video", "audio", "sticker"]);
|
|
415
540
|
var PREVIEW_DOCUMENTS = {
|
|
416
|
-
"5511944443333": (PREVIEW_MESSAGES["5511944443333"] ?? []).filter((message) => message.type
|
|
417
|
-
|
|
418
|
-
|
|
541
|
+
"5511944443333": (PREVIEW_MESSAGES["5511944443333"] ?? []).filter((message) => LIBRARY_MEDIA_TYPES.has(message.type)).map((message) => ({
|
|
542
|
+
// Mídia não tem `uploadId` na mensagem: o backend deriva a key do id da mídia na Meta, e o
|
|
543
|
+
// caminho abaixo imita esse formato para o download assinado receber algo do mesmo feitio.
|
|
544
|
+
id: message.uploadId ?? (message.mediaId ? `preview/inbound/${message.mediaId}` : message.id),
|
|
545
|
+
// Só documento chega nomeado. Para os outros o backend salva o id da mídia como rótulo —
|
|
546
|
+
// manter isso aqui é o que faz o ícone por família do mimeType ser exercitado de verdade.
|
|
547
|
+
filename: message.filename ?? message.mediaId ?? message.id,
|
|
419
548
|
mimeType: message.mimeType ?? "application/octet-stream",
|
|
420
549
|
sizeBytes: message.sizeBytes ?? 0,
|
|
421
550
|
source: message.sender,
|
|
@@ -423,8 +552,85 @@ var PREVIEW_DOCUMENTS = {
|
|
|
423
552
|
}))
|
|
424
553
|
};
|
|
425
554
|
|
|
555
|
+
// src/preview/previewFileSamples.ts
|
|
556
|
+
var PREVIEW_FILE_SAMPLES = {
|
|
557
|
+
"application/pdf": "data:application/pdf;base64,JVBERi0xLjQKMSAwIG9iajw8L1R5cGUvQ2F0YWxvZy9QYWdlcyAyIDAgUj4+ZW5kb2JqCjIgMCBvYmo8PC9UeXBlL1BhZ2VzL0tpZHNbMyAwIFJdL0NvdW50IDE+PmVuZG9iagozIDAgb2JqPDwvVHlwZS9QYWdlL1BhcmVudCAyIDAgUi9NZWRpYUJveFswIDAgMjYwIDExMF0vQ29udGVudHMgNCAwIFIvUmVzb3VyY2VzPDwvRm9udDw8L0YxIDUgMCBSPj4+Pj4+ZW5kb2JqCjQgMCBvYmo8PC9MZW5ndGggNjI+PnN0cmVhbQpCVCAvRjEgMTMgVGYgMTggNTUgVGQgKERvY3VtZW50byBkZSBleGVtcGxvIGRvIHByZXZpZXcpIFRqIEVUCmVuZHN0cmVhbWVuZG9iago1IDAgb2JqPDwvVHlwZS9Gb250L1N1YnR5cGUvVHlwZTEvQmFzZUZvbnQvSGVsdmV0aWNhPj5lbmRvYmoKeHJlZgowIDYKMDAwMDAwMDAwMCA2NTUzNSBmIAowMDAwMDAwMDA5IDAwMDAwIG4gCjAwMDAwMDAwNTIgMDAwMDAgbiAKMDAwMDAwMDEwMSAwMDAwMCBuIAowMDAwMDAwMjExIDAwMDAwIG4gCjAwMDAwMDAzMTcgMDAwMDAgbiAKdHJhaWxlcjw8L1NpemUgNi9Sb290IDEgMCBSPj4Kc3RhcnR4cmVmCjM3OAolJUVPRgo=",
|
|
558
|
+
"image/png": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAIAAADYYG7QAAAACXBIWXMAAAABAAAAAQBPJcTWAAAATElEQVR4nO3OMRHAIAAAMbhDWo2hCYPdu/xIh0RB5rPP+JN1O/AlVISKUBEqQkWoCBWhIlSEilARKkJFqAgVoSJUhIpQESpCRagIlRcgDwI25HmPQAAAAABJRU5ErkJggg==",
|
|
559
|
+
"image/jpeg": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAgAAAQABAAD//gAQTGF2YzYyLjExLjEwMAD/2wBDAAgEBAQEBAUFBQUFBQYGBgYGBgYGBgYGBgYHBwcICAgHBwcGBgcHCAgICAkJCQgICAgJCQoKCgwMCwsODg4RERT/xABMAAEBAAAAAAAAAAAAAAAAAAAABAEBAQAAAAAAAAAAAAAAAAAAAAYQAQAAAAAAAAAAAAAAAAAAAAARAQAAAAAAAAAAAAAAAAAAAAD/wAARCAAwADADASIAAhEAAxEA/9oADAMBAAIRAxEAPwCUBRpwAAAAAAAAAAAAAAAB/9k=",
|
|
560
|
+
"image/webp": "data:image/webp;base64,UklGRhoAAABXRUJQVlA4TA0AAAAvAAAAEAcQERGIiP4HAA==",
|
|
561
|
+
"video/mp4": "data:video/mp4;base64,AAAAIGZ0eXBpc29tAAACAGlzb21pc28yYXZjMW1wNDEAAANdbW9vdgAAAGxtdmhkAAAAAAAAAAAAAAAAAAAD6AAAASwAAQAAAQAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAoh0cmFrAAAAXHRraGQAAAADAAAAAAAAAAAAAAABAAAAAAAAASwAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAABAAAAAAEAAAABAAAAAAAAkZWR0cwAAABxlbHN0AAAAAAAAAAEAAAEsAAAIAAABAAAAAAIAbWRpYQAAACBtZGhkAAAAAAAAAAAAAAAAAAAoAAAAEABVxAAAAAAALWhkbHIAAAAAAAAAAHZpZGUAAAAAAAAAAAAAAABWaWRlb0hhbmRsZXIAAAABq21pbmYAAAAUdm1oZAAAAAEAAAAAAAAAAAAAACRkaW5mAAAAHGRyZWYAAAAAAAAAAQAAAAx1cmwgAAAAAQAAAWtzdGJsAAAAv3N0c2QAAAAAAAAAAQAAAK9hdmMxAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAEAAQABIAAAASAAAAAAAAAABFUxhdmM2Mi4xMS4xMDAgbGlieDI2NAAAAAAAAAAAAAAAGP//AAAANWF2Y0MBZAAK/+EAGGdkAAqs2UQmwEQAAAMABAAAAwBQPEiWWAEABmjr48siwP34+AAAAAAQcGFzcAAAAAEAAAABAAAAFGJ0cnQAAAAAAABO2gAAAAAAAAAYc3R0cwAAAAAAAAABAAAAAwAABAAAAAAUc3RzcwAAAAAAAAABAAAAAQAAAChjdHRzAAAAAAAAAAMAAAABAAAIAAAAAAEAAAwAAAAAAQAABAAAAAAcc3RzYwAAAAAAAAABAAAAAQAAAAMAAAABAAAAIHN0c3oAAAAAAAAAAAAAAAMAAALbAAAADgAAAAwAAAAUc3RjbwAAAAAAAAABAAADjQAAAGF1ZHRhAAAAWW1ldGEAAAAAAAAAIWhkbHIAAAAAAAAAAG1kaXJhcHBsAAAAAAAAAAAAAAAALGlsc3QAAAAkqXRvbwAAABxkYXRhAAAAAQAAAABMYXZmNjIuMy4xMDAAAAAIZnJlZQAAAv1tZGF0AAACrgYF//+q3EXpvebZSLeWLNgg2SPu73gyNjQgLSBjb3JlIDE2NSByMzIyMiBiMzU2MDVhIC0gSC4yNjQvTVBFRy00IEFWQyBjb2RlYyAtIENvcHlsZWZ0IDIwMDMtMjAyNSAtIGh0dHA6Ly93d3cudmlkZW9sYW4ub3JnL3gyNjQuaHRtbCAtIG9wdGlvbnM6IGNhYmFjPTEgcmVmPTMgZGVibG9jaz0xOjA6MCBhbmFseXNlPTB4MzoweDExMyBtZT1oZXggc3VibWU9NyBwc3k9MSBwc3lfcmQ9MS4wMDowLjAwIG1peGVkX3JlZj0xIG1lX3JhbmdlPTE2IGNocm9tYV9tZT0xIHRyZWxsaXM9MSA4eDhkY3Q9MSBjcW09MCBkZWFkem9uZT0yMSwxMSBmYXN0X3Bza2lwPTEgY2hyb21hX3FwX29mZnNldD0tMiB0aHJlYWRzPTIgbG9va2FoZWFkX3RocmVhZHM9MSBzbGljZWRfdGhyZWFkcz0wIG5yPTAgZGVjaW1hdGU9MSBpbnRlcmxhY2VkPTAgYmx1cmF5X2NvbXBhdD0wIGNvbnN0cmFpbmVkX2ludHJhPTAgYmZyYW1lcz0zIGJfcHlyYW1pZD0yIGJfYWRhcHQ9MSBiX2JpYXM9MCBkaXJlY3Q9MSB3ZWlnaHRiPTEgb3Blbl9nb3A9MCB3ZWlnaHRwPTIga2V5aW50PTI1MCBrZXlpbnRfbWluPTEwIHNjZW5lY3V0PTQwIGludHJhX3JlZnJlc2g9MCByY19sb29rYWhlYWQ9NDAgcmM9Y3JmIG1idHJlZT0xIGNyZj0yMy4wIHFjb21wPTAuNjAgcXBtaW49MCBxcG1heD02OSBxcHN0ZXA9NCBpcF9yYXRpbz0xLjQwIGFxPTE6MS4wMACAAAAAJWWIhAA///7mdfgUoKykhh+DdK6xVynnAWznTQ4fVSgMZ1FwY28AAAAKQZoibEN//qePiAAAAAgBnkF5Df8HfQ==",
|
|
562
|
+
"audio/mpeg": "data:audio/mpeg;base64,SUQzBAAAAAAAIlRTU0UAAAAOAAADTGF2ZjYyLjMuMTAwAAAAAAAAAAAAAAD/40jAAAAAAAAAAAAASW5mbwAAAA8AAAAFAAAGwABVVVVVVVVVVVVVVVVVVVVVVVVVgICAgICAgICAgICAgICAgICAgICqqqqqqqqqqqqqqqqqqqqqqqqqqtXV1dXV1dXV1dXV1dXV1dXV1dXV//////////////////////////8AAAAATGF2YzYyLjExAAAAAAAAAAAAAAAAJALAAAAAAAAABsDR4nNFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/40jEAAAAA0gAAAAATEFNRTMuMTAwVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/40jEAAAAA0gAAAAATEFNRTMuMTAwVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/40jEAAAAA0gAAAAATEFNRTMuMTAwVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/40jEAAAAA0gAAAAATEFNRTMuMTAwVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/40jEAAAAA0gAAAAATEFNRTMuMTAwVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVU=",
|
|
563
|
+
"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=",
|
|
564
|
+
"audio/mp4": "data:audio/mp4;base64,AAAAHGZ0eXBNNEEgAAACAE00QSBpc29taXNvMgAAAAhmcmVlAAAAQW1kYXTeAgBMYXZjNjIuMTEuMTAwAAIwQA4BGCAHARggBwEYIAcBGCAHARggBwEYIAcBGCAHARggBwEYIAcAAAMibW9vdgAAAGxtdmhkAAAAAAAAAAAAAAAAAAAD6AAAAMgAAQAAAQAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAk10cmFrAAAAXHRraGQAAAADAAAAAAAAAAAAAAABAAAAAAAAAMgAAAAAAAAAAAAAAAEBAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAkZWR0cwAAABxlbHN0AAAAAAAAAAEAAADIAAAEAAABAAAAAAHFbWRpYQAAACBtZGhkAAAAAAAAAAAAAAAAAACsRAAAJnRVxAAAAAAALWhkbHIAAAAAAAAAAHNvdW4AAAAAAAAAAAAAAABTb3VuZEhhbmRsZXIAAAABcG1pbmYAAAAQc21oZAAAAAAAAAAAAAAAJGRpbmYAAAAcZHJlZgAAAAAAAAABAAAADHVybCAAAAABAAABNHN0YmwAAABqc3RzZAAAAAAAAAABAAAAWm1wNGEAAAAAAAAAAQAAAAAAAAAAAAEAEAAAAACsRAAAAAAANmVzZHMAAAAAA4CAgCUAAQAEgICAF0AVAAAAAAB9AAAAB/oFgICABRIIVuUABoCAgAECAAAAIHN0dHMAAAAAAAAAAgAAAAkAAAQAAAAAAQAAAnQAAAAcc3RzYwAAAAAAAAABAAAAAQAAAAoAAAABAAAAPHN0c3oAAAAAAAAAAAAAAAoAAAAVAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAAFHN0Y28AAAAAAAAAAQAAACwAAAAac2dwZAEAAAByb2xsAAAAAgAAAAH//wAAABxzYmdwAAAAAHJvbGwAAAABAAAACgAAAAEAAABhdWR0YQAAAFltZXRhAAAAAAAAACFoZGxyAAAAAAAAAABtZGlyYXBwbAAAAAAAAAAAAAAAACxpbHN0AAAAJKl0b28AAAAcZGF0YQAAAAEAAAAATGF2ZjYyLjMuMTAw",
|
|
565
|
+
"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==",
|
|
566
|
+
"text/plain": "data:text/plain;base64,TGlzdGEgZGUgY29tcHJhcwotIGFycm96IDJrZwotIG9sZW8gMXVuCg==",
|
|
567
|
+
"text/csv": "data:text/csv;base64,cHJvZHV0byxxdWFudGlkYWRlLHByZWNvCmFycm96LDIsMjQuOTAKb2xlbywxLDcuNTAK",
|
|
568
|
+
"application/zip": "data:application/zip;base64,UEsDBBQAAAAIANFO/FwtmGOxGwAAABkAAAARAAAAY29tcHJvdmFudGUtMS50eHRLzs8tKMovS8wrSVVISVUoSExPzE3NK8nnAgBQSwMEFAAAAAgA0U78XCftV8YWAAAAFAAAABEAAABjb21wcm92YW50ZS0yLnR4dCtOTS/NS8lXSM7PLSjKL0vMK0nlAgBQSwECFAMUAAAACADRTvxcLZhjsRsAAAAZAAAAEQAAAAAAAAAAAAAAgAEAAAAAY29tcHJvdmFudGUtMS50eHRQSwECFAMUAAAACADRTvxcJ+1XxhYAAAAUAAAAEQAAAAAAAAAAAAAAgAFKAAAAY29tcHJvdmFudGUtMi50eHRQSwUGAAAAAAIAAgB+AAAAjwAAAAAA",
|
|
569
|
+
"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==",
|
|
570
|
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": "data:application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;base64,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"
|
|
571
|
+
};
|
|
572
|
+
var FAMILY_FALLBACK = {
|
|
573
|
+
image: "image/png",
|
|
574
|
+
video: "video/mp4",
|
|
575
|
+
audio: "audio/mpeg",
|
|
576
|
+
text: "text/plain"
|
|
577
|
+
};
|
|
578
|
+
function resolvePreviewFileSample(mimeType, filename) {
|
|
579
|
+
const normalized = (mimeType ?? "").split(";")[0].trim().toLowerCase();
|
|
580
|
+
const exact = PREVIEW_FILE_SAMPLES[normalized];
|
|
581
|
+
if (exact) return exact;
|
|
582
|
+
const family = FAMILY_FALLBACK[normalized.split("/")[0] ?? ""];
|
|
583
|
+
const byFamily = family ? PREVIEW_FILE_SAMPLES[family] : void 0;
|
|
584
|
+
if (byFamily) return byFamily;
|
|
585
|
+
const note = `Preview do SDK: nao existe amostra binaria valida para ${normalized || "tipo desconhecido"}${filename ? ` (${filename})` : ""}. Na tela real este arquivo vem do storage.`;
|
|
586
|
+
return `data:text/plain;charset=utf-8,${encodeURIComponent(note)}`;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// src/preview/previewMediaSource.ts
|
|
590
|
+
var blobUrlCache = /* @__PURE__ */ new Map();
|
|
591
|
+
function toBlob(dataUrl) {
|
|
592
|
+
const [head, payload] = dataUrl.split(",");
|
|
593
|
+
const mimeType = head.replace(/^data:/, "").replace(/;base64$/, "").replace(/;charset=.*$/, "");
|
|
594
|
+
if (!head.endsWith(";base64")) {
|
|
595
|
+
return new Blob([decodeURIComponent(payload)], { type: mimeType || "text/plain" });
|
|
596
|
+
}
|
|
597
|
+
const binary = atob(payload);
|
|
598
|
+
const bytes = new Uint8Array(binary.length);
|
|
599
|
+
for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index);
|
|
600
|
+
return new Blob([bytes], { type: mimeType });
|
|
601
|
+
}
|
|
602
|
+
function previewFileUrl(mimeType, filename) {
|
|
603
|
+
const dataUrl = resolvePreviewFileSample(mimeType, filename);
|
|
604
|
+
if (typeof URL === "undefined" || typeof URL.createObjectURL !== "function") return dataUrl;
|
|
605
|
+
const cached = blobUrlCache.get(dataUrl);
|
|
606
|
+
if (cached) return cached;
|
|
607
|
+
const blobUrl = URL.createObjectURL(toBlob(dataUrl));
|
|
608
|
+
blobUrlCache.set(dataUrl, blobUrl);
|
|
609
|
+
return blobUrl;
|
|
610
|
+
}
|
|
611
|
+
function previewFileBase64(mimeType, filename) {
|
|
612
|
+
const dataUrl = resolvePreviewFileSample(mimeType, filename);
|
|
613
|
+
const [head, payload] = dataUrl.split(",");
|
|
614
|
+
const declared = head.replace(/^data:/, "").replace(/;base64$/, "").replace(/;charset=.*$/, "");
|
|
615
|
+
if (head.endsWith(";base64")) return { mimeType: declared, data: payload };
|
|
616
|
+
return { mimeType: declared, data: btoa(decodeURIComponent(payload)) };
|
|
617
|
+
}
|
|
618
|
+
function createPreviewMediaResolver() {
|
|
619
|
+
const byId = /* @__PURE__ */ new Map();
|
|
620
|
+
for (const documents of Object.values(PREVIEW_DOCUMENTS)) {
|
|
621
|
+
for (const document of documents) {
|
|
622
|
+
byId.set(document.id, { mimeType: document.mimeType, filename: document.filename });
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
return async (message) => {
|
|
626
|
+
const reference = message.uploadId ?? (message.mediaId ? `preview/inbound/${message.mediaId}` : void 0);
|
|
627
|
+
if (!reference) return null;
|
|
628
|
+
const known = byId.get(reference);
|
|
629
|
+
return previewFileUrl(known?.mimeType ?? message.mimeType, known?.filename ?? message.filename);
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
|
|
426
633
|
// src/preview/createMockConversationsApi.ts
|
|
427
|
-
var PREVIEW_IMAGE_BASE64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYGD4DwABBAEAX+XyEgAAAABJRU5ErkJggg==";
|
|
428
634
|
var DEFAULT_LATENCY_MS = 120;
|
|
429
635
|
var PREVIEW_AGENT_ID = "preview-agent";
|
|
430
636
|
var PREVIEW_TEMPLATES = [
|
|
@@ -542,11 +748,51 @@ function createMockConversationsApi(params) {
|
|
|
542
748
|
${names.join("\n")}`], { type: "application/zip" });
|
|
543
749
|
});
|
|
544
750
|
},
|
|
545
|
-
|
|
546
|
-
|
|
751
|
+
/** Junta as bibliotecas de todas as conversas do fixture, com a origem de cada arquivo. */
|
|
752
|
+
getAllDocuments(documentParams) {
|
|
753
|
+
return withLatency(() => {
|
|
754
|
+
let all = Object.entries(PREVIEW_DOCUMENTS).flatMap(
|
|
755
|
+
([conversationId, docs]) => docs.map((document) => ({ ...document, conversationId }))
|
|
756
|
+
);
|
|
757
|
+
const search = documentParams?.search?.trim().toLowerCase();
|
|
758
|
+
if (search) {
|
|
759
|
+
const digits = search.replace(/\D/g, "");
|
|
760
|
+
all = all.filter(
|
|
761
|
+
(document) => document.filename.toLowerCase().includes(search) || digits !== "" && document.conversationId.includes(digits)
|
|
762
|
+
);
|
|
763
|
+
}
|
|
764
|
+
const source = documentParams?.source;
|
|
765
|
+
if (source === "team") {
|
|
766
|
+
all = all.filter((document) => document.source === "agent" || document.source === "bot");
|
|
767
|
+
} else if (source) {
|
|
768
|
+
all = all.filter((document) => document.source === source);
|
|
769
|
+
}
|
|
770
|
+
all.sort(
|
|
771
|
+
(left, right) => documentParams?.sortDirection === "asc" ? left.linkedAt.localeCompare(right.linkedAt) : right.linkedAt.localeCompare(left.linkedAt)
|
|
772
|
+
);
|
|
773
|
+
const total = all.length;
|
|
774
|
+
const limit = documentParams?.limit ?? total;
|
|
775
|
+
const page = documentParams?.page ?? 1;
|
|
776
|
+
return { documents: all.slice((page - 1) * limit, page * limit), total };
|
|
777
|
+
});
|
|
778
|
+
},
|
|
779
|
+
// Devolve os bytes DO TIPO do documento, não uma imagem para tudo: antes, abrir um PDF entregava
|
|
780
|
+
// um PNG rotulado `application/pdf` e o leitor recusava o arquivo. O `uploadId` é a única pista
|
|
781
|
+
// que o contrato dá, então o tipo vem da própria biblioteca.
|
|
782
|
+
getDocumentUrl(uploadId) {
|
|
783
|
+
return withLatency(() => {
|
|
784
|
+
const found = Object.values(PREVIEW_DOCUMENTS).flat().find((document) => document.id === uploadId);
|
|
785
|
+
return previewFileUrl(found?.mimeType, found?.filename);
|
|
786
|
+
});
|
|
547
787
|
},
|
|
548
|
-
|
|
549
|
-
|
|
788
|
+
// Caminho da mídia ainda não ingerida: o backend busca na Meta e devolve base64. Resolve pelo
|
|
789
|
+
// id para a bolha receber os bytes DO TIPO dela — devolvendo um PNG para todo id, vídeo e áudio
|
|
790
|
+
// apareciam quebrados na thread mesmo havendo amostra válida do formato.
|
|
791
|
+
getMediaProxyUrl(mediaId) {
|
|
792
|
+
return withLatency(() => {
|
|
793
|
+
const found = Object.values(PREVIEW_DOCUMENTS).flat().find((document) => document.id === `preview/inbound/${mediaId}`);
|
|
794
|
+
return previewFileBase64(found?.mimeType, found?.filename);
|
|
795
|
+
});
|
|
550
796
|
},
|
|
551
797
|
takeover(conversationId) {
|
|
552
798
|
return withLatency(
|
|
@@ -777,12 +1023,65 @@ function startPreviewScript(params) {
|
|
|
777
1023
|
}, intervalMs);
|
|
778
1024
|
return () => clearInterval(timer);
|
|
779
1025
|
}
|
|
1026
|
+
|
|
1027
|
+
// src/preview/MediaTypesPreview.tsx
|
|
1028
|
+
import { useMemo as useMemo2 } from "react";
|
|
1029
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
1030
|
+
var MEDIA_TYPES_CONVERSATION_ID = "5511944443333";
|
|
1031
|
+
function MediaTypesPreview({
|
|
1032
|
+
conversationId = MEDIA_TYPES_CONVERSATION_ID,
|
|
1033
|
+
className
|
|
1034
|
+
}) {
|
|
1035
|
+
const store = useMemo2(
|
|
1036
|
+
() => createPreviewStore({ conversations: PREVIEW_CONVERSATIONS, messages: PREVIEW_MESSAGES }),
|
|
1037
|
+
[]
|
|
1038
|
+
);
|
|
1039
|
+
const api = useMemo2(() => createMockConversationsApi({ store }), [store]);
|
|
1040
|
+
const sse = useMemo2(() => createMockSSEProvider({ store }), [store]);
|
|
1041
|
+
const messages = PREVIEW_MESSAGES[conversationId] ?? [];
|
|
1042
|
+
const documents = PREVIEW_DOCUMENTS[conversationId] ?? [];
|
|
1043
|
+
const mimeTypes = [...new Set(documents.map((document) => document.mimeType))];
|
|
1044
|
+
return /* @__PURE__ */ jsx2(ConversationsProvider, { api, sse, children: /* @__PURE__ */ jsxs2("div", { className, children: [
|
|
1045
|
+
/* @__PURE__ */ jsxs2("header", { className: "border-b px-4 py-3 dark:border-gray-700", children: [
|
|
1046
|
+
/* @__PURE__ */ jsx2("h1", { className: "text-lg font-semibold", children: "Teste manual de m\xEDdia" }),
|
|
1047
|
+
/* @__PURE__ */ jsxs2("p", { className: "text-sm text-gray-500", children: [
|
|
1048
|
+
documents.length,
|
|
1049
|
+
" arquivos, ",
|
|
1050
|
+
mimeTypes.length,
|
|
1051
|
+
" 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."
|
|
1052
|
+
] })
|
|
1053
|
+
] }),
|
|
1054
|
+
/* @__PURE__ */ jsxs2("div", { className: "grid gap-4 p-4 lg:grid-cols-2", children: [
|
|
1055
|
+
/* @__PURE__ */ jsxs2("section", { className: "space-y-3", children: [
|
|
1056
|
+
/* @__PURE__ */ jsx2("h2", { className: "text-sm font-semibold uppercase tracking-wide text-gray-500", children: "Biblioteca da empresa" }),
|
|
1057
|
+
/* @__PURE__ */ jsx2(DocumentsLibrary, { perPage: documents.length || 20 })
|
|
1058
|
+
] }),
|
|
1059
|
+
/* @__PURE__ */ jsxs2("section", { className: "space-y-3", children: [
|
|
1060
|
+
/* @__PURE__ */ jsx2("h2", { className: "text-sm font-semibold uppercase tracking-wide text-gray-500", children: "Painel da conversa" }),
|
|
1061
|
+
/* @__PURE__ */ jsx2(ConversationDocumentsPanel, { conversationId, open: true, perPage: documents.length || 20 }),
|
|
1062
|
+
/* @__PURE__ */ jsx2("h2", { className: "text-sm font-semibold uppercase tracking-wide text-gray-500", children: "Bolhas na thread" }),
|
|
1063
|
+
/* @__PURE__ */ jsx2(ConversationWallpaper, { className: "max-h-[70vh] overflow-y-auto rounded-lg px-3 py-2", children: messages.map((message, index) => /* @__PURE__ */ jsx2(
|
|
1064
|
+
MessageBubble,
|
|
1065
|
+
{
|
|
1066
|
+
message,
|
|
1067
|
+
isMine: message.direction === "outbound",
|
|
1068
|
+
isFirstInGroup: index === 0 || messages[index - 1]?.sender !== message.sender
|
|
1069
|
+
},
|
|
1070
|
+
message.id
|
|
1071
|
+
)) })
|
|
1072
|
+
] })
|
|
1073
|
+
] })
|
|
1074
|
+
] }) });
|
|
1075
|
+
}
|
|
780
1076
|
export {
|
|
781
1077
|
ConversationPreview,
|
|
782
1078
|
DEFAULT_PREVIEW_SCRIPT,
|
|
783
1079
|
GLOBAL_CHANNEL,
|
|
1080
|
+
MEDIA_TYPES_CONVERSATION_ID,
|
|
1081
|
+
MediaTypesPreview,
|
|
784
1082
|
PREVIEW_CONVERSATIONS,
|
|
785
1083
|
PREVIEW_DOCUMENTS,
|
|
1084
|
+
PREVIEW_FILE_SAMPLES,
|
|
786
1085
|
PREVIEW_MESSAGES,
|
|
787
1086
|
PreviewInProductionError,
|
|
788
1087
|
PreviewWebhookRejectedError,
|
|
@@ -791,7 +1090,11 @@ export {
|
|
|
791
1090
|
createMockConversationsApi,
|
|
792
1091
|
createMockEventSource,
|
|
793
1092
|
createMockSSEProvider,
|
|
1093
|
+
createPreviewMediaResolver,
|
|
794
1094
|
createPreviewStore,
|
|
795
1095
|
createPreviewWebhookClient,
|
|
1096
|
+
previewFileBase64,
|
|
1097
|
+
previewFileUrl,
|
|
1098
|
+
resolvePreviewFileSample,
|
|
796
1099
|
startPreviewScript
|
|
797
1100
|
};
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
|
|
1
3
|
interface ConversationsUIConfig {
|
|
2
4
|
apiBaseUrl: string;
|
|
3
5
|
theme?: ConversationsTheme;
|
|
@@ -51,6 +53,16 @@ interface MessagePayload {
|
|
|
51
53
|
isLastInGroup?: boolean;
|
|
52
54
|
}
|
|
53
55
|
|
|
56
|
+
type ResolveMediaUrl = (message: MessagePayload) => Promise<string | null>;
|
|
57
|
+
interface MediaRendererProps {
|
|
58
|
+
message: MessagePayload;
|
|
59
|
+
onLightbox: (src: string) => void;
|
|
60
|
+
onResolveUrl?: ResolveMediaUrl;
|
|
61
|
+
/** Aplicado no wrapper de cada tipo de mídia — imagem, vídeo, áudio e documento. */
|
|
62
|
+
className?: string;
|
|
63
|
+
}
|
|
64
|
+
declare function MediaRenderer({ message, onLightbox, onResolveUrl, className }: MediaRendererProps): react.JSX.Element | null;
|
|
65
|
+
|
|
54
66
|
/**
|
|
55
67
|
* Canal de origem da conversa e o que cada um permite.
|
|
56
68
|
*
|
|
@@ -154,6 +166,14 @@ interface ListDocumentsParams {
|
|
|
154
166
|
source?: string;
|
|
155
167
|
sortDirection?: 'asc' | 'desc';
|
|
156
168
|
}
|
|
169
|
+
/** Arquivo na biblioteca da empresa: o mesmo da conversa, mais de qual conversa veio. */
|
|
170
|
+
interface CompanyDocument extends ConversationDocument {
|
|
171
|
+
conversationId: string;
|
|
172
|
+
}
|
|
173
|
+
interface CompanyDocumentPage {
|
|
174
|
+
documents: CompanyDocument[];
|
|
175
|
+
total: number;
|
|
176
|
+
}
|
|
157
177
|
interface ConversationDocumentPage {
|
|
158
178
|
documents: ConversationDocument[];
|
|
159
179
|
total: number;
|
|
@@ -212,6 +232,13 @@ interface ConversationsApi {
|
|
|
212
232
|
* lote em vez de oferecer um botão que falha.
|
|
213
233
|
*/
|
|
214
234
|
downloadDocumentsArchive?(conversationId: string, uploadIds: readonly string[]): Promise<Blob>;
|
|
235
|
+
/**
|
|
236
|
+
* Biblioteca de TODAS as conversas, para uma tela de Documentos fora do atendimento.
|
|
237
|
+
*
|
|
238
|
+
* Opcional por capacidade: host que só expõe anexo dentro da conversa não implementa, e o
|
|
239
|
+
* componente de biblioteca simplesmente não é usável — melhor que uma tela que sempre erra.
|
|
240
|
+
*/
|
|
241
|
+
getAllDocuments?(params?: ListDocumentsParams): Promise<CompanyDocumentPage>;
|
|
215
242
|
getMediaProxyUrl(mediaId: string): Promise<{
|
|
216
243
|
mimeType: string;
|
|
217
244
|
data: string;
|
|
@@ -294,4 +321,4 @@ interface ConversationDocument {
|
|
|
294
321
|
linkedAt: string;
|
|
295
322
|
}
|
|
296
323
|
|
|
297
|
-
export { CHANNEL_CAPABILITIES as C, DEFAULT_CONVERSATION_CHANNEL as D, type FormatContactHandleParams as F, HANDLE_KIND as H, type ListConversationsParams as L,
|
|
324
|
+
export { contactFlag as A, formatContactHandle as B, CHANNEL_CAPABILITIES as C, DEFAULT_CONVERSATION_CHANNEL as D, type FormatContactHandleParams as F, HANDLE_KIND as H, type ListConversationsParams as L, MediaRenderer as M, REOPEN_MECHANISM as R, type SSEProvider as S, CHANNEL_FILTER_ALL as a, CONVERSATION_CHANNEL as b, type ChannelCapabilities as c, type ChannelFilter as d, type ChannelFilterOption as e, type CompanyDocument as f, type CompanyDocumentPage as g, type ConversationChannel as h, type ConversationDocument as i, type ConversationDocumentPage as j, type ConversationEventSource as k, type ConversationPage as l, type ConversationSummary as m, type ConversationTemplate as n, type ConversationsApi as o, type ConversationsFeatures as p, type ConversationsTheme as q, type ConversationsUIConfig as r, type HandleKind as s, type ListDocumentsParams as t, type MediaRendererProps as u, type MessagePayload as v, type ReopenMechanism as w, type ResolveMediaUrl as x, capabilitiesOf as y, channelFiltersFor as z };
|
package/package.json
CHANGED
package/src/Avatar.tsx
CHANGED
|
@@ -1,8 +1,18 @@
|
|
|
1
|
+
export interface AvatarLabels {
|
|
2
|
+
/** Lido por leitor de tela quando não há nome nem imagem — a silhueta genérica. */
|
|
3
|
+
unnamedContact: string
|
|
4
|
+
}
|
|
5
|
+
|
|
1
6
|
export interface AvatarProps {
|
|
2
7
|
name?: string | null
|
|
3
8
|
avatarUrl?: string
|
|
4
9
|
size?: 'sm' | 'md' | 'lg'
|
|
5
10
|
className?: string
|
|
11
|
+
labels?: Partial<AvatarLabels>
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const DEFAULT_AVATAR_LABELS: AvatarLabels = {
|
|
15
|
+
unnamedContact: 'Contato sem nome',
|
|
6
16
|
}
|
|
7
17
|
|
|
8
18
|
const sizeClasses = {
|
|
@@ -36,8 +46,9 @@ function getBgColor(name: string | undefined | null): string {
|
|
|
36
46
|
|
|
37
47
|
// Paridade com financiamento-imobiliario-bot/apps/web/src/components/Avatar.tsx —
|
|
38
48
|
// mesmas iniciais (primeira+última letra), mesmo hash de cor, mesmas classes de tamanho.
|
|
39
|
-
export function Avatar({ name, avatarUrl, size = 'md', className = '' }: AvatarProps) {
|
|
49
|
+
export function Avatar({ name, avatarUrl, size = 'md', className = '', labels }: AvatarProps) {
|
|
40
50
|
const sizeClass = sizeClasses[size]
|
|
51
|
+
const unnamedContactLabel = labels?.unnamedContact ?? DEFAULT_AVATAR_LABELS.unnamedContact
|
|
41
52
|
|
|
42
53
|
if (avatarUrl) {
|
|
43
54
|
return (
|
|
@@ -58,7 +69,7 @@ export function Avatar({ name, avatarUrl, size = 'md', className = '' }: AvatarP
|
|
|
58
69
|
<div
|
|
59
70
|
className={`${sizeClass} ${bgColor} rounded-full flex items-center justify-center text-white flex-shrink-0 ${className}`}
|
|
60
71
|
role="img"
|
|
61
|
-
aria-label=
|
|
72
|
+
aria-label={unnamedContactLabel}
|
|
62
73
|
>
|
|
63
74
|
<svg viewBox="0 0 24 24" fill="currentColor" className="h-[60%] w-[60%]" aria-hidden>
|
|
64
75
|
<path d="M12 12a5 5 0 1 0 0-10 5 5 0 0 0 0 10Zm0 2c-4.42 0-8 2.24-8 5v1h16v-1c0-2.76-3.58-5-8-5Z" />
|