@adatechnology/conversations-ui 0.1.0-rc.11 → 0.1.0-rc.13

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.
@@ -1,4 +1,5 @@
1
- import { y as MessagePayload, m as ConversationSummary, k as ConversationEventSource, o as ConversationsApi, L as ListConversationsParams, l as ConversationPage, S as SSEProvider, i as ConversationDocument, A as ResolveMediaUrl } from '../types-B5C1DLu1.js';
1
+ import { G as MessagePayload, o as ConversationSummary, m as ConversationEventSource, q as ConversationsApi, L as ListConversationsParams, n as ConversationPage, S as SSEProvider, k as ConversationDocument, K as ResolveMediaUrl } from '../types-C_dqJ83O.js';
2
+ export { A as AudioRecorderButton, a as AudioRecorderButtonLabels, b as AudioRecorderButtonProps, D as DEFAULT_AUDIO_RECORDER_BUTTON_LABELS } from '../types-C_dqJ83O.js';
2
3
  import * as react from 'react';
3
4
  import { InteractiveReplyOption, InboundMediaType } from '@adatechnology/meta-whatsapp-contracts/testing';
4
5
 
@@ -205,41 +206,6 @@ type PreviewUploadedMedia = {
205
206
  declare function mediaTypeOf(mimeType: string): SendPreviewMediaParams['mediaType'];
206
207
  declare function ConversationPreview({ client, sse, conversationId, loadMessages, placeholder, pollIntervalMs, uploadMedia, }: ConversationPreviewProps): react.JSX.Element;
207
208
 
208
- /**
209
- * Gravação de áudio no simulador, pelo microfone do próprio navegador.
210
- *
211
- * Existe porque áudio é o formato que mais chega de cliente real e o que mais quebra fluxo: sem
212
- * poder gravar aqui, testar o caminho de transcrição exigia mandar mensagem do celular de alguém.
213
- *
214
- * O arquivo gravado sai daqui como `File` e segue exatamente o mesmo caminho de um anexo — quem
215
- * hospeda e devolve o `mediaId` é o host, via `uploadMedia`.
216
- */
217
- interface AudioRecorderButtonLabels {
218
- start: string;
219
- stop: string;
220
- unsupported: string;
221
- denied: string;
222
- }
223
- declare const DEFAULT_AUDIO_RECORDER_BUTTON_LABELS: AudioRecorderButtonLabels;
224
- interface AudioRecorderButtonProps {
225
- onRecorded: (file: File) => void | Promise<void>;
226
- onFailure?: (message: string) => void;
227
- /**
228
- * Avisa quando a gravação começa e termina. O botão é um interruptor — o segundo toque é que
229
- * envia — e sem um aviso fora dele o operador grava, não vê nada acontecer e desiste achando
230
- * que o microfone está quebrado.
231
- */
232
- onRecordingChange?: (isRecording: boolean) => void;
233
- /**
234
- * Teto de duração da gravação, em milissegundos. Passado o tempo, o gravador para e envia o que
235
- * tem. Produto com limite próprio sobrescreve.
236
- */
237
- maxDurationMilliseconds?: number;
238
- labels?: Partial<AudioRecorderButtonLabels>;
239
- disabled?: boolean;
240
- }
241
- declare function AudioRecorderButton({ onRecorded, onFailure, onRecordingChange, maxDurationMilliseconds, labels, disabled, }: AudioRecorderButtonProps): react.JSX.Element;
242
-
243
209
  /**
244
210
  * Roteiro que mantém o preview vivo: sem tráfego chegando, a inbox é uma tela estática e as
245
211
  * transições que o atendente precisa testar (fila de espera enchendo, handoff, devolução ao bot)
@@ -338,4 +304,4 @@ type MediaTypesPreviewProps = {
338
304
  };
339
305
  declare function MediaTypesPreview({ conversationId, className, }: MediaTypesPreviewProps): react.JSX.Element;
340
306
 
341
- export { type AppendMessageParams, AudioRecorderButton, type AudioRecorderButtonLabels, type AudioRecorderButtonProps, ConversationPreview, type ConversationPreviewProps, type CreateMockConversationsApiParams, type CreateMockSSEProviderParams, type CreatePreviewStoreParams, type CreatePreviewWebhookClientParams, DEFAULT_AUDIO_RECORDER_BUTTON_LABELS, 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, type PreviewEmission, PreviewInProductionError, type PreviewScriptStep, type PreviewStore, type PreviewStoreListener, type PreviewUploadedMedia, type PreviewWebhookClient, PreviewWebhookRejectedError, type SendPreviewMediaParams, type SetModeParams, type StartPreviewScriptParams, assertPreviewEnvironment, conversationChannel, createMockConversationsApi, createMockEventSource, createMockSSEProvider, createPreviewMediaResolver, createPreviewStore, createPreviewWebhookClient, mediaTypeOf, previewFileBase64, previewFileUrl, resolvePreviewFileSample, signPreviewPayload, startPreviewScript };
307
+ export { type AppendMessageParams, ConversationPreview, type ConversationPreviewProps, type CreateMockConversationsApiParams, type CreateMockSSEProviderParams, type CreatePreviewStoreParams, type CreatePreviewWebhookClientParams, 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, type PreviewEmission, PreviewInProductionError, type PreviewScriptStep, type PreviewStore, type PreviewStoreListener, type PreviewUploadedMedia, type PreviewWebhookClient, PreviewWebhookRejectedError, type SendPreviewMediaParams, type SetModeParams, type StartPreviewScriptParams, assertPreviewEnvironment, conversationChannel, createMockConversationsApi, createMockEventSource, createMockSSEProvider, createPreviewMediaResolver, createPreviewStore, createPreviewWebhookClient, mediaTypeOf, previewFileBase64, previewFileUrl, resolvePreviewFileSample, signPreviewPayload, startPreviewScript };
@@ -1,12 +1,14 @@
1
1
  import {
2
+ AudioRecorderButton,
2
3
  ConversationDocumentsPanel,
3
4
  ConversationWallpaper,
4
5
  ConversationsProvider,
6
+ DEFAULT_AUDIO_RECORDER_BUTTON_LABELS,
5
7
  DateDivider,
6
8
  DocumentsLibrary,
7
9
  MessageBubble,
8
10
  MessageComposer
9
- } from "../chunk-3RKFU46R.js";
11
+ } from "../chunk-QFEESERN.js";
10
12
  import "../chunk-2AYDBWNE.js";
11
13
 
12
14
  // src/preview/previewStore.ts
@@ -845,98 +847,8 @@ function createMockSSEProvider(params) {
845
847
  }
846
848
 
847
849
  // src/preview/ConversationPreview.tsx
848
- import { useCallback as useCallback2, useEffect, useMemo, useRef as useRef2, useState as useState2 } from "react";
849
-
850
- // src/preview/AudioRecorderButton.tsx
851
- import { useCallback, useRef, useState } from "react";
850
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
852
851
  import { jsx, jsxs } from "react/jsx-runtime";
853
- var DEFAULT_AUDIO_RECORDER_BUTTON_LABELS = {
854
- start: "Gravar \xE1udio",
855
- stop: "Parar grava\xE7\xE3o",
856
- unsupported: "Este navegador n\xE3o grava \xE1udio.",
857
- denied: "Sem permiss\xE3o para usar o microfone."
858
- };
859
- var DEFAULT_MAX_RECORDING_MILLISECONDS = 5 * 60 * 1e3;
860
- var RECORDING_FORMATS = [
861
- { mimeType: "audio/ogg;codecs=opus", uploadMimeType: "audio/ogg", extension: "ogg" },
862
- { mimeType: "audio/mp4", uploadMimeType: "audio/mp4", extension: "m4a" },
863
- { mimeType: "audio/webm", uploadMimeType: "audio/webm", extension: "webm" }
864
- ];
865
- function resolveRecordingFormat() {
866
- if (typeof MediaRecorder === "undefined") return void 0;
867
- if (typeof MediaRecorder.isTypeSupported !== "function") return RECORDING_FORMATS[0];
868
- return RECORDING_FORMATS.find((format) => MediaRecorder.isTypeSupported(format.mimeType));
869
- }
870
- function AudioRecorderButton({
871
- onRecorded,
872
- onFailure,
873
- onRecordingChange,
874
- maxDurationMilliseconds = DEFAULT_MAX_RECORDING_MILLISECONDS,
875
- labels,
876
- disabled
877
- }) {
878
- const startLabel = labels?.start ?? DEFAULT_AUDIO_RECORDER_BUTTON_LABELS.start;
879
- const stopLabel = labels?.stop ?? DEFAULT_AUDIO_RECORDER_BUTTON_LABELS.stop;
880
- const [isRecording, setIsRecording] = useState(false);
881
- const recorderRef = useRef(null);
882
- const autoStopRef = useRef(void 0);
883
- const stop = useCallback(() => {
884
- recorderRef.current?.stop();
885
- }, []);
886
- const start = useCallback(async () => {
887
- const format = resolveRecordingFormat();
888
- if (!format || !navigator.mediaDevices?.getUserMedia) {
889
- onFailure?.(labels?.unsupported ?? DEFAULT_AUDIO_RECORDER_BUTTON_LABELS.unsupported);
890
- return;
891
- }
892
- try {
893
- const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
894
- const recorder = new MediaRecorder(stream, { mimeType: format.mimeType });
895
- const chunks = [];
896
- recorder.addEventListener("dataavailable", (event) => {
897
- if (event.data.size > 0) chunks.push(event.data);
898
- });
899
- recorder.addEventListener("stop", () => {
900
- stream.getTracks().forEach((track) => track.stop());
901
- clearTimeout(autoStopRef.current);
902
- setIsRecording(false);
903
- onRecordingChange?.(false);
904
- recorderRef.current = null;
905
- const blob = new Blob(chunks, { type: format.uploadMimeType });
906
- void onRecorded(
907
- new File([blob], `audio-${Date.now()}.${format.extension}`, { type: format.uploadMimeType })
908
- );
909
- });
910
- recorderRef.current = recorder;
911
- recorder.start();
912
- autoStopRef.current = setTimeout(() => recorder.stop(), maxDurationMilliseconds);
913
- setIsRecording(true);
914
- onRecordingChange?.(true);
915
- } catch {
916
- onFailure?.(labels?.denied ?? DEFAULT_AUDIO_RECORDER_BUTTON_LABELS.denied);
917
- }
918
- }, [labels?.denied, labels?.unsupported, maxDurationMilliseconds, onFailure, onRecorded, onRecordingChange]);
919
- return /* @__PURE__ */ jsx(
920
- "button",
921
- {
922
- type: "button",
923
- disabled,
924
- onClick: () => isRecording ? stop() : void start(),
925
- title: isRecording ? stopLabel : startLabel,
926
- "aria-label": isRecording ? stopLabel : startLabel,
927
- "aria-pressed": isRecording,
928
- className: `flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-full transition-colors ${isRecording ? "animate-pulse bg-red-500 text-white ring-4 ring-red-500/30 hover:bg-red-600" : "text-gray-500 hover:bg-gray-200 dark:hover:bg-gray-700"}`,
929
- children: isRecording ? /* @__PURE__ */ jsx("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "currentColor", children: /* @__PURE__ */ jsx("rect", { x: "6", y: "6", width: "12", height: "12", rx: "2" }) }) : /* @__PURE__ */ jsxs("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", children: [
930
- /* @__PURE__ */ jsx("path", { d: "M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3z" }),
931
- /* @__PURE__ */ jsx("path", { d: "M19 11a7 7 0 0 1-14 0" }),
932
- /* @__PURE__ */ jsx("line", { x1: "12", y1: "18", x2: "12", y2: "22" })
933
- ] })
934
- }
935
- );
936
- }
937
-
938
- // src/preview/ConversationPreview.tsx
939
- import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
940
852
  function mediaTypeOf(mimeType) {
941
853
  if (mimeType.startsWith("image/")) return "image";
942
854
  if (mimeType.startsWith("video/")) return "video";
@@ -983,15 +895,15 @@ function ConversationPreview({
983
895
  pollIntervalMs,
984
896
  uploadMedia
985
897
  }) {
986
- const [messages, setMessages] = useState2([]);
987
- const [failure, setFailure] = useState2(void 0);
988
- const [loadFailure, setLoadFailure] = useState2(void 0);
989
- const [isRecording, setIsRecording] = useState2(false);
990
- const [pendingLocal, setPendingLocal] = useState2([]);
991
- const loadMessagesRef = useRef2(loadMessages);
992
- const bottomRef = useRef2(null);
898
+ const [messages, setMessages] = useState([]);
899
+ const [failure, setFailure] = useState(void 0);
900
+ const [loadFailure, setLoadFailure] = useState(void 0);
901
+ const [isRecording, setIsRecording] = useState(false);
902
+ const [pendingLocal, setPendingLocal] = useState([]);
903
+ const loadMessagesRef = useRef(loadMessages);
904
+ const bottomRef = useRef(null);
993
905
  loadMessagesRef.current = loadMessages;
994
- const refresh = useCallback2(async () => {
906
+ const refresh = useCallback(async () => {
995
907
  try {
996
908
  const loaded = await loadMessagesRef.current(conversationId);
997
909
  setMessages(loaded);
@@ -1084,11 +996,11 @@ function ConversationPreview({
1084
996
  setFailure(error instanceof Error ? error.message : "Falha ao enviar o arquivo.");
1085
997
  }
1086
998
  }
1087
- return /* @__PURE__ */ jsxs2("div", { className: "flex h-full min-h-0 flex-col", children: [
1088
- /* @__PURE__ */ jsxs2(ConversationWallpaper, { className: "flex-1 min-h-0 overflow-y-auto px-4 py-3", children: [
1089
- rendered.map(({ message, isFirstInGroup, showDateDivider }) => /* @__PURE__ */ jsxs2("div", { children: [
1090
- showDateDivider ? /* @__PURE__ */ jsx2(DateDivider, { iso: message.timestamp }) : null,
1091
- /* @__PURE__ */ jsx2(
999
+ return /* @__PURE__ */ jsxs("div", { className: "flex h-full min-h-0 flex-col", children: [
1000
+ /* @__PURE__ */ jsxs(ConversationWallpaper, { className: "flex-1 min-h-0 overflow-y-auto px-4 py-3", children: [
1001
+ rendered.map(({ message, isFirstInGroup, showDateDivider }) => /* @__PURE__ */ jsxs("div", { children: [
1002
+ showDateDivider ? /* @__PURE__ */ jsx(DateDivider, { iso: message.timestamp }) : null,
1003
+ /* @__PURE__ */ jsx(
1092
1004
  MessageBubble,
1093
1005
  {
1094
1006
  message,
@@ -1098,17 +1010,17 @@ function ConversationPreview({
1098
1010
  }
1099
1011
  )
1100
1012
  ] }, message.id)),
1101
- /* @__PURE__ */ jsx2("div", { ref: bottomRef })
1013
+ /* @__PURE__ */ jsx("div", { ref: bottomRef })
1102
1014
  ] }),
1103
- failure ? /* @__PURE__ */ jsx2("p", { role: "alert", className: "px-4 py-2 text-sm text-red-600 dark:text-red-400", children: failure }) : null,
1104
- loadFailure ? /* @__PURE__ */ jsx2("p", { role: "status", className: "px-4 py-2 text-sm text-amber-700 dark:text-amber-400", children: loadFailure }) : null,
1105
- /* @__PURE__ */ jsx2(
1015
+ failure ? /* @__PURE__ */ jsx("p", { role: "alert", className: "px-4 py-2 text-sm text-red-600 dark:text-red-400", children: failure }) : null,
1016
+ loadFailure ? /* @__PURE__ */ jsx("p", { role: "status", className: "px-4 py-2 text-sm text-amber-700 dark:text-amber-400", children: loadFailure }) : null,
1017
+ /* @__PURE__ */ jsx(
1106
1018
  MessageComposer,
1107
1019
  {
1108
1020
  onSend: (text) => void handleSend(text),
1109
1021
  onAttach: uploadMedia ? (file) => void handleAttach(file) : void 0,
1110
1022
  placeholder: isRecording ? "Gravando\u2026 toque no quadrado para enviar" : placeholder ?? "Escreva como o cliente\u2026",
1111
- idleAction: uploadMedia ? /* @__PURE__ */ jsx2(
1023
+ idleAction: uploadMedia ? /* @__PURE__ */ jsx(
1112
1024
  AudioRecorderButton,
1113
1025
  {
1114
1026
  onRecorded: (file) => void handleAttach(file),
@@ -1218,7 +1130,7 @@ function startPreviewScript(params) {
1218
1130
 
1219
1131
  // src/preview/MediaTypesPreview.tsx
1220
1132
  import { useMemo as useMemo2 } from "react";
1221
- import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
1133
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
1222
1134
  var MEDIA_TYPES_CONVERSATION_ID = "5511944443333";
1223
1135
  function MediaTypesPreview({
1224
1136
  conversationId = MEDIA_TYPES_CONVERSATION_ID,
@@ -1233,26 +1145,26 @@ function MediaTypesPreview({
1233
1145
  const messages = PREVIEW_MESSAGES[conversationId] ?? [];
1234
1146
  const documents = PREVIEW_DOCUMENTS[conversationId] ?? [];
1235
1147
  const mimeTypes = [...new Set(documents.map((document) => document.mimeType))];
1236
- return /* @__PURE__ */ jsx3(ConversationsProvider, { api, sse, children: /* @__PURE__ */ jsxs3("div", { className, children: [
1237
- /* @__PURE__ */ jsxs3("header", { className: "border-b px-4 py-3 dark:border-gray-700", children: [
1238
- /* @__PURE__ */ jsx3("h1", { className: "text-lg font-semibold", children: "Teste manual de m\xEDdia" }),
1239
- /* @__PURE__ */ jsxs3("p", { className: "text-sm text-gray-500", children: [
1148
+ return /* @__PURE__ */ jsx2(ConversationsProvider, { api, sse, children: /* @__PURE__ */ jsxs2("div", { className, children: [
1149
+ /* @__PURE__ */ jsxs2("header", { className: "border-b px-4 py-3 dark:border-gray-700", children: [
1150
+ /* @__PURE__ */ jsx2("h1", { className: "text-lg font-semibold", children: "Teste manual de m\xEDdia" }),
1151
+ /* @__PURE__ */ jsxs2("p", { className: "text-sm text-gray-500", children: [
1240
1152
  documents.length,
1241
1153
  " arquivos, ",
1242
1154
  mimeTypes.length,
1243
1155
  " tipos. Clique no olho para abrir em aba nova e no bot\xE3o da bolha para carregar a m\xEDdia na thread \u2014 \xE9 o que teste automatizado n\xE3o v\xEA."
1244
1156
  ] })
1245
1157
  ] }),
1246
- /* @__PURE__ */ jsxs3("div", { className: "grid gap-4 p-4 lg:grid-cols-2", children: [
1247
- /* @__PURE__ */ jsxs3("section", { className: "space-y-3", children: [
1248
- /* @__PURE__ */ jsx3("h2", { className: "text-sm font-semibold uppercase tracking-wide text-gray-500", children: "Biblioteca da empresa" }),
1249
- /* @__PURE__ */ jsx3(DocumentsLibrary, { perPage: documents.length || 20 })
1158
+ /* @__PURE__ */ jsxs2("div", { className: "grid gap-4 p-4 lg:grid-cols-2", children: [
1159
+ /* @__PURE__ */ jsxs2("section", { className: "space-y-3", children: [
1160
+ /* @__PURE__ */ jsx2("h2", { className: "text-sm font-semibold uppercase tracking-wide text-gray-500", children: "Biblioteca da empresa" }),
1161
+ /* @__PURE__ */ jsx2(DocumentsLibrary, { perPage: documents.length || 20 })
1250
1162
  ] }),
1251
- /* @__PURE__ */ jsxs3("section", { className: "space-y-3", children: [
1252
- /* @__PURE__ */ jsx3("h2", { className: "text-sm font-semibold uppercase tracking-wide text-gray-500", children: "Painel da conversa" }),
1253
- /* @__PURE__ */ jsx3(ConversationDocumentsPanel, { conversationId, open: true, perPage: documents.length || 20 }),
1254
- /* @__PURE__ */ jsx3("h2", { className: "text-sm font-semibold uppercase tracking-wide text-gray-500", children: "Bolhas na thread" }),
1255
- /* @__PURE__ */ jsx3(ConversationWallpaper, { className: "max-h-[70vh] overflow-y-auto rounded-lg px-3 py-2", children: messages.map((message, index) => /* @__PURE__ */ jsx3(
1163
+ /* @__PURE__ */ jsxs2("section", { className: "space-y-3", children: [
1164
+ /* @__PURE__ */ jsx2("h2", { className: "text-sm font-semibold uppercase tracking-wide text-gray-500", children: "Painel da conversa" }),
1165
+ /* @__PURE__ */ jsx2(ConversationDocumentsPanel, { conversationId, open: true, perPage: documents.length || 20 }),
1166
+ /* @__PURE__ */ jsx2("h2", { className: "text-sm font-semibold uppercase tracking-wide text-gray-500", children: "Bolhas na thread" }),
1167
+ /* @__PURE__ */ jsx2(ConversationWallpaper, { className: "max-h-[70vh] overflow-y-auto rounded-lg px-3 py-2", children: messages.map((message, index) => /* @__PURE__ */ jsx2(
1256
1168
  MessageBubble,
1257
1169
  {
1258
1170
  message,
@@ -104,6 +104,47 @@ interface MediaRendererProps {
104
104
  }
105
105
  declare function MediaRenderer({ message, onLightbox, onResolveUrl, className }: MediaRendererProps): react.JSX.Element | null;
106
106
 
107
+ /**
108
+ * Gravação de áudio no simulador, pelo microfone do próprio navegador.
109
+ *
110
+ * Existe porque áudio é o formato que mais chega de cliente real e o que mais quebra fluxo: sem
111
+ * poder gravar aqui, testar o caminho de transcrição exigia mandar mensagem do celular de alguém.
112
+ *
113
+ * O arquivo gravado sai daqui como `File` e segue exatamente o mesmo caminho de um anexo — quem
114
+ * hospeda e devolve o `mediaId` é o host, via `uploadMedia`.
115
+ */
116
+ interface AudioRecorderButtonLabels {
117
+ start: string;
118
+ stop: string;
119
+ unsupported: string;
120
+ denied: string;
121
+ }
122
+ declare const DEFAULT_AUDIO_RECORDER_BUTTON_LABELS: AudioRecorderButtonLabels;
123
+ interface AudioRecorderButtonProps {
124
+ onRecorded: (file: File) => void | Promise<void>;
125
+ onFailure?: (message: string) => void;
126
+ /**
127
+ * Avisa quando a gravação começa e termina. O botão é um interruptor — o segundo toque é que
128
+ * envia — e sem um aviso fora dele o operador grava, não vê nada acontecer e desiste achando
129
+ * que o microfone está quebrado.
130
+ */
131
+ onRecordingChange?: (isRecording: boolean) => void;
132
+ /**
133
+ * Teto de duração da gravação, em milissegundos. Passado o tempo, o gravador para e envia o que
134
+ * tem. Produto com limite próprio sobrescreve.
135
+ */
136
+ maxDurationMilliseconds?: number;
137
+ labels?: Partial<AudioRecorderButtonLabels>;
138
+ disabled?: boolean;
139
+ }
140
+ /**
141
+ * Cinco minutos: com o codec de voz do WhatsApp isso dá menos de 3MB, folgado dentro do teto de
142
+ * 16MB que a Meta impõe a áudio, e é mais do que qualquer recado de cliente. O corte automático
143
+ * existe porque gravação esquecida aberta só se descobre no envio, com o arquivo inteiro perdido.
144
+ */
145
+ declare const DEFAULT_MAX_RECORDING_MILLISECONDS: number;
146
+ declare function AudioRecorderButton({ onRecorded, onFailure, onRecordingChange, maxDurationMilliseconds, labels, disabled, }: AudioRecorderButtonProps): react.JSX.Element;
147
+
107
148
  /**
108
149
  * Canal de origem da conversa e o que cada um permite.
109
150
  *
@@ -362,4 +403,4 @@ interface ConversationDocument {
362
403
  linkedAt: string;
363
404
  }
364
405
 
365
- export { type ResolveMediaUrl as A, capabilitiesOf as B, CHANNEL_CAPABILITIES as C, DEFAULT_CONVERSATION_CHANNEL as D, channelFiltersFor as E, type FormatContactHandleParams as F, contactFlag as G, HANDLE_KIND as H, type InteractiveOption as I, formatContactHandle as J, type ListConversationsParams as L, MediaRenderer as M, REOPEN_MECHANISM as R, type SSEProvider as S, CHANNEL_FILTER_ALL as a, CONVERSATION_CHANNEL as b, type ChannelCapabilities as c, type ChannelFilter as d, type ChannelFilterOption as e, type CompanyDocument as f, type CompanyDocumentPage as g, type ConversationChannel as h, type ConversationDocument as i, type ConversationDocumentPage as j, type ConversationEventSource as k, type ConversationPage as l, type ConversationSummary as m, type ConversationTemplate as n, type ConversationsApi as o, type ConversationsFeatures as p, type ConversationsTheme as q, type ConversationsUIConfig as r, type HandleKind as s, type InteractivePayload as t, type InteractiveSection as u, type InteractiveSelection as v, type ListDocumentsParams as w, type MediaRendererProps as x, type MessagePayload as y, type ReopenMechanism as z };
406
+ export { AudioRecorderButton as A, type ListDocumentsParams as B, CHANNEL_CAPABILITIES as C, DEFAULT_AUDIO_RECORDER_BUTTON_LABELS as D, type MediaRendererProps as E, type FormatContactHandleParams as F, type MessagePayload as G, HANDLE_KIND as H, type InteractiveOption as I, type ReopenMechanism as J, type ResolveMediaUrl as K, type ListConversationsParams as L, MediaRenderer as M, capabilitiesOf as N, channelFiltersFor as O, contactFlag as P, formatContactHandle as Q, REOPEN_MECHANISM as R, type SSEProvider as S, type AudioRecorderButtonLabels as a, 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, type ConversationSummary as o, type ConversationTemplate as p, type ConversationsApi as q, type ConversationsFeatures as r, type ConversationsTheme as s, type ConversationsUIConfig as t, DEFAULT_CONVERSATION_CHANNEL as u, DEFAULT_MAX_RECORDING_MILLISECONDS as v, type HandleKind as w, type InteractivePayload as x, type InteractiveSection as y, type InteractiveSelection as z };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adatechnology/conversations-ui",
3
- "version": "0.1.0-rc.11",
3
+ "version": "0.1.0-rc.13",
4
4
  "description": "WhatsApp conversation UI components — parametrizável por endpoint, tema e feature flags",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -0,0 +1,83 @@
1
+ import { describe, expect, it } from 'bun:test'
2
+ import { renderToStaticMarkup } from 'react-dom/server'
3
+
4
+ import { RichMessageComposer, type RichComposerQuickReply, type RichComposerVariable } from './RichMessageComposer'
5
+
6
+ const noop = () => {}
7
+
8
+ const QUICK_REPLIES: RichComposerQuickReply[] = [
9
+ { id: 'greeting', label: 'Saudação', text: 'Olá!', tooltip: 'Abre o atendimento' },
10
+ ]
11
+
12
+ const VARIABLES: RichComposerVariable[] = [
13
+ { id: 'name', label: 'Nome do cliente', value: '{{nome}}' },
14
+ ]
15
+
16
+ describe('RichMessageComposer', () => {
17
+ it('mostra as dicas padrão em todas as ações da barra', () => {
18
+ const markup = renderToStaticMarkup(
19
+ <RichMessageComposer value="" onChange={noop} onSend={noop} onAttachFiles={noop} />,
20
+ )
21
+
22
+ for (const tooltip of ['Negrito', 'Itálico', 'Tachado', 'Monoespaçado', 'Anexar arquivo', 'Enviar']) {
23
+ expect(markup).toContain(`title="${tooltip}"`)
24
+ }
25
+ })
26
+
27
+ it('deixa o host trocar o texto de uma dica sem mexer nas outras', () => {
28
+ const markup = renderToStaticMarkup(
29
+ <RichMessageComposer value="" onChange={noop} onSend={noop} tooltips={{ bold: 'Deixar em negrito' }} />,
30
+ )
31
+
32
+ expect(markup).toContain('title="Deixar em negrito"')
33
+ expect(markup).not.toContain('title="Negrito"')
34
+ expect(markup).toContain('title="Itálico"')
35
+ })
36
+
37
+ it('esconde só a ação que o host desligou', () => {
38
+ const markup = renderToStaticMarkup(
39
+ <RichMessageComposer value="" onChange={noop} onSend={noop} toolbar={{ monospace: false }} />,
40
+ )
41
+
42
+ expect(markup).not.toContain('title="Monoespaçado"')
43
+ expect(markup).toContain('title="Negrito"')
44
+ })
45
+
46
+ it('não desenha o anexo quando não há como anexar', () => {
47
+ const markup = renderToStaticMarkup(<RichMessageComposer value="" onChange={noop} onSend={noop} />)
48
+
49
+ expect(markup).not.toContain('title="Anexar arquivo"')
50
+ })
51
+
52
+ it('desenha os chips de resposta rápida com o rótulo e a dica do host', () => {
53
+ const markup = renderToStaticMarkup(
54
+ <RichMessageComposer value="" onChange={noop} onSend={noop} quickReplies={QUICK_REPLIES} />,
55
+ )
56
+
57
+ expect(markup).toContain('Saudação')
58
+ expect(markup).toContain('title="Abre o atendimento"')
59
+ })
60
+
61
+ it('só oferece variáveis quando o host manda alguma', () => {
62
+ const semVariaveis = renderToStaticMarkup(<RichMessageComposer value="" onChange={noop} onSend={noop} />)
63
+ const comVariaveis = renderToStaticMarkup(
64
+ <RichMessageComposer value="" onChange={noop} onSend={noop} variables={VARIABLES} />,
65
+ )
66
+
67
+ expect(semVariaveis).not.toContain('title="Variáveis disponíveis"')
68
+ expect(comVariaveis).toContain('title="Variáveis disponíveis"')
69
+ })
70
+
71
+ it('cede o lugar do enviar à ação ociosa enquanto não há texto', () => {
72
+ const vazio = renderToStaticMarkup(
73
+ <RichMessageComposer value="" onChange={noop} onSend={noop} idleAction={<button aria-label="Gravar áudio" />} />,
74
+ )
75
+ const comTexto = renderToStaticMarkup(
76
+ <RichMessageComposer value="oi" onChange={noop} onSend={noop} idleAction={<button aria-label="Gravar áudio" />} />,
77
+ )
78
+
79
+ expect(vazio).toContain('aria-label="Gravar áudio"')
80
+ expect(vazio).not.toContain('title="Enviar"')
81
+ expect(comTexto).toContain('title="Enviar"')
82
+ })
83
+ })