diff --git a/package-lock.json b/package-lock.json index b0d345b4..a6565e28 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4685,6 +4685,12 @@ "@types/ms": "*" } }, + "node_modules/@types/dom-speech-recognition": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/@types/dom-speech-recognition/-/dom-speech-recognition-0.0.4.tgz", + "integrity": "sha512-zf2GwV/G6TdaLwpLDcGTIkHnXf8JEf/viMux+khqKQKDa8/8BAUtXXZS563GnvJ4Fg0PBLGAaFf2GekEVSZ6GQ==", + "dev": true + }, "node_modules/@types/eslint": { "version": "8.40.0", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.40.0.tgz", @@ -27204,6 +27210,7 @@ "@patternfly/patternfly-a11y": "^4.3.1", "@patternfly/react-code-editor": "6.0.0-alpha.94", "@patternfly/react-table": "6.0.0-alpha.95", + "@types/dom-speech-recognition": "^0.0.4", "@types/react": "^18.2.61", "@types/react-dom": "^18.2.19", "@types/react-router-dom": "^5.3.3", diff --git a/packages/module/package.json b/packages/module/package.json index 03ead9b2..b061ad2b 100644 --- a/packages/module/package.json +++ b/packages/module/package.json @@ -33,20 +33,21 @@ "dependencies": { "@patternfly/react-core": "6.0.0-alpha.94", "@patternfly/react-icons": "6.0.0-alpha.34", + "clsx": "^2.1.0", "react-jss": "^10.10.0", - "react-markdown": "^9.0.1", - "clsx": "^2.1.0" + "react-markdown": "^9.0.1" }, "peerDependencies": { "react": "^17 || ^18", "react-dom": "^17 || ^18" }, "devDependencies": { - "@patternfly/patternfly-a11y": "^4.3.1", "@patternfly/documentation-framework": "6.0.0-alpha.69", - "@patternfly/react-table": "6.0.0-alpha.95", - "@patternfly/react-code-editor": "6.0.0-alpha.94", "@patternfly/patternfly": "6.0.0-alpha.205", + "@patternfly/patternfly-a11y": "^4.3.1", + "@patternfly/react-code-editor": "6.0.0-alpha.94", + "@patternfly/react-table": "6.0.0-alpha.95", + "@types/dom-speech-recognition": "^0.0.4", "@types/react": "^18.2.61", "@types/react-dom": "^18.2.19", "@types/react-router-dom": "^5.3.3", diff --git a/packages/module/patternfly-docs/content/extensions/virtual-assistant/examples/Footer/Footer.md b/packages/module/patternfly-docs/content/extensions/virtual-assistant/examples/Footer/Footer.md index 6ffe9d55..3c8be168 100644 --- a/packages/module/patternfly-docs/content/extensions/virtual-assistant/examples/Footer/Footer.md +++ b/packages/module/patternfly-docs/content/extensions/virtual-assistant/examples/Footer/Footer.md @@ -11,10 +11,19 @@ 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: - ['Footer', 'Footnote', 'FootnotePopover', 'FootnotePopoverCTA', 'FootnotePopoverBannerImage', 'FootnotePopoverLink'] + [ + 'Footer', + 'MessageBar', + 'Footnote', + 'FootnotePopover', + 'FootnotePopoverCTA', + 'FootnotePopoverBannerImage', + 'FootnotePopoverLink' + ] --- import { Footer, Footnote } from '@patternfly/virtual-assistant/dist/dynamic/Footer'; +import { MessageBar } from '@patternfly/virtual-assistant/dist/dynamic/MessageBar'; ### Basic example diff --git a/packages/module/patternfly-docs/content/extensions/virtual-assistant/examples/Footer/Footer.tsx b/packages/module/patternfly-docs/content/extensions/virtual-assistant/examples/Footer/Footer.tsx index 78f717d8..ca2f008a 100644 --- a/packages/module/patternfly-docs/content/extensions/virtual-assistant/examples/Footer/Footer.tsx +++ b/packages/module/patternfly-docs/content/extensions/virtual-assistant/examples/Footer/Footer.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { Footer, Footnote } from '@patternfly/virtual-assistant/dist/dynamic/Footer'; +import { MessageBar } from '@patternfly/virtual-assistant/dist/dynamic/MessageBar'; const footnoteProps = { label: 'Lightspeed uses AI. Check for mistakes.', @@ -23,8 +24,13 @@ const footnoteProps = { } }; -export const BasicDemo: React.FunctionComponent = () => ( - -); +export const BasicDemo: React.FunctionComponent = () => { + const handleSend = (message) => alert(message); + + return ( + + ); +}; diff --git a/packages/module/src/Footer/Footnote.tsx b/packages/module/src/Footer/Footnote.tsx index 83e66a01..cdea45d2 100644 --- a/packages/module/src/Footer/Footnote.tsx +++ b/packages/module/src/Footer/Footnote.tsx @@ -28,8 +28,6 @@ export interface FootnotePopover { title: string; /** Description for the Footnote popover */ description: string; - /** Aria Label for the Popover */ - ariaLabel?: string; /** Optional Banner Image that can be shown in the Footnote Popover */ bannerImage?: FootnotePopoverBannerImage; /** Optional CTA button that can be used to trigger an action and close the popover */ @@ -112,7 +110,7 @@ export const Footnote: React.FunctionComponent = ({ {popover && ( setIsVisible(true)} shouldClose={(_event, _fn) => setIsVisible(false)} diff --git a/packages/module/src/MessageBar/AttachButton.scss b/packages/module/src/MessageBar/AttachButton.scss new file mode 100644 index 00000000..fa4fa875 --- /dev/null +++ b/packages/module/src/MessageBar/AttachButton.scss @@ -0,0 +1,35 @@ +// ============================================================================ +// Chatbot Footer - Message Bar - Attach +// ============================================================================ +.pf-v6-c-button.pf-chatbot__button--attach { + width: 48px; + height: 48px; + border-radius: var(--pf-t--global--border--radius--pill); + + .pf-v6-c-button__text { + display: flex; + align-items: center; + justify-content: center; + color: var(--pf-t--chatbot--message-bar--icon--fill); + } + + svg { height: 20px; } + + // Interactive states + &:hover, &:focus { + .pf-v6-c-button__text { color: var(--pf-t--chatbot--message-bar--icon--fill--hover); } + } + + // Active state + &--active { + background-color: var(--pf-t--color--blue--50); + + .pf-v6-c-button__text { + color: var(--pf-t--color--white); + } + + &:hover, &:focus { + .pf-v6-c-button__text { color: var(--pf-t--color--white); } + } + } +} \ No newline at end of file diff --git a/packages/module/src/MessageBar/AttachButton.tsx b/packages/module/src/MessageBar/AttachButton.tsx new file mode 100644 index 00000000..a44b2ebf --- /dev/null +++ b/packages/module/src/MessageBar/AttachButton.tsx @@ -0,0 +1,61 @@ +// ============================================================================ +// Chatbot Footer - Message Bar - Attach +// ============================================================================ +import React from 'react'; + +// Import PatternFly components +import { Button, ButtonProps, Tooltip, TooltipProps } from '@patternfly/react-core'; + +import { PaperclipIcon } from '@patternfly/react-icons/dist/esm/icons/paperclip-icon'; + +export interface AttachButtonProps extends ButtonProps { + /** OnClick Handler for the Attach Button */ + onClick: () => void; + /** Class Name for the Attach button */ + className?: string; + /** Props to control is the attach button should be disabled */ + isDisabled?: boolean; + /** Props to control the PF Tooltip component */ + tooltipProps?: TooltipProps; +} + +export const AttachButton: React.FunctionComponent = ({ + onClick, + isDisabled, + className, + tooltipProps, + ...props +}: AttachButtonProps) => { + // Configure tooltip + const tooltipAttachRef = React.useRef(); + + return ( + <> + + + + + > + ); +}; + +export default AttachButton; diff --git a/packages/module/src/MessageBar/MessageBar.scss b/packages/module/src/MessageBar/MessageBar.scss new file mode 100644 index 00000000..0ed98824 --- /dev/null +++ b/packages/module/src/MessageBar/MessageBar.scss @@ -0,0 +1,113 @@ +// ============================================================================ +// Chatbot Footer - Message Bar +// ============================================================================ +.pf-chatbot__message-bar { + --pf-t--chatbot--message-bar--BackgroundColor: var(--pf-t--color--white); + --pf-t--chatbot--message-bar--BoxShadow--color--hover: var(--pf-t--color--gray--30); + --pf-t--chatbot--message-bar--BoxShadow--color--focus: var(--pf-t--color--blue--50); + + --pf-t--chatbot--message-bar--icon--fill: var(--pf-t--color--gray--50); + --pf-t--chatbot--message-bar--icon--fill--hover: var(--pf-t--color--gray--70); + --pf-t--chatbot--message-bar--icon--fill--active: var(--pf-t--color--white); + + --pf-t--chatbot--message-bar--send-icon--fill: var(--pf-t--color--blue--50); + --pf-t--chatbot--message-bar--send-icon--fill--hover: var(--pf-t--color--blue--50); + --pf-t--chatbot--message-bar--send-icon--background--color--hover: rgba( + 224, + 240, + 255, + 0.5 + ); // --pf-t--color--blue--10 @ 50% + + position: relative; + display: flex; + align-items: center; + justify-content: flex-end; + flex-wrap: wrap; + min-height: 64px; + background-color: var(--pf-t--chatbot--message-bar--BackgroundColor); + border-radius: calc( + var(--pf-t--global--border--radius--medium) * 2 + ); // 32px corners - universal for single and multi-line height + transition: box-shadow var(--pf-t--chatbot--timing-function) var(--pf-t--global--duration--100); + + &:hover { + box-shadow: inset 0 0 0 1px var(--pf-t--chatbot--message-bar--BoxShadow--color--hover); + } + + &:focus-within { + box-shadow: inset 0 0 0 2px var(--pf-t--chatbot--message-bar--BoxShadow--color--focus); + } + + // Input + // -------------------------------------------------------------------------- + &-input { + align-self: stretch; + display: flex; + // align-items: center; + flex-grow: 1; + padding: calc(var(--pf-t--global--spacer--100) + var(--pf-t--global--spacer--300)) var(--pf-t--global--spacer--400); + } + .pf-chatbot__message-textarea { + align-self: stretch; + display: flex; + // align-items: center; + flex-grow: 1; + max-width: 100%; + max-height: 232px; // 8 rows + padding + font-size: var(--pf-t--global--font--size--md); + line-height: 24px; + } + + // Actions + // -------------------------------------------------------------------------- + &-actions { + display: flex; + align-self: flex-end; + margin: 0 var(--pf-t--global--spacer--200) var(--pf-t--global--spacer--200) 0; + } + + // Standard Buttons + // -------------------------------------------------------------------------- + // .pf-v6-c-button { + // width: 48px; + // height: 48px; + // border-radius: var(--pf-t--global--border--radius--pill); + + // .pf-v6-c-button__text { + // display: flex; + // align-items: center; + // justify-content: center; + // color: var(--pf-t--chatbot--message-bar--icon--fill); + // } + + // svg { height: 20px; } + + // // Interactive states + // &:hover, &:focus { + // .pf-v6-c-button__text { color: var(--pf-t--chatbot--message-bar--icon--fill--hover); } + // } + // } + + // Send Button + // -------------------------------------------------------------------------- +} + +// ============================================================================ +// Chatbot Dark Theme +// ============================================================================ +.pf-v6-theme-dark { + .pf-chatbot__message-bar { + --pf-t--chatbot--message-bar--BackgroundColor: var(--pf-t--color--gray--80); + --pf-t--chatbot--message-bar--BoxShadow--color--hover: var(--pf-t--color--gray--50); + --pf-t--chatbot--message-bar--BoxShadow--color--focus: var(--pf-t--color--blue--30); + + --pf-t--chatbot--message-bar--icon--fill: var(--pf-t--color--gray--30); + --pf-t--chatbot--message-bar--icon--fill--hover: var(--pf-t--color--white); + --pf-t--chatbot--message-bar--icon--fill--active: var(--pf-t--color--white); + + --pf-t--chatbot--message-bar--send-icon--fill: var(--pf-t--color--blue--30); + --pf-t--chatbot--message-bar--send-icon--fill--hover: var(--pf-t--color--white); + --pf-t--chatbot--message-bar--send-icon--background--color--hover: var(--pf-t--color--blue--50); + } +} diff --git a/packages/module/src/MessageBar/MessageBar.tsx b/packages/module/src/MessageBar/MessageBar.tsx new file mode 100644 index 00000000..f9985d03 --- /dev/null +++ b/packages/module/src/MessageBar/MessageBar.tsx @@ -0,0 +1,100 @@ +// ============================================================================ +// Chatbot Footer - Message Bar +// ============================================================================ +import React from 'react'; +import { TextArea, TextAreaProps } from '@patternfly/react-core'; + +// Import Chatbot components +import AttachButton from './AttachButton'; +import MicrophoneButton from './MicrophoneButton'; +import SendButton from './SendButton'; + +export interface MessageBarProps extends TextAreaProps { + /** Callback to get the value of input message by user */ + onSendMessage: (message: string) => void; + /** Class Name for the MessageBar component */ + className?: string; + /** Flag to always to show the send button. By default send button is shown when there is a message in the input field */ + alwayShowSendButton?: boolean; + /** Flag to enable the Attach button */ + hasAttachButton?: boolean; + /** Flag to enable the Microphone button */ + hasMicrophoneButton?: boolean; +} + +export const MessageBar: React.FunctionComponent = ({ + onSendMessage, + className, + alwayShowSendButton, + hasAttachButton, + hasMicrophoneButton, + ...props +}: MessageBarProps) => { + // Text Input + // -------------------------------------------------------------------------- + const [message, setMessage] = React.useState(''); + const [isListeningMessage, setIsListeningMessage] = React.useState(false); + + const textareaRef = React.useRef(null); + + const handleChange = React.useCallback((event) => { + setMessage(event.target.value); + }, []); + + // Handle sending message + const handleSend = React.useCallback(() => { + setMessage((m) => { + onSendMessage(m); + return ''; + }); + }, [onSendMessage]); + + // Attachments + // -------------------------------------------------------------------------- + const handleAttach = React.useCallback(() => { + // eslint-disable-next-line no-console + console.log('Attach button clicked'); + }, []); + + const handleKeyDown = React.useCallback( + (event) => { + if (event.key === 'Enter' && !event.shiftKey) { + event.preventDefault(); + handleSend(); + } + }, + [handleSend] + ); + + return ( + + + + + + + {hasAttachButton && } + {hasMicrophoneButton && ( + + )} + {(alwayShowSendButton || message) && } + + + ); +}; + +export default MessageBar; diff --git a/packages/module/src/MessageBar/MicrophoneButton.scss b/packages/module/src/MessageBar/MicrophoneButton.scss new file mode 100644 index 00000000..6442401b --- /dev/null +++ b/packages/module/src/MessageBar/MicrophoneButton.scss @@ -0,0 +1,54 @@ +// ============================================================================ +// Chatbot Footer - Message Bar - Microphone +// ============================================================================ +.pf-v6-c-button.pf-chatbot__button--microphone { + width: 48px; + height: 48px; + border-radius: var(--pf-t--global--border--radius--pill); + + .pf-v6-c-button__text { + display: flex; + align-items: center; + justify-content: center; + color: var(--pf-t--chatbot--message-bar--icon--fill); + } + + svg { + height: 20px; + } + + // Interactive states + &:hover, + &:focus { + .pf-v6-c-button__text { + color: var(--pf-t--chatbot--message-bar--icon--fill--hover); + } + } + + // Active state + &--active { + background-color: var(--pf-t--color--blue--50); + animation: motionMicButton var(--pf-t--chatbot--timing-function) calc(var(--pf-t--global--duration--200) * 8) + infinite; + + .pf-v6-c-button__text { + color: var(--pf-t--color--white); + } + + &:hover, + &:focus { + .pf-v6-c-button__text { + color: var(--pf-t--color--white); + } + } + } +} + +@keyframes motionMicButton { + 0% { + box-shadow: 0 0 0 0 rgba(0, 102, 204, 1); + } + 100% { + box-shadow: 0 0 0 16px rgba(0, 102, 204, 0); + } +} diff --git a/packages/module/src/MessageBar/MicrophoneButton.tsx b/packages/module/src/MessageBar/MicrophoneButton.tsx new file mode 100644 index 00000000..07759f94 --- /dev/null +++ b/packages/module/src/MessageBar/MicrophoneButton.tsx @@ -0,0 +1,113 @@ +// ============================================================================ +// Chatbot Footer - Message Bar - Microphone +// ============================================================================ +import React from 'react'; + +// Import PatternFly components +import { Button, ButtonProps, Tooltip, TooltipProps } from '@patternfly/react-core'; + +// Import FontAwesome icons +import { MicrophoneIcon } from '@patternfly/react-icons/dist/esm/icons/microphone-icon'; + +export interface MicrophoneButtonProps extends ButtonProps { + /** Boolean check if the browser is listening to speech or not */ + isListening: boolean; + /** Class Name for the Microphone button */ + className?: string; + /** Callback to update the value of isListening */ + onIsListeningChange: React.Dispatch>; + /** Callback to update the message value once speech recognition is complete */ + onSpeechRecognition: React.Dispatch>; + /** Props to control the PF Tooltip component */ + tooltipProps?: TooltipProps; +} + +export const MicrophoneButton: React.FunctionComponent = ({ + isListening, + onIsListeningChange, + onSpeechRecognition, + className, + tooltipProps, + ...props +}: MicrophoneButtonProps) => { + // Configure tooltip + const tooltipUseMicrophoneRef = React.useRef(); + + // Microphone + // -------------------------------------------------------------------------- + const [speechRecognition, setSpeechRecognition] = React.useState(); + + // Listen for speech + const startListening = React.useCallback(() => { + if (speechRecognition) { + speechRecognition.start(); + onIsListeningChange(true); + } + }, [onIsListeningChange, speechRecognition]); + + // Stop listening for speech + const stopListening = React.useCallback(() => { + if (speechRecognition && isListening) { + speechRecognition.stop(); + onIsListeningChange(false); + } + }, [isListening, onIsListeningChange, speechRecognition]); + + // Detect speech recognition browser support + React.useEffect(() => { + if ('SpeechRecognition' in window || 'webkitSpeechRecognition' in window) { + // Initialize SpeechRecognition + const recognition: SpeechRecognition = new (window.SpeechRecognition || window.webkitSpeechRecognition)(); + recognition.continuous = false; + recognition.interimResults = false; + recognition.lang = 'en-US'; + + recognition.onresult = (event) => { + const result = event.results[0][0].transcript; + onSpeechRecognition(result); + recognition.stop(); + }; + + recognition.onerror = (event) => { + // eslint-disable-next-line no-console + console.error('Speech recognition error:', event.error); + recognition.stop(); + }; + + setSpeechRecognition(recognition); + } + }, [onSpeechRecognition]); + + if (!speechRecognition) { + return null; + } + + return ( + <> + + + + + > + ); +}; + +export default MicrophoneButton; diff --git a/packages/module/src/MessageBar/SendButton.scss b/packages/module/src/MessageBar/SendButton.scss new file mode 100644 index 00000000..4fa52a46 --- /dev/null +++ b/packages/module/src/MessageBar/SendButton.scss @@ -0,0 +1,40 @@ +// ============================================================================ +// Chatbot Footer - Message Bar - Send +// ============================================================================ +.pf-v6-c-button.pf-chatbot__button--send { + width: 48px; + height: 48px; + border-radius: var(--pf-t--global--border--radius--pill); + animation: motionSendButton var(--pf-t--chatbot--timing-function) var(--pf-t--global--duration--200) forwards; + + .pf-v6-c-button__text { + display: flex; + align-items: center; + justify-content: center; + color: var(--pf-t--chatbot--message-bar--send-icon--fill); + } + + svg { + height: 20px; + } + + &:hover, + &:focus { + background-color: var(--pf-t--chatbot--message-bar--send-icon--background--color--hover); + + .pf-v6-c-button__text { + color: var(--pf-t--chatbot--message-bar--send-icon--fill--hover); + } + } +} + +@keyframes motionSendButton { + 0% { + opacity: 0; + transform: translate3d(-8px, 0, 0); + } + 100% { + opacity: 1; + transform: translate3d(0, 0, 0); + } +} diff --git a/packages/module/src/MessageBar/SendButton.tsx b/packages/module/src/MessageBar/SendButton.tsx new file mode 100644 index 00000000..c76e022d --- /dev/null +++ b/packages/module/src/MessageBar/SendButton.tsx @@ -0,0 +1,57 @@ +// ============================================================================ +// Chatbot Footer - Message Bar - Send +// ============================================================================ +import React from 'react'; + +// Import PatternFly components +import { Button, ButtonProps, Tooltip, TooltipProps } from '@patternfly/react-core'; + +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 */ + className?: string; + /** Props to control the PF Tooltip component */ + tooltipProps?: TooltipProps; +} + +export const SendButton: React.FunctionComponent = ({ + className, + onClick, + tooltipProps, + ...props +}: SendButtonProps) => { + // Configure tooltip + const tooltipSendMessageRef = React.useRef(); + + return ( + <> + + + + + > + ); +}; + +export default SendButton; diff --git a/packages/module/src/MessageBar/index.ts b/packages/module/src/MessageBar/index.ts new file mode 100644 index 00000000..89acfd39 --- /dev/null +++ b/packages/module/src/MessageBar/index.ts @@ -0,0 +1,6 @@ +export { default } from './MessageBar'; + +export * from './MessageBar'; +export * from './AttachButton'; +export * from './MicrophoneButton'; +export * from './SendButton'; diff --git a/packages/module/src/index.ts b/packages/module/src/index.ts index 63bd1a9c..26c139b9 100644 --- a/packages/module/src/index.ts +++ b/packages/module/src/index.ts @@ -15,6 +15,9 @@ export * from './Footer'; export { default as LoadingMessage } from './LoadingMessage'; export * from './LoadingMessage'; +export { default as MessageBar } from './MessageBar'; +export * from './MessageBar'; + export { default as Popover } from './Popover'; export * from './Popover';