diff --git a/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/Chatbot.md b/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/Chatbot.md index 81a8ee63..e964df15 100644 --- a/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/Chatbot.md +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/Chatbot.md @@ -103,3 +103,9 @@ This demo displays an embedded ChatBot. Embedded ChatBots are meant to be placed ```js file="./EmbeddedChatbot.tsx" isFullscreen ``` + +### Comparison + +```js file="./EmbeddedComparisonChatbot.tsx" isFullscreen + +``` diff --git a/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/EmbeddedComparisonChatbot.tsx b/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/EmbeddedComparisonChatbot.tsx new file mode 100644 index 00000000..99b3f6d5 --- /dev/null +++ b/packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/EmbeddedComparisonChatbot.tsx @@ -0,0 +1,255 @@ +import React from 'react'; + +import { + Page, + Masthead, + MastheadMain, + MastheadBrand, + MastheadLogo, + PageSidebarBody, + PageSidebar, + MastheadToggle, + PageToggleButton, + SkipToContent, + ToggleGroup, + ToggleGroupItem +} from '@patternfly/react-core'; +import Chatbot, { ChatbotDisplayMode } from '@patternfly/chatbot/dist/dynamic/Chatbot'; +import ChatbotContent from '@patternfly/chatbot/dist/dynamic/ChatbotContent'; +import ChatbotWelcomePrompt from '@patternfly/chatbot/dist/dynamic/ChatbotWelcomePrompt'; +import ChatbotFooter from '@patternfly/chatbot/dist/dynamic/ChatbotFooter'; +import MessageBar from '@patternfly/chatbot/dist/dynamic/MessageBar'; +import MessageBox from '@patternfly/chatbot/dist/dynamic/MessageBox'; +import Message, { MessageProps } from '@patternfly/chatbot/dist/dynamic/Message'; +import ChatbotHeader, { ChatbotHeaderMain } from '@patternfly/chatbot/dist/dynamic/ChatbotHeader'; +import { BarsIcon } from '@patternfly/react-icons'; +import userAvatar from '../Messages/user_avatar.svg'; +import patternflyAvatar from '../Messages/patternfly_avatar.jpg'; + +export const CompareChild = ({ name, input, hasNewInput }) => { + const [messages, setMessages] = React.useState([]); + const [announcement, setAnnouncement] = React.useState(); + const scrollToBottomRef = React.useRef(null); + const displayMode = ChatbotDisplayMode.embedded; + + // you will likely want to come up with your own unique id function; this is for demo purposes only + const generateId = () => { + const id = Date.now() + Math.random(); + return id.toString(); + }; + + const handleSend = (input: string) => { + const date = new Date(); + const newMessages: MessageProps[] = []; + messages.forEach((message) => newMessages.push(message)); + newMessages.push({ + avatar: userAvatar, + id: generateId(), + name: 'You', + role: 'user', + content: input, + timestamp: `${date?.toLocaleDateString()} ${date?.toLocaleTimeString()}` + }); + newMessages.push({ + avatar: patternflyAvatar, + id: generateId(), + name, + role: 'bot', + timestamp: `${date?.toLocaleDateString()} ${date?.toLocaleTimeString()}`, + isLoading: true + }); + setMessages(newMessages); + // make announcement to assistive devices that new messages have been added + setAnnouncement(`Message from You: ${input}. Message from ${name} is loading.`); + + // this is for demo purposes only; in a real situation, there would be an API response we would wait for + setTimeout(() => { + const loadedMessages: MessageProps[] = []; + // we can't use structuredClone since messages contains functions, but we can't mutate + // items that are going into state or the UI won't update correctly + newMessages.forEach((message) => loadedMessages.push(message)); + loadedMessages.pop(); + loadedMessages.push({ + id: generateId(), + role: 'bot', + content: `API response from ${name} goes here`, + name, + avatar: patternflyAvatar, + isLoading: false, + actions: { + // eslint-disable-next-line no-console + positive: { onClick: () => console.log('Good response') }, + // eslint-disable-next-line no-console + negative: { onClick: () => console.log('Bad response') }, + // eslint-disable-next-line no-console + copy: { onClick: () => console.log('Copy') }, + // eslint-disable-next-line no-console + share: { onClick: () => console.log('Share') }, + // eslint-disable-next-line no-console + listen: { onClick: () => console.log('Listen') } + }, + timestamp: date.toLocaleString() + }); + setMessages(loadedMessages); + // make announcement to assistive devices that new message has loaded + setAnnouncement(`Message from ${name}: API response goes here`); + }, 5000); + }; + + React.useEffect(() => { + if (input) { + handleSend(input); + } + }, [hasNewInput]); + + // Auto-scrolls to the latest message + React.useEffect(() => { + // don't scroll the first load, but scroll if there's a current stream or a new source has popped up + if (messages.length > 0) { + scrollToBottomRef.current?.scrollIntoView(); + } + }, [messages]); + + return ( + + + {name} + + + + + {messages.map((message) => ( + + ))} +
+
+
+
+ ); +}; + +export const EmbeddedComparisonChatbotDemo: React.FunctionComponent = () => { + const [input, setInput] = React.useState(); + const [hasNewInput, setHasNewInput] = React.useState(false); + const [isSelected, setIsSelected] = React.useState('toggle-group-assistant-1'); + const [showFirstChatbot, setShowFirstChatbot] = React.useState(true); + const [showSecondChatbot, setShowSecondChatbot] = React.useState(false); + const [isSidebarOpen, setIsSidebarOpen] = React.useState(false); + const historyRef = React.useRef(null); + + React.useEffect(() => { + // we want to show the first if we switch to the mobile toggle view + // and reset/switch back to normal otherwise + const updateChatbotVisibility = () => { + if (window.innerWidth >= 901) { + setShowFirstChatbot(true); + setShowSecondChatbot(true); + } else { + setShowFirstChatbot(true); + setShowSecondChatbot(false); + setIsSelected('toggle-group-assistant-1'); + } + }; + window.addEventListener('resize', updateChatbotVisibility); + + return () => { + window.removeEventListener('resize', updateChatbotVisibility); + }; + }, []); + + // this only happens on mobile + const handleChildToggleClick = (event) => { + const id = event.currentTarget.id; + setIsSelected(id); + setShowSecondChatbot(!showSecondChatbot); + setShowFirstChatbot(!showFirstChatbot); + }; + + const handleSend = (value: string) => { + setInput(value); + setHasNewInput(!hasNewInput); + }; + + const masthead = ( + + + + setIsSidebarOpen(!isSidebarOpen)} + id="fill-nav-toggle" + > + + + + + + Logo + + + + + ); + + const sidebar = ( + + Navigation + + ); + + const skipToChatbot = (event: React.MouseEvent) => { + event.preventDefault(); + if (historyRef.current) { + historyRef.current.focus(); + } + }; + + const skipToContent = ( + /* You can also add a SkipToContent for your main content here */ + + Skip to chatbot + + ); + + return ( + +
+
+ + + + +
+
+
+ +
+
+ +
+
+ + + +
+
+ ); +}; diff --git a/packages/module/src/Compare/Compare.scss b/packages/module/src/Compare/Compare.scss new file mode 100644 index 00000000..5a80aaa1 --- /dev/null +++ b/packages/module/src/Compare/Compare.scss @@ -0,0 +1,77 @@ +.pf-chatbot__compare-container { + display: flex; + flex-direction: column; + position: relative; + height: 100%; + + .pf-chatbot__footer { + position: sticky; + bottom: 0; + } +} +.pf-chatbot__compare-toggle { + width: 100%; + + .pf-v6-c-toggle-group__button { + width: 100%; + display: flex; + justify-content: center; + } +} +.pf-chatbot__compare { + display: flex; + height: 100%; + width: 100%; + + @media screen and (max-width: 900px) { + overflow-y: auto; + } + + .pf-chatbot__compare-item:first-of-type { + border-right: 1px solid var(--pf-t--global--border--color--default); + + @media screen and (max-width: 900px) { + border-right: 0px; + } + } +} + +.pf-chatbot__compare-item { + flex: 1; + + .pf-chatbot--embedded .pf-chatbot__messagebox { + width: 100%; + } + + .pf-chatbot__content { + padding: 0; + } + + .pf-chatbot.pf-chatbot--embedded { + @media screen and (max-width: 900px) { + height: 100%; + } + } +} +.pf-chatbot__compare-item-hidden { + display: block; + + @media screen and (max-width: 900px) { + display: none; + } +} + +.pf-chatbot__compare-mobile-controls { + padding: var(--pf-t--global--spacer--md) var(--pf-t--global--spacer--lg) 0 var(--pf-t--global--spacer--lg); + display: none; + background-color: var(--pf-t--global--background--color--secondary--default); + position: sticky; + top: 0; + z-index: 9999; + + @media screen and (max-width: 900px) { + display: flex; + flex-direction: column; + gap: var(--pf-t--global--spacer--md); + } +} diff --git a/packages/module/src/main.scss b/packages/module/src/main.scss index d55d9b56..b6023cdb 100644 --- a/packages/module/src/main.scss +++ b/packages/module/src/main.scss @@ -10,6 +10,7 @@ @import './ChatbotToggle/ChatbotToggle'; @import './ChatbotWelcomePrompt/ChatbotWelcomePrompt'; @import './CodeModal/CodeModal'; +@import './Compare/Compare'; @import './FileDetails/FileDetails'; @import './FileDetailsLabel/FileDetailsLabel'; @import './FileDropZone/FileDropZone';