From c399988d4f7d8925baed51838c9c2afea4e50063 Mon Sep 17 00:00:00 2001 From: Rebecca Alpert Date: Tue, 8 Sep 2026 15:32:11 -0400 Subject: [PATCH 1/2] chore(docs): Add demo for canvas Fixes https://github.com/patternfly/chatbot/issues/899 Assisted-by: Cursor --- .../chatbot/examples/demos/Canvas.tsx | 384 ++++++++++++++++++ .../chatbot/examples/demos/Chatbot.md | 60 ++- packages/module/src/Canvas/Canvas.scss | 99 +++++ packages/module/src/main.scss | 1 + 4 files changed, 541 insertions(+), 3 deletions(-) create mode 100644 packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/Canvas.tsx create mode 100644 packages/module/src/Canvas/Canvas.scss diff --git a/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/Canvas.tsx b/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/Canvas.tsx new file mode 100644 index 000000000..04ccd33f3 --- /dev/null +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/Canvas.tsx @@ -0,0 +1,384 @@ +import { useState, useRef, useCallback, useEffect, FunctionComponent, ReactNode } from 'react'; +import { + Divider, + Drawer, + DrawerActions, + DrawerCloseButton, + DrawerContent, + DrawerContentBody, + DrawerHead, + DrawerPanelContent, + DropdownItem, + DropdownList, + Flex, + FlexItem, + Label, + MenuToggle, + Select, + SelectList, + SelectOption, + Title +} from '@patternfly/react-core'; +import { CodeEditor, Language } from '@patternfly/react-code-editor'; +import Chatbot, { ChatbotDisplayMode } from '@patternfly/chatbot/dist/dynamic/Chatbot'; +import ChatbotContent from '@patternfly/chatbot/dist/dynamic/ChatbotContent'; +import ChatbotFooter from '@patternfly/chatbot/dist/dynamic/ChatbotFooter'; +import MessageBar from '@patternfly/chatbot/dist/dynamic/MessageBar'; +import MessageBox from '@patternfly/chatbot/dist/dynamic/MessageBox'; +import Message, { MessageProps } from '@patternfly/chatbot/dist/dynamic/Message'; +import ChatbotHeader, { ChatbotHeaderMain, ChatbotHeaderTitle } from '@patternfly/chatbot/dist/dynamic/ChatbotHeader'; +import { RhUiAiEditIcon, RhUiImageFillIcon } from '@patternfly/react-icons'; +import RhUiAddIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-add-icon'; +import RhUiClipboardIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-clipboard-icon'; +import RhUiCodeIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-code-icon'; +import RhUiUploadIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-upload-icon'; +import { useDropzone } from 'react-dropzone'; +import userAvatar from '../Messages/user_avatar.svg'; +import '@patternfly/react-core/dist/styles/base.css'; +import '@patternfly/chatbot/dist/css/main.css'; + +const sampleCode = `apiVersion: v1 +kind: ConfigMap +metadata: + name: canvas-demo +data: + greeting: Hello, how can I help you today? +`; + +const date = new Date(); + +const initialMessages: MessageProps[] = [ + { + id: '1', + role: 'bot', + content: 'How can I help?', + name: 'Bot', + timestamp: date.toLocaleString() + }, + { + id: '2', + role: 'user', + content: 'I want to live edit YAML code using canvas mode', + name: 'You', + avatar: userAvatar, + avatarProps: { isBordered: true }, + timestamp: date.toLocaleString() + }, + { + id: '3', + role: 'bot', + content: 'The canvas mode text editor is ready. Edit, share or ask me to change something for you.', + name: 'Bot', + timestamp: date.toLocaleString(), + attachments: [{ name: 'canvas mode.yaml', id: 'canvas-mode' }] + } +]; + +const modelOptions = ['GPT-4', 'GPT-3.5', 'Claude', 'Llama 2']; + +export const Canvas: FunctionComponent = () => { + const [messages, setMessages] = useState(initialMessages); + const [announcement, setAnnouncement] = useState(); + const [isSendButtonDisabled, setIsSendButtonDisabled] = useState(false); + const [isAttachMenuOpen, setIsAttachMenuOpen] = useState(false); + const [isModelSelectOpen, setIsModelSelectOpen] = useState(false); + const [selectedModel, setSelectedModel] = useState('GPT-4'); + const [showCanvasLabel, setShowCanvasLabel] = useState(true); + const [isCanvasOpen, setIsCanvasOpen] = useState(true); + const [code, setCode] = useState(sampleCode); + const scrollToBottomRef = useRef(null); + const drawerRef = useRef(null); + const displayMode = ChatbotDisplayMode.fullscreen; + + const { open, getInputProps } = useDropzone({ + multiple: true, + // eslint-disable-next-line no-console + onDropAccepted: () => console.log('fileUploaded') + }); + + const generateId = () => { + const id = Date.now() + Math.random(); + return id.toString(); + }; + + const handleSend = useCallback( + (input: string) => { + const date = new Date(); + const newMessages: MessageProps[] = []; + messages.forEach((message) => newMessages.push(message)); + newMessages.push({ + avatar: userAvatar, + avatarProps: { isBordered: true }, + id: generateId(), + name: 'You', + role: 'user', + content: input, + timestamp: `${date?.toLocaleDateString()} ${date?.toLocaleTimeString()}` + }); + newMessages.push({ + id: generateId(), + name: 'Bot', + role: 'bot', + timestamp: `${date?.toLocaleDateString()} ${date?.toLocaleTimeString()}`, + isLoading: true + }); + setMessages(newMessages); + setAnnouncement(`Message from You: ${input}. Message from Bot is loading.`); + setIsSendButtonDisabled(true); + + setTimeout(() => { + const loadedMessages: MessageProps[] = []; + newMessages.forEach((message) => loadedMessages.push(message)); + loadedMessages.pop(); + loadedMessages.push({ + id: generateId(), + role: 'bot', + content: 'API response from Bot goes here', + name: 'Bot', + isLoading: false, + actions: { + // eslint-disable-next-line no-console + positive: { onClick: () => console.log('Good response') }, + // eslint-disable-next-line no-console + negative: { onClick: () => console.log('Bad response') }, + // eslint-disable-next-line no-console + copy: { onClick: () => console.log('Copy') }, + // eslint-disable-next-line no-console + download: { onClick: () => console.log('Download') }, + // eslint-disable-next-line no-console + listen: { onClick: () => console.log('Listen') } + }, + timestamp: date.toLocaleString() + }); + setMessages(loadedMessages); + setAnnouncement('Message from Bot: API response goes here'); + setIsSendButtonDisabled(false); + }, 5000); + }, + [messages] + ); + + useEffect(() => { + if (messages.length > 3) { + scrollToBottomRef.current?.scrollIntoView(); + } + }, [messages]); + + const onModelSelect = ( + _event: React.MouseEvent | undefined, + value: string | number | undefined + ) => { + setSelectedModel(value as string); + setIsModelSelectOpen(false); + }; + + const openCanvas = () => { + setShowCanvasLabel(true); + setIsCanvasOpen(true); + }; + + const closeCanvasMode = () => { + setShowCanvasLabel(false); + setIsCanvasOpen(false); + }; + + const attachMenuItems: ReactNode = ( + <> + + + {showCanvasLabel ? 'Disable' : 'Enable'} Canvas + + + }> + Logs + + }> + YAML - Status + + }> + YAML - All contents + + + + + } onClick={open}> + Upload from computer + + + + ); + + const panelContent = ( + +
+ + + + + + Edit code + + + + + + + + + setIsCanvasOpen(false)} /> + + +
+ } + /> +
+
+
+ ); + + return ( + <> + {/* This is required for react-dropzone to work in Safari and Firefox */} + + +
+ drawerRef.current?.focus()} + > + + +
+ + + ChatBot + + + + + {messages.map((message) => ( + ({ + ...attachment, + onClick: openCanvas + }))} + /> + ))} +
+
+
+ + { + // eslint-disable-next-line no-console + console.log('selected', value); + if (value === 'canvas') { + if (showCanvasLabel) { + closeCanvasMode(); + } else { + setShowCanvasLabel(true); + setIsCanvasOpen(true); + } + } + setIsAttachMenuOpen(false); + }, + onAttachMenuToggleClick: () => setIsAttachMenuOpen(!isAttachMenuOpen) + }} + buttonProps={{ + attach: { + icon: , + tooltipContent: 'Message actions', + 'aria-label': 'Message actions' + } + }} + additionalActions={ + <> + {showCanvasLabel && ( + + )} + + + } + /> + +
+
+
+
+
+
+ + ); +}; 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 356927d44..675552491 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 @@ -48,11 +48,28 @@ ChatbotHeaderOptionsDropdown, ChatbotHeaderCloseButton, } from '@patternfly/chatbot/dist/dynamic/ChatbotHeader'; -import { RhUiExpandIcon, RhUiOpenDrawerRightIcon, RhUiMenuBarsIcon, RhUiRestoreWindowIcon } from '@patternfly/react-icons'; +import { RhUiExpandIcon, RhUiOpenDrawerRightIcon, RhUiMenuBarsIcon, RhUiRestoreWindowIcon, RhUiAiEditIcon } from '@patternfly/react-icons'; +import { CodeEditor, Language } from '@patternfly/react-code-editor'; +import { useDropzone } from 'react-dropzone'; +import RhUiAddIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-add-icon'; +import RhUiClipboardIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-clipboard-icon'; +import RhUiCodeIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-code-icon'; +import RhUiUploadIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-upload-icon'; import { BarsIcon } from '@patternfly/react-icons/dist/esm/icons/bars-icon'; import { RhUiBuildFillIcon, RhUiCopyFillIcon } from '@patternfly/react-icons'; +import RhUiImageFillIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-image-fill-icon'; import { Button, +Divider, +Drawer, +DrawerActions, +DrawerCloseButton, +DrawerContent, +DrawerContentBody, +DrawerHead, +DrawerPanelContent, +DropdownItem, +DropdownList, DescriptionList, DescriptionListDescription, DescriptionListGroup, @@ -61,7 +78,12 @@ ExpandableSection, ExpandableSectionVariant, Flex, FlexItem, -Label +Label, +MenuToggle, +Select, +SelectList, +SelectOption, +Title } from '@patternfly/react-core'; import PFHorizontalLogoColor from '../UI/PF-HorizontalLogo-Color.svg'; import PFHorizontalLogoReverse from '../UI/PF-HorizontalLogo-Reverse.svg'; @@ -70,7 +92,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, MouseEvent as ReactMouseEvent } from 'react'; +import { useEffect,useCallback, useRef, useState, FunctionComponent, MouseEvent, MouseEvent as ReactMouseEvent, ReactNode } from 'react'; import saveAs from 'file-saver'; ### Basic ChatBot @@ -187,6 +209,38 @@ Your code structure should look like this: ``` +### Canvas + +To let users work with generated or editable content alongside a conversation, you can open a canvas drawer next to a fullscreen ChatBot. The following demo places the ChatBot on the left and a PatternFly drawer on the right. Canvas content is fully flexible—this example uses a PatternFly [``](/components/code-editor), but you can render any React node in the canvas. + +Canvas mode is launched by clicking the **Canvas** label below the message bar. You can also enable or disable canvas mode from the attach menu, and dismiss the label to exit canvas mode. + +Apply the `pf-chatbot__canvas*` classes from `@patternfly/chatbot` so the drawer matches ChatBot backgrounds and fills the fullscreen layout. +

