// ============================================================================ // Chatbot Main - Message // ============================================================================ import { forwardRef, ReactNode, useEffect, useState } from 'react'; import type { FunctionComponent, HTMLProps, MouseEvent as ReactMouseEvent, Ref } from 'react'; import { Options } from 'react-markdown'; import { AlertProps, Avatar, AvatarProps, ButtonProps, FormProps, Label, LabelGroupProps, Timestamp, Truncate } from '@patternfly/react-core'; import MessageLoading from './MessageLoading'; import { CodeBlockMessageProps } from './CodeBlockMessage/CodeBlockMessage'; import FileDetailsLabel from '../FileDetailsLabel/FileDetailsLabel'; import ResponseActions, { ActionProps } from '../ResponseActions/ResponseActions'; import SourcesCard, { SourcesCardProps } from '../SourcesCard'; import QuickStartTile from './QuickStarts/QuickStartTile'; import { QuickStart, QuickstartAction } from './QuickStarts/types'; import QuickResponse from './QuickResponse/QuickResponse'; import UserFeedback, { UserFeedbackProps } from './UserFeedback/UserFeedback'; import UserFeedbackComplete, { UserFeedbackCompleteProps } from './UserFeedback/UserFeedbackComplete'; import { TableProps } from '@patternfly/react-table'; // see the full list of styles here: https://highlightjs.org/examples import 'highlight.js/styles/vs2015.css'; import { PluggableList } from 'unified'; import ErrorMessage from './ErrorMessage/ErrorMessage'; import MessageInput from './MessageInput'; import ToolResponse, { ToolResponseProps } from '../ToolResponse'; import DeepThinking, { DeepThinkingProps } from '../DeepThinking'; import ToolCall, { ToolCallProps } from '../ToolCall'; import MarkdownContent from '../MarkdownContent'; export interface MessageAttachment { /** Name of file attached to the message */ name: string; /** Unique identifier of file attached to the message */ id?: string | number; /** Callback for when attachment label is clicked */ onClick?: (event: React.MouseEvent, name: string, id?: string | number) => void; /** Callback for when attachment label is closed */ onClose?: (event: React.MouseEvent, name: string, id?: string | number) => void; /** Whether file is loading */ isLoading?: boolean; /** Aria label for attachment close button */ closeButtonAriaLabel?: string; /** Custom test id for the language in the attachment component */ languageTestId?: string; /** Custom test id for the loading spinner in the attachment component */ spinnerTestId?: string; } export interface MessageExtraContent { /** Content to display before the main content */ beforeMainContent?: ReactNode; /** Content to display after the main content */ afterMainContent?: ReactNode; /** Content to display at the end */ endContent?: ReactNode; } export interface MessageProps extends Omit, 'role'> { /** Children to render instead of the default message structure, allowing more fine-tuned message control. When provided, this will override the default rendering of content, toolResponse, deepThinking, toolCall, sources, quickStarts, actions, etc. */ children?: ReactNode; /** Unique id for message */ id?: string; /** Role of the user sending the message */ role: 'user' | 'bot'; /** Message content */ content?: string; /** Extra Message content */ extraContent?: MessageExtraContent; /** Name of the user */ name?: string; /** Avatar src for the user */ avatar?: string; /** Timestamp for the message */ timestamp?: string; /** Set this to true if message is being loaded */ isLoading?: boolean; /** Array of attachments attached to a message */ attachments?: MessageAttachment[]; /** Props for message actions, such as feedback (positive or negative), copy button, edit message, share, and listen. * Can be a single actions object or an array of action group objects. When passing an array, you can pass an object of actions or * an object that contains an actions property for finer control of selection persistence. */ actions?: | { [key: string]: ActionProps; } | { [key: string]: ActionProps; }[] | { actions: { [key: string]: ActionProps; }; persistActionSelection?: boolean; }[]; /** When true, the selected action will persist even when clicking outside the component. * When false (default), clicking outside or clicking another action will deselect the current selection. * For finer control of multiple action groups, use persistActionSelection on each group. */ persistActionSelection?: boolean; /** Sources for message */ sources?: SourcesCardProps; /** Label for the English word "AI," used to tag messages with role "bot" */ botWord?: string; /** Label for the English "Loading message," displayed to screenreaders when loading a message */ loadingWord?: string; /** Props for code blocks */ codeBlockProps?: CodeBlockMessageProps; /** Props for quick responses */ quickResponses?: QuickResponse[]; /** Props for quick responses container */ quickResponseContainerProps?: Omit; /** Props for user feedback card */ userFeedbackForm?: Omit; /** Props for user feedback response */ userFeedbackComplete?: Omit; /** Whether avatar is round */ hasRoundAvatar?: boolean; /** Any additional props applied to the avatar, for additional customization */ avatarProps?: Omit; /** Props for QuickStart card */ quickStarts?: { quickStart: QuickStart; onSelectQuickStart: (id?: string) => void; minuteWord?: string; minuteWordPlural?: string; prerequisiteWord?: string; prerequisiteWordPlural?: string; quickStartButtonAriaLabel?: string; className?: string; onClick?: () => void; action?: QuickstartAction; }; /** Turns the container into a live region so that changes to content within the Message, such as appending a feedback card, are reliably announced to assistive technology. */ isLiveRegion?: boolean; /** Ref applied to message */ innerRef?: Ref; /** Props for table message. It is important to include a detailed aria-label that describes the purpose of the table. */ tableProps?: Required> & TableProps; /** Additional rehype plugins passed from the consumer */ additionalRehypePlugins?: PluggableList; /** Additional remark plugins passed from the consumer */ additionalRemarkPlugins?: PluggableList; /** Whether to open links in message in new tab. */ openLinkInNewTab?: boolean; /** Optional inline error message that can be displayed in the message */ error?: AlertProps; /** Props for links */ linkProps?: ButtonProps; /** Whether message is in edit mode */ isEditable?: boolean; /** Placeholder for edit input */ editPlaceholder?: string; /** Label for the English word "Update" used in edit mode. */ updateWord?: string; /** Label for the English word "Cancel" used in edit mode. */ cancelWord?: string; /** Callback function for when edit mode update button is clicked */ onEditUpdate?: (event: ReactMouseEvent) => void; /** Callback functionf or when edit cancel update button is clicked */ onEditCancel?: (event: ReactMouseEvent) => void; /** Ref applied to editable message input */ inputRef?: Ref; /** Props for edit form */ editFormProps?: FormProps; /** Sets message to compact styling. */ isCompact?: boolean; /** Disables markdown parsing for message, allowing only text input */ isMarkdownDisabled?: boolean; /** Allows passing additional props down to markdown parser react-markdown, such as allowedElements and disallowedElements. See https://github.com/remarkjs/react-markdown?tab=readme-ov-file#options for options */ reactMarkdownProps?: Options; /** Props for tool response card */ toolResponse?: ToolResponseProps; /** Props for deep thinking card */ deepThinking?: DeepThinkingProps; /** Allows passing additional props down to remark-gfm. See https://github.com/remarkjs/remark-gfm?tab=readme-ov-file#options for options */ remarkGfmProps?: Options; /** Props for a tool call message */ toolCall?: ToolCallProps; /** Whether user messages default to stripping out images in markdown */ hasNoImagesInUserMessages?: boolean; /** Sets background colors to be appropriate on primary chatbot background */ isPrimary?: boolean; } export const MessageBase: FunctionComponent = ({ children, role, content, extraContent, name, avatar, timestamp, isLoading, actions, persistActionSelection, sources, botWord = 'AI', loadingWord = 'Loading message', codeBlockProps, quickResponses, quickResponseContainerProps = { numLabels: 5 }, attachments, hasRoundAvatar = true, avatarProps, quickStarts, userFeedbackForm, userFeedbackComplete, isLiveRegion = true, innerRef, tableProps, openLinkInNewTab = true, additionalRehypePlugins = [], additionalRemarkPlugins = [], linkProps, error, isEditable, editPlaceholder = 'Edit prompt message...', updateWord = 'Update', cancelWord = 'Cancel', onEditUpdate, onEditCancel, inputRef, editFormProps, isCompact, isMarkdownDisabled, reactMarkdownProps, toolResponse, deepThinking, remarkGfmProps, toolCall, hasNoImagesInUserMessages = true, isPrimary, ...props }: MessageProps) => { const [messageText, setMessageText] = useState(content); useEffect(() => { setMessageText(content); }, [content]); const { beforeMainContent, afterMainContent, endContent } = extraContent || {}; let avatarClassName: string | undefined; if (avatarProps && 'className' in avatarProps) { const { className, ...rest } = avatarProps; avatarClassName = className; avatarProps = { ...rest }; } // Keep timestamps consistent between Timestamp component and aria-label const date = new Date(); const dateString = timestamp ?? `${date.toLocaleDateString()} ${date.toLocaleTimeString()}`; const handleMarkdown = () => ( ); const renderMessage = () => { if (isLoading) { return ; } if (isEditable) { return ( <> {beforeMainContent && <>{beforeMainContent}} , value: string) => { onEditUpdate && onEditUpdate(event); setMessageText(value); }} onEditCancel={onEditCancel} inputRef={inputRef} {...editFormProps} /> ); } return ( <> {beforeMainContent && <>{beforeMainContent}} {error ? : handleMarkdown()} ); }; return (
{/* We are using an empty alt tag intentionally in order to reduce noise on screen readers */} {avatar && ( )}
{name && ( )} {role === 'bot' && ( )} {timestamp}
{children ? ( <>{children} ) : ( <>
{renderMessage()} {afterMainContent && <>{afterMainContent}} {toolResponse && } {deepThinking && } {toolCall && } {!isLoading && sources && } {quickStarts && quickStarts.quickStart && ( )} {!isLoading && !isEditable && actions && ( <> {Array.isArray(actions) ? (
{actions.map((actionGroup, index) => ( ))}
) : ( )} )} {userFeedbackForm && ( )} {userFeedbackComplete && ( )} {!isLoading && quickResponses && ( )}
{attachments && (
{attachments.map((attachment) => (
))}
)} {!isLoading && endContent && <>{endContent}} )}
); }; const Message = forwardRef((props: MessageProps, ref: Ref) => ( )); export default Message;