Skip to content

Commit

Permalink
feat(content-sidebar): Expand Box AI Sidebar to Modal (#3878)
Browse files Browse the repository at this point in the history
* feat(content-sidebar): Expand Box AI Sidebar to Modal

* feat(content-sidebar): Expand Box AI Sidebar to Modal
  • Loading branch information
kkuliczkowski-box authored Jan 29, 2025
1 parent 512c975 commit 9c7aab0
Show file tree
Hide file tree
Showing 8 changed files with 196 additions and 83 deletions.
2 changes: 2 additions & 0 deletions i18n/en-US.properties
Original file line number Diff line number Diff line change
Expand Up @@ -644,6 +644,8 @@ be.sidebarAccessStats = Access Stats
be.sidebarActivityTitle = Activity
# Generic Box AI content type opened used in welcome message and placeholder
be.sidebarBoxAIContent = content
# Label for button that triggers switch to Box AI Modal
be.sidebarBoxAISwitchToModalView = Switch to modal view
# Title for the preview Box AI feed.
be.sidebarBoxAITitle = Box AI
# Title for the sidebar content insights.
Expand Down
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,8 @@
"@babel/types": "^7.24.7",
"@box/blueprint-web": "^7.36.3",
"@box/blueprint-web-assets": "^4.28.0",
"@box/box-ai-agent-selector": "^0.15.8",
"@box/box-ai-content-answers": "^0.79.3",
"@box/box-ai-agent-selector": "^0.22.0",
"@box/box-ai-content-answers": "^0.85.2",
"@box/cldr-data": "^34.2.0",
"@box/frontend": "^10.0.0",
"@box/item-icon": "^0.9.58",
Expand Down Expand Up @@ -306,8 +306,8 @@
"peerDependencies": {
"@box/blueprint-web": "^7.36.3",
"@box/blueprint-web-assets": "^4.28.0",
"@box/box-ai-agent-selector": "^0.15.8",
"@box/box-ai-content-answers": "^0.79.3",
"@box/box-ai-agent-selector": "^0.22.0",
"@box/box-ai-content-answers": "^0.85.2",
"@box/cldr-data": ">=34.2.0",
"@box/item-icon": "^0.9.58",
"@box/metadata-editor": "^0.79.1",
Expand Down
5 changes: 5 additions & 0 deletions src/elements/common/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,11 @@ const messages = defineMessages({
description: 'Generic Box AI content type opened used in welcome message and placeholder',
defaultMessage: 'content',
},
sidebarBoxAISwitchToModalView: {
id: 'be.sidebarBoxAISwitchToModalView',
description: 'Label for button that triggers switch to Box AI Modal',
defaultMessage: 'Switch to modal view',
},
sidebarActivityTitle: {
id: 'be.sidebarActivityTitle',
description: 'Title for the preview activity feed.',
Expand Down
27 changes: 23 additions & 4 deletions src/elements/content-sidebar/BoxAISidebar.scss
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ $agentSelectorSidebarWidth: 211px;
height: 100%;
max-height: 100%;

&.with-modal-open {
display: none;
}

.bcs-content-header {
margin: 0 tokens.$space-4;
}

.bcs-scroll-content {
width: auto;
height: 100%;
Expand All @@ -34,13 +42,20 @@ $agentSelectorSidebarWidth: 211px;
}
}

// Limit the width of Agent Selector to accomodate action buttons
.bcs-BoxAISidebar-agent-selector-container {
max-width: 217px;
}

@include breakpoint($medium-screen) {
.bcs-BoxAISidebar-agent-selector-container {
max-width: none;
}
}

.bcs-BoxAISidebar-chat-actions {
display: flex;
justify-content: right;

.bcs-BoxAISidebar-expand {
margin-left: tokens.$space-2;
}
}

.sidebar-chip {
Expand All @@ -51,5 +66,9 @@ $agentSelectorSidebarWidth: 211px;
width: 100%;
}
}

.bcs-BoxAISidebar-expand {
margin-left: tokens.$space-2;
}
}
}
21 changes: 18 additions & 3 deletions src/elements/content-sidebar/BoxAISidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,19 @@ export interface BoxAISidebarContextValues {
cache: { encodedSession?: string | null; questions?: QuestionType[] };
contentName: string;
elementId: string;
fileExtension: string;
isStopResponseEnabled: boolean;
itemSize?: string;
recordAction: (params: RecordActionType) => void;
setCacheValue: (key: 'encodedSession' | 'questions', value: string | null | QuestionType[]) => void;
userInfo: { name: string, avatarURL: string };
userInfo: { name: string; avatarURL: string };
}

