Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { FunctionComponent } from 'react';
import Message, {
ErrorMessage,
MessageAndActions,
MessageAttachmentItem,
MessageAttachmentsContainer,
MessageLoading
} from '@patternfly/chatbot/dist/dynamic/Message';
import MarkdownContent from '@patternfly/chatbot/dist/dynamic/MarkdownContent';
import ToolCall from '@patternfly/chatbot/dist/dynamic/ToolCall';
import ToolResponse from '@patternfly/chatbot/dist/dynamic/ToolResponse';
import FileDetailsLabel from '@patternfly/chatbot/dist/dynamic/FileDetailsLabel';
import ResponseActions, { ResponseActionsGroups } from '@patternfly/chatbot/dist/dynamic/ResponseActions';
import patternflyAvatar from './patternfly_avatar.jpg';
import userAvatar from './user_avatar.svg';

const handlePositiveResponse = () => {
// Handle positive response
};

const handleNegativeResponse = () => {
// Handle negative response
};

const handleCopy = () => {
// Handle copy action
};

const handleDownload = () => {
// Handle download action
};

const handleListen = () => {
// Handle listen action
};

export const MessageWithCustomStructure: FunctionComponent = () => (
<>
<Message name="Bot" role="bot" avatar={patternflyAvatar}>
<MessageAndActions>
<MarkdownContent
content={`This is a basic message with a more custom, fine-tuned structure. You can pass markdown to the MarkdownContent component, such as **bold content with double asterisks** or _italic content with single underscores_.`}
/>
<ToolCall titleText="Calling 'awesome_tool'" loadingText="Loading 'awesome_tool'" isLoading={true} />
<ToolResponse
toggleContent="Tool response: fetch_user_data"
subheading="Executed in 0.3 seconds"
body="Successfully retrieved user data from the database."
cardTitle="User Data Response"
cardBody="The tool returned 150 user records matching the specified criteria."
/>
<ErrorMessage title="An issue placed within this custom structure." />
<MarkdownContent
isMarkdownDisabled
textComponent={`You can also pass plain text without markdown to the MarkdownContent component by passing the isMarkdownDisabled prop. Optionally, you can also use the textComponent prop instead of content.`}
/>
<ToolCall titleText="Calling 'more_awesome_tool'" loadingText="Loading 'more_awesome_tool'" isLoading={true} />
<ToolCall titleText="Calling 'even_more_awesome_tool'" loadingText="Loading 'even_more_awesome_tool'" />
<MarkdownContent content={`You can even place a message loading state in the middle of a message:`} />
<MessageLoading loadingWord="Loading something in the middle of a custom structured message" />
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
<MessageLoading loadingWord="Loading something in the middle of a custom structured message" />

<ResponseActionsGroups>
<ResponseActions
actions={{
positive: { onClick: handlePositiveResponse, ariaLabel: 'Good response' },
negative: { onClick: handleNegativeResponse, ariaLabel: 'Bad response' }
}}
persistActionSelection={true}
/>
<ResponseActions
actions={{
copy: { onClick: handleCopy, ariaLabel: 'Copy' },
download: { onClick: handleDownload, ariaLabel: 'Download' }
}}
persistActionSelection={false}
/>
<ResponseActions
actions={{
listen: { onClick: handleListen, ariaLabel: 'Listen' }
}}
persistActionSelection={true}
/>
</ResponseActionsGroups>
</MessageAndActions>
</Message>
<Message name="User" role="user" avatar={userAvatar}>
<MessageAndActions>
<MarkdownContent content="This message is in the MessageAndActions container, and the file attachments below are in their own separate MessageAttachmentsContainer!" />
</MessageAndActions>
<MessageAttachmentsContainer>
<MessageAttachmentItem>
<FileDetailsLabel fileName="project-report.pdf" />
</MessageAttachmentItem>
<MessageAttachmentItem>
<FileDetailsLabel fileName="data-analysis.csv" />
</MessageAttachmentItem>
<MessageAttachmentItem>
<FileDetailsLabel fileName="presentation-slides.pptx" />
</MessageAttachmentItem>
</MessageAttachmentsContainer>
</Message>
</>
);
Original file line number Diff line number Diff line change
Expand Up @@ -14,24 +14,40 @@ propComponents:
[
'AttachMenu',
'AttachmentEdit',
'FileDetailsProps',
'FileDetailsLabelProps',
'FileDropZone',
'PreviewAttachment',
'Message',
'MessageExtraContent',
'PreviewAttachment',
'ErrorMessage',
'MessageLoadingProps',
'MessageInputProps',
'MessageAndActionsProps',
'MarkdownContent',
'QuickResponseProps',
'QuickStartTileProps',
'UserFeedback',
'UserFeedbackComplete',
'DeepThinking',
'ToolCall',
'ToolResponse',
'SourcesCard',
'ResponseActionsGroupsProps',
'ResponseActionProps',
'ActionProps',
'SourcesCardProps',
'UserFeedbackProps',
'UserFeedbackCompleteProps',
'QuickResponseProps'
'MessageAttachmentsContainerProps',
'MessageAttachmentItemProps',
'FileDetailsProps',
'FileDetailsLabelProps',
'MessageExtraContent',
'PreviewAttachment'
]
sortValue: 3
---

