@adatechnology/conversations-ui 0.1.0-rc.2 → 0.1.0-rc.4
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 +726 -0
- package/dist/chunk-NV2RZ5KT.js +56 -0
- package/dist/{chunk-ZDURDZTM.js → chunk-OGRRHQQW.js} +1 -41
- package/dist/flows/index.js +6 -4
- package/dist/index.d.ts +323 -111
- package/dist/index.js +1032 -954
- package/dist/preview/index.d.ts +172 -0
- package/dist/preview/index.js +576 -0
- package/dist/styles.css +198 -0
- package/dist/types-C0PtaO7S.d.ts +207 -0
- package/package.json +10 -3
- package/src/Avatar.tsx +18 -3
- package/src/ChannelIcon.tsx +87 -0
- package/src/ConversationContextPanel.tsx +106 -0
- package/src/ConversationDocumentsPanel.tsx +107 -0
- package/src/ConversationHeader.tsx +239 -0
- package/src/ConversationListItem.tsx +36 -5
- package/src/ConversationLocalesProvider.tsx +16 -0
- package/src/ConversationRow.tsx +137 -0
- package/src/DateDivider.tsx +16 -3
- package/src/MediaRenderer.tsx +9 -9
- package/src/MessageBubble.tsx +24 -2
- package/src/MessageComposer.tsx +15 -2
- package/src/Wallpaper.tsx +4 -2
- package/src/WindowExpiredNotice.tsx +57 -0
- package/src/conversationChannel.test.ts +53 -0
- package/src/conversationChannel.ts +146 -0
- package/src/conversationTranscript.test.ts +65 -0
- package/src/conversationTranscript.ts +64 -0
- package/src/conversationWindow.test.ts +90 -0
- package/src/conversationWindow.ts +78 -0
- package/src/flows/FlowMapCanvas.tsx +2 -2
- package/src/hooks/useConversationDocuments.ts +4 -2
- package/src/index.ts +73 -4
- package/src/lib/cn.ts +15 -0
- package/src/lib/phone.ts +34 -0
- package/src/preview/ConversationPreview.tsx +148 -0
- package/src/preview/createMockConversationsApi.ts +111 -0
- package/src/preview/createMockSSEProvider.ts +40 -0
- package/src/preview/createPreviewWebhookClient.test.ts +105 -0
- package/src/preview/createPreviewWebhookClient.ts +99 -0
- package/src/preview/index.ts +40 -0
- package/src/preview/mockEventSource.ts +53 -0
- package/src/preview/preview.test.ts +175 -0
- package/src/preview/previewFixtures.ts +153 -0
- package/src/preview/previewStore.ts +193 -0
- package/src/preview/startPreviewScript.ts +60 -0
- package/src/providers/types.ts +36 -2
- package/src/settings/WhatsAppTemplatesSettings.tsx +106 -0
- package/src/styles.css +136 -0
- package/src/types.ts +8 -0
- package/src/useDarkMode.ts +26 -0
- package/src/useIsNarrow.ts +29 -0
|
@@ -0,0 +1,576 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ConversationWallpaper,
|
|
3
|
+
DateDivider,
|
|
4
|
+
MessageBubble,
|
|
5
|
+
MessageComposer
|
|
6
|
+
} from "../chunk-4R6Y43DQ.js";
|
|
7
|
+
import "../chunk-OGRRHQQW.js";
|
|
8
|
+
|
|
9
|
+
// src/preview/previewStore.ts
|
|
10
|
+
var GLOBAL_CHANNEL = "global";
|
|
11
|
+
function conversationChannel(conversationId) {
|
|
12
|
+
return `conv:${conversationId}`;
|
|
13
|
+
}
|
|
14
|
+
function createPreviewStore(params) {
|
|
15
|
+
const conversations = params.conversations.map((conversation) => ({ ...conversation }));
|
|
16
|
+
const messages = new Map(
|
|
17
|
+
Object.entries(params.messages).map(([conversationId, list]) => [conversationId, [...list]])
|
|
18
|
+
);
|
|
19
|
+
const listeners = /* @__PURE__ */ new Map();
|
|
20
|
+
const now = params.now ?? (() => /* @__PURE__ */ new Date());
|
|
21
|
+
let messageSequence = 0;
|
|
22
|
+
function emit(emission) {
|
|
23
|
+
for (const listener of listeners.get(emission.channel) ?? []) listener(emission);
|
|
24
|
+
}
|
|
25
|
+
function emitDataChanged() {
|
|
26
|
+
emit({ channel: GLOBAL_CHANNEL, event: "data-changed", payload: {} });
|
|
27
|
+
}
|
|
28
|
+
function findConversation(conversationId) {
|
|
29
|
+
return conversations.find((conversation) => conversation.id === conversationId);
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
listConversations(filters) {
|
|
33
|
+
return conversations.filter((conversation) => filters?.waitingHuman ? conversation.waitingHuman : true).filter((conversation) => {
|
|
34
|
+
const search = filters?.search?.toLowerCase();
|
|
35
|
+
if (!search) return true;
|
|
36
|
+
return conversation.whatsappNumber.includes(search) || (conversation.clientName?.toLowerCase().includes(search) ?? false);
|
|
37
|
+
}).map((conversation) => ({ ...conversation })).sort((left, right) => right.lastAt.localeCompare(left.lastAt));
|
|
38
|
+
},
|
|
39
|
+
listMessages(conversationId) {
|
|
40
|
+
return [...messages.get(conversationId) ?? []];
|
|
41
|
+
},
|
|
42
|
+
appendMessage(appendParams) {
|
|
43
|
+
messageSequence += 1;
|
|
44
|
+
const timestamp = now().toISOString();
|
|
45
|
+
const message = {
|
|
46
|
+
id: `preview-${messageSequence}`,
|
|
47
|
+
type: "text",
|
|
48
|
+
content: appendParams.content,
|
|
49
|
+
direction: appendParams.direction,
|
|
50
|
+
sender: appendParams.sender,
|
|
51
|
+
timestamp,
|
|
52
|
+
status: appendParams.direction === "outbound" ? "sent" : void 0
|
|
53
|
+
};
|
|
54
|
+
const conversationMessages = messages.get(appendParams.conversationId) ?? [];
|
|
55
|
+
messages.set(appendParams.conversationId, [...conversationMessages, message]);
|
|
56
|
+
const conversation = findConversation(appendParams.conversationId);
|
|
57
|
+
if (conversation) {
|
|
58
|
+
conversation.lastContent = appendParams.content;
|
|
59
|
+
conversation.lastDirection = appendParams.direction;
|
|
60
|
+
conversation.lastAt = timestamp;
|
|
61
|
+
if (appendParams.direction === "inbound") {
|
|
62
|
+
conversation.lastInboundAt = timestamp;
|
|
63
|
+
conversation.unread += 1;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
emit({
|
|
67
|
+
channel: conversationChannel(appendParams.conversationId),
|
|
68
|
+
event: "message",
|
|
69
|
+
payload: { direction: message.direction, sender: message.sender }
|
|
70
|
+
});
|
|
71
|
+
emitDataChanged();
|
|
72
|
+
return message;
|
|
73
|
+
},
|
|
74
|
+
setMode(modeParams) {
|
|
75
|
+
const conversation = findConversation(modeParams.conversationId);
|
|
76
|
+
if (!conversation) return;
|
|
77
|
+
conversation.mode = modeParams.mode;
|
|
78
|
+
conversation.assignedUserId = modeParams.assignedUserId ?? null;
|
|
79
|
+
if (modeParams.mode === "human") conversation.waitingHuman = false;
|
|
80
|
+
emit({
|
|
81
|
+
channel: conversationChannel(modeParams.conversationId),
|
|
82
|
+
event: "mode-changed",
|
|
83
|
+
payload: { mode: conversation.mode, assignedUserId: conversation.assignedUserId }
|
|
84
|
+
});
|
|
85
|
+
emitDataChanged();
|
|
86
|
+
},
|
|
87
|
+
requestHuman(conversationId) {
|
|
88
|
+
const conversation = findConversation(conversationId);
|
|
89
|
+
if (!conversation) return;
|
|
90
|
+
conversation.waitingHuman = true;
|
|
91
|
+
emitDataChanged();
|
|
92
|
+
},
|
|
93
|
+
markRead(conversationId) {
|
|
94
|
+
const conversation = findConversation(conversationId);
|
|
95
|
+
if (!conversation) return;
|
|
96
|
+
conversation.unread = 0;
|
|
97
|
+
emitDataChanged();
|
|
98
|
+
},
|
|
99
|
+
subscribe(channel, listener) {
|
|
100
|
+
const channelListeners = listeners.get(channel) ?? /* @__PURE__ */ new Set();
|
|
101
|
+
channelListeners.add(listener);
|
|
102
|
+
listeners.set(channel, channelListeners);
|
|
103
|
+
return () => {
|
|
104
|
+
channelListeners.delete(listener);
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// src/preview/mockEventSource.ts
|
|
111
|
+
function createMockEventSource() {
|
|
112
|
+
const listeners = /* @__PURE__ */ new Map();
|
|
113
|
+
let closed = false;
|
|
114
|
+
return {
|
|
115
|
+
get closed() {
|
|
116
|
+
return closed;
|
|
117
|
+
},
|
|
118
|
+
addEventListener(type, listener) {
|
|
119
|
+
const typeListeners = listeners.get(type) ?? /* @__PURE__ */ new Set();
|
|
120
|
+
typeListeners.add(listener);
|
|
121
|
+
listeners.set(type, typeListeners);
|
|
122
|
+
},
|
|
123
|
+
removeEventListener(type, listener) {
|
|
124
|
+
listeners.get(type)?.delete(listener);
|
|
125
|
+
},
|
|
126
|
+
close() {
|
|
127
|
+
closed = true;
|
|
128
|
+
listeners.clear();
|
|
129
|
+
},
|
|
130
|
+
emit(event, payload) {
|
|
131
|
+
if (closed) return;
|
|
132
|
+
const messageEvent = new MessageEvent(event, { data: JSON.stringify(payload) });
|
|
133
|
+
for (const listener of listeners.get(event) ?? []) listener(messageEvent);
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
}
|
|
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
|
+
// src/preview/previewFixtures.ts
|
|
241
|
+
var BASE_DAY = "2026-07-26";
|
|
242
|
+
function at(time) {
|
|
243
|
+
return `${BASE_DAY}T${time}.000Z`;
|
|
244
|
+
}
|
|
245
|
+
var PREVIEW_CONVERSATIONS = [
|
|
246
|
+
{
|
|
247
|
+
id: "5511988887777",
|
|
248
|
+
whatsappNumber: "5511988887777",
|
|
249
|
+
clientName: "Marina Alves",
|
|
250
|
+
lastContent: "quero 2kg de arroz e um \xF3leo",
|
|
251
|
+
lastDirection: "inbound",
|
|
252
|
+
lastAt: at("14:32:00"),
|
|
253
|
+
lastInboundAt: at("14:32:00"),
|
|
254
|
+
mode: "bot",
|
|
255
|
+
assignedUserId: null,
|
|
256
|
+
waitingHuman: false,
|
|
257
|
+
unread: 2,
|
|
258
|
+
currentState: "list_review"
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
id: "5511977776666",
|
|
262
|
+
whatsappNumber: "5511977776666",
|
|
263
|
+
clientName: "Diego Prado",
|
|
264
|
+
lastContent: "preciso falar com algu\xE9m",
|
|
265
|
+
lastDirection: "inbound",
|
|
266
|
+
lastAt: at("14:20:00"),
|
|
267
|
+
lastInboundAt: at("14:20:00"),
|
|
268
|
+
mode: "bot",
|
|
269
|
+
assignedUserId: null,
|
|
270
|
+
waitingHuman: true,
|
|
271
|
+
unread: 1,
|
|
272
|
+
currentState: "awaiting_human"
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
id: "5511966665555",
|
|
276
|
+
whatsappNumber: "5511966665555",
|
|
277
|
+
clientName: "Sofia Nakamura",
|
|
278
|
+
lastContent: "j\xE1 separei seu pedido, confere?",
|
|
279
|
+
lastDirection: "outbound",
|
|
280
|
+
lastAt: at("13:58:00"),
|
|
281
|
+
lastInboundAt: at("13:50:00"),
|
|
282
|
+
mode: "human",
|
|
283
|
+
assignedUserId: "agent-1",
|
|
284
|
+
waitingHuman: false,
|
|
285
|
+
unread: 0,
|
|
286
|
+
currentState: "human_handling"
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
id: "5511955554444",
|
|
290
|
+
whatsappNumber: "5511955554444",
|
|
291
|
+
lastContent: "\xC1udio",
|
|
292
|
+
lastDirection: "inbound",
|
|
293
|
+
lastAt: at("13:31:00"),
|
|
294
|
+
lastInboundAt: at("13:31:00"),
|
|
295
|
+
mode: "bot",
|
|
296
|
+
assignedUserId: null,
|
|
297
|
+
waitingHuman: false,
|
|
298
|
+
unread: 1,
|
|
299
|
+
currentState: "list_import"
|
|
300
|
+
}
|
|
301
|
+
];
|
|
302
|
+
var PREVIEW_MESSAGES = {
|
|
303
|
+
"5511988887777": [
|
|
304
|
+
{
|
|
305
|
+
id: "fixture-1",
|
|
306
|
+
type: "text",
|
|
307
|
+
content: "oi, boa tarde",
|
|
308
|
+
direction: "inbound",
|
|
309
|
+
sender: "customer",
|
|
310
|
+
timestamp: at("14:30:00")
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
id: "fixture-2",
|
|
314
|
+
type: "text",
|
|
315
|
+
content: "Boa tarde! Me manda sua lista de compras que eu monto o carrinho.",
|
|
316
|
+
direction: "outbound",
|
|
317
|
+
sender: "bot",
|
|
318
|
+
timestamp: at("14:30:30"),
|
|
319
|
+
status: "read"
|
|
320
|
+
},
|
|
321
|
+
{
|
|
322
|
+
id: "fixture-3",
|
|
323
|
+
type: "text",
|
|
324
|
+
content: "quero 2kg de arroz e um \xF3leo",
|
|
325
|
+
direction: "inbound",
|
|
326
|
+
sender: "customer",
|
|
327
|
+
timestamp: at("14:32:00")
|
|
328
|
+
}
|
|
329
|
+
],
|
|
330
|
+
"5511977776666": [
|
|
331
|
+
{
|
|
332
|
+
id: "fixture-4",
|
|
333
|
+
type: "text",
|
|
334
|
+
content: "esse valor do frete est\xE1 certo?",
|
|
335
|
+
direction: "inbound",
|
|
336
|
+
sender: "customer",
|
|
337
|
+
timestamp: at("14:19:00")
|
|
338
|
+
},
|
|
339
|
+
{
|
|
340
|
+
id: "fixture-5",
|
|
341
|
+
type: "text",
|
|
342
|
+
content: "preciso falar com algu\xE9m",
|
|
343
|
+
direction: "inbound",
|
|
344
|
+
sender: "customer",
|
|
345
|
+
timestamp: at("14:20:00")
|
|
346
|
+
}
|
|
347
|
+
],
|
|
348
|
+
"5511966665555": [
|
|
349
|
+
{
|
|
350
|
+
id: "fixture-6",
|
|
351
|
+
type: "text",
|
|
352
|
+
content: "consegue trocar o leite integral por desnatado?",
|
|
353
|
+
direction: "inbound",
|
|
354
|
+
sender: "customer",
|
|
355
|
+
timestamp: at("13:50:00")
|
|
356
|
+
},
|
|
357
|
+
{
|
|
358
|
+
id: "fixture-7",
|
|
359
|
+
type: "text",
|
|
360
|
+
content: "j\xE1 separei seu pedido, confere?",
|
|
361
|
+
direction: "outbound",
|
|
362
|
+
sender: "agent",
|
|
363
|
+
timestamp: at("13:58:00"),
|
|
364
|
+
status: "delivered",
|
|
365
|
+
agentName: "Ana"
|
|
366
|
+
}
|
|
367
|
+
],
|
|
368
|
+
"5511955554444": [
|
|
369
|
+
{
|
|
370
|
+
id: "fixture-8",
|
|
371
|
+
type: "audio",
|
|
372
|
+
mediaId: "preview-audio-1",
|
|
373
|
+
mimeType: "audio/ogg",
|
|
374
|
+
direction: "inbound",
|
|
375
|
+
sender: "customer",
|
|
376
|
+
timestamp: at("13:31:00")
|
|
377
|
+
}
|
|
378
|
+
]
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
// src/preview/ConversationPreview.tsx
|
|
382
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
383
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
384
|
+
var GROUPING_WINDOW_MS = 5 * 60 * 1e3;
|
|
385
|
+
function decorate(messages) {
|
|
386
|
+
return messages.map((message, index) => {
|
|
387
|
+
const previous = index > 0 ? messages[index - 1] : void 0;
|
|
388
|
+
const currentTime = new Date(message.timestamp).getTime();
|
|
389
|
+
const previousTime = previous ? new Date(previous.timestamp).getTime() : 0;
|
|
390
|
+
return {
|
|
391
|
+
message,
|
|
392
|
+
isFirstInGroup: !previous || previous.sender !== message.sender || currentTime - previousTime > GROUPING_WINDOW_MS,
|
|
393
|
+
showDateDivider: !previous || new Date(message.timestamp).toDateString() !== new Date(previous.timestamp).toDateString()
|
|
394
|
+
};
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
function ConversationPreview({
|
|
398
|
+
client,
|
|
399
|
+
sse,
|
|
400
|
+
conversationId,
|
|
401
|
+
loadMessages,
|
|
402
|
+
placeholder
|
|
403
|
+
}) {
|
|
404
|
+
const [messages, setMessages] = useState([]);
|
|
405
|
+
const [failure, setFailure] = useState(void 0);
|
|
406
|
+
const loadMessagesRef = useRef(loadMessages);
|
|
407
|
+
const bottomRef = useRef(null);
|
|
408
|
+
loadMessagesRef.current = loadMessages;
|
|
409
|
+
const refresh = useCallback(async () => {
|
|
410
|
+
try {
|
|
411
|
+
setMessages(await loadMessagesRef.current(conversationId));
|
|
412
|
+
} catch {
|
|
413
|
+
setMessages([]);
|
|
414
|
+
}
|
|
415
|
+
}, [conversationId]);
|
|
416
|
+
useEffect(() => {
|
|
417
|
+
void refresh();
|
|
418
|
+
}, [refresh]);
|
|
419
|
+
useEffect(() => {
|
|
420
|
+
const source = sse.connectConversationStream(conversationId);
|
|
421
|
+
const handler = () => {
|
|
422
|
+
void refresh();
|
|
423
|
+
};
|
|
424
|
+
source.addEventListener("message", handler);
|
|
425
|
+
return () => {
|
|
426
|
+
source.removeEventListener("message", handler);
|
|
427
|
+
source.close();
|
|
428
|
+
};
|
|
429
|
+
}, [sse, conversationId, refresh]);
|
|
430
|
+
useEffect(() => {
|
|
431
|
+
bottomRef.current?.scrollIntoView({ behavior: "smooth" });
|
|
432
|
+
}, [messages]);
|
|
433
|
+
const rendered = useMemo(() => decorate(messages), [messages]);
|
|
434
|
+
async function handleSend(text) {
|
|
435
|
+
setFailure(void 0);
|
|
436
|
+
try {
|
|
437
|
+
await client.sendText(text);
|
|
438
|
+
await refresh();
|
|
439
|
+
} catch (error) {
|
|
440
|
+
setFailure(error instanceof Error ? error.message : "Falha ao entregar a mensagem no webhook.");
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex h-full min-h-0 flex-col", children: [
|
|
444
|
+
/* @__PURE__ */ jsxs(ConversationWallpaper, { className: "flex-1 min-h-0 overflow-y-auto px-4 py-3", children: [
|
|
445
|
+
rendered.map(({ message, isFirstInGroup, showDateDivider }) => /* @__PURE__ */ jsxs("div", { children: [
|
|
446
|
+
showDateDivider ? /* @__PURE__ */ jsx(DateDivider, { iso: message.timestamp }) : null,
|
|
447
|
+
/* @__PURE__ */ jsx(
|
|
448
|
+
MessageBubble,
|
|
449
|
+
{
|
|
450
|
+
message,
|
|
451
|
+
isMine: message.direction === "inbound",
|
|
452
|
+
isFirstInGroup
|
|
453
|
+
}
|
|
454
|
+
)
|
|
455
|
+
] }, message.id)),
|
|
456
|
+
/* @__PURE__ */ jsx("div", { ref: bottomRef })
|
|
457
|
+
] }),
|
|
458
|
+
failure ? /* @__PURE__ */ jsx("p", { role: "alert", className: "px-4 py-2 text-sm text-red-600 dark:text-red-400", children: failure }) : null,
|
|
459
|
+
/* @__PURE__ */ jsx(
|
|
460
|
+
MessageComposer,
|
|
461
|
+
{
|
|
462
|
+
onSend: (text) => void handleSend(text),
|
|
463
|
+
placeholder: placeholder ?? "Escreva como o cliente\u2026"
|
|
464
|
+
}
|
|
465
|
+
)
|
|
466
|
+
] });
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// src/preview/createPreviewWebhookClient.ts
|
|
470
|
+
import {
|
|
471
|
+
buildInboundAudioPayload,
|
|
472
|
+
buildInboundInteractivePayload,
|
|
473
|
+
buildInboundTextPayload,
|
|
474
|
+
serializeWebhookPayload
|
|
475
|
+
} from "@adatechnology/meta-whatsapp-contracts/testing";
|
|
476
|
+
var PreviewInProductionError = class extends Error {
|
|
477
|
+
constructor() {
|
|
478
|
+
super("O preview de conversa carrega um app secret e n\xE3o pode ser montado em produ\xE7\xE3o.");
|
|
479
|
+
this.name = "PreviewInProductionError";
|
|
480
|
+
}
|
|
481
|
+
};
|
|
482
|
+
var PreviewWebhookRejectedError = class extends Error {
|
|
483
|
+
constructor(status) {
|
|
484
|
+
super(`O webhook recusou a entrega do preview (HTTP ${status}).`);
|
|
485
|
+
this.status = status;
|
|
486
|
+
this.name = "PreviewWebhookRejectedError";
|
|
487
|
+
}
|
|
488
|
+
};
|
|
489
|
+
function assertPreviewEnvironment(isProduction) {
|
|
490
|
+
if (isProduction) throw new PreviewInProductionError();
|
|
491
|
+
}
|
|
492
|
+
async function signWithWebCrypto(params) {
|
|
493
|
+
const encoder = new TextEncoder();
|
|
494
|
+
const key = await globalThis.crypto.subtle.importKey(
|
|
495
|
+
"raw",
|
|
496
|
+
encoder.encode(params.appSecret),
|
|
497
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
498
|
+
false,
|
|
499
|
+
["sign"]
|
|
500
|
+
);
|
|
501
|
+
const signature = await globalThis.crypto.subtle.sign("HMAC", key, encoder.encode(params.rawBody));
|
|
502
|
+
return `sha256=${[...new Uint8Array(signature)].map((byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
503
|
+
}
|
|
504
|
+
function createPreviewWebhookClient(params) {
|
|
505
|
+
const sendPayload = async (payload) => {
|
|
506
|
+
const rawBody = serializeWebhookPayload(payload);
|
|
507
|
+
const signature = await signWithWebCrypto({ rawBody, appSecret: params.appSecret });
|
|
508
|
+
const performRequest = params.fetchImplementation ?? fetch;
|
|
509
|
+
const response = await performRequest(params.webhookUrl, {
|
|
510
|
+
method: "POST",
|
|
511
|
+
headers: { "content-type": "application/json", "x-hub-signature-256": signature },
|
|
512
|
+
body: rawBody
|
|
513
|
+
});
|
|
514
|
+
if (!response.ok) throw new PreviewWebhookRejectedError(response.status);
|
|
515
|
+
};
|
|
516
|
+
const envelope = { from: params.from, phoneNumberId: params.phoneNumberId };
|
|
517
|
+
return {
|
|
518
|
+
sendText: (text) => sendPayload(buildInboundTextPayload({ ...envelope, text })),
|
|
519
|
+
sendButtonReply: (reply) => sendPayload(buildInboundInteractivePayload({ ...envelope, buttonReply: reply })),
|
|
520
|
+
sendListReply: (reply) => sendPayload(buildInboundInteractivePayload({ ...envelope, listReply: reply })),
|
|
521
|
+
sendAudio: (mediaId) => sendPayload(buildInboundAudioPayload({ ...envelope, mediaId }))
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// src/preview/startPreviewScript.ts
|
|
526
|
+
var DEFAULT_PREVIEW_SCRIPT = [
|
|
527
|
+
(store) => store.appendMessage({
|
|
528
|
+
conversationId: "5511988887777",
|
|
529
|
+
content: "pode trocar o \xF3leo por azeite?",
|
|
530
|
+
direction: "inbound",
|
|
531
|
+
sender: "customer"
|
|
532
|
+
}),
|
|
533
|
+
(store) => store.requestHuman("5511988887777"),
|
|
534
|
+
(store) => store.appendMessage({
|
|
535
|
+
conversationId: "5511955554444",
|
|
536
|
+
content: "e o troco?",
|
|
537
|
+
direction: "inbound",
|
|
538
|
+
sender: "customer"
|
|
539
|
+
}),
|
|
540
|
+
(store) => store.setMode({ conversationId: "5511977776666", mode: "human", assignedUserId: "agent-2" }),
|
|
541
|
+
(store) => store.appendMessage({
|
|
542
|
+
conversationId: "5511977776666",
|
|
543
|
+
content: "Oi Diego, sou a Ana. J\xE1 vi seu pedido.",
|
|
544
|
+
direction: "outbound",
|
|
545
|
+
sender: "agent"
|
|
546
|
+
}),
|
|
547
|
+
(store) => store.setMode({ conversationId: "5511966665555", mode: "bot" })
|
|
548
|
+
];
|
|
549
|
+
var DEFAULT_INTERVAL_MS = 4e3;
|
|
550
|
+
function startPreviewScript(params) {
|
|
551
|
+
const steps = params.steps ?? DEFAULT_PREVIEW_SCRIPT;
|
|
552
|
+
const intervalMs = params.intervalMs ?? DEFAULT_INTERVAL_MS;
|
|
553
|
+
let index = 0;
|
|
554
|
+
const timer = setInterval(() => {
|
|
555
|
+
steps[index % steps.length]?.(params.store);
|
|
556
|
+
index += 1;
|
|
557
|
+
}, intervalMs);
|
|
558
|
+
return () => clearInterval(timer);
|
|
559
|
+
}
|
|
560
|
+
export {
|
|
561
|
+
ConversationPreview,
|
|
562
|
+
DEFAULT_PREVIEW_SCRIPT,
|
|
563
|
+
GLOBAL_CHANNEL,
|
|
564
|
+
PREVIEW_CONVERSATIONS,
|
|
565
|
+
PREVIEW_MESSAGES,
|
|
566
|
+
PreviewInProductionError,
|
|
567
|
+
PreviewWebhookRejectedError,
|
|
568
|
+
assertPreviewEnvironment,
|
|
569
|
+
conversationChannel,
|
|
570
|
+
createMockConversationsApi,
|
|
571
|
+
createMockEventSource,
|
|
572
|
+
createMockSSEProvider,
|
|
573
|
+
createPreviewStore,
|
|
574
|
+
createPreviewWebhookClient,
|
|
575
|
+
startPreviewScript
|
|
576
|
+
};
|