From 67622ba92c30e64ad0ea72abd7455b44a608d1d4 Mon Sep 17 00:00:00 2001 From: Eric Olkowski Date: Tue, 13 Jan 2026 14:29:27 -0500 Subject: [PATCH 1/3] feat(MessageBar): added custom attach and additional actions --- .../UI/ChatbotMessageBarCustomActions.tsx | 187 ++++++++++++++++++ .../extensions/chatbot/examples/UI/UI.md | 17 +- .../module/src/MessageBar/MessageBar.scss | 16 +- packages/module/src/MessageBar/MessageBar.tsx | 136 ++++++++----- 4 files changed, 303 insertions(+), 53 deletions(-) create mode 100644 packages/module/patternfly-docs/content/extensions/chatbot/examples/UI/ChatbotMessageBarCustomActions.tsx diff --git a/packages/module/patternfly-docs/content/extensions/chatbot/examples/UI/ChatbotMessageBarCustomActions.tsx b/packages/module/patternfly-docs/content/extensions/chatbot/examples/UI/ChatbotMessageBarCustomActions.tsx new file mode 100644 index 000000000..9411cde37 --- /dev/null +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/UI/ChatbotMessageBarCustomActions.tsx @@ -0,0 +1,187 @@ +import { useState, FunctionComponent, ReactNode } from 'react'; +import { MessageBar } from '@patternfly/chatbot/dist/dynamic/MessageBar'; +import { + Divider, + DropdownItem, + DropdownList, + Label, + MenuToggle, + Select, + SelectList, + SelectOption +} from '@patternfly/react-core'; +import { PlusIcon, ClipboardIcon, CodeIcon, UploadIcon } from '@patternfly/react-icons'; +import { useDropzone } from 'react-dropzone'; + +export const ChatbotMessageBarCustomActionsExample: FunctionComponent = () => { + const [isFirstMenuOpen, setIsFirstMenuOpen] = useState(false); + const [isSecondMenuOpen, setIsSecondMenuOpen] = useState(false); + const [isModelSelectOpen, setIsModelSelectOpen] = useState(false); + const [selectedModel, setSelectedModel] = useState('GPT-4'); + const [showCanvasLabel, setShowCanvasLabel] = useState(true); + + const handleSend = (message: string | number) => alert(message); + + const { open, getInputProps } = useDropzone({ + multiple: true, + onDropAccepted: () => console.log('fileUploaded') + }); + + const onFirstMenuToggle = () => { + setIsFirstMenuOpen(!isFirstMenuOpen); + }; + + const onSecondMenuToggle = () => { + setIsSecondMenuOpen(!isSecondMenuOpen); + }; + + const onModelSelect = ( + _event: React.MouseEvent | undefined, + value: string | number | undefined + ) => { + setSelectedModel(value as string); + setIsModelSelectOpen(false); + }; + + const firstMenuItems: ReactNode = ( + + }> + Logs + + }> + YAML - Status + + }> + YAML - All contents + + + } onClick={open}> + Upload from computer + + + ); + + const secondMenuItems: ReactNode = ( + + + {showCanvasLabel ? 'Disable' : 'Enable'} Canvas + + + }> + Logs + + }> + YAML - Status + + }> + YAML - All contents + + + } onClick={open}> + Upload from computer + + + ); + + const modelOptions = ['GPT-4', 'GPT-3.5', 'Claude', 'Llama 2']; + + return ( + <> + {/* This is required for react-dropzone to work in Safari and Firefox */} + +
+

Custom attach menu with Plus icon

+ { + console.log('selected', value); + setIsFirstMenuOpen(false); + }, + attachMenuInputPlaceholder: 'Search options...', + onAttachMenuToggleClick: onFirstMenuToggle, + onAttachMenuOnOpenChangeKeys: ['Escape', 'Tab'] + }} + buttonProps={{ + attach: { + icon: , + tooltipContent: 'Message actions', + 'aria-label': 'Message actions' + } + }} + /> +
+ +
+

Custom attach menu with additional actions

