Skip to content

Commit 1663e56

Browse files
authored
Merge pull request #759 from thatblindgeye/iss668
feat(Calls/Response/Thinking): added support for Markdown
2 parents 8dd9cd1 + bc6af47 commit 1663e56

31 files changed

+1308
-368
lines changed

package-lock.json

Lines changed: 22 additions & 51 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { FunctionComponent } from 'react';
2+
import Message from '@patternfly/chatbot/dist/dynamic/Message';
3+
import patternflyAvatar from './patternfly_avatar.jpg';
4+
5+
export const MessageWithMarkdownDeepThinkingExample: FunctionComponent = () => (
6+
<Message
7+
name="Bot"
8+
role="bot"
9+
avatar={patternflyAvatar}
10+
content="This example shows how to use Markdown formatting in deep thinking content. Note the use of shouldRetainStyles to maintain proper formatting:"
11+
deepThinking={{
12+
shouldRetainStyles: true,
13+
toggleContent: 'Show thinking',
14+
subheading: '> Thought for 3 seconds',
15+
isSubheadingMarkdown: true,
16+
body: `I considered **multiple approaches** to answer your question:
17+
18+
1. *Direct response* - Quick but less comprehensive
19+
2. *Research-based* - Thorough but time-consuming
20+
3. **Balanced approach** - Combines speed and accuracy
21+
22+
I chose option 3 because it provides the best user experience.`,
23+
isBodyMarkdown: true
24+
}}
25+
/>
26+
);
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { FunctionComponent } from 'react';
2+
import Message from '@patternfly/chatbot/dist/dynamic/Message';
3+
import patternflyAvatar from './patternfly_avatar.jpg';
4+
5+
export const MessageWithMarkdownToolCallExample: FunctionComponent = () => (
6+
<Message
7+
name="Bot"
8+
role="bot"
9+
avatar={patternflyAvatar}
10+
content="This example shows how to use Markdown formatting in tool call content. Note the use of shouldRetainStyles to maintain proper formatting:"
11+
toolCall={{
12+
shouldRetainStyles: true,
13+
titleText: "Calling 'data_processor'",
14+
expandableContent: `**Processing data** from the following sources:
15+
16+
- Database query results
17+
- API responses
18+
- *Local cache*
19+
20+
\`\`\`json
21+
{
22+
"status": "processing",
23+
"items": 42
24+
}
25+
\`\`\``,
26+
isExpandableContentMarkdown: true
27+
}}
28+
/>
29+
);
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
import { useState, FunctionComponent, MouseEvent as ReactMouseEvent } from 'react';
2+
import Message from '@patternfly/chatbot/dist/dynamic/Message';
3+
import patternflyAvatar from './patternfly_avatar.jpg';
4+
import { CopyIcon, WrenchIcon } from '@patternfly/react-icons';
5+
import {
6+
Button,
7+
DescriptionList,
8+
DescriptionListDescription,
9+
DescriptionListGroup,
10+
DescriptionListTerm,
11+
ExpandableSection,
12+
ExpandableSectionVariant,
13+
Flex,
14+
FlexItem,
15+
Label
16+
} from '@patternfly/react-core';
17+
export const MessageWithToolResponseExample: FunctionComponent = () => {
18+
const [isExpanded, setIsExpanded] = useState(false);
19+
const onToggle = (_event: ReactMouseEvent, isExpanded: boolean) => {
20+
setIsExpanded(isExpanded);
21+
};
22+
const toolResponseBody = `The tool processed **3 database queries** and returned the following results:
23+
24+
1. User data - *42 records*
25+
2. Transaction history - *128 records*
26+
3. Analytics metrics - *15 data points*
27+
28+
\`\`\`json
29+
{
30+
"status": "success",
31+
"execution_time": "0.12s"
32+
}
33+
\`\`\``;
34+
return (
35+
<Message
36+
name="Bot"
37+
role="bot"
38+
avatar={patternflyAvatar}
39+
content="This example shows how to use Markdown formatting in tool response content. Note the use of shouldRetainStyles to maintain proper formatting:"
40+
toolResponse={{
41+
shouldRetainStyles: true,
42+
isToggleContentMarkdown: true,
43+
toggleContent: '**Tool response:** data_query_tool',
44+
isSubheadingMarkdown: true,
45+
subheading: '> Completed in 0.12 seconds',
46+
body: toolResponseBody,
47+
isBodyMarkdown: true,
48+
cardTitle: (
49+
<Flex
50+
alignItems={{
51+
default: 'alignItemsCenter'
52+
}}
53+
justifyContent={{
54+
default: 'justifyContentSpaceBetween'
55+
}}
56+
>
57+
<FlexItem>
58+
<Flex
59+
direction={{
60+
default: 'column'
61+
}}
62+
gap={{
63+
default: 'gapXs'
64+
}}
65+
>
66+
<FlexItem
67+
grow={{
68+
default: 'grow'
69+
}}
70+
>
71+
<Flex
72+
gap={{
73+
default: 'gapXs'
74+
}}
75+
>
76+
<FlexItem>
77+
<WrenchIcon
78+
style={{
79+
color: 'var(--pf-t--global--icon--color--brand--default'
80+
}}
81+
/>
82+
</FlexItem>
83+
<FlexItem>toolName</FlexItem>
84+
</Flex>
85+
</FlexItem>
86+
<FlexItem>
87+
<Flex
88+
gap={{
89+
default: 'gapSm'
90+
}}
91+
style={{
92+
fontSize: '12px',
93+
fontWeight: '400'
94+
}}
95+
>
96+
<FlexItem>Execution time:</FlexItem>
97+
<FlexItem>0.12 seconds</FlexItem>
98+
</Flex>
99+
</FlexItem>
100+
</Flex>
101+
</FlexItem>
102+
<FlexItem>
103+
<Button
104+
variant="plain"
105+
aria-label="Copy tool response to clipboard"
106+
icon={
107+
<CopyIcon
108+
style={{
109+
color: 'var(--pf-t--global--icon--color--subtle)'
110+
}}
111+
/>
112+
}
113+
></Button>
114+
</FlexItem>
115+
</Flex>
116+
),
117+
cardBody: (
118+
<>
119+
<DescriptionList
120+
style={{
121+
'--pf-v6-c-description-list--RowGap': 'var(--pf-t--global--spacer--md)'
122+
}}
123+
aria-label="Tool response"
124+
>
125+
<DescriptionListGroup
126+
style={{
127+
'--pf-v6-c-description-list__group--RowGap': 'var(--pf-t--global--spacer--xs)'
128+
}}
129+
>
130+
<DescriptionListTerm>Parameters</DescriptionListTerm>
131+
<DescriptionListDescription>
132+
<Flex
133+
direction={{
134+
default: 'column'
135+
}}
136+
>
137+
<FlexItem>Optional description text for parameters.</FlexItem>
138+
<FlexItem>
139+
<Flex
140+
gap={{
141+
default: 'gapSm'
142+
}}
143+
>
144+
<FlexItem>
145+
<Label variant="outline" color="blue">
146+
type
147+
</Label>
148+
</FlexItem>
149+
<FlexItem>
150+
<Label variant="outline" color="blue">
151+
properties
152+
</Label>
153+
</FlexItem>
154+
<FlexItem>
155+
<Label variant="outline" color="blue">
156+
label
157+
</Label>
158+
</FlexItem>
159+
<FlexItem>
160+
<Label variant="outline" color="blue">
161+
label
162+
</Label>
163+
</FlexItem>
164+
</Flex>
165+
</FlexItem>
166+
</Flex>
167+
</DescriptionListDescription>
168+
</DescriptionListGroup>
169+
<DescriptionListGroup
170+
style={{
171+
'--pf-v6-c-description-list__group--RowGap': 'var(--pf-t--global--spacer--xs)'
172+
}}
173+
>
174+
<DescriptionListTerm>Response</DescriptionListTerm>
175+
<DescriptionListDescription>
176+
<ExpandableSection
177+
variant={ExpandableSectionVariant.truncate}
178+
toggleTextExpanded="show less of response"
179+
toggleTextCollapsed="show more of response"
180+
onToggle={onToggle}
181+
isExpanded={isExpanded}
182+
style={{
183+
'--pf-v6-c-expandable-section__content--Opacity': '1',
184+
'--pf-v6-c-expandable-section__content--PaddingInlineStart': 0,
185+
'--pf-v6-c-expandable-section__content--TranslateY': 0,
186+
'--pf-v6-c-expandable-section--m-expand-top__content--TranslateY': 0
187+
}}
188+
>
189+
Descriptive text about the tool response, including completion status, details on the data that was
190+
processed, or anything else relevant to the use case.
191+
</ExpandableSection>
192+
</DescriptionListDescription>
193+
</DescriptionListGroup>
194+
</DescriptionList>
195+
</>
196+
)
197+
}}
198+
/>
199+
);
200+
};

packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/Messages.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,3 +349,35 @@ An attachment dropzone allows users to upload files via drag and drop.
349349
```js file="./FileDropZone.tsx"
350350

351351
```
352+
353+
## Examples with Markdown
354+
355+
The ChatBot supports Markdown formatting in several message components, allowing you to display rich, formatted content. This is particularly useful when you need to include code snippets, lists, emphasis, or other formatted text. The following examples demonstrate different ways you can use Markdown in a few of the ChatBot components, but this is not an exhaustive list of all Markdown customizations you can make.
356+
357+
To enable Markdown rendering, use the appropriate Markdown flag prop (such as `isBodyMarkdown`, `isSubheadingMarkdown`, or `isExpandableContentMarkdown`) depending on the component and content you're formatting.
358+
359+
**Important:** When using Markdown in these components, set `shouldRetainStyles: true` to retain the styling of the context the Markdown is used in. This ensures that Markdown content maintains the proper font sizes, colors, and other styling properties of its parent component. For example, Markdown passed into a toggle will retain the ChatBot toggle styling, while Markdown in a card body will maintain the appropriate card body styling. Without this prop, the Markdown may override the contextual styles and create inconsistencies with the rest of the ChatBot interface.
360+
361+
### Tool calls with Markdown
362+
363+
When displaying tool call information, you can use Markdown in the expandable content to provide formatted details about what the tool is processing. This is useful for showing structured data, code snippets, or formatted lists.
364+
365+
```ts file="./MessageWithMarkdownToolCall.tsx"
366+
367+
```
368+
369+
### Deep thinking with Markdown
370+
371+
Deep thinking content can include Markdown formatting in both the subheading and body to better communicate the LLM's reasoning process. This allows you to emphasize key points, structure thought processes with lists, or include other formatting.
372+
373+
```ts file="./MessageWithMarkdownDeepThinking.tsx"
374+
375+
```
376+
377+
### Tool responses with Markdown
378+
379+
Tool response cards support Markdown in multiple areas, including the toggle content, subheading, and body. Use `shouldRetainStyles: true` along with the appropriate Markdown flag props to ensure proper formatting and spacing.
380+
381+
```ts file="./MessageWithMarkdownToolResponse.tsx"
382+
383+
```

packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/Chatbot.md

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,14 +52,28 @@ import ExpandIcon from '@patternfly/react-icons/dist/esm/icons/expand-icon';
5252
import OpenDrawerRightIcon from '@patternfly/react-icons/dist/esm/icons/open-drawer-right-icon';
5353
import OutlinedWindowRestoreIcon from '@patternfly/react-icons/dist/esm/icons/outlined-window-restore-icon';
5454
import { BarsIcon } from '@patternfly/react-icons/dist/esm/icons/bars-icon';
55+
import { CopyIcon } from '@patternfly/react-icons/dist/esm/icons/copy-icon';
56+
import { WrenchIcon } from '@patternfly/react-icons/dist/esm/icons/wrench-icon';
57+
import {
58+
Button,
59+
DescriptionList,
60+
DescriptionListDescription,
61+
DescriptionListGroup,
62+
DescriptionListTerm,
63+
ExpandableSection,
64+
ExpandableSectionVariant,
65+
Flex,
66+
FlexItem,
67+
Label
68+
} from '@patternfly/react-core';
5569
import PFHorizontalLogoColor from '../UI/PF-HorizontalLogo-Color.svg';
5670
import PFHorizontalLogoReverse from '../UI/PF-HorizontalLogo-Reverse.svg';
5771
import PFIconLogoColor from '../UI/PF-IconLogo-Color.svg';
5872
import PFIconLogoReverse from '../UI/PF-IconLogo-Reverse.svg';
5973
import userAvatar from '../Messages/user_avatar.svg';
6074
import patternflyAvatar from '../Messages/patternfly_avatar.jpg';
6175
import { getTrackingProviders } from "@patternfly/chatbot/dist/dynamic/tracking";
62-
import { useEffect,useCallback, useRef, useState, FunctionComponent, MouseEvent } from 'react';
76+
import { useEffect,useCallback, useRef, useState, FunctionComponent, MouseEvent, MouseEvent as ReactMouseEvent } from 'react';
6377
import saveAs from 'file-saver';
6478

