@patternfly/chatbot 6.5.0-prerelease.21 → 6.5.0-prerelease.23
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/DeepThinking/DeepThinking.d.ts +13 -0
- package/dist/cjs/DeepThinking/DeepThinking.js +31 -3
- package/dist/cjs/DeepThinking/DeepThinking.test.js +80 -0
- package/dist/cjs/MarkdownContent/MarkdownContent.d.ts +39 -0
- package/dist/cjs/MarkdownContent/MarkdownContent.js +181 -0
- package/dist/cjs/MarkdownContent/MarkdownContent.test.d.ts +1 -0
- package/dist/cjs/MarkdownContent/MarkdownContent.test.js +192 -0
- package/dist/cjs/MarkdownContent/index.d.ts +2 -0
- package/dist/cjs/MarkdownContent/index.js +23 -0
- package/dist/cjs/Message/CodeBlockMessage/CodeBlockMessage.d.ts +3 -1
- package/dist/cjs/Message/CodeBlockMessage/CodeBlockMessage.js +3 -2
- package/dist/cjs/Message/LinkMessage/LinkMessage.d.ts +5 -1
- package/dist/cjs/Message/LinkMessage/LinkMessage.js +4 -3
- package/dist/cjs/Message/ListMessage/OrderedListMessage.d.ts +9 -1
- package/dist/cjs/Message/ListMessage/OrderedListMessage.js +2 -1
- package/dist/cjs/Message/ListMessage/UnorderedListMessage.d.ts +7 -1
- package/dist/cjs/Message/ListMessage/UnorderedListMessage.js +2 -1
- package/dist/cjs/Message/Message.js +2 -155
- package/dist/cjs/Message/TableMessage/TableMessage.d.ts +6 -1
- package/dist/cjs/Message/TableMessage/TableMessage.js +3 -2
- package/dist/cjs/Message/TextMessage/TextMessage.d.ts +8 -1
- package/dist/cjs/Message/TextMessage/TextMessage.js +3 -2
- package/dist/cjs/ToolCall/ToolCall.d.ts +11 -0
- package/dist/cjs/ToolCall/ToolCall.js +24 -3
- package/dist/cjs/ToolCall/ToolCall.test.js +57 -0
- package/dist/cjs/ToolResponse/ToolResponse.d.ts +17 -0
- package/dist/cjs/ToolResponse/ToolResponse.js +49 -3
- package/dist/cjs/ToolResponse/ToolResponse.test.js +100 -0
- package/dist/cjs/index.d.ts +2 -0
- package/dist/cjs/index.js +4 -1
- package/dist/css/main.css +48 -0
- package/dist/css/main.css.map +1 -1
- package/dist/dynamic/MarkdownContent/package.json +1 -0
- package/dist/esm/DeepThinking/DeepThinking.d.ts +13 -0
- package/dist/esm/DeepThinking/DeepThinking.js +28 -3
- package/dist/esm/DeepThinking/DeepThinking.test.js +80 -0
- package/dist/esm/MarkdownContent/MarkdownContent.d.ts +39 -0
- package/dist/esm/MarkdownContent/MarkdownContent.js +174 -0
- package/dist/esm/MarkdownContent/MarkdownContent.test.d.ts +1 -0
- package/dist/esm/MarkdownContent/MarkdownContent.test.js +187 -0
- package/dist/esm/MarkdownContent/index.d.ts +2 -0
- package/dist/esm/MarkdownContent/index.js +2 -0
- package/dist/esm/Message/CodeBlockMessage/CodeBlockMessage.d.ts +3 -1
- package/dist/esm/Message/CodeBlockMessage/CodeBlockMessage.js +3 -2
- package/dist/esm/Message/LinkMessage/LinkMessage.d.ts +5 -1
- package/dist/esm/Message/LinkMessage/LinkMessage.js +4 -3
- package/dist/esm/Message/ListMessage/OrderedListMessage.d.ts +9 -1
- package/dist/esm/Message/ListMessage/OrderedListMessage.js +2 -1
- package/dist/esm/Message/ListMessage/UnorderedListMessage.d.ts +7 -1
- package/dist/esm/Message/ListMessage/UnorderedListMessage.js +2 -1
- package/dist/esm/Message/Message.js +3 -156
- package/dist/esm/Message/TableMessage/TableMessage.d.ts +6 -1
- package/dist/esm/Message/TableMessage/TableMessage.js +3 -2
- package/dist/esm/Message/TextMessage/TextMessage.d.ts +8 -1
- package/dist/esm/Message/TextMessage/TextMessage.js +3 -2
- package/dist/esm/ToolCall/ToolCall.d.ts +11 -0
- package/dist/esm/ToolCall/ToolCall.js +21 -3
- package/dist/esm/ToolCall/ToolCall.test.js +57 -0
- package/dist/esm/ToolResponse/ToolResponse.d.ts +17 -0
- package/dist/esm/ToolResponse/ToolResponse.js +46 -3
- package/dist/esm/ToolResponse/ToolResponse.test.js +100 -0
- package/dist/esm/index.d.ts +2 -0
- package/dist/esm/index.js +2 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithDeepThinking.tsx +25 -11
- package/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithMarkdownDeepThinking.tsx +26 -0
- package/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithMarkdownToolCall.tsx +29 -0
- package/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithMarkdownToolResponse.tsx +200 -0
- package/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithToolCall.tsx +14 -1
- package/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithToolResponse.tsx +222 -105
- package/patternfly-docs/content/extensions/chatbot/examples/Messages/Messages.md +32 -0
- package/patternfly-docs/content/extensions/chatbot/examples/demos/Chatbot.md +15 -1
- package/src/DeepThinking/DeepThinking.test.tsx +109 -0
- package/src/DeepThinking/DeepThinking.tsx +54 -5
- package/src/MarkdownContent/MarkdownContent.test.tsx +207 -0
- package/src/MarkdownContent/MarkdownContent.tsx +264 -0
- package/src/MarkdownContent/index.ts +2 -0
- package/src/Message/CodeBlockMessage/CodeBlockMessage.scss +4 -0
- package/src/Message/CodeBlockMessage/CodeBlockMessage.tsx +5 -1
- package/src/Message/LinkMessage/LinkMessage.scss +5 -0
- package/src/Message/LinkMessage/LinkMessage.tsx +24 -2
- package/src/Message/ListMessage/ListMessage.scss +8 -0
- package/src/Message/ListMessage/OrderedListMessage.tsx +16 -2
- package/src/Message/ListMessage/UnorderedListMessage.tsx +12 -2
- package/src/Message/Message.tsx +21 -181
- package/src/Message/TableMessage/TableMessage.scss +11 -0
- package/src/Message/TableMessage/TableMessage.tsx +18 -2
- package/src/Message/TextMessage/TextMessage.scss +8 -0
- package/src/Message/TextMessage/TextMessage.tsx +29 -2
- package/src/ToolCall/ToolCall.test.tsx +91 -0
- package/src/ToolCall/ToolCall.tsx +49 -4
- package/src/ToolResponse/ToolResponse.scss +10 -0
- package/src/ToolResponse/ToolResponse.test.tsx +119 -0
- package/src/ToolResponse/ToolResponse.tsx +82 -7
- package/src/index.ts +3 -0
- package/src/main.scss +1 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import '@testing-library/jest-dom';
|
|
3
|
+
import MarkdownContent from './MarkdownContent';
|
|
4
|
+
import rehypeExternalLinks from '../__mocks__/rehype-external-links';
|
|
5
|
+
|
|
6
|
+
const BOLD_TEXT = '**Bold text**';
|
|
7
|
+
const ITALIC_TEXT = '*Italic text*';
|
|
8
|
+
const INLINE_CODE = 'Here is inline code: `const x = 5`';
|
|
9
|
+
const CODE_BLOCK = `\`\`\`javascript
|
|
10
|
+
function hello() {
|
|
11
|
+
console.log('Hello, world!');
|
|
12
|
+
}
|
|
13
|
+
\`\`\``;
|
|
14
|
+
const HEADING = '# Heading 1';
|
|
15
|
+
const LINK = '[PatternFly](https://www.patternfly.org/)';
|
|
16
|
+
const UNORDERED_LIST = `
|
|
17
|
+
* Item 1
|
|
18
|
+
* Item 2
|
|
19
|
+
* Item 3
|
|
20
|
+
`;
|
|
21
|
+
const ORDERED_LIST = `
|
|
22
|
+
1. First item
|
|
23
|
+
2. Second item
|
|
24
|
+
3. Third item
|
|
25
|
+
`;
|
|
26
|
+
const TABLE = `
|
|
27
|
+
| Column 1 | Column 2 |
|
|
28
|
+
|----------|----------|
|
|
29
|
+
| Cell 1 | Cell 2 |
|
|
30
|
+
| Cell 3 | Cell 4 |
|
|
31
|
+
`;
|
|
32
|
+
const BLOCKQUOTE = '> This is a blockquote';
|
|
33
|
+
const IMAGE = '';
|
|
34
|
+
|
|
35
|
+
describe('MarkdownContent', () => {
|
|
36
|
+
beforeEach(() => {
|
|
37
|
+
jest.clearAllMocks();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should render bold text correctly', () => {
|
|
41
|
+
const { container } = render(<MarkdownContent content={BOLD_TEXT} />);
|
|
42
|
+
expect(container.querySelector('strong')).toBeTruthy();
|
|
43
|
+
expect(screen.getByText('Bold text')).toBeTruthy();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('should render italic text correctly', () => {
|
|
47
|
+
const { container } = render(<MarkdownContent content={ITALIC_TEXT} />);
|
|
48
|
+
expect(container.querySelector('em')).toBeTruthy();
|
|
49
|
+
expect(screen.getByText('Italic text')).toBeTruthy();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should render inline code correctly', () => {
|
|
53
|
+
render(<MarkdownContent content={INLINE_CODE} />);
|
|
54
|
+
expect(screen.getByText(/const x = 5/)).toBeTruthy();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should render code blocks correctly', () => {
|
|
58
|
+
render(<MarkdownContent content={CODE_BLOCK} />);
|
|
59
|
+
|
|
60
|
+
expect(screen.getByText(/function hello/)).toBeVisible();
|
|
61
|
+
expect(screen.getByText(/console.log/)).toBeVisible();
|
|
62
|
+
expect(screen.getByRole('button', { name: 'Copy code' })).toBeVisible();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should render headings correctly', () => {
|
|
66
|
+
render(<MarkdownContent content={HEADING} />);
|
|
67
|
+
expect(screen.getByRole('heading', { name: /Heading 1/i })).toBeTruthy();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should render links correctly', () => {
|
|
71
|
+
render(<MarkdownContent content={LINK} />);
|
|
72
|
+
expect(screen.getByRole('link', { name: /PatternFly/i })).toBeTruthy();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should render unordered lists correctly', () => {
|
|
76
|
+
render(<MarkdownContent content={UNORDERED_LIST} />);
|
|
77
|
+
expect(screen.getByText('Item 1')).toBeTruthy();
|
|
78
|
+
expect(screen.getByText('Item 2')).toBeTruthy();
|
|
79
|
+
expect(screen.getByText('Item 3')).toBeTruthy();
|
|
80
|
+
expect(screen.getAllByRole('listitem')).toHaveLength(3);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should render ordered lists correctly', () => {
|
|
84
|
+
render(<MarkdownContent content={ORDERED_LIST} />);
|
|
85
|
+
expect(screen.getByText('First item')).toBeTruthy();
|
|
86
|
+
expect(screen.getByText('Second item')).toBeTruthy();
|
|
87
|
+
expect(screen.getByText('Third item')).toBeTruthy();
|
|
88
|
+
expect(screen.getAllByRole('listitem')).toHaveLength(3);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('should render tables correctly', () => {
|
|
92
|
+
render(<MarkdownContent content={TABLE} tableProps={{ 'aria-label': 'Test table' }} />);
|
|
93
|
+
expect(screen.getByRole('grid', { name: /Test table/i })).toBeTruthy();
|
|
94
|
+
expect(screen.getByRole('columnheader', { name: /Column 1/i })).toBeTruthy();
|
|
95
|
+
expect(screen.getByRole('columnheader', { name: /Column 2/i })).toBeTruthy();
|
|
96
|
+
expect(screen.getByRole('cell', { name: /Cell 1/i })).toBeTruthy();
|
|
97
|
+
expect(screen.getByRole('cell', { name: /Cell 2/i })).toBeTruthy();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('should render blockquotes correctly', () => {
|
|
101
|
+
render(<MarkdownContent content={BLOCKQUOTE} />);
|
|
102
|
+
|
|
103
|
+
const quote = screen.getByText(/This is a blockquote/);
|
|
104
|
+
expect(quote).toBeVisible();
|
|
105
|
+
expect(quote.closest('.pf-v6-c-content--blockquote')?.tagName).toBe('BLOCKQUOTE');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('should render images when hasNoImages is false', () => {
|
|
109
|
+
render(<MarkdownContent content={IMAGE} hasNoImages={false} />);
|
|
110
|
+
expect(screen.getByRole('img', { name: /Alt text/i })).toBeTruthy();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('should not render images when hasNoImages is true', () => {
|
|
114
|
+
render(<MarkdownContent content={IMAGE} hasNoImages />);
|
|
115
|
+
expect(screen.queryByRole('img', { name: /Alt text/i })).toBeFalsy();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('should disable markdown rendering when isMarkdownDisabled is true', () => {
|
|
119
|
+
render(<MarkdownContent content={BOLD_TEXT} isMarkdownDisabled />);
|
|
120
|
+
expect(screen.getByText('**Bold text**')).toBeTruthy();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('should render text component when isMarkdownDisabled is true and textComponent is provided', () => {
|
|
124
|
+
const textComponent = <div data-testid="custom-text">Custom text component</div>;
|
|
125
|
+
render(<MarkdownContent content={BOLD_TEXT} isMarkdownDisabled textComponent={textComponent} />);
|
|
126
|
+
expect(screen.getByTestId('custom-text')).toBeTruthy();
|
|
127
|
+
expect(screen.getByText('Custom text component')).toBeTruthy();
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('should apply isPrimary prop to elements', () => {
|
|
131
|
+
const { container } = render(<MarkdownContent content={INLINE_CODE} isPrimary />);
|
|
132
|
+
expect(container.querySelector('.pf-m-primary')).toBeTruthy();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('should apply shouldRetainStyles prop to elements', () => {
|
|
136
|
+
const { container } = render(<MarkdownContent content={BOLD_TEXT} shouldRetainStyles />);
|
|
137
|
+
expect(container.querySelector('.pf-m-markdown')).toBeTruthy();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('should pass codeBlockProps to code blocks', () => {
|
|
141
|
+
render(<MarkdownContent content={CODE_BLOCK} codeBlockProps={{ 'aria-label': 'Custom code block' }} />);
|
|
142
|
+
expect(screen.getByRole('button', { name: /Custom code block/i })).toBeTruthy();
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('should pass tableProps to tables', () => {
|
|
146
|
+
render(<MarkdownContent content={TABLE} tableProps={{ 'aria-label': 'Custom table label' }} />);
|
|
147
|
+
expect(screen.getByRole('grid', { name: /Custom table label/i })).toBeTruthy();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('should open links in new tab when openLinkInNewTab is true', () => {
|
|
151
|
+
render(<MarkdownContent content={LINK} openLinkInNewTab />);
|
|
152
|
+
expect(rehypeExternalLinks).toHaveBeenCalledTimes(1);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('should not open links in new tab when openLinkInNewTab is false', () => {
|
|
156
|
+
render(<MarkdownContent content={LINK} openLinkInNewTab={false} />);
|
|
157
|
+
expect(rehypeExternalLinks).not.toHaveBeenCalled();
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('should pass linkProps to links', async () => {
|
|
161
|
+
const onClick = jest.fn();
|
|
162
|
+
render(<MarkdownContent content={LINK} linkProps={{ onClick }} />);
|
|
163
|
+
const link = screen.getByRole('link', { name: /PatternFly/i });
|
|
164
|
+
link.click();
|
|
165
|
+
expect(onClick).toHaveBeenCalledTimes(1);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('should handle reactMarkdownProps.disallowedElements', () => {
|
|
169
|
+
render(<MarkdownContent content={CODE_BLOCK} reactMarkdownProps={{ disallowedElements: ['code'] }} />);
|
|
170
|
+
// Code block should not render when disallowed
|
|
171
|
+
expect(screen.queryByRole('button', { name: /Copy code/i })).toBeFalsy();
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('should render plain text when no markdown is present', () => {
|
|
175
|
+
render(<MarkdownContent content="Plain text without markdown" />);
|
|
176
|
+
expect(screen.getByText('Plain text without markdown')).toBeTruthy();
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('should handle empty content', () => {
|
|
180
|
+
const { container } = render(<MarkdownContent content="" />);
|
|
181
|
+
expect(container.textContent).toBe('');
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('should handle undefined content', () => {
|
|
185
|
+
const { container } = render(<MarkdownContent />);
|
|
186
|
+
expect(container.textContent).toBe('');
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('should render multiple markdown elements together', () => {
|
|
190
|
+
const content = `# Heading
|
|
191
|
+
|
|
192
|
+
**Bold text** and *italic text*
|
|
193
|
+
|
|
194
|
+
\`\`\`javascript
|
|
195
|
+
const x = 5;
|
|
196
|
+
\`\`\`
|
|
197
|
+
|
|
198
|
+
[Link](https://example.com)`;
|
|
199
|
+
|
|
200
|
+
render(<MarkdownContent content={content} />);
|
|
201
|
+
expect(screen.getByRole('heading', { name: /Heading/i })).toBeTruthy();
|
|
202
|
+
expect(screen.getByText('Bold text')).toBeTruthy();
|
|
203
|
+
expect(screen.getByText('italic text')).toBeTruthy();
|
|
204
|
+
expect(screen.getByText(/const x = 5/)).toBeTruthy();
|
|
205
|
+
expect(screen.getByRole('link', { name: /Link/i })).toBeTruthy();
|
|
206
|
+
});
|
|
207
|
+
});
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Markdown Content - Shared component for rendering markdown
|
|
3
|
+
// ============================================================================
|
|
4
|
+
import { type FunctionComponent, ReactNode } from 'react';
|
|
5
|
+
import Markdown, { Options } from 'react-markdown';
|
|
6
|
+
import remarkGfm from 'remark-gfm';
|
|
7
|
+
import { ContentVariants } from '@patternfly/react-core';
|
|
8
|
+
import CodeBlockMessage, { CodeBlockMessageProps } from '../Message/CodeBlockMessage/CodeBlockMessage';
|
|
9
|
+
import TextMessage from '../Message/TextMessage/TextMessage';
|
|
10
|
+
import ListItemMessage from '../Message/ListMessage/ListItemMessage';
|
|
11
|
+
import UnorderedListMessage from '../Message/ListMessage/UnorderedListMessage';
|
|
12
|
+
import OrderedListMessage from '../Message/ListMessage/OrderedListMessage';
|
|
13
|
+
import TableMessage from '../Message/TableMessage/TableMessage';
|
|
14
|
+
import TrMessage from '../Message/TableMessage/TrMessage';
|
|
15
|
+
import TdMessage from '../Message/TableMessage/TdMessage';
|
|
16
|
+
import TbodyMessage from '../Message/TableMessage/TbodyMessage';
|
|
17
|
+
import TheadMessage from '../Message/TableMessage/TheadMessage';
|
|
18
|
+
import ThMessage from '../Message/TableMessage/ThMessage';
|
|
19
|
+
import { TableProps } from '@patternfly/react-table';
|
|
20
|
+
import ImageMessage from '../Message/ImageMessage/ImageMessage';
|
|
21
|
+
import rehypeUnwrapImages from 'rehype-unwrap-images';
|
|
22
|
+
import rehypeExternalLinks from 'rehype-external-links';
|
|
23
|
+
import rehypeSanitize from 'rehype-sanitize';
|
|
24
|
+
import rehypeHighlight from 'rehype-highlight';
|
|
25
|
+
import 'highlight.js/styles/vs2015.css';
|
|
26
|
+
import { PluggableList } from 'unified';
|
|
27
|
+
import LinkMessage from '../Message/LinkMessage/LinkMessage';
|
|
28
|
+
import { rehypeMoveImagesOutOfParagraphs } from '../Message/Plugins/rehypeMoveImagesOutOfParagraphs';
|
|
29
|
+
import SuperscriptMessage from '../Message/SuperscriptMessage/SuperscriptMessage';
|
|
30
|
+
import { ButtonProps } from '@patternfly/react-core';
|
|
31
|
+
import { css } from '@patternfly/react-styles';
|
|
32
|
+
|
|
33
|
+
export interface MarkdownContentProps {
|
|
34
|
+
/** The markdown content to render */
|
|
35
|
+
content?: string;
|
|
36
|
+
/** Disables markdown parsing, allowing only text input */
|
|
37
|
+
isMarkdownDisabled?: boolean;
|
|
38
|
+
/** Props for code blocks */
|
|
39
|
+
codeBlockProps?: CodeBlockMessageProps;
|
|
40
|
+
/** Props for table message. It is important to include a detailed aria-label that describes the purpose of the table. */
|
|
41
|
+
tableProps?: Required<Pick<TableProps, 'aria-label'>> & TableProps;
|
|
42
|
+
/** Additional rehype plugins passed from the consumer */
|
|
43
|
+
additionalRehypePlugins?: PluggableList;
|
|
44
|
+
/** Additional remark plugins passed from the consumer */
|
|
45
|
+
additionalRemarkPlugins?: PluggableList;
|
|
46
|
+
/** Whether to open links in message in new tab. */
|
|
47
|
+
openLinkInNewTab?: boolean;
|
|
48
|
+
/** Props for links */
|
|
49
|
+
linkProps?: ButtonProps;
|
|
50
|
+
/** Allows passing additional props down to markdown parser react-markdown, such as allowedElements and disallowedElements. See https://github.com/remarkjs/react-markdown?tab=readme-ov-file#options for options */
|
|
51
|
+
reactMarkdownProps?: Options;
|
|
52
|
+
/** Allows passing additional props down to remark-gfm. See https://github.com/remarkjs/remark-gfm?tab=readme-ov-file#options for options */
|
|
53
|
+
remarkGfmProps?: Options;
|
|
54
|
+
/** Whether to strip out images in markdown */
|
|
55
|
+
hasNoImages?: boolean;
|
|
56
|
+
/** Sets background colors to be appropriate on primary chatbot background */
|
|
57
|
+
isPrimary?: boolean;
|
|
58
|
+
/** Custom component to render when markdown is disabled */
|
|
59
|
+
textComponent?: ReactNode;
|
|
60
|
+
/** Flag indicating whether content should retain various styles of its context (typically font-size and text color). */
|
|
61
|
+
shouldRetainStyles?: boolean;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export const MarkdownContent: FunctionComponent<MarkdownContentProps> = ({
|
|
65
|
+
content,
|
|
66
|
+
isMarkdownDisabled,
|
|
67
|
+
codeBlockProps,
|
|
68
|
+
tableProps,
|
|
69
|
+
openLinkInNewTab = true,
|
|
70
|
+
additionalRehypePlugins = [],
|
|
71
|
+
additionalRemarkPlugins = [],
|
|
72
|
+
linkProps,
|
|
73
|
+
reactMarkdownProps,
|
|
74
|
+
remarkGfmProps,
|
|
75
|
+
hasNoImages = false,
|
|
76
|
+
isPrimary,
|
|
77
|
+
textComponent,
|
|
78
|
+
shouldRetainStyles
|
|
79
|
+
}: MarkdownContentProps) => {
|
|
80
|
+
let rehypePlugins: PluggableList = [rehypeUnwrapImages, rehypeMoveImagesOutOfParagraphs, rehypeHighlight];
|
|
81
|
+
if (openLinkInNewTab) {
|
|
82
|
+
rehypePlugins = rehypePlugins.concat([[rehypeExternalLinks, { target: '_blank' }, rehypeSanitize]]);
|
|
83
|
+
}
|
|
84
|
+
if (additionalRehypePlugins) {
|
|
85
|
+
rehypePlugins.push(...additionalRehypePlugins);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const disallowedElements = hasNoImages ? ['img'] : [];
|
|
89
|
+
if (reactMarkdownProps && reactMarkdownProps.disallowedElements) {
|
|
90
|
+
disallowedElements.push(...reactMarkdownProps.disallowedElements);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (isMarkdownDisabled) {
|
|
94
|
+
if (textComponent) {
|
|
95
|
+
return <>{textComponent}</>;
|
|
96
|
+
}
|
|
97
|
+
return (
|
|
98
|
+
<TextMessage component={ContentVariants.p} isPrimary={isPrimary}>
|
|
99
|
+
{content}
|
|
100
|
+
</TextMessage>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<Markdown
|
|
106
|
+
components={{
|
|
107
|
+
section: (props) => {
|
|
108
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
109
|
+
const { node, ...rest } = props;
|
|
110
|
+
return (
|
|
111
|
+
<section
|
|
112
|
+
{...rest}
|
|
113
|
+
className={css('pf-chatbot__message-text', shouldRetainStyles && 'pf-m-markdown', rest?.className)}
|
|
114
|
+
/>
|
|
115
|
+
);
|
|
116
|
+
},
|
|
117
|
+
p: (props) => {
|
|
118
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
119
|
+
const { node, ...rest } = props;
|
|
120
|
+
return (
|
|
121
|
+
<TextMessage
|
|
122
|
+
shouldRetainStyles={shouldRetainStyles}
|
|
123
|
+
component={ContentVariants.p}
|
|
124
|
+
{...rest}
|
|
125
|
+
isPrimary={isPrimary}
|
|
126
|
+
/>
|
|
127
|
+
);
|
|
128
|
+
},
|
|
129
|
+
code: ({ children, ...props }) => {
|
|
130
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
131
|
+
const { node, ...codeProps } = props;
|
|
132
|
+
return (
|
|
133
|
+
<CodeBlockMessage
|
|
134
|
+
{...codeProps}
|
|
135
|
+
{...codeBlockProps}
|
|
136
|
+
isPrimary={isPrimary}
|
|
137
|
+
shouldRetainStyles={shouldRetainStyles}
|
|
138
|
+
>
|
|
139
|
+
{children}
|
|
140
|
+
</CodeBlockMessage>
|
|
141
|
+
);
|
|
142
|
+
},
|
|
143
|
+
h1: (props) => {
|
|
144
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
145
|
+
const { node, ...rest } = props;
|
|
146
|
+
return <TextMessage shouldRetainStyles={shouldRetainStyles} component={ContentVariants.h1} {...rest} />;
|
|
147
|
+
},
|
|
148
|
+
h2: (props) => {
|
|
149
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
150
|
+
const { node, ...rest } = props;
|
|
151
|
+
return <TextMessage shouldRetainStyles={shouldRetainStyles} component={ContentVariants.h2} {...rest} />;
|
|
152
|
+
},
|
|
153
|
+
h3: (props) => {
|
|
154
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
155
|
+
const { node, ...rest } = props;
|
|
156
|
+
return <TextMessage shouldRetainStyles={shouldRetainStyles} component={ContentVariants.h3} {...rest} />;
|
|
157
|
+
},
|
|
158
|
+
h4: (props) => {
|
|
159
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
160
|
+
const { node, ...rest } = props;
|
|
161
|
+
return <TextMessage shouldRetainStyles={shouldRetainStyles} component={ContentVariants.h4} {...rest} />;
|
|
162
|
+
},
|
|
163
|
+
h5: (props) => {
|
|
164
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
165
|
+
const { node, ...rest } = props;
|
|
166
|
+
return <TextMessage shouldRetainStyles={shouldRetainStyles} component={ContentVariants.h5} {...rest} />;
|
|
167
|
+
},
|
|
168
|
+
h6: (props) => {
|
|
169
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
170
|
+
const { node, ...rest } = props;
|
|
171
|
+
return <TextMessage shouldRetainStyles={shouldRetainStyles} component={ContentVariants.h6} {...rest} />;
|
|
172
|
+
},
|
|
173
|
+
blockquote: (props) => {
|
|
174
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
175
|
+
const { node, ...rest } = props;
|
|
176
|
+
return (
|
|
177
|
+
<TextMessage shouldRetainStyles={shouldRetainStyles} component={ContentVariants.blockquote} {...rest} />
|
|
178
|
+
);
|
|
179
|
+
},
|
|
180
|
+
ul: (props) => {
|
|
181
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
182
|
+
const { node, ...rest } = props;
|
|
183
|
+
return <UnorderedListMessage shouldRetainStyles={shouldRetainStyles} {...rest} />;
|
|
184
|
+
},
|
|
185
|
+
ol: (props) => {
|
|
186
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
187
|
+
const { node, ...rest } = props;
|
|
188
|
+
return <OrderedListMessage shouldRetainStyles={shouldRetainStyles} {...rest} />;
|
|
189
|
+
},
|
|
190
|
+
li: (props) => {
|
|
191
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
192
|
+
const { node, ...rest } = props;
|
|
193
|
+
return <ListItemMessage {...rest} />;
|
|
194
|
+
},
|
|
195
|
+
// table requires node attribute for calculating headers for mobile breakpoint
|
|
196
|
+
table: (props) => (
|
|
197
|
+
<TableMessage shouldRetainStyles={shouldRetainStyles} {...props} {...tableProps} isPrimary={isPrimary} />
|
|
198
|
+
),
|
|
199
|
+
tbody: (props) => {
|
|
200
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
201
|
+
const { node, ...rest } = props;
|
|
202
|
+
return <TbodyMessage {...rest} />;
|
|
203
|
+
},
|
|
204
|
+
thead: (props) => {
|
|
205
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
206
|
+
const { node, ...rest } = props;
|
|
207
|
+
return <TheadMessage {...rest} />;
|
|
208
|
+
},
|
|
209
|
+
tr: (props) => {
|
|
210
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
211
|
+
const { node, ...rest } = props;
|
|
212
|
+
return <TrMessage {...rest} />;
|
|
213
|
+
},
|
|
214
|
+
td: (props) => {
|
|
215
|
+
// Conflicts with Td type
|
|
216
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
217
|
+
const { node, width, ...rest } = props;
|
|
218
|
+
return <TdMessage {...rest} />;
|
|
219
|
+
},
|
|
220
|
+
th: (props) => {
|
|
221
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
222
|
+
const { node, ...rest } = props;
|
|
223
|
+
return <ThMessage {...rest} />;
|
|
224
|
+
},
|
|
225
|
+
img: (props) => {
|
|
226
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
227
|
+
const { node, ...rest } = props;
|
|
228
|
+
return <ImageMessage {...rest} />;
|
|
229
|
+
},
|
|
230
|
+
a: (props) => {
|
|
231
|
+
// node is just the details of the document structure - not needed
|
|
232
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
233
|
+
const { node, ...rest } = props;
|
|
234
|
+
return (
|
|
235
|
+
// some a types conflict with ButtonProps, but it's ok because we are using an a tag
|
|
236
|
+
// there are too many to handle manually
|
|
237
|
+
<LinkMessage shouldRetainStyles={shouldRetainStyles} {...(rest as any)} {...linkProps}>
|
|
238
|
+
{props.children}
|
|
239
|
+
</LinkMessage>
|
|
240
|
+
);
|
|
241
|
+
},
|
|
242
|
+
// used for footnotes
|
|
243
|
+
sup: (props) => {
|
|
244
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
245
|
+
const { node, ...rest } = props;
|
|
246
|
+
return <SuperscriptMessage {...rest} />;
|
|
247
|
+
}
|
|
248
|
+
}}
|
|
249
|
+
remarkPlugins={[[remarkGfm, { ...remarkGfmProps }], ...additionalRemarkPlugins]}
|
|
250
|
+
rehypePlugins={rehypePlugins}
|
|
251
|
+
{...reactMarkdownProps}
|
|
252
|
+
remarkRehypeOptions={{
|
|
253
|
+
// removes sr-only class from footnote labels applied by default
|
|
254
|
+
footnoteLabelProperties: { className: [''] },
|
|
255
|
+
...reactMarkdownProps?.remarkRehypeOptions
|
|
256
|
+
}}
|
|
257
|
+
disallowedElements={disallowedElements}
|
|
258
|
+
>
|
|
259
|
+
{content}
|
|
260
|
+
</Markdown>
|
|
261
|
+
);
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
export default MarkdownContent;
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
|
|
20
20
|
import { CheckIcon } from '@patternfly/react-icons/dist/esm/icons/check-icon';
|
|
21
21
|
import { CopyIcon } from '@patternfly/react-icons/dist/esm/icons/copy-icon';
|
|
22
|
+
import { css } from '@patternfly/react-styles';
|
|
22
23
|
|
|
23
24
|
export interface CodeBlockMessageProps {
|
|
24
25
|
/** Content rendered in code block */
|
|
@@ -41,6 +42,8 @@ export interface CodeBlockMessageProps {
|
|
|
41
42
|
customActions?: React.ReactNode;
|
|
42
43
|
/** Sets background colors to be appropriate on primary chatbot background */
|
|
43
44
|
isPrimary?: boolean;
|
|
45
|
+
/** Flag indicating that the content should retain message styles when using Markdown. */
|
|
46
|
+
shouldRetainStyles?: boolean;
|
|
44
47
|
}
|
|
45
48
|
|
|
46
49
|
const DEFAULT_EXPANDED_TEXT = 'Show less';
|
|
@@ -57,6 +60,7 @@ const CodeBlockMessage = ({
|
|
|
57
60
|
collapsedText = DEFAULT_COLLAPSED_TEXT,
|
|
58
61
|
customActions,
|
|
59
62
|
isPrimary,
|
|
63
|
+
shouldRetainStyles,
|
|
60
64
|
...props
|
|
61
65
|
}: CodeBlockMessageProps) => {
|
|
62
66
|
const [copied, setCopied] = useState(false);
|
|
@@ -138,7 +142,7 @@ const CodeBlockMessage = ({
|
|
|
138
142
|
);
|
|
139
143
|
|
|
140
144
|
return (
|
|
141
|
-
<div className=
|
|
145
|
+
<div className={css('pf-chatbot__message-code-block', shouldRetainStyles && 'pf-m-markdown')} ref={codeBlockRef}>
|
|
142
146
|
<CodeBlock actions={actions}>
|
|
143
147
|
<CodeBlockCode>
|
|
144
148
|
<>
|
|
@@ -5,8 +5,21 @@
|
|
|
5
5
|
import { Button, ButtonProps } from '@patternfly/react-core';
|
|
6
6
|
import { ExternalLinkSquareAltIcon } from '@patternfly/react-icons';
|
|
7
7
|
import { ExtraProps } from 'react-markdown';
|
|
8
|
+
import { css } from '@patternfly/react-styles';
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
export interface LinkMessageProps {
|
|
11
|
+
/** Flag indicating that the content should retain message styles when using Markdown. */
|
|
12
|
+
shouldRetainStyles?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const LinkMessage = ({
|
|
16
|
+
children,
|
|
17
|
+
target,
|
|
18
|
+
href,
|
|
19
|
+
id,
|
|
20
|
+
shouldRetainStyles,
|
|
21
|
+
...props
|
|
22
|
+
}: LinkMessageProps & ButtonProps & ExtraProps) => {
|
|
10
23
|
if (target === '_blank') {
|
|
11
24
|
return (
|
|
12
25
|
<Button
|
|
@@ -20,6 +33,7 @@ const LinkMessage = ({ children, target, href, id, ...props }: ButtonProps & Ext
|
|
|
20
33
|
// need to explicitly call this out or id doesn't seem to get passed - required for footnotes
|
|
21
34
|
id={id}
|
|
22
35
|
{...props}
|
|
36
|
+
className={css(shouldRetainStyles && 'pf-m-markdown', props?.className)}
|
|
23
37
|
>
|
|
24
38
|
{children}
|
|
25
39
|
</Button>
|
|
@@ -28,7 +42,15 @@ const LinkMessage = ({ children, target, href, id, ...props }: ButtonProps & Ext
|
|
|
28
42
|
|
|
29
43
|
return (
|
|
30
44
|
// need to explicitly call this out or id doesn't seem to get passed - required for footnotes
|
|
31
|
-
<Button
|
|
45
|
+
<Button
|
|
46
|
+
isInline
|
|
47
|
+
component="a"
|
|
48
|
+
href={href}
|
|
49
|
+
variant="link"
|
|
50
|
+
id={id}
|
|
51
|
+
{...props}
|
|
52
|
+
className={css(shouldRetainStyles && 'pf-m-markdown', props?.className)}
|
|
53
|
+
>
|
|
32
54
|
{children}
|
|
33
55
|
</Button>
|
|
34
56
|
);
|
|
@@ -4,9 +4,23 @@
|
|
|
4
4
|
|
|
5
5
|
import { ExtraProps } from 'react-markdown';
|
|
6
6
|
import { List, ListComponent, OrderType } from '@patternfly/react-core';
|
|
7
|
+
import { css } from '@patternfly/react-styles';
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
|
|
9
|
+
export interface OrderedListMessageProps {
|
|
10
|
+
/** The ordered list content */
|
|
11
|
+
children?: React.ReactNode;
|
|
12
|
+
/** The number to start the ordered list at. */
|
|
13
|
+
start?: number;
|
|
14
|
+
/** Flag indicating that the content should retain message styles when using Markdown. */
|
|
15
|
+
shouldRetainStyles?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const OrderedListMessage = ({
|
|
19
|
+
children,
|
|
20
|
+
start,
|
|
21
|
+
shouldRetainStyles
|
|
22
|
+
}: OrderedListMessageProps & JSX.IntrinsicElements['ol'] & ExtraProps) => (
|
|
23
|
+
<div className={css('pf-chatbot__message-ordered-list', shouldRetainStyles && 'pf-m-markdown')}>
|
|
10
24
|
<List component={ListComponent.ol} type={OrderType.number} start={start}>
|
|
11
25
|
{children}
|
|
12
26
|
</List>
|
|
@@ -4,9 +4,19 @@
|
|
|
4
4
|
|
|
5
5
|
import { ExtraProps } from 'react-markdown';
|
|
6
6
|
import { List } from '@patternfly/react-core';
|
|
7
|
+
import { css } from '@patternfly/react-styles';
|
|
8
|
+
export interface UnrderedListMessageProps {
|
|
9
|
+
/** The ordered list content */
|
|
10
|
+
children?: React.ReactNode;
|
|
11
|
+
/** Flag indicating that the content should retain message styles when using Markdown. */
|
|
12
|
+
shouldRetainStyles?: boolean;
|
|
13
|
+
}
|
|
7
14
|
|
|
8
|
-
const UnorderedListMessage = ({
|
|
9
|
-
|
|
15
|
+
const UnorderedListMessage = ({
|
|
16
|
+
children,
|
|
17
|
+
shouldRetainStyles
|
|
18
|
+
}: UnrderedListMessageProps & JSX.IntrinsicElements['ul'] & ExtraProps) => (
|
|
19
|
+
<div className={css('pf-chatbot__message-unordered-list', shouldRetainStyles && 'pf-m-markdown')}>
|
|
10
20
|
<List>{children}</List>
|
|
11
21
|
</div>
|
|
12
22
|
);
|