@adatechnology/conversations-ui 0.1.0 → 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.
- package/dist/{ConversationSimulatorPanel--5fIzXWY.d.ts → ConversationSimulatorPanel-ikgmlciE.d.ts} +103 -1
- package/dist/{chunk-BJNRLLDO.js → chunk-NHQDQJL2.js} +769 -244
- package/dist/index.d.ts +304 -9
- package/dist/index.js +2166 -509
- package/dist/preview/index.d.ts +2 -2
- package/dist/preview/index.js +147 -1
- package/dist/styles.css +135 -0
- package/package.json +2 -2
- package/src/MessageComposer.test.tsx +14 -0
- package/src/MessageComposer.tsx +327 -73
- package/src/RichMessageComposer.test.tsx +22 -4
- package/src/RichMessageComposer.tsx +674 -370
- package/src/index.ts +28 -1
- package/src/preview/createMockConversationsApi.ts +184 -0
- package/src/providers/types.ts +34 -0
- package/src/quickReplies/AttachmentsFormSection.tsx +160 -0
- package/src/quickReplies/QuickRepliesPicker.test.tsx +114 -0
- package/src/quickReplies/QuickRepliesPicker.tsx +188 -0
- package/src/quickReplies/QuickRepliesWorkspace.test.tsx +32 -0
- package/src/quickReplies/QuickRepliesWorkspace.tsx +395 -0
- package/src/quickReplies/createUploadQueue.test.ts +106 -0
- package/src/quickReplies/createUploadQueue.ts +69 -0
- package/src/quickReplies/index.ts +5 -0
- package/src/quickReplies/labels.ts +124 -0
- package/src/quickReplies/quickReply.types.ts +74 -0
- package/src/quickReplies/quickReplyAttachmentUpload.test.ts +76 -0
- package/src/quickReplies/quickReplyAttachmentUpload.ts +81 -0
- package/src/quickReplies/quickReplyAttachments.test.ts +396 -0
- package/src/quickReplies/quickReplyAttachments.ts +289 -0
- package/src/quickReplies/quickReplySearch.test.ts +88 -0
- package/src/quickReplies/quickReplySearch.ts +104 -0
- package/src/quickReplies/quickReplyShortcut.test.ts +38 -0
- package/src/quickReplies/quickReplyShortcut.ts +46 -0
- package/src/quickReplies/resolveConversationVariables.test.ts +63 -0
- package/src/quickReplies/resolveConversationVariables.ts +41 -0
- package/src/quickReplies/useQuickRepliesPicker.test.ts +20 -0
- package/src/quickReplies/useQuickRepliesPicker.ts +121 -0
- package/src/quickReplies/useQuickRepliesWorkspace.test.ts +222 -0
- package/src/quickReplies/useQuickRepliesWorkspace.ts +426 -0
- package/src/quickReplies/useQuickReplyAttachmentUploads.ts +159 -0
- package/src/styles.css +87 -0
- package/src/workspace/ConversationPane.tsx +137 -73
- package/src/workspace/ConversationsWorkspace.tsx +16 -1
- package/src/workspace/QueuedAttachmentsList.test.ts +27 -0
- package/src/workspace/QueuedAttachmentsList.tsx +198 -0
- package/src/workspace/index.ts +1 -0
- package/src/workspace/labels.ts +14 -0
- package/src/workspace/useComposerAttachmentRetry.ts +155 -0
- 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
|
-
|
|
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 {
|
|
84
|
-
|
|
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(
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
const
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
const
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
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
|
-
|
|
432
|
-
|
|
433
|
-
},
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
const
|
|
462
|
-
const
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
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
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
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
|
-
|
|
518
|
+
[emitChange]
|
|
531
519
|
);
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
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
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
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
|
-
|
|
550
|
-
)
|
|
551
|
-
)
|
|
552
|
-
/* @__PURE__ */ jsxs2("div", {
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
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":
|
|
591
|
-
"aria-label":
|
|
592
|
-
onClick: () =>
|
|
593
|
-
|
|
594
|
-
|
|
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
|
-
|
|
603
|
-
)) })
|
|
604
|
-
|
|
605
|
-
|
|
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
|
-
"
|
|
843
|
+
"div",
|
|
608
844
|
{
|
|
609
|
-
ref:
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
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:
|
|
622
|
-
|
|
623
|
-
"
|
|
624
|
-
|
|
625
|
-
|
|
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
|
-
] })
|
|
629
|
-
|
|
630
|
-
|
|
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
|
|
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: () =>
|
|
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/
|
|
3992
|
-
|
|
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/
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
|
|
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
|
-
|
|
4001
|
-
|
|
4002
|
-
|
|
4003
|
-
onSend
|
|
4420
|
+
attachmentSizeLimits,
|
|
4421
|
+
attachmentsCount,
|
|
4422
|
+
onUploaded
|
|
4004
4423
|
}) {
|
|
4005
|
-
const
|
|
4006
|
-
|
|
4007
|
-
|
|
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
|
-
|
|
4010
|
-
|
|
4011
|
-
|
|
4012
|
-
|
|
4013
|
-
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
|
|
4020
|
-
|
|
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
|
-
}
|
|
4023
|
-
|
|
4024
|
-
|
|
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__ */
|
|
4029
|
-
/* @__PURE__ */
|
|
4030
|
-
/* @__PURE__ */
|
|
4031
|
-
/* @__PURE__ */
|
|
4032
|
-
/* @__PURE__ */
|
|
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__ */
|
|
5232
|
+
/* @__PURE__ */ jsx27("button", { "data-cv-tooltip": labels.templateModalCancel, type: "button", onClick: onClose, "aria-label": labels.templateModalCancel, children: "\u2715" })
|
|
4035
5233
|
] }),
|
|
4036
|
-
/* @__PURE__ */
|
|
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__ */
|
|
4047
|
-
filtered.length === 0 ? /* @__PURE__ */
|
|
4048
|
-
filtered.map((template) => /* @__PURE__ */
|
|
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__ */
|
|
4059
|
-
/* @__PURE__ */
|
|
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__ */
|
|
4069
|
-
/* @__PURE__ */
|
|
4070
|
-
/* @__PURE__ */
|
|
4071
|
-
/* @__PURE__ */
|
|
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
|
|
4088
|
-
|
|
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] =
|
|
4122
|
-
const [sendFailure, setSendFailure] =
|
|
4123
|
-
const [selectedMessageIds, setSelectedMessageIds] =
|
|
4124
|
-
const [draft, setDraft] =
|
|
4125
|
-
const
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
|
|
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 =
|
|
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,
|
|
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__ */
|
|
4218
|
-
/* @__PURE__ */
|
|
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__ */
|
|
4234
|
-
/* @__PURE__ */
|
|
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__ */
|
|
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__ */
|
|
4246
|
-
startsNewDay ? /* @__PURE__ */
|
|
4247
|
-
/* @__PURE__ */
|
|
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__ */
|
|
4265
|
-
/* @__PURE__ */
|
|
4266
|
-
/* @__PURE__ */
|
|
4267
|
-
/* @__PURE__ */
|
|
4268
|
-
|
|
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__ */
|
|
4272
|
-
blocked ? /* @__PURE__ */
|
|
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__ */
|
|
4281
|
-
) : composer === "rich" ? /* @__PURE__ */
|
|
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 ? {
|
|
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:
|
|
5921
|
+
hasQueuedAttachments: queue.length > 0,
|
|
4295
5922
|
...onRecordAudio ? {
|
|
4296
|
-
idleAction: /* @__PURE__ */
|
|
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
|
-
...
|
|
4305
|
-
attachmentsPreview: /* @__PURE__ */
|
|
4306
|
-
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
|
|
4311
|
-
|
|
4312
|
-
|
|
4313
|
-
|
|
4314
|
-
|
|
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
|
-
|
|
5951
|
+
}
|
|
5952
|
+
)
|
|
4318
5953
|
} : {},
|
|
4319
5954
|
...richQuickReplies ? { quickReplies: [...richQuickReplies] } : {},
|
|
4320
|
-
...composerVariables ? { variables: [...composerVariables] } : {}
|
|
5955
|
+
...composerVariables ? { variables: [...composerVariables] } : {},
|
|
5956
|
+
...savedQuickReplies ? { savedQuickReplies } : {}
|
|
4321
5957
|
}
|
|
4322
|
-
) : /* @__PURE__ */
|
|
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
|
-
...
|
|
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
|
|
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__ */
|
|
4350
|
-
hasSelection ? /* @__PURE__ */
|
|
4351
|
-
/* @__PURE__ */
|
|
4352
|
-
/* @__PURE__ */
|
|
4353
|
-
/* @__PURE__ */
|
|
4354
|
-
onSendTemplateToSelected ? /* @__PURE__ */
|
|
4355
|
-
inbox.canFinalize ? /* @__PURE__ */
|
|
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__ */
|
|
4374
|
-
/* @__PURE__ */
|
|
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__ */
|
|
4385
|
-
/* @__PURE__ */
|
|
4386
|
-
WINDOW_FILTERS.map((filter) => /* @__PURE__ */
|
|
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__ */
|
|
4405
|
-
/* @__PURE__ */
|
|
4406
|
-
inbox.channelFilters.map((filter) => /* @__PURE__ */
|
|
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__ */
|
|
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__ */
|
|
4425
|
-
/* @__PURE__ */
|
|
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__ */
|
|
6066
|
+
/* @__PURE__ */ jsxs28("div", { className: "cv-workspace-rows", children: [
|
|
4430
6067
|
inbox.pageConversations.map(
|
|
4431
|
-
(conversation) => renderRow ? /* @__PURE__ */
|
|
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__ */
|
|
6083
|
+
!inbox.loading && inbox.filteredCount === 0 ? /* @__PURE__ */ jsx30("p", { className: "cv-workspace-empty", children: labels.emptyList }) : null
|
|
4447
6084
|
] }),
|
|
4448
|
-
/* @__PURE__ */
|
|
4449
|
-
/* @__PURE__ */
|
|
4450
|
-
/* @__PURE__ */
|
|
4451
|
-
/* @__PURE__ */
|
|
4452
|
-
/* @__PURE__ */
|
|
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__ */
|
|
4464
|
-
/* @__PURE__ */
|
|
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__ */
|
|
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
|
|
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] =
|
|
4552
|
-
const [windowFilter, setWindowFilter] =
|
|
4553
|
-
const [channelFilter, setChannelFilter] =
|
|
4554
|
-
const [search, setSearch] =
|
|
4555
|
-
const [page, setPage] =
|
|
4556
|
-
const [selectedId, setSelectedId] =
|
|
4557
|
-
const [selectedIds, setSelectedIds] =
|
|
4558
|
-
const [busy, setBusy] =
|
|
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
|
-
|
|
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
|
-
|
|
6226
|
+
useEffect18(() => {
|
|
4584
6227
|
setPage(1);
|
|
4585
6228
|
}, [search, waitingOnly, windowFilter, channelFilter]);
|
|
4586
|
-
|
|
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 =
|
|
4592
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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] =
|
|
4756
|
-
const [templateModalOpen, setTemplateModalOpen] =
|
|
4757
|
-
const [openedFromLink, setOpenedFromLink] =
|
|
4758
|
-
|
|
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__ */
|
|
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__ */
|
|
4798
|
-
/* @__PURE__ */
|
|
4799
|
-
/* @__PURE__ */
|
|
4800
|
-
/* @__PURE__ */
|
|
4801
|
-
/* @__PURE__ */
|
|
4802
|
-
/* @__PURE__ */
|
|
4803
|
-
/* @__PURE__ */
|
|
4804
|
-
/* @__PURE__ */
|
|
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__ */
|
|
6451
|
+
/* @__PURE__ */ jsxs29("span", { className: "cv-only-wide", children: [
|
|
4808
6452
|
" ",
|
|
4809
6453
|
labels.conversations
|
|
4810
6454
|
] })
|
|
4811
6455
|
] }),
|
|
4812
|
-
/* @__PURE__ */
|
|
4813
|
-
/* @__PURE__ */
|
|
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__ */
|
|
6460
|
+
/* @__PURE__ */ jsxs29("span", { className: "cv-only-wide", children: [
|
|
4817
6461
|
" ",
|
|
4818
6462
|
labels.waiting
|
|
4819
6463
|
] })
|
|
4820
6464
|
] }),
|
|
4821
|
-
/* @__PURE__ */
|
|
4822
|
-
/* @__PURE__ */
|
|
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__ */
|
|
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__ */
|
|
4833
|
-
/* @__PURE__ */
|
|
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__ */
|
|
4844
|
-
/* @__PURE__ */
|
|
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__ */
|
|
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__ */
|
|
4859
|
-
/* @__PURE__ */
|
|
4860
|
-
inbox.selectedIds.size > 0 ? /* @__PURE__ */
|
|
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__ */
|
|
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__ */
|
|
4879
|
-
/* @__PURE__ */
|
|
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__ */
|
|
6530
|
+
inbox.loadFailure ? /* @__PURE__ */ jsxs29("p", { role: "alert", className: "cv-workspace-failure", children: [
|
|
4887
6531
|
inbox.loadFailure,
|
|
4888
|
-
signInHref ? /* @__PURE__ */
|
|
6532
|
+
signInHref ? /* @__PURE__ */ jsxs29(Fragment8, { children: [
|
|
4889
6533
|
" ",
|
|
4890
|
-
/* @__PURE__ */
|
|
6534
|
+
/* @__PURE__ */ jsx31("a", { href: signInHref, className: "cv-workspace-failure__link", children: labels.signIn })
|
|
4891
6535
|
] }) : null
|
|
4892
6536
|
] }) : null,
|
|
4893
|
-
/* @__PURE__ */
|
|
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__ */
|
|
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__ */
|
|
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__ */
|
|
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__ */
|
|
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__ */
|
|
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__ */
|
|
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,
|