Skip to content

Commit

Permalink
adds new "frontdoor" conversation-first UX exploration for workbench …
Browse files Browse the repository at this point in the history
…app (microsoft#178)

This is the first merge of the new UX exploration, it'll remain
accessible at `/frontdoor` while we test and refine further, before
becoming the new default experience at the root path. You can return to
the current dashboard via the menu with your profile in the upper right
corner of the screen.
  • Loading branch information
bkrabach authored Oct 26, 2024
1 parent 9e0dfee commit 1cb0de6
Show file tree
Hide file tree
Showing 60 changed files with 3,005 additions and 783 deletions.
1 change: 1 addition & 0 deletions workbench-app/.vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@
"fastapi",
"fileversion",
"fluentui",
"frontdoor",
"getfixturevalue",
"griffel",
"hashkey",
Expand Down
76 changes: 76 additions & 0 deletions workbench-app/docs/APP_DEV_GUIDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# Semantic Workbench App Dev Guide

This is an early collection of notes for conventions being put in place for the development of the Semantic Workbench React/Typescript web app.

## Design System

The Semantic Workbench app uses the Fluent UI React v9 and Fluent Copilot component libraries.

Fluent UI React v9:

- Docs: https://react.fluentui.dev/
- GitHub: https://github.com/microsoft/fluentui

Fluent Copilot (formerly Fluent AI):

- Docs: https://ai.fluentui.dev/
- GitHub: https://github.com/microsoft/fluentai

### Styling components

Create a `useClasses` function that returns an object of classnames using the `mergeStyle` function from the `@fluentui/react` package. Within your component, create a `const classes = useClasses();` and use the classnames in the component.

Sample:

```
import { mergeStyle } from '@fluentui/react';
const useClasses = {
root: mergeStyle({
color: 'red',
}),
};
const MyButton = () => {
const classes = useClasses();
return (
<div className={classes.root}>
<Button>
Click me
</Button>
</div>
);
};
```

Docs:

- Fluent: Styling components: https://react.fluentui.dev/?path=/docs/concepts-developer-styling-components--docs
- Griffel: https://griffel.js.org/

### Z-index

Use the Fluent tokens for z-index.

- zIndex values
- .zIndexBackground = 0
- .zIndexContent = 1
- .zIndexOverlay = 1000
- .zIndexPopup = 2000
- .zIndexMessage = 3000
- .zIndexFloating = 4000
- .zIndexPriority = 5000
- .zIndexDebug = 6000

Sample:

```
import { mergeStyles, tokens } from '@fluentui/react';
const useClasses = {
root: mergeStyle({
position: 'relative',
zIndex: tokens.zIndexContent,
}),
};
```
5 changes: 3 additions & 2 deletions workbench-app/src/Constants.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export const Constants = {
app: {
name: 'Semantic Workbench',
conversationRedirectPath: '/frontdoor',
defaultTheme: 'light',
defaultBrand: 'local',
autoScrollThreshold: 100,
Expand Down Expand Up @@ -38,14 +39,14 @@ export const Constants = {
],
},
assistantCategories: {
Recommended: [''],
Recommended: ['explorer-assistant.made-exploration-team', 'guided-conversation-assistant.made-exploration'],
'Example Implementations': [
'python-01-echo-bot.workbench-explorer',
'python-02-simple-chatbot.workbench-explorer',
'python-03-multimodel-chatbot.workbench-explorer',
'canonical-assistant.semantic-workbench',
],
Experimental: [''],
Experimental: ['prospector-assistant.made-exploration'],
},
msal: {
method: 'redirect', // 'redirect' | 'popup'
Expand Down
6 changes: 3 additions & 3 deletions workbench-app/src/components/App/AppView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { AppFooter } from './AppFooter';
import { AppHeader } from './AppHeader';

const useClasses = makeStyles({
body: {
documentBody: {
backgroundImage: `url('/assets/background-1.jpg')`,
},
root: {
Expand Down Expand Up @@ -52,11 +52,11 @@ export const AppView: React.FC<AppViewProps> = (props) => {
const navigate = useNavigate();

React.useLayoutEffect(() => {
document.body.className = classes.body;
document.body.className = classes.documentBody;
return () => {
document.body.className = '';
};
}, [classes.body]);
}, [classes.documentBody]);

React.useEffect(() => {
if (!completedFirstRun?.app && window.location.pathname !== '/terms') {
Expand Down
64 changes: 15 additions & 49 deletions workbench-app/src/components/App/CommandButton.tsx
Original file line number Diff line number Diff line change
@@ -1,46 +1,21 @@
// Copyright (c) Microsoft. All rights reserved.

import {
Button,
ButtonProps,
Dialog,
DialogActions,
DialogBody,
DialogContent,
DialogOpenChangeData,
DialogOpenChangeEvent,
DialogSurface,
DialogTitle,
DialogTrigger,
ToolbarButton,
Tooltip,
} from '@fluentui/react-components';
import { Button, ButtonProps, ToolbarButton, Tooltip } from '@fluentui/react-components';
import React from 'react';
import { DialogControl, DialogControlContent } from './DialogControl';

type CommandButtonProps = ButtonProps & {
trigger?: React.ReactElement;
label?: string;
description?: string;
onClick?: () => void;
dialogContent?: {
title: string;
content: React.ReactNode;
closeLabel?: string;
additionalActions?: React.ReactElement[];
onOpenChange?: (event: DialogOpenChangeEvent, data: DialogOpenChangeData) => void;
};
dialogContent?: DialogControlContent;
iconOnly?: boolean;
asToolbarButton?: boolean;
classNames?: {
dialogSurface?: string;
dialogContent?: string;
};
};

export const CommandButton: React.FC<CommandButtonProps> = (props) => {
const {
as,
trigger,
disabled,
icon,
label,
Expand All @@ -51,20 +26,19 @@ export const CommandButton: React.FC<CommandButtonProps> = (props) => {
asToolbarButton,
appearance,
size,
classNames,
} = props;

let commandButton = null;

if (trigger && dialogContent) {
if (dialogContent?.trigger) {
if (description) {
commandButton = (
<Tooltip content={description} relationship="label">
{trigger}
{dialogContent.trigger}
</Tooltip>
);
} else {
commandButton = trigger;
commandButton = dialogContent.trigger;
}
} else if (iconOnly) {
if (description) {
Expand Down Expand Up @@ -102,22 +76,14 @@ export const CommandButton: React.FC<CommandButtonProps> = (props) => {
}

return (
<Dialog onOpenChange={dialogContent.onOpenChange}>
<DialogTrigger>{commandButton}</DialogTrigger>
<DialogSurface className={classNames?.dialogSurface}>
<DialogBody>
<DialogTitle>{dialogContent?.title}</DialogTitle>
<DialogContent className={classNames?.dialogContent}>{dialogContent?.content}</DialogContent>
<DialogActions>
<DialogTrigger>
<Button>{dialogContent.closeLabel ?? 'Close'}</Button>
</DialogTrigger>
{dialogContent?.additionalActions?.map((action, index) => (
<DialogTrigger key={index}>{action}</DialogTrigger>
))}
</DialogActions>
</DialogBody>
</DialogSurface>
</Dialog>
<DialogControl
trigger={commandButton}
classNames={dialogContent.classNames}
title={dialogContent.title}
content={dialogContent.content}
closeLabel={dialogContent.closeLabel}
additionalActions={dialogContent.additionalActions}
onOpenChange={dialogContent.onOpenChange}
/>
);
};
25 changes: 14 additions & 11 deletions workbench-app/src/components/App/ContentExport.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,24 +15,27 @@ interface ContentExportProps {
export const ContentExport: React.FC<ContentExportProps> = (props) => {
const { id, contentTypeLabel, exportFunction, iconOnly, asToolbarButton } = props;

const exportContent = async () => {
const { blob, filename } = await exportFunction(id);
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
};

return (
<CommandButton
description={`Export ${contentTypeLabel}`}
icon={<ArrowDownload24Regular />}
iconOnly={iconOnly}
asToolbarButton={asToolbarButton}
label="Export"
onClick={exportContent}
onClick={() => exportContent(id, exportFunction)}
/>
);
};

export const exportContent = async (
id: string,
exportFunction: (id: string) => Promise<{ blob: Blob; filename: string }>,
) => {
const { blob, filename } = await exportFunction(id);
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
};
55 changes: 55 additions & 0 deletions workbench-app/src/components/App/DialogControl.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import {
Button,
Dialog,
DialogActions,
DialogBody,
DialogContent,
DialogOpenChangeData,
DialogOpenChangeEvent,
DialogSurface,
DialogTitle,
DialogTrigger,
} from '@fluentui/react-components';
import React from 'react';

export interface DialogControlContent {
open?: boolean;
defaultOpen?: boolean;
trigger?: React.ReactElement;
classNames?: {
dialogSurface?: string;
dialogContent?: string;
};
title?: string;
content?: React.ReactNode;
closeLabel?: string;
additionalActions?: React.ReactElement[];
onOpenChange?: (event: DialogOpenChangeEvent, data: DialogOpenChangeData) => void;
}

export const DialogControl: React.FC<DialogControlContent> = (props) => {
const { open, defaultOpen, trigger, classNames, title, content, closeLabel, additionalActions, onOpenChange } =
props;

return (
<Dialog open={open} defaultOpen={defaultOpen} onOpenChange={onOpenChange}>
<DialogTrigger>{trigger}</DialogTrigger>
<DialogSurface className={classNames?.dialogSurface}>
<DialogBody>
{title && <DialogTitle>{title}</DialogTitle>}
{content && <DialogContent className={classNames?.dialogContent}>{content}</DialogContent>}
<DialogActions>
<DialogTrigger>
<Button appearance={additionalActions ? 'secondary' : 'primary'}>
{closeLabel ?? 'Close'}
</Button>
</DialogTrigger>
{additionalActions?.map((action, index) => (
<DialogTrigger key={index}>{action}</DialogTrigger>
))}
</DialogActions>
</DialogBody>
</DialogSurface>
</Dialog>
);
};
Loading

0 comments on commit 1cb0de6

Please sign in to comment.