@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.
Files changed (132) hide show
  1. package/dist/ConversationSimulatorPanel--5fIzXWY.d.ts +804 -0
  2. package/dist/chunk-BJNRLLDO.js +2708 -0
  3. package/dist/chunk-DKPXKQGC.js +110 -0
  4. package/dist/{chunk-OGRRHQQW.js → chunk-WCBDXZ3X.js} +68 -4
  5. package/dist/flows/index.d.ts +422 -5
  6. package/dist/flows/index.js +2502 -678
  7. package/dist/index.d.ts +921 -17
  8. package/dist/index.js +3677 -678
  9. package/dist/preview/index.d.ts +62 -105
  10. package/dist/preview/index.js +162 -284
  11. package/dist/styles.css +893 -0
  12. package/package.json +9 -8
  13. package/src/AudioPlayer.tsx +8 -0
  14. package/src/AudioRecorderButton.test.tsx +30 -0
  15. package/src/AudioRecorderButton.tsx +248 -0
  16. package/src/AudioTranscription.test.tsx +115 -0
  17. package/src/AudioTranscription.tsx +252 -0
  18. package/src/Avatar.tsx +1 -1
  19. package/src/ConversationContextPanel.tsx +218 -44
  20. package/src/ConversationDocumentsPanel.tsx +11 -6
  21. package/src/ConversationHeader.test.tsx +66 -0
  22. package/src/ConversationHeader.tsx +147 -47
  23. package/src/ConversationListItem.tsx +8 -6
  24. package/src/ConversationLocalesProvider.tsx +28 -0
  25. package/src/ConversationRow.tsx +53 -7
  26. package/src/DarkModeToggle.test.tsx +76 -0
  27. package/src/DarkModeToggle.tsx +92 -0
  28. package/src/DocumentsLibrary.tsx +67 -7
  29. package/src/EmojiPicker.tsx +2 -1
  30. package/src/InteractiveMessage.tsx +3 -0
  31. package/src/Lightbox.tsx +1 -1
  32. package/src/MediaRenderer.tsx +88 -15
  33. package/src/MessageBubble.test.tsx +41 -0
  34. package/src/MessageBubble.tsx +47 -5
  35. package/src/MessageComposer.test.tsx +35 -0
  36. package/src/MessageComposer.tsx +122 -17
  37. package/src/MessageText.tsx +2 -1
  38. package/src/MessageTimestamp.tsx +2 -1
  39. package/src/RichMessageComposer.test.tsx +113 -0
  40. package/src/RichMessageComposer.tsx +551 -0
  41. package/src/SimpleEmojiPicker.tsx +5 -3
  42. package/src/StatusTicks.tsx +1 -1
  43. package/src/Toast.tsx +4 -0
  44. package/src/Tooltip.test.ts +42 -0
  45. package/src/Tooltip.tsx +167 -0
  46. package/src/Wallpaper.test.tsx +21 -0
  47. package/src/Wallpaper.tsx +67 -7
  48. package/src/WhatsAppMessageEditor.tsx +10 -7
  49. package/src/WindowExpiredNotice.tsx +12 -4
  50. package/src/{preview/audioRecorderFormat.test.ts → audioRecorderFormat.test.ts} +1 -1
  51. package/src/buildOutput.test.ts +79 -0
  52. package/src/composer.constant.ts +33 -0
  53. package/src/conversationTranscript.test.ts +57 -0
  54. package/src/conversationTranscript.ts +29 -4
  55. package/src/conversationWindow.ts +7 -5
  56. package/src/documentTypeLabel.test.ts +57 -0
  57. package/src/documents/DocumentsWorkspace.tsx +550 -0
  58. package/src/documents/index.ts +8 -0
  59. package/src/documents/labels.ts +92 -0
  60. package/src/flows/FlowConnectionEdge.tsx +104 -0
  61. package/src/flows/FlowGroupHeader.tsx +12 -2
  62. package/src/flows/FlowLegend.tsx +125 -0
  63. package/src/flows/FlowMapCanvas.tsx +15 -12
  64. package/src/flows/FlowMapNode.tsx +4 -1
  65. package/src/flows/FlowNodeCard.tsx +219 -34
  66. package/src/flows/FlowNodePanel.tsx +153 -39
  67. package/src/flows/FlowPalette.tsx +156 -70
  68. package/src/flows/FlowPortalNode.tsx +1 -1
  69. package/src/flows/FlowWhatsAppPreview.tsx +14 -3
  70. package/src/flows/FlowsWorkspace.tsx +1255 -0
  71. package/src/flows/flowCanvasModel.test.ts +456 -0
  72. package/src/flows/flowCanvasModel.ts +378 -0
  73. package/src/flows/flowEditorOps.test.ts +276 -0
  74. package/src/flows/flowEditorOps.ts +202 -0
  75. package/src/flows/flowGraph.ts +78 -53
  76. package/src/flows/flowMenuPlacement.test.ts +130 -0
  77. package/src/flows/flowMenuPlacement.ts +86 -0
  78. package/src/flows/index.ts +51 -2
  79. package/src/flows/labels.ts +180 -0
  80. package/src/flows/workspaceContract.test.ts +126 -0
  81. package/src/hooks/useContainerWidth.ts +35 -0
  82. package/src/hooks/useConversationRealtime.ts +10 -8
  83. package/src/hooks/useScrollToLatestMessage.ts +127 -0
  84. package/src/hooks/useUrlFilterState.ts +107 -0
  85. package/src/icon.constant.ts +12 -0
  86. package/src/index.ts +100 -0
  87. package/src/lib/composer-formatting.test.ts +78 -0
  88. package/src/lib/composer-formatting.ts +145 -0
  89. package/src/lib/whatsapp-formatting.test.tsx +37 -0
  90. package/src/lib/whatsapp-formatting.tsx +28 -3
  91. package/src/listing/index.tsx +202 -0
  92. package/src/pagination.constant.ts +10 -0
  93. package/src/preview/ConversationPreview.tsx +84 -45
  94. package/src/preview/ConversationSimulatorClient.ts +143 -0
  95. package/src/preview/ConversationSimulatorPanel.test.tsx +55 -0
  96. package/src/preview/ConversationSimulatorPanel.tsx +131 -0
  97. package/src/preview/createPreviewBridgeClient.test.ts +92 -0
  98. package/src/preview/createPreviewBridgeClient.ts +124 -0
  99. package/src/preview/createPreviewMediaUploader.ts +82 -0
  100. package/src/preview/createPreviewWebhookClient.test.ts +96 -0
  101. package/src/preview/createPreviewWebhookClient.ts +99 -3
  102. package/src/preview/index.ts +36 -2
  103. package/src/preview/previewMediaUploader.test.ts +61 -0
  104. package/src/providers/ConversationsProvider.tsx +8 -6
  105. package/src/providers/types.ts +59 -2
  106. package/src/quickReply.test.ts +58 -0
  107. package/src/replyLatency.test.ts +71 -0
  108. package/src/replyLatency.ts +57 -0
  109. package/src/settings/MessagesWorkspace.tsx +571 -0
  110. package/src/settings/TopicsForm.tsx +2 -0
  111. package/src/settings/TranscriptionSettingsForm.test.tsx +81 -0
  112. package/src/settings/TranscriptionSettingsForm.tsx +190 -0
  113. package/src/settings/WelcomeFarewellForm.tsx +1 -0
  114. package/src/settings/WhatsAppCreateTemplateForm.tsx +1 -0
  115. package/src/settings/WhatsAppTemplateSettingsForm.tsx +5 -2
  116. package/src/settings/WhatsAppTemplatesSettings.test.tsx +61 -0
  117. package/src/settings/WhatsAppTemplatesSettings.tsx +22 -2
  118. package/src/styles.css +858 -0
  119. package/src/theme.ts +13 -0
  120. package/src/types.ts +26 -0
  121. package/src/workspace/BulkTemplateModal.tsx +132 -0
  122. package/src/workspace/ConversationPane.tsx +432 -0
  123. package/src/workspace/ConversationsInboxList.tsx +194 -0
  124. package/src/workspace/ConversationsWorkspace.tsx +423 -0
  125. package/src/workspace/index.ts +17 -0
  126. package/src/workspace/labels.test.ts +17 -0
  127. package/src/workspace/labels.ts +85 -0
  128. package/src/workspace/useConversationsInbox.ts +332 -0
  129. package/dist/chunk-73MW5HNT.js +0 -1717
  130. package/dist/chunk-NV2RZ5KT.js +0 -56
  131. package/dist/types-B5C1DLu1.d.ts +0 -365
  132. package/src/preview/AudioRecorderButton.tsx +0 -117