6579
### Basic ChatBot

packages/module/src/DeepThinking/DeepThinking.test.tsx

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,4 +119,52 @@ describe('DeepThinking', () => {
119119
expect(toggleButton).toHaveAttribute('aria-expanded', 'false');
120120
expect(screen.getByText('Thinking content')).not.toBeVisible();
121121
});
122+
123+
it('should render toggleContent as markdown when isToggleContentMarkdown is true', () => {
124+
const toggleContent = '**Bold thinking**';
125+
const { container } = render(<DeepThinking toggleContent={toggleContent} isToggleContentMarkdown />);
126+
expect(container.querySelector('strong')).toBeTruthy();
127+
expect(screen.getByText('Bold thinking')).toBeTruthy();
128+
});
129+
130+
it('should not render toggleContent as markdown when isToggleContentMarkdown is false', () => {
131+
const toggleContent = '**Bold thinking**';
132+
const { container } = render(<DeepThinking toggleContent={toggleContent} />);
133+
expect(container.querySelector('strong')).toBeFalsy();
134+
expect(screen.getByText('**Bold thinking**')).toBeTruthy();
135+
});
136+
137+
it('should render subheading as markdown when isSubheadingMarkdown is true', () => {
138+
const subheading = '**Bold subheading**';
139+
const { container } = render(<DeepThinking {...defaultProps} subheading={subheading} isSubheadingMarkdown />);
140+
expect(container.querySelector('strong')).toBeTruthy();
141+
expect(screen.getByText('Bold subheading')).toBeTruthy();
142+
});
143+
144+
it('should not render subheading as markdown when isSubheadingMarkdown is false', () => {
145+
const subheading = '**Bold subheading**';
146+
render(<DeepThinking {...defaultProps} subheading={subheading} />);
147+
expect(screen.getByText('**Bold subheading**')).toBeTruthy();
148+
});
149+
150+
it('should render body as markdown when isBodyMarkdown is true', () => {
151+
const body = '**Bold body**';
152+
const { container } = render(<DeepThinking {...defaultProps} body={body} isBodyMarkdown />);
153+
expect(container.querySelector('strong')).toBeTruthy();
154+
expect(screen.getByText('Bold body')).toBeTruthy();
155+
});
156+
157+
it('should not render body as markdown when isBodyMarkdown is false', () => {
158+
const body = '**Bold body**';
159+
render(<DeepThinking {...defaultProps} body={body} />);
160+
expect(screen.getByText('**Bold body**')).toBeTruthy();
161+
});
162+
163+
it('should pass markdownContentProps to MarkdownContent component', () => {
164+
const body = '**Bold body**';
165+
const { container } = render(
166+
<DeepThinking {...defaultProps} body={body} isBodyMarkdown markdownContentProps={{ isPrimary: true }} />
167+
);
168+
expect(container.querySelector('.pf-m-primary')).toBeTruthy();
169+
});
122170
});

0 commit comments

Comments
 (0)