diff --git a/assistants/explorer-assistant/assistant/config.py b/assistants/explorer-assistant/assistant/config.py index 2c543b81..cdebd360 100644 --- a/assistants/explorer-assistant/assistant/config.py +++ b/assistants/explorer-assistant/assistant/config.py @@ -91,7 +91,7 @@ class RequestConfig(BaseModel): ), ), UISchema(enable_markdown_in_description=True), - ] = 50_000 + ] = 128_000 response_tokens: Annotated[ int, diff --git a/workbench-app/src/components/Assistants/AssistantConfigure.tsx b/workbench-app/src/components/Assistants/AssistantConfigure.tsx index 0bb92a54..63b9b1a9 100644 --- a/workbench-app/src/components/Assistants/AssistantConfigure.tsx +++ b/workbench-app/src/components/Assistants/AssistantConfigure.tsx @@ -1,10 +1,9 @@ // Copyright (c) Microsoft. All rights reserved. -import { Button, makeStyles, tokens } from '@fluentui/react-components'; -import { SettingsRegular } from '@fluentui/react-icons'; +import { DialogOpenChangeData, DialogOpenChangeEvent, makeStyles, tokens } from '@fluentui/react-components'; import React from 'react'; import { Assistant } from '../../models/Assistant'; -import { CommandButton } from '../App/CommandButton'; +import { DialogControl } from '../App/DialogControl'; import { AssistantConfiguration } from './AssistantConfiguration'; const useClasses = makeStyles({ @@ -27,55 +26,50 @@ const useClasses = makeStyles({ }, }); -interface AssistantConfigureProps { - assistant: Assistant; - iconOnly?: boolean; - disabled?: boolean; - simulateMenuItem?: boolean; +interface AssistantConfigureDialogProps { + assistant?: Assistant; + open: boolean; + onOpenChange: (event: DialogOpenChangeEvent, data: DialogOpenChangeData) => void; } -export const AssistantConfigure: React.FC = (props) => { - const { assistant, iconOnly, disabled, simulateMenuItem } = props; +export const AssistantConfigureDialog: React.FC = (props) => { + const { assistant, open, onOpenChange } = props; const classes = useClasses(); - const [open, setOpen] = React.useState(false); const [isDirty, setIsDirty] = React.useState(false); - const handleClose = React.useCallback(() => { - if (isDirty) { - const result = window.confirm('Are you sure you want to close without saving?'); - if (!result) { + const handleOpenChange = React.useCallback( + (event: DialogOpenChangeEvent, data: DialogOpenChangeData) => { + if (data.open) { + setIsDirty(false); return; } - } - setOpen(false); - }, [isDirty]); + + if (isDirty) { + const result = window.confirm('Are you sure you want to close without saving?'); + if (!result) { + return; + } + } + + setIsDirty(false); + onOpenChange(event, data); + }, + [isDirty, onOpenChange], + ); return ( - setOpen(true)} - icon={} - simulateMenuItem={simulateMenuItem} - label="Configure" - iconOnly={iconOnly} - disabled={disabled} - dialogContent={{ - title: `Configure "${assistant.name}"`, - content: ( -
- -
- ), - hideDismissButton: true, - classNames: { - dialogSurface: classes.dialogSurface, - dialogContent: classes.dialogContent, - }, - additionalActions: [ - , - ], + onOpenChange={handleOpenChange} + title={assistant && `Configure "${assistant.name}"`} + content={ +
+ {assistant && } +
+ } + classNames={{ + dialogSurface: classes.dialogSurface, + dialogContent: classes.dialogContent, }} /> ); diff --git a/workbench-app/src/components/Assistants/AssistantRemove.tsx b/workbench-app/src/components/Assistants/AssistantRemove.tsx index de610de4..b8b66982 100644 --- a/workbench-app/src/components/Assistants/AssistantRemove.tsx +++ b/workbench-app/src/components/Assistants/AssistantRemove.tsx @@ -1,87 +1,165 @@ // Copyright (c) Microsoft. All rights reserved. -import { DialogTrigger } from '@fluentui/react-components'; +import { Button, DialogOpenChangeData, DialogOpenChangeEvent } from '@fluentui/react-components'; import { PlugDisconnectedRegular } from '@fluentui/react-icons'; import React from 'react'; -import { Conversation } from '../../models/Conversation'; -import { ConversationParticipant } from '../../models/ConversationParticipant'; +import { Utility } from '../../libs/Utility'; +import { useNotify } from '../../libs/useNotify'; +import { Assistant } from '../../models/Assistant'; import { useCreateConversationMessageMutation, + useGetConversationParticipantsQuery, useRemoveConversationParticipantMutation, } from '../../services/workbench'; import { CommandButton } from '../App/CommandButton'; +import { DialogControl } from '../App/DialogControl'; -interface AssistantRemoveProps { - participant: ConversationParticipant; - conversation: Conversation; - iconOnly?: boolean; - disabled?: boolean; - simulateMenuItem?: boolean; -} - -export const AssistantRemove: React.FC = (props) => { - const { participant, conversation, iconOnly, disabled, simulateMenuItem } = props; - const [removeConversationParticipant] = useRemoveConversationParticipantMutation(); +const useAssistantRemoveControls = (assistant?: Assistant, conversationId?: string) => { const [createConversationMessage] = useCreateConversationMessageMutation(); + const [removeConversationParticipant] = useRemoveConversationParticipantMutation(); + const { refetch: refetchParticipants } = useGetConversationParticipantsQuery(conversationId ?? '', { + skip: !conversationId, + }); const [submitted, setSubmitted] = React.useState(false); - const handleAssistantRemove = React.useCallback(async () => { - if (submitted) { - return; - } - setSubmitted(true); + const handleRemove = React.useCallback( + async (onRemove?: (assistant: Assistant) => Promise, onError?: (error: Error) => void) => { + if (!assistant || !conversationId || submitted) { + return; + } - try { - await removeConversationParticipant({ - conversationId: conversation.id, - participantId: participant.id, - }); + try { + await Utility.withStatus(setSubmitted, async () => { + await removeConversationParticipant({ + conversationId: conversationId, + participantId: assistant.id, + }); + await onRemove?.(assistant); - const content = `${participant.name} removed from conversation`; + const content = `${assistant.name} removed from conversation`; + await createConversationMessage({ + conversationId: conversationId, + content, + messageType: 'notice', + }); - await createConversationMessage({ - conversationId: conversation.id, - content, - messageType: 'notice', + // Refetch participants to update the assistant name in the list + if (conversationId) { + await refetchParticipants(); + } + }); + } catch (error) { + onError?.(error as Error); + } + }, + [ + assistant, + conversationId, + createConversationMessage, + refetchParticipants, + removeConversationParticipant, + submitted, + ], + ); + + const removeAssistantForm = React.useCallback( + (onRemove?: (assistant: Assistant) => Promise) => ( +
{ + event.preventDefault(); + handleRemove(onRemove); + }} + > +