export const BoxAISidebarContext = React.createContext<BoxAISidebarContextValues>({
cache: null,
contentName: '',
elementId: '',
fileExtension: '',
isStopResponseEnabled: false,
recordAction: noop,
setCacheValue: noop,
Expand Down Expand Up @@ -66,7 +69,8 @@ export interface BoxAISidebarProps {
isResetChatEnabled: boolean;
isStopResponseEnabled?: boolean;
isStreamingEnabled: boolean;
userInfo: { name: string, avatarURL: string };
itemSize?: string;
userInfo: { name: string; avatarURL: string };
recordAction: (params: RecordActionType) => void;
setCacheValue: (key: 'encodedSession' | 'questions', value: string | null | QuestionType[]) => void;
}
Expand All @@ -81,6 +85,7 @@ const BoxAISidebar = (props: BoxAISidebarProps) => {
getSuggestedQuestions,
isIntelligentQueryMode,
isStopResponseEnabled,
itemSize,
recordAction,
setCacheValue,
userInfo,
Expand Down Expand Up @@ -113,7 +118,17 @@ const BoxAISidebar = (props: BoxAISidebarProps) => {
// BoxAISidebarContent is using withApiWrapper that is not passing all provided props,
// that's why we need to use provider to pass other props
<BoxAISidebarContext.Provider
value={{ cache, contentName, elementId, isStopResponseEnabled, setCacheValue, recordAction, userInfo }}
value={{
cache,
contentName,
elementId,
fileExtension,
isStopResponseEnabled,
itemSize,
setCacheValue,
recordAction,
userInfo,
}}
>
<BoxAISidebarContent
getSuggestedQuestions={getSuggestedQuestions}
Expand Down
129 changes: 93 additions & 36 deletions src/elements/content-sidebar/BoxAISidebarContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@
import * as React from 'react';
import flow from 'lodash/flow';
import { useIntl } from 'react-intl';
import classNames from 'classnames';
import { AgentsProvider, BoxAiAgentSelectorWithApi } from '@box/box-ai-agent-selector';
import { Text } from '@box/blueprint-web';
import { IconButton, Text, Tooltip } from '@box/blueprint-web';
import { ArrowsExpand } from '@box/blueprint-web-assets/icons/Line';
import {
BoxAiContentAnswers,
ClearConversationButton,
IntelligenceModal,
withApiWrapper,
// @ts-expect-error - TS2305 - Module '"@box/box-ai-content-answers"' has no exported member 'ApiWrapperProps'.
type ApiWrapperProps,
Expand Down Expand Up @@ -47,8 +50,18 @@ function BoxAISidebarContent(props: ApiWrapperProps) {
...rest
} = props;
const { formatMessage } = useIntl();
const { cache, contentName, elementId, isStopResponseEnabled, recordAction, setCacheValue, userInfo } =
React.useContext(BoxAISidebarContext);
const [isModalOpen, setIsModalOpen] = React.useState(false);
const {
cache,
contentName,
elementId,
fileExtension,
isStopResponseEnabled,
itemSize,
recordAction,
setCacheValue,
userInfo,
} = React.useContext(BoxAISidebarContext);
const { questions: cacheQuestions } = cache;

if (cache.encodedSession !== encodedSession) {
Expand All @@ -59,6 +72,14 @@ function BoxAISidebarContent(props: ApiWrapperProps) {
setCacheValue('questions', questions);
}

const onModalClose = () => {
setIsModalOpen(false);
};

const onSwitchToModalClick = () => {
setIsModalOpen(true);
};

React.useEffect(() => {
if (!encodedSession && createSession) {
createSession();
Expand All @@ -83,15 +104,16 @@ function BoxAISidebarContent(props: ApiWrapperProps) {
{formatMessage(messages.sidebarBoxAITitle)}
</Text>
{isAIStudioAgentSelectorEnabled && (
<BoxAiAgentSelectorWithApi
fetcher={getAIStudioAgents}
hostAppName={hostAppName}
onSelectAgent={onSelectAgent}
recordAction={recordAction}
shouldHideAgentSelectorOnLoad
// @ts-ignore variant will be available in higher version
variant="sidebar"
/>
<div className="bcs-BoxAISidebar-agent-selector-container">
<BoxAiAgentSelectorWithApi
fetcher={getAIStudioAgents}
hostAppName={hostAppName}
onSelectAgent={onSelectAgent}
recordAction={recordAction}
shouldHideAgentSelectorOnLoad={false}
variant="sidebar"
/>
</div>
)}
</div>
);
Expand All @@ -101,35 +123,70 @@ function BoxAISidebarContent(props: ApiWrapperProps) {
<>
{renderBoxAISidebarTitle()}
{isResetChatEnabled && <ClearConversationButton onClick={onClearAction} />}
<Tooltip content={formatMessage(messages.sidebarBoxAISwitchToModalView)} variant="standard">
<IconButton
aria-label={formatMessage(messages.sidebarBoxAISwitchToModalView)}
className="bcs-BoxAISidebar-expand"
data-target-id="IconButton-expandBoxAISidebar"
icon={ArrowsExpand}
onClick={onSwitchToModalClick}
size="small"
/>
</Tooltip>
</>
);

return (
<AgentsProvider>
<SidebarContent
actions={renderActions()}
className="bcs-BoxAISidebar"
elementId={elementId}
sidebarView={SIDEBAR_VIEW_BOXAI}
>
<div className="bcs-BoxAISidebar-content">
<BoxAiContentAnswers
className="bcs-BoxAISidebar-contentAnswers"
contentName={contentName}
contentType={formatMessage(messages.sidebarBoxAIContent)}
hostAppName={hostAppName}
isAIStudioAgentSelectorEnabled={isAIStudioAgentSelectorEnabled}
isStopResponseEnabled={isStopResponseEnabled}
questions={questions}
stopQuestion={stopQuestion}
submitQuestion={sendQuestion}
userInfo={userInfo}
variant="sidebar"
recordAction={recordAction}
{...rest}
/>
</div>
</SidebarContent>
<>
<SidebarContent
actions={renderActions()}
className={classNames('bcs-BoxAISidebar', { 'with-modal-open': isModalOpen })}
elementId={elementId}
sidebarView={SIDEBAR_VIEW_BOXAI}
>
<div className="bcs-BoxAISidebar-content">
<BoxAiContentAnswers
className="bcs-BoxAISidebar-contentAnswers"
contentName={contentName}
contentType={formatMessage(messages.sidebarBoxAIContent)}
hostAppName={hostAppName}
isAIStudioAgentSelectorEnabled={isAIStudioAgentSelectorEnabled}
isStopResponseEnabled={isStopResponseEnabled}
questions={questions}
stopQuestion={stopQuestion}
submitQuestion={sendQuestion}
userInfo={userInfo}
variant="sidebar"
recordAction={recordAction}
{...rest}
/>
</div>
</SidebarContent>
<IntelligenceModal
contentName={contentName}
contentType={formatMessage(messages.sidebarBoxAIContent)}
extension={fileExtension}
getAIStudioAgents={getAIStudioAgents}
hostAppName={hostAppName}
isAIStudioAgentSelectorEnabled={isAIStudioAgentSelectorEnabled}
isStopResponseEnabled={isStopResponseEnabled}
itemSize={itemSize}
onModalClose={onModalClose}
onOpenChange={onModalClose}
onSelectAgent={onSelectAgent}
open={isModalOpen}
questions={questions}
recordAction={isModalOpen ? recordAction : undefined}
showLoadingIndicator={false}
stopPropagationOnEsc
submitQuestion={sendQuestion}
userInfo={userInfo}
variant="collapsible"
{...rest}
shouldRenderProviders={false}
/>
</>
</AgentsProvider>
);
}
Expand Down
40 changes: 40 additions & 0 deletions src/elements/content-sidebar/__tests__/BoxAISidebar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ describe('elements/content-sidebar/BoxAISidebar', () => {
getAnswerStreaming: jest.fn(),
getSuggestedQuestions: jest.fn(),
hostAppName: 'appName',
itemSize: '1234',
isAgentSelectorEnabled: false,
isAIStudioAgentSelectorEnabled: true,
isCitationsEnabled: true,
Expand All @@ -93,6 +94,15 @@ describe('elements/content-sidebar/BoxAISidebar', () => {
});
};

beforeAll(() => {
// Required to pass Blueprint Interactivity test for buttons with tooltip
Object.defineProperty(HTMLElement.prototype, 'offsetParent', {
get() {
return this.parentNode;
},
});
});

afterEach(() => {
jest.clearAllMocks();
});
Expand Down Expand Up @@ -180,4 +190,34 @@ describe('elements/content-sidebar/BoxAISidebar', () => {

expect(tooltip).toBeInTheDocument();
});

test('should have accessible "Switch to modal view" button', async () => {
await renderComponent();

expect(screen.getByRole('button', { name: 'Switch to modal view' })).toBeInTheDocument();
});

test('should display "Switch to modal view" tooltip', async () => {
await renderComponent();

const button = screen.getByRole('button', { name: 'Switch to modal view' });
await userEvent.hover(button);
const tooltip = await screen.findByRole('tooltip', { name: 'Switch to modal view' });

expect(tooltip).toBeInTheDocument();
});

test('should open Intelligence Modal when clicking on "Switch to modal view" button and close when clicking "Switch to sidebar view"', async () => {
await renderComponent();

const switchToModalButton = screen.getByRole('button', { name: 'Switch to modal view' });
await userEvent.click(switchToModalButton);

expect(await screen.findByTestId('content-answers-modal')).toBeInTheDocument();

const switchToSidebarButton = screen.getByRole('button', { name: 'Switch to sidebar view' });
await userEvent.click(switchToSidebarButton);

expect(screen.queryByTestId('content-answers-modal')).not.toBeInTheDocument();
});
});
Loading

0 comments on commit 9c7aab0

Please sign in to comment.