@@ -1,13 +1,23 @@
1
1
  import {
2
+ AudioRecorderButton,
2
3
  ConversationDocumentsPanel,
4
+ ConversationPreview,
5
+ ConversationSimulatorPanel,
3
6
  ConversationWallpaper,
4
7
  ConversationsProvider,
5
- DateDivider,
8
+ DEFAULT_AUDIO_RECORDER_BUTTON_LABELS,
9
+ DEFAULT_CONVERSATION_SIMULATOR_PANEL_LABELS,
6
10
  DocumentsLibrary,
7
11
  MessageBubble,
8
- MessageComposer
9
- } from "../chunk-73MW5HNT.js";
10
- import "../chunk-OGRRHQQW.js";
12
+ SIMULATOR_FILE_MEDIA_KINDS,
13
+ acceptsMediaKind,
14
+ isConversationSimulatorClient,
15
+ mediaKindOf,
16
+ mediaTypeOf,
17
+ simulatorPanelLabelsOf,
18
+ toConversationSimulatorClient
19
+ } from "../chunk-BJNRLLDO.js";
20
+ import "../chunk-WCBDXZ3X.js";
11
21
 
12
22
  // src/preview/previewStore.ts
13
23
  var GLOBAL_CHANNEL = "global";
@@ -844,266 +854,6 @@ function createMockSSEProvider(params) {
844
854
  };
