@patternfly/chatbot 2.2.1 → 6.3.0-prerelease.1

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 (95) hide show
  1. package/dist/cjs/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.d.ts +4 -0
  2. package/dist/cjs/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.js +7 -1
  3. package/dist/cjs/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.test.js +23 -0
  4. package/dist/cjs/Message/Message.d.ts +17 -1
  5. package/dist/cjs/Message/Message.js +53 -34
  6. package/dist/cjs/Message/Message.test.js +52 -0
  7. package/dist/cjs/Message/MessageInput.d.ts +18 -0
  8. package/dist/cjs/Message/MessageInput.js +34 -0
  9. package/dist/cjs/MessageBar/MicrophoneButton.js +1 -1
  10. package/dist/cjs/MessageBox/MessageBox.js +5 -5
  11. package/dist/cjs/SourcesCard/SourcesCard.d.ts +7 -1
  12. package/dist/cjs/SourcesCard/SourcesCard.js +16 -10
  13. package/dist/cjs/SourcesCard/SourcesCard.test.js +25 -15
  14. package/dist/cjs/tracking/console_tracking_provider.d.ts +4 -5
  15. package/dist/cjs/tracking/console_tracking_provider.js +22 -15
  16. package/dist/cjs/tracking/posthog_tracking_provider.d.ts +2 -2
  17. package/dist/cjs/tracking/posthog_tracking_provider.js +21 -12
  18. package/dist/cjs/tracking/segment_tracking_provider.d.ts +2 -2
  19. package/dist/cjs/tracking/segment_tracking_provider.js +21 -12
  20. package/dist/cjs/tracking/trackingProviderProxy.d.ts +1 -1
  21. package/dist/cjs/tracking/trackingProviderProxy.js +2 -2
  22. package/dist/cjs/tracking/tracking_api.d.ts +1 -1
  23. package/dist/cjs/tracking/tracking_registry.js +46 -12
  24. package/dist/cjs/tracking/tracking_spi.d.ts +15 -5
  25. package/dist/cjs/tracking/tracking_spi.js +9 -0
  26. package/dist/cjs/tracking/umami_tracking_provider.d.ts +6 -2
  27. package/dist/cjs/tracking/umami_tracking_provider.js +66 -22
  28. package/dist/css/main.css +7 -7
  29. package/dist/css/main.css.map +1 -1
  30. package/dist/esm/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.d.ts +4 -0
  31. package/dist/esm/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.js +7 -1
  32. package/dist/esm/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.test.js +23 -0
  33. package/dist/esm/Message/Message.d.ts +17 -1
  34. package/dist/esm/Message/Message.js +53 -34
  35. package/dist/esm/Message/Message.test.js +52 -0
  36. package/dist/esm/Message/MessageInput.d.ts +18 -0
  37. package/dist/esm/Message/MessageInput.js +29 -0
  38. package/dist/esm/MessageBar/MicrophoneButton.js +1 -1
  39. package/dist/esm/MessageBox/MessageBox.js +5 -5
  40. package/dist/esm/SourcesCard/SourcesCard.d.ts +7 -1
  41. package/dist/esm/SourcesCard/SourcesCard.js +17 -11
  42. package/dist/esm/SourcesCard/SourcesCard.test.js +25 -15
  43. package/dist/esm/tracking/console_tracking_provider.d.ts +4 -5
  44. package/dist/esm/tracking/console_tracking_provider.js +22 -15
  45. package/dist/esm/tracking/posthog_tracking_provider.d.ts +2 -2
  46. package/dist/esm/tracking/posthog_tracking_provider.js +21 -12
  47. package/dist/esm/tracking/segment_tracking_provider.d.ts +2 -2
  48. package/dist/esm/tracking/segment_tracking_provider.js +21 -12
  49. package/dist/esm/tracking/trackingProviderProxy.d.ts +1 -1
  50. package/dist/esm/tracking/trackingProviderProxy.js +2 -2
  51. package/dist/esm/tracking/tracking_api.d.ts +1 -1
  52. package/dist/esm/tracking/tracking_registry.js +46 -12
  53. package/dist/esm/tracking/tracking_spi.d.ts +15 -5
  54. package/dist/esm/tracking/tracking_spi.js +8 -1
  55. package/dist/esm/tracking/umami_tracking_provider.d.ts +6 -2
  56. package/dist/esm/tracking/umami_tracking_provider.js +66 -22
  57. package/dist/tsconfig.tsbuildinfo +1 -1
  58. package/package.json +1 -1
  59. package/patternfly-docs/content/extensions/chatbot/examples/Analytics/Analytics.md +18 -14
  60. package/patternfly-docs/content/extensions/chatbot/examples/Messages/BotMessage.tsx +74 -104
  61. package/patternfly-docs/content/extensions/chatbot/examples/Messages/FileDetailsLabel.tsx +48 -37
  62. package/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithQuickResponses.tsx +10 -0
  63. package/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithSources.tsx +51 -14
  64. package/patternfly-docs/content/extensions/chatbot/examples/Messages/Messages.md +3 -1
  65. package/patternfly-docs/content/extensions/chatbot/examples/Messages/UserMessage.tsx +80 -104
  66. package/patternfly-docs/content/extensions/chatbot/examples/UI/ChatbotHeaderDrawer.tsx +35 -2
  67. package/patternfly-docs/content/extensions/chatbot/examples/UI/ChatbotHeaderDrawerResizable.tsx +13 -2
  68. package/patternfly-docs/content/extensions/chatbot/examples/UI/UI.md +1 -1
  69. package/patternfly-docs/content/extensions/chatbot/examples/demos/Chatbot.tsx +6 -3
  70. package/patternfly-docs/content/extensions/chatbot/examples/demos/ChatbotAttachment.tsx +2 -0
  71. package/patternfly-docs/content/extensions/chatbot/examples/demos/ChatbotAttachmentMenu.tsx +2 -0
  72. package/patternfly-docs/content/extensions/chatbot/examples/demos/ChatbotInDrawer.tsx +2 -0
  73. package/patternfly-docs/content/extensions/chatbot/examples/demos/EmbeddedChatbot.tsx +2 -0
  74. package/patternfly-docs/content/extensions/chatbot/examples/demos/EmbeddedComparisonChatbot.tsx +62 -57
  75. package/patternfly-docs/content/extensions/chatbot/examples/demos/Feedback.tsx +2 -0
  76. package/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.test.tsx +53 -0
  77. package/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.tsx +14 -0
  78. package/src/Message/Message.scss +4 -0
  79. package/src/Message/Message.test.tsx +62 -0
  80. package/src/Message/Message.tsx +111 -53
  81. package/src/Message/MessageInput.tsx +59 -0
  82. package/src/MessageBar/MicrophoneButton.tsx +1 -1
  83. package/src/MessageBox/MessageBox.tsx +5 -5
  84. package/src/SourcesCard/SourcesCard.scss +3 -7
  85. package/src/SourcesCard/SourcesCard.test.tsx +30 -22
  86. package/src/SourcesCard/SourcesCard.tsx +54 -12
  87. package/src/tracking/console_tracking_provider.ts +21 -17
  88. package/src/tracking/posthog_tracking_provider.ts +20 -13
  89. package/src/tracking/segment_tracking_provider.ts +20 -13
  90. package/src/tracking/trackingProviderProxy.ts +2 -2
  91. package/src/tracking/tracking_api.ts +1 -1
  92. package/src/tracking/tracking_registry.ts +46 -13
  93. package/src/tracking/tracking_spi.ts +18 -7
  94. package/src/tracking/umami_tracking_provider.ts +76 -20
  95. package/src/SourcesCard/__snapshots__/SourcesCard.test.tsx.snap +0 -34
