Skip to content

Commit 74babcc

Browse files
committed
feat(Messages): allowed more composable structures
1 parent 6abd00d commit 74babcc

File tree

11 files changed

+289
-64
lines changed

11 files changed

+289
-64
lines changed

packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/Messages.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ propComponents:
1919
'FileDropZone',
2020
'PreviewAttachment',
2121
'Message',
22+
'MessageAndActionsProps',
23+
'MessageAttachmentsContainerProps',
24+
'MessageAttachmentProps',
25+
'ResponseActionsGroupsProps',
2226
'MessageExtraContent',
2327
'PreviewAttachment',
2428
'ActionProps',

packages/module/src/Message/Message.tsx

Lines changed: 75 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ export interface MessageExtraContent {
6767
}
6868

6969
export interface MessageProps extends Omit<HTMLProps<HTMLDivElement>, 'role'> {
70+
/** 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. */
71+
children?: ReactNode;
7072
/** Unique id for message */
7173
id?: string;
7274
/** Role of the user sending the message */
@@ -193,6 +195,7 @@ export interface MessageProps extends Omit<HTMLProps<HTMLDivElement>, 'role'> {
193195
}
194196

195197
export const MessageBase: FunctionComponent<MessageProps> = ({
198+
children,
196199
role,
197200
content,
198201
extraContent,
@@ -341,74 +344,82 @@ export const MessageBase: FunctionComponent<MessageProps> = ({
341344
<Timestamp date={date}>{timestamp}</Timestamp>
342345
</div>
343346
<div className="pf-chatbot__message-response">
344-
<div className="pf-chatbot__message-and-actions">
345-
{renderMessage()}
346-
{afterMainContent && <>{afterMainContent}</>}
347-
{toolResponse && <ToolResponse {...toolResponse} />}
348-
{deepThinking && <DeepThinking {...deepThinking} />}
349-
{toolCall && <ToolCall {...toolCall} />}
350-
{!isLoading && sources && <SourcesCard {...sources} isCompact={isCompact} />}
351-
{quickStarts && quickStarts.quickStart && (
352-
<QuickStartTile
353-
quickStart={quickStarts.quickStart}
354-
onSelectQuickStart={quickStarts.onSelectQuickStart}
355-
minuteWord={quickStarts.minuteWord}
356-
minuteWordPlural={quickStarts.minuteWordPlural}
357-
prerequisiteWord={quickStarts.prerequisiteWord}
358-
prerequisiteWordPlural={quickStarts.prerequisiteWordPlural}
359-
quickStartButtonAriaLabel={quickStarts.quickStartButtonAriaLabel}
360-
isCompact={isCompact}
361-
/>
362-
)}
363-
{!isLoading && !isEditable && actions && (
364-
<>
365-
{Array.isArray(actions) ? (
366-
<div className="pf-chatbot__response-actions-groups">
367-
{actions.map((actionGroup, index) => (
368-
<ResponseActions
369-
key={index}
370-
actions={actionGroup.actions || actionGroup}
371-
persistActionSelection={persistActionSelection || actionGroup.persistActionSelection}
372-
/>
373-
))}
374-
</div>
375-
) : (
376-
<ResponseActions actions={actions} persistActionSelection={persistActionSelection} />
347+
{children ? (
348+
<>{children}</>
349+
) : (
350+
<>
351+
<div className="pf-chatbot__message-and-actions">
352+
{renderMessage()}
353+
{afterMainContent && <>{afterMainContent}</>}
354+
{toolResponse && <ToolResponse {...toolResponse} />}
355+
{deepThinking && <DeepThinking {...deepThinking} />}
356+
{toolCall && <ToolCall {...toolCall} />}
357+
{!isLoading && sources && <SourcesCard {...sources} isCompact={isCompact} />}
358+
{quickStarts && quickStarts.quickStart && (
359+
<QuickStartTile
360+
quickStart={quickStarts.quickStart}
361+
onSelectQuickStart={quickStarts.onSelectQuickStart}
362+
minuteWord={quickStarts.minuteWord}
363+
minuteWordPlural={quickStarts.minuteWordPlural}
364+
prerequisiteWord={quickStarts.prerequisiteWord}
365+
prerequisiteWordPlural={quickStarts.prerequisiteWordPlural}
366+
quickStartButtonAriaLabel={quickStarts.quickStartButtonAriaLabel}
367+
isCompact={isCompact}
368+
/>
369+
)}
370+
{!isLoading && !isEditable && actions && (
371+
<>
372+
{Array.isArray(actions) ? (
373+
<div className="pf-chatbot__response-actions-groups">
374+
{actions.map((actionGroup, index) => (
375+
<ResponseActions
376+
key={index}
377+
actions={actionGroup.actions || actionGroup}
378+
persistActionSelection={persistActionSelection || actionGroup.persistActionSelection}
379+
/>
380+
))}
381+
</div>
382+
) : (
383+
<ResponseActions actions={actions} persistActionSelection={persistActionSelection} />
384+
)}
385+
</>
386+
)}
387+
{userFeedbackForm && (
388+
<UserFeedback {...userFeedbackForm} timestamp={dateString} isCompact={isCompact} />
377389
)}
378-
</>
379-
)}
380-
{userFeedbackForm && <UserFeedback {...userFeedbackForm} timestamp={dateString} isCompact={isCompact} />}
381-
{userFeedbackComplete && (
382-
<UserFeedbackComplete {...userFeedbackComplete} timestamp={dateString} isCompact={isCompact} />
383-
)}
384-
{!isLoading && quickResponses && (
385-
<QuickResponse
386-
quickResponses={quickResponses}
387-
quickResponseContainerProps={quickResponseContainerProps}
388-
isCompact={isCompact}
389-
/>
390-
)}
391-
</div>
392-
{attachments && (
393-
<div className="pf-chatbot__message-attachments-container">
394-
{attachments.map((attachment) => (
395-
<div key={attachment.id ?? attachment.name} className="pf-chatbot__message-attachment">
396-
<FileDetailsLabel
397-
fileName={attachment.name}
398-
fileId={attachment.id}
399-
onClose={attachment.onClose}
400-
onClick={attachment.onClick}
401-
isLoading={attachment.isLoading}
402-
closeButtonAriaLabel={attachment.closeButtonAriaLabel}
403-
languageTestId={attachment.languageTestId}
404-
spinnerTestId={attachment.spinnerTestId}
405-
variant={isPrimary ? 'outline' : undefined}
390+
{userFeedbackComplete && (
391+
<UserFeedbackComplete {...userFeedbackComplete} timestamp={dateString} isCompact={isCompact} />
392+
)}
393+
{!isLoading && quickResponses && (
394+
<QuickResponse
395+
quickResponses={quickResponses}
396+
quickResponseContainerProps={quickResponseContainerProps}
397+
isCompact={isCompact}
406398
/>
399+
)}
400+
</div>
401+
{attachments && (
402+
<div className="pf-chatbot__message-attachments-container">
403+
{attachments.map((attachment) => (
404+
<div key={attachment.id ?? attachment.name} className="pf-chatbot__message-attachment">
405+
<FileDetailsLabel
406+
fileName={attachment.name}
407+
fileId={attachment.id}
408+
onClose={attachment.onClose}
409+
onClick={attachment.onClick}
410+
isLoading={attachment.isLoading}
411+
closeButtonAriaLabel={attachment.closeButtonAriaLabel}
412+
languageTestId={attachment.languageTestId}
413+
spinnerTestId={attachment.spinnerTestId}
414+
variant={isPrimary ? 'outline' : undefined}
415+
/>
416+
</div>
417+
))}
407418
</div>
408-
))}
409-
</div>
419+
)}
420+
{!isLoading && endContent && <>{endContent}</>}
421+
</>
410422
)}
411-
{!isLoading && endContent && <>{endContent}</>}
412423
</div>
413424
</div>
414425
</section>
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import '@testing-library/jest-dom';
2+
import { render, screen } from '@testing-library/react';
3+
import MessageAndActions from './MessageAndActions';
4+
5+
test('Renders with children', () => {
6+
render(<MessageAndActions>Test content</MessageAndActions>);
7+
expect(screen.getByText('Test content')).toBeInTheDocument();
8+
});
9+
10+
test('Renders with pf-chatbot__message-and-actions class by default', () => {
11+
render(<MessageAndActions>Test content</MessageAndActions>);
12+
expect(screen.getByText('Test content')).toHaveClass('pf-chatbot__message-and-actions', { exact: true });
13+
});
14+
15+
test('Renders with custom className', () => {
16+
render(<MessageAndActions className="custom-class">Test content</MessageAndActions>);
17+
expect(screen.getByText('Test content')).toHaveClass('custom-class');
18+
});
19+
20+
test('Spreads additional props', () => {
21+
render(<MessageAndActions id="test-id">Test content</MessageAndActions>);
22+
expect(screen.getByText('Test content')).toHaveAttribute('id', 'test-id');
23+
});
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// ============================================================================
2+
// Message And Actions - Container for message content and actions
3+
// ============================================================================
4+
import { FunctionComponent, HTMLProps, ReactNode } from 'react';
5+
import { css } from '@patternfly/react-styles';
6+
7+
/**
8+
* The container that wraps the primary message content and inline actions, such as ToolCall, ToolResponse, DeepThinking, ResponseActions, etc.
9+
* Attachments should not be rendered inside this container.
10+
* Use this component when passing children to Message to customize its structure.
11+
*/
12+
export interface MessageAndActionsProps extends HTMLProps<HTMLDivElement> {
13+
/** Content to render inside the message and actions container. */
14+
children: ReactNode;
15+
/** Additional classes applied to the message and actions container. */
16+
className?: string;
17+
}
18+
19+
export const MessageAndActions: FunctionComponent<MessageAndActionsProps> = ({ children, className, ...props }) => (
20+
<div className={css('pf-chatbot__message-and-actions', className)} {...props}>
21+
{children}
22+
</div>
23+
);
24+
25+
export default MessageAndActions;
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import '@testing-library/jest-dom';
2+
import { render, screen } from '@testing-library/react';
3+
import MessageAttachment from './MessageAttachment';
4+
5+
test('Renders with children', () => {
6+
render(<MessageAttachment>Test content</MessageAttachment>);
7+
expect(screen.getByText('Test content')).toBeInTheDocument();
8+
});
9+
10+
test('Renders with pf-chatbot__message-attachment class by default', () => {
11+
render(<MessageAttachment>Test content</MessageAttachment>);
12+
expect(screen.getByText('Test content')).toHaveClass('pf-chatbot__message-attachment', { exact: true });
13+
});
14+
15+
test('Renders with custom className', () => {
16+
render(<MessageAttachment className="custom-class">Test content</MessageAttachment>);
17+
expect(screen.getByText('Test content')).toHaveClass('custom-class');
18+
});
19+
20+
test('Spreads additional props', () => {
21+
render(<MessageAttachment id="test-id">Test content</MessageAttachment>);
22+
expect(screen.getByText('Test content')).toHaveAttribute('id', 'test-id');
23+
});
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// ============================================================================
2+
// Message Attachment - Container for a single message attachment
3+
// ============================================================================
4+
import { FunctionComponent, HTMLProps, ReactNode } from 'react';
5+
import { css } from '@patternfly/react-styles';
6+
7+
/**
8+
* The container for a single message attachment item, typically the FileDetailsLabel component. You must wrap any attachment components in this container.
9+
* Use this component within MessageAttachmentsContainer when passing children to Message to customize its structure.
10+
*/
11+
export interface MessageAttachmentProps extends HTMLProps<HTMLDivElement> {
12+
/** Content to render inside a single attachment container */
13+
children: ReactNode;
14+
/** Additional classes applied to the attachment container. */
15+
className?: string;
16+
}
17+
18+
export const MessageAttachment: FunctionComponent<MessageAttachmentProps> = ({ children, className, ...props }) => (
19+
<div className={css('pf-chatbot__message-attachment', className)} {...props}>
20+
{children}
21+
</div>
22+
);
23+
24+
export default MessageAttachment;
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import '@testing-library/jest-dom';
2+
import { render, screen } from '@testing-library/react';
3+
import MessageAttachmentsContainer from './MessageAttachmentsContainer';
4+
5+
test('Renders with children', () => {
6+
render(<MessageAttachmentsContainer>Test content</MessageAttachmentsContainer>);
7+
expect(screen.getByText('Test content')).toBeInTheDocument();
8+
});
9+
10+
test('Renders with pf-chatbot__message-attachments-container class by default', () => {
11+
render(<MessageAttachmentsContainer>Test content</MessageAttachmentsContainer>);
12+
expect(screen.getByText('Test content')).toHaveClass('pf-chatbot__message-attachments-container', { exact: true });
13+
});
14+
15+
test('Renders with custom className', () => {
16+
render(<MessageAttachmentsContainer className="custom-class">Test content</MessageAttachmentsContainer>);
17+
expect(screen.getByText('Test content')).toHaveClass('custom-class');
18+
});
19+
20+
test('Spreads additional props', () => {
21+
render(<MessageAttachmentsContainer id="test-id">Test content</MessageAttachmentsContainer>);
22+
expect(screen.getByText('Test content')).toHaveAttribute('id', 'test-id');
23+
});
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// ============================================================================
2+
// Message Attachments Container - Container for message attachments
3+
// ============================================================================
4+
import { FunctionComponent, HTMLProps, ReactNode } from 'react';
5+
import { css } from '@patternfly/react-styles';
6+
7+
/**
8+
* The container to wrap MessageAttachment components. You must wrap any MessageAttachment components in this container.
9+
* Use this component when passing children to Message to customize its structure.
10+
*/
11+
export interface MessageAttachmentsContainerProps extends HTMLProps<HTMLDivElement> {
12+
/** Content to render inside the attachments container */
13+
children: ReactNode;
14+
/** Additional classes applied to the attachments container. */
15+
className?: string;
16+
}
17+
18+
export const MessageAttachmentsContainer: FunctionComponent<MessageAttachmentsContainerProps> = ({
19+
children,
20+
className,
21+
...props
22+
}) => (
23+
<div className={css('pf-chatbot__message-attachments-container', className)} {...props}>
24+
{children}
25+
</div>
26+
);
27+
28+
export default MessageAttachmentsContainer;
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import '@testing-library/jest-dom';
2+
import { render, screen } from '@testing-library/react';
3+
import ResponseActionsGroups from './ResponseActionsGroups';
4+
5+
test('Renders with children', () => {
6+
render(<ResponseActionsGroups>Test content</ResponseActionsGroups>);
7+
expect(screen.getByText('Test content')).toBeInTheDocument();
8+
});
9+
10+
test('Renders with pf-chatbot__response-actions-groups class by default', () => {
11+
render(<ResponseActionsGroups>Test content</ResponseActionsGroups>);
12+
expect(screen.getByText('Test content')).toHaveClass('pf-chatbot__response-actions-groups', { exact: true });
13+
});
14+
15+
test('Renders with custom className', () => {
16+
render(<ResponseActionsGroups className="custom-class">Test content</ResponseActionsGroups>);
17+
expect(screen.getByText('Test content')).toHaveClass('custom-class');
18+
});
19+
20+
test('Spreads additional props', () => {
21+
render(<ResponseActionsGroups id="test-id">Test content</ResponseActionsGroups>);
22+
expect(screen.getByText('Test content')).toHaveAttribute('id', 'test-id');
23+
});
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// ============================================================================
2+
// Response Actions Groups - Container for multiple action groups
3+
// ============================================================================
4+
import { FunctionComponent, HTMLProps, ReactNode } from 'react';
5+
import { css } from '@patternfly/react-styles';
6+
7+
/**
8+
* The container for grouping multiple related ResponseActions components, typically used for having different persistence states amongst groups.
9+
* Use this component when passing children to Message to customize its structure.
10+
*/
11+
export interface ResponseActionsGroupsProps extends HTMLProps<HTMLDivElement> {
12+
/** Content to render inside the response actions groups container */
13+
children: ReactNode;
14+
/** Additional classes applied to the response actions groups container. */
15+
className?: string;
16+
}
17+
18+
export const ResponseActionsGroups: FunctionComponent<ResponseActionsGroupsProps> = ({
19+
children,
20+
className,
21+
...props
22+
}) => (
23+
<div className={css('pf-chatbot__response-actions-groups', className)} {...props}>
24+
{children}
25+
</div>
26+
);
27+
28+
export default ResponseActionsGroups;

0 commit comments

Comments
 (0)