Skip to content

Commit

Permalink
Merge pull request #272 from rebeccaalpert/stop-button
Browse files Browse the repository at this point in the history
feat(StopButton): Add StopButton for MessageBar
  • Loading branch information
nicolethoen authored Nov 4, 2024
2 parents 87f4ddf + de326a0 commit c7b51b8
Show file tree
Hide file tree
Showing 9 changed files with 192 additions and 34 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,16 @@ Attachments can also be added to the chatbot via [drag and drop.](/patternfly-ai

```

### Message bar with stop button

If you are using streaming, you can add a stop button to the message bar that allows users to stop a response from a chatbot.

To enable the stop button, set `hasStopButton` to `true` and pass in a `handleStopButton` callback function. You can use this callback to trigger an [AbortController](https://developer.mozilla.org/en-US/docs/Web/API/AbortController) configured as part of your API call.

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

```

### Footer with message bar and footnote

A simple footer with a message bar and footnote would have this code structure:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import React from 'react';
import { MessageBar } from '@patternfly/virtual-assistant/dist/dynamic/MessageBar';

export const ChatbotMessageBarStop: React.FunctionComponent = () => {
const handleSend = (message) => alert(message);

const handleStopButton = () => alert('Stop button clicked');

return <MessageBar handleStopButton={handleStopButton} hasStopButton onSendMessage={handleSend} />;
};
17 changes: 10 additions & 7 deletions packages/module/src/MessageBar/AttachButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,20 @@ import { useDropzone } from 'react-dropzone';
import { PaperclipIcon } from '@patternfly/react-icons/dist/esm/icons/paperclip-icon';

export interface AttachButtonProps extends ButtonProps {
/** OnClick Handler for the Attach Button */
onClick?: ((event: MouseEvent | React.MouseEvent<Element, MouseEvent> | KeyboardEvent) => void) | undefined;
/** Callback function for attach button when an attachment is made */
/** Callback for when button is clicked */
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
/** Callback function for AttachButton when an attachment is made */
onAttachAccepted?: (data: File[], event: DropEvent) => void;
/** Class Name for the Attach button */
/** Class name for AttachButton */
className?: string;
/** Props to control is the attach button should be disabled */
/** Props to control if the AttachButton should be disabled */
isDisabled?: boolean;
/** Props to control the PF Tooltip component */
tooltipProps?: TooltipProps;
/** Ref applied to AttachButton and used in tooltip */
innerRef?: React.Ref<any>;

Check warning on line 23 in packages/module/src/MessageBar/AttachButton.tsx

View workflow job for this annotation

GitHub Actions / call-build-lint-test-workflow / lint

Unexpected any. Specify a different type
/** English text "Attach" used in the tooltip */
tooltipContent?: string;
}

const AttachButtonBase: React.FunctionComponent<AttachButtonProps> = ({
Expand All @@ -30,6 +32,7 @@ const AttachButtonBase: React.FunctionComponent<AttachButtonProps> = ({
className,
tooltipProps,
innerRef,
tooltipContent = 'Attach',
...props
}: AttachButtonProps) => {
const { open, getInputProps } = useDropzone({
Expand All @@ -43,7 +46,7 @@ const AttachButtonBase: React.FunctionComponent<AttachButtonProps> = ({
<input {...getInputProps()} />
<Tooltip
id="pf-chatbot__tooltip--attach"
content="Attach"
content={tooltipContent}
position="top"
entryDelay={tooltipProps?.entryDelay || 0}
exitDelay={tooltipProps?.exitDelay || 0}
Expand All @@ -55,7 +58,7 @@ const AttachButtonBase: React.FunctionComponent<AttachButtonProps> = ({
variant="plain"
ref={innerRef}
className={`pf-chatbot__button--attach ${className ?? ''}`}
aria-label={props['aria-label'] || 'Attach Button'}
aria-label={props['aria-label'] || 'Attach button'}
isDisabled={isDisabled}
onClick={onClick ?? open}
icon={
Expand Down
1 change: 1 addition & 0 deletions packages/module/src/MessageBar/MessageBar.scss
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
@import './AttachButton';
@import './MicrophoneButton';
@import './SendButton';
@import './StopButton';

// ============================================================================
// Chatbot Footer - Message Bar
Expand Down
87 changes: 68 additions & 19 deletions packages/module/src/MessageBar/MessageBar.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import React from 'react';
import { DropEvent, TextAreaProps } from '@patternfly/react-core';
import { ButtonProps, DropEvent, TextAreaProps } from '@patternfly/react-core';
import { AutoTextArea } from 'react-textarea-auto-witdth-height';

// Import Chatbot components
import SendButton from './SendButton';
import MicrophoneButton from './MicrophoneButton';
import { AttachButton } from './AttachButton';
import AttachMenu from '../AttachMenu';
import StopButton from './StopButton';

export interface MessageBarWithAttachMenuProps {
/** Flag to enable whether attach menu is open */
Expand Down Expand Up @@ -40,12 +41,23 @@ export interface MessageBarProps extends TextAreaProps {
hasAttachButton?: boolean;
/** Flag to enable the Microphone button */
hasMicrophoneButton?: boolean;
/** Flag to enable the Stop button, used for streaming content */
hasStopButton?: boolean;
/** Callback function for when stop button is clicked */
handleStopButton?: (event: React.MouseEvent<HTMLButtonElement>) => void;
/** Callback function for when attach button is used to upload a file */
handleAttach?: (data: File[], event: DropEvent) => void;
/** Props to enable a menu that opens when the Attach button is clicked, instead of the attachment window */
attachMenuProps?: MessageBarWithAttachMenuProps;
/** Flag to provide manual control over whether send button is disabled */
isSendButtonDisabled?: boolean;
/** Prop to allow passage of additional props to buttons */
buttonProps?: {
attach: { tooltipContent?: string; props?: ButtonProps };
stop: { tooltipContent?: string; props?: ButtonProps };
send: { tooltipContent?: string; props?: ButtonProps };
microphone: { tooltipContent?: { active: string; inactive: string }; props?: ButtonProps };
};
}

export const MessageBar: React.FunctionComponent<MessageBarProps> = ({
Expand All @@ -57,6 +69,9 @@ export const MessageBar: React.FunctionComponent<MessageBarProps> = ({
handleAttach,
attachMenuProps,
isSendButtonDisabled,
handleStopButton,
hasStopButton,
buttonProps,
...props
}: MessageBarProps) => {
// Text Input
Expand All @@ -83,7 +98,7 @@ export const MessageBar: React.FunctionComponent<MessageBarProps> = ({
(event) => {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
if (!isSendButtonDisabled) {
if (!isSendButtonDisabled && !hasStopButton) {
handleSend();
}
}
Expand All @@ -96,38 +111,72 @@ export const MessageBar: React.FunctionComponent<MessageBarProps> = ({
attachMenuProps?.onAttachMenuToggleClick();
};

const messageBarContents = (
<>
<div className="pf-chatbot__message-bar-input">
<AutoTextArea
ref={textareaRef}
className="pf-chatbot__message-textarea"
value={message as any} // Added any to make the third part TextArea component types happy. Remove when replced with PF TextArea
onChange={handleChange as any} // Added any to make the third part TextArea component types happy. Remove when replced with PF TextArea
onKeyDown={handleKeyDown}
placeholder={isListeningMessage ? 'Listening' : 'Send a message...'}
aria-label={isListeningMessage ? 'Listening' : 'Send a message...'}
{...props}
const renderButtons = () => {
if (hasStopButton && handleStopButton) {
return (
<StopButton
onClick={handleStopButton}
tooltipContent={buttonProps?.stop.tooltipContent}
{...buttonProps?.stop.props}
/>
</div>
<div className="pf-chatbot__message-bar-actions">
);
}
return (
<>
{attachMenuProps && (
<AttachButton ref={attachButtonRef} onClick={handleAttachMenuToggle} isDisabled={isListeningMessage} />
<AttachButton
ref={attachButtonRef}
onClick={handleAttachMenuToggle}
isDisabled={isListeningMessage}
tooltipContent={buttonProps?.attach.tooltipContent}
{...buttonProps?.attach.props}
/>
)}
{!attachMenuProps && hasAttachButton && (
<AttachButton onAttachAccepted={handleAttach} isDisabled={isListeningMessage} />
<AttachButton
onAttachAccepted={handleAttach}
isDisabled={isListeningMessage}
tooltipContent={buttonProps?.attach.tooltipContent}
{...buttonProps?.attach.props}
/>
)}
{hasMicrophoneButton && (
<MicrophoneButton
isListening={isListeningMessage}
onIsListeningChange={setIsListeningMessage}
onSpeechRecognition={setMessage}
tooltipContent={buttonProps?.microphone.tooltipContent}
{...buttonProps?.microphone.props}
/>
)}
{(alwayShowSendButton || message) && (
<SendButton value={message} onClick={handleSend} isDisabled={isSendButtonDisabled} />
<SendButton
value={message}
onClick={handleSend}
isDisabled={isSendButtonDisabled}
tooltipContent={buttonProps?.send.tooltipContent}
{...buttonProps?.send.props}
/>
)}
</>
);
};

const messageBarContents = (
<>
<div className="pf-chatbot__message-bar-input">
<AutoTextArea
ref={textareaRef}
className="pf-chatbot__message-textarea"
value={message as any} // Added any to make the third part TextArea component types happy. Remove when replced with PF TextArea

Check warning on line 171 in packages/module/src/MessageBar/MessageBar.tsx

View workflow job for this annotation

GitHub Actions / call-build-lint-test-workflow / lint

Unexpected any. Specify a different type
onChange={handleChange as any} // Added any to make the third part TextArea component types happy. Remove when replced with PF TextArea

Check warning on line 172 in packages/module/src/MessageBar/MessageBar.tsx

View workflow job for this annotation

GitHub Actions / call-build-lint-test-workflow / lint

Unexpected any. Specify a different type
onKeyDown={handleKeyDown}
placeholder={isListeningMessage ? 'Listening' : 'Send a message...'}
aria-label={isListeningMessage ? 'Listening' : 'Send a message...'}
{...props}
/>
</div>
<div className="pf-chatbot__message-bar-actions">{renderButtons()}</div>
</>
);

Expand Down
9 changes: 6 additions & 3 deletions packages/module/src/MessageBar/MicrophoneButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,16 @@ import { MicrophoneIcon } from '@patternfly/react-icons/dist/esm/icons/microphon
export interface MicrophoneButtonProps extends ButtonProps {
/** Boolean check if the browser is listening to speech or not */
isListening: boolean;
/** Class Name for the Microphone button */
/** Class name for MicrophoneButton */
className?: string;
/** Callback to update the value of isListening */
onIsListeningChange: React.Dispatch<React.SetStateAction<boolean>>;
/** Callback to update the message value once speech recognition is complete */
onSpeechRecognition: React.Dispatch<React.SetStateAction<string>>;
/** Props to control the PF Tooltip component */
tooltipProps?: TooltipProps;
/** English text "Use microphone" and "Stop listening" used in the tooltip */
tooltipContent?: { active: string; inactive: string };
}

export const MicrophoneButton: React.FunctionComponent<MicrophoneButtonProps> = ({
Expand All @@ -28,6 +30,7 @@ export const MicrophoneButton: React.FunctionComponent<MicrophoneButtonProps> =
onSpeechRecognition,
className,
tooltipProps,
tooltipContent = { active: 'Stop listening', inactive: 'Use microphone' },
...props
}: MicrophoneButtonProps) => {
// Microphone
Expand Down Expand Up @@ -84,7 +87,7 @@ export const MicrophoneButton: React.FunctionComponent<MicrophoneButtonProps> =
aria="none"
aria-live="polite"
id="pf-chatbot__tooltip--use-microphone"
content={isListening ? 'Stop listening' : 'Use microphone'}
content={isListening ? tooltipContent.active : tooltipContent.inactive}
position={tooltipProps?.position || 'top'}
entryDelay={tooltipProps?.entryDelay || 0}
exitDelay={tooltipProps?.exitDelay || 0}
Expand All @@ -95,7 +98,7 @@ export const MicrophoneButton: React.FunctionComponent<MicrophoneButtonProps> =
<Button
variant="plain"
className={`pf-chatbot__button--microphone ${isListening ? 'pf-chatbot__button--microphone--active' : ''} ${className ?? ''}`}
aria-label={props['aria-label'] || 'Microphone Button'}
aria-label={props['aria-label'] || 'Microphone button'}
onClick={isListening ? stopListening : startListening}
icon={
<Icon iconSize="xl" isInline>
Expand Down
13 changes: 8 additions & 5 deletions packages/module/src/MessageBar/SendButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,26 @@ import { Button, ButtonProps, Tooltip, TooltipProps, Icon } from '@patternfly/re
import { PaperPlaneIcon } from '@patternfly/react-icons/dist/esm/icons/paper-plane-icon';

export interface SendButtonProps extends ButtonProps {
/** OnClick Handler for the Send Button */
onClick: () => void;
/** Class Name for the Send button */
/** Callback for when button is clicked */
onClick: (event: React.MouseEvent<HTMLButtonElement>) => void;
/** Class Name for SendButton */
className?: string;
/** Props to control the PF Tooltip component */
tooltipProps?: TooltipProps;
/** English text "Send" used in the tooltip */
tooltipContent?: string;
}

export const SendButton: React.FunctionComponent<SendButtonProps> = ({
className,
onClick,
tooltipProps,
tooltipContent = 'Send',
...props
}: SendButtonProps) => (
<Tooltip
id="pf-chatbot__tooltip--send"
content="Send"
content={tooltipContent}
position={tooltipProps?.position || 'top'}
entryDelay={tooltipProps?.entryDelay || 0}
exitDelay={tooltipProps?.exitDelay || 0}
Expand All @@ -36,7 +39,7 @@ export const SendButton: React.FunctionComponent<SendButtonProps> = ({
<Button
className={`pf-chatbot__button--send ${className ?? ''}`}
variant="link"
aria-label={props['aria-label'] || 'Send Button'}
aria-label={props['aria-label'] || 'Send button'}
onClick={onClick}
icon={
<Icon iconSize="xl" isInline>
Expand Down
22 changes: 22 additions & 0 deletions packages/module/src/MessageBar/StopButton.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// ============================================================================
// Chatbot Footer - Message Bar - Stop
// ============================================================================
.pf-v6-c-button.pf-chatbot__button--stop {
background-color: var(--pf-t--global--color--brand--default);
border-radius: var(--pf-t--global--border--radius--pill);
padding: var(--pf-t--global--spacer--md);
width: 3rem;
height: 3rem;
display: flex;
justify-content: center;
align-items: center;

.pf-v6-c-button__icon {
color: var(--pf-t--global--icon--color--inverse);
}

&:hover,
&:focus {
background-color: var(--pf-t--global--color--brand--hover);
}
}
Loading

0 comments on commit c7b51b8

Please sign in to comment.