diff --git a/Composer/packages/adaptive-form/jest.config.js b/Composer/packages/adaptive-form/jest.config.js index 2c019947a0..b5a508c259 100644 --- a/Composer/packages/adaptive-form/jest.config.js +++ b/Composer/packages/adaptive-form/jest.config.js @@ -5,5 +5,5 @@ const { createConfig } = require('@bfc/test-utils'); module.exports = createConfig('adaptive-form', 'react', { - coveragePathIgnorePatterns: ['defaultRecognizers.ts', 'defaultRoleSchema.ts', 'defaultUiSchema.ts'], + coveragePathIgnorePatterns: ['defaultRoleSchema.ts', 'defaultUiSchema.ts'], }); diff --git a/Composer/packages/adaptive-form/src/components/FormTitle.tsx b/Composer/packages/adaptive-form/src/components/FormTitle.tsx index 9fe7ba4b38..5946955bbc 100644 --- a/Composer/packages/adaptive-form/src/components/FormTitle.tsx +++ b/Composer/packages/adaptive-form/src/components/FormTitle.tsx @@ -52,9 +52,7 @@ interface FormTitleProps { const FormTitle: React.FC = (props) => { const { description, schema, formData, uiOptions = {} } = props; const { shellApi, ...shellData } = useShellApi(); - const { currentDialog } = shellData; - const recognizers = useRecognizerConfig(); - const selectedRecognizer = recognizers.find((r) => r.isSelected(currentDialog?.content?.recognizer)); + const { currentRecognizer: selectedRecognizer } = useRecognizerConfig(); // use a ref because the syncIntentName is debounced and we need the most current version to invoke the api const shell = useRef({ data: shellData, @@ -69,12 +67,13 @@ const FormTitle: React.FC = (props) => { debounce(async (newIntentName?: string, data?: any) => { if (newIntentName && selectedRecognizer) { const normalizedIntentName = newIntentName?.replace(/[^a-zA-Z0-9-_]+/g, ''); - await selectedRecognizer.renameIntent( - data?.intent, - normalizedIntentName, - shell.current.data, - shell.current.api - ); + typeof selectedRecognizer.renameIntent === 'function' && + (await selectedRecognizer.renameIntent( + data?.intent, + normalizedIntentName, + shell.current.data, + shell.current.api + )); } }, 400), [] diff --git a/Composer/packages/adaptive-form/src/components/fields/CustomRecognizerField.tsx b/Composer/packages/adaptive-form/src/components/fields/CustomRecognizerField.tsx new file mode 100644 index 0000000000..464e6524f3 --- /dev/null +++ b/Composer/packages/adaptive-form/src/components/fields/CustomRecognizerField.tsx @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import React from 'react'; +import { FieldProps } from '@bfc/extension-client'; +import { JsonEditor } from '@bfc/code-editor'; + +export const CustomRecognizerField: React.FC = (props) => { + const { value, onChange } = props; + return ( + + ); +}; diff --git a/Composer/packages/adaptive-form/src/components/fields/IntentField.tsx b/Composer/packages/adaptive-form/src/components/fields/IntentField.tsx index 60fb592079..e9bb3a877c 100644 --- a/Composer/packages/adaptive-form/src/components/fields/IntentField.tsx +++ b/Composer/packages/adaptive-form/src/components/fields/IntentField.tsx @@ -2,37 +2,29 @@ // Licensed under the MIT License. import React from 'react'; -import { FieldProps, useShellApi, useRecognizerConfig, FieldWidget } from '@bfc/extension-client'; +import { FieldProps, useRecognizerConfig } from '@bfc/extension-client'; import formatMessage from 'format-message'; -import { SDKKinds } from '@bfc/shared'; import { FieldLabel } from '../FieldLabel'; const IntentField: React.FC = (props) => { const { id, description, uiOptions, value, required, onChange } = props; - const { currentDialog } = useShellApi(); - const recognizers = useRecognizerConfig(); + const { currentRecognizer } = useRecognizerConfig(); + + const Editor = currentRecognizer?.intentEditor; + const label = formatMessage('Trigger phrases (intent: #{intentName})', { intentName: value }); const handleChange = () => { onChange(value); }; - const recognizer = recognizers.find((r) => r.isSelected(currentDialog?.content?.recognizer)); - let Editor: FieldWidget | undefined; - if (recognizer && recognizer.id === SDKKinds.CrossTrainedRecognizerSet) { - Editor = recognizers.find((r) => r.id === SDKKinds.LuisRecognizer)?.editor; - } else { - Editor = recognizer?.editor; - } - const label = formatMessage('Trigger phrases (intent: #{intentName})', { intentName: value }); - return ( {Editor ? ( ) : ( - formatMessage('No Editor for {type}', { type: recognizer?.id }) + formatMessage('No Editor for {type}', { type: currentRecognizer?.id }) )} ); diff --git a/Composer/packages/adaptive-form/src/components/fields/RecognizerField.tsx b/Composer/packages/adaptive-form/src/components/fields/RecognizerField.tsx deleted file mode 100644 index 4cb714dd7f..0000000000 --- a/Composer/packages/adaptive-form/src/components/fields/RecognizerField.tsx +++ /dev/null @@ -1,129 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. -/** @jsx jsx */ -import { jsx } from '@emotion/core'; -import React, { useMemo, useState, useEffect } from 'react'; -import { FieldProps, useShellApi, useRecognizerConfig } from '@bfc/extension-client'; -import { MicrosoftIRecognizer, SDKKinds } from '@bfc/shared'; -import { Dropdown, ResponsiveMode, IDropdownOption } from 'office-ui-fabric-react/lib/Dropdown'; -import formatMessage from 'format-message'; -import { JsonEditor } from '@bfc/code-editor'; - -import { FieldLabel } from '../FieldLabel'; - -const RecognizerField: React.FC> = (props) => { - const { value, id, label, description, uiOptions, required, onChange } = props; - const { shellApi, ...shellData } = useShellApi(); - const recognizers = useRecognizerConfig(); - const { qnaFiles, luFiles, currentDialog, locale } = shellData; - const [isCustomType, setIsCustomType] = useState(false); - - useEffect(() => { - // this logic is for handling old bot with `recognizer = undefined' - if (value === undefined) { - const qnaFile = qnaFiles.find((f) => f.id === `${currentDialog.id}.${locale}`); - const luFile = luFiles.find((f) => f.id === `${currentDialog.id}.${locale}`); - if (qnaFile && luFile) { - onChange(`${currentDialog.id}.lu.qna`); - } - } - - // transform lu recognizer to crosstrained for old bot - if (value === `${currentDialog.id}.lu`) { - onChange(`${currentDialog.id}.lu.qna`); - } - }, [value]); - - const options = useMemo(() => { - // filter luisRecognizer for dropdown options - return recognizers - .filter((r) => r.id !== SDKKinds.LuisRecognizer) - .map((r) => ({ - key: r.id, - text: typeof r.displayName === 'function' ? r.displayName(value) : r.displayName, - })); - }, [recognizers]); - - const selectedType = useMemo(() => { - if (isCustomType) { - return SDKKinds.CustomRecognizer; - } - const selected = - value === undefined - ? recognizers.length > 0 - ? [recognizers[0].id] - : [] - : recognizers.filter((r) => r.isSelected(value)).map((r) => r.id); - - const involvedCustomItem = selected.find((item) => item !== SDKKinds.CustomRecognizer); - if (involvedCustomItem) { - return involvedCustomItem; - } - if (selected.length < 1) { - /* istanbul ignore next */ - if (process.env.NODE_ENV === 'development') { - console.error( - `Unable to determine selected recognizer.\n - Value: ${JSON.stringify(value)}.\n - Selected Recognizers: [${selected.join(', ')}]` - ); - } - return; - } - - // transform luis recognizer to crosss trained recognizer for old bot. - if (selected[0] === SDKKinds.LuisRecognizer) { - selected[0] = SDKKinds.CrossTrainedRecognizerSet; - } - return selected[0]; - }, [value, isCustomType]); - - const handleChangeRecognizerType = (_, option?: IDropdownOption): void => { - if (option) { - if (option.key === SDKKinds.CustomRecognizer) { - setIsCustomType(true); - return; - } - - setIsCustomType(false); - const handler = recognizers.find((r) => r.id === option.key)?.handleRecognizerChange; - - if (handler) { - handler(props, shellData, shellApi); - } - } - }; - - const handleCustomChange = (value: string): void => { - setIsCustomType(true); - onChange(value); - }; - return ( - - - {selectedType ? ( - - ) : ( - formatMessage('Unable to determine recognizer type from data: {value}', { value }) - )} - {selectedType === SDKKinds.CustomRecognizer && ( - - )} - - ); -}; - -export { RecognizerField }; diff --git a/Composer/packages/adaptive-form/src/components/fields/RecognizerField/RecognizerField.tsx b/Composer/packages/adaptive-form/src/components/fields/RecognizerField/RecognizerField.tsx new file mode 100644 index 0000000000..51c1955f6b --- /dev/null +++ b/Composer/packages/adaptive-form/src/components/fields/RecognizerField/RecognizerField.tsx @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +/** @jsx jsx */ +import { jsx } from '@emotion/core'; +import React, { useMemo } from 'react'; +import { FieldProps, useShellApi, useRecognizerConfig } from '@bfc/extension-client'; +import { MicrosoftIRecognizer } from '@bfc/shared'; +import { Dropdown, ResponsiveMode, IDropdownOption } from 'office-ui-fabric-react/lib/Dropdown'; +import formatMessage from 'format-message'; + +import { FieldLabel } from '../../FieldLabel'; + +import { useMigrationEffect } from './useMigrationEffect'; +import { mapDropdownOptionToRecognizerSchema } from './mappers'; +import { getDropdownOptions } from './getDropdownOptions'; + +export const RecognizerField: React.FC> = (props) => { + const { value, id, label, description, uiOptions, required, onChange } = props; + const { shellApi, ...shellData } = useShellApi(); + + useMigrationEffect(value, onChange); + const { recognizers: recognizerConfigs, currentRecognizer } = useRecognizerConfig(); + const dropdownOptions = useMemo(() => getDropdownOptions(recognizerConfigs), [recognizerConfigs]); + + const RecognizerEditor = currentRecognizer?.recognizerEditor; + const widget = RecognizerEditor ? : null; + + const submit = (_, option?: IDropdownOption): void => { + if (!option) return; + + const recognizerDefinition = mapDropdownOptionToRecognizerSchema(option, recognizerConfigs); + + const seedNewRecognizer = recognizerDefinition?.seedNewRecognizer; + const recognizerInstance = + typeof seedNewRecognizer === 'function' + ? seedNewRecognizer(shellData, shellApi) + : { $kind: option.key as string, intents: [] }; // fallback to default Recognizer instance; + onChange(recognizerInstance); + }; + + return ( + + + + {widget} + + ); +}; diff --git a/Composer/packages/adaptive-form/src/components/fields/RecognizerField/defaultRecognizerOrder.ts b/Composer/packages/adaptive-form/src/components/fields/RecognizerField/defaultRecognizerOrder.ts new file mode 100644 index 0000000000..7836432d4a --- /dev/null +++ b/Composer/packages/adaptive-form/src/components/fields/RecognizerField/defaultRecognizerOrder.ts @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { SDKKinds } from '@bfc/shared'; + +export const defaultRecognizerOrder = [SDKKinds.CrossTrainedRecognizerSet, SDKKinds.RegexRecognizer]; + +export const recognizerOrderMap: { [$kind: string]: number } = defaultRecognizerOrder.reduce((result, $kind, index) => { + result[$kind] = index; + return result; +}, {}); diff --git a/Composer/packages/adaptive-form/src/components/fields/RecognizerField/getDropdownOptions.ts b/Composer/packages/adaptive-form/src/components/fields/RecognizerField/getDropdownOptions.ts new file mode 100644 index 0000000000..eaca2691be --- /dev/null +++ b/Composer/packages/adaptive-form/src/components/fields/RecognizerField/getDropdownOptions.ts @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { RecognizerSchema, FallbackRecognizerKey } from '@bfc/extension-client'; + +import { recognizerOrderMap } from './defaultRecognizerOrder'; +import { mapRecognizerSchemaToDropdownOption } from './mappers'; + +const getRankScore = (r: RecognizerSchema) => { + // Always put disabled recognizer behind. Handle 'disabled' before 'default'. + if (r.disabled) return Number.MAX_VALUE; + // Always put default recognzier ahead. + if (r.default) return -1; + // Put fallback recognizer behind. + if (r.id === FallbackRecognizerKey) return Number.MAX_VALUE - 1; + return recognizerOrderMap[r.id] ?? Number.MAX_VALUE - 1; +}; + +export const getDropdownOptions = (recognizerConfigs: RecognizerSchema[]) => { + return recognizerConfigs + .filter((r) => !r.disabled) + .sort((r1, r2) => { + return getRankScore(r1) - getRankScore(r2); + }) + .map(mapRecognizerSchemaToDropdownOption); +}; diff --git a/Composer/packages/adaptive-form/src/components/fields/RecognizerField/index.ts b/Composer/packages/adaptive-form/src/components/fields/RecognizerField/index.ts new file mode 100644 index 0000000000..24608a9eca --- /dev/null +++ b/Composer/packages/adaptive-form/src/components/fields/RecognizerField/index.ts @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +export { RecognizerField } from './RecognizerField'; diff --git a/Composer/packages/adaptive-form/src/components/fields/RecognizerField/mappers.ts b/Composer/packages/adaptive-form/src/components/fields/RecognizerField/mappers.ts new file mode 100644 index 0000000000..47c629af22 --- /dev/null +++ b/Composer/packages/adaptive-form/src/components/fields/RecognizerField/mappers.ts @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { RecognizerSchema } from '@bfc/extension-client'; +import { IDropdownOption } from 'office-ui-fabric-react/lib/Dropdown'; + +export const mapDropdownOptionToRecognizerSchema = (option: IDropdownOption, recognizerConfigs: RecognizerSchema[]) => { + return recognizerConfigs.find((r) => r.id === option.key); +}; + +export const mapRecognizerSchemaToDropdownOption = (recognizerSchema: RecognizerSchema): IDropdownOption => { + const { id, displayName } = recognizerSchema; + const recognizerName = typeof displayName === 'function' ? displayName({}) : displayName; + return { key: id, text: recognizerName || id }; +}; diff --git a/Composer/packages/adaptive-form/src/components/fields/RecognizerField/useMigrationEffect.ts b/Composer/packages/adaptive-form/src/components/fields/RecognizerField/useMigrationEffect.ts new file mode 100644 index 0000000000..e17a19e57c --- /dev/null +++ b/Composer/packages/adaptive-form/src/components/fields/RecognizerField/useMigrationEffect.ts @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { useShellApi, ChangeHandler } from '@bfc/extension-client'; +import { useEffect } from 'react'; +import { MicrosoftIRecognizer } from '@bfc/shared'; + +export const useMigrationEffect = ( + recognizer: MicrosoftIRecognizer | undefined, + onChangeRecognizer: ChangeHandler +) => { + const { qnaFiles, luFiles, currentDialog, locale } = useShellApi(); + + useEffect(() => { + // this logic is for handling old bot with `recognizer = undefined' + if (recognizer === undefined) { + const qnaFile = qnaFiles.find((f) => f.id === `${currentDialog.id}.${locale}`); + const luFile = luFiles.find((f) => f.id === `${currentDialog.id}.${locale}`); + if (qnaFile && luFile) { + onChangeRecognizer(`${currentDialog.id}.lu.qna`); + } + } + + // transform lu recognizer to crosstrained for old bot + if (recognizer === `${currentDialog.id}.lu`) { + onChangeRecognizer(`${currentDialog.id}.lu.qna`); + } + }, [recognizer]); +}; diff --git a/Composer/packages/adaptive-form/src/components/fields/__tests__/IntentField.test.tsx b/Composer/packages/adaptive-form/src/components/fields/__tests__/IntentField.test.tsx index 73366dc54e..4f1571e169 100644 --- a/Composer/packages/adaptive-form/src/components/fields/__tests__/IntentField.test.tsx +++ b/Composer/packages/adaptive-form/src/components/fields/__tests__/IntentField.test.tsx @@ -4,7 +4,7 @@ import React from 'react'; import { render, fireEvent } from '@bfc/test-utils'; import assign from 'lodash/assign'; -import { useShellApi, useRecognizerConfig } from '@bfc/extension-client'; +import { useRecognizerConfig } from '@bfc/extension-client'; import { IntentField } from '../IntentField'; @@ -20,28 +20,27 @@ function renderSubject(overrides = {}) { return render(); } -describe('', () => { - beforeEach(() => { - (useRecognizerConfig as jest.Mock).mockReturnValue([ - { - id: 'TestRecognizer', - isSelected: (data) => data?.$kind === 'TestRecognizer', - editor: ({ id, onChange }) => ( -
- Test Recognizer -
- ), - }, - { - id: 'OtherRecognizer', - isSelected: (data) => data?.$kind === 'OtherRecognizer', - }, - ]); - }); +const recognizers = [ + { + id: 'TestRecognizer', + displayName: 'TestRecognizer', + intentEditor: ({ id, onChange }) => ( +
+ Test Recognizer +
+ ), + }, + { + id: 'OtherRecognizer', + displayName: 'OtherRecognizer', + }, +]; +describe('', () => { it('uses a custom label', () => { - (useShellApi as jest.Mock).mockReturnValue({ - currentDialog: { content: { recognizer: { $kind: 'TestRecognizer' } } }, + (useRecognizerConfig as jest.Mock).mockReturnValue({ + recognizers, + currentRecognizer: recognizers[0], }); const { getByLabelText } = renderSubject({ value: 'MyIntent' }); @@ -49,9 +48,11 @@ describe('', () => { }); it('invokes change handler with intent name', () => { - (useShellApi as jest.Mock).mockReturnValue({ - currentDialog: { content: { recognizer: { $kind: 'TestRecognizer' } } }, + (useRecognizerConfig as jest.Mock).mockReturnValue({ + recognizers, + currentRecognizer: recognizers[0], }); + const onChange = jest.fn(); const { getByText } = renderSubject({ onChange, value: 'MyIntent' }); @@ -60,8 +61,9 @@ describe('', () => { }); it('renders message when editor not defined', () => { - (useShellApi as jest.Mock).mockReturnValue({ - currentDialog: { content: { recognizer: { $kind: 'OtherRecognizer' } } }, + (useRecognizerConfig as jest.Mock).mockReturnValue({ + recognizers, + currentRecognizer: recognizers[1], }); const { container } = renderSubject({ value: 'MyIntent' }); diff --git a/Composer/packages/adaptive-form/src/components/fields/__tests__/RecognizerField.test.tsx b/Composer/packages/adaptive-form/src/components/fields/__tests__/RecognizerField.test.tsx index 7d0e79090a..f27b62a2dd 100644 --- a/Composer/packages/adaptive-form/src/components/fields/__tests__/RecognizerField.test.tsx +++ b/Composer/packages/adaptive-form/src/components/fields/__tests__/RecognizerField.test.tsx @@ -31,29 +31,27 @@ describe('', () => { }); }); - it('renders error message when no recognizer matched', () => { - (useRecognizerConfig as jest.Mock).mockReturnValue([]); - const { container } = renderSubject(); - expect(container).toHaveTextContent(/Unable to determine recognizer type from data:/); - }); - it('renders a dropdown when recognizer matches', () => { const handleChange = jest.fn(); - (useRecognizerConfig as jest.Mock).mockReturnValue([ + const recognizers = [ { id: 'one', displayName: 'One Recognizer', isSelected: () => false, - handleRecognizerChange: handleChange, + seedNewRecognizer: handleChange, }, { id: 'two', displayName: 'Two Recognizer', isSelected: () => true, - handleRecognizerChange: jest.fn(), + seedNewRecognizer: jest.fn(), }, - ]); - const { getByTestId } = renderSubject({ value: 'one' }); + ]; + (useRecognizerConfig as jest.Mock).mockReturnValue({ + recognizers, + currentRecognizer: recognizers[1], + }); + const { getByTestId } = renderSubject({ value: { $kind: 'two' } }); const dropdown = getByTestId('recognizerTypeDropdown'); expect(dropdown).toHaveTextContent('Two Recognizer'); fireEvent.click(dropdown); diff --git a/Composer/packages/adaptive-form/src/components/fields/index.ts b/Composer/packages/adaptive-form/src/components/fields/index.ts index fdb9c1c115..24246f3a97 100644 --- a/Composer/packages/adaptive-form/src/components/fields/index.ts +++ b/Composer/packages/adaptive-form/src/components/fields/index.ts @@ -2,6 +2,7 @@ // Licensed under the MIT License. export * from './ArrayField'; export * from './BooleanField'; +export * from './CustomRecognizerField'; export * from './EditableField'; export * from './ExpressionField/ExpressionField'; export * from './FieldSets'; diff --git a/Composer/packages/adaptive-form/src/defaultRecognizers.ts b/Composer/packages/adaptive-form/src/defaultRecognizers.ts deleted file mode 100644 index c4a245d1e2..0000000000 --- a/Composer/packages/adaptive-form/src/defaultRecognizers.ts +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -import { RecognizerSchema } from '@bfc/extension-client'; -import { SDKKinds } from '@bfc/shared'; -import formatMessage from 'format-message'; - -import { RegexIntentField } from './components/fields/RegexIntentField'; - -const DefaultRecognizers: RecognizerSchema[] = [ - { - id: SDKKinds.RegexRecognizer, - displayName: () => formatMessage('Regular Expression'), - editor: RegexIntentField, - isSelected: (data) => { - return typeof data === 'object' && data.$kind === SDKKinds.RegexRecognizer; - }, - handleRecognizerChange: (props) => { - props.onChange({ $kind: SDKKinds.RegexRecognizer, intents: [] }); - }, - renameIntent: async (intentName, newIntentName, shellData, shellApi) => { - const { currentDialog } = shellData; - await shellApi.renameRegExIntent(currentDialog.id, intentName, newIntentName); - }, - }, - { - id: SDKKinds.CustomRecognizer, - displayName: () => formatMessage('Custom recognizer'), - isSelected: (data) => typeof data === 'object', - handleRecognizerChange: (props) => - props.onChange({ - $kind: 'Microsoft.MultiLanguageRecognizer', - recognizers: { - 'en-us': { - $kind: 'Microsoft.RegexRecognizer', - intents: [ - { - intent: 'greeting', - pattern: 'hello', - }, - { - intent: 'test', - pattern: 'test', - }, - ], - }, - 'zh-cn': { - $kind: 'Microsoft.RegexRecognizer', - intents: [ - { - intent: 'greeting', - pattern: '你好', - }, - { - intent: 'test', - pattern: '测试', - }, - ], - }, - }, - }), - renameIntent: () => {}, - }, -]; - -export default DefaultRecognizers; diff --git a/Composer/packages/client/__tests__/plugins.test.ts b/Composer/packages/client/__tests__/plugins.test.ts index ea15956780..102d9536ea 100644 --- a/Composer/packages/client/__tests__/plugins.test.ts +++ b/Composer/packages/client/__tests__/plugins.test.ts @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +import { SDKKinds } from '@bfc/shared'; + import { mergePluginConfigs } from '../src/plugins'; describe('mergePluginConfigs', () => { @@ -43,22 +45,35 @@ describe('mergePluginConfigs', () => { }, }, }, - recognizers: [], flowWidgets: {}, }); }); it('adds recognizers', () => { const config1 = { - recognizers: ['recognizer 1'], + uiSchema: { + [SDKKinds.RegexRecognizer]: { + recognizer: { displayName: 'recognizer1' }, + }, + }, }; const config2 = { - recognizers: ['recognizer 2'], + uiSchema: { + [SDKKinds.LuisRecognizer]: { + recognizer: { displayName: 'recognizer2' }, + }, + }, }; - // @ts-expect-error - expect(mergePluginConfigs(config1, config2).recognizers).toEqual(['recognizer 2', 'recognizer 1']); + expect(mergePluginConfigs(config1, config2).uiSchema).toEqual({ + [SDKKinds.RegexRecognizer]: { + recognizer: { displayName: 'recognizer1' }, + }, + [SDKKinds.LuisRecognizer]: { + recognizer: { displayName: 'recognizer2' }, + }, + }); }); it('replaces other arrays', () => { diff --git a/Composer/packages/client/src/pages/design/DesignPage.tsx b/Composer/packages/client/src/pages/design/DesignPage.tsx index 09754e1e46..15e51eb8a0 100644 --- a/Composer/packages/client/src/pages/design/DesignPage.tsx +++ b/Composer/packages/client/src/pages/design/DesignPage.tsx @@ -8,7 +8,7 @@ import { Breadcrumb, IBreadcrumbItem } from 'office-ui-fabric-react/lib/Breadcru import formatMessage from 'format-message'; import { globalHistory, RouteComponentProps } from '@reach/router'; import get from 'lodash/get'; -import { DialogFactory, SDKKinds, DialogInfo, PromptTab, getEditorAPI, registerEditorAPI } from '@bfc/shared'; +import { DialogInfo, PromptTab, getEditorAPI, registerEditorAPI } from '@bfc/shared'; import { ActionButton } from 'office-ui-fabric-react/lib/Button'; import { JsonEditor } from '@bfc/code-editor'; import { EditorExtension, useTriggerApi, PluginConfig } from '@bfc/extension-client'; @@ -46,7 +46,6 @@ import { focusPathState, showCreateDialogModalState, showAddSkillDialogModalState, - actionsSeedState, localeState, } from '../../recoilModel'; import ImportQnAFromUrlModal from '../knowledge-base/ImportQnAFromUrlModal'; @@ -117,7 +116,6 @@ const DesignPage: React.FC }> {showCreateDialogModal && ( - createDialogCancel(projectId)} - onSubmit={handleCreateDialogSubmit} - /> + + createDialogCancel(projectId)} + onSubmit={handleCreateDialogSubmit} + /> + )} {showAddSkillDialogModal && ( void; + onSubmit: (dialogName: string, dialogContent) => void; onDismiss: () => void; onCurrentPathUpdate?: (newPath?: string, storageId?: string) => void; focusedStorageFolder?: StorageFolder; @@ -32,7 +34,14 @@ interface CreateDialogModalProps { export const CreateDialogModal: React.FC = (props) => { const { onSubmit, onDismiss, isOpen, projectId } = props; + + const schemas = useRecoilValue(schemasState(projectId)); const dialogs = useRecoilValue(validateDialogSelectorFamily(projectId)); + const actionsSeed = useRecoilValue(actionsSeedState(projectId)); + + const { shellApi, ...shellData } = useShellApi(); + const { defaultRecognizer } = useRecognizerConfig(); + const formConfig: FieldConfig = { name: { required: true, @@ -54,6 +63,26 @@ export const CreateDialogModal: React.FC = (props) => { const { formData, formErrors, hasErrors, updateField } = useForm(formConfig); + const seedNewRecognizer = (recognizerSchema?: RecognizerSchema) => { + if (recognizerSchema && typeof recognizerSchema.seedNewRecognizer === 'function') { + return recognizerSchema.seedNewRecognizer(shellData, shellApi); + } + return { $kind: recognizerSchema?.id }; + }; + + const seedNewDialog = (formData: DialogFormData) => { + const seededContent = new DialogFactory(schemas.sdk?.content).create(SDKKinds.AdaptiveDialog, { + $designer: { name: formData.name, description: formData.description }, + generator: `${formData.name}.lg`, + recognizer: seedNewRecognizer(defaultRecognizer), + }); + if (seededContent.triggers?.[0]) { + seededContent.triggers[0].actions = actionsSeed; + } + + return seededContent; + }; + const handleSubmit = useCallback( (e) => { e.preventDefault(); @@ -61,9 +90,9 @@ export const CreateDialogModal: React.FC = (props) => { return; } - onSubmit({ - ...formData, - }); + const dialogData = seedNewDialog(formData); + + onSubmit(formData.name, dialogData); }, [hasErrors, formData] ); diff --git a/Composer/packages/client/src/plugins.ts b/Composer/packages/client/src/plugins.ts index 9a67db6574..cd8b8f9dc4 100644 --- a/Composer/packages/client/src/plugins.ts +++ b/Composer/packages/client/src/plugins.ts @@ -29,7 +29,6 @@ const mergeArrays: MergeWithCustomizer = (objValue, srcValue, key) => { const defaultPlugin: Required = { uiSchema: {}, - recognizers: [], flowWidgets: {}, }; diff --git a/Composer/packages/client/src/utils/dialogUtil.ts b/Composer/packages/client/src/utils/dialogUtil.ts index e890e019bb..235868bd23 100644 --- a/Composer/packages/client/src/utils/dialogUtil.ts +++ b/Composer/packages/client/src/utils/dialogUtil.ts @@ -52,7 +52,6 @@ export const intentTypeKey: string = SDKKinds.OnIntent; export const qnaTypeKey: string = SDKKinds.OnQnAMatch; export const activityTypeKey: string = SDKKinds.OnActivity; export const regexRecognizerKey: string = SDKKinds.RegexRecognizer; -export const crossTrainedRecognizerSetKey: string = SDKKinds.CrossTrainedRecognizerSet; export const customEventKey = 'OnCustomEvent'; export const qnaMatcherKey: string = SDKKinds.OnQnAMatch; export const onChooseIntentKey: string = SDKKinds.OnChooseIntent; diff --git a/Composer/packages/extension-client/src/hooks/__tests__/useRecognizerConfig.test.tsx b/Composer/packages/extension-client/src/hooks/__tests__/useRecognizerConfig.test.tsx index efea8491c1..f13b03d3b9 100644 --- a/Composer/packages/extension-client/src/hooks/__tests__/useRecognizerConfig.test.tsx +++ b/Composer/packages/extension-client/src/hooks/__tests__/useRecognizerConfig.test.tsx @@ -7,29 +7,40 @@ import { renderHook } from '@bfc/test-utils/lib/hooks'; import { useRecognizerConfig } from '../useRecognizerConfig'; import { EditorExtensionContext } from '../../EditorExtensionContext'; +const shellData = { currentDialog: { content: {} } }; const plugins = { uiSchema: { foo: { form: {}, menu: {}, + recognizer: { displayName: 'recognizer 1' }, }, bar: { form: {}, menu: {}, + recognizer: { displayName: 'recognizer 2' }, }, }, - recognizers: ['recognizer 1', 'recognizer 2'], }; const wrapper: React.FC = ({ children }) => ( // @ts-expect-error - {children} + {children} ); describe('useRecognizerConfig', () => { it('returns the configured recognizers', () => { const { result } = renderHook(() => useRecognizerConfig(), { wrapper }); - expect(result.current).toEqual(['recognizer 1', 'recognizer 2']); + expect(result.current.recognizers).toEqual([ + { + id: 'foo', + displayName: 'recognizer 1', + }, + { + id: 'bar', + displayName: 'recognizer 2', + }, + ]); }); }); diff --git a/Composer/packages/extension-client/src/hooks/useRecognizerConfig.ts b/Composer/packages/extension-client/src/hooks/useRecognizerConfig.ts index 1d28715cff..a90127aa55 100644 --- a/Composer/packages/extension-client/src/hooks/useRecognizerConfig.ts +++ b/Composer/packages/extension-client/src/hooks/useRecognizerConfig.ts @@ -1,12 +1,87 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { useContext } from 'react'; +import { useContext, useMemo } from 'react'; +import { MicrosoftIRecognizer, SDKKinds } from '@bfc/shared'; +import get from 'lodash/get'; import { EditorExtensionContext } from '../EditorExtensionContext'; +import { RecognizerOptions, RecognizerSchema } from '../types'; -export function useRecognizerConfig() { - const { plugins } = useContext(EditorExtensionContext); +export const FallbackRecognizerKey = 'fallback'; - return plugins.recognizers ?? []; +// TODO: (ze) remove this logic after the ui widget PR. [issue #4167] +const reuseLuisIntentEditor = (recognizers: RecognizerSchema[]) => { + const crosstrainRecognizer = recognizers.find((x) => x.id === SDKKinds.CrossTrainedRecognizerSet); + const luisRecognizer = recognizers.find((x) => x.id === SDKKinds.LuisRecognizer); + if (crosstrainRecognizer && luisRecognizer) { + crosstrainRecognizer.intentEditor = luisRecognizer.intentEditor; + } +}; + +const getDefaultRecognizer = (recognizers: RecognizerSchema[]) => { + const defaultRecognizer = recognizers.find((r) => r.default && !r.disabled); + if (defaultRecognizer) return defaultRecognizer; + + // TODO: (ze) remove this logic after recognizer config is port to SDK component schema. + const crosstrainRecognizer = recognizers.find((r) => r.id === SDKKinds.CrossTrainedRecognizerSet); + if (crosstrainRecognizer) return crosstrainRecognizer; + + const firstAvailableRecognizer = recognizers.find((r) => !r.disabled); + return firstAvailableRecognizer; +}; + +const getFallbackRecognizer = (recognizers: RecognizerSchema[]) => { + return recognizers.find((r) => r.id === FallbackRecognizerKey); +}; + +const findRecognizerByValue = (recognizers: RecognizerSchema[], recognizerValue?: MicrosoftIRecognizer) => { + const matchedRecognizer = recognizers.find((r) => { + if (typeof r.isSelected === 'function') { + return r.isSelected(recognizerValue); + } + return r.id === get(recognizerValue, '$kind'); + }); + return matchedRecognizer; +}; + +export interface RecognizerSchemaConfig { + /** All recognizer definitions from uischema. */ + recognizers: RecognizerSchema[]; + /** Current dialog's in-use recognizer definition. */ + currentRecognizer?: RecognizerSchema; + /** Default recognizer's definition, used when creating new dialog. */ + defaultRecognizer?: RecognizerSchema; +} + +export function useRecognizerConfig(): RecognizerSchemaConfig { + const { plugins, shellData } = useContext(EditorExtensionContext); + + const recognizers: RecognizerSchema[] = useMemo(() => { + if (!plugins.uiSchema) return []; + + const schemas = Object.entries(plugins.uiSchema) + .filter(([_, uiOptions]) => uiOptions && uiOptions.recognizer) + .map(([$kind, uiOptions]) => { + const recognizerOptions = uiOptions?.recognizer as RecognizerOptions; + return { + id: $kind, + ...recognizerOptions, + } as RecognizerSchema; + }); + reuseLuisIntentEditor(schemas); + return schemas; + }, [plugins.uiSchema]); + + const defaultRecognizer = getDefaultRecognizer(recognizers); + const fallbackRecognizer = getFallbackRecognizer(recognizers); + + const currentRecognizerValue = shellData.currentDialog?.content?.recognizer; + const currentRecognizer = findRecognizerByValue(recognizers, currentRecognizerValue) ?? fallbackRecognizer; + + return { + recognizers, + currentRecognizer, + defaultRecognizer, + }; } diff --git a/Composer/packages/extension-client/src/types/extension.ts b/Composer/packages/extension-client/src/types/extension.ts index 7032159e6b..d17d56df10 100644 --- a/Composer/packages/extension-client/src/types/extension.ts +++ b/Composer/packages/extension-client/src/types/extension.ts @@ -3,12 +3,12 @@ import { SDKKinds } from '@bfc/shared'; -import { RecognizerSchema, UIOptions } from './formSchema'; +import { UIOptions } from './formSchema'; import { FlowEditorWidgetMap, FlowWidget } from './flowSchema'; import { MenuOptions } from './menuSchema'; +import { RecognizerOptions } from './recognizerSchema'; export interface PluginConfig { - recognizers?: RecognizerSchema[]; uiSchema?: UISchema; flowWidgets?: FlowEditorWidgetMap; } @@ -18,5 +18,6 @@ export type UISchema = { flow?: FlowWidget; form?: UIOptions; menu?: MenuOptions; + recognizer?: RecognizerOptions; }; }; diff --git a/Composer/packages/extension-client/src/types/formSchema.ts b/Composer/packages/extension-client/src/types/formSchema.ts index c07a175cb3..805475717e 100644 --- a/Composer/packages/extension-client/src/types/formSchema.ts +++ b/Composer/packages/extension-client/src/types/formSchema.ts @@ -1,8 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { SDKKinds, SDKRoles, ShellApi, ShellData } from '@bfc/shared'; +import { MicrosoftIRecognizer, SDKKinds, SDKRoles, ShellApi, ShellData } from '@bfc/shared'; -import { FieldProps, FieldWidget } from './form'; +import { FieldWidget } from './form'; // eslint-disable-next-line @typescript-eslint/no-explicit-any type UIOptionValue = R | UIOptionFunc; @@ -63,16 +63,24 @@ export type FormUISchema = { [key in SDKKinds]?: UIOptions }; export type RecognizerSchema = { /** Unique id to identify recognizer (SDK $kind) */ id: string; - /** Display name used in the UI */ + /** If is default, will be used as dropdown's default selection */ + default?: boolean; + /** If disabled, cannot be selected from dropdown */ + disabled?: boolean; + /** Display name used in the UI. Recommended to use function over static string to enable multi-locale feature. */ displayName: UIOptionValue; /** An inline editor to edit an intent. If none provided, users will not be able to edit. */ - editor?: FieldWidget; + intentEditor?: FieldWidget; /** A function invoked with the form data to determine if this is the currently selected recognizer */ - isSelected: (data: any) => boolean; - /** Invoked when changing the recognizer type */ - handleRecognizerChange: (fieldProps: FieldProps, shellData: ShellData, shellApi: ShellApi) => void; + isSelected?: (data: any) => boolean; + /** Invoked when constructing a new recognizer instance. + * Make sure the instance can be recognized either by $kind or isSelected(). + */ + seedNewRecognizer?: (shellData: ShellData, shellApi: ShellApi) => MicrosoftIRecognizer | any; + /** An inline editor to edit recognizer value. If none provided, users will not be able to edit its value. */ + recognizerEditor?: FieldWidget; /** Function to rename an intent */ - renameIntent: ( + renameIntent?: ( intentName: string, newIntentName: string, shellData: ShellData, diff --git a/Composer/packages/extension-client/src/types/index.ts b/Composer/packages/extension-client/src/types/index.ts index d51cd232e0..a44ce84f99 100644 --- a/Composer/packages/extension-client/src/types/index.ts +++ b/Composer/packages/extension-client/src/types/index.ts @@ -6,3 +6,4 @@ export * from './form'; export * from './formSchema'; export * from './flowSchema'; export * from './menuSchema'; +export * from './recognizerSchema'; diff --git a/Composer/packages/extension-client/src/types/recognizerSchema.ts b/Composer/packages/extension-client/src/types/recognizerSchema.ts new file mode 100644 index 0000000000..e1585ec68e --- /dev/null +++ b/Composer/packages/extension-client/src/types/recognizerSchema.ts @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { SDKKinds } from '@bfc/shared'; + +import { RecognizerSchema } from './formSchema'; + +// Omit the 'id' field because it can be inferred from $kind. +export type RecognizerOptions = Omit; + +export type RecognizerUISchema = { [key in SDKKinds]?: RecognizerOptions }; diff --git a/Composer/packages/extension-client/src/utils/__tests__/mergePluginConfigs.test.ts b/Composer/packages/extension-client/src/utils/__tests__/mergePluginConfigs.test.ts index 93763a0fd4..125e0006c1 100644 --- a/Composer/packages/extension-client/src/utils/__tests__/mergePluginConfigs.test.ts +++ b/Composer/packages/extension-client/src/utils/__tests__/mergePluginConfigs.test.ts @@ -56,23 +56,25 @@ describe('mergePluginConfigs', () => { it('merges recognizers into single list', () => { const plugins: Partial[] = [ { - recognizers: [ - { - id: 'default', - displayName: 'Default', - isSelected: () => false, - handleRecognizerChange: jest.fn(), + uiSchema: { + [SDKKinds.LuisRecognizer]: { + recognizer: { + displayName: 'Default', + isSelected: () => false, + seedNewRecognizer: jest.fn(), + }, }, - { - id: 'new', - displayName: 'New Recognizer', - isSelected: () => false, - handleRecognizerChange: jest.fn(), + [SDKKinds.RegexRecognizer]: { + recognizer: { + displayName: 'New Recognizer', + isSelected: () => false, + seedNewRecognizer: jest.fn(), + }, }, - ], + }, }, ]; - expect(mergePluginConfigs(...plugins).recognizers).toHaveLength(2); + expect(Object.keys(mergePluginConfigs(...plugins).uiSchema)).toHaveLength(2); }); }); diff --git a/Composer/packages/extension-client/src/utils/mergePluginConfigs.ts b/Composer/packages/extension-client/src/utils/mergePluginConfigs.ts index 3b08f6dfca..69870f06b4 100644 --- a/Composer/packages/extension-client/src/utils/mergePluginConfigs.ts +++ b/Composer/packages/extension-client/src/utils/mergePluginConfigs.ts @@ -22,7 +22,6 @@ const mergeArrays: MergeWithCustomizer = (objValue, srcValue, key) => { const defaultPlugin: Required = { uiSchema: {}, - recognizers: [], flowWidgets: {}, }; diff --git a/Composer/packages/lib/shared/src/types/sdk.ts b/Composer/packages/lib/shared/src/types/sdk.ts index d31028a3b6..171c0970bb 100644 --- a/Composer/packages/lib/shared/src/types/sdk.ts +++ b/Composer/packages/lib/shared/src/types/sdk.ts @@ -18,8 +18,8 @@ export interface BaseSchema { $copy?: string; /** Extra information for the Bot Framework Composer. */ $designer?: DesignerData; - /** If 'disabled' set to true, runtime will skip this action. */ - disabled: any; + /** If 'disabled' equals to or be evaluated as 'true', runtime will skip this action. */ + disabled?: boolean | string; } /* Union of components which implement the IActivityTemplate interface */ diff --git a/Composer/packages/ui-plugins/composer/src/defaultRecognizerSchema.ts b/Composer/packages/ui-plugins/composer/src/defaultRecognizerSchema.ts new file mode 100644 index 0000000000..d17a2d3fc3 --- /dev/null +++ b/Composer/packages/ui-plugins/composer/src/defaultRecognizerSchema.ts @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { RecognizerUISchema, FallbackRecognizerKey, RecognizerOptions } from '@bfc/extension-client'; +import { SDKKinds } from '@bfc/shared'; +import formatMessage from 'format-message'; +import { RegexIntentField, CustomRecognizerField } from '@bfc/adaptive-form'; + +const FallbackRecognizerJsonEditor: RecognizerOptions = { + displayName: () => formatMessage('Custom recognizer'), + seedNewRecognizer: () => ({}), + recognizerEditor: CustomRecognizerField, +}; + +export const DefaultRecognizerSchema: RecognizerUISchema = { + [SDKKinds.RegexRecognizer]: { + displayName: () => formatMessage('Regular Expression'), + intentEditor: RegexIntentField, + renameIntent: (intentName, newIntentName, shellData, shellApi) => { + const { currentDialog } = shellData; + shellApi.renameRegExIntent(currentDialog.id, intentName, newIntentName); + }, + }, + [FallbackRecognizerKey as SDKKinds]: FallbackRecognizerJsonEditor, +}; diff --git a/Composer/packages/ui-plugins/composer/src/index.ts b/Composer/packages/ui-plugins/composer/src/index.ts index 813fb72d63..9fa439562e 100644 --- a/Composer/packages/ui-plugins/composer/src/index.ts +++ b/Composer/packages/ui-plugins/composer/src/index.ts @@ -1,69 +1,14 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { PluginConfig, FormUISchema, RecognizerSchema, UISchema, MenuUISchema } from '@bfc/extension-client'; +import { PluginConfig, FormUISchema, UISchema, MenuUISchema, RecognizerUISchema } from '@bfc/extension-client'; import { SDKKinds } from '@bfc/shared'; import formatMessage from 'format-message'; -import mapValues from 'lodash/mapValues'; -import { IntentField, RecognizerField, RegexIntentField, QnAActionsField } from '@bfc/adaptive-form'; +import mergeWith from 'lodash/mergeWith'; +import { IntentField, RecognizerField, QnAActionsField } from '@bfc/adaptive-form'; import { DefaultMenuSchema } from './defaultMenuSchema'; - -const DefaultRecognizers: RecognizerSchema[] = [ - { - id: SDKKinds.RegexRecognizer, - displayName: () => formatMessage('Regular Expression'), - editor: RegexIntentField, - isSelected: (data) => { - return typeof data === 'object' && data.$kind === SDKKinds.RegexRecognizer; - }, - handleRecognizerChange: (props) => { - props.onChange({ $kind: SDKKinds.RegexRecognizer, intents: [] }); - }, - renameIntent: (intentName, newIntentName, shellData, shellApi) => { - const { currentDialog } = shellData; - shellApi.renameRegExIntent(currentDialog.id, intentName, newIntentName); - }, - }, - { - id: SDKKinds.CustomRecognizer, - displayName: () => formatMessage('Custom recognizer'), - isSelected: (data) => typeof data === 'object', - handleRecognizerChange: (props) => - props.onChange({ - $kind: 'Microsoft.MultiLanguageRecognizer', - recognizers: { - 'en-us': { - $kind: 'Microsoft.RegexRecognizer', - intents: [ - { - intent: 'greeting', - pattern: 'hello', - }, - { - intent: 'test', - pattern: 'test', - }, - ], - }, - 'zh-cn': { - $kind: 'Microsoft.RegexRecognizer', - intents: [ - { - intent: 'greeting', - pattern: '你好', - }, - { - intent: 'test', - pattern: '测试', - }, - ], - }, - }, - }), - renameIntent: () => {}, - }, -]; +import { DefaultRecognizerSchema } from './defaultRecognizerSchema'; const DefaultFormSchema: FormUISchema = { [SDKKinds.AdaptiveDialog]: { @@ -216,21 +161,20 @@ const DefaultFormSchema: FormUISchema = { }, }; -const synthesizeUISchema = (formSchema: FormUISchema, menuSchema: MenuUISchema): UISchema => { - const uiSchema: UISchema = mapValues(formSchema, (val) => ({ form: val })); - for (const [$kind, menuConfig] of Object.entries(menuSchema)) { - if (uiSchema[$kind]) { - uiSchema[$kind].menu = menuConfig; - } else { - uiSchema[$kind] = { menu: menuConfig }; - } - } - return uiSchema; +const synthesizeUISchema = ( + formSchema: FormUISchema, + menuSchema: MenuUISchema, + recognizerSchema: RecognizerUISchema +): UISchema => { + let uischema: UISchema = {}; + uischema = mergeWith(uischema, formSchema, (origin, formOption) => ({ ...origin, form: formOption })); + uischema = mergeWith(uischema, menuSchema, (origin, menuOption) => ({ ...origin, menu: menuOption })); + uischema = mergeWith(uischema, recognizerSchema, (origin, opt) => ({ ...origin, recognizer: opt })); + return uischema; }; const config: PluginConfig = { - uiSchema: synthesizeUISchema(DefaultFormSchema, DefaultMenuSchema), - recognizers: DefaultRecognizers, + uiSchema: synthesizeUISchema(DefaultFormSchema, DefaultMenuSchema, DefaultRecognizerSchema), }; export default config; diff --git a/Composer/packages/ui-plugins/cross-trained/src/index.ts b/Composer/packages/ui-plugins/cross-trained/src/index.ts index 220a4054b0..e424e2452a 100644 --- a/Composer/packages/ui-plugins/cross-trained/src/index.ts +++ b/Composer/packages/ui-plugins/cross-trained/src/index.ts @@ -6,29 +6,27 @@ import { SDKKinds } from '@bfc/shared'; import formatMessage from 'format-message'; const config: PluginConfig = { - recognizers: [ - { - id: SDKKinds.CrossTrainedRecognizerSet, - displayName: formatMessage('Default recognizer'), - isSelected: (data) => { - return typeof data === 'string'; - }, - handleRecognizerChange: (props, shellData, _) => { - const { qnaFiles, luFiles, currentDialog, locale } = shellData; - const qnaFile = qnaFiles.find((f) => f.id === `${currentDialog.id}.${locale}`); - const luFile = luFiles.find((f) => f.id === `${currentDialog.id}.${locale}`); + uiSchema: { + [SDKKinds.CrossTrainedRecognizerSet]: { + recognizer: { + displayName: () => formatMessage('Default recognizer'), + isSelected: (data) => { + return typeof data === 'string' && data.endsWith('.lu.qna'); + }, + seedNewRecognizer: (shellData) => { + const { qnaFiles, luFiles, currentDialog, locale } = shellData; + const qnaFile = qnaFiles.find((f) => f.id === `${currentDialog.id}.${locale}`); + const luFile = luFiles.find((f) => f.id === `${currentDialog.id}.${locale}`); + + if (!qnaFile || !luFile) { + alert(formatMessage(`NO LU OR QNA FILE WITH NAME { id }`, { id: currentDialog.id })); + } - if (qnaFile && luFile) { - // strip locale out of id so it doesn't get serialized - // into the .dialog file - props.onChange(`${currentDialog.id}.lu.qna`); - } else { - alert(formatMessage(`NO LU OR QNA FILE WITH NAME { id }`, { id: currentDialog.id })); - } + return `${currentDialog.id}.lu.qna`; + }, }, - renameIntent: () => {}, }, - ], + }, }; export default config; diff --git a/Composer/packages/ui-plugins/luis/src/index.ts b/Composer/packages/ui-plugins/luis/src/index.ts index 36c1dc52fc..732ae29f64 100644 --- a/Composer/packages/ui-plugins/luis/src/index.ts +++ b/Composer/packages/ui-plugins/luis/src/index.ts @@ -8,33 +8,38 @@ import formatMessage from 'format-message'; import { LuisIntentEditor } from './LuisIntentEditor'; const config: PluginConfig = { - recognizers: [ - { - id: SDKKinds.LuisRecognizer, - displayName: formatMessage('LUIS'), - editor: LuisIntentEditor, - isSelected: (data) => { - return typeof data === 'string' && data.endsWith('.lu'); - }, - handleRecognizerChange: (props, shellData) => { - const { luFiles, currentDialog, locale } = shellData; - const luFile = luFiles.find((f) => f.id === `${currentDialog.id}.${locale}`); + uiSchema: { + [SDKKinds.LuisRecognizer]: { + recognizer: { + disabled: true, + displayName: () => formatMessage('LUIS'), + intentEditor: LuisIntentEditor, + isSelected: (data) => { + return typeof data === 'string' && data.endsWith('.lu'); + }, + seedNewRecognizer: (shellData) => { + const { luFiles, currentDialog, locale } = shellData; + const luFile = luFiles.find((f) => f.id === `${currentDialog.id}.${locale}`); - if (luFile) { - // strip locale out of id so it doesn't get serialized - // into the .dialog file - props.onChange(`${luFile.id.split('.')[0]}.lu`); - } else { - alert(formatMessage(`NO LU FILE WITH NAME {id}`, { id: currentDialog.id })); - } - }, - renameIntent: async (intentName, newIntentName, shellData, shellApi) => { - const { currentDialog, locale } = shellData; - shellApi.updateIntentTrigger(currentDialog.id, intentName, newIntentName); - await shellApi.renameLuIntent(`${currentDialog.id}.${locale}`, intentName, newIntentName); + if (!luFile) { + alert(formatMessage(`NO LU FILE WITH NAME {id}`, { id: currentDialog.id })); + return ''; + } + + try { + return `${luFile.id.split('.')[0]}.lu`; + } catch (err) { + return ''; + } + }, + renameIntent: async (intentName, newIntentName, shellData, shellApi) => { + const { currentDialog, locale } = shellData; + shellApi.updateIntentTrigger(currentDialog.id, intentName, newIntentName); + await shellApi.renameLuIntent(`${currentDialog.id}.${locale}`, intentName, newIntentName); + }, }, }, - ], + }, }; export default config; diff --git a/Composer/packages/ui-plugins/prompts/src/PromptField/UserInput.tsx b/Composer/packages/ui-plugins/prompts/src/PromptField/UserInput.tsx index e6b481d525..3f781c2932 100644 --- a/Composer/packages/ui-plugins/prompts/src/PromptField/UserInput.tsx +++ b/Composer/packages/ui-plugins/prompts/src/PromptField/UserInput.tsx @@ -32,21 +32,15 @@ const expectedResponsesPlaceholder = () => const UserInput: React.FC> = (props) => { const { onChange, getSchema, value, id, uiOptions, getError, definitions, depth, schema = {} } = props; - const { currentDialog, designerId } = useShellApi(); - const recognizers = useRecognizerConfig(); + const { designerId } = useShellApi(); + const { currentRecognizer } = useRecognizerConfig(); const { const: $kind } = (schema?.properties?.$kind as { const: string }) || {}; const intentName = new LuMetaData(new LuType($kind).toString(), designerId).toString(); - - const recognizer = recognizers.find((r) => r.isSelected(currentDialog?.content?.recognizer)); - let Editor; - if (recognizer && recognizer.id === SDKKinds.CrossTrainedRecognizerSet) { - Editor = recognizers.find((r) => r.id === SDKKinds.LuisRecognizer)?.editor; - } else { - Editor = recognizer?.editor; - } const intentLabel = formatMessage('Expected responses (intent: #{intentName})', { intentName }); + const Editor = currentRecognizer?.intentEditor; + return (