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 00000000..04ccd33f --- /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 356927d4..67555249 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 00000000..4b39e956 --- /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 e6802036..bd8edd2e 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';