+Your code structure should look like this: + +```noLive + +
+ + + + + + + Canvas} ... /> + + + + +
+
+``` + +```js file="./Canvas.tsx" isFullscreen + +``` + ### Chat transcripts This demo illustrates how you could add downloadable transcripts to your ChatBot, which outline conversation details in a Markdown file. This approach allows users to easily share information from a conversation with others. diff --git a/packages/module/src/Canvas/Canvas.scss b/packages/module/src/Canvas/Canvas.scss new file mode 100644 index 000000000..4b39e9569 --- /dev/null +++ b/packages/module/src/Canvas/Canvas.scss @@ -0,0 +1,99 @@ +// ============================================================================ +// Canvas +// ============================================================================ +.pf-chatbot__canvas { + --pf-chatbot__canvas--BackgroundColor: var(--pf-t--global--background--color--secondary--default); + --pf-chatbot__canvas--Padding: var(--pf-t--global--spacer--lg); + --pf-chatbot__canvas-model-toggle--MinWidth: 7.5rem; + + display: flex; + flex-direction: column; + flex: 1; + min-height: 0; + height: 100%; + background-color: var(--pf-chatbot__canvas--BackgroundColor); +} + +:where(.pf-v6-theme-glass) .pf-chatbot__canvas { + --pf-chatbot__canvas--BackgroundColor: var(--pf-t--global--background--color--glass--primary--default); +} + +// Drawer +// ---------------------------------------------------------------------------- +.pf-chatbot__canvas-drawer { + flex: 1; + min-height: 0; + height: 100%; + background-color: var(--pf-chatbot__canvas--BackgroundColor); + --pf-v6-c-drawer__panel--Opacity: 1; + --pf-v6-c-drawer__panel--BackgroundColor: var(--pf-chatbot__canvas--BackgroundColor); + --pf-v6-c-drawer__panel--m-inline--BackgroundColor: var(--pf-chatbot__canvas--BackgroundColor); + --pf-v6-c-drawer__panel--m-inline--md--BackgroundColor: var(--pf-chatbot__canvas--BackgroundColor); + --pf-v6-c-code-editor__main--BackgroundColor: var(--pf-chatbot__canvas--BackgroundColor); + + .pf-v6-c-drawer__main, + .pf-v6-c-drawer__panel, + .pf-v6-c-drawer__panel-main { + background-color: var(--pf-chatbot__canvas--BackgroundColor); + } +} + +@media screen and (min-width: 48rem) { + .pf-v6-c-drawer.pf-m-inline.pf-chatbot__canvas-drawer { + --pf-v6-c-drawer__panel--BackgroundColor: var(--pf-chatbot__canvas--BackgroundColor); + --pf-v6-c-drawer__panel--m-inline--md--BackgroundColor: var(--pf-chatbot__canvas--BackgroundColor); + } +} + +.pf-chatbot__canvas-body { + height: 100%; +} + +.pf-chatbot__canvas-column { + display: flex; + flex-direction: column; + height: 100%; + min-height: 0; +} + +// Panel +// ---------------------------------------------------------------------------- +.pf-chatbot__canvas-panel { + height: 100%; + overflow: hidden; + background-color: var(--pf-chatbot__canvas--BackgroundColor); + --pf-v6-c-drawer__panel--BackgroundColor: var(--pf-chatbot__canvas--BackgroundColor); + --pf-v6-c-drawer__panel--m-inline--BackgroundColor: var(--pf-chatbot__canvas--BackgroundColor); + --pf-v6-c-drawer__panel--m-inline--md--BackgroundColor: var(--pf-chatbot__canvas--BackgroundColor); +} + +.pf-chatbot__canvas-panel-body { + display: flex; + flex-direction: column; + height: 100%; + min-height: 0; + padding: var(--pf-chatbot__canvas--Padding); + box-sizing: border-box; + background-color: var(--pf-chatbot__canvas--BackgroundColor); +} + +.pf-chatbot__canvas-head { + margin-block-end: var(--pf-t--global--spacer--lg); +} + +.pf-chatbot__canvas-editor { + display: flex; + flex: 1; + flex-direction: column; + min-height: 0; + height: 100%; +} + +.pf-chatbot__canvas-editor-loading { + height: 100%; + background-color: var(--pf-chatbot__canvas--BackgroundColor); +} + +.pf-chatbot__canvas-model-toggle { + min-width: var(--pf-chatbot__canvas-model-toggle--MinWidth); +} diff --git a/packages/module/src/main.scss b/packages/module/src/main.scss index e68020366..bd8edd2e2 100644 --- a/packages/module/src/main.scss +++ b/packages/module/src/main.scss @@ -9,6 +9,7 @@ @import './ChatbotPopover/ChatbotPopover'; @import './ChatbotToggle/ChatbotToggle'; @import './ChatbotWelcomePrompt/ChatbotWelcomePrompt'; +@import './Canvas/Canvas'; @import './CodeModal/CodeModal'; @import './Compare/Compare'; @import './DeepThinking/DeepThinking'; From 4d82fbeffa27989eb99e83b14ecf5acd9a68b31f Mon Sep 17 00:00:00 2001 From: Rebecca Alpert Date: Fri, 11 Sep 2026 15:35:21 -0400 Subject: [PATCH 2/2] Address feedback --- .../content/extensions/chatbot/chatbot.md | 1 + .../chatbot/examples/demos/Canvas.md | 100 ++++ .../chatbot/examples/demos/Canvas.tsx | 433 ++++++++++++------ .../chatbot/examples/demos/Chatbot.md | 32 -- .../patternfly-docs/patternfly-docs.config.js | 2 +- packages/module/src/Canvas/Canvas.scss | 4 - .../ChatbotConversationHistoryNav.scss | 168 +++---- .../ChatbotConversationHistoryNav.test.tsx | 13 + .../ChatbotConversationHistoryNav.tsx | 1 + 9 files changed, 502 insertions(+), 252 deletions(-) create mode 100644 packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/Canvas.md diff --git a/packages/module/patternfly-docs/content/extensions/chatbot/chatbot.md b/packages/module/patternfly-docs/content/extensions/chatbot/chatbot.md index ff4f4fc42..cf4634fc1 100644 --- a/packages/module/patternfly-docs/content/extensions/chatbot/chatbot.md +++ b/packages/module/patternfly-docs/content/extensions/chatbot/chatbot.md @@ -49,6 +49,7 @@ Explore our documentation, which covers both the components you'll need to build - [Primary color background](/extensions/chatbot/overview/demo#primary-color-background) - [Display mode switcher](/extensions/chatbot/overview/demo#display-mode-switcher) - [Chat transcripts](/extensions/chatbot/overview/demo#chat-transcripts) + - [Canvas](/patterns/canvas/demo) - [Analytics](/extensions/chatbot/analytics): Tracking for ChatBot interactions - [Customizing messages](/extensions/chatbot/customizing-messages): Details for the implementation of custom messages - [Tree-shaking](/extensions/chatbot/tree-shaking): Bundle size optimization and import best practices diff --git a/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/Canvas.md b/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/Canvas.md new file mode 100644 index 000000000..4afac2cc8 --- /dev/null +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/Canvas.md @@ -0,0 +1,100 @@ +--- +id: Canvas +section: patterns +source: demo +--- + +import { useState, useRef, useCallback, useEffect, FunctionComponent, ReactNode } from 'react'; +import { +Brand, +Divider, +Drawer, +DrawerActions, +DrawerCloseButton, +DrawerContent, +DrawerContentBody, +DrawerHead, +DrawerPanelContent, +DropdownItem, +DropdownList, +Flex, +FlexItem, +Label, +MenuToggle, +Popover, +Select, +SelectList, +SelectOption, +Title, +Tooltip +} from '@patternfly/react-core'; +import { CodeEditor, CodeEditorControl, Language } from '@patternfly/react-code-editor'; +import Chatbot, { ChatbotDisplayMode } from '@patternfly/chatbot/dist/dynamic/Chatbot'; +import ChatbotContent from '@patternfly/chatbot/dist/dynamic/ChatbotContent'; +import ChatbotFooter, { ChatbotFootnote } from '@patternfly/chatbot/dist/dynamic/ChatbotFooter'; +import MessageBar from '@patternfly/chatbot/dist/dynamic/MessageBar'; +import MessageBox from '@patternfly/chatbot/dist/dynamic/MessageBox'; +import Message from '@patternfly/chatbot/dist/dynamic/Message'; +import ChatbotConversationHistoryNav from '@patternfly/chatbot/dist/dynamic/ChatbotConversationHistoryNav'; +import ChatbotHeader, { +ChatbotHeaderActions, +ChatbotHeaderCloseButton, +ChatbotHeaderMain, +ChatbotHeaderMenu, +ChatbotHeaderTitle +} from '@patternfly/chatbot/dist/dynamic/ChatbotHeader'; +import RhUiImageFillIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-image-fill-icon'; +import RhUiAddIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-add-icon'; +import RhUiClipboardIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-clipboard-icon'; +import RhUiCodeIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-code-icon'; +import RhUiExportIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-export-icon'; +import RhUiRedoIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-redo-icon'; +import RhUiUndoIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-undo-icon'; +import RhUiUploadIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-upload-icon'; +import RhUiAiEditIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-ai-edit-icon'; +import RhUiAiInfoIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-ai-info-icon'; +import { useDropzone } from 'react-dropzone'; +import PFIconLogoColor from '../UI/PF-IconLogo-Color.svg'; +import PFIconLogoReverse from '../UI/PF-IconLogo-Reverse.svg'; +import userAvatar from '../Messages/user_avatar.svg'; + +### Canvas + +To let users work with generated or editable content alongside a conversation, you can open a canvas drawer next to a fullscreen ChatBot. The following demo places the ChatBot on the left and a PatternFly drawer on the right. Canvas content is fully flexible—this example uses a PatternFly [``](/components/code-editor), but you can render any React node in the canvas. + +Canvas mode is launched by clicking the **Canvas** label below the message bar. You can also enable or disable canvas mode from the attach menu, and dismiss the label to exit canvas mode. + +Apply the `pf-chatbot__canvas*` classes from `@patternfly/chatbot` so the drawer matches ChatBot backgrounds and fills the fullscreen layout. +

