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

DOP-4227: Chatbot FAB on all Docs pages #983

Merged
merged 23 commits into from
Jan 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
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
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v18.15
v18.15
1 change: 1 addition & 0 deletions src/components/DocumentBody.js
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ const DocumentBody = (props) => {
publishedBranches={getNestedValue(['publishedBranches'], metadata)}
slug={slug}
isInPresentationMode={isInPresentationMode}
template={template}
>
<FootnoteContext.Provider value={{ footnotes }}>
<Template {...props} useChatbot={useChatbot}>
Expand Down
1 change: 1 addition & 0 deletions src/components/DocumentBodyPreview.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ const DocumentBody = (props) => {
pageTitle={pageTitle}
publishedBranches={getNestedValue(['publishedBranches'], metadata)}
slug={slug}
template={template}
>
<FootnoteContext.Provider value={{ footnotes }}>
<Template {...props} useChatbot={useChatbot}>
Expand Down
59 changes: 59 additions & 0 deletions src/components/Widgets/ChatbotWidget/ChatbotFab.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { lazy } from 'react';
import styled from '@emotion/styled';
import { useSiteMetadata } from '../../../hooks/use-site-metadata';

const Chatbot = lazy(() => import('mongodb-chatbot-ui'));

const InputBarTrigger = lazy(() =>
import('mongodb-chatbot-ui').then((module) => ({ default: module.InputBarTrigger }))
);
const FloatingActionButtonTrigger = lazy(() =>
import('mongodb-chatbot-ui').then((module) => ({ default: module.FloatingActionButtonTrigger }))
);
const ModalView = lazy(() => import('mongodb-chatbot-ui').then((module) => ({ default: module.ModalView })));

const StyledChatBotFabContainer = styled.div`
> div {
seungpark marked this conversation as resolved.
Show resolved Hide resolved
display: none;
}
> button {
border-width: 1px;
position: unset;
}
`;

const ChatbotFab = () => {
const { snootyEnv } = useSiteMetadata();

const suggestedPrompts = [
'How do you deploy a free cluster in Atlas?',
'How do you import or migrate data into MongoDB?',
'Get started with MongoDB',
];
const CHATBOT_SERVER_BASE_URL =
snootyEnv === 'dotcomprd'
? 'https://knowledge.mongodb.com/api/v1'
: 'https://knowledge.staging.corp.mongodb.com/api/v1';
return (
<StyledChatBotFabContainer
// Inline style to default hide Chatbot FAB, Optimizely to flip this on client-side
style={{ display: 'none' }}
// Classname below for Optimizely A/B testing
className={fabChatbot}
>
<Chatbot serverBaseUrl={CHATBOT_SERVER_BASE_URL}>
<InputBarTrigger suggestedPrompts={suggestedPrompts} />
<FloatingActionButtonTrigger text={CHATBOT_WIDGET_TEXT} />
<ModalView
initialMessageText="Welcome to MongoDB AI Assistant. What can I help you with?"
initialMessageSuggestedPrompts={suggestedPrompts}
showDisclaimer
/>
</Chatbot>
</StyledChatBotFabContainer>
);
};

export const fabChatbot = 'fab-chatbot';
export const CHATBOT_WIDGET_TEXT = 'Ask MongoDB AI';
export default ChatbotFab;
3 changes: 0 additions & 3 deletions src/components/Widgets/FeedbackWidget/FeedbackButton.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,7 @@ const containerStyle = css`
border: 1px solid ${palette.blue.light1};
border-radius: 40px;
box-shadow: 0px 4px 10px -4px ${palette.gray.light2};
position: fixed;
z-index: 9;
bottom: ${theme.size.large};
right: ${theme.size.large};
color: ${palette.blue.dark1};
font-weight: 600;
font-size: 13px;
Expand Down
4 changes: 4 additions & 0 deletions src/components/Widgets/FeedbackWidget/handleScreenshot.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
import { capture, OutputType } from 'html-screen-capture-js';
import rasterizeHTML from 'rasterizehtml';
import { isBrowser } from '../../../utils/is-browser';
import { fabChatbot } from '../ChatbotWidget/ChatbotFab';
import { widgetsContainer } from '..';
import { fwTooltipId } from './components/LeafygreenTooltip';
import { fwInstructionsId, fwExitButtonId } from './components/ScreenshotButton';
import { fwFormId } from './FeedbackForm';
Expand Down Expand Up @@ -31,6 +33,8 @@ async function takeFeedbackScreenshot() {
fwTooltipId, // Don't include any button/star tooltips
fwInstructionsId, // Don't include instruction overlay
fwExitButtonId, // Don't include the X button
fabChatbot, // Don't include the FAB chatbot
widgetsContainer, // Don't include Widgets Container (visible on mobile)
],
}));
return dataUri;
Expand Down
49 changes: 43 additions & 6 deletions src/components/Widgets/index.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,41 @@
import React from 'react';
import React, { Suspense } from 'react';
import PropTypes from 'prop-types';
import styled from '@emotion/styled';
import { isBrowser } from '../../utils/is-browser';
import { theme } from '../../theme/docsTheme';
import { FeedbackProvider, FeedbackForm, FeedbackButton, useFeedbackData } from './FeedbackWidget';
import ChatbotFab from './ChatbotWidget/ChatbotFab';

const Widgets = ({ children, pageOptions, pageTitle, publishedBranches, slug, isInPresentationMode }) => {
const WidgetsContainer = styled.div`
display: flex;
flex-direction: column;
justify-content: end;
align-items: end;
gap: ${theme.size.small};
position: fixed;
right: ${theme.size.large};
bottom: ${theme.size.large};

@media ${theme.screenSize.upToSmall} {
background-color: white;
width: 100%;
mmeigs marked this conversation as resolved.
Show resolved Hide resolved
height: 60px;
flex-direction: row;
justify-content: center;
align-items: center;
box-shadow: 0px -1px 0px 0px rgba(0, 0, 0, 0.25);
right: 0;
bottom: 0;
min-width: max-content;

> button {
height: 44px;
position: unset;
}
}
`;

const Widgets = ({ children, pageOptions, pageTitle, publishedBranches, slug, isInPresentationMode, template }) => {
const url = isBrowser ? window.location.href : null;
const hideFeedbackHeader = pageOptions.hidefeedback === 'header';
const feedbackData = useFeedbackData({
Expand All @@ -20,10 +52,14 @@ const Widgets = ({ children, pageOptions, pageTitle, publishedBranches, slug, is
<FeedbackProvider page={feedbackData} hideHeader={hideFeedbackHeader}>
{children}
{!isInPresentationMode && !hideFeedback && (
<>
<FeedbackButton />
<FeedbackForm />
</>
/* Suspense at this level ensures that widgets will appear simultaneously rather than one-by-one as loaded */
<Suspense fallback={null}>
<WidgetsContainer className={widgetsContainer}>
<FeedbackButton />
<FeedbackForm />
{template !== 'landing' && <ChatbotFab />}
</WidgetsContainer>
</Suspense>
)}
</FeedbackProvider>
);
Expand All @@ -44,4 +80,5 @@ Widgets.defaultProps = {
pageOptions: {},
};

export const widgetsContainer = 'widgets';
export default Widgets;
7 changes: 7 additions & 0 deletions src/layouts/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,13 @@ const globalCSS = css`
}

${footerOverrides}

/* To ensure the Chatbot ModalView has precedence over the consistent-nav */
seungpark marked this conversation as resolved.
Show resolved Hide resolved
/* In next Chatbot release, a className can be specified as a prop giving us more granular specificity */
/* At that time, remove or improve the following lines */
div[id^=modal-] {
z-index: 1000 !important;
}
`;

const GlobalGrid = styled('div')`
Expand Down
2 changes: 1 addition & 1 deletion src/styles/mongodb-docs.css
Original file line number Diff line number Diff line change
Expand Up @@ -1056,4 +1056,4 @@ dl.method > dt em {

#tooltip-1 {
z-index: 3;
}
}
1 change: 1 addition & 0 deletions tests/testSetup.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,4 @@ window.matchMedia = () => ({ addListener: () => {}, removeListener: () => {} });
window.EventSource = () => ({ EventSource: EventSource });
window.scrollTo = () => {};
global.fetch = jest.fn();
window.crypto.randomUUID = crypto.randomUUID;
51 changes: 42 additions & 9 deletions tests/unit/Presentation.test.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,37 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { render, screen, waitFor } from '@testing-library/react';
import * as Gatsby from 'gatsby';
import { mockLocation } from '../utils/mock-location';
import DocumentBody from '../../src/components/DocumentBody';
import { FEEDBACK_BUTTON_TEXT } from '../../src/components/Widgets/FeedbackWidget/constants';
import { CHATBOT_WIDGET_TEXT } from '../../src/components/Widgets/ChatbotWidget/ChatbotFab';
import mockPageContext from './data/PageContext.test.json';
import mockSnootyMetadata from './data/SnootyMetadata.json';

