@patternfly/chatbot 6.5.0-prerelease.2 → 6.5.0-prerelease.20

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 (152) hide show
  1. package/dist/cjs/AttachMenu/AttachMenu.d.ts +8 -2
  2. package/dist/cjs/AttachMenu/AttachMenu.js +2 -2
  3. package/dist/cjs/ChatbotFooter/ChatbotFooter.d.ts +5 -2
  4. package/dist/cjs/ChatbotFooter/ChatbotFooter.js +2 -2
  5. package/dist/cjs/ChatbotFooter/ChatbotFooter.test.js +5 -1
  6. package/dist/cjs/ChatbotHeader/ChatbotHeaderMenu.js +29 -2
  7. package/dist/cjs/CodeModal/CodeModal.d.ts +2 -0
  8. package/dist/cjs/CodeModal/CodeModal.js +53 -8
  9. package/dist/cjs/FileDetailsLabel/FileDetailsLabel.d.ts +2 -1
  10. package/dist/cjs/Message/CodeBlockMessage/CodeBlockMessage.d.ts +3 -1
  11. package/dist/cjs/Message/CodeBlockMessage/CodeBlockMessage.js +2 -2
  12. package/dist/cjs/Message/Message.d.ts +7 -1
  13. package/dist/cjs/Message/Message.js +11 -8
  14. package/dist/cjs/Message/Message.test.js +36 -0
  15. package/dist/cjs/Message/MessageLoading.d.ts +2 -1
  16. package/dist/cjs/Message/MessageLoading.js +1 -1
  17. package/dist/cjs/Message/TableMessage/TableMessage.d.ts +4 -1
  18. package/dist/cjs/Message/TableMessage/TableMessage.js +2 -2
  19. package/dist/cjs/Message/TextMessage/TextMessage.d.ts +4 -1
  20. package/dist/cjs/Message/TextMessage/TextMessage.js +2 -2
  21. package/dist/cjs/MessageBar/AttachButton.d.ts +2 -0
  22. package/dist/cjs/MessageBar/AttachButton.js +2 -2
  23. package/dist/cjs/MessageBar/AttachButton.test.js +4 -0
  24. package/dist/cjs/MessageBar/MessageBar.d.ts +16 -6
  25. package/dist/cjs/MessageBar/MessageBar.js +6 -5
  26. package/dist/cjs/MessageBar/MessageBar.test.js +62 -0
  27. package/dist/cjs/Onboarding/Onboarding.d.ts +36 -0
  28. package/dist/cjs/Onboarding/Onboarding.js +37 -0
  29. package/dist/cjs/Onboarding/Onboarding.test.d.ts +1 -0
  30. package/dist/cjs/Onboarding/Onboarding.test.js +80 -0
  31. package/dist/cjs/Onboarding/index.d.ts +2 -0
  32. package/dist/cjs/Onboarding/index.js +23 -0
  33. package/dist/cjs/ResponseActions/ResponseActions.d.ts +3 -0
  34. package/dist/cjs/ResponseActions/ResponseActions.js +28 -7
  35. package/dist/cjs/ResponseActions/ResponseActions.test.js +67 -12
  36. package/dist/cjs/__mocks__/monaco-editor.d.ts +11 -0
  37. package/dist/cjs/__mocks__/monaco-editor.js +18 -0
  38. package/dist/cjs/__mocks__/rehype-highlight.d.ts +2 -0
  39. package/dist/cjs/__mocks__/rehype-highlight.js +4 -0
  40. package/dist/cjs/index.d.ts +2 -0
  41. package/dist/cjs/index.js +4 -1
  42. package/dist/css/main.css +219 -21
  43. package/dist/css/main.css.map +1 -1
  44. package/dist/dynamic/Onboarding/package.json +1 -0
  45. package/dist/esm/AttachMenu/AttachMenu.d.ts +8 -2
  46. package/dist/esm/AttachMenu/AttachMenu.js +2 -2
  47. package/dist/esm/ChatbotFooter/ChatbotFooter.d.ts +5 -2
  48. package/dist/esm/ChatbotFooter/ChatbotFooter.js +2 -2
  49. package/dist/esm/ChatbotFooter/ChatbotFooter.test.js +5 -1
  50. package/dist/esm/ChatbotHeader/ChatbotHeaderMenu.js +30 -3
  51. package/dist/esm/CodeModal/CodeModal.d.ts +2 -0
  52. package/dist/esm/CodeModal/CodeModal.js +54 -9
  53. package/dist/esm/FileDetailsLabel/FileDetailsLabel.d.ts +2 -1
  54. package/dist/esm/Message/CodeBlockMessage/CodeBlockMessage.d.ts +3 -1
  55. package/dist/esm/Message/CodeBlockMessage/CodeBlockMessage.js +2 -2
  56. package/dist/esm/Message/Message.d.ts +7 -1
  57. package/dist/esm/Message/Message.js +11 -8
  58. package/dist/esm/Message/Message.test.js +36 -0
  59. package/dist/esm/Message/MessageLoading.d.ts +2 -1
  60. package/dist/esm/Message/MessageLoading.js +1 -1
  61. package/dist/esm/Message/TableMessage/TableMessage.d.ts +4 -1
  62. package/dist/esm/Message/TableMessage/TableMessage.js +2 -2
  63. package/dist/esm/Message/TextMessage/TextMessage.d.ts +4 -1
  64. package/dist/esm/Message/TextMessage/TextMessage.js +2 -2
  65. package/dist/esm/MessageBar/AttachButton.d.ts +2 -0
  66. package/dist/esm/MessageBar/AttachButton.js +2 -2
  67. package/dist/esm/MessageBar/AttachButton.test.js +4 -0
  68. package/dist/esm/MessageBar/MessageBar.d.ts +16 -6
  69. package/dist/esm/MessageBar/MessageBar.js +6 -5
  70. package/dist/esm/MessageBar/MessageBar.test.js +62 -0
  71. package/dist/esm/Onboarding/Onboarding.d.ts +36 -0
  72. package/dist/esm/Onboarding/Onboarding.js +30 -0
  73. package/dist/esm/Onboarding/Onboarding.test.d.ts +1 -0
  74. package/dist/esm/Onboarding/Onboarding.test.js +75 -0
  75. package/dist/esm/Onboarding/index.d.ts +2 -0
  76. package/dist/esm/Onboarding/index.js +2 -0
  77. package/dist/esm/ResponseActions/ResponseActions.d.ts +3 -0
  78. package/dist/esm/ResponseActions/ResponseActions.js +28 -7
  79. package/dist/esm/ResponseActions/ResponseActions.test.js +67 -12
  80. package/dist/esm/__mocks__/monaco-editor.d.ts +11 -0
  81. package/dist/esm/__mocks__/monaco-editor.js +18 -0
  82. package/dist/esm/__mocks__/rehype-highlight.d.ts +2 -0
  83. package/dist/esm/__mocks__/rehype-highlight.js +2 -0
  84. package/dist/esm/index.d.ts +2 -0
  85. package/dist/esm/index.js +2 -0
  86. package/dist/tsconfig.tsbuildinfo +1 -1
  87. package/package.json +14 -2
  88. package/patternfly-docs/content/extensions/chatbot/chatbot.md +57 -0
  89. package/patternfly-docs/content/extensions/chatbot/design-guidelines.md +12 -12
  90. package/patternfly-docs/content/extensions/chatbot/examples/Analytics/Analytics.md +1 -1
  91. package/patternfly-docs/content/extensions/chatbot/examples/Customizing Messages/Customizing Messages.md +1 -1
  92. package/patternfly-docs/content/extensions/chatbot/examples/Messages/BotMessage.tsx +1 -0
  93. package/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithPersistedActions.tsx +22 -0
  94. package/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithQuickResponses.tsx +11 -0
  95. package/patternfly-docs/content/extensions/chatbot/examples/Messages/Messages.md +18 -4
  96. package/patternfly-docs/content/extensions/chatbot/examples/Messages/UserMessage.tsx +1 -0
  97. package/patternfly-docs/content/extensions/chatbot/examples/Messages/UserMessageWithExtraContent.tsx +3 -1
  98. package/patternfly-docs/content/extensions/chatbot/examples/UI/ChatbotMessageBarIndicatorThinking.tsx +15 -0
  99. package/patternfly-docs/content/extensions/chatbot/examples/UI/CompactOnboarding.tsx +141 -0
  100. package/patternfly-docs/content/extensions/chatbot/examples/UI/Onboarding.tsx +151 -0
  101. package/patternfly-docs/content/extensions/chatbot/examples/UI/RH-Hat-Image.svg +9 -0
  102. package/patternfly-docs/content/extensions/chatbot/examples/UI/Settings.tsx +1 -1
  103. package/patternfly-docs/content/extensions/chatbot/examples/UI/UI.md +55 -27
  104. package/patternfly-docs/content/extensions/chatbot/examples/demos/AttachmentDemos.md +18 -18
  105. package/patternfly-docs/content/extensions/chatbot/examples/demos/Chatbot.md +29 -21
  106. package/patternfly-docs/content/extensions/chatbot/examples/demos/WhiteEmbeddedChatbot.tsx +451 -0
  107. package/patternfly-docs/patternfly-docs.config.js +2 -1
  108. package/patternfly-docs/patternfly-docs.source.js +1 -1
  109. package/src/AttachMenu/AttachMenu.tsx +26 -11
  110. package/src/Chatbot/Chatbot.scss +23 -1
  111. package/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.scss +25 -0
  112. package/src/ChatbotFooter/ChatbotFooter.scss +21 -0
  113. package/src/ChatbotFooter/ChatbotFooter.test.tsx +10 -1
  114. package/src/ChatbotFooter/ChatbotFooter.tsx +10 -3
  115. package/src/ChatbotHeader/ChatbotHeader.scss +19 -0
  116. package/src/ChatbotHeader/ChatbotHeaderMenu.tsx +56 -14
  117. package/src/ChatbotModal/ChatbotModal.scss +3 -0
  118. package/src/CodeModal/CodeModal.tsx +72 -23
  119. package/src/DeepThinking/DeepThinking.scss +1 -1
  120. package/src/FileDetailsLabel/FileDetailsLabel.tsx +2 -2
  121. package/src/Message/CodeBlockMessage/CodeBlockMessage.scss +12 -0
  122. package/src/Message/CodeBlockMessage/CodeBlockMessage.tsx +4 -1
  123. package/src/Message/Message.scss +11 -7
  124. package/src/Message/Message.test.tsx +41 -0
  125. package/src/Message/Message.tsx +28 -13
  126. package/src/Message/MessageLoading.scss +7 -0
  127. package/src/Message/MessageLoading.tsx +2 -2
  128. package/src/Message/TableMessage/TableMessage.scss +6 -1
  129. package/src/Message/TableMessage/TableMessage.tsx +6 -2
  130. package/src/Message/TextMessage/TextMessage.scss +10 -0
  131. package/src/Message/TextMessage/TextMessage.tsx +11 -2
  132. package/src/Message/UserFeedback/UserFeedback.scss +2 -1
  133. package/src/MessageBar/AttachButton.test.tsx +4 -0
  134. package/src/MessageBar/AttachButton.tsx +4 -1
  135. package/src/MessageBar/MessageBar.scss +40 -3
  136. package/src/MessageBar/MessageBar.test.tsx +102 -1
  137. package/src/MessageBar/MessageBar.tsx +44 -11
  138. package/src/Onboarding/Onboarding.scss +101 -0
  139. package/src/Onboarding/Onboarding.test.tsx +148 -0
  140. package/src/Onboarding/Onboarding.tsx +126 -0
  141. package/src/Onboarding/index.ts +3 -0
  142. package/src/ResponseActions/ResponseActions.scss +1 -1
  143. package/src/ResponseActions/ResponseActions.test.tsx +111 -12
  144. package/src/ResponseActions/ResponseActions.tsx +38 -10
  145. package/src/ToolCall/ToolCall.scss +1 -1
  146. package/src/ToolResponse/ToolResponse.scss +3 -3
  147. package/src/__mocks__/monaco-editor.ts +19 -0
  148. package/src/__mocks__/rehype-highlight.ts +3 -0
  149. package/src/index.ts +3 -0
  150. package/src/main.scss +1 -0
  151. package/tsconfig.json +1 -1
  152. package/patternfly-docs/content/extensions/chatbot/about-chatbot.md +0 -44
