@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
@@ -0,0 +1,2708 @@
1
+ import {
2
+ parseWhatsAppFormatting,
3
+ useIsDarkTheme
4
+ } from "./chunk-WCBDXZ3X.js";
5
+
6
+ // src/ConversationLocalesProvider.tsx
7
+ import { createContext, useContext } from "react";
8
+ import { jsx } from "react/jsx-runtime";
9
+ var DEFAULT_LOCALES = {
10
+ bubble: {
11
+ customer: "Cliente",
12
+ bot: "Bot",
13
+ agent: "Atendente",
14
+ document: "documento",
15
+ media: "m\xEDdia",
16
+ templateLabel: "Template",
17
+ readAt: "Lida \xE0s ",
18
+ windowExpired: "Janela de 24h expirada",
19
+ viewImage: "Ver imagem",
20
+ listenAudio: "Ouvir \xE1udio",
21
+ viewVideo: "Ver v\xEDdeo",
22
+ moderationFlagged: "Linguagem ofensiva",
23
+ mediaLoading: "Carregando...",
24
+ mediaRetry: "Erro \u2014 tentar novamente",
25
+ mediaError: "Erro",
26
+ mediaUnavailable: "M\xEDdia indispon\xEDvel",
27
+ imageAlt: "Imagem",
28
+ untitledDocument: "Documento",
29
+ downloadFile: "Baixar"
30
+ },
31
+ transcription: {
32
+ label: "Transcri\xE7\xE3o",
33
+ copy: "Copiar",
34
+ copied: "Copiado!",
35
+ transcribe: "Transcrever \xE1udio",
36
+ transcribing: "Transcrevendo...",
37
+ retry: "Transcrever novamente",
38
+ failed: "Falha ao transcrever \u2014 tentar novamente",
39
+ empty: "Sem fala detectada",
40
+ unsupported: "Formato de \xE1udio n\xE3o suportado para transcri\xE7\xE3o",
41
+ showMore: "ver transcri\xE7\xE3o completa",
42
+ showLess: "ver menos"
43
+ },
44
+ selection: {
45
+ select: "Selecionar"
46
+ },
47
+ dateDivider: {
48
+ today: "Hoje",
49
+ yesterday: "Ontem"
50
+ }
51
+ };
52
+ var ConversationLocalesContext = createContext(DEFAULT_LOCALES);
53
+ function ConversationLocalesProvider({ children, locales }) {
54
+ const merged = {
55
+ bubble: { ...DEFAULT_LOCALES.bubble, ...locales?.bubble },
56
+ transcription: { ...DEFAULT_LOCALES.transcription, ...locales?.transcription },
57
+ selection: { ...DEFAULT_LOCALES.selection, ...locales?.selection },
58
+ dateDivider: { ...DEFAULT_LOCALES.dateDivider, ...locales?.dateDivider }
59
+ };
60
+ return /* @__PURE__ */ jsx(ConversationLocalesContext.Provider, { value: merged, children });
61
+ }
62
+ function useConversationLocales() {
63
+ return useContext(ConversationLocalesContext);
64
+ }
65
+
66
+ // src/StatusTicks.tsx
67
+ import { AlertTriangle } from "lucide-react";
68
+ import { jsx as jsx2, jsxs } from "react/jsx-runtime";
69
+ var STATUS_COLOR_CLASS = {
70
+ sent: "text-black/40 dark:text-white/40",
71
+ delivered: "text-black/40 dark:text-white/40",
72
+ read: "text-sky-500",
73
+ failed: "text-red-500"
74
+ };
75
+ function Ticks({ double }) {
76
+ return /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 20 12", width: "15", height: "9", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: [
77
+ double && /* @__PURE__ */ jsx2("path", { d: "M1 6.5L4.5 10L11 2", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round", strokeLinejoin: "round" }),
78
+ /* @__PURE__ */ jsx2(
79
+ "path",
80
+ {
81
+ d: double ? "M6 6.5L9.5 10L19 1" : "M1 6.5L5 10.5L14.5 1",
82
+ stroke: "currentColor",
83
+ strokeWidth: "1.6",
84
+ strokeLinecap: "round",
85
+ strokeLinejoin: "round"
86
+ }
87
+ )
88
+ ] });
89
+ }
90
+ function StatusTicks({ status, title }) {
91
+ const colorClass = STATUS_COLOR_CLASS[status] ?? STATUS_COLOR_CLASS.sent;
92
+ return /* @__PURE__ */ jsx2("span", { className: `cursor-help leading-none flex items-center ${colorClass}`, "data-cv-tooltip": title, children: status === "failed" ? /* @__PURE__ */ jsx2(AlertTriangle, { size: 11 }) : /* @__PURE__ */ jsx2(Ticks, { double: status !== "sent" }) });
93
+ }
94
+
95
+ // src/AudioPlayer.tsx
96
+ import { useEffect, useRef, useState } from "react";
97
+ import { Pause, Play } from "lucide-react";
98
+ import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
99
+ var BARS = Array.from({ length: 30 }, (_, i) => {
100
+ const heights = [3, 5, 8, 12, 7, 10, 14, 9, 6, 11, 15, 8, 4, 13, 7, 10, 6, 12, 9, 5, 14, 8, 11, 6, 13, 7, 10, 5, 9, 4];
101
+ return heights[i % heights.length];
102
+ });
103
+ var AUDIO_PLAYER_LABELS = {
104
+ play: "Reproduzir",
105
+ pause: "Pausar"
106
+ };
107
+ function fmt(sec) {
108
+ if (!isFinite(sec)) return "0:00";
109
+ const m = Math.floor(sec / 60);
110
+ const s = Math.floor(sec % 60);
111
+ return `${m}:${s.toString().padStart(2, "0")}`;
112
+ }
113
+ function AudioPlayer({ src, isMine = false }) {
114
+ const audioRef = useRef(null);
115
+ const [playing, setPlaying] = useState(false);
116
+ const [progress, setProgress] = useState(0);
117
+ const [duration, setDuration] = useState(0);
118
+ const [current, setCurrent] = useState(0);
119
+ useEffect(() => {
120
+ const audio = audioRef.current;
121
+ if (!audio) return;
122
+ const onTime = () => {
123
+ setCurrent(audio.currentTime);
124
+ setProgress(audio.duration ? audio.currentTime / audio.duration : 0);
125
+ };
126
+ const onMeta = () => setDuration(audio.duration);
127
+ const onEnd = () => {
128
+ setPlaying(false);
129
+ setProgress(0);
130
+ setCurrent(0);
131
+ };
132
+ audio.addEventListener("timeupdate", onTime);
133
+ audio.addEventListener("loadedmetadata", onMeta);
134
+ audio.addEventListener("ended", onEnd);
135
+ return () => {
136
+ audio.removeEventListener("timeupdate", onTime);
137
+ audio.removeEventListener("loadedmetadata", onMeta);
138
+ audio.removeEventListener("ended", onEnd);
139
+ };
140
+ }, []);
141
+ const toggle = () => {
142
+ const audio = audioRef.current;
143
+ if (!audio) return;
144
+ if (playing) {
145
+ audio.pause();
146
+ setPlaying(false);
147
+ } else {
148
+ audio.play();
149
+ setPlaying(true);
150
+ }
151
+ };
152
+ const seek = (e) => {
153
+ const audio = audioRef.current;
154
+ if (!audio || !audio.duration) return;
155
+ const rect = e.currentTarget.getBoundingClientRect();
156
+ const ratio = (e.clientX - rect.left) / rect.width;
157
+ audio.currentTime = ratio * audio.duration;
158
+ };
159
+ const activeBars = Math.round(progress * BARS.length);
160
+ const playBtn = isMine ? "bg-green-600 hover:bg-green-700 text-white" : "bg-gray-600 hover:bg-gray-700 text-white";
161
+ const activeBar = isMine ? "bg-green-700" : "bg-gray-700";
162
+ const inactiveBar = isMine ? "bg-green-300" : "bg-gray-300";
163
+ return /* @__PURE__ */ jsxs2("div", { className: "flex items-center gap-2 w-full", style: { minWidth: "200px", maxWidth: "260px" }, children: [
164
+ /* @__PURE__ */ jsx3("audio", { ref: audioRef, src, preload: "metadata" }),
165
+ /* @__PURE__ */ jsx3(
166
+ "button",
167
+ {
168
+ "data-cv-tooltip": playing ? AUDIO_PLAYER_LABELS.pause : AUDIO_PLAYER_LABELS.play,
169
+ "aria-label": playing ? AUDIO_PLAYER_LABELS.pause : AUDIO_PLAYER_LABELS.play,
170
+ onClick: toggle,
171
+ className: `flex-shrink-0 w-9 h-9 rounded-full flex items-center justify-center transition-colors ${playBtn}`,
172
+ children: playing ? /* @__PURE__ */ jsx3(Pause, { size: 16 }) : /* @__PURE__ */ jsx3(Play, { size: 16, className: "translate-x-0.5" })
173
+ }
174
+ ),
175
+ /* @__PURE__ */ jsxs2("div", { className: "flex-1 flex flex-col gap-1", children: [
176
+ /* @__PURE__ */ jsx3("div", { className: "flex items-end gap-0.5 h-8 cursor-pointer", onClick: seek, children: BARS.map((h, i) => /* @__PURE__ */ jsx3(
177
+ "div",
178
+ {
179
+ className: `flex-1 rounded-full transition-colors ${i < activeBars ? activeBar : inactiveBar}`,
180
+ style: { height: `${h * 2}px` }
181
+ },
182
+ i
183
+ )) }),
184
+ /* @__PURE__ */ jsx3("span", { className: "text-gray-400 tabular-nums text-xs", children: playing || current > 0 ? fmt(current) : fmt(duration) })
185
+ ] })
186
+ ] });
187
+ }
188
+
189
+ // src/AudioTranscription.tsx
190
+ import { useCallback, useState as useState2 } from "react";
191
+ import { Check, Copy, FileText, Loader2, RefreshCw } from "lucide-react";
192
+
193
+ // src/lib/cn.ts
194
+ import { clsx } from "clsx";
195
+ import { twMerge } from "tailwind-merge";
196
+ function cn(...inputs) {
197
+ return twMerge(clsx(inputs));
198
+ }
199
+
200
+ // src/AudioTranscription.tsx
201
+ import { Fragment, jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
202
+ var COPIED_FEEDBACK_MS = 2e3;
203
+ var COLLAPSE_ABOVE_CHARS = 320;
204
+ var COLLAPSED_LINE_CLAMP = 4;
205
+ function AudioTranscription({ transcription, onTranscribe, isMine = false }) {
206
+ const { transcription: locales } = useConversationLocales();
207
+ const [hasCopied, setHasCopied] = useState2(false);
208
+ const [isTranscribing, setIsTranscribing] = useState2(false);
209
+ const [hasRequestFailed, setHasRequestFailed] = useState2(false);
210
+ const [justTranscribed, setJustTranscribed] = useState2();
211
+ const [isExpanded, setIsExpanded] = useState2(false);
212
+ const effective = justTranscribed ?? transcription;
213
+ const text = effective?.text?.trim() ?? "";
214
+ const status = effective?.status;
215
+ const isLong = text.length > COLLAPSE_ABOVE_CHARS;
216
+ const isTruncated = isLong && !isExpanded;
217
+ const isDone = status === "done";
218
+ const hasText = isDone && text.length > 0;
219
+ const isSilent = isDone && text.length === 0;
220
+ const handleCopy = useCallback(async () => {
221
+ if (!text) return;
222
+ try {
223
+ await navigator.clipboard.writeText(text);
224
+ setHasCopied(true);
225
+ setTimeout(() => setHasCopied(false), COPIED_FEEDBACK_MS);
226
+ } catch {
227
+ }
228
+ }, [text]);
229
+ const handleTranscribe = useCallback(async () => {
230
+ if (!onTranscribe || isTranscribing) return;
231
+ setIsTranscribing(true);
232
+ setHasRequestFailed(false);
233
+ try {
234
+ const result = await onTranscribe();
235
+ if (result) setJustTranscribed(result);
236
+ } catch {
237
+ setHasRequestFailed(true);
238
+ } finally {
239
+ setIsTranscribing(false);
240
+ }
241
+ }, [onTranscribe, isTranscribing]);
242
+ const dividerClass = isMine ? "border-black/10 dark:border-white/10" : "border-black/10 dark:border-white/10";
243
+ if (!status && !onTranscribe) return null;
244
+ if (!status || status === "pending" || status === "failed") {
245
+ return /* @__PURE__ */ jsx4("div", { className: `mt-1.5 border-t pt-1.5 ${dividerClass}`, children: /* @__PURE__ */ jsx4(
246
+ TranscribeButton,
247
+ {
248
+ label: resolveActionLabel({ status, hasRequestFailed, isTranscribing, locales }),
249
+ isBusy: isTranscribing,
250
+ onClick: handleTranscribe,
251
+ isDisabled: !onTranscribe
252
+ }
253
+ ) });
254
+ }
255
+ if (status === "unsupported") {
256
+ return /* @__PURE__ */ jsx4("div", { className: `mt-1.5 border-t pt-1.5 ${dividerClass}`, children: /* @__PURE__ */ jsx4("p", { className: "text-xs italic text-gray-500 dark:text-gray-400", children: locales.unsupported }) });
257
+ }
258
+ return /* @__PURE__ */ jsxs3("div", { className: `mt-1.5 border-t pt-1.5 ${dividerClass}`, children: [
259
+ /* @__PURE__ */ jsxs3("div", { className: "mb-0.5 flex items-center gap-1.5", children: [
260
+ /* @__PURE__ */ jsx4(FileText, { size: 11, className: "flex-shrink-0 text-gray-500 dark:text-gray-400", "aria-hidden": true }),
261
+ /* @__PURE__ */ jsx4("span", { className: "text-[11px] font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400", children: locales.label }),
262
+ hasText && /* @__PURE__ */ jsx4(
263
+ "button",
264
+ {
265
+ onClick: handleCopy,
266
+ "data-cv-tooltip": locales.copy,
267
+ "aria-label": hasCopied ? locales.copied : locales.copy,
268
+ className: "ml-auto flex flex-shrink-0 items-center gap-1 rounded px-1.5 py-0.5 text-[11px] text-gray-500 transition-colors hover:bg-black/5 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/10 dark:hover:text-gray-200",
269
+ children: hasCopied ? /* @__PURE__ */ jsxs3(Fragment, { children: [
270
+ /* @__PURE__ */ jsx4(Check, { size: 11, "aria-hidden": true }),
271
+ /* @__PURE__ */ jsx4("span", { children: locales.copied })
272
+ ] }) : /* @__PURE__ */ jsxs3(Fragment, { children: [
273
+ /* @__PURE__ */ jsx4(Copy, { size: 11, "aria-hidden": true }),
274
+ /* @__PURE__ */ jsx4("span", { children: locales.copy })
275
+ ] })
276
+ }
277
+ )
278
+ ] }),
279
+ isSilent ? /* @__PURE__ */ jsx4("p", { className: "text-xs italic text-gray-500 dark:text-gray-400", children: locales.empty }) : /* @__PURE__ */ jsxs3(Fragment, { children: [
280
+ /* @__PURE__ */ jsx4(
281
+ "p",
282
+ {
283
+ className: cn(
284
+ "select-all whitespace-pre-wrap break-words text-[13px] leading-[18px] text-gray-700 dark:text-gray-200",
285
+ isTruncated && "overflow-hidden"
286
+ ),
287
+ style: isTruncated ? {
288
+ display: "-webkit-box",
289
+ WebkitBoxOrient: "vertical",
290
+ WebkitLineClamp: COLLAPSED_LINE_CLAMP
291
+ } : void 0,
292
+ children: text
293
+ }
294
+ ),
295
+ isLong && /* @__PURE__ */ jsx4(
296
+ "button",
297
+ {
298
+ "data-cv-tooltip": isExpanded ? locales.showLess : locales.showMore,
299
+ "aria-label": isExpanded ? locales.showLess : locales.showMore,
300
+ onClick: () => setIsExpanded((current) => !current),
301
+ className: "mt-1 text-[11px] font-medium text-gray-500 underline decoration-dotted hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200",
302
+ children: isExpanded ? locales.showLess : locales.showMore
303
+ }
304
+ )
305
+ ] }),
306
+ onTranscribe && /* @__PURE__ */ jsxs3(
307
+ "button",
308
+ {
309
+ "data-cv-tooltip": isTranscribing ? locales.transcribing : locales.retry,
310
+ "aria-label": isTranscribing ? locales.transcribing : locales.retry,
311
+ onClick: handleTranscribe,
312
+ disabled: isTranscribing,
313
+ className: "mt-1 flex items-center gap-1 text-[11px] text-gray-400 opacity-0 transition-opacity hover:text-gray-600 focus:opacity-100 group-hover:opacity-100 disabled:opacity-50 dark:text-gray-500 dark:hover:text-gray-300",
314
+ children: [
315
+ isTranscribing ? /* @__PURE__ */ jsx4(Loader2, { size: 10, className: "animate-spin", "aria-hidden": true }) : /* @__PURE__ */ jsx4(RefreshCw, { size: 10, "aria-hidden": true }),
316
+ /* @__PURE__ */ jsx4("span", { children: isTranscribing ? locales.transcribing : locales.retry })
317
+ ]
318
+ }
319
+ )
320
+ ] });
321
+ }
322
+ function resolveActionLabel(params) {
323
+ if (params.isTranscribing) return params.locales.transcribing;
324
+ if (params.hasRequestFailed) return params.locales.retry;
325
+ if (params.status === "pending") return params.locales.transcribing;
326
+ if (params.status === "failed") return params.locales.failed;
327
+ return params.locales.transcribe;
328
+ }
329
+ function TranscribeButton({
330
+ label,
331
+ isBusy,
332
+ isDisabled,
333
+ onClick
334
+ }) {
335
+ return /* @__PURE__ */ jsxs3(
336
+ "button",
337
+ {
338
+ "data-cv-tooltip": label,
339
+ "aria-label": label,
340
+ onClick,
341
+ disabled: isBusy || isDisabled,
342
+ className: "flex items-center gap-1.5 rounded px-1.5 py-0.5 text-[11px] text-gray-500 transition-colors hover:bg-black/5 hover:text-gray-700 disabled:opacity-60 dark:text-gray-400 dark:hover:bg-white/10 dark:hover:text-gray-200",
343
+ children: [
344
+ isBusy ? /* @__PURE__ */ jsx4(Loader2, { size: 11, className: "animate-spin", "aria-hidden": true }) : /* @__PURE__ */ jsx4(FileText, { size: 11, "aria-hidden": true }),
345
+ /* @__PURE__ */ jsx4("span", { children: label })
346
+ ]
347
+ }
348
+ );
349
+ }
350
+
351
+ // src/FileIcon.tsx
352
+ import {
353
+ FileArchive,
354
+ FileAudio,
355
+ FileImage,
356
+ FileSpreadsheet,
357
+ FileText as FileText2,
358
+ FileVideo,
359
+ File as FileGeneric,
360
+ Presentation
361
+ } from "lucide-react";
362
+ import { jsx as jsx5 } from "react/jsx-runtime";
363
+ var IMAGE_STYLE = { Icon: FileImage, colorClass: "text-violet-500" };
364
+ var VIDEO_STYLE = { Icon: FileVideo, colorClass: "text-fuchsia-500" };
365
+ var AUDIO_STYLE = { Icon: FileAudio, colorClass: "text-amber-500" };
366
+ var SHEET_STYLE = { Icon: FileSpreadsheet, colorClass: "text-green-600" };
367
+ var WORD_STYLE = { Icon: FileText2, colorClass: "text-blue-500" };
368
+ var SLIDES_STYLE = { Icon: Presentation, colorClass: "text-orange-600" };
369
+ var TEXT_STYLE = { Icon: FileText2, colorClass: "text-gray-500" };
370
+ var EXTENSION_STYLE = {
371
+ pdf: { Icon: FileText2, colorClass: "text-red-500" },
372
+ doc: WORD_STYLE,
373
+ docx: WORD_STYLE,
374
+ xls: SHEET_STYLE,
375
+ xlsx: SHEET_STYLE,
376
+ csv: SHEET_STYLE,
377
+ ppt: SLIDES_STYLE,
378
+ pptx: SLIDES_STYLE,
379
+ zip: { Icon: FileArchive, colorClass: "text-orange-500" },
380
+ txt: TEXT_STYLE,
381
+ plain: TEXT_STYLE,
382
+ image: IMAGE_STYLE,
383
+ jpg: IMAGE_STYLE,
384
+ jpeg: IMAGE_STYLE,
385
+ png: IMAGE_STYLE,
386
+ webp: IMAGE_STYLE,
387
+ gif: IMAGE_STYLE,
388
+ heic: IMAGE_STYLE,
389
+ video: VIDEO_STYLE,
390
+ mp4: VIDEO_STYLE,
391
+ "3gp": VIDEO_STYLE,
392
+ "3gpp": VIDEO_STYLE,
393
+ mov: VIDEO_STYLE,
394
+ webm: VIDEO_STYLE,
395
+ audio: AUDIO_STYLE,
396
+ mp3: AUDIO_STYLE,
397
+ mpeg: AUDIO_STYLE,
398
+ ogg: AUDIO_STYLE,
399
+ oga: AUDIO_STYLE,
400
+ opus: AUDIO_STYLE,
401
+ aac: AUDIO_STYLE,
402
+ amr: AUDIO_STYLE,
403
+ m4a: AUDIO_STYLE,
404
+ wav: AUDIO_STYLE
405
+ };
406
+ var MEDIA_FAMILIES = /* @__PURE__ */ new Set(["image", "video", "audio"]);
407
+ function resolveFileIconExtension(filename, mimeType) {
408
+ const fromFilename = filename?.split(".").pop()?.toLowerCase();
409
+ if (fromFilename && EXTENSION_STYLE[fromFilename]) return fromFilename;
410
+ const [family, subtype] = (mimeType ?? "").split(";")[0].toLowerCase().split("/");
411
+ if (family && MEDIA_FAMILIES.has(family)) return family;
412
+ return subtype ?? "";
413
+ }
414
+ function FileIcon({ filename, mimeType, size = 20, className }) {
415
+ const extension = resolveFileIconExtension(filename, mimeType);
416
+ const style = EXTENSION_STYLE[extension] ?? { Icon: FileGeneric, colorClass: "text-gray-500" };
417
+ const { Icon, colorClass } = style;
418
+ return /* @__PURE__ */ jsx5(Icon, { size, className: cn(colorClass, className) });
419
+ }
420
+
421
+ // src/lib/format.ts
422
+ function formatTimestamp(timestamp) {
423
+ try {
424
+ const date = new Date(timestamp);
425
+ const hours = date.getHours().toString().padStart(2, "0");
426
+ const minutes = date.getMinutes().toString().padStart(2, "0");
427
+ return `${hours}:${minutes}`;
428
+ } catch {
429
+ return timestamp;
430
+ }
431
+ }
432
+ function formatDateTime(iso) {
433
+ const d = new Date(iso);
434
+ return d.toLocaleString("pt-BR", { day: "2-digit", month: "2-digit", hour: "2-digit", minute: "2-digit" });
435
+ }
436
+ function isSameDay(a, b) {
437
+ return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
438
+ }
439
+ var FILE_SIZE_UNITS = ["B", "KB", "MB", "GB"];
440
+ function formatFileSize(bytes) {
441
+ if (!isFinite(bytes) || bytes < 0) return "";
442
+ if (bytes < 1) return "0 B";
443
+ const exponent = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), FILE_SIZE_UNITS.length - 1);
444
+ const value = bytes / 1024 ** exponent;
445
+ const formatted = exponent === 0 ? value.toString() : value.toFixed(value < 10 ? 1 : 0);
446
+ return `${formatted} ${FILE_SIZE_UNITS[exponent]}`;
447
+ }
448
+
449
+ // src/MediaRenderer.tsx
450
+ import { useState as useState3 } from "react";
451
+ import { jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
452
+ function documentTypeLabel(filename, mimeType) {
453
+ const extension = filename?.split(".").pop();
454
+ if (extension && extension.length <= 5 && extension !== filename) return extension.toUpperCase();
455
+ const subtype = mimeType?.split(";")[0]?.split("/")[1];
456
+ if (!subtype) return "FILE";
457
+ const compacto = subtype.split(".").pop() ?? subtype;
458
+ return compacto.slice(0, 12).toUpperCase();
459
+ }
460
+ function resolveMediaSource(message) {
461
+ if (message.mediaUrl) return message.mediaUrl;
462
+ if (message.base64) {
463
+ const prefix = message.mimeType ? `data:${message.mimeType};base64,` : "data:application/octet-stream;base64,";
464
+ return prefix + message.base64;
465
+ }
466
+ return null;
467
+ }
468
+ function hasLazyRef(message) {
469
+ return Boolean(message.uploadId || message.mediaId);
470
+ }
471
+ function useLazyMediaUrl(message, onResolveUrl) {
472
+ const [url, setUrl] = useState3(null);
473
+ const [loading, setLoading] = useState3(false);
474
+ const [error, setError] = useState3(false);
475
+ const load = async () => {
476
+ if (url || loading || !onResolveUrl) return;
477
+ setLoading(true);
478
+ setError(false);
479
+ try {
480
+ const resolved = await onResolveUrl(message);
481
+ if (resolved) setUrl(resolved);
482
+ else setError(true);
483
+ } catch {
484
+ setError(true);
485
+ } finally {
486
+ setLoading(false);
487
+ }
488
+ };
489
+ return { url, loading, error, load };
490
+ }
491
+ function MediaRenderer({
492
+ message,
493
+ onLightbox,
494
+ onResolveUrl,
495
+ onTranscribeAudio,
496
+ className
497
+ }) {
498
+ const { bubble } = useConversationLocales();
499
+ const eagerSrc = resolveMediaSource(message);
500
+ const lazy = useLazyMediaUrl(message, onResolveUrl);
501
+ const src = eagerSrc ?? lazy.url;
502
+ const canLazyLoad = !eagerSrc && hasLazyRef(message) && Boolean(onResolveUrl);
503
+ function LazyMediaButton({ icon, label }) {
504
+ return /* @__PURE__ */ jsxs4(
505
+ "button",
506
+ {
507
+ "data-cv-tooltip": label,
508
+ "aria-label": label,
509
+ onClick: lazy.load,
510
+ disabled: lazy.loading,
511
+ className: "flex min-w-[180px] items-center gap-2 rounded-lg bg-black/5 px-2 py-1.5 text-left transition-colors hover:bg-black/10 disabled:opacity-60 dark:bg-white/10 dark:hover:bg-white/15",
512
+ children: [
513
+ /* @__PURE__ */ jsx6("span", { className: "flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-full bg-gray-600 text-white", children: icon }),
514
+ /* @__PURE__ */ jsx6("span", { className: "truncate text-xs text-gray-600 dark:text-gray-300", children: label })
515
+ ]
516
+ }
517
+ );
518
+ }
519
+ switch (message.type) {
520
+ case "image":
521
+ case "sticker": {
522
+ if (!src && canLazyLoad) {
523
+ return /* @__PURE__ */ jsx6(
524
+ LazyMediaButton,
525
+ {
526
+ icon: /* @__PURE__ */ jsxs4("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
527
+ /* @__PURE__ */ jsx6("rect", { x: "3", y: "3", width: "18", height: "18", rx: "2", ry: "2" }),
528
+ /* @__PURE__ */ jsx6("circle", { cx: "8.5", cy: "8.5", r: "1.5" }),
529
+ /* @__PURE__ */ jsx6("polyline", { points: "21 15 16 10 5 21" })
530
+ ] }),
531
+ label: lazy.loading ? bubble.mediaLoading : lazy.error ? bubble.mediaRetry : bubble.viewImage
532
+ }
533
+ );
534
+ }
535
+ return /* @__PURE__ */ jsx6("div", { className: "min-w-[200px]", children: src ? /* @__PURE__ */ jsx6("img", { src, alt: message.caption ?? bubble.imageAlt, className: "w-full max-h-80 object-cover cursor-pointer hover:opacity-90 transition-opacity", onClick: () => onLightbox(src), loading: "lazy" }) : /* @__PURE__ */ jsx6("div", { className: "w-full h-40 bg-gray-200 flex items-center justify-center text-gray-400", children: /* @__PURE__ */ jsxs4("svg", { width: "32", height: "32", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.5", children: [
536
+ /* @__PURE__ */ jsx6("rect", { x: "3", y: "3", width: "18", height: "18", rx: "2", ry: "2" }),
537
+ /* @__PURE__ */ jsx6("circle", { cx: "8.5", cy: "8.5", r: "1.5" }),
538
+ /* @__PURE__ */ jsx6("polyline", { points: "21 15 16 10 5 21" })
539
+ ] }) }) });
540
+ }
541
+ case "video": {
542
+ if (!src && canLazyLoad) {
543
+ return /* @__PURE__ */ jsx6(
544
+ LazyMediaButton,
545
+ {
546
+ icon: /* @__PURE__ */ jsxs4("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
547
+ /* @__PURE__ */ jsx6("polygon", { points: "23 7 16 12 23 17 23 7" }),
548
+ /* @__PURE__ */ jsx6("rect", { x: "1", y: "5", width: "15", height: "14", rx: "2", ry: "2" })
549
+ ] }),
550
+ label: lazy.loading ? bubble.mediaLoading : lazy.error ? bubble.mediaRetry : bubble.viewVideo
551
+ }
552
+ );
553
+ }
554
+ return /* @__PURE__ */ jsx6("div", { className: "min-w-[200px]", children: src ? /* @__PURE__ */ jsx6("video", { src, className: "w-full max-h-80 rounded-lg", controls: true, preload: "metadata", children: /* @__PURE__ */ jsx6("track", { kind: "captions" }) }) : /* @__PURE__ */ jsx6("div", { className: "w-full h-32 bg-gray-200 rounded-lg flex items-center justify-center text-gray-400", children: /* @__PURE__ */ jsxs4("svg", { width: "32", height: "32", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.5", children: [
555
+ /* @__PURE__ */ jsx6("polygon", { points: "23 7 16 12 23 17 23 7" }),
556
+ /* @__PURE__ */ jsx6("rect", { x: "1", y: "5", width: "15", height: "14", rx: "2", ry: "2" })
557
+ ] }) }) });
558
+ }
559
+ case "audio": {
560
+ const transcriptionBlock = /* @__PURE__ */ jsx6(
561
+ AudioTranscription,
562
+ {
563
+ transcription: message.transcription,
564
+ isMine: message.direction === "outbound",
565
+ ...onTranscribeAudio ? { onTranscribe: onTranscribeAudio } : {}
566
+ }
567
+ );
568
+ if (!src && canLazyLoad) {
569
+ return /* @__PURE__ */ jsxs4("div", { className: "min-w-[200px]", children: [
570
+ /* @__PURE__ */ jsx6(
571
+ LazyMediaButton,
572
+ {
573
+ icon: /* @__PURE__ */ jsx6("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "currentColor", className: "translate-x-0.5", children: /* @__PURE__ */ jsx6("polygon", { points: "5 3 19 12 5 21 5 3" }) }),
574
+ label: lazy.loading ? bubble.mediaLoading : lazy.error ? bubble.mediaRetry : bubble.listenAudio
575
+ }
576
+ ),
577
+ transcriptionBlock
578
+ ] });
579
+ }
580
+ return /* @__PURE__ */ jsxs4("div", { className: "min-w-[200px]", children: [
581
+ src ? /* @__PURE__ */ jsx6(AudioPlayer, { src, isMine: message.direction === "outbound" }) : /* @__PURE__ */ jsx6("div", { className: "h-12 bg-gray-200 rounded-lg flex items-center justify-center text-gray-400 text-xs", children: bubble.mediaUnavailable }),
582
+ transcriptionBlock
583
+ ] });
584
+ }
585
+ case "document": {
586
+ const typeLabel = documentTypeLabel(message.filename, message.mimeType);
587
+ const sizeLabel = message.sizeBytes ? formatFileSize(message.sizeBytes) : null;
588
+ return /* @__PURE__ */ jsxs4("div", { className: cn("flex items-center gap-3 min-w-[200px]", className), children: [
589
+ /* @__PURE__ */ jsx6("div", { className: "w-10 h-10 bg-gray-200 rounded-lg flex items-center justify-center flex-shrink-0", children: /* @__PURE__ */ jsx6(FileIcon, { filename: message.filename, mimeType: message.mimeType }) }),
590
+ /* @__PURE__ */ jsxs4("div", { className: "flex-1 min-w-0", children: [
591
+ /* @__PURE__ */ jsx6("p", { className: "text-sm font-medium truncate", children: message.filename ?? bubble.untitledDocument }),
592
+ /* @__PURE__ */ jsx6("p", { className: "truncate text-xs text-gray-500", children: sizeLabel ? `${typeLabel} \xB7 ${sizeLabel}` : typeLabel })
593
+ ] }),
594
+ src ? /* @__PURE__ */ jsx6("a", { href: src, download: message.filename, className: "w-8 h-8 flex items-center justify-center rounded-full bg-gray-200 hover:bg-gray-300 flex-shrink-0 transition-colors", "aria-label": bubble.downloadFile, children: /* @__PURE__ */ jsxs4("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", className: "text-gray-600", children: [
595
+ /* @__PURE__ */ jsx6("path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" }),
596
+ /* @__PURE__ */ jsx6("polyline", { points: "7 10 12 15 17 10" }),
597
+ /* @__PURE__ */ jsx6("line", { x1: "12", y1: "15", x2: "12", y2: "3" })
598
+ ] }) }) : canLazyLoad ? /* @__PURE__ */ jsx6(
599
+ "button",
600
+ {
601
+ "data-cv-tooltip": bubble.downloadFile,
602
+ onClick: lazy.load,
603
+ disabled: lazy.loading,
604
+ className: "w-8 h-8 flex items-center justify-center rounded-full bg-gray-200 hover:bg-gray-300 flex-shrink-0 transition-colors disabled:opacity-50",
605
+ "aria-label": bubble.downloadFile,
606
+ children: /* @__PURE__ */ jsxs4("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", className: "text-gray-600", children: [
607
+ /* @__PURE__ */ jsx6("path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" }),
608
+ /* @__PURE__ */ jsx6("polyline", { points: "7 10 12 15 17 10" }),
609
+ /* @__PURE__ */ jsx6("line", { x1: "12", y1: "15", x2: "12", y2: "3" })
610
+ ] })
611
+ }
612
+ ) : null,
613
+ lazy.error && /* @__PURE__ */ jsx6("span", { className: "text-xs text-red-500 flex-shrink-0", children: bubble.mediaError })
614
+ ] });
615
+ }
616
+ default:
617
+ return null;
618
+ }
619
+ }
620
+
621
+ // src/Lightbox.tsx
622
+ import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
623
+ var DEFAULT_LIGHTBOX_LABELS = {
624
+ imageAlt: "Imagem",
625
+ close: "Fechar"
626
+ };
627
+ function Lightbox({ imageUrl, caption, onClose, labels }) {
628
+ const imageAltLabel = labels?.imageAlt ?? DEFAULT_LIGHTBOX_LABELS.imageAlt;
629
+ const closeLabel = labels?.close ?? DEFAULT_LIGHTBOX_LABELS.close;
630
+ return /* @__PURE__ */ jsx7("div", { className: "fixed inset-0 z-50 bg-black/85 flex items-center justify-center p-4", onClick: onClose, children: /* @__PURE__ */ jsxs5("div", { className: "max-w-[90vw] max-h-[90vh] flex flex-col items-center", onClick: (e) => e.stopPropagation(), children: [
631
+ /* @__PURE__ */ jsx7("img", { src: imageUrl, alt: caption ?? imageAltLabel, className: "max-w-full max-h-[80vh] object-contain rounded-lg" }),
632
+ caption && /* @__PURE__ */ jsx7("p", { className: "text-white text-sm mt-3 text-center", children: caption }),
633
+ /* @__PURE__ */ jsx7("button", { "data-cv-tooltip": closeLabel, "aria-label": closeLabel, onClick: onClose, className: "mt-4 px-4 py-2 bg-white/20 text-white rounded-lg hover:bg-white/30 transition-colors", children: closeLabel })
634
+ ] }) });
635
+ }
636
+
637
+ // src/InteractiveMessage.tsx
638
+ import { useState as useState4 } from "react";
639
+ import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
640
+ var FORMATTING_CLASSES = "[&_strong]:font-bold [&_em]:italic [&_del]:line-through";
641
+ var DEFAULT_INTERACTIVE_MESSAGE_LABELS = {
642
+ openList: "Ver op\xE7\xF5es"
643
+ };
644
+ function collectRows(payload) {
645
+ return (payload.action?.sections ?? []).flatMap((section) => section.rows ?? []);
646
+ }
647
+ function collectButtons(payload) {
648
+ return (payload.action?.buttons ?? []).map((button) => button.reply).filter((reply) => reply !== void 0);
649
+ }
650
+ function InteractiveMessage({ payload, onSelect, labels, className }) {
651
+ const openListLabel = labels?.openList ?? DEFAULT_INTERACTIVE_MESSAGE_LABELS.openList;
652
+ const [isListOpen, setIsListOpen] = useState4(false);
653
+ const buttons = collectButtons(payload);
654
+ const sections = payload.action?.sections ?? [];
655
+ const hasRows = collectRows(payload).length > 0;
656
+ const isInteractable = onSelect !== void 0;
657
+ return /* @__PURE__ */ jsxs6("div", { className: cn("flex flex-col gap-1", className), children: [
658
+ payload.header?.text ? /* @__PURE__ */ jsx8("p", { className: cn("text-sm font-semibold text-gray-900 dark:text-gray-100", FORMATTING_CLASSES), children: parseWhatsAppFormatting(payload.header.text) }) : null,
659
+ payload.body?.text ? /* @__PURE__ */ jsx8(
660
+ "p",
661
+ {
662
+ className: cn(
663
+ "whitespace-pre-wrap break-words text-sm text-gray-900 dark:text-gray-100",
664
+ FORMATTING_CLASSES
665
+ ),
666
+ children: parseWhatsAppFormatting(payload.body.text)
667
+ }
668
+ ) : null,
669
+ payload.footer?.text ? /* @__PURE__ */ jsx8("p", { className: cn("text-xs text-gray-500 dark:text-gray-400", FORMATTING_CLASSES), children: parseWhatsAppFormatting(payload.footer.text) }) : null,
670
+ buttons.length > 0 ? /* @__PURE__ */ jsx8("div", { className: "mt-1 flex flex-col gap-1 border-t border-gray-200 pt-1 dark:border-gray-700", children: buttons.map((button) => /* @__PURE__ */ jsx8(
671
+ "button",
672
+ {
673
+ "data-cv-tooltip": button.title,
674
+ "aria-label": button.title,
675
+ type: "button",
676
+ disabled: !isInteractable,
677
+ onClick: () => onSelect?.({ kind: "button", option: button }),
678
+ className: "rounded-md px-3 py-1.5 text-sm font-medium text-teal-700 transition-colors enabled:hover:bg-teal-50 disabled:cursor-default dark:text-teal-300 dark:enabled:hover:bg-teal-900/30",
679
+ children: button.title
680
+ },
681
+ button.id
682
+ )) }) : null,
683
+ hasRows ? /* @__PURE__ */ jsxs6("div", { className: "mt-1 border-t border-gray-200 pt-1 dark:border-gray-700", children: [
684
+ /* @__PURE__ */ jsxs6(
685
+ "button",
686
+ {
687
+ "data-cv-tooltip": payload.action?.button ?? openListLabel,
688
+ "aria-label": payload.action?.button ?? openListLabel,
689
+ type: "button",
690
+ onClick: () => setIsListOpen((open) => !open),
691
+ "aria-expanded": isListOpen,
692
+ className: "w-full rounded-md px-3 py-1.5 text-sm font-medium text-teal-700 transition-colors hover:bg-teal-50 dark:text-teal-300 dark:hover:bg-teal-900/30",
693
+ children: [
694
+ "\u2630 ",
695
+ payload.action?.button ?? openListLabel
696
+ ]
697
+ }
698
+ ),
699
+ isListOpen ? /* @__PURE__ */ jsx8("div", { className: "mt-1 flex flex-col gap-1", children: sections.map((section, sectionIndex) => /* @__PURE__ */ jsxs6("div", { className: "flex flex-col", children: [
700
+ section.title ? /* @__PURE__ */ jsx8("p", { className: "px-3 py-1 text-xs font-semibold uppercase text-gray-500 dark:text-gray-400", children: section.title }) : null,
701
+ (section.rows ?? []).map((row) => /* @__PURE__ */ jsxs6(
702
+ "button",
703
+ {
704
+ "data-cv-tooltip": row.title,
705
+ "aria-label": row.title,
706
+ type: "button",
707
+ disabled: !isInteractable,
708
+ onClick: () => onSelect?.({ kind: "list", option: row }),
709
+ className: "rounded-md px-3 py-1.5 text-left text-sm text-gray-900 transition-colors enabled:hover:bg-gray-100 disabled:cursor-default dark:text-gray-100 dark:enabled:hover:bg-gray-700",
710
+ children: [
711
+ /* @__PURE__ */ jsx8("span", { className: "block", children: row.title }),
712
+ row.description ? /* @__PURE__ */ jsx8("span", { className: "block text-xs text-gray-500 dark:text-gray-400", children: row.description }) : null
713
+ ]
714
+ },
715
+ row.id
716
+ ))
717
+ ] }, section.title ?? sectionIndex)) }) : null
718
+ ] }) : null
719
+ ] });
720
+ }
721
+
722
+ // src/lib/createMediaUrlResolver.ts
723
+ function createMediaUrlResolver(api) {
724
+ return async (message) => {
725
+ if (message.uploadId) return api.getDocumentUrl(message.uploadId, "inline");
726
+ if (message.mediaId) {
727
+ const { mimeType, data } = await api.getMediaProxyUrl(message.mediaId);
728
+ return `data:${mimeType};base64,${data}`;
729
+ }
730
+ return null;
731
+ };
732
+ }
733
+
734
+ // src/providers/ConversationsProvider.tsx
735
+ import { createContext as createContext2, useContext as useContext2, useMemo } from "react";
736
+ import { jsx as jsx9 } from "react/jsx-runtime";
737
+ var ConversationsContext = createContext2(null);
738
+ function ConversationsProvider({
739
+ api,
740
+ sse,
741
+ children
742
+ }) {
743
+ const value = useMemo(() => ({ api, sse }), [api, sse]);
744
+ return /* @__PURE__ */ jsx9(ConversationsContext.Provider, { value, children });
745
+ }
746
+ function useConversations() {
747
+ return useContext2(ConversationsContext);
748
+ }
749
+
750
+ // src/MessageBubble.tsx
751
+ import { useMemo as useMemo2, useState as useState5 } from "react";
752
+ import { Check as Check2 } from "lucide-react";
753
+ import { Fragment as Fragment2, jsx as jsx10, jsxs as jsxs7 } from "react/jsx-runtime";
754
+ var BUBBLE_COLOR = {
755
+ agent: "bg-[#d9fdd3] dark:bg-[#005c4b]",
756
+ bot: "bg-[#d7f0ec] dark:bg-[#0a3d3a]",
757
+ customer: "bg-white dark:bg-[#202c33]"
758
+ };
759
+ var MEDIA_TYPES = /* @__PURE__ */ new Set(["image", "audio", "video", "document", "sticker"]);
760
+ function MessageBubble({
761
+ message,
762
+ isMine,
763
+ senderName,
764
+ isFirstInGroup = true,
765
+ isSelecting = false,
766
+ isSelected = false,
767
+ onToggleSelect,
768
+ onResolveMediaUrl,
769
+ onInteractiveSelect,
770
+ onTranscribeAudio,
771
+ className
772
+ }) {
773
+ const { bubble, selection } = useConversationLocales();
774
+ const [lightboxSrc, setLightboxSrc] = useState5(null);
775
+ const context = useConversations();
776
+ const resolveMediaUrl = useMemo2(
777
+ () => onResolveMediaUrl ?? (context?.api ? createMediaUrlResolver(context.api) : void 0),
778
+ [onResolveMediaUrl, context?.api]
779
+ );
780
+ const requestTranscription = useMemo2(() => {
781
+ const transcribe = onTranscribeAudio ?? context?.api?.transcribeAudio?.bind(context.api);
782
+ if (!transcribe) return void 0;
783
+ return () => transcribe(message.id);
784
+ }, [onTranscribeAudio, context?.api, message.id]);
785
+ const bubbleColor = BUBBLE_COLOR[message.sender] ?? BUBBLE_COLOR.customer;
786
+ const hasError = message.status === "failed";
787
+ const isMedia = MEDIA_TYPES.has(message.type);
788
+ const isTemplate = message.type === "template";
789
+ const isInteractive = message.type === "interactive";
790
+ const mediaCaption = isMedia ? [message.caption, message.content].find((text) => {
791
+ const trimmed = text?.trim();
792
+ return trimmed && trimmed !== message.filename;
793
+ }) : void 0;
794
+ const displayName = message.sender === "agent" && senderName ? senderName : bubble[message.sender] ?? message.sender;
795
+ const tooltipText = message.status === "read" && message.readAt ? `${bubble.readAt}${formatDateTime(message.readAt)}` : message.status === "failed" ? bubble.windowExpired : void 0;
796
+ const tailCornerClass = isFirstInGroup ? isMine ? "rounded-tr-md" : "rounded-tl-md" : "";
797
+ const checkbox = /* @__PURE__ */ jsx10(
798
+ "button",
799
+ {
800
+ onClick: (e) => {
801
+ e.stopPropagation();
802
+ onToggleSelect?.();
803
+ },
804
+ "data-cv-tooltip": selection.select,
805
+ "aria-label": selection.select,
806
+ className: `
807
+ flex-shrink-0 self-end mb-1 w-5 h-5 rounded-full border-2 flex items-center justify-center transition-all
808
+ ${isSelected ? "bg-teal-600 border-teal-600" : "bg-white/80 dark:bg-black/40 border-black/20 dark:border-white/30"}
809
+ ${isSelecting ? "opacity-100" : "opacity-0 group-hover:opacity-100"}
810
+ `,
811
+ children: isSelected && /* @__PURE__ */ jsx10(Check2, { size: 12, className: "text-white", strokeWidth: 3 })
812
+ }
813
+ );
814
+ return /* @__PURE__ */ jsxs7(
815
+ "div",
816
+ {
817
+ className: cn(
818
+ "flex items-end gap-1.5 group",
819
+ isMine ? "justify-end" : "justify-start",
820
+ isFirstInGroup ? "mt-2" : "mt-0.5",
821
+ className
822
+ ),
823
+ children: [
824
+ isMine && checkbox,
825
+ /* @__PURE__ */ jsxs7(
826
+ "div",
827
+ {
828
+ onClick: isSelecting ? onToggleSelect : void 0,
829
+ className: `
830
+ max-w-[75%] sm:max-w-[65%] rounded-2xl ${tailCornerClass} px-2.5 py-1.5 shadow-sm
831
+ ${bubbleColor}
832
+ ${hasError ? "ring-1 ring-inset ring-red-400" : ""}
833
+ ${isSelecting ? "cursor-pointer" : ""}
834
+ ${isSelected ? "ring-2 ring-teal-500" : ""}
835
+ relative
836
+ `,
837
+ children: [
838
+ isMine && isFirstInGroup && (message.sender === "bot" || message.sender === "agent" && senderName) && /* @__PURE__ */ jsx10("div", { className: "text-xs font-semibold mb-0.5 text-teal-700 dark:text-teal-400", children: displayName }),
839
+ message.moderation?.isOffensive && /* @__PURE__ */ jsxs7(
840
+ "div",
841
+ {
842
+ className: "mb-1 inline-flex items-center gap-1 rounded-full bg-amber-100 px-2 py-0.5 text-xs font-medium text-amber-800 dark:bg-amber-950/60 dark:text-amber-300",
843
+ "data-cv-tooltip": message.moderation.terms.length > 0 ? message.moderation.terms.join(", ") : void 0,
844
+ children: [
845
+ /* @__PURE__ */ jsx10("span", { "aria-hidden": true, children: "\u26A0\uFE0F" }),
846
+ /* @__PURE__ */ jsx10("span", { children: bubble.moderationFlagged })
847
+ ]
848
+ }
849
+ ),
850
+ isMedia ? /* @__PURE__ */ jsxs7(Fragment2, { children: [
851
+ /* @__PURE__ */ jsx10(
852
+ MediaRenderer,
853
+ {
854
+ message,
855
+ onLightbox: setLightboxSrc,
856
+ onResolveUrl: resolveMediaUrl,
857
+ ...requestTranscription ? { onTranscribeAudio: requestTranscription } : {}
858
+ }
859
+ ),
860
+ mediaCaption ? /* @__PURE__ */ jsx10("div", { className: "mt-1 text-sm text-gray-900 dark:text-gray-100 whitespace-pre-wrap break-words leading-[19px]", children: parseWhatsAppFormatting(mediaCaption) }) : null
861
+ ] }) : isInteractive && message.payload ? (
862
+ // O texto da mensagem interativa mora dentro do payload (`body.text`), e `content` guarda
863
+ // só uma cópia achatada para busca — renderizar `content` aqui duplicaria o corpo.
864
+ /* @__PURE__ */ jsx10(InteractiveMessage, { payload: message.payload, onSelect: onInteractiveSelect })
865
+ ) : /* @__PURE__ */ jsxs7(Fragment2, { children: [
866
+ isTemplate && /* @__PURE__ */ jsxs7("p", { className: "text-xs text-gray-500 dark:text-gray-400 flex items-center gap-1 mb-0.5", children: [
867
+ /* @__PURE__ */ jsx10("span", { children: "\u{1F4E8}" }),
868
+ /* @__PURE__ */ jsxs7("span", { className: "font-medium", children: [
869
+ bubble.templateLabel,
870
+ message.templateName ? ` \u2014 ${message.templateName}` : ""
871
+ ] })
872
+ ] }),
873
+ /* @__PURE__ */ jsx10("div", { className: "text-sm text-gray-900 dark:text-gray-100 whitespace-pre-wrap break-words leading-[19px]", children: parseWhatsAppFormatting(message.content ?? "") })
874
+ ] }),
875
+ /* @__PURE__ */ jsxs7("div", { className: "flex items-center justify-end gap-1 mt-0.5 select-none", children: [
876
+ /* @__PURE__ */ jsx10("span", { className: "text-xs text-black/40 dark:text-white/40 font-medium", children: formatTimestamp(message.timestamp) }),
877
+ isMine && message.status && /* @__PURE__ */ jsx10(StatusTicks, { status: message.status, title: tooltipText })
878
+ ] })
879
+ ]
880
+ }
881
+ ),
882
+ !isMine && checkbox,
883
+ lightboxSrc && /* @__PURE__ */ jsx10(Lightbox, { imageUrl: lightboxSrc, onClose: () => setLightboxSrc(null) })
884
+ ]
885
+ }
886
+ );
887
+ }
888
+
889
+ // src/Wallpaper.tsx
890
+ import { forwardRef } from "react";
891
+ import { jsx as jsx11 } from "react/jsx-runtime";
892
+ function doodlePattern(strokeColor, opacity) {
893
+ const svg = `<svg xmlns='http://www.w3.org/2000/svg' width='100' height='100' viewBox='0 0 100 100'><g fill='none' stroke='${strokeColor}' stroke-width='1.2' opacity='${opacity}'><circle cx='15' cy='15' r='3'/><path d='M40 10 q5 8 0 16 q-5 -8 0 -16z'/><circle cx='70' cy='28' r='2'/><path d='M18 55 l6 6 m-6 0 l6 -6'/><circle cx='55' cy='68' r='2.5'/><path d='M85 58 q6 6 0 12 q-6 -6 0 -12z'/><circle cx='8' cy='85' r='2'/><path d='M65 90 l5 5 m-5 0 l5 -5'/></g></svg>`;
894
+ return `url("data:image/svg+xml,${encodeURIComponent(svg)}")`;
895
+ }
896
+ var LIGHT_WALLPAPER = {
897
+ backgroundColor: "#efeae2",
898
+ backgroundImage: doodlePattern("#d7cfc0", 0.7),
899
+ backgroundRepeat: "repeat",
900
+ backgroundSize: "100px 100px"
901
+ };
902
+ var DARK_WALLPAPER = {
903
+ backgroundColor: "#0b141a",
904
+ backgroundImage: doodlePattern("#19232a", 0.9),
905
+ backgroundRepeat: "repeat",
906
+ backgroundSize: "100px 100px"
907
+ };
908
+ var ConversationWallpaper = forwardRef(
909
+ function ConversationWallpaper2({ children, className, style, onScroll }, ref) {
910
+ const isDark = useIsDarkTheme();
911
+ return /* @__PURE__ */ jsx11(
912
+ "div",
913
+ {
914
+ ref,
915
+ onScroll,
916
+ className: cn("cv-wallpaper", className),
917
+ style: { ...isDark ? DARK_WALLPAPER : LIGHT_WALLPAPER, ...style },
918
+ children
919
+ }
920
+ );
921
+ }
922
+ );
923
+
924
+ // src/emojiCatalog.ts
925
+ var EMOJI_CATEGORIES = [
926
+ {
927
+ name: "Smileys",
928
+ entries: [
929
+ { emoji: "\u{1F600}", keywords: ["sorriso", "feliz", "alegre"] },
930
+ { emoji: "\u{1F603}", keywords: ["sorriso", "feliz", "animado"] },
931
+ { emoji: "\u{1F604}", keywords: ["sorriso", "feliz", "risada"] },
932
+ { emoji: "\u{1F601}", keywords: ["sorriso", "dentes", "feliz"] },
933
+ { emoji: "\u{1F605}", keywords: ["alivio", "al\xEDvio", "suor", "nervoso"] },
934
+ { emoji: "\u{1F602}", keywords: ["risada", "chorando", "engracado", "engra\xE7ado"] },
935
+ { emoji: "\u{1F923}", keywords: ["risada", "rolando", "engracado", "engra\xE7ado"] },
936
+ { emoji: "\u{1F60A}", keywords: ["sorriso", "timido", "t\xEDmido", "feliz"] },
937
+ { emoji: "\u{1F607}", keywords: ["anjo", "inocente", "santo"] },
938
+ { emoji: "\u{1F642}", keywords: ["sorriso", "leve", "ok"] },
939
+ { emoji: "\u{1F609}", keywords: ["piscada", "piscar", "flerte"] },
940
+ { emoji: "\u{1F60C}", keywords: ["aliviado", "calmo", "tranquilo"] },
941
+ { emoji: "\u{1F60D}", keywords: ["amor", "apaixonado", "coracao", "cora\xE7\xE3o", "olhos"] },
942
+ { emoji: "\u{1F970}", keywords: ["amor", "apaixonado", "carinho"] },
943
+ { emoji: "\u{1F618}", keywords: ["beijo", "amor", "carinho"] },
944
+ { emoji: "\u{1F60B}", keywords: ["gostoso", "delicia", "del\xEDcia", "lingua", "l\xEDngua"] },
945
+ { emoji: "\u{1F61C}", keywords: ["lingua", "l\xEDngua", "brincadeira", "piscada"] },
946
+ { emoji: "\u{1F92A}", keywords: ["maluco", "doido", "brincadeira"] },
947
+ { emoji: "\u{1F914}", keywords: ["pensando", "duvida", "d\xFAvida", "hmm"] },
948
+ { emoji: "\u{1F917}", keywords: ["abraco", "abra\xE7o", "carinho"] },
949
+ { emoji: "\u{1F610}", keywords: ["neutro", "serio", "s\xE9rio", "indiferente"] },
950
+ { emoji: "\u{1F634}", keywords: ["dormindo", "sono", "cansado"] },
951
+ { emoji: "\u{1F62D}", keywords: ["chorando", "triste", "lagrima", "l\xE1grima"] },
952
+ { emoji: "\u{1F622}", keywords: ["triste", "chorando", "lagrima", "l\xE1grima"] },
953
+ { emoji: "\u{1F621}", keywords: ["raiva", "bravo", "irritado"] },
954
+ { emoji: "\u{1F631}", keywords: ["susto", "medo", "assustado"] },
955
+ { emoji: "\u{1F92F}", keywords: ["explodindo", "chocado", "surpresa"] },
956
+ { emoji: "\u{1F60E}", keywords: ["oculos", "\xF3culos", "legal", "estiloso"] },
957
+ { emoji: "\u{1F973}", keywords: ["festa", "comemorar", "aniversario", "anivers\xE1rio"] },
958
+ { emoji: "\u{1F637}", keywords: ["mascara", "m\xE1scara", "doente", "saude", "sa\xFAde"] }
959
+ ]
960
+ },
961
+ {
962
+ name: "Gestos",
963
+ entries: [
964
+ { emoji: "\u{1F44D}", keywords: ["joia", "j\xF3ia", "polegar", "ok", "positivo", "curtir"] },
965
+ { emoji: "\u{1F44E}", keywords: ["polegar", "negativo", "ruim", "nao", "n\xE3o"] },
966
+ { emoji: "\u{1F44C}", keywords: ["ok", "certo", "perfeito"] },
967
+ { emoji: "\u270C\uFE0F", keywords: ["paz", "vitoria", "vit\xF3ria", "dois"] },
968
+ { emoji: "\u{1F91E}", keywords: ["sorte", "dedos", "cruzados", "torcendo"] },
969
+ { emoji: "\u{1F919}", keywords: ["chama", "ligar", "shaka"] },
970
+ { emoji: "\u{1F44B}", keywords: ["tchau", "ola", "ol\xE1", "aceno", "oi"] },
971
+ { emoji: "\u270B", keywords: ["mao", "m\xE3o", "parar", "pare"] },
972
+ { emoji: "\u{1F44F}", keywords: ["palmas", "aplauso", "parabens", "parab\xE9ns"] },
973
+ { emoji: "\u{1F64C}", keywords: ["comemorar", "maos", "m\xE3os", "sucesso"] },
974
+ { emoji: "\u{1F91D}", keywords: ["acordo", "aperto", "mao", "m\xE3o", "negocio", "neg\xF3cio", "parceria"] },
975
+ { emoji: "\u{1F64F}", keywords: ["obrigado", "reza", "oracao", "ora\xE7\xE3o", "por favor"] },
976
+ { emoji: "\u270D\uFE0F", keywords: ["escrever", "assinar", "assinatura"] },
977
+ { emoji: "\u{1F4AA}", keywords: ["forca", "for\xE7a", "musculo", "m\xFAsculo", "braco", "bra\xE7o"] },
978
+ { emoji: "\u{1F447}", keywords: ["abaixo", "baixo", "apontar", "seta"] },
979
+ { emoji: "\u{1F449}", keywords: ["direita", "apontar", "seta"] },
980
+ { emoji: "\u261D\uFE0F", keywords: ["acima", "cima", "apontar", "atencao", "aten\xE7\xE3o"] }
981
+ ]
982
+ },
983
+ {
984
+ name: "Cora\xE7\xF5es",
985
+ entries: [
986
+ { emoji: "\u2764\uFE0F", keywords: ["coracao", "cora\xE7\xE3o", "amor", "vermelho"] },
987
+ { emoji: "\u{1F9E1}", keywords: ["coracao", "cora\xE7\xE3o", "laranja"] },
988
+ { emoji: "\u{1F49B}", keywords: ["coracao", "cora\xE7\xE3o", "amarelo"] },
989
+ { emoji: "\u{1F49A}", keywords: ["coracao", "cora\xE7\xE3o", "verde"] },
990
+ { emoji: "\u{1F499}", keywords: ["coracao", "cora\xE7\xE3o", "azul"] },
991
+ { emoji: "\u{1F49C}", keywords: ["coracao", "cora\xE7\xE3o", "roxo"] },
992
+ { emoji: "\u{1F5A4}", keywords: ["coracao", "cora\xE7\xE3o", "preto"] },
993
+ { emoji: "\u{1F90D}", keywords: ["coracao", "cora\xE7\xE3o", "branco"] },
994
+ { emoji: "\u{1F494}", keywords: ["coracao", "cora\xE7\xE3o", "partido", "triste"] },
995
+ { emoji: "\u{1F495}", keywords: ["coracao", "cora\xE7\xE3o", "amor", "casal"] },
996
+ { emoji: "\u{1F496}", keywords: ["coracao", "cora\xE7\xE3o", "brilho", "amor"] },
997
+ { emoji: "\u{1F49D}", keywords: ["coracao", "cora\xE7\xE3o", "presente", "laco", "la\xE7o"] }
998
+ ]
999
+ },
1000
+ {
1001
+ name: "Neg\xF3cios",
1002
+ entries: [
1003
+ { emoji: "\u{1F3E0}", keywords: ["casa", "imovel", "im\xF3vel", "residencia", "resid\xEAncia", "moradia"] },
1004
+ { emoji: "\u{1F3E1}", keywords: ["casa", "imovel", "im\xF3vel", "jardim", "moradia"] },
1005
+ { emoji: "\u{1F3E2}", keywords: ["predio", "pr\xE9dio", "empresa", "escritorio", "escrit\xF3rio"] },
1006
+ { emoji: "\u{1F3E6}", keywords: ["banco", "financiamento", "agencia", "ag\xEAncia"] },
1007
+ { emoji: "\u{1F511}", keywords: ["chave", "casa", "entrega", "imovel", "im\xF3vel"] },
1008
+ { emoji: "\u{1F4C4}", keywords: ["documento", "papel", "contrato", "arquivo"] },
1009
+ { emoji: "\u{1F4CB}", keywords: ["prancheta", "lista", "documento", "checklist"] },
1010
+ { emoji: "\u{1F4DD}", keywords: ["anotar", "escrever", "nota", "formulario", "formul\xE1rio"] },
1011
+ { emoji: "\u2705", keywords: ["ok", "certo", "aprovado", "concluido", "conclu\xEDdo", "check"] },
1012
+ { emoji: "\u274C", keywords: ["errado", "negado", "recusado", "cancelar"] },
1013
+ { emoji: "\u26A0\uFE0F", keywords: ["atencao", "aten\xE7\xE3o", "alerta", "cuidado"] },
1014
+ { emoji: "\u{1F4B0}", keywords: ["dinheiro", "valor", "saco", "grana", "pagamento"] },
1015
+ { emoji: "\u{1F4B5}", keywords: ["dinheiro", "nota", "valor", "pagamento"] },
1016
+ { emoji: "\u{1F4B3}", keywords: ["cartao", "cart\xE3o", "credito", "cr\xE9dito", "pagamento"] },
1017
+ { emoji: "\u{1F9FE}", keywords: ["recibo", "nota", "fiscal", "comprovante"] },
1018
+ { emoji: "\u{1F4CA}", keywords: ["grafico", "gr\xE1fico", "relatorio", "relat\xF3rio", "dados"] },
1019
+ { emoji: "\u{1F4C8}", keywords: ["grafico", "gr\xE1fico", "subindo", "crescimento", "alta"] },
1020
+ { emoji: "\u{1F4C9}", keywords: ["grafico", "gr\xE1fico", "caindo", "queda", "baixa"] },
1021
+ { emoji: "\u{1F5D3}\uFE0F", keywords: ["calendario", "calend\xE1rio", "data", "agenda", "prazo"] },
1022
+ { emoji: "\u23F0", keywords: ["relogio", "rel\xF3gio", "hora", "prazo", "alarme"] },
1023
+ { emoji: "\u{1F4DE}", keywords: ["telefone", "ligar", "contato", "chamada"] },
1024
+ { emoji: "\u{1F4F1}", keywords: ["celular", "telefone", "whatsapp", "contato"] },
1025
+ { emoji: "\u{1F4E7}", keywords: ["email", "e-mail", "mensagem", "contato"] },
1026
+ { emoji: "\u{1F4CE}", keywords: ["anexo", "clipe", "arquivo"] },
1027
+ { emoji: "\u{1F50D}", keywords: ["buscar", "procurar", "lupa", "pesquisa", "consulta"] },
1028
+ { emoji: "\u{1F916}", keywords: ["robo", "rob\xF4", "bot", "assistente", "automatico", "autom\xE1tico"] },
1029
+ { emoji: "\u{1F4AC}", keywords: ["mensagem", "conversa", "balao", "bal\xE3o", "chat"] }
1030
+ ]
1031
+ },
1032
+ {
1033
+ name: "Objetos",
1034
+ entries: [
1035
+ { emoji: "\u{1F381}", keywords: ["presente", "brinde", "surpresa"] },
1036
+ { emoji: "\u{1F389}", keywords: ["festa", "comemorar", "parabens", "parab\xE9ns"] },
1037
+ { emoji: "\u{1F38A}", keywords: ["festa", "confete", "comemorar"] },
1038
+ { emoji: "\u{1F382}", keywords: ["bolo", "aniversario", "anivers\xE1rio", "festa"] },
1039
+ { emoji: "\u{1F4A1}", keywords: ["ideia", "ide\xEDa", "lampada", "l\xE2mpada", "dica"] },
1040
+ { emoji: "\u{1F514}", keywords: ["sino", "aviso", "notificacao", "notifica\xE7\xE3o", "lembrete"] },
1041
+ { emoji: "\u2B50", keywords: ["estrela", "favorito", "avaliacao", "avalia\xE7\xE3o"] },
1042
+ { emoji: "\u{1F525}", keywords: ["fogo", "quente", "destaque", "top"] },
1043
+ { emoji: "\u{1F680}", keywords: ["foguete", "rapido", "r\xE1pido", "lancamento", "lan\xE7amento"] },
1044
+ { emoji: "\u{1F4BB}", keywords: ["computador", "notebook", "trabalho"] },
1045
+ { emoji: "\u{1F4F7}", keywords: ["foto", "camera", "c\xE2mera", "imagem"] },
1046
+ { emoji: "\u{1F697}", keywords: ["carro", "veiculo", "ve\xEDculo", "automovel", "autom\xF3vel"] },
1047
+ { emoji: "\u2708\uFE0F", keywords: ["aviao", "avi\xE3o", "viagem", "voo"] }
1048
+ ]
1049
+ },
1050
+ {
1051
+ name: "Comida",
1052
+ entries: [
1053
+ { emoji: "\u{1F354}", keywords: ["hamburguer", "hamb\xFArguer", "lanche", "comida"] },
1054
+ { emoji: "\u{1F355}", keywords: ["pizza", "comida", "lanche"] },
1055
+ { emoji: "\u{1F35F}", keywords: ["batata", "frita", "lanche"] },
1056
+ { emoji: "\u{1F37F}", keywords: ["pipoca", "cinema", "filme"] },
1057
+ { emoji: "\u{1F35E}", keywords: ["pao", "p\xE3o", "padaria"] },
1058
+ { emoji: "\u{1F9C0}", keywords: ["queijo", "comida"] },
1059
+ { emoji: "\u{1F957}", keywords: ["salada", "saudavel", "saud\xE1vel", "comida"] },
1060
+ { emoji: "\u2615", keywords: ["cafe", "caf\xE9", "bebida", "quente"] },
1061
+ { emoji: "\u{1F37A}", keywords: ["cerveja", "bebida", "chopp"] },
1062
+ { emoji: "\u{1F377}", keywords: ["vinho", "bebida", "taca", "ta\xE7a"] },
1063
+ { emoji: "\u{1F942}", keywords: ["brinde", "comemorar", "champanhe"] },
1064
+ { emoji: "\u{1F964}", keywords: ["refrigerante", "bebida", "copo"] }
1065
+ ]
1066
+ }
1067
+ ];
1068
+ var ALL_ENTRIES = EMOJI_CATEGORIES.flatMap((category) => category.entries);
1069
+ function normalize(value) {
1070
+ return value.toLowerCase().normalize("NFD").replace(/\p{Diacritic}/gu, "").trim();
1071
+ }
1072
+ function searchEmojis(query) {
1073
+ const term = normalize(query);
1074
+ if (!term) return ALL_ENTRIES;
1075
+ return ALL_ENTRIES.filter((entry) => entry.keywords.some((keyword) => normalize(keyword).startsWith(term)));
1076
+ }
1077
+
1078
+ // src/EmojiPicker.tsx
1079
+ import { useState as useState6, useCallback as useCallback2, useMemo as useMemo3 } from "react";
1080
+ import { jsx as jsx12, jsxs as jsxs8 } from "react/jsx-runtime";
1081
+ var DEFAULT_EMOJI_PICKER_LABELS = {
1082
+ search: "Buscar emoji",
1083
+ noResults: "Nenhum emoji encontrado"
1084
+ };
1085
+ var EmojiPicker = ({ onSelect, labels, className = "" }) => {
1086
+ const searchLabel = labels?.search ?? DEFAULT_EMOJI_PICKER_LABELS.search;
1087
+ const noResultsLabel = labels?.noResults ?? DEFAULT_EMOJI_PICKER_LABELS.noResults;
1088
+ const [activeCategory, setActiveCategory] = useState6(0);
1089
+ const [query, setQuery] = useState6("");
1090
+ const handleSelect = useCallback2(
1091
+ (emoji) => {
1092
+ onSelect(emoji);
1093
+ },
1094
+ [onSelect]
1095
+ );
1096
+ const isSearching = query.trim().length > 0;
1097
+ const visibleEntries = useMemo3(
1098
+ () => isSearching ? searchEmojis(query) : EMOJI_CATEGORIES[activeCategory].entries,
1099
+ [isSearching, query, activeCategory]
1100
+ );
1101
+ return /* @__PURE__ */ jsxs8("div", { className: `bg-white border border-gray-200 rounded-lg shadow-lg overflow-hidden ${className}`, children: [
1102
+ /* @__PURE__ */ jsx12("div", { className: "p-2 border-b border-gray-200", children: /* @__PURE__ */ jsx12(
1103
+ "input",
1104
+ {
1105
+ type: "search",
1106
+ value: query,
1107
+ onChange: (event) => setQuery(event.target.value),
1108
+ placeholder: searchLabel,
1109
+ "aria-label": searchLabel,
1110
+ className: "w-full rounded-md border border-gray-200 px-2 py-1.5 text-sm outline-none focus:border-blue-500"
1111
+ }
1112
+ ) }),
1113
+ isSearching ? null : /* @__PURE__ */ jsx12("div", { className: "flex border-b border-gray-200 overflow-x-auto", children: EMOJI_CATEGORIES.map((category, index) => /* @__PURE__ */ jsxs8(
1114
+ "button",
1115
+ {
1116
+ "data-cv-tooltip": category.name,
1117
+ "aria-label": category.name,
1118
+ onClick: () => setActiveCategory(index),
1119
+ className: `px-3 py-2 text-xs font-medium whitespace-nowrap border-b-2 transition-colors ${activeCategory === index ? "border-blue-500 text-blue-600" : "border-transparent text-gray-500 hover:text-gray-700"}`,
1120
+ children: [
1121
+ category.entries[0].emoji,
1122
+ " ",
1123
+ category.name
1124
+ ]
1125
+ },
1126
+ category.name
1127
+ )) }),
1128
+ visibleEntries.length === 0 ? /* @__PURE__ */ jsx12("p", { className: "px-3 py-6 text-center text-sm text-gray-500", children: noResultsLabel }) : /* @__PURE__ */ jsx12("div", { className: "grid grid-cols-10 gap-0.5 p-2 max-h-[240px] overflow-y-auto", children: visibleEntries.map((entry) => /* @__PURE__ */ jsx12(
1129
+ "button",
1130
+ {
1131
+ onClick: () => handleSelect(entry.emoji),
1132
+ className: "w-8 h-8 flex items-center justify-center text-lg hover:bg-gray-100 rounded transition-colors cursor-pointer",
1133
+ "aria-label": entry.emoji,
1134
+ "data-cv-tooltip": entry.keywords[0],
1135
+ children: entry.emoji
1136
+ },
1137
+ entry.emoji
1138
+ )) })
1139
+ ] });
1140
+ };
1141
+
1142
+ // src/AudioRecorderButton.tsx
1143
+ import { useCallback as useCallback3, useEffect as useEffect2, useRef as useRef2, useState as useState7 } from "react";
1144
+ import { jsx as jsx13, jsxs as jsxs9 } from "react/jsx-runtime";
1145
+ var DEFAULT_AUDIO_RECORDER_BUTTON_LABELS = {
1146
+ start: "Gravar \xE1udio",
1147
+ stop: "Parar grava\xE7\xE3o",
1148
+ unsupported: "Este navegador n\xE3o grava \xE1udio.",
1149
+ denied: "Sem permiss\xE3o para usar o microfone.",
1150
+ review: "Ou\xE7a antes de enviar",
1151
+ send: "Enviar \xE1udio",
1152
+ discard: "Descartar \xE1udio",
1153
+ empty: "Nada foi captado pelo microfone."
1154
+ };
1155
+ var DEFAULT_MAX_RECORDING_MILLISECONDS = 5 * 60 * 1e3;
1156
+ var RECORDING_FORMATS = [
1157
+ { mimeType: "audio/ogg;codecs=opus", uploadMimeType: "audio/ogg", extension: "ogg" },
1158
+ { mimeType: "audio/mp4", uploadMimeType: "audio/mp4", extension: "m4a" },
1159
+ { mimeType: "audio/webm", uploadMimeType: "audio/webm", extension: "webm" }
1160
+ ];
1161
+ function resolveRecordingFormat() {
1162
+ if (typeof MediaRecorder === "undefined") return void 0;
1163
+ if (typeof MediaRecorder.isTypeSupported !== "function") return RECORDING_FORMATS[0];
1164
+ return RECORDING_FORMATS.find((format) => MediaRecorder.isTypeSupported(format.mimeType));
1165
+ }
1166
+ function AudioRecorderButton({
1167
+ onRecorded,
1168
+ onFailure,
1169
+ onRecordingChange,
1170
+ reviewBeforeSend = true,
1171
+ maxDurationMilliseconds = DEFAULT_MAX_RECORDING_MILLISECONDS,
1172
+ labels,
1173
+ disabled
1174
+ }) {
1175
+ const labelOf = (key) => labels?.[key] ?? DEFAULT_AUDIO_RECORDER_BUTTON_LABELS[key];
1176
+ const [isRecording, setIsRecording] = useState7(false);
1177
+ const [pending, setPending] = useState7(void 0);
1178
+ const recorderRef = useRef2(null);
1179
+ const autoStopRef = useRef2(void 0);
1180
+ const discard = useCallback3(() => {
1181
+ setPending((current) => {
1182
+ if (current) URL.revokeObjectURL(current.objectURL);
1183
+ return void 0;
1184
+ });
1185
+ }, []);
1186
+ useEffect2(() => discard, [discard]);
1187
+ const confirm = useCallback3(() => {
1188
+ if (!pending) return;
1189
+ void onRecorded(pending.file);
1190
+ discard();
1191
+ }, [discard, onRecorded, pending]);
1192
+ const stop = useCallback3(() => {
1193
+ recorderRef.current?.stop();
1194
+ }, []);
1195
+ const start = useCallback3(async () => {
1196
+ const format = resolveRecordingFormat();
1197
+ if (!format || !navigator.mediaDevices?.getUserMedia) {
1198
+ onFailure?.(labels?.unsupported ?? DEFAULT_AUDIO_RECORDER_BUTTON_LABELS.unsupported);
1199
+ return;
1200
+ }
1201
+ try {
1202
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
1203
+ const recorder = new MediaRecorder(stream, { mimeType: format.mimeType });
1204
+ const chunks = [];
1205
+ recorder.addEventListener("dataavailable", (event) => {
1206
+ if (event.data.size > 0) chunks.push(event.data);
1207
+ });
1208
+ recorder.addEventListener("stop", () => {
1209
+ stream.getTracks().forEach((track) => track.stop());
1210
+ clearTimeout(autoStopRef.current);
1211
+ setIsRecording(false);
1212
+ onRecordingChange?.(false);
1213
+ recorderRef.current = null;
1214
+ const blob = new Blob(chunks, { type: format.uploadMimeType });
1215
+ const file = new File([blob], `audio-${Date.now()}.${format.extension}`, {
1216
+ type: format.uploadMimeType
1217
+ });
1218
+ if (blob.size === 0) {
1219
+ onFailure?.(labelOf("empty"));
1220
+ return;
1221
+ }
1222
+ if (!reviewBeforeSend) {
1223
+ void onRecorded(file);
1224
+ return;
1225
+ }
1226
+ setPending({ file, objectURL: URL.createObjectURL(blob) });
1227
+ });
1228
+ recorderRef.current = recorder;
1229
+ recorder.start();
1230
+ autoStopRef.current = setTimeout(() => recorder.stop(), maxDurationMilliseconds);
1231
+ setIsRecording(true);
1232
+ onRecordingChange?.(true);
1233
+ } catch {
1234
+ onFailure?.(labels?.denied ?? DEFAULT_AUDIO_RECORDER_BUTTON_LABELS.denied);
1235
+ }
1236
+ }, [maxDurationMilliseconds, onFailure, onRecorded, onRecordingChange, reviewBeforeSend]);
1237
+ const toggleLabel = isRecording ? labelOf("stop") : labelOf("start");
1238
+ return (
1239
+ /* O painel de revisão flutua sobre o botão em vez de ocupar espaço na barra: o microfone mora
1240
+ na caixa do botão de enviar, e empurrar o composer para cima a cada gravação faria a
1241
+ conversa saltar. */
1242
+ /* @__PURE__ */ jsxs9("div", { className: "relative flex-shrink-0", children: [
1243
+ pending && /* @__PURE__ */ jsxs9(
1244
+ "div",
1245
+ {
1246
+ role: "group",
1247
+ "aria-label": labelOf("review"),
1248
+ className: "absolute bottom-full right-0 z-20 mb-2 flex w-[calc(100vw-2rem)] max-w-[20rem] flex-wrap items-center justify-end gap-2 rounded-xl border border-gray-200 bg-white p-2 shadow-lg dark:border-gray-700 dark:bg-gray-800",
1249
+ children: [
1250
+ /* @__PURE__ */ jsx13("div", { className: "min-w-0 flex-1", children: /* @__PURE__ */ jsx13(AudioPlayer, { src: pending.objectURL }) }),
1251
+ /* @__PURE__ */ jsx13(
1252
+ "button",
1253
+ {
1254
+ type: "button",
1255
+ onClick: discard,
1256
+ "data-cv-tooltip": labelOf("discard"),
1257
+ "aria-label": labelOf("discard"),
1258
+ className: "flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full text-gray-500 transition-colors hover:bg-gray-100 hover:text-red-500 dark:hover:bg-gray-700",
1259
+ children: /* @__PURE__ */ jsxs9("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", children: [
1260
+ /* @__PURE__ */ jsx13("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
1261
+ /* @__PURE__ */ jsx13("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
1262
+ ] })
1263
+ }
1264
+ ),
1265
+ /* @__PURE__ */ jsx13(
1266
+ "button",
1267
+ {
1268
+ type: "button",
1269
+ onClick: confirm,
1270
+ "data-cv-tooltip": labelOf("send"),
1271
+ "aria-label": labelOf("send"),
1272
+ className: "flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full bg-emerald-500 text-white transition-colors hover:bg-emerald-600",
1273
+ children: /* @__PURE__ */ jsx13("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "currentColor", children: /* @__PURE__ */ jsx13("path", { d: "M2.01 21L23 12 2.01 3 2 10l15 2-15 2z" }) })
1274
+ }
1275
+ )
1276
+ ]
1277
+ }
1278
+ ),
1279
+ /* @__PURE__ */ jsx13(
1280
+ "button",
1281
+ {
1282
+ type: "button",
1283
+ disabled: disabled || pending !== void 0,
1284
+ onClick: () => isRecording ? stop() : void start(),
1285
+ "data-cv-tooltip": toggleLabel,
1286
+ "aria-label": toggleLabel,
1287
+ "aria-pressed": isRecording,
1288
+ className: `flex h-10 w-10 items-center justify-center rounded-full transition-colors disabled:opacity-50 ${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"}`,
1289
+ children: isRecording ? /* @__PURE__ */ jsx13("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "currentColor", children: /* @__PURE__ */ jsx13("rect", { x: "6", y: "6", width: "12", height: "12", rx: "2" }) }) : /* @__PURE__ */ jsxs9("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", children: [
1290
+ /* @__PURE__ */ jsx13("path", { d: "M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3z" }),
1291
+ /* @__PURE__ */ jsx13("path", { d: "M19 11a7 7 0 0 1-14 0" }),
1292
+ /* @__PURE__ */ jsx13("line", { x1: "12", y1: "18", x2: "12", y2: "22" })
1293
+ ] })
1294
+ }
1295
+ )
1296
+ ] })
1297
+ );
1298
+ }
1299
+
1300
+ // src/MessageComposer.tsx
1301
+ import { useState as useState8, useRef as useRef3, useCallback as useCallback4 } from "react";
1302
+
1303
+ // src/composer.constant.ts
1304
+ var COMPOSER_BAR_CLASS = "cv-composer-bar";
1305
+ var COMPOSER_TOOL_BUTTON_CLASS = "flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-full transition-colors hover:bg-teal-50 hover:text-teal-600 dark:hover:bg-teal-900/30 dark:hover:text-teal-400";
1306
+ var COMPOSER_TOOL_BUTTON_ACTIVE_CLASS = "bg-teal-50 text-teal-600 dark:bg-teal-900/40 dark:text-teal-400";
1307
+ var COMPOSER_TOOL_BUTTON_IDLE_CLASS = "text-gray-400 dark:text-gray-500";
1308
+ var COMPOSER_MONOSPACE_CLASS = "bg-black/5 dark:bg-white/10 rounded px-0.5 font-mono text-sm";
1309
+ var COMPOSER_COMPACT_WIDTH = 480;
1310
+ var QUICK_REPLY_PILL_CLASS = "whitespace-nowrap rounded-full border border-gray-200 bg-white px-3 py-1.5 text-xs text-gray-600 shadow-sm transition-colors hover:border-teal-200 hover:bg-teal-50 hover:text-teal-700 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-400 dark:hover:border-teal-800 dark:hover:bg-teal-950/40 dark:hover:text-teal-400";
1311
+
1312
+ // src/MessageComposer.tsx
1313
+ import { Fragment as Fragment3, jsx as jsx14, jsxs as jsxs10 } from "react/jsx-runtime";
1314
+ function applyQuickReplyVariables(template, variables = {}) {
1315
+ return template.replace(/\{\{\s*([\w.]+)\s*\}\}/g, (_, chave) => variables[chave] ?? "");
1316
+ }
1317
+ function resolveQuickReply(quickReply, variables = {}) {
1318
+ return typeof quickReply.text === "function" ? quickReply.text(variables) : applyQuickReplyVariables(quickReply.text, variables);
1319
+ }
1320
+ var DEFAULT_MESSAGE_COMPOSER_LABELS = {
1321
+ emoji: "Emoji",
1322
+ attach: "Anexar",
1323
+ send: "Enviar",
1324
+ removeAttachment: "Remover anexo"
1325
+ };
1326
+ var DEFAULT_ACCEPTED_FILE_TYPES = [
1327
+ "image/jpeg,image/png,image/webp",
1328
+ "audio/aac,audio/mp4,audio/mpeg,audio/amr,audio/ogg",
1329
+ "video/mp4,video/3gpp",
1330
+ "application/pdf,text/plain,text/csv",
1331
+ "application/msword,application/vnd.ms-excel,application/vnd.ms-powerpoint",
1332
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
1333
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
1334
+ "application/vnd.openxmlformats-officedocument.presentationml.presentation"
1335
+ ].join(",");
1336
+ var MessageComposer = ({
1337
+ onSend,
1338
+ onAttach,
1339
+ value: externalValue,
1340
+ onChange: externalOnChange,
1341
+ quickReplies,
1342
+ quickReplyVariables,
1343
+ features,
1344
+ placeholder = "Digite uma mensagem...",
1345
+ maxLength,
1346
+ disabled = false,
1347
+ acceptedFileTypes = DEFAULT_ACCEPTED_FILE_TYPES,
1348
+ idleAction,
1349
+ className,
1350
+ classNames,
1351
+ labels
1352
+ }) => {
1353
+ const emojiLabel = labels?.emoji ?? DEFAULT_MESSAGE_COMPOSER_LABELS.emoji;
1354
+ const attachLabel = labels?.attach ?? DEFAULT_MESSAGE_COMPOSER_LABELS.attach;
1355
+ const sendLabel = labels?.send ?? DEFAULT_MESSAGE_COMPOSER_LABELS.send;
1356
+ const removeAttachmentLabel = labels?.removeAttachment ?? DEFAULT_MESSAGE_COMPOSER_LABELS.removeAttachment;
1357
+ const [internalText, setInternalText] = useState8("");
1358
+ const [showEmoji, setShowEmoji] = useState8(false);
1359
+ const [attachments, setAttachments] = useState8([]);
1360
+ const textareaRef = useRef3(null);
1361
+ const fileInputRef = useRef3(null);
1362
+ const isControlled = externalValue !== void 0;
1363
+ const text = isControlled ? externalValue : internalText;
1364
+ const setText = useCallback4((newText) => {
1365
+ if (isControlled) {
1366
+ externalOnChange?.(newText);
1367
+ } else {
1368
+ setInternalText(newText);
1369
+ }
1370
+ }, [isControlled, externalOnChange]);
1371
+ const showEmojiButton = features?.emoji !== false;
1372
+ const showAttachButton = features?.documents !== false;
1373
+ const sendMessage = useCallback4(() => {
1374
+ const trimmed = text.trim();
1375
+ if (!trimmed && attachments.length === 0) return;
1376
+ if (trimmed) onSend(trimmed);
1377
+ for (const a of attachments) {
1378
+ onAttach?.(a.file);
1379
+ URL.revokeObjectURL(a.previewUrl);
1380
+ }
1381
+ if (!isControlled) setInternalText("");
1382
+ setAttachments([]);
1383
+ setShowEmoji(false);
1384
+ if (textareaRef.current) textareaRef.current.style.height = "auto";
1385
+ }, [text, attachments, onSend, onAttach, isControlled]);
1386
+ const handleKeyDown = useCallback4((e) => {
1387
+ if (e.key === "Enter" && !e.shiftKey) {
1388
+ e.preventDefault();
1389
+ if (!disabled) sendMessage();
1390
+ }
1391
+ }, [sendMessage, disabled]);
1392
+ const handleInput = useCallback4(() => {
1393
+ const ta = textareaRef.current;
1394
+ if (!ta) return;
1395
+ ta.style.height = "auto";
1396
+ ta.style.height = `${Math.min(ta.scrollHeight, 100)}px`;
1397
+ }, []);
1398
+ const handleEmojiSelect = useCallback4((emoji) => {
1399
+ const ta = textareaRef.current;
1400
+ if (!ta) {
1401
+ setText(text + emoji);
1402
+ return;
1403
+ }
1404
+ const start = ta.selectionStart;
1405
+ const end = ta.selectionEnd;
1406
+ const newText = text.slice(0, start) + emoji + text.slice(end);
1407
+ setText(newText);
1408
+ requestAnimationFrame(() => {
1409
+ ta.focus();
1410
+ ta.setSelectionRange(start + emoji.length, start + emoji.length);
1411
+ handleInput();
1412
+ });
1413
+ }, [text, setText, handleInput]);
1414
+ const handleFileChange = useCallback4((e) => {
1415
+ const files = e.target.files;
1416
+ if (!files) return;
1417
+ const previews = [];
1418
+ for (let i = 0; i < files.length; i++) {
1419
+ const file = files[i];
1420
+ previews.push({ file, previewUrl: file.type.startsWith("image/") ? URL.createObjectURL(file) : "" });
1421
+ }
1422
+ setAttachments((prev) => [...prev, ...previews]);
1423
+ if (fileInputRef.current) fileInputRef.current.value = "";
1424
+ }, []);
1425
+ const removeAttachment = useCallback4((index) => {
1426
+ setAttachments((prev) => {
1427
+ const next = [...prev];
1428
+ if (next[index].previewUrl) URL.revokeObjectURL(next[index].previewUrl);
1429
+ next.splice(index, 1);
1430
+ return next;
1431
+ });
1432
+ }, []);
1433
+ const insertFormatting = useCallback4((marker) => {
1434
+ const ta = textareaRef.current;
1435
+ if (!ta) return;
1436
+ const start = ta.selectionStart;
1437
+ const end = ta.selectionEnd;
1438
+ const sel = text.slice(start, end);
1439
+ if (sel) {
1440
+ setText(text.slice(0, start) + marker + sel + marker + text.slice(end));
1441
+ requestAnimationFrame(() => {
1442
+ ta.focus();
1443
+ ta.setSelectionRange(start + marker.length + sel.length + marker.length, start + marker.length + sel.length + marker.length);
1444
+ });
1445
+ }
1446
+ }, [text, setText]);
1447
+ const effectiveIdleAction = idleAction ?? (onAttach ? /* @__PURE__ */ jsx14(AudioRecorderButton, { onRecorded: (file) => onAttach(file) }) : void 0);
1448
+ const canSend = text.trim().length > 0 || attachments.length > 0;
1449
+ const remaining = maxLength ? maxLength - text.length : null;
1450
+ return (
1451
+ /* A barra é a superfície (cinza, largura cheia, sem raio) e o campo dentro é que arredonda —
1452
+ ordem do WhatsApp. Invertido, o pill arredondado ia até a borda da tela e os cantos
1453
+ descobriam o fundo branco da página, que lia como defeito. */
1454
+ /* @__PURE__ */ jsxs10("div", { className: cn(COMPOSER_BAR_CLASS, className), children: [
1455
+ quickReplies && quickReplies.length > 0 && /* @__PURE__ */ jsx14(
1456
+ "div",
1457
+ {
1458
+ className: cn(
1459
+ "mb-2 flex flex-nowrap gap-1 overflow-x-auto px-1 sm:flex-wrap sm:overflow-x-visible [scrollbar-width:none] [&::-webkit-scrollbar]:hidden",
1460
+ classNames?.quickReplies
1461
+ ),
1462
+ children: quickReplies.map((quickReply) => /* @__PURE__ */ jsx14(
1463
+ "button",
1464
+ {
1465
+ "data-cv-tooltip": quickReply.label,
1466
+ "aria-label": quickReply.label,
1467
+ type: "button",
1468
+ onClick: () => {
1469
+ setText(resolveQuickReply(quickReply, quickReplyVariables));
1470
+ textareaRef.current?.focus();
1471
+ },
1472
+ className: cn(QUICK_REPLY_PILL_CLASS, classNames?.quickReply),
1473
+ children: quickReply.label
1474
+ },
1475
+ quickReply.key
1476
+ ))
1477
+ }
1478
+ ),
1479
+ attachments.length > 0 && /* @__PURE__ */ jsx14("div", { className: "flex gap-2 px-1 pb-2 overflow-x-auto", children: attachments.map((a, i) => /* @__PURE__ */ jsxs10("div", { className: "relative flex-shrink-0", children: [
1480
+ a.previewUrl ? /* @__PURE__ */ jsx14("img", { src: a.previewUrl, alt: "", className: "w-16 h-16 object-cover rounded-lg border border-gray-200" }) : /* @__PURE__ */ jsx14("div", { className: "w-16 h-16 bg-gray-100 rounded-lg border border-gray-200 flex items-center justify-center", children: /* @__PURE__ */ jsxs10("svg", { width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "#9ca3af", strokeWidth: "1.5", children: [
1481
+ /* @__PURE__ */ jsx14("path", { d: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" }),
1482
+ /* @__PURE__ */ jsx14("polyline", { points: "14 2 14 8 20 8" })
1483
+ ] }) }),
1484
+ /* @__PURE__ */ jsx14("button", { "data-cv-tooltip": removeAttachmentLabel, "aria-label": removeAttachmentLabel, onClick: () => removeAttachment(i), className: "absolute -top-2 -right-2 w-5 h-5 bg-gray-600 text-white rounded-full flex items-center justify-center hover:bg-gray-800 text-xs", children: "\u2715" })
1485
+ ] }, i)) }),
1486
+ /* @__PURE__ */ jsxs10("div", { className: cn("flex items-end gap-1.5 rounded-xl bg-white px-3 py-2", classNames?.field), children: [
1487
+ showEmojiButton && /* @__PURE__ */ jsxs10("div", { className: "relative flex-shrink-0", children: [
1488
+ /* @__PURE__ */ jsx14("button", { "data-cv-tooltip": emojiLabel, onClick: () => setShowEmoji((v) => !v), className: "w-9 h-9 flex items-center justify-center rounded-full text-gray-500 hover:bg-gray-200 transition-colors", "aria-label": emojiLabel, children: /* @__PURE__ */ jsxs10("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
1489
+ /* @__PURE__ */ jsx14("circle", { cx: "12", cy: "12", r: "10" }),
1490
+ /* @__PURE__ */ jsx14("path", { d: "M8 14s1.5 2 4 2 4-2 4-2" }),
1491
+ /* @__PURE__ */ jsx14("circle", { cx: "9", cy: "9", r: "0.5", fill: "currentColor" }),
1492
+ /* @__PURE__ */ jsx14("circle", { cx: "15", cy: "9", r: "0.5", fill: "currentColor" })
1493
+ ] }) }),
1494
+ showEmoji && /* @__PURE__ */ jsx14("div", { className: "absolute bottom-full left-0 mb-2 z-10", children: /* @__PURE__ */ jsx14(EmojiPicker, { onSelect: handleEmojiSelect }) })
1495
+ ] }),
1496
+ /* @__PURE__ */ jsx14(
1497
+ "textarea",
1498
+ {
1499
+ ref: textareaRef,
1500
+ value: text,
1501
+ onChange: (e) => {
1502
+ setText(e.target.value);
1503
+ handleInput();
1504
+ },
1505
+ onKeyDown: handleKeyDown,
1506
+ placeholder,
1507
+ rows: 1,
1508
+ disabled,
1509
+ className: "flex-1 resize-none bg-transparent text-[15px] text-[#3b4a54] placeholder-[#8696a0] outline-none py-1.5 max-h-[100px] leading-relaxed",
1510
+ style: { fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" }
1511
+ }
1512
+ ),
1513
+ showAttachButton && /* @__PURE__ */ jsxs10(Fragment3, { children: [
1514
+ /* @__PURE__ */ jsx14("input", { ref: fileInputRef, type: "file", multiple: true, accept: acceptedFileTypes, onChange: handleFileChange, className: "hidden" }),
1515
+ /* @__PURE__ */ jsx14("button", { "data-cv-tooltip": attachLabel, onClick: () => fileInputRef.current?.click(), className: "w-9 h-9 flex items-center justify-center rounded-full text-gray-500 hover:bg-gray-200 flex-shrink-0 transition-colors", "aria-label": attachLabel, children: /* @__PURE__ */ jsx14("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: /* @__PURE__ */ jsx14("path", { d: "M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48" }) }) })
1516
+ ] }),
1517
+ !canSend && effectiveIdleAction ? /* @__PURE__ */ jsx14("div", { className: "flex-shrink-0", children: effectiveIdleAction }) : /* @__PURE__ */ jsx14(
1518
+ "button",
1519
+ {
1520
+ "data-cv-tooltip": sendLabel,
1521
+ onClick: sendMessage,
1522
+ disabled: !canSend || disabled,
1523
+ className: `w-10 h-10 flex items-center justify-center rounded-full flex-shrink-0 transition-all ${canSend && !disabled ? "bg-[#00a884] text-white hover:bg-[#06cf9c] shadow-sm" : "bg-gray-200 text-gray-400 cursor-not-allowed"}`,
1524
+ "aria-label": sendLabel,
1525
+ children: /* @__PURE__ */ jsx14("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "currentColor", children: /* @__PURE__ */ jsx14("path", { d: "M2.01 21L23 12 2.01 3 2 10l15 2-15 2z" }) })
1526
+ }
1527
+ )
1528
+ ] }),
1529
+ remaining !== null && /* @__PURE__ */ jsx14("div", { className: "flex justify-end mt-1 pr-1", children: /* @__PURE__ */ jsx14("span", { className: `text-xs ${remaining < 20 ? "text-red-500" : "text-gray-400"}`, children: remaining }) })
1530
+ ] })
1531
+ );
1532
+ };
1533
+
1534
+ // src/DateDivider.tsx
1535
+ import { jsx as jsx15 } from "react/jsx-runtime";
1536
+ function DateDivider({ iso, className, classNames }) {
1537
+ const { dateDivider } = useConversationLocales();
1538
+ return /* @__PURE__ */ jsx15("div", { className: cn("flex justify-center sticky top-0 z-10 my-2 pointer-events-none", classNames?.root, className), children: /* @__PURE__ */ jsx15(
1539
+ "span",
1540
+ {
1541
+ className: cn(
1542
+ "bg-white/95 dark:bg-gray-800/95 text-gray-600 dark:text-gray-300 text-xs font-medium px-3 py-1 rounded-lg shadow-sm",
1543
+ classNames?.label
1544
+ ),
1545
+ children: formatDividerLabel(iso, dateDivider)
1546
+ }
1547
+ ) });
1548
+ }
1549
+ function formatDividerLabel(iso, dateDivider) {
1550
+ const date = new Date(iso);
1551
+ const today = /* @__PURE__ */ new Date();
1552
+ const yesterday = new Date(today);
1553
+ yesterday.setDate(today.getDate() - 1);
1554
+ if (isSameDay(date, today)) return dateDivider.today;
1555
+ if (isSameDay(date, yesterday)) return dateDivider.yesterday;
1556
+ return date.toLocaleDateString("pt-BR", { day: "2-digit", month: "long", year: "numeric" });
1557
+ }
1558
+
1559
+ // src/lib/phone.ts
1560
+ function formatPhone(number) {
1561
+ const digits = number.replace(/\D/g, "");
1562
+ if (digits.length === 13) {
1563
+ return `+${digits.slice(0, 2)} (${digits.slice(2, 4)}) ${digits.slice(4, 9)}-${digits.slice(9, 13)}`;
1564
+ }
1565
+ if (digits.length === 12) {
1566
+ return `+${digits.slice(0, 2)} (${digits.slice(2, 4)}) ${digits.slice(4, 8)}-${digits.slice(8, 12)}`;
1567
+ }
1568
+ if (digits.length === 11) {
1569
+ return `(${digits.slice(0, 2)}) ${digits.slice(2, 7)}-${digits.slice(7, 11)}`;
1570
+ }
1571
+ if (digits.length === 10) {
1572
+ return `(${digits.slice(0, 2)}) ${digits.slice(2, 6)}-${digits.slice(6, 10)}`;
1573
+ }
1574
+ return number;
1575
+ }
1576
+ var COUNTRY_FLAG_BY_DIAL_CODE = {
1577
+ "55": "\u{1F1E7}\u{1F1F7}",
1578
+ "351": "\u{1F1F5}\u{1F1F9}",
1579
+ "34": "\u{1F1EA}\u{1F1F8}",
1580
+ "54": "\u{1F1E6}\u{1F1F7}",
1581
+ "56": "\u{1F1E8}\u{1F1F1}",
1582
+ "57": "\u{1F1E8}\u{1F1F4}",
1583
+ "52": "\u{1F1F2}\u{1F1FD}",
1584
+ "598": "\u{1F1FA}\u{1F1FE}",
1585
+ "595": "\u{1F1F5}\u{1F1FE}",
1586
+ "44": "\u{1F1EC}\u{1F1E7}",
1587
+ "49": "\u{1F1E9}\u{1F1EA}",
1588
+ "39": "\u{1F1EE}\u{1F1F9}",
1589
+ "33": "\u{1F1EB}\u{1F1F7}"
1590
+ };
1591
+ function phoneCountryFlag(number) {
1592
+ const digits = number.replace(/\D/g, "");
1593
+ for (const length of [3, 2, 1]) {
1594
+ const flag = COUNTRY_FLAG_BY_DIAL_CODE[digits.slice(0, length)];
1595
+ if (flag) return flag;
1596
+ }
1597
+ return "";
1598
+ }
1599
+ function phoneInitials(number) {
1600
+ const digits = number.replace(/\D/g, "");
1601
+ return digits.slice(-2);
1602
+ }
1603
+
1604
+ // src/conversationChannel.ts
1605
+ var CONVERSATION_CHANNEL = {
1606
+ WHATSAPP: "whatsapp",
1607
+ MESSENGER: "messenger",
1608
+ INSTAGRAM: "instagram",
1609
+ WEBCHAT: "webchat"
1610
+ };
1611
+ var DEFAULT_CONVERSATION_CHANNEL = CONVERSATION_CHANNEL.WHATSAPP;
1612
+ var REOPEN_MECHANISM = {
1613
+ TEMPLATE: "template",
1614
+ TAG: "tag",
1615
+ NONE: "none"
1616
+ };
1617
+ var HANDLE_KIND = {
1618
+ PHONE: "phone",
1619
+ USERNAME: "username",
1620
+ SESSION: "session"
1621
+ };
1622
+ var CHANNEL_CAPABILITIES = {
1623
+ [CONVERSATION_CHANNEL.WHATSAPP]: {
1624
+ label: "WhatsApp",
1625
+ icon: "\u{1F4AC}",
1626
+ hasSessionWindow: true,
1627
+ windowHours: 24,
1628
+ reopenMechanism: REOPEN_MECHANISM.TEMPLATE,
1629
+ handleKind: HANDLE_KIND.PHONE
1630
+ },
1631
+ [CONVERSATION_CHANNEL.MESSENGER]: {
1632
+ // Messenger também tem 24h, mas reabre com message tag — não com template aprovado.
1633
+ label: "Messenger",
1634
+ icon: "\u{1F4E8}",
1635
+ hasSessionWindow: true,
1636
+ windowHours: 24,
1637
+ reopenMechanism: REOPEN_MECHANISM.TAG,
1638
+ handleKind: HANDLE_KIND.USERNAME
1639
+ },
1640
+ [CONVERSATION_CHANNEL.INSTAGRAM]: {
1641
+ label: "Instagram",
1642
+ icon: "\u{1F4F7}",
1643
+ hasSessionWindow: true,
1644
+ windowHours: 24,
1645
+ reopenMechanism: REOPEN_MECHANISM.TAG,
1646
+ handleKind: HANDLE_KIND.USERNAME
1647
+ },
1648
+ [CONVERSATION_CHANNEL.WEBCHAT]: {
1649
+ // Chat próprio: sem intermediário, sem janela. Bloquear o composer aqui seria inventar limite.
1650
+ label: "Chat do site",
1651
+ icon: "\u{1F310}",
1652
+ hasSessionWindow: false,
1653
+ windowHours: 0,
1654
+ reopenMechanism: REOPEN_MECHANISM.NONE,
1655
+ handleKind: HANDLE_KIND.SESSION
1656
+ }
1657
+ };
1658
+ function capabilitiesOf(channel) {
1659
+ return CHANNEL_CAPABILITIES[channel ?? DEFAULT_CONVERSATION_CHANNEL];
1660
+ }
1661
+ var CHANNEL_FILTER_ALL = "all";
1662
+ function channelFiltersFor(conversations) {
1663
+ const present = new Set(
1664
+ conversations.map((conversation) => conversation.channel ?? DEFAULT_CONVERSATION_CHANNEL)
1665
+ );
1666
+ if (present.size < 2) return [];
1667
+ const ordered = Object.keys(CHANNEL_CAPABILITIES).filter((channel) => present.has(channel));
1668
+ return [
1669
+ { value: CHANNEL_FILTER_ALL, label: "Todos" },
1670
+ ...ordered.map((channel) => ({ value: channel, label: CHANNEL_CAPABILITIES[channel].label }))
1671
+ ];
1672
+ }
1673
+ function formatContactHandle(params) {
1674
+ const { handleKind } = capabilitiesOf(params.channel);
1675
+ if (handleKind === HANDLE_KIND.PHONE) return formatPhone(params.handle);
1676
+ if (handleKind === HANDLE_KIND.USERNAME) return params.handle.startsWith("@") ? params.handle : `@${params.handle}`;
1677
+ return `Visitante ${params.handle.slice(-6)}`;
1678
+ }
1679
+ function contactFlag(params) {
1680
+ return capabilitiesOf(params.channel).handleKind === HANDLE_KIND.PHONE ? phoneCountryFlag(params.handle) : "";
1681
+ }
1682
+
1683
+ // src/hooks/useAsyncResource.ts
1684
+ import { useCallback as useCallback5, useEffect as useEffect3, useRef as useRef4, useState as useState9 } from "react";
1685
+ function useAsyncResource(fetcher, deps) {
1686
+ const [data, setData] = useState9(void 0);
1687
+ const [loading, setLoading] = useState9(false);
1688
+ const [error, setError] = useState9(void 0);
1689
+ const requestIdRef = useRef4(0);
1690
+ const load = useCallback5(async () => {
1691
+ const requestId = ++requestIdRef.current;
1692
+ setLoading(true);
1693
+ setError(void 0);
1694
+ try {
1695
+ const result = await fetcher();
1696
+ if (requestId === requestIdRef.current) setData(result);
1697
+ } catch (err) {
1698
+ if (requestId === requestIdRef.current) setError(err instanceof Error ? err : new Error(String(err)));
1699
+ } finally {
1700
+ if (requestId === requestIdRef.current) setLoading(false);
1701
+ }
1702
+ }, deps);
1703
+ useEffect3(() => {
1704
+ load();
1705
+ }, [load]);
1706
+ return { data, loading, error, refetch: load };
1707
+ }
1708
+
1709
+ // src/lib/paginated.ts
1710
+ function conversationsOf(result) {
1711
+ return Array.isArray(result) ? result : result.conversations;
1712
+ }
1713
+ function documentsOf(result) {
1714
+ return Array.isArray(result) ? result : result.documents;
1715
+ }
1716
+ function totalOf(result) {
1717
+ return Array.isArray(result) ? result.length : result.total;
1718
+ }
1719
+
1720
+ // src/hooks/useConversationDocuments.ts
1721
+ function useConversationDocuments(conversationId, params) {
1722
+ const context = useConversations();
1723
+ if (!context) {
1724
+ throw new Error("useConversationDocuments requires an ancestor <ConversationsProvider>");
1725
+ }
1726
+ const { api } = context;
1727
+ const { data, loading, error, refetch } = useAsyncResource(
1728
+ () => conversationId ? api.getDocuments(conversationId, params) : Promise.resolve([]),
1729
+ [conversationId, params?.search, params?.page, params?.limit, params?.source, params?.sortDirection]
1730
+ );
1731
+ if (data === void 0) {
1732
+ return { documents: [], total: 0, loading, error, refetch };
1733
+ }
1734
+ return { documents: documentsOf(data), total: totalOf(data), loading, error, refetch };
1735
+ }
1736
+
1737
+ // src/ConversationDocumentsPanel.tsx
1738
+ import { useState as useState10 } from "react";
1739
+ import { ArrowUpDown, Bot, Download, Eye, Users } from "lucide-react";
1740
+
1741
+ // src/pagination.constant.ts
1742
+ var PAGINATION_LABELS = {
1743
+ first: "Primeira p\xE1gina",
1744
+ previous: "P\xE1gina anterior",
1745
+ next: "Pr\xF3xima p\xE1gina",
1746
+ last: "\xDAltima p\xE1gina"
1747
+ };
1748
+
1749
+ // src/ConversationDocumentsPanel.tsx
1750
+ import { jsx as jsx16, jsxs as jsxs11 } from "react/jsx-runtime";
1751
+ var DOCUMENT_SOURCE_FILTER = {
1752
+ ALL: "all",
1753
+ CUSTOMER: "customer",
1754
+ TEAM: "team"
1755
+ };
1756
+ var TEAM_SOURCES = /* @__PURE__ */ new Set(["agent", "bot"]);
1757
+ var DEFAULT_CONVERSATION_DOCUMENTS_LABELS = {
1758
+ toggle: "\u{1F4CE} Arquivos",
1759
+ title: "Arquivos da conversa",
1760
+ noResults: "Nenhum arquivo encontrado para os filtros aplicados",
1761
+ view: "Visualizar",
1762
+ sourceFilterAll: "Todas as origens",
1763
+ sourceFilterCustomer: "Cliente",
1764
+ sourceFilterTeam: "Equipe",
1765
+ sortMostRecent: "Mais recentes",
1766
+ sortOldest: "Mais antigos",
1767
+ clearFilters: "Limpar filtros",
1768
+ selectAll: "Selecionar todos desta p\xE1gina",
1769
+ downloadSelected: (count) => `Baixar ${count} selecionado${count === 1 ? "" : "s"} (.zip)`,
1770
+ archiveFailed: "N\xE3o foi poss\xEDvel montar o arquivo compactado.",
1771
+ total: (count) => `${count} arquivo${count === 1 ? "" : "s"}`,
1772
+ page: (current, last) => `${current} / ${last}`,
1773
+ searchPlaceholder: "Buscar por nome do arquivo",
1774
+ empty: "Nenhum arquivo nesta conversa.",
1775
+ loading: "Carregando arquivos\u2026",
1776
+ failure: "N\xE3o foi poss\xEDvel carregar os arquivos.",
1777
+ download: "Baixar"
1778
+ };
1779
+ var DEFAULT_PER_PAGE = 10;
1780
+ function ConversationDocumentsPanel({
1781
+ conversationId,
1782
+ open,
1783
+ perPage = DEFAULT_PER_PAGE,
1784
+ labels: labelsOverride,
1785
+ className,
1786
+ classNames
1787
+ }) {
1788
+ const labels = { ...DEFAULT_CONVERSATION_DOCUMENTS_LABELS, ...labelsOverride };
1789
+ const context = useConversations();
1790
+ const [search, setSearch] = useState10("");
1791
+ const [sourceFilter, setSourceFilter] = useState10(DOCUMENT_SOURCE_FILTER.ALL);
1792
+ const [sortDirection, setSortDirection] = useState10("desc");
1793
+ const [page, setPage] = useState10(1);
1794
+ const [selectedIds, setSelectedIds] = useState10([]);
1795
+ const [archiveError, setArchiveError] = useState10(false);
1796
+ const hasFilters = search !== "" || sourceFilter !== DOCUMENT_SOURCE_FILTER.ALL || sortDirection !== "desc";
1797
+ const { documents, total, loading, error } = useConversationDocuments(open ? conversationId : void 0, {
1798
+ search,
1799
+ page,
1800
+ limit: perPage,
1801
+ sortDirection,
1802
+ ...sourceFilter === DOCUMENT_SOURCE_FILTER.ALL ? {} : { source: sourceFilter }
1803
+ });
1804
+ const lastPage = Math.max(1, Math.ceil(total / perPage));
1805
+ function applyFilter(change) {
1806
+ change();
1807
+ setPage(1);
1808
+ }
1809
+ async function handleOpen(uploadId, disposition) {
1810
+ const url = await context?.api.getDocumentUrl(uploadId, disposition);
1811
+ if (url) window.open(url, "_blank", "noopener,noreferrer");
1812
+ }
1813
+ const canArchive = typeof context?.api.downloadDocumentsArchive === "function";
1814
+ const pageIds = documents.map((document2) => document2.id);
1815
+ const allOnPageSelected = pageIds.length > 0 && pageIds.every((id) => selectedIds.includes(id));
1816
+ function toggleSelected(uploadId) {
1817
+ setArchiveError(false);
1818
+ setSelectedIds(
1819
+ (current) => current.includes(uploadId) ? current.filter((id) => id !== uploadId) : [...current, uploadId]
1820
+ );
1821
+ }
1822
+ function toggleAllOnPage() {
1823
+ setArchiveError(false);
1824
+ setSelectedIds(
1825
+ (current) => allOnPageSelected ? current.filter((id) => !pageIds.includes(id)) : [...current, ...pageIds.filter((id) => !current.includes(id))]
1826
+ );
1827
+ }
1828
+ async function handleDownloadSelected() {
1829
+ const archive = context?.api.downloadDocumentsArchive;
1830
+ if (!archive || selectedIds.length === 0) return;
1831
+ setArchiveError(false);
1832
+ try {
1833
+ const blob = await archive(conversationId, selectedIds);
1834
+ const url = URL.createObjectURL(blob);
1835
+ const anchor = document.createElement("a");
1836
+ anchor.href = url;
1837
+ anchor.download = `conversa-${conversationId}.zip`;
1838
+ anchor.click();
1839
+ URL.revokeObjectURL(url);
1840
+ setSelectedIds([]);
1841
+ } catch {
1842
+ setArchiveError(true);
1843
+ }
1844
+ }
1845
+ if (!open) return null;
1846
+ return /* @__PURE__ */ jsx16("div", { className: cn("border-b", classNames?.root, className), children: /* @__PURE__ */ jsxs11("section", { className: cn("px-4 py-3", classNames?.body), children: [
1847
+ /* @__PURE__ */ jsx16("p", { className: cn("mb-2 text-sm font-medium", classNames?.title), children: labels.title }),
1848
+ /* @__PURE__ */ jsxs11("div", { className: cn("mb-2 flex flex-wrap items-center gap-2 border-b pb-2", classNames?.filters), children: [
1849
+ /* @__PURE__ */ jsx16(
1850
+ "input",
1851
+ {
1852
+ type: "search",
1853
+ value: search,
1854
+ onChange: (event) => applyFilter(() => setSearch(event.target.value)),
1855
+ placeholder: labels.searchPlaceholder,
1856
+ "aria-label": labels.searchPlaceholder,
1857
+ className: cn("w-full rounded-md border px-3 py-2 text-sm sm:w-52", classNames?.search)
1858
+ }
1859
+ ),
1860
+ /* @__PURE__ */ jsxs11(
1861
+ "select",
1862
+ {
1863
+ value: sourceFilter,
1864
+ onChange: (event) => applyFilter(() => setSourceFilter(event.target.value)),
1865
+ "aria-label": labels.sourceFilterAll,
1866
+ className: cn("w-full rounded-md border px-2 py-2 text-sm sm:w-36", classNames?.sourceSelect),
1867
+ children: [
1868
+ /* @__PURE__ */ jsx16("option", { value: DOCUMENT_SOURCE_FILTER.ALL, children: labels.sourceFilterAll }),
1869
+ /* @__PURE__ */ jsx16("option", { value: DOCUMENT_SOURCE_FILTER.CUSTOMER, children: labels.sourceFilterCustomer }),
1870
+ /* @__PURE__ */ jsx16("option", { value: DOCUMENT_SOURCE_FILTER.TEAM, children: labels.sourceFilterTeam })
1871
+ ]
1872
+ }
1873
+ ),
1874
+ /* @__PURE__ */ jsxs11(
1875
+ "button",
1876
+ {
1877
+ "data-cv-tooltip": sortDirection === "desc" ? labels.sortMostRecent : labels.sortOldest,
1878
+ "aria-label": sortDirection === "desc" ? labels.sortMostRecent : labels.sortOldest,
1879
+ type: "button",
1880
+ onClick: () => applyFilter(() => setSortDirection(sortDirection === "desc" ? "asc" : "desc")),
1881
+ className: cn("cv-header-action inline-flex items-center gap-1", classNames?.sortButton),
1882
+ children: [
1883
+ /* @__PURE__ */ jsx16(ArrowUpDown, { size: 14 }),
1884
+ sortDirection === "desc" ? labels.sortMostRecent : labels.sortOldest
1885
+ ]
1886
+ }
1887
+ ),
1888
+ hasFilters ? /* @__PURE__ */ jsx16(
1889
+ "button",
1890
+ {
1891
+ "data-cv-tooltip": labels.clearFilters,
1892
+ "aria-label": labels.clearFilters,
1893
+ type: "button",
1894
+ onClick: () => applyFilter(() => {
1895
+ setSearch("");
1896
+ setSourceFilter(DOCUMENT_SOURCE_FILTER.ALL);
1897
+ setSortDirection("desc");
1898
+ }),
1899
+ className: cn("cv-header-action", classNames?.clearButton),
1900
+ children: labels.clearFilters
1901
+ }
1902
+ ) : null
1903
+ ] }),
1904
+ loading ? /* @__PURE__ */ jsx16("p", { className: cn("text-xs text-gray-500", classNames?.status), children: labels.loading }) : null,
1905
+ error ? /* @__PURE__ */ jsx16("p", { role: "alert", className: cn("text-xs text-red-600 dark:text-red-400", classNames?.status), children: labels.failure }) : null,
1906
+ !loading && !error && documents.length === 0 ? /* @__PURE__ */ jsx16("p", { className: cn("text-xs text-gray-500", classNames?.status), children: hasFilters ? labels.noResults : labels.empty }) : null,
1907
+ canArchive && documents.length > 0 ? /* @__PURE__ */ jsxs11("div", { className: cn("mb-2 flex flex-wrap items-center gap-3 text-xs", classNames?.selectionBar), children: [
1908
+ /* @__PURE__ */ jsxs11("label", { className: "inline-flex items-center gap-1.5", children: [
1909
+ /* @__PURE__ */ jsx16(
1910
+ "input",
1911
+ {
1912
+ type: "checkbox",
1913
+ checked: allOnPageSelected,
1914
+ onChange: toggleAllOnPage,
1915
+ className: cn(classNames?.checkbox)
1916
+ }
1917
+ ),
1918
+ labels.selectAll
1919
+ ] }),
1920
+ selectedIds.length > 0 ? /* @__PURE__ */ jsx16("button", { "data-cv-tooltip": labels.downloadSelected(selectedIds.length), "aria-label": labels.downloadSelected(selectedIds.length), type: "button", onClick: () => void handleDownloadSelected(), className: "cv-header-action", children: labels.downloadSelected(selectedIds.length) }) : null,
1921
+ archiveError ? /* @__PURE__ */ jsx16("span", { role: "alert", className: "text-red-600 dark:text-red-400", children: labels.archiveFailed }) : null
1922
+ ] }) : null,
1923
+ /* @__PURE__ */ jsx16("ul", { className: cn("space-y-2", classNames?.list), children: documents.map((document2) => {
1924
+ const isFromCustomer = !TEAM_SOURCES.has(document2.source);
1925
+ const SourceIcon = isFromCustomer ? Users : Bot;
1926
+ return /* @__PURE__ */ jsxs11(
1927
+ "li",
1928
+ {
1929
+ className: cn(
1930
+ "flex items-center justify-between gap-2 rounded-lg border px-3 py-2 dark:border-gray-700",
1931
+ classNames?.item
1932
+ ),
1933
+ children: [
1934
+ /* @__PURE__ */ jsxs11("div", { className: "flex min-w-0 flex-1 items-center gap-2", children: [
1935
+ canArchive ? /* @__PURE__ */ jsx16(
1936
+ "input",
1937
+ {
1938
+ type: "checkbox",
1939
+ checked: selectedIds.includes(document2.id),
1940
+ onChange: () => toggleSelected(document2.id),
1941
+ "aria-label": `${labels.download}: ${document2.filename}`,
1942
+ className: cn("shrink-0", classNames?.checkbox)
1943
+ }
1944
+ ) : null,
1945
+ /* @__PURE__ */ jsx16(FileIcon, { filename: document2.filename, mimeType: document2.mimeType }),
1946
+ /* @__PURE__ */ jsxs11("div", { className: "min-w-0 flex-1", children: [
1947
+ /* @__PURE__ */ jsxs11(
1948
+ "div",
1949
+ {
1950
+ className: cn(
1951
+ "mb-0.5 flex items-center gap-1 text-[11px] font-medium",
1952
+ isFromCustomer ? "text-blue-600 dark:text-blue-400" : "text-emerald-600 dark:text-emerald-400",
1953
+ classNames?.sourceBadge
1954
+ ),
1955
+ children: [
1956
+ /* @__PURE__ */ jsx16(SourceIcon, { size: 11 }),
1957
+ isFromCustomer ? labels.sourceFilterCustomer : labels.sourceFilterTeam
1958
+ ]
1959
+ }
1960
+ ),
1961
+ /* @__PURE__ */ jsx16(
1962
+ "div",
1963
+ {
1964
+ className: cn("truncate text-sm font-medium", classNames?.filename),
1965
+ "data-cv-tooltip": document2.filename,
1966
+ children: document2.filename
1967
+ }
1968
+ ),
1969
+ /* @__PURE__ */ jsxs11("div", { className: cn("text-xs text-gray-500 dark:text-gray-400", classNames?.meta), children: [
1970
+ formatDateTime(document2.linkedAt),
1971
+ " \xB7 ",
1972
+ formatFileSize(document2.sizeBytes)
1973
+ ] })
1974
+ ] })
1975
+ ] }),
1976
+ /* @__PURE__ */ jsxs11("div", { className: "flex shrink-0 gap-1", children: [
1977
+ /* @__PURE__ */ jsx16(
1978
+ "button",
1979
+ {
1980
+ type: "button",
1981
+ onClick: () => void handleOpen(document2.id, "inline"),
1982
+ "data-cv-tooltip": labels.view,
1983
+ "aria-label": `${labels.view}: ${document2.filename}`,
1984
+ className: cn("cv-header-icon", classNames?.viewButton),
1985
+ children: /* @__PURE__ */ jsx16(Eye, { size: 14 })
1986
+ }
1987
+ ),
1988
+ /* @__PURE__ */ jsx16(
1989
+ "button",
1990
+ {
1991
+ type: "button",
1992
+ onClick: () => void handleOpen(document2.id, "attachment"),
1993
+ "data-cv-tooltip": labels.download,
1994
+ "aria-label": `${labels.download}: ${document2.filename}`,
1995
+ className: cn("cv-header-icon", classNames?.downloadButton),
1996
+ children: /* @__PURE__ */ jsx16(Download, { size: 14 })
1997
+ }
1998
+ )
1999
+ ] })
2000
+ ]
2001
+ },
2002
+ document2.id
2003
+ );
2004
+ }) }),
2005
+ total > perPage ? /* @__PURE__ */ jsxs11(
2006
+ "div",
2007
+ {
2008
+ className: cn(
2009
+ "mt-2 flex items-center justify-between border-t pt-2 text-xs dark:border-gray-700",
2010
+ classNames?.pagination
2011
+ ),
2012
+ children: [
2013
+ /* @__PURE__ */ jsx16("span", { className: "text-gray-400", children: labels.total(total) }),
2014
+ /* @__PURE__ */ jsxs11("div", { className: "flex items-center gap-2", children: [
2015
+ /* @__PURE__ */ jsx16(
2016
+ "button",
2017
+ {
2018
+ type: "button",
2019
+ onClick: () => setPage(page - 1),
2020
+ disabled: page <= 1,
2021
+ "aria-label": PAGINATION_LABELS.previous,
2022
+ "data-cv-tooltip": PAGINATION_LABELS.previous,
2023
+ className: "cv-header-icon disabled:opacity-40",
2024
+ children: "\u2039"
2025
+ }
2026
+ ),
2027
+ /* @__PURE__ */ jsx16("span", { className: "text-gray-500", children: labels.page(page, lastPage) }),
2028
+ /* @__PURE__ */ jsx16(
2029
+ "button",
2030
+ {
2031
+ type: "button",
2032
+ onClick: () => setPage(page + 1),
2033
+ disabled: page >= lastPage,
2034
+ "aria-label": PAGINATION_LABELS.next,
2035
+ "data-cv-tooltip": PAGINATION_LABELS.next,
2036
+ className: "cv-header-icon disabled:opacity-40",
2037
+ children: "\u203A"
2038
+ }
2039
+ )
2040
+ ] })
2041
+ ]
2042
+ }
2043
+ ) : null
2044
+ ] }) });
2045
+ }
2046
+
2047
+ // src/DocumentsLibrary.tsx
2048
+ import { useEffect as useEffect4, useRef as useRef5, useState as useState11 } from "react";
2049
+ import { ArrowUpDown as ArrowUpDown2, Bot as Bot2, Download as Download2, Eye as Eye2, MessageSquare, Upload, Users as Users2 } from "lucide-react";
2050
+ import { Fragment as Fragment4, jsx as jsx17, jsxs as jsxs12 } from "react/jsx-runtime";
2051
+ var DEFAULT_DOCUMENTS_LIBRARY_LABELS = {
2052
+ title: "Documentos",
2053
+ searchPlaceholder: "Buscar por nome do arquivo ou telefone",
2054
+ empty: "Nenhum arquivo trocado ainda.",
2055
+ noResults: "Nenhum arquivo encontrado para os filtros aplicados",
2056
+ loading: "Carregando arquivos\u2026",
2057
+ failure: "N\xE3o foi poss\xEDvel carregar os arquivos.",
2058
+ view: "Visualizar",
2059
+ download: "Baixar",
2060
+ openConversation: "Abrir conversa",
2061
+ sourceFilterAll: "Todas as origens",
2062
+ sourceFilterCustomer: "Cliente",
2063
+ sourceFilterTeam: "Equipe",
2064
+ sortMostRecent: "Mais recentes",
2065
+ sortOldest: "Mais antigos",
2066
+ clearFilters: "Limpar filtros",
2067
+ upload: "Enviar documento",
2068
+ uploadError: "N\xE3o foi poss\xEDvel enviar o arquivo.",
2069
+ total: (count) => `${count} arquivo${count === 1 ? "" : "s"}`,
2070
+ page: (current, last) => `${current} / ${last}`
2071
+ };
2072
+ var TEAM_SOURCES2 = /* @__PURE__ */ new Set(["agent", "bot"]);
2073
+ var DEFAULT_PER_PAGE2 = 20;
2074
+ function DocumentsLibrary({
2075
+ perPage = DEFAULT_PER_PAGE2,
2076
+ onOpenConversation,
2077
+ labels: labelsOverride,
2078
+ className,
2079
+ classNames
2080
+ }) {
2081
+ const labels = { ...DEFAULT_DOCUMENTS_LIBRARY_LABELS, ...labelsOverride };
2082
+ const context = useConversations();
2083
+ const [search, setSearch] = useState11("");
2084
+ const [sourceFilter, setSourceFilter] = useState11(DOCUMENT_SOURCE_FILTER.ALL);
2085
+ const [sortDirection, setSortDirection] = useState11("desc");
2086
+ const [page, setPage] = useState11(1);
2087
+ const [documents, setDocuments] = useState11([]);
2088
+ const [total, setTotal] = useState11(0);
2089
+ const [loading, setLoading] = useState11(false);
2090
+ const [failed, setFailed] = useState11(false);
2091
+ const [uploading, setUploading] = useState11(false);
2092
+ const [uploadFailed, setUploadFailed] = useState11(false);
2093
+ const [reloadToken, setReloadToken] = useState11(0);
2094
+ const fileInputRef = useRef5(null);
2095
+ const hasFilters = search !== "" || sourceFilter !== DOCUMENT_SOURCE_FILTER.ALL || sortDirection !== "desc";
2096
+ const lastPage = Math.max(1, Math.ceil(total / perPage));
2097
+ const fetchAll = context?.api.getAllDocuments;
2098
+ const uploadDocument = context?.api.uploadDocument;
2099
+ useEffect4(() => {
2100
+ if (!fetchAll) return;
2101
+ let active = true;
2102
+ setLoading(true);
2103
+ setFailed(false);
2104
+ void fetchAll({
2105
+ search,
2106
+ page,
2107
+ limit: perPage,
2108
+ sortDirection,
2109
+ ...sourceFilter === DOCUMENT_SOURCE_FILTER.ALL ? {} : { source: sourceFilter }
2110
+ }).then((result) => {
2111
+ if (!active) return;
2112
+ setDocuments(result.documents);
2113
+ setTotal(result.total);
2114
+ }).catch(() => {
2115
+ if (active) setFailed(true);
2116
+ }).finally(() => {
2117
+ if (active) setLoading(false);
2118
+ });
2119
+ return () => {
2120
+ active = false;
2121
+ };
2122
+ }, [fetchAll, search, sourceFilter, sortDirection, page, perPage, reloadToken]);
2123
+ function applyFilter(change) {
2124
+ change();
2125
+ setPage(1);
2126
+ }
2127
+ async function handleOpen(uploadId, disposition) {
2128
+ const url = await context?.api.getDocumentUrl(uploadId, disposition);
2129
+ if (url) window.open(url, "_blank", "noopener,noreferrer");
2130
+ }
2131
+ async function handleUpload(file) {
2132
+ if (!uploadDocument) return;
2133
+ setUploading(true);
2134
+ setUploadFailed(false);
2135
+ try {
2136
+ await uploadDocument(file);
2137
+ setReloadToken((token) => token + 1);
2138
+ } catch {
2139
+ setUploadFailed(true);
2140
+ } finally {
2141
+ setUploading(false);
2142
+ }
2143
+ }
2144
+ if (!fetchAll) return null;
2145
+ return /* @__PURE__ */ jsxs12("div", { className: cn("space-y-3", classNames?.root, className), children: [
2146
+ /* @__PURE__ */ jsx17("h2", { className: cn("text-lg font-semibold", classNames?.title), children: labels.title }),
2147
+ /* @__PURE__ */ jsxs12("div", { className: cn("flex flex-wrap items-center gap-2", classNames?.filters), children: [
2148
+ /* @__PURE__ */ jsx17(
2149
+ "input",
2150
+ {
2151
+ type: "search",
2152
+ value: search,
2153
+ onChange: (event) => applyFilter(() => setSearch(event.target.value)),
2154
+ placeholder: labels.searchPlaceholder,
2155
+ "aria-label": labels.searchPlaceholder,
2156
+ className: cn("w-full rounded-md border px-3 py-2 text-sm sm:w-64", classNames?.search)
2157
+ }
2158
+ ),
2159
+ /* @__PURE__ */ jsxs12(
2160
+ "select",
2161
+ {
2162
+ value: sourceFilter,
2163
+ onChange: (event) => applyFilter(() => setSourceFilter(event.target.value)),
2164
+ "aria-label": labels.sourceFilterAll,
2165
+ className: cn("w-full rounded-md border px-2 py-2 text-sm sm:w-40", classNames?.sourceSelect),
2166
+ children: [
2167
+ /* @__PURE__ */ jsx17("option", { value: DOCUMENT_SOURCE_FILTER.ALL, children: labels.sourceFilterAll }),
2168
+ /* @__PURE__ */ jsx17("option", { value: DOCUMENT_SOURCE_FILTER.CUSTOMER, children: labels.sourceFilterCustomer }),
2169
+ /* @__PURE__ */ jsx17("option", { value: DOCUMENT_SOURCE_FILTER.TEAM, children: labels.sourceFilterTeam })
2170
+ ]
2171
+ }
2172
+ ),
2173
+ /* @__PURE__ */ jsxs12(
2174
+ "button",
2175
+ {
2176
+ "data-cv-tooltip": sortDirection === "desc" ? labels.sortMostRecent : labels.sortOldest,
2177
+ "aria-label": sortDirection === "desc" ? labels.sortMostRecent : labels.sortOldest,
2178
+ type: "button",
2179
+ onClick: () => applyFilter(() => setSortDirection(sortDirection === "desc" ? "asc" : "desc")),
2180
+ className: cn("cv-header-action inline-flex items-center gap-1", classNames?.sortButton),
2181
+ children: [
2182
+ /* @__PURE__ */ jsx17(ArrowUpDown2, { size: 14 }),
2183
+ sortDirection === "desc" ? labels.sortMostRecent : labels.sortOldest
2184
+ ]
2185
+ }
2186
+ ),
2187
+ hasFilters ? /* @__PURE__ */ jsx17(
2188
+ "button",
2189
+ {
2190
+ "data-cv-tooltip": labels.clearFilters,
2191
+ "aria-label": labels.clearFilters,
2192
+ type: "button",
2193
+ onClick: () => applyFilter(() => {
2194
+ setSearch("");
2195
+ setSourceFilter(DOCUMENT_SOURCE_FILTER.ALL);
2196
+ setSortDirection("desc");
2197
+ }),
2198
+ className: cn("cv-header-action", classNames?.clearButton),
2199
+ children: labels.clearFilters
2200
+ }
2201
+ ) : null,
2202
+ uploadDocument ? /* @__PURE__ */ jsxs12(Fragment4, { children: [
2203
+ /* @__PURE__ */ jsx17(
2204
+ "input",
2205
+ {
2206
+ ref: fileInputRef,
2207
+ type: "file",
2208
+ hidden: true,
2209
+ onChange: (event) => {
2210
+ const file = event.target.files?.[0];
2211
+ event.target.value = "";
2212
+ if (file) void handleUpload(file);
2213
+ }
2214
+ }
2215
+ ),
2216
+ /* @__PURE__ */ jsxs12(
2217
+ "button",
2218
+ {
2219
+ "data-cv-tooltip": labels.upload,
2220
+ "aria-label": labels.upload,
2221
+ type: "button",
2222
+ onClick: () => fileInputRef.current?.click(),
2223
+ disabled: uploading,
2224
+ className: "cv-header-action ml-auto inline-flex items-center gap-1 disabled:opacity-40",
2225
+ children: [
2226
+ /* @__PURE__ */ jsx17(Upload, { size: 14, "aria-hidden": "true" }),
2227
+ labels.upload
2228
+ ]
2229
+ }
2230
+ )
2231
+ ] }) : null
2232
+ ] }),
2233
+ loading ? /* @__PURE__ */ jsx17("p", { className: cn("text-sm text-gray-500", classNames?.status), children: labels.loading }) : null,
2234
+ failed ? /* @__PURE__ */ jsx17("p", { role: "alert", className: cn("text-sm text-red-600 dark:text-red-400", classNames?.status), children: labels.failure }) : null,
2235
+ uploadFailed ? /* @__PURE__ */ jsx17("p", { role: "alert", className: cn("text-sm text-red-600 dark:text-red-400", classNames?.status), children: labels.uploadError }) : null,
2236
+ !loading && !failed && documents.length === 0 ? /* @__PURE__ */ jsx17("p", { className: cn("text-sm text-gray-500", classNames?.status), children: hasFilters ? labels.noResults : labels.empty }) : null,
2237
+ /* @__PURE__ */ jsx17("ul", { className: cn("space-y-2", classNames?.list), children: documents.map((document2) => {
2238
+ const isFromCustomer = !TEAM_SOURCES2.has(document2.source);
2239
+ const SourceIcon = isFromCustomer ? Users2 : Bot2;
2240
+ return /* @__PURE__ */ jsxs12(
2241
+ "li",
2242
+ {
2243
+ className: cn(
2244
+ "flex items-center justify-between gap-3 rounded-lg border px-3 py-2 dark:border-gray-700",
2245
+ classNames?.item
2246
+ ),
2247
+ children: [
2248
+ /* @__PURE__ */ jsxs12("div", { className: "flex min-w-0 flex-1 items-center gap-3", children: [
2249
+ /* @__PURE__ */ jsx17(FileIcon, { filename: document2.filename, mimeType: document2.mimeType }),
2250
+ /* @__PURE__ */ jsxs12("div", { className: "min-w-0 flex-1", children: [
2251
+ /* @__PURE__ */ jsx17("div", { className: cn("truncate text-sm font-medium", classNames?.filename), "data-cv-tooltip": document2.filename, children: document2.filename }),
2252
+ /* @__PURE__ */ jsxs12("div", { className: cn("flex flex-wrap items-center gap-x-2 text-xs text-gray-500", classNames?.meta), children: [
2253
+ /* @__PURE__ */ jsxs12("span", { className: "inline-flex items-center gap-1", children: [
2254
+ /* @__PURE__ */ jsx17(SourceIcon, { size: 11 }),
2255
+ isFromCustomer ? labels.sourceFilterCustomer : labels.sourceFilterTeam
2256
+ ] }),
2257
+ /* @__PURE__ */ jsx17("span", { children: "\xB7" }),
2258
+ /* @__PURE__ */ jsx17("span", { children: formatDateTime(document2.linkedAt) }),
2259
+ /* @__PURE__ */ jsx17("span", { children: "\xB7" }),
2260
+ /* @__PURE__ */ jsx17("span", { children: formatFileSize(document2.sizeBytes) })
2261
+ ] })
2262
+ ] })
2263
+ ] }),
2264
+ onOpenConversation ? /* @__PURE__ */ jsxs12(
2265
+ "button",
2266
+ {
2267
+ type: "button",
2268
+ onClick: () => onOpenConversation(document2.conversationId),
2269
+ "data-cv-tooltip": labels.openConversation,
2270
+ "aria-label": labels.openConversation,
2271
+ className: cn("cv-header-action inline-flex shrink-0 items-center gap-1", classNames?.conversationLink),
2272
+ children: [
2273
+ /* @__PURE__ */ jsx17(MessageSquare, { size: 12 }),
2274
+ formatPhone(document2.conversationId)
2275
+ ]
2276
+ }
2277
+ ) : /* @__PURE__ */ jsx17("span", { className: cn("shrink-0 text-xs text-gray-500", classNames?.conversationLink), children: formatPhone(document2.conversationId) }),
2278
+ /* @__PURE__ */ jsxs12("div", { className: "flex shrink-0 gap-1", children: [
2279
+ /* @__PURE__ */ jsx17(
2280
+ "button",
2281
+ {
2282
+ type: "button",
2283
+ onClick: () => void handleOpen(document2.id, "inline"),
2284
+ "data-cv-tooltip": labels.view,
2285
+ "aria-label": `${labels.view}: ${document2.filename}`,
2286
+ className: "cv-header-icon",
2287
+ children: /* @__PURE__ */ jsx17(Eye2, { size: 14 })
2288
+ }
2289
+ ),
2290
+ /* @__PURE__ */ jsx17(
2291
+ "button",
2292
+ {
2293
+ type: "button",
2294
+ onClick: () => void handleOpen(document2.id, "attachment"),
2295
+ "data-cv-tooltip": labels.download,
2296
+ "aria-label": `${labels.download}: ${document2.filename}`,
2297
+ className: "cv-header-icon",
2298
+ children: /* @__PURE__ */ jsx17(Download2, { size: 14 })
2299
+ }
2300
+ )
2301
+ ] })
2302
+ ]
2303
+ },
2304
+ `${document2.conversationId}:${document2.id}`
2305
+ );
2306
+ }) }),
2307
+ total > perPage ? /* @__PURE__ */ jsxs12("div", { className: cn("flex items-center justify-between border-t pt-2 text-xs dark:border-gray-700", classNames?.pagination), children: [
2308
+ /* @__PURE__ */ jsx17("span", { className: "text-gray-400", children: labels.total(total) }),
2309
+ /* @__PURE__ */ jsxs12("div", { className: "flex items-center gap-2", children: [
2310
+ /* @__PURE__ */ jsx17(
2311
+ "button",
2312
+ {
2313
+ "data-cv-tooltip": PAGINATION_LABELS.previous,
2314
+ "aria-label": PAGINATION_LABELS.previous,
2315
+ type: "button",
2316
+ onClick: () => setPage(page - 1),
2317
+ disabled: page <= 1,
2318
+ className: "cv-header-icon disabled:opacity-40",
2319
+ children: "\u2039"
2320
+ }
2321
+ ),
2322
+ /* @__PURE__ */ jsx17("span", { className: "text-gray-500", children: labels.page(page, lastPage) }),
2323
+ /* @__PURE__ */ jsx17(
2324
+ "button",
2325
+ {
2326
+ "data-cv-tooltip": PAGINATION_LABELS.next,
2327
+ "aria-label": PAGINATION_LABELS.next,
2328
+ type: "button",
2329
+ onClick: () => setPage(page + 1),
2330
+ disabled: page >= lastPage,
2331
+ className: "cv-header-icon disabled:opacity-40",
2332
+ children: "\u203A"
2333
+ }
2334
+ )
2335
+ ] })
2336
+ ] }) : null
2337
+ ] });
2338
+ }
2339
+
2340
+ // src/preview/ConversationSimulatorClient.ts
2341
+ var SIMULATOR_FILE_MEDIA_KINDS = ["image", "video", "document"];
2342
+ function mediaKindOf(mimeType) {
2343
+ if (mimeType.startsWith("image/")) return "image";
2344
+ if (mimeType.startsWith("video/")) return "video";
2345
+ if (mimeType.startsWith("audio/")) return "audio";
2346
+ return "document";
2347
+ }
2348
+ function acceptsMediaKind(client, kind) {
2349
+ if (!client.sendMedia) return false;
2350
+ return client.acceptedMediaKinds?.includes(kind) ?? true;
2351
+ }
2352
+ function isConversationSimulatorClient(candidate) {
2353
+ return typeof candidate.sendReply === "function";
2354
+ }
2355
+ function toConversationSimulatorClient({
2356
+ client,
2357
+ uploadMedia
2358
+ }) {
2359
+ const upload = uploadMedia ?? client.uploadMedia;
2360
+ const base = {
2361
+ sendText: (text) => client.sendText(text),
2362
+ sendReply: (selection) => {
2363
+ const reply = { id: selection.option.id, title: selection.option.title };
2364
+ return selection.kind === "button" ? client.sendButtonReply(reply) : client.sendListReply(reply);
2365
+ }
2366
+ };
2367
+ if (!upload) return base;
2368
+ return {
2369
+ ...base,
2370
+ sendMedia: async ({ mediaKind, file, mimeType, filename, caption }) => {
2371
+ const uploaded = await upload(file);
2372
+ await client.sendMedia({
2373
+ // O tipo sai do MIME que o upload devolveu quando ele existe: host que normaliza o formato
2374
+ // (áudio gravado em `webm` que sobe como `ogg`) mudava de tipo, e a mídia chegava como
2375
+ // documento.
2376
+ mediaType: uploaded.mimeType ? mediaKindOf(uploaded.mimeType) : mediaKind,
2377
+ mediaId: uploaded.mediaId,
2378
+ mimeType: uploaded.mimeType ?? mimeType ?? file.type,
2379
+ filename: uploaded.filename ?? filename ?? file.name,
2380
+ ...caption ? { caption } : {}
2381
+ });
2382
+ }
2383
+ };
2384
+ }
2385
+
2386
+ // src/preview/ConversationPreview.tsx
2387
+ import { useCallback as useCallback6, useEffect as useEffect5, useMemo as useMemo4, useRef as useRef6, useState as useState12 } from "react";
2388
+ import { jsx as jsx18, jsxs as jsxs13 } from "react/jsx-runtime";
2389
+ function mediaTypeOf(mimeType) {
2390
+ return mediaKindOf(mimeType);
2391
+ }
2392
+ var GROUPING_WINDOW_MS = 5 * 60 * 1e3;
2393
+ var FOLLOW_UP_REFRESH_MS = [400, 1200, 3e3];
2394
+ function decorate(messages) {
2395
+ return messages.map((message, index) => {
2396
+ const previous = index > 0 ? messages[index - 1] : void 0;
2397
+ const currentTime = new Date(message.timestamp).getTime();
2398
+ const previousTime = previous ? new Date(previous.timestamp).getTime() : 0;
2399
+ return {
2400
+ message,
2401
+ isFirstInGroup: !previous || previous.sender !== message.sender || currentTime - previousTime > GROUPING_WINDOW_MS,
2402
+ showDateDivider: !previous || new Date(message.timestamp).toDateString() !== new Date(previous.timestamp).toDateString()
2403
+ };
2404
+ });
2405
+ }
2406
+ function statusOf(error) {
2407
+ if (typeof error !== "object" || error === null) return void 0;
2408
+ const candidate = error;
2409
+ const value = candidate.status ?? candidate.statusCode;
2410
+ return typeof value === "number" ? value : void 0;
2411
+ }
2412
+ function isNotFound(error) {
2413
+ return statusOf(error) === 404;
2414
+ }
2415
+ function describeLoadFailure(error) {
2416
+ const status = statusOf(error);
2417
+ if (status === 401 || status === 403) {
2418
+ 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.";
2419
+ }
2420
+ if (error instanceof Error && error.message) return `N\xE3o foi poss\xEDvel ler o transcript: ${error.message}`;
2421
+ return "N\xE3o foi poss\xEDvel ler o transcript da conversa.";
2422
+ }
2423
+ function ConversationPreview({
2424
+ client,
2425
+ sse,
2426
+ conversationId,
2427
+ loadMessages,
2428
+ placeholder,
2429
+ pollIntervalMs,
2430
+ uploadMedia
2431
+ }) {
2432
+ const [messages, setMessages] = useState12([]);
2433
+ const [failure, setFailure] = useState12(void 0);
2434
+ const [loadFailure, setLoadFailure] = useState12(void 0);
2435
+ const [isRecording, setIsRecording] = useState12(false);
2436
+ const [pendingLocal, setPendingLocal] = useState12([]);
2437
+ const loadMessagesRef = useRef6(loadMessages);
2438
+ const bottomRef = useRef6(null);
2439
+ loadMessagesRef.current = loadMessages;
2440
+ const simulator = useMemo4(
2441
+ () => isConversationSimulatorClient(client) ? client : toConversationSimulatorClient({ client, ...uploadMedia ? { uploadMedia } : {} }),
2442
+ [client, uploadMedia]
2443
+ );
2444
+ const refresh = useCallback6(async () => {
2445
+ try {
2446
+ const loaded = await loadMessagesRef.current(conversationId);
2447
+ setMessages(loaded);
2448
+ setLoadFailure(void 0);
2449
+ if (loaded.length > 0) setPendingLocal([]);
2450
+ } catch (error) {
2451
+ if (isNotFound(error)) {
2452
+ setMessages([]);
2453
+ setLoadFailure(void 0);
2454
+ return;
2455
+ }
2456
+ setLoadFailure(describeLoadFailure(error));
2457
+ }
2458
+ }, [conversationId]);
2459
+ useEffect5(() => {
2460
+ void refresh();
2461
+ }, [refresh]);
2462
+ useEffect5(() => {
2463
+ const source = sse.connectConversationStream(conversationId);
2464
+ const handler = () => {
2465
+ void refresh();
2466
+ };
2467
+ source.addEventListener("message", handler);
2468
+ return () => {
2469
+ source.removeEventListener("message", handler);
2470
+ source.close();
2471
+ };
2472
+ }, [sse, conversationId, refresh]);
2473
+ useEffect5(() => {
2474
+ if (!pollIntervalMs) return;
2475
+ const timer = setInterval(() => void refresh(), pollIntervalMs);
2476
+ return () => clearInterval(timer);
2477
+ }, [pollIntervalMs, refresh]);
2478
+ useEffect5(() => {
2479
+ bottomRef.current?.scrollIntoView({ behavior: "smooth", block: "nearest" });
2480
+ }, [messages]);
2481
+ const rendered = useMemo4(() => decorate([...messages, ...pendingLocal]), [messages, pendingLocal]);
2482
+ async function refreshWithFollowUps() {
2483
+ await refresh();
2484
+ for (const atraso of FOLLOW_UP_REFRESH_MS) {
2485
+ setTimeout(() => void refresh(), atraso);
2486
+ }
2487
+ }
2488
+ async function handleSend(text) {
2489
+ setFailure(void 0);
2490
+ try {
2491
+ await simulator.sendText(text);
2492
+ setPendingLocal((current) => [
2493
+ ...current,
2494
+ {
2495
+ id: `local-${current.length}-${text.length}`,
2496
+ type: "text",
2497
+ content: text,
2498
+ // Do ponto de vista do servidor, mensagem do cliente é inbound — é assim que ela aparece
2499
+ // como "minha" nesta visão.
2500
+ direction: "inbound",
2501
+ sender: "customer",
2502
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2503
+ status: "sent"
2504
+ }
2505
+ ]);
2506
+ await refreshWithFollowUps();
2507
+ } catch (error) {
2508
+ setFailure(error instanceof Error ? error.message : "Falha ao entregar a mensagem no webhook.");
2509
+ }
2510
+ }
2511
+ async function handleInteractiveSelect(selection) {
2512
+ setFailure(void 0);
2513
+ try {
2514
+ await simulator.sendReply(selection);
2515
+ await refreshWithFollowUps();
2516
+ } catch (error) {
2517
+ setFailure(error instanceof Error ? error.message : "Falha ao entregar a resposta no webhook.");
2518
+ }
2519
+ }
2520
+ const sendMedia = simulator.sendMedia;
2521
+ const canAttachFile = SIMULATOR_FILE_MEDIA_KINDS.some((kind) => acceptsMediaKind(simulator, kind));
2522
+ const canRecordAudio = acceptsMediaKind(simulator, "audio");
2523
+ async function handleAttach(file) {
2524
+ if (!sendMedia) return;
2525
+ setFailure(void 0);
2526
+ try {
2527
+ await sendMedia({
2528
+ mediaKind: mediaKindOf(file.type),
2529
+ file,
2530
+ mimeType: file.type,
2531
+ filename: file.name
2532
+ });
2533
+ await refreshWithFollowUps();
2534
+ } catch (error) {
2535
+ setFailure(error instanceof Error ? error.message : "Falha ao enviar o arquivo.");
2536
+ }
2537
+ }
2538
+ return /* @__PURE__ */ jsxs13("div", { className: "flex h-full min-h-0 flex-col", children: [
2539
+ /* @__PURE__ */ jsxs13(ConversationWallpaper, { className: "flex-1 min-h-0 overflow-y-auto px-4 py-3", children: [
2540
+ rendered.map(({ message, isFirstInGroup, showDateDivider }) => /* @__PURE__ */ jsxs13("div", { children: [
2541
+ showDateDivider ? /* @__PURE__ */ jsx18(DateDivider, { iso: message.timestamp }) : null,
2542
+ /* @__PURE__ */ jsx18(
2543
+ MessageBubble,
2544
+ {
2545
+ message,
2546
+ isMine: message.direction === "inbound",
2547
+ isFirstInGroup,
2548
+ onInteractiveSelect: message.direction === "outbound" ? (selection) => void handleInteractiveSelect(selection) : void 0
2549
+ }
2550
+ )
2551
+ ] }, message.id)),
2552
+ /* @__PURE__ */ jsx18("div", { ref: bottomRef })
2553
+ ] }),
2554
+ failure ? /* @__PURE__ */ jsx18("p", { role: "alert", className: "px-4 py-2 text-sm text-red-600 dark:text-red-400", children: failure }) : null,
2555
+ loadFailure ? /* @__PURE__ */ jsx18("p", { role: "status", className: "px-4 py-2 text-sm text-amber-700 dark:text-amber-400", children: loadFailure }) : null,
2556
+ /* @__PURE__ */ jsx18(
2557
+ MessageComposer,
2558
+ {
2559
+ onSend: (text) => void handleSend(text),
2560
+ onAttach: canAttachFile ? (file) => void handleAttach(file) : void 0,
2561
+ placeholder: isRecording ? "Gravando\u2026 toque no quadrado para ouvir" : placeholder ?? "Escreva como o cliente\u2026",
2562
+ idleAction: canRecordAudio ? /* @__PURE__ */ jsx18(
2563
+ AudioRecorderButton,
2564
+ {
2565
+ onRecorded: (file) => void handleAttach(file),
2566
+ onFailure: (message) => setFailure(message),
2567
+ onRecordingChange: setIsRecording
2568
+ }
2569
+ ) : void 0
2570
+ }
2571
+ )
2572
+ ] });
2573
+ }
2574
+
2575
+ // src/preview/ConversationSimulatorPanel.tsx
2576
+ import { jsx as jsx19, jsxs as jsxs14 } from "react/jsx-runtime";
2577
+ var DEFAULT_CONVERSATION_SIMULATOR_PANEL_LABELS = {
2578
+ title: "Simulador do cliente",
2579
+ destinationHint: "entrega no webhook real",
2580
+ close: "Fechar simulador",
2581
+ placeholder: "Escreva como o cliente\u2026"
2582
+ };
2583
+ var SIMULATOR_PANEL_CHANNEL_WORDING = {
2584
+ [CONVERSATION_CHANNEL.WHATSAPP]: {
2585
+ destinationHint: DEFAULT_CONVERSATION_SIMULATOR_PANEL_LABELS.destinationHint,
2586
+ placeholder: DEFAULT_CONVERSATION_SIMULATOR_PANEL_LABELS.placeholder
2587
+ },
2588
+ [CONVERSATION_CHANNEL.MESSENGER]: {
2589
+ destinationHint: DEFAULT_CONVERSATION_SIMULATOR_PANEL_LABELS.destinationHint,
2590
+ placeholder: DEFAULT_CONVERSATION_SIMULATOR_PANEL_LABELS.placeholder
2591
+ },
2592
+ [CONVERSATION_CHANNEL.INSTAGRAM]: {
2593
+ destinationHint: DEFAULT_CONVERSATION_SIMULATOR_PANEL_LABELS.destinationHint,
2594
+ placeholder: DEFAULT_CONVERSATION_SIMULATOR_PANEL_LABELS.placeholder
2595
+ },
2596
+ [CONVERSATION_CHANNEL.WEBCHAT]: {
2597
+ destinationHint: "entrega na API do chat do site",
2598
+ placeholder: "Escreva como o visitante\u2026"
2599
+ }
2600
+ };
2601
+ function simulatorPanelLabelsOf(channel) {
2602
+ return {
2603
+ ...DEFAULT_CONVERSATION_SIMULATOR_PANEL_LABELS,
2604
+ ...SIMULATOR_PANEL_CHANNEL_WORDING[channel ?? DEFAULT_CONVERSATION_CHANNEL]
2605
+ };
2606
+ }
2607
+ function ConversationSimulatorPanel({
2608
+ onClose,
2609
+ channel,
2610
+ displayHandle,
2611
+ displayNumber,
2612
+ labels,
2613
+ headerActions,
2614
+ ...previewProps
2615
+ }) {
2616
+ const text = { ...simulatorPanelLabelsOf(channel), ...labels };
2617
+ const subtitle = [displayHandle ?? displayNumber ?? previewProps.conversationId, text.destinationHint].join(" \xB7 ");
2618
+ return /* @__PURE__ */ jsxs14("aside", { className: "cv-simulator-panel", "aria-label": text.title, children: [
2619
+ /* @__PURE__ */ jsxs14("header", { className: "cv-simulator-panel__header", children: [
2620
+ /* @__PURE__ */ jsxs14("div", { className: "cv-simulator-panel__heading", children: [
2621
+ /* @__PURE__ */ jsx19("h2", { className: "cv-simulator-panel__title", children: text.title }),
2622
+ /* @__PURE__ */ jsx19("p", { className: "cv-simulator-panel__subtitle", children: subtitle })
2623
+ ] }),
2624
+ /* @__PURE__ */ jsxs14("div", { className: "cv-simulator-panel__actions", children: [
2625
+ headerActions,
2626
+ /* @__PURE__ */ jsx19("button", { type: "button", onClick: onClose, "data-cv-tooltip": text.close, "aria-label": text.close, className: "cv-simulator-panel__close", children: /* @__PURE__ */ jsx19("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", "aria-hidden": "true", children: /* @__PURE__ */ jsx19("path", { d: "M18 6 6 18M6 6l12 12" }) }) })
2627
+ ] })
2628
+ ] }),
2629
+ /* @__PURE__ */ jsx19("div", { className: "cv-simulator-panel__body", children: /* @__PURE__ */ jsx19(ConversationPreview, { ...previewProps, placeholder: text.placeholder }) })
2630
+ ] });
2631
+ }
2632
+
2633
+ export {
2634
+ ConversationLocalesProvider,
2635
+ useConversationLocales,
2636
+ StatusTicks,
2637
+ AudioPlayer,
2638
+ cn,
2639
+ AudioTranscription,
2640
+ FileIcon,
2641
+ formatTimestamp,
2642
+ formatDateTime,
2643
+ isSameDay,
2644
+ formatFileSize,
2645
+ documentTypeLabel,
2646
+ MediaRenderer,
2647
+ DEFAULT_LIGHTBOX_LABELS,
2648
+ Lightbox,
2649
+ DEFAULT_INTERACTIVE_MESSAGE_LABELS,
2650
+ InteractiveMessage,
2651
+ createMediaUrlResolver,
2652
+ ConversationsProvider,
2653
+ useConversations,
2654
+ MessageBubble,
2655
+ ConversationWallpaper,
2656
+ EMOJI_CATEGORIES,
2657
+ searchEmojis,
2658
+ DEFAULT_EMOJI_PICKER_LABELS,
2659
+ EmojiPicker,
2660
+ DEFAULT_AUDIO_RECORDER_BUTTON_LABELS,
2661
+ DEFAULT_MAX_RECORDING_MILLISECONDS,
2662
+ AudioRecorderButton,
2663
+ COMPOSER_BAR_CLASS,
2664
+ COMPOSER_TOOL_BUTTON_CLASS,
2665
+ COMPOSER_TOOL_BUTTON_ACTIVE_CLASS,
2666
+ COMPOSER_TOOL_BUTTON_IDLE_CLASS,
2667
+ COMPOSER_MONOSPACE_CLASS,
2668
+ COMPOSER_COMPACT_WIDTH,
2669
+ QUICK_REPLY_PILL_CLASS,
2670
+ applyQuickReplyVariables,
2671
+ resolveQuickReply,
2672
+ DEFAULT_MESSAGE_COMPOSER_LABELS,
2673
+ DEFAULT_ACCEPTED_FILE_TYPES,
2674
+ MessageComposer,
2675
+ DateDivider,
2676
+ formatPhone,
2677
+ phoneInitials,
2678
+ CONVERSATION_CHANNEL,
2679
+ DEFAULT_CONVERSATION_CHANNEL,
2680
+ REOPEN_MECHANISM,
2681
+ HANDLE_KIND,
2682
+ CHANNEL_CAPABILITIES,
2683
+ capabilitiesOf,
2684
+ CHANNEL_FILTER_ALL,
2685
+ channelFiltersFor,
2686
+ formatContactHandle,
2687
+ contactFlag,
2688
+ useAsyncResource,
2689
+ conversationsOf,
2690
+ totalOf,
2691
+ useConversationDocuments,
2692
+ PAGINATION_LABELS,
2693
+ DOCUMENT_SOURCE_FILTER,
2694
+ DEFAULT_CONVERSATION_DOCUMENTS_LABELS,
2695
+ ConversationDocumentsPanel,
2696
+ DEFAULT_DOCUMENTS_LIBRARY_LABELS,
2697
+ DocumentsLibrary,
2698
+ SIMULATOR_FILE_MEDIA_KINDS,
2699
+ mediaKindOf,
2700
+ acceptsMediaKind,
2701
+ isConversationSimulatorClient,
2702
+ toConversationSimulatorClient,
2703
+ mediaTypeOf,
2704
+ ConversationPreview,
2705
+ DEFAULT_CONVERSATION_SIMULATOR_PANEL_LABELS,
2706
+ simulatorPanelLabelsOf,
2707
+ ConversationSimulatorPanel
2708
+ };