Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(UserFeedback): Add user feedback cards #409

Merged
merged 10 commits into from
Jan 27, 2025
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import React from 'react';
import Message from '@patternfly/chatbot/dist/dynamic/Message';
import patternflyAvatar from './patternfly_avatar.jpg';
import { Checkbox, FormGroup, Stack } from '@patternfly/react-core';

export const MessageWithFeedbackExample: React.FunctionComponent = () => {
const [hasCloseButton, setHasCloseButton] = React.useState(false);
const [hasTextArea, setHasTextArea] = React.useState(false);

return (
<>
<Stack hasGutter>
<FormGroup role="radiogroup" isInline fieldId="feedback-card" label="Variant">
<Checkbox
isChecked={hasTextArea}
onChange={() => {
setHasTextArea(!hasTextArea);
}}
name="basic-inline-radio"
label="Has text area"
id="has-text-area"
/>
</FormGroup>
<Message
name="Bot"
role="bot"
avatar={patternflyAvatar}
content="This is a message with the feedback card:"
userFeedbackForm={{
quickResponses: [
{ id: '1', content: 'Helpful information' },
{ id: '2', content: 'Easy to understand' },
{ id: '3', content: 'Resolved my issue' }
],
onSubmit: (quickResponse, additionalFeedback) =>
alert(`Selected ${quickResponse} and received the additional feedback: ${additionalFeedback}`),
hasTextArea,
// eslint-disable-next-line no-console
onClose: () => console.log('closed feedback form'),
focusOnLoad: false
}}
/>
</Stack>
<Stack hasGutter>
<FormGroup role="radiogroup" isInline fieldId="feedback-thank-you" label="Variant">
<Checkbox
isChecked={hasCloseButton}
onChange={() => {
setHasCloseButton(!hasCloseButton);
}}
name="basic-inline-radio"
label="Has close button"
id="has-close"
/>
</FormGroup>
<Message
name="Bot"
role="bot"
avatar={patternflyAvatar}
content="This is a thank-you message, which is displayed once the feedback card is submitted:"
// eslint-disable-next-line no-console
userFeedbackComplete={{
// eslint-disable-next-line no-console
onClose: hasCloseButton ? () => console.log('closed completion message') : undefined,
focusOnLoad: false
}}
/>
</Stack>
</>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React from 'react';
import Message from '@patternfly/chatbot/dist/dynamic/Message';
import patternflyAvatar from './patternfly_avatar.jpg';
import { Button } from '@patternfly/react-core';

export const MessageWithFeedbackTimeoutExample: React.FunctionComponent = () => {
const [hasFeedback, setHasFeedback] = React.useState(false);

return (
<>
<Button variant="secondary" onClick={() => setHasFeedback(true)}>
Show card
</Button>
<Button variant="secondary" onClick={() => setHasFeedback(false)}>
Remove card
</Button>
<Message
name="Bot"
role="bot"
avatar={patternflyAvatar}
content="This completion message times out after you click **Show card**:"
userFeedbackComplete={hasFeedback ? { timeout: true, onTimeout: () => setHasFeedback(false) } : undefined}
isLiveRegion
/>
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,17 @@ propComponents:
[
'AttachMenu',
'AttachmentEdit',
'FileDetails',
'FileDetailsLabel',
'FileDetailsProps',
'FileDetailsLabelProps',
'FileDropZone',
'PreviewAttachment',
'Message',
'PreviewAttachment',
'ActionProps',
'SourcesCardProps'
'SourcesCardProps',
'UserFeedbackProps',
'UserFeedbackCompleteProps',
'QuickResponseProps'
]
sortValue: 3
---
Expand Down Expand Up @@ -52,12 +55,8 @@ The `content` prop of the `<Message>` component is passed to a `<Markdown>` comp

Messages from the ChatBot will be marked with an "AI" label to clearly communicate the use of AI to users. The ChatBot can display different `content` types, including plain text, code, or a loading animation (via `isLoading`).

<br />

By default, a date and timestamp is displayed with each message. We recommend using the `timestamp` prop in real ChatBots, since it will allow you to set persistent dates and times on messages, even if the messages re-render. You can update `timestamp` with a different [date and time format](/ux-writing/numerics) as needed.

<br />

You can further customize the avatar by applying an additional class or passing [PatternFly avatar props](/components/avatar) to the `<Message>` component via `avatarProps`.

```js file="./BotMessage.tsx"
Expand Down Expand Up @@ -97,6 +96,37 @@ You can apply a `clickedAriaLabel` and `clickedTooltipContent` once a button is

```

### Message feedback

When a user selects a positive or negative [message action](#message-actions), you can display a message feedback card that acknowledges their response and provides space for additional written feedback. These cards can be manually dismissed via the close button and the thank-you card can be [configured to time out automatically](/patternfly-ai/chatbot/messages#message-feedback-with-timeouts).

You can see the full feedback flow [in the message demos](/patternfly-ai/chatbot/messages/demo#message-feedback).

The message feedback cards will immediately receive focus by default, but you can remove this behavior by passing `focusOnLoad: false` to the `<Message>` (as shown in the following examples). For better usability, you should generally keep the default focus behavior.

The following examples demonstrate:

- A basic feedback card. To toggle the text input area, select the **Has text area** checkbox.
- A thank-you card. To toggle the close button, select the **Has close button** checkbox.

```js file="./MessageWithFeedback.tsx"

```

### Message feedback with timeouts

The feedback thank-you message can be configured to time out and automatically close after a period of time. The default time-out period is 8000 ms, but it can be customized via `timeout`.

To display the thank-you message in this example, click **Show card**.

The card will not dismiss within the default time if a user is hovering over it or if it has keyboard focus. Instead, it will dismiss after they remove focus, via `timeoutAnimation`, which is 3000 ms by default. You can adjust this duration and set an `onTimeout` callback, as well as optional `onMouseEnter` and `onMouseLeave` callbacks.

For accessibility purposes, be sure to announce when new content appears onscreen. `isLiveRegion` is set to true by default on `<Message>` so it will make appropriate announcements for you when the thank-you card appears.

```js file="./MessageWithFeedbackTimeout.tsx"

```

### Messages with quick responses

You can offer convenient, clickable responses to messages in the form of quick actions. Quick actions are [PatternFly labels](/components/label/) in a label group, configured to display up to 5 visible labels. Only 1 response can be selected at a time.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,11 +102,9 @@ The "embedded" display mode is meant to be used within a [PatternFly page](/comp
### Content and message box

The `<ChatbotContent>` component is the container that is placed within the `<Chatbot>`, between the [`<ChatbotHeader>`](/patternfly-ai/chatbot/ui#header) and [`<ChatbotFooter>`.](/patternfly-ai/chatbot/ui#footer)
<br />
<br />

`<ChatbotContent>` usually contains a `<ChatbotMessageBox>` for displaying messages.
<br />
<br />

Your code structure should look like this:

```noLive
Expand Down Expand Up @@ -144,8 +142,7 @@ To provide users with a more specific direction, you can also include optional w
### Skip to content

To provide page context, we recommend using a "Skip to chatbot" button. This allows you to skip past other content on the page, directly to the ChatBot content, using a [PatternFly skip to content component](/components/skip-to-content). To display this button, you must tab into the main window.
<br />
<br />

When using default or docked modes, we recommend putting focus on the toggle if the ChatBot is closed, and the ChatBot when it is open. For fullscreen and embedded, we recommend putting the focus on the first focusable item in the ChatBot, such as a menu toggle. This can be seen in our more fully-featured demos for the [default, embedded, and fullscreen ChatBot](/patternfly-ai/chatbot/overview/demo#basic-chatbot) and the [embedded ChatBot](/patternfly-ai/chatbot/overview/demo#embedded-chatbot).

```js file="./SkipToContent.tsx" isFullscreen
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,20 @@ import patternflyAvatar from '../Messages/patternfly_avatar.jpg';

## Demos

### Message feedback

When a user selects a positive or negative message action, you can display a message feedback card that acknowledges their response and provides space for additional written feedback. These cards can be manually dismissed via the close button and the thank-you card can be configured to time out automatically.

The following example demonstrates a full feedback flow, which accepts written feedback submission and displays a thank you card.

It also demonstrates how to handle focus appropriately for accessibility. The card will be focused when it appears in the DOM. When the card closes, place the focus back on the launching button. To provide additional context on what the button controls, you can also add `aria-expanded` and `aria-controls` attributes to the feedback buttons.

It is also important to announce when new content appears onscreen for accessibility purposes. `isLiveRegion` is set to true by default on `<Message>` so it will make appropriate announcements for you when the feedback card appears.

```js file="./Feedback.tsx"

```

### Attach via upload button in message bar

This demo displays unique attachment features, including:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import React from 'react';
import Message from '@patternfly/chatbot/dist/dynamic/Message';
import patternflyAvatar from '../Messages/patternfly_avatar.jpg';

export const MessageWithFeedbackExample: React.FunctionComponent = () => {
const [showUserFeedbackForm, setShowUserFeedbackForm] = React.useState(false);
const [showCompletionForm, setShowCompletionForm] = React.useState(false);
const [launchButton, setLaunchButton] = React.useState<string>();
const positiveRef = React.useRef<HTMLButtonElement>(null);
const negativeRef = React.useRef<HTMLButtonElement>(null);
const feedbackId = 'user-feedback-form';
const completeId = 'user-feedback-received';

const getCurrentCard = () => {
if (showUserFeedbackForm) {
return feedbackId;
}
if (showCompletionForm) {
return completeId;
}
};

const isExpanded = showUserFeedbackForm || showCompletionForm;

const focusLaunchButton = () => {
if (launchButton === 'positive') {
positiveRef.current?.focus();
}
if (launchButton === 'negative') {
negativeRef.current?.focus();
}
};

return (
<Message
name="Bot"
role="bot"
avatar={patternflyAvatar}
content="Bot message with user feedback flow; click on a message action to launch the feedback flow. Click submit to see the thank-you message."
actions={{
positive: {
onClick: () => {
setShowUserFeedbackForm(true);
setShowCompletionForm(false);
setLaunchButton('positive');
},
/* These are important for accessibility */
'aria-expanded': isExpanded,
'aria-controls': getCurrentCard(),
ref: positiveRef
},
negative: {
onClick: () => {
setShowUserFeedbackForm(true);
setShowCompletionForm(false);
setLaunchButton('negative');
},
/* These are important for accessibility */
'aria-expanded': isExpanded,
'aria-controls': getCurrentCard(),
ref: negativeRef
}
}}
userFeedbackForm={
showUserFeedbackForm
? /* eslint-disable indent */
{
quickResponses: [
{ id: '1', content: 'Helpful information' },
{ id: '2', content: 'Easy to understand' },
{ id: '3', content: 'Resolved my issue' }
],
onSubmit: (quickResponse, additionalFeedback) => {
alert(`Selected ${quickResponse} and received the additional feedback: ${additionalFeedback}`);
setShowUserFeedbackForm(false);
setShowCompletionForm(true);
focusLaunchButton();
},
hasTextArea: true,
onClose: () => {
setShowUserFeedbackForm(false);
focusLaunchButton();
},
id: feedbackId
}
: undefined
/* eslint-enable indent */
}
userFeedbackComplete={
showCompletionForm
? /* eslint-disable indent */
{
onClose: () => {
setShowCompletionForm(false);
focusLaunchButton();
},
id: completeId
}
: undefined
/* eslint-enable indent */
}
/>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,13 @@ export const ChatbotConversationHistoryDropdown: React.FunctionComponent<Chatbot
const [isOpen, setIsOpen] = React.useState(false);

const toggle = (toggleRef: React.Ref<MenuToggleElement>) => (
<Tooltip className="pf-chatbot__tooltip" content={label ?? 'Conversation options'} position="bottom">
<Tooltip
className="pf-chatbot__tooltip"
content={label ?? 'Conversation options'}
position="bottom"
// prevents VO announcements of both aria label and tooltip
aria="none"
>
<MenuToggle
className="pf-chatbot__history-actions"
variant="plain"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,13 @@ const ChatbotHeaderCloseButtonBase: React.FunctionComponent<ChatbotHeaderCloseBu
tooltipContent = 'Close'
}: ChatbotHeaderCloseButtonProps) => (
<div className={`pf-chatbot__menu ${className}`}>
<Tooltip content={tooltipContent} position="bottom" {...tooltipProps}>
<Tooltip
content={tooltipContent}
position="bottom"
// prevents VO announcements of both aria label and tooltip
aria="none"
{...tooltipProps}
>
<Button
className="pf-chatbot__button--toggle-menu"
variant="plain"
Expand Down
8 changes: 7 additions & 1 deletion packages/module/src/ChatbotHeader/ChatbotHeaderMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,13 @@ const ChatbotHeaderMenuBase: React.FunctionComponent<ChatbotHeaderMenuProps> = (
tooltipContent = 'Menu'
}: ChatbotHeaderMenuProps) => (
<div className={`pf-chatbot__menu ${className}`}>
<Tooltip content={tooltipContent} position="bottom" {...tooltipProps}>
<Tooltip
content={tooltipContent}
position="bottom"
// prevents VO announcements of both aria label and tooltip
aria="none"
{...tooltipProps}
>
<Button
className="pf-chatbot__button--toggle-menu"
variant="plain"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,14 @@ export const ChatbotHeaderOptionsDropdown: React.FunctionComponent<ChatbotHeader
const [isOptionsMenuOpen, setIsOptionsMenuOpen] = React.useState(false);

const toggle = (toggleRef: React.Ref<MenuToggleElement>) => (
<Tooltip className="pf-chatbot__tooltip" content="Chatbot options" position="bottom" {...tooltipProps}>
<Tooltip
className="pf-chatbot__tooltip"
content="Chatbot options"
position="bottom"
// prevents VO announcements of both aria label and tooltip
aria="none"
{...tooltipProps}
>
<MenuToggle
className="pf-chatbot__button--toggle-options"
variant="plain"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,14 @@ export const ChatbotHeaderSelectorDropdown: React.FunctionComponent<ChatbotHeade
const [defaultAriaLabel, setDefaultAriaLabel] = React.useState('Chatbot selector');

const toggle = (toggleRef: React.Ref<MenuToggleElement>) => (
<Tooltip className="pf-chatbot__tooltip" content={tooltipContent} position="bottom" {...tooltipProps}>
<Tooltip
className="pf-chatbot__tooltip"
content={tooltipContent}
position="bottom"
// prevents VO announcements of both aria label and tooltip
aria="none"
{...tooltipProps}
>
<MenuToggle
variant="secondary"
aria-label={menuToggleAriaLabel ?? defaultAriaLabel}
Expand Down
Loading
Loading