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

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.
@@ -3,7 +3,7 @@ import {
3
3
  DateDivider,
4
4
  MessageBubble,
5
5
  MessageComposer
6
- } from "../chunk-4R6Y43DQ.js";
6
+ } from "../chunk-YWITIIHD.js";
7
7
  import "../chunk-OGRRHQQW.js";
8
8
 
9
9
  // src/preview/previewStore.ts
@@ -135,108 +135,6 @@ function createMockEventSource() {
135
135
  };
136
136
  }
137
137
 
138
- // src/preview/createMockConversationsApi.ts
139
- var PREVIEW_IMAGE_BASE64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYGD4DwABBAEAX+XyEgAAAABJRU5ErkJggg==";
140
- var DEFAULT_LATENCY_MS = 120;
141
- function createMockConversationsApi(params) {
142
- const latencyMs = params.latencyMs ?? DEFAULT_LATENCY_MS;
143
- async function withLatency(produce) {
144
- await new Promise((resolve) => setTimeout(resolve, latencyMs));
145
- return produce();
146
- }
147
- return {
148
- fetchConversations(fetchParams) {
149
- return withLatency(() => {
150
- const conversations = params.store.listConversations({
151
- waitingHuman: fetchParams?.waitingHuman,
152
- search: fetchParams?.search
153
- });
154
- const limit = fetchParams?.limit ?? conversations.length;
155
- const page = fetchParams?.page ?? 1;
156
- return conversations.slice((page - 1) * limit, page * limit);
157
- });
158
- },
159
- fetchMessages(conversationId, fetchParams) {
160
- return withLatency(() => {
161
- const messages = params.store.listMessages(conversationId);
162
- const limit = fetchParams?.limit;
163
- return limit ? messages.slice(-limit) : messages;
164
- });
165
- },
166
- sendMessage(conversationId, text) {
167
- return withLatency(
168
- () => params.store.appendMessage({ conversationId, content: text, direction: "outbound", sender: "agent" })
169
- );
170
- },
171
- sendMedia(conversationId, data) {
172
- return withLatency(
173
- () => params.store.appendMessage({
174
- conversationId,
175
- content: data.caption ?? data.filename,
176
- direction: "outbound",
177
- sender: "agent"
178
- })
179
- );
180
- },
181
- sendTemplate(conversationId, data) {
182
- return withLatency(() => {
183
- params.store.appendMessage({
184
- conversationId,
185
- content: `[template] ${data.templateName}`,
186
- direction: "outbound",
187
- sender: "agent"
188
- });
189
- });
190
- },
191
- markRead(conversationId) {
192
- return withLatency(() => params.store.markRead(conversationId));
193
- },
194
- getContext(conversationId) {
195
- return withLatency(() => {
196
- const conversation = params.store.listConversations().find((item) => item.id === conversationId);
197
- return {
198
- currentState: conversation?.currentState ?? "unknown",
199
- mode: conversation?.mode ?? "bot",
200
- preview: true
201
- };
202
- });
203
- },
204
- getDocuments() {
205
- return withLatency(() => []);
206
- },
207
- getDocumentUrl() {
208
- return withLatency(() => `data:image/png;base64,${PREVIEW_IMAGE_BASE64}`);
209
- },
210
- getMediaProxyUrl() {
211
- return withLatency(() => ({ mimeType: "image/png", data: PREVIEW_IMAGE_BASE64 }));
212
- }
213
- };
214
- }
215
-
216
- // src/preview/createMockSSEProvider.ts
217
- function createMockSSEProvider(params) {
218
- function connect(channel) {
219
- const source = createMockEventSource();
220
- const unsubscribe = params.store.subscribe(channel, (emission) => {
221
- source.emit(emission.event, emission.payload);
222
- });
223
- const close = source.close.bind(source);
224
- source.close = () => {
225
- unsubscribe();
226
- close();
227
- };
228
- return source;
229
- }
230
- return {
231
- connectConversationStream(conversationId) {
232
- return connect(conversationChannel(conversationId));
233
- },
234
- connectGlobalStream() {
235
- return connect(GLOBAL_CHANNEL);
236
- }
237
- };
238
- }
239
-
240
138
  // src/preview/previewFixtures.ts
