From 9caed0a892a1ea661a8f867cccacbc3f9f7b03bd Mon Sep 17 00:00:00 2001 From: Rebecca Alpert Date: Fri, 15 Nov 2024 17:48:23 -0500 Subject: [PATCH] fix(MessageBox): Adjust spacing WelcomePrompt had spacing on top, while MessageBox didn't. I moved the spacing onto the parent since WelcomePrompt is optional. Also adding prop to control position of MessageBox content, and adding a demo for how the removal interaction could work. Design wants to demonstrate the welcome message going away when new messages are sent. --- .../examples/UI/ChatbotWelcomeInteraction.tsx | 144 ++++++++++++++++++ .../virtual-assistant/examples/UI/UI.md | 68 +++++---- .../ChatbotWelcomePrompt.scss | 1 - .../ChatbotWelcomePrompt.tsx | 34 +++-- .../ChatbotWelcomePrompt.test.tsx.snap | 3 - .../module/src/MessageBox/MessageBox.scss | 6 +- packages/module/src/MessageBox/MessageBox.tsx | 7 +- 7 files changed, 212 insertions(+), 51 deletions(-) create mode 100644 packages/module/patternfly-docs/content/extensions/virtual-assistant/examples/UI/ChatbotWelcomeInteraction.tsx diff --git a/packages/module/patternfly-docs/content/extensions/virtual-assistant/examples/UI/ChatbotWelcomeInteraction.tsx b/packages/module/patternfly-docs/content/extensions/virtual-assistant/examples/UI/ChatbotWelcomeInteraction.tsx new file mode 100644 index 00000000..4f68e5d9 --- /dev/null +++ b/packages/module/patternfly-docs/content/extensions/virtual-assistant/examples/UI/ChatbotWelcomeInteraction.tsx @@ -0,0 +1,144 @@ +import React from 'react'; + +import Chatbot, { ChatbotDisplayMode } from '@patternfly/virtual-assistant/dist/dynamic/Chatbot'; +import ChatbotContent from '@patternfly/virtual-assistant/dist/dynamic/ChatbotContent'; +import ChatbotWelcomePrompt from '@patternfly/virtual-assistant/dist/dynamic/ChatbotWelcomePrompt'; +import ChatbotFooter from '@patternfly/virtual-assistant/dist/dynamic/ChatbotFooter'; +import MessageBar from '@patternfly/virtual-assistant/dist/dynamic/MessageBar'; +import MessageBox from '@patternfly/virtual-assistant/dist/dynamic/MessageBox'; +import Message, { MessageProps } from '@patternfly/virtual-assistant/dist/dynamic/Message'; +import userAvatar from '../Messages/user_avatar.jpg'; +import patternflyAvatar from '../Messages/patternfly_avatar.jpg'; +import { FormGroup, Radio } from '@patternfly/react-core'; + +export const ChatbotWelcomeInteractionDemo: React.FunctionComponent = () => { + const [messages, setMessages] = React.useState([]); + const [isSendButtonDisabled, setIsSendButtonDisabled] = React.useState(false); + const [announcement, setAnnouncement] = React.useState(); + const [position, setPosition] = React.useState<'top' | 'bottom'>('top'); + const scrollToBottomRef = React.useRef(null); + const isVisible = true; + const displayMode = ChatbotDisplayMode.default; + + // you will likely want to come up with your own unique id function; this is for demo purposes only + const generateId = () => { + const id = Date.now() + Math.random(); + return id.toString(); + }; + + const handleSend = (message: string) => { + setIsSendButtonDisabled(true); + const newMessages: MessageProps[] = []; + // We can't use structuredClone since messages contains functions, but we can't mutate + // items that are going into state or the UI won't update correctly + messages.forEach((message) => newMessages.push(message)); + // It's important to set a timestamp prop since the Message components re-render. + // The timestamps re-render with them. + const date = new Date(); + newMessages.push({ + id: generateId(), + role: 'user', + content: message, + name: 'User', + avatar: userAvatar, + timestamp: date.toLocaleString() + }); + newMessages.push({ + id: generateId(), + role: 'bot', + content: 'API response goes here', + name: 'Bot', + isLoading: true, + avatar: patternflyAvatar, + timestamp: date.toLocaleString() + }); + setMessages(newMessages); + // make announcement to assistive devices that new messages have been added + setAnnouncement(`Message from User: ${message}. Message from Bot is loading.`); + + // this is for demo purposes only; in a real situation, there would be an API response we would wait for + setTimeout(() => { + const loadedMessages: MessageProps[] = []; + // We can't use structuredClone since messages contains functions, but we can't mutate + // items that are going into state or the UI won't update correctly + newMessages.forEach((message) => loadedMessages.push(message)); + loadedMessages.pop(); + loadedMessages.push({ + id: generateId(), + role: 'bot', + content: 'API response goes here', + name: 'Bot', + isLoading: false, + avatar: patternflyAvatar, + timestamp: date.toLocaleString(), + 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 + share: { onClick: () => console.log('Share') }, + // eslint-disable-next-line no-console + listen: { onClick: () => console.log('Listen') } + } + }); + setMessages(loadedMessages); + // make announcement to assistive devices that new message has loaded + setAnnouncement(`Message from Bot: API response goes here`); + setIsSendButtonDisabled(false); + }, 5000); + }; + + return ( + <> + + setPosition('top')} + name="basic-inline-radio" + label="Top" + id="top" + /> + setPosition('bottom')} + name="basic-inline-radio" + label="Bottom" + id="bottom" + /> + + + + {/* Update the announcement prop on MessageBox whenever a new message is sent + so that users of assistive devices receive sufficient context */} + + {messages.length === 0 && ( + + )} + {/* This code block enables scrolling to the top of the last message. + You can instead choose to move the div with scrollToBottomRef on it below + the map of messages, so that users are forced to scroll to the bottom. + If you are using streaming, you will want to take a different approach; + see: https://github.com/patternfly/virtual-assistant/issues/201#issuecomment-2400725173 */} + {messages.map((message, index) => { + if (index === messages.length - 1) { + return ( + <> +
+ + + ); + } + return ; + })} +
+
+ + + +
+ + ); +}; diff --git a/packages/module/patternfly-docs/content/extensions/virtual-assistant/examples/UI/UI.md b/packages/module/patternfly-docs/content/extensions/virtual-assistant/examples/UI/UI.md index 179f6566..4cb648c9 100644 --- a/packages/module/patternfly-docs/content/extensions/virtual-assistant/examples/UI/UI.md +++ b/packages/module/patternfly-docs/content/extensions/virtual-assistant/examples/UI/UI.md @@ -10,32 +10,33 @@ id: UI source: react # If you use typescript, the name of the interface to display props for # These are found through the sourceProps function provided in patternfly-docs.source.js -propComponents: [ - 'Chatbot', - 'ChatbotContent', - 'MessageBox', - 'ChatbotWelcomePrompt', - 'WelcomePrompt', - 'ChatbotToggle', - 'ChatbotHeader', - 'ChatbotHeaderMain', - 'ChatbotHeaderMenu', - 'ChatbotHeaderActions', - 'ChatbotHeaderTitle', - 'ChatbotHeaderOptionsDropdown', - 'ChatbotHeaderSelectorDropdown', - 'ChatbotFooter', - 'MessageBar', - 'ChatbotFootnote', - 'ChatbotFootnotePopover', - 'ChatbotFootnotePopoverCTA', - 'ChatbotFootnotePopoverBannerImage', - 'ChatbotFootnotePopoverLink', - 'MessageBarWithAttachMenuProps', - 'SourceDetailsMenuItem', - 'ChatbotConversationHistoryNav', - 'Conversation' -] +propComponents: + [ + 'Chatbot', + 'ChatbotContent', + 'MessageBox', + 'ChatbotWelcomePrompt', + 'WelcomePrompt', + 'ChatbotToggle', + 'ChatbotHeader', + 'ChatbotHeaderMain', + 'ChatbotHeaderMenu', + 'ChatbotHeaderActions', + 'ChatbotHeaderTitle', + 'ChatbotHeaderOptionsDropdown', + 'ChatbotHeaderSelectorDropdown', + 'ChatbotFooter', + 'MessageBar', + 'ChatbotFootnote', + 'ChatbotFootnotePopover', + 'ChatbotFootnotePopoverCTA', + 'ChatbotFootnotePopoverBannerImage', + 'ChatbotFootnotePopoverLink', + 'MessageBarWithAttachMenuProps', + 'SourceDetailsMenuItem', + 'ChatbotConversationHistoryNav', + 'Conversation' + ] sortValue: 2 --- @@ -74,6 +75,8 @@ import ExpandIcon from '@patternfly/react-icons/dist/esm/icons/expand-icon'; import OpenDrawerRightIcon from '@patternfly/react-icons/dist/esm/icons/open-drawer-right-icon'; import PFHorizontalLogoColor from './PF-HorizontalLogo-Color.svg'; import PFHorizontalLogoReverse from './PF-HorizontalLogo-Reverse.svg'; +import userAvatar from '../Messages/user_avatar.jpg'; +import patternflyAvatar from '../Messages/patternfly_avatar.jpg'; ## Structure @@ -113,9 +116,17 @@ Your code structure should look like this: **Note**: When messages update, it is important to announce new messages to users of assistive technology. To do this, make sure to set the `announcement` prop on `` whenever you display a new message in ``. You can view this in action in our [basic chatbot](/patternfly-ai/chatbot/overview/demo#basic-chatbot) and [embedded chatbot](/patternfly-ai/chatbot/overview/demo#embedded-chatbot) demos. -### Welcome prompt +### Welcome message + +To introduce users to the chatbot experience, display a welcome message before they input their first message. This brief message should follow our [conversation design guidelines](/patternfly-ai/conversation-design) to welcome users to the chatbot experience and encourage them to interact. + +This message can be dismissed once a user sends their first message. To change the arrangement of the message within the message box, specify the `position` in the `` component. + +```js file="./ChatbotWelcomeInteraction.tsx" isFullscreen -To introduce users to the chatbot experience, a welcome prompt can fill the message box before they input their first message. This brief message should follow our [conversation design guidelines](/patternfly-ai/conversation-design) to welcome users to the chatbot experience and encourage them to interact. +``` + +### Welcome prompt To provide users with a more specific direction, you can also include optional welcome prompts. @@ -265,6 +276,7 @@ To enable the stop button, set `hasStopButton` to `true` and pass in a `handleSt ## Navigation ### Side nav in a drawer + The chatbot conversation history is contained in an interactive drawer, where users can interact with previous conversations or start a new conversation. The `` component is a wrapper placed within ``, which contains all other chatbot components in `drawerContent`. There is a focus trap so users can only tab within the drawer while it is open. diff --git a/packages/module/src/ChatbotWelcomePrompt/ChatbotWelcomePrompt.scss b/packages/module/src/ChatbotWelcomePrompt/ChatbotWelcomePrompt.scss index a7345cf4..bfbdbae3 100644 --- a/packages/module/src/ChatbotWelcomePrompt/ChatbotWelcomePrompt.scss +++ b/packages/module/src/ChatbotWelcomePrompt/ChatbotWelcomePrompt.scss @@ -3,7 +3,6 @@ // ============================================================================ .pf-chatbot--layout--welcome { padding-block-end: var(--pf-t--global--spacer--lg); - padding-block-start: var(--pf-t--global--spacer--lg); flex-direction: column; display: flex; gap: var(--pf-t--global--spacer--lg); diff --git a/packages/module/src/ChatbotWelcomePrompt/ChatbotWelcomePrompt.tsx b/packages/module/src/ChatbotWelcomePrompt/ChatbotWelcomePrompt.tsx index b95c5ea7..afe7a6e2 100644 --- a/packages/module/src/ChatbotWelcomePrompt/ChatbotWelcomePrompt.tsx +++ b/packages/module/src/ChatbotWelcomePrompt/ChatbotWelcomePrompt.tsx @@ -42,22 +42,24 @@ export const ChatbotWelcomePrompt: React.FunctionComponent{description} -
- {prompts?.map((prompt, index) => ( - - - {prompt.title} - - {prompt.message && {prompt.message}} - - ))} -
+ {prompts && ( +
+ {prompts?.map((prompt, index) => ( + + + {prompt.title} + + {prompt.message && {prompt.message}} + + ))} +
+ )} ); diff --git a/packages/module/src/ChatbotWelcomePrompt/__snapshots__/ChatbotWelcomePrompt.test.tsx.snap b/packages/module/src/ChatbotWelcomePrompt/__snapshots__/ChatbotWelcomePrompt.test.tsx.snap index c5ba441d..f6f8cff2 100644 --- a/packages/module/src/ChatbotWelcomePrompt/__snapshots__/ChatbotWelcomePrompt.test.tsx.snap +++ b/packages/module/src/ChatbotWelcomePrompt/__snapshots__/ChatbotWelcomePrompt.test.tsx.snap @@ -24,9 +24,6 @@ exports[`ChatbotWelcomePrompt should render welcome prompt 1`] = ` How may I help you today? -
`; diff --git a/packages/module/src/MessageBox/MessageBox.scss b/packages/module/src/MessageBox/MessageBox.scss index e7731b11..798f90b9 100644 --- a/packages/module/src/MessageBox/MessageBox.scss +++ b/packages/module/src/MessageBox/MessageBox.scss @@ -6,7 +6,11 @@ display: flex; flex-direction: column; row-gap: var(--pf-t--global--spacer--sm); - padding: 0 var(--pf-t--global--spacer--lg) var(--pf-t--global--spacer--lg) var(--pf-t--global--spacer--lg); + padding: var(--pf-t--global--spacer--lg); +} + +.pf-chatbot__messagebox--bottom { + justify-content: flex-end; } // hide from view but not assistive technologies diff --git a/packages/module/src/MessageBox/MessageBox.tsx b/packages/module/src/MessageBox/MessageBox.tsx index ed8e2b4e..3604664e 100644 --- a/packages/module/src/MessageBox/MessageBox.tsx +++ b/packages/module/src/MessageBox/MessageBox.tsx @@ -15,6 +15,8 @@ export interface MessageBoxProps extends React.HTMLProps { className?: string; /** Ref applied to message box */ innerRef?: React.Ref; + /** Modifier that controls how content in MessageBox is positioned within the container */ + position?: 'top' | 'bottom'; } const MessageBoxBase: React.FunctionComponent = ({ @@ -22,7 +24,8 @@ const MessageBoxBase: React.FunctionComponent = ({ ariaLabel = 'Scrollable message log', children, innerRef, - className + className, + position = 'top' }: MessageBoxProps) => { const [atTop, setAtTop] = React.useState(false); const [atBottom, setAtBottom] = React.useState(true); @@ -91,7 +94,7 @@ const MessageBoxBase: React.FunctionComponent = ({ role="region" tabIndex={0} aria-label={ariaLabel} - className={`pf-chatbot__messagebox ${className ?? ''}`} + className={`pf-chatbot__messagebox ${position === 'bottom' && 'pf-chatbot__messagebox--bottom'} ${className ?? ''}`} ref={innerRef ?? messageBoxRef} > {children}