@adatechnology/conversations-ui 0.1.0-rc.8 → 0.1.0
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/ConversationSimulatorPanel--5fIzXWY.d.ts +804 -0
- package/dist/chunk-BJNRLLDO.js +2708 -0
- package/dist/chunk-DKPXKQGC.js +110 -0
- package/dist/{chunk-OGRRHQQW.js → chunk-WCBDXZ3X.js} +68 -4
- package/dist/flows/index.d.ts +422 -5
- package/dist/flows/index.js +2502 -678
- package/dist/index.d.ts +921 -17
- package/dist/index.js +3677 -678
- package/dist/preview/index.d.ts +62 -105
- package/dist/preview/index.js +162 -284
- package/dist/styles.css +893 -0
- package/package.json +9 -8
- package/src/AudioPlayer.tsx +8 -0
- package/src/AudioRecorderButton.test.tsx +30 -0
- package/src/AudioRecorderButton.tsx +248 -0
- package/src/AudioTranscription.test.tsx +115 -0
- package/src/AudioTranscription.tsx +252 -0
- package/src/Avatar.tsx +1 -1
- package/src/ConversationContextPanel.tsx +218 -44
- package/src/ConversationDocumentsPanel.tsx +11 -6
- package/src/ConversationHeader.test.tsx +66 -0
- package/src/ConversationHeader.tsx +147 -47
- package/src/ConversationListItem.tsx +8 -6
- package/src/ConversationLocalesProvider.tsx +28 -0
- package/src/ConversationRow.tsx +53 -7
- package/src/DarkModeToggle.test.tsx +76 -0
- package/src/DarkModeToggle.tsx +92 -0
- package/src/DocumentsLibrary.tsx +67 -7
- package/src/EmojiPicker.tsx +2 -1
- package/src/InteractiveMessage.tsx +3 -0
- package/src/Lightbox.tsx +1 -1
- package/src/MediaRenderer.tsx +88 -15
- package/src/MessageBubble.test.tsx +41 -0
- package/src/MessageBubble.tsx +47 -5
- package/src/MessageComposer.test.tsx +35 -0
- package/src/MessageComposer.tsx +122 -17
- package/src/MessageText.tsx +2 -1
- package/src/MessageTimestamp.tsx +2 -1
- package/src/RichMessageComposer.test.tsx +113 -0
- package/src/RichMessageComposer.tsx +551 -0
- package/src/SimpleEmojiPicker.tsx +5 -3
- package/src/StatusTicks.tsx +1 -1
- package/src/Toast.tsx +4 -0
- package/src/Tooltip.test.ts +42 -0
- package/src/Tooltip.tsx +167 -0
- package/src/Wallpaper.test.tsx +21 -0
- package/src/Wallpaper.tsx +67 -7
- package/src/WhatsAppMessageEditor.tsx +10 -7
- package/src/WindowExpiredNotice.tsx +12 -4
- package/src/{preview/audioRecorderFormat.test.ts → audioRecorderFormat.test.ts} +1 -1
- package/src/buildOutput.test.ts +79 -0
- package/src/composer.constant.ts +33 -0
- package/src/conversationTranscript.test.ts +57 -0
- package/src/conversationTranscript.ts +29 -4
- package/src/conversationWindow.ts +7 -5
- package/src/documentTypeLabel.test.ts +57 -0
- package/src/documents/DocumentsWorkspace.tsx +550 -0
- package/src/documents/index.ts +8 -0
- package/src/documents/labels.ts +92 -0
- package/src/flows/FlowConnectionEdge.tsx +104 -0
- package/src/flows/FlowGroupHeader.tsx +12 -2
- package/src/flows/FlowLegend.tsx +125 -0
- package/src/flows/FlowMapCanvas.tsx +15 -12
- package/src/flows/FlowMapNode.tsx +4 -1
- package/src/flows/FlowNodeCard.tsx +219 -34
- package/src/flows/FlowNodePanel.tsx +153 -39
- package/src/flows/FlowPalette.tsx +156 -70
- package/src/flows/FlowPortalNode.tsx +1 -1
- package/src/flows/FlowWhatsAppPreview.tsx +14 -3
- package/src/flows/FlowsWorkspace.tsx +1255 -0
- package/src/flows/flowCanvasModel.test.ts +456 -0
- package/src/flows/flowCanvasModel.ts +378 -0
- package/src/flows/flowEditorOps.test.ts +276 -0
- package/src/flows/flowEditorOps.ts +202 -0
- package/src/flows/flowGraph.ts +78 -53
- package/src/flows/flowMenuPlacement.test.ts +130 -0
- package/src/flows/flowMenuPlacement.ts +86 -0
- package/src/flows/index.ts +51 -2
- package/src/flows/labels.ts +180 -0
- package/src/flows/workspaceContract.test.ts +126 -0
- package/src/hooks/useContainerWidth.ts +35 -0
- package/src/hooks/useConversationRealtime.ts +10 -8
- package/src/hooks/useScrollToLatestMessage.ts +127 -0
- package/src/hooks/useUrlFilterState.ts +107 -0
- package/src/icon.constant.ts +12 -0
- package/src/index.ts +100 -0
- package/src/lib/composer-formatting.test.ts +78 -0
- package/src/lib/composer-formatting.ts +145 -0
- package/src/lib/whatsapp-formatting.test.tsx +37 -0
- package/src/lib/whatsapp-formatting.tsx +28 -3
- package/src/listing/index.tsx +202 -0
- package/src/pagination.constant.ts +10 -0
- package/src/preview/ConversationPreview.tsx +84 -45
- package/src/preview/ConversationSimulatorClient.ts +143 -0
- package/src/preview/ConversationSimulatorPanel.test.tsx +55 -0
- package/src/preview/ConversationSimulatorPanel.tsx +131 -0
- package/src/preview/createPreviewBridgeClient.test.ts +92 -0
- package/src/preview/createPreviewBridgeClient.ts +124 -0
- package/src/preview/createPreviewMediaUploader.ts +82 -0
- package/src/preview/createPreviewWebhookClient.test.ts +96 -0
- package/src/preview/createPreviewWebhookClient.ts +99 -3
- package/src/preview/index.ts +36 -2
- package/src/preview/previewMediaUploader.test.ts +61 -0
- package/src/providers/ConversationsProvider.tsx +8 -6
- package/src/providers/types.ts +59 -2
- package/src/quickReply.test.ts +58 -0
- package/src/replyLatency.test.ts +71 -0
- package/src/replyLatency.ts +57 -0
- package/src/settings/MessagesWorkspace.tsx +571 -0
- package/src/settings/TopicsForm.tsx +2 -0
- package/src/settings/TranscriptionSettingsForm.test.tsx +81 -0
- package/src/settings/TranscriptionSettingsForm.tsx +190 -0
- package/src/settings/WelcomeFarewellForm.tsx +1 -0
- package/src/settings/WhatsAppCreateTemplateForm.tsx +1 -0
- package/src/settings/WhatsAppTemplateSettingsForm.tsx +5 -2
- package/src/settings/WhatsAppTemplatesSettings.test.tsx +61 -0
- package/src/settings/WhatsAppTemplatesSettings.tsx +22 -2
- package/src/styles.css +858 -0
- package/src/theme.ts +13 -0
- package/src/types.ts +26 -0
- package/src/workspace/BulkTemplateModal.tsx +132 -0
- package/src/workspace/ConversationPane.tsx +432 -0
- package/src/workspace/ConversationsInboxList.tsx +194 -0
- package/src/workspace/ConversationsWorkspace.tsx +423 -0
- package/src/workspace/index.ts +17 -0
- package/src/workspace/labels.test.ts +17 -0
- package/src/workspace/labels.ts +85 -0
- package/src/workspace/useConversationsInbox.ts +332 -0
- package/dist/chunk-73MW5HNT.js +0 -1717
- package/dist/chunk-NV2RZ5KT.js +0 -56
- package/dist/types-B5C1DLu1.d.ts +0 -365
- package/src/preview/AudioRecorderButton.tsx +0 -117
|
@@ -0,0 +1,804 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
import { InteractiveReplyOption, InboundMediaType } from '@adatechnology/meta-whatsapp-contracts/testing';
|
|
4
|
+
|
|
5
|
+
interface ConversationsUIConfig {
|
|
6
|
+
apiBaseUrl: string;
|
|
7
|
+
theme?: ConversationsTheme;
|
|
8
|
+
features?: ConversationsFeatures;
|
|
9
|
+
}
|
|
10
|
+
interface ConversationsTheme {
|
|
11
|
+
primaryColor?: string;
|
|
12
|
+
backgroundColor?: string;
|
|
13
|
+
bubbleSent?: string;
|
|
14
|
+
bubbleReceived?: string;
|
|
15
|
+
textPrimary?: string;
|
|
16
|
+
textSecondary?: string;
|
|
17
|
+
}
|
|
18
|
+
interface ConversationsFeatures {
|
|
19
|
+
audio?: boolean;
|
|
20
|
+
documents?: boolean;
|
|
21
|
+
emoji?: boolean;
|
|
22
|
+
darkMode?: boolean;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Recorte do bloco `interactive` da Meta que a UI precisa para desenhar o menu. Fica solto (e não
|
|
26
|
+
* espelhando o contrato inteiro) porque o que chega do banco é o payload cru já enviado ao
|
|
27
|
+
* WhatsApp: qualquer campo que a UI não conheça é ignorado, nunca causa erro de render.
|
|
28
|
+
*/
|
|
29
|
+
interface InteractiveOption {
|
|
30
|
+
id: string;
|
|
31
|
+
title: string;
|
|
32
|
+
description?: string;
|
|
33
|
+
}
|
|
34
|
+
interface InteractiveSection {
|
|
35
|
+
title?: string;
|
|
36
|
+
rows?: InteractiveOption[];
|
|
37
|
+
}
|
|
38
|
+
interface InteractivePayload {
|
|
39
|
+
type?: 'button' | 'list' | string;
|
|
40
|
+
header?: {
|
|
41
|
+
text?: string;
|
|
42
|
+
};
|
|
43
|
+
body?: {
|
|
44
|
+
text?: string;
|
|
45
|
+
};
|
|
46
|
+
footer?: {
|
|
47
|
+
text?: string;
|
|
48
|
+
};
|
|
49
|
+
action?: {
|
|
50
|
+
/** Rótulo do botão que abre a lista — só existe em `type: 'list'`. */
|
|
51
|
+
button?: string;
|
|
52
|
+
sections?: InteractiveSection[];
|
|
53
|
+
buttons?: {
|
|
54
|
+
reply?: InteractiveOption;
|
|
55
|
+
}[];
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/** Como o cliente respondeu a um menu: por botão ou por item de lista. */
|
|
59
|
+
type InteractiveSelection = {
|
|
60
|
+
readonly kind: 'button' | 'list';
|
|
61
|
+
readonly option: InteractiveOption;
|
|
62
|
+
};
|
|
63
|
+
interface MessagePayload {
|
|
64
|
+
id: string;
|
|
65
|
+
type: 'text' | 'image' | 'video' | 'audio' | 'document' | 'sticker' | 'template' | 'interactive';
|
|
66
|
+
/** Payload cru da mensagem. Em `type: 'interactive'`, carrega o menu que o cliente vê. */
|
|
67
|
+
payload?: InteractivePayload | null;
|
|
68
|
+
content?: string;
|
|
69
|
+
caption?: string;
|
|
70
|
+
mediaUrl?: string;
|
|
71
|
+
base64?: string;
|
|
72
|
+
uploadId?: string;
|
|
73
|
+
mediaId?: string;
|
|
74
|
+
mimeType?: string;
|
|
75
|
+
filename?: string;
|
|
76
|
+
sizeBytes?: number;
|
|
77
|
+
direction: 'inbound' | 'outbound';
|
|
78
|
+
sender: 'bot' | 'customer' | 'agent';
|
|
79
|
+
timestamp: string;
|
|
80
|
+
status?: 'sent' | 'delivered' | 'read' | 'failed';
|
|
81
|
+
readAt?: string;
|
|
82
|
+
agentName?: string | null;
|
|
83
|
+
templateName?: string;
|
|
84
|
+
/**
|
|
85
|
+
* Veredito de moderação vindo do backend — a UI só exibe, nunca calcula. Dicionário no browser
|
|
86
|
+
* seria peso morto e daria veredito diferente por versão de cliente.
|
|
87
|
+
*
|
|
88
|
+
* `null`/ausente = não avaliado (moderação desligada, ou mensagem anterior ao recurso), que é
|
|
89
|
+
* diferente de avaliado e limpo.
|
|
90
|
+
*/
|
|
91
|
+
moderation?: {
|
|
92
|
+
isOffensive: boolean;
|
|
93
|
+
terms: string[];
|
|
94
|
+
} | null;
|
|
95
|
+
/**
|
|
96
|
+
* Transcrição do áudio, vinda do backend — a UI só exibe, nunca transcreve. Rodar STT no browser
|
|
97
|
+
* exigiria baixar modelo por aba e daria resultado diferente por versão de cliente.
|
|
98
|
+
*
|
|
99
|
+
* `null`/ausente = não avaliado, que é diferente de `'done'` com texto vazio (áudio em silêncio,
|
|
100
|
+
* já processado). É essa distinção que decide se o balão oferece "transcrever" ou "sem fala
|
|
101
|
+
* detectada".
|
|
102
|
+
*/
|
|
103
|
+
transcription?: MessageTranscription | null;
|
|
104
|
+
isFirstInGroup?: boolean;
|
|
105
|
+
isLastInGroup?: boolean;
|
|
106
|
+
}
|
|
107
|
+
type TranscriptionStatus = 'pending' | 'done' | 'failed' | 'unsupported';
|
|
108
|
+
/**
|
|
109
|
+
* Quando transcrever, escolhido nas configurações da empresa. Espelha o
|
|
110
|
+
* `TranscriptionMode` de `@adatechnology/meta-whatsapp-contracts`; declarado aqui para o pacote de
|
|
111
|
+
* UI não obrigar quem só desenha telas a instalar os contratos do backend.
|
|
112
|
+
*/
|
|
113
|
+
type TranscriptionMode = 'auto' | 'onDemand';
|
|
114
|
+
interface MessageTranscription {
|
|
115
|
+
status: TranscriptionStatus;
|
|
116
|
+
text?: string | null;
|
|
117
|
+
/** ISO 639-1 ou nome do idioma, conforme o engine. Exibido como dica, não interpretado. */
|
|
118
|
+
language?: string | null;
|
|
119
|
+
engine?: string | null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
type ResolveMediaUrl = (message: MessagePayload) => Promise<string | null>;
|
|
123
|
+
interface MediaRendererProps {
|
|
124
|
+
message: MessagePayload;
|
|
125
|
+
onLightbox: (src: string) => void;
|
|
126
|
+
onResolveUrl?: ResolveMediaUrl;
|
|
127
|
+
/**
|
|
128
|
+
* Pede ao backend a transcrição do áudio desta mensagem. Ausente, o bloco de transcrição só exibe
|
|
129
|
+
* o que já veio pronto — sem oferecer um botão que o host não sabe atender.
|
|
130
|
+
*
|
|
131
|
+
* O que devolver é exibido na hora, sem esperar refetch da lista.
|
|
132
|
+
*/
|
|
133
|
+
onTranscribeAudio?: () => Promise<MessageTranscription | void>;
|
|
134
|
+
/** Aplicado no wrapper de cada tipo de mídia — imagem, vídeo, áudio e documento. */
|
|
135
|
+
className?: string;
|
|
136
|
+
}
|
|
137
|
+
declare function MediaRenderer({ message, onLightbox, onResolveUrl, onTranscribeAudio, className, }: MediaRendererProps): react.JSX.Element | null;
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Gravação de áudio no simulador, pelo microfone do próprio navegador.
|
|
141
|
+
*
|
|
142
|
+
* Existe porque áudio é o formato que mais chega de cliente real e o que mais quebra fluxo: sem
|
|
143
|
+
* poder gravar aqui, testar o caminho de transcrição exigia mandar mensagem do celular de alguém.
|
|
144
|
+
*
|
|
145
|
+
* O arquivo gravado sai daqui como `File` e segue exatamente o mesmo caminho de um anexo — quem
|
|
146
|
+
* hospeda e devolve o `mediaId` é o host, via `uploadMedia`.
|
|
147
|
+
*/
|
|
148
|
+
interface AudioRecorderButtonLabels {
|
|
149
|
+
start: string;
|
|
150
|
+
stop: string;
|
|
151
|
+
unsupported: string;
|
|
152
|
+
denied: string;
|
|
153
|
+
review: string;
|
|
154
|
+
send: string;
|
|
155
|
+
discard: string;
|
|
156
|
+
empty: string;
|
|
157
|
+
}
|
|
158
|
+
declare const DEFAULT_AUDIO_RECORDER_BUTTON_LABELS: AudioRecorderButtonLabels;
|
|
159
|
+
interface AudioRecorderButtonProps {
|
|
160
|
+
onRecorded: (file: File) => void | Promise<void>;
|
|
161
|
+
onFailure?: (message: string) => void;
|
|
162
|
+
/**
|
|
163
|
+
* Avisa quando a gravação começa e termina. O botão é um interruptor — o segundo toque é que
|
|
164
|
+
* envia — e sem um aviso fora dele o operador grava, não vê nada acontecer e desiste achando
|
|
165
|
+
* que o microfone está quebrado.
|
|
166
|
+
*/
|
|
167
|
+
onRecordingChange?: (isRecording: boolean) => void;
|
|
168
|
+
/**
|
|
169
|
+
* Abre uma etapa de revisão quando a gravação para: o áudio toca ali mesmo e só sai depois de
|
|
170
|
+
* confirmado. Ligado por padrão — voz é o único anexo que quem envia não viu antes de mandar, e
|
|
171
|
+
* sem ouvir não há como saber se o microfone captou alguma coisa. Desligar volta ao envio direto.
|
|
172
|
+
*/
|
|
173
|
+
reviewBeforeSend?: boolean;
|
|
174
|
+
/**
|
|
175
|
+
* Teto de duração da gravação, em milissegundos. Passado o tempo, o gravador para e envia o que
|
|
176
|
+
* tem. Produto com limite próprio sobrescreve.
|
|
177
|
+
*/
|
|
178
|
+
maxDurationMilliseconds?: number;
|
|
179
|
+
labels?: Partial<AudioRecorderButtonLabels>;
|
|
180
|
+
disabled?: boolean;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Cinco minutos: com o codec de voz do WhatsApp isso dá menos de 3MB, folgado dentro do teto de
|
|
184
|
+
* 16MB que a Meta impõe a áudio, e é mais do que qualquer recado de cliente. O corte automático
|
|
185
|
+
* existe porque gravação esquecida aberta só se descobre no envio, com o arquivo inteiro perdido.
|
|
186
|
+
*/
|
|
187
|
+
declare const DEFAULT_MAX_RECORDING_MILLISECONDS: number;
|
|
188
|
+
declare function AudioRecorderButton({ onRecorded, onFailure, onRecordingChange, reviewBeforeSend, maxDurationMilliseconds, labels, disabled, }: AudioRecorderButtonProps): react.JSX.Element;
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Canal de origem da conversa e o que cada um permite.
|
|
192
|
+
*
|
|
193
|
+
* Existe porque as regras que a inbox precisa respeitar não são do WhatsApp, são **de cada canal**:
|
|
194
|
+
* a janela de sessão, o jeito de reabrir a conversa e o formato do identificador do contato mudam
|
|
195
|
+
* entre WhatsApp, Messenger, Instagram e chat de site. Tratar a regra do WhatsApp como universal
|
|
196
|
+
* faria a UI bloquear o composer num chat de site, onde janela nenhuma existe.
|
|
197
|
+
*
|
|
198
|
+
* `whatsapp` é o padrão em todo lugar: instalações que ainda não informam canal continuam
|
|
199
|
+
* funcionando exatamente como antes.
|
|
200
|
+
*/
|
|
201
|
+
declare const CONVERSATION_CHANNEL: {
|
|
202
|
+
readonly WHATSAPP: "whatsapp";
|
|
203
|
+
readonly MESSENGER: "messenger";
|
|
204
|
+
readonly INSTAGRAM: "instagram";
|
|
205
|
+
readonly WEBCHAT: "webchat";
|
|
206
|
+
};
|
|
207
|
+
type ConversationChannel = (typeof CONVERSATION_CHANNEL)[keyof typeof CONVERSATION_CHANNEL];
|
|
208
|
+
declare const DEFAULT_CONVERSATION_CHANNEL: ConversationChannel;
|
|
209
|
+
/** Como o canal reabre uma conversa fora da janela de sessão. */
|
|
210
|
+
declare const REOPEN_MECHANISM: {
|
|
211
|
+
readonly TEMPLATE: "template";
|
|
212
|
+
readonly TAG: "tag";
|
|
213
|
+
readonly NONE: "none";
|
|
214
|
+
};
|
|
215
|
+
type ReopenMechanism = (typeof REOPEN_MECHANISM)[keyof typeof REOPEN_MECHANISM];
|
|
216
|
+
/** Natureza do identificador do contato — decide como exibi-lo. */
|
|
217
|
+
declare const HANDLE_KIND: {
|
|
218
|
+
readonly PHONE: "phone";
|
|
219
|
+
readonly USERNAME: "username";
|
|
220
|
+
readonly SESSION: "session";
|
|
221
|
+
};
|
|
222
|
+
type HandleKind = (typeof HANDLE_KIND)[keyof typeof HANDLE_KIND];
|
|
223
|
+
type ChannelCapabilities = {
|
|
224
|
+
readonly label: string;
|
|
225
|
+
readonly icon: string;
|
|
226
|
+
readonly hasSessionWindow: boolean;
|
|
227
|
+
readonly windowHours: number;
|
|
228
|
+
readonly reopenMechanism: ReopenMechanism;
|
|
229
|
+
readonly handleKind: HandleKind;
|
|
230
|
+
};
|
|
231
|
+
declare const CHANNEL_CAPABILITIES: Readonly<Record<ConversationChannel, ChannelCapabilities>>;
|
|
232
|
+
declare function capabilitiesOf(channel: ConversationChannel | undefined): ChannelCapabilities;
|
|
233
|
+
declare const CHANNEL_FILTER_ALL = "all";
|
|
234
|
+
type ChannelFilter = ConversationChannel | typeof CHANNEL_FILTER_ALL;
|
|
235
|
+
type ChannelFilterOption = {
|
|
236
|
+
readonly value: ChannelFilter;
|
|
237
|
+
readonly label: string;
|
|
238
|
+
};
|
|
239
|
+
/**
|
|
240
|
+
* Opções derivadas do que existe na lista, não do catálogo inteiro: oferecer Instagram numa conta
|
|
241
|
+
* que só tem WhatsApp promete um recorte que nunca traz resultado.
|
|
242
|
+
*
|
|
243
|
+
* Devolve vazio com menos de dois canais — um filtro de opção única não filtra nada, e a barra só
|
|
244
|
+
* ocuparia espaço. O host usa isso para esconder a seção.
|
|
245
|
+
*/
|
|
246
|
+
declare function channelFiltersFor(conversations: readonly {
|
|
247
|
+
readonly channel?: ConversationChannel | undefined;
|
|
248
|
+
}[]): ChannelFilterOption[];
|
|
249
|
+
type FormatContactHandleParams = {
|
|
250
|
+
readonly handle: string;
|
|
251
|
+
readonly channel?: ConversationChannel | undefined;
|
|
252
|
+
};
|
|
253
|
+
/**
|
|
254
|
+
* Exibição do identificador conforme a natureza dele. Formatar tudo como telefone — o que a UI
|
|
255
|
+
* fazia — transforma um `@perfil` do Instagram em dígitos sem sentido.
|
|
256
|
+
*/
|
|
257
|
+
declare function formatContactHandle(params: FormatContactHandleParams): string;
|
|
258
|
+
/** Bandeira só faz sentido quando o identificador é telefone. */
|
|
259
|
+
declare function contactFlag(params: FormatContactHandleParams): string;
|
|
260
|
+
|
|
261
|
+
interface ListConversationsParams {
|
|
262
|
+
page?: number;
|
|
263
|
+
limit?: number;
|
|
264
|
+
waitingHuman?: boolean;
|
|
265
|
+
search?: string;
|
|
266
|
+
/**
|
|
267
|
+
* Recortes que só o produto conhece (tipo de financiamento, carteira, campanha) repassados
|
|
268
|
+
* crus ao backend dele. É o que evita o vocabulário de uma vertical virar campo fixo aqui:
|
|
269
|
+
* o pacote transporta o filtro sem saber o que ele significa.
|
|
270
|
+
*/
|
|
271
|
+
filters?: Record<string, string | undefined>;
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Página com o total, para a UI conseguir desenhar controles de paginação.
|
|
275
|
+
*
|
|
276
|
+
* `fetchConversations` devolve isto **ou** o array puro de antes: implementações existentes
|
|
277
|
+
* continuam válidas sem mudar uma linha, e quem precisa paginar passa a ter o total. Sem a união
|
|
278
|
+
* seria impossível saber se um retorno curto é a última página ou uma página cheia por acaso.
|
|
279
|
+
*/
|
|
280
|
+
interface ConversationPage {
|
|
281
|
+
conversations: ConversationSummary[];
|
|
282
|
+
total: number;
|
|
283
|
+
}
|
|
284
|
+
interface ListDocumentsParams {
|
|
285
|
+
search?: string;
|
|
286
|
+
page?: number;
|
|
287
|
+
/** Tamanho da página. Sem ele, `page` sozinho não define fatia nenhuma. */
|
|
288
|
+
limit?: number;
|
|
289
|
+
/**
|
|
290
|
+
* Origem do arquivo (`customer`, `agent`, `bot`…). O vocabulário é do host.
|
|
291
|
+
*
|
|
292
|
+
* Seleção múltipla viaja como lista separada por vírgula em vez de virar `string[]`: mudar o
|
|
293
|
+
* tipo quebraria em compile-time toda implementação de host que já repassa este campo adiante,
|
|
294
|
+
* e o ganho seria nenhum — quem recebe faz `split(',')`.
|
|
295
|
+
*/
|
|
296
|
+
source?: string;
|
|
297
|
+
/** Categoria do arquivo (`document`, `image`, `audio`, `video`…), mesma convenção de lista. */
|
|
298
|
+
fileCategory?: string;
|
|
299
|
+
/** Recorte por data de recebimento, em `YYYY-MM-DD`. */
|
|
300
|
+
startDate?: string;
|
|
301
|
+
endDate?: string;
|
|
302
|
+
sortDirection?: 'asc' | 'desc';
|
|
303
|
+
/** Coluna ordenada. Ausente, o host ordena pela data — é o padrão de toda listagem de arquivo. */
|
|
304
|
+
sortField?: string;
|
|
305
|
+
/**
|
|
306
|
+
* Filtros que só existem no produto (`clientId`, `unidade`…). O pacote não os interpreta: passa
|
|
307
|
+
* adiante o que o host injetou pelo slot de filtros. É a porta que evita um fork da tela por
|
|
308
|
+
* causa de um `<select>`.
|
|
309
|
+
*/
|
|
310
|
+
extra?: Readonly<Record<string, string | number>>;
|
|
311
|
+
}
|
|
312
|
+
/** Arquivo na biblioteca da empresa: o mesmo da conversa, mais de qual conversa veio. */
|
|
313
|
+
interface CompanyDocument extends ConversationDocument {
|
|
314
|
+
conversationId: string;
|
|
315
|
+
/**
|
|
316
|
+
* Nome de quem enviou, quando o host o conhece. Opcional porque a biblioteca sempre tem o
|
|
317
|
+
* telefone e nem todo produto tem cadastro por trás dele — ausente, a coluna cai para o número.
|
|
318
|
+
*/
|
|
319
|
+
contactName?: string | null;
|
|
320
|
+
}
|
|
321
|
+
interface CompanyDocumentPage {
|
|
322
|
+
documents: CompanyDocument[];
|
|
323
|
+
total: number;
|
|
324
|
+
}
|
|
325
|
+
interface ConversationDocumentPage {
|
|
326
|
+
documents: ConversationDocument[];
|
|
327
|
+
total: number;
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Template disponível para envio a partir da inbox. Distinto do `WhatsAppTemplateSummary` de
|
|
331
|
+
* `settings/`, e de propósito: aquele serve ao formulário que **edita** template e carrega o que
|
|
332
|
+
* a edição precisa (`shortId`, `variableCount`); este serve a quem só vai **escolher um para
|
|
333
|
+
* enviar**, e pedir os campos de edição obrigaria todo host a produzi-los sem uso.
|
|
334
|
+
*/
|
|
335
|
+
interface ConversationTemplate {
|
|
336
|
+
name: string;
|
|
337
|
+
language: string;
|
|
338
|
+
status: string;
|
|
339
|
+
category?: string;
|
|
340
|
+
bodyText?: string | null;
|
|
341
|
+
}
|
|
342
|
+
interface ConversationsApi {
|
|
343
|
+
fetchMessages(conversationId: string, params?: {
|
|
344
|
+
limit?: number;
|
|
345
|
+
before?: string;
|
|
346
|
+
}): Promise<MessagePayload[]>;
|
|
347
|
+
fetchConversations(params?: ListConversationsParams): Promise<ConversationSummary[] | ConversationPage>;
|
|
348
|
+
sendMessage(conversationId: string, text: string): Promise<MessagePayload>;
|
|
349
|
+
sendMedia(conversationId: string, data: {
|
|
350
|
+
base64: string;
|
|
351
|
+
mimeType: string;
|
|
352
|
+
filename: string;
|
|
353
|
+
caption?: string;
|
|
354
|
+
}): Promise<MessagePayload>;
|
|
355
|
+
/**
|
|
356
|
+
* `templateName` é opcional porque reabrir a janela é a operação, e escolher *qual* template a
|
|
357
|
+
* usa nem sempre é decisão da UI: backends que guardam um template padrão configurado só
|
|
358
|
+
* precisam do "reabra". Exigir o nome obrigaria toda inbox a listar templates antes de poder
|
|
359
|
+
* mandar o primeiro — e a listagem é `listTemplates?`, opcional.
|
|
360
|
+
*/
|
|
361
|
+
sendTemplate(conversationId: string, data: {
|
|
362
|
+
templateName?: string;
|
|
363
|
+
languageCode?: string;
|
|
364
|
+
bodyParams?: string[];
|
|
365
|
+
}): Promise<void>;
|
|
366
|
+
markRead(conversationId: string): Promise<void>;
|
|
367
|
+
getContext(conversationId: string): Promise<Record<string, unknown>>;
|
|
368
|
+
getDocuments(conversationId: string, params?: ListDocumentsParams): Promise<ConversationDocument[] | ConversationDocumentPage>;
|
|
369
|
+
/**
|
|
370
|
+
* `disposition` decide entre abrir no navegador e baixar. É o backend que assina a URL e grava
|
|
371
|
+
* o `Content-Disposition` nela, então a escolha precisa viajar na chamada — depois de assinada
|
|
372
|
+
* não há como o cliente mudá-la. Ausente = o padrão do host.
|
|
373
|
+
*/
|
|
374
|
+
getDocumentUrl(uploadId: string, disposition?: 'inline' | 'attachment'): Promise<string>;
|
|
375
|
+
/**
|
|
376
|
+
* Baixa vários arquivos num zip único.
|
|
377
|
+
*
|
|
378
|
+
* **Opcional por capacidade:** montar zip exige o host LER os bytes do storage, o que nem toda
|
|
379
|
+
* instalação faz — as que só assinam URL não conseguem. Ausente, o painel esconde a seleção em
|
|
380
|
+
* lote em vez de oferecer um botão que falha.
|
|
381
|
+
*/
|
|
382
|
+
downloadDocumentsArchive?(conversationId: string, uploadIds: readonly string[]): Promise<Blob>;
|
|
383
|
+
/**
|
|
384
|
+
* Biblioteca de TODAS as conversas, para uma tela de Documentos fora do atendimento.
|
|
385
|
+
*
|
|
386
|
+
* Opcional por capacidade: host que só expõe anexo dentro da conversa não implementa, e o
|
|
387
|
+
* componente de biblioteca simplesmente não é usável — melhor que uma tela que sempre erra.
|
|
388
|
+
*/
|
|
389
|
+
getAllDocuments?(params?: ListDocumentsParams): Promise<CompanyDocumentPage>;
|
|
390
|
+
/**
|
|
391
|
+
* Remove um arquivo da biblioteca. **Opcional por capacidade:** apagar anexo trocado com o
|
|
392
|
+
* cliente é decisão de retenção do produto — instalação que precisa guardar tudo por obrigação
|
|
393
|
+
* legal não implementa, e a tela simplesmente não desenha a lixeira.
|
|
394
|
+
*/
|
|
395
|
+
deleteDocument?(uploadId: string): Promise<void>;
|
|
396
|
+
/**
|
|
397
|
+
* Zip de arquivos avulsos da biblioteca, sem conversa de origem única — irmão do
|
|
398
|
+
* `downloadDocumentsArchive`, que é por conversa. Ausente, a seleção em lote não oferece o botão.
|
|
399
|
+
*/
|
|
400
|
+
downloadDocumentsArchiveByIds?(uploadIds: readonly string[]): Promise<Blob>;
|
|
401
|
+
/**
|
|
402
|
+
* Envia um arquivo avulso direto pra biblioteca, fora do fluxo de uma conversa. **Opcional por
|
|
403
|
+
* capacidade:** cada host tem seu próprio contrato de upload (base64, multipart, presigned URL) —
|
|
404
|
+
* o pacote não escolhe um formato de payload, só entrega o `File` do input e deixa o host montar
|
|
405
|
+
* a chamada do jeito que seu backend espera. Ausente, a tela de biblioteca não desenha o botão de
|
|
406
|
+
* enviar, em vez de oferecer uma ação que sempre falha.
|
|
407
|
+
*
|
|
408
|
+
* `extra` é o mesmo vocabulário livre do produto que já viaja em `renderFilters` — cliente,
|
|
409
|
+
* unidade, campanha — pra associar o arquivo enviado ao contexto que a tela estava filtrando.
|
|
410
|
+
*/
|
|
411
|
+
uploadDocument?(file: File, extra?: Readonly<Record<string, string | number>>): Promise<ConversationDocument>;
|
|
412
|
+
getMediaProxyUrl(mediaId: string): Promise<{
|
|
413
|
+
mimeType: string;
|
|
414
|
+
data: string;
|
|
415
|
+
}>;
|
|
416
|
+
/**
|
|
417
|
+
* Operações de atendimento humano. **Opcionais por capacidade, não por descuido:** nem toda
|
|
418
|
+
* inbox tem fila humana — um canal só-bot, ou um chat de site sem operador, não sabe o que é
|
|
419
|
+
* assumir conversa. Quem não implementa não ganha o botão, em vez de ganhar um botão que
|
|
420
|
+
* estoura no clique. Os hooks devolvem `undefined` para a ação ausente, e é isso que a UI
|
|
421
|
+
* consulta para decidir se desenha a afordância.
|
|
422
|
+
*/
|
|
423
|
+
takeover?(conversationId: string): Promise<void>;
|
|
424
|
+
release?(conversationId: string): Promise<void>;
|
|
425
|
+
/** Encerra o atendimento. Despedida, se houver, é decisão do host — o pacote não a inventa. */
|
|
426
|
+
finalize?(conversationId: string): Promise<void>;
|
|
427
|
+
markAllRead?(): Promise<void>;
|
|
428
|
+
listTemplates?(): Promise<ConversationTemplate[]>;
|
|
429
|
+
/**
|
|
430
|
+
* Transcrição completa gerada pelo servidor. Existe ao lado de `buildTranscriptText`, que monta
|
|
431
|
+
* a partir das mensagens já em memória: a tela costuma ter só a última página carregada, e
|
|
432
|
+
* exportar dali entregaria um recorte parcial com cara de histórico inteiro. Opcional porque
|
|
433
|
+
* nem todo backend expõe a rota — quem não tem continua usando o builder local.
|
|
434
|
+
*/
|
|
435
|
+
exportTranscript?(conversationId: string): Promise<{
|
|
436
|
+
transcript: string;
|
|
437
|
+
filename: string;
|
|
438
|
+
}>;
|
|
439
|
+
/**
|
|
440
|
+
* Transcreve o áudio de uma mensagem e devolve o resultado.
|
|
441
|
+
*
|
|
442
|
+
* **Opcional por capacidade.** Um host em modo automático transcreve na ingestão e não expõe rota
|
|
443
|
+
* nenhuma; um host sem engine configurado não transcreve de jeito algum. Nos dois casos o balão
|
|
444
|
+
* simplesmente não desenha o botão, em vez de oferecer uma ação que estoura no clique.
|
|
445
|
+
*
|
|
446
|
+
* `messageId` e não `conversationId`: transcrição é por áudio, e uma conversa tem vários.
|
|
447
|
+
*/
|
|
448
|
+
transcribeAudio?(messageId: string): Promise<MessageTranscription>;
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Superfície mínima de stream que o pacote consome — exatamente o que `useConversationRealtime`
|
|
452
|
+
* usa: assinar 'message', desassinar e fechar. Deliberadamente estrutural em vez de
|
|
453
|
+
* `EventSource`: sem servidor HTTP não existe `EventSource`, e é isso que impediria alimentar a
|
|
454
|
+
* inbox com dados mockados em desenvolvimento. Um `EventSource` nativo satisfaz este tipo, então
|
|
455
|
+
* quem já implementa `SSEProvider` continua válido sem mudança.
|
|
456
|
+
*/
|
|
457
|
+
interface ConversationEventSource {
|
|
458
|
+
addEventListener(type: string, listener: (event: MessageEvent) => void): void;
|
|
459
|
+
removeEventListener(type: string, listener: (event: MessageEvent) => void): void;
|
|
460
|
+
close(): void;
|
|
461
|
+
}
|
|
462
|
+
interface SSEProvider {
|
|
463
|
+
connectConversationStream(conversationId: string): ConversationEventSource;
|
|
464
|
+
connectGlobalStream(): ConversationEventSource;
|
|
465
|
+
}
|
|
466
|
+
interface ConversationSummary {
|
|
467
|
+
id: string;
|
|
468
|
+
/**
|
|
469
|
+
* @deprecated Use `contactId` com `channel`. Mantido obrigatório para não quebrar quem já
|
|
470
|
+
* consome; some quando o segundo canal entrar em produção.
|
|
471
|
+
*/
|
|
472
|
+
whatsappNumber: string;
|
|
473
|
+
/** Identificador neutro do contato. Ausente = usa `whatsappNumber`. */
|
|
474
|
+
contactId?: string;
|
|
475
|
+
/** Ausente = `whatsapp`, o comportamento de antes desta mudança. */
|
|
476
|
+
channel?: ConversationChannel;
|
|
477
|
+
clientName?: string;
|
|
478
|
+
lastContent?: string;
|
|
479
|
+
lastDirection?: 'inbound' | 'outbound';
|
|
480
|
+
lastAt: string;
|
|
481
|
+
lastInboundAt: string | null;
|
|
482
|
+
mode: 'bot' | 'human';
|
|
483
|
+
assignedUserId: string | null;
|
|
484
|
+
waitingHuman: boolean;
|
|
485
|
+
unread: number;
|
|
486
|
+
currentState: string;
|
|
487
|
+
/**
|
|
488
|
+
* Atributos que só o produto conhece e desenha (tipo de financiamento, carteira, campanha). É a
|
|
489
|
+
* contraparte de leitura do `filters` de `ListConversationsParams`: o pacote transporta e nunca
|
|
490
|
+
* interpreta. Sem isto, exibir um selo próprio na linha exigiria o host manter uma segunda
|
|
491
|
+
* consulta paralela à mesma listagem — a implementação duplicada que o pacote existe para evitar.
|
|
492
|
+
*/
|
|
493
|
+
attributes?: Record<string, string | undefined>;
|
|
494
|
+
}
|
|
495
|
+
interface ConversationDocument {
|
|
496
|
+
id: string;
|
|
497
|
+
filename: string;
|
|
498
|
+
mimeType: string;
|
|
499
|
+
sizeBytes: number;
|
|
500
|
+
source: string;
|
|
501
|
+
linkedAt: string;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Entrega ao simulador o `uploadMedia` que ele precisa para desenhar o microfone.
|
|
506
|
+
*
|
|
507
|
+
* O `ConversationPreview` esconde o gravador sem esta função, e com razão: microfone que grava sem
|
|
508
|
+
* ter onde guardar o arquivo faz o operador falar para o vazio. O que faltava era montar isto —
|
|
509
|
+
* lê o `File`, manda para a rota do host, devolve o `mediaId` prefixado que o webhook referencia.
|
|
510
|
+
*
|
|
511
|
+
* Fica no pacote porque a parte que erra é sempre a mesma em todo produto: converter o binário sem
|
|
512
|
+
* estourar a pilha e marcar o id com o prefixo que o backend reconhece. O que muda por produto é só
|
|
513
|
+
* a rota e o cliente HTTP — e é exatamente isso que entra por parâmetro.
|
|
514
|
+
*/
|
|
515
|
+
/**
|
|
516
|
+
* Do `contracts`, que este pacote já consome — não uma cópia.
|
|
517
|
+
*
|
|
518
|
+
* A convenção tem duas pontas (o front gera o id, o backend resolve) e a versão anterior disso vivia
|
|
519
|
+
* duplicada em dois pacotes de um produto, cada cópia com um comentário pedindo para não divergir.
|
|
520
|
+
* Contrato compartilhado é o que o `contracts` existe para guardar.
|
|
521
|
+
*/
|
|
522
|
+
|
|
523
|
+
type PreviewUploadedMedia$1 = {
|
|
524
|
+
readonly mediaId: string;
|
|
525
|
+
readonly mimeType?: string;
|
|
526
|
+
readonly filename?: string;
|
|
527
|
+
};
|
|
528
|
+
type PreviewMediaUploadRequest = {
|
|
529
|
+
readonly base64: string;
|
|
530
|
+
readonly mimeType: string;
|
|
531
|
+
readonly filename: string;
|
|
532
|
+
};
|
|
533
|
+
type CreatePreviewMediaUploaderParams = {
|
|
534
|
+
/**
|
|
535
|
+
* Envia o arquivo à rota do host e devolve o `uploadId` (sem prefixo) que o backend gerou.
|
|
536
|
+
*
|
|
537
|
+
* Recebe a função inteira, e não uma URL, porque autenticação varia: uma instalação assina com
|
|
538
|
+
* HMAC, outra manda token de admin, outra usa cookie de sessão. Pedir a URL obrigaria o pacote a
|
|
539
|
+
* escolher por elas.
|
|
540
|
+
*/
|
|
541
|
+
readonly upload: (request: PreviewMediaUploadRequest) => Promise<{
|
|
542
|
+
uploadId: string;
|
|
543
|
+
}>;
|
|
544
|
+
/** Nome usado quando o gravador entrega o áudio sem nome próprio. */
|
|
545
|
+
readonly fallbackFilename?: string;
|
|
546
|
+
readonly fallbackMimeType?: string;
|
|
547
|
+
};
|
|
548
|
+
declare function createPreviewMediaUploader(params: CreatePreviewMediaUploaderParams): (file: File) => Promise<PreviewUploadedMedia$1>;
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Cliente que entrega mensagens do preview no webhook real, assinadas com HMAC — a mesma validação
|
|
552
|
+
* de staging e produção, sem rota alternativa e sem bypass. Do ponto de vista da API, este cliente
|
|
553
|
+
* é indistinguível da Meta; o que muda é apenas quem assina.
|
|
554
|
+
*
|
|
555
|
+
* Assina com WebCrypto porque `node:crypto` não existe no navegador. Os builders vêm dos contratos
|
|
556
|
+
* (isomórficos) justamente para que o mesmo payload seja montado nos dois runtimes.
|
|
557
|
+
*
|
|
558
|
+
* ⚠️ SOMENTE EXECUÇÃO LOCAL. Isto carrega o app secret no bundle, e bundle é público onde quer que
|
|
559
|
+
* seja servido — em qualquer ambiente com URL acessível (homologação inclusive) usar esta fábrica
|
|
560
|
+
* equivale a publicar o segredo, e quem o tiver forja webhooks válidos daquele app da Meta: injeta
|
|
561
|
+
* mensagem de qualquer número e dispara os fluxos. `assertPreviewEnvironment` barra produção, mas
|
|
562
|
+
* homologação passaria, então a barreira não basta.
|
|
563
|
+
*
|
|
564
|
+
* Para qualquer ambiente publicado use `createPreviewBridgeClient`: o navegador manda a intenção e
|
|
565
|
+
* o servidor assina com o segredo que ele já tem.
|
|
566
|
+
*/
|
|
567
|
+
|
|
568
|
+
type PreviewWebhookClient = {
|
|
569
|
+
sendText(text: string): Promise<void>;
|
|
570
|
+
sendButtonReply(reply: InteractiveReplyOption): Promise<void>;
|
|
571
|
+
sendListReply(reply: InteractiveReplyOption): Promise<void>;
|
|
572
|
+
sendAudio(mediaId: string): Promise<void>;
|
|
573
|
+
sendMedia(params: SendPreviewMediaParams): Promise<void>;
|
|
574
|
+
/**
|
|
575
|
+
* Guarda um arquivo gravado e devolve o `mediaId` já prefixado, pronto para `sendMedia`.
|
|
576
|
+
*
|
|
577
|
+
* Existe no cliente, e não como prop de quem monta a tela, porque isto é exatamente o que ele já
|
|
578
|
+
* sabe fazer: falar com ESTE host usando ESTE segredo. Enquanto era responsabilidade do produto,
|
|
579
|
+
* o resultado prático foi um produto com microfone no simulador e outro sem — não por decisão,
|
|
580
|
+
* por esquecimento. Cliente montado, microfone na tela.
|
|
581
|
+
*
|
|
582
|
+
* Opcional porque o cliente-ponte só consegue oferecer isto quando sabe a rota de mídia (ou quando
|
|
583
|
+
* o host injeta a função): sem destino, gravar áudio seria falar para o vazio, e aí a tela
|
|
584
|
+
* corretamente não desenha o gravador.
|
|
585
|
+
*/
|
|
586
|
+
uploadMedia?(file: File): Promise<PreviewUploadedMedia$1>;
|
|
587
|
+
};
|
|
588
|
+
type SendPreviewMediaParams = {
|
|
589
|
+
readonly mediaType: InboundMediaType;
|
|
590
|
+
/**
|
|
591
|
+
* Id que o host já usa para buscar o arquivo. Não é bytes: o webhook da Meta entrega mídia por
|
|
592
|
+
* referência, e o consumidor baixa depois — mandar base64 aqui simularia um payload que a Meta
|
|
593
|
+
* nunca produz, e o caminho testado deixaria de ser o de produção.
|
|
594
|
+
*/
|
|
595
|
+
readonly mediaId: string;
|
|
596
|
+
readonly mimeType?: string;
|
|
597
|
+
readonly filename?: string;
|
|
598
|
+
readonly caption?: string;
|
|
599
|
+
};
|
|
600
|
+
type CreatePreviewWebhookClientParams = {
|
|
601
|
+
readonly webhookUrl: string;
|
|
602
|
+
readonly appSecret: string;
|
|
603
|
+
readonly from: string;
|
|
604
|
+
readonly phoneNumberId?: string;
|
|
605
|
+
/**
|
|
606
|
+
* Rota que guarda o áudio gravado. Por padrão, `/v1/preview/media` na mesma origem do webhook.
|
|
607
|
+
*
|
|
608
|
+
* O padrão cobre o caso normal — as duas rotas são do mesmo servidor — e a prop existe para quem
|
|
609
|
+
* publica a API em outro host ou versiona o caminho.
|
|
610
|
+
*/
|
|
611
|
+
readonly mediaUploadUrl?: string;
|
|
612
|
+
readonly fetchImplementation?: typeof fetch;
|
|
613
|
+
};
|
|
614
|
+
/** Falha da rota de upload, separada da do webhook: os dois lados quebram por motivos diferentes. */
|
|
615
|
+
declare class PreviewMediaUploadRejectedError extends Error {
|
|
616
|
+
readonly status: number;
|
|
617
|
+
constructor(status: number);
|
|
618
|
+
}
|
|
619
|
+
declare class PreviewInProductionError extends Error {
|
|
620
|
+
constructor();
|
|
621
|
+
}
|
|
622
|
+
declare class PreviewWebhookRejectedError extends Error {
|
|
623
|
+
readonly status: number;
|
|
624
|
+
constructor(status: number);
|
|
625
|
+
}
|
|
626
|
+
/**
|
|
627
|
+
* Falha alto em vez de degradar em silêncio: um preview que "quase funciona" em produção é pior
|
|
628
|
+
* que um que se recusa a montar.
|
|
629
|
+
*/
|
|
630
|
+
declare function assertPreviewEnvironment(isProduction: boolean): void;
|
|
631
|
+
/**
|
|
632
|
+
* Assina um texto qualquer com o app secret, no mesmo formato do header da Meta.
|
|
633
|
+
*
|
|
634
|
+
* Exportada porque o preview precisa provar identidade em MAIS de um lugar: além de entregar a
|
|
635
|
+
* mensagem no webhook, ele lê o transcript de volta — e ler pela API de admin exigia uma sessão que
|
|
636
|
+
* a aba do simulador não tem. Assinar a leitura com o segredo que ele já carrega resolve sem token
|
|
637
|
+
* de admin e sem rota aberta.
|
|
638
|
+
*/
|
|
639
|
+
declare function signPreviewPayload(params: {
|
|
640
|
+
rawBody: string;
|
|
641
|
+
appSecret: string;
|
|
642
|
+
}): Promise<string>;
|
|
643
|
+
declare const DEFAULT_MEDIA_UPLOAD_PATH = "/v1/preview/media";
|
|
644
|
+
/**
|
|
645
|
+
* O POST de mídia, sem a parte de assinatura — para os dois clientes usarem o mesmo caminho.
|
|
646
|
+
*
|
|
647
|
+
* O cliente-ponte autentica por sessão e o de webhook por HMAC; o que não muda é a rota, o formato
|
|
648
|
+
* do corpo e a leitura do `uploadId`. Duas cópias disso é como o prefixo de mídia divergiu antes.
|
|
649
|
+
*/
|
|
650
|
+
declare function createPreviewMediaPoster(params: {
|
|
651
|
+
readonly url: string;
|
|
652
|
+
readonly headers?: (mimeType: string) => Promise<Readonly<Record<string, string>>>;
|
|
653
|
+
readonly fetchImplementation?: typeof fetch;
|
|
654
|
+
}): (file: File) => Promise<PreviewUploadedMedia$1>;
|
|
655
|
+
declare function createPreviewWebhookClient(params: CreatePreviewWebhookClientParams): PreviewWebhookClient;
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* Copyright (c) 2026 Ada Technology. All rights reserved.
|
|
659
|
+
*
|
|
660
|
+
* This source code is proprietary and confidential. Unauthorized copying,
|
|
661
|
+
* modification, distribution, or use of this file, via any medium, is
|
|
662
|
+
* strictly prohibited without prior written permission from Ada Technology.
|
|
663
|
+
*
|
|
664
|
+
* Author: Anderson Filho <andersonfrfilho@gmail.com>
|
|
665
|
+
*
|
|
666
|
+
* Porta do simulador: o que a visão lado-cliente precisa saber fazer, sem canal no nome.
|
|
667
|
+
*
|
|
668
|
+
* O simulador nasceu falando WhatsApp — `PreviewWebhookClient`, payload assinado, mídia por
|
|
669
|
+
* `mediaId`. Só que a mesma casa atende pelo chat do próprio site, e simular ali não é uma segunda
|
|
670
|
+
* tela: é o mesmo painel, no mesmo lugar da conversa, com outro transporte. Sem esta porta cada
|
|
671
|
+
* canal novo viraria uma cópia da tela — e cópia de tela diverge, que é exatamente o que o
|
|
672
|
+
* `ConversationSimulatorPanel` existe para ter parado.
|
|
673
|
+
*
|
|
674
|
+
* O que é específico de canal fica no adaptador, nunca aqui:
|
|
675
|
+
* - **resposta de menu:** a Meta distingue `button_reply` de `list_reply`, e o roteador do fluxo lê
|
|
676
|
+
* campos diferentes; o chat do site manda o rótulo como texto, que é literalmente o que o
|
|
677
|
+
* visitante produz ao tocar no botão do widget. A porta entrega a seleção inteira e deixa cada
|
|
678
|
+
* adaptador escolher a forma de fio.
|
|
679
|
+
* - **mídia:** a Meta entrega por REFERÊNCIA (sobe o arquivo primeiro, o webhook carrega o `id`); o
|
|
680
|
+
* widget manda os BYTES no `FormData`. A porta trafega o `File` que a tela tem em mão — quem
|
|
681
|
+
* tiver passo de upload faz o upload por dentro.
|
|
682
|
+
*/
|
|
683
|
+
|
|
684
|
+
/**
|
|
685
|
+
* Tipos de mídia que o cliente pode mandar de dentro do simulador.
|
|
686
|
+
*
|
|
687
|
+
* Subconjunto proposital do que a Meta aceita: `sticker` chega do aparelho, mas não há como
|
|
688
|
+
* escolher um no seletor de arquivo do navegador — oferecer o tipo aqui seria um caminho morto.
|
|
689
|
+
*/
|
|
690
|
+
type SimulatorMediaKind = 'image' | 'video' | 'audio' | 'document';
|
|
691
|
+
/** Os tipos que saem do seletor de arquivo. `audio` fica de fora: ele vem do microfone. */
|
|
692
|
+
declare const SIMULATOR_FILE_MEDIA_KINDS: readonly SimulatorMediaKind[];
|
|
693
|
+
/** Deriva o tipo de mídia a partir do MIME do arquivo escolhido. */
|
|
694
|
+
declare function mediaKindOf(mimeType: string): SimulatorMediaKind;
|
|
695
|
+
type SendSimulatorMediaParams = {
|
|
696
|
+
readonly mediaKind: SimulatorMediaKind;
|
|
697
|
+
/** O arquivo do disco ou o áudio recém-gravado. Referência × bytes é decisão do adaptador. */
|
|
698
|
+
readonly file: File;
|
|
699
|
+
readonly mimeType?: string;
|
|
700
|
+
readonly filename?: string;
|
|
701
|
+
readonly caption?: string;
|
|
702
|
+
};
|
|
703
|
+
type ConversationSimulatorClient = {
|
|
704
|
+
sendText(text: string): Promise<void>;
|
|
705
|
+
sendReply(selection: InteractiveSelection): Promise<void>;
|
|
706
|
+
/**
|
|
707
|
+
* Ausente = este canal não recebe mídia do cliente, e o compositor não desenha clipe nem
|
|
708
|
+
* microfone. Melhor um botão que não existe do que um que falha ao ser tocado.
|
|
709
|
+
*/
|
|
710
|
+
sendMedia?(params: SendSimulatorMediaParams): Promise<void>;
|
|
711
|
+
/**
|
|
712
|
+
* Restringe o que `sendMedia` aceita. Ausente = todos os tipos.
|
|
713
|
+
*
|
|
714
|
+
* Existe porque canal com meia capacidade é comum: o chat do site sobe áudio (a API transcreve)
|
|
715
|
+
* mas não tem rota para imagem. Sem esta lista o clipe e o microfone apareciam juntos, e um dos
|
|
716
|
+
* dois falhava ao ser tocado.
|
|
717
|
+
*/
|
|
718
|
+
readonly acceptedMediaKinds?: readonly SimulatorMediaKind[];
|
|
719
|
+
};
|
|
720
|
+
/** Responde se o compositor deve desenhar o affordance daquele tipo. */
|
|
721
|
+
declare function acceptsMediaKind(client: ConversationSimulatorClient, kind: SimulatorMediaKind): boolean;
|
|
722
|
+
type ToSimulatorClientParams = {
|
|
723
|
+
readonly client: PreviewWebhookClient;
|
|
724
|
+
/** Destino alternativo do upload. Sem isto, usa o do próprio `client`. */
|
|
725
|
+
readonly uploadMedia?: (file: File) => Promise<PreviewUploadedMedia$1>;
|
|
726
|
+
};
|
|
727
|
+
/**
|
|
728
|
+
* Distingue a porta neutra do cliente WhatsApp legado, que continua aceito na prop.
|
|
729
|
+
*
|
|
730
|
+
* Leitura estrutural e não `instanceof`: os dois são objetos literais devolvidos por fábrica, e o
|
|
731
|
+
* host pode ter montado o seu à mão.
|
|
732
|
+
*/
|
|
733
|
+
declare function isConversationSimulatorClient(candidate: ConversationSimulatorClient | PreviewWebhookClient): candidate is ConversationSimulatorClient;
|
|
734
|
+
/**
|
|
735
|
+
* Adapta o cliente WhatsApp (webhook assinado ou ponte) para a porta neutra.
|
|
736
|
+
*
|
|
737
|
+
* O upload vive aqui dentro porque ele é uma etapa DO CANAL: no caminho da Meta a mídia precisa
|
|
738
|
+
* existir como `id` antes do webhook citá-la. Sem passo de upload disponível, `sendMedia` sai
|
|
739
|
+
* ausente — é o que mantém o clipe escondido em host que não montou destino para o arquivo, o
|
|
740
|
+
* comportamento que já existia antes desta porta.
|
|
741
|
+
*/
|
|
742
|
+
declare function toConversationSimulatorClient({ client, uploadMedia, }: ToSimulatorClientParams): ConversationSimulatorClient;
|
|
743
|
+
|
|
744
|
+
type ConversationPreviewProps = {
|
|
745
|
+
/**
|
|
746
|
+
* Transporte do canal. `PreviewWebhookClient` continua aceito — é o caminho WhatsApp de antes
|
|
747
|
+
* desta porta, adaptado aqui dentro para não obrigar host nenhum a mudar de chamada.
|
|
748
|
+
*/
|
|
749
|
+
client: ConversationSimulatorClient | PreviewWebhookClient;
|
|
750
|
+
sse: SSEProvider;
|
|
751
|
+
conversationId: string;
|
|
752
|
+
loadMessages: (conversationId: string) => Promise<MessagePayload[]>;
|
|
753
|
+
placeholder?: string;
|
|
754
|
+
/**
|
|
755
|
+
* Recarrega o transcript a cada N ms. Serve a host SEM stream: a resposta do bot é assíncrona, e
|
|
756
|
+
* sem SSE nem polling ela só apareceria no próximo envio — o sintoma é "às vezes ele não
|
|
757
|
+
* responde". Ausente, não faz polling (host com SSE não precisa).
|
|
758
|
+
*/
|
|
759
|
+
pollIntervalMs?: number;
|
|
760
|
+
/**
|
|
761
|
+
* Destino alternativo do upload, no canal que sobe a mídia antes de citá-la (o caminho da Meta
|
|
762
|
+
* entrega mídia por `id`). Sem isto, usa o do próprio `client`. Canal que manda os bytes direto
|
|
763
|
+
* ignora esta prop: quem decide referência × bytes é o adaptador do canal.
|
|
764
|
+
*/
|
|
765
|
+
uploadMedia?: (file: File) => Promise<PreviewUploadedMedia>;
|
|
766
|
+
};
|
|
767
|
+
type PreviewUploadedMedia = {
|
|
768
|
+
readonly mediaId: string;
|
|
769
|
+
readonly mimeType?: string;
|
|
770
|
+
readonly filename?: string;
|
|
771
|
+
};
|
|
772
|
+
/** @deprecated Use `mediaKindOf`, que não nomeia canal. Mantido para quem já importa. */
|
|
773
|
+
declare function mediaTypeOf(mimeType: string): SimulatorMediaKind;
|
|
774
|
+
declare function ConversationPreview({ client, sse, conversationId, loadMessages, placeholder, pollIntervalMs, uploadMedia, }: ConversationPreviewProps): react.JSX.Element;
|
|
775
|
+
|
|
776
|
+
type ConversationSimulatorPanelLabels = {
|
|
777
|
+
readonly title: string;
|
|
778
|
+
/** Complementa o identificador no subtítulo, explicando para onde a mensagem realmente vai. */
|
|
779
|
+
readonly destinationHint: string;
|
|
780
|
+
readonly close: string;
|
|
781
|
+
readonly placeholder: string;
|
|
782
|
+
};
|
|
783
|
+
declare const DEFAULT_CONVERSATION_SIMULATOR_PANEL_LABELS: ConversationSimulatorPanelLabels;
|
|
784
|
+
/** Rótulos do painel já resolvidos para o canal — útil para o host que monta o cabeçalho por fora. */
|
|
785
|
+
declare function simulatorPanelLabelsOf(channel: ConversationChannel | undefined): ConversationSimulatorPanelLabels;
|
|
786
|
+
type ConversationSimulatorPanelProps = Omit<ConversationPreviewProps, 'placeholder'> & {
|
|
787
|
+
readonly onClose: () => void;
|
|
788
|
+
/** Ausente = WhatsApp, que era o único canal antes desta prop existir. */
|
|
789
|
+
readonly channel?: ConversationChannel;
|
|
790
|
+
/**
|
|
791
|
+
* Identificador do contato já formatado para leitura — telefone no WhatsApp, apelido no Instagram,
|
|
792
|
+
* "Visitante 3f9c21" no chat do site. É o host que formata: máscara de telefone é convenção
|
|
793
|
+
* regional, e o pacote não tem como saber a do produto.
|
|
794
|
+
*/
|
|
795
|
+
readonly displayHandle?: string;
|
|
796
|
+
/** @deprecated Use `displayHandle` — o simulador deixou de ser só telefone. */
|
|
797
|
+
readonly displayNumber?: string;
|
|
798
|
+
readonly labels?: Partial<ConversationSimulatorPanelLabels>;
|
|
799
|
+
/** Ações extras no cabeçalho — roteiro automático, limpar conversa, trocar de contato. */
|
|
800
|
+
readonly headerActions?: ReactNode;
|
|
801
|
+
};
|
|
802
|
+
declare function ConversationSimulatorPanel({ onClose, channel, displayHandle, displayNumber, labels, headerActions, ...previewProps }: ConversationSimulatorPanelProps): react.JSX.Element;
|
|
803
|
+
|
|
804
|
+
export { type PreviewWebhookClient as $, AudioRecorderButton as A, type CreatePreviewMediaUploaderParams as B, CHANNEL_CAPABILITIES as C, type CreatePreviewWebhookClientParams as D, DEFAULT_AUDIO_RECORDER_BUTTON_LABELS as E, DEFAULT_CONVERSATION_CHANNEL as F, DEFAULT_CONVERSATION_SIMULATOR_PANEL_LABELS as G, DEFAULT_MAX_RECORDING_MILLISECONDS as H, DEFAULT_MEDIA_UPLOAD_PATH as I, type FormatContactHandleParams as J, HANDLE_KIND as K, type HandleKind as L, type InteractiveOption as M, type InteractivePayload as N, type InteractiveSection as O, type InteractiveSelection as P, type ListConversationsParams as Q, type ListDocumentsParams as R, MediaRenderer as S, type MediaRendererProps as T, type MessagePayload as U, type MessageTranscription as V, PreviewInProductionError as W, PreviewMediaUploadRejectedError as X, type PreviewMediaUploadRequest as Y, type PreviewUploadedMedia$1 as Z, type PreviewUploadedMedia as _, type AudioRecorderButtonLabels as a, PreviewWebhookRejectedError as a0, REOPEN_MECHANISM as a1, type ReopenMechanism as a2, type ResolveMediaUrl as a3, SIMULATOR_FILE_MEDIA_KINDS as a4, type SSEProvider as a5, type SendPreviewMediaParams as a6, type SendSimulatorMediaParams as a7, type SimulatorMediaKind as a8, type ToSimulatorClientParams as a9, type TranscriptionMode as aa, type TranscriptionStatus as ab, acceptsMediaKind as ac, assertPreviewEnvironment as ad, capabilitiesOf as ae, channelFiltersFor as af, contactFlag as ag, createPreviewMediaPoster as ah, createPreviewMediaUploader as ai, createPreviewWebhookClient as aj, formatContactHandle as ak, isConversationSimulatorClient as al, mediaKindOf as am, mediaTypeOf as an, signPreviewPayload as ao, simulatorPanelLabelsOf as ap, toConversationSimulatorClient as aq, type AudioRecorderButtonProps as b, CHANNEL_FILTER_ALL as c, CONVERSATION_CHANNEL as d, type ChannelCapabilities as e, type ChannelFilter as f, type ChannelFilterOption as g, type CompanyDocument as h, type CompanyDocumentPage as i, type ConversationChannel as j, type ConversationDocument as k, type ConversationDocumentPage as l, type ConversationEventSource as m, type ConversationPage as n, ConversationPreview as o, type ConversationPreviewProps as p, type ConversationSimulatorClient as q, ConversationSimulatorPanel as r, type ConversationSimulatorPanelLabels as s, type ConversationSimulatorPanelProps as t, type ConversationSummary as u, type ConversationTemplate as v, type ConversationsApi as w, type ConversationsFeatures as x, type ConversationsTheme as y, type ConversationsUIConfig as z };
|