diff --git a/package-lock.json b/package-lock.json index 280cd0a95..f95fd49a7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -114,7 +114,6 @@ "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", @@ -3133,7 +3132,6 @@ "version": "29.7.0", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/environment": "^29.7.0", "@jest/expect": "^29.7.0", @@ -3781,7 +3779,6 @@ "resolved": "https://registry.npmjs.org/@monaco-editor/react/-/react-4.7.0.tgz", "integrity": "sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA==", "license": "MIT", - "peer": true, "dependencies": { "@monaco-editor/loader": "^1.5.0" }, @@ -3835,7 +3832,6 @@ "version": "3.6.0", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@octokit/auth-token": "^2.4.4", "@octokit/graphql": "^4.5.8", @@ -4220,8 +4216,7 @@ "resolved": "https://registry.npmjs.org/@patternfly/patternfly/-/patternfly-6.4.0.tgz", "integrity": "sha512-4drFhg74sEc/fftark5wZevODIog17qR4pwLCdB3j5iK3Uu5oMA2SdLhsEeEQggalfnFzve/Km87MdVR0ghhvQ==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@patternfly/patternfly-a11y": { "version": "5.1.0", @@ -4301,7 +4296,6 @@ "resolved": "https://registry.npmjs.org/@patternfly/react-core/-/react-core-6.4.0.tgz", "integrity": "sha512-zMgJmcFohp2FqgAoZHg7EXZS7gnaFESquk0qIavemYI0FsqspVlzV2/PUru7w+86+jXfqebRhgubPRsv1eJwEg==", "license": "MIT", - "peer": true, "dependencies": { "@patternfly/react-icons": "^6.4.0", "@patternfly/react-styles": "^6.4.0", @@ -4336,7 +4330,6 @@ "resolved": "https://registry.npmjs.org/@patternfly/react-table/-/react-table-6.4.0.tgz", "integrity": "sha512-yv0sFOLGts8a2q9C1xUegjp50ayYyVRe0wKjMf+aMSNIK8sVYu8qu0yfBsCDybsUCldue7+qsYKRLFZosTllWQ==", "license": "MIT", - "peer": true, "dependencies": { "@patternfly/react-core": "^6.4.0", "@patternfly/react-icons": "^6.4.0", @@ -4586,7 +4579,6 @@ "dev": true, "hasInstallScript": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@swc/counter": "^0.1.1", "@swc/types": "^0.1.5" @@ -5151,7 +5143,6 @@ "version": "29.5.12", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "expect": "^29.0.0", "pretty-format": "^29.0.0" @@ -5266,7 +5257,6 @@ "node_modules/@types/react": { "version": "18.2.61", "license": "MIT", - "peer": true, "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -5444,7 +5434,6 @@ "version": "5.62.0", "dev": true, "license": "BSD-2-Clause", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "5.62.0", "@typescript-eslint/types": "5.62.0", @@ -5896,7 +5885,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -6018,7 +6006,6 @@ "version": "6.12.6", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -7227,7 +7214,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001735", "electron-to-chromium": "^1.5.204", @@ -10059,8 +10045,7 @@ "node_modules/devtools-protocol": { "version": "0.0.1367902", "dev": true, - "license": "BSD-3-Clause", - "peer": true + "license": "BSD-3-Clause" }, "node_modules/diff-sequences": { "version": "29.6.3", @@ -10144,7 +10129,8 @@ "version": "3.1.7", "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.7.tgz", "integrity": "sha512-VaTstWtsneJY8xzy7DekmYWEOZcmzIe3Qb3zPd4STve1OBTa+e+WmS1ITQec1fZYXI3HCsOZZiSMpG6oxoWMWQ==", - "license": "(MPL-2.0 OR Apache-2.0)" + "license": "(MPL-2.0 OR Apache-2.0)", + "peer": true }, "node_modules/dot-case": { "version": "3.0.4", @@ -10298,7 +10284,6 @@ "version": "0.1.13", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "iconv-lite": "^0.6.2" } @@ -10340,7 +10325,6 @@ "version": "2.4.1", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "ansi-colors": "^4.1.1", "strip-ansi": "^6.0.1" @@ -10651,7 +10635,6 @@ "version": "8.57.0", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -10706,7 +10689,6 @@ "version": "9.1.0", "dev": true, "license": "MIT", - "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -10842,7 +10824,6 @@ "version": "2.29.1", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "array-includes": "^3.1.7", "array.prototype.findlastindex": "^1.2.3", @@ -11032,7 +11013,6 @@ "version": "15.7.0", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "builtins": "^5.0.1", "eslint-plugin-es": "^4.1.0", @@ -11116,7 +11096,6 @@ "version": "6.1.1", "dev": true, "license": "ISC", - "peer": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, @@ -14720,7 +14699,6 @@ "version": "29.7.0", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -18805,6 +18783,7 @@ "resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz", "integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==", "license": "MIT", + "peer": true, "bin": { "marked": "bin/marked.js" }, @@ -20129,7 +20108,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", @@ -20249,7 +20227,6 @@ "version": "3.2.5", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -20682,6 +20659,22 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/puppeteer/node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/pure-rand": { "version": "6.0.4", "dev": true, @@ -20811,7 +20804,6 @@ "node_modules/react": { "version": "18.2.0", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -20848,7 +20840,6 @@ "node_modules/react-dom": { "version": "18.2.0", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.0" @@ -21679,7 +21670,6 @@ "version": "7.10.5", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/generator": "^7.10.5", @@ -22453,7 +22443,6 @@ "version": "1.72.0", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "chokidar": ">=3.0.0 <4.0.0", "immutable": "^4.0.0", @@ -22550,7 +22539,6 @@ "version": "8.12.0", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", @@ -24375,7 +24363,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -24666,8 +24653,7 @@ }, "node_modules/tslib": { "version": "2.8.1", - "license": "0BSD", - "peer": true + "license": "0BSD" }, "node_modules/tsutils": { "version": "3.21.0", @@ -24874,7 +24860,6 @@ "version": "4.7.4", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -25667,7 +25652,6 @@ "version": "37.3.6", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "lodash": "^4.17.19", "victory-core": "37.3.6", @@ -25700,7 +25684,6 @@ "version": "37.3.6", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "lodash": "^4.17.21", "react-fast-compare": "^3.2.0", @@ -25717,7 +25700,6 @@ "version": "37.3.6", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "lodash": "^4.17.19", "victory-brush-container": "37.3.6", @@ -25738,7 +25720,6 @@ "version": "37.3.6", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "lodash": "^4.17.19", "victory-core": "37.3.6" @@ -25754,7 +25735,6 @@ "version": "37.3.6", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "lodash": "^4.17.19", "react-fast-compare": "^3.2.0", @@ -25772,7 +25752,6 @@ "version": "37.3.6", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "lodash": "^4.17.19", "victory-core": "37.3.6" @@ -25788,7 +25767,6 @@ "version": "37.3.6", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "lodash": "^4.17.19", "victory-core": "37.3.6", @@ -25805,7 +25783,6 @@ "version": "37.3.6", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "lodash": "^4.17.19", "victory-core": "37.3.6", @@ -25822,7 +25799,6 @@ "version": "37.3.6", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "lodash": "^4.17.19", "victory-core": "37.3.6" @@ -25870,7 +25846,6 @@ "version": "37.3.6", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "lodash": "^4.17.19", "react-fast-compare": "^3.2.0", @@ -25888,7 +25863,6 @@ "version": "37.3.6", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "lodash": "^4.17.19", "victory-core": "37.3.6" @@ -25925,7 +25899,6 @@ "version": "37.3.6", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "delaunay-find": "0.0.6", "lodash": "^4.17.19", @@ -26061,7 +26034,6 @@ "integrity": "sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "^1.0.5", "@webassemblyjs/ast": "^1.12.1", @@ -26179,7 +26151,6 @@ "version": "5.0.1", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@discoveryjs/json-ext": "^0.5.0", "@webpack-cli/configtest": "^2.0.1", diff --git a/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithMarkdownDeepThinking.tsx b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithMarkdownDeepThinking.tsx new file mode 100644 index 000000000..f9d9e37dc --- /dev/null +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithMarkdownDeepThinking.tsx @@ -0,0 +1,26 @@ +import { FunctionComponent } from 'react'; +import Message from '@patternfly/chatbot/dist/dynamic/Message'; +import patternflyAvatar from './patternfly_avatar.jpg'; + +export const MessageWithMarkdownDeepThinkingExample: FunctionComponent = () => ( + Thought for 3 seconds', + isSubheadingMarkdown: true, + body: `I considered **multiple approaches** to answer your question: + +1. *Direct response* - Quick but less comprehensive +2. *Research-based* - Thorough but time-consuming +3. **Balanced approach** - Combines speed and accuracy + +I chose option 3 because it provides the best user experience.`, + isBodyMarkdown: true + }} + /> +); diff --git a/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithMarkdownToolCall.tsx b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithMarkdownToolCall.tsx new file mode 100644 index 000000000..1c1cd5051 --- /dev/null +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithMarkdownToolCall.tsx @@ -0,0 +1,29 @@ +import { FunctionComponent } from 'react'; +import Message from '@patternfly/chatbot/dist/dynamic/Message'; +import patternflyAvatar from './patternfly_avatar.jpg'; + +export const MessageWithMarkdownToolCallExample: FunctionComponent = () => ( + +); diff --git a/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithMarkdownToolResponse.tsx b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithMarkdownToolResponse.tsx new file mode 100644 index 000000000..915944bcf --- /dev/null +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithMarkdownToolResponse.tsx @@ -0,0 +1,200 @@ +import { useState, FunctionComponent, MouseEvent as ReactMouseEvent } from 'react'; +import Message from '@patternfly/chatbot/dist/dynamic/Message'; +import patternflyAvatar from './patternfly_avatar.jpg'; +import { CopyIcon, WrenchIcon } from '@patternfly/react-icons'; +import { + Button, + DescriptionList, + DescriptionListDescription, + DescriptionListGroup, + DescriptionListTerm, + ExpandableSection, + ExpandableSectionVariant, + Flex, + FlexItem, + Label +} from '@patternfly/react-core'; +export const MessageWithToolResponseExample: FunctionComponent = () => { + const [isExpanded, setIsExpanded] = useState(false); + const onToggle = (_event: ReactMouseEvent, isExpanded: boolean) => { + setIsExpanded(isExpanded); + }; + const toolResponseBody = `The tool processed **3 database queries** and returned the following results: + +1. User data - *42 records* +2. Transaction history - *128 records* +3. Analytics metrics - *15 data points* + +\`\`\`json +{ + "status": "success", + "execution_time": "0.12s" +} +\`\`\``; + return ( + Completed in 0.12 seconds', + body: toolResponseBody, + isBodyMarkdown: true, + cardTitle: ( + + + + + + + + + toolName + + + + + Execution time: + 0.12 seconds + + + + + + + + + ), + cardBody: ( + <> + + + Parameters + + + Optional description text for parameters. + + + + + + + + + + + + + + + + + + + + + Response + + + Descriptive text about the tool response, including completion status, details on the data that was + processed, or anything else relevant to the use case. + + + + + + ) + }} + /> + ); +}; 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 101a8ff73..a42378d1e 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 @@ -349,3 +349,35 @@ An attachment dropzone allows users to upload files via drag and drop. ```js file="./FileDropZone.tsx" ``` + +## Examples with Markdown + +The ChatBot supports Markdown formatting in several message components, allowing you to display rich, formatted content. This is particularly useful when you need to include code snippets, lists, emphasis, or other formatted text. The following examples demonstrate different ways you can use Markdown in a few of the ChatBot components, but this is not an exhaustive list of all Markdown customizations you can make. + +To enable Markdown rendering, use the appropriate Markdown flag prop (such as `isBodyMarkdown`, `isSubheadingMarkdown`, or `isExpandableContentMarkdown`) depending on the component and content you're formatting. + +**Important:** When using Markdown in these components, set `shouldRetainStyles: true` to retain the styling of the context the Markdown is used in. This ensures that Markdown content maintains the proper font sizes, colors, and other styling properties of its parent component. For example, Markdown passed into a toggle will retain the ChatBot toggle styling, while Markdown in a card body will maintain the appropriate card body styling. Without this prop, the Markdown may override the contextual styles and create inconsistencies with the rest of the ChatBot interface. + +### Tool calls with Markdown + +When displaying tool call information, you can use Markdown in the expandable content to provide formatted details about what the tool is processing. This is useful for showing structured data, code snippets, or formatted lists. + +```ts file="./MessageWithMarkdownToolCall.tsx" + +``` + +### Deep thinking with Markdown + +Deep thinking content can include Markdown formatting in both the subheading and body to better communicate the LLM's reasoning process. This allows you to emphasize key points, structure thought processes with lists, or include other formatting. + +```ts file="./MessageWithMarkdownDeepThinking.tsx" + +``` + +### Tool responses with Markdown + +Tool response cards support Markdown in multiple areas, including the toggle content, subheading, and body. Use `shouldRetainStyles: true` along with the appropriate Markdown flag props to ensure proper formatting and spacing. + +```ts file="./MessageWithMarkdownToolResponse.tsx" + +``` diff --git a/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/Chatbot.md b/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/Chatbot.md index 680932aae..8814917f7 100644 --- a/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/Chatbot.md +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/Chatbot.md @@ -52,6 +52,20 @@ import ExpandIcon from '@patternfly/react-icons/dist/esm/icons/expand-icon'; import OpenDrawerRightIcon from '@patternfly/react-icons/dist/esm/icons/open-drawer-right-icon'; import OutlinedWindowRestoreIcon from '@patternfly/react-icons/dist/esm/icons/outlined-window-restore-icon'; import { BarsIcon } from '@patternfly/react-icons/dist/esm/icons/bars-icon'; +import { CopyIcon } from '@patternfly/react-icons/dist/esm/icons/copy-icon'; +import { WrenchIcon } from '@patternfly/react-icons/dist/esm/icons/wrench-icon'; +import { +Button, +DescriptionList, +DescriptionListDescription, +DescriptionListGroup, +DescriptionListTerm, +ExpandableSection, +ExpandableSectionVariant, +Flex, +FlexItem, +Label +} from '@patternfly/react-core'; import PFHorizontalLogoColor from '../UI/PF-HorizontalLogo-Color.svg'; import PFHorizontalLogoReverse from '../UI/PF-HorizontalLogo-Reverse.svg'; import PFIconLogoColor from '../UI/PF-IconLogo-Color.svg'; @@ -59,7 +73,7 @@ import PFIconLogoReverse from '../UI/PF-IconLogo-Reverse.svg'; import userAvatar from '../Messages/user_avatar.svg'; import patternflyAvatar from '../Messages/patternfly_avatar.jpg'; import { getTrackingProviders } from "@patternfly/chatbot/dist/dynamic/tracking"; -import { useEffect,useCallback, useRef, useState, FunctionComponent, MouseEvent } from 'react'; +import { useEffect,useCallback, useRef, useState, FunctionComponent, MouseEvent, MouseEvent as ReactMouseEvent } from 'react'; import saveAs from 'file-saver'; ### Basic ChatBot diff --git a/packages/module/src/DeepThinking/DeepThinking.test.tsx b/packages/module/src/DeepThinking/DeepThinking.test.tsx index 1621e4e02..7c0e0666b 100644 --- a/packages/module/src/DeepThinking/DeepThinking.test.tsx +++ b/packages/module/src/DeepThinking/DeepThinking.test.tsx @@ -119,4 +119,52 @@ describe('DeepThinking', () => { expect(toggleButton).toHaveAttribute('aria-expanded', 'false'); expect(screen.getByText('Thinking content')).not.toBeVisible(); }); + + it('should render toggleContent as markdown when isToggleContentMarkdown is true', () => { + const toggleContent = '**Bold thinking**'; + const { container } = render(); + expect(container.querySelector('strong')).toBeTruthy(); + expect(screen.getByText('Bold thinking')).toBeTruthy(); + }); + + it('should not render toggleContent as markdown when isToggleContentMarkdown is false', () => { + const toggleContent = '**Bold thinking**'; + const { container } = render(); + expect(container.querySelector('strong')).toBeFalsy(); + expect(screen.getByText('**Bold thinking**')).toBeTruthy(); + }); + + it('should render subheading as markdown when isSubheadingMarkdown is true', () => { + const subheading = '**Bold subheading**'; + const { container } = render(); + expect(container.querySelector('strong')).toBeTruthy(); + expect(screen.getByText('Bold subheading')).toBeTruthy(); + }); + + it('should not render subheading as markdown when isSubheadingMarkdown is false', () => { + const subheading = '**Bold subheading**'; + render(); + expect(screen.getByText('**Bold subheading**')).toBeTruthy(); + }); + + it('should render body as markdown when isBodyMarkdown is true', () => { + const body = '**Bold body**'; + const { container } = render(); + expect(container.querySelector('strong')).toBeTruthy(); + expect(screen.getByText('Bold body')).toBeTruthy(); + }); + + it('should not render body as markdown when isBodyMarkdown is false', () => { + const body = '**Bold body**'; + render(); + expect(screen.getByText('**Bold body**')).toBeTruthy(); + }); + + it('should pass markdownContentProps to MarkdownContent component', () => { + const body = '**Bold body**'; + const { container } = render( + + ); + expect(container.querySelector('.pf-m-primary')).toBeTruthy(); + }); }); diff --git a/packages/module/src/DeepThinking/DeepThinking.tsx b/packages/module/src/DeepThinking/DeepThinking.tsx index 11c29549d..4bcf51227 100644 --- a/packages/module/src/DeepThinking/DeepThinking.tsx +++ b/packages/module/src/DeepThinking/DeepThinking.tsx @@ -10,6 +10,8 @@ import { ExpandableSectionProps } from '@patternfly/react-core'; import { useState, type FunctionComponent } from 'react'; +import MarkdownContent from '../MarkdownContent'; +import type { MarkdownContentProps } from '../MarkdownContent'; export interface DeepThinkingProps { /** Toggle content shown for expandable section */ @@ -26,6 +28,16 @@ export interface DeepThinkingProps { cardProps?: CardProps; /** Additional props passed to main card body */ cardBodyProps?: CardBodyProps; + /** Whether to enable markdown rendering for toggleContent. When true and toggleContent is a string, it will be parsed as markdown. */ + isToggleContentMarkdown?: boolean; + /** Whether to enable markdown rendering for subheading. When true, subheading will be parsed as markdown. */ + isSubheadingMarkdown?: boolean; + /** Whether to enable markdown rendering for body. When true and body is a string, it will be parsed as markdown. */ + isBodyMarkdown?: boolean; + /** Props passed to MarkdownContent component when markdown is enabled */ + markdownContentProps?: Omit; + /** Whether to retain styles in the MarkdownContent component. Defaults to false. */ + shouldRetainStyles?: boolean; } export const DeepThinking: FunctionComponent = ({ @@ -35,7 +47,12 @@ export const DeepThinking: FunctionComponent = ({ subheading, toggleContent, isDefaultExpanded = true, - cardBodyProps + cardBodyProps, + isToggleContentMarkdown, + isSubheadingMarkdown, + isBodyMarkdown, + markdownContentProps, + shouldRetainStyles = false }: DeepThinkingProps) => { const [isExpanded, setIsExpanded] = useState(isDefaultExpanded); @@ -43,11 +60,40 @@ export const DeepThinking: FunctionComponent = ({ setIsExpanded(isExpanded); }; + const renderToggleContent = () => { + if (isToggleContentMarkdown && typeof toggleContent === 'string') { + return ( + + ); + } + return toggleContent; + }; + + const renderSubheading = () => { + if (!subheading) { + return null; + } + if (isSubheadingMarkdown) { + return ; + } + return subheading; + }; + + const renderBody = () => { + if (!body) { + return null; + } + if (isBodyMarkdown && typeof body === 'string') { + return ; + } + return body; + }; + return ( = ({
{subheading && (
- {subheading} + {renderSubheading()}
)} - {body &&
{body}
} + {body &&
{renderBody()}
}
diff --git a/packages/module/src/MarkdownContent/MarkdownContent.test.tsx b/packages/module/src/MarkdownContent/MarkdownContent.test.tsx new file mode 100644 index 000000000..c05292347 --- /dev/null +++ b/packages/module/src/MarkdownContent/MarkdownContent.test.tsx @@ -0,0 +1,207 @@ +import { render, screen } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import MarkdownContent from './MarkdownContent'; +import rehypeExternalLinks from '../__mocks__/rehype-external-links'; + +const BOLD_TEXT = '**Bold text**'; +const ITALIC_TEXT = '*Italic text*'; +const INLINE_CODE = 'Here is inline code: `const x = 5`'; +const CODE_BLOCK = `\`\`\`javascript +function hello() { + console.log('Hello, world!'); +} +\`\`\``; +const HEADING = '# Heading 1'; +const LINK = '[PatternFly](https://www.patternfly.org/)'; +const UNORDERED_LIST = ` +* Item 1 +* Item 2 +* Item 3 +`; +const ORDERED_LIST = ` +1. First item +2. Second item +3. Third item +`; +const TABLE = ` +| Column 1 | Column 2 | +|----------|----------| +| Cell 1 | Cell 2 | +| Cell 3 | Cell 4 | +`; +const BLOCKQUOTE = '> This is a blockquote'; +const IMAGE = '![Alt text](https://example.com/image.png)'; + +describe('MarkdownContent', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should render bold text correctly', () => { + const { container } = render(); + expect(container.querySelector('strong')).toBeTruthy(); + expect(screen.getByText('Bold text')).toBeTruthy(); + }); + + it('should render italic text correctly', () => { + const { container } = render(); + expect(container.querySelector('em')).toBeTruthy(); + expect(screen.getByText('Italic text')).toBeTruthy(); + }); + + it('should render inline code correctly', () => { + render(); + expect(screen.getByText(/const x = 5/)).toBeTruthy(); + }); + + it('should render code blocks correctly', () => { + render(); + + expect(screen.getByText(/function hello/)).toBeVisible(); + expect(screen.getByText(/console.log/)).toBeVisible(); + expect(screen.getByRole('button', { name: 'Copy code' })).toBeVisible(); + }); + + it('should render headings correctly', () => { + render(); + expect(screen.getByRole('heading', { name: /Heading 1/i })).toBeTruthy(); + }); + + it('should render links correctly', () => { + render(); + expect(screen.getByRole('link', { name: /PatternFly/i })).toBeTruthy(); + }); + + it('should render unordered lists correctly', () => { + render(); + expect(screen.getByText('Item 1')).toBeTruthy(); + expect(screen.getByText('Item 2')).toBeTruthy(); + expect(screen.getByText('Item 3')).toBeTruthy(); + expect(screen.getAllByRole('listitem')).toHaveLength(3); + }); + + it('should render ordered lists correctly', () => { + render(); + expect(screen.getByText('First item')).toBeTruthy(); + expect(screen.getByText('Second item')).toBeTruthy(); + expect(screen.getByText('Third item')).toBeTruthy(); + expect(screen.getAllByRole('listitem')).toHaveLength(3); + }); + + it('should render tables correctly', () => { + render(); + expect(screen.getByRole('grid', { name: /Test table/i })).toBeTruthy(); + expect(screen.getByRole('columnheader', { name: /Column 1/i })).toBeTruthy(); + expect(screen.getByRole('columnheader', { name: /Column 2/i })).toBeTruthy(); + expect(screen.getByRole('cell', { name: /Cell 1/i })).toBeTruthy(); + expect(screen.getByRole('cell', { name: /Cell 2/i })).toBeTruthy(); + }); + + it('should render blockquotes correctly', () => { + render(); + + const quote = screen.getByText(/This is a blockquote/); + expect(quote).toBeVisible(); + expect(quote.closest('.pf-v6-c-content--blockquote')?.tagName).toBe('BLOCKQUOTE'); + }); + + it('should render images when hasNoImages is false', () => { + render(); + expect(screen.getByRole('img', { name: /Alt text/i })).toBeTruthy(); + }); + + it('should not render images when hasNoImages is true', () => { + render(); + expect(screen.queryByRole('img', { name: /Alt text/i })).toBeFalsy(); + }); + + it('should disable markdown rendering when isMarkdownDisabled is true', () => { + render(); + expect(screen.getByText('**Bold text**')).toBeTruthy(); + }); + + it('should render text component when isMarkdownDisabled is true and textComponent is provided', () => { + const textComponent =
Custom text component
; + render(); + expect(screen.getByTestId('custom-text')).toBeTruthy(); + expect(screen.getByText('Custom text component')).toBeTruthy(); + }); + + it('should apply isPrimary prop to elements', () => { + const { container } = render(); + expect(container.querySelector('.pf-m-primary')).toBeTruthy(); + }); + + it('should apply shouldRetainStyles prop to elements', () => { + const { container } = render(); + expect(container.querySelector('.pf-m-markdown')).toBeTruthy(); + }); + + it('should pass codeBlockProps to code blocks', () => { + render(); + expect(screen.getByRole('button', { name: /Custom code block/i })).toBeTruthy(); + }); + + it('should pass tableProps to tables', () => { + render(); + expect(screen.getByRole('grid', { name: /Custom table label/i })).toBeTruthy(); + }); + + it('should open links in new tab when openLinkInNewTab is true', () => { + render(); + expect(rehypeExternalLinks).toHaveBeenCalledTimes(1); + }); + + it('should not open links in new tab when openLinkInNewTab is false', () => { + render(); + expect(rehypeExternalLinks).not.toHaveBeenCalled(); + }); + + it('should pass linkProps to links', async () => { + const onClick = jest.fn(); + render(); + const link = screen.getByRole('link', { name: /PatternFly/i }); + link.click(); + expect(onClick).toHaveBeenCalledTimes(1); + }); + + it('should handle reactMarkdownProps.disallowedElements', () => { + render(); + // Code block should not render when disallowed + expect(screen.queryByRole('button', { name: /Copy code/i })).toBeFalsy(); + }); + + it('should render plain text when no markdown is present', () => { + render(); + expect(screen.getByText('Plain text without markdown')).toBeTruthy(); + }); + + it('should handle empty content', () => { + const { container } = render(); + expect(container.textContent).toBe(''); + }); + + it('should handle undefined content', () => { + const { container } = render(); + expect(container.textContent).toBe(''); + }); + + it('should render multiple markdown elements together', () => { + const content = `# Heading + +**Bold text** and *italic text* + +\`\`\`javascript +const x = 5; +\`\`\` + +[Link](https://example.com)`; + + render(); + expect(screen.getByRole('heading', { name: /Heading/i })).toBeTruthy(); + expect(screen.getByText('Bold text')).toBeTruthy(); + expect(screen.getByText('italic text')).toBeTruthy(); + expect(screen.getByText(/const x = 5/)).toBeTruthy(); + expect(screen.getByRole('link', { name: /Link/i })).toBeTruthy(); + }); +}); diff --git a/packages/module/src/MarkdownContent/MarkdownContent.tsx b/packages/module/src/MarkdownContent/MarkdownContent.tsx new file mode 100644 index 000000000..f852ea003 --- /dev/null +++ b/packages/module/src/MarkdownContent/MarkdownContent.tsx @@ -0,0 +1,264 @@ +// ============================================================================ +// Markdown Content - Shared component for rendering markdown +// ============================================================================ +import { type FunctionComponent, ReactNode } from 'react'; +import Markdown, { Options } from 'react-markdown'; +import remarkGfm from 'remark-gfm'; +import { ContentVariants } from '@patternfly/react-core'; +import CodeBlockMessage, { CodeBlockMessageProps } from '../Message/CodeBlockMessage/CodeBlockMessage'; +import TextMessage from '../Message/TextMessage/TextMessage'; +import ListItemMessage from '../Message/ListMessage/ListItemMessage'; +import UnorderedListMessage from '../Message/ListMessage/UnorderedListMessage'; +import OrderedListMessage from '../Message/ListMessage/OrderedListMessage'; +import TableMessage from '../Message/TableMessage/TableMessage'; +import TrMessage from '../Message/TableMessage/TrMessage'; +import TdMessage from '../Message/TableMessage/TdMessage'; +import TbodyMessage from '../Message/TableMessage/TbodyMessage'; +import TheadMessage from '../Message/TableMessage/TheadMessage'; +import ThMessage from '../Message/TableMessage/ThMessage'; +import { TableProps } from '@patternfly/react-table'; +import ImageMessage from '../Message/ImageMessage/ImageMessage'; +import rehypeUnwrapImages from 'rehype-unwrap-images'; +import rehypeExternalLinks from 'rehype-external-links'; +import rehypeSanitize from 'rehype-sanitize'; +import rehypeHighlight from 'rehype-highlight'; +import 'highlight.js/styles/vs2015.css'; +import { PluggableList } from 'unified'; +import LinkMessage from '../Message/LinkMessage/LinkMessage'; +import { rehypeMoveImagesOutOfParagraphs } from '../Message/Plugins/rehypeMoveImagesOutOfParagraphs'; +import SuperscriptMessage from '../Message/SuperscriptMessage/SuperscriptMessage'; +import { ButtonProps } from '@patternfly/react-core'; +import { css } from '@patternfly/react-styles'; + +export interface MarkdownContentProps { + /** The markdown content to render */ + content?: string; + /** Disables markdown parsing, allowing only text input */ + isMarkdownDisabled?: boolean; + /** Props for code blocks */ + codeBlockProps?: CodeBlockMessageProps; + /** Props for table message. It is important to include a detailed aria-label that describes the purpose of the table. */ + tableProps?: Required> & TableProps; + /** Additional rehype plugins passed from the consumer */ + additionalRehypePlugins?: PluggableList; + /** Additional remark plugins passed from the consumer */ + additionalRemarkPlugins?: PluggableList; + /** Whether to open links in message in new tab. */ + openLinkInNewTab?: boolean; + /** Props for links */ + linkProps?: ButtonProps; + /** Allows passing additional props down to markdown parser react-markdown, such as allowedElements and disallowedElements. See https://github.com/remarkjs/react-markdown?tab=readme-ov-file#options for options */ + reactMarkdownProps?: Options; + /** Allows passing additional props down to remark-gfm. See https://github.com/remarkjs/remark-gfm?tab=readme-ov-file#options for options */ + remarkGfmProps?: Options; + /** Whether to strip out images in markdown */ + hasNoImages?: boolean; + /** Sets background colors to be appropriate on primary chatbot background */ + isPrimary?: boolean; + /** Custom component to render when markdown is disabled */ + textComponent?: ReactNode; + /** Flag indicating whether content should retain various styles of its context (typically font-size and text color). */ + shouldRetainStyles?: boolean; +} + +export const MarkdownContent: FunctionComponent = ({ + content, + isMarkdownDisabled, + codeBlockProps, + tableProps, + openLinkInNewTab = true, + additionalRehypePlugins = [], + additionalRemarkPlugins = [], + linkProps, + reactMarkdownProps, + remarkGfmProps, + hasNoImages = false, + isPrimary, + textComponent, + shouldRetainStyles +}: MarkdownContentProps) => { + let rehypePlugins: PluggableList = [rehypeUnwrapImages, rehypeMoveImagesOutOfParagraphs, rehypeHighlight]; + if (openLinkInNewTab) { + rehypePlugins = rehypePlugins.concat([[rehypeExternalLinks, { target: '_blank' }, rehypeSanitize]]); + } + if (additionalRehypePlugins) { + rehypePlugins.push(...additionalRehypePlugins); + } + + const disallowedElements = hasNoImages ? ['img'] : []; + if (reactMarkdownProps && reactMarkdownProps.disallowedElements) { + disallowedElements.push(...reactMarkdownProps.disallowedElements); + } + + if (isMarkdownDisabled) { + if (textComponent) { + return <>{textComponent}; + } + return ( + + {content} + + ); + } + + return ( + { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { node, ...rest } = props; + return ( +
+ ); + }, + p: (props) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { node, ...rest } = props; + return ( + + ); + }, + code: ({ children, ...props }) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { node, ...codeProps } = props; + return ( + + {children} + + ); + }, + h1: (props) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { node, ...rest } = props; + return ; + }, + h2: (props) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { node, ...rest } = props; + return ; + }, + h3: (props) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { node, ...rest } = props; + return ; + }, + h4: (props) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { node, ...rest } = props; + return ; + }, + h5: (props) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { node, ...rest } = props; + return ; + }, + h6: (props) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { node, ...rest } = props; + return ; + }, + blockquote: (props) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { node, ...rest } = props; + return ( + + ); + }, + ul: (props) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { node, ...rest } = props; + return ; + }, + ol: (props) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { node, ...rest } = props; + return ; + }, + li: (props) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { node, ...rest } = props; + return ; + }, + // table requires node attribute for calculating headers for mobile breakpoint + table: (props) => ( + + ), + tbody: (props) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { node, ...rest } = props; + return ; + }, + thead: (props) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { node, ...rest } = props; + return ; + }, + tr: (props) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { node, ...rest } = props; + return ; + }, + td: (props) => { + // Conflicts with Td type + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { node, width, ...rest } = props; + return ; + }, + th: (props) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { node, ...rest } = props; + return ; + }, + img: (props) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { node, ...rest } = props; + return ; + }, + a: (props) => { + // node is just the details of the document structure - not needed + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { node, ...rest } = props; + return ( + // some a types conflict with ButtonProps, but it's ok because we are using an a tag + // there are too many to handle manually + + {props.children} + + ); + }, + // used for footnotes + sup: (props) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { node, ...rest } = props; + return ; + } + }} + remarkPlugins={[[remarkGfm, { ...remarkGfmProps }], ...additionalRemarkPlugins]} + rehypePlugins={rehypePlugins} + {...reactMarkdownProps} + remarkRehypeOptions={{ + // removes sr-only class from footnote labels applied by default + footnoteLabelProperties: { className: [''] }, + ...reactMarkdownProps?.remarkRehypeOptions + }} + disallowedElements={disallowedElements} + > + {content} + + ); +}; + +export default MarkdownContent; diff --git a/packages/module/src/MarkdownContent/index.ts b/packages/module/src/MarkdownContent/index.ts new file mode 100644 index 000000000..8269e6e03 --- /dev/null +++ b/packages/module/src/MarkdownContent/index.ts @@ -0,0 +1,2 @@ +export { default } from './MarkdownContent'; +export * from './MarkdownContent'; diff --git a/packages/module/src/Message/CodeBlockMessage/CodeBlockMessage.scss b/packages/module/src/Message/CodeBlockMessage/CodeBlockMessage.scss index 1c7a905eb..d62bf567b 100644 --- a/packages/module/src/Message/CodeBlockMessage/CodeBlockMessage.scss +++ b/packages/module/src/Message/CodeBlockMessage/CodeBlockMessage.scss @@ -75,6 +75,10 @@ overflow: hidden !important; } } + + &.pf-m-markdown .pf-v6-c-code-block__code { + font-size: inherit; + } } .pf-chatbot__message-inline-code { diff --git a/packages/module/src/Message/CodeBlockMessage/CodeBlockMessage.tsx b/packages/module/src/Message/CodeBlockMessage/CodeBlockMessage.tsx index 7eb295bd3..1e051ff42 100644 --- a/packages/module/src/Message/CodeBlockMessage/CodeBlockMessage.tsx +++ b/packages/module/src/Message/CodeBlockMessage/CodeBlockMessage.tsx @@ -19,6 +19,7 @@ import { import { CheckIcon } from '@patternfly/react-icons/dist/esm/icons/check-icon'; import { CopyIcon } from '@patternfly/react-icons/dist/esm/icons/copy-icon'; +import { css } from '@patternfly/react-styles'; export interface CodeBlockMessageProps { /** Content rendered in code block */ @@ -41,6 +42,8 @@ export interface CodeBlockMessageProps { customActions?: React.ReactNode; /** Sets background colors to be appropriate on primary chatbot background */ isPrimary?: boolean; + /** Flag indicating that the content should retain message styles when using Markdown. */ + shouldRetainStyles?: boolean; } const DEFAULT_EXPANDED_TEXT = 'Show less'; @@ -57,6 +60,7 @@ const CodeBlockMessage = ({ collapsedText = DEFAULT_COLLAPSED_TEXT, customActions, isPrimary, + shouldRetainStyles, ...props }: CodeBlockMessageProps) => { const [copied, setCopied] = useState(false); @@ -138,7 +142,7 @@ const CodeBlockMessage = ({ ); return ( -
+
<> diff --git a/packages/module/src/Message/LinkMessage/LinkMessage.scss b/packages/module/src/Message/LinkMessage/LinkMessage.scss new file mode 100644 index 000000000..2f8ac675f --- /dev/null +++ b/packages/module/src/Message/LinkMessage/LinkMessage.scss @@ -0,0 +1,5 @@ +.pf-v6-c-button.pf-m-link.pf-m-inline { + &.pf-m-markdown { + font-size: inherit; + } +} diff --git a/packages/module/src/Message/LinkMessage/LinkMessage.tsx b/packages/module/src/Message/LinkMessage/LinkMessage.tsx index de32e46b6..3e89d86b2 100644 --- a/packages/module/src/Message/LinkMessage/LinkMessage.tsx +++ b/packages/module/src/Message/LinkMessage/LinkMessage.tsx @@ -5,8 +5,21 @@ import { Button, ButtonProps } from '@patternfly/react-core'; import { ExternalLinkSquareAltIcon } from '@patternfly/react-icons'; import { ExtraProps } from 'react-markdown'; +import { css } from '@patternfly/react-styles'; -const LinkMessage = ({ children, target, href, id, ...props }: ButtonProps & ExtraProps) => { +export interface LinkMessageProps { + /** Flag indicating that the content should retain message styles when using Markdown. */ + shouldRetainStyles?: boolean; +} + +const LinkMessage = ({ + children, + target, + href, + id, + shouldRetainStyles, + ...props +}: LinkMessageProps & ButtonProps & ExtraProps) => { if (target === '_blank') { return ( @@ -28,7 +42,15 @@ const LinkMessage = ({ children, target, href, id, ...props }: ButtonProps & Ext return ( // need to explicitly call this out or id doesn't seem to get passed - required for footnotes - ); diff --git a/packages/module/src/Message/ListMessage/ListMessage.scss b/packages/module/src/Message/ListMessage/ListMessage.scss index 0dfb7fcd5..f3dad3947 100644 --- a/packages/module/src/Message/ListMessage/ListMessage.scss +++ b/packages/module/src/Message/ListMessage/ListMessage.scss @@ -13,6 +13,14 @@ li { font-size: var(--pf-t--global--font--size--md); } + + &.pf-m-markdown { + .pf-v6-c-list, + ul, + li { + font-size: inherit; + } + } } .pf-chatbot__message--user { diff --git a/packages/module/src/Message/ListMessage/OrderedListMessage.tsx b/packages/module/src/Message/ListMessage/OrderedListMessage.tsx index cc5fbff23..0bdd8e9f4 100644 --- a/packages/module/src/Message/ListMessage/OrderedListMessage.tsx +++ b/packages/module/src/Message/ListMessage/OrderedListMessage.tsx @@ -4,9 +4,23 @@ import { ExtraProps } from 'react-markdown'; import { List, ListComponent, OrderType } from '@patternfly/react-core'; +import { css } from '@patternfly/react-styles'; -const OrderedListMessage = ({ children, start }: JSX.IntrinsicElements['ol'] & ExtraProps) => ( -
+export interface OrderedListMessageProps { + /** The ordered list content */ + children?: React.ReactNode; + /** The number to start the ordered list at. */ + start?: number; + /** Flag indicating that the content should retain message styles when using Markdown. */ + shouldRetainStyles?: boolean; +} + +const OrderedListMessage = ({ + children, + start, + shouldRetainStyles +}: OrderedListMessageProps & JSX.IntrinsicElements['ol'] & ExtraProps) => ( +
{children} diff --git a/packages/module/src/Message/ListMessage/UnorderedListMessage.tsx b/packages/module/src/Message/ListMessage/UnorderedListMessage.tsx index b30875cf2..a51b2f17f 100644 --- a/packages/module/src/Message/ListMessage/UnorderedListMessage.tsx +++ b/packages/module/src/Message/ListMessage/UnorderedListMessage.tsx @@ -4,9 +4,19 @@ import { ExtraProps } from 'react-markdown'; import { List } from '@patternfly/react-core'; +import { css } from '@patternfly/react-styles'; +export interface UnrderedListMessageProps { + /** The ordered list content */ + children?: React.ReactNode; + /** Flag indicating that the content should retain message styles when using Markdown. */ + shouldRetainStyles?: boolean; +} -const UnorderedListMessage = ({ children }: JSX.IntrinsicElements['ul'] & ExtraProps) => ( -
+const UnorderedListMessage = ({ + children, + shouldRetainStyles +}: UnrderedListMessageProps & JSX.IntrinsicElements['ul'] & ExtraProps) => ( +
{children}
); diff --git a/packages/module/src/Message/Message.tsx b/packages/module/src/Message/Message.tsx index c487c59b2..8b298c325 100644 --- a/packages/module/src/Message/Message.tsx +++ b/packages/module/src/Message/Message.tsx @@ -3,14 +3,12 @@ // ============================================================================ import { forwardRef, ReactNode, useEffect, useState } from 'react'; import type { FunctionComponent, HTMLProps, MouseEvent as ReactMouseEvent, Ref } from 'react'; -import Markdown, { Options } from 'react-markdown'; -import remarkGfm from 'remark-gfm'; +import { Options } from 'react-markdown'; import { AlertProps, Avatar, AvatarProps, ButtonProps, - ContentVariants, FormProps, Label, LabelGroupProps, @@ -18,42 +16,25 @@ import { Truncate } from '@patternfly/react-core'; import MessageLoading from './MessageLoading'; -import CodeBlockMessage, { CodeBlockMessageProps } from './CodeBlockMessage/CodeBlockMessage'; -import TextMessage from './TextMessage/TextMessage'; +import { CodeBlockMessageProps } from './CodeBlockMessage/CodeBlockMessage'; import FileDetailsLabel from '../FileDetailsLabel/FileDetailsLabel'; import ResponseActions, { ActionProps } from '../ResponseActions/ResponseActions'; import SourcesCard, { SourcesCardProps } from '../SourcesCard'; -import ListItemMessage from './ListMessage/ListItemMessage'; -import UnorderedListMessage from './ListMessage/UnorderedListMessage'; -import OrderedListMessage from './ListMessage/OrderedListMessage'; import QuickStartTile from './QuickStarts/QuickStartTile'; import { QuickStart, QuickstartAction } from './QuickStarts/types'; import QuickResponse from './QuickResponse/QuickResponse'; import UserFeedback, { UserFeedbackProps } from './UserFeedback/UserFeedback'; import UserFeedbackComplete, { UserFeedbackCompleteProps } from './UserFeedback/UserFeedbackComplete'; -import TableMessage from './TableMessage/TableMessage'; -import TrMessage from './TableMessage/TrMessage'; -import TdMessage from './TableMessage/TdMessage'; -import TbodyMessage from './TableMessage/TbodyMessage'; -import TheadMessage from './TableMessage/TheadMessage'; -import ThMessage from './TableMessage/ThMessage'; import { TableProps } from '@patternfly/react-table'; -import ImageMessage from './ImageMessage/ImageMessage'; -import rehypeUnwrapImages from 'rehype-unwrap-images'; -import rehypeExternalLinks from 'rehype-external-links'; -import rehypeSanitize from 'rehype-sanitize'; -import rehypeHighlight from 'rehype-highlight'; // see the full list of styles here: https://highlightjs.org/examples import 'highlight.js/styles/vs2015.css'; import { PluggableList } from 'unified'; -import LinkMessage from './LinkMessage/LinkMessage'; import ErrorMessage from './ErrorMessage/ErrorMessage'; import MessageInput from './MessageInput'; -import { rehypeMoveImagesOutOfParagraphs } from './Plugins/rehypeMoveImagesOutOfParagraphs'; import ToolResponse, { ToolResponseProps } from '../ToolResponse'; import DeepThinking, { DeepThinkingProps } from '../DeepThinking'; -import SuperscriptMessage from './SuperscriptMessage/SuperscriptMessage'; import ToolCall, { ToolCallProps } from '../ToolCall'; +import MarkdownContent from '../MarkdownContent'; export interface MessageAttachment { /** Name of file attached to the message */ @@ -267,14 +248,8 @@ export const MessageBase: FunctionComponent = ({ }, [content]); const { beforeMainContent, afterMainContent, endContent } = extraContent || {}; - let rehypePlugins: PluggableList = [rehypeUnwrapImages, rehypeMoveImagesOutOfParagraphs, rehypeHighlight]; - if (openLinkInNewTab) { - rehypePlugins = rehypePlugins.concat([[rehypeExternalLinks, { target: '_blank' }, rehypeSanitize]]); - } - if (additionalRehypePlugins) { - rehypePlugins.push(...additionalRehypePlugins); - } - let avatarClassName; + + let avatarClassName: string | undefined; if (avatarProps && 'className' in avatarProps) { const { className, ...rest } = avatarProps; avatarClassName = className; @@ -284,157 +259,22 @@ export const MessageBase: FunctionComponent = ({ const date = new Date(); const dateString = timestamp ?? `${date.toLocaleDateString()} ${date.toLocaleTimeString()}`; - const disallowedElements = role === 'user' && hasNoImagesInUserMessages ? ['img'] : []; - if (reactMarkdownProps && reactMarkdownProps.disallowedElements) { - disallowedElements.push(...reactMarkdownProps.disallowedElements); - } - - const handleMarkdown = () => { - if (isMarkdownDisabled) { - return ( - - {messageText} - - ); - } - return ( - { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { node, ...rest } = props; - return
; - }, - p: (props) => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { node, ...rest } = props; - return ; - }, - code: ({ children, ...props }) => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { node, ...codeProps } = props; - return ( - - {children} - - ); - }, - h1: (props) => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { node, ...rest } = props; - return ; - }, - h2: (props) => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { node, ...rest } = props; - return ; - }, - h3: (props) => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { node, ...rest } = props; - return ; - }, - h4: (props) => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { node, ...rest } = props; - return ; - }, - h5: (props) => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { node, ...rest } = props; - return ; - }, - h6: (props) => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { node, ...rest } = props; - return ; - }, - blockquote: (props) => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { node, ...rest } = props; - return ; - }, - ul: (props) => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { node, ...rest } = props; - return ; - }, - ol: (props) => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { node, ...rest } = props; - return ; - }, - li: (props) => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { node, ...rest } = props; - return ; - }, - // table requires node attribute for calculating headers for mobile breakpoint - table: (props) => , - tbody: (props) => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { node, ...rest } = props; - return ; - }, - thead: (props) => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { node, ...rest } = props; - return ; - }, - tr: (props) => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { node, ...rest } = props; - return ; - }, - td: (props) => { - // Conflicts with Td type - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { node, width, ...rest } = props; - return ; - }, - th: (props) => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { node, ...rest } = props; - return ; - }, - img: (props) => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { node, ...rest } = props; - return ; - }, - a: (props) => { - // node is just the details of the document structure - not needed - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { node, ...rest } = props; - return ( - // some a types conflict with ButtonProps, but it's ok because we are using an a tag - // there are too many to handle manually - - {props.children} - - ); - }, - // used for footnotes - sup: (props) => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { node, ...rest } = props; - return ; - } - }} - remarkPlugins={[[remarkGfm, { ...remarkGfmProps }], ...additionalRemarkPlugins]} - rehypePlugins={rehypePlugins} - {...reactMarkdownProps} - remarkRehypeOptions={{ - // removes sr-only class from footnote labels applied by default - footnoteLabelProperties: { className: [''] }, - ...reactMarkdownProps?.remarkRehypeOptions - }} - disallowedElements={disallowedElements} - > - {messageText} - - ); - }; + const handleMarkdown = () => ( + + ); const renderMessage = () => { if (isLoading) { diff --git a/packages/module/src/Message/TableMessage/TableMessage.scss b/packages/module/src/Message/TableMessage/TableMessage.scss index 16a4fba0f..b53a5e66f 100644 --- a/packages/module/src/Message/TableMessage/TableMessage.scss +++ b/packages/module/src/Message/TableMessage/TableMessage.scss @@ -25,4 +25,15 @@ .pf-v6-c-table__tr:last-of-type { border-block-end: 0; } + + &.pf-m-markdown { + table, + tbody, + td, + thead, + th, + tr { + font-size: inherit; + } + } } diff --git a/packages/module/src/Message/TableMessage/TableMessage.tsx b/packages/module/src/Message/TableMessage/TableMessage.tsx index befda02c4..251fe0d1e 100644 --- a/packages/module/src/Message/TableMessage/TableMessage.tsx +++ b/packages/module/src/Message/TableMessage/TableMessage.tsx @@ -5,6 +5,7 @@ import { Children, cloneElement } from 'react'; import { ExtraProps } from 'react-markdown'; import { Table, TableProps } from '@patternfly/react-table'; +import { css } from '@patternfly/react-styles'; interface Properties { line: number; @@ -20,10 +21,20 @@ export interface TableNode { } export interface TableMessageProps { + /** Content of the table */ + children?: React.ReactNode; + /** Flag indicating whether primary styles should be applied. */ isPrimary?: boolean; + /** Flag indicating that the content should retain message styles when using Markdown. */ + shouldRetainStyles?: boolean; } -const TableMessage = ({ children, isPrimary, ...props }: Omit & ExtraProps & TableMessageProps) => { +const TableMessage = ({ + children, + isPrimary, + shouldRetainStyles, + ...props +}: Omit & ExtraProps & TableMessageProps) => { const { className, ...rest } = props; // This allows us to parse the nested data we get back from the 3rd party Markdown parser @@ -76,7 +87,12 @@ const TableMessage = ({ children, isPrimary, ...props }: Omit {modifyChildren(children)} diff --git a/packages/module/src/Message/TextMessage/TextMessage.scss b/packages/module/src/Message/TextMessage/TextMessage.scss index df022f907..59ed28641 100644 --- a/packages/module/src/Message/TextMessage/TextMessage.scss +++ b/packages/module/src/Message/TextMessage/TextMessage.scss @@ -53,6 +53,14 @@ background-color: var(--pf-t--global--background--color--secondary--default); } } + + &.pf-m-markdown { + display: block; + } + &.pf-m-markdown > [class^='pf-v6-c-content'] { + font-size: inherit; + color: inherit; + } } // ============================================================================ diff --git a/packages/module/src/Message/TextMessage/TextMessage.tsx b/packages/module/src/Message/TextMessage/TextMessage.tsx index 6b8a23fa5..8ed20ef9b 100644 --- a/packages/module/src/Message/TextMessage/TextMessage.tsx +++ b/packages/module/src/Message/TextMessage/TextMessage.tsx @@ -4,19 +4,46 @@ import { ExtraProps } from 'react-markdown'; import { Content, ContentProps } from '@patternfly/react-core'; +import { css } from '@patternfly/react-styles'; export interface TextMessageProps { + /** The text message content */ + children?: React.ReactNode; + /** Flag indicating whether primary styling is applied. */ isPrimary?: boolean; + /** The wrapper component to use for the PatternFly Content component. Defaults to a div. */ + component?: + | 'h1' + | 'h2' + | 'h3' + | 'h4' + | 'h5' + | 'h6' + | 'p' + | 'a' + | 'small' + | 'blockquote' + | 'pre' + | 'hr' + | 'ul' + | 'ol' + | 'dl' + | 'li' + | 'dt' + | 'dd'; + /** Flag indicating that the content should retain message styles when using Markdown. */ + shouldRetainStyles?: boolean; } const TextMessage = ({ component, children, isPrimary, + shouldRetainStyles, ...props }: Omit & ExtraProps & TextMessageProps) => ( - - + + {children} diff --git a/packages/module/src/ToolCall/ToolCall.test.tsx b/packages/module/src/ToolCall/ToolCall.test.tsx index 91f4e7432..fa418d78f 100644 --- a/packages/module/src/ToolCall/ToolCall.test.tsx +++ b/packages/module/src/ToolCall/ToolCall.test.tsx @@ -232,4 +232,44 @@ describe('ToolCall', () => { expect(toggleButton).toHaveAttribute('aria-expanded', 'false'); expect(screen.queryByText('Expandable Content')).not.toBeVisible(); }); + + it('should render titleText as markdown when isTitleMarkdown is true', () => { + const titleText = '**Bold title**'; + const { container } = render(); + expect(container.querySelector('strong')).toBeTruthy(); + expect(screen.getByText('Bold title')).toBeTruthy(); + }); + + it('should not render titleText as markdown when isTitleMarkdown is false', () => { + const titleText = '**Bold title**'; + render(); + expect(screen.getByText('**Bold title**')).toBeTruthy(); + }); + + it('should render expandableContent as markdown when isExpandableContentMarkdown is true', async () => { + const user = userEvent.setup(); + const expandableContent = '**Bold expandable content**'; + const { container } = render( + + ); + await user.click(screen.getByRole('button', { name: defaultProps.titleText })); + expect(container.querySelector('strong')).toBeTruthy(); + expect(screen.getByText('Bold expandable content')).toBeTruthy(); + }); + + it('should not render expandableContent as markdown when isExpandableContentMarkdown is false', async () => { + const user = userEvent.setup(); + const expandableContent = '**Bold expandable content**'; + render(); + await user.click(screen.getByRole('button', { name: defaultProps.titleText })); + expect(screen.getByText('**Bold expandable content**')).toBeTruthy(); + }); + + it('should pass markdownContentProps to MarkdownContent component', () => { + const titleText = '**Bold title**'; + const { container } = render( + + ); + expect(container.querySelector('.pf-m-primary')).toBeTruthy(); + }); }); diff --git a/packages/module/src/ToolCall/ToolCall.tsx b/packages/module/src/ToolCall/ToolCall.tsx index 774984813..afdc29db0 100644 --- a/packages/module/src/ToolCall/ToolCall.tsx +++ b/packages/module/src/ToolCall/ToolCall.tsx @@ -19,6 +19,8 @@ import { Spinner, SpinnerProps } from '@patternfly/react-core'; +import MarkdownContent from '../MarkdownContent'; +import type { MarkdownContentProps } from '../MarkdownContent'; export interface ToolCallProps { /** Title text for the tool call. */ @@ -61,6 +63,14 @@ export interface ToolCallProps { cardFooterProps?: CardFooterProps; /** Additional props for the expandable section when expandableContent is passed. */ expandableSectionProps?: Omit; + /** Whether to enable markdown rendering for titleText. When true, titleText will be parsed as markdown. */ + isTitleMarkdown?: boolean; + /** Whether to enable markdown rendering for expandableContent. When true and expandableContent is a string, it will be parsed as markdown. */ + isExpandableContentMarkdown?: boolean; + /** Props passed to MarkdownContent component when markdown is enabled */ + markdownContentProps?: Omit; + /** Whether to retain styles in the MarkdownContent component. Defaults to false. */ + shouldRetainStyles?: boolean; } export const ToolCall: FunctionComponent = ({ @@ -83,7 +93,11 @@ export const ToolCall: FunctionComponent = ({ cardBodyProps, cardFooterProps, expandableSectionProps, - spinnerProps + spinnerProps, + isTitleMarkdown, + isExpandableContentMarkdown, + markdownContentProps, + shouldRetainStyles = false }: ToolCallProps) => { const [isExpanded, setIsExpanded] = useState(isDefaultExpanded); @@ -91,6 +105,13 @@ export const ToolCall: FunctionComponent = ({ setIsExpanded(isExpanded); }; + const renderTitle = () => { + if (isTitleMarkdown) { + return ; + } + return titleText; + }; + const titleContent = ( {isLoading ? ( @@ -99,10 +120,23 @@ export const ToolCall: FunctionComponent = ({ {{loadingText}} ) : ( - {titleText} + {renderTitle()} )} ); + + const renderExpandableContent = () => { + if (isExpandableContentMarkdown && typeof expandableContent === 'string') { + return ( + + ); + } + return expandableContent; + }; const defaultActions = ( <> @@ -138,7 +172,7 @@ export const ToolCall: FunctionComponent = ({ isIndented {...expandableSectionProps} > - {expandableContent} + {renderExpandableContent()} ) : ( titleContent diff --git a/packages/module/src/ToolResponse/ToolResponse.scss b/packages/module/src/ToolResponse/ToolResponse.scss index 3d8a5b547..ce10df3ab 100644 --- a/packages/module/src/ToolResponse/ToolResponse.scss +++ b/packages/module/src/ToolResponse/ToolResponse.scss @@ -34,3 +34,13 @@ --pf-v6-c-divider--Color: var(--pf-t--global--border--color--default); } } + +.pf-chatbot__tool-response-expandable-section .pf-v6-c-expandable-section__toggle .pf-m-markdown { + padding: inherit; +} + +.pf-chatbot__tool-response { + .pf-chatbot__message-image { + max-width: 100%; + } +} diff --git a/packages/module/src/ToolResponse/ToolResponse.test.tsx b/packages/module/src/ToolResponse/ToolResponse.test.tsx index 8d4e3135d..6737363a3 100644 --- a/packages/module/src/ToolResponse/ToolResponse.test.tsx +++ b/packages/module/src/ToolResponse/ToolResponse.test.tsx @@ -149,4 +149,79 @@ describe('ToolResponse', () => { expect(screen.getByText(defaultProps.cardTitle)).not.toBeVisible(); expect(screen.getByText(defaultProps.cardBody)).not.toBeVisible(); }); + + it('should render toggleContent as markdown when isToggleContentMarkdown is true', () => { + const toggleContent = '**Bold toggle**'; + const { container } = render( + + ); + expect(container.querySelector('strong')).toBeTruthy(); + expect(screen.getByText('Bold toggle')).toBeTruthy(); + }); + + it('should not render toggleContent as markdown when isToggleContentMarkdown is false', () => { + const toggleContent = '**Bold toggle**'; + render(); + expect(screen.getByText('**Bold toggle**')).toBeTruthy(); + }); + + it('should render subheading as markdown when isSubheadingMarkdown is true', () => { + const subheading = '**Bold subheading**'; + const { container } = render(); + expect(container.querySelector('strong')).toBeTruthy(); + expect(screen.getByText('Bold subheading')).toBeTruthy(); + }); + + it('should not render subheading as markdown when isSubheadingMarkdown is false', () => { + const subheading = '**Bold subheading**'; + render(); + expect(screen.getByText('**Bold subheading**')).toBeTruthy(); + }); + + it('should render body as markdown when isBodyMarkdown is true', () => { + const body = '**Bold body**'; + const { container } = render(); + expect(container.querySelector('strong')).toBeTruthy(); + expect(screen.getByText('Bold body')).toBeTruthy(); + }); + + it('should not render body as markdown when isBodyMarkdown is false', () => { + const body = '**Bold body**'; + render(); + expect(screen.getByText('**Bold body**')).toBeTruthy(); + }); + + it('should render cardTitle as markdown when isCardTitleMarkdown is true', () => { + const cardTitle = '**Bold card title**'; + const { container } = render(); + expect(container.querySelector('strong')).toBeTruthy(); + expect(screen.getByText('Bold card title')).toBeTruthy(); + }); + + it('should not render cardTitle as markdown when isCardTitleMarkdown is false', () => { + const cardTitle = '**Bold card title**'; + render(); + expect(screen.getByText('**Bold card title**')).toBeTruthy(); + }); + + it('should render cardBody as markdown when isCardBodyMarkdown is true', () => { + const cardBody = '**Bold card body**'; + const { container } = render(); + expect(container.querySelector('strong')).toBeTruthy(); + expect(screen.getByText('Bold card body')).toBeTruthy(); + }); + + it('should not render cardBody as markdown when isCardBodyMarkdown is false', () => { + const cardBody = '**Bold card body**'; + render(); + expect(screen.getByText('**Bold card body**')).toBeTruthy(); + }); + + it('should pass markdownContentProps to MarkdownContent component', () => { + const body = '**Bold body**'; + const { container } = render( + + ); + expect(container.querySelector('.pf-m-primary')).toBeTruthy(); + }); }); diff --git a/packages/module/src/ToolResponse/ToolResponse.tsx b/packages/module/src/ToolResponse/ToolResponse.tsx index 39bfdb269..6c3289872 100644 --- a/packages/module/src/ToolResponse/ToolResponse.tsx +++ b/packages/module/src/ToolResponse/ToolResponse.tsx @@ -14,6 +14,8 @@ import { ExpandableSectionProps } from '@patternfly/react-core'; import { useState, type FunctionComponent } from 'react'; +import MarkdownContent from '../MarkdownContent'; +import type { MarkdownContentProps } from '../MarkdownContent'; export interface ToolResponseProps { /** Toggle content shown for expandable section */ @@ -42,6 +44,20 @@ export interface ToolResponseProps { toolResponseCardDividerProps?: DividerProps; /** Additional props passed to tool response card title */ toolResponseCardTitleProps?: CardTitleProps; + /** Whether to enable markdown rendering for toggleContent. When true and toggleContent is a string, it will be parsed as markdown. */ + isToggleContentMarkdown?: boolean; + /** Whether to enable markdown rendering for subheading. When true, subheading will be parsed as markdown. */ + isSubheadingMarkdown?: boolean; + /** Whether to enable markdown rendering for body. When true and body is a string, it will be parsed as markdown. */ + isBodyMarkdown?: boolean; + /** Whether to enable markdown rendering for cardBody. When true and cardBody is a string, it will be parsed as markdown. */ + isCardBodyMarkdown?: boolean; + /** Whether to enable markdown rendering for cardTitle. When true and cardTitle is a string, it will be parsed as markdown. */ + isCardTitleMarkdown?: boolean; + /** Props passed to MarkdownContent component when markdown is enabled */ + markdownContentProps?: Omit; + /** Whether to retain styles in the MarkdownContent component. Defaults to false. */ + shouldRetainStyles?: boolean; } export const ToolResponse: FunctionComponent = ({ @@ -57,7 +73,14 @@ export const ToolResponse: FunctionComponent = ({ toolResponseCardBodyProps, toolResponseCardDividerProps, toolResponseCardProps, - toolResponseCardTitleProps + toolResponseCardTitleProps, + isToggleContentMarkdown, + isSubheadingMarkdown, + isBodyMarkdown, + isCardBodyMarkdown, + isCardTitleMarkdown, + markdownContentProps, + shouldRetainStyles = false }: ToolResponseProps) => { const [isExpanded, setIsExpanded] = useState(isDefaultExpanded); @@ -65,11 +88,60 @@ export const ToolResponse: FunctionComponent = ({ setIsExpanded(isExpanded); }; + const renderToggleContent = () => { + if (isToggleContentMarkdown && typeof toggleContent === 'string') { + return ( + + ); + } + return toggleContent; + }; + + const renderSubheading = () => { + if (!subheading) { + return null; + } + if (isSubheadingMarkdown) { + return ; + } + return subheading; + }; + + const renderBody = () => { + if (!body) { + return null; + } + if (isBodyMarkdown && typeof body === 'string') { + return ; + } + return body; + }; + + const renderCardTitle = () => { + if (!cardTitle) { + return null; + } + if (isCardTitleMarkdown && typeof cardTitle === 'string') { + return ; + } + return cardTitle; + }; + + const renderCardBody = () => { + if (!cardBody) { + return null; + } + if (isCardBodyMarkdown && typeof cardBody === 'string') { + return ; + } + return cardBody; + }; + return ( = ({
{subheading && (
- {subheading} + {renderSubheading()}
)} - {body &&
{body}
} + {body &&
{renderBody()}
} {(cardTitle || cardBody) && ( - {cardTitle && {cardTitle}} + {cardTitle && {renderCardTitle()}} {cardTitle && cardBody && } - {cardBody && {cardBody}} + {cardBody && {renderCardBody()}} )}
diff --git a/packages/module/src/index.ts b/packages/module/src/index.ts index 757c148bd..c692ab34d 100644 --- a/packages/module/src/index.ts +++ b/packages/module/src/index.ts @@ -63,6 +63,9 @@ export * from './ImagePreview'; export { default as LoadingMessage } from './LoadingMessage'; export * from './LoadingMessage'; +export { default as MarkdownContent } from './MarkdownContent'; +export * from './MarkdownContent'; + export { default as Message } from './Message'; export * from './Message'; diff --git a/packages/module/src/main.scss b/packages/module/src/main.scss index 42701d28d..e68020366 100644 --- a/packages/module/src/main.scss +++ b/packages/module/src/main.scss @@ -20,6 +20,7 @@ @import './Message/Message'; @import './Message/CodeBlockMessage/CodeBlockMessage'; @import './Message/ImageMessage/ImageMessage'; +@import './Message/LinkMessage/LinkMessage'; @import './Message/TextMessage/TextMessage'; @import './Message/ListMessage/ListMessage'; @import './Message/TableMessage/TableMessage'; diff --git a/yarn.lock b/yarn.lock index bafd8ce08..6a2da9002 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1005,13 +1005,6 @@ "@discoveryjs/json-ext@^0.5.0", "@discoveryjs/json-ext@0.5.7": version "0.5.7" -"@emnapi/runtime@^0.44.0": - version "0.44.0" - resolved "https://registry.npmjs.org/@emnapi/runtime/-/runtime-0.44.0.tgz" - integrity sha512-ZX/etZEZw8DR7zAB1eVQT40lNo0jeqpb6dCgOvctB6FIQ5PoXfMuNY8+ayQfu8tNQbAB8gQWSSJupR8NxeiZXw== - dependencies: - tslib "^2.4.0" - "@eslint-community/eslint-utils@^4.2.0": version "4.4.0" dependencies: @@ -1064,112 +1057,11 @@ optionalDependencies: "@img/sharp-libvips-darwin-arm64" "1.0.0" -"@img/sharp-darwin-x64@0.33.0": - version "0.33.0" - resolved "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.0.tgz" - integrity sha512-pu/nvn152F3qbPeUkr+4e9zVvEhD3jhwzF473veQfMPkOYo9aoWXSfdZH/E6F+nYC3qvFjbxbvdDbUtEbghLqw== - optionalDependencies: - "@img/sharp-libvips-darwin-x64" "1.0.0" - "@img/sharp-libvips-darwin-arm64@1.0.0": version "1.0.0" resolved "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.0.tgz" integrity sha512-VzYd6OwnUR81sInf3alj1wiokY50DjsHz5bvfnsFpxs5tqQxESoHtJO6xyksDs3RIkyhMWq2FufXo6GNSU9BMw== -"@img/sharp-libvips-darwin-x64@1.0.0": - version "1.0.0" - resolved "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.0.tgz" - integrity sha512-dD9OznTlHD6aovRswaPNEy8dKtSAmNo4++tO7uuR4o5VxbVAOoEQ1uSmN4iFAdQneTHws1lkTZeiXPrcCkh6IA== - -"@img/sharp-libvips-linux-arm@1.0.0": - version "1.0.0" - resolved "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.0.tgz" - integrity sha512-VwgD2eEikDJUk09Mn9Dzi1OW2OJFRQK+XlBTkUNmAWPrtj8Ly0yq05DFgu1VCMx2/DqCGQVi5A1dM9hTmxf3uw== - -"@img/sharp-libvips-linux-arm64@1.0.0": - version "1.0.0" - resolved "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.0.tgz" - integrity sha512-xTYThiqEZEZc0PRU90yVtM3KE7lw1bKdnDQ9kCTHWbqWyHOe4NpPOtMGy27YnN51q0J5dqRrvicfPbALIOeAZA== - -"@img/sharp-libvips-linux-s390x@1.0.0": - version "1.0.0" - resolved "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.0.tgz" - integrity sha512-o9E46WWBC6JsBlwU4QyU9578G77HBDT1NInd+aERfxeOPbk0qBZHgoDsQmA2v9TbqJRWzoBPx1aLOhprBMgPjw== - -"@img/sharp-libvips-linux-x64@1.0.0": - version "1.0.0" - resolved "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.0.tgz" - integrity sha512-naldaJy4hSVhWBgEjfdBY85CAa4UO+W1nx6a1sWStHZ7EUfNiuBTTN2KUYT5dH1+p/xij1t2QSXfCiFJoC5S/Q== - -"@img/sharp-libvips-linuxmusl-arm64@1.0.0": - version "1.0.0" - resolved "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.0.tgz" - integrity sha512-OdorplCyvmSAPsoJLldtLh3nLxRrkAAAOHsGWGDYfN0kh730gifK+UZb3dWORRa6EusNqCTjfXV4GxvgJ/nPDQ== - -"@img/sharp-libvips-linuxmusl-x64@1.0.0": - version "1.0.0" - resolved "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.0.tgz" - integrity sha512-FW8iK6rJrg+X2jKD0Ajhjv6y74lToIBEvkZhl42nZt563FfxkCYacrXZtd+q/sRQDypQLzY5WdLkVTbJoPyqNg== - -"@img/sharp-linux-arm@0.33.0": - version "0.33.0" - resolved "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.0.tgz" - integrity sha512-4horD3wMFd5a0ddbDY8/dXU9CaOgHjEHALAddXgafoR5oWq5s8X61PDgsSeh4Qupsdo6ycfPPSSNBrfVQnwwrg== - optionalDependencies: - "@img/sharp-libvips-linux-arm" "1.0.0" - -"@img/sharp-linux-arm64@0.33.0": - version "0.33.0" - resolved "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.0.tgz" - integrity sha512-dcomVSrtgF70SyOr8RCOCQ8XGVThXwe71A1d8MGA+mXEVRJ/J6/TrCbBEJh9ddcEIIsrnrkolaEvYSHqVhswQw== - optionalDependencies: - "@img/sharp-libvips-linux-arm64" "1.0.0" - -"@img/sharp-linux-s390x@0.33.0": - version "0.33.0" - resolved "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.0.tgz" - integrity sha512-TiVJbx38J2rNVfA309ffSOB+3/7wOsZYQEOlKqOUdWD/nqkjNGrX+YQGz7nzcf5oy2lC+d37+w183iNXRZNngQ== - optionalDependencies: - "@img/sharp-libvips-linux-s390x" "1.0.0" - -"@img/sharp-linux-x64@0.33.0": - version "0.33.0" - resolved "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.0.tgz" - integrity sha512-PaZM4Zi7/Ek71WgTdvR+KzTZpBqrQOFcPe7/8ZoPRlTYYRe43k6TWsf4GVH6XKRLMYeSp8J89RfAhBrSP4itNA== - optionalDependencies: - "@img/sharp-libvips-linux-x64" "1.0.0" - -"@img/sharp-linuxmusl-arm64@0.33.0": - version "0.33.0" - resolved "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.0.tgz" - integrity sha512-1QLbbN0zt+32eVrg7bb1lwtvEaZwlhEsY1OrijroMkwAqlHqFj6R33Y47s2XUv7P6Ie1PwCxK/uFnNqMnkd5kg== - optionalDependencies: - "@img/sharp-libvips-linuxmusl-arm64" "1.0.0" - -"@img/sharp-linuxmusl-x64@0.33.0": - version "0.33.0" - resolved "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.0.tgz" - integrity sha512-CecqgB/CnkvCWFhmfN9ZhPGMLXaEBXl4o7WtA6U3Ztrlh/s7FUKX4vNxpMSYLIrWuuzjiaYdfU3+Tdqh1xaHfw== - optionalDependencies: - "@img/sharp-libvips-linuxmusl-x64" "1.0.0" - -"@img/sharp-wasm32@0.33.0": - version "0.33.0" - resolved "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.0.tgz" - integrity sha512-Hn4js32gUX9qkISlemZBUPuMs0k/xNJebUNl/L6djnU07B/HAA2KaxRVb3HvbU5fL242hLOcp0+tR+M8dvJUFw== - dependencies: - "@emnapi/runtime" "^0.44.0" - -"@img/sharp-win32-ia32@0.33.0": - version "0.33.0" - resolved "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.0.tgz" - integrity sha512-5HfcsCZi3l5nPRF2q3bllMVMDXBqEWI3Q8KQONfzl0TferFE5lnsIG0A1YrntMAGqvkzdW6y1Ci1A2uTvxhfzg== - -"@img/sharp-win32-x64@0.33.0": - version "0.33.0" - resolved "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.0.tgz" - integrity sha512-i3DtP/2ce1yKFj4OzOnOYltOEL/+dp4dc4dJXJBv6god1AFTcmkaA99H/7SwOmkCOBQkbVvA3lCGm3/5nDtf9Q== - "@isaacs/balanced-match@^4.0.1": version "4.0.1" resolved "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz" @@ -1582,7 +1474,7 @@ acorn-static-class-features "^1.0.0" astring "^1.7.5" -"@patternfly/chatbot@file:/Users/erindonehoo/Desktop/repos/chatbot/packages/module": +"@patternfly/chatbot@file:/Users/eolkowsk/GitHub/PatternFly/chatbot/packages/module": version "1.0.0" resolved "file:packages/module" dependencies: @@ -10419,7 +10311,7 @@ tslib@^1.8.1: tslib@^1.9.0: version "1.14.1" -tslib@^2, tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.4.0, tslib@^2.4.1, tslib@^2.6.2, tslib@^2.7.0, tslib@^2.8.1, tslib@2: +tslib@^2, tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.4.1, tslib@^2.6.2, tslib@^2.7.0, tslib@^2.8.1, tslib@2: version "2.8.1" tsutils@^3.21.0: @@ -10514,12 +10406,17 @@ typedoc@0.23.0: minimatch "^5.1.0" shiki "^0.10.1" -typescript@*, "typescript@>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta", "typescript@>=4.3 <6", typescript@>=4.9.5, "typescript@4.6.x || 4.7.x", typescript@4.7.4: +typescript@*, "typescript@>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta", "typescript@>=4.3 <6", "typescript@4.6.x || 4.7.x", typescript@4.7.4: version "4.7.4" typescript@^5.3.3: version "5.6.3" +typescript@>=4.9.5: + version "5.9.3" + resolved "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz" + integrity sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw== + uglify-js@^3.1.4: version "3.17.4"