@@ -0,0 +1,148 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import '@testing-library/jest-dom';
3
+ import userEvent from '@testing-library/user-event';
4
+ import Onboarding from './Onboarding';
5
+
6
+ const handleModalToggle = jest.fn();
7
+ const onPrimaryAction = jest.fn();
8
+ const onSecondaryAction = jest.fn();
9
+
10
+ const body =
11
+ 'Experience personalized assistance and seamless problem-solving, simplifying your journey with Red Hat every step of the way.';
12
+
13
+ describe('Onboarding', () => {
14
+ afterEach(() => {
15
+ jest.clearAllMocks();
16
+ });
17
+ it('should render modal correctly', () => {
18
+ render(
19
+ <Onboarding
20
+ isModalOpen
21
+ onSecondaryAction={onSecondaryAction}
22
+ handleModalToggle={handleModalToggle}
23
+ ouiaId="Terms"
24
+ >
25
+ {body}
26
+ </Onboarding>
27
+ );
28
+ expect(screen.getByRole('heading', { name: /Onboarding/i })).toBeTruthy();
29
+ expect(screen.getByRole('button', { name: /Continue/i })).toBeTruthy();
30
+ expect(screen.getByRole('button', { name: /Skip/i })).toBeTruthy();
31
+ expect(screen.getByText(body)).toBeTruthy();
32
+ expect(screen.getByRole('dialog')).toHaveClass('pf-chatbot__onboarding-modal');
33
+ expect(screen.getByRole('dialog')).toHaveClass('pf-chatbot__onboarding-modal--default');
34
+ });
35
+ it('should handle image and altText props', () => {
36
+ render(
37
+ <Onboarding
38
+ isModalOpen
39
+ onSecondaryAction={onSecondaryAction}
40
+ handleModalToggle={handleModalToggle}
41
+ headerImage="./image.png"
42
+ headerImageAltText="Test image"
43
+ >
44
+ {body}
45
+ </Onboarding>
46
+ );
47
+ expect(screen.getByRole('img')).toBeTruthy();
48
+ expect(screen.getByRole('img')).toHaveAttribute('alt', 'Test image');
49
+ });
50
+ it('should handle className prop', () => {
51
+ render(
52
+ <Onboarding
53
+ isModalOpen
54
+ onSecondaryAction={onSecondaryAction}
55
+ handleModalToggle={handleModalToggle}
56
+ className="test"
57
+ >
58
+ {body}
59
+ </Onboarding>
60
+ );
61
+ expect(screen.getByRole('dialog')).toHaveClass('pf-chatbot__onboarding-modal');
62
+ expect(screen.getByRole('dialog')).toHaveClass('pf-chatbot__onboarding-modal--default');
63
+ expect(screen.getByRole('dialog')).toHaveClass('test');
64
+ });
65
+ it('should handle title prop', () => {
66
+ render(
67
+ <Onboarding
68
+ isModalOpen
69
+ onSecondaryAction={onSecondaryAction}
70
+ handleModalToggle={handleModalToggle}
71
+ title="Updated title"
72
+ >
73
+ {body}
74
+ </Onboarding>
75
+ );
76
+ expect(screen.getByRole('heading', { name: /Updated title/i })).toBeTruthy();
77
+ expect(screen.queryByRole('heading', { name: /Onboarding/i })).toBeFalsy();
78
+ });
79
+ it('should handle primary button prop', () => {
80
+ render(
81
+ <Onboarding
82
+ isModalOpen
83
+ onSecondaryAction={onSecondaryAction}
84
+ handleModalToggle={handleModalToggle}
85
+ primaryActionBtn="First"
86
+ >
87
+ {body}
88
+ </Onboarding>
89
+ );
90
+ expect(screen.getByRole('button', { name: /First/i })).toBeTruthy();
91
+ expect(screen.queryByRole('button', { name: /Continue/i })).toBeFalsy();
92
+ });
93
+ it('should handle secondary button prop', () => {
94
+ render(
95
+ <Onboarding
96
+ isModalOpen
97
+ onSecondaryAction={onSecondaryAction}
98
+ handleModalToggle={handleModalToggle}
99
+ secondaryActionBtn="Second"
100
+ >
101
+ {body}
102
+ </Onboarding>
103
+ );
104
+ expect(screen.getByRole('button', { name: /Second/i })).toBeTruthy();
105
+ expect(screen.queryByRole('button', { name: /Skip/i })).toBeFalsy();
106
+ });
107
+ it('should handle primary button click', async () => {
108
+ render(
109
+ <Onboarding
110
+ isModalOpen
111
+ onPrimaryAction={onPrimaryAction}
112
+ onSecondaryAction={onSecondaryAction}
113
+ handleModalToggle={handleModalToggle}
114
+ >
115
+ {body}
116
+ </Onboarding>
117
+ );
118
+ await userEvent.click(screen.getByRole('button', { name: /Continue/i }));
119
+ expect(onPrimaryAction).toHaveBeenCalledTimes(1);
120
+ expect(handleModalToggle).toHaveBeenCalledTimes(1);
121
+ });
122
+ it('should handle secondary button click', async () => {
123
+ render(
124
+ <Onboarding isModalOpen onSecondaryAction={onSecondaryAction} handleModalToggle={handleModalToggle}>
125
+ {body}
126
+ </Onboarding>
127
+ );
128
+ await userEvent.click(screen.getByRole('button', { name: /Skip/i }));
129
+ expect(onSecondaryAction).toHaveBeenCalledTimes(1);
130
+ expect(handleModalToggle).not.toHaveBeenCalled();
131
+ });
132
+ it('should handle isCompact prop', () => {
133
+ render(
134
+ <Onboarding
135
+ isModalOpen
136
+ onSecondaryAction={onSecondaryAction}
137
+ handleModalToggle={handleModalToggle}
138
+ isCompact={true}
139
+ headerImage="./image.png"
140
+ headerImageAltText="Test image"
141
+ >
142
+ {body}
143
+ </Onboarding>
144
+ );
145
+ expect(screen.getByRole('dialog')).toHaveClass('pf-m-compact');
146
+ expect(screen.queryByRole('img')).toBeFalsy();
147
+ });
148
+ });
@@ -0,0 +1,126 @@
1
+ // ============================================================================
2
+ // Terms of Use Modal - Chatbot Modal Extension
3
+ // ============================================================================
4
+ import type { FunctionComponent, MouseEvent as ReactMouseEvent, Ref } from 'react';
5
+ import { forwardRef } from 'react';
6
+ import { Button, Content, ModalBody, ModalFooter, ModalProps } from '@patternfly/react-core';
7
+ import { ChatbotDisplayMode } from '../Chatbot';
8
+ import ChatbotModal from '../ChatbotModal/ChatbotModal';
9
+
10
+ export interface OnboardingProps extends ModalProps {
11
+ /** Class applied to modal */
12
+ className?: string;
13
+ /** Action assigned to primary modal button */
14
+ onPrimaryAction?: (event: React.MouseEvent | MouseEvent | KeyboardEvent) => void;
15
+ /** Action assigned to secondary modal button */
16
+ onSecondaryAction: (event: React.MouseEvent | MouseEvent | KeyboardEvent) => void;
17
+ /** Name of primary modal button */
18
+ primaryActionBtn?: string;
19
+ /** Name of secondary modal button */
20
+ secondaryActionBtn?: string;
21
+ /** Function that handles modal toggle */
22
+ handleModalToggle: (event: React.MouseEvent | MouseEvent | KeyboardEvent) => void;
23
+ /** Whether modal is open */
24
+ isModalOpen: boolean;
25
+ /** Title of modal */
26
+ title?: string;
27
+ /** Display mode for the Chatbot parent; this influences the styles applied */
28
+ displayMode?: ChatbotDisplayMode;
29
+ /** Optional image displayed in header */
30
+ headerImage?: string;
31
+ /** Alt text for optional image displayed in header */
32
+ headerImageAltText?: string;
33
+ /** Ref applied to modal */
34
+ innerRef?: React.Ref<HTMLDivElement>;
35
+ /** OuiaID applied to modal */
36
+ ouiaId?: string;
37
+ /** Sets modal to compact styling. */
38
+ isCompact?: boolean;
39
+ }
40
+
41
+ export const OnboardingBase: FunctionComponent<OnboardingProps> = ({
42
+ handleModalToggle,
43
+ isModalOpen,
44
+ onPrimaryAction,
45
+ onSecondaryAction,
46
+ primaryActionBtn = 'Continue',
47
+ secondaryActionBtn = 'Skip',
48
+ title = 'Onboarding',
49
+ headerImage,
50
+ headerImageAltText = '',
51
+ displayMode = ChatbotDisplayMode.default,
52
+ className,
53
+ children,
54
+ innerRef,
55
+ ouiaId = 'Onboarding',
56
+ isCompact,
57
+ ...props
58
+ }: OnboardingProps) => {
59
+ const handlePrimaryAction = (_event: ReactMouseEvent | MouseEvent | KeyboardEvent) => {
60
+ handleModalToggle(_event);
61
+ onPrimaryAction && onPrimaryAction(_event);
62
+ };
63
+
64
+ const handleSecondaryAction = (_event: ReactMouseEvent | MouseEvent | KeyboardEvent) => {
65
+ onSecondaryAction(_event);
66
+ };
67
+
68
+ const modal = (
69
+ <ChatbotModal
70
+ isOpen={isModalOpen}
71
+ ouiaId={ouiaId}
72
+ aria-labelledby="onboarding-title"
73
+ aria-describedby="onboarding-modal"
74
+ className={`pf-chatbot__onboarding-modal pf-chatbot__onboarding-modal--${displayMode} ${isCompact ? 'pf-m-compact' : ''} ${className ? className : ''}`}
75
+ displayMode={displayMode}
76
+ onClose={handleModalToggle}
77
+ {...props}
78
+ >
79
+ {/* This is a workaround since the PatternFly modal doesn't have ref forwarding */}
80
+ <section className={`pf-chatbot__onboarding--section`} aria-label={title} tabIndex={-1} ref={innerRef}>
81
+ <>
82
+ <ModalBody className="pf-chatbot__onboarding--modal-body">
83
+ {!isCompact && headerImage && (
84
+ <div className="pf-chatbot__onboarding--header">
85
+ <img src={headerImage} className="pf-chatbot__onboarding--image" alt={headerImageAltText} />
86
+ </div>
87
+ )}
88
+ <div className="pf-chatbot__onboarding--modal-text">
89
+ <h1 className="pf-chatbot__onboarding--title">{title}</h1>
90
+ <Content>{children}</Content>
91
+ </div>
92
+ </ModalBody>
93
+ <ModalFooter className="pf-chatbot__onboarding--footer">
94
+ <Button
95
+ isBlock
96
+ key="onboarding-modal-primary"
97
+ variant="primary"
98
+ onClick={handlePrimaryAction}
99
+ form="onboarding-form"
100
+ size="lg"
101
+ >
102
+ {primaryActionBtn}
103
+ </Button>
104
+ <Button
105
+ isBlock
106
+ key="onboarding-modal-secondary"
107
+ variant="secondary"
108
+ onClick={handleSecondaryAction}
109
+ size="lg"
110
+ >
111
+ {secondaryActionBtn}
112
+ </Button>
113
+ </ModalFooter>
114
+ </>
115
+ </section>
116
+ </ChatbotModal>
117
+ );
118
+
119
+ return modal;
120
+ };
121
+
122
+ const Onboarding = forwardRef((props: OnboardingProps, ref: Ref<HTMLDivElement>) => (
123
+ <OnboardingBase innerRef={ref} {...props} />
124
+ ));
125
+
126
+ export default Onboarding;
@@ -0,0 +1,3 @@
1
+ export { default } from './Onboarding';
2
+
3
+ export * from './Onboarding';
@@ -16,8 +16,8 @@
16
16
  --pf-v6-c-button__icon--Color: var(--pf-t--global--icon--color--subtle);
