From c399988d4f7d8925baed51838c9c2afea4e50063 Mon Sep 17 00:00:00 2001 From: Rebecca Alpert Date: Tue, 8 Sep 2026 15:32:11 -0400 Subject: [PATCH 1/9] 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 696402ae68d97574b0f5467cc27fd3f048d6ee5e Mon Sep 17 00:00:00 2001 From: Rebecca Alpert Date: Fri, 11 Sep 2026 15:35:21 -0400 Subject: [PATCH 2/9] Address feedback --- .../content/extensions/chatbot/chatbot.md | 1 + .../chatbot/examples/demos/Canvas.md | 101 ++++ .../chatbot/examples/demos/Canvas.tsx | 463 ++++++++++++------ .../chatbot/examples/demos/Chatbot.md | 32 -- .../patternfly-docs/patternfly-docs.config.js | 2 +- packages/module/src/Canvas/Canvas.scss | 76 ++- .../ChatbotConversationHistoryNav.scss | 168 ++++--- .../ChatbotConversationHistoryNav.test.tsx | 13 + .../ChatbotConversationHistoryNav.tsx | 1 + 9 files changed, 574 insertions(+), 283 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..8728aa1b0 --- /dev/null +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/Canvas.md @@ -0,0 +1,101 @@ +--- +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 RhUiTaskFillIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-task-fill-icon'; +import RhUiNotificationFillIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-notification-fill-icon'; +import RhUiCalendarFillIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-calendar-fill-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 RhUiServerUploadFillIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-server-upload-fill-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..33665d6f8 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,44 @@ 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 RhUiUploadIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-upload-icon'; +import RhUiTaskFillIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-task-fill-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 RhUiServerUploadFillIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-server-upload-fill-icon'; +import RhUiNotificationFillIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-notification-fill-icon'; +import RhUiCalendarFillIcon from '@patternfly/react-icons/dist/esm/icons/rh-ui-calendar-fill-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 +94,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 +128,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,32 +232,73 @@ 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 = ( <> - - {showCanvasLabel ? 'Disable' : 'Enable'} Canvas + }> + Alerts - - }> - Logs + }> + Events - }> - YAML - Status - - }> - YAML - All contents + }> + Logs - + - } onClick={open}> + } onClick={open}> Upload from computer + }> + Canvas mode + ); + 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..5e82cb17b 100644 --- a/packages/module/src/Canvas/Canvas.scss +++ b/packages/module/src/Canvas/Canvas.scss @@ -4,7 +4,6 @@ .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; @@ -29,11 +28,12 @@ --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-drawer__panel--m-glass--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 { + > .pf-v6-c-drawer__main, + > .pf-v6-c-drawer__main > .pf-v6-c-drawer__panel, + > .pf-v6-c-drawer__main > .pf-v6-c-drawer__panel .pf-v6-c-drawer__panel-main { background-color: var(--pf-chatbot__canvas--BackgroundColor); } } @@ -65,20 +65,56 @@ --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-drawer__panel--m-glass--BackgroundColor: var(--pf-chatbot__canvas--BackgroundColor); + --pf-v6-c-drawer__panel--PaddingBlockStart: 0; + --pf-v6-c-drawer__panel--PaddingBlockEnd: 0; + --pf-v6-c-drawer__panel--RowGap: 0; -.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-v6-c-drawer__panel-main { + display: flex; + flex-direction: column; + min-height: 0; + height: 100%; + } -.pf-chatbot__canvas-head { - margin-block-end: var(--pf-t--global--spacer--lg); + .pf-chatbot__canvas-panel-body { + display: flex; + flex-direction: column; + flex: 1; + min-height: 0; + padding-block-end: var(--pf-chatbot__canvas--Padding); + padding-inline: var(--pf-chatbot__canvas--Padding); + box-sizing: border-box; + background-color: var(--pf-chatbot__canvas--BackgroundColor); + } + + .pf-chatbot__canvas-head { + width: 100%; + grid-template-columns: minmax(0, 1fr) max-content; + --pf-v6-c-drawer__head--PaddingBlockStart: var(--pf-chatbot__canvas--Padding); + --pf-v6-c-drawer__head--PaddingBlockEnd: var(--pf-t--global--spacer--lg); + --pf-v6-c-drawer__head--PaddingInlineStart: var(--pf-chatbot__canvas--Padding); + --pf-v6-c-drawer__head--PaddingInlineEnd: var(--pf-chatbot__canvas--Padding); + + .pf-v6-c-drawer__actions { + grid-column: 2; + justify-self: end; + align-self: start; + width: max-content; + --pf-v6-c-drawer__actions--MarginBlockStart: 0; + --pf-v6-c-drawer__actions--MarginInlineEnd: 0; + + .pf-v6-c-button { + border-radius: var(--pf-t--global--border--radius--pill); + justify-content: center; + align-items: center; + + &::before { + border-radius: inherit; + } + } + } + } } .pf-chatbot__canvas-editor { @@ -87,13 +123,13 @@ flex-direction: column; min-height: 0; height: 100%; + + .pf-v6-c-code-editor { + --pf-v6-c-code-editor__main--BackgroundColor: var(--pf-chatbot__canvas--BackgroundColor); + } } .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/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} From a39c52f4bc321e98ee11774cc375e16a375a1017 Mon Sep 17 00:00:00 2001 From: Rebecca Alpert Date: Mon, 14 Sep 2026 11:11:25 -0400 Subject: [PATCH 3/9] Update packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/Canvas.md Co-authored-by: Eric Olkowski <70952936+thatblindgeye@users.noreply.github.com> --- .../extensions/chatbot/examples/demos/Canvas.md | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) 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 index 8728aa1b0..4363118a5 100644 --- a/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/Canvas.md +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/Canvas.md @@ -59,15 +59,13 @@ import PFIconLogoColor from '../UI/PF-IconLogo-Color.svg'; import PFIconLogoReverse from '../UI/PF-IconLogo-Reverse.svg'; import userAvatar from '../Messages/user_avatar.svg'; -### Canvas +**Canvas** lets users work with generated or editable content alongside a conversation. Canvas content is fully flexible, allowing you to render any content suitable for your use case. It is recommended to only utilize canvas mode with a fullscreen Chatbot. -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. +## Composable structure -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. +When utilizing a canvas layout, it's recommended to use a PatternFly [drawer](/components/drawer). You must apply the `pf-chatbot__canvas*` classes from `@patternfly/chatbot` so the drawer matches ChatBot backgrounds and fills the fullscreen layout. -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: +The general recommended structure is as follows: ```noLive @@ -95,6 +93,11 @@ Your code structure should look like this:
``` +## Demos + +### With code editor + +This demo shows canvas mode being used to render a PatternFly [code editor](/components/code-editor). Canvas mode is launched by clicking a file chip below a message or 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. ```js file="./Canvas.tsx" isFullscreen From c67dd22eecab3303f2c75a62e859632ef43408d3 Mon Sep 17 00:00:00 2001 From: Rebecca Alpert Date: Mon, 14 Sep 2026 12:11:07 -0400 Subject: [PATCH 4/9] Address focus, etc --- .../chatbot/examples/demos/Canvas.tsx | 43 ++++++++++++++++--- 1 file changed, 38 insertions(+), 5 deletions(-) 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 33665d6f8..998b4cb9e 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 @@ -135,9 +135,12 @@ export const Canvas: FunctionComponent = () => { ); const [code, setCode] = useState(sampleCode); const scrollToBottomRef = useRef(null); - const drawerRef = useRef(null); const historyRef = useRef(null); const editorRef = useRef(null); + const messageInputRef = useRef(null); + const messageActionsRef = useRef(null); + const wasCanvasOpen = useRef(isCanvasOpen); + const shouldFocusCanvasCloseButton = useRef(false); const displayMode = ChatbotDisplayMode.fullscreen; const { open, getInputProps } = useDropzone({ @@ -230,6 +233,33 @@ export const Canvas: FunctionComponent = () => { const closeCanvasMode = () => { setShowCanvasLabel(false); setIsCanvasOpen(false); + window.requestAnimationFrame(() => messageInputRef.current?.focus()); + }; + + const focusCanvasCloseButton = () => { + document.querySelector('#chatbot-canvas-panel .pf-v6-c-drawer__close button')?.focus(); + }; + + useEffect(() => { + const wasClosed = !wasCanvasOpen.current; + wasCanvasOpen.current = isCanvasOpen; + + if (isCanvasOpen && wasClosed) { + shouldFocusCanvasCloseButton.current = true; + } else if (!isCanvasOpen) { + shouldFocusCanvasCloseButton.current = false; + } + }, [isCanvasOpen]); + + const onCanvasExpand = () => { + if (shouldFocusCanvasCloseButton.current) { + shouldFocusCanvasCloseButton.current = false; + focusCanvasCloseButton(); + } + }; + + const focusMessageActions = () => { + window.requestAnimationFrame(() => messageActionsRef.current?.focus()); }; const findMatchingItems = (targetValue: string) => { @@ -345,9 +375,9 @@ export const Canvas: FunctionComponent = () => { - + - setIsCanvasOpen(false)} /> + @@ -410,7 +440,7 @@ export const Canvas: FunctionComponent = () => { isExpanded={isCanvasOpen} isInline position="end" - onExpand={() => drawerRef.current?.focus()} + onExpand={onCanvasExpand} > @@ -452,6 +482,7 @@ export const Canvas: FunctionComponent = () => { { } } setIsAttachMenuOpen(false); + focusMessageActions(); }, onAttachMenuToggleClick: () => setIsAttachMenuOpen(!isAttachMenuOpen) }} buttonProps={{ attach: { + innerRef: messageActionsRef, icon: , tooltipContent: 'Message actions', 'aria-label': 'Message actions' @@ -487,7 +520,7 @@ export const Canvas: FunctionComponent = () => { {showCanvasLabel && (
``` + +## Class names + +| Class name | Purpose | +| --- | --- | +| `pf-chatbot__canvas` | Container for the fullscreen canvas layout and its background. | +| `pf-chatbot__canvas-drawer` | Styles the drawer that contains the canvas panel. | +| `pf-chatbot__canvas-body` | Ensures the drawer content body fills the available height. | +| `pf-chatbot__canvas-column` | Arranges the canvas header, content, and footer vertically. | +| `pf-chatbot__canvas-panel` | Styles the canvas drawer panel and removes the default panel spacing. | +| `pf-chatbot__canvas-panel-body` | Provides the canvas panel content area and its spacing. | +| `pf-chatbot__canvas-head` | Styles and positions the canvas panel header and close action. | +| `pf-chatbot__canvas-editor` | Makes the code editor fill the available canvas space. | + ## Demos ### With code editor 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 998b4cb9e..3e7257611 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 @@ -398,7 +398,6 @@ export const Canvas: FunctionComponent = () => { onEditorDidMount={(editor) => { editorRef.current = editor; }} - loading={
} />
diff --git a/packages/module/src/Canvas/Canvas.scss b/packages/module/src/Canvas/Canvas.scss index 5e82cb17b..dc89e4a71 100644 --- a/packages/module/src/Canvas/Canvas.scss +++ b/packages/module/src/Canvas/Canvas.scss @@ -128,8 +128,3 @@ --pf-v6-c-code-editor__main--BackgroundColor: var(--pf-chatbot__canvas--BackgroundColor); } } - -.pf-chatbot__canvas-editor-loading { - height: 100%; - background-color: var(--pf-chatbot__canvas--BackgroundColor); -} From 5b05fbe2366dc3fa4f4b6c95f5270f76fc0bb31c Mon Sep 17 00:00:00 2001 From: Rebecca Alpert Date: Mon, 14 Sep 2026 15:01:59 -0400 Subject: [PATCH 6/9] Address feedback --- .../chatbot/examples/demos/Canvas.tsx | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) 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 3e7257611..ead0c59f7 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 @@ -262,6 +262,13 @@ export const Canvas: FunctionComponent = () => { window.requestAnimationFrame(() => messageActionsRef.current?.focus()); }; + const handleGeneratedAiLabelKeyDown = (event: React.KeyboardEvent) => { + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault(); + event.currentTarget.click(); + } + }; + const findMatchingItems = (targetValue: string) => { let filteredConversations = Object.entries(initialConversations).reduce((acc, [key, items]) => { const filteredItems = items.filter((item) => item.text.toLowerCase().includes(targetValue.toLowerCase())); @@ -296,7 +303,7 @@ export const Canvas: FunctionComponent = () => { Upload from computer }> - Canvas mode + {`${isCanvasOpen ? 'Disable' : 'Enable'} canvas mode`} @@ -368,7 +375,14 @@ export const Canvas: FunctionComponent = () => { } > - From 5a8ea7eb43bf4a1702963766c892fbf1cb52b7fb Mon Sep 17 00:00:00 2001 From: Rebecca Alpert Date: Tue, 15 Sep 2026 09:11:07 -0400 Subject: [PATCH 8/9] Update packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/Canvas.tsx Co-authored-by: Eric Olkowski <70952936+thatblindgeye@users.noreply.github.com> --- .../content/extensions/chatbot/examples/demos/Canvas.tsx | 1 - 1 file changed, 1 deletion(-) 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 b7666840c..1f2025447 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 @@ -534,7 +534,6 @@ export const Canvas: FunctionComponent = () => { } ... /> - - +
+ + + +
@@ -100,6 +91,7 @@ The general recommended structure is as follows: | --- | --- | | `pf-chatbot__canvas` | Container for the fullscreen canvas layout and its background. | | `pf-chatbot__canvas-drawer` | Styles the drawer that contains the canvas panel. | +| `pf-chatbot__canvas-section` | Styles the section used within the drawer for focus. | | `pf-chatbot__canvas-body` | Ensures the drawer content body fills the available height. | | `pf-chatbot__canvas-column` | Arranges the canvas header, content, and footer vertically. | | `pf-chatbot__canvas-panel` | Styles the canvas drawer panel and removes the default panel spacing. | 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 1f2025447..a4a036707 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 @@ -128,6 +128,7 @@ export const Canvas: FunctionComponent = () => { const [selectedModel, setSelectedModel] = useState('GPT-4'); const [showCanvasLabel, setShowCanvasLabel] = useState(true); const [isCanvasOpen, setIsCanvasOpen] = useState(true); + const [isGeneratedAiPopoverOpen, setIsGeneratedAiPopoverOpen] = useState(false); const [isDrawerOpen, setIsDrawerOpen] = useState(false); const [chatbotVisible, setChatbotVisible] = useState(true); const [conversations, setConversations] = useState( @@ -139,6 +140,7 @@ export const Canvas: FunctionComponent = () => { const editorRef = useRef(null); const messageInputRef = useRef(null); const messageActionsRef = useRef(null); + const canvasSectionRef = useRef(null); const wasCanvasOpen = useRef(isCanvasOpen); const shouldFocusCanvasCloseButton = useRef(false); const displayMode = ChatbotDisplayMode.fullscreen; @@ -236,8 +238,8 @@ export const Canvas: FunctionComponent = () => { window.requestAnimationFrame(() => messageInputRef.current?.focus()); }; - const focusCanvasCloseButton = () => { - document.querySelector('#chatbot-canvas-panel .pf-v6-c-drawer__close button')?.focus(); + const focusCanvasSection = () => { + canvasSectionRef.current?.focus(); }; useEffect(() => { @@ -254,7 +256,7 @@ export const Canvas: FunctionComponent = () => { const onCanvasExpand = () => { if (shouldFocusCanvasCloseButton.current) { shouldFocusCanvasCloseButton.current = false; - focusCanvasCloseButton(); + focusCanvasSection(); } }; @@ -314,21 +316,33 @@ export const Canvas: FunctionComponent = () => { key="undo" icon={} aria-label="Undo" - tooltipProps={{ content: 'Undo' }} + tooltipProps={{ + content: 'Undo', + // prevents VO announcements of both aria label and tooltip + aria: 'none' + }} onClick={() => editorRef.current?.trigger('keyboard', 'undo', null)} />, } aria-label="Redo" - tooltipProps={{ content: 'Redo' }} + tooltipProps={{ + content: 'Redo', + // prevents VO announcements of both aria label and tooltip + aria: 'none' + }} onClick={() => editorRef.current?.trigger('keyboard', 'redo', null)} />, } aria-label="Export" - tooltipProps={{ content: 'Export' }} + tooltipProps={{ + content: 'Export', + // prevents VO announcements of both aria label and tooltip + aria: 'none' + }} onClick={(value) => { // eslint-disable-next-line no-console console.log('Export', value); @@ -343,78 +357,89 @@ export const Canvas: FunctionComponent = () => { defaultSize="50%" minSize="20%" id="chatbot-canvas-panel" - aria-label="Canvas" resizeAriaLabel="Resize canvas" className="pf-chatbot__canvas-panel" > - - - - - Edit code - - - - - - - - What is canvas mode? - - } - bodyContent={ - <> -
- This canvas is a collaborative workspace that blends AI-generated content with manual human edits. - You can accept an AI-generated baseline and immediately mold it with direct keystrokes, creating a - seamless partnership over a shared deliverable. -
-
Always review AI-generated code prior to use.
- - } - > -
); diff --git a/packages/module/src/Canvas/Canvas.scss b/packages/module/src/Canvas/Canvas.scss index dc89e4a71..7fb3ef27a 100644 --- a/packages/module/src/Canvas/Canvas.scss +++ b/packages/module/src/Canvas/Canvas.scss @@ -58,6 +58,14 @@ // Panel // ---------------------------------------------------------------------------- +.pf-chatbot__canvas-section { + display: flex; + flex-direction: column; + flex: 1; + min-height: 0; + height: 100%; +} + .pf-chatbot__canvas-panel { height: 100%; overflow: hidden;