@patternfly/chatbot 6.5.0-prerelease.21 → 6.5.0-prerelease.22
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 +2 -0
- package/dist/cjs/DeepThinking/DeepThinking.js +2 -2
- package/dist/cjs/DeepThinking/DeepThinking.test.js +41 -0
- package/dist/cjs/ToolCall/ToolCall.d.ts +2 -0
- package/dist/cjs/ToolCall/ToolCall.js +7 -2
- package/dist/cjs/ToolCall/ToolCall.test.js +26 -0
- package/dist/cjs/ToolResponse/ToolResponse.d.ts +2 -0
- package/dist/cjs/ToolResponse/ToolResponse.js +2 -2
- package/dist/cjs/ToolResponse/ToolResponse.test.js +40 -0
- package/dist/esm/DeepThinking/DeepThinking.d.ts +2 -0
- package/dist/esm/DeepThinking/DeepThinking.js +2 -2
- package/dist/esm/DeepThinking/DeepThinking.test.js +41 -0
- package/dist/esm/ToolCall/ToolCall.d.ts +2 -0
- package/dist/esm/ToolCall/ToolCall.js +7 -2
- package/dist/esm/ToolCall/ToolCall.test.js +26 -0
- package/dist/esm/ToolResponse/ToolResponse.d.ts +2 -0
- package/dist/esm/ToolResponse/ToolResponse.js +2 -2
- package/dist/esm/ToolResponse/ToolResponse.test.js +40 -0
- 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/MessageWithToolCall.tsx +14 -1
- package/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithToolResponse.tsx +222 -105
- package/src/DeepThinking/DeepThinking.test.tsx +61 -0
- package/src/DeepThinking/DeepThinking.tsx +4 -1
- package/src/ToolCall/ToolCall.test.tsx +51 -0
- package/src/ToolCall/ToolCall.tsx +12 -1
- package/src/ToolResponse/ToolResponse.test.tsx +44 -0
- package/src/ToolResponse/ToolResponse.tsx +4 -1
|
@@ -3,6 +3,8 @@ import { type FunctionComponent } from 'react';
|
|
|
3
3
|
export interface DeepThinkingProps {
|
|
4
4
|
/** Toggle content shown for expandable section */
|
|
5
5
|
toggleContent: React.ReactNode;
|
|
6
|
+
/** Flag indicating whether the expandable content is expanded by default. */
|
|
7
|
+
isDefaultExpanded?: boolean;
|
|
6
8
|
/** Additional props passed to expandable section */
|
|
7
9
|
expandableSectionProps?: Omit<ExpandableSectionProps, 'ref'>;
|
|
8
10
|
/** Subheading rendered inside expandable section */
|
|
@@ -7,8 +7,8 @@ const jsx_runtime_1 = require("react/jsx-runtime");
|
|
|
7
7
|
// ============================================================================
|
|
8
8
|
const react_core_1 = require("@patternfly/react-core");
|
|
9
9
|
const react_1 = require("react");
|
|
10
|
-
const DeepThinking = ({ body, cardProps, expandableSectionProps, subheading, toggleContent, cardBodyProps }) => {
|
|
11
|
-
const [isExpanded, setIsExpanded] = (0, react_1.useState)(
|
|
10
|
+
const DeepThinking = ({ body, cardProps, expandableSectionProps, subheading, toggleContent, isDefaultExpanded = true, cardBodyProps }) => {
|
|
11
|
+
const [isExpanded, setIsExpanded] = (0, react_1.useState)(isDefaultExpanded);
|
|
12
12
|
const onToggle = (_event, isExpanded) => {
|
|
13
13
|
setIsExpanded(isExpanded);
|
|
14
14
|
};
|
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
2
11
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
12
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
13
|
};
|
|
5
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
15
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
7
16
|
const react_1 = require("@testing-library/react");
|
|
17
|
+
const user_event_1 = __importDefault(require("@testing-library/user-event"));
|
|
8
18
|
require("@testing-library/jest-dom");
|
|
9
19
|
const DeepThinking_1 = __importDefault(require("./DeepThinking"));
|
|
10
20
|
describe('DeepThinking', () => {
|
|
@@ -45,4 +55,35 @@ describe('DeepThinking', () => {
|
|
|
45
55
|
const subheadingContainer = container.querySelector('.pf-chatbot__tool-response-subheading');
|
|
46
56
|
expect(subheadingContainer).toBeFalsy();
|
|
47
57
|
});
|
|
58
|
+
it('should pass through cardBodyProps', () => {
|
|
59
|
+
(0, react_1.render)((0, jsx_runtime_1.jsx)(DeepThinking_1.default, Object.assign({}, defaultProps, { body: "Thinking content", cardBodyProps: { className: 'custom-card-body-class' } })));
|
|
60
|
+
const cardBody = react_1.screen.getByText('Thinking content').closest('.pf-v6-c-card__body');
|
|
61
|
+
expect(cardBody).toHaveClass('custom-card-body-class');
|
|
62
|
+
});
|
|
63
|
+
it('Renders expanded by default', () => {
|
|
64
|
+
(0, react_1.render)((0, jsx_runtime_1.jsx)(DeepThinking_1.default, Object.assign({}, defaultProps, { body: "Thinking content" })));
|
|
65
|
+
expect(react_1.screen.getByRole('button', { name: defaultProps.toggleContent })).toHaveAttribute('aria-expanded', 'true');
|
|
66
|
+
expect(react_1.screen.getByText('Thinking content')).toBeVisible();
|
|
67
|
+
});
|
|
68
|
+
it('Renders collapsed when isDefaultExpanded is false', () => {
|
|
69
|
+
(0, react_1.render)((0, jsx_runtime_1.jsx)(DeepThinking_1.default, Object.assign({ isDefaultExpanded: false }, defaultProps, { body: "Thinking content" })));
|
|
70
|
+
expect(react_1.screen.getByRole('button', { name: defaultProps.toggleContent })).toHaveAttribute('aria-expanded', 'false');
|
|
71
|
+
expect(react_1.screen.getByText('Thinking content')).not.toBeVisible();
|
|
72
|
+
});
|
|
73
|
+
it('expandableSectionProps.isExpanded overrides isDefaultExpanded', () => {
|
|
74
|
+
(0, react_1.render)((0, jsx_runtime_1.jsx)(DeepThinking_1.default, Object.assign({}, defaultProps, { isDefaultExpanded: false, body: "Thinking content", expandableSectionProps: { isExpanded: true } })));
|
|
75
|
+
expect(react_1.screen.getByRole('button', { name: defaultProps.toggleContent })).toHaveAttribute('aria-expanded', 'true');
|
|
76
|
+
expect(react_1.screen.getByText('Thinking content')).toBeVisible();
|
|
77
|
+
});
|
|
78
|
+
it('expandableSectionProps.onToggle overrides internal onToggle behavior', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
79
|
+
const user = user_event_1.default.setup();
|
|
80
|
+
const customOnToggle = jest.fn();
|
|
81
|
+
(0, react_1.render)((0, jsx_runtime_1.jsx)(DeepThinking_1.default, Object.assign({}, defaultProps, { isDefaultExpanded: false, body: "Thinking content", expandableSectionProps: { onToggle: customOnToggle } })));
|
|
82
|
+
const toggleButton = react_1.screen.getByRole('button', { name: defaultProps.toggleContent });
|
|
83
|
+
expect(toggleButton).toHaveAttribute('aria-expanded', 'false');
|
|
84
|
+
yield user.click(toggleButton);
|
|
85
|
+
expect(customOnToggle).toHaveBeenCalled();
|
|
86
|
+
expect(toggleButton).toHaveAttribute('aria-expanded', 'false');
|
|
87
|
+
expect(react_1.screen.getByText('Thinking content')).not.toBeVisible();
|
|
88
|
+
}));
|
|
48
89
|
});
|
|
@@ -11,6 +11,8 @@ export interface ToolCallProps {
|
|
|
11
11
|
spinnerProps?: SpinnerProps;
|
|
12
12
|
/** Content to render within an expandable section. */
|
|
13
13
|
expandableContent?: React.ReactNode;
|
|
14
|
+
/** Flag indicating whether the expandable content is expanded by default. */
|
|
15
|
+
isDefaultExpanded?: boolean;
|
|
14
16
|
/** Text content for the "run" action button. */
|
|
15
17
|
runButtonText?: string;
|
|
16
18
|
/** Additional props for the "run" action button. */
|
|
@@ -2,13 +2,18 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.ToolCall = void 0;
|
|
4
4
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
5
|
+
const react_1 = require("react");
|
|
5
6
|
const react_core_1 = require("@patternfly/react-core");
|
|
6
|
-
const ToolCall = ({ titleText, loadingText, isLoading, expandableContent, runButtonText = 'Run tool', runButtonProps, runActionItemProps, cancelButtonText = 'Cancel', cancelButtonProps, cancelActionItemProps, actions, actionListProps, actionListGroupProps, actionListItemProps, cardProps, cardBodyProps, cardFooterProps, expandableSectionProps, spinnerProps }) => {
|
|
7
|
+
const ToolCall = ({ titleText, loadingText, isLoading, expandableContent, isDefaultExpanded = false, runButtonText = 'Run tool', runButtonProps, runActionItemProps, cancelButtonText = 'Cancel', cancelButtonProps, cancelActionItemProps, actions, actionListProps, actionListGroupProps, actionListItemProps, cardProps, cardBodyProps, cardFooterProps, expandableSectionProps, spinnerProps }) => {
|
|
8
|
+
const [isExpanded, setIsExpanded] = (0, react_1.useState)(isDefaultExpanded);
|
|
9
|
+
const onToggle = (_event, isExpanded) => {
|
|
10
|
+
setIsExpanded(isExpanded);
|
|
11
|
+
};
|
|
7
12
|
const titleContent = ((0, jsx_runtime_1.jsx)("span", { className: `pf-chatbot__tool-call-title-content`, children: isLoading ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(react_core_1.Spinner, Object.assign({ diameter: "1em" }, spinnerProps)), ' ', (0, jsx_runtime_1.jsx)("span", { className: "pf-chatbot__tool-call-title-text", children: loadingText })] })) : ((0, jsx_runtime_1.jsx)("span", { className: "pf-chatbot__tool-call-title-text", children: titleText })) }));
|
|
8
13
|
const defaultActions = ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(react_core_1.ActionListItem, Object.assign({}, actionListItemProps, cancelActionItemProps, { children: (0, jsx_runtime_1.jsx)(react_core_1.Button, Object.assign({ variant: "link" }, cancelButtonProps, { children: cancelButtonText })) })), (0, jsx_runtime_1.jsx)(react_core_1.ActionListItem, Object.assign({}, actionListItemProps, runActionItemProps, { children: (0, jsx_runtime_1.jsx)(react_core_1.Button, Object.assign({ variant: "secondary" }, runButtonProps, { children: runButtonText })) }))] }));
|
|
9
14
|
const customActions = actions &&
|
|
10
15
|
actions.map((action, index) => ((0, jsx_runtime_1.jsx)(react_core_1.ActionListItem, Object.assign({}, actionListItemProps, { children: action }), index)));
|
|
11
|
-
return ((0, jsx_runtime_1.jsxs)(react_core_1.Card, Object.assign({ isCompact: true, className: "pf-chatbot__tool-call" }, cardProps, { children: [(0, jsx_runtime_1.jsx)(react_core_1.CardBody, Object.assign({ className: "pf-chatbot__tool-call-title" }, cardBodyProps, { children: expandableContent && !isLoading ? ((0, jsx_runtime_1.jsx)(react_core_1.ExpandableSection, Object.assign({ className: "pf-chatbot__tool-call-expandable-section", toggleContent: titleContent, isIndented: true }, expandableSectionProps, { children: expandableContent }))) : (titleContent) })), !isLoading && ((0, jsx_runtime_1.jsx)(react_core_1.CardFooter, Object.assign({}, cardFooterProps, { children: (0, jsx_runtime_1.jsx)(react_core_1.ActionList, Object.assign({ className: "pf-chatbot__tool-call-action-list" }, actionListProps, { children: (0, jsx_runtime_1.jsx)(react_core_1.ActionListGroup, Object.assign({}, actionListGroupProps, { children: customActions || defaultActions })) })) })))] })));
|
|
16
|
+
return ((0, jsx_runtime_1.jsxs)(react_core_1.Card, Object.assign({ isCompact: true, className: "pf-chatbot__tool-call" }, cardProps, { children: [(0, jsx_runtime_1.jsx)(react_core_1.CardBody, Object.assign({ className: "pf-chatbot__tool-call-title" }, cardBodyProps, { children: expandableContent && !isLoading ? ((0, jsx_runtime_1.jsx)(react_core_1.ExpandableSection, Object.assign({ className: "pf-chatbot__tool-call-expandable-section", toggleContent: titleContent, onToggle: onToggle, isExpanded: isExpanded, isIndented: true }, expandableSectionProps, { children: expandableContent }))) : (titleContent) })), !isLoading && ((0, jsx_runtime_1.jsx)(react_core_1.CardFooter, Object.assign({}, cardFooterProps, { children: (0, jsx_runtime_1.jsx)(react_core_1.ActionList, Object.assign({ className: "pf-chatbot__tool-call-action-list" }, actionListProps, { children: (0, jsx_runtime_1.jsx)(react_core_1.ActionListGroup, Object.assign({}, actionListGroupProps, { children: customActions || defaultActions })) })) })))] })));
|
|
12
17
|
};
|
|
13
18
|
exports.ToolCall = ToolCall;
|
|
14
19
|
exports.default = exports.ToolCall;
|
|
@@ -141,4 +141,30 @@ describe('ToolCall', () => {
|
|
|
141
141
|
(0, react_1.render)((0, jsx_runtime_1.jsx)(ToolCall_1.default, Object.assign({}, defaultProps, { cardFooterProps: { id: 'card-footer-test-id' } })));
|
|
142
142
|
expect(react_1.screen.getByRole('button', { name: 'Run tool' }).closest('#card-footer-test-id')).toBeVisible();
|
|
143
143
|
});
|
|
144
|
+
it('Renders collapsed by default when expandableContent is provided', () => {
|
|
145
|
+
(0, react_1.render)((0, jsx_runtime_1.jsx)(ToolCall_1.default, Object.assign({}, defaultProps, { expandableContent: "Expandable Content" })));
|
|
146
|
+
expect(react_1.screen.getByRole('button', { name: defaultProps.titleText })).toHaveAttribute('aria-expanded', 'false');
|
|
147
|
+
expect(react_1.screen.queryByText('Expandable Content')).not.toBeVisible();
|
|
148
|
+
});
|
|
149
|
+
it('Renders expanded when isDefaultExpanded is true', () => {
|
|
150
|
+
(0, react_1.render)((0, jsx_runtime_1.jsx)(ToolCall_1.default, Object.assign({}, defaultProps, { isDefaultExpanded: true, expandableContent: "Expandable Content" })));
|
|
151
|
+
expect(react_1.screen.getByRole('button', { name: defaultProps.titleText })).toHaveAttribute('aria-expanded', 'true');
|
|
152
|
+
expect(react_1.screen.getByText('Expandable Content')).toBeVisible();
|
|
153
|
+
});
|
|
154
|
+
it('expandableSectionProps.isExpanded overrides isDefaultExpanded', () => {
|
|
155
|
+
(0, react_1.render)((0, jsx_runtime_1.jsx)(ToolCall_1.default, Object.assign({}, defaultProps, { isDefaultExpanded: false, expandableContent: "Expandable Content", expandableSectionProps: { isExpanded: true } })));
|
|
156
|
+
expect(react_1.screen.getByRole('button', { name: defaultProps.titleText })).toHaveAttribute('aria-expanded', 'true');
|
|
157
|
+
expect(react_1.screen.getByText('Expandable Content')).toBeVisible();
|
|
158
|
+
});
|
|
159
|
+
it('expandableSectionProps.onToggle overrides internal onToggle behavior', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
160
|
+
const user = user_event_1.default.setup();
|
|
161
|
+
const customOnToggle = jest.fn();
|
|
162
|
+
(0, react_1.render)((0, jsx_runtime_1.jsx)(ToolCall_1.default, Object.assign({}, defaultProps, { isDefaultExpanded: false, expandableContent: "Expandable Content", expandableSectionProps: { onToggle: customOnToggle } })));
|
|
163
|
+
const toggleButton = react_1.screen.getByRole('button', { name: defaultProps.titleText });
|
|
164
|
+
expect(toggleButton).toHaveAttribute('aria-expanded', 'false');
|
|
165
|
+
yield user.click(toggleButton);
|
|
166
|
+
expect(customOnToggle).toHaveBeenCalledTimes(1);
|
|
167
|
+
expect(toggleButton).toHaveAttribute('aria-expanded', 'false');
|
|
168
|
+
expect(react_1.screen.queryByText('Expandable Content')).not.toBeVisible();
|
|
169
|
+
}));
|
|
144
170
|
});
|
|
@@ -3,6 +3,8 @@ import { type FunctionComponent } from 'react';
|
|
|
3
3
|
export interface ToolResponseProps {
|
|
4
4
|
/** Toggle content shown for expandable section */
|
|
5
5
|
toggleContent: React.ReactNode;
|
|
6
|
+
/** Flag indicating whether the expandable content is expanded by default. */
|
|
7
|
+
isDefaultExpanded?: boolean;
|
|
6
8
|
/** Additional props passed to expandable section */
|
|
7
9
|
expandableSectionProps?: Omit<ExpandableSectionProps, 'ref'>;
|
|
8
10
|
/** Subheading rendered inside expandable section */
|
|
@@ -7,8 +7,8 @@ const jsx_runtime_1 = require("react/jsx-runtime");
|
|
|
7
7
|
// ============================================================================
|
|
8
8
|
const react_core_1 = require("@patternfly/react-core");
|
|
9
9
|
const react_1 = require("react");
|
|
10
|
-
const ToolResponse = ({ body, cardProps, expandableSectionProps, subheading, cardBody, cardTitle, cardBodyProps, toggleContent, toolResponseCardBodyProps, toolResponseCardDividerProps, toolResponseCardProps, toolResponseCardTitleProps }) => {
|
|
11
|
-
const [isExpanded, setIsExpanded] = (0, react_1.useState)(
|
|
10
|
+
const ToolResponse = ({ body, cardProps, expandableSectionProps, subheading, cardBody, cardTitle, cardBodyProps, toggleContent, isDefaultExpanded = true, toolResponseCardBodyProps, toolResponseCardDividerProps, toolResponseCardProps, toolResponseCardTitleProps }) => {
|
|
11
|
+
const [isExpanded, setIsExpanded] = (0, react_1.useState)(isDefaultExpanded);
|
|
12
12
|
const onToggle = (_event, isExpanded) => {
|
|
13
13
|
setIsExpanded(isExpanded);
|
|
14
14
|
};
|
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
2
11
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
12
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
13
|
};
|
|
5
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
15
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
7
16
|
const react_1 = require("@testing-library/react");
|
|
17
|
+
const user_event_1 = __importDefault(require("@testing-library/user-event"));
|
|
8
18
|
require("@testing-library/jest-dom");
|
|
9
19
|
const ToolResponse_1 = __importDefault(require("./ToolResponse"));
|
|
10
20
|
describe('ToolResponse', () => {
|
|
@@ -81,4 +91,34 @@ describe('ToolResponse', () => {
|
|
|
81
91
|
const { container } = (0, react_1.render)((0, jsx_runtime_1.jsx)(ToolResponse_1.default, Object.assign({}, defaultProps, { cardBody: undefined })));
|
|
82
92
|
expect(container.querySelector('.pf-v6-c-divider')).toBeFalsy();
|
|
83
93
|
});
|
|
94
|
+
it('Renders expanded by default', () => {
|
|
95
|
+
(0, react_1.render)((0, jsx_runtime_1.jsx)(ToolResponse_1.default, Object.assign({}, defaultProps)));
|
|
96
|
+
expect(react_1.screen.getByRole('button', { name: defaultProps.toggleContent })).toHaveAttribute('aria-expanded', 'true');
|
|
97
|
+
expect(react_1.screen.getByText(defaultProps.cardTitle)).toBeVisible();
|
|
98
|
+
expect(react_1.screen.getByText(defaultProps.cardBody)).toBeVisible();
|
|
99
|
+
});
|
|
100
|
+
it('Renders collapsed when isDefaultExpanded is false', () => {
|
|
101
|
+
(0, react_1.render)((0, jsx_runtime_1.jsx)(ToolResponse_1.default, Object.assign({ isDefaultExpanded: false }, defaultProps)));
|
|
102
|
+
expect(react_1.screen.getByRole('button', { name: defaultProps.toggleContent })).toHaveAttribute('aria-expanded', 'false');
|
|
103
|
+
expect(react_1.screen.getByText(defaultProps.cardTitle)).not.toBeVisible();
|
|
104
|
+
expect(react_1.screen.getByText(defaultProps.cardBody)).not.toBeVisible();
|
|
105
|
+
});
|
|
106
|
+
it('expandableSectionProps.isExpanded overrides isDefaultExpanded', () => {
|
|
107
|
+
(0, react_1.render)((0, jsx_runtime_1.jsx)(ToolResponse_1.default, Object.assign({}, defaultProps, { isDefaultExpanded: false, expandableSectionProps: { isExpanded: true } })));
|
|
108
|
+
expect(react_1.screen.getByRole('button', { name: defaultProps.toggleContent })).toHaveAttribute('aria-expanded', 'true');
|
|
109
|
+
expect(react_1.screen.getByText(defaultProps.cardTitle)).toBeVisible();
|
|
110
|
+
expect(react_1.screen.getByText(defaultProps.cardBody)).toBeVisible();
|
|
111
|
+
});
|
|
112
|
+
it('expandableSectionProps.onToggle overrides internal onToggle behavior', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
113
|
+
const user = user_event_1.default.setup();
|
|
114
|
+
const customOnToggle = jest.fn();
|
|
115
|
+
(0, react_1.render)((0, jsx_runtime_1.jsx)(ToolResponse_1.default, Object.assign({}, defaultProps, { isDefaultExpanded: false, expandableSectionProps: { onToggle: customOnToggle } })));
|
|
116
|
+
const toggleButton = react_1.screen.getByRole('button', { name: defaultProps.toggleContent });
|
|
117
|
+
expect(toggleButton).toHaveAttribute('aria-expanded', 'false');
|
|
118
|
+
yield user.click(toggleButton);
|
|
119
|
+
expect(customOnToggle).toHaveBeenCalledTimes(1);
|
|
120
|
+
expect(toggleButton).toHaveAttribute('aria-expanded', 'false');
|
|
121
|
+
expect(react_1.screen.getByText(defaultProps.cardTitle)).not.toBeVisible();
|
|
122
|
+
expect(react_1.screen.getByText(defaultProps.cardBody)).not.toBeVisible();
|
|
123
|
+
}));
|
|
84
124
|
});
|
|
@@ -3,6 +3,8 @@ import { type FunctionComponent } from 'react';
|
|
|
3
3
|
export interface DeepThinkingProps {
|
|
4
4
|
/** Toggle content shown for expandable section */
|
|
5
5
|
toggleContent: React.ReactNode;
|
|
6
|
+
/** Flag indicating whether the expandable content is expanded by default. */
|
|
7
|
+
isDefaultExpanded?: boolean;
|
|
6
8
|
/** Additional props passed to expandable section */
|
|
7
9
|
expandableSectionProps?: Omit<ExpandableSectionProps, 'ref'>;
|
|
8
10
|
/** Subheading rendered inside expandable section */
|
|
@@ -4,8 +4,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
4
4
|
// ============================================================================
|
|
5
5
|
import { Card, CardBody, ExpandableSection } from '@patternfly/react-core';
|
|
6
6
|
import { useState } from 'react';
|
|
7
|
-
export const DeepThinking = ({ body, cardProps, expandableSectionProps, subheading, toggleContent, cardBodyProps }) => {
|
|
8
|
-
const [isExpanded, setIsExpanded] = useState(
|
|
7
|
+
export const DeepThinking = ({ body, cardProps, expandableSectionProps, subheading, toggleContent, isDefaultExpanded = true, cardBodyProps }) => {
|
|
8
|
+
const [isExpanded, setIsExpanded] = useState(isDefaultExpanded);
|
|
9
9
|
const onToggle = (_event, isExpanded) => {
|
|
10
10
|
setIsExpanded(isExpanded);
|
|
11
11
|
};
|
|
@@ -1,5 +1,15 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
1
10
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
11
|
import { render, screen } from '@testing-library/react';
|
|
12
|
+
import userEvent from '@testing-library/user-event';
|
|
3
13
|
import '@testing-library/jest-dom';
|
|
4
14
|
import DeepThinking from './DeepThinking';
|
|
5
15
|
describe('DeepThinking', () => {
|
|
@@ -40,4 +50,35 @@ describe('DeepThinking', () => {
|
|
|
40
50
|
const subheadingContainer = container.querySelector('.pf-chatbot__tool-response-subheading');
|
|
41
51
|
expect(subheadingContainer).toBeFalsy();
|
|
42
52
|
});
|
|
53
|
+
it('should pass through cardBodyProps', () => {
|
|
54
|
+
render(_jsx(DeepThinking, Object.assign({}, defaultProps, { body: "Thinking content", cardBodyProps: { className: 'custom-card-body-class' } })));
|
|
55
|
+
const cardBody = screen.getByText('Thinking content').closest('.pf-v6-c-card__body');
|
|
56
|
+
expect(cardBody).toHaveClass('custom-card-body-class');
|
|
57
|
+
});
|
|
58
|
+
it('Renders expanded by default', () => {
|
|
59
|
+
render(_jsx(DeepThinking, Object.assign({}, defaultProps, { body: "Thinking content" })));
|
|
60
|
+
expect(screen.getByRole('button', { name: defaultProps.toggleContent })).toHaveAttribute('aria-expanded', 'true');
|
|
61
|
+
expect(screen.getByText('Thinking content')).toBeVisible();
|
|
62
|
+
});
|
|
63
|
+
it('Renders collapsed when isDefaultExpanded is false', () => {
|
|
64
|
+
render(_jsx(DeepThinking, Object.assign({ isDefaultExpanded: false }, defaultProps, { body: "Thinking content" })));
|
|
65
|
+
expect(screen.getByRole('button', { name: defaultProps.toggleContent })).toHaveAttribute('aria-expanded', 'false');
|
|
66
|
+
expect(screen.getByText('Thinking content')).not.toBeVisible();
|
|
67
|
+
});
|
|
68
|
+
it('expandableSectionProps.isExpanded overrides isDefaultExpanded', () => {
|
|
69
|
+
render(_jsx(DeepThinking, Object.assign({}, defaultProps, { isDefaultExpanded: false, body: "Thinking content", expandableSectionProps: { isExpanded: true } })));
|
|
70
|
+
expect(screen.getByRole('button', { name: defaultProps.toggleContent })).toHaveAttribute('aria-expanded', 'true');
|
|
71
|
+
expect(screen.getByText('Thinking content')).toBeVisible();
|
|
72
|
+
});
|
|
73
|
+
it('expandableSectionProps.onToggle overrides internal onToggle behavior', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
74
|
+
const user = userEvent.setup();
|
|
75
|
+
const customOnToggle = jest.fn();
|
|
76
|
+
render(_jsx(DeepThinking, Object.assign({}, defaultProps, { isDefaultExpanded: false, body: "Thinking content", expandableSectionProps: { onToggle: customOnToggle } })));
|
|
77
|
+
const toggleButton = screen.getByRole('button', { name: defaultProps.toggleContent });
|
|
78
|
+
expect(toggleButton).toHaveAttribute('aria-expanded', 'false');
|
|
79
|
+
yield user.click(toggleButton);
|
|
80
|
+
expect(customOnToggle).toHaveBeenCalled();
|
|
81
|
+
expect(toggleButton).toHaveAttribute('aria-expanded', 'false');
|
|
82
|
+
expect(screen.getByText('Thinking content')).not.toBeVisible();
|
|
83
|
+
}));
|
|
43
84
|
});
|
|
@@ -11,6 +11,8 @@ export interface ToolCallProps {
|
|
|
11
11
|
spinnerProps?: SpinnerProps;
|
|
12
12
|
/** Content to render within an expandable section. */
|
|
13
13
|
expandableContent?: React.ReactNode;
|
|
14
|
+
/** Flag indicating whether the expandable content is expanded by default. */
|
|
15
|
+
isDefaultExpanded?: boolean;
|
|
14
16
|
/** Text content for the "run" action button. */
|
|
15
17
|
runButtonText?: string;
|
|
16
18
|
/** Additional props for the "run" action button. */
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
2
3
|
import { ActionList, ActionListGroup, ActionListItem, Button, Card, CardBody, CardFooter, ExpandableSection, Spinner } from '@patternfly/react-core';
|
|
3
|
-
export const ToolCall = ({ titleText, loadingText, isLoading, expandableContent, runButtonText = 'Run tool', runButtonProps, runActionItemProps, cancelButtonText = 'Cancel', cancelButtonProps, cancelActionItemProps, actions, actionListProps, actionListGroupProps, actionListItemProps, cardProps, cardBodyProps, cardFooterProps, expandableSectionProps, spinnerProps }) => {
|
|
4
|
+
export const ToolCall = ({ titleText, loadingText, isLoading, expandableContent, isDefaultExpanded = false, runButtonText = 'Run tool', runButtonProps, runActionItemProps, cancelButtonText = 'Cancel', cancelButtonProps, cancelActionItemProps, actions, actionListProps, actionListGroupProps, actionListItemProps, cardProps, cardBodyProps, cardFooterProps, expandableSectionProps, spinnerProps }) => {
|
|
5
|
+
const [isExpanded, setIsExpanded] = useState(isDefaultExpanded);
|
|
6
|
+
const onToggle = (_event, isExpanded) => {
|
|
7
|
+
setIsExpanded(isExpanded);
|
|
8
|
+
};
|
|
4
9
|
const titleContent = (_jsx("span", { className: `pf-chatbot__tool-call-title-content`, children: isLoading ? (_jsxs(_Fragment, { children: [_jsx(Spinner, Object.assign({ diameter: "1em" }, spinnerProps)), ' ', _jsx("span", { className: "pf-chatbot__tool-call-title-text", children: loadingText })] })) : (_jsx("span", { className: "pf-chatbot__tool-call-title-text", children: titleText })) }));
|
|
5
10
|
const defaultActions = (_jsxs(_Fragment, { children: [_jsx(ActionListItem, Object.assign({}, actionListItemProps, cancelActionItemProps, { children: _jsx(Button, Object.assign({ variant: "link" }, cancelButtonProps, { children: cancelButtonText })) })), _jsx(ActionListItem, Object.assign({}, actionListItemProps, runActionItemProps, { children: _jsx(Button, Object.assign({ variant: "secondary" }, runButtonProps, { children: runButtonText })) }))] }));
|
|
6
11
|
const customActions = actions &&
|
|
7
12
|
actions.map((action, index) => (_jsx(ActionListItem, Object.assign({}, actionListItemProps, { children: action }), index)));
|
|
8
|
-
return (_jsxs(Card, Object.assign({ isCompact: true, className: "pf-chatbot__tool-call" }, cardProps, { children: [_jsx(CardBody, Object.assign({ className: "pf-chatbot__tool-call-title" }, cardBodyProps, { children: expandableContent && !isLoading ? (_jsx(ExpandableSection, Object.assign({ className: "pf-chatbot__tool-call-expandable-section", toggleContent: titleContent, isIndented: true }, expandableSectionProps, { children: expandableContent }))) : (titleContent) })), !isLoading && (_jsx(CardFooter, Object.assign({}, cardFooterProps, { children: _jsx(ActionList, Object.assign({ className: "pf-chatbot__tool-call-action-list" }, actionListProps, { children: _jsx(ActionListGroup, Object.assign({}, actionListGroupProps, { children: customActions || defaultActions })) })) })))] })));
|
|
13
|
+
return (_jsxs(Card, Object.assign({ isCompact: true, className: "pf-chatbot__tool-call" }, cardProps, { children: [_jsx(CardBody, Object.assign({ className: "pf-chatbot__tool-call-title" }, cardBodyProps, { children: expandableContent && !isLoading ? (_jsx(ExpandableSection, Object.assign({ className: "pf-chatbot__tool-call-expandable-section", toggleContent: titleContent, onToggle: onToggle, isExpanded: isExpanded, isIndented: true }, expandableSectionProps, { children: expandableContent }))) : (titleContent) })), !isLoading && (_jsx(CardFooter, Object.assign({}, cardFooterProps, { children: _jsx(ActionList, Object.assign({ className: "pf-chatbot__tool-call-action-list" }, actionListProps, { children: _jsx(ActionListGroup, Object.assign({}, actionListGroupProps, { children: customActions || defaultActions })) })) })))] })));
|
|
9
14
|
};
|
|
10
15
|
export default ToolCall;
|
|
@@ -136,4 +136,30 @@ describe('ToolCall', () => {
|
|
|
136
136
|
render(_jsx(ToolCall, Object.assign({}, defaultProps, { cardFooterProps: { id: 'card-footer-test-id' } })));
|
|
137
137
|
expect(screen.getByRole('button', { name: 'Run tool' }).closest('#card-footer-test-id')).toBeVisible();
|
|
138
138
|
});
|
|
139
|
+
it('Renders collapsed by default when expandableContent is provided', () => {
|
|
140
|
+
render(_jsx(ToolCall, Object.assign({}, defaultProps, { expandableContent: "Expandable Content" })));
|
|
141
|
+
expect(screen.getByRole('button', { name: defaultProps.titleText })).toHaveAttribute('aria-expanded', 'false');
|
|
142
|
+
expect(screen.queryByText('Expandable Content')).not.toBeVisible();
|
|
143
|
+
});
|
|
144
|
+
it('Renders expanded when isDefaultExpanded is true', () => {
|
|
145
|
+
render(_jsx(ToolCall, Object.assign({}, defaultProps, { isDefaultExpanded: true, expandableContent: "Expandable Content" })));
|
|
146
|
+
expect(screen.getByRole('button', { name: defaultProps.titleText })).toHaveAttribute('aria-expanded', 'true');
|
|
147
|
+
expect(screen.getByText('Expandable Content')).toBeVisible();
|
|
148
|
+
});
|
|
149
|
+
it('expandableSectionProps.isExpanded overrides isDefaultExpanded', () => {
|
|
150
|
+
render(_jsx(ToolCall, Object.assign({}, defaultProps, { isDefaultExpanded: false, expandableContent: "Expandable Content", expandableSectionProps: { isExpanded: true } })));
|
|
151
|
+
expect(screen.getByRole('button', { name: defaultProps.titleText })).toHaveAttribute('aria-expanded', 'true');
|
|
152
|
+
expect(screen.getByText('Expandable Content')).toBeVisible();
|
|
153
|
+
});
|
|
154
|
+
it('expandableSectionProps.onToggle overrides internal onToggle behavior', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
155
|
+
const user = userEvent.setup();
|
|
156
|
+
const customOnToggle = jest.fn();
|
|
157
|
+
render(_jsx(ToolCall, Object.assign({}, defaultProps, { isDefaultExpanded: false, expandableContent: "Expandable Content", expandableSectionProps: { onToggle: customOnToggle } })));
|
|
158
|
+
const toggleButton = screen.getByRole('button', { name: defaultProps.titleText });
|
|
159
|
+
expect(toggleButton).toHaveAttribute('aria-expanded', 'false');
|
|
160
|
+
yield user.click(toggleButton);
|
|
161
|
+
expect(customOnToggle).toHaveBeenCalledTimes(1);
|
|
162
|
+
expect(toggleButton).toHaveAttribute('aria-expanded', 'false');
|
|
163
|
+
expect(screen.queryByText('Expandable Content')).not.toBeVisible();
|
|
164
|
+
}));
|
|
139
165
|
});
|
|
@@ -3,6 +3,8 @@ import { type FunctionComponent } from 'react';
|
|
|
3
3
|
export interface ToolResponseProps {
|
|
4
4
|
/** Toggle content shown for expandable section */
|
|
5
5
|
toggleContent: React.ReactNode;
|
|
6
|
+
/** Flag indicating whether the expandable content is expanded by default. */
|
|
7
|
+
isDefaultExpanded?: boolean;
|
|
6
8
|
/** Additional props passed to expandable section */
|
|
7
9
|
expandableSectionProps?: Omit<ExpandableSectionProps, 'ref'>;
|
|
8
10
|
/** Subheading rendered inside expandable section */
|
|
@@ -4,8 +4,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
4
4
|
// ============================================================================
|
|
5
5
|
import { Card, CardBody, CardTitle, Divider, ExpandableSection } from '@patternfly/react-core';
|
|
6
6
|
import { useState } from 'react';
|
|
7
|
-
export const ToolResponse = ({ body, cardProps, expandableSectionProps, subheading, cardBody, cardTitle, cardBodyProps, toggleContent, toolResponseCardBodyProps, toolResponseCardDividerProps, toolResponseCardProps, toolResponseCardTitleProps }) => {
|
|
8
|
-
const [isExpanded, setIsExpanded] = useState(
|
|
7
|
+
export const ToolResponse = ({ body, cardProps, expandableSectionProps, subheading, cardBody, cardTitle, cardBodyProps, toggleContent, isDefaultExpanded = true, toolResponseCardBodyProps, toolResponseCardDividerProps, toolResponseCardProps, toolResponseCardTitleProps }) => {
|
|
8
|
+
const [isExpanded, setIsExpanded] = useState(isDefaultExpanded);
|
|
9
9
|
const onToggle = (_event, isExpanded) => {
|
|
10
10
|
setIsExpanded(isExpanded);
|
|
11
11
|
};
|
|
@@ -1,5 +1,15 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
1
10
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
11
|
import { render, screen } from '@testing-library/react';
|
|
12
|
+
import userEvent from '@testing-library/user-event';
|
|
3
13
|
import '@testing-library/jest-dom';
|
|
4
14
|
import ToolResponse from './ToolResponse';
|
|
5
15
|
describe('ToolResponse', () => {
|
|
@@ -76,4 +86,34 @@ describe('ToolResponse', () => {
|
|
|
76
86
|
const { container } = render(_jsx(ToolResponse, Object.assign({}, defaultProps, { cardBody: undefined })));
|
|
77
87
|
expect(container.querySelector('.pf-v6-c-divider')).toBeFalsy();
|
|
78
88
|
});
|
|
89
|
+
it('Renders expanded by default', () => {
|
|
90
|
+
render(_jsx(ToolResponse, Object.assign({}, defaultProps)));
|
|
91
|
+
expect(screen.getByRole('button', { name: defaultProps.toggleContent })).toHaveAttribute('aria-expanded', 'true');
|
|
92
|
+
expect(screen.getByText(defaultProps.cardTitle)).toBeVisible();
|
|
93
|
+
expect(screen.getByText(defaultProps.cardBody)).toBeVisible();
|
|
94
|
+
});
|
|
95
|
+
it('Renders collapsed when isDefaultExpanded is false', () => {
|
|
96
|
+
render(_jsx(ToolResponse, Object.assign({ isDefaultExpanded: false }, defaultProps)));
|
|
97
|
+
expect(screen.getByRole('button', { name: defaultProps.toggleContent })).toHaveAttribute('aria-expanded', 'false');
|
|
98
|
+
expect(screen.getByText(defaultProps.cardTitle)).not.toBeVisible();
|
|
99
|
+
expect(screen.getByText(defaultProps.cardBody)).not.toBeVisible();
|
|
100
|
+
});
|
|
101
|
+
it('expandableSectionProps.isExpanded overrides isDefaultExpanded', () => {
|
|
102
|
+
render(_jsx(ToolResponse, Object.assign({}, defaultProps, { isDefaultExpanded: false, expandableSectionProps: { isExpanded: true } })));
|
|
103
|
+
expect(screen.getByRole('button', { name: defaultProps.toggleContent })).toHaveAttribute('aria-expanded', 'true');
|
|
104
|
+
expect(screen.getByText(defaultProps.cardTitle)).toBeVisible();
|
|
105
|
+
expect(screen.getByText(defaultProps.cardBody)).toBeVisible();
|
|
106
|
+
});
|
|
107
|
+
it('expandableSectionProps.onToggle overrides internal onToggle behavior', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
108
|
+
const user = userEvent.setup();
|
|
109
|
+
const customOnToggle = jest.fn();
|
|
110
|
+
render(_jsx(ToolResponse, Object.assign({}, defaultProps, { isDefaultExpanded: false, expandableSectionProps: { onToggle: customOnToggle } })));
|
|
111
|
+
const toggleButton = screen.getByRole('button', { name: defaultProps.toggleContent });
|
|
112
|
+
expect(toggleButton).toHaveAttribute('aria-expanded', 'false');
|
|
113
|
+
yield user.click(toggleButton);
|
|
114
|
+
expect(customOnToggle).toHaveBeenCalledTimes(1);
|
|
115
|
+
expect(toggleButton).toHaveAttribute('aria-expanded', 'false');
|
|
116
|
+
expect(screen.getByText(defaultProps.cardTitle)).not.toBeVisible();
|
|
117
|
+
expect(screen.getByText(defaultProps.cardBody)).not.toBeVisible();
|
|
118
|
+
}));
|
|
79
119
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@patternfly/chatbot",
|
|
3
|
-
"version": "6.5.0-prerelease.
|
|
3
|
+
"version": "6.5.0-prerelease.22",
|
|
4
4
|
"description": "This library provides React components based on PatternFly 6 that can be used to build chatbots.",
|
|
5
5
|
"main": "dist/cjs/index.js",
|
|
6
6
|
"module": "dist/esm/index.js",
|
package/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithDeepThinking.tsx
CHANGED
|
@@ -3,15 +3,29 @@ import Message from '@patternfly/chatbot/dist/dynamic/Message';
|
|
|
3
3
|
import patternflyAvatar from './patternfly_avatar.jpg';
|
|
4
4
|
|
|
5
5
|
export const MessageWithDeepThinkingExample: FunctionComponent = () => (
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
6
|
+
<>
|
|
7
|
+
<Message
|
|
8
|
+
name="Bot"
|
|
9
|
+
role="bot"
|
|
10
|
+
avatar={patternflyAvatar}
|
|
11
|
+
content="This example has a body description that's within the recommended limit of 2 lines."
|
|
12
|
+
deepThinking={{
|
|
13
|
+
toggleContent: 'Show thinking',
|
|
14
|
+
subheading: 'Thought for 3 seconds',
|
|
15
|
+
body: "Here's why I said this."
|
|
16
|
+
}}
|
|
17
|
+
/>
|
|
18
|
+
<Message
|
|
19
|
+
name="Bot"
|
|
20
|
+
role="bot"
|
|
21
|
+
avatar={patternflyAvatar}
|
|
22
|
+
content="This example has deep thinking that is collapsed by default:"
|
|
23
|
+
deepThinking={{
|
|
24
|
+
isDefaultExpanded: false,
|
|
25
|
+
toggleContent: 'Show thinking',
|
|
26
|
+
subheading: 'Thought for 3 seconds',
|
|
27
|
+
body: "Here's why I said this."
|
|
28
|
+
}}
|
|
29
|
+
/>
|
|
30
|
+
</>
|
|
17
31
|
);
|
package/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithToolCall.tsx
CHANGED
|
@@ -31,7 +31,7 @@ export const MessageWithToolCallExample: FunctionComponent = () => {
|
|
|
31
31
|
name="Bot"
|
|
32
32
|
role="bot"
|
|
33
33
|
avatar={patternflyAvatar}
|
|
34
|
-
content="This example has an expandable tool call title, with an additional description
|
|
34
|
+
content="This example has an expandable tool call title, with an additional description:"
|
|
35
35
|
toolCall={{
|
|
36
36
|
titleText: "Calling 'awesome_tool_expansion'",
|
|
37
37
|
expandableContent: 'This is the expandable content for the tool call.',
|
|
@@ -39,6 +39,19 @@ export const MessageWithToolCallExample: FunctionComponent = () => {
|
|
|
39
39
|
loadingText: "Loading 'awesome_tool_expansion'"
|
|
40
40
|
}}
|
|
41
41
|
/>
|
|
42
|
+
<Message
|
|
43
|
+
name="Bot"
|
|
44
|
+
role="bot"
|
|
45
|
+
avatar={patternflyAvatar}
|
|
46
|
+
content="This example has an expandable tool call that is expanded by default:"
|
|
47
|
+
toolCall={{
|
|
48
|
+
isDefaultExpanded: true,
|
|
49
|
+
titleText: "Calling 'awesome_tool_expansion'",
|
|
50
|
+
expandableContent: 'This is the expandable content for the tool call.',
|
|
51
|
+
isLoading: toolCallsAreLoading,
|
|
52
|
+
loadingText: "Loading 'awesome_tool_expansion'"
|
|
53
|
+
}}
|
|
54
|
+
/>
|
|
42
55
|
</FlexItem>
|
|
43
56
|
</Flex>
|
|
44
57
|
);
|
package/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithToolResponse.tsx
CHANGED
|
@@ -23,113 +23,230 @@ export const MessageWithToolResponseExample: FunctionComponent = () => {
|
|
|
23
23
|
};
|
|
24
24
|
|
|
25
25
|
return (
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
<
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
<Flex gap={{ default: 'gapXs' }}>
|
|
41
|
-
<FlexItem>
|
|
42
|
-
<WrenchIcon style={{ color: 'var(--pf-t--global--icon--color--brand--default' }} />
|
|
43
|
-
</FlexItem>
|
|
44
|
-
<FlexItem>toolName</FlexItem>
|
|
45
|
-
</Flex>
|
|
46
|
-
</FlexItem>
|
|
47
|
-
<FlexItem>
|
|
48
|
-
<Flex gap={{ default: 'gapSm' }} style={{ fontSize: '12px', fontWeight: '400' }}>
|
|
49
|
-
<FlexItem>Execution time:</FlexItem>
|
|
50
|
-
<FlexItem>0.12 seconds</FlexItem>
|
|
51
|
-
</Flex>
|
|
52
|
-
</FlexItem>
|
|
53
|
-
</Flex>
|
|
54
|
-
</FlexItem>
|
|
55
|
-
<FlexItem>
|
|
56
|
-
<Button
|
|
57
|
-
variant="plain"
|
|
58
|
-
aria-label="Copy tool response to clipboard"
|
|
59
|
-
icon={<CopyIcon style={{ color: 'var(--pf-t--global--icon--color--subtle)' }} />}
|
|
60
|
-
></Button>
|
|
61
|
-
</FlexItem>
|
|
62
|
-
</Flex>
|
|
63
|
-
),
|
|
64
|
-
cardBody: (
|
|
65
|
-
<>
|
|
66
|
-
<DescriptionList
|
|
67
|
-
style={{ '--pf-v6-c-description-list--RowGap': 'var(--pf-t--global--spacer--md)' } as any}
|
|
68
|
-
aria-label="Tool response"
|
|
26
|
+
<>
|
|
27
|
+
<Message
|
|
28
|
+
name="Bot"
|
|
29
|
+
role="bot"
|
|
30
|
+
avatar={patternflyAvatar}
|
|
31
|
+
content="This message has a body description that's within the recommended limit of 2 lines:"
|
|
32
|
+
toolResponse={{
|
|
33
|
+
toggleContent: 'Tool response: toolName',
|
|
34
|
+
subheading: 'Thought for 3 seconds',
|
|
35
|
+
body: "Here's the summary for your toolName response:",
|
|
36
|
+
cardTitle: (
|
|
37
|
+
<Flex
|
|
38
|
+
alignItems={{ default: 'alignItemsCenter' }}
|
|
39
|
+
justifyContent={{ default: 'justifyContentSpaceBetween' }}
|
|
69
40
|
>
|
|
70
|
-
<
|
|
71
|
-
|
|
41
|
+
<FlexItem>
|
|
42
|
+
<Flex direction={{ default: 'column' }} gap={{ default: 'gapXs' }}>
|
|
43
|
+
<FlexItem grow={{ default: 'grow' }}>
|
|
44
|
+
<Flex gap={{ default: 'gapXs' }}>
|
|
45
|
+
<FlexItem>
|
|
46
|
+
<WrenchIcon style={{ color: 'var(--pf-t--global--icon--color--brand--default' }} />
|
|
47
|
+
</FlexItem>
|
|
48
|
+
<FlexItem>toolName</FlexItem>
|
|
49
|
+
</Flex>
|
|
50
|
+
</FlexItem>
|
|
51
|
+
<FlexItem>
|
|
52
|
+
<Flex gap={{ default: 'gapSm' }} style={{ fontSize: '12px', fontWeight: '400' }}>
|
|
53
|
+
<FlexItem>Execution time:</FlexItem>
|
|
54
|
+
<FlexItem>0.12 seconds</FlexItem>
|
|
55
|
+
</Flex>
|
|
56
|
+
</FlexItem>
|
|
57
|
+
</Flex>
|
|
58
|
+
</FlexItem>
|
|
59
|
+
<FlexItem>
|
|
60
|
+
<Button
|
|
61
|
+
variant="plain"
|
|
62
|
+
aria-label="Copy tool response to clipboard"
|
|
63
|
+
icon={<CopyIcon style={{ color: 'var(--pf-t--global--icon--color--subtle)' }} />}
|
|
64
|
+
></Button>
|
|
65
|
+
</FlexItem>
|
|
66
|
+
</Flex>
|
|
67
|
+
),
|
|
68
|
+
cardBody: (
|
|
69
|
+
<>
|
|
70
|
+
<DescriptionList
|
|
71
|
+
style={{ '--pf-v6-c-description-list--RowGap': 'var(--pf-t--global--spacer--md)' } as any}
|
|
72
|
+
aria-label="Tool response"
|
|
72
73
|
>
|
|
73
|
-
<
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
74
|
+
<DescriptionListGroup
|
|
75
|
+
style={{ '--pf-v6-c-description-list__group--RowGap': 'var(--pf-t--global--spacer--xs)' } as any}
|
|
76
|
+
>
|
|
77
|
+
<DescriptionListTerm>Parameters</DescriptionListTerm>
|
|
78
|
+
<DescriptionListDescription>
|
|
79
|
+
<Flex direction={{ default: 'column' }}>
|
|
80
|
+
<FlexItem>Optional description text for parameters.</FlexItem>
|
|
81
|
+
<FlexItem>
|
|
82
|
+
<Flex gap={{ default: 'gapSm' }}>
|
|
83
|
+
<FlexItem>
|
|
84
|
+
<Label variant="outline" color="blue">
|
|
85
|
+
type
|
|
86
|
+
</Label>
|
|
87
|
+
</FlexItem>
|
|
88
|
+
<FlexItem>
|
|
89
|
+
<Label variant="outline" color="blue">
|
|
90
|
+
properties
|
|
91
|
+
</Label>
|
|
92
|
+
</FlexItem>
|
|
93
|
+
<FlexItem>
|
|
94
|
+
<Label variant="outline" color="blue">
|
|
95
|
+
label
|
|
96
|
+
</Label>
|
|
97
|
+
</FlexItem>
|
|
98
|
+
<FlexItem>
|
|
99
|
+
<Label variant="outline" color="blue">
|
|
100
|
+
label
|
|
101
|
+
</Label>
|
|
102
|
+
</FlexItem>
|
|
103
|
+
</Flex>
|
|
104
|
+
</FlexItem>
|
|
105
|
+
</Flex>
|
|
106
|
+
</DescriptionListDescription>
|
|
107
|
+
</DescriptionListGroup>
|
|
108
|
+
<DescriptionListGroup
|
|
109
|
+
style={{ '--pf-v6-c-description-list__group--RowGap': 'var(--pf-t--global--spacer--xs)' } as any}
|
|
110
|
+
>
|
|
111
|
+
<DescriptionListTerm>Response</DescriptionListTerm>
|
|
112
|
+
<DescriptionListDescription>
|
|
113
|
+
<ExpandableSection
|
|
114
|
+
variant={ExpandableSectionVariant.truncate}
|
|
115
|
+
toggleTextExpanded="show less of response"
|
|
116
|
+
toggleTextCollapsed="show more of response"
|
|
117
|
+
onToggle={onToggle}
|
|
118
|
+
isExpanded={isExpanded}
|
|
119
|
+
style={
|
|
120
|
+
{
|
|
121
|
+
'--pf-v6-c-expandable-section__content--Opacity': '1',
|
|
122
|
+
'--pf-v6-c-expandable-section__content--PaddingInlineStart': 0,
|
|
123
|
+
'--pf-v6-c-expandable-section__content--TranslateY': 0,
|
|
124
|
+
'--pf-v6-c-expandable-section--m-expand-top__content--TranslateY': 0
|
|
125
|
+
} as any
|
|
126
|
+
}
|
|
127
|
+
>
|
|
128
|
+
Descriptive text about the tool response, including completion status, details on the data that
|
|
129
|
+
was processed, or anything else relevant to the use case.
|
|
130
|
+
</ExpandableSection>
|
|
131
|
+
</DescriptionListDescription>
|
|
132
|
+
</DescriptionListGroup>
|
|
133
|
+
</DescriptionList>
|
|
134
|
+
</>
|
|
135
|
+
)
|
|
136
|
+
}}
|
|
137
|
+
/>
|
|
138
|
+
<Message
|
|
139
|
+
name="Bot"
|
|
140
|
+
role="bot"
|
|
141
|
+
avatar={patternflyAvatar}
|
|
142
|
+
content="This message has a tool response that is collapsed by default:"
|
|
143
|
+
toolResponse={{
|
|
144
|
+
isDefaultExpanded: false,
|
|
145
|
+
toggleContent: 'Tool response: toolName',
|
|
146
|
+
subheading: 'Thought for 3 seconds',
|
|
147
|
+
body: "Here's the summary for your toolName response:",
|
|
148
|
+
cardTitle: (
|
|
149
|
+
<Flex
|
|
150
|
+
alignItems={{ default: 'alignItemsCenter' }}
|
|
151
|
+
justifyContent={{ default: 'justifyContentSpaceBetween' }}
|
|
152
|
+
>
|
|
153
|
+
<FlexItem>
|
|
154
|
+
<Flex direction={{ default: 'column' }} gap={{ default: 'gapXs' }}>
|
|
155
|
+
<FlexItem grow={{ default: 'grow' }}>
|
|
156
|
+
<Flex gap={{ default: 'gapXs' }}>
|
|
157
|
+
<FlexItem>
|
|
158
|
+
<WrenchIcon style={{ color: 'var(--pf-t--global--icon--color--brand--default' }} />
|
|
159
|
+
</FlexItem>
|
|
160
|
+
<FlexItem>toolName</FlexItem>
|
|
161
|
+
</Flex>
|
|
162
|
+
</FlexItem>
|
|
163
|
+
<FlexItem>
|
|
164
|
+
<Flex gap={{ default: 'gapSm' }} style={{ fontSize: '12px', fontWeight: '400' }}>
|
|
165
|
+
<FlexItem>Execution time:</FlexItem>
|
|
166
|
+
<FlexItem>0.12 seconds</FlexItem>
|
|
167
|
+
</Flex>
|
|
168
|
+
</FlexItem>
|
|
169
|
+
</Flex>
|
|
170
|
+
</FlexItem>
|
|
171
|
+
<FlexItem>
|
|
172
|
+
<Button
|
|
173
|
+
variant="plain"
|
|
174
|
+
aria-label="Copy tool response to clipboard"
|
|
175
|
+
icon={<CopyIcon style={{ color: 'var(--pf-t--global--icon--color--subtle)' }} />}
|
|
176
|
+
></Button>
|
|
177
|
+
</FlexItem>
|
|
178
|
+
</Flex>
|
|
179
|
+
),
|
|
180
|
+
cardBody: (
|
|
181
|
+
<>
|
|
182
|
+
<DescriptionList
|
|
183
|
+
style={{ '--pf-v6-c-description-list--RowGap': 'var(--pf-t--global--spacer--md)' } as any}
|
|
184
|
+
aria-label="Tool response"
|
|
106
185
|
>
|
|
107
|
-
<
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
186
|
+
<DescriptionListGroup
|
|
187
|
+
style={{ '--pf-v6-c-description-list__group--RowGap': 'var(--pf-t--global--spacer--xs)' } as any}
|
|
188
|
+
>
|
|
189
|
+
<DescriptionListTerm>Parameters</DescriptionListTerm>
|
|
190
|
+
<DescriptionListDescription>
|
|
191
|
+
<Flex direction={{ default: 'column' }}>
|
|
192
|
+
<FlexItem>Optional description text for parameters.</FlexItem>
|
|
193
|
+
<FlexItem>
|
|
194
|
+
<Flex gap={{ default: 'gapSm' }}>
|
|
195
|
+
<FlexItem>
|
|
196
|
+
<Label variant="outline" color="blue">
|
|
197
|
+
type
|
|
198
|
+
</Label>
|
|
199
|
+
</FlexItem>
|
|
200
|
+
<FlexItem>
|
|
201
|
+
<Label variant="outline" color="blue">
|
|
202
|
+
properties
|
|
203
|
+
</Label>
|
|
204
|
+
</FlexItem>
|
|
205
|
+
<FlexItem>
|
|
206
|
+
<Label variant="outline" color="blue">
|
|
207
|
+
label
|
|
208
|
+
</Label>
|
|
209
|
+
</FlexItem>
|
|
210
|
+
<FlexItem>
|
|
211
|
+
<Label variant="outline" color="blue">
|
|
212
|
+
label
|
|
213
|
+
</Label>
|
|
214
|
+
</FlexItem>
|
|
215
|
+
</Flex>
|
|
216
|
+
</FlexItem>
|
|
217
|
+
</Flex>
|
|
218
|
+
</DescriptionListDescription>
|
|
219
|
+
</DescriptionListGroup>
|
|
220
|
+
<DescriptionListGroup
|
|
221
|
+
style={{ '--pf-v6-c-description-list__group--RowGap': 'var(--pf-t--global--spacer--xs)' } as any}
|
|
222
|
+
>
|
|
223
|
+
<DescriptionListTerm>Response</DescriptionListTerm>
|
|
224
|
+
<DescriptionListDescription>
|
|
225
|
+
<ExpandableSection
|
|
226
|
+
variant={ExpandableSectionVariant.truncate}
|
|
227
|
+
toggleTextExpanded="show less of response"
|
|
228
|
+
toggleTextCollapsed="show more of response"
|
|
229
|
+
onToggle={onToggle}
|
|
230
|
+
isExpanded={isExpanded}
|
|
231
|
+
style={
|
|
232
|
+
{
|
|
233
|
+
'--pf-v6-c-expandable-section__content--Opacity': '1',
|
|
234
|
+
'--pf-v6-c-expandable-section__content--PaddingInlineStart': 0,
|
|
235
|
+
'--pf-v6-c-expandable-section__content--TranslateY': 0,
|
|
236
|
+
'--pf-v6-c-expandable-section--m-expand-top__content--TranslateY': 0
|
|
237
|
+
} as any
|
|
238
|
+
}
|
|
239
|
+
>
|
|
240
|
+
Descriptive text about the tool response, including completion status, details on the data that
|
|
241
|
+
was processed, or anything else relevant to the use case.
|
|
242
|
+
</ExpandableSection>
|
|
243
|
+
</DescriptionListDescription>
|
|
244
|
+
</DescriptionListGroup>
|
|
245
|
+
</DescriptionList>
|
|
246
|
+
</>
|
|
247
|
+
)
|
|
248
|
+
}}
|
|
249
|
+
/>
|
|
250
|
+
</>
|
|
134
251
|
);
|
|
135
252
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
2
3
|
import '@testing-library/jest-dom';
|
|
3
4
|
import DeepThinking from './DeepThinking';
|
|
4
5
|
|
|
@@ -58,4 +59,64 @@ describe('DeepThinking', () => {
|
|
|
58
59
|
const subheadingContainer = container.querySelector('.pf-chatbot__tool-response-subheading');
|
|
59
60
|
expect(subheadingContainer).toBeFalsy();
|
|
60
61
|
});
|
|
62
|
+
|
|
63
|
+
it('should pass through cardBodyProps', () => {
|
|
64
|
+
render(
|
|
65
|
+
<DeepThinking {...defaultProps} body="Thinking content" cardBodyProps={{ className: 'custom-card-body-class' }} />
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const cardBody = screen.getByText('Thinking content').closest('.pf-v6-c-card__body');
|
|
69
|
+
expect(cardBody).toHaveClass('custom-card-body-class');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('Renders expanded by default', () => {
|
|
73
|
+
render(<DeepThinking {...defaultProps} body="Thinking content" />);
|
|
74
|
+
|
|
75
|
+
expect(screen.getByRole('button', { name: defaultProps.toggleContent })).toHaveAttribute('aria-expanded', 'true');
|
|
76
|
+
expect(screen.getByText('Thinking content')).toBeVisible();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('Renders collapsed when isDefaultExpanded is false', () => {
|
|
80
|
+
render(<DeepThinking isDefaultExpanded={false} {...defaultProps} body="Thinking content" />);
|
|
81
|
+
|
|
82
|
+
expect(screen.getByRole('button', { name: defaultProps.toggleContent })).toHaveAttribute('aria-expanded', 'false');
|
|
83
|
+
expect(screen.getByText('Thinking content')).not.toBeVisible();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('expandableSectionProps.isExpanded overrides isDefaultExpanded', () => {
|
|
87
|
+
render(
|
|
88
|
+
<DeepThinking
|
|
89
|
+
{...defaultProps}
|
|
90
|
+
isDefaultExpanded={false}
|
|
91
|
+
body="Thinking content"
|
|
92
|
+
expandableSectionProps={{ isExpanded: true }}
|
|
93
|
+
/>
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
expect(screen.getByRole('button', { name: defaultProps.toggleContent })).toHaveAttribute('aria-expanded', 'true');
|
|
97
|
+
expect(screen.getByText('Thinking content')).toBeVisible();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('expandableSectionProps.onToggle overrides internal onToggle behavior', async () => {
|
|
101
|
+
const user = userEvent.setup();
|
|
102
|
+
const customOnToggle = jest.fn();
|
|
103
|
+
|
|
104
|
+
render(
|
|
105
|
+
<DeepThinking
|
|
106
|
+
{...defaultProps}
|
|
107
|
+
isDefaultExpanded={false}
|
|
108
|
+
body="Thinking content"
|
|
109
|
+
expandableSectionProps={{ onToggle: customOnToggle }}
|
|
110
|
+
/>
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
const toggleButton = screen.getByRole('button', { name: defaultProps.toggleContent });
|
|
114
|
+
expect(toggleButton).toHaveAttribute('aria-expanded', 'false');
|
|
115
|
+
|
|
116
|
+
await user.click(toggleButton);
|
|
117
|
+
|
|
118
|
+
expect(customOnToggle).toHaveBeenCalled();
|
|
119
|
+
expect(toggleButton).toHaveAttribute('aria-expanded', 'false');
|
|
120
|
+
expect(screen.getByText('Thinking content')).not.toBeVisible();
|
|
121
|
+
});
|
|
61
122
|
});
|
|
@@ -14,6 +14,8 @@ import { useState, type FunctionComponent } from 'react';
|
|
|
14
14
|
export interface DeepThinkingProps {
|
|
15
15
|
/** Toggle content shown for expandable section */
|
|
16
16
|
toggleContent: React.ReactNode;
|
|
17
|
+
/** Flag indicating whether the expandable content is expanded by default. */
|
|
18
|
+
isDefaultExpanded?: boolean;
|
|
17
19
|
/** Additional props passed to expandable section */
|
|
18
20
|
expandableSectionProps?: Omit<ExpandableSectionProps, 'ref'>;
|
|
19
21
|
/** Subheading rendered inside expandable section */
|
|
@@ -32,9 +34,10 @@ export const DeepThinking: FunctionComponent<DeepThinkingProps> = ({
|
|
|
32
34
|
expandableSectionProps,
|
|
33
35
|
subheading,
|
|
34
36
|
toggleContent,
|
|
37
|
+
isDefaultExpanded = true,
|
|
35
38
|
cardBodyProps
|
|
36
39
|
}: DeepThinkingProps) => {
|
|
37
|
-
const [isExpanded, setIsExpanded] = useState(
|
|
40
|
+
const [isExpanded, setIsExpanded] = useState(isDefaultExpanded);
|
|
38
41
|
|
|
39
42
|
const onToggle = (_event: React.MouseEvent, isExpanded: boolean) => {
|
|
40
43
|
setIsExpanded(isExpanded);
|
|
@@ -181,4 +181,55 @@ describe('ToolCall', () => {
|
|
|
181
181
|
render(<ToolCall {...defaultProps} cardFooterProps={{ id: 'card-footer-test-id' }} />);
|
|
182
182
|
expect(screen.getByRole('button', { name: 'Run tool' }).closest('#card-footer-test-id')).toBeVisible();
|
|
183
183
|
});
|
|
184
|
+
|
|
185
|
+
it('Renders collapsed by default when expandableContent is provided', () => {
|
|
186
|
+
render(<ToolCall {...defaultProps} expandableContent="Expandable Content" />);
|
|
187
|
+
|
|
188
|
+
expect(screen.getByRole('button', { name: defaultProps.titleText })).toHaveAttribute('aria-expanded', 'false');
|
|
189
|
+
expect(screen.queryByText('Expandable Content')).not.toBeVisible();
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('Renders expanded when isDefaultExpanded is true', () => {
|
|
193
|
+
render(<ToolCall {...defaultProps} isDefaultExpanded expandableContent="Expandable Content" />);
|
|
194
|
+
|
|
195
|
+
expect(screen.getByRole('button', { name: defaultProps.titleText })).toHaveAttribute('aria-expanded', 'true');
|
|
196
|
+
expect(screen.getByText('Expandable Content')).toBeVisible();
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('expandableSectionProps.isExpanded overrides isDefaultExpanded', () => {
|
|
200
|
+
render(
|
|
201
|
+
<ToolCall
|
|
202
|
+
{...defaultProps}
|
|
203
|
+
isDefaultExpanded={false}
|
|
204
|
+
expandableContent="Expandable Content"
|
|
205
|
+
expandableSectionProps={{ isExpanded: true }}
|
|
206
|
+
/>
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
expect(screen.getByRole('button', { name: defaultProps.titleText })).toHaveAttribute('aria-expanded', 'true');
|
|
210
|
+
expect(screen.getByText('Expandable Content')).toBeVisible();
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('expandableSectionProps.onToggle overrides internal onToggle behavior', async () => {
|
|
214
|
+
const user = userEvent.setup();
|
|
215
|
+
const customOnToggle = jest.fn();
|
|
216
|
+
|
|
217
|
+
render(
|
|
218
|
+
<ToolCall
|
|
219
|
+
{...defaultProps}
|
|
220
|
+
isDefaultExpanded={false}
|
|
221
|
+
expandableContent="Expandable Content"
|
|
222
|
+
expandableSectionProps={{ onToggle: customOnToggle }}
|
|
223
|
+
/>
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
const toggleButton = screen.getByRole('button', { name: defaultProps.titleText });
|
|
227
|
+
expect(toggleButton).toHaveAttribute('aria-expanded', 'false');
|
|
228
|
+
|
|
229
|
+
await user.click(toggleButton);
|
|
230
|
+
|
|
231
|
+
expect(customOnToggle).toHaveBeenCalledTimes(1);
|
|
232
|
+
expect(toggleButton).toHaveAttribute('aria-expanded', 'false');
|
|
233
|
+
expect(screen.queryByText('Expandable Content')).not.toBeVisible();
|
|
234
|
+
});
|
|
184
235
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type FunctionComponent } from 'react';
|
|
1
|
+
import { useState, type FunctionComponent } from 'react';
|
|
2
2
|
import {
|
|
3
3
|
ActionList,
|
|
4
4
|
ActionListProps,
|
|
@@ -31,6 +31,8 @@ export interface ToolCallProps {
|
|
|
31
31
|
spinnerProps?: SpinnerProps;
|
|
32
32
|
/** Content to render within an expandable section. */
|
|
33
33
|
expandableContent?: React.ReactNode;
|
|
34
|
+
/** Flag indicating whether the expandable content is expanded by default. */
|
|
35
|
+
isDefaultExpanded?: boolean;
|
|
34
36
|
/** Text content for the "run" action button. */
|
|
35
37
|
runButtonText?: string;
|
|
36
38
|
/** Additional props for the "run" action button. */
|
|
@@ -66,6 +68,7 @@ export const ToolCall: FunctionComponent<ToolCallProps> = ({
|
|
|
66
68
|
loadingText,
|
|
67
69
|
isLoading,
|
|
68
70
|
expandableContent,
|
|
71
|
+
isDefaultExpanded = false,
|
|
69
72
|
runButtonText = 'Run tool',
|
|
70
73
|
runButtonProps,
|
|
71
74
|
runActionItemProps,
|
|
@@ -82,6 +85,12 @@ export const ToolCall: FunctionComponent<ToolCallProps> = ({
|
|
|
82
85
|
expandableSectionProps,
|
|
83
86
|
spinnerProps
|
|
84
87
|
}: ToolCallProps) => {
|
|
88
|
+
const [isExpanded, setIsExpanded] = useState(isDefaultExpanded);
|
|
89
|
+
|
|
90
|
+
const onToggle = (_event: React.MouseEvent, isExpanded: boolean) => {
|
|
91
|
+
setIsExpanded(isExpanded);
|
|
92
|
+
};
|
|
93
|
+
|
|
85
94
|
const titleContent = (
|
|
86
95
|
<span className={`pf-chatbot__tool-call-title-content`}>
|
|
87
96
|
{isLoading ? (
|
|
@@ -124,6 +133,8 @@ export const ToolCall: FunctionComponent<ToolCallProps> = ({
|
|
|
124
133
|
<ExpandableSection
|
|
125
134
|
className="pf-chatbot__tool-call-expandable-section"
|
|
126
135
|
toggleContent={titleContent}
|
|
136
|
+
onToggle={onToggle}
|
|
137
|
+
isExpanded={isExpanded}
|
|
127
138
|
isIndented
|
|
128
139
|
{...expandableSectionProps}
|
|
129
140
|
>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
2
3
|
import '@testing-library/jest-dom';
|
|
3
4
|
import ToolResponse from './ToolResponse';
|
|
4
5
|
|
|
@@ -105,4 +106,47 @@ describe('ToolResponse', () => {
|
|
|
105
106
|
const { container } = render(<ToolResponse {...defaultProps} cardBody={undefined} />);
|
|
106
107
|
expect(container.querySelector('.pf-v6-c-divider')).toBeFalsy();
|
|
107
108
|
});
|
|
109
|
+
|
|
110
|
+
it('Renders expanded by default', () => {
|
|
111
|
+
render(<ToolResponse {...defaultProps} />);
|
|
112
|
+
|
|
113
|
+
expect(screen.getByRole('button', { name: defaultProps.toggleContent })).toHaveAttribute('aria-expanded', 'true');
|
|
114
|
+
expect(screen.getByText(defaultProps.cardTitle)).toBeVisible();
|
|
115
|
+
expect(screen.getByText(defaultProps.cardBody)).toBeVisible();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('Renders collapsed when isDefaultExpanded is false', () => {
|
|
119
|
+
render(<ToolResponse isDefaultExpanded={false} {...defaultProps} />);
|
|
120
|
+
|
|
121
|
+
expect(screen.getByRole('button', { name: defaultProps.toggleContent })).toHaveAttribute('aria-expanded', 'false');
|
|
122
|
+
expect(screen.getByText(defaultProps.cardTitle)).not.toBeVisible();
|
|
123
|
+
expect(screen.getByText(defaultProps.cardBody)).not.toBeVisible();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('expandableSectionProps.isExpanded overrides isDefaultExpanded', () => {
|
|
127
|
+
render(<ToolResponse {...defaultProps} isDefaultExpanded={false} expandableSectionProps={{ isExpanded: true }} />);
|
|
128
|
+
|
|
129
|
+
expect(screen.getByRole('button', { name: defaultProps.toggleContent })).toHaveAttribute('aria-expanded', 'true');
|
|
130
|
+
expect(screen.getByText(defaultProps.cardTitle)).toBeVisible();
|
|
131
|
+
expect(screen.getByText(defaultProps.cardBody)).toBeVisible();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('expandableSectionProps.onToggle overrides internal onToggle behavior', async () => {
|
|
135
|
+
const user = userEvent.setup();
|
|
136
|
+
const customOnToggle = jest.fn();
|
|
137
|
+
|
|
138
|
+
render(
|
|
139
|
+
<ToolResponse {...defaultProps} isDefaultExpanded={false} expandableSectionProps={{ onToggle: customOnToggle }} />
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
const toggleButton = screen.getByRole('button', { name: defaultProps.toggleContent });
|
|
143
|
+
expect(toggleButton).toHaveAttribute('aria-expanded', 'false');
|
|
144
|
+
|
|
145
|
+
await user.click(toggleButton);
|
|
146
|
+
|
|
147
|
+
expect(customOnToggle).toHaveBeenCalledTimes(1);
|
|
148
|
+
expect(toggleButton).toHaveAttribute('aria-expanded', 'false');
|
|
149
|
+
expect(screen.getByText(defaultProps.cardTitle)).not.toBeVisible();
|
|
150
|
+
expect(screen.getByText(defaultProps.cardBody)).not.toBeVisible();
|
|
151
|
+
});
|
|
108
152
|
});
|
|
@@ -18,6 +18,8 @@ import { useState, type FunctionComponent } from 'react';
|
|
|
18
18
|
export interface ToolResponseProps {
|
|
19
19
|
/** Toggle content shown for expandable section */
|
|
20
20
|
toggleContent: React.ReactNode;
|
|
21
|
+
/** Flag indicating whether the expandable content is expanded by default. */
|
|
22
|
+
isDefaultExpanded?: boolean;
|
|
21
23
|
/** Additional props passed to expandable section */
|
|
22
24
|
expandableSectionProps?: Omit<ExpandableSectionProps, 'ref'>;
|
|
23
25
|
/** Subheading rendered inside expandable section */
|
|
@@ -51,12 +53,13 @@ export const ToolResponse: FunctionComponent<ToolResponseProps> = ({
|
|
|
51
53
|
cardTitle,
|
|
52
54
|
cardBodyProps,
|
|
53
55
|
toggleContent,
|
|
56
|
+
isDefaultExpanded = true,
|
|
54
57
|
toolResponseCardBodyProps,
|
|
55
58
|
toolResponseCardDividerProps,
|
|
56
59
|
toolResponseCardProps,
|
|
57
60
|
toolResponseCardTitleProps
|
|
58
61
|
}: ToolResponseProps) => {
|
|
59
|
-
const [isExpanded, setIsExpanded] = useState(
|
|
62
|
+
const [isExpanded, setIsExpanded] = useState(isDefaultExpanded);
|
|
60
63
|
|
|
61
64
|
const onToggle = (_event: React.MouseEvent, isExpanded: boolean) => {
|
|
62
65
|
setIsExpanded(isExpanded);
|