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