@adatechnology/conversations-ui 0.1.1 → 0.2.1

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 (49) hide show
  1. package/dist/{ConversationSimulatorPanel--5fIzXWY.d.ts → ConversationSimulatorPanel-ikgmlciE.d.ts} +103 -1
  2. package/dist/{chunk-BJNRLLDO.js → chunk-NHQDQJL2.js} +769 -244
  3. package/dist/index.d.ts +304 -9
  4. package/dist/index.js +2181 -507
  5. package/dist/preview/index.d.ts +2 -2
  6. package/dist/preview/index.js +147 -1
  7. package/dist/styles.css +135 -0
  8. package/package.json +1 -1
  9. package/src/MessageComposer.test.tsx +14 -0
  10. package/src/MessageComposer.tsx +327 -73
  11. package/src/RichMessageComposer.test.tsx +22 -4
  12. package/src/RichMessageComposer.tsx +674 -370
  13. package/src/index.ts +28 -1
  14. package/src/preview/createMockConversationsApi.ts +184 -0
  15. package/src/providers/types.ts +34 -0
  16. package/src/quickReplies/AttachmentsFormSection.tsx +160 -0
  17. package/src/quickReplies/QuickRepliesPicker.test.tsx +114 -0
  18. package/src/quickReplies/QuickRepliesPicker.tsx +188 -0
  19. package/src/quickReplies/QuickRepliesWorkspace.test.tsx +32 -0
  20. package/src/quickReplies/QuickRepliesWorkspace.tsx +395 -0
  21. package/src/quickReplies/createUploadQueue.test.ts +106 -0
  22. package/src/quickReplies/createUploadQueue.ts +69 -0
  23. package/src/quickReplies/index.ts +5 -0
  24. package/src/quickReplies/labels.ts +124 -0
  25. package/src/quickReplies/quickReply.types.ts +74 -0
  26. package/src/quickReplies/quickReplyAttachmentUpload.test.ts +76 -0
  27. package/src/quickReplies/quickReplyAttachmentUpload.ts +81 -0
  28. package/src/quickReplies/quickReplyAttachments.test.ts +467 -0
  29. package/src/quickReplies/quickReplyAttachments.ts +328 -0
  30. package/src/quickReplies/quickReplySearch.test.ts +88 -0
  31. package/src/quickReplies/quickReplySearch.ts +104 -0
  32. package/src/quickReplies/quickReplyShortcut.test.ts +38 -0
  33. package/src/quickReplies/quickReplyShortcut.ts +46 -0
  34. package/src/quickReplies/resolveConversationVariables.test.ts +63 -0
  35. package/src/quickReplies/resolveConversationVariables.ts +41 -0
  36. package/src/quickReplies/useQuickRepliesPicker.test.ts +20 -0
  37. package/src/quickReplies/useQuickRepliesPicker.ts +121 -0
  38. package/src/quickReplies/useQuickRepliesWorkspace.test.ts +222 -0
  39. package/src/quickReplies/useQuickRepliesWorkspace.ts +426 -0
  40. package/src/quickReplies/useQuickReplyAttachmentUploads.ts +159 -0
  41. package/src/styles.css +87 -0
  42. package/src/workspace/ConversationPane.tsx +137 -73
  43. package/src/workspace/ConversationsWorkspace.tsx +16 -1
  44. package/src/workspace/QueuedAttachmentsList.test.ts +27 -0
  45. package/src/workspace/QueuedAttachmentsList.tsx +198 -0
  46. package/src/workspace/index.ts +1 -0
  47. package/src/workspace/labels.ts +14 -0
  48. package/src/workspace/useComposerAttachmentRetry.ts +169 -0
  49. package/src/workspace/useComposerQueue.ts +229 -0
