@adatechnology/conversations-ui 0.1.0-rc.9 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/dist/ConversationSimulatorPanel--5fIzXWY.d.ts +804 -0
  2. package/dist/{chunk-G5BM3VBP.js → chunk-BJNRLLDO.js} +1248 -282
  3. package/dist/chunk-DKPXKQGC.js +110 -0
  4. package/dist/{chunk-2AYDBWNE.js → chunk-WCBDXZ3X.js} +13 -3
  5. package/dist/flows/index.d.ts +422 -5
  6. package/dist/flows/index.js +2502 -676
  7. package/dist/index.d.ts +919 -17
  8. package/dist/index.js +3676 -675
  9. package/dist/preview/index.d.ts +62 -105
  10. package/dist/preview/index.js +162 -284
  11. package/dist/styles.css +893 -0
  12. package/package.json +9 -8
  13. package/src/AudioPlayer.tsx +8 -0
  14. package/src/AudioRecorderButton.test.tsx +30 -0
  15. package/src/AudioRecorderButton.tsx +248 -0
  16. package/src/AudioTranscription.test.tsx +115 -0
  17. package/src/AudioTranscription.tsx +252 -0
  18. package/src/Avatar.tsx +1 -1
  19. package/src/ConversationContextPanel.tsx +218 -44
  20. package/src/ConversationDocumentsPanel.tsx +11 -6
  21. package/src/ConversationHeader.test.tsx +66 -0
  22. package/src/ConversationHeader.tsx +147 -47
  23. package/src/ConversationListItem.tsx +8 -6
  24. package/src/ConversationLocalesProvider.tsx +28 -0
  25. package/src/ConversationRow.tsx +53 -7
  26. package/src/DarkModeToggle.test.tsx +76 -0
  27. package/src/DarkModeToggle.tsx +92 -0
  28. package/src/DocumentsLibrary.tsx +67 -7
  29. package/src/EmojiPicker.tsx +2 -1
  30. package/src/InteractiveMessage.tsx +3 -0
  31. package/src/Lightbox.tsx +1 -1
  32. package/src/MediaRenderer.tsx +88 -15
  33. package/src/MessageBubble.test.tsx +41 -0
  34. package/src/MessageBubble.tsx +47 -5
  35. package/src/MessageComposer.test.tsx +35 -0
  36. package/src/MessageComposer.tsx +122 -17
  37. package/src/MessageText.tsx +2 -1
  38. package/src/MessageTimestamp.tsx +2 -1
  39. package/src/RichMessageComposer.test.tsx +113 -0
  40. package/src/RichMessageComposer.tsx +551 -0
  41. package/src/SimpleEmojiPicker.tsx +5 -3
  42. package/src/StatusTicks.tsx +1 -1
  43. package/src/Toast.tsx +4 -0
  44. package/src/Tooltip.test.ts +42 -0
  45. package/src/Tooltip.tsx +167 -0
  46. package/src/Wallpaper.tsx +27 -13
  47. package/src/WhatsAppMessageEditor.tsx +10 -7
  48. package/src/WindowExpiredNotice.tsx +12 -4
  49. package/src/{preview/audioRecorderFormat.test.ts → audioRecorderFormat.test.ts} +1 -1
  50. package/src/buildOutput.test.ts +79 -0
  51. package/src/composer.constant.ts +33 -0
  52. package/src/conversationTranscript.test.ts +57 -0
  53. package/src/conversationTranscript.ts +29 -4
  54. package/src/conversationWindow.ts +7 -5
  55. package/src/documentTypeLabel.test.ts +57 -0
  56. package/src/documents/DocumentsWorkspace.tsx +550 -0
  57. package/src/documents/index.ts +8 -0
  58. package/src/documents/labels.ts +92 -0
  59. package/src/flows/FlowConnectionEdge.tsx +104 -0
  60. package/src/flows/FlowGroupHeader.tsx +12 -2
  61. package/src/flows/FlowLegend.tsx +125 -0
  62. package/src/flows/FlowMapCanvas.tsx +15 -12
  63. package/src/flows/FlowMapNode.tsx +4 -1
  64. package/src/flows/FlowNodeCard.tsx +219 -34
  65. package/src/flows/FlowNodePanel.tsx +153 -39
  66. package/src/flows/FlowPalette.tsx +156 -70
  67. package/src/flows/FlowPortalNode.tsx +1 -1
  68. package/src/flows/FlowWhatsAppPreview.tsx +14 -3
  69. package/src/flows/FlowsWorkspace.tsx +1255 -0
  70. package/src/flows/flowCanvasModel.test.ts +456 -0
  71. package/src/flows/flowCanvasModel.ts +378 -0
  72. package/src/flows/flowEditorOps.test.ts +276 -0
  73. package/src/flows/flowEditorOps.ts +202 -0
  74. package/src/flows/flowGraph.ts +78 -53
  75. package/src/flows/flowMenuPlacement.test.ts +130 -0
  76. package/src/flows/flowMenuPlacement.ts +86 -0
  77. package/src/flows/index.ts +51 -2
  78. package/src/flows/labels.ts +180 -0
  79. package/src/flows/workspaceContract.test.ts +126 -0
  80. package/src/hooks/useContainerWidth.ts +35 -0
  81. package/src/hooks/useConversationRealtime.ts +10 -8
  82. package/src/hooks/useScrollToLatestMessage.ts +127 -0
  83. package/src/hooks/useUrlFilterState.ts +107 -0
  84. package/src/icon.constant.ts +12 -0
  85. package/src/index.ts +100 -0
  86. package/src/lib/composer-formatting.test.ts +78 -0
  87. package/src/lib/composer-formatting.ts +145 -0
  88. package/src/lib/whatsapp-formatting.test.tsx +37 -0
  89. package/src/lib/whatsapp-formatting.tsx +28 -3
  90. package/src/listing/index.tsx +202 -0
  91. package/src/pagination.constant.ts +10 -0
  92. package/src/preview/ConversationPreview.tsx +84 -45
  93. package/src/preview/ConversationSimulatorClient.ts +143 -0
  94. package/src/preview/ConversationSimulatorPanel.test.tsx +55 -0
  95. package/src/preview/ConversationSimulatorPanel.tsx +131 -0
  96. package/src/preview/createPreviewBridgeClient.test.ts +92 -0
  97. package/src/preview/createPreviewBridgeClient.ts +124 -0
  98. package/src/preview/createPreviewMediaUploader.ts +82 -0
  99. package/src/preview/createPreviewWebhookClient.test.ts +96 -0
  100. package/src/preview/createPreviewWebhookClient.ts +99 -3
  101. package/src/preview/index.ts +36 -2
  102. package/src/preview/previewMediaUploader.test.ts +61 -0
  103. package/src/providers/ConversationsProvider.tsx +8 -6
  104. package/src/providers/types.ts +59 -2
  105. package/src/quickReply.test.ts +58 -0
  106. package/src/replyLatency.test.ts +71 -0
  107. package/src/replyLatency.ts +57 -0
  108. package/src/settings/MessagesWorkspace.tsx +571 -0
  109. package/src/settings/TopicsForm.tsx +2 -0
  110. package/src/settings/TranscriptionSettingsForm.test.tsx +81 -0
  111. package/src/settings/TranscriptionSettingsForm.tsx +190 -0
  112. package/src/settings/WelcomeFarewellForm.tsx +1 -0
  113. package/src/settings/WhatsAppCreateTemplateForm.tsx +1 -0
  114. package/src/settings/WhatsAppTemplateSettingsForm.tsx +5 -2
  115. package/src/settings/WhatsAppTemplatesSettings.test.tsx +61 -0
  116. package/src/settings/WhatsAppTemplatesSettings.tsx +22 -2
  117. package/src/styles.css +858 -0
  118. package/src/theme.ts +13 -0
  119. package/src/types.ts +26 -0
  120. package/src/workspace/BulkTemplateModal.tsx +132 -0
  121. package/src/workspace/ConversationPane.tsx +432 -0
  122. package/src/workspace/ConversationsInboxList.tsx +194 -0
  123. package/src/workspace/ConversationsWorkspace.tsx +423 -0
  124. package/src/workspace/index.ts +17 -0
  125. package/src/workspace/labels.test.ts +17 -0
  126. package/src/workspace/labels.ts +85 -0
  127. package/src/workspace/useConversationsInbox.ts +332 -0
  128. package/dist/types-B5C1DLu1.d.ts +0 -365
  129. package/src/preview/AudioRecorderButton.tsx +0 -117
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  parseWhatsAppFormatting,
3
3
  useIsDarkTheme
4
- } from "./chunk-2AYDBWNE.js";
4
+ } from "./chunk-WCBDXZ3X.js";
5
5
 
6
6
  // src/ConversationLocalesProvider.tsx
7
7
  import { createContext, useContext } from "react";
@@ -28,6 +28,19 @@ var DEFAULT_LOCALES = {
28
28
  untitledDocument: "Documento",
29
29
  downloadFile: "Baixar"
30
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
+ },
31
44
  selection: {
32
45
  select: "Selecionar"
33
46
  },
@@ -40,6 +53,7 @@ var ConversationLocalesContext = createContext(DEFAULT_LOCALES);
40
53
  function ConversationLocalesProvider({ children, locales }) {
41
54
  const merged = {
42
55
  bubble: { ...DEFAULT_LOCALES.bubble, ...locales?.bubble },
56
+ transcription: { ...DEFAULT_LOCALES.transcription, ...locales?.transcription },
43
57
  selection: { ...DEFAULT_LOCALES.selection, ...locales?.selection },
44
58
  dateDivider: { ...DEFAULT_LOCALES.dateDivider, ...locales?.dateDivider }
45
59
  };
@@ -75,7 +89,7 @@ function Ticks({ double }) {
75
89
  }
76
90
  function StatusTicks({ status, title }) {
77
91
  const colorClass = STATUS_COLOR_CLASS[status] ?? STATUS_COLOR_CLASS.sent;
78
- 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" }) });
92
+ return /* @__PURE__ */ jsx2("span", { className: `cursor-help leading-none flex items-center ${colorClass}`, "data-cv-tooltip": title, children: status === "failed" ? /* @__PURE__ */ jsx2(AlertTriangle, { size: 11 }) : /* @__PURE__ */ jsx2(Ticks, { double: status !== "sent" }) });
79
93
  }
80
94
 
81
95
  // src/AudioPlayer.tsx
@@ -86,6 +100,10 @@ var BARS = Array.from({ length: 30 }, (_, i) => {
86
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];
87
101
  return heights[i % heights.length];
88
102
  });
