@patternfly/chatbot 6.6.0-prerelease.4 → 6.6.0-prerelease.6

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.
@@ -9,6 +9,23 @@ import rehypeExternalLinks from '../__mocks__/rehype-external-links';
9
9
  import { AlertActionLink, Button, CodeBlockAction } from '@patternfly/react-core';
10
10
  import { DeepThinkingProps } from '../DeepThinking';
11
11
 
12
+ // Mock the icon components
13
+ jest.mock('@patternfly/react-icons', () => ({
14
+ OutlinedThumbsUpIcon: () => <div>OutlinedThumbsUpIcon</div>,
15
+ ThumbsUpIcon: () => <div>ThumbsUpIcon</div>,
16
+ OutlinedThumbsDownIcon: () => <div>OutlinedThumbsDownIcon</div>,
17
+ ThumbsDownIcon: () => <div>ThumbsDownIcon</div>,
18
+ OutlinedCopyIcon: () => <div>OutlinedCopyIcon</div>,
19
+ DownloadIcon: () => <div>DownloadIcon</div>,
20
+ ExternalLinkAltIcon: () => <div>ExternalLinkAltIcon</div>,
21
+ VolumeUpIcon: () => <div>VolumeUpIcon</div>,
22
+ PencilAltIcon: () => <div>PencilAltIcon</div>,
23
+ CheckIcon: () => <div>CheckIcon</div>,
24
+ CloseIcon: () => <div>CloseIcon</div>,
25
+ ExternalLinkSquareAltIcon: () => <div>ExternalLinkSquareAltIcon</div>,
26
+ TimesIcon: () => <div>TimesIcon</div>
27
+ }));
28
+
12
29
  const ALL_ACTIONS = [
13
30
  { label: /Good response/i },
14
31
  { label: /Bad response/i },
@@ -1351,4 +1368,187 @@ describe('Message', () => {
1351
1368
  render(<Message alignment="end" avatar="./img" role="user" name="User" content="" />);
1352
1369
  expect(screen.getByRole('region')).toHaveClass('pf-m-end');
1353
1370
  });
1371
+
1372
+ // We're just testing the positive action here to ensure logic passes through as needed, the other actions are
1373
+ // tested in ResponseActions.test.tsx along with other aspects of this functionality
1374
+ it('should not swap icons when useFilledIconsOnClick is omitted', async () => {
1375
+ const user = userEvent.setup();
1376
+
1377
+ render(
1378
+ <Message
1379
+ avatar="./img"
1380
+ role="bot"
1381
+ name="Bot"
1382
+ content="Hi"
1383
+ actions={{
1384
+ positive: { onClick: jest.fn() }
1385
+ }}
1386
+ />
1387
+ );
1388
+
1389
+ expect(screen.getByText('OutlinedThumbsUpIcon')).toBeInTheDocument();
1390
+
1391
+ await user.click(screen.getByRole('button', { name: /Good response/i }));
1392
+
1393
+ expect(screen.getByText('OutlinedThumbsUpIcon')).toBeInTheDocument();
1394
+ expect(screen.queryByText('ThumbsUpIcon')).not.toBeInTheDocument();
1395
+ });
1396
+
1397
+ it('should swap icons when useFilledIconsOnClick is true', async () => {
1398
+ const user = userEvent.setup();
1399
+
1400
+ render(
1401
+ <Message
1402
+ avatar="./img"
1403
+ role="bot"
1404
+ name="Bot"
1405
+ content="Hi"
1406
+ actions={{
1407
+ positive: { onClick: jest.fn() }
1408
+ }}
1409
+ useFilledIconsOnClick
1410
+ />
1411
+ );
1412
+
1413
+ await user.click(screen.getByRole('button', { name: /Good response/i }));
1414
+
1415
+ expect(screen.getByText('ThumbsUpIcon')).toBeInTheDocument();
1416
+ expect(screen.queryByText('OutlinedThumbsUpIcon')).not.toBeInTheDocument();
1417
+ });
1418
+
1419
+ it('should apply pf-m-visible-interaction class to response actions when showActionsOnInteraction is true', () => {
1420
+ render(
1421
+ <Message
1422
+ avatar="./img"
1423
+ role="bot"
1424
+ name="Bot"
1425
+ content="Hi"
1426
+ showActionsOnInteraction
1427
+ actions={{
1428
+ positive: { onClick: jest.fn() }
1429
+ }}
1430
+ />
1431
+ );
1432
+
1433
+ const responseContainer = screen
1434
+ .getByRole('button', { name: 'Good response' })
1435
+ .closest('.pf-chatbot__response-actions');
1436
+ expect(responseContainer).toHaveClass('pf-m-visible-interaction');
1437
+ });
1438
+
1439
+ it('should not apply pf-m-visible-interaction class to response actions when showActionsOnInteraction is false', () => {
1440
+ render(
1441
+ <Message
1442
+ avatar="./img"
1443
+ role="bot"
1444
+ name="Bot"
1445
+ content="Hi"
1446
+ showActionsOnInteraction={false}
1447
+ actions={{
1448
+ positive: { onClick: jest.fn() }
1449
+ }}
1450
+ />
1451
+ );
1452
+
1453
+ const responseContainer = screen
1454
+ .getByRole('button', { name: 'Good response' })
1455
+ .closest('.pf-chatbot__response-actions');
1456
+ expect(responseContainer).not.toHaveClass('pf-m-visible-interaction');
1457
+ });
1458
+
1459
+ it('should not apply pf-m-visible-interaction class to response actions by default', () => {
1460
+ render(
1461
+ <Message
1462
+ avatar="./img"
1463
+ role="bot"
1464
+ name="Bot"
1465
+ content="Hi"
1466
+ actions={{
1467
+ positive: { onClick: jest.fn() }
1468
+ }}
1469
+ />
1470
+ );
1471
+
1472
+ const responseContainer = screen
1473
+ .getByRole('button', { name: 'Good response' })
1474
+ .closest('.pf-chatbot__response-actions');
1475
+ expect(responseContainer).not.toHaveClass('pf-m-visible-interaction');
1476
+ });
1477
+
1478
+ it('should apply pf-m-visible-interaction class to grouped actions container when showActionsOnInteraction is true', () => {
1479
+ render(
1480
+ <Message
1481
+ avatar="./img"
1482
+ role="bot"
1483
+ name="Bot"
1484
+ content="Hi"
1485
+ showActionsOnInteraction
1486
+ actions={[
1487
+ {
1488
+ positive: { onClick: jest.fn() },
1489
+ negative: { onClick: jest.fn() }
1490
+ },
1491
+ {
1492
+ copy: { onClick: jest.fn() }
1493
+ }
1494
+ ]}
1495
+ />
1496
+ );
1497
+
1498
+ const responseContainer = screen
1499
+ .getByRole('button', { name: 'Good response' })
1500
+ .closest('.pf-chatbot__response-actions-groups');
1501
+ expect(responseContainer).toHaveClass('pf-m-visible-interaction');
1502
+ });
1503
+
1504
+ it('should not apply pf-m-visible-interaction class to grouped actions container when showActionsOnInteraction is false', () => {
1505
+ render(
1506
+ <Message
1507
+ avatar="./img"
1508
+ role="bot"
1509
+ name="Bot"
1510
+ content="Hi"
1511
+ showActionsOnInteraction={false}
1512
+ actions={[
1513
+ {
1514
+ positive: { onClick: jest.fn() },
1515
+ negative: { onClick: jest.fn() }
1516
+ },
1517
+ {
1518
+ copy: { onClick: jest.fn() }
1519
+ }
1520
+ ]}
1521
+ />
1522
+ );
1523
+
1524
+ const responseContainer = screen
1525
+ .getByRole('button', { name: 'Good response' })
1526
+ .closest('.pf-chatbot__response-actions-groups');
1527
+ expect(responseContainer).not.toHaveClass('pf-m-visible-interaction');
1528
+ });
1529
+
1530
+ it('should not apply pf-m-visible-interaction class to grouped actions container by default', () => {
1531
+ render(
1532
+ <Message
1533
+ avatar="./img"
1534
+ role="bot"
1535
+ name="Bot"
1536
+ content="Hi"
1537
+ actions={[
1538
+ {
1539
+ positive: { onClick: jest.fn() },
1540
+ negative: { onClick: jest.fn() }
1541
+ },
1542
+ {
1543
+ copy: { onClick: jest.fn() }
1544
+ }
1545
+ ]}
1546
+ />
1547
+ );
1548
+
1549
+ const responseContainer = screen
1550
+ .getByRole('button', { name: 'Good response' })
1551
+ .closest('.pf-chatbot__response-actions-groups');
1552
+ expect(responseContainer).not.toHaveClass('pf-m-visible-interaction');
1553
+ });
1354
1554
  });