+Your code structure should look like this: + +```noLive + +
+ + + + + + + {/* PatternFly logo */} + + + + + + + + Canvas} ... /> + + + + + +
+
+``` + +```js file="./Canvas.tsx" isFullscreen + +``` diff --git a/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/Canvas.tsx b/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/Canvas.tsx index 04ccd33f3..b29eb1740 100644 --- a/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/Canvas.tsx +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/Canvas.tsx @@ -1,5 +1,6 @@ import { useState, useRef, useCallback, useEffect, FunctionComponent, ReactNode } from 'react'; import { + Brand, Divider, Drawer, DrawerActions, @@ -14,25 +15,43 @@ import { FlexItem, Label, MenuToggle, + Popover, Select, SelectList, SelectOption, - Title + Title, + Tooltip } from '@patternfly/react-core'; -import { CodeEditor, Language } from '@patternfly/react-code-editor'; +import { CodeEditor, CodeEditorControl, Language } from '@patternfly/react-code-editor'; import Chatbot, { ChatbotDisplayMode } from '@patternfly/chatbot/dist/dynamic/Chatbot'; import ChatbotContent from '@patternfly/chatbot/dist/dynamic/ChatbotContent'; -import ChatbotFooter from '@patternfly/chatbot/dist/dynamic/ChatbotFooter'; +import ChatbotFooter, { ChatbotFootnote } from '@patternfly/chatbot/dist/dynamic/ChatbotFooter'; import MessageBar from '@patternfly/chatbot/dist/dynamic/MessageBar'; import MessageBox from '@patternfly/chatbot/dist/dynamic/MessageBox'; import Message, { MessageProps } from '@patternfly/chatbot/dist/dynamic/Message'; -import ChatbotHeader, { ChatbotHeaderMain, ChatbotHeaderTitle } from '@patternfly/chatbot/dist/dynamic/ChatbotHeader'; -import { RhUiAiEditIcon, RhUiImageFillIcon } from '@patternfly/react-icons'; +import ChatbotConversationHistoryNav, { + Conversation +} from '@patternfly/chatbot/dist/dynamic/ChatbotConversationHistoryNav'; +import ChatbotHeader, { + ChatbotHeaderActions, + ChatbotHeaderCloseButton, + ChatbotHeaderMain, + ChatbotHeaderMenu, + ChatbotHeaderTitle +} from '@patternfly/chatbot/dist/dynamic/ChatbotHeader'; +import RhUiAiEditIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-ai-edit-icon'; +import RhUiAiInfoIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-ai-info-icon'; +import RhUiImageFillIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-image-fill-icon'; import RhUiAddIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-add-icon'; import RhUiClipboardIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-clipboard-icon'; import RhUiCodeIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-code-icon'; +import RhUiExportIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-export-icon'; +import RhUiRedoIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-redo-icon'; +import RhUiUndoIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-undo-icon'; import RhUiUploadIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-upload-icon'; import { useDropzone } from 'react-dropzone'; +import PFIconLogoColor from '../UI/PF-IconLogo-Color.svg'; +import PFIconLogoReverse from '../UI/PF-IconLogo-Reverse.svg'; import userAvatar from '../Messages/user_avatar.svg'; import '@patternfly/react-core/dist/styles/base.css'; import '@patternfly/chatbot/dist/css/main.css'; @@ -74,8 +93,31 @@ const initialMessages: MessageProps[] = [ } ]; +const initialConversations = { + Today: [{ id: '1', text: 'I want to live edit YAML code using canvas mode' }], + 'This month': [ + { id: '2', text: 'Enterprise Linux installation and setup' }, + { id: '3', text: 'Troubleshoot system crash' } + ] +}; + +const footnoteProps = { + label: 'Always review AI-generated content prior to use.' +}; + const modelOptions = ['GPT-4', 'GPT-3.5', 'Claude', 'Llama 2']; +interface CanvasEditor { + trigger: (source: string, handlerId: string, payload?: unknown) => void; +} + +const iconLogo = ( + <> + + + +); + export const Canvas: FunctionComponent = () => { const [messages, setMessages] = useState(initialMessages); const [announcement, setAnnouncement] = useState(); @@ -85,9 +127,16 @@ export const Canvas: FunctionComponent = () => { const [selectedModel, setSelectedModel] = useState('GPT-4'); const [showCanvasLabel, setShowCanvasLabel] = useState(true); const [isCanvasOpen, setIsCanvasOpen] = useState(true); + const [isDrawerOpen, setIsDrawerOpen] = useState(false); + const [chatbotVisible, setChatbotVisible] = useState(true); + const [conversations, setConversations] = useState( + initialConversations + ); const [code, setCode] = useState(sampleCode); const scrollToBottomRef = useRef(null); const drawerRef = useRef(null); + const historyRef = useRef(null); + const editorRef = useRef(null); const displayMode = ChatbotDisplayMode.fullscreen; const { open, getInputProps } = useDropzone({ @@ -182,6 +231,21 @@ export const Canvas: FunctionComponent = () => { setIsCanvasOpen(false); }; + const findMatchingItems = (targetValue: string) => { + let filteredConversations = Object.entries(initialConversations).reduce((acc, [key, items]) => { + const filteredItems = items.filter((item) => item.text.toLowerCase().includes(targetValue.toLowerCase())); + if (filteredItems.length > 0) { + acc[key] = filteredItems; + } + return acc; + }, {}); + + if (Object.keys(filteredConversations).length === 0) { + filteredConversations = [{ id: '13', noIcon: true, text: 'No results found' }]; + } + return filteredConversations; + }; + const attachMenuItems: ReactNode = ( <> @@ -208,6 +272,33 @@ export const Canvas: FunctionComponent = () => { ); + const customControls = [ + } + aria-label="Undo" + tooltipProps={{ content: 'Undo' }} + onClick={() => editorRef.current?.trigger('keyboard', 'undo', null)} + />, + } + aria-label="Redo" + tooltipProps={{ content: 'Redo' }} + onClick={() => editorRef.current?.trigger('keyboard', 'redo', null)} + />, + } + aria-label="Export" + tooltipProps={{ content: 'Export' }} + onClick={(value) => { + // eslint-disable-next-line no-console + console.log('Export', value); + }} + /> + ]; + const panelContent = ( { resizeAriaLabel="Resize canvas" className="pf-chatbot__canvas-panel" > -
- - - - - - Edit code - - - - - - - - setIsCanvasOpen(false)} /> - - + + + + + + + setIsCanvasOpen(false)} /> + + + + +
{ + editorRef.current = editor; + }} loading={
} />
@@ -263,120 +379,161 @@ export const Canvas: FunctionComponent = () => { <> {/* This is required for react-dropzone to work in Safari and Firefox */} - +
- drawerRef.current?.focus()} - > - - -
- - - ChatBot - - - - - {messages.map((message) => ( - ({ - ...attachment, - onClick: openCanvas - }))} - /> - ))} -
-
-
- - { - // eslint-disable-next-line no-console - console.log('selected', value); - if (value === 'canvas') { - if (showCanvasLabel) { - closeCanvasMode(); - } else { - setShowCanvasLabel(true); - setIsCanvasOpen(true); + { + setIsDrawerOpen(!isDrawerOpen); + setConversations(initialConversations); + }} + isDrawerOpen={isDrawerOpen} + setIsDrawerOpen={setIsDrawerOpen} + activeItemId="1" + conversations={conversations} + drawerCloseButtonProps={{ 'aria-label': 'Close chat history' }} + onNewChat={() => { + setIsDrawerOpen(!isDrawerOpen); + setMessages([]); + setConversations(initialConversations); + }} + handleTextInputChange={(value: string) => { + if (value === '') { + setConversations(initialConversations); + } + const newConversations: { [key: string]: Conversation[] } = findMatchingItems(value); + setConversations(newConversations); + }} + drawerContent={ + drawerRef.current?.focus()} + > + + +
+ + + setIsDrawerOpen(!isDrawerOpen)} + tooltipContent="Chat history" + menuAriaLabel="Chat history" + /> + {iconLogo} + + + setChatbotVisible(false)} + /> + + + + + {messages.map((message) => ( + ({ + ...attachment, + onClick: openCanvas + }))} + /> + ))} +
+
+
+ + { + // eslint-disable-next-line no-console + console.log('selected', value); + if (value === 'canvas') { + if (showCanvasLabel) { + closeCanvasMode(); + } else { + setShowCanvasLabel(true); + setIsCanvasOpen(true); + } + } + setIsAttachMenuOpen(false); + }, + onAttachMenuToggleClick: () => setIsAttachMenuOpen(!isAttachMenuOpen) + }} + buttonProps={{ + attach: { + icon: , + tooltipContent: 'Message actions', + 'aria-label': 'Message actions' } - } - setIsAttachMenuOpen(false); - }, - onAttachMenuToggleClick: () => setIsAttachMenuOpen(!isAttachMenuOpen) - }} - buttonProps={{ - attach: { - icon: , - tooltipContent: 'Message actions', - 'aria-label': 'Message actions' - } - }} - additionalActions={ - <> - {showCanvasLabel && ( - - )} - setIsModelSelectOpen(isOpen)} + toggle={(toggleRef) => ( + setIsModelSelectOpen(!isModelSelectOpen)} + isExpanded={isModelSelectOpen} + aria-label={`${selectedModel}, Select a model`} + > + {selectedModel} + + )} > - {selectedModel} - - )} - > - - {modelOptions.map((option) => ( - - {option} - - ))} - - - - } - /> - -
-
-
-
+ + {modelOptions.map((option) => ( + + {option} + + ))} + + + + } + /> + +
+
+
+
+
+ } + />
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 675552491..92924d611 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 @@ -209,38 +209,6 @@ Your code structure should look like this: ``` -### Canvas - -To let users work with generated or editable content alongside a conversation, you can open a canvas drawer next to a fullscreen ChatBot. The following demo places the ChatBot on the left and a PatternFly drawer on the right. Canvas content is fully flexible—this example uses a PatternFly [``](/components/code-editor), but you can render any React node in the canvas. - -Canvas mode is launched by clicking the **Canvas** label below the message bar. You can also enable or disable canvas mode from the attach menu, and dismiss the label to exit canvas mode. - -Apply the `pf-chatbot__canvas*` classes from `@patternfly/chatbot` so the drawer matches ChatBot backgrounds and fills the fullscreen layout. -

-Your code structure should look like this: - -```noLive - -
- - - - - - - Canvas} ... /> - - - - -
-
-``` - -```js file="./Canvas.tsx" isFullscreen - -``` - ### Chat transcripts This demo illustrates how you could add downloadable transcripts to your ChatBot, which outline conversation details in a Markdown file. This approach allows users to easily share information from a conversation with others. diff --git a/packages/module/patternfly-docs/patternfly-docs.config.js b/packages/module/patternfly-docs/patternfly-docs.config.js index a0e203511..811cf8483 100644 --- a/packages/module/patternfly-docs/patternfly-docs.config.js +++ b/packages/module/patternfly-docs/patternfly-docs.config.js @@ -1,6 +1,6 @@ // This module is shared between NodeJS and babelled ES5 module.exports = { - sideNavItems: [{ section: 'extensions' }], + sideNavItems: [{ section: 'extensions' }, { section: 'patterns' }], topNavItems: [], hasThemeSwitcher: true, hasRTLSwitcher: true, diff --git a/packages/module/src/Canvas/Canvas.scss b/packages/module/src/Canvas/Canvas.scss index 4b39e9569..01da5cb99 100644 --- a/packages/module/src/Canvas/Canvas.scss +++ b/packages/module/src/Canvas/Canvas.scss @@ -77,10 +77,6 @@ background-color: var(--pf-chatbot__canvas--BackgroundColor); } -.pf-chatbot__canvas-head { - margin-block-end: var(--pf-t--global--spacer--lg); -} - .pf-chatbot__canvas-editor { display: flex; flex: 1; diff --git a/packages/module/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.scss b/packages/module/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.scss index 052b7c2b9..a4cf7fff4 100644 --- a/packages/module/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.scss +++ b/packages/module/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.scss @@ -172,81 +172,91 @@ // Chatbot Header - Drawer // ---------------------------------------------------------------------------- +// Child combinators keep these rules off nested drawers such as canvas. .pf-chatbot__history.pf-v6-c-drawer { - --pf-v6-c-drawer__panel--MinWidth: 24rem; - --pf-v6-c-drawer__panel--xl--MinWidth: 24rem; height: 70vh; // Drawer panel - .pf-v6-c-drawer__panel { + > .pf-v6-c-drawer__main > .pf-v6-c-drawer__panel { + --pf-v6-c-drawer__panel--MinWidth: 24rem; + --pf-v6-c-drawer__panel--xl--MinWidth: 24rem; --pf-v6-c-drawer__panel--BackgroundColor: var(--pf-t--global--background--color--floating--default); + --pf-v6-c-drawer__panel--m-inline--BackgroundColor: var(--pf-t--global--background--color--floating--default); + --pf-v6-c-drawer__panel--m-inline--md--BackgroundColor: var(--pf-t--global--background--color--floating--default); --pf-v6-c-drawer__panel--PaddingBlockStart: var(--pf-t--global--spacer--lg); --pf-v6-c-drawer__panel--PaddingBlockEnd: var(--pf-t--global--spacer--lg); --pf-v6-c-drawer__panel--RowGap: var(--pf-t--global--spacer--gap--group-to-group--vertical--default); overflow-x: hidden; overflow-y: hidden; - } - // Drawer head - .pf-v6-c-drawer__head { - --pf-v6-c-drawer__head--PaddingBlockStart: 0; - --pf-v6-c-drawer__head--PaddingBlockEnd: 0; - --pf-v6-c-drawer__head--PaddingInlineStart: var(--pf-t--global--spacer--lg); - --pf-v6-c-drawer__head--PaddingInlineEnd: var(--pf-t--global--spacer--lg); + // Drawer head + .pf-v6-c-drawer__head { + --pf-v6-c-drawer__head--PaddingBlockStart: 0; + --pf-v6-c-drawer__head--PaddingBlockEnd: 0; + --pf-v6-c-drawer__head--PaddingInlineStart: var(--pf-t--global--spacer--lg); + --pf-v6-c-drawer__head--PaddingInlineEnd: var(--pf-t--global--spacer--lg); - display: flex; - align-items: center; - justify-content: space-between; - height: var(--pf-t--global--spacer--2xl); - } + display: flex; + align-items: center; + justify-content: space-between; + height: var(--pf-t--global--spacer--2xl); + } - // Drawer actions - .pf-v6-c-drawer__actions { - --pf-v6-c-drawer__actions--MarginBlockStart: 0; - --pf-v6-c-drawer__actions--MarginInlineEnd: 0; + // Drawer actions + .pf-v6-c-drawer__actions { + --pf-v6-c-drawer__actions--MarginBlockStart: 0; + --pf-v6-c-drawer__actions--MarginInlineEnd: 0; - display: flex; - align-items: center; - justify-content: space-between; - align-self: center; - width: 100%; - height: 100%; - } + display: flex; + align-items: center; + justify-content: space-between; + align-self: center; + width: 100%; + height: 100%; + } - .pf-v6-c-drawer__actions--reversed { - flex-direction: row-reverse; - } + .pf-v6-c-drawer__actions--reversed { + flex-direction: row-reverse; + } - // Close drawer - .pf-v6-c-drawer__close { - .pf-v6-c-button { - width: var(--pf-t--global--spacer--2xl); - height: var(--pf-t--global--spacer--2xl); - border-radius: var(--pf-t--global--border--radius--pill); - justify-content: center; - align-items: center; + // Close drawer + .pf-v6-c-drawer__close { + .pf-v6-c-button { + width: var(--pf-t--global--spacer--2xl); + height: var(--pf-t--global--spacer--2xl); + border-radius: var(--pf-t--global--border--radius--pill); + justify-content: center; + align-items: center; + } } - } - // Drawer body - .pf-v6-c-drawer__body { - --pf-v6-c-drawer__panel__body--PaddingBlockStart: 0; - --pf-v6-c-drawer__panel__body--PaddingBlockEnd: 0; - --pf-v6-c-drawer__panel__body--PaddingInlineStart: var(--pf-t--global--spacer--lg); - --pf-v6-c-drawer__panel__body--PaddingInlineEnd: var(--pf-t--global--spacer--lg); + // Drawer body + .pf-v6-c-drawer__body { + --pf-v6-c-drawer__panel__body--PaddingBlockStart: 0; + --pf-v6-c-drawer__panel__body--PaddingBlockEnd: 0; + --pf-v6-c-drawer__panel__body--PaddingInlineStart: var(--pf-t--global--spacer--lg); + --pf-v6-c-drawer__panel__body--PaddingInlineEnd: var(--pf-t--global--spacer--lg); + + overflow-y: auto; + display: flex; + flex-direction: column; + height: 70vh; + } + // Resizable drawer spacing is different - needs manual adjustment for gaps between buttons, inputs, etc. + &.pf-m-resizable { + .pf-v6-c-drawer__panel-main { + row-gap: var(--pf-v6-c-drawer__panel--RowGap); + } + } + } + + > .pf-v6-c-drawer__main > .pf-v6-c-drawer__content > .pf-v6-c-drawer__body { overflow-y: auto; display: flex; flex-direction: column; height: 70vh; } - - // Resizable drawer spacing is different - needs manual adjustment for gaps between buttons, inputs, etc. - .pf-v6-c-drawer__panel.pf-m-resizable { - .pf-v6-c-drawer__panel-main { - row-gap: var(--pf-v6-c-drawer__panel--RowGap); - } - } } .pf-chatbot:is(.pf-chatbot--drawer, .pf-chatbot--docked) .pf-chatbot__history { @@ -279,7 +289,9 @@ .pf-chatbot--fullscreen { .pf-chatbot__history.pf-v6-c-drawer { height: 100vh; - .pf-v6-c-drawer__body { + + > .pf-v6-c-drawer__main > .pf-v6-c-drawer__content > .pf-v6-c-drawer__body, + > .pf-v6-c-drawer__main > .pf-v6-c-drawer__panel .pf-v6-c-drawer__body { width: 100%; } } @@ -291,11 +303,14 @@ .pf-chatbot--embedded { .pf-chatbot__history.pf-v6-c-drawer { height: 100%; - .pf-v6-c-drawer__body { + + > .pf-v6-c-drawer__main > .pf-v6-c-drawer__content > .pf-v6-c-drawer__body, + > .pf-v6-c-drawer__main > .pf-v6-c-drawer__panel .pf-v6-c-drawer__body { width: 100%; height: 100%; } - .pf-v6-c-drawer__panel { + + > .pf-v6-c-drawer__main > .pf-v6-c-drawer__panel { height: 100%; } } @@ -327,37 +342,36 @@ } .pf-chatbot__history.pf-m-compact { - .pf-v6-c-drawer__main { - .pf-v6-c-drawer__panel { - --pf-v6-c-drawer__panel--PaddingBlockStart: var(--pf-t--global--spacer--sm); - --pf-v6-c-drawer__panel--PaddingBlockEnd: var(--pf-t--global--spacer--sm); - --pf-v6-c-drawer__head--PaddingInlineStart: var(--pf-t--global--spacer--md); - --pf-v6-c-drawer__head--PaddingInlineEnd: var(--pf-t--global--spacer--md); - --pf-v6-c-drawer__panel--RowGap: var(--pf-t--global--spacer--md); - } + > .pf-v6-c-drawer__main > .pf-v6-c-drawer__panel { + --pf-v6-c-drawer__panel--PaddingBlockStart: var(--pf-t--global--spacer--sm); + --pf-v6-c-drawer__panel--PaddingBlockEnd: var(--pf-t--global--spacer--sm); + --pf-v6-c-drawer__head--PaddingInlineStart: var(--pf-t--global--spacer--md); + --pf-v6-c-drawer__head--PaddingInlineEnd: var(--pf-t--global--spacer--md); + --pf-v6-c-drawer__panel--RowGap: var(--pf-t--global--spacer--md); .pf-v6-c-drawer__body { --pf-v6-c-drawer__panel__body--PaddingInlineStart: var(--pf-t--global--spacer--md); --pf-v6-c-drawer__panel__body--PaddingInlineEnd: var(--pf-t--global--spacer--md); } + + .pf-v6-c-drawer__head { + --pf-v6-c-drawer__head--PaddingInlineStart: var(--pf-t--global--spacer--lg); + --pf-v6-c-drawer__head--PaddingInlineEnd: var(--pf-t--global--spacer--lg); + } + + .pf-v6-c-drawer__close { + .pf-v6-c-button { + width: 2rem; + height: 2rem; + --pf-v6-c-button--m-plain--PaddingInlineEnd: var(--pf-t--global--spacer--xs); + --pf-v6-c-button--m-plain--PaddingInlineStart: var(--pf-t--global--spacer--xs); + --pf-v6-c-button--PaddingBlockStart: var(--pf-t--global--spacer--xs); + --pf-v6-c-button--PaddingBlockEnd: var(--pf-t--global--spacer--xs); + } + } } .pf-chatbot__menu-item { font-size: var(--pf-t--global--font--size--body--md); } - - .pf-v6-c-drawer__head { - --pf-v6-c-drawer__head--PaddingInlineStart: var(--pf-t--global--spacer--lg); - --pf-v6-c-drawer__head--PaddingInlineEnd: var(--pf-t--global--spacer--lg); - } - .pf-v6-c-drawer__close { - .pf-v6-c-button { - width: 2rem; - height: 2rem; - --pf-v6-c-button--m-plain--PaddingInlineEnd: var(--pf-t--global--spacer--xs); - --pf-v6-c-button--m-plain--PaddingInlineStart: var(--pf-t--global--spacer--xs); - --pf-v6-c-button--PaddingBlockStart: var(--pf-t--global--spacer--xs); - --pf-v6-c-button--PaddingBlockEnd: var(--pf-t--global--spacer--xs); - } - } } diff --git a/packages/module/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.test.tsx b/packages/module/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.test.tsx index 478d8fe5a..e7634ba7a 100644 --- a/packages/module/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.test.tsx +++ b/packages/module/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.test.tsx @@ -204,6 +204,19 @@ describe('ChatbotConversationHistoryNav', () => { ); }); + it('should keep an opaque panel background in glass theme', () => { + render( + + ); + expect(screen.getByRole('dialog')).toHaveClass('pf-m-no-plain-on-glass'); + }); + it('should accept drawerContentProps', () => { const { container } = render( {drawer}