package/dist/index.js CHANGED
@@ -30,6 +30,8 @@ import {
30
30
  DEFAULT_LIGHTBOX_LABELS,
31
31
  DEFAULT_MAX_RECORDING_MILLISECONDS,
32
32
  DEFAULT_MESSAGE_COMPOSER_LABELS,
33
+ DEFAULT_QUICK_REPLIES_PICKER_LABELS,
34
+ DEFAULT_QUICK_REPLIES_WORKSPACE_LABELS,
33
35
  DOCUMENT_SOURCE_FILTER,
34
36
  DateDivider,
35
37
  DocumentsLibrary,
@@ -44,6 +46,7 @@ import {
44
46
  MessageComposer,
45
47
  PAGINATION_LABELS,
46
48
  QUICK_REPLY_PILL_CLASS,
49
+ QuickRepliesPicker,
47
50
  REOPEN_MECHANISM,
48
51
  StatusTicks,
49
52
  applyQuickReplyVariables,
@@ -53,13 +56,17 @@ import {
53
56
  contactFlag,
54
57
  conversationsOf,
55
58
  createMediaUrlResolver,
59
+ detectQuickReplyShortcut,
56
60
  documentTypeLabel,
61
+ filterQuickReplies,
57
62
  formatContactHandle,
58
63
  formatDateTime,
59
64
  formatFileSize,
60
65
  formatPhone,
61
66
  formatTimestamp,
67
+ highlightMatch,
62
68
  isSameDay,
69
+ normalizeForSearch,
63
70
  phoneInitials,
64
71
  resolveQuickReply,
65
72
  searchEmojis,
@@ -67,8 +74,9 @@ import {
67
74
  useAsyncResource,
68
75
  useConversationDocuments,
69
76
  useConversationLocales,
70
- useConversations
71
- } from "./chunk-BJNRLLDO.js";
77
+ useConversations,
78
+ useQuickRepliesPicker
79
+ } from "./chunk-NHQDQJL2.js";
72
80
  import {
73
81
  ZERO_WIDTH_SPACE,
74
82
  htmlToWA,
@@ -80,8 +88,16 @@ import {
80
88
  } from "./chunk-WCBDXZ3X.js";
81
89
 
82
90
  // src/RichMessageComposer.tsx
83
- import { forwardRef, useCallback, useEffect as useEffect3, useImperativeHandle, useRef as useRef2, useState as useState3 } from "react";
84
- import { Bold, Italic, Strikethrough, Code, Paperclip, SendHorizonal, Braces, Type } from "lucide-react";
91
+ import {
92
+ forwardRef,
93
+ useCallback,
94
+ useEffect as useEffect3,
95
+ useId,
96
+ useImperativeHandle,
97
+ useRef as useRef2,
98
+ useState as useState3
99
+ } from "react";
100
+ import { Bold, Italic, Strikethrough, Code, Paperclip, SendHorizonal, Braces, Type, Zap } from "lucide-react";
85
101
 
86
102
  // src/hooks/useContainerWidth.ts
87
103
  import { useEffect, useState } from "react";
@@ -328,7 +344,8 @@ var RICH_COMPOSER_ACTION = {
328
344
  MONOSPACE: "monospace",
329
345
  EMOJI: "emoji",
330
346
  ATTACH: "attach",
331
- VARIABLES: "variables"
347
+ VARIABLES: "variables",
348
+ QUICK_REPLIES: "quickReplies"
332
349
  };
333
350
  var DEFAULT_RICH_COMPOSER_TOOLTIPS = {
334
351
  bold: "Negrito",
@@ -339,7 +356,8 @@ var DEFAULT_RICH_COMPOSER_TOOLTIPS = {
339
356
  attach: "Anexar arquivo",
340
357
  send: "Enviar",
341
358
  variables: "Vari\xE1veis dispon\xEDveis",
342
- formatting: "Formata\xE7\xE3o"
359
+ formatting: "Formata\xE7\xE3o",
360
+ quickReplies: "Mensagens prontas"
343
361
  };
344
362
  var FORMATTING_ELEMENT = {
345
363
  bold: "strong",
@@ -347,316 +365,515 @@ var FORMATTING_ELEMENT = {
347
365
  strikethrough: "del",
348
366
  monospace: "code"
349
367
  };
350
- var RichMessageComposer = forwardRef(function RichMessageComposer2({
351
- value,
352
- onChange,
353
- onSend,
354
- onAttachFiles,
355
- quickReplies,
356
- variables,
357
- toolbar,
358
- tooltips,
359
- placeholder,
360
- disabled = false,
361
- isSending = false,
362
- idleAction,
363
- attachmentsPreview,
364
- hasQueuedAttachments = false,
365
- acceptedFileTypes = DEFAULT_ACCEPTED_FILE_TYPES,
366
- className
367
- }, ref) {
368
- const editorRef = useRef2(null);
369
- const fileInputRef = useRef2(null);
370
- const variablesRef = useRef2(null);
371
- const formattingRef = useRef2(null);
372
- const barRef = useRef2(null);
373
- const [isVariablesOpen, setIsVariablesOpen] = useState3(false);
374
- const [isFormattingOpen, setIsFormattingOpen] = useState3(false);
375
- const [activeFormatting, setActiveFormatting] = useState3("");
376
- const barWidth = useContainerWidth(barRef);
377
- const isCompact = barWidth !== void 0 && barWidth < COMPOSER_COMPACT_WIDTH;
378
- const lastEmittedRef = useRef2("");
379
- const emitChange = useCallback((html) => {
380
- const text = htmlToWA(html);
381
- lastEmittedRef.current = text;
382
- onChange(text);
383
- }, [onChange]);
384
- useEffect3(() => {
385
- const editor = editorRef.current;
386
- if (!editor || value === lastEmittedRef.current) return;
387
- lastEmittedRef.current = value;
388
- editor.innerHTML = waToHTML(value);
389
- }, [value]);
390
- const refreshActiveFormatting = useCallback(() => {
391
- const editor = editorRef.current;
392
- if (editor) setActiveFormatting(activeFormattingIn(editor));
393
- }, []);
394
- useEffect3(() => {
395
- document.addEventListener("selectionchange", refreshActiveFormatting);
396
- return () => document.removeEventListener("selectionchange", refreshActiveFormatting);
397
- }, [refreshActiveFormatting]);
398
- useEffect3(() => {
399
- if (!isVariablesOpen) return;
400
- const closeOnOutsideClick = (event) => {
401
- if (!variablesRef.current?.contains(event.target)) setIsVariablesOpen(false);
402
- };
403
- document.addEventListener("mousedown", closeOnOutsideClick);
404
- return () => document.removeEventListener("mousedown", closeOnOutsideClick);
405
- }, [isVariablesOpen]);
406
- useEffect3(() => {
407
- if (!isFormattingOpen) return;
408
- const closeOnOutsideClick = (event) => {
409
- if (!formattingRef.current?.contains(event.target)) setIsFormattingOpen(false);
410
- };
411
- document.addEventListener("mousedown", closeOnOutsideClick);
412
- return () => document.removeEventListener("mousedown", closeOnOutsideClick);
413
- }, [isFormattingOpen]);
414
- useEffect3(() => {
415
- if (!isCompact) setIsFormattingOpen(false);
416
- }, [isCompact]);
417
- const tooltipOf = (action) => tooltips?.[action] ?? DEFAULT_RICH_COMPOSER_TOOLTIPS[action];
418
- const shows = (action) => toolbar?.[action] !== false;
419
- const replaceContent = useCallback((text) => {
420
- const editor = editorRef.current;
421
- if (!editor) return;
422
- editor.innerHTML = waToHTML(text);
423
- emitChange(editor.innerHTML);
424
- editor.focus();
425
- }, [emitChange]);
426
- useImperativeHandle(ref, () => ({
427
- setContent: replaceContent,
428
- clear: () => {
368
+ var RichMessageComposer = forwardRef(
369
+ function RichMessageComposer2({
370
+ value,
371
+ onChange,
372
+ onSend,
373
+ onAttachFiles,
374
+ quickReplies,
375
+ variables,
376
+ savedQuickReplies,
377
+ toolbar,
378
+ tooltips,
379
+ placeholder,
380
+ disabled = false,
381
+ isSending = false,
382
+ idleAction,
383
+ attachmentsPreview,
384
+ hasQueuedAttachments = false,
385
+ acceptedFileTypes = DEFAULT_ACCEPTED_FILE_TYPES,
386
+ className
387
+ }, ref) {
388
+ const editorRef = useRef2(null);
389
+ const fileInputRef = useRef2(null);
390
+ const variablesRef = useRef2(null);
391
+ const formattingRef = useRef2(null);
392
+ const barRef = useRef2(null);
393
+ const [isVariablesOpen, setIsVariablesOpen] = useState3(false);
394
+ const [isFormattingOpen, setIsFormattingOpen] = useState3(false);
395
+ const [activeFormatting, setActiveFormatting] = useState3("");
396
+ const quickRepliesListboxId = useId();
397
+ const showQuickRepliesButton = Boolean(savedQuickReplies);
398
+ const [isQuickRepliesOpen, setIsQuickRepliesOpen] = useState3(false);
399
+ const [quickRepliesTerm, setQuickRepliesTerm] = useState3("");
400
+ const [quickRepliesMode, setQuickRepliesMode] = useState3("shortcut");
401
+ const quickRepliesPopoverRef = useRef2(null);
402
+ const quickRepliesTriggerRef = useRef2(null);
403
+ const savedRangeRef = useRef2(void 0);
404
+ const barWidth = useContainerWidth(barRef);
405
+ const isCompact = barWidth !== void 0 && barWidth < COMPOSER_COMPACT_WIDTH;
406
+ const lastEmittedRef = useRef2("");
407
+ const emitChange = useCallback(
408
+ (html) => {
409
+ const text = htmlToWA(html);
410
+ lastEmittedRef.current = text;
411
+ onChange(text);
412
+ },
413
+ [onChange]
414
+ );
415
+ useEffect3(() => {
429
416
  const editor = editorRef.current;
430
- if (!editor) return;
431
- editor.innerHTML = "";
432
- emitChange("");
433
- },
434
- focus: () => editorRef.current?.focus()
435
- }), [emitChange, replaceContent]);
436
- const currentRange = useCallback(() => {
437
- const editor = editorRef.current;
438
- if (!editor) return void 0;
439
- editor.focus();
440
- const selection = window.getSelection();
441
- if (!selection?.rangeCount || !editor.contains(selection.anchorNode)) return void 0;
442
- return selection.getRangeAt(0);
443
- }, []);
444
- const commitRange = useCallback((range, node) => {
445
- const selection = window.getSelection();
446
- range.setStartAfter(node);
447
- range.collapse(true);
448
- selection?.removeAllRanges();
449
- selection?.addRange(range);
450
- emitChange(editorRef.current?.innerHTML ?? "");
451
- }, [emitChange]);
452
- const insertAtCursor = useCallback((fragment) => {
453
- const range = currentRange();
454
- if (!range) return;
455
- range.deleteContents();
456
- const textNode = document.createTextNode(fragment);
457
- range.insertNode(textNode);
458
- commitRange(range, textNode);
459
- }, [commitRange, currentRange]);
460
- const wrapSelection = useCallback((action) => {
461
- const range = currentRange();
462
- const selectedText = range?.toString();
463
- if (!range || !selectedText) return;
464
- range.deleteContents();
465
- const wrapper = document.createElement(FORMATTING_ELEMENT[action]);
466
- if (action === "monospace") wrapper.className = COMPOSER_MONOSPACE_CLASS;
467
- wrapper.textContent = selectedText;
468
- range.insertNode(wrapper);
469
- commitRange(range, wrapper);
470
- }, [commitRange, currentRange]);
471
- const applyFormatting = useCallback((action) => {
472
- const editor = editorRef.current;
473
- const range = currentRange();
474
- if (!editor || !range) return;
475
- if (action === FORMATTING_ACTION.MONOSPACE) {
476
- const code = codeAncestorOf({ node: range.startContainer, editor });
477
- if (range.collapsed) {
478
- if (code) exitMonospaceAfter(code);
479
- else startMonospaceAt(range);
480
- } else if (code) {
481
- unwrapMonospace(code);
482
- } else {
483
- wrapSelection(action);
417
+ if (!editor || value === lastEmittedRef.current) return;
418
+ lastEmittedRef.current = value;
419
+ editor.innerHTML = waToHTML(value);
420
+ }, [value]);
421
+ const refreshActiveFormatting = useCallback(() => {
422
+ const editor = editorRef.current;
423
+ if (editor) setActiveFormatting(activeFormattingIn(editor));
424
+ }, []);
425
+ useEffect3(() => {
426
+ document.addEventListener("selectionchange", refreshActiveFormatting);
427
+ return () => document.removeEventListener("selectionchange", refreshActiveFormatting);
428
+ }, [refreshActiveFormatting]);
429
+ useEffect3(() => {
430
+ if (!isVariablesOpen) return;
431
+ const closeOnOutsideClick = (event) => {
432
+ if (!variablesRef.current?.contains(event.target)) setIsVariablesOpen(false);
433
+ };
434
+ document.addEventListener("mousedown", closeOnOutsideClick);
435
+ return () => document.removeEventListener("mousedown", closeOnOutsideClick);
436
+ }, [isVariablesOpen]);
437
+ useEffect3(() => {
438
+ if (!isFormattingOpen) return;
439
+ const closeOnOutsideClick = (event) => {
440
+ if (!formattingRef.current?.contains(event.target)) setIsFormattingOpen(false);
441
+ };
442
+ document.addEventListener("mousedown", closeOnOutsideClick);
443
+ return () => document.removeEventListener("mousedown", closeOnOutsideClick);
444
+ }, [isFormattingOpen]);
445
+ useEffect3(() => {
446
+ if (!isCompact) setIsFormattingOpen(false);
447
+ }, [isCompact]);
448
+ const tooltipOf = (action) => tooltips?.[action] ?? DEFAULT_RICH_COMPOSER_TOOLTIPS[action];
449
+ const shows = (action) => toolbar?.[action] !== false;
450
+ const replaceContent = useCallback(
451
+ (text) => {
452
+ const editor = editorRef.current;
453
+ if (!editor) return;
454
+ editor.innerHTML = waToHTML(text);
455
+ emitChange(editor.innerHTML);
456
+ editor.focus();
457
+ },
458
+ [emitChange]
459
+ );
460
+ useImperativeHandle(
461
+ ref,
462
+ () => ({
463
+ setContent: replaceContent,
464
+ clear: () => {
465
+ const editor = editorRef.current;
466
+ if (!editor) return;
467
+ editor.innerHTML = "";
468
+ emitChange("");
469
+ },
470
+ focus: () => editorRef.current?.focus()
471
+ }),
472
+ [emitChange, replaceContent]
473
+ );
474
+ const currentRange = useCallback(() => {
475
+ const editor = editorRef.current;
476
+ if (!editor) return void 0;
477
+ editor.focus();
478
+ const selection = window.getSelection();
479
+ if (!selection?.rangeCount || !editor.contains(selection.anchorNode)) return void 0;
480
+ return selection.getRangeAt(0);
481
+ }, []);
482
+ const rangeIfFocused = useCallback(() => {
483
+ const editor = editorRef.current;
484
+ if (!editor || document.activeElement !== editor) return void 0;
485
+ const selection = window.getSelection();
486
+ if (!selection?.rangeCount || !editor.contains(selection.anchorNode)) return void 0;
487
+ return selection.getRangeAt(0);
488
+ }, []);
489
+ const textBeforeCaret = useCallback((range) => {
490
+ const editor = editorRef.current;
491
+ if (!editor) return "";
492
+ const preRange = document.createRange();
493
+ preRange.selectNodeContents(editor);
494
+ preRange.setEnd(range.startContainer, range.startOffset);
495
+ return preRange.toString();
496
+ }, []);
497
+ useEffect3(() => {
498
+ if (!showQuickRepliesButton) {
499
+ setIsQuickRepliesOpen(false);
500
+ return;
484
501
  }
485
- emitChange(editor.innerHTML);
486
- refreshActiveFormatting();
487
- return;
488
- }
489
- if (toggleFormattingCommand(action)) emitChange(editor.innerHTML);
490
- else wrapSelection(action);
491
- refreshActiveFormatting();
492
- }, [currentRange, emitChange, refreshActiveFormatting, wrapSelection]);
493
- const handleInput = useCallback((event) => {
494
- emitChange(event.currentTarget.innerHTML);
495
- refreshActiveFormatting();
496
- }, [emitChange, refreshActiveFormatting]);
497
- const handleKeyDown = useCallback((event) => {
498
- if (event.key === "Enter" && !event.shiftKey) {
499
- event.preventDefault();
500
- if (!disabled && !isSending) onSend();
501
- }
502
- }, [disabled, isSending, onSend]);
503
- const handleFileChange = useCallback((event) => {
504
- if (event.target.files?.length) onAttachFiles?.(event.target.files);
505
- if (fileInputRef.current) fileInputRef.current.value = "";
506
- }, [onAttachFiles]);
507
- const canSend = value.trim().length > 0 || hasQueuedAttachments;
508
- const formattingButtons = [
509
- [RICH_COMPOSER_ACTION.BOLD, Bold],
510
- [RICH_COMPOSER_ACTION.ITALIC, Italic],
511
- [RICH_COMPOSER_ACTION.STRIKETHROUGH, Strikethrough],
512
- [RICH_COMPOSER_ACTION.MONOSPACE, Code]
513
- ].filter(([action]) => shows(action)).map(([action, Icon]) => {
514
- const isActive = isFormattingActive({ active: activeFormatting, action });
515
- return /* @__PURE__ */ jsx2(
516
- "button",
517
- {
518
- type: "button",
519
- onMouseDown: (event) => event.preventDefault(),
520
- onClick: () => applyFormatting(action),
521
- "data-cv-tooltip": tooltipOf(action),
522
- "aria-label": tooltipOf(action),
523
- "aria-pressed": isActive,
524
- className: cn(
525
- COMPOSER_TOOL_BUTTON_CLASS,
526
- isActive ? COMPOSER_TOOL_BUTTON_ACTIVE_CLASS : COMPOSER_TOOL_BUTTON_IDLE_CLASS
527
- ),
528
- children: /* @__PURE__ */ jsx2(Icon, { size: 18 })
502
+ if (quickRepliesMode === "button") return;
503
+ const range = rangeIfFocused();
504
+ const textToCaret = range ? textBeforeCaret(range) : "";
505
+ const match = detectQuickReplyShortcut(textToCaret);
506
+ setIsQuickRepliesOpen(Boolean(match));
507
+ setQuickRepliesTerm(match?.term ?? "");
508
+ }, [value, showQuickRepliesButton, quickRepliesMode, rangeIfFocused, textBeforeCaret]);
509
+ const commitRange = useCallback(
510
+ (range, node) => {
511
+ const selection = window.getSelection();
512
+ range.setStartAfter(node);
513
+ range.collapse(true);
514
+ selection?.removeAllRanges();
515
+ selection?.addRange(range);
516
+ emitChange(editorRef.current?.innerHTML ?? "");
529
517
  },
530
- action
518
+ [emitChange]
531
519
  );
532
- });
533
- return /* @__PURE__ */ jsxs2("div", { className: cn(COMPOSER_BAR_CLASS, "flex flex-col", className), children: [
534
- attachmentsPreview,
535
- quickReplies?.length ? (
536
- /* Uma linha só, rolando na horizontal. Deixar quebrar em várias linhas faz a barra crescer
537
- conforme o número de respostas rápidas e empurrar a conversa para cima — o composer
538
- precisa ter altura previsível, independente de quantos atalhos o host configurou. */
539
- /* @__PURE__ */ jsx2("div", { className: "mb-2 flex flex-nowrap gap-1 overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden", children: quickReplies.map((quickReply) => /* @__PURE__ */ jsx2(
520
+ const insertAtCursor2 = useCallback(
521
+ (fragment) => {
522
+ const range = currentRange();
523
+ if (!range) return;
524
+ range.deleteContents();
525
+ const textNode = document.createTextNode(fragment);
526
+ range.insertNode(textNode);
527
+ commitRange(range, textNode);
528
+ },
529
+ [commitRange, currentRange]
530
+ );
531
+ const closeQuickReplies = useCallback(() => {
532
+ setIsQuickRepliesOpen(false);
533
+ setQuickRepliesTerm("");
534
+ setQuickRepliesMode("shortcut");
535
+ }, []);
536
+ const closeQuickRepliesAndRefocus = useCallback(() => {
537
+ closeQuickReplies();
538
+ editorRef.current?.focus();
539
+ }, [closeQuickReplies]);
540
+ const insertSavedQuickReply = useCallback(
541
+ (quickReply) => {
542
+ savedQuickReplies?.onSelect?.(quickReply);
543
+ const resolvedBody = applyQuickReplyVariables(quickReply.body, savedQuickReplies?.variables);
544
+ if (quickRepliesMode === "button") {
545
+ const editor = editorRef.current;
546
+ const savedRange = savedRangeRef.current;
547
+ if (editor && savedRange) {
548
+ editor.focus();
549
+ const selection = window.getSelection();
550
+ selection?.removeAllRanges();
551
+ selection?.addRange(savedRange);
552
+ }
553
+ insertAtCursor2(resolvedBody);
554
+ closeQuickReplies();
555
+ requestAnimationFrame(() => editor?.focus());
556
+ return;
557
+ }
558
+ const range = currentRange();
559
+ if (!range) return;
560
+ const match = detectQuickReplyShortcut(textBeforeCaret(range));
561
+ if (!match) {
562
+ closeQuickReplies();
563
+ return;
564
+ }
565
+ const shortcutLength = match.term.length + 1;
566
+ if (range.startContainer.nodeType === Node.TEXT_NODE && range.startOffset >= shortcutLength) {
567
+ range.setStart(range.startContainer, range.startOffset - shortcutLength);
568
+ range.deleteContents();
569
+ const textNode = document.createTextNode(resolvedBody);
570
+ range.insertNode(textNode);
571
+ commitRange(range, textNode);
572
+ }
573
+ closeQuickReplies();
574
+ },
575
+ [
576
+ quickRepliesMode,
577
+ currentRange,
578
+ textBeforeCaret,
579
+ commitRange,
580
+ insertAtCursor2,
581
+ savedQuickReplies,
582
+ closeQuickReplies
583
+ ]
584
+ );
585
+ const quickRepliesPicker = useQuickRepliesPicker({
586
+ isOpen: isQuickRepliesOpen,
587
+ search: quickRepliesTerm,
588
+ listQuickReplies: savedQuickReplies?.listQuickReplies,
589
+ quickReplyVariables: savedQuickReplies?.variables,
590
+ onSelect: insertSavedQuickReply,
591
+ onClose: closeQuickRepliesAndRefocus
592
+ });
593
+ const openQuickRepliesViaButton = useCallback(() => {
594
+ if (isQuickRepliesOpen && quickRepliesMode === "button") {
595
+ closeQuickRepliesAndRefocus();
596
+ return;
597
+ }
598
+ savedRangeRef.current = currentRange();
599
+ setQuickRepliesMode("button");
600
+ setQuickRepliesTerm("");
601
+ setIsQuickRepliesOpen(true);
602
+ }, [isQuickRepliesOpen, quickRepliesMode, currentRange, closeQuickRepliesAndRefocus]);
603
+ useEffect3(() => {
604
+ if (!isQuickRepliesOpen || quickRepliesMode !== "button") return;
605
+ const handleOutsideClick = (event) => {
606
+ const target = event.target;
607
+ if (quickRepliesPopoverRef.current?.contains(target)) return;
608
+ if (quickRepliesTriggerRef.current?.contains(target)) return;
609
+ closeQuickRepliesAndRefocus();
610
+ };
611
+ document.addEventListener("mousedown", handleOutsideClick);
612
+ return () => document.removeEventListener("mousedown", handleOutsideClick);
613
+ }, [isQuickRepliesOpen, quickRepliesMode, closeQuickRepliesAndRefocus]);
614
+ const wrapSelection = useCallback(
615
+ (action) => {
616
+ const range = currentRange();
617
+ const selectedText = range?.toString();
618
+ if (!range || !selectedText) return;
619
+ range.deleteContents();
620
+ const wrapper = document.createElement(FORMATTING_ELEMENT[action]);
621
+ if (action === "monospace") wrapper.className = COMPOSER_MONOSPACE_CLASS;
622
+ wrapper.textContent = selectedText;
623
+ range.insertNode(wrapper);
624
+ commitRange(range, wrapper);
625
+ },
626
+ [commitRange, currentRange]
627
+ );
628
+ const applyFormatting = useCallback(
629
+ (action) => {
630
+ const editor = editorRef.current;
631
+ const range = currentRange();
632
+ if (!editor || !range) return;
633
+ if (action === FORMATTING_ACTION.MONOSPACE) {
634
+ const code = codeAncestorOf({ node: range.startContainer, editor });
635
+ if (range.collapsed) {
636
+ if (code) exitMonospaceAfter(code);
637
+ else startMonospaceAt(range);
638
+ } else if (code) {
639
+ unwrapMonospace(code);
640
+ } else {
641
+ wrapSelection(action);
642
+ }
643
+ emitChange(editor.innerHTML);
644
+ refreshActiveFormatting();
645
+ return;
646
+ }
647
+ if (toggleFormattingCommand(action)) emitChange(editor.innerHTML);
648
+ else wrapSelection(action);
649
+ refreshActiveFormatting();
650
+ },
651
+ [currentRange, emitChange, refreshActiveFormatting, wrapSelection]
652
+ );
653
+ const handleInput = useCallback(
654
+ (event) => {
655
+ emitChange(event.currentTarget.innerHTML);
656
+ refreshActiveFormatting();
657
+ },
658
+ [emitChange, refreshActiveFormatting]
659
+ );
660
+ const handleKeyDown = useCallback(
661
+ (event) => {
662
+ if (isQuickRepliesOpen && quickRepliesMode === "shortcut") {
663
+ quickRepliesPicker.handleKeyDown(event);
664
+ return;
665
+ }
666
+ if (event.key === "Enter" && !event.shiftKey) {
667
+ event.preventDefault();
668
+ if (!disabled && !isSending) onSend();
669
+ }
670
+ },
671
+ [disabled, isSending, onSend, isQuickRepliesOpen, quickRepliesMode, quickRepliesPicker]
672
+ );
673
+ const handleFileChange = useCallback(
674
+ (event) => {
675
+ if (event.target.files?.length) onAttachFiles?.(event.target.files);
676
+ if (fileInputRef.current) fileInputRef.current.value = "";
677
+ },
678
+ [onAttachFiles]
679
+ );
680
+ const canSend = value.trim().length > 0 || hasQueuedAttachments;
681
+ const formattingButtons = [
682
+ [RICH_COMPOSER_ACTION.BOLD, Bold],
683
+ [RICH_COMPOSER_ACTION.ITALIC, Italic],
684
+ [RICH_COMPOSER_ACTION.STRIKETHROUGH, Strikethrough],
685
+ [RICH_COMPOSER_ACTION.MONOSPACE, Code]
686
+ ].filter(([action]) => shows(action)).map(([action, Icon]) => {
687
+ const isActive = isFormattingActive({ active: activeFormatting, action });
688
+ return /* @__PURE__ */ jsx2(
540
689
  "button",
541
690
  {
542
691
  type: "button",
543
- "data-cv-tooltip": quickReply.tooltip,
544
- "aria-label": quickReply.tooltip,
545
- onClick: () => replaceContent(quickReply.text),
546
- className: cn(QUICK_REPLY_PILL_CLASS, "flex-shrink-0"),
547
- children: quickReply.label
692
+ onMouseDown: (event) => event.preventDefault(),
693
+ onClick: () => applyFormatting(action),
694
+ "data-cv-tooltip": tooltipOf(action),
695
+ "aria-label": tooltipOf(action),
696
+ "aria-pressed": isActive,
697
+ className: cn(
698
+ COMPOSER_TOOL_BUTTON_CLASS,
699
+ isActive ? COMPOSER_TOOL_BUTTON_ACTIVE_CLASS : COMPOSER_TOOL_BUTTON_IDLE_CLASS
700
+ ),
701
+ children: /* @__PURE__ */ jsx2(Icon, { size: 18 })
548
702
  },
549
- quickReply.id
550
- )) })
551
- ) : null,
552
- /* @__PURE__ */ jsxs2("div", { ref: barRef, className: "flex flex-nowrap items-end gap-1", children: [
553
- formattingButtons.length > 0 ? isCompact ? /* @__PURE__ */ jsxs2("div", { ref: formattingRef, className: "relative hidden flex-shrink-0 sm:block", children: [
554
- /* @__PURE__ */ jsx2(
555
- "button",
556
- {
557
- type: "button",
558
- onMouseDown: (event) => event.preventDefault(),
559
- onClick: () => setIsFormattingOpen((open) => !open),
560
- "data-cv-tooltip": tooltipOf("formatting"),
561
- "aria-label": tooltipOf("formatting"),
562
- "aria-expanded": isFormattingOpen,
563
- className: cn(
564
- COMPOSER_TOOL_BUTTON_CLASS,
565
- activeFormatting || isFormattingOpen ? COMPOSER_TOOL_BUTTON_ACTIVE_CLASS : COMPOSER_TOOL_BUTTON_IDLE_CLASS
566
- ),
567
- children: /* @__PURE__ */ jsx2(Type, { size: 18 })
568
- }
569
- ),
570
- isFormattingOpen ? /* @__PURE__ */ jsx2("div", { className: "absolute bottom-full left-0 z-20 mb-2 flex items-center gap-1 rounded-full border border-gray-200 bg-white px-1 py-1 shadow-lg dark:border-gray-700 dark:bg-gray-800", children: formattingButtons }) : null
571
- ] }) : /* @__PURE__ */ jsx2("div", { className: "hidden flex-shrink-0 items-center gap-1 sm:flex", children: formattingButtons }) : null,
572
- shows(RICH_COMPOSER_ACTION.EMOJI) ? /* @__PURE__ */ jsx2(SimpleEmojiPicker, { onSelect: insertAtCursor, label: tooltipOf("emoji") }) : null,
573
- variables?.length && shows(RICH_COMPOSER_ACTION.VARIABLES) ? /* @__PURE__ */ jsxs2("div", { ref: variablesRef, className: "relative", children: [
574
- /* @__PURE__ */ jsx2(
575
- "button",
576
- {
577
- type: "button",
578
- onClick: () => setIsVariablesOpen((open) => !open),
579
- "data-cv-tooltip": tooltipOf("variables"),
580
- "aria-label": tooltipOf("variables"),
581
- "aria-expanded": isVariablesOpen,
582
- className: cn(COMPOSER_TOOL_BUTTON_CLASS, COMPOSER_TOOL_BUTTON_IDLE_CLASS),
583
- children: /* @__PURE__ */ jsx2(Braces, { size: 18 })
584
- }
585
- ),
586
- isVariablesOpen ? /* @__PURE__ */ jsx2("div", { className: "absolute bottom-full left-0 z-20 mb-2 max-h-56 w-64 overflow-y-auto rounded-xl border border-gray-200 bg-white py-1 shadow-lg dark:border-gray-700 dark:bg-gray-800", children: variables.map((variable) => /* @__PURE__ */ jsxs2(
703
+ action
704
+ );
705
+ });
706
+ return /* @__PURE__ */ jsxs2("div", { className: cn(COMPOSER_BAR_CLASS, "flex flex-col", className), children: [
707
+ attachmentsPreview,
708
+ quickReplies?.length ? (
709
+ /* Uma linha só, rolando na horizontal. Deixar quebrar em várias linhas faz a barra crescer
710
+ conforme o número de respostas rápidas e empurrar a conversa para cima — o composer
711
+ precisa ter altura previsível, independente de quantos atalhos o host configurou. */
712
+ /* @__PURE__ */ jsx2("div", { className: "mb-2 flex flex-nowrap gap-1 overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden", children: quickReplies.map((quickReply) => /* @__PURE__ */ jsx2(
587
713
  "button",
588
714
  {
589
715
  type: "button",
590
- "data-cv-tooltip": variable.tooltip ?? variable.value,
591
- "aria-label": variable.tooltip ?? variable.value,
592
- onClick: () => {
593
- insertAtCursor(variable.value);
594
- setIsVariablesOpen(false);
595
- },
596
- className: "flex w-full flex-col items-start px-3 py-1.5 text-left hover:bg-teal-50 dark:hover:bg-teal-950/40",
597
- children: [
598
- /* @__PURE__ */ jsx2("span", { className: "text-sm text-gray-800 dark:text-gray-100", children: variable.label }),
599
- /* @__PURE__ */ jsx2("span", { className: "font-mono text-xs text-gray-400 dark:text-gray-500", children: variable.value })
600
- ]
716
+ "data-cv-tooltip": quickReply.tooltip,
717
+ "aria-label": quickReply.tooltip,
718
+ onClick: () => replaceContent(quickReply.text),
719
+ className: cn(QUICK_REPLY_PILL_CLASS, "flex-shrink-0"),
720
+ children: quickReply.label
601
721
  },
602
- variable.id
603
- )) }) : null
604
- ] }) : null,
605
- onAttachFiles && shows(RICH_COMPOSER_ACTION.ATTACH) ? /* @__PURE__ */ jsxs2(Fragment, { children: [
722
+ quickReply.id
723
+ )) })
724
+ ) : null,
725
+ /* @__PURE__ */ jsxs2("div", { ref: barRef, className: "flex flex-nowrap items-end gap-1", children: [
726
+ formattingButtons.length > 0 ? isCompact ? /* @__PURE__ */ jsxs2("div", { ref: formattingRef, className: "relative hidden flex-shrink-0 sm:block", children: [
727
+ /* @__PURE__ */ jsx2(
728
+ "button",
729
+ {
730
+ type: "button",
731
+ onMouseDown: (event) => event.preventDefault(),
732
+ onClick: () => setIsFormattingOpen((open) => !open),
733
+ "data-cv-tooltip": tooltipOf("formatting"),
734
+ "aria-label": tooltipOf("formatting"),
735
+ "aria-expanded": isFormattingOpen,
736
+ className: cn(
737
+ COMPOSER_TOOL_BUTTON_CLASS,
738
+ activeFormatting || isFormattingOpen ? COMPOSER_TOOL_BUTTON_ACTIVE_CLASS : COMPOSER_TOOL_BUTTON_IDLE_CLASS
739
+ ),
740
+ children: /* @__PURE__ */ jsx2(Type, { size: 18 })
741
+ }
742
+ ),
743
+ isFormattingOpen ? /* @__PURE__ */ jsx2("div", { className: "absolute bottom-full left-0 z-20 mb-2 flex items-center gap-1 rounded-full border border-gray-200 bg-white px-1 py-1 shadow-lg dark:border-gray-700 dark:bg-gray-800", children: formattingButtons }) : null
744
+ ] }) : /* @__PURE__ */ jsx2("div", { className: "hidden flex-shrink-0 items-center gap-1 sm:flex", children: formattingButtons }) : null,
745
+ shows(RICH_COMPOSER_ACTION.EMOJI) ? /* @__PURE__ */ jsx2(SimpleEmojiPicker, { onSelect: insertAtCursor2, label: tooltipOf("emoji") }) : null,
746
+ variables?.length && shows(RICH_COMPOSER_ACTION.VARIABLES) ? /* @__PURE__ */ jsxs2("div", { ref: variablesRef, className: "relative", children: [
747
+ /* @__PURE__ */ jsx2(
748
+ "button",
749
+ {
750
+ type: "button",
751
+ onClick: () => setIsVariablesOpen((open) => !open),
752
+ "data-cv-tooltip": tooltipOf("variables"),
753
+ "aria-label": tooltipOf("variables"),
754
+ "aria-expanded": isVariablesOpen,
755
+ className: cn(COMPOSER_TOOL_BUTTON_CLASS, COMPOSER_TOOL_BUTTON_IDLE_CLASS),
756
+ children: /* @__PURE__ */ jsx2(Braces, { size: 18 })
757
+ }
758
+ ),
759
+ isVariablesOpen ? /* @__PURE__ */ jsx2("div", { className: "absolute bottom-full left-0 z-20 mb-2 max-h-56 w-64 overflow-y-auto rounded-xl border border-gray-200 bg-white py-1 shadow-lg dark:border-gray-700 dark:bg-gray-800", children: variables.map((variable) => /* @__PURE__ */ jsxs2(
760
+ "button",
761
+ {
762
+ type: "button",
763
+ "data-cv-tooltip": variable.tooltip ?? variable.value,
764
+ "aria-label": variable.tooltip ?? variable.value,
765
+ onClick: () => {
766
+ insertAtCursor2(variable.value);
767
+ setIsVariablesOpen(false);
768
+ },
769
+ className: "flex w-full flex-col items-start px-3 py-1.5 text-left hover:bg-teal-50 dark:hover:bg-teal-950/40",
770
+ children: [
771
+ /* @__PURE__ */ jsx2("span", { className: "text-sm text-gray-800 dark:text-gray-100", children: variable.label }),
772
+ /* @__PURE__ */ jsx2("span", { className: "font-mono text-xs text-gray-400 dark:text-gray-500", children: variable.value })
773
+ ]
774
+ },
775
+ variable.id
776
+ )) }) : null
777
+ ] }) : null,
778
+ showQuickRepliesButton && shows(RICH_COMPOSER_ACTION.QUICK_REPLIES) ? /* @__PURE__ */ jsxs2("div", { className: "relative", children: [
779
+ /* @__PURE__ */ jsx2(
780
+ "button",
781
+ {
782
+ ref: quickRepliesTriggerRef,
783
+ type: "button",
784
+ onClick: openQuickRepliesViaButton,
785
+ "data-cv-tooltip": tooltipOf("quickReplies"),
786
+ "aria-label": tooltipOf("quickReplies"),
787
+ "aria-haspopup": "listbox",
788
+ "aria-expanded": isQuickRepliesOpen,
789
+ "aria-controls": quickRepliesListboxId,
790
+ className: cn(COMPOSER_TOOL_BUTTON_CLASS, COMPOSER_TOOL_BUTTON_IDLE_CLASS),
791
+ children: /* @__PURE__ */ jsx2(Zap, { size: 18 })
792
+ }
793
+ ),
794
+ isQuickRepliesOpen ? /* @__PURE__ */ jsx2("div", { ref: quickRepliesPopoverRef, className: "absolute bottom-full left-0 z-20 mb-2", children: /* @__PURE__ */ jsx2(
795
+ QuickRepliesPicker,
796
+ {
797
+ id: quickRepliesListboxId,
798
+ items: quickRepliesPicker.items,
799
+ search: quickRepliesTerm,
800
+ highlightedIndex: quickRepliesPicker.highlightedIndex,
801
+ isLoading: quickRepliesPicker.isLoading,
802
+ hasError: quickRepliesPicker.hasError,
803
+ onHover: quickRepliesPicker.setHighlightedIndex,
804
+ onSelect: insertSavedQuickReply,
805
+ labels: savedQuickReplies?.labels,
806
+ variables: savedQuickReplies?.variables,
807
+ hasAttachmentsCapability: savedQuickReplies?.hasAttachmentsCapability,
808
+ ownSearch: quickRepliesMode === "button" ? {
809
+ value: quickRepliesTerm,
810
+ onChange: setQuickRepliesTerm,
811
+ onKeyDown: quickRepliesPicker.handleKeyDown,
812
+ label: tooltipOf("quickReplies")
813
+ } : void 0
814
+ },
815
+ quickRepliesMode
816
+ ) }) : null
817
+ ] }) : null,
818
+ onAttachFiles && shows(RICH_COMPOSER_ACTION.ATTACH) ? /* @__PURE__ */ jsxs2(Fragment, { children: [
819
+ /* @__PURE__ */ jsx2(
820
+ "input",
821
+ {
822
+ ref: fileInputRef,
823
+ type: "file",
824
+ multiple: true,
825
+ accept: acceptedFileTypes,
826
+ onChange: handleFileChange,
827
+ className: "hidden"
828
+ }
829
+ ),
830
+ /* @__PURE__ */ jsx2(
831
+ "button",
832
+ {
833
+ type: "button",
834
+ onClick: () => fileInputRef.current?.click(),
835
+ "data-cv-tooltip": tooltipOf("attach"),
836
+ "aria-label": tooltipOf("attach"),
837
+ className: cn(COMPOSER_TOOL_BUTTON_CLASS, COMPOSER_TOOL_BUTTON_IDLE_CLASS),
838
+ children: /* @__PURE__ */ jsx2(Paperclip, { size: 18 })
839
+ }
840
+ )
841
+ ] }) : null,
606
842
  /* @__PURE__ */ jsx2(
607
- "input",
843
+ "div",
608
844
  {
609
- ref: fileInputRef,
610
- type: "file",
611
- multiple: true,
612
- accept: acceptedFileTypes,
613
- onChange: handleFileChange,
614
- className: "hidden"
845
+ ref: editorRef,
846
+ contentEditable: !disabled,
847
+ suppressContentEditableWarning: true,
848
+ role: "textbox",
849
+ "aria-multiline": "true",
850
+ "aria-label": placeholder,
851
+ "aria-expanded": showQuickRepliesButton ? isQuickRepliesOpen : void 0,
852
+ "aria-controls": showQuickRepliesButton ? quickRepliesListboxId : void 0,
853
+ "aria-activedescendant": isQuickRepliesOpen && quickRepliesMode === "shortcut" ? `${quickRepliesListboxId}-option-${quickRepliesPicker.highlightedIndex}` : void 0,
854
+ "data-placeholder": placeholder,
855
+ onInput: handleInput,
856
+ onKeyDown: handleKeyDown,
857
+ style: { minHeight: "36px", maxHeight: "120px" },
858
+ className: "ml-1 min-w-0 flex-1 overflow-y-auto rounded-2xl border border-transparent bg-white px-4 py-2 text-sm text-gray-800 transition-all focus:border-transparent focus:outline-none focus:ring-2 focus:ring-teal-300 dark:border-gray-700 dark:bg-gray-700 dark:text-gray-100 empty:before:content-[attr(data-placeholder)] empty:before:text-gray-400 dark:empty:before:text-gray-500 before:pointer-events-none [&_strong]:font-semibold [&_b]:font-semibold [&_em]:italic [&_i]:italic [&_del]:line-through [&_s]:line-through [&_strike]:line-through [&_code]:rounded [&_code]:bg-black/5 [&_code]:px-0.5 [&_code]:font-mono [&_code]:text-sm dark:[&_code]:bg-white/10"
615
859
  }
616
860
  ),
617
- /* @__PURE__ */ jsx2(
861
+ !canSend && idleAction ? /* @__PURE__ */ jsx2("div", { className: "flex-shrink-0", children: idleAction }) : /* @__PURE__ */ jsx2(
618
862
  "button",
619
863
  {
620
864
  type: "button",
621
- onClick: () => fileInputRef.current?.click(),
622
- "data-cv-tooltip": tooltipOf("attach"),
623
- "aria-label": tooltipOf("attach"),
624
- className: cn(COMPOSER_TOOL_BUTTON_CLASS, COMPOSER_TOOL_BUTTON_IDLE_CLASS),
625
- children: /* @__PURE__ */ jsx2(Paperclip, { size: 18 })
865
+ onClick: onSend,
866
+ disabled: disabled || isSending || !canSend,
867
+ "data-cv-tooltip": tooltipOf("send"),
868
+ "aria-label": tooltipOf("send"),
869
+ className: "flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-full bg-teal-600 text-white transition-colors hover:bg-teal-700 disabled:cursor-not-allowed disabled:opacity-40",
870
+ children: isSending ? /* @__PURE__ */ jsx2("span", { className: "text-xs", children: "\u23F3" }) : /* @__PURE__ */ jsx2(SendHorizonal, { size: 16 })
626
871
  }
627
872
  )
628
- ] }) : null,
629
- /* @__PURE__ */ jsx2(
630
- "div",
631
- {
632
- ref: editorRef,
633
- contentEditable: !disabled,
634
- suppressContentEditableWarning: true,
635
- role: "textbox",
636
- "aria-multiline": "true",
637
- "aria-label": placeholder,
638
- "data-placeholder": placeholder,
639
- onInput: handleInput,
640
- onKeyDown: handleKeyDown,
641
- style: { minHeight: "36px", maxHeight: "120px" },
642
- className: "ml-1 min-w-0 flex-1 overflow-y-auto rounded-2xl border border-transparent bg-white px-4 py-2 text-sm text-gray-800 transition-all focus:border-transparent focus:outline-none focus:ring-2 focus:ring-teal-300 dark:border-gray-700 dark:bg-gray-700 dark:text-gray-100 empty:before:content-[attr(data-placeholder)] empty:before:text-gray-400 dark:empty:before:text-gray-500 before:pointer-events-none [&_strong]:font-semibold [&_b]:font-semibold [&_em]:italic [&_i]:italic [&_del]:line-through [&_s]:line-through [&_strike]:line-through [&_code]:rounded [&_code]:bg-black/5 [&_code]:px-0.5 [&_code]:font-mono [&_code]:text-sm dark:[&_code]:bg-white/10"
643
- }
644
- ),
645
- !canSend && idleAction ? /* @__PURE__ */ jsx2("div", { className: "flex-shrink-0", children: idleAction }) : /* @__PURE__ */ jsx2(
646
- "button",
647
- {
648
- type: "button",
649
- onClick: onSend,
650
- disabled: disabled || isSending || !canSend,
651
- "data-cv-tooltip": tooltipOf("send"),
652
- "aria-label": tooltipOf("send"),
653
- className: "flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-full bg-teal-600 text-white transition-colors hover:bg-teal-700 disabled:cursor-not-allowed disabled:opacity-40",
654
- children: isSending ? /* @__PURE__ */ jsx2("span", { className: "text-xs", children: "\u23F3" }) : /* @__PURE__ */ jsx2(SendHorizonal, { size: 16 })
655
- }
656
- )
657
- ] })
658
- ] });
659
- });
873
+ ] })
874
+ ] });
875
+ }
876
+ );
660
877
 
661
878
  // src/WhatsAppMessageEditor.tsx
662
879
  import { useRef as useRef3 } from "react";
@@ -724,7 +941,7 @@ function WhatsAppMessageEditor({
724
941
  textarea.setSelectionRange(start + marker.length, start + marker.length + selected.length);
725
942
  });
726
943
  }
727
- function insertAtCursor(snippet) {
944
+ function insertAtCursor2(snippet) {
728
945
  const textarea = textareaRef.current;
729
946
  if (!textarea) return;
730
947
  const start = textarea.selectionStart;
@@ -749,7 +966,7 @@ function WhatsAppMessageEditor({
749
966
  "button",
750
967
  {
751
968
  type: "button",
752
- onClick: () => insertAtCursor(token),
969
+ onClick: () => insertAtCursor2(token),
753
970
  className: "inline-flex items-center h-8 px-2 rounded-lg border border-gray-200 dark:border-gray-600 text-xs text-blue-600 dark:text-blue-300 hover:bg-blue-50 dark:hover:bg-blue-900/40 transition-colors",
754
971
  "data-cv-tooltip": editorLabels.insertPlaceholder(token),
755
972
  "aria-label": editorLabels.insertPlaceholder(token),
@@ -3988,52 +4205,1043 @@ function useInboxActions() {
3988
4205
  );
3989
4206
  }
3990
4207
 
3991
- // src/workspace/ConversationsWorkspace.tsx
3992
- import { useEffect as useEffect14, useMemo as useMemo6, useState as useState19 } from "react";
3993
- import { Check as Check2, CheckCheck, FlaskConical, Hourglass as Hourglass2, Mail as Mail2, MessagesSquare } from "lucide-react";
4208
+ // src/quickReplies/quickReply.types.ts
4209
+ var QUICK_REPLY_ATTACHMENT_LIMIT = 10;
3994
4210
 
3995
- // src/workspace/BulkTemplateModal.tsx
3996
- import { useEffect as useEffect11, useMemo as useMemo4, useState as useState16 } from "react";
3997
- import { jsx as jsx25, jsxs as jsxs23 } from "react/jsx-runtime";
3998
- function BulkTemplateModal({
4211
+ // src/quickReplies/quickReplyAttachments.ts
4212
+ function queuedAttachmentsFromQuickReply(quickReply, hasAttachmentsCapability) {
4213
+ if (!hasAttachmentsCapability || !quickReply.attachments?.length) return [];
4214
+ return quickReply.attachments.map(
4215
+ (attachment) => ({
4216
+ kind: "stored",
4217
+ uploadId: attachment.uploadId,
4218
+ filename: attachment.filename,
4219
+ mimeType: attachment.mimeType,
4220
+ sizeBytes: attachment.sizeBytes
4221
+ })
4222
+ );
4223
+ }
4224
+ function attachmentKey(item) {
4225
+ return item.kind === "stored" ? item.uploadId : item.localId;
4226
+ }
4227
+ var DEFAULT_MAX_ATTACHMENT_SIZE_BYTES = {
4228
+ document: 100 * 1024 * 1024,
4229
+ image: 5 * 1024 * 1024,
4230
+ audio: 16 * 1024 * 1024,
4231
+ video: 16 * 1024 * 1024
4232
+ };
4233
+ function orderOutgoingItems(text, queue) {
4234
+ const stored = queue.filter((item) => item.kind === "stored");
4235
+ const local = queue.filter((item) => item.kind === "local");
4236
+ return { text, attachments: [...stored, ...local] };
4237
+ }
4238
+ function applySendResults(queue, results) {
4239
+ const sentUploadIds = new Set(results.filter((result) => result.status === "sent").map((result) => result.uploadId));
4240
+ return queue.filter((item) => item.kind === "local" || !sentUploadIds.has(item.uploadId));
4241
+ }
4242
+ function canAddAttachments(current, adding) {
4243
+ return current + adding <= QUICK_REPLY_ATTACHMENT_LIMIT;
4244
+ }
4245
+ function resolveMaxAttachmentSizeBytes(mimeType, limits = DEFAULT_MAX_ATTACHMENT_SIZE_BYTES) {
4246
+ if (mimeType.startsWith("image/")) return limits.image;
4247
+ if (mimeType.startsWith("audio/")) return limits.audio;
4248
+ if (mimeType.startsWith("video/")) return limits.video;
4249
+ return limits.document;
4250
+ }
4251
+ function resolveIdempotencyKey(previous, uploadIds, generateKey = () => crypto.randomUUID()) {
4252
+ if (previous && sameUploadIds(previous.uploadIds, uploadIds)) return previous;
4253
+ return { key: generateKey(), uploadIds };
4254
+ }
4255
+ function sameUploadIds(a, b) {
4256
+ if (a.length !== b.length) return false;
4257
+ return a.every((id, index) => id === b[index]);
4258
+ }
4259
+ function hasSentEveryStoredUpload(storedUploadIds, sentAttachmentKeys) {
4260
+ if (storedUploadIds.length === 0) return true;
4261
+ const sentKeys = new Set(sentAttachmentKeys);
4262
+ return storedUploadIds.every((uploadId) => sentKeys.has(uploadId));
4263
+ }
4264
+ function shouldResetRetryKey(params) {
4265
+ const { current, attempted, sentAttachmentKeys, uploadId } = params;
4266
+ if (!sentAttachmentKeys.includes(uploadId)) return false;
4267
+ return current === attempted;
4268
+ }
4269
+ function attachmentSendStatusOf(status) {
4270
+ if (status === "sent") return "sent";
4271
+ if (status === "skipped") return "skipped";
4272
+ return "failed";
4273
+ }
4274
+ async function sendStoredBatch(stored, idempotencyKey, sendStoredAttachments, onAttachmentStatus) {
4275
+ for (const item of stored) onAttachmentStatus?.(attachmentKey(item), "sending");
4276
+ try {
4277
+ const response = await sendStoredAttachments({ uploadIds: stored.map((item) => item.uploadId), idempotencyKey });
4278
+ let results = response.results;
4279
+ const resultedUploadIds = new Set(results.map((result) => result.uploadId));
4280
+ for (const item of stored) {
4281
+ if (!resultedUploadIds.has(item.uploadId)) results = [...results, { uploadId: item.uploadId, status: "failed" }];
4282
+ }
4283
+ for (const result of results) {
4284
+ onAttachmentStatus?.(result.uploadId, attachmentSendStatusOf(result.status));
4285
+ }
4286
+ return results;
4287
+ } catch {
4288
+ for (const item of stored) onAttachmentStatus?.(attachmentKey(item), "failed");
4289
+ return stored.map((item) => ({ uploadId: item.uploadId, status: "failed" }));
4290
+ }
4291
+ }
4292
+ async function retryStoredAttachments(params) {
4293
+ const { queue, uploadIds, idempotencyKey, sendStoredAttachments, onAttachmentStatus } = params;
4294
+ const targetIds = new Set(uploadIds);
4295
+ const targets = queue.filter(
4296
+ (item) => item.kind === "stored" && targetIds.has(item.uploadId)
4297
+ );
4298
+ if (targets.length === 0) return { remainingQueue: queue, sentAttachmentKeys: [] };
4299
+ const results = await sendStoredBatch(targets, idempotencyKey, sendStoredAttachments, onAttachmentStatus);
4300
+ const sentAttachmentKeys = results.filter((result) => result.status === "sent").map((result) => result.uploadId);
4301
+ return { remainingQueue: applySendResults(queue, results), sentAttachmentKeys };
4302
+ }
4303
+ function resolveRetryOutcome(params) {
4304
+ const { conversationIdAtRetry, currentConversationId, sentAttachmentKeys, queue } = params;
4305
+ if (conversationIdAtRetry !== currentConversationId) return void 0;
4306
+ const sentKeys = new Set(sentAttachmentKeys);
4307
+ return queue.filter((item) => !sentKeys.has(attachmentKey(item)));
4308
+ }
4309
+ function excludeRetryingItems(queue, retryingKeys) {
4310
+ if (retryingKeys.size === 0) return queue;
4311
+ return queue.filter((item) => !retryingKeys.has(attachmentKey(item)));
4312
+ }
4313
+ async function sendQueuedMessage(params) {
4314
+ const { text, queue, idempotencyKey, sendText, sendStoredAttachments, sendLocalAttachments, onAttachmentStatus } = params;
4315
+ if (text.trim()) {
4316
+ const textSent = await sendText(text);
4317
+ if (!textSent) return { textSent: false, remainingQueue: queue, sentAttachmentKeys: [] };
4318
+ }
4319
+ const { attachments } = orderOutgoingItems(text, queue);
4320
+ const stored = attachments.filter((item) => item.kind === "stored");
4321
+ const local = attachments.filter(
4322
+ (item) => item.kind === "local"
4323
+ );
4324
+ const results = stored.length > 0 && sendStoredAttachments ? await sendStoredBatch(stored, idempotencyKey, sendStoredAttachments, onAttachmentStatus) : [];
4325
+ let remainingQueue = applySendResults(queue, results);
4326
+ const sentAttachmentKeys = results.filter((result) => result.status === "sent").map((result) => result.uploadId);
4327
+ if (local.length > 0 && sendLocalAttachments) {
4328
+ for (const item of local) onAttachmentStatus?.(attachmentKey(item), "sending");
4329
+ try {
4330
+ await sendLocalAttachments(local.map((item) => item.file));
4331
+ for (const item of local) onAttachmentStatus?.(attachmentKey(item), "sent");
4332
+ const sentKeys = new Set(local.map((item) => attachmentKey(item)));
4333
+ remainingQueue = remainingQueue.filter((item) => item.kind !== "local" || !sentKeys.has(attachmentKey(item)));
4334
+ sentAttachmentKeys.push(...sentKeys);
4335
+ } catch {
4336
+ for (const item of local) onAttachmentStatus?.(attachmentKey(item), "failed");
4337
+ }
4338
+ }
4339
+ return { textSent: true, remainingQueue, sentAttachmentKeys };
4340
+ }
4341
+
4342
+ // src/quickReplies/QuickRepliesWorkspace.tsx
4343
+ import { useEffect as useEffect13, useId as useId2, useRef as useRef11, useState as useState18 } from "react";
4344
+
4345
+ // src/quickReplies/useQuickRepliesWorkspace.ts
4346
+ import { useCallback as useCallback10, useEffect as useEffect12, useState as useState17 } from "react";
4347
+
4348
+ // src/quickReplies/quickReplyAttachmentUpload.ts
4349
+ function validateAttachmentFiles(files, currentCount, limits) {
4350
+ const accepted = [];
4351
+ const rejected = [];
4352
+ let count = currentCount;
4353
+ for (const file of files) {
4354
+ if (!canAddAttachments(count, 1)) {
4355
+ rejected.push({ file, reason: "limit" });
4356
+ continue;
4357
+ }
4358
+ const maxSize = resolveMaxAttachmentSizeBytes(file.type, limits);
4359
+ if (file.size > maxSize) {
4360
+ rejected.push({ file, reason: "size" });
4361
+ continue;
4362
+ }
4363
+ accepted.push(file);
4364
+ count += 1;
4365
+ }
4366
+ return { accepted, rejected };
4367
+ }
4368
+ function moveAttachment(list, index, direction) {
4369
+ const target = index + direction;
4370
+ if (index < 0 || index >= list.length || target < 0 || target >= list.length) return list;
4371
+ const next = [...list];
4372
+ const [item] = next.splice(index, 1);
4373
+ next.splice(target, 0, item);
4374
+ return next;
4375
+ }
4376
+
4377
+ // src/quickReplies/useQuickReplyAttachmentUploads.ts
4378
+ import { useCallback as useCallback9, useEffect as useEffect11, useRef as useRef9, useState as useState16 } from "react";
4379
+
4380
+ // src/quickReplies/createUploadQueue.ts
4381
+ function createUploadQueue(maxConcurrent) {
4382
+ const pending = [];
4383
+ const runners = /* @__PURE__ */ new Map();
4384
+ const controllers = /* @__PURE__ */ new Map();
4385
+ let activeCount = 0;
4386
+ function pump() {
4387
+ while (activeCount < maxConcurrent && pending.length > 0) {
4388
+ const id = pending.shift();
4389
+ const run = runners.get(id);
4390
+ runners.delete(id);
4391
+ if (!run) continue;
4392
+ const controller = new AbortController();
4393
+ controllers.set(id, controller);
4394
+ activeCount += 1;
4395
+ void run(controller.signal).finally(() => {
4396
+ controllers.delete(id);
4397
+ activeCount -= 1;
4398
+ pump();
4399
+ });
4400
+ }
4401
+ }
4402
+ function enqueue(id, run) {
4403
+ if (controllers.has(id)) return;
4404
+ runners.set(id, run);
4405
+ if (!pending.includes(id)) pending.push(id);
4406
+ pump();
4407
+ }
4408
+ function abort(id) {
4409
+ const controller = controllers.get(id);
4410
+ if (controller) {
4411
+ controller.abort();
4412
+ return;
4413
+ }
4414
+ const index = pending.indexOf(id);
4415
+ if (index >= 0) pending.splice(index, 1);
4416
+ runners.delete(id);
4417
+ }
4418
+ function abortAll() {
4419
+ for (const controller of controllers.values()) controller.abort();
4420
+ pending.length = 0;
4421
+ runners.clear();
4422
+ }
4423
+ return { enqueue, abort, abortAll };
4424
+ }
4425
+
4426
+ // src/quickReplies/useQuickReplyAttachmentUploads.ts
4427
+ function useQuickReplyAttachmentUploads({
4428
+ upload,
3999
4429
  labels,
4000
- expiredCount,
4001
- sending,
4002
- onClose,
4003
- onSend
4430
+ attachmentSizeLimits,
4431
+ attachmentsCount,
4432
+ onUploaded
4004
4433
  }) {
4005
- const context = useConversations();
4006
- if (!context) {
4007
- throw new Error("BulkTemplateModal requires an ancestor <ConversationsProvider>");
4434
+ const [pendingUploads, setPendingUploads] = useState16([]);
4435
+ const [attachmentRejections, setAttachmentRejections] = useState16([]);
4436
+ const uploadQueueRef = useRef9(createUploadQueue(3));
4437
+ const isMountedRef = useRef9(true);
4438
+ useEffect11(
4439
+ () => () => {
4440
+ isMountedRef.current = false;
4441
+ uploadQueueRef.current.abortAll();
4442
+ },
4443
+ []
4444
+ );
4445
+ const abortAllUploads = useCallback9(() => {
4446
+ uploadQueueRef.current.abortAll();
4447
+ setPendingUploads([]);
4448
+ }, []);
4449
+ const dismissAttachmentRejections = useCallback9(() => setAttachmentRejections([]), []);
4450
+ const cancelAttachmentUpload = useCallback9((localId) => {
4451
+ uploadQueueRef.current.abort(localId);
4452
+ setPendingUploads((current) => current.filter((item) => item.localId !== localId));
4453
+ }, []);
4454
+ const updatePendingUpload = useCallback9((localId, patch) => {
4455
+ setPendingUploads((current) => current.map((item) => item.localId === localId ? { ...item, ...patch } : item));
4456
+ }, []);
4457
+ const uploadOneFile = useCallback9(
4458
+ (localId, file) => {
4459
+ if (!upload) return;
4460
+ uploadQueueRef.current.enqueue(localId, async (signal) => {
4461
+ try {
4462
+ const attachment = await upload(file, {
4463
+ onProgress: (fraction) => {
4464
+ if (isMountedRef.current) updatePendingUpload(localId, { progress: fraction });
4465
+ },
4466
+ signal
4467
+ });
4468
+ if (!isMountedRef.current) return;
4469
+ onUploaded(attachment);
4470
+ setPendingUploads((current) => current.filter((item) => item.localId !== localId));
4471
+ } catch (caught) {
4472
+ if (signal.aborted || !isMountedRef.current) return;
4473
+ updatePendingUpload(localId, {
4474
+ status: "error",
4475
+ error: caught instanceof Error ? caught.message : labels.saveError
4476
+ });
4477
+ }
4478
+ });
4479
+ },
4480
+ [upload, updatePendingUpload, onUploaded, labels.saveError]
4481
+ );
4482
+ const retryAttachmentUpload = useCallback9(
4483
+ (localId) => {
4484
+ const item = pendingUploads.find((pending) => pending.localId === localId);
4485
+ if (!item) return;
4486
+ updatePendingUpload(localId, { status: "uploading", progress: 0, error: void 0 });
4487
+ uploadOneFile(localId, item.file);
4488
+ },
4489
+ [pendingUploads, updatePendingUpload, uploadOneFile]
4490
+ );
4491
+ const addAttachmentFiles = useCallback9(
4492
+ (files) => {
4493
+ if (!upload) return;
4494
+ const currentCount = attachmentsCount + pendingUploads.length;
4495
+ const { accepted, rejected } = validateAttachmentFiles(Array.from(files), currentCount, attachmentSizeLimits);
4496
+ setAttachmentRejections(rejected);
4497
+ if (accepted.length === 0) return;
4498
+ const newItems = accepted.map((file) => ({
4499
+ localId: `${file.name}-${file.size}-${file.lastModified}-${Math.random().toString(36).slice(2)}`,
4500
+ file,
4501
+ status: "uploading",
4502
+ progress: 0
4503
+ }));
4504
+ setPendingUploads((current) => [...current, ...newItems]);
4505
+ for (const item of newItems) uploadOneFile(item.localId, item.file);
4506
+ },
4507
+ [upload, attachmentsCount, pendingUploads.length, uploadOneFile, attachmentSizeLimits]
4508
+ );
4509
+ return {
4510
+ pendingUploads,
4511
+ attachmentRejections,
4512
+ addAttachmentFiles,
4513
+ retryAttachmentUpload,
4514
+ cancelAttachmentUpload,
4515
+ dismissAttachmentRejections,
4516
+ abortAllUploads
4517
+ };
4518
+ }
4519
+
4520
+ // src/quickReplies/useQuickRepliesWorkspace.ts
4521
+ var TITLE_MAX_LENGTH = 40;
4522
+ var BODY_MAX_LENGTH = 1e3;
4523
+ var SHORTCUT_PATTERN = /^[a-z0-9-]{1,20}$/;
4524
+ var MAX_ERROR_ENVELOPE_DEPTH = 3;
4525
+ function apiErrorEnvelopeOf(error, depth = 0) {
4526
+ if (depth >= MAX_ERROR_ENVELOPE_DEPTH) return void 0;
4527
+ if (!error || typeof error !== "object") return void 0;
4528
+ const candidate = error;
4529
+ if ("code" in candidate || "details" in candidate) return candidate;
4530
+ const nested = candidate.error ?? candidate.response?.data ?? candidate.body;
4531
+ if (nested && typeof nested === "object") return apiErrorEnvelopeOf(nested, depth + 1);
4532
+ return void 0;
4533
+ }
4534
+ function fieldErrorsOf(error) {
4535
+ const envelope = apiErrorEnvelopeOf(error);
4536
+ if (!envelope) return {};
4537
+ const details = envelope.details;
4538
+ if (!Array.isArray(details)) return {};
4539
+ const result = {};
4540
+ for (const detail of details) {
4541
+ if (!detail || typeof detail !== "object") continue;
4542
+ const field = detail.field;
4543
+ const message = detail.message;
4544
+ if (typeof field === "string" && typeof message === "string" && isFormField(field)) {
4545
+ result[field] = message;
4546
+ }
4008
4547
  }
4009
- const { api } = context;
4010
- const [templates, setTemplates] = useState16([]);
4011
- const [search, setSearch] = useState16("");
4012
- const [selected, setSelected] = useState16("");
4013
- useEffect11(() => {
4014
- let active = true;
4015
- void api.listTemplates?.().then((loaded) => {
4016
- if (active) setTemplates(loaded);
4017
- }).catch(() => {
4018
- });
4019
- return () => {
4020
- active = false;
4548
+ return result;
4549
+ }
4550
+ function isFormField(value) {
4551
+ return value === "title" || value === "shortcut" || value === "body";
4552
+ }
4553
+ function errorCodeOf(error) {
4554
+ const code = apiErrorEnvelopeOf(error)?.code;
4555
+ return typeof code === "string" ? code : void 0;
4556
+ }
4557
+ function validateQuickReplyInput(input, labels) {
4558
+ const errors = {};
4559
+ if (!input.title.trim() || input.title.length > TITLE_MAX_LENGTH) errors.title = labels.fieldTitleInvalid;
4560
+ if (!SHORTCUT_PATTERN.test(input.shortcut)) errors.shortcut = labels.fieldShortcutInvalid;
4561
+ if (!input.body.trim() || input.body.length > BODY_MAX_LENGTH) errors.body = labels.fieldBodyInvalid;
4562
+ return errors;
4563
+ }
4564
+ async function submitQuickReply({
4565
+ api,
4566
+ editing,
4567
+ labels
4568
+ }) {
4569
+ const input = {
4570
+ title: editing.title.trim(),
4571
+ shortcut: editing.shortcut.trim(),
4572
+ body: editing.body,
4573
+ // Só manda a lista quando o host oferece a porta de upload — ausente, `attachmentUploadIds`
4574
+ // some do payload e o backend não mexe nos anexos já gravados (contrato do tipo).
4575
+ ...api.uploadQuickReplyAttachment ? { attachmentUploadIds: editing.attachments.map((attachment) => attachment.uploadId) } : {}
4576
+ };
4577
+ const validationErrors = validateQuickReplyInput(input, labels);
4578
+ if (Object.keys(validationErrors).length > 0) return { outcome: "invalid", fieldErrors: validationErrors };
4579
+ const editingId = editing.id;
4580
+ if (editingId === null && !api.createQuickReply) {
4581
+ return { outcome: "rejected", fieldErrors: {}, formError: labels.saveUnavailable };
4582
+ }
4583
+ if (editingId !== null && !api.updateQuickReply) {
4584
+ return { outcome: "rejected", fieldErrors: {}, formError: labels.saveUnavailable };
4585
+ }
4586
+ const save = editingId === null ? api.createQuickReply : (input2) => api.updateQuickReply(editingId, input2);
4587
+ try {
4588
+ const quickReply = await save(input);
4589
+ if (!quickReply) return { outcome: "invalid", fieldErrors: {} };
4590
+ return { outcome: "saved", quickReply };
4591
+ } catch (caught) {
4592
+ const detailErrors = fieldErrorsOf(caught);
4593
+ const code = errorCodeOf(caught);
4594
+ const shortcutMessage = detailErrors.shortcut ?? (code === "QUICK_REPLY_SHORTCUT_TAKEN" ? labels.shortcutTaken : void 0);
4595
+ if (shortcutMessage || Object.keys(detailErrors).length > 0) {
4596
+ return {
4597
+ outcome: "rejected",
4598
+ fieldErrors: { ...detailErrors, ...shortcutMessage ? { shortcut: shortcutMessage } : {} }
4599
+ };
4600
+ }
4601
+ return {
4602
+ outcome: "rejected",
4603
+ fieldErrors: {},
4604
+ formError: labels.saveError
4021
4605
  };
4022
- }, [api]);
4606
+ }
4607
+ }
4608
+ function insertAtCursor(text, start, end, marker) {
4609
+ return { text: text.slice(0, start) + marker + text.slice(end), caret: start + marker.length };
4610
+ }
4611
+ function useQuickRepliesWorkspace({
4612
+ api,
4613
+ labels,
4614
+ attachmentSizeLimits
4615
+ }) {
4616
+ const [quickReplies, setQuickReplies] = useState17([]);
4617
+ const [isLoading, setIsLoading] = useState17(false);
4618
+ const [loadError, setLoadError] = useState17(void 0);
4619
+ const [search, setSearch] = useState17("");
4620
+ const [editing, setEditing] = useState17(void 0);
4621
+ const [fieldErrors, setFieldErrors] = useState17({});
4622
+ const [isSaving, setIsSaving] = useState17(false);
4623
+ const [saveError, setSaveError] = useState17(void 0);
4624
+ const [deletingId, setDeletingId] = useState17(void 0);
4625
+ const uploadAttachment = useCallback10((attachment) => {
4626
+ setEditing((current) => current ? { ...current, attachments: [...current.attachments, attachment] } : current);
4627
+ }, []);
4628
+ const {
4629
+ pendingUploads,
4630
+ attachmentRejections,
4631
+ addAttachmentFiles,
4632
+ retryAttachmentUpload,
4633
+ cancelAttachmentUpload,
4634
+ dismissAttachmentRejections,
4635
+ abortAllUploads
4636
+ } = useQuickReplyAttachmentUploads({
4637
+ ...api.uploadQuickReplyAttachment ? { upload: api.uploadQuickReplyAttachment } : {},
4638
+ labels,
4639
+ ...attachmentSizeLimits ? { attachmentSizeLimits } : {},
4640
+ attachmentsCount: editing?.attachments.length ?? 0,
4641
+ onUploaded: uploadAttachment
4642
+ });
4643
+ useEffect12(() => {
4644
+ if (!api.listQuickReplies) return;
4645
+ let cancelled = false;
4646
+ setIsLoading(true);
4647
+ setLoadError(void 0);
4648
+ api.listQuickReplies().then((result) => {
4649
+ if (!cancelled) setQuickReplies(result);
4650
+ }).catch((caught) => {
4651
+ if (!cancelled) setLoadError(caught instanceof Error ? caught.message : labels.failure);
4652
+ }).finally(() => {
4653
+ if (!cancelled) setIsLoading(false);
4654
+ });
4655
+ return () => {
4656
+ cancelled = true;
4657
+ };
4658
+ }, [api.listQuickReplies]);
4659
+ const filtered = filterQuickReplies({ quickReplies, search });
4660
+ const startCreate = useCallback10(() => {
4661
+ abortAllUploads();
4662
+ dismissAttachmentRejections();
4663
+ setEditing({ id: null, title: "", shortcut: "", body: "", attachments: [] });
4664
+ setFieldErrors({});
4665
+ setSaveError(void 0);
4666
+ }, [abortAllUploads, dismissAttachmentRejections]);
4667
+ const startEdit = useCallback10(
4668
+ (quickReply) => {
4669
+ abortAllUploads();
4670
+ dismissAttachmentRejections();
4671
+ setEditing({
4672
+ id: quickReply.id,
4673
+ title: quickReply.title,
4674
+ shortcut: quickReply.shortcut,
4675
+ body: quickReply.body,
4676
+ attachments: quickReply.attachments ?? []
4677
+ });
4678
+ setFieldErrors({});
4679
+ setSaveError(void 0);
4680
+ },
4681
+ [abortAllUploads, dismissAttachmentRejections]
4682
+ );
4683
+ const cancelEdit = useCallback10(() => {
4684
+ abortAllUploads();
4685
+ dismissAttachmentRejections();
4686
+ setEditing(void 0);
4687
+ setFieldErrors({});
4688
+ setSaveError(void 0);
4689
+ }, [abortAllUploads, dismissAttachmentRejections]);
4690
+ const updateField = useCallback10((field, value) => {
4691
+ setEditing((current) => current ? { ...current, [field]: value } : current);
4692
+ setFieldErrors((current) => {
4693
+ if (!(field in current)) return current;
4694
+ const next = { ...current };
4695
+ delete next[field];
4696
+ return next;
4697
+ });
4698
+ }, []);
4699
+ const submit = useCallback10(async () => {
4700
+ if (!editing || pendingUploads.length > 0) return;
4701
+ setIsSaving(true);
4702
+ setSaveError(void 0);
4703
+ setFieldErrors({});
4704
+ const result = await submitQuickReply({ api, editing, labels });
4705
+ setIsSaving(false);
4706
+ if (result.outcome === "saved") {
4707
+ setQuickReplies(
4708
+ (current) => editing.id === null ? [result.quickReply, ...current] : current.map((item) => item.id === result.quickReply.id ? result.quickReply : item)
4709
+ );
4710
+ setEditing(void 0);
4711
+ return;
4712
+ }
4713
+ setFieldErrors(result.fieldErrors);
4714
+ if (result.outcome === "rejected") setSaveError(result.formError);
4715
+ }, [editing, api, labels, pendingUploads.length]);
4716
+ const remove = useCallback10(
4717
+ async (id) => {
4718
+ if (!api.deleteQuickReply) return;
4719
+ setDeletingId(id);
4720
+ try {
4721
+ await api.deleteQuickReply(id);
4722
+ setQuickReplies((current) => current.filter((item) => item.id !== id));
4723
+ } finally {
4724
+ setDeletingId(void 0);
4725
+ }
4726
+ },
4727
+ [api]
4728
+ );
4729
+ const removeAttachment = useCallback10((uploadId) => {
4730
+ setEditing(
4731
+ (current) => current ? { ...current, attachments: current.attachments.filter((attachment) => attachment.uploadId !== uploadId) } : current
4732
+ );
4733
+ }, []);
4734
+ const moveAttachmentAt = useCallback10((index, direction) => {
4735
+ setEditing(
4736
+ (current) => current ? { ...current, attachments: moveAttachment(current.attachments, index, direction) } : current
4737
+ );
4738
+ }, []);
4739
+ return {
4740
+ quickReplies,
4741
+ filtered,
4742
+ isLoading,
4743
+ loadError,
4744
+ search,
4745
+ setSearch,
4746
+ readOnly: !api.createQuickReply,
4747
+ canEdit: Boolean(api.updateQuickReply),
4748
+ canDelete: Boolean(api.deleteQuickReply),
4749
+ editing,
4750
+ startCreate,
4751
+ startEdit,
4752
+ cancelEdit,
4753
+ updateField,
4754
+ fieldErrors,
4755
+ isSaving,
4756
+ saveError,
4757
+ submit,
4758
+ remove,
4759
+ deletingId,
4760
+ hasAttachmentsCapability: Boolean(api.uploadQuickReplyAttachment),
4761
+ pendingUploads,
4762
+ addAttachmentFiles,
4763
+ retryAttachmentUpload,
4764
+ cancelAttachmentUpload,
4765
+ removeAttachment,
4766
+ moveAttachmentAt,
4767
+ attachmentRejections,
4768
+ dismissAttachmentRejections
4769
+ };
4770
+ }
4771
+
4772
+ // src/quickReplies/AttachmentsFormSection.tsx
4773
+ import { useRef as useRef10 } from "react";
4774
+ import { Paperclip as Paperclip2, X as X4 } from "lucide-react";
4775
+ import { Fragment as Fragment7, jsx as jsx25, jsxs as jsxs23 } from "react/jsx-runtime";
4776
+ function AttachmentsFormSection({
4777
+ labels: text,
4778
+ attachments,
4779
+ pendingUploads,
4780
+ attachmentRejections,
4781
+ onAddFiles,
4782
+ onRetryUpload,
4783
+ onCancelUpload,
4784
+ onRemoveAttachment,
4785
+ onMoveAttachment,
4786
+ onDismissRejections
4787
+ }) {
4788
+ const attachmentFileInputRef = useRef10(null);
4789
+ return /* @__PURE__ */ jsxs23("div", { className: "space-y-2", children: [
4790
+ /* @__PURE__ */ jsx25("span", { className: "block text-sm font-medium", children: text.attachmentsTitle }),
4791
+ attachments.length === 0 && pendingUploads.length === 0 ? /* @__PURE__ */ jsx25("p", { className: "text-xs text-gray-500 dark:text-gray-400", children: text.attachmentsEmpty }) : /* @__PURE__ */ jsxs23("ul", { className: "space-y-1", children: [
4792
+ attachments.map((attachment, index) => /* @__PURE__ */ jsxs23(
4793
+ "li",
4794
+ {
4795
+ className: "flex items-center gap-2 rounded-md border border-gray-200 px-2 py-1.5 text-xs dark:border-gray-700",
4796
+ children: [
4797
+ /* @__PURE__ */ jsx25(Paperclip2, { "aria-hidden": "true", className: "h-3.5 w-3.5 flex-none text-gray-400" }),
4798
+ /* @__PURE__ */ jsx25("span", { className: "min-w-0 flex-1 truncate", children: attachment.filename }),
4799
+ /* @__PURE__ */ jsx25("span", { className: "flex-none text-gray-400", children: formatFileSize(attachment.sizeBytes) }),
4800
+ /* @__PURE__ */ jsx25(
4801
+ "button",
4802
+ {
4803
+ type: "button",
4804
+ disabled: index === 0,
4805
+ "aria-label": text.attachmentMoveUp,
4806
+ onClick: () => onMoveAttachment(index, -1),
4807
+ className: "flex-none disabled:opacity-30",
4808
+ children: "\u2191"
4809
+ }
4810
+ ),
4811
+ /* @__PURE__ */ jsx25(
4812
+ "button",
4813
+ {
4814
+ type: "button",
4815
+ disabled: index === attachments.length - 1,
4816
+ "aria-label": text.attachmentMoveDown,
4817
+ onClick: () => onMoveAttachment(index, 1),
4818
+ className: "flex-none disabled:opacity-30",
4819
+ children: "\u2193"
4820
+ }
4821
+ ),
4822
+ /* @__PURE__ */ jsx25(
4823
+ "button",
4824
+ {
4825
+ type: "button",
4826
+ "aria-label": text.attachmentRemove,
4827
+ onClick: () => onRemoveAttachment(attachment.uploadId),
4828
+ className: "flex-none text-red-600 dark:text-red-400",
4829
+ children: /* @__PURE__ */ jsx25(X4, { "aria-hidden": "true", className: "h-3.5 w-3.5" })
4830
+ }
4831
+ )
4832
+ ]
4833
+ },
4834
+ attachment.uploadId
4835
+ )),
4836
+ pendingUploads.map((pending) => /* @__PURE__ */ jsxs23(
4837
+ "li",
4838
+ {
4839
+ className: "flex items-center gap-2 rounded-md border border-gray-200 px-2 py-1.5 text-xs dark:border-gray-700",
4840
+ "aria-busy": pending.status === "uploading",
4841
+ "aria-live": "polite",
4842
+ children: [
4843
+ /* @__PURE__ */ jsx25(Paperclip2, { "aria-hidden": "true", className: "h-3.5 w-3.5 flex-none text-gray-400" }),
4844
+ /* @__PURE__ */ jsx25("span", { className: "min-w-0 flex-1 truncate", children: pending.file.name }),
4845
+ pending.status === "uploading" ? /* @__PURE__ */ jsx25("span", { className: "flex-none text-gray-500 dark:text-gray-400", children: text.attachmentUploading(Math.round(pending.progress * 100)) }) : /* @__PURE__ */ jsxs23(Fragment7, { children: [
4846
+ /* @__PURE__ */ jsx25("span", { role: "alert", className: "flex-none text-red-600 dark:text-red-400", children: pending.error }),
4847
+ /* @__PURE__ */ jsx25(
4848
+ "button",
4849
+ {
4850
+ type: "button",
4851
+ onClick: () => onRetryUpload(pending.localId),
4852
+ className: "flex-none font-medium text-blue-600 hover:underline dark:text-blue-400",
4853
+ children: text.attachmentRetry
4854
+ }
4855
+ )
4856
+ ] }),
4857
+ /* @__PURE__ */ jsx25(
4858
+ "button",
4859
+ {
4860
+ type: "button",
4861
+ "aria-label": text.attachmentCancel,
4862
+ onClick: () => onCancelUpload(pending.localId),
4863
+ className: "flex-none text-gray-400",
4864
+ children: /* @__PURE__ */ jsx25(X4, { "aria-hidden": "true", className: "h-3.5 w-3.5" })
4865
+ }
4866
+ )
4867
+ ]
4868
+ },
4869
+ pending.localId
4870
+ ))
4871
+ ] }),
4872
+ attachmentRejections.length > 0 ? /* @__PURE__ */ jsxs23("div", { role: "alert", className: "space-y-0.5 text-xs text-red-600 dark:text-red-400", children: [
4873
+ attachmentRejections.map((rejection, index) => /* @__PURE__ */ jsx25("p", { children: rejection.reason === "limit" ? text.attachmentLimitReached : text.attachmentTooLarge(rejection.file.name) }, index)),
4874
+ /* @__PURE__ */ jsx25("button", { type: "button", onClick: onDismissRejections, className: "hover:underline", children: text.cancel })
4875
+ ] }) : null,
4876
+ /* @__PURE__ */ jsx25(
4877
+ "input",
4878
+ {
4879
+ ref: attachmentFileInputRef,
4880
+ type: "file",
4881
+ multiple: true,
4882
+ hidden: true,
4883
+ onChange: (event) => {
4884
+ if (event.target.files) onAddFiles(event.target.files);
4885
+ event.target.value = "";
4886
+ }
4887
+ }
4888
+ ),
4889
+ /* @__PURE__ */ jsx25(
4890
+ "button",
4891
+ {
4892
+ type: "button",
4893
+ onClick: () => attachmentFileInputRef.current?.click(),
4894
+ className: "text-xs font-medium text-blue-600 hover:underline dark:text-blue-400",
4895
+ children: text.attachmentsAdd
4896
+ }
4897
+ )
4898
+ ] });
4899
+ }
4900
+
4901
+ // src/quickReplies/QuickRepliesWorkspace.tsx
4902
+ import { jsx as jsx26, jsxs as jsxs24 } from "react/jsx-runtime";
4903
+ var TABLE_SKELETON_ROWS = 3;
4904
+ function QuickRepliesWorkspace({
4905
+ api,
4906
+ variables,
4907
+ labels,
4908
+ className,
4909
+ attachmentSizeLimits
4910
+ }) {
4911
+ const text = { ...DEFAULT_QUICK_REPLIES_WORKSPACE_LABELS, ...labels };
4912
+ const bodyFieldRef = useRef11(null);
4913
+ const formId = useId2();
4914
+ const titleFieldId = `${formId}-title`;
4915
+ const shortcutFieldId = `${formId}-shortcut`;
4916
+ const bodyFieldId = `${formId}-body`;
4917
+ const [confirmingDeleteId, setConfirmingDeleteId] = useState18(null);
4918
+ const deleteButtonRefs = useRef11({});
4919
+ const confirmButtonRef = useRef11(null);
4920
+ useEffect13(() => {
4921
+ if (confirmingDeleteId) confirmButtonRef.current?.focus();
4922
+ }, [confirmingDeleteId]);
4923
+ const cancelDeleteConfirm = (id) => {
4924
+ setConfirmingDeleteId(null);
4925
+ deleteButtonRefs.current[id]?.focus();
4926
+ };
4927
+ const handleDeleteConfirmKeyDown = (id) => (event) => {
4928
+ if (event.key === "Escape") {
4929
+ event.preventDefault();
4930
+ cancelDeleteConfirm(id);
4931
+ }
4932
+ };
4933
+ const {
4934
+ quickReplies,
4935
+ filtered,
4936
+ isLoading,
4937
+ loadError,
4938
+ search,
4939
+ setSearch,
4940
+ readOnly,
4941
+ canEdit,
4942
+ canDelete,
4943
+ editing,
4944
+ startCreate,
4945
+ startEdit,
4946
+ cancelEdit,
4947
+ updateField,
4948
+ fieldErrors,
4949
+ isSaving,
4950
+ saveError,
4951
+ submit,
4952
+ remove,
4953
+ deletingId,
4954
+ hasAttachmentsCapability,
4955
+ pendingUploads,
4956
+ addAttachmentFiles,
4957
+ retryAttachmentUpload,
4958
+ cancelAttachmentUpload,
4959
+ removeAttachment,
4960
+ moveAttachmentAt,
4961
+ attachmentRejections,
4962
+ dismissAttachmentRejections
4963
+ } = useQuickRepliesWorkspace({ api, labels: text, ...attachmentSizeLimits ? { attachmentSizeLimits } : {} });
4964
+ const hasPendingUploads = pendingUploads.length > 0;
4965
+ const insertVariableAtCursor = (marker) => {
4966
+ const field = bodyFieldRef.current;
4967
+ if (!field || !editing) return;
4968
+ const { text: nextBody, caret } = insertAtCursor(editing.body, field.selectionStart, field.selectionEnd, marker);
4969
+ updateField("body", nextBody);
4970
+ requestAnimationFrame(() => {
4971
+ field.focus();
4972
+ field.setSelectionRange(caret, caret);
4973
+ });
4974
+ };
4975
+ return /* @__PURE__ */ jsxs24("div", { className: cn("space-y-4", className), children: [
4976
+ /* @__PURE__ */ jsxs24("header", { className: "space-y-0.5", children: [
4977
+ /* @__PURE__ */ jsx26("h2", { className: "text-lg font-semibold", children: text.title }),
4978
+ /* @__PURE__ */ jsx26("p", { className: "text-sm text-gray-500 dark:text-gray-400", children: text.subtitle(quickReplies.length) })
4979
+ ] }),
4980
+ readOnly ? /* @__PURE__ */ jsx26("p", { className: "text-sm text-gray-500 dark:text-gray-400", children: text.readOnlyNotice }) : null,
4981
+ /* @__PURE__ */ jsxs24("div", { className: "flex flex-wrap items-center gap-2", children: [
4982
+ /* @__PURE__ */ jsx26(
4983
+ "input",
4984
+ {
4985
+ type: "search",
4986
+ value: search,
4987
+ onChange: (event) => setSearch(event.target.value),
4988
+ placeholder: text.searchPlaceholder,
4989
+ "aria-label": text.searchPlaceholder,
4990
+ className: "w-full rounded-md border border-gray-300 px-3 py-2 text-sm dark:border-gray-700 dark:bg-gray-900 sm:w-64"
4991
+ }
4992
+ ),
4993
+ !readOnly ? /* @__PURE__ */ jsx26(
4994
+ "button",
4995
+ {
4996
+ type: "button",
4997
+ onClick: startCreate,
4998
+ className: "cv-header-action ml-auto inline-flex items-center gap-1",
4999
+ children: text.create
5000
+ }
5001
+ ) : null
5002
+ ] }),
5003
+ isLoading ? /* @__PURE__ */ jsx26("span", { className: "sr-only", "aria-live": "polite", children: text.loading }) : null,
5004
+ loadError ? /* @__PURE__ */ jsx26("p", { role: "alert", className: "text-sm text-red-600 dark:text-red-400", children: loadError || text.failure }) : null,
5005
+ editing ? /* @__PURE__ */ jsxs24(
5006
+ "form",
5007
+ {
5008
+ className: "space-y-3 rounded-lg border border-gray-200 p-4 dark:border-gray-700",
5009
+ onSubmit: (event) => {
5010
+ event.preventDefault();
5011
+ void submit();
5012
+ },
5013
+ children: [
5014
+ /* @__PURE__ */ jsxs24("div", { className: "space-y-1", children: [
5015
+ /* @__PURE__ */ jsx26("label", { className: "block text-sm font-medium", htmlFor: titleFieldId, children: text.fieldTitle }),
5016
+ /* @__PURE__ */ jsx26(
5017
+ "input",
5018
+ {
5019
+ id: titleFieldId,
5020
+ type: "text",
5021
+ maxLength: 40,
5022
+ value: editing.title,
5023
+ onChange: (event) => updateField("title", event.target.value),
5024
+ "aria-invalid": Boolean(fieldErrors.title),
5025
+ "aria-describedby": fieldErrors.title ? `${titleFieldId}-error` : void 0,
5026
+ className: "w-full rounded-md border border-gray-300 px-3 py-2 text-sm dark:border-gray-700 dark:bg-gray-900"
5027
+ }
5028
+ ),
5029
+ fieldErrors.title ? /* @__PURE__ */ jsx26("p", { id: `${titleFieldId}-error`, role: "alert", className: "text-xs text-red-600 dark:text-red-400", children: fieldErrors.title }) : null
5030
+ ] }),
5031
+ /* @__PURE__ */ jsxs24("div", { className: "space-y-1", children: [
5032
+ /* @__PURE__ */ jsx26("label", { className: "block text-sm font-medium", htmlFor: shortcutFieldId, children: text.fieldShortcut }),
5033
+ /* @__PURE__ */ jsx26(
5034
+ "input",
5035
+ {
5036
+ id: shortcutFieldId,
5037
+ type: "text",
5038
+ maxLength: 20,
5039
+ value: editing.shortcut,
5040
+ onChange: (event) => updateField("shortcut", event.target.value),
5041
+ "aria-invalid": Boolean(fieldErrors.shortcut),
5042
+ "aria-describedby": fieldErrors.shortcut ? `${shortcutFieldId}-error` : `${shortcutFieldId}-hint`,
5043
+ className: "w-full rounded-md border border-gray-300 px-3 py-2 text-sm font-mono dark:border-gray-700 dark:bg-gray-900"
5044
+ }
5045
+ ),
5046
+ fieldErrors.shortcut ? /* @__PURE__ */ jsx26("p", { id: `${shortcutFieldId}-error`, role: "alert", className: "text-xs text-red-600 dark:text-red-400", children: fieldErrors.shortcut }) : /* @__PURE__ */ jsx26("p", { id: `${shortcutFieldId}-hint`, className: "text-xs text-gray-400", children: text.fieldShortcutHint })
5047
+ ] }),
5048
+ /* @__PURE__ */ jsxs24("div", { className: "space-y-1", children: [
5049
+ /* @__PURE__ */ jsx26("label", { className: "block text-sm font-medium", htmlFor: bodyFieldId, children: text.fieldBody }),
5050
+ /* @__PURE__ */ jsx26(
5051
+ "textarea",
5052
+ {
5053
+ id: bodyFieldId,
5054
+ ref: bodyFieldRef,
5055
+ maxLength: 1e3,
5056
+ rows: 4,
5057
+ value: editing.body,
5058
+ onChange: (event) => updateField("body", event.target.value),
5059
+ "aria-invalid": Boolean(fieldErrors.body),
5060
+ "aria-describedby": fieldErrors.body ? `${bodyFieldId}-error` : void 0,
5061
+ className: "w-full resize-none rounded-md border border-gray-300 px-3 py-2 text-sm dark:border-gray-700 dark:bg-gray-900"
5062
+ }
5063
+ ),
5064
+ fieldErrors.body ? /* @__PURE__ */ jsx26("p", { id: `${bodyFieldId}-error`, role: "alert", className: "text-xs text-red-600 dark:text-red-400", children: fieldErrors.body }) : null,
5065
+ variables && variables.length > 0 ? /* @__PURE__ */ jsx26("div", { className: "flex flex-wrap gap-1 pt-1", children: variables.map((variable) => /* @__PURE__ */ jsx26(
5066
+ "button",
5067
+ {
5068
+ type: "button",
5069
+ onClick: () => insertVariableAtCursor(variable.marker),
5070
+ "aria-label": `${text.insertVariable}: ${variable.label}`,
5071
+ className: "rounded-full border border-gray-200 px-2 py-0.5 text-xs text-gray-600 hover:bg-gray-100 dark:border-gray-700 dark:text-gray-300 dark:hover:bg-gray-800",
5072
+ children: variable.label
5073
+ },
5074
+ variable.id
5075
+ )) }) : null
5076
+ ] }),
5077
+ hasAttachmentsCapability ? /* @__PURE__ */ jsx26(
5078
+ AttachmentsFormSection,
5079
+ {
5080
+ labels: text,
5081
+ attachments: editing.attachments,
5082
+ pendingUploads,
5083
+ attachmentRejections,
5084
+ onAddFiles: addAttachmentFiles,
5085
+ onRetryUpload: retryAttachmentUpload,
5086
+ onCancelUpload: cancelAttachmentUpload,
5087
+ onRemoveAttachment: removeAttachment,
5088
+ onMoveAttachment: moveAttachmentAt,
5089
+ onDismissRejections: dismissAttachmentRejections
5090
+ }
5091
+ ) : null,
5092
+ saveError ? /* @__PURE__ */ jsx26("p", { role: "alert", className: "text-sm text-red-600 dark:text-red-400", children: saveError }) : null,
5093
+ /* @__PURE__ */ jsxs24("div", { className: "flex items-center gap-2", children: [
5094
+ /* @__PURE__ */ jsx26(
5095
+ "button",
5096
+ {
5097
+ type: "submit",
5098
+ disabled: isSaving || hasPendingUploads,
5099
+ "aria-busy": isSaving,
5100
+ title: hasPendingUploads ? text.saveBlockedUploading : void 0,
5101
+ className: "cv-header-action inline-flex items-center gap-1 disabled:opacity-40",
5102
+ children: isSaving ? text.saving : hasPendingUploads ? text.saveBlockedUploading : text.save
5103
+ }
5104
+ ),
5105
+ /* @__PURE__ */ jsx26("button", { type: "button", onClick: cancelEdit, className: "text-sm text-gray-500 hover:underline", children: text.cancel })
5106
+ ] })
5107
+ ]
5108
+ }
5109
+ ) : null,
5110
+ /* @__PURE__ */ jsx26("div", { className: "cv-table-card", children: /* @__PURE__ */ jsxs24("table", { className: "cv-table", children: [
5111
+ /* @__PURE__ */ jsx26("thead", { children: /* @__PURE__ */ jsxs24("tr", { children: [
5112
+ /* @__PURE__ */ jsx26("th", { scope: "col", className: "px-3 py-2 text-left text-xs font-medium", children: text.columnTitle }),
5113
+ /* @__PURE__ */ jsx26("th", { scope: "col", className: "px-3 py-2 text-left text-xs font-medium", children: text.columnShortcut }),
5114
+ /* @__PURE__ */ jsx26("th", { scope: "col", className: "hidden px-3 py-2 text-left text-xs font-medium sm:table-cell", children: text.columnBody }),
5115
+ !readOnly ? /* @__PURE__ */ jsx26("th", { scope: "col", className: "px-3 py-2 text-left text-xs font-medium", children: text.columnActions }) : null
5116
+ ] }) }),
5117
+ /* @__PURE__ */ jsxs24("tbody", { children: [
5118
+ isLoading ? Array.from({ length: TABLE_SKELETON_ROWS }).map((_, index) => /* @__PURE__ */ jsxs24("tr", { "aria-hidden": "true", children: [
5119
+ /* @__PURE__ */ jsx26("td", { className: "px-3 py-3", children: /* @__PURE__ */ jsx26("span", { className: "cv-skeleton-line cv-skeleton-line--title block" }) }),
5120
+ /* @__PURE__ */ jsx26("td", { className: "px-3 py-3", children: /* @__PURE__ */ jsx26("span", { className: "cv-skeleton-line cv-skeleton-line--shortcut block" }) }),
5121
+ /* @__PURE__ */ jsx26("td", { className: "hidden px-3 py-3 sm:table-cell", children: /* @__PURE__ */ jsx26("span", { className: "cv-skeleton-line cv-skeleton-line--body block" }) }),
5122
+ !readOnly ? /* @__PURE__ */ jsx26("td", { className: "px-3 py-3" }) : null
5123
+ ] }, index)) : filtered.length === 0 ? /* @__PURE__ */ jsx26("tr", { children: /* @__PURE__ */ jsx26("td", { colSpan: 4, className: "px-3 py-4 text-center text-sm text-gray-500 dark:text-gray-400", children: search.trim() ? text.noResults : text.empty }) }) : null,
5124
+ !isLoading && filtered.map((quickReply) => /* @__PURE__ */ jsxs24("tr", { className: cn(deletingId === quickReply.id ? "cv-row-departing" : void 0), children: [
5125
+ /* @__PURE__ */ jsx26("td", { className: "px-3 py-2 font-medium", children: quickReply.title }),
5126
+ /* @__PURE__ */ jsxs24("td", { className: "px-3 py-2 font-mono text-xs text-gray-500", children: [
5127
+ "/",
5128
+ quickReply.shortcut
5129
+ ] }),
5130
+ /* @__PURE__ */ jsx26("td", { className: "hidden max-w-sm truncate px-3 py-2 text-gray-500 sm:table-cell", children: quickReply.body }),
5131
+ !readOnly ? /* @__PURE__ */ jsxs24("td", { className: "flex gap-2 px-3 py-2", children: [
5132
+ canEdit ? /* @__PURE__ */ jsx26(
5133
+ "button",
5134
+ {
5135
+ type: "button",
5136
+ onClick: () => startEdit(quickReply),
5137
+ className: "text-xs text-blue-600 hover:underline dark:text-blue-400",
5138
+ children: text.edit
5139
+ }
5140
+ ) : null,
5141
+ canDelete ? confirmingDeleteId === quickReply.id ? (
5142
+ // Linha de confirmação inline em vez de `window.confirm`: trava a aba
5143
+ // inteira e não segue os tokens visuais do pacote (`web.md` §14).
5144
+ /* @__PURE__ */ jsxs24(
5145
+ "span",
5146
+ {
5147
+ role: "group",
5148
+ "aria-label": text.removeConfirm(quickReply.title),
5149
+ onKeyDown: handleDeleteConfirmKeyDown(quickReply.id),
5150
+ className: "flex items-center gap-2 text-xs",
5151
+ children: [
5152
+ text.removeConfirm(quickReply.title),
5153
+ /* @__PURE__ */ jsx26(
5154
+ "button",
5155
+ {
5156
+ ref: confirmButtonRef,
5157
+ type: "button",
5158
+ onClick: () => {
5159
+ setConfirmingDeleteId(null);
5160
+ void remove(quickReply.id);
5161
+ },
5162
+ className: "font-medium text-red-600 hover:underline dark:text-red-400",
5163
+ children: text.remove
5164
+ }
5165
+ ),
5166
+ /* @__PURE__ */ jsx26(
5167
+ "button",
5168
+ {
5169
+ type: "button",
5170
+ onClick: () => cancelDeleteConfirm(quickReply.id),
5171
+ className: "text-gray-500 hover:underline dark:text-gray-400",
5172
+ children: text.cancel
5173
+ }
5174
+ )
5175
+ ]
5176
+ }
5177
+ )
5178
+ ) : /* @__PURE__ */ jsx26(
5179
+ "button",
5180
+ {
5181
+ ref: (node) => {
5182
+ deleteButtonRefs.current[quickReply.id] = node;
5183
+ },
5184
+ type: "button",
5185
+ disabled: deletingId === quickReply.id,
5186
+ "aria-busy": deletingId === quickReply.id,
5187
+ onClick: () => setConfirmingDeleteId(quickReply.id),
5188
+ className: "text-xs text-red-600 hover:underline disabled:opacity-50 dark:text-red-400",
5189
+ children: deletingId === quickReply.id ? text.deleting : text.remove
5190
+ }
5191
+ ) : null
5192
+ ] }) : null
5193
+ ] }, quickReply.id))
5194
+ ] })
5195
+ ] }) })
5196
+ ] });
5197
+ }
5198
+
5199
+ // src/workspace/ConversationsWorkspace.tsx
5200
+ import { useEffect as useEffect19, useMemo as useMemo6, useState as useState25 } from "react";
5201
+ import { Check as Check2, CheckCheck, FlaskConical, Hourglass as Hourglass2, Mail as Mail2, MessagesSquare } from "lucide-react";
5202
+
5203
+ // src/workspace/BulkTemplateModal.tsx
5204
+ import { useEffect as useEffect14, useMemo as useMemo4, useState as useState19 } from "react";
5205
+ import { jsx as jsx27, jsxs as jsxs25 } from "react/jsx-runtime";
5206
+ function BulkTemplateModal({
5207
+ labels,
5208
+ expiredCount,
5209
+ sending,
5210
+ onClose,
5211
+ onSend
5212
+ }) {
5213
+ const context = useConversations();
5214
+ if (!context) {
5215
+ throw new Error("BulkTemplateModal requires an ancestor <ConversationsProvider>");
5216
+ }
5217
+ const { api } = context;
5218
+ const [templates, setTemplates] = useState19([]);
5219
+ const [search, setSearch] = useState19("");
5220
+ const [selected, setSelected] = useState19("");
5221
+ useEffect14(() => {
5222
+ let active = true;
5223
+ void api.listTemplates?.().then((loaded) => {
5224
+ if (active) setTemplates(loaded);
5225
+ }).catch(() => {
5226
+ });
5227
+ return () => {
5228
+ active = false;
5229
+ };
5230
+ }, [api]);
4023
5231
  const filtered = useMemo4(() => {
4024
5232
  const term = search.trim().toLowerCase();
4025
5233
  if (!term) return templates;
4026
5234
  return templates.filter((template) => template.name.toLowerCase().includes(term));
4027
5235
  }, [templates, search]);
4028
- return /* @__PURE__ */ jsx25("div", { className: "cv-workspace-modal", role: "dialog", "aria-modal": "true", "aria-label": labels.templateModalTitle, children: /* @__PURE__ */ jsxs23("div", { className: "cv-workspace-modal__box", children: [
4029
- /* @__PURE__ */ jsxs23("header", { className: "cv-workspace-modal__header", children: [
4030
- /* @__PURE__ */ jsxs23("div", { children: [
4031
- /* @__PURE__ */ jsx25("h3", { children: labels.templateModalTitle }),
4032
- /* @__PURE__ */ jsx25("p", { children: labels.templateModalAvailable(templates.length) })
5236
+ return /* @__PURE__ */ jsx27("div", { className: "cv-workspace-modal", role: "dialog", "aria-modal": "true", "aria-label": labels.templateModalTitle, children: /* @__PURE__ */ jsxs25("div", { className: "cv-workspace-modal__box", children: [
5237
+ /* @__PURE__ */ jsxs25("header", { className: "cv-workspace-modal__header", children: [
5238
+ /* @__PURE__ */ jsxs25("div", { children: [
5239
+ /* @__PURE__ */ jsx27("h3", { children: labels.templateModalTitle }),
5240
+ /* @__PURE__ */ jsx27("p", { children: labels.templateModalAvailable(templates.length) })
4033
5241
  ] }),
4034
- /* @__PURE__ */ jsx25("button", { "data-cv-tooltip": labels.templateModalCancel, type: "button", onClick: onClose, "aria-label": labels.templateModalCancel, children: "\u2715" })
5242
+ /* @__PURE__ */ jsx27("button", { "data-cv-tooltip": labels.templateModalCancel, type: "button", onClick: onClose, "aria-label": labels.templateModalCancel, children: "\u2715" })
4035
5243
  ] }),
4036
- /* @__PURE__ */ jsx25("div", { className: "cv-workspace-modal__search", children: /* @__PURE__ */ jsx25(
5244
+ /* @__PURE__ */ jsx27("div", { className: "cv-workspace-modal__search", children: /* @__PURE__ */ jsx27(
4037
5245
  "input",
4038
5246
  {
4039
5247
  type: "search",
@@ -4043,9 +5251,9 @@ function BulkTemplateModal({
4043
5251
  autoFocus: true
4044
5252
  }
4045
5253
  ) }),
4046
- /* @__PURE__ */ jsxs23("div", { className: "cv-workspace-modal__list", children: [
4047
- filtered.length === 0 ? /* @__PURE__ */ jsx25("p", { className: "cv-workspace-empty", children: labels.templateModalEmpty }) : null,
4048
- filtered.map((template) => /* @__PURE__ */ jsxs23(
5254
+ /* @__PURE__ */ jsxs25("div", { className: "cv-workspace-modal__list", children: [
5255
+ filtered.length === 0 ? /* @__PURE__ */ jsx27("p", { className: "cv-workspace-empty", children: labels.templateModalEmpty }) : null,
5256
+ filtered.map((template) => /* @__PURE__ */ jsxs25(
4049
5257
  "button",
4050
5258
  {
4051
5259
  "data-cv-tooltip": template.name,
@@ -4055,8 +5263,8 @@ function BulkTemplateModal({
4055
5263
  "aria-pressed": template.name === selected,
4056
5264
  className: `cv-workspace-modal__item${template.name === selected ? " cv-workspace-modal__item--on" : ""}`,
4057
5265
  children: [
4058
- /* @__PURE__ */ jsx25("span", { className: "cv-workspace-modal__item-name", children: template.name }),
4059
- /* @__PURE__ */ jsxs23("span", { className: "cv-workspace-modal__item-meta", children: [
5266
+ /* @__PURE__ */ jsx27("span", { className: "cv-workspace-modal__item-name", children: template.name }),
5267
+ /* @__PURE__ */ jsxs25("span", { className: "cv-workspace-modal__item-meta", children: [
4060
5268
  template.language,
4061
5269
  template.category ? ` \xB7 ${template.category.toLowerCase()}` : ""
4062
5270
  ] })
@@ -4065,10 +5273,10 @@ function BulkTemplateModal({
4065
5273
  template.name
4066
5274
  ))
4067
5275
  ] }),
4068
- expiredCount === 0 ? /* @__PURE__ */ jsx25("p", { className: "cv-workspace-modal__warning", children: labels.templateModalNoneExpired }) : null,
4069
- /* @__PURE__ */ jsxs23("footer", { className: "cv-workspace-modal__footer", children: [
4070
- /* @__PURE__ */ jsx25("button", { "data-cv-tooltip": labels.templateModalCancel, "aria-label": labels.templateModalCancel, type: "button", onClick: onClose, children: labels.templateModalCancel }),
4071
- /* @__PURE__ */ jsx25(
5276
+ expiredCount === 0 ? /* @__PURE__ */ jsx27("p", { className: "cv-workspace-modal__warning", children: labels.templateModalNoneExpired }) : null,
5277
+ /* @__PURE__ */ jsxs25("footer", { className: "cv-workspace-modal__footer", children: [
5278
+ /* @__PURE__ */ jsx27("button", { "data-cv-tooltip": labels.templateModalCancel, "aria-label": labels.templateModalCancel, type: "button", onClick: onClose, children: labels.templateModalCancel }),
5279
+ /* @__PURE__ */ jsx27(
4072
5280
  "button",
4073
5281
  {
4074
5282
  "data-cv-tooltip": sending ? labels.templateModalSending : labels.templateModalSend(expiredCount),
@@ -4084,8 +5292,402 @@ function BulkTemplateModal({
4084
5292
  }
4085
5293
 
4086
5294
  // src/workspace/ConversationPane.tsx
4087
- import { useEffect as useEffect12, useRef as useRef9, useState as useState17 } from "react";
4088
- import { jsx as jsx26, jsxs as jsxs24 } from "react/jsx-runtime";
5295
+ import { useCallback as useCallback13, useEffect as useEffect17, useRef as useRef15, useState as useState23 } from "react";
5296
+
5297
+ // src/quickReplies/resolveConversationVariables.ts
5298
+ var MARKER_PATTERN = /^\{\{\s*([\w.]+)\s*\}\}$/;
5299
+ function variableNameOf(variable) {
5300
+ return MARKER_PATTERN.exec(variable.marker)?.[1] ?? variable.id;
5301
+ }
5302
+ function resolveConversationVariables({
5303
+ conversationVariables,
5304
+ quickReplyVariables,
5305
+ composerVariables
5306
+ }) {
5307
+ if (!conversationVariables) return { quickReplyVariables, composerVariables };
5308
+ const known = conversationVariables.filter((variable) => variable.value !== "");
5309
+ return {
5310
+ quickReplyVariables: Object.fromEntries(known.map((variable) => [variableNameOf(variable), variable.value])),
5311
+ composerVariables: known.map(({ id, label, value }) => ({ id, label, value }))
5312
+ };
5313
+ }
5314
+
5315
+ // src/workspace/QueuedAttachmentsList.tsx
5316
+ import { useEffect as useEffect15, useRef as useRef12, useState as useState20 } from "react";
5317
+ import { jsx as jsx28, jsxs as jsxs26 } from "react/jsx-runtime";
5318
+ function nameOf(item) {
5319
+ return item.kind === "local" ? item.file.name : item.filename;
5320
+ }
5321
+ function mimeTypeOf(item) {
5322
+ return item.kind === "local" ? item.file.type : item.mimeType;
5323
+ }
5324
+ function sizeOf(item) {
5325
+ return item.kind === "local" ? item.file.size : item.sizeBytes;
5326
+ }
5327
+ function AttachmentThumbnail({
5328
+ item,
5329
+ getThumbnailUrl
5330
+ }) {
5331
+ const isImage = mimeTypeOf(item).startsWith("image/");
5332
+ const [url, setUrl] = useState20(item.kind === "local" ? void 0 : item.previewUrl);
5333
+ const localFile = item.kind === "local" ? item.file : void 0;
5334
+ const uploadId = item.kind === "stored" ? item.uploadId : void 0;
5335
+ useEffect15(() => {
5336
+ if (!isImage || !localFile) return;
5337
+ const objectUrl = URL.createObjectURL(localFile);
5338
+ setUrl(objectUrl);
5339
+ return () => URL.revokeObjectURL(objectUrl);
5340
+ }, [isImage, localFile]);
5341
+ useEffect15(() => {
5342
+ if (!isImage || !uploadId || !getThumbnailUrl) return;
5343
+ let cancelled = false;
5344
+ getThumbnailUrl(uploadId).then((resolved) => {
5345
+ if (!cancelled) setUrl(resolved);
5346
+ }).catch(() => void 0);
5347
+ return () => {
5348
+ cancelled = true;
5349
+ };
5350
+ }, [isImage, uploadId, getThumbnailUrl]);
5351
+ if (!isImage) return null;
5352
+ return /* @__PURE__ */ jsx28("span", { className: "cv-attachment-item__thumbnail", "aria-hidden": "true", children: url ? /* @__PURE__ */ jsx28("img", { src: url, alt: "" }) : null });
5353
+ }
5354
+ function departedItemsOf(previouslyRendered, items) {
5355
+ const currentKeys = new Set(items.map(attachmentKey));
5356
+ return previouslyRendered.filter((item) => !currentKeys.has(attachmentKey(item)));
5357
+ }
5358
+ function useDepartingItems(items) {
5359
+ const [rendered, setRendered] = useState20(items);
5360
+ const [departingKeys, setDepartingKeys] = useState20(/* @__PURE__ */ new Set());
5361
+ const renderedRef = useRef12(rendered);
5362
+ renderedRef.current = rendered;
5363
+ useEffect15(() => {
5364
+ const removedItems = departedItemsOf(renderedRef.current, items);
5365
+ if (removedItems.length === 0) {
5366
+ setRendered(items);
5367
+ return;
5368
+ }
5369
+ setDepartingKeys(new Set(removedItems.map(attachmentKey)));
5370
+ const reduceMotion = typeof window !== "undefined" && window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
5371
+ const delay = reduceMotion ? 0 : 220;
5372
+ const timer = setTimeout(() => {
5373
+ setRendered(items);
5374
+ setDepartingKeys(/* @__PURE__ */ new Set());
5375
+ }, delay);
5376
+ return () => clearTimeout(timer);
5377
+ }, [items]);
5378
+ return { rendered: departingKeys.size > 0 ? [...rendered] : items, departingKeys };
5379
+ }
5380
+ function QueuedAttachmentsList({
5381
+ items,
5382
+ statusOf: statusOf2,
5383
+ onRemove,
5384
+ onRetry,
5385
+ retryingKeys,
5386
+ getThumbnailUrl,
5387
+ labels,
5388
+ busy
5389
+ }) {
5390
+ const { rendered, departingKeys } = useDepartingItems(items);
5391
+ if (rendered.length === 0) return null;
5392
+ const statusLabelOf = (status) => {
5393
+ if (status === "sending") return labels.sending;
5394
+ if (status === "sent") return labels.sent;
5395
+ if (status === "failed") return labels.failed;
5396
+ if (status === "skipped") return labels.skipped;
5397
+ return labels.waiting;
5398
+ };
5399
+ return /* @__PURE__ */ jsx28("ul", { className: "cv-workspace-attachments", "aria-busy": busy, "aria-live": "polite", children: rendered.map((item) => {
5400
+ const key = attachmentKey(item);
5401
+ const status = statusOf2(key);
5402
+ const isDeparting = departingKeys.has(key);
5403
+ return /* @__PURE__ */ jsxs26(
5404
+ "li",
5405
+ {
5406
+ className: `cv-attachment-item cv-attachment-item--${status}${isDeparting ? " cv-attachment-item--departing" : ""}`,
5407
+ children: [
5408
+ /* @__PURE__ */ jsx28(AttachmentThumbnail, { item, getThumbnailUrl }),
5409
+ /* @__PURE__ */ jsxs26("span", { className: "cv-attachment-item__info", children: [
5410
+ /* @__PURE__ */ jsx28("span", { className: "cv-attachment-item__name", children: nameOf(item) }),
5411
+ /* @__PURE__ */ jsxs26("span", { className: "cv-attachment-item__meta", children: [
5412
+ formatFileSize(sizeOf(item)),
5413
+ " \xB7 ",
5414
+ statusLabelOf(status)
5415
+ ] })
5416
+ ] }),
5417
+ (status === "failed" || status === "skipped") && onRetry ? /* @__PURE__ */ jsx28(
5418
+ "button",
5419
+ {
5420
+ type: "button",
5421
+ className: "cv-attachment-item__retry",
5422
+ disabled: busy || (retryingKeys?.has(key) ?? false),
5423
+ "aria-busy": retryingKeys?.has(key) ?? false,
5424
+ onClick: () => onRetry(item),
5425
+ children: labels.retry
5426
+ }
5427
+ ) : null,
5428
+ /* @__PURE__ */ jsx28(
5429
+ "button",
5430
+ {
5431
+ "data-cv-tooltip": labels.remove,
5432
+ type: "button",
5433
+ "aria-label": labels.remove,
5434
+ disabled: status === "sending",
5435
+ onClick: () => onRemove(item),
5436
+ children: "\u2715"
5437
+ }
5438
+ )
5439
+ ]
5440
+ },
5441
+ key
5442
+ );
5443
+ }) });
5444
+ }
5445
+
5446
+ // src/workspace/useComposerQueue.ts
5447
+ import { useCallback as useCallback12, useEffect as useEffect16, useRef as useRef14, useState as useState22 } from "react";
5448
+
5449
+ // src/workspace/useComposerAttachmentRetry.ts
5450
+ import { useCallback as useCallback11, useRef as useRef13, useState as useState21 } from "react";
5451
+ function useComposerAttachmentRetry(params) {
5452
+ const {
5453
+ conversationId,
5454
+ currentConversationIdRef,
5455
+ queue,
5456
+ setQueue,
5457
+ setAttachmentStatus,
5458
+ setSendFailure,
5459
+ api,
5460
+ onSendAttachments,
5461
+ labels,
5462
+ sendInFlightRef
5463
+ } = params;
5464
+ const retryIdempotencyKeyRef = useRef13(void 0);
5465
+ const retryingKeysRef = useRef13(/* @__PURE__ */ new Set());
5466
+ const [retryingKeys, setRetryingKeys] = useState21(/* @__PURE__ */ new Set());
5467
+ function markRetrying(key) {
5468
+ retryingKeysRef.current.add(key);
5469
+ setRetryingKeys(new Set(retryingKeysRef.current));
5470
+ }
5471
+ function unmarkRetrying(key) {
5472
+ retryingKeysRef.current.delete(key);
5473
+ setRetryingKeys(new Set(retryingKeysRef.current));
5474
+ }
5475
+ async function retryLocalAttachment(item) {
5476
+ if (!onSendAttachments) return;
5477
+ const key = attachmentKey(item);
5478
+ const conversationIdAtRetry = conversationId;
5479
+ const isSameConversation = () => currentConversationIdRef.current === conversationIdAtRetry;
5480
+ setAttachmentStatus((current) => ({ ...current, [key]: "sending" }));
5481
+ try {
5482
+ await onSendAttachments([item.file], "");
5483
+ if (!isSameConversation()) return;
5484
+ setAttachmentStatus((current) => ({ ...current, [key]: "sent" }));
5485
+ setQueue((current) => current.filter((queued) => attachmentKey(queued) !== key));
5486
+ } catch (error) {
5487
+ if (!isSameConversation()) return;
5488
+ setAttachmentStatus((current) => ({ ...current, [key]: "failed" }));
5489
+ setSendFailure(error instanceof Error ? error.message : labels.attachFailure);
5490
+ }
5491
+ }
5492
+ async function retryStoredAttachment(item) {
5493
+ const sendStoredAttachmentsApi = api.sendStoredAttachments;
5494
+ if (!sendStoredAttachmentsApi) return;
5495
+ const conversationIdAtRetry = conversationId;
5496
+ const isSameConversation = () => currentConversationIdRef.current === conversationIdAtRetry;
5497
+ const uploadIds = [item.uploadId];
5498
+ const idempotencyState = resolveIdempotencyKey(retryIdempotencyKeyRef.current, uploadIds);
5499
+ retryIdempotencyKeyRef.current = idempotencyState;
5500
+ try {
5501
+ const result = await retryStoredAttachments({
5502
+ queue,
5503
+ uploadIds,
5504
+ idempotencyKey: idempotencyState.key,
5505
+ sendStoredAttachments: (uploadParams) => sendStoredAttachmentsApi({ conversationId: conversationIdAtRetry, ...uploadParams }),
5506
+ onAttachmentStatus: (statusKey, status) => {
5507
+ if (isSameConversation()) setAttachmentStatus((current) => ({ ...current, [statusKey]: status }));
5508
+ }
5509
+ });
5510
+ if (shouldResetRetryKey({
5511
+ current: retryIdempotencyKeyRef.current,
5512
+ attempted: idempotencyState,
5513
+ sentAttachmentKeys: result.sentAttachmentKeys,
5514
+ uploadId: item.uploadId
5515
+ })) {
5516
+ retryIdempotencyKeyRef.current = void 0;
5517
+ }
5518
+ setQueue(
5519
+ (current) => resolveRetryOutcome({
5520
+ conversationIdAtRetry,
5521
+ currentConversationId: currentConversationIdRef.current,
5522
+ sentAttachmentKeys: result.sentAttachmentKeys,
5523
+ queue: current
5524
+ }) ?? current
5525
+ );
5526
+ } catch (error) {
5527
+ if (isSameConversation()) setSendFailure(error instanceof Error ? error.message : labels.attachFailure);
5528
+ }
5529
+ }
5530
+ function retryQueuedAttachment(item) {
5531
+ if (sendInFlightRef.current) return;
5532
+ const key = attachmentKey(item);
5533
+ if (retryingKeysRef.current.has(key)) return;
5534
+ markRetrying(key);
5535
+ const release = () => unmarkRetrying(key);
5536
+ if (item.kind === "local") {
5537
+ void retryLocalAttachment(item).finally(release);
5538
+ return;
5539
+ }
5540
+ void retryStoredAttachment(item).finally(release);
5541
+ }
5542
+ const resetRetryState = useCallback11(() => {
5543
+ retryIdempotencyKeyRef.current = void 0;
5544
+ retryingKeysRef.current.clear();
5545
+ setRetryingKeys(/* @__PURE__ */ new Set());
5546
+ }, []);
5547
+ return { retryQueuedAttachment, resetRetryState, retryingKeys };
5548
+ }
5549
+
5550
+ // src/workspace/useComposerQueue.ts
5551
+ function useComposerQueue(params) {
5552
+ const { conversationId, draft, setDraft, api, labels, onSendAttachments, refetch, setSendFailure } = params;
5553
+ const [queue, setQueue] = useState22([]);
5554
+ const [attachmentStatus, setAttachmentStatus] = useState22({});
5555
+ const [isSendingDraft, setIsSendingDraft] = useState22(false);
5556
+ const sendInFlightRef = useRef14(false);
5557
+ const idempotencyKeyRef = useRef14(void 0);
5558
+ const currentConversationIdRef = useRef14(conversationId);
5559
+ const { retryQueuedAttachment, resetRetryState, retryingKeys } = useComposerAttachmentRetry({
5560
+ conversationId,
5561
+ currentConversationIdRef,
5562
+ queue,
5563
+ setQueue,
5564
+ setAttachmentStatus,
5565
+ setSendFailure,
5566
+ api,
5567
+ labels,
5568
+ sendInFlightRef,
5569
+ ...onSendAttachments ? { onSendAttachments } : {}
5570
+ });
5571
+ useEffect16(() => {
5572
+ currentConversationIdRef.current = conversationId;
5573
+ setQueue([]);
5574
+ setAttachmentStatus({});
5575
+ idempotencyKeyRef.current = void 0;
5576
+ sendInFlightRef.current = false;
5577
+ setIsSendingDraft(false);
5578
+ resetRetryState();
5579
+ }, [conversationId, resetRetryState]);
5580
+ const enqueueAttachments = useCallback12((items) => {
5581
+ if (items.length === 0) return;
5582
+ setQueue((current) => [...current, ...items]);
5583
+ }, []);
5584
+ const removeQueuedAttachment = useCallback12((item) => {
5585
+ setQueue((current) => current.filter((queued) => attachmentKey(queued) !== attachmentKey(item)));
5586
+ }, []);
5587
+ async function runSend(action, fallback) {
5588
+ setSendFailure(void 0);
5589
+ try {
5590
+ await action();
5591
+ await refetch();
5592
+ return true;
5593
+ } catch (error) {
5594
+ setSendFailure(error instanceof Error ? error.message : fallback);
5595
+ return false;
5596
+ }
5597
+ }
5598
+ async function sendLocalOnlyAttachments(isSameConversation) {
5599
+ if (!onSendAttachments) return;
5600
+ const files = excludeRetryingItems(queue, retryingKeys).filter((item) => item.kind === "local").map((item) => item.file);
5601
+ try {
5602
+ const didSend = await runSend(() => onSendAttachments(files, draft), labels.attachFailure);
5603
+ if (!isSameConversation()) return;
5604
+ if (didSend) {
5605
+ setQueue((current) => current.filter((item) => retryingKeys.has(attachmentKey(item))));
5606
+ setAttachmentStatus(
5607
+ (current) => Object.fromEntries(Object.entries(current).filter(([key]) => retryingKeys.has(key)))
5608
+ );
5609
+ idempotencyKeyRef.current = void 0;
5610
+ setDraft("");
5611
+ }
5612
+ } finally {
5613
+ if (isSameConversation()) {
5614
+ sendInFlightRef.current = false;
5615
+ setIsSendingDraft(false);
5616
+ }
5617
+ }
5618
+ }
5619
+ async function sendQueuedDraft(isSameConversation) {
5620
+ const sendStoredAttachmentsApi = api.sendStoredAttachments;
5621
+ const sendableQueue = excludeRetryingItems(queue, retryingKeys);
5622
+ const storedUploadIds = sendableQueue.filter((item) => item.kind === "stored").map((item) => item.uploadId);
5623
+ const idempotencyState = resolveIdempotencyKey(idempotencyKeyRef.current, storedUploadIds);
5624
+ idempotencyKeyRef.current = idempotencyState;
5625
+ try {
5626
+ const result = await sendQueuedMessage({
5627
+ text: draft,
5628
+ queue: sendableQueue,
5629
+ idempotencyKey: idempotencyState.key,
5630
+ sendText: (text) => runSend(() => api.sendMessage(conversationId, text), labels.sendFailure),
5631
+ ...sendStoredAttachmentsApi ? {
5632
+ sendStoredAttachments: (uploadParams) => sendStoredAttachmentsApi({ conversationId, ...uploadParams })
5633
+ } : {},
5634
+ ...onSendAttachments ? { sendLocalAttachments: (files) => onSendAttachments(files, "") } : {},
5635
+ onAttachmentStatus: (key, status) => {
5636
+ if (isSameConversation()) setAttachmentStatus((current) => ({ ...current, [key]: status }));
5637
+ }
5638
+ });
5639
+ if (!isSameConversation()) return;
5640
+ if (!result.textSent) return;
5641
+ const sentKeys = new Set(result.sentAttachmentKeys);
5642
+ setQueue((current) => current.filter((item) => !sentKeys.has(attachmentKey(item))));
5643
+ setAttachmentStatus((current) => {
5644
+ const next = { ...current };
5645
+ for (const key of sentKeys) delete next[key];
5646
+ return next;
5647
+ });
5648
+ if (idempotencyKeyRef.current === idempotencyState && hasSentEveryStoredUpload(idempotencyState.uploadIds, result.sentAttachmentKeys)) {
5649
+ idempotencyKeyRef.current = void 0;
5650
+ }
5651
+ if (draft.trim()) setDraft("");
5652
+ await refetch();
5653
+ } catch (error) {
5654
+ if (isSameConversation()) setSendFailure(error instanceof Error ? error.message : labels.attachFailure);
5655
+ } finally {
5656
+ if (isSameConversation()) {
5657
+ sendInFlightRef.current = false;
5658
+ setIsSendingDraft(false);
5659
+ }
5660
+ }
5661
+ }
5662
+ async function handleRichSend() {
5663
+ if (sendInFlightRef.current) return;
5664
+ if (!draft.trim() && queue.length === 0) return;
5665
+ sendInFlightRef.current = true;
5666
+ setIsSendingDraft(true);
5667
+ setSendFailure(void 0);
5668
+ const conversationIdAtSend = conversationId;
5669
+ const isSameConversation = () => currentConversationIdRef.current === conversationIdAtSend;
5670
+ const hasStoredItems = queue.some((item) => item.kind === "stored");
5671
+ if (onSendAttachments && queue.length > 0 && !hasStoredItems) {
5672
+ await sendLocalOnlyAttachments(isSameConversation);
5673
+ return;
5674
+ }
5675
+ await sendQueuedDraft(isSameConversation);
5676
+ }
5677
+ return {
5678
+ queue,
5679
+ enqueueAttachments,
5680
+ attachmentStatus,
5681
+ isSendingDraft,
5682
+ handleRichSend,
5683
+ removeQueuedAttachment,
5684
+ retryQueuedAttachment,
5685
+ retryingKeys
5686
+ };
5687
+ }
5688
+
5689
+ // src/workspace/ConversationPane.tsx
5690
+ import { jsx as jsx29, jsxs as jsxs27 } from "react/jsx-runtime";
4089
5691
  function ConversationPane({
4090
5692
  conversation,
4091
5693
  now,
@@ -4108,6 +5710,7 @@ function ConversationPane({
4108
5710
  onAttach,
4109
5711
  composer = "simple",
4110
5712
  composerVariablesFor,
5713
+ conversationVariablesFor,
4111
5714
  onSendAttachments,
4112
5715
  onRecordAudio
4113
5716
  }) {
@@ -4118,17 +5721,44 @@ function ConversationPane({
4118
5721
  const { api } = context;
4119
5722
  const { messages, refetch } = useConversationMessages(conversation.id);
4120
5723
  const { context: conversationContext } = useConversationContext(conversation.id);
4121
- const [documentsOpen, setDocumentsOpen] = useState17(false);
4122
- const [sendFailure, setSendFailure] = useState17(void 0);
4123
- const [selectedMessageIds, setSelectedMessageIds] = useState17(/* @__PURE__ */ new Set());
4124
- const [draft, setDraft] = useState17(initialComposerText ?? "");
4125
- const [queuedFiles, setQueuedFiles] = useState17([]);
4126
- const [isSendingDraft, setIsSendingDraft] = useState17(false);
4127
- const sendInFlightRef = useRef9(false);
4128
- useEffect12(() => {
5724
+ const [documentsOpen, setDocumentsOpen] = useState23(false);
5725
+ const [sendFailure, setSendFailure] = useState23(void 0);
5726
+ const [selectedMessageIds, setSelectedMessageIds] = useState23(/* @__PURE__ */ new Set());
5727
+ const [draft, setDraft] = useState23(initialComposerText ?? "");
5728
+ const {
5729
+ queue,
5730
+ enqueueAttachments,
5731
+ attachmentStatus,
5732
+ isSendingDraft,
5733
+ handleRichSend,
5734
+ removeQueuedAttachment,
5735
+ retryQueuedAttachment,
5736
+ retryingKeys
5737
+ } = useComposerQueue({
5738
+ conversationId: conversation.id,
5739
+ draft,
5740
+ setDraft,
5741
+ api,
5742
+ labels: { sendFailure: labels.sendFailure, attachFailure: labels.attachFailure },
5743
+ ...onSendAttachments ? { onSendAttachments } : {},
5744
+ refetch,
5745
+ setSendFailure
5746
+ });
5747
+ const thumbnailUrlCacheRef = useRef15(/* @__PURE__ */ new Map());
5748
+ const getQueuedAttachmentThumbnailUrl = useCallback13(
5749
+ (uploadId) => {
5750
+ const cached = thumbnailUrlCacheRef.current.get(uploadId);
5751
+ if (cached) return cached;
5752
+ const pending = api.getDocumentUrl(uploadId, "inline");
5753
+ thumbnailUrlCacheRef.current.set(uploadId, pending);
5754
+ pending.catch(() => thumbnailUrlCacheRef.current.delete(uploadId));
5755
+ return pending;
5756
+ },
5757
+ [api]
5758
+ );
5759
+ useEffect17(() => {
4129
5760
  setSelectedMessageIds(/* @__PURE__ */ new Set());
4130
5761
  setDraft(initialComposerText ?? "");
4131
- setQueuedFiles([]);
4132
5762
  }, [conversation.id, initialComposerText]);
4133
5763
  function toggleMessageSelected(messageId) {
4134
5764
  setSelectedMessageIds((current) => {
@@ -4170,28 +5800,6 @@ function ConversationPane({
4170
5800
  if (conversation.mode !== "human" && onTakeover) await onTakeover();
4171
5801
  await runSend(() => api.sendTemplate(conversation.id, {}), labels.sendFailure);
4172
5802
  }
4173
- async function handleRichSend() {
4174
- if (sendInFlightRef.current) return;
4175
- sendInFlightRef.current = true;
4176
- setIsSendingDraft(true);
4177
- try {
4178
- if (onSendAttachments && queuedFiles.length > 0) {
4179
- const files = queuedFiles;
4180
- const caption = draft;
4181
- const didSend = await runSend(() => onSendAttachments(files, caption), labels.attachFailure);
4182
- if (didSend) {
4183
- setQueuedFiles([]);
4184
- setDraft("");
4185
- }
4186
- return;
4187
- }
4188
- if (!draft.trim()) return;
4189
- if (await handleSend(draft)) setDraft("");
4190
- } finally {
4191
- sendInFlightRef.current = false;
4192
- setIsSendingDraft(false);
4193
- }
4194
- }
4195
5803
  async function handleAttach(file) {
4196
5804
  if (!onAttach) return;
4197
5805
  await runSend(() => onAttach(file), labels.attachFailure);
@@ -4207,15 +5815,33 @@ function ConversationPane({
4207
5815
  );
4208
5816
  }
4209
5817
  const contextEntries = contextEntriesOf?.(conversationContext);
4210
- const composerVariables = composerVariablesFor?.(conversation, conversationContext);
5818
+ const { quickReplyVariables, composerVariables } = resolveConversationVariables({
5819
+ conversationVariables: conversationVariablesFor?.(conversation, conversationContext),
5820
+ quickReplyVariables: quickReplyVariablesFor?.(conversation, conversationContext),
5821
+ composerVariables: composerVariablesFor?.(conversation, conversationContext)
5822
+ });
4211
5823
  const richQuickReplies = quickReplies?.map((reply) => ({
4212
5824
  id: reply.key,
4213
5825
  label: reply.label,
4214
- text: typeof reply.text === "string" ? applyQuickReplyVariables(reply.text, quickReplyVariablesFor?.(conversation, conversationContext) ?? {}) : reply.text(quickReplyVariablesFor?.(conversation, conversationContext) ?? {})
5826
+ text: typeof reply.text === "string" ? applyQuickReplyVariables(reply.text, quickReplyVariables ?? {}) : reply.text(quickReplyVariables ?? {})
4215
5827
  }));
5828
+ const savedQuickReplies = api.listQuickReplies ? {
5829
+ // Arrow em vez de repassar o método direto: `api.listQuickReplies` solto perde o `this` do
5830
+ // objeto que o implementa, e um cliente HTTP real costuma depender dele internamente.
5831
+ listQuickReplies: (params) => api.listQuickReplies(params),
5832
+ conversationId: conversation.id,
5833
+ variables: quickReplyVariables,
5834
+ hasAttachmentsCapability: Boolean(api.sendStoredAttachments),
5835
+ // Empurra os anexos da mensagem escolhida como itens guardados (QR-32) — sem a porta, a
5836
+ // linha do picker já avisou e o texto entra sozinho, sem silenciosamente perder o anexo.
5837
+ onSelect: (quickReply) => {
5838
+ const attachments = queuedAttachmentsFromQuickReply(quickReply, Boolean(api.sendStoredAttachments));
5839
+ enqueueAttachments(attachments);
5840
+ }
5841
+ } : void 0;
4216
5842
  const botOwnsConversation = Boolean(requireTakeoverToReply) && conversation.mode !== "human";
4217
- return /* @__PURE__ */ jsxs24("div", { className: "cv-workspace-pane", children: [
4218
- /* @__PURE__ */ jsx26(
5843
+ return /* @__PURE__ */ jsxs27("div", { className: "cv-workspace-pane", children: [
5844
+ /* @__PURE__ */ jsx29(
4219
5845
  ConversationHeader,
4220
5846
  {
4221
5847
  conversation,
@@ -4230,10 +5856,10 @@ function ConversationPane({
4230
5856
  ...extraUtilities ? { extraUtilities } : {}
4231
5857
  }
4232
5858
  ),
4233
- contextEntries && contextEntries.length > 0 || flowLabel ? /* @__PURE__ */ jsx26(ConversationContextPanel, { entries: contextEntries ?? [], ...flowLabel ? { flowLabel } : {} }) : null,
4234
- /* @__PURE__ */ jsx26(ConversationDocumentsPanel, { conversationId: conversation.id, open: documentsOpen }),
5859
+ contextEntries && contextEntries.length > 0 || flowLabel ? /* @__PURE__ */ jsx29(ConversationContextPanel, { entries: contextEntries ?? [], ...flowLabel ? { flowLabel } : {} }) : null,
5860
+ /* @__PURE__ */ jsx29(ConversationDocumentsPanel, { conversationId: conversation.id, open: documentsOpen }),
4235
5861
  renderAboveTranscript?.(conversation, conversationContext),
4236
- /* @__PURE__ */ jsx26(
5862
+ /* @__PURE__ */ jsx29(
4237
5863
  ConversationWallpaper,
4238
5864
  {
4239
5865
  ref: scroll.containerRef,
@@ -4242,9 +5868,9 @@ function ConversationPane({
4242
5868
  children: messages.map((message, index) => {
4243
5869
  const previous = index > 0 ? messages[index - 1] : void 0;
4244
5870
  const startsNewDay = !previous || new Date(message.timestamp).toDateString() !== new Date(previous.timestamp).toDateString();
4245
- return /* @__PURE__ */ jsxs24("div", { children: [
4246
- startsNewDay ? /* @__PURE__ */ jsx26(DateDivider, { iso: message.timestamp }) : null,
4247
- /* @__PURE__ */ jsx26(
5871
+ return /* @__PURE__ */ jsxs27("div", { children: [
5872
+ startsNewDay ? /* @__PURE__ */ jsx29(DateDivider, { iso: message.timestamp }) : null,
5873
+ /* @__PURE__ */ jsx29(
4248
5874
  MessageBubble,
4249
5875
  {
4250
5876
  message,
@@ -4261,39 +5887,57 @@ function ConversationPane({
4261
5887
  })
4262
5888
  }
4263
5889
  ),
4264
- messageSelection && selectedMessageIds.size > 0 ? /* @__PURE__ */ jsxs24("div", { className: "cv-workspace-selection", children: [
4265
- /* @__PURE__ */ jsx26("span", { children: labels.messagesSelected(selectedMessageIds.size) }),
4266
- /* @__PURE__ */ jsxs24("div", { className: "cv-workspace-selection__actions", children: [
4267
- /* @__PURE__ */ jsx26("button", { "data-cv-tooltip": labels.bulkClear, "aria-label": labels.bulkClear, type: "button", onClick: () => setSelectedMessageIds(/* @__PURE__ */ new Set()), children: labels.bulkClear }),
4268
- /* @__PURE__ */ jsx26("button", { "data-cv-tooltip": labels.copySelected, "aria-label": labels.copySelected, type: "button", onClick: copySelectedMessages, children: labels.copySelected })
5890
+ messageSelection && selectedMessageIds.size > 0 ? /* @__PURE__ */ jsxs27("div", { className: "cv-workspace-selection", children: [
5891
+ /* @__PURE__ */ jsx29("span", { children: labels.messagesSelected(selectedMessageIds.size) }),
5892
+ /* @__PURE__ */ jsxs27("div", { className: "cv-workspace-selection__actions", children: [
5893
+ /* @__PURE__ */ jsx29(
5894
+ "button",
5895
+ {
5896
+ "data-cv-tooltip": labels.bulkClear,
5897
+ "aria-label": labels.bulkClear,
5898
+ type: "button",
5899
+ onClick: () => setSelectedMessageIds(/* @__PURE__ */ new Set()),
5900
+ children: labels.bulkClear
5901
+ }
5902
+ ),
5903
+ /* @__PURE__ */ jsx29(
5904
+ "button",
5905
+ {
5906
+ "data-cv-tooltip": labels.copySelected,
5907
+ "aria-label": labels.copySelected,
5908
+ type: "button",
5909
+ onClick: copySelectedMessages,
5910
+ children: labels.copySelected
5911
+ }
5912
+ )
4269
5913
  ] })
4270
5914
  ] }) : null,
4271
- sendFailure ? /* @__PURE__ */ jsx26("p", { role: "alert", className: "cv-workspace-alert", children: sendFailure }) : null,
4272
- blocked ? /* @__PURE__ */ jsx26(
4273
- WindowExpiredNotice,
4274
- {
4275
- disabled: busy,
4276
- onSendTemplate: () => void handleSendTemplate()
4277
- }
4278
- ) : botOwnsConversation ? (
5915
+ sendFailure ? /* @__PURE__ */ jsx29("p", { role: "alert", className: "cv-workspace-alert", children: sendFailure }) : null,
5916
+ blocked ? /* @__PURE__ */ jsx29(WindowExpiredNotice, { disabled: busy, onSendTemplate: () => void handleSendTemplate() }) : botOwnsConversation ? (
4279
5917
  // Responder com a conversa no bot atropelaria o fluxo automático no meio de uma pergunta.
4280
- /* @__PURE__ */ jsx26("p", { className: "cv-workspace-notice", children: labels.takeoverToReply })
4281
- ) : composer === "rich" ? /* @__PURE__ */ jsx26(
5918
+ /* @__PURE__ */ jsx29("p", { className: "cv-workspace-notice", children: labels.takeoverToReply })
5919
+ ) : composer === "rich" ? /* @__PURE__ */ jsx29(
4282
5920
  RichMessageComposer,
4283
5921
  {
4284
5922
  value: draft,
4285
5923
  onChange: setDraft,
4286
5924
  onSend: () => void handleRichSend(),
4287
- ...onSendAttachments ? { onAttachFiles: (files) => setQueuedFiles((current) => [...current, ...Array.from(files)]) } : onAttach ? {
5925
+ ...onSendAttachments ? {
5926
+ onAttachFiles: (files) => enqueueAttachments(
5927
+ Array.from(files).map(
5928
+ (file) => ({ kind: "local", localId: crypto.randomUUID(), file })
5929
+ )
5930
+ )
5931
+ } : onAttach ? {
4288
5932
  onAttachFiles: (files) => {
4289
5933
  for (const file of Array.from(files)) void handleAttach(file);
4290
5934
  }
4291
5935
  } : {},
4292
5936
  placeholder: labels.composerPlaceholder,
4293
5937
  isSending: busy || isSendingDraft,
4294
- hasQueuedAttachments: queuedFiles.length > 0,
5938
+ hasQueuedAttachments: queue.length > 0,
4295
5939
  ...onRecordAudio ? {
4296
- idleAction: /* @__PURE__ */ jsx26(
5940
+ idleAction: /* @__PURE__ */ jsx29(
4297
5941
  AudioRecorderButton,
4298
5942
  {
4299
5943
  onRecorded: (file) => void runSend(() => onRecordAudio(file), labels.recordFailure),
@@ -4301,25 +5945,34 @@ function ConversationPane({
4301
5945
  }
4302
5946
  )
4303
5947
  } : {},
4304
- ...queuedFiles.length > 0 ? {
4305
- attachmentsPreview: /* @__PURE__ */ jsx26("ul", { className: "cv-workspace-attachments", children: queuedFiles.map((file, index) => /* @__PURE__ */ jsxs24("li", { children: [
4306
- /* @__PURE__ */ jsx26("span", { children: file.name }),
4307
- /* @__PURE__ */ jsx26(
4308
- "button",
4309
- {
4310
- "data-cv-tooltip": labels.attachmentRemove,
4311
- type: "button",
4312
- "aria-label": labels.attachmentRemove,
4313
- onClick: () => setQueuedFiles((current) => current.filter((_, position) => position !== index)),
4314
- children: "\u2715"
5948
+ ...queue.length > 0 ? {
5949
+ attachmentsPreview: /* @__PURE__ */ jsx29(
5950
+ QueuedAttachmentsList,
5951
+ {
5952
+ items: queue,
5953
+ statusOf: (key) => attachmentStatus[key] ?? "waiting",
5954
+ onRemove: removeQueuedAttachment,
5955
+ onRetry: retryQueuedAttachment,
5956
+ retryingKeys,
5957
+ getThumbnailUrl: getQueuedAttachmentThumbnailUrl,
5958
+ busy: isSendingDraft,
5959
+ labels: {
5960
+ remove: labels.attachmentRemove,
5961
+ waiting: labels.attachmentWaiting,
5962
+ sending: labels.attachmentSending,
5963
+ sent: labels.attachmentSent,
5964
+ failed: labels.attachmentFailed,
5965
+ skipped: labels.attachmentSkipped,
5966
+ retry: labels.attachmentRetry
4315
5967
  }
4316
- )
4317
- ] }, `${file.name}-${index}`)) })
5968
+ }
5969
+ )
4318
5970
  } : {},
4319
5971
  ...richQuickReplies ? { quickReplies: [...richQuickReplies] } : {},
4320
- ...composerVariables ? { variables: [...composerVariables] } : {}
5972
+ ...composerVariables ? { variables: [...composerVariables] } : {},
5973
+ ...savedQuickReplies ? { savedQuickReplies } : {}
4321
5974
  }
4322
- ) : /* @__PURE__ */ jsx26(
5975
+ ) : /* @__PURE__ */ jsx29(
4323
5976
  MessageComposer,
4324
5977
  {
4325
5978
  value: draft,
@@ -4328,14 +5981,15 @@ function ConversationPane({
4328
5981
  ...onAttach ? { onAttach: (file) => void handleAttach(file) } : {},
4329
5982
  placeholder: labels.composerPlaceholder,
4330
5983
  ...quickReplies ? { quickReplies } : {},
4331
- ...quickReplyVariablesFor ? { quickReplyVariables: quickReplyVariablesFor(conversation, conversationContext) } : {}
5984
+ ...quickReplyVariables ? { quickReplyVariables } : {},
5985
+ ...savedQuickReplies ? { savedQuickReplies } : {}
4332
5986
  }
4333
5987
  )
4334
5988
  ] });
4335
5989
  }
4336
5990
 
4337
5991
  // src/workspace/ConversationsInboxList.tsx
4338
- import { jsx as jsx27, jsxs as jsxs25 } from "react/jsx-runtime";
5992
+ import { jsx as jsx30, jsxs as jsxs28 } from "react/jsx-runtime";
4339
5993
  function ConversationsInboxList({
4340
5994
  inbox,
4341
5995
  labels,
@@ -4346,13 +6000,13 @@ function ConversationsInboxList({
4346
6000
  onSendTemplateToSelected
4347
6001
  }) {
4348
6002
  const hasSelection = inbox.selectedIds.size > 0;
4349
- return /* @__PURE__ */ jsxs25("aside", { className, children: [
4350
- hasSelection ? /* @__PURE__ */ jsxs25("div", { className: "cv-workspace-bulk", children: [
4351
- /* @__PURE__ */ jsx27("span", { className: "cv-workspace-bulk__count", children: labels.bulkSelected(inbox.selectedIds.size) }),
4352
- /* @__PURE__ */ jsxs25("div", { className: "cv-workspace-bulk__actions", children: [
4353
- /* @__PURE__ */ jsx27("button", { "data-cv-tooltip": labels.bulkClear, "aria-label": labels.bulkClear, type: "button", onClick: inbox.clearBulkSelection, children: labels.bulkClear }),
4354
- onSendTemplateToSelected ? /* @__PURE__ */ jsx27("button", { "data-cv-tooltip": labels.bulkTemplate, "aria-label": labels.bulkTemplate, type: "button", onClick: onSendTemplateToSelected, disabled: inbox.busy, children: labels.bulkTemplate }) : null,
4355
- inbox.canFinalize ? /* @__PURE__ */ jsx27(
6003
+ return /* @__PURE__ */ jsxs28("aside", { className, children: [
6004
+ hasSelection ? /* @__PURE__ */ jsxs28("div", { className: "cv-workspace-bulk", children: [
6005
+ /* @__PURE__ */ jsx30("span", { className: "cv-workspace-bulk__count", children: labels.bulkSelected(inbox.selectedIds.size) }),
6006
+ /* @__PURE__ */ jsxs28("div", { className: "cv-workspace-bulk__actions", children: [
6007
+ /* @__PURE__ */ jsx30("button", { "data-cv-tooltip": labels.bulkClear, "aria-label": labels.bulkClear, type: "button", onClick: inbox.clearBulkSelection, children: labels.bulkClear }),
6008
+ onSendTemplateToSelected ? /* @__PURE__ */ jsx30("button", { "data-cv-tooltip": labels.bulkTemplate, "aria-label": labels.bulkTemplate, type: "button", onClick: onSendTemplateToSelected, disabled: inbox.busy, children: labels.bulkTemplate }) : null,
6009
+ inbox.canFinalize ? /* @__PURE__ */ jsx30(
4356
6010
  "button",
4357
6011
  {
4358
6012
  "data-cv-tooltip": labels.bulkFinalize,
@@ -4370,8 +6024,8 @@ function ConversationsInboxList({
4370
6024
  renderBulkActions?.(inbox)
4371
6025
  ] })
4372
6026
  ] }) : null,
4373
- /* @__PURE__ */ jsxs25("div", { className: "cv-workspace-filters", children: [
4374
- /* @__PURE__ */ jsx27(
6027
+ /* @__PURE__ */ jsxs28("div", { className: "cv-workspace-filters", children: [
6028
+ /* @__PURE__ */ jsx30(
4375
6029
  "input",
4376
6030
  {
4377
6031
  type: "search",
@@ -4381,9 +6035,9 @@ function ConversationsInboxList({
4381
6035
  className: "cv-workspace-search"
4382
6036
  }
4383
6037
  ),
4384
- /* @__PURE__ */ jsxs25("div", { className: "cv-workspace-chips", children: [
4385
- /* @__PURE__ */ jsx27("span", { className: "cv-workspace-chips__legend", children: labels.windowLegend }),
4386
- WINDOW_FILTERS.map((filter) => /* @__PURE__ */ jsx27(
6038
+ /* @__PURE__ */ jsxs28("div", { className: "cv-workspace-chips", children: [
6039
+ /* @__PURE__ */ jsx30("span", { className: "cv-workspace-chips__legend", children: labels.windowLegend }),
6040
+ WINDOW_FILTERS.map((filter) => /* @__PURE__ */ jsx30(
4387
6041
  "button",
4388
6042
  {
4389
6043
  type: "button",
@@ -4401,9 +6055,9 @@ function ConversationsInboxList({
4401
6055
  filter.value
4402
6056
  ))
4403
6057
  ] }),
4404
- inbox.channelFilters.length > 0 ? /* @__PURE__ */ jsxs25("div", { className: "cv-workspace-chips", children: [
4405
- /* @__PURE__ */ jsx27("span", { className: "cv-workspace-chips__legend", children: labels.channelLegend }),
4406
- inbox.channelFilters.map((filter) => /* @__PURE__ */ jsxs25(
6058
+ inbox.channelFilters.length > 0 ? /* @__PURE__ */ jsxs28("div", { className: "cv-workspace-chips", children: [
6059
+ /* @__PURE__ */ jsx30("span", { className: "cv-workspace-chips__legend", children: labels.channelLegend }),
6060
+ inbox.channelFilters.map((filter) => /* @__PURE__ */ jsxs28(
4407
6061
  "button",
4408
6062
  {
4409
6063
  "data-cv-tooltip": filter.label,
@@ -4413,7 +6067,7 @@ function ConversationsInboxList({
4413
6067
  "aria-pressed": inbox.channelFilter === filter.value,
4414
6068
  className: `cv-workspace-chip${inbox.channelFilter === filter.value ? " cv-workspace-chip--on" : ""}`,
4415
6069
  children: [
4416
- filter.value === CHANNEL_FILTER_ALL ? null : /* @__PURE__ */ jsx27(ChannelIcon, { channel: filter.value }),
6070
+ filter.value === CHANNEL_FILTER_ALL ? null : /* @__PURE__ */ jsx30(ChannelIcon, { channel: filter.value }),
4417
6071
  filter.label
4418
6072
  ]
4419
6073
  },
@@ -4421,14 +6075,14 @@ function ConversationsInboxList({
4421
6075
  ))
4422
6076
  ] }) : null,
4423
6077
  renderFilters?.(inbox),
4424
- /* @__PURE__ */ jsxs25("label", { className: "cv-workspace-selectall", children: [
4425
- /* @__PURE__ */ jsx27("input", { type: "checkbox", checked: inbox.allOnPageSelected, onChange: inbox.toggleSelectAllOnPage }),
6078
+ /* @__PURE__ */ jsxs28("label", { className: "cv-workspace-selectall", children: [
6079
+ /* @__PURE__ */ jsx30("input", { type: "checkbox", checked: inbox.allOnPageSelected, onChange: inbox.toggleSelectAllOnPage }),
4426
6080
  labels.selectAll
4427
6081
  ] })
4428
6082
  ] }),
4429
- /* @__PURE__ */ jsxs25("div", { className: "cv-workspace-rows", children: [
6083
+ /* @__PURE__ */ jsxs28("div", { className: "cv-workspace-rows", children: [
4430
6084
  inbox.pageConversations.map(
4431
- (conversation) => renderRow ? /* @__PURE__ */ jsx27("div", { children: renderRow(conversation) }, conversation.id) : /* @__PURE__ */ jsx27(
6085
+ (conversation) => renderRow ? /* @__PURE__ */ jsx30("div", { children: renderRow(conversation) }, conversation.id) : /* @__PURE__ */ jsx30(
4432
6086
  ConversationRow,
4433
6087
  {
4434
6088
  conversation,
@@ -4443,13 +6097,13 @@ function ConversationsInboxList({
4443
6097
  conversation.id
4444
6098
  )
4445
6099
  ),
4446
- !inbox.loading && inbox.filteredCount === 0 ? /* @__PURE__ */ jsx27("p", { className: "cv-workspace-empty", children: labels.emptyList }) : null
6100
+ !inbox.loading && inbox.filteredCount === 0 ? /* @__PURE__ */ jsx30("p", { className: "cv-workspace-empty", children: labels.emptyList }) : null
4447
6101
  ] }),
4448
- /* @__PURE__ */ jsxs25("div", { className: "cv-workspace-pager", children: [
4449
- /* @__PURE__ */ jsx27("span", { children: labels.rangeOf(inbox.firstOnPage, inbox.lastOnPage, inbox.filteredCount) }),
4450
- /* @__PURE__ */ jsxs25("div", { className: "cv-workspace-pager__buttons", children: [
4451
- /* @__PURE__ */ jsx27("button", { type: "button", onClick: () => inbox.goToPage(1), disabled: inbox.page === 1, "aria-label": PAGINATION_LABELS.first, "data-cv-tooltip": PAGINATION_LABELS.first, children: "\xAB" }),
4452
- /* @__PURE__ */ jsx27(
6102
+ /* @__PURE__ */ jsxs28("div", { className: "cv-workspace-pager", children: [
6103
+ /* @__PURE__ */ jsx30("span", { children: labels.rangeOf(inbox.firstOnPage, inbox.lastOnPage, inbox.filteredCount) }),
6104
+ /* @__PURE__ */ jsxs28("div", { className: "cv-workspace-pager__buttons", children: [
6105
+ /* @__PURE__ */ jsx30("button", { type: "button", onClick: () => inbox.goToPage(1), disabled: inbox.page === 1, "aria-label": PAGINATION_LABELS.first, "data-cv-tooltip": PAGINATION_LABELS.first, children: "\xAB" }),
6106
+ /* @__PURE__ */ jsx30(
4453
6107
  "button",
4454
6108
  {
4455
6109
  type: "button",
@@ -4460,8 +6114,8 @@ function ConversationsInboxList({
4460
6114
  children: "\u2039"
4461
6115
  }
4462
6116
  ),
4463
- /* @__PURE__ */ jsx27("span", { children: labels.pageOf(inbox.page, inbox.pageCount) }),
4464
- /* @__PURE__ */ jsx27(
6117
+ /* @__PURE__ */ jsx30("span", { children: labels.pageOf(inbox.page, inbox.pageCount) }),
6118
+ /* @__PURE__ */ jsx30(
4465
6119
  "button",
4466
6120
  {
4467
6121
  type: "button",
@@ -4472,7 +6126,7 @@ function ConversationsInboxList({
4472
6126
  children: "\u203A"
4473
6127
  }
4474
6128
  ),
4475
- /* @__PURE__ */ jsx27(
6129
+ /* @__PURE__ */ jsx30(
4476
6130
  "button",
4477
6131
  {
4478
6132
  type: "button",
@@ -4521,6 +6175,12 @@ var DEFAULT_CONVERSATIONS_WORKSPACE_LABELS = {
4521
6175
  composerPlaceholder: "Responder como atendente\u2026",
4522
6176
  attachFailure: "Falha ao enviar o arquivo.",
4523
6177
  attachmentRemove: "Remover anexo",
6178
+ attachmentWaiting: "Aguardando",
6179
+ attachmentSending: "Enviando\u2026",
6180
+ attachmentSent: "Enviado \u2713",
6181
+ attachmentFailed: "Falhou",
6182
+ attachmentSkipped: "N\xE3o enviado \u2014 aguardando o anterior",
6183
+ attachmentRetry: "Tentar de novo",
4524
6184
  recordFailure: "Falha ao gravar o \xE1udio.",
4525
6185
  sendFailure: "Falha ao enviar a mensagem.",
4526
6186
  takeoverToReply: "Assuma o atendimento para responder diretamente ao cliente.",
@@ -4530,7 +6190,7 @@ var DEFAULT_CONVERSATIONS_WORKSPACE_LABELS = {
4530
6190
  };
4531
6191
 
4532
6192
  // src/workspace/useConversationsInbox.ts
4533
- import { useCallback as useCallback9, useEffect as useEffect13, useMemo as useMemo5, useRef as useRef10, useState as useState18 } from "react";
6193
+ import { useCallback as useCallback14, useEffect as useEffect18, useMemo as useMemo5, useRef as useRef16, useState as useState24 } from "react";
4534
6194
  var CONVERSATIONS_PER_PAGE = 50;
4535
6195
  function defaultDescribeFailure(error) {
4536
6196
  const status = error.status;
@@ -4548,14 +6208,14 @@ function useConversationsInbox(params = {}) {
4548
6208
  const perPage = params.perPage ?? CONVERSATIONS_PER_PAGE;
4549
6209
  const markReadOnOpen = params.markReadOnOpen ?? true;
4550
6210
  const describeFailure = params.describeFailure ?? defaultDescribeFailure;
4551
- const [waitingOnly, setWaitingOnly] = useState18(false);
4552
- const [windowFilter, setWindowFilter] = useState18(CONVERSATION_WINDOW.ALL);
4553
- const [channelFilter, setChannelFilter] = useState18(CHANNEL_FILTER_ALL);
4554
- const [search, setSearch] = useState18("");
4555
- const [page, setPage] = useState18(1);
4556
- const [selectedId, setSelectedId] = useState18(void 0);
4557
- const [selectedIds, setSelectedIds] = useState18(/* @__PURE__ */ new Set());
4558
- const [busy, setBusy] = useState18(false);
6211
+ const [waitingOnly, setWaitingOnly] = useState24(false);
6212
+ const [windowFilter, setWindowFilter] = useState24(CONVERSATION_WINDOW.ALL);
6213
+ const [channelFilter, setChannelFilter] = useState24(CHANNEL_FILTER_ALL);
6214
+ const [search, setSearch] = useState24("");
6215
+ const [page, setPage] = useState24(1);
6216
+ const [selectedId, setSelectedId] = useState24(void 0);
6217
+ const [selectedIds, setSelectedIds] = useState24(/* @__PURE__ */ new Set());
6218
+ const [busy, setBusy] = useState24(false);
4559
6219
  const { conversations, total, loading, error, refetch } = useConversationList({
4560
6220
  ...waitingOnly ? { waitingHuman: true } : {},
4561
6221
  ...search ? { search } : {},
@@ -4563,7 +6223,7 @@ function useConversationsInbox(params = {}) {
4563
6223
  ...params.serverPaginated ? { page, limit: perPage } : {}
4564
6224
  });
4565
6225
  useGlobalRealtime(
4566
- useCallback9(() => {
6226
+ useCallback14(() => {
4567
6227
  void refetch();
4568
6228
  }, [refetch])
4569
6229
  );
@@ -4580,16 +6240,16 @@ function useConversationsInbox(params = {}) {
4580
6240
  const pageCount = Math.max(1, Math.ceil(totalForPaging / perPage));
4581
6241
  const currentPage = Math.min(page, pageCount);
4582
6242
  const pageConversations = params.serverPaginated ? filtered : filtered.slice((currentPage - 1) * perPage, currentPage * perPage);
4583
- useEffect13(() => {
6243
+ useEffect18(() => {
4584
6244
  setPage(1);
4585
6245
  }, [search, waitingOnly, windowFilter, channelFilter]);
4586
- useEffect13(() => {
6246
+ useEffect18(() => {
4587
6247
  if (selectedId && !conversations.some((conversation) => conversation.id === selectedId)) {
4588
6248
  setSelectedId(void 0);
4589
6249
  }
4590
6250
  }, [conversations, selectedId]);
4591
- const lastMarkReadAttempt = useRef10(void 0);
4592
- useEffect13(() => {
6251
+ const lastMarkReadAttempt = useRef16(void 0);
6252
+ useEffect18(() => {
4593
6253
  if (!markReadOnOpen || !selectedId) return;
4594
6254
  const opened = conversations.find((conversation) => conversation.id === selectedId);
4595
6255
  if (!opened || opened.unread === 0) return;
@@ -4599,7 +6259,7 @@ function useConversationsInbox(params = {}) {
4599
6259
  void api.markRead(selectedId).then(() => refetch()).catch(() => {
4600
6260
  });
4601
6261
  }, [markReadOnOpen, selectedId, conversations, refetch, api]);
4602
- const toggleSelected = useCallback9((conversationId) => {
6262
+ const toggleSelected = useCallback14((conversationId) => {
4603
6263
  setSelectedIds((current) => {
4604
6264
  const next = new Set(current);
4605
6265
  if (next.has(conversationId)) next.delete(conversationId);
@@ -4609,7 +6269,7 @@ function useConversationsInbox(params = {}) {
4609
6269
  }, []);
4610
6270
  const pageIds = pageConversations.map((conversation) => conversation.id);
4611
6271
  const allOnPageSelected = pageIds.length > 0 && pageIds.every((id) => selectedIds.has(id));
4612
- const toggleSelectAllOnPage = useCallback9(() => {
6272
+ const toggleSelectAllOnPage = useCallback14(() => {
4613
6273
  setSelectedIds((current) => {
4614
6274
  const next = new Set(current);
4615
6275
  if (allOnPageSelected) pageIds.forEach((id) => next.delete(id));
@@ -4617,7 +6277,7 @@ function useConversationsInbox(params = {}) {
4617
6277
  return next;
4618
6278
  });
4619
6279
  }, [allOnPageSelected, pageIds]);
4620
- const runOnSelection = useCallback9(
6280
+ const runOnSelection = useCallback14(
4621
6281
  async (action) => {
4622
6282
  setBusy(true);
4623
6283
  try {
@@ -4630,7 +6290,7 @@ function useConversationsInbox(params = {}) {
4630
6290
  },
4631
6291
  [selectedIds, refetch]
4632
6292
  );
4633
- const runConversationAction = useCallback9(
6293
+ const runConversationAction = useCallback14(
4634
6294
  async (action, conversationId) => {
4635
6295
  if (!action) return;
4636
6296
  setBusy(true);
@@ -4712,7 +6372,7 @@ function useConversationsInbox(params = {}) {
4712
6372
  }
4713
6373
 
4714
6374
  // src/workspace/ConversationsWorkspace.tsx
4715
- import { Fragment as Fragment7, jsx as jsx28, jsxs as jsxs26 } from "react/jsx-runtime";
6375
+ import { Fragment as Fragment8, jsx as jsx31, jsxs as jsxs29 } from "react/jsx-runtime";
4716
6376
  function ConversationsWorkspace({
4717
6377
  labels: labelsOverride,
4718
6378
  filters,
@@ -4731,6 +6391,7 @@ function ConversationsWorkspace({
4731
6391
  initialComposerText,
4732
6392
  composer,
4733
6393
  composerVariablesFor,
6394
+ conversationVariablesFor,
4734
6395
  onSendAttachments,
4735
6396
  onRecordAudio,
4736
6397
  contextEntriesOf,
@@ -4752,10 +6413,10 @@ function ConversationsWorkspace({
4752
6413
  ...markReadOnOpen === void 0 ? {} : { markReadOnOpen },
4753
6414
  ...serverPaginated ? { serverPaginated } : {}
4754
6415
  });
4755
- const [simulatorOpen, setSimulatorOpen] = useState19(false);
4756
- const [templateModalOpen, setTemplateModalOpen] = useState19(false);
4757
- const [openedFromLink, setOpenedFromLink] = useState19(void 0);
4758
- useEffect14(() => {
6416
+ const [simulatorOpen, setSimulatorOpen] = useState25(false);
6417
+ const [templateModalOpen, setTemplateModalOpen] = useState25(false);
6418
+ const [openedFromLink, setOpenedFromLink] = useState25(void 0);
6419
+ useEffect19(() => {
4759
6420
  const link = initialConversationId ?? initialWhatsappNumber;
4760
6421
  if (!link || openedFromLink === link) return;
4761
6422
  const target = inbox.conversations.find(
@@ -4787,50 +6448,50 @@ function ConversationsWorkspace({
4787
6448
  ...fromProduct,
4788
6449
  {
4789
6450
  key: "simulator",
4790
- icon: simulator?.icon ?? /* @__PURE__ */ jsx28(FlaskConical, { size: ICON_SIZE_ACTION }),
6451
+ icon: simulator?.icon ?? /* @__PURE__ */ jsx31(FlaskConical, { size: ICON_SIZE_ACTION }),
4791
6452
  label: simulator?.label ?? "Simular cliente",
4792
6453
  active: simulatorOpen,
4793
6454
  run: () => setSimulatorOpen((open) => !open)
4794
6455
  }
4795
6456
  ];
4796
6457
  }, [selected, extraUtilitiesFor, simulatorEnabled, simulator, simulatorOpen]);
4797
- return /* @__PURE__ */ jsxs26("div", { className: `cv-workspace${className ? ` ${className}` : ""}`, children: [
4798
- /* @__PURE__ */ jsx28(TooltipLayer, {}),
4799
- /* @__PURE__ */ jsxs26("header", { className: "cv-workspace-header", children: [
4800
- /* @__PURE__ */ jsxs26("div", { className: "cv-workspace-header__titles", children: [
4801
- /* @__PURE__ */ jsx28("h1", { children: labels.title }),
4802
- /* @__PURE__ */ jsxs26("p", { children: [
4803
- /* @__PURE__ */ jsxs26("span", { "data-cv-tooltip": labels.conversations, className: "cv-stat", children: [
4804
- /* @__PURE__ */ jsx28(MessagesSquare, { size: ICON_SIZE_INLINE, "aria-hidden": "true" }),
6458
+ return /* @__PURE__ */ jsxs29("div", { className: `cv-workspace${className ? ` ${className}` : ""}`, children: [
6459
+ /* @__PURE__ */ jsx31(TooltipLayer, {}),
6460
+ /* @__PURE__ */ jsxs29("header", { className: "cv-workspace-header", children: [
6461
+ /* @__PURE__ */ jsxs29("div", { className: "cv-workspace-header__titles", children: [
6462
+ /* @__PURE__ */ jsx31("h1", { children: labels.title }),
6463
+ /* @__PURE__ */ jsxs29("p", { children: [
6464
+ /* @__PURE__ */ jsxs29("span", { "data-cv-tooltip": labels.conversations, className: "cv-stat", children: [
6465
+ /* @__PURE__ */ jsx31(MessagesSquare, { size: ICON_SIZE_INLINE, "aria-hidden": "true" }),
4805
6466
  " ",
4806
6467
  inbox.totalCount,
4807
- /* @__PURE__ */ jsxs26("span", { className: "cv-only-wide", children: [
6468
+ /* @__PURE__ */ jsxs29("span", { className: "cv-only-wide", children: [
4808
6469
  " ",
4809
6470
  labels.conversations
4810
6471
  ] })
4811
6472
  ] }),
4812
- /* @__PURE__ */ jsxs26("span", { "data-cv-tooltip": labels.waiting, className: "cv-stat cv-workspace-header__waiting", children: [
4813
- /* @__PURE__ */ jsx28(Hourglass2, { size: ICON_SIZE_INLINE, "aria-hidden": "true" }),
6473
+ /* @__PURE__ */ jsxs29("span", { "data-cv-tooltip": labels.waiting, className: "cv-stat cv-workspace-header__waiting", children: [
6474
+ /* @__PURE__ */ jsx31(Hourglass2, { size: ICON_SIZE_INLINE, "aria-hidden": "true" }),
4814
6475
  " ",
4815
6476
  inbox.waitingCount,
4816
- /* @__PURE__ */ jsxs26("span", { className: "cv-only-wide", children: [
6477
+ /* @__PURE__ */ jsxs29("span", { className: "cv-only-wide", children: [
4817
6478
  " ",
4818
6479
  labels.waiting
4819
6480
  ] })
4820
6481
  ] }),
4821
- /* @__PURE__ */ jsxs26("span", { "data-cv-tooltip": labels.unread, className: "cv-stat", children: [
4822
- /* @__PURE__ */ jsx28(Mail2, { size: ICON_SIZE_INLINE, "aria-hidden": "true" }),
6482
+ /* @__PURE__ */ jsxs29("span", { "data-cv-tooltip": labels.unread, className: "cv-stat", children: [
6483
+ /* @__PURE__ */ jsx31(Mail2, { size: ICON_SIZE_INLINE, "aria-hidden": "true" }),
4823
6484
  " ",
4824
6485
  inbox.unreadCount,
4825
- /* @__PURE__ */ jsxs26("span", { className: "cv-only-wide", children: [
6486
+ /* @__PURE__ */ jsxs29("span", { className: "cv-only-wide", children: [
4826
6487
  " ",
4827
6488
  labels.unread
4828
6489
  ] })
4829
6490
  ] })
4830
6491
  ] })
4831
6492
  ] }),
4832
- /* @__PURE__ */ jsxs26("div", { className: "cv-workspace-header__actions", children: [
4833
- /* @__PURE__ */ jsxs26(
6493
+ /* @__PURE__ */ jsxs29("div", { className: "cv-workspace-header__actions", children: [
6494
+ /* @__PURE__ */ jsxs29(
4834
6495
  "button",
4835
6496
  {
4836
6497
  type: "button",
@@ -4840,12 +6501,12 @@ function ConversationsWorkspace({
4840
6501
  "aria-label": labels.waitingOnly,
4841
6502
  className: inbox.waitingOnly ? "cv-workspace-toggle cv-workspace-toggle--on" : "cv-workspace-toggle",
4842
6503
  children: [
4843
- /* @__PURE__ */ jsx28(Hourglass2, { size: ICON_SIZE_ACTION, "aria-hidden": "true" }),
4844
- /* @__PURE__ */ jsx28("span", { className: "cv-only-wide", children: labels.waitingOnly })
6504
+ /* @__PURE__ */ jsx31(Hourglass2, { size: ICON_SIZE_ACTION, "aria-hidden": "true" }),
6505
+ /* @__PURE__ */ jsx31("span", { className: "cv-only-wide", children: labels.waitingOnly })
4845
6506
  ]
4846
6507
  }
4847
6508
  ),
4848
- /* @__PURE__ */ jsxs26(
6509
+ /* @__PURE__ */ jsxs29(
4849
6510
  "button",
4850
6511
  {
4851
6512
  type: "button",
@@ -4855,9 +6516,9 @@ function ConversationsWorkspace({
4855
6516
  "aria-label": labels.markSelectedAsRead,
4856
6517
  className: "cv-workspace-toggle",
4857
6518
  children: [
4858
- /* @__PURE__ */ jsx28(Check2, { size: ICON_SIZE_ACTION, "aria-hidden": "true" }),
4859
- /* @__PURE__ */ jsx28("span", { className: "cv-only-wide", children: labels.markSelectedAsRead }),
4860
- inbox.selectedIds.size > 0 ? /* @__PURE__ */ jsxs26("span", { children: [
6519
+ /* @__PURE__ */ jsx31(Check2, { size: ICON_SIZE_ACTION, "aria-hidden": "true" }),
6520
+ /* @__PURE__ */ jsx31("span", { className: "cv-only-wide", children: labels.markSelectedAsRead }),
6521
+ inbox.selectedIds.size > 0 ? /* @__PURE__ */ jsxs29("span", { children: [
4861
6522
  "(",
4862
6523
  inbox.selectedIds.size,
4863
6524
  ")"
@@ -4865,7 +6526,7 @@ function ConversationsWorkspace({
4865
6526
  ]
4866
6527
  }
4867
6528
  ),
4868
- inbox.canMarkAllRead && inbox.unreadCount > 0 ? /* @__PURE__ */ jsxs26(
6529
+ inbox.canMarkAllRead && inbox.unreadCount > 0 ? /* @__PURE__ */ jsxs29(
4869
6530
  "button",
4870
6531
  {
4871
6532
  type: "button",
@@ -4875,28 +6536,28 @@ function ConversationsWorkspace({
4875
6536
  "aria-label": labels.markAllAsRead,
4876
6537
  className: "cv-workspace-toggle",
4877
6538
  children: [
4878
- /* @__PURE__ */ jsx28(CheckCheck, { size: ICON_SIZE_ACTION, "aria-hidden": "true" }),
4879
- /* @__PURE__ */ jsx28("span", { className: "cv-only-wide", children: labels.markAllAsRead })
6539
+ /* @__PURE__ */ jsx31(CheckCheck, { size: ICON_SIZE_ACTION, "aria-hidden": "true" }),
6540
+ /* @__PURE__ */ jsx31("span", { className: "cv-only-wide", children: labels.markAllAsRead })
4880
6541
  ]
4881
6542
  }
4882
6543
  ) : null,
4883
6544
  renderHeaderActions?.(inbox)
4884
6545
  ] })
4885
6546
  ] }),
4886
- inbox.loadFailure ? /* @__PURE__ */ jsxs26("p", { role: "alert", className: "cv-workspace-failure", children: [
6547
+ inbox.loadFailure ? /* @__PURE__ */ jsxs29("p", { role: "alert", className: "cv-workspace-failure", children: [
4887
6548
  inbox.loadFailure,
4888
- signInHref ? /* @__PURE__ */ jsxs26(Fragment7, { children: [
6549
+ signInHref ? /* @__PURE__ */ jsxs29(Fragment8, { children: [
4889
6550
  " ",
4890
- /* @__PURE__ */ jsx28("a", { href: signInHref, className: "cv-workspace-failure__link", children: labels.signIn })
6551
+ /* @__PURE__ */ jsx31("a", { href: signInHref, className: "cv-workspace-failure__link", children: labels.signIn })
4891
6552
  ] }) : null
4892
6553
  ] }) : null,
4893
- /* @__PURE__ */ jsxs26(
6554
+ /* @__PURE__ */ jsxs29(
4894
6555
  "div",
4895
6556
  {
4896
6557
  className: `cv-workspace-grid${showSimulator ? " cv-workspace-grid--with-simulator" : ""}`,
4897
6558
  "data-detail": selected ? "open" : "closed",
4898
6559
  children: [
4899
- /* @__PURE__ */ jsx28(
6560
+ /* @__PURE__ */ jsx31(
4900
6561
  ConversationsInboxList,
4901
6562
  {
4902
6563
  inbox,
@@ -4908,7 +6569,7 @@ function ConversationsWorkspace({
4908
6569
  ...onSendTemplateToSelected ? { onSendTemplateToSelected: () => onSendTemplateToSelected(inbox) } : inbox.canListTemplates ? { onSendTemplateToSelected: () => setTemplateModalOpen(true) } : {}
4909
6570
  }
4910
6571
  ),
4911
- /* @__PURE__ */ jsx28("section", { className: "cv-workspace-detail", children: selected ? /* @__PURE__ */ jsx28(
6572
+ /* @__PURE__ */ jsx31("section", { className: "cv-workspace-detail", children: selected ? /* @__PURE__ */ jsx31(
4912
6573
  ConversationPane,
4913
6574
  {
4914
6575
  conversation: selected,
@@ -4925,6 +6586,7 @@ function ConversationsWorkspace({
4925
6586
  ...initialComposerText ? { initialComposerText } : {},
4926
6587
  ...composer ? { composer } : {},
4927
6588
  ...composerVariablesFor ? { composerVariablesFor } : {},
6589
+ ...conversationVariablesFor ? { conversationVariablesFor } : {},
4928
6590
  ...onSendAttachments ? {
4929
6591
  onSendAttachments: (files, caption) => onSendAttachments(selected, files, caption)
4930
6592
  } : {},
@@ -4937,14 +6599,14 @@ function ConversationsWorkspace({
4937
6599
  ...renderAboveTranscript ? { renderAboveTranscript } : {},
4938
6600
  ...onAttach ? { onAttach: (file) => onAttach(selected, file) } : {}
4939
6601
  }
4940
- ) : /* @__PURE__ */ jsx28("p", { className: "cv-workspace-empty", children: labels.emptyDetail }) }),
6602
+ ) : /* @__PURE__ */ jsx31("p", { className: "cv-workspace-empty", children: labels.emptyDetail }) }),
4941
6603
  showSimulator && selected ? (
4942
6604
  // `min-height:0` junto do `min-width:0`: sem isso a linha do grid cresce com o conteúdo do
4943
6605
  // painel, o scroll interno nunca ativa e quem rola passa a ser a página inteira.
4944
- /* @__PURE__ */ jsx28("div", { className: "cv-workspace-simulator", children: simulator?.render ? simulator.render({ conversationId: selected.id, channel, close: () => setSimulatorOpen(false) }) : simulatorClient && conversations ? (
6606
+ /* @__PURE__ */ jsx31("div", { className: "cv-workspace-simulator", children: simulator?.render ? simulator.render({ conversationId: selected.id, channel, close: () => setSimulatorOpen(false) }) : simulatorClient && conversations ? (
4945
6607
  // `key` pela conversa: trocar de contato sem remontar deixaria o transcript e o campo
4946
6608
  // de texto do contato anterior na tela.
4947
- /* @__PURE__ */ jsx28(
6609
+ /* @__PURE__ */ jsx31(
4948
6610
  ConversationSimulatorPanel,
4949
6611
  {
4950
6612
  client: simulatorClient,
@@ -4965,7 +6627,7 @@ function ConversationsWorkspace({
4965
6627
  ]
4966
6628
  }
4967
6629
  ),
4968
- templateModalOpen ? /* @__PURE__ */ jsx28(
6630
+ templateModalOpen ? /* @__PURE__ */ jsx31(
4969
6631
  BulkTemplateModal,
4970
6632
  {
4971
6633
  labels,
@@ -5018,8 +6680,11 @@ export {
5018
6680
  DEFAULT_EMOJI_PICKER_LABELS,
5019
6681
  DEFAULT_INTERACTIVE_MESSAGE_LABELS,
5020
6682
  DEFAULT_LIGHTBOX_LABELS,
6683
+ DEFAULT_MAX_ATTACHMENT_SIZE_BYTES,
5021
6684
  DEFAULT_MAX_RECORDING_MILLISECONDS,
5022
6685
  DEFAULT_MESSAGE_COMPOSER_LABELS,
6686
+ DEFAULT_QUICK_REPLIES_PICKER_LABELS,
6687
+ DEFAULT_QUICK_REPLIES_WORKSPACE_LABELS,
5023
6688
  DEFAULT_RICH_COMPOSER_TOOLTIPS,
5024
6689
  DEFAULT_TEMPLATES_SETTINGS_LABELS,
5025
6690
  DEFAULT_WHATSAPP_MESSAGE_EDITOR_LABELS,
@@ -5045,6 +6710,10 @@ export {
5045
6710
  MessagesWorkspace,
5046
6711
  MultiSelectFilter,
5047
6712
  NARROW_MAX_WIDTH_PX,
6713
+ QUICK_REPLY_ATTACHMENT_LIMIT,
6714
+ QueuedAttachmentsList,
6715
+ QuickRepliesPicker,
6716
+ QuickRepliesWorkspace,
5048
6717
  REOPEN_MECHANISM,
5049
6718
  REPLY_LATENCY,
5050
6719
  REPLY_LATENCY_CRITICAL_HOURS,
@@ -5075,19 +6744,24 @@ export {
5075
6744
  contactFlag,
5076
6745
  createMediaUrlResolver,
5077
6746
  downloadTextFile,
6747
+ filterQuickReplies,
5078
6748
  formatContactHandle,
5079
6749
  formatDateTime,
5080
6750
  formatFileSize,
5081
6751
  formatPhone,
5082
6752
  formatStalledFor,
5083
6753
  formatTimestamp,
6754
+ highlightMatch,
5084
6755
  htmlToWA,
5085
6756
  isReplyOverdue,
5086
6757
  isSameDay,
5087
6758
  isWindowBlocking,
6759
+ normalizeForSearch,
5088
6760
  parseWhatsAppFormatting,
5089
6761
  phoneInitials,
6762
+ queuedAttachmentsFromQuickReply,
5090
6763
  replyLatencyOf,
6764
+ resolveMaxAttachmentSizeBytes,
5091
6765
  resolveQuickReply,
5092
6766
  searchEmojis,
5093
6767
  toast,