@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
package/dist/preview/index.js
CHANGED
|
@@ -1,13 +1,23 @@
|
|
|
1
1
|
import {
|
|
2
|
+
AudioRecorderButton,
|
|
2
3
|
ConversationDocumentsPanel,
|
|
4
|
+
ConversationPreview,
|
|
5
|
+
ConversationSimulatorPanel,
|
|
3
6
|
ConversationWallpaper,
|
|
4
7
|
ConversationsProvider,
|
|
5
|
-
|
|
8
|
+
DEFAULT_AUDIO_RECORDER_BUTTON_LABELS,
|
|
9
|
+
DEFAULT_CONVERSATION_SIMULATOR_PANEL_LABELS,
|
|
6
10
|
DocumentsLibrary,
|
|
7
11
|
MessageBubble,
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
|
1204
|
-
import { jsx
|
|
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 =
|
|
1073
|
+
const store = useMemo(
|
|
1211
1074
|
() => createPreviewStore({ conversations: PREVIEW_CONVERSATIONS, messages: PREVIEW_MESSAGES }),
|
|
1212
1075
|
[]
|
|
1213
1076
|
);
|
|
1214
|
-
const api =
|
|
1215
|
-
const sse =
|
|
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__ */
|
|
1220
|
-
/* @__PURE__ */
|
|
1221
|
-
/* @__PURE__ */
|
|
1222
|
-
/* @__PURE__ */
|
|
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__ */
|
|
1230
|
-
/* @__PURE__ */
|
|
1231
|
-
/* @__PURE__ */
|
|
1232
|
-
/* @__PURE__ */
|
|
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__ */
|
|
1235
|
-
/* @__PURE__ */
|
|
1236
|
-
/* @__PURE__ */
|
|
1237
|
-
/* @__PURE__ */
|
|
1238
|
-
/* @__PURE__ */
|
|
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
|
-
|
|
1154
|
+
simulatorPanelLabelsOf,
|
|
1155
|
+
startPreviewScript,
|
|
1156
|
+
toConversationSimulatorClient
|
|
1279
1157
|
};
|