From 8f31619891170ca2f50fd19cd2edcb6f74dc2eec Mon Sep 17 00:00:00 2001 From: Rebecca Alpert Date: Tue, 2 Sep 2025 15:15:58 -0400 Subject: [PATCH 1/2] feat(DeepThinking): Add deep thinking card to Message --- .../Messages/MessageWithDeepThinking.tsx | 18 ++++ .../chatbot/examples/Messages/Messages.md | 8 ++ .../module/src/DeepThinking/DeepThinking.scss | 24 ++++++ .../src/DeepThinking/DeepThinking.test.tsx | 84 +++++++++++++++++++ .../module/src/DeepThinking/DeepThinking.tsx | 71 ++++++++++++++++ packages/module/src/DeepThinking/index.ts | 3 + packages/module/src/Message/Message.test.tsx | 14 ++++ packages/module/src/Message/Message.tsx | 5 ++ packages/module/src/index.ts | 3 + packages/module/src/main.scss | 1 + 10 files changed, 231 insertions(+) create mode 100644 packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithDeepThinking.tsx create mode 100644 packages/module/src/DeepThinking/DeepThinking.scss create mode 100644 packages/module/src/DeepThinking/DeepThinking.test.tsx create mode 100644 packages/module/src/DeepThinking/DeepThinking.tsx create mode 100644 packages/module/src/DeepThinking/index.ts diff --git a/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithDeepThinking.tsx b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithDeepThinking.tsx new file mode 100644 index 000000000..5ce76113c --- /dev/null +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithDeepThinking.tsx @@ -0,0 +1,18 @@ +import { FunctionComponent } from 'react'; +import Message from '@patternfly/chatbot/dist/dynamic/Message'; +import patternflyAvatar from './patternfly_avatar.jpg'; + +export const MessageWithDeepThinkingExample: FunctionComponent = () => ( + +); diff --git a/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/Messages.md b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/Messages.md index caef87e76..cbbc1faeb 100644 --- a/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/Messages.md +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/Messages.md @@ -186,6 +186,14 @@ If you are using [model context protocol (MCP)](https://www.redhat.com/en/blog/m ``` +### Messages with deep thinking + +You may find it useful to display information on an LLM's "thought process." Passing `deepThinking` to `` allows you to display a card with an optional subheading and body. Content is intentionally left fully customizable for now as this is an evolving area. + +```js file="./MessageWithDeepThinking.tsx" + +``` + ### Messages with quick start tiles [Quick start](/extensions/quick-starts/) tiles can be added to messages via the `quickStarts` prop. Users can initiate the quick start from a link within the message tile. diff --git a/packages/module/src/DeepThinking/DeepThinking.scss b/packages/module/src/DeepThinking/DeepThinking.scss new file mode 100644 index 000000000..7a5f49f71 --- /dev/null +++ b/packages/module/src/DeepThinking/DeepThinking.scss @@ -0,0 +1,24 @@ +.pf-chatbot__deep-thinking { + --pf-v6-c-card--BorderColor: var(--pf-t--global--border--color--control--read-only); + overflow: unset; +} + +.pf-chatbot__deep-thinking-expandable-section { + --pf-v6-c-expandable-section--Gap: var(--pf-t--global--spacer--xs); +} + +.pf-chatbot__deep-thinking-section { + display: flex; + flex-direction: column; + gap: var(--pf-t--global--spacer--xs); +} + +.pf-chatbot__deep-thinking-subheading { + font-size: var(--pf-t--global--font--size--body--sm); + font-weight: var(--pf-t--global--font--weight--body--default); + color: var(--pf-t--global--text--color--subtle); +} + +.pf-chatbot__deep-thinking-body { + color: var(--pf-t--global--text--color--subtle); +} diff --git a/packages/module/src/DeepThinking/DeepThinking.test.tsx b/packages/module/src/DeepThinking/DeepThinking.test.tsx new file mode 100644 index 000000000..8e6845ff0 --- /dev/null +++ b/packages/module/src/DeepThinking/DeepThinking.test.tsx @@ -0,0 +1,84 @@ +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import '@testing-library/jest-dom'; +import DeepThinking from './DeepThinking'; + +describe('DeepThinking', () => { + const defaultProps = { + collapsedToggleText: 'Show details', + expandedToggleText: 'Hide details' + }; + + it('should render with required props only', () => { + render(); + expect(screen.getByText('Hide details')).toBeTruthy(); + }); + + it('should render expanded by default', () => { + render(); + expect(screen.getByText('Hide details')).toBeTruthy(); + expect(screen.queryByText('Show details')).toBeFalsy(); + }); + + it('should toggle between expanded and collapsed states', async () => { + const user = userEvent.setup(); + render(); + const toggleButton = screen.getByRole('button', { name: /Hide details/i }); + expect(screen.getByText('Hide details')).toBeTruthy(); + await user.click(toggleButton); + expect(screen.getByText('Show details')).toBeTruthy(); + expect(screen.queryByText('Hide details')).toBeFalsy(); + + // Click to expand again + await user.click(screen.getByRole('button', { name: /Show details/i })); + expect(screen.getByText('Hide details')).toBeTruthy(); + expect(screen.queryByText('Show details')).toBeFalsy(); + }); + + it('should render subheading when provided', () => { + const subheading = 'Thought for 3 seconds'; + render(); + expect(screen.getByText(subheading)).toBeTruthy(); + }); + + it('should render body content when provided', () => { + const body = "Here's why I think that"; + render(); + expect(screen.getByText(body)).toBeTruthy(); + }); + + it('should render with complex content including React elements', () => { + const body = ( +
+

Complex body content

+
    +
  • Item 1
  • +
  • Item 2
  • +
+
+ ); + + render(); + expect(screen.getByText('Complex body content')).toBeTruthy(); + expect(screen.getByText('Item 1')).toBeTruthy(); + expect(screen.getByText('Item 2')).toBeTruthy(); + }); + + it('should apply custom className from cardProps', () => { + const { container } = render( + + ); + expect(container.querySelector('.custom-tool-response-class')).toBeTruthy(); + }); + + it('should pass through expandableSectionProps', () => { + render(); + expect(document.querySelector('.custom-expandable-class')).toBeTruthy(); + }); + + it('should not render subheading span when subheading is not provided', () => { + const { container } = render(); + const subheadingContainer = container.querySelector('.pf-chatbot__tool-response-subheading'); + expect(subheadingContainer).toBeFalsy(); + }); +}); diff --git a/packages/module/src/DeepThinking/DeepThinking.tsx b/packages/module/src/DeepThinking/DeepThinking.tsx new file mode 100644 index 000000000..576d48bff --- /dev/null +++ b/packages/module/src/DeepThinking/DeepThinking.tsx @@ -0,0 +1,71 @@ +// ============================================================================ +// Deep Thinking +// ============================================================================ +import { + Card, + CardBody, + CardBodyProps, + CardProps, + ExpandableSection, + ExpandableSectionProps +} from '@patternfly/react-core'; +import { useState, type FunctionComponent } from 'react'; + +export interface DeepThinkingProps { + /** Toggle text shown on expandable section when it is collapsed */ + collapsedToggleText: string; + /** Toggle text shown on expandable section when it is expanded */ + expandedToggleText: string; + /** Additional props passed to expandable section */ + expandableSectionProps?: Omit; + /** Subheading rendered inside expandable section */ + subheading?: string; + /** Body text rendered inside expandable section */ + body?: React.ReactNode | string; + /** Additional props passed to main card */ + cardProps?: CardProps; + /** Additional props passed to main card body */ + cardBodyProps?: CardBodyProps; +} + +export const DeepThinking: FunctionComponent = ({ + body, + cardProps, + collapsedToggleText, + expandableSectionProps, + expandedToggleText, + subheading, + cardBodyProps +}: DeepThinkingProps) => { + const [isExpanded, setIsExpanded] = useState(true); + + const onToggle = (_event: React.MouseEvent, isExpanded: boolean) => { + setIsExpanded(isExpanded); + }; + + return ( + + + +
+ {subheading && ( +
+ {subheading} +
+ )} + {body &&
{body}
} +
+
+
+
+ ); +}; + +export default DeepThinking; diff --git a/packages/module/src/DeepThinking/index.ts b/packages/module/src/DeepThinking/index.ts new file mode 100644 index 000000000..5b072f993 --- /dev/null +++ b/packages/module/src/DeepThinking/index.ts @@ -0,0 +1,3 @@ +export { default } from './DeepThinking'; + +export * from './DeepThinking'; diff --git a/packages/module/src/Message/Message.test.tsx b/packages/module/src/Message/Message.test.tsx index c64c191f3..929497aa2 100644 --- a/packages/module/src/Message/Message.test.tsx +++ b/packages/module/src/Message/Message.test.tsx @@ -7,6 +7,7 @@ import { monitorSampleAppQuickStart } from './QuickStarts/monitor-sampleapp-quic import { monitorSampleAppQuickStartWithImage } from './QuickStarts/monitor-sampleapp-quickstart-with-image'; import rehypeExternalLinks from '../__mocks__/rehype-external-links'; import { AlertActionLink } from '@patternfly/react-core'; +import { DeepThinkingProps } from '../DeepThinking'; const ALL_ACTIONS = [ { label: /Good response/i }, @@ -145,6 +146,13 @@ const IMAGE = `![Multi-colored wavy lines on a black background](https://cdn.dri const INLINE_IMAGE = `inline text ![Multi-colored wavy lines on a black background](https://cdn.dribbble.com/userupload/10651749/file/original-8a07b8e39d9e8bf002358c66fce1223e.gif)`; +const DEEP_THINKING: DeepThinkingProps = { + collapsedToggleText: 'Show thinking', + expandedToggleText: 'Show thinking', + subheading: 'Thought for 3 seconds', + body: "Here's why I said this." +}; + const ERROR = { title: 'Could not load chat', children: 'Wait a few minutes and check your network settings. If the issue persists: ', @@ -1003,4 +1011,10 @@ describe('Message', () => { // code block isn't rendering expect(screen.queryByRole('button', { name: 'Copy code' })).toBeFalsy(); }); + it('should render deep thinking section correctly', () => { + render(); + expect(screen.getByRole('button', { name: /Show thinking/i })).toBeTruthy(); + expect(screen.getByText('Thought for 3 seconds')).toBeTruthy(); + expect(screen.getByText("Here's why I said this.")).toBeTruthy(); + }); }); diff --git a/packages/module/src/Message/Message.tsx b/packages/module/src/Message/Message.tsx index 9e28d4f1e..067c04475 100644 --- a/packages/module/src/Message/Message.tsx +++ b/packages/module/src/Message/Message.tsx @@ -50,6 +50,7 @@ import ErrorMessage from './ErrorMessage/ErrorMessage'; import MessageInput from './MessageInput'; import { rehypeMoveImagesOutOfParagraphs } from './Plugins/rehypeMoveImagesOutOfParagraphs'; import ToolResponse, { ToolResponseProps } from '../ToolResponse'; +import DeepThinking, { DeepThinkingProps } from '../DeepThinking'; export interface MessageAttachment { /** Name of file attached to the message */ @@ -192,6 +193,8 @@ export interface MessageProps extends Omit, 'role'> { reactMarkdownProps?: Options; /** Props for tool response card */ toolResponse?: ToolResponseProps; + /** Props for deep thinking card */ + deepThinking?: DeepThinkingProps; } export const MessageBase: FunctionComponent = ({ @@ -234,6 +237,7 @@ export const MessageBase: FunctionComponent = ({ isMarkdownDisabled, reactMarkdownProps, toolResponse, + deepThinking, ...props }: MessageProps) => { const [messageText, setMessageText] = useState(content); @@ -381,6 +385,7 @@ export const MessageBase: FunctionComponent = ({ {renderMessage()} {afterMainContent && <>{afterMainContent}} {toolResponse && } + {deepThinking && } {!isLoading && sources && } {quickStarts && quickStarts.quickStart && ( Date: Wed, 3 Sep 2025 15:08:39 -0400 Subject: [PATCH 2/2] Address PR feedback Co-authored-by: Erin Donehoo <105813956+edonehoo@users.noreply.github.com> --- .../Messages/MessageWithDeepThinking.tsx | 5 ++-- .../chatbot/examples/Messages/Messages.md | 4 ++- .../src/DeepThinking/DeepThinking.test.tsx | 27 ++----------------- .../module/src/DeepThinking/DeepThinking.tsx | 11 +++----- packages/module/src/Message/Message.test.tsx | 3 +-- 5 files changed, 12 insertions(+), 38 deletions(-) diff --git a/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithDeepThinking.tsx b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithDeepThinking.tsx index 5ce76113c..2131b7f55 100644 --- a/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithDeepThinking.tsx +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithDeepThinking.tsx @@ -7,10 +7,9 @@ export const MessageWithDeepThinkingExample: FunctionComponent = () => ( name="Bot" role="bot" avatar={patternflyAvatar} - content="This example has a body description that's within the recommended limit of 2 lines:" + content="This example has a body description that's within the recommended limit of 2 lines." deepThinking={{ - collapsedToggleText: 'Show thinking', - expandedToggleText: 'Show thinking', + toggleContent: 'Show thinking', subheading: 'Thought for 3 seconds', body: "Here's why I said this." }} diff --git a/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/Messages.md b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/Messages.md index cbbc1faeb..b5a741073 100644 --- a/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/Messages.md +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/Messages.md @@ -188,7 +188,9 @@ If you are using [model context protocol (MCP)](https://www.redhat.com/en/blog/m ### Messages with deep thinking -You may find it useful to display information on an LLM's "thought process." Passing `deepThinking` to `` allows you to display a card with an optional subheading and body. Content is intentionally left fully customizable for now as this is an evolving area. +You can share details about the "thought process" behind an LLM's response, also known as deep thinking. To display a customizable, expandable card with these details, pass `deepThinking` to `` and provide a subheading (optional) and content body. + +Because this is an evolving area, this card content is currently fully customizable. ```js file="./MessageWithDeepThinking.tsx" diff --git a/packages/module/src/DeepThinking/DeepThinking.test.tsx b/packages/module/src/DeepThinking/DeepThinking.test.tsx index 8e6845ff0..03f333385 100644 --- a/packages/module/src/DeepThinking/DeepThinking.test.tsx +++ b/packages/module/src/DeepThinking/DeepThinking.test.tsx @@ -1,38 +1,15 @@ import { render, screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; import '@testing-library/jest-dom'; import DeepThinking from './DeepThinking'; describe('DeepThinking', () => { const defaultProps = { - collapsedToggleText: 'Show details', - expandedToggleText: 'Hide details' + toggleContent: 'Show thinking' }; it('should render with required props only', () => { render(); - expect(screen.getByText('Hide details')).toBeTruthy(); - }); - - it('should render expanded by default', () => { - render(); - expect(screen.getByText('Hide details')).toBeTruthy(); - expect(screen.queryByText('Show details')).toBeFalsy(); - }); - - it('should toggle between expanded and collapsed states', async () => { - const user = userEvent.setup(); - render(); - const toggleButton = screen.getByRole('button', { name: /Hide details/i }); - expect(screen.getByText('Hide details')).toBeTruthy(); - await user.click(toggleButton); - expect(screen.getByText('Show details')).toBeTruthy(); - expect(screen.queryByText('Hide details')).toBeFalsy(); - - // Click to expand again - await user.click(screen.getByRole('button', { name: /Show details/i })); - expect(screen.getByText('Hide details')).toBeTruthy(); - expect(screen.queryByText('Show details')).toBeFalsy(); + expect(screen.getByText('Show thinking')).toBeTruthy(); }); it('should render subheading when provided', () => { diff --git a/packages/module/src/DeepThinking/DeepThinking.tsx b/packages/module/src/DeepThinking/DeepThinking.tsx index 576d48bff..9b485cb6b 100644 --- a/packages/module/src/DeepThinking/DeepThinking.tsx +++ b/packages/module/src/DeepThinking/DeepThinking.tsx @@ -12,10 +12,8 @@ import { import { useState, type FunctionComponent } from 'react'; export interface DeepThinkingProps { - /** Toggle text shown on expandable section when it is collapsed */ - collapsedToggleText: string; - /** Toggle text shown on expandable section when it is expanded */ - expandedToggleText: string; + /** Toggle content shown for expandable section */ + toggleContent: React.ReactNode; /** Additional props passed to expandable section */ expandableSectionProps?: Omit; /** Subheading rendered inside expandable section */ @@ -31,10 +29,9 @@ export interface DeepThinkingProps { export const DeepThinking: FunctionComponent = ({ body, cardProps, - collapsedToggleText, expandableSectionProps, - expandedToggleText, subheading, + toggleContent, cardBodyProps }: DeepThinkingProps) => { const [isExpanded, setIsExpanded] = useState(true); @@ -47,7 +44,7 @@ export const DeepThinking: FunctionComponent = ({