@handled-ai/design-system 0.20.25 → 0.20.27
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/components/button.d.ts +1 -1
- package/dist/components/conversation-panel.d.ts +30 -2
- package/dist/components/conversation-panel.js +108 -22
- package/dist/components/conversation-panel.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/package.json +1 -1
- package/src/components/__tests__/conversation-panel.test.tsx +123 -0
- package/src/components/conversation-panel.tsx +129 -6
|
@@ -4,7 +4,7 @@ import { VariantProps } from 'class-variance-authority';
|
|
|
4
4
|
|
|
5
5
|
declare const buttonVariants: (props?: ({
|
|
6
6
|
variant?: "link" | "default" | "secondary" | "destructive" | "outline" | "ghost" | null | undefined;
|
|
7
|
-
size?: "
|
|
7
|
+
size?: "icon" | "default" | "sm" | "lg" | null | undefined;
|
|
8
8
|
} & class_variance_authority_types.ClassProp) | undefined) => string;
|
|
9
9
|
declare function Button({ className, variant, size, asChild, ...props }: React.ComponentProps<"button"> & VariantProps<typeof buttonVariants> & {
|
|
10
10
|
asChild?: boolean;
|
|
@@ -79,6 +79,20 @@ interface ConvMessage {
|
|
|
79
79
|
};
|
|
80
80
|
}
|
|
81
81
|
type ConvStatus = "responded" | "awaiting" | "viewing" | "draft";
|
|
82
|
+
/**
|
|
83
|
+
* A follow-up template offered in the in-composer "Templates" picker (WIT-970).
|
|
84
|
+
* Presentational: the consumer fills variables for THIS thread's recipient
|
|
85
|
+
* before passing it in, so `body` is composer-ready text. `tags` (e.g. "Reply")
|
|
86
|
+
* are shown as chips so reps can recognize the right template at a glance.
|
|
87
|
+
*/
|
|
88
|
+
interface ConversationReplyTemplate {
|
|
89
|
+
id: string;
|
|
90
|
+
name: string;
|
|
91
|
+
/** Composer-ready reply body, already personalized by the consumer. */
|
|
92
|
+
body: string;
|
|
93
|
+
/** Optional tags shown as chips in the picker for findability. */
|
|
94
|
+
tags?: string[];
|
|
95
|
+
}
|
|
82
96
|
interface ConversationThread {
|
|
83
97
|
threadId: string;
|
|
84
98
|
subject: string;
|
|
@@ -106,6 +120,12 @@ interface ConversationThread {
|
|
|
106
120
|
draft?: string;
|
|
107
121
|
/** Signature text appended to replies (plain text). */
|
|
108
122
|
signature?: string;
|
|
123
|
+
/**
|
|
124
|
+
* Follow-up templates offered in the composer's "Templates" picker, already
|
|
125
|
+
* personalized for this thread's recipient (WIT-970). Omit/empty hides the
|
|
126
|
+
* picker.
|
|
127
|
+
*/
|
|
128
|
+
replyTemplates?: ConversationReplyTemplate[];
|
|
109
129
|
}
|
|
110
130
|
interface ConversationReplyPayload {
|
|
111
131
|
threadId: string;
|
|
@@ -140,10 +160,18 @@ interface ConversationPanelProps {
|
|
|
140
160
|
*/
|
|
141
161
|
onPreviewReply?: (payload: ConversationReplyPayload) => Promise<ConversationReplyPreview>;
|
|
142
162
|
onOpenInGmail?: (threadId: string) => void;
|
|
163
|
+
/**
|
|
164
|
+
* Fired when the rep applies a reply template from the composer picker
|
|
165
|
+
* (WIT-970) — for analytics / template-usage tracking. Optional.
|
|
166
|
+
*/
|
|
167
|
+
onApplyReplyTemplate?: (info: {
|
|
168
|
+
threadId: string;
|
|
169
|
+
templateId: string;
|
|
170
|
+
}) => void;
|
|
143
171
|
/** Inline-open this thread initially (defaults to the first responded one). */
|
|
144
172
|
defaultOpenThreadId?: string;
|
|
145
173
|
className?: string;
|
|
146
174
|
}
|
|
147
|
-
declare function ConversationPanel({ threads, me, tenantName, onSendReply, onCreateGmailDraft, onPreviewReply, onOpenInGmail, defaultOpenThreadId, className, }: ConversationPanelProps): React.JSX.Element | null;
|
|
175
|
+
declare function ConversationPanel({ threads, me, tenantName, onSendReply, onCreateGmailDraft, onPreviewReply, onOpenInGmail, onApplyReplyTemplate, defaultOpenThreadId, className, }: ConversationPanelProps): React.JSX.Element | null;
|
|
148
176
|
|
|
149
|
-
export { type ConvMessage, type ConvParticipant, type ConvStatus, ConversationPanel, type ConversationPanelProps, type ConversationReplyPayload, type ConversationReplyPreview, type ConversationThread };
|
|
177
|
+
export { type ConvMessage, type ConvParticipant, type ConvStatus, ConversationPanel, type ConversationPanelProps, type ConversationReplyPayload, type ConversationReplyPreview, type ConversationReplyTemplate, type ConversationThread };
|
|
@@ -37,7 +37,8 @@ import {
|
|
|
37
37
|
Pause,
|
|
38
38
|
GitMerge,
|
|
39
39
|
Check,
|
|
40
|
-
X
|
|
40
|
+
X,
|
|
41
|
+
FileText
|
|
41
42
|
} from "lucide-react";
|
|
42
43
|
import { cn } from "../lib/utils.js";
|
|
43
44
|
import { getInitials } from "../lib/user-display.js";
|
|
@@ -47,6 +48,14 @@ import { Button } from "./button.js";
|
|
|
47
48
|
import { Switch } from "./switch.js";
|
|
48
49
|
import { Textarea } from "./textarea.js";
|
|
49
50
|
import { RichTextToolbar } from "./rich-text-toolbar.js";
|
|
51
|
+
import {
|
|
52
|
+
DropdownMenu,
|
|
53
|
+
DropdownMenuContent,
|
|
54
|
+
DropdownMenuItem,
|
|
55
|
+
DropdownMenuLabel,
|
|
56
|
+
DropdownMenuSeparator,
|
|
57
|
+
DropdownMenuTrigger
|
|
58
|
+
} from "./dropdown-menu.js";
|
|
50
59
|
import { EmailBody } from "./email-body.js";
|
|
51
60
|
import { decodeEmailDisplayText, emailBodySnippet, formatAddressList, normalizeEmailSender } from "./email-display-helpers.js";
|
|
52
61
|
import {
|
|
@@ -313,6 +322,7 @@ function MessageView({
|
|
|
313
322
|
] });
|
|
314
323
|
}
|
|
315
324
|
const IDLE_PREVIEW = { loading: false, html: null, confirmationToken: null, error: null, local: false };
|
|
325
|
+
const MANUAL_FILL_TOKEN_RE = /\{\{\s*personaliz(?:e|ation)(?:_[a-z0-9]+)*\s*\}\}/i;
|
|
316
326
|
function ReplyComposer({
|
|
317
327
|
thread,
|
|
318
328
|
me,
|
|
@@ -322,16 +332,25 @@ function ReplyComposer({
|
|
|
322
332
|
onSend,
|
|
323
333
|
onDraft,
|
|
324
334
|
onPreviewReply,
|
|
335
|
+
onApplyTemplate,
|
|
325
336
|
draftDisabledReason
|
|
326
337
|
}) {
|
|
327
|
-
var _a, _b, _c, _d, _e, _f;
|
|
338
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
328
339
|
const [body, setBody] = React.useState((_a = thread.draft) != null ? _a : "");
|
|
340
|
+
const [appliedTemplate, setAppliedTemplate] = React.useState(null);
|
|
329
341
|
const [sig, setSig] = React.useState(true);
|
|
342
|
+
const replyTemplates = (_b = thread.replyTemplates) != null ? _b : [];
|
|
343
|
+
const hasManualFillToken = MANUAL_FILL_TOKEN_RE.test(body);
|
|
344
|
+
const applyTemplate = (template) => {
|
|
345
|
+
setBody(template.body);
|
|
346
|
+
setAppliedTemplate(template.name);
|
|
347
|
+
onApplyTemplate == null ? void 0 : onApplyTemplate(template.id);
|
|
348
|
+
};
|
|
330
349
|
const [preview, setPreview] = React.useState(false);
|
|
331
350
|
const [previewState, setPreviewState] = React.useState(IDLE_PREVIEW);
|
|
332
351
|
const [sending, setSending] = React.useState(false);
|
|
333
352
|
const [sendError, setSendError] = React.useState(null);
|
|
334
|
-
const ccList = replyAll ? (
|
|
353
|
+
const ccList = replyAll ? (_c = thread.cc) != null ? _c : [] : [];
|
|
335
354
|
const subject = /^re:/i.test(thread.subject) ? thread.subject : `Re: ${thread.subject}`;
|
|
336
355
|
const draftDisabled = Boolean(draftDisabledReason);
|
|
337
356
|
const localPreviewHtml = textToHtml(body) + (sig && thread.signature ? textToHtml(thread.signature) : "");
|
|
@@ -415,7 +434,7 @@ function ReplyComposer({
|
|
|
415
434
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5", children: [
|
|
416
435
|
/* @__PURE__ */ jsx("span", { className: "text-muted-foreground w-12 shrink-0 text-[11px] font-medium", children: "To" }),
|
|
417
436
|
/* @__PURE__ */ jsx("span", { className: "font-medium", children: displayParticipant(thread.contact).name }),
|
|
418
|
-
/* @__PURE__ */ jsx("span", { className: "text-muted-foreground/60 truncate text-xs", children: (
|
|
437
|
+
/* @__PURE__ */ jsx("span", { className: "text-muted-foreground/60 truncate text-xs", children: (_d = displayParticipant(thread.contact).email) != null ? _d : thread.contact.email })
|
|
419
438
|
] }),
|
|
420
439
|
replyAll && ccList.length ? /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-1.5", children: [
|
|
421
440
|
/* @__PURE__ */ jsx("span", { className: "text-muted-foreground w-12 shrink-0 text-[11px] font-medium", children: "Cc" }),
|
|
@@ -443,7 +462,7 @@ function ReplyComposer({
|
|
|
443
462
|
onKeyDown: (e) => {
|
|
444
463
|
if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
|
|
445
464
|
e.preventDefault();
|
|
446
|
-
void openPreview();
|
|
465
|
+
if (!hasManualFillToken) void openPreview();
|
|
447
466
|
}
|
|
448
467
|
}
|
|
449
468
|
}
|
|
@@ -452,20 +471,81 @@ function ReplyComposer({
|
|
|
452
471
|
/* @__PURE__ */ jsx("span", { className: "text-muted-foreground/60 mr-1", children: "--" }),
|
|
453
472
|
thread.signature
|
|
454
473
|
] }) : null,
|
|
474
|
+
replyTemplates.length > 0 ? /* @__PURE__ */ jsxs("div", { className: "mt-2 flex flex-wrap items-center gap-2", children: [
|
|
475
|
+
/* @__PURE__ */ jsxs(DropdownMenu, { children: [
|
|
476
|
+
/* @__PURE__ */ jsx(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(Button, { type: "button", variant: "outline", size: "sm", disabled: sending, "data-slot": "conv-reply-template-trigger", children: [
|
|
477
|
+
/* @__PURE__ */ jsx(FileText, { size: 14 }),
|
|
478
|
+
" Templates"
|
|
479
|
+
] }) }),
|
|
480
|
+
/* @__PURE__ */ jsxs(DropdownMenuContent, { align: "start", className: "max-w-xs", children: [
|
|
481
|
+
/* @__PURE__ */ jsx(DropdownMenuLabel, { children: "Templates" }),
|
|
482
|
+
/* @__PURE__ */ jsx(DropdownMenuSeparator, {}),
|
|
483
|
+
replyTemplates.map((template) => /* @__PURE__ */ jsxs(
|
|
484
|
+
DropdownMenuItem,
|
|
485
|
+
{
|
|
486
|
+
onSelect: () => applyTemplate(template),
|
|
487
|
+
className: "flex flex-col items-start gap-0.5",
|
|
488
|
+
children: [
|
|
489
|
+
/* @__PURE__ */ jsx("span", { className: "text-[13px] font-medium", children: template.name }),
|
|
490
|
+
template.tags && template.tags.length ? /* @__PURE__ */ jsx("span", { className: "flex flex-wrap gap-1", children: template.tags.map((tag) => /* @__PURE__ */ jsx(
|
|
491
|
+
"span",
|
|
492
|
+
{
|
|
493
|
+
className: "border-border bg-muted text-muted-foreground rounded border px-1 py-px text-[10px] leading-4",
|
|
494
|
+
children: tag
|
|
495
|
+
},
|
|
496
|
+
tag
|
|
497
|
+
)) }) : null
|
|
498
|
+
]
|
|
499
|
+
},
|
|
500
|
+
template.id
|
|
501
|
+
))
|
|
502
|
+
] })
|
|
503
|
+
] }),
|
|
504
|
+
appliedTemplate ? /* @__PURE__ */ jsxs("span", { className: "text-muted-foreground inline-flex items-center gap-1 text-[11px]", children: [
|
|
505
|
+
/* @__PURE__ */ jsx(Check, { size: 11 }),
|
|
506
|
+
" Applied \u201C",
|
|
507
|
+
appliedTemplate,
|
|
508
|
+
"\u201D \xB7 edit before sending"
|
|
509
|
+
] }) : null
|
|
510
|
+
] }) : null,
|
|
511
|
+
hasManualFillToken ? /* @__PURE__ */ jsxs("p", { "data-slot": "conv-reply-manual-fill", role: "alert", className: "text-destructive mt-2 text-[12px]", children: [
|
|
512
|
+
"Fill in the personalize placeholder before sending. Replace each ",
|
|
513
|
+
"{{personalize}}",
|
|
514
|
+
" with a real, account-specific line."
|
|
515
|
+
] }) : null,
|
|
455
516
|
/* @__PURE__ */ jsxs("div", { className: "mt-2 flex flex-wrap items-center gap-2", children: [
|
|
456
517
|
/* @__PURE__ */ jsx(RichTextToolbar, {}),
|
|
457
518
|
/* @__PURE__ */ jsxs("label", { className: "text-muted-foreground ml-auto inline-flex cursor-pointer items-center gap-1.5 text-[12px]", children: [
|
|
458
519
|
/* @__PURE__ */ jsx(Switch, { checked: sig, onCheckedChange: setSig, "aria-label": "Toggle signature" }),
|
|
459
520
|
"Signature"
|
|
460
521
|
] }),
|
|
461
|
-
/* @__PURE__ */ jsxs(
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
522
|
+
/* @__PURE__ */ jsxs(
|
|
523
|
+
Button,
|
|
524
|
+
{
|
|
525
|
+
type: "button",
|
|
526
|
+
variant: "outline",
|
|
527
|
+
size: "sm",
|
|
528
|
+
disabled: sending || hasManualFillToken,
|
|
529
|
+
onClick: () => void openPreview(),
|
|
530
|
+
children: [
|
|
531
|
+
/* @__PURE__ */ jsx(Eye, { size: 14 }),
|
|
532
|
+
" Preview"
|
|
533
|
+
]
|
|
534
|
+
}
|
|
535
|
+
),
|
|
536
|
+
/* @__PURE__ */ jsxs(
|
|
537
|
+
Button,
|
|
538
|
+
{
|
|
539
|
+
type: "button",
|
|
540
|
+
size: "sm",
|
|
541
|
+
disabled: sending || hasManualFillToken,
|
|
542
|
+
onClick: () => void openPreview(),
|
|
543
|
+
children: [
|
|
544
|
+
/* @__PURE__ */ jsx(Send, { size: 14 }),
|
|
545
|
+
" Send"
|
|
546
|
+
]
|
|
547
|
+
}
|
|
548
|
+
)
|
|
469
549
|
] }),
|
|
470
550
|
/* @__PURE__ */ jsx(Dialog, { open: preview, onOpenChange: (open) => {
|
|
471
551
|
if (!sending) {
|
|
@@ -479,7 +559,7 @@ function ReplyComposer({
|
|
|
479
559
|
" ",
|
|
480
560
|
previewState.local ? "Local draft preview" : "Reply preview"
|
|
481
561
|
] }),
|
|
482
|
-
/* @__PURE__ */ jsx(DialogDescription, { children: previewState.local ? "Local draft preview only
|
|
562
|
+
/* @__PURE__ */ jsx(DialogDescription, { children: previewState.local ? "Local draft preview only. The server prepares the exact message on send." : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
483
563
|
"Stays on the ",
|
|
484
564
|
subject.replace(/^Re:\s*/i, ""),
|
|
485
565
|
" thread. Gmail keeps it threaded."
|
|
@@ -492,7 +572,7 @@ function ReplyComposer({
|
|
|
492
572
|
" ",
|
|
493
573
|
/* @__PURE__ */ jsxs("span", { className: "text-muted-foreground/60", children: [
|
|
494
574
|
"<",
|
|
495
|
-
(
|
|
575
|
+
(_e = displayParticipant(thread.contact).email) != null ? _e : thread.contact.email,
|
|
496
576
|
">"
|
|
497
577
|
] })
|
|
498
578
|
] }),
|
|
@@ -515,9 +595,9 @@ function ReplyComposer({
|
|
|
515
595
|
"div",
|
|
516
596
|
{
|
|
517
597
|
"data-slot": "conv-preview-body",
|
|
518
|
-
"data-confirmation-token": (
|
|
598
|
+
"data-confirmation-token": (_f = previewState.confirmationToken) != null ? _f : void 0,
|
|
519
599
|
className: "max-h-72 overflow-auto",
|
|
520
|
-
children: /* @__PURE__ */ jsx(EmailBody, { html: (
|
|
600
|
+
children: /* @__PURE__ */ jsx(EmailBody, { html: (_g = previewState.html) != null ? _g : "", variant: "preview", collapseDetails: false, defaultDetailsOpen: true })
|
|
521
601
|
}
|
|
522
602
|
),
|
|
523
603
|
sendError ? /* @__PURE__ */ jsx("p", { role: "alert", className: "text-destructive text-sm", children: sendError }) : null,
|
|
@@ -546,7 +626,7 @@ function ReplyComposer({
|
|
|
546
626
|
{
|
|
547
627
|
type: "button",
|
|
548
628
|
size: "sm",
|
|
549
|
-
disabled: sending || previewState.loading,
|
|
629
|
+
disabled: sending || previewState.loading || hasManualFillToken,
|
|
550
630
|
onClick: handleSend,
|
|
551
631
|
children: [
|
|
552
632
|
/* @__PURE__ */ jsx(Send, { size: 14 }),
|
|
@@ -568,7 +648,8 @@ function ThreadBody({
|
|
|
568
648
|
onSendReply,
|
|
569
649
|
onCreateGmailDraft,
|
|
570
650
|
onPreviewReply,
|
|
571
|
-
onOpenInGmail
|
|
651
|
+
onOpenInGmail,
|
|
652
|
+
onApplyReplyTemplate
|
|
572
653
|
}) {
|
|
573
654
|
var _a;
|
|
574
655
|
const canReply = thread.canReply !== false;
|
|
@@ -646,6 +727,7 @@ function ThreadBody({
|
|
|
646
727
|
replyAll,
|
|
647
728
|
tenantName,
|
|
648
729
|
onPreviewReply,
|
|
730
|
+
onApplyTemplate: (templateId) => onApplyReplyTemplate == null ? void 0 : onApplyReplyTemplate({ threadId: thread.threadId, templateId }),
|
|
649
731
|
onClose: () => setMode("idle"),
|
|
650
732
|
onSend: async (body, includeSignature) => {
|
|
651
733
|
await (onSendReply == null ? void 0 : onSendReply({ threadId: thread.threadId, body, includeSignature, replyAll }));
|
|
@@ -697,7 +779,8 @@ function ThreadRow({
|
|
|
697
779
|
onSendReply,
|
|
698
780
|
onCreateGmailDraft,
|
|
699
781
|
onPreviewReply,
|
|
700
|
-
onOpenInGmail
|
|
782
|
+
onOpenInGmail,
|
|
783
|
+
onApplyReplyTemplate
|
|
701
784
|
}) {
|
|
702
785
|
var _a;
|
|
703
786
|
const status = effectiveStatus(thread);
|
|
@@ -752,7 +835,8 @@ function ThreadRow({
|
|
|
752
835
|
onSendReply,
|
|
753
836
|
onCreateGmailDraft,
|
|
754
837
|
onPreviewReply,
|
|
755
|
-
onOpenInGmail
|
|
838
|
+
onOpenInGmail,
|
|
839
|
+
onApplyReplyTemplate
|
|
756
840
|
}
|
|
757
841
|
) }) : null
|
|
758
842
|
]
|
|
@@ -767,6 +851,7 @@ function ConversationPanel({
|
|
|
767
851
|
onCreateGmailDraft,
|
|
768
852
|
onPreviewReply,
|
|
769
853
|
onOpenInGmail,
|
|
854
|
+
onApplyReplyTemplate,
|
|
770
855
|
defaultOpenThreadId,
|
|
771
856
|
className
|
|
772
857
|
}) {
|
|
@@ -865,7 +950,8 @@ function ConversationPanel({
|
|
|
865
950
|
onSendReply,
|
|
866
951
|
onCreateGmailDraft,
|
|
867
952
|
onPreviewReply,
|
|
868
|
-
onOpenInGmail
|
|
953
|
+
onOpenInGmail,
|
|
954
|
+
onApplyReplyTemplate
|
|
869
955
|
},
|
|
870
956
|
t.threadId
|
|
871
957
|
)) }) : null
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/conversation-panel.tsx"],"sourcesContent":["\"use client\"\n\n/**\n * conversation-panel.tsx — in-case email-thread reader + reply, for the case\n * panel (\"Email response detected\" hub).\n *\n * v1 scope (WIT-853 / WIT-802): INLINE thread reading + reply only.\n * - A collapsible hub header with a pulse badge whose color reflects state:\n * responded (a reply needs you) / awaiting (sent, waiting) / viewing (read-only).\n * - A list of thread rows; clicking one opens a Gmail-style reader inline,\n * right under the row, with collapsible messages + quoted-history toggle.\n * - Reply / Reply-all composer with a signature toggle and two send paths:\n * Preview→Send (in-app) and \"Open draft in Gmail\" (deep link).\n * - A \"playbook stopped\" banner when the customer's reply halted the sequence,\n * and a read-only notice when the operator is not a thread participant.\n *\n * The bottom-right floating dock / side-by-side compare is intentionally OUT of\n * scope here and tracked separately (WIT-855).\n *\n * Presentational: all data + side effects come from the consumer. Email bodies\n * (`bodyHtml`, `quoted.html`) are sanitized before rendering.\n */\n\nimport * as React from \"react\"\nimport {\n ChevronDown,\n ChevronUp,\n CornerUpLeft,\n CheckCheck,\n MailOpen,\n FilePenLine,\n Reply,\n ReplyAll,\n Eye,\n Send,\n Lock,\n Pause,\n GitMerge,\n Check,\n X,\n} from \"lucide-react\"\n\nimport { cn } from \"../lib/utils\"\nimport { getInitials } from \"../lib/user-display\"\nimport { BRAND_ICONS } from \"../lib/icons\"\nimport { Avatar, AvatarFallback, AvatarImage } from \"./avatar\"\nimport { Button } from \"./button\"\nimport { Switch } from \"./switch\"\nimport { Textarea } from \"./textarea\"\nimport { RichTextToolbar } from \"./rich-text-toolbar\"\nimport { EmailBody } from \"./email-body\"\nimport { decodeEmailDisplayText, emailBodySnippet, formatAddressList, normalizeEmailSender } from \"./email-display-helpers\"\nimport {\n Dialog,\n DialogContent,\n DialogHeader,\n DialogTitle,\n DialogDescription,\n DialogFooter,\n} from \"./dialog\"\n\n/* ── Types ───────────────────────────────────────────────────────────────── */\n\nexport interface ConvParticipant {\n name: string\n email: string\n avatarUrl?: string | null\n role?: string\n}\n\nexport interface ConvMessage {\n id: string\n direction: \"inbound\" | \"outbound\"\n from: ConvParticipant\n to: ConvParticipant\n /** Absolute timestamp label, e.g. \"Jun 1, 2026, 9:12 AM\". */\n date: string\n /**\n * Raw chronological timestamp for deterministic thread ordering. Prefer\n * `sentAt` for outbound messages and `receivedAt` for inbound messages.\n * Accepts ISO/RFC822 strings, Date objects, epoch milliseconds, or Gmail\n * internalDate values as strings/numbers. Display-only `date` / `ago` labels\n * are never parsed for ordering.\n */\n timestamp?: string | number | Date | null\n rawTimestamp?: string | number | Date | null\n sentAt?: string | number | Date | null\n receivedAt?: string | number | Date | null\n /** Compatibility with data contracts that pass through source field names. */\n sent_at?: string | number | Date | null\n received_at?: string | number | Date | null\n internalDate?: string | number | Date | null\n gmailInternalDate?: string | number | Date | null\n internal_date?: string | number | Date | null\n rfc822Date?: string | number | Date | null\n dateHeader?: string | number | Date | null\n /** Relative label, e.g. \"2 days ago\". */\n ago?: string\n receipt?: { kind: \"new\" | \"read\" | \"opened\" | \"sent\" | \"draft\"; label: string }\n /** HTML body (preferred). Sanitized by the component before rendering. */\n bodyHtml?: string\n /**\n * Pre-split signature HTML for this message, sanitized server-side by the\n * sender's signature pipeline (which preserves the authored Gmail look —\n * fonts, colors, logos). When present it renders inside the collapsed\n * details section (\"•••\" toggle) instead of relying on the heuristic footer\n * split of `bodyHtml`, so long signatures never expand the thread by\n * default.\n */\n signatureHtml?: string | null\n /** Plain-text fallback when `bodyHtml` is absent. */\n body?: string\n /** Quoted prior message, collapsed behind a toggle. Sanitized before rendering. */\n quoted?: { attr: string; html: string }\n}\n\nexport type ConvStatus = \"responded\" | \"awaiting\" | \"viewing\" | \"draft\"\n\nexport interface ConversationThread {\n threadId: string\n subject: string\n status: ConvStatus\n /** Relative label for the most recent activity. */\n lastWhen?: string\n contact: ConvParticipant\n cc?: ConvParticipant[]\n /** Set when this thread's reply halted a playbook (terminal). */\n paused?: { playbook: string } | null\n /** false => operator cannot reply or create drafts from this thread. */\n canReply?: boolean\n /** Explains why reply and draft creation are disabled. */\n replyDisabledReason?: string\n /** Existing Gmail draft or thread URL. Rendered as a new-tab link when present. */\n openInGmailUrl?: string | null\n /** Forces the Open in Gmail action into a disabled state. */\n openInGmailDisabled?: boolean\n /** Tooltip/read-only copy for a disabled Open in Gmail action. */\n openInGmailDisabledReason?: string | null\n messages: ConvMessage[]\n /** Prefilled reply draft body. */\n draft?: string\n /** Signature text appended to replies (plain text). */\n signature?: string\n}\n\nexport interface ConversationReplyPayload {\n threadId: string\n body: string\n includeSignature: boolean\n replyAll: boolean\n}\n\n/**\n * Result of a server-side reply preview. `htmlBody` is the exact HTML the server\n * prepared for this reply (sanitized by the component before render).\n * `confirmationToken`, when present, identifies the prepared confirmation so a\n * consumer can reuse it for the final send instead of re-preparing the message.\n */\nexport interface ConversationReplyPreview {\n htmlBody: string\n confirmationToken?: string\n}\n\nexport interface ConversationPanelProps {\n threads: ConversationThread[]\n /** Current operator: drives \"to me\" + the reply avatar. */\n me?: ConvParticipant\n /** Deployment brand, used in the paused-playbook copy. */\n tenantName?: string\n onSendReply?: (payload: ConversationReplyPayload) => void | Promise<void>\n onCreateGmailDraft?: (payload: ConversationReplyPayload) => void | Promise<void>\n /**\n * Server-side preview contract. When provided, the reply preview requests the\n * exact send HTML from the server, the component sanitizes it before render,\n * and retains any returned `confirmationToken` in preview state so a consumer\n * can reuse it for the final send. When omitted, the composer falls back to a\n * clearly labeled local draft preview only.\n */\n onPreviewReply?: (payload: ConversationReplyPayload) => Promise<ConversationReplyPreview>\n onOpenInGmail?: (threadId: string) => void\n /** Inline-open this thread initially (defaults to the first responded one). */\n defaultOpenThreadId?: string\n className?: string\n}\n\n/* ── Shared helpers ──────────────────────────────────────────────────────── */\n\nfunction escapeHtml(s: string): string {\n return s.replace(/&/g, \"&\").replace(/</g, \"<\").replace(/>/g, \">\")\n}\n\n/** Plain-text -> simple paragraph HTML for the Preview / sent-message body. */\nfunction textToHtml(text: string): string {\n return decodeEmailDisplayText(text)\n .split(/\\n{2,}/)\n .map((p) => p.trim())\n .filter(Boolean)\n .map((p) => `<p>${escapeHtml(p).replace(/\\n/g, \"<br>\")}</p>`)\n .join(\"\")\n}\n\nfunction displayParticipant(person: ConvParticipant) {\n return normalizeEmailSender({ name: person.name, email: person.email, fallbackName: person.email || person.name })\n}\n\nfunction firstName(name: string): string {\n return decodeEmailDisplayText(name).split(\" \")[0] || decodeEmailDisplayText(name)\n}\n\nfunction sameEmail(a?: string | null, b?: string | null): boolean {\n return Boolean(a && b && a.trim().toLowerCase() === b.trim().toLowerCase())\n}\n\nfunction messageBodySnippet(message: Pick<ConvMessage, \"body\" | \"bodyHtml\">, maxLength = 140): string {\n return emailBodySnippet({ bodyHtml: message.bodyHtml, body: message.body }, maxLength)\n}\n\n\ntype MessageTimestampValue = string | number | Date | null | undefined\n\nfunction parseMessageTimestampValue(value: MessageTimestampValue): number | null {\n if (value == null || value === \"\") return null\n\n if (value instanceof Date) {\n const time = value.getTime()\n return Number.isNaN(time) ? null : time\n }\n\n if (typeof value === \"number\") {\n if (!Number.isFinite(value)) return null\n return value < 10_000_000_000 ? value * 1000 : value\n }\n\n const trimmed = value.trim()\n if (!trimmed) return null\n\n if (/^\\d+$/.test(trimmed)) {\n const numeric = Number(trimmed)\n if (!Number.isFinite(numeric)) return null\n return numeric < 10_000_000_000 ? numeric * 1000 : numeric\n }\n\n const parsed = Date.parse(trimmed)\n return Number.isNaN(parsed) ? null : parsed\n}\n\nfunction messageTimestamp(message: ConvMessage): number | null {\n const directional = message.direction === \"outbound\"\n ? [message.sentAt, message.sent_at]\n : [message.receivedAt, message.received_at]\n\n const candidates: MessageTimestampValue[] = [\n ...directional,\n message.rawTimestamp,\n message.timestamp,\n message.gmailInternalDate,\n message.internalDate,\n message.internal_date,\n message.rfc822Date,\n message.dateHeader,\n ]\n\n for (const candidate of candidates) {\n const parsed = parseMessageTimestampValue(candidate)\n if (parsed !== null) return parsed\n }\n\n return null\n}\n\nfunction sortMessagesChronologically(messages: ConvMessage[]): ConvMessage[] {\n return messages\n .map((message, index) => ({ message, index, timestamp: messageTimestamp(message) }))\n .sort((a, b) => {\n if (a.timestamp !== null && b.timestamp !== null && a.timestamp !== b.timestamp) {\n return a.timestamp - b.timestamp\n }\n return a.index - b.index\n })\n .map((entry) => entry.message)\n}\n\nfunction GmailMark({ size = 14 }: { size?: number }) {\n return (\n // eslint-disable-next-line @next/next/no-img-element\n <img\n src={BRAND_ICONS.gmail.icon}\n alt=\"Gmail\"\n width={size}\n height={size}\n style={{ width: size, height: size, objectFit: \"contain\", display: \"block\" }}\n />\n )\n}\n\nfunction PersonAvatar({ person, size = \"sm\" }: { person: ConvParticipant; size?: \"sm\" | \"default\" }) {\n const display = displayParticipant(person)\n\n return (\n <Avatar size={size}>\n {person.avatarUrl ? <AvatarImage src={person.avatarUrl} alt={display.name} /> : null}\n <AvatarFallback className=\"bg-muted text-muted-foreground text-[10px] font-medium uppercase\">\n {getInitials({ name: display.name, email: display.email ?? person.email })}\n </AvatarFallback>\n </Avatar>\n )\n}\n\nconst STATUS_PILL: Record<ConvStatus, { label: string; cls: string }> = {\n responded: { label: \"NEW REPLY\", cls: \"bg-status-warning-bg text-status-warning-fg border-status-warning-border\" },\n draft: { label: \"Draft\", cls: \"bg-background text-foreground/80 border-border\" },\n awaiting: { label: \"SENT\", cls: \"bg-status-info-bg text-status-info-fg border-status-info-border\" },\n viewing: { label: \"Viewing\", cls: \"bg-muted text-muted-foreground border-border\" },\n}\n\nconst STATUS_DOT: Record<ConvStatus, string> = {\n responded: \"bg-status-warning-fg\",\n draft: \"bg-status-pending-fg\",\n awaiting: \"bg-status-info-fg\",\n viewing: \"bg-muted-foreground/50\",\n}\n\nconst THREAD_ROW_ACCENT: Record<ConvStatus, string> = {\n responded: \"border-l-4 border-l-status-warning-border bg-status-warning-bg/25\",\n awaiting: \"border-l-4 border-l-status-info-border bg-status-info-bg/25\",\n draft: \"border-l-4 border-l-status-pending-border bg-status-pending-bg/20\",\n viewing: \"\",\n}\n\nconst RECEIPT_CHIP: Record<NonNullable<ConvMessage[\"receipt\"]>[\"kind\"], string> = {\n new: \"border-status-warning-border bg-status-warning-bg text-status-warning-fg\",\n read: \"border-status-info-border bg-status-info-bg text-status-info-fg\",\n opened: \"border-status-info-border bg-status-info-bg text-status-info-fg\",\n sent: \"border-status-info-border bg-status-info-bg text-status-info-fg\",\n draft: \"border-status-pending-border bg-status-pending-bg text-status-pending-fg\",\n}\n\nfunction effectiveStatus(t: ConversationThread): ConvStatus {\n return t.canReply === false ? \"viewing\" : t.status\n}\n\nfunction disabledOpenInGmailReason(thread: ConversationThread): string {\n return (\n thread.openInGmailDisabledReason?.trim() ||\n thread.replyDisabledReason?.trim() ||\n \"Gmail access is not available for this thread.\"\n )\n}\n\nfunction canOpenInGmail(thread: ConversationThread, onOpenInGmail?: (threadId: string) => void): boolean {\n return thread.openInGmailDisabled !== true && Boolean(thread.openInGmailUrl || onOpenInGmail)\n}\n\nfunction OpenInGmailButton({\n thread,\n onOpenInGmail,\n label = \"Open in Gmail\",\n}: {\n thread: ConversationThread\n onOpenInGmail?: (threadId: string) => void\n label?: string\n}) {\n const hasConfiguredAction = Boolean(thread.openInGmailUrl || onOpenInGmail || thread.openInGmailDisabled || thread.openInGmailDisabledReason)\n if (!hasConfiguredAction) return null\n\n const disabled = !canOpenInGmail(thread, onOpenInGmail)\n const disabledReason = disabled\n ? disabledOpenInGmailReason(thread)\n : undefined\n\n if (!disabled && thread.openInGmailUrl) {\n return (\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" asChild>\n <a href={thread.openInGmailUrl} target=\"_blank\" rel=\"noopener noreferrer\">\n <GmailMark size={14} /> {label}\n </a>\n </Button>\n )\n }\n\n return (\n <span className=\"inline-flex\" title={disabledReason}>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n disabled={disabled}\n aria-disabled={disabled || undefined}\n aria-label={disabledReason ? `${label}: ${disabledReason}` : label}\n onClick={disabled ? undefined : () => onOpenInGmail?.(thread.threadId)}\n >\n <GmailMark size={14} /> {label}\n </Button>\n </span>\n )\n}\n\n/* ── One message (collapsible) ──────────────────────────────────────────── */\n\nfunction MessageView({\n message,\n expanded,\n onToggle,\n me,\n}: {\n message: ConvMessage\n expanded: boolean\n onToggle: () => void\n me?: ConvParticipant\n}) {\n const [quoteOpen, setQuoteOpen] = React.useState(false)\n const fromDisplay = displayParticipant(message.from)\n const toDisplay = displayParticipant(message.to)\n\n if (!expanded) {\n const snippet = messageBodySnippet(message, 140)\n\n return (\n <button\n type=\"button\"\n data-slot=\"conv-message-collapsed\"\n onClick={onToggle}\n className=\"flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left hover:bg-muted/40\"\n >\n <PersonAvatar person={message.from} />\n <span className=\"text-muted-foreground min-w-0 flex-1 truncate text-[13px]\">\n <b className=\"text-foreground\">{firstName(fromDisplay.name)}</b> · {snippet}\n </span>\n <span className=\"text-muted-foreground/60 shrink-0 text-xs\">{message.ago ?? message.date}</span>\n <ChevronDown size={13} className=\"text-muted-foreground shrink-0\" />\n </button>\n )\n }\n\n const meDisplay = me ? displayParticipant(me) : null\n const toLabel = meDisplay && sameEmail(toDisplay.email, meDisplay.email) ? \"me\" : firstName(toDisplay.name)\n\n return (\n <div data-slot=\"conv-message\" className=\"rounded-md border border-border bg-background\">\n <button\n type=\"button\"\n onClick={onToggle}\n className=\"flex w-full items-start gap-2 px-3 py-2 text-left\"\n >\n <PersonAvatar person={message.from} size=\"default\" />\n <span className=\"min-w-0 flex-1\">\n <span className=\"flex flex-wrap items-baseline gap-x-1.5\">\n <span className=\"text-[13px] font-semibold\">{fromDisplay.name}</span>\n {fromDisplay.email ? (\n <span className=\"text-muted-foreground/60 truncate text-xs\"><{fromDisplay.email}></span>\n ) : null}\n </span>\n <span className=\"text-muted-foreground block text-xs\">\n to <b>{toLabel}</b>\n </span>\n </span>\n <span className=\"flex shrink-0 items-center gap-2\">\n {message.receipt ? (\n <span className={cn(\"inline-flex items-center gap-1 rounded-md border px-1.5 py-px text-[10px] font-semibold leading-4\", RECEIPT_CHIP[message.receipt.kind])}>\n {message.receipt.kind === \"new\" ? (\n <CornerUpLeft size={11} />\n ) : message.receipt.kind === \"read\" || message.receipt.kind === \"sent\" ? (\n <CheckCheck size={11} />\n ) : message.receipt.kind === \"draft\" ? (\n <FilePenLine size={11} />\n ) : (\n <MailOpen size={11} />\n )}\n {message.receipt.label}\n </span>\n ) : null}\n <span className=\"text-muted-foreground/60 text-xs\">{message.date}</span>\n <ChevronUp size={13} className=\"text-muted-foreground\" />\n </span>\n </button>\n\n <div className=\"px-3 pb-2.5\">\n <div data-slot=\"conv-message-body\">\n <EmailBody\n html={message.bodyHtml}\n text={message.body}\n detailsHtml={message.signatureHtml ?? undefined}\n variant=\"history\"\n collapseDetails={true}\n className=\"text-sm\"\n />\n </div>\n\n {message.quoted ? (\n <div className=\"mt-2\">\n <button\n type=\"button\"\n onClick={() => setQuoteOpen((v) => !v)}\n className=\"text-muted-foreground hover:bg-muted rounded px-1.5 text-xs leading-5\"\n title={quoteOpen ? \"Hide quoted text\" : \"Show quoted text\"}\n >\n •••\n </button>\n {quoteOpen ? (\n <div className=\"border-border text-muted-foreground mt-1 border-l-2 pl-3 text-[13px]\">\n <p className=\"mb-1\">{decodeEmailDisplayText(message.quoted.attr)}</p>\n <div data-slot=\"conv-quoted-body\">\n <EmailBody html={message.quoted.html} variant=\"history\" collapseDetails={false} />\n </div>\n </div>\n ) : null}\n </div>\n ) : null}\n </div>\n </div>\n )\n}\n\n/* ── Reply composer ─────────────────────────────────────────────────────── */\n\ntype PreviewState = {\n loading: boolean\n /** Sanitized HTML ready to render. */\n html: string | null\n /** Confirmation token returned by the server preview, retained for reuse on send. */\n confirmationToken: string | null\n error: string | null\n /** True when `html` came from the local fallback rather than the server. */\n local: boolean\n}\n\nconst IDLE_PREVIEW: PreviewState = { loading: false, html: null, confirmationToken: null, error: null, local: false }\n\nfunction ReplyComposer({\n thread,\n me,\n replyAll,\n tenantName,\n onClose,\n onSend,\n onDraft,\n onPreviewReply,\n draftDisabledReason,\n}: {\n thread: ConversationThread\n me?: ConvParticipant\n replyAll: boolean\n tenantName?: string\n onClose: () => void\n onSend: (body: string, includeSignature: boolean) => void | Promise<void>\n onDraft: (body: string, includeSignature: boolean) => void | Promise<void>\n onPreviewReply?: (payload: ConversationReplyPayload) => Promise<ConversationReplyPreview>\n draftDisabledReason?: string | null\n}) {\n const [body, setBody] = React.useState(thread.draft ?? \"\")\n const [sig, setSig] = React.useState(true)\n const [preview, setPreview] = React.useState(false)\n const [previewState, setPreviewState] = React.useState<PreviewState>(IDLE_PREVIEW)\n const [sending, setSending] = React.useState(false)\n const [sendError, setSendError] = React.useState<string | null>(null)\n const ccList = replyAll ? thread.cc ?? [] : []\n const subject = /^re:/i.test(thread.subject) ? thread.subject : `Re: ${thread.subject}`\n const draftDisabled = Boolean(draftDisabledReason)\n\n const localPreviewHtml = textToHtml(body) + (sig && thread.signature ? textToHtml(thread.signature) : \"\")\n\n const openPreview = async () => {\n setPreview(true)\n setSendError(null)\n\n if (!onPreviewReply) {\n // No server preview contract: render a sanitized local draft preview only.\n setPreviewState({ loading: false, html: localPreviewHtml, confirmationToken: null, error: null, local: true })\n return\n }\n\n setPreviewState({ loading: true, html: null, confirmationToken: null, error: null, local: false })\n try {\n const result = await onPreviewReply({ threadId: thread.threadId, body, includeSignature: sig, replyAll })\n setPreviewState({\n loading: false,\n html: result.htmlBody ?? \"\",\n confirmationToken: result.confirmationToken ?? null,\n error: null,\n local: false,\n })\n } catch (error) {\n setPreviewState({\n loading: false,\n html: null,\n confirmationToken: null,\n error: error instanceof Error ? error.message : \"Could not load the preview. Please try again.\",\n local: false,\n })\n }\n }\n\n const handleSend = async () => {\n setSending(true)\n setSendError(null)\n try {\n await onSend(body, sig)\n setPreview(false)\n setPreviewState(IDLE_PREVIEW)\n } catch (error) {\n setSendError(error instanceof Error ? error.message : \"Could not send this reply. Please try again.\")\n } finally {\n setSending(false)\n }\n }\n\n const handleDraft = async () => {\n if (draftDisabled) return\n setSending(true)\n setSendError(null)\n try {\n await onDraft(body, sig)\n setPreview(false)\n setPreviewState(IDLE_PREVIEW)\n } catch (error) {\n setSendError(error instanceof Error ? error.message : \"Could not create the Gmail draft. Please try again.\")\n } finally {\n setSending(false)\n }\n }\n\n return (\n <div data-slot=\"conv-reply\" className=\"border-border bg-muted/20 rounded-md border p-3\">\n <div className=\"mb-2 flex items-center gap-2\">\n {me ? <PersonAvatar person={me} /> : null}\n <span className=\"flex-1 text-[13px] font-medium\">\n {replyAll ? (\n <>\n Reply all{\" \"}\n <span className=\"text-muted-foreground font-normal\">· {1 + ccList.length} recipients</span>\n </>\n ) : (\n <>\n Reply to <b>{firstName(displayParticipant(thread.contact).name)}</b>\n </>\n )}\n </span>\n <span className=\"text-muted-foreground inline-flex items-center gap-1 text-[11px]\">\n <GitMerge size={11} /> Same thread\n </span>\n <button type=\"button\" onClick={onClose} title=\"Discard reply\" className=\"text-muted-foreground hover:text-foreground\">\n <X size={15} />\n </button>\n </div>\n\n <div className=\"border-border mb-2 space-y-1 border-b pb-2 text-[13px]\">\n <div className=\"flex items-center gap-1.5\">\n <span className=\"text-muted-foreground w-12 shrink-0 text-[11px] font-medium\">To</span>\n <span className=\"font-medium\">{displayParticipant(thread.contact).name}</span>\n <span className=\"text-muted-foreground/60 truncate text-xs\">{displayParticipant(thread.contact).email ?? thread.contact.email}</span>\n </div>\n {replyAll && ccList.length ? (\n <div className=\"flex items-start gap-1.5\">\n <span className=\"text-muted-foreground w-12 shrink-0 text-[11px] font-medium\">Cc</span>\n <span className=\"text-muted-foreground text-xs\">\n {formatAddressList(ccList.map((c) => `${displayParticipant(c).name} <${displayParticipant(c).email ?? c.email}>`))}\n </span>\n </div>\n ) : null}\n <div className=\"flex items-center gap-1.5\">\n <span className=\"text-muted-foreground w-12 shrink-0 text-[11px] font-medium\">Subject</span>\n <span className=\"truncate\">{subject}</span>\n <span className=\"text-muted-foreground/60 ml-auto inline-flex items-center gap-1 text-[11px]\">\n <Lock size={10} /> on thread\n </span>\n </div>\n </div>\n\n <Textarea\n value={body}\n onChange={(e) => setBody(e.target.value)}\n placeholder=\"Write your reply…\"\n className=\"min-h-28 resize-y text-sm\"\n onKeyDown={(e) => {\n if ((e.metaKey || e.ctrlKey) && e.key === \"Enter\") {\n e.preventDefault()\n void openPreview()\n }\n }}\n />\n\n {sig && thread.signature ? (\n <div className=\"text-muted-foreground mt-2 whitespace-pre-line border-l-2 border-border pl-3 text-[13px]\">\n <span className=\"text-muted-foreground/60 mr-1\">--</span>\n {thread.signature}\n </div>\n ) : null}\n\n <div className=\"mt-2 flex flex-wrap items-center gap-2\">\n <RichTextToolbar />\n <label className=\"text-muted-foreground ml-auto inline-flex cursor-pointer items-center gap-1.5 text-[12px]\">\n <Switch checked={sig} onCheckedChange={setSig} aria-label=\"Toggle signature\" />\n Signature\n </label>\n <Button type=\"button\" variant=\"outline\" size=\"sm\" disabled={sending} onClick={() => void openPreview()}>\n <Eye size={14} /> Preview\n </Button>\n <Button type=\"button\" size=\"sm\" disabled={sending} onClick={() => void openPreview()}>\n <Send size={14} /> Send\n </Button>\n </div>\n\n <Dialog open={preview} onOpenChange={(open) => { if (!sending) { setPreview(open); if (!open) setPreviewState(IDLE_PREVIEW) } }}>\n <DialogContent className=\"max-w-xl\">\n <DialogHeader>\n <DialogTitle className=\"flex items-center gap-1.5 text-[15px]\">\n <Eye size={15} /> {previewState.local ? \"Local draft preview\" : \"Reply preview\"}\n </DialogTitle>\n <DialogDescription>\n {previewState.local\n ? \"Local draft preview only — the server prepares the exact message on send.\"\n : <>Stays on the {subject.replace(/^Re:\\s*/i, \"\")} thread. Gmail keeps it threaded.</>}\n </DialogDescription>\n </DialogHeader>\n <div className=\"border-border space-y-1 rounded-md border p-3 text-[13px]\">\n <div>\n <span className=\"text-muted-foreground\">To </span>\n <b>{displayParticipant(thread.contact).name}</b>{\" \"}\n <span className=\"text-muted-foreground/60\"><{displayParticipant(thread.contact).email ?? thread.contact.email}></span>\n </div>\n {replyAll && ccList.length ? (\n <div className=\"text-muted-foreground\">Cc {formatAddressList(ccList.map((c) => `${displayParticipant(c).name} <${displayParticipant(c).email ?? c.email}>`))}</div>\n ) : null}\n <div>\n <span className=\"text-muted-foreground\">Subject </span>\n {subject}\n </div>\n </div>\n {previewState.loading ? (\n <div data-slot=\"conv-preview-loading\" role=\"status\" className=\"text-muted-foreground flex items-center gap-2 px-1 py-6 text-[13px]\">\n <span className=\"border-muted-foreground/40 border-t-foreground size-4 animate-spin rounded-full border-2\" aria-hidden />\n Loading preview…\n </div>\n ) : previewState.error ? (\n <p role=\"alert\" className=\"text-destructive text-sm\">\n {previewState.error}\n </p>\n ) : (\n <div\n data-slot=\"conv-preview-body\"\n data-confirmation-token={previewState.confirmationToken ?? undefined}\n className=\"max-h-72 overflow-auto\"\n >\n <EmailBody html={previewState.html ?? \"\"} variant=\"preview\" collapseDetails={false} defaultDetailsOpen />\n </div>\n )}\n {sendError ? (\n <p role=\"alert\" className=\"text-destructive text-sm\">\n {sendError}\n </p>\n ) : null}\n <DialogFooter className=\"sm:justify-between\">\n <span className=\"inline-flex\" title={draftDisabledReason ?? undefined}>\n <button\n type=\"button\"\n disabled={sending || previewState.loading || draftDisabled}\n onClick={handleDraft}\n aria-label={draftDisabledReason ? `Open draft in Gmail: ${draftDisabledReason}` : \"Open draft in Gmail\"}\n className=\"text-muted-foreground hover:text-foreground inline-flex items-center gap-1.5 text-[13px] disabled:pointer-events-none disabled:opacity-50\"\n >\n <GmailMark size={14} /> Open draft in Gmail\n </button>\n </span>\n <span className=\"flex items-center gap-2\">\n <Button type=\"button\" variant=\"outline\" size=\"sm\" disabled={sending} onClick={() => { setPreview(false); setPreviewState(IDLE_PREVIEW) }}>\n Keep editing\n </Button>\n <Button\n type=\"button\"\n size=\"sm\"\n disabled={sending || previewState.loading}\n onClick={handleSend}\n >\n <Send size={14} /> {sending ? \"Sending...\" : \"Send now\"}\n </Button>\n </span>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n\n {tenantName ? (\n <p className=\"text-muted-foreground/70 mt-2 text-[11px]\">Sends via Gmail · playbooks stay stopped.</p>\n ) : null}\n </div>\n )\n}\n\n/* ── Thread body (messages + footer/composer/done states) ───────────────── */\n\ntype ThreadMode = \"idle\" | \"replying\" | \"sent\" | \"draft\"\n\nfunction ThreadBody({\n thread,\n me,\n tenantName,\n onSendReply,\n onCreateGmailDraft,\n onPreviewReply,\n onOpenInGmail,\n}: {\n thread: ConversationThread\n me?: ConvParticipant\n tenantName?: string\n onSendReply?: (p: ConversationReplyPayload) => void | Promise<void>\n onCreateGmailDraft?: (p: ConversationReplyPayload) => void | Promise<void>\n onPreviewReply?: (p: ConversationReplyPayload) => Promise<ConversationReplyPreview>\n onOpenInGmail?: (threadId: string) => void\n}) {\n const canReply = thread.canReply !== false\n const replyDisabledReason = thread.replyDisabledReason?.trim() || \"You are not a participant on this thread, so replying is disabled here.\"\n const draftDisabledReason = onCreateGmailDraft ? null : \"Gmail draft creation is not available for this thread.\"\n const hasCc = !!(thread.cc && thread.cc.length)\n const sortedMessages = React.useMemo(() => sortMessagesChronologically(thread.messages), [thread.messages])\n const [mode, setMode] = React.useState<ThreadMode>(\"idle\")\n const [replyAll, setReplyAll] = React.useState(false)\n const [expanded, setExpanded] = React.useState<Record<string, boolean>>(() => {\n const o: Record<string, boolean> = {}\n sortedMessages.forEach((m, i) => {\n o[m.id] = i === sortedMessages.length - 1\n })\n return o\n })\n\n React.useEffect(() => {\n setExpanded((current) => {\n const next = { ...current }\n sortedMessages.forEach((m, i) => {\n if (next[m.id] === undefined) next[m.id] = i === sortedMessages.length - 1\n })\n return next\n })\n }, [sortedMessages])\n\n const toggle = (id: string) => setExpanded((e) => ({ ...e, [id]: !e[id] }))\n\n return (\n <div data-slot=\"conv-thread-body\" className=\"space-y-2\">\n {canReply && thread.paused ? (\n <div className=\"border-status-warning-border bg-status-warning-bg text-status-warning-fg flex items-start gap-2 rounded-md border border-l-4 p-2.5 text-[12px]\">\n <Pause size={13} className=\"mt-0.5 shrink-0\" />\n <span>\n <b>Playbook stopped.</b> Follow-up actions for {thread.paused.playbook} won’t send\n automatically while this conversation is live. Continue it in {tenantName ?? \"the app\"} or Gmail.\n </span>\n </div>\n ) : null}\n\n <div className=\"space-y-1\">\n {sortedMessages.map((m) => (\n <MessageView key={m.id} message={m} expanded={!!expanded[m.id]} onToggle={() => toggle(m.id)} me={me} />\n ))}\n </div>\n\n {!canReply ? (\n <div className=\"border-border bg-muted/30 text-muted-foreground flex flex-wrap items-start gap-2 rounded-md border p-2.5 text-[12px]\">\n <Eye size={14} className=\"mt-0.5 shrink-0\" />\n <span className=\"min-w-0 flex-1\">\n <b>Viewing only.</b> {replyDisabledReason}\n </span>\n <OpenInGmailButton thread={thread} onOpenInGmail={onOpenInGmail} />\n </div>\n ) : null}\n\n {canReply && mode === \"idle\" ? (\n <div data-slot=\"conv-action-row\" className=\"border-border/70 mt-1 flex flex-wrap items-center gap-x-3 gap-y-2 border-t pt-3\">\n <Button type=\"button\" size=\"sm\" onClick={() => { setReplyAll(false); setMode(\"replying\") }}>\n <Reply size={15} /> Reply\n </Button>\n {hasCc ? (\n <Button type=\"button\" variant=\"outline\" size=\"sm\" onClick={() => { setReplyAll(true); setMode(\"replying\") }}>\n <ReplyAll size={14} /> Reply all\n </Button>\n ) : null}\n <OpenInGmailButton thread={thread} onOpenInGmail={onOpenInGmail} />\n <span className=\"text-muted-foreground/70 ml-auto inline-flex items-center gap-1.5 text-[12px]\">\n <GitMerge size={13} /> Stays on this thread\n </span>\n </div>\n ) : null}\n\n {canReply && mode === \"replying\" ? (\n <ReplyComposer\n thread={thread}\n me={me}\n replyAll={replyAll}\n tenantName={tenantName}\n onPreviewReply={onPreviewReply}\n onClose={() => setMode(\"idle\")}\n onSend={async (body, includeSignature) => {\n await onSendReply?.({ threadId: thread.threadId, body, includeSignature, replyAll })\n setMode(\"sent\")\n }}\n onDraft={async (body, includeSignature) => {\n if (!onCreateGmailDraft) return\n await onCreateGmailDraft({ threadId: thread.threadId, body, includeSignature, replyAll })\n setMode(\"draft\")\n }}\n draftDisabledReason={draftDisabledReason}\n />\n ) : null}\n\n {canReply && mode === \"sent\" ? (\n <div className=\"border-status-active-border bg-status-active-bg flex items-center gap-2 rounded-md border p-3 text-[13px]\">\n <Check size={16} className=\"text-status-active-fg shrink-0\" />\n <span className=\"flex-1\">\n <b>{replyAll ? \"Reply all sent\" : \"Reply sent\"}</b> · added to the thread. Delivered to{\" \"}\n <b>{displayParticipant(thread.contact).name}</b>. This action stays <b>Pending</b>; playbooks remain stopped.\n </span>\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" onClick={() => setMode(\"idle\")}>\n Done\n </Button>\n </div>\n ) : null}\n\n {canReply && mode === \"draft\" ? (\n <div className=\"border-border bg-muted/30 flex items-center gap-2 rounded-md border p-3 text-[13px]\">\n <GmailMark size={16} />\n <span className=\"flex-1\">\n <b>Draft saved to Gmail.</b> Waiting on the <b>Re: {thread.subject}</b> thread; open it there to finish. Nothing was sent.\n </span>\n <OpenInGmailButton thread={thread} onOpenInGmail={onOpenInGmail} />\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" onClick={() => setMode(\"idle\")}>\n Done\n </Button>\n </div>\n ) : null}\n </div>\n )\n}\n\n/* ── A thread row + its inline reader ───────────────────────────────────── */\n\nfunction ThreadRow({\n thread,\n open,\n onToggleOpen,\n me,\n tenantName,\n onSendReply,\n onCreateGmailDraft,\n onPreviewReply,\n onOpenInGmail,\n}: {\n thread: ConversationThread\n open: boolean\n onToggleOpen: () => void\n} & Pick<ConversationPanelProps, \"me\" | \"tenantName\" | \"onSendReply\" | \"onCreateGmailDraft\" | \"onPreviewReply\" | \"onOpenInGmail\">) {\n const status = effectiveStatus(thread)\n const sortedMessages = React.useMemo(() => sortMessagesChronologically(thread.messages), [thread.messages])\n const last = sortedMessages[sortedMessages.length - 1]\n const lastSender = last ? displayParticipant(last.from) : null\n const meDisplay = me ? displayParticipant(me) : null\n const who = last?.direction === \"outbound\" && sameEmail(lastSender?.email, meDisplay?.email) ? \"You\" : firstName(lastSender?.name ?? \"\")\n const lastSnippet = last ? messageBodySnippet(last, 120) : \"\"\n const pill = STATUS_PILL[status]\n\n return (\n <div\n data-slot=\"conv-thread\"\n data-status={status}\n data-open={open ? \"true\" : undefined}\n className={cn(\"border-border border-b last:border-b-0\", THREAD_ROW_ACCENT[status])}\n >\n <button\n type=\"button\"\n onClick={onToggleOpen}\n aria-expanded={open}\n className=\"flex w-full items-center gap-2.5 px-3 py-2.5 text-left hover:bg-muted/30\"\n >\n <span className={cn(\"size-2 shrink-0 rounded-full\", STATUS_DOT[status])} aria-hidden />\n <span className=\"min-w-0 flex-1\">\n <span className=\"flex items-center gap-2\">\n <span className=\"truncate text-sm font-semibold\">{thread.subject}</span>\n <span className={cn(\"shrink-0 rounded-md border px-1.5 py-px text-[10px] font-medium leading-4\", pill.cls)}>\n {pill.label}\n </span>\n </span>\n <span className=\"text-muted-foreground block truncate text-xs\">\n <b className=\"text-foreground/80\">{displayParticipant(thread.contact).name}</b> · {who}: {lastSnippet}\n </span>\n </span>\n <span className=\"text-muted-foreground/60 shrink-0 text-xs\">{thread.lastWhen}</span>\n {open ? (\n <ChevronUp size={15} className=\"text-muted-foreground shrink-0\" />\n ) : (\n <ChevronDown size={15} className=\"text-muted-foreground shrink-0\" />\n )}\n </button>\n\n {open ? (\n <div className=\"px-3 pb-3\">\n <ThreadBody\n thread={thread}\n me={me}\n tenantName={tenantName}\n onSendReply={onSendReply}\n onCreateGmailDraft={onCreateGmailDraft}\n onPreviewReply={onPreviewReply}\n onOpenInGmail={onOpenInGmail}\n />\n </div>\n ) : null}\n </div>\n )\n}\n\n/* ── The hub ─────────────────────────────────────────────────────────────── */\n\nfunction ConversationPanel({\n threads,\n me,\n tenantName,\n onSendReply,\n onCreateGmailDraft,\n onPreviewReply,\n onOpenInGmail,\n defaultOpenThreadId,\n className,\n}: ConversationPanelProps) {\n const responded = threads.filter((t) => t.status === \"responded\" && t.canReply !== false).length\n const draft = threads.filter((t) => effectiveStatus(t) === \"draft\").length\n const awaiting = threads.filter((t) => effectiveStatus(t) === \"awaiting\").length\n const anyPaused = threads.some((t) => t.paused)\n const prioritizedThread =\n threads.find((t) => t.status === \"responded\" && t.canReply !== false) ??\n threads.find((t) => effectiveStatus(t) === \"draft\") ??\n threads.find((t) => effectiveStatus(t) === \"awaiting\")\n const hubGmailThread =\n threads.find((t) => t.status === \"responded\" && t.canReply !== false && canOpenInGmail(t, onOpenInGmail)) ??\n threads.find((t) => effectiveStatus(t) === \"draft\" && canOpenInGmail(t, onOpenInGmail)) ??\n threads.find((t) => effectiveStatus(t) === \"awaiting\" && canOpenInGmail(t, onOpenInGmail)) ??\n threads.find((t) => canOpenInGmail(t, onOpenInGmail))\n const firstAwaiting = threads.find((t) => effectiveStatus(t) === \"awaiting\")\n\n const [hubOpen, setHubOpen] = React.useState(true)\n const [openId, setOpenId] = React.useState<string | null>(() => {\n if (defaultOpenThreadId) return defaultOpenThreadId\n return prioritizedThread ? prioritizedThread.threadId : null\n })\n\n if (!threads.length) return null\n\n // Header badge state: a responded reply leads, then drafts to finish, then sent mail awaiting a reply.\n const badge =\n responded > 0\n ? { label: \"Email response detected\", dot: \"bg-status-warning-fg\", ring: \"bg-status-warning-fg/30\" }\n : draft > 0\n ? { label: \"Draft ready\", dot: \"bg-status-pending-fg\", ring: \"bg-status-pending-fg/30\" }\n : awaiting > 0\n ? { label: \"Email sent · awaiting reply\", dot: \"bg-status-info-fg\", ring: \"bg-status-info-fg/30\" }\n : { label: \"Conversations\", dot: \"bg-muted-foreground/50\", ring: \"bg-muted-foreground/20\" }\n\n const headTitle =\n responded > 0\n ? `${responded} ${responded === 1 ? \"reply needs\" : \"replies need\"} your response`\n : draft > 0\n ? `Draft ready on ${draft} ${draft === 1 ? \"thread\" : \"threads\"}`\n : awaiting > 0\n ? awaiting === 1 && firstAwaiting\n ? `Awaiting a response from ${firstName(displayParticipant(firstAwaiting.contact).name)}`\n : `Awaiting responses on ${awaiting} threads`\n : `${threads.length} email ${threads.length === 1 ? \"thread\" : \"threads\"}`\n\n const panelState = responded > 0 ? \"responded\" : draft > 0 ? \"draft\" : awaiting > 0 ? \"awaiting\" : \"viewing\"\n\n return (\n <section\n data-slot=\"conversation-panel\"\n data-responded={responded > 0 ? \"true\" : undefined}\n data-state={panelState}\n className={cn(\n \"bg-background overflow-hidden rounded-xl border\",\n panelState === \"responded\"\n ? \"border-status-warning-border\"\n : panelState === \"awaiting\"\n ? \"border-status-info-border\"\n : \"border-border\",\n className,\n )}\n >\n <div\n data-slot=\"conversation-panel-header\"\n className={cn(\n \"flex w-full items-center gap-2 px-3 py-2.5\",\n panelState === \"responded\"\n ? \"bg-status-warning-bg/45\"\n : panelState === \"awaiting\"\n ? \"bg-status-info-bg/55\"\n : \"bg-background\",\n )}\n >\n <button\n type=\"button\"\n onClick={() => setHubOpen((v) => !v)}\n aria-expanded={hubOpen}\n className=\"flex min-w-0 flex-1 items-center gap-3 text-left\"\n >\n <span\n data-slot=\"conversation-badge\"\n className={cn(\n \"inline-flex items-center gap-1.5 rounded-full px-2 py-0.5 text-[11px] font-semibold\",\n responded > 0\n ? \"bg-status-warning-bg text-status-warning-fg\"\n : draft > 0\n ? \"bg-status-pending-bg text-status-pending-fg\"\n : awaiting > 0\n ? \"bg-status-info-bg text-status-info-fg\"\n : \"bg-muted text-muted-foreground\"\n )}\n >\n <span className=\"relative inline-flex size-2\">\n <span className={cn(\"absolute inline-flex h-full w-full animate-ping rounded-full opacity-75\", badge.ring)} />\n <span className={cn(\"relative inline-flex size-2 rounded-full\", badge.dot)} />\n </span>\n {badge.label}\n </span>\n <span className=\"min-w-0 flex-1\">\n <span className=\"block truncate text-sm font-semibold leading-tight\">{headTitle}</span>\n <span className=\"text-muted-foreground mt-0.5 block truncate text-xs\">\n {threads.length} {threads.length === 1 ? \"thread\" : \"threads\"} on this action\n {anyPaused ? <> · <b>playbook stopped</b></> : null}\n </span>\n </span>\n {hubOpen ? (\n <ChevronUp size={16} className=\"text-muted-foreground shrink-0\" />\n ) : (\n <ChevronDown size={16} className=\"text-muted-foreground shrink-0\" />\n )}\n </button>\n {hubGmailThread ? (\n <div className=\"shrink-0\" onClick={(event) => event.stopPropagation()}>\n <OpenInGmailButton thread={hubGmailThread} onOpenInGmail={onOpenInGmail} />\n </div>\n ) : null}\n </div>\n\n {hubOpen ? (\n <div className=\"border-border border-t\">\n {threads.map((t) => (\n <ThreadRow\n key={t.threadId}\n thread={t}\n open={openId === t.threadId}\n onToggleOpen={() => setOpenId((cur) => (cur === t.threadId ? null : t.threadId))}\n me={me}\n tenantName={tenantName}\n onSendReply={onSendReply}\n onCreateGmailDraft={onCreateGmailDraft}\n onPreviewReply={onPreviewReply}\n onOpenInGmail={onOpenInGmail}\n />\n ))}\n </div>\n ) : null}\n </section>\n )\n}\n\nexport { ConversationPanel }\n"],"mappings":"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","names":["_a","_b"]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/conversation-panel.tsx"],"sourcesContent":["\"use client\"\n\n/**\n * conversation-panel.tsx — in-case email-thread reader + reply, for the case\n * panel (\"Email response detected\" hub).\n *\n * v1 scope (WIT-853 / WIT-802): INLINE thread reading + reply only.\n * - A collapsible hub header with a pulse badge whose color reflects state:\n * responded (a reply needs you) / awaiting (sent, waiting) / viewing (read-only).\n * - A list of thread rows; clicking one opens a Gmail-style reader inline,\n * right under the row, with collapsible messages + quoted-history toggle.\n * - Reply / Reply-all composer with a signature toggle and two send paths:\n * Preview→Send (in-app) and \"Open draft in Gmail\" (deep link).\n * - A \"playbook stopped\" banner when the customer's reply halted the sequence,\n * and a read-only notice when the operator is not a thread participant.\n *\n * The bottom-right floating dock / side-by-side compare is intentionally OUT of\n * scope here and tracked separately (WIT-855).\n *\n * Presentational: all data + side effects come from the consumer. Email bodies\n * (`bodyHtml`, `quoted.html`) are sanitized before rendering.\n */\n\nimport * as React from \"react\"\nimport {\n ChevronDown,\n ChevronUp,\n CornerUpLeft,\n CheckCheck,\n MailOpen,\n FilePenLine,\n Reply,\n ReplyAll,\n Eye,\n Send,\n Lock,\n Pause,\n GitMerge,\n Check,\n X,\n FileText,\n} from \"lucide-react\"\n\nimport { cn } from \"../lib/utils\"\nimport { getInitials } from \"../lib/user-display\"\nimport { BRAND_ICONS } from \"../lib/icons\"\nimport { Avatar, AvatarFallback, AvatarImage } from \"./avatar\"\nimport { Button } from \"./button\"\nimport { Switch } from \"./switch\"\nimport { Textarea } from \"./textarea\"\nimport { RichTextToolbar } from \"./rich-text-toolbar\"\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuLabel,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n} from \"./dropdown-menu\"\nimport { EmailBody } from \"./email-body\"\nimport { decodeEmailDisplayText, emailBodySnippet, formatAddressList, normalizeEmailSender } from \"./email-display-helpers\"\nimport {\n Dialog,\n DialogContent,\n DialogHeader,\n DialogTitle,\n DialogDescription,\n DialogFooter,\n} from \"./dialog\"\n\n/* ── Types ───────────────────────────────────────────────────────────────── */\n\nexport interface ConvParticipant {\n name: string\n email: string\n avatarUrl?: string | null\n role?: string\n}\n\nexport interface ConvMessage {\n id: string\n direction: \"inbound\" | \"outbound\"\n from: ConvParticipant\n to: ConvParticipant\n /** Absolute timestamp label, e.g. \"Jun 1, 2026, 9:12 AM\". */\n date: string\n /**\n * Raw chronological timestamp for deterministic thread ordering. Prefer\n * `sentAt` for outbound messages and `receivedAt` for inbound messages.\n * Accepts ISO/RFC822 strings, Date objects, epoch milliseconds, or Gmail\n * internalDate values as strings/numbers. Display-only `date` / `ago` labels\n * are never parsed for ordering.\n */\n timestamp?: string | number | Date | null\n rawTimestamp?: string | number | Date | null\n sentAt?: string | number | Date | null\n receivedAt?: string | number | Date | null\n /** Compatibility with data contracts that pass through source field names. */\n sent_at?: string | number | Date | null\n received_at?: string | number | Date | null\n internalDate?: string | number | Date | null\n gmailInternalDate?: string | number | Date | null\n internal_date?: string | number | Date | null\n rfc822Date?: string | number | Date | null\n dateHeader?: string | number | Date | null\n /** Relative label, e.g. \"2 days ago\". */\n ago?: string\n receipt?: { kind: \"new\" | \"read\" | \"opened\" | \"sent\" | \"draft\"; label: string }\n /** HTML body (preferred). Sanitized by the component before rendering. */\n bodyHtml?: string\n /**\n * Pre-split signature HTML for this message, sanitized server-side by the\n * sender's signature pipeline (which preserves the authored Gmail look —\n * fonts, colors, logos). When present it renders inside the collapsed\n * details section (\"•••\" toggle) instead of relying on the heuristic footer\n * split of `bodyHtml`, so long signatures never expand the thread by\n * default.\n */\n signatureHtml?: string | null\n /** Plain-text fallback when `bodyHtml` is absent. */\n body?: string\n /** Quoted prior message, collapsed behind a toggle. Sanitized before rendering. */\n quoted?: { attr: string; html: string }\n}\n\nexport type ConvStatus = \"responded\" | \"awaiting\" | \"viewing\" | \"draft\"\n\n/**\n * A follow-up template offered in the in-composer \"Templates\" picker (WIT-970).\n * Presentational: the consumer fills variables for THIS thread's recipient\n * before passing it in, so `body` is composer-ready text. `tags` (e.g. \"Reply\")\n * are shown as chips so reps can recognize the right template at a glance.\n */\nexport interface ConversationReplyTemplate {\n id: string\n name: string\n /** Composer-ready reply body, already personalized by the consumer. */\n body: string\n /** Optional tags shown as chips in the picker for findability. */\n tags?: string[]\n}\n\nexport interface ConversationThread {\n threadId: string\n subject: string\n status: ConvStatus\n /** Relative label for the most recent activity. */\n lastWhen?: string\n contact: ConvParticipant\n cc?: ConvParticipant[]\n /** Set when this thread's reply halted a playbook (terminal). */\n paused?: { playbook: string } | null\n /** false => operator cannot reply or create drafts from this thread. */\n canReply?: boolean\n /** Explains why reply and draft creation are disabled. */\n replyDisabledReason?: string\n /** Existing Gmail draft or thread URL. Rendered as a new-tab link when present. */\n openInGmailUrl?: string | null\n /** Forces the Open in Gmail action into a disabled state. */\n openInGmailDisabled?: boolean\n /** Tooltip/read-only copy for a disabled Open in Gmail action. */\n openInGmailDisabledReason?: string | null\n messages: ConvMessage[]\n /** Prefilled reply draft body. */\n draft?: string\n /** Signature text appended to replies (plain text). */\n signature?: string\n /**\n * Follow-up templates offered in the composer's \"Templates\" picker, already\n * personalized for this thread's recipient (WIT-970). Omit/empty hides the\n * picker.\n */\n replyTemplates?: ConversationReplyTemplate[]\n}\n\nexport interface ConversationReplyPayload {\n threadId: string\n body: string\n includeSignature: boolean\n replyAll: boolean\n}\n\n/**\n * Result of a server-side reply preview. `htmlBody` is the exact HTML the server\n * prepared for this reply (sanitized by the component before render).\n * `confirmationToken`, when present, identifies the prepared confirmation so a\n * consumer can reuse it for the final send instead of re-preparing the message.\n */\nexport interface ConversationReplyPreview {\n htmlBody: string\n confirmationToken?: string\n}\n\nexport interface ConversationPanelProps {\n threads: ConversationThread[]\n /** Current operator: drives \"to me\" + the reply avatar. */\n me?: ConvParticipant\n /** Deployment brand, used in the paused-playbook copy. */\n tenantName?: string\n onSendReply?: (payload: ConversationReplyPayload) => void | Promise<void>\n onCreateGmailDraft?: (payload: ConversationReplyPayload) => void | Promise<void>\n /**\n * Server-side preview contract. When provided, the reply preview requests the\n * exact send HTML from the server, the component sanitizes it before render,\n * and retains any returned `confirmationToken` in preview state so a consumer\n * can reuse it for the final send. When omitted, the composer falls back to a\n * clearly labeled local draft preview only.\n */\n onPreviewReply?: (payload: ConversationReplyPayload) => Promise<ConversationReplyPreview>\n onOpenInGmail?: (threadId: string) => void\n /**\n * Fired when the rep applies a reply template from the composer picker\n * (WIT-970) — for analytics / template-usage tracking. Optional.\n */\n onApplyReplyTemplate?: (info: { threadId: string; templateId: string }) => void\n /** Inline-open this thread initially (defaults to the first responded one). */\n defaultOpenThreadId?: string\n className?: string\n}\n\n/* ── Shared helpers ──────────────────────────────────────────────────────── */\n\nfunction escapeHtml(s: string): string {\n return s.replace(/&/g, \"&\").replace(/</g, \"<\").replace(/>/g, \">\")\n}\n\n/** Plain-text -> simple paragraph HTML for the Preview / sent-message body. */\nfunction textToHtml(text: string): string {\n return decodeEmailDisplayText(text)\n .split(/\\n{2,}/)\n .map((p) => p.trim())\n .filter(Boolean)\n .map((p) => `<p>${escapeHtml(p).replace(/\\n/g, \"<br>\")}</p>`)\n .join(\"\")\n}\n\nfunction displayParticipant(person: ConvParticipant) {\n return normalizeEmailSender({ name: person.name, email: person.email, fallbackName: person.email || person.name })\n}\n\nfunction firstName(name: string): string {\n return decodeEmailDisplayText(name).split(\" \")[0] || decodeEmailDisplayText(name)\n}\n\nfunction sameEmail(a?: string | null, b?: string | null): boolean {\n return Boolean(a && b && a.trim().toLowerCase() === b.trim().toLowerCase())\n}\n\nfunction messageBodySnippet(message: Pick<ConvMessage, \"body\" | \"bodyHtml\">, maxLength = 140): string {\n return emailBodySnippet({ bodyHtml: message.bodyHtml, body: message.body }, maxLength)\n}\n\n\ntype MessageTimestampValue = string | number | Date | null | undefined\n\nfunction parseMessageTimestampValue(value: MessageTimestampValue): number | null {\n if (value == null || value === \"\") return null\n\n if (value instanceof Date) {\n const time = value.getTime()\n return Number.isNaN(time) ? null : time\n }\n\n if (typeof value === \"number\") {\n if (!Number.isFinite(value)) return null\n return value < 10_000_000_000 ? value * 1000 : value\n }\n\n const trimmed = value.trim()\n if (!trimmed) return null\n\n if (/^\\d+$/.test(trimmed)) {\n const numeric = Number(trimmed)\n if (!Number.isFinite(numeric)) return null\n return numeric < 10_000_000_000 ? numeric * 1000 : numeric\n }\n\n const parsed = Date.parse(trimmed)\n return Number.isNaN(parsed) ? null : parsed\n}\n\nfunction messageTimestamp(message: ConvMessage): number | null {\n const directional = message.direction === \"outbound\"\n ? [message.sentAt, message.sent_at]\n : [message.receivedAt, message.received_at]\n\n const candidates: MessageTimestampValue[] = [\n ...directional,\n message.rawTimestamp,\n message.timestamp,\n message.gmailInternalDate,\n message.internalDate,\n message.internal_date,\n message.rfc822Date,\n message.dateHeader,\n ]\n\n for (const candidate of candidates) {\n const parsed = parseMessageTimestampValue(candidate)\n if (parsed !== null) return parsed\n }\n\n return null\n}\n\nfunction sortMessagesChronologically(messages: ConvMessage[]): ConvMessage[] {\n return messages\n .map((message, index) => ({ message, index, timestamp: messageTimestamp(message) }))\n .sort((a, b) => {\n if (a.timestamp !== null && b.timestamp !== null && a.timestamp !== b.timestamp) {\n return a.timestamp - b.timestamp\n }\n return a.index - b.index\n })\n .map((entry) => entry.message)\n}\n\nfunction GmailMark({ size = 14 }: { size?: number }) {\n return (\n // eslint-disable-next-line @next/next/no-img-element\n <img\n src={BRAND_ICONS.gmail.icon}\n alt=\"Gmail\"\n width={size}\n height={size}\n style={{ width: size, height: size, objectFit: \"contain\", display: \"block\" }}\n />\n )\n}\n\nfunction PersonAvatar({ person, size = \"sm\" }: { person: ConvParticipant; size?: \"sm\" | \"default\" }) {\n const display = displayParticipant(person)\n\n return (\n <Avatar size={size}>\n {person.avatarUrl ? <AvatarImage src={person.avatarUrl} alt={display.name} /> : null}\n <AvatarFallback className=\"bg-muted text-muted-foreground text-[10px] font-medium uppercase\">\n {getInitials({ name: display.name, email: display.email ?? person.email })}\n </AvatarFallback>\n </Avatar>\n )\n}\n\nconst STATUS_PILL: Record<ConvStatus, { label: string; cls: string }> = {\n responded: { label: \"NEW REPLY\", cls: \"bg-status-warning-bg text-status-warning-fg border-status-warning-border\" },\n draft: { label: \"Draft\", cls: \"bg-background text-foreground/80 border-border\" },\n awaiting: { label: \"SENT\", cls: \"bg-status-info-bg text-status-info-fg border-status-info-border\" },\n viewing: { label: \"Viewing\", cls: \"bg-muted text-muted-foreground border-border\" },\n}\n\nconst STATUS_DOT: Record<ConvStatus, string> = {\n responded: \"bg-status-warning-fg\",\n draft: \"bg-status-pending-fg\",\n awaiting: \"bg-status-info-fg\",\n viewing: \"bg-muted-foreground/50\",\n}\n\nconst THREAD_ROW_ACCENT: Record<ConvStatus, string> = {\n responded: \"border-l-4 border-l-status-warning-border bg-status-warning-bg/25\",\n awaiting: \"border-l-4 border-l-status-info-border bg-status-info-bg/25\",\n draft: \"border-l-4 border-l-status-pending-border bg-status-pending-bg/20\",\n viewing: \"\",\n}\n\nconst RECEIPT_CHIP: Record<NonNullable<ConvMessage[\"receipt\"]>[\"kind\"], string> = {\n new: \"border-status-warning-border bg-status-warning-bg text-status-warning-fg\",\n read: \"border-status-info-border bg-status-info-bg text-status-info-fg\",\n opened: \"border-status-info-border bg-status-info-bg text-status-info-fg\",\n sent: \"border-status-info-border bg-status-info-bg text-status-info-fg\",\n draft: \"border-status-pending-border bg-status-pending-bg text-status-pending-fg\",\n}\n\nfunction effectiveStatus(t: ConversationThread): ConvStatus {\n return t.canReply === false ? \"viewing\" : t.status\n}\n\nfunction disabledOpenInGmailReason(thread: ConversationThread): string {\n return (\n thread.openInGmailDisabledReason?.trim() ||\n thread.replyDisabledReason?.trim() ||\n \"Gmail access is not available for this thread.\"\n )\n}\n\nfunction canOpenInGmail(thread: ConversationThread, onOpenInGmail?: (threadId: string) => void): boolean {\n return thread.openInGmailDisabled !== true && Boolean(thread.openInGmailUrl || onOpenInGmail)\n}\n\nfunction OpenInGmailButton({\n thread,\n onOpenInGmail,\n label = \"Open in Gmail\",\n}: {\n thread: ConversationThread\n onOpenInGmail?: (threadId: string) => void\n label?: string\n}) {\n const hasConfiguredAction = Boolean(thread.openInGmailUrl || onOpenInGmail || thread.openInGmailDisabled || thread.openInGmailDisabledReason)\n if (!hasConfiguredAction) return null\n\n const disabled = !canOpenInGmail(thread, onOpenInGmail)\n const disabledReason = disabled\n ? disabledOpenInGmailReason(thread)\n : undefined\n\n if (!disabled && thread.openInGmailUrl) {\n return (\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" asChild>\n <a href={thread.openInGmailUrl} target=\"_blank\" rel=\"noopener noreferrer\">\n <GmailMark size={14} /> {label}\n </a>\n </Button>\n )\n }\n\n return (\n <span className=\"inline-flex\" title={disabledReason}>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n disabled={disabled}\n aria-disabled={disabled || undefined}\n aria-label={disabledReason ? `${label}: ${disabledReason}` : label}\n onClick={disabled ? undefined : () => onOpenInGmail?.(thread.threadId)}\n >\n <GmailMark size={14} /> {label}\n </Button>\n </span>\n )\n}\n\n/* ── One message (collapsible) ──────────────────────────────────────────── */\n\nfunction MessageView({\n message,\n expanded,\n onToggle,\n me,\n}: {\n message: ConvMessage\n expanded: boolean\n onToggle: () => void\n me?: ConvParticipant\n}) {\n const [quoteOpen, setQuoteOpen] = React.useState(false)\n const fromDisplay = displayParticipant(message.from)\n const toDisplay = displayParticipant(message.to)\n\n if (!expanded) {\n const snippet = messageBodySnippet(message, 140)\n\n return (\n <button\n type=\"button\"\n data-slot=\"conv-message-collapsed\"\n onClick={onToggle}\n className=\"flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left hover:bg-muted/40\"\n >\n <PersonAvatar person={message.from} />\n <span className=\"text-muted-foreground min-w-0 flex-1 truncate text-[13px]\">\n <b className=\"text-foreground\">{firstName(fromDisplay.name)}</b> · {snippet}\n </span>\n <span className=\"text-muted-foreground/60 shrink-0 text-xs\">{message.ago ?? message.date}</span>\n <ChevronDown size={13} className=\"text-muted-foreground shrink-0\" />\n </button>\n )\n }\n\n const meDisplay = me ? displayParticipant(me) : null\n const toLabel = meDisplay && sameEmail(toDisplay.email, meDisplay.email) ? \"me\" : firstName(toDisplay.name)\n\n return (\n <div data-slot=\"conv-message\" className=\"rounded-md border border-border bg-background\">\n <button\n type=\"button\"\n onClick={onToggle}\n className=\"flex w-full items-start gap-2 px-3 py-2 text-left\"\n >\n <PersonAvatar person={message.from} size=\"default\" />\n <span className=\"min-w-0 flex-1\">\n <span className=\"flex flex-wrap items-baseline gap-x-1.5\">\n <span className=\"text-[13px] font-semibold\">{fromDisplay.name}</span>\n {fromDisplay.email ? (\n <span className=\"text-muted-foreground/60 truncate text-xs\"><{fromDisplay.email}></span>\n ) : null}\n </span>\n <span className=\"text-muted-foreground block text-xs\">\n to <b>{toLabel}</b>\n </span>\n </span>\n <span className=\"flex shrink-0 items-center gap-2\">\n {message.receipt ? (\n <span className={cn(\"inline-flex items-center gap-1 rounded-md border px-1.5 py-px text-[10px] font-semibold leading-4\", RECEIPT_CHIP[message.receipt.kind])}>\n {message.receipt.kind === \"new\" ? (\n <CornerUpLeft size={11} />\n ) : message.receipt.kind === \"read\" || message.receipt.kind === \"sent\" ? (\n <CheckCheck size={11} />\n ) : message.receipt.kind === \"draft\" ? (\n <FilePenLine size={11} />\n ) : (\n <MailOpen size={11} />\n )}\n {message.receipt.label}\n </span>\n ) : null}\n <span className=\"text-muted-foreground/60 text-xs\">{message.date}</span>\n <ChevronUp size={13} className=\"text-muted-foreground\" />\n </span>\n </button>\n\n <div className=\"px-3 pb-2.5\">\n <div data-slot=\"conv-message-body\">\n <EmailBody\n html={message.bodyHtml}\n text={message.body}\n detailsHtml={message.signatureHtml ?? undefined}\n variant=\"history\"\n collapseDetails={true}\n className=\"text-sm\"\n />\n </div>\n\n {message.quoted ? (\n <div className=\"mt-2\">\n <button\n type=\"button\"\n onClick={() => setQuoteOpen((v) => !v)}\n className=\"text-muted-foreground hover:bg-muted rounded px-1.5 text-xs leading-5\"\n title={quoteOpen ? \"Hide quoted text\" : \"Show quoted text\"}\n >\n •••\n </button>\n {quoteOpen ? (\n <div className=\"border-border text-muted-foreground mt-1 border-l-2 pl-3 text-[13px]\">\n <p className=\"mb-1\">{decodeEmailDisplayText(message.quoted.attr)}</p>\n <div data-slot=\"conv-quoted-body\">\n <EmailBody html={message.quoted.html} variant=\"history\" collapseDetails={false} />\n </div>\n </div>\n ) : null}\n </div>\n ) : null}\n </div>\n </div>\n )\n}\n\n/* ── Reply composer ─────────────────────────────────────────────────────── */\n\ntype PreviewState = {\n loading: boolean\n /** Sanitized HTML ready to render. */\n html: string | null\n /** Confirmation token returned by the server preview, retained for reuse on send. */\n confirmationToken: string | null\n error: string | null\n /** True when `html` came from the local fallback rather than the server. */\n local: boolean\n}\n\nconst IDLE_PREVIEW: PreviewState = { loading: false, html: null, confirmationToken: null, error: null, local: false }\n\n/**\n * Matches an author-declared manual-fill placeholder ({{personalize}},\n * {{personalization_recent_news}}, …). These have no resolver and must be\n * replaced by the rep before sending; the composer blocks send while any remain.\n * Non-global so repeated `.test()` calls during render are stateless.\n */\nconst MANUAL_FILL_TOKEN_RE = /\\{\\{\\s*personaliz(?:e|ation)(?:_[a-z0-9]+)*\\s*\\}\\}/i\n\nfunction ReplyComposer({\n thread,\n me,\n replyAll,\n tenantName,\n onClose,\n onSend,\n onDraft,\n onPreviewReply,\n onApplyTemplate,\n draftDisabledReason,\n}: {\n thread: ConversationThread\n me?: ConvParticipant\n replyAll: boolean\n tenantName?: string\n onClose: () => void\n onSend: (body: string, includeSignature: boolean) => void | Promise<void>\n onDraft: (body: string, includeSignature: boolean) => void | Promise<void>\n onPreviewReply?: (payload: ConversationReplyPayload) => Promise<ConversationReplyPreview>\n onApplyTemplate?: (templateId: string) => void\n draftDisabledReason?: string | null\n}) {\n const [body, setBody] = React.useState(thread.draft ?? \"\")\n const [appliedTemplate, setAppliedTemplate] = React.useState<string | null>(null)\n const [sig, setSig] = React.useState(true)\n const replyTemplates = thread.replyTemplates ?? []\n // Manual-fill placeholders ({{personalize…}}) are author-required: the rep\n // must replace them before sending. While any remain in the body we block\n // send/preview so the personalization is never sent as a literal token.\n const hasManualFillToken = MANUAL_FILL_TOKEN_RE.test(body)\n\n const applyTemplate = (template: ConversationReplyTemplate) => {\n setBody(template.body)\n setAppliedTemplate(template.name)\n onApplyTemplate?.(template.id)\n }\n const [preview, setPreview] = React.useState(false)\n const [previewState, setPreviewState] = React.useState<PreviewState>(IDLE_PREVIEW)\n const [sending, setSending] = React.useState(false)\n const [sendError, setSendError] = React.useState<string | null>(null)\n const ccList = replyAll ? thread.cc ?? [] : []\n const subject = /^re:/i.test(thread.subject) ? thread.subject : `Re: ${thread.subject}`\n const draftDisabled = Boolean(draftDisabledReason)\n\n const localPreviewHtml = textToHtml(body) + (sig && thread.signature ? textToHtml(thread.signature) : \"\")\n\n const openPreview = async () => {\n setPreview(true)\n setSendError(null)\n\n if (!onPreviewReply) {\n // No server preview contract: render a sanitized local draft preview only.\n setPreviewState({ loading: false, html: localPreviewHtml, confirmationToken: null, error: null, local: true })\n return\n }\n\n setPreviewState({ loading: true, html: null, confirmationToken: null, error: null, local: false })\n try {\n const result = await onPreviewReply({ threadId: thread.threadId, body, includeSignature: sig, replyAll })\n setPreviewState({\n loading: false,\n html: result.htmlBody ?? \"\",\n confirmationToken: result.confirmationToken ?? null,\n error: null,\n local: false,\n })\n } catch (error) {\n setPreviewState({\n loading: false,\n html: null,\n confirmationToken: null,\n error: error instanceof Error ? error.message : \"Could not load the preview. Please try again.\",\n local: false,\n })\n }\n }\n\n const handleSend = async () => {\n setSending(true)\n setSendError(null)\n try {\n await onSend(body, sig)\n setPreview(false)\n setPreviewState(IDLE_PREVIEW)\n } catch (error) {\n setSendError(error instanceof Error ? error.message : \"Could not send this reply. Please try again.\")\n } finally {\n setSending(false)\n }\n }\n\n const handleDraft = async () => {\n if (draftDisabled) return\n setSending(true)\n setSendError(null)\n try {\n await onDraft(body, sig)\n setPreview(false)\n setPreviewState(IDLE_PREVIEW)\n } catch (error) {\n setSendError(error instanceof Error ? error.message : \"Could not create the Gmail draft. Please try again.\")\n } finally {\n setSending(false)\n }\n }\n\n return (\n <div data-slot=\"conv-reply\" className=\"border-border bg-muted/20 rounded-md border p-3\">\n <div className=\"mb-2 flex items-center gap-2\">\n {me ? <PersonAvatar person={me} /> : null}\n <span className=\"flex-1 text-[13px] font-medium\">\n {replyAll ? (\n <>\n Reply all{\" \"}\n <span className=\"text-muted-foreground font-normal\">· {1 + ccList.length} recipients</span>\n </>\n ) : (\n <>\n Reply to <b>{firstName(displayParticipant(thread.contact).name)}</b>\n </>\n )}\n </span>\n <span className=\"text-muted-foreground inline-flex items-center gap-1 text-[11px]\">\n <GitMerge size={11} /> Same thread\n </span>\n <button type=\"button\" onClick={onClose} title=\"Discard reply\" className=\"text-muted-foreground hover:text-foreground\">\n <X size={15} />\n </button>\n </div>\n\n <div className=\"border-border mb-2 space-y-1 border-b pb-2 text-[13px]\">\n <div className=\"flex items-center gap-1.5\">\n <span className=\"text-muted-foreground w-12 shrink-0 text-[11px] font-medium\">To</span>\n <span className=\"font-medium\">{displayParticipant(thread.contact).name}</span>\n <span className=\"text-muted-foreground/60 truncate text-xs\">{displayParticipant(thread.contact).email ?? thread.contact.email}</span>\n </div>\n {replyAll && ccList.length ? (\n <div className=\"flex items-start gap-1.5\">\n <span className=\"text-muted-foreground w-12 shrink-0 text-[11px] font-medium\">Cc</span>\n <span className=\"text-muted-foreground text-xs\">\n {formatAddressList(ccList.map((c) => `${displayParticipant(c).name} <${displayParticipant(c).email ?? c.email}>`))}\n </span>\n </div>\n ) : null}\n <div className=\"flex items-center gap-1.5\">\n <span className=\"text-muted-foreground w-12 shrink-0 text-[11px] font-medium\">Subject</span>\n <span className=\"truncate\">{subject}</span>\n <span className=\"text-muted-foreground/60 ml-auto inline-flex items-center gap-1 text-[11px]\">\n <Lock size={10} /> on thread\n </span>\n </div>\n </div>\n\n <Textarea\n value={body}\n onChange={(e) => setBody(e.target.value)}\n placeholder=\"Write your reply…\"\n className=\"min-h-28 resize-y text-sm\"\n onKeyDown={(e) => {\n if ((e.metaKey || e.ctrlKey) && e.key === \"Enter\") {\n e.preventDefault()\n if (!hasManualFillToken) void openPreview()\n }\n }}\n />\n\n {sig && thread.signature ? (\n <div className=\"text-muted-foreground mt-2 whitespace-pre-line border-l-2 border-border pl-3 text-[13px]\">\n <span className=\"text-muted-foreground/60 mr-1\">--</span>\n {thread.signature}\n </div>\n ) : null}\n\n {replyTemplates.length > 0 ? (\n <div className=\"mt-2 flex flex-wrap items-center gap-2\">\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button type=\"button\" variant=\"outline\" size=\"sm\" disabled={sending} data-slot=\"conv-reply-template-trigger\">\n <FileText size={14} /> Templates\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"start\" className=\"max-w-xs\">\n <DropdownMenuLabel>Templates</DropdownMenuLabel>\n <DropdownMenuSeparator />\n {replyTemplates.map((template) => (\n <DropdownMenuItem\n key={template.id}\n onSelect={() => applyTemplate(template)}\n className=\"flex flex-col items-start gap-0.5\"\n >\n <span className=\"text-[13px] font-medium\">{template.name}</span>\n {template.tags && template.tags.length ? (\n <span className=\"flex flex-wrap gap-1\">\n {template.tags.map((tag) => (\n <span\n key={tag}\n className=\"border-border bg-muted text-muted-foreground rounded border px-1 py-px text-[10px] leading-4\"\n >\n {tag}\n </span>\n ))}\n </span>\n ) : null}\n </DropdownMenuItem>\n ))}\n </DropdownMenuContent>\n </DropdownMenu>\n {appliedTemplate ? (\n <span className=\"text-muted-foreground inline-flex items-center gap-1 text-[11px]\">\n <Check size={11} /> Applied “{appliedTemplate}” · edit before sending\n </span>\n ) : null}\n </div>\n ) : null}\n\n {hasManualFillToken ? (\n <p data-slot=\"conv-reply-manual-fill\" role=\"alert\" className=\"text-destructive mt-2 text-[12px]\">\n Fill in the personalize placeholder before sending. Replace each {\"{{personalize}}\"} with a real, account-specific line.\n </p>\n ) : null}\n\n <div className=\"mt-2 flex flex-wrap items-center gap-2\">\n <RichTextToolbar />\n <label className=\"text-muted-foreground ml-auto inline-flex cursor-pointer items-center gap-1.5 text-[12px]\">\n <Switch checked={sig} onCheckedChange={setSig} aria-label=\"Toggle signature\" />\n Signature\n </label>\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n disabled={sending || hasManualFillToken}\n onClick={() => void openPreview()}\n >\n <Eye size={14} /> Preview\n </Button>\n <Button\n type=\"button\"\n size=\"sm\"\n disabled={sending || hasManualFillToken}\n onClick={() => void openPreview()}\n >\n <Send size={14} /> Send\n </Button>\n </div>\n\n <Dialog open={preview} onOpenChange={(open) => { if (!sending) { setPreview(open); if (!open) setPreviewState(IDLE_PREVIEW) } }}>\n <DialogContent className=\"max-w-xl\">\n <DialogHeader>\n <DialogTitle className=\"flex items-center gap-1.5 text-[15px]\">\n <Eye size={15} /> {previewState.local ? \"Local draft preview\" : \"Reply preview\"}\n </DialogTitle>\n <DialogDescription>\n {previewState.local\n ? \"Local draft preview only. The server prepares the exact message on send.\"\n : <>Stays on the {subject.replace(/^Re:\\s*/i, \"\")} thread. Gmail keeps it threaded.</>}\n </DialogDescription>\n </DialogHeader>\n <div className=\"border-border space-y-1 rounded-md border p-3 text-[13px]\">\n <div>\n <span className=\"text-muted-foreground\">To </span>\n <b>{displayParticipant(thread.contact).name}</b>{\" \"}\n <span className=\"text-muted-foreground/60\"><{displayParticipant(thread.contact).email ?? thread.contact.email}></span>\n </div>\n {replyAll && ccList.length ? (\n <div className=\"text-muted-foreground\">Cc {formatAddressList(ccList.map((c) => `${displayParticipant(c).name} <${displayParticipant(c).email ?? c.email}>`))}</div>\n ) : null}\n <div>\n <span className=\"text-muted-foreground\">Subject </span>\n {subject}\n </div>\n </div>\n {previewState.loading ? (\n <div data-slot=\"conv-preview-loading\" role=\"status\" className=\"text-muted-foreground flex items-center gap-2 px-1 py-6 text-[13px]\">\n <span className=\"border-muted-foreground/40 border-t-foreground size-4 animate-spin rounded-full border-2\" aria-hidden />\n Loading preview…\n </div>\n ) : previewState.error ? (\n <p role=\"alert\" className=\"text-destructive text-sm\">\n {previewState.error}\n </p>\n ) : (\n <div\n data-slot=\"conv-preview-body\"\n data-confirmation-token={previewState.confirmationToken ?? undefined}\n className=\"max-h-72 overflow-auto\"\n >\n <EmailBody html={previewState.html ?? \"\"} variant=\"preview\" collapseDetails={false} defaultDetailsOpen />\n </div>\n )}\n {sendError ? (\n <p role=\"alert\" className=\"text-destructive text-sm\">\n {sendError}\n </p>\n ) : null}\n <DialogFooter className=\"sm:justify-between\">\n <span className=\"inline-flex\" title={draftDisabledReason ?? undefined}>\n <button\n type=\"button\"\n disabled={sending || previewState.loading || draftDisabled}\n onClick={handleDraft}\n aria-label={draftDisabledReason ? `Open draft in Gmail: ${draftDisabledReason}` : \"Open draft in Gmail\"}\n className=\"text-muted-foreground hover:text-foreground inline-flex items-center gap-1.5 text-[13px] disabled:pointer-events-none disabled:opacity-50\"\n >\n <GmailMark size={14} /> Open draft in Gmail\n </button>\n </span>\n <span className=\"flex items-center gap-2\">\n <Button type=\"button\" variant=\"outline\" size=\"sm\" disabled={sending} onClick={() => { setPreview(false); setPreviewState(IDLE_PREVIEW) }}>\n Keep editing\n </Button>\n <Button\n type=\"button\"\n size=\"sm\"\n disabled={sending || previewState.loading || hasManualFillToken}\n onClick={handleSend}\n >\n <Send size={14} /> {sending ? \"Sending...\" : \"Send now\"}\n </Button>\n </span>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n\n {tenantName ? (\n <p className=\"text-muted-foreground/70 mt-2 text-[11px]\">Sends via Gmail · playbooks stay stopped.</p>\n ) : null}\n </div>\n )\n}\n\n/* ── Thread body (messages + footer/composer/done states) ───────────────── */\n\ntype ThreadMode = \"idle\" | \"replying\" | \"sent\" | \"draft\"\n\nfunction ThreadBody({\n thread,\n me,\n tenantName,\n onSendReply,\n onCreateGmailDraft,\n onPreviewReply,\n onOpenInGmail,\n onApplyReplyTemplate,\n}: {\n thread: ConversationThread\n me?: ConvParticipant\n tenantName?: string\n onSendReply?: (p: ConversationReplyPayload) => void | Promise<void>\n onCreateGmailDraft?: (p: ConversationReplyPayload) => void | Promise<void>\n onPreviewReply?: (p: ConversationReplyPayload) => Promise<ConversationReplyPreview>\n onOpenInGmail?: (threadId: string) => void\n onApplyReplyTemplate?: ConversationPanelProps[\"onApplyReplyTemplate\"]\n}) {\n const canReply = thread.canReply !== false\n const replyDisabledReason = thread.replyDisabledReason?.trim() || \"You are not a participant on this thread, so replying is disabled here.\"\n const draftDisabledReason = onCreateGmailDraft ? null : \"Gmail draft creation is not available for this thread.\"\n const hasCc = !!(thread.cc && thread.cc.length)\n const sortedMessages = React.useMemo(() => sortMessagesChronologically(thread.messages), [thread.messages])\n const [mode, setMode] = React.useState<ThreadMode>(\"idle\")\n const [replyAll, setReplyAll] = React.useState(false)\n const [expanded, setExpanded] = React.useState<Record<string, boolean>>(() => {\n const o: Record<string, boolean> = {}\n sortedMessages.forEach((m, i) => {\n o[m.id] = i === sortedMessages.length - 1\n })\n return o\n })\n\n React.useEffect(() => {\n setExpanded((current) => {\n const next = { ...current }\n sortedMessages.forEach((m, i) => {\n if (next[m.id] === undefined) next[m.id] = i === sortedMessages.length - 1\n })\n return next\n })\n }, [sortedMessages])\n\n const toggle = (id: string) => setExpanded((e) => ({ ...e, [id]: !e[id] }))\n\n return (\n <div data-slot=\"conv-thread-body\" className=\"space-y-2\">\n {canReply && thread.paused ? (\n <div className=\"border-status-warning-border bg-status-warning-bg text-status-warning-fg flex items-start gap-2 rounded-md border border-l-4 p-2.5 text-[12px]\">\n <Pause size={13} className=\"mt-0.5 shrink-0\" />\n <span>\n <b>Playbook stopped.</b> Follow-up actions for {thread.paused.playbook} won’t send\n automatically while this conversation is live. Continue it in {tenantName ?? \"the app\"} or Gmail.\n </span>\n </div>\n ) : null}\n\n <div className=\"space-y-1\">\n {sortedMessages.map((m) => (\n <MessageView key={m.id} message={m} expanded={!!expanded[m.id]} onToggle={() => toggle(m.id)} me={me} />\n ))}\n </div>\n\n {!canReply ? (\n <div className=\"border-border bg-muted/30 text-muted-foreground flex flex-wrap items-start gap-2 rounded-md border p-2.5 text-[12px]\">\n <Eye size={14} className=\"mt-0.5 shrink-0\" />\n <span className=\"min-w-0 flex-1\">\n <b>Viewing only.</b> {replyDisabledReason}\n </span>\n <OpenInGmailButton thread={thread} onOpenInGmail={onOpenInGmail} />\n </div>\n ) : null}\n\n {canReply && mode === \"idle\" ? (\n <div data-slot=\"conv-action-row\" className=\"border-border/70 mt-1 flex flex-wrap items-center gap-x-3 gap-y-2 border-t pt-3\">\n <Button type=\"button\" size=\"sm\" onClick={() => { setReplyAll(false); setMode(\"replying\") }}>\n <Reply size={15} /> Reply\n </Button>\n {hasCc ? (\n <Button type=\"button\" variant=\"outline\" size=\"sm\" onClick={() => { setReplyAll(true); setMode(\"replying\") }}>\n <ReplyAll size={14} /> Reply all\n </Button>\n ) : null}\n <OpenInGmailButton thread={thread} onOpenInGmail={onOpenInGmail} />\n <span className=\"text-muted-foreground/70 ml-auto inline-flex items-center gap-1.5 text-[12px]\">\n <GitMerge size={13} /> Stays on this thread\n </span>\n </div>\n ) : null}\n\n {canReply && mode === \"replying\" ? (\n <ReplyComposer\n thread={thread}\n me={me}\n replyAll={replyAll}\n tenantName={tenantName}\n onPreviewReply={onPreviewReply}\n onApplyTemplate={(templateId) => onApplyReplyTemplate?.({ threadId: thread.threadId, templateId })}\n onClose={() => setMode(\"idle\")}\n onSend={async (body, includeSignature) => {\n await onSendReply?.({ threadId: thread.threadId, body, includeSignature, replyAll })\n setMode(\"sent\")\n }}\n onDraft={async (body, includeSignature) => {\n if (!onCreateGmailDraft) return\n await onCreateGmailDraft({ threadId: thread.threadId, body, includeSignature, replyAll })\n setMode(\"draft\")\n }}\n draftDisabledReason={draftDisabledReason}\n />\n ) : null}\n\n {canReply && mode === \"sent\" ? (\n <div className=\"border-status-active-border bg-status-active-bg flex items-center gap-2 rounded-md border p-3 text-[13px]\">\n <Check size={16} className=\"text-status-active-fg shrink-0\" />\n <span className=\"flex-1\">\n <b>{replyAll ? \"Reply all sent\" : \"Reply sent\"}</b> · added to the thread. Delivered to{\" \"}\n <b>{displayParticipant(thread.contact).name}</b>. This action stays <b>Pending</b>; playbooks remain stopped.\n </span>\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" onClick={() => setMode(\"idle\")}>\n Done\n </Button>\n </div>\n ) : null}\n\n {canReply && mode === \"draft\" ? (\n <div className=\"border-border bg-muted/30 flex items-center gap-2 rounded-md border p-3 text-[13px]\">\n <GmailMark size={16} />\n <span className=\"flex-1\">\n <b>Draft saved to Gmail.</b> Waiting on the <b>Re: {thread.subject}</b> thread; open it there to finish. Nothing was sent.\n </span>\n <OpenInGmailButton thread={thread} onOpenInGmail={onOpenInGmail} />\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" onClick={() => setMode(\"idle\")}>\n Done\n </Button>\n </div>\n ) : null}\n </div>\n )\n}\n\n/* ── A thread row + its inline reader ───────────────────────────────────── */\n\nfunction ThreadRow({\n thread,\n open,\n onToggleOpen,\n me,\n tenantName,\n onSendReply,\n onCreateGmailDraft,\n onPreviewReply,\n onOpenInGmail,\n onApplyReplyTemplate,\n}: {\n thread: ConversationThread\n open: boolean\n onToggleOpen: () => void\n} & Pick<ConversationPanelProps, \"me\" | \"tenantName\" | \"onSendReply\" | \"onCreateGmailDraft\" | \"onPreviewReply\" | \"onOpenInGmail\" | \"onApplyReplyTemplate\">) {\n const status = effectiveStatus(thread)\n const sortedMessages = React.useMemo(() => sortMessagesChronologically(thread.messages), [thread.messages])\n const last = sortedMessages[sortedMessages.length - 1]\n const lastSender = last ? displayParticipant(last.from) : null\n const meDisplay = me ? displayParticipant(me) : null\n const who = last?.direction === \"outbound\" && sameEmail(lastSender?.email, meDisplay?.email) ? \"You\" : firstName(lastSender?.name ?? \"\")\n const lastSnippet = last ? messageBodySnippet(last, 120) : \"\"\n const pill = STATUS_PILL[status]\n\n return (\n <div\n data-slot=\"conv-thread\"\n data-status={status}\n data-open={open ? \"true\" : undefined}\n className={cn(\"border-border border-b last:border-b-0\", THREAD_ROW_ACCENT[status])}\n >\n <button\n type=\"button\"\n onClick={onToggleOpen}\n aria-expanded={open}\n className=\"flex w-full items-center gap-2.5 px-3 py-2.5 text-left hover:bg-muted/30\"\n >\n <span className={cn(\"size-2 shrink-0 rounded-full\", STATUS_DOT[status])} aria-hidden />\n <span className=\"min-w-0 flex-1\">\n <span className=\"flex items-center gap-2\">\n <span className=\"truncate text-sm font-semibold\">{thread.subject}</span>\n <span className={cn(\"shrink-0 rounded-md border px-1.5 py-px text-[10px] font-medium leading-4\", pill.cls)}>\n {pill.label}\n </span>\n </span>\n <span className=\"text-muted-foreground block truncate text-xs\">\n <b className=\"text-foreground/80\">{displayParticipant(thread.contact).name}</b> · {who}: {lastSnippet}\n </span>\n </span>\n <span className=\"text-muted-foreground/60 shrink-0 text-xs\">{thread.lastWhen}</span>\n {open ? (\n <ChevronUp size={15} className=\"text-muted-foreground shrink-0\" />\n ) : (\n <ChevronDown size={15} className=\"text-muted-foreground shrink-0\" />\n )}\n </button>\n\n {open ? (\n <div className=\"px-3 pb-3\">\n <ThreadBody\n thread={thread}\n me={me}\n tenantName={tenantName}\n onSendReply={onSendReply}\n onCreateGmailDraft={onCreateGmailDraft}\n onPreviewReply={onPreviewReply}\n onOpenInGmail={onOpenInGmail}\n onApplyReplyTemplate={onApplyReplyTemplate}\n />\n </div>\n ) : null}\n </div>\n )\n}\n\n/* ── The hub ─────────────────────────────────────────────────────────────── */\n\nfunction ConversationPanel({\n threads,\n me,\n tenantName,\n onSendReply,\n onCreateGmailDraft,\n onPreviewReply,\n onOpenInGmail,\n onApplyReplyTemplate,\n defaultOpenThreadId,\n className,\n}: ConversationPanelProps) {\n const responded = threads.filter((t) => t.status === \"responded\" && t.canReply !== false).length\n const draft = threads.filter((t) => effectiveStatus(t) === \"draft\").length\n const awaiting = threads.filter((t) => effectiveStatus(t) === \"awaiting\").length\n const anyPaused = threads.some((t) => t.paused)\n const prioritizedThread =\n threads.find((t) => t.status === \"responded\" && t.canReply !== false) ??\n threads.find((t) => effectiveStatus(t) === \"draft\") ??\n threads.find((t) => effectiveStatus(t) === \"awaiting\")\n const hubGmailThread =\n threads.find((t) => t.status === \"responded\" && t.canReply !== false && canOpenInGmail(t, onOpenInGmail)) ??\n threads.find((t) => effectiveStatus(t) === \"draft\" && canOpenInGmail(t, onOpenInGmail)) ??\n threads.find((t) => effectiveStatus(t) === \"awaiting\" && canOpenInGmail(t, onOpenInGmail)) ??\n threads.find((t) => canOpenInGmail(t, onOpenInGmail))\n const firstAwaiting = threads.find((t) => effectiveStatus(t) === \"awaiting\")\n\n const [hubOpen, setHubOpen] = React.useState(true)\n const [openId, setOpenId] = React.useState<string | null>(() => {\n if (defaultOpenThreadId) return defaultOpenThreadId\n return prioritizedThread ? prioritizedThread.threadId : null\n })\n\n if (!threads.length) return null\n\n // Header badge state: a responded reply leads, then drafts to finish, then sent mail awaiting a reply.\n const badge =\n responded > 0\n ? { label: \"Email response detected\", dot: \"bg-status-warning-fg\", ring: \"bg-status-warning-fg/30\" }\n : draft > 0\n ? { label: \"Draft ready\", dot: \"bg-status-pending-fg\", ring: \"bg-status-pending-fg/30\" }\n : awaiting > 0\n ? { label: \"Email sent · awaiting reply\", dot: \"bg-status-info-fg\", ring: \"bg-status-info-fg/30\" }\n : { label: \"Conversations\", dot: \"bg-muted-foreground/50\", ring: \"bg-muted-foreground/20\" }\n\n const headTitle =\n responded > 0\n ? `${responded} ${responded === 1 ? \"reply needs\" : \"replies need\"} your response`\n : draft > 0\n ? `Draft ready on ${draft} ${draft === 1 ? \"thread\" : \"threads\"}`\n : awaiting > 0\n ? awaiting === 1 && firstAwaiting\n ? `Awaiting a response from ${firstName(displayParticipant(firstAwaiting.contact).name)}`\n : `Awaiting responses on ${awaiting} threads`\n : `${threads.length} email ${threads.length === 1 ? \"thread\" : \"threads\"}`\n\n const panelState = responded > 0 ? \"responded\" : draft > 0 ? \"draft\" : awaiting > 0 ? \"awaiting\" : \"viewing\"\n\n return (\n <section\n data-slot=\"conversation-panel\"\n data-responded={responded > 0 ? \"true\" : undefined}\n data-state={panelState}\n className={cn(\n \"bg-background overflow-hidden rounded-xl border\",\n panelState === \"responded\"\n ? \"border-status-warning-border\"\n : panelState === \"awaiting\"\n ? \"border-status-info-border\"\n : \"border-border\",\n className,\n )}\n >\n <div\n data-slot=\"conversation-panel-header\"\n className={cn(\n \"flex w-full items-center gap-2 px-3 py-2.5\",\n panelState === \"responded\"\n ? \"bg-status-warning-bg/45\"\n : panelState === \"awaiting\"\n ? \"bg-status-info-bg/55\"\n : \"bg-background\",\n )}\n >\n <button\n type=\"button\"\n onClick={() => setHubOpen((v) => !v)}\n aria-expanded={hubOpen}\n className=\"flex min-w-0 flex-1 items-center gap-3 text-left\"\n >\n <span\n data-slot=\"conversation-badge\"\n className={cn(\n \"inline-flex items-center gap-1.5 rounded-full px-2 py-0.5 text-[11px] font-semibold\",\n responded > 0\n ? \"bg-status-warning-bg text-status-warning-fg\"\n : draft > 0\n ? \"bg-status-pending-bg text-status-pending-fg\"\n : awaiting > 0\n ? \"bg-status-info-bg text-status-info-fg\"\n : \"bg-muted text-muted-foreground\"\n )}\n >\n <span className=\"relative inline-flex size-2\">\n <span className={cn(\"absolute inline-flex h-full w-full animate-ping rounded-full opacity-75\", badge.ring)} />\n <span className={cn(\"relative inline-flex size-2 rounded-full\", badge.dot)} />\n </span>\n {badge.label}\n </span>\n <span className=\"min-w-0 flex-1\">\n <span className=\"block truncate text-sm font-semibold leading-tight\">{headTitle}</span>\n <span className=\"text-muted-foreground mt-0.5 block truncate text-xs\">\n {threads.length} {threads.length === 1 ? \"thread\" : \"threads\"} on this action\n {anyPaused ? <> · <b>playbook stopped</b></> : null}\n </span>\n </span>\n {hubOpen ? (\n <ChevronUp size={16} className=\"text-muted-foreground shrink-0\" />\n ) : (\n <ChevronDown size={16} className=\"text-muted-foreground shrink-0\" />\n )}\n </button>\n {hubGmailThread ? (\n <div className=\"shrink-0\" onClick={(event) => event.stopPropagation()}>\n <OpenInGmailButton thread={hubGmailThread} onOpenInGmail={onOpenInGmail} />\n </div>\n ) : null}\n </div>\n\n {hubOpen ? (\n <div className=\"border-border border-t\">\n {threads.map((t) => (\n <ThreadRow\n key={t.threadId}\n thread={t}\n open={openId === t.threadId}\n onToggleOpen={() => setOpenId((cur) => (cur === t.threadId ? null : t.threadId))}\n me={me}\n tenantName={tenantName}\n onSendReply={onSendReply}\n onCreateGmailDraft={onCreateGmailDraft}\n onPreviewReply={onPreviewReply}\n onOpenInGmail={onOpenInGmail}\n onApplyReplyTemplate={onApplyReplyTemplate}\n />\n ))}\n </div>\n ) : null}\n </section>\n )\n}\n\nexport { ConversationPanel }\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAgUI,SA4WQ,UA5WR,KAcA,YAdA;AAzSJ,YAAY,WAAW;AACvB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEP,SAAS,UAAU;AACnB,SAAS,mBAAmB;AAC5B,SAAS,mBAAmB;AAC5B,SAAS,QAAQ,gBAAgB,mBAAmB;AACpD,SAAS,cAAc;AACvB,SAAS,cAAc;AACvB,SAAS,gBAAgB;AACzB,SAAS,uBAAuB;AAChC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,iBAAiB;AAC1B,SAAS,wBAAwB,kBAAkB,mBAAmB,4BAA4B;AAClG;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AA0JP,SAAS,WAAW,GAAmB;AACrC,SAAO,EAAE,QAAQ,MAAM,OAAO,EAAE,QAAQ,MAAM,MAAM,EAAE,QAAQ,MAAM,MAAM;AAC5E;AAGA,SAAS,WAAW,MAAsB;AACxC,SAAO,uBAAuB,IAAI,EAC/B,MAAM,QAAQ,EACd,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,EACnB,OAAO,OAAO,EACd,IAAI,CAAC,MAAM,MAAM,WAAW,CAAC,EAAE,QAAQ,OAAO,MAAM,CAAC,MAAM,EAC3D,KAAK,EAAE;AACZ;AAEA,SAAS,mBAAmB,QAAyB;AACnD,SAAO,qBAAqB,EAAE,MAAM,OAAO,MAAM,OAAO,OAAO,OAAO,cAAc,OAAO,SAAS,OAAO,KAAK,CAAC;AACnH;AAEA,SAAS,UAAU,MAAsB;AACvC,SAAO,uBAAuB,IAAI,EAAE,MAAM,GAAG,EAAE,CAAC,KAAK,uBAAuB,IAAI;AAClF;AAEA,SAAS,UAAU,GAAmB,GAA4B;AAChE,SAAO,QAAQ,KAAK,KAAK,EAAE,KAAK,EAAE,YAAY,MAAM,EAAE,KAAK,EAAE,YAAY,CAAC;AAC5E;AAEA,SAAS,mBAAmB,SAAiD,YAAY,KAAa;AACpG,SAAO,iBAAiB,EAAE,UAAU,QAAQ,UAAU,MAAM,QAAQ,KAAK,GAAG,SAAS;AACvF;AAKA,SAAS,2BAA2B,OAA6C;AAC/E,MAAI,SAAS,QAAQ,UAAU,GAAI,QAAO;AAE1C,MAAI,iBAAiB,MAAM;AACzB,UAAM,OAAO,MAAM,QAAQ;AAC3B,WAAO,OAAO,MAAM,IAAI,IAAI,OAAO;AAAA,EACrC;AAEA,MAAI,OAAO,UAAU,UAAU;AAC7B,QAAI,CAAC,OAAO,SAAS,KAAK,EAAG,QAAO;AACpC,WAAO,QAAQ,OAAiB,QAAQ,MAAO;AAAA,EACjD;AAEA,QAAM,UAAU,MAAM,KAAK;AAC3B,MAAI,CAAC,QAAS,QAAO;AAErB,MAAI,QAAQ,KAAK,OAAO,GAAG;AACzB,UAAM,UAAU,OAAO,OAAO;AAC9B,QAAI,CAAC,OAAO,SAAS,OAAO,EAAG,QAAO;AACtC,WAAO,UAAU,OAAiB,UAAU,MAAO;AAAA,EACrD;AAEA,QAAM,SAAS,KAAK,MAAM,OAAO;AACjC,SAAO,OAAO,MAAM,MAAM,IAAI,OAAO;AACvC;AAEA,SAAS,iBAAiB,SAAqC;AAC7D,QAAM,cAAc,QAAQ,cAAc,aACtC,CAAC,QAAQ,QAAQ,QAAQ,OAAO,IAChC,CAAC,QAAQ,YAAY,QAAQ,WAAW;AAE5C,QAAM,aAAsC;AAAA,IAC1C,GAAG;AAAA,IACH,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,QAAQ;AAAA,EACV;AAEA,aAAW,aAAa,YAAY;AAClC,UAAM,SAAS,2BAA2B,SAAS;AACnD,QAAI,WAAW,KAAM,QAAO;AAAA,EAC9B;AAEA,SAAO;AACT;AAEA,SAAS,4BAA4B,UAAwC;AAC3E,SAAO,SACJ,IAAI,CAAC,SAAS,WAAW,EAAE,SAAS,OAAO,WAAW,iBAAiB,OAAO,EAAE,EAAE,EAClF,KAAK,CAAC,GAAG,MAAM;AACd,QAAI,EAAE,cAAc,QAAQ,EAAE,cAAc,QAAQ,EAAE,cAAc,EAAE,WAAW;AAC/E,aAAO,EAAE,YAAY,EAAE;AAAA,IACzB;AACA,WAAO,EAAE,QAAQ,EAAE;AAAA,EACrB,CAAC,EACA,IAAI,CAAC,UAAU,MAAM,OAAO;AACjC;AAEA,SAAS,UAAU,EAAE,OAAO,GAAG,GAAsB;AACnD;AAAA;AAAA,IAEE;AAAA,MAAC;AAAA;AAAA,QACC,KAAK,YAAY,MAAM;AAAA,QACvB,KAAI;AAAA,QACJ,OAAO;AAAA,QACP,QAAQ;AAAA,QACR,OAAO,EAAE,OAAO,MAAM,QAAQ,MAAM,WAAW,WAAW,SAAS,QAAQ;AAAA;AAAA,IAC7E;AAAA;AAEJ;AAEA,SAAS,aAAa,EAAE,QAAQ,OAAO,KAAK,GAAyD;AA1UrG;AA2UE,QAAM,UAAU,mBAAmB,MAAM;AAEzC,SACE,qBAAC,UAAO,MACL;AAAA,WAAO,YAAY,oBAAC,eAAY,KAAK,OAAO,WAAW,KAAK,QAAQ,MAAM,IAAK;AAAA,IAChF,oBAAC,kBAAe,WAAU,oEACvB,sBAAY,EAAE,MAAM,QAAQ,MAAM,QAAO,aAAQ,UAAR,YAAiB,OAAO,MAAM,CAAC,GAC3E;AAAA,KACF;AAEJ;AAEA,MAAM,cAAkE;AAAA,EACtE,WAAW,EAAE,OAAO,aAAa,KAAK,2EAA2E;AAAA,EACjH,OAAO,EAAE,OAAO,SAAS,KAAK,iDAAiD;AAAA,EAC/E,UAAU,EAAE,OAAO,QAAQ,KAAK,kEAAkE;AAAA,EAClG,SAAS,EAAE,OAAO,WAAW,KAAK,+CAA+C;AACnF;AAEA,MAAM,aAAyC;AAAA,EAC7C,WAAW;AAAA,EACX,OAAO;AAAA,EACP,UAAU;AAAA,EACV,SAAS;AACX;AAEA,MAAM,oBAAgD;AAAA,EACpD,WAAW;AAAA,EACX,UAAU;AAAA,EACV,OAAO;AAAA,EACP,SAAS;AACX;AAEA,MAAM,eAA4E;AAAA,EAChF,KAAK;AAAA,EACL,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,MAAM;AAAA,EACN,OAAO;AACT;AAEA,SAAS,gBAAgB,GAAmC;AAC1D,SAAO,EAAE,aAAa,QAAQ,YAAY,EAAE;AAC9C;AAEA,SAAS,0BAA0B,QAAoC;AAxXvE;AAyXE,WACE,YAAO,8BAAP,mBAAkC,aAClC,YAAO,wBAAP,mBAA4B,WAC5B;AAEJ;AAEA,SAAS,eAAe,QAA4B,eAAqD;AACvG,SAAO,OAAO,wBAAwB,QAAQ,QAAQ,OAAO,kBAAkB,aAAa;AAC9F;AAEA,SAAS,kBAAkB;AAAA,EACzB;AAAA,EACA;AAAA,EACA,QAAQ;AACV,GAIG;AACD,QAAM,sBAAsB,QAAQ,OAAO,kBAAkB,iBAAiB,OAAO,uBAAuB,OAAO,yBAAyB;AAC5I,MAAI,CAAC,oBAAqB,QAAO;AAEjC,QAAM,WAAW,CAAC,eAAe,QAAQ,aAAa;AACtD,QAAM,iBAAiB,WACnB,0BAA0B,MAAM,IAChC;AAEJ,MAAI,CAAC,YAAY,OAAO,gBAAgB;AACtC,WACE,oBAAC,UAAO,MAAK,UAAS,SAAQ,SAAQ,MAAK,MAAK,SAAO,MACrD,+BAAC,OAAE,MAAM,OAAO,gBAAgB,QAAO,UAAS,KAAI,uBAClD;AAAA,0BAAC,aAAU,MAAM,IAAI;AAAA,MAAE;AAAA,MAAE;AAAA,OAC3B,GACF;AAAA,EAEJ;AAEA,SACE,oBAAC,UAAK,WAAU,eAAc,OAAO,gBACnC;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,SAAQ;AAAA,MACR,MAAK;AAAA,MACL;AAAA,MACA,iBAAe,YAAY;AAAA,MAC3B,cAAY,iBAAiB,GAAG,KAAK,KAAK,cAAc,KAAK;AAAA,MAC7D,SAAS,WAAW,SAAY,MAAM,+CAAgB,OAAO;AAAA,MAE7D;AAAA,4BAAC,aAAU,MAAM,IAAI;AAAA,QAAE;AAAA,QAAE;AAAA;AAAA;AAAA,EAC3B,GACF;AAEJ;AAIA,SAAS,YAAY;AAAA,EACnB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAKG;AA5bH;AA6bE,QAAM,CAAC,WAAW,YAAY,IAAI,MAAM,SAAS,KAAK;AACtD,QAAM,cAAc,mBAAmB,QAAQ,IAAI;AACnD,QAAM,YAAY,mBAAmB,QAAQ,EAAE;AAE/C,MAAI,CAAC,UAAU;AACb,UAAM,UAAU,mBAAmB,SAAS,GAAG;AAE/C,WACE;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,aAAU;AAAA,QACV,SAAS;AAAA,QACT,WAAU;AAAA,QAEV;AAAA,8BAAC,gBAAa,QAAQ,QAAQ,MAAM;AAAA,UACpC,qBAAC,UAAK,WAAU,6DACd;AAAA,gCAAC,OAAE,WAAU,mBAAmB,oBAAU,YAAY,IAAI,GAAE;AAAA,YAAI;AAAA,YAAI;AAAA,aACtE;AAAA,UACA,oBAAC,UAAK,WAAU,6CAA6C,wBAAQ,QAAR,YAAe,QAAQ,MAAK;AAAA,UACzF,oBAAC,eAAY,MAAM,IAAI,WAAU,kCAAiC;AAAA;AAAA;AAAA,IACpE;AAAA,EAEJ;AAEA,QAAM,YAAY,KAAK,mBAAmB,EAAE,IAAI;AAChD,QAAM,UAAU,aAAa,UAAU,UAAU,OAAO,UAAU,KAAK,IAAI,OAAO,UAAU,UAAU,IAAI;AAE1G,SACE,qBAAC,SAAI,aAAU,gBAAe,WAAU,iDACtC;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,SAAS;AAAA,QACT,WAAU;AAAA,QAEV;AAAA,8BAAC,gBAAa,QAAQ,QAAQ,MAAM,MAAK,WAAU;AAAA,UACnD,qBAAC,UAAK,WAAU,kBACd;AAAA,iCAAC,UAAK,WAAU,2CACd;AAAA,kCAAC,UAAK,WAAU,6BAA6B,sBAAY,MAAK;AAAA,cAC7D,YAAY,QACX,qBAAC,UAAK,WAAU,6CAA4C;AAAA;AAAA,gBAAK,YAAY;AAAA,gBAAM;AAAA,iBAAI,IACrF;AAAA,eACN;AAAA,YACA,qBAAC,UAAK,WAAU,uCAAsC;AAAA;AAAA,cACjD,oBAAC,OAAG,mBAAQ;AAAA,eACjB;AAAA,aACF;AAAA,UACA,qBAAC,UAAK,WAAU,oCACb;AAAA,oBAAQ,UACP,qBAAC,UAAK,WAAW,GAAG,qGAAqG,aAAa,QAAQ,QAAQ,IAAI,CAAC,GACxJ;AAAA,sBAAQ,QAAQ,SAAS,QACxB,oBAAC,gBAAa,MAAM,IAAI,IACtB,QAAQ,QAAQ,SAAS,UAAU,QAAQ,QAAQ,SAAS,SAC9D,oBAAC,cAAW,MAAM,IAAI,IACpB,QAAQ,QAAQ,SAAS,UAC3B,oBAAC,eAAY,MAAM,IAAI,IAEvB,oBAAC,YAAS,MAAM,IAAI;AAAA,cAErB,QAAQ,QAAQ;AAAA,eACnB,IACE;AAAA,YACJ,oBAAC,UAAK,WAAU,oCAAoC,kBAAQ,MAAK;AAAA,YACjE,oBAAC,aAAU,MAAM,IAAI,WAAU,yBAAwB;AAAA,aACzD;AAAA;AAAA;AAAA,IACF;AAAA,IAEA,qBAAC,SAAI,WAAU,eACb;AAAA,0BAAC,SAAI,aAAU,qBACb;AAAA,QAAC;AAAA;AAAA,UACC,MAAM,QAAQ;AAAA,UACd,MAAM,QAAQ;AAAA,UACd,cAAa,aAAQ,kBAAR,YAAyB;AAAA,UACtC,SAAQ;AAAA,UACR,iBAAiB;AAAA,UACjB,WAAU;AAAA;AAAA,MACZ,GACF;AAAA,MAEC,QAAQ,SACP,qBAAC,SAAI,WAAU,QACb;AAAA;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,SAAS,MAAM,aAAa,CAAC,MAAM,CAAC,CAAC;AAAA,YACrC,WAAU;AAAA,YACV,OAAO,YAAY,qBAAqB;AAAA,YACzC;AAAA;AAAA,QAED;AAAA,QACC,YACC,qBAAC,SAAI,WAAU,wEACb;AAAA,8BAAC,OAAE,WAAU,QAAQ,iCAAuB,QAAQ,OAAO,IAAI,GAAE;AAAA,UACjE,oBAAC,SAAI,aAAU,oBACb,8BAAC,aAAU,MAAM,QAAQ,OAAO,MAAM,SAAQ,WAAU,iBAAiB,OAAO,GAClF;AAAA,WACF,IACE;AAAA,SACN,IACE;AAAA,OACN;AAAA,KACF;AAEJ;AAeA,MAAM,eAA6B,EAAE,SAAS,OAAO,MAAM,MAAM,mBAAmB,MAAM,OAAO,MAAM,OAAO,MAAM;AAQpH,MAAM,uBAAuB;AAE7B,SAAS,cAAc;AAAA,EACrB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAWG;AAjlBH;AAklBE,QAAM,CAAC,MAAM,OAAO,IAAI,MAAM,UAAS,YAAO,UAAP,YAAgB,EAAE;AACzD,QAAM,CAAC,iBAAiB,kBAAkB,IAAI,MAAM,SAAwB,IAAI;AAChF,QAAM,CAAC,KAAK,MAAM,IAAI,MAAM,SAAS,IAAI;AACzC,QAAM,kBAAiB,YAAO,mBAAP,YAAyB,CAAC;AAIjD,QAAM,qBAAqB,qBAAqB,KAAK,IAAI;AAEzD,QAAM,gBAAgB,CAAC,aAAwC;AAC7D,YAAQ,SAAS,IAAI;AACrB,uBAAmB,SAAS,IAAI;AAChC,uDAAkB,SAAS;AAAA,EAC7B;AACA,QAAM,CAAC,SAAS,UAAU,IAAI,MAAM,SAAS,KAAK;AAClD,QAAM,CAAC,cAAc,eAAe,IAAI,MAAM,SAAuB,YAAY;AACjF,QAAM,CAAC,SAAS,UAAU,IAAI,MAAM,SAAS,KAAK;AAClD,QAAM,CAAC,WAAW,YAAY,IAAI,MAAM,SAAwB,IAAI;AACpE,QAAM,SAAS,YAAW,YAAO,OAAP,YAAa,CAAC,IAAI,CAAC;AAC7C,QAAM,UAAU,QAAQ,KAAK,OAAO,OAAO,IAAI,OAAO,UAAU,OAAO,OAAO,OAAO;AACrF,QAAM,gBAAgB,QAAQ,mBAAmB;AAEjD,QAAM,mBAAmB,WAAW,IAAI,KAAK,OAAO,OAAO,YAAY,WAAW,OAAO,SAAS,IAAI;AAEtG,QAAM,cAAc,YAAY;AA1mBlC,QAAAA,KAAAC;AA2mBI,eAAW,IAAI;AACf,iBAAa,IAAI;AAEjB,QAAI,CAAC,gBAAgB;AAEnB,sBAAgB,EAAE,SAAS,OAAO,MAAM,kBAAkB,mBAAmB,MAAM,OAAO,MAAM,OAAO,KAAK,CAAC;AAC7G;AAAA,IACF;AAEA,oBAAgB,EAAE,SAAS,MAAM,MAAM,MAAM,mBAAmB,MAAM,OAAO,MAAM,OAAO,MAAM,CAAC;AACjG,QAAI;AACF,YAAM,SAAS,MAAM,eAAe,EAAE,UAAU,OAAO,UAAU,MAAM,kBAAkB,KAAK,SAAS,CAAC;AACxG,sBAAgB;AAAA,QACd,SAAS;AAAA,QACT,OAAMD,MAAA,OAAO,aAAP,OAAAA,MAAmB;AAAA,QACzB,oBAAmBC,MAAA,OAAO,sBAAP,OAAAA,MAA4B;AAAA,QAC/C,OAAO;AAAA,QACP,OAAO;AAAA,MACT,CAAC;AAAA,IACH,SAAS,OAAO;AACd,sBAAgB;AAAA,QACd,SAAS;AAAA,QACT,MAAM;AAAA,QACN,mBAAmB;AAAA,QACnB,OAAO,iBAAiB,QAAQ,MAAM,UAAU;AAAA,QAChD,OAAO;AAAA,MACT,CAAC;AAAA,IACH;AAAA,EACF;AAEA,QAAM,aAAa,YAAY;AAC7B,eAAW,IAAI;AACf,iBAAa,IAAI;AACjB,QAAI;AACF,YAAM,OAAO,MAAM,GAAG;AACtB,iBAAW,KAAK;AAChB,sBAAgB,YAAY;AAAA,IAC9B,SAAS,OAAO;AACd,mBAAa,iBAAiB,QAAQ,MAAM,UAAU,8CAA8C;AAAA,IACtG,UAAE;AACA,iBAAW,KAAK;AAAA,IAClB;AAAA,EACF;AAEA,QAAM,cAAc,YAAY;AAC9B,QAAI,cAAe;AACnB,eAAW,IAAI;AACf,iBAAa,IAAI;AACjB,QAAI;AACF,YAAM,QAAQ,MAAM,GAAG;AACvB,iBAAW,KAAK;AAChB,sBAAgB,YAAY;AAAA,IAC9B,SAAS,OAAO;AACd,mBAAa,iBAAiB,QAAQ,MAAM,UAAU,qDAAqD;AAAA,IAC7G,UAAE;AACA,iBAAW,KAAK;AAAA,IAClB;AAAA,EACF;AAEA,SACE,qBAAC,SAAI,aAAU,cAAa,WAAU,mDACpC;AAAA,yBAAC,SAAI,WAAU,gCACZ;AAAA,WAAK,oBAAC,gBAAa,QAAQ,IAAI,IAAK;AAAA,MACrC,oBAAC,UAAK,WAAU,kCACb,qBACC,iCAAE;AAAA;AAAA,QACU;AAAA,QACV,qBAAC,UAAK,WAAU,qCAAoC;AAAA;AAAA,UAAG,IAAI,OAAO;AAAA,UAAO;AAAA,WAAW;AAAA,SACtF,IAEA,iCAAE;AAAA;AAAA,QACS,oBAAC,OAAG,oBAAU,mBAAmB,OAAO,OAAO,EAAE,IAAI,GAAE;AAAA,SAClE,GAEJ;AAAA,MACA,qBAAC,UAAK,WAAU,oEACd;AAAA,4BAAC,YAAS,MAAM,IAAI;AAAA,QAAE;AAAA,SACxB;AAAA,MACA,oBAAC,YAAO,MAAK,UAAS,SAAS,SAAS,OAAM,iBAAgB,WAAU,+CACtE,8BAAC,KAAE,MAAM,IAAI,GACf;AAAA,OACF;AAAA,IAEA,qBAAC,SAAI,WAAU,0DACb;AAAA,2BAAC,SAAI,WAAU,6BACb;AAAA,4BAAC,UAAK,WAAU,+DAA8D,gBAAE;AAAA,QAChF,oBAAC,UAAK,WAAU,eAAe,6BAAmB,OAAO,OAAO,EAAE,MAAK;AAAA,QACvE,oBAAC,UAAK,WAAU,6CAA6C,mCAAmB,OAAO,OAAO,EAAE,UAAnC,YAA4C,OAAO,QAAQ,OAAM;AAAA,SAChI;AAAA,MACC,YAAY,OAAO,SAClB,qBAAC,SAAI,WAAU,4BACb;AAAA,4BAAC,UAAK,WAAU,+DAA8D,gBAAE;AAAA,QAChF,oBAAC,UAAK,WAAU,iCACb,4BAAkB,OAAO,IAAI,CAAC,MAAG;AAxsBhD,cAAAD;AAwsBmD,oBAAG,mBAAmB,CAAC,EAAE,IAAI,MAAKA,MAAA,mBAAmB,CAAC,EAAE,UAAtB,OAAAA,MAA+B,EAAE,KAAK;AAAA,SAAG,CAAC,GACnH;AAAA,SACF,IACE;AAAA,MACJ,qBAAC,SAAI,WAAU,6BACb;AAAA,4BAAC,UAAK,WAAU,+DAA8D,qBAAO;AAAA,QACrF,oBAAC,UAAK,WAAU,YAAY,mBAAQ;AAAA,QACpC,qBAAC,UAAK,WAAU,+EACd;AAAA,8BAAC,QAAK,MAAM,IAAI;AAAA,UAAE;AAAA,WACpB;AAAA,SACF;AAAA,OACF;AAAA,IAEA;AAAA,MAAC;AAAA;AAAA,QACC,OAAO;AAAA,QACP,UAAU,CAAC,MAAM,QAAQ,EAAE,OAAO,KAAK;AAAA,QACvC,aAAY;AAAA,QACZ,WAAU;AAAA,QACV,WAAW,CAAC,MAAM;AAChB,eAAK,EAAE,WAAW,EAAE,YAAY,EAAE,QAAQ,SAAS;AACjD,cAAE,eAAe;AACjB,gBAAI,CAAC,mBAAoB,MAAK,YAAY;AAAA,UAC5C;AAAA,QACF;AAAA;AAAA,IACF;AAAA,IAEC,OAAO,OAAO,YACb,qBAAC,SAAI,WAAU,4FACb;AAAA,0BAAC,UAAK,WAAU,iCAAgC,gBAAE;AAAA,MACjD,OAAO;AAAA,OACV,IACE;AAAA,IAEH,eAAe,SAAS,IACvB,qBAAC,SAAI,WAAU,0CACb;AAAA,2BAAC,gBACC;AAAA,4BAAC,uBAAoB,SAAO,MAC1B,+BAAC,UAAO,MAAK,UAAS,SAAQ,WAAU,MAAK,MAAK,UAAU,SAAS,aAAU,+BAC7E;AAAA,8BAAC,YAAS,MAAM,IAAI;AAAA,UAAE;AAAA,WACxB,GACF;AAAA,QACA,qBAAC,uBAAoB,OAAM,SAAQ,WAAU,YAC3C;AAAA,8BAAC,qBAAkB,uBAAS;AAAA,UAC5B,oBAAC,yBAAsB;AAAA,UACtB,eAAe,IAAI,CAAC,aACnB;AAAA,YAAC;AAAA;AAAA,cAEC,UAAU,MAAM,cAAc,QAAQ;AAAA,cACtC,WAAU;AAAA,cAEV;AAAA,oCAAC,UAAK,WAAU,2BAA2B,mBAAS,MAAK;AAAA,gBACxD,SAAS,QAAQ,SAAS,KAAK,SAC9B,oBAAC,UAAK,WAAU,wBACb,mBAAS,KAAK,IAAI,CAAC,QAClB;AAAA,kBAAC;AAAA;AAAA,oBAEC,WAAU;AAAA,oBAET;AAAA;AAAA,kBAHI;AAAA,gBAIP,CACD,GACH,IACE;AAAA;AAAA;AAAA,YAhBC,SAAS;AAAA,UAiBhB,CACD;AAAA,WACH;AAAA,SACF;AAAA,MACC,kBACC,qBAAC,UAAK,WAAU,oEACd;AAAA,4BAAC,SAAM,MAAM,IAAI;AAAA,QAAE;AAAA,QAAW;AAAA,QAAgB;AAAA,SAChD,IACE;AAAA,OACN,IACE;AAAA,IAEH,qBACC,qBAAC,OAAE,aAAU,0BAAyB,MAAK,SAAQ,WAAU,qCAAoC;AAAA;AAAA,MAC7B;AAAA,MAAkB;AAAA,OACtF,IACE;AAAA,IAEJ,qBAAC,SAAI,WAAU,0CACb;AAAA,0BAAC,mBAAgB;AAAA,MACjB,qBAAC,WAAM,WAAU,6FACf;AAAA,4BAAC,UAAO,SAAS,KAAK,iBAAiB,QAAQ,cAAW,oBAAmB;AAAA,QAAE;AAAA,SAEjF;AAAA,MACA;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,SAAQ;AAAA,UACR,MAAK;AAAA,UACL,UAAU,WAAW;AAAA,UACrB,SAAS,MAAM,KAAK,YAAY;AAAA,UAEhC;AAAA,gCAAC,OAAI,MAAM,IAAI;AAAA,YAAE;AAAA;AAAA;AAAA,MACnB;AAAA,MACA;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,MAAK;AAAA,UACL,UAAU,WAAW;AAAA,UACrB,SAAS,MAAM,KAAK,YAAY;AAAA,UAEhC;AAAA,gCAAC,QAAK,MAAM,IAAI;AAAA,YAAE;AAAA;AAAA;AAAA,MACpB;AAAA,OACF;AAAA,IAEA,oBAAC,UAAO,MAAM,SAAS,cAAc,CAAC,SAAS;AAAE,UAAI,CAAC,SAAS;AAAE,mBAAW,IAAI;AAAG,YAAI,CAAC,KAAM,iBAAgB,YAAY;AAAA,MAAE;AAAA,IAAE,GAC5H,+BAAC,iBAAc,WAAU,YACvB;AAAA,2BAAC,gBACC;AAAA,6BAAC,eAAY,WAAU,yCACrB;AAAA,8BAAC,OAAI,MAAM,IAAI;AAAA,UAAE;AAAA,UAAE,aAAa,QAAQ,wBAAwB;AAAA,WAClE;AAAA,QACA,oBAAC,qBACE,uBAAa,QACV,6EACA,iCAAE;AAAA;AAAA,UAAc,QAAQ,QAAQ,YAAY,EAAE;AAAA,UAAE;AAAA,WAAiC,GACvF;AAAA,SACF;AAAA,MACA,qBAAC,SAAI,WAAU,6DACb;AAAA,6BAAC,SACC;AAAA,8BAAC,UAAK,WAAU,yBAAwB,iBAAG;AAAA,UAC3C,oBAAC,OAAG,6BAAmB,OAAO,OAAO,EAAE,MAAK;AAAA,UAAK;AAAA,UACjD,qBAAC,UAAK,WAAU,4BAA2B;AAAA;AAAA,aAAK,wBAAmB,OAAO,OAAO,EAAE,UAAnC,YAA4C,OAAO,QAAQ;AAAA,YAAM;AAAA,aAAI;AAAA,WACvH;AAAA,QACC,YAAY,OAAO,SAClB,qBAAC,SAAI,WAAU,yBAAwB;AAAA;AAAA,UAAI,kBAAkB,OAAO,IAAI,CAAC,MAAG;AAr0B1F,gBAAAA;AAq0B6F,sBAAG,mBAAmB,CAAC,EAAE,IAAI,MAAKA,MAAA,mBAAmB,CAAC,EAAE,UAAtB,OAAAA,MAA+B,EAAE,KAAK;AAAA,WAAG,CAAC;AAAA,WAAE,IAC3J;AAAA,QACJ,qBAAC,SACC;AAAA,8BAAC,UAAK,WAAU,yBAAwB,sBAAQ;AAAA,UAC/C;AAAA,WACH;AAAA,SACF;AAAA,MACC,aAAa,UACZ,qBAAC,SAAI,aAAU,wBAAuB,MAAK,UAAS,WAAU,uEAC5D;AAAA,4BAAC,UAAK,WAAU,4FAA2F,eAAW,MAAC;AAAA,QAAE;AAAA,SAE3H,IACE,aAAa,QACf,oBAAC,OAAE,MAAK,SAAQ,WAAU,4BACvB,uBAAa,OAChB,IAEA;AAAA,QAAC;AAAA;AAAA,UACC,aAAU;AAAA,UACV,4BAAyB,kBAAa,sBAAb,YAAkC;AAAA,UAC3D,WAAU;AAAA,UAEV,8BAAC,aAAU,OAAM,kBAAa,SAAb,YAAqB,IAAI,SAAQ,WAAU,iBAAiB,OAAO,oBAAkB,MAAC;AAAA;AAAA,MACzG;AAAA,MAED,YACC,oBAAC,OAAE,MAAK,SAAQ,WAAU,4BACvB,qBACH,IACE;AAAA,MACJ,qBAAC,gBAAa,WAAU,sBACtB;AAAA,4BAAC,UAAK,WAAU,eAAc,OAAO,oDAAuB,QAC1D;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,UAAU,WAAW,aAAa,WAAW;AAAA,YAC7C,SAAS;AAAA,YACT,cAAY,sBAAsB,wBAAwB,mBAAmB,KAAK;AAAA,YAClF,WAAU;AAAA,YAEV;AAAA,kCAAC,aAAU,MAAM,IAAI;AAAA,cAAE;AAAA;AAAA;AAAA,QACzB,GACF;AAAA,QACA,qBAAC,UAAK,WAAU,2BACd;AAAA,8BAAC,UAAO,MAAK,UAAS,SAAQ,WAAU,MAAK,MAAK,UAAU,SAAS,SAAS,MAAM;AAAE,uBAAW,KAAK;AAAG,4BAAgB,YAAY;AAAA,UAAE,GAAG,0BAE1I;AAAA,UACA;AAAA,YAAC;AAAA;AAAA,cACC,MAAK;AAAA,cACL,MAAK;AAAA,cACL,UAAU,WAAW,aAAa,WAAW;AAAA,cAC7C,SAAS;AAAA,cAET;AAAA,oCAAC,QAAK,MAAM,IAAI;AAAA,gBAAE;AAAA,gBAAE,UAAU,eAAe;AAAA;AAAA;AAAA,UAC/C;AAAA,WACF;AAAA,SACF;AAAA,OACF,GACF;AAAA,IAEC,aACC,oBAAC,OAAE,WAAU,6CAA4C,0DAAyC,IAChG;AAAA,KACN;AAEJ;AAMA,SAAS,WAAW;AAAA,EAClB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GASG;AA75BH;AA85BE,QAAM,WAAW,OAAO,aAAa;AACrC,QAAM,wBAAsB,YAAO,wBAAP,mBAA4B,WAAU;AAClE,QAAM,sBAAsB,qBAAqB,OAAO;AACxD,QAAM,QAAQ,CAAC,EAAE,OAAO,MAAM,OAAO,GAAG;AACxC,QAAM,iBAAiB,MAAM,QAAQ,MAAM,4BAA4B,OAAO,QAAQ,GAAG,CAAC,OAAO,QAAQ,CAAC;AAC1G,QAAM,CAAC,MAAM,OAAO,IAAI,MAAM,SAAqB,MAAM;AACzD,QAAM,CAAC,UAAU,WAAW,IAAI,MAAM,SAAS,KAAK;AACpD,QAAM,CAAC,UAAU,WAAW,IAAI,MAAM,SAAkC,MAAM;AAC5E,UAAM,IAA6B,CAAC;AACpC,mBAAe,QAAQ,CAAC,GAAG,MAAM;AAC/B,QAAE,EAAE,EAAE,IAAI,MAAM,eAAe,SAAS;AAAA,IAC1C,CAAC;AACD,WAAO;AAAA,EACT,CAAC;AAED,QAAM,UAAU,MAAM;AACpB,gBAAY,CAAC,YAAY;AACvB,YAAM,OAAO,mBAAK;AAClB,qBAAe,QAAQ,CAAC,GAAG,MAAM;AAC/B,YAAI,KAAK,EAAE,EAAE,MAAM,OAAW,MAAK,EAAE,EAAE,IAAI,MAAM,eAAe,SAAS;AAAA,MAC3E,CAAC;AACD,aAAO;AAAA,IACT,CAAC;AAAA,EACH,GAAG,CAAC,cAAc,CAAC;AAEnB,QAAM,SAAS,CAAC,OAAe,YAAY,CAAC,MAAO,iCAAK,IAAL,EAAQ,CAAC,EAAE,GAAG,CAAC,EAAE,EAAE,EAAE,EAAE;AAE1E,SACE,qBAAC,SAAI,aAAU,oBAAmB,WAAU,aACzC;AAAA,gBAAY,OAAO,SAClB,qBAAC,SAAI,WAAU,kJACb;AAAA,0BAAC,SAAM,MAAM,IAAI,WAAU,mBAAkB;AAAA,MAC7C,qBAAC,UACC;AAAA,4BAAC,OAAE,+BAAiB;AAAA,QAAI;AAAA,QAAwB,OAAO,OAAO;AAAA,QAAS;AAAA,QACR,kCAAc;AAAA,QAAU;AAAA,SACzF;AAAA,OACF,IACE;AAAA,IAEJ,oBAAC,SAAI,WAAU,aACZ,yBAAe,IAAI,CAAC,MACnB,oBAAC,eAAuB,SAAS,GAAG,UAAU,CAAC,CAAC,SAAS,EAAE,EAAE,GAAG,UAAU,MAAM,OAAO,EAAE,EAAE,GAAG,MAA5E,EAAE,EAAkF,CACvG,GACH;AAAA,IAEC,CAAC,WACA,qBAAC,SAAI,WAAU,wHACb;AAAA,0BAAC,OAAI,MAAM,IAAI,WAAU,mBAAkB;AAAA,MAC3C,qBAAC,UAAK,WAAU,kBACd;AAAA,4BAAC,OAAE,2BAAa;AAAA,QAAI;AAAA,QAAE;AAAA,SACxB;AAAA,MACA,oBAAC,qBAAkB,QAAgB,eAA8B;AAAA,OACnE,IACE;AAAA,IAEH,YAAY,SAAS,SACpB,qBAAC,SAAI,aAAU,mBAAkB,WAAU,mFACzC;AAAA,2BAAC,UAAO,MAAK,UAAS,MAAK,MAAK,SAAS,MAAM;AAAE,oBAAY,KAAK;AAAG,gBAAQ,UAAU;AAAA,MAAE,GACvF;AAAA,4BAAC,SAAM,MAAM,IAAI;AAAA,QAAE;AAAA,SACrB;AAAA,MACC,QACC,qBAAC,UAAO,MAAK,UAAS,SAAQ,WAAU,MAAK,MAAK,SAAS,MAAM;AAAE,oBAAY,IAAI;AAAG,gBAAQ,UAAU;AAAA,MAAE,GACxG;AAAA,4BAAC,YAAS,MAAM,IAAI;AAAA,QAAE;AAAA,SACxB,IACE;AAAA,MACJ,oBAAC,qBAAkB,QAAgB,eAA8B;AAAA,MACjE,qBAAC,UAAK,WAAU,iFACd;AAAA,4BAAC,YAAS,MAAM,IAAI;AAAA,QAAE;AAAA,SACxB;AAAA,OACF,IACE;AAAA,IAEH,YAAY,SAAS,aACpB;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA,iBAAiB,CAAC,eAAe,6DAAuB,EAAE,UAAU,OAAO,UAAU,WAAW;AAAA,QAChG,SAAS,MAAM,QAAQ,MAAM;AAAA,QAC7B,QAAQ,OAAO,MAAM,qBAAqB;AACxC,iBAAM,2CAAc,EAAE,UAAU,OAAO,UAAU,MAAM,kBAAkB,SAAS;AAClF,kBAAQ,MAAM;AAAA,QAChB;AAAA,QACA,SAAS,OAAO,MAAM,qBAAqB;AACzC,cAAI,CAAC,mBAAoB;AACzB,gBAAM,mBAAmB,EAAE,UAAU,OAAO,UAAU,MAAM,kBAAkB,SAAS,CAAC;AACxF,kBAAQ,OAAO;AAAA,QACjB;AAAA,QACA;AAAA;AAAA,IACF,IACE;AAAA,IAEH,YAAY,SAAS,SACpB,qBAAC,SAAI,WAAU,6GACb;AAAA,0BAAC,SAAM,MAAM,IAAI,WAAU,kCAAiC;AAAA,MAC5D,qBAAC,UAAK,WAAU,UACd;AAAA,4BAAC,OAAG,qBAAW,mBAAmB,cAAa;AAAA,QAAI;AAAA,QAAqC;AAAA,QACxF,oBAAC,OAAG,6BAAmB,OAAO,OAAO,EAAE,MAAK;AAAA,QAAI;AAAA,QAAoB,oBAAC,OAAE,qBAAO;AAAA,QAAI;AAAA,SACpF;AAAA,MACA,oBAAC,UAAO,MAAK,UAAS,SAAQ,SAAQ,MAAK,MAAK,SAAS,MAAM,QAAQ,MAAM,GAAG,kBAEhF;AAAA,OACF,IACE;AAAA,IAEH,YAAY,SAAS,UACpB,qBAAC,SAAI,WAAU,uFACb;AAAA,0BAAC,aAAU,MAAM,IAAI;AAAA,MACrB,qBAAC,UAAK,WAAU,UACd;AAAA,4BAAC,OAAE,mCAAqB;AAAA,QAAI;AAAA,QAAgB,qBAAC,OAAE;AAAA;AAAA,UAAK,OAAO;AAAA,WAAQ;AAAA,QAAI;AAAA,SACzE;AAAA,MACA,oBAAC,qBAAkB,QAAgB,eAA8B;AAAA,MACjE,oBAAC,UAAO,MAAK,UAAS,SAAQ,SAAQ,MAAK,MAAK,SAAS,MAAM,QAAQ,MAAM,GAAG,kBAEhF;AAAA,OACF,IACE;AAAA,KACN;AAEJ;AAIA,SAAS,UAAU;AAAA,EACjB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAI4J;AA1iC5J;AA2iCE,QAAM,SAAS,gBAAgB,MAAM;AACrC,QAAM,iBAAiB,MAAM,QAAQ,MAAM,4BAA4B,OAAO,QAAQ,GAAG,CAAC,OAAO,QAAQ,CAAC;AAC1G,QAAM,OAAO,eAAe,eAAe,SAAS,CAAC;AACrD,QAAM,aAAa,OAAO,mBAAmB,KAAK,IAAI,IAAI;AAC1D,QAAM,YAAY,KAAK,mBAAmB,EAAE,IAAI;AAChD,QAAM,OAAM,6BAAM,eAAc,cAAc,UAAU,yCAAY,OAAO,uCAAW,KAAK,IAAI,QAAQ,WAAU,8CAAY,SAAZ,YAAoB,EAAE;AACvI,QAAM,cAAc,OAAO,mBAAmB,MAAM,GAAG,IAAI;AAC3D,QAAM,OAAO,YAAY,MAAM;AAE/B,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,eAAa;AAAA,MACb,aAAW,OAAO,SAAS;AAAA,MAC3B,WAAW,GAAG,0CAA0C,kBAAkB,MAAM,CAAC;AAAA,MAEjF;AAAA;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,SAAS;AAAA,YACT,iBAAe;AAAA,YACf,WAAU;AAAA,YAEV;AAAA,kCAAC,UAAK,WAAW,GAAG,gCAAgC,WAAW,MAAM,CAAC,GAAG,eAAW,MAAC;AAAA,cACrF,qBAAC,UAAK,WAAU,kBACd;AAAA,qCAAC,UAAK,WAAU,2BACd;AAAA,sCAAC,UAAK,WAAU,kCAAkC,iBAAO,SAAQ;AAAA,kBACjE,oBAAC,UAAK,WAAW,GAAG,6EAA6E,KAAK,GAAG,GACtG,eAAK,OACR;AAAA,mBACF;AAAA,gBACA,qBAAC,UAAK,WAAU,gDACd;AAAA,sCAAC,OAAE,WAAU,sBAAsB,6BAAmB,OAAO,OAAO,EAAE,MAAK;AAAA,kBAAI;AAAA,kBAAI;AAAA,kBAAI;AAAA,kBAAG;AAAA,mBAC5F;AAAA,iBACF;AAAA,cACA,oBAAC,UAAK,WAAU,6CAA6C,iBAAO,UAAS;AAAA,cAC5E,OACC,oBAAC,aAAU,MAAM,IAAI,WAAU,kCAAiC,IAEhE,oBAAC,eAAY,MAAM,IAAI,WAAU,kCAAiC;AAAA;AAAA;AAAA,QAEtE;AAAA,QAEC,OACC,oBAAC,SAAI,WAAU,aACb;AAAA,UAAC;AAAA;AAAA,YACC;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA;AAAA,QACF,GACF,IACE;AAAA;AAAA;AAAA,EACN;AAEJ;AAIA,SAAS,kBAAkB;AAAA,EACzB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAA2B;AApnC3B;AAqnCE,QAAM,YAAY,QAAQ,OAAO,CAAC,MAAM,EAAE,WAAW,eAAe,EAAE,aAAa,KAAK,EAAE;AAC1F,QAAM,QAAQ,QAAQ,OAAO,CAAC,MAAM,gBAAgB,CAAC,MAAM,OAAO,EAAE;AACpE,QAAM,WAAW,QAAQ,OAAO,CAAC,MAAM,gBAAgB,CAAC,MAAM,UAAU,EAAE;AAC1E,QAAM,YAAY,QAAQ,KAAK,CAAC,MAAM,EAAE,MAAM;AAC9C,QAAM,qBACJ,mBAAQ,KAAK,CAAC,MAAM,EAAE,WAAW,eAAe,EAAE,aAAa,KAAK,MAApE,YACA,QAAQ,KAAK,CAAC,MAAM,gBAAgB,CAAC,MAAM,OAAO,MADlD,YAEA,QAAQ,KAAK,CAAC,MAAM,gBAAgB,CAAC,MAAM,UAAU;AACvD,QAAM,kBACJ,yBAAQ,KAAK,CAAC,MAAM,EAAE,WAAW,eAAe,EAAE,aAAa,SAAS,eAAe,GAAG,aAAa,CAAC,MAAxG,YACA,QAAQ,KAAK,CAAC,MAAM,gBAAgB,CAAC,MAAM,WAAW,eAAe,GAAG,aAAa,CAAC,MADtF,YAEA,QAAQ,KAAK,CAAC,MAAM,gBAAgB,CAAC,MAAM,cAAc,eAAe,GAAG,aAAa,CAAC,MAFzF,YAGA,QAAQ,KAAK,CAAC,MAAM,eAAe,GAAG,aAAa,CAAC;AACtD,QAAM,gBAAgB,QAAQ,KAAK,CAAC,MAAM,gBAAgB,CAAC,MAAM,UAAU;AAE3E,QAAM,CAAC,SAAS,UAAU,IAAI,MAAM,SAAS,IAAI;AACjD,QAAM,CAAC,QAAQ,SAAS,IAAI,MAAM,SAAwB,MAAM;AAC9D,QAAI,oBAAqB,QAAO;AAChC,WAAO,oBAAoB,kBAAkB,WAAW;AAAA,EAC1D,CAAC;AAED,MAAI,CAAC,QAAQ,OAAQ,QAAO;AAG5B,QAAM,QACJ,YAAY,IACR,EAAE,OAAO,2BAA2B,KAAK,wBAAwB,MAAM,0BAA0B,IACjG,QAAQ,IACN,EAAE,OAAO,eAAe,KAAK,wBAAwB,MAAM,0BAA0B,IACrF,WAAW,IACT,EAAE,OAAO,kCAA+B,KAAK,qBAAqB,MAAM,uBAAuB,IAC/F,EAAE,OAAO,iBAAiB,KAAK,0BAA0B,MAAM,yBAAyB;AAElG,QAAM,YACJ,YAAY,IACR,GAAG,SAAS,IAAI,cAAc,IAAI,gBAAgB,cAAc,mBAChE,QAAQ,IACN,kBAAkB,KAAK,IAAI,UAAU,IAAI,WAAW,SAAS,KAC7D,WAAW,IACT,aAAa,KAAK,gBAChB,4BAA4B,UAAU,mBAAmB,cAAc,OAAO,EAAE,IAAI,CAAC,KACrF,yBAAyB,QAAQ,aACnC,GAAG,QAAQ,MAAM,UAAU,QAAQ,WAAW,IAAI,WAAW,SAAS;AAEhF,QAAM,aAAa,YAAY,IAAI,cAAc,QAAQ,IAAI,UAAU,WAAW,IAAI,aAAa;AAEnG,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,kBAAgB,YAAY,IAAI,SAAS;AAAA,MACzC,cAAY;AAAA,MACZ,WAAW;AAAA,QACT;AAAA,QACA,eAAe,cACX,iCACA,eAAe,aACb,8BACA;AAAA,QACN;AAAA,MACF;AAAA,MAEA;AAAA;AAAA,UAAC;AAAA;AAAA,YACC,aAAU;AAAA,YACV,WAAW;AAAA,cACT;AAAA,cACA,eAAe,cACX,4BACA,eAAe,aACb,yBACA;AAAA,YACR;AAAA,YAEA;AAAA;AAAA,gBAAC;AAAA;AAAA,kBACC,MAAK;AAAA,kBACL,SAAS,MAAM,WAAW,CAAC,MAAM,CAAC,CAAC;AAAA,kBACnC,iBAAe;AAAA,kBACf,WAAU;AAAA,kBAEZ;AAAA;AAAA,sBAAC;AAAA;AAAA,wBACC,aAAU;AAAA,wBACV,WAAW;AAAA,0BACT;AAAA,0BACA,YAAY,IACR,gDACA,QAAQ,IACN,gDACA,WAAW,IACT,0CACA;AAAA,wBACV;AAAA,wBAEA;AAAA,+CAAC,UAAK,WAAU,+BACd;AAAA,gDAAC,UAAK,WAAW,GAAG,2EAA2E,MAAM,IAAI,GAAG;AAAA,4BAC5G,oBAAC,UAAK,WAAW,GAAG,4CAA4C,MAAM,GAAG,GAAG;AAAA,6BAC9E;AAAA,0BACC,MAAM;AAAA;AAAA;AAAA,oBACT;AAAA,oBACA,qBAAC,UAAK,WAAU,kBACd;AAAA,0CAAC,UAAK,WAAU,sDAAsD,qBAAU;AAAA,sBAChF,qBAAC,UAAK,WAAU,uDACb;AAAA,gCAAQ;AAAA,wBAAO;AAAA,wBAAE,QAAQ,WAAW,IAAI,WAAW;AAAA,wBAAU;AAAA,wBAC7D,YAAY,iCAAE;AAAA;AAAA,0BAAG,oBAAC,OAAE,8BAAgB;AAAA,2BAAI,IAAM;AAAA,yBACjD;AAAA,uBACF;AAAA,oBACG,UACC,oBAAC,aAAU,MAAM,IAAI,WAAU,kCAAiC,IAEhE,oBAAC,eAAY,MAAM,IAAI,WAAU,kCAAiC;AAAA;AAAA;AAAA,cAEtE;AAAA,cACC,iBACC,oBAAC,SAAI,WAAU,YAAW,SAAS,CAAC,UAAU,MAAM,gBAAgB,GAClE,8BAAC,qBAAkB,QAAQ,gBAAgB,eAA8B,GAC3E,IACE;AAAA;AAAA;AAAA,QACN;AAAA,QAEC,UACC,oBAAC,SAAI,WAAU,0BACZ,kBAAQ,IAAI,CAAC,MACZ;AAAA,UAAC;AAAA;AAAA,YAEC,QAAQ;AAAA,YACR,MAAM,WAAW,EAAE;AAAA,YACnB,cAAc,MAAM,UAAU,CAAC,QAAS,QAAQ,EAAE,WAAW,OAAO,EAAE,QAAS;AAAA,YAC/E;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA;AAAA,UAVK,EAAE;AAAA,QAWT,CACD,GACH,IACE;AAAA;AAAA;AAAA,EACN;AAEJ;","names":["_a","_b"]}
|
package/dist/index.d.ts
CHANGED
|
@@ -26,7 +26,7 @@ export { ComplianceBadge, ComplianceBadgeProps, ComplianceStatus } from './compo
|
|
|
26
26
|
export { ContactChip, ContactChipProps } from './components/contact-chip.js';
|
|
27
27
|
export { ContactChannel, ContactItem, ContactList, ContactListProps } from './components/contact-list.js';
|
|
28
28
|
export { ContextualQuickActionContextLabel, ContextualQuickActionContextLabelProps, ContextualQuickActionItem, ContextualQuickActionLauncher, ContextualQuickActionLauncherProps, ContextualQuickActionLauncherVariant } from './components/contextual-quick-action-launcher.js';
|
|
29
|
-
export { ConvMessage, ConvParticipant, ConvStatus, ConversationPanel, ConversationPanelProps, ConversationReplyPayload, ConversationReplyPreview, ConversationThread } from './components/conversation-panel.js';
|
|
29
|
+
export { ConvMessage, ConvParticipant, ConvStatus, ConversationPanel, ConversationPanelProps, ConversationReplyPayload, ConversationReplyPreview, ConversationReplyTemplate, ConversationThread } from './components/conversation-panel.js';
|
|
30
30
|
export { CheckInsCard, RecentlyCompletedCard, TopTasksCard, UpcomingMeetingsCard } from './components/dashboard-cards.js';
|
|
31
31
|
export { DataRow, DataTable, DataTableProps } from './components/data-table.js';
|
|
32
32
|
export { ConditionFieldDef, ConditionFieldOption, ConditionFilterValue, ConditionOperator, ConditionOptionObject, DEFAULT_OPERATORS, DataTableConditionFilter, DataTableConditionFilterProps, OPERATOR_LABELS, generateConditionId, getOperators, shouldShowOptionSearch } from './components/data-table-condition-filter.js';
|
package/package.json
CHANGED
|
@@ -57,6 +57,129 @@ describe("ConversationPanel", () => {
|
|
|
57
57
|
expect(container.querySelector('[data-slot="conversation-panel"]')).toBeNull();
|
|
58
58
|
});
|
|
59
59
|
|
|
60
|
+
describe("reply template picker (WIT-970)", () => {
|
|
61
|
+
function openReply(container: HTMLElement) {
|
|
62
|
+
// The responded thread auto-opens; click its "Reply" action to open the composer.
|
|
63
|
+
fireEvent.click(screen.getByRole("button", { name: /^Reply$/i }));
|
|
64
|
+
return container.querySelector<HTMLTextAreaElement>('[data-slot="conv-reply"] textarea')!;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
it("does not render the picker when a thread has no reply templates", () => {
|
|
68
|
+
const { container } = render(<ConversationPanel threads={[thread()]} me={me} />);
|
|
69
|
+
openReply(container);
|
|
70
|
+
expect(container.querySelector('[data-slot="conv-reply-template-trigger"]')).toBeNull();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("applies a template into the composer body and fires onApplyReplyTemplate", async () => {
|
|
74
|
+
const onApplyReplyTemplate = vi.fn();
|
|
75
|
+
const { container } = render(
|
|
76
|
+
<ConversationPanel
|
|
77
|
+
me={me}
|
|
78
|
+
onApplyReplyTemplate={onApplyReplyTemplate}
|
|
79
|
+
threads={[
|
|
80
|
+
thread({
|
|
81
|
+
replyTemplates: [
|
|
82
|
+
{
|
|
83
|
+
id: "tmpl-pricing",
|
|
84
|
+
name: "Pricing follow-up",
|
|
85
|
+
body: "Hi Priya, here is the pricing breakdown you asked for.",
|
|
86
|
+
tags: ["Reply", "Pricing"],
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
}),
|
|
90
|
+
]}
|
|
91
|
+
/>,
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
const textarea = openReply(container);
|
|
95
|
+
expect(textarea.value).toBe("");
|
|
96
|
+
|
|
97
|
+
// Open the Radix dropdown (pointerDown, mirroring rich-text-toolbar.test).
|
|
98
|
+
fireEvent.pointerDown(
|
|
99
|
+
container.querySelector('[data-slot="conv-reply-template-trigger"]')!,
|
|
100
|
+
{ button: 0, ctrlKey: false },
|
|
101
|
+
);
|
|
102
|
+
expect(await screen.findByRole("menu")).toBeDefined();
|
|
103
|
+
// Tag chips render inside the menu item.
|
|
104
|
+
expect(screen.getByText("Pricing")).toBeDefined();
|
|
105
|
+
|
|
106
|
+
fireEvent.click(screen.getByText("Pricing follow-up"));
|
|
107
|
+
|
|
108
|
+
await waitFor(() =>
|
|
109
|
+
expect(textarea.value).toContain("here is the pricing breakdown"),
|
|
110
|
+
);
|
|
111
|
+
expect(onApplyReplyTemplate).toHaveBeenCalledWith({
|
|
112
|
+
threadId: "t1",
|
|
113
|
+
templateId: "tmpl-pricing",
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("labels the picker trigger 'Templates'", () => {
|
|
118
|
+
const { container } = render(
|
|
119
|
+
<ConversationPanel
|
|
120
|
+
me={me}
|
|
121
|
+
threads={[
|
|
122
|
+
thread({
|
|
123
|
+
replyTemplates: [
|
|
124
|
+
{ id: "t", name: "A template", body: "Hi Priya, here you go.", tags: ["Follow up"] },
|
|
125
|
+
],
|
|
126
|
+
}),
|
|
127
|
+
]}
|
|
128
|
+
/>,
|
|
129
|
+
);
|
|
130
|
+
openReply(container);
|
|
131
|
+
expect(
|
|
132
|
+
container.querySelector('[data-slot="conv-reply-template-trigger"]')!.textContent,
|
|
133
|
+
).toContain("Templates");
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("blocks send while a {{personalize}} placeholder remains, then re-enables once filled", async () => {
|
|
137
|
+
const { container } = render(
|
|
138
|
+
<ConversationPanel
|
|
139
|
+
me={me}
|
|
140
|
+
threads={[
|
|
141
|
+
thread({
|
|
142
|
+
replyTemplates: [
|
|
143
|
+
{
|
|
144
|
+
id: "tmpl-personalize",
|
|
145
|
+
name: "Personalize me",
|
|
146
|
+
body: "Hi Priya, {{personalize}}",
|
|
147
|
+
tags: ["Follow up"],
|
|
148
|
+
},
|
|
149
|
+
],
|
|
150
|
+
}),
|
|
151
|
+
]}
|
|
152
|
+
/>,
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
const textarea = openReply(container);
|
|
156
|
+
fireEvent.pointerDown(
|
|
157
|
+
container.querySelector('[data-slot="conv-reply-template-trigger"]')!,
|
|
158
|
+
{ button: 0, ctrlKey: false },
|
|
159
|
+
);
|
|
160
|
+
expect(await screen.findByRole("menu")).toBeDefined();
|
|
161
|
+
fireEvent.click(screen.getByText("Personalize me"));
|
|
162
|
+
|
|
163
|
+
await waitFor(() => expect(textarea.value).toContain("{{personalize}}"));
|
|
164
|
+
|
|
165
|
+
// The manual-fill alert shows and Send/Preview are blocked.
|
|
166
|
+
expect(container.querySelector('[data-slot="conv-reply-manual-fill"]')).not.toBeNull();
|
|
167
|
+
const sendButton = screen.getByRole("button", { name: "Send" }) as HTMLButtonElement;
|
|
168
|
+
expect(sendButton.disabled).toBe(true);
|
|
169
|
+
|
|
170
|
+
// Replace the placeholder: the alert clears and Send re-enables.
|
|
171
|
+
fireEvent.change(textarea, {
|
|
172
|
+
target: { value: "Hi Priya, here is your tailored Q2 update." },
|
|
173
|
+
});
|
|
174
|
+
await waitFor(() =>
|
|
175
|
+
expect(container.querySelector('[data-slot="conv-reply-manual-fill"]')).toBeNull(),
|
|
176
|
+
);
|
|
177
|
+
expect((screen.getByRole("button", { name: "Send" }) as HTMLButtonElement).disabled).toBe(
|
|
178
|
+
false,
|
|
179
|
+
);
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
60
183
|
it("shows the amber response-detected treatment when a thread has responded", () => {
|
|
61
184
|
const { container } = render(<ConversationPanel threads={[thread({ paused: { playbook: "Mercury Renewal Save" } })]} me={me} />);
|
|
62
185
|
|
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
GitMerge,
|
|
39
39
|
Check,
|
|
40
40
|
X,
|
|
41
|
+
FileText,
|
|
41
42
|
} from "lucide-react"
|
|
42
43
|
|
|
43
44
|
import { cn } from "../lib/utils"
|
|
@@ -48,6 +49,14 @@ import { Button } from "./button"
|
|
|
48
49
|
import { Switch } from "./switch"
|
|
49
50
|
import { Textarea } from "./textarea"
|
|
50
51
|
import { RichTextToolbar } from "./rich-text-toolbar"
|
|
52
|
+
import {
|
|
53
|
+
DropdownMenu,
|
|
54
|
+
DropdownMenuContent,
|
|
55
|
+
DropdownMenuItem,
|
|
56
|
+
DropdownMenuLabel,
|
|
57
|
+
DropdownMenuSeparator,
|
|
58
|
+
DropdownMenuTrigger,
|
|
59
|
+
} from "./dropdown-menu"
|
|
51
60
|
import { EmailBody } from "./email-body"
|
|
52
61
|
import { decodeEmailDisplayText, emailBodySnippet, formatAddressList, normalizeEmailSender } from "./email-display-helpers"
|
|
53
62
|
import {
|
|
@@ -116,6 +125,21 @@ export interface ConvMessage {
|
|
|
116
125
|
|
|
117
126
|
export type ConvStatus = "responded" | "awaiting" | "viewing" | "draft"
|
|
118
127
|
|
|
128
|
+
/**
|
|
129
|
+
* A follow-up template offered in the in-composer "Templates" picker (WIT-970).
|
|
130
|
+
* Presentational: the consumer fills variables for THIS thread's recipient
|
|
131
|
+
* before passing it in, so `body` is composer-ready text. `tags` (e.g. "Reply")
|
|
132
|
+
* are shown as chips so reps can recognize the right template at a glance.
|
|
133
|
+
*/
|
|
134
|
+
export interface ConversationReplyTemplate {
|
|
135
|
+
id: string
|
|
136
|
+
name: string
|
|
137
|
+
/** Composer-ready reply body, already personalized by the consumer. */
|
|
138
|
+
body: string
|
|
139
|
+
/** Optional tags shown as chips in the picker for findability. */
|
|
140
|
+
tags?: string[]
|
|
141
|
+
}
|
|
142
|
+
|
|
119
143
|
export interface ConversationThread {
|
|
120
144
|
threadId: string
|
|
121
145
|
subject: string
|
|
@@ -141,6 +165,12 @@ export interface ConversationThread {
|
|
|
141
165
|
draft?: string
|
|
142
166
|
/** Signature text appended to replies (plain text). */
|
|
143
167
|
signature?: string
|
|
168
|
+
/**
|
|
169
|
+
* Follow-up templates offered in the composer's "Templates" picker, already
|
|
170
|
+
* personalized for this thread's recipient (WIT-970). Omit/empty hides the
|
|
171
|
+
* picker.
|
|
172
|
+
*/
|
|
173
|
+
replyTemplates?: ConversationReplyTemplate[]
|
|
144
174
|
}
|
|
145
175
|
|
|
146
176
|
export interface ConversationReplyPayload {
|
|
@@ -178,6 +208,11 @@ export interface ConversationPanelProps {
|
|
|
178
208
|
*/
|
|
179
209
|
onPreviewReply?: (payload: ConversationReplyPayload) => Promise<ConversationReplyPreview>
|
|
180
210
|
onOpenInGmail?: (threadId: string) => void
|
|
211
|
+
/**
|
|
212
|
+
* Fired when the rep applies a reply template from the composer picker
|
|
213
|
+
* (WIT-970) — for analytics / template-usage tracking. Optional.
|
|
214
|
+
*/
|
|
215
|
+
onApplyReplyTemplate?: (info: { threadId: string; templateId: string }) => void
|
|
181
216
|
/** Inline-open this thread initially (defaults to the first responded one). */
|
|
182
217
|
defaultOpenThreadId?: string
|
|
183
218
|
className?: string
|
|
@@ -526,6 +561,14 @@ type PreviewState = {
|
|
|
526
561
|
|
|
527
562
|
const IDLE_PREVIEW: PreviewState = { loading: false, html: null, confirmationToken: null, error: null, local: false }
|
|
528
563
|
|
|
564
|
+
/**
|
|
565
|
+
* Matches an author-declared manual-fill placeholder ({{personalize}},
|
|
566
|
+
* {{personalization_recent_news}}, …). These have no resolver and must be
|
|
567
|
+
* replaced by the rep before sending; the composer blocks send while any remain.
|
|
568
|
+
* Non-global so repeated `.test()` calls during render are stateless.
|
|
569
|
+
*/
|
|
570
|
+
const MANUAL_FILL_TOKEN_RE = /\{\{\s*personaliz(?:e|ation)(?:_[a-z0-9]+)*\s*\}\}/i
|
|
571
|
+
|
|
529
572
|
function ReplyComposer({
|
|
530
573
|
thread,
|
|
531
574
|
me,
|
|
@@ -535,6 +578,7 @@ function ReplyComposer({
|
|
|
535
578
|
onSend,
|
|
536
579
|
onDraft,
|
|
537
580
|
onPreviewReply,
|
|
581
|
+
onApplyTemplate,
|
|
538
582
|
draftDisabledReason,
|
|
539
583
|
}: {
|
|
540
584
|
thread: ConversationThread
|
|
@@ -545,10 +589,23 @@ function ReplyComposer({
|
|
|
545
589
|
onSend: (body: string, includeSignature: boolean) => void | Promise<void>
|
|
546
590
|
onDraft: (body: string, includeSignature: boolean) => void | Promise<void>
|
|
547
591
|
onPreviewReply?: (payload: ConversationReplyPayload) => Promise<ConversationReplyPreview>
|
|
592
|
+
onApplyTemplate?: (templateId: string) => void
|
|
548
593
|
draftDisabledReason?: string | null
|
|
549
594
|
}) {
|
|
550
595
|
const [body, setBody] = React.useState(thread.draft ?? "")
|
|
596
|
+
const [appliedTemplate, setAppliedTemplate] = React.useState<string | null>(null)
|
|
551
597
|
const [sig, setSig] = React.useState(true)
|
|
598
|
+
const replyTemplates = thread.replyTemplates ?? []
|
|
599
|
+
// Manual-fill placeholders ({{personalize…}}) are author-required: the rep
|
|
600
|
+
// must replace them before sending. While any remain in the body we block
|
|
601
|
+
// send/preview so the personalization is never sent as a literal token.
|
|
602
|
+
const hasManualFillToken = MANUAL_FILL_TOKEN_RE.test(body)
|
|
603
|
+
|
|
604
|
+
const applyTemplate = (template: ConversationReplyTemplate) => {
|
|
605
|
+
setBody(template.body)
|
|
606
|
+
setAppliedTemplate(template.name)
|
|
607
|
+
onApplyTemplate?.(template.id)
|
|
608
|
+
}
|
|
552
609
|
const [preview, setPreview] = React.useState(false)
|
|
553
610
|
const [previewState, setPreviewState] = React.useState<PreviewState>(IDLE_PREVIEW)
|
|
554
611
|
const [sending, setSending] = React.useState(false)
|
|
@@ -674,7 +731,7 @@ function ReplyComposer({
|
|
|
674
731
|
onKeyDown={(e) => {
|
|
675
732
|
if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
|
|
676
733
|
e.preventDefault()
|
|
677
|
-
void openPreview()
|
|
734
|
+
if (!hasManualFillToken) void openPreview()
|
|
678
735
|
}
|
|
679
736
|
}}
|
|
680
737
|
/>
|
|
@@ -686,16 +743,75 @@ function ReplyComposer({
|
|
|
686
743
|
</div>
|
|
687
744
|
) : null}
|
|
688
745
|
|
|
746
|
+
{replyTemplates.length > 0 ? (
|
|
747
|
+
<div className="mt-2 flex flex-wrap items-center gap-2">
|
|
748
|
+
<DropdownMenu>
|
|
749
|
+
<DropdownMenuTrigger asChild>
|
|
750
|
+
<Button type="button" variant="outline" size="sm" disabled={sending} data-slot="conv-reply-template-trigger">
|
|
751
|
+
<FileText size={14} /> Templates
|
|
752
|
+
</Button>
|
|
753
|
+
</DropdownMenuTrigger>
|
|
754
|
+
<DropdownMenuContent align="start" className="max-w-xs">
|
|
755
|
+
<DropdownMenuLabel>Templates</DropdownMenuLabel>
|
|
756
|
+
<DropdownMenuSeparator />
|
|
757
|
+
{replyTemplates.map((template) => (
|
|
758
|
+
<DropdownMenuItem
|
|
759
|
+
key={template.id}
|
|
760
|
+
onSelect={() => applyTemplate(template)}
|
|
761
|
+
className="flex flex-col items-start gap-0.5"
|
|
762
|
+
>
|
|
763
|
+
<span className="text-[13px] font-medium">{template.name}</span>
|
|
764
|
+
{template.tags && template.tags.length ? (
|
|
765
|
+
<span className="flex flex-wrap gap-1">
|
|
766
|
+
{template.tags.map((tag) => (
|
|
767
|
+
<span
|
|
768
|
+
key={tag}
|
|
769
|
+
className="border-border bg-muted text-muted-foreground rounded border px-1 py-px text-[10px] leading-4"
|
|
770
|
+
>
|
|
771
|
+
{tag}
|
|
772
|
+
</span>
|
|
773
|
+
))}
|
|
774
|
+
</span>
|
|
775
|
+
) : null}
|
|
776
|
+
</DropdownMenuItem>
|
|
777
|
+
))}
|
|
778
|
+
</DropdownMenuContent>
|
|
779
|
+
</DropdownMenu>
|
|
780
|
+
{appliedTemplate ? (
|
|
781
|
+
<span className="text-muted-foreground inline-flex items-center gap-1 text-[11px]">
|
|
782
|
+
<Check size={11} /> Applied “{appliedTemplate}” · edit before sending
|
|
783
|
+
</span>
|
|
784
|
+
) : null}
|
|
785
|
+
</div>
|
|
786
|
+
) : null}
|
|
787
|
+
|
|
788
|
+
{hasManualFillToken ? (
|
|
789
|
+
<p data-slot="conv-reply-manual-fill" role="alert" className="text-destructive mt-2 text-[12px]">
|
|
790
|
+
Fill in the personalize placeholder before sending. Replace each {"{{personalize}}"} with a real, account-specific line.
|
|
791
|
+
</p>
|
|
792
|
+
) : null}
|
|
793
|
+
|
|
689
794
|
<div className="mt-2 flex flex-wrap items-center gap-2">
|
|
690
795
|
<RichTextToolbar />
|
|
691
796
|
<label className="text-muted-foreground ml-auto inline-flex cursor-pointer items-center gap-1.5 text-[12px]">
|
|
692
797
|
<Switch checked={sig} onCheckedChange={setSig} aria-label="Toggle signature" />
|
|
693
798
|
Signature
|
|
694
799
|
</label>
|
|
695
|
-
<Button
|
|
800
|
+
<Button
|
|
801
|
+
type="button"
|
|
802
|
+
variant="outline"
|
|
803
|
+
size="sm"
|
|
804
|
+
disabled={sending || hasManualFillToken}
|
|
805
|
+
onClick={() => void openPreview()}
|
|
806
|
+
>
|
|
696
807
|
<Eye size={14} /> Preview
|
|
697
808
|
</Button>
|
|
698
|
-
<Button
|
|
809
|
+
<Button
|
|
810
|
+
type="button"
|
|
811
|
+
size="sm"
|
|
812
|
+
disabled={sending || hasManualFillToken}
|
|
813
|
+
onClick={() => void openPreview()}
|
|
814
|
+
>
|
|
699
815
|
<Send size={14} /> Send
|
|
700
816
|
</Button>
|
|
701
817
|
</div>
|
|
@@ -708,7 +824,7 @@ function ReplyComposer({
|
|
|
708
824
|
</DialogTitle>
|
|
709
825
|
<DialogDescription>
|
|
710
826
|
{previewState.local
|
|
711
|
-
? "Local draft preview only
|
|
827
|
+
? "Local draft preview only. The server prepares the exact message on send."
|
|
712
828
|
: <>Stays on the {subject.replace(/^Re:\s*/i, "")} thread. Gmail keeps it threaded.</>}
|
|
713
829
|
</DialogDescription>
|
|
714
830
|
</DialogHeader>
|
|
@@ -768,7 +884,7 @@ function ReplyComposer({
|
|
|
768
884
|
<Button
|
|
769
885
|
type="button"
|
|
770
886
|
size="sm"
|
|
771
|
-
disabled={sending || previewState.loading}
|
|
887
|
+
disabled={sending || previewState.loading || hasManualFillToken}
|
|
772
888
|
onClick={handleSend}
|
|
773
889
|
>
|
|
774
890
|
<Send size={14} /> {sending ? "Sending..." : "Send now"}
|
|
@@ -797,6 +913,7 @@ function ThreadBody({
|
|
|
797
913
|
onCreateGmailDraft,
|
|
798
914
|
onPreviewReply,
|
|
799
915
|
onOpenInGmail,
|
|
916
|
+
onApplyReplyTemplate,
|
|
800
917
|
}: {
|
|
801
918
|
thread: ConversationThread
|
|
802
919
|
me?: ConvParticipant
|
|
@@ -805,6 +922,7 @@ function ThreadBody({
|
|
|
805
922
|
onCreateGmailDraft?: (p: ConversationReplyPayload) => void | Promise<void>
|
|
806
923
|
onPreviewReply?: (p: ConversationReplyPayload) => Promise<ConversationReplyPreview>
|
|
807
924
|
onOpenInGmail?: (threadId: string) => void
|
|
925
|
+
onApplyReplyTemplate?: ConversationPanelProps["onApplyReplyTemplate"]
|
|
808
926
|
}) {
|
|
809
927
|
const canReply = thread.canReply !== false
|
|
810
928
|
const replyDisabledReason = thread.replyDisabledReason?.trim() || "You are not a participant on this thread, so replying is disabled here."
|
|
@@ -885,6 +1003,7 @@ function ThreadBody({
|
|
|
885
1003
|
replyAll={replyAll}
|
|
886
1004
|
tenantName={tenantName}
|
|
887
1005
|
onPreviewReply={onPreviewReply}
|
|
1006
|
+
onApplyTemplate={(templateId) => onApplyReplyTemplate?.({ threadId: thread.threadId, templateId })}
|
|
888
1007
|
onClose={() => setMode("idle")}
|
|
889
1008
|
onSend={async (body, includeSignature) => {
|
|
890
1009
|
await onSendReply?.({ threadId: thread.threadId, body, includeSignature, replyAll })
|
|
@@ -940,11 +1059,12 @@ function ThreadRow({
|
|
|
940
1059
|
onCreateGmailDraft,
|
|
941
1060
|
onPreviewReply,
|
|
942
1061
|
onOpenInGmail,
|
|
1062
|
+
onApplyReplyTemplate,
|
|
943
1063
|
}: {
|
|
944
1064
|
thread: ConversationThread
|
|
945
1065
|
open: boolean
|
|
946
1066
|
onToggleOpen: () => void
|
|
947
|
-
} & Pick<ConversationPanelProps, "me" | "tenantName" | "onSendReply" | "onCreateGmailDraft" | "onPreviewReply" | "onOpenInGmail">) {
|
|
1067
|
+
} & Pick<ConversationPanelProps, "me" | "tenantName" | "onSendReply" | "onCreateGmailDraft" | "onPreviewReply" | "onOpenInGmail" | "onApplyReplyTemplate">) {
|
|
948
1068
|
const status = effectiveStatus(thread)
|
|
949
1069
|
const sortedMessages = React.useMemo(() => sortMessagesChronologically(thread.messages), [thread.messages])
|
|
950
1070
|
const last = sortedMessages[sortedMessages.length - 1]
|
|
@@ -997,6 +1117,7 @@ function ThreadRow({
|
|
|
997
1117
|
onCreateGmailDraft={onCreateGmailDraft}
|
|
998
1118
|
onPreviewReply={onPreviewReply}
|
|
999
1119
|
onOpenInGmail={onOpenInGmail}
|
|
1120
|
+
onApplyReplyTemplate={onApplyReplyTemplate}
|
|
1000
1121
|
/>
|
|
1001
1122
|
</div>
|
|
1002
1123
|
) : null}
|
|
@@ -1014,6 +1135,7 @@ function ConversationPanel({
|
|
|
1014
1135
|
onCreateGmailDraft,
|
|
1015
1136
|
onPreviewReply,
|
|
1016
1137
|
onOpenInGmail,
|
|
1138
|
+
onApplyReplyTemplate,
|
|
1017
1139
|
defaultOpenThreadId,
|
|
1018
1140
|
className,
|
|
1019
1141
|
}: ConversationPanelProps) {
|
|
@@ -1148,6 +1270,7 @@ function ConversationPanel({
|
|
|
1148
1270
|
onCreateGmailDraft={onCreateGmailDraft}
|
|
1149
1271
|
onPreviewReply={onPreviewReply}
|
|
1150
1272
|
onOpenInGmail={onOpenInGmail}
|
|
1273
|
+
onApplyReplyTemplate={onApplyReplyTemplate}
|
|
1151
1274
|
/>
|
|
1152
1275
|
))}
|
|
1153
1276
|
</div>
|