845
855
  }
846
856
 
847
- // 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";
852
- import { jsx } 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 RECORDING_FORMATS = [
860
- { mimeType: "audio/ogg;codecs=opus", uploadMimeType: "audio/ogg", extension: "ogg" },
861
- { mimeType: "audio/mp4", uploadMimeType: "audio/mp4", extension: "m4a" },
862
- { mimeType: "audio/webm", uploadMimeType: "audio/webm", extension: "webm" }
863
- ];
864
- function resolveRecordingFormat() {
865
- if (typeof MediaRecorder === "undefined") return void 0;
866
- if (typeof MediaRecorder.isTypeSupported !== "function") return RECORDING_FORMATS[0];
867
- return RECORDING_FORMATS.find((format) => MediaRecorder.isTypeSupported(format.mimeType));
868
- }
869
- function AudioRecorderButton({ onRecorded, onFailure, labels, disabled }) {
870
- const startLabel = labels?.start ?? DEFAULT_AUDIO_RECORDER_BUTTON_LABELS.start;
871
- const stopLabel = labels?.stop ?? DEFAULT_AUDIO_RECORDER_BUTTON_LABELS.stop;
872
- const [isRecording, setIsRecording] = useState(false);
873
- const recorderRef = useRef(null);
874
- const stop = useCallback(() => {
875
- recorderRef.current?.stop();
876
- }, []);
877
- const start = useCallback(async () => {
878
- const format = resolveRecordingFormat();
879
- if (!format || !navigator.mediaDevices?.getUserMedia) {
880
- onFailure?.(labels?.unsupported ?? DEFAULT_AUDIO_RECORDER_BUTTON_LABELS.unsupported);
881
- return;
882
- }
883
- try {
884
- const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
885
- const recorder = new MediaRecorder(stream, { mimeType: format.mimeType });
886
- const chunks = [];
887
- recorder.addEventListener("dataavailable", (event) => {
888
- if (event.data.size > 0) chunks.push(event.data);
889
- });
890
- recorder.addEventListener("stop", () => {
891
- stream.getTracks().forEach((track) => track.stop());
892
- setIsRecording(false);
893
- recorderRef.current = null;
894
- const blob = new Blob(chunks, { type: format.uploadMimeType });
895
- void onRecorded(
896
- new File([blob], `audio-${Date.now()}.${format.extension}`, { type: format.uploadMimeType })
897
- );
898
- });
899
- recorderRef.current = recorder;
900
- recorder.start();
901
- setIsRecording(true);
902
- } catch {
903
- onFailure?.(labels?.denied ?? DEFAULT_AUDIO_RECORDER_BUTTON_LABELS.denied);
904
- }
905
- }, [labels?.denied, labels?.unsupported, onFailure, onRecorded]);
906
- return /* @__PURE__ */ jsx(
907
- "button",
908
- {
909
- type: "button",
910
- disabled,
911
- onClick: () => isRecording ? stop() : void start(),
912
- title: isRecording ? stopLabel : startLabel,
913
- "aria-label": isRecording ? stopLabel : startLabel,
914
- "aria-pressed": isRecording,
915
- className: `flex h-9 w-9 items-center justify-center rounded-full transition-colors ${isRecording ? "bg-red-100 text-red-600 dark:bg-red-900/40" : "text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-700"}`,
916
- children: isRecording ? "\u25A0" : "\u{1F3A4}"
917
- }
918
- );
919
- }
920
-
921
- // src/preview/ConversationPreview.tsx
922
- import { jsx as jsx2, jsxs } from "react/jsx-runtime";
923
- function mediaTypeOf(mimeType) {
924
- if (mimeType.startsWith("image/")) return "image";
925
- if (mimeType.startsWith("video/")) return "video";
926
- if (mimeType.startsWith("audio/")) return "audio";
927
- return "document";
928
- }
929
- var GROUPING_WINDOW_MS = 5 * 60 * 1e3;
930
- var FOLLOW_UP_REFRESH_MS = [400, 1200, 3e3];
931
- function decorate(messages) {
932
- return messages.map((message, index) => {
933
- const previous = index > 0 ? messages[index - 1] : void 0;
934
- const currentTime = new Date(message.timestamp).getTime();
935
- const previousTime = previous ? new Date(previous.timestamp).getTime() : 0;
936
- return {
937
- message,
938
- isFirstInGroup: !previous || previous.sender !== message.sender || currentTime - previousTime > GROUPING_WINDOW_MS,
939
- showDateDivider: !previous || new Date(message.timestamp).toDateString() !== new Date(previous.timestamp).toDateString()
940
- };
941
- });
942
- }
943
- function statusOf(error) {
944
- if (typeof error !== "object" || error === null) return void 0;
945
- const candidate = error;
946
- const value = candidate.status ?? candidate.statusCode;
947
- return typeof value === "number" ? value : void 0;
948
- }
949
- function isNotFound(error) {
950
- return statusOf(error) === 404;
951
- }
952
- function describeLoadFailure(error) {
953
- const status = statusOf(error);
954
- if (status === 401 || status === 403) {
955
- return "Sem sess\xE3o de administrador nesta aba: a mensagem \xE9 entregue no webhook, mas o transcript n\xE3o pode ser lido. Entre no painel nesta mesma aba e reabra o simulador.";
956
- }
957
- if (error instanceof Error && error.message) return `N\xE3o foi poss\xEDvel ler o transcript: ${error.message}`;
958
- return "N\xE3o foi poss\xEDvel ler o transcript da conversa.";
959
- }
960
- function ConversationPreview({
961
- client,
962
- sse,
963
- conversationId,
964
- loadMessages,
965
- placeholder,
966
- pollIntervalMs,
967
- uploadMedia
968
- }) {
969
- const [messages, setMessages] = useState2([]);
970
- const [failure, setFailure] = useState2(void 0);
971
- const [loadFailure, setLoadFailure] = useState2(void 0);
972
- const [pendingLocal, setPendingLocal] = useState2([]);
973
- const loadMessagesRef = useRef2(loadMessages);
974
- const bottomRef = useRef2(null);
975
- loadMessagesRef.current = loadMessages;
976
- const refresh = useCallback2(async () => {
977
- try {
978
- const loaded = await loadMessagesRef.current(conversationId);
979
- setMessages(loaded);
980
- setLoadFailure(void 0);
981
- if (loaded.length > 0) setPendingLocal([]);
982
- } catch (error) {
983
- if (isNotFound(error)) {
984
- setMessages([]);
985
- setLoadFailure(void 0);
986
- return;
987
- }
988
- setLoadFailure(describeLoadFailure(error));
989
- }
990
- }, [conversationId]);
991
- useEffect(() => {
992
- void refresh();
993
- }, [refresh]);
994
- useEffect(() => {
995
- const source = sse.connectConversationStream(conversationId);
996
- const handler = () => {
997
- void refresh();
998
- };
999
- source.addEventListener("message", handler);
1000
- return () => {
1001
- source.removeEventListener("message", handler);
1002
- source.close();
1003
- };
1004
- }, [sse, conversationId, refresh]);
1005
- useEffect(() => {
1006
- if (!pollIntervalMs) return;
1007
- const timer = setInterval(() => void refresh(), pollIntervalMs);
1008
- return () => clearInterval(timer);
1009
- }, [pollIntervalMs, refresh]);
1010
- useEffect(() => {
1011
- bottomRef.current?.scrollIntoView({ behavior: "smooth" });
1012
- }, [messages]);
1013
- const rendered = useMemo(() => decorate([...messages, ...pendingLocal]), [messages, pendingLocal]);
1014
- async function refreshWithFollowUps() {
1015
- await refresh();
1016
- for (const atraso of FOLLOW_UP_REFRESH_MS) {
1017
- setTimeout(() => void refresh(), atraso);
1018
- }
1019
- }
1020
- async function handleSend(text) {
1021
- setFailure(void 0);
1022
- try {
1023
- await client.sendText(text);
1024
- setPendingLocal((current) => [
1025
- ...current,
1026
- {
1027
- id: `local-${current.length}-${text.length}`,
1028
- type: "text",
1029
- content: text,
1030
- // Do ponto de vista do servidor, mensagem do cliente é inbound — é assim que ela aparece
1031
- // como "minha" nesta visão.
1032
- direction: "inbound",
1033
- sender: "customer",
1034
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1035
- status: "sent"
1036
- }
1037
- ]);
1038
- await refreshWithFollowUps();
1039
- } catch (error) {
1040
- setFailure(error instanceof Error ? error.message : "Falha ao entregar a mensagem no webhook.");
1041
- }
1042
- }
1043
- async function handleInteractiveSelect(selection) {
1044
- setFailure(void 0);
1045
- const reply = { id: selection.option.id, title: selection.option.title };
1046
- try {
1047
- await (selection.kind === "button" ? client.sendButtonReply(reply) : client.sendListReply(reply));
1048
- await refreshWithFollowUps();
1049
- } catch (error) {
1050
- setFailure(error instanceof Error ? error.message : "Falha ao entregar a resposta no webhook.");
1051
- }
1052
- }
1053
- async function handleAttach(file) {
1054
- if (!uploadMedia) return;
1055
- setFailure(void 0);
1056
- try {
1057
- const uploaded = await uploadMedia(file);
1058
- await client.sendMedia({
1059
- mediaType: mediaTypeOf(uploaded.mimeType ?? file.type),
1060
- mediaId: uploaded.mediaId,
1061
- mimeType: uploaded.mimeType ?? file.type,
1062
- filename: uploaded.filename ?? file.name
1063
- });
1064
- await refreshWithFollowUps();
1065
- } catch (error) {
1066
- setFailure(error instanceof Error ? error.message : "Falha ao enviar o arquivo.");
1067
- }
1068
- }
1069
- return /* @__PURE__ */ jsxs("div", { className: "flex h-full min-h-0 flex-col", children: [
1070
- /* @__PURE__ */ jsxs(ConversationWallpaper, { className: "flex-1 min-h-0 overflow-y-auto px-4 py-3", children: [
1071
- rendered.map(({ message, isFirstInGroup, showDateDivider }) => /* @__PURE__ */ jsxs("div", { children: [
1072
- showDateDivider ? /* @__PURE__ */ jsx2(DateDivider, { iso: message.timestamp }) : null,
1073
- /* @__PURE__ */ jsx2(
1074
- MessageBubble,
1075
- {
1076
- message,
1077
- isMine: message.direction === "inbound",
1078
- isFirstInGroup,
1079
- onInteractiveSelect: message.direction === "outbound" ? (selection) => void handleInteractiveSelect(selection) : void 0
1080
- }
1081
- )
1082
- ] }, message.id)),
1083
- /* @__PURE__ */ jsx2("div", { ref: bottomRef })
1084
- ] }),
1085
- failure ? /* @__PURE__ */ jsx2("p", { role: "alert", className: "px-4 py-2 text-sm text-red-600 dark:text-red-400", children: failure }) : null,
1086
- loadFailure ? /* @__PURE__ */ jsx2("p", { role: "status", className: "px-4 py-2 text-sm text-amber-700 dark:text-amber-400", children: loadFailure }) : null,
1087
- /* @__PURE__ */ jsxs("div", { className: "flex items-end gap-1", children: [
1088
- /* @__PURE__ */ jsx2("div", { className: "min-w-0 flex-1", children: /* @__PURE__ */ jsx2(
1089
- MessageComposer,
1090
- {
1091
- onSend: (text) => void handleSend(text),
1092
- onAttach: uploadMedia ? (file) => void handleAttach(file) : void 0,
1093
- placeholder: placeholder ?? "Escreva como o cliente\u2026"
1094
- }
1095
- ) }),
1096
- uploadMedia ? /* @__PURE__ */ jsx2(
1097
- AudioRecorderButton,
1098
- {
1099
- onRecorded: (file) => void handleAttach(file),
1100
- onFailure: (message) => setFailure(message)
1101
- }
1102
- ) : null
1103
- ] })
1104
- ] });
1105
- }
1106
-
1107
857
  // src/preview/createPreviewWebhookClient.ts
