@adatechnology/conversations-ui 0.1.0-rc.34 → 0.1.0-rc.36
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/{types-De5aN-E_.d.ts → ConversationSimulatorPanel--5fIzXWY.d.ts} +303 -1
- package/dist/{chunk-CUYYYZWD.js → chunk-BJNRLLDO.js} +393 -2
- package/dist/index.d.ts +69 -8
- package/dist/index.js +123 -92
- package/dist/preview/index.d.ts +5 -210
- package/dist/preview/index.js +36 -242
- package/package.json +1 -1
- package/src/index.ts +12 -0
- package/src/preview/ConversationPreview.tsx +56 -34
- package/src/preview/ConversationSimulatorClient.ts +143 -0
- package/src/preview/ConversationSimulatorPanel.tsx +46 -4
- package/src/preview/index.ts +19 -1
- package/src/settings/MessagesWorkspace.tsx +113 -10
- package/src/settings/WhatsAppTemplatesSettings.test.tsx +61 -0
- package/src/settings/WhatsAppTemplatesSettings.tsx +14 -2
- package/src/workspace/ConversationsWorkspace.tsx +82 -5
- package/src/workspace/index.ts +6 -1
package/dist/index.js
CHANGED
|
@@ -6,18 +6,23 @@ import {
|
|
|
6
6
|
AudioPlayer,
|
|
7
7
|
AudioRecorderButton,
|
|
8
8
|
AudioTranscription,
|
|
9
|
+
CHANNEL_CAPABILITIES,
|
|
10
|
+
CHANNEL_FILTER_ALL,
|
|
9
11
|
COMPOSER_BAR_CLASS,
|
|
10
12
|
COMPOSER_COMPACT_WIDTH,
|
|
11
13
|
COMPOSER_MONOSPACE_CLASS,
|
|
12
14
|
COMPOSER_TOOL_BUTTON_ACTIVE_CLASS,
|
|
13
15
|
COMPOSER_TOOL_BUTTON_CLASS,
|
|
14
16
|
COMPOSER_TOOL_BUTTON_IDLE_CLASS,
|
|
17
|
+
CONVERSATION_CHANNEL,
|
|
15
18
|
ConversationDocumentsPanel,
|
|
16
19
|
ConversationLocalesProvider,
|
|
20
|
+
ConversationSimulatorPanel,
|
|
17
21
|
ConversationWallpaper,
|
|
18
22
|
ConversationsProvider,
|
|
19
23
|
DEFAULT_ACCEPTED_FILE_TYPES,
|
|
20
24
|
DEFAULT_AUDIO_RECORDER_BUTTON_LABELS,
|
|
25
|
+
DEFAULT_CONVERSATION_CHANNEL,
|
|
21
26
|
DEFAULT_CONVERSATION_DOCUMENTS_LABELS,
|
|
22
27
|
DEFAULT_DOCUMENTS_LIBRARY_LABELS,
|
|
23
28
|
DEFAULT_EMOJI_PICKER_LABELS,
|
|
@@ -31,6 +36,7 @@ import {
|
|
|
31
36
|
EMOJI_CATEGORIES,
|
|
32
37
|
EmojiPicker,
|
|
33
38
|
FileIcon,
|
|
39
|
+
HANDLE_KIND,
|
|
34
40
|
InteractiveMessage,
|
|
35
41
|
Lightbox,
|
|
36
42
|
MediaRenderer,
|
|
@@ -38,18 +44,22 @@ import {
|
|
|
38
44
|
MessageComposer,
|
|
39
45
|
PAGINATION_LABELS,
|
|
40
46
|
QUICK_REPLY_PILL_CLASS,
|
|
47
|
+
REOPEN_MECHANISM,
|
|
41
48
|
StatusTicks,
|
|
42
49
|
applyQuickReplyVariables,
|
|
50
|
+
capabilitiesOf,
|
|
51
|
+
channelFiltersFor,
|
|
43
52
|
cn,
|
|
53
|
+
contactFlag,
|
|
44
54
|
conversationsOf,
|
|
45
55
|
createMediaUrlResolver,
|
|
46
56
|
documentTypeLabel,
|
|
57
|
+
formatContactHandle,
|
|
47
58
|
formatDateTime,
|
|
48
59
|
formatFileSize,
|
|
49
60
|
formatPhone,
|
|
50
61
|
formatTimestamp,
|
|
51
62
|
isSameDay,
|
|
52
|
-
phoneCountryFlag,
|
|
53
63
|
phoneInitials,
|
|
54
64
|
resolveQuickReply,
|
|
55
65
|
searchEmojis,
|
|
@@ -58,7 +68,7 @@ import {
|
|
|
58
68
|
useConversationDocuments,
|
|
59
69
|
useConversationLocales,
|
|
60
70
|
useConversations
|
|
61
|
-
} from "./chunk-
|
|
71
|
+
} from "./chunk-BJNRLLDO.js";
|
|
62
72
|
import {
|
|
63
73
|
ZERO_WIDTH_SPACE,
|
|
64
74
|
htmlToWA,
|
|
@@ -834,87 +844,6 @@ function Avatar({ name, avatarUrl, size = "md", className = "", labels }) {
|
|
|
834
844
|
|
|
835
845
|
// src/ConversationListItem.tsx
|
|
836
846
|
import { useMemo } from "react";
|
|
837
|
-
|
|
838
|
-
// src/conversationChannel.ts
|
|
839
|
-
var CONVERSATION_CHANNEL = {
|
|
840
|
-
WHATSAPP: "whatsapp",
|
|
841
|
-
MESSENGER: "messenger",
|
|
842
|
-
INSTAGRAM: "instagram",
|
|
843
|
-
WEBCHAT: "webchat"
|
|
844
|
-
};
|
|
845
|
-
var DEFAULT_CONVERSATION_CHANNEL = CONVERSATION_CHANNEL.WHATSAPP;
|
|
846
|
-
var REOPEN_MECHANISM = {
|
|
847
|
-
TEMPLATE: "template",
|
|
848
|
-
TAG: "tag",
|
|
849
|
-
NONE: "none"
|
|
850
|
-
};
|
|
851
|
-
var HANDLE_KIND = {
|
|
852
|
-
PHONE: "phone",
|
|
853
|
-
USERNAME: "username",
|
|
854
|
-
SESSION: "session"
|
|
855
|
-
};
|
|
856
|
-
var CHANNEL_CAPABILITIES = {
|
|
857
|
-
[CONVERSATION_CHANNEL.WHATSAPP]: {
|
|
858
|
-
label: "WhatsApp",
|
|
859
|
-
icon: "\u{1F4AC}",
|
|
860
|
-
hasSessionWindow: true,
|
|
861
|
-
windowHours: 24,
|
|
862
|
-
reopenMechanism: REOPEN_MECHANISM.TEMPLATE,
|
|
863
|
-
handleKind: HANDLE_KIND.PHONE
|
|
864
|
-
},
|
|
865
|
-
[CONVERSATION_CHANNEL.MESSENGER]: {
|
|
866
|
-
// Messenger também tem 24h, mas reabre com message tag — não com template aprovado.
|
|
867
|
-
label: "Messenger",
|
|
868
|
-
icon: "\u{1F4E8}",
|
|
869
|
-
hasSessionWindow: true,
|
|
870
|
-
windowHours: 24,
|
|
871
|
-
reopenMechanism: REOPEN_MECHANISM.TAG,
|
|
872
|
-
handleKind: HANDLE_KIND.USERNAME
|
|
873
|
-
},
|
|
874
|
-
[CONVERSATION_CHANNEL.INSTAGRAM]: {
|
|
875
|
-
label: "Instagram",
|
|
876
|
-
icon: "\u{1F4F7}",
|
|
877
|
-
hasSessionWindow: true,
|
|
878
|
-
windowHours: 24,
|
|
879
|
-
reopenMechanism: REOPEN_MECHANISM.TAG,
|
|
880
|
-
handleKind: HANDLE_KIND.USERNAME
|
|
881
|
-
},
|
|
882
|
-
[CONVERSATION_CHANNEL.WEBCHAT]: {
|
|
883
|
-
// Chat próprio: sem intermediário, sem janela. Bloquear o composer aqui seria inventar limite.
|
|
884
|
-
label: "Chat do site",
|
|
885
|
-
icon: "\u{1F310}",
|
|
886
|
-
hasSessionWindow: false,
|
|
887
|
-
windowHours: 0,
|
|
888
|
-
reopenMechanism: REOPEN_MECHANISM.NONE,
|
|
889
|
-
handleKind: HANDLE_KIND.SESSION
|
|
890
|
-
}
|
|
891
|
-
};
|
|
892
|
-
function capabilitiesOf(channel) {
|
|
893
|
-
return CHANNEL_CAPABILITIES[channel ?? DEFAULT_CONVERSATION_CHANNEL];
|
|
894
|
-
}
|
|
895
|
-
var CHANNEL_FILTER_ALL = "all";
|
|
896
|
-
function channelFiltersFor(conversations) {
|
|
897
|
-
const present = new Set(
|
|
898
|
-
conversations.map((conversation) => conversation.channel ?? DEFAULT_CONVERSATION_CHANNEL)
|
|
899
|
-
);
|
|
900
|
-
if (present.size < 2) return [];
|
|
901
|
-
const ordered = Object.keys(CHANNEL_CAPABILITIES).filter((channel) => present.has(channel));
|
|
902
|
-
return [
|
|
903
|
-
{ value: CHANNEL_FILTER_ALL, label: "Todos" },
|
|
904
|
-
...ordered.map((channel) => ({ value: channel, label: CHANNEL_CAPABILITIES[channel].label }))
|
|
905
|
-
];
|
|
906
|
-
}
|
|
907
|
-
function formatContactHandle(params) {
|
|
908
|
-
const { handleKind } = capabilitiesOf(params.channel);
|
|
909
|
-
if (handleKind === HANDLE_KIND.PHONE) return formatPhone(params.handle);
|
|
910
|
-
if (handleKind === HANDLE_KIND.USERNAME) return params.handle.startsWith("@") ? params.handle : `@${params.handle}`;
|
|
911
|
-
return `Visitante ${params.handle.slice(-6)}`;
|
|
912
|
-
}
|
|
913
|
-
function contactFlag(params) {
|
|
914
|
-
return capabilitiesOf(params.channel).handleKind === HANDLE_KIND.PHONE ? phoneCountryFlag(params.handle) : "";
|
|
915
|
-
}
|
|
916
|
-
|
|
917
|
-
// src/ConversationListItem.tsx
|
|
918
847
|
import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
919
848
|
var DEFAULT_CONVERSATION_LIST_ITEM_LABELS = {
|
|
920
849
|
expiredWindow: "Janela expirada",
|
|
@@ -3338,6 +3267,7 @@ function WhatsAppTemplatesSettings({
|
|
|
3338
3267
|
labels: labelsOverride,
|
|
3339
3268
|
settingsLabels,
|
|
3340
3269
|
create,
|
|
3270
|
+
extraRoleForms,
|
|
3341
3271
|
...settingsProps
|
|
3342
3272
|
}) {
|
|
3343
3273
|
const labels = { ...DEFAULT_TEMPLATES_SETTINGS_LABELS, ...labelsOverride };
|
|
@@ -3371,7 +3301,10 @@ function WhatsAppTemplatesSettings({
|
|
|
3371
3301
|
}
|
|
3372
3302
|
) : null
|
|
3373
3303
|
] }),
|
|
3374
|
-
tab === TEMPLATE_SETTINGS_TAB.SELECT ? /* @__PURE__ */
|
|
3304
|
+
tab === TEMPLATE_SETTINGS_TAB.SELECT ? /* @__PURE__ */ jsxs20("div", { className: "space-y-6", children: [
|
|
3305
|
+
/* @__PURE__ */ jsx22(WhatsAppTemplateSettingsForm, { ...settingsProps, ...settingsLabels ? { labels: settingsLabels } : {} }),
|
|
3306
|
+
extraRoleForms
|
|
3307
|
+
] }) : create ? /* @__PURE__ */ jsx22(WhatsAppCreateTemplateForm, { ...create }) : null
|
|
3375
3308
|
] });
|
|
3376
3309
|
}
|
|
3377
3310
|
|
|
@@ -3461,7 +3394,7 @@ function TopicsForm({
|
|
|
3461
3394
|
|
|
3462
3395
|
// src/settings/MessagesWorkspace.tsx
|
|
3463
3396
|
import { useCallback as useCallback6, useEffect as useEffect9, useState as useState14 } from "react";
|
|
3464
|
-
import { jsx as jsx24, jsxs as jsxs22 } from "react/jsx-runtime";
|
|
3397
|
+
import { Fragment as Fragment6, jsx as jsx24, jsxs as jsxs22 } from "react/jsx-runtime";
|
|
3465
3398
|
var DEFAULT_LABELS7 = {
|
|
3466
3399
|
title: "Mensagens",
|
|
3467
3400
|
subtitle: "Mensagens do bot e templates do WhatsApp.",
|
|
@@ -3510,9 +3443,12 @@ function MessagesWorkspace({
|
|
|
3510
3443
|
farewellPlaceholders,
|
|
3511
3444
|
availableVariables,
|
|
3512
3445
|
renderTemplatesNotice,
|
|
3446
|
+
createTemplatePreviewCompanyName,
|
|
3447
|
+
createTemplateVariableExamples,
|
|
3513
3448
|
className
|
|
3514
3449
|
}) {
|
|
3515
3450
|
const labels = { ...DEFAULT_LABELS7, ...labelsOverride };
|
|
3451
|
+
const templateRoles = api.templateRoles ?? [];
|
|
3516
3452
|
const hasTopics = Boolean(api.getTopics && api.saveTopics);
|
|
3517
3453
|
const hasTemplates = Boolean(api.getTemplateSettings && api.saveTemplateSettings);
|
|
3518
3454
|
const hasTranscription = Boolean(api.getTranscription && api.saveTranscription);
|
|
@@ -3530,6 +3466,9 @@ function MessagesWorkspace({
|
|
|
3530
3466
|
const [templatesError, setTemplatesError] = useState14(false);
|
|
3531
3467
|
const [savingTemplate, setSavingTemplate] = useState14(false);
|
|
3532
3468
|
const [templateSaved, setTemplateSaved] = useState14(false);
|
|
3469
|
+
const [roleSettings, setRoleSettings] = useState14({});
|
|
3470
|
+
const [savingRole, setSavingRole] = useState14({});
|
|
3471
|
+
const [roleSaved, setRoleSaved] = useState14({});
|
|
3533
3472
|
const [createTemplate, setCreateTemplate] = useState14(EMPTY_CREATE_TEMPLATE);
|
|
3534
3473
|
const [creatingTemplate, setCreatingTemplate] = useState14(false);
|
|
3535
3474
|
const [createResult, setCreateResult] = useState14(null);
|
|
@@ -3552,17 +3491,20 @@ function MessagesWorkspace({
|
|
|
3552
3491
|
let active = true;
|
|
3553
3492
|
async function load() {
|
|
3554
3493
|
try {
|
|
3555
|
-
const
|
|
3494
|
+
const roles = api.templateRoles ?? [];
|
|
3495
|
+
const [loadedMessages, loadedTopics, loadedTemplateSettings, loadedTranscription, loadedRoleSettings] = await Promise.all([
|
|
3556
3496
|
api.getMessages(),
|
|
3557
3497
|
api.getTopics?.(),
|
|
3558
3498
|
api.getTemplateSettings?.(),
|
|
3559
|
-
api.getTranscription?.()
|
|
3499
|
+
api.getTranscription?.(),
|
|
3500
|
+
Promise.all(roles.map((role) => role.getSettings()))
|
|
3560
3501
|
]);
|
|
3561
3502
|
if (!active) return;
|
|
3562
3503
|
setMessages(loadedMessages);
|
|
3563
3504
|
if (loadedTopics) setTopics(loadedTopics);
|
|
3564
3505
|
if (loadedTemplateSettings) setTemplateSettings(loadedTemplateSettings);
|
|
3565
3506
|
if (loadedTranscription) setTranscription(loadedTranscription);
|
|
3507
|
+
setRoleSettings(Object.fromEntries(roles.map((role, index) => [role.key, loadedRoleSettings[index]])));
|
|
3566
3508
|
setLoadState("ready");
|
|
3567
3509
|
} catch {
|
|
3568
3510
|
if (active) setLoadState("error");
|
|
@@ -3612,6 +3554,36 @@ function MessagesWorkspace({
|
|
|
3612
3554
|
setSavingTemplate(false);
|
|
3613
3555
|
}
|
|
3614
3556
|
}
|
|
3557
|
+
function handleSaveRole(role) {
|
|
3558
|
+
return async (event) => {
|
|
3559
|
+
event.preventDefault();
|
|
3560
|
+
const settings = roleSettings[role.key];
|
|
3561
|
+
if (!settings) return;
|
|
3562
|
+
setSavingRole((previous) => ({ ...previous, [role.key]: true }));
|
|
3563
|
+
try {
|
|
3564
|
+
await role.saveSettings(settings);
|
|
3565
|
+
setRoleSaved((previous) => ({ ...previous, [role.key]: true }));
|
|
3566
|
+
setTimeout(() => setRoleSaved((previous) => ({ ...previous, [role.key]: false })), SAVE_FEEDBACK_MS);
|
|
3567
|
+
} finally {
|
|
3568
|
+
setSavingRole((previous) => ({ ...previous, [role.key]: false }));
|
|
3569
|
+
}
|
|
3570
|
+
};
|
|
3571
|
+
}
|
|
3572
|
+
function handleSelectRoleTemplate(role, name, template) {
|
|
3573
|
+
setRoleSettings((previous) => {
|
|
3574
|
+
const current = previous[role.key] ?? EMPTY_TEMPLATE_SETTINGS;
|
|
3575
|
+
if (!template) return { ...previous, [role.key]: { ...current, templateName: name } };
|
|
3576
|
+
const shouldSeedVariables = template.variableCount > 0 && current.variables.length === 0;
|
|
3577
|
+
return {
|
|
3578
|
+
...previous,
|
|
3579
|
+
[role.key]: {
|
|
3580
|
+
templateName: name,
|
|
3581
|
+
templateLanguage: template.language,
|
|
3582
|
+
variables: shouldSeedVariables ? Array.from({ length: template.variableCount }, () => "") : current.variables
|
|
3583
|
+
}
|
|
3584
|
+
};
|
|
3585
|
+
});
|
|
3586
|
+
}
|
|
3615
3587
|
async function handleCreateTemplate(event) {
|
|
3616
3588
|
event.preventDefault();
|
|
3617
3589
|
if (!api.createTemplate) return;
|
|
@@ -3742,8 +3714,37 @@ function MessagesWorkspace({
|
|
|
3742
3714
|
onSubmit: handleCreateTemplate,
|
|
3743
3715
|
submitting: creatingTemplate,
|
|
3744
3716
|
result: createResult,
|
|
3745
|
-
labels: labels.createTemplate
|
|
3717
|
+
labels: labels.createTemplate,
|
|
3718
|
+
...createTemplatePreviewCompanyName ? { previewCompanyName: createTemplatePreviewCompanyName } : {},
|
|
3719
|
+
...createTemplateVariableExamples ? { variableExamples: createTemplateVariableExamples } : {}
|
|
3746
3720
|
}
|
|
3721
|
+
} : {},
|
|
3722
|
+
...templateRoles.length > 0 ? {
|
|
3723
|
+
extraRoleForms: /* @__PURE__ */ jsx24(Fragment6, { children: templateRoles.map((role) => {
|
|
3724
|
+
const settings = roleSettings[role.key] ?? EMPTY_TEMPLATE_SETTINGS;
|
|
3725
|
+
return /* @__PURE__ */ jsx24(
|
|
3726
|
+
WhatsAppTemplateSettingsForm,
|
|
3727
|
+
{
|
|
3728
|
+
templates,
|
|
3729
|
+
loadingTemplates,
|
|
3730
|
+
templatesError,
|
|
3731
|
+
selectedTemplateName: settings.templateName,
|
|
3732
|
+
onSelectTemplate: (name, template) => handleSelectRoleTemplate(role, name, template),
|
|
3733
|
+
variables: settings.variables,
|
|
3734
|
+
onVariablesChange: (variables) => setRoleSettings((previous) => ({
|
|
3735
|
+
...previous,
|
|
3736
|
+
[role.key]: { ...previous[role.key] ?? EMPTY_TEMPLATE_SETTINGS, variables }
|
|
3737
|
+
})),
|
|
3738
|
+
onSave: handleSaveRole(role),
|
|
3739
|
+
saving: Boolean(savingRole[role.key]),
|
|
3740
|
+
saveSuccess: Boolean(roleSaved[role.key]),
|
|
3741
|
+
labels: { ...labels.templateSettings, ...role.labels },
|
|
3742
|
+
...api.listTemplates ? { onRefreshTemplates: () => void reloadTemplates() } : {},
|
|
3743
|
+
...availableVariables ? { availableVariables } : {}
|
|
3744
|
+
},
|
|
3745
|
+
role.key
|
|
3746
|
+
);
|
|
3747
|
+
}) })
|
|
3747
3748
|
} : {}
|
|
3748
3749
|
}
|
|
3749
3750
|
)
|
|
@@ -4662,7 +4663,7 @@ function useConversationsInbox(params = {}) {
|
|
|
4662
4663
|
}
|
|
4663
4664
|
|
|
4664
4665
|
// src/workspace/ConversationsWorkspace.tsx
|
|
4665
|
-
import { Fragment as
|
|
4666
|
+
import { Fragment as Fragment7, jsx as jsx28, jsxs as jsxs26 } from "react/jsx-runtime";
|
|
4666
4667
|
function ConversationsWorkspace({
|
|
4667
4668
|
labels: labelsOverride,
|
|
4668
4669
|
filters,
|
|
@@ -4716,8 +4717,19 @@ function ConversationsWorkspace({
|
|
|
4716
4717
|
setOpenedFromLink(link);
|
|
4717
4718
|
}, [initialConversationId, initialWhatsappNumber, openedFromLink, inbox]);
|
|
4718
4719
|
const selected = inbox.selectedConversation;
|
|
4719
|
-
const
|
|
4720
|
+
const conversations = useConversations();
|
|
4721
|
+
const selectedId = selected?.id;
|
|
4722
|
+
const channel = selected?.channel ?? DEFAULT_CONVERSATION_CHANNEL;
|
|
4723
|
+
const handle = selected ? selected.contactId ?? selected.whatsappNumber : "";
|
|
4724
|
+
const transport = simulator?.transports?.[channel];
|
|
4725
|
+
const simulatorEnabled = Boolean(
|
|
4726
|
+
simulator && (simulator.enabled ?? true) && (simulator.render ?? transport)
|
|
4727
|
+
);
|
|
4720
4728
|
const showSimulator = simulatorEnabled && simulatorOpen && Boolean(selected);
|
|
4729
|
+
const simulatorClient = useMemo6(
|
|
4730
|
+
() => transport && selectedId ? transport({ conversationId: selectedId, channel, handle }) : void 0,
|
|
4731
|
+
[transport, selectedId, channel, handle]
|
|
4732
|
+
);
|
|
4721
4733
|
const paneUtilities = useMemo6(() => {
|
|
4722
4734
|
if (!selected) return void 0;
|
|
4723
4735
|
const fromProduct = extraUtilitiesFor?.(selected) ?? [];
|
|
@@ -4824,7 +4836,7 @@ function ConversationsWorkspace({
|
|
|
4824
4836
|
] }),
|
|
4825
4837
|
inbox.loadFailure ? /* @__PURE__ */ jsxs26("p", { role: "alert", className: "cv-workspace-failure", children: [
|
|
4826
4838
|
inbox.loadFailure,
|
|
4827
|
-
signInHref ? /* @__PURE__ */ jsxs26(
|
|
4839
|
+
signInHref ? /* @__PURE__ */ jsxs26(Fragment7, { children: [
|
|
4828
4840
|
" ",
|
|
4829
4841
|
/* @__PURE__ */ jsx28("a", { href: signInHref, className: "cv-workspace-failure__link", children: labels.signIn })
|
|
4830
4842
|
] }) : null
|
|
@@ -4880,7 +4892,26 @@ function ConversationsWorkspace({
|
|
|
4880
4892
|
showSimulator && selected ? (
|
|
4881
4893
|
// `min-height:0` junto do `min-width:0`: sem isso a linha do grid cresce com o conteúdo do
|
|
4882
4894
|
// painel, o scroll interno nunca ativa e quem rola passa a ser a página inteira.
|
|
4883
|
-
/* @__PURE__ */ jsx28("div", { className: "cv-workspace-simulator", children: simulator?.render({ conversationId: selected.id, close: () => setSimulatorOpen(false) })
|
|
4895
|
+
/* @__PURE__ */ jsx28("div", { className: "cv-workspace-simulator", children: simulator?.render ? simulator.render({ conversationId: selected.id, channel, close: () => setSimulatorOpen(false) }) : simulatorClient && conversations ? (
|
|
4896
|
+
// `key` pela conversa: trocar de contato sem remontar deixaria o transcript e o campo
|
|
4897
|
+
// de texto do contato anterior na tela.
|
|
4898
|
+
/* @__PURE__ */ jsx28(
|
|
4899
|
+
ConversationSimulatorPanel,
|
|
4900
|
+
{
|
|
4901
|
+
client: simulatorClient,
|
|
4902
|
+
sse: conversations.sse,
|
|
4903
|
+
conversationId: selected.id,
|
|
4904
|
+
channel,
|
|
4905
|
+
displayHandle: formatContactHandle({ handle, channel }),
|
|
4906
|
+
loadMessages: (conversationId) => conversations.api.fetchMessages(conversationId),
|
|
4907
|
+
onClose: () => setSimulatorOpen(false),
|
|
4908
|
+
...simulator?.labels ? { labels: simulator.labels } : {},
|
|
4909
|
+
...simulator?.uploadMedia ? { uploadMedia: simulator.uploadMedia } : {},
|
|
4910
|
+
...simulator?.pollIntervalMs ? { pollIntervalMs: simulator.pollIntervalMs } : {}
|
|
4911
|
+
},
|
|
4912
|
+
selected.id
|
|
4913
|
+
)
|
|
4914
|
+
) : null })
|
|
4884
4915
|
) : null
|
|
4885
4916
|
]
|
|
4886
4917
|
}
|
package/dist/preview/index.d.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export { A as AudioRecorderButton, a as AudioRecorderButtonLabels, b as AudioRecorderButtonProps, D as DEFAULT_AUDIO_RECORDER_BUTTON_LABELS } from '../
|
|
1
|
+
import { U as MessagePayload, u as ConversationSummary, m as ConversationEventSource, w as ConversationsApi, Q as ListConversationsParams, n as ConversationPage, a5 as SSEProvider, k as ConversationDocument, a6 as SendPreviewMediaParams, Z as PreviewUploadedMedia, $ as PreviewWebhookClient, a3 as ResolveMediaUrl } from '../ConversationSimulatorPanel--5fIzXWY.js';
|
|
2
|
+
export { A as AudioRecorderButton, a as AudioRecorderButtonLabels, b as AudioRecorderButtonProps, o as ConversationPreview, p as ConversationPreviewProps, q as ConversationSimulatorClient, r as ConversationSimulatorPanel, s as ConversationSimulatorPanelLabels, t as ConversationSimulatorPanelProps, B as CreatePreviewMediaUploaderParams, D as CreatePreviewWebhookClientParams, E as DEFAULT_AUDIO_RECORDER_BUTTON_LABELS, G as DEFAULT_CONVERSATION_SIMULATOR_PANEL_LABELS, I as DEFAULT_MEDIA_UPLOAD_PATH, W as PreviewInProductionError, X as PreviewMediaUploadRejectedError, Y as PreviewMediaUploadRequest, _ as PreviewUploadedMedia, a0 as PreviewWebhookRejectedError, a4 as SIMULATOR_FILE_MEDIA_KINDS, a7 as SendSimulatorMediaParams, a8 as SimulatorMediaKind, a9 as ToSimulatorClientParams, ac as acceptsMediaKind, ad as assertPreviewEnvironment, ah as createPreviewMediaPoster, ai as createPreviewMediaUploader, aj as createPreviewWebhookClient, al as isConversationSimulatorClient, am as mediaKindOf, an as mediaTypeOf, ao as signPreviewPayload, ap as simulatorPanelLabelsOf, aq as toConversationSimulatorClient } from '../ConversationSimulatorPanel--5fIzXWY.js';
|
|
3
|
+
import { InteractiveReplyOption } from '@adatechnology/meta-whatsapp-contracts/testing';
|
|
3
4
|
import * as react from 'react';
|
|
4
|
-
import { ReactNode } from 'react';
|
|
5
|
-
import { InteractiveReplyOption, InboundMediaType } from '@adatechnology/meta-whatsapp-contracts/testing';
|
|
6
5
|
export { PREVIEW_MEDIA_ID_PREFIX } from '@adatechnology/meta-whatsapp-contracts';
|
|
7
6
|
|
|
8
7
|
/**
|
|
@@ -114,210 +113,6 @@ declare const PREVIEW_CONVERSATIONS: readonly ConversationSummary[];
|
|
|
114
113
|
declare const PREVIEW_MESSAGES: Readonly<Record<string, readonly MessagePayload[]>>;
|
|
115
114
|
declare const PREVIEW_DOCUMENTS: Readonly<Record<string, readonly ConversationDocument[]>>;
|
|
116
115
|
|
|
117
|
-
/**
|
|
118
|
-
* Entrega ao simulador o `uploadMedia` que ele precisa para desenhar o microfone.
|
|
119
|
-
*
|
|
120
|
-
* O `ConversationPreview` esconde o gravador sem esta função, e com razão: microfone que grava sem
|
|
121
|
-
* ter onde guardar o arquivo faz o operador falar para o vazio. O que faltava era montar isto —
|
|
122
|
-
* lê o `File`, manda para a rota do host, devolve o `mediaId` prefixado que o webhook referencia.
|
|
123
|
-
*
|
|
124
|
-
* Fica no pacote porque a parte que erra é sempre a mesma em todo produto: converter o binário sem
|
|
125
|
-
* estourar a pilha e marcar o id com o prefixo que o backend reconhece. O que muda por produto é só
|
|
126
|
-
* a rota e o cliente HTTP — e é exatamente isso que entra por parâmetro.
|
|
127
|
-
*/
|
|
128
|
-
/**
|
|
129
|
-
* Do `contracts`, que este pacote já consome — não uma cópia.
|
|
130
|
-
*
|
|
131
|
-
* A convenção tem duas pontas (o front gera o id, o backend resolve) e a versão anterior disso vivia
|
|
132
|
-
* duplicada em dois pacotes de um produto, cada cópia com um comentário pedindo para não divergir.
|
|
133
|
-
* Contrato compartilhado é o que o `contracts` existe para guardar.
|
|
134
|
-
*/
|
|
135
|
-
|
|
136
|
-
type PreviewUploadedMedia$1 = {
|
|
137
|
-
readonly mediaId: string;
|
|
138
|
-
readonly mimeType?: string;
|
|
139
|
-
readonly filename?: string;
|
|
140
|
-
};
|
|
141
|
-
type PreviewMediaUploadRequest = {
|
|
142
|
-
readonly base64: string;
|
|
143
|
-
readonly mimeType: string;
|
|
144
|
-
readonly filename: string;
|
|
145
|
-
};
|
|
146
|
-
type CreatePreviewMediaUploaderParams = {
|
|
147
|
-
/**
|
|
148
|
-
* Envia o arquivo à rota do host e devolve o `uploadId` (sem prefixo) que o backend gerou.
|
|
149
|
-
*
|
|
150
|
-
* Recebe a função inteira, e não uma URL, porque autenticação varia: uma instalação assina com
|
|
151
|
-
* HMAC, outra manda token de admin, outra usa cookie de sessão. Pedir a URL obrigaria o pacote a
|
|
152
|
-
* escolher por elas.
|
|
153
|
-
*/
|
|
154
|
-
readonly upload: (request: PreviewMediaUploadRequest) => Promise<{
|
|
155
|
-
uploadId: string;
|
|
156
|
-
}>;
|
|
157
|
-
/** Nome usado quando o gravador entrega o áudio sem nome próprio. */
|
|
158
|
-
readonly fallbackFilename?: string;
|
|
159
|
-
readonly fallbackMimeType?: string;
|
|
160
|
-
};
|
|
161
|
-
declare function createPreviewMediaUploader(params: CreatePreviewMediaUploaderParams): (file: File) => Promise<PreviewUploadedMedia$1>;
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* Cliente que entrega mensagens do preview no webhook real, assinadas com HMAC — a mesma validação
|
|
165
|
-
* de staging e produção, sem rota alternativa e sem bypass. Do ponto de vista da API, este cliente
|
|
166
|
-
* é indistinguível da Meta; o que muda é apenas quem assina.
|
|
167
|
-
*
|
|
168
|
-
* Assina com WebCrypto porque `node:crypto` não existe no navegador. Os builders vêm dos contratos
|
|
169
|
-
* (isomórficos) justamente para que o mesmo payload seja montado nos dois runtimes.
|
|
170
|
-
*
|
|
171
|
-
* ⚠️ SOMENTE EXECUÇÃO LOCAL. Isto carrega o app secret no bundle, e bundle é público onde quer que
|
|
172
|
-
* seja servido — em qualquer ambiente com URL acessível (homologação inclusive) usar esta fábrica
|
|
173
|
-
* equivale a publicar o segredo, e quem o tiver forja webhooks válidos daquele app da Meta: injeta
|
|
174
|
-
* mensagem de qualquer número e dispara os fluxos. `assertPreviewEnvironment` barra produção, mas
|
|
175
|
-
* homologação passaria, então a barreira não basta.
|
|
176
|
-
*
|
|
177
|
-
* Para qualquer ambiente publicado use `createPreviewBridgeClient`: o navegador manda a intenção e
|
|
178
|
-
* o servidor assina com o segredo que ele já tem.
|
|
179
|
-
*/
|
|
180
|
-
|
|
181
|
-
type PreviewWebhookClient = {
|
|
182
|
-
sendText(text: string): Promise<void>;
|
|
183
|
-
sendButtonReply(reply: InteractiveReplyOption): Promise<void>;
|
|
184
|
-
sendListReply(reply: InteractiveReplyOption): Promise<void>;
|
|
185
|
-
sendAudio(mediaId: string): Promise<void>;
|
|
186
|
-
sendMedia(params: SendPreviewMediaParams): Promise<void>;
|
|
187
|
-
/**
|
|
188
|
-
* Guarda um arquivo gravado e devolve o `mediaId` já prefixado, pronto para `sendMedia`.
|
|
189
|
-
*
|
|
190
|
-
* Existe no cliente, e não como prop de quem monta a tela, porque isto é exatamente o que ele já
|
|
191
|
-
* sabe fazer: falar com ESTE host usando ESTE segredo. Enquanto era responsabilidade do produto,
|
|
192
|
-
* o resultado prático foi um produto com microfone no simulador e outro sem — não por decisão,
|
|
193
|
-
* por esquecimento. Cliente montado, microfone na tela.
|
|
194
|
-
*
|
|
195
|
-
* Opcional porque o cliente-ponte só consegue oferecer isto quando sabe a rota de mídia (ou quando
|
|
196
|
-
* o host injeta a função): sem destino, gravar áudio seria falar para o vazio, e aí a tela
|
|
197
|
-
* corretamente não desenha o gravador.
|
|
198
|
-
*/
|
|
199
|
-
uploadMedia?(file: File): Promise<PreviewUploadedMedia$1>;
|
|
200
|
-
};
|
|
201
|
-
type SendPreviewMediaParams = {
|
|
202
|
-
readonly mediaType: InboundMediaType;
|
|
203
|
-
/**
|
|
204
|
-
* Id que o host já usa para buscar o arquivo. Não é bytes: o webhook da Meta entrega mídia por
|
|
205
|
-
* referência, e o consumidor baixa depois — mandar base64 aqui simularia um payload que a Meta
|
|
206
|
-
* nunca produz, e o caminho testado deixaria de ser o de produção.
|
|
207
|
-
*/
|
|
208
|
-
readonly mediaId: string;
|
|
209
|
-
readonly mimeType?: string;
|
|
210
|
-
readonly filename?: string;
|
|
211
|
-
readonly caption?: string;
|
|
212
|
-
};
|
|
213
|
-
type CreatePreviewWebhookClientParams = {
|
|
214
|
-
readonly webhookUrl: string;
|
|
215
|
-
readonly appSecret: string;
|
|
216
|
-
readonly from: string;
|
|
217
|
-
readonly phoneNumberId?: string;
|
|
218
|
-
/**
|
|
219
|
-
* Rota que guarda o áudio gravado. Por padrão, `/v1/preview/media` na mesma origem do webhook.
|
|
220
|
-
*
|
|
221
|
-
* O padrão cobre o caso normal — as duas rotas são do mesmo servidor — e a prop existe para quem
|
|
222
|
-
* publica a API em outro host ou versiona o caminho.
|
|
223
|
-
*/
|
|
224
|
-
readonly mediaUploadUrl?: string;
|
|
225
|
-
readonly fetchImplementation?: typeof fetch;
|
|
226
|
-
};
|
|
227
|
-
/** Falha da rota de upload, separada da do webhook: os dois lados quebram por motivos diferentes. */
|
|
228
|
-
declare class PreviewMediaUploadRejectedError extends Error {
|
|
229
|
-
readonly status: number;
|
|
230
|
-
constructor(status: number);
|
|
231
|
-
}
|
|
232
|
-
declare class PreviewInProductionError extends Error {
|
|
233
|
-
constructor();
|
|
234
|
-
}
|
|
235
|
-
declare class PreviewWebhookRejectedError extends Error {
|
|
236
|
-
readonly status: number;
|
|
237
|
-
constructor(status: number);
|
|
238
|
-
}
|
|
239
|
-
/**
|
|
240
|
-
* Falha alto em vez de degradar em silêncio: um preview que "quase funciona" em produção é pior
|
|
241
|
-
* que um que se recusa a montar.
|
|
242
|
-
*/
|
|
243
|
-
declare function assertPreviewEnvironment(isProduction: boolean): void;
|
|
244
|
-
/**
|
|
245
|
-
* Assina um texto qualquer com o app secret, no mesmo formato do header da Meta.
|
|
246
|
-
*
|
|
247
|
-
* Exportada porque o preview precisa provar identidade em MAIS de um lugar: além de entregar a
|
|
248
|
-
* mensagem no webhook, ele lê o transcript de volta — e ler pela API de admin exigia uma sessão que
|
|
249
|
-
* a aba do simulador não tem. Assinar a leitura com o segredo que ele já carrega resolve sem token
|
|
250
|
-
* de admin e sem rota aberta.
|
|
251
|
-
*/
|
|
252
|
-
declare function signPreviewPayload(params: {
|
|
253
|
-
rawBody: string;
|
|
254
|
-
appSecret: string;
|
|
255
|
-
}): Promise<string>;
|
|
256
|
-
declare const DEFAULT_MEDIA_UPLOAD_PATH = "/v1/preview/media";
|
|
257
|
-
/**
|
|
258
|
-
* O POST de mídia, sem a parte de assinatura — para os dois clientes usarem o mesmo caminho.
|
|
259
|
-
*
|
|
260
|
-
* O cliente-ponte autentica por sessão e o de webhook por HMAC; o que não muda é a rota, o formato
|
|
261
|
-
* do corpo e a leitura do `uploadId`. Duas cópias disso é como o prefixo de mídia divergiu antes.
|
|
262
|
-
*/
|
|
263
|
-
declare function createPreviewMediaPoster(params: {
|
|
264
|
-
readonly url: string;
|
|
265
|
-
readonly headers?: (mimeType: string) => Promise<Readonly<Record<string, string>>>;
|
|
266
|
-
readonly fetchImplementation?: typeof fetch;
|
|
267
|
-
}): (file: File) => Promise<PreviewUploadedMedia$1>;
|
|
268
|
-
declare function createPreviewWebhookClient(params: CreatePreviewWebhookClientParams): PreviewWebhookClient;
|
|
269
|
-
|
|
270
|
-
type ConversationPreviewProps = {
|
|
271
|
-
client: PreviewWebhookClient;
|
|
272
|
-
sse: SSEProvider;
|
|
273
|
-
conversationId: string;
|
|
274
|
-
loadMessages: (conversationId: string) => Promise<MessagePayload[]>;
|
|
275
|
-
placeholder?: string;
|
|
276
|
-
/**
|
|
277
|
-
* Recarrega o transcript a cada N ms. Serve a host SEM stream: a resposta do bot é assíncrona, e
|
|
278
|
-
* sem SSE nem polling ela só apareceria no próximo envio — o sintoma é "às vezes ele não
|
|
279
|
-
* responde". Ausente, não faz polling (host com SSE não precisa).
|
|
280
|
-
*/
|
|
281
|
-
pollIntervalMs?: number;
|
|
282
|
-
/**
|
|
283
|
-
* Como transformar um arquivo do disco (ou o áudio gravado) na referência que o webhook carrega.
|
|
284
|
-
* O caminho da Meta entrega mídia por `id`, e quem sabe hospedar o arquivo é o host — a SDK não
|
|
285
|
-
* inventa um endpoint de upload. Ausente, o compositor não oferece anexo nem gravação: melhor um
|
|
286
|
-
* botão que não existe do que um que falha ao ser tocado.
|
|
287
|
-
*/
|
|
288
|
-
/** Destino alternativo do áudio gravado. Sem isto, usa o do próprio `client`. */
|
|
289
|
-
uploadMedia?: (file: File) => Promise<PreviewUploadedMedia>;
|
|
290
|
-
};
|
|
291
|
-
type PreviewUploadedMedia = {
|
|
292
|
-
readonly mediaId: string;
|
|
293
|
-
readonly mimeType?: string;
|
|
294
|
-
readonly filename?: string;
|
|
295
|
-
};
|
|
296
|
-
/** Deriva o tipo de mídia do WhatsApp a partir do MIME do arquivo escolhido. */
|
|
297
|
-
declare function mediaTypeOf(mimeType: string): SendPreviewMediaParams['mediaType'];
|
|
298
|
-
declare function ConversationPreview({ client, sse, conversationId, loadMessages, placeholder, pollIntervalMs, uploadMedia, }: ConversationPreviewProps): react.JSX.Element;
|
|
299
|
-
|
|
300
|
-
type ConversationSimulatorPanelLabels = {
|
|
301
|
-
readonly title: string;
|
|
302
|
-
/** Complementa o telefone no subtítulo, explicando para onde a mensagem realmente vai. */
|
|
303
|
-
readonly destinationHint: string;
|
|
304
|
-
readonly close: string;
|
|
305
|
-
readonly placeholder: string;
|
|
306
|
-
};
|
|
307
|
-
declare const DEFAULT_CONVERSATION_SIMULATOR_PANEL_LABELS: ConversationSimulatorPanelLabels;
|
|
308
|
-
type ConversationSimulatorPanelProps = Omit<ConversationPreviewProps, 'placeholder'> & {
|
|
309
|
-
readonly onClose: () => void;
|
|
310
|
-
/**
|
|
311
|
-
* Telefone já formatado para leitura. É o host que formata: máscara de telefone é convenção
|
|
312
|
-
* regional, e o pacote não tem como saber a do produto.
|
|
313
|
-
*/
|
|
314
|
-
readonly displayNumber?: string;
|
|
315
|
-
readonly labels?: Partial<ConversationSimulatorPanelLabels>;
|
|
316
|
-
/** Ações extras no cabeçalho — roteiro automático, limpar conversa, trocar de contato. */
|
|
317
|
-
readonly headerActions?: ReactNode;
|
|
318
|
-
};
|
|
319
|
-
declare function ConversationSimulatorPanel({ onClose, displayNumber, labels, headerActions, ...previewProps }: ConversationSimulatorPanelProps): react.JSX.Element;
|
|
320
|
-
|
|
321
116
|
/**
|
|
322
117
|
* Cliente do preview que NÃO carrega segredo: em vez de montar e assinar o payload da Meta no
|
|
323
118
|
* navegador, manda um comando semântico (`{ kind: 'text', text }`) para uma rota do próprio host,
|
|
@@ -387,7 +182,7 @@ type CreatePreviewBridgeClientParams = {
|
|
|
387
182
|
* Substitui o upload embutido. Necessário para host que só passa `sendCommand`: sem `endpointUrl`
|
|
388
183
|
* não há origem a derivar, e sem destino o gravador não é desenhado.
|
|
389
184
|
*/
|
|
390
|
-
readonly uploadMedia?: (file: File) => Promise<PreviewUploadedMedia
|
|
185
|
+
readonly uploadMedia?: (file: File) => Promise<PreviewUploadedMedia>;
|
|
391
186
|
};
|
|
392
187
|
declare function createPreviewBridgeClient(params: CreatePreviewBridgeClientParams): PreviewWebhookClient;
|
|
393
188
|
|
|
@@ -489,4 +284,4 @@ type MediaTypesPreviewProps = {
|
|
|
489
284
|
};
|
|
490
285
|
declare function MediaTypesPreview({ conversationId, className, }: MediaTypesPreviewProps): react.JSX.Element;
|
|
491
286
|
|
|
492
|
-
export { type AppendMessageParams,
|
|
287
|
+
export { type AppendMessageParams, type CreateMockConversationsApiParams, type CreateMockSSEProviderParams, type CreatePreviewBridgeClientParams, type CreatePreviewStoreParams, 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, PreviewBridgeRejectedError, type PreviewEmission, type PreviewInboundCommand, type PreviewScriptStep, type PreviewStore, type PreviewStoreListener, PreviewWebhookClient, type SendPreviewInboundCommand, SendPreviewMediaParams, type SetModeParams, type StartPreviewScriptParams, conversationChannel, createMockConversationsApi, createMockEventSource, createMockSSEProvider, createPreviewBridgeClient, createPreviewMediaResolver, createPreviewStore, previewFileBase64, previewFileUrl, resolvePreviewFileSample, startPreviewScript };
|