// Copyright (c) Meta Platforms, Inc. and affiliates. 'use client'; /** * @file ChatMessageBubble.tsx * @input Uses React, StyleX, ChatMessageContext, theme tokens * @output Exports ChatMessageBubble component and ChatMessageBubbleProps * @position Styled content container — the actual "chat bubble" with sender-aware styling * * Reads sender from parent ChatMessage context to auto-style background. * Optional — not all message content needs bubble treatment. * * Usage guidance: * - If you use bubbles on one side (e.g. assistant), use them consistently * for all messages on that side. Use `ghost` variant for content that * needs alignment without a visual boundary. * - For custom content (cards, attachments, citations) that should span * the full message column instead of the default width cap, combine * `ghost` with `width="100%"` (#2574). * - Put `name` on the first bubble in a message, `metadata` on the last. * - For unbubbled messages, use ChatMessage's `name` and `metadata` * props instead. * * SYNC: When modified, update these files to stay in sync: * - /packages/core/src/Chat/index.ts (exports) * - /apps/storybook/stories/Chat.stories.tsx * - /packages/cli/assets/templates/blocks/components/ChatMessageBubble/ (block examples) */ import type { ReactNode } from 'react'; import * as stylex from "../style-runtime.js"; import "../theme/tokens.stylex.js"; import { colorVars, spacingVars, radiusVars, typeScaleVars, typographyVars } from "../theme/tokens.stylex.js"; import { useChatMessageContext } from "./ChatContext.js"; import { mergeProps } from "../utils/index.js"; import type { SizeValue } from "../utils/types.js"; import type { BaseProps } from "../BaseProps.js"; import { themeProps } from "../utils/themeProps.js"; export type ChatMessageBubbleVariant = 'filled' | 'ghost'; export interface ChatMessageBubbleProps extends BaseProps { /** Ref forwarded to the root element */ ref?: React.Ref; /** * Bubble content — text, Markdown, or any ReactNode. */ children: ReactNode; /** * Visual variant. * - 'filled': background color based on sender (default) * - 'ghost': no background, but keeps padding for consistent alignment * @default 'filled' */ variant?: ChatMessageBubbleVariant; /** * Sender name rendered above the bubble, aligned with bubble text padding. * Use when the first content in a message is a bubble. * If the first content is raw (no bubble), use ChatMessage's `name` * prop instead. */ name?: ReactNode; /** * Metadata content rendered below the bubble, aligned with bubble text padding. * Use when the last content in a message is a bubble. * If the last content is raw (no bubble), use ChatMessage's `metadata` * prop instead. */ metadata?: ReactNode; /** * Position within a multi-bubble group. * Controls corner radius reduction on the sender side. * - 'first': bottom sender-side corner tightened * - 'middle': both sender-side corners tightened * - 'last': top sender-side corner tightened * Leave unset for standalone bubbles (full radius). */ group?: 'first' | 'middle' | 'last'; /** * Width of the bubble. * Numbers are treated as pixels, strings are used as-is (e.g. "100%"). * When set, replaces the default `max(80%, 280px)` width cap; leave unset * to keep the cap. Combine with `variant="ghost"` to let custom content * (an artifact card, attachments) span the full message column. */ width?: SizeValue; } // ============================================================================= // Styles // ============================================================================= const styles = { content: { k1xSpc: "x78zum5", kXwgrk: "xdt5ytf", ks0D6T: "xl6temz", kaIpWk: "xashgol", kMv6JI: "x9ynric", kGuDYH: "xjm74w1", kLWn49: "xw6l6zx", kHjlTd: "x1mzt3pk", kTgw9: "x13faqbe", $$css: true as const }, radiusCompact: { kaIpWk: "x1hviunn", $$css: true as const }, paddingCompact: { k8WAf4: "x8o8v82", kg3NbH: "x1pzlopt", $$css: true as const }, paddingBalanced: { k8WAf4: "x8o8v82", kg3NbH: "x1pzlopt", $$css: true as const }, paddingSpacious: { k8WAf4: "x1na6nto", kg3NbH: "xzz68wx", $$css: true as const }, paddingBlockNone: { k8WAf4: "xt970qd", $$css: true as const }, metadataPaddingCompact: { kg3NbH: "x1pzlopt", $$css: true as const }, metadataPaddingBalanced: { kg3NbH: "x1pzlopt", $$css: true as const }, metadataPaddingSpacious: { kg3NbH: "xzz68wx", $$css: true as const }, metadataReducedGap: { keoZOQ: "x1c40v9y", $$css: true as const }, headerReducedGap: { k1K539: "xdrfirk", $$css: true as const }, nameRow: { kZKoxP: "x1grt7ep", k1xSpc: "x78zum5", kGNEyG: "x6s0dn4", $$css: true as const }, alignEnd: { k9WMMc: "xp4054r", $$css: true as const }, assistant: { kWkggS: "x17x4s8c", kMwMTN: "x1tgivj0", $$css: true as const }, user: { kWkggS: "x17x4s8c", kMwMTN: "x1tgivj0", $$css: true as const }, ghost: { kWkggS: "xjbqb8w", kMwMTN: "x1tgivj0", $$css: true as const }, groupFirstAssistant: { kVL7Gh: "xrfs2vl", $$css: true as const }, groupMiddleAssistant: { krdFHd: "x3qb8m", kVL7Gh: "xrfs2vl", $$css: true as const }, groupLastAssistant: { krdFHd: "x3qb8m", $$css: true as const }, groupFirstUser: { kT0f0o: "x1whkvtz", $$css: true as const }, groupMiddleUser: { kfmiAY: "x126per9", kT0f0o: "x1whkvtz", $$css: true as const }, groupLastUser: { kfmiAY: "x126per9", $$css: true as const } }; // Dynamic styles for sizing props const _temp = { ks0D6T: "x1x1rfll", "$$css": true }; const dynamicStyles = { sizing: (width: SizeValue) => [_temp, { kzqmXN: width != null ? "x5lhr3w" : width, $$css: true as const }, { "--x-width": (val => typeof val === "number" ? val + "px" : val != null ? val : undefined)(width) }] }; // ============================================================================= // Component // ============================================================================= /** * Styled content container — the chat "bubble." * * Reads sender from parent ChatMessage context to auto-style background. * Use `group` prop for multi-bubble corner grouping. * * @example * ``` * * }> * Hey, how's it going? * * * ``` */ export function ChatMessageBubble({ children, variant = 'filled', name, metadata, group, width, xstyle, className, style: styleProp, 'data-testid': testId, ref, ...rest }: ChatMessageBubbleProps) { const msgContext = useChatMessageContext(); const sender = msgContext?.sender ?? 'assistant'; const density = msgContext?.density ?? 'balanced'; const paddingStyle = density === 'compact' ? styles.paddingCompact : density === 'spacious' ? styles.paddingSpacious : styles.paddingBalanced; const isUser = sender === 'user'; const senderStyle = variant === 'ghost' ? styles.ghost : isUser ? styles.user : styles.assistant; const groupStyle = group === 'first' ? isUser ? styles.groupFirstUser : styles.groupFirstAssistant : group === 'middle' ? isUser ? styles.groupMiddleUser : styles.groupMiddleAssistant : group === 'last' ? isUser ? styles.groupLastUser : styles.groupLastAssistant : null; const metadataPaddingStyle = density === 'compact' ? styles.metadataPaddingCompact : density === 'spacious' ? styles.metadataPaddingSpacious : styles.metadataPaddingBalanced; return <> {name &&
{name}
}
{children}
{metadata &&
{metadata}
} ; } ChatMessageBubble.displayName = 'ChatMessageBubble';