@adatechnology/conversations-ui 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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 +2166 -509
  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 +396 -0
  29. package/src/quickReplies/quickReplyAttachments.ts +289 -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 +155 -0
  49. package/src/workspace/useComposerQueue.ts +220 -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,1033 @@ 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 attachmentSendStatusOf(status) {
4260
+ if (status === "sent") return "sent";
4261
+ if (status === "skipped") return "skipped";
4262
+ return "failed";
4263
+ }
4264
+ async function sendStoredBatch(stored, idempotencyKey, sendStoredAttachments, onAttachmentStatus) {
4265
+ for (const item of stored) onAttachmentStatus?.(attachmentKey(item), "sending");
4266
+ try {
4267
+ const response = await sendStoredAttachments({ uploadIds: stored.map((item) => item.uploadId), idempotencyKey });
4268
+ let results = response.results;
4269
+ const resultedUploadIds = new Set(results.map((result) => result.uploadId));
4270
+ for (const item of stored) {
4271
+ if (!resultedUploadIds.has(item.uploadId)) results = [...results, { uploadId: item.uploadId, status: "failed" }];
4272
+ }
4273
+ for (const result of results) {
4274
+ onAttachmentStatus?.(result.uploadId, attachmentSendStatusOf(result.status));
4275
+ }
4276
+ return results;
4277
+ } catch {
4278
+ for (const item of stored) onAttachmentStatus?.(attachmentKey(item), "failed");
4279
+ return stored.map((item) => ({ uploadId: item.uploadId, status: "failed" }));
4280
+ }
4281
+ }
4282
+ async function retryStoredAttachments(params) {
4283
+ const { queue, uploadIds, idempotencyKey, sendStoredAttachments, onAttachmentStatus } = params;
4284
+ const targetIds = new Set(uploadIds);
4285
+ const targets = queue.filter(
4286
+ (item) => item.kind === "stored" && targetIds.has(item.uploadId)
4287
+ );
4288
+ if (targets.length === 0) return { remainingQueue: queue, sentAttachmentKeys: [] };
4289
+ const results = await sendStoredBatch(targets, idempotencyKey, sendStoredAttachments, onAttachmentStatus);
4290
+ const sentAttachmentKeys = results.filter((result) => result.status === "sent").map((result) => result.uploadId);
4291
+ return { remainingQueue: applySendResults(queue, results), sentAttachmentKeys };
4292
+ }
4293
+ function resolveRetryOutcome(params) {
4294
+ const { conversationIdAtRetry, currentConversationId, sentAttachmentKeys, queue } = params;
4295
+ if (conversationIdAtRetry !== currentConversationId) return void 0;
4296
+ const sentKeys = new Set(sentAttachmentKeys);
4297
+ return queue.filter((item) => !sentKeys.has(attachmentKey(item)));
4298
+ }
4299
+ function excludeRetryingItems(queue, retryingKeys) {
4300
+ if (retryingKeys.size === 0) return queue;
4301
+ return queue.filter((item) => !retryingKeys.has(attachmentKey(item)));
4302
+ }
4303
+ async function sendQueuedMessage(params) {
4304
+ const { text, queue, idempotencyKey, sendText, sendStoredAttachments, sendLocalAttachments, onAttachmentStatus } = params;
4305
+ if (text.trim()) {
4306
+ const textSent = await sendText(text);
4307
+ if (!textSent) return { textSent: false, remainingQueue: queue, sentAttachmentKeys: [] };
4308
+ }
4309
+ const { attachments } = orderOutgoingItems(text, queue);
4310
+ const stored = attachments.filter((item) => item.kind === "stored");
4311
+ const local = attachments.filter(
4312
+ (item) => item.kind === "local"
4313
+ );
4314
+ const results = stored.length > 0 && sendStoredAttachments ? await sendStoredBatch(stored, idempotencyKey, sendStoredAttachments, onAttachmentStatus) : [];
4315
+ let remainingQueue = applySendResults(queue, results);
4316
+ const sentAttachmentKeys = results.filter((result) => result.status === "sent").map((result) => result.uploadId);
4317
+ if (local.length > 0 && sendLocalAttachments) {
4318
+ for (const item of local) onAttachmentStatus?.(attachmentKey(item), "sending");
4319
+ try {
4320
+ await sendLocalAttachments(local.map((item) => item.file));
4321
+ for (const item of local) onAttachmentStatus?.(attachmentKey(item), "sent");
4322
+ const sentKeys = new Set(local.map((item) => attachmentKey(item)));
4323
+ remainingQueue = remainingQueue.filter((item) => item.kind !== "local" || !sentKeys.has(attachmentKey(item)));
4324
+ sentAttachmentKeys.push(...sentKeys);
4325
+ } catch {
4326
+ for (const item of local) onAttachmentStatus?.(attachmentKey(item), "failed");
4327
+ }
4328
+ }
4329
+ return { textSent: true, remainingQueue, sentAttachmentKeys };
4330
+ }
4331
+
4332
+ // src/quickReplies/QuickRepliesWorkspace.tsx
4333
+ import { useEffect as useEffect13, useId as useId2, useRef as useRef11, useState as useState18 } from "react";
4334
+
4335
+ // src/quickReplies/useQuickRepliesWorkspace.ts
4336
+ import { useCallback as useCallback10, useEffect as useEffect12, useState as useState17 } from "react";
4337
+
4338
+ // src/quickReplies/quickReplyAttachmentUpload.ts
4339
+ function validateAttachmentFiles(files, currentCount, limits) {
4340
+ const accepted = [];
4341
+ const rejected = [];
4342
+ let count = currentCount;
4343
+ for (const file of files) {
4344
+ if (!canAddAttachments(count, 1)) {
4345
+ rejected.push({ file, reason: "limit" });
4346
+ continue;
4347
+ }
4348
+ const maxSize = resolveMaxAttachmentSizeBytes(file.type, limits);
4349
+ if (file.size > maxSize) {
4350
+ rejected.push({ file, reason: "size" });
4351
+ continue;
4352
+ }
4353
+ accepted.push(file);
4354
+ count += 1;
4355
+ }
4356
+ return { accepted, rejected };
4357
+ }
4358
+ function moveAttachment(list, index, direction) {
4359
+ const target = index + direction;
4360
+ if (index < 0 || index >= list.length || target < 0 || target >= list.length) return list;
4361
+ const next = [...list];
4362
+ const [item] = next.splice(index, 1);
4363
+ next.splice(target, 0, item);
4364
+ return next;
4365
+ }
4366
+
4367
+ // src/quickReplies/useQuickReplyAttachmentUploads.ts
4368
+ import { useCallback as useCallback9, useEffect as useEffect11, useRef as useRef9, useState as useState16 } from "react";
4369
+
4370
+ // src/quickReplies/createUploadQueue.ts
4371
+ function createUploadQueue(maxConcurrent) {
4372
+ const pending = [];
4373
+ const runners = /* @__PURE__ */ new Map();
4374
+ const controllers = /* @__PURE__ */ new Map();
4375
+ let activeCount = 0;
4376
+ function pump() {
4377
+ while (activeCount < maxConcurrent && pending.length > 0) {
4378
+ const id = pending.shift();
4379
+ const run = runners.get(id);
4380
+ runners.delete(id);
4381
+ if (!run) continue;
4382
+ const controller = new AbortController();
4383
+ controllers.set(id, controller);
4384
+ activeCount += 1;
4385
+ void run(controller.signal).finally(() => {
4386
+ controllers.delete(id);
4387
+ activeCount -= 1;
4388
+ pump();
4389
+ });
4390
+ }
4391
+ }
4392
+ function enqueue(id, run) {
4393
+ if (controllers.has(id)) return;
4394
+ runners.set(id, run);
4395
+ if (!pending.includes(id)) pending.push(id);
4396
+ pump();
4397
+ }
4398
+ function abort(id) {
4399
+ const controller = controllers.get(id);
4400
+ if (controller) {
4401
+ controller.abort();
4402
+ return;
4403
+ }
4404
+ const index = pending.indexOf(id);
4405
+ if (index >= 0) pending.splice(index, 1);
4406
+ runners.delete(id);
4407
+ }
4408
+ function abortAll() {
4409
+ for (const controller of controllers.values()) controller.abort();
4410
+ pending.length = 0;
4411
+ runners.clear();
4412
+ }
4413
+ return { enqueue, abort, abortAll };
4414
+ }
4415
+
4416
+ // src/quickReplies/useQuickReplyAttachmentUploads.ts
4417
+ function useQuickReplyAttachmentUploads({
4418
+ upload,
3999
4419
  labels,
4000
- expiredCount,
4001
- sending,
4002
- onClose,
4003
- onSend
4420
+ attachmentSizeLimits,
4421
+ attachmentsCount,
4422
+ onUploaded
4004
4423
  }) {
4005
- const context = useConversations();
4006
- if (!context) {
4007
- throw new Error("BulkTemplateModal requires an ancestor <ConversationsProvider>");
4424
+ const [pendingUploads, setPendingUploads] = useState16([]);
4425
+ const [attachmentRejections, setAttachmentRejections] = useState16([]);
4426
+ const uploadQueueRef = useRef9(createUploadQueue(3));
4427
+ const isMountedRef = useRef9(true);
4428
+ useEffect11(
4429
+ () => () => {
4430
+ isMountedRef.current = false;
4431
+ uploadQueueRef.current.abortAll();
4432
+ },
4433
+ []
4434
+ );
4435
+ const abortAllUploads = useCallback9(() => {
4436
+ uploadQueueRef.current.abortAll();
4437
+ setPendingUploads([]);
4438
+ }, []);
4439
+ const dismissAttachmentRejections = useCallback9(() => setAttachmentRejections([]), []);
4440
+ const cancelAttachmentUpload = useCallback9((localId) => {
4441
+ uploadQueueRef.current.abort(localId);
4442
+ setPendingUploads((current) => current.filter((item) => item.localId !== localId));
4443
+ }, []);
4444
+ const updatePendingUpload = useCallback9((localId, patch) => {
4445
+ setPendingUploads((current) => current.map((item) => item.localId === localId ? { ...item, ...patch } : item));
4446
+ }, []);
4447
+ const uploadOneFile = useCallback9(
4448
+ (localId, file) => {
4449
+ if (!upload) return;
4450
+ uploadQueueRef.current.enqueue(localId, async (signal) => {
4451
+ try {
4452
+ const attachment = await upload(file, {
4453
+ onProgress: (fraction) => {
4454
+ if (isMountedRef.current) updatePendingUpload(localId, { progress: fraction });
4455
+ },
4456
+ signal
4457
+ });
4458
+ if (!isMountedRef.current) return;
4459
+ onUploaded(attachment);
4460
+ setPendingUploads((current) => current.filter((item) => item.localId !== localId));
4461
+ } catch (caught) {
4462
+ if (signal.aborted || !isMountedRef.current) return;
4463
+ updatePendingUpload(localId, {
4464
+ status: "error",
4465
+ error: caught instanceof Error ? caught.message : labels.saveError
4466
+ });
4467
+ }
4468
+ });
4469
+ },
4470
+ [upload, updatePendingUpload, onUploaded, labels.saveError]
4471
+ );
4472
+ const retryAttachmentUpload = useCallback9(
4473
+ (localId) => {
4474
+ const item = pendingUploads.find((pending) => pending.localId === localId);
4475
+ if (!item) return;
4476
+ updatePendingUpload(localId, { status: "uploading", progress: 0, error: void 0 });
4477
+ uploadOneFile(localId, item.file);
4478
+ },
4479
+ [pendingUploads, updatePendingUpload, uploadOneFile]
4480
+ );
4481
+ const addAttachmentFiles = useCallback9(
4482
+ (files) => {
4483
+ if (!upload) return;
4484
+ const currentCount = attachmentsCount + pendingUploads.length;
4485
+ const { accepted, rejected } = validateAttachmentFiles(Array.from(files), currentCount, attachmentSizeLimits);
4486
+ setAttachmentRejections(rejected);
4487
+ if (accepted.length === 0) return;
4488
+ const newItems = accepted.map((file) => ({
4489
+ localId: `${file.name}-${file.size}-${file.lastModified}-${Math.random().toString(36).slice(2)}`,
4490
+ file,
4491
+ status: "uploading",
4492
+ progress: 0
4493
+ }));
4494
+ setPendingUploads((current) => [...current, ...newItems]);
4495
+ for (const item of newItems) uploadOneFile(item.localId, item.file);
4496
+ },
4497
+ [upload, attachmentsCount, pendingUploads.length, uploadOneFile, attachmentSizeLimits]
4498
+ );
4499
+ return {
4500
+ pendingUploads,
4501
+ attachmentRejections,
4502
+ addAttachmentFiles,
4503
+ retryAttachmentUpload,
4504
+ cancelAttachmentUpload,
4505
+ dismissAttachmentRejections,
4506
+ abortAllUploads
4507
+ };
4508
+ }
4509
+
4510
+ // src/quickReplies/useQuickRepliesWorkspace.ts
4511
+ var TITLE_MAX_LENGTH = 40;
4512
+ var BODY_MAX_LENGTH = 1e3;
4513
+ var SHORTCUT_PATTERN = /^[a-z0-9-]{1,20}$/;
4514
+ var MAX_ERROR_ENVELOPE_DEPTH = 3;
4515
+ function apiErrorEnvelopeOf(error, depth = 0) {
4516
+ if (depth >= MAX_ERROR_ENVELOPE_DEPTH) return void 0;
4517
+ if (!error || typeof error !== "object") return void 0;
4518
+ const candidate = error;
4519
+ if ("code" in candidate || "details" in candidate) return candidate;
4520
+ const nested = candidate.error ?? candidate.response?.data ?? candidate.body;
4521
+ if (nested && typeof nested === "object") return apiErrorEnvelopeOf(nested, depth + 1);
4522
+ return void 0;
4523
+ }
4524
+ function fieldErrorsOf(error) {
4525
+ const envelope = apiErrorEnvelopeOf(error);
4526
+ if (!envelope) return {};
4527
+ const details = envelope.details;
4528
+ if (!Array.isArray(details)) return {};
4529
+ const result = {};
4530
+ for (const detail of details) {
4531
+ if (!detail || typeof detail !== "object") continue;
4532
+ const field = detail.field;
4533
+ const message = detail.message;
4534
+ if (typeof field === "string" && typeof message === "string" && isFormField(field)) {
4535
+ result[field] = message;
4536
+ }
4008
4537
  }
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;
4538
+ return result;
4539
+ }
4540
+ function isFormField(value) {
4541
+ return value === "title" || value === "shortcut" || value === "body";
4542
+ }
4543
+ function errorCodeOf(error) {
4544
+ const code = apiErrorEnvelopeOf(error)?.code;
4545
+ return typeof code === "string" ? code : void 0;
4546
+ }
4547
+ function validateQuickReplyInput(input, labels) {
4548
+ const errors = {};
4549
+ if (!input.title.trim() || input.title.length > TITLE_MAX_LENGTH) errors.title = labels.fieldTitleInvalid;
4550
+ if (!SHORTCUT_PATTERN.test(input.shortcut)) errors.shortcut = labels.fieldShortcutInvalid;
4551
+ if (!input.body.trim() || input.body.length > BODY_MAX_LENGTH) errors.body = labels.fieldBodyInvalid;
4552
+ return errors;
4553
+ }
4554
+ async function submitQuickReply({
4555
+ api,
4556
+ editing,
4557
+ labels
4558
+ }) {
4559
+ const input = {
4560
+ title: editing.title.trim(),
4561
+ shortcut: editing.shortcut.trim(),
4562
+ body: editing.body,
4563
+ // Só manda a lista quando o host oferece a porta de upload — ausente, `attachmentUploadIds`
4564
+ // some do payload e o backend não mexe nos anexos já gravados (contrato do tipo).
4565
+ ...api.uploadQuickReplyAttachment ? { attachmentUploadIds: editing.attachments.map((attachment) => attachment.uploadId) } : {}
4566
+ };
4567
+ const validationErrors = validateQuickReplyInput(input, labels);
4568
+ if (Object.keys(validationErrors).length > 0) return { outcome: "invalid", fieldErrors: validationErrors };
4569
+ const editingId = editing.id;
4570
+ if (editingId === null && !api.createQuickReply) {
4571
+ return { outcome: "rejected", fieldErrors: {}, formError: labels.saveUnavailable };
4572
+ }
4573
+ if (editingId !== null && !api.updateQuickReply) {
4574
+ return { outcome: "rejected", fieldErrors: {}, formError: labels.saveUnavailable };
4575
+ }
4576
+ const save = editingId === null ? api.createQuickReply : (input2) => api.updateQuickReply(editingId, input2);
4577
+ try {
4578
+ const quickReply = await save(input);
4579
+ if (!quickReply) return { outcome: "invalid", fieldErrors: {} };
4580
+ return { outcome: "saved", quickReply };
4581
+ } catch (caught) {
4582
+ const detailErrors = fieldErrorsOf(caught);
4583
+ const code = errorCodeOf(caught);
4584
+ const shortcutMessage = detailErrors.shortcut ?? (code === "QUICK_REPLY_SHORTCUT_TAKEN" ? labels.shortcutTaken : void 0);
4585
+ if (shortcutMessage || Object.keys(detailErrors).length > 0) {
4586
+ return {
4587
+ outcome: "rejected",
4588
+ fieldErrors: { ...detailErrors, ...shortcutMessage ? { shortcut: shortcutMessage } : {} }
4589
+ };
4590
+ }
4591
+ return {
4592
+ outcome: "rejected",
4593
+ fieldErrors: {},
4594
+ formError: labels.saveError
4021
4595
  };
4022
- }, [api]);
4023
- const filtered = useMemo4(() => {
4024
- const term = search.trim().toLowerCase();
4596
+ }
4597
+ }
4598
+ function insertAtCursor(text, start, end, marker) {
4599
+ return { text: text.slice(0, start) + marker + text.slice(end), caret: start + marker.length };
4600
+ }
4601
+ function useQuickRepliesWorkspace({
4602
+ api,
4603
+ labels,
4604
+ attachmentSizeLimits
4605
+ }) {
4606
+ const [quickReplies, setQuickReplies] = useState17([]);
4607
+ const [isLoading, setIsLoading] = useState17(false);
4608
+ const [loadError, setLoadError] = useState17(void 0);
4609
+ const [search, setSearch] = useState17("");
4610
+ const [editing, setEditing] = useState17(void 0);
4611
+ const [fieldErrors, setFieldErrors] = useState17({});
4612
+ const [isSaving, setIsSaving] = useState17(false);
4613
+ const [saveError, setSaveError] = useState17(void 0);
4614
+ const [deletingId, setDeletingId] = useState17(void 0);
4615
+ const uploadAttachment = useCallback10((attachment) => {
4616
+ setEditing((current) => current ? { ...current, attachments: [...current.attachments, attachment] } : current);
4617
+ }, []);
4618
+ const {
4619
+ pendingUploads,
4620
+ attachmentRejections,
4621
+ addAttachmentFiles,
4622
+ retryAttachmentUpload,
4623
+ cancelAttachmentUpload,
4624
+ dismissAttachmentRejections,
4625
+ abortAllUploads
4626
+ } = useQuickReplyAttachmentUploads({
4627
+ ...api.uploadQuickReplyAttachment ? { upload: api.uploadQuickReplyAttachment } : {},
4628
+ labels,
4629
+ ...attachmentSizeLimits ? { attachmentSizeLimits } : {},
4630
+ attachmentsCount: editing?.attachments.length ?? 0,
4631
+ onUploaded: uploadAttachment
4632
+ });
4633
+ useEffect12(() => {
4634
+ if (!api.listQuickReplies) return;
4635
+ let cancelled = false;
4636
+ setIsLoading(true);
4637
+ setLoadError(void 0);
4638
+ api.listQuickReplies().then((result) => {
4639
+ if (!cancelled) setQuickReplies(result);
4640
+ }).catch((caught) => {
4641
+ if (!cancelled) setLoadError(caught instanceof Error ? caught.message : labels.failure);
4642
+ }).finally(() => {
4643
+ if (!cancelled) setIsLoading(false);
4644
+ });
4645
+ return () => {
4646
+ cancelled = true;
4647
+ };
4648
+ }, [api.listQuickReplies]);
4649
+ const filtered = filterQuickReplies({ quickReplies, search });
4650
+ const startCreate = useCallback10(() => {
4651
+ abortAllUploads();
4652
+ dismissAttachmentRejections();
4653
+ setEditing({ id: null, title: "", shortcut: "", body: "", attachments: [] });
4654
+ setFieldErrors({});
4655
+ setSaveError(void 0);
4656
+ }, [abortAllUploads, dismissAttachmentRejections]);
4657
+ const startEdit = useCallback10(
4658
+ (quickReply) => {
4659
+ abortAllUploads();
4660
+ dismissAttachmentRejections();
4661
+ setEditing({
4662
+ id: quickReply.id,
4663
+ title: quickReply.title,
4664
+ shortcut: quickReply.shortcut,
4665
+ body: quickReply.body,
4666
+ attachments: quickReply.attachments ?? []
4667
+ });
4668
+ setFieldErrors({});
4669
+ setSaveError(void 0);
4670
+ },
4671
+ [abortAllUploads, dismissAttachmentRejections]
4672
+ );
4673
+ const cancelEdit = useCallback10(() => {
4674
+ abortAllUploads();
4675
+ dismissAttachmentRejections();
4676
+ setEditing(void 0);
4677
+ setFieldErrors({});
4678
+ setSaveError(void 0);
4679
+ }, [abortAllUploads, dismissAttachmentRejections]);
4680
+ const updateField = useCallback10((field, value) => {
4681
+ setEditing((current) => current ? { ...current, [field]: value } : current);
4682
+ setFieldErrors((current) => {
4683
+ if (!(field in current)) return current;
4684
+ const next = { ...current };
4685
+ delete next[field];
4686
+ return next;
4687
+ });
4688
+ }, []);
4689
+ const submit = useCallback10(async () => {
4690
+ if (!editing || pendingUploads.length > 0) return;
4691
+ setIsSaving(true);
4692
+ setSaveError(void 0);
4693
+ setFieldErrors({});
4694
+ const result = await submitQuickReply({ api, editing, labels });
4695
+ setIsSaving(false);
4696
+ if (result.outcome === "saved") {
4697
+ setQuickReplies(
4698
+ (current) => editing.id === null ? [result.quickReply, ...current] : current.map((item) => item.id === result.quickReply.id ? result.quickReply : item)
4699
+ );
4700
+ setEditing(void 0);
4701
+ return;
4702
+ }
4703
+ setFieldErrors(result.fieldErrors);
4704
+ if (result.outcome === "rejected") setSaveError(result.formError);
4705
+ }, [editing, api, labels, pendingUploads.length]);
4706
+ const remove = useCallback10(
4707
+ async (id) => {
4708
+ if (!api.deleteQuickReply) return;
4709
+ setDeletingId(id);
4710
+ try {
4711
+ await api.deleteQuickReply(id);
4712
+ setQuickReplies((current) => current.filter((item) => item.id !== id));
4713
+ } finally {
4714
+ setDeletingId(void 0);
4715
+ }
4716
+ },
4717
+ [api]
4718
+ );
4719
+ const removeAttachment = useCallback10((uploadId) => {
4720
+ setEditing(
4721
+ (current) => current ? { ...current, attachments: current.attachments.filter((attachment) => attachment.uploadId !== uploadId) } : current
4722
+ );
4723
+ }, []);
4724
+ const moveAttachmentAt = useCallback10((index, direction) => {
4725
+ setEditing(
4726
+ (current) => current ? { ...current, attachments: moveAttachment(current.attachments, index, direction) } : current
4727
+ );
4728
+ }, []);
4729
+ return {
4730
+ quickReplies,
4731
+ filtered,
4732
+ isLoading,
4733
+ loadError,
4734
+ search,
4735
+ setSearch,
4736
+ readOnly: !api.createQuickReply,
4737
+ canEdit: Boolean(api.updateQuickReply),
4738
+ canDelete: Boolean(api.deleteQuickReply),
4739
+ editing,
4740
+ startCreate,
4741
+ startEdit,
4742
+ cancelEdit,
4743
+ updateField,
4744
+ fieldErrors,
4745
+ isSaving,
4746
+ saveError,
4747
+ submit,
4748
+ remove,
4749
+ deletingId,
4750
+ hasAttachmentsCapability: Boolean(api.uploadQuickReplyAttachment),
4751
+ pendingUploads,
4752
+ addAttachmentFiles,
4753
+ retryAttachmentUpload,
4754
+ cancelAttachmentUpload,
4755
+ removeAttachment,
4756
+ moveAttachmentAt,
4757
+ attachmentRejections,
4758
+ dismissAttachmentRejections
4759
+ };
4760
+ }
4761
+
4762
+ // src/quickReplies/AttachmentsFormSection.tsx
4763
+ import { useRef as useRef10 } from "react";
4764
+ import { Paperclip as Paperclip2, X as X4 } from "lucide-react";
4765
+ import { Fragment as Fragment7, jsx as jsx25, jsxs as jsxs23 } from "react/jsx-runtime";
4766
+ function AttachmentsFormSection({
4767
+ labels: text,
4768
+ attachments,
4769
+ pendingUploads,
4770
+ attachmentRejections,
4771
+ onAddFiles,
4772
+ onRetryUpload,
4773
+ onCancelUpload,
4774
+ onRemoveAttachment,
4775
+ onMoveAttachment,
4776
+ onDismissRejections
4777
+ }) {
4778
+ const attachmentFileInputRef = useRef10(null);
4779
+ return /* @__PURE__ */ jsxs23("div", { className: "space-y-2", children: [
4780
+ /* @__PURE__ */ jsx25("span", { className: "block text-sm font-medium", children: text.attachmentsTitle }),
4781
+ 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: [
4782
+ attachments.map((attachment, index) => /* @__PURE__ */ jsxs23(
4783
+ "li",
4784
+ {
4785
+ className: "flex items-center gap-2 rounded-md border border-gray-200 px-2 py-1.5 text-xs dark:border-gray-700",
4786
+ children: [
4787
+ /* @__PURE__ */ jsx25(Paperclip2, { "aria-hidden": "true", className: "h-3.5 w-3.5 flex-none text-gray-400" }),
4788
+ /* @__PURE__ */ jsx25("span", { className: "min-w-0 flex-1 truncate", children: attachment.filename }),
4789
+ /* @__PURE__ */ jsx25("span", { className: "flex-none text-gray-400", children: formatFileSize(attachment.sizeBytes) }),
4790
+ /* @__PURE__ */ jsx25(
4791
+ "button",
4792
+ {
4793
+ type: "button",
4794
+ disabled: index === 0,
4795
+ "aria-label": text.attachmentMoveUp,
4796
+ onClick: () => onMoveAttachment(index, -1),
4797
+ className: "flex-none disabled:opacity-30",
4798
+ children: "\u2191"
4799
+ }
4800
+ ),
4801
+ /* @__PURE__ */ jsx25(
4802
+ "button",
4803
+ {
4804
+ type: "button",
4805
+ disabled: index === attachments.length - 1,
4806
+ "aria-label": text.attachmentMoveDown,
4807
+ onClick: () => onMoveAttachment(index, 1),
4808
+ className: "flex-none disabled:opacity-30",
4809
+ children: "\u2193"
4810
+ }
4811
+ ),
4812
+ /* @__PURE__ */ jsx25(
4813
+ "button",
4814
+ {
4815
+ type: "button",
4816
+ "aria-label": text.attachmentRemove,
4817
+ onClick: () => onRemoveAttachment(attachment.uploadId),
4818
+ className: "flex-none text-red-600 dark:text-red-400",
4819
+ children: /* @__PURE__ */ jsx25(X4, { "aria-hidden": "true", className: "h-3.5 w-3.5" })
4820
+ }
4821
+ )
4822
+ ]
4823
+ },
4824
+ attachment.uploadId
4825
+ )),
4826
+ pendingUploads.map((pending) => /* @__PURE__ */ jsxs23(
4827
+ "li",
4828
+ {
4829
+ className: "flex items-center gap-2 rounded-md border border-gray-200 px-2 py-1.5 text-xs dark:border-gray-700",
4830
+ "aria-busy": pending.status === "uploading",
4831
+ "aria-live": "polite",
4832
+ children: [
4833
+ /* @__PURE__ */ jsx25(Paperclip2, { "aria-hidden": "true", className: "h-3.5 w-3.5 flex-none text-gray-400" }),
4834
+ /* @__PURE__ */ jsx25("span", { className: "min-w-0 flex-1 truncate", children: pending.file.name }),
4835
+ 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: [
4836
+ /* @__PURE__ */ jsx25("span", { role: "alert", className: "flex-none text-red-600 dark:text-red-400", children: pending.error }),
4837
+ /* @__PURE__ */ jsx25(
4838
+ "button",
4839
+ {
4840
+ type: "button",
4841
+ onClick: () => onRetryUpload(pending.localId),
4842
+ className: "flex-none font-medium text-blue-600 hover:underline dark:text-blue-400",
4843
+ children: text.attachmentRetry
4844
+ }
4845
+ )
4846
+ ] }),
4847
+ /* @__PURE__ */ jsx25(
4848
+ "button",
4849
+ {
4850
+ type: "button",
4851
+ "aria-label": text.attachmentCancel,
4852
+ onClick: () => onCancelUpload(pending.localId),
4853
+ className: "flex-none text-gray-400",
4854
+ children: /* @__PURE__ */ jsx25(X4, { "aria-hidden": "true", className: "h-3.5 w-3.5" })
4855
+ }
4856
+ )
4857
+ ]
4858
+ },
4859
+ pending.localId
4860
+ ))
4861
+ ] }),
4862
+ attachmentRejections.length > 0 ? /* @__PURE__ */ jsxs23("div", { role: "alert", className: "space-y-0.5 text-xs text-red-600 dark:text-red-400", children: [
4863
+ attachmentRejections.map((rejection, index) => /* @__PURE__ */ jsx25("p", { children: rejection.reason === "limit" ? text.attachmentLimitReached : text.attachmentTooLarge(rejection.file.name) }, index)),
4864
+ /* @__PURE__ */ jsx25("button", { type: "button", onClick: onDismissRejections, className: "hover:underline", children: text.cancel })
4865
+ ] }) : null,
4866
+ /* @__PURE__ */ jsx25(
4867
+ "input",
4868
+ {
4869
+ ref: attachmentFileInputRef,
4870
+ type: "file",
4871
+ multiple: true,
4872
+ hidden: true,
4873
+ onChange: (event) => {
4874
+ if (event.target.files) onAddFiles(event.target.files);
4875
+ event.target.value = "";
4876
+ }
4877
+ }
4878
+ ),
4879
+ /* @__PURE__ */ jsx25(
4880
+ "button",
4881
+ {
4882
+ type: "button",
4883
+ onClick: () => attachmentFileInputRef.current?.click(),
4884
+ className: "text-xs font-medium text-blue-600 hover:underline dark:text-blue-400",
4885
+ children: text.attachmentsAdd
4886
+ }
4887
+ )
4888
+ ] });
4889
+ }
4890
+
4891
+ // src/quickReplies/QuickRepliesWorkspace.tsx
4892
+ import { jsx as jsx26, jsxs as jsxs24 } from "react/jsx-runtime";
4893
+ var TABLE_SKELETON_ROWS = 3;
4894
+ function QuickRepliesWorkspace({
4895
+ api,
4896
+ variables,
4897
+ labels,
4898
+ className,
4899
+ attachmentSizeLimits
4900
+ }) {
4901
+ const text = { ...DEFAULT_QUICK_REPLIES_WORKSPACE_LABELS, ...labels };
4902
+ const bodyFieldRef = useRef11(null);
4903
+ const formId = useId2();
4904
+ const titleFieldId = `${formId}-title`;
4905
+ const shortcutFieldId = `${formId}-shortcut`;
4906
+ const bodyFieldId = `${formId}-body`;
4907
+ const [confirmingDeleteId, setConfirmingDeleteId] = useState18(null);
4908
+ const deleteButtonRefs = useRef11({});
4909
+ const confirmButtonRef = useRef11(null);
4910
+ useEffect13(() => {
4911
+ if (confirmingDeleteId) confirmButtonRef.current?.focus();
4912
+ }, [confirmingDeleteId]);
4913
+ const cancelDeleteConfirm = (id) => {
4914
+ setConfirmingDeleteId(null);
4915
+ deleteButtonRefs.current[id]?.focus();
4916
+ };
4917
+ const handleDeleteConfirmKeyDown = (id) => (event) => {
4918
+ if (event.key === "Escape") {
4919
+ event.preventDefault();
4920
+ cancelDeleteConfirm(id);
4921
+ }
4922
+ };
4923
+ const {
4924
+ quickReplies,
4925
+ filtered,
4926
+ isLoading,
4927
+ loadError,
4928
+ search,
4929
+ setSearch,
4930
+ readOnly,
4931
+ canEdit,
4932
+ canDelete,
4933
+ editing,
4934
+ startCreate,
4935
+ startEdit,
4936
+ cancelEdit,
4937
+ updateField,
4938
+ fieldErrors,
4939
+ isSaving,
4940
+ saveError,
4941
+ submit,
4942
+ remove,
4943
+ deletingId,
4944
+ hasAttachmentsCapability,
4945
+ pendingUploads,
4946
+ addAttachmentFiles,
4947
+ retryAttachmentUpload,
4948
+ cancelAttachmentUpload,
4949
+ removeAttachment,
4950
+ moveAttachmentAt,
4951
+ attachmentRejections,
4952
+ dismissAttachmentRejections
4953
+ } = useQuickRepliesWorkspace({ api, labels: text, ...attachmentSizeLimits ? { attachmentSizeLimits } : {} });
4954
+ const hasPendingUploads = pendingUploads.length > 0;
4955
+ const insertVariableAtCursor = (marker) => {
4956
+ const field = bodyFieldRef.current;
4957
+ if (!field || !editing) return;
4958
+ const { text: nextBody, caret } = insertAtCursor(editing.body, field.selectionStart, field.selectionEnd, marker);
4959
+ updateField("body", nextBody);
4960
+ requestAnimationFrame(() => {
4961
+ field.focus();
4962
+ field.setSelectionRange(caret, caret);
4963
+ });
4964
+ };
4965
+ return /* @__PURE__ */ jsxs24("div", { className: cn("space-y-4", className), children: [
4966
+ /* @__PURE__ */ jsxs24("header", { className: "space-y-0.5", children: [
4967
+ /* @__PURE__ */ jsx26("h2", { className: "text-lg font-semibold", children: text.title }),
4968
+ /* @__PURE__ */ jsx26("p", { className: "text-sm text-gray-500 dark:text-gray-400", children: text.subtitle(quickReplies.length) })
4969
+ ] }),
4970
+ readOnly ? /* @__PURE__ */ jsx26("p", { className: "text-sm text-gray-500 dark:text-gray-400", children: text.readOnlyNotice }) : null,
4971
+ /* @__PURE__ */ jsxs24("div", { className: "flex flex-wrap items-center gap-2", children: [
4972
+ /* @__PURE__ */ jsx26(
4973
+ "input",
4974
+ {
4975
+ type: "search",
4976
+ value: search,
4977
+ onChange: (event) => setSearch(event.target.value),
4978
+ placeholder: text.searchPlaceholder,
4979
+ "aria-label": text.searchPlaceholder,
4980
+ 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"
4981
+ }
4982
+ ),
4983
+ !readOnly ? /* @__PURE__ */ jsx26(
4984
+ "button",
4985
+ {
4986
+ type: "button",
4987
+ onClick: startCreate,
4988
+ className: "cv-header-action ml-auto inline-flex items-center gap-1",
4989
+ children: text.create
4990
+ }
4991
+ ) : null
4992
+ ] }),
4993
+ isLoading ? /* @__PURE__ */ jsx26("span", { className: "sr-only", "aria-live": "polite", children: text.loading }) : null,
4994
+ loadError ? /* @__PURE__ */ jsx26("p", { role: "alert", className: "text-sm text-red-600 dark:text-red-400", children: loadError || text.failure }) : null,
4995
+ editing ? /* @__PURE__ */ jsxs24(
4996
+ "form",
4997
+ {
4998
+ className: "space-y-3 rounded-lg border border-gray-200 p-4 dark:border-gray-700",
4999
+ onSubmit: (event) => {
5000
+ event.preventDefault();
5001
+ void submit();
5002
+ },
5003
+ children: [
5004
+ /* @__PURE__ */ jsxs24("div", { className: "space-y-1", children: [
5005
+ /* @__PURE__ */ jsx26("label", { className: "block text-sm font-medium", htmlFor: titleFieldId, children: text.fieldTitle }),
5006
+ /* @__PURE__ */ jsx26(
5007
+ "input",
5008
+ {
5009
+ id: titleFieldId,
5010
+ type: "text",
5011
+ maxLength: 40,
5012
+ value: editing.title,
5013
+ onChange: (event) => updateField("title", event.target.value),
5014
+ "aria-invalid": Boolean(fieldErrors.title),
5015
+ "aria-describedby": fieldErrors.title ? `${titleFieldId}-error` : void 0,
5016
+ className: "w-full rounded-md border border-gray-300 px-3 py-2 text-sm dark:border-gray-700 dark:bg-gray-900"
5017
+ }
5018
+ ),
5019
+ fieldErrors.title ? /* @__PURE__ */ jsx26("p", { id: `${titleFieldId}-error`, role: "alert", className: "text-xs text-red-600 dark:text-red-400", children: fieldErrors.title }) : null
5020
+ ] }),
5021
+ /* @__PURE__ */ jsxs24("div", { className: "space-y-1", children: [
5022
+ /* @__PURE__ */ jsx26("label", { className: "block text-sm font-medium", htmlFor: shortcutFieldId, children: text.fieldShortcut }),
5023
+ /* @__PURE__ */ jsx26(
5024
+ "input",
5025
+ {
5026
+ id: shortcutFieldId,
5027
+ type: "text",
5028
+ maxLength: 20,
5029
+ value: editing.shortcut,
5030
+ onChange: (event) => updateField("shortcut", event.target.value),
5031
+ "aria-invalid": Boolean(fieldErrors.shortcut),
5032
+ "aria-describedby": fieldErrors.shortcut ? `${shortcutFieldId}-error` : `${shortcutFieldId}-hint`,
5033
+ 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"
5034
+ }
5035
+ ),
5036
+ 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 })
5037
+ ] }),
5038
+ /* @__PURE__ */ jsxs24("div", { className: "space-y-1", children: [
5039
+ /* @__PURE__ */ jsx26("label", { className: "block text-sm font-medium", htmlFor: bodyFieldId, children: text.fieldBody }),
5040
+ /* @__PURE__ */ jsx26(
5041
+ "textarea",
5042
+ {
5043
+ id: bodyFieldId,
5044
+ ref: bodyFieldRef,
5045
+ maxLength: 1e3,
5046
+ rows: 4,
5047
+ value: editing.body,
5048
+ onChange: (event) => updateField("body", event.target.value),
5049
+ "aria-invalid": Boolean(fieldErrors.body),
5050
+ "aria-describedby": fieldErrors.body ? `${bodyFieldId}-error` : void 0,
5051
+ 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"
5052
+ }
5053
+ ),
5054
+ fieldErrors.body ? /* @__PURE__ */ jsx26("p", { id: `${bodyFieldId}-error`, role: "alert", className: "text-xs text-red-600 dark:text-red-400", children: fieldErrors.body }) : null,
5055
+ variables && variables.length > 0 ? /* @__PURE__ */ jsx26("div", { className: "flex flex-wrap gap-1 pt-1", children: variables.map((variable) => /* @__PURE__ */ jsx26(
5056
+ "button",
5057
+ {
5058
+ type: "button",
5059
+ onClick: () => insertVariableAtCursor(variable.marker),
5060
+ "aria-label": `${text.insertVariable}: ${variable.label}`,
5061
+ 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",
5062
+ children: variable.label
5063
+ },
5064
+ variable.id
5065
+ )) }) : null
5066
+ ] }),
5067
+ hasAttachmentsCapability ? /* @__PURE__ */ jsx26(
5068
+ AttachmentsFormSection,
5069
+ {
5070
+ labels: text,
5071
+ attachments: editing.attachments,
5072
+ pendingUploads,
5073
+ attachmentRejections,
5074
+ onAddFiles: addAttachmentFiles,
5075
+ onRetryUpload: retryAttachmentUpload,
5076
+ onCancelUpload: cancelAttachmentUpload,
5077
+ onRemoveAttachment: removeAttachment,
5078
+ onMoveAttachment: moveAttachmentAt,
5079
+ onDismissRejections: dismissAttachmentRejections
5080
+ }
5081
+ ) : null,
5082
+ saveError ? /* @__PURE__ */ jsx26("p", { role: "alert", className: "text-sm text-red-600 dark:text-red-400", children: saveError }) : null,
5083
+ /* @__PURE__ */ jsxs24("div", { className: "flex items-center gap-2", children: [
5084
+ /* @__PURE__ */ jsx26(
5085
+ "button",
5086
+ {
5087
+ type: "submit",
5088
+ disabled: isSaving || hasPendingUploads,
5089
+ "aria-busy": isSaving,
5090
+ title: hasPendingUploads ? text.saveBlockedUploading : void 0,
5091
+ className: "cv-header-action inline-flex items-center gap-1 disabled:opacity-40",
5092
+ children: isSaving ? text.saving : hasPendingUploads ? text.saveBlockedUploading : text.save
5093
+ }
5094
+ ),
5095
+ /* @__PURE__ */ jsx26("button", { type: "button", onClick: cancelEdit, className: "text-sm text-gray-500 hover:underline", children: text.cancel })
5096
+ ] })
5097
+ ]
5098
+ }
5099
+ ) : null,
5100
+ /* @__PURE__ */ jsx26("div", { className: "cv-table-card", children: /* @__PURE__ */ jsxs24("table", { className: "cv-table", children: [
5101
+ /* @__PURE__ */ jsx26("thead", { children: /* @__PURE__ */ jsxs24("tr", { children: [
5102
+ /* @__PURE__ */ jsx26("th", { scope: "col", className: "px-3 py-2 text-left text-xs font-medium", children: text.columnTitle }),
5103
+ /* @__PURE__ */ jsx26("th", { scope: "col", className: "px-3 py-2 text-left text-xs font-medium", children: text.columnShortcut }),
5104
+ /* @__PURE__ */ jsx26("th", { scope: "col", className: "hidden px-3 py-2 text-left text-xs font-medium sm:table-cell", children: text.columnBody }),
5105
+ !readOnly ? /* @__PURE__ */ jsx26("th", { scope: "col", className: "px-3 py-2 text-left text-xs font-medium", children: text.columnActions }) : null
5106
+ ] }) }),
5107
+ /* @__PURE__ */ jsxs24("tbody", { children: [
5108
+ isLoading ? Array.from({ length: TABLE_SKELETON_ROWS }).map((_, index) => /* @__PURE__ */ jsxs24("tr", { "aria-hidden": "true", children: [
5109
+ /* @__PURE__ */ jsx26("td", { className: "px-3 py-3", children: /* @__PURE__ */ jsx26("span", { className: "cv-skeleton-line cv-skeleton-line--title block" }) }),
5110
+ /* @__PURE__ */ jsx26("td", { className: "px-3 py-3", children: /* @__PURE__ */ jsx26("span", { className: "cv-skeleton-line cv-skeleton-line--shortcut block" }) }),
5111
+ /* @__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" }) }),
5112
+ !readOnly ? /* @__PURE__ */ jsx26("td", { className: "px-3 py-3" }) : null
5113
+ ] }, 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,
5114
+ !isLoading && filtered.map((quickReply) => /* @__PURE__ */ jsxs24("tr", { className: cn(deletingId === quickReply.id ? "cv-row-departing" : void 0), children: [
5115
+ /* @__PURE__ */ jsx26("td", { className: "px-3 py-2 font-medium", children: quickReply.title }),
5116
+ /* @__PURE__ */ jsxs24("td", { className: "px-3 py-2 font-mono text-xs text-gray-500", children: [
5117
+ "/",
5118
+ quickReply.shortcut
5119
+ ] }),
5120
+ /* @__PURE__ */ jsx26("td", { className: "hidden max-w-sm truncate px-3 py-2 text-gray-500 sm:table-cell", children: quickReply.body }),
5121
+ !readOnly ? /* @__PURE__ */ jsxs24("td", { className: "flex gap-2 px-3 py-2", children: [
5122
+ canEdit ? /* @__PURE__ */ jsx26(
5123
+ "button",
5124
+ {
5125
+ type: "button",
5126
+ onClick: () => startEdit(quickReply),
5127
+ className: "text-xs text-blue-600 hover:underline dark:text-blue-400",
5128
+ children: text.edit
5129
+ }
5130
+ ) : null,
5131
+ canDelete ? confirmingDeleteId === quickReply.id ? (
5132
+ // Linha de confirmação inline em vez de `window.confirm`: trava a aba
5133
+ // inteira e não segue os tokens visuais do pacote (`web.md` §14).
5134
+ /* @__PURE__ */ jsxs24(
5135
+ "span",
5136
+ {
5137
+ role: "group",
5138
+ "aria-label": text.removeConfirm(quickReply.title),
5139
+ onKeyDown: handleDeleteConfirmKeyDown(quickReply.id),
5140
+ className: "flex items-center gap-2 text-xs",
5141
+ children: [
5142
+ text.removeConfirm(quickReply.title),
5143
+ /* @__PURE__ */ jsx26(
5144
+ "button",
5145
+ {
5146
+ ref: confirmButtonRef,
5147
+ type: "button",
5148
+ onClick: () => {
5149
+ setConfirmingDeleteId(null);
5150
+ void remove(quickReply.id);
5151
+ },
5152
+ className: "font-medium text-red-600 hover:underline dark:text-red-400",
5153
+ children: text.remove
5154
+ }
5155
+ ),
5156
+ /* @__PURE__ */ jsx26(
5157
+ "button",
5158
+ {
5159
+ type: "button",
5160
+ onClick: () => cancelDeleteConfirm(quickReply.id),
5161
+ className: "text-gray-500 hover:underline dark:text-gray-400",
5162
+ children: text.cancel
5163
+ }
5164
+ )
5165
+ ]
5166
+ }
5167
+ )
5168
+ ) : /* @__PURE__ */ jsx26(
5169
+ "button",
5170
+ {
5171
+ ref: (node) => {
5172
+ deleteButtonRefs.current[quickReply.id] = node;
5173
+ },
5174
+ type: "button",
5175
+ disabled: deletingId === quickReply.id,
5176
+ "aria-busy": deletingId === quickReply.id,
5177
+ onClick: () => setConfirmingDeleteId(quickReply.id),
5178
+ className: "text-xs text-red-600 hover:underline disabled:opacity-50 dark:text-red-400",
5179
+ children: deletingId === quickReply.id ? text.deleting : text.remove
5180
+ }
5181
+ ) : null
5182
+ ] }) : null
5183
+ ] }, quickReply.id))
5184
+ ] })
5185
+ ] }) })
5186
+ ] });
5187
+ }
5188
+
5189
+ // src/workspace/ConversationsWorkspace.tsx
5190
+ import { useEffect as useEffect19, useMemo as useMemo6, useState as useState25 } from "react";
5191
+ import { Check as Check2, CheckCheck, FlaskConical, Hourglass as Hourglass2, Mail as Mail2, MessagesSquare } from "lucide-react";
5192
+
5193
+ // src/workspace/BulkTemplateModal.tsx
5194
+ import { useEffect as useEffect14, useMemo as useMemo4, useState as useState19 } from "react";
5195
+ import { jsx as jsx27, jsxs as jsxs25 } from "react/jsx-runtime";
5196
+ function BulkTemplateModal({
5197
+ labels,
5198
+ expiredCount,
5199
+ sending,
5200
+ onClose,
5201
+ onSend
5202
+ }) {
5203
+ const context = useConversations();
5204
+ if (!context) {
5205
+ throw new Error("BulkTemplateModal requires an ancestor <ConversationsProvider>");
5206
+ }
5207
+ const { api } = context;
5208
+ const [templates, setTemplates] = useState19([]);
5209
+ const [search, setSearch] = useState19("");
5210
+ const [selected, setSelected] = useState19("");
5211
+ useEffect14(() => {
5212
+ let active = true;
5213
+ void api.listTemplates?.().then((loaded) => {
5214
+ if (active) setTemplates(loaded);
5215
+ }).catch(() => {
5216
+ });
5217
+ return () => {
5218
+ active = false;
5219
+ };
5220
+ }, [api]);
5221
+ const filtered = useMemo4(() => {
5222
+ const term = search.trim().toLowerCase();
4025
5223
  if (!term) return templates;
4026
5224
  return templates.filter((template) => template.name.toLowerCase().includes(term));
4027
5225
  }, [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) })
