Skip to content

Commit

Permalink
load all messages for explorer assistant response, app conversation d…
Browse files Browse the repository at this point in the history
…isplay, and transcript export; plus overflow support for assistant canvas (#228)
  • Loading branch information
bkrabach authored Nov 7, 2024
1 parent 026162e commit 27da430
Show file tree
Hide file tree
Showing 19 changed files with 702 additions and 449 deletions.
1 change: 1 addition & 0 deletions workbench-app/src/Constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export const Constants = {
maxInputLength: 2000000, // 2M tokens, effectively unlimited
minChatWidthPercent: 20,
defaultChatWidthPercent: 33,
maxMessagesPerRequest: 500,
maxFileAttachmentsPerMessage: 10,
loaderDelayMs: 100,
responsiveBreakpoints: {
Expand Down
13 changes: 10 additions & 3 deletions workbench-app/src/components/App/Loading.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Copyright (c) Microsoft. All rights reserved.

import { Spinner, makeStyles, shorthands, tokens } from '@fluentui/react-components';
import { Spinner, makeStyles, mergeClasses, shorthands, tokens } from '@fluentui/react-components';
import React from 'react';
import { Constants } from '../../Constants';

Expand All @@ -10,7 +10,12 @@ const useClasses = makeStyles({
},
});

export const Loading: React.FC = () => {
interface LoadingProps {
className?: string;
}

export const Loading: React.FC<LoadingProps> = (props) => {
const { className } = props;
const classes = useClasses();
const [showSpinner, setShowSpinner] = React.useState(false);

Expand All @@ -22,5 +27,7 @@ export const Loading: React.FC = () => {
return () => clearTimeout(timer);
}, []);

return showSpinner ? <Spinner className={classes.root} size="medium" label="Loading..." /> : null;
return showSpinner ? (
<Spinner className={mergeClasses(classes.root, className)} size="medium" label="Loading..." />
) : null;
};
91 changes: 91 additions & 0 deletions workbench-app/src/components/App/OverflowMenu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import {
Button,
makeStyles,
Menu,
MenuItem,
MenuList,
MenuPopover,
MenuTrigger,
Slot,
tokens,
useIsOverflowItemVisible,
useOverflowMenu,
} from '@fluentui/react-components';
import { MoreHorizontalRegular } from '@fluentui/react-icons';
import React from 'react';

const useClasses = makeStyles({
menu: {
backgroundColor: tokens.colorNeutralBackground1,
},
menuButton: {
alignSelf: 'center',
},
});

export interface OverflowMenuItemData {
id: string;
icon?: Slot<'span'>;
name?: string;
}

interface OverflowMenuItemProps {
item: OverflowMenuItemData;
onClick: (event: React.MouseEvent, id: string) => void;
}

export const OverflowMenuItem: React.FC<OverflowMenuItemProps> = (props) => {
const { item, onClick } = props;
const isVisible = useIsOverflowItemVisible(item.id);

if (isVisible) {
return null;
}

return (
<MenuItem key={item.id} icon={item.icon} onClick={(event) => onClick(event, item.id)}>
{item.name}
</MenuItem>
);
};

interface OverflowMenuProps {
items: OverflowMenuItemData[];
onItemSelect: (id: string) => void;
}

export const OverflowMenu: React.FC<OverflowMenuProps> = (props) => {
const { items, onItemSelect } = props;
const classes = useClasses();
const { ref, isOverflowing, overflowCount } = useOverflowMenu<HTMLButtonElement>();

const handleItemClick = (_event: React.MouseEvent, id: string) => {
onItemSelect(id);
};

if (!isOverflowing) {
return null;
}

return (
<Menu hasIcons={items.find((item) => item.icon !== undefined) !== undefined}>
<MenuTrigger disableButtonEnhancement>
<Button
className={classes.menuButton}
appearance="transparent"
ref={ref}
icon={<MoreHorizontalRegular />}
aria-label={`${overflowCount} more options`}
role="tab"
/>
</MenuTrigger>
<MenuPopover>
<MenuList className={classes.menu}>
{items.map((item) => (
<OverflowMenuItem key={item.id} item={item} onClick={handleItemClick}></OverflowMenuItem>
))}
</MenuList>
</MenuPopover>
</Menu>
);
};
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
// Copyright (c) Microsoft. All rights reserved.

import { Tab, TabList, makeStyles, shorthands, tokens } from '@fluentui/react-components';
import { Overflow, OverflowItem, Tab, TabList, makeStyles, shorthands, tokens } from '@fluentui/react-components';
import React from 'react';
import { useChatCanvasController } from '../../../libs/useChatCanvasController';
import { Assistant } from '../../../models/Assistant';
import { Conversation } from '../../../models/Conversation';
import { OverflowMenu, OverflowMenuItemData } from '../../App/OverflowMenu';
import { AssistantCanvas } from './AssistantCanvas';

const useClasses = makeStyles({
Expand All @@ -20,13 +21,10 @@ const useClasses = makeStyles({
justifyContent: 'space-between',
...shorthands.padding(tokens.spacingVerticalS),
},
headerContent: {
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
gap: tokens.spacingHorizontalM,
header: {
overflow: 'hidden',
...shorthands.padding(tokens.spacingVerticalS),
...shorthands.borderBottom(tokens.strokeWidthThin, 'solid', tokens.colorNeutralStroke1),
},
});

Expand All @@ -41,40 +39,66 @@ export const AssistantCanvasList: React.FC<AssistantCanvasListProps> = (props) =
const classes = useClasses();
const chatCanvasController = useChatCanvasController();

const tabItems = React.useMemo(
() =>
conversationAssistants.slice().map(
(assistant): OverflowMenuItemData => ({
id: assistant.id,
name: assistant.name,
}),
),
[conversationAssistants],
);

const handleTabSelect = React.useCallback(
(id: string) => {
// Find the assistant that corresponds to the selected tab
const conversationAssistant = conversationAssistants.find(
(conversationAssistant) => conversationAssistant.id === id,
);

// Set the new assistant as the active assistant
// If we can't find the assistant, we'll set the assistant to undefined
chatCanvasController.transitionToState({
selectedAssistantId: conversationAssistant?.id,
selectedAssistantStateId: undefined,
});
},
[chatCanvasController, conversationAssistants],
);

const assistant = React.useMemo(
() => selectedAssistant ?? conversationAssistants[0],
[selectedAssistant, conversationAssistants],
);

if (conversationAssistants.length === 1) {
// Only one assistant, no need to show tabs, just show the single assistant
return <AssistantCanvas assistant={conversationAssistants[0]} conversationId={conversation.id} />;
}

const assistant = selectedAssistant ?? conversationAssistants[0];

// Multiple assistants, show tabs
return (
<div className={classes.root}>
<div className={classes.headerContent}>
<TabList
selectedValue={assistant.id}
onTabSelect={(_event, selectedItem) => {
// Find the assistant that corresponds to the selected tab
const conversationAssistant = conversationAssistants.find(
(conversationAssistant) => conversationAssistant.id === selectedItem.value,
);

// Set the new assistant as the active assistant
// If we can't find the assistant, we'll set the assistant to undefined
chatCanvasController.transitionToState({
selectedAssistantId: conversationAssistant?.id,
selectedAssistantStateId: undefined,
});
}}
size="small"
>
{conversationAssistants.slice().map((assistant) => (
<Tab value={assistant.id} key={assistant.id}>
{assistant.name}
</Tab>
))}
</TabList>
<div className={classes.header}>
<Overflow minimumVisible={1}>
<TabList
selectedValue={assistant.id}
onTabSelect={(_, data) => handleTabSelect(data.value as string)}
size="small"
>
{tabItems.map((tabItem) => (
<OverflowItem
key={tabItem.id}
id={tabItem.id}
priority={tabItem.id === assistant.id ? 2 : 1}
>
<Tab value={tabItem.id}>{tabItem.name}</Tab>
</OverflowItem>
))}
<OverflowMenu items={tabItems} onItemSelect={handleTabSelect} />
</TabList>
</Overflow>
</div>
<AssistantCanvas assistant={assistant} conversationId={conversation.id} />
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,12 @@
// Copyright (c) Microsoft. All rights reserved.

import {
SelectTabData,
SelectTabEvent,
SelectTabEventHandler,
Tab,
TabList,
makeStyles,
shorthands,
tokens,
} from '@fluentui/react-components';
import { Overflow, OverflowItem, Tab, TabList, makeStyles, shorthands, tokens } from '@fluentui/react-components';
import React from 'react';
import { useChatCanvasController } from '../../../libs/useChatCanvasController';
import { Assistant } from '../../../models/Assistant';
import { AssistantStateDescription } from '../../../models/AssistantStateDescription';
import { useAppSelector } from '../../../redux/app/hooks';
import { OverflowMenu, OverflowMenuItemData } from '../../App/OverflowMenu';
import { AssistantInspector } from './AssistantInspector';

const useClasses = makeStyles({
Expand All @@ -25,17 +17,8 @@ const useClasses = makeStyles({
},
header: {
flexShrink: 0,
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
backgroundImage: `linear-gradient(to right, ${tokens.colorNeutralBackground1}, ${tokens.colorBrandBackground2})`,
},
headerContent: {
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
gap: tokens.spacingHorizontalM,
height: 'fit-content',
overflow: 'hidden',
...shorthands.padding(tokens.spacingVerticalS),
...shorthands.borderBottom(tokens.strokeWidthThin, 'solid', tokens.colorNeutralStroke1),
},
Expand All @@ -57,6 +40,34 @@ export const AssistantInspectorList: React.FC<AssistantInspectorListProps> = (pr
const chatCanvasState = useAppSelector((state) => state.chatCanvas);
const chatCanvasController = useChatCanvasController();

const selectedStateDescription = React.useMemo(
() =>
stateDescriptions.find(
(stateDescription) => stateDescription.id === chatCanvasState.selectedAssistantStateId,
) ?? stateDescriptions[0],
[stateDescriptions, chatCanvasState.selectedAssistantStateId],
);

const tabItems = React.useMemo(
() =>
stateDescriptions
.filter((stateDescription) => stateDescription.id !== 'config')
.map(
(stateDescription): OverflowMenuItemData => ({
id: stateDescription.id,
name: stateDescription.displayName,
}),
),
[stateDescriptions],
);

const handleTabSelect = React.useCallback(
(id: string) => {
chatCanvasController.transitionToState({ selectedAssistantStateId: id });
},
[chatCanvasController],
);

if (stateDescriptions.length === 1) {
// Only one assistant state, no need to show tabs, just show the single assistant state
return (
Expand All @@ -68,42 +79,37 @@ export const AssistantInspectorList: React.FC<AssistantInspectorListProps> = (pr
);
}

const onTabSelect: SelectTabEventHandler = (_event: SelectTabEvent, data: SelectTabData) => {
chatCanvasController.transitionToState({ selectedAssistantStateId: data.value as string });
};

if (stateDescriptions.length === 0) {
return (
<div className={classes.root}>
<div className={classes.header}>
<div className={classes.headerContent}>
<div>No assistant state inspectors available</div>
</div>
<div>No assistant state inspectors available</div>
</div>
</div>
);
}

const selectedStateDescription =
stateDescriptions.find(
(stateDescription) => stateDescription.id === chatCanvasState.selectedAssistantStateId,
) ?? stateDescriptions[0];
const selectedTab = selectedStateDescription.id;

return (
<div className={classes.root}>
<div className={classes.header}>
<div className={classes.headerContent}>
<TabList selectedValue={selectedTab} onTabSelect={onTabSelect} size="small">
{stateDescriptions
.filter((stateDescription) => stateDescription.id !== 'config')
.map((stateDescription) => (
<Tab key={stateDescription.id} value={stateDescription.id}>
{stateDescription.displayName}
</Tab>
))}
<Overflow minimumVisible={1}>
<TabList
selectedValue={selectedStateDescription.id}
onTabSelect={(_, data) => handleTabSelect(data.value as string)}
size="small"
>
{tabItems.map((tabItem) => (
<OverflowItem
key={tabItem.id}
id={tabItem.id}
priority={tabItem.id === selectedStateDescription.id ? 2 : 1}
>
<Tab value={tabItem.id}>{tabItem.name}</Tab>
</OverflowItem>
))}
<OverflowMenu items={tabItems} onItemSelect={handleTabSelect} />
</TabList>
</div>
</Overflow>
</div>
<div className={classes.body}>
<AssistantInspector
Expand Down
Loading

0 comments on commit 27da430

Please sign in to comment.