import Message from '@patternfly/chatbot/dist/dynamic/Message';
import Message, { ErrorMessage, MessageAndActions, MessageLoading, MessageAttachmentItem, MessageAttachmentsContainer } from '@patternfly/chatbot/dist/dynamic/Message';
import MarkdownContent from '@patternfly/chatbot/dist/dynamic/MarkdownContent';
import MessageDivider from '@patternfly/chatbot/dist/dynamic/MessageDivider';
import ToolCall from '@patternfly/chatbot/dist/dynamic/ToolCall';
import ResponseActions, { ResponseActionsGroups } from '@patternfly/chatbot/dist/dynamic/ResponseActions';
import ToolResponse from '@patternfly/chatbot/dist/dynamic/ToolResponse';
import { rehypeCodeBlockToggle } from '@patternfly/chatbot/dist/esm/Message/Plugins/rehypeCodeBlockToggle';
import SourcesCard from '@patternfly/chatbot/dist/dynamic/SourcesCard';
import { ArrowCircleDownIcon, ArrowRightIcon, CheckCircleIcon, CopyIcon, CubeIcon, CubesIcon, DownloadIcon, InfoCircleIcon, OutlinedQuestionCircleIcon, RedoIcon, RobotIcon, WrenchIcon } from '@patternfly/react-icons';
Expand Down Expand Up @@ -271,6 +287,35 @@ You can add custom content to specific parts of a `<Message>` via the `extraCont

```

### Custom message structure

For more advanced use cases, you can build completely custom message structures by passing children directly to `<Message>`. This approach is useful when you need to customize the order or structure of message elements beyond what the standard props allow.

When creating custom message structures, you must follow an intended composable structure.

1. **Message content and actions:** Wrap in `<MessageAndActions>`. This includes, but is not limited to:

- `<MarkdownContent>`: For rendering markdown or plain text content
- `<ErrorMessage>`
- `<MessageLoading>`
- `<MessageInput>`
- `<ToolCall>`
- `<ToolResponse>`
- `<DeepThinking>`
- `<QuickResponse>`
- `<QuickStartTile>`
- `<UserFeedback>` and `<UserFeedbackComplete>`
- `<SourcesCard>`
- `<ResponseActionsGroups>` and `<ResponseActions>`

2. **File attachments:** Placed outside `<MessageAndActions>` and wrapped in attachment containers:
- `<MessageAttachmentsContainer>`: Container for all attachments
- `<MessageAttachmentItem>`: Individual attachment wrapper (contains `<FileDetailsLabel>` or other attachment components)

```ts file="./MessageWithCustomStructure.tsx"

```

## File attachments

### Messages with attachments
Expand Down
9 changes: 7 additions & 2 deletions packages/module/src/MarkdownContent/MarkdownContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,15 @@ import SuperscriptMessage from '../Message/SuperscriptMessage/SuperscriptMessage
import { ButtonProps } from '@patternfly/react-core';
import { css } from '@patternfly/react-styles';

/**
* MarkdownContent renders content either as plain text or with content with markdown support.
*
* Use this component when passing children to Message to customize its structure.
*/
export interface MarkdownContentProps {
/** The markdown content to render */
/** The content to render. Supports markdown formatting by default, or plain text when isMarkdownDisabled is true. */
content?: string;
/** Disables markdown parsing, allowing only text input */
/** Disables markdown parsing, allowing only plain text input */
isMarkdownDisabled?: boolean;
/** Props for code blocks */
codeBlockProps?: CodeBlockMessageProps;
Expand Down
38 changes: 38 additions & 0 deletions packages/module/src/Message/ErrorMessage/ErrorMessage.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom';
import ErrorMessage from './ErrorMessage';

test('Renders with title', () => {
render(<ErrorMessage title="Error occurred" />);

expect(screen.getByText('Error occurred')).toBeVisible();
});

test('Renders with children', () => {
render(<ErrorMessage title="Error occurred">This is the error message body</ErrorMessage>);

expect(screen.getByText('This is the error message body')).toBeVisible();
});

test('Renders with action links', () => {
const actionLinks = (
<a href="#retry" data-testid="retry-link">
Retry action link
</a>
);
render(<ErrorMessage title="Error occurred" actionLinks={actionLinks} />);

expect(screen.getByText('Retry action link')).toBeVisible();
});

test('Renders with custom className', () => {
render(<ErrorMessage title="Error occurred" className="custom-error-class" />);

expect(screen.getByText('Error occurred').parentElement).toHaveClass('custom-error-class');
});

test('Renders with spread props', () => {
render(<ErrorMessage title="Error occurred" id="test-error-id" />);

expect(screen.getByText('Error occurred').parentElement).toHaveAttribute('id', 'test-error-id');
});
19 changes: 17 additions & 2 deletions packages/module/src/Message/ErrorMessage/ErrorMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,23 @@

import { Alert, AlertProps } from '@patternfly/react-core';

const ErrorMessage = ({ title, actionLinks, children, ...props }: AlertProps) => (
<Alert isInline variant="danger" title={title} actionLinks={actionLinks} {...props}>
/**
* ErrorMessage displays an inline danger alert for error states in messages.
* Use this component when passing children to Message to display error information.
*/
export interface ErrorMessageProps extends Partial<AlertProps> {
/** Content to display in the error alert body */
children?: React.ReactNode;
/** Additional classes for the error alert */
className?: string;
/** Title of the error alert */
title?: React.ReactNode;
/** Action links to display in the alert footer */
actionLinks?: React.ReactNode;
}

export const ErrorMessage = ({ title, actionLinks, children, className, ...props }: ErrorMessageProps) => (
<Alert isInline variant="danger" title={title} actionLinks={actionLinks} className={className} {...props}>
{children}
</Alert>
);
Expand Down
Loading
Loading