241
139
  var BASE_DAY = "2026-07-26";
242
140
  function at(time) {
@@ -297,6 +195,24 @@ var PREVIEW_CONVERSATIONS = [
297
195
  waitingHuman: false,
298
196
  unread: 1,
299
197
  currentState: "list_import"
198
+ },
199
+ {
200
+ // Cobre TODO tipo que o composer aceita (DEFAULT_ACCEPTED_FILE_TYPES: image/*, video/*,
201
+ // audio/*, .pdf, .doc, .docx, .xls, .xlsx, .zip) mais sticker. Existe para que cada ramo do
202
+ // MediaRenderer e cada ícone/cor do FileIcon apareçam em algum lugar — ramo sem fixture é ramo
203
+ // que ninguém olha até quebrar em produção.
204
+ id: "5511944443333",
205
+ whatsappNumber: "5511944443333",
206
+ clientName: "Rita Documentos",
207
+ lastContent: "segue a planilha do pedido",
208
+ lastDirection: "inbound",
209
+ lastAt: at("15:10:00"),
210
+ lastInboundAt: at("15:10:00"),
211
+ mode: "human",
212
+ assignedUserId: "agent-1",
213
+ waitingHuman: false,
214
+ unread: 3,
215
+ currentState: "human_handling"
300
216
  }
301
217
  ];
302
218
  var PREVIEW_MESSAGES = {
@@ -375,8 +291,312 @@ var PREVIEW_MESSAGES = {
375
291
  sender: "customer",
376
292
  timestamp: at("13:31:00")
377
293
  }
294
+ ],
295
+ // Um tipo por mensagem, na ordem em que o MediaRenderer os trata.
296
+ "5511944443333": [
297
+ {
298
+ id: "fixture-doc-image",
299
+ type: "image",
300
+ mediaId: "preview-image-1",
301
+ mimeType: "image/png",
302
+ caption: "foto da prateleira",
303
+ direction: "inbound",
304
+ sender: "customer",
305
+ timestamp: at("15:00:00")
306
+ },
307
+ {
308
+ id: "fixture-doc-video",
309
+ type: "video",
310
+ mediaId: "preview-video-1",
311
+ mimeType: "video/mp4",
312
+ direction: "inbound",
313
+ sender: "customer",
314
+ timestamp: at("15:01:00")
315
+ },
316
+ {
317
+ id: "fixture-doc-audio",
318
+ type: "audio",
319
+ mediaId: "preview-audio-2",
320
+ mimeType: "audio/ogg",
321
+ direction: "inbound",
322
+ sender: "customer",
323
+ timestamp: at("15:02:00")
324
+ },
325
+ {
326
+ id: "fixture-doc-sticker",
327
+ type: "sticker",
328
+ mediaId: "preview-sticker-1",
329
+ mimeType: "image/webp",
330
+ direction: "inbound",
331
+ sender: "customer",
332
+ timestamp: at("15:03:00")
333
+ },
334
+ // Os cinco ramos do FileIcon: pdf, doc, xls, zip e o genérico do fallback.
335
+ {
336
+ id: "fixture-doc-pdf",
337
+ type: "document",
338
+ uploadId: "preview/documentos/nota-fiscal.pdf",
339
+ filename: "nota-fiscal.pdf",
340
+ mimeType: "application/pdf",
341
+ sizeBytes: 184320,
342
+ direction: "inbound",
343
+ sender: "customer",
344
+ timestamp: at("15:04:00")
345
+ },
346
+ {
347
+ id: "fixture-doc-docx",
348
+ type: "document",
349
+ uploadId: "preview/documentos/contrato.docx",
350
+ filename: "contrato.docx",
351
+ mimeType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
352
+ sizeBytes: 42112,
353
+ direction: "inbound",
354
+ sender: "customer",
355
+ timestamp: at("15:05:00")
356
+ },
357
+ {
358
+ id: "fixture-doc-doc",
359
+ type: "document",
360
+ uploadId: "preview/documentos/procuracao.doc",
361
+ filename: "procuracao.doc",
362
+ mimeType: "application/msword",
363
+ sizeBytes: 31744,
364
+ direction: "inbound",
365
+ sender: "customer",
366
+ timestamp: at("15:06:00")
367
+ },
368
+ {
369
+ id: "fixture-doc-xlsx",
370
+ type: "document",
371
+ uploadId: "preview/documentos/pedido.xlsx",
372
+ filename: "pedido.xlsx",
373
+ mimeType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
374
+ sizeBytes: 15872,
375
+ direction: "inbound",
376
+ sender: "customer",
377
+ timestamp: at("15:07:00")
378
+ },
379
+ {
380
+ id: "fixture-doc-xls",
381
+ type: "document",
382
+ uploadId: "preview/documentos/tabela-antiga.xls",
383
+ filename: "tabela-antiga.xls",
384
+ mimeType: "application/vnd.ms-excel",
385
+ sizeBytes: 9216,
386
+ direction: "inbound",
387
+ sender: "customer",
388
+ timestamp: at("15:08:00")
389
+ },
390
+ {
391
+ id: "fixture-doc-zip",
392
+ type: "document",
393
+ uploadId: "preview/documentos/comprovantes.zip",
394
+ filename: "comprovantes.zip",
395
+ mimeType: "application/zip",
396
+ sizeBytes: 2355200,
397
+ direction: "inbound",
398
+ sender: "customer",
399
+ timestamp: at("15:09:00")
400
+ },
401
+ {
402
+ // Extensão fora do EXTENSION_STYLE: garante que o ícone genérico cinza também apareça.
403
+ id: "fixture-doc-generic",
404
+ type: "document",
405
+ uploadId: "preview/documentos/lista-compras.txt",
406
+ filename: "lista-compras.txt",
407
+ mimeType: "text/plain",
408
+ sizeBytes: 1024,
409
+ direction: "inbound",
410
+ sender: "customer",
411
+ timestamp: at("15:10:00")
412
+ }
378
413
  ]
