// Copyright (c) Meta Platforms, Inc. and affiliates. 'use client'; /** * @file ChatMessage.tsx * @input Uses React, StyleX, ChatContext, theme tokens * @output Exports ChatMessage component and ChatMessageProps * @position Sender context wrapper — handles avatar, name, alignment by sender role * * Layout (with avatar): * [Avatar] [Name ] * [Content/Bubbles ] * [Metadata ] * * 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/ChatMessage/ (block examples) */ import { type ReactNode, useMemo, useId } from 'react'; import * as stylex from "../style-runtime.js"; import "../theme/tokens.stylex.js"; import { colorVars, spacingVars, typeScaleVars, fontWeightVars } from "../theme/tokens.stylex.js"; import { ChatMessageContext, useChatListContext, type ChatMessageSender, type ChatDensity } from "./ChatContext.js"; import { mergeProps } from "../utils/index.js"; import type { BaseProps } from "../BaseProps.js"; import { themeProps } from "../utils/themeProps.js"; import { useTranslator } from "../i18n/index.js"; export interface ChatMessageProps extends BaseProps { ref?: React.Ref; sender: ChatMessageSender; /** * Message body — bubbles, tool calls, images, or any free-form content. * Custom (non-bubble) children render flush with the message edge; wrap * them in a ghost bubble (``) to align * them with the bubble's text column, and add `width="100%"` when they * should span the full message column. */ children: ReactNode; avatar?: ReactNode; /** * Sender name rendered above the message body. * Use when the first child is raw content (not a bubble). * If the first child is a ChatMessageBubble, put the name on the * bubble's `name` prop instead — it aligns with the bubble's padding. */ name?: ReactNode; /** * Metadata rendered below the message body. * Use when the last child is raw content (not a bubble). * If the last child is a ChatMessageBubble, put metadata on the * bubble's `metadata` prop instead — it aligns with the bubble's padding. */ metadata?: ReactNode; density?: ChatDensity; } const styles = { root: { k1xSpc: "x78zum5", kGNEyG: "x1cy8zhl", ks0D6T: "x193iq5w", $$css: true as const }, rootGapCompact: { kOIVth: "x1s4dlld", $$css: true as const }, rootGapBalanced: { kOIVth: "x1txdalj", $$css: true as const }, rootGapSpacious: { kOIVth: "xjcht0a", $$css: true as const }, rootAssistant: { kXwgrk: "x1q0g3np", kjj79g: "x1nhvcw1", $$css: true as const }, rootUser: { kXwgrk: "x15zctf7", kjj79g: "x1nhvcw1", $$css: true as const }, rootSystem: { kXwgrk: "x1q0g3np", kjj79g: "xl56j7k", $$css: true as const }, contentColumn: { k1xSpc: "x78zum5", kXwgrk: "xdt5ytf", kUk6DE: "x98rzlu", k7Eaqz: "xeuugli", $$css: true as const }, contentColumnSystem: { ks0D6T: "xq8v1hd", kGNEyG: "x6s0dn4", $$css: true as const }, contentColumnAssistant: { kGNEyG: "x1cy8zhl", $$css: true as const }, contentColumnUser: { kGNEyG: "xuk3077", $$css: true as const }, childrenWrap: { k1xSpc: "x78zum5", kXwgrk: "xdt5ytf", k7Eaqz: "xeuugli", kzqmXN: "xh8yej3", $$css: true as const }, childrenAssistant: { kGNEyG: "x1cy8zhl", $$css: true as const }, childrenUser: { kGNEyG: "xuk3077", $$css: true as const }, childrenSystem: { kGNEyG: "x6s0dn4", $$css: true as const }, childrenGapCompact: { kOIVth: "x1lsbc85", $$css: true as const }, childrenGapBalanced: { kOIVth: "xzye2dw", $$css: true as const }, childrenGapSpacious: { kOIVth: "x1s4dlld", $$css: true as const } }; /** * Sender context wrapper for chat messages. * * Provides sender and density context to child components. * Use ChatMessageMetadata as a child for timestamp, status, and footer. * * @example * ``` * }> * Hello! * * * ``` */ export function ChatMessage({ sender, children, avatar, name, metadata, density: densityProp, xstyle, className, style: styleProp, 'data-testid': testId, ref, ...rest }: ChatMessageProps) { const t = useTranslator(); const listContext = useChatListContext(); const density = densityProp ?? listContext?.density ?? 'balanced'; const contextValue = useMemo(() => ({ sender, density }), [sender, density]); const rootGap = density === 'compact' ? styles.rootGapCompact : density === 'spacious' ? styles.rootGapSpacious : styles.rootGapBalanced; const childrenGap = density === 'compact' ? styles.childrenGapCompact : density === 'spacious' ? styles.childrenGapSpacious : styles.childrenGapBalanced; const rootAlignment = sender === 'system' ? styles.rootSystem : sender === 'user' ? styles.rootUser : styles.rootAssistant; const columnAlignment = sender === 'system' ? styles.contentColumnSystem : sender === 'user' ? styles.contentColumnUser : styles.contentColumnAssistant; const childrenAlignment = sender === 'system' ? styles.childrenSystem : sender === 'user' ? styles.childrenUser : styles.childrenAssistant; const isSystem = sender === 'system'; const hasAvatar = avatar != null && !isSystem; const hasName = name != null && !isSystem; const nameId = useId(); return
{hasAvatar &&
{avatar}
}
{hasName &&
{name}
}
{children}
{metadata != null && !isSystem &&
{metadata}
}
; } ChatMessage.displayName = 'ChatMessage';