@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.
- package/dist/cjs/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.d.ts +4 -0
- package/dist/cjs/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.js +7 -1
- package/dist/cjs/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.test.js +23 -0
- package/dist/cjs/Message/Message.d.ts +17 -1
- package/dist/cjs/Message/Message.js +53 -34
- package/dist/cjs/Message/Message.test.js +52 -0
- package/dist/cjs/Message/MessageInput.d.ts +18 -0
- package/dist/cjs/Message/MessageInput.js +34 -0
- package/dist/cjs/MessageBar/MicrophoneButton.js +1 -1
- package/dist/cjs/MessageBox/MessageBox.js +5 -5
- package/dist/cjs/SourcesCard/SourcesCard.d.ts +7 -1
- package/dist/cjs/SourcesCard/SourcesCard.js +16 -10
- package/dist/cjs/SourcesCard/SourcesCard.test.js +25 -15
- package/dist/cjs/tracking/console_tracking_provider.d.ts +4 -5
- package/dist/cjs/tracking/console_tracking_provider.js +22 -15
- package/dist/cjs/tracking/posthog_tracking_provider.d.ts +2 -2
- package/dist/cjs/tracking/posthog_tracking_provider.js +21 -12
- package/dist/cjs/tracking/segment_tracking_provider.d.ts +2 -2
- package/dist/cjs/tracking/segment_tracking_provider.js +21 -12
- package/dist/cjs/tracking/trackingProviderProxy.d.ts +1 -1
- package/dist/cjs/tracking/trackingProviderProxy.js +2 -2
- package/dist/cjs/tracking/tracking_api.d.ts +1 -1
- package/dist/cjs/tracking/tracking_registry.js +46 -12
- package/dist/cjs/tracking/tracking_spi.d.ts +15 -5
- package/dist/cjs/tracking/tracking_spi.js +9 -0
- package/dist/cjs/tracking/umami_tracking_provider.d.ts +6 -2
- package/dist/cjs/tracking/umami_tracking_provider.js +66 -22
- package/dist/css/main.css +7 -7
- package/dist/css/main.css.map +1 -1
- package/dist/esm/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.d.ts +4 -0
- package/dist/esm/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.js +7 -1
- package/dist/esm/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.test.js +23 -0
- package/dist/esm/Message/Message.d.ts +17 -1
- package/dist/esm/Message/Message.js +53 -34
- package/dist/esm/Message/Message.test.js +52 -0
- package/dist/esm/Message/MessageInput.d.ts +18 -0
- package/dist/esm/Message/MessageInput.js +29 -0
- package/dist/esm/MessageBar/MicrophoneButton.js +1 -1
- package/dist/esm/MessageBox/MessageBox.js +5 -5
- package/dist/esm/SourcesCard/SourcesCard.d.ts +7 -1
- package/dist/esm/SourcesCard/SourcesCard.js +17 -11
- package/dist/esm/SourcesCard/SourcesCard.test.js +25 -15
- package/dist/esm/tracking/console_tracking_provider.d.ts +4 -5
- package/dist/esm/tracking/console_tracking_provider.js +22 -15
- package/dist/esm/tracking/posthog_tracking_provider.d.ts +2 -2
- package/dist/esm/tracking/posthog_tracking_provider.js +21 -12
- package/dist/esm/tracking/segment_tracking_provider.d.ts +2 -2
- package/dist/esm/tracking/segment_tracking_provider.js +21 -12
- package/dist/esm/tracking/trackingProviderProxy.d.ts +1 -1
- package/dist/esm/tracking/trackingProviderProxy.js +2 -2
- package/dist/esm/tracking/tracking_api.d.ts +1 -1
- package/dist/esm/tracking/tracking_registry.js +46 -12
- package/dist/esm/tracking/tracking_spi.d.ts +15 -5
- package/dist/esm/tracking/tracking_spi.js +8 -1
- package/dist/esm/tracking/umami_tracking_provider.d.ts +6 -2
- package/dist/esm/tracking/umami_tracking_provider.js +66 -22
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/patternfly-docs/content/extensions/chatbot/examples/Analytics/Analytics.md +18 -14
- package/patternfly-docs/content/extensions/chatbot/examples/Messages/BotMessage.tsx +74 -104
- package/patternfly-docs/content/extensions/chatbot/examples/Messages/FileDetailsLabel.tsx +48 -37
- package/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithQuickResponses.tsx +10 -0
- package/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithSources.tsx +51 -14
- package/patternfly-docs/content/extensions/chatbot/examples/Messages/Messages.md +3 -1
- package/patternfly-docs/content/extensions/chatbot/examples/Messages/UserMessage.tsx +80 -104
- package/patternfly-docs/content/extensions/chatbot/examples/UI/ChatbotHeaderDrawer.tsx +35 -2
- package/patternfly-docs/content/extensions/chatbot/examples/UI/ChatbotHeaderDrawerResizable.tsx +13 -2
- package/patternfly-docs/content/extensions/chatbot/examples/UI/UI.md +1 -1
- package/patternfly-docs/content/extensions/chatbot/examples/demos/Chatbot.tsx +6 -3
- package/patternfly-docs/content/extensions/chatbot/examples/demos/ChatbotAttachment.tsx +2 -0
- package/patternfly-docs/content/extensions/chatbot/examples/demos/ChatbotAttachmentMenu.tsx +2 -0
- package/patternfly-docs/content/extensions/chatbot/examples/demos/ChatbotInDrawer.tsx +2 -0
- package/patternfly-docs/content/extensions/chatbot/examples/demos/EmbeddedChatbot.tsx +2 -0
- package/patternfly-docs/content/extensions/chatbot/examples/demos/EmbeddedComparisonChatbot.tsx +62 -57
- package/patternfly-docs/content/extensions/chatbot/examples/demos/Feedback.tsx +2 -0
- package/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.test.tsx +53 -0
- package/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.tsx +14 -0
- package/src/Message/Message.scss +4 -0
- package/src/Message/Message.test.tsx +62 -0
- package/src/Message/Message.tsx +111 -53
- package/src/Message/MessageInput.tsx +59 -0
- package/src/MessageBar/MicrophoneButton.tsx +1 -1
- package/src/MessageBox/MessageBox.tsx +5 -5
- package/src/SourcesCard/SourcesCard.scss +3 -7
- package/src/SourcesCard/SourcesCard.test.tsx +30 -22
- package/src/SourcesCard/SourcesCard.tsx +54 -12
- package/src/tracking/console_tracking_provider.ts +21 -17
- package/src/tracking/posthog_tracking_provider.ts +20 -13
- package/src/tracking/segment_tracking_provider.ts +20 -13
- package/src/tracking/trackingProviderProxy.ts +2 -2
- package/src/tracking/tracking_api.ts +1 -1
- package/src/tracking/tracking_registry.ts +46 -13
- package/src/tracking/tracking_spi.ts +18 -7
- package/src/tracking/umami_tracking_provider.ts +76 -20
- 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>
|
package/src/Message/Message.scss
CHANGED
@@ -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
|
});
|
package/src/Message/Message.tsx
CHANGED
@@ -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
|
-
{
|
229
|
-
|
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
|
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
|
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
|
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
|
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
|
});
|