+ { + console.log('selected', value); + if (value === 'canvas') { + setShowCanvasLabel(!showCanvasLabel); + } + setIsSecondMenuOpen(false); + }, + onAttachMenuToggleClick: onSecondMenuToggle + }} + buttonProps={{ + attach: { + icon: , + tooltipContent: 'Message actions', + 'aria-label': 'Message actions' + } + }} + additionalActions={ + <> + + {showCanvasLabel && ( + + )} + + } + /> +
+ + ); +}; diff --git a/packages/module/patternfly-docs/content/extensions/chatbot/examples/UI/UI.md b/packages/module/patternfly-docs/content/extensions/chatbot/examples/UI/UI.md index fb035c4c3..2d138d6e3 100644 --- a/packages/module/patternfly-docs/content/extensions/chatbot/examples/UI/UI.md +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/UI/UI.md @@ -70,11 +70,11 @@ import { MessageBar } from '@patternfly/chatbot/dist/dynamic/MessageBar'; import SourceDetailsMenuItem from '@patternfly/chatbot/dist/dynamic/SourceDetailsMenuItem'; import { ChatbotModal } from '@patternfly/chatbot/dist/dynamic/ChatbotModal'; import SettingsForm from '@patternfly/chatbot/dist/dynamic/Settings'; -import { BellIcon, CalendarAltIcon, ClipboardIcon, CodeIcon, ThumbtackIcon, UploadIcon } from '@patternfly/react-icons'; +import { BellIcon, CalendarAltIcon, ClipboardIcon, CodeIcon, PlusIcon, ThumbtackIcon, UploadIcon } from '@patternfly/react-icons'; import { useDropzone } from 'react-dropzone'; import ChatbotConversationHistoryNav from '@patternfly/chatbot/dist/dynamic/ChatbotConversationHistoryNav'; -import { Button, DropdownItem, DropdownList, Checkbox, MenuToggle, Select, SelectList, SelectOption } from '@patternfly/react-core'; +import { Button, Label, DropdownItem, DropdownList, Checkbox, MenuToggle, Select, SelectList, SelectOption } from '@patternfly/react-core'; import OutlinedWindowRestoreIcon from '@patternfly/react-icons/dist/esm/icons/outlined-window-restore-icon'; import ExpandIcon from '@patternfly/react-icons/dist/esm/icons/expand-icon'; @@ -291,6 +291,19 @@ Attachments can also be added to the ChatBot via [drag and drop.](/extensions/ch ``` +### Message bar with custom attach menu and additional actions + +You can position the attach button at the start of the message bar and customize it with a different icon (like a Plus icon). Additionally, you can use the `additionalActions` prop to add custom controls such as a model selector or dismissable labels. + +This example shows two variations: + +1. A message bar with a custom attach menu using a Plus icon positioned at the start +2. A message bar with the same attach menu plus additional actions including a model selector and a dismissable "Canvas" label + +```js file="./ChatbotMessageBarCustomActions.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/src/MessageBar/MessageBar.scss b/packages/module/src/MessageBar/MessageBar.scss index c917cb3a6..e39daa9e1 100644 --- a/packages/module/src/MessageBar/MessageBar.scss +++ b/packages/module/src/MessageBar/MessageBar.scss @@ -66,6 +66,19 @@ padding-block-start: var(--pf-t--global--spacer--xs); padding-block-end: var(--pf-t--global--spacer--xs); gap: var(--pf-t--global--spacer--gap--action-to-action--plain); + + &.pf-m-grouped { + flex-basis: 100%; + justify-content: space-between; + } + } + + &-actions-group { + display: flex; + padding-block-start: var(--pf-t--global--spacer--xs); + padding-block-end: var(--pf-t--global--spacer--xs); + gap: var(--pf-t--global--spacer--gap--action-to-action--plain); + align-items: center; } &-input { @@ -150,7 +163,8 @@ } .pf-m-compact { - .pf-chatbot__message-bar-actions { + .pf-chatbot__message-bar-actions, + .pf-chatbot__message-bar-actions-group { padding-block-start: var(--pf-t--global--spacer--sm); padding-block-end: var(--pf-t--global--spacer--sm); } diff --git a/packages/module/src/MessageBar/MessageBar.tsx b/packages/module/src/MessageBar/MessageBar.tsx index 578f21e82..493740744 100644 --- a/packages/module/src/MessageBar/MessageBar.tsx +++ b/packages/module/src/MessageBar/MessageBar.tsx @@ -58,6 +58,8 @@ export interface MessageBarProps extends Omit { placeholder?: string; /** Flag to disable/enable the Attach button */ hasAttachButton?: boolean; + /** Whether the attach button is rendered before or after the message input. */ + attachButtonPosition?: 'start' | 'end'; /** Flag to enable the Microphone button */ hasMicrophoneButton?: boolean; /** Placeholder text when listening */ @@ -116,6 +118,10 @@ export interface MessageBarProps extends Omit { innerRef?: React.Ref; /** Sets background color to primary */ isPrimary?: boolean; + /** Additional actions to render for the message bar. This will force a multiline layout, and the actions will render at the start of the container. */ + additionalActions?: React.ReactNode; + /** Flag indicating whether a multiline layout for the message input and actions should be forced. This can be used to always render actions below the message input. */ + forceMultilineLayout?: boolean; /** @beta Flag indicating whether the message bar has an AI indicator border. */ hasAiIndicator?: boolean; /** @beta Flag indicating whether the chatbot is thinking in response to a query, adding an animation to the message bar. */ @@ -128,6 +134,7 @@ export const MessageBarBase: FunctionComponent = ({ alwayShowSendButton, placeholder = 'Send a message...', hasAttachButton = true, + attachButtonPosition = 'end', hasMicrophoneButton, listeningText = 'Listening', handleAttach, @@ -151,6 +158,8 @@ export const MessageBarBase: FunctionComponent = ({ dropzoneProps, innerRef, isPrimary, + additionalActions, + forceMultilineLayout = false, hasAiIndicator, isThinking, ...props @@ -161,7 +170,9 @@ export const MessageBarBase: FunctionComponent = ({ const [isListeningMessage, setIsListeningMessage] = useState(false); const [hasSentMessage, setHasSentMessage] = useState(false); const [isComposing, setIsComposing] = useState(false); - const [isMultiline, setIsMultiline] = useState(false); + + const shouldForceMultiline = forceMultilineLayout || additionalActions; + const [isMultiline, setIsMultiline] = useState(shouldForceMultiline); const inputRef = useRef(null); const textareaRef = (innerRef as React.RefObject) ?? inputRef; const attachButtonRef = useRef(null); @@ -178,7 +189,7 @@ export const MessageBarBase: FunctionComponent = ({ const grandparent = parent.parentElement; if (grandparent) { - grandparent.style.setProperty('flex-basis', 'auto'); + grandparent.style.setProperty('flex-basis', shouldForceMultiline ? '100%' : 'auto'); } } }; @@ -227,7 +238,7 @@ export const MessageBarBase: FunctionComponent = ({ const parent = field.parentElement; if (parent) { const grandparent = parent.parentElement; - if (textIsLongerThan2Lines(field) && grandparent) { + if ((textIsLongerThan2Lines(field) || shouldForceMultiline) && grandparent) { grandparent.style.setProperty('flex-basis', `100%`); } } @@ -277,14 +288,14 @@ export const MessageBarBase: FunctionComponent = ({ if (field) { if (field.value === '') { setInitialLineHeight(field); - setIsMultiline(false); + !shouldForceMultiline && setIsMultiline(false); } else { setAutoHeight(field); setAutoWidth(field); - checkIfMultiline(field); + !shouldForceMultiline && checkIfMultiline(field); } } - }, [displayMode, message, setAutoWidth, checkIfMultiline]); + }, [displayMode, message, setAutoWidth, shouldForceMultiline, checkIfMultiline]); useEffect(() => { const field = textareaRef.current; @@ -300,10 +311,10 @@ export const MessageBarBase: FunctionComponent = ({ if (textareaRef.current) { if (event.target.value === '') { setInitialLineHeight(textareaRef.current); - setIsMultiline(false); + !shouldForceMultiline && setIsMultiline(false); } else { setAutoHeight(textareaRef.current); - checkIfMultiline(textareaRef.current); + !shouldForceMultiline && checkIfMultiline(textareaRef.current); } } setMessage(event.target.value); @@ -365,6 +376,55 @@ export const MessageBarBase: FunctionComponent = ({ onChange && onChange({} as ChangeEvent, message); }; + const renderAttachButton = () => { + if (!attachMenuProps && hasAttachButton) { + return ( + + ); + } + if (attachMenuProps) { + return ( + + ); + } + }; + + const isAttachButtonAtStart = attachButtonPosition === 'start'; const renderButtons = () => { if (hasStopButton && handleStopButton) { return ( @@ -379,47 +439,7 @@ export const MessageBarBase: FunctionComponent = ({ } return ( <> - {attachMenuProps && ( - - )} - {!attachMenuProps && hasAttachButton && ( - - )} + {!isAttachButtonAtStart && renderAttachButton()} {hasMicrophoneButton && ( = ({ ); }; + const hasGroupedActions = additionalActions || (isAttachButtonAtStart && isMultiline); const messageBarContents = ( <> + {isAttachButtonAtStart && !isMultiline && ( +
{renderAttachButton()}
+ )}