jest.mock(`../../src/utils/use-snooty-metadata`, () => {
return () => mockSnootyMetadata;
});

const useStaticQuery = jest.spyOn(Gatsby, 'useStaticQuery');
useStaticQuery.mockImplementation(() => ({
site: {
siteMetadata: {
commitHash: '',
parserBranch: '',
patchId: '',
pathPrefix: '',
snootyBranch: '',
user: '',
snootyEnv: 'production',
},
},
}));

describe('DocumentBody', () => {
beforeAll(() => {
jest.spyOn(document, 'querySelector');
});
it('renders the necessary elements', () => {
it('renders the necessary elements', async () => {
mockLocation(null);
render(<DocumentBody location={window.location} pageContext={mockPageContext} />);

Expand All @@ -28,24 +45,40 @@ describe('DocumentBody', () => {
expect(languageSelector.querySelectorAll('li')).toHaveLength(2);
}

const feedbackWidget = screen.getByText(FEEDBACK_BUTTON_TEXT);
expect(feedbackWidget).toBeVisible();
expect(feedbackWidget).toMatchSnapshot();
await waitFor(
() => {
const feedbackWidget = screen.getByText(FEEDBACK_BUTTON_TEXT);
expect(feedbackWidget).toBeVisible();
expect(feedbackWidget).toMatchSnapshot();

const chatbotWidget = screen.getByText(CHATBOT_WIDGET_TEXT);
/* NOT to be visible for now, with display:none */
expect(chatbotWidget).not.toBeVisible();
expect(chatbotWidget).toMatchSnapshot();
},
{ timeout: 8000 }
);

const mainNav = screen.getByRole('img', { name: 'MongoDB logo' });
expect(mainNav).toBeVisible();
expect(mainNav).toMatchSnapshot();
});
/* Give more time for lazy-loaded components to be found */
}, 12000);

it('does not render the following elements, footer, feedback widget, navigation', () => {
it('does not render the following elements, footer, feedback widget, navigation', async () => {
mockLocation('?presentation=true');
render(<DocumentBody location={window.location} pageContext={mockPageContext} />);
// use `queryBy` to avoid throwing an error with `getBy`
const footer = screen.queryByTestId('consistent-footer');
expect(footer).not.toBeInTheDocument();

const feedbackWidget = screen.queryByText(FEEDBACK_BUTTON_TEXT);
expect(feedbackWidget).not.toBeInTheDocument();
await waitFor(
() => {
const feedbackWidget = screen.queryByText(FEEDBACK_BUTTON_TEXT);
expect(feedbackWidget).not.toBeInTheDocument();
},
{ timeout: 5000 }
);

const mainNav = screen.queryByRole('img', { name: 'MongoDB logo' });
expect(mainNav).not.toBeInTheDocument();
Expand Down
9 changes: 6 additions & 3 deletions tests/unit/__snapshots__/Presentation.test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -774,10 +774,7 @@ exports[`DocumentBody renders the necessary elements 2`] = `
border: 1px solid #0498EC;
border-radius: 40px;
box-shadow: 0px 4px 10px -4px #E8EDEB;
position: fixed;
z-index: 9;
bottom: 32px;
right: 32px;
color: #1254B7;
font-weight: 600;
font-size: 13px;
Expand Down Expand Up @@ -822,6 +819,12 @@ exports[`DocumentBody renders the necessary elements 2`] = `
`;

exports[`DocumentBody renders the necessary elements 3`] = `
<b>
Ask MongoDB AI
</b>
`;

exports[`DocumentBody renders the necessary elements 4`] = `
.emotion-0 {
width: 100%;
min-width: 100px;
Expand Down
Loading