@patternfly/chatbot 6.4.0-prerelease.20 → 6.4.0-prerelease.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/dist/cjs/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.js +1 -1
  2. package/dist/cjs/FileDetails/FileDetails.d.ts +22 -3
  3. package/dist/cjs/FileDetails/FileDetails.js +27 -912
  4. package/dist/cjs/FileDetails/FileDetails.test.js +16 -0
  5. package/dist/cjs/FileDetailsLabel/FileDetailsLabel.d.ts +8 -2
  6. package/dist/cjs/FileDetailsLabel/FileDetailsLabel.js +14 -2
  7. package/dist/cjs/FileDetailsLabel/FileDetailsLabel.test.js +19 -1
  8. package/dist/cjs/ImagePreview/ImagePreview.d.ts +53 -0
  9. package/dist/cjs/ImagePreview/ImagePreview.js +47 -0
  10. package/dist/cjs/ImagePreview/ImagePreview.test.d.ts +1 -0
  11. package/dist/cjs/ImagePreview/ImagePreview.test.js +225 -0
  12. package/dist/cjs/ImagePreview/index.d.ts +2 -0
  13. package/dist/cjs/ImagePreview/index.js +23 -0
  14. package/dist/cjs/Message/Message.d.ts +3 -0
  15. package/dist/cjs/Message/Message.js +3 -2
  16. package/dist/cjs/MessageBox/MessageBox.js +1 -1
  17. package/dist/cjs/ToolCall/ToolCall.d.ts +44 -0
  18. package/dist/cjs/ToolCall/ToolCall.js +14 -0
  19. package/dist/cjs/ToolCall/ToolCall.test.d.ts +1 -0
  20. package/dist/cjs/ToolCall/ToolCall.test.js +144 -0
  21. package/dist/cjs/ToolCall/index.d.ts +2 -0
  22. package/dist/cjs/ToolCall/index.js +23 -0
  23. package/dist/cjs/index.d.ts +4 -0
  24. package/dist/cjs/index.js +7 -1
  25. package/dist/css/main.css +104 -19
  26. package/dist/css/main.css.map +1 -1
  27. package/dist/dynamic/ImagePreview/package.json +1 -0
  28. package/dist/dynamic/ToolCall/package.json +1 -0
  29. package/dist/esm/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.js +1 -1
  30. package/dist/esm/FileDetails/FileDetails.d.ts +22 -3
  31. package/dist/esm/FileDetails/FileDetails.js +27 -912
  32. package/dist/esm/FileDetails/FileDetails.test.js +16 -0
  33. package/dist/esm/FileDetailsLabel/FileDetailsLabel.d.ts +8 -2
  34. package/dist/esm/FileDetailsLabel/FileDetailsLabel.js +14 -2
  35. package/dist/esm/FileDetailsLabel/FileDetailsLabel.test.js +19 -1
  36. package/dist/esm/ImagePreview/ImagePreview.d.ts +53 -0
  37. package/dist/esm/ImagePreview/ImagePreview.js +42 -0
  38. package/dist/esm/ImagePreview/ImagePreview.test.d.ts +1 -0
  39. package/dist/esm/ImagePreview/ImagePreview.test.js +220 -0
  40. package/dist/esm/ImagePreview/index.d.ts +2 -0
  41. package/dist/esm/ImagePreview/index.js +2 -0
  42. package/dist/esm/Message/Message.d.ts +3 -0
  43. package/dist/esm/Message/Message.js +3 -2
  44. package/dist/esm/MessageBox/MessageBox.js +1 -1
  45. package/dist/esm/ToolCall/ToolCall.d.ts +44 -0
  46. package/dist/esm/ToolCall/ToolCall.js +10 -0
  47. package/dist/esm/ToolCall/ToolCall.test.d.ts +1 -0
  48. package/dist/esm/ToolCall/ToolCall.test.js +139 -0
  49. package/dist/esm/ToolCall/index.d.ts +2 -0
  50. package/dist/esm/ToolCall/index.js +2 -0
  51. package/dist/esm/index.d.ts +4 -0
  52. package/dist/esm/index.js +4 -0
  53. package/dist/tsconfig.tsbuildinfo +1 -1
  54. package/package.json +1 -1
  55. package/patternfly-docs/content/extensions/chatbot/examples/Messages/AttachmentEdit.tsx +1 -1
  56. package/patternfly-docs/content/extensions/chatbot/examples/Messages/ImagePreview.tsx +53 -0
  57. package/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithToolCall.tsx +45 -0
  58. package/patternfly-docs/content/extensions/chatbot/examples/Messages/Messages.md +21 -1
  59. package/patternfly-docs/content/extensions/chatbot/examples/Messages/PreviewAttachment.tsx +1 -1
  60. package/patternfly-docs/content/extensions/chatbot/examples/Messages/file-preview.svg +9 -0
  61. package/patternfly-docs/content/extensions/chatbot/examples/demos/Chatbot.md +1 -1
  62. package/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.scss +0 -12
  63. package/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.tsx +1 -1
  64. package/src/FileDetails/FileDetails.scss +10 -0
  65. package/src/FileDetails/FileDetails.test.tsx +16 -0
  66. package/src/FileDetails/FileDetails.tsx +89 -32
  67. package/src/FileDetails/__snapshots__/FileDetails.test.tsx.snap +25 -16
  68. package/src/FileDetailsLabel/FileDetailsLabel.test.tsx +21 -1
  69. package/src/FileDetailsLabel/FileDetailsLabel.tsx +16 -3
  70. package/src/FileDetailsLabel/__snapshots__/FileDetailsLabel.test.tsx.snap +25 -16
  71. package/src/ImagePreview/ImagePreview.scss +61 -0
  72. package/src/ImagePreview/ImagePreview.test.tsx +253 -0
  73. package/src/ImagePreview/ImagePreview.tsx +200 -0
  74. package/src/ImagePreview/index.ts +3 -0
  75. package/src/Message/Message.tsx +5 -0
  76. package/src/MessageBox/MessageBox.scss +0 -12
  77. package/src/MessageBox/MessageBox.tsx +1 -1
  78. package/src/ToolCall/ToolCall.scss +37 -0
  79. package/src/ToolCall/ToolCall.test.tsx +184 -0
  80. package/src/ToolCall/ToolCall.tsx +147 -0
  81. package/src/ToolCall/index.ts +3 -0
  82. package/src/index.ts +6 -0
  83. package/src/main.scss +14 -0
