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..2131b7f55 --- /dev/null +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithDeepThinking.tsx @@ -0,0 +1,17 @@ +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..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 @@ -186,6 +186,16 @@ If you are using [model context protocol (MCP)](https://www.redhat.com/en/blog/m ``` +### Messages with deep thinking + +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" + +``` + ### 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..03f333385 --- /dev/null +++ b/packages/module/src/DeepThinking/DeepThinking.test.tsx @@ -0,0 +1,61 @@ +import { render, screen } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import DeepThinking from './DeepThinking'; + +describe('DeepThinking', () => { + const defaultProps = { + toggleContent: 'Show thinking' + }; + + it('should render with required props only', () => { + render(); + expect(screen.getByText('Show thinking')).toBeTruthy(); + }); + + 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..9b485cb6b --- /dev/null +++ b/packages/module/src/DeepThinking/DeepThinking.tsx @@ -0,0 +1,68 @@ +// ============================================================================ +// Deep Thinking +// ============================================================================ +import { + Card, + CardBody, + CardBodyProps, + CardProps, + ExpandableSection, + ExpandableSectionProps +} from '@patternfly/react-core'; +import { useState, type FunctionComponent } from 'react'; + +export interface DeepThinkingProps { + /** Toggle content shown for expandable section */ + toggleContent: React.ReactNode; + /** 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, + expandableSectionProps, + subheading, + toggleContent, + 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..c05753475 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,12 @@ 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 = { + toggleContent: '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 +1010,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 && (