@adatechnology/conversations-ui 0.1.0-rc.2 → 0.1.0-rc.21

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