diff --git a/Composer/packages/client/src/components/AddRemoteSkillModal/Orchestractor.tsx b/Composer/packages/client/src/components/AddRemoteSkillModal/EnableOrchestrator.tsx similarity index 59% rename from Composer/packages/client/src/components/AddRemoteSkillModal/Orchestractor.tsx rename to Composer/packages/client/src/components/AddRemoteSkillModal/EnableOrchestrator.tsx index 1c2d137298..4925f50927 100644 --- a/Composer/packages/client/src/components/AddRemoteSkillModal/Orchestractor.tsx +++ b/Composer/packages/client/src/components/AddRemoteSkillModal/EnableOrchestrator.tsx @@ -1,62 +1,70 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -import React, { useState } from 'react'; -import formatMessage from 'format-message'; -import { Link } from 'office-ui-fabric-react/lib/Link'; -import { Checkbox } from 'office-ui-fabric-react/lib/Checkbox'; -import { Stack, StackItem } from 'office-ui-fabric-react/lib/Stack'; -import { PrimaryButton, DefaultButton } from 'office-ui-fabric-react/lib/Button'; -import { useRecoilValue } from 'recoil'; - -import { dispatcherState } from '../../recoilModel'; -import { enableOrchestratorDialog } from '../../constants'; - -import { importOrchestractor } from './helper'; - -const learnMoreUrl = 'https://aka.ms/bf-composer-docs-publish-bot'; - -export const Orchestractor = (props) => { - const { projectId, onSubmit, onBack } = props; - const [enableOrchestrator, setEnableOrchestrator] = useState(true); - const { setApplicationLevelError, reloadProject } = useRecoilValue(dispatcherState); - const onChange = (ev, check) => { - setEnableOrchestrator(check); - }; - return ( - - - - {enableOrchestratorDialog.content} - - {formatMessage('Learn more about Orchestractor')} - - - - - - - - - { - onSubmit(event, enableOrchestrator); - if (enableOrchestrator) { - // TODO. show notification - // download orchestrator first - importOrchestractor(projectId, reloadProject, setApplicationLevelError); - // TODO. update notification - } - }} - /> - - - - ); -}; +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import React, { useState } from 'react'; +import formatMessage from 'format-message'; +import { Link } from 'office-ui-fabric-react/lib/Link'; +import { Checkbox } from 'office-ui-fabric-react/lib/Checkbox'; +import { Stack, StackItem } from 'office-ui-fabric-react/lib/Stack'; +import { PrimaryButton, DefaultButton, Button } from 'office-ui-fabric-react/lib/Button'; +import { useRecoilValue } from 'recoil'; + +import { dispatcherState } from '../../recoilModel'; +import { enableOrchestratorDialog } from '../../constants'; + +import { importOrchestrator } from './helper'; + +const learnMoreUrl = 'https://aka.ms/LearnMoreOrchestrator'; + +type OrchestratorProps = { + projectId: string; + onSubmit: (event: React.MouseEvent, userSelected?: boolean) => Promise; + onBack?: (event: React.MouseEvent) => void; + hideBackButton?: boolean; +}; + +const EnableOrchestrator: React.FC = (props) => { + const { projectId, onSubmit, onBack, hideBackButton = false } = props; + const [enableOrchestrator, setEnableOrchestrator] = useState(true); + const { setApplicationLevelError, reloadProject } = useRecoilValue(dispatcherState); + const onChange = (ev, check) => { + setEnableOrchestrator(check); + }; + return ( + + + + {enableOrchestratorDialog.content} + + {formatMessage('Learn more about Orchestrator')} + + + + + + {!hideBackButton && } + + + { + onSubmit(event, enableOrchestrator); + if (enableOrchestrator) { + // TODO: Block UI from doing any work until import is complete. Item #7531 + importOrchestrator(projectId, reloadProject, setApplicationLevelError); + } + }} + /> + + + + ); +}; + +export { OrchestratorProps, EnableOrchestrator }; diff --git a/Composer/packages/client/src/components/AddRemoteSkillModal/SelectIntent.tsx b/Composer/packages/client/src/components/AddRemoteSkillModal/SelectIntent.tsx index 5299b9ccb8..7d0e86281c 100644 --- a/Composer/packages/client/src/components/AddRemoteSkillModal/SelectIntent.tsx +++ b/Composer/packages/client/src/components/AddRemoteSkillModal/SelectIntent.tsx @@ -22,7 +22,7 @@ import luWorker from '../../recoilModel/parsers/luWorker'; import { localeState, dispatcherState } from '../../recoilModel'; import { recognizersSelectorFamily } from '../../recoilModel/selectors/recognizers'; -import { Orchestractor } from './Orchestractor'; +import { EnableOrchestrator } from './EnableOrchestrator'; const detailListContainer = css` width: 100%; @@ -248,7 +248,7 @@ export const SelectIntent: React.FC = (props) => { return ( {showOrchestratorDialog ? ( - { onUpdateTitle(selectIntentDialog.ADD_OR_EDIT_PHRASE(dialogId, manifest.name)); @@ -313,10 +313,10 @@ export const SelectIntent: React.FC = (props) => { onClick={(ev) => { if (pageIndex === 1) { if (hasOrchestrator) { - // skip orchestractor modal + // skip orchestrator modal handleSubmit(ev, true); } else { - // show orchestractor + // show orchestrator onUpdateTitle(enableOrchestratorDialog); setShowOrchestratorDialog(true); } diff --git a/Composer/packages/client/src/components/AddRemoteSkillModal/helper.ts b/Composer/packages/client/src/components/AddRemoteSkillModal/helper.ts index 2e4f5787e7..dd6f6a8aeb 100644 --- a/Composer/packages/client/src/components/AddRemoteSkillModal/helper.ts +++ b/Composer/packages/client/src/components/AddRemoteSkillModal/helper.ts @@ -13,10 +13,10 @@ const conflictConfirmationPrompt = formatMessage( 'This operation will overwrite changes made to previously imported files. Do you want to proceed?' ); -export const importOrchestractor = async (projectId: string, reloadProject, setApplicationLevelError) => { +export const importOrchestrator = async (projectId: string, reloadProject, setApplicationLevelError) => { const reqBody = { package: 'Microsoft.Bot.Builder.AI.Orchestrator', - version: '4.13.0', + version: '4.13.1', source: 'https://api.nuget.org/v3/index.json', isUpdating: false, isPreview: false, diff --git a/Composer/packages/client/src/components/Orchestrator/OrchestratorForSkillsDialog.tsx b/Composer/packages/client/src/components/Orchestrator/OrchestratorForSkillsDialog.tsx new file mode 100644 index 0000000000..0a8325f2fe --- /dev/null +++ b/Composer/packages/client/src/components/Orchestrator/OrchestratorForSkillsDialog.tsx @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { DialogTypes, DialogWrapper } from '@bfc/ui-shared/lib/components/DialogWrapper'; +import { SDKKinds } from '@botframework-composer/types'; +import { Button } from 'office-ui-fabric-react/lib/components/Button/Button'; +import React, { useMemo } from 'react'; +import { useRecoilState, useRecoilValue } from 'recoil'; + +import { enableOrchestratorDialog } from '../../constants'; +import { + designPageLocationState, + dispatcherState, + localeState, + orchestratorForSkillsDialogState, + rootBotProjectIdSelector, +} from '../../recoilModel'; +import { recognizersSelectorFamily } from '../../recoilModel/selectors/recognizers'; +import { EnableOrchestrator } from '../AddRemoteSkillModal/EnableOrchestrator'; + +export const OrchestratorForSkillsDialog = () => { + const [showOrchestratorDialog, setShowOrchestratorDialog] = useRecoilState(orchestratorForSkillsDialogState); + const rootProjectId = useRecoilValue(rootBotProjectIdSelector) || ''; + const { dialogId } = useRecoilValue(designPageLocationState(rootProjectId)); + const locale = useRecoilValue(localeState(rootProjectId)); + const curRecognizers = useRecoilValue(recognizersSelectorFamily(rootProjectId)); + + const { updateRecognizer } = useRecoilValue(dispatcherState); + + const hasOrchestrator = useMemo(() => { + const fileName = `${dialogId}.${locale}.lu.dialog`; + return curRecognizers.some((f) => f.id === fileName && f.content.$kind === SDKKinds.OrchestratorRecognizer); + }, [curRecognizers, dialogId, locale]); + + const handleOrchestratorSubmit = async (event: React.MouseEvent, enable?: boolean) => { + event.preventDefault(); + if (enable) { + // update recognizor type to orchestrator + await updateRecognizer(rootProjectId, dialogId, SDKKinds.OrchestratorRecognizer); + } + setShowOrchestratorDialog(false); + }; + + const setVisibility = () => { + if (showOrchestratorDialog) { + if (hasOrchestrator) { + setShowOrchestratorDialog(false); + return false; + } + return true; + } + return false; + }; + + const onDismissHandler = (event: React.MouseEvent | undefined) => { + setShowOrchestratorDialog(false); + }; + + return ( + + + + ); +}; diff --git a/Composer/packages/client/src/components/Orchestrator/__tests__/OrchestratorForSkillsDialog.test.tsx b/Composer/packages/client/src/components/Orchestrator/__tests__/OrchestratorForSkillsDialog.test.tsx new file mode 100644 index 0000000000..bdd6ef5181 --- /dev/null +++ b/Composer/packages/client/src/components/Orchestrator/__tests__/OrchestratorForSkillsDialog.test.tsx @@ -0,0 +1,98 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { act, getQueriesForElement, within } from '@botframework-composer/test-utils'; +import { SDKKinds } from '@botframework-composer/types'; +import * as React from 'react'; +import userEvent from '@testing-library/user-event'; + +import { renderWithRecoil } from '../../../../__tests__/testUtils/renderWithRecoil'; +import { + botProjectFileState, + botProjectIdsState, + designPageLocationState, + localeState, + orchestratorForSkillsDialogState, + projectMetaDataState, +} from '../../../recoilModel'; +import { recognizersSelectorFamily } from '../../../recoilModel/selectors/recognizers'; +import { OrchestratorForSkillsDialog } from '../OrchestratorForSkillsDialog'; +import { importOrchestrator } from '../../AddRemoteSkillModal/helper'; + +jest.mock('../../AddRemoteSkillModal/helper'); + +// mimick a project setup with a rootbot and dialog files, and provide conditions for orchestrator skill dialog to be visible +const makeInitialState = (set: any) => { + set(orchestratorForSkillsDialogState, true); + set(botProjectIdsState, ['rootBotId']); + set(botProjectFileState('rootBotId'), { content: { name: 'rootBot', skills: {} }, id: 'rootBot', lastModified: '' }); + set(projectMetaDataState('rootBotId'), { isRootBot: true, isRemote: false }); + set(designPageLocationState('rootBotId'), { dialogId: 'rootBotRootDialogId', focused: 'na', selected: 'na' }); + set(localeState('rootBotId'), 'en-us'); + set(recognizersSelectorFamily('rootBotId'), [ + { id: 'rootBotRootDialogId.en-us.lu.dialog', content: { $kind: SDKKinds.LuisRecognizer } }, + ]); +}; + +const orchestratorTestId = 'orchestrator-skill'; + +describe('', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should not open OrchestratorForSkillsDialog if orchestratorForSkillsDialogState is false', () => { + const { baseElement } = renderWithRecoil(, ({ set }) => { + makeInitialState(set); + set(orchestratorForSkillsDialogState, false); + }); + const dialog = getQueriesForElement(baseElement).queryByTestId(orchestratorTestId); + expect(dialog).toBeNull(); + }); + + it('should not open OrchestratorForSkillsDialog if orchestrator already being used in root', () => { + const { baseElement } = renderWithRecoil(, ({ set }) => { + makeInitialState(set); + set(recognizersSelectorFamily('rootBotId'), [ + { id: 'rootBotRootDialogId.en-us.lu.dialog', content: { $kind: SDKKinds.OrchestratorRecognizer } }, + ]); + }); + const dialog = getQueriesForElement(baseElement).queryByTestId(orchestratorTestId); + expect(dialog).toBeNull(); + }); + + it('open OrchestratorForSkillsDialog if orchestratorForSkillsDialogState and Orchestrator not used in Root Bot Root Dialog', () => { + const { baseElement } = renderWithRecoil(, ({ set }) => { + makeInitialState(set); + }); + const dialog = getQueriesForElement(baseElement).queryByTestId(orchestratorTestId); + expect(dialog).toBeTruthy(); + }); + + it('should install Orchestrator package when user clicks Continue', async () => { + const { baseElement } = renderWithRecoil(, ({ set }) => { + makeInitialState(set); + }); + + await act(async () => { + userEvent.click(within(baseElement).getByTestId('import-orchestrator')); + }); + + expect(importOrchestrator).toBeCalledWith('rootBotId', expect.anything(), expect.anything()); + }); + + it('should not install Orchestrator package when user clicks skip', async () => { + const { baseElement } = renderWithRecoil(, ({ set }) => { + makeInitialState(set); + }); + + await act(async () => { + userEvent.click(await within(baseElement).findByText('Skip')); + }); + + const dialog = getQueriesForElement(baseElement).queryByTestId(orchestratorTestId); + expect(dialog).toBeNull(); + + expect(importOrchestrator).toBeCalledTimes(0); + }); +}); diff --git a/Composer/packages/client/src/constants.ts b/Composer/packages/client/src/constants.ts index 561cf2769c..cb650526bf 100644 --- a/Composer/packages/client/src/constants.ts +++ b/Composer/packages/client/src/constants.ts @@ -356,14 +356,14 @@ export const selectIntentDialog = { export const enableOrchestratorDialog = { get title() { - return formatMessage('Enable Orchestrator'); + return formatMessage('Enable Orchestrator Recognizer'); }, get subText() { - return formatMessage('Enable orchestrator as the recognizer at the root dialog to add this skill'); + return formatMessage('Enable Orchestrator as the recognizer for routing to other skills'); }, get content() { return formatMessage( - 'Multi-bot projects work best with the Orchestrator recognizer set at the root dialog. Orchestrator helps identify and dispatch user intents from the root dialog to the respective skill that can handle the intent. Orchestrator does not support entity extraction at the root dialog level.' + 'Multi-bot projects work best with the Orchestrator recognizer set at the dispatching dialog (typically the root dialog). Orchestrator helps identify and dispatch user intents from the root dialog to the respective skill that handles the intent. Orchestrator does not support entity extraction. If you plan to combine entity extraction and routing at the root dialog, use LUIS instead.' ); }, }; diff --git a/Composer/packages/client/src/pages/design/Modals.tsx b/Composer/packages/client/src/pages/design/Modals.tsx index b8414a72f5..6b99ab5176 100644 --- a/Composer/packages/client/src/pages/design/Modals.tsx +++ b/Composer/packages/client/src/pages/design/Modals.tsx @@ -27,6 +27,7 @@ import { undoFunctionState } from '../../recoilModel/undo/history'; import { CreationFlowStatus } from '../../constants'; import { RepairSkillModalOptionKeys } from '../../components/RepairSkillModal'; import { createQnAOnState, exportSkillModalInfoState } from '../../recoilModel/atoms/appState'; +import { OrchestratorForSkillsDialog } from '../../components/Orchestrator/OrchestratorForSkillsDialog'; import CreationModal from './creationModal'; @@ -170,6 +171,8 @@ const Modals: React.FC = ({ projectId = '' }) => { onSubmit={handleCreateQnA} /> + + {displaySkillManifestNameIdentifier && ( ({ default: false, }); +export const orchestratorForSkillsDialogState = atom({ + key: getFullyQualifiedKey('orchestratorForSkillsDialogState'), + default: false, +}); + export const warnAboutFunctionsState = atom({ key: getFullyQualifiedKey('warnAboutFunctionsState'), default: false, diff --git a/Composer/packages/client/src/recoilModel/dispatchers/project.ts b/Composer/packages/client/src/recoilModel/dispatchers/project.ts index 0c3b11782b..833a6a6e62 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/project.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/project.ts @@ -39,6 +39,7 @@ import { warnAboutDotNetState, warnAboutFunctionsState, settingsState, + orchestratorForSkillsDialogState, } from '../atoms'; import { botRuntimeOperationsSelector, rootBotProjectIdSelector } from '../selectors'; import { mergePropertiesManagedByRootBot, postRootBotCreation } from '../../recoilModel/dispatchers/utils/project'; @@ -166,6 +167,7 @@ export const projectDispatcher = () => { set(botProjectIdsState, (current) => [...current, projectId]); await dispatcher.addLocalSkillToBotProjectFile(projectId); navigateToSkillBot(rootBotProjectId, projectId, mainDialog); + callbackHelpers.set(orchestratorForSkillsDialogState, true); } catch (ex) { handleProjectFailure(callbackHelpers, ex); } finally {