379
414
  };
415
+ var PREVIEW_DOCUMENTS = {
416
+ "5511944443333": (PREVIEW_MESSAGES["5511944443333"] ?? []).filter((message) => message.type === "document").map((message) => ({
417
+ id: message.uploadId ?? message.id,
418
+ filename: message.filename ?? message.id,
419
+ mimeType: message.mimeType ?? "application/octet-stream",
420
+ sizeBytes: message.sizeBytes ?? 0,
421
+ source: message.sender,
422
+ linkedAt: message.timestamp
423
+ }))
424
+ };
425
+
426
+ // src/preview/createMockConversationsApi.ts
427
+ var PREVIEW_IMAGE_BASE64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYGD4DwABBAEAX+XyEgAAAABJRU5ErkJggg==";
428
+ var DEFAULT_LATENCY_MS = 120;
429
+ var PREVIEW_AGENT_ID = "preview-agent";
430
+ var PREVIEW_TEMPLATES = [
431
+ { name: "retomada_atendimento", language: "pt_BR", status: "APPROVED", category: "UTILITY" },
432
+ { name: "lembrete_documentos", language: "pt_BR", status: "APPROVED", category: "UTILITY" },
433
+ { name: "promocao_taxa", language: "pt_BR", status: "PENDING", category: "MARKETING" }
434
+ ];
435
+ function createMockConversationsApi(params) {
436
+ const latencyMs = params.latencyMs ?? DEFAULT_LATENCY_MS;
437
+ async function withLatency(produce) {
438
+ await new Promise((resolve) => setTimeout(resolve, latencyMs));
439
+ return produce();
440
+ }
441
+ return {
442
+ // Devolve a forma paginada, não o array puro: é a que o contrato passou a oferecer e a que
443
+ // permite o preview desenhar controles de página. O total é contado ANTES do corte — depois
444
+ // dele seria sempre o tamanho da página, e a paginação nunca sairia da primeira.
445
+ fetchConversations(fetchParams) {
446
+ return withLatency(() => {
447
+ const conversations = params.store.listConversations({
448
+ waitingHuman: fetchParams?.waitingHuman,
449
+ search: fetchParams?.search
450
+ });
451
+ const limit = fetchParams?.limit ?? conversations.length;
452
+ const page = fetchParams?.page ?? 1;
453
+ return {
454
+ conversations: conversations.slice((page - 1) * limit, page * limit),
455
+ total: conversations.length
456
+ };
457
+ });
458
+ },
459
+ fetchMessages(conversationId, fetchParams) {
460
+ return withLatency(() => {
461
+ const messages = params.store.listMessages(conversationId);
462
+ const limit = fetchParams?.limit;
463
+ return limit ? messages.slice(-limit) : messages;
464
+ });
465
+ },
466
+ sendMessage(conversationId, text) {
467
+ return withLatency(
468
+ () => params.store.appendMessage({ conversationId, content: text, direction: "outbound", sender: "agent" })
469
+ );
470
+ },
471
+ sendMedia(conversationId, data) {
472
+ return withLatency(
473
+ () => params.store.appendMessage({
474
+ conversationId,
475
+ content: data.caption ?? data.filename,
476
+ direction: "outbound",
477
+ sender: "agent"
478
+ })
479
+ );
480
+ },
481
+ sendTemplate(conversationId, data) {
482
+ return withLatency(() => {
483
+ params.store.appendMessage({
484
+ conversationId,
485
+ // Sem nome, o host está pedindo o template padrão do backend — o mock representa isso
486
+ // pelo que o atendente veria, não por um nome inventado.
487
+ content: `[template] ${data.templateName ?? PREVIEW_TEMPLATES[0]?.name ?? "padrao"}`,
488
+ direction: "outbound",
489
+ sender: "agent"
490
+ });
491
+ });
492
+ },
493
+ markRead(conversationId) {
494
+ return withLatency(() => params.store.markRead(conversationId));
495
+ },
496
+ getContext(conversationId) {
497
+ return withLatency(() => {
498
+ const conversation = params.store.listConversations().find((item) => item.id === conversationId);
499
+ return {
500
+ currentState: conversation?.currentState ?? "unknown",
501
+ mode: conversation?.mode ?? "bot",
502
+ preview: true
503
+ };
504
+ });
505
+ },
506
+ /**
507
+ * Espelha o backend em busca, filtro de origem, ordenação E paginação. Mock que ignora params
508
+ * faz o painel parecer quebrado aqui e, pior, esconde o caso em que o backend também os ignora
509
+ * — foi exatamente assim que o filtro de origem passou a existir só no contrato.
510
+ */
511
+ getDocuments(conversationId, documentParams) {
512
+ return withLatency(() => {
513
+ let documents = [...PREVIEW_DOCUMENTS[conversationId] ?? []];
514
+ const search = documentParams?.search?.trim().toLowerCase();
515
+ if (search) {
516
+ documents = documents.filter((document) => document.filename.toLowerCase().includes(search));
517
+ }
518
+ const source = documentParams?.source;
519
+ if (source === "team") {
520
+ documents = documents.filter((document) => document.source === "agent" || document.source === "bot");
521
+ } else if (source) {
522
+ documents = documents.filter((document) => document.source === source);
523
+ }
524
+ documents.sort(
525
+ (left, right) => documentParams?.sortDirection === "asc" ? left.linkedAt.localeCompare(right.linkedAt) : right.linkedAt.localeCompare(left.linkedAt)
526
+ );
527
+ const total = documents.length;
528
+ const limit = documentParams?.limit ?? total;
529
+ const page = documentParams?.page ?? 1;
530
+ return { documents: documents.slice((page - 1) * limit, page * limit), total };
531
+ });
532
+ },
533
+ /**
534
+ * Zip de mentira: um texto listando o que entraria. Basta para exercitar seleção, botão e o
535
+ * caminho de download no preview, sem arrastar uma lib de compactação para o pacote.
536
+ */
537
+ downloadDocumentsArchive(conversationId, uploadIds) {
538
+ return withLatency(() => {
539
+ const known = PREVIEW_DOCUMENTS[conversationId] ?? [];
540
+ const names = uploadIds.map((id) => known.find((document) => document.id === id)?.filename ?? id);
541
+ return new Blob([`preview: ${names.length} arquivo(s)
542
+ ${names.join("\n")}`], { type: "application/zip" });
543
+ });
544
+ },
545
+ getDocumentUrl() {
546
+ return withLatency(() => `data:image/png;base64,${PREVIEW_IMAGE_BASE64}`);
547
+ },
548
+ getMediaProxyUrl() {
549
+ return withLatency(() => ({ mimeType: "image/png", data: PREVIEW_IMAGE_BASE64 }));
550
+ },
551
+ takeover(conversationId) {
552
+ return withLatency(
553
+ () => params.store.setMode({ conversationId, mode: "human", assignedUserId: PREVIEW_AGENT_ID })
554
+ );
555
+ },
556
+ release(conversationId) {
557
+ return withLatency(() => params.store.setMode({ conversationId, mode: "bot" }));
558
+ },
559
+ // Encerrar devolve ao bot como o release, e é de propósito: a diferença entre os dois é a
560
+ // despedida, que o host manda antes de chamar aqui. O mock não a inventa.
561
+ finalize(conversationId) {
562
+ return withLatency(() => params.store.setMode({ conversationId, mode: "bot" }));
563
+ },
564
+ markAllRead() {
565
+ return withLatency(() => {
566
+ for (const conversation of params.store.listConversations()) {
567
+ params.store.markRead(conversation.id);
568
+ }
569
+ });
570
+ },
571
+ listTemplates() {
572
+ return withLatency(() => [...PREVIEW_TEMPLATES]);
573
+ }
574
+ };
575
+ }
576
+
577
+ // src/preview/createMockSSEProvider.ts
578
+ function createMockSSEProvider(params) {
579
+ function connect(channel) {
580
+ const source = createMockEventSource();
581
+ const unsubscribe = params.store.subscribe(channel, (emission) => {
582
+ source.emit(emission.event, emission.payload);
583
+ });
584
+ const close = source.close.bind(source);
585
+ source.close = () => {
586
+ unsubscribe();
587
+ close();
588
+ };
589
+ return source;
590
+ }
591
+ return {
592
+ connectConversationStream(conversationId) {
593
+ return connect(conversationChannel(conversationId));
594
+ },
595
+ connectGlobalStream() {
596
+ return connect(GLOBAL_CHANNEL);
597
+ }
598
+ };
599
+ }
380
600
 
