Skip to content

Commit

Permalink
fix(MessageBox): Adjust spacing
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
rebeccaalpert committed Nov 20, 2024
1 parent 0019c2e commit 9caed0a
Show file tree
Hide file tree
Showing 7 changed files with 212 additions and 51 deletions.
Original file line number Diff line number Diff line change
@@ -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<MessageProps[]>([]);
const [isSendButtonDisabled, setIsSendButtonDisabled] = React.useState(false);
const [announcement, setAnnouncement] = React.useState<string>();
const [position, setPosition] = React.useState<'top' | 'bottom'>('top');
const scrollToBottomRef = React.useRef<HTMLDivElement>(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 (
<>
<FormGroup role="radiogroup" isInline fieldId="basic-form-radio-group" label="Position">
<Radio
isChecked={position === 'top'}
onChange={() => setPosition('top')}
name="basic-inline-radio"
label="Top"
id="top"
/>
<Radio
isChecked={position === 'bottom'}
onChange={() => setPosition('bottom')}
name="basic-inline-radio"
label="Bottom"
id="bottom"
/>
</FormGroup>
<Chatbot displayMode={displayMode} isVisible={isVisible}>
<ChatbotContent>
{/* Update the announcement prop on MessageBox whenever a new message is sent
so that users of assistive devices receive sufficient context */}
<MessageBox announcement={announcement} position={position}>
{messages.length === 0 && (
<ChatbotWelcomePrompt title="Hello, Chatbot User" description="How may I help you today?" />
)}
{/* 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 (
<>
<div ref={scrollToBottomRef}></div>
<Message key={message.id} {...message} />
</>
);
}
return <Message key={message.id} {...message} />;
})}
</MessageBox>
</ChatbotContent>
<ChatbotFooter>
<MessageBar onSendMessage={handleSend} hasMicrophoneButton isSendButtonDisabled={isSendButtonDisabled} />
</ChatbotFooter>
</Chatbot>
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -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
---

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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 `<MessageBox>` whenever you display a new message in `<MessageBox>`. 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 `<MessageBox>` 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.

Expand Down Expand Up @@ -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 `<ChatbotConversationHistoryNav>` component is a wrapper placed within `<Chatbot>`, 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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
34 changes: 18 additions & 16 deletions packages/module/src/ChatbotWelcomePrompt/ChatbotWelcomePrompt.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,22 +42,24 @@ export const ChatbotWelcomePrompt: React.FunctionComponent<ChatbotWelcomePromptP
<span className="pf-chatbot__question">{description}</span>
</Content>

<div className="pf-chatbot__prompt-suggestions">
{prompts?.map((prompt, index) => (
<Card key={`welcome-prompt-${index}`} className="pf-chatbot__prompt-suggestion" isClickable>
<CardHeader
selectableActions={{
onClickAction: prompt.onClick,
selectableActionId: `welcome-prompt-input-${index}`,
selectableActionAriaLabelledby: `welcome-prompt-title-${index}`
}}
>
<CardTitle id={`welcome-prompt-title-${index}`}>{prompt.title}</CardTitle>
</CardHeader>
{prompt.message && <CardBody>{prompt.message}</CardBody>}
</Card>
))}
</div>
{prompts && (
<div className="pf-chatbot__prompt-suggestions">
{prompts?.map((prompt, index) => (
<Card key={`welcome-prompt-${index}`} className="pf-chatbot__prompt-suggestion" isClickable>
<CardHeader
selectableActions={{
onClickAction: prompt.onClick,
selectableActionId: `welcome-prompt-input-${index}`,
selectableActionAriaLabelledby: `welcome-prompt-title-${index}`
}}
>
<CardTitle id={`welcome-prompt-title-${index}`}>{prompt.title}</CardTitle>
</CardHeader>
{prompt.message && <CardBody>{prompt.message}</CardBody>}
</Card>
))}
</div>
)}
</div>
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,6 @@ exports[`ChatbotWelcomePrompt should render welcome prompt 1`] = `
How may I help you today?
</span>
</h1>
<div
class="pf-chatbot__prompt-suggestions"
/>
</div>
</div>
`;
6 changes: 5 additions & 1 deletion packages/module/src/MessageBox/MessageBox.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 5 additions & 2 deletions packages/module/src/MessageBox/MessageBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,17 @@ export interface MessageBoxProps extends React.HTMLProps<HTMLDivElement> {
className?: string;
/** Ref applied to message box */
innerRef?: React.Ref<HTMLDivElement>;
/** Modifier that controls how content in MessageBox is positioned within the container */
position?: 'top' | 'bottom';
}

const MessageBoxBase: React.FunctionComponent<MessageBoxProps> = ({
announcement,
ariaLabel = 'Scrollable message log',
children,
innerRef,
className
className,
position = 'top'
}: MessageBoxProps) => {
const [atTop, setAtTop] = React.useState(false);
const [atBottom, setAtBottom] = React.useState(true);
Expand Down Expand Up @@ -91,7 +94,7 @@ const MessageBoxBase: React.FunctionComponent<MessageBoxProps> = ({
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}
Expand Down

0 comments on commit 9caed0a

Please sign in to comment.