diff --git a/webapi/CopilotChat/Skills/ChatSkills/ExternalInformationSkill.cs b/webapi/CopilotChat/Skills/ChatSkills/ExternalInformationSkill.cs index eb95bc66a..38e0a2836 100644 --- a/webapi/CopilotChat/Skills/ChatSkills/ExternalInformationSkill.cs +++ b/webapi/CopilotChat/Skills/ChatSkills/ExternalInformationSkill.cs @@ -122,7 +122,7 @@ public async Task 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) { diff --git a/webapp/src/components/chat/plan-viewer/PlanJsonViewer.tsx b/webapp/src/components/chat/plan-viewer/PlanJsonViewer.tsx new file mode 100644 index 000000000..49613f085 --- /dev/null +++ b/webapp/src/components/chat/plan-viewer/PlanJsonViewer.tsx @@ -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 = ({ goal, json }) => { + return ( + + + {goal} + + + + Plan in JSON format + +
+                            {JSON.stringify(JSON.parse(json), null, 2)}
+                        
+
+ + + + + +
+
+
+ ); +}; diff --git a/webapp/src/components/chat/plan-viewer/PlanViewer.tsx b/webapp/src/components/chat/plan-viewer/PlanViewer.tsx index c47b4a6b5..e03990865 100644 --- a/webapp/src/components/chat/plan-viewer/PlanViewer.tsx +++ b/webapp/src/components/chat/plan-viewer/PlanViewer.tsx @@ -119,7 +119,7 @@ export const PlanViewer: React.FC = ({ message, messageIndex, g await getResponse({ value: planState === PlanState.PlanApproved ? 'Yes, proceed' : 'No, cancel', contextVariables, - messageType: ChatMessageType.Plan, + messageType: ChatMessageType.Message, chatId: selectedId, }); }; diff --git a/webapp/src/components/chat/tabs/PlansTab.tsx b/webapp/src/components/chat/tabs/PlansTab.tsx index 3ea7ea4d3..fdb58304c 100644 --- a/webapp/src/components/chat/tabs/PlansTab.tsx +++ b/webapp/src/components/chat/tabs/PlansTab.tsx @@ -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 ( - + + + + {columns.map((column) => column.renderHeaderCell())} + + + {rows.map((item) => ( + {columns.map((column) => column.renderCell(item))} + ))} + +
+
); }; + +function useTable(planMessages: IChatMessage[]) { + const headerSortProps = (columnId: TableColumnId): TableHeaderCellProps => ({ + onClick: (e: React.MouseEvent) => { + toggleColumnSort(e, columnId); + }, + sortDirection: getSortDirection(columnId), + }); + + const columns: Array> = [ + createTableColumn({ + columnId: 'ask', + renderHeaderCell: () => ( + + Ask + + ), + renderCell: (item) => ( + + + + + + ), + compare: (a, b) => { + const comparison = a.ask.localeCompare(b.ask); + return getSortDirection('name') === 'ascending' ? comparison : comparison * -1; + }, + }), + createTableColumn({ + columnId: 'createdOn', + renderHeaderCell: () => ( + + Created on + + ), + renderCell: (item) => ( + + {item.createdOn.label} + + ), + compare: (a, b) => { + const comparison = a.createdOn.timestamp > b.createdOn.timestamp ? 1 : -1; + return getSortDirection('createdOn') === 'ascending' ? comparison : comparison * -1; + }, + }), + createTableColumn({ + columnId: 'tokenCounts', + renderHeaderCell: () => ( + + Token Count + + ), + renderCell: (item) => {item.tokens}, + 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 }; +}