103
+ var AUDIO_PLAYER_LABELS = {
104
+ play: "Reproduzir",
105
+ pause: "Pausar"
106
+ };
89
107
  function fmt(sec) {
90
108
  if (!isFinite(sec)) return "0:00";
91
109
  const m = Math.floor(sec / 60);
@@ -147,6 +165,8 @@ function AudioPlayer({ src, isMine = false }) {
147
165
  /* @__PURE__ */ jsx3(
148
166
  "button",
149
167
  {
168
+ "data-cv-tooltip": playing ? AUDIO_PLAYER_LABELS.pause : AUDIO_PLAYER_LABELS.play,
169
+ "aria-label": playing ? AUDIO_PLAYER_LABELS.pause : AUDIO_PLAYER_LABELS.play,
150
170
  onClick: toggle,
151
171
  className: `flex-shrink-0 w-9 h-9 rounded-full flex items-center justify-center transition-colors ${playBtn}`,
152
172
  children: playing ? /* @__PURE__ */ jsx3(Pause, { size: 16 }) : /* @__PURE__ */ jsx3(Play, { size: 16, className: "translate-x-0.5" })
@@ -166,36 +186,189 @@ function AudioPlayer({ src, isMine = false }) {
166
186
  ] });
167
187
  }
168
188
 
189
+ // src/AudioTranscription.tsx
190
+ import { useCallback, useState as useState2 } from "react";
191
+ import { Check, Copy, FileText, Loader2, RefreshCw } from "lucide-react";
192
+
193
+ // src/lib/cn.ts
194
+ import { clsx } from "clsx";
195
+ import { twMerge } from "tailwind-merge";
196
+ function cn(...inputs) {
197
+ return twMerge(clsx(inputs));
198
+ }
199
+
200
+ // src/AudioTranscription.tsx
201
+ import { Fragment, jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
202
+ var COPIED_FEEDBACK_MS = 2e3;
203
+ var COLLAPSE_ABOVE_CHARS = 320;
204
+ var COLLAPSED_LINE_CLAMP = 4;
205
+ function AudioTranscription({ transcription, onTranscribe, isMine = false }) {
206
+ const { transcription: locales } = useConversationLocales();
207
+ const [hasCopied, setHasCopied] = useState2(false);
208
+ const [isTranscribing, setIsTranscribing] = useState2(false);
209
+ const [hasRequestFailed, setHasRequestFailed] = useState2(false);
210
+ const [justTranscribed, setJustTranscribed] = useState2();
211
+ const [isExpanded, setIsExpanded] = useState2(false);
212
+ const effective = justTranscribed ?? transcription;
213
+ const text = effective?.text?.trim() ?? "";
214
+ const status = effective?.status;
215
+ const isLong = text.length > COLLAPSE_ABOVE_CHARS;
216
+ const isTruncated = isLong && !isExpanded;
217
+ const isDone = status === "done";
218
+ const hasText = isDone && text.length > 0;
219
+ const isSilent = isDone && text.length === 0;
220
+ const handleCopy = useCallback(async () => {
221
+ if (!text) return;
222
+ try {
223
+ await navigator.clipboard.writeText(text);
224
+ setHasCopied(true);
225
+ setTimeout(() => setHasCopied(false), COPIED_FEEDBACK_MS);
226
+ } catch {
227
+ }
228
+ }, [text]);
229
+ const handleTranscribe = useCallback(async () => {
230
+ if (!onTranscribe || isTranscribing) return;
231
+ setIsTranscribing(true);
232
+ setHasRequestFailed(false);
233
+ try {
234
+ const result = await onTranscribe();
235
+ if (result) setJustTranscribed(result);
236
+ } catch {
237
+ setHasRequestFailed(true);
238
+ } finally {
239
+ setIsTranscribing(false);
240
+ }
241
+ }, [onTranscribe, isTranscribing]);
242
+ const dividerClass = isMine ? "border-black/10 dark:border-white/10" : "border-black/10 dark:border-white/10";
243
+ if (!status && !onTranscribe) return null;
244
+ if (!status || status === "pending" || status === "failed") {
245
+ return /* @__PURE__ */ jsx4("div", { className: `mt-1.5 border-t pt-1.5 ${dividerClass}`, children: /* @__PURE__ */ jsx4(
246
+ TranscribeButton,
247
+ {
248
+ label: resolveActionLabel({ status, hasRequestFailed, isTranscribing, locales }),
249
+ isBusy: isTranscribing,
250
+ onClick: handleTranscribe,
251
+ isDisabled: !onTranscribe
252
+ }
253
+ ) });
254
+ }
255
+ if (status === "unsupported") {
256
+ return /* @__PURE__ */ jsx4("div", { className: `mt-1.5 border-t pt-1.5 ${dividerClass}`, children: /* @__PURE__ */ jsx4("p", { className: "text-xs italic text-gray-500 dark:text-gray-400", children: locales.unsupported }) });
257
+ }
258
+ return /* @__PURE__ */ jsxs3("div", { className: `mt-1.5 border-t pt-1.5 ${dividerClass}`, children: [
259
+ /* @__PURE__ */ jsxs3("div", { className: "mb-0.5 flex items-center gap-1.5", children: [
260
+ /* @__PURE__ */ jsx4(FileText, { size: 11, className: "flex-shrink-0 text-gray-500 dark:text-gray-400", "aria-hidden": true }),
261
+ /* @__PURE__ */ jsx4("span", { className: "text-[11px] font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400", children: locales.label }),
262
+ hasText && /* @__PURE__ */ jsx4(
263
+ "button",
264
+ {
265
+ onClick: handleCopy,
266
+ "data-cv-tooltip": locales.copy,
267
+ "aria-label": hasCopied ? locales.copied : locales.copy,
268
+ className: "ml-auto flex flex-shrink-0 items-center gap-1 rounded px-1.5 py-0.5 text-[11px] text-gray-500 transition-colors hover:bg-black/5 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/10 dark:hover:text-gray-200",
269
+ children: hasCopied ? /* @__PURE__ */ jsxs3(Fragment, { children: [
270
+ /* @__PURE__ */ jsx4(Check, { size: 11, "aria-hidden": true }),
271
+ /* @__PURE__ */ jsx4("span", { children: locales.copied })
272
+ ] }) : /* @__PURE__ */ jsxs3(Fragment, { children: [
273
+ /* @__PURE__ */ jsx4(Copy, { size: 11, "aria-hidden": true }),
274
+ /* @__PURE__ */ jsx4("span", { children: locales.copy })
275
+ ] })
276
+ }
277
+ )
278
+ ] }),
279
+ isSilent ? /* @__PURE__ */ jsx4("p", { className: "text-xs italic text-gray-500 dark:text-gray-400", children: locales.empty }) : /* @__PURE__ */ jsxs3(Fragment, { children: [
280
+ /* @__PURE__ */ jsx4(
281
+ "p",
282
+ {
283
+ className: cn(
284
+ "select-all whitespace-pre-wrap break-words text-[13px] leading-[18px] text-gray-700 dark:text-gray-200",
285
+ isTruncated && "overflow-hidden"
286
+ ),
287
+ style: isTruncated ? {
288
+ display: "-webkit-box",
289
+ WebkitBoxOrient: "vertical",
290
+ WebkitLineClamp: COLLAPSED_LINE_CLAMP
291
+ } : void 0,
292
+ children: text
293
+ }
294
+ ),
295
+ isLong && /* @__PURE__ */ jsx4(
296
+ "button",
297
+ {
298
+ "data-cv-tooltip": isExpanded ? locales.showLess : locales.showMore,
299
+ "aria-label": isExpanded ? locales.showLess : locales.showMore,
300
+ onClick: () => setIsExpanded((current) => !current),
301
+ className: "mt-1 text-[11px] font-medium text-gray-500 underline decoration-dotted hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200",
302
+ children: isExpanded ? locales.showLess : locales.showMore
303
+ }
304
+ )
305
+ ] }),
306
+ onTranscribe && /* @__PURE__ */ jsxs3(
307
+ "button",
308
+ {
309
+ "data-cv-tooltip": isTranscribing ? locales.transcribing : locales.retry,
310
+ "aria-label": isTranscribing ? locales.transcribing : locales.retry,
311
+ onClick: handleTranscribe,
312
+ disabled: isTranscribing,
313
+ className: "mt-1 flex items-center gap-1 text-[11px] text-gray-400 opacity-0 transition-opacity hover:text-gray-600 focus:opacity-100 group-hover:opacity-100 disabled:opacity-50 dark:text-gray-500 dark:hover:text-gray-300",
314
+ children: [
315
+ isTranscribing ? /* @__PURE__ */ jsx4(Loader2, { size: 10, className: "animate-spin", "aria-hidden": true }) : /* @__PURE__ */ jsx4(RefreshCw, { size: 10, "aria-hidden": true }),
316
+ /* @__PURE__ */ jsx4("span", { children: isTranscribing ? locales.transcribing : locales.retry })
317
+ ]
318
+ }
319
+ )
320
+ ] });
321
+ }
322
+ function resolveActionLabel(params) {
323
+ if (params.isTranscribing) return params.locales.transcribing;
324
+ if (params.hasRequestFailed) return params.locales.retry;
325
+ if (params.status === "pending") return params.locales.transcribing;
326
+ if (params.status === "failed") return params.locales.failed;
327
+ return params.locales.transcribe;
328
+ }
329
+ function TranscribeButton({
330
+ label,
331
+ isBusy,
332
+ isDisabled,
333
+ onClick
334
+ }) {
335
+ return /* @__PURE__ */ jsxs3(
336
+ "button",
337
+ {
338
+ "data-cv-tooltip": label,
339
+ "aria-label": label,
340
+ onClick,
341
+ disabled: isBusy || isDisabled,
342
+ className: "flex items-center gap-1.5 rounded px-1.5 py-0.5 text-[11px] text-gray-500 transition-colors hover:bg-black/5 hover:text-gray-700 disabled:opacity-60 dark:text-gray-400 dark:hover:bg-white/10 dark:hover:text-gray-200",
343
+ children: [
344
+ isBusy ? /* @__PURE__ */ jsx4(Loader2, { size: 11, className: "animate-spin", "aria-hidden": true }) : /* @__PURE__ */ jsx4(FileText, { size: 11, "aria-hidden": true }),
345
+ /* @__PURE__ */ jsx4("span", { children: label })
346
+ ]
347
+ }
348
+ );
349
+ }
350
+
169
351
  // src/FileIcon.tsx
170
352
  import {
171
353
  FileArchive,
172
354
  FileAudio,
173
355
  FileImage,
174
356
  FileSpreadsheet,
175
- FileText,
357
+ FileText as FileText2,
176
358
  FileVideo,
177
359
  File as FileGeneric,
178
360
  Presentation
179
361
  } from "lucide-react";
180
-
181
- // src/lib/cn.ts
182
- import { clsx } from "clsx";
183
- import { twMerge } from "tailwind-merge";
184
- function cn(...inputs) {
185
- return twMerge(clsx(inputs));
186
- }
187
-
188
- // src/FileIcon.tsx
189
- import { jsx as jsx4 } from "react/jsx-runtime";
362
+ import { jsx as jsx5 } from "react/jsx-runtime";
190
363
  var IMAGE_STYLE = { Icon: FileImage, colorClass: "text-violet-500" };
191
364
  var VIDEO_STYLE = { Icon: FileVideo, colorClass: "text-fuchsia-500" };
192
365
  var AUDIO_STYLE = { Icon: FileAudio, colorClass: "text-amber-500" };
193
366
  var SHEET_STYLE = { Icon: FileSpreadsheet, colorClass: "text-green-600" };
194
- var WORD_STYLE = { Icon: FileText, colorClass: "text-blue-500" };
367
+ var WORD_STYLE = { Icon: FileText2, colorClass: "text-blue-500" };
195
368
  var SLIDES_STYLE = { Icon: Presentation, colorClass: "text-orange-600" };
196
- var TEXT_STYLE = { Icon: FileText, colorClass: "text-gray-500" };
369
+ var TEXT_STYLE = { Icon: FileText2, colorClass: "text-gray-500" };
197
370
  var EXTENSION_STYLE = {
198
- pdf: { Icon: FileText, colorClass: "text-red-500" },
371
+ pdf: { Icon: FileText2, colorClass: "text-red-500" },
199
372
  doc: WORD_STYLE,
200
373
  docx: WORD_STYLE,
201
374
  xls: SHEET_STYLE,
@@ -242,7 +415,7 @@ function FileIcon({ filename, mimeType, size = 20, className }) {
242
415
  const extension = resolveFileIconExtension(filename, mimeType);
243
416
  const style = EXTENSION_STYLE[extension] ?? { Icon: FileGeneric, colorClass: "text-gray-500" };
244
417
  const { Icon, colorClass } = style;
245
- return /* @__PURE__ */ jsx4(Icon, { size, className: cn(colorClass, className) });
418
+ return /* @__PURE__ */ jsx5(Icon, { size, className: cn(colorClass, className) });
246
419
  }
247
420
 
248
421
  // src/lib/format.ts
@@ -274,8 +447,16 @@ function formatFileSize(bytes) {
274
447
  }
275
448
 
276
449
  // src/MediaRenderer.tsx
277
- import { useState as useState2 } from "react";
278
- import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
450
+ import { useState as useState3 } from "react";
451
+ import { jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
452
+ function documentTypeLabel(filename, mimeType) {
453
+ const extension = filename?.split(".").pop();
454
+ if (extension && extension.length <= 5 && extension !== filename) return extension.toUpperCase();
455
+ const subtype = mimeType?.split(";")[0]?.split("/")[1];
456
+ if (!subtype) return "FILE";
457
+ const compacto = subtype.split(".").pop() ?? subtype;
458
+ return compacto.slice(0, 12).toUpperCase();
459
+ }
279
460
  function resolveMediaSource(message) {
280
461
  if (message.mediaUrl) return message.mediaUrl;
281
462
  if (message.base64) {
@@ -288,9 +469,9 @@ function hasLazyRef(message) {
288
469
  return Boolean(message.uploadId || message.mediaId);
289
470
  }
290
471
  function useLazyMediaUrl(message, onResolveUrl) {
291
- const [url, setUrl] = useState2(null);
292
- const [loading, setLoading] = useState2(false);
293
- const [error, setError] = useState2(false);
472
+ const [url, setUrl] = useState3(null);
473
+ const [loading, setLoading] = useState3(false);
474
+ const [error, setError] = useState3(false);
294
475
  const load = async () => {
295
476
  if (url || loading || !onResolveUrl) return;
296
477
  setLoading(true);
@@ -307,68 +488,129 @@ function useLazyMediaUrl(message, onResolveUrl) {
307
488
  };
308
489
  return { url, loading, error, load };
309
490
  }
310
- function MediaRenderer({ message, onLightbox, onResolveUrl, className }) {
491
+ function MediaRenderer({
492
+ message,
493
+ onLightbox,
494
+ onResolveUrl,
495
+ onTranscribeAudio,
496
+ className
497
+ }) {
311
498
  const { bubble } = useConversationLocales();
312
499
  const eagerSrc = resolveMediaSource(message);
313
500
  const lazy = useLazyMediaUrl(message, onResolveUrl);
314
501
  const src = eagerSrc ?? lazy.url;
315
502
  const canLazyLoad = !eagerSrc && hasLazyRef(message) && Boolean(onResolveUrl);
316
- const lazyButtonClass = "text-xs text-blue-600 underline flex items-center gap-1";
503
+ function LazyMediaButton({ icon, label }) {
504
+ return /* @__PURE__ */ jsxs4(
505
+ "button",
506
+ {
507
+ "data-cv-tooltip": label,
508
+ "aria-label": label,
509
+ onClick: lazy.load,
510
+ disabled: lazy.loading,
511
+ className: "flex min-w-[180px] items-center gap-2 rounded-lg bg-black/5 px-2 py-1.5 text-left transition-colors hover:bg-black/10 disabled:opacity-60 dark:bg-white/10 dark:hover:bg-white/15",
512
+ children: [
513
+ /* @__PURE__ */ jsx6("span", { className: "flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-full bg-gray-600 text-white", children: icon }),
514
+ /* @__PURE__ */ jsx6("span", { className: "truncate text-xs text-gray-600 dark:text-gray-300", children: label })
515
+ ]
516
+ }
517
+ );
518
+ }
317
519
  switch (message.type) {
318
520
  case "image":
319
521
  case "sticker": {
320
522
  if (!src && canLazyLoad) {
321
- return /* @__PURE__ */ jsx5("button", { onClick: lazy.load, className: lazyButtonClass, children: lazy.loading ? bubble.mediaLoading : lazy.error ? bubble.mediaRetry : bubble.viewImage });
523
+ return /* @__PURE__ */ jsx6(
524
+ LazyMediaButton,
525
+ {
526
+ icon: /* @__PURE__ */ jsxs4("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
527
+ /* @__PURE__ */ jsx6("rect", { x: "3", y: "3", width: "18", height: "18", rx: "2", ry: "2" }),
528
+ /* @__PURE__ */ jsx6("circle", { cx: "8.5", cy: "8.5", r: "1.5" }),
529
+ /* @__PURE__ */ jsx6("polyline", { points: "21 15 16 10 5 21" })
530
+ ] }),
531
+ label: lazy.loading ? bubble.mediaLoading : lazy.error ? bubble.mediaRetry : bubble.viewImage
532
+ }
533
+ );
322
534
  }
323
- return /* @__PURE__ */ jsx5("div", { className: "min-w-[200px]", children: src ? /* @__PURE__ */ jsx5("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__ */ jsx5("div", { className: "w-full h-40 bg-gray-200 flex items-center justify-center text-gray-400", children: /* @__PURE__ */ jsxs3("svg", { width: "32", height: "32", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.5", children: [
324
- /* @__PURE__ */ jsx5("rect", { x: "3", y: "3", width: "18", height: "18", rx: "2", ry: "2" }),
325
- /* @__PURE__ */ jsx5("circle", { cx: "8.5", cy: "8.5", r: "1.5" }),
326
- /* @__PURE__ */ jsx5("polyline", { points: "21 15 16 10 5 21" })
535
+ return /* @__PURE__ */ jsx6("div", { className: "min-w-[200px]", children: src ? /* @__PURE__ */ jsx6("img", { src, alt: message.caption ?? bubble.imageAlt, className: "w-full max-h-80 object-cover cursor-pointer hover:opacity-90 transition-opacity", onClick: () => onLightbox(src), loading: "lazy" }) : /* @__PURE__ */ jsx6("div", { className: "w-full h-40 bg-gray-200 flex items-center justify-center text-gray-400", children: /* @__PURE__ */ jsxs4("svg", { width: "32", height: "32", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.5", children: [
536
+ /* @__PURE__ */ jsx6("rect", { x: "3", y: "3", width: "18", height: "18", rx: "2", ry: "2" }),
537
+ /* @__PURE__ */ jsx6("circle", { cx: "8.5", cy: "8.5", r: "1.5" }),
538
+ /* @__PURE__ */ jsx6("polyline", { points: "21 15 16 10 5 21" })
327
539
  ] }) }) });
328
540
  }
329
541
  case "video": {
330
542
  if (!src && canLazyLoad) {
331
- return /* @__PURE__ */ jsx5("button", { onClick: lazy.load, className: lazyButtonClass, children: lazy.loading ? bubble.mediaLoading : lazy.error ? bubble.mediaRetry : bubble.viewVideo });
543
+ return /* @__PURE__ */ jsx6(
544
+ LazyMediaButton,
545
+ {
546
+ icon: /* @__PURE__ */ jsxs4("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
547
+ /* @__PURE__ */ jsx6("polygon", { points: "23 7 16 12 23 17 23 7" }),
548
+ /* @__PURE__ */ jsx6("rect", { x: "1", y: "5", width: "15", height: "14", rx: "2", ry: "2" })
549
+ ] }),
550
+ label: lazy.loading ? bubble.mediaLoading : lazy.error ? bubble.mediaRetry : bubble.viewVideo
551
+ }
552
+ );
332
553
  }
333
- return /* @__PURE__ */ jsx5("div", { className: "min-w-[200px]", children: src ? /* @__PURE__ */ jsx5("video", { src, className: "w-full max-h-80 rounded-lg", controls: true, preload: "metadata", children: /* @__PURE__ */ jsx5("track", { kind: "captions" }) }) : /* @__PURE__ */ jsx5("div", { className: "w-full h-32 bg-gray-200 rounded-lg flex items-center justify-center text-gray-400", children: /* @__PURE__ */ jsxs3("svg", { width: "32", height: "32", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.5", children: [
334
- /* @__PURE__ */ jsx5("polygon", { points: "23 7 16 12 23 17 23 7" }),
335
- /* @__PURE__ */ jsx5("rect", { x: "1", y: "5", width: "15", height: "14", rx: "2", ry: "2" })
554
+ return /* @__PURE__ */ jsx6("div", { className: "min-w-[200px]", children: src ? /* @__PURE__ */ jsx6("video", { src, className: "w-full max-h-80 rounded-lg", controls: true, preload: "metadata", children: /* @__PURE__ */ jsx6("track", { kind: "captions" }) }) : /* @__PURE__ */ jsx6("div", { className: "w-full h-32 bg-gray-200 rounded-lg flex items-center justify-center text-gray-400", children: /* @__PURE__ */ jsxs4("svg", { width: "32", height: "32", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.5", children: [
555
+ /* @__PURE__ */ jsx6("polygon", { points: "23 7 16 12 23 17 23 7" }),
556
+ /* @__PURE__ */ jsx6("rect", { x: "1", y: "5", width: "15", height: "14", rx: "2", ry: "2" })
336
557
  ] }) }) });
337
558
  }
338
559
  case "audio": {
560
+ const transcriptionBlock = /* @__PURE__ */ jsx6(
561
+ AudioTranscription,
562
+ {
563
+ transcription: message.transcription,
564
+ isMine: message.direction === "outbound",
565
+ ...onTranscribeAudio ? { onTranscribe: onTranscribeAudio } : {}
566
+ }
567
+ );
339
568
  if (!src && canLazyLoad) {
340
- return /* @__PURE__ */ jsx5("button", { onClick: lazy.load, className: lazyButtonClass, children: lazy.loading ? bubble.mediaLoading : lazy.error ? bubble.mediaRetry : bubble.listenAudio });
569
+ return /* @__PURE__ */ jsxs4("div", { className: "min-w-[200px]", children: [
570
+ /* @__PURE__ */ jsx6(
571
+ LazyMediaButton,
572
+ {
573
+ icon: /* @__PURE__ */ jsx6("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "currentColor", className: "translate-x-0.5", children: /* @__PURE__ */ jsx6("polygon", { points: "5 3 19 12 5 21 5 3" }) }),
574
+ label: lazy.loading ? bubble.mediaLoading : lazy.error ? bubble.mediaRetry : bubble.listenAudio
575
+ }
576
+ ),
577
+ transcriptionBlock
578
+ ] });
341
579
  }
342
- return /* @__PURE__ */ jsx5("div", { className: "min-w-[200px]", children: src ? /* @__PURE__ */ jsx5(AudioPlayer, { src, isMine: message.direction === "outbound" }) : /* @__PURE__ */ jsx5("div", { className: "h-12 bg-gray-200 rounded-lg flex items-center justify-center text-gray-400 text-xs", children: bubble.mediaUnavailable }) });
580
+ return /* @__PURE__ */ jsxs4("div", { className: "min-w-[200px]", children: [
581
+ src ? /* @__PURE__ */ jsx6(AudioPlayer, { src, isMine: message.direction === "outbound" }) : /* @__PURE__ */ jsx6("div", { className: "h-12 bg-gray-200 rounded-lg flex items-center justify-center text-gray-400 text-xs", children: bubble.mediaUnavailable }),
582
+ transcriptionBlock
583
+ ] });
343
584
  }
344
585
  case "document": {
345
- const typeLabel = message.mimeType?.split("/")[1]?.toUpperCase() ?? "FILE";
586
+ const typeLabel = documentTypeLabel(message.filename, message.mimeType);
346
587
  const sizeLabel = message.sizeBytes ? formatFileSize(message.sizeBytes) : null;
347
- return /* @__PURE__ */ jsxs3("div", { className: cn("flex items-center gap-3 min-w-[200px]", className), children: [
348
- /* @__PURE__ */ jsx5("div", { className: "w-10 h-10 bg-gray-200 rounded-lg flex items-center justify-center flex-shrink-0", children: /* @__PURE__ */ jsx5(FileIcon, { filename: message.filename, mimeType: message.mimeType }) }),
349
- /* @__PURE__ */ jsxs3("div", { className: "flex-1 min-w-0", children: [
350
- /* @__PURE__ */ jsx5("p", { className: "text-sm font-medium truncate", children: message.filename ?? bubble.untitledDocument }),
351
- /* @__PURE__ */ jsx5("p", { className: "text-xs text-gray-500", children: sizeLabel ? `${typeLabel} \xB7 ${sizeLabel}` : typeLabel })
588
+ return /* @__PURE__ */ jsxs4("div", { className: cn("flex items-center gap-3 min-w-[200px]", className), children: [
589
+ /* @__PURE__ */ jsx6("div", { className: "w-10 h-10 bg-gray-200 rounded-lg flex items-center justify-center flex-shrink-0", children: /* @__PURE__ */ jsx6(FileIcon, { filename: message.filename, mimeType: message.mimeType }) }),
590
+ /* @__PURE__ */ jsxs4("div", { className: "flex-1 min-w-0", children: [
591
+ /* @__PURE__ */ jsx6("p", { className: "text-sm font-medium truncate", children: message.filename ?? bubble.untitledDocument }),
592
+ /* @__PURE__ */ jsx6("p", { className: "truncate text-xs text-gray-500", children: sizeLabel ? `${typeLabel} \xB7 ${sizeLabel}` : typeLabel })
352
593
  ] }),
353
- src ? /* @__PURE__ */ jsx5("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__ */ jsxs3("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", className: "text-gray-600", children: [
354
- /* @__PURE__ */ jsx5("path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" }),
355
- /* @__PURE__ */ jsx5("polyline", { points: "7 10 12 15 17 10" }),
356
- /* @__PURE__ */ jsx5("line", { x1: "12", y1: "15", x2: "12", y2: "3" })
357
- ] }) }) : canLazyLoad ? /* @__PURE__ */ jsx5(
594
+ src ? /* @__PURE__ */ jsx6("a", { href: src, download: message.filename, className: "w-8 h-8 flex items-center justify-center rounded-full bg-gray-200 hover:bg-gray-300 flex-shrink-0 transition-colors", "aria-label": bubble.downloadFile, children: /* @__PURE__ */ jsxs4("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", className: "text-gray-600", children: [
595
+ /* @__PURE__ */ jsx6("path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" }),
596
+ /* @__PURE__ */ jsx6("polyline", { points: "7 10 12 15 17 10" }),
597
+ /* @__PURE__ */ jsx6("line", { x1: "12", y1: "15", x2: "12", y2: "3" })
598
+ ] }) }) : canLazyLoad ? /* @__PURE__ */ jsx6(
358
599
  "button",
359
600
  {
601
+ "data-cv-tooltip": bubble.downloadFile,
360
602
  onClick: lazy.load,
361
603
  disabled: lazy.loading,
362
604
  className: "w-8 h-8 flex items-center justify-center rounded-full bg-gray-200 hover:bg-gray-300 flex-shrink-0 transition-colors disabled:opacity-50",
363
605
  "aria-label": bubble.downloadFile,
364
- children: /* @__PURE__ */ jsxs3("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", className: "text-gray-600", children: [
365
- /* @__PURE__ */ jsx5("path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" }),
366
- /* @__PURE__ */ jsx5("polyline", { points: "7 10 12 15 17 10" }),
367
- /* @__PURE__ */ jsx5("line", { x1: "12", y1: "15", x2: "12", y2: "3" })
606
+ children: /* @__PURE__ */ jsxs4("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", className: "text-gray-600", children: [
607
+ /* @__PURE__ */ jsx6("path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" }),
608
+ /* @__PURE__ */ jsx6("polyline", { points: "7 10 12 15 17 10" }),
609
+ /* @__PURE__ */ jsx6("line", { x1: "12", y1: "15", x2: "12", y2: "3" })
368
610
  ] })
369
611
  }
370
612
  ) : null,
371
- lazy.error && /* @__PURE__ */ jsx5("span", { className: "text-xs text-red-500 flex-shrink-0", children: bubble.mediaError })
613
+ lazy.error && /* @__PURE__ */ jsx6("span", { className: "text-xs text-red-500 flex-shrink-0", children: bubble.mediaError })
372
614
  ] });
373
615
  }
374
616
  default:
@@ -377,7 +619,7 @@ function MediaRenderer({ message, onLightbox, onResolveUrl, className }) {
377
619
  }
378
620
 
379
621
  // src/Lightbox.tsx
380
- import { jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
622
+ import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
381
623
  var DEFAULT_LIGHTBOX_LABELS = {
382
624
  imageAlt: "Imagem",
383
625
  close: "Fechar"
@@ -385,16 +627,16 @@ var DEFAULT_LIGHTBOX_LABELS = {
385
627
  function Lightbox({ imageUrl, caption, onClose, labels }) {
386
628
  const imageAltLabel = labels?.imageAlt ?? DEFAULT_LIGHTBOX_LABELS.imageAlt;
387
629
  const closeLabel = labels?.close ?? DEFAULT_LIGHTBOX_LABELS.close;
388
- return /* @__PURE__ */ jsx6("div", { className: "fixed inset-0 z-50 bg-black/85 flex items-center justify-center p-4", onClick: onClose, children: /* @__PURE__ */ jsxs4("div", { className: "max-w-[90vw] max-h-[90vh] flex flex-col items-center", onClick: (e) => e.stopPropagation(), children: [
389
- /* @__PURE__ */ jsx6("img", { src: imageUrl, alt: caption ?? imageAltLabel, className: "max-w-full max-h-[80vh] object-contain rounded-lg" }),
390
- caption && /* @__PURE__ */ jsx6("p", { className: "text-white text-sm mt-3 text-center", children: caption }),
391
- /* @__PURE__ */ jsx6("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 })
630
+ return /* @__PURE__ */ jsx7("div", { className: "fixed inset-0 z-50 bg-black/85 flex items-center justify-center p-4", onClick: onClose, children: /* @__PURE__ */ jsxs5("div", { className: "max-w-[90vw] max-h-[90vh] flex flex-col items-center", onClick: (e) => e.stopPropagation(), children: [
631
+ /* @__PURE__ */ jsx7("img", { src: imageUrl, alt: caption ?? imageAltLabel, className: "max-w-full max-h-[80vh] object-contain rounded-lg" }),
632
+ caption && /* @__PURE__ */ jsx7("p", { className: "text-white text-sm mt-3 text-center", children: caption }),
633
+ /* @__PURE__ */ jsx7("button", { "data-cv-tooltip": closeLabel, "aria-label": closeLabel, onClick: onClose, className: "mt-4 px-4 py-2 bg-white/20 text-white rounded-lg hover:bg-white/30 transition-colors", children: closeLabel })
392
634
  ] }) });
393
635
  }
394
636
 
395
637
  // src/InteractiveMessage.tsx
396
- import { useState as useState3 } from "react";
397
- import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
638
+ import { useState as useState4 } from "react";
639
+ import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
398
640
  var FORMATTING_CLASSES = "[&_strong]:font-bold [&_em]:italic [&_del]:line-through";
399
641
  var DEFAULT_INTERACTIVE_MESSAGE_LABELS = {
400
642
  openList: "Ver op\xE7\xF5es"
@@ -407,14 +649,14 @@ function collectButtons(payload) {
407
649
  }
408
650
  function InteractiveMessage({ payload, onSelect, labels, className }) {
409
651
  const openListLabel = labels?.openList ?? DEFAULT_INTERACTIVE_MESSAGE_LABELS.openList;
410
- const [isListOpen, setIsListOpen] = useState3(false);
652
+ const [isListOpen, setIsListOpen] = useState4(false);
411
653
  const buttons = collectButtons(payload);
412
654
  const sections = payload.action?.sections ?? [];
413
655
  const hasRows = collectRows(payload).length > 0;
414
656
  const isInteractable = onSelect !== void 0;
415
- return /* @__PURE__ */ jsxs5("div", { className: cn("flex flex-col gap-1", className), children: [
416
- payload.header?.text ? /* @__PURE__ */ jsx7("p", { className: cn("text-sm font-semibold text-gray-900 dark:text-gray-100", FORMATTING_CLASSES), children: parseWhatsAppFormatting(payload.header.text) }) : null,
417
- payload.body?.text ? /* @__PURE__ */ jsx7(
657
+ return /* @__PURE__ */ jsxs6("div", { className: cn("flex flex-col gap-1", className), children: [
658
+ payload.header?.text ? /* @__PURE__ */ jsx8("p", { className: cn("text-sm font-semibold text-gray-900 dark:text-gray-100", FORMATTING_CLASSES), children: parseWhatsAppFormatting(payload.header.text) }) : null,
659
+ payload.body?.text ? /* @__PURE__ */ jsx8(
418
660
  "p",
419
661
  {
420
662
  className: cn(
@@ -424,10 +666,12 @@ function InteractiveMessage({ payload, onSelect, labels, className }) {
424
666
  children: parseWhatsAppFormatting(payload.body.text)
425
667
  }
426
668
  ) : null,
427
- payload.footer?.text ? /* @__PURE__ */ jsx7("p", { className: cn("text-xs text-gray-500 dark:text-gray-400", FORMATTING_CLASSES), children: parseWhatsAppFormatting(payload.footer.text) }) : null,
428
- buttons.length > 0 ? /* @__PURE__ */ jsx7("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__ */ jsx7(
669
+ payload.footer?.text ? /* @__PURE__ */ jsx8("p", { className: cn("text-xs text-gray-500 dark:text-gray-400", FORMATTING_CLASSES), children: parseWhatsAppFormatting(payload.footer.text) }) : null,
670
+ buttons.length > 0 ? /* @__PURE__ */ jsx8("div", { className: "mt-1 flex flex-col gap-1 border-t border-gray-200 pt-1 dark:border-gray-700", children: buttons.map((button) => /* @__PURE__ */ jsx8(
429
671
  "button",
430
672
  {
673
+ "data-cv-tooltip": button.title,
674
+ "aria-label": button.title,
431
675
  type: "button",
432
676
  disabled: !isInteractable,
433
677
  onClick: () => onSelect?.({ kind: "button", option: button }),
@@ -436,10 +680,12 @@ function InteractiveMessage({ payload, onSelect, labels, className }) {
436
680
  },
437
681
  button.id
438
682
  )) }) : null,
439
- hasRows ? /* @__PURE__ */ jsxs5("div", { className: "mt-1 border-t border-gray-200 pt-1 dark:border-gray-700", children: [
440
- /* @__PURE__ */ jsxs5(
683
+ hasRows ? /* @__PURE__ */ jsxs6("div", { className: "mt-1 border-t border-gray-200 pt-1 dark:border-gray-700", children: [
684
+ /* @__PURE__ */ jsxs6(
441
685
  "button",
442
686
  {
687
+ "data-cv-tooltip": payload.action?.button ?? openListLabel,
688
+ "aria-label": payload.action?.button ?? openListLabel,
443
689
  type: "button",
444
690
  onClick: () => setIsListOpen((open) => !open),
445
691
  "aria-expanded": isListOpen,
@@ -450,18 +696,20 @@ function InteractiveMessage({ payload, onSelect, labels, className }) {
450
696
  ]
451
697
  }
452
698
  ),
453
- isListOpen ? /* @__PURE__ */ jsx7("div", { className: "mt-1 flex flex-col gap-1", children: sections.map((section, sectionIndex) => /* @__PURE__ */ jsxs5("div", { className: "flex flex-col", children: [
454
- section.title ? /* @__PURE__ */ jsx7("p", { className: "px-3 py-1 text-xs font-semibold uppercase text-gray-500 dark:text-gray-400", children: section.title }) : null,
455
- (section.rows ?? []).map((row) => /* @__PURE__ */ jsxs5(
699
+ isListOpen ? /* @__PURE__ */ jsx8("div", { className: "mt-1 flex flex-col gap-1", children: sections.map((section, sectionIndex) => /* @__PURE__ */ jsxs6("div", { className: "flex flex-col", children: [
700
+ section.title ? /* @__PURE__ */ jsx8("p", { className: "px-3 py-1 text-xs font-semibold uppercase text-gray-500 dark:text-gray-400", children: section.title }) : null,
701
+ (section.rows ?? []).map((row) => /* @__PURE__ */ jsxs6(
456
702
  "button",
457
703
  {
704
+ "data-cv-tooltip": row.title,
705
+ "aria-label": row.title,
458
706
  type: "button",
459
707
  disabled: !isInteractable,
460
708
  onClick: () => onSelect?.({ kind: "list", option: row }),
461
709
  className: "rounded-md px-3 py-1.5 text-left text-sm text-gray-900 transition-colors enabled:hover:bg-gray-100 disabled:cursor-default dark:text-gray-100 dark:enabled:hover:bg-gray-700",
462
710
  children: [
463
- /* @__PURE__ */ jsx7("span", { className: "block", children: row.title }),
464
- row.description ? /* @__PURE__ */ jsx7("span", { className: "block text-xs text-gray-500 dark:text-gray-400", children: row.description }) : null
711
+ /* @__PURE__ */ jsx8("span", { className: "block", children: row.title }),
712
+ row.description ? /* @__PURE__ */ jsx8("span", { className: "block text-xs text-gray-500 dark:text-gray-400", children: row.description }) : null
465
713
  ]
466
714
  },
467
715
  row.id
@@ -484,24 +732,25 @@ function createMediaUrlResolver(api) {
484
732
  }
485
733
 
486
734
  // src/providers/ConversationsProvider.tsx
487
- import { createContext as createContext2, useContext as useContext2 } from "react";
488
- import { jsx as jsx8 } from "react/jsx-runtime";
735
+ import { createContext as createContext2, useContext as useContext2, useMemo } from "react";
736
+ import { jsx as jsx9 } from "react/jsx-runtime";
489
737
  var ConversationsContext = createContext2(null);
490
738
  function ConversationsProvider({
491
739
  api,
492
740
  sse,
493
741
  children
494
742
  }) {
495
- return /* @__PURE__ */ jsx8(ConversationsContext.Provider, { value: { api, sse }, children });
743
+ const value = useMemo(() => ({ api, sse }), [api, sse]);
744
+ return /* @__PURE__ */ jsx9(ConversationsContext.Provider, { value, children });
496
745
  }
497
746
  function useConversations() {
498
747
  return useContext2(ConversationsContext);
499
748
  }
500
749
 
501
750
  // src/MessageBubble.tsx
502
- import { useMemo, useState as useState4 } from "react";
503
- import { Check } from "lucide-react";
504
- import { Fragment, jsx as jsx9, jsxs as jsxs6 } from "react/jsx-runtime";
751
+ import { useMemo as useMemo2, useState as useState5 } from "react";
752
+ import { Check as Check2 } from "lucide-react";
753
+ import { Fragment as Fragment2, jsx as jsx10, jsxs as jsxs7 } from "react/jsx-runtime";
505
754
  var BUBBLE_COLOR = {
506
755
  agent: "bg-[#d9fdd3] dark:bg-[#005c4b]",
507
756
  bot: "bg-[#d7f0ec] dark:bg-[#0a3d3a]",
@@ -518,40 +767,51 @@ function MessageBubble({
518
767
  onToggleSelect,
519
768
  onResolveMediaUrl,
520
769
  onInteractiveSelect,
770
+ onTranscribeAudio,
521
771
  className
522
772
  }) {
523
773
  const { bubble, selection } = useConversationLocales();
524
- const [lightboxSrc, setLightboxSrc] = useState4(null);
774
+ const [lightboxSrc, setLightboxSrc] = useState5(null);
525
775
  const context = useConversations();
526
- const resolveMediaUrl = useMemo(
776
+ const resolveMediaUrl = useMemo2(
527
777
  () => onResolveMediaUrl ?? (context?.api ? createMediaUrlResolver(context.api) : void 0),
528
778
  [onResolveMediaUrl, context?.api]
529
779
  );
780
+ const requestTranscription = useMemo2(() => {
781
+ const transcribe = onTranscribeAudio ?? context?.api?.transcribeAudio?.bind(context.api);
782
+ if (!transcribe) return void 0;
783
+ return () => transcribe(message.id);
784
+ }, [onTranscribeAudio, context?.api, message.id]);
530
785
  const bubbleColor = BUBBLE_COLOR[message.sender] ?? BUBBLE_COLOR.customer;
531
786
  const hasError = message.status === "failed";
532
787
  const isMedia = MEDIA_TYPES.has(message.type);
533
788
  const isTemplate = message.type === "template";
534
789
  const isInteractive = message.type === "interactive";
790
+ const mediaCaption = isMedia ? [message.caption, message.content].find((text) => {
791
+ const trimmed = text?.trim();
792
+ return trimmed && trimmed !== message.filename;
793
+ }) : void 0;
535
794
  const displayName = message.sender === "agent" && senderName ? senderName : bubble[message.sender] ?? message.sender;
536
795
  const tooltipText = message.status === "read" && message.readAt ? `${bubble.readAt}${formatDateTime(message.readAt)}` : message.status === "failed" ? bubble.windowExpired : void 0;
537
796
  const tailCornerClass = isFirstInGroup ? isMine ? "rounded-tr-md" : "rounded-tl-md" : "";
538
- const checkbox = /* @__PURE__ */ jsx9(
797
+ const checkbox = /* @__PURE__ */ jsx10(
539
798
  "button",
540
799
  {
541
800
  onClick: (e) => {
542
801
  e.stopPropagation();
543
802
  onToggleSelect?.();
544
803
  },
545
- title: selection.select,
804
+ "data-cv-tooltip": selection.select,
805
+ "aria-label": selection.select,
546
806
  className: `
547
807
  flex-shrink-0 self-end mb-1 w-5 h-5 rounded-full border-2 flex items-center justify-center transition-all
548
808
  ${isSelected ? "bg-teal-600 border-teal-600" : "bg-white/80 dark:bg-black/40 border-black/20 dark:border-white/30"}
549
809
  ${isSelecting ? "opacity-100" : "opacity-0 group-hover:opacity-100"}
550
810
  `,
551
- children: isSelected && /* @__PURE__ */ jsx9(Check, { size: 12, className: "text-white", strokeWidth: 3 })
811
+ children: isSelected && /* @__PURE__ */ jsx10(Check2, { size: 12, className: "text-white", strokeWidth: 3 })
552
812
  }
553
813
  );
554
- return /* @__PURE__ */ jsxs6(
814
+ return /* @__PURE__ */ jsxs7(
555
815
  "div",
556
816
  {
557
817
  className: cn(
@@ -562,7 +822,7 @@ function MessageBubble({
562
822
  ),
563
823
  children: [
564
824
  isMine && checkbox,
565
- /* @__PURE__ */ jsxs6(
825
+ /* @__PURE__ */ jsxs7(
566
826
  "div",
567
827
  {
568
828
  onClick: isSelecting ? onToggleSelect : void 0,
@@ -575,48 +835,60 @@ function MessageBubble({
575
835
  relative
576
836
  `,
577
837
  children: [
578
- isMine && isFirstInGroup && (message.sender === "bot" || message.sender === "agent" && senderName) && /* @__PURE__ */ jsx9("div", { className: "text-xs font-semibold mb-0.5 text-teal-700 dark:text-teal-400", children: displayName }),
579
- message.moderation?.isOffensive && /* @__PURE__ */ jsxs6(
838
+ isMine && isFirstInGroup && (message.sender === "bot" || message.sender === "agent" && senderName) && /* @__PURE__ */ jsx10("div", { className: "text-xs font-semibold mb-0.5 text-teal-700 dark:text-teal-400", children: displayName }),
839
+ message.moderation?.isOffensive && /* @__PURE__ */ jsxs7(
580
840
  "div",
581
841
  {
582
842
  className: "mb-1 inline-flex items-center gap-1 rounded-full bg-amber-100 px-2 py-0.5 text-xs font-medium text-amber-800 dark:bg-amber-950/60 dark:text-amber-300",
583
- title: message.moderation.terms.length > 0 ? message.moderation.terms.join(", ") : void 0,
843
+ "data-cv-tooltip": message.moderation.terms.length > 0 ? message.moderation.terms.join(", ") : void 0,
584
844
  children: [
585
- /* @__PURE__ */ jsx9("span", { "aria-hidden": true, children: "\u26A0\uFE0F" }),
586
- /* @__PURE__ */ jsx9("span", { children: bubble.moderationFlagged })
845
+ /* @__PURE__ */ jsx10("span", { "aria-hidden": true, children: "\u26A0\uFE0F" }),
846
+ /* @__PURE__ */ jsx10("span", { children: bubble.moderationFlagged })
587
847
  ]
588
848
  }
589
849
  ),
590
- isMedia ? /* @__PURE__ */ jsx9(MediaRenderer, { message, onLightbox: setLightboxSrc, onResolveUrl: resolveMediaUrl }) : isInteractive && message.payload ? (
850
+ isMedia ? /* @__PURE__ */ jsxs7(Fragment2, { children: [
851
+ /* @__PURE__ */ jsx10(
852
+ MediaRenderer,
853
+ {
854
+ message,
855
+ onLightbox: setLightboxSrc,
856
+ onResolveUrl: resolveMediaUrl,
857
+ ...requestTranscription ? { onTranscribeAudio: requestTranscription } : {}
858
+ }
859
+ ),
860
+ mediaCaption ? /* @__PURE__ */ jsx10("div", { className: "mt-1 text-sm text-gray-900 dark:text-gray-100 whitespace-pre-wrap break-words leading-[19px]", children: parseWhatsAppFormatting(mediaCaption) }) : null
861
+ ] }) : isInteractive && message.payload ? (
591
862
  // O texto da mensagem interativa mora dentro do payload (`body.text`), e `content` guarda
592
863
  // só uma cópia achatada para busca — renderizar `content` aqui duplicaria o corpo.
593
- /* @__PURE__ */ jsx9(InteractiveMessage, { payload: message.payload, onSelect: onInteractiveSelect })
594
- ) : /* @__PURE__ */ jsxs6(Fragment, { children: [
595
- isTemplate && /* @__PURE__ */ jsxs6("p", { className: "text-xs text-gray-500 dark:text-gray-400 flex items-center gap-1 mb-0.5", children: [
596
- /* @__PURE__ */ jsx9("span", { children: "\u{1F4E8}" }),
597
- /* @__PURE__ */ jsxs6("span", { className: "font-medium", children: [
864
+ /* @__PURE__ */ jsx10(InteractiveMessage, { payload: message.payload, onSelect: onInteractiveSelect })
865
+ ) : /* @__PURE__ */ jsxs7(Fragment2, { children: [
866
+ isTemplate && /* @__PURE__ */ jsxs7("p", { className: "text-xs text-gray-500 dark:text-gray-400 flex items-center gap-1 mb-0.5", children: [
867
+ /* @__PURE__ */ jsx10("span", { children: "\u{1F4E8}" }),
868
+ /* @__PURE__ */ jsxs7("span", { className: "font-medium", children: [
598
869
  bubble.templateLabel,
599
870
  message.templateName ? ` \u2014 ${message.templateName}` : ""
600
871
  ] })
601
872
  ] }),
602
- /* @__PURE__ */ jsx9("div", { className: "text-sm text-gray-900 dark:text-gray-100 whitespace-pre-wrap break-words leading-[19px]", children: parseWhatsAppFormatting(message.content ?? "") })
873
+ /* @__PURE__ */ jsx10("div", { className: "text-sm text-gray-900 dark:text-gray-100 whitespace-pre-wrap break-words leading-[19px]", children: parseWhatsAppFormatting(message.content ?? "") })
603
874
  ] }),
604
- /* @__PURE__ */ jsxs6("div", { className: "flex items-center justify-end gap-1 mt-0.5 select-none", children: [
605
- /* @__PURE__ */ jsx9("span", { className: "text-xs text-black/40 dark:text-white/40 font-medium", children: formatTimestamp(message.timestamp) }),
606
- isMine && message.status && /* @__PURE__ */ jsx9(StatusTicks, { status: message.status, title: tooltipText })
875
+ /* @__PURE__ */ jsxs7("div", { className: "flex items-center justify-end gap-1 mt-0.5 select-none", children: [
876
+ /* @__PURE__ */ jsx10("span", { className: "text-xs text-black/40 dark:text-white/40 font-medium", children: formatTimestamp(message.timestamp) }),
877
+ isMine && message.status && /* @__PURE__ */ jsx10(StatusTicks, { status: message.status, title: tooltipText })
607
878
  ] })
608
879
  ]
609
880
  }
610
881
  ),
611
882
  !isMine && checkbox,
612
- lightboxSrc && /* @__PURE__ */ jsx9(Lightbox, { imageUrl: lightboxSrc, onClose: () => setLightboxSrc(null) })
883
+ lightboxSrc && /* @__PURE__ */ jsx10(Lightbox, { imageUrl: lightboxSrc, onClose: () => setLightboxSrc(null) })
613
884
  ]
614
885
  }
615
886
  );
616
887
  }
617
888
 
618
889
  // src/Wallpaper.tsx
619
- import { jsx as jsx10 } from "react/jsx-runtime";
890
+ import { forwardRef } from "react";
891
+ import { jsx as jsx11 } from "react/jsx-runtime";
620
892
  function doodlePattern(strokeColor, opacity) {
621
893
  const svg = `<svg xmlns='http://www.w3.org/2000/svg' width='100' height='100' viewBox='0 0 100 100'><g fill='none' stroke='${strokeColor}' stroke-width='1.2' opacity='${opacity}'><circle cx='15' cy='15' r='3'/><path d='M40 10 q5 8 0 16 q-5 -8 0 -16z'/><circle cx='70' cy='28' r='2'/><path d='M18 55 l6 6 m-6 0 l6 -6'/><circle cx='55' cy='68' r='2.5'/><path d='M85 58 q6 6 0 12 q-6 -6 0 -12z'/><circle cx='8' cy='85' r='2'/><path d='M65 90 l5 5 m-5 0 l5 -5'/></g></svg>`;
622
894
  return `url("data:image/svg+xml,${encodeURIComponent(svg)}")`;
@@ -633,17 +905,21 @@ var DARK_WALLPAPER = {
633
905
  backgroundRepeat: "repeat",
634
906
  backgroundSize: "100px 100px"
635
907
  };
636
- function ConversationWallpaper({ children, className, style }) {
637
- const isDark = useIsDarkTheme();
638
- return /* @__PURE__ */ jsx10(
639
- "div",
640
- {
641
- className: cn("cv-wallpaper", className),
642
- style: { ...isDark ? DARK_WALLPAPER : LIGHT_WALLPAPER, ...style },
643
- children
644
- }
645
- );
646
- }
908
+ var ConversationWallpaper = forwardRef(
909
+ function ConversationWallpaper2({ children, className, style, onScroll }, ref) {
910
+ const isDark = useIsDarkTheme();
911
+ return /* @__PURE__ */ jsx11(
912
+ "div",
913
+ {
914
+ ref,
915
+ onScroll,
916
+ className: cn("cv-wallpaper", className),
917
+ style: { ...isDark ? DARK_WALLPAPER : LIGHT_WALLPAPER, ...style },
918
+ children
919
+ }
920
+ );
921
+ }
922
+ );
647
923
 
648
924
  // src/emojiCatalog.ts
649
925
  var EMOJI_CATEGORIES = [
@@ -800,8 +1076,8 @@ function searchEmojis(query) {
800
1076
  }
801
1077
 
802
1078
  // src/EmojiPicker.tsx
803
- import { useState as useState5, useCallback, useMemo as useMemo2 } from "react";
804
- import { jsx as jsx11, jsxs as jsxs7 } from "react/jsx-runtime";
1079
+ import { useState as useState6, useCallback as useCallback2, useMemo as useMemo3 } from "react";
1080
+ import { jsx as jsx12, jsxs as jsxs8 } from "react/jsx-runtime";
805
1081
  var DEFAULT_EMOJI_PICKER_LABELS = {
806
1082
  search: "Buscar emoji",
807
1083
  noResults: "Nenhum emoji encontrado"
@@ -809,21 +1085,21 @@ var DEFAULT_EMOJI_PICKER_LABELS = {
809
1085
  var EmojiPicker = ({ onSelect, labels, className = "" }) => {
810
1086
  const searchLabel = labels?.search ?? DEFAULT_EMOJI_PICKER_LABELS.search;
811
1087
  const noResultsLabel = labels?.noResults ?? DEFAULT_EMOJI_PICKER_LABELS.noResults;
812
- const [activeCategory, setActiveCategory] = useState5(0);
813
- const [query, setQuery] = useState5("");
814
- const handleSelect = useCallback(
1088
+ const [activeCategory, setActiveCategory] = useState6(0);
1089
+ const [query, setQuery] = useState6("");
1090
+ const handleSelect = useCallback2(
815
1091
  (emoji) => {
816
1092
  onSelect(emoji);
817
1093
  },
818
1094
  [onSelect]
819
1095
  );
820
1096
  const isSearching = query.trim().length > 0;
821
- const visibleEntries = useMemo2(
1097
+ const visibleEntries = useMemo3(
822
1098
  () => isSearching ? searchEmojis(query) : EMOJI_CATEGORIES[activeCategory].entries,
823
1099
  [isSearching, query, activeCategory]
824
1100
  );
825
- return /* @__PURE__ */ jsxs7("div", { className: `bg-white border border-gray-200 rounded-lg shadow-lg overflow-hidden ${className}`, children: [
826
- /* @__PURE__ */ jsx11("div", { className: "p-2 border-b border-gray-200", children: /* @__PURE__ */ jsx11(
1101
+ return /* @__PURE__ */ jsxs8("div", { className: `bg-white border border-gray-200 rounded-lg shadow-lg overflow-hidden ${className}`, children: [
1102
+ /* @__PURE__ */ jsx12("div", { className: "p-2 border-b border-gray-200", children: /* @__PURE__ */ jsx12(
827
1103
  "input",
828
1104
  {
829
1105
  type: "search",
@@ -834,9 +1110,11 @@ var EmojiPicker = ({ onSelect, labels, className = "" }) => {
834
1110
  className: "w-full rounded-md border border-gray-200 px-2 py-1.5 text-sm outline-none focus:border-blue-500"
835
1111
  }
836
1112
  ) }),
837
- isSearching ? null : /* @__PURE__ */ jsx11("div", { className: "flex border-b border-gray-200 overflow-x-auto", children: EMOJI_CATEGORIES.map((category, index) => /* @__PURE__ */ jsxs7(
1113
+ isSearching ? null : /* @__PURE__ */ jsx12("div", { className: "flex border-b border-gray-200 overflow-x-auto", children: EMOJI_CATEGORIES.map((category, index) => /* @__PURE__ */ jsxs8(
838
1114
  "button",
839
1115
  {
1116
+ "data-cv-tooltip": category.name,
1117
+ "aria-label": category.name,
840
1118
  onClick: () => setActiveCategory(index),
841
1119
  className: `px-3 py-2 text-xs font-medium whitespace-nowrap border-b-2 transition-colors ${activeCategory === index ? "border-blue-500 text-blue-600" : "border-transparent text-gray-500 hover:text-gray-700"}`,
842
1120
  children: [
@@ -847,13 +1125,13 @@ var EmojiPicker = ({ onSelect, labels, className = "" }) => {
847
1125
  },
848
1126
  category.name
849
1127
  )) }),
850
- visibleEntries.length === 0 ? /* @__PURE__ */ jsx11("p", { className: "px-3 py-6 text-center text-sm text-gray-500", children: noResultsLabel }) : /* @__PURE__ */ jsx11("div", { className: "grid grid-cols-10 gap-0.5 p-2 max-h-[240px] overflow-y-auto", children: visibleEntries.map((entry) => /* @__PURE__ */ jsx11(
1128
+ visibleEntries.length === 0 ? /* @__PURE__ */ jsx12("p", { className: "px-3 py-6 text-center text-sm text-gray-500", children: noResultsLabel }) : /* @__PURE__ */ jsx12("div", { className: "grid grid-cols-10 gap-0.5 p-2 max-h-[240px] overflow-y-auto", children: visibleEntries.map((entry) => /* @__PURE__ */ jsx12(
851
1129
  "button",
852
1130
  {
853
1131
  onClick: () => handleSelect(entry.emoji),
854
1132
  className: "w-8 h-8 flex items-center justify-center text-lg hover:bg-gray-100 rounded transition-colors cursor-pointer",
855
1133
  "aria-label": entry.emoji,
856
- title: entry.keywords[0],
1134
+ "data-cv-tooltip": entry.keywords[0],
857
1135
  children: entry.emoji
858
1136
  },
859
1137
  entry.emoji
@@ -861,13 +1139,189 @@ var EmojiPicker = ({ onSelect, labels, className = "" }) => {
861
1139
  ] });
862
1140
  };
863
1141
 
1142
+ // src/AudioRecorderButton.tsx
1143
+ import { useCallback as useCallback3, useEffect as useEffect2, useRef as useRef2, useState as useState7 } from "react";
1144
+ import { jsx as jsx13, jsxs as jsxs9 } from "react/jsx-runtime";
1145
+ var DEFAULT_AUDIO_RECORDER_BUTTON_LABELS = {
1146
+ start: "Gravar \xE1udio",
1147
+ stop: "Parar grava\xE7\xE3o",
1148
+ unsupported: "Este navegador n\xE3o grava \xE1udio.",
1149
+ denied: "Sem permiss\xE3o para usar o microfone.",
1150
+ review: "Ou\xE7a antes de enviar",
1151
+ send: "Enviar \xE1udio",
1152
+ discard: "Descartar \xE1udio",
1153
+ empty: "Nada foi captado pelo microfone."
1154
+ };
1155
+ var DEFAULT_MAX_RECORDING_MILLISECONDS = 5 * 60 * 1e3;
1156
+ var RECORDING_FORMATS = [
1157
+ { mimeType: "audio/ogg;codecs=opus", uploadMimeType: "audio/ogg", extension: "ogg" },
1158
+ { mimeType: "audio/mp4", uploadMimeType: "audio/mp4", extension: "m4a" },
1159
+ { mimeType: "audio/webm", uploadMimeType: "audio/webm", extension: "webm" }
1160
+ ];
1161
+ function resolveRecordingFormat() {
1162
+ if (typeof MediaRecorder === "undefined") return void 0;
1163
+ if (typeof MediaRecorder.isTypeSupported !== "function") return RECORDING_FORMATS[0];
1164
+ return RECORDING_FORMATS.find((format) => MediaRecorder.isTypeSupported(format.mimeType));
1165
+ }
1166
+ function AudioRecorderButton({
1167
+ onRecorded,
1168
+ onFailure,
1169
+ onRecordingChange,
1170
+ reviewBeforeSend = true,
1171
+ maxDurationMilliseconds = DEFAULT_MAX_RECORDING_MILLISECONDS,
1172
+ labels,
1173
+ disabled
1174
+ }) {
1175
+ const labelOf = (key) => labels?.[key] ?? DEFAULT_AUDIO_RECORDER_BUTTON_LABELS[key];
1176
+ const [isRecording, setIsRecording] = useState7(false);
1177
+ const [pending, setPending] = useState7(void 0);
1178
+ const recorderRef = useRef2(null);
1179
+ const autoStopRef = useRef2(void 0);
1180
+ const discard = useCallback3(() => {
1181
+ setPending((current) => {
1182
+ if (current) URL.revokeObjectURL(current.objectURL);
1183
+ return void 0;
1184
+ });
1185
+ }, []);
1186
+ useEffect2(() => discard, [discard]);
1187
+ const confirm = useCallback3(() => {
1188
+ if (!pending) return;
1189
+ void onRecorded(pending.file);
1190
+ discard();
1191
+ }, [discard, onRecorded, pending]);
1192
+ const stop = useCallback3(() => {
1193
+ recorderRef.current?.stop();
1194
+ }, []);
1195
+ const start = useCallback3(async () => {
1196
+ const format = resolveRecordingFormat();
1197
+ if (!format || !navigator.mediaDevices?.getUserMedia) {
1198
+ onFailure?.(labels?.unsupported ?? DEFAULT_AUDIO_RECORDER_BUTTON_LABELS.unsupported);
1199
+ return;
1200
+ }
1201
+ try {
1202
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
1203
+ const recorder = new MediaRecorder(stream, { mimeType: format.mimeType });
1204
+ const chunks = [];
1205
+ recorder.addEventListener("dataavailable", (event) => {
1206
+ if (event.data.size > 0) chunks.push(event.data);
1207
+ });
1208
+ recorder.addEventListener("stop", () => {
1209
+ stream.getTracks().forEach((track) => track.stop());
1210
+ clearTimeout(autoStopRef.current);
1211
+ setIsRecording(false);
1212
+ onRecordingChange?.(false);
1213
+ recorderRef.current = null;
1214
+ const blob = new Blob(chunks, { type: format.uploadMimeType });
1215
+ const file = new File([blob], `audio-${Date.now()}.${format.extension}`, {
1216
+ type: format.uploadMimeType
1217
+ });
1218
+ if (blob.size === 0) {
1219
+ onFailure?.(labelOf("empty"));
1220
+ return;
1221
+ }
1222
+ if (!reviewBeforeSend) {
1223
+ void onRecorded(file);
1224
+ return;
1225
+ }
1226
+ setPending({ file, objectURL: URL.createObjectURL(blob) });
1227
+ });
1228
+ recorderRef.current = recorder;
1229
+ recorder.start();
1230
+ autoStopRef.current = setTimeout(() => recorder.stop(), maxDurationMilliseconds);
1231
+ setIsRecording(true);
1232
+ onRecordingChange?.(true);
1233
+ } catch {
1234
+ onFailure?.(labels?.denied ?? DEFAULT_AUDIO_RECORDER_BUTTON_LABELS.denied);
1235
+ }
1236
+ }, [maxDurationMilliseconds, onFailure, onRecorded, onRecordingChange, reviewBeforeSend]);
1237
+ const toggleLabel = isRecording ? labelOf("stop") : labelOf("start");
1238
+ return (
1239
+ /* O painel de revisão flutua sobre o botão em vez de ocupar espaço na barra: o microfone mora
1240
+ na caixa do botão de enviar, e empurrar o composer para cima a cada gravação faria a
1241
+ conversa saltar. */
1242
+ /* @__PURE__ */ jsxs9("div", { className: "relative flex-shrink-0", children: [
1243
+ pending && /* @__PURE__ */ jsxs9(
1244
+ "div",
1245
+ {
1246
+ role: "group",
1247
+ "aria-label": labelOf("review"),
1248
+ className: "absolute bottom-full right-0 z-20 mb-2 flex w-[calc(100vw-2rem)] max-w-[20rem] flex-wrap items-center justify-end gap-2 rounded-xl border border-gray-200 bg-white p-2 shadow-lg dark:border-gray-700 dark:bg-gray-800",
1249
+ children: [
1250
+ /* @__PURE__ */ jsx13("div", { className: "min-w-0 flex-1", children: /* @__PURE__ */ jsx13(AudioPlayer, { src: pending.objectURL }) }),
1251
+ /* @__PURE__ */ jsx13(
1252
+ "button",
1253
+ {
1254
+ type: "button",
1255
+ onClick: discard,
1256
+ "data-cv-tooltip": labelOf("discard"),
1257
+ "aria-label": labelOf("discard"),
1258
+ className: "flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full text-gray-500 transition-colors hover:bg-gray-100 hover:text-red-500 dark:hover:bg-gray-700",
1259
+ children: /* @__PURE__ */ jsxs9("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", children: [
1260
+ /* @__PURE__ */ jsx13("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
1261
+ /* @__PURE__ */ jsx13("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
1262
+ ] })
1263
+ }
1264
+ ),
1265
+ /* @__PURE__ */ jsx13(
1266
+ "button",
1267
+ {
1268
+ type: "button",
1269
+ onClick: confirm,
1270
+ "data-cv-tooltip": labelOf("send"),
1271
+ "aria-label": labelOf("send"),
1272
+ className: "flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full bg-emerald-500 text-white transition-colors hover:bg-emerald-600",
1273
+ children: /* @__PURE__ */ jsx13("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "currentColor", children: /* @__PURE__ */ jsx13("path", { d: "M2.01 21L23 12 2.01 3 2 10l15 2-15 2z" }) })
1274
+ }
1275
+ )
1276
+ ]
1277
+ }
1278
+ ),
1279
+ /* @__PURE__ */ jsx13(
1280
+ "button",
1281
+ {
1282
+ type: "button",
1283
+ disabled: disabled || pending !== void 0,
1284
+ onClick: () => isRecording ? stop() : void start(),
1285
+ "data-cv-tooltip": toggleLabel,
1286
+ "aria-label": toggleLabel,
1287
+ "aria-pressed": isRecording,
1288
+ className: `flex h-10 w-10 items-center justify-center rounded-full transition-colors disabled:opacity-50 ${isRecording ? "animate-pulse bg-red-500 text-white ring-4 ring-red-500/30 hover:bg-red-600" : "text-gray-500 hover:bg-gray-200 dark:hover:bg-gray-700"}`,
1289
+ children: isRecording ? /* @__PURE__ */ jsx13("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "currentColor", children: /* @__PURE__ */ jsx13("rect", { x: "6", y: "6", width: "12", height: "12", rx: "2" }) }) : /* @__PURE__ */ jsxs9("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", children: [
1290
+ /* @__PURE__ */ jsx13("path", { d: "M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3z" }),
1291
+ /* @__PURE__ */ jsx13("path", { d: "M19 11a7 7 0 0 1-14 0" }),
1292
+ /* @__PURE__ */ jsx13("line", { x1: "12", y1: "18", x2: "12", y2: "22" })
1293
+ ] })
1294
+ }
1295
+ )
1296
+ ] })
1297
+ );
1298
+ }
1299
+
1300
+ // src/MessageComposer.tsx
1301
+ import { useState as useState8, useRef as useRef3, useCallback as useCallback4 } from "react";
1302
+
1303
+ // src/composer.constant.ts
1304
+ var COMPOSER_BAR_CLASS = "cv-composer-bar";
1305
+ var COMPOSER_TOOL_BUTTON_CLASS = "flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-full transition-colors hover:bg-teal-50 hover:text-teal-600 dark:hover:bg-teal-900/30 dark:hover:text-teal-400";
1306
+ var COMPOSER_TOOL_BUTTON_ACTIVE_CLASS = "bg-teal-50 text-teal-600 dark:bg-teal-900/40 dark:text-teal-400";
1307
+ var COMPOSER_TOOL_BUTTON_IDLE_CLASS = "text-gray-400 dark:text-gray-500";
1308
+ var COMPOSER_MONOSPACE_CLASS = "bg-black/5 dark:bg-white/10 rounded px-0.5 font-mono text-sm";
1309
+ var COMPOSER_COMPACT_WIDTH = 480;
1310
+ var QUICK_REPLY_PILL_CLASS = "whitespace-nowrap rounded-full border border-gray-200 bg-white px-3 py-1.5 text-xs text-gray-600 shadow-sm transition-colors hover:border-teal-200 hover:bg-teal-50 hover:text-teal-700 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-400 dark:hover:border-teal-800 dark:hover:bg-teal-950/40 dark:hover:text-teal-400";
1311
+
864
1312
  // src/MessageComposer.tsx
865
- import { useState as useState6, useRef as useRef2, useCallback as useCallback2 } from "react";
866
- import { Fragment as Fragment2, jsx as jsx12, jsxs as jsxs8 } from "react/jsx-runtime";
1313
+ import { Fragment as Fragment3, jsx as jsx14, jsxs as jsxs10 } from "react/jsx-runtime";
1314
+ function applyQuickReplyVariables(template, variables = {}) {
1315
+ return template.replace(/\{\{\s*([\w.]+)\s*\}\}/g, (_, chave) => variables[chave] ?? "");
1316
+ }
1317
+ function resolveQuickReply(quickReply, variables = {}) {
1318
+ return typeof quickReply.text === "function" ? quickReply.text(variables) : applyQuickReplyVariables(quickReply.text, variables);
1319
+ }
867
1320
  var DEFAULT_MESSAGE_COMPOSER_LABELS = {
868
1321
  emoji: "Emoji",
869
1322
  attach: "Anexar",
870
- send: "Enviar"
1323
+ send: "Enviar",
1324
+ removeAttachment: "Remover anexo"
871
1325
  };
872
1326
  var DEFAULT_ACCEPTED_FILE_TYPES = [
873
1327
  "image/jpeg,image/png,image/webp",
@@ -884,11 +1338,14 @@ var MessageComposer = ({
884
1338
  onAttach,
885
1339
  value: externalValue,
886
1340
  onChange: externalOnChange,
1341
+ quickReplies,
1342
+ quickReplyVariables,
887
1343
  features,
888
1344
  placeholder = "Digite uma mensagem...",
889
1345
  maxLength,
890
1346
  disabled = false,
891
1347
  acceptedFileTypes = DEFAULT_ACCEPTED_FILE_TYPES,
1348
+ idleAction,
892
1349
  className,
893
1350
  classNames,
894
1351
  labels
@@ -896,14 +1353,15 @@ var MessageComposer = ({
896
1353
  const emojiLabel = labels?.emoji ?? DEFAULT_MESSAGE_COMPOSER_LABELS.emoji;
897
1354
  const attachLabel = labels?.attach ?? DEFAULT_MESSAGE_COMPOSER_LABELS.attach;
898
1355
  const sendLabel = labels?.send ?? DEFAULT_MESSAGE_COMPOSER_LABELS.send;
899
- const [internalText, setInternalText] = useState6("");
900
- const [showEmoji, setShowEmoji] = useState6(false);
901
- const [attachments, setAttachments] = useState6([]);
902
- const textareaRef = useRef2(null);
903
- const fileInputRef = useRef2(null);
1356
+ const removeAttachmentLabel = labels?.removeAttachment ?? DEFAULT_MESSAGE_COMPOSER_LABELS.removeAttachment;
1357
+ const [internalText, setInternalText] = useState8("");
1358
+ const [showEmoji, setShowEmoji] = useState8(false);
1359
+ const [attachments, setAttachments] = useState8([]);
1360
+ const textareaRef = useRef3(null);
1361
+ const fileInputRef = useRef3(null);
904
1362
  const isControlled = externalValue !== void 0;
905
1363
  const text = isControlled ? externalValue : internalText;
906
- const setText = useCallback2((newText) => {
1364
+ const setText = useCallback4((newText) => {
907
1365
  if (isControlled) {
908
1366
  externalOnChange?.(newText);
909
1367
  } else {
@@ -912,7 +1370,7 @@ var MessageComposer = ({
912
1370
  }, [isControlled, externalOnChange]);
913
1371
  const showEmojiButton = features?.emoji !== false;
914
1372
  const showAttachButton = features?.documents !== false;
915
- const sendMessage = useCallback2(() => {
1373
+ const sendMessage = useCallback4(() => {
916
1374
  const trimmed = text.trim();
917
1375
  if (!trimmed && attachments.length === 0) return;
918
1376
  if (trimmed) onSend(trimmed);
@@ -925,19 +1383,19 @@ var MessageComposer = ({
925
1383
  setShowEmoji(false);
926
1384
  if (textareaRef.current) textareaRef.current.style.height = "auto";
927
1385
  }, [text, attachments, onSend, onAttach, isControlled]);
928
- const handleKeyDown = useCallback2((e) => {
1386
+ const handleKeyDown = useCallback4((e) => {
929
1387
  if (e.key === "Enter" && !e.shiftKey) {
930
1388
  e.preventDefault();
931
1389
  if (!disabled) sendMessage();
932
1390
  }
933
1391
  }, [sendMessage, disabled]);
934
- const handleInput = useCallback2(() => {
1392
+ const handleInput = useCallback4(() => {
935
1393
  const ta = textareaRef.current;
936
1394
  if (!ta) return;
937
1395
  ta.style.height = "auto";
938
1396
  ta.style.height = `${Math.min(ta.scrollHeight, 100)}px`;
939
1397
  }, []);
940
- const handleEmojiSelect = useCallback2((emoji) => {
1398
+ const handleEmojiSelect = useCallback4((emoji) => {
941
1399
  const ta = textareaRef.current;
942
1400
  if (!ta) {
943
1401
  setText(text + emoji);
@@ -953,7 +1411,7 @@ var MessageComposer = ({
953
1411
  handleInput();
954
1412
  });
955
1413
  }, [text, setText, handleInput]);
956
- const handleFileChange = useCallback2((e) => {
1414
+ const handleFileChange = useCallback4((e) => {
957
1415
  const files = e.target.files;
958
1416
  if (!files) return;
959
1417
  const previews = [];
@@ -964,7 +1422,7 @@ var MessageComposer = ({
964
1422
  setAttachments((prev) => [...prev, ...previews]);
965
1423
  if (fileInputRef.current) fileInputRef.current.value = "";
966
1424
  }, []);
967
- const removeAttachment = useCallback2((index) => {
1425
+ const removeAttachment = useCallback4((index) => {
968
1426
  setAttachments((prev) => {
969
1427
  const next = [...prev];
970
1428
  if (next[index].previewUrl) URL.revokeObjectURL(next[index].previewUrl);
@@ -972,7 +1430,7 @@ var MessageComposer = ({
972
1430
  return next;
973
1431
  });
974
1432
  }, []);
975
- const insertFormatting = useCallback2((marker) => {
1433
+ const insertFormatting = useCallback4((marker) => {
976
1434
  const ta = textareaRef.current;
977
1435
  if (!ta) return;
978
1436
  const start = ta.selectionStart;
@@ -986,31 +1444,56 @@ var MessageComposer = ({
986
1444
  });
987
1445
  }
988
1446
  }, [text, setText]);
1447
+ const effectiveIdleAction = idleAction ?? (onAttach ? /* @__PURE__ */ jsx14(AudioRecorderButton, { onRecorded: (file) => onAttach(file) }) : void 0);
989
1448
  const canSend = text.trim().length > 0 || attachments.length > 0;
990
1449
  const remaining = maxLength ? maxLength - text.length : null;
991
1450
  return (
992
1451
  /* A barra é a superfície (cinza, largura cheia, sem raio) e o campo dentro é que arredonda —
993
1452
  ordem do WhatsApp. Invertido, o pill arredondado ia até a borda da tela e os cantos
994
1453
  descobriam o fundo branco da página, que lia como defeito. */
995
- /* @__PURE__ */ jsxs8("div", { className: cn("bg-[#f0f2f5] px-2 py-2", className), children: [
996
- attachments.length > 0 && /* @__PURE__ */ jsx12("div", { className: "flex gap-2 px-1 pb-2 overflow-x-auto", children: attachments.map((a, i) => /* @__PURE__ */ jsxs8("div", { className: "relative flex-shrink-0", children: [
997
- a.previewUrl ? /* @__PURE__ */ jsx12("img", { src: a.previewUrl, alt: "", className: "w-16 h-16 object-cover rounded-lg border border-gray-200" }) : /* @__PURE__ */ jsx12("div", { className: "w-16 h-16 bg-gray-100 rounded-lg border border-gray-200 flex items-center justify-center", children: /* @__PURE__ */ jsxs8("svg", { width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "#9ca3af", strokeWidth: "1.5", children: [
998
- /* @__PURE__ */ jsx12("path", { d: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" }),
999
- /* @__PURE__ */ jsx12("polyline", { points: "14 2 14 8 20 8" })
1454
+ /* @__PURE__ */ jsxs10("div", { className: cn(COMPOSER_BAR_CLASS, className), children: [
1455
+ quickReplies && quickReplies.length > 0 && /* @__PURE__ */ jsx14(
1456
+ "div",
1457
+ {
1458
+ className: cn(
1459
+ "mb-2 flex flex-nowrap gap-1 overflow-x-auto px-1 sm:flex-wrap sm:overflow-x-visible [scrollbar-width:none] [&::-webkit-scrollbar]:hidden",
1460
+ classNames?.quickReplies
1461
+ ),
1462
+ children: quickReplies.map((quickReply) => /* @__PURE__ */ jsx14(
1463
+ "button",
1464
+ {
1465
+ "data-cv-tooltip": quickReply.label,
1466
+ "aria-label": quickReply.label,
1467
+ type: "button",
1468
+ onClick: () => {
1469
+ setText(resolveQuickReply(quickReply, quickReplyVariables));
1470
+ textareaRef.current?.focus();
1471
+ },
1472
+ className: cn(QUICK_REPLY_PILL_CLASS, classNames?.quickReply),
1473
+ children: quickReply.label
1474
+ },
1475
+ quickReply.key
1476
+ ))
1477
+ }
1478
+ ),
1479
+ attachments.length > 0 && /* @__PURE__ */ jsx14("div", { className: "flex gap-2 px-1 pb-2 overflow-x-auto", children: attachments.map((a, i) => /* @__PURE__ */ jsxs10("div", { className: "relative flex-shrink-0", children: [
1480
+ a.previewUrl ? /* @__PURE__ */ jsx14("img", { src: a.previewUrl, alt: "", className: "w-16 h-16 object-cover rounded-lg border border-gray-200" }) : /* @__PURE__ */ jsx14("div", { className: "w-16 h-16 bg-gray-100 rounded-lg border border-gray-200 flex items-center justify-center", children: /* @__PURE__ */ jsxs10("svg", { width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "#9ca3af", strokeWidth: "1.5", children: [
1481
+ /* @__PURE__ */ jsx14("path", { d: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" }),
1482
+ /* @__PURE__ */ jsx14("polyline", { points: "14 2 14 8 20 8" })
1000
1483
  ] }) }),
1001
- /* @__PURE__ */ jsx12("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" })
1484
+ /* @__PURE__ */ jsx14("button", { "data-cv-tooltip": removeAttachmentLabel, "aria-label": removeAttachmentLabel, onClick: () => removeAttachment(i), className: "absolute -top-2 -right-2 w-5 h-5 bg-gray-600 text-white rounded-full flex items-center justify-center hover:bg-gray-800 text-xs", children: "\u2715" })
1002
1485
  ] }, i)) }),
1003
- /* @__PURE__ */ jsxs8("div", { className: cn("flex items-end gap-1.5 rounded-xl bg-white px-3 py-2", classNames?.field), children: [
1004
- showEmojiButton && /* @__PURE__ */ jsxs8("div", { className: "relative flex-shrink-0", children: [
1005
- /* @__PURE__ */ jsx12("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__ */ jsxs8("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
1006
- /* @__PURE__ */ jsx12("circle", { cx: "12", cy: "12", r: "10" }),
1007
- /* @__PURE__ */ jsx12("path", { d: "M8 14s1.5 2 4 2 4-2 4-2" }),
1008
- /* @__PURE__ */ jsx12("circle", { cx: "9", cy: "9", r: "0.5", fill: "currentColor" }),
1009
- /* @__PURE__ */ jsx12("circle", { cx: "15", cy: "9", r: "0.5", fill: "currentColor" })
1486
+ /* @__PURE__ */ jsxs10("div", { className: cn("flex items-end gap-1.5 rounded-xl bg-white px-3 py-2", classNames?.field), children: [
1487
+ showEmojiButton && /* @__PURE__ */ jsxs10("div", { className: "relative flex-shrink-0", children: [
1488
+ /* @__PURE__ */ jsx14("button", { "data-cv-tooltip": emojiLabel, onClick: () => setShowEmoji((v) => !v), className: "w-9 h-9 flex items-center justify-center rounded-full text-gray-500 hover:bg-gray-200 transition-colors", "aria-label": emojiLabel, children: /* @__PURE__ */ jsxs10("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
1489
+ /* @__PURE__ */ jsx14("circle", { cx: "12", cy: "12", r: "10" }),
1490
+ /* @__PURE__ */ jsx14("path", { d: "M8 14s1.5 2 4 2 4-2 4-2" }),
1491
+ /* @__PURE__ */ jsx14("circle", { cx: "9", cy: "9", r: "0.5", fill: "currentColor" }),
1492
+ /* @__PURE__ */ jsx14("circle", { cx: "15", cy: "9", r: "0.5", fill: "currentColor" })
1010
1493
  ] }) }),
1011
- showEmoji && /* @__PURE__ */ jsx12("div", { className: "absolute bottom-full left-0 mb-2 z-10", children: /* @__PURE__ */ jsx12(EmojiPicker, { onSelect: handleEmojiSelect }) })
1494
+ showEmoji && /* @__PURE__ */ jsx14("div", { className: "absolute bottom-full left-0 mb-2 z-10", children: /* @__PURE__ */ jsx14(EmojiPicker, { onSelect: handleEmojiSelect }) })
1012
1495
  ] }),
1013
- /* @__PURE__ */ jsx12(
1496
+ /* @__PURE__ */ jsx14(
1014
1497
  "textarea",
1015
1498
  {
1016
1499
  ref: textareaRef,
@@ -1027,31 +1510,32 @@ var MessageComposer = ({
1027
1510
  style: { fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" }
1028
1511
  }
1029
1512
  ),
1030
- showAttachButton && /* @__PURE__ */ jsxs8(Fragment2, { children: [
1031
- /* @__PURE__ */ jsx12("input", { ref: fileInputRef, type: "file", multiple: true, accept: acceptedFileTypes, onChange: handleFileChange, className: "hidden" }),
1032
- /* @__PURE__ */ jsx12("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__ */ jsx12("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: /* @__PURE__ */ jsx12("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" }) }) })
1513
+ showAttachButton && /* @__PURE__ */ jsxs10(Fragment3, { children: [
1514
+ /* @__PURE__ */ jsx14("input", { ref: fileInputRef, type: "file", multiple: true, accept: acceptedFileTypes, onChange: handleFileChange, className: "hidden" }),
1515
+ /* @__PURE__ */ jsx14("button", { "data-cv-tooltip": attachLabel, onClick: () => fileInputRef.current?.click(), className: "w-9 h-9 flex items-center justify-center rounded-full text-gray-500 hover:bg-gray-200 flex-shrink-0 transition-colors", "aria-label": attachLabel, children: /* @__PURE__ */ jsx14("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: /* @__PURE__ */ jsx14("path", { d: "M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48" }) }) })
1033
1516
  ] }),
1034
- /* @__PURE__ */ jsx12(
1517
+ !canSend && effectiveIdleAction ? /* @__PURE__ */ jsx14("div", { className: "flex-shrink-0", children: effectiveIdleAction }) : /* @__PURE__ */ jsx14(
1035
1518
  "button",
1036
1519
  {
1520
+ "data-cv-tooltip": sendLabel,
1037
1521
  onClick: sendMessage,
1038
1522
  disabled: !canSend || disabled,
1039
1523
  className: `w-10 h-10 flex items-center justify-center rounded-full flex-shrink-0 transition-all ${canSend && !disabled ? "bg-[#00a884] text-white hover:bg-[#06cf9c] shadow-sm" : "bg-gray-200 text-gray-400 cursor-not-allowed"}`,
1040
1524
  "aria-label": sendLabel,
1041
- children: /* @__PURE__ */ jsx12("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "currentColor", children: /* @__PURE__ */ jsx12("path", { d: "M2.01 21L23 12 2.01 3 2 10l15 2-15 2z" }) })
1525
+ children: /* @__PURE__ */ jsx14("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "currentColor", children: /* @__PURE__ */ jsx14("path", { d: "M2.01 21L23 12 2.01 3 2 10l15 2-15 2z" }) })
1042
1526
  }
1043
1527
  )
1044
1528
  ] }),
1045
- remaining !== null && /* @__PURE__ */ jsx12("div", { className: "flex justify-end mt-1 pr-1", children: /* @__PURE__ */ jsx12("span", { className: `text-xs ${remaining < 20 ? "text-red-500" : "text-gray-400"}`, children: remaining }) })
1529
+ remaining !== null && /* @__PURE__ */ jsx14("div", { className: "flex justify-end mt-1 pr-1", children: /* @__PURE__ */ jsx14("span", { className: `text-xs ${remaining < 20 ? "text-red-500" : "text-gray-400"}`, children: remaining }) })
1046
1530
  ] })
1047
1531
  );
1048
1532
  };
1049
1533
 
1050
1534
  // src/DateDivider.tsx
1051
- import { jsx as jsx13 } from "react/jsx-runtime";
1535
+ import { jsx as jsx15 } from "react/jsx-runtime";
1052
1536
  function DateDivider({ iso, className, classNames }) {
1053
1537
  const { dateDivider } = useConversationLocales();
1054
- return /* @__PURE__ */ jsx13("div", { className: cn("flex justify-center sticky top-0 z-10 my-2 pointer-events-none", classNames?.root, className), children: /* @__PURE__ */ jsx13(
1538
+ return /* @__PURE__ */ jsx15("div", { className: cn("flex justify-center sticky top-0 z-10 my-2 pointer-events-none", classNames?.root, className), children: /* @__PURE__ */ jsx15(
1055
1539
  "span",
1056
1540
  {
1057
1541
  className: cn(
@@ -1117,14 +1601,93 @@ function phoneInitials(number) {
1117
1601
  return digits.slice(-2);
1118
1602
  }
1119
1603
 
1604
+ // src/conversationChannel.ts
1605
+ var CONVERSATION_CHANNEL = {
1606
+ WHATSAPP: "whatsapp",
1607
+ MESSENGER: "messenger",
1608
+ INSTAGRAM: "instagram",
1609
+ WEBCHAT: "webchat"
1610
+ };
1611
+ var DEFAULT_CONVERSATION_CHANNEL = CONVERSATION_CHANNEL.WHATSAPP;
1612
+ var REOPEN_MECHANISM = {
1613
+ TEMPLATE: "template",
1614
+ TAG: "tag",
1615
+ NONE: "none"
1616
+ };
1617
+ var HANDLE_KIND = {
1618
+ PHONE: "phone",
1619
+ USERNAME: "username",
1620
+ SESSION: "session"
1621
+ };
1622
+ var CHANNEL_CAPABILITIES = {
1623
+ [CONVERSATION_CHANNEL.WHATSAPP]: {
1624
+ label: "WhatsApp",
1625
+ icon: "\u{1F4AC}",
1626
+ hasSessionWindow: true,
1627
+ windowHours: 24,
1628
+ reopenMechanism: REOPEN_MECHANISM.TEMPLATE,
1629
+ handleKind: HANDLE_KIND.PHONE
1630
+ },
1631
+ [CONVERSATION_CHANNEL.MESSENGER]: {
1632
+ // Messenger também tem 24h, mas reabre com message tag — não com template aprovado.
1633
+ label: "Messenger",
1634
+ icon: "\u{1F4E8}",
1635
+ hasSessionWindow: true,
1636
+ windowHours: 24,
1637
+ reopenMechanism: REOPEN_MECHANISM.TAG,
1638
+ handleKind: HANDLE_KIND.USERNAME
1639
+ },
1640
+ [CONVERSATION_CHANNEL.INSTAGRAM]: {
1641
+ label: "Instagram",
1642
+ icon: "\u{1F4F7}",
1643
+ hasSessionWindow: true,
1644
+ windowHours: 24,
1645
+ reopenMechanism: REOPEN_MECHANISM.TAG,
1646
+ handleKind: HANDLE_KIND.USERNAME
1647
+ },
1648
+ [CONVERSATION_CHANNEL.WEBCHAT]: {
1649
+ // Chat próprio: sem intermediário, sem janela. Bloquear o composer aqui seria inventar limite.
1650
+ label: "Chat do site",
1651
+ icon: "\u{1F310}",
1652
+ hasSessionWindow: false,
1653
+ windowHours: 0,
1654
+ reopenMechanism: REOPEN_MECHANISM.NONE,
1655
+ handleKind: HANDLE_KIND.SESSION
1656
+ }
1657
+ };
1658
+ function capabilitiesOf(channel) {
1659
+ return CHANNEL_CAPABILITIES[channel ?? DEFAULT_CONVERSATION_CHANNEL];
1660
+ }
1661
+ var CHANNEL_FILTER_ALL = "all";
1662
+ function channelFiltersFor(conversations) {
1663
+ const present = new Set(
1664
+ conversations.map((conversation) => conversation.channel ?? DEFAULT_CONVERSATION_CHANNEL)
1665
+ );
1666
+ if (present.size < 2) return [];
1667
+ const ordered = Object.keys(CHANNEL_CAPABILITIES).filter((channel) => present.has(channel));
1668
+ return [
1669
+ { value: CHANNEL_FILTER_ALL, label: "Todos" },
1670
+ ...ordered.map((channel) => ({ value: channel, label: CHANNEL_CAPABILITIES[channel].label }))
1671
+ ];
1672
+ }
1673
+ function formatContactHandle(params) {
1674
+ const { handleKind } = capabilitiesOf(params.channel);
1675
+ if (handleKind === HANDLE_KIND.PHONE) return formatPhone(params.handle);
1676
+ if (handleKind === HANDLE_KIND.USERNAME) return params.handle.startsWith("@") ? params.handle : `@${params.handle}`;
1677
+ return `Visitante ${params.handle.slice(-6)}`;
1678
+ }
1679
+ function contactFlag(params) {
1680
+ return capabilitiesOf(params.channel).handleKind === HANDLE_KIND.PHONE ? phoneCountryFlag(params.handle) : "";
1681
+ }
1682
+
1120
1683
  // src/hooks/useAsyncResource.ts
1121
- import { useCallback as useCallback3, useEffect as useEffect2, useRef as useRef3, useState as useState7 } from "react";
1684
+ import { useCallback as useCallback5, useEffect as useEffect3, useRef as useRef4, useState as useState9 } from "react";
1122
1685
  function useAsyncResource(fetcher, deps) {
1123
- const [data, setData] = useState7(void 0);
1124
- const [loading, setLoading] = useState7(false);
1125
- const [error, setError] = useState7(void 0);
1126
- const requestIdRef = useRef3(0);
1127
- const load = useCallback3(async () => {
1686
+ const [data, setData] = useState9(void 0);
1687
+ const [loading, setLoading] = useState9(false);
1688
+ const [error, setError] = useState9(void 0);
1689
+ const requestIdRef = useRef4(0);
1690
+ const load = useCallback5(async () => {
1128
1691
  const requestId = ++requestIdRef.current;
1129
1692
  setLoading(true);
1130
1693
  setError(void 0);
@@ -1137,7 +1700,7 @@ function useAsyncResource(fetcher, deps) {
1137
1700
  if (requestId === requestIdRef.current) setLoading(false);
1138
1701
  }
1139
1702
  }, deps);
1140
- useEffect2(() => {
1703
+ useEffect3(() => {
1141
1704
  load();
1142
1705
  }, [load]);
1143
1706
  return { data, loading, error, refetch: load };
@@ -1172,9 +1735,19 @@ function useConversationDocuments(conversationId, params) {
1172
1735
  }
1173
1736
 
1174
1737
  // src/ConversationDocumentsPanel.tsx
1175
- import { useState as useState8 } from "react";
1738
+ import { useState as useState10 } from "react";
1176
1739
  import { ArrowUpDown, Bot, Download, Eye, Users } from "lucide-react";
1177
- import { jsx as jsx14, jsxs as jsxs9 } from "react/jsx-runtime";
1740
+
1741
+ // src/pagination.constant.ts
1742
+ var PAGINATION_LABELS = {
1743
+ first: "Primeira p\xE1gina",
1744
+ previous: "P\xE1gina anterior",
1745
+ next: "Pr\xF3xima p\xE1gina",
1746
+ last: "\xDAltima p\xE1gina"
1747
+ };
1748
+
1749
+ // src/ConversationDocumentsPanel.tsx
1750
+ import { jsx as jsx16, jsxs as jsxs11 } from "react/jsx-runtime";
1178
1751
  var DOCUMENT_SOURCE_FILTER = {
1179
1752
  ALL: "all",
1180
1753
  CUSTOMER: "customer",
@@ -1214,12 +1787,12 @@ function ConversationDocumentsPanel({
1214
1787
  }) {
1215
1788
  const labels = { ...DEFAULT_CONVERSATION_DOCUMENTS_LABELS, ...labelsOverride };
1216
1789
  const context = useConversations();
1217
- const [search, setSearch] = useState8("");
1218
- const [sourceFilter, setSourceFilter] = useState8(DOCUMENT_SOURCE_FILTER.ALL);
1219
- const [sortDirection, setSortDirection] = useState8("desc");
1220
- const [page, setPage] = useState8(1);
1221
- const [selectedIds, setSelectedIds] = useState8([]);
1222
- const [archiveError, setArchiveError] = useState8(false);
1790
+ const [search, setSearch] = useState10("");
1791
+ const [sourceFilter, setSourceFilter] = useState10(DOCUMENT_SOURCE_FILTER.ALL);
1792
+ const [sortDirection, setSortDirection] = useState10("desc");
1793
+ const [page, setPage] = useState10(1);
1794
+ const [selectedIds, setSelectedIds] = useState10([]);
1795
+ const [archiveError, setArchiveError] = useState10(false);
1223
1796
  const hasFilters = search !== "" || sourceFilter !== DOCUMENT_SOURCE_FILTER.ALL || sortDirection !== "desc";
1224
1797
  const { documents, total, loading, error } = useConversationDocuments(open ? conversationId : void 0, {
1225
1798
  search,
@@ -1270,10 +1843,10 @@ function ConversationDocumentsPanel({
1270
1843
  }
1271
1844
  }
1272
1845
  if (!open) return null;
1273
- return /* @__PURE__ */ jsx14("div", { className: cn("border-b", classNames?.root, className), children: /* @__PURE__ */ jsxs9("section", { className: cn("px-4 py-3", classNames?.body), children: [
1274
- /* @__PURE__ */ jsx14("p", { className: cn("mb-2 text-sm font-medium", classNames?.title), children: labels.title }),
1275
- /* @__PURE__ */ jsxs9("div", { className: cn("mb-2 flex flex-wrap items-center gap-2 border-b pb-2", classNames?.filters), children: [
1276
- /* @__PURE__ */ jsx14(
1846
+ return /* @__PURE__ */ jsx16("div", { className: cn("border-b", classNames?.root, className), children: /* @__PURE__ */ jsxs11("section", { className: cn("px-4 py-3", classNames?.body), children: [
1847
+ /* @__PURE__ */ jsx16("p", { className: cn("mb-2 text-sm font-medium", classNames?.title), children: labels.title }),
1848
+ /* @__PURE__ */ jsxs11("div", { className: cn("mb-2 flex flex-wrap items-center gap-2 border-b pb-2", classNames?.filters), children: [
1849
+ /* @__PURE__ */ jsx16(
1277
1850
  "input",
1278
1851
  {
1279
1852
  type: "search",
@@ -1284,7 +1857,7 @@ function ConversationDocumentsPanel({
1284
1857
  className: cn("w-full rounded-md border px-3 py-2 text-sm sm:w-52", classNames?.search)
1285
1858
  }
1286
1859
  ),
1287
- /* @__PURE__ */ jsxs9(
1860
+ /* @__PURE__ */ jsxs11(
1288
1861
  "select",
1289
1862
  {
1290
1863
  value: sourceFilter,
@@ -1292,27 +1865,31 @@ function ConversationDocumentsPanel({
1292
1865
  "aria-label": labels.sourceFilterAll,
1293
1866
  className: cn("w-full rounded-md border px-2 py-2 text-sm sm:w-36", classNames?.sourceSelect),
1294
1867
  children: [
1295
- /* @__PURE__ */ jsx14("option", { value: DOCUMENT_SOURCE_FILTER.ALL, children: labels.sourceFilterAll }),
1296
- /* @__PURE__ */ jsx14("option", { value: DOCUMENT_SOURCE_FILTER.CUSTOMER, children: labels.sourceFilterCustomer }),
1297
- /* @__PURE__ */ jsx14("option", { value: DOCUMENT_SOURCE_FILTER.TEAM, children: labels.sourceFilterTeam })
1868
+ /* @__PURE__ */ jsx16("option", { value: DOCUMENT_SOURCE_FILTER.ALL, children: labels.sourceFilterAll }),
1869
+ /* @__PURE__ */ jsx16("option", { value: DOCUMENT_SOURCE_FILTER.CUSTOMER, children: labels.sourceFilterCustomer }),
1870
+ /* @__PURE__ */ jsx16("option", { value: DOCUMENT_SOURCE_FILTER.TEAM, children: labels.sourceFilterTeam })
1298
1871
  ]
1299
1872
  }
1300
1873
  ),
1301
- /* @__PURE__ */ jsxs9(
1874
+ /* @__PURE__ */ jsxs11(
1302
1875
  "button",
1303
1876
  {
1877
+ "data-cv-tooltip": sortDirection === "desc" ? labels.sortMostRecent : labels.sortOldest,
1878
+ "aria-label": sortDirection === "desc" ? labels.sortMostRecent : labels.sortOldest,
1304
1879
  type: "button",
1305
1880
  onClick: () => applyFilter(() => setSortDirection(sortDirection === "desc" ? "asc" : "desc")),
1306
1881
  className: cn("cv-header-action inline-flex items-center gap-1", classNames?.sortButton),
1307
1882
  children: [
1308
- /* @__PURE__ */ jsx14(ArrowUpDown, { size: 14 }),
1883
+ /* @__PURE__ */ jsx16(ArrowUpDown, { size: 14 }),
1309
1884
  sortDirection === "desc" ? labels.sortMostRecent : labels.sortOldest
1310
1885
  ]
1311
1886
  }
1312
1887
  ),
1313
- hasFilters ? /* @__PURE__ */ jsx14(
1888
+ hasFilters ? /* @__PURE__ */ jsx16(
1314
1889
  "button",
1315
1890
  {
1891
+ "data-cv-tooltip": labels.clearFilters,
1892
+ "aria-label": labels.clearFilters,
1316
1893
  type: "button",
1317
1894
  onClick: () => applyFilter(() => {
1318
1895
  setSearch("");
@@ -1324,12 +1901,12 @@ function ConversationDocumentsPanel({
1324
1901
  }
1325
1902
  ) : null
1326
1903
  ] }),
1327
- loading ? /* @__PURE__ */ jsx14("p", { className: cn("text-xs text-gray-500", classNames?.status), children: labels.loading }) : null,
1328
- error ? /* @__PURE__ */ jsx14("p", { role: "alert", className: cn("text-xs text-red-600 dark:text-red-400", classNames?.status), children: labels.failure }) : null,
1329
- !loading && !error && documents.length === 0 ? /* @__PURE__ */ jsx14("p", { className: cn("text-xs text-gray-500", classNames?.status), children: hasFilters ? labels.noResults : labels.empty }) : null,
1330
- canArchive && documents.length > 0 ? /* @__PURE__ */ jsxs9("div", { className: cn("mb-2 flex flex-wrap items-center gap-3 text-xs", classNames?.selectionBar), children: [
1331
- /* @__PURE__ */ jsxs9("label", { className: "inline-flex items-center gap-1.5", children: [
1332
- /* @__PURE__ */ jsx14(
1904
+ loading ? /* @__PURE__ */ jsx16("p", { className: cn("text-xs text-gray-500", classNames?.status), children: labels.loading }) : null,
1905
+ error ? /* @__PURE__ */ jsx16("p", { role: "alert", className: cn("text-xs text-red-600 dark:text-red-400", classNames?.status), children: labels.failure }) : null,
1906
+ !loading && !error && documents.length === 0 ? /* @__PURE__ */ jsx16("p", { className: cn("text-xs text-gray-500", classNames?.status), children: hasFilters ? labels.noResults : labels.empty }) : null,
1907
+ canArchive && documents.length > 0 ? /* @__PURE__ */ jsxs11("div", { className: cn("mb-2 flex flex-wrap items-center gap-3 text-xs", classNames?.selectionBar), children: [
1908
+ /* @__PURE__ */ jsxs11("label", { className: "inline-flex items-center gap-1.5", children: [
1909
+ /* @__PURE__ */ jsx16(
1333
1910
  "input",
1334
1911
  {
1335
1912
  type: "checkbox",
@@ -1340,13 +1917,13 @@ function ConversationDocumentsPanel({
1340
1917
  ),
1341
1918
  labels.selectAll
1342
1919
  ] }),
1343
- selectedIds.length > 0 ? /* @__PURE__ */ jsx14("button", { type: "button", onClick: () => void handleDownloadSelected(), className: "cv-header-action", children: labels.downloadSelected(selectedIds.length) }) : null,
1344
- archiveError ? /* @__PURE__ */ jsx14("span", { role: "alert", className: "text-red-600 dark:text-red-400", children: labels.archiveFailed }) : null
1920
+ selectedIds.length > 0 ? /* @__PURE__ */ jsx16("button", { "data-cv-tooltip": labels.downloadSelected(selectedIds.length), "aria-label": labels.downloadSelected(selectedIds.length), type: "button", onClick: () => void handleDownloadSelected(), className: "cv-header-action", children: labels.downloadSelected(selectedIds.length) }) : null,
1921
+ archiveError ? /* @__PURE__ */ jsx16("span", { role: "alert", className: "text-red-600 dark:text-red-400", children: labels.archiveFailed }) : null
1345
1922
  ] }) : null,
1346
- /* @__PURE__ */ jsx14("ul", { className: cn("space-y-2", classNames?.list), children: documents.map((document2) => {
1923
+ /* @__PURE__ */ jsx16("ul", { className: cn("space-y-2", classNames?.list), children: documents.map((document2) => {
1347
1924
  const isFromCustomer = !TEAM_SOURCES.has(document2.source);
1348
1925
  const SourceIcon = isFromCustomer ? Users : Bot;
1349
- return /* @__PURE__ */ jsxs9(
1926
+ return /* @__PURE__ */ jsxs11(
1350
1927
  "li",
1351
1928
  {
1352
1929
  className: cn(
@@ -1354,8 +1931,8 @@ function ConversationDocumentsPanel({
1354
1931
  classNames?.item
1355
1932
  ),
1356
1933
  children: [
1357
- /* @__PURE__ */ jsxs9("div", { className: "flex min-w-0 flex-1 items-center gap-2", children: [
1358
- canArchive ? /* @__PURE__ */ jsx14(
1934
+ /* @__PURE__ */ jsxs11("div", { className: "flex min-w-0 flex-1 items-center gap-2", children: [
1935
+ canArchive ? /* @__PURE__ */ jsx16(
1359
1936
  "input",
1360
1937
  {
1361
1938
  type: "checkbox",
@@ -1365,9 +1942,9 @@ function ConversationDocumentsPanel({
1365
1942
  className: cn("shrink-0", classNames?.checkbox)
1366
1943
  }
1367
1944
  ) : null,
1368
- /* @__PURE__ */ jsx14(FileIcon, { filename: document2.filename, mimeType: document2.mimeType }),
1369
- /* @__PURE__ */ jsxs9("div", { className: "min-w-0 flex-1", children: [
1370
- /* @__PURE__ */ jsxs9(
1945
+ /* @__PURE__ */ jsx16(FileIcon, { filename: document2.filename, mimeType: document2.mimeType }),
1946
+ /* @__PURE__ */ jsxs11("div", { className: "min-w-0 flex-1", children: [
1947
+ /* @__PURE__ */ jsxs11(
1371
1948
  "div",
1372
1949
  {
1373
1950
  className: cn(
@@ -1376,47 +1953,47 @@ function ConversationDocumentsPanel({
1376
1953
  classNames?.sourceBadge
1377
1954
  ),
1378
1955
  children: [
1379
- /* @__PURE__ */ jsx14(SourceIcon, { size: 11 }),
1956
+ /* @__PURE__ */ jsx16(SourceIcon, { size: 11 }),
1380
1957
  isFromCustomer ? labels.sourceFilterCustomer : labels.sourceFilterTeam
1381
1958
  ]
1382
1959
  }
1383
1960
  ),
1384
- /* @__PURE__ */ jsx14(
1961
+ /* @__PURE__ */ jsx16(
1385
1962
  "div",
1386
1963
  {
1387
1964
  className: cn("truncate text-sm font-medium", classNames?.filename),
1388
- title: document2.filename,
1965
+ "data-cv-tooltip": document2.filename,
1389
1966
  children: document2.filename
1390
1967
  }
1391
1968
  ),
1392
- /* @__PURE__ */ jsxs9("div", { className: cn("text-xs text-gray-500 dark:text-gray-400", classNames?.meta), children: [
1969
+ /* @__PURE__ */ jsxs11("div", { className: cn("text-xs text-gray-500 dark:text-gray-400", classNames?.meta), children: [
1393
1970
  formatDateTime(document2.linkedAt),
1394
1971
  " \xB7 ",
1395
1972
  formatFileSize(document2.sizeBytes)
1396
1973
  ] })
1397
1974
  ] })
1398
1975
  ] }),
1399
- /* @__PURE__ */ jsxs9("div", { className: "flex shrink-0 gap-1", children: [
1400
- /* @__PURE__ */ jsx14(
1976
+ /* @__PURE__ */ jsxs11("div", { className: "flex shrink-0 gap-1", children: [
1977
+ /* @__PURE__ */ jsx16(
1401
1978
  "button",
1402
1979
  {
1403
1980
  type: "button",
1404
1981
  onClick: () => void handleOpen(document2.id, "inline"),
1405
- title: labels.view,
1982
+ "data-cv-tooltip": labels.view,
1406
1983
  "aria-label": `${labels.view}: ${document2.filename}`,
1407
1984
  className: cn("cv-header-icon", classNames?.viewButton),
1408
- children: /* @__PURE__ */ jsx14(Eye, { size: 14 })
1985
+ children: /* @__PURE__ */ jsx16(Eye, { size: 14 })
1409
1986
  }
1410
1987
  ),
1411
- /* @__PURE__ */ jsx14(
1988
+ /* @__PURE__ */ jsx16(
1412
1989
  "button",
1413
1990
  {
1414
1991
  type: "button",
1415
1992
  onClick: () => void handleOpen(document2.id, "attachment"),
1416
- title: labels.download,
1993
+ "data-cv-tooltip": labels.download,
1417
1994
  "aria-label": `${labels.download}: ${document2.filename}`,
1418
1995
  className: cn("cv-header-icon", classNames?.downloadButton),
1419
- children: /* @__PURE__ */ jsx14(Download, { size: 14 })
1996
+ children: /* @__PURE__ */ jsx16(Download, { size: 14 })
1420
1997
  }
1421
1998
  )
1422
1999
  ] })
@@ -1425,7 +2002,7 @@ function ConversationDocumentsPanel({
1425
2002
  document2.id
1426
2003
  );
1427
2004
  }) }),
1428
- total > perPage ? /* @__PURE__ */ jsxs9(
2005
+ total > perPage ? /* @__PURE__ */ jsxs11(
1429
2006
  "div",
1430
2007
  {
1431
2008
  className: cn(
@@ -1433,27 +2010,29 @@ function ConversationDocumentsPanel({
1433
2010
  classNames?.pagination
1434
2011
  ),
1435
2012
  children: [
1436
- /* @__PURE__ */ jsx14("span", { className: "text-gray-400", children: labels.total(total) }),
1437
- /* @__PURE__ */ jsxs9("div", { className: "flex items-center gap-2", children: [
1438
- /* @__PURE__ */ jsx14(
2013
+ /* @__PURE__ */ jsx16("span", { className: "text-gray-400", children: labels.total(total) }),
2014
+ /* @__PURE__ */ jsxs11("div", { className: "flex items-center gap-2", children: [
2015
+ /* @__PURE__ */ jsx16(
1439
2016
  "button",
1440
2017
  {
1441
2018
  type: "button",
1442
2019
  onClick: () => setPage(page - 1),
1443
2020
  disabled: page <= 1,
1444
- "aria-label": labels.sortOldest,
2021
+ "aria-label": PAGINATION_LABELS.previous,
2022
+ "data-cv-tooltip": PAGINATION_LABELS.previous,
1445
2023
  className: "cv-header-icon disabled:opacity-40",
1446
2024
  children: "\u2039"
1447
2025
  }
1448
2026
  ),
1449
- /* @__PURE__ */ jsx14("span", { className: "text-gray-500", children: labels.page(page, lastPage) }),
1450
- /* @__PURE__ */ jsx14(
2027
+ /* @__PURE__ */ jsx16("span", { className: "text-gray-500", children: labels.page(page, lastPage) }),
2028
+ /* @__PURE__ */ jsx16(
1451
2029
  "button",
1452
2030
  {
1453
2031
  type: "button",
1454
2032
  onClick: () => setPage(page + 1),
1455
2033
  disabled: page >= lastPage,
1456
- "aria-label": labels.sortMostRecent,
2034
+ "aria-label": PAGINATION_LABELS.next,
2035
+ "data-cv-tooltip": PAGINATION_LABELS.next,
1457
2036
  className: "cv-header-icon disabled:opacity-40",
1458
2037
  children: "\u203A"
1459
2038
  }
@@ -1466,9 +2045,9 @@ function ConversationDocumentsPanel({
1466
2045
  }
1467
2046
 
1468
2047
  // src/DocumentsLibrary.tsx
1469
- import { useEffect as useEffect3, useState as useState9 } from "react";
1470
- import { ArrowUpDown as ArrowUpDown2, Bot as Bot2, Download as Download2, Eye as Eye2, MessageSquare, Users as Users2 } from "lucide-react";
1471
- import { jsx as jsx15, jsxs as jsxs10 } from "react/jsx-runtime";
2048
+ import { useEffect as useEffect4, useRef as useRef5, useState as useState11 } from "react";
2049
+ import { ArrowUpDown as ArrowUpDown2, Bot as Bot2, Download as Download2, Eye as Eye2, MessageSquare, Upload, Users as Users2 } from "lucide-react";
2050
+ import { Fragment as Fragment4, jsx as jsx17, jsxs as jsxs12 } from "react/jsx-runtime";
1472
2051
  var DEFAULT_DOCUMENTS_LIBRARY_LABELS = {
1473
2052
  title: "Documentos",
1474
2053
  searchPlaceholder: "Buscar por nome do arquivo ou telefone",
@@ -1485,6 +2064,8 @@ var DEFAULT_DOCUMENTS_LIBRARY_LABELS = {
1485
2064
  sortMostRecent: "Mais recentes",
1486
2065
  sortOldest: "Mais antigos",
1487
2066
  clearFilters: "Limpar filtros",
2067
+ upload: "Enviar documento",
2068
+ uploadError: "N\xE3o foi poss\xEDvel enviar o arquivo.",
1488
2069
  total: (count) => `${count} arquivo${count === 1 ? "" : "s"}`,
1489
2070
  page: (current, last) => `${current} / ${last}`
1490
2071
  };
@@ -1499,18 +2080,23 @@ function DocumentsLibrary({
1499
2080
  }) {
1500
2081
  const labels = { ...DEFAULT_DOCUMENTS_LIBRARY_LABELS, ...labelsOverride };
1501
2082
  const context = useConversations();
1502
- const [search, setSearch] = useState9("");
1503
- const [sourceFilter, setSourceFilter] = useState9(DOCUMENT_SOURCE_FILTER.ALL);
1504
- const [sortDirection, setSortDirection] = useState9("desc");
1505
- const [page, setPage] = useState9(1);
1506
- const [documents, setDocuments] = useState9([]);
1507
- const [total, setTotal] = useState9(0);
1508
- const [loading, setLoading] = useState9(false);
1509
- const [failed, setFailed] = useState9(false);
2083
+ const [search, setSearch] = useState11("");
2084
+ const [sourceFilter, setSourceFilter] = useState11(DOCUMENT_SOURCE_FILTER.ALL);
2085
+ const [sortDirection, setSortDirection] = useState11("desc");
2086
+ const [page, setPage] = useState11(1);
2087
+ const [documents, setDocuments] = useState11([]);
2088
+ const [total, setTotal] = useState11(0);
2089
+ const [loading, setLoading] = useState11(false);
2090
+ const [failed, setFailed] = useState11(false);
2091
+ const [uploading, setUploading] = useState11(false);
2092
+ const [uploadFailed, setUploadFailed] = useState11(false);
2093
+ const [reloadToken, setReloadToken] = useState11(0);
2094
+ const fileInputRef = useRef5(null);
1510
2095
  const hasFilters = search !== "" || sourceFilter !== DOCUMENT_SOURCE_FILTER.ALL || sortDirection !== "desc";
1511
2096
  const lastPage = Math.max(1, Math.ceil(total / perPage));
1512
2097
  const fetchAll = context?.api.getAllDocuments;
1513
- useEffect3(() => {
2098
+ const uploadDocument = context?.api.uploadDocument;
2099
+ useEffect4(() => {
1514
2100
  if (!fetchAll) return;
1515
2101
  let active = true;
1516
2102
  setLoading(true);
@@ -1533,7 +2119,7 @@ function DocumentsLibrary({
1533
2119
  return () => {
1534
2120
  active = false;
1535
2121
  };
1536
- }, [fetchAll, search, sourceFilter, sortDirection, page, perPage]);
2122
+ }, [fetchAll, search, sourceFilter, sortDirection, page, perPage, reloadToken]);
1537
2123
  function applyFilter(change) {
1538
2124
  change();
1539
2125
  setPage(1);
@@ -1542,11 +2128,24 @@ function DocumentsLibrary({
1542
2128
  const url = await context?.api.getDocumentUrl(uploadId, disposition);
1543
2129
  if (url) window.open(url, "_blank", "noopener,noreferrer");
1544
2130
  }
2131
+ async function handleUpload(file) {
2132
+ if (!uploadDocument) return;
2133
+ setUploading(true);
2134
+ setUploadFailed(false);
2135
+ try {
2136
+ await uploadDocument(file);
2137
+ setReloadToken((token) => token + 1);
2138
+ } catch {
2139
+ setUploadFailed(true);
2140
+ } finally {
2141
+ setUploading(false);
2142
+ }
2143
+ }
1545
2144
  if (!fetchAll) return null;
1546
- return /* @__PURE__ */ jsxs10("div", { className: cn("space-y-3", classNames?.root, className), children: [
1547
- /* @__PURE__ */ jsx15("h2", { className: cn("text-lg font-semibold", classNames?.title), children: labels.title }),
1548
- /* @__PURE__ */ jsxs10("div", { className: cn("flex flex-wrap items-center gap-2", classNames?.filters), children: [
1549
- /* @__PURE__ */ jsx15(
2145
+ return /* @__PURE__ */ jsxs12("div", { className: cn("space-y-3", classNames?.root, className), children: [
2146
+ /* @__PURE__ */ jsx17("h2", { className: cn("text-lg font-semibold", classNames?.title), children: labels.title }),
2147
+ /* @__PURE__ */ jsxs12("div", { className: cn("flex flex-wrap items-center gap-2", classNames?.filters), children: [
2148
+ /* @__PURE__ */ jsx17(
1550
2149
  "input",
1551
2150
  {
1552
2151
  type: "search",
@@ -1557,7 +2156,7 @@ function DocumentsLibrary({
1557
2156
  className: cn("w-full rounded-md border px-3 py-2 text-sm sm:w-64", classNames?.search)
1558
2157
  }
1559
2158
  ),
1560
- /* @__PURE__ */ jsxs10(
2159
+ /* @__PURE__ */ jsxs12(
1561
2160
  "select",
1562
2161
  {
1563
2162
  value: sourceFilter,
@@ -1565,27 +2164,31 @@ function DocumentsLibrary({
1565
2164
  "aria-label": labels.sourceFilterAll,
1566
2165
  className: cn("w-full rounded-md border px-2 py-2 text-sm sm:w-40", classNames?.sourceSelect),
1567
2166
  children: [
1568
- /* @__PURE__ */ jsx15("option", { value: DOCUMENT_SOURCE_FILTER.ALL, children: labels.sourceFilterAll }),
1569
- /* @__PURE__ */ jsx15("option", { value: DOCUMENT_SOURCE_FILTER.CUSTOMER, children: labels.sourceFilterCustomer }),
1570
- /* @__PURE__ */ jsx15("option", { value: DOCUMENT_SOURCE_FILTER.TEAM, children: labels.sourceFilterTeam })
2167
+ /* @__PURE__ */ jsx17("option", { value: DOCUMENT_SOURCE_FILTER.ALL, children: labels.sourceFilterAll }),
2168
+ /* @__PURE__ */ jsx17("option", { value: DOCUMENT_SOURCE_FILTER.CUSTOMER, children: labels.sourceFilterCustomer }),
2169
+ /* @__PURE__ */ jsx17("option", { value: DOCUMENT_SOURCE_FILTER.TEAM, children: labels.sourceFilterTeam })
1571
2170
  ]
1572
2171
  }
1573
2172
  ),
1574
- /* @__PURE__ */ jsxs10(
2173
+ /* @__PURE__ */ jsxs12(
1575
2174
  "button",
1576
2175
  {
2176
+ "data-cv-tooltip": sortDirection === "desc" ? labels.sortMostRecent : labels.sortOldest,
2177
+ "aria-label": sortDirection === "desc" ? labels.sortMostRecent : labels.sortOldest,
1577
2178
  type: "button",
1578
2179
  onClick: () => applyFilter(() => setSortDirection(sortDirection === "desc" ? "asc" : "desc")),
1579
2180
  className: cn("cv-header-action inline-flex items-center gap-1", classNames?.sortButton),
1580
2181
  children: [
1581
- /* @__PURE__ */ jsx15(ArrowUpDown2, { size: 14 }),
2182
+ /* @__PURE__ */ jsx17(ArrowUpDown2, { size: 14 }),
1582
2183
  sortDirection === "desc" ? labels.sortMostRecent : labels.sortOldest
1583
2184
  ]
1584
2185
  }
1585
2186
  ),
1586
- hasFilters ? /* @__PURE__ */ jsx15(
2187
+ hasFilters ? /* @__PURE__ */ jsx17(
1587
2188
  "button",
1588
2189
  {
2190
+ "data-cv-tooltip": labels.clearFilters,
2191
+ "aria-label": labels.clearFilters,
1589
2192
  type: "button",
1590
2193
  onClick: () => applyFilter(() => {
1591
2194
  setSearch("");
@@ -1595,15 +2198,46 @@ function DocumentsLibrary({
1595
2198
  className: cn("cv-header-action", classNames?.clearButton),
1596
2199
  children: labels.clearFilters
1597
2200
  }
1598
- ) : null
2201
+ ) : null,
2202
+ uploadDocument ? /* @__PURE__ */ jsxs12(Fragment4, { children: [
2203
+ /* @__PURE__ */ jsx17(
2204
+ "input",
2205
+ {
2206
+ ref: fileInputRef,
2207
+ type: "file",
2208
+ hidden: true,
2209
+ onChange: (event) => {
2210
+ const file = event.target.files?.[0];
2211
+ event.target.value = "";
2212
+ if (file) void handleUpload(file);
2213
+ }
2214
+ }
2215
+ ),
2216
+ /* @__PURE__ */ jsxs12(
2217
+ "button",
2218
+ {
2219
+ "data-cv-tooltip": labels.upload,
2220
+ "aria-label": labels.upload,
2221
+ type: "button",
2222
+ onClick: () => fileInputRef.current?.click(),
2223
+ disabled: uploading,
2224
+ className: "cv-header-action ml-auto inline-flex items-center gap-1 disabled:opacity-40",
2225
+ children: [
2226
+ /* @__PURE__ */ jsx17(Upload, { size: 14, "aria-hidden": "true" }),
2227
+ labels.upload
2228
+ ]
2229
+ }
2230
+ )
2231
+ ] }) : null
1599
2232
  ] }),
1600
- loading ? /* @__PURE__ */ jsx15("p", { className: cn("text-sm text-gray-500", classNames?.status), children: labels.loading }) : null,
1601
- failed ? /* @__PURE__ */ jsx15("p", { role: "alert", className: cn("text-sm text-red-600 dark:text-red-400", classNames?.status), children: labels.failure }) : null,
1602
- !loading && !failed && documents.length === 0 ? /* @__PURE__ */ jsx15("p", { className: cn("text-sm text-gray-500", classNames?.status), children: hasFilters ? labels.noResults : labels.empty }) : null,
1603
- /* @__PURE__ */ jsx15("ul", { className: cn("space-y-2", classNames?.list), children: documents.map((document2) => {
2233
+ loading ? /* @__PURE__ */ jsx17("p", { className: cn("text-sm text-gray-500", classNames?.status), children: labels.loading }) : null,
2234
+ failed ? /* @__PURE__ */ jsx17("p", { role: "alert", className: cn("text-sm text-red-600 dark:text-red-400", classNames?.status), children: labels.failure }) : null,
2235
+ uploadFailed ? /* @__PURE__ */ jsx17("p", { role: "alert", className: cn("text-sm text-red-600 dark:text-red-400", classNames?.status), children: labels.uploadError }) : null,
2236
+ !loading && !failed && documents.length === 0 ? /* @__PURE__ */ jsx17("p", { className: cn("text-sm text-gray-500", classNames?.status), children: hasFilters ? labels.noResults : labels.empty }) : null,
2237
+ /* @__PURE__ */ jsx17("ul", { className: cn("space-y-2", classNames?.list), children: documents.map((document2) => {
1604
2238
  const isFromCustomer = !TEAM_SOURCES2.has(document2.source);
1605
2239
  const SourceIcon = isFromCustomer ? Users2 : Bot2;
1606
- return /* @__PURE__ */ jsxs10(
2240
+ return /* @__PURE__ */ jsxs12(
1607
2241
  "li",
1608
2242
  {
1609
2243
  className: cn(
@@ -1611,56 +2245,57 @@ function DocumentsLibrary({
1611
2245
  classNames?.item
1612
2246
  ),
1613
2247
  children: [
1614
- /* @__PURE__ */ jsxs10("div", { className: "flex min-w-0 flex-1 items-center gap-3", children: [
1615
- /* @__PURE__ */ jsx15(FileIcon, { filename: document2.filename, mimeType: document2.mimeType }),
1616
- /* @__PURE__ */ jsxs10("div", { className: "min-w-0 flex-1", children: [
1617
- /* @__PURE__ */ jsx15("div", { className: cn("truncate text-sm font-medium", classNames?.filename), title: document2.filename, children: document2.filename }),
1618
- /* @__PURE__ */ jsxs10("div", { className: cn("flex flex-wrap items-center gap-x-2 text-xs text-gray-500", classNames?.meta), children: [
1619
- /* @__PURE__ */ jsxs10("span", { className: "inline-flex items-center gap-1", children: [
1620
- /* @__PURE__ */ jsx15(SourceIcon, { size: 11 }),
2248
+ /* @__PURE__ */ jsxs12("div", { className: "flex min-w-0 flex-1 items-center gap-3", children: [
2249
+ /* @__PURE__ */ jsx17(FileIcon, { filename: document2.filename, mimeType: document2.mimeType }),
2250
+ /* @__PURE__ */ jsxs12("div", { className: "min-w-0 flex-1", children: [
2251
+ /* @__PURE__ */ jsx17("div", { className: cn("truncate text-sm font-medium", classNames?.filename), "data-cv-tooltip": document2.filename, children: document2.filename }),
2252
+ /* @__PURE__ */ jsxs12("div", { className: cn("flex flex-wrap items-center gap-x-2 text-xs text-gray-500", classNames?.meta), children: [
2253
+ /* @__PURE__ */ jsxs12("span", { className: "inline-flex items-center gap-1", children: [
2254
+ /* @__PURE__ */ jsx17(SourceIcon, { size: 11 }),
1621
2255
  isFromCustomer ? labels.sourceFilterCustomer : labels.sourceFilterTeam
1622
2256
  ] }),
1623
- /* @__PURE__ */ jsx15("span", { children: "\xB7" }),
1624
- /* @__PURE__ */ jsx15("span", { children: formatDateTime(document2.linkedAt) }),
1625
- /* @__PURE__ */ jsx15("span", { children: "\xB7" }),
1626
- /* @__PURE__ */ jsx15("span", { children: formatFileSize(document2.sizeBytes) })
2257
+ /* @__PURE__ */ jsx17("span", { children: "\xB7" }),
2258
+ /* @__PURE__ */ jsx17("span", { children: formatDateTime(document2.linkedAt) }),
2259
+ /* @__PURE__ */ jsx17("span", { children: "\xB7" }),
2260
+ /* @__PURE__ */ jsx17("span", { children: formatFileSize(document2.sizeBytes) })
1627
2261
  ] })
1628
2262
  ] })
1629
2263
  ] }),
1630
- onOpenConversation ? /* @__PURE__ */ jsxs10(
2264
+ onOpenConversation ? /* @__PURE__ */ jsxs12(
1631
2265
  "button",
1632
2266
  {
1633
2267
  type: "button",
1634
2268
  onClick: () => onOpenConversation(document2.conversationId),
1635
- title: labels.openConversation,
2269
+ "data-cv-tooltip": labels.openConversation,
2270
+ "aria-label": labels.openConversation,
1636
2271
  className: cn("cv-header-action inline-flex shrink-0 items-center gap-1", classNames?.conversationLink),
1637
2272
  children: [
1638
- /* @__PURE__ */ jsx15(MessageSquare, { size: 12 }),
2273
+ /* @__PURE__ */ jsx17(MessageSquare, { size: 12 }),
1639
2274
  formatPhone(document2.conversationId)
1640
2275
  ]
1641
2276
  }
1642
- ) : /* @__PURE__ */ jsx15("span", { className: cn("shrink-0 text-xs text-gray-500", classNames?.conversationLink), children: formatPhone(document2.conversationId) }),
1643
- /* @__PURE__ */ jsxs10("div", { className: "flex shrink-0 gap-1", children: [
1644
- /* @__PURE__ */ jsx15(
2277
+ ) : /* @__PURE__ */ jsx17("span", { className: cn("shrink-0 text-xs text-gray-500", classNames?.conversationLink), children: formatPhone(document2.conversationId) }),
2278
+ /* @__PURE__ */ jsxs12("div", { className: "flex shrink-0 gap-1", children: [
2279
+ /* @__PURE__ */ jsx17(
1645
2280
  "button",
1646
2281
  {
1647
2282
  type: "button",
1648
2283
  onClick: () => void handleOpen(document2.id, "inline"),
1649
- title: labels.view,
2284
+ "data-cv-tooltip": labels.view,
1650
2285
  "aria-label": `${labels.view}: ${document2.filename}`,
1651
2286
  className: "cv-header-icon",
1652
- children: /* @__PURE__ */ jsx15(Eye2, { size: 14 })
2287
+ children: /* @__PURE__ */ jsx17(Eye2, { size: 14 })
1653
2288
  }
1654
2289
  ),
1655
- /* @__PURE__ */ jsx15(
2290
+ /* @__PURE__ */ jsx17(
1656
2291
  "button",
1657
2292
  {
1658
2293
  type: "button",
1659
2294
  onClick: () => void handleOpen(document2.id, "attachment"),
1660
- title: labels.download,
2295
+ "data-cv-tooltip": labels.download,
1661
2296
  "aria-label": `${labels.download}: ${document2.filename}`,
1662
2297
  className: "cv-header-icon",
1663
- children: /* @__PURE__ */ jsx15(Download2, { size: 14 })
2298
+ children: /* @__PURE__ */ jsx17(Download2, { size: 14 })
1664
2299
  }
1665
2300
  )
1666
2301
  ] })
@@ -1669,12 +2304,14 @@ function DocumentsLibrary({
1669
2304
  `${document2.conversationId}:${document2.id}`
1670
2305
  );
1671
2306
  }) }),
1672
- total > perPage ? /* @__PURE__ */ jsxs10("div", { className: cn("flex items-center justify-between border-t pt-2 text-xs dark:border-gray-700", classNames?.pagination), children: [
1673
- /* @__PURE__ */ jsx15("span", { className: "text-gray-400", children: labels.total(total) }),
1674
- /* @__PURE__ */ jsxs10("div", { className: "flex items-center gap-2", children: [
1675
- /* @__PURE__ */ jsx15(
2307
+ total > perPage ? /* @__PURE__ */ jsxs12("div", { className: cn("flex items-center justify-between border-t pt-2 text-xs dark:border-gray-700", classNames?.pagination), children: [
2308
+ /* @__PURE__ */ jsx17("span", { className: "text-gray-400", children: labels.total(total) }),
2309
+ /* @__PURE__ */ jsxs12("div", { className: "flex items-center gap-2", children: [
2310
+ /* @__PURE__ */ jsx17(
1676
2311
  "button",
1677
2312
  {
2313
+ "data-cv-tooltip": PAGINATION_LABELS.previous,
2314
+ "aria-label": PAGINATION_LABELS.previous,
1678
2315
  type: "button",
1679
2316
  onClick: () => setPage(page - 1),
1680
2317
  disabled: page <= 1,
@@ -1682,10 +2319,12 @@ function DocumentsLibrary({
1682
2319
  children: "\u2039"
1683
2320
  }
1684
2321
  ),
1685
- /* @__PURE__ */ jsx15("span", { className: "text-gray-500", children: labels.page(page, lastPage) }),
1686
- /* @__PURE__ */ jsx15(
2322
+ /* @__PURE__ */ jsx17("span", { className: "text-gray-500", children: labels.page(page, lastPage) }),
2323
+ /* @__PURE__ */ jsx17(
1687
2324
  "button",
1688
2325
  {
2326
+ "data-cv-tooltip": PAGINATION_LABELS.next,
2327
+ "aria-label": PAGINATION_LABELS.next,
1689
2328
  type: "button",
1690
2329
  onClick: () => setPage(page + 1),
1691
2330
  disabled: page >= lastPage,
@@ -1698,17 +2337,312 @@ function DocumentsLibrary({
1698
2337
  ] });
1699
2338
  }
1700
2339
 
2340
+ // src/preview/ConversationSimulatorClient.ts
2341
+ var SIMULATOR_FILE_MEDIA_KINDS = ["image", "video", "document"];
2342
+ function mediaKindOf(mimeType) {
2343
+ if (mimeType.startsWith("image/")) return "image";
2344
+ if (mimeType.startsWith("video/")) return "video";
2345
+ if (mimeType.startsWith("audio/")) return "audio";
2346
+ return "document";
2347
+ }
2348
+ function acceptsMediaKind(client, kind) {
2349
+ if (!client.sendMedia) return false;
2350
+ return client.acceptedMediaKinds?.includes(kind) ?? true;
2351
+ }
2352
+ function isConversationSimulatorClient(candidate) {
2353
+ return typeof candidate.sendReply === "function";
2354
+ }
2355
+ function toConversationSimulatorClient({
2356
+ client,
2357
+ uploadMedia
2358
+ }) {
2359
+ const upload = uploadMedia ?? client.uploadMedia;
2360
+ const base = {
2361
+ sendText: (text) => client.sendText(text),
2362
+ sendReply: (selection) => {
2363
+ const reply = { id: selection.option.id, title: selection.option.title };
2364
+ return selection.kind === "button" ? client.sendButtonReply(reply) : client.sendListReply(reply);
2365
+ }
2366
+ };
2367
+ if (!upload) return base;
2368
+ return {
2369
+ ...base,
2370
+ sendMedia: async ({ mediaKind, file, mimeType, filename, caption }) => {
2371
+ const uploaded = await upload(file);
2372
+ await client.sendMedia({
2373
+ // O tipo sai do MIME que o upload devolveu quando ele existe: host que normaliza o formato
2374
+ // (áudio gravado em `webm` que sobe como `ogg`) mudava de tipo, e a mídia chegava como
2375
+ // documento.
2376
+ mediaType: uploaded.mimeType ? mediaKindOf(uploaded.mimeType) : mediaKind,
2377
+ mediaId: uploaded.mediaId,
2378
+ mimeType: uploaded.mimeType ?? mimeType ?? file.type,
2379
+ filename: uploaded.filename ?? filename ?? file.name,
2380
+ ...caption ? { caption } : {}
2381
+ });
2382
+ }
2383
+ };
2384
+ }
2385
+
2386
+ // src/preview/ConversationPreview.tsx
2387
+ import { useCallback as useCallback6, useEffect as useEffect5, useMemo as useMemo4, useRef as useRef6, useState as useState12 } from "react";
2388
+ import { jsx as jsx18, jsxs as jsxs13 } from "react/jsx-runtime";
2389
+ function mediaTypeOf(mimeType) {
2390
+ return mediaKindOf(mimeType);
2391
+ }
2392
+ var GROUPING_WINDOW_MS = 5 * 60 * 1e3;
2393
+ var FOLLOW_UP_REFRESH_MS = [400, 1200, 3e3];
2394
+ function decorate(messages) {
2395
+ return messages.map((message, index) => {
2396
+ const previous = index > 0 ? messages[index - 1] : void 0;
2397
+ const currentTime = new Date(message.timestamp).getTime();
2398
+ const previousTime = previous ? new Date(previous.timestamp).getTime() : 0;
2399
+ return {
2400
+ message,
2401
+ isFirstInGroup: !previous || previous.sender !== message.sender || currentTime - previousTime > GROUPING_WINDOW_MS,
2402
+ showDateDivider: !previous || new Date(message.timestamp).toDateString() !== new Date(previous.timestamp).toDateString()
2403
+ };
2404
+ });
2405
+ }
2406
+ function statusOf(error) {
2407
+ if (typeof error !== "object" || error === null) return void 0;
2408
+ const candidate = error;
2409
+ const value = candidate.status ?? candidate.statusCode;
2410
+ return typeof value === "number" ? value : void 0;
2411
+ }
2412
+ function isNotFound(error) {
2413
+ return statusOf(error) === 404;
2414
+ }
2415
+ function describeLoadFailure(error) {
2416
+ const status = statusOf(error);
2417
+ if (status === 401 || status === 403) {
2418
+ return "Sem sess\xE3o de administrador nesta aba: a mensagem \xE9 entregue no webhook, mas o transcript n\xE3o pode ser lido. Entre no painel nesta mesma aba e reabra o simulador.";
2419
+ }
2420
+ if (error instanceof Error && error.message) return `N\xE3o foi poss\xEDvel ler o transcript: ${error.message}`;
2421
+ return "N\xE3o foi poss\xEDvel ler o transcript da conversa.";
2422
+ }
2423
+ function ConversationPreview({
2424
+ client,
2425
+ sse,
2426
+ conversationId,
2427
+ loadMessages,
2428
+ placeholder,
2429
+ pollIntervalMs,
2430
+ uploadMedia
2431
+ }) {
2432
+ const [messages, setMessages] = useState12([]);
2433
+ const [failure, setFailure] = useState12(void 0);
2434
+ const [loadFailure, setLoadFailure] = useState12(void 0);
2435
+ const [isRecording, setIsRecording] = useState12(false);
2436
+ const [pendingLocal, setPendingLocal] = useState12([]);
2437
+ const loadMessagesRef = useRef6(loadMessages);
2438
+ const bottomRef = useRef6(null);
2439
+ loadMessagesRef.current = loadMessages;
2440
+ const simulator = useMemo4(
2441
+ () => isConversationSimulatorClient(client) ? client : toConversationSimulatorClient({ client, ...uploadMedia ? { uploadMedia } : {} }),
2442
+ [client, uploadMedia]
2443
+ );
2444
+ const refresh = useCallback6(async () => {
2445
+ try {
2446
+ const loaded = await loadMessagesRef.current(conversationId);
2447
+ setMessages(loaded);
2448
+ setLoadFailure(void 0);
2449
+ if (loaded.length > 0) setPendingLocal([]);
2450
+ } catch (error) {
2451
+ if (isNotFound(error)) {
2452
+ setMessages([]);
2453
+ setLoadFailure(void 0);
2454
+ return;
2455
+ }
2456
+ setLoadFailure(describeLoadFailure(error));
2457
+ }
2458
+ }, [conversationId]);
2459
+ useEffect5(() => {
2460
+ void refresh();
2461
+ }, [refresh]);
2462
+ useEffect5(() => {
2463
+ const source = sse.connectConversationStream(conversationId);
2464
+ const handler = () => {
2465
+ void refresh();
2466
+ };
2467
+ source.addEventListener("message", handler);
2468
+ return () => {
2469
+ source.removeEventListener("message", handler);
2470
+ source.close();
2471
+ };
2472
+ }, [sse, conversationId, refresh]);
2473
+ useEffect5(() => {
2474
+ if (!pollIntervalMs) return;
2475
+ const timer = setInterval(() => void refresh(), pollIntervalMs);
2476
+ return () => clearInterval(timer);
2477
+ }, [pollIntervalMs, refresh]);
2478
+ useEffect5(() => {
2479
+ bottomRef.current?.scrollIntoView({ behavior: "smooth", block: "nearest" });
2480
+ }, [messages]);
2481
+ const rendered = useMemo4(() => decorate([...messages, ...pendingLocal]), [messages, pendingLocal]);
2482
+ async function refreshWithFollowUps() {
2483
+ await refresh();
2484
+ for (const atraso of FOLLOW_UP_REFRESH_MS) {
2485
+ setTimeout(() => void refresh(), atraso);
2486
+ }
2487
+ }
2488
+ async function handleSend(text) {
2489
+ setFailure(void 0);
2490
+ try {
2491
+ await simulator.sendText(text);
2492
+ setPendingLocal((current) => [
2493
+ ...current,
2494
+ {
2495
+ id: `local-${current.length}-${text.length}`,
2496
+ type: "text",
2497
+ content: text,
2498
+ // Do ponto de vista do servidor, mensagem do cliente é inbound — é assim que ela aparece
2499
+ // como "minha" nesta visão.
2500
+ direction: "inbound",
2501
+ sender: "customer",
2502
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2503
+ status: "sent"
2504
+ }
2505
+ ]);
2506
+ await refreshWithFollowUps();
2507
+ } catch (error) {
2508
+ setFailure(error instanceof Error ? error.message : "Falha ao entregar a mensagem no webhook.");
2509
+ }
2510
+ }
2511
+ async function handleInteractiveSelect(selection) {
2512
+ setFailure(void 0);
2513
+ try {
2514
+ await simulator.sendReply(selection);
2515
+ await refreshWithFollowUps();
2516
+ } catch (error) {
2517
+ setFailure(error instanceof Error ? error.message : "Falha ao entregar a resposta no webhook.");
2518
+ }
2519
+ }
2520
+ const sendMedia = simulator.sendMedia;
2521
+ const canAttachFile = SIMULATOR_FILE_MEDIA_KINDS.some((kind) => acceptsMediaKind(simulator, kind));
2522
+ const canRecordAudio = acceptsMediaKind(simulator, "audio");
2523
+ async function handleAttach(file) {
2524
+ if (!sendMedia) return;
2525
+ setFailure(void 0);
2526
+ try {
2527
+ await sendMedia({
2528
+ mediaKind: mediaKindOf(file.type),
2529
+ file,
2530
+ mimeType: file.type,
2531
+ filename: file.name
2532
+ });
2533
+ await refreshWithFollowUps();
2534
+ } catch (error) {
2535
+ setFailure(error instanceof Error ? error.message : "Falha ao enviar o arquivo.");
2536
+ }
2537
+ }
2538
+ return /* @__PURE__ */ jsxs13("div", { className: "flex h-full min-h-0 flex-col", children: [
2539
+ /* @__PURE__ */ jsxs13(ConversationWallpaper, { className: "flex-1 min-h-0 overflow-y-auto px-4 py-3", children: [
2540
+ rendered.map(({ message, isFirstInGroup, showDateDivider }) => /* @__PURE__ */ jsxs13("div", { children: [
2541
+ showDateDivider ? /* @__PURE__ */ jsx18(DateDivider, { iso: message.timestamp }) : null,
2542
+ /* @__PURE__ */ jsx18(
2543
+ MessageBubble,
2544
+ {
2545
+ message,
2546
+ isMine: message.direction === "inbound",
2547
+ isFirstInGroup,
2548
+ onInteractiveSelect: message.direction === "outbound" ? (selection) => void handleInteractiveSelect(selection) : void 0
2549
+ }
2550
+ )
2551
+ ] }, message.id)),
2552
+ /* @__PURE__ */ jsx18("div", { ref: bottomRef })
2553
+ ] }),
2554
+ failure ? /* @__PURE__ */ jsx18("p", { role: "alert", className: "px-4 py-2 text-sm text-red-600 dark:text-red-400", children: failure }) : null,
2555
+ loadFailure ? /* @__PURE__ */ jsx18("p", { role: "status", className: "px-4 py-2 text-sm text-amber-700 dark:text-amber-400", children: loadFailure }) : null,
2556
+ /* @__PURE__ */ jsx18(
2557
+ MessageComposer,
2558
+ {
2559
+ onSend: (text) => void handleSend(text),
2560
+ onAttach: canAttachFile ? (file) => void handleAttach(file) : void 0,
2561
+ placeholder: isRecording ? "Gravando\u2026 toque no quadrado para ouvir" : placeholder ?? "Escreva como o cliente\u2026",
2562
+ idleAction: canRecordAudio ? /* @__PURE__ */ jsx18(
2563
+ AudioRecorderButton,
2564
+ {
2565
+ onRecorded: (file) => void handleAttach(file),
2566
+ onFailure: (message) => setFailure(message),
2567
+ onRecordingChange: setIsRecording
2568
+ }
2569
+ ) : void 0
2570
+ }
2571
+ )
2572
+ ] });
2573
+ }
2574
+
2575
+ // src/preview/ConversationSimulatorPanel.tsx
2576
+ import { jsx as jsx19, jsxs as jsxs14 } from "react/jsx-runtime";
2577
+ var DEFAULT_CONVERSATION_SIMULATOR_PANEL_LABELS = {
2578
+ title: "Simulador do cliente",
2579
+ destinationHint: "entrega no webhook real",
2580
+ close: "Fechar simulador",
2581
+ placeholder: "Escreva como o cliente\u2026"
2582
+ };
2583
+ var SIMULATOR_PANEL_CHANNEL_WORDING = {
2584
+ [CONVERSATION_CHANNEL.WHATSAPP]: {
2585
+ destinationHint: DEFAULT_CONVERSATION_SIMULATOR_PANEL_LABELS.destinationHint,
2586
+ placeholder: DEFAULT_CONVERSATION_SIMULATOR_PANEL_LABELS.placeholder
2587
+ },
2588
+ [CONVERSATION_CHANNEL.MESSENGER]: {
2589
+ destinationHint: DEFAULT_CONVERSATION_SIMULATOR_PANEL_LABELS.destinationHint,
2590
+ placeholder: DEFAULT_CONVERSATION_SIMULATOR_PANEL_LABELS.placeholder
2591
+ },
2592
+ [CONVERSATION_CHANNEL.INSTAGRAM]: {
2593
+ destinationHint: DEFAULT_CONVERSATION_SIMULATOR_PANEL_LABELS.destinationHint,
2594
+ placeholder: DEFAULT_CONVERSATION_SIMULATOR_PANEL_LABELS.placeholder
2595
+ },
2596
+ [CONVERSATION_CHANNEL.WEBCHAT]: {
2597
+ destinationHint: "entrega na API do chat do site",
2598
+ placeholder: "Escreva como o visitante\u2026"
2599
+ }
2600
+ };
2601
+ function simulatorPanelLabelsOf(channel) {
2602
+ return {
2603
+ ...DEFAULT_CONVERSATION_SIMULATOR_PANEL_LABELS,
2604
+ ...SIMULATOR_PANEL_CHANNEL_WORDING[channel ?? DEFAULT_CONVERSATION_CHANNEL]
2605
+ };
2606
+ }
2607
+ function ConversationSimulatorPanel({
2608
+ onClose,
2609
+ channel,
2610
+ displayHandle,
2611
+ displayNumber,
2612
+ labels,
2613
+ headerActions,
2614
+ ...previewProps
2615
+ }) {
2616
+ const text = { ...simulatorPanelLabelsOf(channel), ...labels };
2617
+ const subtitle = [displayHandle ?? displayNumber ?? previewProps.conversationId, text.destinationHint].join(" \xB7 ");
2618
+ return /* @__PURE__ */ jsxs14("aside", { className: "cv-simulator-panel", "aria-label": text.title, children: [
2619
+ /* @__PURE__ */ jsxs14("header", { className: "cv-simulator-panel__header", children: [
2620
+ /* @__PURE__ */ jsxs14("div", { className: "cv-simulator-panel__heading", children: [
2621
+ /* @__PURE__ */ jsx19("h2", { className: "cv-simulator-panel__title", children: text.title }),
2622
+ /* @__PURE__ */ jsx19("p", { className: "cv-simulator-panel__subtitle", children: subtitle })
2623
+ ] }),
2624
+ /* @__PURE__ */ jsxs14("div", { className: "cv-simulator-panel__actions", children: [
2625
+ headerActions,
2626
+ /* @__PURE__ */ jsx19("button", { type: "button", onClick: onClose, "data-cv-tooltip": text.close, "aria-label": text.close, className: "cv-simulator-panel__close", children: /* @__PURE__ */ jsx19("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", "aria-hidden": "true", children: /* @__PURE__ */ jsx19("path", { d: "M18 6 6 18M6 6l12 12" }) }) })
2627
+ ] })
2628
+ ] }),
2629
+ /* @__PURE__ */ jsx19("div", { className: "cv-simulator-panel__body", children: /* @__PURE__ */ jsx19(ConversationPreview, { ...previewProps, placeholder: text.placeholder }) })
2630
+ ] });
2631
+ }
2632
+
1701
2633
  export {
1702
2634
  ConversationLocalesProvider,
1703
2635
  useConversationLocales,
1704
2636
  StatusTicks,
1705
2637
  AudioPlayer,
1706
2638
  cn,
2639
+ AudioTranscription,
1707
2640
  FileIcon,
1708
2641
  formatTimestamp,
1709
2642
  formatDateTime,
1710
2643
  isSameDay,
1711
2644
  formatFileSize,
2645
+ documentTypeLabel,
1712
2646
  MediaRenderer,
1713
2647
  DEFAULT_LIGHTBOX_LABELS,
1714
2648
  Lightbox,
@@ -1723,20 +2657,52 @@ export {
1723
2657
  searchEmojis,
1724
2658
  DEFAULT_EMOJI_PICKER_LABELS,
1725
2659
  EmojiPicker,
2660
+ DEFAULT_AUDIO_RECORDER_BUTTON_LABELS,
2661
+ DEFAULT_MAX_RECORDING_MILLISECONDS,
2662
+ AudioRecorderButton,
2663
+ COMPOSER_BAR_CLASS,
2664
+ COMPOSER_TOOL_BUTTON_CLASS,
2665
+ COMPOSER_TOOL_BUTTON_ACTIVE_CLASS,
2666
+ COMPOSER_TOOL_BUTTON_IDLE_CLASS,
2667
+ COMPOSER_MONOSPACE_CLASS,
2668
+ COMPOSER_COMPACT_WIDTH,
2669
+ QUICK_REPLY_PILL_CLASS,
2670
+ applyQuickReplyVariables,
2671
+ resolveQuickReply,
1726
2672
  DEFAULT_MESSAGE_COMPOSER_LABELS,
1727
2673
  DEFAULT_ACCEPTED_FILE_TYPES,
1728
2674
  MessageComposer,
1729
2675
  DateDivider,
1730
2676
  formatPhone,
1731
- phoneCountryFlag,
1732
2677
  phoneInitials,
2678
+ CONVERSATION_CHANNEL,
2679
+ DEFAULT_CONVERSATION_CHANNEL,
2680
+ REOPEN_MECHANISM,
2681
+ HANDLE_KIND,
2682
+ CHANNEL_CAPABILITIES,
2683
+ capabilitiesOf,
2684
+ CHANNEL_FILTER_ALL,
2685
+ channelFiltersFor,
2686
+ formatContactHandle,
2687
+ contactFlag,
1733
2688
  useAsyncResource,
1734
2689
  conversationsOf,
1735
2690
  totalOf,
1736
2691
  useConversationDocuments,
2692
+ PAGINATION_LABELS,
1737
2693
  DOCUMENT_SOURCE_FILTER,
1738
2694
  DEFAULT_CONVERSATION_DOCUMENTS_LABELS,
1739
2695
  ConversationDocumentsPanel,
1740
2696
  DEFAULT_DOCUMENTS_LIBRARY_LABELS,
1741
- DocumentsLibrary
2697
+ DocumentsLibrary,
2698
+ SIMULATOR_FILE_MEDIA_KINDS,
2699
+ mediaKindOf,
2700
+ acceptsMediaKind,
2701
+ isConversationSimulatorClient,
2702
+ toConversationSimulatorClient,
2703
+ mediaTypeOf,
2704
+ ConversationPreview,
2705
+ DEFAULT_CONVERSATION_SIMULATOR_PANEL_LABELS,
2706
+ simulatorPanelLabelsOf,
2707
+ ConversationSimulatorPanel
1742
2708
  };