@@ -114,6 +114,10 @@ export interface MessageProps extends Omit<HTMLProps<HTMLDivElement>, 'role'> {
114
114
  * For finer control of multiple action groups, use persistActionSelection on each group.
115
115
  */
116
116
  persistActionSelection?: boolean;
117
+ /** Flag indicating whether the actions container is only visible when a message is hovered or an action would receive focus. Note
118
+ * that setting this to true will append tooltips inline instead of the document.body.
119
+ */
120
+ showActionsOnInteraction?: boolean;
117
121
  /** Sources for message */
118
122
  sources?: SourcesCardProps;
119
123
  /** Label for the English word "AI," used to tag messages with role "bot" */
@@ -197,6 +201,8 @@ export interface MessageProps extends Omit<HTMLProps<HTMLDivElement>, 'role'> {
197
201
  hasNoImagesInUserMessages?: boolean;
198
202
  /** Sets background colors to be appropriate on primary chatbot background */
199
203
  isPrimary?: boolean;
204
+ /** When true, automatically swaps to filled icon variants when predefined actions are clicked. */
205
+ useFilledIconsOnClick?: boolean;
200
206
  }
201
207
 
202
208
  export const MessageBase: FunctionComponent<MessageProps> = ({
@@ -212,6 +218,7 @@ export const MessageBase: FunctionComponent<MessageProps> = ({
212
218
  isLoading,
213
219
  actions,
214
220
  persistActionSelection,
221
+ showActionsOnInteraction = false,
215
222
  sources,
216
223
  botWord = 'AI',
217
224
  loadingWord = 'Loading message',
@@ -249,6 +256,7 @@ export const MessageBase: FunctionComponent<MessageProps> = ({
249
256
  toolCall,
250
257
  hasNoImagesInUserMessages = true,
251
258
  isPrimary,
259
+ useFilledIconsOnClick,
252
260
  ...props
253
261
  }: MessageProps) => {
254
262
  const [messageText, setMessageText] = useState(content);
@@ -379,17 +387,28 @@ export const MessageBase: FunctionComponent<MessageProps> = ({
379
387
  {!isLoading && !isEditable && actions && (
380
388
  <>
381
389
  {Array.isArray(actions) ? (
382
- <div className="pf-chatbot__response-actions-groups">
390
+ <div
391
+ className={css(
392
+ 'pf-chatbot__response-actions-groups',
393
+ showActionsOnInteraction && 'pf-m-visible-interaction'
394
+ )}
395
+ >
383
396
  {actions.map((actionGroup, index) => (
384
397
  <ResponseActions
385
398
  key={index}
386
399
  actions={actionGroup.actions || actionGroup}
387
400
  persistActionSelection={persistActionSelection || actionGroup.persistActionSelection}
401
+ useFilledIconsOnClick={useFilledIconsOnClick}
388
402
  />
389
403
  ))}
390
404
  </div>
391
405
  ) : (
392
- <ResponseActions actions={actions} persistActionSelection={persistActionSelection} />
406
+ <ResponseActions
407
+ actions={actions}
408
+ persistActionSelection={persistActionSelection}
409
+ useFilledIconsOnClick={useFilledIconsOnClick}
410
+ showActionsOnInteraction={showActionsOnInteraction}
411
+ />
393
412
  )}
394
413
  </>
395
414
  )}
@@ -5,6 +5,21 @@ import userEvent from '@testing-library/user-event';
5
5
  import { DownloadIcon, InfoCircleIcon, RedoIcon } from '@patternfly/react-icons';
6
6
  import Message from '../Message';
7
7
 
8
+ // Mock the icon components
9
+ jest.mock('@patternfly/react-icons', () => ({
10
+ OutlinedThumbsUpIcon: () => <div>OutlinedThumbsUpIcon</div>,
11
+ ThumbsUpIcon: () => <div>ThumbsUpIcon</div>,
12
+ OutlinedThumbsDownIcon: () => <div>OutlinedThumbsDownIcon</div>,
13
+ ThumbsDownIcon: () => <div>ThumbsDownIcon</div>,
14
+ OutlinedCopyIcon: () => <div>OutlinedCopyIcon</div>,
15
+ DownloadIcon: () => <div>DownloadIcon</div>,
16
+ InfoCircleIcon: () => <div>InfoCircleIcon</div>,
17
+ RedoIcon: () => <div>RedoIcon</div>,
18
+ ExternalLinkAltIcon: () => <div>ExternalLinkAltIcon</div>,
19
+ VolumeUpIcon: () => <div>VolumeUpIcon</div>,
20
+ PencilAltIcon: () => <div>PencilAltIcon</div>
21
+ }));
22
+
8
23
  const ALL_ACTIONS = [
9
24
  { type: 'positive', label: 'Good response', clickedLabel: 'Good response recorded' },
10
25
  { type: 'negative', label: 'Bad response', clickedLabel: 'Bad response recorded' },
@@ -421,4 +436,248 @@ describe('ResponseActions', () => {
421
436
  await userEvent.click(customBtn);
422
437
  expect(customBtn).not.toHaveClass('pf-chatbot__button--response-action-clicked');
423
438
  });
439
+
440
+ it('should apply pf-m-visible-interaction class when showActionsOnInteraction is true', () => {
441
+ render(
442
+ <ResponseActions
443
+ data-testid="test-id"
444
+ actions={{
445
+ positive: { onClick: jest.fn() },
446
+ negative: { onClick: jest.fn() }
447
+ }}
448
+ showActionsOnInteraction
449
+ />
450
+ );
451
+
452
+ expect(screen.getByTestId('test-id')).toHaveClass('pf-m-visible-interaction');
453
+ });
454
+
455
+ it('should not apply pf-m-visible-interaction class when showActionsOnInteraction is false', () => {
456
+ render(
457
+ <ResponseActions
458
+ data-testid="test-id"
459
+ actions={{
460
+ positive: { onClick: jest.fn() },
461
+ negative: { onClick: jest.fn() }
462
+ }}
463
+ showActionsOnInteraction={false}
464
+ />
465
+ );
466
+
467
+ expect(screen.getByTestId('test-id')).not.toHaveClass('pf-m-visible-interaction');
468
+ });
469
+
470
+ it('should not apply pf-m-visible-interaction class by default', () => {
471
+ render(
472
+ <ResponseActions
473
+ data-testid="test-id"
474
+ actions={{
475
+ positive: { onClick: jest.fn() },
476
+ negative: { onClick: jest.fn() }
477
+ }}
478
+ />
479
+ );
480
+
481
+ expect(screen.getByTestId('test-id')).not.toHaveClass('pf-m-visible-interaction');
482
+ });
483
+
484
+ it('should render with custom className', () => {
485
+ render(
486
+ <ResponseActions
487
+ data-testid="test-id"
488
+ actions={{
489
+ positive: { onClick: jest.fn() },
490
+ negative: { onClick: jest.fn() }
491
+ }}
492
+ className="custom-class"
493
+ />
494
+ );
495
+
496
+ expect(screen.getByTestId('test-id')).toHaveClass('custom-class');
497
+ });
498
+
499
+ describe('icon swapping with useFilledIconsOnClick', () => {
500
+ it('should render outline icons by default', () => {
501
+ render(
502
+ <ResponseActions
503
+ actions={{
504
+ positive: { onClick: jest.fn() },
505
+ negative: { onClick: jest.fn() }
506
+ }}
507
+ />
508
+ );
509
+
510
+ expect(screen.getByText('OutlinedThumbsUpIcon')).toBeInTheDocument();
511
+ expect(screen.getByText('OutlinedThumbsDownIcon')).toBeInTheDocument();
512
+
513
+ expect(screen.queryByText('ThumbsUpIcon')).not.toBeInTheDocument();
514
+ expect(screen.queryByText('ThumbsDownIcon')).not.toBeInTheDocument();
515
+ });
516
+
517
+ describe('positive actions', () => {
518
+ it('should not swap positive icon when clicked and useFilledIconsOnClick is false', async () => {
519
+ const user = userEvent.setup();
520
+
521
+ render(
522
+ <ResponseActions
523
+ actions={{
524
+ positive: { onClick: jest.fn() }
525
+ }}
526
+ useFilledIconsOnClick={false}
527
+ />
528
+ );
529
+
530
+ await user.click(screen.getByRole('button', { name: 'Good response' }));
531
+
532
+ expect(screen.getByText('OutlinedThumbsUpIcon')).toBeInTheDocument();
533
+ expect(screen.queryByText('ThumbsUpIcon')).not.toBeInTheDocument();
534
+ });
535
+
536
+ it('should swap positive icon from outline to filled when clicked with useFilledIconsOnClick', async () => {
537
+ const user = userEvent.setup();
538
+
539
+ render(
540
+ <ResponseActions
541
+ actions={{
542
+ positive: { onClick: jest.fn() }
543
+ }}
544
+ useFilledIconsOnClick
545
+ />
546
+ );
547
+
548
+ await user.click(screen.getByRole('button', { name: 'Good response' }));
549
+
550
+ expect(screen.getByText('ThumbsUpIcon')).toBeInTheDocument();
551
+ expect(screen.queryByText('OutlinedThumbsUpIcon')).not.toBeInTheDocument();
552
+ });
553
+
554
+ it('should revert positive icon to outline icon when clicking outside', async () => {
555
+ const user = userEvent.setup();
556
+
557
+ render(
558
+ <div>
559
+ <ResponseActions
560
+ actions={{
561
+ positive: { onClick: jest.fn() }
562
+ }}
563
+ useFilledIconsOnClick
564
+ />
565
+ <div data-testid="outside">Outside</div>
566
+ </div>
567
+ );
568
+
569
+ await user.click(screen.getByRole('button', { name: 'Good response' }));
570
+ expect(screen.getByText('ThumbsUpIcon')).toBeInTheDocument();
571
+
572
+ await user.click(screen.getByTestId('outside'));
573
+ expect(screen.getByText('OutlinedThumbsUpIcon')).toBeInTheDocument();
574
+ });
575
+
576
+ it('should not revert positive icon to outline icon when clicking outside if persistActionSelection is true', async () => {
577
+ const user = userEvent.setup();
578
+
579
+ render(
580
+ <div>
581
+ <ResponseActions
582
+ actions={{
583
+ positive: { onClick: jest.fn() }
584
+ }}
585
+ persistActionSelection
586
+ useFilledIconsOnClick
587
+ />
588
+ <div data-testid="outside">Outside</div>
589
+ </div>
590
+ );
591
+
592
+ await user.click(screen.getByRole('button', { name: 'Good response' }));
593
+ expect(screen.getByText('ThumbsUpIcon')).toBeInTheDocument();
594
+
595
+ await user.click(screen.getByTestId('outside'));
596
+ expect(screen.getByText('ThumbsUpIcon')).toBeInTheDocument();
597
+ });
598
+
599
+ describe('negative actions', () => {
600
+ it('should not swap negative icon when clicked and useFilledIconsOnClick is false', async () => {
601
+ const user = userEvent.setup();
602
+
603
+ render(
604
+ <ResponseActions
605
+ actions={{
606
+ negative: { onClick: jest.fn() }
607
+ }}
608
+ useFilledIconsOnClick={false}
609
+ />
610
+ );
611
+
612
+ await user.click(screen.getByRole('button', { name: 'Bad response' }));
613
+
614
+ expect(screen.getByText('OutlinedThumbsDownIcon')).toBeInTheDocument();
615
+ expect(screen.queryByText('ThumbsDownIcon')).not.toBeInTheDocument();
616
+ });
617
+
618
+ it('should swap negative icon from outline to filled when clicked with useFilledIconsOnClick', async () => {
619
+ const user = userEvent.setup();
620
+
621
+ render(
622
+ <ResponseActions
623
+ actions={{
624
+ negative: { onClick: jest.fn() }
625
+ }}
626
+ useFilledIconsOnClick
627
+ />
628
+ );
629
+
630
+ await user.click(screen.getByRole('button', { name: 'Bad response' }));
631
+
632
+ expect(screen.getByText('ThumbsDownIcon')).toBeInTheDocument();
633
+ expect(screen.queryByText('OutlinedThumbsDownIcon')).not.toBeInTheDocument();
634
+ });
635
+
636
+ it('should revert negative icon to outline when clicking outside', async () => {
637
+ const user = userEvent.setup();
638
+
639
+ render(
640
+ <div>
641
+ <ResponseActions
642
+ actions={{
643
+ negative: { onClick: jest.fn() }
644
+ }}
645
+ useFilledIconsOnClick
646
+ />
647
+ <div data-testid="outside">Outside</div>
648
+ </div>
649
+ );
650
+
651
+ await user.click(screen.getByRole('button', { name: 'Bad response' }));
652
+ expect(screen.getByText('ThumbsDownIcon')).toBeInTheDocument();
653
+
654
+ await user.click(screen.getByTestId('outside'));
655
+ expect(screen.getByText('OutlinedThumbsDownIcon')).toBeInTheDocument();
656
+ });
657
+
658
+ it('should not revert negative icon to outline icon when clicking outside if persistActionSelection is true', async () => {
659
+ const user = userEvent.setup();
660
+
661
+ render(
662
+ <div>
663
+ <ResponseActions
664
+ actions={{
665
+ negative: { onClick: jest.fn() }
666
+ }}
667
+ persistActionSelection
668
+ useFilledIconsOnClick
669
+ />
670
+ <div data-testid="outside">Outside</div>
671
+ </div>
672
+ );
673
+
674
+ await user.click(screen.getByRole('button', { name: 'Bad response' }));
675
+ expect(screen.getByText('ThumbsDownIcon')).toBeInTheDocument();
676
+
677
+ await user.click(screen.getByTestId('outside'));
678
+ expect(screen.getByText('ThumbsDownIcon')).toBeInTheDocument();
679
+ });
680
+ });
681
+ });
682
+ });
424
683
  });