17
17
  }
18
18
  &:focus {
19
- --pf-v6-c-button--hover--BackgroundColor: var(--pf-t--global--background--color--action--plain--alt--clicked);
20
19
  --pf-v6-c-button__icon--Color: var(--pf-t--global--icon--color--regular);
20
+ --pf-v6-c-button--BackgroundColor: var(--pf-v6-c-button--hover--BackgroundColor);
21
21
  }
22
22
  }
23
23
  }
@@ -6,8 +6,8 @@ import { DownloadIcon, InfoCircleIcon, RedoIcon } from '@patternfly/react-icons'
6
6
  import Message from '../Message';
7
7
 
8
8
  const ALL_ACTIONS = [
9
- { type: 'positive', label: 'Good response', clickedLabel: 'Response recorded' },
10
- { type: 'negative', label: 'Bad response', clickedLabel: 'Response recorded' },
9
+ { type: 'positive', label: 'Good response', clickedLabel: 'Good response recorded' },
10
+ { type: 'negative', label: 'Bad response', clickedLabel: 'Bad response recorded' },
11
11
  { type: 'copy', label: 'Copy', clickedLabel: 'Copied' },
12
12
  { type: 'edit', label: 'Edit', clickedLabel: 'Editing' },
13
13
  { type: 'share', label: 'Share', clickedLabel: 'Shared' },
@@ -81,7 +81,7 @@ describe('ResponseActions', () => {
81
81
  expect(button).toBeTruthy();
82
82
  });
83
83
  await userEvent.click(goodBtn);
84
- expect(screen.getByRole('button', { name: 'Response recorded' })).toHaveClass(
84
+ expect(screen.getByRole('button', { name: 'Good response recorded' })).toHaveClass(
85
85
  'pf-chatbot__button--response-action-clicked'
86
86
  );
87
87
  let unclickedButtons = buttons.filter((button) => button !== goodBtn);
@@ -89,7 +89,7 @@ describe('ResponseActions', () => {
89
89
  expect(button).not.toHaveClass('pf-chatbot__button--response-action-clicked');
90
90
  });
91
91
  await userEvent.click(badBtn);
92
- expect(screen.getByRole('button', { name: 'Response recorded' })).toHaveClass(
92
+ expect(screen.getByRole('button', { name: 'Bad response recorded' })).toHaveClass(
93
93
  'pf-chatbot__button--response-action-clicked'
94
94
  );
95
95
  unclickedButtons = buttons.filter((button) => button !== badBtn);
@@ -117,13 +117,13 @@ describe('ResponseActions', () => {
117
117
  expect(badBtn).toBeTruthy();
118
118
 
119
119
  await userEvent.click(goodBtn);
120
- expect(screen.getByRole('button', { name: 'Response recorded' })).toHaveClass(
120
+ expect(screen.getByRole('button', { name: 'Good response recorded' })).toHaveClass(
121
121
  'pf-chatbot__button--response-action-clicked'
122
122
  );
123
123
  expect(badBtn).not.toHaveClass('pf-chatbot__button--response-action-clicked');
124
124
 
125
125
  await userEvent.click(badBtn);
126
- expect(screen.getByRole('button', { name: 'Response recorded' })).toHaveClass(
126
+ expect(screen.getByRole('button', { name: 'Bad response recorded' })).toHaveClass(
127
127
  'pf-chatbot__button--response-action-clicked'
128
128
  );
129
129
  expect(goodBtn).not.toHaveClass('pf-chatbot__button--response-action-clicked');
@@ -238,30 +238,30 @@ describe('ResponseActions', () => {
238
238
  });
239
239
 
240
240
  it('should be able to call onClick correctly', async () => {
241
- ALL_ACTIONS.forEach(async ({ type, label }) => {
241
+ for (const { type, label } of ALL_ACTIONS) {
242
242
  const spy = jest.fn();
243
243
  render(<ResponseActions actions={{ [type]: { onClick: spy } }} />);
244
244
  await userEvent.click(screen.getByRole('button', { name: label }));
245
245
  expect(spy).toHaveBeenCalledTimes(1);
246
- });
246
+ }
247
247
  });
248
248
 
249
249
  it('should swap clicked and non-clicked aria labels on click', async () => {
250
- ALL_ACTIONS.forEach(async ({ type, label, clickedLabel }) => {
250
+ for (const { type, label, clickedLabel } of ALL_ACTIONS) {
251
251
  render(<ResponseActions actions={{ [type]: { onClick: jest.fn() } }} />);
252
252
  expect(screen.getByRole('button', { name: label })).toBeTruthy();
253
253
  await userEvent.click(screen.getByRole('button', { name: label }));
254
254
  expect(screen.getByRole('button', { name: clickedLabel })).toBeTruthy();
255
- });
255
+ }
256
256
  });
257
257
 
258
258
  it('should swap clicked and non-clicked tooltips on click', async () => {
259
- ALL_ACTIONS.forEach(async ({ type, label, clickedLabel }) => {
259
+ for (const { type, label, clickedLabel } of ALL_ACTIONS) {
260
260
  render(<ResponseActions actions={{ [type]: { onClick: jest.fn() } }} />);
261
261
  expect(screen.getByRole('button', { name: label })).toBeTruthy();
262
262
  await userEvent.click(screen.getByRole('button', { name: label }));
263
263
  expect(screen.getByRole('tooltip', { name: clickedLabel })).toBeTruthy();
264
- });
264
+ }
265
265
  });
266
266
 
267
267
  it('should be able to change aria labels', () => {
@@ -322,4 +322,103 @@ describe('ResponseActions', () => {
322
322
  expect(screen.getByTestId(action[key])).toBeTruthy();
323
323
  });
324
324
  });
325
+
326
+ // we are testing for the reverse case already above
327
+ it('should not deselect when clicking outside when persistActionSelection is true', async () => {
328
+ render(
329
+ <Message
330
+ name="Bot"
331
+ role="bot"
332
+ avatar=""
333
+ content="Test content"
334
+ actions={{
335
+ positive: {},
336
+ negative: {}
337
+ }}
338
+ persistActionSelection
339
+ />
340
+ );
341
+ const goodBtn = screen.getByRole('button', { name: 'Good response' });
342
+
343
+ await userEvent.click(goodBtn);
344
+ expect(screen.getByRole('button', { name: 'Good response recorded' })).toHaveClass(
345
+ 'pf-chatbot__button--response-action-clicked'
346
+ );
347
+
348
+ await userEvent.click(screen.getByText('Test content'));
349
+
350
+ expect(screen.getByRole('button', { name: 'Good response recorded' })).toHaveClass(
351
+ 'pf-chatbot__button--response-action-clicked'
352
+ );
353
+ });
354
+
355
+ it('should switch selection to another button when persistActionSelection is true', async () => {
356
+ render(
357
+ <Message
358
+ name="Bot"
359
+ role="bot"
360
+ avatar=""
361
+ content="Test content"
362
+ actions={{
363
+ positive: {},
364
+ negative: {}
365
+ }}
366
+ persistActionSelection
367
+ />
368
+ );
369
+ const goodBtn = screen.getByRole('button', { name: 'Good response' });
370
+ const badBtn = screen.getByRole('button', { name: 'Bad response' });
371
+
372
+ await userEvent.click(goodBtn);
373
+ expect(goodBtn).toHaveClass('pf-chatbot__button--response-action-clicked');
374
+
375
+ await userEvent.click(badBtn);
376
+ expect(badBtn).toHaveClass('pf-chatbot__button--response-action-clicked');
377
+ expect(goodBtn).not.toHaveClass('pf-chatbot__button--response-action-clicked');
378
+ });
379
+
380
+ it('should toggle off when clicking the same button when persistActionSelection is true', async () => {
381
+ render(
382
+ <Message
383
+ name="Bot"
384
+ role="bot"
385
+ avatar=""
386
+ content="Test content"
387
+ actions={{
388
+ positive: {},
389
+ negative: {}
390
+ }}
391
+ persistActionSelection
392
+ />
393
+ );
394
+ const goodBtn = screen.getByRole('button', { name: 'Good response' });
395
+
396
+ await userEvent.click(goodBtn);
397
+ expect(goodBtn).toHaveClass('pf-chatbot__button--response-action-clicked');
398
+
399
+ await userEvent.click(goodBtn);
400
+ expect(goodBtn).not.toHaveClass('pf-chatbot__button--response-action-clicked');
401
+ });
402
+
403
+ it('should work with custom actions when persistActionSelection is true', async () => {
404
+ const actions = {
405
+ positive: { 'data-testid': 'positive', onClick: jest.fn() },
406
+ negative: { 'data-testid': 'negative', onClick: jest.fn() },
407
+ custom: {
408
+ 'data-testid': 'custom',
409
+ onClick: jest.fn(),
410
+ ariaLabel: 'Custom',
411
+ tooltipContent: 'Custom action',
412
+ icon: <DownloadIcon />
413
+ }
414
+ };
415
+ render(<ResponseActions actions={actions} persistActionSelection />);
416
+
417
+ const customBtn = screen.getByTestId('custom');
418
+ await userEvent.click(customBtn);
419
+ expect(customBtn).toHaveClass('pf-chatbot__button--response-action-clicked');
420
+
421
+ await userEvent.click(customBtn);
422
+ expect(customBtn).not.toHaveClass('pf-chatbot__button--response-action-clicked');
423
+ });
325
424
  });
@@ -53,11 +53,20 @@ export interface ResponseActionProps {
53
53
  listen?: ActionProps;
54
54
  edit?: ActionProps;
55
55
  };
56
+ /** When true, the selected action will persist even when clicking outside the component.
57
+ * When false (default), clicking outside or clicking another action will deselect the current selection. */
58
+ persistActionSelection?: boolean;
56
59
  }
57
60
 
58
- export const ResponseActions: FunctionComponent<ResponseActionProps> = ({ actions }) => {
61
+ export const ResponseActions: FunctionComponent<ResponseActionProps> = ({
62
+ actions,
63
+ persistActionSelection = false
64
+ }) => {
59
65
  const [activeButton, setActiveButton] = useState<string>();
60
66
  const [clickStatePersisted, setClickStatePersisted] = useState<boolean>(false);
67
+
68
+ const { positive, negative, copy, edit, share, download, listen, ...additionalActions } = actions;
69
+
61
70
  useEffect(() => {
62
71
  // Define the order of precedence for checking initial `isClicked`
63
72
  const actionPrecedence = ['positive', 'negative', 'copy', 'edit', 'share', 'download', 'listen'];
@@ -82,13 +91,21 @@ export const ResponseActions: FunctionComponent<ResponseActionProps> = ({ action
82
91
  // Click state is explicitly controlled by consumer.
83
92
  setClickStatePersisted(true);
84
93
  }
94
+ // If persistActionSelection is true, all selections are persisted
95
+ if (persistActionSelection) {
96
+ setClickStatePersisted(true);
97
+ }
85
98
  setActiveButton(initialActive);
86
- }, [actions]);
99
+ }, [actions, persistActionSelection]);
87
100
 
88
- const { positive, negative, copy, edit, share, download, listen, ...additionalActions } = actions;
89
101
  const responseActions = useRef<HTMLDivElement>(null);
90
102
 
91
103
  useEffect(() => {
104
+ // Only add click outside listener if not persisting selection
105
+ if (persistActionSelection) {
106
+ return;
107
+ }
108
+
92
109
  const handleClickOutside = (e) => {
93
110
  if (responseActions.current && !responseActions.current.contains(e.target) && !clickStatePersisted) {
94
111
  setActiveButton(undefined);
@@ -99,15 +116,26 @@ export const ResponseActions: FunctionComponent<ResponseActionProps> = ({ action
99
116
  return () => {
100
117
  window.removeEventListener('click', handleClickOutside);
101
118
  };
102
- }, [clickStatePersisted]);
119
+ }, [clickStatePersisted, persistActionSelection]);
103
120
 
104
121
  const handleClick = (
105
122
  e: MouseEvent | MouseEvent<Element, MouseEvent> | KeyboardEvent,
106
123
  id: string,
107
124
  onClick?: (event: MouseEvent | MouseEvent<Element, MouseEvent> | KeyboardEvent) => void
108
125
  ) => {
109
- setClickStatePersisted(false);
110
- setActiveButton(id);
126
+ if (persistActionSelection) {
127
+ if (activeButton === id) {
128
+ // Toggle off if clicking the same button
129
+ setActiveButton(undefined);
130
+ } else {
131
+ // Set new active button
132
+ setActiveButton(id);
133
+ }
134
+ setClickStatePersisted(true);
135
+ } else {
136
+ setClickStatePersisted(false);
137
+ setActiveButton(id);
138
+ }
111
139
  onClick && onClick(e);
112
140
  };
113
141
 
@@ -117,12 +145,12 @@ export const ResponseActions: FunctionComponent<ResponseActionProps> = ({ action
117
145
  <ResponseActionButton
118
146
  {...positive}
119
147
  ariaLabel={positive.ariaLabel ?? 'Good response'}
120
- clickedAriaLabel={positive.ariaLabel ?? 'Response recorded'}
148
+ clickedAriaLabel={positive.ariaLabel ?? 'Good response recorded'}
121
149
  onClick={(e) => handleClick(e, 'positive', positive.onClick)}
122
150
  className={positive.className}
123
151
  isDisabled={positive.isDisabled}
124
152
  tooltipContent={positive.tooltipContent ?? 'Good response'}
125
- clickedTooltipContent={positive.clickedTooltipContent ?? 'Response recorded'}
153
+ clickedTooltipContent={positive.clickedTooltipContent ?? 'Good response recorded'}
126
154
  tooltipProps={positive.tooltipProps}
127
155
  icon={<OutlinedThumbsUpIcon />}
128
156
  isClicked={activeButton === 'positive'}
@@ -135,12 +163,12 @@ export const ResponseActions: FunctionComponent<ResponseActionProps> = ({ action
135
163
  <ResponseActionButton
136
164
  {...negative}
137
165
  ariaLabel={negative.ariaLabel ?? 'Bad response'}
138
- clickedAriaLabel={negative.ariaLabel ?? 'Response recorded'}
166
+ clickedAriaLabel={negative.ariaLabel ?? 'Bad response recorded'}
139
167
  onClick={(e) => handleClick(e, 'negative', negative.onClick)}
140
168
  className={negative.className}
141
169
  isDisabled={negative.isDisabled}
142
170
  tooltipContent={negative.tooltipContent ?? 'Bad response'}
143
- clickedTooltipContent={negative.clickedTooltipContent ?? 'Response recorded'}
171
+ clickedTooltipContent={negative.clickedTooltipContent ?? 'Bad response recorded'}
144
172
  tooltipProps={negative.tooltipProps}
145
173
  icon={<OutlinedThumbsDownIcon />}
146
174
  isClicked={activeButton === 'negative'}
@@ -1,6 +1,6 @@
1
1
  .pf-chatbot__tool-call {
2
- --pf-v6-c-card--BorderColor: var(--pf-t--global--border--color--control--read-only);
3
2
  --pf-v6-c-card--BorderRadius: var(--pf-t--global--border--radius--small);
3
+ --pf-v6-c-card--BorderColor: var(--pf-t--global--border--color--default);
4
4
 
5
5
  overflow: unset;
6
6
  row-gap: var(--pf-t--global--spacer--sm);
@@ -1,5 +1,5 @@
1
1
  .pf-chatbot__tool-response {
2
- --pf-v6-c-card--BorderColor: var(--pf-t--global--border--color--control--read-only);
2
+ --pf-v6-c-card--BorderColor: var(--pf-t--global--border--color--default);
3
3
  overflow: unset;
4
4
  }
5
5
 
@@ -25,12 +25,12 @@
25
25
  }
26
26
 
27
27
  .pf-chatbot__tool-response-card {
28
- --pf-v6-c-card--BorderColor: var(--pf-t--global--border--color--control--read-only);
28
+ --pf-v6-c-card--BorderColor: var(--pf-t--global--border--color--default);
29
29
  --pf-v6-c-card--first-child--PaddingBlockStart: var(--pf-t--global--spacer--sm);
30
30
  --pf-v6-c-card__title--not--last-child--PaddingBlockEnd: var(--pf-t--global--spacer--sm);
31
31
  --pf-v6-c-card--c-divider--child--PaddingBlockStart: var(--pf-t--global--spacer--sm);
32
32
 
33
33
  .pf-v6-c-divider {
34
- --pf-v6-c-divider--Color: var(--pf-t--global--border--color--control--read-only);
34
+ --pf-v6-c-divider--Color: var(--pf-t--global--border--color--default);
35
35
  }
36
36
  }
@@ -0,0 +1,19 @@
1
+ const mockEditor = {
2
+ layout: jest.fn(),
3
+ focus: jest.fn(),
4
+ dispose: jest.fn(),
5
+ getModel: jest.fn(),
6
+ updateOptions: jest.fn()
7
+ };
8
+
9
+ const mockModel = {
10
+ updateOptions: jest.fn(),
11
+ dispose: jest.fn()
12
+ };
13
+
14
+ module.exports = {
15
+ editor: {
16
+ create: jest.fn(() => mockEditor),
17
+ getModels: jest.fn(() => [mockModel])
18
+ }
19
+ };
@@ -0,0 +1,3 @@
1
+ const rehypeHighlight = () => (tree) => tree;
2
+
3
+ export default rehypeHighlight;
package/src/index.ts CHANGED
@@ -75,6 +75,9 @@ export * from './MessageBox';
75
75
  export { default as MessageDivider } from './MessageDivider';
76
76
  export * from './MessageDivider';
77
77
 
78
+ export { default as Onboarding } from './Onboarding';
79
+ export * from './Onboarding';
80
+
78
81
  export { default as PreviewAttachment } from './PreviewAttachment';
79
82
  export * from './PreviewAttachment';
80
83
 
package/src/main.scss CHANGED
@@ -31,6 +31,7 @@
31
31
  @import './MessageBox/MessageBox';
32
32
  @import './MessageDivider/MessageDivider';
33
33
  @import './MessageBox/JumpButton';
34
+ @import './Onboarding/Onboarding';
34
35
  @import './ResponseActions/ResponseActions';
35
36
  @import './Settings/Settings';
36
37
  @import './SourcesCard/SourcesCard.scss';
package/tsconfig.json CHANGED
@@ -5,7 +5,7 @@
5
5
  /* Basic Options */
6
6
  // "incremental": true, /* Enable incremental compilation */
7
7
  "target": "es2015" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', or 'ESNEXT'. */,
8
- "module": "es2015" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */,
8
+ "module": "es2020" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */,
9
9
  // "lib": [], /* Specify library files to be included in the compilation. */
10
10
  // "allowJs": true, /* Allow javascript files to be compiled. */
11
11
  // "checkJs": true, /* Report errors in .js files. */