@@ -0,0 +1,200 @@
1
+ import {
2
+ Button,
3
+ ButtonVariant,
4
+ Icon,
5
+ ModalBody,
6
+ ModalBodyProps,
7
+ ModalFooter,
8
+ ModalHeader,
9
+ ModalHeaderProps,
10
+ Stack,
11
+ StackItem
12
+ } from '@patternfly/react-core';
13
+ import {
14
+ useState,
15
+ useEffect,
16
+ type FunctionComponent,
17
+ MouseEvent as ReactMouseEvent,
18
+ KeyboardEvent as ReactKeyboardEvent
19
+ } from 'react';
20
+ import { ChatbotDisplayMode } from '../Chatbot';
21
+ import ChatbotModal, { ChatbotModalProps } from '../ChatbotModal';
22
+ import FileDetailsLabel, { FileDetailsLabelProps } from '../FileDetailsLabel';
23
+ import { TrashIcon } from '@patternfly/react-icons';
24
+
25
+ export interface ImagePreviewProps extends Omit<ChatbotModalProps, 'children'> {
26
+ /** Class applied to modal */
27
+ className?: string;
28
+ /** Function that handles modal toggle */
29
+ handleModalToggle: (event: React.MouseEvent | MouseEvent | KeyboardEvent) => void;
30
+ /** Whether modal is open */
31
+ isModalOpen: boolean;
32
+ /** Title of modal */
33
+ title?: string;
34
+ /** Display mode for the Chatbot parent; this influences the styles applied */
35
+ displayMode?: ChatbotDisplayMode;
36
+ /** Sets modal to compact styling. */
37
+ isCompact?: boolean;
38
+ /** Additional props passed to modal header */
39
+ modalHeaderProps?: ModalHeaderProps;
40
+ /** Additional props passed to modal body */
41
+ modalBodyProps?: ModalBodyProps;
42
+ /** Images displayed in modal */
43
+ images: { fileName: string; fileSize?: string; image: React.ReactNode }[];
44
+ /** Flag indicating if the pagination is disabled. */
45
+ isDisabled?: boolean;
46
+ /** Accessible label for the pagination component. */
47
+ paginationAriaLabel?: string;
48
+ /** Accessible label for the button which moves to the next page. */
49
+ toNextPageAriaLabel?: string;
50
+ /** Accessible label for the button which moves to the previous page. */
51
+ toPreviousPageAriaLabel?: string;
52
+ /** Function called when user clicks to navigate to next page. */
53
+ onNextClick?: (event: React.SyntheticEvent<HTMLButtonElement>, page: number) => void;
54
+ /** Function called when user clicks to navigate to previous page. */
55
+ onPreviousClick?: (event: React.SyntheticEvent<HTMLButtonElement>, page: number) => void;
56
+ /** Function called when page is changed. */
57
+ onSetPage?: (event: React.MouseEvent | React.KeyboardEvent | MouseEvent, newPage: number) => void;
58
+ /** Callback function for when file details label close button is clicked */
59
+ onCloseFileDetailsLabel?: (event: React.MouseEvent, fileName: string, fileId?: string | number) => void;
60
+ /** Props passed to file details label */
61
+ fileDetailsLabelProps?: Omit<FileDetailsLabelProps, 'fileName'>;
62
+ /** Text shown in navigation */
63
+ paginationContent?: string;
64
+ /** Navigation progress announced to assistive devices. Should state the current page/image. */
65
+ screenreaderText?: string;
66
+ }
67
+
68
+ const ImagePreview: FunctionComponent<ImagePreviewProps> = ({
69
+ isModalOpen,
70
+ displayMode = ChatbotDisplayMode.default,
71
+ isCompact,
72
+ className,
73
+ handleModalToggle,
74
+ title = 'Preview images',
75
+ modalHeaderProps,
76
+ modalBodyProps,
77
+ images,
78
+ isDisabled,
79
+ onSetPage,
80
+ onPreviousClick,
81
+ toNextPageAriaLabel = 'Go to next image',
82
+ toPreviousPageAriaLabel = 'Go to previous image',
83
+ onNextClick,
84
+ paginationAriaLabel,
85
+ onCloseFileDetailsLabel,
86
+ fileDetailsLabelProps,
87
+ paginationContent,
88
+ screenreaderText,
89
+ ...props
90
+ }: ImagePreviewProps) => {
91
+ const [page, setPage] = useState(1);
92
+ const paginationText = paginationContent || `${page}/${images.length}`;
93
+
94
+ useEffect(() => {
95
+ if (images.length === 0 || page > images.length) {
96
+ setPage(1);
97
+ }
98
+ }, [images.length, page]);
99
+
100
+ const handleNewPage = (_evt: ReactMouseEvent | ReactKeyboardEvent | MouseEvent, newPage: number) => {
101
+ setPage(newPage);
102
+ onSetPage && onSetPage(_evt, newPage);
103
+ };
104
+
105
+ return (
106
+ <ChatbotModal
107
+ isOpen={isModalOpen}
108
+ className={`pf-chatbot__image-preview-modal pf-chatbot__image-preview-modal--${displayMode} ${isCompact ? 'pf-m-compact' : ''} ${className ? className : ''}`}
109
+ displayMode={displayMode}
110
+ onClose={handleModalToggle}
111
+ isCompact={isCompact}
112
+ {...props}
113
+ >
114
+ <ModalHeader title={title} {...modalHeaderProps} />
115
+ <ModalBody className="pf-chatbot__image-preview-body" {...modalBodyProps}>
116
+ {images.length > 0 && images[page - 1] && (
117
+ <Stack hasGutter className="pf-chatbot__image-preview-stack">
118
+ <StackItem>
119
+ <FileDetailsLabel
120
+ fileName={images[page - 1].fileName}
121
+ fileSize={images[page - 1].fileSize}
122
+ hasTruncation={false}
123
+ onClose={onCloseFileDetailsLabel}
124
+ closeButtonIcon={<TrashIcon />}
125
+ {...fileDetailsLabelProps}
126
+ />
127
+ </StackItem>
128
+ <StackItem>
129
+ <div className="pf-chatbot__image-preview-body">{images[page - 1].image}</div>
130
+ </StackItem>
131
+ </Stack>
132
+ )}
133
+ </ModalBody>
134
+ {images.length > 1 && (
135
+ <ModalFooter className="pf-chatbot__image-preview-footer">
136
+ <nav className={`pf-chatbot__image-preview-footer-buttons`} aria-label={paginationAriaLabel}>
137
+ <Button
138
+ variant={ButtonVariant.plain}
139
+ isDisabled={isDisabled || page === 1}
140
+ data-action="previous"
141
+ onClick={(event) => {
142
+ const newPage = page > 1 ? page - 1 : 1;
143
+ handleNewPage(event, newPage);
144
+ onPreviousClick && onPreviousClick(event, newPage);
145
+ }}
146
+ aria-label={toPreviousPageAriaLabel}
147
+ >
148
+ <Icon iconSize="lg">
149
+ {/* these are inline because the viewBox that works in a round icon is different than the PatternFly default */}
150
+ <svg
151
+ className="pf-v6-svg"
152
+ viewBox="0 0 280 500"
153
+ fill="currentColor"
154
+ aria-hidden="true"
155
+ role="img"
156
+ width="1em"
157
+ height="1em"
158
+ >
159
+ <path d="M31.7 239l136-136c9.4-9.4 24.6-9.4 33.9 0l22.6 22.6c9.4 9.4 9.4 24.6 0 33.9L127.9 256l96.4 96.4c9.4 9.4 9.4 24.6 0 33.9L201.7 409c-9.4 9.4-24.6 9.4-33.9 0l-136-136c-9.5-9.4-9.5-24.6-.1-34z"></path>
160
+ </svg>
161
+ </Icon>
162
+ </Button>
163
+ <span>{paginationText}</span>
164
+ <div className="pf-chatbot-m-hidden" aria-live="polite">
165
+ {screenreaderText ?? `Image ${page} of ${images.length}`}
166
+ </div>
167
+ <Button
168
+ variant={ButtonVariant.plain}
169
+ isDisabled={isDisabled || page === images.length}
170
+ aria-label={toNextPageAriaLabel}
171
+ data-action="next"
172
+ onClick={(event) => {
173
+ const newPage = page + 1 <= images.length ? page + 1 : images.length;
174
+ handleNewPage(event, newPage);
175
+ onNextClick && onNextClick(event, newPage);
176
+ }}
177
+ >
178
+ <Icon isInline iconSize="lg">
179
+ {/* these are inline because the viewBox that works in a round icon is different than the PatternFly default */}
180
+ <svg
181
+ className="pf-v6-svg"
182
+ viewBox="0 0 180 500"
183
+ fill="currentColor"
184
+ aria-hidden="true"
185
+ role="img"
186
+ width="1em"
187
+ height="1em"
188
+ >
189
+ <path d="M224.3 273l-136 136c-9.4 9.4-24.6 9.4-33.9 0l-22.6-22.6c-9.4-9.4-9.4-24.6 0-33.9l96.4-96.4-96.4-96.4c-9.4-9.4-9.4-24.6 0-33.9L54.3 103c9.4-9.4 24.6-9.4 33.9 0l136 136c9.5 9.4 9.5 24.6.1 34z"></path>
190
+ </svg>
191
+ </Icon>
192
+ </Button>
193
+ </nav>
194
+ </ModalFooter>
195
+ )}
196
+ </ChatbotModal>
197
+ );
198
+ };
199
+
200
+ export default ImagePreview;
@@ -0,0 +1,3 @@
1
+ export { default } from './ImagePreview';
2
+
3
+ export * from './ImagePreview';
@@ -52,6 +52,7 @@ import { rehypeMoveImagesOutOfParagraphs } from './Plugins/rehypeMoveImagesOutOf
52
52
  import ToolResponse, { ToolResponseProps } from '../ToolResponse';
