import { EmailCard, emailBrandFromContext, resolveEmailBrand, type EmailBrand, } from "@dbx-tools/shared-email-template"; import { Button } from "@dbx-tools/ui-appkit/react"; import { useBrand } from "@dbx-tools/ui-branding/react"; import { CheckIcon, MailIcon, PaperclipIcon, XIcon } from "lucide-react"; import { attachmentNames, joinAddresses, type EmailDraft } from "./fields.ts"; // Presentational pieces for an outbound email awaiting a human Approve / // Deny: a mail-client chrome (sender, recipients, subject, attachments) // around the shared React Email card, plus an approval card wrapping it. // State and the resolve transport belong to the caller; these components // only render and report intent. The editable counterpart is // `EmailComposeView` in `./email-compose`; both share `./fields` and // `./email-body`. export type { EmailDraft } from "./fields.ts"; /** Props for {@link EmailPreview}. */ export interface EmailPreviewProps { email: EmailDraft; /** Optional email-only brand. Defaults to the active `BrandProvider` context. */ brand?: EmailBrand; } /** Initials for the sender avatar when no logo is available. */ function senderInitials(name: string): string { const parts = name.trim().split(/\s+/).filter(Boolean); if (parts.length === 0) return "?"; if (parts.length === 1) return parts[0]!.slice(0, 2).toUpperCase(); return `${parts[0]![0] ?? ""}${parts[1]![0] ?? ""}`.toUpperCase(); } /** One muted "Label: value" row in the client chrome. */ const ChromeRow = ({ label, value }: { label: string; value: string }) => (

{label} {value}

); /** * Render an email draft the way a mail client shows a received message: sender * identity, recipients, subject, and attachment chips in chrome around the * same branded React Email card used for delivery. Envelope fields stay out of * the delivered HTML — they are transport metadata, not body content. */ export const EmailPreview = ({ email, brand }: EmailPreviewProps) => { const { context } = useBrand(); const theme = resolveEmailBrand(brand ?? emailBrandFromContext(context)); const to = joinAddresses(email.to); const cc = joinAddresses(email.cc); const bcc = joinAddresses(email.bcc); const subject = email.subject?.trim() || "Message"; const files = email.attachments?.map((att) => att.filename).filter(Boolean) ?? []; const attachmentSummary = attachmentNames(email.attachments); return (
{senderInitials(theme.name)}

{theme.name}

{theme.website ? (

{theme.website}

) : null}
{to ? : null} {cc ? : null} {bcc ? : null}

{subject}

{files.length > 0 ? ( ) : null}
); }; /** Props for {@link EmailApprovalCard}. */ export interface EmailApprovalCardProps { email: EmailDraft; /** Called when the user approves the send. */ onApprove?: () => void | Promise; /** Called when the user denies the send. */ onDeny?: () => void | Promise; /** Disable both actions while a decision is in flight. */ pending?: boolean; /** Disable both actions regardless of pending state. */ disabled?: boolean; /** Header label. Defaults to "Approval needed: send email". */ title?: string; } /** * A drop-in approval card for the `send_email` tool: the email preview * plus Approve / Deny actions. Wire `onApprove` / `onDeny` to whatever * resumes the suspended tool call (e.g. the AI-SDK `addToolResult`). */ export const EmailApprovalCard = ({ email, onApprove, onDeny, pending, disabled, title = "Approval needed: send email", }: EmailApprovalCardProps) => { const blocked = Boolean(disabled) || Boolean(pending); return (
{title}
); };