+ Are you sure you want to remove assistant {assistant?.name} from this conversation? +

+
+ ), + [assistant?.name, handleRemove], + ); + + const removeAssistantButton = React.useCallback( + (onRemove?: (assistant: Assistant) => Promise, onError?: (error: Error) => void) => ( + + ), + [handleRemove, submitted], + ); + + return { + removeAssistantForm, + removeAssistantButton, + }; +}; + +interface AssistantRemoveDialogProps { + assistant?: Assistant; + conversationId?: string; + onRemove?: (assistant: Assistant) => Promise; + open: boolean; + onOpenChange: (event: DialogOpenChangeEvent, data: DialogOpenChangeData) => void; +} + +export const AssistantRemoveDialog: React.FC = (props) => { + const { assistant, conversationId, open, onOpenChange, onRemove } = props; + const { removeAssistantForm, removeAssistantButton } = useAssistantRemoveControls(assistant, conversationId); + const { notifyWarning } = useNotify(); + + const handleError = React.useCallback( + (error: Error) => { + notifyWarning({ + id: 'assistant-remove-error', + title: 'Remove assistant failed', + message: error.message, }); - } finally { - setSubmitted(false); - } - }, [ - conversation.id, - createConversationMessage, - participant.id, - participant.name, - removeConversationParticipant, - submitted, - ]); + }, + [notifyWarning], + ); + + return ( + + ); +}; + +interface AssistantRemoveProps { + assistant: Assistant; + conversationId: string; + disabled?: boolean; + onRemove?: (assistant: Assistant) => Promise; + iconOnly?: boolean; + asToolbarButton?: boolean; +} + +export const AssistantRemove: React.FC = (props) => { + const { assistant, conversationId, disabled, onRemove, iconOnly, asToolbarButton } = props; + const { removeAssistantForm, removeAssistantButton } = useAssistantRemoveControls(assistant, conversationId); return ( } - simulateMenuItem={simulateMenuItem} label="Remove" - iconOnly={iconOnly} disabled={disabled} + description="Remove assistant" + asToolbarButton={asToolbarButton} dialogContent={{ - title: `Remove "${participant.name}"`, - content: ( -

- Are you sure you want to remove assistant {participant.name} from this - conversation? -

- ), + title: `Remove "${assistant.name}"`, + content: removeAssistantForm(onRemove), closeLabel: 'Cancel', - additionalActions: [ - - } - disabled={submitted} - label={submitted ? 'Removing...' : 'Remove'} - onClick={handleAssistantRemove} - /> - , - ], + additionalActions: [removeAssistantButton(onRemove)], }} /> ); diff --git a/workbench-app/src/components/Assistants/AssistantRename.tsx b/workbench-app/src/components/Assistants/AssistantRename.tsx index 65964355..b382a379 100644 --- a/workbench-app/src/components/Assistants/AssistantRename.tsx +++ b/workbench-app/src/components/Assistants/AssistantRename.tsx @@ -1,79 +1,145 @@ // Copyright (c) Microsoft. All rights reserved. -import { Button, DialogTrigger, Field, Input } from '@fluentui/react-components'; +import { Button, DialogOpenChangeData, DialogOpenChangeEvent, Field, Input } from '@fluentui/react-components'; import { EditRegular } from '@fluentui/react-icons'; import React from 'react'; +import { Utility } from '../../libs/Utility'; +import { useNotify } from '../../libs/useNotify'; import { Assistant } from '../../models/Assistant'; import { useGetConversationParticipantsQuery, useUpdateAssistantMutation } from '../../services/workbench'; import { CommandButton } from '../App/CommandButton'; +import { DialogControl } from '../App/DialogControl'; -interface AssistantRenameProps { - assistant: Assistant; - conversationId?: string; - iconOnly?: boolean; - simulateMenuItem?: boolean; - onRename?: (value: string) => Promise; -} - -export const AssistantRename: React.FC = (props) => { - const { assistant, conversationId, iconOnly, simulateMenuItem, onRename } = props; - const [name, setName] = React.useState(assistant.name); - const [submitted, setSubmitted] = React.useState(false); - const [open, setOpen] = React.useState(false); +const useAssistantRenameControls = (assistant?: Assistant, conversationId?: string) => { const [updateAssistant] = useUpdateAssistantMutation(); const { refetch: refetchParticipants } = useGetConversationParticipantsQuery(conversationId ?? '', { skip: !conversationId, }); + const [newName, setNewName] = React.useState(); + const [submitted, setSubmitted] = React.useState(false); + + const handleRename = React.useCallback( + async (onRename?: (id: string, value: string) => Promise, onError?: (error: Error) => void) => { + if (!assistant || !newName || submitted) { + return; + } + + try { + await Utility.withStatus(setSubmitted, async () => { + await updateAssistant({ ...assistant, name: newName }); + await onRename?.(assistant.id, newName); + + // Refetch participants to update the assistant name in the list + if (conversationId) { + await refetchParticipants(); + } + }); + } catch (error) { + onError?.(error as Error); + } + }, + [assistant, conversationId, newName, refetchParticipants, submitted, updateAssistant], + ); + + const renameAssistantForm = React.useCallback( + (onRename?: (id: string, value: string) => Promise) => ( +
{ + event.preventDefault(); + handleRename(onRename); + }} + > + + setNewName(data.value)} + /> + +
+ ), + [assistant?.name, handleRename, submitted], + ); + + const renameAssistantButton = React.useCallback( + (onRename?: (id: string, value: string) => Promise, onError?: (error: Error) => void) => ( + + ), + [handleRename, newName, submitted], + ); + + return { renameAssistantForm, renameAssistantButton }; +}; + +interface AssistantRenameDialogProps { + assistant?: Assistant; + conversationId?: string; + onRename?: (value: string) => Promise; + open?: boolean; + onOpenChange?: (event: DialogOpenChangeEvent, data: DialogOpenChangeData) => void; +} - const handleRename = React.useCallback(async () => { - if (submitted) { - return; - } - setSubmitted(true); - await updateAssistant({ ...assistant, name }); +export const AssistantRenameDialog: React.FC = (props) => { + const { assistant, conversationId, onRename, open, onOpenChange } = props; + const { renameAssistantForm, renameAssistantButton } = useAssistantRenameControls(assistant, conversationId); + const { notifyWarning } = useNotify(); - // Refetch participants to update the assistant name in the list - if (conversationId) { - await refetchParticipants(); - } + const handleError = React.useCallback( + (error: Error) => { + notifyWarning({ + id: 'assistant-rename-error', + title: 'Rename assistant failed', + message: error.message, + }); + }, + [notifyWarning], + ); - await onRename?.(name); - setOpen(false); - setSubmitted(false); - }, [assistant, conversationId, name, onRename, refetchParticipants, submitted, updateAssistant]); + return ( + + ); +}; + +interface AssistantRenameProps { + assistant: Assistant; + conversationId?: string; + disabled?: boolean; + onRename?: (value: string) => Promise; + iconOnly?: boolean; + asToolbarButton?: boolean; +} + +export const AssistantRename: React.FC = (props) => { + const { assistant, conversationId, disabled, onRename, iconOnly, asToolbarButton } = props; + const { renameAssistantForm, renameAssistantButton } = useAssistantRenameControls(assistant, conversationId); return ( setOpen(true)} + iconOnly={iconOnly} icon={} label="Rename" - iconOnly={iconOnly} - simulateMenuItem={simulateMenuItem} + disabled={disabled} description="Rename assistant" + asToolbarButton={asToolbarButton} dialogContent={{ - title: 'Rename Assistant', - content: ( -
{ - event.preventDefault(); - handleRename(); - }} - > - - setName(data.value)} /> - -
- , - ], + additionalActions: [renameAssistantButton(onRename)], }} /> ); diff --git a/workbench-app/src/components/Assistants/AssistantServiceInfo.tsx b/workbench-app/src/components/Assistants/AssistantServiceInfo.tsx index 8d5c49b7..15586fbe 100644 --- a/workbench-app/src/components/Assistants/AssistantServiceInfo.tsx +++ b/workbench-app/src/components/Assistants/AssistantServiceInfo.tsx @@ -1,34 +1,27 @@ // Copyright (c) Microsoft. All rights reserved. -import { DatabaseRegular } from '@fluentui/react-icons'; +import { DialogOpenChangeData, DialogOpenChangeEvent } from '@fluentui/react-components'; import React from 'react'; import { Assistant } from '../../models/Assistant'; -import { CommandButton } from '../App/CommandButton'; +import { DialogControl } from '../App/DialogControl'; import { AssistantServiceMetadata } from './AssistantServiceMetadata'; -interface AssistantServiceInfoProps { - assistant: Assistant; - iconOnly?: boolean; - disabled?: boolean; - simulateMenuItem?: boolean; +interface AssistantServiceInfoDialogProps { + assistant?: Assistant; + open: boolean; + onOpenChange: (event: DialogOpenChangeEvent, data: DialogOpenChangeData) => void; } -export const AssistantServiceInfo: React.FC = (props) => { - const { assistant, iconOnly, disabled, simulateMenuItem } = props; +export const AssistantServiceInfoDialog: React.FC = (props) => { + const { assistant, open, onOpenChange } = props; return ( - } - simulateMenuItem={simulateMenuItem} - label="Service Info" - iconOnly={iconOnly} - description="View assistant service info" - disabled={disabled} - dialogContent={{ - title: `"${assistant.name}" Service Info`, - content: , - closeLabel: 'Close', - }} + } + closeLabel="Close" /> ); }; diff --git a/workbench-app/src/components/Conversations/ConversationRename.tsx b/workbench-app/src/components/Conversations/ConversationRename.tsx index 46151a58..2ede476f 100644 --- a/workbench-app/src/components/Conversations/ConversationRename.tsx +++ b/workbench-app/src/components/Conversations/ConversationRename.tsx @@ -80,7 +80,7 @@ export const ConversationRenameDialog: React.FC = const handleError = React.useCallback( (error: Error) => { notifyWarning({ - id: 'error', + id: 'conversation-rename-error', title: 'Rename conversation failed', message: error.message, }); diff --git a/workbench-app/src/components/Conversations/ParticipantItem.tsx b/workbench-app/src/components/Conversations/ParticipantItem.tsx index ecc45c93..a02928ee 100644 --- a/workbench-app/src/components/Conversations/ParticipantItem.tsx +++ b/workbench-app/src/components/Conversations/ParticipantItem.tsx @@ -3,6 +3,7 @@ import { Button, Menu, + MenuItem, MenuList, MenuPopover, MenuTrigger, @@ -10,16 +11,19 @@ import { makeStyles, tokens, } from '@fluentui/react-components'; -import { MoreHorizontalRegular } from '@fluentui/react-icons'; +import { + DatabaseRegular, + EditRegular, + MoreHorizontalRegular, + PlugDisconnectedRegular, + SettingsRegular, +} from '@fluentui/react-icons'; import React from 'react'; import { useParticipantUtility } from '../../libs/useParticipantUtility'; +import { Assistant } from '../../models/Assistant'; import { Conversation } from '../../models/Conversation'; import { ConversationParticipant } from '../../models/ConversationParticipant'; import { useGetAssistantQuery } from '../../services/workbench'; -import { AssistantConfigure } from '../Assistants/AssistantConfigure'; -import { AssistantRemove } from '../Assistants/AssistantRemove'; -import { AssistantRename } from '../Assistants/AssistantRename'; -import { AssistantServiceInfo } from '../Assistants/AssistantServiceInfo'; const useClasses = makeStyles({ root: { @@ -44,11 +48,14 @@ interface ParticipantItemProps { conversation: Conversation; participant: ConversationParticipant; readOnly?: boolean; - preventAssistantModifyOnParticipantIds?: string[]; + onConfigure?: (assistant: Assistant) => void; + onRename?: (assistant: Assistant) => void; + onServiceInfo?: (assistant: Assistant) => void; + onRemove?: (assistant: Assistant) => void; } export const ParticipantItem: React.FC = (props) => { - const { conversation, participant, readOnly, preventAssistantModifyOnParticipantIds } = props; + const { conversation, participant, readOnly, onConfigure, onRename, onServiceInfo, onRemove } = props; const classes = useClasses(); const { getAvatarData } = useParticipantUtility(); @@ -61,8 +68,16 @@ export const ParticipantItem: React.FC = (props) => { throw new Error(`Error loading assistant (${participant.id}): ${errorMessage}`); } + const handleMenuItemClick = React.useCallback( + (event: React.MouseEvent, handler?: (conversation: Conversation) => void) => { + event.stopPropagation(); + handler?.(conversation); + }, + [conversation], + ); + const assistantActions = React.useMemo(() => { - if (participant.role !== 'assistant') { + if (participant.role !== 'assistant' || !assistant || readOnly) { return null; } @@ -73,34 +88,44 @@ export const ParticipantItem: React.FC = (props) => { - {assistant && ( - <> - - - - + {/* FIXME: complete the menu items */} + {onConfigure && ( + } + onClick={(event) => handleMenuItemClick(event, () => onConfigure(assistant))} + > + Configure + + )} + {onRename && ( + } + onClick={(event) => handleMenuItemClick(event, () => onRename(assistant))} + > + Rename + + )} + {onServiceInfo && ( + } + onClick={(event) => handleMenuItemClick(event, () => onServiceInfo(assistant))} + > + Service Info + + )} + {onRemove && ( + } + onClick={(event) => handleMenuItemClick(event, () => onRemove(assistant))} + > + Remove + )} - ); - }, [assistant, conversation, participant, preventAssistantModifyOnParticipantIds, readOnly]); + }, [participant.role, assistant, readOnly, onConfigure, onRename, onServiceInfo, onRemove, handleMenuItemClick]); return (
diff --git a/workbench-app/src/components/Conversations/ParticipantList.tsx b/workbench-app/src/components/Conversations/ParticipantList.tsx index be881156..e49c6466 100644 --- a/workbench-app/src/components/Conversations/ParticipantList.tsx +++ b/workbench-app/src/components/Conversations/ParticipantList.tsx @@ -8,6 +8,10 @@ import { Conversation } from '../../models/Conversation'; import { ConversationParticipant } from '../../models/ConversationParticipant'; import { useAddConversationParticipantMutation, useCreateConversationMessageMutation } from '../../services/workbench'; import { AssistantAdd } from '../Assistants/AssistantAdd'; +import { AssistantConfigureDialog } from '../Assistants/AssistantConfigure'; +import { AssistantRemoveDialog } from '../Assistants/AssistantRemove'; +import { AssistantRenameDialog } from '../Assistants/AssistantRename'; +import { AssistantServiceInfoDialog } from '../Assistants/AssistantServiceInfo'; import { ParticipantItem } from './ParticipantItem'; const useClasses = makeStyles({ @@ -45,6 +49,11 @@ export const ParticipantList: React.FC = (props) => { const [addConversationParticipant] = useAddConversationParticipantMutation(); const [createConversationMessage] = useCreateConversationMessageMutation(); + const [configureAssistant, setConfigureAssistant] = React.useState(); + const [renameAssistant, setRenameAssistant] = React.useState(); + const [serviceInfoAssistant, setServiceInfoAssistant] = React.useState(); + const [removeAssistant, setRemoveAssistant] = React.useState(); + const handleAssistantAdd = async (assistant: Assistant) => { // send notice message first, to announce before assistant reacts to create event await createConversationMessage({ @@ -59,20 +68,56 @@ export const ParticipantList: React.FC = (props) => { }); }; + const actionHelpers = React.useMemo( + () => ( + <> + setConfigureAssistant(undefined)} + /> + setRenameAssistant(undefined)} + onRename={async () => setRenameAssistant(undefined)} + /> + setServiceInfoAssistant(undefined)} + /> + setRemoveAssistant(undefined)} + onRemove={async () => setRemoveAssistant(undefined)} + /> + + ), + [configureAssistant, conversation.id, removeAssistant, renameAssistant, serviceInfoAssistant], + ); + const exceptAssistantIds = participants .filter((participant) => participant.active && participant.role === 'assistant') .map((participant) => participant.id); return (
+ {actionHelpers} {sortParticipants(participants).map((participant) => ( setConfigureAssistant(assistant)} + onRename={(assistant) => setRenameAssistant(assistant)} + onServiceInfo={(assistant) => setServiceInfoAssistant(assistant)} + onRemove={(assistant) => setRemoveAssistant(assistant)} /> ))}