@@ -112,6 +112,10 @@ export interface ChatbotConversationHistoryNavProps extends DrawerProps {
112
112
  loadingState?: SkeletonProps;
113
113
  /** Content to show in error state. Error state will appear once content is passed in. */
114
114
  errorState?: HistoryEmptyStateProps;
115
+ /** Content to show in empty state. Empty state will appear once content is passed in. */
116
+ emptyState?: HistoryEmptyStateProps;
117
+ /** Content to show in no results state. No results state will appear once content is passed in. */
118
+ noResultsState?: HistoryEmptyStateProps;
115
119
  }
116
120
 
117
121
  export const ChatbotConversationHistoryNav: React.FunctionComponent<ChatbotConversationHistoryNavProps> = ({
@@ -141,6 +145,8 @@ export const ChatbotConversationHistoryNav: React.FunctionComponent<ChatbotConve
141
145
  isLoading,
142
146
  loadingState,
143
147
  errorState,
148
+ emptyState,
149
+ noResultsState,
144
150
  ...props
145
151
  }: ChatbotConversationHistoryNavProps) => {
146
152
  const drawerRef = React.useRef<HTMLDivElement>(null);
@@ -210,6 +216,14 @@ export const ChatbotConversationHistoryNav: React.FunctionComponent<ChatbotConve
210
216
  if (errorState) {
211
217
  return <HistoryEmptyState {...errorState} />;
212
218
  }
219
+
220
+ if (emptyState) {
221
+ return <HistoryEmptyState {...emptyState} />;
222
+ }
223
+
224
+ if (noResultsState) {
225
+ return <HistoryEmptyState {...noResultsState} />;
226
+ }
213
227
  return (
214
228
  <Menu isPlain onSelect={onSelectActiveItem} activeItemId={activeItemId} {...menuProps}>
215
229
  <MenuContent>{buildMenu()}</MenuContent>
@@ -97,6 +97,10 @@
97
97
  flex-wrap: wrap;
98
98
  }
99
99
 
100
+ .pf-chatbot__message-edit-buttons {
101
+ --pf-v6-c-form__group--m-action--MarginBlockStart: 0;
102
+ }
103
+
100
104
  @import './MessageLoading';
101
105
  @import './CodeBlockMessage/CodeBlockMessage';
102
106
  @import './TextMessage/TextMessage';
@@ -784,6 +784,20 @@ describe('Message', () => {
784
784
  // we are mocking rehype libraries, so we can't test target _blank addition on links directly with RTL
785
785
  expect(rehypeExternalLinks).not.toHaveBeenCalled();
786
786
  });
787
+ it('should handle extra link props correctly', async () => {
788
+ const spy = jest.fn();
789
+ render(
790
+ <Message
791
+ avatar="./img"
792
+ role="user"
793
+ name="User"
794
+ content={`[PatternFly](https://www.patternfly.org/)`}
795
+ linkProps={{ onClick: spy }}
796
+ />
797
+ );
798
+ await userEvent.click(screen.getByRole('link', { name: /PatternFly/i }));
799
+ expect(spy).toHaveBeenCalledTimes(1);
800
+ });
787
801
  it('should handle error correctly', () => {
788
802
  render(<Message avatar="./img" role="user" name="User" error={ERROR} />);
789
803
  expect(screen.getByRole('heading', { name: /Could not load chat/i })).toBeTruthy();
@@ -801,4 +815,52 @@ describe('Message', () => {
801
815
  expect(screen.getByRole('heading', { name: /Could not load chat/i })).toBeTruthy();
802
816
  expect(screen.queryByText('Test')).toBeFalsy();
803
817
  });
818
+ it('should handle isEditable when there is message content', () => {
819
+ render(<Message avatar="./img" role="user" name="User" isEditable content="Test" />);
820
+ expect(screen.getByRole('textbox')).toBeTruthy();
821
+ expect(screen.getByRole('textbox')).toHaveValue('Test');
822
+ expect(screen.getByRole('button', { name: /Update/i })).toBeTruthy();
823
+ expect(screen.getByRole('button', { name: /Cancel/i })).toBeTruthy();
824
+ });
825
+ it('should handle isEditable when there is no message content', () => {
826
+ render(<Message avatar="./img" role="user" name="User" isEditable />);
827
+ expect(screen.getByRole('textbox')).toBeTruthy();
828
+ expect(screen.getByRole('textbox')).toHaveValue('');
829
+ expect(screen.getByRole('textbox')).toHaveAttribute('placeholder', 'Edit prompt message...');
830
+ expect(screen.getByRole('button', { name: /Update/i })).toBeTruthy();
831
+ expect(screen.getByRole('button', { name: /Cancel/i })).toBeTruthy();
832
+ });
833
+ it('should be able to change edit placeholder', () => {
834
+ render(<Message avatar="./img" role="user" name="User" isEditable editPlaceholder="I am a placeholder" />);
835
+ expect(screen.getByRole('textbox')).toBeTruthy();
836
+ expect(screen.getByRole('textbox')).toHaveValue('');
837
+ expect(screen.getByRole('textbox')).toHaveAttribute('placeholder', 'I am a placeholder');
838
+ });
839
+ it('should be able to change updateWord', () => {
840
+ render(<Message avatar="./img" role="user" name="User" isEditable updateWord="Submit" />);
841
+ expect(screen.getByRole('button', { name: /Submit/i })).toBeTruthy();
842
+ });
843
+ it('should be able to change cancelWord', () => {
844
+ render(<Message avatar="./img" role="user" name="User" isEditable cancelWord="Don't submit" />);
845
+ expect(screen.getByRole('button', { name: /Don't submit/i })).toBeTruthy();
846
+ });
847
+ it('should be able to add onEditUpdate', async () => {
848
+ const spy = jest.fn();
849
+ render(<Message avatar="./img" role="user" name="User" isEditable onEditUpdate={spy} />);
850
+ await userEvent.click(screen.getByRole('button', { name: /Update/i }));
851
+ expect(spy).toHaveBeenCalledTimes(1);
852
+ });
853
+ it('should be able to add onEditCancel', async () => {
854
+ const spy = jest.fn();
855
+ render(<Message avatar="./img" role="user" name="User" isEditable onEditCancel={spy} />);
856
+ await userEvent.click(screen.getByRole('button', { name: /Cancel/i }));
857
+ expect(spy).toHaveBeenCalledTimes(1);
858
+ });
859
+ it('should be able to add editFormProps', () => {
860
+ const { container } = render(
861
+ <Message avatar="./img" role="user" name="User" isEditable editFormProps={{ className: 'test' }} />
862
+ );
863
+ const form = container.querySelector('form');
864
+ expect(form).toHaveClass('test');
865
+ });
804
866
  });
@@ -10,7 +10,9 @@ import {
10
10
  AlertProps,
11
11
  Avatar,
12
12
  AvatarProps,
13
+ ButtonProps,
13
14
  ContentVariants,
15
+ FormProps,
14
16
  Label,
15
17
  LabelGroupProps,
16
18
  Timestamp,
@@ -44,6 +46,7 @@ import rehypeSanitize from 'rehype-sanitize';
44
46
  import { PluggableList } from 'react-markdown/lib';
45
47
  import LinkMessage from './LinkMessage/LinkMessage';
46
48
  import ErrorMessage from './ErrorMessage/ErrorMessage';
49
+ import MessageInput from './MessageInput';
47
50
 
48
51
  export interface MessageAttachment {
49
52
  /** Name of file attached to the message */
@@ -145,6 +148,22 @@ export interface MessageProps extends Omit<React.HTMLProps<HTMLDivElement>, 'rol
145
148
  openLinkInNewTab?: boolean;
146
149
  /** Optional inline error message that can be displayed in the message */
147
150
  error?: AlertProps;
151
+ /** Props for links */
152
+ linkProps?: ButtonProps;
153
+ /** Whether message is in edit mode */
154
+ isEditable?: boolean;
155
+ /** Placeholder for edit input */
156
+ editPlaceholder?: string;
157
+ /** Label for the English word "Update" used in edit mode. */
158
+ updateWord?: string;
159
+ /** Label for the English word "Cancel" used in edit mode. */
160
+ cancelWord?: string;
161
+ /** Callback function for when edit mode update button is clicked */
162
+ onEditUpdate?: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
163
+ /** Callback functionf or when edit cancel update button is clicked */
164
+ onEditCancel?: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
165
+ /** Props for edit form */
166
+ editFormProps?: FormProps;
148
167
  }
149
168
 
150
169
  export const MessageBase: React.FunctionComponent<MessageProps> = ({
@@ -173,9 +192,23 @@ export const MessageBase: React.FunctionComponent<MessageProps> = ({
173
192
  tableProps,
174
193
  openLinkInNewTab = true,
175
194
  additionalRehypePlugins = [],
195
+ linkProps,
176
196
  error,
197
+ isEditable,
198
+ editPlaceholder = 'Edit prompt message...',
199
+ updateWord = 'Update',
200
+ cancelWord = 'Cancel',
201
+ onEditUpdate,
202
+ onEditCancel,
203
+ editFormProps,
177
204
  ...props
178
205
  }: MessageProps) => {
206
+ const [messageText, setMessageText] = React.useState(content);
207
+
208
+ React.useEffect(() => {
209
+ setMessageText(content);
210
+ }, [content]);
211
+
179
212
  const { beforeMainContent, afterMainContent, endContent } = extraContent || {};
180
213
  let rehypePlugins: PluggableList = [rehypeUnwrapImages];
181
214
  if (openLinkInNewTab) {
@@ -193,6 +226,82 @@ export const MessageBase: React.FunctionComponent<MessageProps> = ({
193
226
  // Keep timestamps consistent between Timestamp component and aria-label
194
227
  const date = new Date();
195
228
  const dateString = timestamp ?? `${date.toLocaleDateString()} ${date.toLocaleTimeString()}`;
229
+
230
+ const renderMessage = () => {
231
+ if (isLoading) {
232
+ return <MessageLoading loadingWord={loadingWord} />;
233
+ }
234
+ if (isEditable) {
235
+ return (
236
+ <>
237
+ {beforeMainContent && <>{beforeMainContent}</>}
238
+ <MessageInput
239
+ content={content}
240
+ editPlaceholder={editPlaceholder}
241
+ updateWord={updateWord}
242
+ cancelWord={cancelWord}
243
+ onEditUpdate={(event, text) => {
244
+ onEditUpdate && onEditUpdate(event);
245
+ setMessageText(text);
246
+ }}
247
+ onEditCancel={onEditCancel}
248
+ {...editFormProps}
249
+ />
250
+ </>
251
+ );
252
+ }
253
+ return (
254
+ <>
255
+ {beforeMainContent && <>{beforeMainContent}</>}
256
+ {error ? (
257
+ <ErrorMessage {...error} />
258
+ ) : (
259
+ <Markdown
260
+ components={{
261
+ p: (props) => <TextMessage component={ContentVariants.p} {...props} />,
262
+ code: ({ children, ...props }) => (
263
+ <CodeBlockMessage {...props} {...codeBlockProps}>
264
+ {children}
265
+ </CodeBlockMessage>
266
+ ),
267
+ h1: (props) => <TextMessage component={ContentVariants.h1} {...props} />,
268
+ h2: (props) => <TextMessage component={ContentVariants.h2} {...props} />,
269
+ h3: (props) => <TextMessage component={ContentVariants.h3} {...props} />,
270
+ h4: (props) => <TextMessage component={ContentVariants.h4} {...props} />,
271
+ h5: (props) => <TextMessage component={ContentVariants.h5} {...props} />,
272
+ h6: (props) => <TextMessage component={ContentVariants.h6} {...props} />,
273
+ blockquote: (props) => <TextMessage component={ContentVariants.blockquote} {...props} />,
274
+ ul: (props) => <UnorderedListMessage {...props} />,
275
+ ol: (props) => <OrderedListMessage {...props} />,
276
+ li: (props) => <ListItemMessage {...props} />,
277
+ table: (props) => <TableMessage {...props} {...tableProps} />,
278
+ tbody: (props) => <TbodyMessage {...props} />,
279
+ thead: (props) => <TheadMessage {...props} />,
280
+ tr: (props) => <TrMessage {...props} />,
281
+ td: (props) => {
282
+ // Conflicts with Td type
283
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
284
+ const { width, ...rest } = props;
285
+ return <TdMessage {...rest} />;
286
+ },
287
+ th: (props) => <ThMessage {...props} />,
288
+ img: (props) => <ImageMessage {...props} />,
289
+ a: (props) => (
290
+ <LinkMessage href={props.href} rel={props.rel} target={props.target} {...linkProps}>
291
+ {props.children}
292
+ </LinkMessage>
293
+ )
294
+ }}
295
+ remarkPlugins={[remarkGfm]}
296
+ rehypePlugins={rehypePlugins}
297
+ >
298
+ {messageText}
299
+ </Markdown>
300
+ )}
301
+ </>
302
+ );
303
+ };
304
+
196
305
  return (
197
306
  <section
198
307
  aria-label={`Message from ${role} - ${dateString}`}
@@ -225,59 +334,8 @@ export const MessageBase: React.FunctionComponent<MessageProps> = ({
225
334
  </div>
226
335
  <div className="pf-chatbot__message-response">
227
336
  <div className="pf-chatbot__message-and-actions">
228
- {isLoading ? (
229
- <MessageLoading loadingWord={loadingWord} />
230
- ) : (
231
- <>
232
- {beforeMainContent && <>{beforeMainContent}</>}
233
- {error ? (
234
- <ErrorMessage {...error} />
235
- ) : (
236
- <Markdown
237
- components={{
238
- p: (props) => <TextMessage component={ContentVariants.p} {...props} />,
239
- code: ({ children, ...props }) => (
240
- <CodeBlockMessage {...props} {...codeBlockProps}>
241
- {children}
242
- </CodeBlockMessage>
243
- ),
244
- h1: (props) => <TextMessage component={ContentVariants.h1} {...props} />,
245
- h2: (props) => <TextMessage component={ContentVariants.h2} {...props} />,
246
- h3: (props) => <TextMessage component={ContentVariants.h3} {...props} />,
247
- h4: (props) => <TextMessage component={ContentVariants.h4} {...props} />,
248
- h5: (props) => <TextMessage component={ContentVariants.h5} {...props} />,
249
- h6: (props) => <TextMessage component={ContentVariants.h6} {...props} />,
250
- blockquote: (props) => <TextMessage component={ContentVariants.blockquote} {...props} />,
251
- ul: (props) => <UnorderedListMessage {...props} />,
252
- ol: (props) => <OrderedListMessage {...props} />,
253
- li: (props) => <ListItemMessage {...props} />,
254
- table: (props) => <TableMessage {...props} {...tableProps} />,
255
- tbody: (props) => <TbodyMessage {...props} />,
256
- thead: (props) => <TheadMessage {...props} />,
257
- tr: (props) => <TrMessage {...props} />,
258
- td: (props) => {
259
- // Conflicts with Td type
260
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
261
- const { width, ...rest } = props;
262
- return <TdMessage {...rest} />;
263
- },
264
- th: (props) => <ThMessage {...props} />,
265
- img: (props) => <ImageMessage {...props} />,
266
- a: (props) => (
267
- <LinkMessage href={props.href} rel={props.rel} target={props.target}>
268
- {props.children}
269
- </LinkMessage>
270
- )
271
- }}
272
- remarkPlugins={[remarkGfm]}
273
- rehypePlugins={rehypePlugins}
274
- >
275
- {content}
276
- </Markdown>
277
- )}
278
- {afterMainContent && <>{afterMainContent}</>}
279
- </>
280
- )}
337
+ {renderMessage()}
338
+ {afterMainContent && <>{afterMainContent}</>}
281
339
  {!isLoading && sources && <SourcesCard {...sources} />}
282
340
  {quickStarts && quickStarts.quickStart && (
283
341
  <QuickStartTile
@@ -0,0 +1,59 @@
1
+ // ============================================================================
2
+ // Chatbot Main - Message Input
3
+ // ============================================================================
4
+
5
+ import React from 'react';
6
+ import { ActionGroup, Button, Form, FormProps, TextArea } from '@patternfly/react-core';
7
+
8
+ export interface MessageInputProps extends FormProps {
9
+ /** Placeholder for edit input */
10
+ editPlaceholder?: string;
11
+ /** Label for the English word "Update" used in edit mode. */
12
+ updateWord?: string;
13
+ /** Label for the English word "Cancel" used in edit mode. */
14
+ cancelWord?: string;
15
+ /** Callback function for when edit mode update button is clicked */
16
+ onEditUpdate?: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>, value: string) => void;
17
+ /** Callback functionf or when edit cancel update button is clicked */
18
+ onEditCancel?: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
19
+ /** Message text */
20
+ content?: string;
21
+ }
22
+
23
+ const MessageInput: React.FunctionComponent<MessageInputProps> = ({
24
+ editPlaceholder = 'Edit prompt message...',
25
+ updateWord = 'Update',
26
+ cancelWord = 'Cancel',
27
+ onEditUpdate,
28
+ onEditCancel,
29
+ content,
30
+ ...props
31
+ }: MessageInputProps) => {
32
+ const [messageText, setMessageText] = React.useState(content ?? '');
33
+
34
+ const onChange = (event: React.FormEvent<HTMLTextAreaElement>, value: string) => {
35
+ setMessageText(value);
36
+ };
37
+
38
+ return (
39
+ <Form {...props}>
40
+ <TextArea
41
+ placeholder={editPlaceholder}
42
+ value={messageText}
43
+ onChange={onChange}
44
+ aria-label={editPlaceholder}
45
+ autoResize
46
+ />
47
+ <ActionGroup className="pf-chatbot__message-edit-buttons">
48
+ <Button variant="primary" onClick={(event) => onEditUpdate && onEditUpdate(event, messageText)}>
49
+ {updateWord}
50
+ </Button>
51
+ <Button variant="secondary" onClick={onEditCancel}>
52
+ {cancelWord}
53
+ </Button>
54
+ </ActionGroup>
55
+ </Form>
56
+ );
57
+ };
58
+
59
+ export default MessageInput;
@@ -81,7 +81,7 @@ export const MicrophoneButton: React.FunctionComponent<MicrophoneButtonProps> =
81
81
 
82
82
  setSpeechRecognition(recognition);
83
83
  }
84
- }, [onSpeechRecognition]);
84
+ }, [onSpeechRecognition, language, onIsListeningChange]);
85
85
 
86
86
  if (!speechRecognition) {
87
87
  return null;
@@ -46,7 +46,7 @@ const MessageBoxBase: React.FunctionComponent<MessageBoxProps> = ({
46
46
  setAtTop(scrollTop === 0);
47
47
  setAtBottom(Math.round(scrollTop) + Math.round(clientHeight) >= Math.round(scrollHeight) - 1); // rounding means it could be within a pixel of the bottom
48
48
  }
49
- }, []);
49
+ }, [messageBoxRef]);
50
50
 
51
51
  const checkOverflow = React.useCallback(() => {
52
52
  const element = messageBoxRef.current;
@@ -54,21 +54,21 @@ const MessageBoxBase: React.FunctionComponent<MessageBoxProps> = ({
54
54
  const { scrollHeight, clientHeight } = element;
55
55
  setIsOverflowing(scrollHeight >= clientHeight);
56
56
  }
57
- }, []);
57
+ }, [messageBoxRef]);
58
58
 
59
59
  const scrollToTop = React.useCallback(() => {
60
60
  const element = messageBoxRef.current;
61
61
  if (element) {
62
62
  element.scrollTo({ top: 0, behavior: 'smooth' });
63
63
  }
64
- }, []);
64
+ }, [messageBoxRef]);
65
65
 
66
66
  const scrollToBottom = React.useCallback(() => {
67
67
  const element = messageBoxRef.current;
68
68
  if (element) {
69
69
  element.scrollTo({ top: element.scrollHeight, behavior: 'smooth' });
70
70
  }
71
- }, []);
71
+ }, [messageBoxRef]);
72
72
 
73
73
  // Detect scroll position
74
74
  React.useEffect(() => {
@@ -85,7 +85,7 @@ const MessageBoxBase: React.FunctionComponent<MessageBoxProps> = ({
85
85
  element.removeEventListener('scroll', handleScroll);
86
86
  };
87
87
  }
88
- }, [checkOverflow, handleScroll]);
88
+ }, [checkOverflow, handleScroll, messageBoxRef]);
89
89
 
90
90
  return (
91
91
  <>
@@ -16,7 +16,7 @@
16
16
  box-shadow: var(--pf-t--global--box-shadow--sm);
17
17
  }
18
18
 
19
- .pf-chatbot__sources-card-body {
19
+ .pf-chatbot__sources-card-body-text {
20
20
  display: block;
21
21
  display: -webkit-box;
22
22
  height: 2.8125rem;
@@ -25,11 +25,6 @@
25
25
  -webkit-box-orient: vertical;
26
26
  overflow: hidden;
27
27
  text-overflow: ellipsis;
28
- margin-bottom: var(--pf-t--global--spacer--md);
29
- }
30
-
31
- .pf-chatbot__sources-card-no-footer {
32
- margin-bottom: var(--pf-t--global--spacer--lg);
33
28
  }
34
29
 
35
30
  .pf-chatbot__sources-card-footer-container {
@@ -38,13 +33,14 @@
38
33
  var(--pf-t--global--spacer--sm) !important;
39
34
  .pf-chatbot__sources-card-footer {
40
35
  display: flex;
41
- justify-content: space-between;
42
36
  align-items: center;
43
37
 
44
38
  &-buttons {
45
39
  display: flex;
46
40
  gap: var(--pf-t--global--spacer--xs);
47
41
  align-items: center;
42
+ justify-content: space-between;
43
+ flex: 1;
48
44
 
49
45
  .pf-v6-c-button {
50
46
  border-radius: var(--pf-t--global--border--radius--pill);
@@ -5,18 +5,13 @@ import '@testing-library/jest-dom';
5
5
  import SourcesCard from './SourcesCard';
6
6
 
7
7
  describe('SourcesCard', () => {
8
- it('should render card', () => {
9
- const { container } = render(<SourcesCard sources={[{ link: '' }]} />);
10
- expect(container).toMatchSnapshot();
11
- });
12
-
13
8
  it('should render card correctly if one source with only a link is passed in', () => {
14
9
  render(<SourcesCard sources={[{ link: '' }]} />);
15
10
  expect(screen.getByText('1 source')).toBeTruthy();
16
11
  expect(screen.getByText('Source 1')).toBeTruthy();
17
12
  // no buttons or navigation when there is only 1 source
18
13
  expect(screen.queryByRole('button')).toBeFalsy();
19
- expect(screen.queryByText('1 of 1')).toBeFalsy();
14
+ expect(screen.queryByText('1/1')).toBeFalsy();
20
15
  });
21
16
 
22
17
  it('should render card correctly if one source with a title is passed in', () => {
@@ -53,7 +48,7 @@ describe('SourcesCard', () => {
53
48
  );
54
49
  expect(screen.getByText('2 sources')).toBeTruthy();
55
50
  expect(screen.getByText('How to make an apple pie')).toBeTruthy();
56
- expect(screen.getByText('1 of 2')).toBeTruthy();
51
+ expect(screen.getByText('1/2')).toBeTruthy();
57
52
  screen.getByRole('button', { name: /Go to previous page/i });
58
53
  screen.getByRole('button', { name: /Go to next page/i });
59
54
  });
@@ -68,12 +63,12 @@ describe('SourcesCard', () => {
68
63
  />
69
64
  );
70
65
  expect(screen.getByText('How to make an apple pie')).toBeTruthy();
71
- expect(screen.getByText('1 of 2')).toBeTruthy();
66
+ expect(screen.getByText('1/2')).toBeTruthy();
72
67
  expect(screen.getByRole('button', { name: /Go to previous page/i })).toBeDisabled();
73
68
  await userEvent.click(screen.getByRole('button', { name: /Go to next page/i }));
74
69
  expect(screen.queryByText('How to make an apple pie')).toBeFalsy();
75
70
  expect(screen.getByText('How to make cookies')).toBeTruthy();
76
- expect(screen.getByText('2 of 2')).toBeTruthy();
71
+ expect(screen.getByText('2/2')).toBeTruthy();
77
72
  expect(screen.getByRole('button', { name: /Go to previous page/i })).toBeEnabled();
78
73
  expect(screen.getByRole('button', { name: /Go to next page/i })).toBeDisabled();
79
74
  });
@@ -106,19 +101,6 @@ describe('SourcesCard', () => {
106
101
  expect(screen.getByRole('button', { name: /Go to next page/i })).toBeDisabled();
107
102
  });
108
103
 
109
- it('should change ofWord appropriately', () => {
110
- render(
111
- <SourcesCard
112
- sources={[
113
- { title: 'How to make an apple pie', link: '' },
114
- { title: 'How to make cookies', link: '' }
115
- ]}
116
- ofWord={'de'}
117
- />
118
- );
119
- expect(screen.getByText('1 de 2')).toBeTruthy();
120
- });
121
-
122
104
  it('should render navigation aria label appropriately', () => {
123
105
  render(
124
106
  <SourcesCard
@@ -235,4 +217,30 @@ describe('SourcesCard', () => {
235
217
  await userEvent.click(screen.getByRole('button', { name: /Go to previous page/i }));
236
218
  expect(spy).toHaveBeenCalledTimes(2);
237
219
  });
220
+
221
+ it('should handle showMore appropriately', async () => {
222
+ render(
223
+ <SourcesCard
224
+ sources={[
225
+ {
226
+ title: 'Getting started with Red Hat OpenShift',
227
+ link: '#',
228
+ body: 'Red Hat OpenShift on IBM Cloud is a managed offering to create your own cluster of compute hosts where you can deploy and manage containerized apps on IBM Cloud ...',
229
+ hasShowMore: true
230
+ },
231
+ {
232
+ title: 'Azure Red Hat OpenShift documentation',
233
+ link: '#',
234
+ body: 'Microsoft Azure Red Hat OpenShift allows you to deploy a production ready Red Hat OpenShift cluster in Azure ...'
235
+ },
236
+ {
237
+ title: 'OKD Documentation: Home',
238
+ link: '#',
239
+ body: 'OKD is a distribution of Kubernetes optimized for continuous application development and multi-tenant deployment. OKD also serves as the upstream code base upon ...'
240
+ }
241
+ ]}
242
+ />
243
+ );
244
+ expect(screen.getByRole('region')).toHaveAttribute('class', 'pf-v6-c-expandable-section__content');
245
+ });
238
246
  });