5226
+ 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: [
5227
+ /* @__PURE__ */ jsxs25("header", { className: "cv-workspace-modal__header", children: [
5228
+ /* @__PURE__ */ jsxs25("div", { children: [
5229
+ /* @__PURE__ */ jsx27("h3", { children: labels.templateModalTitle }),
5230
+ /* @__PURE__ */ jsx27("p", { children: labels.templateModalAvailable(templates.length) })
4033
5231
  ] }),
4034
- /* @__PURE__ */ jsx25("button", { "data-cv-tooltip": labels.templateModalCancel, type: "button", onClick: onClose, "aria-label": labels.templateModalCancel, children: "\u2715" })
5232
+ /* @__PURE__ */ jsx27("button", { "data-cv-tooltip": labels.templateModalCancel, type: "button", onClick: onClose, "aria-label": labels.templateModalCancel, children: "\u2715" })
4035
5233
  ] }),
4036
- /* @__PURE__ */ jsx25("div", { className: "cv-workspace-modal__search", children: /* @__PURE__ */ jsx25(
5234
+ /* @__PURE__ */ jsx27("div", { className: "cv-workspace-modal__search", children: /* @__PURE__ */ jsx27(
4037
5235
  "input",
4038
5236
  {
4039
5237
  type: "search",
@@ -4043,9 +5241,9 @@ function BulkTemplateModal({
4043
5241
  autoFocus: true
4044
5242
  }
4045
5243
  ) }),
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(
5244
+ /* @__PURE__ */ jsxs25("div", { className: "cv-workspace-modal__list", children: [
5245
+ filtered.length === 0 ? /* @__PURE__ */ jsx27("p", { className: "cv-workspace-empty", children: labels.templateModalEmpty }) : null,
5246
+ filtered.map((template) => /* @__PURE__ */ jsxs25(
4049
5247
  "button",
4050
5248
  {
4051
5249
  "data-cv-tooltip": template.name,
@@ -4055,8 +5253,8 @@ function BulkTemplateModal({
4055
5253
  "aria-pressed": template.name === selected,
4056
5254
  className: `cv-workspace-modal__item${template.name === selected ? " cv-workspace-modal__item--on" : ""}`,
4057
5255
  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: [
5256
+ /* @__PURE__ */ jsx27("span", { className: "cv-workspace-modal__item-name", children: template.name }),
5257
+ /* @__PURE__ */ jsxs25("span", { className: "cv-workspace-modal__item-meta", children: [
4060
5258
  template.language,
4061
5259
  template.category ? ` \xB7 ${template.category.toLowerCase()}` : ""
4062
5260
  ] })
@@ -4065,10 +5263,10 @@ function BulkTemplateModal({
4065
5263
  template.name
4066
5264
  ))
4067
5265
  ] }),
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(
5266
+ expiredCount === 0 ? /* @__PURE__ */ jsx27("p", { className: "cv-workspace-modal__warning", children: labels.templateModalNoneExpired }) : null,
5267
+ /* @__PURE__ */ jsxs25("footer", { className: "cv-workspace-modal__footer", children: [
5268
+ /* @__PURE__ */ jsx27("button", { "data-cv-tooltip": labels.templateModalCancel, "aria-label": labels.templateModalCancel, type: "button", onClick: onClose, children: labels.templateModalCancel }),
5269
+ /* @__PURE__ */ jsx27(
4072
5270
  "button",
4073
5271
  {
4074
5272
  "data-cv-tooltip": sending ? labels.templateModalSending : labels.templateModalSend(expiredCount),
@@ -4084,8 +5282,395 @@ function BulkTemplateModal({
4084
5282
  }
4085
5283
 
4086
5284
  // 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";
5285
+ import { useCallback as useCallback13, useEffect as useEffect17, useRef as useRef15, useState as useState23 } from "react";
5286
+
5287
+ // src/quickReplies/resolveConversationVariables.ts
5288
+ var MARKER_PATTERN = /^\{\{\s*([\w.]+)\s*\}\}$/;
5289
+ function variableNameOf(variable) {
5290
+ return MARKER_PATTERN.exec(variable.marker)?.[1] ?? variable.id;
5291
+ }
5292
+ function resolveConversationVariables({
5293
+ conversationVariables,
5294
+ quickReplyVariables,
5295
+ composerVariables
5296
+ }) {
5297
+ if (!conversationVariables) return { quickReplyVariables, composerVariables };
5298
+ const known = conversationVariables.filter((variable) => variable.value !== "");
5299
+ return {
5300
+ quickReplyVariables: Object.fromEntries(known.map((variable) => [variableNameOf(variable), variable.value])),
5301
+ composerVariables: known.map(({ id, label, value }) => ({ id, label, value }))
5302
+ };
5303
+ }
5304
+
5305
+ // src/workspace/QueuedAttachmentsList.tsx
5306
+ import { useEffect as useEffect15, useRef as useRef12, useState as useState20 } from "react";
5307
+ import { jsx as jsx28, jsxs as jsxs26 } from "react/jsx-runtime";
5308
+ function nameOf(item) {
5309
+ return item.kind === "local" ? item.file.name : item.filename;
5310
+ }
5311
+ function mimeTypeOf(item) {
5312
+ return item.kind === "local" ? item.file.type : item.mimeType;
5313
+ }
5314
+ function sizeOf(item) {
5315
+ return item.kind === "local" ? item.file.size : item.sizeBytes;
5316
+ }
5317
+ function AttachmentThumbnail({
5318
+ item,
5319
+ getThumbnailUrl
5320
+ }) {
5321
+ const isImage = mimeTypeOf(item).startsWith("image/");
5322
+ const [url, setUrl] = useState20(item.kind === "local" ? void 0 : item.previewUrl);
5323
+ const localFile = item.kind === "local" ? item.file : void 0;
5324
+ const uploadId = item.kind === "stored" ? item.uploadId : void 0;
5325
+ useEffect15(() => {
5326
+ if (!isImage || !localFile) return;
5327
+ const objectUrl = URL.createObjectURL(localFile);
5328
+ setUrl(objectUrl);
5329
+ return () => URL.revokeObjectURL(objectUrl);
5330
+ }, [isImage, localFile]);
5331
+ useEffect15(() => {
5332
+ if (!isImage || !uploadId || !getThumbnailUrl) return;
5333
+ let cancelled = false;
5334
+ getThumbnailUrl(uploadId).then((resolved) => {
5335
+ if (!cancelled) setUrl(resolved);
5336
+ }).catch(() => void 0);
5337
+ return () => {
5338
+ cancelled = true;
5339
+ };
5340
+ }, [isImage, uploadId, getThumbnailUrl]);
5341
+ if (!isImage) return null;
5342
+ return /* @__PURE__ */ jsx28("span", { className: "cv-attachment-item__thumbnail", "aria-hidden": "true", children: url ? /* @__PURE__ */ jsx28("img", { src: url, alt: "" }) : null });
5343
+ }
5344
+ function departedItemsOf(previouslyRendered, items) {
5345
+ const currentKeys = new Set(items.map(attachmentKey));
5346
+ return previouslyRendered.filter((item) => !currentKeys.has(attachmentKey(item)));
5347
+ }
5348
+ function useDepartingItems(items) {
5349
+ const [rendered, setRendered] = useState20(items);
5350
+ const [departingKeys, setDepartingKeys] = useState20(/* @__PURE__ */ new Set());
5351
+ const renderedRef = useRef12(rendered);
5352
+ renderedRef.current = rendered;
5353
+ useEffect15(() => {
5354
+ const removedItems = departedItemsOf(renderedRef.current, items);
5355
+ if (removedItems.length === 0) {
5356
+ setRendered(items);
5357
+ return;
5358
+ }
5359
+ setDepartingKeys(new Set(removedItems.map(attachmentKey)));
5360
+ const reduceMotion = typeof window !== "undefined" && window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
5361
+ const delay = reduceMotion ? 0 : 220;
5362
+ const timer = setTimeout(() => {
5363
+ setRendered(items);
5364
+ setDepartingKeys(/* @__PURE__ */ new Set());
5365
+ }, delay);
5366
+ return () => clearTimeout(timer);
5367
+ }, [items]);
5368
+ return { rendered: departingKeys.size > 0 ? [...rendered] : items, departingKeys };
5369
+ }
5370
+ function QueuedAttachmentsList({
5371
+ items,
5372
+ statusOf: statusOf2,
5373
+ onRemove,
5374
+ onRetry,
5375
+ retryingKeys,
5376
+ getThumbnailUrl,
5377
+ labels,
5378
+ busy
5379
+ }) {
5380
+ const { rendered, departingKeys } = useDepartingItems(items);
5381
+ if (rendered.length === 0) return null;
5382
+ const statusLabelOf = (status) => {
5383
+ if (status === "sending") return labels.sending;
5384
+ if (status === "sent") return labels.sent;
5385
+ if (status === "failed") return labels.failed;
5386
+ if (status === "skipped") return labels.skipped;
5387
+ return labels.waiting;
5388
+ };
5389
+ return /* @__PURE__ */ jsx28("ul", { className: "cv-workspace-attachments", "aria-busy": busy, "aria-live": "polite", children: rendered.map((item) => {
5390
+ const key = attachmentKey(item);
5391
+ const status = statusOf2(key);
5392
+ const isDeparting = departingKeys.has(key);
5393
+ return /* @__PURE__ */ jsxs26(
5394
+ "li",
5395
+ {
5396
+ className: `cv-attachment-item cv-attachment-item--${status}${isDeparting ? " cv-attachment-item--departing" : ""}`,
5397
+ children: [
5398
+ /* @__PURE__ */ jsx28(AttachmentThumbnail, { item, getThumbnailUrl }),
5399
+ /* @__PURE__ */ jsxs26("span", { className: "cv-attachment-item__info", children: [
5400
+ /* @__PURE__ */ jsx28("span", { className: "cv-attachment-item__name", children: nameOf(item) }),
5401
+ /* @__PURE__ */ jsxs26("span", { className: "cv-attachment-item__meta", children: [
5402
+ formatFileSize(sizeOf(item)),
5403
+ " \xB7 ",
5404
+ statusLabelOf(status)
5405
+ ] })
5406
+ ] }),
5407
+ (status === "failed" || status === "skipped") && onRetry ? /* @__PURE__ */ jsx28(
5408
+ "button",
5409
+ {
5410
+ type: "button",
5411
+ className: "cv-attachment-item__retry",
5412
+ disabled: busy || (retryingKeys?.has(key) ?? false),
5413
+ "aria-busy": retryingKeys?.has(key) ?? false,
5414
+ onClick: () => onRetry(item),
5415
+ children: labels.retry
5416
+ }
5417
+ ) : null,
5418
+ /* @__PURE__ */ jsx28(
5419
+ "button",
5420
+ {
5421
+ "data-cv-tooltip": labels.remove,
5422
+ type: "button",
5423
+ "aria-label": labels.remove,
5424
+ disabled: status === "sending",
5425
+ onClick: () => onRemove(item),
5426
+ children: "\u2715"
5427
+ }
5428
+ )
5429
+ ]
5430
+ },
5431
+ key
5432
+ );
5433
+ }) });
5434
+ }
5435
+
5436
+ // src/workspace/useComposerQueue.ts
5437
+ import { useCallback as useCallback12, useEffect as useEffect16, useRef as useRef14, useState as useState22 } from "react";
5438
+
5439
+ // src/workspace/useComposerAttachmentRetry.ts
5440
+ import { useCallback as useCallback11, useRef as useRef13, useState as useState21 } from "react";
5441
+ function useComposerAttachmentRetry(params) {
5442
+ const {
5443
+ conversationId,
5444
+ currentConversationIdRef,
5445
+ queue,
5446
+ setQueue,
5447
+ setAttachmentStatus,
5448
+ setSendFailure,
5449
+ api,
5450
+ onSendAttachments,
5451
+ labels,
5452
+ sendInFlightRef
5453
+ } = params;
5454
+ const retryIdempotencyKeyRef = useRef13(void 0);
5455
+ const retryingKeysRef = useRef13(/* @__PURE__ */ new Set());
5456
+ const [retryingKeys, setRetryingKeys] = useState21(/* @__PURE__ */ new Set());
5457
+ function markRetrying(key) {
5458
+ retryingKeysRef.current.add(key);
5459
+ setRetryingKeys(new Set(retryingKeysRef.current));
5460
+ }
5461
+ function unmarkRetrying(key) {
5462
+ retryingKeysRef.current.delete(key);
5463
+ setRetryingKeys(new Set(retryingKeysRef.current));
5464
+ }
5465
+ async function retryLocalAttachment(item) {
5466
+ if (!onSendAttachments) return;
5467
+ const key = attachmentKey(item);
5468
+ const conversationIdAtRetry = conversationId;
5469
+ const isSameConversation = () => currentConversationIdRef.current === conversationIdAtRetry;
5470
+ setAttachmentStatus((current) => ({ ...current, [key]: "sending" }));
5471
+ try {
5472
+ await onSendAttachments([item.file], "");
5473
+ if (!isSameConversation()) return;
5474
+ setAttachmentStatus((current) => ({ ...current, [key]: "sent" }));
5475
+ setQueue((current) => current.filter((queued) => attachmentKey(queued) !== key));
5476
+ } catch (error) {
5477
+ if (!isSameConversation()) return;
5478
+ setAttachmentStatus((current) => ({ ...current, [key]: "failed" }));
5479
+ setSendFailure(error instanceof Error ? error.message : labels.attachFailure);
5480
+ }
5481
+ }
5482
+ async function retryStoredAttachment(item) {
5483
+ const sendStoredAttachmentsApi = api.sendStoredAttachments;
5484
+ if (!sendStoredAttachmentsApi) return;
5485
+ const conversationIdAtRetry = conversationId;
5486
+ const isSameConversation = () => currentConversationIdRef.current === conversationIdAtRetry;
5487
+ const uploadIds = [item.uploadId];
5488
+ const idempotencyState = resolveIdempotencyKey(retryIdempotencyKeyRef.current, uploadIds);
5489
+ retryIdempotencyKeyRef.current = idempotencyState;
5490
+ try {
5491
+ const result = await retryStoredAttachments({
5492
+ queue,
5493
+ uploadIds,
5494
+ idempotencyKey: idempotencyState.key,
5495
+ sendStoredAttachments: (uploadParams) => sendStoredAttachmentsApi({ conversationId: conversationIdAtRetry, ...uploadParams }),
5496
+ onAttachmentStatus: (statusKey, status) => {
5497
+ if (isSameConversation()) setAttachmentStatus((current) => ({ ...current, [statusKey]: status }));
5498
+ }
5499
+ });
5500
+ setQueue(
5501
+ (current) => resolveRetryOutcome({
5502
+ conversationIdAtRetry,
5503
+ currentConversationId: currentConversationIdRef.current,
5504
+ sentAttachmentKeys: result.sentAttachmentKeys,
5505
+ queue: current
5506
+ }) ?? current
5507
+ );
5508
+ } catch (error) {
5509
+ if (isSameConversation()) setSendFailure(error instanceof Error ? error.message : labels.attachFailure);
5510
+ }
5511
+ }
5512
+ function retryQueuedAttachment(item) {
5513
+ if (sendInFlightRef.current) return;
5514
+ const key = attachmentKey(item);
5515
+ if (retryingKeysRef.current.has(key)) return;
5516
+ markRetrying(key);
5517
+ const release = () => unmarkRetrying(key);
5518
+ if (item.kind === "local") {
5519
+ void retryLocalAttachment(item).finally(release);
5520
+ return;
5521
+ }
5522
+ void retryStoredAttachment(item).finally(release);
5523
+ }
5524
+ const resetRetryState = useCallback11(() => {
5525
+ retryIdempotencyKeyRef.current = void 0;
5526
+ retryingKeysRef.current.clear();
5527
+ setRetryingKeys(/* @__PURE__ */ new Set());
5528
+ }, []);
5529
+ return { retryQueuedAttachment, resetRetryState, retryingKeys };
5530
+ }
5531
+
5532
+ // src/workspace/useComposerQueue.ts
5533
+ function useComposerQueue(params) {
5534
+ const { conversationId, draft, setDraft, api, labels, onSendAttachments, refetch, setSendFailure } = params;
5535
+ const [queue, setQueue] = useState22([]);
5536
+ const [attachmentStatus, setAttachmentStatus] = useState22({});
5537
+ const [isSendingDraft, setIsSendingDraft] = useState22(false);
5538
+ const sendInFlightRef = useRef14(false);
5539
+ const idempotencyKeyRef = useRef14(void 0);
5540
+ const currentConversationIdRef = useRef14(conversationId);
5541
+ const { retryQueuedAttachment, resetRetryState, retryingKeys } = useComposerAttachmentRetry({
5542
+ conversationId,
5543
+ currentConversationIdRef,
5544
+ queue,
5545
+ setQueue,
5546
+ setAttachmentStatus,
5547
+ setSendFailure,
5548
+ api,
5549
+ labels,
5550
+ sendInFlightRef,
5551
+ ...onSendAttachments ? { onSendAttachments } : {}
5552
+ });
5553
+ useEffect16(() => {
5554
+ currentConversationIdRef.current = conversationId;
5555
+ setQueue([]);
5556
+ setAttachmentStatus({});
5557
+ idempotencyKeyRef.current = void 0;
5558
+ sendInFlightRef.current = false;
5559
+ setIsSendingDraft(false);
5560
+ resetRetryState();
5561
+ }, [conversationId, resetRetryState]);
5562
+ const enqueueAttachments = useCallback12((items) => {
5563
+ if (items.length === 0) return;
5564
+ setQueue((current) => [...current, ...items]);
5565
+ }, []);
5566
+ const removeQueuedAttachment = useCallback12((item) => {
5567
+ setQueue((current) => current.filter((queued) => attachmentKey(queued) !== attachmentKey(item)));
5568
+ }, []);
5569
+ async function runSend(action, fallback) {
5570
+ setSendFailure(void 0);
5571
+ try {
5572
+ await action();
5573
+ await refetch();
5574
+ return true;
5575
+ } catch (error) {
5576
+ setSendFailure(error instanceof Error ? error.message : fallback);
5577
+ return false;
5578
+ }
5579
+ }
5580
+ async function sendLocalOnlyAttachments(isSameConversation) {
5581
+ if (!onSendAttachments) return;
5582
+ const files = excludeRetryingItems(queue, retryingKeys).filter((item) => item.kind === "local").map((item) => item.file);
5583
+ try {
5584
+ const didSend = await runSend(() => onSendAttachments(files, draft), labels.attachFailure);
5585
+ if (!isSameConversation()) return;
5586
+ if (didSend) {
5587
+ setQueue((current) => current.filter((item) => retryingKeys.has(attachmentKey(item))));
5588
+ setAttachmentStatus(
5589
+ (current) => Object.fromEntries(Object.entries(current).filter(([key]) => retryingKeys.has(key)))
5590
+ );
5591
+ idempotencyKeyRef.current = void 0;
5592
+ setDraft("");
5593
+ }
5594
+ } finally {
5595
+ if (isSameConversation()) {
5596
+ sendInFlightRef.current = false;
5597
+ setIsSendingDraft(false);
5598
+ }
5599
+ }
5600
+ }
5601
+ async function sendQueuedDraft(isSameConversation) {
5602
+ const sendStoredAttachmentsApi = api.sendStoredAttachments;
5603
+ const sendableQueue = excludeRetryingItems(queue, retryingKeys);
5604
+ const storedUploadIds = sendableQueue.filter((item) => item.kind === "stored").map((item) => item.uploadId);
5605
+ const idempotencyState = resolveIdempotencyKey(idempotencyKeyRef.current, storedUploadIds);
5606
+ idempotencyKeyRef.current = idempotencyState;
5607
+ try {
5608
+ const result = await sendQueuedMessage({
5609
+ text: draft,
5610
+ queue: sendableQueue,
5611
+ idempotencyKey: idempotencyState.key,
5612
+ sendText: (text) => runSend(() => api.sendMessage(conversationId, text), labels.sendFailure),
5613
+ ...sendStoredAttachmentsApi ? {
5614
+ sendStoredAttachments: (uploadParams) => sendStoredAttachmentsApi({ conversationId, ...uploadParams })
5615
+ } : {},
5616
+ ...onSendAttachments ? { sendLocalAttachments: (files) => onSendAttachments(files, "") } : {},
5617
+ onAttachmentStatus: (key, status) => {
5618
+ if (isSameConversation()) setAttachmentStatus((current) => ({ ...current, [key]: status }));
5619
+ }
5620
+ });
5621
+ if (!isSameConversation()) return;
5622
+ if (!result.textSent) return;
5623
+ const sentKeys = new Set(result.sentAttachmentKeys);
5624
+ let queueEmptyAfterSend = false;
5625
+ setQueue((current) => {
5626
+ const next = current.filter((item) => !sentKeys.has(attachmentKey(item)));
5627
+ queueEmptyAfterSend = next.length === 0;
5628
+ return next;
5629
+ });
5630
+ if (queueEmptyAfterSend) {
5631
+ idempotencyKeyRef.current = void 0;
5632
+ setAttachmentStatus({});
5633
+ }
5634
+ if (draft.trim()) setDraft("");
5635
+ await refetch();
5636
+ } catch (error) {
5637
+ if (isSameConversation()) setSendFailure(error instanceof Error ? error.message : labels.attachFailure);
5638
+ } finally {
5639
+ if (isSameConversation()) {
5640
+ sendInFlightRef.current = false;
5641
+ setIsSendingDraft(false);
5642
+ }
5643
+ }
5644
+ }
5645
+ async function handleRichSend() {
5646
+ if (sendInFlightRef.current) return;
5647
+ if (!draft.trim() && queue.length === 0) return;
5648
+ sendInFlightRef.current = true;
5649
+ setIsSendingDraft(true);
5650
+ setSendFailure(void 0);
5651
+ const conversationIdAtSend = conversationId;
5652
+ const isSameConversation = () => currentConversationIdRef.current === conversationIdAtSend;
5653
+ const hasStoredItems = queue.some((item) => item.kind === "stored");
5654
+ if (onSendAttachments && queue.length > 0 && !hasStoredItems) {
5655
+ await sendLocalOnlyAttachments(isSameConversation);
5656
+ return;
5657
+ }
5658
+ await sendQueuedDraft(isSameConversation);
5659
+ }
5660
+ return {
5661
+ queue,
5662
+ enqueueAttachments,
5663
+ attachmentStatus,
5664
+ isSendingDraft,
5665
+ handleRichSend,
5666
+ removeQueuedAttachment,
5667
+ retryQueuedAttachment,
5668
+ retryingKeys
5669
+ };
5670
+ }
5671
+
5672
+ // src/workspace/ConversationPane.tsx
5673
+ import { jsx as jsx29, jsxs as jsxs27 } from "react/jsx-runtime";
4089
5674
  function ConversationPane({
4090
5675
  conversation,
4091
5676
  now,
@@ -4108,6 +5693,7 @@ function ConversationPane({
4108
5693
  onAttach,
4109
5694
  composer = "simple",
4110
5695
  composerVariablesFor,
5696
+ conversationVariablesFor,
4111
5697
  onSendAttachments,
4112
5698
  onRecordAudio
4113
5699
  }) {
@@ -4118,17 +5704,44 @@ function ConversationPane({
4118
5704
  const { api } = context;
4119
5705
  const { messages, refetch } = useConversationMessages(conversation.id);
4120
5706
  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(() => {
5707
+ const [documentsOpen, setDocumentsOpen] = useState23(false);
5708
+ const [sendFailure, setSendFailure] = useState23(void 0);
5709
+ const [selectedMessageIds, setSelectedMessageIds] = useState23(/* @__PURE__ */ new Set());
5710
+ const [draft, setDraft] = useState23(initialComposerText ?? "");
5711
+ const {
5712
+ queue,
5713
+ enqueueAttachments,
5714
+ attachmentStatus,
5715
+ isSendingDraft,
5716
+ handleRichSend,
5717
+ removeQueuedAttachment,
5718
+ retryQueuedAttachment,
5719
+ retryingKeys
5720
+ } = useComposerQueue({
5721
+ conversationId: conversation.id,
5722
+ draft,
5723
+ setDraft,
5724
+ api,
5725
+ labels: { sendFailure: labels.sendFailure, attachFailure: labels.attachFailure },
5726
+ ...onSendAttachments ? { onSendAttachments } : {},
5727
+ refetch,
5728
+ setSendFailure
5729
+ });
5730
+ const thumbnailUrlCacheRef = useRef15(/* @__PURE__ */ new Map());
5731
+ const getQueuedAttachmentThumbnailUrl = useCallback13(
5732
+ (uploadId) => {
5733
+ const cached = thumbnailUrlCacheRef.current.get(uploadId);
5734
+ if (cached) return cached;
5735
+ const pending = api.getDocumentUrl(uploadId, "inline");
5736
+ thumbnailUrlCacheRef.current.set(uploadId, pending);
5737
+ pending.catch(() => thumbnailUrlCacheRef.current.delete(uploadId));
5738
+ return pending;
5739
+ },
5740
+ [api]
5741
+ );
5742
+ useEffect17(() => {
4129
5743
  setSelectedMessageIds(/* @__PURE__ */ new Set());
4130
5744
  setDraft(initialComposerText ?? "");
4131
- setQueuedFiles([]);
4132
5745
  }, [conversation.id, initialComposerText]);
4133
5746
  function toggleMessageSelected(messageId) {
4134
5747
  setSelectedMessageIds((current) => {
@@ -4170,28 +5783,6 @@ function ConversationPane({
4170
5783
  if (conversation.mode !== "human" && onTakeover) await onTakeover();
4171
5784
  await runSend(() => api.sendTemplate(conversation.id, {}), labels.sendFailure);
4172
5785
  }
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
5786
  async function handleAttach(file) {
4196
5787
  if (!onAttach) return;
4197
5788
  await runSend(() => onAttach(file), labels.attachFailure);
@@ -4207,15 +5798,33 @@ function ConversationPane({
4207
5798
  );
4208
5799
  }
4209
5800
  const contextEntries = contextEntriesOf?.(conversationContext);
4210
- const composerVariables = composerVariablesFor?.(conversation, conversationContext);
5801
+ const { quickReplyVariables, composerVariables } = resolveConversationVariables({
5802
+ conversationVariables: conversationVariablesFor?.(conversation, conversationContext),
5803
+ quickReplyVariables: quickReplyVariablesFor?.(conversation, conversationContext),
5804
+ composerVariables: composerVariablesFor?.(conversation, conversationContext)
5805
+ });
4211
5806
  const richQuickReplies = quickReplies?.map((reply) => ({
4212
5807
  id: reply.key,
4213
5808
  label: reply.label,
4214
- text: typeof reply.text === "string" ? applyQuickReplyVariables(reply.text, quickReplyVariablesFor?.(conversation, conversationContext) ?? {}) : reply.text(quickReplyVariablesFor?.(conversation, conversationContext) ?? {})
5809
+ text: typeof reply.text === "string" ? applyQuickReplyVariables(reply.text, quickReplyVariables ?? {}) : reply.text(quickReplyVariables ?? {})
4215
5810
  }));
5811
+ const savedQuickReplies = api.listQuickReplies ? {
5812
+ // Arrow em vez de repassar o método direto: `api.listQuickReplies` solto perde o `this` do
5813
+ // objeto que o implementa, e um cliente HTTP real costuma depender dele internamente.
5814
+ listQuickReplies: (params) => api.listQuickReplies(params),
5815
+ conversationId: conversation.id,
5816
+ variables: quickReplyVariables,
5817
+ hasAttachmentsCapability: Boolean(api.sendStoredAttachments),
5818
+ // Empurra os anexos da mensagem escolhida como itens guardados (QR-32) — sem a porta, a
5819
+ // linha do picker já avisou e o texto entra sozinho, sem silenciosamente perder o anexo.
5820
+ onSelect: (quickReply) => {
5821
+ const attachments = queuedAttachmentsFromQuickReply(quickReply, Boolean(api.sendStoredAttachments));
5822
+ enqueueAttachments(attachments);
5823
+ }
5824
+ } : void 0;
4216
5825
  const botOwnsConversation = Boolean(requireTakeoverToReply) && conversation.mode !== "human";
4217
- return /* @__PURE__ */ jsxs24("div", { className: "cv-workspace-pane", children: [
4218
- /* @__PURE__ */ jsx26(
5826
+ return /* @__PURE__ */ jsxs27("div", { className: "cv-workspace-pane", children: [
5827
+ /* @__PURE__ */ jsx29(
4219
5828
  ConversationHeader,
4220
5829
  {
4221
5830
  conversation,
@@ -4230,10 +5839,10 @@ function ConversationPane({
4230
5839
  ...extraUtilities ? { extraUtilities } : {}
4231
5840
  }
4232
5841
  ),
4233
- contextEntries && contextEntries.length > 0 || flowLabel ? /* @__PURE__ */ jsx26(ConversationContextPanel, { entries: contextEntries ?? [], ...flowLabel ? { flowLabel } : {} }) : null,
4234
- /* @__PURE__ */ jsx26(ConversationDocumentsPanel, { conversationId: conversation.id, open: documentsOpen }),
5842
+ contextEntries && contextEntries.length > 0 || flowLabel ? /* @__PURE__ */ jsx29(ConversationContextPanel, { entries: contextEntries ?? [], ...flowLabel ? { flowLabel } : {} }) : null,
5843
+ /* @__PURE__ */ jsx29(ConversationDocumentsPanel, { conversationId: conversation.id, open: documentsOpen }),
4235
5844
  renderAboveTranscript?.(conversation, conversationContext),
4236
- /* @__PURE__ */ jsx26(
5845
+ /* @__PURE__ */ jsx29(
4237
5846
  ConversationWallpaper,
4238
5847
  {
4239
5848
  ref: scroll.containerRef,
@@ -4242,9 +5851,9 @@ function ConversationPane({
4242
5851
  children: messages.map((message, index) => {
4243
5852
  const previous = index > 0 ? messages[index - 1] : void 0;
4244
5853
  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(
5854
+ return /* @__PURE__ */ jsxs27("div", { children: [
5855
+ startsNewDay ? /* @__PURE__ */ jsx29(DateDivider, { iso: message.timestamp }) : null,
5856
+ /* @__PURE__ */ jsx29(
4248
5857
  MessageBubble,
4249
5858
  {
4250
5859
  message,
@@ -4261,39 +5870,57 @@ function ConversationPane({
4261
5870
  })
4262
5871
  }
4263
5872
  ),
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 })
5873
+ messageSelection && selectedMessageIds.size > 0 ? /* @__PURE__ */ jsxs27("div", { className: "cv-workspace-selection", children: [
5874
+ /* @__PURE__ */ jsx29("span", { children: labels.messagesSelected(selectedMessageIds.size) }),
5875
+ /* @__PURE__ */ jsxs27("div", { className: "cv-workspace-selection__actions", children: [
5876
+ /* @__PURE__ */ jsx29(
5877
+ "button",
5878
+ {
5879
+ "data-cv-tooltip": labels.bulkClear,
5880
+ "aria-label": labels.bulkClear,
5881
+ type: "button",
5882
+ onClick: () => setSelectedMessageIds(/* @__PURE__ */ new Set()),
5883
+ children: labels.bulkClear
5884
+ }
5885
+ ),
5886
+ /* @__PURE__ */ jsx29(
5887
+ "button",
5888
+ {
5889
+ "data-cv-tooltip": labels.copySelected,
5890
+ "aria-label": labels.copySelected,
5891
+ type: "button",
5892
+ onClick: copySelectedMessages,
5893
+ children: labels.copySelected
5894
+ }
5895
+ )
4269
5896
  ] })
4270
5897
  ] }) : 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 ? (
5898
+ sendFailure ? /* @__PURE__ */ jsx29("p", { role: "alert", className: "cv-workspace-alert", children: sendFailure }) : null,
5899
+ blocked ? /* @__PURE__ */ jsx29(WindowExpiredNotice, { disabled: busy, onSendTemplate: () => void handleSendTemplate() }) : botOwnsConversation ? (
4279
5900
  // 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(
5901
+ /* @__PURE__ */ jsx29("p", { className: "cv-workspace-notice", children: labels.takeoverToReply })
5902
+ ) : composer === "rich" ? /* @__PURE__ */ jsx29(
4282
5903
  RichMessageComposer,
4283
5904
  {
4284
5905
  value: draft,
4285
5906
  onChange: setDraft,
4286
5907
  onSend: () => void handleRichSend(),
4287
- ...onSendAttachments ? { onAttachFiles: (files) => setQueuedFiles((current) => [...current, ...Array.from(files)]) } : onAttach ? {
5908
+ ...onSendAttachments ? {
5909
+ onAttachFiles: (files) => enqueueAttachments(
5910
+ Array.from(files).map(
5911
+ (file) => ({ kind: "local", localId: crypto.randomUUID(), file })
5912
+ )
5913
+ )
5914
+ } : onAttach ? {
4288
5915
  onAttachFiles: (files) => {
4289
5916
  for (const file of Array.from(files)) void handleAttach(file);
4290
5917
  }
4291
5918
  } : {},
4292
5919
  placeholder: labels.composerPlaceholder,
4293
5920
  isSending: busy || isSendingDraft,
4294
- hasQueuedAttachments: queuedFiles.length > 0,
5921
+ hasQueuedAttachments: queue.length > 0,
4295
5922
  ...onRecordAudio ? {
4296
- idleAction: /* @__PURE__ */ jsx26(
5923
+ idleAction: /* @__PURE__ */ jsx29(
4297
5924
  AudioRecorderButton,
4298
5925
  {
4299
5926
  onRecorded: (file) => void runSend(() => onRecordAudio(file), labels.recordFailure),
@@ -4301,25 +5928,34 @@ function ConversationPane({
4301
5928
  }
4302
5929
  )
4303
5930
  } : {},
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"
5931
+ ...queue.length > 0 ? {
5932
+ attachmentsPreview: /* @__PURE__ */ jsx29(
5933
+ QueuedAttachmentsList,
5934
+ {
5935
+ items: queue,
5936
+ statusOf: (key) => attachmentStatus[key] ?? "waiting",
5937
+ onRemove: removeQueuedAttachment,
5938
+ onRetry: retryQueuedAttachment,
5939
+ retryingKeys,
5940
+ getThumbnailUrl: getQueuedAttachmentThumbnailUrl,
5941
+ busy: isSendingDraft,
5942
+ labels: {
5943
+ remove: labels.attachmentRemove,
5944
+ waiting: labels.attachmentWaiting,
5945
+ sending: labels.attachmentSending,
5946
+ sent: labels.attachmentSent,
5947
+ failed: labels.attachmentFailed,
5948
+ skipped: labels.attachmentSkipped,
5949
+ retry: labels.attachmentRetry
4315
5950
  }
4316
- )
4317
- ] }, `${file.name}-${index}`)) })
5951
+ }
5952
+ )
4318
5953
  } : {},
4319
5954
  ...richQuickReplies ? { quickReplies: [...richQuickReplies] } : {},
4320
- ...composerVariables ? { variables: [...composerVariables] } : {}
5955
+ ...composerVariables ? { variables: [...composerVariables] } : {},
5956
+ ...savedQuickReplies ? { savedQuickReplies } : {}
4321
5957
  }
4322
- ) : /* @__PURE__ */ jsx26(
5958
+ ) : /* @__PURE__ */ jsx29(
4323
5959
  MessageComposer,
4324
5960
  {
4325
5961
  value: draft,
@@ -4328,14 +5964,15 @@ function ConversationPane({
4328
5964
  ...onAttach ? { onAttach: (file) => void handleAttach(file) } : {},
4329
5965
  placeholder: labels.composerPlaceholder,
4330
5966
  ...quickReplies ? { quickReplies } : {},
4331
- ...quickReplyVariablesFor ? { quickReplyVariables: quickReplyVariablesFor(conversation, conversationContext) } : {}
5967
+ ...quickReplyVariables ? { quickReplyVariables } : {},
5968
+ ...savedQuickReplies ? { savedQuickReplies } : {}
4332
5969
  }
4333
5970
  )
4334
5971
  ] });
4335
5972
  }
4336
5973
 
4337
5974
  // src/workspace/ConversationsInboxList.tsx
4338
- import { jsx as jsx27, jsxs as jsxs25 } from "react/jsx-runtime";
5975
+ import { jsx as jsx30, jsxs as jsxs28 } from "react/jsx-runtime";
4339
5976
  function ConversationsInboxList({
4340
5977
  inbox,
4341
5978
  labels,
@@ -4346,13 +5983,13 @@ function ConversationsInboxList({
4346
5983
  onSendTemplateToSelected
4347
5984
  }) {
4348
5985
  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(
5986
+ return /* @__PURE__ */ jsxs28("aside", { className, children: [
5987
+ hasSelection ? /* @__PURE__ */ jsxs28("div", { className: "cv-workspace-bulk", children: [
5988
+ /* @__PURE__ */ jsx30("span", { className: "cv-workspace-bulk__count", children: labels.bulkSelected(inbox.selectedIds.size) }),
5989
+ /* @__PURE__ */ jsxs28("div", { className: "cv-workspace-bulk__actions", children: [
5990
+ /* @__PURE__ */ jsx30("button", { "data-cv-tooltip": labels.bulkClear, "aria-label": labels.bulkClear, type: "button", onClick: inbox.clearBulkSelection, children: labels.bulkClear }),
5991
+ onSendTemplateToSelected ? /* @__PURE__ */ jsx30("button", { "data-cv-tooltip": labels.bulkTemplate, "aria-label": labels.bulkTemplate, type: "button", onClick: onSendTemplateToSelected, disabled: inbox.busy, children: labels.bulkTemplate }) : null,
5992
+ inbox.canFinalize ? /* @__PURE__ */ jsx30(
4356
5993
  "button",
4357
5994
  {
4358
5995
  "data-cv-tooltip": labels.bulkFinalize,
@@ -4370,8 +6007,8 @@ function ConversationsInboxList({
4370
6007
  renderBulkActions?.(inbox)
4371
6008
  ] })
4372
6009
  ] }) : null,
4373
- /* @__PURE__ */ jsxs25("div", { className: "cv-workspace-filters", children: [
4374
- /* @__PURE__ */ jsx27(
6010
+ /* @__PURE__ */ jsxs28("div", { className: "cv-workspace-filters", children: [
6011
+ /* @__PURE__ */ jsx30(
4375
6012
  "input",
4376
6013
  {
4377
6014
  type: "search",
@@ -4381,9 +6018,9 @@ function ConversationsInboxList({
4381
6018
  className: "cv-workspace-search"
4382
6019
  }
4383
6020
  ),
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(
6021
+ /* @__PURE__ */ jsxs28("div", { className: "cv-workspace-chips", children: [
6022
+ /* @__PURE__ */ jsx30("span", { className: "cv-workspace-chips__legend", children: labels.windowLegend }),
6023
+ WINDOW_FILTERS.map((filter) => /* @__PURE__ */ jsx30(
4387
6024
  "button",
4388
6025
  {
4389
6026
  type: "button",
@@ -4401,9 +6038,9 @@ function ConversationsInboxList({
4401
6038
  filter.value
4402
6039
  ))
4403
6040
  ] }),
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(
6041
+ inbox.channelFilters.length > 0 ? /* @__PURE__ */ jsxs28("div", { className: "cv-workspace-chips", children: [
6042
+ /* @__PURE__ */ jsx30("span", { className: "cv-workspace-chips__legend", children: labels.channelLegend }),
6043
+ inbox.channelFilters.map((filter) => /* @__PURE__ */ jsxs28(
4407
6044
  "button",
4408
6045
  {
4409
6046
  "data-cv-tooltip": filter.label,
@@ -4413,7 +6050,7 @@ function ConversationsInboxList({
4413
6050
  "aria-pressed": inbox.channelFilter === filter.value,
4414
6051
  className: `cv-workspace-chip${inbox.channelFilter === filter.value ? " cv-workspace-chip--on" : ""}`,
4415
6052
  children: [
4416
- filter.value === CHANNEL_FILTER_ALL ? null : /* @__PURE__ */ jsx27(ChannelIcon, { channel: filter.value }),
6053
+ filter.value === CHANNEL_FILTER_ALL ? null : /* @__PURE__ */ jsx30(ChannelIcon, { channel: filter.value }),
4417
6054
  filter.label
4418
6055
  ]
4419
6056
  },
@@ -4421,14 +6058,14 @@ function ConversationsInboxList({
4421
6058
  ))
4422
6059
  ] }) : null,
4423
6060
  renderFilters?.(inbox),
4424
- /* @__PURE__ */ jsxs25("label", { className: "cv-workspace-selectall", children: [
4425
- /* @__PURE__ */ jsx27("input", { type: "checkbox", checked: inbox.allOnPageSelected, onChange: inbox.toggleSelectAllOnPage }),
6061
+ /* @__PURE__ */ jsxs28("label", { className: "cv-workspace-selectall", children: [
6062
+ /* @__PURE__ */ jsx30("input", { type: "checkbox", checked: inbox.allOnPageSelected, onChange: inbox.toggleSelectAllOnPage }),
4426
6063
  labels.selectAll
4427
6064
  ] })
4428
6065
  ] }),
4429
- /* @__PURE__ */ jsxs25("div", { className: "cv-workspace-rows", children: [
6066
+ /* @__PURE__ */ jsxs28("div", { className: "cv-workspace-rows", children: [
4430
6067
  inbox.pageConversations.map(
4431
- (conversation) => renderRow ? /* @__PURE__ */ jsx27("div", { children: renderRow(conversation) }, conversation.id) : /* @__PURE__ */ jsx27(
6068
+ (conversation) => renderRow ? /* @__PURE__ */ jsx30("div", { children: renderRow(conversation) }, conversation.id) : /* @__PURE__ */ jsx30(
4432
6069
  ConversationRow,
4433
6070
  {
4434
6071
  conversation,
@@ -4443,13 +6080,13 @@ function ConversationsInboxList({
4443
6080
  conversation.id
4444
6081
  )
4445
6082
  ),
4446
- !inbox.loading && inbox.filteredCount === 0 ? /* @__PURE__ */ jsx27("p", { className: "cv-workspace-empty", children: labels.emptyList }) : null
6083
+ !inbox.loading && inbox.filteredCount === 0 ? /* @__PURE__ */ jsx30("p", { className: "cv-workspace-empty", children: labels.emptyList }) : null
4447
6084
  ] }),
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(
6085
+ /* @__PURE__ */ jsxs28("div", { className: "cv-workspace-pager", children: [
6086
+ /* @__PURE__ */ jsx30("span", { children: labels.rangeOf(inbox.firstOnPage, inbox.lastOnPage, inbox.filteredCount) }),
6087
+ /* @__PURE__ */ jsxs28("div", { className: "cv-workspace-pager__buttons", children: [
6088
+ /* @__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" }),
6089
+ /* @__PURE__ */ jsx30(
4453
6090
  "button",
4454
6091
  {
4455
6092
  type: "button",
@@ -4460,8 +6097,8 @@ function ConversationsInboxList({
4460
6097
  children: "\u2039"
4461
6098
  }
4462
6099
  ),
4463
- /* @__PURE__ */ jsx27("span", { children: labels.pageOf(inbox.page, inbox.pageCount) }),
4464
- /* @__PURE__ */ jsx27(
6100
+ /* @__PURE__ */ jsx30("span", { children: labels.pageOf(inbox.page, inbox.pageCount) }),
6101
+ /* @__PURE__ */ jsx30(
4465
6102
  "button",
4466
6103
  {
4467
6104
  type: "button",
@@ -4472,7 +6109,7 @@ function ConversationsInboxList({
4472
6109
  children: "\u203A"
4473
6110
  }
4474
6111
  ),
4475
- /* @__PURE__ */ jsx27(
6112
+ /* @__PURE__ */ jsx30(
4476
6113
  "button",
4477
6114
  {
4478
6115
  type: "button",
@@ -4521,6 +6158,12 @@ var DEFAULT_CONVERSATIONS_WORKSPACE_LABELS = {
4521
6158
  composerPlaceholder: "Responder como atendente\u2026",
4522
6159
  attachFailure: "Falha ao enviar o arquivo.",
4523
6160
  attachmentRemove: "Remover anexo",
6161
+ attachmentWaiting: "Aguardando",
6162
+ attachmentSending: "Enviando\u2026",
6163
+ attachmentSent: "Enviado \u2713",
6164
+ attachmentFailed: "Falhou",
6165
+ attachmentSkipped: "N\xE3o enviado \u2014 aguardando o anterior",
6166
+ attachmentRetry: "Tentar de novo",
4524
6167
  recordFailure: "Falha ao gravar o \xE1udio.",
4525
6168
  sendFailure: "Falha ao enviar a mensagem.",
4526
6169
  takeoverToReply: "Assuma o atendimento para responder diretamente ao cliente.",
@@ -4530,7 +6173,7 @@ var DEFAULT_CONVERSATIONS_WORKSPACE_LABELS = {
4530
6173
  };
4531
6174
 
4532
6175
  // src/workspace/useConversationsInbox.ts
4533
- import { useCallback as useCallback9, useEffect as useEffect13, useMemo as useMemo5, useRef as useRef10, useState as useState18 } from "react";
6176
+ import { useCallback as useCallback14, useEffect as useEffect18, useMemo as useMemo5, useRef as useRef16, useState as useState24 } from "react";
4534
6177
  var CONVERSATIONS_PER_PAGE = 50;
4535
6178
  function defaultDescribeFailure(error) {
4536
6179
  const status = error.status;
@@ -4548,14 +6191,14 @@ function useConversationsInbox(params = {}) {
4548
6191
  const perPage = params.perPage ?? CONVERSATIONS_PER_PAGE;
4549
6192
  const markReadOnOpen = params.markReadOnOpen ?? true;
4550
6193
  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);
6194
+ const [waitingOnly, setWaitingOnly] = useState24(false);
6195
+ const [windowFilter, setWindowFilter] = useState24(CONVERSATION_WINDOW.ALL);
6196
+ const [channelFilter, setChannelFilter] = useState24(CHANNEL_FILTER_ALL);
6197
+ const [search, setSearch] = useState24("");
6198
+ const [page, setPage] = useState24(1);
6199
+ const [selectedId, setSelectedId] = useState24(void 0);
6200
+ const [selectedIds, setSelectedIds] = useState24(/* @__PURE__ */ new Set());
6201
+ const [busy, setBusy] = useState24(false);
4559
6202
  const { conversations, total, loading, error, refetch } = useConversationList({
4560
6203
  ...waitingOnly ? { waitingHuman: true } : {},
4561
6204
  ...search ? { search } : {},
@@ -4563,7 +6206,7 @@ function useConversationsInbox(params = {}) {
4563
6206
  ...params.serverPaginated ? { page, limit: perPage } : {}
4564
6207
  });
4565
6208
  useGlobalRealtime(
4566
- useCallback9(() => {
6209
+ useCallback14(() => {
4567
6210
  void refetch();
4568
6211
  }, [refetch])
4569
6212
  );
@@ -4580,16 +6223,16 @@ function useConversationsInbox(params = {}) {
4580
6223
  const pageCount = Math.max(1, Math.ceil(totalForPaging / perPage));
4581
6224
  const currentPage = Math.min(page, pageCount);
4582
6225
  const pageConversations = params.serverPaginated ? filtered : filtered.slice((currentPage - 1) * perPage, currentPage * perPage);
4583
- useEffect13(() => {
6226
+ useEffect18(() => {
4584
6227
  setPage(1);
4585
6228
  }, [search, waitingOnly, windowFilter, channelFilter]);
4586
- useEffect13(() => {
6229
+ useEffect18(() => {
4587
6230
  if (selectedId && !conversations.some((conversation) => conversation.id === selectedId)) {
4588
6231
  setSelectedId(void 0);
4589
6232
  }
4590
6233
  }, [conversations, selectedId]);
4591
- const lastMarkReadAttempt = useRef10(void 0);
4592
- useEffect13(() => {
6234
+ const lastMarkReadAttempt = useRef16(void 0);
6235
+ useEffect18(() => {
4593
6236
  if (!markReadOnOpen || !selectedId) return;
4594
6237
  const opened = conversations.find((conversation) => conversation.id === selectedId);
4595
6238
  if (!opened || opened.unread === 0) return;
@@ -4599,7 +6242,7 @@ function useConversationsInbox(params = {}) {
4599
6242
  void api.markRead(selectedId).then(() => refetch()).catch(() => {
4600
6243
  });
4601
6244
  }, [markReadOnOpen, selectedId, conversations, refetch, api]);
4602
- const toggleSelected = useCallback9((conversationId) => {
6245
+ const toggleSelected = useCallback14((conversationId) => {
4603
6246
  setSelectedIds((current) => {
4604
6247
  const next = new Set(current);
4605
6248
  if (next.has(conversationId)) next.delete(conversationId);
@@ -4609,7 +6252,7 @@ function useConversationsInbox(params = {}) {
4609
6252
  }, []);
4610
6253
  const pageIds = pageConversations.map((conversation) => conversation.id);
4611
6254
  const allOnPageSelected = pageIds.length > 0 && pageIds.every((id) => selectedIds.has(id));
4612
- const toggleSelectAllOnPage = useCallback9(() => {
6255
+ const toggleSelectAllOnPage = useCallback14(() => {
4613
6256
  setSelectedIds((current) => {
4614
6257
  const next = new Set(current);
4615
6258
  if (allOnPageSelected) pageIds.forEach((id) => next.delete(id));
@@ -4617,7 +6260,7 @@ function useConversationsInbox(params = {}) {
4617
6260
  return next;
4618
6261
  });
4619
6262
  }, [allOnPageSelected, pageIds]);
4620
- const runOnSelection = useCallback9(
6263
+ const runOnSelection = useCallback14(
4621
6264
  async (action) => {
4622
6265
  setBusy(true);
4623
6266
  try {
@@ -4630,7 +6273,7 @@ function useConversationsInbox(params = {}) {
4630
6273
  },
4631
6274
  [selectedIds, refetch]
4632
6275
  );
4633
- const runConversationAction = useCallback9(
6276
+ const runConversationAction = useCallback14(
4634
6277
  async (action, conversationId) => {
4635
6278
  if (!action) return;
4636
6279
  setBusy(true);
@@ -4712,7 +6355,7 @@ function useConversationsInbox(params = {}) {
4712
6355
  }
4713
6356
 
4714
6357
  // src/workspace/ConversationsWorkspace.tsx
4715
- import { Fragment as Fragment7, jsx as jsx28, jsxs as jsxs26 } from "react/jsx-runtime";
6358
+ import { Fragment as Fragment8, jsx as jsx31, jsxs as jsxs29 } from "react/jsx-runtime";
4716
6359
  function ConversationsWorkspace({
4717
6360
  labels: labelsOverride,
4718
6361
  filters,
@@ -4731,6 +6374,7 @@ function ConversationsWorkspace({
4731
6374
  initialComposerText,
4732
6375
  composer,
4733
6376
  composerVariablesFor,
6377
+ conversationVariablesFor,
4734
6378
  onSendAttachments,
4735
6379
  onRecordAudio,
4736
6380
  contextEntriesOf,
@@ -4752,10 +6396,10 @@ function ConversationsWorkspace({
4752
6396
  ...markReadOnOpen === void 0 ? {} : { markReadOnOpen },
4753
6397
  ...serverPaginated ? { serverPaginated } : {}
4754
6398
  });
4755
- const [simulatorOpen, setSimulatorOpen] = useState19(false);
4756
- const [templateModalOpen, setTemplateModalOpen] = useState19(false);
4757
- const [openedFromLink, setOpenedFromLink] = useState19(void 0);
4758
- useEffect14(() => {
6399
+ const [simulatorOpen, setSimulatorOpen] = useState25(false);
6400
+ const [templateModalOpen, setTemplateModalOpen] = useState25(false);
6401
+ const [openedFromLink, setOpenedFromLink] = useState25(void 0);
6402
+ useEffect19(() => {
4759
6403
  const link = initialConversationId ?? initialWhatsappNumber;
4760
6404
  if (!link || openedFromLink === link) return;
4761
6405
  const target = inbox.conversations.find(
@@ -4787,50 +6431,50 @@ function ConversationsWorkspace({
4787
6431
  ...fromProduct,
4788
6432
  {
4789
6433
  key: "simulator",
4790
- icon: simulator?.icon ?? /* @__PURE__ */ jsx28(FlaskConical, { size: ICON_SIZE_ACTION }),
6434
+ icon: simulator?.icon ?? /* @__PURE__ */ jsx31(FlaskConical, { size: ICON_SIZE_ACTION }),
4791
6435
  label: simulator?.label ?? "Simular cliente",
4792
6436
  active: simulatorOpen,
4793
6437
  run: () => setSimulatorOpen((open) => !open)
4794
6438
  }
4795
6439
  ];
4796
6440
  }, [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" }),
6441
+ return /* @__PURE__ */ jsxs29("div", { className: `cv-workspace${className ? ` ${className}` : ""}`, children: [
6442
+ /* @__PURE__ */ jsx31(TooltipLayer, {}),
6443
+ /* @__PURE__ */ jsxs29("header", { className: "cv-workspace-header", children: [
6444
+ /* @__PURE__ */ jsxs29("div", { className: "cv-workspace-header__titles", children: [
6445
+ /* @__PURE__ */ jsx31("h1", { children: labels.title }),
6446
+ /* @__PURE__ */ jsxs29("p", { children: [
6447
+ /* @__PURE__ */ jsxs29("span", { "data-cv-tooltip": labels.conversations, className: "cv-stat", children: [
6448
+ /* @__PURE__ */ jsx31(MessagesSquare, { size: ICON_SIZE_INLINE, "aria-hidden": "true" }),
4805
6449
  " ",
4806
6450
  inbox.totalCount,
4807
- /* @__PURE__ */ jsxs26("span", { className: "cv-only-wide", children: [
6451
+ /* @__PURE__ */ jsxs29("span", { className: "cv-only-wide", children: [
4808
6452
  " ",
4809
6453
  labels.conversations
4810
6454
  ] })
4811
6455
  ] }),
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" }),
6456
+ /* @__PURE__ */ jsxs29("span", { "data-cv-tooltip": labels.waiting, className: "cv-stat cv-workspace-header__waiting", children: [
6457
+ /* @__PURE__ */ jsx31(Hourglass2, { size: ICON_SIZE_INLINE, "aria-hidden": "true" }),
4814
6458
  " ",
4815
6459
  inbox.waitingCount,
4816
- /* @__PURE__ */ jsxs26("span", { className: "cv-only-wide", children: [
6460
+ /* @__PURE__ */ jsxs29("span", { className: "cv-only-wide", children: [
4817
6461
  " ",
4818
6462
  labels.waiting
4819
6463
  ] })
4820
6464
  ] }),
4821
- /* @__PURE__ */ jsxs26("span", { "data-cv-tooltip": labels.unread, className: "cv-stat", children: [
4822
- /* @__PURE__ */ jsx28(Mail2, { size: ICON_SIZE_INLINE, "aria-hidden": "true" }),
6465
+ /* @__PURE__ */ jsxs29("span", { "data-cv-tooltip": labels.unread, className: "cv-stat", children: [
6466
+ /* @__PURE__ */ jsx31(Mail2, { size: ICON_SIZE_INLINE, "aria-hidden": "true" }),
4823
6467
  " ",
4824
6468
  inbox.unreadCount,
4825
- /* @__PURE__ */ jsxs26("span", { className: "cv-only-wide", children: [
6469
+ /* @__PURE__ */ jsxs29("span", { className: "cv-only-wide", children: [
4826
6470
  " ",
4827
6471
  labels.unread
4828
6472
  ] })
4829
6473
  ] })
4830
6474
  ] })
4831
6475
  ] }),
4832
- /* @__PURE__ */ jsxs26("div", { className: "cv-workspace-header__actions", children: [
4833
- /* @__PURE__ */ jsxs26(
6476
+ /* @__PURE__ */ jsxs29("div", { className: "cv-workspace-header__actions", children: [
6477
+ /* @__PURE__ */ jsxs29(
4834
6478
  "button",
4835
6479
  {
4836
6480
  type: "button",
@@ -4840,12 +6484,12 @@ function ConversationsWorkspace({
4840
6484
  "aria-label": labels.waitingOnly,
4841
6485
  className: inbox.waitingOnly ? "cv-workspace-toggle cv-workspace-toggle--on" : "cv-workspace-toggle",
4842
6486
  children: [
4843
- /* @__PURE__ */ jsx28(Hourglass2, { size: ICON_SIZE_ACTION, "aria-hidden": "true" }),
4844
- /* @__PURE__ */ jsx28("span", { className: "cv-only-wide", children: labels.waitingOnly })
6487
+ /* @__PURE__ */ jsx31(Hourglass2, { size: ICON_SIZE_ACTION, "aria-hidden": "true" }),
6488
+ /* @__PURE__ */ jsx31("span", { className: "cv-only-wide", children: labels.waitingOnly })
4845
6489
  ]
4846
6490
  }
4847
6491
  ),
4848
- /* @__PURE__ */ jsxs26(
6492
+ /* @__PURE__ */ jsxs29(
4849
6493
  "button",
4850
6494
  {
4851
6495
  type: "button",
@@ -4855,9 +6499,9 @@ function ConversationsWorkspace({
4855
6499
  "aria-label": labels.markSelectedAsRead,
4856
6500
  className: "cv-workspace-toggle",
4857
6501
  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: [
6502
+ /* @__PURE__ */ jsx31(Check2, { size: ICON_SIZE_ACTION, "aria-hidden": "true" }),
6503
+ /* @__PURE__ */ jsx31("span", { className: "cv-only-wide", children: labels.markSelectedAsRead }),
6504
+ inbox.selectedIds.size > 0 ? /* @__PURE__ */ jsxs29("span", { children: [
4861
6505
  "(",
4862
6506
  inbox.selectedIds.size,
4863
6507
  ")"
@@ -4865,7 +6509,7 @@ function ConversationsWorkspace({
4865
6509
  ]
4866
6510
  }
4867
6511
  ),
4868
- inbox.canMarkAllRead && inbox.unreadCount > 0 ? /* @__PURE__ */ jsxs26(
6512
+ inbox.canMarkAllRead && inbox.unreadCount > 0 ? /* @__PURE__ */ jsxs29(
4869
6513
  "button",
4870
6514
  {
4871
6515
  type: "button",
@@ -4875,28 +6519,28 @@ function ConversationsWorkspace({
4875
6519
  "aria-label": labels.markAllAsRead,
4876
6520
  className: "cv-workspace-toggle",
4877
6521
  children: [
4878
- /* @__PURE__ */ jsx28(CheckCheck, { size: ICON_SIZE_ACTION, "aria-hidden": "true" }),
4879
- /* @__PURE__ */ jsx28("span", { className: "cv-only-wide", children: labels.markAllAsRead })
6522
+ /* @__PURE__ */ jsx31(CheckCheck, { size: ICON_SIZE_ACTION, "aria-hidden": "true" }),
6523
+ /* @__PURE__ */ jsx31("span", { className: "cv-only-wide", children: labels.markAllAsRead })
4880
6524
  ]
4881
6525
  }
4882
6526
  ) : null,
4883
6527
  renderHeaderActions?.(inbox)
4884
6528
  ] })
4885
6529
  ] }),
4886
- inbox.loadFailure ? /* @__PURE__ */ jsxs26("p", { role: "alert", className: "cv-workspace-failure", children: [
6530
+ inbox.loadFailure ? /* @__PURE__ */ jsxs29("p", { role: "alert", className: "cv-workspace-failure", children: [
4887
6531
  inbox.loadFailure,
4888
- signInHref ? /* @__PURE__ */ jsxs26(Fragment7, { children: [
6532
+ signInHref ? /* @__PURE__ */ jsxs29(Fragment8, { children: [
4889
6533
  " ",
4890
- /* @__PURE__ */ jsx28("a", { href: signInHref, className: "cv-workspace-failure__link", children: labels.signIn })
6534
+ /* @__PURE__ */ jsx31("a", { href: signInHref, className: "cv-workspace-failure__link", children: labels.signIn })
4891
6535
  ] }) : null
4892
6536
  ] }) : null,
4893
- /* @__PURE__ */ jsxs26(
6537
+ /* @__PURE__ */ jsxs29(
4894
6538
  "div",
4895
6539
  {
4896
6540
  className: `cv-workspace-grid${showSimulator ? " cv-workspace-grid--with-simulator" : ""}`,
4897
6541
  "data-detail": selected ? "open" : "closed",
4898
6542
  children: [
4899
- /* @__PURE__ */ jsx28(
6543
+ /* @__PURE__ */ jsx31(
4900
6544
  ConversationsInboxList,
4901
6545
  {
4902
6546
  inbox,
@@ -4908,7 +6552,7 @@ function ConversationsWorkspace({
4908
6552
  ...onSendTemplateToSelected ? { onSendTemplateToSelected: () => onSendTemplateToSelected(inbox) } : inbox.canListTemplates ? { onSendTemplateToSelected: () => setTemplateModalOpen(true) } : {}
4909
6553
  }
4910
6554
  ),
4911
- /* @__PURE__ */ jsx28("section", { className: "cv-workspace-detail", children: selected ? /* @__PURE__ */ jsx28(
6555
+ /* @__PURE__ */ jsx31("section", { className: "cv-workspace-detail", children: selected ? /* @__PURE__ */ jsx31(
4912
6556
  ConversationPane,
4913
6557
  {
4914
6558
  conversation: selected,
@@ -4925,6 +6569,7 @@ function ConversationsWorkspace({
4925
6569
  ...initialComposerText ? { initialComposerText } : {},
4926
6570
  ...composer ? { composer } : {},
4927
6571
  ...composerVariablesFor ? { composerVariablesFor } : {},
6572
+ ...conversationVariablesFor ? { conversationVariablesFor } : {},
4928
6573
  ...onSendAttachments ? {
4929
6574
  onSendAttachments: (files, caption) => onSendAttachments(selected, files, caption)
4930
6575
  } : {},
@@ -4937,14 +6582,14 @@ function ConversationsWorkspace({
4937
6582
  ...renderAboveTranscript ? { renderAboveTranscript } : {},
4938
6583
  ...onAttach ? { onAttach: (file) => onAttach(selected, file) } : {}
4939
6584
  }
4940
- ) : /* @__PURE__ */ jsx28("p", { className: "cv-workspace-empty", children: labels.emptyDetail }) }),
6585
+ ) : /* @__PURE__ */ jsx31("p", { className: "cv-workspace-empty", children: labels.emptyDetail }) }),
4941
6586
  showSimulator && selected ? (
4942
6587
  // `min-height:0` junto do `min-width:0`: sem isso a linha do grid cresce com o conteúdo do
4943
6588
  // 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 ? (
6589
+ /* @__PURE__ */ jsx31("div", { className: "cv-workspace-simulator", children: simulator?.render ? simulator.render({ conversationId: selected.id, channel, close: () => setSimulatorOpen(false) }) : simulatorClient && conversations ? (
4945
6590
  // `key` pela conversa: trocar de contato sem remontar deixaria o transcript e o campo
4946
6591
  // de texto do contato anterior na tela.
4947
- /* @__PURE__ */ jsx28(
6592
+ /* @__PURE__ */ jsx31(
4948
6593
  ConversationSimulatorPanel,
4949
6594
  {
4950
6595
  client: simulatorClient,
@@ -4965,7 +6610,7 @@ function ConversationsWorkspace({
4965
6610
  ]
4966
6611
  }
4967
6612
  ),
4968
- templateModalOpen ? /* @__PURE__ */ jsx28(
6613
+ templateModalOpen ? /* @__PURE__ */ jsx31(
4969
6614
  BulkTemplateModal,
4970
6615
  {
4971
6616
  labels,
@@ -5018,8 +6663,11 @@ export {
5018
6663
  DEFAULT_EMOJI_PICKER_LABELS,
5019
6664
  DEFAULT_INTERACTIVE_MESSAGE_LABELS,
5020
6665
  DEFAULT_LIGHTBOX_LABELS,
6666
+ DEFAULT_MAX_ATTACHMENT_SIZE_BYTES,
5021
6667
  DEFAULT_MAX_RECORDING_MILLISECONDS,
5022
6668
  DEFAULT_MESSAGE_COMPOSER_LABELS,
6669
+ DEFAULT_QUICK_REPLIES_PICKER_LABELS,
6670
+ DEFAULT_QUICK_REPLIES_WORKSPACE_LABELS,
5023
6671
  DEFAULT_RICH_COMPOSER_TOOLTIPS,
5024
6672
  DEFAULT_TEMPLATES_SETTINGS_LABELS,
5025
6673
  DEFAULT_WHATSAPP_MESSAGE_EDITOR_LABELS,
@@ -5045,6 +6693,10 @@ export {
5045
6693
  MessagesWorkspace,
5046
6694
  MultiSelectFilter,
5047
6695
  NARROW_MAX_WIDTH_PX,
6696
+ QUICK_REPLY_ATTACHMENT_LIMIT,
6697
+ QueuedAttachmentsList,
6698
+ QuickRepliesPicker,
6699
+ QuickRepliesWorkspace,
5048
6700
  REOPEN_MECHANISM,
5049
6701
  REPLY_LATENCY,
5050
6702
  REPLY_LATENCY_CRITICAL_HOURS,
@@ -5075,19 +6727,24 @@ export {
5075
6727
  contactFlag,
5076
6728
  createMediaUrlResolver,
5077
6729
  downloadTextFile,
6730
+ filterQuickReplies,
5078
6731
  formatContactHandle,
5079
6732
  formatDateTime,
5080
6733
  formatFileSize,
5081
6734
  formatPhone,
5082
6735
  formatStalledFor,
5083
6736
  formatTimestamp,
6737
+ highlightMatch,
5084
6738
  htmlToWA,
5085
6739
  isReplyOverdue,
5086
6740
  isSameDay,
5087
6741
  isWindowBlocking,
6742
+ normalizeForSearch,
5088
6743
  parseWhatsAppFormatting,
5089
6744
  phoneInitials,
6745
+ queuedAttachmentsFromQuickReply,
5090
6746
  replyLatencyOf,
6747
+ resolveMaxAttachmentSizeBytes,
5091
6748
  resolveQuickReply,
5092
6749
  searchEmojis,
5093
6750
  toast,