Skip to content

Commit

Permalink
feat(Compare): Add compare layout
Browse files Browse the repository at this point in the history
  • Loading branch information
rebeccaalpert committed Jan 8, 2025
1 parent 9653fd2 commit 1e4495f
Show file tree
Hide file tree
Showing 4 changed files with 339 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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

```
Original file line number Diff line number Diff line change
@@ -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<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,
id: generateId(),
name: 'You',
role: 'user',
content: input,
timestamp: `${date?.toLocaleDateString()} ${date?.toLocaleTimeString()}`
});
newMessages.push({
avatar: patternflyAvatar,
id: generateId(),
name: name,

Check failure on line 56 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

Expected property shorthand
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: name,

Check failure on line 76 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

Expected property shorthand
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]);

Check warning on line 103 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-assistant-1');
const [showFirstChatbot, setShowFirstChatbot] = React.useState(true);
const [showSecondChatbot, setShowSecondChatbot] = React.useState(false);
const [isSidebarOpen, setIsSidebarOpen] = React.useState(false);
const historyRef = React.useRef<HTMLButtonElement>(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 = (
<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>
);

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 */
<SkipToContent href="#" onClick={skipToChatbot}>
Skip to chatbot
</SkipToContent>
);

return (
<Page skipToContent={skipToContent} masthead={masthead} sidebar={sidebar} isContentFilled>
<div className="pf-chatbot__compare-container">
<div className="pf-chatbot__compare-mobile-controls">
<ToggleGroup aria-label="Select which assistant to display">
<ToggleGroupItem
className="pf-chatbot__compare-toggle"
text="Assistant 1"
buttonId="toggle-group-assistant-1"
isSelected={isSelected === 'toggle-group-assistant-1'}
onChange={handleChildToggleClick}
/>
<ToggleGroupItem
className="pf-chatbot__compare-toggle"
text="Assistant 2"
buttonId="toggle-group-assistant-2"
isSelected={isSelected === 'toggle-group-assistant-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 One" />
</div>
<div
className={`pf-chatbot__compare-item ${!showSecondChatbot ? 'pf-chatbot__compare-item-hidden' : undefined}`}
>
<CompareChild input={input} hasNewInput={hasNewInput} name="ChatBot Two" />
</div>
</div>
<ChatbotFooter>
<MessageBar onSendMessage={handleSend} hasAttachButton={false} />
</ChatbotFooter>
</div>
</Page>
);
};
77 changes: 77 additions & 0 deletions packages/module/src/Compare/Compare.scss
Original file line number Diff line number Diff line change
@@ -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);
}
}
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

0 comments on commit 1e4495f

Please sign in to comment.