diff --git a/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithCustomStructure.tsx b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithCustomStructure.tsx new file mode 100644 index 000000000..e49187b9d --- /dev/null +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithCustomStructure.tsx @@ -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 = () => ( + <> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +); diff --git a/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/Messages.md b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/Messages.md index a42378d1e..c6e167960 100644 --- a/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/Messages.md +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/Messages.md @@ -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'; @@ -271,6 +287,35 @@ You can add custom content to specific parts of a `` via the `extraCont ``` +### Custom message structure + +For more advanced use cases, you can build completely custom message structures by passing children directly to ``. 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 ``. This includes, but is not limited to: + + - ``: For rendering markdown or plain text content + - `` + - `` + - `` + - `` + - `` + - `` + - `` + - `` + - `` and `` + - `` + - `` and `` + +2. **File attachments:** Placed outside `` and wrapped in attachment containers: + - ``: Container for all attachments + - ``: Individual attachment wrapper (contains `` or other attachment components) + +```ts file="./MessageWithCustomStructure.tsx" + +``` + ## File attachments ### Messages with attachments diff --git a/packages/module/src/MarkdownContent/MarkdownContent.tsx b/packages/module/src/MarkdownContent/MarkdownContent.tsx index f852ea003..5b158b881 100644 --- a/packages/module/src/MarkdownContent/MarkdownContent.tsx +++ b/packages/module/src/MarkdownContent/MarkdownContent.tsx @@ -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; diff --git a/packages/module/src/Message/ErrorMessage/ErrorMessage.test.tsx b/packages/module/src/Message/ErrorMessage/ErrorMessage.test.tsx new file mode 100644 index 000000000..5b97acb2d --- /dev/null +++ b/packages/module/src/Message/ErrorMessage/ErrorMessage.test.tsx @@ -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(); + + expect(screen.getByText('Error occurred')).toBeVisible(); +}); + +test('Renders with children', () => { + render(This is the error message body); + + expect(screen.getByText('This is the error message body')).toBeVisible(); +}); + +test('Renders with action links', () => { + const actionLinks = ( + + Retry action link + + ); + render(); + + expect(screen.getByText('Retry action link')).toBeVisible(); +}); + +test('Renders with custom className', () => { + render(); + + expect(screen.getByText('Error occurred').parentElement).toHaveClass('custom-error-class'); +}); + +test('Renders with spread props', () => { + render(); + + expect(screen.getByText('Error occurred').parentElement).toHaveAttribute('id', 'test-error-id'); +}); diff --git a/packages/module/src/Message/ErrorMessage/ErrorMessage.tsx b/packages/module/src/Message/ErrorMessage/ErrorMessage.tsx index 000f2a7bc..229bb75e3 100644 --- a/packages/module/src/Message/ErrorMessage/ErrorMessage.tsx +++ b/packages/module/src/Message/ErrorMessage/ErrorMessage.tsx @@ -4,8 +4,23 @@ import { Alert, AlertProps } from '@patternfly/react-core'; -const ErrorMessage = ({ title, actionLinks, children, ...props }: AlertProps) => ( - +/** + * 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 { + /** 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) => ( + {children} ); diff --git a/packages/module/src/Message/Message.tsx b/packages/module/src/Message/Message.tsx index 8b298c325..90777d81a 100644 --- a/packages/module/src/Message/Message.tsx +++ b/packages/module/src/Message/Message.tsx @@ -67,6 +67,8 @@ export interface MessageExtraContent { } 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 */ @@ -193,6 +195,7 @@ export interface MessageProps extends Omit, 'role'> { } export const MessageBase: FunctionComponent = ({ + children, role, content, extraContent, @@ -341,74 +344,82 @@ export const MessageBase: FunctionComponent = ({ {timestamp}
-
- {renderMessage()} - {afterMainContent && <>{afterMainContent}} - {toolResponse && } - {deepThinking && } - {toolCall && } - {!isLoading && sources && } - {quickStarts && quickStarts.quickStart && ( - - )} - {!isLoading && !isEditable && actions && ( - <> - {Array.isArray(actions) ? ( -
- {actions.map((actionGroup, index) => ( - - ))} -
- ) : ( - + {children ? ( + <>{children} + ) : ( + <> +
+ {renderMessage()} + {afterMainContent && <>{afterMainContent}} + {toolResponse && } + {deepThinking && } + {toolCall && } + {!isLoading && sources && } + {quickStarts && quickStarts.quickStart && ( + + )} + {!isLoading && !isEditable && actions && ( + <> + {Array.isArray(actions) ? ( +
+ {actions.map((actionGroup, index) => ( + + ))} +
+ ) : ( + + )} + + )} + {userFeedbackForm && ( + )} - - )} - {userFeedbackForm && } - {userFeedbackComplete && ( - - )} - {!isLoading && quickResponses && ( - - )} -
- {attachments && ( -
- {attachments.map((attachment) => ( -
- + )} + {!isLoading && quickResponses && ( + + )} +
+ {attachments && ( +
+ {attachments.map((attachment) => ( +
+ +
+ ))}
- ))} -
+ )} + {!isLoading && endContent && <>{endContent}} + )} - {!isLoading && endContent && <>{endContent}}
diff --git a/packages/module/src/Message/MessageAndActions/MessageAndActions.test.tsx b/packages/module/src/Message/MessageAndActions/MessageAndActions.test.tsx new file mode 100644 index 000000000..21032240e --- /dev/null +++ b/packages/module/src/Message/MessageAndActions/MessageAndActions.test.tsx @@ -0,0 +1,23 @@ +import '@testing-library/jest-dom'; +import { render, screen } from '@testing-library/react'; +import MessageAndActions from './MessageAndActions'; + +test('Renders with children', () => { + render(Test content); + expect(screen.getByText('Test content')).toBeInTheDocument(); +}); + +test('Renders with pf-chatbot__message-and-actions class by default', () => { + render(Test content); + expect(screen.getByText('Test content')).toHaveClass('pf-chatbot__message-and-actions', { exact: true }); +}); + +test('Renders with custom className', () => { + render(Test content); + expect(screen.getByText('Test content')).toHaveClass('custom-class'); +}); + +test('Spreads additional props', () => { + render(Test content); + expect(screen.getByText('Test content')).toHaveAttribute('id', 'test-id'); +}); diff --git a/packages/module/src/Message/MessageAndActions/MessageAndActions.tsx b/packages/module/src/Message/MessageAndActions/MessageAndActions.tsx new file mode 100644 index 000000000..5547f7078 --- /dev/null +++ b/packages/module/src/Message/MessageAndActions/MessageAndActions.tsx @@ -0,0 +1,22 @@ +import { FunctionComponent, HTMLProps, ReactNode } from 'react'; +import { css } from '@patternfly/react-styles'; + +/** + * The container that wraps the primary message content and inline actions, such as ToolCall, ToolResponse, DeepThinking, ResponseActions, etc. + * Attachments should not be rendered inside this container. + * Use this component when passing children to Message to customize its structure. + */ +export interface MessageAndActionsProps extends HTMLProps { + /** Content to render inside the message and actions container. */ + children: ReactNode; + /** Additional classes applied to the message and actions container. */ + className?: string; +} + +export const MessageAndActions: FunctionComponent = ({ children, className, ...props }) => ( +
+ {children} +
+); + +export default MessageAndActions; diff --git a/packages/module/src/Message/MessageAndActions/index.ts b/packages/module/src/Message/MessageAndActions/index.ts new file mode 100644 index 000000000..4b01abd71 --- /dev/null +++ b/packages/module/src/Message/MessageAndActions/index.ts @@ -0,0 +1 @@ +export * from './MessageAndActions'; diff --git a/packages/module/src/Message/MessageAttachments/MessageAttachmentItem.test.tsx b/packages/module/src/Message/MessageAttachments/MessageAttachmentItem.test.tsx new file mode 100644 index 000000000..49e58d726 --- /dev/null +++ b/packages/module/src/Message/MessageAttachments/MessageAttachmentItem.test.tsx @@ -0,0 +1,23 @@ +import '@testing-library/jest-dom'; +import { render, screen } from '@testing-library/react'; +import MessageAttachmentItem from './MessageAttachmentItem'; + +test('Renders with children', () => { + render(Test content); + expect(screen.getByText('Test content')).toBeInTheDocument(); +}); + +test('Renders with pf-chatbot__message-attachment class by default', () => { + render(Test content); + expect(screen.getByText('Test content')).toHaveClass('pf-chatbot__message-attachment', { exact: true }); +}); + +test('Renders with custom className', () => { + render(Test content); + expect(screen.getByText('Test content')).toHaveClass('custom-class'); +}); + +test('Spreads additional props', () => { + render(Test content); + expect(screen.getByText('Test content')).toHaveAttribute('id', 'test-id'); +}); diff --git a/packages/module/src/Message/MessageAttachments/MessageAttachmentItem.tsx b/packages/module/src/Message/MessageAttachments/MessageAttachmentItem.tsx new file mode 100644 index 000000000..3d4a8abd9 --- /dev/null +++ b/packages/module/src/Message/MessageAttachments/MessageAttachmentItem.tsx @@ -0,0 +1,25 @@ +import { FunctionComponent, HTMLProps, ReactNode } from 'react'; +import { css } from '@patternfly/react-styles'; + +/** + * The container for a single message attachment item, typically the FileDetailsLabel component. You must wrap any attachment components in this container. + * Use this component within MessageAttachmentsContainer when passing children to Message to customize its structure. + */ +export interface MessageAttachmentItemProps extends HTMLProps { + /** Content to render inside a single attachment container */ + children: ReactNode; + /** Additional classes applied to the attachment container. */ + className?: string; +} + +export const MessageAttachmentItem: FunctionComponent = ({ + children, + className, + ...props +}) => ( +
+ {children} +
+); + +export default MessageAttachmentItem; diff --git a/packages/module/src/Message/MessageAttachments/MessageAttachmentsContainer.test.tsx b/packages/module/src/Message/MessageAttachments/MessageAttachmentsContainer.test.tsx new file mode 100644 index 000000000..88b3776e9 --- /dev/null +++ b/packages/module/src/Message/MessageAttachments/MessageAttachmentsContainer.test.tsx @@ -0,0 +1,23 @@ +import '@testing-library/jest-dom'; +import { render, screen } from '@testing-library/react'; +import MessageAttachmentsContainer from './MessageAttachmentsContainer'; + +test('Renders with children', () => { + render(Test content); + expect(screen.getByText('Test content')).toBeInTheDocument(); +}); + +test('Renders with pf-chatbot__message-attachments-container class by default', () => { + render(Test content); + expect(screen.getByText('Test content')).toHaveClass('pf-chatbot__message-attachments-container', { exact: true }); +}); + +test('Renders with custom className', () => { + render(Test content); + expect(screen.getByText('Test content')).toHaveClass('custom-class'); +}); + +test('Spreads additional props', () => { + render(Test content); + expect(screen.getByText('Test content')).toHaveAttribute('id', 'test-id'); +}); diff --git a/packages/module/src/Message/MessageAttachments/MessageAttachmentsContainer.tsx b/packages/module/src/Message/MessageAttachments/MessageAttachmentsContainer.tsx new file mode 100644 index 000000000..ffe31ca65 --- /dev/null +++ b/packages/module/src/Message/MessageAttachments/MessageAttachmentsContainer.tsx @@ -0,0 +1,25 @@ +import { FunctionComponent, HTMLProps, ReactNode } from 'react'; +import { css } from '@patternfly/react-styles'; + +/** + * The container to wrap MessageAttachment components. You must wrap any MessageAttachment components in this container. + * Use this component when passing children to Message to customize its structure. + */ +export interface MessageAttachmentsContainerProps extends HTMLProps { + /** Content to render inside the attachments container */ + children: ReactNode; + /** Additional classes applied to the attachments container. */ + className?: string; +} + +export const MessageAttachmentsContainer: FunctionComponent = ({ + children, + className, + ...props +}) => ( +
+ {children} +
+); + +export default MessageAttachmentsContainer; diff --git a/packages/module/src/Message/MessageAttachments/index.ts b/packages/module/src/Message/MessageAttachments/index.ts new file mode 100644 index 000000000..95b4fbae1 --- /dev/null +++ b/packages/module/src/Message/MessageAttachments/index.ts @@ -0,0 +1,2 @@ +export * from './MessageAttachmentItem'; +export * from './MessageAttachmentsContainer'; diff --git a/packages/module/src/Message/MessageInput.tsx b/packages/module/src/Message/MessageInput.tsx index 02f42c6b0..9cad0eb11 100644 --- a/packages/module/src/Message/MessageInput.tsx +++ b/packages/module/src/Message/MessageInput.tsx @@ -22,7 +22,7 @@ export interface MessageInputProps extends FormProps { content?: string; } -const MessageInput: FunctionComponent = ({ +export const MessageInput: FunctionComponent = ({ editPlaceholder = 'Edit prompt message...', updateWord = 'Update', cancelWord = 'Cancel', diff --git a/packages/module/src/Message/MessageLoading.test.tsx b/packages/module/src/Message/MessageLoading.test.tsx new file mode 100644 index 000000000..c610bd56a --- /dev/null +++ b/packages/module/src/Message/MessageLoading.test.tsx @@ -0,0 +1,23 @@ +import '@testing-library/jest-dom'; +import { render, screen } from '@testing-library/react'; +import MessageLoading from './MessageLoading'; + +test('Renders with pf-chatbot__message-loading class by default', () => { + render(); + expect(screen.getByTestId('test-id')).toHaveClass('pf-chatbot__message-loading', { exact: true }); +}); + +test('Renders with pf-m-primary class when isPrimary is true', () => { + render(); + expect(screen.getByTestId('test-id')).toHaveClass('pf-chatbot__message-loading pf-m-primary'); +}); + +test('Renders loading word when loadingWord is passed', () => { + render(); + expect(screen.getByText('Loading message')).toBeInTheDocument(); +}); + +test('Spreads additional props', () => { + render(); + expect(screen.getByTestId('test-id')).toHaveAttribute('id', 'custom-id'); +}); diff --git a/packages/module/src/Message/MessageLoading.tsx b/packages/module/src/Message/MessageLoading.tsx index 96f8015fa..61faa0c18 100644 --- a/packages/module/src/Message/MessageLoading.tsx +++ b/packages/module/src/Message/MessageLoading.tsx @@ -2,8 +2,23 @@ // Chatbot Main - Message - Processing // ============================================================================ -const MessageLoading = ({ loadingWord, isPrimary }) => ( -
+import { FunctionComponent } from 'react'; +import type { HTMLProps } from 'react'; +import { css } from '@patternfly/react-styles'; + +/** + * MessageLoading displays a loading animation for messages. + * Use this component when passing children to Message to show a loading state. + */ +export interface MessageLoadingProps extends HTMLProps { + /** Text announced to screen readers during loading. */ + loadingWord?: string; + /** Flag indicating whether primary styling is applied */ + isPrimary?: boolean; +} + +export const MessageLoading: FunctionComponent = ({ loadingWord, isPrimary, ...props }) => ( +
{loadingWord} diff --git a/packages/module/src/Message/QuickResponse/QuickResponse.test.tsx b/packages/module/src/Message/QuickResponse/QuickResponse.test.tsx new file mode 100644 index 000000000..67bbf2076 --- /dev/null +++ b/packages/module/src/Message/QuickResponse/QuickResponse.test.tsx @@ -0,0 +1,131 @@ +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import '@testing-library/jest-dom'; +import QuickResponse from './QuickResponse'; + +test('Renders with quick responses', () => { + const quickResponses = [ + { id: '1', content: 'Response 1' }, + { id: '2', content: 'Response 2' }, + { id: '3', content: 'Response 3' } + ]; + render(); + + expect(screen.getByText('Response 1')).toBeVisible(); + expect(screen.getByText('Response 2')).toBeVisible(); + expect(screen.getByText('Response 3')).toBeVisible(); +}); + +test('Renders with compact styling', () => { + const quickResponses = [{ id: '1', content: 'Compact response' }]; + render(); + + expect(screen.getByText('Compact response').closest('.pf-v6-c-label')).toHaveClass('pf-m-compact'); +}); + +test('Renders with custom className on response', () => { + const quickResponses = [{ id: '1', content: 'Custom class response', className: 'custom-response-class' }]; + render(); + + expect(screen.getByText('Custom class response').closest('.pf-v6-c-label')).toHaveClass('custom-response-class'); +}); + +test('Renders with custom container className', () => { + const quickResponses = [ + { id: '1', content: 'Response 1' }, + { id: '2', content: 'Response 2' } + ]; + render( + + ); + + expect(screen.getByText('Response 1').closest('.pf-v6-c-label-group')).toHaveClass('custom-container-class'); +}); + +test('Spreads additional custom container props', () => { + const quickResponses = [ + { id: '1', content: 'Response 1' }, + { id: '2', content: 'Response 2' } + ]; + render(); + + expect(screen.getByText('Response 1').closest('.pf-v6-c-label-group__list')).toHaveAttribute( + 'id', + 'custom-container-id' + ); +}); + +test('Renders with pf-chatbot__message-quick-response--selected class after click', async () => { + const user = userEvent.setup(); + const quickResponses = [ + { id: '1', content: 'Response 1' }, + { id: '2', content: 'Response 2' } + ]; + render(); + + await user.click(screen.getByText('Response 1')); + + expect(screen.getByText('Response 1').closest('.pf-v6-c-label')).toHaveClass( + 'pf-chatbot__message-quick-response--selected' + ); +}); + +test('Does not calls onClick handler when not passed', async () => { + const user = userEvent.setup(); + const handleClick = jest.fn(); + const quickResponses = [{ id: '1', content: 'Clickable response' }]; + render(); + + await user.click(screen.getByText('Clickable response')); + + expect(handleClick).not.toHaveBeenCalled(); +}); + +test('Calls onClick handler when passed', async () => { + const user = userEvent.setup(); + const handleClick = jest.fn(); + const quickResponses = [{ id: '1', content: 'Clickable response', onClick: handleClick }]; + render(); + + await user.click(screen.getByText('Clickable response')); + + expect(handleClick).toHaveBeenCalled(); +}); + +test('Does not call onSelect when not passed', async () => { + const user = userEvent.setup(); + const handleSelect = jest.fn(); + const quickResponses = [ + { id: '1', content: 'Response 1' }, + { id: '2', content: 'Response 2' } + ]; + render(); + + await user.click(screen.getByText('Response 2')); + + expect(handleSelect).not.toHaveBeenCalled(); +}); + +test('Calls onSelect when passed', async () => { + const user = userEvent.setup(); + const handleSelect = jest.fn(); + const quickResponses = [ + { id: '1', content: 'Response 1' }, + { id: '2', content: 'Response 2' } + ]; + render(); + + await user.click(screen.getByText('Response 2')); + + expect(handleSelect).toHaveBeenCalledWith('2'); +}); + +test('Spreads additional response props', () => { + const quickResponses = [{ id: '1', content: 'Response with props', isCompact: true, 'aria-label': 'Test label' }]; + render(); + + expect(screen.getByText('Response with props').closest('.pf-v6-c-label')).toHaveAttribute('aria-label', 'Test label'); +}); diff --git a/packages/module/src/Message/QuickResponse/QuickResponse.tsx b/packages/module/src/Message/QuickResponse/QuickResponse.tsx index 36e62cdbd..033cdd9fa 100644 --- a/packages/module/src/Message/QuickResponse/QuickResponse.tsx +++ b/packages/module/src/Message/QuickResponse/QuickResponse.tsx @@ -2,6 +2,7 @@ import type { FunctionComponent } from 'react'; import { useState } from 'react'; import { Label, LabelGroup, LabelGroupProps, LabelProps } from '@patternfly/react-core'; import { CheckIcon } from '@patternfly/react-icons'; +import { css } from '@patternfly/react-styles'; export interface QuickResponse extends Omit { content: string; @@ -35,7 +36,7 @@ export const QuickResponse: FunctionComponent = ({ }; return ( {quickResponses.map(({ id, onClick, content, className, ...props }: QuickResponse) => ( @@ -45,7 +46,7 @@ export const QuickResponse: FunctionComponent = ({ color="blue" key={id} onClick={() => handleQuickResponseClick(id, onClick)} - className={`${id === selectedQuickResponse ? 'pf-chatbot__message-quick-response--selected' : ''} ${className ? className : ''}`} + className={css(id === selectedQuickResponse && 'pf-chatbot__message-quick-response--selected', className)} isCompact={isCompact} {...props} > diff --git a/packages/module/src/Message/QuickResponse/index.ts b/packages/module/src/Message/QuickResponse/index.ts new file mode 100644 index 000000000..edcff972b --- /dev/null +++ b/packages/module/src/Message/QuickResponse/index.ts @@ -0,0 +1 @@ +export * from './QuickResponse'; diff --git a/packages/module/src/Message/QuickStarts/QuickStartTile.tsx b/packages/module/src/Message/QuickStarts/QuickStartTile.tsx index ae8528f5d..c154674e7 100644 --- a/packages/module/src/Message/QuickStarts/QuickStartTile.tsx +++ b/packages/module/src/Message/QuickStarts/QuickStartTile.tsx @@ -53,7 +53,7 @@ export interface QuickStartTileProps { isCompact?: boolean; } -const QuickStartTile: FC = ({ +export const QuickStartTile: FC = ({ className, quickStart, onClick, diff --git a/packages/module/src/Message/QuickStarts/index.ts b/packages/module/src/Message/QuickStarts/index.ts new file mode 100644 index 000000000..a7688a023 --- /dev/null +++ b/packages/module/src/Message/QuickStarts/index.ts @@ -0,0 +1,2 @@ +export * from './QuickStartTile'; +export * from './types'; diff --git a/packages/module/src/Message/UserFeedback/UserFeedback.tsx b/packages/module/src/Message/UserFeedback/UserFeedback.tsx index f71854b99..39a524161 100644 --- a/packages/module/src/Message/UserFeedback/UserFeedback.tsx +++ b/packages/module/src/Message/UserFeedback/UserFeedback.tsx @@ -78,7 +78,7 @@ export interface UserFeedbackProps extends Omit, OUIAProp privacyStatement?: string; } -const UserFeedback: FunctionComponent = ({ +export const UserFeedback: FunctionComponent = ({ className, timestamp, title = 'Why did you choose this rating?', diff --git a/packages/module/src/Message/UserFeedback/UserFeedbackComplete.tsx b/packages/module/src/Message/UserFeedback/UserFeedbackComplete.tsx index 4fc50abc1..6b13e3751 100644 --- a/packages/module/src/Message/UserFeedback/UserFeedbackComplete.tsx +++ b/packages/module/src/Message/UserFeedback/UserFeedbackComplete.tsx @@ -2,10 +2,7 @@ // Chatbot Main - Messages - Feedback Complete Card // ============================================================================ import type { MouseEvent as ReactMouseEvent, FunctionComponent } from 'react'; - import { useState, useRef, useEffect } from 'react'; - -// Import PatternFly components import { Card, CardBody, CardHeader, CardProps, CardTitle, OUIAProps, useOUIAProps } from '@patternfly/react-core'; import CloseButton from './CloseButton'; @@ -48,7 +45,7 @@ export interface UserFeedbackCompleteProps extends Omit, OUIAP timestamp?: string; } -const UserFeedbackComplete: FunctionComponent = ({ +export const UserFeedbackComplete: FunctionComponent = ({ className, title = 'Feedback submitted', body = "We've received your response. Thank you for sharing your feedback!", diff --git a/packages/module/src/Message/UserFeedback/index.ts b/packages/module/src/Message/UserFeedback/index.ts new file mode 100644 index 000000000..26df8cba9 --- /dev/null +++ b/packages/module/src/Message/UserFeedback/index.ts @@ -0,0 +1,2 @@ +export * from './UserFeedback'; +export * from './UserFeedbackComplete'; diff --git a/packages/module/src/Message/index.ts b/packages/module/src/Message/index.ts index d62ce192d..606605d8a 100644 --- a/packages/module/src/Message/index.ts +++ b/packages/module/src/Message/index.ts @@ -1,4 +1,12 @@ export { default } from './Message'; export { rehypeCodeBlockToggle } from './Plugins/rehypeCodeBlockToggle'; +export * from './ErrorMessage/ErrorMessage'; +export * from './MessageAndActions'; +export * from './MessageAttachments'; export * from './Message'; +export * from './MessageLoading'; +export * from './MessageInput'; +export * from './QuickResponse'; +export * from './QuickStarts'; +export * from './UserFeedback'; diff --git a/packages/module/src/ResponseActions/ResponseActions.tsx b/packages/module/src/ResponseActions/ResponseActions.tsx index 7598f9227..c1cfbcff8 100644 --- a/packages/module/src/ResponseActions/ResponseActions.tsx +++ b/packages/module/src/ResponseActions/ResponseActions.tsx @@ -42,6 +42,12 @@ export interface ActionProps extends Omit { type ExtendedActionProps = ActionProps & { [key: string]: any; }; + +/** + * The various actions that can be attached to a bot message for users to interact with. + * Use this component when passing children to Message to customize its structure. + */ + export interface ResponseActionProps { /** Props for message actions, such as feedback (positive or negative), copy button, share, and listen */ actions: Record & { diff --git a/packages/module/src/ResponseActions/ResponseActionsGroups.test.tsx b/packages/module/src/ResponseActions/ResponseActionsGroups.test.tsx new file mode 100644 index 000000000..be14f6c6e --- /dev/null +++ b/packages/module/src/ResponseActions/ResponseActionsGroups.test.tsx @@ -0,0 +1,23 @@ +import '@testing-library/jest-dom'; +import { render, screen } from '@testing-library/react'; +import ResponseActionsGroups from './ResponseActionsGroups'; + +test('Renders with children', () => { + render(Test content); + expect(screen.getByText('Test content')).toBeInTheDocument(); +}); + +test('Renders with pf-chatbot__response-actions-groups class by default', () => { + render(Test content); + expect(screen.getByText('Test content')).toHaveClass('pf-chatbot__response-actions-groups', { exact: true }); +}); + +test('Renders with custom className', () => { + render(Test content); + expect(screen.getByText('Test content')).toHaveClass('custom-class'); +}); + +test('Spreads additional props', () => { + render(Test content); + expect(screen.getByText('Test content')).toHaveAttribute('id', 'test-id'); +}); diff --git a/packages/module/src/ResponseActions/ResponseActionsGroups.tsx b/packages/module/src/ResponseActions/ResponseActionsGroups.tsx new file mode 100644 index 000000000..602654782 --- /dev/null +++ b/packages/module/src/ResponseActions/ResponseActionsGroups.tsx @@ -0,0 +1,28 @@ +// ============================================================================ +// Response Actions Groups - Container for multiple action groups +// ============================================================================ +import { FunctionComponent, HTMLProps, ReactNode } from 'react'; +import { css } from '@patternfly/react-styles'; + +/** + * The container for grouping multiple related ResponseActions components, typically used for having different persistence states amongst groups. + * Use this component when passing children to Message to customize its structure. + */ +export interface ResponseActionsGroupsProps extends HTMLProps { + /** Content to render inside the response actions groups container */ + children: ReactNode; + /** Additional classes applied to the response actions groups container. */ + className?: string; +} + +export const ResponseActionsGroups: FunctionComponent = ({ + children, + className, + ...props +}) => ( +
+ {children} +
+); + +export default ResponseActionsGroups; diff --git a/packages/module/src/ResponseActions/index.ts b/packages/module/src/ResponseActions/index.ts index c429555cb..08a5e9496 100644 --- a/packages/module/src/ResponseActions/index.ts +++ b/packages/module/src/ResponseActions/index.ts @@ -1,3 +1,4 @@ export { default } from './ResponseActions'; export * from './ResponseActions'; +export * from './ResponseActionsGroups';