@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.
Files changed (28) hide show
  1. package/dist/cjs/DeepThinking/DeepThinking.d.ts +2 -0
  2. package/dist/cjs/DeepThinking/DeepThinking.js +2 -2
  3. package/dist/cjs/DeepThinking/DeepThinking.test.js +41 -0
  4. package/dist/cjs/ToolCall/ToolCall.d.ts +2 -0
  5. package/dist/cjs/ToolCall/ToolCall.js +7 -2
  6. package/dist/cjs/ToolCall/ToolCall.test.js +26 -0
  7. package/dist/cjs/ToolResponse/ToolResponse.d.ts +2 -0
  8. package/dist/cjs/ToolResponse/ToolResponse.js +2 -2
  9. package/dist/cjs/ToolResponse/ToolResponse.test.js +40 -0
  10. package/dist/esm/DeepThinking/DeepThinking.d.ts +2 -0
  11. package/dist/esm/DeepThinking/DeepThinking.js +2 -2
  12. package/dist/esm/DeepThinking/DeepThinking.test.js +41 -0
  13. package/dist/esm/ToolCall/ToolCall.d.ts +2 -0
  14. package/dist/esm/ToolCall/ToolCall.js +7 -2
  15. package/dist/esm/ToolCall/ToolCall.test.js +26 -0
  16. package/dist/esm/ToolResponse/ToolResponse.d.ts +2 -0
  17. package/dist/esm/ToolResponse/ToolResponse.js +2 -2
  18. package/dist/esm/ToolResponse/ToolResponse.test.js +40 -0
  19. package/package.json +1 -1
  20. package/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithDeepThinking.tsx +25 -11
  21. package/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithToolCall.tsx +14 -1
  22. package/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithToolResponse.tsx +222 -105
  23. package/src/DeepThinking/DeepThinking.test.tsx +61 -0
  24. package/src/DeepThinking/DeepThinking.tsx +4 -1
  25. package/src/ToolCall/ToolCall.test.tsx +51 -0
  26. package/src/ToolCall/ToolCall.tsx +12 -1
  27. package/src/ToolResponse/ToolResponse.test.tsx +44 -0
  28. 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)(true);
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)(true);
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(true);
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(true);
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.21",
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",
@@ -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
- <Message
7
- name="Bot"
8
- role="bot"
9
- avatar={patternflyAvatar}
10
- content="This example has a body description that's within the recommended limit of 2 lines."
11
- deepThinking={{
12
- toggleContent: 'Show thinking',
13
- subheading: 'Thought for 3 seconds',
14
- body: "Here's why I said this."
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
  );
@@ -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
  );
@@ -23,113 +23,230 @@ export const MessageWithToolResponseExample: FunctionComponent = () => {
23
23
  };
24
24
 
25
25
  return (
26
- <Message
27
- name="Bot"
28
- role="bot"
29
- avatar={patternflyAvatar}
30
- content="This example has a body description that's within the recommended limit of 2 lines:"
31
- toolResponse={{
32
- toggleContent: 'Tool response: toolName',
33
- subheading: 'Thought for 3 seconds',
34
- body: "Here's the summary for your toolName response:",
35
- cardTitle: (
36
- <Flex alignItems={{ default: 'alignItemsCenter' }} justifyContent={{ default: 'justifyContentSpaceBetween' }}>
37
- <FlexItem>
38
- <Flex direction={{ default: 'column' }} gap={{ default: 'gapXs' }}>
39
- <FlexItem grow={{ default: 'grow' }}>
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
- <DescriptionListGroup
71
- style={{ '--pf-v6-c-description-list__group--RowGap': 'var(--pf-t--global--spacer--xs)' } as any}
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
- <DescriptionListTerm>Parameters</DescriptionListTerm>
74
- <DescriptionListDescription>
75
- <Flex direction={{ default: 'column' }}>
76
- <FlexItem>Optional description text for parameters.</FlexItem>
77
- <FlexItem>
78
- <Flex gap={{ default: 'gapSm' }}>
79
- <FlexItem>
80
- <Label variant="outline" color="blue">
81
- type
82
- </Label>
83
- </FlexItem>
84
- <FlexItem>
85
- <Label variant="outline" color="blue">
86
- properties
87
- </Label>
88
- </FlexItem>
89
- <FlexItem>
90
- <Label variant="outline" color="blue">
91
- label
92
- </Label>
93
- </FlexItem>
94
- <FlexItem>
95
- <Label variant="outline" color="blue">
96
- label
97
- </Label>
98
- </FlexItem>
99
- </Flex>
100
- </FlexItem>
101
- </Flex>
102
- </DescriptionListDescription>
103
- </DescriptionListGroup>
104
- <DescriptionListGroup
105
- style={{ '--pf-v6-c-description-list__group--RowGap': 'var(--pf-t--global--spacer--xs)' } as any}
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
- <DescriptionListTerm>Response</DescriptionListTerm>
108
- <DescriptionListDescription>
109
- <ExpandableSection
110
- variant={ExpandableSectionVariant.truncate}
111
- toggleTextExpanded="show less of response"
112
- toggleTextCollapsed="show more of response"
113
- onToggle={onToggle}
114
- isExpanded={isExpanded}
115
- style={
116
- {
117
- '--pf-v6-c-expandable-section__content--Opacity': '1',
118
- '--pf-v6-c-expandable-section__content--PaddingInlineStart': 0,
119
- '--pf-v6-c-expandable-section__content--TranslateY': 0,
120
- '--pf-v6-c-expandable-section--m-expand-top__content--TranslateY': 0
121
- } as any
122
- }
123
- >
124
- Descriptive text about the tool response, including completion status, details on the data that was
125
- processed, or anything else relevant to the use case.
126
- </ExpandableSection>
127
- </DescriptionListDescription>
128
- </DescriptionListGroup>
129
- </DescriptionList>
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(true);
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(true);
62
+ const [isExpanded, setIsExpanded] = useState(isDefaultExpanded);
60
63
 
61
64
  const onToggle = (_event: React.MouseEvent, isExpanded: boolean) => {
62
65
  setIsExpanded(isExpanded);