@patternfly/chatbot 2.2.0-prerelease.44 → 2.2.0-prerelease.46
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/Message/Message.d.ts +15 -1
- package/dist/cjs/Message/Message.js +53 -34
- package/dist/cjs/Message/Message.test.js +46 -0
- package/dist/cjs/Message/MessageInput.d.ts +18 -0
- package/dist/cjs/Message/MessageInput.js +34 -0
- package/dist/cjs/SourcesCard/SourcesCard.d.ts +6 -1
- package/dist/cjs/SourcesCard/SourcesCard.js +14 -9
- package/dist/cjs/SourcesCard/SourcesCard.test.js +25 -11
- package/dist/css/main.css +7 -7
- package/dist/css/main.css.map +1 -1
- package/dist/esm/Message/Message.d.ts +15 -1
- package/dist/esm/Message/Message.js +53 -34
- package/dist/esm/Message/Message.test.js +46 -0
- package/dist/esm/Message/MessageInput.d.ts +18 -0
- package/dist/esm/Message/MessageInput.js +29 -0
- package/dist/esm/SourcesCard/SourcesCard.d.ts +6 -1
- package/dist/esm/SourcesCard/SourcesCard.js +15 -10
- package/dist/esm/SourcesCard/SourcesCard.test.js +25 -11
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/patternfly-docs/content/extensions/chatbot/examples/Messages/BotMessage.tsx +1 -1
- package/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithSources.tsx +34 -5
- package/patternfly-docs/content/extensions/chatbot/examples/Messages/Messages.md +1 -1
- package/patternfly-docs/content/extensions/chatbot/examples/Messages/UserMessage.tsx +61 -14
- package/src/Message/Message.scss +4 -0
- package/src/Message/Message.test.tsx +48 -0
- package/src/Message/Message.tsx +107 -53
- package/src/Message/MessageInput.tsx +59 -0
- package/src/SourcesCard/SourcesCard.scss +3 -7
- package/src/SourcesCard/SourcesCard.test.tsx +30 -17
- package/src/SourcesCard/SourcesCard.tsx +41 -11
@@ -6,6 +6,7 @@ import { AlertActionLink, Form, FormGroup, Radio } from '@patternfly/react-core'
|
|
6
6
|
|
7
7
|
export const UserMessageExample: React.FunctionComponent = () => {
|
8
8
|
const [variant, setVariant] = React.useState('code');
|
9
|
+
const [isEditable, setIsEditable] = React.useState(true);
|
9
10
|
|
10
11
|
/* eslint-disable indent */
|
11
12
|
const renderContent = () => {
|
@@ -33,7 +34,7 @@ export const UserMessageExample: React.FunctionComponent = () => {
|
|
33
34
|
case 'image':
|
34
35
|
return image;
|
35
36
|
default:
|
36
|
-
return;
|
37
|
+
return '';
|
37
38
|
}
|
38
39
|
};
|
39
40
|
/* eslint-enable indent */
|
@@ -175,88 +176,131 @@ _Italic text, formatted with single underscores_
|
|
175
176
|
<FormGroup role="radiogroup" isInline fieldId="user-message-type" label="Message content type">
|
176
177
|
<Radio
|
177
178
|
isChecked={variant === 'code'}
|
178
|
-
onChange={() =>
|
179
|
+
onChange={() => {
|
180
|
+
setVariant('code');
|
181
|
+
setIsEditable(true);
|
182
|
+
}}
|
179
183
|
name="user-message-type"
|
180
184
|
label="Code"
|
181
185
|
id="user-code"
|
182
186
|
/>
|
183
187
|
<Radio
|
184
188
|
isChecked={variant === 'inlineCode'}
|
185
|
-
onChange={() =>
|
189
|
+
onChange={() => {
|
190
|
+
setVariant('inlineCode');
|
191
|
+
setIsEditable(true);
|
192
|
+
}}
|
186
193
|
name="user-message-type"
|
187
194
|
label="Inline code"
|
188
195
|
id="user-inline-code"
|
189
196
|
/>
|
190
197
|
<Radio
|
191
198
|
isChecked={variant === 'heading'}
|
192
|
-
onChange={() =>
|
199
|
+
onChange={() => {
|
200
|
+
setVariant('heading');
|
201
|
+
setIsEditable(true);
|
202
|
+
}}
|
193
203
|
name="user-message-type"
|
194
204
|
label="Heading"
|
195
205
|
id="user-heading"
|
196
206
|
/>
|
197
207
|
<Radio
|
198
208
|
isChecked={variant === 'blockQuotes'}
|
199
|
-
onChange={() =>
|
209
|
+
onChange={() => {
|
210
|
+
setVariant('blockQuotes');
|
211
|
+
setIsEditable(true);
|
212
|
+
}}
|
200
213
|
name="user-message-type"
|
201
214
|
label="Block quote"
|
202
215
|
id="user-block-quotes"
|
203
216
|
/>
|
204
217
|
<Radio
|
205
218
|
isChecked={variant === 'emphasis'}
|
206
|
-
onChange={() =>
|
219
|
+
onChange={() => {
|
220
|
+
setVariant('emphasis');
|
221
|
+
setIsEditable(true);
|
222
|
+
}}
|
207
223
|
name="user-message-type"
|
208
224
|
label="Emphasis"
|
209
225
|
id="user-emphasis"
|
210
226
|
/>
|
211
227
|
<Radio
|
212
228
|
isChecked={variant === 'link'}
|
213
|
-
onChange={() =>
|
229
|
+
onChange={() => {
|
230
|
+
setVariant('link');
|
231
|
+
setIsEditable(true);
|
232
|
+
}}
|
214
233
|
name="user-message-type"
|
215
234
|
label="Link"
|
216
235
|
id="user-link"
|
217
236
|
/>
|
218
237
|
<Radio
|
219
238
|
isChecked={variant === 'unorderedList'}
|
220
|
-
onChange={() =>
|
239
|
+
onChange={() => {
|
240
|
+
setVariant('unorderedList');
|
241
|
+
setIsEditable(true);
|
242
|
+
}}
|
221
243
|
name="user-message-type"
|
222
244
|
label="Unordered list"
|
223
245
|
id="user-unordered-list"
|
224
246
|
/>
|
225
247
|
<Radio
|
226
248
|
isChecked={variant === 'orderedList'}
|
227
|
-
onChange={() =>
|
249
|
+
onChange={() => {
|
250
|
+
setVariant('orderedList');
|
251
|
+
setIsEditable(true);
|
252
|
+
}}
|
228
253
|
name="user-message-type"
|
229
254
|
label="Ordered list"
|
230
255
|
id="user-ordered-list"
|
231
256
|
/>
|
232
257
|
<Radio
|
233
258
|
isChecked={variant === 'moreComplexList'}
|
234
|
-
onChange={() =>
|
259
|
+
onChange={() => {
|
260
|
+
setVariant('moreComplexList');
|
261
|
+
setIsEditable(true);
|
262
|
+
}}
|
235
263
|
name="user-message-type"
|
236
264
|
label="More complex list"
|
237
265
|
id="user-more-complex-list"
|
238
266
|
/>
|
239
267
|
<Radio
|
240
268
|
isChecked={variant === 'table'}
|
241
|
-
onChange={() =>
|
269
|
+
onChange={() => {
|
270
|
+
setVariant('table');
|
271
|
+
setIsEditable(true);
|
272
|
+
}}
|
242
273
|
name="user-message-type"
|
243
274
|
label="Table"
|
244
275
|
id="user-table"
|
245
276
|
/>
|
246
277
|
<Radio
|
247
278
|
isChecked={variant === 'image'}
|
248
|
-
onChange={() =>
|
279
|
+
onChange={() => {
|
280
|
+
setVariant('image');
|
281
|
+
setIsEditable(true);
|
282
|
+
}}
|
249
283
|
name="user-message-type"
|
250
284
|
label="Image"
|
251
285
|
id="user-image"
|
252
286
|
/>
|
253
287
|
<Radio
|
254
288
|
isChecked={variant === 'error'}
|
255
|
-
onChange={() =>
|
256
|
-
|
289
|
+
onChange={() => {
|
290
|
+
setVariant('error');
|
291
|
+
setIsEditable(true);
|
292
|
+
}}
|
293
|
+
name="user-message-type"
|
257
294
|
label="Error"
|
258
295
|
id="user-error"
|
259
296
|
/>
|
297
|
+
<Radio
|
298
|
+
isChecked={variant === 'editable'}
|
299
|
+
onChange={() => setVariant('editable')}
|
300
|
+
name="user-message-type"
|
301
|
+
label="Editable"
|
302
|
+
id="user-edit"
|
303
|
+
/>
|
260
304
|
</FormGroup>
|
261
305
|
</Form>
|
262
306
|
<Message
|
@@ -267,7 +311,10 @@ _Italic text, formatted with single underscores_
|
|
267
311
|
tableProps={
|
268
312
|
variant === 'table' ? { 'aria-label': 'App information and user roles for user messages' } : undefined
|
269
313
|
}
|
314
|
+
isEditable={variant === 'editable' ? isEditable : false}
|
270
315
|
error={variant === 'error' ? error : undefined}
|
316
|
+
onEditUpdate={() => setIsEditable(false)}
|
317
|
+
onEditCancel={() => setIsEditable(false)}
|
271
318
|
/>
|
272
319
|
</>
|
273
320
|
);
|
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';
|
@@ -815,4 +815,52 @@ describe('Message', () => {
|
|
815
815
|
expect(screen.getByRole('heading', { name: /Could not load chat/i })).toBeTruthy();
|
816
816
|
expect(screen.queryByText('Test')).toBeFalsy();
|
817
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
|
+
});
|
818
866
|
});
|
package/src/Message/Message.tsx
CHANGED
@@ -12,6 +12,7 @@ import {
|
|
12
12
|
AvatarProps,
|
13
13
|
ButtonProps,
|
14
14
|
ContentVariants,
|
15
|
+
FormProps,
|
15
16
|
Label,
|
16
17
|
LabelGroupProps,
|
17
18
|
Timestamp,
|
@@ -45,6 +46,7 @@ import rehypeSanitize from 'rehype-sanitize';
|
|
45
46
|
import { PluggableList } from 'react-markdown/lib';
|
46
47
|
import LinkMessage from './LinkMessage/LinkMessage';
|
47
48
|
import ErrorMessage from './ErrorMessage/ErrorMessage';
|
49
|
+
import MessageInput from './MessageInput';
|
48
50
|
|
49
51
|
export interface MessageAttachment {
|
50
52
|
/** Name of file attached to the message */
|
@@ -148,6 +150,20 @@ export interface MessageProps extends Omit<React.HTMLProps<HTMLDivElement>, 'rol
|
|
148
150
|
error?: AlertProps;
|
149
151
|
/** Props for links */
|
150
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;
|
151
167
|
}
|
152
168
|
|
153
169
|
export const MessageBase: React.FunctionComponent<MessageProps> = ({
|
@@ -178,8 +194,21 @@ export const MessageBase: React.FunctionComponent<MessageProps> = ({
|
|
178
194
|
additionalRehypePlugins = [],
|
179
195
|
linkProps,
|
180
196
|
error,
|
197
|
+
isEditable,
|
198
|
+
editPlaceholder = 'Edit prompt message...',
|
199
|
+
updateWord = 'Update',
|
200
|
+
cancelWord = 'Cancel',
|
201
|
+
onEditUpdate,
|
202
|
+
onEditCancel,
|
203
|
+
editFormProps,
|
181
204
|
...props
|
182
205
|
}: MessageProps) => {
|
206
|
+
const [messageText, setMessageText] = React.useState(content);
|
207
|
+
|
208
|
+
React.useEffect(() => {
|
209
|
+
setMessageText(content);
|
210
|
+
}, [content]);
|
211
|
+
|
183
212
|
const { beforeMainContent, afterMainContent, endContent } = extraContent || {};
|
184
213
|
let rehypePlugins: PluggableList = [rehypeUnwrapImages];
|
185
214
|
if (openLinkInNewTab) {
|
@@ -197,6 +226,82 @@ export const MessageBase: React.FunctionComponent<MessageProps> = ({
|
|
197
226
|
// Keep timestamps consistent between Timestamp component and aria-label
|
198
227
|
const date = new Date();
|
199
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
|
+
|
200
305
|
return (
|
201
306
|
<section
|
202
307
|
aria-label={`Message from ${role} - ${dateString}`}
|
@@ -229,59 +334,8 @@ export const MessageBase: React.FunctionComponent<MessageProps> = ({
|
|
229
334
|
</div>
|
230
335
|
<div className="pf-chatbot__message-response">
|
231
336
|
<div className="pf-chatbot__message-and-actions">
|
232
|
-
{
|
233
|
-
|
234
|
-
) : (
|
235
|
-
<>
|
236
|
-
{beforeMainContent && <>{beforeMainContent}</>}
|
237
|
-
{error ? (
|
238
|
-
<ErrorMessage {...error} />
|
239
|
-
) : (
|
240
|
-
<Markdown
|
241
|
-
components={{
|
242
|
-
p: (props) => <TextMessage component={ContentVariants.p} {...props} />,
|
243
|
-
code: ({ children, ...props }) => (
|
244
|
-
<CodeBlockMessage {...props} {...codeBlockProps}>
|
245
|
-
{children}
|
246
|
-
</CodeBlockMessage>
|
247
|
-
),
|
248
|
-
h1: (props) => <TextMessage component={ContentVariants.h1} {...props} />,
|
249
|
-
h2: (props) => <TextMessage component={ContentVariants.h2} {...props} />,
|
250
|
-
h3: (props) => <TextMessage component={ContentVariants.h3} {...props} />,
|
251
|
-
h4: (props) => <TextMessage component={ContentVariants.h4} {...props} />,
|
252
|
-
h5: (props) => <TextMessage component={ContentVariants.h5} {...props} />,
|
253
|
-
h6: (props) => <TextMessage component={ContentVariants.h6} {...props} />,
|
254
|
-
blockquote: (props) => <TextMessage component={ContentVariants.blockquote} {...props} />,
|
255
|
-
ul: (props) => <UnorderedListMessage {...props} />,
|
256
|
-
ol: (props) => <OrderedListMessage {...props} />,
|
257
|
-
li: (props) => <ListItemMessage {...props} />,
|
258
|
-
table: (props) => <TableMessage {...props} {...tableProps} />,
|
259
|
-
tbody: (props) => <TbodyMessage {...props} />,
|
260
|
-
thead: (props) => <TheadMessage {...props} />,
|
261
|
-
tr: (props) => <TrMessage {...props} />,
|
262
|
-
td: (props) => {
|
263
|
-
// Conflicts with Td type
|
264
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
265
|
-
const { width, ...rest } = props;
|
266
|
-
return <TdMessage {...rest} />;
|
267
|
-
},
|
268
|
-
th: (props) => <ThMessage {...props} />,
|
269
|
-
img: (props) => <ImageMessage {...props} />,
|
270
|
-
a: (props) => (
|
271
|
-
<LinkMessage href={props.href} rel={props.rel} target={props.target} {...linkProps}>
|
272
|
-
{props.children}
|
273
|
-
</LinkMessage>
|
274
|
-
)
|
275
|
-
}}
|
276
|
-
remarkPlugins={[remarkGfm]}
|
277
|
-
rehypePlugins={rehypePlugins}
|
278
|
-
>
|
279
|
-
{content}
|
280
|
-
</Markdown>
|
281
|
-
)}
|
282
|
-
{afterMainContent && <>{afterMainContent}</>}
|
283
|
-
</>
|
284
|
-
)}
|
337
|
+
{renderMessage()}
|
338
|
+
{afterMainContent && <>{afterMainContent}</>}
|
285
339
|
{!isLoading && sources && <SourcesCard {...sources} />}
|
286
340
|
{quickStarts && quickStarts.quickStart && (
|
287
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;
|
@@ -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);
|
@@ -11,7 +11,7 @@ describe('SourcesCard', () => {
|
|
11
11
|
expect(screen.getByText('Source 1')).toBeTruthy();
|
12
12
|
// no buttons or navigation when there is only 1 source
|
13
13
|
expect(screen.queryByRole('button')).toBeFalsy();
|
14
|
-
expect(screen.queryByText('1
|
14
|
+
expect(screen.queryByText('1/1')).toBeFalsy();
|
15
15
|
});
|
16
16
|
|
17
17
|
it('should render card correctly if one source with a title is passed in', () => {
|
@@ -48,7 +48,7 @@ describe('SourcesCard', () => {
|
|
48
48
|
);
|
49
49
|
expect(screen.getByText('2 sources')).toBeTruthy();
|
50
50
|
expect(screen.getByText('How to make an apple pie')).toBeTruthy();
|
51
|
-
expect(screen.getByText('1
|
51
|
+
expect(screen.getByText('1/2')).toBeTruthy();
|
52
52
|
screen.getByRole('button', { name: /Go to previous page/i });
|
53
53
|
screen.getByRole('button', { name: /Go to next page/i });
|
54
54
|
});
|
@@ -63,12 +63,12 @@ describe('SourcesCard', () => {
|
|
63
63
|
/>
|
64
64
|
);
|
65
65
|
expect(screen.getByText('How to make an apple pie')).toBeTruthy();
|
66
|
-
expect(screen.getByText('1
|
66
|
+
expect(screen.getByText('1/2')).toBeTruthy();
|
67
67
|
expect(screen.getByRole('button', { name: /Go to previous page/i })).toBeDisabled();
|
68
68
|
await userEvent.click(screen.getByRole('button', { name: /Go to next page/i }));
|
69
69
|
expect(screen.queryByText('How to make an apple pie')).toBeFalsy();
|
70
70
|
expect(screen.getByText('How to make cookies')).toBeTruthy();
|
71
|
-
expect(screen.getByText('2
|
71
|
+
expect(screen.getByText('2/2')).toBeTruthy();
|
72
72
|
expect(screen.getByRole('button', { name: /Go to previous page/i })).toBeEnabled();
|
73
73
|
expect(screen.getByRole('button', { name: /Go to next page/i })).toBeDisabled();
|
74
74
|
});
|
@@ -101,19 +101,6 @@ describe('SourcesCard', () => {
|
|
101
101
|
expect(screen.getByRole('button', { name: /Go to next page/i })).toBeDisabled();
|
102
102
|
});
|
103
103
|
|
104
|
-
it('should change ofWord appropriately', () => {
|
105
|
-
render(
|
106
|
-
<SourcesCard
|
107
|
-
sources={[
|
108
|
-
{ title: 'How to make an apple pie', link: '' },
|
109
|
-
{ title: 'How to make cookies', link: '' }
|
110
|
-
]}
|
111
|
-
ofWord={'de'}
|
112
|
-
/>
|
113
|
-
);
|
114
|
-
expect(screen.getByText('1 de 2')).toBeTruthy();
|
115
|
-
});
|
116
|
-
|
117
104
|
it('should render navigation aria label appropriately', () => {
|
118
105
|
render(
|
119
106
|
<SourcesCard
|
@@ -230,4 +217,30 @@ describe('SourcesCard', () => {
|
|
230
217
|
await userEvent.click(screen.getByRole('button', { name: /Go to previous page/i }));
|
231
218
|
expect(spy).toHaveBeenCalledTimes(2);
|
232
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
|
+
});
|
233
246
|
});
|