diff --git a/packages/module/patternfly-docs/content/extensions/virtual-assistant/examples/ChatbotHeader/ChatbotHeader.md b/packages/module/patternfly-docs/content/extensions/virtual-assistant/examples/ChatbotHeader/ChatbotHeader.md index 382c800b..d7bc5c25 100644 --- a/packages/module/patternfly-docs/content/extensions/virtual-assistant/examples/ChatbotHeader/ChatbotHeader.md +++ b/packages/module/patternfly-docs/content/extensions/virtual-assistant/examples/ChatbotHeader/ChatbotHeader.md @@ -39,6 +39,7 @@ import ExpandIcon from '@patternfly/react-icons/dist/esm/icons/expand-icon'; import OpenDrawerRightIcon from '@patternfly/react-icons/dist/esm/icons/open-drawer-right-icon'; import PFHorizontalLogoColor from './PF-HorizontalLogo-Color.svg'; import PFHorizontalLogoReverse from './PF-HorizontalLogo-Reverse.svg'; +import ChatbotConversationHistoryNav from '@patternfly/virtual-assistant/dist/dynamic/ChatbotConversationHistoryNav'; ### Chatbot header with controls @@ -53,3 +54,15 @@ By default, HeaderTitle renders whatever children are passed in. Optionally, you ```js file="./ChatbotHeaderTitle.tsx" ``` + +### Chatbot header drawer + +```js file="./ChatbotHeaderDrawer.tsx" + +``` + +### Chatbot header drawer with actions + +```js file="./ChatbotHeaderDrawerWithActions.tsx" + +``` diff --git a/packages/module/patternfly-docs/content/extensions/virtual-assistant/examples/ChatbotHeader/ChatbotHeaderDrawer.tsx b/packages/module/patternfly-docs/content/extensions/virtual-assistant/examples/ChatbotHeader/ChatbotHeaderDrawer.tsx new file mode 100644 index 00000000..bd7cd6ef --- /dev/null +++ b/packages/module/patternfly-docs/content/extensions/virtual-assistant/examples/ChatbotHeader/ChatbotHeaderDrawer.tsx @@ -0,0 +1,110 @@ +import React from 'react'; +import { ChatbotDisplayMode } from '@patternfly/virtual-assistant/dist/dynamic/Chatbot'; +import ChatbotConversationHistoryNav from '@patternfly/virtual-assistant/dist/dynamic/ChatbotConversationHistoryNav'; +import { + ChatbotHeader, + ChatbotHeaderMain, + ChatbotHeaderMenu +} from '@patternfly/virtual-assistant/dist/dynamic/ChatbotHeader'; +interface navItem { + [key: string]: { id: string; text: string }[]; +} + +interface Conversation { + /** Conversation id */ + id: string; + /** Connversation icon */ + icon?: React.ReactNode; + /** Conversation */ + text: string; + /** Dropdown items rendered in conversation options dropdown */ + menuItems?: React.ReactNode; + /** Optional classname applied to conversation options dropdown */ + menuClassName?: string; + /** Tooltip content and aria-label applied to conversation options dropdown */ + label?: string; + /** Callback for when user selects item. */ + onSelectItem?: (event?: React.MouseEvent, value?: string | number) => void; +} + +interface ConversationObject { + [key: string]: Conversation[]; +} + +const initialConversations: ConversationObject = { + Today: [{ id: '1', text: 'Red Hat products and services' }], + 'This month': [ + { + id: '2', + text: 'Enterprise Linux installation and setup' + }, + { id: '3', text: 'Troubleshoot system crash' } + ], + March: [ + { id: '4', text: 'Ansible security and updates' }, + { id: '5', text: 'Red Hat certification' }, + { id: '6', text: 'Lightspeed user documentation' } + ], + February: [ + { id: '7', text: 'Crashing pod assistance' }, + { id: '8', text: 'OpenShift AI pipelines' }, + { id: '9', text: 'Updating subscription plan' }, + { id: '10', text: 'Red Hat licensing options' } + ], + January: [ + { id: '11', text: 'RHEL system performance' }, + { id: '12', text: 'Manage user accounts' } + ] +}; + +export const ChatbotHeaderTitleDemo: React.FunctionComponent = () => { + const [isDrawerOpen, setIsDrawerOpen] = React.useState(false); + const [conversations, setConversations] = React.useState(initialConversations); + const displayMode = ChatbotDisplayMode.fullscreen; + + const findMatchingItems = (targetValue: string) => { + let filteredConversations = Object.entries(initialConversations).reduce((acc, [key, items]) => { + const filteredItems = items.filter((item) => item.text.toLowerCase().includes(targetValue.toLowerCase())); + if (filteredItems.length > 0) { + acc[key] = filteredItems; + } + return acc; + }, {}); + + if (Object.keys(filteredConversations).length === 0) { + filteredConversations = [{ id: '13', icon: '', text: 'No results found' }]; + } + return filteredConversations; + }; + + return ( + setIsDrawerOpen(!isDrawerOpen)} + isDrawerOpen={isDrawerOpen} + activeItemId="1" + // eslint-disable-next-line no-console + onSelectActiveItem={(e, selectedItem) => console.log(`Selected history item with id ${selectedItem}`)} + conversations={conversations} + onNewChat={() => { + setIsDrawerOpen(!isDrawerOpen); + }} + handleTextInputChange={(value: string) => { + if (value === '') { + setConversations(initialConversations); + } + // this is where you would perform search on the items in the drawer + // and update the state + const newConversations: navItem = findMatchingItems(value); + setConversations(newConversations); + }} + drawerContent={ + + + setIsDrawerOpen(!isDrawerOpen)} /> + + + } + > + ); +}; diff --git a/packages/module/patternfly-docs/content/extensions/virtual-assistant/examples/ChatbotHeader/ChatbotHeaderDrawerWithActions.tsx b/packages/module/patternfly-docs/content/extensions/virtual-assistant/examples/ChatbotHeader/ChatbotHeaderDrawerWithActions.tsx new file mode 100644 index 00000000..10379911 --- /dev/null +++ b/packages/module/patternfly-docs/content/extensions/virtual-assistant/examples/ChatbotHeader/ChatbotHeaderDrawerWithActions.tsx @@ -0,0 +1,124 @@ +import React from 'react'; +import { ChatbotDisplayMode } from '@patternfly/virtual-assistant/dist/dynamic/Chatbot'; +import ChatbotConversationHistoryNav from '../../../../../../dist/esm/ChatbotConversationHistoryNav'; +import ChatbotHeader, { ChatbotHeaderMain, ChatbotHeaderMenu } from '../../../../../../dist/esm/ChatbotHeader'; +import { DropdownItem, DropdownList } from '@patternfly/react-core'; + +interface Conversation { + /** Conversation id */ + id: string; + /** Connversation icon */ + icon?: React.ReactNode; + /** Conversation */ + text: string; + /** Dropdown items rendered in conversation options dropdown */ + menuItems?: React.ReactNode; + /** Optional classname applied to conversation options dropdown */ + menuClassName?: string; + /** Tooltip content and aria-label applied to conversation options dropdown */ + label?: string; + /** Callback for when user selects item. */ + onSelectItem?: (event?: React.MouseEvent, value?: string | number) => void; +} + +interface ConversationObject { + [key: string]: Conversation[]; +} + +const menuItems = [ + + + Share + + + Rename + + + Archive + + + Delete + + +]; + +const initialConversations: ConversationObject = { + Today: [{ id: '1', text: 'Red Hat products and services', menuItems }], + 'This month': [ + { + id: '2', + text: 'Enterprise Linux installation and setup', + menuItems + }, + { id: '3', text: 'Troubleshoot system crash', menuItems } + ], + March: [ + { id: '4', text: 'Ansible security and updates', menuItems }, + { id: '5', text: 'Red Hat certification', menuItems }, + { id: '6', text: 'Lightspeed user documentation', menuItems } + ], + February: [ + { id: '7', text: 'Crashing pod assistance', menuItems }, + { id: '8', text: 'OpenShift AI pipelines', menuItems }, + { id: '9', text: 'Updating subscription plan', menuItems }, + { id: '10', text: 'Red Hat licensing options', menuItems } + ], + January: [ + { id: '11', text: 'RHEL system performance', menuItems }, + { id: '12', text: 'Manage user accounts', menuItems } + ] +}; + +export const ChatbotHeaderTitleDemo: React.FunctionComponent = () => { + const [isDrawerOpen, setIsDrawerOpen] = React.useState(false); + const [conversations, setConversations] = React.useState(initialConversations); + + const displayMode = ChatbotDisplayMode.fullscreen; + + const findMatchingItems = (targetValue: string) => { + let filteredConversations = Object.entries(initialConversations).reduce((acc, [key, items]) => { + const filteredItems = items.filter((item) => item.text.toLowerCase().includes(targetValue.toLowerCase())); + if (filteredItems.length > 0) { + acc[key] = filteredItems; + } + return acc; + }, {}); + + if (Object.keys(filteredConversations).length === 0) { + filteredConversations = [{ id: '13', icon: '', text: 'No results found' }]; + } + + return filteredConversations; + }; + + return ( + setIsDrawerOpen(!isDrawerOpen)} + isDrawerOpen={isDrawerOpen} + activeItemId="1" + // eslint-disable-next-line no-console + onSelectActiveItem={(e, selectedItem) => console.log(`Selected history item with id ${selectedItem}`)} + conversations={conversations} + onNewChat={() => { + setIsDrawerOpen(!isDrawerOpen); + }} + handleTextInputChange={(value: string) => { + if (value === '') { + setConversations(initialConversations); + } + // this is where you would perform search on the items in the drawer + // and update the state + const newConversations = findMatchingItems(value); + setConversations(newConversations); + }} + drawerContent={ + + + setIsDrawerOpen(!isDrawerOpen)} /> + + + } + > + ); +}; diff --git a/packages/module/patternfly-docs/content/extensions/virtual-assistant/examples/demos/Chatbot.md b/packages/module/patternfly-docs/content/extensions/virtual-assistant/examples/demos/Chatbot.md index 8a8698ad..8044dc34 100644 --- a/packages/module/patternfly-docs/content/extensions/virtual-assistant/examples/demos/Chatbot.md +++ b/packages/module/patternfly-docs/content/extensions/virtual-assistant/examples/demos/Chatbot.md @@ -33,6 +33,7 @@ import ChatbotFooter, { ChatbotFootnote } from '@patternfly/virtual-assistant/di import MessageBar from '@patternfly/virtual-assistant/dist/dynamic/MessageBar'; import MessageBox from '@patternfly/virtual-assistant/dist/dynamic/MessageBox'; import Message from '@patternfly/virtual-assistant/dist/dynamic/Message'; +import ChatbotConversationHistoryNav from '@patternfly/virtual-assistant/dist/dynamic/ChatbotConversationHistoryNav'; import ChatbotHeader, { ChatbotHeaderMain, diff --git a/packages/module/patternfly-docs/content/extensions/virtual-assistant/examples/demos/Chatbot.tsx b/packages/module/patternfly-docs/content/extensions/virtual-assistant/examples/demos/Chatbot.tsx index a2be7a97..2324bd93 100644 --- a/packages/module/patternfly-docs/content/extensions/virtual-assistant/examples/demos/Chatbot.tsx +++ b/packages/module/patternfly-docs/content/extensions/virtual-assistant/examples/demos/Chatbot.tsx @@ -10,6 +10,7 @@ import ChatbotFooter, { ChatbotFootnote } from '@patternfly/virtual-assistant/di import MessageBar from '@patternfly/virtual-assistant/dist/dynamic/MessageBar'; import MessageBox from '@patternfly/virtual-assistant/dist/dynamic/MessageBox'; import Message, { MessageProps } from '@patternfly/virtual-assistant/dist/dynamic/Message'; +import ChatbotConversationHistoryNav from '@patternfly/virtual-assistant/dist/dynamic/ChatbotConversationHistoryNav'; import ChatbotHeader, { ChatbotHeaderMenu, ChatbotHeaderMain, @@ -122,12 +123,40 @@ const welcomePrompts = [ } ]; +const initialConversations = { + Today: [{ id: '1', text: 'Hello, can you give me an example of what you can do?' }], + 'This month': [ + { + id: '2', + text: 'Enterprise Linux installation and setup' + }, + { id: '3', text: 'Troubleshoot system crash' } + ], + March: [ + { id: '4', text: 'Ansible security and updates' }, + { id: '5', text: 'Red Hat certification' }, + { id: '6', text: 'Lightspeed user documentation' } + ], + February: [ + { id: '7', text: 'Crashing pod assistance' }, + { id: '8', text: 'OpenShift AI pipelines' }, + { id: '9', text: 'Updating subscription plan' }, + { id: '10', text: 'Red Hat licensing options' } + ], + January: [ + { id: '11', text: 'RHEL system performance' }, + { id: '12', text: 'Manage user accounts' } + ] +}; + export const ChatbotDemo: React.FunctionComponent = () => { const [chatbotVisible, setChatbotVisible] = React.useState(false); const [displayMode, setDisplayMode] = React.useState(ChatbotDisplayMode.default); const [messages, setMessages] = React.useState(initialMessages); const [selectedModel, setSelectedModel] = React.useState('Granite 7B'); const [isSendButtonDisabled, setIsSendButtonDisabled] = React.useState(false); + const [isDrawerOpen, setIsDrawerOpen] = React.useState(false); + const [conversations, setConversations] = React.useState(initialConversations); const dummyRef = React.useRef(null); // Autu-scrolls to the latest message @@ -191,78 +220,106 @@ export const ChatbotDemo: React.FunctionComponent = () => { onToggleChatbot={() => setChatbotVisible(!chatbotVisible)} /> - - - alert('Menu toggle clicked')} /> - - - - - - - Granite 7B - - - Llama 3.0 - - - Mistral 3B - - - - - - - } - isSelected={displayMode === ChatbotDisplayMode.default} - > - Overlay - - } - isSelected={displayMode === ChatbotDisplayMode.docked} - > - Dock to window - - } - isSelected={displayMode === ChatbotDisplayMode.fullscreen} - > - Fullscreen - - - - - - - - - - {messages.map((message) => ( - - ))} -
-
-
- - - - + setIsDrawerOpen(!isDrawerOpen)} + isDrawerOpen={isDrawerOpen} + activeItemId="1" + // eslint-disable-next-line no-console + onSelectActiveItem={(e, selectedItem) => console.log(`Selected history item with id ${selectedItem}`)} + conversations={conversations} + onNewChat={() => { + setIsDrawerOpen(!isDrawerOpen); + setMessages([]); + }} + handleTextInputChange={(value: string) => { + if (value === '') { + setConversations(initialConversations); + } + // this is where you would perform search on the items in the drawer + // and update the state + }} + drawerContent={ + <> + + + setIsDrawerOpen(!isDrawerOpen)} /> + + + + + + + Granite 7B + + + Llama 3.0 + + + Mistral 3B + + + + + + + } + isSelected={displayMode === ChatbotDisplayMode.default} + > + Overlay + + } + isSelected={displayMode === ChatbotDisplayMode.docked} + > + Dock to window + + } + isSelected={displayMode === ChatbotDisplayMode.fullscreen} + > + Fullscreen + + + + + + + + + + {messages.map((message) => ( + + ))} +
+
+
+ + + + + + } + >
); diff --git a/packages/module/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryDropdown.tsx b/packages/module/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryDropdown.tsx new file mode 100644 index 00000000..f41a1b08 --- /dev/null +++ b/packages/module/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryDropdown.tsx @@ -0,0 +1,61 @@ +// ============================================================================ +// Chatbot Header - Chatbot Conversation History Nav +// ============================================================================ +import React from 'react'; + +// Import PatternFly components +import { MenuToggleElement, Tooltip, MenuToggle, Dropdown } from '@patternfly/react-core'; + +import EllipsisIcon from '@patternfly/react-icons/dist/esm/icons/ellipsis-v-icon'; + +export interface ChatbotConversationHistoryDropdownProps { + /** Dropdown items rendered in conversation options dropdown */ + menuItems: React.ReactNode; + /** Optional classname applied to conversation options dropdown */ + menuClassName?: string; + /** Tooltip content and aria-label applied to conversation options dropdown */ + label?: string; + /** Callback for when user selects item. */ + onSelectItem?: (event?: React.MouseEvent, value?: string | number) => void; +} + +export const ChatbotConversationHistoryDropdown: React.FunctionComponent = ({ + menuItems, + menuClassName, + onSelectItem, + label +}: ChatbotConversationHistoryDropdownProps) => { + const [isOpen, setIsOpen] = React.useState(false); + + const toggle = (toggleRef: React.Ref) => ( + + setIsOpen(!isOpen)} + > + + + + ); + + return ( + setIsOpen(isOpen)} + popperProps={{ position: 'right' }} + shouldFocusToggleOnSelect + shouldFocusFirstItemOnOpen + toggle={toggle} + > + {menuItems} + + ); +}; + +export default ChatbotConversationHistoryDropdown; diff --git a/packages/module/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.scss b/packages/module/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.scss index cd9fff10..b80ced66 100644 --- a/packages/module/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.scss +++ b/packages/module/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.scss @@ -1,74 +1,67 @@ // ============================================================================ // Chatbot Header - Menu // ============================================================================ -.pf-chatbot__menu { - .pf-v6-c-nav { - --pf-v6-c-nav--PaddingBlockStart: 0; - --pf-v6-c-nav--PaddingBlockEnd: 0; - --pf-v6-c-nav--PaddingInlineStart: 0; - --pf-v6-c-nav--PaddingInlineEnd: 0; +.pf-chatbot__history { + .pf-chatbot__drawer-backdrop { + position: absolute; + } + // Drawer input + // ---------------------------------------------------------------------------- + .pf-chatbot__input { + padding-inline-start: var(--pf-t--global--spacer--xl); + padding-inline-end: var(--pf-t--global--spacer--xl); } - .pf-v6-c-nav__section-title { - --pf-v6-c-nav__section-title--PaddingInlineStart: var(--pf-t--global--spacer--200); - --pf-v6-c-nav__section-title--PaddingInlineEnd: 0; - --pf-v6-c-nav__section-title--Color: var(--pf-t--color--gray--50); - + // Drawer menu + // ---------------------------------------------------------------------------- + .pf-v6-c-menu { + --pf-v6-c-menu--PaddingBlockStart: 0; + --pf-v6-c-menu--BackgroundColor: var(--pf-t--global--background--color--floating--default); + overflow: initial; + position: relative; + } + .pf-v6-c-menu__item-main { + --pf-v6-c-menu__item-main--ColumnGap: var(--pf-t--global--spacer--md); + } + .pf-chatbot__menu-item-header > .pf-v6-c-menu__group-title { + color: var(--pf-t--global--text--color--subtle); + font-weight: 500; + font-size: var(--pf-t--global--icon--size--font--sm); + --pf-v6-c-menu__group-title--PaddingInlineStart: var(--pf-t--global--spacer--sm); + --pf-v6-c-menu__group-title--PaddingInlineEnd: var(--pf-t--global--spacer--sm); + position: -webkit-sticky; position: sticky; top: 0; - background-color: var(--pf-v6-c-drawer__panel--BackgroundColor); - z-index: var(--pf-t--global--z-index--xs); + background-color: var(--pf-t--global--background--color--floating--default); + z-index: var(--pf-t--global--z-index--md); } - - .pf-v6-c-nav__link { - --pf-v6-c-nav__link--PaddingBlockStart: var(--pf-t--global--spacer--200); - --pf-v6-c-nav__link--PaddingBlockEnd: var(--pf-t--global--spacer--200); - --pf-v6-c-nav__link--PaddingInlineStart: var(--pf-t--global--spacer--200); - --pf-v6-c-nav__link--PaddingInlineEnd: var(--pf-t--global--spacer--200); - --pf-v6-c-nav__link--FontWeight: 500; - - font-family: var(--pf-t--chatbot--heading--font-family); - - &:hover, &:focus { - --pf-v6-c-nav__link--hover--BackgroundColor: var(--pf-t--color--gray--10); - } - - &.pf-m-current { - --pf-v6-c-nav__link--m-current--BackgroundColor: var(--pf-t--color--gray--10); - --pf-v6-c-nav__link--m-current--Color: var(--pf-t--color--black); - } + .pf-chatbot__menu-item { + --pf-v6-c-menu__item--PaddingInlineStart: var(--pf-t--global--spacer--sm); + --pf-v6-c-menu__item--PaddingInlineEnd: var(--pf-t--global--spacer--sm); + padding-block-start: var(--pf-t--global--spacer--xs); + padding-block-end: var(--pf-t--global--spacer--xs); + color: var(--pf-t--global--text--color--regular); + font-size: var(--pf-t--global--font--size--body--lg); + font-weight: var(--pf-t--global--font--weight--body--default); } - - .pf-v6-c-nav__link-text { - display: flex; - align-items: center; - gap: var(--pf-t--global--spacer--200); + .pf-chatbot__history-actions { + transform: rotate(90deg); } } - -// Chatbot Header - Menu (Drawer) +// Chatbot Header - Drawer // ---------------------------------------------------------------------------- -.pf-chatbot__menu.pf-v6-c-drawer { +.pf-chatbot__history.pf-v6-c-drawer { --pf-v6-c-drawer__panel--MinWidth: 384px; --pf-v6-c-drawer__panel--xl--MinWidth: 384px; - position: absolute; - top: 0; - left: 0; - pointer-events: none; - z-index: calc(var(--pf-t--global--z-index--600) * 2); - - // Expanded drawer - &.pf-m-expanded { pointer-events: auto; } - // Drawer panel .pf-v6-c-drawer__panel { + --pf-v6-c-drawer__panel--BackgroundColor: var(--pf-t--global--background--color--floating--default); --pf-v6-c-drawer__panel--PaddingBlockStart: var(--pf-t--global--spacer--400); --pf-v6-c-drawer__panel--PaddingBlockEnd: var(--pf-t--global--spacer--400); --pf-v6-c-drawer__panel--RowGap: var(--pf-t--global--spacer--400); - - overflow: hidden; + overflow-x: hidden; } // Drawer head @@ -103,6 +96,8 @@ width: 48px; height: 48px; border-radius: var(--pf-t--global--border--radius--pill); + justify-content: center; + align-items: center; } } @@ -112,108 +107,48 @@ --pf-v6-c-drawer__panel__body--PaddingBlockEnd: 0; --pf-v6-c-drawer__panel__body--PaddingInlineStart: var(--pf-t--global--spacer--400); --pf-v6-c-drawer__panel__body--PaddingInlineEnd: var(--pf-t--global--spacer--400); - + overflow-y: auto; + + display: grid; + grid-template-rows: auto 1fr auto; + grid-auto-rows: auto; + + height: 70vh; } } - // ============================================================================ -// Chatbot Display Mode - Fullscreen +// Chatbot Display Mode - Docked // ============================================================================ -.pf-chatbot--fullscreen { - .pf-chatbot__menu.pf-v6-c-drawer { - --pf-v6-c-drawer__panel--MinWidth: 0; - --pf-v6-c-drawer__panel--xl--MinWidth: 0; - position: static; - width: 0; - - &.pf-m-expanded { - width: 384px; - } - - .pf-v6-c-drawer__panel { - // transition: none; - - &::after { - --pf-v6-c-drawer--m-inline--m-expanded__panel--after--BackgroundColor: var(--pf-t--color--gray--10); - } +.pf-chatbot--docked { + .pf-chatbot__history.pf-v6-c-drawer { + .pf-v6-c-drawer__body { + height: 100%; } } } - // ============================================================================ -// Chatbot Display Mode - Embedded +// Chatbot Display Mode - Fullscreen // ============================================================================ -@media screen and (min-width: 1024px) { - .pf-chatbot--embedded { - .pf-chatbot__menu.pf-v6-c-drawer { - --pf-v6-c-drawer__panel--MinWidth: 0; - --pf-v6-c-drawer__panel--xl--MinWidth: 0; - position: static; - width: 0; - - &.pf-m-expanded { - width: 384px; - } - - .pf-v6-c-drawer__panel { - // transition: none; - - &::after { - --pf-v6-c-drawer--m-inline--m-expanded__panel--after--BackgroundColor: var(--pf-t--color--gray--10); - } - } +.pf-chatbot--fullscreen { + .pf-chatbot__history.pf-v6-c-drawer { + .pf-v6-c-drawer__body { + height: 100%; + width: 100%; } } } - // ============================================================================ -// Chatbot Dark Theme +// Chatbot Display Mode - embedded // ============================================================================ -.pf-v6-theme-dark { - - .pf-chatbot__menu { - .pf-v6-c-nav__section-title { - --pf-v6-c-nav__section-title--Color: var(--pf-t--color--gray--40); - } - - .pf-v6-c-nav__link { - &:hover, &:focus { - --pf-v6-c-nav__link--hover--BackgroundColor: var(--pf-t--color--gray--70); - } - - &.pf-m-current { - --pf-v6-c-nav__link--m-current--BackgroundColor: var(--pf-t--color--gray--70); - --pf-v6-c-nav__link--m-current--Color: var(--pf-t--color--white); - } - } - } - - .pf-chatbot--fullscreen { - .pf-chatbot__menu.pf-v6-c-drawer { - .pf-v6-c-drawer__panel { - --pf-v6-c-drawer__panel--BackgroundColor: var(--pf-t--color--gray--90); - &::after { - --pf-v6-c-drawer--m-inline--m-expanded__panel--after--BackgroundColor: var(--pf-t--color--gray--95); - } - } +.pf-chatbot--embedded { + .pf-chatbot__history.pf-v6-c-drawer { + .pf-v6-c-drawer__body { + width: 100%; + min-height: 100%; } } - - @media screen and (min-width: 1024px) { - .pf-chatbot--embedded { - .pf-chatbot__menu.pf-v6-c-drawer { - .pf-v6-c-drawer__panel { - --pf-v6-c-drawer__panel--BackgroundColor: var(--pf-t--color--gray--90); - &::after { - --pf-v6-c-drawer--m-inline--m-expanded__panel--after--BackgroundColor: var(--pf-t--color--gray--95); - } - } - } - } - } - -} \ No newline at end of file +} diff --git a/packages/module/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.tsx b/packages/module/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.tsx index 0380849e..f1b11add 100644 --- a/packages/module/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.tsx +++ b/packages/module/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.tsx @@ -14,31 +14,58 @@ import { DrawerHead, DrawerActions, DrawerCloseButton, - Nav, - NavItem, - NavGroup, - Truncate + DrawerContentBody, + SearchInput, + Menu, + MenuList, + MenuGroup, + MenuItem, + MenuContent } from '@patternfly/react-core'; -import ChatIcon from '@patternfly/react-icons/dist/esm/icons/chat-icon'; +import { OutlinedCommentAltIcon } from '@patternfly/react-icons'; +import { ChatbotDisplayMode } from '../Chatbot/Chatbot'; +import ConversationHistoryDropdown from './ChatbotConversationHistoryDropdown'; -export interface NavItemObject { +export interface Conversation { + /** Conversation id */ id: string; + /** Connversation icon */ icon?: React.ReactNode; + /** Conversation */ text: string; + /** Dropdown items rendered in conversation options dropdown */ + menuItems?: React.ReactNode; + /** Optional classname applied to conversation options dropdown */ + menuClassName?: string; + /** Tooltip content and aria-label applied to conversation options dropdown */ + label?: string; + /** Callback for when user selects item. */ + onSelectItem?: (event?: React.MouseEvent, value?: string | number) => void; } - export interface ChatbotConversationHistoryNavProps extends DrawerProps { - onDrawerToggle: (event: KeyboardEvent | React.MouseEvent | React.TransitionEvent) => void; + /** Function called to toggle drawer */ + onDrawerToggle: (event: React.KeyboardEvent | React.MouseEvent | React.TransitionEvent) => void; + /** Flag to indicate whether drawer is open */ isDrawerOpen: boolean; + /** ID of active item */ activeItemId: string | number; - onSelectActiveItem: ( - event: React.FormEvent, - selectedItem: { groupId: string | number; itemId: string | number; to: string } - ) => void; - navAriaLabel: string; - navItems: NavItemObject[] | { [key: string]: NavItemObject[] }; + /** Callback function for when an item is selected */ + onSelectActiveItem?: (event?: React.MouseEvent, itemId?: string | number) => void; + /** Items shown in conversation history */ + conversations: Conversation[] | { [key: string]: Conversation[] }; + /** Text shown in blue button */ newChatButtonText?: string; + /** Callback function for when blue button is clicked */ + onNewChat?: () => void; + /** Content wrapped by conversation history nav */ + drawerContent?: React.ReactNode; + /** Placeholder for search input */ + searchInputPlaceholder?: string; + /** A callback for when the input value changes. */ + handleTextInputChange?: (value: string) => void; + /** Display mode of chatbot */ + displayMode: ChatbotDisplayMode; } export const ChatbotConversationHistoryNav: React.FunctionComponent = ({ @@ -46,43 +73,68 @@ export const ChatbotConversationHistoryNav: React.FunctionComponent { - const getNavItem = (conversation: NavItemObject) => ( - (null); + + const onExpand = () => { + drawerRef.current && drawerRef.current.focus(); + }; + + const getNavItem = (conversation: Conversation) => ( + } + icon={conversation.icon ?? } + /* eslint-disable indent */ + {...(conversation.menuItems + ? { + actions: ( + + ) + } + : {})} + /* eslint-enable indent */ > - - + {conversation.text} + ); - const buildNav = () => { - if (Array.isArray(navItems)) { - // Render for array of NavItemObject + const buildMenu = () => { + if (Array.isArray(conversations)) { + // Render for array of MenuItemObject return ( - <> - {navItems.map((conversation) => ( + + {conversations.map((conversation) => ( {getNavItem(conversation)} ))} - + ); } else { // Render for object with NavItemObject arrays as values return ( <> - {Object.keys(navItems).map((navGroup) => ( - - {navItems[navGroup].map((conversation) => ( - {getNavItem(conversation)} - ))} - + {Object.keys(conversations).map((navGroup) => ( + + + {conversations[navGroup].map((conversation) => ( + {getNavItem(conversation)} + ))} + + ))} ); @@ -93,37 +145,67 @@ export const ChatbotConversationHistoryNav: React.FunctionComponent of the list of conversations // - Groups could be optional, but items need to be ordered by date const menuContent = ( - <> - - + + {buildMenu()} + ); const panelContent = ( - <> - - - - - - - - {menuContent} - - + + + + + + + + {handleTextInputChange && ( +
+ handleTextInputChange(value)} + placeholder={searchInputPlaceholder ?? 'Search...'} + /> +
+ )} + {menuContent} +
); + // An onKeyDown property must be passed to the Drawer component to handle closing + // the drawer panel and deactivating the focus trap via the Escape key. + const onEscape = (event: React.KeyboardEvent) => { + if (event.key === 'Escape') { + onDrawerToggle(event); + } + }; + + // Prevent 'Escape' key usage in main chatbot from toggling drawer; only panel content should work + const onChildKeyDown = (event: React.KeyboardEvent) => { + event.stopPropagation(); + }; + return ( - + + + {isDrawerOpen && (displayMode === ChatbotDisplayMode.default || displayMode === ChatbotDisplayMode.docked) ? ( + <> +
+ {drawerContent} + + ) : ( + drawerContent + )} +
+
); }; diff --git a/packages/module/src/ChatbotConversationHistoryNav/inxdex.ts b/packages/module/src/ChatbotConversationHistoryNav/index.ts similarity index 66% rename from packages/module/src/ChatbotConversationHistoryNav/inxdex.ts rename to packages/module/src/ChatbotConversationHistoryNav/index.ts index f3131028..3a649c26 100644 --- a/packages/module/src/ChatbotConversationHistoryNav/inxdex.ts +++ b/packages/module/src/ChatbotConversationHistoryNav/index.ts @@ -1,3 +1,4 @@ export { default } from './ChatbotConversationHistoryNav'; export * from './ChatbotConversationHistoryNav'; +export * from './ChatbotConversationHistoryDropdown'; diff --git a/packages/module/src/ChatbotHeader/ChatbotHeader.scss b/packages/module/src/ChatbotHeader/ChatbotHeader.scss index 88a71c79..6990aacf 100644 --- a/packages/module/src/ChatbotHeader/ChatbotHeader.scss +++ b/packages/module/src/ChatbotHeader/ChatbotHeader.scss @@ -43,7 +43,9 @@ display: flex; gap: var(--pf-t--global--spacer--sm); justify-content: flex-end; - width: 18rem; + .pf-v6-c-menu-toggle.pf-m-secondary { + width: 160px; + } } } diff --git a/packages/module/src/index.ts b/packages/module/src/index.ts index 880fda63..e22144da 100644 --- a/packages/module/src/index.ts +++ b/packages/module/src/index.ts @@ -15,6 +15,9 @@ export * from './Chatbot'; export { default as ChatbotContent } from './ChatbotContent'; export * from './ChatbotContent'; +export { default as ChatbotConversationHistoryNav } from './ChatbotConversationHistoryNav'; +export * from './ChatbotConversationHistoryNav'; + export { default as ChatbotFooter } from './ChatbotFooter'; export * from './ChatbotFooter';