Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(Compare): Add compare layout #399

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,11 @@ This demo displays an embedded ChatBot. Embedded ChatBots are meant to be placed
```js file="./EmbeddedChatbot.tsx" isFullscreen

```

### Comparing ChatBots

To let users compare how different ChatBots respond to the same prompt, you can add multiple ChatBots within the same window. The following demo illustrates a comparison view pattern that allows users to toggle between different conversations in a single ChatBot window.
rebeccaalpert marked this conversation as resolved.
Show resolved Hide resolved

```js file="./EmbeddedComparisonChatbot.tsx" isFullscreen

```
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
import React from 'react';

import {
Page,
Masthead,
MastheadMain,
MastheadBrand,
MastheadLogo,
PageSidebarBody,
PageSidebar,
MastheadToggle,
PageToggleButton,
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, setIsSendButtonDisabled }) => {
const [messages, setMessages] = React.useState<MessageProps[]>([]);
const [announcement, setAnnouncement] = React.useState<string>();
const scrollToBottomRef = React.useRef<HTMLDivElement>(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,
avatarProps: { isBordered: true },
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`);
setIsSendButtonDisabled(false);
}, 5000);
};

React.useEffect(() => {
if (input) {
handleSend(input);
}
}, [hasNewInput]);

Check warning on line 104 in packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/EmbeddedComparisonChatbot.tsx

View workflow job for this annotation

GitHub Actions / call-build-lint-test-workflow / lint

React Hook React.useEffect has missing dependencies: 'handleSend' and 'input'. Either include them or remove the dependency array

// 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 (
<Chatbot displayMode={displayMode}>
<ChatbotHeader className="compare-header">
<ChatbotHeaderMain>{name}</ChatbotHeaderMain>
</ChatbotHeader>
<ChatbotContent>
<MessageBox ariaLabel={`Scrollable message log for ${name}`} announcement={announcement}>
<ChatbotWelcomePrompt title="Hello, Chatbot User" description="How may I help you today?" />
{messages.map((message) => (
<Message key={message.id} {...message} />
))}
<div ref={scrollToBottomRef}></div>
</MessageBox>
</ChatbotContent>
</Chatbot>
);
};

export const EmbeddedComparisonChatbotDemo: React.FunctionComponent = () => {
const [input, setInput] = React.useState<string>();
const [hasNewInput, setHasNewInput] = React.useState(false);
const [isSelected, setIsSelected] = React.useState('toggle-group-chatbot-1');
const [showFirstChatbot, setShowFirstChatbot] = React.useState(true);
const [showSecondChatbot, setShowSecondChatbot] = React.useState(false);
const [isSidebarOpen, setIsSidebarOpen] = React.useState(false);
const [isSendButtonDisabled, setIsSendButtonDisabled] = React.useState(false);

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-chatbot-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);
setIsSendButtonDisabled(true);
};

const masthead = (
<Masthead>
<MastheadMain>
<MastheadToggle>
<PageToggleButton
variant="plain"
aria-label="Global navigation"
isSidebarOpen={isSidebarOpen}
onSidebarToggle={() => setIsSidebarOpen(!isSidebarOpen)}
id="fill-nav-toggle"
>
<BarsIcon />
</PageToggleButton>
</MastheadToggle>
<MastheadBrand>
<MastheadLogo href="https://patternfly.org" target="_blank">
Logo
</MastheadLogo>
</MastheadBrand>
</MastheadMain>
</Masthead>
);

const sidebar = (
<PageSidebar isSidebarOpen={isSidebarOpen} id="fill-sidebar">
<PageSidebarBody>Navigation</PageSidebarBody>
</PageSidebar>
);

return (
<Page masthead={masthead} sidebar={sidebar} isContentFilled>
<div className="pf-chatbot__compare-container">
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so these classnames are being used in the demo as necessary to create the layout and apply styles. I think they are going to need to be included/called out in the demo docs somehow. Could you maybe add a nolive code block in the content above the example to outline the structure and classes needed? Something like an outline... like:

<Page or PageSection>
  <div className="pf-chatbot__compare-container">
    <div className="pf-chatbot__compare-mobile-controls">
      ...
    </div>
  </div>
</Page or PageSection>

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All set!

<div className="pf-chatbot__compare-mobile-controls">
<ToggleGroup aria-label="Select which chatbot to display">
<ToggleGroupItem
className="pf-chatbot__compare-toggle"
text="ChatBot 1"
buttonId="toggle-group-chatbot-1"
isSelected={isSelected === 'toggle-group-chatbot-1'}
onChange={handleChildToggleClick}
/>
<ToggleGroupItem
className="pf-chatbot__compare-toggle"
text="ChatBot 2"
buttonId="toggle-group-chatbot-2"
isSelected={isSelected === 'toggle-group-chatbot-2'}
onChange={handleChildToggleClick}
/>
</ToggleGroup>
</div>
<div className="pf-chatbot__compare">
<div
className={`pf-chatbot__compare-item ${!showFirstChatbot ? 'pf-chatbot__compare-item-hidden' : undefined}`}
>
<CompareChild
input={input}
hasNewInput={hasNewInput}
name="ChatBot 1"
setIsSendButtonDisabled={setIsSendButtonDisabled}
/>
</div>
<div
className={`pf-chatbot__compare-item ${!showSecondChatbot ? 'pf-chatbot__compare-item-hidden' : undefined}`}
>
<CompareChild
input={input}
hasNewInput={hasNewInput}
name="ChatBot 2"
setIsSendButtonDisabled={setIsSendButtonDisabled}
/>
</div>
</div>
<ChatbotFooter>
<MessageBar
onSendMessage={handleSend}
hasAttachButton={false}
alwayShowSendButton
isSendButtonDisabled={isSendButtonDisabled}
/>
</ChatbotFooter>
</div>
</Page>
);
};
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
72 changes: 72 additions & 0 deletions packages/module/src/Compare/Compare.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
.pf-chatbot__compare-container {
display: flex;
flex-direction: column;
position: relative;
height: 100%;
}
.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);
}
}
1 change: 1 addition & 0 deletions packages/module/src/main.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Loading