381
601
  // src/preview/ConversationPreview.tsx
382
602
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
@@ -562,6 +782,7 @@ export {
562
782
  DEFAULT_PREVIEW_SCRIPT,
563
783
  GLOBAL_CHANNEL,
564
784
  PREVIEW_CONVERSATIONS,
785
+ PREVIEW_DOCUMENTS,
565
786
  PREVIEW_MESSAGES,
566
787
  PreviewInProductionError,
567
788
  PreviewWebhookRejectedError,
@@ -122,17 +122,61 @@ declare function formatContactHandle(params: FormatContactHandleParams): string;
122
122
  /** Bandeira só faz sentido quando o identificador é telefone. */
123
123
  declare function contactFlag(params: FormatContactHandleParams): string;
124
124
 
125
+ interface ListConversationsParams {
126
+ page?: number;
127
+ limit?: number;
128
+ waitingHuman?: boolean;
129
+ search?: string;
130
+ /**
131
+ * Recortes que só o produto conhece (tipo de financiamento, carteira, campanha) repassados
132
+ * crus ao backend dele. É o que evita o vocabulário de uma vertical virar campo fixo aqui:
133
+ * o pacote transporta o filtro sem saber o que ele significa.
134
+ */
135
+ filters?: Record<string, string | undefined>;
136
+ }
137
+ /**
138
+ * Página com o total, para a UI conseguir desenhar controles de paginação.
139
+ *
140
+ * `fetchConversations` devolve isto **ou** o array puro de antes: implementações existentes
141
+ * continuam válidas sem mudar uma linha, e quem precisa paginar passa a ter o total. Sem a união
142
+ * seria impossível saber se um retorno curto é a última página ou uma página cheia por acaso.
143
+ */
144
+ interface ConversationPage {
145
+ conversations: ConversationSummary[];
146
+ total: number;
147
+ }
148
+ interface ListDocumentsParams {
149
+ search?: string;
150
+ page?: number;
151
+ /** Tamanho da página. Sem ele, `page` sozinho não define fatia nenhuma. */
152
+ limit?: number;
153
+ /** Origem do arquivo (`customer`, `agent`, `bot`…). O vocabulário é do host. */
154
+ source?: string;
155
+ sortDirection?: 'asc' | 'desc';
156
+ }
157
+ interface ConversationDocumentPage {
158
+ documents: ConversationDocument[];
159
+ total: number;
160
+ }
161
+ /**
162
+ * Template disponível para envio a partir da inbox. Distinto do `WhatsAppTemplateSummary` de
163
+ * `settings/`, e de propósito: aquele serve ao formulário que **edita** template e carrega o que
164
+ * a edição precisa (`shortId`, `variableCount`); este serve a quem só vai **escolher um para
165
+ * enviar**, e pedir os campos de edição obrigaria todo host a produzi-los sem uso.
166
+ */
167
+ interface ConversationTemplate {
168
+ name: string;
169
+ language: string;
170
+ status: string;
171
+ category?: string;
172
+ bodyText?: string | null;
173
+ }
125
174
  interface ConversationsApi {
126
175
  fetchMessages(conversationId: string, params?: {
127
176
  limit?: number;
128
177
  before?: string;
129
178
  }): Promise<MessagePayload[]>;
130
- fetchConversations(params?: {
131
- page?: number;
132
- limit?: number;
133
- waitingHuman?: boolean;
134
- search?: string;
135
- }): Promise<ConversationSummary[]>;
179
+ fetchConversations(params?: ListConversationsParams): Promise<ConversationSummary[] | ConversationPage>;
136
180
  sendMessage(conversationId: string, text: string): Promise<MessagePayload>;
137
181
  sendMedia(conversationId: string, data: {
138
182
  base64: string;
@@ -140,22 +184,61 @@ interface ConversationsApi {
140
184
  filename: string;
141
185
  caption?: string;
142
186
  }): Promise<MessagePayload>;
187
+ /**
188
+ * `templateName` é opcional porque reabrir a janela é a operação, e escolher *qual* template a
189
+ * usa nem sempre é decisão da UI: backends que guardam um template padrão configurado só
190
+ * precisam do "reabra". Exigir o nome obrigaria toda inbox a listar templates antes de poder
191
+ * mandar o primeiro — e a listagem é `listTemplates?`, opcional.
192
+ */
143
193
  sendTemplate(conversationId: string, data: {
144
- templateName: string;
194
+ templateName?: string;
145
195
  languageCode?: string;
146
196
  bodyParams?: string[];
147
197
  }): Promise<void>;
148
198
  markRead(conversationId: string): Promise<void>;
149
199
  getContext(conversationId: string): Promise<Record<string, unknown>>;
150
- getDocuments(conversationId: string, params?: {
151
- search?: string;
152
- page?: number;
153
- }): Promise<ConversationDocument[]>;
154
- getDocumentUrl(uploadId: string): Promise<string>;
200
+ getDocuments(conversationId: string, params?: ListDocumentsParams): Promise<ConversationDocument[] | ConversationDocumentPage>;
201
+ /**
202
+ * `disposition` decide entre abrir no navegador e baixar. É o backend que assina a URL e grava
203
+ * o `Content-Disposition` nela, então a escolha precisa viajar na chamada — depois de assinada
204
+ * não há como o cliente mudá-la. Ausente = o padrão do host.
205
+ */
206
+ getDocumentUrl(uploadId: string, disposition?: 'inline' | 'attachment'): Promise<string>;
207
+ /**
208
+ * Baixa vários arquivos num zip único.
209
+ *
210
+ * **Opcional por capacidade:** montar zip exige o host LER os bytes do storage, o que nem toda
211
+ * instalação faz — as que só assinam URL não conseguem. Ausente, o painel esconde a seleção em
212
+ * lote em vez de oferecer um botão que falha.
213
+ */
214
+ downloadDocumentsArchive?(conversationId: string, uploadIds: readonly string[]): Promise<Blob>;
155
215
  getMediaProxyUrl(mediaId: string): Promise<{
156
216
  mimeType: string;
157
217
  data: string;
158
218
  }>;
219
+ /**
220
+ * Operações de atendimento humano. **Opcionais por capacidade, não por descuido:** nem toda
221
+ * inbox tem fila humana — um canal só-bot, ou um chat de site sem operador, não sabe o que é
222
+ * assumir conversa. Quem não implementa não ganha o botão, em vez de ganhar um botão que
223
+ * estoura no clique. Os hooks devolvem `undefined` para a ação ausente, e é isso que a UI
224
+ * consulta para decidir se desenha a afordância.
225
+ */
226
+ takeover?(conversationId: string): Promise<void>;
227
+ release?(conversationId: string): Promise<void>;
228
+ /** Encerra o atendimento. Despedida, se houver, é decisão do host — o pacote não a inventa. */
229
+ finalize?(conversationId: string): Promise<void>;
230
+ markAllRead?(): Promise<void>;
231
+ listTemplates?(): Promise<ConversationTemplate[]>;
232
+ /**
233
+ * Transcrição completa gerada pelo servidor. Existe ao lado de `buildTranscriptText`, que monta
234
+ * a partir das mensagens já em memória: a tela costuma ter só a última página carregada, e
235
+ * exportar dali entregaria um recorte parcial com cara de histórico inteiro. Opcional porque
236
+ * nem todo backend expõe a rota — quem não tem continua usando o builder local.
237
+ */
238
+ exportTranscript?(conversationId: string): Promise<{
239
+ transcript: string;
240
+ filename: string;
241
+ }>;
159
242
  }
160
243
  /**
161
244
  * Superfície mínima de stream que o pacote consome — exatamente o que `useConversationRealtime`
@@ -194,6 +277,13 @@ interface ConversationSummary {
194
277
  waitingHuman: boolean;
195
278
  unread: number;
196
279
  currentState: string;
280
+ /**
281
+ * Atributos que só o produto conhece e desenha (tipo de financiamento, carteira, campanha). É a
282
+ * contraparte de leitura do `filters` de `ListConversationsParams`: o pacote transporta e nunca
283
+ * interpreta. Sem isto, exibir um selo próprio na linha exigiria o host manter uma segunda
284
+ * consulta paralela à mesma listagem — a implementação duplicada que o pacote existe para evitar.
285
+ */
286
+ attributes?: Record<string, string | undefined>;
197
287
  }
198
288
  interface ConversationDocument {
199
289
  id: string;
@@ -204,4 +294,4 @@ interface ConversationDocument {
204
294
  linkedAt: string;
205
295
  }
206
296
 
207
- export { CHANNEL_CAPABILITIES as C, DEFAULT_CONVERSATION_CHANNEL as D, type FormatContactHandleParams as F, HANDLE_KIND as H, type MessagePayload 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 ConversationChannel as f, type ConversationDocument as g, type ConversationEventSource as h, type ConversationSummary as i, type ConversationsApi as j, type ConversationsFeatures as k, type ConversationsTheme as l, type ConversationsUIConfig as m, type HandleKind as n, type ReopenMechanism as o, capabilitiesOf as p, channelFiltersFor as q, contactFlag as r, formatContactHandle as s };
297
+ export { CHANNEL_CAPABILITIES as C, DEFAULT_CONVERSATION_CHANNEL as D, type FormatContactHandleParams as F, HANDLE_KIND as H, type ListConversationsParams as L, type MessagePayload 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 ConversationChannel as f, type ConversationDocument as g, type ConversationDocumentPage as h, type ConversationEventSource as i, type ConversationPage as j, type ConversationSummary as k, type ConversationTemplate as l, type ConversationsApi as m, type ConversationsFeatures as n, type ConversationsTheme as o, type ConversationsUIConfig as p, type HandleKind as q, type ListDocumentsParams as r, type ReopenMechanism as s, capabilitiesOf as t, channelFiltersFor as u, contactFlag as v, formatContactHandle as w };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adatechnology/conversations-ui",
3
- "version": "0.1.0-rc.4",
3
+ "version": "0.1.0-rc.5",
4
4
  "description": "WhatsApp conversation UI components — parametrizável por endpoint, tema e feature flags",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -31,7 +31,7 @@
31
31
  "clsx": "^2.1.1",
32
32
  "lucide-react": "^1.21.0",
33
33
  "tailwind-merge": "^3.6.0",
34
- "@adatechnology/meta-whatsapp-contracts": "0.2.0-rc.4"
34
+ "@adatechnology/meta-whatsapp-contracts": "0.2.0-rc.5"
35
35
  },
36
36
  "peerDependencies": {
37
37
  "react": "^18 || ^19",