53
53
  import DeepThinking, { DeepThinkingProps } from '../DeepThinking';
54
54
  import SuperscriptMessage from './SuperscriptMessage/SuperscriptMessage';
55
+ import ToolCall, { ToolCallProps } from '../ToolCall';
55
56
 
56
57
  export interface MessageAttachment {
57
58
  /** Name of file attached to the message */
@@ -200,6 +201,8 @@ export interface MessageProps extends Omit<HTMLProps<HTMLDivElement>, 'role'> {
200
201
  deepThinking?: DeepThinkingProps;
201
202
  /** Allows passing additional props down to remark-gfm. See https://github.com/remarkjs/remark-gfm?tab=readme-ov-file#options for options */
202
203
  remarkGfmProps?: Options;
204
+ /** Props for a tool call message */
205
+ toolCall?: ToolCallProps;
203
206
  }
204
207
 
205
208
  export const MessageBase: FunctionComponent<MessageProps> = ({
@@ -245,6 +248,7 @@ export const MessageBase: FunctionComponent<MessageProps> = ({
245
248
  toolResponse,
246
249
  deepThinking,
247
250
  remarkGfmProps,
251
+ toolCall,
248
252
  ...props
249
253
  }: MessageProps) => {
250
254
  const [messageText, setMessageText] = useState(content);
@@ -485,6 +489,7 @@ export const MessageBase: FunctionComponent<MessageProps> = ({
485
489
  {afterMainContent && <>{afterMainContent}</>}
486
490
  {toolResponse && <ToolResponse {...toolResponse} />}
487
491
  {deepThinking && <DeepThinking {...deepThinking} />}
492
+ {toolCall && <ToolCall {...toolCall} />}
488
493
  {!isLoading && sources && <SourcesCard {...sources} isCompact={isCompact} />}
489
494
  {quickStarts && quickStarts.quickStart && (
490
495
  <QuickStartTile
@@ -23,18 +23,6 @@
23
23
  margin-top: auto !important;
24
24
  }
25
25
 
26
- // hide from view but not assistive technologies
27
- // https://css-tricks.com/inclusively-hidden/
28
- .pf-chatbot__messagebox-announcement {
29
- clip: rect(0 0 0 0);
30
- clip-path: inset(50%);
31
- height: 1px;
32
- overflow: hidden;
33
- position: absolute;
34
- white-space: nowrap;
35
- width: 1px;
36
- }
37
-
38
26
  @media screen and (min-width: 64rem) {
39
27
  .pf-chatbot--embedded,
40
28
  .pf-chatbot--drawer,
@@ -335,7 +335,7 @@ export const MessageBox = forwardRef(
335
335
  {...(enableSmartScroll ? { ...smartScrollHandlers } : {})}
336
336
  >
337
337
  {children}
338
- <div className="pf-chatbot__messagebox-announcement" aria-live="polite">
338
+ <div className="pf-chatbot__messagebox-announcement pf-chatbot-m-hidden" aria-live="polite">
339
339
  {announcement}
340
340
  </div>
341
341
  </div>
@@ -0,0 +1,37 @@
1
+ .pf-chatbot__tool-call {
2
+ --pf-v6-c-card--BorderColor: var(--pf-t--global--border--color--control--read-only);
3
+ --pf-v6-c-card--BorderRadius: var(--pf-t--global--border--radius--small);
4
+
5
+ overflow: unset;
6
+ row-gap: var(--pf-t--global--spacer--sm);
7
+
8
+ .pf-chatbot__tool-call-title-content {
9
+ display: flex;
10
+ gap: var(--pf-t--global--spacer--xs);
11
+ align-items: center;
12
+ }
13
+
14
+ .pf-chatbot__tool-call-title:not(:has(.pf-chatbot__tool-call-expandable-section)) {
15
+ .pf-chatbot__tool-call-title-text {
16
+ color: var(--pf-t--global--text--color--regular);
17
+ font-size: var(--pf-t--global--font--size--body--default);
18
+ font-weight: var(--pf-t--global--font--weight--body--default);
19
+ }
20
+ }
21
+
22
+ .pf-chatbot__tool-call-title {
23
+ overflow: unset;
24
+ }
25
+
26
+ .pf-chatbot__tool-call-expandable-section {
27
+ --pf-v6-c-expandable-section--Gap: var(--pf-t--global--spacer--xs);
28
+
29
+ .pf-v6-c-expandable-section__content {
30
+ color: var(--pf-t--global--text--color--subtle);
31
+ }
32
+ }
33
+
34
+ .pf-chatbot__tool-call-action-list {
35
+ justify-content: flex-end;
36
+ }
37
+ }
@@ -0,0 +1,184 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import userEvent from '@testing-library/user-event';
3
+ import '@testing-library/jest-dom';
4
+ import ToolCall from './ToolCall';
5
+
6
+ describe('ToolCall', () => {
7
+ const defaultProps = {
8
+ titleText: 'ToolCall Title',
9
+ loadingText: 'Loading ToolCall'
10
+ };
11
+
12
+ it('Renders with passed in titleText', () => {
13
+ render(<ToolCall {...defaultProps} />);
14
+ expect(screen.getByText(defaultProps.titleText)).toBeVisible();
15
+ });
16
+
17
+ it('Does not render with passed in loadingText when isLoading is false', () => {
18
+ render(<ToolCall {...defaultProps} />);
19
+ expect(screen.queryByText(defaultProps.loadingText)).not.toBeInTheDocument();
20
+ });
21
+
22
+ it('Renders with passed in loadingText when isLoading is true', () => {
23
+ render(<ToolCall {...defaultProps} isLoading />);
24
+ expect(screen.getByText(defaultProps.loadingText)).toBeVisible();
25
+ });
26
+
27
+ it('Does not render titleText when isLoading is true', () => {
28
+ render(<ToolCall {...defaultProps} isLoading />);
29
+ expect(screen.queryByText(defaultProps.titleText)).not.toBeInTheDocument();
30
+ });
31
+
32
+ it('Passes spinnerProps to Spinner', () => {
33
+ render(<ToolCall {...defaultProps} isLoading spinnerProps={{ id: 'spinner-test-id' }} />);
34
+
35
+ expect(screen.getByRole('progressbar')).toHaveAttribute('id', 'spinner-test-id');
36
+ });
37
+
38
+ it('Does not render expandable toggle by default', () => {
39
+ render(<ToolCall {...defaultProps} />);
40
+ expect(screen.queryByRole('button', { name: defaultProps.titleText })).not.toBeInTheDocument();
41
+ });
42
+
43
+ it('Renders titleText inside expandable toggle when expandableContent is passed', () => {
44
+ render(<ToolCall {...defaultProps} expandableContent="Expandable Content" />);
45
+ expect(screen.getByRole('button', { name: defaultProps.titleText })).toBeVisible();
46
+ });
47
+
48
+ it('Does not render expandable content when expandableContent is passed by default', () => {
49
+ render(<ToolCall {...defaultProps} expandableContent="Expandable Content" />);
50
+ expect(screen.queryByText('Expandable Content')).not.toBeVisible();
51
+ });
52
+
53
+ it('Renders expandable content when expandableContent is passed and toggle is clicked', async () => {
54
+ const user = userEvent.setup();
55
+ render(<ToolCall {...defaultProps} expandableContent="Expandable Content" />);
56
+ await user.click(screen.getByRole('button', { name: defaultProps.titleText }));
57
+
58
+ expect(screen.getByText('Expandable Content')).toBeVisible();
59
+ });
60
+
61
+ it('Passes expandableSectionProps to ExpandableSection', () => {
62
+ render(
63
+ <ToolCall
64
+ {...defaultProps}
65
+ expandableContent="Expandable Content"
66
+ expandableSectionProps={{ id: 'expandable-section-test-id', isExpanded: true }}
67
+ />
68
+ );
69
+ expect(screen.getByRole('region').parentElement).toHaveAttribute('id', 'expandable-section-test-id');
70
+ });
71
+
72
+ it('Renders "run" action button by default', () => {
73
+ render(<ToolCall {...defaultProps} />);
74
+ expect(screen.getByRole('button', { name: 'Run tool' })).toBeVisible();
75
+ });
76
+
77
+ it('Renders "cancel" action button by default', () => {
78
+ render(<ToolCall {...defaultProps} />);
79
+ expect(screen.getByRole('button', { name: 'Cancel' })).toBeVisible();
80
+ });
81
+
82
+ it('Does not render "run" action button when isLoading is true', () => {
83
+ render(<ToolCall {...defaultProps} isLoading />);
84
+ expect(screen.queryByRole('button', { name: 'Run tool' })).not.toBeInTheDocument();
85
+ });
86
+
87
+ it('Does not render "cancel" action button when isLoading is true', () => {
88
+ render(<ToolCall {...defaultProps} isLoading />);
89
+ expect(screen.queryByRole('button', { name: 'Cancel' })).not.toBeInTheDocument();
90
+ });
91
+
92
+ it('Renders runButtonText when passed', () => {
93
+ render(<ToolCall {...defaultProps} runButtonText="Run my custom tool" />);
94
+ expect(screen.getByRole('button', { name: 'Run my custom tool' })).toBeVisible();
95
+ });
96
+
97
+ it('Renders cancelButtonText when passed', () => {
98
+ render(<ToolCall {...defaultProps} cancelButtonText="Cancel my custom tool" />);
99
+ expect(screen.getByRole('button', { name: 'Cancel my custom tool' })).toBeVisible();
100
+ });
101
+
102
+ it('Passes runButtonProps to Button', () => {
103
+ render(<ToolCall {...defaultProps} runButtonProps={{ id: 'run-button-test-id' }} />);
104
+ expect(screen.getByRole('button', { name: 'Run tool' })).toHaveAttribute('id', 'run-button-test-id');
105
+ });
106
+
107
+ it('Passes cancelButtonProps to Button', () => {
108
+ render(<ToolCall {...defaultProps} cancelButtonProps={{ id: 'cancel-button-test-id' }} />);
109
+ expect(screen.getByRole('button', { name: 'Cancel' })).toHaveAttribute('id', 'cancel-button-test-id');
110
+ });
111
+
112
+ it('Passes runActionItemProps to ActionListItem', () => {
113
+ render(<ToolCall {...defaultProps} runActionItemProps={{ id: 'run-action-item-test-id' }} />);
114
+ expect(screen.getByRole('button', { name: 'Run tool' }).parentElement).toHaveAttribute(
115
+ 'id',
116
+ 'run-action-item-test-id'
117
+ );
118
+ });
119
+
120
+ it('Passes cancelActionItemProps to ActionListItem', () => {
121
+ render(<ToolCall {...defaultProps} cancelActionItemProps={{ id: 'cancel-action-item-test-id' }} />);
122
+ expect(screen.getByRole('button', { name: 'Cancel' }).parentElement).toHaveAttribute(
123
+ 'id',
124
+ 'cancel-action-item-test-id'
125
+ );
126
+ });
127
+
128
+ it('Passes actionListProps to ActionList', () => {
129
+ render(<ToolCall {...defaultProps} actionListProps={{ id: 'action-list-test-id' }} />);
130
+ expect(screen.getByRole('button', { name: 'Run tool' }).closest('#action-list-test-id')).toBeVisible();
131
+ });
132
+
133
+ it('Passes actionListGroupProps to ActionListGroup', () => {
134
+ render(<ToolCall {...defaultProps} actionListGroupProps={{ id: 'action-list-group-test-id' }} />);
135
+ expect(screen.getByRole('button', { name: 'Run tool' }).closest('#action-list-group-test-id')).toBeVisible();
136
+ });
137
+
138
+ it('Passes actionListItemProps to ActionListItem for default actions', () => {
139
+ render(<ToolCall {...defaultProps} actionListItemProps={{ className: 'action-list-item-test-class' }} />);
140
+ expect(screen.getByRole('button', { name: 'Run tool' }).parentElement).toHaveClass('action-list-item-test-class');
141
+ expect(screen.getByRole('button', { name: 'Cancel' }).parentElement).toHaveClass('action-list-item-test-class');
142
+ });
143
+
144
+ it('Renders custom actions instead of default actions when actions are passed', () => {
145
+ render(
146
+ <ToolCall
147
+ {...defaultProps}
148
+ actions={[<div key="custom-action-1">Custom action 1</div>, <div key="custom-action-2">Custom action 2</div>]}
149
+ />
150
+ );
151
+
152
+ expect(screen.getByText('Custom action 1')).toBeVisible();
153
+ expect(screen.getByText('Custom action 2')).toBeVisible();
154
+ expect(screen.queryByRole('button', { name: 'Run tool' })).not.toBeInTheDocument();
155
+ expect(screen.queryByRole('button', { name: 'Cancel' })).not.toBeInTheDocument();
156
+ });
157
+
158
+ it('Passes actionListItemProps to ActionListItem for custom actions', () => {
159
+ render(
160
+ <ToolCall
161
+ {...defaultProps}
162
+ actions={[<div key="custom-action-1">Custom action 1</div>, <div key="custom-action-2">Custom action 2</div>]}
163
+ actionListItemProps={{ className: 'action-list-item-test-class' }}
164
+ />
165
+ );
166
+ expect(screen.getByText('Custom action 1').parentElement).toHaveClass('action-list-item-test-class');
167
+ expect(screen.getByText('Custom action 2').parentElement).toHaveClass('action-list-item-test-class');
168
+ });
169
+
170
+ it('Passes cardProps to Card', () => {
171
+ render(<ToolCall {...defaultProps} cardProps={{ id: 'card-test-id' }} />);
172
+ expect(screen.getByRole('button', { name: 'Run tool' }).closest('#card-test-id')).toBeVisible();
173
+ });
174
+
175
+ it('Passes cardBodyProps to CardBody', () => {
176
+ render(<ToolCall {...defaultProps} cardBodyProps={{ id: 'card-body-test-id' }} />);
177
+ expect(screen.getByText(defaultProps.titleText).closest('#card-body-test-id')).toBeVisible();
178
+ });
179
+
180
+ it('Passes cardFooterProps to CardFooter', () => {
181
+ render(<ToolCall {...defaultProps} cardFooterProps={{ id: 'card-footer-test-id' }} />);
182
+ expect(screen.getByRole('button', { name: 'Run tool' }).closest('#card-footer-test-id')).toBeVisible();
183
+ });
184
+ });
@@ -0,0 +1,147 @@
1
+ import { type FunctionComponent } from 'react';
2
+ import {
3
+ ActionList,
4
+ ActionListProps,
5
+ ActionListGroup,
6
+ ActionListGroupProps,
7
+ ActionListItem,
8
+ ActionListItemProps,
9
+ Button,
10
+ ButtonProps,
11
+ Card,
12
+ CardProps,
13
+ CardBody,
14
+ CardBodyProps,
15
+ CardFooter,
16
+ CardFooterProps,
17
+ ExpandableSection,
18
+ ExpandableSectionProps,
19
+ Spinner,
20
+ SpinnerProps
21
+ } from '@patternfly/react-core';
22
+
23
+ export interface ToolCallProps {
24
+ /** Title text for the tool call. */
25
+ titleText: string;
26
+ /** Loading text for the tool call. */
27
+ loadingText?: string;
28
+ /** Flag indicating whether the tool call is loading or not. */
29
+ isLoading?: boolean;
30
+ /** Additional props for the spinner that is rendered when isLoading is true. */
31
+ spinnerProps?: SpinnerProps;
32
+ /** Content to render within an expandable section. */
33
+ expandableContent?: React.ReactNode;
34
+ /** Text content for the "run" action button. */
35
+ runButtonText?: string;
36
+ /** Additional props for the "run" action button. */
37
+ runButtonProps?: ButtonProps;
38
+ /** Additional props for the "run" action list item. */
39
+ runActionItemProps?: ActionListItemProps;
40
+ /** Text content for the "cancel" action button. */
41
+ cancelButtonText?: string;
42
+ /** Additional props for the "cancel" action button. */
43
+ cancelButtonProps?: ButtonProps;
44
+ /** Additional props for the "cancel" action list item. */
45
+ cancelActionItemProps?: ActionListItemProps;
46
+ /** Custom actions to render, typically a "cancel" and "run" action. This will override the default actions. */
47
+ actions?: React.ReactNode[];
48
+ /** Additional props for the action list */
49
+ actionListProps?: ActionListProps;
50
+ /** Additional props for the action list group. */
51
+ actionListGroupProps?: ActionListGroupProps;
52
+ /** Additional props for all action list items. */
53
+ actionListItemProps?: ActionListItemProps;
54
+ /** Additional props for the card. */
55
+ cardProps?: CardProps;
56
+ /** Additional props for the card body that contains the main tool call content. */
57
+ cardBodyProps?: CardBodyProps;
58
+ /** Additional props for the card footer that contains the tool call actions. */
59
+ cardFooterProps?: CardFooterProps;
60
+ /** Additional props for the expandable section when expandableContent is passed. */
61
+ expandableSectionProps?: Omit<ExpandableSectionProps, 'ref'>;
62
+ }
63
+
64
+ export const ToolCall: FunctionComponent<ToolCallProps> = ({
65
+ titleText,
66
+ loadingText,
67
+ isLoading,
68
+ expandableContent,
69
+ runButtonText = 'Run tool',
70
+ runButtonProps,
71
+ runActionItemProps,
72
+ cancelButtonText = 'Cancel',
73
+ cancelButtonProps,
74
+ cancelActionItemProps,
75
+ actions,
76
+ actionListProps,
77
+ actionListGroupProps,
78
+ actionListItemProps,
79
+ cardProps,
80
+ cardBodyProps,
81
+ cardFooterProps,
82
+ expandableSectionProps,
83
+ spinnerProps
84
+ }: ToolCallProps) => {
85
+ const titleContent = (
86
+ <span className={`pf-chatbot__tool-call-title-content`}>
87
+ {isLoading ? (
88
+ <>
89
+ <Spinner diameter="1em" {...spinnerProps} />{' '}
90
+ {<span className="pf-chatbot__tool-call-title-text">{loadingText}</span>}
91
+ </>
92
+ ) : (
93
+ <span className="pf-chatbot__tool-call-title-text">{titleText}</span>
94
+ )}
95
+ </span>
96
+ );
97
+ const defaultActions = (
98
+ <>
99
+ <ActionListItem {...actionListItemProps} {...cancelActionItemProps}>
100
+ <Button variant="link" {...cancelButtonProps}>
101
+ {cancelButtonText}
102
+ </Button>
103
+ </ActionListItem>
104
+ <ActionListItem {...actionListItemProps} {...runActionItemProps}>
105
+ <Button variant="secondary" {...runButtonProps}>
106
+ {runButtonText}
107
+ </Button>
108
+ </ActionListItem>
109
+ </>
110
+ );
111
+
112
+ const customActions =
113
+ actions &&
114
+ actions.map((action, index) => (
115
+ <ActionListItem key={index} {...actionListItemProps}>
116
+ {action}
117
+ </ActionListItem>
118
+ ));
119
+
120
+ return (
121
+ <Card isCompact className="pf-chatbot__tool-call" {...cardProps}>
122
+ <CardBody className="pf-chatbot__tool-call-title" {...cardBodyProps}>
123
+ {expandableContent && !isLoading ? (
124
+ <ExpandableSection
125
+ className="pf-chatbot__tool-call-expandable-section"
126
+ toggleContent={titleContent}
127
+ isIndented
128
+ {...expandableSectionProps}
129
+ >
130
+ {expandableContent}
131
+ </ExpandableSection>
132
+ ) : (
133
+ titleContent
134
+ )}
135
+ </CardBody>
136
+ {!isLoading && (
137
+ <CardFooter {...cardFooterProps}>
138
+ <ActionList className="pf-chatbot__tool-call-action-list" {...actionListProps}>
139
+ <ActionListGroup {...actionListGroupProps}>{customActions || defaultActions}</ActionListGroup>
140
+ </ActionList>
141
+ </CardFooter>
142
+ )}
143
+ </Card>
144
+ );
145
+ };
146
+
147
+ export default ToolCall;
@@ -0,0 +1,3 @@
1
+ export { default } from './ToolCall';
2
+
3
+ export * from './ToolCall';
package/src/index.ts CHANGED
@@ -57,6 +57,9 @@ export * from './FileDropZone';
57
57
  export { default as FilePreview } from './FilePreview';
58
58
  export * from './FilePreview';
59
59
 
60
+ export { default as ImagePreview } from './ImagePreview';
61
+ export * from './ImagePreview';
62
+
60
63
  export { default as LoadingMessage } from './LoadingMessage';
61
64
  export * from './LoadingMessage';
62
65
 
@@ -93,5 +96,8 @@ export * from './TermsOfUse';
93
96
  export { default as ToolResponse } from './ToolResponse';
94
97
  export * from './ToolResponse';
95
98
 
99
+ export { default as ToolCall } from './ToolCall';
100
+ export * from './ToolCall';
101
+
96
102
  export { default as tracking } from './tracking';
97
103
  export * from './tracking';