1108
858
  import {
1109
859
  buildInboundAudioPayload,
@@ -1112,6 +862,38 @@ import {
1112
862
  buildInboundTextPayload,
1113
863
  serializeWebhookPayload
1114
864
  } from "@adatechnology/meta-whatsapp-contracts/testing";
865
+
866
+ // src/preview/createPreviewMediaUploader.ts
867
+ import { PREVIEW_MEDIA_ID_PREFIX } from "@adatechnology/meta-whatsapp-contracts";
868
+ import { toPreviewMediaId } from "@adatechnology/meta-whatsapp-contracts";
869
+ var CHUNK_SIZE = 8192;
870
+ async function fileToBase64(file) {
871
+ const bytes = new Uint8Array(await file.arrayBuffer());
872
+ let binary = "";
873
+ for (let offset = 0; offset < bytes.length; offset += CHUNK_SIZE) {
874
+ binary += String.fromCharCode(...bytes.subarray(offset, offset + CHUNK_SIZE));
875
+ }
876
+ return btoa(binary);
877
+ }
878
+ function createPreviewMediaUploader(params) {
879
+ const fallbackMimeType = params.fallbackMimeType ?? "audio/ogg";
880
+ const fallbackFilename = params.fallbackFilename ?? "audio.ogg";
881
+ return async function uploadPreviewMedia(file) {
882
+ const mimeType = file.type || fallbackMimeType;
883
+ const filename = file.name || fallbackFilename;
884
+ const { uploadId } = await params.upload({ base64: await fileToBase64(file), mimeType, filename });
885
+ return { mediaId: toPreviewMediaId(uploadId), mimeType, filename };
886
+ };
887
+ }
888
+
889
+ // src/preview/createPreviewWebhookClient.ts
890
+ var PreviewMediaUploadRejectedError = class extends Error {
891
+ constructor(status) {
892
+ super(`A rota de m\xEDdia do simulador recusou o upload (HTTP ${status}).`);
893
+ this.status = status;
894
+ this.name = "PreviewMediaUploadRejectedError";
895
+ }
896
+ };
1115
897
  var PreviewInProductionError = class extends Error {
1116
898
  constructor() {
1117
899
  super("O preview de conversa carrega um app secret e n\xE3o pode ser montado em produ\xE7\xE3o.");
@@ -1141,6 +923,30 @@ async function signPreviewPayload(params) {
1141
923
  return `sha256=${[...new Uint8Array(signature)].map((byte) => byte.toString(16).padStart(2, "0")).join("")}`;
1142
924
  }
1143
925
  var signWithWebCrypto = signPreviewPayload;
926
+ var DEFAULT_MEDIA_UPLOAD_PATH = "/v1/preview/media";
927
+ function defaultMediaUploadUrl(webhookUrl) {
928
+ try {
929
+ return new URL(DEFAULT_MEDIA_UPLOAD_PATH, webhookUrl).toString();
930
+ } catch {
931
+ return DEFAULT_MEDIA_UPLOAD_PATH;
932
+ }
933
+ }
934
+ function createPreviewMediaPoster(params) {
935
+ return createPreviewMediaUploader({
936
+ upload: async (request) => {
937
+ const performRequest = params.fetchImplementation ?? fetch;
938
+ const extraHeaders = await params.headers?.(request.mimeType) ?? {};
939
+ const response = await performRequest(params.url, {
940
+ method: "POST",
941
+ headers: { "content-type": "application/json", ...extraHeaders },
942
+ body: JSON.stringify(request)
943
+ });
944
+ if (!response.ok) throw new PreviewMediaUploadRejectedError(response.status);
945
+ const body = await response.json();
946
+ return { uploadId: body.data.uploadId };
947
+ }
948
+ });
949
+ }
1144
950
  function createPreviewWebhookClient(params) {
1145
951
  const sendPayload = async (payload) => {
1146
952
  const rawBody = serializeWebhookPayload(payload);
@@ -1153,13 +959,70 @@ function createPreviewWebhookClient(params) {
1153
959
  });
1154
960
  if (!response.ok) throw new PreviewWebhookRejectedError(response.status);
1155
961
  };
962
+ const uploadMedia = createPreviewMediaPoster({
963
+ url: params.mediaUploadUrl ?? defaultMediaUploadUrl(params.webhookUrl),
964
+ headers: async (mimeType) => ({
965
+ "x-preview-signature": await signWithWebCrypto({ rawBody: mimeType, appSecret: params.appSecret })
966
+ }),
967
+ ...params.fetchImplementation ? { fetchImplementation: params.fetchImplementation } : {}
968
+ });
1156
969
  const envelope = { from: params.from, phoneNumberId: params.phoneNumberId };
1157
970
  return {
1158
971
  sendText: (text) => sendPayload(buildInboundTextPayload({ ...envelope, text })),
1159
972
  sendButtonReply: (reply) => sendPayload(buildInboundInteractivePayload({ ...envelope, buttonReply: reply })),
1160
973
  sendListReply: (reply) => sendPayload(buildInboundInteractivePayload({ ...envelope, listReply: reply })),
1161
974
  sendAudio: (mediaId) => sendPayload(buildInboundAudioPayload({ ...envelope, mediaId })),
1162
- sendMedia: (media) => sendPayload(buildInboundMediaPayload({ ...envelope, ...media }))
975
+ sendMedia: (media) => sendPayload(buildInboundMediaPayload({ ...envelope, ...media })),
976
+ uploadMedia
977
+ };
978
+ }
979
+
980
+ // src/preview/createPreviewBridgeClient.ts
981
+ var PreviewBridgeRejectedError = class extends Error {
982
+ constructor(status) {
983
+ super(`A rota de preview do host recusou a entrega (HTTP ${status}).`);
984
+ this.status = status;
985
+ this.name = "PreviewBridgeRejectedError";
986
+ }
987
+ };
988
+ function buildFetchSender(params) {
989
+ const endpointUrl = params.endpointUrl;
990
+ if (!endpointUrl) {
991
+ throw new Error("createPreviewBridgeClient exige `sendCommand` ou `endpointUrl`.");
992
+ }
993
+ return async (command) => {
994
+ const performRequest = params.fetchImplementation ?? fetch;
995
+ const response = await performRequest(endpointUrl, {
996
+ method: "POST",
997
+ // `credentials` fica com o host via `headers`/`fetchImplementation`: sessão por cookie e por
998
+ // bearer não convivem numa escolha default sem quebrar um dos dois.
999
+ headers: { "content-type": "application/json", ...params.headers },
1000
+ body: JSON.stringify(command)
1001
+ });
1002
+ if (!response.ok) throw new PreviewBridgeRejectedError(response.status);
1003
+ };
1004
+ }
1005
+ function resolveBridgeUpload(params) {
1006
+ if (params.uploadMedia) return params.uploadMedia;
1007
+ const url = params.mediaUploadUrl ?? (params.endpointUrl ? defaultMediaUploadUrl(params.endpointUrl) : void 0);
1008
+ if (!url) return void 0;
1009
+ return createPreviewMediaPoster({
1010
+ url,
1011
+ ...params.headers ? { headers: async () => params.headers ?? {} } : {},
1012
+ ...params.fetchImplementation ? { fetchImplementation: params.fetchImplementation } : {}
1013
+ });
1014
+ }
1015
+ function createPreviewBridgeClient(params) {
1016
+ const send = params.sendCommand ?? buildFetchSender(params);
1017
+ const from = params.from;
1018
+ const uploadMedia = resolveBridgeUpload(params);
1019
+ return {
1020
+ sendText: (text) => send({ kind: "text", from, text }),
1021
+ sendButtonReply: (reply) => send({ kind: "buttonReply", from, reply }),
1022
+ sendListReply: (reply) => send({ kind: "listReply", from, reply }),
1023
+ sendAudio: (mediaId) => send({ kind: "audio", from, mediaId }),
1024
+ sendMedia: (media) => send({ kind: "media", from, ...media }),
1025
+ ...uploadMedia ? { uploadMedia } : {}
1163
1026
  };
1164
1027
  }
1165
1028
 
@@ -1200,42 +1063,42 @@ function startPreviewScript(params) {
1200
1063
  }
1201
1064
 
1202
1065
  // src/preview/MediaTypesPreview.tsx
1203
- import { useMemo as useMemo2 } from "react";
1204
- import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
1066
+ import { useMemo } from "react";
1067
+ import { jsx, jsxs } from "react/jsx-runtime";
1205
1068
  var MEDIA_TYPES_CONVERSATION_ID = "5511944443333";
1206
1069
  function MediaTypesPreview({
1207
1070
  conversationId = MEDIA_TYPES_CONVERSATION_ID,
1208
1071
  className
1209
1072
  }) {
1210
- const store = useMemo2(
1073
+ const store = useMemo(
1211
1074
  () => createPreviewStore({ conversations: PREVIEW_CONVERSATIONS, messages: PREVIEW_MESSAGES }),
1212
1075
  []
1213
1076
  );
1214
- const api = useMemo2(() => createMockConversationsApi({ store }), [store]);
1215
- const sse = useMemo2(() => createMockSSEProvider({ store }), [store]);
1077
+ const api = useMemo(() => createMockConversationsApi({ store }), [store]);
1078
+ const sse = useMemo(() => createMockSSEProvider({ store }), [store]);
1216
1079
  const messages = PREVIEW_MESSAGES[conversationId] ?? [];
1217
1080
  const documents = PREVIEW_DOCUMENTS[conversationId] ?? [];
1218
1081
  const mimeTypes = [...new Set(documents.map((document) => document.mimeType))];
1219
- return /* @__PURE__ */ jsx3(ConversationsProvider, { api, sse, children: /* @__PURE__ */ jsxs2("div", { className, children: [
1220
- /* @__PURE__ */ jsxs2("header", { className: "border-b px-4 py-3 dark:border-gray-700", children: [
1221
- /* @__PURE__ */ jsx3("h1", { className: "text-lg font-semibold", children: "Teste manual de m\xEDdia" }),
1222
- /* @__PURE__ */ jsxs2("p", { className: "text-sm text-gray-500", children: [
1082
+ return /* @__PURE__ */ jsx(ConversationsProvider, { api, sse, children: /* @__PURE__ */ jsxs("div", { className, children: [
1083
+ /* @__PURE__ */ jsxs("header", { className: "border-b px-4 py-3 dark:border-gray-700", children: [
1084
+ /* @__PURE__ */ jsx("h1", { className: "text-lg font-semibold", children: "Teste manual de m\xEDdia" }),
1085
+ /* @__PURE__ */ jsxs("p", { className: "text-sm text-gray-500", children: [
1223
1086
  documents.length,
1224
1087
  " arquivos, ",
1225
1088
  mimeTypes.length,
1226
1089
  " 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."
1227
1090
  ] })
1228
1091
  ] }),
1229
- /* @__PURE__ */ jsxs2("div", { className: "grid gap-4 p-4 lg:grid-cols-2", children: [
1230
- /* @__PURE__ */ jsxs2("section", { className: "space-y-3", children: [
1231
- /* @__PURE__ */ jsx3("h2", { className: "text-sm font-semibold uppercase tracking-wide text-gray-500", children: "Biblioteca da empresa" }),
1232
- /* @__PURE__ */ jsx3(DocumentsLibrary, { perPage: documents.length || 20 })
1092
+ /* @__PURE__ */ jsxs("div", { className: "grid gap-4 p-4 lg:grid-cols-2", children: [
1093
+ /* @__PURE__ */ jsxs("section", { className: "space-y-3", children: [
1094
+ /* @__PURE__ */ jsx("h2", { className: "text-sm font-semibold uppercase tracking-wide text-gray-500", children: "Biblioteca da empresa" }),
1095
+ /* @__PURE__ */ jsx(DocumentsLibrary, { perPage: documents.length || 20 })
1233
1096
  ] }),
1234
- /* @__PURE__ */ jsxs2("section", { className: "space-y-3", children: [
1235
- /* @__PURE__ */ jsx3("h2", { className: "text-sm font-semibold uppercase tracking-wide text-gray-500", children: "Painel da conversa" }),
1236
- /* @__PURE__ */ jsx3(ConversationDocumentsPanel, { conversationId, open: true, perPage: documents.length || 20 }),
1237
- /* @__PURE__ */ jsx3("h2", { className: "text-sm font-semibold uppercase tracking-wide text-gray-500", children: "Bolhas na thread" }),
1238
- /* @__PURE__ */ jsx3(ConversationWallpaper, { className: "max-h-[70vh] overflow-y-auto rounded-lg px-3 py-2", children: messages.map((message, index) => /* @__PURE__ */ jsx3(
1097
+ /* @__PURE__ */ jsxs("section", { className: "space-y-3", children: [
1098
+ /* @__PURE__ */ jsx("h2", { className: "text-sm font-semibold uppercase tracking-wide text-gray-500", children: "Painel da conversa" }),
1099
+ /* @__PURE__ */ jsx(ConversationDocumentsPanel, { conversationId, open: true, perPage: documents.length || 20 }),
1100
+ /* @__PURE__ */ jsx("h2", { className: "text-sm font-semibold uppercase tracking-wide text-gray-500", children: "Bolhas na thread" }),
1101
+ /* @__PURE__ */ jsx(ConversationWallpaper, { className: "max-h-[70vh] overflow-y-auto rounded-lg px-3 py-2", children: messages.map((message, index) => /* @__PURE__ */ jsx(
1239
1102
  MessageBubble,
1240
1103
  {
1241
1104
  message,
@@ -1251,7 +1114,10 @@ function MediaTypesPreview({
1251
1114
  export {
1252
1115
  AudioRecorderButton,
1253
1116
  ConversationPreview,
1117
+ ConversationSimulatorPanel,
1254
1118
  DEFAULT_AUDIO_RECORDER_BUTTON_LABELS,
1119
+ DEFAULT_CONVERSATION_SIMULATOR_PANEL_LABELS,
1120
+ DEFAULT_MEDIA_UPLOAD_PATH,
1255
1121
  DEFAULT_PREVIEW_SCRIPT,
1256
1122
  GLOBAL_CHANNEL,
1257
1123
  MEDIA_TYPES_CONVERSATION_ID,
@@ -1259,21 +1125,33 @@ export {
1259
1125
  PREVIEW_CONVERSATIONS,
1260
1126
  PREVIEW_DOCUMENTS,
1261
1127
  PREVIEW_FILE_SAMPLES,
1128
+ PREVIEW_MEDIA_ID_PREFIX,
1262
1129
  PREVIEW_MESSAGES,
1130
+ PreviewBridgeRejectedError,
1263
1131
  PreviewInProductionError,
1132
+ PreviewMediaUploadRejectedError,
1264
1133
  PreviewWebhookRejectedError,
1134
+ SIMULATOR_FILE_MEDIA_KINDS,
1135
+ acceptsMediaKind,
1265
1136
  assertPreviewEnvironment,
1266
1137
  conversationChannel,
1267
1138
  createMockConversationsApi,
1268
1139
  createMockEventSource,
1269
1140
  createMockSSEProvider,
1141
+ createPreviewBridgeClient,
1142
+ createPreviewMediaPoster,
1270
1143
  createPreviewMediaResolver,
1144
+ createPreviewMediaUploader,
1271
1145
  createPreviewStore,
1272
1146
  createPreviewWebhookClient,
1147
+ isConversationSimulatorClient,
1148
+ mediaKindOf,
1273
1149
  mediaTypeOf,
1274
1150
  previewFileBase64,
1275
1151
  previewFileUrl,
1276
1152
  resolvePreviewFileSample,
1277
1153
  signPreviewPayload,
1278
- startPreviewScript
1154
+ simulatorPanelLabelsOf,
1155
+ startPreviewScript,
1156
+ toConversationSimulatorClient
1279
1157
  };