@handled-ai/design-system 0.20.26 → 0.20.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/conversation-panel.d.ts +2 -2
- package/dist/components/conversation-panel.js +39 -13
- package/dist/components/conversation-panel.js.map +1 -1
- package/package.json +1 -1
- package/src/components/__tests__/conversation-panel.test.tsx +65 -0
- package/src/components/conversation-panel.tsx +38 -9
|
@@ -80,7 +80,7 @@ interface ConvMessage {
|
|
|
80
80
|
}
|
|
81
81
|
type ConvStatus = "responded" | "awaiting" | "viewing" | "draft";
|
|
82
82
|
/**
|
|
83
|
-
* A
|
|
83
|
+
* A follow-up template offered in the in-composer "Templates" picker (WIT-970).
|
|
84
84
|
* Presentational: the consumer fills variables for THIS thread's recipient
|
|
85
85
|
* before passing it in, so `body` is composer-ready text. `tags` (e.g. "Reply")
|
|
86
86
|
* are shown as chips so reps can recognize the right template at a glance.
|
|
@@ -121,7 +121,7 @@ interface ConversationThread {
|
|
|
121
121
|
/** Signature text appended to replies (plain text). */
|
|
122
122
|
signature?: string;
|
|
123
123
|
/**
|
|
124
|
-
*
|
|
124
|
+
* Follow-up templates offered in the composer's "Templates" picker, already
|
|
125
125
|
* personalized for this thread's recipient (WIT-970). Omit/empty hides the
|
|
126
126
|
* picker.
|
|
127
127
|
*/
|
|
@@ -322,6 +322,7 @@ function MessageView({
|
|
|
322
322
|
] });
|
|
323
323
|
}
|
|
324
324
|
const IDLE_PREVIEW = { loading: false, html: null, confirmationToken: null, error: null, local: false };
|
|
325
|
+
const MANUAL_FILL_TOKEN_RE = /\{\{\s*personaliz(?:e|ation)(?:_[a-z0-9]+)*\s*\}\}/i;
|
|
325
326
|
function ReplyComposer({
|
|
326
327
|
thread,
|
|
327
328
|
me,
|
|
@@ -339,6 +340,7 @@ function ReplyComposer({
|
|
|
339
340
|
const [appliedTemplate, setAppliedTemplate] = React.useState(null);
|
|
340
341
|
const [sig, setSig] = React.useState(true);
|
|
341
342
|
const replyTemplates = (_b = thread.replyTemplates) != null ? _b : [];
|
|
343
|
+
const hasManualFillToken = MANUAL_FILL_TOKEN_RE.test(body);
|
|
342
344
|
const applyTemplate = (template) => {
|
|
343
345
|
setBody(template.body);
|
|
344
346
|
setAppliedTemplate(template.name);
|
|
@@ -460,7 +462,7 @@ function ReplyComposer({
|
|
|
460
462
|
onKeyDown: (e) => {
|
|
461
463
|
if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
|
|
462
464
|
e.preventDefault();
|
|
463
|
-
void openPreview();
|
|
465
|
+
if (!hasManualFillToken) void openPreview();
|
|
464
466
|
}
|
|
465
467
|
}
|
|
466
468
|
}
|
|
@@ -473,10 +475,10 @@ function ReplyComposer({
|
|
|
473
475
|
/* @__PURE__ */ jsxs(DropdownMenu, { children: [
|
|
474
476
|
/* @__PURE__ */ jsx(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(Button, { type: "button", variant: "outline", size: "sm", disabled: sending, "data-slot": "conv-reply-template-trigger", children: [
|
|
475
477
|
/* @__PURE__ */ jsx(FileText, { size: 14 }),
|
|
476
|
-
"
|
|
478
|
+
" Templates"
|
|
477
479
|
] }) }),
|
|
478
480
|
/* @__PURE__ */ jsxs(DropdownMenuContent, { align: "start", className: "max-w-xs", children: [
|
|
479
|
-
/* @__PURE__ */ jsx(DropdownMenuLabel, { children: "
|
|
481
|
+
/* @__PURE__ */ jsx(DropdownMenuLabel, { children: "Templates" }),
|
|
480
482
|
/* @__PURE__ */ jsx(DropdownMenuSeparator, {}),
|
|
481
483
|
replyTemplates.map((template) => /* @__PURE__ */ jsxs(
|
|
482
484
|
DropdownMenuItem,
|
|
@@ -506,20 +508,44 @@ function ReplyComposer({
|
|
|
506
508
|
"\u201D \xB7 edit before sending"
|
|
507
509
|
] }) : null
|
|
508
510
|
] }) : null,
|
|
511
|
+
hasManualFillToken ? /* @__PURE__ */ jsxs("p", { "data-slot": "conv-reply-manual-fill", role: "alert", className: "text-destructive mt-2 text-[12px]", children: [
|
|
512
|
+
"Fill in the personalize placeholder before sending. Replace each ",
|
|
513
|
+
"{{personalize}}",
|
|
514
|
+
" with a real, account-specific line."
|
|
515
|
+
] }) : null,
|
|
509
516
|
/* @__PURE__ */ jsxs("div", { className: "mt-2 flex flex-wrap items-center gap-2", children: [
|
|
510
517
|
/* @__PURE__ */ jsx(RichTextToolbar, {}),
|
|
511
518
|
/* @__PURE__ */ jsxs("label", { className: "text-muted-foreground ml-auto inline-flex cursor-pointer items-center gap-1.5 text-[12px]", children: [
|
|
512
519
|
/* @__PURE__ */ jsx(Switch, { checked: sig, onCheckedChange: setSig, "aria-label": "Toggle signature" }),
|
|
513
520
|
"Signature"
|
|
514
521
|
] }),
|
|
515
|
-
/* @__PURE__ */ jsxs(
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
522
|
+
/* @__PURE__ */ jsxs(
|
|
523
|
+
Button,
|
|
524
|
+
{
|
|
525
|
+
type: "button",
|
|
526
|
+
variant: "outline",
|
|
527
|
+
size: "sm",
|
|
528
|
+
disabled: sending || hasManualFillToken,
|
|
529
|
+
onClick: () => void openPreview(),
|
|
530
|
+
children: [
|
|
531
|
+
/* @__PURE__ */ jsx(Eye, { size: 14 }),
|
|
532
|
+
" Preview"
|
|
533
|
+
]
|
|
534
|
+
}
|
|
535
|
+
),
|
|
536
|
+
/* @__PURE__ */ jsxs(
|
|
537
|
+
Button,
|
|
538
|
+
{
|
|
539
|
+
type: "button",
|
|
540
|
+
size: "sm",
|
|
541
|
+
disabled: sending || hasManualFillToken,
|
|
542
|
+
onClick: () => void openPreview(),
|
|
543
|
+
children: [
|
|
544
|
+
/* @__PURE__ */ jsx(Send, { size: 14 }),
|
|
545
|
+
" Send"
|
|
546
|
+
]
|
|
547
|
+
}
|
|
548
|
+
)
|
|
523
549
|
] }),
|
|
524
550
|
/* @__PURE__ */ jsx(Dialog, { open: preview, onOpenChange: (open) => {
|
|
525
551
|
if (!sending) {
|
|
@@ -533,7 +559,7 @@ function ReplyComposer({
|
|
|
533
559
|
" ",
|
|
534
560
|
previewState.local ? "Local draft preview" : "Reply preview"
|
|
535
561
|
] }),
|
|
536
|
-
/* @__PURE__ */ jsx(DialogDescription, { children: previewState.local ? "Local draft preview only
|
|
562
|
+
/* @__PURE__ */ jsx(DialogDescription, { children: previewState.local ? "Local draft preview only. The server prepares the exact message on send." : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
537
563
|
"Stays on the ",
|
|
538
564
|
subject.replace(/^Re:\s*/i, ""),
|
|
539
565
|
" thread. Gmail keeps it threaded."
|
|
@@ -600,7 +626,7 @@ function ReplyComposer({
|
|
|
600
626
|
{
|
|
601
627
|
type: "button",
|
|
602
628
|
size: "sm",
|
|
603
|
-
disabled: sending || previewState.loading,
|
|
629
|
+
disabled: sending || previewState.loading || hasManualFillToken,
|
|
604
630
|
onClick: handleSend,
|
|
605
631
|
children: [
|
|
606
632
|
/* @__PURE__ */ jsx(Send, { size: 14 }),
|
|
@@ -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 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"]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/conversation-panel.tsx"],"sourcesContent":["\"use client\"\n\n/**\n * conversation-panel.tsx — in-case email-thread reader + reply, for the case\n * panel (\"Email response detected\" hub).\n *\n * v1 scope (WIT-853 / WIT-802): INLINE thread reading + reply only.\n * - A collapsible hub header with a pulse badge whose color reflects state:\n * responded (a reply needs you) / awaiting (sent, waiting) / viewing (read-only).\n * - A list of thread rows; clicking one opens a Gmail-style reader inline,\n * right under the row, with collapsible messages + quoted-history toggle.\n * - Reply / Reply-all composer with a signature toggle and two send paths:\n * Preview→Send (in-app) and \"Open draft in Gmail\" (deep link).\n * - A \"playbook stopped\" banner when the customer's reply halted the sequence,\n * and a read-only notice when the operator is not a thread participant.\n *\n * The bottom-right floating dock / side-by-side compare is intentionally OUT of\n * scope here and tracked separately (WIT-855).\n *\n * Presentational: all data + side effects come from the consumer. Email bodies\n * (`bodyHtml`, `quoted.html`) are sanitized before rendering.\n */\n\nimport * as React from \"react\"\nimport {\n ChevronDown,\n ChevronUp,\n CornerUpLeft,\n CheckCheck,\n MailOpen,\n FilePenLine,\n Reply,\n ReplyAll,\n Eye,\n Send,\n Lock,\n Pause,\n GitMerge,\n Check,\n X,\n FileText,\n} from \"lucide-react\"\n\nimport { cn } from \"../lib/utils\"\nimport { getInitials } from \"../lib/user-display\"\nimport { BRAND_ICONS } from \"../lib/icons\"\nimport { Avatar, AvatarFallback, AvatarImage } from \"./avatar\"\nimport { Button } from \"./button\"\nimport { Switch } from \"./switch\"\nimport { Textarea } from \"./textarea\"\nimport { RichTextToolbar } from \"./rich-text-toolbar\"\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuLabel,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n} from \"./dropdown-menu\"\nimport { EmailBody } from \"./email-body\"\nimport { decodeEmailDisplayText, emailBodySnippet, formatAddressList, normalizeEmailSender } from \"./email-display-helpers\"\nimport {\n Dialog,\n DialogContent,\n DialogHeader,\n DialogTitle,\n DialogDescription,\n DialogFooter,\n} from \"./dialog\"\n\n/* ── Types ───────────────────────────────────────────────────────────────── */\n\nexport interface ConvParticipant {\n name: string\n email: string\n avatarUrl?: string | null\n role?: string\n}\n\nexport interface ConvMessage {\n id: string\n direction: \"inbound\" | \"outbound\"\n from: ConvParticipant\n to: ConvParticipant\n /** Absolute timestamp label, e.g. \"Jun 1, 2026, 9:12 AM\". */\n date: string\n /**\n * Raw chronological timestamp for deterministic thread ordering. Prefer\n * `sentAt` for outbound messages and `receivedAt` for inbound messages.\n * Accepts ISO/RFC822 strings, Date objects, epoch milliseconds, or Gmail\n * internalDate values as strings/numbers. Display-only `date` / `ago` labels\n * are never parsed for ordering.\n */\n timestamp?: string | number | Date | null\n rawTimestamp?: string | number | Date | null\n sentAt?: string | number | Date | null\n receivedAt?: string | number | Date | null\n /** Compatibility with data contracts that pass through source field names. */\n sent_at?: string | number | Date | null\n received_at?: string | number | Date | null\n internalDate?: string | number | Date | null\n gmailInternalDate?: string | number | Date | null\n internal_date?: string | number | Date | null\n rfc822Date?: string | number | Date | null\n dateHeader?: string | number | Date | null\n /** Relative label, e.g. \"2 days ago\". */\n ago?: string\n receipt?: { kind: \"new\" | \"read\" | \"opened\" | \"sent\" | \"draft\"; label: string }\n /** HTML body (preferred). Sanitized by the component before rendering. */\n bodyHtml?: string\n /**\n * Pre-split signature HTML for this message, sanitized server-side by the\n * sender's signature pipeline (which preserves the authored Gmail look —\n * fonts, colors, logos). When present it renders inside the collapsed\n * details section (\"•••\" toggle) instead of relying on the heuristic footer\n * split of `bodyHtml`, so long signatures never expand the thread by\n * default.\n */\n signatureHtml?: string | null\n /** Plain-text fallback when `bodyHtml` is absent. */\n body?: string\n /** Quoted prior message, collapsed behind a toggle. Sanitized before rendering. */\n quoted?: { attr: string; html: string }\n}\n\nexport type ConvStatus = \"responded\" | \"awaiting\" | \"viewing\" | \"draft\"\n\n/**\n * A follow-up template offered in the in-composer \"Templates\" picker (WIT-970).\n * Presentational: the consumer fills variables for THIS thread's recipient\n * before passing it in, so `body` is composer-ready text. `tags` (e.g. \"Reply\")\n * are shown as chips so reps can recognize the right template at a glance.\n */\nexport interface ConversationReplyTemplate {\n id: string\n name: string\n /** Composer-ready reply body, already personalized by the consumer. */\n body: string\n /** Optional tags shown as chips in the picker for findability. */\n tags?: string[]\n}\n\nexport interface ConversationThread {\n threadId: string\n subject: string\n status: ConvStatus\n /** Relative label for the most recent activity. */\n lastWhen?: string\n contact: ConvParticipant\n cc?: ConvParticipant[]\n /** Set when this thread's reply halted a playbook (terminal). */\n paused?: { playbook: string } | null\n /** false => operator cannot reply or create drafts from this thread. */\n canReply?: boolean\n /** Explains why reply and draft creation are disabled. */\n replyDisabledReason?: string\n /** Existing Gmail draft or thread URL. Rendered as a new-tab link when present. */\n openInGmailUrl?: string | null\n /** Forces the Open in Gmail action into a disabled state. */\n openInGmailDisabled?: boolean\n /** Tooltip/read-only copy for a disabled Open in Gmail action. */\n openInGmailDisabledReason?: string | null\n messages: ConvMessage[]\n /** Prefilled reply draft body. */\n draft?: string\n /** Signature text appended to replies (plain text). */\n signature?: string\n /**\n * Follow-up templates offered in the composer's \"Templates\" picker, already\n * personalized for this thread's recipient (WIT-970). Omit/empty hides the\n * picker.\n */\n replyTemplates?: ConversationReplyTemplate[]\n}\n\nexport interface ConversationReplyPayload {\n threadId: string\n body: string\n includeSignature: boolean\n replyAll: boolean\n}\n\n/**\n * Result of a server-side reply preview. `htmlBody` is the exact HTML the server\n * prepared for this reply (sanitized by the component before render).\n * `confirmationToken`, when present, identifies the prepared confirmation so a\n * consumer can reuse it for the final send instead of re-preparing the message.\n */\nexport interface ConversationReplyPreview {\n htmlBody: string\n confirmationToken?: string\n}\n\nexport interface ConversationPanelProps {\n threads: ConversationThread[]\n /** Current operator: drives \"to me\" + the reply avatar. */\n me?: ConvParticipant\n /** Deployment brand, used in the paused-playbook copy. */\n tenantName?: string\n onSendReply?: (payload: ConversationReplyPayload) => void | Promise<void>\n onCreateGmailDraft?: (payload: ConversationReplyPayload) => void | Promise<void>\n /**\n * Server-side preview contract. When provided, the reply preview requests the\n * exact send HTML from the server, the component sanitizes it before render,\n * and retains any returned `confirmationToken` in preview state so a consumer\n * can reuse it for the final send. When omitted, the composer falls back to a\n * clearly labeled local draft preview only.\n */\n onPreviewReply?: (payload: ConversationReplyPayload) => Promise<ConversationReplyPreview>\n onOpenInGmail?: (threadId: string) => void\n /**\n * Fired when the rep applies a reply template from the composer picker\n * (WIT-970) — for analytics / template-usage tracking. Optional.\n */\n onApplyReplyTemplate?: (info: { threadId: string; templateId: string }) => void\n /** Inline-open this thread initially (defaults to the first responded one). */\n defaultOpenThreadId?: string\n className?: string\n}\n\n/* ── Shared helpers ──────────────────────────────────────────────────────── */\n\nfunction escapeHtml(s: string): string {\n return s.replace(/&/g, \"&\").replace(/</g, \"<\").replace(/>/g, \">\")\n}\n\n/** Plain-text -> simple paragraph HTML for the Preview / sent-message body. */\nfunction textToHtml(text: string): string {\n return decodeEmailDisplayText(text)\n .split(/\\n{2,}/)\n .map((p) => p.trim())\n .filter(Boolean)\n .map((p) => `<p>${escapeHtml(p).replace(/\\n/g, \"<br>\")}</p>`)\n .join(\"\")\n}\n\nfunction displayParticipant(person: ConvParticipant) {\n return normalizeEmailSender({ name: person.name, email: person.email, fallbackName: person.email || person.name })\n}\n\nfunction firstName(name: string): string {\n return decodeEmailDisplayText(name).split(\" \")[0] || decodeEmailDisplayText(name)\n}\n\nfunction sameEmail(a?: string | null, b?: string | null): boolean {\n return Boolean(a && b && a.trim().toLowerCase() === b.trim().toLowerCase())\n}\n\nfunction messageBodySnippet(message: Pick<ConvMessage, \"body\" | \"bodyHtml\">, maxLength = 140): string {\n return emailBodySnippet({ bodyHtml: message.bodyHtml, body: message.body }, maxLength)\n}\n\n\ntype MessageTimestampValue = string | number | Date | null | undefined\n\nfunction parseMessageTimestampValue(value: MessageTimestampValue): number | null {\n if (value == null || value === \"\") return null\n\n if (value instanceof Date) {\n const time = value.getTime()\n return Number.isNaN(time) ? null : time\n }\n\n if (typeof value === \"number\") {\n if (!Number.isFinite(value)) return null\n return value < 10_000_000_000 ? value * 1000 : value\n }\n\n const trimmed = value.trim()\n if (!trimmed) return null\n\n if (/^\\d+$/.test(trimmed)) {\n const numeric = Number(trimmed)\n if (!Number.isFinite(numeric)) return null\n return numeric < 10_000_000_000 ? numeric * 1000 : numeric\n }\n\n const parsed = Date.parse(trimmed)\n return Number.isNaN(parsed) ? null : parsed\n}\n\nfunction messageTimestamp(message: ConvMessage): number | null {\n const directional = message.direction === \"outbound\"\n ? [message.sentAt, message.sent_at]\n : [message.receivedAt, message.received_at]\n\n const candidates: MessageTimestampValue[] = [\n ...directional,\n message.rawTimestamp,\n message.timestamp,\n message.gmailInternalDate,\n message.internalDate,\n message.internal_date,\n message.rfc822Date,\n message.dateHeader,\n ]\n\n for (const candidate of candidates) {\n const parsed = parseMessageTimestampValue(candidate)\n if (parsed !== null) return parsed\n }\n\n return null\n}\n\nfunction sortMessagesChronologically(messages: ConvMessage[]): ConvMessage[] {\n return messages\n .map((message, index) => ({ message, index, timestamp: messageTimestamp(message) }))\n .sort((a, b) => {\n if (a.timestamp !== null && b.timestamp !== null && a.timestamp !== b.timestamp) {\n return a.timestamp - b.timestamp\n }\n return a.index - b.index\n })\n .map((entry) => entry.message)\n}\n\nfunction GmailMark({ size = 14 }: { size?: number }) {\n return (\n // eslint-disable-next-line @next/next/no-img-element\n <img\n src={BRAND_ICONS.gmail.icon}\n alt=\"Gmail\"\n width={size}\n height={size}\n style={{ width: size, height: size, objectFit: \"contain\", display: \"block\" }}\n />\n )\n}\n\nfunction PersonAvatar({ person, size = \"sm\" }: { person: ConvParticipant; size?: \"sm\" | \"default\" }) {\n const display = displayParticipant(person)\n\n return (\n <Avatar size={size}>\n {person.avatarUrl ? <AvatarImage src={person.avatarUrl} alt={display.name} /> : null}\n <AvatarFallback className=\"bg-muted text-muted-foreground text-[10px] font-medium uppercase\">\n {getInitials({ name: display.name, email: display.email ?? person.email })}\n </AvatarFallback>\n </Avatar>\n )\n}\n\nconst STATUS_PILL: Record<ConvStatus, { label: string; cls: string }> = {\n responded: { label: \"NEW REPLY\", cls: \"bg-status-warning-bg text-status-warning-fg border-status-warning-border\" },\n draft: { label: \"Draft\", cls: \"bg-background text-foreground/80 border-border\" },\n awaiting: { label: \"SENT\", cls: \"bg-status-info-bg text-status-info-fg border-status-info-border\" },\n viewing: { label: \"Viewing\", cls: \"bg-muted text-muted-foreground border-border\" },\n}\n\nconst STATUS_DOT: Record<ConvStatus, string> = {\n responded: \"bg-status-warning-fg\",\n draft: \"bg-status-pending-fg\",\n awaiting: \"bg-status-info-fg\",\n viewing: \"bg-muted-foreground/50\",\n}\n\nconst THREAD_ROW_ACCENT: Record<ConvStatus, string> = {\n responded: \"border-l-4 border-l-status-warning-border bg-status-warning-bg/25\",\n awaiting: \"border-l-4 border-l-status-info-border bg-status-info-bg/25\",\n draft: \"border-l-4 border-l-status-pending-border bg-status-pending-bg/20\",\n viewing: \"\",\n}\n\nconst RECEIPT_CHIP: Record<NonNullable<ConvMessage[\"receipt\"]>[\"kind\"], string> = {\n new: \"border-status-warning-border bg-status-warning-bg text-status-warning-fg\",\n read: \"border-status-info-border bg-status-info-bg text-status-info-fg\",\n opened: \"border-status-info-border bg-status-info-bg text-status-info-fg\",\n sent: \"border-status-info-border bg-status-info-bg text-status-info-fg\",\n draft: \"border-status-pending-border bg-status-pending-bg text-status-pending-fg\",\n}\n\nfunction effectiveStatus(t: ConversationThread): ConvStatus {\n return t.canReply === false ? \"viewing\" : t.status\n}\n\nfunction disabledOpenInGmailReason(thread: ConversationThread): string {\n return (\n thread.openInGmailDisabledReason?.trim() ||\n thread.replyDisabledReason?.trim() ||\n \"Gmail access is not available for this thread.\"\n )\n}\n\nfunction canOpenInGmail(thread: ConversationThread, onOpenInGmail?: (threadId: string) => void): boolean {\n return thread.openInGmailDisabled !== true && Boolean(thread.openInGmailUrl || onOpenInGmail)\n}\n\nfunction OpenInGmailButton({\n thread,\n onOpenInGmail,\n label = \"Open in Gmail\",\n}: {\n thread: ConversationThread\n onOpenInGmail?: (threadId: string) => void\n label?: string\n}) {\n const hasConfiguredAction = Boolean(thread.openInGmailUrl || onOpenInGmail || thread.openInGmailDisabled || thread.openInGmailDisabledReason)\n if (!hasConfiguredAction) return null\n\n const disabled = !canOpenInGmail(thread, onOpenInGmail)\n const disabledReason = disabled\n ? disabledOpenInGmailReason(thread)\n : undefined\n\n if (!disabled && thread.openInGmailUrl) {\n return (\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" asChild>\n <a href={thread.openInGmailUrl} target=\"_blank\" rel=\"noopener noreferrer\">\n <GmailMark size={14} /> {label}\n </a>\n </Button>\n )\n }\n\n return (\n <span className=\"inline-flex\" title={disabledReason}>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n disabled={disabled}\n aria-disabled={disabled || undefined}\n aria-label={disabledReason ? `${label}: ${disabledReason}` : label}\n onClick={disabled ? undefined : () => onOpenInGmail?.(thread.threadId)}\n >\n <GmailMark size={14} /> {label}\n </Button>\n </span>\n )\n}\n\n/* ── One message (collapsible) ──────────────────────────────────────────── */\n\nfunction MessageView({\n message,\n expanded,\n onToggle,\n me,\n}: {\n message: ConvMessage\n expanded: boolean\n onToggle: () => void\n me?: ConvParticipant\n}) {\n const [quoteOpen, setQuoteOpen] = React.useState(false)\n const fromDisplay = displayParticipant(message.from)\n const toDisplay = displayParticipant(message.to)\n\n if (!expanded) {\n const snippet = messageBodySnippet(message, 140)\n\n return (\n <button\n type=\"button\"\n data-slot=\"conv-message-collapsed\"\n onClick={onToggle}\n className=\"flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left hover:bg-muted/40\"\n >\n <PersonAvatar person={message.from} />\n <span className=\"text-muted-foreground min-w-0 flex-1 truncate text-[13px]\">\n <b className=\"text-foreground\">{firstName(fromDisplay.name)}</b> · {snippet}\n </span>\n <span className=\"text-muted-foreground/60 shrink-0 text-xs\">{message.ago ?? message.date}</span>\n <ChevronDown size={13} className=\"text-muted-foreground shrink-0\" />\n </button>\n )\n }\n\n const meDisplay = me ? displayParticipant(me) : null\n const toLabel = meDisplay && sameEmail(toDisplay.email, meDisplay.email) ? \"me\" : firstName(toDisplay.name)\n\n return (\n <div data-slot=\"conv-message\" className=\"rounded-md border border-border bg-background\">\n <button\n type=\"button\"\n onClick={onToggle}\n className=\"flex w-full items-start gap-2 px-3 py-2 text-left\"\n >\n <PersonAvatar person={message.from} size=\"default\" />\n <span className=\"min-w-0 flex-1\">\n <span className=\"flex flex-wrap items-baseline gap-x-1.5\">\n <span className=\"text-[13px] font-semibold\">{fromDisplay.name}</span>\n {fromDisplay.email ? (\n <span className=\"text-muted-foreground/60 truncate text-xs\"><{fromDisplay.email}></span>\n ) : null}\n </span>\n <span className=\"text-muted-foreground block text-xs\">\n to <b>{toLabel}</b>\n </span>\n </span>\n <span className=\"flex shrink-0 items-center gap-2\">\n {message.receipt ? (\n <span className={cn(\"inline-flex items-center gap-1 rounded-md border px-1.5 py-px text-[10px] font-semibold leading-4\", RECEIPT_CHIP[message.receipt.kind])}>\n {message.receipt.kind === \"new\" ? (\n <CornerUpLeft size={11} />\n ) : message.receipt.kind === \"read\" || message.receipt.kind === \"sent\" ? (\n <CheckCheck size={11} />\n ) : message.receipt.kind === \"draft\" ? (\n <FilePenLine size={11} />\n ) : (\n <MailOpen size={11} />\n )}\n {message.receipt.label}\n </span>\n ) : null}\n <span className=\"text-muted-foreground/60 text-xs\">{message.date}</span>\n <ChevronUp size={13} className=\"text-muted-foreground\" />\n </span>\n </button>\n\n <div className=\"px-3 pb-2.5\">\n <div data-slot=\"conv-message-body\">\n <EmailBody\n html={message.bodyHtml}\n text={message.body}\n detailsHtml={message.signatureHtml ?? undefined}\n variant=\"history\"\n collapseDetails={true}\n className=\"text-sm\"\n />\n </div>\n\n {message.quoted ? (\n <div className=\"mt-2\">\n <button\n type=\"button\"\n onClick={() => setQuoteOpen((v) => !v)}\n className=\"text-muted-foreground hover:bg-muted rounded px-1.5 text-xs leading-5\"\n title={quoteOpen ? \"Hide quoted text\" : \"Show quoted text\"}\n >\n •••\n </button>\n {quoteOpen ? (\n <div className=\"border-border text-muted-foreground mt-1 border-l-2 pl-3 text-[13px]\">\n <p className=\"mb-1\">{decodeEmailDisplayText(message.quoted.attr)}</p>\n <div data-slot=\"conv-quoted-body\">\n <EmailBody html={message.quoted.html} variant=\"history\" collapseDetails={false} />\n </div>\n </div>\n ) : null}\n </div>\n ) : null}\n </div>\n </div>\n )\n}\n\n/* ── Reply composer ─────────────────────────────────────────────────────── */\n\ntype PreviewState = {\n loading: boolean\n /** Sanitized HTML ready to render. */\n html: string | null\n /** Confirmation token returned by the server preview, retained for reuse on send. */\n confirmationToken: string | null\n error: string | null\n /** True when `html` came from the local fallback rather than the server. */\n local: boolean\n}\n\nconst IDLE_PREVIEW: PreviewState = { loading: false, html: null, confirmationToken: null, error: null, local: false }\n\n/**\n * Matches an author-declared manual-fill placeholder ({{personalize}},\n * {{personalization_recent_news}}, …). These have no resolver and must be\n * replaced by the rep before sending; the composer blocks send while any remain.\n * Non-global so repeated `.test()` calls during render are stateless.\n */\nconst MANUAL_FILL_TOKEN_RE = /\\{\\{\\s*personaliz(?:e|ation)(?:_[a-z0-9]+)*\\s*\\}\\}/i\n\nfunction ReplyComposer({\n thread,\n me,\n replyAll,\n tenantName,\n onClose,\n onSend,\n onDraft,\n onPreviewReply,\n onApplyTemplate,\n draftDisabledReason,\n}: {\n thread: ConversationThread\n me?: ConvParticipant\n replyAll: boolean\n tenantName?: string\n onClose: () => void\n onSend: (body: string, includeSignature: boolean) => void | Promise<void>\n onDraft: (body: string, includeSignature: boolean) => void | Promise<void>\n onPreviewReply?: (payload: ConversationReplyPayload) => Promise<ConversationReplyPreview>\n onApplyTemplate?: (templateId: string) => void\n draftDisabledReason?: string | null\n}) {\n const [body, setBody] = React.useState(thread.draft ?? \"\")\n const [appliedTemplate, setAppliedTemplate] = React.useState<string | null>(null)\n const [sig, setSig] = React.useState(true)\n const replyTemplates = thread.replyTemplates ?? []\n // Manual-fill placeholders ({{personalize…}}) are author-required: the rep\n // must replace them before sending. While any remain in the body we block\n // send/preview so the personalization is never sent as a literal token.\n const hasManualFillToken = MANUAL_FILL_TOKEN_RE.test(body)\n\n const applyTemplate = (template: ConversationReplyTemplate) => {\n setBody(template.body)\n setAppliedTemplate(template.name)\n onApplyTemplate?.(template.id)\n }\n const [preview, setPreview] = React.useState(false)\n const [previewState, setPreviewState] = React.useState<PreviewState>(IDLE_PREVIEW)\n const [sending, setSending] = React.useState(false)\n const [sendError, setSendError] = React.useState<string | null>(null)\n const ccList = replyAll ? thread.cc ?? [] : []\n const subject = /^re:/i.test(thread.subject) ? thread.subject : `Re: ${thread.subject}`\n const draftDisabled = Boolean(draftDisabledReason)\n\n const localPreviewHtml = textToHtml(body) + (sig && thread.signature ? textToHtml(thread.signature) : \"\")\n\n const openPreview = async () => {\n setPreview(true)\n setSendError(null)\n\n if (!onPreviewReply) {\n // No server preview contract: render a sanitized local draft preview only.\n setPreviewState({ loading: false, html: localPreviewHtml, confirmationToken: null, error: null, local: true })\n return\n }\n\n setPreviewState({ loading: true, html: null, confirmationToken: null, error: null, local: false })\n try {\n const result = await onPreviewReply({ threadId: thread.threadId, body, includeSignature: sig, replyAll })\n setPreviewState({\n loading: false,\n html: result.htmlBody ?? \"\",\n confirmationToken: result.confirmationToken ?? null,\n error: null,\n local: false,\n })\n } catch (error) {\n setPreviewState({\n loading: false,\n html: null,\n confirmationToken: null,\n error: error instanceof Error ? error.message : \"Could not load the preview. Please try again.\",\n local: false,\n })\n }\n }\n\n const handleSend = async () => {\n setSending(true)\n setSendError(null)\n try {\n await onSend(body, sig)\n setPreview(false)\n setPreviewState(IDLE_PREVIEW)\n } catch (error) {\n setSendError(error instanceof Error ? error.message : \"Could not send this reply. Please try again.\")\n } finally {\n setSending(false)\n }\n }\n\n const handleDraft = async () => {\n if (draftDisabled) return\n setSending(true)\n setSendError(null)\n try {\n await onDraft(body, sig)\n setPreview(false)\n setPreviewState(IDLE_PREVIEW)\n } catch (error) {\n setSendError(error instanceof Error ? error.message : \"Could not create the Gmail draft. Please try again.\")\n } finally {\n setSending(false)\n }\n }\n\n return (\n <div data-slot=\"conv-reply\" className=\"border-border bg-muted/20 rounded-md border p-3\">\n <div className=\"mb-2 flex items-center gap-2\">\n {me ? <PersonAvatar person={me} /> : null}\n <span className=\"flex-1 text-[13px] font-medium\">\n {replyAll ? (\n <>\n Reply all{\" \"}\n <span className=\"text-muted-foreground font-normal\">· {1 + ccList.length} recipients</span>\n </>\n ) : (\n <>\n Reply to <b>{firstName(displayParticipant(thread.contact).name)}</b>\n </>\n )}\n </span>\n <span className=\"text-muted-foreground inline-flex items-center gap-1 text-[11px]\">\n <GitMerge size={11} /> Same thread\n </span>\n <button type=\"button\" onClick={onClose} title=\"Discard reply\" className=\"text-muted-foreground hover:text-foreground\">\n <X size={15} />\n </button>\n </div>\n\n <div className=\"border-border mb-2 space-y-1 border-b pb-2 text-[13px]\">\n <div className=\"flex items-center gap-1.5\">\n <span className=\"text-muted-foreground w-12 shrink-0 text-[11px] font-medium\">To</span>\n <span className=\"font-medium\">{displayParticipant(thread.contact).name}</span>\n <span className=\"text-muted-foreground/60 truncate text-xs\">{displayParticipant(thread.contact).email ?? thread.contact.email}</span>\n </div>\n {replyAll && ccList.length ? (\n <div className=\"flex items-start gap-1.5\">\n <span className=\"text-muted-foreground w-12 shrink-0 text-[11px] font-medium\">Cc</span>\n <span className=\"text-muted-foreground text-xs\">\n {formatAddressList(ccList.map((c) => `${displayParticipant(c).name} <${displayParticipant(c).email ?? c.email}>`))}\n </span>\n </div>\n ) : null}\n <div className=\"flex items-center gap-1.5\">\n <span className=\"text-muted-foreground w-12 shrink-0 text-[11px] font-medium\">Subject</span>\n <span className=\"truncate\">{subject}</span>\n <span className=\"text-muted-foreground/60 ml-auto inline-flex items-center gap-1 text-[11px]\">\n <Lock size={10} /> on thread\n </span>\n </div>\n </div>\n\n <Textarea\n value={body}\n onChange={(e) => setBody(e.target.value)}\n placeholder=\"Write your reply…\"\n className=\"min-h-28 resize-y text-sm\"\n onKeyDown={(e) => {\n if ((e.metaKey || e.ctrlKey) && e.key === \"Enter\") {\n e.preventDefault()\n if (!hasManualFillToken) void openPreview()\n }\n }}\n />\n\n {sig && thread.signature ? (\n <div className=\"text-muted-foreground mt-2 whitespace-pre-line border-l-2 border-border pl-3 text-[13px]\">\n <span className=\"text-muted-foreground/60 mr-1\">--</span>\n {thread.signature}\n </div>\n ) : null}\n\n {replyTemplates.length > 0 ? (\n <div className=\"mt-2 flex flex-wrap items-center gap-2\">\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button type=\"button\" variant=\"outline\" size=\"sm\" disabled={sending} data-slot=\"conv-reply-template-trigger\">\n <FileText size={14} /> Templates\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"start\" className=\"max-w-xs\">\n <DropdownMenuLabel>Templates</DropdownMenuLabel>\n <DropdownMenuSeparator />\n {replyTemplates.map((template) => (\n <DropdownMenuItem\n key={template.id}\n onSelect={() => applyTemplate(template)}\n className=\"flex flex-col items-start gap-0.5\"\n >\n <span className=\"text-[13px] font-medium\">{template.name}</span>\n {template.tags && template.tags.length ? (\n <span className=\"flex flex-wrap gap-1\">\n {template.tags.map((tag) => (\n <span\n key={tag}\n className=\"border-border bg-muted text-muted-foreground rounded border px-1 py-px text-[10px] leading-4\"\n >\n {tag}\n </span>\n ))}\n </span>\n ) : null}\n </DropdownMenuItem>\n ))}\n </DropdownMenuContent>\n </DropdownMenu>\n {appliedTemplate ? (\n <span className=\"text-muted-foreground inline-flex items-center gap-1 text-[11px]\">\n <Check size={11} /> Applied “{appliedTemplate}” · edit before sending\n </span>\n ) : null}\n </div>\n ) : null}\n\n {hasManualFillToken ? (\n <p data-slot=\"conv-reply-manual-fill\" role=\"alert\" className=\"text-destructive mt-2 text-[12px]\">\n Fill in the personalize placeholder before sending. Replace each {\"{{personalize}}\"} with a real, account-specific line.\n </p>\n ) : null}\n\n <div className=\"mt-2 flex flex-wrap items-center gap-2\">\n <RichTextToolbar />\n <label className=\"text-muted-foreground ml-auto inline-flex cursor-pointer items-center gap-1.5 text-[12px]\">\n <Switch checked={sig} onCheckedChange={setSig} aria-label=\"Toggle signature\" />\n Signature\n </label>\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n disabled={sending || hasManualFillToken}\n onClick={() => void openPreview()}\n >\n <Eye size={14} /> Preview\n </Button>\n <Button\n type=\"button\"\n size=\"sm\"\n disabled={sending || hasManualFillToken}\n onClick={() => void openPreview()}\n >\n <Send size={14} /> Send\n </Button>\n </div>\n\n <Dialog open={preview} onOpenChange={(open) => { if (!sending) { setPreview(open); if (!open) setPreviewState(IDLE_PREVIEW) } }}>\n <DialogContent className=\"max-w-xl\">\n <DialogHeader>\n <DialogTitle className=\"flex items-center gap-1.5 text-[15px]\">\n <Eye size={15} /> {previewState.local ? \"Local draft preview\" : \"Reply preview\"}\n </DialogTitle>\n <DialogDescription>\n {previewState.local\n ? \"Local draft preview only. The server prepares the exact message on send.\"\n : <>Stays on the {subject.replace(/^Re:\\s*/i, \"\")} thread. Gmail keeps it threaded.</>}\n </DialogDescription>\n </DialogHeader>\n <div className=\"border-border space-y-1 rounded-md border p-3 text-[13px]\">\n <div>\n <span className=\"text-muted-foreground\">To </span>\n <b>{displayParticipant(thread.contact).name}</b>{\" \"}\n <span className=\"text-muted-foreground/60\"><{displayParticipant(thread.contact).email ?? thread.contact.email}></span>\n </div>\n {replyAll && ccList.length ? (\n <div className=\"text-muted-foreground\">Cc {formatAddressList(ccList.map((c) => `${displayParticipant(c).name} <${displayParticipant(c).email ?? c.email}>`))}</div>\n ) : null}\n <div>\n <span className=\"text-muted-foreground\">Subject </span>\n {subject}\n </div>\n </div>\n {previewState.loading ? (\n <div data-slot=\"conv-preview-loading\" role=\"status\" className=\"text-muted-foreground flex items-center gap-2 px-1 py-6 text-[13px]\">\n <span className=\"border-muted-foreground/40 border-t-foreground size-4 animate-spin rounded-full border-2\" aria-hidden />\n Loading preview…\n </div>\n ) : previewState.error ? (\n <p role=\"alert\" className=\"text-destructive text-sm\">\n {previewState.error}\n </p>\n ) : (\n <div\n data-slot=\"conv-preview-body\"\n data-confirmation-token={previewState.confirmationToken ?? undefined}\n className=\"max-h-72 overflow-auto\"\n >\n <EmailBody html={previewState.html ?? \"\"} variant=\"preview\" collapseDetails={false} defaultDetailsOpen />\n </div>\n )}\n {sendError ? (\n <p role=\"alert\" className=\"text-destructive text-sm\">\n {sendError}\n </p>\n ) : null}\n <DialogFooter className=\"sm:justify-between\">\n <span className=\"inline-flex\" title={draftDisabledReason ?? undefined}>\n <button\n type=\"button\"\n disabled={sending || previewState.loading || draftDisabled}\n onClick={handleDraft}\n aria-label={draftDisabledReason ? `Open draft in Gmail: ${draftDisabledReason}` : \"Open draft in Gmail\"}\n className=\"text-muted-foreground hover:text-foreground inline-flex items-center gap-1.5 text-[13px] disabled:pointer-events-none disabled:opacity-50\"\n >\n <GmailMark size={14} /> Open draft in Gmail\n </button>\n </span>\n <span className=\"flex items-center gap-2\">\n <Button type=\"button\" variant=\"outline\" size=\"sm\" disabled={sending} onClick={() => { setPreview(false); setPreviewState(IDLE_PREVIEW) }}>\n Keep editing\n </Button>\n <Button\n type=\"button\"\n size=\"sm\"\n disabled={sending || previewState.loading || hasManualFillToken}\n onClick={handleSend}\n >\n <Send size={14} /> {sending ? \"Sending...\" : \"Send now\"}\n </Button>\n </span>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n\n {tenantName ? (\n <p className=\"text-muted-foreground/70 mt-2 text-[11px]\">Sends via Gmail · playbooks stay stopped.</p>\n ) : null}\n </div>\n )\n}\n\n/* ── Thread body (messages + footer/composer/done states) ───────────────── */\n\ntype ThreadMode = \"idle\" | \"replying\" | \"sent\" | \"draft\"\n\nfunction ThreadBody({\n thread,\n me,\n tenantName,\n onSendReply,\n onCreateGmailDraft,\n onPreviewReply,\n onOpenInGmail,\n onApplyReplyTemplate,\n}: {\n thread: ConversationThread\n me?: ConvParticipant\n tenantName?: string\n onSendReply?: (p: ConversationReplyPayload) => void | Promise<void>\n onCreateGmailDraft?: (p: ConversationReplyPayload) => void | Promise<void>\n onPreviewReply?: (p: ConversationReplyPayload) => Promise<ConversationReplyPreview>\n onOpenInGmail?: (threadId: string) => void\n onApplyReplyTemplate?: ConversationPanelProps[\"onApplyReplyTemplate\"]\n}) {\n const canReply = thread.canReply !== false\n const replyDisabledReason = thread.replyDisabledReason?.trim() || \"You are not a participant on this thread, so replying is disabled here.\"\n const draftDisabledReason = onCreateGmailDraft ? null : \"Gmail draft creation is not available for this thread.\"\n const hasCc = !!(thread.cc && thread.cc.length)\n const sortedMessages = React.useMemo(() => sortMessagesChronologically(thread.messages), [thread.messages])\n const [mode, setMode] = React.useState<ThreadMode>(\"idle\")\n const [replyAll, setReplyAll] = React.useState(false)\n const [expanded, setExpanded] = React.useState<Record<string, boolean>>(() => {\n const o: Record<string, boolean> = {}\n sortedMessages.forEach((m, i) => {\n o[m.id] = i === sortedMessages.length - 1\n })\n return o\n })\n\n React.useEffect(() => {\n setExpanded((current) => {\n const next = { ...current }\n sortedMessages.forEach((m, i) => {\n if (next[m.id] === undefined) next[m.id] = i === sortedMessages.length - 1\n })\n return next\n })\n }, [sortedMessages])\n\n const toggle = (id: string) => setExpanded((e) => ({ ...e, [id]: !e[id] }))\n\n return (\n <div data-slot=\"conv-thread-body\" className=\"space-y-2\">\n {canReply && thread.paused ? (\n <div className=\"border-status-warning-border bg-status-warning-bg text-status-warning-fg flex items-start gap-2 rounded-md border border-l-4 p-2.5 text-[12px]\">\n <Pause size={13} className=\"mt-0.5 shrink-0\" />\n <span>\n <b>Playbook stopped.</b> Follow-up actions for {thread.paused.playbook} won’t send\n automatically while this conversation is live. Continue it in {tenantName ?? \"the app\"} or Gmail.\n </span>\n </div>\n ) : null}\n\n <div className=\"space-y-1\">\n {sortedMessages.map((m) => (\n <MessageView key={m.id} message={m} expanded={!!expanded[m.id]} onToggle={() => toggle(m.id)} me={me} />\n ))}\n </div>\n\n {!canReply ? (\n <div className=\"border-border bg-muted/30 text-muted-foreground flex flex-wrap items-start gap-2 rounded-md border p-2.5 text-[12px]\">\n <Eye size={14} className=\"mt-0.5 shrink-0\" />\n <span className=\"min-w-0 flex-1\">\n <b>Viewing only.</b> {replyDisabledReason}\n </span>\n <OpenInGmailButton thread={thread} onOpenInGmail={onOpenInGmail} />\n </div>\n ) : null}\n\n {canReply && mode === \"idle\" ? (\n <div data-slot=\"conv-action-row\" className=\"border-border/70 mt-1 flex flex-wrap items-center gap-x-3 gap-y-2 border-t pt-3\">\n <Button type=\"button\" size=\"sm\" onClick={() => { setReplyAll(false); setMode(\"replying\") }}>\n <Reply size={15} /> Reply\n </Button>\n {hasCc ? (\n <Button type=\"button\" variant=\"outline\" size=\"sm\" onClick={() => { setReplyAll(true); setMode(\"replying\") }}>\n <ReplyAll size={14} /> Reply all\n </Button>\n ) : null}\n <OpenInGmailButton thread={thread} onOpenInGmail={onOpenInGmail} />\n <span className=\"text-muted-foreground/70 ml-auto inline-flex items-center gap-1.5 text-[12px]\">\n <GitMerge size={13} /> Stays on this thread\n </span>\n </div>\n ) : null}\n\n {canReply && mode === \"replying\" ? (\n <ReplyComposer\n thread={thread}\n me={me}\n replyAll={replyAll}\n tenantName={tenantName}\n onPreviewReply={onPreviewReply}\n onApplyTemplate={(templateId) => onApplyReplyTemplate?.({ threadId: thread.threadId, templateId })}\n onClose={() => setMode(\"idle\")}\n onSend={async (body, includeSignature) => {\n await onSendReply?.({ threadId: thread.threadId, body, includeSignature, replyAll })\n setMode(\"sent\")\n }}\n onDraft={async (body, includeSignature) => {\n if (!onCreateGmailDraft) return\n await onCreateGmailDraft({ threadId: thread.threadId, body, includeSignature, replyAll })\n setMode(\"draft\")\n }}\n draftDisabledReason={draftDisabledReason}\n />\n ) : null}\n\n {canReply && mode === \"sent\" ? (\n <div className=\"border-status-active-border bg-status-active-bg flex items-center gap-2 rounded-md border p-3 text-[13px]\">\n <Check size={16} className=\"text-status-active-fg shrink-0\" />\n <span className=\"flex-1\">\n <b>{replyAll ? \"Reply all sent\" : \"Reply sent\"}</b> · added to the thread. Delivered to{\" \"}\n <b>{displayParticipant(thread.contact).name}</b>. This action stays <b>Pending</b>; playbooks remain stopped.\n </span>\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" onClick={() => setMode(\"idle\")}>\n Done\n </Button>\n </div>\n ) : null}\n\n {canReply && mode === \"draft\" ? (\n <div className=\"border-border bg-muted/30 flex items-center gap-2 rounded-md border p-3 text-[13px]\">\n <GmailMark size={16} />\n <span className=\"flex-1\">\n <b>Draft saved to Gmail.</b> Waiting on the <b>Re: {thread.subject}</b> thread; open it there to finish. Nothing was sent.\n </span>\n <OpenInGmailButton thread={thread} onOpenInGmail={onOpenInGmail} />\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" onClick={() => setMode(\"idle\")}>\n Done\n </Button>\n </div>\n ) : null}\n </div>\n )\n}\n\n/* ── A thread row + its inline reader ───────────────────────────────────── */\n\nfunction ThreadRow({\n thread,\n open,\n onToggleOpen,\n me,\n tenantName,\n onSendReply,\n onCreateGmailDraft,\n onPreviewReply,\n onOpenInGmail,\n onApplyReplyTemplate,\n}: {\n thread: ConversationThread\n open: boolean\n onToggleOpen: () => void\n} & Pick<ConversationPanelProps, \"me\" | \"tenantName\" | \"onSendReply\" | \"onCreateGmailDraft\" | \"onPreviewReply\" | \"onOpenInGmail\" | \"onApplyReplyTemplate\">) {\n const status = effectiveStatus(thread)\n const sortedMessages = React.useMemo(() => sortMessagesChronologically(thread.messages), [thread.messages])\n const last = sortedMessages[sortedMessages.length - 1]\n const lastSender = last ? displayParticipant(last.from) : null\n const meDisplay = me ? displayParticipant(me) : null\n const who = last?.direction === \"outbound\" && sameEmail(lastSender?.email, meDisplay?.email) ? \"You\" : firstName(lastSender?.name ?? \"\")\n const lastSnippet = last ? messageBodySnippet(last, 120) : \"\"\n const pill = STATUS_PILL[status]\n\n return (\n <div\n data-slot=\"conv-thread\"\n data-status={status}\n data-open={open ? \"true\" : undefined}\n className={cn(\"border-border border-b last:border-b-0\", THREAD_ROW_ACCENT[status])}\n >\n <button\n type=\"button\"\n onClick={onToggleOpen}\n aria-expanded={open}\n className=\"flex w-full items-center gap-2.5 px-3 py-2.5 text-left hover:bg-muted/30\"\n >\n <span className={cn(\"size-2 shrink-0 rounded-full\", STATUS_DOT[status])} aria-hidden />\n <span className=\"min-w-0 flex-1\">\n <span className=\"flex items-center gap-2\">\n <span className=\"truncate text-sm font-semibold\">{thread.subject}</span>\n <span className={cn(\"shrink-0 rounded-md border px-1.5 py-px text-[10px] font-medium leading-4\", pill.cls)}>\n {pill.label}\n </span>\n </span>\n <span className=\"text-muted-foreground block truncate text-xs\">\n <b className=\"text-foreground/80\">{displayParticipant(thread.contact).name}</b> · {who}: {lastSnippet}\n </span>\n </span>\n <span className=\"text-muted-foreground/60 shrink-0 text-xs\">{thread.lastWhen}</span>\n {open ? (\n <ChevronUp size={15} className=\"text-muted-foreground shrink-0\" />\n ) : (\n <ChevronDown size={15} className=\"text-muted-foreground shrink-0\" />\n )}\n </button>\n\n {open ? (\n <div className=\"px-3 pb-3\">\n <ThreadBody\n thread={thread}\n me={me}\n tenantName={tenantName}\n onSendReply={onSendReply}\n onCreateGmailDraft={onCreateGmailDraft}\n onPreviewReply={onPreviewReply}\n onOpenInGmail={onOpenInGmail}\n onApplyReplyTemplate={onApplyReplyTemplate}\n />\n </div>\n ) : null}\n </div>\n )\n}\n\n/* ── The hub ─────────────────────────────────────────────────────────────── */\n\nfunction ConversationPanel({\n threads,\n me,\n tenantName,\n onSendReply,\n onCreateGmailDraft,\n onPreviewReply,\n onOpenInGmail,\n onApplyReplyTemplate,\n defaultOpenThreadId,\n className,\n}: ConversationPanelProps) {\n const responded = threads.filter((t) => t.status === \"responded\" && t.canReply !== false).length\n const draft = threads.filter((t) => effectiveStatus(t) === \"draft\").length\n const awaiting = threads.filter((t) => effectiveStatus(t) === \"awaiting\").length\n const anyPaused = threads.some((t) => t.paused)\n const prioritizedThread =\n threads.find((t) => t.status === \"responded\" && t.canReply !== false) ??\n threads.find((t) => effectiveStatus(t) === \"draft\") ??\n threads.find((t) => effectiveStatus(t) === \"awaiting\")\n const hubGmailThread =\n threads.find((t) => t.status === \"responded\" && t.canReply !== false && canOpenInGmail(t, onOpenInGmail)) ??\n threads.find((t) => effectiveStatus(t) === \"draft\" && canOpenInGmail(t, onOpenInGmail)) ??\n threads.find((t) => effectiveStatus(t) === \"awaiting\" && canOpenInGmail(t, onOpenInGmail)) ??\n threads.find((t) => canOpenInGmail(t, onOpenInGmail))\n const firstAwaiting = threads.find((t) => effectiveStatus(t) === \"awaiting\")\n\n const [hubOpen, setHubOpen] = React.useState(true)\n const [openId, setOpenId] = React.useState<string | null>(() => {\n if (defaultOpenThreadId) return defaultOpenThreadId\n return prioritizedThread ? prioritizedThread.threadId : null\n })\n\n if (!threads.length) return null\n\n // Header badge state: a responded reply leads, then drafts to finish, then sent mail awaiting a reply.\n const badge =\n responded > 0\n ? { label: \"Email response detected\", dot: \"bg-status-warning-fg\", ring: \"bg-status-warning-fg/30\" }\n : draft > 0\n ? { label: \"Draft ready\", dot: \"bg-status-pending-fg\", ring: \"bg-status-pending-fg/30\" }\n : awaiting > 0\n ? { label: \"Email sent · awaiting reply\", dot: \"bg-status-info-fg\", ring: \"bg-status-info-fg/30\" }\n : { label: \"Conversations\", dot: \"bg-muted-foreground/50\", ring: \"bg-muted-foreground/20\" }\n\n const headTitle =\n responded > 0\n ? `${responded} ${responded === 1 ? \"reply needs\" : \"replies need\"} your response`\n : draft > 0\n ? `Draft ready on ${draft} ${draft === 1 ? \"thread\" : \"threads\"}`\n : awaiting > 0\n ? awaiting === 1 && firstAwaiting\n ? `Awaiting a response from ${firstName(displayParticipant(firstAwaiting.contact).name)}`\n : `Awaiting responses on ${awaiting} threads`\n : `${threads.length} email ${threads.length === 1 ? \"thread\" : \"threads\"}`\n\n const panelState = responded > 0 ? \"responded\" : draft > 0 ? \"draft\" : awaiting > 0 ? \"awaiting\" : \"viewing\"\n\n return (\n <section\n data-slot=\"conversation-panel\"\n data-responded={responded > 0 ? \"true\" : undefined}\n data-state={panelState}\n className={cn(\n \"bg-background overflow-hidden rounded-xl border\",\n panelState === \"responded\"\n ? \"border-status-warning-border\"\n : panelState === \"awaiting\"\n ? \"border-status-info-border\"\n : \"border-border\",\n className,\n )}\n >\n <div\n data-slot=\"conversation-panel-header\"\n className={cn(\n \"flex w-full items-center gap-2 px-3 py-2.5\",\n panelState === \"responded\"\n ? \"bg-status-warning-bg/45\"\n : panelState === \"awaiting\"\n ? \"bg-status-info-bg/55\"\n : \"bg-background\",\n )}\n >\n <button\n type=\"button\"\n onClick={() => setHubOpen((v) => !v)}\n aria-expanded={hubOpen}\n className=\"flex min-w-0 flex-1 items-center gap-3 text-left\"\n >\n <span\n data-slot=\"conversation-badge\"\n className={cn(\n \"inline-flex items-center gap-1.5 rounded-full px-2 py-0.5 text-[11px] font-semibold\",\n responded > 0\n ? \"bg-status-warning-bg text-status-warning-fg\"\n : draft > 0\n ? \"bg-status-pending-bg text-status-pending-fg\"\n : awaiting > 0\n ? \"bg-status-info-bg text-status-info-fg\"\n : \"bg-muted text-muted-foreground\"\n )}\n >\n <span className=\"relative inline-flex size-2\">\n <span className={cn(\"absolute inline-flex h-full w-full animate-ping rounded-full opacity-75\", badge.ring)} />\n <span className={cn(\"relative inline-flex size-2 rounded-full\", badge.dot)} />\n </span>\n {badge.label}\n </span>\n <span className=\"min-w-0 flex-1\">\n <span className=\"block truncate text-sm font-semibold leading-tight\">{headTitle}</span>\n <span className=\"text-muted-foreground mt-0.5 block truncate text-xs\">\n {threads.length} {threads.length === 1 ? \"thread\" : \"threads\"} on this action\n {anyPaused ? <> · <b>playbook stopped</b></> : null}\n </span>\n </span>\n {hubOpen ? (\n <ChevronUp size={16} className=\"text-muted-foreground shrink-0\" />\n ) : (\n <ChevronDown size={16} className=\"text-muted-foreground shrink-0\" />\n )}\n </button>\n {hubGmailThread ? (\n <div className=\"shrink-0\" onClick={(event) => event.stopPropagation()}>\n <OpenInGmailButton thread={hubGmailThread} onOpenInGmail={onOpenInGmail} />\n </div>\n ) : null}\n </div>\n\n {hubOpen ? (\n <div className=\"border-border border-t\">\n {threads.map((t) => (\n <ThreadRow\n key={t.threadId}\n thread={t}\n open={openId === t.threadId}\n onToggleOpen={() => setOpenId((cur) => (cur === t.threadId ? null : t.threadId))}\n me={me}\n tenantName={tenantName}\n onSendReply={onSendReply}\n onCreateGmailDraft={onCreateGmailDraft}\n onPreviewReply={onPreviewReply}\n onOpenInGmail={onOpenInGmail}\n onApplyReplyTemplate={onApplyReplyTemplate}\n />\n ))}\n </div>\n ) : null}\n </section>\n )\n}\n\nexport { ConversationPanel }\n"],"mappings":"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","names":["_a","_b"]}
|
package/package.json
CHANGED
|
@@ -113,6 +113,71 @@ describe("ConversationPanel", () => {
|
|
|
113
113
|
templateId: "tmpl-pricing",
|
|
114
114
|
});
|
|
115
115
|
});
|
|
116
|
+
|
|
117
|
+
it("labels the picker trigger 'Templates'", () => {
|
|
118
|
+
const { container } = render(
|
|
119
|
+
<ConversationPanel
|
|
120
|
+
me={me}
|
|
121
|
+
threads={[
|
|
122
|
+
thread({
|
|
123
|
+
replyTemplates: [
|
|
124
|
+
{ id: "t", name: "A template", body: "Hi Priya, here you go.", tags: ["Follow up"] },
|
|
125
|
+
],
|
|
126
|
+
}),
|
|
127
|
+
]}
|
|
128
|
+
/>,
|
|
129
|
+
);
|
|
130
|
+
openReply(container);
|
|
131
|
+
expect(
|
|
132
|
+
container.querySelector('[data-slot="conv-reply-template-trigger"]')!.textContent,
|
|
133
|
+
).toContain("Templates");
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("blocks send while a {{personalize}} placeholder remains, then re-enables once filled", async () => {
|
|
137
|
+
const { container } = render(
|
|
138
|
+
<ConversationPanel
|
|
139
|
+
me={me}
|
|
140
|
+
threads={[
|
|
141
|
+
thread({
|
|
142
|
+
replyTemplates: [
|
|
143
|
+
{
|
|
144
|
+
id: "tmpl-personalize",
|
|
145
|
+
name: "Personalize me",
|
|
146
|
+
body: "Hi Priya, {{personalize}}",
|
|
147
|
+
tags: ["Follow up"],
|
|
148
|
+
},
|
|
149
|
+
],
|
|
150
|
+
}),
|
|
151
|
+
]}
|
|
152
|
+
/>,
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
const textarea = openReply(container);
|
|
156
|
+
fireEvent.pointerDown(
|
|
157
|
+
container.querySelector('[data-slot="conv-reply-template-trigger"]')!,
|
|
158
|
+
{ button: 0, ctrlKey: false },
|
|
159
|
+
);
|
|
160
|
+
expect(await screen.findByRole("menu")).toBeDefined();
|
|
161
|
+
fireEvent.click(screen.getByText("Personalize me"));
|
|
162
|
+
|
|
163
|
+
await waitFor(() => expect(textarea.value).toContain("{{personalize}}"));
|
|
164
|
+
|
|
165
|
+
// The manual-fill alert shows and Send/Preview are blocked.
|
|
166
|
+
expect(container.querySelector('[data-slot="conv-reply-manual-fill"]')).not.toBeNull();
|
|
167
|
+
const sendButton = screen.getByRole("button", { name: "Send" }) as HTMLButtonElement;
|
|
168
|
+
expect(sendButton.disabled).toBe(true);
|
|
169
|
+
|
|
170
|
+
// Replace the placeholder: the alert clears and Send re-enables.
|
|
171
|
+
fireEvent.change(textarea, {
|
|
172
|
+
target: { value: "Hi Priya, here is your tailored Q2 update." },
|
|
173
|
+
});
|
|
174
|
+
await waitFor(() =>
|
|
175
|
+
expect(container.querySelector('[data-slot="conv-reply-manual-fill"]')).toBeNull(),
|
|
176
|
+
);
|
|
177
|
+
expect((screen.getByRole("button", { name: "Send" }) as HTMLButtonElement).disabled).toBe(
|
|
178
|
+
false,
|
|
179
|
+
);
|
|
180
|
+
});
|
|
116
181
|
});
|
|
117
182
|
|
|
118
183
|
it("shows the amber response-detected treatment when a thread has responded", () => {
|
|
@@ -126,7 +126,7 @@ export interface ConvMessage {
|
|
|
126
126
|
export type ConvStatus = "responded" | "awaiting" | "viewing" | "draft"
|
|
127
127
|
|
|
128
128
|
/**
|
|
129
|
-
* A
|
|
129
|
+
* A follow-up template offered in the in-composer "Templates" picker (WIT-970).
|
|
130
130
|
* Presentational: the consumer fills variables for THIS thread's recipient
|
|
131
131
|
* before passing it in, so `body` is composer-ready text. `tags` (e.g. "Reply")
|
|
132
132
|
* are shown as chips so reps can recognize the right template at a glance.
|
|
@@ -166,7 +166,7 @@ export interface ConversationThread {
|
|
|
166
166
|
/** Signature text appended to replies (plain text). */
|
|
167
167
|
signature?: string
|
|
168
168
|
/**
|
|
169
|
-
*
|
|
169
|
+
* Follow-up templates offered in the composer's "Templates" picker, already
|
|
170
170
|
* personalized for this thread's recipient (WIT-970). Omit/empty hides the
|
|
171
171
|
* picker.
|
|
172
172
|
*/
|
|
@@ -561,6 +561,14 @@ type PreviewState = {
|
|
|
561
561
|
|
|
562
562
|
const IDLE_PREVIEW: PreviewState = { loading: false, html: null, confirmationToken: null, error: null, local: false }
|
|
563
563
|
|
|
564
|
+
/**
|
|
565
|
+
* Matches an author-declared manual-fill placeholder ({{personalize}},
|
|
566
|
+
* {{personalization_recent_news}}, …). These have no resolver and must be
|
|
567
|
+
* replaced by the rep before sending; the composer blocks send while any remain.
|
|
568
|
+
* Non-global so repeated `.test()` calls during render are stateless.
|
|
569
|
+
*/
|
|
570
|
+
const MANUAL_FILL_TOKEN_RE = /\{\{\s*personaliz(?:e|ation)(?:_[a-z0-9]+)*\s*\}\}/i
|
|
571
|
+
|
|
564
572
|
function ReplyComposer({
|
|
565
573
|
thread,
|
|
566
574
|
me,
|
|
@@ -588,6 +596,10 @@ function ReplyComposer({
|
|
|
588
596
|
const [appliedTemplate, setAppliedTemplate] = React.useState<string | null>(null)
|
|
589
597
|
const [sig, setSig] = React.useState(true)
|
|
590
598
|
const replyTemplates = thread.replyTemplates ?? []
|
|
599
|
+
// Manual-fill placeholders ({{personalize…}}) are author-required: the rep
|
|
600
|
+
// must replace them before sending. While any remain in the body we block
|
|
601
|
+
// send/preview so the personalization is never sent as a literal token.
|
|
602
|
+
const hasManualFillToken = MANUAL_FILL_TOKEN_RE.test(body)
|
|
591
603
|
|
|
592
604
|
const applyTemplate = (template: ConversationReplyTemplate) => {
|
|
593
605
|
setBody(template.body)
|
|
@@ -719,7 +731,7 @@ function ReplyComposer({
|
|
|
719
731
|
onKeyDown={(e) => {
|
|
720
732
|
if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
|
|
721
733
|
e.preventDefault()
|
|
722
|
-
void openPreview()
|
|
734
|
+
if (!hasManualFillToken) void openPreview()
|
|
723
735
|
}
|
|
724
736
|
}}
|
|
725
737
|
/>
|
|
@@ -736,11 +748,11 @@ function ReplyComposer({
|
|
|
736
748
|
<DropdownMenu>
|
|
737
749
|
<DropdownMenuTrigger asChild>
|
|
738
750
|
<Button type="button" variant="outline" size="sm" disabled={sending} data-slot="conv-reply-template-trigger">
|
|
739
|
-
<FileText size={14} />
|
|
751
|
+
<FileText size={14} /> Templates
|
|
740
752
|
</Button>
|
|
741
753
|
</DropdownMenuTrigger>
|
|
742
754
|
<DropdownMenuContent align="start" className="max-w-xs">
|
|
743
|
-
<DropdownMenuLabel>
|
|
755
|
+
<DropdownMenuLabel>Templates</DropdownMenuLabel>
|
|
744
756
|
<DropdownMenuSeparator />
|
|
745
757
|
{replyTemplates.map((template) => (
|
|
746
758
|
<DropdownMenuItem
|
|
@@ -773,16 +785,33 @@ function ReplyComposer({
|
|
|
773
785
|
</div>
|
|
774
786
|
) : null}
|
|
775
787
|
|
|
788
|
+
{hasManualFillToken ? (
|
|
789
|
+
<p data-slot="conv-reply-manual-fill" role="alert" className="text-destructive mt-2 text-[12px]">
|
|
790
|
+
Fill in the personalize placeholder before sending. Replace each {"{{personalize}}"} with a real, account-specific line.
|
|
791
|
+
</p>
|
|
792
|
+
) : null}
|
|
793
|
+
|
|
776
794
|
<div className="mt-2 flex flex-wrap items-center gap-2">
|
|
777
795
|
<RichTextToolbar />
|
|
778
796
|
<label className="text-muted-foreground ml-auto inline-flex cursor-pointer items-center gap-1.5 text-[12px]">
|
|
779
797
|
<Switch checked={sig} onCheckedChange={setSig} aria-label="Toggle signature" />
|
|
780
798
|
Signature
|
|
781
799
|
</label>
|
|
782
|
-
<Button
|
|
800
|
+
<Button
|
|
801
|
+
type="button"
|
|
802
|
+
variant="outline"
|
|
803
|
+
size="sm"
|
|
804
|
+
disabled={sending || hasManualFillToken}
|
|
805
|
+
onClick={() => void openPreview()}
|
|
806
|
+
>
|
|
783
807
|
<Eye size={14} /> Preview
|
|
784
808
|
</Button>
|
|
785
|
-
<Button
|
|
809
|
+
<Button
|
|
810
|
+
type="button"
|
|
811
|
+
size="sm"
|
|
812
|
+
disabled={sending || hasManualFillToken}
|
|
813
|
+
onClick={() => void openPreview()}
|
|
814
|
+
>
|
|
786
815
|
<Send size={14} /> Send
|
|
787
816
|
</Button>
|
|
788
817
|
</div>
|
|
@@ -795,7 +824,7 @@ function ReplyComposer({
|
|
|
795
824
|
</DialogTitle>
|
|
796
825
|
<DialogDescription>
|
|
797
826
|
{previewState.local
|
|
798
|
-
? "Local draft preview only
|
|
827
|
+
? "Local draft preview only. The server prepares the exact message on send."
|
|
799
828
|
: <>Stays on the {subject.replace(/^Re:\s*/i, "")} thread. Gmail keeps it threaded.</>}
|
|
800
829
|
</DialogDescription>
|
|
801
830
|
</DialogHeader>
|
|
@@ -855,7 +884,7 @@ function ReplyComposer({
|
|
|
855
884
|
<Button
|
|
856
885
|
type="button"
|
|
857
886
|
size="sm"
|
|
858
|
-
disabled={sending || previewState.loading}
|
|
887
|
+
disabled={sending || previewState.loading || hasManualFillToken}
|
|
859
888
|
onClick={handleSend}
|
|
860
889
|
>
|
|
861
890
|
<Send size={14} /> {sending ? "Sending..." : "Send now"}
|