@handled-ai/design-system 0.20.25 → 0.20.26
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 +71 -11
- 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 +58 -0
- package/src/components/conversation-panel.tsx +95 -1
|
@@ -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 reply template offered in the in-composer "Apply template" 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
|
+
* Reply templates offered in the composer's "Apply template" 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 {
|
|
@@ -322,16 +331,24 @@ function ReplyComposer({
|
|
|
322
331
|
onSend,
|
|
323
332
|
onDraft,
|
|
324
333
|
onPreviewReply,
|
|
334
|
+
onApplyTemplate,
|
|
325
335
|
draftDisabledReason
|
|
326
336
|
}) {
|
|
327
|
-
var _a, _b, _c, _d, _e, _f;
|
|
337
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
328
338
|
const [body, setBody] = React.useState((_a = thread.draft) != null ? _a : "");
|
|
339
|
+
const [appliedTemplate, setAppliedTemplate] = React.useState(null);
|
|
329
340
|
const [sig, setSig] = React.useState(true);
|
|
341
|
+
const replyTemplates = (_b = thread.replyTemplates) != null ? _b : [];
|
|
342
|
+
const applyTemplate = (template) => {
|
|
343
|
+
setBody(template.body);
|
|
344
|
+
setAppliedTemplate(template.name);
|
|
345
|
+
onApplyTemplate == null ? void 0 : onApplyTemplate(template.id);
|
|
346
|
+
};
|
|
330
347
|
const [preview, setPreview] = React.useState(false);
|
|
331
348
|
const [previewState, setPreviewState] = React.useState(IDLE_PREVIEW);
|
|
332
349
|
const [sending, setSending] = React.useState(false);
|
|
333
350
|
const [sendError, setSendError] = React.useState(null);
|
|
334
|
-
const ccList = replyAll ? (
|
|
351
|
+
const ccList = replyAll ? (_c = thread.cc) != null ? _c : [] : [];
|
|
335
352
|
const subject = /^re:/i.test(thread.subject) ? thread.subject : `Re: ${thread.subject}`;
|
|
336
353
|
const draftDisabled = Boolean(draftDisabledReason);
|
|
337
354
|
const localPreviewHtml = textToHtml(body) + (sig && thread.signature ? textToHtml(thread.signature) : "");
|
|
@@ -415,7 +432,7 @@ function ReplyComposer({
|
|
|
415
432
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5", children: [
|
|
416
433
|
/* @__PURE__ */ jsx("span", { className: "text-muted-foreground w-12 shrink-0 text-[11px] font-medium", children: "To" }),
|
|
417
434
|
/* @__PURE__ */ jsx("span", { className: "font-medium", children: displayParticipant(thread.contact).name }),
|
|
418
|
-
/* @__PURE__ */ jsx("span", { className: "text-muted-foreground/60 truncate text-xs", children: (
|
|
435
|
+
/* @__PURE__ */ jsx("span", { className: "text-muted-foreground/60 truncate text-xs", children: (_d = displayParticipant(thread.contact).email) != null ? _d : thread.contact.email })
|
|
419
436
|
] }),
|
|
420
437
|
replyAll && ccList.length ? /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-1.5", children: [
|
|
421
438
|
/* @__PURE__ */ jsx("span", { className: "text-muted-foreground w-12 shrink-0 text-[11px] font-medium", children: "Cc" }),
|
|
@@ -452,6 +469,43 @@ function ReplyComposer({
|
|
|
452
469
|
/* @__PURE__ */ jsx("span", { className: "text-muted-foreground/60 mr-1", children: "--" }),
|
|
453
470
|
thread.signature
|
|
454
471
|
] }) : null,
|
|
472
|
+
replyTemplates.length > 0 ? /* @__PURE__ */ jsxs("div", { className: "mt-2 flex flex-wrap items-center gap-2", children: [
|
|
473
|
+
/* @__PURE__ */ jsxs(DropdownMenu, { children: [
|
|
474
|
+
/* @__PURE__ */ jsx(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(Button, { type: "button", variant: "outline", size: "sm", disabled: sending, "data-slot": "conv-reply-template-trigger", children: [
|
|
475
|
+
/* @__PURE__ */ jsx(FileText, { size: 14 }),
|
|
476
|
+
" Apply template"
|
|
477
|
+
] }) }),
|
|
478
|
+
/* @__PURE__ */ jsxs(DropdownMenuContent, { align: "start", className: "max-w-xs", children: [
|
|
479
|
+
/* @__PURE__ */ jsx(DropdownMenuLabel, { children: "Reply templates" }),
|
|
480
|
+
/* @__PURE__ */ jsx(DropdownMenuSeparator, {}),
|
|
481
|
+
replyTemplates.map((template) => /* @__PURE__ */ jsxs(
|
|
482
|
+
DropdownMenuItem,
|
|
483
|
+
{
|
|
484
|
+
onSelect: () => applyTemplate(template),
|
|
485
|
+
className: "flex flex-col items-start gap-0.5",
|
|
486
|
+
children: [
|
|
487
|
+
/* @__PURE__ */ jsx("span", { className: "text-[13px] font-medium", children: template.name }),
|
|
488
|
+
template.tags && template.tags.length ? /* @__PURE__ */ jsx("span", { className: "flex flex-wrap gap-1", children: template.tags.map((tag) => /* @__PURE__ */ jsx(
|
|
489
|
+
"span",
|
|
490
|
+
{
|
|
491
|
+
className: "border-border bg-muted text-muted-foreground rounded border px-1 py-px text-[10px] leading-4",
|
|
492
|
+
children: tag
|
|
493
|
+
},
|
|
494
|
+
tag
|
|
495
|
+
)) }) : null
|
|
496
|
+
]
|
|
497
|
+
},
|
|
498
|
+
template.id
|
|
499
|
+
))
|
|
500
|
+
] })
|
|
501
|
+
] }),
|
|
502
|
+
appliedTemplate ? /* @__PURE__ */ jsxs("span", { className: "text-muted-foreground inline-flex items-center gap-1 text-[11px]", children: [
|
|
503
|
+
/* @__PURE__ */ jsx(Check, { size: 11 }),
|
|
504
|
+
" Applied \u201C",
|
|
505
|
+
appliedTemplate,
|
|
506
|
+
"\u201D \xB7 edit before sending"
|
|
507
|
+
] }) : null
|
|
508
|
+
] }) : null,
|
|
455
509
|
/* @__PURE__ */ jsxs("div", { className: "mt-2 flex flex-wrap items-center gap-2", children: [
|
|
456
510
|
/* @__PURE__ */ jsx(RichTextToolbar, {}),
|
|
457
511
|
/* @__PURE__ */ jsxs("label", { className: "text-muted-foreground ml-auto inline-flex cursor-pointer items-center gap-1.5 text-[12px]", children: [
|
|
@@ -492,7 +546,7 @@ function ReplyComposer({
|
|
|
492
546
|
" ",
|
|
493
547
|
/* @__PURE__ */ jsxs("span", { className: "text-muted-foreground/60", children: [
|
|
494
548
|
"<",
|
|
495
|
-
(
|
|
549
|
+
(_e = displayParticipant(thread.contact).email) != null ? _e : thread.contact.email,
|
|
496
550
|
">"
|
|
497
551
|
] })
|
|
498
552
|
] }),
|
|
@@ -515,9 +569,9 @@ function ReplyComposer({
|
|
|
515
569
|
"div",
|
|
516
570
|
{
|
|
517
571
|
"data-slot": "conv-preview-body",
|
|
518
|
-
"data-confirmation-token": (
|
|
572
|
+
"data-confirmation-token": (_f = previewState.confirmationToken) != null ? _f : void 0,
|
|
519
573
|
className: "max-h-72 overflow-auto",
|
|
520
|
-
children: /* @__PURE__ */ jsx(EmailBody, { html: (
|
|
574
|
+
children: /* @__PURE__ */ jsx(EmailBody, { html: (_g = previewState.html) != null ? _g : "", variant: "preview", collapseDetails: false, defaultDetailsOpen: true })
|
|
521
575
|
}
|
|
522
576
|
),
|
|
523
577
|
sendError ? /* @__PURE__ */ jsx("p", { role: "alert", className: "text-destructive text-sm", children: sendError }) : null,
|
|
@@ -568,7 +622,8 @@ function ThreadBody({
|
|
|
568
622
|
onSendReply,
|
|
569
623
|
onCreateGmailDraft,
|
|
570
624
|
onPreviewReply,
|
|
571
|
-
onOpenInGmail
|
|
625
|
+
onOpenInGmail,
|
|
626
|
+
onApplyReplyTemplate
|
|
572
627
|
}) {
|
|
573
628
|
var _a;
|
|
574
629
|
const canReply = thread.canReply !== false;
|
|
@@ -646,6 +701,7 @@ function ThreadBody({
|
|
|
646
701
|
replyAll,
|
|
647
702
|
tenantName,
|
|
648
703
|
onPreviewReply,
|
|
704
|
+
onApplyTemplate: (templateId) => onApplyReplyTemplate == null ? void 0 : onApplyReplyTemplate({ threadId: thread.threadId, templateId }),
|
|
649
705
|
onClose: () => setMode("idle"),
|
|
650
706
|
onSend: async (body, includeSignature) => {
|
|
651
707
|
await (onSendReply == null ? void 0 : onSendReply({ threadId: thread.threadId, body, includeSignature, replyAll }));
|
|
@@ -697,7 +753,8 @@ function ThreadRow({
|
|
|
697
753
|
onSendReply,
|
|
698
754
|
onCreateGmailDraft,
|
|
699
755
|
onPreviewReply,
|
|
700
|
-
onOpenInGmail
|
|
756
|
+
onOpenInGmail,
|
|
757
|
+
onApplyReplyTemplate
|
|
701
758
|
}) {
|
|
702
759
|
var _a;
|
|
703
760
|
const status = effectiveStatus(thread);
|
|
@@ -752,7 +809,8 @@ function ThreadRow({
|
|
|
752
809
|
onSendReply,
|
|
753
810
|
onCreateGmailDraft,
|
|
754
811
|
onPreviewReply,
|
|
755
|
-
onOpenInGmail
|
|
812
|
+
onOpenInGmail,
|
|
813
|
+
onApplyReplyTemplate
|
|
756
814
|
}
|
|
757
815
|
) }) : null
|
|
758
816
|
]
|
|
@@ -767,6 +825,7 @@ function ConversationPanel({
|
|
|
767
825
|
onCreateGmailDraft,
|
|
768
826
|
onPreviewReply,
|
|
769
827
|
onOpenInGmail,
|
|
828
|
+
onApplyReplyTemplate,
|
|
770
829
|
defaultOpenThreadId,
|
|
771
830
|
className
|
|
772
831
|
}) {
|
|
@@ -865,7 +924,8 @@ function ConversationPanel({
|
|
|
865
924
|
onSendReply,
|
|
866
925
|
onCreateGmailDraft,
|
|
867
926
|
onPreviewReply,
|
|
868
|
-
onOpenInGmail
|
|
927
|
+
onOpenInGmail,
|
|
928
|
+
onApplyReplyTemplate
|
|
869
929
|
},
|
|
870
930
|
t.threadId
|
|
871
931
|
)) }) : 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 reply template offered in the in-composer \"Apply template\" 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 * Reply templates offered in the composer's \"Apply template\" 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\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\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 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} /> Apply template\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"start\" className=\"max-w-xs\">\n <DropdownMenuLabel>Reply 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 <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 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":"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","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,64 @@ 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
|
+
|
|
60
118
|
it("shows the amber response-detected treatment when a thread has responded", () => {
|
|
61
119
|
const { container } = render(<ConversationPanel threads={[thread({ paused: { playbook: "Mercury Renewal Save" } })]} me={me} />);
|
|
62
120
|
|
|
@@ -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 reply template offered in the in-composer "Apply template" 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
|
+
* Reply templates offered in the composer's "Apply template" 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
|
|
@@ -535,6 +570,7 @@ function ReplyComposer({
|
|
|
535
570
|
onSend,
|
|
536
571
|
onDraft,
|
|
537
572
|
onPreviewReply,
|
|
573
|
+
onApplyTemplate,
|
|
538
574
|
draftDisabledReason,
|
|
539
575
|
}: {
|
|
540
576
|
thread: ConversationThread
|
|
@@ -545,10 +581,19 @@ function ReplyComposer({
|
|
|
545
581
|
onSend: (body: string, includeSignature: boolean) => void | Promise<void>
|
|
546
582
|
onDraft: (body: string, includeSignature: boolean) => void | Promise<void>
|
|
547
583
|
onPreviewReply?: (payload: ConversationReplyPayload) => Promise<ConversationReplyPreview>
|
|
584
|
+
onApplyTemplate?: (templateId: string) => void
|
|
548
585
|
draftDisabledReason?: string | null
|
|
549
586
|
}) {
|
|
550
587
|
const [body, setBody] = React.useState(thread.draft ?? "")
|
|
588
|
+
const [appliedTemplate, setAppliedTemplate] = React.useState<string | null>(null)
|
|
551
589
|
const [sig, setSig] = React.useState(true)
|
|
590
|
+
const replyTemplates = thread.replyTemplates ?? []
|
|
591
|
+
|
|
592
|
+
const applyTemplate = (template: ConversationReplyTemplate) => {
|
|
593
|
+
setBody(template.body)
|
|
594
|
+
setAppliedTemplate(template.name)
|
|
595
|
+
onApplyTemplate?.(template.id)
|
|
596
|
+
}
|
|
552
597
|
const [preview, setPreview] = React.useState(false)
|
|
553
598
|
const [previewState, setPreviewState] = React.useState<PreviewState>(IDLE_PREVIEW)
|
|
554
599
|
const [sending, setSending] = React.useState(false)
|
|
@@ -686,6 +731,48 @@ function ReplyComposer({
|
|
|
686
731
|
</div>
|
|
687
732
|
) : null}
|
|
688
733
|
|
|
734
|
+
{replyTemplates.length > 0 ? (
|
|
735
|
+
<div className="mt-2 flex flex-wrap items-center gap-2">
|
|
736
|
+
<DropdownMenu>
|
|
737
|
+
<DropdownMenuTrigger asChild>
|
|
738
|
+
<Button type="button" variant="outline" size="sm" disabled={sending} data-slot="conv-reply-template-trigger">
|
|
739
|
+
<FileText size={14} /> Apply template
|
|
740
|
+
</Button>
|
|
741
|
+
</DropdownMenuTrigger>
|
|
742
|
+
<DropdownMenuContent align="start" className="max-w-xs">
|
|
743
|
+
<DropdownMenuLabel>Reply templates</DropdownMenuLabel>
|
|
744
|
+
<DropdownMenuSeparator />
|
|
745
|
+
{replyTemplates.map((template) => (
|
|
746
|
+
<DropdownMenuItem
|
|
747
|
+
key={template.id}
|
|
748
|
+
onSelect={() => applyTemplate(template)}
|
|
749
|
+
className="flex flex-col items-start gap-0.5"
|
|
750
|
+
>
|
|
751
|
+
<span className="text-[13px] font-medium">{template.name}</span>
|
|
752
|
+
{template.tags && template.tags.length ? (
|
|
753
|
+
<span className="flex flex-wrap gap-1">
|
|
754
|
+
{template.tags.map((tag) => (
|
|
755
|
+
<span
|
|
756
|
+
key={tag}
|
|
757
|
+
className="border-border bg-muted text-muted-foreground rounded border px-1 py-px text-[10px] leading-4"
|
|
758
|
+
>
|
|
759
|
+
{tag}
|
|
760
|
+
</span>
|
|
761
|
+
))}
|
|
762
|
+
</span>
|
|
763
|
+
) : null}
|
|
764
|
+
</DropdownMenuItem>
|
|
765
|
+
))}
|
|
766
|
+
</DropdownMenuContent>
|
|
767
|
+
</DropdownMenu>
|
|
768
|
+
{appliedTemplate ? (
|
|
769
|
+
<span className="text-muted-foreground inline-flex items-center gap-1 text-[11px]">
|
|
770
|
+
<Check size={11} /> Applied “{appliedTemplate}” · edit before sending
|
|
771
|
+
</span>
|
|
772
|
+
) : null}
|
|
773
|
+
</div>
|
|
774
|
+
) : null}
|
|
775
|
+
|
|
689
776
|
<div className="mt-2 flex flex-wrap items-center gap-2">
|
|
690
777
|
<RichTextToolbar />
|
|
691
778
|
<label className="text-muted-foreground ml-auto inline-flex cursor-pointer items-center gap-1.5 text-[12px]">
|
|
@@ -797,6 +884,7 @@ function ThreadBody({
|
|
|
797
884
|
onCreateGmailDraft,
|
|
798
885
|
onPreviewReply,
|
|
799
886
|
onOpenInGmail,
|
|
887
|
+
onApplyReplyTemplate,
|
|
800
888
|
}: {
|
|
801
889
|
thread: ConversationThread
|
|
802
890
|
me?: ConvParticipant
|
|
@@ -805,6 +893,7 @@ function ThreadBody({
|
|
|
805
893
|
onCreateGmailDraft?: (p: ConversationReplyPayload) => void | Promise<void>
|
|
806
894
|
onPreviewReply?: (p: ConversationReplyPayload) => Promise<ConversationReplyPreview>
|
|
807
895
|
onOpenInGmail?: (threadId: string) => void
|
|
896
|
+
onApplyReplyTemplate?: ConversationPanelProps["onApplyReplyTemplate"]
|
|
808
897
|
}) {
|
|
809
898
|
const canReply = thread.canReply !== false
|
|
810
899
|
const replyDisabledReason = thread.replyDisabledReason?.trim() || "You are not a participant on this thread, so replying is disabled here."
|
|
@@ -885,6 +974,7 @@ function ThreadBody({
|
|
|
885
974
|
replyAll={replyAll}
|
|
886
975
|
tenantName={tenantName}
|
|
887
976
|
onPreviewReply={onPreviewReply}
|
|
977
|
+
onApplyTemplate={(templateId) => onApplyReplyTemplate?.({ threadId: thread.threadId, templateId })}
|
|
888
978
|
onClose={() => setMode("idle")}
|
|
889
979
|
onSend={async (body, includeSignature) => {
|
|
890
980
|
await onSendReply?.({ threadId: thread.threadId, body, includeSignature, replyAll })
|
|
@@ -940,11 +1030,12 @@ function ThreadRow({
|
|
|
940
1030
|
onCreateGmailDraft,
|
|
941
1031
|
onPreviewReply,
|
|
942
1032
|
onOpenInGmail,
|
|
1033
|
+
onApplyReplyTemplate,
|
|
943
1034
|
}: {
|
|
944
1035
|
thread: ConversationThread
|
|
945
1036
|
open: boolean
|
|
946
1037
|
onToggleOpen: () => void
|
|
947
|
-
} & Pick<ConversationPanelProps, "me" | "tenantName" | "onSendReply" | "onCreateGmailDraft" | "onPreviewReply" | "onOpenInGmail">) {
|
|
1038
|
+
} & Pick<ConversationPanelProps, "me" | "tenantName" | "onSendReply" | "onCreateGmailDraft" | "onPreviewReply" | "onOpenInGmail" | "onApplyReplyTemplate">) {
|
|
948
1039
|
const status = effectiveStatus(thread)
|
|
949
1040
|
const sortedMessages = React.useMemo(() => sortMessagesChronologically(thread.messages), [thread.messages])
|
|
950
1041
|
const last = sortedMessages[sortedMessages.length - 1]
|
|
@@ -997,6 +1088,7 @@ function ThreadRow({
|
|
|
997
1088
|
onCreateGmailDraft={onCreateGmailDraft}
|
|
998
1089
|
onPreviewReply={onPreviewReply}
|
|
999
1090
|
onOpenInGmail={onOpenInGmail}
|
|
1091
|
+
onApplyReplyTemplate={onApplyReplyTemplate}
|
|
1000
1092
|
/>
|
|
1001
1093
|
</div>
|
|
1002
1094
|
) : null}
|
|
@@ -1014,6 +1106,7 @@ function ConversationPanel({
|
|
|
1014
1106
|
onCreateGmailDraft,
|
|
1015
1107
|
onPreviewReply,
|
|
1016
1108
|
onOpenInGmail,
|
|
1109
|
+
onApplyReplyTemplate,
|
|
1017
1110
|
defaultOpenThreadId,
|
|
1018
1111
|
className,
|
|
1019
1112
|
}: ConversationPanelProps) {
|
|
@@ -1148,6 +1241,7 @@ function ConversationPanel({
|
|
|
1148
1241
|
onCreateGmailDraft={onCreateGmailDraft}
|
|
1149
1242
|
onPreviewReply={onPreviewReply}
|
|
1150
1243
|
onOpenInGmail={onOpenInGmail}
|
|
1244
|
+
onApplyReplyTemplate={onApplyReplyTemplate}
|
|
1151
1245
|
/>
|
|
1152
1246
|
))}
|
|
1153
1247
|
</div>
|