diff --git a/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/FilePreview.tsx b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/FilePreview.tsx
new file mode 100644
index 000000000..a3cc14960
--- /dev/null
+++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/FilePreview.tsx
@@ -0,0 +1,33 @@
+import { useState, FunctionComponent, MouseEvent as ReactMouseEvent } from 'react';
+import { Button, Checkbox } from '@patternfly/react-core';
+import FilePreview from '@patternfly/chatbot/dist/dynamic/FilePreview';
+
+export const AttachmentEditModalExample: FunctionComponent = () => {
+ const [isModalOpen, setIsModalOpen] = useState(false);
+ const [isCompact, setIsCompact] = useState(false);
+
+ const handleModalToggle = (_event: ReactMouseEvent | MouseEvent | KeyboardEvent) => {
+ setIsModalOpen(!isModalOpen);
+ };
+
+ return (
+ <>
+ setIsCompact(!isCompact)}
+ id="modal-compact-no-preview"
+ name="modal-compact-no-preview"
+ >
+
+
+ Preview unavailable
+
+ >
+ );
+};
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 8fff11d4b..a18895bf8 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
@@ -47,6 +47,7 @@ import { monitorSampleAppQuickStart } from '@patternfly/chatbot/src/Message/Quic
import userAvatar from './user_avatar.svg';
import squareImg from './PF-social-color-square.svg';
import { CSSProperties, useState, Fragment, FunctionComponent, MouseEvent as ReactMouseEvent, KeyboardEvent as ReactKeyboardEvent, Ref, isValidElement, cloneElement, Children, ReactNode, useRef, useEffect } from 'react';
+import FilePreview from '@patternfly/chatbot/dist/dynamic/FilePreview';
The `content` prop of the `` component is passed to a `` component (from [react-markdown](https://remarkjs.github.io/react-markdown/)), which is configured to translate plain text strings into PatternFly [`` components](/components/content) and code blocks into PatternFly [`` components.](/components/code-block)
@@ -255,6 +256,14 @@ To allow users to edit an attached file, load a new code editor within the ChatB
```
+### File preview
+
+If the contents of an attachment cannot be previewed, load a file preview modal with a view of the file name and an unavailable message. When users close the modal, return to the main ChatBot window.
+
+```js file="./FilePreview.tsx"
+
+```
+
### Failed attachment error
When an attachment upload fails, a [danger alert](/components/alert) is displayed to provide details about the reason for failure.
diff --git a/packages/module/patternfly-docs/content/extensions/chatbot/examples/UI/UI.md b/packages/module/patternfly-docs/content/extensions/chatbot/examples/UI/UI.md
index 1afae00ed..26fb43d15 100644
--- a/packages/module/patternfly-docs/content/extensions/chatbot/examples/UI/UI.md
+++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/UI/UI.md
@@ -87,6 +87,7 @@ import patternflyAvatar from '../Messages/patternfly_avatar.jpg';
import termsAndConditionsHeader from './PF-TermsAndConditionsHeader.svg';
import { CloseIcon, SearchIcon, OutlinedCommentsIcon } from '@patternfly/react-icons';
import { FunctionComponent, FormEvent, useState, useRef, MouseEvent, isValidElement, cloneElement, Children, ReactNode, Ref, MouseEvent as ReactMouseEvent, CSSProperties, useEffect} from 'react';
+import FilePreview from '@patternfly/chatbot/dist/dynamic/FilePreview';
## Structure
diff --git a/packages/module/src/FilePreview/FilePreview.scss b/packages/module/src/FilePreview/FilePreview.scss
new file mode 100644
index 000000000..90b9f80a3
--- /dev/null
+++ b/packages/module/src/FilePreview/FilePreview.scss
@@ -0,0 +1,22 @@
+.pf-chatbot__file-preview-body {
+ display: flex;
+ flex-direction: column;
+ gap: var(--pf-t--global--spacer--md);
+ align-items: center;
+ justify-content: center;
+}
+
+.pf-chatbot__file-preview-icon {
+ color: var(--pf-t--global--icon--color--subtle);
+ width: var(--pf-t--global--icon--size--2xl);
+ height: var(--pf-t--global--icon--size--2xl);
+}
+
+.pf-chatbot__file-preview-name {
+ font-size: var(--pf-t--global--font--size--xl);
+ font-weight: var(--pf-t--global--font--weight--heading--default);
+}
+.pf-chatbot__file-preview-body {
+ color: var(--pf-t--global--text--color--subtle);
+ font-size: var(--pf-t--global--font--size--body--lg);
+}
diff --git a/packages/module/src/FilePreview/FilePreview.test.tsx b/packages/module/src/FilePreview/FilePreview.test.tsx
new file mode 100644
index 000000000..e819d04df
--- /dev/null
+++ b/packages/module/src/FilePreview/FilePreview.test.tsx
@@ -0,0 +1,112 @@
+import { render, screen } from '@testing-library/react';
+import '@testing-library/jest-dom';
+import FilePreview from './FilePreview';
+import { ChatbotDisplayMode } from '../Chatbot';
+import { Button, ModalBodyProps, ModalHeaderProps } from '@patternfly/react-core';
+
+describe('FilePreview', () => {
+ const defaultProps = {
+ isModalOpen: true,
+ handleModalToggle: jest.fn(),
+ fileName: 'test-file.txt',
+ children: 'File content preview'
+ };
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should render with basic props', () => {
+ render();
+ expect(screen.getByText('File preview')).toBeInTheDocument();
+ expect(screen.getByText('test-file.txt')).toBeInTheDocument();
+ });
+
+ it('should render with custom title', () => {
+ const customTitle = 'Custom file preview title';
+ render();
+ expect(screen.getByRole('heading', { name: customTitle })).toBeTruthy();
+ });
+
+ it('should handle modal toggle when closed', () => {
+ const mockToggle = jest.fn();
+ render();
+ expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
+ });
+
+ it('should apply default display mode class', () => {
+ render();
+ const modal = screen.getByRole('dialog');
+ expect(modal).toHaveClass('pf-chatbot__file-preview-modal--default');
+ });
+
+ it('should apply custom display mode class', () => {
+ render();
+ const modal = screen.getByRole('dialog');
+ expect(modal).toHaveClass('pf-chatbot__file-preview-modal--fullscreen');
+ });
+
+ it('should apply compact styling when isCompact is true', () => {
+ render();
+ const modal = screen.getByRole('dialog');
+ expect(modal).toHaveClass('pf-m-compact');
+ });
+
+ it('should not apply compact styling when isCompact is false', () => {
+ render();
+ const modal = screen.getByRole('dialog');
+ expect(modal).not.toHaveClass('pf-m-compact');
+ });
+
+ it('should apply custom className', () => {
+ const customClass = 'custom-file-preview';
+ render();
+ const modal = screen.getByRole('dialog');
+ expect(modal).toHaveClass(customClass);
+ });
+
+ it('should pass through additional props to ChatbotModal', () => {
+ render();
+ const modal = screen.getByTestId('file-preview-modal');
+ expect(modal).toBeInTheDocument();
+ });
+
+ it('should pass modalHeaderProps to ModalHeader', () => {
+ const modalHeaderProps = {
+ 'data-testid': 'custom-header'
+ } as ModalHeaderProps;
+ render();
+ const header = screen.getByTestId('custom-header');
+ expect(header).toBeInTheDocument();
+ });
+
+ it('should pass modalBodyProps to ModalBody', () => {
+ const modalBodyProps = {
+ 'data-testid': 'custom-body'
+ } as ModalBodyProps;
+ render();
+ const body = screen.getByTestId('custom-body');
+ expect(body).toBeInTheDocument();
+ });
+
+ it('should pass ouiaId to ChatbotModal', () => {
+ const ouiaId = 'file-preview-ouia-id';
+ render();
+ const modal = screen.getByRole('dialog');
+ expect(modal).toHaveAttribute('data-ouia-component-id', ouiaId);
+ });
+
+ it('should handle complex children', () => {
+ const complexChildren = (
+
+
File details
+
Size: 1.2 MB
+
+
+ );
+ render({complexChildren});
+ expect(screen.getByRole('heading', { name: /File details/i })).toBeTruthy();
+ expect(screen.getByText('Size: 1.2 MB')).toBeTruthy();
+ expect(screen.getByRole('button', { name: /Download/i })).toBeTruthy();
+ });
+});
diff --git a/packages/module/src/FilePreview/FilePreview.tsx b/packages/module/src/FilePreview/FilePreview.tsx
new file mode 100644
index 000000000..adbc53ffa
--- /dev/null
+++ b/packages/module/src/FilePreview/FilePreview.tsx
@@ -0,0 +1,58 @@
+import { ModalBody, ModalBodyProps, ModalHeader, ModalHeaderProps } from '@patternfly/react-core';
+import type { FunctionComponent } from 'react';
+import { ChatbotDisplayMode } from '../Chatbot';
+import ChatbotModal, { ChatbotModalProps } from '../ChatbotModal';
+import { FileIcon } from '@patternfly/react-icons';
+
+export interface FilePreviewProps extends ChatbotModalProps {
+ /** Class applied to modal */
+ className?: string;
+ /** Function that handles modal toggle */
+ handleModalToggle: (event: React.MouseEvent | MouseEvent | KeyboardEvent) => void;
+ /** Whether modal is open */
+ isModalOpen: boolean;
+ /** Title of modal */
+ title?: string;
+ /** Display mode for the Chatbot parent; this influences the styles applied */
+ displayMode?: ChatbotDisplayMode;
+ /** File name */
+ fileName: string;
+ /** Sets modal to compact styling. */
+ isCompact?: boolean;
+ /** Additional props passed to modal header */
+ modalHeaderProps?: ModalHeaderProps;
+ /** Additional props passed to modal body */
+ modalBodyProps?: ModalBodyProps;
+}
+
+const FilePreview: FunctionComponent = ({
+ isModalOpen,
+ displayMode = ChatbotDisplayMode.default,
+ children,
+ fileName,
+ isCompact,
+ className,
+ handleModalToggle,
+ title = 'File preview',
+ modalHeaderProps,
+ modalBodyProps,
+ ...props
+}: FilePreviewProps) => (
+
+
+
+
+ {fileName}
+ {children && {children}
}
+
+
+);
+
+export default FilePreview;
diff --git a/packages/module/src/FilePreview/index.ts b/packages/module/src/FilePreview/index.ts
new file mode 100644
index 000000000..9d51281e8
--- /dev/null
+++ b/packages/module/src/FilePreview/index.ts
@@ -0,0 +1,3 @@
+export { default } from './FilePreview';
+
+export * from './FilePreview';
diff --git a/packages/module/src/index.ts b/packages/module/src/index.ts
index 98167b41d..84eacae53 100644
--- a/packages/module/src/index.ts
+++ b/packages/module/src/index.ts
@@ -51,6 +51,9 @@ export * from './FileDetailsLabel';
export { default as FileDropZone } from './FileDropZone';
export * from './FileDropZone';
+export { default as FilePreview } from './FilePreview';
+export * from './FilePreview';
+
export { default as LoadingMessage } from './LoadingMessage';
export * from './LoadingMessage';
diff --git a/packages/module/src/main.scss b/packages/module/src/main.scss
index 699a59bd1..3f6a43ab4 100644
--- a/packages/module/src/main.scss
+++ b/packages/module/src/main.scss
@@ -14,6 +14,7 @@
@import './FileDetails/FileDetails';
@import './FileDetailsLabel/FileDetailsLabel';
@import './FileDropZone/FileDropZone';
+@import './FilePreview/FilePreview.scss';
@import './Message/Message';
@import './Message/CodeBlockMessage/CodeBlockMessage';
@import './Message/ImageMessage/ImageMessage';