diff --git a/packages/module/patternfly-docs/content/extensions/virtual-assistant/examples/ChatbotFooter/ChatbotFooter.md b/packages/module/patternfly-docs/content/extensions/virtual-assistant/examples/ChatbotFooter/ChatbotFooter.md index a464b6ba..578f5ba7 100644 --- a/packages/module/patternfly-docs/content/extensions/virtual-assistant/examples/ChatbotFooter/ChatbotFooter.md +++ b/packages/module/patternfly-docs/content/extensions/virtual-assistant/examples/ChatbotFooter/ChatbotFooter.md @@ -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: diff --git a/packages/module/patternfly-docs/content/extensions/virtual-assistant/examples/ChatbotFooter/ChatbotMessageBarStop.tsx b/packages/module/patternfly-docs/content/extensions/virtual-assistant/examples/ChatbotFooter/ChatbotMessageBarStop.tsx new file mode 100644 index 00000000..f94f2d8b --- /dev/null +++ b/packages/module/patternfly-docs/content/extensions/virtual-assistant/examples/ChatbotFooter/ChatbotMessageBarStop.tsx @@ -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 ; +}; diff --git a/packages/module/src/MessageBar/AttachButton.tsx b/packages/module/src/MessageBar/AttachButton.tsx index 022c7b00..51d8f69b 100644 --- a/packages/module/src/MessageBar/AttachButton.tsx +++ b/packages/module/src/MessageBar/AttachButton.tsx @@ -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 | KeyboardEvent) => void) | undefined; - /** Callback function for attach button when an attachment is made */ + /** Callback for when button is clicked */ + onClick?: (event: React.MouseEvent) => 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; + /** English text "Attach" used in the tooltip */ + tooltipContent?: string; } const AttachButtonBase: React.FunctionComponent = ({ @@ -30,6 +32,7 @@ const AttachButtonBase: React.FunctionComponent = ({ className, tooltipProps, innerRef, + tooltipContent = 'Attach', ...props }: AttachButtonProps) => { const { open, getInputProps } = useDropzone({ @@ -43,7 +46,7 @@ const AttachButtonBase: React.FunctionComponent = ({ = ({ 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={ diff --git a/packages/module/src/MessageBar/MessageBar.scss b/packages/module/src/MessageBar/MessageBar.scss index 7640a746..87c80fa8 100644 --- a/packages/module/src/MessageBar/MessageBar.scss +++ b/packages/module/src/MessageBar/MessageBar.scss @@ -2,6 +2,7 @@ @import './AttachButton'; @import './MicrophoneButton'; @import './SendButton'; +@import './StopButton'; // ============================================================================ // Chatbot Footer - Message Bar diff --git a/packages/module/src/MessageBar/MessageBar.tsx b/packages/module/src/MessageBar/MessageBar.tsx index a9e1d7cf..fe69852c 100644 --- a/packages/module/src/MessageBar/MessageBar.tsx +++ b/packages/module/src/MessageBar/MessageBar.tsx @@ -1,5 +1,5 @@ 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 @@ -7,6 +7,7 @@ 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 */ @@ -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) => 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 = ({ @@ -57,6 +69,9 @@ export const MessageBar: React.FunctionComponent = ({ handleAttach, attachMenuProps, isSendButtonDisabled, + handleStopButton, + hasStopButton, + buttonProps, ...props }: MessageBarProps) => { // Text Input @@ -83,7 +98,7 @@ export const MessageBar: React.FunctionComponent = ({ (event) => { if (event.key === 'Enter' && !event.shiftKey) { event.preventDefault(); - if (!isSendButtonDisabled) { + if (!isSendButtonDisabled && !hasStopButton) { handleSend(); } } @@ -96,38 +111,72 @@ export const MessageBar: React.FunctionComponent = ({ attachMenuProps?.onAttachMenuToggleClick(); }; - const messageBarContents = ( - <> -
- { + if (hasStopButton && handleStopButton) { + return ( + -
-
+ ); + } + return ( + <> {attachMenuProps && ( - + )} {!attachMenuProps && hasAttachButton && ( - + )} {hasMicrophoneButton && ( )} {(alwayShowSendButton || message) && ( - + )} + + ); + }; + + const messageBarContents = ( + <> +
+
+
{renderButtons()}
); diff --git a/packages/module/src/MessageBar/MicrophoneButton.tsx b/packages/module/src/MessageBar/MicrophoneButton.tsx index 5d5a9d30..931e5f46 100644 --- a/packages/module/src/MessageBar/MicrophoneButton.tsx +++ b/packages/module/src/MessageBar/MicrophoneButton.tsx @@ -12,7 +12,7 @@ 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>; @@ -20,6 +20,8 @@ export interface MicrophoneButtonProps extends ButtonProps { onSpeechRecognition: React.Dispatch>; /** 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 = ({ @@ -28,6 +30,7 @@ export const MicrophoneButton: React.FunctionComponent = onSpeechRecognition, className, tooltipProps, + tooltipContent = { active: 'Stop listening', inactive: 'Use microphone' }, ...props }: MicrophoneButtonProps) => { // Microphone @@ -84,7 +87,7 @@ export const MicrophoneButton: React.FunctionComponent = 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} @@ -95,7 +98,7 @@ export const MicrophoneButton: React.FunctionComponent =