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

Add plans tab #59

Merged
merged 4 commits into from
Jul 28, 2023
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
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ public async Task<string> AcquireExternalInformationAsync(
{
// Create a plan and set it in context for approval.
var contextString = string.Join("\n", context.Variables.Where(v => v.Key != "userIntent").Select(v => $"{v.Key}: {v.Value}"));
Plan plan = await this._planner.CreatePlanAsync($"Given the following context, accomplish the user intent.\nContext:{contextString}\nUser Intent:{userIntent}");
Plan plan = await this._planner.CreatePlanAsync($"Given the following context, accomplish the user intent.\nContext:\n{contextString}\nUser Intent:{userIntent}");

if (plan.Steps.Count > 0)
{
Expand Down
44 changes: 44 additions & 0 deletions webapp/src/components/chat/plan-viewer/PlanJsonViewer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Copyright (c) Microsoft. All rights reserved.

import {
Button,
Dialog,
DialogActions,
DialogBody,
DialogContent,
DialogSurface,
DialogTitle,
DialogTrigger,
Link,
} from '@fluentui/react-components';
import React from 'react';

interface IPlanJsonViewerProps {
goal: string;
json: string;
}

export const PlanJsonViewer: React.FC<IPlanJsonViewerProps> = ({ goal, json }) => {
return (
<Dialog>
<DialogTrigger disableButtonEnhancement>
<Link>{goal}</Link>
</DialogTrigger>
<DialogSurface>
<DialogBody>
<DialogTitle>Plan in JSON format</DialogTitle>
<DialogContent>
<pre>
<code>{JSON.stringify(JSON.parse(json), null, 2)}</code>
</pre>
</DialogContent>
<DialogActions>
<DialogTrigger disableButtonEnhancement>
<Button appearance="secondary">Close</Button>
</DialogTrigger>
</DialogActions>
</DialogBody>
</DialogSurface>
</Dialog>
);
};
2 changes: 1 addition & 1 deletion webapp/src/components/chat/plan-viewer/PlanViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ export const PlanViewer: React.FC<PlanViewerProps> = ({ message, messageIndex, g
await getResponse({
value: planState === PlanState.PlanApproved ? 'Yes, proceed' : 'No, cancel',
contextVariables,
messageType: ChatMessageType.Plan,
messageType: ChatMessageType.Message,
chatId: selectedId,
});
};
Expand Down
177 changes: 172 additions & 5 deletions webapp/src/components/chat/tabs/PlansTab.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,180 @@
// Copyright (c) Microsoft. All rights reserved.

import {
Table,
TableBody,
TableCell,
TableCellLayout,
TableColumnDefinition,
TableColumnId,
TableHeader,
TableHeaderCell,
TableHeaderCellProps,
TableRow,
createTableColumn,
makeStyles,
tokens,
useTableFeatures,
useTableSort,
} from '@fluentui/react-components';
import { ChatMessageType, IChatMessage } from '../../../libs/models/ChatMessage';
import { useAppSelector } from '../../../redux/app/hooks';
import { RootState } from '../../../redux/app/store';
import { timestampToDateString } from '../../utils/TextUtils';
import { PlanJsonViewer } from '../plan-viewer/PlanJsonViewer';
import { TabView } from './TabView';

const useClasses = makeStyles({
table: {
backgroundColor: tokens.colorNeutralBackground1,
},
tableHeader: {
fontWeight: tokens.fontSizeBase600,
},
});

interface TableItem {
index: number;
ask: string;
createdOn: {
label: string;
timestamp: number;
};
tokens: number;
message: IChatMessage;
}

export const PlansTab: React.FC = () => {
const classes = useClasses();

const { conversations, selectedId } = useAppSelector((state: RootState) => state.conversations);
const chatMessages = conversations[selectedId].messages;
const planMessages = chatMessages.filter((message) => message.type === ChatMessageType.Plan);

const { columns, rows } = useTable(planMessages);
return (
<TabView
title="Plans"
learnMoreDescription="custom plans"
learnMoreLink="https://aka.ms/sk-docs-planner"
></TabView>
<TabView title="Plans" learnMoreDescription="custom plans" learnMoreLink="https://aka.ms/sk-docs-planner">
<Table aria-label="Processes plan table" className={classes.table}>
<TableHeader>
<TableRow>{columns.map((column) => column.renderHeaderCell())}</TableRow>
</TableHeader>
<TableBody>
{rows.map((item) => (
<TableRow key={item.ask}>{columns.map((column) => column.renderCell(item))}</TableRow>
))}
</TableBody>
</Table>
</TabView>
);
};

function useTable(planMessages: IChatMessage[]) {
const headerSortProps = (columnId: TableColumnId): TableHeaderCellProps => ({
onClick: (e: React.MouseEvent) => {
toggleColumnSort(e, columnId);
},
sortDirection: getSortDirection(columnId),
});

const columns: Array<TableColumnDefinition<TableItem>> = [
createTableColumn<TableItem>({
columnId: 'ask',
renderHeaderCell: () => (
<TableHeaderCell key="ask" {...headerSortProps('ask')}>
Ask
</TableHeaderCell>
),
renderCell: (item) => (
<TableCell key={`plan-${item.index}`}>
<TableCellLayout>
<PlanJsonViewer goal={item.ask} json={item.message.content} />
</TableCellLayout>
</TableCell>
),
compare: (a, b) => {
const comparison = a.ask.localeCompare(b.ask);
return getSortDirection('name') === 'ascending' ? comparison : comparison * -1;
},
}),
createTableColumn<TableItem>({
columnId: 'createdOn',
renderHeaderCell: () => (
<TableHeaderCell key="createdOn" {...headerSortProps('createdOn')}>
Created on
</TableHeaderCell>
),
renderCell: (item) => (
<TableCell key={item.createdOn.timestamp} title={new Date(item.createdOn.timestamp).toLocaleString()}>
{item.createdOn.label}
</TableCell>
),
compare: (a, b) => {
const comparison = a.createdOn.timestamp > b.createdOn.timestamp ? 1 : -1;
return getSortDirection('createdOn') === 'ascending' ? comparison : comparison * -1;
},
}),
createTableColumn<TableItem>({
columnId: 'tokenCounts',
renderHeaderCell: () => (
<TableHeaderCell key="tokenCounts" {...headerSortProps('tokenCounts')}>
Token Count
</TableHeaderCell>
),
renderCell: (item) => <TableCell key={`plan-${item.index}-tokens`}>{item.tokens}</TableCell>,
compare: (a, b) => {
const comparison = a.tokens - b.tokens;
return getSortDirection('tokenCounts') === 'ascending' ? comparison : comparison * -1;
},
}),
];

// TODO: [Issue #63] Define a plan model
/*
eslint-disable
@typescript-eslint/no-unsafe-assignment,
@typescript-eslint/no-unsafe-member-access,
*/
const items = planMessages.map((message, index) => {
const plan = JSON.parse(message.content);
const planDescription = plan.proposedPlan.description as string;
const planAsk =
planDescription
.split('\n')
.find((line: string) => line.startsWith('INPUT:'))
?.replace('INPUT:', '')
.trim() ?? 'N/A';
return {
index: index,
ask: planAsk,
createdOn: {
label: timestampToDateString(message.timestamp),
timestamp: message.timestamp,
},
tokens: 0, // TODO: [Issue #2106] Get token count from plan
message: message,
};
});

const {
sort: { getSortDirection, toggleColumnSort, sortColumn },
} = useTableFeatures(
{
columns,
items,
},
[
useTableSort({
defaultSortState: { sortColumn: 'createdOn', sortDirection: 'descending' },
}),
],
);

if (sortColumn) {
items.sort((a, b) => {
const compare = columns.find((column) => column.columnId === sortColumn)?.compare;
return compare?.(a, b) ?? 0;
});
}

return { columns, rows: items };
}