@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.
- package/dist/{chunk-4R6Y43DQ.js → chunk-YWITIIHD.js} +18 -16
- package/dist/index.d.ts +102 -25
- package/dist/index.js +371 -60
- package/dist/preview/index.d.ts +17 -3
- package/dist/preview/index.js +324 -103
- package/dist/{types-C0PtaO7S.d.ts → types-C2Yexi8A.d.ts} +103 -13
- package/package.json +2 -2
- package/src/ConversationDocumentsPanel.tsx +342 -24
- package/src/FileIcon.test.ts +38 -0
- package/src/FileIcon.tsx +15 -4
- package/src/MediaRenderer.tsx +5 -2
- package/src/hooks/useConversationActions.ts +56 -0
- package/src/hooks/useConversationDocuments.ts +11 -7
- package/src/hooks/useConversationList.ts +15 -9
- package/src/hooks/useConversationMessages.ts +2 -2
- package/src/index.ts +15 -1
- package/src/lib/cn.test.ts +29 -0
- package/src/lib/paginated.test.ts +33 -0
- package/src/lib/paginated.ts +26 -0
- package/src/preview/createMockConversationsApi.ts +113 -7
- package/src/preview/index.ts +1 -1
- package/src/preview/preview.test.ts +5 -3
- package/src/preview/previewFixtures.ts +156 -1
- package/src/providers/types.ts +110 -9
- package/src/useWaitingNotifications.ts +74 -29
package/dist/preview/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
DateDivider,
|
|
4
4
|
MessageBubble,
|
|
5
5
|
MessageComposer
|
|
6
|
-
} from "../chunk-
|
|
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
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
|
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.
|
|
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.
|
|
34
|
+
"@adatechnology/meta-whatsapp-contracts": "0.2.0-rc.5"
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|
|
37
37
|
"react": "^18 || ^19",
|