From 3424f7887ffffdfd4c89789e9acec596ff063016 Mon Sep 17 00:00:00 2001 From: Ben Brown Date: Mon, 24 Feb 2020 12:15:12 -0600 Subject: [PATCH 01/40] fix: replace animated screenshots with a static screenshot (#2045) --- .../deployment/deploy-create-output.png | Bin 0 -> 202448 bytes .../deployment/deploy-deploy-output.png | Bin 0 -> 134789 bytes .../deployment/deployWizardStep-getCreate.js | 4 ++-- .../deployment/deployWizardStep-getDeploy.js | 4 ++-- 4 files changed, 4 insertions(+), 4 deletions(-) create mode 100644 Composer/packages/client/src/pages/setting/deployment/deploy-create-output.png create mode 100644 Composer/packages/client/src/pages/setting/deployment/deploy-deploy-output.png diff --git a/Composer/packages/client/src/pages/setting/deployment/deploy-create-output.png b/Composer/packages/client/src/pages/setting/deployment/deploy-create-output.png new file mode 100644 index 0000000000000000000000000000000000000000..ed9c82e38f6a31ce9f1add408e2deb456b040f79 GIT binary patch literal 202448 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

_J!b_wwqQyry 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 b`uP6=)v2 { const { state } = useContext(StoreContext); @@ -57,7 +57,7 @@ export const DeployWizardStep2 = props => { {formatMessage('Animation diff --git a/Composer/packages/client/src/pages/setting/deployment/deployWizardStep-getDeploy.js b/Composer/packages/client/src/pages/setting/deployment/deployWizardStep-getDeploy.js index 5697cfc81d..f9b18fbee1 100644 --- a/Composer/packages/client/src/pages/setting/deployment/deployWizardStep-getDeploy.js +++ b/Composer/packages/client/src/pages/setting/deployment/deployWizardStep-getDeploy.js @@ -9,7 +9,7 @@ import { DialogFooter } from 'office-ui-fabric-react/lib/Dialog'; import { TextField } from 'office-ui-fabric-react/lib/TextField'; import { styles } from './styles'; -import processGif from './deploy-deploy-output.gif'; +import processGif from './deploy-deploy-output.png'; export const DeployWizardStep3 = props => { const { closeModal, botValues } = props; @@ -50,7 +50,7 @@ export const DeployWizardStep3 = props => { {formatMessage('Animation From 1b14a18bd3e1ad9543426ce1c6f43be76d5df0cb Mon Sep 17 00:00:00 2001 From: Zhixiang Zhan Date: Wed, 26 Feb 2020 01:22:23 +0800 Subject: [PATCH 02/40] feat: support inline LU section editing (#1994) * update lu sample * inline lu * update * refactor lu page, enable inline editor * move test * nested section editing * check for nestedSection * update * if file not found, redirect * update inline diagnostic * entity suggestion in inline * fix inline tokenizer * code refine --- .../pages/language-generation/code-editor.tsx | 4 +- .../src/pages/language-generation/index.tsx | 11 +- .../language-understanding/code-editor.js | 66 --- .../language-understanding/code-editor.tsx | 171 +++++++ .../pages/language-understanding/index.tsx | 104 ++-- .../language-understanding/table-view.tsx | 58 ++- Composer/packages/client/src/router.tsx | 3 +- .../packages/client/src/store/action/lu.ts | 65 ++- .../client/src/store/reducer/index.ts | 20 +- Composer/packages/client/src/utils/luUtil.ts | 140 +----- .../lib/code-editor/demo/src/index.tsx | 3 +- .../lib/code-editor/demo/src/luEditor.tsx | 446 ++++++++++-------- .../code-editor/demo/src/luInlineEditor.tsx | 40 ++ .../packages/lib/code-editor/src/LuEditor.tsx | 36 +- .../lib/code-editor/src/languages/lu.ts | 3 +- .../lib/indexers/__tests__/luIndexer.test.ts | 6 +- .../indexers/__tests__}/luUtil.test.ts | 97 +++- .../packages/lib/indexers/src/luIndexer.ts | 35 +- Composer/packages/lib/indexers/src/type.ts | 2 + .../lib/indexers/src/types/bf-lu.d.ts | 3 +- .../lib/indexers/src/utils/diagnosticUtil.ts | 5 +- .../packages/lib/indexers/src/utils/luUtil.ts | 183 +++++++ .../ComposerDialogs/Main/Main.lg | 2 +- .../ComposerDialogs/Main/Main.lu | 16 + Composer/packages/server/src/server.ts | 4 +- .../packages/server/src/services/project.ts | 11 + .../language-understanding/package.json | 28 +- .../language-understanding/src/LUServer.ts | 199 ++++++-- .../language-understanding/src/utils.ts | 68 +++ Composer/yarn.lock | 5 - 30 files changed, 1203 insertions(+), 631 deletions(-) delete mode 100644 Composer/packages/client/src/pages/language-understanding/code-editor.js create mode 100644 Composer/packages/client/src/pages/language-understanding/code-editor.tsx create mode 100644 Composer/packages/lib/code-editor/demo/src/luInlineEditor.tsx rename Composer/packages/{client/__tests__/utils => lib/indexers/__tests__}/luUtil.test.ts (85%) create mode 100644 Composer/packages/lib/indexers/src/utils/luUtil.ts create mode 100644 Composer/packages/tools/language-servers/language-understanding/src/utils.ts diff --git a/Composer/packages/client/src/pages/language-generation/code-editor.tsx b/Composer/packages/client/src/pages/language-generation/code-editor.tsx index e7f591b297..fabd5b42f1 100644 --- a/Composer/packages/client/src/pages/language-generation/code-editor.tsx +++ b/Composer/packages/client/src/pages/language-generation/code-editor.tsx @@ -51,10 +51,10 @@ const CodeEditor: React.FC = props => { useEffect(() => { // reset content with file.content's initial state - if (!file || isEmpty(file)) return; + if (!file || isEmpty(file) || content) return; const value = template ? template.body : file.content; setContent(value); - }, [fileId, templateId]); + }, [file, templateId]); useEffect(() => { const currentDiagnostics = inlineMode && template ? filterTemplateDiagnostics(diagnostics, template) : diagnostics; diff --git a/Composer/packages/client/src/pages/language-generation/index.tsx b/Composer/packages/client/src/pages/language-generation/index.tsx index e97feb30d4..01939f236a 100644 --- a/Composer/packages/client/src/pages/language-generation/index.tsx +++ b/Composer/packages/client/src/pages/language-generation/index.tsx @@ -3,7 +3,7 @@ /** @jsx jsx */ import { jsx } from '@emotion/core'; -import React, { useContext, Fragment, useMemo, useCallback, Suspense } from 'react'; +import React, { useContext, Fragment, useMemo, useCallback, Suspense, useEffect } from 'react'; import formatMessage from 'format-message'; import { Toggle } from 'office-ui-fabric-react/lib/Toggle'; import { RouteComponentProps, Router } from '@reach/router'; @@ -36,7 +36,7 @@ const LGPage: React.FC = props => { const { dialogs } = state; const path = props.location?.pathname ?? ''; - const { fileId = 'common' } = props; + const { fileId = '' } = props; const edit = /\/edit(\/)?$/.test(path); const navLinks = useMemo(() => { const newDialogLinks = dialogs.map(dialog => { @@ -57,6 +57,13 @@ const LGPage: React.FC = props => { return newDialogLinks; }, [dialogs]); + useEffect(() => { + const activeDialog = dialogs.find(({ id }) => id === fileId); + if (!activeDialog && dialogs.length) { + navigateTo('/language-generation/common'); + } + }, [fileId, dialogs]); + const onSelect = useCallback( id => { const url = `/language-generation/${id}`; diff --git a/Composer/packages/client/src/pages/language-understanding/code-editor.js b/Composer/packages/client/src/pages/language-understanding/code-editor.js deleted file mode 100644 index a51ba5b456..0000000000 --- a/Composer/packages/client/src/pages/language-understanding/code-editor.js +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -/* eslint-disable react/display-name */ -import React, { useState, useEffect } from 'react'; -import { PropTypes } from 'prop-types'; -import { LuEditor } from '@bfc/code-editor'; -import get from 'lodash/get'; -import debounce from 'lodash/debounce'; -import isEmpty from 'lodash/isEmpty'; -import { combineMessage, isValid } from '@bfc/indexers'; - -export default function CodeEditor(props) { - const { file, errorMsg: updateErrorMsg } = props; - const onChange = debounce(props.onChange, 500); - const diagnostics = get(file, 'diagnostics', []); - const [content, setContent] = useState(get(file, 'content', '')); - - const fileId = file && file.id; - useEffect(() => { - // reset content with file.content's initial state - if (isEmpty(file)) return; - setContent(file.content); - }, [fileId]); - - // local content maybe invalid and should always sync real-time - // file.content assume to be load from server - const _onChange = value => { - setContent(value); - // TODO: validate before request update server like lg, when luParser is ready - onChange(value); - }; - - // diagnostics is load file error, - // updateErrorMsg is save file return error. - const isInvalid = !isValid(file.diagnostics) || updateErrorMsg !== ''; - const errorMsg = isInvalid ? `${combineMessage(diagnostics)}\n ${updateErrorMsg}` : ''; - - return ( - - ); -} - -CodeEditor.propTypes = { - file: PropTypes.object, - onChange: PropTypes.func, - errorMsg: PropTypes.string, -}; diff --git a/Composer/packages/client/src/pages/language-understanding/code-editor.tsx b/Composer/packages/client/src/pages/language-understanding/code-editor.tsx new file mode 100644 index 0000000000..577b47acdc --- /dev/null +++ b/Composer/packages/client/src/pages/language-understanding/code-editor.tsx @@ -0,0 +1,171 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/* eslint-disable react/display-name */ +import React, { useState, useEffect, useMemo, useContext, useCallback } from 'react'; +import { LuEditor } from '@bfc/code-editor'; +import get from 'lodash/get'; +import debounce from 'lodash/debounce'; +import isEmpty from 'lodash/isEmpty'; +import { editor } from '@bfcomposer/monaco-editor/esm/vs/editor/editor.api'; +import { luIndexer, combineMessage, isValid, filterTemplateDiagnostics } from '@bfc/indexers'; +import { RouteComponentProps } from '@reach/router'; +import querystring from 'query-string'; + +import { StoreContext } from '../../store'; +import * as luUtil from '../../utils/luUtil'; + +const { parse } = luIndexer; + +const lspServerPath = '/lu-language-server'; + +interface CodeEditorProps extends RouteComponentProps<{}> { + fileId: string; +} + +const CodeEditor: React.FC = props => { + const { actions, state } = useContext(StoreContext); + const { luFiles } = state; + const { fileId } = props; + const file = luFiles?.find(({ id }) => id === fileId); + const [diagnostics, setDiagnostics] = useState(get(file, 'diagnostics', [])); + const [httpErrorMsg, setHttpErrorMsg] = useState(''); + const [luEditor, setLuEditor] = useState(null); + + const search = props.location?.search ?? ''; + const searchSectionName = querystring.parse(search).t; + const sectionId = Array.isArray(searchSectionName) + ? searchSectionName[0] + : typeof searchSectionName === 'string' + ? searchSectionName + : undefined; + const intent = sectionId && file ? file.intents.find(({ Name }) => Name === sectionId) : undefined; + + const hash = props.location?.hash ?? ''; + const hashLine = querystring.parse(hash).L; + const line = Array.isArray(hashLine) ? +hashLine[0] : typeof hashLine === 'string' ? +hashLine : undefined; + + const inlineMode = !!intent; + const [content, setContent] = useState(intent?.Body || file?.content); + + useEffect(() => { + // reset content with file.content initial state + if (!file || isEmpty(file) || content) return; + const value = intent ? intent.Body : file.content; + setContent(value); + }, [file, sectionId]); + + const errorMsg = useMemo(() => { + const currentDiagnostics = inlineMode && intent ? filterTemplateDiagnostics(diagnostics, intent) : diagnostics; + const isInvalid = !isValid(currentDiagnostics); + return isInvalid ? combineMessage(diagnostics) : httpErrorMsg; + }, [diagnostics, httpErrorMsg]); + + const editorDidMount = (luEditor: editor.IStandaloneCodeEditor) => { + setLuEditor(luEditor); + }; + + useEffect(() => { + if (luEditor && line !== undefined) { + window.requestAnimationFrame(() => { + luEditor.revealLine(line); + luEditor.focus(); + luEditor.setPosition({ lineNumber: line, column: 1 }); + }); + } + }, [line, luEditor]); + + const updateLuIntent = useMemo( + () => + debounce((Body: string) => { + if (!file || !intent) return; + const { Name } = intent; + const payload = { + file, + intentName: Name, + intent: { + Name, + Body, + }, + }; + actions.updateLuIntent(payload); + }, 500), + [file, intent] + ); + + const updateLuFile = useMemo( + () => + debounce((content: string) => { + if (!file) return; + const { id } = file; + const payload = { + id, + content, + }; + actions.updateLuFile(payload); + }, 500), + [file] + ); + + const updateDiagnostics = useMemo( + () => + debounce((value: string) => { + if (!file) return; + const { id } = file; + if (inlineMode) { + if (!intent) return; + const { Name } = intent; + const { content } = file; + try { + const newContent = luUtil.updateIntent(content, Name, { + Name, + Body: value, + }); + const { diagnostics } = parse(newContent, id); + setDiagnostics(diagnostics); + } catch (error) { + setHttpErrorMsg(error.error); + } + } else { + const { diagnostics } = parse(value, id); + setDiagnostics(diagnostics); + } + }, 1000), + [file, intent] + ); + + const _onChange = useCallback( + value => { + setContent(value); + updateDiagnostics(value); + if (!file) return; + if (inlineMode) { + updateLuIntent(value); + } else { + updateLuFile(value); + } + }, + [file, intent] + ); + + const luOption = { + fileId, + sectionId: intent?.Name, + }; + + return ( + + ); +}; + +export default CodeEditor; diff --git a/Composer/packages/client/src/pages/language-understanding/index.tsx b/Composer/packages/client/src/pages/language-understanding/index.tsx index 1d75ce283b..960a8730a7 100644 --- a/Composer/packages/client/src/pages/language-understanding/index.tsx +++ b/Composer/packages/client/src/pages/language-understanding/index.tsx @@ -2,9 +2,10 @@ // Licensed under the MIT License. /** @jsx jsx */ import { jsx } from '@emotion/core'; -import React, { useContext, Fragment, useEffect, useState, useMemo, Suspense } from 'react'; +import React, { useContext, Fragment, useMemo, Suspense, useCallback, useEffect } from 'react'; import formatMessage from 'format-message'; import { Toggle } from 'office-ui-fabric-react/lib/Toggle'; +import { RouteComponentProps, Router } from '@reach/router'; import { StoreContext } from '../../store'; import { projectContainer } from '../design/styles'; @@ -26,20 +27,18 @@ import { } from './styles'; const CodeEditor = React.lazy(() => import('./code-editor')); -interface DefineConversationProps { +interface LUPageProps extends RouteComponentProps<{}> { + fileId?: string; path: string; } -const LUPage: React.FC = props => { - const { state, actions } = useContext(StoreContext); - const { luFiles, dialogs } = state; - const [editMode, setEditMode] = useState(false); - const [errorMsg, setErrorMsg] = useState(''); - const fileId = props['*']; - const isRoot = fileId === ''; - const activeDialog = dialogs.find(item => item.id === fileId); - - const luFile = luFiles.length && activeDialog ? luFiles.find(luFile => luFile.id === activeDialog.id) : null; +const LUPage: React.FC = props => { + const { state } = useContext(StoreContext); + const { dialogs } = state; + const path = props.location?.pathname ?? ''; + const { fileId = '' } = props; + const edit = /\/edit(\/)?$/.test(path); + const isRoot = fileId === 'all'; const navLinks = useMemo(() => { const newDialogLinks = dialogs.map(dialog => { @@ -55,49 +54,28 @@ const LUPage: React.FC = props => { }, [dialogs]); useEffect(() => { - // root view merge all lu file into one list, we can't edit multi file. - if (isRoot) { - setEditMode(false); - } - - // fall back to the all-up page if we don't have an active dialog - if (!isRoot && !activeDialog && dialogs.length) { - navigateTo('/language-understanding'); + const activeDialog = dialogs.find(({ id }) => id === fileId); + if (!activeDialog && fileId !== 'all' && dialogs.length) { + navigateTo('/language-understanding/all'); } }, [fileId, dialogs]); - useEffect(() => { - setErrorMsg(''); - }, [luFile]); - - function onSelect(id) { - if (id === '_all') { - navigateTo('/language-understanding'); - } else { - navigateTo(`/language-understanding/${id}`); - } - setEditMode(false); - } - - async function onChange(newContent: string) { - const id = activeDialog ? activeDialog.id : undefined; - const payload = { - id: id, // current opened lu file - content: newContent, - }; - try { - await actions.updateLuFile(payload); - } catch (error) { - setErrorMsg(error.message); - } - } + const onSelect = useCallback( + id => { + const url = `/language-understanding/${id}`; + navigateTo(url); + }, + [edit] + ); - // #TODO: get line number from lu parser, then deep link to code editor this - // Line - function onTableViewClickEdit({ fileId = '' }) { - navigateTo(`language-understanding/${fileId}`); - setEditMode(true); - } + const onToggleEditMode = useCallback( + (_e, checked) => { + let url = `/language-understanding/${fileId}`; + if (checked) url += `/edit`; + navigateTo(url); + }, + [fileId] + ); const toolbarItems = [ { @@ -118,19 +96,20 @@ const LUPage: React.FC = props => { css={actionButton} onText={formatMessage('Edit mode')} offText={formatMessage('Edit mode')} - checked={editMode} - disabled={isRoot && editMode === false} - onChange={() => setEditMode(!editMode)} + defaultChecked={false} + checked={!!edit} + disabled={isRoot && edit === false} + onChange={onToggleEditMode} />

{ - onSelect('_all'); + onSelect('all'); }} > {'All'} @@ -138,13 +117,12 @@ const LUPage: React.FC = props => {
- {editMode ? ( - }> - - - ) : ( - - )} + }> + + + + +
diff --git a/Composer/packages/client/src/pages/language-understanding/table-view.tsx b/Composer/packages/client/src/pages/language-understanding/table-view.tsx index 6548ee126d..b04afef158 100644 --- a/Composer/packages/client/src/pages/language-understanding/table-view.tsx +++ b/Composer/packages/client/src/pages/language-understanding/table-view.tsx @@ -16,15 +16,15 @@ import { ScrollablePane, ScrollbarVisibility } from 'office-ui-fabric-react/lib/ import { Sticky, StickyPositionType } from 'office-ui-fabric-react/lib/Sticky'; import formatMessage from 'format-message'; import { NeutralColors, FontSizes } from '@uifabric/fluent-theme'; -import { isValid, DialogInfo, LuFile } from '@bfc/indexers'; +import { isValid, LuFile } from '@bfc/indexers'; +import { RouteComponentProps } from '@reach/router'; import { StoreContext } from '../../store'; import { navigateTo } from '../../utils'; import { formCell, luPhraseCell } from './styles'; -interface TableViewProps { - activeDialog: DialogInfo | undefined; - onClickEdit: ({ fileId: string }) => void; +interface TableViewProps extends RouteComponentProps<{}> { + fileId: string; } interface Intent { @@ -38,16 +38,34 @@ interface Intent { const TableView: React.FC = props => { const { state } = useContext(StoreContext); const { dialogs, luFiles } = state; - const { activeDialog, onClickEdit } = props; + const { fileId } = props; + const activeDialog = dialogs.find(({ id }) => id === fileId); + const [intents, setIntents] = useState([]); const listRef = useRef(null); + function checkErrors(files: LuFile[]): LuFile[] { + return files.filter(file => !isValid(file.diagnostics)); + } + + function getIntentState(file: LuFile): string { + if (!file.diagnostics) { + return formatMessage('Error'); + } else if (file.status && file.status.lastUpdateTime >= file.status.lastPublishTime) { + return formatMessage('Not yet published'); + } else if (file.status && file.status.lastPublishTime > file.status.lastUpdateTime) { + return formatMessage('Published'); + } else { + return formatMessage('Unknown State'); // It's a bug in most cases. + } + } + useEffect(() => { if (isEmpty(luFiles)) return; const errorFiles = checkErrors(luFiles); if (errorFiles.length !== 0) { - onClickEdit({ fileId: errorFiles[0].id }); + navigateTo(`/language-understanding/${errorFiles[0].id}/edit`); return; } @@ -76,29 +94,14 @@ const TableView: React.FC = props => { } }, [luFiles, activeDialog]); - function checkErrors(files: LuFile[]): LuFile[] { - return files.filter(file => !isValid(file.diagnostics)); - } - - function getIntentState(file: LuFile): string { - if (!file.diagnostics) { - return formatMessage('Error'); - } else if (file.status && file.status.lastUpdateTime >= file.status.lastPublishTime) { - return formatMessage('Not yet published'); - } else if (file.status && file.status.lastPublishTime > file.status.lastUpdateTime) { - return formatMessage('Published'); - } else { - return formatMessage('Unknown State'); // It's a bug in most cases. - } - } - const getTemplatesMoreButtons = (item, index): IContextualMenuItem[] => { const buttons = [ { key: 'edit', name: 'Edit', onClick: () => { - onClickEdit(intents[index]); + const { name, fileId } = intents[index]; + navigateTo(`/language-understanding/${fileId}/edit?t=${encodeURIComponent(name)}`); }, }, ]; @@ -114,8 +117,13 @@ const TableView: React.FC = props => { minWidth: 100, maxWidth: 150, data: 'string', - onRender: item => { - return
#{item.name}
; + onRender: (item: Intent) => { + let displayName = `#${item.name}`; + if (item.name.includes('/')) { + const [, childName] = item.name.split('/'); + displayName = `##${childName}`; + } + return
{displayName}
; }, }, { diff --git a/Composer/packages/client/src/router.tsx b/Composer/packages/client/src/router.tsx index e8271517fb..a730f121b9 100644 --- a/Composer/packages/client/src/router.tsx +++ b/Composer/packages/client/src/router.tsx @@ -48,7 +48,8 @@ const Routes = props => { - + + diff --git a/Composer/packages/client/src/store/action/lu.ts b/Composer/packages/client/src/store/action/lu.ts index 9c9b3f1ddb..63639811c1 100644 --- a/Composer/packages/client/src/store/action/lu.ts +++ b/Composer/packages/client/src/store/action/lu.ts @@ -1,28 +1,50 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +import clonedeep from 'lodash/cloneDeep'; +import debounce from 'lodash/debounce'; -import { ActionCreator } from '../types'; +import * as luUtil from '../../utils/luUtil'; +import { undoable } from '../middlewares/undo'; +import { ActionCreator, State } from '../types'; -import { ActionTypes } from './../../constants/index'; import httpClient from './../../utils/httpUtil'; - -export const updateLuFile: ActionCreator = async ({ dispatch }, { id, content }) => { +import { ActionTypes } from './../../constants/index'; +import { fetchProject } from './project'; +import { setError } from './error'; +//remove editor's debounce and add it to action +export const debouncedUpdateLu = debounce(async (store, id, content) => { try { - const response = await httpClient.put(`/projects/opened/luFiles/${id}`, { id, content }); - dispatch({ - type: ActionTypes.UPDATE_LU_SUCCESS, - payload: { response }, - }); + await httpClient.put(`/projects/opened/luFiles/${id}`, { id, content }); } catch (err) { - dispatch({ - type: ActionTypes.UPDATE_LU_FAILURE, - payload: null, - error: err, + setError(store, { + message: err.response && err.response.data.message ? err.response.data.message : err, + summary: 'UPDATE LU ERROR', }); - throw new Error(err.response.data.message); + //if update lu error, do a full refresh. + fetchProject(store); } +}, 500); + +export const updateLuFile: ActionCreator = async (store, { id, content }) => { + store.dispatch({ type: ActionTypes.UPDATE_LU_SUCCESS, payload: { id, content } }); + debouncedUpdateLu(store, id, content); }; +export const undoableUpdateLuFile = undoable( + updateLuFile, + (state: State, args: any[], isEmpty) => { + if (isEmpty) { + const id = args[0].id; + const content = clonedeep(state.luFiles.find(luFile => luFile.id === id)?.content); + return [{ id, content }]; + } else { + return args; + } + }, + updateLuFile, + updateLuFile +); + export const createLuFile: ActionCreator = async ({ dispatch }, { id, content }) => { try { const response = await httpClient.post(`/projects/opened/luFiles`, { id, content }); @@ -57,6 +79,21 @@ export const removeLuFile: ActionCreator = async ({ dispatch }, { id }) => { } }; +export const updateLuIntent: ActionCreator = async (store, { file, intentName, intent }) => { + const newContent = luUtil.updateIntent(file.content, intentName, intent); + return await undoableUpdateLuFile(store, { id: file.id, content: newContent }); +}; + +export const createLuIntent: ActionCreator = async (store, { file, intent }) => { + const newContent = luUtil.addIntent(file.content, intent); + return await undoableUpdateLuFile(store, { id: file.id, content: newContent }); +}; + +export const removeLuIntent: ActionCreator = async (store, { file, intentName }) => { + const newContent = luUtil.removeIntent(file.content, intentName); + return await undoableUpdateLuFile(store, { id: file.id, content: newContent }); +}; + export const publishLuis: ActionCreator = async ({ dispatch }, authoringKey) => { try { const response = await httpClient.post(`/projects/opened/luFiles/publish`, { authoringKey }); diff --git a/Composer/packages/client/src/store/reducer/index.ts b/Composer/packages/client/src/store/reducer/index.ts index 09177cc313..79cb94e55a 100644 --- a/Composer/packages/client/src/store/reducer/index.ts +++ b/Composer/packages/client/src/store/reducer/index.ts @@ -5,7 +5,7 @@ import get from 'lodash/get'; import set from 'lodash/set'; import { dialogIndexer } from '@bfc/indexers'; import { SensitiveProperties } from '@bfc/shared'; -import { Diagnostic, DiagnosticSeverity, LgTemplate, lgIndexer } from '@bfc/indexers'; +import { Diagnostic, DiagnosticSeverity, LgTemplate, lgIndexer, luIndexer } from '@bfc/indexers'; import { ImportResolverDelegate } from 'botbuilder-lg'; import { ActionTypes, FileTypes } from '../../constants'; @@ -140,8 +140,22 @@ const updateLgTemplate: ReducerFunc = (state, { id, content }) => { return state; }; -const updateLuTemplate: ReducerFunc = (state, { response }) => { - state.luFiles = response.data.luFiles; +const updateLuTemplate: ReducerFunc = (state, { id, content }) => { + const luFiles = state.luFiles.map(luFile => { + if (luFile.id === id) { + luFile.content = content; + return luFile; + } + return luFile; + }); + + state.luFiles = luFiles.map(luFile => { + const { parse } = luIndexer; + const { id, content } = luFile; + const { intents, diagnostics } = parse(content, id); + return { ...luFile, intents, diagnostics, content }; + }); + return state; }; diff --git a/Composer/packages/client/src/utils/luUtil.ts b/Composer/packages/client/src/utils/luUtil.ts index 14f9ba5951..30023da7a5 100644 --- a/Composer/packages/client/src/utils/luUtil.ts +++ b/Composer/packages/client/src/utils/luUtil.ts @@ -7,146 +7,8 @@ * for more usage detail, please check client/__tests__/utils/luUtil.test.ts */ -import { sectionHandler } from '@bfcomposer/bf-lu/lib/parser'; import { LuFile, DialogInfo } from '@bfc/indexers'; -import isEmpty from 'lodash/isEmpty'; - -const { luParser, sectionOperator } = sectionHandler; - -export interface LuIntent { - Name: string; - Body: string; -} - -const NEWLINE = '\n'; - -export function textFromIntent(intent: LuIntent | null, secondary = false): string { - if (!intent || isEmpty(intent)) return ''; - const { Name, Body } = intent; - const textBuilder: string[] = []; - if (Name && Body) { - if (secondary) { - textBuilder.push(`## ${Name.trim()}`); - } else { - textBuilder.push(`# ${Name.trim()}`); - } - textBuilder.push(Body); - } - return textBuilder.join(NEWLINE); -} - -export function textFromIntents(intents: LuIntent[], secondary = false): string { - return intents.map(intent => textFromIntent(intent, secondary)).join(`${NEWLINE}${NEWLINE}`); -} - -function updateInSections(sections: LuIntent[], intentName: string, updatedIntent: LuIntent | null): LuIntent[] { - // remove - if (!updatedIntent || isEmpty(updatedIntent)) { - return sections.filter(({ Name }) => Name !== intentName); - } - // add - if (sections.findIndex(({ Name }) => Name === intentName) === -1) { - sections.push(updatedIntent); - return sections; - } - // update - return sections.map(section => { - if (section.Name === intentName) { - return updatedIntent; - } - return section; - }); -} - -/** - * - * @param content origin lu file content - * @param intentName intent Name, support subSection naming 'CheckEmail/CheckUnreadEmail'. if #CheckEmail not exist will do recursive add. - * @param {Name, Body} intent the updates. if intent is empty will do remove. - */ -export function updateIntent(content: string, intentName: string, intent: LuIntent | null): string { - let targetSection; - let targetSectionContent; - const updatedSectionContent = textFromIntent(intent); - const resource = luParser.parse(content); - const { Sections } = resource; - - // if intent is null, do remove - // if remove target not exist return origin content; - if (!intent || isEmpty(intent)) { - if (intentName.includes('/')) { - const [parrentName, childName] = intentName.split('/'); - const targetChildSection = Sections.find(({ Name }) => Name === parrentName)?.SimpleIntentSections.find( - ({ Name }) => Name === childName - ); - if (!targetChildSection) { - return content; - } - } else { - const targetSection = Sections.find(({ Name }) => Name === intentName); - if (targetSection) { - return new sectionOperator(resource).deleteSection(targetSection.Id).Content; - } - return content; - } - } - - // nestedSection name path - if (intentName.includes('/')) { - const [parrentName, childName] = intentName.split('/'); - targetSection = Sections.find(({ Name }) => Name === parrentName); - - if (targetSection) { - const updatedSections = updateInSections(targetSection.SimpleIntentSections, childName, intent); - targetSectionContent = textFromIntent({ Name: targetSection.Name, Body: textFromIntents(updatedSections, true) }); - } else { - targetSectionContent = textFromIntent({ Name: parrentName, Body: textFromIntent(intent, true) }); - } - } else { - targetSection = Sections.find(({ Name }) => Name === intentName); - targetSectionContent = updatedSectionContent; - } - - // update - if (targetSection) { - return new sectionOperator(resource).updateSection(targetSection.Id, targetSectionContent).Content; - // add if not exist - } else { - return new sectionOperator(resource).addSection(['', targetSectionContent].join(NEWLINE)).Content; - } -} - -/** - * - * @param content origin lu file content - * @param {Name, Body} intent the adds. Name support subSection naming 'CheckEmail/CheckUnreadEmail', if #CheckEmail not exist will do recursive add. - */ -export function addIntent(content: string, { Name, Body }: LuIntent): string { - const intentName = Name; - if (Name.includes('/')) { - const [, childName] = Name.split('/'); - Name = childName; - } - return updateIntent(content, intentName, { Name, Body }); -} - -/** - * - * @param content origin lu file content - * @param intentName the remove intentName. Name support subSection naming 'CheckEmail/CheckUnreadEmail', if any of them not exist will do nothing. - */ -export function removeIntent(content: string, intentName: string): string { - if (intentName.includes('/')) { - return updateIntent(content, intentName, null); - } - const resource = luParser.parse(content); - const { Sections } = resource; - const targetSection = Sections.find(({ Name }) => Name === intentName); - if (targetSection) { - return new sectionOperator(resource).deleteSection(targetSection.Id).Content; - } - return content; -} +export * from '@bfc/indexers/lib/utils/luUtil'; export function getReferredFiles(luFiles: LuFile[], dialogs: DialogInfo[]) { return luFiles.filter(file => { diff --git a/Composer/packages/lib/code-editor/demo/src/index.tsx b/Composer/packages/lib/code-editor/demo/src/index.tsx index db9db561d6..75522e4417 100644 --- a/Composer/packages/lib/code-editor/demo/src/index.tsx +++ b/Composer/packages/lib/code-editor/demo/src/index.tsx @@ -6,7 +6,8 @@ import ReactDOM from 'react-dom'; // import App from './lgEditor'; // import App from './jsonEditor'; -import App from './luEditor'; +// import App from './luEditor'; +import App from './luInlineEditor'; // import App from './lgJsonEditor'; // import App from './inlineEditor'; // import App from './multiEditors'; diff --git a/Composer/packages/lib/code-editor/demo/src/luEditor.tsx b/Composer/packages/lib/code-editor/demo/src/luEditor.tsx index 6dedb51fac..d27f03184e 100644 --- a/Composer/packages/lib/code-editor/demo/src/luEditor.tsx +++ b/Composer/packages/lib/code-editor/demo/src/luEditor.tsx @@ -5,214 +5,212 @@ import React, { useState } from 'react'; import { LuEditor } from '../../src'; -const content = `> Entity definitions -$ itemTitle : simple - -> Add a Phrase List with todo variations. Mark them as interchangeable. -$ todoItem : PhraseList interchangeable - - todo - - todos - - to dos - - todo list - - todos list - - item list - - items collection - - collection - - list - -> Add a list entity to detect list tye -$ listType : todo = -- to do -- todos -- laundry - -$ listType : shopping = -- shopping -- shop -- shoppers - -$ listType : grocery = -- groceries -- fruits -- vegetables -- household items -- house hold items - -> Help intent and related utterances. -# Help -- What can I say? -- Who are you? -- I need help -- Not sure what I can do -- What do you want me to say? -- What can you do? -- What can you help with? -- help please -- What are my options? -- well, I do not know what my todo's title is -- I do not know -- can you please help? - -> Greeting intent and related utterances +const content = `> ## Intent definition # Greeting - hi - hello -- hiya -- how are you? -- how do you do? - -> Cancel intent and related utterances -# Cancel -- cancel -- cancel all -- stop that -- do not do it -- abort -- please stop what you are doing -- I changed my mind -- cancel add todo -- cancel that -- I do not want to add a todo -- No todo for me -- Cancel this -- cancel delete todo -- Let's just leave it as is -- I do not wish to delete my todo anymore -- Keep my todos as is -- No todo for me - -> Add item -# AddItem -- Add todo -- add a to do item -- Please remind me to {itemTitle=buy milk} -- Please remember that I need to {itemTitle=buy milk} -- I need you to remember that {itemTitle=my wife's birthday is Jan 9th} -- Add a todo named {itemTitle=send report over this weekend} -- Add {itemTitle=get a new car} to the todo list -- Add {itemTitle=write a spec} to the list -- Add {itemTitle=finish this demo} to my todo list -- add a todo item {itemTitle=vacuuming by october 3rd} -- add {itemTitle=call my mother} to my todo list -- add {itemTitle=due date august to peanut butter jelly bread milk} on todos list -- add {itemTitle=go running} to my todos -- add to my todos list {itemTitle=mail the insurance forms out by saturday} -- can i add {itemTitle=shirts} on the todos list -- could i add {itemTitle=medicine} to the todos list -- would you add {itemTitle=heavy cream} to the todos list -- add {itemTitle} to my todo list -- add a to do that {itemTitle=purchase a nice sweater} -- add a to do to {itemTitle=buy shoes} -- add an task of {itemTitle=chores to do around the house} -- add {itemTitle=go to whole foods} in my to do list -- add {itemTitle=reading} to my to do list -- add this thing in to do list -- add to my to do list {itemTitle=pick up clothes} -- add to my to do list {itemTitle=print papers for 10 copies this afternoon} -- create to do -- create to do that {itemTitle=read a book tonight} -- create to do to {itemTitle=go running in the park} -- put {itemTitle=hikes} on my to do list -- remind me to {itemTitle = pick up dry cleaning} -- new to do -- add another one -- add -> Add patterns -- Please remember [to] {itemTitle} -- I need you to remember [that] {itemTitle} -- Add a todo named {itemTitle} -- Add {itemTitle} to the list -- [Please] add {itemTitle} to the todo list -- Add {itemTitle} to my todo -- add {itemTitle} to my to do list -- add {itemTitle} to my to dos -- add a to do that buy {itemTitle} -- add a to do that purchase {itemTitle} -- add a to do that shop {itemTitle} -- add a to do to {itemTitle} -- add to do that {itemTitle} -- add to do to {itemTitle} -- create a to do to {itemTitle} -- create to do to {itemTitle} -- remind me to {itemTitle} - -# DeleteItem -- Remove todo -- Mark {itemTitle = buy milk} as complete -- Flag {itemTitle = first one} as done -- Remove {itemTitle = finish this demo} from the todo list -- remove another one -- remove -- clear my todo named {itemTitle = get a new car} -- I'm done with the first todo -- I finished the las todo -- Remove the first todo -- Delete todo -- Clear my todos -- Delete all my todos -- Remove all my todo -- Forget the list -- Purge the todo list -- can you delete {itemTitle=todo1} -- can you delete {itemTitle=xxx} item -- delete {itemTitle=eggs} from list -- delete off {itemTitle=pancake mix} on the shopping list -- delete {itemTitle=shampoo} from shopping list -- delete {itemTitle=shirts} from list -- delete task {itemTitle=go fishing} -- delete task {itemTitle=go to cinema tonight} -- delete the item {itemTitle=buy socks} from my todo list -- delete the second task in my shopping list -- delete the task {itemTitle=house cleanup this weekend} -- delete the task that {itemTitle=hit the gym every morning} -- delete the to do {itemTitle=meet my friends tomorrow} -- delete the to do that {itemTitle=daily practice piano} -- delete the to do that {itemTitle=meet john when he come here the next friday} -- delete to do {itemTitle=buy milk} -- delete to do {itemTitle=go shopping} -- delete to do that {itemTitle=go hiking tomorrow} -- erase {itemTitle=bananas} from shopping list -- erase {itemTitle=peanuts} on the shopping list -- remove {itemTitle=asprin} from shopping list -- remove {itemTitle=black shoes} from shopping list -- remove {itemTitle=class} from todo list -- remove {itemTitle=salad vegetables} from grocery list -- remove task {itemTitle=buy dog food} -- remove task {itemTitle=go shopping} -- remove task that {itemTitle=go hiking this weekend} -- remove task that {itemTitle=lawn mowing} -- remove the item {itemTitle=paris} from my list -- remove the task that {itemTitle=go to library after work} -- remove the to do {itemTitle=physical examination} -- remove the to do that {itemTitle=pick tom up at six p.m.} -- remove to do {itemTitle=go to the gym} -- remove to do that {itemTitle=go to the dentist tomorrow morning} -> Add patterns -- I did {itemTitle} -- I completed {itemTitle} -- Delete {itemTitle} -- Mark {itemTitle} as complete -- Remove {itemTitle} from my [todo] list -- [Please] delete {itemTitle} from the list - -# ViewCollection -- show my todo -- can you please show my todos? -- please show my todo list -- todo list please -- I need to see my todo list -- can you show me the list? -- please show the list -- what do i have on my todo? -- what is on my list? -- do i have anything left on my todo list? -- I hope I do not have any todo left -- do i have any tasks left? -- hit me up with more items -- view my todos -- can you show my todo -- see todo -- I would like to see my todos list`; + +> ## Machine learned entity +@ ml name firstName, lastName +# getUserName +- my name is {username=vishwac} + +> Without an explicit entity definition, 'userName' defaults to 'ml' entity type. + +> ## Prebuilt entity +@ prebuilt number numOfGuests +@ prebuilt datetimeV2 fromDate, toDate +@ prebuilt age userAge + +> ## List entity +@ list color favColor, screenColor +@ color = + - red + - deep red + - light red + +> Alternate definition + +@ list color2 favColor2, screenColor2 = + - red + - deep red + - light red + +> ## Composite entity +@ composite deviceTemperature from, to +@ deviceTemperature = + - child1, child2 + +> Alternate definition + +@ composite deviceTemperature from, to = [child1, child2] +# setThermostat +> This utterance labels ‘thermostat to 72’ as composite entity deviceTemperature + - Please set {deviceTemperature = thermostat to 72} +> This is an example utterance that labels ‘owen’ as customDevice (ml entity) and wraps ‘owen to 72’ with the ‘deviceTemperature’ composite entity + - Set {deviceTemperature = {customDevice = owen} to 72} + +> Define a composite entity ‘deviceTemperature’ that has device (list entity), customDevice (ml entity), temperature (pre-built entity) as children + +@ composite deviceTemperature = [device, customDevice, temperature] + +@ list device = + - thermostat : + - Thermostat + - Heater + - AC + - Air conditioner + - refrigterator : + - Fridge + - Cooler + +@ ml customDevice + +@ prebuilt temperature + +> ## Regex entity +@ regex hrf-number from, to +@ hrf-number = /hrf-[0-9]{6}/ + +> Alternate definition + +@ regex hrf-number from, to = /hrf-[0-9]{6}/ + +> ## Roles +> # ml entity definition with roles + +@ ml name role1, role2 + +> this is the same as + +@ ml name hasRoles role1, role2 + +> this is also the same as + +@ ml name +@ name hasRoles role1, role2 + +> Also same as + +@ ml name +@ name hasRole role1 +@ name hasRole role2 + +# AskForUserName +- {userName:firstName=vishwac} {userName:lastName=kannan} +- I'm {userName:firstName=vishwac} +- my first name is {userName:firstName=vishwac} +- {userName=vishwac} is my name + +> This definition is same as including an explicit defintion for userName with 'lastName', 'firstName' as roles + +> @ ml userName hasRoles lastName, firstName + +> In patterns, you can use roles using the {:} notation. Here's an example: +# getUserName +- call me {name:userName} +- I'm {name:userName} +- my name is {name:userName} + +> roles can be specified for list entity types as well - in this case fromCity and toCity are added as roles to the 'city' list entity defined further below + +# BookFlight +- book flight from {city:fromCity} to {city:toCity} +- [can you] get me a flight from {city:fromCity} to {city:toCity} +- get me a flight to {city:toCity} +- I need to fly from {city:fromCity} + +$city:Seattle= +- Seattle +- Tacoma +- SeaTac +- SEA + +$city:Portland= +- Portland +- PDX + +> ## Patterns +# DeleteAlarm +- delete the {alarmTime} alarm +# DeleteAlarm2 +- delete the {alarmTime=7AM} alarm + +> ## Phrase list definition +@ phraseList Want +@ phraseList Want = + - require, need, desire, know + + +> You can also break up the phrase list values into an actual list + +@ phraseList Want = + - require + - need + - desire + - know + + +> ## Utterances +# getUserProfile +- my name is vishwac and I'm 36 years old + - my name is {@userProfile = vishwac and I'm 36 years old} + - my name is {@firstName = vishwac} and I'm 36 years old + - my name is vishwac and I'm {@userAge = 36} years old +- i'm {@userProfile = {@firstName = vishwac}} + +@ ml userProfile + - @personName firstName + - @personName lastName + +@ prebuilt personName + + +> ## External references +> You can include references to other .lu files + +[All LU files](./all.lu) + +> References to other files can have wildcards in them + +[en-us](./en-us/*) + +> References to other lu files can include sub-folders as well. +> /** indicates to the parser to recursively look for .lu files in all subfolders as well. + +[all LU files](../**) + +> You can include deep references to intents defined in a .lu file in utterances + +# None +- [None uttearnces](./all.lu#Help) + +> With the above statement, the parser will parse all.lu and extract out all utterances associated with the 'Help' intent and add them under 'None' intent as defined in this file. + +> NOTE: This **only** works for utterances as entities that are referenced by the uttearnces in the 'Help' intent will not be brought forward to this .lu file. + +# All utterances +> you can use the *utterances* wild card to include all utterances from a lu file. This includes utterances across all intents defined in that .lu file. +- [all.lu](./all.lu#*utterances*) +> you can use the *patterns* wild card to include all patterns from a lu file. +> - [all.lu](./all.lu#*patterns*) +> you can use the *utterancesAndPatterns* wild card to include all utterances and patterns from a lu file. +> - [all.lu](./all.lu#*utterancesAndPatterns*) + +> You can include wild cards with deep references to QnA maker questions defined in a .qna file in utterances + +# None +- [QnA questions](./*#?) + +> With the above statement, the parser will parse **all** .lu files under ./, extract out all questions from QnA pairs in those files and add them under 'None' intent as defined in this file. + +> You can include deep references to QnA maker questions defined in a .qna file in utterances + +# None +- [QnA questions](./qna1.qna#?) + +> With the above statement, the parser will parse qna1.lu and extract out all questions from QnA pairs in that file and add them under 'None' intent as defined in this file. +`; export default function App() { const [value, setValue] = useState(content); @@ -224,10 +222,44 @@ export default function App() { const props = { value, onChange, + // options: { + // lineNumbers: 'on', + // minimap: { + // enabled: true, + // }, + // // minimap: 'on', + // lineDecorationsWidth: undefined, + // lineNumbersMinChars: false, + // glyphMargin: true, + // autoClosingBrackets: 'always', + // autoIndent: true, + // lightbulb: { + // enabled: true, + // }, + // }, languageServer: { - port: 5003, + port: 5000, path: '/lu-language-server', }, }; - return ; + + return ( + + ); } diff --git a/Composer/packages/lib/code-editor/demo/src/luInlineEditor.tsx b/Composer/packages/lib/code-editor/demo/src/luInlineEditor.tsx new file mode 100644 index 0000000000..208a3eec61 --- /dev/null +++ b/Composer/packages/lib/code-editor/demo/src/luInlineEditor.tsx @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import React, { useState } from 'react'; + +import { LuEditor } from '../../src'; + +// body will fill in editor +const intent = { + name: 'Greeting', + body: `- hi +- hello +- hiya +- how are you? +- how do you do?`, +}; + +const luOption = { + fileId: 'Main', + sectionId: 'Greeting', +}; + +export default function App() { + const [value, setValue] = useState(intent.body); + + const onChange = value => { + setValue(value); + }; + + const props = { + value, + onChange, + luOption, + languageServer: { + port: 5000, + path: '/lu-language-server', + }, + }; + return ; +} diff --git a/Composer/packages/lib/code-editor/src/LuEditor.tsx b/Composer/packages/lib/code-editor/src/LuEditor.tsx index 8cfaf3255b..28d2385684 100644 --- a/Composer/packages/lib/code-editor/src/LuEditor.tsx +++ b/Composer/packages/lib/code-editor/src/LuEditor.tsx @@ -17,13 +17,8 @@ const placeholder = `> To learn more about the LU file format, read the document > ${LU_HELP}`; export interface LUOption { - inline: boolean; - content: string; - template?: { - name: string; - parameters?: string[]; - body: string; - }; + fileId: string; + sectionId?: string; } export interface LULSPEditorProps extends RichEditorProps { @@ -69,15 +64,34 @@ function convertEdit(serverEdit: ServerEdit) { }; } +async function initializeDocuments(luOption: LUOption | undefined, uri: string) { + const languageClient = window.monacoLUEditorInstance; + if (languageClient) { + await languageClient.onReady(); + languageClient.sendRequest('initializeDocuments', { uri, luOption }); + } +} + export function LuEditor(props: LULSPEditorProps) { - const options = { + const options: monacoEditor.editor.IEditorConstructionOptions = { quickSuggestions: true, wordBasedSuggestions: false, formatOnType: true, + lineNumbers: 'on', + minimap: { + enabled: true, + }, + lineDecorationsWidth: undefined, + glyphMargin: true, + autoClosingBrackets: 'always', + autoIndent: true, + lightbulb: { + enabled: true, + }, ...props.options, }; - const { languageServer, ...restProps } = props; + const { luOption, languageServer, ...restProps } = props; const luServer = languageServer || defaultLUServer; const editorWillMount = (monaco: typeof monacoEditor) => { @@ -107,6 +121,7 @@ export function LuEditor(props: LULSPEditorProps) { const position = editor.getPosition(); languageClient.sendRequest('labelingExperienceRequest', { uri, position }); }); + initializeDocuments(luOption, uri); languageClient.onReady().then(() => languageClient.onNotification('addUnlabelUtterance', result => { const edits = result.edits.map(e => { @@ -119,6 +134,9 @@ export function LuEditor(props: LULSPEditorProps) { connection.onClose(() => disposable.dispose()); }, }); + } else { + const uri = get(editor.getModel(), 'uri._formatted', ''); + initializeDocuments(luOption, uri); } if (typeof props.editorDidMount === 'function') { diff --git a/Composer/packages/lib/code-editor/src/languages/lu.ts b/Composer/packages/lib/code-editor/src/languages/lu.ts index ac5fb127c7..37f459beac 100644 --- a/Composer/packages/lib/code-editor/src/languages/lu.ts +++ b/Composer/packages/lib/code-editor/src/languages/lu.ts @@ -10,6 +10,7 @@ export function registerLULanguage(monaco: typeof monacoEditor) { [/^\s*#/, { token: 'intent', next: '@intent' }], [/^\s*@/, { token: 'entity-identifier', goBack: 1, next: '@entityMode' }], [/^\s*>\s*[\s\S]*$/, { token: 'comments' }], + [/^\s*-/, { token: 'utterrance-indentifier', next: '@utterrance' }], ], intent: [ @@ -24,7 +25,7 @@ export function registerLULanguage(monaco: typeof monacoEditor) { [/^\s*>\s*[\s\S]*$/, { token: 'comments' }], [/^\s*-/, { token: 'utterrance-indentifier', next: 'utterrance' }], [/^\s*@/, { token: 'entity-identifier', goBack: 1, next: '@entityMode' }], - [/({)(\s*[\w.@:\s]*\s*)(=)(\s*[\w.]*\s*)(})/, ['lb', 'pattern', 'equal', 'entity-name', 'rb']], + [/({)(\s*[\w.@:\s]*\s*)(=)(\s*[\w.\s]*\s*)(})/, ['lb', 'pattern', 'equal', 'entity-name', 'rb']], [/({\s*@)(\s*[\w.]*\s*)(})/, ['lb', 'entity-name', 'rb']], // eslint-disable-next-line security/detect-unsafe-regex [/\s*\[[\w\s.]+\]\(.{1,2}\/[\w.*]+(#[\w.?]+)?\)/, 'import-desc'], diff --git a/Composer/packages/lib/indexers/__tests__/luIndexer.test.ts b/Composer/packages/lib/indexers/__tests__/luIndexer.test.ts index 7cc87dbde9..06f954a42f 100644 --- a/Composer/packages/lib/indexers/__tests__/luIndexer.test.ts +++ b/Composer/packages/lib/indexers/__tests__/luIndexer.test.ts @@ -32,9 +32,11 @@ describe('parse', () => { `; const { intents, diagnostics }: any = parse(content); expect(diagnostics.length).toEqual(0); - expect(intents.length).toEqual(2); - expect(intents[1].Name).toEqual('Greeting'); + expect(intents.length).toEqual(4); expect(intents[0].Name).toEqual('CheckTodo'); + expect(intents[1].Name).toEqual('CheckTodo/CheckUnreadTodo'); + expect(intents[2].Name).toEqual('CheckTodo/CheckDeletedTodo'); + expect(intents[3].Name).toEqual('Greeting'); expect(intents[0].Children.length).toEqual(2); expect(intents[0].Children[0].Name).toEqual('CheckUnreadTodo'); expect(intents[0].Children[0].Entities.length).toEqual(1); diff --git a/Composer/packages/client/__tests__/utils/luUtil.test.ts b/Composer/packages/lib/indexers/__tests__/luUtil.test.ts similarity index 85% rename from Composer/packages/client/__tests__/utils/luUtil.test.ts rename to Composer/packages/lib/indexers/__tests__/luUtil.test.ts index 590b7f8b15..916061b82d 100644 --- a/Composer/packages/client/__tests__/utils/luUtil.test.ts +++ b/Composer/packages/lib/indexers/__tests__/luUtil.test.ts @@ -3,7 +3,7 @@ import { sectionHandler } from '@bfcomposer/bf-lu/lib/parser'; -import { updateIntent, addIntent, removeIntent } from '../../src/utils/luUtil'; +import { updateIntent, addIntent, removeIntent } from '../src/utils/luUtil'; const { luParser, luSectionTypes } = sectionHandler; @@ -79,6 +79,51 @@ describe('LU Section CRUD test', () => { expect(luresource.Sections[1].UtteranceAndEntitiesMap[2].utterance).toEqual('check my mail box please'); }); + it('update section with invalid lu syntax', () => { + const intentName = 'CheckEmail'; + + const validFileContent = `#CheckEmail +- check my email +- show my emails`; + + const validIntent = { + Name: 'CheckEmail', + Body: `- check my email +- show my emails +`, + }; + + const invalidFileContent = `#CheckEmail +- check my email +- show my emails +@`; + + const invalidIntent = { + Name: 'CheckEmail', + Body: `- check my email +- show my emails +@`, + }; + + const invalidIntent4 = { + Name: 'CheckEmail', + Body: `- check my email +- show my emails + +# UnexpectedIntentDefination +- unexpected intent body +`, + }; + // intent invalid + const updatedContent2 = updateIntent(validFileContent, intentName, invalidIntent); + expect(updatedContent2).toEqual(validFileContent); + const updatedContent4 = updateIntent(validFileContent, intentName, invalidIntent4); + expect(updatedContent4).toEqual(validFileContent); + // file invalid + const updatedContent3 = updateIntent(invalidFileContent, intentName, validIntent); + expect(updatedContent3).toEqual(invalidFileContent); + }); + it('delete section test', () => { const intentName = 'CheckEmail'; const fileContentUpdated = removeIntent(fileContent, intentName); @@ -110,6 +155,30 @@ describe('LU Nested Section CRUD test', () => { @ simple todoSubject`; + it('update IntentSection test', () => { + const intentName = 'CheckTodo'; + const intent = { + Name: 'CheckTodo', + Body: `## CheckUnreadTodo + - please check my unread todo + - show my unread todos + + @ simple todoTitle + `, + }; + + const fileContentUpdated = updateIntent(fileContent, intentName, intent); + const luresource = luParser.parse(fileContentUpdated); + const { Sections, Errors } = luresource; + + expect(Errors.length).toEqual(0); + expect(Sections.length).toEqual(2); + expect(Sections[0].SectionType).toEqual(luSectionTypes.MODELINFOSECTION); + expect(Sections[1].SectionType).toEqual(luSectionTypes.NESTEDINTENTSECTION); + expect(Sections[1].Name).toEqual('CheckTodo'); + expect(Sections[1].SimpleIntentSections.length).toEqual(1); + }); + it('parse Nested section test', () => { const luresource = luParser.parse(fileContent); const { Sections, Errors, Content } = luresource; @@ -134,10 +203,10 @@ describe('LU Nested Section CRUD test', () => { const intent = { Name: 'CheckTodo/CheckCompletedTodo', Body: `- check my completed todo -- show my completed todos + - show my completed todos -@ simple todoTime -`, + @ simple todoTime + `, }; const fileContentUpdated = addIntent(fileContent, intent); @@ -164,10 +233,10 @@ describe('LU Nested Section CRUD test', () => { const intent = { Name: 'CheckMyTodo/CheckCompletedTodo', Body: `- check my completed todo -- show my completed todos + - show my completed todos -@ simple todoTime -`, + @ simple todoTime + `, }; const fileContentUpdated = addIntent(fileContent, intent); @@ -196,11 +265,11 @@ describe('LU Nested Section CRUD test', () => { const intent = { Name: 'CheckMyUnreadTodo', Body: `- please check my unread todo -- please show my unread todos + - please show my unread todos -@ simple todoTitle -@ simple todoContent -`, + @ simple todoTitle + @ simple todoContent + `, }; const fileContentUpdated = updateIntent(fileContent, intentName, intent); @@ -236,10 +305,10 @@ describe('LU Nested Section CRUD test', () => { const intent = { Name: 'CheckMyUnreadTodo', Body: `- please check my unread todo -- please show my unread todos + - please show my unread todos -@ simple todoContent -`, + @ simple todoContent + `, }; const fileContentUpdated = updateIntent(fileContent, intentName, intent); diff --git a/Composer/packages/lib/indexers/src/luIndexer.ts b/Composer/packages/lib/indexers/src/luIndexer.ts index 868724893e..49a579bcc0 100644 --- a/Composer/packages/lib/indexers/src/luIndexer.ts +++ b/Composer/packages/lib/indexers/src/luIndexer.ts @@ -2,8 +2,9 @@ // Licensed under the MIT License. import { sectionHandler } from '@bfcomposer/bf-lu/lib/parser'; +import get from 'lodash/get'; -import { FileInfo, LuFile, LuParsed, LuSectionTypes } from './type'; +import { FileInfo, LuFile, LuParsed, LuSectionTypes, LuIntentSection } from './type'; import { getBaseName } from './utils/help'; import { Diagnostic, Position, Range, DiagnosticSeverity } from './diagnostic'; import { FileExtensions } from './utils/fileExtensions'; @@ -28,32 +29,52 @@ function convertLuDiagnostic(d: any, source: string): Diagnostic { function parse(content: string, id = ''): LuParsed { const { Sections, Errors } = luParser.parse(content); - const intents = Sections.map(section => { + const intents: LuIntentSection[] = []; + Sections.forEach(section => { const { Name, Body, SectionType } = section; + const range = { + startLineNumber: get(section, 'ParseTree.start.line', 0), + endLineNumber: get(section, 'ParseTree.stop.line', 0), + }; if (SectionType === LuSectionTypes.SIMPLEINTENTSECTION) { const Entities = section.Entities.map(({ Name }) => Name); - return { + intents.push({ Name, Body, Entities, - }; + range, + }); } else if (SectionType === LuSectionTypes.NESTEDINTENTSECTION) { const Children = section.SimpleIntentSections.map(subSection => { const { Name, Body } = subSection; + const range = { + startLineNumber: get(subSection, 'ParseTree.start.line', 0), + endLineNumber: get(subSection, 'ParseTree.stop.line', 0), + }; const Entities = subSection.Entities.map(({ Name }) => Name); return { Name, Body, Entities, + range, }; }); - return { + intents.push({ Name, Body, Children, - }; + range, + }); + intents.push( + ...Children.map(subSection => { + return { + ...subSection, + Name: `${section.Name}/${subSection.Name}`, + }; + }) + ); } - }).filter(item => item !== undefined); + }); const diagnostics = Errors.map(e => convertLuDiagnostic(e, id)); return { intents, diff --git a/Composer/packages/lib/indexers/src/type.ts b/Composer/packages/lib/indexers/src/type.ts index c02d2d5498..5e9f3d62ce 100644 --- a/Composer/packages/lib/indexers/src/type.ts +++ b/Composer/packages/lib/indexers/src/type.ts @@ -55,6 +55,7 @@ export interface LuParsed { export enum LuSectionTypes { SIMPLEINTENTSECTION = 'simpleIntentSection', NESTEDINTENTSECTION = 'nestedIntentSection', + MODELINFOSECTION = 'modelInfoSection', } export interface LuEntity { @@ -66,6 +67,7 @@ export interface LuIntentSection { Body: string; Entities?: LuEntity[]; Children?: LuIntentSection[]; + range?: CodeRange; } export interface LuFile { diff --git a/Composer/packages/lib/indexers/src/types/bf-lu.d.ts b/Composer/packages/lib/indexers/src/types/bf-lu.d.ts index 7d5544e02c..2383096793 100644 --- a/Composer/packages/lib/indexers/src/types/bf-lu.d.ts +++ b/Composer/packages/lib/indexers/src/types/bf-lu.d.ts @@ -10,6 +10,7 @@ declare module '@bfcomposer/bf-lu/lib/parser' { namespace luParser { function parse(content: string): any; } - function sectionOperator(resource: any): any; + function sectionOperator(resource: any): void; + const luSectionTypes: any; } } diff --git a/Composer/packages/lib/indexers/src/utils/diagnosticUtil.ts b/Composer/packages/lib/indexers/src/utils/diagnosticUtil.ts index 7747a1e04b..41695327c2 100644 --- a/Composer/packages/lib/indexers/src/utils/diagnosticUtil.ts +++ b/Composer/packages/lib/indexers/src/utils/diagnosticUtil.ts @@ -2,7 +2,7 @@ // Licensed under the MIT License. import { Diagnostic, DiagnosticSeverity, Range, Position } from '../diagnostic'; -import { LgTemplate } from '../type'; +import { CodeRange } from '../type'; export function createSingleMessage(d: Diagnostic): string { let msg = `${d.message}\n`; @@ -28,8 +28,7 @@ export function offsetRange(range: Range, offset: number): Range { ); } -export function filterTemplateDiagnostics(diagnostics: Diagnostic[], template: LgTemplate): Diagnostic[] { - const { range } = template; +export function filterTemplateDiagnostics(diagnostics: Diagnostic[], { range }: { range?: CodeRange }): Diagnostic[] { if (!range) return diagnostics; const filteredDiags = diagnostics.filter(d => { return d.range && d.range.start.line >= range.startLineNumber && d.range.end.line <= range.endLineNumber; diff --git a/Composer/packages/lib/indexers/src/utils/luUtil.ts b/Composer/packages/lib/indexers/src/utils/luUtil.ts new file mode 100644 index 0000000000..ef0867686d --- /dev/null +++ b/Composer/packages/lib/indexers/src/utils/luUtil.ts @@ -0,0 +1,183 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * luUtil.ts is a single place use lu-parser handle lu file operation. + * it's designed have no state, input text file, output text file. + * for more usage detail, please check client/__tests__/utils/luUtil.test.ts + */ + +import { sectionHandler } from '@bfcomposer/bf-lu/lib/parser'; +import isEmpty from 'lodash/isEmpty'; + +import { LuIntentSection, LuSectionTypes } from '../type'; +import { luIndexer } from '../luIndexer'; +import { Diagnostic } from '../diagnostic'; +const { parse } = luIndexer; + +const { luParser, sectionOperator } = sectionHandler; + +const NEWLINE = '\n'; + +export function isValid(diagnostics: any[]) { + return diagnostics.every(item => { + item.Severity !== 'ERROR'; + }); +} + +export function textFromIntent(intent: LuIntentSection | null, secondary = false, enableSections = false): string { + if (!intent || isEmpty(intent)) return ''; + let { Name } = intent; + const { Body } = intent; + if (Name.includes('/')) { + const [, childName] = Name.split('/'); + Name = childName; + } + const textBuilder: string[] = []; + if (Name && Body) { + if (secondary) { + textBuilder.push(`## ${Name.trim()}`); + } else { + textBuilder.push(`# ${Name.trim()}`); + } + textBuilder.push(Body); + } + let text = textBuilder.join(NEWLINE); + if (enableSections) { + const nestedSectionDeclareHeader = `> !# @enableSections = true`; + text = [nestedSectionDeclareHeader, text].join('\r\n'); + } + return text; +} + +export function textFromIntents(intents: LuIntentSection[], secondary = false): string { + return intents.map(intent => textFromIntent(intent, secondary)).join(`${NEWLINE}${NEWLINE}`); +} + +export function checkSection(intent: LuIntentSection, enableSections = true): Diagnostic[] { + const text = textFromIntent(intent, false, enableSections); + return parse(text).diagnostics; +} + +export function checkSingleSectionValid(intent: LuIntentSection, enableSections = true): boolean { + const text = textFromIntent(intent, false, enableSections); + const { Sections, Errors } = luParser.parse(text); + return ( + isValid(Errors) && Sections.filter(section => section.SectionType !== LuSectionTypes.MODELINFOSECTION).length === 1 + ); +} + +function updateInSections( + sections: LuIntentSection[], + intentName: string, + updatedIntent: LuIntentSection | null +): LuIntentSection[] { + // remove + if (!updatedIntent || isEmpty(updatedIntent)) { + return sections.filter(({ Name }) => Name !== intentName); + } + // add + if (sections.findIndex(({ Name }) => Name === intentName) === -1) { + sections.push(updatedIntent); + return sections; + } + // update + return sections.map(section => { + if (section.Name === intentName) { + return updatedIntent; + } + return section; + }); +} + +/** + * + * @param content origin lu file content + * @param intentName intent Name, support subSection naming 'CheckEmail/CheckUnreadEmail'. if #CheckEmail not exist will do recursive add. + * @param {Name, Body} intent the updates. if intent is empty will do remove. + */ +export function updateIntent(content: string, intentName: string, intent: LuIntentSection | null): string { + let targetSection; + let targetSectionContent; + const updatedSectionContent = textFromIntent(intent); + const resource = luParser.parse(content); + const { Sections, Errors } = resource; + // if has error, do nothing. + if (intent && checkSingleSectionValid(intent) === false) return content; + if (isValid(Errors) === false) return content; + // if intent is null, do remove + // if remove target not exist return origin content; + if (!intent || isEmpty(intent)) { + if (intentName.includes('/')) { + const [parrentName, childName] = intentName.split('/'); + const targetChildSection = Sections.find(({ Name }) => Name === parrentName)?.SimpleIntentSections.find( + ({ Name }) => Name === childName + ); + if (!targetChildSection) { + return content; + } + } else { + const targetSection = Sections.find(({ Name }) => Name === intentName); + if (targetSection) { + return new sectionOperator(resource).deleteSection(targetSection.Id).Content; + } + return content; + } + } + + // nestedSection name path + if (intentName.includes('/')) { + const [parrentName, childName] = intentName.split('/'); + targetSection = Sections.find(({ Name }) => Name === parrentName); + + if (targetSection) { + const updatedSections = updateInSections(targetSection.SimpleIntentSections, childName, intent); + targetSectionContent = textFromIntent({ Name: targetSection.Name, Body: textFromIntents(updatedSections, true) }); + } else { + targetSectionContent = textFromIntent({ Name: parrentName, Body: textFromIntent(intent, true) }); + } + } else { + targetSection = Sections.find(({ Name }) => Name === intentName); + targetSectionContent = updatedSectionContent; + } + + // update + if (targetSection) { + return new sectionOperator(resource).updateSection(targetSection.Id, targetSectionContent).Content; + // add if not exist + } else { + return new sectionOperator(resource).addSection(['', targetSectionContent].join(NEWLINE)).Content; + } +} + +/** + * + * @param content origin lu file content + * @param {Name, Body} intent the adds. Name support subSection naming 'CheckEmail/CheckUnreadEmail', if #CheckEmail not exist will do recursive add. + */ +export function addIntent(content: string, { Name, Body }: LuIntentSection): string { + const intentName = Name; + if (Name.includes('/')) { + const [, childName] = Name.split('/'); + Name = childName; + } + return updateIntent(content, intentName, { Name, Body }); +} + +/** + * + * @param content origin lu file content + * @param intentName the remove intentName. Name support subSection naming 'CheckEmail/CheckUnreadEmail', if any of them not exist will do nothing. + */ +export function removeIntent(content: string, intentName: string): string { + if (intentName.includes('/')) { + return updateIntent(content, intentName, null); + } + const resource = luParser.parse(content); + const { Sections } = resource; + const targetSection = Sections.find(({ Name }) => Name === intentName); + if (targetSection) { + return new sectionOperator(resource).deleteSection(targetSection.Id).Content; + } + return content; +} diff --git a/Composer/packages/server/assets/projects/ToDoBotWithLuisSample/ComposerDialogs/Main/Main.lg b/Composer/packages/server/assets/projects/ToDoBotWithLuisSample/ComposerDialogs/Main/Main.lg index 0bde7283e2..93d54c6f8e 100644 --- a/Composer/packages/server/assets/projects/ToDoBotWithLuisSample/ComposerDialogs/Main/Main.lg +++ b/Composer/packages/server/assets/projects/ToDoBotWithLuisSample/ComposerDialogs/Main/Main.lg @@ -67,4 +67,4 @@ [Activity Text = @{Items-readback()} @{Welcome-Actions()} -] \ No newline at end of file +] diff --git a/Composer/packages/server/assets/projects/ToDoBotWithLuisSample/ComposerDialogs/Main/Main.lu b/Composer/packages/server/assets/projects/ToDoBotWithLuisSample/ComposerDialogs/Main/Main.lu index f3f0e28c37..4aeb05580c 100644 --- a/Composer/packages/server/assets/projects/ToDoBotWithLuisSample/ComposerDialogs/Main/Main.lu +++ b/Composer/packages/server/assets/projects/ToDoBotWithLuisSample/ComposerDialogs/Main/Main.lu @@ -1,3 +1,5 @@ +> !# @enableSections = true + > Entity definitions $ itemTitle : simple @@ -31,6 +33,20 @@ $ listType : grocery = - household items - house hold items +> Sample for nested section +# CheckTodo +## CheckUnreadTodo +- check my unread todo +- show my unread todos + +@ simple todoTitle + +## CheckDeletedTodo +- check my deleted todo +- show my deleted todos + +@ simple todoSubject + > Help intent and related utterances. # Help - What can I say? diff --git a/Composer/packages/server/src/server.ts b/Composer/packages/server/src/server.ts index 8324009dfe..55a4b73cd0 100644 --- a/Composer/packages/server/src/server.ts +++ b/Composer/packages/server/src/server.ts @@ -115,7 +115,7 @@ const wss: ws.Server = new ws.Server({ perMessageDeflate: false, }); -const { lgImportResolver, staticMemoryResolver } = BotProjectService; +const { lgImportResolver, luImportResolver, staticMemoryResolver } = BotProjectService; function launchLanguageServer(socket: rpc.IWebSocket) { const reader = new rpc.WebSocketMessageReader(socket); @@ -129,7 +129,7 @@ function launchLuLanguageServer(socket: rpc.IWebSocket) { const reader = new rpc.WebSocketMessageReader(socket); const writer = new rpc.WebSocketMessageWriter(socket); const connection: IConnection = createConnection(reader, writer); - const server = new LUServer(connection); + const server = new LUServer(connection, luImportResolver); server.start(); } diff --git a/Composer/packages/server/src/services/project.ts b/Composer/packages/server/src/services/project.ts index 81d3c9ad14..76d462254b 100644 --- a/Composer/packages/server/src/services/project.ts +++ b/Composer/packages/server/src/services/project.ts @@ -44,6 +44,17 @@ export class BotProjectService { }; } + public static luImportResolver(_source: string, id: string): any { + BotProjectService.initialize(); + const targetId = Path.basename(id, '.lu'); + const targetFile = BotProjectService.currentBotProject?.luFiles.find(({ id }) => id === targetId); + if (!targetFile) throw new Error('lu file not found'); + return { + id, + content: targetFile.content, + }; + } + public static staticMemoryResolver(): string[] { const defaultProperties = [ 'this.value', diff --git a/Composer/packages/tools/language-servers/language-understanding/package.json b/Composer/packages/tools/language-servers/language-understanding/package.json index fa8ebaddee..62510a16d7 100644 --- a/Composer/packages/tools/language-servers/language-understanding/package.json +++ b/Composer/packages/tools/language-servers/language-understanding/package.json @@ -4,22 +4,22 @@ "version": "0.1.0", "main": "lib/index.js", "scripts": { - "build": "tsc --build tsconfig.build.json", - "build:demo": "cd demo && tsc --build tsconfig.json", - "prepublishOnly": "npm run build", - "clean": "rimraf lib demo/dist", - "start": "cd demo && ts-node ./src/server.ts", - "test": "jest", - "lint": "eslint --quiet --ext .js,.jsx,.ts,.tsx ./src ./__tests__", - "lint:fix": "yarn lint --fix", - "lint:typecheck": "tsc --noEmit", - "startDemo": "concurrently --kill-others \"yarn start:server\" \"yarn start:client\"", - "start:client": "cd demo && webpack-dev-server", - "start:server": "cd demo && ts-node ./src/server.ts" + "build": "tsc --build tsconfig.build.json", + "build:demo": "cd demo && tsc --build tsconfig.json", + "prepublishOnly": "npm run build", + "clean": "rimraf lib demo/dist", + "start": "cd demo && ts-node ./src/server.ts", + "test": "jest", + "lint": "eslint --quiet --ext .js,.jsx,.ts,.tsx ./src ./__tests__", + "lint:fix": "yarn lint --fix", + "lint:typecheck": "tsc --noEmit", + "startDemo": "concurrently --kill-others \"yarn start:server\" \"yarn start:client\"", + "start:client": "cd demo && webpack-dev-server", + "start:server": "cd demo && ts-node ./src/server.ts" }, "dependencies": { - "@microsoft/bf-cli-command": "https://botbuilder.myget.org/F/botbuilder-declarative/npm/@microsoft/bf-cli-command/-/@microsoft/bf-cli-command-1.0.0.tgz", "@bfcomposer/bf-lu": "https://botbuilder.myget.org/F/botbuilder-declarative/npm/@bfcomposer/bf-lu/-/@bfcomposer/bf-lu-1.1.8.tgz", + "@microsoft/bf-cli-command": "https://botbuilder.myget.org/F/botbuilder-declarative/npm/@microsoft/bf-cli-command/-/@microsoft/bf-cli-command-1.0.0.tgz", "@types/node": "^12.0.4", "express": "^4.15.2", "monaco-editor-core": "^0.17.0", @@ -41,7 +41,7 @@ "style-loader": "^1.0.0", "ts-loader": "^6.1.0", "ts-node": "^8.3.0", - "typescript": "^3.6.3", + "typescript": "3.7.2", "uglifyjs-webpack-plugin": "^2.2.0", "webpack": "^4.39.3", "webpack-cli": "^3.3.8", diff --git a/Composer/packages/tools/language-servers/language-understanding/src/LUServer.ts b/Composer/packages/tools/language-servers/language-understanding/src/LUServer.ts index d5f6855cb6..a4b43eef93 100644 --- a/Composer/packages/tools/language-servers/language-understanding/src/LUServer.ts +++ b/Composer/packages/tools/language-servers/language-understanding/src/LUServer.ts @@ -18,20 +18,28 @@ import { TextEdit, } from 'vscode-languageserver-types'; import { TextDocumentPositionParams, DocumentOnTypeFormattingParams } from 'vscode-languageserver-protocol'; +import { updateIntent, isValid, checkSection } from '@bfc/indexers/lib/utils/luUtil'; +import { luIndexer } from '@bfc/indexers'; +import { parser } from '@bfcomposer/bf-lu/lib/parser'; import { EntityTypesObj, LineState } from './entityEnum'; import * as util from './matchingPattern'; +import { ImportResolverDelegate, LUOption, LUDocument, generageDiagnostic, convertDiagnostics } from './utils'; -const parseFile = require('@bfcomposer/bf-lu/lib/parser/lufile/parseFileContents.js').parseFile; -// eslint-disable-next-line @typescript-eslint/no-var-requires -const validateLUISBlob = require('@bfcomposer/bf-lu/lib/parser/luis/luisValidator'); +// define init methods call from client const LABELEXPERIENCEREQUEST = 'labelingExperienceRequest'; +const InitializeDocumentsMethodName = 'initializeDocuments'; + +const { parse } = luIndexer; +const { parseFile } = parser; + export class LUServer { protected workspaceRoot: URI | undefined; protected readonly documents = new TextDocuments(); protected readonly pendingValidationRequests = new Map(); + protected LUDocuments: LUDocument[] = []; - constructor(protected readonly connection: IConnection) { + constructor(protected readonly connection: IConnection, protected readonly importResolver?: ImportResolverDelegate) { this.documents.listen(this.connection); this.documents.onDidChangeContent(change => this.validate(change.document)); this.documents.onDidClose(event => { @@ -66,6 +74,14 @@ export class LUServer { this.connection.onRequest((method, params) => { if (method === LABELEXPERIENCEREQUEST) { this.labelingExperienceHandler(params); + } else if (InitializeDocumentsMethodName === method) { + const { uri, luOption }: { uri: string; luOption?: LUOption } = params; + const textDocument = this.documents.get(uri); + if (textDocument) { + this.addLUDocument(textDocument, luOption); + this.validateLuOption(textDocument, luOption); + this.validate(textDocument); + } } }); } @@ -74,6 +90,100 @@ export class LUServer { this.connection.listen(); } + protected validateLuOption(document: TextDocument, luOption?: LUOption) { + if (!luOption) return; + + const diagnostics: string[] = []; + + if (!this.importResolver) { + diagnostics.push('[Error luOption] importResolver is required but not exist.'); + } else { + const { fileId, sectionId } = luOption; + const luFile = this.getLUDocument(document)?.index(); + if (!luFile) { + diagnostics.push(`[Error luOption] File ${fileId}.lu do not exist`); + } else if (sectionId) { + const { sections } = luFile; + const section = sections.find(({ Name }) => Name === sectionId); + if (!section) diagnostics.push(`Section ${fileId}.lu#${sectionId} do not exist`); + } + } + this.connection.console.log(diagnostics.join('\n')); + this.sendDiagnostics( + document, + diagnostics.map(errorMsg => generageDiagnostic(errorMsg, DiagnosticSeverity.Error, document)) + ); + } + + protected getImportResolver(document: TextDocument) { + const editorContent = document.getText(); + const internalImportResolver = () => { + return { + id: document.uri, + content: editorContent, + }; + }; + const { fileId, sectionId } = this.getLUDocument(document) || {}; + + if (this.importResolver && fileId) { + const resolver = this.importResolver; + return (source: string, id: string) => { + const luFile = resolver(source, id); + if (!luFile) { + this.sendDiagnostics(document, [ + generageDiagnostic(`lu file: ${fileId}.lu not exist on server`, DiagnosticSeverity.Error, document), + ]); + } + let { content } = luFile; + /** + * source is . means use as file resolver, not import resolver + * if inline editor, server file write may have delay than webSocket updated LSP server + * so here build the full content from server file content and editor content + */ + if (source === '.' && sectionId) { + content = updateIntent(luFile.content, sectionId, { Name: sectionId, Body: editorContent }); + } + return { id, content }; + }; + } + + return internalImportResolver; + } + + protected addLUDocument(document: TextDocument, luOption?: LUOption) { + const { uri } = document; + const { fileId, sectionId } = luOption || {}; + const index = () => { + const importResolver: ImportResolverDelegate = this.getImportResolver(document); + let content: string = document.getText(); + // if inline mode, composite local with server resolved file. + if (this.importResolver && fileId && sectionId) { + try { + content = importResolver('.', `${fileId}.lu`).content; + } catch (error) { + // ignore if file not exist + } + } + + const id = fileId || uri; + const { intents: sections, diagnostics: bfIndexerDiags } = parse(content, id); + const diagnostics = convertDiagnostics(bfIndexerDiags, document); + + return { sections, diagnostics, content }; + }; + const luDocument: LUDocument = { + uri, + fileId, + sectionId, + index, + }; + this.LUDocuments.push(luDocument); + } + + protected getLUDocument(document: TextDocument): LUDocument | undefined { + return this.LUDocuments.find(({ uri }) => uri === document.uri); + } + protected labelingExperienceHandler(params: any): void { const document: TextDocument | undefined = this.documents.get(params.uri); if (!document) { @@ -108,7 +218,8 @@ export class LUServer { const edits: TextEdit[] = []; const curLineNumber = params.position.line; const lineCount = document.lineCount; - const text = document.getText(); + const luDoc = this.getLUDocument(document); + const text = luDoc?.index().content || document.getText(); const lines = text.split('\n'); const position = params.position; const textBeforeCurLine = lines.slice(0, curLineNumber).join('\n'); @@ -234,43 +345,6 @@ export class LUServer { return Promise.reject(error.responseText || getErrorStatusDescription(error.status) || error.toString()); } } - - private async validateLuBody(content: string): Promise<{ parsedContent: any; errors: any }> { - const errors: Diagnostic[] = []; - let parsedContent: any; - const log = false; - const locale = 'en-us'; - try { - parsedContent = await parseFile(content, log, locale); - if (parsedContent !== undefined) { - try { - validateLUISBlob(parsedContent.LUISJsonStructure); - } catch (e) { - e.diagnostics.forEach(diag => { - const range = Range.create(0, 0, 0, 1); - const message = diag.Message; - const severity = DiagnosticSeverity.Error; - errors.push(Diagnostic.create(range, message, severity)); - }); - } - } - } catch (e) { - e.diagnostics.forEach(diag => { - const range = Range.create( - diag.Range.Start.Line - 1, - diag.Range.Start.Character, - diag.Range.End.Line - 1, - diag.Range.End.Character - ); - const message = diag.Message; - const severity = DiagnosticSeverity.Error; - errors.push(Diagnostic.create(range, message, severity)); - }); - } - - return Promise.resolve({ parsedContent, errors }); - } - private async extractLUISContent(text: string): Promise { let parsedContent: any; const log = false; @@ -297,7 +371,8 @@ export class LUServer { const position = params.position; const range = Range.create(position.line, 0, position.line, position.character); const curLineContent = document.getText(range); - const text = document.getText(); + const luDoc = this.getLUDocument(document); + const text = luDoc?.index().content || document.getText(); const lines = text.split('\n'); const curLineNumber = params.position.line; //const textBeforeCurLine = lines.slice(0, curLineNumber).join('\n'); @@ -526,16 +601,42 @@ export class LUServer { } protected doValidate(document: TextDocument): void { - if (document.getText().length === 0) { + const text = document.getText(); + const luDoc = this.getLUDocument(document); + if (!luDoc) { + return; + } + const { fileId, sectionId } = luDoc; + const luFile = luDoc.index(); + if (!luFile) { + return; + } + + if (text.length === 0) { this.cleanDiagnostics(document); return; } - const text = document.getText(); - this.validateLuBody(text).then(result => { - const diagnostics: Diagnostic[] = result.errors; - this.sendDiagnostics(document, diagnostics); - }); + const { diagnostics } = luFile; + + // if inline editor, concat new content for validate + if (fileId && sectionId) { + const sectionDiags = checkSection( + { + Name: sectionId, + Body: text, + }, + true + ); + // error in section. + if (isValid(sectionDiags) === false) { + const lspDiagnostics = convertDiagnostics(sectionDiags, document, 1); + this.sendDiagnostics(document, lspDiagnostics); + return; + } + } + const lspDiagnostics = convertDiagnostics(diagnostics, document); + this.sendDiagnostics(document, lspDiagnostics); } protected cleanDiagnostics(document: TextDocument): void { diff --git a/Composer/packages/tools/language-servers/language-understanding/src/utils.ts b/Composer/packages/tools/language-servers/language-understanding/src/utils.ts new file mode 100644 index 0000000000..417baabcf7 --- /dev/null +++ b/Composer/packages/tools/language-servers/language-understanding/src/utils.ts @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { TextDocument, Range, Position, DiagnosticSeverity, Diagnostic } from 'vscode-languageserver-types'; +import { DiagnosticSeverity as BFDiagnosticSeverity, Diagnostic as BFDiagnostic, offsetRange } from '@bfc/indexers'; + +export interface LUOption { + fileId: string; + sectionId: string; +} + +export interface Template { + name: string; + parameters?: string[]; + body: string; +} + +export interface LUDocument { + uri: string; + fileId?: string; + sectionId?: string; + index: () => any; +} + +export declare type ImportResolverDelegate = ( + source: string, + resourceId: string +) => { + content: string; + id: string; +}; + +const severityMap = { + [BFDiagnosticSeverity.Error]: DiagnosticSeverity.Error, + [BFDiagnosticSeverity.Hint]: DiagnosticSeverity.Hint, + [BFDiagnosticSeverity.Information]: DiagnosticSeverity.Information, + [BFDiagnosticSeverity.Warning]: DiagnosticSeverity.Warning, +}; + +export function convertSeverity(severity: BFDiagnosticSeverity): DiagnosticSeverity { + return severityMap[severity]; +} + +export function generageDiagnostic(message: string, severity: DiagnosticSeverity, document: TextDocument): Diagnostic { + return { + severity, + range: Range.create(Position.create(0, 0), Position.create(0, 0)), + message, + source: document.uri, + }; +} + +// if section, offset +1 to exclude #IntentName +export function convertDiagnostics(lgDiags: BFDiagnostic[] = [], document: TextDocument, offset = 0): Diagnostic[] { + const diagnostics: Diagnostic[] = []; + const defaultRange = Range.create(Position.create(0, 0), Position.create(0, 0)); + lgDiags.forEach(diag => { + const range = diag.range ? offsetRange(diag.range, offset) : defaultRange; + const diagnostic: Diagnostic = { + severity: convertSeverity(diag.severity), + range, + message: diag.message, + source: document.uri, + }; + diagnostics.push(diagnostic); + }); + return diagnostics; +} diff --git a/Composer/yarn.lock b/Composer/yarn.lock index 18aa59cc63..160659b32a 100644 --- a/Composer/yarn.lock +++ b/Composer/yarn.lock @@ -16627,11 +16627,6 @@ typescript@3.7.2: resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.7.2.tgz#27e489b95fa5909445e9fef5ee48d81697ad18fb" integrity sha512-ml7V7JfiN2Xwvcer+XAf2csGO1bPBdRbFCkYBczNZggrBZ9c7G3riSUeJmqEU5uOtXNPMhE3n+R4FA/3YOAWOQ== -typescript@^3.6.3: - version "3.6.3" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.6.3.tgz#fea942fabb20f7e1ca7164ff626f1a9f3f70b4da" - integrity sha512-N7bceJL1CtRQ2RiG0AQME13ksR7DiuQh/QehubYcghzv20tnh+MQnQIuJddTmsbqYj+dztchykemz0zFzlvdQw== - ua-parser-js@^0.7.18: version "0.7.19" resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.19.tgz#94151be4c0a7fb1d001af7022fdaca4642659e4b" From d57e478ccb31cf2aa49a3f33045f60940df35935 Mon Sep 17 00:00:00 2001 From: Long Alan Date: Wed, 26 Feb 2020 02:12:09 +0800 Subject: [PATCH 03/40] style: fix hover state nodes ui (#2065) * ui fix * use box-shadow Co-authored-by: zeye <2295905420@qq.com> Co-authored-by: Andy Brown --- .../src/components/renderers/ElementWrapper.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Composer/packages/extensions/visual-designer/src/components/renderers/ElementWrapper.tsx b/Composer/packages/extensions/visual-designer/src/components/renderers/ElementWrapper.tsx index 7d5dbebd73..2bd6d19760 100644 --- a/Composer/packages/extensions/visual-designer/src/components/renderers/ElementWrapper.tsx +++ b/Composer/packages/extensions/visual-designer/src/components/renderers/ElementWrapper.tsx @@ -21,8 +21,7 @@ const nodeBorderSelectedStyle = css` // BotAsks, UserAnswers and InvalidPromptBrick nodes selected style const nodeBorderDoubleSelectedStyle = css` - outline: 2px solid #0078d4; - box-shadow: 0px 0px 0px 6px rgba(0, 120, 212, 0.3); + box-shadow: 0px 0px 0px 2px #0078d4, 0px 0px 0px 6px rgba(0, 120, 212, 0.3); `; export interface ElementWrapperProps { id: string; From 9eeca92117aed2e2884785b4515e05d19296a928 Mon Sep 17 00:00:00 2001 From: Andy Brown Date: Tue, 25 Feb 2020 11:57:27 -0800 Subject: [PATCH 04/40] feat: add more help links (#2070) * enable help link for oauth step * add learn more link to publishing page * add learn more link to onboarding page --- .../packages/client/src/pages/setting/deployment/index.js | 7 ++++++- .../client/src/pages/setting/onboarding-settings/index.tsx | 4 ++++ Composer/packages/server/schemas/editor.schema | 5 +++-- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/Composer/packages/client/src/pages/setting/deployment/index.js b/Composer/packages/client/src/pages/setting/deployment/index.js index 33a9064cbf..1e6cb904ec 100644 --- a/Composer/packages/client/src/pages/setting/deployment/index.js +++ b/Composer/packages/client/src/pages/setting/deployment/index.js @@ -5,6 +5,7 @@ import React, { useState, Fragment } from 'react'; import formatMessage from 'format-message'; import { DefaultButton } from 'office-ui-fabric-react/lib/Button'; import { Stack } from 'office-ui-fabric-react/lib/Stack'; +import { Link } from 'office-ui-fabric-react/lib/Link'; import { DeployWizard } from './deployWizard.js'; import { styles } from './styles'; @@ -16,6 +17,7 @@ const instructions = { ), button1: formatMessage('Create Azure Resources'), button2: formatMessage('Deploy Bot to Azure'), + helpLink: 'https://aka.ms/bfc-publishing-your-bot', }; export const Deployment = () => { @@ -44,10 +46,13 @@ export const Deployment = () => {
{instructions.title}

{instructions.description}

- + + + {formatMessage('Learn more')} +
diff --git a/Composer/packages/client/src/pages/setting/onboarding-settings/index.tsx b/Composer/packages/client/src/pages/setting/onboarding-settings/index.tsx index f0f394bd61..da2244268a 100644 --- a/Composer/packages/client/src/pages/setting/onboarding-settings/index.tsx +++ b/Composer/packages/client/src/pages/setting/onboarding-settings/index.tsx @@ -6,6 +6,7 @@ import { jsx } from '@emotion/core'; import { useCallback, useContext } from 'react'; import formatMessage from 'format-message'; import { Toggle } from 'office-ui-fabric-react/lib/Toggle'; +import { Link } from 'office-ui-fabric-react/lib/Link'; import { StoreContext } from '../../../store'; @@ -35,6 +36,9 @@ export const OnboardingSettings = () => { onChange={onChange} onText={formatMessage('Enabled')} /> + + {formatMessage('Learn more')} +
); }; diff --git a/Composer/packages/server/schemas/editor.schema b/Composer/packages/server/schemas/editor.schema index 8bef83d2bb..9d581ca9bb 100644 --- a/Composer/packages/server/schemas/editor.schema +++ b/Composer/packages/server/schemas/editor.schema @@ -150,7 +150,8 @@ }, "Microsoft.OAuthInput": { "title": "OAuth Login", - "helpLink": "https://aka.ms/bfc-using-oauth" + "helpLink": "https://aka.ms/bfc-using-oauth", + "helpLinkText": "Learn more" }, "Microsoft.OnActivity": { "title": "Activities", @@ -299,4 +300,4 @@ "helpLinkText": "Learn more" } } -} \ No newline at end of file +} From 7b452d33eeaeaa27de6fe573c237f77db76ebb09 Mon Sep 17 00:00:00 2001 From: zeye <2295905420@qq.com> Date: Wed, 26 Feb 2020 06:19:37 +0800 Subject: [PATCH 05/40] feat: support multi-line node block in Visual Editor (#2005) * draft: DynamicBlock * don't setState in effect hook * set default demo to editor * give decorators unique id * implement ElementMeasurer for dynamic size * pass onResize handler to enable boundary propagation * update FormCard css to not truncate text * apply ElementMeasurer to necessary widgets * css: word break in FormCard * support smart layout in all input types * revert exp changes in uischema * add new hook useSmartLayout for layout changes * apply useSmartLayouter to PromptWidgets * apply useSmartLayout hook to IfConditionWidget * include boundary merge logic in useSmartLayout * IfCondition, Prompt adapt to new smart layout hook * apply useSmartLayout to ForeachWidget * apply useSmartLayout to SwitchConditionWidget * add comments to ElementMeasurer * add comments in useSmartLayout * revert ObiEditor changes * update FormCard style * revert unnecessary changes * revert unnecessary transformer changes * fix IfConditionWidget layout * fix a potential undefined func call * improve ts type in Switch * sort Switch case keys * StepGroup apply useSmartLayouter * feat: boundary cache mechanism to fix flickering * remove size limit in uischema (deprecated by smart layouter) * fix FormCard css compatibility Co-authored-by: Andy Brown --- .../extensions/visual-designer/package.json | 1 + .../src/components/groups/StepGroup.tsx | 48 ++++---- .../src/components/nodes/nodeProps.ts | 2 +- .../components/nodes/templates/FormCard.tsx | 16 +-- .../components/renderers/ElementMeasurer.tsx | 34 ++++++ .../src/components/renderers/StepRenderer.tsx | 7 +- .../src/hooks/useSmartLayout.ts | 69 +++++++++++ .../src/layouters/measureJsonBoundary.ts | 6 + .../visual-designer/src/schema/uischema.tsx | 2 - .../src/schema/uischemaRenderer.tsx | 5 + .../src/store/DesignerCache.ts | 47 ++++++++ .../src/widgets/ForeachWidget.tsx | 86 ++++++------- .../src/widgets/IfConditionWidget.tsx | 114 +++++++++--------- .../src/widgets/PromptWidget.tsx | 56 +++++++-- .../src/widgets/SwitchConditionWidget.tsx | 94 ++++++++------- 15 files changed, 397 insertions(+), 190 deletions(-) create mode 100644 Composer/packages/extensions/visual-designer/src/components/renderers/ElementMeasurer.tsx create mode 100644 Composer/packages/extensions/visual-designer/src/hooks/useSmartLayout.ts create mode 100644 Composer/packages/extensions/visual-designer/src/store/DesignerCache.ts diff --git a/Composer/packages/extensions/visual-designer/package.json b/Composer/packages/extensions/visual-designer/package.json index 10909e14cc..acb0e94a99 100644 --- a/Composer/packages/extensions/visual-designer/package.json +++ b/Composer/packages/extensions/visual-designer/package.json @@ -36,6 +36,7 @@ "lodash": "^4.17.15", "office-ui-fabric-react": "7.62.0", "prop-types": "^15.7.2", + "react-measure": "^2.3.0", "source-map-loader": "^0.2.4" }, "peerDependencies": { diff --git a/Composer/packages/extensions/visual-designer/src/components/groups/StepGroup.tsx b/Composer/packages/extensions/visual-designer/src/components/groups/StepGroup.tsx index b852607f9e..83d4b57ebc 100644 --- a/Composer/packages/extensions/visual-designer/src/components/groups/StepGroup.tsx +++ b/Composer/packages/extensions/visual-designer/src/components/groups/StepGroup.tsx @@ -3,10 +3,9 @@ /** @jsx jsx */ import { jsx } from '@emotion/core'; -import { useState, useMemo, useEffect, FunctionComponent } from 'react'; +import { useMemo, FunctionComponent } from 'react'; import { GraphNode } from '../../models/GraphNode'; -import { areBoundariesEqual } from '../../models/Boundary'; import { sequentialLayouter } from '../../layouters/sequentialLayouter'; import { ElementInterval, EdgeAddButtonSize } from '../../constants/ElementSizes'; import { NodeEventTypes } from '../../constants/NodeEventTypes'; @@ -18,46 +17,42 @@ import { GraphLayout } from '../../models/GraphLayout'; import { EdgeMenu } from '../menus/EdgeMenu'; import { SVGContainer } from '../lib/SVGContainer'; import { renderEdge } from '../lib/EdgeUtil'; +import { GraphNodeMap, useSmartLayout } from '../../hooks/useSmartLayout'; +import { designerCache } from '../../store/DesignerCache'; const StepInterval = ElementInterval.y; -const calculateNodes = (groupId: string, data): GraphNode[] => { +type StepNodeKey = string; + +const getStepKey = (stepOrder: number): StepNodeKey => `steps[${stepOrder}]`; + +const calculateNodes = (groupId: string, data): GraphNodeMap => { const steps = transformStepGroup(data, groupId); - return steps.map((x): GraphNode => GraphNode.fromIndexedJson(x)); + const stepNodes = steps.map((x): GraphNode => GraphNode.fromIndexedJson(x)); + return stepNodes.reduce((result, node, index) => { + result[getStepKey(index)] = node; + return result; + }, {} as GraphNodeMap); }; -const calculateLayout = (nodes, boundaryMap): GraphLayout => { - nodes.forEach((x): void => (x.boundary = boundaryMap[x.id] || x.boundary)); +const calculateLayout = (nodeMap: GraphNodeMap): GraphLayout => { + const nodes = Object.keys(nodeMap) + .sort() + .map(stepName => nodeMap[stepName]); return sequentialLayouter(nodes); }; export const StepGroup: FunctionComponent = ({ id, data, onEvent, onResize }: NodeProps): JSX.Element => { - const [boundaryMap, setBoundaryMap] = useState({}); - const initialNodes = useMemo((): GraphNode[] => calculateNodes(id, data), [id, data]); - const layout = useMemo((): GraphLayout => calculateLayout(initialNodes, boundaryMap), [initialNodes, boundaryMap]); - const accumulatedPatches = {}; - - const patchBoundary = (id, boundary): void => { - if (!boundaryMap[id] || !areBoundariesEqual(boundaryMap[id], boundary)) { - accumulatedPatches[id] = boundary; - setBoundaryMap({ - ...boundaryMap, - ...accumulatedPatches, - }); - } - }; + const initialNodes = useMemo(() => calculateNodes(id, data), [id, data]); + const { layout, updateNodeBoundary } = useSmartLayout(initialNodes, calculateLayout, onResize); const { boundary, nodes, edges } = layout; - useEffect(() => { - onResize(layout.boundary); - }, [layout]); - return (
{Array.isArray(edges) ? edges.map(x => renderEdge(x)) : null} {nodes - ? nodes.map(x => ( + ? nodes.map((x, index) => ( = ({ id, data, onEvent, onR data={x.data} onEvent={onEvent} onResize={size => { - patchBoundary(x.id, size); + designerCache.cacheBoundary(x.data, size); + updateNodeBoundary(getStepKey(index), size); }} /> diff --git a/Composer/packages/extensions/visual-designer/src/components/nodes/nodeProps.ts b/Composer/packages/extensions/visual-designer/src/components/nodes/nodeProps.ts index f6a5a9c010..b3471220c2 100644 --- a/Composer/packages/extensions/visual-designer/src/components/nodes/nodeProps.ts +++ b/Composer/packages/extensions/visual-designer/src/components/nodes/nodeProps.ts @@ -9,7 +9,7 @@ export interface NodeProps { data: any; focused?: boolean; onEvent: (action, id, ...rest) => object | void; - onResize: (boundary?: Boundary, id?) => object | void; + onResize: (boundary: Boundary, id?) => object | void; isRoot?: boolean; } diff --git a/Composer/packages/extensions/visual-designer/src/components/nodes/templates/FormCard.tsx b/Composer/packages/extensions/visual-designer/src/components/nodes/templates/FormCard.tsx index 792d708c6d..116595a902 100644 --- a/Composer/packages/extensions/visual-designer/src/components/nodes/templates/FormCard.tsx +++ b/Composer/packages/extensions/visual-designer/src/components/nodes/templates/FormCard.tsx @@ -16,7 +16,7 @@ const contentHeight = boxHeight - headerHeight; const containerStyle = { width: boxWidth, - height: boxHeight, + minHeight: boxHeight, fontSize: '12px', cursor: 'pointer', overflow: 'hidden', @@ -92,17 +92,18 @@ export const FormCard: FunctionComponent = ({ className="card__content" css={{ width: '100%', - height: contentHeight, + minHeight: contentHeight, + display: 'inline-block', }} >
@@ -124,12 +125,13 @@ export const FormCard: FunctionComponent = ({ css={{ height: '100%', width: 'calc(100% - 20px)', - whiteSpace: 'nowrap', - textOverflow: 'ellipsis', - overflow: 'hidden', + whiteSpace: 'initial', fontSize: '12px', lineHeight: '19px', fontFamily: 'Segoe UI', + overflowWrap: 'break-word', + wordBreak: 'break-all', + display: 'inline-block', }} title={typeof label === 'string' ? label : ''} > diff --git a/Composer/packages/extensions/visual-designer/src/components/renderers/ElementMeasurer.tsx b/Composer/packages/extensions/visual-designer/src/components/renderers/ElementMeasurer.tsx new file mode 100644 index 0000000000..17d7a5f346 --- /dev/null +++ b/Composer/packages/extensions/visual-designer/src/components/renderers/ElementMeasurer.tsx @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import React from 'react'; +import Measure from 'react-measure'; + +import { Boundary } from '../../models/Boundary'; + +export interface ElementMeasurerProps { + children: React.ReactNode; + style?: React.CSSProperties; + onResize: (boundary: Boundary) => void; +} + +/** + * Notify a ReactNode's size once its size has been changed. + * Remember to use it inside the focus border component (ElementWrapper). + */ +export const ElementMeasurer: React.FC = ({ children, style, onResize }) => { + return ( + { + onResize(new Boundary(width, height)); + }} + > + {({ measureRef }) => ( +
+ {children} +
+ )} +
+ ); +}; diff --git a/Composer/packages/extensions/visual-designer/src/components/renderers/StepRenderer.tsx b/Composer/packages/extensions/visual-designer/src/components/renderers/StepRenderer.tsx index 0f1c9fcd65..6674904de7 100644 --- a/Composer/packages/extensions/visual-designer/src/components/renderers/StepRenderer.tsx +++ b/Composer/packages/extensions/visual-designer/src/components/renderers/StepRenderer.tsx @@ -12,6 +12,7 @@ import { renderUIWidget } from '../../schema/uischemaRenderer'; import { UISchemaContext } from '../../store/UISchemaContext'; import { ElementWrapper } from './ElementWrapper'; +import { ElementMeasurer } from './ElementMeasurer'; /** TODO: (zeye) integrate this array into UISchema */ const TypesWithoutWrapper = [ @@ -27,19 +28,19 @@ const TypesWithoutWrapper = [ SDKTypes.ChoiceInput, ]; -export const StepRenderer: FC = ({ id, data, onEvent }): JSX.Element => { +export const StepRenderer: FC = ({ id, data, onEvent, onResize }): JSX.Element => { const schemaProvider = useContext(UISchemaContext); const $type = get(data, '$type', ''); const widgetSchema = schemaProvider.get($type); - const content = renderUIWidget(widgetSchema, { id, data, onEvent }); + const content = renderUIWidget(widgetSchema, { id, data, onEvent, onResize }); if (TypesWithoutWrapper.some(x => $type === x)) { return content; } return ( - {content} + onResize(boundary)}>{content} ); }; diff --git a/Composer/packages/extensions/visual-designer/src/hooks/useSmartLayout.ts b/Composer/packages/extensions/visual-designer/src/hooks/useSmartLayout.ts new file mode 100644 index 0000000000..496ec79591 --- /dev/null +++ b/Composer/packages/extensions/visual-designer/src/hooks/useSmartLayout.ts @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { useState, useEffect, useMemo } from 'react'; + +import { Boundary, areBoundariesEqual } from '../models/Boundary'; +import { GraphLayout } from '../models/GraphLayout'; +import { GraphNode } from '../models/GraphNode'; + +// 'T extends string' means an Enum. Reference: https://github.com/microsoft/TypeScript/issues/30611#issuecomment-565384924 +type MapWithEnumKey = { [key in KeyType]: ValueType }; + +type BoundaryMap = MapWithEnumKey; + +export type GraphNodeMap = MapWithEnumKey; + +export function useSmartLayout( + nodeMap: GraphNodeMap, + layouter: (nodeMap: GraphNodeMap) => GraphLayout, + onResize: (boundary: Boundary) => void +): { + layout: GraphLayout; + updateNodeBoundary: (nodeName: T, boundary: Boundary) => void; +} { + const [boundaryMap, setBoundaryMap] = useState>({} as BoundaryMap); + /** + * The object `accumulatedPatches` is used to collect all accumulated + * boundary changes happen in a same JS event cyle. After collecting + * them together, they will be submitted to component states to guide + * next redraw. + * + * We shouldn't use `setState()` here because of `patchBoundary` may be + * fired multiple times (especially at the init render cycle), changes + * will be lost by using `setState()`; + * + * We shouldn't use `useRef` here since `accumulatedPatches` as a local + * cache needs to be cleared after taking effect in one redraw. + */ + const accumulatedPatches = {}; + const patchBoundary = (nodeName: string, boundary: Boundary) => { + if (!boundaryMap[nodeName] || !areBoundariesEqual(boundaryMap[nodeName], boundary)) { + accumulatedPatches[nodeName] = boundary; + setBoundaryMap({ + ...boundaryMap, + ...accumulatedPatches, + }); + } + }; + + const layout = useMemo(() => { + // write updated boundaries to nodes + Object.keys(nodeMap).map(nodeName => { + const node = nodeMap[nodeName]; + if (node) { + node.boundary = boundaryMap[nodeName] || node.boundary; + } + }); + return layouter(nodeMap); + }, [nodeMap, boundaryMap]); + + useEffect(() => { + onResize && onResize(layout.boundary); + }, [layout]); + + return { + layout, + updateNodeBoundary: patchBoundary, + }; +} diff --git a/Composer/packages/extensions/visual-designer/src/layouters/measureJsonBoundary.ts b/Composer/packages/extensions/visual-designer/src/layouters/measureJsonBoundary.ts index 03420424b2..4937873417 100644 --- a/Composer/packages/extensions/visual-designer/src/layouters/measureJsonBoundary.ts +++ b/Composer/packages/extensions/visual-designer/src/layouters/measureJsonBoundary.ts @@ -16,6 +16,7 @@ import { transformIfCondtion } from '../transformers/transformIfCondition'; import { transformSwitchCondition } from '../transformers/transformSwitchCondition'; import { transformForeach } from '../transformers/transformForeach'; import { transformBaseInput } from '../transformers/transformBaseInput'; +import { designerCache } from '../store/DesignerCache'; import { calculateIfElseBoundary, @@ -81,6 +82,11 @@ export function measureJsonBoundary(json): Boundary { let boundary = new Boundary(); if (!json || !json.$type) return boundary; + const cachedBoundary = designerCache.loadBounary(json); + if (cachedBoundary) { + return cachedBoundary; + } + switch (json.$type) { case ObiTypes.ChoiceDiamond: boundary = new Boundary(DiamondSize.width, DiamondSize.height); diff --git a/Composer/packages/extensions/visual-designer/src/schema/uischema.tsx b/Composer/packages/extensions/visual-designer/src/schema/uischema.tsx index 879cce4f58..a8177ac85f 100644 --- a/Composer/packages/extensions/visual-designer/src/schema/uischema.tsx +++ b/Composer/packages/extensions/visual-designer/src/schema/uischema.tsx @@ -16,7 +16,6 @@ import { ForeachWidget } from '../widgets/ForeachWidget'; import { ChoiceInputChoices } from '../widgets/ChoiceInput'; import { ElementIcon } from '../utils/obiPropertyResolver'; import { ObiColors } from '../constants/ElementColors'; -import { measureChoiceInputDetailBoundary } from '../layouters/measureJsonBoundary'; import { UISchema, UIWidget } from './uischema.types'; @@ -41,7 +40,6 @@ const BaseInputSchema: UIWidget = { menu: 'none', content: data => data.property || '', children: data => (data.$type === SDKTypes.ChoiceInput ? : null), - size: data => measureChoiceInputDetailBoundary(data), colors: { theme: ObiColors.LightBlue, icon: ObiColors.AzureBlue, diff --git a/Composer/packages/extensions/visual-designer/src/schema/uischemaRenderer.tsx b/Composer/packages/extensions/visual-designer/src/schema/uischemaRenderer.tsx index 56f9c1594b..8e5841aecc 100644 --- a/Composer/packages/extensions/visual-designer/src/schema/uischemaRenderer.tsx +++ b/Composer/packages/extensions/visual-designer/src/schema/uischemaRenderer.tsx @@ -4,6 +4,8 @@ import React from 'react'; import { BaseSchema } from '@bfc/shared'; +import { Boundary } from '../models/Boundary'; + import { UIWidget, UI_WIDGET_KEY, UIWidgetProp, WidgetEventHandler } from './uischema.types'; export interface UIWidgetContext { @@ -15,6 +17,9 @@ export interface UIWidgetContext { /** Handle UI events */ onEvent: WidgetEventHandler; + + /** Report widget boundary */ + onResize: (boundary: Boundary) => void; } const parseWidgetSchema = (widgetSchema: UIWidget) => { diff --git a/Composer/packages/extensions/visual-designer/src/store/DesignerCache.ts b/Composer/packages/extensions/visual-designer/src/store/DesignerCache.ts new file mode 100644 index 0000000000..39e27f9bb7 --- /dev/null +++ b/Composer/packages/extensions/visual-designer/src/store/DesignerCache.ts @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { BaseSchema } from '@bfc/shared'; +import get from 'lodash/get'; + +import { Boundary } from '../models/Boundary'; + +const MAX_CACHE_SIZE = 99999; + +export class DesignerCache { + private boundaryCache = {}; + private cacheSize = 0; + + private getActionDataHash(actionData: BaseSchema): string | null { + const designerId = get(actionData, '$designer.id', ''); + if (!designerId) return null; + + const $type = get(actionData, '$type'); + return `${$type}-${designerId}`; + } + + cacheBoundary(actionData: BaseSchema, boundary: Boundary): boolean { + const key = this.getActionDataHash(actionData); + if (!key) { + return false; + } + + if (this.cacheSize > MAX_CACHE_SIZE) { + delete this.boundaryCache; + this.boundaryCache = {}; + this.cacheSize = 0; + } + this.boundaryCache[key] = boundary; + this.cacheSize += 1; + return true; + } + + loadBounary(actionData: BaseSchema): Boundary | undefined { + const key = this.getActionDataHash(actionData); + if (key) { + return this.boundaryCache[key]; + } + } +} + +export const designerCache = new DesignerCache(); diff --git a/Composer/packages/extensions/visual-designer/src/widgets/ForeachWidget.tsx b/Composer/packages/extensions/visual-designer/src/widgets/ForeachWidget.tsx index 8aabdba14f..a31786e8fa 100644 --- a/Composer/packages/extensions/visual-designer/src/widgets/ForeachWidget.tsx +++ b/Composer/packages/extensions/visual-designer/src/widgets/ForeachWidget.tsx @@ -3,43 +3,52 @@ /** @jsx jsx */ import { jsx } from '@emotion/core'; -import { useMemo, useEffect, useState, FunctionComponent } from 'react'; +import { useMemo, FunctionComponent } from 'react'; import { transformForeach } from '../transformers/transformForeach'; import { foreachLayouter } from '../layouters/foreachLayouter'; -import { areBoundariesEqual, Boundary } from '../models/Boundary'; import { GraphNode } from '../models/GraphNode'; import { NodeEventTypes } from '../constants/NodeEventTypes'; import { OffsetContainer } from '../components/lib/OffsetContainer'; import { LoopIndicator } from '../components/decorations/LoopIndicator'; import { StepGroup } from '../components/groups'; import { ElementWrapper } from '../components/renderers/ElementWrapper'; -import { NodeMap, BoundaryMap } from '../components/nodes/types'; +import { ElementMeasurer } from '../components/renderers/ElementMeasurer'; import { WidgetContainerProps } from '../schema/uischema.types'; import { renderEdge } from '../components/lib/EdgeUtil'; import { SVGContainer } from '../components/lib/SVGContainer'; +import { useSmartLayout, GraphNodeMap } from '../hooks/useSmartLayout'; +import { designerCache } from '../store/DesignerCache'; -const calculateNodeMap = (jsonpath, data): NodeMap => { +enum ForeachNodes { + Foreach = 'foreachNode', + LoopBegin = 'loopBeginNode', + LoopEnd = 'loopEndNode', + LoopActions = 'loopActionsNode', +} + +const calculateNodeMap = (jsonpath, data): GraphNodeMap => { const result = transformForeach(data, jsonpath); - if (!result) return {}; + if (!result) + return { + [ForeachNodes.Foreach]: new GraphNode(), + [ForeachNodes.LoopActions]: new GraphNode(), + [ForeachNodes.LoopBegin]: new GraphNode(), + [ForeachNodes.LoopEnd]: new GraphNode(), + }; const { foreachDetail, stepGroup, loopBegin, loopEnd } = result; return { - foreachNode: GraphNode.fromIndexedJson(foreachDetail), - stepGroupNode: GraphNode.fromIndexedJson(stepGroup), - loopBeginNode: GraphNode.fromIndexedJson(loopBegin), - loopEndNode: GraphNode.fromIndexedJson(loopEnd), + [ForeachNodes.Foreach]: GraphNode.fromIndexedJson(foreachDetail), + [ForeachNodes.LoopActions]: GraphNode.fromIndexedJson(stepGroup), + [ForeachNodes.LoopBegin]: GraphNode.fromIndexedJson(loopBegin), + [ForeachNodes.LoopEnd]: GraphNode.fromIndexedJson(loopEnd), }; }; -const calculateLayout = (nodeMap: NodeMap, boundaryMap: BoundaryMap) => { - Object.values(nodeMap) - .filter(x => !!x) - .forEach((x: GraphNode) => { - x.boundary = boundaryMap[x.id] || x.boundary; - }); - - return foreachLayouter(nodeMap.foreachNode, nodeMap.stepGroupNode, nodeMap.loopBeginNode, nodeMap.loopEndNode); +const calculateForeachLayout = (nodeMap: GraphNodeMap) => { + const { foreachNode, loopActionsNode, loopBeginNode, loopEndNode } = nodeMap; + return foreachLayouter(foreachNode, loopActionsNode, loopBeginNode, loopEndNode); }; export interface ForeachWidgetProps extends WidgetContainerProps { @@ -47,46 +56,37 @@ export interface ForeachWidgetProps extends WidgetContainerProps { } export const ForeachWidget: FunctionComponent = ({ id, data, onEvent, onResize, loop }) => { - const [boundaryMap, setBoundaryMap] = useState({}); - const initialNodeMap = useMemo(() => calculateNodeMap(id, data), [id, data]); - const layout = useMemo(() => calculateLayout(initialNodeMap, boundaryMap), [initialNodeMap, boundaryMap]); - const accumulatedPatches = {}; - - const patchBoundary = (id, boundary?: Boundary) => { - if (!boundaryMap[id] || !areBoundariesEqual(boundaryMap[id], boundary)) { - accumulatedPatches[id] = boundary; - setBoundaryMap({ - ...boundaryMap, - ...accumulatedPatches, - }); - } - }; - - useEffect(() => { - onResize(layout.boundary); - }, [layout]); + const nodeMap = useMemo(() => calculateNodeMap(id, data), [id, data]); + const { layout, updateNodeBoundary } = useSmartLayout(nodeMap, calculateForeachLayout, onResize); - const { boundary, nodeMap, edges } = layout; + const { boundary, edges } = layout; if (!nodeMap) { return null; } - const { foreachNode, stepsNode, loopBeginNode, loopEndNode } = nodeMap; + const { foreachNode, loopActionsNode, loopBeginNode, loopEndNode } = nodeMap; return (
- {loop} + { + designerCache.cacheBoundary(foreachNode.data, boundary); + updateNodeBoundary(ForeachNodes.Foreach, boundary); + }} + > + {loop} + - + { - patchBoundary(stepsNode.id, size); + updateNodeBoundary(ForeachNodes.LoopActions, size); }} /> diff --git a/Composer/packages/extensions/visual-designer/src/widgets/IfConditionWidget.tsx b/Composer/packages/extensions/visual-designer/src/widgets/IfConditionWidget.tsx index e991d35025..10426593c9 100644 --- a/Composer/packages/extensions/visual-designer/src/widgets/IfConditionWidget.tsx +++ b/Composer/packages/extensions/visual-designer/src/widgets/IfConditionWidget.tsx @@ -3,41 +3,52 @@ /** @jsx jsx */ import { jsx } from '@emotion/core'; -import { FunctionComponent, useEffect, useState, useMemo } from 'react'; +import { FunctionComponent, useMemo } from 'react'; import { transformIfCondtion } from '../transformers/transformIfCondition'; import { ifElseLayouter } from '../layouters/ifelseLayouter'; import { NodeEventTypes } from '../constants/NodeEventTypes'; import { GraphNode } from '../models/GraphNode'; -import { areBoundariesEqual, Boundary } from '../models/Boundary'; import { OffsetContainer } from '../components/lib/OffsetContainer'; import { StepGroup } from '../components/groups'; import { Diamond } from '../components/nodes/templates/Diamond'; import { ElementWrapper } from '../components/renderers/ElementWrapper'; -import { NodeMap, BoundaryMap } from '../components/nodes/types'; +import { ElementMeasurer } from '../components/renderers/ElementMeasurer'; import { WidgetContainerProps } from '../schema/uischema.types'; import { SVGContainer } from '../components/lib/SVGContainer'; import { renderEdge } from '../components/lib/EdgeUtil'; +import { useSmartLayout, GraphNodeMap } from '../hooks/useSmartLayout'; +import { designerCache } from '../store/DesignerCache'; -const calculateNodeMap = (path, data): NodeMap => { +enum IfElseNodes { + Condition = 'conditionNode', + Choice = 'choiceNode', + IfBranch = 'ifBranchNode', + ElseBranch = 'elseBranchNode', +} + +const calculateNodeMap = (path: string, data): GraphNodeMap => { const result = transformIfCondtion(data, path); - if (!result) return {}; + if (!result) + return { + [IfElseNodes.Condition]: new GraphNode(), + [IfElseNodes.Choice]: new GraphNode(), + [IfElseNodes.IfBranch]: new GraphNode(), + [IfElseNodes.ElseBranch]: new GraphNode(), + }; const { condition, choice, ifGroup, elseGroup } = result; return { - conditionNode: GraphNode.fromIndexedJson(condition), - choiceNode: GraphNode.fromIndexedJson(choice), - ifGroupNode: GraphNode.fromIndexedJson(ifGroup), - elseGroupNode: GraphNode.fromIndexedJson(elseGroup), + [IfElseNodes.Condition]: GraphNode.fromIndexedJson(condition), + [IfElseNodes.Choice]: GraphNode.fromIndexedJson(choice), + [IfElseNodes.IfBranch]: GraphNode.fromIndexedJson(ifGroup), + [IfElseNodes.ElseBranch]: GraphNode.fromIndexedJson(elseGroup), }; }; -const calculateLayout = (nodeMap: NodeMap, boundaryMap: BoundaryMap) => { - Object.values(nodeMap) - .filter(x => !!x) - .forEach((x: GraphNode) => (x.boundary = boundaryMap[x.id] || x.boundary)); - - return ifElseLayouter(nodeMap.conditionNode, nodeMap.choiceNode, nodeMap.ifGroupNode, nodeMap.elseGroupNode); +const calculateIfElseLayout = (nodeMap: GraphNodeMap) => { + const { conditionNode, choiceNode, ifBranchNode, elseBranchNode } = nodeMap; + return ifElseLayouter(conditionNode, choiceNode, ifBranchNode, elseBranchNode); }; export interface IfConditionWidgetProps extends WidgetContainerProps { @@ -51,60 +62,49 @@ export const IfConditionWidget: FunctionComponent = ({ onResize, judgement, }) => { - const [boundaryMap, setBoundaryMap] = useState({}); - const initialNodeMap = useMemo(() => calculateNodeMap(id, data), [id, data]); - const layout = useMemo(() => calculateLayout(initialNodeMap, boundaryMap), [initialNodeMap, boundaryMap]); - const accumulatedPatches = {}; - - const patchBoundary = (id, boundary?: Boundary) => { - if (!boundaryMap[id] || !areBoundariesEqual(boundaryMap[id], boundary)) { - accumulatedPatches[id] = boundary; - setBoundaryMap({ - ...boundaryMap, - ...accumulatedPatches, - }); - } - }; - - useEffect(() => { - onResize(layout.boundary); - }, [layout]); + const nodeMap = useMemo(() => calculateNodeMap(id, data), [id, data]); + const { layout, updateNodeBoundary } = useSmartLayout(nodeMap, calculateIfElseLayout, onResize); - const { boundary, nodeMap, edges } = layout; - const condition = nodeMap.condition || new GraphNode(); - const choice = nodeMap.choice || new GraphNode(); + const { boundary, edges } = layout; + const { conditionNode, choiceNode } = nodeMap; return (
- - - {judgement} + + + { + designerCache.cacheBoundary(conditionNode.data, boundary); + updateNodeBoundary(IfElseNodes.Condition, boundary); + }} + > + {judgement} + - + { onEvent(NodeEventTypes.Focus, { id }); }} /> - {nodeMap - ? [nodeMap.if, nodeMap.else] - .filter(x => !!x) - .map(x => ( - - { - patchBoundary(x.id, size); - }} - /> - - )) - : null} + {[IfElseNodes.IfBranch, IfElseNodes.ElseBranch].map(nodeName => { + const node = nodeMap[nodeName]; + return ( + + { + updateNodeBoundary(nodeName, size); + }} + /> + + ); + })} {Array.isArray(edges) ? edges.map(x => renderEdge(x)) : null}
); diff --git a/Composer/packages/extensions/visual-designer/src/widgets/PromptWidget.tsx b/Composer/packages/extensions/visual-designer/src/widgets/PromptWidget.tsx index 1355a2c3f0..248da13dac 100644 --- a/Composer/packages/extensions/visual-designer/src/widgets/PromptWidget.tsx +++ b/Composer/packages/extensions/visual-designer/src/widgets/PromptWidget.tsx @@ -3,7 +3,7 @@ /** @jsx jsx */ import { jsx } from '@emotion/core'; -import { FC } from 'react'; +import { FC, useMemo } from 'react'; import { PromptTab } from '@bfc/shared'; import { baseInputLayouter } from '../layouters/baseInputLayouter'; @@ -16,24 +16,46 @@ import { NodeEventTypes } from '../constants/NodeEventTypes'; import { IconBrick } from '../components/decorations/IconBrick'; import { renderEdge } from '../components/lib/EdgeUtil'; import { SVGContainer } from '../components/lib/SVGContainer'; +import { GraphLayout } from '../models/GraphLayout'; +import { ElementMeasurer } from '../components/renderers/ElementMeasurer'; +import { useSmartLayout, GraphNodeMap } from '../hooks/useSmartLayout'; +import { designerCache } from '../store/DesignerCache'; -const calculateNodes = (data, jsonpath: string) => { +enum PromptNodes { + BotAsks = 'botAsksNode', + UserAnswers = 'userAnswersNode', + InvalidPrompt = 'invalidPromptyNode', +} + +const calculateNodes = (jsonpath: string, data) => { const { botAsks, userAnswers, invalidPrompt } = transformBaseInput(data, jsonpath); return { - botAsksNode: GraphNode.fromIndexedJson(botAsks), - userAnswersNode: GraphNode.fromIndexedJson(userAnswers), - invalidPromptNode: GraphNode.fromIndexedJson(invalidPrompt), + [PromptNodes.BotAsks]: GraphNode.fromIndexedJson(botAsks), + [PromptNodes.UserAnswers]: GraphNode.fromIndexedJson(userAnswers), + [PromptNodes.InvalidPrompt]: GraphNode.fromIndexedJson(invalidPrompt), }; }; +const calculateLayout = (nodeMap: GraphNodeMap): GraphLayout => { + const { botAsksNode, userAnswersNode, invalidPromptyNode } = nodeMap; + return baseInputLayouter(botAsksNode, userAnswersNode, invalidPromptyNode); +}; + export interface PromptWdigetProps extends WidgetContainerProps { botAsks: JSX.Element; userInput: JSX.Element; } -export const PromptWidget: FC = ({ id, data, onEvent, botAsks, userInput }): JSX.Element => { - const nodes = calculateNodes(data, id); - const layout = baseInputLayouter(nodes.botAsksNode, nodes.userAnswersNode, nodes.invalidPromptNode); +export const PromptWidget: FC = ({ + id, + data, + onEvent, + onResize, + botAsks, + userInput, +}): JSX.Element => { + const nodes = useMemo(() => calculateNodes(id, data), [id, data]); + const { layout, updateNodeBoundary } = useSmartLayout(nodes, calculateLayout, onResize); const { boundary, nodeMap, edges } = layout; const { botAsksNode, userAnswersNode, invalidPromptNode: brickNode } = nodeMap; @@ -42,12 +64,26 @@ export const PromptWidget: FC = ({ id, data, onEvent, botAsks
- {botAsks} + { + designerCache.cacheBoundary(botAsksNode.data, boundary); + updateNodeBoundary(PromptNodes.BotAsks, boundary); + }} + > + {botAsks} + - {userInput} + { + designerCache.cacheBoundary(userAnswersNode.data, boundary); + updateNodeBoundary(PromptNodes.UserAnswers, boundary); + }} + > + {userInput} + diff --git a/Composer/packages/extensions/visual-designer/src/widgets/SwitchConditionWidget.tsx b/Composer/packages/extensions/visual-designer/src/widgets/SwitchConditionWidget.tsx index 66a74a2775..6d3c3b6f86 100644 --- a/Composer/packages/extensions/visual-designer/src/widgets/SwitchConditionWidget.tsx +++ b/Composer/packages/extensions/visual-designer/src/widgets/SwitchConditionWidget.tsx @@ -3,39 +3,61 @@ /** @jsx jsx */ import { jsx } from '@emotion/core'; -import { FunctionComponent, useEffect, useState, useMemo } from 'react'; +import { FunctionComponent, useMemo } from 'react'; import { NodeEventTypes } from '../constants/NodeEventTypes'; import { transformSwitchCondition } from '../transformers/transformSwitchCondition'; import { switchCaseLayouter } from '../layouters/switchCaseLayouter'; import { GraphNode } from '../models/GraphNode'; -import { areBoundariesEqual } from '../models/Boundary'; import { OffsetContainer } from '../components/lib/OffsetContainer'; import { StepGroup } from '../components/groups'; import { Diamond } from '../components/nodes/templates/Diamond'; import { ElementWrapper } from '../components/renderers/ElementWrapper'; +import { ElementMeasurer } from '../components/renderers/ElementMeasurer'; import { WidgetContainerProps } from '../schema/uischema.types'; import { renderEdge } from '../components/lib/EdgeUtil'; import { SVGContainer } from '../components/lib/SVGContainer'; +import { GraphNodeMap, useSmartLayout } from '../hooks/useSmartLayout'; +import { designerCache } from '../store/DesignerCache'; -const calculateNodeMap = (path, data) => { +enum SwitchNodes { + Switch = 'switchNode', + Choice = 'choiceNode', +} + +type CaseNodeKey = string; + +const getCaseKey = (caseIndex: number): CaseNodeKey => `cases[${caseIndex}]`; + +const calculateNodeMap = (path: string, data): GraphNodeMap => { const result = transformSwitchCondition(data, path); - if (!result) return {}; + if (!result) + return { + [SwitchNodes.Switch]: new GraphNode(), + [SwitchNodes.Choice]: new GraphNode(), + }; const { condition, choice, branches } = result; - return { - conditionNode: GraphNode.fromIndexedJson(condition), - choiceNode: GraphNode.fromIndexedJson(choice), - branchNodes: branches.map(x => GraphNode.fromIndexedJson(x)), + const nodeMap = { + [SwitchNodes.Switch]: GraphNode.fromIndexedJson(condition), + [SwitchNodes.Choice]: GraphNode.fromIndexedJson(choice), }; -}; -const calculateLayout = (nodeMap, boundaryMap) => { - [nodeMap.conditionNode, nodeMap.choiceNode, ...nodeMap.branchNodes] - .filter(x => !!x) - .forEach(x => (x.boundary = boundaryMap[x.id] || x.boundary)); + branches.forEach((branch, index) => { + const key = getCaseKey(index); + const value = GraphNode.fromIndexedJson(branch); + nodeMap[key] = value; + }); - return switchCaseLayouter(nodeMap.conditionNode, nodeMap.choiceNode, nodeMap.branchNodes); + return nodeMap; +}; + +const calculateLayout = (nodeMap: GraphNodeMap) => { + const { switchNode, choiceNode, ...cases } = nodeMap as GraphNodeMap; + const casesNodes = Object.keys(cases) + .sort() + .map(caseName => nodeMap[caseName]); + return switchCaseLayouter(switchNode, choiceNode, casesNodes); }; export interface SwitchConditionWidgetProps extends WidgetContainerProps { @@ -49,35 +71,25 @@ export const SwitchConditionWidget: FunctionComponent { - const [boundaryMap, setBoundaryMap] = useState({}); - const initialNodeMap = useMemo(() => calculateNodeMap(id, data), [id, data]); - const layout = useMemo(() => calculateLayout(initialNodeMap, boundaryMap), [initialNodeMap, boundaryMap]); - const accumulatedPatches = {}; - - const patchBoundary = (id, boundary) => { - if (!boundaryMap[id] || !areBoundariesEqual(boundaryMap[id], boundary)) { - accumulatedPatches[id] = boundary; - setBoundaryMap({ - ...boundaryMap, - ...accumulatedPatches, - }); - } - }; + const nodeMap = useMemo(() => calculateNodeMap(id, data), [id, data]); + const { layout, updateNodeBoundary } = useSmartLayout(nodeMap, calculateLayout, onResize); - useEffect(() => { - onResize(layout.boundary); - }, [layout]); - - const { boundary, nodeMap, edges } = layout; - const conditionNode = nodeMap.conditionNode; - const choiceNode = nodeMap.choiceNode; - const branchNodes = nodeMap.branchNodes || []; + const { boundary, edges } = layout; + const { switchNode, choiceNode, ...cases } = nodeMap as GraphNodeMap; + const casesNodes = Object.keys(cases).map(x => nodeMap[x]); return (
- - - {judgement} + + + { + designerCache.cacheBoundary(switchNode.data, boundary); + updateNodeBoundary(SwitchNodes.Switch, boundary); + }} + > + {judgement} + @@ -88,7 +100,7 @@ export const SwitchConditionWidget: FunctionComponent - {(branchNodes as any).map(x => ( + {(casesNodes as any).map((x, index) => ( { - patchBoundary(x.id, size); + updateNodeBoundary(getCaseKey(index), size); }} /> From 2a5a0fc991c25456c4166d9171eaef58a401abf1 Mon Sep 17 00:00:00 2001 From: Vishwac Sena Kannan Date: Tue, 25 Feb 2020 15:11:07 -0800 Subject: [PATCH 06/40] docs: update docs to fix LU file format link (#2071) Co-authored-by: Andy Brown --- docs/concept-language-understanding.md | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/docs/concept-language-understanding.md b/docs/concept-language-understanding.md index afd0b8c70f..91a490902c 100644 --- a/docs/concept-language-understanding.md +++ b/docs/concept-language-understanding.md @@ -4,7 +4,7 @@ Language Understanding (LU) is used by a bot to understand language naturally an LU has the following characteristics when used in the Bot Framework Composer: -- LU content can be authored in an inline editor or in **User Input** using the [.lu file format](https://github.com/Microsoft/botbuilder-tools/blob/master/packages/Ludown/docs/lu-file-format.md). +- LU content can be authored in an inline editor or in **User Input** using the [.lu file format][100]. - LU content is training data for recognizers. - Composer currently supports LU technologies such as LUIS and Regular Expression. @@ -48,7 +48,7 @@ You may have noticed that LU format is very similar to LG format but there are s ### Entities -Entities are a collection of objects, each consisting of data extracted from an utterance such as places, time, and people. Entities and intents are both important data extracted from utterances, but they are different. An intent indicates what the user is trying to do. An utterance may include zero or more entities, while an utterance usually represents one intent. In Composer, all entities are defined and managed inline. Entities in the [.lu file format](https://github.com/microsoft/botbuilder-tools/blob/master/packages/Ludown/docs/lu-file-format.md) are denoted using {\=\} notation. For example: +Entities are a collection of objects, each consisting of data extracted from an utterance such as places, time, and people. Entities and intents are both important data extracted from utterances, but they are different. An intent indicates what the user is trying to do. An utterance may include zero or more entities, while an utterance usually represents one intent. In Composer, all entities are defined and managed inline. Entities in the [.lu file format][100] are denoted using {\=\} notation. For example: # BookFlight - book a flight to {toCity=seattle} @@ -58,7 +58,7 @@ The example above shows the definition of a `BookFlight` intent with two example ### Example -The table below shows an example of an intent with its corresponding utterances and entities. All three utterances share the same intent _BookFlight_ each with a different entity. There are different types of entities, you can find more information in this article on the [LU file format](https://github.com/microsoft/botbuilder-tools/blob/master/packages/Ludown/docs/lu-file-format.md). +The table below shows an example of an intent with its corresponding utterances and entities. All three utterances share the same intent _BookFlight_ each with a different entity. There are different types of entities, you can find more information in this article on the [LU file format][100]. | Intent | Utterances | Entity | | ---------- | --------------------------------------------- | ----------------------- | @@ -84,13 +84,13 @@ To enable your bot to understand user's input contextually and conversationally To author proper LU content in Composer, you need to know: - [LU concepts](https://aka.ms/botbuilder-luis-concept?view=azure-bot-service-4.0) -- [.lu file format](https://github.com/Microsoft/botbuilder-tools/blob/master/packages/Ludown/docs/lu-file-format.md) +- [.lu file format][100] - [Common Expression Language](https://github.com/microsoft/BotBuilder-Samples/tree/master/experimental/common-expression-language#readme) To create the LU content, follow these steps: - Set up a **Recognizer** for a specific dialog (per dialog per recognizer). -- Author LU content as training data in [.lu format](https://github.com/Microsoft/botbuilder-tools/blob/master/packages/Ludown/docs/lu-file-format.md). +- Author LU content as training data in [.lu format][100]. - Create **Intent** triggers to wire up the LU content. - Publish LU content (for LUIS). @@ -103,7 +103,7 @@ Composer currently supports two types of recognizers: LUIS (by default) and Regu #### Author LU content -After you set up the recognizer type, you can customize your LU content in the editor using the [.lu format](https://github.com/Microsoft/botbuilder-tools/blob/master/packages/Ludown/docs/lu-file-format.md). +After you set up the recognizer type, you can customize your LU content in the editor using the [.lu format][100]. For this example define two intents: **Greeting** and **CheckWeather** with some example utterances inline: @@ -143,7 +143,7 @@ Any time you select **Start Bot** (or **Restart Bot**), Composer will evaluate i - [What is LUIS](https://aka.ms/luis-what-is-luis) - [Language Understanding](https://aka.ms/botbuilder-luis-concept?view=azure-bot-service-4.0) -- [.lu file format](https://github.com/Microsoft/botbuilder-tools/blob/master/packages/Ludown/docs/lu-file-format.md) +- [.lu file format][100] - [Common Expression Language](https://github.com/microsoft/BotBuilder-Samples/tree/master/experimental/common-expression-language#readme) - [Using LUIS for language understanding](https://github.com/microsoft/BotFramework-Composer/blob/kaiqb/Ignite2019/docs/howto-using-LUIS.md) - [Extract data from utterance text with intents and entities](https://aka.ms/luis-concept-data-extraction?tabs=V2) @@ -152,3 +152,5 @@ Any time you select **Start Bot** (or **Restart Bot**), Composer will evaluate i - Learn how to [use validation](./how-to-use-validation.md) - Learn how to [send messages to users](how-to-send-messages.md) + +[100]:http://aka.ms/lu-file-format From 1f8536746683a926893de610e648598606c79b5b Mon Sep 17 00:00:00 2001 From: Long Alan Date: Wed, 26 Feb 2020 16:03:16 +0800 Subject: [PATCH 07/40] style change (#2095) --- .../visual-designer/src/components/nodes/templates/FormCard.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/Composer/packages/extensions/visual-designer/src/components/nodes/templates/FormCard.tsx b/Composer/packages/extensions/visual-designer/src/components/nodes/templates/FormCard.tsx index 116595a902..498663409e 100644 --- a/Composer/packages/extensions/visual-designer/src/components/nodes/templates/FormCard.tsx +++ b/Composer/packages/extensions/visual-designer/src/components/nodes/templates/FormCard.tsx @@ -124,7 +124,6 @@ export const FormCard: FunctionComponent = ({
Date: Thu, 27 Feb 2020 00:58:12 +0800 Subject: [PATCH 08/40] fix: no longer show duplicate lg error notifications (#2100) --- .../src/pages/language-generation/index.tsx | 2 +- .../pages/notifications/useNotifications.tsx | 43 ++++++++++--------- 2 files changed, 24 insertions(+), 21 deletions(-) diff --git a/Composer/packages/client/src/pages/language-generation/index.tsx b/Composer/packages/client/src/pages/language-generation/index.tsx index 01939f236a..727a2a2878 100644 --- a/Composer/packages/client/src/pages/language-generation/index.tsx +++ b/Composer/packages/client/src/pages/language-generation/index.tsx @@ -59,7 +59,7 @@ const LGPage: React.FC = props => { useEffect(() => { const activeDialog = dialogs.find(({ id }) => id === fileId); - if (!activeDialog && dialogs.length) { + if (!activeDialog && dialogs.length && fileId !== 'common') { navigateTo('/language-generation/common'); } }, [fileId, dialogs]); diff --git a/Composer/packages/client/src/pages/notifications/useNotifications.tsx b/Composer/packages/client/src/pages/notifications/useNotifications.tsx index 529e1005d9..8865060dc7 100644 --- a/Composer/packages/client/src/pages/notifications/useNotifications.tsx +++ b/Composer/packages/client/src/pages/notifications/useNotifications.tsx @@ -44,27 +44,30 @@ export default function useNotifications(filter?: string) { }); lgFiles.forEach(lgFile => { const lgTemplates = get(lgFile, 'templates', []); - lgFile.diagnostics.map(diagnostic => { - const mappedTemplate = lgTemplates.find( - t => - get(diagnostic, 'range.start.line') >= get(t, 'range.startLineNumber') && - get(diagnostic, 'range.end.line') <= get(t, 'range.endLineNumber') - ); - let id = lgFile.id; - const location = `${lgFile.id}.lg`; - if (mappedTemplate && mappedTemplate.name.match(LgNamePattern)) { - //should navigate to design page - id = `${lgFile.id}#${mappedTemplate.name}`; - } - notifactions.push({ - type: 'lg', - severity: DiagnosticSeverity[diagnostic.severity] || '', - location, - message: createSingleMessage(diagnostic), - diagnostic, - id, + lgFile.diagnostics + // only report diagnostics belong to itself. + .filter(({ source, message }) => message.includes(`source: ${source}`)) + .map(diagnostic => { + const mappedTemplate = lgTemplates.find( + t => + get(diagnostic, 'range.start.line') >= get(t, 'range.startLineNumber') && + get(diagnostic, 'range.end.line') <= get(t, 'range.endLineNumber') + ); + let id = lgFile.id; + const location = `${lgFile.id}.lg`; + if (mappedTemplate && mappedTemplate.name.match(LgNamePattern)) { + //should navigate to design page + id = `${lgFile.id}#${mappedTemplate.name}`; + } + notifactions.push({ + type: 'lg', + severity: DiagnosticSeverity[diagnostic.severity] || '', + location, + message: createSingleMessage(diagnostic), + diagnostic, + id, + }); }); - }); }); return notifactions; }, [dialogs, luFiles, lgFiles]); From 1112c868f1e07c12387c67c53f7a130ad043fa72 Mon Sep 17 00:00:00 2001 From: Shuai Wang Date: Thu, 27 Feb 2020 13:36:19 +0800 Subject: [PATCH 09/40] fix: resolve known bugs in LU LSP. (#2098) * init * fix LU LSP known bugs * remove comment * fix bugs * remove unused variable Co-authored-by: Andy Brown --- .../packages/lib/code-editor/src/LuEditor.tsx | 2 +- .../lib/code-editor/src/languages/lu.ts | 2 +- .../language-understanding/src/LUServer.ts | 72 +++++++++++++------ .../language-understanding/src/entityEnum.ts | 2 +- .../src/matchingPattern.ts | 16 +++-- 5 files changed, 66 insertions(+), 28 deletions(-) diff --git a/Composer/packages/lib/code-editor/src/LuEditor.tsx b/Composer/packages/lib/code-editor/src/LuEditor.tsx index 28d2385684..8b584900b8 100644 --- a/Composer/packages/lib/code-editor/src/LuEditor.tsx +++ b/Composer/packages/lib/code-editor/src/LuEditor.tsx @@ -12,7 +12,7 @@ import { registerLULanguage } from './languages'; import { createUrl, createWebSocket, createLanguageClient } from './utils/lspUtil'; import { RichEditor, RichEditorProps } from './RichEditor'; -const LU_HELP = 'https://github.com/microsoft/botframework-cli/blob/master/packages/lu/docs/lu-file-format.md'; +const LU_HELP = 'https://github.com/microsoft/botframework-cli/blob/master/packages/luis/docs/lu-file-format.md'; const placeholder = `> To learn more about the LU file format, read the documentation at > ${LU_HELP}`; diff --git a/Composer/packages/lib/code-editor/src/languages/lu.ts b/Composer/packages/lib/code-editor/src/languages/lu.ts index 37f459beac..d709d8206a 100644 --- a/Composer/packages/lib/code-editor/src/languages/lu.ts +++ b/Composer/packages/lib/code-editor/src/languages/lu.ts @@ -41,7 +41,7 @@ export function registerLULanguage(monaco: typeof monacoEditor) { ], [ // eslint-disable-next-line security/detect-unsafe-regex - /(@\s*)(ml|prebuilt|regex|list|composite|patternany|phraselist)(\s*[\w_]+)/, + /(@\s*)(ml|prebuilt|regex|list|composite|Pattern\.Any|phraseList)(\s*[\w_]+)/, ['intent-indentifier', 'entity-type', 'entity-name'], ], [/(@\s*)(\s*[\w_]+)/, ['intent-indentifier', 'entity-name']], diff --git a/Composer/packages/tools/language-servers/language-understanding/src/LUServer.ts b/Composer/packages/tools/language-servers/language-understanding/src/LUServer.ts index a4b43eef93..b2aab572b0 100644 --- a/Composer/packages/tools/language-servers/language-understanding/src/LUServer.ts +++ b/Composer/packages/tools/language-servers/language-understanding/src/LUServer.ts @@ -60,7 +60,7 @@ export class LUServer { codeActionProvider: false, completionProvider: { resolveProvider: true, - triggerCharacters: ['@', ' ', '{', ':', '['], + triggerCharacters: ['@', ' ', '{', ':', '[', '('], }, foldingRangeProvider: false, documentOnTypeFormattingProvider: { @@ -217,7 +217,6 @@ export class LUServer { const lastLineContent = this.getLastLineContent(params); const edits: TextEdit[] = []; const curLineNumber = params.position.line; - const lineCount = document.lineCount; const luDoc = this.getLUDocument(document); const text = luDoc?.index().content || document.getText(); const lines = text.split('\n'); @@ -229,12 +228,7 @@ export class LUServer { const inputState = this.getInputLineState(params); const pos = params.position; - if ( - key === '\n' && - inputState === 'utterance' && - lastLineContent.trim() !== '-' && - curLineNumber === lineCount - 1 - ) { + if (key === '\n' && inputState === 'utterance' && lastLineContent.trim() !== '-') { const newPos = Position.create(pos.line + 1, 0); const item: TextEdit = TextEdit.insert(newPos, '- '); edits.push(item); @@ -257,21 +251,23 @@ export class LUServer { } } - if ( - key === '\n' && - inputState === 'listEntity' && - lastLineContent.trim() !== '-' && - curLineNumber === lineCount - 1 - ) { - const newPos = Position.create(pos.line + 1, 0); + if (key === '\n' && inputState === 'listEntity' && lastLineContent.trim() !== '-') { + const newPos = Position.create(pos.line, 0); let insertStr = ''; + const indentLevel = this.getIndentLevel(lastLineContent); if (lastLineContent.trim().endsWith(':') || lastLineContent.trim().endsWith('=')) { - insertStr = '\t-'; + insertStr = '\t'.repeat(indentLevel + 1) + '-'; } else { - insertStr = '-'; + insertStr = '\t'.repeat(indentLevel) + '-'; } + const item: TextEdit = TextEdit.insert(newPos, insertStr); edits.push(item); + + //delete redundent \t from autoIndent + const deleteRange = Range.create(pos.line, pos.character - indentLevel, pos.line, pos.character); + const deleteItem: TextEdit = TextEdit.del(deleteRange); + edits.push(deleteItem); } if (lastLineContent.trim() === '-') { @@ -297,10 +293,31 @@ export class LUServer { } } + private getIndentLevel(lineContent: string): number { + if (lineContent.includes('-')) { + const tabStr = lineContent.split('-')[0]; + let numOfTab = 0; + let validIndentStr = true; + tabStr.split('').forEach(u => { + if (u === '\t') { + numOfTab += 1; + } else { + validIndentStr = false; + } + }); + + if (validIndentStr) { + return numOfTab; + } + } + + return 0; + } + private getInputLineState(params: DocumentOnTypeFormattingParams): LineState { const document = this.documents.get(params.textDocument.uri); const position = params.position; - const regListEnity = /^\s*@\s*list\s*.*$/; + const regListEnity = /^\s*@\s*(list|phraseList)\s*.*$/; const regUtterance = /^\s*#.*$/; const regDashLine = /^\s*-.*$/; const mlEntity = /^\s*@\s*ml\s*.*$/; @@ -382,12 +399,14 @@ export class LUServer { .join('\n'); const completionList: CompletionItem[] = []; if (util.isEntityType(curLineContent)) { + const triggerChar = curLineContent[position.character - 1]; + const extraWhiteSpace = triggerChar === '@' ? ' ' : ''; const entityTypes: string[] = EntityTypesObj.EntityType; entityTypes.forEach(entity => { const item = { label: entity, kind: CompletionItemKind.Keyword, - insertText: `${entity}`, + insertText: `${extraWhiteSpace}${entity}`, documentation: `Enitity type: ${entity}`, }; @@ -397,11 +416,13 @@ export class LUServer { if (util.isPrebuiltEntity(curLineContent)) { const prebuiltTypes: string[] = EntityTypesObj.Prebuilt; + const triggerChar = curLineContent[position.character - 1]; + const extraWhiteSpace = triggerChar !== ' ' ? ' ' : ''; prebuiltTypes.forEach(entity => { const item = { label: entity, kind: CompletionItemKind.Keyword, - insertText: `${entity}`, + insertText: `${extraWhiteSpace}${entity}`, documentation: `Prebuilt enitity: ${entity}`, }; @@ -439,6 +460,17 @@ export class LUServer { completionList.push(item2); } + if (util.isPhraseListEntity(curLineContent)) { + const item = { + label: 'interchangeable synonyms?', + kind: CompletionItemKind.Keyword, + insertText: `interchangeable`, + documentation: `interchangeable synonyms as part of the entity definition`, + }; + + completionList.push(item); + } + // completion for entities and patterns, use the text without current line due to usually it will cause parser errors, the luisjson will be undefined let luisJson = await this.extractLUISContent(text); diff --git a/Composer/packages/tools/language-servers/language-understanding/src/entityEnum.ts b/Composer/packages/tools/language-servers/language-understanding/src/entityEnum.ts index c30b5c1039..778e42895f 100644 --- a/Composer/packages/tools/language-servers/language-understanding/src/entityEnum.ts +++ b/Composer/packages/tools/language-servers/language-understanding/src/entityEnum.ts @@ -2,7 +2,7 @@ // Licensed under the MIT License. export const EntityTypesObj = { - EntityType: ['ml', 'prebuilt', 'regex', 'list', 'composite', 'patternany', 'phraselist'], + EntityType: ['ml', 'prebuilt', 'regex', 'list', 'composite', 'Pattern.any', 'phraseList'], Prebuilt: [ 'age', 'datetimeV2', diff --git a/Composer/packages/tools/language-servers/language-understanding/src/matchingPattern.ts b/Composer/packages/tools/language-servers/language-understanding/src/matchingPattern.ts index 368768c2b9..8d574d6ffc 100644 --- a/Composer/packages/tools/language-servers/language-understanding/src/matchingPattern.ts +++ b/Composer/packages/tools/language-servers/language-understanding/src/matchingPattern.ts @@ -82,8 +82,8 @@ export function isSeperatedEntityDef(content: string): boolean { } export function isEntityName(content: string): boolean { - const hasNameEntifyDef = /^\s*@\s*(ml|list|regex|prebuilt|composite|patternany|phraselist)\s*([\w._]+|"[\w._\s]+")\s*$/; - const hasTypeEntityDef = /^\s*@\s*(ml|list|regex|prebuilt|composite|patternany|phraselist|intent)\s*$/; + const hasNameEntifyDef = /^\s*@\s*(ml|list|regex|prebuilt|composite|Pattern\.any|phraseList)\s*([\w._]+|"[\w._\s]+")\s*$/; + const hasTypeEntityDef = /^\s*@\s*(ml|list|regex|prebuilt|composite|Pattern\.any|phraseList|intent)\s*$/; const hasNameEntifyDef2 = /^\s*@\s*([\w._]+|"[\w._\s]+")\s*$/; return hasNameEntifyDef.test(content) || (!hasTypeEntityDef.test(content) && hasNameEntifyDef2.test(content)); } @@ -93,6 +93,12 @@ export function isCompositeEntity(content: string): boolean { const compositePatternDef2 = /^\s*@\s*composite\s*[\w]*\s*=\s*\[\s*.*\s*\]\s*$/; return compositePatternDef.test(content) || compositePatternDef2.test(content); } + +export function isPhraseListEntity(content: string): boolean { + const phraseListEntityPatternDef = /^\s*@\s*phraseList\s*[\w]+\s*\(\s*$/; + return phraseListEntityPatternDef.test(content); +} + export function matchedEnterPattern(content: string): boolean { const regexPatternDef = /^\s*-.*{\s*$/; const regexPatternDef2 = /^\s*-.*{\s*}$/; @@ -147,7 +153,7 @@ export const suggestionAllEntityTypes = [ 'patternAnyEntities', 'preBuiltEntities', 'closedLists', - 'phraselists', + 'phraseLists', 'composites', ]; @@ -156,7 +162,7 @@ export const suggestionNoPatternAnyEntityTypes = [ 'regex_entities', 'preBuiltEntities', 'closedLists', - 'phraselists', + 'phraseLists', 'composites', ]; @@ -166,7 +172,7 @@ export const suggestionNoCompositeEntityTypes = [ 'patternAnyEntities', 'preBuiltEntities', 'closedLists', - 'phraselists', + 'phraseLists', ]; export function getSuggestionRoles(luisJson: any, suggestionEntityTypes: string[]): string[] { From 482aaf7a2377c3dd9f72b8801a31ba96751aef6b Mon Sep 17 00:00:00 2001 From: zeye <2295905420@qq.com> Date: Fri, 28 Feb 2020 00:35:16 +0800 Subject: [PATCH 10/40] feat: display 6 actions as contentless node (#2108) * display 6 types as contentless node * fix ut * header color to darker gray * fix UT * put copyright ahead --- .../layouters/measureJsonBoundary.test.ts | 6 +- .../src/constants/ElementColors.ts | 2 +- .../src/constants/ElementSizes.ts | 5 +- .../src/layouters/measureJsonBoundary.ts | 10 +++ .../visual-designer/src/schema/uischema.tsx | 19 +++-- .../src/widgets/ActionHeader.tsx | 79 +++++++++++++++++++ 6 files changed, 106 insertions(+), 15 deletions(-) create mode 100644 Composer/packages/extensions/visual-designer/src/widgets/ActionHeader.tsx diff --git a/Composer/packages/extensions/visual-designer/__tests__/layouters/measureJsonBoundary.test.ts b/Composer/packages/extensions/visual-designer/__tests__/layouters/measureJsonBoundary.test.ts index 54f3ca632a..8e1cb2310d 100644 --- a/Composer/packages/extensions/visual-designer/__tests__/layouters/measureJsonBoundary.test.ts +++ b/Composer/packages/extensions/visual-designer/__tests__/layouters/measureJsonBoundary.test.ts @@ -11,6 +11,8 @@ import { ChoiceInputSize, ChoiceInputMarginTop, ChoiceInputMarginBottom, + StandardNodeWidth, + HeaderHeight, } from '../../src/constants/ElementSizes'; describe('measureJsonBoundary', () => { @@ -30,9 +32,7 @@ describe('measureJsonBoundary', () => { expect(measureJsonBoundary({ $type: ObiTypes.LoopIndicator })).toEqual( new Boundary(LoopIconSize.width, LoopIconSize.height) ); - expect(measureJsonBoundary({ $type: ObiTypes.LogAction })).toEqual( - new Boundary(InitNodeSize.width, InitNodeSize.height) - ); + expect(measureJsonBoundary({ $type: ObiTypes.LogAction })).toEqual(new Boundary(StandardNodeWidth, HeaderHeight)); }); it("should return boundary whose size is determined by the data's choices when json.$type is choiceInput", () => { const data1: { [key: string]: any } = { diff --git a/Composer/packages/extensions/visual-designer/src/constants/ElementColors.ts b/Composer/packages/extensions/visual-designer/src/constants/ElementColors.ts index 4c22410045..7d3f192b92 100644 --- a/Composer/packages/extensions/visual-designer/src/constants/ElementColors.ts +++ b/Composer/packages/extensions/visual-designer/src/constants/ElementColors.ts @@ -8,7 +8,7 @@ const Colors = { Black: '#000000', AzureGray: '#3C3C41', AzureGray2: '#656565', - AzureGray3: '#EBEBEB', + AzureGray3: '#D7D7D7', Gray80: '#B3B0AD', Gray60: '#C8C6C4', diff --git a/Composer/packages/extensions/visual-designer/src/constants/ElementSizes.ts b/Composer/packages/extensions/visual-designer/src/constants/ElementSizes.ts index a683fbb397..1ec57683df 100644 --- a/Composer/packages/extensions/visual-designer/src/constants/ElementSizes.ts +++ b/Composer/packages/extensions/visual-designer/src/constants/ElementSizes.ts @@ -1,8 +1,11 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +export const StandardNodeWidth = 200; +export const HeaderHeight = 24; + export const InitNodeSize = { - width: 200, + width: StandardNodeWidth, height: 48, }; diff --git a/Composer/packages/extensions/visual-designer/src/layouters/measureJsonBoundary.ts b/Composer/packages/extensions/visual-designer/src/layouters/measureJsonBoundary.ts index 4937873417..cbea2e38e3 100644 --- a/Composer/packages/extensions/visual-designer/src/layouters/measureJsonBoundary.ts +++ b/Composer/packages/extensions/visual-designer/src/layouters/measureJsonBoundary.ts @@ -4,6 +4,8 @@ import { ObiTypes } from '../constants/ObiTypes'; import { Boundary } from '../models/Boundary'; import { + StandardNodeWidth, + HeaderHeight, DiamondSize, InitNodeSize, LoopIconSize, @@ -124,6 +126,14 @@ export function measureJsonBoundary(json): Boundary { case ObiTypes.InvalidPromptBrick: boundary = new Boundary(IconBrickSize.width, IconBrickSize.height); break; + case ObiTypes.EndDialog: + case ObiTypes.EndTurn: + case ObiTypes.RepeatDialog: + case ObiTypes.CancelAllDialogs: + case ObiTypes.LogAction: + case ObiTypes.TraceActivity: + boundary = new Boundary(StandardNodeWidth, HeaderHeight); + break; default: boundary = new Boundary(InitNodeSize.width, InitNodeSize.height); break; diff --git a/Composer/packages/extensions/visual-designer/src/schema/uischema.tsx b/Composer/packages/extensions/visual-designer/src/schema/uischema.tsx index a8177ac85f..07d454c2af 100644 --- a/Composer/packages/extensions/visual-designer/src/schema/uischema.tsx +++ b/Composer/packages/extensions/visual-designer/src/schema/uischema.tsx @@ -14,6 +14,7 @@ import { IfConditionWidget } from '../widgets/IfConditionWidget'; import { SwitchConditionWidget } from '../widgets/SwitchConditionWidget'; import { ForeachWidget } from '../widgets/ForeachWidget'; import { ChoiceInputChoices } from '../widgets/ChoiceInput'; +import { ActionHeader } from '../widgets/ActionHeader'; import { ElementIcon } from '../utils/obiPropertyResolver'; import { ObiColors } from '../constants/ElementColors'; @@ -136,16 +137,16 @@ export const uiSchema: UISchema = { content: data => `Delete ${Array.isArray(data.properties) ? data.properties.length : 0} properties`, }, [SDKTypes.EndDialog]: { - 'ui:widget': ActionCard, - content: 'End this dialog', + 'ui:widget': ActionHeader, + }, + [SDKTypes.RepeatDialog]: { + 'ui:widget': ActionHeader, }, [SDKTypes.CancelAllDialogs]: { - 'ui:widget': ActionCard, - content: 'Cancel all active dialogs', + 'ui:widget': ActionHeader, }, [SDKTypes.EndTurn]: { - 'ui:widget': ActionCard, - content: 'Wait for another message', + 'ui:widget': ActionHeader, }, [SDKTypes.EmitEvent]: { 'ui:widget': ActionCard, @@ -156,12 +157,10 @@ export const uiSchema: UISchema = { content: data => data.url, }, [SDKTypes.TraceActivity]: { - 'ui:widget': ActionCard, - content: data => data.name, + 'ui:widget': ActionHeader, }, [SDKTypes.LogAction]: { - 'ui:widget': ActionCard, - content: data => data.text, + 'ui:widget': ActionHeader, }, [SDKTypes.EditActions]: { 'ui:widget': ActionCard, diff --git a/Composer/packages/extensions/visual-designer/src/widgets/ActionHeader.tsx b/Composer/packages/extensions/visual-designer/src/widgets/ActionHeader.tsx new file mode 100644 index 0000000000..e8b7db1dd6 --- /dev/null +++ b/Composer/packages/extensions/visual-designer/src/widgets/ActionHeader.tsx @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** @jsx jsx */ +import { jsx, css } from '@emotion/core'; +import { generateSDKTitle } from '@bfc/shared'; + +import { WidgetComponent, WidgetContainerProps } from '../schema/uischema.types'; +import { StandardNodeWidth, HeaderHeight } from '../constants/ElementSizes'; +import { ObiColors } from '../constants/ElementColors'; +import { NodeMenu } from '../components/menus/NodeMenu'; + +export interface ActionHeaderProps extends WidgetContainerProps { + title: string; + disableSDKTitle?: boolean; + menu?: JSX.Element | 'none'; + colors: { + theme: string; + icon: string; + }; +} + +const DefaultColors = { + theme: ObiColors.AzureGray3, + icon: ObiColors.AzureGray2, +}; + +const container = css` + cursor: pointer; + font-family: Segoe UI; + font-size: 12px; + line-height: 14px; + color: black; +`; + +const header = css` + font-size: 12px; + font-family: Segoe UI; + line-height: 14px; + overflow: hidden; + text-overflow: ellipsis; + whitespace: pre; +`; + +export const ActionHeader: WidgetComponent = ({ + id, + data, + onEvent, + title, + disableSDKTitle, + menu, + colors = DefaultColors, +}) => { + const headerContent = disableSDKTitle ? title : generateSDKTitle(data, title); + + return ( +
+
+ {headerContent} +
+
+ {menu === 'none' ? null : menu || } +
+
+ ); +}; From 8b0510be6b7b0b81933453dcbe22b0e18e0f8f6a Mon Sep 17 00:00:00 2001 From: liweitian Date: Fri, 28 Feb 2020 00:51:49 +0800 Subject: [PATCH 11/40] fix: update lu format link (#2107) * update lu format link * use aka link for lu file format and point to latest emulator release * remove anchor from lu file format aka links Co-authored-by: Andy Brown --- .../lib/code-editor/demo/src/richEditor.tsx | 3 +- .../packages/lib/code-editor/src/LuEditor.tsx | 2 +- docs/Create-Your-First-Bot.md | 4 +- docs/CreateYourFirstBot.md | 41 +++--- docs/concept-events-and-triggers.md | 2 +- ...how-to-define-advanced-intents-entities.md | 119 ++++++++------- docs/how-to-define-triggers.md | 136 ++++++++++-------- docs/how-to-use-LUIS.md | 63 ++++---- docs/how-to-use-validation.md | 32 ++--- docs/olddocs/CreateYourFirstBot.md | 41 +++--- docs/olddocs/testing_debugging.md | 4 +- docs/olddocs/triggers_and_events.md | 4 +- docs/tutorial/tutorial-luis.md | 74 +++++----- toc.md | 50 ++++--- 14 files changed, 309 insertions(+), 266 deletions(-) diff --git a/Composer/packages/lib/code-editor/demo/src/richEditor.tsx b/Composer/packages/lib/code-editor/demo/src/richEditor.tsx index 6784fa1030..0e345fdc47 100644 --- a/Composer/packages/lib/code-editor/demo/src/richEditor.tsx +++ b/Composer/packages/lib/code-editor/demo/src/richEditor.tsx @@ -5,8 +5,7 @@ import React, { useState } from 'react'; import { RichEditor } from '../../src'; -const LU_HELP = - 'https://github.com/Microsoft/botbuilder-tools/blob/master/packages/Ludown/docs/lu-file-format.md#lu-file-format'; +const LU_HELP = 'https://aka.ms/lu-file-format'; const content = `# Greeting -Good morning diff --git a/Composer/packages/lib/code-editor/src/LuEditor.tsx b/Composer/packages/lib/code-editor/src/LuEditor.tsx index 8b584900b8..2df912c55e 100644 --- a/Composer/packages/lib/code-editor/src/LuEditor.tsx +++ b/Composer/packages/lib/code-editor/src/LuEditor.tsx @@ -12,7 +12,7 @@ import { registerLULanguage } from './languages'; import { createUrl, createWebSocket, createLanguageClient } from './utils/lspUtil'; import { RichEditor, RichEditorProps } from './RichEditor'; -const LU_HELP = 'https://github.com/microsoft/botframework-cli/blob/master/packages/luis/docs/lu-file-format.md'; +const LU_HELP = 'https://aka.ms/lu-file-format'; const placeholder = `> To learn more about the LU file format, read the documentation at > ${LU_HELP}`; diff --git a/docs/Create-Your-First-Bot.md b/docs/Create-Your-First-Bot.md index 996ccbb039..6792514327 100644 --- a/docs/Create-Your-First-Bot.md +++ b/docs/Create-Your-First-Bot.md @@ -5,7 +5,7 @@ Follow these six steps to create a weather bot from scratch using Bot Framework ## Prerequisites - [Bot Framework Composer](https://github.com/microsoft/BotFramework-Composer) -- Bot Framework Emulator ([download](https://github.com/microsoft/BotFramework-Emulator/releases/tag/v4.5.2)) +- Bot Framework Emulator ([download](https://github.com/microsoft/BotFramework-Emulator/releases/latest)) - LUIS authoring key ([where to find](https://docs.microsoft.com/en-us/azure/cognitive-services/luis/luis-concept-keys#programmatic-key)) ## Step 1: Creating a new bot @@ -22,7 +22,7 @@ On the right-hand side of the page, select **BF Language Understanding** for **R In the text editor, type in the intents and utterances, then click **Save**. -**Note**: You should use the correct file format to edit the recognizer (see example below). Follow this [link](https://github.com/Microsoft/botbuilder-tools/blob/master/packages/Ludown/docs/lu-file-format.md#lu-file-format) to learn more about the LU format. +**Note**: You should use the correct file format to edit the recognizer (see example below). Follow this [link](https://aka.ms/lu-file-format) to learn more about the LU format. ![screenshot: editing the LU](Assets/LU.png) diff --git a/docs/CreateYourFirstBot.md b/docs/CreateYourFirstBot.md index 3c526462b1..6792514327 100644 --- a/docs/CreateYourFirstBot.md +++ b/docs/CreateYourFirstBot.md @@ -1,74 +1,77 @@ # Create your first bot + Follow these six steps to create a weather bot from scratch using Bot Framework Composer. Alternatively, watch the [video](https://microsoft-my.sharepoint.com/:v:/p/t-yangxi/EVcxF6YjGKZOvJjPZstfS5EBLVlDldzoZ5yQiqJlNs_NKw?e=zkzymp). ## Prerequisites -* [Bot Framework Composer](https://github.com/microsoft/BotFramework-Composer) -* Bot Framework Emulator ([download](https://github.com/microsoft/BotFramework-Emulator/releases/tag/v4.5.2)) -* LUIS authoring key ([where to find](https://docs.microsoft.com/en-us/azure/cognitive-services/luis/luis-concept-keys#programmatic-key)) + +- [Bot Framework Composer](https://github.com/microsoft/BotFramework-Composer) +- Bot Framework Emulator ([download](https://github.com/microsoft/BotFramework-Emulator/releases/latest)) +- LUIS authoring key ([where to find](https://docs.microsoft.com/en-us/azure/cognitive-services/luis/luis-concept-keys#programmatic-key)) ## Step 1: Creating a new bot -On the Composer homepage, click **New**. Select **Create from scratch**. Click **Next**. Give your bot a name, for example *WeatherBot*. Once created, you should be able to see the following screen. -![screenshot: creating a new bot](Assets/DesignFlow.png) +On the Composer homepage, click **New**. Select **Create from scratch**. Click **Next**. Give your bot a name, for example _WeatherBot_. Once created, you should be able to see the following screen. +![screenshot: creating a new bot](Assets/DesignFlow.png) ## Step 2: Setting up the intent recognizer + On the right-hand side of the page, select **BF Language Understanding** for **Recognizer Type**. ![screenshot: setting up the recognizer](Assets/Recognizer.png) In the text editor, type in the intents and utterances, then click **Save**. -**Note**: You should use the correct file format to edit the recognizer (see example below). Follow this [link](https://github.com/Microsoft/botbuilder-tools/blob/master/packages/Ludown/docs/lu-file-format.md#lu-file-format) to learn more about the LU format. +**Note**: You should use the correct file format to edit the recognizer (see example below). Follow this [link](https://aka.ms/lu-file-format) to learn more about the LU format. ![screenshot: editing the LU](Assets/LU.png) - ## Step 3: Creating event handlers to handle the intents + On the **Events** pane, click the **add icon**, then select **Handle an Intent**. create intent handler -On the right-hand side of the page, give the intent a name, for example, *GreetingIntentHandler*. In the **Intent** field, type in the name of the intent that this event is intended to handle. **Note**: The name should match the intent name in the recognizer. +On the right-hand side of the page, give the intent a name, for example, _GreetingIntentHandler_. In the **Intent** field, type in the name of the intent that this event is intended to handle. **Note**: The name should match the intent name in the recognizer. -It's a good practice to create a handler for each intent. In this guide, we’ve created two handlers named *GreetingIntentHandler* and *CheckWeatherIntentHandler*. +It's a good practice to create a handler for each intent. In this guide, we’ve created two handlers named _GreetingIntentHandler_ and _CheckWeatherIntentHandler_. Intent name - ## Step 4: Adding actions to the handlers Here we define the actions that the bot will take when an intent is recognized. You can add actions such as sending messages or performing computational tasks. You can also call a dialog ([SDK docs: Bot Framework Dialogs](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-concept-dialog?view=azure-bot-service-4.0)) as part of the actions. -### 4.1: Adding actions to *GreetingIntentHandler* -Double-click on the *GreetingIntentHandler* node to get started. +### 4.1: Adding actions to _GreetingIntentHandler_ + +Double-click on the _GreetingIntentHandler_ node to get started. Other event handler -By design, we want the bot to send a message to the user in response to the *Greeting* intent. To do this, click the **add icon**, select **Send Messages**, then select **Send Activity**. On the **Language Generation** pane, type in the bot response. For example, *Hello from the bot!*. +By design, we want the bot to send a message to the user in response to the _Greeting_ intent. To do this, click the **add icon**, select **Send Messages**, then select **Send Activity**. On the **Language Generation** pane, type in the bot response. For example, _Hello from the bot!_. ![screenshot: LG](Assets/AddActions.png) -### 4.2 Adding actions to *CheckWeatherIntentHandler* -Double-click on the *CheckWeatherIntentHandler* node to get started. By design, we want the bot to begin a dialog (and take actions as specified in that dialog) in response to the *CheckWeather* intent. To do this, we first need to create a dialog (e.g. *CheckWeatherDialog*), then add this dialog to the *CheckWeatherIntentHandler*. +### 4.2 Adding actions to _CheckWeatherIntentHandler_ + +Double-click on the _CheckWeatherIntentHandler_ node to get started. By design, we want the bot to begin a dialog (and take actions as specified in that dialog) in response to the _CheckWeather_ intent. To do this, we first need to create a dialog (e.g. _CheckWeatherDialog_), then add this dialog to the _CheckWeatherIntentHandler_. To create a dialog, click **Add** on the **Dialogs** pane, and follow the instructions to complete the process. Once a dialog is created, add actions to this dialog following the same process as described in 4.1. Other event handler - -To add the newly created dialog to the event, first, go back to the root dialog, then select the event to which you want to add the dialog. Click the **add icon**, select **Dialogs**, select **Begin a Dialog**, then choose a dialog from the list. +To add the newly created dialog to the event, first, go back to the root dialog, then select the event to which you want to add the dialog. Click the **add icon**, select **Dialogs**, select **Begin a Dialog**, then choose a dialog from the list. ![screenshot: LG](Assets/BeginADialog.png) - ## Step 5: (Optional) Creating handlers for other types of events + Besides recognized intents, you may want to handle other types of events, for example, when there are unrecognized intents or when a user joins the conversation. Creating handlers for these types of events is no different from creating intent handlers. You just need to select a different event type as shown below. For example, select **Greeting (ConversationUpdate)** if you want to send a welcome message to the user when they first join the conversation (when a conversationUpdate event is trigged). Other event handler - ## Step 6: Testing your bot + Now your bot is ready for testing! Click **Connect/Reload** on the top right corner of the screen, then select **Test in Emulator**. **Note**: You may be prompted to **Publish LUIS models**. Please follow the link to the LUIS site, go to **Settings**, copy the **Authoring Key**, and fill out the fields to publish LUIS models. diff --git a/docs/concept-events-and-triggers.md b/docs/concept-events-and-triggers.md index 268686d479..5da7b3450b 100644 --- a/docs/concept-events-and-triggers.md +++ b/docs/concept-events-and-triggers.md @@ -96,7 +96,7 @@ For additional information, see [Custom event](how-to-define-triggers.md#custom- ## Further reading - [Adaptive dialog: Recognizers, rules, steps and inputs](https://github.com/microsoft/BotBuilder-Samples/blob/master/experimental/adaptive-dialog/docs/recognizers-rules-steps-reference.md#Rules) -- [.lu format file](https://github.com/microsoft/botbuilder-tools/blob/master/packages/Ludown/docs/lu-file-format.md) +- [.lu format file](https://aka.ms/lu-file-format) - [RegEx recognizer and LUIS recognizer](https://github.com/microsoft/BotBuilder-Samples/blob/master/experimental/adaptive-dialog/docs/recognizers-rules-steps-reference.md#regex-recognizer) ## Next diff --git a/docs/how-to-define-advanced-intents-entities.md b/docs/how-to-define-advanced-intents-entities.md index ee814edcfc..01677bbb76 100644 --- a/docs/how-to-define-advanced-intents-entities.md +++ b/docs/how-to-define-advanced-intents-entities.md @@ -1,17 +1,20 @@ -# Advanced intent and entity definition -In this article, we will cover some details of how LUIS recognizer extracts the intent and entity you may define in Composer. We will also instruct on how to define the different types of entities as shown in the [ToDoBotWithLuisSample](https://github.com/microsoft/BotFramework-Composer/tree/master/Composer/packages/server/assets/projects/ToDoBotWithLuisSample). +# Advanced intent and entity definition -## Prerequisites: -- basic knowledge of [intent and entity](concept-language-understanding.md#core-lu-concepts-in-composer) +In this article, we will cover some details of how LUIS recognizer extracts the intent and entity you may define in Composer. We will also instruct on how to define the different types of entities as shown in the [ToDoBotWithLuisSample](https://github.com/microsoft/BotFramework-Composer/tree/master/Composer/packages/server/assets/projects/ToDoBotWithLuisSample). + +## Prerequisites: + +- basic knowledge of [intent and entity](concept-language-understanding.md#core-lu-concepts-in-composer) - basic knowledge of [how to define an intent trigger](howto-defining-triggers.md#intent) - basic knowledge of [how to use LUIS in Composer](howto-using-LUIS.md) - LUIS account (apply [here](https://www.luis.ai/home)) - LUIS authoring key (how to get [here](https://docs.microsoft.com/en-us/azure/cognitive-services/luis/luis-concept-keys?tabs=V2#programmatic-key)) ## LUIS for entity extraction -In addition to specifying intents and utterances as instructed in the [how to use LUIS in Composer](howto-using-LUIS.md) article, it is also possible to train LUIS to recognize named entities. Read more about the full capabilities of LUIS recognizers [here](https://github.com/microsoft/botbuilder-tools/blob/master/packages/Ludown/docs/lu-file-format.md). -Extracted entities are passed along to any triggered actions or child dialogs using the syntax `@{Entity Name}`. For example, given an intent definition like below: +In addition to specifying intents and utterances as instructed in the [how to use LUIS in Composer](howto-using-LUIS.md) article, it is also possible to train LUIS to recognize named entities. Read more about the full capabilities of LUIS recognizers [here](https://aka.ms/lu-file-format). + +Extracted entities are passed along to any triggered actions or child dialogs using the syntax `@{Entity Name}`. For example, given an intent definition like below: ``` # BookFlight @@ -20,7 +23,7 @@ Extracted entities are passed along to any triggered actions or child dialogs us - i want to go to {city=paris} ``` -When triggered, if LUIS is able to identify a city, the city name will be made available as `@city` within the triggered actions. The entity value can be used directly in expressions and LG templates, or [stored into a memory property](concept-memory.md) for later use. The JSON view of the query "book me a flight to London" in LUIS app looks like this: +When triggered, if LUIS is able to identify a city, the city name will be made available as `@city` within the triggered actions. The entity value can be used directly in expressions and LG templates, or [stored into a memory property](concept-memory.md) for later use. The JSON view of the query "book me a flight to London" in LUIS app looks like this: ```json { @@ -34,9 +37,7 @@ When triggered, if LUIS is able to identify a city, the city name will be made a } }, "entities": { - "city": [ - "london" - ], + "city": ["london"], "$instance": { "city": [ { @@ -47,9 +48,7 @@ When triggered, if LUIS is able to identify a city, the city name will be made a "score": 0.834206, "modelTypeId": 1, "modelType": "Entity Extractor", - "recognitionSources": [ - "model" - ] + "recognitionSources": ["model"] } ] } @@ -59,53 +58,62 @@ When triggered, if LUIS is able to identify a city, the city name will be made a ``` ## Entities of different types -In Composer, you can also define entities of different types. In this section, we focus on the following entity types and their definitions. For more entity types read [here](https://github.com/microsoft/botbuilder-tools/blob/master/packages/Ludown/docs/lu-file-format.md). + +In Composer, you can also define entities of different types. In this section, we focus on the following entity types and their definitions. For more entity types read [here](https://aka.ms/lu-file-format). | Type | Description | | ------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------- | | [Simple](https://docs.microsoft.com/en-us/azure/cognitive-services/luis/luis-quickstart-primary-and-secondary-data) | extract a single data concept contained in words or phrases | | [List](https://docs.microsoft.com/en-us/azure/cognitive-services/luis/luis-quickstart-intent-and-list-entity) | extract entity data that matches a predefined list of items | -### Simple entity -The purpose of defining a Simple type entity is to teach LUIS to detect a single concept data in an utterance. +### Simple entity + +The purpose of defining a Simple type entity is to teach LUIS to detect a single concept data in an utterance. Use Simple type entities when the data has the following characteristics: -- The data is a single concept. -- The data is not well-formatted. -- The data is not a prebuilt entity. + +- The data is a single concept. +- The data is not well-formatted. +- The data is not a prebuilt entity. - The data does not match exactly to a list of known words. -- The data does not contain other data items. +- The data does not contain other data items. -You can define [Simple]((https://docs.microsoft.com/en-us/azure/cognitive-services/luis/luis-quickstart-primary-and-secondary-data)) type entities using $\:simple notation. Here is an example from the [ToDoBotWithLuisSample](https://github.com/microsoft/BotFramework-Composer/tree/master/Composer/packages/server/assets/projects/ToDoBotWithLuisSample): +You can define [Simple](<(https://docs.microsoft.com/en-us/azure/cognitive-services/luis/luis-quickstart-primary-and-secondary-data)>) type entities using \$\:simple notation. Here is an example from the [ToDoBotWithLuisSample](https://github.com/microsoft/BotFramework-Composer/tree/master/Composer/packages/server/assets/projects/ToDoBotWithLuisSample): ```markdown -> Entity definitions -$ itemTitle : simple +> Entity definitions +> \$ itemTitle : simple + +> AddItem intent that contains simple entity -> AddItem intent that contains simple entity # AddItem + - Add todo - add a to do item - could i add {itemTitle=medicine} to the todos list - add {itemTitle} to my todo list -... + ... ``` -This entity definition means that if the `AddItem` intent is detected, the extracted `itemTile` entity value will be passed along to any triggered actions or child dialogs, and is available as `@itemTitle`. ->![NOTE] +This entity definition means that if the `AddItem` intent is detected, the extracted `itemTile` entity value will be passed along to any triggered actions or child dialogs, and is available as `@itemTitle`. + +> ![NOTE] > For any labelled entity that is not explicitly assigned a type, the ludown parser defaults to simple entity type for that entity. ```markdown -# getUserName +# getUserName + - my name is {username=susan} -> Without any explicit entity type definition, the entity defaults to 'Simple' entity type. +> Without any explicit entity type definition, the entity defaults to 'Simple' entity type. ``` -### List entity -The purpose of defining a List type entity is to teach LUIS to get entity data that matches a predefined list of items. +### List entity + +The purpose of defining a List type entity is to teach LUIS to get entity data that matches a predefined list of items. Use List type entities when the data has the following characteristics: + - Values of the data are a known set. - The data does not exceed maximum [LUIS boundaries](https://docs.microsoft.com/en-us/azure/cognitive-services/luis/luis-boundaries). - The text in the utterance is an exact match with a synonym or the canonical name. @@ -113,39 +121,41 @@ Use List type entities when the data has the following characteristics: You can define [List](https://docs.microsoft.com/en-us/azure/cognitive-services/luis/luis-quickstart-intent-and-list-entity) type entities using the following notation: ```markdown -$listEntity:\= - - \ - - \ +\$listEntity:\= - \ - \ ``` -When using list entity, you should include a value from the list directly in the utterance, not an entity label or any other value. Here is an example definition of a list entity from the [ToDoBotWithLuisSample](https://github.com/microsoft/BotFramework-Composer/tree/master/Composer/packages/server/assets/projects/ToDoBotWithLuisSample): +When using list entity, you should include a value from the list directly in the utterance, not an entity label or any other value. Here is an example definition of a list entity from the [ToDoBotWithLuisSample](https://github.com/microsoft/BotFramework-Composer/tree/master/Composer/packages/server/assets/projects/ToDoBotWithLuisSample): ```markdown > Add a list entity to detect list tye -$ listType : todo = +> \$ listType : todo = + - to do - todos -- laundry +- laundry -$ listType : shopping = -- shopping +\$ listType : shopping = + +- shopping - shop - shoppers -$ listType : grocery = +\$ listType : grocery = + - groceries - fruits - vegetables - household items - house hold items ``` -This entity definition contains three lists of entities. Based on the user's input, any extracted normalized entity value (e.g. todo/shopping/grocery) will be available as `@listType` within any triggered actions. -After publishing the model of [ToDoBotWithLuisSample](https://github.com/microsoft/BotFramework-Composer/tree/master/Composer/packages/server/assets/projects/ToDoBotWithLuisSample) to [LUIS](https://www.luis.ai/), you can view in [LUIS](https://www.luis.ai/) the intents and entities you have defined in Composer: +This entity definition contains three lists of entities. Based on the user's input, any extracted normalized entity value (e.g. todo/shopping/grocery) will be available as `@listType` within any triggered actions. + +After publishing the model of [ToDoBotWithLuisSample](https://github.com/microsoft/BotFramework-Composer/tree/master/Composer/packages/server/assets/projects/ToDoBotWithLuisSample) to [LUIS](https://www.luis.ai/), you can view in [LUIS](https://www.luis.ai/) the intents and entities you have defined in Composer: ![entities](./media/entities/entities.png) -And test the model using an example utterance such as "add an apple to my shopping list". The JSON view is as follows: +And test the model using an example utterance such as "add an apple to my shopping list". The JSON view is as follows: ```json { @@ -159,14 +169,8 @@ And test the model using an example utterance such as "add an apple to my shoppi } }, "entities": { - "itemTitle": [ - "apple" - ], - "listType": [ - [ - "shopping" - ] - ], + "itemTitle": ["apple"], + "listType": [["shopping"]], "$instance": { "itemTitle": [ { @@ -177,9 +181,7 @@ And test the model using an example utterance such as "add an apple to my shoppi "score": 0.905890763, "modelTypeId": 1, "modelType": "Entity Extractor", - "recognitionSources": [ - "model" - ] + "recognitionSources": ["model"] } ], "listType": [ @@ -190,19 +192,16 @@ And test the model using an example utterance such as "add an apple to my shoppi "length": 8, "modelTypeId": 5, "modelType": "List Entity Extractor", - "recognitionSources": [ - "model" - ] + "recognitionSources": ["model"] } ] } } } } - ``` # Further reading -- [Entities and their purpose in LUIS](https://docs.microsoft.com/en-us/azure/cognitive-services/luis/luis-concept-entity-types) -- [.lu file format](https://github.com/microsoft/botbuilder-tools/blob/master/packages/Ludown/docs/lu-file-format.md) +- [Entities and their purpose in LUIS](https://docs.microsoft.com/en-us/azure/cognitive-services/luis/luis-concept-entity-types) +- [.lu file format](https://aka.ms/lu-file-format) diff --git a/docs/how-to-define-triggers.md b/docs/how-to-define-triggers.md index 4ba6a62024..508284c4d2 100644 --- a/docs/how-to-define-triggers.md +++ b/docs/how-to-define-triggers.md @@ -1,18 +1,20 @@ # Defining triggers + Each dialog in the Bot Framework Composer includes a set of triggers (event handlers) that contain actions (instructions) for how the bot will respond to inputs received when the dialog is active. There are several different types of triggers in Composer. They all work in a similar manner and can even be interchanged in some cases. This article explains how to define each type of trigger. Before you walk through this article, please read the [events and triggers](concept-events-and-triggers.md) concept article. The table below lists the six different types of triggers in Composer and their descriptions. -| Trigger Type | Description | -| ----------------- | ------------------------------------------------------------------------------------------- | -| [Intent recognized](#intent-recognized) | When an intent is recognized the **Intent recognized** trigger fires. | -| [Unknown intent](#Unknown-intent) | The **Unknown intent** trigger fires when an intent is defined and recognized but there is no **Intent recognized** trigger defined for that intent.| -| [Dialog events](#Dialog-events) | When a dialog event such as **BeginDialog** occurs it will fire the specified trigger. | -| [Activities](h#Activities) | When an activity event occurs, such as when a new conversation starts, the **Activities** trigger will fire. | -| [Message events](#message-events) | When a message activity occurs such as when a message is updated, deleted or reacted to, the **Message events** trigger will fire. | -| [Custom event](#custom-event) | When an **Emit a custom event** occurs the **Custom event** trigger will fire.| +| Trigger Type | Description | +| --------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | +| [Intent recognized](#intent-recognized) | When an intent is recognized the **Intent recognized** trigger fires. | +| [Unknown intent](#Unknown-intent) | The **Unknown intent** trigger fires when an intent is defined and recognized but there is no **Intent recognized** trigger defined for that intent. | +| [Dialog events](#Dialog-events) | When a dialog event such as **BeginDialog** occurs it will fire the specified trigger. | +| [Activities](h#Activities) | When an activity event occurs, such as when a new conversation starts, the **Activities** trigger will fire. | +| [Message events](#message-events) | When a message activity occurs such as when a message is updated, deleted or reacted to, the **Message events** trigger will fire. | +| [Custom event](#custom-event) | When an **Emit a custom event** occurs the **Custom event** trigger will fire. | ## Intent recognized + This trigger type is used to define the actions to execute when an [intent](concept-language-understanding.md#intents) is found in a message sent from the user. The **Intent recognized** trigger works in conjunction with **recognizers**. There are two [recognizers](concept-dialog.md#recognizer) in Composer, one for [LUIS](#luis-recognizer) and the other for [Regular Expression](#regular-expression-recognizer). You define which recognizer is used, if any, at the dialog level. To create the **Intent recognized** trigger, select **New Trigger** in the navigation pane then **Intent recognized** from the drop-down list. You will see the intent trigger menu as follows: @@ -26,16 +28,17 @@ Once the **Intent recognized** trigger has been created, you can further refine > [!TIP] > You need to press the **Enter** key after entering an entity or it will not be saved. -It is also possible to add a **condition** to the trigger. A condition is an expression that follows [Common Expression Language](https://github.com/microsoft/BotBuilder-Samples/tree/master/experimental/common-expression-language) syntax. If a condition is specified, it must evaluate to "true" for the event to fire. +It is also possible to add a **condition** to the trigger. A condition is an expression that follows [Common Expression Language](https://github.com/microsoft/BotBuilder-Samples/tree/master/experimental/common-expression-language) syntax. If a condition is specified, it must evaluate to "true" for the event to fire. - The basic steps to define an **Intent recognized** trigger are as follows: +The basic steps to define an **Intent recognized** trigger are as follows: 1. Set up a [recognizer](./concept-dialog.md#recognizer) type in your dialog. 2. Define [intents](concept-language-understanding.md#intents) in the Language Understanding editor. 3. Create an **Intent recognized** trigger to handle each intent you created (one trigger per intent). -4. Define [actions]((./concept-dialog.md#action)) in the trigger. +4. Define [actions](<(./concept-dialog.md#action)>) in the trigger. ### LUIS recognizer + [LUIS](https://www.luis.ai/home) is a machine learning-based service you can use to build natural language capabilities into your bot. Using a LUIS recognizer enables you to extract intents and entities based on a LUIS application. Composer enables developers to create language training data in the dialog authoring canvas because it is deeply integrated with the [LUIS](https://www.luis.ai/home) API. LUIS is able to take natural language input from users and translate it into a named intent and a set of extracted entity values the message contains. @@ -44,46 +47,47 @@ Follow the steps to define an **Intent recognized** trigger with a LUIS recogniz 1. In the properties panel of your selected dialog, choose **LUIS** as recognizer type. - ![luis recognizer](./media/events-triggers/luis-recognizer.png) + ![luis recognizer](./media/events-triggers/luis-recognizer.png) + +2. In the Language Understanding editor, create intents with sample [utterances](concept-language-understanding.md#utterances) following the [.lu file format](https://aka.ms/lu-file-format). -2. In the Language Understanding editor, create intents with sample [utterances](concept-language-understanding.md#utterances) following the [.lu file format](https://github.com/Microsoft/botbuilder-tools/blob/master/packages/Ludown/docs/lu-file-format.md#lu-file-format). + Below is a screenshot showing the **text editor** in a dialogs properties panel. This example captures two simple _intents_ ("Greeting" and "BookFlight") each with a list of example _utterances_ that capture ways users might express these two intents. You can use - or + or \* to denote lists. Numbered lists are not supported. - Below is a screenshot showing the **text editor** in a dialogs properties panel. This example captures two simple _intents_ ("Greeting" and "BookFlight") each with a list of example _utterances_ that capture ways users might express these two intents. You can use - or + or * to denote lists. Numbered lists are not supported. + ![LUIS intent](./media/events-triggers/LUIS-intent.png) - ![LUIS intent](./media/events-triggers/LUIS-intent.png) + > [!NOTE] + > Each intent contains a series of sample utterances which will be used as training data in LUIS to recognize any pre-defined intent. - >[!NOTE] - > Each intent contains a series of sample utterances which will be used as training data in LUIS to recognize any pre-defined intent. - - > [!IMPORTANT] - > You will need a [LUIS authoring key](https://aka.ms/bot-framework-emulator-LUIS-keys?tabs=V2#programmatic-key) to get your training data published. For details, read [using LUIS for language understanding](how-to-use-LUIS.md) article. + > [!IMPORTANT] + > You will need a [LUIS authoring key](https://aka.ms/bot-framework-emulator-LUIS-keys?tabs=V2#programmatic-key) to get your training data published. For details, read [using LUIS for language understanding](how-to-use-LUIS.md) article. -3. Select **Intent recognized** from the trigger menu and pick the intent you want the trigger to handle. Each **Intent** trigger handles one intent. +3. Select **Intent recognized** from the trigger menu and pick the intent you want the trigger to handle. Each **Intent** trigger handles one intent. - ![BookFlight configure](./media/events-triggers/BookFlight-configure.png) + ![BookFlight configure](./media/events-triggers/BookFlight-configure.png) -4. Optionally, you can set the **Condition** property to avoid low confidence results given that LUIS is a machine learning based intent classifier. For example, set the **Condition** property to this in the **Greeting** intent: +4. Optionally, you can set the **Condition** property to avoid low confidence results given that LUIS is a machine learning based intent classifier. For example, set the **Condition** property to this in the **Greeting** intent: - `#Greeting.Score >=0.8` + `#Greeting.Score >=0.8` - ![Score](./media/events-triggers/score.png) + ![Score](./media/events-triggers/score.png) -This definition means that the **Greeting** intent trigger will only fire when the confidence score returned by LUIS is equal to or greater than 0.8. +This definition means that the **Greeting** intent trigger will only fire when the confidence score returned by LUIS is equal to or greater than 0.8. ### Regular Expression recognizer + A [regular expression](https://regexr.com/) is a special text string for describing a search pattern that can be used to match simple or sophisticated patterns in a string. Composer exposes the ability to define intents using regular expressions and also allows regular expressions to extract simple entity values. While LUIS offers the flexibility of a more fully featured language understanding technology, the [regular expression recognizer](https://github.com/microsoft/BotBuilder-Samples/blob/master/experimental/adaptive-dialog/docs/recognizers-rules-steps-reference.md#regex-recognizer) works well when you need to match a narrow set of highly structured commands or keywords. In the example below, a similar book-flight intent is defined. However, this will _only_ match the very narrow pattern "book flight to [somewhere]", whereas the LUIS recognizer will be able to match a much wider variety of messages. Follow the steps to define **Intent recognized** trigger with [Regular Expression](https://regexr.com/) recognizer: -1. In the properties panel of your selected dialog, choose **Regular Expression** as recognizer type for your dialog. +1. In the properties panel of your selected dialog, choose **Regular Expression** as recognizer type for your dialog. - ![regex recognizer](./media/events-triggers/regex-recognizer.png) + ![regex recognizer](./media/events-triggers/regex-recognizer.png) -2. In the regular expression editor, create a regular expression **intent** and **pattern** as shown in the screenshot below: +2. In the regular expression editor, create a regular expression **intent** and **pattern** as shown in the screenshot below: - ![regular expression intent](./media/events-triggers/regular-expression-intent.png) + ![regular expression intent](./media/events-triggers/regular-expression-intent.png) 3. You can then create an **Intent recognized** trigger to handle each intent you define as instructed in the [LUIS recognizer](how-to-define-triggers.md#LUIS-recognizer) section. @@ -91,96 +95,102 @@ Follow the steps to define **Intent recognized** trigger with [Regular Expressio > For more information on how to write regular expression, read [here](https://regexr.com/). ## Unknown intent + This is a trigger type used to define actions to take when there is no **Intent recognized** trigger to handle an existing intent. Follow the steps to define an **Unknown intent** trigger: 1. In the navigation pane, select **New Trigger**. -2. Select **Create a Trigger** from the **What is the type of this trigger?** drop-down list, then **Submit**. +2. Select **Create a Trigger** from the **What is the type of this trigger?** drop-down list, then **Submit**. - ![Unknown intent](./media/events-triggers/unknown-intent.png) + ![Unknown intent](./media/events-triggers/unknown-intent.png) -2. After you select **Submit**, you will see an empty **Unknown intent** trigger in the authoring canvas. +3. After you select **Submit**, you will see an empty **Unknown intent** trigger in the authoring canvas. -3. Select the **+** sign under the trigger node to add any action node(s) you want to include. For example, you can select **Send a response** to send a message "This is an unknown intent trigger!". When this trigger is fired, the response message will be sent to the user. +4. Select the **+** sign under the trigger node to add any action node(s) you want to include. For example, you can select **Send a response** to send a message "This is an unknown intent trigger!". When this trigger is fired, the response message will be sent to the user. - ![Unknown intent response](./media/events-triggers/unknown-intent-response.gif) + ![Unknown intent response](./media/events-triggers/unknown-intent-response.gif) ## Dialog events -This is a trigger type used to define actions to take when a dialog event such as `BeginDialog` is fired. Most dialogs will include an event handler (trigger) configured to respond to the `BeginDialog` event, which fires when the dialog begins and allows the bot to respond immediately. Follow the steps below to define a **Dialog started** trigger: + +This is a trigger type used to define actions to take when a dialog event such as `BeginDialog` is fired. Most dialogs will include an event handler (trigger) configured to respond to the `BeginDialog` event, which fires when the dialog begins and allows the bot to respond immediately. Follow the steps below to define a **Dialog started** trigger: 1. Select **New Trigger** in the navigation pane then select **Dialog events** from the drop-down list. - ![Dialog events](./media/events-triggers/dialog-events.png) + ![Dialog events](./media/events-triggers/dialog-events.png) -2. Select **Dialog started (Begin dialog event)** from the **Which event?** drop-down list then select **Submit**. +2. Select **Dialog started (Begin dialog event)** from the **Which event?** drop-down list then select **Submit**. - ![Begin dialog](./media/events-triggers/begin-dialog.png) + ![Begin dialog](./media/events-triggers/begin-dialog.png) -3. Select the **+** sign under the *Dialog started* node and then select **Begin a new dialog** from the **Dialog management** menu. +3. Select the **+** sign under the _Dialog started_ node and then select **Begin a new dialog** from the **Dialog management** menu. - ![Begin a new dialog](./media/events-triggers/begin-a-new-dialog.png) + ![Begin a new dialog](./media/events-triggers/begin-a-new-dialog.png) -4. Before you can use this trigger you must associate a dialog to it. You do this by selecting a dialog from the **Dialog name** drop-down list in the **properties panel** on the right side of the Composer window. You can select an existing dialog or create a new one. the example below demonstrates selecting and existing dialog named *weather*. +4. Before you can use this trigger you must associate a dialog to it. You do this by selecting a dialog from the **Dialog name** drop-down list in the **properties panel** on the right side of the Composer window. You can select an existing dialog or create a new one. the example below demonstrates selecting and existing dialog named _weather_. - ![Configure dialog](./media/events-triggers/wire-up-dialog.gif) + ![Configure dialog](./media/events-triggers/wire-up-dialog.gif) ## Activities -This type of trigger is used to handle activity events such as your bot receiving a `ConversationUpdate` Activity. This indicates a new conversation began and you use a **Greeting (ConversationUpdate activity)** trigger to handle it. -The following steps demonstrate hot to create a **Greeting (ConversationUpdate activity)** trigger to send a welcome message: +This type of trigger is used to handle activity events such as your bot receiving a `ConversationUpdate` Activity. This indicates a new conversation began and you use a **Greeting (ConversationUpdate activity)** trigger to handle it. -1. Select **New Trigger** in the navigation pane then **Activities** from the drop-down list. +The following steps demonstrate hot to create a **Greeting (ConversationUpdate activity)** trigger to send a welcome message: - ![Activities](./media/events-triggers/activities.png) +1. Select **New Trigger** in the navigation pane then **Activities** from the drop-down list. + ![Activities](./media/events-triggers/activities.png) -2. Select **Greeting (ConversationUpdate activity)** from the **Which activity type?** drop-down list then select **Submit**. +2) Select **Greeting (ConversationUpdate activity)** from the **Which activity type?** drop-down list then select **Submit**. - ![Conversation update](./media/events-triggers/conversation-update.png) + ![Conversation update](./media/events-triggers/conversation-update.png) -3. Select the **+** sign under the *ConversationUpdate Activity* node and then select **Send a response**. +3) Select the **+** sign under the _ConversationUpdate Activity_ node and then select **Send a response**. -4. Author your response in the **Language Generation** editor in the **properties panel** on the right side of the Composer window, by entering a message following [.lg file format](https://github.com/microsoft/BotBuilder-Samples/blob/master/experimental/language-generation/docs/lg-file-format.md) as demonstrated in the image below. +4) Author your response in the **Language Generation** editor in the **properties panel** on the right side of the Composer window, by entering a message following [.lg file format](https://github.com/microsoft/BotBuilder-Samples/blob/master/experimental/language-generation/docs/lg-file-format.md) as demonstrated in the image below. - ![Welcome](./media/events-triggers/welcome.gif) + ![Welcome](./media/events-triggers/welcome.gif) ## Custom event + The **Custom event** trigger will only fire when a matching **Emit a custom event** occurs. It is a trigger that any dialog in your bot can consume. To define and consume a **Custom event** trigger, you need to create a **Emit a custom event** first. Follow the steps below to create a **Emit a custom event**: ### Emit a custom event 1. In the Composer navigation pane select the trigger you want to associate your custom event with. This opens the trigger in the authoring canvas where you can specify exactly where in the flow you want to trigger this event from. Once determined, select the **+** sign and then select **Emit a custom event** from the **Access external resources** drop-down list. - ![Emit custom event](./media/events-triggers/emit-custom-event.png) + ![Emit custom event](./media/events-triggers/emit-custom-event.png) -2. In the _properties panel_ of this activity, on the right side of the Composer window, enter a name ("*Weather*") into the **Event name** field, then select **Bubble event**. +2. In the _properties panel_ of this activity, on the right side of the Composer window, enter a name ("_Weather_") into the **Event name** field, then select **Bubble event**. - ![Emit custom event property](./media/events-triggers/emit-custom-event-property.png) + ![Emit custom event property](./media/events-triggers/emit-custom-event-property.png) > [!TIP] -> When **Bubble event** is selected, any event that is not handled in the current dialog will _bubble up_ to that dialogs parent dialog where it will continue to look for handlers for the custom event. +> When **Bubble event** is selected, any event that is not handled in the current dialog will _bubble up_ to that dialogs parent dialog where it will continue to look for handlers for the custom event. ### Create a custom event trigger -Now that your **Emit a custom event** has been created, you can create a **Custom event** trigger to handle this event. When the **Emit a custom event** occurs, any matching **Custom event** trigger at any dialog level will fire. Follow the steps to create a **Custom event** trigger to be associated with the previously defined **Emit a custom event**. -1. Select **New Trigger** in the navigation pane, then select **Custom event** from the drop-down list, then **Submit**. +Now that your **Emit a custom event** has been created, you can create a **Custom event** trigger to handle this event. When the **Emit a custom event** occurs, any matching **Custom event** trigger at any dialog level will fire. Follow the steps to create a **Custom event** trigger to be associated with the previously defined **Emit a custom event**. -2. Enter "*Weather*" into the **Custom event name** field in the properties panel on the right side of the Composer window. You must enter the same name ("*Weather*") to associate this trigger with the **Emit a custom event** you defined previously. +1. Select **New Trigger** in the navigation pane, then select **Custom event** from the drop-down list, then **Submit**. - ![Custom event property](./media/events-triggers/custom-event-property.png) +2. Enter "_Weather_" into the **Custom event name** field in the properties panel on the right side of the Composer window. You must enter the same name ("_Weather_") to associate this trigger with the **Emit a custom event** you defined previously. -4. Now you can add an action to your custom event handler, this defines what will happen when it is triggered. Do this by selecting the **+** sign and then **Send a response** from the actions menu. Enter the desired response for this action in the Language Generation editor, for this example enter "This is a custom trigger!". + ![Custom event property](./media/events-triggers/custom-event-property.png) - ![Custom event response](./media/events-triggers/custom-event-response.gif) +3. Now you can add an action to your custom event handler, this defines what will happen when it is triggered. Do this by selecting the **+** sign and then **Send a response** from the actions menu. Enter the desired response for this action in the Language Generation editor, for this example enter "This is a custom trigger!". + + ![Custom event response](./media/events-triggers/custom-event-response.gif) Now you have completed both of the required steps needed to create and execute a custom event. When **Emit a custom event** fires, your custom event handler will fire and handle this event, sending the response you defined. ![Custom event response](./media/events-triggers/custom-event-response.png) ## References + - The [Events and triggers](./concept-events-and-triggers.md) concept article. -## Next +## Next + - Learn how to [control conversation flow](./how-to-control-conversation-flow.md). diff --git a/docs/how-to-use-LUIS.md b/docs/how-to-use-LUIS.md index 444dd6c526..7c547d0c23 100644 --- a/docs/how-to-use-LUIS.md +++ b/docs/how-to-use-LUIS.md @@ -1,76 +1,85 @@ -# Using LUIS for language understanding -Language Understanding Intelligent Service ([LUIS](https://www.luis.ai/home)) is a cloud-based API service to build natural language into apps and bots. Adding LUIS to your bots enables them to understand the users' intents conversationally and contextually so that your bots can decide what to respond to the users. Bot Framework Composer provides tools to train and manage language understanding components and it's easier for developers to add LUIS when they develop bots with Composer. In this article, we will walk you through the steps to use LUIS when you develop your bots with Composer. To further explore how to use LUIS in Bot Composer, you may refer to the [ToDoBotWithLuisSample](https://github.com/microsoft/BotFramework-Composer/tree/master/Composer/packages/server/assets/projects/ToDoBotWithLuisSample). +# Using LUIS for language understanding + +Language Understanding Intelligent Service ([LUIS](https://www.luis.ai/home)) is a cloud-based API service to build natural language into apps and bots. Adding LUIS to your bots enables them to understand the users' intents conversationally and contextually so that your bots can decide what to respond to the users. Bot Framework Composer provides tools to train and manage language understanding components and it's easier for developers to add LUIS when they develop bots with Composer. In this article, we will walk you through the steps to use LUIS when you develop your bots with Composer. To further explore how to use LUIS in Bot Composer, you may refer to the [ToDoBotWithLuisSample](https://github.com/microsoft/BotFramework-Composer/tree/master/Composer/packages/server/assets/projects/ToDoBotWithLuisSample). + +## Prerequisites -## Prerequisites - Basic knowledge of language understanding (concept article [here](./concept-language-understanding.md)) - Basic knowledge of events and triggers (concept article [here](./concept-events-and-triggers.md)) - LUIS account (apply [here](https://www.luis.ai/home)) - LUIS authoring key (how to get [here](https://docs.microsoft.com/en-us/azure/cognitive-services/luis/luis-concept-keys?tabs=V2#programmatic-key)) -## How to add LUIS -To determine user's intent, in Composer you define the **Intent recognized** trigger, and then specify the actions to take when an intent is recognized (and optionally **entities**). For more details please read the [events and triggers](./concept-events-and-triggers.md) article. +## How to add LUIS + +To determine user's intent, in Composer you define the **Intent recognized** trigger, and then specify the actions to take when an intent is recognized (and optionally **entities**). For more details please read the [events and triggers](./concept-events-and-triggers.md) article. -Composer currently supports two types of recognizers: LUIS recognizer (default) and Regular expression recognizer. You can only choose one type of recognizer for each dialog. Besides the recognizer, each dialog may contain a set of Language Understanding data authored in [.lu format](https://github.com/microsoft/botbuilder-tools/blob/master/packages/Ludown/docs/lu-file-format.md). +Composer currently supports two types of recognizers: LUIS recognizer (default) and Regular expression recognizer. You can only choose one type of recognizer for each dialog. Besides the recognizer, each dialog may contain a set of Language Understanding data authored in [.lu format](https://aka.ms/lu-file-format). -In this section, we will cover the steps to use LUIS as recognizer in your bot. These steps include the following: set a recognizer type for each dialog, author Language Understanding training data, publish your Language Understanding (LU) data, and test them in emulator. +In this section, we will cover the steps to use LUIS as recognizer in your bot. These steps include the following: set a recognizer type for each dialog, author Language Understanding training data, publish your Language Understanding (LU) data, and test them in emulator. -### Set LUIS as recognizer -In Composer, each dialog can have one type of recognizer and might contain a set of Language Understanding training data. To add LUIS to your bot, you need to select LUIS as the recognizer type for the specific dialog you want to define. To set LUIS as recognizer, follow the steps: +### Set LUIS as recognizer -1. On the left navigation pane, select the dialog you want to set LUIS recognizer. +In Composer, each dialog can have one type of recognizer and might contain a set of Language Understanding training data. To add LUIS to your bot, you need to select LUIS as the recognizer type for the specific dialog you want to define. To set LUIS as recognizer, follow the steps: + +1. On the left navigation pane, select the dialog you want to set LUIS recognizer. 2. On the properties panel on the right side, select **LUIS** as recognizer type. ![luis_recognizer](./media/tutorial-weatherbot/07/luis-recognizer.png) ### Author LU -You can author Language Understanding (LU) in the inline editor and follow the [.lu file format](https://github.com/microsoft/botbuilder-tools/blob/master/packages/Ludown/docs/lu-file-format.md). An LU file usually consists of an intent with matching example utterances. You can author as many intents as you want to include in the dialog. The following screenshot shows two intents: **Greeting** and **CheckWeather** with the respective matching utterances. + +You can author Language Understanding (LU) in the inline editor and follow the [.lu file format](https://aka.ms/lu-file-format). An LU file usually consists of an intent with matching example utterances. You can author as many intents as you want to include in the dialog. The following screenshot shows two intents: **Greeting** and **CheckWeather** with the respective matching utterances. ![author_lu](./media/add_luis/author_lu.png) -### Define an **Intent recognized** trigger -You need to define an **Intent recognized** trigger to handle the pre-defined intents. This type of trigger works with the **recognizers**. Each **Intent recognized** trigger handles one intent. Follow the steps to define an **Intent recognized** trigger. +### Define an **Intent recognized** trigger + +You need to define an **Intent recognized** trigger to handle the pre-defined intents. This type of trigger works with the **recognizers**. Each **Intent recognized** trigger handles one intent. Follow the steps to define an **Intent recognized** trigger. 1. Select the dialog on the left side navigation pane and click **New Trigger** ![create_trigger](./media/add_luis/create_trigger.png) -2. In the pop-up window, select **Intent recognized** from the drop-down menu and then configure the intent to this trigger from the drop-down menu. Click **Submit**. You need to define an **Intent recognized** trigger to handle each pre-defined intent. +2. In the pop-up window, select **Intent recognized** from the drop-down menu and then configure the intent to this trigger from the drop-down menu. Click **Submit**. You need to define an **Intent recognized** trigger to handle each pre-defined intent. ![configure_intent](./media/add_luis/configure_intent.png) -After you define your trigger and configure it to specific intent, you can add actions to be executed after the trigger is fired. For example, you can send a response message. +After you define your trigger and configure it to specific intent, you can add actions to be executed after the trigger is fired. For example, you can send a response message. 3. Click the " + " sign below the trigger node and select **Send a response** ![send_response_message](./media/add_luis/send_response_message.png) -4. Define the response message in the language generation editor and follow the [.lg file format](https://github.com/microsoft/BotBuilder-Samples/blob/master/experimental/language-generation/docs/lg-file-format.md). The screenshot below shows how to author a response message "This is a Greeting intent!" in the Language Generation editor. +4. Define the response message in the language generation editor and follow the [.lg file format](https://github.com/microsoft/BotBuilder-Samples/blob/master/experimental/language-generation/docs/lg-file-format.md). The screenshot below shows how to author a response message "This is a Greeting intent!" in the Language Generation editor. ![greeting_message](./media/add_luis/greeting_message.png) -### Publish -After you are done with all previous steps, you are ready to publish your language model to LUIS. Please do the following steps to publish your training data: +### Publish + +After you are done with all previous steps, you are ready to publish your language model to LUIS. Please do the following steps to publish your training data: 1. click **Start Bot** on the upper right corner of your Composer -2. fill in your LUIS authoring key and -3. click **Publish**. +2. fill in your LUIS authoring key and +3. click **Publish**. ![publish_luis](./media/add_luis/publish_luis.png) -Any time you hit **start bot** (or **restart bot**), Composer will evaluate if your LU content has changed. If so Composer will automatically make required updates to your LUIS application, train and publish them. If you go to your LUIS app website, you will find the newly published LU model. +Any time you hit **start bot** (or **restart bot**), Composer will evaluate if your LU content has changed. If so Composer will automatically make required updates to your LUIS application, train and publish them. If you go to your LUIS app website, you will find the newly published LU model. -### Test -To test your bot which you just added LUIS to, click the **Test in Emulator** button on the upper right corner of Composer. When you emulator is running, send in messages indicating different intents to see if they match the pre-defined intents. +### Test + +To test your bot which you just added LUIS to, click the **Test in Emulator** button on the upper right corner of Composer. When you emulator is running, send in messages indicating different intents to see if they match the pre-defined intents. ![emulator](./media/add_luis/emulator.gif) -## References +## References + - [LUIS.ai](https://www.luis.ai/home) - [Add natural language understanding to your bot](https://docs.microsoft.com/azure/bot-service/bot-builder-howto-v4-luis) -- [Events and triggers](./concept-events-and-triggers.md) +- [Events and triggers](./concept-events-and-triggers.md) - [Language Understanding](./concept-language-understanding.md) -## Next -- Learn [how to add a QnA Maker knowledge base to your bot](./how-to-add-qna-to-bot.md). +## Next +- Learn [how to add a QnA Maker knowledge base to your bot](./how-to-add-qna-to-bot.md). diff --git a/docs/how-to-use-validation.md b/docs/how-to-use-validation.md index 1635ef0180..7a5053c079 100644 --- a/docs/how-to-use-validation.md +++ b/docs/how-to-use-validation.md @@ -1,22 +1,23 @@ # Linting and validation -As an integrated development tool, Bot Framework Composer supports validation of data when you author an [.lg](https://github.com/microsoft/BotBuilder-Samples/blob/master/experimental/language-generation/docs/lg-file-format.md) template, an [.lu](https://github.com/Microsoft/botbuilder-tools/blob/master/packages/Ludown/docs/lu-file-format.md) template, and [expressions](https://github.com/microsoft/BotBuilder-Samples/tree/master/experimental/common-expression-language) to fill in property fields. Error indicators will show in both inline editors and corresponding all-up views (**Bot Responses** and **User Input**). The **Notifications** page (click **Notifications** icon on the left navigation pane) displays an aggregation of all errors and warnings. With the help of linting and validation, your bot-authoring experience will be improved and you can easily build a functional bot that can "run". +As an integrated development tool, Bot Framework Composer supports validation of data when you author an [.lg](https://github.com/microsoft/BotBuilder-Samples/blob/master/experimental/language-generation/docs/lg-file-format.md) template, an [.lu](https://aka.ms/lu-file-format) template, and [expressions](https://github.com/microsoft/BotBuilder-Samples/tree/master/experimental/common-expression-language) to fill in property fields. Error indicators will show in both inline editors and corresponding all-up views (**Bot Responses** and **User Input**). The **Notifications** page (click **Notifications** icon on the left navigation pane) displays an aggregation of all errors and warnings. With the help of linting and validation, your bot-authoring experience will be improved and you can easily build a functional bot that can "run". > [!NOTE] -> We are still working to improve the implementation of linting and validation in Composer. More user scenarios will be updated. +> We are still working to improve the implementation of linting and validation in Composer. More user scenarios will be updated. -Whenever you see `Start Bot` button in grey, this indicates your bot has errors that prevent it from running successfully. +Whenever you see `Start Bot` button in grey, this indicates your bot has errors that prevent it from running successfully. ![start-grey](./media/validation/start-grey.png) -The number beside the error icon indicates the number of errors. Click the error icon you will be navigated to the **Notifications** page listing all errors and warnings. +The number beside the error icon indicates the number of errors. Click the error icon you will be navigated to the **Notifications** page listing all errors and warnings. ![notification-all-up-view](./media/validation/notification-all-up-view.png) -Click any of the errors on the **Notifications** page will navigate you to the matching error location. After you fix all the errors, you will see the `Start Bot` button in blue, this indicates your bot has no errors and it will run successfully. +Click any of the errors on the **Notifications** page will navigate you to the matching error location. After you fix all the errors, you will see the `Start Bot` button in blue, this indicates your bot has no errors and it will run successfully. ## .lg files -When you author an [.lg template](https://github.com/microsoft/BotBuilder-Samples/blob/master/experimental/language-generation/docs/lg-file-format.md) that has syntax errors, an indicator will show in the inline editor with a red wiggling line. + +When you author an [.lg template](https://github.com/microsoft/BotBuilder-Samples/blob/master/experimental/language-generation/docs/lg-file-format.md) that has syntax errors, an indicator will show in the inline editor with a red wiggling line. ![inline-error-lg](./media/validation/inline-error-lg.png) @@ -24,35 +25,34 @@ To diagnose and fix the error, you can read the error message beneath the editor ![hover-message-lg](./media/validation/hover-message-lg.png) -Click **Bot Responses** on the left navigation pane, you will find the error is also saved and updated in the lg all-up view. The tiny red rectangle on the right end of the editor helps you to identify where the error is. This is especially helpful when you have a long list of templates. Hover your mouse over the erroneous part you will see detailed error message with suggested fixes. +Click **Bot Responses** on the left navigation pane, you will find the error is also saved and updated in the lg all-up view. The tiny red rectangle on the right end of the editor helps you to identify where the error is. This is especially helpful when you have a long list of templates. Hover your mouse over the erroneous part you will see detailed error message with suggested fixes. ![lg-all-up-view](./media/validation/lg-all-up-view.png) - ## .lu files -When you author an [.lu template](https://github.com/Microsoft/botbuilder-tools/blob/master/packages/Ludown/docs/lu-file-format.md) that has syntax errors, the entire lu inline editor will be in red frame. To diagnose and fix the error, you can read the error message beneath the editor and click `here` to refer to the syntax documentation. + +When you author an [.lu template](https://aka.ms/lu-file-format) that has syntax errors, the entire lu inline editor will be in red frame. To diagnose and fix the error, you can read the error message beneath the editor and click `here` to refer to the syntax documentation. ![lu-inline-error](./media/validation/lu-inline-error.png) -Click **User Input** on the left navigation pane, you will see a popup window describing the lu error message. +Click **User Input** on the left navigation pane, you will see a popup window describing the lu error message. ![lu-popup](./media/validation/lu-popup.png) -Click **Edit** button you will see the error is also saved and updated in the lu all-up view. You can choose to fix the error either in the lu all-up view or in the inline editor. +Click **Edit** button you will see the error is also saved and updated in the lu all-up view. You can choose to fix the error either in the lu all-up view or in the inline editor. ![lu-all-up-view](./media/validation/lu-all-up-view.png) - ## Expressions -When you fill in property fields with invalid [expressions](https://github.com/microsoft/BotBuilder-Samples/tree/master/experimental/common-expression-language) or when a required property is missing, the entire form editor will be in red frame. + +When you fill in property fields with invalid [expressions](https://github.com/microsoft/BotBuilder-Samples/tree/master/experimental/common-expression-language) or when a required property is missing, the entire form editor will be in red frame. ![expression-inline-error](./media/validation/expression-inline-error.png) -To diagnose and fix the error, you can read the error message beneath the editor and click `here` to refer to the syntax documentation. +To diagnose and fix the error, you can read the error message beneath the editor and click `here` to refer to the syntax documentation. ![expression-missing-property](./media/validation/expression-missing-property.png) - # Next -TBD +TBD diff --git a/docs/olddocs/CreateYourFirstBot.md b/docs/olddocs/CreateYourFirstBot.md index 4040625567..c6dc33d8ee 100644 --- a/docs/olddocs/CreateYourFirstBot.md +++ b/docs/olddocs/CreateYourFirstBot.md @@ -1,74 +1,77 @@ # Create your first bot + Follow these six steps to create a weather bot from scratch using Bot Framework Composer. Alternatively, watch the [video](https://microsoft-my.sharepoint.com/:v:/p/t-yangxi/EVcxF6YjGKZOvJjPZstfS5EBLVlDldzoZ5yQiqJlNs_NKw?e=zkzymp). ## Prerequisites -* [Bot Framework Composer](https://github.com/microsoft/BotFramework-Composer) -* Bot Framework Emulator ([download](https://github.com/microsoft/BotFramework-Emulator/releases/tag/v4.5.2)) -* LUIS authoring key ([where to find](https://docs.microsoft.com/en-us/azure/cognitive-services/luis/luis-concept-keys#programmatic-key)) + +- [Bot Framework Composer](https://github.com/microsoft/BotFramework-Composer) +- Bot Framework Emulator ([download](https://github.com/microsoft/BotFramework-Emulator/releases/latest)) +- LUIS authoring key ([where to find](https://docs.microsoft.com/en-us/azure/cognitive-services/luis/luis-concept-keys#programmatic-key)) ## Step 1: Creating a new bot -On the Composer homepage, click **New**. Select **Create from scratch**. Click **Next**. Give your bot a name, for example *WeatherBot*. Once created, you should be able to see the following screen. -![screenshot: creating a new bot](Assets/DesignFlow.png) +On the Composer homepage, click **New**. Select **Create from scratch**. Click **Next**. Give your bot a name, for example _WeatherBot_. Once created, you should be able to see the following screen. +![screenshot: creating a new bot](Assets/DesignFlow.png) ## Step 2: Setting up the intent recognizer + On the right-hand side of the page, select **BF Language Understanding** for **Recognizer Type**. ![screenshot: setting up the recognizer](Assets/Recognizer.png) In the text editor, type in the intents and utterances, then click **Save**. -**Note**: You should use the correct file format to edit the recognizer (see example below). Follow this [link](https://github.com/Microsoft/botbuilder-tools/blob/master/packages/Ludown/docs/lu-file-format.md#lu-file-format) to learn more about the LU format. +**Note**: You should use the correct file format to edit the recognizer (see example below). Follow this [link](https://aka.ms/lu-file-format) to learn more about the LU format. ![screenshot: editing the LU](Assets/LU.png) - ## Step 3: Creating event handlers to handle the intents + On the **Events** pane, click the **add icon**, then select **Handle an Intent**. create intent handler -On the right-hand side of the page, give the intent a name, for example, *GreetingIntentHandler*. In the **Intent** field, type in the name of the intent that this event is intended to handle. **Note**: The name should match the intent name in the recognizer. +On the right-hand side of the page, give the intent a name, for example, _GreetingIntentHandler_. In the **Intent** field, type in the name of the intent that this event is intended to handle. **Note**: The name should match the intent name in the recognizer. -It's a good practice to create a handler for each intent. In this guide, we’ve created two handlers named *GreetingIntentHandler* and *CheckWeatherIntentHandler*. +It's a good practice to create a handler for each intent. In this guide, we’ve created two handlers named _GreetingIntentHandler_ and _CheckWeatherIntentHandler_. Intent name - ## Step 4: Adding actions to the handlers Here we define the actions that the bot will take when an intent is recognized. You can add actions such as sending messages or performing computational tasks. You can also call a dialog ([SDK docs: Bot Framework Dialogs](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-concept-dialog?view=azure-bot-service-4.0)) as part of the actions. -### 4.1: Adding actions to *GreetingIntentHandler* -Double-click on the *GreetingIntentHandler* node to get started. +### 4.1: Adding actions to _GreetingIntentHandler_ + +Double-click on the _GreetingIntentHandler_ node to get started. Other event handler -By design, we want the bot to send a message to the user in response to the *Greeting* intent. To do this, click the **add icon**, select **Send Messages**, then select **Send Activity**. On the **Language Generation** pane, type in the bot response. For example, *Hello from the bot!*. +By design, we want the bot to send a message to the user in response to the _Greeting_ intent. To do this, click the **add icon**, select **Send Messages**, then select **Send Activity**. On the **Language Generation** pane, type in the bot response. For example, _Hello from the bot!_. ![screenshot: LG](Assets/AddActions.png) -### 4.2 Adding actions to *CheckWeatherIntentHandler* -Double-click on the *CheckWeatherIntentHandler* node to get started. By design, we want the bot to begin a dialog (and take actions as specified in that dialog) in response to the *CheckWeather* intent. To do this, we first need to create a dialog (e.g. *CheckWeatherDialog*), then add this dialog to the *CheckWeatherIntentHandler*. +### 4.2 Adding actions to _CheckWeatherIntentHandler_ + +Double-click on the _CheckWeatherIntentHandler_ node to get started. By design, we want the bot to begin a dialog (and take actions as specified in that dialog) in response to the _CheckWeather_ intent. To do this, we first need to create a dialog (e.g. _CheckWeatherDialog_), then add this dialog to the _CheckWeatherIntentHandler_. To create a dialog, click **Add** on the **Dialogs** pane, and follow the instructions to complete the process. Once a dialog is created, add actions to this dialog following the same process as described in 4.1. Other event handler - -To add the newly created dialog to the event, first, go back to the root dialog, then select the event to which you want to add the dialog. Click the **add icon**, select **Dialogs**, select **Begin a Dialog**, then choose a dialog from the list. +To add the newly created dialog to the event, first, go back to the root dialog, then select the event to which you want to add the dialog. Click the **add icon**, select **Dialogs**, select **Begin a Dialog**, then choose a dialog from the list. ![screenshot: LG](Assets/BeginADialog.png) - ## Step 5: (Optional) Creating handlers for other types of events + Besides recognized intents, you may want to handle other types of events, for example, when there are unrecognized intents or when a user joins the conversation. Creating handlers for these types of events is no different from creating intent handlers. You just need to select a different event type as shown below. For example, select **Handle ConversationUpdate** if you want to send a welcome message to the user when they first join the conversation (when a conversationUpdate event is trigged). Other event handler - ## Step 6: Testing your bot + Now your bot is ready for testing! Click **Connect/Reload** on the top right corner of the screen, then select **Test in Emulator**. **Note**: You may be prompted to **Publish LUIS models**. Please follow the link to the LUIS site, go to **Settings**, copy the **Authoring Key**, and fill out the fields to publish LUIS models. diff --git a/docs/olddocs/testing_debugging.md b/docs/olddocs/testing_debugging.md index 0469e708c7..96896f32f0 100644 --- a/docs/olddocs/testing_debugging.md +++ b/docs/olddocs/testing_debugging.md @@ -4,7 +4,7 @@ This document will explain different techniques you can use to test & debug your **Pre-requisites:** -[Bot Framework Emulator](https://github.com/microsoft/BotFramework-Emulator/releases/tag/v4.5.2) +[Bot Framework Emulator](https://github.com/microsoft/BotFramework-Emulator/releases/latest) [Bot Framework Composer](https://github.com/Microsoft/BotFramework-Composer#installing-bot-framework-composer) @@ -78,7 +78,7 @@ Attempting to save changes that do not match the LU file format results in an er ![LU format error](./Assets/testing-debugging-luformaterror.png) -Seeing this error means that you have badly formatted lu data. Refer to the [LU file format](https://github.com/microsoft/botbuilder-tools/blob/master/packages/Ludown/docs/lu-file-format.md) for troubleshooting. +Seeing this error means that you have badly formatted lu data. Refer to the [LU file format](https://aka.ms/lu-file-format) for troubleshooting. _Saving LG data_ diff --git a/docs/olddocs/triggers_and_events.md b/docs/olddocs/triggers_and_events.md index f8e4465ed6..50f55309ca 100644 --- a/docs/olddocs/triggers_and_events.md +++ b/docs/olddocs/triggers_and_events.md @@ -71,7 +71,7 @@ Once intents have been defined in the Language Understanding configuration of a ![Specify an intent to handle](./Assets/intent-handler.png) -In addition to specifying intents and utterances, it is also possible to train LUIS to recognize named entities and patterns. [Read more about the full capabilities of LUIS recognizers here](https://github.com/microsoft/botbuilder-tools/blob/master/packages/Ludown/docs/lu-file-format.md) +In addition to specifying intents and utterances, it is also possible to train LUIS to recognize named entities and patterns. [Read more about the full capabilities of LUIS recognizers here](https://aka.ms/lu-file-format) Extracted entities are passed along to any triggered actions or child dialogs using the syntax memory path `@[Entity Name]`. For example, given an intent definition like below: @@ -117,7 +117,7 @@ TODO: clean this up ## Further Reading -- [LU file format](https://github.com/microsoft/botbuilder-tools/blob/master/packages/Ludown/docs/lu-file-format.md) +- [LU file format](https://aka.ms/lu-file-format) - [LUIS.ai docs](https://docs.microsoft.com/en-us/azure/cognitive-services/luis/home) - [Adaptive Dialog Events](https://github.com/microsoft/BotBuilder-Samples/blob/master/experimental/adaptive-dialog/docs/recognizers-rules-steps-reference.md#Rules) - [Messaging processing pipeline in adaptive dialogs](https://github.com/microsoft/BotBuilder-Samples/blob/master/experimental/adaptive-dialog/docs/anatomy-and-runtime-behavior.md#runtime-behavior-adaptive-dialog) diff --git a/docs/tutorial/tutorial-luis.md b/docs/tutorial/tutorial-luis.md index c223581f25..73abac6086 100644 --- a/docs/tutorial/tutorial-luis.md +++ b/docs/tutorial/tutorial-luis.md @@ -1,16 +1,18 @@ # Tutorial: Using LUIS for Language Understanding -Up until this point, we have been using a simple regex recognizer to detect user intent. Bot Framework Composer has deep integration with LUIS. + +Up until this point, we have been using a simple regex recognizer to detect user intent. Bot Framework Composer has deep integration with LUIS. In this tutorial, you learn how to: > [!div class="checklist"] -> * Add LUIS into your bot with Composer for Language Understanding +> +> - Add LUIS into your bot with Composer for Language Understanding ## Prerequisites + - Completion of the tutorial [Incorporating cards and buttons into your bot](./tutorial-cards.md). - A working knowledge of the concepts taught in the [Language Understanding](../concept-language-understanding.md) article. -- [LUIS](https://www.luis.ai/home) and how to get [LUIS authoring key](https://docs.microsoft.com/en-us/azure/cognitive-services/luis/luis-concept-keys#programmatic-key). - +- [LUIS](https://www.luis.ai/home) and how to get [LUIS authoring key](https://docs.microsoft.com/en-us/azure/cognitive-services/luis/luis-concept-keys#programmatic-key). ## Update recognizer @@ -18,11 +20,12 @@ In this tutorial, you learn how to: ![](../media/tutorial-weatherbot/07/luis-recognizer.png) -2. To work with LUIS recognizer, you can provide content in the [***.lu file foramt***](https://github.com/microsoft/botbuilder-tools/blob/master/packages/Ludown/docs/lu-file-format.md) that is highly similar to Language Generation format. +2. To work with LUIS recognizer, you can provide content in the [**_.lu file format_**](https://aka.ms/lu-file-format) that is highly similar to Language Generation format. - With this, intents are denoted using the markdown section notation - e.g. `# intentName` and utterances are provided as a list. + With this, intents are denoted using the markdown section notation - e.g. `# intentName` and utterances are provided as a list. + + Replace the recognizer content with this - - Replace the recognizer content with this - ``` > See https://aka.ms/lu-file-format to learn about supported LU concepts. @@ -51,8 +54,8 @@ In this tutorial, you learn how to: ![](../media/tutorial-weatherbot/07/luis-with-lu-content.png) -3. Once you have done this, you need to re-configure the various **Intent** triggers within that dialog. -4. Click on `weather` trigger in the left navigation. Select `Weather` from the intent drop-down menu in the properties panel on the right side of the Composer. +3. Once you have done this, you need to re-configure the various **Intent** triggers within that dialog. +4. Click on `weather` trigger in the left navigation. Select `Weather` from the intent drop-down menu in the properties panel on the right side of the Composer. Update the title of the trigger to `Weather` instead of **Intent** @@ -62,11 +65,11 @@ In this tutorial, you learn how to: Update the title of the trigger to `Cancel` instead of **Intent** -6. Given we are using LUIS which is a machine learning based intent classifier, we want to avoid low confidence results. To do this, +6. Given we are using LUIS which is a machine learning based intent classifier, we want to avoid low confidence results. To do this, - Set the **Condition** property to this + Set the **Condition** property to this - `#Cancel.Score >= 0.8` + `#Cancel.Score >= 0.8` ![](../media/tutorial-weatherbot/07/luis-score.png) @@ -76,31 +79,31 @@ In this tutorial, you learn how to: Update the title of the trigger to `Help` instead of **Intent** - Set the `Condition` property to this + Set the `Condition` property to this - `#Help.Score >= 0.5` + `#Help.Score >= 0.5` > This says do not fire the cancel trigger if the confidence score returned by LUIS is lower than or equal to 0.5 -7. Click on **Restart Bot** +8. Click on **Restart Bot** - Composer has now detected that you have LU information specified and it needs to create/ update corresponding LUIS applications. + Composer has now detected that you have LU information specified and it needs to create/ update corresponding LUIS applications. For `Authoring key`, click on `environment` in the top right corner of your screen (this is right above where this documenation content is displayed) and copy `LUIS authoring key 1` into the composer window ![](../media/tutorial-weatherbot/07/luis-key.png) -8. Click `Publish`. This should take a minute or two to complete. Composer will render progress at the top right corner of the screen. -9. Click on **Test in Emulator** and talk to the bot. +9. Click `Publish`. This should take a minute or two to complete. Composer will render progress at the top right corner of the screen. +10. Click on **Test in Emulator** and talk to the bot. --- With LUIS, you no longer have to type in exact regex patterns to trigger specific scenarios for your bot. Try things like: -* "How is the weather" -* "Weather please" -* "Can you help me" -* "Cancel please" +- "How is the weather" +- "Weather please" +- "Can you help me" +- "Cancel please" ![](../media/tutorial-weatherbot/07/luis-wired-up.png) @@ -108,16 +111,16 @@ With LUIS, you no longer have to type in exact regex patterns to trigger specifi ## Using LUIS for entity extraction -You can use LUIS to recognize entities from what the user says. +You can use LUIS to recognize entities from what the user says. -As an example, the user could say "How is the weather in 98052?" and instead of prompting the user again for a zipcode, we could just go straight to pulling up weather information if the user had already provided that as part of their initial query. +As an example, the user could say "How is the weather in 98052?" and instead of prompting the user again for a zipcode, we could just go straight to pulling up weather information if the user had already provided that as part of their initial query. -Let's get this wired up. +Let's get this wired up. -10. Step one is to add a regex entity extractor to the LUIS app. To do this, click on `WeatherBot.Main` and on the right side, add the following entity definition at the end of the LU content - +10. Step one is to add a regex entity extractor to the LUIS app. To do this, click on `WeatherBot.Main` and on the right side, add the following entity definition at the end of the LU content - ``` - > regex zipcode entity. Any time LUIS sees a five digit number, it will flag it as 'zipcode' entity. + > regex zipcode entity. Any time LUIS sees a five digit number, it will flag it as 'zipcode' entity. $ zipcode : /[0-9]{5}/ ``` @@ -126,20 +129,20 @@ Let's get this wired up. 11. Next, let's configure your `prompt for zipcode` to use the entity if LUIS finds it. To do this, open `getWeather` dialog from the left navigation menu, select `BeginDialog` trigger and scroll to and find the prompt. - ![](../media/tutorial-weatherbot/07/back-at-zipcode-prompt.png) + ![](../media/tutorial-weatherbot/07/back-at-zipcode-prompt.png) 12. Let's insert an action **before** the prompt by clicking on **+**, musing over **Manage Properties** and selecting **Set a property** -13. Since the prompt itself is trying to set the zipcode in the `user.zipcode`, let's set that property to the `@zipcode` entity. +13. Since the prompt itself is trying to set the zipcode in the `user.zipcode`, let's set that property to the `@zipcode` entity. - Set **Property** to + Set **Property** to - `user.zipcode` + `user.zipcode` - Set **Value** to + Set **Value** to - `@zipcode` + `@zipcode` - ![](../media/tutorial-weatherbot/07/set-property-zipcode.png) + ![](../media/tutorial-weatherbot/07/set-property-zipcode.png) 14. Click on **Restart Bot**, wait for the LUIS application to be updated (since you added a new entity) and then click on **Test in Emulator** @@ -147,5 +150,4 @@ Let's get this wired up. Now when you say "how is the weather in 98052" you should see your bot directly provide weather for that location instead of prompting you for zipcode. -![](../media/tutorial-weatherbot/07/with-entity-lookup.png) ---- +## ![](../media/tutorial-weatherbot/07/with-entity-lookup.png) diff --git a/toc.md b/toc.md index 720cfb389e..5071868fb7 100644 --- a/toc.md +++ b/toc.md @@ -1,23 +1,31 @@ - # Microsoft Bot Framework Composer + ### Overview + - [Introduction to Bot Framework Composer](./docs/introduction.md) -### Installation + +### Installation + - [Set up Composer using Yarn](./docs/setup-yarn.md) + ### QuickStart + - [Tour of Composer](./docs/onboarding.md) -- [Create your first bot](./docs/quickstart-create-bot.md) +- [Create your first bot](./docs/quickstart-create-bot.md) + ### Tutorials + 0. [Introduction](./docs/tutorial/tutorial-introduction.md) 1. [Create a bot](./docs/tutorial/tutorial-create-bot.md) -2. [Add a dialog](./docs/tutorial/tutorial-add-dialog.md) -3. [Get weather report](./docs/tutorial/tutorial-get-weather.md) -4. [Add help and cancel commands](./docs/tutorial/tutorial-add-help.md) -5. [Add Language Generation](./docs/tutorial/tutorial-lg.md) -6. [Use cards](./docs/tutorial/tutorial-cards.md) -7. [Add LUIS](./docs/tutorial/tutorial-luis.md) - -### Concepts +1. [Add a dialog](./docs/tutorial/tutorial-add-dialog.md) +1. [Get weather report](./docs/tutorial/tutorial-get-weather.md) +1. [Add help and cancel commands](./docs/tutorial/tutorial-add-help.md) +1. [Add Language Generation](./docs/tutorial/tutorial-lg.md) +1. [Use cards](./docs/tutorial/tutorial-cards.md) +1. [Add LUIS](./docs/tutorial/tutorial-luis.md) + +### Concepts + - [Dialogs](./docs/concept-dialog.md) - [Events and triggers](./docs/concept-events-and-triggers.md) - [Conversation flow and memory](./docs/concept-memory.md) @@ -25,32 +33,42 @@ - [Language understanding](./docs/concept-language-understanding.md) ### How to + #### Develop + - [Send messages](./docs/how-to-send-messages.md) - [Ask for user input](./docs/how-to-ask-for-user-input.md) - [Manage conversation flow](./docs/how-to-control-conversation-flow.md) - [Add LUIS to your bot](./docs/how-to-use-LUIS.md) - [Add QnA to your bot](./docs/how-to-add-qna-to-bot.md) -- [Send cards](./docs/how-to-send-cards.md) +- [Send cards](./docs/how-to-send-cards.md) - [Define triggers and events](./docs/how-to-define-triggers.md) - [Define advanced intents and entities](./docs/how-to-define-advanced-intents-entities.md) - [Use OAuth](./docs/how-to-use-oauth.md) - [Send an HTTP request](./docs/how-to-send-http-request.md) + #### Test + - [Test in Emulator](https://docs.microsoft.com/en-us/azure/bot-service/bot-service-debug-emulator?view=azure-bot-service-4.0&tabs=csharp) + #### Debug + - [Linting and validation](./docs/how-to-use-validation.md) + #### Publish + - [Publish your bot to Azure](/docs/how-to-publish-bot.md) ### Resources + - [Bot Framework Adaptive Dialog](https://github.com/microsoft/BotBuilder-Samples/tree/master/experimental/adaptive-dialog) - [Language generation](https://github.com/microsoft/BotBuilder-Samples/tree/master/experimental/language-generation) - [Common expression language](https://github.com/microsoft/BotBuilder-Samples/blob/master/experimental/common-expression-language) -- [.lu file format](https://github.com/microsoft/botbuilder-tools/blob/master/packages/Ludown/docs/lu-file-format.md) +- [.lu file format](https://aka.ms/lu-file-format) - [.lg file format](https://github.com/microsoft/BotBuilder-Samples/blob/master/experimental/language-generation/docs/lg-file-format.md) ## Related projects -* [Bot Framework](https://github.com/microsoft/botframework) -* [Bot Framework SDK](https://github.com/microsoft/botframework-sdk) -* [Bot Framework Emulator](https://github.com/microsoft/botframework-emulator) + +- [Bot Framework](https://github.com/microsoft/botframework) +- [Bot Framework SDK](https://github.com/microsoft/botframework-sdk) +- [Bot Framework Emulator](https://github.com/microsoft/botframework-emulator) From 7198c7ffd217a3be4b366a44664c4fa7acf8b764 Mon Sep 17 00:00:00 2001 From: liweitian Date: Mon, 2 Mar 2020 01:04:38 +0800 Subject: [PATCH 12/40] feat: update new trigger modal according to design (#1786) * update new trigger modal according to design * handle comments * auto-saved vscode lint setting * handle comments * css update * use luUtil api * feat: [Form Editor]inline lu editor in trigger (#1872) * lu in form editor * fix naming * bugfix * fix circular reference in shared and indexer * add start and end line for each intent * add diagnostics * delete lu editor in dialog * remove notice * fix bug: composer crashed when switch page to lu page * fix tslint error * support navigation back for lu error in notification * remove unused referrence * update the lib Co-authored-by: Zhixiang Zhan Co-authored-by: liweitian Co-authored-by: leileizhang * switch tsx back to js * switch js to tsx * add editor in trigger wizard * fix unit test * when lu is not valid, don't do update * handle comments * rebase code * resolve rebase conflict * enable inline mode * refactor luEditorWidget * replace third-part npm registry * update LgNotification class * handle comments * handle comments * fix lu editor widget * Configure Inline LU Editor to match Inline LG Editor Co-authored-by: Long Alan Co-authored-by: Zhixiang Zhan Co-authored-by: leileizhang Co-authored-by: zeye <2295905420@qq.com> Co-authored-by: Chris Whitten --- .../__tests__/components/design.test.js | 7 ++ Composer/packages/client/src/ShellApi.ts | 12 +- .../ProjectTree/TriggerCreationModal.tsx | 113 +++++++++++++----- .../src/components/ProjectTree/styles.ts | 18 ++- .../ExtensionContainer.tsx | 12 ++ .../client/src/pages/design/index.tsx | 11 +- .../client/src/pages/design/styles.js | 4 +- .../{styles.js => styles.ts} | 0 .../language-understanding/table-view.tsx | 2 +- .../client/src/pages/notifications/index.tsx | 36 +++--- .../client/src/pages/notifications/types.ts | 75 +++++++++++- .../pages/notifications/useNotifications.tsx | 40 ++----- .../packages/client/src/utils/dialogUtil.ts | 5 +- .../packages/client/src/utils/navigation.ts | 11 +- .../obiformeditor/demo/src/index.tsx | 3 + .../fields/RecognizerField/InlineLuEditor.tsx | 8 +- .../src/Form/fields/RecognizerField/index.tsx | 38 +----- .../src/Form/widgets/IntentWidget.tsx | 66 +++++----- .../src/Form/widgets/LuEditorWidget.tsx | 107 +++++++++++++++++ .../lib/indexers/src/dialogIndexer.ts | 16 ++- .../packages/lib/indexers/src/luIndexer.ts | 3 +- Composer/packages/lib/indexers/src/type.ts | 17 ++- .../lib/indexers/src/utils/diagnosticUtil.ts | 25 +++- .../packages/lib/indexers/src/utils/luUtil.ts | 3 +- .../packages/lib/shared/src/types/shell.ts | 20 ++++ Composer/yarn.lock | 54 ++++++--- 26 files changed, 483 insertions(+), 223 deletions(-) rename Composer/packages/client/src/pages/language-understanding/{styles.js => styles.ts} (100%) create mode 100644 Composer/packages/extensions/obiformeditor/src/Form/widgets/LuEditorWidget.tsx diff --git a/Composer/packages/client/__tests__/components/design.test.js b/Composer/packages/client/__tests__/components/design.test.js index ead441d63f..dbd3ce46fa 100644 --- a/Composer/packages/client/__tests__/components/design.test.js +++ b/Composer/packages/client/__tests__/components/design.test.js @@ -10,6 +10,13 @@ import { dialogs } from '../constants.json'; import { TriggerCreationModal } from './../../src/components/ProjectTree/TriggerCreationModal'; import { ProjectTree } from './../../src/components/ProjectTree'; import { CreateDialogModal } from './../../src/pages/design/createDialogModal'; + +jest.mock('@bfc/code-editor', () => { + return { + LuEditor: () =>
, + }; +}); + describe('', () => { it('should render the ProjectTree', async () => { const dialogId = 'Main'; diff --git a/Composer/packages/client/src/ShellApi.ts b/Composer/packages/client/src/ShellApi.ts index ddbacb8775..ed3dd20dea 100644 --- a/Composer/packages/client/src/ShellApi.ts +++ b/Composer/packages/client/src/ShellApi.ts @@ -217,9 +217,9 @@ export const ShellApi: React.FC = () => { if (!file) throw new Error(`lu file ${id} not found`); if (!intentName) throw new Error(`intentName is missing or empty`); - const newLuContent = luUtil.updateIntent(file.content, intentName, intent); + const content = luUtil.updateIntent(file.content, intentName, intent); - return await updateLuFile({ id, newLuContent }); + return await updateLuFile({ id, content }); } async function addLuIntentHandler({ id, intent }, event) { @@ -227,9 +227,9 @@ export const ShellApi: React.FC = () => { const file = luFiles.find(file => file.id === id); if (!file) throw new Error(`lu file ${id} not found`); - const newLuContent = luUtil.addIntent(file.content, intent); + const content = luUtil.addIntent(file.content, intent); - return await updateLuFile({ id, newLuContent }); + return await updateLuFile({ id, content }); } async function removeLuIntentHandler({ id, intentName }, event) { @@ -238,9 +238,9 @@ export const ShellApi: React.FC = () => { if (!file) throw new Error(`lu file ${id} not found`); if (!intentName) throw new Error(`intentName is missing or empty`); - const newLuContent = luUtil.removeIntent(file.content, intentName); + const content = luUtil.removeIntent(file.content, intentName); - return await updateLuFile({ id, newLuContent }); + return await updateLuFile({ id, content }); } async function fileHandler(fileTargetType, fileChangeType, { id, content }, event) { diff --git a/Composer/packages/client/src/components/ProjectTree/TriggerCreationModal.tsx b/Composer/packages/client/src/components/ProjectTree/TriggerCreationModal.tsx index ade8dd9461..472df83534 100644 --- a/Composer/packages/client/src/components/ProjectTree/TriggerCreationModal.tsx +++ b/Composer/packages/client/src/components/ProjectTree/TriggerCreationModal.tsx @@ -7,11 +7,14 @@ import React, { useState, useContext } from 'react'; import formatMessage from 'format-message'; import { Dialog, DialogType, DialogFooter } from 'office-ui-fabric-react/lib/Dialog'; import { PrimaryButton, DefaultButton } from 'office-ui-fabric-react/lib/Button'; +import { Label } from 'office-ui-fabric-react/lib/Label'; import { Stack } from 'office-ui-fabric-react/lib/Stack'; import { IDropdownOption } from 'office-ui-fabric-react/lib/Dropdown'; import { Dropdown } from 'office-ui-fabric-react/lib/Dropdown'; +import { TextField } from 'office-ui-fabric-react/lib/TextField'; +import { DialogInfo, luIndexer, combineMessage } from '@bfc/indexers'; import get from 'lodash/get'; -import { DialogInfo } from '@bfc/indexers'; +import { LuEditor } from '@bfc/code-editor'; import { addNewTrigger, @@ -25,15 +28,16 @@ import { getEventTypes, getActivityTypes, getMessageTypes, - regexRecognizerKey, } from '../../utils/dialogUtil'; +import { addIntent } from '../../utils/luUtil'; import { StoreContext } from '../../store'; -import { styles, dropdownStyles, dialogWindow } from './styles'; +import { styles, dropdownStyles, dialogWindow, intent } from './styles'; +const nameRegex = /^[a-zA-Z0-9-_.]+$/; const validateForm = (data: TriggerFormData): TriggerFormDataErrors => { const errors: TriggerFormDataErrors = {}; - const { $type, specifiedType } = data; + const { $type, specifiedType, intent, triggerPhrases } = data; if ($type === eventTypeKey && !specifiedType) { errors.specifiedType = formatMessage('Please select a event type'); @@ -46,21 +50,40 @@ const validateForm = (data: TriggerFormData): TriggerFormDataErrors => { if (!$type) { errors.$type = formatMessage('Please select a trigger type'); } + + if (!intent || !nameRegex.test(intent)) { + errors.intent = formatMessage( + 'Spaces and special characters are not allowed. Use letters, numbers, -, or _., numbers, -, and _' + ); + } + + if (!triggerPhrases) { + errors.triggerPhrases = formatMessage('Please input trigger phrases'); + } + if (data.errors.triggerPhrases) { + errors.triggerPhrases = data.errors.triggerPhrases; + } return errors; }; +interface LuFilePayload { + id: string; + content: string; +} + interface TriggerCreationModalProps { dialogId: string; isOpen: boolean; onDismiss: () => void; - onSubmit: (dialog: DialogInfo) => void; + onSubmit: (dialog: DialogInfo, luFilePayload: LuFilePayload) => void; } const initialFormData: TriggerFormData = { errors: {}, $type: intentTypeKey, - intent: '', specifiedType: '', + intent: '', + triggerPhrases: '', }; const triggerTypeOptions: IDropdownOption[] = getTriggerTypes(); @@ -71,7 +94,7 @@ export const TriggerCreationModal: React.FC = props = const { state } = useContext(StoreContext); const { dialogs, luFiles } = state; const luFile = luFiles.find(lu => lu.id === dialogId); - const dialogFile = dialogs.find(dialog => dialog.id === dialogId); + const onClickSubmitButton = e => { e.preventDefault(); const errors = validateForm(formData); @@ -83,8 +106,15 @@ export const TriggerCreationModal: React.FC = props = }); return; } + + const content = get(luFile, 'content', ''); + const newContent = addIntent(content, { Name: formData.intent, Body: formData.triggerPhrases }); + const updateLuFile = { + id: dialogId, + content: newContent, + }; const newDialog = addNewTrigger(dialogs, dialogId, formData); - onSubmit(newDialog); + onSubmit(newDialog, updateLuFile); onDismiss(); }; @@ -92,29 +122,27 @@ export const TriggerCreationModal: React.FC = props = setFormData({ ...initialFormData, $type: option.key }); }; - const onSelectIntent = (e, option) => { - setFormData({ ...formData, intent: option.key }); - }; - const onSelectSpecifiedTypeType = (e, option) => { setFormData({ ...formData, specifiedType: option.key }); }; + const onNameChange = (e, name) => { + setFormData({ ...formData, intent: name }); + }; + + const onTriggerPhrasesChange = (body: string) => { + const errors = formData.errors; + const content = '#' + formData.intent + '\n' + body; + const { diagnostics } = luIndexer.parse(content); + errors.triggerPhrases = combineMessage(diagnostics); + setFormData({ ...formData, triggerPhrases: body, errors }); + }; + const eventTypes: IDropdownOption[] = getEventTypes(); const activityTypes: IDropdownOption[] = getActivityTypes(); const messageTypes: IDropdownOption[] = getMessageTypes(); - const isRegEx = get(dialogFile, 'content.recognizer.$type', '') === regexRecognizerKey; - - const regexIntents = get(dialogFile, 'content.recognizer.intents', []); - const luisIntents = get(luFile, 'intents', []); - const intents = isRegEx ? regexIntents : luisIntents; - - const intentOptions = intents.map(t => { - return { key: t.name || t.Name || t.intent, text: t.name || t.Name || t.intent }; - }); - - const showIntentDropDown = formData.$type === intentTypeKey; + const showIntentFields = formData.$type === intentTypeKey; const showEventDropDown = formData.$type === eventTypeKey; const showActivityDropDown = formData.$type === activityTypeKey; const showMessageDropDown = formData.$type === messageTypeKey; @@ -178,15 +206,38 @@ export const TriggerCreationModal: React.FC = props = data-testid={'messageTypeDropDown'} /> )} - {showIntentDropDown && ( - + )} + {showIntentFields && } + {showIntentFields && ( + )} diff --git a/Composer/packages/client/src/components/ProjectTree/styles.ts b/Composer/packages/client/src/components/ProjectTree/styles.ts index afb5647bd4..0d53763b9a 100644 --- a/Composer/packages/client/src/components/ProjectTree/styles.ts +++ b/Composer/packages/client/src/components/ProjectTree/styles.ts @@ -137,7 +137,7 @@ export const dropdownStyles = { fontWeight: FontWeights.semibold, }, dropdown: { - width: '300px', + width: '400px', }, root: { paddingBottom: '20px', @@ -148,7 +148,7 @@ export const dialogWindow = css` display: flex; flex-direction: column; width: 400px; - height: 250px; + min-height: 300px; `; export const textFieldlabel = { @@ -162,11 +162,17 @@ export const textFieldlabel = { }; export const intent = { - fieldGroup: { - width: 200, + root: { + width: '400px', + paddingBottom: '20px', }, +}; + +export const triggerPhrases = { root: { - height: '90px', + width: '400px', + }, + fieldGroup: { + height: 80, }, - subComponentStyles: textFieldlabel, }; diff --git a/Composer/packages/client/src/extension-container/ExtensionContainer.tsx b/Composer/packages/client/src/extension-container/ExtensionContainer.tsx index bb37bc25b1..9ad680cfd9 100644 --- a/Composer/packages/client/src/extension-container/ExtensionContainer.tsx +++ b/Composer/packages/client/src/extension-container/ExtensionContainer.tsx @@ -98,6 +98,18 @@ const shellApi: ShellApi = { }); }, + addLuIntent: (id, intent) => { + return apiClient.apiCall('addLuIntent', { id, intent }); + }, + + updateLuIntent: (id, intentName, intent) => { + return apiClient.apiCall('updateLuIntent', { id, intentName, intent }); + }, + + removeLuIntent: (id, intentName) => { + return apiClient.apiCall('removeLuIntent', { id, intentName }); + }, + createDialog: () => { return apiClient.apiCall('createDialog'); }, diff --git a/Composer/packages/client/src/pages/design/index.tsx b/Composer/packages/client/src/pages/design/index.tsx index 778be7846f..25655f874f 100644 --- a/Composer/packages/client/src/pages/design/index.tsx +++ b/Composer/packages/client/src/pages/design/index.tsx @@ -171,14 +171,19 @@ function DesignPage(props) { setTriggerModalVisibility(true); }; - const onTriggerCreationSubmit = dialog => { - const payload = { + const onTriggerCreationSubmit = (dialog, luFile) => { + const dialogPayload = { id: dialog.id, content: dialog.content, }; + const luFilePayload = { + id: luFile.id, + content: luFile.content, + }; const index = get(dialog, 'content.triggers', []).length - 1; actions.selectTo(`triggers[${index}]`); - actions.updateDialog(payload); + actions.updateLuFile(luFilePayload); + actions.updateDialog(dialogPayload); }; function handleSelect(id, selected = '') { diff --git a/Composer/packages/client/src/pages/design/styles.js b/Composer/packages/client/src/pages/design/styles.js index 5b267a00eb..a155b225fd 100644 --- a/Composer/packages/client/src/pages/design/styles.js +++ b/Composer/packages/client/src/pages/design/styles.js @@ -113,9 +113,9 @@ export const middleTriggerContainer = css` display: flex; justify-content: center; align-items: center; - background: #e5e5e5; + background: #f6f6f6; width: 100%; - margin-top: 48px; + margin-top: 55px; height: calc(100% - 48px); position: absolute; `; diff --git a/Composer/packages/client/src/pages/language-understanding/styles.js b/Composer/packages/client/src/pages/language-understanding/styles.ts similarity index 100% rename from Composer/packages/client/src/pages/language-understanding/styles.js rename to Composer/packages/client/src/pages/language-understanding/styles.ts diff --git a/Composer/packages/client/src/pages/language-understanding/table-view.tsx b/Composer/packages/client/src/pages/language-understanding/table-view.tsx index b04afef158..2edbf09e13 100644 --- a/Composer/packages/client/src/pages/language-understanding/table-view.tsx +++ b/Composer/packages/client/src/pages/language-understanding/table-view.tsx @@ -79,7 +79,7 @@ const TableView: React.FC = props => { name, phrases, fileId: luFile.id, - used: luDialog ? luDialog.luIntents.includes(name) : false, // used by it's dialog or not + used: !!luDialog && luDialog.referredLuIntents.some(lu => lu.name === name), // used by it's dialog or not state, }); }); diff --git a/Composer/packages/client/src/pages/notifications/index.tsx b/Composer/packages/client/src/pages/notifications/index.tsx index cccac44dc3..093238013a 100644 --- a/Composer/packages/client/src/pages/notifications/index.tsx +++ b/Composer/packages/client/src/pages/notifications/index.tsx @@ -3,49 +3,41 @@ /** @jsx jsx */ import { jsx } from '@emotion/core'; -import { useState, useContext } from 'react'; +import { useState } from 'react'; import { RouteComponentProps } from '@reach/router'; -import { StoreContext } from '../../store'; - import { ToolBar } from './../../components/ToolBar/index'; import useNotifications from './useNotifications'; import { NotificationList } from './NotificationList'; import { NotificationHeader } from './NotificationHeader'; import { root } from './styles'; -import { INotification } from './types'; +import { INotification, NotificationType } from './types'; import { navigateTo } from './../../utils'; -import { convertDialogDiagnosticToUrl, toUrlUtil } from './../../utils/navigation'; +import { convertPathToUrl, toUrlUtil } from './../../utils/navigation'; const Notifications: React.FC = () => { const [filter, setFilter] = useState(''); - const { state } = useContext(StoreContext); - const { dialogs } = state; const notifications = useNotifications(filter); const navigations = { - lg: (item: INotification) => { + [NotificationType.LG]: (item: INotification) => { let url = `/language-generation/${item.id}/edit#L=${item.diagnostic.range?.start.line || 0}`; - const dividerIndex = item.id.indexOf('#'); //the format of item.id is lgFile#inlineTemplateId - if (dividerIndex > -1) { - const templateId = item.id.substring(dividerIndex + 1); - const lgFile = item.id.substring(0, dividerIndex); - const dialog = dialogs.find(d => d.lgFile === lgFile); - const lgTemplate = dialog ? dialog.lgTemplates.find(lg => lg.name === templateId) : null; - const path = lgTemplate ? lgTemplate.path : ''; - if (path && dialog) { - url = toUrlUtil(dialog.id, path); - } + if (item.dialogPath) { + url = toUrlUtil(item.dialogPath); } navigateTo(url); }, - lu: (item: INotification) => { - navigateTo(`/dialogs/${item.id}`); + [NotificationType.LU]: (item: INotification) => { + let uri = `/language-understanding/${item.id}`; + if (item.dialogPath) { + uri = convertPathToUrl(item.id, item.dialogPath); + } + navigateTo(uri); }, - dialog: (item: INotification) => { + [NotificationType.DIALOG]: (item: INotification) => { //path is like main.trigers[0].actions[0] //uri = id?selected=triggers[0]&focused=triggers[0].actions[0] - const uri = convertDialogDiagnosticToUrl(item.diagnostic); + const uri = convertPathToUrl(item.id, item.dialogPath); navigateTo(uri); }, }; diff --git a/Composer/packages/client/src/pages/notifications/types.ts b/Composer/packages/client/src/pages/notifications/types.ts index bce5eef0a0..707e3823da 100644 --- a/Composer/packages/client/src/pages/notifications/types.ts +++ b/Composer/packages/client/src/pages/notifications/types.ts @@ -1,13 +1,84 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +import { Diagnostic, createSingleMessage, DialogInfo, LuFile, isDiagnosticWithInRange } from '@bfc/indexers'; + +import { replaceDialogDiagnosticLabel } from '../../utils'; +export const DiagnosticSeverity = ['Error', 'Warning']; //'Information', 'Hint' + +export enum NotificationType { + DIALOG, + LG, + LU, + GENERAL, +} export interface INotification { id: string; severity: string; - type: string; + type: NotificationType; location: string; message: string; diagnostic: any; + dialogPath?: string; //the data path in dialog } -export const DiagnosticSeverity = ['Error', 'Warning']; //'Information', 'Hint' +export abstract class Notification implements INotification { + id: string; + severity: string; + type = NotificationType.GENERAL; + location: string; + message = ''; + diagnostic: Diagnostic; + dialogPath?: string; + constructor(id: string, location: string, diagnostic: Diagnostic) { + this.id = id; + this.severity = DiagnosticSeverity[diagnostic.severity] || ''; + this.diagnostic = diagnostic; + this.location = location; + } +} + +export class DialogNotification extends Notification { + type = NotificationType.DIALOG; + constructor(id: string, location: string, diagnostic: Diagnostic) { + super(id, location, diagnostic); + this.message = `In ${replaceDialogDiagnosticLabel(diagnostic.path)} ${diagnostic.message}`; + this.dialogPath = diagnostic.path; + } +} + +export class LgNotification extends Notification { + type = NotificationType.LG; + constructor(id: string, lgTemplateName: string, location: string, diagnostic: Diagnostic, dialogs: DialogInfo[]) { + super(id, location, diagnostic); + this.message = createSingleMessage(diagnostic); + this.dialogPath = this.findDialogPath(dialogs, id, lgTemplateName); + } + private findDialogPath(dialogs: DialogInfo[], id: string, lgTemplateName: string) { + if (lgTemplateName) { + const dialog = dialogs.find(d => d.lgFile === id); + const lgTemplate = dialog ? dialog.lgTemplates.find(lg => lg.name === lgTemplateName) : null; + const path = lgTemplate ? lgTemplate.path : ''; + return path; + } + } +} + +export class LuNotification extends Notification { + type = NotificationType.LU; + constructor(id: string, location: string, diagnostic: Diagnostic, luFile: LuFile, dialogs: DialogInfo[]) { + super(id, location, diagnostic); + this.dialogPath = this.findDialogPath(luFile, dialogs, diagnostic); + this.message = createSingleMessage(diagnostic); + } + + private findDialogPath(luFile: LuFile, dialogs: DialogInfo[], d: Diagnostic) { + const intentName = luFile.intents.find(intent => { + const { range } = intent; + if (!range) return false; + return isDiagnosticWithInRange(d, range); + })?.Name; + + return dialogs.find(dialog => dialog.id === luFile.id)?.referredLuIntents.find(lu => lu.name === intentName)?.path; + } +} diff --git a/Composer/packages/client/src/pages/notifications/useNotifications.tsx b/Composer/packages/client/src/pages/notifications/useNotifications.tsx index 8865060dc7..91441951de 100644 --- a/Composer/packages/client/src/pages/notifications/useNotifications.tsx +++ b/Composer/packages/client/src/pages/notifications/useNotifications.tsx @@ -2,44 +2,28 @@ // Licensed under the MIT License. import { useContext, useMemo } from 'react'; -import { createSingleMessage } from '@bfc/indexers'; import get from 'lodash/get'; import { LgNamePattern } from '@bfc/shared'; import { StoreContext } from '../../store'; -import { replaceDialogDiagnosticLabel } from '../../utils'; -import { INotification, DiagnosticSeverity } from './types'; +import { Notification, DialogNotification, LuNotification, LgNotification } from './types'; import { getReferredFiles } from './../../utils/luUtil'; export default function useNotifications(filter?: string) { const { state } = useContext(StoreContext); const { dialogs, luFiles, lgFiles } = state; const memoized = useMemo(() => { - const notifactions: INotification[] = []; + const notifactions: Notification[] = []; dialogs.forEach(dialog => { dialog.diagnostics.map(diagnostic => { const location = `${dialog.id}.dialog`; - notifactions.push({ - type: 'dialog', - location, - message: `In ${replaceDialogDiagnosticLabel(diagnostic.path)} ${diagnostic.message}`, - severity: DiagnosticSeverity[diagnostic.severity] || '', - diagnostic, - id: dialog.id, - }); + notifactions.push(new DialogNotification(dialog.id, location, diagnostic)); }); }); getReferredFiles(luFiles, dialogs).forEach(lufile => { lufile.diagnostics.map(diagnostic => { const location = `${lufile.id}.lu`; - notifactions.push({ - type: 'lu', - location, - message: createSingleMessage(diagnostic), - severity: 'Error', - diagnostic, - id: lufile.id, - }); + notifactions.push(new LuNotification(lufile.id, location, diagnostic, lufile, dialogs)); }); }); lgFiles.forEach(lgFile => { @@ -53,26 +37,20 @@ export default function useNotifications(filter?: string) { get(diagnostic, 'range.start.line') >= get(t, 'range.startLineNumber') && get(diagnostic, 'range.end.line') <= get(t, 'range.endLineNumber') ); - let id = lgFile.id; + const id = lgFile.id; const location = `${lgFile.id}.lg`; + let lgTemplateName = ''; if (mappedTemplate && mappedTemplate.name.match(LgNamePattern)) { //should navigate to design page - id = `${lgFile.id}#${mappedTemplate.name}`; + lgTemplateName = mappedTemplate.name; } - notifactions.push({ - type: 'lg', - severity: DiagnosticSeverity[diagnostic.severity] || '', - location, - message: createSingleMessage(diagnostic), - diagnostic, - id, - }); + notifactions.push(new LgNotification(id, lgTemplateName, location, diagnostic, dialogs)); }); }); return notifactions; }, [dialogs, luFiles, lgFiles]); - const notifications: INotification[] = !filter ? memoized : memoized.filter(x => x.severity === filter); + const notifications: Notification[] = filter ? memoized.filter(x => x.severity === filter) : memoized; return notifications; } diff --git a/Composer/packages/client/src/utils/dialogUtil.ts b/Composer/packages/client/src/utils/dialogUtil.ts index e388decfb0..e54934034c 100644 --- a/Composer/packages/client/src/utils/dialogUtil.ts +++ b/Composer/packages/client/src/utils/dialogUtil.ts @@ -21,14 +21,16 @@ interface DialogsMap { export interface TriggerFormData { errors: TriggerFormDataErrors; $type: string; - intent: string; specifiedType: string; + intent: string; + triggerPhrases: string; } export interface TriggerFormDataErrors { $type?: string; intent?: string; specifiedType?: string; + triggerPhrases?: string; } export function getDialog(dialogs: DialogInfo[], dialogId: string) { @@ -67,7 +69,6 @@ export function insert(content, path: string, position: number | undefined, data if (data.intent) { optionalAttributes.intent = data.intent; } - const newStep = { $type: data.$type, ...seedNewDialog(data.$type, {}, optionalAttributes), diff --git a/Composer/packages/client/src/utils/navigation.ts b/Composer/packages/client/src/utils/navigation.ts index 415f6285a1..117734326b 100644 --- a/Composer/packages/client/src/utils/navigation.ts +++ b/Composer/packages/client/src/utils/navigation.ts @@ -3,7 +3,6 @@ import cloneDeep from 'lodash/cloneDeep'; import { navigate, NavigateOptions } from '@reach/router'; -import { Diagnostic } from '@bfc/indexers'; import { BreadcrumbItem, DesignPageLocation } from '../store/types'; @@ -77,13 +76,11 @@ interface NavigationState { breadcrumb: BreadcrumbItem[]; } -export function convertDialogDiagnosticToUrl(diagnostic: Diagnostic): string { +export function convertPathToUrl(id: string, path?: string): string { //path is like main.trigers[0].actions[0] //uri = id?selected=triggers[0]&focused=triggers[0].actions[0] - const { path, source } = diagnostic; - if (!source) return ''; - let uri = `/dialogs/${source}`; + let uri = `/dialogs/${id}`; if (!path) return uri; const items = path.split('#'); @@ -107,8 +104,10 @@ export function convertDialogDiagnosticToUrl(diagnostic: Diagnostic): string { return uri; } -export function toUrlUtil(dialogId: string, path: string): string { +export function toUrlUtil(path: string): string { const tokens = path.split('#'); + const firstDotIndex = tokens[0].indexOf('.'); + const dialogId = tokens[0].substring(0, firstDotIndex); const focusedPath = parsePathToFocused(tokens[0]); const selectedPath = parsePathToSelected(tokens[0]); const type = tokens[1]; diff --git a/Composer/packages/extensions/obiformeditor/demo/src/index.tsx b/Composer/packages/extensions/obiformeditor/demo/src/index.tsx index 8897c9d207..a399e293d7 100644 --- a/Composer/packages/extensions/obiformeditor/demo/src/index.tsx +++ b/Composer/packages/extensions/obiformeditor/demo/src/index.tsx @@ -173,6 +173,9 @@ const mockShellApi = [ 'getLgTemplates', 'createLgTemplate', 'updateLgTemplate', + 'addLuIntent', + 'updateLuIntent', + 'removeLuIntent', 'validateExpression', 'onFocusSteps', 'onFocusEvent', diff --git a/Composer/packages/extensions/obiformeditor/src/Form/fields/RecognizerField/InlineLuEditor.tsx b/Composer/packages/extensions/obiformeditor/src/Form/fields/RecognizerField/InlineLuEditor.tsx index 65f989aabf..2652028a8e 100644 --- a/Composer/packages/extensions/obiformeditor/src/Form/fields/RecognizerField/InlineLuEditor.tsx +++ b/Composer/packages/extensions/obiformeditor/src/Form/fields/RecognizerField/InlineLuEditor.tsx @@ -20,8 +20,12 @@ const InlineLuEditor: React.FC = props => { setLocalContent(value); onSave(value); }; - - return ; + const luOption = { + fileId: file.id, + }; + return ( + + ); }; export default InlineLuEditor; diff --git a/Composer/packages/extensions/obiformeditor/src/Form/fields/RecognizerField/index.tsx b/Composer/packages/extensions/obiformeditor/src/Form/fields/RecognizerField/index.tsx index a4ba6f1048..5a490cfda5 100644 --- a/Composer/packages/extensions/obiformeditor/src/Form/fields/RecognizerField/index.tsx +++ b/Composer/packages/extensions/obiformeditor/src/Form/fields/RecognizerField/index.tsx @@ -1,28 +1,24 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import React, { useState, ReactElement, Suspense, useEffect } from 'react'; +import React, { useState, ReactElement } from 'react'; import formatMessage from 'format-message'; import { FieldProps } from '@bfcomposer/react-jsonschema-form'; import { Dropdown, ResponsiveMode, IDropdownOption } from 'office-ui-fabric-react/lib/Dropdown'; import { Spinner, SpinnerSize } from 'office-ui-fabric-react/lib/Spinner'; import { MicrosoftIRecognizer } from '@bfc/shared'; -import { LuFile, combineMessage } from '@bfc/indexers'; +import { LuFile } from '@bfc/indexers'; import { BaseField } from '../BaseField'; -import { LoadingSpinner } from '../../../LoadingSpinner'; import ToggleEditor from './ToggleEditor'; import RegexEditor from './RegexEditor'; import './styles.css'; -const InlineLuEditor = React.lazy(() => import('./InlineLuEditor')); - export const RecognizerField: React.FC> = props => { const { formData } = props; const [loading, setLoading] = useState(false); - const [errorMsg, setErrorMsg] = useState(''); const { formContext: { luFiles, shellApi, currentDialog }, @@ -32,19 +28,6 @@ export const RecognizerField: React.FC> = props const isRegex = typeof formData === 'object' && formData.$type === 'Microsoft.RegexRecognizer'; const currentDialogId = currentDialog.id; const selectedFile: LuFile | void = luFiles.find(f => f.id === currentDialogId); - const isLuFileSelected = Boolean( - selectedFile && typeof props.formData === 'string' && props.formData.startsWith(selectedFile.id) - ); - - //make the inline editor show error message - useEffect(() => { - if (selectedFile && selectedFile.diagnostics.length > 0) { - const msg = combineMessage(selectedFile.diagnostics); - setErrorMsg(msg); - } else { - setErrorMsg(''); - } - }, [selectedFile]); const handleChange = (_, option?: IDropdownOption): void => { if (option) { @@ -144,23 +127,8 @@ export const RecognizerField: React.FC> = props responsiveMode={ResponsiveMode.large} onRenderTitle={onRenderTitle} /> - + {() => { - if (selectedFile && isLuFileSelected) { - const updateLuFile = (newValue?: string): void => { - shellApi.updateLuFile({ id: selectedFile.id, content: newValue }).catch(setErrorMsg); - }; - - return ( - }> - - - ); - } if (isRegex) { return ; } diff --git a/Composer/packages/extensions/obiformeditor/src/Form/widgets/IntentWidget.tsx b/Composer/packages/extensions/obiformeditor/src/Form/widgets/IntentWidget.tsx index dbe9bef687..bc3579bb6d 100644 --- a/Composer/packages/extensions/obiformeditor/src/Form/widgets/IntentWidget.tsx +++ b/Composer/packages/extensions/obiformeditor/src/Form/widgets/IntentWidget.tsx @@ -5,11 +5,12 @@ import React from 'react'; import { Dropdown, ResponsiveMode, IDropdownOption } from 'office-ui-fabric-react/lib/Dropdown'; import formatMessage from 'format-message'; import { RegexRecognizer } from '@bfc/shared'; -import { LuFile, DialogInfo } from '@bfc/indexers'; +import { DialogInfo } from '@bfc/indexers'; -import { BFDWidgetProps, FormContext } from '../types'; +import { BFDWidgetProps } from '../types'; import { WidgetLabel } from './WidgetLabel'; +import { LuEditorWidget } from './LuEditorWidget'; const EMPTY_OPTION = { key: '', text: '' }; @@ -48,42 +49,26 @@ function regexIntentOptions(currentDialog: DialogInfo): IDropdownOption[] { return options; } -function luIntentOptions(formContext: FormContext): IDropdownOption[] { - const luFile: LuFile | void = formContext.luFiles.find(f => f.id === formContext.currentDialog.id); - let options: IDropdownOption[] = [EMPTY_OPTION]; - - if (luFile) { - const intents: { name: string }[] = luFile.intents.map(({ Name: name }) => { - return { - name, - }; - }); - - options = options.concat( - intents.map(i => ({ - key: i.name, - text: i.name, - })) - ); - } - - return options; -} - export const IntentWidget: React.FC = props => { const { disabled, onChange, id, onFocus, onBlur, value, formContext, placeholder, label, schema } = props; const { description } = schema; + const { currentDialog } = formContext; let options: IDropdownOption[] = []; + let widgetLabel = label; + let isLuisSelected = false; - switch (recognizerType(formContext.currentDialog)) { + switch (recognizerType(currentDialog)) { case RecognizerType.regex: - options = regexIntentOptions(formContext.currentDialog); + options = regexIntentOptions(currentDialog); + isLuisSelected = false; break; case RecognizerType.luis: - options = luIntentOptions(formContext); + widgetLabel = `Trigger phrases(intent name: #${value || ''})`; + isLuisSelected = true; break; default: options = [EMPTY_OPTION]; + isLuisSelected = false; break; } @@ -95,18 +80,21 @@ export const IntentWidget: React.FC = props => { return ( <> - - onBlur && onBlur(id, value)} - onChange={handleChange} - onFocus={() => onFocus && onFocus(id, value)} - options={options} - selectedKey={value || null} - responsiveMode={ResponsiveMode.large} - disabled={disabled || options.length === 1} - placeholder={options.length > 1 ? placeholder : formatMessage('No intents configured for this dialog')} - /> + + {!isLuisSelected && ( + onBlur && onBlur(id, value)} + onChange={handleChange} + onFocus={() => onFocus && onFocus(id, value)} + options={options} + selectedKey={value || null} + responsiveMode={ResponsiveMode.large} + disabled={disabled || options.length === 1} + placeholder={options.length > 1 ? placeholder : formatMessage('No intents configured for this dialog')} + /> + )} + {isLuisSelected && } ); }; diff --git a/Composer/packages/extensions/obiformeditor/src/Form/widgets/LuEditorWidget.tsx b/Composer/packages/extensions/obiformeditor/src/Form/widgets/LuEditorWidget.tsx new file mode 100644 index 0000000000..0d05aac956 --- /dev/null +++ b/Composer/packages/extensions/obiformeditor/src/Form/widgets/LuEditorWidget.tsx @@ -0,0 +1,107 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import React from 'react'; +import { LuEditor } from '@bfc/code-editor'; +import debounce from 'lodash/debounce'; +import { LuIntentSection } from '@bfc/shared'; +import { LuFile, filterSectionDiagnostics } from '@bfc/indexers'; + +import { FormContext } from '../types'; + +interface LuEditorWidgetProps { + formContext: FormContext; + name: string; + height?: number | string; + onChange: (template?: string) => void; +} + +export class LuEditorWidget extends React.Component { + constructor(props) { + super(props); + this.debounceUpdate = debounce(this.updateLuIntent, 500); + this.name = this.props.name; + this.formContext = this.props.formContext; + this.luFileId = this.formContext.currentDialog.id; + this.luFile = this.formContext.luFiles.find(f => f.id === this.luFileId); + this.luIntent = (this.luFile && this.luFile.intents.find(intent => intent.Name === this.name)) || { + Name: this.name, + Body: '', + }; + } + + formContext: FormContext; + name: string; + luFileId: string; + luFile: LuFile | null; + luIntent: LuIntentSection; + state = { localValue: '' }; + debounceUpdate; + updateLuIntent = (body: string) => { + this.formContext.shellApi.updateLuIntent(this.luFileId, this.name, { Name: this.name, Body: body }).catch(() => {}); + }; + + static getDerivedStateFromProps(nextProps, prevState) { + const name = nextProps.name; + const formContext = nextProps.formContext; + const luFileId = formContext.currentDialog.id; + const luFile = formContext.luFiles.find(f => f.id === luFileId); + const luIntent = (luFile && luFile.intents.find(intent => intent.Name === name)) || { + Name: name, + Body: '', + }; + if (!prevState.localValue) { + return { + localValue: luIntent.Body, + }; + } + return null; + } + + onChange = (body: string) => { + this.setState({ + localValue: body, + }); + if (this.luFileId) { + if (body) { + this.updateLuIntent(body); + } else { + this.formContext.shellApi.removeLuIntent(this.luFileId, this.name); + } + } + }; + render() { + const diagnostic = this.luFile && filterSectionDiagnostics(this.luFile.diagnostics, this.luIntent)[0]; + + const errorMsg = diagnostic + ? diagnostic.message.split('error message: ')[diagnostic.message.split('error message: ').length - 1] + : ''; + const luOption = { + fileId: this.luFileId, + sectionId: this.luIntent?.Name, + }; + const height = this.props.height || 250; + + return ( + + ); + } +} diff --git a/Composer/packages/lib/indexers/src/dialogIndexer.ts b/Composer/packages/lib/indexers/src/dialogIndexer.ts index 0940379b96..eb36ee6ae6 100644 --- a/Composer/packages/lib/indexers/src/dialogIndexer.ts +++ b/Composer/packages/lib/indexers/src/dialogIndexer.ts @@ -5,8 +5,9 @@ import has from 'lodash/has'; import uniq from 'lodash/uniq'; import { extractLgTemplateRefs } from '@bfc/shared'; +import { createPath } from './dialogUtils/dialogChecker'; import { checkerFuncs } from './dialogUtils/dialogChecker'; -import { ITrigger, DialogInfo, FileInfo, LgTemplateJsonPath } from './type'; +import { ITrigger, DialogInfo, FileInfo, LgTemplateJsonPath, ReferredLuIntents } from './type'; import { JsonWalk, VisitorFunc } from './utils/jsonWalk'; import { getBaseName } from './utils/help'; import { Diagnostic } from './diagnostic'; @@ -70,8 +71,8 @@ function ExtractLgTemplates(id, dialog): LgTemplateJsonPath[] { } // find out all lu intents given dialog -function ExtractLuIntents(dialog): string[] { - const intents: string[] = []; +function ExtractLuIntents(dialog, id: string): ReferredLuIntents[] { + const intents: ReferredLuIntents[] = []; /** * * @param path , jsonPath string * @param value , current node value * @@ -81,11 +82,14 @@ function ExtractLuIntents(dialog): string[] { // it's a valid schema dialog node. if (has(value, '$type') && value.$type === 'Microsoft.OnIntent') { const intentName = value.intent; - intents.push(intentName); + intents.push({ + name: intentName, + path: createPath(path, value.$type), + }); } return false; }; - JsonWalk('$', dialog, visitor); + JsonWalk(id, dialog, visitor); return uniq(intents); } @@ -195,8 +199,8 @@ function parse(id: string, content: any, schema: any) { diagnostics: validate(id, content, schema), referredDialogs: ExtractReferredDialogs(content), lgTemplates: ExtractLgTemplates(id, content), - luIntents: ExtractLuIntents(content), userDefinedVariables: ExtractMemoryPaths(content), + referredLuIntents: ExtractLuIntents(content, id), luFile: getBaseName(luFile, '.lu'), lgFile: getBaseName(lgFile, '.lg'), triggers: ExtractTriggers(content), diff --git a/Composer/packages/lib/indexers/src/luIndexer.ts b/Composer/packages/lib/indexers/src/luIndexer.ts index 49a579bcc0..164ea2d89c 100644 --- a/Composer/packages/lib/indexers/src/luIndexer.ts +++ b/Composer/packages/lib/indexers/src/luIndexer.ts @@ -3,8 +3,9 @@ import { sectionHandler } from '@bfcomposer/bf-lu/lib/parser'; import get from 'lodash/get'; +import { LuIntentSection } from '@bfc/shared'; -import { FileInfo, LuFile, LuParsed, LuSectionTypes, LuIntentSection } from './type'; +import { FileInfo, LuFile, LuParsed, LuSectionTypes } from './type'; import { getBaseName } from './utils/help'; import { Diagnostic, Position, Range, DiagnosticSeverity } from './diagnostic'; import { FileExtensions } from './utils/fileExtensions'; diff --git a/Composer/packages/lib/indexers/src/type.ts b/Composer/packages/lib/indexers/src/type.ts index 5e9f3d62ce..5b47641a57 100644 --- a/Composer/packages/lib/indexers/src/type.ts +++ b/Composer/packages/lib/indexers/src/type.ts @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +import { LuIntentSection } from '@bfc/shared'; + import { Diagnostic } from './diagnostic'; export interface FileInfo { @@ -17,6 +19,11 @@ export interface ITrigger { isIntent: boolean; } +export interface ReferredLuIntents { + name: string; + path: string; +} + export interface DialogInfo { content: any; diagnostics: Diagnostic[]; @@ -26,7 +33,7 @@ export interface DialogInfo { lgFile: string; lgTemplates: LgTemplateJsonPath[]; luFile: string; - luIntents: string[]; + referredLuIntents: ReferredLuIntents[]; referredDialogs: string[]; relativePath: string; userDefinedVariables: string[]; @@ -62,14 +69,6 @@ export interface LuEntity { Name: string; } -export interface LuIntentSection { - Name: string; - Body: string; - Entities?: LuEntity[]; - Children?: LuIntentSection[]; - range?: CodeRange; -} - export interface LuFile { id: string; relativePath: string; diff --git a/Composer/packages/lib/indexers/src/utils/diagnosticUtil.ts b/Composer/packages/lib/indexers/src/utils/diagnosticUtil.ts index 41695327c2..92c16061f3 100644 --- a/Composer/packages/lib/indexers/src/utils/diagnosticUtil.ts +++ b/Composer/packages/lib/indexers/src/utils/diagnosticUtil.ts @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +import { LuIntentSection } from '@bfc/shared'; + import { Diagnostic, DiagnosticSeverity, Range, Position } from '../diagnostic'; import { CodeRange } from '../type'; @@ -28,10 +30,15 @@ export function offsetRange(range: Range, offset: number): Range { ); } +export function isDiagnosticWithInRange(diagnostic: Diagnostic, range: CodeRange): boolean { + if (!diagnostic.range) return false; + return diagnostic.range.start.line >= range.startLineNumber && diagnostic.range.end.line <= range.endLineNumber; +} + export function filterTemplateDiagnostics(diagnostics: Diagnostic[], { range }: { range?: CodeRange }): Diagnostic[] { if (!range) return diagnostics; const filteredDiags = diagnostics.filter(d => { - return d.range && d.range.start.line >= range.startLineNumber && d.range.end.line <= range.endLineNumber; + return d.range && isDiagnosticWithInRange(d, range); }); const offset = range.startLineNumber; return filteredDiags.map(d => { @@ -46,6 +53,22 @@ export function filterTemplateDiagnostics(diagnostics: Diagnostic[], { range }: }); } +export function filterSectionDiagnostics(diagnostics: Diagnostic[], section: LuIntentSection): Diagnostic[] { + const { range } = section; + if (!range) return diagnostics; + const filteredDiags = diagnostics.filter(d => { + return isDiagnosticWithInRange(d, range); + }); + const offset = range.startLineNumber; + return filteredDiags.map(d => { + const { range } = d; + if (range) { + d.range = offsetRange(range, offset); + } + return d; + }); +} + export function findErrors(diagnostics: Diagnostic[]): Diagnostic[] { return diagnostics.filter(d => d.severity === DiagnosticSeverity.Error); } diff --git a/Composer/packages/lib/indexers/src/utils/luUtil.ts b/Composer/packages/lib/indexers/src/utils/luUtil.ts index ef0867686d..0baac955ff 100644 --- a/Composer/packages/lib/indexers/src/utils/luUtil.ts +++ b/Composer/packages/lib/indexers/src/utils/luUtil.ts @@ -9,8 +9,9 @@ import { sectionHandler } from '@bfcomposer/bf-lu/lib/parser'; import isEmpty from 'lodash/isEmpty'; +import { LuIntentSection } from '@bfc/shared'; -import { LuIntentSection, LuSectionTypes } from '../type'; +import { LuSectionTypes } from '../type'; import { luIndexer } from '../luIndexer'; import { Diagnostic } from '../diagnostic'; const { parse } = luIndexer; diff --git a/Composer/packages/lib/shared/src/types/shell.ts b/Composer/packages/lib/shared/src/types/shell.ts index 0077441749..521d3600a1 100644 --- a/Composer/packages/lib/shared/src/types/shell.ts +++ b/Composer/packages/lib/shared/src/types/shell.ts @@ -3,6 +3,23 @@ import { LGTemplate as LgTemplate } from 'botbuilder-lg'; +export interface LuIntentSection { + Name: string; + Body: string; + Entities?: LuEntity[]; + Children?: LuIntentSection[]; + range?: CodeRange; +} + +export interface CodeRange { + startLineNumber: number; + endLineNumber: number; +} + +export interface LuEntity { + Name: string; +} + export interface EditorSchema { content?: { fieldTemplateOverrides?: any; @@ -53,6 +70,9 @@ export interface ShellApi { updateLgTemplate: (id: string, templateName: string, templateStr: string) => Promise; removeLgTemplate: (id: string, templateName: string) => Promise; removeLgTemplates: (id: string, templateNames: string[]) => Promise; + addLuIntent: (id: string, intent: LuIntentSection | null) => Promise; + updateLuIntent: (id: string, intentName: string, intent: LuIntentSection | null) => Promise; + removeLuIntent: (id: string, intentName: string) => Promise; createDialog: () => Promise; validateExpression: (expression?: string) => Promise; // TODO: fix these types diff --git a/Composer/yarn.lock b/Composer/yarn.lock index 160659b32a..be76d64dd2 100644 --- a/Composer/yarn.lock +++ b/Composer/yarn.lock @@ -1972,6 +1972,13 @@ dependencies: regenerator-runtime "^0.13.2" +"@babel/runtime@^7.2.0", "@babel/runtime@^7.8.4": + version "7.8.4" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.8.4.tgz#d79f5a2040f7caa24d53e563aad49cbc05581308" + integrity sha512-neAp3zt80trRVBI1x0azq6c57aNBqYZH8KhMm3TaB7wEI5Q4A2SHfBHE8w9gOhI/lrqxtEbXZgQIrHP+wvSGwQ== + dependencies: + regenerator-runtime "^0.13.2" + "@babel/runtime@^7.3.1", "@babel/runtime@^7.3.4": version "7.4.2" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.4.2.tgz#f5ab6897320f16decd855eed70b705908a313fe8" @@ -2014,13 +2021,6 @@ dependencies: regenerator-runtime "^0.13.2" -"@babel/runtime@^7.8.4": - version "7.8.4" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.8.4.tgz#d79f5a2040f7caa24d53e563aad49cbc05581308" - integrity sha512-neAp3zt80trRVBI1x0azq6c57aNBqYZH8KhMm3TaB7wEI5Q4A2SHfBHE8w9gOhI/lrqxtEbXZgQIrHP+wvSGwQ== - dependencies: - regenerator-runtime "^0.13.2" - "@babel/template@^7.0.0", "@babel/template@^7.1.0", "@babel/template@^7.2.2", "@babel/template@^7.4.0": version "7.4.0" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.4.0.tgz#12474e9c077bae585c5d835a95c0b0b790c25c8b" @@ -8554,7 +8554,7 @@ findup-sync@3.0.0: findup-sync@^2.0.0: version "2.0.0" - resolved "https://registry.npm.taobao.org/findup-sync/download/findup-sync-2.0.0.tgz#9326b1488c22d1a6088650a86901b2d9a90a2cbc" + resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-2.0.0.tgz#9326b1488c22d1a6088650a86901b2d9a90a2cbc" integrity sha1-kyaxSIwi0aYIhlCoaQGy2akKLLw= dependencies: detect-file "^1.0.0" @@ -8869,6 +8869,11 @@ get-caller-file@^2.0.1: resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== +get-node-dimensions@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/get-node-dimensions/-/get-node-dimensions-1.2.1.tgz#fb7b4bb57060fb4247dd51c9d690dfbec56b0823" + integrity sha512-2MSPMu7S1iOTL+BOa6K1S62hB2zUAYNF/lV0gSVlOaacd087lc6nR1H1r0e3B1CerTo+RceOmi1iJW+vp21xcQ== + get-own-enumerable-property-symbols@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.0.tgz#b877b49a5c16aefac3655f2ed2ea5b684df8d203" @@ -9561,7 +9566,7 @@ https-browserify@^1.0.0: resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM= -https-proxy-agent@^2.2.1: +https-proxy-agent@^2.2.1, https-proxy-agent@^2.2.3: version "2.2.4" resolved "https://botbuilder.myget.org/F/botbuilder-declarative/npm/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz#4ee7a737abd92678a293d9b34a1af4d0d08c787b" integrity sha1-TuenN6vZJniik9mzShr00NCMeHs= @@ -14474,6 +14479,16 @@ react-lifecycles-compat@^3.0.4: resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA== +react-measure@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/react-measure/-/react-measure-2.3.0.tgz#75835d39abec9ae13517f35a819c160997a7a44e" + integrity sha512-dwAvmiOeblj5Dvpnk8Jm7Q8B4THF/f1l1HtKVi0XDecsG6LXwGvzV5R1H32kq3TW6RW64OAf5aoQxpIgLa4z8A== + dependencies: + "@babel/runtime" "^7.2.0" + get-node-dimensions "^1.2.1" + prop-types "^15.6.2" + resize-observer-polyfill "^1.5.0" + react-testing-library@^6.0.1: version "6.0.2" resolved "https://registry.yarnpkg.com/react-testing-library/-/react-testing-library-6.0.2.tgz#afd7ddaa174e21cf672605e4e4f6f8156c4c9ef9" @@ -14622,8 +14637,8 @@ realpath-native@^1.1.0: reconnecting-websocket@^3.2.2: version "3.2.2" - resolved "https://registry.npm.taobao.org/reconnecting-websocket/download/reconnecting-websocket-3.2.2.tgz#8097514e926e9855e03c39e76efa2e3d1f371bee" - integrity sha1-gJdRTpJumFXgPDnnbvouPR83G+4= + resolved "https://registry.yarnpkg.com/reconnecting-websocket/-/reconnecting-websocket-3.2.2.tgz#8097514e926e9855e03c39e76efa2e3d1f371bee" + integrity sha512-SWSfoXiaHVOqXuPWFgGWeUxKnb5HIY7I/Fh5C/hy4wUOgeOh7YIMXEiv5/eHBlNs4tNzCrO5YDR9AH62NWle0Q== recursive-readdir@2.2.2: version "2.2.2" @@ -14834,13 +14849,13 @@ replace-ext@1.0.0: integrity sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs= request-light@^0.2.2: - version "0.2.4" - resolved "https://registry.npm.taobao.org/request-light/download/request-light-0.2.4.tgz#3cea29c126682e6bcadf7915353322eeba01a755" - integrity sha1-POopwSZoLmvK33kVNTMi7roBp1U= + version "0.2.5" + resolved "https://registry.yarnpkg.com/request-light/-/request-light-0.2.5.tgz#38a3da7b2e56f7af8cbba57e8a94930ee2380746" + integrity sha512-eBEh+GzJAftUnex6tcL6eV2JCifY0+sZMIUpUPOVXbs2nV5hla4ZMmO3icYKGuGVuQ2zHE9evh4OrRcH4iyYYw== dependencies: http-proxy-agent "^2.1.0" - https-proxy-agent "^2.2.1" - vscode-nls "^4.0.0" + https-proxy-agent "^2.2.3" + vscode-nls "^4.1.1" request-progress@3.0.0: version "3.0.0" @@ -14926,6 +14941,11 @@ requires-port@^1.0.0: resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8= +resize-observer-polyfill@^1.5.0: + version "1.5.1" + resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464" + integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg== + resolve-cwd@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a" @@ -17102,7 +17122,7 @@ vscode-languageserver@^5.3.0-next: vscode-languageserver-protocol "^3.15.0-next.8" vscode-textbuffer "^1.0.0" -vscode-nls@^4.0.0, vscode-nls@^4.1.1: +vscode-nls@^4.1.1: version "4.1.1" resolved "https://botbuilder.myget.org/F/botbuilder-declarative/npm/vscode-nls/-/vscode-nls-4.1.1.tgz#f9916b64e4947b20322defb1e676a495861f133c" integrity sha1-+ZFrZOSUeyAyLe+x5naklYYfEzw= From 998219ad4e8600393537f42c50356f5afd59eef7 Mon Sep 17 00:00:00 2001 From: Lu Han <32191031+luhan2017@users.noreply.github.com> Date: Tue, 3 Mar 2020 06:57:53 +0800 Subject: [PATCH 13/40] feat: Update package and schema to 200216 (#1997) * Update package and sample bot * Update package to 200216 * Update sdk.schema * refactor: Migrate lubuild to bf cli tool chain (#1957) * remove lubuild * update the bf-lu * update the version * update the version * add to log to builder * update the name * upgrade lg-parser/lgIndexer/lg-lsp-editor from @{} to ${} * Update to UseLuisSettings * replace @{} to ${} * add environment field into setting * update environment default value * add luis config adaptor * Update interuption and qna sample * chore: remove 'InitProperty' type in Composer (#2096) * disable InitProperty entry * remove InitProperty definition * fix lgPatterns: @ -> $ (#2097) * Fix action test case failure * update to the latest package * Update main.yml * Update main.yml * Update main.yml * Removed the BotProject\CSharp * update test case from @{} to ${} * merge remote seperate the new recognizer set sample * update test case Co-authored-by: leileizhang Co-authored-by: Zhixiang Zhan Co-authored-by: VanyLaw <835493129@qq.com> Co-authored-by: zeye <2295905420@qq.com> --- .github/workflows/main.yml | 6 +- BotProject/CSharp/.dockerignore | 2 - BotProject/CSharp/.gitignore | 9 - BotProject/CSharp/BotManager.cs | 229 - BotProject/CSharp/BotProject.csproj | 46 - BotProject/CSharp/BotProject.ruleset | 199 - BotProject/CSharp/BotProject.sln | 31 - BotProject/CSharp/ComposerBot.cs | 63 - .../CSharp/ComposerDialogs/Main/Main.dialog | 17 - .../CSharp/ComposerDialogs/common/common.lg | 2 - .../CSharp/Controllers/BotAdminController.cs | 41 - .../CSharp/Controllers/BotController.cs | 38 - .../new-rg-parameters.json | 42 - .../preexisting-rg-parameters.json | 39 - .../template-with-new-rg.json | 183 - .../template-with-preexisting-rg.json | 154 - BotProject/CSharp/IBotManager.cs | 17 - BotProject/CSharp/LuisConfig.cs | 36 - BotProject/CSharp/LuisCustomConfig.cs | 12 - BotProject/CSharp/LuisKey.cs | 15 - BotProject/CSharp/NuGet.Config | 7 - BotProject/CSharp/Program.cs | 42 - .../CSharp/Properties/launchSettings.json | 27 - BotProject/CSharp/README.md | 55 - BotProject/CSharp/Schemas/sdk.schema | 6998 ----------------- BotProject/CSharp/Scripts/create.ps1 | 185 - BotProject/CSharp/Scripts/deploy.ps1 | 137 - BotProject/CSharp/Startup.cs | 63 - BotProject/CSharp/Tests/ActionsTests.cs | 235 - .../Tests/ControllingConversationTests.cs | 116 - BotProject/CSharp/Tests/InputsTests.cs | 148 - BotProject/CSharp/Tests/MessageTests.cs | 101 - BotProject/CSharp/Tests/Tests.csproj | 27 - BotProject/CSharp/Tests/ToDoBotTests.cs | 101 - .../CSharp/appsettings.Development.json | 9 - BotProject/CSharp/appsettings.json | 5 - BotProject/Templates/CSharp/BotProject.csproj | 26 +- BotProject/Templates/CSharp/ComposerBot.cs | 17 +- .../Templates/CSharp/LuisConfigAdaptor.cs | 23 + BotProject/Templates/CSharp/Program.cs | 49 +- BotProject/Templates/CSharp/README.md | 39 +- .../Templates/CSharp/Schemas/sdk.schema | 52 +- BotProject/Templates/CSharp/Startup.cs | 34 +- .../Templates/CSharp/Tests/ActionsTests.cs | 14 +- .../Tests/ControllingConversationTests.cs | 13 +- .../Templates/CSharp/Tests/InputsTests.cs | 11 +- .../Templates/CSharp/Tests/MessageTests.cs | 11 +- .../Templates/CSharp/Tests/ToDoBotTests.cs | 11 +- .../integration/NotificationPage.spec.ts | 2 +- .../__tests__/store/reducer/reducer.test.js | 2 +- Composer/packages/client/package.json | 6 +- .../obiformeditor/demo/src/editorschema.json | 3 - .../extensions/obiformeditor/package.json | 2 +- .../demo/src/samples/todo/AddToDo.json | 2 +- .../demo/src/stories/VisualSDKDemo.js | 1 - .../visual-designer/src/schema/uischema.tsx | 4 - .../lib/code-editor/src/languages/lg.ts | 25 +- Composer/packages/lib/indexers/package.json | 6 +- .../lgUtils/models/LgTemplateRef.test.ts | 14 +- .../parsers/parseLgTemplateRef.test.ts | 12 +- Composer/packages/lib/shared/src/appschema.ts | 27 - Composer/packages/lib/shared/src/labelMap.ts | 3 - .../shared/src/lgUtils/parsers/lgPatterns.ts | 2 +- .../buildLgTemplateRefString.ts | 2 +- .../packages/lib/shared/src/types/schema.ts | 1 - Composer/packages/lib/shared/src/viewUtils.ts | 1 - .../bot1/ComposerDialogs/Main.dialog | 15 +- .../ComposerDialogs/Actions/Actions.dialog | 14 +- .../ComposerDialogs/Actions/Actions.lg | 4 +- .../EditActions/EditActions.dialog | 9 +- .../EditActions/EditActions.lg | 2 +- .../EditArray/EditArray.dialog | 31 +- .../EmitAnEvent/EmitAnEvent.dialog | 4 +- .../EndDialog/EndDialog.dialog | 6 +- .../ComposerDialogs/EndDialog/EndDialog.lg | 2 +- .../TellJoke/EndDialog.TellJoke.dialog | 5 +- .../ComposerDialogs/EndTurn/EndTurn.dialog | 4 +- .../HttpRequest/HttpRequest.dialog | 6 +- .../HttpRequest/HttpRequest.lg | 6 +- .../IfCondition/IfCondition.dialog | 2 +- .../IfCondition/IfCondition.lg | 2 +- .../ComposerDialogs/Main/Main.dialog | 19 +- .../ComposerDialogs/Main/Main.lg | 2 +- .../FortuneTellerDialog.dialog | 6 +- .../ReplaceDialog/Replace.dialog | 6 +- .../ComposerDialogs/ReplaceDialog/Replace.lg | 2 +- .../TellJokeDialog/TellJokeDialog.dialog | 4 +- .../SwitchCondition/SwitchCondition.dialog | 8 +- .../SwitchCondition/SwitchCondition.lg | 2 +- .../TraceAndLog/TraceAndLog.dialog | 2 +- .../ComposerDialogs/common/common.lg | 15 +- .../AttachmentInput/AttachmentInput.dialog | 2 +- .../AttachmentInput/AttachmentInput.lg | 2 +- .../ChoiceInput/ChoiceInput.dialog | 2 +- .../ChoiceInput/ChoiceInput.lg | 2 +- .../ConfirmInput/ConfirmInput.dialog | 2 +- .../ConfirmInput/ConfirmInput.lg | 2 +- .../DateTimeInput/DateTimeInput.dialog | 2 +- .../DateTimeInput/DateTimeInput.lg | 2 +- .../ComposerDialogs/Main/Main.dialog | 14 +- .../ComposerDialogs/Main/Main.lg | 4 +- .../NumberInput/NumberInput.dialog | 6 +- .../NumberInput/NumberInput.lg | 6 +- .../OAuthInput/OAuthInput.dialog | 2 +- .../ComposerDialogs/OAuthInput/OAuthInput.lg | 2 +- .../TextInput/TextInput.dialog | 2 +- .../ComposerDialogs/TextInput/TextInput.lg | 2 +- .../ComposerDialogs/common/common.lg | 15 +- .../ForeachPageStep/ForeachPageStep.dialog | 27 +- .../ForeachPageStep/ForeachPageStep.lg | 2 +- .../ForeachStep/ForeachStep.dialog | 27 +- .../ForeachStep/ForeachStep.lg | 2 +- .../IfCondition/IfCondition.dialog | 4 +- .../IfCondition/IfCondition.lg | 4 +- .../ComposerDialogs/Main/Main.dialog | 11 +- .../SwitchCondition/SwitchCondition.dialog | 8 +- .../SwitchCondition/SwitchCondition.lg | 2 +- .../ComposerDialogs/common/common.lg | 2 +- .../EchoBot/ComposerDialogs/Main/Main.dialog | 2 +- .../EchoBot/ComposerDialogs/Main/Main.lg | 2 +- .../GetProfile/GetProfile.dialog | 26 +- .../ComposerDialogs/GetProfile/GetProfile.lg | 6 +- .../ComposerDialogs/Main/Main.dialog | 14 +- .../ComposerDialogs/Main/Main.lg | 6 +- .../ComposerDialogs/common/common.lg | 8 +- .../ComposerDialogs/Main/Main.dialog | 12 +- .../ComposerDialogs/Main/Main.dialog | 22 +- .../ComposerDialogs/Main/Main.lg | 18 +- .../ComposerDialogs/common/adaptiveCard.json | 6 +- .../ComposerDialogs/common/common.lg | 40 +- .../IfElseCondition/IfElseCondition.dialog | 4 +- .../IfElseCondition/IfElseCondition.lg | 2 +- .../LGComposition/LGComposition.dialog | 2 +- .../LGComposition/LGComposition.lg | 2 +- .../LGWithParam/LGWithParam.dialog | 2 +- .../LGWithParam/LGWithParam.lg | 2 +- .../ComposerDialogs/Main/Main.dialog | 4 +- .../ComposerDialogs/Main/Main.lg | 2 +- .../MultiLineText/MultiLineText.dialog | 2 +- .../MultiLineText/MultiLineText.lg | 2 +- .../SimpleText/SimpleText.dialog | 2 +- .../ComposerDialogs/SimpleText/SimpleText.lg | 2 +- .../StructuredLG/StructuredLG.dialog | 2 +- .../StructuredLG/StructuredLG.lg | 2 +- .../SwitchCondition/SwitchCondition.dialog | 2 +- .../SwitchCondition/SwitchCondition.lg | 2 +- .../TextWithMemory/TextWithMemory.dialog | 4 +- .../TextWithMemory/TextWithMemory.lg | 2 +- .../ComposerDialogs/common/common.lg | 14 +- .../ComposerDialogs/Main/Main.dialog | 82 +- .../ComposerDialogs/Main/Main.lg | 30 +- .../ComposerDialogs/common/common.lg | 40 +- .../ComposerDialogs/AddToDo/AddToDo.dialog | 48 + .../ComposerDialogs/AddToDo/AddToDo.lg | 4 + .../ComposerDialogs/AddToDo/AddToDo.lu | 0 .../ClearToDos/ClearToDos.dialog | 54 + .../ComposerDialogs/ClearToDos/ClearToDos.lg | 7 + .../ComposerDialogs/ClearToDos/ClearToDos.lu | 0 .../DeleteToDo/DeleteToDo.dialog | 67 + .../ComposerDialogs/DeleteToDo/DeleteToDo.lg | 10 + .../ComposerDialogs/DeleteToDo/DeleteToDo.lu | 0 .../ComposerDialogs/Main/Main.dialog | 202 + .../ComposerDialogs/Main/Main.lg | 10 + .../ComposerDialogs/Main/Main.lu | 0 .../ShowToDos/ShowToDos.dialog | 47 + .../ComposerDialogs/ShowToDos/ShowToDos.lg | 8 + .../ComposerDialogs/ShowToDos/ShowToDos.lu | 0 .../ComposerDialogs/common/common.lg | 39 + .../ComposerDialogs/AddToDo/AddToDo.dialog | 32 +- .../ComposerDialogs/AddToDo/AddToDo.lg | 2 +- .../ClearToDos/ClearToDos.dialog | 4 +- .../DeleteToDo/DeleteToDo.dialog | 15 +- .../ComposerDialogs/DeleteToDo/DeleteToDo.lg | 4 +- .../ComposerDialogs/Main/Main.dialog | 277 +- .../TodoSample/ComposerDialogs/Main/Main.lg | 2 +- .../ShowToDos/ShowToDos.dialog | 4 +- .../ComposerDialogs/ShowToDos/ShowToDos.lg | 2 +- .../ComposerDialogs/common/common.lg | 20 +- Composer/packages/server/package.json | 2 +- .../packages/server/schemas/editor.schema | 5 - Composer/packages/server/schemas/sdk.schema | 52 +- .../server/src/models/bot/luPublisher.ts | 74 +- .../models/connector/csharpBotConnector.ts | 4 - Composer/packages/server/src/types/bf-lu.d.ts | 35 + .../language-generation/__tests__/app.test.ts | 2 +- .../language-generation/package.json | 4 +- .../language-generation/src/LGServer.ts | 2 +- .../language-understanding/package.json | 2 +- Composer/yarn.lock | 292 +- 189 files changed, 1252 insertions(+), 10638 deletions(-) delete mode 100644 BotProject/CSharp/.dockerignore delete mode 100644 BotProject/CSharp/.gitignore delete mode 100644 BotProject/CSharp/BotManager.cs delete mode 100644 BotProject/CSharp/BotProject.csproj delete mode 100644 BotProject/CSharp/BotProject.ruleset delete mode 100644 BotProject/CSharp/BotProject.sln delete mode 100644 BotProject/CSharp/ComposerBot.cs delete mode 100644 BotProject/CSharp/ComposerDialogs/Main/Main.dialog delete mode 100644 BotProject/CSharp/ComposerDialogs/common/common.lg delete mode 100644 BotProject/CSharp/Controllers/BotAdminController.cs delete mode 100644 BotProject/CSharp/Controllers/BotController.cs delete mode 100644 BotProject/CSharp/DeploymentTemplates/new-rg-parameters.json delete mode 100644 BotProject/CSharp/DeploymentTemplates/preexisting-rg-parameters.json delete mode 100644 BotProject/CSharp/DeploymentTemplates/template-with-new-rg.json delete mode 100644 BotProject/CSharp/DeploymentTemplates/template-with-preexisting-rg.json delete mode 100644 BotProject/CSharp/IBotManager.cs delete mode 100644 BotProject/CSharp/LuisConfig.cs delete mode 100644 BotProject/CSharp/LuisCustomConfig.cs delete mode 100644 BotProject/CSharp/LuisKey.cs delete mode 100644 BotProject/CSharp/NuGet.Config delete mode 100644 BotProject/CSharp/Program.cs delete mode 100644 BotProject/CSharp/Properties/launchSettings.json delete mode 100644 BotProject/CSharp/README.md delete mode 100644 BotProject/CSharp/Schemas/sdk.schema delete mode 100644 BotProject/CSharp/Scripts/create.ps1 delete mode 100644 BotProject/CSharp/Scripts/deploy.ps1 delete mode 100644 BotProject/CSharp/Startup.cs delete mode 100644 BotProject/CSharp/Tests/ActionsTests.cs delete mode 100644 BotProject/CSharp/Tests/ControllingConversationTests.cs delete mode 100644 BotProject/CSharp/Tests/InputsTests.cs delete mode 100644 BotProject/CSharp/Tests/MessageTests.cs delete mode 100644 BotProject/CSharp/Tests/Tests.csproj delete mode 100644 BotProject/CSharp/Tests/ToDoBotTests.cs delete mode 100644 BotProject/CSharp/appsettings.Development.json delete mode 100644 BotProject/CSharp/appsettings.json create mode 100644 BotProject/Templates/CSharp/LuisConfigAdaptor.cs create mode 100644 Composer/packages/server/assets/projects/TodoRecognizerSetSample/ComposerDialogs/AddToDo/AddToDo.dialog create mode 100644 Composer/packages/server/assets/projects/TodoRecognizerSetSample/ComposerDialogs/AddToDo/AddToDo.lg rename BotProject/CSharp/ComposerDialogs/Main/Main.lu => Composer/packages/server/assets/projects/TodoRecognizerSetSample/ComposerDialogs/AddToDo/AddToDo.lu (100%) create mode 100644 Composer/packages/server/assets/projects/TodoRecognizerSetSample/ComposerDialogs/ClearToDos/ClearToDos.dialog create mode 100644 Composer/packages/server/assets/projects/TodoRecognizerSetSample/ComposerDialogs/ClearToDos/ClearToDos.lg rename BotProject/CSharp/packages/packages.json => Composer/packages/server/assets/projects/TodoRecognizerSetSample/ComposerDialogs/ClearToDos/ClearToDos.lu (100%) create mode 100644 Composer/packages/server/assets/projects/TodoRecognizerSetSample/ComposerDialogs/DeleteToDo/DeleteToDo.dialog create mode 100644 Composer/packages/server/assets/projects/TodoRecognizerSetSample/ComposerDialogs/DeleteToDo/DeleteToDo.lg create mode 100644 Composer/packages/server/assets/projects/TodoRecognizerSetSample/ComposerDialogs/DeleteToDo/DeleteToDo.lu create mode 100644 Composer/packages/server/assets/projects/TodoRecognizerSetSample/ComposerDialogs/Main/Main.dialog create mode 100644 Composer/packages/server/assets/projects/TodoRecognizerSetSample/ComposerDialogs/Main/Main.lg create mode 100644 Composer/packages/server/assets/projects/TodoRecognizerSetSample/ComposerDialogs/Main/Main.lu create mode 100644 Composer/packages/server/assets/projects/TodoRecognizerSetSample/ComposerDialogs/ShowToDos/ShowToDos.dialog create mode 100644 Composer/packages/server/assets/projects/TodoRecognizerSetSample/ComposerDialogs/ShowToDos/ShowToDos.lg create mode 100644 Composer/packages/server/assets/projects/TodoRecognizerSetSample/ComposerDialogs/ShowToDos/ShowToDos.lu create mode 100644 Composer/packages/server/assets/projects/TodoRecognizerSetSample/ComposerDialogs/common/common.lg create mode 100644 Composer/packages/server/src/types/bf-lu.d.ts diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b3de1d17bf..e2f664b4f2 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -60,13 +60,13 @@ jobs: - name: Set Dotnet Version uses: actions/setup-dotnet@v1 with: - dotnet-version: "3.0.100" # SDK Version to use. + dotnet-version: "3.1.102" # SDK Version to use. - name: dotnet build run: dotnet build - working-directory: BotProject/CSharp + working-directory: BotProject/Templates/CSharp - name: dotnet test run: dotnet test - working-directory: BotProject/CSharp + working-directory: BotProject/Templates/CSharp docker-build: name: Docker Build diff --git a/BotProject/CSharp/.dockerignore b/BotProject/CSharp/.dockerignore deleted file mode 100644 index 31db54001a..0000000000 --- a/BotProject/CSharp/.dockerignore +++ /dev/null @@ -1,2 +0,0 @@ -**/bin -**/obj \ No newline at end of file diff --git a/BotProject/CSharp/.gitignore b/BotProject/CSharp/.gitignore deleted file mode 100644 index 1557a3a3b1..0000000000 --- a/BotProject/CSharp/.gitignore +++ /dev/null @@ -1,9 +0,0 @@ -# NuGet Packages -*.nupkg -# The packages folder can be ignored because of Package Restore -**/[Pp]ackages/* -# except build/, which is used as an MSBuild target. -!**/[Pp]ackages/build/ - -!Properties/launchSettings.json -!packages/packages.json diff --git a/BotProject/CSharp/BotManager.cs b/BotProject/CSharp/BotManager.cs deleted file mode 100644 index 89b76a332b..0000000000 --- a/BotProject/CSharp/BotManager.cs +++ /dev/null @@ -1,229 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; -using System.Collections.Generic; -using System.IO; -using System.IO.Compression; -using System.Linq; -using System.Reflection; -using System.Threading.Tasks; -using Microsoft.Bot.Builder.BotFramework; -using Microsoft.Bot.Builder.Dialogs.Adaptive; -using Microsoft.Bot.Builder.Dialogs.Debugging; -using Microsoft.Bot.Builder.Dialogs.Declarative; -using Microsoft.Bot.Builder.Dialogs.Declarative.Resources; -using Microsoft.Bot.Builder.Integration.AspNet.Core; -using Microsoft.Bot.Connector.Authentication; -using Microsoft.Extensions.Configuration; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - -namespace Microsoft.Bot.Builder.ComposerBot.Json -{ - public class BotManager : IBotManager - { - private static readonly object Locker = new object(); - - public BotManager(IConfiguration config) - { - Config = config; - - // init work dir - WorkDir = ConvertPath("tmp"); - EnsureDirExists(WorkDir); - CleanDir(WorkDir); - - // set init bot - var bot = Config.GetSection("bot").Get(); - SetCurrent(bot); - } - - public IConfiguration Config { get; } - - public IBotFrameworkHttpAdapter CurrentAdapter { get; set; } - - public IBot CurrentBot { get; set; } - - private string WorkDir { get; } - - public static string ConvertPath(string relativePath) - { - var curDir = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location); - return Path.Combine(curDir, relativePath); - } - - public static void EnsureDirExists(string dirPath) - { - var dirInfo = new DirectoryInfo(dirPath); - if (!dirInfo.Exists) - { - dirInfo.Create(); - } - } - - public static void CleanDir(string dirPath) - { - var dir = new DirectoryInfo(dirPath); - dir.GetFiles().ToList().ForEach(f => f.Delete()); - dir.GetDirectories().ToList().ForEach(d => d.Delete(true)); - } - - public void SetCurrent(string botDir) - { - IStorage storage = new MemoryStorage(); - var userState = new UserState(storage); - var conversationState = new ConversationState(storage); - var inspectionState = new InspectionState(storage); - - // manage all bot resources - var resourceExplorer = new ResourceExplorer().AddFolder(botDir); - - var adapter = new BotFrameworkHttpAdapter(new ConfigurationCredentialProvider(Config)); - - var credentials = new MicrosoftAppCredentials(Config["MicrosoftAppId"], Config["MicrosoftAppPassword"]); - - adapter - .UseStorage(storage) - .UseState(userState, conversationState) - .UseAdaptiveDialogs() - .UseResourceExplorer(resourceExplorer) - .UseLanguageGeneration(resourceExplorer, "common.lg") - .Use(new RegisterClassMiddleware(Config)) - .Use(new InspectionMiddleware(inspectionState, userState, conversationState, credentials)); - - adapter.OnTurnError = async (turnContext, exception) => - { - await turnContext.SendActivityAsync(exception.Message).ConfigureAwait(false); - - await conversationState.ClearStateAsync(turnContext).ConfigureAwait(false); - await conversationState.SaveChangesAsync(turnContext).ConfigureAwait(false); - }; - CurrentAdapter = adapter; - - CurrentBot = new ComposerBot("Main.dialog", conversationState, userState, resourceExplorer, DebugSupport.SourceMap); - } - - public void SetCurrent(Stream fileStream, string endpointKey = null, string appPwd = null) - { - lock (Locker) - { - // download file as tmp.zip - var downloadPath = SaveFile(fileStream, "tmp.zip").GetAwaiter().GetResult(); - - // extract to bot folder - var extractPath = ExtractFile(downloadPath, GenNewBotDir()); - - RetrieveSettingsFile(extractPath, endpointKey, appPwd); - SetCurrent(extractPath); - } - } - - public void RetrieveSettingsFile(string extractPath, string endpointKey, string appPwd) - { - var settingsPaths = Directory.GetFiles(extractPath, "appsettings.json", SearchOption.AllDirectories); - if (settingsPaths.Length == 0) - { - return; - } - - var settingsPath = settingsPaths.FirstOrDefault(); - - var settings = JsonConvert.DeserializeObject>(File.ReadAllText(settingsPath)); - - foreach (var pair in settings) - { - if (pair.Value is JObject) - { - foreach (var token in pair.Value as JObject) - { - string subkey = token.Key; - JToken subvalue = token.Value; - this.Config[$"{pair.Key}:{subkey}"] = subvalue.Value(); - } - } - else - { - this.Config[pair.Key.ToString()] = pair.Value.ToString(); - } - } - - if (!string.IsNullOrEmpty(endpointKey)) - { - var luconfigFile = JsonConvert.DeserializeObject(settings["luis"].ToString()); - AddLuisConfig(extractPath, luconfigFile, endpointKey); - } - - if (!string.IsNullOrEmpty(appPwd)) - { - AddOAuthConfig(appPwd); - } - } - - public void AddLuisConfig(string extractPath, LuisConfig luisConfig, string endpointKey) - { - var settingsName = $"luis.settings.{luisConfig.Environment}.{luisConfig.AuthoringRegion}.json"; - var luisEndpoint = $"https://{luisConfig.AuthoringRegion}.api.cognitive.microsoft.com"; - this.Config["luis:endpoint"] = luisEndpoint; - - // No luis settings - var luisPaths = Directory.GetFiles(extractPath, settingsName, SearchOption.AllDirectories); - if (luisPaths.Length == 0) - { - return; - } - - var luisPath = luisPaths[0]; - - var luisConfigJson = JsonConvert.DeserializeObject(File.ReadAllText(luisPath)); - - luisConfigJson.Luis.Add("endpointKey", endpointKey); - - foreach (var item in luisConfigJson.Luis) - { - this.Config[$"luis:{item.Key}"] = item.Value; - } - } - - private void AddOAuthConfig(string appPwd) - { - if (string.IsNullOrEmpty(appPwd)) - { - this.Config["MicrosoftAppPassword"] = string.Empty; - } - else - { - this.Config["MicrosoftAppPassword"] = appPwd; - } - } - - private string GenNewBotDir() - { - return System.Guid.NewGuid().ToString("N"); - } - - private async Task SaveFile(Stream fileStream, string fileName) - { - EnsureDirExists(WorkDir); - - var filePath = Path.Combine(WorkDir, fileName); - using (var outStream = new FileStream(filePath, FileMode.Create)) - { - await fileStream.CopyToAsync(outStream); - } - - return filePath; - } - - // Extract file to a dir - private string ExtractFile(string filePath, string dstDirPath) - { - EnsureDirExists(WorkDir); - - var finalDstPath = Path.Combine(WorkDir, dstDirPath); - - ZipFile.ExtractToDirectory(filePath, finalDstPath); - return finalDstPath; - } - } -} \ No newline at end of file diff --git a/BotProject/CSharp/BotProject.csproj b/BotProject/CSharp/BotProject.csproj deleted file mode 100644 index 4f0d7a31df..0000000000 --- a/BotProject/CSharp/BotProject.csproj +++ /dev/null @@ -1,46 +0,0 @@ - - - - netcoreapp2.1 - ade3d9c2-1633-4922-89e5-a4a50ccb3bc8 - - - - BotProject.ruleset - - - - BotProject.ruleset - - - - - - - - - - - - Always - - - - - - - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers - - - - diff --git a/BotProject/CSharp/BotProject.ruleset b/BotProject/CSharp/BotProject.ruleset deleted file mode 100644 index 845cc367f1..0000000000 --- a/BotProject/CSharp/BotProject.ruleset +++ /dev/null @@ -1,199 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/BotProject/CSharp/BotProject.sln b/BotProject/CSharp/BotProject.sln deleted file mode 100644 index db73c02cd8..0000000000 --- a/BotProject/CSharp/BotProject.sln +++ /dev/null @@ -1,31 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.28307.136 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BotProject", "BotProject.csproj", "{80ACF5BE-4A04-46F8-A83E-530FB21948D5}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests", "Tests\Tests.csproj", "{5AFADEA2-A18F-46DF-8080-2CA418880318}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {80ACF5BE-4A04-46F8-A83E-530FB21948D5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {80ACF5BE-4A04-46F8-A83E-530FB21948D5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {80ACF5BE-4A04-46F8-A83E-530FB21948D5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {80ACF5BE-4A04-46F8-A83E-530FB21948D5}.Release|Any CPU.Build.0 = Release|Any CPU - {5AFADEA2-A18F-46DF-8080-2CA418880318}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5AFADEA2-A18F-46DF-8080-2CA418880318}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5AFADEA2-A18F-46DF-8080-2CA418880318}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5AFADEA2-A18F-46DF-8080-2CA418880318}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {B13FC467-1A63-4C8F-A29E-43B2D8B79B17} - EndGlobalSection -EndGlobal diff --git a/BotProject/CSharp/ComposerBot.cs b/BotProject/CSharp/ComposerBot.cs deleted file mode 100644 index f8d08a5977..0000000000 --- a/BotProject/CSharp/ComposerBot.cs +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Bot.Builder.AI.QnA; -using Microsoft.Bot.Builder.Dialogs; -using Microsoft.Bot.Builder.Dialogs.Adaptive; -using Microsoft.Bot.Builder.Dialogs.Debugging; -using Microsoft.Bot.Builder.Dialogs.Declarative; -using Microsoft.Bot.Builder.Dialogs.Declarative.Resources; - -namespace Microsoft.Bot.Builder.ComposerBot.Json -{ - public class ComposerBot : ActivityHandler - { - private AdaptiveDialog rootDialog; - private readonly ResourceExplorer resourceExplorer; - private readonly UserState userState; - private DialogManager dialogManager; - private readonly ConversationState conversationState; - private readonly IStatePropertyAccessor dialogState; - private readonly ISourceMap sourceMap; - - public ComposerBot(string rootDialogFile, ConversationState conversationState, UserState userState, ResourceExplorer resourceExplorer, ISourceMap sourceMap) - { - this.conversationState = conversationState; - this.userState = userState; - this.dialogState = conversationState.CreateProperty("DialogState"); - this.sourceMap = sourceMap; - this.resourceExplorer = resourceExplorer; - this.RootDialogFile = rootDialogFile; - DeclarativeTypeLoader.AddComponent(new QnAMakerComponentRegistration()); - - // auto reload dialogs when file changes - this.resourceExplorer.Changed += (resources) => - { - if (resources.Any(resource => resource.Id == ".dialog")) - { - Task.Run(() => this.LoadRootDialogAsync()); - } - }; - LoadRootDialogAsync(); - } - - private string RootDialogFile { get; set; } - - public override async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default(CancellationToken)) - { - await this.dialogManager.OnTurnAsync(turnContext, cancellationToken: cancellationToken); - await this.conversationState.SaveChangesAsync(turnContext, false, cancellationToken); - await this.userState.SaveChangesAsync(turnContext, false, cancellationToken); - } - - private void LoadRootDialogAsync() - { - var rootFile = resourceExplorer.GetResource(RootDialogFile); - rootDialog = DeclarativeTypeLoader.Load(rootFile, resourceExplorer, sourceMap); - this.dialogManager = new DialogManager(rootDialog); - } - } -} \ No newline at end of file diff --git a/BotProject/CSharp/ComposerDialogs/Main/Main.dialog b/BotProject/CSharp/ComposerDialogs/Main/Main.dialog deleted file mode 100644 index cb675db8c8..0000000000 --- a/BotProject/CSharp/ComposerDialogs/Main/Main.dialog +++ /dev/null @@ -1,17 +0,0 @@ -{ - "$type": "Microsoft.AdaptiveDialog", - "autoEndDialog": true, - "generator": "common.lg", - "$schema": "https://raw.githubusercontent.com/microsoft/BotFramework-Composer/stable/Composer/packages/server/schemas/sdk.schema", - "triggers": [ - { - "$type": "Microsoft.OnBeginDialog", - "actions": [ - { - "$type": "Microsoft.SendActivity", - "activity": "@{bfdactivity-003038()}" - } - ] - } - ] -} diff --git a/BotProject/CSharp/ComposerDialogs/common/common.lg b/BotProject/CSharp/ComposerDialogs/common/common.lg deleted file mode 100644 index 681faf19ad..0000000000 --- a/BotProject/CSharp/ComposerDialogs/common/common.lg +++ /dev/null @@ -1,2 +0,0 @@ -# bfdactivity-003038 -- This is a default bot. diff --git a/BotProject/CSharp/Controllers/BotAdminController.cs b/BotProject/CSharp/Controllers/BotAdminController.cs deleted file mode 100644 index 25a325ed38..0000000000 --- a/BotProject/CSharp/Controllers/BotAdminController.cs +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -// -// Generated with Bot Builder V4 SDK Template for Visual Studio EchoBot v4.3.0 - -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; - -namespace Microsoft.Bot.Builder.ComposerBot.Json -{ - [Route("api/admin")] - [ApiController] - public class BotAdminController : ControllerBase - { - private readonly BotManager botManager; - - public BotAdminController(BotManager botManager) - { - this.botManager = botManager; - } - - [HttpGet] - public IActionResult GetAsync() - { - return Ok(); - } - - [HttpPost] - public IActionResult PostAsync(IFormFile file, [FromForm]string endpointKey = null, [FromForm]string microsoftAppPassword = null) - { - if (file == null) - { - return BadRequest(); - } - - botManager.SetCurrent(file.OpenReadStream(), endpointKey, microsoftAppPassword); - - return Ok(); - } - } -} diff --git a/BotProject/CSharp/Controllers/BotController.cs b/BotProject/CSharp/Controllers/BotController.cs deleted file mode 100644 index dc1ad5c782..0000000000 --- a/BotProject/CSharp/Controllers/BotController.cs +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -// -// Generated with Bot Builder V4 SDK Template for Visual Studio EchoBot v4.3.0 - -using System.Threading.Tasks; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Bot.Builder; -using Microsoft.Bot.Builder.Dialogs.Debugging; -using Microsoft.Bot.Builder.Integration.AspNet.Core; - -namespace Microsoft.Bot.Builder.ComposerBot.Json -{ - // This ASP Controller is created to handle a request. Dependency Injection will provide the Adapter and IBot - // implementation at runtime. Multiple different IBot implementations running at different endpoints can be - // achieved by specifying a more specific type for the bot constructor argument. - [Route("api/messages")] - [ApiController] - public class BotController : ControllerBase - { - private readonly BotManager botManager; - - public BotController(BotManager botManager) - { - this.botManager = botManager; - } - - [HttpPost] - [HttpGet] - public async Task PostAsync() - { - // Delegate the processing of the HTTP POST to the adapter. - // The adapter will invoke the bot. - await botManager.CurrentAdapter.ProcessAsync(Request, Response, botManager.CurrentBot); - } - } -} diff --git a/BotProject/CSharp/DeploymentTemplates/new-rg-parameters.json b/BotProject/CSharp/DeploymentTemplates/new-rg-parameters.json deleted file mode 100644 index ead3390932..0000000000 --- a/BotProject/CSharp/DeploymentTemplates/new-rg-parameters.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", - "contentVersion": "1.0.0.0", - "parameters": { - "groupLocation": { - "value": "" - }, - "groupName": { - "value": "" - }, - "appId": { - "value": "" - }, - "appSecret": { - "value": "" - }, - "botId": { - "value": "" - }, - "botSku": { - "value": "" - }, - "newAppServicePlanName": { - "value": "" - }, - "newAppServicePlanSku": { - "value": { - "name": "S1", - "tier": "Standard", - "size": "S1", - "family": "S", - "capacity": 1 - } - }, - "newAppServicePlanLocation": { - "value": "" - }, - "newWebAppName": { - "value": "" - } - } -} \ No newline at end of file diff --git a/BotProject/CSharp/DeploymentTemplates/preexisting-rg-parameters.json b/BotProject/CSharp/DeploymentTemplates/preexisting-rg-parameters.json deleted file mode 100644 index b6f5114fcc..0000000000 --- a/BotProject/CSharp/DeploymentTemplates/preexisting-rg-parameters.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", - "contentVersion": "1.0.0.0", - "parameters": { - "appId": { - "value": "" - }, - "appSecret": { - "value": "" - }, - "botId": { - "value": "" - }, - "botSku": { - "value": "" - }, - "newAppServicePlanName": { - "value": "" - }, - "newAppServicePlanSku": { - "value": { - "name": "S1", - "tier": "Standard", - "size": "S1", - "family": "S", - "capacity": 1 - } - }, - "appServicePlanLocation": { - "value": "" - }, - "existingAppServicePlan": { - "value": "" - }, - "newWebAppName": { - "value": "" - } - } -} \ No newline at end of file diff --git a/BotProject/CSharp/DeploymentTemplates/template-with-new-rg.json b/BotProject/CSharp/DeploymentTemplates/template-with-new-rg.json deleted file mode 100644 index 06b8284158..0000000000 --- a/BotProject/CSharp/DeploymentTemplates/template-with-new-rg.json +++ /dev/null @@ -1,183 +0,0 @@ -{ - "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "parameters": { - "groupLocation": { - "type": "string", - "metadata": { - "description": "Specifies the location of the Resource Group." - } - }, - "groupName": { - "type": "string", - "metadata": { - "description": "Specifies the name of the Resource Group." - } - }, - "appId": { - "type": "string", - "metadata": { - "description": "Active Directory App ID, set as MicrosoftAppId in the Web App's Application Settings." - } - }, - "appSecret": { - "type": "string", - "metadata": { - "description": "Active Directory App Password, set as MicrosoftAppPassword in the Web App's Application Settings." - } - }, - "botId": { - "type": "string", - "metadata": { - "description": "The globally unique and immutable bot ID. Also used to configure the displayName of the bot, which is mutable." - } - }, - "botSku": { - "type": "string", - "metadata": { - "description": "The pricing tier of the Bot Service Registration. Acceptable values are F0 and S1." - } - }, - "newAppServicePlanName": { - "type": "string", - "metadata": { - "description": "The name of the App Service Plan." - } - }, - "newAppServicePlanSku": { - "type": "object", - "defaultValue": { - "name": "S1", - "tier": "Standard", - "size": "S1", - "family": "S", - "capacity": 1 - }, - "metadata": { - "description": "The SKU of the App Service Plan. Defaults to Standard values." - } - }, - "newAppServicePlanLocation": { - "type": "string", - "metadata": { - "description": "The location of the App Service Plan. Defaults to \"westus\"." - } - }, - "newWebAppName": { - "type": "string", - "defaultValue": "", - "metadata": { - "description": "The globally unique name of the Web App. Defaults to the value passed in for \"botId\"." - } - } - }, - "variables": { - "appServicePlanName": "[parameters('newAppServicePlanName')]", - "resourcesLocation": "[parameters('newAppServicePlanLocation')]", - "webAppName": "[if(empty(parameters('newWebAppName')), parameters('botId'), parameters('newWebAppName'))]", - "siteHost": "[concat(variables('webAppName'), '.azurewebsites.net')]", - "botEndpoint": "[concat('https://', variables('siteHost'), '/api/messages')]" - }, - "resources": [ - { - "name": "[parameters('groupName')]", - "type": "Microsoft.Resources/resourceGroups", - "apiVersion": "2018-05-01", - "location": "[parameters('groupLocation')]", - "properties": { - } - }, - { - "type": "Microsoft.Resources/deployments", - "apiVersion": "2018-05-01", - "name": "storageDeployment", - "resourceGroup": "[parameters('groupName')]", - "dependsOn": [ - "[resourceId('Microsoft.Resources/resourceGroups/', parameters('groupName'))]" - ], - "properties": { - "mode": "Incremental", - "template": { - "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "parameters": {}, - "variables": {}, - "resources": [ - { - "comments": "Create a new App Service Plan", - "type": "Microsoft.Web/serverfarms", - "name": "[variables('appServicePlanName')]", - "apiVersion": "2018-02-01", - "location": "[variables('resourcesLocation')]", - "sku": "[parameters('newAppServicePlanSku')]", - "properties": { - "name": "[variables('appServicePlanName')]" - } - }, - { - "comments": "Create a Web App using the new App Service Plan", - "type": "Microsoft.Web/sites", - "apiVersion": "2015-08-01", - "location": "[variables('resourcesLocation')]", - "kind": "app", - "dependsOn": [ - "[resourceId('Microsoft.Web/serverfarms/', variables('appServicePlanName'))]" - ], - "name": "[variables('webAppName')]", - "properties": { - "name": "[variables('webAppName')]", - "serverFarmId": "[variables('appServicePlanName')]", - "siteConfig": { - "appSettings": [ - { - "name": "WEBSITE_NODE_DEFAULT_VERSION", - "value": "10.14.1" - }, - { - "name": "MicrosoftAppId", - "value": "[parameters('appId')]" - }, - { - "name": "MicrosoftAppPassword", - "value": "[parameters('appSecret')]" - } - ], - "cors": { - "allowedOrigins": [ - "https://botservice.hosting.portal.azure.net", - "https://hosting.onecloud.azure-test.net/" - ] - } - } - } - }, - { - "apiVersion": "2017-12-01", - "type": "Microsoft.BotService/botServices", - "name": "[parameters('botId')]", - "location": "global", - "kind": "bot", - "sku": { - "name": "[parameters('botSku')]" - }, - "properties": { - "name": "[parameters('botId')]", - "displayName": "[parameters('botId')]", - "endpoint": "[variables('botEndpoint')]", - "msaAppId": "[parameters('appId')]", - "developerAppInsightsApplicationId": null, - "developerAppInsightKey": null, - "publishingCredentials": null, - "storageResourceId": null - }, - "dependsOn": [ - "[resourceId('Microsoft.Web/sites/', variables('webAppName'))]" - ] - } - ], - "outputs": {} - } - } - } - ] -} \ No newline at end of file diff --git a/BotProject/CSharp/DeploymentTemplates/template-with-preexisting-rg.json b/BotProject/CSharp/DeploymentTemplates/template-with-preexisting-rg.json deleted file mode 100644 index 43943b6581..0000000000 --- a/BotProject/CSharp/DeploymentTemplates/template-with-preexisting-rg.json +++ /dev/null @@ -1,154 +0,0 @@ -{ - "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "parameters": { - "appId": { - "type": "string", - "metadata": { - "description": "Active Directory App ID, set as MicrosoftAppId in the Web App's Application Settings." - } - }, - "appSecret": { - "type": "string", - "metadata": { - "description": "Active Directory App Password, set as MicrosoftAppPassword in the Web App's Application Settings. Defaults to \"\"." - } - }, - "botId": { - "type": "string", - "metadata": { - "description": "The globally unique and immutable bot ID. Also used to configure the displayName of the bot, which is mutable." - } - }, - "botSku": { - "defaultValue": "F0", - "type": "string", - "metadata": { - "description": "The pricing tier of the Bot Service Registration. Acceptable values are F0 and S1." - } - }, - "newAppServicePlanName": { - "type": "string", - "defaultValue": "", - "metadata": { - "description": "The name of the new App Service Plan." - } - }, - "newAppServicePlanSku": { - "type": "object", - "defaultValue": { - "name": "S1", - "tier": "Standard", - "size": "S1", - "family": "S", - "capacity": 1 - }, - "metadata": { - "description": "The SKU of the App Service Plan. Defaults to Standard values." - } - }, - "appServicePlanLocation": { - "type": "string", - "metadata": { - "description": "The location of the App Service Plan." - } - }, - "existingAppServicePlan": { - "type": "string", - "defaultValue": "", - "metadata": { - "description": "Name of the existing App Service Plan used to create the Web App for the bot." - } - }, - "newWebAppName": { - "type": "string", - "defaultValue": "", - "metadata": { - "description": "The globally unique name of the Web App. Defaults to the value passed in for \"botId\"." - } - } - }, - "variables": { - "defaultAppServicePlanName": "[if(empty(parameters('existingAppServicePlan')), 'createNewAppServicePlan', parameters('existingAppServicePlan'))]", - "useExistingAppServicePlan": "[not(equals(variables('defaultAppServicePlanName'), 'createNewAppServicePlan'))]", - "servicePlanName": "[if(variables('useExistingAppServicePlan'), parameters('existingAppServicePlan'), parameters('newAppServicePlanName'))]", - "resourcesLocation": "[parameters('appServicePlanLocation')]", - "webAppName": "[if(empty(parameters('newWebAppName')), parameters('botId'), parameters('newWebAppName'))]", - "siteHost": "[concat(variables('webAppName'), '.azurewebsites.net')]", - "botEndpoint": "[concat('https://', variables('siteHost'), '/api/messages')]" - }, - "resources": [ - { - "comments": "Create a new App Service Plan if no existing App Service Plan name was passed in.", - "type": "Microsoft.Web/serverfarms", - "condition": "[not(variables('useExistingAppServicePlan'))]", - "name": "[variables('servicePlanName')]", - "apiVersion": "2018-02-01", - "location": "[variables('resourcesLocation')]", - "sku": "[parameters('newAppServicePlanSku')]", - "properties": { - "name": "[variables('servicePlanName')]" - } - }, - { - "comments": "Create a Web App using an App Service Plan", - "type": "Microsoft.Web/sites", - "apiVersion": "2015-08-01", - "location": "[variables('resourcesLocation')]", - "kind": "app", - "dependsOn": [ - "[resourceId('Microsoft.Web/serverfarms/', variables('servicePlanName'))]" - ], - "name": "[variables('webAppName')]", - "properties": { - "name": "[variables('webAppName')]", - "serverFarmId": "[variables('servicePlanName')]", - "siteConfig": { - "appSettings": [ - { - "name": "WEBSITE_NODE_DEFAULT_VERSION", - "value": "10.14.1" - }, - { - "name": "MicrosoftAppId", - "value": "[parameters('appId')]" - }, - { - "name": "MicrosoftAppPassword", - "value": "[parameters('appSecret')]" - } - ], - "cors": { - "allowedOrigins": [ - "https://botservice.hosting.portal.azure.net", - "https://hosting.onecloud.azure-test.net/" - ] - } - } - } - }, - { - "apiVersion": "2017-12-01", - "type": "Microsoft.BotService/botServices", - "name": "[parameters('botId')]", - "location": "global", - "kind": "bot", - "sku": { - "name": "[parameters('botSku')]" - }, - "properties": { - "name": "[parameters('botId')]", - "displayName": "[parameters('botId')]", - "endpoint": "[variables('botEndpoint')]", - "msaAppId": "[parameters('appId')]", - "developerAppInsightsApplicationId": null, - "developerAppInsightKey": null, - "publishingCredentials": null, - "storageResourceId": null - }, - "dependsOn": [ - "[resourceId('Microsoft.Web/sites/', variables('webAppName'))]" - ] - } - ] -} \ No newline at end of file diff --git a/BotProject/CSharp/IBotManager.cs b/BotProject/CSharp/IBotManager.cs deleted file mode 100644 index 99e421c818..0000000000 --- a/BotProject/CSharp/IBotManager.cs +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System.IO; -using Microsoft.Bot.Builder.Integration.AspNet.Core; - -namespace Microsoft.Bot.Builder.ComposerBot.Json -{ - public interface IBotManager - { - IBotFrameworkHttpAdapter CurrentAdapter { get; } - - IBot CurrentBot { get; } - - void SetCurrent(Stream fileStream, string endpointKey = null, string appPwd = null); - } -} diff --git a/BotProject/CSharp/LuisConfig.cs b/BotProject/CSharp/LuisConfig.cs deleted file mode 100644 index 506d52e343..0000000000 --- a/BotProject/CSharp/LuisConfig.cs +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System.Collections.Generic; - -namespace Microsoft.Bot.Builder.ComposerBot.Json -{ - public class LuisConfig - { - public string Name { get; set; } - - public string DefaultLanguage { get; set; } - - public List Models { get; set; } - - public string AuthoringKey { get; set; } - - public bool Dialogs { get; set; } - - public string Environment { get; set; } - - public bool Autodelete { get; set; } - - public string AuthoringRegion { get; set; } - - public string Folder { get; set; } - - public bool Help { get; set; } - - public bool Force { get; set; } - - public string Config { get; set; } - - public string EndpointKeys { get; set; } - } -} diff --git a/BotProject/CSharp/LuisCustomConfig.cs b/BotProject/CSharp/LuisCustomConfig.cs deleted file mode 100644 index 5b468ccdc7..0000000000 --- a/BotProject/CSharp/LuisCustomConfig.cs +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System.Collections.Generic; - -namespace Microsoft.Bot.Builder.ComposerBot.Json -{ - public class LuisCustomConfig - { - public Dictionary Luis { get; set; } - } -} diff --git a/BotProject/CSharp/LuisKey.cs b/BotProject/CSharp/LuisKey.cs deleted file mode 100644 index e8564950e1..0000000000 --- a/BotProject/CSharp/LuisKey.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Microsoft.Bot.Builder.ComposerBot.Json -{ - public class LuisKey - { - public string Key { get; set; } - } -} diff --git a/BotProject/CSharp/NuGet.Config b/BotProject/CSharp/NuGet.Config deleted file mode 100644 index 11ff1952c1..0000000000 --- a/BotProject/CSharp/NuGet.Config +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/BotProject/CSharp/Program.cs b/BotProject/CSharp/Program.cs deleted file mode 100644 index d2fa81364c..0000000000 --- a/BotProject/CSharp/Program.cs +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; -using System.IO; -using Microsoft.AspNetCore; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; - -namespace Microsoft.Bot.Builder.ComposerBot.Json -{ - public class Program - { - public static void Main(string[] args) - { - BuildWebHost(args).Run(); - } - - public static IWebHost BuildWebHost(string[] args) => - WebHost.CreateDefaultBuilder(args) - .ConfigureAppConfiguration((hostingContext, config) => - { - var env = hostingContext.HostingEnvironment; - var luisAuthoringRegion = Environment.GetEnvironmentVariable("LUIS_AUTHORING_REGION") ?? "westus"; - config - .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true) - .AddJsonFile($"luis.settings.{env.EnvironmentName}.{luisAuthoringRegion}.json", optional: true, reloadOnChange: true) - .AddJsonFile($"luis.settings.{Environment.UserName}.{luisAuthoringRegion}.json", optional: true, reloadOnChange: true); - - if (env.IsDevelopment()) - { - config.AddUserSecrets(); - } - - config - .AddEnvironmentVariables() - .AddCommandLine(args); - }).UseStartup() - .Build(); - } -} diff --git a/BotProject/CSharp/Properties/launchSettings.json b/BotProject/CSharp/Properties/launchSettings.json deleted file mode 100644 index 33c8346152..0000000000 --- a/BotProject/CSharp/Properties/launchSettings.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:3979/", - "sslPort": 0 - } - }, - "profiles": { - "IIS Express": { - "commandName": "IISExpress", - "launchBrowser": true, - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, - "BotProject": { - "commandName": "Project", - "launchBrowser": true, - "applicationUrl": "http://localhost:3979", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - } - } -} \ No newline at end of file diff --git a/BotProject/CSharp/README.md b/BotProject/CSharp/README.md deleted file mode 100644 index 7f9cbbefb2..0000000000 --- a/BotProject/CSharp/README.md +++ /dev/null @@ -1,55 +0,0 @@ -## Bot Project -Bot project is the launcher project for the bots written in declarative form (JSON), using the Composer, for the Bot Framework SDK. - -## Instructions for setting up the Bot Project runtime -The Bot Project is a regular Bot Framework SDK V4 project. Before you can launch it from the emulator, you need to make sure you can run the bot. - -### Prerequisite: -* Install .Netcore 2 - -### Commands: - -* from root folder -* cd BotProject -* cd CSharp -* dotnet restore // for the package updates -* dotnet build // build -* dotnet run // start the bot -* It will start a web server and listening at http://localhost:3979. - -### Test bot -* You can set you emulator to connect to http://localhost:3979/api/messages. - -### config your bot -This setup is required for local testing of your Bot Runtime. -* The only thing you need to config is appsetting.json, which has a bot setting to launch the bot: - -``` -appsettings.json: -"bot": { - "provider": "localDisk", - "path": "../../Bots/SampleBot3/bot3.botproj" -} -``` - -## .botproj folder structure -``` -bot.botproj, bot project got the rootDialog from "entry" -{ - "services": [{ - "type": "luis", - "id": "1", - "name": "TodoBotLuis", - "lufile": "todo.lu", - "applicationId": "TodoBotLuis.applicationId", - "endpointKey": "TodoBotLuis.endpointKey", - "endpoint": "TodoBotLuis.endpoint" - }], - "files": [ - "*.dialog", - "*.lg" - ], - "entry": "main.dialog" -} -``` -* Please refer to [Samples](https://github.com/Microsoft/BotFramework-Composer/tree/master/SampleBots) for more samples. diff --git a/BotProject/CSharp/Schemas/sdk.schema b/BotProject/CSharp/Schemas/sdk.schema deleted file mode 100644 index fc90997234..0000000000 --- a/BotProject/CSharp/Schemas/sdk.schema +++ /dev/null @@ -1,6998 +0,0 @@ -{ - "$schema": "https://raw.githubusercontent.com/microsoft/botbuilder-dotnet/4.Future/schemas/component.schema", - "$id": "sdk.schema", - "type": "object", - "title": "Component types", - "description": "These are all of the types that can be created by the loader.", - "oneOf": [ - { - "title": "Microsoft.ActivityTemplate", - "description": "", - "$ref": "#/definitions/Microsoft.ActivityTemplate" - }, - { - "title": "Microsoft.AdaptiveDialog", - "description": "Flexible, data driven dialog that can adapt to the conversation.", - "$ref": "#/definitions/Microsoft.AdaptiveDialog" - }, - { - "title": "Microsoft.AgeEntityRecognizer", - "description": "Recognizer which recognizes age.", - "$ref": "#/definitions/Microsoft.AgeEntityRecognizer" - }, - { - "title": "Microsoft.AttachmentInput", - "description": "Collect information - Ask for a file or image.", - "$ref": "#/definitions/Microsoft.AttachmentInput" - }, - { - "title": "Microsoft.BeginDialog", - "description": "Begin another dialog.", - "$ref": "#/definitions/Microsoft.BeginDialog" - }, - { - "title": "Microsoft.CancelAllDialogs", - "description": "Cancel all active dialogs. All dialogs in the dialog chain will need a trigger to capture the event configured in this action.", - "$ref": "#/definitions/Microsoft.CancelAllDialogs" - }, - { - "title": "Microsoft.ChoiceInput", - "description": "Collect information - Pick from a list of choices", - "$ref": "#/definitions/Microsoft.ChoiceInput" - }, - { - "title": "Microsoft.ConditionalSelector", - "description": "Use a rule selector based on a condition", - "$ref": "#/definitions/Microsoft.ConditionalSelector" - }, - { - "title": "Microsoft.ConfirmInput", - "description": "Collect information - Ask for confirmation (yes or no).", - "$ref": "#/definitions/Microsoft.ConfirmInput" - }, - { - "title": "Microsoft.ConfirmationEntityRecognizer", - "description": "Recognizer which recognizes confirmation choices (yes/no).", - "$ref": "#/definitions/Microsoft.ConfirmationEntityRecognizer" - }, - { - "title": "Microsoft.CurrencyEntityRecognizer", - "description": "Recognizer which recognizes currency.", - "$ref": "#/definitions/Microsoft.CurrencyEntityRecognizer" - }, - { - "title": "Microsoft.DateTimeEntityRecognizer", - "description": "Recognizer which recognizes dates and time fragments.", - "$ref": "#/definitions/Microsoft.DateTimeEntityRecognizer" - }, - { - "title": "Microsoft.DateTimeInput", - "description": "Collect information - Ask for date and/ or time", - "$ref": "#/definitions/Microsoft.DateTimeInput" - }, - { - "title": "Microsoft.DebugBreak", - "description": "If debugger is attached, stop the execution at this point in the conversation.", - "$ref": "#/definitions/Microsoft.DebugBreak" - }, - { - "title": "Microsoft.DeleteProperty", - "description": "Delete a property and any value it holds.", - "$ref": "#/definitions/Microsoft.DeleteProperty" - }, - { - "title": "Microsoft.DimensionEntityRecognizer", - "description": "Recognizer which recognizes dimension.", - "$ref": "#/definitions/Microsoft.DimensionEntityRecognizer" - }, - { - "title": "Microsoft.EditActions", - "description": "Edit the current list of actions.", - "$ref": "#/definitions/Microsoft.EditActions" - }, - { - "title": "Microsoft.EditArray", - "description": "Modify an array in memory", - "$ref": "#/definitions/Microsoft.EditArray" - }, - { - "title": "Microsoft.EmailEntityRecognizer", - "description": "Recognizer which recognizes email.", - "$ref": "#/definitions/Microsoft.EmailEntityRecognizer" - }, - { - "title": "Microsoft.EmitEvent", - "description": "Emit an event. Capture this event with a trigger.", - "$ref": "#/definitions/Microsoft.EmitEvent" - }, - { - "title": "Microsoft.EndDialog", - "description": "End this dialog.", - "$ref": "#/definitions/Microsoft.EndDialog" - }, - { - "title": "Microsoft.EndTurn", - "description": "End the current turn without ending the dialog.", - "$ref": "#/definitions/Microsoft.EndTurn" - }, - { - "title": "Microsoft.FirstSelector", - "description": "Selector for first true rule", - "$ref": "#/definitions/Microsoft.FirstSelector" - }, - { - "title": "Microsoft.Foreach", - "description": "Execute actions on each item in an a collection.", - "$ref": "#/definitions/Microsoft.Foreach" - }, - { - "title": "Microsoft.ForeachPage", - "description": "Execute actions on each page (collection of items) in an array.", - "$ref": "#/definitions/Microsoft.ForeachPage" - }, - { - "title": "Microsoft.GuidEntityRecognizer", - "description": "Recognizer which recognizes guids.", - "$ref": "#/definitions/Microsoft.GuidEntityRecognizer" - }, - { - "title": "Microsoft.HashtagEntityRecognizer", - "description": "Recognizer which recognizes Hashtags.", - "$ref": "#/definitions/Microsoft.HashtagEntityRecognizer" - }, - { - "title": "Microsoft.HttpRequest", - "description": "Make a HTTP request.", - "$ref": "#/definitions/Microsoft.HttpRequest" - }, - { - "title": "Microsoft.IfCondition", - "description": "Two-way branch the conversation flow based on a condition.", - "$ref": "#/definitions/Microsoft.IfCondition" - }, - { - "title": "Microsoft.InitProperty", - "description": "Define and initialize a property to be an array or object.", - "$ref": "#/definitions/Microsoft.InitProperty" - }, - { - "title": "Microsoft.IpEntityRecognizer", - "description": "Recognizer which recognizes internet IP patterns (like 192.1.1.1).", - "$ref": "#/definitions/Microsoft.IpEntityRecognizer" - }, - { - "title": "Microsoft.LanguagePolicy", - "description": "This represents a policy map for locales lookups to use for language", - "$ref": "#/definitions/Microsoft.LanguagePolicy" - }, - { - "title": "Microsoft.LogAction", - "description": "Log a message to the host application. Send a TraceActivity to Bot Framework Emulator (optional).", - "$ref": "#/definitions/Microsoft.LogAction" - }, - { - "title": "Microsoft.LuisRecognizer", - "description": "LUIS recognizer.", - "$ref": "#/definitions/Microsoft.LuisRecognizer" - }, - { - "title": "Microsoft.MentionEntityRecognizer", - "description": "Recognizer which recognizes @Mentions", - "$ref": "#/definitions/Microsoft.MentionEntityRecognizer" - }, - { - "title": "Microsoft.MostSpecificSelector", - "description": "Select most specific true events with optional additional selector", - "$ref": "#/definitions/Microsoft.MostSpecificSelector" - }, - { - "title": "Microsoft.MultiLanguageRecognizer", - "description": "Configure one recognizer per language and the specify the language fallback policy.", - "$ref": "#/definitions/Microsoft.MultiLanguageRecognizer" - }, - { - "title": "Microsoft.NumberEntityRecognizer", - "description": "Recognizer which recognizes numbers.", - "$ref": "#/definitions/Microsoft.NumberEntityRecognizer" - }, - { - "title": "Microsoft.NumberInput", - "description": "Collect information - Ask for a number.", - "$ref": "#/definitions/Microsoft.NumberInput" - }, - { - "title": "Microsoft.NumberRangeEntityRecognizer", - "description": "Recognizer which recognizes ranges of numbers (Example:2 to 5).", - "$ref": "#/definitions/Microsoft.NumberRangeEntityRecognizer" - }, - { - "title": "Microsoft.OAuthInput", - "description": "Collect login information.", - "$ref": "#/definitions/Microsoft.OAuthInput" - }, - { - "title": "Microsoft.OnActivity", - "description": "Actions to perform on receipt of a generic activity.", - "$ref": "#/definitions/Microsoft.OnActivity" - }, - { - "title": "Microsoft.OnBeginDialog", - "description": "Actions to perform when this dialog begins.", - "$ref": "#/definitions/Microsoft.OnBeginDialog" - }, - { - "title": "Microsoft.OnCancelDialog", - "description": "Actions to perform on cancel dialog event.", - "$ref": "#/definitions/Microsoft.OnCancelDialog" - }, - { - "title": "Microsoft.OnCondition", - "description": "Actions to perform when specified condition is true.", - "$ref": "#/definitions/Microsoft.OnCondition" - }, - { - "title": "Microsoft.OnConversationUpdateActivity", - "description": "Actions to perform on receipt of an activity with type 'ConversationUpdate'.", - "$ref": "#/definitions/Microsoft.OnConversationUpdateActivity" - }, - { - "title": "Microsoft.OnCustomEvent", - "description": "Actions to perform when a custom event is detected. Use 'Emit a custom event' action to raise a custom event.", - "$ref": "#/definitions/Microsoft.OnCustomEvent" - }, - { - "title": "Microsoft.OnDialogEvent", - "description": "Actions to perform when a specific dialog event occurs.", - "$ref": "#/definitions/Microsoft.OnDialogEvent" - }, - { - "title": "Microsoft.OnEndOfConversationActivity", - "description": "Actions to perform on receipt of an activity with type 'EndOfConversation'.", - "$ref": "#/definitions/Microsoft.OnEndOfConversationActivity" - }, - { - "title": "Microsoft.OnError", - "description": "Action to perform when an 'Error' dialog event occurs.", - "$ref": "#/definitions/Microsoft.OnError" - }, - { - "title": "Microsoft.OnEventActivity", - "description": "Actions to perform on receipt of an activity with type 'Event'.", - "$ref": "#/definitions/Microsoft.OnEventActivity" - }, - { - "title": "Microsoft.OnHandoffActivity", - "description": "Actions to perform on receipt of an activity with type 'HandOff'.", - "$ref": "#/definitions/Microsoft.OnHandoffActivity" - }, - { - "title": "Microsoft.OnIntent", - "description": "Actions to perform when specified intent is recognized.", - "$ref": "#/definitions/Microsoft.OnIntent" - }, - { - "title": "Microsoft.OnInvokeActivity", - "description": "Actions to perform on receipt of an activity with type 'Invoke'.", - "$ref": "#/definitions/Microsoft.OnInvokeActivity" - }, - { - "title": "Microsoft.OnMessageActivity", - "description": "Actions to perform on receipt of an activity with type 'Message'. Overrides Intent trigger.", - "$ref": "#/definitions/Microsoft.OnMessageActivity" - }, - { - "title": "Microsoft.OnMessageDeleteActivity", - "description": "Actions to perform on receipt of an activity with type 'MessageDelete'.", - "$ref": "#/definitions/Microsoft.OnMessageDeleteActivity" - }, - { - "title": "Microsoft.OnMessageReactionActivity", - "description": "Actions to perform on receipt of an activity with type 'MessageReaction'.", - "$ref": "#/definitions/Microsoft.OnMessageReactionActivity" - }, - { - "title": "Microsoft.OnMessageUpdateActivity", - "description": "Actions to perform on receipt of an activity with type 'MessageUpdate'.", - "$ref": "#/definitions/Microsoft.OnMessageUpdateActivity" - }, - { - "title": "Microsoft.OnRepromptDialog", - "description": "Actions to perform when 'RepromptDialog' event occurs.", - "$ref": "#/definitions/Microsoft.OnRepromptDialog" - }, - { - "title": "Microsoft.OnTypingActivity", - "description": "Actions to perform on receipt of an activity with type 'Typing'.", - "$ref": "#/definitions/Microsoft.OnTypingActivity" - }, - { - "title": "Microsoft.OnUnknownIntent", - "description": "Action to perform when user input is unrecognized and if none of the 'on intent recognition' triggers match recognized intent.", - "$ref": "#/definitions/Microsoft.OnUnknownIntent" - }, - { - "title": "Microsoft.OrdinalEntityRecognizer", - "description": "Recognizer which recognizes ordinals (example: first, second, 3rd).", - "$ref": "#/definitions/Microsoft.OrdinalEntityRecognizer" - }, - { - "title": "Microsoft.PercentageEntityRecognizer", - "description": "Recognizer which recognizes percentages.", - "$ref": "#/definitions/Microsoft.PercentageEntityRecognizer" - }, - { - "title": "Microsoft.PhoneNumberEntityRecognizer", - "description": "Recognizer which recognizes phone numbers.", - "$ref": "#/definitions/Microsoft.PhoneNumberEntityRecognizer" - }, - { - "title": "Microsoft.QnAMakerDialog", - "description": "Dialog which uses QnAMAker knowledge base to answer questions.", - "$ref": "#/definitions/Microsoft.QnAMakerDialog" - }, - { - "title": "Microsoft.RandomSelector", - "description": "Select most specific true rule", - "$ref": "#/definitions/Microsoft.RandomSelector" - }, - { - "title": "Microsoft.RegExEntityRecognizer", - "description": "Recognizer which recognizes patterns of input based on regex.", - "$ref": "#/definitions/Microsoft.RegExEntityRecognizer" - }, - { - "title": "Microsoft.RegexRecognizer", - "description": "Use regular expressions to recognize intents and entities from user input.", - "$ref": "#/definitions/Microsoft.RegexRecognizer" - }, - { - "title": "Microsoft.RepeatDialog", - "description": "Repeat current dialog.", - "$ref": "#/definitions/Microsoft.RepeatDialog" - }, - { - "title": "Microsoft.ReplaceDialog", - "description": "Replace current dialog with another dialog.", - "$ref": "#/definitions/Microsoft.ReplaceDialog" - }, - { - "title": "Microsoft.SendActivity", - "description": "Respond with an activity.", - "$ref": "#/definitions/Microsoft.SendActivity" - }, - { - "title": "Microsoft.SetProperty", - "description": "Set property to a value.", - "$ref": "#/definitions/Microsoft.SetProperty" - }, - { - "title": "Microsoft.StaticActivityTemplate", - "description": "This allows you to define a static Activity object", - "$ref": "#/definitions/Microsoft.StaticActivityTemplate" - }, - { - "title": "Microsoft.SwitchCondition", - "description": "Execute different actions based on the value of a property.", - "$ref": "#/definitions/Microsoft.SwitchCondition" - }, - { - "title": "Microsoft.TemperatureEntityRecognizer", - "description": "Recognizer which recognizes temperatures.", - "$ref": "#/definitions/Microsoft.TemperatureEntityRecognizer" - }, - { - "title": "Microsoft.TextInput", - "description": "Collection information - Ask for a word or sentence.", - "$ref": "#/definitions/Microsoft.TextInput" - }, - { - "title": "Microsoft.TextTemplate", - "description": "Lg tempalte to evaluate to create text", - "$ref": "#/definitions/Microsoft.TextTemplate" - }, - { - "title": "Microsoft.TraceActivity", - "description": "Send a trace activity to the transcript logger and/ or Bot Framework Emulator.", - "$ref": "#/definitions/Microsoft.TraceActivity" - }, - { - "title": "Microsoft.TrueSelector", - "description": "Selector for all true events", - "$ref": "#/definitions/Microsoft.TrueSelector" - }, - { - "title": "Microsoft.UrlEntityRecognizer", - "description": "Recognizer which recognizes urls (example: http://bing.com)", - "$ref": "#/definitions/Microsoft.UrlEntityRecognizer" - } - ], - "definitions": { - "Microsoft.ActivityTemplate": { - "$role": "unionType(Microsoft.IActivityTemplate)", - "title": "Microsoft ActivityTemplate", - "type": "object", - "properties": { - "$type": { - "title": "$type", - "description": "Defines the valid properties for the component you are configuring (from a dialog .schema file)", - "type": "string", - "pattern": "^[a-zA-Z][a-zA-Z0-9.]*$", - "const": "Microsoft.ActivityTemplate" - }, - "$copy": { - "title": "$copy", - "description": "Copy the definition by id from a .dialog file.", - "type": "string", - "pattern": "^(([a-zA-Z][a-zA-Z0-9.]*)?(#[a-zA-Z][a-zA-Z0-9.]*)?)$" - }, - "$id": { - "title": "$id", - "description": "Inline id for reuse of an inline definition", - "type": "string", - "pattern": "^([a-zA-Z][a-zA-Z0-9.]*)$" - }, - "$designer": { - "title": "$designer", - "type": "object", - "description": "Extra information for the Bot Framework Designer." - }, - "template": { - "title": "Template", - "Description": "Language Generator template to use to create the activity", - "type": "string" - } - }, - "additionalProperties": false, - "patternProperties": { - "^\\$": { - "type": "string" - } - }, - "anyOf": [ - { - "title": "Reference", - "required": [ - "$copy" - ] - }, - { - "title": "Type", - "required": [ - "template", - "$type" - ] - } - ] - }, - "Microsoft.AdaptiveDialog": { - "$role": "unionType(Microsoft.IDialog)", - "title": "Adaptive Dialog", - "description": "Flexible, data driven dialog that can adapt to the conversation.", - "type": "object", - "properties": { - "$type": { - "title": "$type", - "description": "Defines the valid properties for the component you are configuring (from a dialog .schema file)", - "type": "string", - "pattern": "^[a-zA-Z][a-zA-Z0-9.]*$", - "const": "Microsoft.AdaptiveDialog" - }, - "$copy": { - "title": "$copy", - "description": "Copy the definition by id from a .dialog file.", - "type": "string", - "pattern": "^(([a-zA-Z][a-zA-Z0-9.]*)?(#[a-zA-Z][a-zA-Z0-9.]*)?)$" - }, - "$id": { - "title": "$id", - "description": "Inline id for reuse of an inline definition", - "type": "string", - "pattern": "^([a-zA-Z][a-zA-Z0-9.]*)$" - }, - "$designer": { - "title": "$designer", - "type": "object", - "description": "Extra information for the Bot Framework Designer." - }, - "id": { - "type": "string", - "title": "Id", - "description": "Optional dialog ID." - }, - "autoEndDialog": { - "type": "boolean", - "title": "Auto end dialog", - "description": "If set to true the dialog will automatically end when there are no further actions. If set to false, remember to manually end the dialog using EndDialog action.", - "default": "true" - }, - "defaultResultProperty": { - "type": "string", - "title": "Default result property", - "description": "Value that will be passed back to the parent dialog.", - "default": "dialog.result" - }, - "recognizer": { - "$type": "Microsoft.IRecognizer", - "title": "Recognizer", - "description": "Language Understanding recognizer that interprets user input into intent and entities.", - "$ref": "#/definitions/Microsoft.IRecognizer" - }, - "generator": { - "$type": "Microsoft.ILanguageGenerator", - "title": "Language Generator", - "description": "Language generator that generates bot responses.", - "$ref": "#/definitions/Microsoft.ILanguageGenerator" - }, - "selector": { - "$type": "Microsoft.ITriggerSelector", - "title": "Selector", - "description": "Policy to determine which trigger is executed. Defaults to a 'best match' selector (optional).", - "$ref": "#/definitions/Microsoft.ITriggerSelector" - }, - "triggers": { - "type": "array", - "description": "List of triggers defined for this dialog.", - "title": "Triggers", - "items": { - "$type": "Microsoft.ITriggerCondition", - "$ref": "#/definitions/Microsoft.ITriggerCondition" - } - } - }, - "additionalProperties": false, - "patternProperties": { - "^\\$": { - "type": "string" - } - }, - "anyOf": [ - { - "title": "Reference", - "required": [ - "$copy" - ] - }, - { - "title": "Type", - "required": [ - "$type" - ] - } - ] - }, - "Microsoft.AgeEntityRecognizer": { - "$role": "unionType(Microsoft.EntityRecognizers)", - "title": "Age Entity Recognizer", - "description": "Recognizer which recognizes age.", - "type": "object", - "properties": { - "$type": { - "title": "$type", - "description": "Defines the valid properties for the component you are configuring (from a dialog .schema file)", - "type": "string", - "pattern": "^[a-zA-Z][a-zA-Z0-9.]*$", - "const": "Microsoft.AgeEntityRecognizer" - }, - "$copy": { - "title": "$copy", - "description": "Copy the definition by id from a .dialog file.", - "type": "string", - "pattern": "^(([a-zA-Z][a-zA-Z0-9.]*)?(#[a-zA-Z][a-zA-Z0-9.]*)?)$" - }, - "$id": { - "title": "$id", - "description": "Inline id for reuse of an inline definition", - "type": "string", - "pattern": "^([a-zA-Z][a-zA-Z0-9.]*)$" - }, - "$designer": { - "title": "$designer", - "type": "object", - "description": "Extra information for the Bot Framework Designer." - } - }, - "additionalProperties": false, - "patternProperties": { - "^\\$": { - "type": "string" - } - }, - "anyOf": [ - { - "title": "Reference", - "required": [ - "$copy" - ] - }, - { - "title": "Type", - "required": [ - "$type" - ] - } - ] - }, - "Microsoft.AttachmentInput": { - "$role": "unionType(Microsoft.IDialog)", - "title": "Attachment input dialog", - "description": "Collect information - Ask for a file or image.", - "type": "object", - "properties": { - "$type": { - "title": "$type", - "description": "Defines the valid properties for the component you are configuring (from a dialog .schema file)", - "type": "string", - "pattern": "^[a-zA-Z][a-zA-Z0-9.]*$", - "const": "Microsoft.AttachmentInput" - }, - "$copy": { - "title": "$copy", - "description": "Copy the definition by id from a .dialog file.", - "type": "string", - "pattern": "^(([a-zA-Z][a-zA-Z0-9.]*)?(#[a-zA-Z][a-zA-Z0-9.]*)?)$" - }, - "$id": { - "title": "$id", - "description": "Inline id for reuse of an inline definition", - "type": "string", - "pattern": "^([a-zA-Z][a-zA-Z0-9.]*)$" - }, - "$designer": { - "title": "$designer", - "type": "object", - "description": "Extra information for the Bot Framework Designer." - }, - "id": { - "type": "string", - "title": "Id", - "description": "Optional id for the dialog" - }, - "prompt": { - "$type": "Microsoft.IActivityTemplate", - "title": "Initial prompt", - "description": "Message to send to collect information.", - "examples": [ - "What is your birth date?" - ], - "$ref": "#/definitions/Microsoft.IActivityTemplate" - }, - "unrecognizedPrompt": { - "$type": "Microsoft.IActivityTemplate", - "title": "Unrecognized prompt", - "description": "Message to send if user response is not recognized.", - "examples": [ - "Sorry, I do not understand '{turn.activity.text'}. Let's try again. What is your birth date?" - ], - "$ref": "#/definitions/Microsoft.IActivityTemplate" - }, - "invalidPrompt": { - "$type": "Microsoft.IActivityTemplate", - "title": "Invalid prompt", - "description": "Message to send if user response is invalid. Relies on specified validation expressions.", - "examples": [ - "Sorry, '{this.value}' does not work. I need a number between 1-150. What is your age?" - ], - "$ref": "#/definitions/Microsoft.IActivityTemplate" - }, - "defaultValueResponse": { - "$type": "Microsoft.IActivityTemplate", - "title": "Default value response", - "description": "Message to send when max turn count (if specified) has been exceeded and the default value is selected as the value.", - "examples": [ - "Sorry, I'm having trouble understanding you. I will just use {this.options.defaultValue} for now. You can say 'I'm 36 years old' to change it." - ], - "$ref": "#/definitions/Microsoft.IActivityTemplate" - }, - "maxTurnCount": { - "type": "integer", - "title": "Max turn count", - "description": "Maximum number of re-prompt attempts to collect information.", - "default": 3, - "examples": [ - 3 - ] - }, - "validations": { - "type": "array", - "title": "Validation expressions", - "description": "Expression to validate user input.", - "examples": [ - "int(this.value) > 1 && int(this.value) <= 150", - "count(this.value) < 300" - ], - "items": { - "$role": "expression", - "type": "string", - "description": "String must contain an expression." - } - }, - "property": { - "$role": "expression", - "title": "Property", - "description": "Property to store collected information. Input will be skipped if property has value (unless 'Always prompt' is true).", - "examples": [ - "$birthday", - "user.name", - "conversation.issueTitle", - "dialog.favColor" - ], - "type": "string" - }, - "defaultValue": { - "$role": "expression", - "title": "Default value", - "description": "Expression to examine on each turn of the conversation as possible value to the property.", - "examples": [ - "@userName", - "coalesce(@number, @partySize)" - ], - "type": "string" - }, - "alwaysPrompt": { - "type": "boolean", - "title": "Always prompt", - "description": "Collect information even if the specified 'property' is not empty.", - "default": false, - "examples": [ - false - ] - }, - "allowInterruptions": { - "type": "string", - "title": "Allow Interruptions", - "description": "A boolean expression that determines whether the parent should be allowed to interrupt the input.", - "default": "true", - "examples": [ - "true" - ] - }, - "outputFormat": { - "type": "string", - "enum": [ - "all", - "first" - ], - "title": "Output format", - "description": "Attachment output format.", - "default": "first" - } - }, - "additionalProperties": false, - "patternProperties": { - "^\\$": { - "type": "string" - } - }, - "anyOf": [ - { - "title": "Reference", - "required": [ - "$copy" - ] - }, - { - "title": "Type", - "required": [ - "$type" - ] - } - ] - }, - "Microsoft.BeginDialog": { - "$role": "unionType(Microsoft.IDialog)", - "title": "Begin a dialog", - "description": "Begin another dialog.", - "type": "object", - "properties": { - "$type": { - "title": "$type", - "description": "Defines the valid properties for the component you are configuring (from a dialog .schema file)", - "type": "string", - "pattern": "^[a-zA-Z][a-zA-Z0-9.]*$", - "const": "Microsoft.BeginDialog" - }, - "$copy": { - "title": "$copy", - "description": "Copy the definition by id from a .dialog file.", - "type": "string", - "pattern": "^(([a-zA-Z][a-zA-Z0-9.]*)?(#[a-zA-Z][a-zA-Z0-9.]*)?)$" - }, - "$id": { - "title": "$id", - "description": "Inline id for reuse of an inline definition", - "type": "string", - "pattern": "^([a-zA-Z][a-zA-Z0-9.]*)$" - }, - "$designer": { - "title": "$designer", - "type": "object", - "description": "Extra information for the Bot Framework Designer." - }, - "dialog": { - "$type": "Microsoft.IDialog", - "title": "Dialog name", - "description": "Name of the dialog to call.", - "examples": [ - "AddToDoDialog" - ], - "$ref": "#/definitions/Microsoft.IDialog" - }, - "options": { - "type": "object", - "title": "Options", - "description": "One or more options that are passed to the dialog that is called.", - "additionalProperties": { - "type": "string", - "title": "Options" - } - }, - "resultProperty": { - "$role": "expression", - "title": "Property", - "description": "Property to store any value returned by the dialog that is called.", - "examples": [ - "dialog.userName" - ], - "type": "string" - } - }, - "additionalProperties": false, - "patternProperties": { - "^\\$": { - "type": "string" - } - }, - "anyOf": [ - { - "title": "Reference", - "required": [ - "$copy" - ] - }, - { - "title": "Type", - "required": [ - "$type" - ] - } - ] - }, - "Microsoft.CancelAllDialogs": { - "$role": "unionType(Microsoft.IDialog)", - "title": "Cancel all dialogs", - "description": "Cancel all active dialogs. All dialogs in the dialog chain will need a trigger to capture the event configured in this action.", - "type": "object", - "properties": { - "$type": { - "title": "$type", - "description": "Defines the valid properties for the component you are configuring (from a dialog .schema file)", - "type": "string", - "pattern": "^[a-zA-Z][a-zA-Z0-9.]*$", - "const": "Microsoft.CancelAllDialogs" - }, - "$copy": { - "title": "$copy", - "description": "Copy the definition by id from a .dialog file.", - "type": "string", - "pattern": "^(([a-zA-Z][a-zA-Z0-9.]*)?(#[a-zA-Z][a-zA-Z0-9.]*)?)$" - }, - "$id": { - "title": "$id", - "description": "Inline id for reuse of an inline definition", - "type": "string", - "pattern": "^([a-zA-Z][a-zA-Z0-9.]*)$" - }, - "$designer": { - "title": "$designer", - "type": "object", - "description": "Extra information for the Bot Framework Designer." - }, - "eventName": { - "title": "Event name", - "description": "Name of the event to emit.", - "type": "string" - }, - "eventValue": { - "type": "object", - "title": "Event value", - "description": "Value to emit with the event (optional).", - "additionalProperties": true - } - }, - "additionalProperties": false, - "patternProperties": { - "^\\$": { - "type": "string" - } - }, - "anyOf": [ - { - "title": "Reference", - "required": [ - "$copy" - ] - }, - { - "title": "Type", - "required": [ - "$type" - ] - } - ] - }, - "Microsoft.ChoiceInput": { - "$role": "unionType(Microsoft.IDialog)", - "title": "Choice input dialog", - "description": "Collect information - Pick from a list of choices", - "type": "object", - "properties": { - "$type": { - "title": "$type", - "description": "Defines the valid properties for the component you are configuring (from a dialog .schema file)", - "type": "string", - "pattern": "^[a-zA-Z][a-zA-Z0-9.]*$", - "const": "Microsoft.ChoiceInput" - }, - "$copy": { - "title": "$copy", - "description": "Copy the definition by id from a .dialog file.", - "type": "string", - "pattern": "^(([a-zA-Z][a-zA-Z0-9.]*)?(#[a-zA-Z][a-zA-Z0-9.]*)?)$" - }, - "$id": { - "title": "$id", - "description": "Inline id for reuse of an inline definition", - "type": "string", - "pattern": "^([a-zA-Z][a-zA-Z0-9.]*)$" - }, - "$designer": { - "title": "$designer", - "type": "object", - "description": "Extra information for the Bot Framework Designer." - }, - "id": { - "type": "string", - "title": "Id", - "description": "Optional id for the dialog" - }, - "prompt": { - "$type": "Microsoft.IActivityTemplate", - "title": "Initial prompt", - "description": "Message to send to collect information.", - "examples": [ - "What is your birth date?" - ], - "$ref": "#/definitions/Microsoft.IActivityTemplate" - }, - "unrecognizedPrompt": { - "$type": "Microsoft.IActivityTemplate", - "title": "Unrecognized prompt", - "description": "Message to send if user response is not recognized.", - "examples": [ - "Sorry, I do not understand '{turn.activity.text'}. Let's try again. What is your birth date?" - ], - "$ref": "#/definitions/Microsoft.IActivityTemplate" - }, - "invalidPrompt": { - "$type": "Microsoft.IActivityTemplate", - "title": "Invalid prompt", - "description": "Message to send if user response is invalid. Relies on specified validation expressions.", - "examples": [ - "Sorry, '{this.value}' does not work. I need a number between 1-150. What is your age?" - ], - "$ref": "#/definitions/Microsoft.IActivityTemplate" - }, - "defaultValueResponse": { - "$type": "Microsoft.IActivityTemplate", - "title": "Default value response", - "description": "Message to send when max turn count (if specified) has been exceeded and the default value is selected as the value.", - "examples": [ - "Sorry, I'm having trouble understanding you. I will just use {this.options.defaultValue} for now. You can say 'I'm 36 years old' to change it." - ], - "$ref": "#/definitions/Microsoft.IActivityTemplate" - }, - "maxTurnCount": { - "type": "integer", - "title": "Max turn count", - "description": "Maximum number of re-prompt attempts to collect information.", - "default": 3, - "examples": [ - 3 - ] - }, - "validations": { - "type": "array", - "title": "Validation expressions", - "description": "Expression to validate user input.", - "examples": [ - "int(this.value) > 1 && int(this.value) <= 150", - "count(this.value) < 300" - ], - "items": { - "$role": "expression", - "type": "string", - "description": "String must contain an expression." - } - }, - "property": { - "$role": "expression", - "title": "Property", - "description": "Property to store collected information. Input will be skipped if property has value (unless 'Always prompt' is true).", - "examples": [ - "$birthday", - "user.name", - "conversation.issueTitle", - "dialog.favColor" - ], - "type": "string" - }, - "defaultValue": { - "$role": "expression", - "title": "Default value", - "description": "Expression to examine on each turn of the conversation as possible value to the property.", - "examples": [ - "@userName", - "coalesce(@number, @partySize)" - ], - "type": "string" - }, - "alwaysPrompt": { - "type": "boolean", - "title": "Always prompt", - "description": "Collect information even if the specified 'property' is not empty.", - "default": false, - "examples": [ - false - ] - }, - "allowInterruptions": { - "type": "string", - "title": "Allow Interruptions", - "description": "A boolean expression that determines whether the parent should be allowed to interrupt the input.", - "default": "true", - "examples": [ - "true" - ] - }, - "outputFormat": { - "type": "string", - "enum": [ - "value", - "index" - ], - "title": "Output format", - "description": "Choice output format.", - "default": "value" - }, - "choices": { - "anyOf": [ - { - "$role": "expression", - "type": "string", - "description": "String must contain an expression." - }, - { - "type": "array", - "items": [ - { - "type": "string" - } - ] - }, - { - "type": "array", - "items": [ - { - "title": "Choice", - "type": "object", - "properties": { - "value": { - "type": "string", - "title": "Value", - "description": "Value to return when this choice is selected." - }, - "action": { - "type": "object", - "title": "Action", - "description": "Card action for the choice." - }, - "synonyms": { - "type": "array", - "title": "Synonyms", - "description": "List of synonyms to recognize in addition to the value (optional).", - "items": { - "type": "string" - } - } - } - } - ] - } - ] - }, - "appendChoices": { - "type": "boolean", - "title": "Append choices", - "description": "Compose an output activity containing a set of choices", - "default": "true" - }, - "defaultLocale": { - "type": "string", - "title": "Default locale", - "description": "Default locale.", - "default": "en-us" - }, - "style": { - "type": "string", - "enum": [ - "None", - "Auto", - "Inline", - "List", - "SuggestedAction", - "HeroCard" - ], - "title": "List style", - "description": "Style to render choices.", - "default": "Auto" - }, - "choiceOptions": { - "type": "object", - "properties": { - "inlineSeparator": { - "type": "string", - "title": "Inline separator", - "description": "Character used to separate individual choices when there are more than 2 choices", - "default": ", " - }, - "inlineOr": { - "type": "string", - "title": "Inline or", - "description": "Separator inserted between the choices when there are only 2 choices", - "default": " or " - }, - "inlineOrMore": { - "type": "string", - "title": "Inline or more", - "description": "Separator inserted between the last 2 choices when their are more than 2 choices.", - "default": ", or " - }, - "includeNumbers": { - "type": "boolean", - "title": "Include numbers", - "description": "If true, 'inline' and 'list' list style will be prefixed with the index of the choice.", - "default": true - } - } - }, - "recognizerOptions": { - "type": "object", - "properties": { - "noValue": { - "type": "boolean", - "title": "No value", - "description": "If true, the choices value field will NOT be search over", - "default": false - }, - "noAction": { - "type": "boolean", - "title": "No action", - "description": "If true, the the choices action.title field will NOT be searched over", - "default": false - } - } - } - }, - "additionalProperties": false, - "patternProperties": { - "^\\$": { - "type": "string" - } - }, - "anyOf": [ - { - "title": "Reference", - "required": [ - "$copy" - ] - }, - { - "title": "Type", - "required": [ - "$type" - ] - } - ] - }, - "Microsoft.ConditionalSelector": { - "$role": "unionType(Microsoft.ITriggerSelector)", - "title": "Condtional Trigger Selector", - "description": "Use a rule selector based on a condition", - "type": "object", - "properties": { - "$type": { - "title": "$type", - "description": "Defines the valid properties for the component you are configuring (from a dialog .schema file)", - "type": "string", - "pattern": "^[a-zA-Z][a-zA-Z0-9.]*$", - "const": "Microsoft.ConditionalSelector" - }, - "$copy": { - "title": "$copy", - "description": "Copy the definition by id from a .dialog file.", - "type": "string", - "pattern": "^(([a-zA-Z][a-zA-Z0-9.]*)?(#[a-zA-Z][a-zA-Z0-9.]*)?)$" - }, - "$id": { - "title": "$id", - "description": "Inline id for reuse of an inline definition", - "type": "string", - "pattern": "^([a-zA-Z][a-zA-Z0-9.]*)$" - }, - "$designer": { - "title": "$designer", - "type": "object", - "description": "Extra information for the Bot Framework Designer." - }, - "condition": { - "$role": "expression", - "type": "string", - "description": "String must contain an expression." - }, - "ifTrue": { - "$type": "Microsoft.ITriggerSelector", - "$ref": "#/definitions/Microsoft.ITriggerSelector" - }, - "ifFalse": { - "$type": "Microsoft.ITriggerSelector", - "$ref": "#/definitions/Microsoft.ITriggerSelector" - } - }, - "additionalProperties": false, - "patternProperties": { - "^\\$": { - "type": "string" - } - }, - "anyOf": [ - { - "title": "Reference", - "required": [ - "$copy" - ] - }, - { - "title": "Type", - "required": [ - "condition", - "ifTrue", - "ifFalse", - "$type" - ] - } - ] - }, - "Microsoft.ConfirmInput": { - "$role": "unionType(Microsoft.IDialog)", - "title": "Confirm input dialog", - "description": "Collect information - Ask for confirmation (yes or no).", - "type": "object", - "properties": { - "$type": { - "title": "$type", - "description": "Defines the valid properties for the component you are configuring (from a dialog .schema file)", - "type": "string", - "pattern": "^[a-zA-Z][a-zA-Z0-9.]*$", - "const": "Microsoft.ConfirmInput" - }, - "$copy": { - "title": "$copy", - "description": "Copy the definition by id from a .dialog file.", - "type": "string", - "pattern": "^(([a-zA-Z][a-zA-Z0-9.]*)?(#[a-zA-Z][a-zA-Z0-9.]*)?)$" - }, - "$id": { - "title": "$id", - "description": "Inline id for reuse of an inline definition", - "type": "string", - "pattern": "^([a-zA-Z][a-zA-Z0-9.]*)$" - }, - "$designer": { - "title": "$designer", - "type": "object", - "description": "Extra information for the Bot Framework Designer." - }, - "id": { - "type": "string", - "title": "Id", - "description": "Optional id for the dialog" - }, - "prompt": { - "$type": "Microsoft.IActivityTemplate", - "title": "Initial prompt", - "description": "Message to send to collect information.", - "examples": [ - "What is your birth date?" - ], - "$ref": "#/definitions/Microsoft.IActivityTemplate" - }, - "unrecognizedPrompt": { - "$type": "Microsoft.IActivityTemplate", - "title": "Unrecognized prompt", - "description": "Message to send if user response is not recognized.", - "examples": [ - "Sorry, I do not understand '{turn.activity.text'}. Let's try again. What is your birth date?" - ], - "$ref": "#/definitions/Microsoft.IActivityTemplate" - }, - "invalidPrompt": { - "$type": "Microsoft.IActivityTemplate", - "title": "Invalid prompt", - "description": "Message to send if user response is invalid. Relies on specified validation expressions.", - "examples": [ - "Sorry, '{this.value}' does not work. I need a number between 1-150. What is your age?" - ], - "$ref": "#/definitions/Microsoft.IActivityTemplate" - }, - "defaultValueResponse": { - "$type": "Microsoft.IActivityTemplate", - "title": "Default value response", - "description": "Message to send when max turn count (if specified) has been exceeded and the default value is selected as the value.", - "examples": [ - "Sorry, I'm having trouble understanding you. I will just use {this.options.defaultValue} for now. You can say 'I'm 36 years old' to change it." - ], - "$ref": "#/definitions/Microsoft.IActivityTemplate" - }, - "maxTurnCount": { - "type": "integer", - "title": "Max turn count", - "description": "Maximum number of re-prompt attempts to collect information.", - "default": 3, - "examples": [ - 3 - ] - }, - "validations": { - "type": "array", - "title": "Validation expressions", - "description": "Expression to validate user input.", - "examples": [ - "int(this.value) > 1 && int(this.value) <= 150", - "count(this.value) < 300" - ], - "items": { - "$role": "expression", - "type": "string", - "description": "String must contain an expression." - } - }, - "property": { - "$role": "expression", - "title": "Property", - "description": "Property to store collected information. Input will be skipped if property has value (unless 'Always prompt' is true).", - "examples": [ - "$birthday", - "user.name", - "conversation.issueTitle", - "dialog.favColor" - ], - "type": "string" - }, - "defaultValue": { - "$role": "expression", - "title": "Default value", - "description": "Expression to examine on each turn of the conversation as possible value to the property.", - "examples": [ - "@userName", - "coalesce(@number, @partySize)" - ], - "type": "string" - }, - "alwaysPrompt": { - "type": "boolean", - "title": "Always prompt", - "description": "Collect information even if the specified 'property' is not empty.", - "default": false, - "examples": [ - false - ] - }, - "allowInterruptions": { - "type": "string", - "title": "Allow Interruptions", - "description": "A boolean expression that determines whether the parent should be allowed to interrupt the input.", - "default": "true", - "examples": [ - "true" - ] - }, - "defaultLocale": { - "type": "string", - "title": "Default locale", - "description": "Default locale.", - "default": "en-us" - }, - "style": { - "type": "string", - "enum": [ - "None", - "Auto", - "Inline", - "List", - "SuggestedAction", - "HeroCard" - ], - "title": "List style", - "description": "Style to render choices.", - "default": "Auto" - }, - "choiceOptions": { - "type": "object", - "properties": { - "inlineSeparator": { - "type": "string", - "title": "Inline separator", - "description": "Character used to separate individual choices when there are more than 2 choices", - "default": ", " - }, - "inlineOr": { - "type": "string", - "title": "Inline or", - "description": "Separator inserted between the choices when their are only 2 choices", - "default": " or " - }, - "inlineOrMore": { - "type": "string", - "title": "Inline or more", - "description": "Separator inserted between the last 2 choices when their are more than 2 choices.", - "default": ", or " - }, - "includeNumbers": { - "type": "boolean", - "title": "Include numbers", - "description": "If true, inline and list style choices will be prefixed with the index of the choice.", - "default": true - } - } - }, - "confirmChoices": { - "type": "array", - "items": [ - { - "type": "object", - "properties": { - "value": { - "type": "string", - "title": "Value", - "description": "Value to return when this choice is selected." - }, - "action": { - "type": "object", - "title": "Action", - "description": "Card action for the choice" - }, - "synonyms": { - "type": "array", - "title": "Synonyms", - "description": "List of synonyms to recognize in addition to the value (optional)", - "items": { - "type": "string" - } - } - } - } - ] - } - }, - "additionalProperties": false, - "patternProperties": { - "^\\$": { - "type": "string" - } - }, - "anyOf": [ - { - "title": "Reference", - "required": [ - "$copy" - ] - }, - { - "title": "Type", - "required": [ - "$type" - ] - } - ] - }, - "Microsoft.ConfirmationEntityRecognizer": { - "$role": "unionType(Microsoft.EntityRecognizers)", - "title": "Confirmation Entity Recognizer", - "description": "Recognizer which recognizes confirmation choices (yes/no).", - "type": "object", - "properties": { - "$type": { - "title": "$type", - "description": "Defines the valid properties for the component you are configuring (from a dialog .schema file)", - "type": "string", - "pattern": "^[a-zA-Z][a-zA-Z0-9.]*$", - "const": "Microsoft.ConfirmationEntityRecognizer" - }, - "$copy": { - "title": "$copy", - "description": "Copy the definition by id from a .dialog file.", - "type": "string", - "pattern": "^(([a-zA-Z][a-zA-Z0-9.]*)?(#[a-zA-Z][a-zA-Z0-9.]*)?)$" - }, - "$id": { - "title": "$id", - "description": "Inline id for reuse of an inline definition", - "type": "string", - "pattern": "^([a-zA-Z][a-zA-Z0-9.]*)$" - }, - "$designer": { - "title": "$designer", - "type": "object", - "description": "Extra information for the Bot Framework Designer." - } - }, - "additionalProperties": false, - "patternProperties": { - "^\\$": { - "type": "string" - } - }, - "anyOf": [ - { - "title": "Reference", - "required": [ - "$copy" - ] - }, - { - "title": "Type", - "required": [ - "$type" - ] - } - ] - }, - "Microsoft.CurrencyEntityRecognizer": { - "$role": "unionType(Microsoft.EntityRecognizers)", - "title": "Currency Entity Recognizer", - "description": "Recognizer which recognizes currency.", - "type": "object", - "properties": { - "$type": { - "title": "$type", - "description": "Defines the valid properties for the component you are configuring (from a dialog .schema file)", - "type": "string", - "pattern": "^[a-zA-Z][a-zA-Z0-9.]*$", - "const": "Microsoft.CurrencyEntityRecognizer" - }, - "$copy": { - "title": "$copy", - "description": "Copy the definition by id from a .dialog file.", - "type": "string", - "pattern": "^(([a-zA-Z][a-zA-Z0-9.]*)?(#[a-zA-Z][a-zA-Z0-9.]*)?)$" - }, - "$id": { - "title": "$id", - "description": "Inline id for reuse of an inline definition", - "type": "string", - "pattern": "^([a-zA-Z][a-zA-Z0-9.]*)$" - }, - "$designer": { - "title": "$designer", - "type": "object", - "description": "Extra information for the Bot Framework Designer." - } - }, - "additionalProperties": false, - "patternProperties": { - "^\\$": { - "type": "string" - } - }, - "anyOf": [ - { - "title": "Reference", - "required": [ - "$copy" - ] - }, - { - "title": "Type", - "required": [ - "$type" - ] - } - ] - }, - "Microsoft.DateTimeEntityRecognizer": { - "$role": "unionType(Microsoft.EntityRecognizers)", - "title": "DateTime Entity Recognizer", - "description": "Recognizer which recognizes dates and time fragments.", - "type": "object", - "properties": { - "$type": { - "title": "$type", - "description": "Defines the valid properties for the component you are configuring (from a dialog .schema file)", - "type": "string", - "pattern": "^[a-zA-Z][a-zA-Z0-9.]*$", - "const": "Microsoft.DateTimeEntityRecognizer" - }, - "$copy": { - "title": "$copy", - "description": "Copy the definition by id from a .dialog file.", - "type": "string", - "pattern": "^(([a-zA-Z][a-zA-Z0-9.]*)?(#[a-zA-Z][a-zA-Z0-9.]*)?)$" - }, - "$id": { - "title": "$id", - "description": "Inline id for reuse of an inline definition", - "type": "string", - "pattern": "^([a-zA-Z][a-zA-Z0-9.]*)$" - }, - "$designer": { - "title": "$designer", - "type": "object", - "description": "Extra information for the Bot Framework Designer." - } - }, - "additionalProperties": false, - "patternProperties": { - "^\\$": { - "type": "string" - } - }, - "anyOf": [ - { - "title": "Reference", - "required": [ - "$copy" - ] - }, - { - "title": "Type", - "required": [ - "$type" - ] - } - ] - }, - "Microsoft.DateTimeInput": { - "$role": "unionType(Microsoft.IDialog)", - "title": "Date/time input dialog", - "description": "Collect information - Ask for date and/ or time", - "type": "object", - "properties": { - "$type": { - "title": "$type", - "description": "Defines the valid properties for the component you are configuring (from a dialog .schema file)", - "type": "string", - "pattern": "^[a-zA-Z][a-zA-Z0-9.]*$", - "const": "Microsoft.DateTimeInput" - }, - "$copy": { - "title": "$copy", - "description": "Copy the definition by id from a .dialog file.", - "type": "string", - "pattern": "^(([a-zA-Z][a-zA-Z0-9.]*)?(#[a-zA-Z][a-zA-Z0-9.]*)?)$" - }, - "$id": { - "title": "$id", - "description": "Inline id for reuse of an inline definition", - "type": "string", - "pattern": "^([a-zA-Z][a-zA-Z0-9.]*)$" - }, - "$designer": { - "title": "$designer", - "type": "object", - "description": "Extra information for the Bot Framework Designer." - }, - "id": { - "type": "string", - "title": "Id", - "description": "Optional id for the dialog" - }, - "prompt": { - "$type": "Microsoft.IActivityTemplate", - "title": "Initial prompt", - "description": "Message to send to collect information.", - "examples": [ - "What is your birth date?" - ], - "$ref": "#/definitions/Microsoft.IActivityTemplate" - }, - "unrecognizedPrompt": { - "$type": "Microsoft.IActivityTemplate", - "title": "Unrecognized prompt", - "description": "Message to send if user response is not recognized.", - "examples": [ - "Sorry, I do not understand '{turn.activity.text'}. Let's try again. What is your birth date?" - ], - "$ref": "#/definitions/Microsoft.IActivityTemplate" - }, - "invalidPrompt": { - "$type": "Microsoft.IActivityTemplate", - "title": "Invalid prompt", - "description": "Message to send if user response is invalid. Relies on specified validation expressions.", - "examples": [ - "Sorry, '{this.value}' does not work. I need a number between 1-150. What is your age?" - ], - "$ref": "#/definitions/Microsoft.IActivityTemplate" - }, - "defaultValueResponse": { - "$type": "Microsoft.IActivityTemplate", - "title": "Default value response", - "description": "Message to send when max turn count (if specified) has been exceeded and the default value is selected as the value.", - "examples": [ - "Sorry, I'm having trouble understanding you. I will just use {this.options.defaultValue} for now. You can say 'I'm 36 years old' to change it." - ], - "$ref": "#/definitions/Microsoft.IActivityTemplate" - }, - "maxTurnCount": { - "type": "integer", - "title": "Max turn count", - "description": "Maximum number of re-prompt attempts to collect information.", - "default": 3, - "examples": [ - 3 - ] - }, - "validations": { - "type": "array", - "title": "Validation expressions", - "description": "Expression to validate user input.", - "examples": [ - "int(this.value) > 1 && int(this.value) <= 150", - "count(this.value) < 300" - ], - "items": { - "$role": "expression", - "type": "string", - "description": "String must contain an expression." - } - }, - "property": { - "$role": "expression", - "title": "Property", - "description": "Property to store collected information. Input will be skipped if property has value (unless 'Always prompt' is true).", - "examples": [ - "$birthday", - "user.name", - "conversation.issueTitle", - "dialog.favColor" - ], - "type": "string" - }, - "defaultValue": { - "$role": "expression", - "title": "Default value", - "description": "Expression to examine on each turn of the conversation as possible value to the property.", - "examples": [ - "@userName", - "coalesce(@number, @partySize)" - ], - "type": "string" - }, - "alwaysPrompt": { - "type": "boolean", - "title": "Always prompt", - "description": "Collect information even if the specified 'property' is not empty.", - "default": false, - "examples": [ - false - ] - }, - "allowInterruptions": { - "type": "string", - "title": "Allow Interruptions", - "description": "A boolean expression that determines whether the parent should be allowed to interrupt the input.", - "default": "true", - "examples": [ - "true" - ] - }, - "defaultLocale": { - "type": "string", - "title": "Default locale", - "description": "Default locale.", - "default": "en-us" - } - }, - "additionalProperties": false, - "patternProperties": { - "^\\$": { - "type": "string" - } - }, - "anyOf": [ - { - "title": "Reference", - "required": [ - "$copy" - ] - }, - { - "title": "Type", - "required": [ - "$type" - ] - } - ] - }, - "Microsoft.DebugBreak": { - "$role": "unionType(Microsoft.IDialog)", - "title": "Debugger break", - "description": "If debugger is attached, stop the execution at this point in the conversation.", - "type": "object", - "properties": { - "$type": { - "title": "$type", - "description": "Defines the valid properties for the component you are configuring (from a dialog .schema file)", - "type": "string", - "pattern": "^[a-zA-Z][a-zA-Z0-9.]*$", - "const": "Microsoft.DebugBreak" - }, - "$copy": { - "title": "$copy", - "description": "Copy the definition by id from a .dialog file.", - "type": "string", - "pattern": "^(([a-zA-Z][a-zA-Z0-9.]*)?(#[a-zA-Z][a-zA-Z0-9.]*)?)$" - }, - "$id": { - "title": "$id", - "description": "Inline id for reuse of an inline definition", - "type": "string", - "pattern": "^([a-zA-Z][a-zA-Z0-9.]*)$" - }, - "$designer": { - "title": "$designer", - "type": "object", - "description": "Extra information for the Bot Framework Designer." - } - }, - "additionalProperties": false, - "patternProperties": { - "^\\$": { - "type": "string" - } - }, - "anyOf": [ - { - "title": "Reference", - "required": [ - "$copy" - ] - }, - { - "title": "Type", - "required": [ - "$type" - ] - } - ] - }, - "Microsoft.DeleteProperty": { - "$role": "unionType(Microsoft.IDialog)", - "title": "Delete Property", - "description": "Delete a property and any value it holds.", - "type": "object", - "properties": { - "$type": { - "title": "$type", - "description": "Defines the valid properties for the component you are configuring (from a dialog .schema file)", - "type": "string", - "pattern": "^[a-zA-Z][a-zA-Z0-9.]*$", - "const": "Microsoft.DeleteProperty" - }, - "$copy": { - "title": "$copy", - "description": "Copy the definition by id from a .dialog file.", - "type": "string", - "pattern": "^(([a-zA-Z][a-zA-Z0-9.]*)?(#[a-zA-Z][a-zA-Z0-9.]*)?)$" - }, - "$id": { - "title": "$id", - "description": "Inline id for reuse of an inline definition", - "type": "string", - "pattern": "^([a-zA-Z][a-zA-Z0-9.]*)$" - }, - "$designer": { - "title": "$designer", - "type": "object", - "description": "Extra information for the Bot Framework Designer." - }, - "property": { - "$role": "expression", - "title": "Property", - "description": "Property to delete.", - "type": "string" - } - }, - "additionalProperties": false, - "patternProperties": { - "^\\$": { - "type": "string" - } - }, - "anyOf": [ - { - "title": "Reference", - "required": [ - "$copy" - ] - }, - { - "title": "Type", - "required": [ - "property", - "$type" - ] - } - ] - }, - "Microsoft.DimensionEntityRecognizer": { - "$role": "unionType(Microsoft.EntityRecognizers)", - "title": "Dimension Entity Recognizer", - "description": "Recognizer which recognizes dimension.", - "type": "object", - "properties": { - "$type": { - "title": "$type", - "description": "Defines the valid properties for the component you are configuring (from a dialog .schema file)", - "type": "string", - "pattern": "^[a-zA-Z][a-zA-Z0-9.]*$", - "const": "Microsoft.DimensionEntityRecognizer" - }, - "$copy": { - "title": "$copy", - "description": "Copy the definition by id from a .dialog file.", - "type": "string", - "pattern": "^(([a-zA-Z][a-zA-Z0-9.]*)?(#[a-zA-Z][a-zA-Z0-9.]*)?)$" - }, - "$id": { - "title": "$id", - "description": "Inline id for reuse of an inline definition", - "type": "string", - "pattern": "^([a-zA-Z][a-zA-Z0-9.]*)$" - }, - "$designer": { - "title": "$designer", - "type": "object", - "description": "Extra information for the Bot Framework Designer." - } - }, - "additionalProperties": false, - "patternProperties": { - "^\\$": { - "type": "string" - } - }, - "anyOf": [ - { - "title": "Reference", - "required": [ - "$copy" - ] - }, - { - "title": "Type", - "required": [ - "$type" - ] - } - ] - }, - "Microsoft.EditActions": { - "$role": "unionType(Microsoft.IDialog)", - "title": "Edit actions.", - "description": "Edit the current list of actions.", - "type": "object", - "properties": { - "$type": { - "title": "$type", - "description": "Defines the valid properties for the component you are configuring (from a dialog .schema file)", - "type": "string", - "pattern": "^[a-zA-Z][a-zA-Z0-9.]*$", - "const": "Microsoft.EditActions" - }, - "$copy": { - "title": "$copy", - "description": "Copy the definition by id from a .dialog file.", - "type": "string", - "pattern": "^(([a-zA-Z][a-zA-Z0-9.]*)?(#[a-zA-Z][a-zA-Z0-9.]*)?)$" - }, - "$id": { - "title": "$id", - "description": "Inline id for reuse of an inline definition", - "type": "string", - "pattern": "^([a-zA-Z][a-zA-Z0-9.]*)$" - }, - "$designer": { - "title": "$designer", - "type": "object", - "description": "Extra information for the Bot Framework Designer." - }, - "changeType": { - "type": "string", - "title": "Type of change", - "description": "Type of change to apply to the current actions.", - "enum": [ - "InsertActions", - "InsertActionsBeforeTags", - "AppendActions", - "EndSequence", - "ReplaceSequence" - ] - }, - "actions": { - "type": "array", - "title": "Actions", - "description": "Actions to apply.", - "items": { - "$type": "Microsoft.IDialog", - "$ref": "#/definitions/Microsoft.IDialog" - } - } - }, - "additionalProperties": false, - "patternProperties": { - "^\\$": { - "type": "string" - } - }, - "anyOf": [ - { - "title": "Reference", - "required": [ - "$copy" - ] - }, - { - "title": "Type", - "required": [ - "changeType", - "actions", - "$type" - ] - } - ] - }, - "Microsoft.EditArray": { - "$role": "unionType(Microsoft.IDialog)", - "title": "Edit array", - "description": "Modify an array in memory", - "type": "object", - "properties": { - "$type": { - "title": "$type", - "description": "Defines the valid properties for the component you are configuring (from a dialog .schema file)", - "type": "string", - "pattern": "^[a-zA-Z][a-zA-Z0-9.]*$", - "const": "Microsoft.EditArray" - }, - "$copy": { - "title": "$copy", - "description": "Copy the definition by id from a .dialog file.", - "type": "string", - "pattern": "^(([a-zA-Z][a-zA-Z0-9.]*)?(#[a-zA-Z][a-zA-Z0-9.]*)?)$" - }, - "$id": { - "title": "$id", - "description": "Inline id for reuse of an inline definition", - "type": "string", - "pattern": "^([a-zA-Z][a-zA-Z0-9.]*)$" - }, - "$designer": { - "title": "$designer", - "type": "object", - "description": "Extra information for the Bot Framework Designer." - }, - "changeType": { - "type": "string", - "title": "Type of change", - "description": "Type of change to the array in memory.", - "enum": [ - "Push", - "Pop", - "Take", - "Remove", - "Clear" - ] - }, - "itemsProperty": { - "$role": "expression", - "title": "Items property", - "description": "Property that holds the array to update.", - "type": "string" - }, - "resultProperty": { - "$role": "expression", - "title": "Result Property", - "description": "Property to store the result of this action.", - "type": "string" - }, - "value": { - "$role": "expression", - "title": "Value", - "description": "New value or expression.", - "examples": [ - "'milk'", - "dialog.favColor", - "dialog.favColor == 'red'" - ], - "type": "string" - } - }, - "additionalProperties": false, - "patternProperties": { - "^\\$": { - "type": "string" - } - }, - "anyOf": [ - { - "title": "Reference", - "required": [ - "$copy" - ] - }, - { - "title": "Type", - "required": [ - "changeType", - "itemsProperty", - "$type" - ] - } - ] - }, - "Microsoft.EmailEntityRecognizer": { - "$role": "unionType(Microsoft.EntityRecognizers)", - "title": "Email Entity Recognizer", - "description": "Recognizer which recognizes email.", - "type": "object", - "properties": { - "$type": { - "title": "$type", - "description": "Defines the valid properties for the component you are configuring (from a dialog .schema file)", - "type": "string", - "pattern": "^[a-zA-Z][a-zA-Z0-9.]*$", - "const": "Microsoft.EmailEntityRecognizer" - }, - "$copy": { - "title": "$copy", - "description": "Copy the definition by id from a .dialog file.", - "type": "string", - "pattern": "^(([a-zA-Z][a-zA-Z0-9.]*)?(#[a-zA-Z][a-zA-Z0-9.]*)?)$" - }, - "$id": { - "title": "$id", - "description": "Inline id for reuse of an inline definition", - "type": "string", - "pattern": "^([a-zA-Z][a-zA-Z0-9.]*)$" - }, - "$designer": { - "title": "$designer", - "type": "object", - "description": "Extra information for the Bot Framework Designer." - } - }, - "additionalProperties": false, - "patternProperties": { - "^\\$": { - "type": "string" - } - }, - "anyOf": [ - { - "title": "Reference", - "required": [ - "$copy" - ] - }, - { - "title": "Type", - "required": [ - "$type" - ] - } - ] - }, - "Microsoft.EmitEvent": { - "$role": "unionType(Microsoft.IDialog)", - "title": "Emit a custom event", - "description": "Emit an event. Capture this event with a trigger.", - "type": "object", - "properties": { - "$type": { - "title": "$type", - "description": "Defines the valid properties for the component you are configuring (from a dialog .schema file)", - "type": "string", - "pattern": "^[a-zA-Z][a-zA-Z0-9.]*$", - "const": "Microsoft.EmitEvent" - }, - "$copy": { - "title": "$copy", - "description": "Copy the definition by id from a .dialog file.", - "type": "string", - "pattern": "^(([a-zA-Z][a-zA-Z0-9.]*)?(#[a-zA-Z][a-zA-Z0-9.]*)?)$" - }, - "$id": { - "title": "$id", - "description": "Inline id for reuse of an inline definition", - "type": "string", - "pattern": "^([a-zA-Z][a-zA-Z0-9.]*)$" - }, - "$designer": { - "title": "$designer", - "type": "object", - "description": "Extra information for the Bot Framework Designer." - }, - "eventName": { - "title": "Event name", - "description": "Name of the event to emit.", - "anyOf": [ - { - "enum": [ - "beginDialog", - "resumeDialog", - "repromptDialog", - "cancelDialog", - "endDialog", - "activityReceived", - "recognizedIntent", - "unknownIntent", - "actionsStarted", - "actionsSaved", - "actionsEnded", - "actionsResumed" - ] - }, - { - "type": "string" - } - ] - }, - "eventValue": { - "type": "object", - "title": "Event value", - "description": "Value to emit with the event (optional).", - "additionalProperties": true - }, - "bubbleEvent": { - "type": "boolean", - "title": "Bubble event", - "description": "If true this event is passed on to parent dialogs." - } - }, - "additionalProperties": false, - "patternProperties": { - "^\\$": { - "type": "string" - } - }, - "anyOf": [ - { - "title": "Reference", - "required": [ - "$copy" - ] - }, - { - "title": "Type", - "required": [ - "eventName", - "$type" - ] - } - ] - }, - "Microsoft.EndDialog": { - "$role": "unionType(Microsoft.IDialog)", - "title": "End dialog", - "description": "End this dialog.", - "type": "object", - "properties": { - "$type": { - "title": "$type", - "description": "Defines the valid properties for the component you are configuring (from a dialog .schema file)", - "type": "string", - "pattern": "^[a-zA-Z][a-zA-Z0-9.]*$", - "const": "Microsoft.EndDialog" - }, - "$copy": { - "title": "$copy", - "description": "Copy the definition by id from a .dialog file.", - "type": "string", - "pattern": "^(([a-zA-Z][a-zA-Z0-9.]*)?(#[a-zA-Z][a-zA-Z0-9.]*)?)$" - }, - "$id": { - "title": "$id", - "description": "Inline id for reuse of an inline definition", - "type": "string", - "pattern": "^([a-zA-Z][a-zA-Z0-9.]*)$" - }, - "$designer": { - "title": "$designer", - "type": "object", - "description": "Extra information for the Bot Framework Designer." - }, - "value": { - "$role": "expression", - "title": "Value", - "description": "Result value returned to the parent dialog.", - "examples": [ - "dialog.userName", - "'tomato'" - ], - "type": "string" - } - }, - "additionalProperties": false, - "patternProperties": { - "^\\$": { - "type": "string" - } - }, - "anyOf": [ - { - "title": "Reference", - "required": [ - "$copy" - ] - }, - { - "title": "Type", - "required": [ - "$type" - ] - } - ] - }, - "Microsoft.EndTurn": { - "$role": "unionType(Microsoft.IDialog)", - "title": "End turn", - "description": "End the current turn without ending the dialog.", - "type": "object", - "properties": { - "$type": { - "title": "$type", - "description": "Defines the valid properties for the component you are configuring (from a dialog .schema file)", - "type": "string", - "pattern": "^[a-zA-Z][a-zA-Z0-9.]*$", - "const": "Microsoft.EndTurn" - }, - "$copy": { - "title": "$copy", - "description": "Copy the definition by id from a .dialog file.", - "type": "string", - "pattern": "^(([a-zA-Z][a-zA-Z0-9.]*)?(#[a-zA-Z][a-zA-Z0-9.]*)?)$" - }, - "$id": { - "title": "$id", - "description": "Inline id for reuse of an inline definition", - "type": "string", - "pattern": "^([a-zA-Z][a-zA-Z0-9.]*)$" - }, - "$designer": { - "title": "$designer", - "type": "object", - "description": "Extra information for the Bot Framework Designer." - } - }, - "additionalProperties": false, - "patternProperties": { - "^\\$": { - "type": "string" - } - }, - "anyOf": [ - { - "title": "Reference", - "required": [ - "$copy" - ] - }, - { - "title": "Type", - "required": [ - "$type" - ] - } - ] - }, - "Microsoft.EntityRecognizers": { - "$role": "unionType", - "title": "Entity Recognizers", - "description": "Union of components which derive from EntityRecognizer abstract class.", - "type": "object", - "oneOf": [ - { - "title": "Microsoft.AgeEntityRecognizer", - "description": "Recognizer which recognizes age.", - "$ref": "#/definitions/Microsoft.AgeEntityRecognizer" - }, - { - "title": "Microsoft.ConfirmationEntityRecognizer", - "description": "Recognizer which recognizes confirmation choices (yes/no).", - "$ref": "#/definitions/Microsoft.ConfirmationEntityRecognizer" - }, - { - "title": "Microsoft.CurrencyEntityRecognizer", - "description": "Recognizer which recognizes currency.", - "$ref": "#/definitions/Microsoft.CurrencyEntityRecognizer" - }, - { - "title": "Microsoft.DateTimeEntityRecognizer", - "description": "Recognizer which recognizes dates and time fragments.", - "$ref": "#/definitions/Microsoft.DateTimeEntityRecognizer" - }, - { - "title": "Microsoft.DimensionEntityRecognizer", - "description": "Recognizer which recognizes dimension.", - "$ref": "#/definitions/Microsoft.DimensionEntityRecognizer" - }, - { - "title": "Microsoft.EmailEntityRecognizer", - "description": "Recognizer which recognizes email.", - "$ref": "#/definitions/Microsoft.EmailEntityRecognizer" - }, - { - "title": "Microsoft.GuidEntityRecognizer", - "description": "Recognizer which recognizes guids.", - "$ref": "#/definitions/Microsoft.GuidEntityRecognizer" - }, - { - "title": "Microsoft.HashtagEntityRecognizer", - "description": "Recognizer which recognizes Hashtags.", - "$ref": "#/definitions/Microsoft.HashtagEntityRecognizer" - }, - { - "title": "Microsoft.IpEntityRecognizer", - "description": "Recognizer which recognizes internet IP patterns (like 192.1.1.1).", - "$ref": "#/definitions/Microsoft.IpEntityRecognizer" - }, - { - "title": "Microsoft.MentionEntityRecognizer", - "description": "Recognizer which recognizes @Mentions", - "$ref": "#/definitions/Microsoft.MentionEntityRecognizer" - }, - { - "title": "Microsoft.NumberEntityRecognizer", - "description": "Recognizer which recognizes numbers.", - "$ref": "#/definitions/Microsoft.NumberEntityRecognizer" - }, - { - "title": "Microsoft.NumberRangeEntityRecognizer", - "description": "Recognizer which recognizes ranges of numbers (Example:2 to 5).", - "$ref": "#/definitions/Microsoft.NumberRangeEntityRecognizer" - }, - { - "title": "Microsoft.OrdinalEntityRecognizer", - "description": "Recognizer which recognizes ordinals (example: first, second, 3rd).", - "$ref": "#/definitions/Microsoft.OrdinalEntityRecognizer" - }, - { - "title": "Microsoft.PercentageEntityRecognizer", - "description": "Recognizer which recognizes percentages.", - "$ref": "#/definitions/Microsoft.PercentageEntityRecognizer" - }, - { - "title": "Microsoft.PhoneNumberEntityRecognizer", - "description": "Recognizer which recognizes phone numbers.", - "$ref": "#/definitions/Microsoft.PhoneNumberEntityRecognizer" - }, - { - "title": "Microsoft.RegExEntityRecognizer", - "description": "Recognizer which recognizes patterns of input based on regex.", - "$ref": "#/definitions/Microsoft.RegExEntityRecognizer" - }, - { - "title": "Microsoft.TemperatureEntityRecognizer", - "description": "Recognizer which recognizes temperatures.", - "$ref": "#/definitions/Microsoft.TemperatureEntityRecognizer" - }, - { - "title": "Microsoft.UrlEntityRecognizer", - "description": "Recognizer which recognizes urls (example: http://bing.com)", - "$ref": "#/definitions/Microsoft.UrlEntityRecognizer" - } - ] - }, - "Microsoft.FirstSelector": { - "$role": "unionType(Microsoft.ITriggerSelector)", - "title": "First Trigger Selector", - "description": "Selector for first true rule", - "type": "object", - "properties": { - "$type": { - "title": "$type", - "description": "Defines the valid properties for the component you are configuring (from a dialog .schema file)", - "type": "string", - "pattern": "^[a-zA-Z][a-zA-Z0-9.]*$", - "const": "Microsoft.FirstSelector" - }, - "$copy": { - "title": "$copy", - "description": "Copy the definition by id from a .dialog file.", - "type": "string", - "pattern": "^(([a-zA-Z][a-zA-Z0-9.]*)?(#[a-zA-Z][a-zA-Z0-9.]*)?)$" - }, - "$id": { - "title": "$id", - "description": "Inline id for reuse of an inline definition", - "type": "string", - "pattern": "^([a-zA-Z][a-zA-Z0-9.]*)$" - }, - "$designer": { - "title": "$designer", - "type": "object", - "description": "Extra information for the Bot Framework Designer." - } - }, - "additionalProperties": false, - "patternProperties": { - "^\\$": { - "type": "string" - } - }, - "anyOf": [ - { - "title": "Reference", - "required": [ - "$copy" - ] - }, - { - "title": "Type", - "required": [ - "$type" - ] - } - ] - }, - "Microsoft.Foreach": { - "$role": "unionType(Microsoft.IDialog)", - "title": "For each item", - "description": "Execute actions on each item in an a collection.", - "type": "object", - "properties": { - "$type": { - "title": "$type", - "description": "Defines the valid properties for the component you are configuring (from a dialog .schema file)", - "type": "string", - "pattern": "^[a-zA-Z][a-zA-Z0-9.]*$", - "const": "Microsoft.Foreach" - }, - "$copy": { - "title": "$copy", - "description": "Copy the definition by id from a .dialog file.", - "type": "string", - "pattern": "^(([a-zA-Z][a-zA-Z0-9.]*)?(#[a-zA-Z][a-zA-Z0-9.]*)?)$" - }, - "$id": { - "title": "$id", - "description": "Inline id for reuse of an inline definition", - "type": "string", - "pattern": "^([a-zA-Z][a-zA-Z0-9.]*)$" - }, - "$designer": { - "title": "$designer", - "type": "object", - "description": "Extra information for the Bot Framework Designer." - }, - "itemsProperty": { - "$role": "expression", - "title": "Items property", - "description": "Property that holds the array.", - "examples": [ - "user.todoList" - ], - "type": "string" - }, - "actions": { - "type": "array", - "title": "Actions", - "description": "Actions to execute for each item. Use '$foreach.value' to access the value of each item. Use '$foreach.index' to access the index of each item.", - "items": { - "$type": "Microsoft.IDialog", - "$ref": "#/definitions/Microsoft.IDialog" - } - } - }, - "additionalProperties": false, - "patternProperties": { - "^\\$": { - "type": "string" - } - }, - "anyOf": [ - { - "title": "Reference", - "required": [ - "$copy" - ] - }, - { - "title": "Type", - "required": [ - "itemsProperty", - "actions", - "$type" - ] - } - ] - }, - "Microsoft.ForeachPage": { - "$role": "unionType(Microsoft.IDialog)", - "title": "For each page", - "description": "Execute actions on each page (collection of items) in an array.", - "type": "object", - "properties": { - "$type": { - "title": "$type", - "description": "Defines the valid properties for the component you are configuring (from a dialog .schema file)", - "type": "string", - "pattern": "^[a-zA-Z][a-zA-Z0-9.]*$", - "const": "Microsoft.ForeachPage" - }, - "$copy": { - "title": "$copy", - "description": "Copy the definition by id from a .dialog file.", - "type": "string", - "pattern": "^(([a-zA-Z][a-zA-Z0-9.]*)?(#[a-zA-Z][a-zA-Z0-9.]*)?)$" - }, - "$id": { - "title": "$id", - "description": "Inline id for reuse of an inline definition", - "type": "string", - "pattern": "^([a-zA-Z][a-zA-Z0-9.]*)$" - }, - "$designer": { - "title": "$designer", - "type": "object", - "description": "Extra information for the Bot Framework Designer." - }, - "itemsProperty": { - "$role": "expression", - "title": "Items property", - "description": "Property that holds the array.", - "examples": [ - "user.todoList" - ], - "type": "string" - }, - "actions": { - "type": "array", - "title": "Actions", - "description": "Actions to execute for each page. Use '$foreach.page' to access each page.", - "items": { - "$type": "Microsoft.IDialog", - "$ref": "#/definitions/Microsoft.IDialog" - } - }, - "pageSize": { - "type": "integer", - "title": "Page size", - "description": "Number of items in each page.", - "default": 10 - } - }, - "additionalProperties": false, - "patternProperties": { - "^\\$": { - "type": "string" - } - }, - "anyOf": [ - { - "title": "Reference", - "required": [ - "$copy" - ] - }, - { - "title": "Type", - "required": [ - "itemsProperty", - "actions", - "$type" - ] - } - ] - }, - "Microsoft.GuidEntityRecognizer": { - "$role": "unionType(Microsoft.EntityRecognizers)", - "title": "Guid Entity Recognizer", - "description": "Recognizer which recognizes guids.", - "type": "object", - "properties": { - "$type": { - "title": "$type", - "description": "Defines the valid properties for the component you are configuring (from a dialog .schema file)", - "type": "string", - "pattern": "^[a-zA-Z][a-zA-Z0-9.]*$", - "const": "Microsoft.GuidEntityRecognizer" - }, - "$copy": { - "title": "$copy", - "description": "Copy the definition by id from a .dialog file.", - "type": "string", - "pattern": "^(([a-zA-Z][a-zA-Z0-9.]*)?(#[a-zA-Z][a-zA-Z0-9.]*)?)$" - }, - "$id": { - "title": "$id", - "description": "Inline id for reuse of an inline definition", - "type": "string", - "pattern": "^([a-zA-Z][a-zA-Z0-9.]*)$" - }, - "$designer": { - "title": "$designer", - "type": "object", - "description": "Extra information for the Bot Framework Designer." - } - }, - "additionalProperties": false, - "patternProperties": { - "^\\$": { - "type": "string" - } - }, - "anyOf": [ - { - "title": "Reference", - "required": [ - "$copy" - ] - }, - { - "title": "Type", - "required": [ - "$type" - ] - } - ] - }, - "Microsoft.HashtagEntityRecognizer": { - "$role": "unionType(Microsoft.EntityRecognizers)", - "title": "Hashtag Entity Recognizer", - "description": "Recognizer which recognizes Hashtags.", - "type": "object", - "properties": { - "$type": { - "title": "$type", - "description": "Defines the valid properties for the component you are configuring (from a dialog .schema file)", - "type": "string", - "pattern": "^[a-zA-Z][a-zA-Z0-9.]*$", - "const": "Microsoft.HashtagEntityRecognizer" - }, - "$copy": { - "title": "$copy", - "description": "Copy the definition by id from a .dialog file.", - "type": "string", - "pattern": "^(([a-zA-Z][a-zA-Z0-9.]*)?(#[a-zA-Z][a-zA-Z0-9.]*)?)$" - }, - "$id": { - "title": "$id", - "description": "Inline id for reuse of an inline definition", - "type": "string", - "pattern": "^([a-zA-Z][a-zA-Z0-9.]*)$" - }, - "$designer": { - "title": "$designer", - "type": "object", - "description": "Extra information for the Bot Framework Designer." - } - }, - "additionalProperties": false, - "patternProperties": { - "^\\$": { - "type": "string" - } - }, - "anyOf": [ - { - "title": "Reference", - "required": [ - "$copy" - ] - }, - { - "title": "Type", - "required": [ - "$type" - ] - } - ] - }, - "Microsoft.HttpRequest": { - "$role": "unionType(Microsoft.IDialog)", - "type": "object", - "title": "HTTP request", - "description": "Make a HTTP request.", - "properties": { - "$type": { - "title": "$type", - "description": "Defines the valid properties for the component you are configuring (from a dialog .schema file)", - "type": "string", - "pattern": "^[a-zA-Z][a-zA-Z0-9.]*$", - "const": "Microsoft.HttpRequest" - }, - "$copy": { - "title": "$copy", - "description": "Copy the definition by id from a .dialog file.", - "type": "string", - "pattern": "^(([a-zA-Z][a-zA-Z0-9.]*)?(#[a-zA-Z][a-zA-Z0-9.]*)?)$" - }, - "$id": { - "title": "$id", - "description": "Inline id for reuse of an inline definition", - "type": "string", - "pattern": "^([a-zA-Z][a-zA-Z0-9.]*)$" - }, - "$designer": { - "title": "$designer", - "type": "object", - "description": "Extra information for the Bot Framework Designer." - }, - "method": { - "type": "string", - "title": "HTTP method", - "description": "HTTP method to use.", - "enum": [ - "GET", - "POST", - "PATCH", - "PUT", - "DELETE" - ], - "examples": [ - "GET", - "POST" - ] - }, - "url": { - "type": "string", - "title": "Url", - "description": "URL to call (supports data binding).", - "examples": [ - "https://contoso.com" - ] - }, - "body": { - "type": "object", - "title": "Body", - "description": "Body to include in the HTTP call (supports data binding).", - "additionalProperties": true - }, - "resultProperty": { - "$role": "expression", - "title": "Result property", - "description": "Property to store the result of this action. The result includes 4 properties from the http response: statusCode, reasonPhrase, content and headers. If the content is json it will be a deserialized object.", - "examples": [ - "dialog.contosodata" - ], - "type": "string" - }, - "headers": { - "type": "object", - "additionProperties": true, - "title": "Headers", - "description": "One or more headers to include in the request (supports data binding)." - }, - "responseType": { - "type": "string", - "title": "Response type", - "description": "Defines the type of HTTP response. Automatically calls the 'Send a response' action if set to 'Activity' or 'Activities'.", - "enum": [ - "None", - "Json", - "Activity", - "Activities" - ], - "default": "Json" - } - }, - "additionalProperties": false, - "patternProperties": { - "^\\$": { - "type": "string" - } - }, - "anyOf": [ - { - "title": "Reference", - "required": [ - "$copy" - ] - }, - { - "title": "Type", - "required": [ - "url", - "method", - "$type" - ] - } - ] - }, - "Microsoft.IActivityTemplate": { - "title": "Microsoft ActivityTemplates", - "description": "Components which are IActivityTemplates", - "$role": "unionType", - "oneOf": [ - { - "title": "Microsoft.ActivityTemplate", - "description": "", - "$ref": "#/definitions/Microsoft.ActivityTemplate" - }, - { - "title": "Microsoft.StaticActivityTemplate", - "description": "This allows you to define a static Activity object", - "$ref": "#/definitions/Microsoft.StaticActivityTemplate" - }, - { - "type": "string", - "title": "string" - } - ] - }, - "Microsoft.IDialog": { - "title": "Microsoft Dialogs", - "description": "Union of components which implement the Dialog contract", - "$role": "unionType", - "oneOf": [ - { - "title": "Microsoft.AdaptiveDialog", - "description": "Flexible, data driven dialog that can adapt to the conversation.", - "$ref": "#/definitions/Microsoft.AdaptiveDialog" - }, - { - "title": "Microsoft.AttachmentInput", - "description": "Collect information - Ask for a file or image.", - "$ref": "#/definitions/Microsoft.AttachmentInput" - }, - { - "title": "Microsoft.BeginDialog", - "description": "Begin another dialog.", - "$ref": "#/definitions/Microsoft.BeginDialog" - }, - { - "title": "Microsoft.CancelAllDialogs", - "description": "Cancel all active dialogs. All dialogs in the dialog chain will need a trigger to capture the event configured in this action.", - "$ref": "#/definitions/Microsoft.CancelAllDialogs" - }, - { - "title": "Microsoft.ChoiceInput", - "description": "Collect information - Pick from a list of choices", - "$ref": "#/definitions/Microsoft.ChoiceInput" - }, - { - "title": "Microsoft.ConfirmInput", - "description": "Collect information - Ask for confirmation (yes or no).", - "$ref": "#/definitions/Microsoft.ConfirmInput" - }, - { - "title": "Microsoft.DateTimeInput", - "description": "Collect information - Ask for date and/ or time", - "$ref": "#/definitions/Microsoft.DateTimeInput" - }, - { - "title": "Microsoft.DebugBreak", - "description": "If debugger is attached, stop the execution at this point in the conversation.", - "$ref": "#/definitions/Microsoft.DebugBreak" - }, - { - "title": "Microsoft.DeleteProperty", - "description": "Delete a property and any value it holds.", - "$ref": "#/definitions/Microsoft.DeleteProperty" - }, - { - "title": "Microsoft.EditActions", - "description": "Edit the current list of actions.", - "$ref": "#/definitions/Microsoft.EditActions" - }, - { - "title": "Microsoft.EditArray", - "description": "Modify an array in memory", - "$ref": "#/definitions/Microsoft.EditArray" - }, - { - "title": "Microsoft.EmitEvent", - "description": "Emit an event. Capture this event with a trigger.", - "$ref": "#/definitions/Microsoft.EmitEvent" - }, - { - "title": "Microsoft.EndDialog", - "description": "End this dialog.", - "$ref": "#/definitions/Microsoft.EndDialog" - }, - { - "title": "Microsoft.EndTurn", - "description": "End the current turn without ending the dialog.", - "$ref": "#/definitions/Microsoft.EndTurn" - }, - { - "title": "Microsoft.Foreach", - "description": "Execute actions on each item in an a collection.", - "$ref": "#/definitions/Microsoft.Foreach" - }, - { - "title": "Microsoft.ForeachPage", - "description": "Execute actions on each page (collection of items) in an array.", - "$ref": "#/definitions/Microsoft.ForeachPage" - }, - { - "title": "Microsoft.HttpRequest", - "description": "Make a HTTP request.", - "$ref": "#/definitions/Microsoft.HttpRequest" - }, - { - "title": "Microsoft.IfCondition", - "description": "Two-way branch the conversation flow based on a condition.", - "$ref": "#/definitions/Microsoft.IfCondition" - }, - { - "title": "Microsoft.InitProperty", - "description": "Define and initialize a property to be an array or object.", - "$ref": "#/definitions/Microsoft.InitProperty" - }, - { - "title": "Microsoft.LogAction", - "description": "Log a message to the host application. Send a TraceActivity to Bot Framework Emulator (optional).", - "$ref": "#/definitions/Microsoft.LogAction" - }, - { - "title": "Microsoft.NumberInput", - "description": "Collect information - Ask for a number.", - "$ref": "#/definitions/Microsoft.NumberInput" - }, - { - "title": "Microsoft.OAuthInput", - "description": "Collect login information.", - "$ref": "#/definitions/Microsoft.OAuthInput" - }, - { - "title": "Microsoft.QnAMakerDialog", - "description": "Dialog which uses QnAMAker knowledge base to answer questions.", - "$ref": "#/definitions/Microsoft.QnAMakerDialog" - }, - { - "title": "Microsoft.RepeatDialog", - "description": "Repeat current dialog.", - "$ref": "#/definitions/Microsoft.RepeatDialog" - }, - { - "title": "Microsoft.ReplaceDialog", - "description": "Replace current dialog with another dialog.", - "$ref": "#/definitions/Microsoft.ReplaceDialog" - }, - { - "title": "Microsoft.SendActivity", - "description": "Respond with an activity.", - "$ref": "#/definitions/Microsoft.SendActivity" - }, - { - "title": "Microsoft.SetProperty", - "description": "Set property to a value.", - "$ref": "#/definitions/Microsoft.SetProperty" - }, - { - "title": "Microsoft.SwitchCondition", - "description": "Execute different actions based on the value of a property.", - "$ref": "#/definitions/Microsoft.SwitchCondition" - }, - { - "title": "Microsoft.TextInput", - "description": "Collection information - Ask for a word or sentence.", - "$ref": "#/definitions/Microsoft.TextInput" - }, - { - "title": "Microsoft.TraceActivity", - "description": "Send a trace activity to the transcript logger and/ or Bot Framework Emulator.", - "$ref": "#/definitions/Microsoft.TraceActivity" - }, - { - "type": "string", - "title": "string" - } - ] - }, - "Microsoft.ILanguageGenerator": { - "title": "Microsoft ILanguageGenerator", - "description": "Union of components which implement the ILanguageGenerator interface", - "$role": "unionType", - "oneOf": [ - { - "type": "string", - "title": "string" - } - ] - }, - "Microsoft.IRecognizer": { - "title": "Microsoft IRecognizer", - "description": "Union of components which implement the IRecognizer interface", - "$role": "unionType", - "oneOf": [ - { - "title": "Microsoft.LuisRecognizer", - "description": "LUIS recognizer.", - "$ref": "#/definitions/Microsoft.LuisRecognizer" - }, - { - "title": "Microsoft.MultiLanguageRecognizer", - "description": "Configure one recognizer per language and the specify the language fallback policy.", - "$ref": "#/definitions/Microsoft.MultiLanguageRecognizer" - }, - { - "title": "Microsoft.RegexRecognizer", - "description": "Use regular expressions to recognize intents and entities from user input.", - "$ref": "#/definitions/Microsoft.RegexRecognizer" - }, - { - "type": "string", - "title": "string" - } - ] - }, - "Microsoft.ITextTemplate": { - "title": "Microsoft TextTemplate", - "description": "Union of components which implement the TextTemplate", - "$role": "unionType", - "oneOf": [ - { - "title": "Microsoft.TextTemplate", - "description": "Lg tempalte to evaluate to create text", - "$ref": "#/definitions/Microsoft.TextTemplate" - }, - { - "type": "string", - "title": "string" - } - ] - }, - "Microsoft.ITriggerCondition": { - "$role": "unionType", - "title": "Microsoft Triggers", - "description": "Union of components which implement the OnCondition", - "oneOf": [ - { - "title": "Microsoft.OnActivity", - "description": "Actions to perform on receipt of a generic activity.", - "$ref": "#/definitions/Microsoft.OnActivity" - }, - { - "title": "Microsoft.OnBeginDialog", - "description": "Actions to perform when this dialog begins.", - "$ref": "#/definitions/Microsoft.OnBeginDialog" - }, - { - "title": "Microsoft.OnCancelDialog", - "description": "Actions to perform on cancel dialog event.", - "$ref": "#/definitions/Microsoft.OnCancelDialog" - }, - { - "title": "Microsoft.OnCondition", - "description": "Actions to perform when specified condition is true.", - "$ref": "#/definitions/Microsoft.OnCondition" - }, - { - "title": "Microsoft.OnConversationUpdateActivity", - "description": "Actions to perform on receipt of an activity with type 'ConversationUpdate'.", - "$ref": "#/definitions/Microsoft.OnConversationUpdateActivity" - }, - { - "title": "Microsoft.OnCustomEvent", - "description": "Actions to perform when a custom event is detected. Use 'Emit a custom event' action to raise a custom event.", - "$ref": "#/definitions/Microsoft.OnCustomEvent" - }, - { - "title": "Microsoft.OnDialogEvent", - "description": "Actions to perform when a specific dialog event occurs.", - "$ref": "#/definitions/Microsoft.OnDialogEvent" - }, - { - "title": "Microsoft.OnEndOfConversationActivity", - "description": "Actions to perform on receipt of an activity with type 'EndOfConversation'.", - "$ref": "#/definitions/Microsoft.OnEndOfConversationActivity" - }, - { - "title": "Microsoft.OnError", - "description": "Action to perform when an 'Error' dialog event occurs.", - "$ref": "#/definitions/Microsoft.OnError" - }, - { - "title": "Microsoft.OnEventActivity", - "description": "Actions to perform on receipt of an activity with type 'Event'.", - "$ref": "#/definitions/Microsoft.OnEventActivity" - }, - { - "title": "Microsoft.OnHandoffActivity", - "description": "Actions to perform on receipt of an activity with type 'HandOff'.", - "$ref": "#/definitions/Microsoft.OnHandoffActivity" - }, - { - "title": "Microsoft.OnIntent", - "description": "Actions to perform when specified intent is recognized.", - "$ref": "#/definitions/Microsoft.OnIntent" - }, - { - "title": "Microsoft.OnInvokeActivity", - "description": "Actions to perform on receipt of an activity with type 'Invoke'.", - "$ref": "#/definitions/Microsoft.OnInvokeActivity" - }, - { - "title": "Microsoft.OnMessageActivity", - "description": "Actions to perform on receipt of an activity with type 'Message'. Overrides Intent trigger.", - "$ref": "#/definitions/Microsoft.OnMessageActivity" - }, - { - "title": "Microsoft.OnMessageDeleteActivity", - "description": "Actions to perform on receipt of an activity with type 'MessageDelete'.", - "$ref": "#/definitions/Microsoft.OnMessageDeleteActivity" - }, - { - "title": "Microsoft.OnMessageReactionActivity", - "description": "Actions to perform on receipt of an activity with type 'MessageReaction'.", - "$ref": "#/definitions/Microsoft.OnMessageReactionActivity" - }, - { - "title": "Microsoft.OnMessageUpdateActivity", - "description": "Actions to perform on receipt of an activity with type 'MessageUpdate'.", - "$ref": "#/definitions/Microsoft.OnMessageUpdateActivity" - }, - { - "title": "Microsoft.OnRepromptDialog", - "description": "Actions to perform when 'RepromptDialog' event occurs.", - "$ref": "#/definitions/Microsoft.OnRepromptDialog" - }, - { - "title": "Microsoft.OnTypingActivity", - "description": "Actions to perform on receipt of an activity with type 'Typing'.", - "$ref": "#/definitions/Microsoft.OnTypingActivity" - }, - { - "title": "Microsoft.OnUnknownIntent", - "description": "Action to perform when user input is unrecognized and if none of the 'on intent recognition' triggers match recognized intent.", - "$ref": "#/definitions/Microsoft.OnUnknownIntent" - } - ] - }, - "Microsoft.ITriggerSelector": { - "$role": "unionType", - "title": "Selectors", - "description": "Union of components which are trigger selectors", - "oneOf": [ - { - "title": "Microsoft.ConditionalSelector", - "description": "Use a rule selector based on a condition", - "$ref": "#/definitions/Microsoft.ConditionalSelector" - }, - { - "title": "Microsoft.FirstSelector", - "description": "Selector for first true rule", - "$ref": "#/definitions/Microsoft.FirstSelector" - }, - { - "title": "Microsoft.MostSpecificSelector", - "description": "Select most specific true events with optional additional selector", - "$ref": "#/definitions/Microsoft.MostSpecificSelector" - }, - { - "title": "Microsoft.RandomSelector", - "description": "Select most specific true rule", - "$ref": "#/definitions/Microsoft.RandomSelector" - }, - { - "title": "Microsoft.TrueSelector", - "description": "Selector for all true events", - "$ref": "#/definitions/Microsoft.TrueSelector" - } - ] - }, - "Microsoft.IfCondition": { - "$role": "unionType(Microsoft.IDialog)", - "title": "If condition", - "description": "Two-way branch the conversation flow based on a condition.", - "type": "object", - "properties": { - "$type": { - "title": "$type", - "description": "Defines the valid properties for the component you are configuring (from a dialog .schema file)", - "type": "string", - "pattern": "^[a-zA-Z][a-zA-Z0-9.]*$", - "const": "Microsoft.IfCondition" - }, - "$copy": { - "title": "$copy", - "description": "Copy the definition by id from a .dialog file.", - "type": "string", - "pattern": "^(([a-zA-Z][a-zA-Z0-9.]*)?(#[a-zA-Z][a-zA-Z0-9.]*)?)$" - }, - "$id": { - "title": "$id", - "description": "Inline id for reuse of an inline definition", - "type": "string", - "pattern": "^([a-zA-Z][a-zA-Z0-9.]*)$" - }, - "$designer": { - "title": "$designer", - "type": "object", - "description": "Extra information for the Bot Framework Designer." - }, - "condition": { - "$role": "expression", - "title": "Condition", - "description": "Expression to evaluate.", - "examples": [ - "user.age > 3" - ], - "type": "string" - }, - "actions": { - "type": "array", - "title": "Actions", - "description": "Actions to execute if condition is true.", - "items": { - "$type": "Microsoft.IDialog", - "$ref": "#/definitions/Microsoft.IDialog" - } - }, - "elseActions": { - "type": "array", - "title": "Else", - "description": "Actions to execute if condition is false.", - "items": { - "$type": "Microsoft.IDialog", - "$ref": "#/definitions/Microsoft.IDialog" - } - } - }, - "additionalProperties": false, - "patternProperties": { - "^\\$": { - "type": "string" - } - }, - "anyOf": [ - { - "title": "Reference", - "required": [ - "$copy" - ] - }, - { - "title": "Type", - "required": [ - "condition", - "actions", - "$type" - ] - } - ] - }, - "Microsoft.InitProperty": { - "$role": "unionType(Microsoft.IDialog)", - "title": "Initialize property", - "description": "Define and initialize a property to be an array or object.", - "type": "object", - "properties": { - "$type": { - "title": "$type", - "description": "Defines the valid properties for the component you are configuring (from a dialog .schema file)", - "type": "string", - "pattern": "^[a-zA-Z][a-zA-Z0-9.]*$", - "const": "Microsoft.InitProperty" - }, - "$copy": { - "title": "$copy", - "description": "Copy the definition by id from a .dialog file.", - "type": "string", - "pattern": "^(([a-zA-Z][a-zA-Z0-9.]*)?(#[a-zA-Z][a-zA-Z0-9.]*)?)$" - }, - "$id": { - "title": "$id", - "description": "Inline id for reuse of an inline definition", - "type": "string", - "pattern": "^([a-zA-Z][a-zA-Z0-9.]*)$" - }, - "$designer": { - "title": "$designer", - "type": "object", - "description": "Extra information for the Bot Framework Designer." - }, - "property": { - "$role": "expression", - "title": "Property", - "description": "Property (named location to store information).", - "examples": [ - "user.age" - ], - "type": "string" - }, - "type": { - "type": "string", - "title": "Type", - "description": "Type of value.", - "enum": [ - "object", - "array" - ] - } - }, - "additionalProperties": false, - "patternProperties": { - "^\\$": { - "type": "string" - } - }, - "anyOf": [ - { - "title": "Reference", - "required": [ - "$copy" - ] - }, - { - "title": "Type", - "required": [ - "property", - "type", - "$type" - ] - } - ] - }, - "Microsoft.IpEntityRecognizer": { - "$role": "unionType(Microsoft.EntityRecognizers)", - "title": "Ip Entity Recognizer", - "description": "Recognizer which recognizes internet IP patterns (like 192.1.1.1).", - "type": "object", - "properties": { - "$type": { - "title": "$type", - "description": "Defines the valid properties for the component you are configuring (from a dialog .schema file)", - "type": "string", - "pattern": "^[a-zA-Z][a-zA-Z0-9.]*$", - "const": "Microsoft.IpEntityRecognizer" - }, - "$copy": { - "title": "$copy", - "description": "Copy the definition by id from a .dialog file.", - "type": "string", - "pattern": "^(([a-zA-Z][a-zA-Z0-9.]*)?(#[a-zA-Z][a-zA-Z0-9.]*)?)$" - }, - "$id": { - "title": "$id", - "description": "Inline id for reuse of an inline definition", - "type": "string", - "pattern": "^([a-zA-Z][a-zA-Z0-9.]*)$" - }, - "$designer": { - "title": "$designer", - "type": "object", - "description": "Extra information for the Bot Framework Designer." - } - }, - "additionalProperties": false, - "patternProperties": { - "^\\$": { - "type": "string" - } - }, - "anyOf": [ - { - "title": "Reference", - "required": [ - "$copy" - ] - }, - { - "title": "Type", - "required": [ - "$type" - ] - } - ] - }, - "Microsoft.LanguagePolicy": { - "title": "Language Policy", - "description": "This represents a policy map for locales lookups to use for language", - "type": "object", - "additionalProperties": false, - "properties": { - "$type": { - "title": "$type", - "description": "Defines the valid properties for the component you are configuring (from a dialog .schema file)", - "type": "string", - "pattern": "^[a-zA-Z][a-zA-Z0-9.]*$", - "const": "Microsoft.LanguagePolicy" - }, - "$copy": { - "title": "$copy", - "description": "Copy the definition by id from a .dialog file.", - "type": "string", - "pattern": "^(([a-zA-Z][a-zA-Z0-9.]*)?(#[a-zA-Z][a-zA-Z0-9.]*)?)$" - }, - "$id": { - "title": "$id", - "description": "Inline id for reuse of an inline definition", - "type": "string", - "pattern": "^([a-zA-Z][a-zA-Z0-9.]*)$" - }, - "$designer": { - "title": "$designer", - "type": "object", - "description": "Extra information for the Bot Framework Designer." - } - }, - "patternProperties": { - "^\\$": { - "type": "string" - } - }, - "anyOf": [ - { - "title": "Reference", - "required": [ - "$copy" - ] - }, - { - "title": "Type", - "required": [ - "$type" - ] - } - ] - }, - "Microsoft.LogAction": { - "$role": "unionType(Microsoft.IDialog)", - "title": "Log to console", - "description": "Log a message to the host application. Send a TraceActivity to Bot Framework Emulator (optional).", - "type": "object", - "properties": { - "$type": { - "title": "$type", - "description": "Defines the valid properties for the component you are configuring (from a dialog .schema file)", - "type": "string", - "pattern": "^[a-zA-Z][a-zA-Z0-9.]*$", - "const": "Microsoft.LogAction" - }, - "$copy": { - "title": "$copy", - "description": "Copy the definition by id from a .dialog file.", - "type": "string", - "pattern": "^(([a-zA-Z][a-zA-Z0-9.]*)?(#[a-zA-Z][a-zA-Z0-9.]*)?)$" - }, - "$id": { - "title": "$id", - "description": "Inline id for reuse of an inline definition", - "type": "string", - "pattern": "^([a-zA-Z][a-zA-Z0-9.]*)$" - }, - "$designer": { - "title": "$designer", - "type": "object", - "description": "Extra information for the Bot Framework Designer." - }, - "text": { - "type": "string", - "title": "Text", - "description": "Information to log." - }, - "traceActivity": { - "type": "boolean", - "title": "Send Trace Activity", - "description": "If true, automatically sends a TraceActivity (view in Bot Framework Emulator).", - "default": false - } - }, - "additionalProperties": false, - "patternProperties": { - "^\\$": { - "type": "string" - } - }, - "anyOf": [ - { - "title": "Reference", - "required": [ - "$copy" - ] - }, - { - "title": "Type", - "required": [ - "text", - "$type" - ] - } - ] - }, - "Microsoft.LuisRecognizer": { - "$role": "unionType(Microsoft.IRecognizer)", - "title": "LUIS Recognizer", - "description": "LUIS recognizer.", - "type": "object", - "additionalProperties": false, - "properties": { - "$type": { - "title": "$type", - "description": "Defines the valid properties for the component you are configuring (from a dialog .schema file)", - "type": "string", - "pattern": "^[a-zA-Z][a-zA-Z0-9.]*$", - "const": "Microsoft.LuisRecognizer" - }, - "$copy": { - "title": "$copy", - "description": "Copy the definition by id from a .dialog file.", - "type": "string", - "pattern": "^(([a-zA-Z][a-zA-Z0-9.]*)?(#[a-zA-Z][a-zA-Z0-9.]*)?)$" - }, - "$id": { - "title": "$id", - "description": "Inline id for reuse of an inline definition", - "type": "string", - "pattern": "^([a-zA-Z][a-zA-Z0-9.]*)$" - }, - "$designer": { - "title": "$designer", - "type": "object", - "description": "Extra information for the Bot Framework Designer." - }, - "applicationId": { - "type": "string" - }, - "endpoint": { - "type": "string" - }, - "endpointKey": { - "type": "string" - } - }, - "patternProperties": { - "^\\$": { - "type": "string" - } - }, - "anyOf": [ - { - "title": "Reference", - "required": [ - "$copy" - ] - }, - { - "title": "Type", - "required": [ - "applicationId", - "endpoint", - "endpointKey", - "$type" - ] - } - ] - }, - "Microsoft.MentionEntityRecognizer": { - "$role": "unionType(Microsoft.EntityRecognizers)", - "title": "Mentions Entity Recognizer", - "description": "Recognizer which recognizes @Mentions", - "type": "object", - "properties": { - "$type": { - "title": "$type", - "description": "Defines the valid properties for the component you are configuring (from a dialog .schema file)", - "type": "string", - "pattern": "^[a-zA-Z][a-zA-Z0-9.]*$", - "const": "Microsoft.MentionEntityRecognizer" - }, - "$copy": { - "title": "$copy", - "description": "Copy the definition by id from a .dialog file.", - "type": "string", - "pattern": "^(([a-zA-Z][a-zA-Z0-9.]*)?(#[a-zA-Z][a-zA-Z0-9.]*)?)$" - }, - "$id": { - "title": "$id", - "description": "Inline id for reuse of an inline definition", - "type": "string", - "pattern": "^([a-zA-Z][a-zA-Z0-9.]*)$" - }, - "$designer": { - "title": "$designer", - "type": "object", - "description": "Extra information for the Bot Framework Designer." - } - }, - "additionalProperties": false, - "patternProperties": { - "^\\$": { - "type": "string" - } - }, - "anyOf": [ - { - "title": "Reference", - "required": [ - "$copy" - ] - }, - { - "title": "Type", - "required": [ - "$type" - ] - } - ] - }, - "Microsoft.MostSpecificSelector": { - "$role": "unionType(Microsoft.ITriggerSelector)", - "title": "Most Specific Trigger Selector", - "description": "Select most specific true events with optional additional selector", - "type": "object", - "properties": { - "$type": { - "title": "$type", - "description": "Defines the valid properties for the component you are configuring (from a dialog .schema file)", - "type": "string", - "pattern": "^[a-zA-Z][a-zA-Z0-9.]*$", - "const": "Microsoft.MostSpecificSelector" - }, - "$copy": { - "title": "$copy", - "description": "Copy the definition by id from a .dialog file.", - "type": "string", - "pattern": "^(([a-zA-Z][a-zA-Z0-9.]*)?(#[a-zA-Z][a-zA-Z0-9.]*)?)$" - }, - "$id": { - "title": "$id", - "description": "Inline id for reuse of an inline definition", - "type": "string", - "pattern": "^([a-zA-Z][a-zA-Z0-9.]*)$" - }, - "$designer": { - "title": "$designer", - "type": "object", - "description": "Extra information for the Bot Framework Designer." - }, - "selector": { - "$type": "Microsoft.ITriggerSelector", - "$ref": "#/definitions/Microsoft.ITriggerSelector" - } - }, - "additionalProperties": false, - "patternProperties": { - "^\\$": { - "type": "string" - } - }, - "anyOf": [ - { - "title": "Reference", - "required": [ - "$copy" - ] - }, - { - "title": "Type", - "required": [ - "$type" - ] - } - ] - }, - "Microsoft.MultiLanguageRecognizer": { - "$role": "unionType(Microsoft.IRecognizer)", - "title": "Multi-language recognizer", - "description": "Configure one recognizer per language and the specify the language fallback policy.", - "type": "object", - "properties": { - "$type": { - "title": "$type", - "description": "Defines the valid properties for the component you are configuring (from a dialog .schema file)", - "type": "string", - "pattern": "^[a-zA-Z][a-zA-Z0-9.]*$", - "const": "Microsoft.MultiLanguageRecognizer" - }, - "$copy": { - "title": "$copy", - "description": "Copy the definition by id from a .dialog file.", - "type": "string", - "pattern": "^(([a-zA-Z][a-zA-Z0-9.]*)?(#[a-zA-Z][a-zA-Z0-9.]*)?)$" - }, - "$id": { - "title": "$id", - "description": "Inline id for reuse of an inline definition", - "type": "string", - "pattern": "^([a-zA-Z][a-zA-Z0-9.]*)$" - }, - "$designer": { - "title": "$designer", - "type": "object", - "description": "Extra information for the Bot Framework Designer." - }, - "languagePolicy": { - "$type": "Microsoft.LanguagePolicy", - "type": "object", - "title": "Language policy", - "description": "Defines fall back languages to try per user input language.", - "$ref": "#/definitions/Microsoft.LanguagePolicy" - }, - "recognizers": { - "type": "object", - "title": "Recognizers", - "description": "Map of language -> IRecognizer", - "additionalProperties": { - "$type": "Microsoft.IRecognizer", - "$ref": "#/definitions/Microsoft.IRecognizer" - } - } - }, - "additionalProperties": false, - "patternProperties": { - "^\\$": { - "type": "string" - } - }, - "anyOf": [ - { - "title": "Reference", - "required": [ - "$copy" - ] - }, - { - "title": "Type", - "required": [ - "recognizers", - "$type" - ] - } - ] - }, - "Microsoft.NumberEntityRecognizer": { - "$role": "unionType(Microsoft.EntityRecognizers)", - "title": "Number Entity Recognizer", - "description": "Recognizer which recognizes numbers.", - "type": "object", - "properties": { - "$type": { - "title": "$type", - "description": "Defines the valid properties for the component you are configuring (from a dialog .schema file)", - "type": "string", - "pattern": "^[a-zA-Z][a-zA-Z0-9.]*$", - "const": "Microsoft.NumberEntityRecognizer" - }, - "$copy": { - "title": "$copy", - "description": "Copy the definition by id from a .dialog file.", - "type": "string", - "pattern": "^(([a-zA-Z][a-zA-Z0-9.]*)?(#[a-zA-Z][a-zA-Z0-9.]*)?)$" - }, - "$id": { - "title": "$id", - "description": "Inline id for reuse of an inline definition", - "type": "string", - "pattern": "^([a-zA-Z][a-zA-Z0-9.]*)$" - }, - "$designer": { - "title": "$designer", - "type": "object", - "description": "Extra information for the Bot Framework Designer." - } - }, - "additionalProperties": false, - "patternProperties": { - "^\\$": { - "type": "string" - } - }, - "anyOf": [ - { - "title": "Reference", - "required": [ - "$copy" - ] - }, - { - "title": "Type", - "required": [ - "$type" - ] - } - ] - }, - "Microsoft.NumberInput": { - "$role": "unionType(Microsoft.IDialog)", - "title": "Number input dialog", - "description": "Collect information - Ask for a number.", - "type": "object", - "properties": { - "$type": { - "title": "$type", - "description": "Defines the valid properties for the component you are configuring (from a dialog .schema file)", - "type": "string", - "pattern": "^[a-zA-Z][a-zA-Z0-9.]*$", - "const": "Microsoft.NumberInput" - }, - "$copy": { - "title": "$copy", - "description": "Copy the definition by id from a .dialog file.", - "type": "string", - "pattern": "^(([a-zA-Z][a-zA-Z0-9.]*)?(#[a-zA-Z][a-zA-Z0-9.]*)?)$" - }, - "$id": { - "title": "$id", - "description": "Inline id for reuse of an inline definition", - "type": "string", - "pattern": "^([a-zA-Z][a-zA-Z0-9.]*)$" - }, - "$designer": { - "title": "$designer", - "type": "object", - "description": "Extra information for the Bot Framework Designer." - }, - "id": { - "type": "string", - "title": "Id", - "description": "Optional id for the dialog" - }, - "prompt": { - "$type": "Microsoft.IActivityTemplate", - "title": "Initial prompt", - "description": "Message to send to collect information.", - "examples": [ - "What is your birth date?" - ], - "$ref": "#/definitions/Microsoft.IActivityTemplate" - }, - "unrecognizedPrompt": { - "$type": "Microsoft.IActivityTemplate", - "title": "Unrecognized prompt", - "description": "Message to send if user response is not recognized.", - "examples": [ - "Sorry, I do not understand '{turn.activity.text'}. Let's try again. What is your birth date?" - ], - "$ref": "#/definitions/Microsoft.IActivityTemplate" - }, - "invalidPrompt": { - "$type": "Microsoft.IActivityTemplate", - "title": "Invalid prompt", - "description": "Message to send if user response is invalid. Relies on specified validation expressions.", - "examples": [ - "Sorry, '{this.value}' does not work. I need a number between 1-150. What is your age?" - ], - "$ref": "#/definitions/Microsoft.IActivityTemplate" - }, - "defaultValueResponse": { - "$type": "Microsoft.IActivityTemplate", - "title": "Default value response", - "description": "Message to send when max turn count (if specified) has been exceeded and the default value is selected as the value.", - "examples": [ - "Sorry, I'm having trouble understanding you. I will just use {this.options.defaultValue} for now. You can say 'I'm 36 years old' to change it." - ], - "$ref": "#/definitions/Microsoft.IActivityTemplate" - }, - "maxTurnCount": { - "type": "integer", - "title": "Max turn count", - "description": "Maximum number of re-prompt attempts to collect information.", - "default": 3, - "examples": [ - 3 - ] - }, - "validations": { - "type": "array", - "title": "Validation expressions", - "description": "Expression to validate user input.", - "examples": [ - "int(this.value) > 1 && int(this.value) <= 150", - "count(this.value) < 300" - ], - "items": { - "$role": "expression", - "type": "string", - "description": "String must contain an expression." - } - }, - "property": { - "$role": "expression", - "title": "Property", - "description": "Property to store collected information. Input will be skipped if property has value (unless 'Always prompt' is true).", - "examples": [ - "$birthday", - "user.name", - "conversation.issueTitle", - "dialog.favColor" - ], - "type": "string" - }, - "defaultValue": { - "$role": "expression", - "title": "Default value", - "description": "Expression to examine on each turn of the conversation as possible value to the property.", - "examples": [ - "@userName", - "coalesce(@number, @partySize)" - ], - "type": "string" - }, - "alwaysPrompt": { - "type": "boolean", - "title": "Always prompt", - "description": "Collect information even if the specified 'property' is not empty.", - "default": false, - "examples": [ - false - ] - }, - "allowInterruptions": { - "type": "string", - "title": "Allow Interruptions", - "description": "A boolean expression that determines whether the parent should be allowed to interrupt the input.", - "default": "true", - "examples": [ - "true" - ] - }, - "outputFormat": { - "type": "string", - "enum": [ - "float", - "integer" - ], - "title": "Output format", - "description": "Number output format.", - "default": "float" - }, - "defaultLocale": { - "type": "string", - "title": "Default locale", - "description": "Default locale.", - "default": "en-us" - } - }, - "additionalProperties": false, - "patternProperties": { - "^\\$": { - "type": "string" - } - }, - "anyOf": [ - { - "title": "Reference", - "required": [ - "$copy" - ] - }, - { - "title": "Type", - "required": [ - "$type" - ] - } - ] - }, - "Microsoft.NumberRangeEntityRecognizer": { - "$role": "unionType(Microsoft.EntityRecognizers)", - "title": "NumberRange Entity Recognizer", - "description": "Recognizer which recognizes ranges of numbers (Example:2 to 5).", - "type": "object", - "properties": { - "$type": { - "title": "$type", - "description": "Defines the valid properties for the component you are configuring (from a dialog .schema file)", - "type": "string", - "pattern": "^[a-zA-Z][a-zA-Z0-9.]*$", - "const": "Microsoft.NumberRangeEntityRecognizer" - }, - "$copy": { - "title": "$copy", - "description": "Copy the definition by id from a .dialog file.", - "type": "string", - "pattern": "^(([a-zA-Z][a-zA-Z0-9.]*)?(#[a-zA-Z][a-zA-Z0-9.]*)?)$" - }, - "$id": { - "title": "$id", - "description": "Inline id for reuse of an inline definition", - "type": "string", - "pattern": "^([a-zA-Z][a-zA-Z0-9.]*)$" - }, - "$designer": { - "title": "$designer", - "type": "object", - "description": "Extra information for the Bot Framework Designer." - } - }, - "additionalProperties": false, - "patternProperties": { - "^\\$": { - "type": "string" - } - }, - "anyOf": [ - { - "title": "Reference", - "required": [ - "$copy" - ] - }, - { - "title": "Type", - "required": [ - "$type" - ] - } - ] - }, - "Microsoft.OAuthInput": { - "$role": "unionType(Microsoft.IDialog)", - "title": "OAuthInput Dialog", - "description": "Collect login information.", - "type": "object", - "properties": { - "$type": { - "title": "$type", - "description": "Defines the valid properties for the component you are configuring (from a dialog .schema file)", - "type": "string", - "pattern": "^[a-zA-Z][a-zA-Z0-9.]*$", - "const": "Microsoft.OAuthInput" - }, - "$copy": { - "title": "$copy", - "description": "Copy the definition by id from a .dialog file.", - "type": "string", - "pattern": "^(([a-zA-Z][a-zA-Z0-9.]*)?(#[a-zA-Z][a-zA-Z0-9.]*)?)$" - }, - "$id": { - "title": "$id", - "description": "Inline id for reuse of an inline definition", - "type": "string", - "pattern": "^([a-zA-Z][a-zA-Z0-9.]*)$" - }, - "$designer": { - "title": "$designer", - "type": "object", - "description": "Extra information for the Bot Framework Designer." - }, - "connectionName": { - "type": "string", - "title": "Connection name", - "description": "The connection name configured in Azure Web App Bot OAuth settings.", - "examples": [ - "msgraphOAuthConnection" - ] - }, - "text": { - "type": "string", - "title": "Text", - "description": "Text shown in the OAuth signin card.", - "examples": [ - "Please sign in. " - ] - }, - "title": { - "type": "string", - "title": "Title", - "description": "Title shown in the OAuth signin card.", - "examples": [ - "Login" - ] - }, - "timeout": { - "type": "integer", - "title": "Timeout", - "description": "Time out setting for the OAuth signin card.", - "default": "900000" - }, - "tokenProperty": { - "$role": "expression", - "title": "Token property", - "description": "Property to store the OAuth token result.", - "examples": [ - "dialog.token" - ], - "type": "string" - } - }, - "additionalProperties": false, - "patternProperties": { - "^\\$": { - "type": "string" - } - }, - "anyOf": [ - { - "title": "Reference", - "required": [ - "$copy" - ] - }, - { - "title": "Type", - "required": [ - "connectionName", - "$type" - ] - } - ] - }, - "Microsoft.OnActivity": { - "$role": "unionType(Microsoft.ITriggerCondition)", - "title": "On activity", - "description": "Actions to perform on receipt of a generic activity.", - "type": "object", - "properties": { - "$type": { - "title": "$type", - "description": "Defines the valid properties for the component you are configuring (from a dialog .schema file)", - "type": "string", - "pattern": "^[a-zA-Z][a-zA-Z0-9.]*$", - "const": "Microsoft.OnActivity" - }, - "$copy": { - "title": "$copy", - "description": "Copy the definition by id from a .dialog file.", - "type": "string", - "pattern": "^(([a-zA-Z][a-zA-Z0-9.]*)?(#[a-zA-Z][a-zA-Z0-9.]*)?)$" - }, - "$id": { - "title": "$id", - "description": "Inline id for reuse of an inline definition", - "type": "string", - "pattern": "^([a-zA-Z][a-zA-Z0-9.]*)$" - }, - "$designer": { - "title": "$designer", - "type": "object", - "description": "Extra information for the Bot Framework Designer." - }, - "condition": { - "$role": "expression", - "title": "Condition", - "description": "Condition (expression).", - "examples": [ - "user.vip == true" - ], - "type": "string" - }, - "actions": { - "type": "array", - "description": "Sequence of actions to execute.", - "items": { - "$type": "Microsoft.IDialog", - "$ref": "#/definitions/Microsoft.IDialog" - } - }, - "type": { - "type": "string", - "title": "Activity type", - "description": "The Activity.Type to match" - } - }, - "additionalProperties": false, - "patternProperties": { - "^\\$": { - "type": "string" - } - }, - "anyOf": [ - { - "title": "Reference", - "required": [ - "$copy" - ] - }, - { - "title": "Type", - "required": [ - "actions", - "type", - "$type" - ] - } - ] - }, - "Microsoft.OnBeginDialog": { - "$role": "unionType(Microsoft.ITriggerCondition)", - "title": "On begin dialog", - "description": "Actions to perform when this dialog begins.", - "type": "object", - "properties": { - "$type": { - "title": "$type", - "description": "Defines the valid properties for the component you are configuring (from a dialog .schema file)", - "type": "string", - "pattern": "^[a-zA-Z][a-zA-Z0-9.]*$", - "const": "Microsoft.OnBeginDialog" - }, - "$copy": { - "title": "$copy", - "description": "Copy the definition by id from a .dialog file.", - "type": "string", - "pattern": "^(([a-zA-Z][a-zA-Z0-9.]*)?(#[a-zA-Z][a-zA-Z0-9.]*)?)$" - }, - "$id": { - "title": "$id", - "description": "Inline id for reuse of an inline definition", - "type": "string", - "pattern": "^([a-zA-Z][a-zA-Z0-9.]*)$" - }, - "$designer": { - "title": "$designer", - "type": "object", - "description": "Extra information for the Bot Framework Designer." - }, - "condition": { - "$role": "expression", - "title": "Condition", - "description": "Condition (expression).", - "examples": [ - "user.vip == true" - ], - "type": "string" - }, - "actions": { - "type": "array", - "description": "Sequence of actions to execute.", - "items": { - "$type": "Microsoft.IDialog", - "$ref": "#/definitions/Microsoft.IDialog" - } - } - }, - "additionalProperties": false, - "patternProperties": { - "^\\$": { - "type": "string" - } - }, - "anyOf": [ - { - "title": "Reference", - "required": [ - "$copy" - ] - }, - { - "title": "Type", - "required": [ - "actions", - "$type" - ] - } - ] - }, - "Microsoft.OnCancelDialog": { - "$role": "unionType(Microsoft.ITriggerCondition)", - "title": "On cancel dialog", - "description": "Actions to perform on cancel dialog event.", - "type": "object", - "properties": { - "$type": { - "title": "$type", - "description": "Defines the valid properties for the component you are configuring (from a dialog .schema file)", - "type": "string", - "pattern": "^[a-zA-Z][a-zA-Z0-9.]*$", - "const": "Microsoft.OnCancelDialog" - }, - "$copy": { - "title": "$copy", - "description": "Copy the definition by id from a .dialog file.", - "type": "string", - "pattern": "^(([a-zA-Z][a-zA-Z0-9.]*)?(#[a-zA-Z][a-zA-Z0-9.]*)?)$" - }, - "$id": { - "title": "$id", - "description": "Inline id for reuse of an inline definition", - "type": "string", - "pattern": "^([a-zA-Z][a-zA-Z0-9.]*)$" - }, - "$designer": { - "title": "$designer", - "type": "object", - "description": "Extra information for the Bot Framework Designer." - }, - "condition": { - "$role": "expression", - "title": "Condition", - "description": "Condition (expression).", - "examples": [ - "user.vip == true" - ], - "type": "string" - }, - "actions": { - "type": "array", - "description": "Sequence of actions to execute.", - "items": { - "$type": "Microsoft.IDialog", - "$ref": "#/definitions/Microsoft.IDialog" - } - } - }, - "additionalProperties": false, - "patternProperties": { - "^\\$": { - "type": "string" - } - }, - "anyOf": [ - { - "title": "Reference", - "required": [ - "$copy" - ] - }, - { - "title": "Type", - "required": [ - "actions", - "$type" - ] - } - ] - }, - "Microsoft.OnCondition": { - "$role": "unionType(Microsoft.ITriggerCondition)", - "title": "On condition", - "description": "Actions to perform when specified condition is true.", - "type": "object", - "properties": { - "$type": { - "title": "$type", - "description": "Defines the valid properties for the component you are configuring (from a dialog .schema file)", - "type": "string", - "pattern": "^[a-zA-Z][a-zA-Z0-9.]*$", - "const": "Microsoft.OnCondition" - }, - "$copy": { - "title": "$copy", - "description": "Copy the definition by id from a .dialog file.", - "type": "string", - "pattern": "^(([a-zA-Z][a-zA-Z0-9.]*)?(#[a-zA-Z][a-zA-Z0-9.]*)?)$" - }, - "$id": { - "title": "$id", - "description": "Inline id for reuse of an inline definition", - "type": "string", - "pattern": "^([a-zA-Z][a-zA-Z0-9.]*)$" - }, - "$designer": { - "title": "$designer", - "type": "object", - "description": "Extra information for the Bot Framework Designer." - }, - "condition": { - "$role": "expression", - "title": "Condition", - "description": "Condition (expression).", - "examples": [ - "user.vip == true" - ], - "type": "string" - }, - "actions": { - "type": "array", - "description": "Sequence of actions to execute.", - "items": { - "$type": "Microsoft.IDialog", - "$ref": "#/definitions/Microsoft.IDialog" - } - } - }, - "additionalProperties": false, - "patternProperties": { - "^\\$": { - "type": "string" - } - }, - "anyOf": [ - { - "title": "Reference", - "required": [ - "$copy" - ] - }, - { - "title": "Type", - "required": [ - "actions", - "$type" - ] - } - ] - }, - "Microsoft.OnConversationUpdateActivity": { - "$role": "unionType(Microsoft.ITriggerCondition)", - "title": "On ConversationUpdate activity", - "description": "Actions to perform on receipt of an activity with type 'ConversationUpdate'.", - "type": "object", - "properties": { - "$type": { - "title": "$type", - "description": "Defines the valid properties for the component you are configuring (from a dialog .schema file)", - "type": "string", - "pattern": "^[a-zA-Z][a-zA-Z0-9.]*$", - "const": "Microsoft.OnConversationUpdateActivity" - }, - "$copy": { - "title": "$copy", - "description": "Copy the definition by id from a .dialog file.", - "type": "string", - "pattern": "^(([a-zA-Z][a-zA-Z0-9.]*)?(#[a-zA-Z][a-zA-Z0-9.]*)?)$" - }, - "$id": { - "title": "$id", - "description": "Inline id for reuse of an inline definition", - "type": "string", - "pattern": "^([a-zA-Z][a-zA-Z0-9.]*)$" - }, - "$designer": { - "title": "$designer", - "type": "object", - "description": "Extra information for the Bot Framework Designer." - }, - "condition": { - "$role": "expression", - "title": "Condition", - "description": "Condition (expression).", - "examples": [ - "user.vip == true" - ], - "type": "string" - }, - "actions": { - "type": "array", - "description": "Sequence of actions to execute.", - "items": { - "$type": "Microsoft.IDialog", - "$ref": "#/definitions/Microsoft.IDialog" - } - } - }, - "additionalProperties": false, - "patternProperties": { - "^\\$": { - "type": "string" - } - }, - "anyOf": [ - { - "title": "Reference", - "required": [ - "$copy" - ] - }, - { - "title": "Type", - "required": [ - "actions", - "$type" - ] - } - ] - }, - "Microsoft.OnCustomEvent": { - "$role": "unionType(Microsoft.ITriggerCondition)", - "title": "On custom event", - "description": "Actions to perform when a custom event is detected. Use 'Emit a custom event' action to raise a custom event.", - "type": "object", - "properties": { - "$type": { - "title": "$type", - "description": "Defines the valid properties for the component you are configuring (from a dialog .schema file)", - "type": "string", - "pattern": "^[a-zA-Z][a-zA-Z0-9.]*$", - "const": "Microsoft.OnCustomEvent" - }, - "$copy": { - "title": "$copy", - "description": "Copy the definition by id from a .dialog file.", - "type": "string", - "pattern": "^(([a-zA-Z][a-zA-Z0-9.]*)?(#[a-zA-Z][a-zA-Z0-9.]*)?)$" - }, - "$id": { - "title": "$id", - "description": "Inline id for reuse of an inline definition", - "type": "string", - "pattern": "^([a-zA-Z][a-zA-Z0-9.]*)$" - }, - "$designer": { - "title": "$designer", - "type": "object", - "description": "Extra information for the Bot Framework Designer." - }, - "condition": { - "$role": "expression", - "title": "Condition", - "description": "Condition (expression).", - "examples": [ - "user.vip == true" - ], - "type": "string" - }, - "actions": { - "type": "array", - "description": "Sequence of actions to execute.", - "items": { - "$type": "Microsoft.IDialog", - "$ref": "#/definitions/Microsoft.IDialog" - } - }, - "event": { - "type": "string", - "title": "Custom event name", - "description": "Name of the custom event." - } - }, - "additionalProperties": false, - "patternProperties": { - "^\\$": { - "type": "string" - } - }, - "anyOf": [ - { - "title": "Reference", - "required": [ - "$copy" - ] - }, - { - "title": "Type", - "required": [ - "actions", - "event", - "$type" - ] - } - ] - }, - "Microsoft.OnDialogEvent": { - "$role": "unionType(Microsoft.ITriggerCondition)", - "title": "On dialog event", - "description": "Actions to perform when a specific dialog event occurs.", - "type": "object", - "properties": { - "$type": { - "title": "$type", - "description": "Defines the valid properties for the component you are configuring (from a dialog .schema file)", - "type": "string", - "pattern": "^[a-zA-Z][a-zA-Z0-9.]*$", - "const": "Microsoft.OnDialogEvent" - }, - "$copy": { - "title": "$copy", - "description": "Copy the definition by id from a .dialog file.", - "type": "string", - "pattern": "^(([a-zA-Z][a-zA-Z0-9.]*)?(#[a-zA-Z][a-zA-Z0-9.]*)?)$" - }, - "$id": { - "title": "$id", - "description": "Inline id for reuse of an inline definition", - "type": "string", - "pattern": "^([a-zA-Z][a-zA-Z0-9.]*)$" - }, - "$designer": { - "title": "$designer", - "type": "object", - "description": "Extra information for the Bot Framework Designer." - }, - "condition": { - "$role": "expression", - "title": "Condition", - "description": "Condition (expression).", - "examples": [ - "user.vip == true" - ], - "type": "string" - }, - "actions": { - "type": "array", - "description": "Sequence of actions to execute.", - "items": { - "$type": "Microsoft.IDialog", - "$ref": "#/definitions/Microsoft.IDialog" - } - }, - "event": { - "type": "string", - "title": "Dialog event name", - "description": "Name of dialog event." - } - }, - "additionalProperties": false, - "patternProperties": { - "^\\$": { - "type": "string" - } - }, - "anyOf": [ - { - "title": "Reference", - "required": [ - "$copy" - ] - }, - { - "title": "Type", - "required": [ - "actions", - "event", - "$type" - ] - } - ] - }, - "Microsoft.OnEndOfConversationActivity": { - "$role": "unionType(Microsoft.ITriggerCondition)", - "title": "On EndOfConversation activity", - "description": "Actions to perform on receipt of an activity with type 'EndOfConversation'.", - "type": "object", - "properties": { - "$type": { - "title": "$type", - "description": "Defines the valid properties for the component you are configuring (from a dialog .schema file)", - "type": "string", - "pattern": "^[a-zA-Z][a-zA-Z0-9.]*$", - "const": "Microsoft.OnEndOfConversationActivity" - }, - "$copy": { - "title": "$copy", - "description": "Copy the definition by id from a .dialog file.", - "type": "string", - "pattern": "^(([a-zA-Z][a-zA-Z0-9.]*)?(#[a-zA-Z][a-zA-Z0-9.]*)?)$" - }, - "$id": { - "title": "$id", - "description": "Inline id for reuse of an inline definition", - "type": "string", - "pattern": "^([a-zA-Z][a-zA-Z0-9.]*)$" - }, - "$designer": { - "title": "$designer", - "type": "object", - "description": "Extra information for the Bot Framework Designer." - }, - "condition": { - "$role": "expression", - "title": "Condition", - "description": "Condition (expression).", - "examples": [ - "user.vip == true" - ], - "type": "string" - }, - "actions": { - "type": "array", - "description": "Sequence of actions to execute.", - "items": { - "$type": "Microsoft.IDialog", - "$ref": "#/definitions/Microsoft.IDialog" - } - } - }, - "additionalProperties": false, - "patternProperties": { - "^\\$": { - "type": "string" - } - }, - "anyOf": [ - { - "title": "Reference", - "required": [ - "$copy" - ] - }, - { - "title": "Type", - "required": [ - "actions", - "$type" - ] - } - ] - }, - "Microsoft.OnError": { - "$role": "unionType(Microsoft.ITriggerCondition)", - "title": "On Error", - "description": "Action to perform when an 'Error' dialog event occurs.", - "type": "object", - "properties": { - "$type": { - "title": "$type", - "description": "Defines the valid properties for the component you are configuring (from a dialog .schema file)", - "type": "string", - "pattern": "^[a-zA-Z][a-zA-Z0-9.]*$", - "const": "Microsoft.OnError" - }, - "$copy": { - "title": "$copy", - "description": "Copy the definition by id from a .dialog file.", - "type": "string", - "pattern": "^(([a-zA-Z][a-zA-Z0-9.]*)?(#[a-zA-Z][a-zA-Z0-9.]*)?)$" - }, - "$id": { - "title": "$id", - "description": "Inline id for reuse of an inline definition", - "type": "string", - "pattern": "^([a-zA-Z][a-zA-Z0-9.]*)$" - }, - "$designer": { - "title": "$designer", - "type": "object", - "description": "Extra information for the Bot Framework Designer." - }, - "condition": { - "$role": "expression", - "title": "Condition", - "description": "Condition (expression).", - "examples": [ - "user.vip == true" - ], - "type": "string" - }, - "actions": { - "type": "array", - "description": "Sequence of actions to execute.", - "items": { - "$type": "Microsoft.IDialog", - "$ref": "#/definitions/Microsoft.IDialog" - } - } - }, - "additionalProperties": false, - "patternProperties": { - "^\\$": { - "type": "string" - } - }, - "anyOf": [ - { - "title": "Reference", - "required": [ - "$copy" - ] - }, - { - "title": "Type", - "required": [ - "actions", - "$type" - ] - } - ] - }, - "Microsoft.OnEventActivity": { - "$role": "unionType(Microsoft.ITriggerCondition)", - "title": "On Event activity", - "description": "Actions to perform on receipt of an activity with type 'Event'.", - "type": "object", - "properties": { - "$type": { - "title": "$type", - "description": "Defines the valid properties for the component you are configuring (from a dialog .schema file)", - "type": "string", - "pattern": "^[a-zA-Z][a-zA-Z0-9.]*$", - "const": "Microsoft.OnEventActivity" - }, - "$copy": { - "title": "$copy", - "description": "Copy the definition by id from a .dialog file.", - "type": "string", - "pattern": "^(([a-zA-Z][a-zA-Z0-9.]*)?(#[a-zA-Z][a-zA-Z0-9.]*)?)$" - }, - "$id": { - "title": "$id", - "description": "Inline id for reuse of an inline definition", - "type": "string", - "pattern": "^([a-zA-Z][a-zA-Z0-9.]*)$" - }, - "$designer": { - "title": "$designer", - "type": "object", - "description": "Extra information for the Bot Framework Designer." - }, - "condition": { - "$role": "expression", - "title": "Condition", - "description": "Condition (expression).", - "examples": [ - "user.vip == true" - ], - "type": "string" - }, - "actions": { - "type": "array", - "description": "Sequence of actions to execute.", - "items": { - "$type": "Microsoft.IDialog", - "$ref": "#/definitions/Microsoft.IDialog" - } - } - }, - "additionalProperties": false, - "patternProperties": { - "^\\$": { - "type": "string" - } - }, - "anyOf": [ - { - "title": "Reference", - "required": [ - "$copy" - ] - }, - { - "title": "Type", - "required": [ - "actions", - "$type" - ] - } - ] - }, - "Microsoft.OnHandoffActivity": { - "$role": "unionType(Microsoft.ITriggerCondition)", - "title": "On Handoff activity", - "description": "Actions to perform on receipt of an activity with type 'HandOff'.", - "type": "object", - "properties": { - "$type": { - "title": "$type", - "description": "Defines the valid properties for the component you are configuring (from a dialog .schema file)", - "type": "string", - "pattern": "^[a-zA-Z][a-zA-Z0-9.]*$", - "const": "Microsoft.OnHandoffActivity" - }, - "$copy": { - "title": "$copy", - "description": "Copy the definition by id from a .dialog file.", - "type": "string", - "pattern": "^(([a-zA-Z][a-zA-Z0-9.]*)?(#[a-zA-Z][a-zA-Z0-9.]*)?)$" - }, - "$id": { - "title": "$id", - "description": "Inline id for reuse of an inline definition", - "type": "string", - "pattern": "^([a-zA-Z][a-zA-Z0-9.]*)$" - }, - "$designer": { - "title": "$designer", - "type": "object", - "description": "Extra information for the Bot Framework Designer." - }, - "condition": { - "$role": "expression", - "title": "Condition", - "description": "Condition (expression).", - "examples": [ - "user.vip == true" - ], - "type": "string" - }, - "actions": { - "type": "array", - "description": "Sequence of actions to execute.", - "items": { - "$type": "Microsoft.IDialog", - "$ref": "#/definitions/Microsoft.IDialog" - } - } - }, - "additionalProperties": false, - "patternProperties": { - "^\\$": { - "type": "string" - } - }, - "anyOf": [ - { - "title": "Reference", - "required": [ - "$copy" - ] - }, - { - "title": "Type", - "required": [ - "actions", - "$type" - ] - } - ] - }, - "Microsoft.OnIntent": { - "$role": "unionType(Microsoft.ITriggerCondition)", - "title": "On intent recognition", - "description": "Actions to perform when specified intent is recognized.", - "type": "object", - "properties": { - "$type": { - "title": "$type", - "description": "Defines the valid properties for the component you are configuring (from a dialog .schema file)", - "type": "string", - "pattern": "^[a-zA-Z][a-zA-Z0-9.]*$", - "const": "Microsoft.OnIntent" - }, - "$copy": { - "title": "$copy", - "description": "Copy the definition by id from a .dialog file.", - "type": "string", - "pattern": "^(([a-zA-Z][a-zA-Z0-9.]*)?(#[a-zA-Z][a-zA-Z0-9.]*)?)$" - }, - "$id": { - "title": "$id", - "description": "Inline id for reuse of an inline definition", - "type": "string", - "pattern": "^([a-zA-Z][a-zA-Z0-9.]*)$" - }, - "$designer": { - "title": "$designer", - "type": "object", - "description": "Extra information for the Bot Framework Designer." - }, - "condition": { - "$role": "expression", - "title": "Condition", - "description": "Condition (expression).", - "examples": [ - "user.vip == true" - ], - "type": "string" - }, - "actions": { - "type": "array", - "description": "Sequence of actions to execute.", - "items": { - "$type": "Microsoft.IDialog", - "$ref": "#/definitions/Microsoft.IDialog" - } - }, - "intent": { - "type": "string", - "title": "Intent", - "description": "Name of intent." - }, - "entities": { - "type": "array", - "title": "Entities", - "description": "Required entities.", - "items": { - "type": "string" - } - } - }, - "additionalProperties": false, - "patternProperties": { - "^\\$": { - "type": "string" - } - }, - "anyOf": [ - { - "title": "Reference", - "required": [ - "$copy" - ] - }, - { - "title": "Type", - "required": [ - "actions", - "intent", - "$type" - ] - } - ] - }, - "Microsoft.OnInvokeActivity": { - "$role": "unionType(Microsoft.ITriggerCondition)", - "title": "On Invoke activity", - "description": "Actions to perform on receipt of an activity with type 'Invoke'.", - "type": "object", - "properties": { - "$type": { - "title": "$type", - "description": "Defines the valid properties for the component you are configuring (from a dialog .schema file)", - "type": "string", - "pattern": "^[a-zA-Z][a-zA-Z0-9.]*$", - "const": "Microsoft.OnInvokeActivity" - }, - "$copy": { - "title": "$copy", - "description": "Copy the definition by id from a .dialog file.", - "type": "string", - "pattern": "^(([a-zA-Z][a-zA-Z0-9.]*)?(#[a-zA-Z][a-zA-Z0-9.]*)?)$" - }, - "$id": { - "title": "$id", - "description": "Inline id for reuse of an inline definition", - "type": "string", - "pattern": "^([a-zA-Z][a-zA-Z0-9.]*)$" - }, - "$designer": { - "title": "$designer", - "type": "object", - "description": "Extra information for the Bot Framework Designer." - }, - "condition": { - "$role": "expression", - "title": "Condition", - "description": "Condition (expression).", - "examples": [ - "user.vip == true" - ], - "type": "string" - }, - "actions": { - "type": "array", - "description": "Sequence of actions to execute.", - "items": { - "$type": "Microsoft.IDialog", - "$ref": "#/definitions/Microsoft.IDialog" - } - } - }, - "additionalProperties": false, - "patternProperties": { - "^\\$": { - "type": "string" - } - }, - "anyOf": [ - { - "title": "Reference", - "required": [ - "$copy" - ] - }, - { - "title": "Type", - "required": [ - "actions", - "$type" - ] - } - ] - }, - "Microsoft.OnMessageActivity": { - "$role": "unionType(Microsoft.ITriggerCondition)", - "title": "On Message activity", - "description": "Actions to perform on receipt of an activity with type 'Message'. Overrides Intent trigger.", - "type": "object", - "properties": { - "$type": { - "title": "$type", - "description": "Defines the valid properties for the component you are configuring (from a dialog .schema file)", - "type": "string", - "pattern": "^[a-zA-Z][a-zA-Z0-9.]*$", - "const": "Microsoft.OnMessageActivity" - }, - "$copy": { - "title": "$copy", - "description": "Copy the definition by id from a .dialog file.", - "type": "string", - "pattern": "^(([a-zA-Z][a-zA-Z0-9.]*)?(#[a-zA-Z][a-zA-Z0-9.]*)?)$" - }, - "$id": { - "title": "$id", - "description": "Inline id for reuse of an inline definition", - "type": "string", - "pattern": "^([a-zA-Z][a-zA-Z0-9.]*)$" - }, - "$designer": { - "title": "$designer", - "type": "object", - "description": "Extra information for the Bot Framework Designer." - }, - "condition": { - "$role": "expression", - "title": "Condition", - "description": "Condition (expression).", - "examples": [ - "user.vip == true" - ], - "type": "string" - }, - "actions": { - "type": "array", - "description": "Sequence of actions to execute.", - "items": { - "$type": "Microsoft.IDialog", - "$ref": "#/definitions/Microsoft.IDialog" - } - } - }, - "additionalProperties": false, - "patternProperties": { - "^\\$": { - "type": "string" - } - }, - "anyOf": [ - { - "title": "Reference", - "required": [ - "$copy" - ] - }, - { - "title": "Type", - "required": [ - "actions", - "$type" - ] - } - ] - }, - "Microsoft.OnMessageDeleteActivity": { - "$role": "unionType(Microsoft.ITriggerCondition)", - "title": "On MessageDelete activity", - "description": "Actions to perform on receipt of an activity with type 'MessageDelete'.", - "type": "object", - "properties": { - "$type": { - "title": "$type", - "description": "Defines the valid properties for the component you are configuring (from a dialog .schema file)", - "type": "string", - "pattern": "^[a-zA-Z][a-zA-Z0-9.]*$", - "const": "Microsoft.OnMessageDeleteActivity" - }, - "$copy": { - "title": "$copy", - "description": "Copy the definition by id from a .dialog file.", - "type": "string", - "pattern": "^(([a-zA-Z][a-zA-Z0-9.]*)?(#[a-zA-Z][a-zA-Z0-9.]*)?)$" - }, - "$id": { - "title": "$id", - "description": "Inline id for reuse of an inline definition", - "type": "string", - "pattern": "^([a-zA-Z][a-zA-Z0-9.]*)$" - }, - "$designer": { - "title": "$designer", - "type": "object", - "description": "Extra information for the Bot Framework Designer." - }, - "condition": { - "$role": "expression", - "title": "Condition", - "description": "Condition (expression).", - "examples": [ - "user.vip == true" - ], - "type": "string" - }, - "actions": { - "type": "array", - "description": "Sequence of actions to execute.", - "items": { - "$type": "Microsoft.IDialog", - "$ref": "#/definitions/Microsoft.IDialog" - } - } - }, - "additionalProperties": false, - "patternProperties": { - "^\\$": { - "type": "string" - } - }, - "anyOf": [ - { - "title": "Reference", - "required": [ - "$copy" - ] - }, - { - "title": "Type", - "required": [ - "actions", - "$type" - ] - } - ] - }, - "Microsoft.OnMessageReactionActivity": { - "$role": "unionType(Microsoft.ITriggerCondition)", - "title": "On MessageReaction activity", - "description": "Actions to perform on receipt of an activity with type 'MessageReaction'.", - "type": "object", - "properties": { - "$type": { - "title": "$type", - "description": "Defines the valid properties for the component you are configuring (from a dialog .schema file)", - "type": "string", - "pattern": "^[a-zA-Z][a-zA-Z0-9.]*$", - "const": "Microsoft.OnMessageReactionActivity" - }, - "$copy": { - "title": "$copy", - "description": "Copy the definition by id from a .dialog file.", - "type": "string", - "pattern": "^(([a-zA-Z][a-zA-Z0-9.]*)?(#[a-zA-Z][a-zA-Z0-9.]*)?)$" - }, - "$id": { - "title": "$id", - "description": "Inline id for reuse of an inline definition", - "type": "string", - "pattern": "^([a-zA-Z][a-zA-Z0-9.]*)$" - }, - "$designer": { - "title": "$designer", - "type": "object", - "description": "Extra information for the Bot Framework Designer." - }, - "condition": { - "$role": "expression", - "title": "Condition", - "description": "Condition (expression).", - "examples": [ - "user.vip == true" - ], - "type": "string" - }, - "actions": { - "type": "array", - "description": "Sequence of actions to execute.", - "items": { - "$type": "Microsoft.IDialog", - "$ref": "#/definitions/Microsoft.IDialog" - } - } - }, - "additionalProperties": false, - "patternProperties": { - "^\\$": { - "type": "string" - } - }, - "anyOf": [ - { - "title": "Reference", - "required": [ - "$copy" - ] - }, - { - "title": "Type", - "required": [ - "actions", - "$type" - ] - } - ] - }, - "Microsoft.OnMessageUpdateActivity": { - "$role": "unionType(Microsoft.ITriggerCondition)", - "title": "On MessageUpdate activity", - "description": "Actions to perform on receipt of an activity with type 'MessageUpdate'.", - "type": "object", - "properties": { - "$type": { - "title": "$type", - "description": "Defines the valid properties for the component you are configuring (from a dialog .schema file)", - "type": "string", - "pattern": "^[a-zA-Z][a-zA-Z0-9.]*$", - "const": "Microsoft.OnMessageUpdateActivity" - }, - "$copy": { - "title": "$copy", - "description": "Copy the definition by id from a .dialog file.", - "type": "string", - "pattern": "^(([a-zA-Z][a-zA-Z0-9.]*)?(#[a-zA-Z][a-zA-Z0-9.]*)?)$" - }, - "$id": { - "title": "$id", - "description": "Inline id for reuse of an inline definition", - "type": "string", - "pattern": "^([a-zA-Z][a-zA-Z0-9.]*)$" - }, - "$designer": { - "title": "$designer", - "type": "object", - "description": "Extra information for the Bot Framework Designer." - }, - "condition": { - "$role": "expression", - "title": "Condition", - "description": "Condition (expression).", - "examples": [ - "user.vip == true" - ], - "type": "string" - }, - "actions": { - "type": "array", - "description": "Sequence of actions to execute.", - "items": { - "$type": "Microsoft.IDialog", - "$ref": "#/definitions/Microsoft.IDialog" - } - } - }, - "additionalProperties": false, - "patternProperties": { - "^\\$": { - "type": "string" - } - }, - "anyOf": [ - { - "title": "Reference", - "required": [ - "$copy" - ] - }, - { - "title": "Type", - "required": [ - "actions", - "$type" - ] - } - ] - }, - "Microsoft.OnRepromptDialog": { - "$role": "unionType(Microsoft.ITriggerCondition)", - "title": "On RepromptDialog", - "description": "Actions to perform when 'RepromptDialog' event occurs.", - "type": "object", - "properties": { - "$type": { - "title": "$type", - "description": "Defines the valid properties for the component you are configuring (from a dialog .schema file)", - "type": "string", - "pattern": "^[a-zA-Z][a-zA-Z0-9.]*$", - "const": "Microsoft.OnRepromptDialog" - }, - "$copy": { - "title": "$copy", - "description": "Copy the definition by id from a .dialog file.", - "type": "string", - "pattern": "^(([a-zA-Z][a-zA-Z0-9.]*)?(#[a-zA-Z][a-zA-Z0-9.]*)?)$" - }, - "$id": { - "title": "$id", - "description": "Inline id for reuse of an inline definition", - "type": "string", - "pattern": "^([a-zA-Z][a-zA-Z0-9.]*)$" - }, - "$designer": { - "title": "$designer", - "type": "object", - "description": "Extra information for the Bot Framework Designer." - }, - "condition": { - "$role": "expression", - "title": "Condition", - "description": "Condition (expression).", - "examples": [ - "user.vip == true" - ], - "type": "string" - }, - "actions": { - "type": "array", - "description": "Sequence of actions to execute.", - "items": { - "$type": "Microsoft.IDialog", - "$ref": "#/definitions/Microsoft.IDialog" - } - } - }, - "additionalProperties": false, - "patternProperties": { - "^\\$": { - "type": "string" - } - }, - "anyOf": [ - { - "title": "Reference", - "required": [ - "$copy" - ] - }, - { - "title": "Type", - "required": [ - "actions", - "$type" - ] - } - ] - }, - "Microsoft.OnTypingActivity": { - "$role": "unionType(Microsoft.ITriggerCondition)", - "title": "On Typing activity", - "description": "Actions to perform on receipt of an activity with type 'Typing'.", - "type": "object", - "properties": { - "$type": { - "title": "$type", - "description": "Defines the valid properties for the component you are configuring (from a dialog .schema file)", - "type": "string", - "pattern": "^[a-zA-Z][a-zA-Z0-9.]*$", - "const": "Microsoft.OnTypingActivity" - }, - "$copy": { - "title": "$copy", - "description": "Copy the definition by id from a .dialog file.", - "type": "string", - "pattern": "^(([a-zA-Z][a-zA-Z0-9.]*)?(#[a-zA-Z][a-zA-Z0-9.]*)?)$" - }, - "$id": { - "title": "$id", - "description": "Inline id for reuse of an inline definition", - "type": "string", - "pattern": "^([a-zA-Z][a-zA-Z0-9.]*)$" - }, - "$designer": { - "title": "$designer", - "type": "object", - "description": "Extra information for the Bot Framework Designer." - }, - "condition": { - "$role": "expression", - "title": "Condition", - "description": "Condition (expression).", - "examples": [ - "user.vip == true" - ], - "type": "string" - }, - "actions": { - "type": "array", - "description": "Sequence of actions to execute.", - "items": { - "$type": "Microsoft.IDialog", - "$ref": "#/definitions/Microsoft.IDialog" - } - } - }, - "additionalProperties": false, - "patternProperties": { - "^\\$": { - "type": "string" - } - }, - "anyOf": [ - { - "title": "Reference", - "required": [ - "$copy" - ] - }, - { - "title": "Type", - "required": [ - "actions", - "$type" - ] - } - ] - }, - "Microsoft.OnUnknownIntent": { - "title": "On unknown intent", - "description": "Action to perform when user input is unrecognized and if none of the 'on intent recognition' triggers match recognized intent.", - "type": "object", - "$role": "unionType(Microsoft.ITriggerCondition)", - "properties": { - "$type": { - "title": "$type", - "description": "Defines the valid properties for the component you are configuring (from a dialog .schema file)", - "type": "string", - "pattern": "^[a-zA-Z][a-zA-Z0-9.]*$", - "const": "Microsoft.OnUnknownIntent" - }, - "$copy": { - "title": "$copy", - "description": "Copy the definition by id from a .dialog file.", - "type": "string", - "pattern": "^(([a-zA-Z][a-zA-Z0-9.]*)?(#[a-zA-Z][a-zA-Z0-9.]*)?)$" - }, - "$id": { - "title": "$id", - "description": "Inline id for reuse of an inline definition", - "type": "string", - "pattern": "^([a-zA-Z][a-zA-Z0-9.]*)$" - }, - "$designer": { - "title": "$designer", - "type": "object", - "description": "Extra information for the Bot Framework Designer." - }, - "condition": { - "$role": "expression", - "title": "Condition", - "description": "Condition (expression).", - "examples": [ - "user.vip == true" - ], - "type": "string" - }, - "actions": { - "type": "array", - "description": "Sequence of actions to execute.", - "items": { - "$type": "Microsoft.IDialog", - "$ref": "#/definitions/Microsoft.IDialog" - } - } - }, - "additionalProperties": false, - "patternProperties": { - "^\\$": { - "type": "string" - } - }, - "anyOf": [ - { - "title": "Reference", - "required": [ - "$copy" - ] - }, - { - "title": "Type", - "required": [ - "actions", - "$type" - ] - } - ] - }, - "Microsoft.OrdinalEntityRecognizer": { - "$role": "unionType(Microsoft.EntityRecognizers)", - "title": "Ordinal Entity Recognizer", - "description": "Recognizer which recognizes ordinals (example: first, second, 3rd).", - "type": "object", - "properties": { - "$type": { - "title": "$type", - "description": "Defines the valid properties for the component you are configuring (from a dialog .schema file)", - "type": "string", - "pattern": "^[a-zA-Z][a-zA-Z0-9.]*$", - "const": "Microsoft.OrdinalEntityRecognizer" - }, - "$copy": { - "title": "$copy", - "description": "Copy the definition by id from a .dialog file.", - "type": "string", - "pattern": "^(([a-zA-Z][a-zA-Z0-9.]*)?(#[a-zA-Z][a-zA-Z0-9.]*)?)$" - }, - "$id": { - "title": "$id", - "description": "Inline id for reuse of an inline definition", - "type": "string", - "pattern": "^([a-zA-Z][a-zA-Z0-9.]*)$" - }, - "$designer": { - "title": "$designer", - "type": "object", - "description": "Extra information for the Bot Framework Designer." - } - }, - "additionalProperties": false, - "patternProperties": { - "^\\$": { - "type": "string" - } - }, - "anyOf": [ - { - "title": "Reference", - "required": [ - "$copy" - ] - }, - { - "title": "Type", - "required": [ - "$type" - ] - } - ] - }, - "Microsoft.PercentageEntityRecognizer": { - "$role": "unionType(Microsoft.EntityRecognizers)", - "title": "Percentage Entity Recognizer", - "description": "Recognizer which recognizes percentages.", - "type": "object", - "properties": { - "$type": { - "title": "$type", - "description": "Defines the valid properties for the component you are configuring (from a dialog .schema file)", - "type": "string", - "pattern": "^[a-zA-Z][a-zA-Z0-9.]*$", - "const": "Microsoft.PercentageEntityRecognizer" - }, - "$copy": { - "title": "$copy", - "description": "Copy the definition by id from a .dialog file.", - "type": "string", - "pattern": "^(([a-zA-Z][a-zA-Z0-9.]*)?(#[a-zA-Z][a-zA-Z0-9.]*)?)$" - }, - "$id": { - "title": "$id", - "description": "Inline id for reuse of an inline definition", - "type": "string", - "pattern": "^([a-zA-Z][a-zA-Z0-9.]*)$" - }, - "$designer": { - "title": "$designer", - "type": "object", - "description": "Extra information for the Bot Framework Designer." - } - }, - "additionalProperties": false, - "patternProperties": { - "^\\$": { - "type": "string" - } - }, - "anyOf": [ - { - "title": "Reference", - "required": [ - "$copy" - ] - }, - { - "title": "Type", - "required": [ - "$type" - ] - } - ] - }, - "Microsoft.PhoneNumberEntityRecognizer": { - "$role": "unionType(Microsoft.EntityRecognizers)", - "title": "Phone Number Entity Recognizer", - "description": "Recognizer which recognizes phone numbers.", - "type": "object", - "properties": { - "$type": { - "title": "$type", - "description": "Defines the valid properties for the component you are configuring (from a dialog .schema file)", - "type": "string", - "pattern": "^[a-zA-Z][a-zA-Z0-9.]*$", - "const": "Microsoft.PhoneNumberEntityRecognizer" - }, - "$copy": { - "title": "$copy", - "description": "Copy the definition by id from a .dialog file.", - "type": "string", - "pattern": "^(([a-zA-Z][a-zA-Z0-9.]*)?(#[a-zA-Z][a-zA-Z0-9.]*)?)$" - }, - "$id": { - "title": "$id", - "description": "Inline id for reuse of an inline definition", - "type": "string", - "pattern": "^([a-zA-Z][a-zA-Z0-9.]*)$" - }, - "$designer": { - "title": "$designer", - "type": "object", - "description": "Extra information for the Bot Framework Designer." - } - }, - "additionalProperties": false, - "patternProperties": { - "^\\$": { - "type": "string" - } - }, - "anyOf": [ - { - "title": "Reference", - "required": [ - "$copy" - ] - }, - { - "title": "Type", - "required": [ - "$type" - ] - } - ] - }, - "Microsoft.QnAMakerDialog": { - "$role": "unionType(Microsoft.IDialog)", - "title": "QnAMaker Dialog", - "description": "Dialog which uses QnAMAker knowledge base to answer questions.", - "type": "object", - "additionalProperties": false, - "properties": { - "$type": { - "title": "$type", - "description": "Defines the valid properties for the component you are configuring (from a dialog .schema file)", - "type": "string", - "pattern": "^[a-zA-Z][a-zA-Z0-9.]*$", - "const": "Microsoft.QnAMakerDialog" - }, - "$copy": { - "title": "$copy", - "description": "Copy the definition by id from a .dialog file.", - "type": "string", - "pattern": "^(([a-zA-Z][a-zA-Z0-9.]*)?(#[a-zA-Z][a-zA-Z0-9.]*)?)$" - }, - "$id": { - "title": "$id", - "description": "Inline id for reuse of an inline definition", - "type": "string", - "pattern": "^([a-zA-Z][a-zA-Z0-9.]*)$" - }, - "$designer": { - "title": "$designer", - "type": "object", - "description": "Extra information for the Bot Framework Designer." - }, - "knowledgeBaseId": { - "$role": "expression", - "title": "KnowledgeBase Id", - "description": "KnowledgeBase Id of your QnA Maker KnowledgeBase.", - "default": "settings.qna.knowledgebaseid", - "type": "string" - }, - "endpointKey": { - "$role": "expression", - "title": "Endpoint Key", - "description": "Endpoint key for the QnA Maker KB.", - "default": "settings.qna.endpointkey", - "type": "string" - }, - "hostname": { - "$role": "expression", - "title": "Hostname", - "description": "Hostname for your QnA Maker service.", - "default": "settings.qna.hostname", - "examples": [ - "https://yourserver.azurewebsites.net/qnamaker" - ], - "type": "string" - }, - "noAnswer": { - "$type": "Microsoft.IActivityTemplate", - "title": "Fallback answer", - "description": "Default answer to return when none found in KB.", - "default": "Sorry, I did not find an answer.", - "$ref": "#/definitions/Microsoft.IActivityTemplate" - }, - "threshold": { - "type": "number", - "title": "Threshold", - "description": "Threshold score to filter results.", - "default": 0.3 - }, - "activeLearningCardTitle": { - "type": "string", - "title": "Active learning card title", - "description": "Title for active learning suggestions card.", - "default": "Did you mean:" - }, - "cardNoMatchText": { - "type": "string", - "title": "Card no match text", - "description": "Text for no match option.", - "default": "None of the above." - }, - "cardNoMatchResponse ": { - "$type": "Microsoft.IActivityTemplate", - "title": "Card no match response", - "description": "Custom response when no match option was selected.", - "default": "Thanks for the feedback.", - "$ref": "#/definitions/Microsoft.IActivityTemplate" - }, - "strictFilters": { - "type": "array", - "title": "Strict Filter Property", - "description": "Memory property that holds strict filters to use when calling the QnA Maker KB.", - "items": { - "type": "object" - } - } - }, - "patternProperties": { - "^\\$": { - "type": "string" - } - }, - "anyOf": [ - { - "title": "Reference", - "required": [ - "$copy" - ] - }, - { - "title": "Type", - "required": [ - "knowledgeBaseId", - "endpointKey", - "hostname", - "$type" - ] - } - ] - }, - "Microsoft.RandomSelector": { - "$role": "unionType(Microsoft.ITriggerSelector)", - "title": "Random rule selector", - "description": "Select most specific true rule", - "type": "object", - "properties": { - "$type": { - "title": "$type", - "description": "Defines the valid properties for the component you are configuring (from a dialog .schema file)", - "type": "string", - "pattern": "^[a-zA-Z][a-zA-Z0-9.]*$", - "const": "Microsoft.RandomSelector" - }, - "$copy": { - "title": "$copy", - "description": "Copy the definition by id from a .dialog file.", - "type": "string", - "pattern": "^(([a-zA-Z][a-zA-Z0-9.]*)?(#[a-zA-Z][a-zA-Z0-9.]*)?)$" - }, - "$id": { - "title": "$id", - "description": "Inline id for reuse of an inline definition", - "type": "string", - "pattern": "^([a-zA-Z][a-zA-Z0-9.]*)$" - }, - "$designer": { - "title": "$designer", - "type": "object", - "description": "Extra information for the Bot Framework Designer." - }, - "seed": { - "type": "integer" - } - }, - "additionalProperties": false, - "patternProperties": { - "^\\$": { - "type": "string" - } - }, - "anyOf": [ - { - "title": "Reference", - "required": [ - "$copy" - ] - }, - { - "title": "Type", - "required": [ - "$type" - ] - } - ] - }, - "Microsoft.RegExEntityRecognizer": { - "$role": "unionType(Microsoft.EntityRecognizers)", - "title": "Regex Entity Recognizer", - "description": "Recognizer which recognizes patterns of input based on regex.", - "type": "object", - "properties": { - "$type": { - "title": "$type", - "description": "Defines the valid properties for the component you are configuring (from a dialog .schema file)", - "type": "string", - "pattern": "^[a-zA-Z][a-zA-Z0-9.]*$", - "const": "Microsoft.RegExEntityRecognizer" - }, - "$copy": { - "title": "$copy", - "description": "Copy the definition by id from a .dialog file.", - "type": "string", - "pattern": "^(([a-zA-Z][a-zA-Z0-9.]*)?(#[a-zA-Z][a-zA-Z0-9.]*)?)$" - }, - "$id": { - "title": "$id", - "description": "Inline id for reuse of an inline definition", - "type": "string", - "pattern": "^([a-zA-Z][a-zA-Z0-9.]*)$" - }, - "$designer": { - "title": "$designer", - "type": "object", - "description": "Extra information for the Bot Framework Designer." - }, - "name": { - "type": "string", - "title": "Name", - "description": "Name of the entity" - }, - "pattern": { - "type": "string", - "title": "Pattern", - "description": "Pattern expressed as regular expression." - } - }, - "additionalProperties": false, - "patternProperties": { - "^\\$": { - "type": "string" - } - }, - "anyOf": [ - { - "title": "Reference", - "required": [ - "$copy" - ] - }, - { - "title": "Type", - "required": [ - "name", - "pattern", - "$type" - ] - } - ] - }, - "Microsoft.RegexRecognizer": { - "$role": "unionType(Microsoft.IRecognizer)", - "title": "Regex recognizer", - "description": "Use regular expressions to recognize intents and entities from user input.", - "type": "object", - "properties": { - "$type": { - "title": "$type", - "description": "Defines the valid properties for the component you are configuring (from a dialog .schema file)", - "type": "string", - "pattern": "^[a-zA-Z][a-zA-Z0-9.]*$", - "const": "Microsoft.RegexRecognizer" - }, - "$copy": { - "title": "$copy", - "description": "Copy the definition by id from a .dialog file.", - "type": "string", - "pattern": "^(([a-zA-Z][a-zA-Z0-9.]*)?(#[a-zA-Z][a-zA-Z0-9.]*)?)$" - }, - "$id": { - "title": "$id", - "description": "Inline id for reuse of an inline definition", - "type": "string", - "pattern": "^([a-zA-Z][a-zA-Z0-9.]*)$" - }, - "$designer": { - "title": "$designer", - "type": "object", - "description": "Extra information for the Bot Framework Designer." - }, - "intents": { - "type": "array", - "title": "RegEx patterns to intents", - "description": "Collection of patterns to match for an intent.", - "items": { - "type": "object", - "properties": { - "intent": { - "type": "string", - "title": "Intent", - "description": "The intent name." - }, - "pattern": { - "type": "string", - "title": "Pattern", - "description": "The regular expression pattern." - } - } - } - }, - "entities": { - "type": "array", - "title": "Entity recognizers", - "description": "Collection of entity recognizers to use.", - "items": { - "$type": "Microsoft.EntityRecognizers", - "$ref": "#/definitions/Microsoft.EntityRecognizers" - } - } - }, - "additionalProperties": false, - "patternProperties": { - "^\\$": { - "type": "string" - } - }, - "anyOf": [ - { - "title": "Reference", - "required": [ - "$copy" - ] - }, - { - "title": "Type", - "required": [ - "intents", - "$type" - ] - } - ] - }, - "Microsoft.RepeatDialog": { - "$role": "unionType(Microsoft.IDialog)", - "type": "object", - "title": "Repeat dialog", - "description": "Repeat current dialog.", - "properties": { - "$type": { - "title": "$type", - "description": "Defines the valid properties for the component you are configuring (from a dialog .schema file)", - "type": "string", - "pattern": "^[a-zA-Z][a-zA-Z0-9.]*$", - "const": "Microsoft.RepeatDialog" - }, - "$copy": { - "title": "$copy", - "description": "Copy the definition by id from a .dialog file.", - "type": "string", - "pattern": "^(([a-zA-Z][a-zA-Z0-9.]*)?(#[a-zA-Z][a-zA-Z0-9.]*)?)$" - }, - "$id": { - "title": "$id", - "description": "Inline id for reuse of an inline definition", - "type": "string", - "pattern": "^([a-zA-Z][a-zA-Z0-9.]*)$" - }, - "$designer": { - "title": "$designer", - "type": "object", - "description": "Extra information for the Bot Framework Designer." - } - }, - "additionalProperties": false, - "patternProperties": { - "^\\$": { - "type": "string" - } - }, - "anyOf": [ - { - "title": "Reference", - "required": [ - "$copy" - ] - }, - { - "title": "Type", - "required": [ - "$type" - ] - } - ] - }, - "Microsoft.ReplaceDialog": { - "$role": "unionType(Microsoft.IDialog)", - "type": "object", - "title": "Replace dialog", - "description": "Replace current dialog with another dialog.", - "properties": { - "$type": { - "title": "$type", - "description": "Defines the valid properties for the component you are configuring (from a dialog .schema file)", - "type": "string", - "pattern": "^[a-zA-Z][a-zA-Z0-9.]*$", - "const": "Microsoft.ReplaceDialog" - }, - "$copy": { - "title": "$copy", - "description": "Copy the definition by id from a .dialog file.", - "type": "string", - "pattern": "^(([a-zA-Z][a-zA-Z0-9.]*)?(#[a-zA-Z][a-zA-Z0-9.]*)?)$" - }, - "$id": { - "title": "$id", - "description": "Inline id for reuse of an inline definition", - "type": "string", - "pattern": "^([a-zA-Z][a-zA-Z0-9.]*)$" - }, - "$designer": { - "title": "$designer", - "type": "object", - "description": "Extra information for the Bot Framework Designer." - }, - "dialog": { - "$type": "Microsoft.IDialog", - "title": "Dialog name", - "description": "Current dialog will be replaced by this dialog.", - "$ref": "#/definitions/Microsoft.IDialog" - }, - "options": { - "type": "object", - "title": "Options", - "description": "One or more options that are passed to the dialog that is called.", - "additionalProperties": { - "type": "string", - "title": "Options" - } - } - }, - "additionalProperties": false, - "patternProperties": { - "^\\$": { - "type": "string" - } - }, - "anyOf": [ - { - "title": "Reference", - "required": [ - "$copy" - ] - }, - { - "title": "Type", - "required": [ - "$type" - ] - } - ] - }, - "Microsoft.SendActivity": { - "$role": "unionType(Microsoft.IDialog)", - "title": "Send an activity", - "description": "Respond with an activity.", - "type": "object", - "properties": { - "$type": { - "title": "$type", - "description": "Defines the valid properties for the component you are configuring (from a dialog .schema file)", - "type": "string", - "pattern": "^[a-zA-Z][a-zA-Z0-9.]*$", - "const": "Microsoft.SendActivity" - }, - "$copy": { - "title": "$copy", - "description": "Copy the definition by id from a .dialog file.", - "type": "string", - "pattern": "^(([a-zA-Z][a-zA-Z0-9.]*)?(#[a-zA-Z][a-zA-Z0-9.]*)?)$" - }, - "$id": { - "title": "$id", - "description": "Inline id for reuse of an inline definition", - "type": "string", - "pattern": "^([a-zA-Z][a-zA-Z0-9.]*)$" - }, - "$designer": { - "title": "$designer", - "type": "object", - "description": "Extra information for the Bot Framework Designer." - }, - "activity": { - "$type": "Microsoft.IActivityTemplate", - "title": "Activity", - "description": "Activity to send.", - "$ref": "#/definitions/Microsoft.IActivityTemplate" - } - }, - "additionalProperties": false, - "patternProperties": { - "^\\$": { - "type": "string" - } - }, - "anyOf": [ - { - "title": "Reference", - "required": [ - "$copy" - ] - }, - { - "title": "Type", - "required": [ - "$type" - ] - } - ] - }, - "Microsoft.SetProperty": { - "$role": "unionType(Microsoft.IDialog)", - "title": "Set property", - "description": "Set property to a value.", - "type": "object", - "properties": { - "$type": { - "title": "$type", - "description": "Defines the valid properties for the component you are configuring (from a dialog .schema file)", - "type": "string", - "pattern": "^[a-zA-Z][a-zA-Z0-9.]*$", - "const": "Microsoft.SetProperty" - }, - "$copy": { - "title": "$copy", - "description": "Copy the definition by id from a .dialog file.", - "type": "string", - "pattern": "^(([a-zA-Z][a-zA-Z0-9.]*)?(#[a-zA-Z][a-zA-Z0-9.]*)?)$" - }, - "$id": { - "title": "$id", - "description": "Inline id for reuse of an inline definition", - "type": "string", - "pattern": "^([a-zA-Z][a-zA-Z0-9.]*)$" - }, - "$designer": { - "title": "$designer", - "type": "object", - "description": "Extra information for the Bot Framework Designer." - }, - "property": { - "$role": "expression", - "title": "Property", - "description": "Property (named location to store information).", - "examples": [ - "user.age" - ], - "type": "string" - }, - "value": { - "$role": "expression", - "title": "Value", - "description": "New value or expression.", - "examples": [ - "'milk'", - "dialog.favColor", - "dialog.favColor == 'red'" - ], - "type": "string" - } - }, - "additionalProperties": false, - "patternProperties": { - "^\\$": { - "type": "string" - } - }, - "anyOf": [ - { - "title": "Reference", - "required": [ - "$copy" - ] - }, - { - "title": "Type", - "required": [ - "property", - "value", - "$type" - ] - } - ] - }, - "Microsoft.StaticActivityTemplate": { - "$role": "unionType(Microsoft.IActivityTemplate)", - "title": "Microsoft Static Activity Template", - "description": "This allows you to define a static Activity object", - "type": "object", - "properties": { - "$type": { - "title": "$type", - "description": "Defines the valid properties for the component you are configuring (from a dialog .schema file)", - "type": "string", - "pattern": "^[a-zA-Z][a-zA-Z0-9.]*$", - "const": "Microsoft.StaticActivityTemplate" - }, - "$copy": { - "title": "$copy", - "description": "Copy the definition by id from a .dialog file.", - "type": "string", - "pattern": "^(([a-zA-Z][a-zA-Z0-9.]*)?(#[a-zA-Z][a-zA-Z0-9.]*)?)$" - }, - "$id": { - "title": "$id", - "description": "Inline id for reuse of an inline definition", - "type": "string", - "pattern": "^([a-zA-Z][a-zA-Z0-9.]*)$" - }, - "$designer": { - "title": "$designer", - "type": "object", - "description": "Extra information for the Bot Framework Designer." - }, - "activity": { - "type": "object", - "title": "Activity", - "Description": "A static Activity to used" - } - }, - "additionalProperties": false, - "patternProperties": { - "^\\$": { - "type": "string" - } - }, - "anyOf": [ - { - "title": "Reference", - "required": [ - "$copy" - ] - }, - { - "title": "Type", - "required": [ - "activity", - "$type" - ] - } - ] - }, - "Microsoft.SwitchCondition": { - "$role": "unionType(Microsoft.IDialog)", - "title": "Switch condition", - "description": "Execute different actions based on the value of a property.", - "type": "object", - "properties": { - "$type": { - "title": "$type", - "description": "Defines the valid properties for the component you are configuring (from a dialog .schema file)", - "type": "string", - "pattern": "^[a-zA-Z][a-zA-Z0-9.]*$", - "const": "Microsoft.SwitchCondition" - }, - "$copy": { - "title": "$copy", - "description": "Copy the definition by id from a .dialog file.", - "type": "string", - "pattern": "^(([a-zA-Z][a-zA-Z0-9.]*)?(#[a-zA-Z][a-zA-Z0-9.]*)?)$" - }, - "$id": { - "title": "$id", - "description": "Inline id for reuse of an inline definition", - "type": "string", - "pattern": "^([a-zA-Z][a-zA-Z0-9.]*)$" - }, - "$designer": { - "title": "$designer", - "type": "object", - "description": "Extra information for the Bot Framework Designer." - }, - "condition": { - "$role": "expression", - "title": "Condition", - "description": "Property to evaluate.", - "examples": [ - "user.favColor" - ], - "type": "string" - }, - "cases": { - "type": "array", - "title": "Cases", - "desc": "Actions for each possible condition.", - "items": { - "type": "object", - "required": [ - "value", - "case" - ], - "properties": { - "value": { - "$role": "expression", - "title": "Value", - "description": "Value.", - "examples": [ - "'red'", - "dialog.colors.red" - ], - "type": "string" - }, - "actions": { - "type": "array", - "title": "Actions", - "description": "Actions to execute.", - "items": { - "$type": "Microsoft.IDialog", - "$ref": "#/definitions/Microsoft.IDialog" - } - } - } - } - }, - "default": { - "type": "array", - "title": "Default", - "description": "Actions to execute if none of the cases meet the condition.", - "items": { - "$type": "Microsoft.IDialog", - "$ref": "#/definitions/Microsoft.IDialog" - } - } - }, - "additionalProperties": false, - "patternProperties": { - "^\\$": { - "type": "string" - } - }, - "anyOf": [ - { - "title": "Reference", - "required": [ - "$copy" - ] - }, - { - "title": "Type", - "required": [ - "condition", - "$type" - ] - } - ] - }, - "Microsoft.TemperatureEntityRecognizer": { - "$role": "unionType(Microsoft.EntityRecognizers)", - "title": "Temperature Entity Recognizer", - "description": "Recognizer which recognizes temperatures.", - "type": "object", - "properties": { - "$type": { - "title": "$type", - "description": "Defines the valid properties for the component you are configuring (from a dialog .schema file)", - "type": "string", - "pattern": "^[a-zA-Z][a-zA-Z0-9.]*$", - "const": "Microsoft.TemperatureEntityRecognizer" - }, - "$copy": { - "title": "$copy", - "description": "Copy the definition by id from a .dialog file.", - "type": "string", - "pattern": "^(([a-zA-Z][a-zA-Z0-9.]*)?(#[a-zA-Z][a-zA-Z0-9.]*)?)$" - }, - "$id": { - "title": "$id", - "description": "Inline id for reuse of an inline definition", - "type": "string", - "pattern": "^([a-zA-Z][a-zA-Z0-9.]*)$" - }, - "$designer": { - "title": "$designer", - "type": "object", - "description": "Extra information for the Bot Framework Designer." - } - }, - "additionalProperties": false, - "patternProperties": { - "^\\$": { - "type": "string" - } - }, - "anyOf": [ - { - "title": "Reference", - "required": [ - "$copy" - ] - }, - { - "title": "Type", - "required": [ - "$type" - ] - } - ] - }, - "Microsoft.TextInput": { - "$role": "unionType(Microsoft.IDialog)", - "type": "object", - "title": "Text input dialog", - "description": "Collection information - Ask for a word or sentence.", - "properties": { - "$type": { - "title": "$type", - "description": "Defines the valid properties for the component you are configuring (from a dialog .schema file)", - "type": "string", - "pattern": "^[a-zA-Z][a-zA-Z0-9.]*$", - "const": "Microsoft.TextInput" - }, - "$copy": { - "title": "$copy", - "description": "Copy the definition by id from a .dialog file.", - "type": "string", - "pattern": "^(([a-zA-Z][a-zA-Z0-9.]*)?(#[a-zA-Z][a-zA-Z0-9.]*)?)$" - }, - "$id": { - "title": "$id", - "description": "Inline id for reuse of an inline definition", - "type": "string", - "pattern": "^([a-zA-Z][a-zA-Z0-9.]*)$" - }, - "$designer": { - "title": "$designer", - "type": "object", - "description": "Extra information for the Bot Framework Designer." - }, - "id": { - "type": "string", - "title": "Id", - "description": "Optional id for the dialog" - }, - "prompt": { - "$type": "Microsoft.IActivityTemplate", - "title": "Initial prompt", - "description": "Message to send to collect information.", - "examples": [ - "What is your birth date?" - ], - "$ref": "#/definitions/Microsoft.IActivityTemplate" - }, - "unrecognizedPrompt": { - "$type": "Microsoft.IActivityTemplate", - "title": "Unrecognized prompt", - "description": "Message to send if user response is not recognized.", - "examples": [ - "Sorry, I do not understand '{turn.activity.text'}. Let's try again. What is your birth date?" - ], - "$ref": "#/definitions/Microsoft.IActivityTemplate" - }, - "invalidPrompt": { - "$type": "Microsoft.IActivityTemplate", - "title": "Invalid prompt", - "description": "Message to send if user response is invalid. Relies on specified validation expressions.", - "examples": [ - "Sorry, '{this.value}' does not work. I need a number between 1-150. What is your age?" - ], - "$ref": "#/definitions/Microsoft.IActivityTemplate" - }, - "defaultValueResponse": { - "$type": "Microsoft.IActivityTemplate", - "title": "Default value response", - "description": "Message to send when max turn count (if specified) has been exceeded and the default value is selected as the value.", - "examples": [ - "Sorry, I'm having trouble understanding you. I will just use {this.options.defaultValue} for now. You can say 'I'm 36 years old' to change it." - ], - "$ref": "#/definitions/Microsoft.IActivityTemplate" - }, - "maxTurnCount": { - "type": "integer", - "title": "Max turn count", - "description": "Maximum number of re-prompt attempts to collect information.", - "default": 3, - "examples": [ - 3 - ] - }, - "validations": { - "type": "array", - "title": "Validation expressions", - "description": "Expression to validate user input.", - "examples": [ - "int(this.value) > 1 && int(this.value) <= 150", - "count(this.value) < 300" - ], - "items": { - "$role": "expression", - "type": "string", - "description": "String must contain an expression." - } - }, - "property": { - "$role": "expression", - "title": "Property", - "description": "Property to store collected information. Input will be skipped if property has value (unless 'Always prompt' is true).", - "examples": [ - "$birthday", - "user.name", - "conversation.issueTitle", - "dialog.favColor" - ], - "type": "string" - }, - "defaultValue": { - "$role": "expression", - "title": "Default value", - "description": "Expression to examine on each turn of the conversation as possible value to the property.", - "examples": [ - "@userName", - "coalesce(@number, @partySize)" - ], - "type": "string" - }, - "alwaysPrompt": { - "type": "boolean", - "title": "Always prompt", - "description": "Collect information even if the specified 'property' is not empty.", - "default": false, - "examples": [ - false - ] - }, - "allowInterruptions": { - "type": "string", - "title": "Allow Interruptions", - "description": "A boolean expression that determines whether the parent should be allowed to interrupt the input.", - "default": "true", - "examples": [ - "true" - ] - }, - "outputFormat": { - "type": "string", - "enum": [ - "none", - "trim", - "lowercase", - "uppercase" - ], - "title": "Output format", - "description": "Format of output.", - "default": "none" - } - }, - "additionalProperties": false, - "patternProperties": { - "^\\$": { - "type": "string" - } - }, - "anyOf": [ - { - "title": "Reference", - "required": [ - "$copy" - ] - }, - { - "title": "Type", - "required": [ - "$type" - ] - } - ] - }, - "Microsoft.TextTemplate": { - "$role": "unionType(Microsoft.ITextTemplate)", - "title": "Microsoft TextTemplate", - "description": "Lg tempalte to evaluate to create text", - "type": "object", - "properties": { - "$type": { - "title": "$type", - "description": "Defines the valid properties for the component you are configuring (from a dialog .schema file)", - "type": "string", - "pattern": "^[a-zA-Z][a-zA-Z0-9.]*$", - "const": "Microsoft.TextTemplate" - }, - "$copy": { - "title": "$copy", - "description": "Copy the definition by id from a .dialog file.", - "type": "string", - "pattern": "^(([a-zA-Z][a-zA-Z0-9.]*)?(#[a-zA-Z][a-zA-Z0-9.]*)?)$" - }, - "$id": { - "title": "$id", - "description": "Inline id for reuse of an inline definition", - "type": "string", - "pattern": "^([a-zA-Z][a-zA-Z0-9.]*)$" - }, - "$designer": { - "title": "$designer", - "type": "object", - "description": "Extra information for the Bot Framework Designer." - }, - "template": { - "title": "Template", - "Description": "Language Generator template to evaluate to create the text", - "type": "string" - } - }, - "additionalProperties": false, - "patternProperties": { - "^\\$": { - "type": "string" - } - }, - "anyOf": [ - { - "title": "Reference", - "required": [ - "$copy" - ] - }, - { - "title": "Type", - "required": [ - "template", - "$type" - ] - } - ] - }, - "Microsoft.TraceActivity": { - "$role": "unionType(Microsoft.IDialog)", - "title": "Send a TraceActivity", - "description": "Send a trace activity to the transcript logger and/ or Bot Framework Emulator.", - "type": "object", - "properties": { - "$type": { - "title": "$type", - "description": "Defines the valid properties for the component you are configuring (from a dialog .schema file)", - "type": "string", - "pattern": "^[a-zA-Z][a-zA-Z0-9.]*$", - "const": "Microsoft.TraceActivity" - }, - "$copy": { - "title": "$copy", - "description": "Copy the definition by id from a .dialog file.", - "type": "string", - "pattern": "^(([a-zA-Z][a-zA-Z0-9.]*)?(#[a-zA-Z][a-zA-Z0-9.]*)?)$" - }, - "$id": { - "title": "$id", - "description": "Inline id for reuse of an inline definition", - "type": "string", - "pattern": "^([a-zA-Z][a-zA-Z0-9.]*)$" - }, - "$designer": { - "title": "$designer", - "type": "object", - "description": "Extra information for the Bot Framework Designer." - }, - "name": { - "type": "string", - "title": "Name", - "description": "Name of the trace activity" - }, - "valueType": { - "type": "string", - "title": "Value type", - "description": "Type of value" - }, - "value": { - "$role": "expression", - "title": "Value", - "description": "Property that holds the value to send as trace activity.", - "type": "string" - } - }, - "additionalProperties": false, - "patternProperties": { - "^\\$": { - "type": "string" - } - }, - "anyOf": [ - { - "title": "Reference", - "required": [ - "$copy" - ] - }, - { - "title": "Type", - "required": [ - "$type" - ] - } - ] - }, - "Microsoft.TrueSelector": { - "$role": "unionType(Microsoft.ITriggerSelector)", - "title": "True Trigger Selector", - "description": "Selector for all true events", - "type": "object", - "properties": { - "$type": { - "title": "$type", - "description": "Defines the valid properties for the component you are configuring (from a dialog .schema file)", - "type": "string", - "pattern": "^[a-zA-Z][a-zA-Z0-9.]*$", - "const": "Microsoft.TrueSelector" - }, - "$copy": { - "title": "$copy", - "description": "Copy the definition by id from a .dialog file.", - "type": "string", - "pattern": "^(([a-zA-Z][a-zA-Z0-9.]*)?(#[a-zA-Z][a-zA-Z0-9.]*)?)$" - }, - "$id": { - "title": "$id", - "description": "Inline id for reuse of an inline definition", - "type": "string", - "pattern": "^([a-zA-Z][a-zA-Z0-9.]*)$" - }, - "$designer": { - "title": "$designer", - "type": "object", - "description": "Extra information for the Bot Framework Designer." - } - }, - "additionalProperties": false, - "patternProperties": { - "^\\$": { - "type": "string" - } - }, - "anyOf": [ - { - "title": "Reference", - "required": [ - "$copy" - ] - }, - { - "title": "Type", - "required": [ - "$type" - ] - } - ] - }, - "Microsoft.UrlEntityRecognizer": { - "$role": "unionType(Microsoft.EntityRecognizers)", - "title": "Url Entity Recognizer", - "description": "Recognizer which recognizes urls (example: http://bing.com)", - "type": "object", - "properties": { - "$type": { - "title": "$type", - "description": "Defines the valid properties for the component you are configuring (from a dialog .schema file)", - "type": "string", - "pattern": "^[a-zA-Z][a-zA-Z0-9.]*$", - "const": "Microsoft.UrlEntityRecognizer" - }, - "$copy": { - "title": "$copy", - "description": "Copy the definition by id from a .dialog file.", - "type": "string", - "pattern": "^(([a-zA-Z][a-zA-Z0-9.]*)?(#[a-zA-Z][a-zA-Z0-9.]*)?)$" - }, - "$id": { - "title": "$id", - "description": "Inline id for reuse of an inline definition", - "type": "string", - "pattern": "^([a-zA-Z][a-zA-Z0-9.]*)$" - }, - "$designer": { - "title": "$designer", - "type": "object", - "description": "Extra information for the Bot Framework Designer." - } - }, - "additionalProperties": false, - "patternProperties": { - "^\\$": { - "type": "string" - } - }, - "anyOf": [ - { - "title": "Reference", - "required": [ - "$copy" - ] - }, - { - "title": "Type", - "required": [ - "$type" - ] - } - ] - } - } -} diff --git a/BotProject/CSharp/Scripts/create.ps1 b/BotProject/CSharp/Scripts/create.ps1 deleted file mode 100644 index bd30ddf40c..0000000000 --- a/BotProject/CSharp/Scripts/create.ps1 +++ /dev/null @@ -1,185 +0,0 @@ -Param( - [string] $name, - [string] $location, - [string] $appId, - [string] $appPassword, - [string] $environment, - [string] $projDir = $(Get-Location), - [string] $logFile = $(Join-Path $PSScriptRoot .. "create_log.txt") -) - -if ($PSVersionTable.PSVersion.Major -lt 6){ - Write-Host "! Powershell 6 is required, current version is $($PSVersionTable.PSVersion.Major), please refer following documents for help." - Write-Host "For Windows - https://docs.microsoft.com/en-us/powershell/scripting/install/installing-powershell-core-on-windows?view=powershell-6" - Write-Host "For Mac - https://docs.microsoft.com/en-us/powershell/scripting/install/installing-powershell-core-on-macos?view=powershell-6" - Break -} - -# Reset log file -if (Test-Path $logFile) { - Clear-Content $logFile -Force | Out-Null -} -else { - New-Item -Path $logFile | Out-Null -} - -if (-not (Test-Path (Join-Path $projDir 'appsettings.json'))) -{ - Write-Host "! Could not find an 'appsettings.json' file in the current directory." -ForegroundColor DarkRed - Write-Host "+ Please re-run this script from your project directory." -ForegroundColor Magenta - Break -} - -# Get mandatory parameters -if (-not $name) { - $name = Read-Host "? Bot Name (used as default name for resource group and deployed resources)" -} - -if (-not $environment) -{ - $environment = Read-Host "? Environment Name (single word, all lowercase)" - $environment = $environment.ToLower().Split(" ") | Select-Object -First 1 -} - -if (-not $location) { - $location = Read-Host "? Azure resource group region" -} - -if (-not $appPassword) { - $appPassword = Read-Host "? Password for MSA app registration (must be at least 16 characters long, contain at least 1 special character, and contain at least 1 numeric character)" -} - -if (-not $appId) { - # Create app registration - $app = (az ad app create ` - --display-name $name ` - --password `"$($appPassword)`" ` - --available-to-other-tenants ` - --reply-urls 'https://token.botframework.com/.auth/web/redirect' ` - --output json) - - # Retrieve AppId - if ($app) { - $appId = ($app | ConvertFrom-Json) | Select-Object -ExpandProperty appId - } - - if(-not $appId) { - Write-Host "! Could not provision Microsoft App Registration automatically. Review the log for more information." -ForegroundColor DarkRed - Write-Host "! Log: $($logFile)" -ForegroundColor DarkRed - Write-Host "+ Provision an app manually in the Azure Portal, then try again providing the -appId and -appPassword arguments. See https://aka.ms/vamanualappcreation for more information." -ForegroundColor Magenta - Break - } -} - -$resourceGroup = "$name-$environment" -$servicePlanName = "$name-$environment" - -# Get timestamp -$timestamp = Get-Date -f MMddyyyyHHmmss - -# Create resource group -Write-Host "> Creating resource group ..." -(az group create --name $resourceGroup --location $location --output json) 2>> $logFile | Out-Null - -# Deploy Azure services -Write-Host "> Validating Azure deployment ..." -$validation = az group deployment validate ` - --resource-group $resourcegroup ` - --template-file "$(Join-Path $PSScriptRoot '..' 'DeploymentTemplates' 'template-with-preexisting-rg.json')" ` - --parameters appId=$appId appSecret="`"$($appPassword)`"" newAppServicePlanName=$servicePlanName appServicePlanLocation=$location botId=$name ` - --output json - -if ($validation) { - $validation >> $logFile - $validation = $validation | ConvertFrom-Json - - if (-not $validation.error) { - Write-Host "> Deploying Azure services (this could take a while)..." -ForegroundColor Yellow - $deployment = az group deployment create ` - --name $timestamp ` - --resource-group $resourceGroup ` - --template-file "$(Join-Path $PSScriptRoot '..' 'DeploymentTemplates' 'template-with-preexisting-rg.json')" ` - --parameters appId=$appId appSecret="`"$($appPassword)`"" newAppServicePlanName=$servicePlanName appServicePlanLocation=$location botId=$name ` - --output json - } - else { - Write-Host "! Template is not valid with provided parameters. Review the log for more information." -ForegroundColor DarkRed - Write-Host "! Error: $($validation.error.message)" -ForegroundColor DarkRed - Write-Host "! Log: $($logFile)" -ForegroundColor DarkRed - Write-Host "+ To delete this resource group, run 'az group delete -g $($resourceGroup) --no-wait'" -ForegroundColor Magenta - Break - } -} - - -# Get deployment outputs -$outputs = az group deployment show ` - --name $timestamp ` - --resource-group $resourceGroup ` - --output json 2>> $logFile - -# If it succeeded then we perform the remainder of the steps -if ($outputs) -{ - # Log and convert to JSON - $outputs >> $logFile - $outputs = $outputs | ConvertFrom-Json - $outputMap = @{} - $outputs.PSObject.Properties | Foreach-Object { $outputMap[$_.Name] = $_.Value } - - # Update appsettings.json - Write-Host "> Updating appsettings.json ..." - if (Test-Path $(Join-Path $projDir appsettings.json)) { - $settings = Get-Content $(Join-Path $projDir appsettings.json) | ConvertFrom-Json - } - else { - $settings = New-Object PSObject - } - - $settings | Add-Member -Type NoteProperty -Force -Name 'microsoftAppId' -Value $appId - $settings | Add-Member -Type NoteProperty -Force -Name 'microsoftAppPassword' -Value $appPassword - $settings | Add-Member -Type NoteProperty -Force -Name 'bot' -Value "RunningInstance" - - foreach ($key in $outputMap.Keys) { $settings | Add-Member -Type NoteProperty -Force -Name $key -Value $outputMap[$key].value } - $settings | ConvertTo-Json -depth 100 | Out-File $(Join-Path $projDir appsettings.json) - - Write-Host "> Done." - Write-Host "- App Id: $appId" - Write-Host "- App Password: $appPassword" - Write-Host "- Resource Group: $resourceGroup" - Write-Host "- ServicePlan: $servicePlanName" - Write-Host "- Bot Name: $name" - Write-Host "- Web App Name : $name" -} -else -{ - # Check for failed deployments - $operations = az group deployment operation list -g $resourceGroup -n $timestamp --output json 2>> $logFile | Out-Null - - if ($operations) { - $operations = $operations | ConvertFrom-Json - $failedOperations = $operations | Where { $_.properties.statusmessage.error -ne $null } - if ($failedOperations) { - foreach ($operation in $failedOperations) { - switch ($operation.properties.statusmessage.error.code) { - "MissingRegistrationForLocation" { - Write-Host "! Deployment failed for resource of type $($operation.properties.targetResource.resourceType). This resource is not avaliable in the location provided." -ForegroundColor DarkRed - Write-Host "+ Update the .\Deployment\Resources\parameters.template.json file with a valid region for this resource and provide the file path in the -parametersFile parameter." -ForegroundColor Magenta - } - default { - Write-Host "! Deployment failed for resource of type $($operation.properties.targetResource.resourceType)." - Write-Host "! Code: $($operation.properties.statusMessage.error.code)." - Write-Host "! Message: $($operation.properties.statusMessage.error.message)." - } - } - } - } - } - else { - Write-Host "! Deployment failed. Please refer to the log file for more information." -ForegroundColor DarkRed - Write-Host "! Log: $($logFile)" -ForegroundColor DarkRed - } - - Write-Host "+ To delete this resource group, run 'az group delete -g $($resourceGroup) --no-wait'" -ForegroundColor Magenta - Break -} \ No newline at end of file diff --git a/BotProject/CSharp/Scripts/deploy.ps1 b/BotProject/CSharp/Scripts/deploy.ps1 deleted file mode 100644 index 1efad16116..0000000000 --- a/BotProject/CSharp/Scripts/deploy.ps1 +++ /dev/null @@ -1,137 +0,0 @@ -Param( - [string] $name, - [string] $environment, - [string] $luisAuthoringKey, - [string] $luisAuthoringRegion, - [string] $projFolder = $(Get-Location), - [string] $botPath, - [string] $logFile = $(Join-Path $PSScriptRoot .. "deploy_log.txt") -) - -if ($PSVersionTable.PSVersion.Major -lt 6){ - Write-Host "! Powershell 6 is required, current version is $($PSVersionTable.PSVersion.Major), please refer following documents for help." - Write-Host "For Windows - https://docs.microsoft.com/en-us/powershell/scripting/install/installing-powershell-core-on-windows?view=powershell-6" - Write-Host "For Mac - https://docs.microsoft.com/en-us/powershell/scripting/install/installing-powershell-core-on-macos?view=powershell-6" - Break -} - -# Get mandatory parameters -if (-not $name) { - $name = Read-Host "? Bot Web App Name" -} - -if (-not $environment) -{ - $environment = Read-Host "? Environment Name (single word, all lowercase)" - $environment = $environment.ToLower().Split(" ") | Select-Object -First 1 -} - -if (-not $botPath) { - $botPath = Read-Host "? The Reletive Path Of Bot" -} - - -# Reset log file -if (Test-Path $logFile) { - Clear-Content $logFile -Force | Out-Null -} -else { - New-Item -Path $logFile | Out-Null -} - -# Check for existing deployment files -if (-not (Test-Path (Join-Path $projFolder '.deployment'))) { - # Add needed deployment files for az - az bot prepare-deploy --lang Csharp --code-dir $projFolder --proj-file-path BotProject.csproj --output json | Out-Null -} - -# Delete src zip, if it exists -$zipPath = $(Join-Path $projFolder 'code.zip') -if (Test-Path $zipPath) { - Remove-Item $zipPath -Force | Out-Null -} - -# Perform dotnet publish step ahead of zipping up -$publishFolder = $(Join-Path $projFolder 'bin\Release\netcoreapp2.2') -dotnet publish -c release -o $publishFolder -v q > $logFile - -# Copy bot files to running folder -$remoteBotPath = $(Join-Path $publishFolder "RunningInstance") -Remove-Item $remoteBotPath -Recurse -ErrorAction Ignore -Copy-Item -Path $botPath -Recurse -Destination $remoteBotPath -Container -Force - -# Merge from custom config files -$customConfigFiles = Get-ChildItem -Path $remoteBotPath -Include "appsettings.json" -Recurse -Force -if ($customConfigFiles) -{ - if (Test-Path $(Join-Path $publishFolder appsettings.json)) { - $settings = Get-Content $(Join-Path $publishFolder appsettings.json) | ConvertFrom-Json - } - else { - $settings = New-Object PSObject - } - - $customConfig = @{} - $customSetting = Get-Content $customConfigFiles.FullName | ConvertFrom-Json - $customSetting.PSObject.Properties | Foreach-Object { $customConfig[$_.Name] = $_.Value } - foreach ($key in $customConfig.Keys) { $settings | Add-Member -Type NoteProperty -Force -Name $key -Value $customConfig[$key] } - - $settings | ConvertTo-Json -depth 100 | Out-File $(Join-Path $publishFolder appsettings.json) -} - -# Add Luis Config to appsettings -if ($luisAuthoringKey -and $luisAuthoringRegion) -{ - # change setting file in publish folder - if (Test-Path $(Join-Path $publishFolder appsettings.json)) { - $settings = Get-Content $(Join-Path $publishFolder appsettings.json) | ConvertFrom-Json - } - else { - $settings = New-Object PSObject - } - - $luisConfigFiles = Get-ChildItem -Path $publishFolder -Include "luis.settings*" -Recurse -Force - - $luisAppIds = @{} - - foreach ($luisConfigFile in $luisConfigFiles) - { - $luisSetting = Get-Content $luisConfigFile.FullName | ConvertFrom-Json - $luis = $luisSetting.luis - $luis.PSObject.Properties | Foreach-Object { $luisAppIds[$_.Name] = $_.Value } - } - - $luisEndpoint = "https://$luisAuthoringRegion.api.cognitive.microsoft.com" - - $luisConfig = @{} - - $luisConfig["endpointKey"] = $luisAuthoringKey - $luisConfig["endpoint"] = $luisEndpoint - - foreach ($key in $luisAppIds.Keys) { $luisConfig[$key] = $luisAppIds[$key] } - - $settings | Add-Member -Type NoteProperty -Force -Name 'luis' -Value $luisConfig - - $settings | ConvertTo-Json -depth 100 | Out-File $(Join-Path $publishFolder appsettings.json) -} - -$resourceGroup = "$name-$environment" - -if($?) -{ - # Compress source code - Get-ChildItem -Path "$($publishFolder)" | Compress-Archive -DestinationPath "$($zipPath)" -Force | Out-Null - - # Publish zip to Azure - Write-Host "> Publishing to Azure ..." -ForegroundColor Green - (az webapp deployment source config-zip ` - --resource-group $resourceGroup ` - --name $name ` - --src $zipPath ` - --output json) 2>> $logFile | Out-Null -} -else -{ - Write-Host "! Could not deploy automatically to Azure. Review the log for more information." -ForegroundColor DarkRed - Write-Host "! Log: $($logFile)" -ForegroundColor DarkRed -} \ No newline at end of file diff --git a/BotProject/CSharp/Startup.cs b/BotProject/CSharp/Startup.cs deleted file mode 100644 index 12358ce19d..0000000000 --- a/BotProject/CSharp/Startup.cs +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Bot.Builder.BotFramework; -using Microsoft.Bot.Builder.Dialogs.Declarative.Types; -using Microsoft.Bot.Connector.Authentication; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; - -namespace Microsoft.Bot.Builder.ComposerBot.Json -{ - public class Startup - { - public Startup(IHostingEnvironment env, IConfiguration configuration) - { - this.HostingEnvironment = env; - this.Configuration = configuration; - } - - public IHostingEnvironment HostingEnvironment { get; } - - public IConfiguration Configuration { get; } - - // This method gets called by the runtime. Use this method to add services to the container. - public void ConfigureServices(IServiceCollection services) - { - services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1); - - services.AddSingleton(this.Configuration); - - // Create the credential provider to be used with the Bot Framework Adapter. - services.AddSingleton(); - - services.AddSingleton(); - - TypeFactory.Configuration = this.Configuration; - services.AddSingleton((sp) => new BotManager(this.Configuration)); - } - - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IHostingEnvironment env) - { - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - else - { - app.UseHsts(); - } - - app.UseDefaultFiles(); - app.UseStaticFiles(); - app.UseWebSockets(); - - //app.UseHttpsRedirection(); - app.UseMvc(); - } - } -} diff --git a/BotProject/CSharp/Tests/ActionsTests.cs b/BotProject/CSharp/Tests/ActionsTests.cs deleted file mode 100644 index 173086e547..0000000000 --- a/BotProject/CSharp/Tests/ActionsTests.cs +++ /dev/null @@ -1,235 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using Microsoft.Bot.Builder; -using Microsoft.Bot.Builder.Adapters; -using Microsoft.Bot.Builder.Dialogs; -using Microsoft.Bot.Builder.Dialogs.Adaptive; -using Microsoft.Bot.Builder.Dialogs.Debugging; -using Microsoft.Bot.Builder.Dialogs.Declarative; -using Microsoft.Bot.Builder.Dialogs.Declarative.Resources; -using Microsoft.Bot.Builder.Dialogs.Declarative.Types; -using Microsoft.Bot.Schema; -using Microsoft.Extensions.Configuration; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using System; -using System.IO; -using System.Threading.Tasks; - -namespace Tests -{ - [TestClass] - public class ActionsTests - { - private static string getOsPath(string path) => Path.Combine(path.TrimEnd('\\').Split('\\')); - - private static readonly string samplesDirectory = getOsPath(@"..\..\..\..\..\..\Composer\packages\server\assets\projects"); - - private static string getFolderPath(string path) - { - return Path.Combine(samplesDirectory, path); - } - - - [ClassInitialize] - public static void ClassInitialize(TestContext context) - { - TypeFactory.Configuration = new ConfigurationBuilder().AddInMemoryCollection().Build(); - string path = Path.GetFullPath(Path.Combine(Environment.CurrentDirectory, samplesDirectory)); - } - - public TestContext TestContext { get; set; } - - [TestMethod] - public async Task Actions_01Actions() - { - await BuildTestFlow(getFolderPath("ActionsSample")) - .SendConversationUpdate() - .AssertReply(String.Format("I can show you examples on how to use actions. Enter the number next to the entity that you with to see in action.{0}01 - Actions{0}02 - EndTurn{0}03 - IfCondiftion{0}04 - EditArray, Foreach{0}05 - EndDialog{0}06 - HttpRequest{0}07 - SwitchCondition{0}08 - RepeatDialog{0}09 - TraceAndLog{0}10 - EditActions{0}11 - ReplaceDialog{0}12 - EmitEvent{0}13 - QnAMaker", Environment.NewLine)) - .Send("01") - .AssertReply("Step 1") - .AssertReply("Step 2") - .AssertReply("Step 3") - .AssertReply("user.age is set to 18") - .AssertReply("user.age is set to null") - .StartTestAsync(); - } - - [TestMethod] - public async Task Actions_02EndTurn() - { - await BuildTestFlow(getFolderPath("ActionsSample")) - .SendConversationUpdate() - .AssertReply(String.Format("I can show you examples on how to use actions. Enter the number next to the entity that you with to see in action.{0}01 - Actions{0}02 - EndTurn{0}03 - IfCondiftion{0}04 - EditArray, Foreach{0}05 - EndDialog{0}06 - HttpRequest{0}07 - SwitchCondition{0}08 - RepeatDialog{0}09 - TraceAndLog{0}10 - EditActions{0}11 - ReplaceDialog{0}12 - EmitEvent{0}13 - QnAMaker", Environment.NewLine)) - .Send("02") - .AssertReply("What's up?") - .Send("Nothing") - .AssertReply("Oh I see!") - .StartTestAsync(); - } - - [TestMethod] - public async Task Actions_03IfCondition() - { - await BuildTestFlow(getFolderPath("ActionsSample")) - .SendConversationUpdate() - .AssertReply(String.Format("I can show you examples on how to use actions. Enter the number next to the entity that you with to see in action.{0}01 - Actions{0}02 - EndTurn{0}03 - IfCondiftion{0}04 - EditArray, Foreach{0}05 - EndDialog{0}06 - HttpRequest{0}07 - SwitchCondition{0}08 - RepeatDialog{0}09 - TraceAndLog{0}10 - EditActions{0}11 - ReplaceDialog{0}12 - EmitEvent{0}13 - QnAMaker", Environment.NewLine)).Send("03") - .AssertReply("Hello, I'm Zoidberg. What is your name?") - .Send("Carlos") - .AssertReply("Hello Carlos, nice to talk to you!") - .StartTestAsync(); - } - - [TestMethod] - public async Task Actions_04EditArray() - { - await BuildTestFlow(getFolderPath("ActionsSample")) - .SendConversationUpdate() - .AssertReply(String.Format("I can show you examples on how to use actions. Enter the number next to the entity that you with to see in action.{0}01 - Actions{0}02 - EndTurn{0}03 - IfCondiftion{0}04 - EditArray, Foreach{0}05 - EndDialog{0}06 - HttpRequest{0}07 - SwitchCondition{0}08 - RepeatDialog{0}09 - TraceAndLog{0}10 - EditActions{0}11 - ReplaceDialog{0}12 - EmitEvent{0}13 - QnAMaker", Environment.NewLine)).Send("04") - .AssertReply("Here are the index and values in the array.") - .AssertReply("0: 11111") - .AssertReply("1: 40000") - .AssertReply("2: 222222") - .AssertReply("If each page shows two items, here are the index and values") - .AssertReply("0: 11111") - .AssertReply("1: 40000") - .AssertReply("0: 222222") - .StartTestAsync(); - } - - [TestMethod] - public async Task Actions_05EndDialog() - { - await BuildTestFlow(getFolderPath("ActionsSample")) - .SendConversationUpdate() - .AssertReply(String.Format("I can show you examples on how to use actions. Enter the number next to the entity that you with to see in action.{0}01 - Actions{0}02 - EndTurn{0}03 - IfCondiftion{0}04 - EditArray, Foreach{0}05 - EndDialog{0}06 - HttpRequest{0}07 - SwitchCondition{0}08 - RepeatDialog{0}09 - TraceAndLog{0}10 - EditActions{0}11 - ReplaceDialog{0}12 - EmitEvent{0}13 - QnAMaker", Environment.NewLine)).Send("05") - .AssertReply("Hello, I'm Zoidberg. What is your name?") - .Send("luhan") - .AssertReply("Hello luhan, nice to talk to you!") - .AssertReply("I'm a joke bot. To get started say \"joke\".") - .Send("joke") - .AssertReply("Why did the chicken cross the road?") - .Send("I don't know") - .AssertReply("To get to the other side!") - .StartTestAsync(); - } - - [TestMethod] - public async Task Actions_06SwitchCondition() - { - await BuildTestFlow(getFolderPath("ActionsSample")) - .SendConversationUpdate() - .AssertReply(String.Format("I can show you examples on how to use actions. Enter the number next to the entity that you with to see in action.{0}01 - Actions{0}02 - EndTurn{0}03 - IfCondiftion{0}04 - EditArray, Foreach{0}05 - EndDialog{0}06 - HttpRequest{0}07 - SwitchCondition{0}08 - RepeatDialog{0}09 - TraceAndLog{0}10 - EditActions{0}11 - ReplaceDialog{0}12 - EmitEvent{0}13 - QnAMaker", Environment.NewLine)).Send("07") - .AssertReply("Please select a value from below:\n\n 1. Test1\n 2. Test2\n 3. Test3") - .Send("Test1") - .AssertReply("You select: Test1") - .AssertReply("You select: 1") - .StartTestAsync(); - } - - [TestMethod] - public async Task Actions_07RepeatDialog() - { - await BuildTestFlow(getFolderPath("ActionsSample")) - .SendConversationUpdate() - .AssertReply(String.Format("I can show you examples on how to use actions. Enter the number next to the entity that you with to see in action.{0}01 - Actions{0}02 - EndTurn{0}03 - IfCondiftion{0}04 - EditArray, Foreach{0}05 - EndDialog{0}06 - HttpRequest{0}07 - SwitchCondition{0}08 - RepeatDialog{0}09 - TraceAndLog{0}10 - EditActions{0}11 - ReplaceDialog{0}12 - EmitEvent{0}13 - QnAMaker", Environment.NewLine)).Send("08") - .AssertReply("Do you want to repeat this dialog, yes to repeat, no to end this dialog (1) Yes or (2) No") - .Send("Yes") - .AssertReply("Do you want to repeat this dialog, yes to repeat, no to end this dialog (1) Yes or (2) No") - .Send("No") - .StartTestAsync(); - } - - [TestMethod] - public async Task Actions_08TraceAndLog() - { - await BuildTestFlow(getFolderPath("ActionsSample"), sendTrace: true) - .SendConversationUpdate() - .AssertReply(String.Format("I can show you examples on how to use actions. Enter the number next to the entity that you with to see in action.{0}01 - Actions{0}02 - EndTurn{0}03 - IfCondiftion{0}04 - EditArray, Foreach{0}05 - EndDialog{0}06 - HttpRequest{0}07 - SwitchCondition{0}08 - RepeatDialog{0}09 - TraceAndLog{0}10 - EditActions{0}11 - ReplaceDialog{0}12 - EmitEvent{0}13 - QnAMaker", Environment.NewLine)).Send("09") - .Send("luhan") - .AssertReply(activity => - { - var trace = (Activity)activity; - Assert.AreEqual(ActivityTypes.Trace, trace.Type, "should be trace activity"); - }) - .StartTestAsync(); - } - - [TestMethod] - public async Task Actions_09EditActions() - { - await BuildTestFlow(getFolderPath("ActionsSample")) - .SendConversationUpdate() - .AssertReply(String.Format("I can show you examples on how to use actions. Enter the number next to the entity that you with to see in action.{0}01 - Actions{0}02 - EndTurn{0}03 - IfCondiftion{0}04 - EditArray, Foreach{0}05 - EndDialog{0}06 - HttpRequest{0}07 - SwitchCondition{0}08 - RepeatDialog{0}09 - TraceAndLog{0}10 - EditActions{0}11 - ReplaceDialog{0}12 - EmitEvent{0}13 - QnAMaker", Environment.NewLine)).Send("10") - .AssertReply("Hello, I'm Zoidberg. What is your name?") - .Send("luhan") - .AssertReply("Hello luhan, nice to talk to you!") - .AssertReply("Goodbye!") - .StartTestAsync(); - } - - [TestMethod] - public async Task Actions_10ReplaceDialog() - { - await BuildTestFlow(getFolderPath("ActionsSample")) - .SendConversationUpdate() - .AssertReply(String.Format("I can show you examples on how to use actions. Enter the number next to the entity that you with to see in action.{0}01 - Actions{0}02 - EndTurn{0}03 - IfCondiftion{0}04 - EditArray, Foreach{0}05 - EndDialog{0}06 - HttpRequest{0}07 - SwitchCondition{0}08 - RepeatDialog{0}09 - TraceAndLog{0}10 - EditActions{0}11 - ReplaceDialog{0}12 - EmitEvent{0}13 - QnAMaker", Environment.NewLine)).Send("11") - .AssertReply("Hello, I'm Zoidberg. What is your name?") - .Send("luhan") - .AssertReply("Hello luhan, nice to talk to you! Please either enter 'joke' or 'fortune' to replace the dialog you want.") - .Send("joke") - .AssertReply("Why did the chicken cross the road?") - .Send("Why?") - .AssertReply("To get to the other side!") - .Send("future") - .AssertReply("Seeing into your future...") - .AssertReply("I see great things in your future!") - .AssertReply("Potentially a successful demo") - .StartTestAsync(); - } - - [TestMethod] - public async Task Actions_11EmitEvent() - { - await BuildTestFlow(getFolderPath("ActionsSample")) - .SendConversationUpdate() - .AssertReply(String.Format("I can show you examples on how to use actions. Enter the number next to the entity that you with to see in action.{0}01 - Actions{0}02 - EndTurn{0}03 - IfCondiftion{0}04 - EditArray, Foreach{0}05 - EndDialog{0}06 - HttpRequest{0}07 - SwitchCondition{0}08 - RepeatDialog{0}09 - TraceAndLog{0}10 - EditActions{0}11 - ReplaceDialog{0}12 - EmitEvent{0}13 - QnAMaker", Environment.NewLine)).Send("12") - .AssertReply("Say moo to get a response, say emit to emit a event.") - .Send("moo") - .AssertReply("Yippee ki-yay!") - .Send("emit") - .AssertReply("CustomEvent Fired.") - .StartTestAsync(); - } - - private TestFlow BuildTestFlow(string folderPath, bool sendTrace = false) - { - TypeFactory.Configuration = new ConfigurationBuilder().Build(); - var storage = new MemoryStorage(); - var convoState = new ConversationState(storage); - var userState = new UserState(storage); - var adapter = new TestAdapter(TestAdapter.CreateConversation(TestContext.TestName), sendTrace); - var resourceExplorer = new ResourceExplorer(); - resourceExplorer.AddFolder(folderPath); - adapter - .UseStorage(storage) - .UseState(userState, convoState) - .UseAdaptiveDialogs() - .UseLanguageGeneration(resourceExplorer, "common.lg") - .UseResourceExplorer(resourceExplorer) - .Use(new TranscriptLoggerMiddleware(new FileTranscriptLogger())); - - var resource = resourceExplorer.GetResource("Main.dialog"); - var dialog = DeclarativeTypeLoader.Load(resource, resourceExplorer, DebugSupport.SourceMap); - DialogManager dm = new DialogManager(dialog); - - return new TestFlow(adapter, async (turnContext, cancellationToken) => - { - if (dialog is AdaptiveDialog planningDialog) - { - await dm.OnTurnAsync(turnContext, cancellationToken).ConfigureAwait(false); - } - }); - } - } -} diff --git a/BotProject/CSharp/Tests/ControllingConversationTests.cs b/BotProject/CSharp/Tests/ControllingConversationTests.cs deleted file mode 100644 index ad75bbae45..0000000000 --- a/BotProject/CSharp/Tests/ControllingConversationTests.cs +++ /dev/null @@ -1,116 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using Microsoft.Bot.Builder; -using Microsoft.Bot.Builder.Adapters; -using Microsoft.Bot.Builder.Dialogs; -using Microsoft.Bot.Builder.Dialogs.Adaptive; -using Microsoft.Bot.Builder.Dialogs.Debugging; -using Microsoft.Bot.Builder.Dialogs.Declarative; -using Microsoft.Bot.Builder.Dialogs.Declarative.Resources; -using Microsoft.Bot.Builder.Dialogs.Declarative.Types; -using Microsoft.Bot.Builder.LanguageGeneration; -using Microsoft.Bot.Builder.ComposerBot.Json; -using Microsoft.Bot.Schema; -using Microsoft.Extensions.Configuration; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using System; -using System.Collections.Generic; -using System.IO; -using System.Threading.Tasks; - -namespace Tests -{ - [TestClass] - public class ControllingConversationTests - { - private static string getOsPath(string path) => Path.Combine(path.TrimEnd('\\').Split('\\')); - - private static readonly string samplesDirectory = getOsPath(@"..\..\..\..\..\..\Composer\packages\server\assets\projects"); - - private static ResourceExplorer resourceExplorer = new ResourceExplorer(); - - - [ClassInitialize] - public static void ClassInitialize(TestContext context) - { - TypeFactory.Configuration = new ConfigurationBuilder().AddInMemoryCollection().Build(); - string path = Path.GetFullPath(Path.Combine(Environment.CurrentDirectory, samplesDirectory, "ControllingConversationFlowSample")); - resourceExplorer.AddFolder(path); - } - - [ClassCleanup] - public static void ClassCleanup() - { - resourceExplorer.Dispose(); - } - - public TestContext TestContext { get; set; } - - [TestMethod] - public async Task ControllingConversationBotTest() - { - await BuildTestFlow() - .SendConversationUpdate() - .AssertReply(String.Format("Welcome to the Controlling Conversation sample. Choose from the list below to try.{0}You can also type \"Cancel\" to cancel any dialog or \"Endturn\" to explicitly accept an input.", Environment.NewLine)) - .Send("01") - .AssertReply("Hello, What's your age?") - .Send("18") - .AssertReply("Your age is 18 which satisified the condition that was evaluated") - .Send("02") - .AssertReply("Who are your?\n\n 1. Susan\n 2. Nick\n 3. Tom") - .Send("2") - .AssertReply("You selected Nick") - .AssertReply("This is the logic inside the \"Nick\" switch block.") - .Send("03") - .AssertReply("Pushed dialog.id into a list") - .AssertReply("0: 11111") - .AssertReply("1: 40000") - .AssertReply("2: 222222") - .Send("04") - .AssertReply("Pushed dialog.ids into a list") - .AssertReply("0: 11111") - .AssertReply("1: 40000") - .AssertReply("0: 222222") - .Send("06") - .Send("hi") - .Send("07") - .AssertReply("Do you want to repeat this dialog, yes to repeat, no to end this dialog (1) Yes or (2) No") - .Send("Yes") - .AssertReply("Do you want to repeat this dialog, yes to repeat, no to end this dialog (1) Yes or (2) No") - .Send("No") - .Send("05") - .AssertReply("Canceled.") - .StartTestAsync(); - } - - - private TestFlow BuildTestFlow(bool sendTrace = false) - { - TypeFactory.Configuration = new ConfigurationBuilder().Build(); - var storage = new MemoryStorage(); - var convoState = new ConversationState(storage); - var userState = new UserState(storage); - var adapter = new TestAdapter(TestAdapter.CreateConversation(TestContext.TestName), sendTrace); - adapter - .UseStorage(storage) - .UseState(userState, convoState) - .UseAdaptiveDialogs() - .UseLanguageGeneration(resourceExplorer, "common.lg") - .UseResourceExplorer(resourceExplorer) - .Use(new TranscriptLoggerMiddleware(new FileTranscriptLogger())); - - var resource = resourceExplorer.GetResource("Main.dialog"); - var dialog = DeclarativeTypeLoader.Load(resource, resourceExplorer, DebugSupport.SourceMap); - DialogManager dm = new DialogManager(dialog); - - return new TestFlow(adapter, async (turnContext, cancellationToken) => - { - if (dialog is AdaptiveDialog planningDialog) - { - await dm.OnTurnAsync(turnContext, cancellationToken).ConfigureAwait(false); - } - }); - } - } -} diff --git a/BotProject/CSharp/Tests/InputsTests.cs b/BotProject/CSharp/Tests/InputsTests.cs deleted file mode 100644 index a893278ca3..0000000000 --- a/BotProject/CSharp/Tests/InputsTests.cs +++ /dev/null @@ -1,148 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using Microsoft.Bot.Builder; -using Microsoft.Bot.Builder.Adapters; -using Microsoft.Bot.Builder.Dialogs; -using Microsoft.Bot.Builder.Dialogs.Adaptive; -using Microsoft.Bot.Builder.Dialogs.Debugging; -using Microsoft.Bot.Builder.Dialogs.Declarative; -using Microsoft.Bot.Builder.Dialogs.Declarative.Resources; -using Microsoft.Bot.Builder.Dialogs.Declarative.Types; -using Microsoft.Bot.Builder.LanguageGeneration; -using Microsoft.Bot.Builder.ComposerBot.Json; -using Microsoft.Bot.Schema; -using Microsoft.Extensions.Configuration; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using System; -using System.Collections.Generic; -using System.IO; -using System.Threading.Tasks; - -namespace Tests -{ - [TestClass] - public class InputsTests - { - private static string getOsPath(string path) => Path.Combine(path.TrimEnd('\\').Split('\\')); - - private static readonly string samplesDirectory = getOsPath(@"..\..\..\..\..\..\Composer\packages\server\assets\projects"); - - private static ResourceExplorer resourceExplorer = new ResourceExplorer(); - - - [ClassInitialize] - public static void ClassInitialize(TestContext context) - { - TypeFactory.Configuration = new ConfigurationBuilder().AddInMemoryCollection().Build(); - string path = Path.GetFullPath(Path.Combine(Environment.CurrentDirectory, samplesDirectory, "AskingQuestionsSample")); - resourceExplorer.AddFolder(path); - } - - [ClassCleanup] - public static void ClassCleanup() - { - resourceExplorer.Dispose(); - } - - public TestContext TestContext { get; set; } - - [TestMethod] - public async Task Inputs_01TextInput() - { - await BuildTestFlow() - .SendConversationUpdate() - .AssertReply(String.Format("Welcome to Input Sample Bot.{0}I can show you examples on how to use actions, You can enter number 01-07{0}01 - TextInput{0}02 - NumberInput{0}03 - ConfirmInput{0}04 - ChoiceInput{0}05 - AttachmentInput{0}06 - DateTimeInput{0}07 - OAuthInput{0}", Environment.NewLine)) - .Send("01") - .AssertReply("Hello, I'm Zoidberg. What is your name? (This can't be interrupted)") - .Send("02") - .AssertReply("Hello 02, nice to talk to you!") - .Send("02") - .AssertReply("What is your age?") - .StartTestAsync(); - } - - [TestMethod] - public async Task Inputs_02NumberInput() - { - await BuildTestFlow() - .SendConversationUpdate() - .AssertReply(String.Format("Welcome to Input Sample Bot.{0}I can show you examples on how to use actions, You can enter number 01-07{0}01 - TextInput{0}02 - NumberInput{0}03 - ConfirmInput{0}04 - ChoiceInput{0}05 - AttachmentInput{0}06 - DateTimeInput{0}07 - OAuthInput{0}", Environment.NewLine)) - .Send("02") - .AssertReply("What is your age?") - .Send("18") - .AssertReply("Hello, your age is 18!") - .AssertReply("2 * 2.2 equals?") - .Send("4.4") - .AssertReply("2 * 2.2 equals 4.4, that's right!") - .StartTestAsync(); - } - - [TestMethod] - public async Task Inputs_03ConfirmInput() - { - await BuildTestFlow() - .SendConversationUpdate() - .AssertReply(String.Format("Welcome to Input Sample Bot.{0}I can show you examples on how to use actions, You can enter number 01-07{0}01 - TextInput{0}02 - NumberInput{0}03 - ConfirmInput{0}04 - ChoiceInput{0}05 - AttachmentInput{0}06 - DateTimeInput{0}07 - OAuthInput{0}", Environment.NewLine)) - .Send("03") - .AssertReply("yes or no (1) Yes or (2) No") - .Send("asdasd") - .AssertReply("I need a yes or no. (1) Yes or (2) No") - .Send("yes") - .AssertReply("confirmation: True") - .StartTestAsync(); - } - - [TestMethod] - public async Task Inputs_04ChoiceInput() - { - await BuildTestFlow() - .SendConversationUpdate() - .AssertReply(String.Format("Welcome to Input Sample Bot.{0}I can show you examples on how to use actions, You can enter number 01-07{0}01 - TextInput{0}02 - NumberInput{0}03 - ConfirmInput{0}04 - ChoiceInput{0}05 - AttachmentInput{0}06 - DateTimeInput{0}07 - OAuthInput{0}", Environment.NewLine)).Send("04") - .AssertReply("Please select a value from below:\n\n 1. Test1\n 2. Test2\n 3. Test3") - .Send("Test1") - .AssertReply("You select: Test1") - .StartTestAsync(); - } - - [TestMethod] - public async Task Inputs_06DateTimeInput() - { - await BuildTestFlow() - .SendConversationUpdate() - .AssertReply(String.Format("Welcome to Input Sample Bot.{0}I can show you examples on how to use actions, You can enter number 01-07{0}01 - TextInput{0}02 - NumberInput{0}03 - ConfirmInput{0}04 - ChoiceInput{0}05 - AttachmentInput{0}06 - DateTimeInput{0}07 - OAuthInput{0}", Environment.NewLine)).Send("06") - .AssertReply("Please enter a date.") - .Send("June 1st") - .AssertReply("You entered: 2019-06-01") - .StartTestAsync(); - } - - private TestFlow BuildTestFlow(bool sendTrace = false) - { - TypeFactory.Configuration = new ConfigurationBuilder().Build(); - var storage = new MemoryStorage(); - var convoState = new ConversationState(storage); - var userState = new UserState(storage); - var adapter = new TestAdapter(TestAdapter.CreateConversation(TestContext.TestName), sendTrace); - adapter - .UseStorage(storage) - .UseState(userState, convoState) - .UseAdaptiveDialogs() - .UseLanguageGeneration(resourceExplorer, "common.lg") - .UseResourceExplorer(resourceExplorer) - .Use(new TranscriptLoggerMiddleware(new FileTranscriptLogger())); - - var resource = resourceExplorer.GetResource("Main.dialog"); - var dialog = DeclarativeTypeLoader.Load(resource, resourceExplorer, DebugSupport.SourceMap); - DialogManager dm = new DialogManager(dialog); - - return new TestFlow(adapter, async (turnContext, cancellationToken) => - { - if (dialog is AdaptiveDialog planningDialog) - { - await dm.OnTurnAsync(turnContext, cancellationToken).ConfigureAwait(false); - } - }); - } - } -} diff --git a/BotProject/CSharp/Tests/MessageTests.cs b/BotProject/CSharp/Tests/MessageTests.cs deleted file mode 100644 index c077c6a3b7..0000000000 --- a/BotProject/CSharp/Tests/MessageTests.cs +++ /dev/null @@ -1,101 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using Microsoft.Bot.Builder; -using Microsoft.Bot.Builder.Adapters; -using Microsoft.Bot.Builder.Dialogs; -using Microsoft.Bot.Builder.Dialogs.Adaptive; -using Microsoft.Bot.Builder.Dialogs.Debugging; -using Microsoft.Bot.Builder.Dialogs.Declarative; -using Microsoft.Bot.Builder.Dialogs.Declarative.Resources; -using Microsoft.Bot.Builder.Dialogs.Declarative.Types; -using Microsoft.Bot.Builder.LanguageGeneration; -using Microsoft.Bot.Builder.ComposerBot.Json; -using Microsoft.Bot.Schema; -using Microsoft.Extensions.Configuration; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using System; -using System.Collections.Generic; -using System.IO; -using System.Threading.Tasks; - -namespace Tests -{ - [TestClass] - public class MessageTests - { - private static string getOsPath(string path) => Path.Combine(path.TrimEnd('\\').Split('\\')); - - private static readonly string samplesDirectory = getOsPath(@"..\..\..\..\..\..\Composer\packages\server\assets\projects"); - - private static ResourceExplorer resourceExplorer = new ResourceExplorer(); - - - [ClassInitialize] - public static void ClassInitialize(TestContext context) - { - TypeFactory.Configuration = new ConfigurationBuilder().AddInMemoryCollection().Build(); - string path = Path.GetFullPath(Path.Combine(Environment.CurrentDirectory, samplesDirectory, "RespondingWithTextSample")); - resourceExplorer.AddFolder(path); - } - - [ClassCleanup] - public static void ClassCleanup() - { - resourceExplorer.Dispose(); - } - - public TestContext TestContext { get; set; } - - [TestMethod] - public async Task MessageTest() - { - await BuildTestFlow() - .SendConversationUpdate() - .AssertReply("What type of message would you like to send?\n\n 1. Simple Text\n 2. Text With Memory\n 3. LGWithParam\n 4. LGComposition\n 5. Structured LG\n 6. MultiLineText\n 7. IfElseCondition\n 8. SwitchCondition") - .Send("1") - .AssertReplyOneOf(new string[] { "Hi, this is simple text", "Hey, this is simple text", "Hello, this is simple text" }) - .AssertReply("What type of message would you like to send?\n\n 1. Simple Text\n 2. Text With Memory\n 3. LGWithParam\n 4. LGComposition\n 5. Structured LG\n 6. MultiLineText\n 7. IfElseCondition\n 8. SwitchCondition") - .Send("2") - .AssertReply("This is a text saved in memory.") - .AssertReply("What type of message would you like to send?\n\n 1. Simple Text\n 2. Text With Memory\n 3. LGWithParam\n 4. LGComposition\n 5. Structured LG\n 6. MultiLineText\n 7. IfElseCondition\n 8. SwitchCondition") - .Send("3") - .AssertReply("Hello, I'm Zoidberg. What is your name?") - .Send("luhan") - .AssertReply("Hello luhan, nice to talk to you!") - .AssertReply("What type of message would you like to send?\n\n 1. Simple Text\n 2. Text With Memory\n 3. LGWithParam\n 4. LGComposition\n 5. Structured LG\n 6. MultiLineText\n 7. IfElseCondition\n 8. SwitchCondition") - .Send("4") - .AssertReply("luhan nice to talk to you!") - .AssertReply("What type of message would you like to send?\n\n 1. Simple Text\n 2. Text With Memory\n 3. LGWithParam\n 4. LGComposition\n 5. Structured LG\n 6. MultiLineText\n 7. IfElseCondition\n 8. SwitchCondition") - .StartTestAsync(); - } - - private TestFlow BuildTestFlow(bool sendTrace = false) - { - TypeFactory.Configuration = new ConfigurationBuilder().Build(); - var storage = new MemoryStorage(); - var convoState = new ConversationState(storage); - var userState = new UserState(storage); - var adapter = new TestAdapter(TestAdapter.CreateConversation(TestContext.TestName), sendTrace); - adapter - .UseStorage(storage) - .UseState(userState, convoState) - .UseAdaptiveDialogs() - .UseLanguageGeneration(resourceExplorer, "common.lg") - .UseResourceExplorer(resourceExplorer) - .Use(new TranscriptLoggerMiddleware(new FileTranscriptLogger())); - - var resource = resourceExplorer.GetResource("Main.dialog"); - var dialog = DeclarativeTypeLoader.Load(resource, resourceExplorer, DebugSupport.SourceMap); - DialogManager dm = new DialogManager(dialog); - - return new TestFlow(adapter, async (turnContext, cancellationToken) => - { - if (dialog is AdaptiveDialog planningDialog) - { - await dm.OnTurnAsync(turnContext, cancellationToken).ConfigureAwait(false); - } - }); - } - } -} diff --git a/BotProject/CSharp/Tests/Tests.csproj b/BotProject/CSharp/Tests/Tests.csproj deleted file mode 100644 index 4ddbb74f07..0000000000 --- a/BotProject/CSharp/Tests/Tests.csproj +++ /dev/null @@ -1,27 +0,0 @@ - - - - netcoreapp2.1 - - false - - - - ..\BotProject.ruleset - - - - ..\BotProject.ruleset - - - - - - - - - - - - - diff --git a/BotProject/CSharp/Tests/ToDoBotTests.cs b/BotProject/CSharp/Tests/ToDoBotTests.cs deleted file mode 100644 index b2f4fc5ac9..0000000000 --- a/BotProject/CSharp/Tests/ToDoBotTests.cs +++ /dev/null @@ -1,101 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using Microsoft.Bot.Builder; -using Microsoft.Bot.Builder.Adapters; -using Microsoft.Bot.Builder.Dialogs; -using Microsoft.Bot.Builder.Dialogs.Adaptive; -using Microsoft.Bot.Builder.Dialogs.Debugging; -using Microsoft.Bot.Builder.Dialogs.Declarative; -using Microsoft.Bot.Builder.Dialogs.Declarative.Resources; -using Microsoft.Bot.Builder.Dialogs.Declarative.Types; -using Microsoft.Bot.Builder.LanguageGeneration; -using Microsoft.Bot.Builder.ComposerBot.Json; -using Microsoft.Bot.Schema; -using Microsoft.Extensions.Configuration; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using System; -using System.Collections.Generic; -using System.IO; -using System.Threading.Tasks; - -namespace Tests -{ - [TestClass] - public class ToDoBotTests - { - private static string getOsPath(string path) => Path.Combine(path.TrimEnd('\\').Split('\\')); - - private static readonly string samplesDirectory = getOsPath(@"..\..\..\..\..\..\Composer\packages\server\assets\projects"); - - private static ResourceExplorer resourceExplorer = new ResourceExplorer(); - - - [ClassInitialize] - public static void ClassInitialize(TestContext context) - { - TypeFactory.Configuration = new ConfigurationBuilder().AddInMemoryCollection().Build(); - string path = Path.GetFullPath(Path.Combine(Environment.CurrentDirectory, samplesDirectory, "TodoSample")); - resourceExplorer.AddFolder(path); - } - - [ClassCleanup] - public static void ClassCleanup() - { - resourceExplorer.Dispose(); - } - - public TestContext TestContext { get; set; } - - [TestMethod] - public async Task ToDoBotTest() - { - await BuildTestFlow() - .SendConversationUpdate() - .AssertReply("Hi! I'm a ToDo bot. Say \"add a todo named first\" to get started.") - .Send("add a todo named first") - .AssertReply("Successfully added a todo named first") - .Send("add a todo named second") - .AssertReply("Successfully added a todo named second") - .Send("add a todo") - .AssertReply("OK, please enter the title of your todo.") - .Send("third") - .AssertReply("Successfully added a todo named third") - .Send("show todos") - .AssertReply(String.Format("Your most recent 3 tasks are{0}* first\n* second\n* third", Environment.NewLine)) - .Send("delete todo named second") - .AssertReply("Successfully removed a todo named second") - .Send("show todos") - .AssertReply(String.Format("Your most recent 2 tasks are{0}* first\n* third", Environment.NewLine)) - .StartTestAsync(); - } - - private TestFlow BuildTestFlow(bool sendTrace = false) - { - TypeFactory.Configuration = new ConfigurationBuilder().Build(); - var storage = new MemoryStorage(); - var convoState = new ConversationState(storage); - var userState = new UserState(storage); - var adapter = new TestAdapter(TestAdapter.CreateConversation(TestContext.TestName), sendTrace); - adapter - .UseStorage(storage) - .UseState(userState, convoState) - .UseAdaptiveDialogs() - .UseLanguageGeneration(resourceExplorer, "common.lg") - .UseResourceExplorer(resourceExplorer) - .Use(new TranscriptLoggerMiddleware(new FileTranscriptLogger())); - - var resource = resourceExplorer.GetResource("Main.dialog"); - var dialog = DeclarativeTypeLoader.Load(resource, resourceExplorer, DebugSupport.SourceMap); - DialogManager dm = new DialogManager(dialog); - - return new TestFlow(adapter, async (turnContext, cancellationToken) => - { - if (dialog is AdaptiveDialog planningDialog) - { - await dm.OnTurnAsync(turnContext, cancellationToken).ConfigureAwait(false); - } - }); - } - } -} diff --git a/BotProject/CSharp/appsettings.Development.json b/BotProject/CSharp/appsettings.Development.json deleted file mode 100644 index e203e9407e..0000000000 --- a/BotProject/CSharp/appsettings.Development.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Debug", - "System": "Information", - "Microsoft": "Information" - } - } -} diff --git a/BotProject/CSharp/appsettings.json b/BotProject/CSharp/appsettings.json deleted file mode 100644 index e3a8437897..0000000000 --- a/BotProject/CSharp/appsettings.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "bot": "ComposerDialogs", - "MicrosoftAppId": "", - "MicrosoftAppPassword": "" -} \ No newline at end of file diff --git a/BotProject/Templates/CSharp/BotProject.csproj b/BotProject/Templates/CSharp/BotProject.csproj index 3b888c5943..3cc5c22939 100644 --- a/BotProject/Templates/CSharp/BotProject.csproj +++ b/BotProject/Templates/CSharp/BotProject.csproj @@ -1,4 +1,5 @@ - + + netcoreapp3.1 @@ -13,7 +14,6 @@ Always - BotProject.ruleset @@ -21,16 +21,16 @@ BotProject.ruleset - - - - - - - - - - + + + + + + + + + + all @@ -40,4 +40,4 @@ - + \ No newline at end of file diff --git a/BotProject/Templates/CSharp/ComposerBot.cs b/BotProject/Templates/CSharp/ComposerBot.cs index 45cb85659a..0589c862fc 100644 --- a/BotProject/Templates/CSharp/ComposerBot.cs +++ b/BotProject/Templates/CSharp/ComposerBot.cs @@ -14,7 +14,6 @@ namespace Microsoft.Bot.Builder.ComposerBot.Json { public class ComposerBot : ActivityHandler { - private AdaptiveDialog rootDialog; private readonly ResourceExplorer resourceExplorer; private readonly UserState userState; private DialogManager dialogManager; @@ -23,9 +22,7 @@ public class ComposerBot : ActivityHandler private readonly ISourceMap sourceMap; private readonly string rootDialogFile; - private readonly IBotTelemetryClient telemetryClient; - - public ComposerBot(string rootDialogFile, ConversationState conversationState, UserState userState, ResourceExplorer resourceExplorer, ISourceMap sourceMap, IBotTelemetryClient telemetryClient) + public ComposerBot(string rootDialogFile, ConversationState conversationState, UserState userState, ResourceExplorer resourceExplorer, ISourceMap sourceMap) { this.conversationState = conversationState; this.userState = userState; @@ -33,15 +30,11 @@ public ComposerBot(string rootDialogFile, ConversationState conversationState, U this.sourceMap = sourceMap; this.resourceExplorer = resourceExplorer; this.rootDialogFile = rootDialogFile; - this.telemetryClient = telemetryClient; - DeclarativeTypeLoader.AddComponent(new QnAMakerComponentRegistration()); - LoadRootDialogAsync(); } - + public override async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default(CancellationToken)) { - this.telemetryClient.TrackTrace("Activity:" + turnContext.Activity.Text, Severity.Information, null); await this.dialogManager.OnTurnAsync(turnContext, cancellationToken: cancellationToken); await this.conversationState.SaveChangesAsync(turnContext, false, cancellationToken); await this.userState.SaveChangesAsync(turnContext, false, cancellationToken); @@ -50,8 +43,10 @@ public ComposerBot(string rootDialogFile, ConversationState conversationState, U private void LoadRootDialogAsync() { var rootFile = resourceExplorer.GetResource(rootDialogFile); - rootDialog = DeclarativeTypeLoader.Load(rootFile, resourceExplorer, sourceMap); - this.dialogManager = new DialogManager(rootDialog); + var rootDialog = resourceExplorer.LoadType(rootFile); + this.dialogManager = new DialogManager(rootDialog) + .UseResourceExplorer(resourceExplorer) + .UseLanguageGeneration(); } } } diff --git a/BotProject/Templates/CSharp/LuisConfigAdaptor.cs b/BotProject/Templates/CSharp/LuisConfigAdaptor.cs new file mode 100644 index 0000000000..6f23086f3b --- /dev/null +++ b/BotProject/Templates/CSharp/LuisConfigAdaptor.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; + +namespace Microsoft.Bot.Builder.ComposerBot.Json +{ + public static class LuisConfigAdaptor + { + public static IConfigurationBuilder UseLuisConfigAdaptor(this IConfigurationBuilder builder) + { + var configuration = builder.Build(); + var settings = new Dictionary(); + settings["environment"] = configuration.GetValue("luis:environment"); + settings["region"] = configuration.GetValue("luis:authoringRegion"); + builder.AddInMemoryCollection(settings); + return builder; + } + } +} diff --git a/BotProject/Templates/CSharp/Program.cs b/BotProject/Templates/CSharp/Program.cs index 01920c40ba..c5869104f3 100644 --- a/BotProject/Templates/CSharp/Program.cs +++ b/BotProject/Templates/CSharp/Program.cs @@ -1,11 +1,13 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. using System; +using System.Collections.Generic; using System.Diagnostics; using System.IO; using Microsoft.AspNetCore; using Microsoft.AspNetCore.Hosting; +using Microsoft.Bot.Builder.AI.Luis; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; @@ -15,42 +17,29 @@ public class Program { public static void Main(string[] args) { - BuildWebHost(args).Run(); + CreateHostBuilder(args).Build().Run(); } - public static IWebHost BuildWebHost(string[] args) => - WebHost.CreateDefaultBuilder(args) - .ConfigureAppConfiguration((hostingContext, config) => + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureAppConfiguration((hostingContext, builder) => { + builder.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) + .AddJsonFile($"ComposerDialogs/settings/appsettings.json", optional: true, reloadOnChange: true) + .UseLuisConfigAdaptor() + .UseLuisSettings(); var env = hostingContext.HostingEnvironment; - var luisAuthoringRegion = Environment.GetEnvironmentVariable("LUIS_AUTHORING_REGION") ?? "westus"; - config - .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true) - .AddJsonFile($"ComposerDialogs/settings/appsettings.json", optional: true, reloadOnChange: true) - .AddJsonFile($"luis.settings.{env.EnvironmentName}.{luisAuthoringRegion}.json", optional: true, reloadOnChange: true) - .AddJsonFile($"luis.settings.{Environment.UserName}.{luisAuthoringRegion}.json", optional: true, reloadOnChange: true); - try - { - foreach (string filePath in Directory.GetFiles($"ComposerDialogs", "generated/luis.settings.*.json")) - { - config.AddJsonFile(filePath, optional: true, reloadOnChange: true); - } - } - catch (Exception ex) - { - Trace.WriteLine(ex.Message); - } - if (env.IsDevelopment()) { - config.AddUserSecrets(); + builder.AddUserSecrets(); } - config - .AddEnvironmentVariables() - .AddCommandLine(args); - }).UseStartup() - .Build(); + builder.AddEnvironmentVariables() + .AddCommandLine(args); + }) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); } } diff --git a/BotProject/Templates/CSharp/README.md b/BotProject/Templates/CSharp/README.md index 7f9cbbefb2..b096ecf323 100644 --- a/BotProject/Templates/CSharp/README.md +++ b/BotProject/Templates/CSharp/README.md @@ -5,14 +5,14 @@ Bot project is the launcher project for the bots written in declarative form (JS The Bot Project is a regular Bot Framework SDK V4 project. Before you can launch it from the emulator, you need to make sure you can run the bot. ### Prerequisite: -* Install .Netcore 2 +* Install .Netcore 3.1 ### Commands: * from root folder * cd BotProject -* cd CSharp -* dotnet restore // for the package updates +* cd Templates/CSharp +* dotnet user-secrets init // init the user secret id * dotnet build // build * dotnet run // start the bot * It will start a web server and listening at http://localhost:3979. @@ -20,36 +20,3 @@ The Bot Project is a regular Bot Framework SDK V4 project. Before you can launch ### Test bot * You can set you emulator to connect to http://localhost:3979/api/messages. -### config your bot -This setup is required for local testing of your Bot Runtime. -* The only thing you need to config is appsetting.json, which has a bot setting to launch the bot: - -``` -appsettings.json: -"bot": { - "provider": "localDisk", - "path": "../../Bots/SampleBot3/bot3.botproj" -} -``` - -## .botproj folder structure -``` -bot.botproj, bot project got the rootDialog from "entry" -{ - "services": [{ - "type": "luis", - "id": "1", - "name": "TodoBotLuis", - "lufile": "todo.lu", - "applicationId": "TodoBotLuis.applicationId", - "endpointKey": "TodoBotLuis.endpointKey", - "endpoint": "TodoBotLuis.endpoint" - }], - "files": [ - "*.dialog", - "*.lg" - ], - "entry": "main.dialog" -} -``` -* Please refer to [Samples](https://github.com/Microsoft/BotFramework-Composer/tree/master/SampleBots) for more samples. diff --git a/BotProject/Templates/CSharp/Schemas/sdk.schema b/BotProject/Templates/CSharp/Schemas/sdk.schema index 1fafc1696f..a1c8661f27 100644 --- a/BotProject/Templates/CSharp/Schemas/sdk.schema +++ b/BotProject/Templates/CSharp/Schemas/sdk.schema @@ -999,7 +999,7 @@ "unrecognizedPrompt": { "$kind": "Microsoft.IActivityTemplate", "title": "Unrecognized prompt", - "description": "Message to send if user response is not recognized.", + "description": "Message to send when the recognizer does not understand the user input.", "examples": [ "Sorry, I do not understand '{turn.activity.text'}. Let's try again. What is your birth date?" ], @@ -1008,7 +1008,7 @@ "invalidPrompt": { "$kind": "Microsoft.IActivityTemplate", "title": "Invalid prompt", - "description": "Message to send if user response is invalid. Relies on specified validation expressions.", + "description": "Message to send when the user input does not meet any validation expression.", "examples": [ "Sorry, '{this.value}' does not work. I need a number between 1-150. What is your age?" ], @@ -1072,7 +1072,7 @@ "string" ], "title": "Default value", - "description": "Expression to examine on each turn of the conversation as possible value to the property.", + "description": "'Property' will be set to the value of this expression when max turn count is exceeded.", "examples": [ "@userName", "coalesce(@number, @partySize)" @@ -1089,7 +1089,7 @@ "string" ], "title": "Value", - "description": "Gets or sets a value expression which can be used to intialize the input prompt.", + "description": "'Property' will be set to the value of this expression unless it evaluates to null.", "examples": [ "@userName" ] @@ -1527,7 +1527,7 @@ "unrecognizedPrompt": { "$kind": "Microsoft.IActivityTemplate", "title": "Unrecognized prompt", - "description": "Message to send if user response is not recognized.", + "description": "Message to send when the recognizer does not understand the user input.", "examples": [ "Sorry, I do not understand '{turn.activity.text'}. Let's try again. What is your birth date?" ], @@ -1536,7 +1536,7 @@ "invalidPrompt": { "$kind": "Microsoft.IActivityTemplate", "title": "Invalid prompt", - "description": "Message to send if user response is invalid. Relies on specified validation expressions.", + "description": "Message to send when the user input does not meet any validation expression.", "examples": [ "Sorry, '{this.value}' does not work. I need a number between 1-150. What is your age?" ], @@ -1600,7 +1600,7 @@ "string" ], "title": "Default value", - "description": "Expression to examine on each turn of the conversation as possible value to the property.", + "description": "'Property' will be set to the value of this expression when max turn count is exceeded.", "examples": [ "@userName", "coalesce(@number, @partySize)" @@ -1617,7 +1617,7 @@ "string" ], "title": "Value", - "description": "Gets or sets a value expression which can be used to intialize the input prompt.", + "description": "'Property' will be set to the value of this expression unless it evaluates to null.", "examples": [ "@userName" ] @@ -1939,7 +1939,7 @@ "unrecognizedPrompt": { "$kind": "Microsoft.IActivityTemplate", "title": "Unrecognized prompt", - "description": "Message to send if user response is not recognized.", + "description": "Message to send when the recognizer does not understand the user input.", "examples": [ "Sorry, I do not understand '{turn.activity.text'}. Let's try again. What is your birth date?" ], @@ -1948,7 +1948,7 @@ "invalidPrompt": { "$kind": "Microsoft.IActivityTemplate", "title": "Invalid prompt", - "description": "Message to send if user response is invalid. Relies on specified validation expressions.", + "description": "Message to send when the user input does not meet any validation expression.", "examples": [ "Sorry, '{this.value}' does not work. I need a number between 1-150. What is your age?" ], @@ -2012,7 +2012,7 @@ "string" ], "title": "Default value", - "description": "Expression to examine on each turn of the conversation as possible value to the property.", + "description": "'Property' will be set to the value of this expression when max turn count is exceeded.", "examples": [ "@userName", "coalesce(@number, @partySize)" @@ -2029,7 +2029,7 @@ "string" ], "title": "Value", - "description": "Gets or sets a value expression which can be used to intialize the input prompt.", + "description": "'Property' will be set to the value of this expression unless it evaluates to null.", "examples": [ "@userName" ] @@ -2552,7 +2552,7 @@ "unrecognizedPrompt": { "$kind": "Microsoft.IActivityTemplate", "title": "Unrecognized prompt", - "description": "Message to send if user response is not recognized.", + "description": "Message to send when the recognizer does not understand the user input.", "examples": [ "Sorry, I do not understand '{turn.activity.text'}. Let's try again. What is your birth date?" ], @@ -2561,7 +2561,7 @@ "invalidPrompt": { "$kind": "Microsoft.IActivityTemplate", "title": "Invalid prompt", - "description": "Message to send if user response is invalid. Relies on specified validation expressions.", + "description": "Message to send when the user input does not meet any validation expression.", "examples": [ "Sorry, '{this.value}' does not work. I need a number between 1-150. What is your age?" ], @@ -2625,7 +2625,7 @@ "string" ], "title": "Default value", - "description": "Expression to examine on each turn of the conversation as possible value to the property.", + "description": "'Property' will be set to the value of this expression when max turn count is exceeded.", "examples": [ "@userName", "coalesce(@number, @partySize)" @@ -2642,7 +2642,7 @@ "string" ], "title": "Value", - "description": "Gets or sets a value expression which can be used to intialize the input prompt.", + "description": "'Property' will be set to the value of this expression unless it evaluates to null.", "examples": [ "@userName" ] @@ -5763,7 +5763,7 @@ "unrecognizedPrompt": { "$kind": "Microsoft.IActivityTemplate", "title": "Unrecognized prompt", - "description": "Message to send if user response is not recognized.", + "description": "Message to send when the recognizer does not understand the user input.", "examples": [ "Sorry, I do not understand '{turn.activity.text'}. Let's try again. What is your birth date?" ], @@ -5772,7 +5772,7 @@ "invalidPrompt": { "$kind": "Microsoft.IActivityTemplate", "title": "Invalid prompt", - "description": "Message to send if user response is invalid. Relies on specified validation expressions.", + "description": "Message to send when the user input does not meet any validation expression.", "examples": [ "Sorry, '{this.value}' does not work. I need a number between 1-150. What is your age?" ], @@ -5836,7 +5836,7 @@ "string" ], "title": "Default value", - "description": "Expression to examine on each turn of the conversation as possible value to the property.", + "description": "'Property' will be set to the value of this expression when max turn count is exceeded.", "examples": [ "@userName", "coalesce(@number, @partySize)" @@ -5853,7 +5853,7 @@ "string" ], "title": "Value", - "description": "Gets or sets a value expression which can be used to intialize the input prompt.", + "description": "'Property' will be set to the value of this expression unless it evaluates to null.", "examples": [ "@userName" ] @@ -11520,7 +11520,7 @@ "unrecognizedPrompt": { "$kind": "Microsoft.IActivityTemplate", "title": "Unrecognized prompt", - "description": "Message to send if user response is not recognized.", + "description": "Message to send when the recognizer does not understand the user input.", "examples": [ "Sorry, I do not understand '{turn.activity.text'}. Let's try again. What is your birth date?" ], @@ -11529,7 +11529,7 @@ "invalidPrompt": { "$kind": "Microsoft.IActivityTemplate", "title": "Invalid prompt", - "description": "Message to send if user response is invalid. Relies on specified validation expressions.", + "description": "Message to send when the user input does not meet any validation expression.", "examples": [ "Sorry, '{this.value}' does not work. I need a number between 1-150. What is your age?" ], @@ -11593,7 +11593,7 @@ "string" ], "title": "Default value", - "description": "Expression to examine on each turn of the conversation as possible value to the property.", + "description": "'Property' will be set to the value of this expression when max turn count is exceeded.", "examples": [ "@userName", "coalesce(@number, @partySize)" @@ -11610,7 +11610,7 @@ "string" ], "title": "Value", - "description": "Gets or sets a value expression which can be used to intialize the input prompt.", + "description": "'Property' will be set to the value of this expression unless it evaluates to null.", "examples": [ "@userName" ] @@ -11648,7 +11648,7 @@ "description": "Expression to format the output.", "examples": [ "=toUpper(this.value)", - "@{toUpper(this.value)}" + "${toUpper(this.value)}" ] } }, @@ -12034,4 +12034,4 @@ ] } } -} \ No newline at end of file +} diff --git a/BotProject/Templates/CSharp/Startup.cs b/BotProject/Templates/CSharp/Startup.cs index f6b769dd96..f4f532942d 100644 --- a/BotProject/Templates/CSharp/Startup.cs +++ b/BotProject/Templates/CSharp/Startup.cs @@ -44,8 +44,6 @@ public void ConfigureServices(IServiceCollection services) // Create the credential provider to be used with the Bot Framework Adapter. services.AddSingleton(); - services.AddSingleton(); - // Load settings var settings = new BotSettings(); Configuration.Bind(settings); @@ -66,18 +64,9 @@ public void ConfigureServices(IServiceCollection services) services.AddSingleton(storage); var userState = new UserState(storage); var conversationState = new ConversationState(storage); - var inspectionState = new InspectionState(storage); - - // Configure telemetry - services.AddApplicationInsightsTelemetry(); - var telemetryClient = new BotTelemetryClient(new TelemetryClient()); - services.AddSingleton(telemetryClient); - services.AddBotApplicationInsights(telemetryClient); var botFile = Configuration.GetSection("bot").Get(); - TypeFactory.Configuration = this.Configuration; - // manage all bot resources var resourceExplorer = new ResourceExplorer().AddFolder(botFile); @@ -85,15 +74,12 @@ public void ConfigureServices(IServiceCollection services) services.AddSingleton((s) => { + HostContext.Current.Set(Configuration); + var adapter = new BotFrameworkHttpAdapter(new ConfigurationCredentialProvider(this.Configuration)); adapter .UseStorage(storage) - .UseState(userState, conversationState) - .UseAdaptiveDialogs() - .UseResourceExplorer(resourceExplorer) - .UseLanguageGeneration(resourceExplorer, "common.lg") - .Use(new RegisterClassMiddleware(Configuration)) - .Use(new InspectionMiddleware(inspectionState, userState, conversationState, credentials)); + .UseState(userState, conversationState); if (!string.IsNullOrEmpty(settings.BlobStorage.ConnectionString) && !string.IsNullOrEmpty(settings.BlobStorage.Container)) { @@ -107,28 +93,18 @@ public void ConfigureServices(IServiceCollection services) adapter.OnTurnError = async (turnContext, exception) => { await turnContext.SendActivityAsync(exception.Message).ConfigureAwait(false); - telemetryClient.TrackException(new Exception("Exceptions: " + exception.Message)); await conversationState.ClearStateAsync(turnContext).ConfigureAwait(false); await conversationState.SaveChangesAsync(turnContext).ConfigureAwait(false); }; return adapter; }); - services.AddSingleton((sp) => new ComposerBot("Main.dialog", conversationState, userState, resourceExplorer, DebugSupport.SourceMap, telemetryClient)); + services.AddSingleton((sp) => new ComposerBot("Main.dialog", conversationState, userState, resourceExplorer, DebugSupport.SourceMap)); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IHostingEnvironment env) + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - else - { - app.UseHsts(); - } - app.UseDefaultFiles(); app.UseStaticFiles(); app.UseWebSockets(); diff --git a/BotProject/Templates/CSharp/Tests/ActionsTests.cs b/BotProject/Templates/CSharp/Tests/ActionsTests.cs index fbfedc8690..920cd9a00f 100644 --- a/BotProject/Templates/CSharp/Tests/ActionsTests.cs +++ b/BotProject/Templates/CSharp/Tests/ActionsTests.cs @@ -34,7 +34,6 @@ private static string getFolderPath(string path) [ClassInitialize] public static void ClassInitialize(TestContext context) { - TypeFactory.Configuration = new ConfigurationBuilder().AddInMemoryCollection().Build(); string path = Path.GetFullPath(Path.Combine(Environment.CurrentDirectory, samplesDirectory)); } @@ -156,6 +155,7 @@ await BuildTestFlow(getFolderPath("ActionsSample"), sendTrace: true) } [TestMethod] + [Ignore] public async Task Actions_09EditActions() { await BuildTestFlow(getFolderPath("ActionsSample")) @@ -204,7 +204,6 @@ await BuildTestFlow(getFolderPath("ActionsSample")) private TestFlow BuildTestFlow(string folderPath, bool sendTrace = false) { - TypeFactory.Configuration = new ConfigurationBuilder().Build(); var storage = new MemoryStorage(); var convoState = new ConversationState(storage); var userState = new UserState(storage); @@ -213,15 +212,14 @@ private TestFlow BuildTestFlow(string folderPath, bool sendTrace = false) resourceExplorer.AddFolder(folderPath); adapter .UseStorage(storage) - .UseState(userState, convoState) - .UseAdaptiveDialogs() - .UseLanguageGeneration(resourceExplorer, "common.lg") - .UseResourceExplorer(resourceExplorer) + .UseState(userState, convoState) .Use(new TranscriptLoggerMiddleware(new FileTranscriptLogger())); var resource = resourceExplorer.GetResource("Main.dialog"); - var dialog = DeclarativeTypeLoader.Load(resource, resourceExplorer, DebugSupport.SourceMap); - DialogManager dm = new DialogManager(dialog); + var dialog = resourceExplorer.LoadType(resource); + DialogManager dm = new DialogManager(dialog) + .UseResourceExplorer(resourceExplorer) + .UseLanguageGeneration(); ; return new TestFlow(adapter, async (turnContext, cancellationToken) => { diff --git a/BotProject/Templates/CSharp/Tests/ControllingConversationTests.cs b/BotProject/Templates/CSharp/Tests/ControllingConversationTests.cs index 593d7666f8..a100e4565e 100644 --- a/BotProject/Templates/CSharp/Tests/ControllingConversationTests.cs +++ b/BotProject/Templates/CSharp/Tests/ControllingConversationTests.cs @@ -30,7 +30,6 @@ public class ControllingConversationTests [ClassInitialize] public static void ClassInitialize(TestContext context) { - TypeFactory.Configuration = new ConfigurationBuilder().AddInMemoryCollection().Build(); string path = Path.GetFullPath(Path.Combine(Environment.CurrentDirectory, samplesDirectory, "ControllingConversationFlowSample")); resourceExplorer.AddFolder(path); } @@ -75,15 +74,12 @@ await BuildTestFlow() .Send("Yes") .AssertReply("Do you want to repeat this dialog, yes to repeat, no to end this dialog (1) Yes or (2) No") .Send("No") - .Send("05") - .AssertReply("Canceled.") .StartTestAsync(); } private TestFlow BuildTestFlow(bool sendTrace = false) { - TypeFactory.Configuration = new ConfigurationBuilder().Build(); var storage = new MemoryStorage(); var convoState = new ConversationState(storage); var userState = new UserState(storage); @@ -91,14 +87,13 @@ private TestFlow BuildTestFlow(bool sendTrace = false) adapter .UseStorage(storage) .UseState(userState, convoState) - .UseAdaptiveDialogs() - .UseLanguageGeneration(resourceExplorer, "common.lg") - .UseResourceExplorer(resourceExplorer) .Use(new TranscriptLoggerMiddleware(new FileTranscriptLogger())); var resource = resourceExplorer.GetResource("Main.dialog"); - var dialog = DeclarativeTypeLoader.Load(resource, resourceExplorer, DebugSupport.SourceMap); - DialogManager dm = new DialogManager(dialog); + var dialog = resourceExplorer.LoadType(resource); + DialogManager dm = new DialogManager(dialog) + .UseResourceExplorer(resourceExplorer) + .UseLanguageGeneration(); return new TestFlow(adapter, async (turnContext, cancellationToken) => { diff --git a/BotProject/Templates/CSharp/Tests/InputsTests.cs b/BotProject/Templates/CSharp/Tests/InputsTests.cs index 0bf735d2fe..fe1ce5e0e0 100644 --- a/BotProject/Templates/CSharp/Tests/InputsTests.cs +++ b/BotProject/Templates/CSharp/Tests/InputsTests.cs @@ -31,7 +31,6 @@ public class InputsTests [ClassInitialize] public static void ClassInitialize(TestContext context) { - TypeFactory.Configuration = new ConfigurationBuilder().AddInMemoryCollection().Build(); string path = Path.GetFullPath(Path.Combine(Environment.CurrentDirectory, samplesDirectory, "AskingQuestionsSample")); resourceExplorer.AddFolder(path); } @@ -116,7 +115,6 @@ await BuildTestFlow() private TestFlow BuildTestFlow(bool sendTrace = false) { - TypeFactory.Configuration = new ConfigurationBuilder().Build(); var storage = new MemoryStorage(); var convoState = new ConversationState(storage); var userState = new UserState(storage); @@ -124,14 +122,13 @@ private TestFlow BuildTestFlow(bool sendTrace = false) adapter .UseStorage(storage) .UseState(userState, convoState) - .UseAdaptiveDialogs() - .UseLanguageGeneration(resourceExplorer, "common.lg") - .UseResourceExplorer(resourceExplorer) .Use(new TranscriptLoggerMiddleware(new FileTranscriptLogger())); var resource = resourceExplorer.GetResource("Main.dialog"); - var dialog = DeclarativeTypeLoader.Load(resource, resourceExplorer, DebugSupport.SourceMap); - DialogManager dm = new DialogManager(dialog); + var dialog = resourceExplorer.LoadType(resource); + DialogManager dm = new DialogManager(dialog) + .UseResourceExplorer(resourceExplorer) + .UseLanguageGeneration(); return new TestFlow(adapter, async (turnContext, cancellationToken) => { diff --git a/BotProject/Templates/CSharp/Tests/MessageTests.cs b/BotProject/Templates/CSharp/Tests/MessageTests.cs index d5fb6dc42c..d68c119860 100644 --- a/BotProject/Templates/CSharp/Tests/MessageTests.cs +++ b/BotProject/Templates/CSharp/Tests/MessageTests.cs @@ -34,7 +34,6 @@ public class MessageTests [ClassInitialize] public static void ClassInitialize(TestContext context) { - TypeFactory.Configuration = new ConfigurationBuilder().AddInMemoryCollection().Build(); string path = Path.GetFullPath(Path.Combine(Environment.CurrentDirectory, samplesDirectory, "RespondingWithTextSample")); resourceExplorer.AddFolder(path); } @@ -72,7 +71,6 @@ await BuildTestFlow() private TestFlow BuildTestFlow(bool sendTrace = false) { - TypeFactory.Configuration = new ConfigurationBuilder().Build(); var storage = new MemoryStorage(); var convoState = new ConversationState(storage); var userState = new UserState(storage); @@ -80,14 +78,13 @@ private TestFlow BuildTestFlow(bool sendTrace = false) adapter .UseStorage(storage) .UseState(userState, convoState) - .UseAdaptiveDialogs() - .UseLanguageGeneration(resourceExplorer, "common.lg") - .UseResourceExplorer(resourceExplorer) .Use(new TranscriptLoggerMiddleware(new FileTranscriptLogger())); var resource = resourceExplorer.GetResource("Main.dialog"); - var dialog = DeclarativeTypeLoader.Load(resource, resourceExplorer, DebugSupport.SourceMap); - DialogManager dm = new DialogManager(dialog); + var dialog = resourceExplorer.LoadType(resource); + DialogManager dm = new DialogManager(dialog) + .UseResourceExplorer(resourceExplorer) + .UseLanguageGeneration(); return new TestFlow(adapter, async (turnContext, cancellationToken) => { diff --git a/BotProject/Templates/CSharp/Tests/ToDoBotTests.cs b/BotProject/Templates/CSharp/Tests/ToDoBotTests.cs index b2dca363f0..b60b279307 100644 --- a/BotProject/Templates/CSharp/Tests/ToDoBotTests.cs +++ b/BotProject/Templates/CSharp/Tests/ToDoBotTests.cs @@ -30,7 +30,6 @@ public class ToDoBotTests [ClassInitialize] public static void ClassInitialize(TestContext context) { - TypeFactory.Configuration = new ConfigurationBuilder().AddInMemoryCollection().Build(); string path = Path.GetFullPath(Path.Combine(Environment.CurrentDirectory, samplesDirectory, "TodoSample")); resourceExplorer.AddFolder(path); } @@ -68,7 +67,6 @@ await BuildTestFlow() private TestFlow BuildTestFlow(bool sendTrace = false) { - TypeFactory.Configuration = new ConfigurationBuilder().Build(); var storage = new MemoryStorage(); var convoState = new ConversationState(storage); var userState = new UserState(storage); @@ -76,14 +74,13 @@ private TestFlow BuildTestFlow(bool sendTrace = false) adapter .UseStorage(storage) .UseState(userState, convoState) - .UseAdaptiveDialogs() - .UseLanguageGeneration(resourceExplorer, "common.lg") - .UseResourceExplorer(resourceExplorer) .Use(new TranscriptLoggerMiddleware(new FileTranscriptLogger())); var resource = resourceExplorer.GetResource("Main.dialog"); - var dialog = DeclarativeTypeLoader.Load(resource, resourceExplorer, DebugSupport.SourceMap); - DialogManager dm = new DialogManager(dialog); + var dialog = resourceExplorer.LoadType(resource); + DialogManager dm = new DialogManager(dialog) + .UseResourceExplorer(resourceExplorer) + .UseLanguageGeneration(); return new TestFlow(adapter, async (turnContext, cancellationToken) => { diff --git a/Composer/cypress/integration/NotificationPage.spec.ts b/Composer/cypress/integration/NotificationPage.spec.ts index ba00dc873d..eb7b06dfe8 100644 --- a/Composer/cypress/integration/NotificationPage.spec.ts +++ b/Composer/cypress/integration/NotificationPage.spec.ts @@ -13,7 +13,7 @@ context('Notification Page', () => { cy.get('.toggleEditMode button').as('switchButton'); cy.get('@switchButton').click(); - cy.get('textarea').type('test lg syntax error'); + cy.get('textarea').type('test lg syntax error#'); cy.visitPage('Notifications'); diff --git a/Composer/packages/client/__tests__/store/reducer/reducer.test.js b/Composer/packages/client/__tests__/store/reducer/reducer.test.js index cc0e7521dd..ea8c9368eb 100644 --- a/Composer/packages/client/__tests__/store/reducer/reducer.test.js +++ b/Composer/packages/client/__tests__/store/reducer/reducer.test.js @@ -31,7 +31,7 @@ describe('test all reducer handlers', () => { payload: { id: 'common.lg', content: ` # bfdactivity-003038 - - You said '@{turn.activity.text}'`, + - You said '\${turn.activity.text}'`, }, } ); diff --git a/Composer/packages/client/package.json b/Composer/packages/client/package.json index c0fcfb6d77..5c8da74fd9 100644 --- a/Composer/packages/client/package.json +++ b/Composer/packages/client/package.json @@ -20,12 +20,12 @@ "@bfc/extensions": "*", "@bfc/indexers": "*", "@bfc/shared": "*", - "@bfcomposer/bf-lu": "^1.1.8", + "@bfcomposer/bf-lu": "^1.2.5", "@emotion/core": "^10.0.7", "@reach/router": "^1.2.1", "axios": "^0.18.0", - "botbuilder-lg": "4.7.0-preview.93464", - "botframework-expressions": "4.7.0-preview.93464", + "botbuilder-lg": "^4.8.0-preview.106823", + "botframework-expressions": "^4.8.0-preview.106476", "format-message": "^6.2.3", "immer": "^5.2.0", "jwt-decode": "^2.2.0", diff --git a/Composer/packages/extensions/obiformeditor/demo/src/editorschema.json b/Composer/packages/extensions/obiformeditor/demo/src/editorschema.json index 7875250f47..b41d827cdb 100644 --- a/Composer/packages/extensions/obiformeditor/demo/src/editorschema.json +++ b/Composer/packages/extensions/obiformeditor/demo/src/editorschema.json @@ -63,9 +63,6 @@ "Microsoft.IfCondition": { "title": "Branch: If/Else" }, - "Microsoft.InitProperty": { - "title": "Initialize Property" - }, "Microsoft.OnIntent": { "title": "Intent" }, diff --git a/Composer/packages/extensions/obiformeditor/package.json b/Composer/packages/extensions/obiformeditor/package.json index a74aae2939..b575a1f5a4 100644 --- a/Composer/packages/extensions/obiformeditor/package.json +++ b/Composer/packages/extensions/obiformeditor/package.json @@ -61,7 +61,7 @@ "@types/react": "16.9.0", "@types/react-dom": "16.9.0", "autoprefixer": "^9.5.1", - "botframework-expressions": "4.7.0-preview.93464", + "botframework-expressions": "^4.8.0-preview.106476", "codemirror": "^5.44.0", "copyfiles": "^2.1.0", "css-loader": "^2.1.1", diff --git a/Composer/packages/extensions/visual-designer/demo/src/samples/todo/AddToDo.json b/Composer/packages/extensions/visual-designer/demo/src/samples/todo/AddToDo.json index f6f7be93f3..390fa63365 100644 --- a/Composer/packages/extensions/visual-designer/demo/src/samples/todo/AddToDo.json +++ b/Composer/packages/extensions/visual-designer/demo/src/samples/todo/AddToDo.json @@ -49,7 +49,7 @@ "condition": "user.todos == null", "actions": [ { - "$type": "Microsoft.InitProperty", + "$type": "Microsoft.SetProperty", "$designer": { "createdAt": "2019-07-16T20:01:05.970Z", "updatedAt": "2019-07-16T20:01:13.866Z", diff --git a/Composer/packages/extensions/visual-designer/demo/src/stories/VisualSDKDemo.js b/Composer/packages/extensions/visual-designer/demo/src/stories/VisualSDKDemo.js index c061775c26..70b1ea7f12 100644 --- a/Composer/packages/extensions/visual-designer/demo/src/stories/VisualSDKDemo.js +++ b/Composer/packages/extensions/visual-designer/demo/src/stories/VisualSDKDemo.js @@ -20,7 +20,6 @@ export class VisualSDKDemo extends Component { const initialTypes = [ SDKTypes.SendActivity, SDKTypes.EditArray, - SDKTypes.InitProperty, SDKTypes.SetProperties, SDKTypes.SetProperty, SDKTypes.DeleteProperties, diff --git a/Composer/packages/extensions/visual-designer/src/schema/uischema.tsx b/Composer/packages/extensions/visual-designer/src/schema/uischema.tsx index 07d454c2af..e0ad27eb69 100644 --- a/Composer/packages/extensions/visual-designer/src/schema/uischema.tsx +++ b/Composer/packages/extensions/visual-designer/src/schema/uischema.tsx @@ -116,10 +116,6 @@ export const uiSchema: UISchema = { 'ui:widget': ActionCard, content: data => `${data.changeType} {${data.itemsProperty || '?'}}`, }, - [SDKTypes.InitProperty]: { - 'ui:widget': ActionCard, - content: data => `{${data.property || '?'}} = new ${data.type || '?'}`, - }, [SDKTypes.SetProperty]: { 'ui:widget': ActionCard, content: data => `{${data.property || '?'}} = ${data.value || '?'}`, diff --git a/Composer/packages/lib/code-editor/src/languages/lg.ts b/Composer/packages/lib/code-editor/src/languages/lg.ts index 08ec79c018..126d0e509e 100644 --- a/Composer/packages/lib/code-editor/src/languages/lg.ts +++ b/Composer/packages/lib/code-editor/src/languages/lg.ts @@ -11,34 +11,25 @@ function createKeywordsProposals(range) { { label: 'IF', kind: monaco.languages.CompletionItemKind.Keyword, - insertText: `IF: @{} -- -- ELSEIF: @{} - - -- ELSE: - - `, + insertText: ['IF: ${}', '- ELSEIF: ${}', ' -', '- ELSE:', ' -'].join('\r\n'), range: range, }, { label: 'ELSEIF', kind: monaco.languages.CompletionItemKind.Keyword, - insertText: 'ELSEIF:@{}', + insertText: 'ELSEIF:${}', range: range, }, { label: 'ELSE', kind: monaco.languages.CompletionItemKind.Keyword, - insertText: 'ELSE:\n', + insertText: 'ELSE:\r\n', range: range, }, { label: 'SWITCH', kind: monaco.languages.CompletionItemKind.Keyword, - insertText: `SWITCH: @{} -- CASE: @{} - - -- DEFAULT: - - `, + insertText: ['SWITCH: ${}', '- CASE: ${}', ' -', '- DEFAULT:', ' -'].join('\r\n'), range: range, }, ]; @@ -94,11 +85,11 @@ export function registerLGLanguage(monaco: typeof monacoEditor) { //template_body [/^\s*-/, { token: 'template-body-identifier', next: '@template_body' }], //expression - [/@\{/, { token: 'expression', next: '@expression' }], + [/\$\{/, { token: 'expression', next: '@expression' }], ], fence_block: [ [/`{3}\s*/, { token: 'fence-block', next: '@pop' }], - [/@\{/, { token: 'expression', next: '@expression' }], + [/\$\{/, { token: 'expression', next: '@expression' }], [/./, 'fence-block'], ], expression: [ @@ -109,7 +100,7 @@ export function registerLGLanguage(monaco: typeof monacoEditor) { [/(\s*'[^']*?'\s*)(,|\))/, ['string', 'delimeter']], [/(\s*"[^"]*?"\s*)(,|\))/, ['string', 'delimeter']], [/(\s*[^},'"(]*\s*)(,|\))/, ['other-expression', 'delimeter']], - [/[^@}]*$/, { token: 'expression.content', next: '@pop' }], + [/[^$}]*$/, { token: 'expression.content', next: '@pop' }], ], structure_lg: [ [/^\s*\]\s*$/, 'structure-lg', '@pop'], @@ -117,7 +108,7 @@ export function registerLGLanguage(monaco: typeof monacoEditor) { [/(\s*\[\s*)([a-zA-Z0-9_-]+\s*$)/, ['stucture-lg-identifier', 'structure-name']], [/^\s*>[\s\S]*$/, 'comments'], [/\|/, { token: 'alternative' }], - [/@\{/, { token: 'expression', next: '@expression' }], + [/\$\{/, { token: 'expression', next: '@expression' }], ], }, }); diff --git a/Composer/packages/lib/indexers/package.json b/Composer/packages/lib/indexers/package.json index a8511930bc..461948af75 100644 --- a/Composer/packages/lib/indexers/package.json +++ b/Composer/packages/lib/indexers/package.json @@ -29,9 +29,9 @@ }, "dependencies": { "@bfc/shared": "*", - "@bfcomposer/bf-lu": "^1.1.8", - "botbuilder-lg": "^4.8.0-preview.97252", - "botframework-expressions": "4.7.0-preview.93464", + "@bfcomposer/bf-lu": "^1.2.5", + "botbuilder-lg": "^4.8.0-preview.106823", + "botframework-expressions": "^4.8.0-preview.106476", "lodash": "^4.17.15" } } diff --git a/Composer/packages/lib/shared/__tests__/lgUtils/models/LgTemplateRef.test.ts b/Composer/packages/lib/shared/__tests__/lgUtils/models/LgTemplateRef.test.ts index af6d59ba5d..b708c60893 100644 --- a/Composer/packages/lib/shared/__tests__/lgUtils/models/LgTemplateRef.test.ts +++ b/Composer/packages/lib/shared/__tests__/lgUtils/models/LgTemplateRef.test.ts @@ -20,30 +20,30 @@ describe('LgTemplateRef#', () => { it('can output correct strings', () => { const a = new LgTemplateRef('a', undefined); - expect(a.toString()).toEqual('@{a()}'); + expect(a.toString()).toEqual('${a()}'); const b = new LgTemplateRef('b', []); - expect(b.toString()).toEqual('@{b()}'); + expect(b.toString()).toEqual('${b()}'); const c = new LgTemplateRef('c', ['1', '2']); - expect(c.toString()).toEqual('@{c(1,2)}'); + expect(c.toString()).toEqual('${c(1,2)}'); }); describe('parse()', () => { it('should return null when inputs are invalid', () => { expect(LgTemplateRef.parse('')).toEqual(null); expect(LgTemplateRef.parse('xxx')).toEqual(null); - expect(LgTemplateRef.parse('@{0}')).toEqual(null); + expect(LgTemplateRef.parse('${0}')).toEqual(null); }); it('should return LgTemplateRef when inputs are valid', () => { - const a = LgTemplateRef.parse('@{bfdactivity-123456()}'); + const a = LgTemplateRef.parse('${bfdactivity-123456()}'); expect(a).toEqual(new LgTemplateRef('bfdactivity-123456')); - const a2 = LgTemplateRef.parse('@{bfdactivity-123456()}'); + const a2 = LgTemplateRef.parse('${bfdactivity-123456()}'); expect(a2).toEqual(new LgTemplateRef('bfdactivity-123456')); - const b = LgTemplateRef.parse('@{greeting(1,2)}'); + const b = LgTemplateRef.parse('${greeting(1,2)}'); expect(b).toEqual(new LgTemplateRef('greeting', ['1', '2'])); }); }); diff --git a/Composer/packages/lib/shared/__tests__/lgUtils/parsers/parseLgTemplateRef.test.ts b/Composer/packages/lib/shared/__tests__/lgUtils/parsers/parseLgTemplateRef.test.ts index cc983fdce1..7ecbe397be 100644 --- a/Composer/packages/lib/shared/__tests__/lgUtils/parsers/parseLgTemplateRef.test.ts +++ b/Composer/packages/lib/shared/__tests__/lgUtils/parsers/parseLgTemplateRef.test.ts @@ -8,15 +8,15 @@ describe('parseLgTemplateRef', () => { it('should return null when inputs are invalid', () => { expect(parseLgTemplateRef('')).toEqual(null); expect(parseLgTemplateRef('xxx')).toEqual(null); - expect(parseLgTemplateRef('@{0}')).toEqual(null); - expect(parseLgTemplateRef('hi, @{greeting()}. @{greeting()}')).toEqual(null); + expect(parseLgTemplateRef('${0}')).toEqual(null); + expect(parseLgTemplateRef('hi, ${greeting()}. ${greeting()}')).toEqual(null); }); it('should return LgTemplateRef when inputs are valid', () => { - const a = parseLgTemplateRef('@{bfdactivity-123456()}'); + const a = parseLgTemplateRef('${bfdactivity-123456()}'); expect(a).toEqual(new LgTemplateRef('bfdactivity-123456')); - const b = parseLgTemplateRef('@{greeting(1,2)}'); + const b = parseLgTemplateRef('${greeting(1,2)}'); expect(b).toEqual(new LgTemplateRef('greeting', ['1', '2'])); }); }); @@ -24,8 +24,8 @@ describe('parseLgTemplateRef', () => { describe('extractLgTemplateRefs', () => { it('can extract lg refs from input string', () => { expect(extractLgTemplateRefs('Hi')).toEqual([]); - expect(extractLgTemplateRefs('@{bfdactivity-123456()}')).toEqual([new LgTemplateRef('bfdactivity-123456')]); - expect(extractLgTemplateRefs(`-@{Greeting()}, I'm a fancy bot, @{Bye()}`)).toEqual([ + expect(extractLgTemplateRefs('${bfdactivity-123456()}')).toEqual([new LgTemplateRef('bfdactivity-123456')]); + expect(extractLgTemplateRefs(`-\${Greeting()}, I'm a fancy bot, \${Bye()}`)).toEqual([ new LgTemplateRef('Greeting'), new LgTemplateRef('Bye'), ]); diff --git a/Composer/packages/lib/shared/src/appschema.ts b/Composer/packages/lib/shared/src/appschema.ts index 711f1045dc..5330d03cfa 100644 --- a/Composer/packages/lib/shared/src/appschema.ts +++ b/Composer/packages/lib/shared/src/appschema.ts @@ -1400,11 +1400,6 @@ export const appschema: OBISchema = { description: 'Two-way branch the conversation flow based on a condition.', $ref: '#/definitions/Microsoft.IfCondition', }, - { - title: 'Microsoft.InitProperty', - description: 'Define and initialize a property to be an array or object.', - $ref: '#/definitions/Microsoft.InitProperty', - }, { title: 'Microsoft.LogAction', description: @@ -1700,28 +1695,6 @@ export const appschema: OBISchema = { }, }, }, - 'Microsoft.InitProperty': { - $role: 'unionType(Microsoft.IDialog)', - title: 'Initialize property', - description: 'Define and initialize a property to be an array or object.', - type: 'object', - properties: { - ...$properties(SDKTypes.InitProperty), - property: { - $role: 'expression', - title: 'Property', - description: 'Property (named location to store information).', - examples: ['user.age'], - type: 'string', - }, - type: { - type: 'string', - title: 'Type', - description: 'Type of value.', - enum: ['object', 'array'], - }, - }, - }, 'Microsoft.IpEntityRecognizer': { $role: 'unionType(Microsoft.EntityRecognizers)', title: 'Ip Entity Recognizer', diff --git a/Composer/packages/lib/shared/src/labelMap.ts b/Composer/packages/lib/shared/src/labelMap.ts index 48712512cd..b49e7a4714 100644 --- a/Composer/packages/lib/shared/src/labelMap.ts +++ b/Composer/packages/lib/shared/src/labelMap.ts @@ -91,9 +91,6 @@ export const ConceptLabels: { [key in ConceptLabelKey]?: LabelOverride } = { [SDKTypes.IfCondition]: { title: formatMessage('Branch: if/else'), }, - [SDKTypes.InitProperty]: { - title: formatMessage('Initialize a property'), - }, [SDKTypes.LanguagePolicy]: { title: formatMessage('LanguagePolicy'), }, diff --git a/Composer/packages/lib/shared/src/lgUtils/parsers/lgPatterns.ts b/Composer/packages/lib/shared/src/lgUtils/parsers/lgPatterns.ts index bdfcaa6023..11a4a3cd41 100644 --- a/Composer/packages/lib/shared/src/lgUtils/parsers/lgPatterns.ts +++ b/Composer/packages/lib/shared/src/lgUtils/parsers/lgPatterns.ts @@ -3,4 +3,4 @@ export const LgNamePattern = `bfd(\\w+)-(\\d+)`; -export const LgTemplateRefPattern = `@\\{([A-Za-z_][-\\w]+)(\\([^\\)]*\\))?\\}`; +export const LgTemplateRefPattern = `\\$\\{([A-Za-z_][-\\w]+)(\\([^\\)]*\\))?\\}`; diff --git a/Composer/packages/lib/shared/src/lgUtils/stringBuilders/buildLgTemplateRefString.ts b/Composer/packages/lib/shared/src/lgUtils/stringBuilders/buildLgTemplateRefString.ts index d0029c82a2..c445697275 100644 --- a/Composer/packages/lib/shared/src/lgUtils/stringBuilders/buildLgTemplateRefString.ts +++ b/Composer/packages/lib/shared/src/lgUtils/stringBuilders/buildLgTemplateRefString.ts @@ -11,5 +11,5 @@ import buildLgParamString from './buildLgParamString'; */ export default function buildLgTemplateRefString(lgTemplateRef: LgTemplateRef): LgTemplateRefString { const { name, parameters } = lgTemplateRef; - return `@{${name}${buildLgParamString(parameters)}}`; + return `\${${name}${buildLgParamString(parameters)}}`; } diff --git a/Composer/packages/lib/shared/src/types/schema.ts b/Composer/packages/lib/shared/src/types/schema.ts index 37221d09c8..6bcd5b2818 100644 --- a/Composer/packages/lib/shared/src/types/schema.ts +++ b/Composer/packages/lib/shared/src/types/schema.ts @@ -49,7 +49,6 @@ export enum SDKTypes { HashtagEntityRecognizer = 'Microsoft.HashtagEntityRecognizer', HttpRequest = 'Microsoft.HttpRequest', IfCondition = 'Microsoft.IfCondition', - InitProperty = 'Microsoft.InitProperty', IpEntityRecognizer = 'Microsoft.IpEntityRecognizer', LanguagePolicy = 'Microsoft.LanguagePolicy', LogAction = 'Microsoft.LogAction', diff --git a/Composer/packages/lib/shared/src/viewUtils.ts b/Composer/packages/lib/shared/src/viewUtils.ts index df4b202ea6..923c54b228 100644 --- a/Composer/packages/lib/shared/src/viewUtils.ts +++ b/Composer/packages/lib/shared/src/viewUtils.ts @@ -69,7 +69,6 @@ export const dialogGroups: DialogGroupsMap = { types: [ SDKTypes.SetProperty, SDKTypes.SetProperties, - SDKTypes.InitProperty, SDKTypes.DeleteProperty, SDKTypes.DeleteProperties, SDKTypes.EditArray, diff --git a/Composer/packages/server/__tests__/mocks/samplebots/bot1/ComposerDialogs/Main.dialog b/Composer/packages/server/__tests__/mocks/samplebots/bot1/ComposerDialogs/Main.dialog index 466beb28e8..54f8b4ef1d 100644 --- a/Composer/packages/server/__tests__/mocks/samplebots/bot1/ComposerDialogs/Main.dialog +++ b/Composer/packages/server/__tests__/mocks/samplebots/bot1/ComposerDialogs/Main.dialog @@ -3,27 +3,27 @@ "steps": [ { "$type": "Microsoft.TextInput", - "prompt": "@{hello()} I'm a ToDo bot" + "prompt": "${hello()} I'm a ToDo bot" }, { "$type": "Microsoft.SendActivity", - "activity": "@{bye()} See you again" + "activity": "${bye()} See you again" }, { "$type": "Microsoft.SendActivity", - "activity": "@{bye()} bye bye again" + "activity": "${bye()} bye bye again" }, { "$type": "Microsoft.SendActivity", - "activity": "@{ShowImage(dialog.attachments[0].contentUrl, dialog.attachments[0].contentType)}" + "activity": "${ShowImage(dialog.attachments[0].contentUrl, dialog.attachments[0].contentType)}" }, { "$type": "Microsoft.SendActivity", - "activity": "You entered: @{user.date[0].value}" + "activity": "You entered: ${user.date[0].value}" }, { "$type": "Microsoft.TextInput", - "activity": "@{bye3()} See you again" + "activity": "${bye3()} See you again" }, { "$type": "Microsoft.OnIntent", @@ -35,5 +35,6 @@ } ] } - ] + ], + "generator": "Main.lg" } diff --git a/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/Actions/Actions.dialog b/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/Actions/Actions.dialog index 4f3d21d90a..a883e20720 100644 --- a/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/Actions/Actions.dialog +++ b/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/Actions/Actions.dialog @@ -18,21 +18,21 @@ "$designer": { "id": "166131" }, - "activity": "@{bfdactivity-522200()}" + "activity": "${bfdactivity-522200()}" }, { "$type": "Microsoft.SendActivity", "$designer": { "id": "350559" }, - "activity": "@{bfdactivity-350559()}" + "activity": "${bfdactivity-350559()}" }, { "$type": "Microsoft.SendActivity", "$designer": { "id": "309397" }, - "activity": "@{bfdactivity-309397()}" + "activity": "${bfdactivity-309397()}" }, { "$type": "Microsoft.SetProperty", @@ -40,14 +40,14 @@ "id": "026341" }, "property": "user.age", - "value": "9+9" + "value": "18" }, { "$type": "Microsoft.SendActivity", "$designer": { "id": "643043" }, - "activity": "@{bfdactivity-797905()}" + "activity": "${bfdactivity-797905()}" }, { "$type": "Microsoft.DeleteProperty", @@ -68,7 +68,7 @@ "$designer": { "id": "166131" }, - "activity": "@{bfdactivity-166131()}" + "activity": "${bfdactivity-166131()}" } ], "elseActions": [ @@ -77,7 +77,7 @@ "$designer": { "id": "643043" }, - "activity": "@{bfdactivity-643043()}" + "activity": "${bfdactivity-643043()}" } ] } diff --git a/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/Actions/Actions.lg b/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/Actions/Actions.lg index 9d3305086b..bbc23edd3d 100644 --- a/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/Actions/Actions.lg +++ b/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/Actions/Actions.lg @@ -10,10 +10,10 @@ -Step 3 # bfdactivity-797905 --user.age is set to @{user.age} +-user.age is set to ${user.age} # bfdactivity-166131 --user.age is set to @{user.age} +-user.age is set to ${user.age} # bfdactivity-643043 -user.age is set to null \ No newline at end of file diff --git a/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/EditActions/EditActions.dialog b/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/EditActions/EditActions.dialog index f694e97540..7fb50111c8 100644 --- a/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/EditActions/EditActions.dialog +++ b/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/EditActions/EditActions.dialog @@ -17,12 +17,13 @@ "$designer": { "id": "250234" }, - "changeType": "Insertactions", + "changeType": "insertActions", "actions": [ { "$type": "Microsoft.TextInput", "prompt": "Hello, I'm Zoidberg. What is your name?", - "property": "user.name" + "property": "user.name", + "alwaysPrompt": "true" } ] }, @@ -31,7 +32,7 @@ "$designer": { "id": "443878" }, - "changeType": "Appendactions", + "changeType": "appendActions", "actions": [ { "$type": "Microsoft.SendActivity", @@ -44,7 +45,7 @@ "$designer": { "id": "644042" }, - "activity": "@{bfdactivity-644042()}" + "activity": "${bfdactivity-644042()}" } ] } diff --git a/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/EditActions/EditActions.lg b/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/EditActions/EditActions.lg index a83d59bfe8..b35fd7d4cb 100644 --- a/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/EditActions/EditActions.lg +++ b/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/EditActions/EditActions.lg @@ -1,4 +1,4 @@ [import](common.lg) # bfdactivity-644042 --Hello @{user.name}, nice to talk to you! \ No newline at end of file +-Hello ${user.name}, nice to talk to you! \ No newline at end of file diff --git a/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/EditArray/EditArray.dialog b/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/EditArray/EditArray.dialog index 1598dc0331..df0e72613f 100644 --- a/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/EditArray/EditArray.dialog +++ b/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/EditArray/EditArray.dialog @@ -12,23 +12,6 @@ "id": "356862" }, "actions": [ - { - "$type": "Microsoft.IfCondition", - "$designer": { - "id": "356862" - }, - "condition": "user.ids == null", - "actions": [ - { - "$type": "Microsoft.InitProperty", - "$designer": { - "id": "384024" - }, - "property": "user.ids", - "type": "array" - } - ] - }, { "$type": "Microsoft.EditArray", "$designer": { @@ -36,7 +19,7 @@ }, "changeType": "Push", "itemsProperty": "user.ids", - "value": "10000+1000+100+10+1" + "value": "=10000+1000+100+10+1" }, { "$type": "Microsoft.EditArray", @@ -45,13 +28,13 @@ }, "changeType": "Push", "itemsProperty": "user.ids", - "value": "200*200" + "value": "=200*200" }, { "$type": "Microsoft.EditArray", "changeType": "Push", "itemsProperty": "user.ids", - "value": "888888/4", + "value": "=888888/4", "$designer": { "id": "393322" } @@ -61,7 +44,7 @@ "$designer": { "id": "666135" }, - "activity": "@{bfdactivity-666135()}" + "activity": "${bfdactivity-666135()}" }, { "$type": "Microsoft.Foreach", @@ -72,7 +55,7 @@ "actions": [ { "$type": "Microsoft.SendActivity", - "activity": "@{dialog.foreach.index}: @{dialog.foreach.value}" + "activity": "${dialog.foreach.index}: ${dialog.foreach.value}" } ] }, @@ -81,7 +64,7 @@ "$designer": { "id": "763672" }, - "activity": "@{bfdactivity-763672()}" + "activity": "${bfdactivity-763672()}" }, { "$type": "Microsoft.ForeachPage", @@ -97,7 +80,7 @@ "actions": [ { "$type": "Microsoft.SendActivity", - "activity": "@{dialog.foreach.index}: @{dialog.foreach.value}" + "activity": "${dialog.foreach.index}: ${dialog.foreach.value}" } ], "$designer": { diff --git a/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/EmitAnEvent/EmitAnEvent.dialog b/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/EmitAnEvent/EmitAnEvent.dialog index 158f5704b0..3026c4a8f0 100644 --- a/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/EmitAnEvent/EmitAnEvent.dialog +++ b/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/EmitAnEvent/EmitAnEvent.dialog @@ -12,12 +12,10 @@ "$type": "Microsoft.RegexRecognizer", "intents": [ { - "$type": "Microsoft.IntentPattern", "intent": "EmitEvent", "pattern": "emit" }, { - "$type": "Microsoft.IntentPattern", "intent": "CowboyIntent", "pattern": "moo" } @@ -57,7 +55,7 @@ "id": "576334", "name": "Send a response" }, - "activity": "@{bfdactivity-576334()}" + "activity": "${bfdactivity-576334()}" } ] } diff --git a/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/EndDialog/EndDialog.dialog b/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/EndDialog/EndDialog.dialog index ca6efc1f7a..98069e9c27 100644 --- a/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/EndDialog/EndDialog.dialog +++ b/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/EndDialog/EndDialog.dialog @@ -8,12 +8,10 @@ "$type": "Microsoft.RegexRecognizer", "intents": [ { - "$type": "Microsoft.IntentPattern", "intent": "JokeIntent", "pattern": "(?i)joke" }, { - "$type": "Microsoft.IntentPattern", "intent": "CancelIntent", "pattern": "(?i)cancel|never mind" } @@ -74,14 +72,14 @@ "$designer": { "id": "604381" }, - "activity": "@{bfdactivity-604381()}" + "activity": "${bfdactivity-604381()}" }, { "$type": "Microsoft.SendActivity", "$designer": { "id": "338063" }, - "activity": "@{bfdactivity-338063()}" + "activity": "${bfdactivity-338063()}" } ] } diff --git a/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/EndDialog/EndDialog.lg b/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/EndDialog/EndDialog.lg index 3f1382ec89..a8da6abca5 100644 --- a/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/EndDialog/EndDialog.lg +++ b/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/EndDialog/EndDialog.lg @@ -1,7 +1,7 @@ [import](common.lg) # bfdactivity-604381 --Hello @{user.name}, nice to talk to you! +-Hello ${user.name}, nice to talk to you! # bfdactivity-338063 -I'm a joke bot. To get started say "joke". \ No newline at end of file diff --git a/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/EndDialog/TellJoke/EndDialog.TellJoke.dialog b/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/EndDialog/TellJoke/EndDialog.TellJoke.dialog index a2b038b2e0..1999301f21 100644 --- a/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/EndDialog/TellJoke/EndDialog.TellJoke.dialog +++ b/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/EndDialog/TellJoke/EndDialog.TellJoke.dialog @@ -8,7 +8,6 @@ "$type": "Microsoft.RegexRecognizer", "intents": [ { - "$type": "Microsoft.IntentPattern", "intent": "CancelIntent", "pattern": "(?i)cancel|never mind" } @@ -38,7 +37,7 @@ "$designer": { "id": "150220" }, - "activity": "@{bfdactivity-150220()}" + "activity": "${bfdactivity-150220()}" }, { "$type": "Microsoft.EndTurn", @@ -51,7 +50,7 @@ "$designer": { "id": "451180" }, - "activity": "@{bfdactivity-451180()}" + "activity": "${bfdactivity-451180()}" } ] } diff --git a/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/EndTurn/EndTurn.dialog b/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/EndTurn/EndTurn.dialog index 7cdd076cc2..52c2b6a80e 100644 --- a/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/EndTurn/EndTurn.dialog +++ b/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/EndTurn/EndTurn.dialog @@ -17,7 +17,7 @@ "$designer": { "id": "423305" }, - "activity": "@{bfdactivity-423305()}" + "activity": "${bfdactivity-423305()}" }, { "$type": "Microsoft.EndTurn", @@ -30,7 +30,7 @@ "$designer": { "id": "448895" }, - "activity": "@{bfdactivity-448895()}" + "activity": "${bfdactivity-448895()}" } ] } diff --git a/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/HttpRequest/HttpRequest.dialog b/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/HttpRequest/HttpRequest.dialog index 73835c51f9..1870dd309b 100644 --- a/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/HttpRequest/HttpRequest.dialog +++ b/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/HttpRequest/HttpRequest.dialog @@ -28,7 +28,7 @@ "$designer": { "id": "229784" }, - "activity": "@{bfdactivity-229784()}" + "activity": "${bfdactivity-229784()}" }, { "$type": "Microsoft.TextInput", @@ -77,7 +77,7 @@ "$designer": { "id": "200761" }, - "activity": "@{bfdactivity-200761()}" + "activity": "${bfdactivity-200761()}" }, { "$type": "Microsoft.TextInput", @@ -104,7 +104,7 @@ "$designer": { "id": "212615" }, - "activity": "@{bfdactivity-212615()}" + "activity": "${bfdactivity-212615()}" } ] } diff --git a/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/HttpRequest/HttpRequest.lg b/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/HttpRequest/HttpRequest.lg index 7e60af22e3..f5a127a0e5 100644 --- a/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/HttpRequest/HttpRequest.lg +++ b/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/HttpRequest/HttpRequest.lg @@ -1,10 +1,10 @@ [import](common.lg) # bfdactivity-229784 --Great! Your pet's name is @{user.petname} +-Great! Your pet's name is ${user.petname} # bfdactivity-200761 --Done! You have added a pet named "@{user.postResponse.content.name}" with id "@{user.postResponse.content.id}" +-Done! You have added a pet named "${user.postResponse.content.name}" with id "${user.postResponse.content.id}" # bfdactivity-212615 --Great! I found your pet named "@{user.getResponse.content.name}" \ No newline at end of file +-Great! I found your pet named "${user.getResponse.content.name}" \ No newline at end of file diff --git a/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/IfCondition/IfCondition.dialog b/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/IfCondition/IfCondition.dialog index 10a970750d..bf75175f08 100644 --- a/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/IfCondition/IfCondition.dialog +++ b/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/IfCondition/IfCondition.dialog @@ -37,7 +37,7 @@ "$designer": { "id": "485963" }, - "activity": "@{bfdactivity-485963()}" + "activity": "${bfdactivity-485963()}" } ] } diff --git a/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/IfCondition/IfCondition.lg b/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/IfCondition/IfCondition.lg index 823cc66414..84338357a6 100644 --- a/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/IfCondition/IfCondition.lg +++ b/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/IfCondition/IfCondition.lg @@ -1,4 +1,4 @@ [import](common.lg) # bfdactivity-485963 --Hello @{user.name}, nice to talk to you! \ No newline at end of file +-Hello ${user.name}, nice to talk to you! \ No newline at end of file diff --git a/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/Main/Main.dialog b/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/Main/Main.dialog index 41293bfc03..a1c8ee3108 100644 --- a/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/Main/Main.dialog +++ b/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/Main/Main.dialog @@ -10,62 +10,50 @@ "$type": "Microsoft.RegexRecognizer", "intents": [ { - "$type": "Microsoft.IntentPattern", "intent": "Actions", "pattern": "Actions|01" }, { - "$type": "Microsoft.IntentPattern", "intent": "EndTurn", "pattern": "EndTurn|02" }, { - "$type": "Microsoft.IntentPattern", "intent": "IfCondition", "pattern": "IfCondition|03" }, { - "$type": "Microsoft.IntentPattern", "intent": "EditArray", "pattern": "EditArray|04" }, { - "$type": "Microsoft.IntentPattern", "intent": "EndDialog", "pattern": "EndDialog|05" }, { - "$type": "Microsoft.IntentPattern", "intent": "HttpRequest", "pattern": "HttpRequest|06" }, { - "$type": "Microsoft.IntentPattern", "intent": "SwitchCondition", "pattern": "SwitchCondition|07" }, { - "$type": "Microsoft.IntentPattern", "intent": "RepeatDialog", "pattern": "RepeatDialog|08" }, { - "$type": "Microsoft.IntentPattern", "intent": "TraceAndLog", "pattern": "TraceAndLog|trace|log|09" }, { - "$type": "Microsoft.IntentPattern", "intent": "EditActions", "pattern": "EditActions|10" }, { - "$type": "Microsoft.IntentPattern", "intent": "ReplaceDialog", "pattern": "ReplaceDialog|11" }, { - "$type": "Microsoft.IntentPattern", "intent": "EmitEvent", "pattern": "EmitEvent|12" }, @@ -74,7 +62,6 @@ "pattern": "QnAMaker|13" }, { - "$type": "Microsoft.IntentPattern", "intent": "CancelIntent", "pattern": "(?i)cancel|never mind" } @@ -263,7 +250,7 @@ "$designer": { "id": "347762" }, - "activity": "@{bfdactivity-640616()}" + "activity": "${bfdactivity-640616()}" } ] }, @@ -288,7 +275,7 @@ "id": "641773", "name": "Branch: if/else" }, - "condition": "string(dialog.foreach.value.id) != string(turn.Activity.Recipient.id)", + "condition": "=string(dialog.foreach.value.id) != string(turn.Activity.Recipient.id)", "actions": [ { "$type": "Microsoft.SendActivity", @@ -296,7 +283,7 @@ "id": "640616", "name": "Send a response" }, - "activity": "@{bfdactivity-640616()}" + "activity": "${bfdactivity-640616()}" } ] } diff --git a/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/Main/Main.lg b/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/Main/Main.lg index 213acb31bb..865780213f 100644 --- a/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/Main/Main.lg +++ b/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/Main/Main.lg @@ -1,4 +1,4 @@ [import](common.lg) # bfdactivity-640616 --@{help()} \ No newline at end of file +-${help()} \ No newline at end of file diff --git a/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/ReplaceDialog/FortuneTellerDialog/FortuneTellerDialog.dialog b/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/ReplaceDialog/FortuneTellerDialog/FortuneTellerDialog.dialog index 7c7e46bae6..e3b45abdc4 100644 --- a/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/ReplaceDialog/FortuneTellerDialog/FortuneTellerDialog.dialog +++ b/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/ReplaceDialog/FortuneTellerDialog/FortuneTellerDialog.dialog @@ -17,21 +17,21 @@ "$designer": { "id": "554030" }, - "activity": "@{bfdactivity-554030()}" + "activity": "${bfdactivity-554030()}" }, { "$type": "Microsoft.SendActivity", "$designer": { "id": "302405" }, - "activity": "@{bfdactivity-302405()}" + "activity": "${bfdactivity-302405()}" }, { "$type": "Microsoft.SendActivity", "$designer": { "id": "322106" }, - "activity": "@{bfdactivity-322106()}" + "activity": "${bfdactivity-322106()}" } ] } diff --git a/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/ReplaceDialog/Replace.dialog b/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/ReplaceDialog/Replace.dialog index 33faac0ba1..b7f9a5b172 100644 --- a/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/ReplaceDialog/Replace.dialog +++ b/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/ReplaceDialog/Replace.dialog @@ -8,12 +8,10 @@ "$type": "Microsoft.RegexRecognizer", "intents": [ { - "$type": "Microsoft.IntentPattern", "intent": "JokeIntent", "pattern": "(?i)joke" }, { - "$type": "Microsoft.IntentPattern", "intent": "FortuneTellerIntent", "pattern": "(?i)fortune|future" } @@ -66,7 +64,7 @@ }, "property": "user.name", "prompt": "Hello, I'm Zoidberg. What is your name?", - "maxTurnCount": 2147483647, + "maxTurnCount": 3, "alwaysPrompt": false, "allowInterruptions": "true" } @@ -77,7 +75,7 @@ "$designer": { "id": "120128" }, - "activity": "@{bfdactivity-120128()}" + "activity": "${bfdactivity-120128()}" } ] } diff --git a/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/ReplaceDialog/Replace.lg b/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/ReplaceDialog/Replace.lg index a7ade4f9cb..1e603d6df5 100644 --- a/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/ReplaceDialog/Replace.lg +++ b/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/ReplaceDialog/Replace.lg @@ -1,4 +1,4 @@ [import](common.lg) # bfdactivity-120128 --Hello @{user.name}, nice to talk to you! Please either enter 'joke' or 'fortune' to replace the dialog you want. \ No newline at end of file +-Hello ${user.name}, nice to talk to you! Please either enter 'joke' or 'fortune' to replace the dialog you want. \ No newline at end of file diff --git a/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/ReplaceDialog/TellJokeDialog/TellJokeDialog.dialog b/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/ReplaceDialog/TellJokeDialog/TellJokeDialog.dialog index 762d108e6b..68ed7ed16c 100644 --- a/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/ReplaceDialog/TellJokeDialog/TellJokeDialog.dialog +++ b/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/ReplaceDialog/TellJokeDialog/TellJokeDialog.dialog @@ -17,7 +17,7 @@ "$designer": { "id": "577618" }, - "activity": "@{bfdactivity-577618()}" + "activity": "${bfdactivity-577618()}" }, { "$type": "Microsoft.EndTurn", @@ -30,7 +30,7 @@ "$designer": { "id": "286023" }, - "activity": "@{bfdactivity-286023()}" + "activity": "${bfdactivity-286023()}" } ] } diff --git a/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/SwitchCondition/SwitchCondition.dialog b/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/SwitchCondition/SwitchCondition.dialog index cd1b09f32c..ad34bcf593 100644 --- a/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/SwitchCondition/SwitchCondition.dialog +++ b/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/SwitchCondition/SwitchCondition.dialog @@ -52,7 +52,7 @@ "$designer": { "id": "160914" }, - "activity": "@{bfdactivity-160914()}" + "activity": "${bfdactivity-160914()}" }, { "$type": "Microsoft.SwitchCondition", @@ -69,7 +69,7 @@ "$designer": { "id": "576002" }, - "activity": "@{bfdactivity-576002()}" + "activity": "${bfdactivity-576002()}" } ] }, @@ -81,7 +81,7 @@ "$designer": { "id": "677412" }, - "activity": "@{bfdactivity-677412()}" + "activity": "${bfdactivity-677412()}" } ] }, @@ -93,7 +93,7 @@ "$designer": { "id": "700082" }, - "activity": "@{bfdactivity-700082()}" + "activity": "${bfdactivity-700082()}" } ] } diff --git a/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/SwitchCondition/SwitchCondition.lg b/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/SwitchCondition/SwitchCondition.lg index a85c5c4551..a9b546285d 100644 --- a/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/SwitchCondition/SwitchCondition.lg +++ b/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/SwitchCondition/SwitchCondition.lg @@ -1,7 +1,7 @@ [import](common.lg) # bfdactivity-160914 --You select: @{user.style} +-You select: ${user.style} # bfdactivity-576002 -You select: 1 diff --git a/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/TraceAndLog/TraceAndLog.dialog b/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/TraceAndLog/TraceAndLog.dialog index 400e32dd58..22af895044 100644 --- a/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/TraceAndLog/TraceAndLog.dialog +++ b/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/TraceAndLog/TraceAndLog.dialog @@ -37,7 +37,7 @@ "$designer": { "id": "219510" }, - "text": "@{user.name}", + "text": "${user.name}", "traceActivity": false } ] diff --git a/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/common/common.lg b/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/common/common.lg index cce649deca..1778971b28 100644 --- a/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/common/common.lg +++ b/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/common/common.lg @@ -14,18 +14,5 @@ 12 - EmitEvent 13 - QnAMaker``` - - - - - - - - - - - - - # bfdactivity-839941 --Hello @{user.name}, nice to meet you! \ No newline at end of file +-Hello ${user.name}, nice to meet you! \ No newline at end of file diff --git a/Composer/packages/server/assets/projects/AskingQuestionsSample/ComposerDialogs/AttachmentInput/AttachmentInput.dialog b/Composer/packages/server/assets/projects/AskingQuestionsSample/ComposerDialogs/AttachmentInput/AttachmentInput.dialog index 1bca05a015..6a5a5f712a 100644 --- a/Composer/packages/server/assets/projects/AskingQuestionsSample/ComposerDialogs/AttachmentInput/AttachmentInput.dialog +++ b/Composer/packages/server/assets/projects/AskingQuestionsSample/ComposerDialogs/AttachmentInput/AttachmentInput.dialog @@ -29,7 +29,7 @@ "$designer": { "id": "365176" }, - "activity": "@{bfdactivity-365176()}" + "activity": "${bfdactivity-365176()}" } ] } diff --git a/Composer/packages/server/assets/projects/AskingQuestionsSample/ComposerDialogs/AttachmentInput/AttachmentInput.lg b/Composer/packages/server/assets/projects/AskingQuestionsSample/ComposerDialogs/AttachmentInput/AttachmentInput.lg index b231d0f602..102448684a 100644 --- a/Composer/packages/server/assets/projects/AskingQuestionsSample/ComposerDialogs/AttachmentInput/AttachmentInput.lg +++ b/Composer/packages/server/assets/projects/AskingQuestionsSample/ComposerDialogs/AttachmentInput/AttachmentInput.lg @@ -1,4 +1,4 @@ [import](common.lg) # bfdactivity-365176 --@{ShowImage(dialog.attachments[0].contentUrl, dialog.attachments[0].contentType)} \ No newline at end of file +-${ShowImage(dialog.attachments[0].contentUrl, dialog.attachments[0].contentType)} \ No newline at end of file diff --git a/Composer/packages/server/assets/projects/AskingQuestionsSample/ComposerDialogs/ChoiceInput/ChoiceInput.dialog b/Composer/packages/server/assets/projects/AskingQuestionsSample/ComposerDialogs/ChoiceInput/ChoiceInput.dialog index e9ee6b489b..3d235ebd9e 100644 --- a/Composer/packages/server/assets/projects/AskingQuestionsSample/ComposerDialogs/ChoiceInput/ChoiceInput.dialog +++ b/Composer/packages/server/assets/projects/AskingQuestionsSample/ComposerDialogs/ChoiceInput/ChoiceInput.dialog @@ -54,7 +54,7 @@ "$designer": { "id": "004212" }, - "activity": "@{bfdactivity-004212()}" + "activity": "${bfdactivity-004212()}" } ] } diff --git a/Composer/packages/server/assets/projects/AskingQuestionsSample/ComposerDialogs/ChoiceInput/ChoiceInput.lg b/Composer/packages/server/assets/projects/AskingQuestionsSample/ComposerDialogs/ChoiceInput/ChoiceInput.lg index 5d96e6a4e2..540c92f505 100644 --- a/Composer/packages/server/assets/projects/AskingQuestionsSample/ComposerDialogs/ChoiceInput/ChoiceInput.lg +++ b/Composer/packages/server/assets/projects/AskingQuestionsSample/ComposerDialogs/ChoiceInput/ChoiceInput.lg @@ -1,4 +1,4 @@ [import](common.lg) # bfdactivity-004212 --You select: @{user.style} \ No newline at end of file +-You select: ${user.style} \ No newline at end of file diff --git a/Composer/packages/server/assets/projects/AskingQuestionsSample/ComposerDialogs/ConfirmInput/ConfirmInput.dialog b/Composer/packages/server/assets/projects/AskingQuestionsSample/ComposerDialogs/ConfirmInput/ConfirmInput.dialog index adb992ee9b..035e7026f6 100644 --- a/Composer/packages/server/assets/projects/AskingQuestionsSample/ComposerDialogs/ConfirmInput/ConfirmInput.dialog +++ b/Composer/packages/server/assets/projects/AskingQuestionsSample/ComposerDialogs/ConfirmInput/ConfirmInput.dialog @@ -36,7 +36,7 @@ "$designer": { "id": "458458" }, - "activity": "@{bfdactivity-458458()}" + "activity": "${bfdactivity-458458()}" } ] } diff --git a/Composer/packages/server/assets/projects/AskingQuestionsSample/ComposerDialogs/ConfirmInput/ConfirmInput.lg b/Composer/packages/server/assets/projects/AskingQuestionsSample/ComposerDialogs/ConfirmInput/ConfirmInput.lg index 395e5383c1..d644bece89 100644 --- a/Composer/packages/server/assets/projects/AskingQuestionsSample/ComposerDialogs/ConfirmInput/ConfirmInput.lg +++ b/Composer/packages/server/assets/projects/AskingQuestionsSample/ComposerDialogs/ConfirmInput/ConfirmInput.lg @@ -1,4 +1,4 @@ [import](common.lg) # bfdactivity-458458 --confirmation: @{user.confirmed} \ No newline at end of file +-confirmation: ${user.confirmed} \ No newline at end of file diff --git a/Composer/packages/server/assets/projects/AskingQuestionsSample/ComposerDialogs/DateTimeInput/DateTimeInput.dialog b/Composer/packages/server/assets/projects/AskingQuestionsSample/ComposerDialogs/DateTimeInput/DateTimeInput.dialog index 47598932d5..5711dff8b2 100644 --- a/Composer/packages/server/assets/projects/AskingQuestionsSample/ComposerDialogs/DateTimeInput/DateTimeInput.dialog +++ b/Composer/packages/server/assets/projects/AskingQuestionsSample/ComposerDialogs/DateTimeInput/DateTimeInput.dialog @@ -30,7 +30,7 @@ "$designer": { "id": "828009" }, - "activity": "@{bfdactivity-828009()}" + "activity": "${bfdactivity-828009()}" } ] } diff --git a/Composer/packages/server/assets/projects/AskingQuestionsSample/ComposerDialogs/DateTimeInput/DateTimeInput.lg b/Composer/packages/server/assets/projects/AskingQuestionsSample/ComposerDialogs/DateTimeInput/DateTimeInput.lg index 398191b4d1..e809133c2c 100644 --- a/Composer/packages/server/assets/projects/AskingQuestionsSample/ComposerDialogs/DateTimeInput/DateTimeInput.lg +++ b/Composer/packages/server/assets/projects/AskingQuestionsSample/ComposerDialogs/DateTimeInput/DateTimeInput.lg @@ -1,4 +1,4 @@ [import](common.lg) # bfdactivity-828009 --You entered: @{user.date[0].value} \ No newline at end of file +-You entered: ${user.date[0].value} \ No newline at end of file diff --git a/Composer/packages/server/assets/projects/AskingQuestionsSample/ComposerDialogs/Main/Main.dialog b/Composer/packages/server/assets/projects/AskingQuestionsSample/ComposerDialogs/Main/Main.dialog index a52fb91df5..f27b0a48a8 100644 --- a/Composer/packages/server/assets/projects/AskingQuestionsSample/ComposerDialogs/Main/Main.dialog +++ b/Composer/packages/server/assets/projects/AskingQuestionsSample/ComposerDialogs/Main/Main.dialog @@ -10,42 +10,34 @@ "$type": "Microsoft.RegexRecognizer", "intents": [ { - "$type": "Microsoft.IntentPattern", "intent": "TextInput", "pattern": "TextInput|01" }, { - "$type": "Microsoft.IntentPattern", "intent": "NumberInput", "pattern": "NumberInput|02" }, { - "$type": "Microsoft.IntentPattern", "intent": "ConfirmInput", "pattern": "ConfirmInput|03" }, { - "$type": "Microsoft.IntentPattern", "intent": "ChoiceInput", "pattern": "ChoiceInput|04" }, { - "$type": "Microsoft.IntentPattern", "intent": "AttachmentInput", "pattern": "AttachmentInput|05" }, { - "$type": "Microsoft.IntentPattern", "intent": "DateTimeInput", "pattern": "DateTimeInput|06" }, { - "$type": "Microsoft.IntentPattern", "intent": "OAuthInput", "pattern": "OAuthInput|07" }, { - "$type": "Microsoft.IntentPattern", "intent": "cancel", "pattern": "cancel" } @@ -73,7 +65,7 @@ "actions": [ { "$type": "Microsoft.SendActivity", - "activity": "@{help()}" + "activity": "${help()}" } ] } @@ -183,7 +175,7 @@ "$designer": { "id": "068558" }, - "activity": "@{bfdactivity-068558()}" + "activity": "${bfdactivity-068558()}" } ], "intent": "CancelIntent" @@ -199,7 +191,7 @@ "$designer": { "id": "581197" }, - "activity": "@{bfdactivity-581197()}" + "activity": "${bfdactivity-581197()}" } ] } diff --git a/Composer/packages/server/assets/projects/AskingQuestionsSample/ComposerDialogs/Main/Main.lg b/Composer/packages/server/assets/projects/AskingQuestionsSample/ComposerDialogs/Main/Main.lg index a1e329c485..4137f1038c 100644 --- a/Composer/packages/server/assets/projects/AskingQuestionsSample/ComposerDialogs/Main/Main.lg +++ b/Composer/packages/server/assets/projects/AskingQuestionsSample/ComposerDialogs/Main/Main.lg @@ -1,7 +1,7 @@ [import](common.lg) # bfdactivity-068558 --@{help()} +-${help()} # bfdactivity-581197 --@{help()} \ No newline at end of file +-${help()} \ No newline at end of file diff --git a/Composer/packages/server/assets/projects/AskingQuestionsSample/ComposerDialogs/NumberInput/NumberInput.dialog b/Composer/packages/server/assets/projects/AskingQuestionsSample/ComposerDialogs/NumberInput/NumberInput.dialog index dbc351ce38..0aa64c1e5b 100644 --- a/Composer/packages/server/assets/projects/AskingQuestionsSample/ComposerDialogs/NumberInput/NumberInput.dialog +++ b/Composer/packages/server/assets/projects/AskingQuestionsSample/ComposerDialogs/NumberInput/NumberInput.dialog @@ -30,7 +30,7 @@ "$designer": { "id": "303304" }, - "activity": "@{bfdactivity-303304()}" + "activity": "${bfdactivity-303304()}" }, { "$type": "Microsoft.NumberInput", @@ -56,7 +56,7 @@ "$designer": { "id": "284482" }, - "activity": "@{bfdactivity-284482()}" + "activity": "${bfdactivity-284482()}" } ], "elseActions": [ @@ -65,7 +65,7 @@ "$designer": { "id": "172233" }, - "activity": "@{bfdactivity-172233()}" + "activity": "${bfdactivity-172233()}" } ] } diff --git a/Composer/packages/server/assets/projects/AskingQuestionsSample/ComposerDialogs/NumberInput/NumberInput.lg b/Composer/packages/server/assets/projects/AskingQuestionsSample/ComposerDialogs/NumberInput/NumberInput.lg index ed50fd486c..c0ad2f2fc0 100644 --- a/Composer/packages/server/assets/projects/AskingQuestionsSample/ComposerDialogs/NumberInput/NumberInput.lg +++ b/Composer/packages/server/assets/projects/AskingQuestionsSample/ComposerDialogs/NumberInput/NumberInput.lg @@ -1,10 +1,10 @@ [import](common.lg) # bfdactivity-303304 --Hello, your age is @{user.age}! +-Hello, your age is ${user.age}! # bfdactivity-284482 --2 * 2.2 equals @{user.result}, that's right! +-2 * 2.2 equals ${user.result}, that's right! # bfdactivity-172233 --2 * 2.2 equals @{user.result}, that's wrong! \ No newline at end of file +-2 * 2.2 equals ${user.result}, that's wrong! \ No newline at end of file diff --git a/Composer/packages/server/assets/projects/AskingQuestionsSample/ComposerDialogs/OAuthInput/OAuthInput.dialog b/Composer/packages/server/assets/projects/AskingQuestionsSample/ComposerDialogs/OAuthInput/OAuthInput.dialog index b0cc560259..fd8d7f04c0 100644 --- a/Composer/packages/server/assets/projects/AskingQuestionsSample/ComposerDialogs/OAuthInput/OAuthInput.dialog +++ b/Composer/packages/server/assets/projects/AskingQuestionsSample/ComposerDialogs/OAuthInput/OAuthInput.dialog @@ -48,7 +48,7 @@ "$designer": { "id": "991558" }, - "activity": "@{bfdactivity-991558()}" + "activity": "${bfdactivity-991558()}" } ] } diff --git a/Composer/packages/server/assets/projects/AskingQuestionsSample/ComposerDialogs/OAuthInput/OAuthInput.lg b/Composer/packages/server/assets/projects/AskingQuestionsSample/ComposerDialogs/OAuthInput/OAuthInput.lg index 7e5d56c20a..550972f230 100644 --- a/Composer/packages/server/assets/projects/AskingQuestionsSample/ComposerDialogs/OAuthInput/OAuthInput.lg +++ b/Composer/packages/server/assets/projects/AskingQuestionsSample/ComposerDialogs/OAuthInput/OAuthInput.lg @@ -1,4 +1,4 @@ [import](common.lg) # bfdactivity-991558 --@{ShowEmailSummary(user)} \ No newline at end of file +-${ShowEmailSummary(user)} \ No newline at end of file diff --git a/Composer/packages/server/assets/projects/AskingQuestionsSample/ComposerDialogs/TextInput/TextInput.dialog b/Composer/packages/server/assets/projects/AskingQuestionsSample/ComposerDialogs/TextInput/TextInput.dialog index 7e8bd3581e..4cdf31e829 100644 --- a/Composer/packages/server/assets/projects/AskingQuestionsSample/ComposerDialogs/TextInput/TextInput.dialog +++ b/Composer/packages/server/assets/projects/AskingQuestionsSample/ComposerDialogs/TextInput/TextInput.dialog @@ -28,7 +28,7 @@ "$designer": { "id": "538962" }, - "activity": "@{bfdactivity-538962()}" + "activity": "${bfdactivity-538962()}" } ] } diff --git a/Composer/packages/server/assets/projects/AskingQuestionsSample/ComposerDialogs/TextInput/TextInput.lg b/Composer/packages/server/assets/projects/AskingQuestionsSample/ComposerDialogs/TextInput/TextInput.lg index 5ba7ddd1e5..473ca73db6 100644 --- a/Composer/packages/server/assets/projects/AskingQuestionsSample/ComposerDialogs/TextInput/TextInput.lg +++ b/Composer/packages/server/assets/projects/AskingQuestionsSample/ComposerDialogs/TextInput/TextInput.lg @@ -1,4 +1,4 @@ [import](common.lg) # bfdactivity-538962 --Hello @{user.name}, nice to talk to you! \ No newline at end of file +-Hello ${user.name}, nice to talk to you! \ No newline at end of file diff --git a/Composer/packages/server/assets/projects/AskingQuestionsSample/ComposerDialogs/common/common.lg b/Composer/packages/server/assets/projects/AskingQuestionsSample/ComposerDialogs/common/common.lg index 5eebc5130c..218fcce578 100644 --- a/Composer/packages/server/assets/projects/AskingQuestionsSample/ComposerDialogs/common/common.lg +++ b/Composer/packages/server/assets/projects/AskingQuestionsSample/ComposerDialogs/common/common.lg @@ -12,21 +12,8 @@ I can show you examples on how to use actions, You can enter number 01-07 # ShowImage(contentUrl, contentType) [HeroCard title = Here is the attachment - image = @{contentUrl} + image = ${contentUrl} ] -# ShowEmailSummary(user) -- IF: @{count(user.getGraphEmails.value) == 1} - - You have\s@{count(user.getGraphEmails.value)} email. This email is @{ShowEmail(user.getGraphEmails.value[0])}. -- ELSEIF: @{count(user.getGraphEmails.value) >= 2} - - You have @{count(user.getGraphEmails.value)} emails, the first email is @{ShowEmail(user.getGraphEmails.value[0])}. -- ELSEIF: @{count(user.getGraphEmails.value) == 0} - - You don't have any email. -- ELSE: - - You should not be here. - -# ShowEmail(email) -- @{email.subject} - # Welcome -Welcome to input samples. \ No newline at end of file diff --git a/Composer/packages/server/assets/projects/ControllingConversationFlowSample/ComposerDialogs/ForeachPageStep/ForeachPageStep.dialog b/Composer/packages/server/assets/projects/ControllingConversationFlowSample/ComposerDialogs/ForeachPageStep/ForeachPageStep.dialog index 4235347b4e..91c4a1483d 100644 --- a/Composer/packages/server/assets/projects/ControllingConversationFlowSample/ComposerDialogs/ForeachPageStep/ForeachPageStep.dialog +++ b/Composer/packages/server/assets/projects/ControllingConversationFlowSample/ComposerDialogs/ForeachPageStep/ForeachPageStep.dialog @@ -12,23 +12,6 @@ "id": "455902" }, "actions": [ - { - "$type": "Microsoft.IfCondition", - "$designer": { - "id": "400171" - }, - "condition": "dialog.ids == null", - "actions": [ - { - "$type": "Microsoft.InitProperty", - "$designer": { - "id": "879949" - }, - "property": "dialog.ids", - "type": "array" - } - ] - }, { "$type": "Microsoft.EditArray", "$designer": { @@ -36,7 +19,7 @@ }, "changeType": "Push", "itemsProperty": "dialog.ids", - "value": "10000+1000+100+10+1" + "value": "=10000+1000+100+10+1" }, { "$type": "Microsoft.EditArray", @@ -45,7 +28,7 @@ }, "changeType": "Push", "itemsProperty": "dialog.ids", - "value": "200*200" + "value": "=200*200" }, { "$type": "Microsoft.EditArray", @@ -54,14 +37,14 @@ }, "changeType": "Push", "itemsProperty": "dialog.ids", - "value": "888888/4" + "value": "=888888/4" }, { "$type": "Microsoft.SendActivity", "$designer": { "id": "623448" }, - "activity": "@{bfdactivity-623448()}" + "activity": "${bfdactivity-623448()}" }, { "$type": "Microsoft.ForeachPage", @@ -81,7 +64,7 @@ "id": "636747", "name": "Send a response" }, - "activity": "@{bfdactivity-636747()}" + "activity": "${bfdactivity-636747()}" } ] } diff --git a/Composer/packages/server/assets/projects/ControllingConversationFlowSample/ComposerDialogs/ForeachPageStep/ForeachPageStep.lg b/Composer/packages/server/assets/projects/ControllingConversationFlowSample/ComposerDialogs/ForeachPageStep/ForeachPageStep.lg index 1aab16f405..af1fbdfc1c 100644 --- a/Composer/packages/server/assets/projects/ControllingConversationFlowSample/ComposerDialogs/ForeachPageStep/ForeachPageStep.lg +++ b/Composer/packages/server/assets/projects/ControllingConversationFlowSample/ComposerDialogs/ForeachPageStep/ForeachPageStep.lg @@ -4,4 +4,4 @@ -Pushed dialog.ids into a list # bfdactivity-636747() -- @{dialog.foreach.index}: @{dialog.foreach.value} \ No newline at end of file +- ${dialog.foreach.index}: ${dialog.foreach.value} \ No newline at end of file diff --git a/Composer/packages/server/assets/projects/ControllingConversationFlowSample/ComposerDialogs/ForeachStep/ForeachStep.dialog b/Composer/packages/server/assets/projects/ControllingConversationFlowSample/ComposerDialogs/ForeachStep/ForeachStep.dialog index b4f7899ad4..513daed40c 100644 --- a/Composer/packages/server/assets/projects/ControllingConversationFlowSample/ComposerDialogs/ForeachStep/ForeachStep.dialog +++ b/Composer/packages/server/assets/projects/ControllingConversationFlowSample/ComposerDialogs/ForeachStep/ForeachStep.dialog @@ -12,23 +12,6 @@ "id": "614429" }, "actions": [ - { - "$type": "Microsoft.IfCondition", - "$designer": { - "id": "810905" - }, - "condition": "dialog.ids == null", - "actions": [ - { - "$type": "Microsoft.InitProperty", - "$designer": { - "id": "931303" - }, - "property": "dialog.ids", - "type": "array" - } - ] - }, { "$type": "Microsoft.EditArray", "$designer": { @@ -36,7 +19,7 @@ }, "changeType": "Push", "itemsProperty": "dialog.ids", - "value": "10000+1000+100+10+1" + "value": "=10000+1000+100+10+1" }, { "$type": "Microsoft.EditArray", @@ -45,7 +28,7 @@ }, "changeType": "Push", "itemsProperty": "dialog.ids", - "value": "200*200" + "value": "=200*200" }, { "$type": "Microsoft.EditArray", @@ -54,14 +37,14 @@ }, "changeType": "Push", "itemsProperty": "dialog.ids", - "value": "888888/4" + "value": "=888888/4" }, { "$type": "Microsoft.SendActivity", "$designer": { "id": "638869" }, - "activity": "@{bfdactivity-638869()}" + "activity": "${bfdactivity-638869()}" }, { "$type": "Microsoft.Foreach", @@ -76,7 +59,7 @@ "id": "006441", "name": "Send a response" }, - "activity": "@{bfdactivity-006441()}" + "activity": "${bfdactivity-006441()}" } ] } diff --git a/Composer/packages/server/assets/projects/ControllingConversationFlowSample/ComposerDialogs/ForeachStep/ForeachStep.lg b/Composer/packages/server/assets/projects/ControllingConversationFlowSample/ComposerDialogs/ForeachStep/ForeachStep.lg index 21d2bcba65..77783bef5d 100644 --- a/Composer/packages/server/assets/projects/ControllingConversationFlowSample/ComposerDialogs/ForeachStep/ForeachStep.lg +++ b/Composer/packages/server/assets/projects/ControllingConversationFlowSample/ComposerDialogs/ForeachStep/ForeachStep.lg @@ -4,4 +4,4 @@ -Pushed dialog.id into a list # bfdactivity-006441() -- @{dialog.foreach.index}: @{dialog.foreach.value} \ No newline at end of file +- ${dialog.foreach.index}: ${dialog.foreach.value} \ No newline at end of file diff --git a/Composer/packages/server/assets/projects/ControllingConversationFlowSample/ComposerDialogs/IfCondition/IfCondition.dialog b/Composer/packages/server/assets/projects/ControllingConversationFlowSample/ComposerDialogs/IfCondition/IfCondition.dialog index 7044c193c6..05901fc34f 100644 --- a/Composer/packages/server/assets/projects/ControllingConversationFlowSample/ComposerDialogs/IfCondition/IfCondition.dialog +++ b/Composer/packages/server/assets/projects/ControllingConversationFlowSample/ComposerDialogs/IfCondition/IfCondition.dialog @@ -36,7 +36,7 @@ "$designer": { "id": "164444" }, - "activity": "@{bfdactivity-164444()}" + "activity": "${bfdactivity-164444()}" } ], "elseActions": [ @@ -45,7 +45,7 @@ "$designer": { "id": "619321" }, - "activity": "@{bfdactivity-619321()}" + "activity": "${bfdactivity-619321()}" } ] } diff --git a/Composer/packages/server/assets/projects/ControllingConversationFlowSample/ComposerDialogs/IfCondition/IfCondition.lg b/Composer/packages/server/assets/projects/ControllingConversationFlowSample/ComposerDialogs/IfCondition/IfCondition.lg index 0409fc4fc3..7c15f706c6 100644 --- a/Composer/packages/server/assets/projects/ControllingConversationFlowSample/ComposerDialogs/IfCondition/IfCondition.lg +++ b/Composer/packages/server/assets/projects/ControllingConversationFlowSample/ComposerDialogs/IfCondition/IfCondition.lg @@ -1,7 +1,7 @@ [import](common.lg) # bfdactivity-164444() --Your age is @{user.age} which satisified the condition that was evaluated +-Your age is ${user.age} which satisified the condition that was evaluated # bfdactivity-619321() --Your age is @{user.age} which did not satisfy the condition that we evaluated \ No newline at end of file +-Your age is ${user.age} which did not satisfy the condition that we evaluated \ No newline at end of file diff --git a/Composer/packages/server/assets/projects/ControllingConversationFlowSample/ComposerDialogs/Main/Main.dialog b/Composer/packages/server/assets/projects/ControllingConversationFlowSample/ComposerDialogs/Main/Main.dialog index 005561040d..25373fe23c 100644 --- a/Composer/packages/server/assets/projects/ControllingConversationFlowSample/ComposerDialogs/Main/Main.dialog +++ b/Composer/packages/server/assets/projects/ControllingConversationFlowSample/ComposerDialogs/Main/Main.dialog @@ -10,37 +10,30 @@ "$type": "Microsoft.RegexRecognizer", "intents": [ { - "$type": "Microsoft.IntentPattern", "intent": "IfCondition", "pattern": "(?i)IfCondition|01" }, { - "$type": "Microsoft.IntentPattern", "intent": "SwitchCondition", "pattern": "SwitchCondition|02" }, { - "$type": "Microsoft.IntentPattern", "intent": "ForeachStep", "pattern": "ForeachStep|03" }, { - "$type": "Microsoft.IntentPattern", "intent": "ForeachPageStep", "pattern": "ForeachPageStep|04" }, { - "$type": "Microsoft.IntentPattern", "intent": "Cancel", "pattern": "Cancel|05" }, { - "$type": "Microsoft.IntentPattern", "intent": "EndTurn", "pattern": "EndTurn|06" }, { - "$type": "Microsoft.IntentPattern", "intent": "RepeatDialog", "pattern": "RepeatDialog|07" } @@ -163,7 +156,7 @@ "$designer": { "id": "953339" }, - "activity": "@{help()}" + "activity": "${help()}" } ] }, @@ -196,7 +189,7 @@ "id": "859266", "name": "Send a response" }, - "activity": "@{help()}" + "activity": "${help()}" } ] } diff --git a/Composer/packages/server/assets/projects/ControllingConversationFlowSample/ComposerDialogs/SwitchCondition/SwitchCondition.dialog b/Composer/packages/server/assets/projects/ControllingConversationFlowSample/ComposerDialogs/SwitchCondition/SwitchCondition.dialog index 5445e16a1c..07d5dee23e 100644 --- a/Composer/packages/server/assets/projects/ControllingConversationFlowSample/ComposerDialogs/SwitchCondition/SwitchCondition.dialog +++ b/Composer/packages/server/assets/projects/ControllingConversationFlowSample/ComposerDialogs/SwitchCondition/SwitchCondition.dialog @@ -53,7 +53,7 @@ "$designer": { "id": "097130" }, - "activity": "@{bfdactivity-097130()}" + "activity": "${bfdactivity-097130()}" }, { "$type": "Microsoft.SwitchCondition", @@ -70,7 +70,7 @@ "$designer": { "id": "040464" }, - "activity": "@{bfdactivity-040464()}" + "activity": "${bfdactivity-040464()}" } ] }, @@ -82,7 +82,7 @@ "$designer": { "id": "230206" }, - "activity": "@{bfdactivity-230206()}" + "activity": "${bfdactivity-230206()}" } ] }, @@ -94,7 +94,7 @@ "$designer": { "id": "604251" }, - "activity": "@{bfdactivity-604251()}" + "activity": "${bfdactivity-604251()}" } ] } diff --git a/Composer/packages/server/assets/projects/ControllingConversationFlowSample/ComposerDialogs/SwitchCondition/SwitchCondition.lg b/Composer/packages/server/assets/projects/ControllingConversationFlowSample/ComposerDialogs/SwitchCondition/SwitchCondition.lg index ec79f5ac82..ad6dd437ab 100644 --- a/Composer/packages/server/assets/projects/ControllingConversationFlowSample/ComposerDialogs/SwitchCondition/SwitchCondition.lg +++ b/Composer/packages/server/assets/projects/ControllingConversationFlowSample/ComposerDialogs/SwitchCondition/SwitchCondition.lg @@ -1,7 +1,7 @@ [import](common.lg) # bfdactivity-097130() --You selected @{user.name} +-You selected ${user.name} # bfdactivity-040464() -This is the logic inside the "Susan" switch block. diff --git a/Composer/packages/server/assets/projects/ControllingConversationFlowSample/ComposerDialogs/common/common.lg b/Composer/packages/server/assets/projects/ControllingConversationFlowSample/ComposerDialogs/common/common.lg index 8115e6baf9..b4e0b6dff8 100644 --- a/Composer/packages/server/assets/projects/ControllingConversationFlowSample/ComposerDialogs/common/common.lg +++ b/Composer/packages/server/assets/projects/ControllingConversationFlowSample/ComposerDialogs/common/common.lg @@ -1,6 +1,6 @@ # help [Activity - Text = @{helpText()} + Text = ${helpText()} SuggestedActions = IfCondition|SwitchCondition|ForeachStep|ForeachPageStep|Cancel|Endturn|RepeatDialog ] diff --git a/Composer/packages/server/assets/projects/EchoBot/ComposerDialogs/Main/Main.dialog b/Composer/packages/server/assets/projects/EchoBot/ComposerDialogs/Main/Main.dialog index fc69d23127..56a8ccd31e 100644 --- a/Composer/packages/server/assets/projects/EchoBot/ComposerDialogs/Main/Main.dialog +++ b/Composer/packages/server/assets/projects/EchoBot/ComposerDialogs/Main/Main.dialog @@ -18,7 +18,7 @@ "$designer": { "id": "003038" }, - "activity": "@{bfdactivity-003038()}" + "activity": "${bfdactivity-003038()}" } ] }, diff --git a/Composer/packages/server/assets/projects/EchoBot/ComposerDialogs/Main/Main.lg b/Composer/packages/server/assets/projects/EchoBot/ComposerDialogs/Main/Main.lg index 721ea81e0c..4d2da1b448 100644 --- a/Composer/packages/server/assets/projects/EchoBot/ComposerDialogs/Main/Main.lg +++ b/Composer/packages/server/assets/projects/EchoBot/ComposerDialogs/Main/Main.lg @@ -1,4 +1,4 @@ [import](common.lg) # bfdactivity-003038 -- You said '@{turn.activity.text}' \ No newline at end of file +- You said '${turn.activity.text}' \ No newline at end of file diff --git a/Composer/packages/server/assets/projects/InterruptionSample/ComposerDialogs/GetProfile/GetProfile.dialog b/Composer/packages/server/assets/projects/InterruptionSample/ComposerDialogs/GetProfile/GetProfile.dialog index d00668d6b8..01f408dda4 100644 --- a/Composer/packages/server/assets/projects/InterruptionSample/ComposerDialogs/GetProfile/GetProfile.dialog +++ b/Composer/packages/server/assets/projects/InterruptionSample/ComposerDialogs/GetProfile/GetProfile.dialog @@ -21,7 +21,7 @@ "id": "362298" }, "prompt": "What is your name? \\n \\[Suggestions=Why? | No name | Cancel | Reset profile\\]", - "invalidPrompt": "@{bfdinvalidPrompt-362298()}", + "invalidPrompt": "${bfdinvalidPrompt-362298()}", "maxTurnCount": 3, "validations": [ "length(this.value) <= 150", @@ -29,10 +29,10 @@ ], "property": "user.profile.name", "defaultValue": "'Human'", - "value": "@userName", + "value": "=@userName", "alwaysPrompt": false, "allowInterruptions": "true", - "outputFormat": "trim(this.value)" + "outputFormat": "=trim(this.value)" }, { "$type": "Microsoft.NumberInput", @@ -40,9 +40,9 @@ "name": "Prompt for a number", "id": "005947" }, - "prompt": "Hello {user.profile.name}, how old are you? \\n \\[Suggestions=Why? | Reset profile | Cancel | No age\\]", - "unrecognizedPrompt": "Hello {user.profile.name}, how old are you? \\n \\[Suggestions=Why? | Reset profile | Cancel | No age\\]", - "invalidPrompt": "@{bfdinvalidPrompt-005947()}", + "prompt": "Hello ${user.profile.name}, how old are you? \\n \\[Suggestions=Why? | Reset profile | Cancel | No age\\]", + "unrecognizedPrompt": "Hello ${user.profile.name}, how old are you? \\n \\[Suggestions=Why? | Reset profile | Cancel | No age\\]", + "invalidPrompt": "${bfdinvalidPrompt-005947()}", "maxTurnCount": 3, "validations": [ "int(this.value) >= 1", @@ -50,7 +50,7 @@ ], "property": "user.profile.age", "defaultValue": "30", - "value": "@userAge", + "value": "=@userAge", "alwaysPrompt": false, "allowInterruptions": "true", "defaultLocale": "en-us" @@ -61,7 +61,7 @@ "name": "Send an Activity", "id": "296924" }, - "activity": "@{bfdactivity-296924()}" + "activity": "${bfdactivity-296924()}" } ] }, @@ -86,7 +86,7 @@ "name": "Send an Activity", "id": "907674" }, - "activity": "@{bfdactivity-907674()}" + "activity": "${bfdactivity-907674()}" } ], "elseActions": [ @@ -96,7 +96,7 @@ "name": "Send an Activity", "id": "558329" }, - "activity": "@{bfdactivity-558329()}" + "activity": "${bfdactivity-558329()}" } ] } @@ -124,7 +124,7 @@ "name": "Send an Activity", "id": "074631" }, - "activity": "@{bfdactivity-074631()}" + "activity": "${bfdactivity-074631()}" }, { "$type": "Microsoft.SetProperty", @@ -143,7 +143,7 @@ "name": "Send an Activity", "id": "758791" }, - "activity": "@{bfdactivity-758791()}" + "activity": "${bfdactivity-758791()}" }, { "$type": "Microsoft.SetProperty", @@ -152,7 +152,7 @@ "id": "142109" }, "property": "user.profile.name", - "value": "'Human'" + "value": "Human" } ] } diff --git a/Composer/packages/server/assets/projects/InterruptionSample/ComposerDialogs/GetProfile/GetProfile.lg b/Composer/packages/server/assets/projects/InterruptionSample/ComposerDialogs/GetProfile/GetProfile.lg index 13245f20f6..ae999ec000 100644 --- a/Composer/packages/server/assets/projects/InterruptionSample/ComposerDialogs/GetProfile/GetProfile.lg +++ b/Composer/packages/server/assets/projects/InterruptionSample/ComposerDialogs/GetProfile/GetProfile.lg @@ -2,7 +2,7 @@ # bfdactivity-296924 [Activity - Text = Hello @{user.profile.name}, I have your age as @{user.profile.age}. + Text = Hello ${user.profile.name}, I have your age as ${user.profile.age}. SuggestedActions = Reset profile ] @@ -26,12 +26,12 @@ # bfdinvalidPrompt-362298 [Activity - Text = Sorry, '@{this.value}' does not work. I'm looking for 2-150 characters. What is your name? + Text = Sorry, '${this.value}' does not work. I'm looking for 2-150 characters. What is your name? SuggestedActions = Why? | No name | Cancel | Reset profile ] # bfdinvalidPrompt-005947 [Activity - Text = Sorry, @{this.value} does not work. I'm looking for a value between 1-150. What is your age? + Text = Sorry, ${this.value} does not work. I'm looking for a value between 1-150. What is your age? SuggestedActions = Why? | Reset profile | Cancel | No age ] \ No newline at end of file diff --git a/Composer/packages/server/assets/projects/InterruptionSample/ComposerDialogs/Main/Main.dialog b/Composer/packages/server/assets/projects/InterruptionSample/ComposerDialogs/Main/Main.dialog index 90154412e6..9de3377cf5 100644 --- a/Composer/packages/server/assets/projects/InterruptionSample/ComposerDialogs/Main/Main.dialog +++ b/Composer/packages/server/assets/projects/InterruptionSample/ComposerDialogs/Main/Main.dialog @@ -38,7 +38,7 @@ "id": "753396", "name": "Send a response" }, - "activity": "@{bfdactivity-753396()}" + "activity": "${bfdactivity-753396()}" } ] } @@ -94,7 +94,7 @@ "name": "Send an Activity", "id": "032735" }, - "activity": "@{bfdactivity-032735()}" + "activity": "${bfdactivity-032735()}" } ] } @@ -114,7 +114,7 @@ "name": "Send an Activity", "id": "650736" }, - "activity": "@{bfdactivity-650736()}" + "activity": "${bfdactivity-650736()}" }, { "$type": "Microsoft.CancelAllDialogs", @@ -139,7 +139,7 @@ "name": "Send an Activity", "id": "031899" }, - "activity": "@{bfdactivity-031899()}" + "activity": "${bfdactivity-031899()}" }, { "$type": "Microsoft.IfCondition", @@ -155,7 +155,7 @@ "name": "Send an Activity", "id": "309274" }, - "activity": "@{bfdactivity-309274()}" + "activity": "${bfdactivity-309274()}" } ], "elseActions": [ @@ -165,7 +165,7 @@ "name": "Send an Activity", "id": "912837" }, - "activity": "@{bfdactivity-912837()}" + "activity": "${bfdactivity-912837()}" } ] } @@ -185,7 +185,7 @@ "name": "Send an Activity", "id": "924700" }, - "activity": "@{bfdactivity-924700()}" + "activity": "${bfdactivity-924700()}" } ], "intent": "Help" diff --git a/Composer/packages/server/assets/projects/InterruptionSample/ComposerDialogs/Main/Main.lg b/Composer/packages/server/assets/projects/InterruptionSample/ComposerDialogs/Main/Main.lg index 91bada34e5..4381c94027 100644 --- a/Composer/packages/server/assets/projects/InterruptionSample/ComposerDialogs/Main/Main.lg +++ b/Composer/packages/server/assets/projects/InterruptionSample/ComposerDialogs/Main/Main.lg @@ -13,13 +13,13 @@ - Sure, I've cancelled that. # bfdactivity-031899 -- @{user.profile.name} +- ${user.profile.name} # bfdactivity-309274 - ``` Here's what I know about you - -- @{NameReadBack()} -- @{AgeReadBack()} +- ${NameReadBack()} +- ${AgeReadBack()} ``` # bfdactivity-912837 diff --git a/Composer/packages/server/assets/projects/InterruptionSample/ComposerDialogs/common/common.lg b/Composer/packages/server/assets/projects/InterruptionSample/ComposerDialogs/common/common.lg index 5e8c80303a..bfd92b0b03 100644 --- a/Composer/packages/server/assets/projects/InterruptionSample/ComposerDialogs/common/common.lg +++ b/Composer/packages/server/assets/projects/InterruptionSample/ComposerDialogs/common/common.lg @@ -1,11 +1,11 @@ # NameReadBack -- IF : @{exists(user.profile.name)} - - Name : @{user.profile.name} +- IF : ${exists(user.profile.name)} + - Name : ${user.profile.name} - ELSE : - Name : unknown # AgeReadBack -- IF : @{exists(user.profile.age)} - - Age : @{user.profile.age} +- IF : ${exists(user.profile.age)} + - Age : ${user.profile.age} - ELSE: - Age : unknown \ No newline at end of file diff --git a/Composer/packages/server/assets/projects/QnAMakerLUISSample/ComposerDialogs/Main/Main.dialog b/Composer/packages/server/assets/projects/QnAMakerLUISSample/ComposerDialogs/Main/Main.dialog index e7d21e4fa6..b15e87264a 100644 --- a/Composer/packages/server/assets/projects/QnAMakerLUISSample/ComposerDialogs/Main/Main.dialog +++ b/Composer/packages/server/assets/projects/QnAMakerLUISSample/ComposerDialogs/Main/Main.dialog @@ -36,7 +36,7 @@ "id": "266608", "name": "Send a response" }, - "activity": "@{bfdactivity-266608()}" + "activity": "${bfdactivity-266608()}" } ] } @@ -57,7 +57,7 @@ "id": "771838", "name": "Send a response" }, - "activity": "@{bfdactivity-771838()}" + "activity": "${bfdactivity-771838()}" } ], "intent": "Help" @@ -74,9 +74,9 @@ "id": "284337", "name": "Connect to QnA Knowledgebase" }, - "knowledgeBaseId": "settings.qna.knowledgebaseid", - "endpointKey": "settings.qna.endpointkey", - "hostname": "settings.qna.hostname", + "knowledgeBaseId": "=settings.qna.knowledgebaseid", + "endpointKey": "=settings.qna.endpointkey", + "hostname": "=settings.qna.hostname", "noAnswer": "Sorry, I did not find an answer.", "threshold": 0.3, "activeLearningCardTitle": "Did you mean:", @@ -98,7 +98,7 @@ "id": "313066", "name": "Send a response" }, - "activity": "@{bfdactivity-313066()}" + "activity": "${bfdactivity-313066()}" } ], "intent": "BuySurface" diff --git a/Composer/packages/server/assets/projects/RespondingWithCardsSample/ComposerDialogs/Main/Main.dialog b/Composer/packages/server/assets/projects/RespondingWithCardsSample/ComposerDialogs/Main/Main.dialog index b9e698a369..e1d0914c94 100644 --- a/Composer/packages/server/assets/projects/RespondingWithCardsSample/ComposerDialogs/Main/Main.dialog +++ b/Composer/packages/server/assets/projects/RespondingWithCardsSample/ComposerDialogs/Main/Main.dialog @@ -80,7 +80,7 @@ "name": "Send an Activity", "id": "159442" }, - "activity": "@{bfdactivity-159442()}" + "activity": "${bfdactivity-159442()}" } ] }, @@ -93,7 +93,7 @@ "id": "735465", "name": "Text input" }, - "prompt": "@{bfdprompt-735465()}", + "prompt": "${bfdprompt-735465()}", "maxTurnCount": 3, "property": "user.name", "alwaysPrompt": false, @@ -105,7 +105,7 @@ "id": "167246", "name": "Send an Activity" }, - "activity": "@{bfdactivity-167246()}" + "activity": "${bfdactivity-167246()}" } ] }, @@ -118,7 +118,7 @@ "name": "Send an Activity", "id": "762914" }, - "activity": "@{bfdactivity-762914()}" + "activity": "${bfdactivity-762914()}" } ] }, @@ -131,7 +131,7 @@ "name": "Send an Activity", "id": "241579" }, - "activity": "@{bfdactivity-241579()}" + "activity": "${bfdactivity-241579()}" } ] }, @@ -144,7 +144,7 @@ "name": "Send an Activity", "id": "901582" }, - "activity": "@{bfdactivity-901582()}" + "activity": "${bfdactivity-901582()}" } ] }, @@ -157,7 +157,7 @@ "name": "Send an Activity", "id": "553859" }, - "activity": "@{bfdactivity-553859()}" + "activity": "${bfdactivity-553859()}" } ] }, @@ -170,7 +170,7 @@ "name": "Send an Activity", "id": "190928" }, - "activity": "@{bfdactivity-190928()}" + "activity": "${bfdactivity-190928()}" } ] }, @@ -183,7 +183,7 @@ "name": "Send an Activity", "id": "806895" }, - "activity": "@{bfdactivity-806895()}" + "activity": "${bfdactivity-806895()}" } ] }, @@ -196,7 +196,7 @@ "name": "Send an Activity", "id": "997450" }, - "activity": "@{bfdactivity-997450()}" + "activity": "${bfdactivity-997450()}" } ] } @@ -238,7 +238,7 @@ "id": "729500", "name": "Send a response" }, - "activity": "@{bfdactivity-729500()}" + "activity": "${bfdactivity-729500()}" } ] } diff --git a/Composer/packages/server/assets/projects/RespondingWithCardsSample/ComposerDialogs/Main/Main.lg b/Composer/packages/server/assets/projects/RespondingWithCardsSample/ComposerDialogs/Main/Main.lg index a4376940b1..22a4c754d1 100644 --- a/Composer/packages/server/assets/projects/RespondingWithCardsSample/ComposerDialogs/Main/Main.lg +++ b/Composer/packages/server/assets/projects/RespondingWithCardsSample/ComposerDialogs/Main/Main.lg @@ -1,34 +1,34 @@ [import](common.lg) # bfdactivity-159442 --@{HeroCard()} +-${HeroCard()} # bfdprompt-735465 - What is your name? # bfdactivity-167246 -- @{HeroCardWithMemory(user.name)} +- ${HeroCardWithMemory(user.name)} # bfdactivity-762914 --@{ThumbnailCard()} +-${ThumbnailCard()} # bfdactivity-241579 --@{SigninCard()} +-${SigninCard()} # bfdactivity-901582 --@{AnimationCard()} +-${AnimationCard()} # bfdactivity-553859 --@{VideoCard()} +-${VideoCard()} # bfdactivity-190928 --@{AudioCard()} +-${AudioCard()} # bfdactivity-806895 --@{AdaptiveCard()} +-${AdaptiveCard()} # bfdactivity-997450 --@{AllCards()} +-${AllCards()} # bfdactivity-729500 -Welcome to Card Samples Bot. \ No newline at end of file diff --git a/Composer/packages/server/assets/projects/RespondingWithCardsSample/ComposerDialogs/common/adaptiveCard.json b/Composer/packages/server/assets/projects/RespondingWithCardsSample/ComposerDialogs/common/adaptiveCard.json index 2fd3201b24..1949095870 100644 --- a/Composer/packages/server/assets/projects/RespondingWithCardsSample/ComposerDialogs/common/adaptiveCard.json +++ b/Composer/packages/server/assets/projects/RespondingWithCardsSample/ComposerDialogs/common/adaptiveCard.json @@ -15,17 +15,17 @@ }, { "type": "TextBlock", - "text": "@{PassengerName()}", + "text": "${PassengerName()}", "separator": true }, { "type": "TextBlock", - "text": "@{PassengerName()}", + "text": "${PassengerName()}", "spacing": "none" }, { "type": "TextBlock", - "text": "@{PassengerName()}", + "text": "${PassengerName()}", "spacing": "none" }, { diff --git a/Composer/packages/server/assets/projects/RespondingWithCardsSample/ComposerDialogs/common/common.lg b/Composer/packages/server/assets/projects/RespondingWithCardsSample/ComposerDialogs/common/common.lg index 959de5f1ac..39a3d70912 100644 --- a/Composer/packages/server/assets/projects/RespondingWithCardsSample/ComposerDialogs/common/common.lg +++ b/Composer/packages/server/assets/projects/RespondingWithCardsSample/ComposerDialogs/common/common.lg @@ -1,8 +1,8 @@ > All cards can be defined and managed through .lg files. > All cards use the chatdown notation - see here - https://github.com/Microsoft/botbuilder-tools/tree/master/packages/Chatdown#message-commands > Multi-line text are enclosed in ``` -> Multi-line text can include inline expressions enclosed in @{expression}. -> @{TemplateName()} is an inline expression that uses the lgTemplate pre-built function to evaluate a template by name. +> Multi-line text can include inline expressions enclosed in ${expression}. +> ${TemplateName()} is an inline expression that uses the lgTemplate pre-built function to evaluate a template by name. # HeroCard [HeroCard @@ -10,20 +10,20 @@ subtitle = Microsoft Bot Framework text = Build and connect intelligent bots to interact with your users naturally wherever they are, from text/sms to Skype, Slack, Office 365 mail and other popular services. image = https://sec.ch9.ms/ch9/7ff5/e07cfef0-aa3b-40bb-9baa-7c9ef8ff7ff5/buildreactionbotframework_960.jpg - buttons = @{cardActionTemplate('imBack', 'Show more cards', 'Show more cards')} + buttons = ${cardActionTemplate('imBack', 'Show more cards', 'Show more cards')} ] # HeroCardWithMemory(name) [Herocard - title=@{TitleText(name)} - subtitle=@{SubText()} - text=@{DescriptionText()} - images=@{CardImages()} - buttons=@{cardActionTemplate('imBack', 'Show more cards', 'Show more cards')} + title=${TitleText(name)} + subtitle=${SubText()} + text=${DescriptionText()} + images=${CardImages()} + buttons=${cardActionTemplate('imBack', 'Show more cards', 'Show more cards')} ] # TitleText(name) -- Hello, @{name} +- Hello, ${name} # SubText - What is your favorite? @@ -40,10 +40,10 @@ # cardActionTemplate(type, title, value) [CardAction - Type = @{if(type == null, 'imBack', type)} - Title = @{title} - Value = @{value} - Text = @{title} + Type = ${if(type == null, 'imBack', type)} + Title = ${title} + Value = ${value} + Text = ${title} ] # ThumbnailCard @@ -58,7 +58,7 @@ # SigninCard [SigninCard text = BotFramework Sign-in Card - buttons = @{cardActionTemplate('signin', 'Sign-in', 'https://login.microsoftonline.com/')} + buttons = ${cardActionTemplate('signin', 'Sign-in', 'https://login.microsoftonline.com/')} ] # AnimationCard @@ -96,7 +96,7 @@ # AdaptiveCard [Activity - Attachments = @{json(adaptivecardjson())} + Attachments = ${json(adaptivecardjson())} ] @@ -112,8 +112,8 @@ # AllCards [Activity - Attachments = @{HeroCard()} | @{ThumbnailCard()} | @{SigninCard()} | @{AnimationCard()} | @{VideoCard()} | @{AudioCard()} | @{AdaptiveCard()} - AttachmentLayout = @{AttachmentLayoutType()} + Attachments = ${HeroCard()} | ${ThumbnailCard()} | ${SigninCard()} | ${AnimationCard()} | ${VideoCard()} | ${AudioCard()} | ${AdaptiveCard()} + AttachmentLayout = ${AttachmentLayoutType()} ] @@ -148,17 +148,17 @@ }, { "type": "TextBlock", - "text": "@{PassengerName()}", + "text": "${PassengerName()}", "separator": true }, { "type": "TextBlock", - "text": "@{PassengerName()}", + "text": "${PassengerName()}", "spacing": "none" }, { "type": "TextBlock", - "text": "@{PassengerName()}", + "text": "${PassengerName()}", "spacing": "none" }, { diff --git a/Composer/packages/server/assets/projects/RespondingWithTextSample/ComposerDialogs/IfElseCondition/IfElseCondition.dialog b/Composer/packages/server/assets/projects/RespondingWithTextSample/ComposerDialogs/IfElseCondition/IfElseCondition.dialog index af3cc70ba6..93126591d5 100644 --- a/Composer/packages/server/assets/projects/RespondingWithTextSample/ComposerDialogs/IfElseCondition/IfElseCondition.dialog +++ b/Composer/packages/server/assets/projects/RespondingWithTextSample/ComposerDialogs/IfElseCondition/IfElseCondition.dialog @@ -20,7 +20,7 @@ "id": "383595", "name": "Multiple choice" }, - "prompt": "@{bfdprompt-383595()}", + "prompt": "${bfdprompt-383595()}", "maxTurnCount": 3, "property": "user.timeofday", "alwaysPrompt": false, @@ -56,7 +56,7 @@ "id": "749181", "name": "Send a response" }, - "activity": "@{bfdactivity-749181()}" + "activity": "${bfdactivity-749181()}" } ] } diff --git a/Composer/packages/server/assets/projects/RespondingWithTextSample/ComposerDialogs/IfElseCondition/IfElseCondition.lg b/Composer/packages/server/assets/projects/RespondingWithTextSample/ComposerDialogs/IfElseCondition/IfElseCondition.lg index bb05f073a0..0a88fbd0cc 100644 --- a/Composer/packages/server/assets/projects/RespondingWithTextSample/ComposerDialogs/IfElseCondition/IfElseCondition.lg +++ b/Composer/packages/server/assets/projects/RespondingWithTextSample/ComposerDialogs/IfElseCondition/IfElseCondition.lg @@ -4,4 +4,4 @@ - what the time of day # bfdactivity-749181 -- @{timeOfDayGreeting(user.timeofday)} \ No newline at end of file +- ${timeOfDayGreeting(user.timeofday)} \ No newline at end of file diff --git a/Composer/packages/server/assets/projects/RespondingWithTextSample/ComposerDialogs/LGComposition/LGComposition.dialog b/Composer/packages/server/assets/projects/RespondingWithTextSample/ComposerDialogs/LGComposition/LGComposition.dialog index 4b68fe3cdc..efc6e98243 100644 --- a/Composer/packages/server/assets/projects/RespondingWithTextSample/ComposerDialogs/LGComposition/LGComposition.dialog +++ b/Composer/packages/server/assets/projects/RespondingWithTextSample/ComposerDialogs/LGComposition/LGComposition.dialog @@ -28,7 +28,7 @@ "$designer": { "id": "823322" }, - "activity": "@{bfdactivity-823322()}" + "activity": "${bfdactivity-823322()}" } ] } diff --git a/Composer/packages/server/assets/projects/RespondingWithTextSample/ComposerDialogs/LGComposition/LGComposition.lg b/Composer/packages/server/assets/projects/RespondingWithTextSample/ComposerDialogs/LGComposition/LGComposition.lg index 859aae0a0e..6771020b83 100644 --- a/Composer/packages/server/assets/projects/RespondingWithTextSample/ComposerDialogs/LGComposition/LGComposition.lg +++ b/Composer/packages/server/assets/projects/RespondingWithTextSample/ComposerDialogs/LGComposition/LGComposition.lg @@ -1,4 +1,4 @@ [import](common.lg) # bfdactivity-823322 --@{LGComposition(user)} \ No newline at end of file +-${LGComposition(user)} \ No newline at end of file diff --git a/Composer/packages/server/assets/projects/RespondingWithTextSample/ComposerDialogs/LGWithParam/LGWithParam.dialog b/Composer/packages/server/assets/projects/RespondingWithTextSample/ComposerDialogs/LGWithParam/LGWithParam.dialog index 29748e15f8..b31b4e5deb 100644 --- a/Composer/packages/server/assets/projects/RespondingWithTextSample/ComposerDialogs/LGWithParam/LGWithParam.dialog +++ b/Composer/packages/server/assets/projects/RespondingWithTextSample/ComposerDialogs/LGWithParam/LGWithParam.dialog @@ -28,7 +28,7 @@ "$designer": { "id": "176070" }, - "activity": "@{bfdactivity-176070()}" + "activity": "${bfdactivity-176070()}" } ] } diff --git a/Composer/packages/server/assets/projects/RespondingWithTextSample/ComposerDialogs/LGWithParam/LGWithParam.lg b/Composer/packages/server/assets/projects/RespondingWithTextSample/ComposerDialogs/LGWithParam/LGWithParam.lg index 927c34108f..ba8ee42fc4 100644 --- a/Composer/packages/server/assets/projects/RespondingWithTextSample/ComposerDialogs/LGWithParam/LGWithParam.lg +++ b/Composer/packages/server/assets/projects/RespondingWithTextSample/ComposerDialogs/LGWithParam/LGWithParam.lg @@ -1,4 +1,4 @@ [import](common.lg) # bfdactivity-176070 --@{LGWithParam(user)} \ No newline at end of file +-${LGWithParam(user)} \ No newline at end of file diff --git a/Composer/packages/server/assets/projects/RespondingWithTextSample/ComposerDialogs/Main/Main.dialog b/Composer/packages/server/assets/projects/RespondingWithTextSample/ComposerDialogs/Main/Main.dialog index 7d2907cbff..2565a6460f 100644 --- a/Composer/packages/server/assets/projects/RespondingWithTextSample/ComposerDialogs/Main/Main.dialog +++ b/Composer/packages/server/assets/projects/RespondingWithTextSample/ComposerDialogs/Main/Main.dialog @@ -17,7 +17,7 @@ "$designer": { "id": "551445" }, - "activity": "@{bfdactivity-551445()}" + "activity": "${bfdactivity-551445()}" } ] }, @@ -50,7 +50,7 @@ "id": "576166", "name": "Send a response" }, - "activity": "@{bfdactivity-576166()}" + "activity": "${bfdactivity-576166()}" } ] } diff --git a/Composer/packages/server/assets/projects/RespondingWithTextSample/ComposerDialogs/Main/Main.lg b/Composer/packages/server/assets/projects/RespondingWithTextSample/ComposerDialogs/Main/Main.lg index 8348aafffc..3ec57ec62c 100644 --- a/Composer/packages/server/assets/projects/RespondingWithTextSample/ComposerDialogs/Main/Main.lg +++ b/Composer/packages/server/assets/projects/RespondingWithTextSample/ComposerDialogs/Main/Main.lg @@ -1,7 +1,7 @@ [import](common.lg) # bfdactivity-551445 --@{help()} +-${help()} # bfdactivity-576166 -Welcome to the Message Samples. You can use this sample to explore different capabilities of sending messages to users. \ No newline at end of file diff --git a/Composer/packages/server/assets/projects/RespondingWithTextSample/ComposerDialogs/MultiLineText/MultiLineText.dialog b/Composer/packages/server/assets/projects/RespondingWithTextSample/ComposerDialogs/MultiLineText/MultiLineText.dialog index 362f78e4b2..07781fb615 100644 --- a/Composer/packages/server/assets/projects/RespondingWithTextSample/ComposerDialogs/MultiLineText/MultiLineText.dialog +++ b/Composer/packages/server/assets/projects/RespondingWithTextSample/ComposerDialogs/MultiLineText/MultiLineText.dialog @@ -20,7 +20,7 @@ "id": "458516", "name": "Send a response" }, - "activity": "@{bfdactivity-458516()}" + "activity": "${bfdactivity-458516()}" } ] } diff --git a/Composer/packages/server/assets/projects/RespondingWithTextSample/ComposerDialogs/MultiLineText/MultiLineText.lg b/Composer/packages/server/assets/projects/RespondingWithTextSample/ComposerDialogs/MultiLineText/MultiLineText.lg index a5f1fb730b..837c499f7e 100644 --- a/Composer/packages/server/assets/projects/RespondingWithTextSample/ComposerDialogs/MultiLineText/MultiLineText.lg +++ b/Composer/packages/server/assets/projects/RespondingWithTextSample/ComposerDialogs/MultiLineText/MultiLineText.lg @@ -1,4 +1,4 @@ [import](common.lg) # bfdactivity-458516 -- @{multilineText()} \ No newline at end of file +- ${multilineText()} \ No newline at end of file diff --git a/Composer/packages/server/assets/projects/RespondingWithTextSample/ComposerDialogs/SimpleText/SimpleText.dialog b/Composer/packages/server/assets/projects/RespondingWithTextSample/ComposerDialogs/SimpleText/SimpleText.dialog index afa7a2596b..c85078b6c0 100644 --- a/Composer/packages/server/assets/projects/RespondingWithTextSample/ComposerDialogs/SimpleText/SimpleText.dialog +++ b/Composer/packages/server/assets/projects/RespondingWithTextSample/ComposerDialogs/SimpleText/SimpleText.dialog @@ -17,7 +17,7 @@ "$designer": { "id": "219943" }, - "activity": "@{bfdactivity-219943()}" + "activity": "${bfdactivity-219943()}" } ] } diff --git a/Composer/packages/server/assets/projects/RespondingWithTextSample/ComposerDialogs/SimpleText/SimpleText.lg b/Composer/packages/server/assets/projects/RespondingWithTextSample/ComposerDialogs/SimpleText/SimpleText.lg index 2561584cac..11945a3e5d 100644 --- a/Composer/packages/server/assets/projects/RespondingWithTextSample/ComposerDialogs/SimpleText/SimpleText.lg +++ b/Composer/packages/server/assets/projects/RespondingWithTextSample/ComposerDialogs/SimpleText/SimpleText.lg @@ -1,4 +1,4 @@ [import](common.lg) # bfdactivity-219943 --@{SimpleText()} \ No newline at end of file +-${SimpleText()} \ No newline at end of file diff --git a/Composer/packages/server/assets/projects/RespondingWithTextSample/ComposerDialogs/StructuredLG/StructuredLG.dialog b/Composer/packages/server/assets/projects/RespondingWithTextSample/ComposerDialogs/StructuredLG/StructuredLG.dialog index f49ec2cc0e..343eecb04e 100644 --- a/Composer/packages/server/assets/projects/RespondingWithTextSample/ComposerDialogs/StructuredLG/StructuredLG.dialog +++ b/Composer/packages/server/assets/projects/RespondingWithTextSample/ComposerDialogs/StructuredLG/StructuredLG.dialog @@ -20,7 +20,7 @@ "id": "862531", "name": "Send a response" }, - "activity": "@{bfdactivity-862531()}" + "activity": "${bfdactivity-862531()}" } ] } diff --git a/Composer/packages/server/assets/projects/RespondingWithTextSample/ComposerDialogs/StructuredLG/StructuredLG.lg b/Composer/packages/server/assets/projects/RespondingWithTextSample/ComposerDialogs/StructuredLG/StructuredLG.lg index 873af082ee..1915dae7b2 100644 --- a/Composer/packages/server/assets/projects/RespondingWithTextSample/ComposerDialogs/StructuredLG/StructuredLG.lg +++ b/Composer/packages/server/assets/projects/RespondingWithTextSample/ComposerDialogs/StructuredLG/StructuredLG.lg @@ -1,4 +1,4 @@ [import](common.lg) # bfdactivity-862531 -- @{StructuredText()} \ No newline at end of file +- ${StructuredText()} \ No newline at end of file diff --git a/Composer/packages/server/assets/projects/RespondingWithTextSample/ComposerDialogs/SwitchCondition/SwitchCondition.dialog b/Composer/packages/server/assets/projects/RespondingWithTextSample/ComposerDialogs/SwitchCondition/SwitchCondition.dialog index 8a9ab15b22..9683a81166 100644 --- a/Composer/packages/server/assets/projects/RespondingWithTextSample/ComposerDialogs/SwitchCondition/SwitchCondition.dialog +++ b/Composer/packages/server/assets/projects/RespondingWithTextSample/ComposerDialogs/SwitchCondition/SwitchCondition.dialog @@ -20,7 +20,7 @@ "id": "958316", "name": "Send a response" }, - "activity": "@{bfdactivity-958316()}" + "activity": "${bfdactivity-958316()}" } ] } diff --git a/Composer/packages/server/assets/projects/RespondingWithTextSample/ComposerDialogs/SwitchCondition/SwitchCondition.lg b/Composer/packages/server/assets/projects/RespondingWithTextSample/ComposerDialogs/SwitchCondition/SwitchCondition.lg index 6cc4687d3c..d96dc948c0 100644 --- a/Composer/packages/server/assets/projects/RespondingWithTextSample/ComposerDialogs/SwitchCondition/SwitchCondition.lg +++ b/Composer/packages/server/assets/projects/RespondingWithTextSample/ComposerDialogs/SwitchCondition/SwitchCondition.lg @@ -1,4 +1,4 @@ [import](common.lg) # bfdactivity-958316 -- @{greetInAWeek()} \ No newline at end of file +- ${greetInAWeek()} \ No newline at end of file diff --git a/Composer/packages/server/assets/projects/RespondingWithTextSample/ComposerDialogs/TextWithMemory/TextWithMemory.dialog b/Composer/packages/server/assets/projects/RespondingWithTextSample/ComposerDialogs/TextWithMemory/TextWithMemory.dialog index 82c5d4d950..a5f5a9a73a 100644 --- a/Composer/packages/server/assets/projects/RespondingWithTextSample/ComposerDialogs/TextWithMemory/TextWithMemory.dialog +++ b/Composer/packages/server/assets/projects/RespondingWithTextSample/ComposerDialogs/TextWithMemory/TextWithMemory.dialog @@ -18,14 +18,14 @@ "id": "765039" }, "property": "user.message", - "value": "'This is a text saved in memory.'" + "value": "This is a text saved in memory." }, { "$type": "Microsoft.SendActivity", "$designer": { "id": "822060" }, - "activity": "@{bfdactivity-822060()}" + "activity": "${bfdactivity-822060()}" } ] } diff --git a/Composer/packages/server/assets/projects/RespondingWithTextSample/ComposerDialogs/TextWithMemory/TextWithMemory.lg b/Composer/packages/server/assets/projects/RespondingWithTextSample/ComposerDialogs/TextWithMemory/TextWithMemory.lg index 51ef818258..764388935e 100644 --- a/Composer/packages/server/assets/projects/RespondingWithTextSample/ComposerDialogs/TextWithMemory/TextWithMemory.lg +++ b/Composer/packages/server/assets/projects/RespondingWithTextSample/ComposerDialogs/TextWithMemory/TextWithMemory.lg @@ -1,4 +1,4 @@ [import](common.lg) # bfdactivity-822060 --@{user.message} \ No newline at end of file +-${user.message} \ No newline at end of file diff --git a/Composer/packages/server/assets/projects/RespondingWithTextSample/ComposerDialogs/common/common.lg b/Composer/packages/server/assets/projects/RespondingWithTextSample/ComposerDialogs/common/common.lg index c050afc1c7..e871b7d453 100644 --- a/Composer/packages/server/assets/projects/RespondingWithTextSample/ComposerDialogs/common/common.lg +++ b/Composer/packages/server/assets/projects/RespondingWithTextSample/ComposerDialogs/common/common.lg @@ -2,10 +2,10 @@ - nice to talk to you! # LGComposition(user) -- @{user.name} @{Greeting()} +- ${user.name} ${Greeting()} # LGWithParam(user) -- Hello @{user.name}, nice to talk to you! +- Hello ${user.name}, nice to talk to you! # SimpleText - Hi, this is simple text @@ -18,18 +18,18 @@ ``` # greetInAWeek -- SWITCH: @{dayOfWeek(utcNow())} -- CASE: @{0} +- SWITCH: ${dayOfWeek(utcNow())} +- CASE: ${0} - Happy Sunday! --CASE: @{6} +-CASE: ${6} - Happy Saturday! -DEFAULT: - Working day! # timeOfDayGreeting(timeOfDay) -- IF: @{timeOfDay == 'morning'} +- IF: ${timeOfDay == 'morning'} - good morning -- ELSEIF: @{timeOfDay == 'afternoon'} +- ELSEIF: ${timeOfDay == 'afternoon'} - good afternoon - ELSE: - good evening diff --git a/Composer/packages/server/assets/projects/ToDoBotWithLuisSample/ComposerDialogs/Main/Main.dialog b/Composer/packages/server/assets/projects/ToDoBotWithLuisSample/ComposerDialogs/Main/Main.dialog index 47bfb7e19e..d760ed5083 100644 --- a/Composer/packages/server/assets/projects/ToDoBotWithLuisSample/ComposerDialogs/Main/Main.dialog +++ b/Composer/packages/server/assets/projects/ToDoBotWithLuisSample/ComposerDialogs/Main/Main.dialog @@ -37,47 +37,11 @@ "id": "037171", "name": "Send a response" }, - "activity": "@{bfdactivity-037171()}" + "activity": "${bfdactivity-037171()}" } ] } ] - }, - { - "$type": "Microsoft.InitProperty", - "$designer": { - "id": "200509", - "name": "Initialize a Property" - }, - "property": "user.taskLists", - "type": "object" - }, - { - "$type": "Microsoft.InitProperty", - "$designer": { - "id": "192864", - "name": "Initialize a Property" - }, - "property": "user.taskLists.grocery", - "type": "array" - }, - { - "$type": "Microsoft.InitProperty", - "$designer": { - "id": "905432", - "name": "Initialize a Property" - }, - "property": "user.taskLists.shopping", - "type": "array" - }, - { - "$type": "Microsoft.InitProperty", - "$designer": { - "id": "773009", - "name": "Initialize a Property" - }, - "property": "user.taskLists.todo", - "type": "array" } ] }, @@ -94,7 +58,7 @@ "id": "113335", "name": "Send a response" }, - "activity": "@{bfdactivity-113335()}" + "activity": "${bfdactivity-113335()}" } ], "intent": "Help" @@ -112,7 +76,7 @@ "id": "183920", "name": "Confirmation" }, - "prompt": "@{bfdprompt-183920()}", + "prompt": "${bfdprompt-183920()}", "maxTurnCount": 3, "property": "turn.cancelConfirmation", "defaultValue": "false", @@ -141,7 +105,7 @@ "id": "304360", "name": "Send a response" }, - "activity": "@{bfdactivity-304360()}" + "activity": "${bfdactivity-304360()}" }, { "$type": "Microsoft.CancelAllDialogs", @@ -158,7 +122,7 @@ "id": "470975", "name": "Send a response" }, - "activity": "@{bfdactivity-470975()}" + "activity": "${bfdactivity-470975()}" } ] } @@ -178,7 +142,7 @@ "id": "456012", "name": "Send a response" }, - "activity": "@{bfdactivity-456012()}" + "activity": "${bfdactivity-456012()}" } ], "intent": "Greeting" @@ -195,12 +159,12 @@ "id": "069270", "name": "Text input" }, - "prompt": "@{bfdprompt-069270()}", + "prompt": "${bfdprompt-069270()}", "maxTurnCount": 3, "property": "dialog.item", "alwaysPrompt": false, "allowInterruptions": "turn.recognized.score > 0.7 && !#AddItem && !@itemTitle", - "value": "@itemTitle" + "value": "=@itemTitle" }, { "$type": "Microsoft.IfCondition", @@ -216,7 +180,7 @@ "id": "781953", "name": "Multiple choice" }, - "prompt": "@{bfdprompt-781953()}", + "prompt": "${bfdprompt-781953()}", "maxTurnCount": 3, "property": "dialog.listType", "alwaysPrompt": false, @@ -245,7 +209,7 @@ "recognizerOptions": { "noValue": false }, - "value": "@listType" + "value": "=@listType" } ] }, @@ -257,7 +221,7 @@ }, "changeType": "Push", "itemsProperty": "user.taskLists[dialog.listType]", - "value": "dialog.item" + "value": "=dialog.item" }, { "$type": "Microsoft.SendActivity", @@ -265,7 +229,7 @@ "id": "775572", "name": "Send a response" }, - "activity": "@{bfdactivity-775572()}" + "activity": "${bfdactivity-775572()}" }, { "$type": "Microsoft.DeleteProperty", @@ -306,13 +270,13 @@ "id": "637111", "name": "Multiple choice" }, - "prompt": "@{bfdprompt-637111()}", + "prompt": "${bfdprompt-637111()}", "maxTurnCount": 3, "property": "dialog.listType", "alwaysPrompt": false, "allowInterruptions": "turn.recognized.score > 0.7 && !#DeleteItem && !@listType", "outputFormat": "value", - "value": "@listType", + "value": "=@listType", "choices": [ { "value": "Shopping" @@ -353,7 +317,7 @@ "id": "684852", "name": "Send a response" }, - "activity": "@{bfdactivity-212971()}" + "activity": "${bfdactivity-212971()}" }, { "$type": "Microsoft.TextInput", @@ -361,12 +325,12 @@ "id": "038757", "name": "Text input" }, - "prompt": "@{bfdprompt-879447()}", + "prompt": "${bfdprompt-879447()}", "maxTurnCount": 3, "property": "dialog.item", "alwaysPrompt": false, "allowInterruptions": "turn.recognized.score > 0.7 && !#DeleteItem && !@itemTitle", - "value": "@itemTitle" + "value": "=@itemTitle" }, { "$type": "Microsoft.EditArray", @@ -377,7 +341,7 @@ "changeType": "Remove", "itemsProperty": "user.taskLists[dialog.listType]", "resultProperty": "turn.itemDeleted", - "value": "dialog.item" + "value": "=dialog.item" }, { "$type": "Microsoft.SendActivity", @@ -385,7 +349,7 @@ "id": "593087", "name": "Send a response" }, - "activity": "@{bfdactivity-056974()}" + "activity": "${bfdactivity-056974()}" } ], "elseActions": [ @@ -395,7 +359,7 @@ "id": "253146", "name": "Send a response" }, - "activity": "@{bfdactivity-084161()}" + "activity": "${bfdactivity-084161()}" } ] }, @@ -438,13 +402,13 @@ "id": "059540", "name": "Multiple choice" }, - "prompt": "@{bfdprompt-059540()}", + "prompt": "${bfdprompt-059540()}", "maxTurnCount": 3, "property": "dialog.listType", "alwaysPrompt": false, "allowInterruptions": "turn.recognized.score > 0.7 && !#ViewCollection && !@listType", "outputFormat": "value", - "value": "@listType", + "value": "=@listType", "choices": [ { "value": "Todo" @@ -483,7 +447,7 @@ "id": "052697", "name": "Send a response" }, - "activity": "@{bfdactivity-052697()}" + "activity": "${bfdactivity-052697()}" }, { "$type": "Microsoft.DeleteProperty", diff --git a/Composer/packages/server/assets/projects/ToDoBotWithLuisSample/ComposerDialogs/Main/Main.lg b/Composer/packages/server/assets/projects/ToDoBotWithLuisSample/ComposerDialogs/Main/Main.lg index 93d54c6f8e..0cd081b29b 100644 --- a/Composer/packages/server/assets/projects/ToDoBotWithLuisSample/ComposerDialogs/Main/Main.lg +++ b/Composer/packages/server/assets/projects/ToDoBotWithLuisSample/ComposerDialogs/Main/Main.lg @@ -1,10 +1,10 @@ [import](common.lg) # bfdactivity-037171 -- @{Welcome-Message()} +- ${Welcome-Message()} # bfdactivity-113335 -- @{Welcome-Message()} +- ${Welcome-Message()} # bfdprompt-183920 - Are you sure you want to cancel? @@ -12,14 +12,14 @@ # bfdactivity-304360 [Activity Text = Cancelling ... - @{Welcome-Actions()} + ${Welcome-Actions()} ] # bfdactivity-470975 - No worries. Let's get right back to where we were... # bfdactivity-456012 -- @{Welcome-Message()} +- ${Welcome-Message()} # bfdprompt-069270 [Activity @@ -28,19 +28,19 @@ ] # bfdprompt-781953 -- Which list should I add **@{dialog.item}** to? +- Which list should I add **${dialog.item}** to? # bfdactivity-775572 [Activity -    Text = @{Add-Todo-Text()} -    @{Welcome-Actions()} +    Text = ${Add-Todo-Text()} +    ${Welcome-Actions()} ] # bfdprompt-637111 - Which list do you want to remove an item from? # bfdactivity-212971 -- @{List-readback(user.taskLists, dialog.listType)} +- ${List-readback(user.taskLists, dialog.listType)} # bfdprompt-879447 [Activity @@ -50,14 +50,14 @@ # bfdactivity-056974 [Activity - Text = @{Delete-Items-readback()} - @{Welcome-Actions()} + Text = ${Delete-Items-readback()} + ${Welcome-Actions()} ] # bfdactivity-084161 [Activity - Text = @{Items-readback()} - @{Welcome-Actions()} + Text = ${Items-readback()} + ${Welcome-Actions()} ] # bfdprompt-059540 @@ -65,6 +65,6 @@ # bfdactivity-052697 [Activity - Text = @{Items-readback()} - @{Welcome-Actions()} -] + Text = ${Items-readback()} + ${Welcome-Actions()} +] diff --git a/Composer/packages/server/assets/projects/ToDoBotWithLuisSample/ComposerDialogs/common/common.lg b/Composer/packages/server/assets/projects/ToDoBotWithLuisSample/ComposerDialogs/common/common.lg index 6a13dfb640..703ab0c800 100644 --- a/Composer/packages/server/assets/projects/ToDoBotWithLuisSample/ComposerDialogs/common/common.lg +++ b/Composer/packages/server/assets/projects/ToDoBotWithLuisSample/ComposerDialogs/common/common.lg @@ -22,59 +22,59 @@ # Welcome-Message [Activity - Text = @{Welcome-Prefix()}, I'm the Todo sample bot. @{Welcome-Suffix()} - @{Welcome-Actions()} + Text = ${Welcome-Prefix()}, I'm the Todo sample bot. ${Welcome-Suffix()} + ${Welcome-Actions()} ] > This template uses pre-built functions to construct a response. # Delete-Items-readback -- IF: @{turn.itemDeleted} - - ```I have removed **@{dialog.item}** from your @{dialog.listType} list. - You have **@{count(user.taskLists[dialog.listType])}** items in your @{dialog.listType} list. +- IF: ${turn.itemDeleted} + - ```I have removed **${dialog.item}** from your ${dialog.listType} list. + You have **${count(user.taskLists[dialog.listType])}** items in your ${dialog.listType} list. ``` - ELSE : - - Sorry, I did not find **@{dialog.item}** in your todo list + - Sorry, I did not find **${dialog.item}** in your todo list # Items-Ordinality (list) -- IF: @{list == null || count(list) == 1} +- IF: ${list == null || count(list) == 1} - item - ELSE: - items # Items-readback -- IF : @{toLower(dialog.listType) == 'all' } - - @{Show-all-lists()} +- IF : ${toLower(dialog.listType) == 'all' } + - ${Show-all-lists()} - ELSE : - - @{List-readback (user.taskLists, dialog.listType)} + - ${List-readback (user.taskLists, dialog.listType)} # Show-all-lists - ``` ## Shopping list -@{List-readback (user.taskLists, 'shopping')} +${List-readback (user.taskLists, 'shopping')} ## Grocery list -@{List-readback (user.taskLists, 'grocery')} +${List-readback (user.taskLists, 'grocery')} ## Todo list -@{List-readback (user.taskLists, 'todo')} +${List-readback (user.taskLists, 'todo')} ``` > This template uses a foreach prebuilt function to call a template. The template helps with putting together each item in a line prefixed with `-` > This way, clients that support multi-line markdown for text can render the items as a list. # List-readback (collection, type) -- IF: @{collection != null && collection[type] != null && count(collection[type]) != 0} +- IF: ${collection != null && collection[type] != null && count(collection[type]) != 0} - ``` -You have @{count(collection[type])} @{Items-Ordinality(collection[type])} in your @{type} collection. - - @{join(foreach(collection[type], item, todo-line(item)), '')} +You have ${count(collection[type])} ${Items-Ordinality(collection[type])} in your ${type} collection. + - ${join(foreach(collection[type], item, todo-line(item)), '')} ``` - ELSE: - ``` -You do not have any items in your @{type} list. +You do not have any items in your ${type} list. ``` # todo-line (item) - ``` -- @{item}``` +- ${item}``` # Help-prefix - Sure @@ -83,6 +83,6 @@ You do not have any items in your @{type} list. # Add-Todo-Text - ``` -@{Help-prefix()}, I have added **@{dialog.item}** to your @{dialog.listType} list.  -You have **@{count(user.taskLists[dialog.listType])}** items in your @{dialog.listType} list. +${Help-prefix()}, I have added **${dialog.item}** to your ${dialog.listType} list.  +You have **${count(user.taskLists[dialog.listType])}** items in your ${dialog.listType} list. ``` \ No newline at end of file diff --git a/Composer/packages/server/assets/projects/TodoRecognizerSetSample/ComposerDialogs/AddToDo/AddToDo.dialog b/Composer/packages/server/assets/projects/TodoRecognizerSetSample/ComposerDialogs/AddToDo/AddToDo.dialog new file mode 100644 index 0000000000..5ee4e61c45 --- /dev/null +++ b/Composer/packages/server/assets/projects/TodoRecognizerSetSample/ComposerDialogs/AddToDo/AddToDo.dialog @@ -0,0 +1,48 @@ +{ + "$type": "Microsoft.AdaptiveDialog", + "$designer": { + "id": "808722" + }, + "autoEndDialog": true, + "defaultResultProperty": "dialog.result", + "triggers": [ + { + "$type": "Microsoft.OnBeginDialog", + "$designer": { + "id": "335456" + }, + "actions": [ + { + "$type": "Microsoft.TextInput", + "$designer": { + "id": "298897" + }, + "property": "dialog.todo", + "prompt": "OK, please enter the title of your todo.", + "maxTurnCount": 3, + "value": "=@title", + "alwaysPrompt": false, + "allowInterruptions": "true" + }, + { + "$type": "Microsoft.EditArray", + "$designer": { + "id": "567087" + }, + "changeType": "push", + "itemsProperty": "user.todos", + "value": "=dialog.todo" + }, + { + "$type": "Microsoft.SendActivity", + "$designer": { + "id": "116673" + }, + "activity": "${bfdactivity-116673()}" + } + ] + } + ], + "generator": "AddToDo.lg", + "$schema": "https://raw.githubusercontent.com/microsoft/BotFramework-Composer/stable/Composer/packages/server/schemas/sdk.schema" +} diff --git a/Composer/packages/server/assets/projects/TodoRecognizerSetSample/ComposerDialogs/AddToDo/AddToDo.lg b/Composer/packages/server/assets/projects/TodoRecognizerSetSample/ComposerDialogs/AddToDo/AddToDo.lg new file mode 100644 index 0000000000..de1e038fa7 --- /dev/null +++ b/Composer/packages/server/assets/projects/TodoRecognizerSetSample/ComposerDialogs/AddToDo/AddToDo.lg @@ -0,0 +1,4 @@ +[import](common.lg) + +# bfdactivity-116673 +-Successfully added a todo named ${dialog.todo} diff --git a/BotProject/CSharp/ComposerDialogs/Main/Main.lu b/Composer/packages/server/assets/projects/TodoRecognizerSetSample/ComposerDialogs/AddToDo/AddToDo.lu similarity index 100% rename from BotProject/CSharp/ComposerDialogs/Main/Main.lu rename to Composer/packages/server/assets/projects/TodoRecognizerSetSample/ComposerDialogs/AddToDo/AddToDo.lu diff --git a/Composer/packages/server/assets/projects/TodoRecognizerSetSample/ComposerDialogs/ClearToDos/ClearToDos.dialog b/Composer/packages/server/assets/projects/TodoRecognizerSetSample/ComposerDialogs/ClearToDos/ClearToDos.dialog new file mode 100644 index 0000000000..4038bfaaa8 --- /dev/null +++ b/Composer/packages/server/assets/projects/TodoRecognizerSetSample/ComposerDialogs/ClearToDos/ClearToDos.dialog @@ -0,0 +1,54 @@ +{ + "$type": "Microsoft.AdaptiveDialog", + "$designer": { + "id": "316336" + }, + "autoEndDialog": true, + "defaultResultProperty": "dialog.result", + "triggers": [ + { + "$type": "Microsoft.OnBeginDialog", + "$designer": { + "id": "480162" + }, + "actions": [ + { + "$type": "Microsoft.EditArray", + "$designer": { + "id": "832307" + }, + "changeType": "Clear", + "itemsProperty": "user.todos", + "resultProperty": "dialog.cleared" + }, + { + "$type": "Microsoft.IfCondition", + "$designer": { + "id": "983761" + }, + "condition": "dialog.cleared", + "actions": [ + { + "$type": "Microsoft.SendActivity", + "$designer": { + "id": "832307" + }, + "activity": "${bfdactivity-832307()}" + } + ], + "elseActions": [ + { + "$type": "Microsoft.SendActivity", + "$designer": { + "id": "983761" + }, + "activity": "${bfdactivity-983761()}" + } + ] + } + ] + } + ], + "generator": "ClearToDos.lg", + "$schema": "https://raw.githubusercontent.com/microsoft/BotFramework-Composer/stable/Composer/packages/server/schemas/sdk.schema" +} diff --git a/Composer/packages/server/assets/projects/TodoRecognizerSetSample/ComposerDialogs/ClearToDos/ClearToDos.lg b/Composer/packages/server/assets/projects/TodoRecognizerSetSample/ComposerDialogs/ClearToDos/ClearToDos.lg new file mode 100644 index 0000000000..6f87860ae7 --- /dev/null +++ b/Composer/packages/server/assets/projects/TodoRecognizerSetSample/ComposerDialogs/ClearToDos/ClearToDos.lg @@ -0,0 +1,7 @@ +[import](common.lg) + +# bfdactivity-832307 +-Successfully cleared items in the Todo List. + +# bfdactivity-983761 +-You don't have any items in the Todo List. diff --git a/BotProject/CSharp/packages/packages.json b/Composer/packages/server/assets/projects/TodoRecognizerSetSample/ComposerDialogs/ClearToDos/ClearToDos.lu similarity index 100% rename from BotProject/CSharp/packages/packages.json rename to Composer/packages/server/assets/projects/TodoRecognizerSetSample/ComposerDialogs/ClearToDos/ClearToDos.lu diff --git a/Composer/packages/server/assets/projects/TodoRecognizerSetSample/ComposerDialogs/DeleteToDo/DeleteToDo.dialog b/Composer/packages/server/assets/projects/TodoRecognizerSetSample/ComposerDialogs/DeleteToDo/DeleteToDo.dialog new file mode 100644 index 0000000000..a4f151bec9 --- /dev/null +++ b/Composer/packages/server/assets/projects/TodoRecognizerSetSample/ComposerDialogs/DeleteToDo/DeleteToDo.dialog @@ -0,0 +1,67 @@ +{ + "$type": "Microsoft.AdaptiveDialog", + "$designer": { + "id": "114909" + }, + "autoEndDialog": true, + "defaultResultProperty": "dialog.result", + "triggers": [ + { + "$type": "Microsoft.OnBeginDialog", + "$designer": { + "id": "768658" + }, + "actions": [ + { + "$type": "Microsoft.TextInput", + "$designer": { + "id": "870620" + }, + "property": "dialog.todo", + "prompt": "OK, please enter the title of the todo you want to remove.", + "maxTurnCount": 3, + "value": "=@title", + "alwaysPrompt": false, + "allowInterruptions": "false" + }, + { + "$type": "Microsoft.EditArray", + "$designer": { + "id": "492096" + }, + "changeType": "Remove", + "itemsProperty": "user.todos", + "resultProperty": "dialog.removed", + "value": "=dialog.todo" + }, + { + "$type": "Microsoft.IfCondition", + "$designer": { + "id": "549615" + }, + "condition": "dialog.removed", + "actions": [ + { + "$type": "Microsoft.SendActivity", + "$designer": { + "id": "725469" + }, + "activity": "${bfdactivity-725469()}" + } + ], + "elseActions": [ + { + "$type": "Microsoft.SendActivity", + "$designer": { + "id": "549615" + }, + "activity": "${bfdactivity-549615()}" + } + ] + } + ] + } + ], + "generator": "DeleteToDo.lg", + "$schema": "https://raw.githubusercontent.com/microsoft/BotFramework-Composer/stable/Composer/packages/server/schemas/sdk.schema" +} diff --git a/Composer/packages/server/assets/projects/TodoRecognizerSetSample/ComposerDialogs/DeleteToDo/DeleteToDo.lg b/Composer/packages/server/assets/projects/TodoRecognizerSetSample/ComposerDialogs/DeleteToDo/DeleteToDo.lg new file mode 100644 index 0000000000..883fe4dde6 --- /dev/null +++ b/Composer/packages/server/assets/projects/TodoRecognizerSetSample/ComposerDialogs/DeleteToDo/DeleteToDo.lg @@ -0,0 +1,10 @@ +[import](common.lg) + +# bfdactivity-725469 +-Successfully removed a todo named ${dialog.todo} + +# bfdactivity-549615 +-${dialog.todo} is not in the Todo List + +# bfdprompt-870620() +- OK, please enter the title of the todo you want to remove. diff --git a/Composer/packages/server/assets/projects/TodoRecognizerSetSample/ComposerDialogs/DeleteToDo/DeleteToDo.lu b/Composer/packages/server/assets/projects/TodoRecognizerSetSample/ComposerDialogs/DeleteToDo/DeleteToDo.lu new file mode 100644 index 0000000000..e69de29bb2 diff --git a/Composer/packages/server/assets/projects/TodoRecognizerSetSample/ComposerDialogs/Main/Main.dialog b/Composer/packages/server/assets/projects/TodoRecognizerSetSample/ComposerDialogs/Main/Main.dialog new file mode 100644 index 0000000000..bf4c4f9df0 --- /dev/null +++ b/Composer/packages/server/assets/projects/TodoRecognizerSetSample/ComposerDialogs/Main/Main.dialog @@ -0,0 +1,202 @@ +{ + "$type": "Microsoft.AdaptiveDialog", + "$designer": { + "id": "288769", + "description": "This is a bot that demonstrates how to manage a ToDo list using Regular Expressions." + }, + "autoEndDialog": false, + "defaultResultProperty": "dialog.result", + "recognizer": { + "$kind": "Microsoft.RecognizerSet", + "recognizers": [ + { + "$kind": "Microsoft.MultiLanguageRecognizer", + "recognizers": { + "en-us": { + "$kind": "Microsoft.CrossTrainedRecognizerSet", + "recognizers": [ + { + "$kind": "Microsoft.RegexRecognizer", + "id": "regex", + "intents": [ + { + "intent": "AddIntent", + "pattern": "(?i)(?:add|create) .*(?:to-do|todo|task)(?: )?(?:named (?.*))?" + }, + { + "intent": "ClearIntent", + "pattern": "(?i)(?:delete|remove|clear) (?:all|every) (?:to-dos|todos|tasks)" + }, + { + "intent": "DeleteIntent", + "pattern": "(?i)(?:delete|remove|clear) .*(?:to-do|todo|task)(?: )?(?:named (?<title>.*))?" + }, + { + "intent": "ShowIntent", + "pattern": "(?i)(?:show|see|view) .*(?:to-do|todo|task)" + }, + { + "intent": "HelpIntent", + "pattern": "(?i)help" + }, + { + "intent": "CancelIntent", + "pattern": "(?i)cancel|never mind" + } + ] + } + ] + } + } + }, + { + "$kind": "Microsoft.ValueRecognizer" + } + ] + }, + "generator": "Main.lg", + "triggers": [ + { + "$type": "Microsoft.OnConversationUpdateActivity", + "$designer": { + "id": "376720" + }, + "actions": [ + { + "$type": "Microsoft.Foreach", + "$designer": { + "id": "518944", + "name": "Loop: for each item" + }, + "itemsProperty": "turn.Activity.membersAdded", + "actions": [ + { + "$type": "Microsoft.IfCondition", + "$designer": { + "id": "641773", + "name": "Branch: if/else" + }, + "condition": "string(dialog.foreach.value.id) != string(turn.Activity.Recipient.id)", + "actions": [ + { + "$type": "Microsoft.SendActivity", + "$designer": { + "id": "157674", + "name": "Send a response" + }, + "activity": "${bfdactivity-157674()}" + } + ] + } + ] + } + ] + }, + { + "$type": "Microsoft.OnIntent", + "$designer": { + "id": "064505" + }, + "actions": [ + { + "$type": "Microsoft.BeginDialog", + "dialog": "AddToDo" + } + ], + "intent": "AddIntent" + }, + { + "$type": "Microsoft.OnIntent", + "$designer": { + "id": "114961" + }, + "actions": [ + { + "$type": "Microsoft.BeginDialog", + "dialog": "DeleteToDo", + "$designer": { + "id": "978613" + } + } + ], + "intent": "DeleteIntent" + }, + { + "$type": "Microsoft.OnIntent", + "$designer": { + "id": "088050" + }, + "actions": [ + { + "$type": "Microsoft.BeginDialog", + "dialog": "ClearToDos" + } + ], + "intent": "ClearIntent" + }, + { + "$type": "Microsoft.OnIntent", + "$designer": { + "id": "633942" + }, + "actions": [ + { + "$type": "Microsoft.SendActivity", + "$designer": { + "id": "696707" + }, + "activity": "${bfdactivity-696707()}" + } + ], + "intent": "HelpIntent" + }, + { + "$type": "Microsoft.OnIntent", + "$designer": { + "id": "794124" + }, + "actions": [ + { + "$type": "Microsoft.BeginDialog", + "dialog": "ShowToDos" + } + ], + "intent": "ShowIntent" + }, + { + "$type": "Microsoft.OnIntent", + "$designer": { + "id": "179728" + }, + "actions": [ + { + "$type": "Microsoft.SendActivity", + "$designer": { + "id": "677440" + }, + "activity": "ok." + }, + { + "$type": "Microsoft.EndDialog" + } + ], + "intent": "CancelIntent" + }, + { + "$type": "Microsoft.OnUnknownIntent", + "$designer": { + "id": "677447" + }, + "actions": [ + { + "$type": "Microsoft.SendActivity", + "$designer": { + "id": "677448" + }, + "activity": "Hi! I'm a ToDo bot. Say \"add a todo named first\" to get started." + } + ] + } + ], + "$schema": "https://raw.githubusercontent.com/microsoft/BotFramework-Composer/stable/Composer/packages/server/schemas/sdk.schema" +} \ No newline at end of file diff --git a/Composer/packages/server/assets/projects/TodoRecognizerSetSample/ComposerDialogs/Main/Main.lg b/Composer/packages/server/assets/projects/TodoRecognizerSetSample/ComposerDialogs/Main/Main.lg new file mode 100644 index 0000000000..46c46aa61a --- /dev/null +++ b/Composer/packages/server/assets/projects/TodoRecognizerSetSample/ComposerDialogs/Main/Main.lg @@ -0,0 +1,10 @@ +[import](common.lg) + +# bfdactivity-696707 +-${help()} + +# bfdactivity-157674() +- Hi! I'm a ToDo bot. Say "add a todo named first" to get started. + +# foo +- You are so smart! diff --git a/Composer/packages/server/assets/projects/TodoRecognizerSetSample/ComposerDialogs/Main/Main.lu b/Composer/packages/server/assets/projects/TodoRecognizerSetSample/ComposerDialogs/Main/Main.lu new file mode 100644 index 0000000000..e69de29bb2 diff --git a/Composer/packages/server/assets/projects/TodoRecognizerSetSample/ComposerDialogs/ShowToDos/ShowToDos.dialog b/Composer/packages/server/assets/projects/TodoRecognizerSetSample/ComposerDialogs/ShowToDos/ShowToDos.dialog new file mode 100644 index 0000000000..869ecbbb98 --- /dev/null +++ b/Composer/packages/server/assets/projects/TodoRecognizerSetSample/ComposerDialogs/ShowToDos/ShowToDos.dialog @@ -0,0 +1,47 @@ +{ + "$type": "Microsoft.AdaptiveDialog", + "$designer": { + "id": "709692" + }, + "autoEndDialog": true, + "defaultResultProperty": "dialog.result", + "triggers": [ + { + "$type": "Microsoft.OnBeginDialog", + "$designer": { + "id": "783343" + }, + "actions": [ + { + "$type": "Microsoft.IfCondition", + "$designer": { + "id": "662084" + }, + "condition": "user.todos == null", + "actions": [ + { + "$type": "Microsoft.SendActivity", + "$designer": { + "id": "339580", + "name": "Send an Activity" + }, + "activity": "${bfdactivity-339580()}" + } + ], + "elseActions": [ + { + "$type": "Microsoft.SendActivity", + "$designer": { + "id": "662084", + "name": "Send an Activity" + }, + "activity": "${bfdactivity-662084()}" + } + ] + } + ] + } + ], + "generator": "ShowToDos.lg", + "$schema": "https://raw.githubusercontent.com/microsoft/BotFramework-Composer/stable/Composer/packages/server/schemas/sdk.schema" +} diff --git a/Composer/packages/server/assets/projects/TodoRecognizerSetSample/ComposerDialogs/ShowToDos/ShowToDos.lg b/Composer/packages/server/assets/projects/TodoRecognizerSetSample/ComposerDialogs/ShowToDos/ShowToDos.lg new file mode 100644 index 0000000000..9983313454 --- /dev/null +++ b/Composer/packages/server/assets/projects/TodoRecognizerSetSample/ComposerDialogs/ShowToDos/ShowToDos.lg @@ -0,0 +1,8 @@ +[import](common.lg) + + +# bfdactivity-339580 +-You have no todos. + +# bfdactivity-662084 +-${ShowTodo()} diff --git a/Composer/packages/server/assets/projects/TodoRecognizerSetSample/ComposerDialogs/ShowToDos/ShowToDos.lu b/Composer/packages/server/assets/projects/TodoRecognizerSetSample/ComposerDialogs/ShowToDos/ShowToDos.lu new file mode 100644 index 0000000000..e69de29bb2 diff --git a/Composer/packages/server/assets/projects/TodoRecognizerSetSample/ComposerDialogs/common/common.lg b/Composer/packages/server/assets/projects/TodoRecognizerSetSample/ComposerDialogs/common/common.lg new file mode 100644 index 0000000000..951d3595ca --- /dev/null +++ b/Composer/packages/server/assets/projects/TodoRecognizerSetSample/ComposerDialogs/common/common.lg @@ -0,0 +1,39 @@ +# Hello +-${Welcome(time)} ${name} + +# Welcome(time) +-IF:${time == 'morning'} + - Good morning +-ELSEIF:${time == 'evening'} + - Good evening +-ELSE: + - How are you doing, + +# Exit +-Thanks for using todo bot. + +# Greeting +-What's up bro + +# ShowTodo +-IF:${count(user.todos) > 0} +-``` +${HelperFunction()} +${join(foreach(user.todos, x, showSingleTodo(x)), '\n')} +``` +-ELSE: +-You don't have any todos. + +# showSingleTodo(x) +-* ${x} + +# HelperFunction +- IF: ${count(user.todos) == 1} + - Your most recent ${count(user.todos)} task is +- ELSE: + - Your most recent ${count(user.todos)} tasks are + +# help +-I can add a todo, show todos, remove a todo, and clear all todos +-I can help you yes I can +-Help, we don't need no stinkin' help! \ No newline at end of file diff --git a/Composer/packages/server/assets/projects/TodoSample/ComposerDialogs/AddToDo/AddToDo.dialog b/Composer/packages/server/assets/projects/TodoSample/ComposerDialogs/AddToDo/AddToDo.dialog index 0670abd04f..5ee4e61c45 100644 --- a/Composer/packages/server/assets/projects/TodoSample/ComposerDialogs/AddToDo/AddToDo.dialog +++ b/Composer/packages/server/assets/projects/TodoSample/ComposerDialogs/AddToDo/AddToDo.dialog @@ -12,14 +12,6 @@ "id": "335456" }, "actions": [ - { - "$type": "Microsoft.SetProperty", - "$designer": { - "id": "201694" - }, - "property": "dialog.todo", - "value": "@title" - }, { "$type": "Microsoft.TextInput", "$designer": { @@ -28,41 +20,25 @@ "property": "dialog.todo", "prompt": "OK, please enter the title of your todo.", "maxTurnCount": 3, + "value": "=@title", "alwaysPrompt": false, "allowInterruptions": "true" }, - { - "$type": "Microsoft.IfCondition", - "$designer": { - "id": "015420" - }, - "condition": "user.todos == null", - "actions": [ - { - "$type": "Microsoft.InitProperty", - "$designer": { - "id": "015420" - }, - "property": "user.todos", - "type": "array" - } - ] - }, { "$type": "Microsoft.EditArray", "$designer": { "id": "567087" }, - "changeType": "Push", + "changeType": "push", "itemsProperty": "user.todos", - "value": "dialog.todo" + "value": "=dialog.todo" }, { "$type": "Microsoft.SendActivity", "$designer": { "id": "116673" }, - "activity": "@{bfdactivity-116673()}" + "activity": "${bfdactivity-116673()}" } ] } diff --git a/Composer/packages/server/assets/projects/TodoSample/ComposerDialogs/AddToDo/AddToDo.lg b/Composer/packages/server/assets/projects/TodoSample/ComposerDialogs/AddToDo/AddToDo.lg index b22ba8fae6..de1e038fa7 100644 --- a/Composer/packages/server/assets/projects/TodoSample/ComposerDialogs/AddToDo/AddToDo.lg +++ b/Composer/packages/server/assets/projects/TodoSample/ComposerDialogs/AddToDo/AddToDo.lg @@ -1,4 +1,4 @@ [import](common.lg) # bfdactivity-116673 --Successfully added a todo named @{dialog.todo} +-Successfully added a todo named ${dialog.todo} diff --git a/Composer/packages/server/assets/projects/TodoSample/ComposerDialogs/ClearToDos/ClearToDos.dialog b/Composer/packages/server/assets/projects/TodoSample/ComposerDialogs/ClearToDos/ClearToDos.dialog index ef4a4fb651..4038bfaaa8 100644 --- a/Composer/packages/server/assets/projects/TodoSample/ComposerDialogs/ClearToDos/ClearToDos.dialog +++ b/Composer/packages/server/assets/projects/TodoSample/ComposerDialogs/ClearToDos/ClearToDos.dialog @@ -33,7 +33,7 @@ "$designer": { "id": "832307" }, - "activity": "@{bfdactivity-832307()}" + "activity": "${bfdactivity-832307()}" } ], "elseActions": [ @@ -42,7 +42,7 @@ "$designer": { "id": "983761" }, - "activity": "@{bfdactivity-983761()}" + "activity": "${bfdactivity-983761()}" } ] } diff --git a/Composer/packages/server/assets/projects/TodoSample/ComposerDialogs/DeleteToDo/DeleteToDo.dialog b/Composer/packages/server/assets/projects/TodoSample/ComposerDialogs/DeleteToDo/DeleteToDo.dialog index 0b87a9ff54..a4f151bec9 100644 --- a/Composer/packages/server/assets/projects/TodoSample/ComposerDialogs/DeleteToDo/DeleteToDo.dialog +++ b/Composer/packages/server/assets/projects/TodoSample/ComposerDialogs/DeleteToDo/DeleteToDo.dialog @@ -12,14 +12,6 @@ "id": "768658" }, "actions": [ - { - "$type": "Microsoft.SetProperty", - "$designer": { - "id": "725469" - }, - "property": "dialog.todo", - "value": "@title" - }, { "$type": "Microsoft.TextInput", "$designer": { @@ -28,6 +20,7 @@ "property": "dialog.todo", "prompt": "OK, please enter the title of the todo you want to remove.", "maxTurnCount": 3, + "value": "=@title", "alwaysPrompt": false, "allowInterruptions": "false" }, @@ -39,7 +32,7 @@ "changeType": "Remove", "itemsProperty": "user.todos", "resultProperty": "dialog.removed", - "value": "dialog.todo" + "value": "=dialog.todo" }, { "$type": "Microsoft.IfCondition", @@ -53,7 +46,7 @@ "$designer": { "id": "725469" }, - "activity": "@{bfdactivity-725469()}" + "activity": "${bfdactivity-725469()}" } ], "elseActions": [ @@ -62,7 +55,7 @@ "$designer": { "id": "549615" }, - "activity": "@{bfdactivity-549615()}" + "activity": "${bfdactivity-549615()}" } ] } diff --git a/Composer/packages/server/assets/projects/TodoSample/ComposerDialogs/DeleteToDo/DeleteToDo.lg b/Composer/packages/server/assets/projects/TodoSample/ComposerDialogs/DeleteToDo/DeleteToDo.lg index 9032f289f0..883fe4dde6 100644 --- a/Composer/packages/server/assets/projects/TodoSample/ComposerDialogs/DeleteToDo/DeleteToDo.lg +++ b/Composer/packages/server/assets/projects/TodoSample/ComposerDialogs/DeleteToDo/DeleteToDo.lg @@ -1,10 +1,10 @@ [import](common.lg) # bfdactivity-725469 --Successfully removed a todo named @{dialog.todo} +-Successfully removed a todo named ${dialog.todo} # bfdactivity-549615 --@{dialog.todo} is not in the Todo List +-${dialog.todo} is not in the Todo List # bfdprompt-870620() - OK, please enter the title of the todo you want to remove. diff --git a/Composer/packages/server/assets/projects/TodoSample/ComposerDialogs/Main/Main.dialog b/Composer/packages/server/assets/projects/TodoSample/ComposerDialogs/Main/Main.dialog index 0a52604d6b..80fc61097e 100644 --- a/Composer/packages/server/assets/projects/TodoSample/ComposerDialogs/Main/Main.dialog +++ b/Composer/packages/server/assets/projects/TodoSample/ComposerDialogs/Main/Main.dialog @@ -7,183 +7,176 @@ "autoEndDialog": false, "defaultResultProperty": "dialog.result", "recognizer": { - "$type": "Microsoft.RegexRecognizer", + "$kind": "Microsoft.RegexRecognizer", + "id": "regex", "intents": [ { - "$type": "Microsoft.IntentPattern", "intent": "AddIntent", "pattern": "(?i)(?:add|create) .*(?:to-do|todo|task)(?: )?(?:named (?<title>.*))?" }, { - "$type": "Microsoft.IntentPattern", "intent": "ClearIntent", "pattern": "(?i)(?:delete|remove|clear) (?:all|every) (?:to-dos|todos|tasks)" }, { - "$type": "Microsoft.IntentPattern", "intent": "DeleteIntent", "pattern": "(?i)(?:delete|remove|clear) .*(?:to-do|todo|task)(?: )?(?:named (?<title>.*))?" }, { - "$type": "Microsoft.IntentPattern", "intent": "ShowIntent", "pattern": "(?i)(?:show|see|view) .*(?:to-do|todo|task)" }, { - "$type": "Microsoft.IntentPattern", "intent": "HelpIntent", "pattern": "(?i)help" }, { - "$type": "Microsoft.IntentPattern", "intent": "CancelIntent", "pattern": "(?i)cancel|never mind" } ] }, - "generator": "Main.lg", + "generator": "Main.lg", "triggers": [ - { - "$type": "Microsoft.OnConversationUpdateActivity", - "$designer": { - "id": "376720" - }, - "actions": [ - { - "$type": "Microsoft.Foreach", - "$designer": { - "id": "518944", - "name": "Loop: for each item" - }, - "itemsProperty": "turn.Activity.membersAdded", - "actions": [ - { - "$type": "Microsoft.IfCondition", - "$designer": { - "id": "641773", - "name": "Branch: if/else" - }, - "condition": "string(dialog.foreach.value.id) != string(turn.Activity.Recipient.id)", - "actions": [ - { - "$type": "Microsoft.SendActivity", - "$designer": { - "id": "157674", - "name": "Send a response" - }, - "activity": "@{bfdactivity-157674()}" - } - ] - } - ] - } - ] - }, - { - "$type": "Microsoft.OnIntent", - "$designer": { - "id": "064505" - }, - "actions": [ - { - "$type": "Microsoft.BeginDialog", - "dialog": "AddToDo" - } - ], - "intent": "AddIntent" + { + "$type": "Microsoft.OnConversationUpdateActivity", + "$designer": { + "id": "376720" }, - { - "$type": "Microsoft.OnIntent", - "$designer": { - "id": "114961" - }, - "actions": [ - { - "$type": "Microsoft.BeginDialog", - "dialog": "DeleteToDo", - "$designer": { - "id": "978613" + "actions": [ + { + "$type": "Microsoft.Foreach", + "$designer": { + "id": "518944", + "name": "Loop: for each item" + }, + "itemsProperty": "turn.Activity.membersAdded", + "actions": [ + { + "$type": "Microsoft.IfCondition", + "$designer": { + "id": "641773", + "name": "Branch: if/else" + }, + "condition": "string(dialog.foreach.value.id) != string(turn.Activity.Recipient.id)", + "actions": [ + { + "$type": "Microsoft.SendActivity", + "$designer": { + "id": "157674", + "name": "Send a response" + }, + "activity": "${bfdactivity-157674()}" + } + ] } - } - ], - "intent": "DeleteIntent" + ] + } + ] + }, + { + "$type": "Microsoft.OnIntent", + "$designer": { + "id": "064505" }, - { - "$type": "Microsoft.OnIntent", - "$designer": { - "id": "088050" - }, - "actions": [ - { - "$type": "Microsoft.BeginDialog", - "dialog": "ClearToDos" - } - ], - "intent": "ClearIntent" + "actions": [ + { + "$type": "Microsoft.BeginDialog", + "dialog": "AddToDo" + } + ], + "intent": "AddIntent" + }, + { + "$type": "Microsoft.OnIntent", + "$designer": { + "id": "114961" }, - { - "$type": "Microsoft.OnIntent", - "$designer": { - "id": "633942" - }, - "actions": [ - { - "$type": "Microsoft.SendActivity", - "$designer": { - "id": "696707" - }, - "activity": "@{bfdactivity-696707()}" + "actions": [ + { + "$type": "Microsoft.BeginDialog", + "dialog": "DeleteToDo", + "$designer": { + "id": "978613" } - ], - "intent": "HelpIntent" + } + ], + "intent": "DeleteIntent" + }, + { + "$type": "Microsoft.OnIntent", + "$designer": { + "id": "088050" }, - { - "$type": "Microsoft.OnIntent", - "$designer": { - "id": "794124" - }, - "actions": [ - { - "$type": "Microsoft.BeginDialog", - "dialog": "ShowToDos" - } - ], - "intent": "ShowIntent" + "actions": [ + { + "$type": "Microsoft.BeginDialog", + "dialog": "ClearToDos" + } + ], + "intent": "ClearIntent" + }, + { + "$type": "Microsoft.OnIntent", + "$designer": { + "id": "633942" }, - { - "$type": "Microsoft.OnIntent", - "$designer": { - "id": "179728" - }, - "actions": [ - { - "$type": "Microsoft.SendActivity", - "$designer": { - "id": "677440" - }, - "activity": "ok." + "actions": [ + { + "$type": "Microsoft.SendActivity", + "$designer": { + "id": "696707" }, - { - "$type": "Microsoft.EndDialog" - } - ], - "intent": "CancelIntent" + "activity": "${bfdactivity-696707()}" + } + ], + "intent": "HelpIntent" + }, + { + "$type": "Microsoft.OnIntent", + "$designer": { + "id": "794124" + }, + "actions": [ + { + "$type": "Microsoft.BeginDialog", + "dialog": "ShowToDos" + } + ], + "intent": "ShowIntent" + }, + { + "$type": "Microsoft.OnIntent", + "$designer": { + "id": "179728" }, - { - "$type": "Microsoft.OnUnknownIntent", - "$designer": { - "id": "677447" + "actions": [ + { + "$type": "Microsoft.SendActivity", + "$designer": { + "id": "677440" + }, + "activity": "ok." }, - "actions": [ - { - "$type": "Microsoft.SendActivity", - "$designer": { - "id": "677448" - }, - "activity": "Hi! I'm a ToDo bot. Say \"add a todo named first\" to get started." - } - ] - } - ], - "$schema": "https://raw.githubusercontent.com/microsoft/BotFramework-Composer/stable/Composer/packages/server/schemas/sdk.schema" -} + { + "$type": "Microsoft.EndDialog" + } + ], + "intent": "CancelIntent" + }, + { + "$type": "Microsoft.OnUnknownIntent", + "$designer": { + "id": "677447" + }, + "actions": [ + { + "$type": "Microsoft.SendActivity", + "$designer": { + "id": "677448" + }, + "activity": "Hi! I'm a ToDo bot. Say \"add a todo named first\" to get started." + } + ] + } +], "$schema": "https://raw.githubusercontent.com/microsoft/BotFramework-Composer/stable/Composer/packages/server/schemas/sdk.schema" } diff --git a/Composer/packages/server/assets/projects/TodoSample/ComposerDialogs/Main/Main.lg b/Composer/packages/server/assets/projects/TodoSample/ComposerDialogs/Main/Main.lg index ad1159cd7d..46c46aa61a 100644 --- a/Composer/packages/server/assets/projects/TodoSample/ComposerDialogs/Main/Main.lg +++ b/Composer/packages/server/assets/projects/TodoSample/ComposerDialogs/Main/Main.lg @@ -1,7 +1,7 @@ [import](common.lg) # bfdactivity-696707 --@{help()} +-${help()} # bfdactivity-157674() - Hi! I'm a ToDo bot. Say "add a todo named first" to get started. diff --git a/Composer/packages/server/assets/projects/TodoSample/ComposerDialogs/ShowToDos/ShowToDos.dialog b/Composer/packages/server/assets/projects/TodoSample/ComposerDialogs/ShowToDos/ShowToDos.dialog index 9494d6928e..869ecbbb98 100644 --- a/Composer/packages/server/assets/projects/TodoSample/ComposerDialogs/ShowToDos/ShowToDos.dialog +++ b/Composer/packages/server/assets/projects/TodoSample/ComposerDialogs/ShowToDos/ShowToDos.dialog @@ -25,7 +25,7 @@ "id": "339580", "name": "Send an Activity" }, - "activity": "@{bfdactivity-339580()}" + "activity": "${bfdactivity-339580()}" } ], "elseActions": [ @@ -35,7 +35,7 @@ "id": "662084", "name": "Send an Activity" }, - "activity": "@{bfdactivity-662084()}" + "activity": "${bfdactivity-662084()}" } ] } diff --git a/Composer/packages/server/assets/projects/TodoSample/ComposerDialogs/ShowToDos/ShowToDos.lg b/Composer/packages/server/assets/projects/TodoSample/ComposerDialogs/ShowToDos/ShowToDos.lg index f7ad27a35b..9983313454 100644 --- a/Composer/packages/server/assets/projects/TodoSample/ComposerDialogs/ShowToDos/ShowToDos.lg +++ b/Composer/packages/server/assets/projects/TodoSample/ComposerDialogs/ShowToDos/ShowToDos.lg @@ -5,4 +5,4 @@ -You have no todos. # bfdactivity-662084 --@{ShowTodo()} +-${ShowTodo()} diff --git a/Composer/packages/server/assets/projects/TodoSample/ComposerDialogs/common/common.lg b/Composer/packages/server/assets/projects/TodoSample/ComposerDialogs/common/common.lg index cefda30532..951d3595ca 100644 --- a/Composer/packages/server/assets/projects/TodoSample/ComposerDialogs/common/common.lg +++ b/Composer/packages/server/assets/projects/TodoSample/ComposerDialogs/common/common.lg @@ -1,10 +1,10 @@ # Hello --@{Welcome(time)} @{name} +-${Welcome(time)} ${name} # Welcome(time) --IF:@{time == 'morning'} +-IF:${time == 'morning'} - Good morning --ELSEIF:@{time == 'evening'} +-ELSEIF:${time == 'evening'} - Good evening -ELSE: - How are you doing, @@ -16,22 +16,22 @@ -What's up bro # ShowTodo --IF:@{count(user.todos) > 0} +-IF:${count(user.todos) > 0} -``` -@{HelperFunction()} -@{join(foreach(user.todos, x, showSingleTodo(x)), '\n')} +${HelperFunction()} +${join(foreach(user.todos, x, showSingleTodo(x)), '\n')} ``` -ELSE: -You don't have any todos. # showSingleTodo(x) --* @{x} +-* ${x} # HelperFunction -- IF: @{count(user.todos) == 1} - - Your most recent @{count(user.todos)} task is +- IF: ${count(user.todos) == 1} + - Your most recent ${count(user.todos)} task is - ELSE: - - Your most recent @{count(user.todos)} tasks are + - Your most recent ${count(user.todos)} tasks are # help -I can add a todo, show todos, remove a todo, and clear all todos diff --git a/Composer/packages/server/package.json b/Composer/packages/server/package.json index 3920015655..08e07b6842 100644 --- a/Composer/packages/server/package.json +++ b/Composer/packages/server/package.json @@ -57,7 +57,7 @@ "@bfc/lg-languageserver": "*", "@bfc/lu-languageserver": "*", "@bfc/shared": "*", - "@bfcomposer/lubuild": "1.1.2-preview", + "@bfcomposer/bf-lu": "^1.2.5", "archiver": "^3.0.0", "axios": "^0.18.0", "azure-storage": "^2.10.3", diff --git a/Composer/packages/server/schemas/editor.schema b/Composer/packages/server/schemas/editor.schema index 9d581ca9bb..3d4c987261 100644 --- a/Composer/packages/server/schemas/editor.schema +++ b/Composer/packages/server/schemas/editor.schema @@ -115,11 +115,6 @@ "helpLink": "https://aka.ms/bfc-controlling-conversation-flow", "helpLinkText": "Learn more" }, - "Microsoft.InitProperty": { - "title": "Initialize Property", - "helpLink": "https://aka.ms/bfc-using-memory", - "helpLinkText": "Learn more" - }, "Microsoft.LanguagePolicy": { "title": "LanguagePolicy" }, diff --git a/Composer/packages/server/schemas/sdk.schema b/Composer/packages/server/schemas/sdk.schema index 1fafc1696f..a1c8661f27 100644 --- a/Composer/packages/server/schemas/sdk.schema +++ b/Composer/packages/server/schemas/sdk.schema @@ -999,7 +999,7 @@ "unrecognizedPrompt": { "$kind": "Microsoft.IActivityTemplate", "title": "Unrecognized prompt", - "description": "Message to send if user response is not recognized.", + "description": "Message to send when the recognizer does not understand the user input.", "examples": [ "Sorry, I do not understand '{turn.activity.text'}. Let's try again. What is your birth date?" ], @@ -1008,7 +1008,7 @@ "invalidPrompt": { "$kind": "Microsoft.IActivityTemplate", "title": "Invalid prompt", - "description": "Message to send if user response is invalid. Relies on specified validation expressions.", + "description": "Message to send when the user input does not meet any validation expression.", "examples": [ "Sorry, '{this.value}' does not work. I need a number between 1-150. What is your age?" ], @@ -1072,7 +1072,7 @@ "string" ], "title": "Default value", - "description": "Expression to examine on each turn of the conversation as possible value to the property.", + "description": "'Property' will be set to the value of this expression when max turn count is exceeded.", "examples": [ "@userName", "coalesce(@number, @partySize)" @@ -1089,7 +1089,7 @@ "string" ], "title": "Value", - "description": "Gets or sets a value expression which can be used to intialize the input prompt.", + "description": "'Property' will be set to the value of this expression unless it evaluates to null.", "examples": [ "@userName" ] @@ -1527,7 +1527,7 @@ "unrecognizedPrompt": { "$kind": "Microsoft.IActivityTemplate", "title": "Unrecognized prompt", - "description": "Message to send if user response is not recognized.", + "description": "Message to send when the recognizer does not understand the user input.", "examples": [ "Sorry, I do not understand '{turn.activity.text'}. Let's try again. What is your birth date?" ], @@ -1536,7 +1536,7 @@ "invalidPrompt": { "$kind": "Microsoft.IActivityTemplate", "title": "Invalid prompt", - "description": "Message to send if user response is invalid. Relies on specified validation expressions.", + "description": "Message to send when the user input does not meet any validation expression.", "examples": [ "Sorry, '{this.value}' does not work. I need a number between 1-150. What is your age?" ], @@ -1600,7 +1600,7 @@ "string" ], "title": "Default value", - "description": "Expression to examine on each turn of the conversation as possible value to the property.", + "description": "'Property' will be set to the value of this expression when max turn count is exceeded.", "examples": [ "@userName", "coalesce(@number, @partySize)" @@ -1617,7 +1617,7 @@ "string" ], "title": "Value", - "description": "Gets or sets a value expression which can be used to intialize the input prompt.", + "description": "'Property' will be set to the value of this expression unless it evaluates to null.", "examples": [ "@userName" ] @@ -1939,7 +1939,7 @@ "unrecognizedPrompt": { "$kind": "Microsoft.IActivityTemplate", "title": "Unrecognized prompt", - "description": "Message to send if user response is not recognized.", + "description": "Message to send when the recognizer does not understand the user input.", "examples": [ "Sorry, I do not understand '{turn.activity.text'}. Let's try again. What is your birth date?" ], @@ -1948,7 +1948,7 @@ "invalidPrompt": { "$kind": "Microsoft.IActivityTemplate", "title": "Invalid prompt", - "description": "Message to send if user response is invalid. Relies on specified validation expressions.", + "description": "Message to send when the user input does not meet any validation expression.", "examples": [ "Sorry, '{this.value}' does not work. I need a number between 1-150. What is your age?" ], @@ -2012,7 +2012,7 @@ "string" ], "title": "Default value", - "description": "Expression to examine on each turn of the conversation as possible value to the property.", + "description": "'Property' will be set to the value of this expression when max turn count is exceeded.", "examples": [ "@userName", "coalesce(@number, @partySize)" @@ -2029,7 +2029,7 @@ "string" ], "title": "Value", - "description": "Gets or sets a value expression which can be used to intialize the input prompt.", + "description": "'Property' will be set to the value of this expression unless it evaluates to null.", "examples": [ "@userName" ] @@ -2552,7 +2552,7 @@ "unrecognizedPrompt": { "$kind": "Microsoft.IActivityTemplate", "title": "Unrecognized prompt", - "description": "Message to send if user response is not recognized.", + "description": "Message to send when the recognizer does not understand the user input.", "examples": [ "Sorry, I do not understand '{turn.activity.text'}. Let's try again. What is your birth date?" ], @@ -2561,7 +2561,7 @@ "invalidPrompt": { "$kind": "Microsoft.IActivityTemplate", "title": "Invalid prompt", - "description": "Message to send if user response is invalid. Relies on specified validation expressions.", + "description": "Message to send when the user input does not meet any validation expression.", "examples": [ "Sorry, '{this.value}' does not work. I need a number between 1-150. What is your age?" ], @@ -2625,7 +2625,7 @@ "string" ], "title": "Default value", - "description": "Expression to examine on each turn of the conversation as possible value to the property.", + "description": "'Property' will be set to the value of this expression when max turn count is exceeded.", "examples": [ "@userName", "coalesce(@number, @partySize)" @@ -2642,7 +2642,7 @@ "string" ], "title": "Value", - "description": "Gets or sets a value expression which can be used to intialize the input prompt.", + "description": "'Property' will be set to the value of this expression unless it evaluates to null.", "examples": [ "@userName" ] @@ -5763,7 +5763,7 @@ "unrecognizedPrompt": { "$kind": "Microsoft.IActivityTemplate", "title": "Unrecognized prompt", - "description": "Message to send if user response is not recognized.", + "description": "Message to send when the recognizer does not understand the user input.", "examples": [ "Sorry, I do not understand '{turn.activity.text'}. Let's try again. What is your birth date?" ], @@ -5772,7 +5772,7 @@ "invalidPrompt": { "$kind": "Microsoft.IActivityTemplate", "title": "Invalid prompt", - "description": "Message to send if user response is invalid. Relies on specified validation expressions.", + "description": "Message to send when the user input does not meet any validation expression.", "examples": [ "Sorry, '{this.value}' does not work. I need a number between 1-150. What is your age?" ], @@ -5836,7 +5836,7 @@ "string" ], "title": "Default value", - "description": "Expression to examine on each turn of the conversation as possible value to the property.", + "description": "'Property' will be set to the value of this expression when max turn count is exceeded.", "examples": [ "@userName", "coalesce(@number, @partySize)" @@ -5853,7 +5853,7 @@ "string" ], "title": "Value", - "description": "Gets or sets a value expression which can be used to intialize the input prompt.", + "description": "'Property' will be set to the value of this expression unless it evaluates to null.", "examples": [ "@userName" ] @@ -11520,7 +11520,7 @@ "unrecognizedPrompt": { "$kind": "Microsoft.IActivityTemplate", "title": "Unrecognized prompt", - "description": "Message to send if user response is not recognized.", + "description": "Message to send when the recognizer does not understand the user input.", "examples": [ "Sorry, I do not understand '{turn.activity.text'}. Let's try again. What is your birth date?" ], @@ -11529,7 +11529,7 @@ "invalidPrompt": { "$kind": "Microsoft.IActivityTemplate", "title": "Invalid prompt", - "description": "Message to send if user response is invalid. Relies on specified validation expressions.", + "description": "Message to send when the user input does not meet any validation expression.", "examples": [ "Sorry, '{this.value}' does not work. I need a number between 1-150. What is your age?" ], @@ -11593,7 +11593,7 @@ "string" ], "title": "Default value", - "description": "Expression to examine on each turn of the conversation as possible value to the property.", + "description": "'Property' will be set to the value of this expression when max turn count is exceeded.", "examples": [ "@userName", "coalesce(@number, @partySize)" @@ -11610,7 +11610,7 @@ "string" ], "title": "Value", - "description": "Gets or sets a value expression which can be used to intialize the input prompt.", + "description": "'Property' will be set to the value of this expression unless it evaluates to null.", "examples": [ "@userName" ] @@ -11648,7 +11648,7 @@ "description": "Expression to format the output.", "examples": [ "=toUpper(this.value)", - "@{toUpper(this.value)}" + "${toUpper(this.value)}" ] } }, @@ -12034,4 +12034,4 @@ ] } } -} \ No newline at end of file +} diff --git a/Composer/packages/server/src/models/bot/luPublisher.ts b/Composer/packages/server/src/models/bot/luPublisher.ts index f117227f8a..12fcc28a09 100644 --- a/Composer/packages/server/src/models/bot/luPublisher.ts +++ b/Composer/packages/server/src/models/bot/luPublisher.ts @@ -2,12 +2,14 @@ // Licensed under the MIT License. import isEqual from 'lodash/isEqual'; -import { runBuild } from '@bfcomposer/lubuild'; +import { luBuild } from '@bfcomposer/bf-lu/lib/parser/lubuild'; import { LuFile } from '@bfc/indexers'; import { Path } from './../../utility/path'; import { IFileStorage } from './../storage/interface'; import { ILuisConfig, LuisStatus, FileUpdateType } from './interface'; +import log from './../../logger'; + const GENERATEDFOLDER = 'ComposerDialogs/generated'; const LU_STATUS_FILE = 'luis.status.json'; const DEFAULT_STATUS = { @@ -24,6 +26,11 @@ export class LuPublisher { // key: filePath relative to bot dir // value: lastUpdateTime && lastPublishTime public status: { [key: string]: LuisStatus } = {}; + + private builder = new luBuild.Builder(message => { + log(message); + }); + constructor(path: string, storage: IFileStorage) { this.botDir = path; this.generatedFolderPath = Path.join(this.botDir, GENERATEDFOLDER); @@ -80,24 +87,35 @@ export class LuPublisher { }; public publish = async (luFiles: LuFile[]) => { - const config = this._getConfig(luFiles); - if (config.models.length === 0) { + if (!luFiles.length) { throw new Error('No luis file exist'); } + const config = this._getConfig(); const curTime = Date.now(); try { - await runBuild(config); + const loadResult = await this._loadLuConatents(luFiles); + const buildResult = await this.builder.build( + loadResult.luContents, + loadResult.recognizers, + config.authoringKey, + config.region, + config.botName, + config.suffix, + config.fallbackLocal, + false, + loadResult.multiRecognizers, + loadResult.settings + ); // update pubish status after sucessfully published luFiles.forEach(f => { this.status[f.relativePath].lastPublishTime = curTime; }); await this.saveStatus(); + await this.builder.writeDialogAssets(buildResult, true, this.generatedFolderPath); } catch (error) { - throw new Error(error?.body?.error?.message ?? 'Error publishing to LUIS.'); + throw new Error(error.body?.error?.message || error.message || 'Error publishing to LUIS.'); } - - await this._copyDialogsToTargetFolder(config); }; public getUnpublisedFiles = (files: LuFile[]) => { @@ -147,33 +165,27 @@ export class LuPublisher { } } - private _copyDialogsToTargetFolder = async (config: any) => { - const defaultLanguage = config.defaultLanguage; - await config.models.forEach(async (filePath: string) => { - const baseName = Path.basename(filePath, '.lu'); - const rootPath = Path.dirname(filePath); - const currentPath = `${filePath}.dialog`; - const targetPath = Path.join(this.generatedFolderPath, `${baseName}.lu.dialog`); - const currentVariantPath = Path.join(rootPath, `${baseName}.${defaultLanguage}.lu.dialog`); - const targetVariantPath = Path.join(this.generatedFolderPath, `${baseName}.${defaultLanguage}.lu.dialog`); - await this.storage.copyFile(currentPath, targetPath); - await this.storage.copyFile(currentVariantPath, targetVariantPath); - await this.storage.removeFile(currentPath); - await this.storage.removeFile(currentVariantPath); - }); + private _getConfig = () => { + const luConfig = { + authoringKey: this.config?.authoringKey || '', + region: this.config?.authoringRegion || '', + botName: this.config?.name || '', + suffix: this.config?.environment || '', + fallbackLocal: this.config?.defaultLanguage || 'en-us', + }; + return luConfig; }; - private _getConfig = (luFiles: LuFile[]) => { - const luConfig: any = { ...this.config }; - luConfig.models = []; - luConfig.autodelete = true; - luConfig.dialogs = true; - luConfig.force = false; - luConfig.folder = this.generatedFolderPath; - luFiles.forEach(file => { - luConfig.models.push(Path.resolve(this.botDir, file.relativePath)); + private _loadLuConatents = async (luFiles: LuFile[]) => { + const pathList = luFiles.map(file => { + return Path.resolve(this.botDir, file.relativePath); }); - return luConfig; + return await this.builder.loadContents( + pathList, + this.config?.defaultLanguage || '', + this.config?.environment || '', + this.config?.authoringRegion || '' + ); }; } diff --git a/Composer/packages/server/src/models/connector/csharpBotConnector.ts b/Composer/packages/server/src/models/connector/csharpBotConnector.ts index e77bddcd0d..f97dadb2a4 100644 --- a/Composer/packages/server/src/models/connector/csharpBotConnector.ts +++ b/Composer/packages/server/src/models/connector/csharpBotConnector.ts @@ -103,10 +103,6 @@ export class CSharpBotConnector implements IBotConnector { configList.push('--luis:endpointKey'); configList.push(config.luis.authoringKey); } - if (config.luis.authoringRegion) { - configList.push('--luis:endpoint'); - configList.push(`https://${config.luis.authoringRegion}.api.cognitive.microsoft.com`); - } } return configList; diff --git a/Composer/packages/server/src/types/bf-lu.d.ts b/Composer/packages/server/src/types/bf-lu.d.ts new file mode 100644 index 0000000000..1eff54c908 --- /dev/null +++ b/Composer/packages/server/src/types/bf-lu.d.ts @@ -0,0 +1,35 @@ +/// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +declare module '@bfcomposer/bf-lu/lib/parser/lubuild' { + namespace luBuild { + class Builder { + private readonly handler: (input: string) => any; + + constructor(handler: any); + + loadContents( + files: string[], + culture: string, + suffix: string, + region: string, + stdin?: string + ): { luContents: any[]; recognizers: Map<string, any>; multiRecognizers: Map<string, any>; settings: any }; + + build( + luContents: any[], + recognizers: Map<string, any>, + authoringKey: string, + region: string, + botName: string, + suffix: string, + fallbackLocale: string, + deleteOldVersion: boolean, + multiRecognizers: Map<string, any>, + settings: any + ): any[]; + + writeDialogAssets(contents: any[], force: boolean, out: string): void; + } + } +} diff --git a/Composer/packages/tools/language-servers/language-generation/__tests__/app.test.ts b/Composer/packages/tools/language-servers/language-generation/__tests__/app.test.ts index 980c0de87d..341ea74f63 100644 --- a/Composer/packages/tools/language-servers/language-generation/__tests__/app.test.ts +++ b/Composer/packages/tools/language-servers/language-generation/__tests__/app.test.ts @@ -120,7 +120,7 @@ describe('lg lsp server', () => { it('didChange', async () => { // didChange - const newContent = `${content}-@{G\\r\\n`; + const newContent = `${content}-\${G\\r\\n`; const payload = `{"jsonrpc":"2.0","method":"textDocument/didChange","params":{"textDocument":{"uri":"inmemory://model/1","version":3},"contentChanges":[{"text": "${newContent}"}]}}`; await send(payload, [ response => { diff --git a/Composer/packages/tools/language-servers/language-generation/package.json b/Composer/packages/tools/language-servers/language-generation/package.json index 3d8e889244..8d3cd2032f 100644 --- a/Composer/packages/tools/language-servers/language-generation/package.json +++ b/Composer/packages/tools/language-servers/language-generation/package.json @@ -16,8 +16,8 @@ }, "dependencies": { "@bfc/indexers": "*", - "botbuilder-lg": "4.7.0-preview.93464", - "botframework-expressions": "4.7.0-preview.93464", + "botbuilder-lg": "^4.8.0-preview.106823", + "botframework-expressions": "^4.8.0-preview.106476", "request-light": "^0.2.2", "vscode-languageserver": "^5.3.0-next" }, diff --git a/Composer/packages/tools/language-servers/language-generation/src/LGServer.ts b/Composer/packages/tools/language-servers/language-generation/src/LGServer.ts index 3ee73eb7fd..51511bbed4 100644 --- a/Composer/packages/tools/language-servers/language-generation/src/LGServer.ts +++ b/Composer/packages/tools/language-servers/language-generation/src/LGServer.ts @@ -354,7 +354,7 @@ export class LGServer { } if (char === '{' && i >= 1 && state[state.length - 1] !== SINGLE && state[state.length - 1] !== DOUBLE) { - if (lineContent.charAt(i - 1) === '@') { + if (lineContent.charAt(i - 1) === '$') { state.push(EXPRESSION); } } diff --git a/Composer/packages/tools/language-servers/language-understanding/package.json b/Composer/packages/tools/language-servers/language-understanding/package.json index 62510a16d7..02bc7a28fa 100644 --- a/Composer/packages/tools/language-servers/language-understanding/package.json +++ b/Composer/packages/tools/language-servers/language-understanding/package.json @@ -18,8 +18,8 @@ "start:server": "cd demo && ts-node ./src/server.ts" }, "dependencies": { - "@bfcomposer/bf-lu": "https://botbuilder.myget.org/F/botbuilder-declarative/npm/@bfcomposer/bf-lu/-/@bfcomposer/bf-lu-1.1.8.tgz", "@microsoft/bf-cli-command": "https://botbuilder.myget.org/F/botbuilder-declarative/npm/@microsoft/bf-cli-command/-/@microsoft/bf-cli-command-1.0.0.tgz", + "@bfcomposer/bf-lu": "^1.2.5", "@types/node": "^12.0.4", "express": "^4.15.2", "monaco-editor-core": "^0.17.0", diff --git a/Composer/yarn.lock b/Composer/yarn.lock index be76d64dd2..5673f0b793 100644 --- a/Composer/yarn.lock +++ b/Composer/yarn.lock @@ -2120,32 +2120,10 @@ lodash "^4.17.13" to-fast-properties "^2.0.0" -"@bfcomposer/bf-lu@1.1.2": - version "1.1.2" - resolved "https://botbuilder.myget.org/F/botbuilder-declarative/npm/@bfcomposer/bf-lu/-/@bfcomposer/bf-lu-1.1.2.tgz#0dcd1efa9f839daefe9e2dfc6f4b6db731b701a3" - integrity sha1-Dc0e+p+Dna7+ni38b0tttzG3AaM= - dependencies: - "@microsoft/bf-cli-command" "1.0.0" - "@oclif/command" "~1.5.19" - "@oclif/config" "~1.13.3" - "@oclif/errors" "~1.2.2" - antlr4 "^4.7.2" - chalk "2.4.1" - console-stream "^0.1.1" - deep-equal "^1.0.1" - fs-extra "^8.1.0" - get-stdin "^6.0.0" - intercept-stdout "^0.1.2" - lodash "^4.17.15" - node-fetch "^2.1.2" - semver "^5.5.1" - tslib "^1.10.0" - uuid "~3.3.3" - -"@bfcomposer/bf-lu@^1.1.8", "@bfcomposer/bf-lu@https://botbuilder.myget.org/F/botbuilder-declarative/npm/@bfcomposer/bf-lu/-/@bfcomposer/bf-lu-1.1.8.tgz": - version "1.1.8" - resolved "https://botbuilder.myget.org/F/botbuilder-declarative/npm/@bfcomposer/bf-lu/-/@bfcomposer/bf-lu-1.1.8.tgz#a3c7767de038025157bdc8cf9f56b393f1428fd4" - integrity sha1-o8d2feA4AlFXvcjPn1azk/FCj9Q= +"@bfcomposer/bf-lu@^1.2.5": + version "1.2.5" + resolved "https://botbuilder.myget.org/F/botbuilder-declarative/npm/@bfcomposer/bf-lu/-/@bfcomposer/bf-lu-1.2.5.tgz#4d3707746518d013e19aa6f26467c94342874459" + integrity sha1-TTcHdGUY0BPhmqbyZGfJQ0KHRFk= dependencies: "@azure/cognitiveservices-luis-authoring" "3.0.1" "@azure/ms-rest-azure-js" "2.0.1" @@ -2166,27 +2144,6 @@ semver "^5.5.1" tslib "^1.10.0" -"@bfcomposer/lubuild@1.1.2-preview": - version "1.1.2-preview" - resolved "https://botbuilder.myget.org/F/botbuilder-declarative/npm/@bfcomposer/lubuild/-/@bfcomposer/lubuild-1.1.2-preview.tgz#426e5e109c2d6752745dc363aa824a653a3757f7" - integrity sha1-Qm5eEJwtZ1J0XcNjqoJKZTo3V/c= - dependencies: - "@azure/ms-rest-js" "1.7.0" - "@bfcomposer/bf-lu" "1.1.2" - async-file "^2.0.2" - await-delay "^1.0.0" - chalk "^2.4.2" - cli-position "^1.0.1" - cli-table3 "^0.5.1" - fs-extra "^7.0.1" - latest-version "^5.1.0" - luis-apis "2.5.1" - minimist "^1.2.0" - read-text-file "^1.1.0" - semver "^6.0.0" - username "^4.1.0" - window-size "^1.1.1" - "@bfcomposer/monaco-editor-webpack-plugin@^1.7.2": version "1.7.2" resolved "https://botbuilder.myget.org/F/botbuilder-declarative/npm/@bfcomposer/monaco-editor-webpack-plugin/-/@bfcomposer/monaco-editor-webpack-plugin-1.7.2.tgz#f00ac5cec496dc3d44713d9142956b3336033eab" @@ -2798,11 +2755,6 @@ resolved "https://registry.yarnpkg.com/@sheerun/mutationobserver-shim/-/mutationobserver-shim-0.3.2.tgz#8013f2af54a2b7d735f71560ff360d3a8176a87b" integrity sha512-vTCdPp/T/Q3oSqwHmZ5Kpa9oI7iLtGl3RQaA/NyLHikvcrPxACkkKVr/XzkSPJWXHRhKGzVvb0urJsbMlRxi1Q== -"@sindresorhus/is@^0.14.0": - version "0.14.0" - resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea" - integrity sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ== - "@sindresorhus/is@^0.7.0": version "0.7.0" resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.7.0.tgz#9a06f4f137ee84d7df0460c1fdb1135ffa6c50fd" @@ -2913,13 +2865,6 @@ "@svgr/plugin-svgo" "^4.0.3" loader-utils "^1.1.0" -"@szmarczak/http-timer@^1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-1.1.2.tgz#b1665e2c461a2cd92f4c1bbf50d5454de0d4b421" - integrity sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA== - dependencies: - defer-to-connect "^1.0.1" - "@testing-library/cypress@^5.2.1": version "5.2.1" resolved "https://registry.yarnpkg.com/@testing-library/cypress/-/cypress-5.2.1.tgz#fc9f40fe0de95b1f4c3888a908c432e11a092fe7" @@ -3901,6 +3846,22 @@ acorn@^7.1.0: resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.1.0.tgz#949d36f2c292535da602283586c2477c57eb2d6c" integrity sha512-kL5CuoXA/dgxlBbVrflsflzQ3PAas7RYZB52NOm/6839iVYJgKMJ3cQJD+t2i5+qFa8h3MDpEOJiS64E8JLnSQ== +adaptive-expressions@4.8.0-preview.106823: + version "4.8.0-preview.106823" + resolved "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/adaptive-expressions/-/adaptive-expressions-4.8.0-preview.106823.tgz#f5fe72797fd0c2a4eb12afaab85da0756e0ae4b5" + integrity sha1-9f5yeX/QwqTrEq+quF2gdW4K5LU= + dependencies: + "@microsoft/recognizers-text-data-types-timex-expression" "^1.1.4" + "@types/lru-cache" "^5.1.0" + "@types/moment-timezone" "^0.5.12" + "@types/xmldom" "^0.1.29" + antlr4ts "0.5.0-alpha.1" + jspath "^0.4.0" + lodash "^4.17.15" + lru-cache "^5.1.1" + moment "2.24.0" + moment-timezone "^0.5.25" + address@1.0.3, address@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/address/-/address-1.0.3.tgz#b5f50631f8d6cec8bd20c963963afb55e06cbce9" @@ -4285,13 +4246,6 @@ async-each@^1.0.1: resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.2.tgz#8b8a7ca2a658f927e9f307d6d1a42f4199f0f735" integrity sha512-6xrbvN0MOBKSJDdonmSSz2OwFSgxRaVtBDes26mj9KIGtDo+g9xosFRSC+i1gQh2oAN/tQ62AI/pGZGQjVOiRg== -async-file@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/async-file/-/async-file-2.0.2.tgz#02ad07856ac3717e836b20aec5a4cfe00c46df23" - integrity sha1-Aq0HhWrDcX6DayCuxaTP4AxG3yM= - dependencies: - rimraf "^2.5.2" - async-hook-jl@^1.7.6: version "1.7.6" resolved "https://registry.yarnpkg.com/async-hook-jl/-/async-hook-jl-1.7.6.tgz#4fd25c2f864dbaf279c610d73bf97b1b28595e68" @@ -4817,65 +4771,32 @@ boolbase@^1.0.0, boolbase@~1.0.0: resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24= -botbuilder-core@4.7.0-preview.93464: - version "4.7.0-preview.93464" - resolved "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/botbuilder-core/-/botbuilder-core-4.7.0-preview.93464.tgz#31b3a4d3c87ed82f89b993eebea684708eaca1d6" - integrity sha1-MbOk08h+2C+JuZPuvqaEcI6sodY= - dependencies: - assert "^1.4.1" - botframework-schema "4.7.0-preview.93464" - -botbuilder-core@4.8.0-preview.97252: - version "4.8.0-preview.97252" - resolved "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/botbuilder-core/-/botbuilder-core-4.8.0-preview.97252.tgz#eed85ff5c7136e5d6187d6627a27daf0f4187443" - integrity sha1-7thf9ccTbl1hh9Zieifa8PQYdEM= +botbuilder-core@4.8.0-preview.106823: + version "4.8.0-preview.106823" + resolved "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/botbuilder-core/-/botbuilder-core-4.8.0-preview.106823.tgz#ab1dec97e67ba0504669cc0eec4741a531d5b687" + integrity sha1-qx3sl+Z7oFBGacwO7EdBpTHVtoc= dependencies: assert "^1.4.1" - botframework-schema "4.8.0-preview.97252" - -botbuilder-lg@4.7.0-preview.93464: - version "4.7.0-preview.93464" - resolved "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/botbuilder-lg/-/botbuilder-lg-4.7.0-preview.93464.tgz#504c647aca501998a0bcf89da33612c808334825" - integrity sha1-UExkespQGZigvPidozYSyAgzSCU= - dependencies: - antlr4ts "0.5.0-alpha.1" - botbuilder-core "4.7.0-preview.93464" - botframework-expressions "4.7.0-preview.93464" - lodash "^4.17.11" - uuid "^3.3.3" + botframework-schema "4.8.0-preview.106823" -botbuilder-lg@^4.8.0-preview.97252: - version "4.8.0-preview.97252" - resolved "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/botbuilder-lg/-/botbuilder-lg-4.8.0-preview.97252.tgz#2bd92cf0009bae0de77d6695c206960b8028c40e" - integrity sha1-K9ks8ACbrg3nfWaVwgaWC4AoxA4= +botbuilder-lg@^4.8.0-preview.106823: + version "4.8.0-preview.106823" + resolved "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/botbuilder-lg/-/botbuilder-lg-4.8.0-preview.106823.tgz#9b144401c75493243def5c3c3347021c60dbbbbd" + integrity sha1-mxREAcdUkyQ971w8M0cCHGDbu70= dependencies: + adaptive-expressions "4.8.0-preview.106823" antlr4ts "0.5.0-alpha.1" - botbuilder-core "4.8.0-preview.97252" - botframework-expressions "4.8.0-preview.97252" + botbuilder-core "4.8.0-preview.106823" lodash "^4.17.11" uuid "^3.3.3" -botframework-expressions@4.7.0-preview.93464: - version "4.7.0-preview.93464" - resolved "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/botframework-expressions/-/botframework-expressions-4.7.0-preview.93464.tgz#576e6d48f7dd1c615d23894de758b7b5b4afc505" - integrity sha1-V25tSPfdHGFdI4lN51i3tbSvxQU= - dependencies: - "@microsoft/recognizers-text-data-types-timex-expression" "^1.1.4" - "@types/moment-timezone" "^0.5.12" - "@types/xmldom" "^0.1.29" - antlr4ts "0.5.0-alpha.1" - jspath "^0.4.0" - lodash "^4.17.15" - lru-cache "^5.1.1" - moment "2.24.0" - moment-timezone "^0.5.25" - -botframework-expressions@4.8.0-preview.97252: - version "4.8.0-preview.97252" - resolved "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/botframework-expressions/-/botframework-expressions-4.8.0-preview.97252.tgz#ea650dafd11b58eaceb735893d330454a5bd0495" - integrity sha1-6mUNr9EbWOrOtzWJPTMEVKW9BJU= +botframework-expressions@^4.8.0-preview.106476: + version "4.8.0-preview.106476" + resolved "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/botframework-expressions/-/botframework-expressions-4.8.0-preview.106476.tgz#984c155efbac673a0ad9945220691040b93996f9" + integrity sha1-mEwVXvusZzoK2ZRSIGkQQLk5lvk= dependencies: "@microsoft/recognizers-text-data-types-timex-expression" "^1.1.4" + "@types/lru-cache" "^5.1.0" "@types/moment-timezone" "^0.5.12" "@types/xmldom" "^0.1.29" antlr4ts "0.5.0-alpha.1" @@ -4885,15 +4806,10 @@ botframework-expressions@4.8.0-preview.97252: moment "2.24.0" moment-timezone "^0.5.25" -botframework-schema@4.7.0-preview.93464: - version "4.7.0-preview.93464" - resolved "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/botframework-schema/-/botframework-schema-4.7.0-preview.93464.tgz#ff6d11cde4e57e12632d18855e9ba31b541f9583" - integrity sha1-/20RzeTlfhJjLRiFXpujG1QflYM= - -botframework-schema@4.8.0-preview.97252: - version "4.8.0-preview.97252" - resolved "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/botframework-schema/-/botframework-schema-4.8.0-preview.97252.tgz#123ca59efd8efba94d2e339f9b571fd1969ef705" - integrity sha1-Ejylnv2O+6lNLjOfm1cf0Zae9wU= +botframework-schema@4.8.0-preview.106823: + version "4.8.0-preview.106823" + resolved "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/botframework-schema/-/botframework-schema-4.8.0-preview.106823.tgz#e9766d34c2e031e90d0950c51352c3848e368102" + integrity sha1-6XZtNMLgMekNCVDFE1LDhI42gQI= boxen@^1.2.1: version "1.3.0" @@ -5239,19 +5155,6 @@ cacheable-request@^2.1.1: normalize-url "2.0.1" responselike "1.0.2" -cacheable-request@^6.0.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-6.1.0.tgz#20ffb8bd162ba4be11e9567d823db651052ca912" - integrity sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg== - dependencies: - clone-response "^1.0.2" - get-stream "^5.1.0" - http-cache-semantics "^4.0.0" - keyv "^3.0.0" - lowercase-keys "^2.0.0" - normalize-url "^4.1.0" - responselike "^1.0.2" - cachedir@1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/cachedir/-/cachedir-1.3.0.tgz#5e01928bf2d95b5edd94b0942188246740e0dbc4" @@ -5689,7 +5592,7 @@ clone-deep@^0.2.4: lazy-cache "^1.0.3" shallow-clone "^0.1.2" -clone-response@1.0.2, clone-response@^1.0.2: +clone-response@1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.2.tgz#d1dc973920314df67fbeb94223b4ee350239e96b" integrity sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws= @@ -7208,11 +7111,6 @@ default-require-extensions@^2.0.0: dependencies: strip-bom "^3.0.0" -defer-to-connect@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-1.0.2.tgz#4bae758a314b034ae33902b5aac25a8dd6a8633e" - integrity sha512-k09hcQcTDY+cwgiwa6PYKLm3jlagNzQ+RSvhjzESOGOx+MNOuXkxTfEvPrO1IOQ81tArCFYQgi631clB70RpQw== - define-properties@^1.1.2, define-properties@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" @@ -8894,20 +8792,13 @@ get-stream@3.0.0, get-stream@^3.0.0: resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" integrity sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ= -get-stream@^4.0.0, get-stream@^4.1.0: +get-stream@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w== dependencies: pump "^3.0.0" -get-stream@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.1.0.tgz#01203cdc92597f9b909067c3e656cc1f4d3c4dc9" - integrity sha512-EXr1FOzrzTfGeL0gQdeFEvOMm2mzMOglyiOXSTpPC+iAjAKftbr3jpCMWynogwYnM+eSj9sHGc6wjIcDvYiygw== - dependencies: - pump "^3.0.0" - get-value@^2.0.3, get-value@^2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" @@ -9133,23 +9024,6 @@ got@^8.3.1: url-parse-lax "^3.0.0" url-to-options "^1.0.1" -got@^9.6.0: - version "9.6.0" - resolved "https://registry.yarnpkg.com/got/-/got-9.6.0.tgz#edf45e7d67f99545705de1f7bbeeeb121765ed85" - integrity sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q== - dependencies: - "@sindresorhus/is" "^0.14.0" - "@szmarczak/http-timer" "^1.1.2" - cacheable-request "^6.0.0" - decompress-response "^3.3.0" - duplexer3 "^0.1.4" - get-stream "^4.1.0" - lowercase-keys "^1.0.1" - mimic-response "^1.0.1" - p-cancelable "^1.0.0" - to-readable-stream "^1.0.0" - url-parse-lax "^3.0.0" - graceful-fs@^4.1.0, graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6: version "4.1.15" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.15.tgz#ffb703e1066e8a0eeaa4c8b80ba9253eeefbfb00" @@ -9489,11 +9363,6 @@ http-cache-semantics@3.8.1: resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-3.8.1.tgz#39b0e16add9b605bf0a9ef3d9daaf4843b4cacd2" integrity sha512-5ai2iksyV8ZXmnZhHH4rWPoxxistEexSi5936zIQ1bnNTW5VnA85B6P/VpXiRM017IgRvb2kKo1a//y+0wSp3w== -http-cache-semantics@^4.0.0: - version "4.0.3" - resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.0.3.tgz#495704773277eeef6e43f9ab2c2c7d259dda25c5" - integrity sha512-TcIMG3qeVLgDr1TEd2XvHaTnMPwYQUQMIBLy+5pLSDKYFc7UIqj39w8EGzZkaxoLv/l2K8HaI0t5AVA+YYgUew== - http-deceiver@^1.2.7: version "1.2.7" resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87" @@ -11042,13 +10911,6 @@ keyv@3.0.0: dependencies: json-buffer "3.0.0" -keyv@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/keyv/-/keyv-3.1.0.tgz#ecc228486f69991e49e9476485a5be1e8fc5c4d9" - integrity sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA== - dependencies: - json-buffer "3.0.0" - killable@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/killable/-/killable-1.0.1.tgz#4c8ce441187a061c7474fb87ca08e2a638194892" @@ -11112,13 +10974,6 @@ latest-version@^4.0.0: dependencies: package-json "^5.0.0" -latest-version@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-5.1.0.tgz#119dfe908fe38d15dfa43ecd13fa12ec8832face" - integrity sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA== - dependencies: - package-json "^6.3.0" - lazy-ass@1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/lazy-ass/-/lazy-ass-1.6.0.tgz#7999655e8646c17f089fdd187d150d3324d54513" @@ -11620,16 +11475,11 @@ lowercase-keys@1.0.0: resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.0.tgz#4e3366b39e7f5457e35f1324bdf6f88d0bfc7306" integrity sha1-TjNms55/VFfjXxMkvfb4jQv8cwY= -lowercase-keys@^1.0.0, lowercase-keys@^1.0.1: +lowercase-keys@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f" integrity sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA== -lowercase-keys@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479" - integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA== - lru-cache@^4.0.1: version "4.1.5" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd" @@ -11953,7 +11803,7 @@ mimic-fn@^2.1.0: resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== -mimic-response@^1.0.0, mimic-response@^1.0.1: +mimic-response@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b" integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ== @@ -12506,11 +12356,6 @@ normalize-url@^3.0.0: resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-3.3.0.tgz#b2e1c4dc4f7c6d57743df733a4f5978d18650559" integrity sha512-U+JJi7duF1o+u2pynbp2zXDW2/PADgC30f0GsHZtRh+HOcXHnw137TrNlyxxRvWW5fjKd3bcLHPxofWuCjaeZg== -normalize-url@^4.1.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.3.0.tgz#9c49e10fc1876aeb76dba88bf1b2b5d9fa57b2ee" - integrity sha512-0NLtR71o4k6GLP+mr6Ty34c5GA6CMoEsncKJxvQd8NzPxaHRJNnb5gZE8R1XF4CPIS7QPHLJ74IFszwtNVAHVQ== - normalize-url@^4.5.0: version "4.5.0" resolved "https://botbuilder.myget.org/F/botbuilder-declarative/npm/normalize-url/-/normalize-url-4.5.0.tgz#453354087e6ca96957bd8f5baf753f5982142129" @@ -12911,11 +12756,6 @@ p-cancelable@^0.4.0: resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-0.4.1.tgz#35f363d67d52081c8d9585e37bcceb7e0bbcb2a0" integrity sha512-HNa1A8LvB1kie7cERyy21VNeHb2CWJJYqyyC2o3klWFfMGlFmWv2Z7sFgZH8ZiaYL95ydToKTFVXgMV/Os0bBQ== -p-cancelable@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-1.1.0.tgz#d078d15a3af409220c886f1d9a0ca2e441ab26cc" - integrity sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw== - p-defer@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/p-defer/-/p-defer-1.0.0.tgz#9f6eb182f6c9aa8cd743004a7d4f96b196b0fb0c" @@ -13051,16 +12891,6 @@ package-json@^5.0.0: registry-url "^3.1.0" semver "^5.5.0" -package-json@^6.3.0: - version "6.4.0" - resolved "https://registry.yarnpkg.com/package-json/-/package-json-6.4.0.tgz#4f626976604f4a9a41723ce1792b204a60b1b61e" - integrity sha512-bd1T8OBG7hcvMd9c/udgv6u5v9wISP3Oyl9Cm7Weop8EFwrtcQDnS2sb6zhwqus2WslSr5wSTIPiTTpxxmPm7Q== - dependencies: - got "^9.6.0" - registry-auth-token "^3.4.0" - registry-url "^5.0.0" - semver "^6.1.1" - pako@~1.0.5: version "1.0.10" resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.10.tgz#4328badb5086a426aa90f541977d4955da5c9732" @@ -14383,7 +14213,7 @@ raw-body@2.4.0: iconv-lite "0.4.24" unpipe "1.0.0" -rc@^1.0.1, rc@^1.1.6, rc@^1.2.7, rc@^1.2.8: +rc@^1.0.1, rc@^1.1.6, rc@^1.2.7: version "1.2.8" resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== @@ -14762,7 +14592,7 @@ regexpu-core@^4.6.0: unicode-match-property-ecmascript "^1.0.4" unicode-match-property-value-ecmascript "^1.1.0" -registry-auth-token@^3.0.1, registry-auth-token@^3.3.2, registry-auth-token@^3.4.0: +registry-auth-token@^3.0.1, registry-auth-token@^3.3.2: version "3.4.0" resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-3.4.0.tgz#d7446815433f5d5ed6431cd5dca21048f66b397e" integrity sha512-4LM6Fw8eBQdwMYcES4yTnn2TqIasbXuwDx3um+QRs7S55aMKCBKBxvPXl2RiUjHwuJLTyYfxSpmfSAjQpcuP+A== @@ -14777,13 +14607,6 @@ registry-url@^3.0.3, registry-url@^3.1.0: dependencies: rc "^1.0.1" -registry-url@^5.0.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/registry-url/-/registry-url-5.1.0.tgz#e98334b50d5434b81136b44ec638d9c2009c5009" - integrity sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw== - dependencies: - rc "^1.2.8" - regjsgen@^0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.5.0.tgz#a7634dc08f89209c2049adda3525711fb97265dd" @@ -15002,7 +14825,7 @@ resolve@^1.13.1: dependencies: path-parse "^1.0.6" -responselike@1.0.2, responselike@^1.0.2: +responselike@1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/responselike/-/responselike-1.0.2.tgz#918720ef3b631c5642be068f15ade5a46f4ba1e7" integrity sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec= @@ -15053,7 +14876,7 @@ rgba-regex@^1.0.0: resolved "https://registry.yarnpkg.com/rgba-regex/-/rgba-regex-1.0.0.tgz#43374e2e2ca0968b0ef1523460b7d730ff22eeb3" integrity sha1-QzdOLiyglosO8VI0YLfXMP8i7rM= -rimraf@2.6.3, rimraf@^2.2.8, rimraf@^2.5.2, rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.2, rimraf@^2.6.3, rimraf@~2.6.2: +rimraf@2.6.3, rimraf@^2.2.8, rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.2, rimraf@^2.6.3, rimraf@~2.6.2: version "2.6.3" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab" integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA== @@ -15224,7 +15047,7 @@ semver@^6.0.0: resolved "https://registry.yarnpkg.com/semver/-/semver-6.0.0.tgz#05e359ee571e5ad7ed641a6eec1e547ba52dea65" integrity sha512-0UewU+9rFapKFnlbirLi3byoOuhrSsli/z/ihNnvM24vgF+8sNBiI1LZPBSH9wJKUwaUbw+s3hToDLCXkrghrQ== -semver@^6.1.0, semver@^6.1.1: +semver@^6.1.0: version "6.1.1" resolved "https://registry.yarnpkg.com/semver/-/semver-6.1.1.tgz#53f53da9b30b2103cd4f15eab3a18ecbcb210c9b" integrity sha512-rWYq2e5iYW+fFe/oPPtYJxYgjBm8sC4rmoGdUOgBB7VnwKt6HrL793l2voH1UlsyYZpJ4g0wfjnTEO1s1NP2eQ== @@ -16360,11 +16183,6 @@ to-object-path@^0.3.0: dependencies: kind-of "^3.0.2" -to-readable-stream@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/to-readable-stream/-/to-readable-stream-1.0.0.tgz#ce0aa0c2f3df6adf852efb404a783e77c0475771" - integrity sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q== - to-regex-range@^2.1.0: version "2.1.1" resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-2.1.1.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38" @@ -16931,14 +16749,6 @@ use@^3.1.0: resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ== -username@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/username/-/username-4.1.0.tgz#640f2ae13d17c51e7fb1d3517ad7c17fcd5d1670" - integrity sha512-sKh1KCsMfv8jPIC9VdeQhrNAgkl842jS/M74HQv7Byr0AMAwKZt8mLWX9DmtMeD8nQA3eKa10f5LbqlSVmokMg== - dependencies: - execa "^1.0.0" - mem "^4.0.0" - util-deprecate@^1.0.1, util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" @@ -16981,7 +16791,7 @@ uuid@^3.0.0, uuid@^3.0.1, uuid@^3.2.1, uuid@^3.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA== -uuid@^3.3.3, uuid@~3.3.3: +uuid@^3.3.3: version "3.3.3" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.3.tgz#4568f0216e78760ee1dbf3a4d2cf53e224112866" integrity sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ== @@ -17598,7 +17408,7 @@ widest-line@^2.0.0, widest-line@^2.0.1: dependencies: string-width "^2.1.1" -window-size@^1.1.0, window-size@^1.1.1: +window-size@^1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/window-size/-/window-size-1.1.1.tgz#9858586580ada78ab26ecd6978a6e03115c1af20" integrity sha512-5D/9vujkmVQ7pSmc0SCBmHXbkv6eaHwXEx65MywhmUMsI8sGqJ972APq1lotfcwMKPFLuCFfL8xGHLIp7jaBmA== From aed7c658c1236fe844f90d1d2e332dd31567a37d Mon Sep 17 00:00:00 2001 From: Long Alan <julong@microsoft.com> Date: Tue, 3 Mar 2020 12:36:01 +0800 Subject: [PATCH 14/40] feat: repaint ui for setProperties in visual editor (#2017) * elements interface * styled component * use styledComponent in visual editor * refactor * add styled package * module name * revert uischema & refactor itemOverview component * ui adjust * rename & delete needless code * truncate text * copy formcard to listoverviewcard for choiceinput and setProperties * refactor code * delete listOverviewWidget * css * fix some bug Co-authored-by: Andy Brown <asbrown002@gmail.com> --- .../__tests__/widgets/ChoiceInput.test.tsx | 84 ------------- .../extensions/visual-designer/package.json | 3 +- .../src/components/common/ListOverview.tsx | 39 ++++++ .../components/elements/styledComponents.tsx | 64 ++++++++++ .../elements/styledComponents.types.ts | 15 +++ .../src/constants/ElementSizes.ts | 8 ++ .../src/layouters/measureJsonBoundary.ts | 20 +++ .../visual-designer/src/schema/uischema.tsx | 48 ++++++- .../src/widgets/ChoiceInput.tsx | 93 -------------- .../src/widgets/DialogRefCard.tsx | 6 +- Composer/yarn.lock | 118 ++++++++++++++++++ 11 files changed, 314 insertions(+), 184 deletions(-) delete mode 100644 Composer/packages/extensions/visual-designer/__tests__/widgets/ChoiceInput.test.tsx create mode 100644 Composer/packages/extensions/visual-designer/src/components/common/ListOverview.tsx create mode 100644 Composer/packages/extensions/visual-designer/src/components/elements/styledComponents.tsx create mode 100644 Composer/packages/extensions/visual-designer/src/components/elements/styledComponents.types.ts delete mode 100644 Composer/packages/extensions/visual-designer/src/widgets/ChoiceInput.tsx diff --git a/Composer/packages/extensions/visual-designer/__tests__/widgets/ChoiceInput.test.tsx b/Composer/packages/extensions/visual-designer/__tests__/widgets/ChoiceInput.test.tsx deleted file mode 100644 index c7f44f0458..0000000000 --- a/Composer/packages/extensions/visual-designer/__tests__/widgets/ChoiceInput.test.tsx +++ /dev/null @@ -1,84 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -import React from 'react'; -import { render, cleanup } from 'react-testing-library'; - -import { ChoiceInput } from '../../src/widgets/ChoiceInput'; -import { ObiTypes } from '../../src/constants/ObiTypes'; - -describe('<ChoiceInput />', () => { - let renderResult, id, data, onEvent, onResize; - - beforeEach(() => { - onEvent = jest.fn(); - onResize = jest.fn(); - id = 'choiceInput'; - }); - afterEach(cleanup); - - it("should have no choice dom when data's choice is null", () => { - data = { - $type: ObiTypes.ChoiceInput, - choices: null, - }; - - renderResult = render(<ChoiceInput id={id} data={data} onEvent={onEvent} onResize={onResize} />); - - const { queryByTestId } = renderResult; - expect(queryByTestId('ChoiceInput')).toBeNull(); - }); - - it("should show as many choice items as there are when data's choices length is less than 4", () => { - data = { - $type: ObiTypes.ChoiceInput, - choices: [ - { - value: '1', - }, - { - value: '2', - }, - { - value: '3', - }, - ], - }; - - renderResult = render(<ChoiceInput id={id} data={data} onEvent={onEvent} onResize={onResize} />); - - const { getAllByRole } = renderResult; - const choices = getAllByRole('choice'); - - expect(choices).toHaveLength(3); - }); - - it("should show three choice items and 'more' dom when data's choices length is more than 3", () => { - data = { - $type: ObiTypes.ChoiceInput, - choices: [ - { - value: '1', - }, - { - value: '2', - }, - { - value: '3', - }, - { - value: '4', - }, - ], - }; - - renderResult = render(<ChoiceInput id={id} data={data} onEvent={onEvent} onResize={onResize} />); - - const { getAllByRole, getByTestId } = renderResult; - const choices = getAllByRole('choice'); - const hasMore = getByTestId('hasMore'); - - expect(choices).toHaveLength(3); - expect(hasMore).toBeTruthy(); - }); -}); diff --git a/Composer/packages/extensions/visual-designer/package.json b/Composer/packages/extensions/visual-designer/package.json index acb0e94a99..736dba2a2a 100644 --- a/Composer/packages/extensions/visual-designer/package.json +++ b/Composer/packages/extensions/visual-designer/package.json @@ -25,7 +25,8 @@ }, "dependencies": { "@bfc/shared": "*", - "@emotion/core": "^10.0.7", + "@emotion/core": "^10.0.27", + "@emotion/styled": "^10.0.27", "@types/react": "16.9.0", "classnames": "^2.2.6", "create-react-class": "^15.6.3", diff --git a/Composer/packages/extensions/visual-designer/src/components/common/ListOverview.tsx b/Composer/packages/extensions/visual-designer/src/components/common/ListOverview.tsx new file mode 100644 index 0000000000..ef90a437fe --- /dev/null +++ b/Composer/packages/extensions/visual-designer/src/components/common/ListOverview.tsx @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import React, { FC } from 'react'; + +import { SingleLineDiv } from '../elements/styledComponents'; + +export interface ListOverviewProps { + items: string[]; + renderItem: (item: object | string) => JSX.Element; + maxCount: number; +} +export const ListOverview: FC<ListOverviewProps> = ({ items, renderItem, maxCount }) => { + if (!Array.isArray(items)) { + return null; + } + return ( + <div style={{ padding: '0 0 8px 8px' }}> + {items.slice(0, maxCount).map((item, index) => { + return ( + <div style={{ marginTop: '8px' }} key={index}> + {renderItem(item)} + </div> + ); + })} + {items.length > maxCount ? ( + <SingleLineDiv + data-testid="hasMore" + style={{ + marginTop: '8px', + textAlign: 'center', + }} + > + {`${items.length - maxCount} more`} + </SingleLineDiv> + ) : null} + </div> + ); +}; diff --git a/Composer/packages/extensions/visual-designer/src/components/elements/styledComponents.tsx b/Composer/packages/extensions/visual-designer/src/components/elements/styledComponents.tsx new file mode 100644 index 0000000000..f4afc5467f --- /dev/null +++ b/Composer/packages/extensions/visual-designer/src/components/elements/styledComponents.tsx @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { css } from '@emotion/core'; +import styled from '@emotion/styled'; +import { Link } from 'office-ui-fabric-react/lib/Link'; + +import { ObiColors } from '../../constants/ElementColors'; + +import { MultiLineDivProps, DivProps } from './styledComponents.types'; + +const dynamicStyle = props => + css` + color: ${props.color || ObiColors.Black}; + `; + +export const LinkBtn = styled(Link)(props => ({ + color: props.color || ObiColors.AzureBlue, +})); + +export const Span = styled.span` + ${dynamicStyle} +`; + +export const BorderedDiv = styled.div<DivProps>(props => ({ + color: props.color || ObiColors.Black, + width: props.width, + height: props.height, + padding: '2px 0 0 8px', + whiteSpace: 'nowrap', + textOverflow: 'ellipsis', + overflow: 'hidden', + fontFamily: 'Segoe UI', + fontSize: '12px', + lineHeight: '14px', + border: '1px solid #C4C4C4', + boxSizing: 'border-box', +})); + +export const MultiLineDiv = styled.div<MultiLineDivProps>(props => ({ + color: props.color || ObiColors.Black, + fontSize: '12px', + height: `${(props.lineNum || 1) * 19}px`, + lineHeight: '19px', + fontFamily: 'Segoe UI', + overflow: 'hidden', + textOverflow: 'ellipsis', + wordBreak: 'break-word', + display: '-webkit-box', + '-webkit-line-clamp': `${props.lineNum || 1}`, + '-webkit-box-orient': 'vertical', +})); + +export const SingleLineDiv = styled.div<DivProps>(props => ({ + width: props.width || 150, + height: props.height || '19px', + color: props.color || ObiColors.Black, + fontSize: '12px', + whiteSpace: 'nowrap', + textOverflow: 'ellipsis', + overflow: 'hidden', + lineHeight: '19px', + fontFamily: 'Segoe UI', +})); diff --git a/Composer/packages/extensions/visual-designer/src/components/elements/styledComponents.types.ts b/Composer/packages/extensions/visual-designer/src/components/elements/styledComponents.types.ts new file mode 100644 index 0000000000..dd6d1680e8 --- /dev/null +++ b/Composer/packages/extensions/visual-designer/src/components/elements/styledComponents.types.ts @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { DetailedHTMLProps, HTMLAttributes, ComponentClass, FC } from 'react'; + +export type ElementProps = DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>; +export interface DivProps extends ElementProps { + width?: number; + height?: number; +} + +export interface MultiLineDivProps extends DivProps { + lineNum?: number; +} +export type ElementComponent<T extends ElementProps> = FC<T> | ComponentClass<T, any>; diff --git a/Composer/packages/extensions/visual-designer/src/constants/ElementSizes.ts b/Composer/packages/extensions/visual-designer/src/constants/ElementSizes.ts index 1ec57683df..4800cf9a5c 100644 --- a/Composer/packages/extensions/visual-designer/src/constants/ElementSizes.ts +++ b/Composer/packages/extensions/visual-designer/src/constants/ElementSizes.ts @@ -39,6 +39,14 @@ export const ChoiceInputSize = { export const ChoiceInputMarginTop = 8; export const ChoiceInputMarginBottom = 10; +export const PropertyAssignmentSize = { + width: 155, + height: 16, +}; + +export const AssignmentMarginTop = 8; +export const AssignmentMarginBottom = 8; + export const EventNodeSize = { width: 240, height: 125, diff --git a/Composer/packages/extensions/visual-designer/src/layouters/measureJsonBoundary.ts b/Composer/packages/extensions/visual-designer/src/layouters/measureJsonBoundary.ts index cbea2e38e3..17c9f34b51 100644 --- a/Composer/packages/extensions/visual-designer/src/layouters/measureJsonBoundary.ts +++ b/Composer/packages/extensions/visual-designer/src/layouters/measureJsonBoundary.ts @@ -13,6 +13,9 @@ import { ChoiceInputMarginTop, ChoiceInputMarginBottom, IconBrickSize, + AssignmentMarginTop, + PropertyAssignmentSize, + AssignmentMarginBottom, } from '../constants/ElementSizes'; import { transformIfCondtion } from '../transformers/transformIfCondition'; import { transformSwitchCondition } from '../transformers/transformSwitchCondition'; @@ -75,6 +78,20 @@ export function measureChoiceInputDetailBoundary(data): Boundary { return new Boundary(width, height); } +export function measurePropertyAssignmentBoundary(data): Boundary { + const width = InitNodeSize.width; + const height = Math.max( + InitNodeSize.height / 2 + + (data.assignments && Array.isArray(data.assignments) + ? (data.assignments.length < 4 + ? data.assignments.length * (PropertyAssignmentSize.height + AssignmentMarginTop) + : 4 * (PropertyAssignmentSize.height + AssignmentMarginTop)) + AssignmentMarginBottom + : 0), + InitNodeSize.height + ); + return new Boundary(width, height); +} + function measureBaseInputBoundary(data): Boundary { const { botAsks, userAnswers } = transformBaseInput(data, ''); return calculateBaseInputBoundary(measureJsonBoundary(botAsks.json), measureJsonBoundary(userAnswers.json)); @@ -126,6 +143,9 @@ export function measureJsonBoundary(json): Boundary { case ObiTypes.InvalidPromptBrick: boundary = new Boundary(IconBrickSize.width, IconBrickSize.height); break; + case ObiTypes.SetProperties: + boundary = measurePropertyAssignmentBoundary(json); + break; case ObiTypes.EndDialog: case ObiTypes.EndTurn: case ObiTypes.RepeatDialog: diff --git a/Composer/packages/extensions/visual-designer/src/schema/uischema.tsx b/Composer/packages/extensions/visual-designer/src/schema/uischema.tsx index e0ad27eb69..34b36837df 100644 --- a/Composer/packages/extensions/visual-designer/src/schema/uischema.tsx +++ b/Composer/packages/extensions/visual-designer/src/schema/uischema.tsx @@ -13,10 +13,12 @@ import { PromptWidget } from '../widgets/PromptWidget'; import { IfConditionWidget } from '../widgets/IfConditionWidget'; import { SwitchConditionWidget } from '../widgets/SwitchConditionWidget'; import { ForeachWidget } from '../widgets/ForeachWidget'; -import { ChoiceInputChoices } from '../widgets/ChoiceInput'; import { ActionHeader } from '../widgets/ActionHeader'; import { ElementIcon } from '../utils/obiPropertyResolver'; import { ObiColors } from '../constants/ElementColors'; +import { SingleLineDiv, BorderedDiv } from '../components/elements/styledComponents'; +import { PropertyAssignmentSize, ChoiceInputSize } from '../constants/ElementSizes'; +import { ListOverview } from '../components/common/ListOverview'; import { UISchema, UIWidget } from './uischema.types'; @@ -40,7 +42,6 @@ const BaseInputSchema: UIWidget = { icon: ElementIcon.User, menu: 'none', content: data => data.property || '<property>', - children: data => (data.$type === SDKTypes.ChoiceInput ? <ChoiceInputChoices choices={data.choices} /> : null), colors: { theme: ObiColors.LightBlue, icon: ObiColors.AzureBlue, @@ -48,6 +49,31 @@ const BaseInputSchema: UIWidget = { }, }; +const ChoiceInputSchema: UIWidget = Object.assign({}, BaseInputSchema, { + userInput: { + 'ui:widget': ActionCard, + title: data => `User Input (${getInputType(data.$type)})`, + disableSDKTitle: true, + icon: ElementIcon.User, + menu: 'none', + content: data => <SingleLineDiv>{data.property || '<property>'}</SingleLineDiv>, + children: data => { + const renderItem = item => { + const content = item.value; + return ( + <BorderedDiv width={ChoiceInputSize.width} height={ChoiceInputSize.height} title={content}> + {content} + </BorderedDiv> + ); + }; + return <ListOverview items={data.choices} renderItem={renderItem} maxCount={3} />; + }, + colors: { + theme: ObiColors.LightBlue, + icon: ObiColors.AzureBlue, + }, + }, +}); export const uiSchema: UISchema = { default: { 'ui:widget': ActionCard, @@ -102,7 +128,7 @@ export const uiSchema: UISchema = { [SDKTypes.DateTimeInput]: BaseInputSchema, [SDKTypes.NumberInput]: BaseInputSchema, [SDKTypes.TextInput]: BaseInputSchema, - [SDKTypes.ChoiceInput]: BaseInputSchema, + [SDKTypes.ChoiceInput]: ChoiceInputSchema, [SDKTypes.BeginDialog]: { 'ui:widget': DialogRefCard, dialog: data => data.dialog, @@ -123,6 +149,22 @@ export const uiSchema: UISchema = { [SDKTypes.SetProperties]: { 'ui:widget': ActionCard, content: data => `Set ${Array.isArray(data.assignments) ? data.assignments.length : 0} property values`, + children: data => { + const renderItem = item => { + const content = `${item.property} = ${item.value}`; + return ( + <SingleLineDiv + width={PropertyAssignmentSize.width} + height={PropertyAssignmentSize.height} + title={content} + style={{ paddingLeft: '3px' }} + > + {content} + </SingleLineDiv> + ); + }; + return <ListOverview items={data.assignments} renderItem={renderItem} maxCount={3} />; + }, }, [SDKTypes.DeleteProperty]: { 'ui:widget': ActionCard, diff --git a/Composer/packages/extensions/visual-designer/src/widgets/ChoiceInput.tsx b/Composer/packages/extensions/visual-designer/src/widgets/ChoiceInput.tsx deleted file mode 100644 index b225f17ebd..0000000000 --- a/Composer/packages/extensions/visual-designer/src/widgets/ChoiceInput.tsx +++ /dev/null @@ -1,93 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -/** @jsx jsx */ -import { jsx } from '@emotion/core'; -import { FC } from 'react'; -import { DialogGroup, PromptTab } from '@bfc/shared'; - -import { ChoiceInputSize, ChoiceInputMarginTop } from '../constants/ElementSizes'; -import { NodeEventTypes } from '../constants/NodeEventTypes'; -import { NodeColors } from '../constants/ElementColors'; -import { measureJsonBoundary } from '../layouters/measureJsonBoundary'; -import { ElementIcon } from '../utils/obiPropertyResolver'; -import { FormCard } from '../components/nodes/templates/FormCard'; -import { NodeProps } from '../components/nodes/nodeProps'; -import { getUserAnswersTitle } from '../components/nodes/utils'; - -export const ChoiceInputChoices = ({ choices }) => { - if (!Array.isArray(choices)) { - return null; - } - - return ( - <div data-testid="ChoiceInput" css={{ padding: '0 0 16px 29px' }}> - {choices.map(({ value }, index) => { - if (index < 3) { - return ( - <div - key={index} - role="choice" - css={{ - height: ChoiceInputSize.height, - width: ChoiceInputSize.width, - marginTop: ChoiceInputMarginTop, - paddingLeft: '7px', - whiteSpace: 'nowrap', - textOverflow: 'ellipsis', - overflow: 'hidden', - fontFamily: 'Segoe UI', - fontSize: '12px', - lineHeight: '19px', - border: '1px solid #B3B0AD', - boxSizing: 'border-box', - borderRadius: '2px', - }} - title={typeof value === 'string' ? value : ''} - > - {value} - </div> - ); - } - })} - {Array.isArray(choices) && choices.length > 3 ? ( - <div - data-testid="hasMore" - css={{ - height: ChoiceInputSize.height, - width: ChoiceInputSize.width, - marginTop: ChoiceInputMarginTop, - textAlign: 'center', - fontFamily: 'Segoe UI', - fontSize: '12px', - lineHeight: '14px', - boxSizing: 'border-box', - }} - > - {`${choices.length - 3} more`} - </div> - ) : null} - </div> - ); -}; - -export const ChoiceInput: FC<NodeProps> = ({ id, data, onEvent }): JSX.Element => { - const boundary = measureJsonBoundary(data); - const { choices } = data; - const children = <ChoiceInputChoices choices={choices} />; - - return ( - <FormCard - nodeColors={NodeColors[DialogGroup.INPUT]} - header={getUserAnswersTitle(data._type)} - icon={ElementIcon.User} - label={data.property || '<property>'} - onClick={() => { - onEvent(NodeEventTypes.Focus, { id, tab: PromptTab.USER_INPUT }); - }} - styles={{ width: boundary.width, height: boundary.height }} - > - {children} - </FormCard> - ); -}; diff --git a/Composer/packages/extensions/visual-designer/src/widgets/DialogRefCard.tsx b/Composer/packages/extensions/visual-designer/src/widgets/DialogRefCard.tsx index 9b90ed157e..75a7a97a3d 100644 --- a/Composer/packages/extensions/visual-designer/src/widgets/DialogRefCard.tsx +++ b/Composer/packages/extensions/visual-designer/src/widgets/DialogRefCard.tsx @@ -5,8 +5,8 @@ import { jsx } from '@emotion/core'; import { generateSDKTitle } from '@bfc/shared'; import get from 'lodash/get'; -import { Link } from 'office-ui-fabric-react/lib/Link'; +import { LinkBtn } from '../components/elements/styledComponents'; import { FormCard } from '../components/nodes/templates/FormCard'; import { WidgetContainerProps, WidgetComponent } from '../schema/uischema.types'; import { ObiColors } from '../constants/ElementColors'; @@ -39,14 +39,14 @@ export const DialogRefCard: WidgetComponent<DialogRefCardProps> = ({ const nodeColors = { themeColor: colors.theme, iconColor: colors.icon }; const calleeDialog = typeof dialog === 'object' ? get(dialog, '$ref') : dialog; const dialogRef = ( - <Link + <LinkBtn onClick={e => { e.stopPropagation(); onEvent(NodeEventTypes.OpenDialog, { caller: id, callee: calleeDialog }); }} > {calleeDialog} - </Link> + </LinkBtn> ); return ( <FormCard diff --git a/Composer/yarn.lock b/Composer/yarn.lock index 5673f0b793..3f746b86fe 100644 --- a/Composer/yarn.lock +++ b/Composer/yarn.lock @@ -2256,6 +2256,16 @@ "@emotion/utils" "0.11.2" "@emotion/weak-memoize" "0.2.4" +"@emotion/cache@^10.0.27": + version "10.0.27" + resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-10.0.27.tgz#7895db204e2c1a991ae33d51262a3a44f6737303" + integrity sha512-Zp8BEpbMunFsTcqAK4D7YTm3MvCp1SekflSLJH8lze2fCcSZ/yMkXHo8kb3t1/1Tdd3hAqf3Fb7z9VZ+FMiC9w== + dependencies: + "@emotion/sheet" "0.9.4" + "@emotion/stylis" "0.8.5" + "@emotion/utils" "0.11.3" + "@emotion/weak-memoize" "0.2.5" + "@emotion/cache@^10.0.9": version "10.0.9" resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-10.0.9.tgz#e0c7b7a289f7530edcfad4dcf3858bd2e5700a6f" @@ -2278,6 +2288,18 @@ "@emotion/sheet" "0.9.3" "@emotion/utils" "0.11.2" +"@emotion/core@^10.0.27": + version "10.0.27" + resolved "https://registry.yarnpkg.com/@emotion/core/-/core-10.0.27.tgz#7c3f78be681ab2273f3bf11ca3e2edc4a9dd1fdc" + integrity sha512-XbD5R36pVbohQMnKfajHv43g8EbN4NHdF6Zh9zg/C0nr0jqwOw3gYnC07Xj3yG43OYSRyrGsoQ5qPwc8ycvLZw== + dependencies: + "@babel/runtime" "^7.5.5" + "@emotion/cache" "^10.0.27" + "@emotion/css" "^10.0.27" + "@emotion/serialize" "^0.11.15" + "@emotion/sheet" "0.9.4" + "@emotion/utils" "0.11.3" + "@emotion/core@^10.0.7": version "10.0.9" resolved "https://registry.yarnpkg.com/@emotion/core/-/core-10.0.9.tgz#f8afbccb0011100680f5dc94657b410c6aa1350e" @@ -2298,6 +2320,15 @@ "@emotion/utils" "0.11.2" babel-plugin-emotion "^10.0.14" +"@emotion/css@^10.0.27": + version "10.0.27" + resolved "https://registry.yarnpkg.com/@emotion/css/-/css-10.0.27.tgz#3a7458198fbbebb53b01b2b87f64e5e21241e14c" + integrity sha512-6wZjsvYeBhyZQYNrGoR5yPMYbMBNEnanDrqmsqS1mzDm1cOTu12shvl2j4QHNS36UaTE0USIJawCH9C8oW34Zw== + dependencies: + "@emotion/serialize" "^0.11.15" + "@emotion/utils" "0.11.3" + babel-plugin-emotion "^10.0.27" + "@emotion/css@^10.0.9": version "10.0.9" resolved "https://registry.yarnpkg.com/@emotion/css/-/css-10.0.9.tgz#ea0df431965a308f6cb1d61386df8ad61e5befb5" @@ -2322,6 +2353,18 @@ resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.7.3.tgz#a166882c81c0c6040975dd30df24fae8549bd96f" integrity sha512-14ZVlsB9akwvydAdaEnVnvqu6J2P6ySv39hYyl/aoB6w/V+bXX0tay8cF6paqbgZsN2n5Xh15uF4pE+GvE+itw== +"@emotion/hash@0.7.4": + version "0.7.4" + resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.7.4.tgz#f14932887422c9056b15a8d222a9074a7dfa2831" + integrity sha512-fxfMSBMX3tlIbKUdtGKxqB1fyrH6gVrX39Gsv3y8lRYKUqlgDt3UMqQyGnR1bQMa2B8aGnhLZokZgg8vT0Le+A== + +"@emotion/is-prop-valid@0.8.6": + version "0.8.6" + resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-0.8.6.tgz#4757646f0a58e9dec614c47c838e7147d88c263c" + integrity sha512-mnZMho3Sq8BfzkYYRVc8ilQTnc8U02Ytp6J1AwM6taQStZ3AhsEJBX2LzhA/LJirNCwM2VtHL3VFIZ+sNJUgUQ== + dependencies: + "@emotion/memoize" "0.7.4" + "@emotion/is-prop-valid@^0.7.3": version "0.7.3" resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-0.7.3.tgz#a6bf4fa5387cbba59d44e698a4680f481a8da6cc" @@ -2344,6 +2387,11 @@ resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.7.3.tgz#5b6b1c11d6a6dddf1f2fc996f74cf3b219644d78" integrity sha512-2Md9mH6mvo+ygq1trTeVp2uzAKwE2P7In0cRpD/M9Q70aH8L+rxMLbb3JCN2JoSWsV2O+DdFjfbbXoMoLBczow== +"@emotion/memoize@0.7.4": + version "0.7.4" + resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.7.4.tgz#19bf0f5af19149111c40d98bb0cf82119f5d9eeb" + integrity sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw== + "@emotion/serialize@^0.11.10", "@emotion/serialize@^0.11.11", "@emotion/serialize@^0.11.8": version "0.11.11" resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-0.11.11.tgz#c92a5e5b358070a7242d10508143306524e842a4" @@ -2355,6 +2403,17 @@ "@emotion/utils" "0.11.2" csstype "^2.5.7" +"@emotion/serialize@^0.11.15": + version "0.11.15" + resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-0.11.15.tgz#9a0f5873fb458d87d4f23e034413c12ed60a705a" + integrity sha512-YE+qnrmGwyR+XB5j7Bi+0GT1JWsdcjM/d4POu+TXkcnrRs4RFCCsi3d/Ebf+wSStHqAlTT2+dfd+b9N9EO2KBg== + dependencies: + "@emotion/hash" "0.7.4" + "@emotion/memoize" "0.7.4" + "@emotion/unitless" "0.7.5" + "@emotion/utils" "0.11.3" + csstype "^2.5.7" + "@emotion/serialize@^0.11.6": version "0.11.6" resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-0.11.6.tgz#78be8b9ee9ff49e0196233ba6ec1c1768ba1e1fc" @@ -2387,6 +2446,29 @@ resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-0.9.3.tgz#689f135ecf87d3c650ed0c4f5ddcbe579883564a" integrity sha512-c3Q6V7Df7jfwSq5AzQWbXHa5soeE4F5cbqi40xn0CzXxWW9/6Mxq48WJEtqfWzbZtW9odZdnRAkwCQwN12ob4A== +"@emotion/sheet@0.9.4": + version "0.9.4" + resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-0.9.4.tgz#894374bea39ec30f489bbfc3438192b9774d32e5" + integrity sha512-zM9PFmgVSqBw4zL101Q0HrBVTGmpAxFZH/pYx/cjJT5advXguvcgjHFTCaIO3enL/xr89vK2bh0Mfyj9aa0ANA== + +"@emotion/styled-base@^10.0.27": + version "10.0.27" + resolved "https://registry.yarnpkg.com/@emotion/styled-base/-/styled-base-10.0.27.tgz#d9efa307ae4e938fcc4d0596b40b7e8bc10f7c7c" + integrity sha512-ufHM/HhE3nr309hJG9jxuFt71r6aHn7p+bwXduFxcwPFEfBIqvmZUMtZ9YxIsY61PVwK3bp4G1XhaCzy9smVvw== + dependencies: + "@babel/runtime" "^7.5.5" + "@emotion/is-prop-valid" "0.8.6" + "@emotion/serialize" "^0.11.15" + "@emotion/utils" "0.11.3" + +"@emotion/styled@^10.0.27": + version "10.0.27" + resolved "https://registry.yarnpkg.com/@emotion/styled/-/styled-10.0.27.tgz#12cb67e91f7ad7431e1875b1d83a94b814133eaf" + integrity sha512-iK/8Sh7+NLJzyp9a5+vIQIXTYxfT4yB/OJbjzQanB2RZpvmzBQOHZWhpAMZWYEKRNNbsD6WfBw5sVWkb6WzS/Q== + dependencies: + "@emotion/styled-base" "^10.0.27" + babel-plugin-emotion "^10.0.27" + "@emotion/stylis@0.8.3": version "0.8.3" resolved "https://registry.yarnpkg.com/@emotion/stylis/-/stylis-0.8.3.tgz#3ca7e9bcb31b3cb4afbaeb66156d86ee85e23246" @@ -2397,6 +2479,11 @@ resolved "https://registry.yarnpkg.com/@emotion/stylis/-/stylis-0.8.4.tgz#6c51afdf1dd0d73666ba09d2eb6c25c220d6fe4c" integrity sha512-TLmkCVm8f8gH0oLv+HWKiu7e8xmBIaokhxcEKPh1m8pXiV/akCiq50FvYgOwY42rjejck8nsdQxZlXZ7pmyBUQ== +"@emotion/stylis@0.8.5": + version "0.8.5" + resolved "https://registry.yarnpkg.com/@emotion/stylis/-/stylis-0.8.5.tgz#deacb389bd6ee77d1e7fcaccce9e16c5c7e78e04" + integrity sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ== + "@emotion/unitless@0.7.3", "@emotion/unitless@^0.7.0": version "0.7.3" resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.7.3.tgz#6310a047f12d21a1036fb031317219892440416f" @@ -2407,6 +2494,11 @@ resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.7.4.tgz#a87b4b04e5ae14a88d48ebef15015f6b7d1f5677" integrity sha512-kBa+cDHOR9jpRJ+kcGMsysrls0leukrm68DmFQoMIWQcXdr2cZvyvypWuGYT7U+9kAExUE7+T7r6G3C3A6L8MQ== +"@emotion/unitless@0.7.5": + version "0.7.5" + resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.7.5.tgz#77211291c1900a700b8a78cfafda3160d76949ed" + integrity sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg== + "@emotion/utils@0.11.1": version "0.11.1" resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-0.11.1.tgz#8529b7412a6eb4b48bdf6e720cc1b8e6e1e17628" @@ -2417,6 +2509,11 @@ resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-0.11.2.tgz#713056bfdffb396b0a14f1c8f18e7b4d0d200183" integrity sha512-UHX2XklLl3sIaP6oiMmlVzT0J+2ATTVpf0dHQVyPJHTkOITvXfaSqnRk6mdDhV9pR8T/tHc3cex78IKXssmzrA== +"@emotion/utils@0.11.3": + version "0.11.3" + resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-0.11.3.tgz#a759863867befa7e583400d322652a3f44820924" + integrity sha512-0o4l6pZC+hI88+bzuaX/6BgOvQVhbt2PfmxauVaYOGgbsAw14wdKyvMCZXnsnsHys94iadcF+RG/wZyx6+ZZBw== + "@emotion/weak-memoize@0.2.2": version "0.2.2" resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.2.2.tgz#63985d3d8b02530e0869962f4da09142ee8e200e" @@ -2427,6 +2524,11 @@ resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.2.4.tgz#622a72bebd1e3f48d921563b4b60a762295a81fc" integrity sha512-6PYY5DVdAY1ifaQW6XYTnOMihmBVT27elqSjEoodchsGjzYlEsTQMcEhSud99kVawatyTZRTiVkJ/c6lwbQ7nA== +"@emotion/weak-memoize@0.2.5": + version "0.2.5" + resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz#8eed982e2ee6f7f4e44c253e12962980791efd46" + integrity sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA== + "@hapi/address@2.x.x": version "2.1.2" resolved "https://botbuilder.myget.org/F/botbuilder-declarative/npm/@hapi/address/-/@hapi/address-2.1.2.tgz#1c794cd6dbf2354d1eb1ef10e0303f573e1c7222" @@ -4493,6 +4595,22 @@ babel-plugin-emotion@^10.0.17: find-root "^1.1.0" source-map "^0.5.7" +babel-plugin-emotion@^10.0.27: + version "10.0.27" + resolved "https://registry.yarnpkg.com/babel-plugin-emotion/-/babel-plugin-emotion-10.0.27.tgz#59001cf5de847c1d61f2079cd906a90a00d3184f" + integrity sha512-SUNYcT4FqhOqvwv0z1oeYhqgheU8qrceLojuHyX17ngo7WtWqN5I9l3IGHzf21Xraj465CVzF4IvOlAF+3ed0A== + dependencies: + "@babel/helper-module-imports" "^7.0.0" + "@emotion/hash" "0.7.4" + "@emotion/memoize" "0.7.4" + "@emotion/serialize" "^0.11.15" + babel-plugin-macros "^2.0.0" + babel-plugin-syntax-jsx "^6.18.0" + convert-source-map "^1.5.0" + escape-string-regexp "^1.0.5" + find-root "^1.1.0" + source-map "^0.5.7" + babel-plugin-emotion@^10.0.7, babel-plugin-emotion@^10.0.9: version "10.0.9" resolved "https://registry.yarnpkg.com/babel-plugin-emotion/-/babel-plugin-emotion-10.0.9.tgz#04a0404d5a4084d5296357a393d344c0f8303ae4" From a20d2b2c2823612734a83ae7222e8245437505e0 Mon Sep 17 00:00:00 2001 From: zeye <2295905420@qq.com> Date: Thu, 5 Mar 2020 01:08:00 +0800 Subject: [PATCH 15/40] sort casesKey and stepsKey correctly (#2166) --- .../visual-designer/src/components/groups/StepGroup.tsx | 3 ++- .../visual-designer/src/widgets/SwitchConditionWidget.tsx | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Composer/packages/extensions/visual-designer/src/components/groups/StepGroup.tsx b/Composer/packages/extensions/visual-designer/src/components/groups/StepGroup.tsx index 83d4b57ebc..eada5efc13 100644 --- a/Composer/packages/extensions/visual-designer/src/components/groups/StepGroup.tsx +++ b/Composer/packages/extensions/visual-designer/src/components/groups/StepGroup.tsx @@ -25,6 +25,7 @@ const StepInterval = ElementInterval.y; type StepNodeKey = string; const getStepKey = (stepOrder: number): StepNodeKey => `steps[${stepOrder}]`; +const parseStepIndex = (stepKey: string): number => parseInt(stepKey.replace(/steps\[(\d+)\]/, '$1')); const calculateNodes = (groupId: string, data): GraphNodeMap<StepNodeKey> => { const steps = transformStepGroup(data, groupId); @@ -37,7 +38,7 @@ const calculateNodes = (groupId: string, data): GraphNodeMap<StepNodeKey> => { const calculateLayout = (nodeMap: GraphNodeMap<StepNodeKey>): GraphLayout => { const nodes = Object.keys(nodeMap) - .sort() + .sort((a, b) => parseStepIndex(a) - parseStepIndex(b)) .map(stepName => nodeMap[stepName]); return sequentialLayouter(nodes); }; diff --git a/Composer/packages/extensions/visual-designer/src/widgets/SwitchConditionWidget.tsx b/Composer/packages/extensions/visual-designer/src/widgets/SwitchConditionWidget.tsx index 6d3c3b6f86..f9a539bde8 100644 --- a/Composer/packages/extensions/visual-designer/src/widgets/SwitchConditionWidget.tsx +++ b/Composer/packages/extensions/visual-designer/src/widgets/SwitchConditionWidget.tsx @@ -28,6 +28,7 @@ enum SwitchNodes { type CaseNodeKey = string; const getCaseKey = (caseIndex: number): CaseNodeKey => `cases[${caseIndex}]`; +const parseCaseIndex = (caseKey: CaseNodeKey): number => parseInt(caseKey.replace(/cases\[(\d+)\]/, '$1')); const calculateNodeMap = (path: string, data): GraphNodeMap<SwitchNodes | CaseNodeKey> => { const result = transformSwitchCondition(data, path); @@ -55,7 +56,7 @@ const calculateNodeMap = (path: string, data): GraphNodeMap<SwitchNodes | CaseNo const calculateLayout = (nodeMap: GraphNodeMap<SwitchNodes | CaseNodeKey>) => { const { switchNode, choiceNode, ...cases } = nodeMap as GraphNodeMap<SwitchNodes>; const casesNodes = Object.keys(cases) - .sort() + .sort((a, b) => parseCaseIndex(a) - parseCaseIndex(b)) .map(caseName => nodeMap[caseName]); return switchCaseLayouter(switchNode, choiceNode, casesNodes); }; From 7191c2ff56fb109e77216dae90897f9bab273ed5 Mon Sep 17 00:00:00 2001 From: Long Alan <julong@microsoft.com> Date: Thu, 5 Mar 2020 01:32:20 +0800 Subject: [PATCH 16/40] add role, name, posinset for paste in edgeMenu (#2126) Co-authored-by: zeye <2295905420@qq.com> Co-authored-by: Chris Whitten <christopher.whitten@microsoft.com> --- .../visual-designer/src/components/menus/EdgeMenu.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Composer/packages/extensions/visual-designer/src/components/menus/EdgeMenu.tsx b/Composer/packages/extensions/visual-designer/src/components/menus/EdgeMenu.tsx index 9679033006..fd18129b61 100644 --- a/Composer/packages/extensions/visual-designer/src/components/menus/EdgeMenu.tsx +++ b/Composer/packages/extensions/visual-designer/src/components/menus/EdgeMenu.tsx @@ -46,15 +46,21 @@ const buildEdgeMenuItemsFromClipboardContext = ( ); const enablePaste = Array.isArray(clipboardActions) && clipboardActions.length > 0; + const menuItemCount = menuItems.length; menuItems.unshift( { key: 'Paste', name: 'Paste', + ariaLabel: 'Paste', disabled: !enablePaste, onRender: () => { return ( <button disabled={!enablePaste} + role="menuitem" + name="Paste" + aria-posinset={1} + aria-setsize={menuItemCount + 1} css={css` color: ${enablePaste ? '#0078D4' : '#BDBDBD'}; background: #fff; From 4a8a086aab0f2c8f8226c114c7fc5f998b308297 Mon Sep 17 00:00:00 2001 From: Andy Brown <asbrown002@gmail.com> Date: Wed, 4 Mar 2020 12:51:41 -0800 Subject: [PATCH 17/40] ci: add a11y pr title prefix for accessibility prs (#2171) --- .github/actions/conventional-pr/src/utils.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/actions/conventional-pr/src/utils.ts b/.github/actions/conventional-pr/src/utils.ts index 407b03953f..4e4f1a484a 100644 --- a/.github/actions/conventional-pr/src/utils.ts +++ b/.github/actions/conventional-pr/src/utils.ts @@ -20,6 +20,7 @@ const validTypes = [ 'chore', 'revert', 'release', + 'a11y', ]; const typeList = validTypes.map(t => ` - ${t}`).join('\n'); From 66986a2ecb175bf11dffee4552d890c182887b68 Mon Sep 17 00:00:00 2001 From: Chris Whitten <christopher.whitten@microsoft.com> Date: Wed, 4 Mar 2020 14:10:26 -0800 Subject: [PATCH 18/40] feat: implement new action design to improve readability (#2136) * implement new CardTemplate * show icon inside ActionHeader * update card body style * experiment new CardTemplate on several types * Revert "experiment new CardTemplate on several types" This reverts commit bc00c5c91fb947606d73afa308fe2ca55c226603. * add CardTemplate sparateline placeholder * update ListOverview types * migrate userInput to cardTemplate * assign ListOverview default value * make ActivityRenderer pure and apply to uischema * migrate SetProperties to CardTemplate * adjust CardTemplate style * merge ChoiceInput schem into BaseInput * migrate DialogRef to new template * deprecate FormCard with CardTemplate * style: ActionHeader * fix: null content bug * implement ArrowLine as card's separator * adjust Separator style * style: icon menu padding -> 0 * style: ArrowLine stroke fix * style: bigger node * style: ActionHeader * update dev demo * display more types in demo * share duplicated font styles * removed MultilineDiv and unref defs * extract css related to ellipsis * apply EllipsisCSS to styledComponent * css: rename to TruncatedCSS * refine all ListOverview related styles * control footer visibility in CardTemplate * card footer style * update footer of SetProperties & BaseInput * update rest types * fix display problems * fix placeholder problem of ChoiceInput * show '?' as some default values * fix DialogRef, CardTemplate * remove outdated titles * use ActionCard in ReplaceDialog * show DeleteProperties as ListOverview Co-authored-by: Chris Whitten <christopher.whitten@microsoft.com> --- .../components/nodes/FormCard.test.tsx | 32 --- .../demo/src/stories/VisualSDKDemo.js | 35 +-- .../src/components/common/ListOverview.tsx | 37 ++- .../src/components/elements/sharedCSS.tsx | 23 ++ .../components/elements/styledComponents.tsx | 81 +++--- .../elements/styledComponents.types.ts | 8 +- .../src/components/lib/ArrowLine.tsx | 34 +++ .../src/components/menus/IconMenu.tsx | 2 +- .../nodes/templates/CardTemplate.tsx | 126 +++++++++ .../components/nodes/templates/FormCard.tsx | 144 ---------- .../src/constants/ElementSizes.ts | 7 +- .../visual-designer/src/schema/uischema.tsx | 260 ++++++++++++------ .../src/widgets/ActionCard.tsx | 54 +--- .../src/widgets/ActionHeader.tsx | 54 ++-- .../src/widgets/ActivityRenderer.tsx | 42 +-- .../visual-designer/src/widgets/DialogRef.tsx | 30 ++ .../src/widgets/DialogRefCard.tsx | 59 ---- 17 files changed, 520 insertions(+), 508 deletions(-) delete mode 100644 Composer/packages/extensions/visual-designer/__tests__/components/nodes/FormCard.test.tsx create mode 100644 Composer/packages/extensions/visual-designer/src/components/elements/sharedCSS.tsx create mode 100644 Composer/packages/extensions/visual-designer/src/components/lib/ArrowLine.tsx create mode 100644 Composer/packages/extensions/visual-designer/src/components/nodes/templates/CardTemplate.tsx delete mode 100644 Composer/packages/extensions/visual-designer/src/components/nodes/templates/FormCard.tsx create mode 100644 Composer/packages/extensions/visual-designer/src/widgets/DialogRef.tsx delete mode 100644 Composer/packages/extensions/visual-designer/src/widgets/DialogRefCard.tsx diff --git a/Composer/packages/extensions/visual-designer/__tests__/components/nodes/FormCard.test.tsx b/Composer/packages/extensions/visual-designer/__tests__/components/nodes/FormCard.test.tsx deleted file mode 100644 index 63d303ae46..0000000000 --- a/Composer/packages/extensions/visual-designer/__tests__/components/nodes/FormCard.test.tsx +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -import React from 'react'; -import { render, fireEvent } from 'react-testing-library'; - -import { FormCard } from '../../../src/components/nodes/templates/FormCard'; -import { getElementColor } from '../../../src/utils/obiPropertyResolver'; - -describe('<FormCard />', () => { - let renderResult, header, label, nodeColors, onClick; - - beforeEach(() => { - header = 'InputFormCard'; - label = 'FormCard'; - nodeColors = getElementColor('Microsoft.SendActivity'); - - onClick = jest.fn(); - - renderResult = render( - <FormCard nodeColors={nodeColors} header={header} label={label} corner={<div />} onClick={onClick} /> - ); - }); - - it('can be clicked', async () => { - const { findByTestId } = renderResult; - const formCard = await findByTestId('FormCard'); - - fireEvent.click(formCard); - expect(onClick).toHaveBeenCalled(); - }); -}); diff --git a/Composer/packages/extensions/visual-designer/demo/src/stories/VisualSDKDemo.js b/Composer/packages/extensions/visual-designer/demo/src/stories/VisualSDKDemo.js index 70b1ea7f12..1d6e470b23 100644 --- a/Composer/packages/extensions/visual-designer/demo/src/stories/VisualSDKDemo.js +++ b/Composer/packages/extensions/visual-designer/demo/src/stories/VisualSDKDemo.js @@ -1,5 +1,5 @@ import React, { Component } from 'react'; -import { seedNewDialog, SDKTypes } from '@bfc/shared'; +import { seedNewDialog, SDKTypes, dialogGroups, DialogGroup } from '@bfc/shared'; import { EdgeMenu } from '../../../src/components/menus/EdgeMenu'; import { JsonBlock } from '../components/json-block'; @@ -18,26 +18,21 @@ export class VisualSDKDemo extends Component { seedInitialActions() { const initialTypes = [ - SDKTypes.SendActivity, - SDKTypes.EditArray, - SDKTypes.SetProperties, - SDKTypes.SetProperty, - SDKTypes.DeleteProperties, - SDKTypes.DeleteProperty, - SDKTypes.BeginDialog, - SDKTypes.EndDialog, - SDKTypes.RepeatDialog, - SDKTypes.ReplaceDialog, - SDKTypes.CancelAllDialogs, - SDKTypes.EmitEvent, + ...dialogGroups[DialogGroup.RESPONSE].types, + ...dialogGroups[DialogGroup.INPUT].types, + ...dialogGroups[DialogGroup.BRANCHING].types, + ...dialogGroups[DialogGroup.MEMORY].types, + ...dialogGroups[DialogGroup.STEP].types, + ...dialogGroups[DialogGroup.CODE].types, + ...dialogGroups[DialogGroup.LOG].types, ]; const initalActions = initialTypes.map(t => seedNewDialog(t)); return initalActions; } - appendActionPreview($type) { + insertActionPreview($type) { this.setState({ - actions: [...this.state.actions, seedNewDialog($type)], + actions: [seedNewDialog($type), ...this.state.actions], }); } @@ -51,7 +46,7 @@ export class VisualSDKDemo extends Component { <div className="action-preview--raw"> <JsonBlock styles={{ - width: '200px', + width: '300px', height: '80px', fontSize: '8px', }} @@ -65,7 +60,7 @@ export class VisualSDKDemo extends Component { }} /> </div> - <div className="action-preview--visual"> + <div className="action-preview--visual" style={{ marginLeft: 20 }}> {renderUIWidget(uiSchemaPrivider.get(action.$type), { id: `actions[${index}]`, data: action, @@ -78,9 +73,9 @@ export class VisualSDKDemo extends Component { renderActionFactory() { return ( - <div style={{ height: 100, margin: 20 }}> + <div style={{ width: '100%', height: 100, margin: 20 }}> <h3>Create action by $type</h3> - <EdgeMenu id="visual-sdk-demo" onClick={$type => this.appendActionPreview($type)} /> + <EdgeMenu id="visual-sdk-demo" onClick={$type => this.insertActionPreview($type)} /> </div> ); } @@ -90,8 +85,8 @@ export class VisualSDKDemo extends Component { <div className="story-container"> <h1 className="story-title">Visual SDK Demo</h1> <div className="story-content" style={{ display: 'flex', flexFlow: 'wrap' }}> - {this.state.actions.map((action, index) => this.renderActionPreview(action, index))} {this.renderActionFactory()} + {this.state.actions.map((action, index) => this.renderActionPreview(action, index))} </div> </div> ); diff --git a/Composer/packages/extensions/visual-designer/src/components/common/ListOverview.tsx b/Composer/packages/extensions/visual-designer/src/components/common/ListOverview.tsx index ef90a437fe..26640d92d6 100644 --- a/Composer/packages/extensions/visual-designer/src/components/common/ListOverview.tsx +++ b/Composer/packages/extensions/visual-designer/src/components/common/ListOverview.tsx @@ -1,24 +1,38 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import React, { FC } from 'react'; +/** @jsx jsx */ +import { jsx, css } from '@emotion/core'; +import { FC } from 'react'; import { SingleLineDiv } from '../elements/styledComponents'; -export interface ListOverviewProps { - items: string[]; - renderItem: (item: object | string) => JSX.Element; - maxCount: number; +export interface ListOverviewProps<T> { + items: T[]; + renderItem: (item: T) => JSX.Element; + maxCount?: number; + itemPadding?: number; } -export const ListOverview: FC<ListOverviewProps> = ({ items, renderItem, maxCount }) => { - if (!Array.isArray(items)) { + +export const ListOverview: FC<ListOverviewProps<any>> = ({ + items, + renderItem, + maxCount = 3, + itemPadding: itemInterval = 4, +}) => { + if (!Array.isArray(items) || !items.length) { return null; } return ( - <div style={{ padding: '0 0 8px 8px' }}> + <div + className="ListOverview" + css={css` + width: 100%; + `} + > {items.slice(0, maxCount).map((item, index) => { return ( - <div style={{ marginTop: '8px' }} key={index}> + <div style={{ marginTop: index ? itemInterval : 0 }} key={index}> {renderItem(item)} </div> ); @@ -26,9 +40,10 @@ export const ListOverview: FC<ListOverviewProps> = ({ items, renderItem, maxCoun {items.length > maxCount ? ( <SingleLineDiv data-testid="hasMore" - style={{ - marginTop: '8px', + css={{ + marginTop: '3px', textAlign: 'center', + color: '#757575', }} > {`${items.length - maxCount} more`} diff --git a/Composer/packages/extensions/visual-designer/src/components/elements/sharedCSS.tsx b/Composer/packages/extensions/visual-designer/src/components/elements/sharedCSS.tsx new file mode 100644 index 0000000000..0b31181742 --- /dev/null +++ b/Composer/packages/extensions/visual-designer/src/components/elements/sharedCSS.tsx @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { css } from '@emotion/core'; + +export const StandardFontCSS = css` + font-size: 12px; + line-height: 14px; + font-family: Segoe UI; + color: black; +`; + +export const TruncatedCSS = css` + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +`; + +export const MultilineCSS = css` + white-space: initial; + overflow-wrap: break-word; + word-break: break-all; +`; diff --git a/Composer/packages/extensions/visual-designer/src/components/elements/styledComponents.tsx b/Composer/packages/extensions/visual-designer/src/components/elements/styledComponents.tsx index f4afc5467f..1bdc8fa122 100644 --- a/Composer/packages/extensions/visual-designer/src/components/elements/styledComponents.tsx +++ b/Composer/packages/extensions/visual-designer/src/components/elements/styledComponents.tsx @@ -7,7 +7,8 @@ import { Link } from 'office-ui-fabric-react/lib/Link'; import { ObiColors } from '../../constants/ElementColors'; -import { MultiLineDivProps, DivProps } from './styledComponents.types'; +import { DivProps } from './styledComponents.types'; +import { StandardFontCSS, TruncatedCSS, MultilineCSS } from './sharedCSS'; const dynamicStyle = props => css` @@ -22,43 +23,45 @@ export const Span = styled.span` ${dynamicStyle} `; -export const BorderedDiv = styled.div<DivProps>(props => ({ - color: props.color || ObiColors.Black, - width: props.width, - height: props.height, - padding: '2px 0 0 8px', - whiteSpace: 'nowrap', - textOverflow: 'ellipsis', - overflow: 'hidden', - fontFamily: 'Segoe UI', - fontSize: '12px', - lineHeight: '14px', - border: '1px solid #C4C4C4', - boxSizing: 'border-box', -})); +export const BorderedDiv = styled.div<DivProps>( + css` + ${StandardFontCSS}; + ${TruncatedCSS}; + padding: 2px 0 0 8px; + border: 1px solid #c4c4c4; + box-sizing: border-box; + `, + props => ({ + color: props.color || ObiColors.Black, + width: props.width, + height: props.height, + }) +); -export const MultiLineDiv = styled.div<MultiLineDivProps>(props => ({ - color: props.color || ObiColors.Black, - fontSize: '12px', - height: `${(props.lineNum || 1) * 19}px`, - lineHeight: '19px', - fontFamily: 'Segoe UI', - overflow: 'hidden', - textOverflow: 'ellipsis', - wordBreak: 'break-word', - display: '-webkit-box', - '-webkit-line-clamp': `${props.lineNum || 1}`, - '-webkit-box-orient': 'vertical', -})); +export const SingleLineDiv = styled.div<DivProps>` + ${StandardFontCSS}; + ${TruncatedCSS}; + line-height: ${height => (height ? height + 'px' : undefined)}; +`; -export const SingleLineDiv = styled.div<DivProps>(props => ({ - width: props.width || 150, - height: props.height || '19px', - color: props.color || ObiColors.Black, - fontSize: '12px', - whiteSpace: 'nowrap', - textOverflow: 'ellipsis', - overflow: 'hidden', - lineHeight: '19px', - fontFamily: 'Segoe UI', -})); +export const TextDiv = styled.div` + ${StandardFontCSS}; + ${MultilineCSS}; + line-height: 16px; + display: inline-block; +`; + +export const Text = styled.span( + css` + ${StandardFontCSS}; + `, + ({ color }) => + css` + color: ${color}; + ` +); + +export const FixedInfo = styled.span` + ${StandardFontCSS}; + color: #757575; +`; diff --git a/Composer/packages/extensions/visual-designer/src/components/elements/styledComponents.types.ts b/Composer/packages/extensions/visual-designer/src/components/elements/styledComponents.types.ts index dd6d1680e8..df0e8a9c9e 100644 --- a/Composer/packages/extensions/visual-designer/src/components/elements/styledComponents.types.ts +++ b/Composer/packages/extensions/visual-designer/src/components/elements/styledComponents.types.ts @@ -1,15 +1,11 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { DetailedHTMLProps, HTMLAttributes, ComponentClass, FC } from 'react'; +import { DetailedHTMLProps, HTMLAttributes } from 'react'; export type ElementProps = DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>; + export interface DivProps extends ElementProps { width?: number; height?: number; } - -export interface MultiLineDivProps extends DivProps { - lineNum?: number; -} -export type ElementComponent<T extends ElementProps> = FC<T> | ComponentClass<T, any>; diff --git a/Composer/packages/extensions/visual-designer/src/components/lib/ArrowLine.tsx b/Composer/packages/extensions/visual-designer/src/components/lib/ArrowLine.tsx new file mode 100644 index 0000000000..ae07ba0611 --- /dev/null +++ b/Composer/packages/extensions/visual-designer/src/components/lib/ArrowLine.tsx @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** @jsx jsx */ +import { jsx } from '@emotion/core'; + +export interface ArrowLineProps { + width: number; + arrowsize: number; + containerHeight?: number; + color?: string; +} +export const ArrowLine: React.FC<ArrowLineProps> = ({ width, arrowsize, color }) => { + const points = [ + { x: 0, y: 0 }, + { x: width / 2 - (arrowsize + 2), y: 0 }, + { x: width / 2, y: arrowsize }, + { x: width / 2 + (arrowsize + 2), y: 0 }, + { x: width, y: 0 }, + ]; + const pointsString = points.map(p => `${p.x},${p.y}`).join(' '); + return ( + <svg css={{ display: 'block' }} width={width} height={1} overflow="visible"> + <polyline points={pointsString} stroke={color} strokeWidth="1" fill="none" strokeDasharray="none" /> + </svg> + ); +}; + +ArrowLine.defaultProps = { + width: 200, + arrowsize: 8, + containerHeight: 1, + color: 'black', +}; diff --git a/Composer/packages/extensions/visual-designer/src/components/menus/IconMenu.tsx b/Composer/packages/extensions/visual-designer/src/components/menus/IconMenu.tsx index 551c8b5b6c..4e70184efe 100644 --- a/Composer/packages/extensions/visual-designer/src/components/menus/IconMenu.tsx +++ b/Composer/packages/extensions/visual-designer/src/components/menus/IconMenu.tsx @@ -62,7 +62,7 @@ export const IconMenu: React.FC<IconMenuProps> = ({ const buttonStyles: IButtonStyles = { root: { minWidth: 0, - padding: '0 8px', + padding: 0, margin: 0, alignSelf: 'stretch', height: 'auto', diff --git a/Composer/packages/extensions/visual-designer/src/components/nodes/templates/CardTemplate.tsx b/Composer/packages/extensions/visual-designer/src/components/nodes/templates/CardTemplate.tsx new file mode 100644 index 0000000000..dd32126f8e --- /dev/null +++ b/Composer/packages/extensions/visual-designer/src/components/nodes/templates/CardTemplate.tsx @@ -0,0 +1,126 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** @jsx jsx */ +import { jsx, css } from '@emotion/core'; +import { FC, ReactNode } from 'react'; + +import { StandardNodeWidth, HeaderHeight, StandardSectionHeight } from '../../../constants/ElementSizes'; +import { ObiColors } from '../../../constants/ElementColors'; +import { ArrowLine } from '../../lib/ArrowLine'; +import { TextDiv } from '../../elements/styledComponents'; + +const containerCSS = css` + font-size: 12px; + cursor: pointer; + overflow: hidden; + background-color: white; + border-radius: 2px 2px 0 0; + box-shadow: 0px 2px 8px rgba(0, 0, 0, 0.1); +`; + +const fullWidthSection = css` + width: 100%; + box-sizing: border-box; +`; + +export interface CardTemplateProps { + header: ReactNode; + body?: ReactNode; + footer?: ReactNode; + onClick?: (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => void; + onClickHeader?: (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => void; + onClickBody?: (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => void; + onClickFooter?: (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => void; +} + +export const CardTemplate: FC<CardTemplateProps> = ({ + header, + body, + footer, + onClick, + onClickHeader, + onClickBody, + onClickFooter, +}) => { + const renderHeader = (header: ReactNode) => ( + <div + className="CardNode__Header" + css={css` + ${fullWidthSection}; + height: ${HeaderHeight}px; + `} + onClick={onClickHeader} + > + {header} + </div> + ); + + const renderBody = (body: ReactNode) => ( + <div + className="CardNode__Body" + css={css` + ${fullWidthSection}; + min-height: ${StandardSectionHeight}px; + padding: 7px 8px; + `} + onClick={onClickBody} + > + <TextDiv css={{ width: '100%' }}>{body}</TextDiv> + </div> + ); + + const renderFooter = (footer: ReactNode) => ( + <div + className="CardNode__Footer" + css={css` + ${fullWidthSection}; + min-height: ${StandardSectionHeight}px; + padding: 8px 8px; + `} + onClick={onClickFooter} + > + <TextDiv css={{ width: '100%' }}>{footer}</TextDiv> + </div> + ); + + const renderSeparateline = () => ( + <div + className="Separator" + css={css` + display: block; + height: 0px; + overflow: visible; + `} + > + <ArrowLine width={StandardNodeWidth} arrowsize={8} color={ObiColors.AzureGray3} /> + </div> + ); + + // If body is null but footer not null, show footer as body. + const [displayedBody, displayedFooter] = [body, footer].filter(x => x !== undefined && x !== null); + const showFooter = displayedFooter !== undefined; + return ( + <div + className="CardNode" + css={css` + ${containerCSS}; + width: ${StandardNodeWidth}px; + min-height: ${HeaderHeight}px; + `} + onClick={ + onClick + ? e => { + e.stopPropagation(); + onClick(e); + } + : undefined + } + > + {renderHeader(header)} + {renderBody(displayedBody)} + {showFooter ? renderSeparateline() : null} + {showFooter ? renderFooter(footer) : null} + </div> + ); +}; diff --git a/Composer/packages/extensions/visual-designer/src/components/nodes/templates/FormCard.tsx b/Composer/packages/extensions/visual-designer/src/components/nodes/templates/FormCard.tsx deleted file mode 100644 index 498663409e..0000000000 --- a/Composer/packages/extensions/visual-designer/src/components/nodes/templates/FormCard.tsx +++ /dev/null @@ -1,144 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -/** @jsx jsx */ -import { jsx } from '@emotion/core'; -import { FunctionComponent } from 'react'; - -import { InitNodeSize } from '../../../constants/ElementSizes'; -import { ElementIcon } from '../../../utils/obiPropertyResolver'; -import { Icon } from '../../decorations/icon'; - -const boxWidth = InitNodeSize.width; -const boxHeight = InitNodeSize.height; -const headerHeight = InitNodeSize.height / 2; -const contentHeight = boxHeight - headerHeight; - -const containerStyle = { - width: boxWidth, - minHeight: boxHeight, - fontSize: '12px', - cursor: 'pointer', - overflow: 'hidden', - backgroundColor: 'white', - borderRadius: '2px 2px 0 0', - boxShadow: '0px 2px 8px rgba(0, 0, 0, 0.1)', -}; - -interface NodeProps { - header: string; - corner?: any; - label: any; - icon?: string; - iconSize?: number; - styles?: object; - nodeColors: { [key: string]: any }; - onClick?: () => void; - children?: any; -} -export const FormCard: FunctionComponent<NodeProps> = ({ - header, - corner, - label, - icon, - iconSize, - nodeColors, - onClick, - children = null, - styles = {}, -}) => { - const { themeColor, iconColor } = nodeColors; - return ( - <div - className="card" - data-testid="FormCard" - css={[containerStyle, { ...styles }]} - onClick={e => { - if (typeof onClick === 'function') { - e.stopPropagation(); - onClick(); - } - }} - > - <div - className="card__header" - css={{ - width: '100%', - height: `${headerHeight}px`, - backgroundColor: themeColor, - fontFamily: 'Segoe UI', - fontSize: '12px', - lineHeight: '14px', - color: 'black', - }} - > - <div - css={{ - padding: '4px 8px', - fontSize: '12px', - fontFamily: 'Segoe UI', - lineHeight: '14px', - overflow: 'hidden', - textOverflow: 'ellipsis', - width: 'calc(100% - 40px)', - whiteSpace: 'pre', - }} - > - {header} - </div> - <div css={{ position: 'absolute', top: 4, right: 0 }}>{corner}</div> - </div> - <div - className="card__content" - css={{ - width: '100%', - minHeight: contentHeight, - display: 'inline-block', - }} - > - <div - css={{ - fontWeight: 400, - paddingLeft: '5px', - margin: '2px 5px', - fontSize: '14px', - lineHeight: '19px', - display: 'inline-flex', - alignItems: 'center', - }} - > - {icon && icon !== ElementIcon.None && ( - <div - css={{ - width: 16, - height: 16, - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - marginRight: '5px', - }} - > - <Icon icon={icon} color={iconColor} size={iconSize || 16} /> - </div> - )} - <div - css={{ - height: '100%', - whiteSpace: 'initial', - fontSize: '12px', - lineHeight: '19px', - fontFamily: 'Segoe UI', - overflowWrap: 'break-word', - wordBreak: 'break-all', - display: 'inline-block', - }} - title={typeof label === 'string' ? label : ''} - > - {label} - </div> - </div> - {children} - </div> - </div> - ); -}; diff --git a/Composer/packages/extensions/visual-designer/src/constants/ElementSizes.ts b/Composer/packages/extensions/visual-designer/src/constants/ElementSizes.ts index 4800cf9a5c..6d46ceca03 100644 --- a/Composer/packages/extensions/visual-designer/src/constants/ElementSizes.ts +++ b/Composer/packages/extensions/visual-designer/src/constants/ElementSizes.ts @@ -1,12 +1,13 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -export const StandardNodeWidth = 200; -export const HeaderHeight = 24; +export const StandardNodeWidth = 300; +export const StandardSectionHeight = 30; +export const HeaderHeight = StandardSectionHeight; export const InitNodeSize = { width: StandardNodeWidth, - height: 48, + height: StandardSectionHeight * 2, }; export const DiamondSize = { diff --git a/Composer/packages/extensions/visual-designer/src/schema/uischema.tsx b/Composer/packages/extensions/visual-designer/src/schema/uischema.tsx index 34b36837df..a6b805550f 100644 --- a/Composer/packages/extensions/visual-designer/src/schema/uischema.tsx +++ b/Composer/packages/extensions/visual-designer/src/schema/uischema.tsx @@ -8,7 +8,7 @@ import get from 'lodash/get'; import { ActionCard } from '../widgets/ActionCard'; import { ActivityRenderer } from '../widgets/ActivityRenderer'; -import { DialogRefCard } from '../widgets/DialogRefCard'; +import { DialogRef } from '../widgets/DialogRef'; import { PromptWidget } from '../widgets/PromptWidget'; import { IfConditionWidget } from '../widgets/IfConditionWidget'; import { SwitchConditionWidget } from '../widgets/SwitchConditionWidget'; @@ -16,64 +16,64 @@ import { ForeachWidget } from '../widgets/ForeachWidget'; import { ActionHeader } from '../widgets/ActionHeader'; import { ElementIcon } from '../utils/obiPropertyResolver'; import { ObiColors } from '../constants/ElementColors'; -import { SingleLineDiv, BorderedDiv } from '../components/elements/styledComponents'; -import { PropertyAssignmentSize, ChoiceInputSize } from '../constants/ElementSizes'; +import { SingleLineDiv, BorderedDiv, FixedInfo } from '../components/elements/styledComponents'; import { ListOverview } from '../components/common/ListOverview'; +import { CardTemplate } from '../components/nodes/templates/CardTemplate'; import { UISchema, UIWidget } from './uischema.types'; const BaseInputSchema: UIWidget = { 'ui:widget': PromptWidget, botAsks: { - 'ui:widget': ActivityRenderer, - title: data => `Bot Asks (${getInputType(data.$type)})`, - field: 'prompt', - defaultContent: '<prompt>', - icon: ElementIcon.MessageBot, - colors: { - theme: ObiColors.BlueMagenta20, - icon: ObiColors.BlueMagenta30, + 'ui:widget': CardTemplate, + header: { + 'ui:widget': ActionHeader, + title: data => `Bot Asks (${getInputType(data.$type)})`, + icon: ElementIcon.MessageBot, + colors: { + theme: ObiColors.BlueMagenta20, + icon: ObiColors.BlueMagenta30, + }, + }, + body: { + 'ui:widget': ActivityRenderer, + field: 'prompt', + defaultContent: '<prompt>', }, }, userInput: { - 'ui:widget': ActionCard, - title: data => `User Input (${getInputType(data.$type)})`, - disableSDKTitle: true, - icon: ElementIcon.User, - menu: 'none', - content: data => data.property || '<property>', - colors: { - theme: ObiColors.LightBlue, - icon: ObiColors.AzureBlue, + 'ui:widget': CardTemplate, + header: { + 'ui:widget': ActionHeader, + title: data => `User Input (${getInputType(data.$type)})`, + disableSDKTitle: true, + icon: ElementIcon.User, + menu: 'none', + colors: { + theme: ObiColors.LightBlue, + icon: ObiColors.AzureBlue, + }, }, + body: data => + data.$type === SDKTypes.ChoiceInput && Array.isArray(data.choices) && data.choices.length ? ( + <ListOverview + items={data.choices} + renderItem={item => ( + <BorderedDiv height={20} title={item.value}> + {item.value} + </BorderedDiv> + )} + /> + ) : null, + footer: data => + data.property ? ( + <> + {data.property} <FixedInfo>= Input({getInputType(data.$type)})</FixedInfo> + </> + ) : null, }, }; -const ChoiceInputSchema: UIWidget = Object.assign({}, BaseInputSchema, { - userInput: { - 'ui:widget': ActionCard, - title: data => `User Input (${getInputType(data.$type)})`, - disableSDKTitle: true, - icon: ElementIcon.User, - menu: 'none', - content: data => <SingleLineDiv>{data.property || '<property>'}</SingleLineDiv>, - children: data => { - const renderItem = item => { - const content = item.value; - return ( - <BorderedDiv width={ChoiceInputSize.width} height={ChoiceInputSize.height} title={content}> - {content} - </BorderedDiv> - ); - }; - return <ListOverview items={data.choices} renderItem={renderItem} maxCount={3} />; - }, - colors: { - theme: ObiColors.LightBlue, - icon: ObiColors.AzureBlue, - }, - }, -}); export const uiSchema: UISchema = { default: { 'ui:widget': ActionCard, @@ -82,7 +82,6 @@ export const uiSchema: UISchema = { 'ui:widget': IfConditionWidget, judgement: { 'ui:widget': ActionCard, - title: formatMessage('Branch'), content: data => data.condition, }, }, @@ -90,7 +89,6 @@ export const uiSchema: UISchema = { 'ui:widget': SwitchConditionWidget, judgement: { 'ui:widget': ActionCard, - title: formatMessage('Branch'), content: data => data.condition, }, }, @@ -98,7 +96,6 @@ export const uiSchema: UISchema = { 'ui:widget': ForeachWidget, loop: { 'ui:widget': ActionCard, - title: formatMessage('Loop: For Each'), content: data => `${formatMessage('Each value in')} {${data.itemsProperty || '?'}}`, }, }, @@ -106,7 +103,6 @@ export const uiSchema: UISchema = { 'ui:widget': ForeachWidget, loop: { 'ui:widget': ActionCard, - title: formatMessage('Loop: For Each Page'), content: data => { const pageSizeString = get(data, 'pageSize', '?'); const propString = get(data, 'itemsProperty', '?'); @@ -115,12 +111,18 @@ export const uiSchema: UISchema = { }, }, [SDKTypes.SendActivity]: { - 'ui:widget': ActivityRenderer, - field: 'activity', - icon: ElementIcon.MessageBot, - colors: { - theme: ObiColors.BlueMagenta20, - icon: ObiColors.BlueMagenta30, + 'ui:widget': CardTemplate, + header: { + 'ui:widget': ActionHeader, + icon: ElementIcon.MessageBot, + colors: { + theme: ObiColors.BlueMagenta20, + icon: ObiColors.BlueMagenta30, + }, + }, + body: { + 'ui:widget': ActivityRenderer, + field: 'activity', }, }, [SDKTypes.AttachmentInput]: BaseInputSchema, @@ -128,43 +130,78 @@ export const uiSchema: UISchema = { [SDKTypes.DateTimeInput]: BaseInputSchema, [SDKTypes.NumberInput]: BaseInputSchema, [SDKTypes.TextInput]: BaseInputSchema, - [SDKTypes.ChoiceInput]: ChoiceInputSchema, + [SDKTypes.ChoiceInput]: BaseInputSchema, [SDKTypes.BeginDialog]: { - 'ui:widget': DialogRefCard, - dialog: data => data.dialog, + 'ui:widget': CardTemplate, + header: { + 'ui:widget': ActionHeader, + }, + body: { + 'ui:widget': DialogRef, + dialog: data => data.dialog, + getRefContent: data => dialogRef => ( + <> + {dialogRef || '?'} <FixedInfo>(Dialog)</FixedInfo> + </> + ), + }, + footer: data => + data.property ? ( + <> + {data.property} <FixedInfo>= Return value</FixedInfo> + </> + ) : null, }, [SDKTypes.ReplaceDialog]: { - 'ui:widget': DialogRefCard, - dialog: data => data.dialog, - getRefContent: data => dialogRef => <>Switch to {dialogRef}</>, + 'ui:widget': ActionCard, + content: { + 'ui:widget': DialogRef, + dialog: data => data.dialog, + getRefContent: data => dialogRef => ( + <> + {dialogRef || '?'} <FixedInfo>(Dialog)</FixedInfo> + </> + ), + }, }, [SDKTypes.EditArray]: { - 'ui:widget': ActionCard, - content: data => `${data.changeType} {${data.itemsProperty || '?'}}`, + 'ui:widget': CardTemplate, + header: { + 'ui:widget': ActionHeader, + }, + body: data => ( + <> + <FixedInfo>{data.changeType || '?'}</FixedInfo> {data.itemsProperty || '?'} + </> + ), + footer: data => + data.resultProperty ? ( + <> + {data.resultProperty} + <FixedInfo> = Result</FixedInfo> + </> + ) : null, }, [SDKTypes.SetProperty]: { 'ui:widget': ActionCard, - content: data => `{${data.property || '?'}} = ${data.value || '?'}`, + content: data => `${data.property || '?'} = ${data.value || '?'}`, }, [SDKTypes.SetProperties]: { 'ui:widget': ActionCard, - content: data => `Set ${Array.isArray(data.assignments) ? data.assignments.length : 0} property values`, - children: data => { - const renderItem = item => { - const content = `${item.property} = ${item.value}`; - return ( - <SingleLineDiv - width={PropertyAssignmentSize.width} - height={PropertyAssignmentSize.height} - title={content} - style={{ paddingLeft: '3px' }} - > - {content} - </SingleLineDiv> - ); - }; - return <ListOverview items={data.assignments} renderItem={renderItem} maxCount={3} />; - }, + content: data => ( + <ListOverview + items={data.assignments} + itemPadding={8} + renderItem={item => { + const content = `${item.property} = ${item.value}`; + return ( + <SingleLineDiv height={16} title={content}> + {content} + </SingleLineDiv> + ); + }} + /> + ), }, [SDKTypes.DeleteProperty]: { 'ui:widget': ActionCard, @@ -172,7 +209,17 @@ export const uiSchema: UISchema = { }, [SDKTypes.DeleteProperties]: { 'ui:widget': ActionCard, - content: data => `Delete ${Array.isArray(data.properties) ? data.properties.length : 0} properties`, + content: data => ( + <ListOverview + items={data.properties} + itemPadding={8} + renderItem={item => ( + <SingleLineDiv height={16} title={item}> + {item} + </SingleLineDiv> + )} + /> + ), }, [SDKTypes.EndDialog]: { 'ui:widget': ActionHeader, @@ -181,18 +228,45 @@ export const uiSchema: UISchema = { 'ui:widget': ActionHeader, }, [SDKTypes.CancelAllDialogs]: { - 'ui:widget': ActionHeader, + 'ui:widget': ActionCard, + content: data => + data.eventName ? ( + <> + {data.eventName || '?'} + <FixedInfo> (Event)</FixedInfo> + </> + ) : null, }, [SDKTypes.EndTurn]: { 'ui:widget': ActionHeader, }, [SDKTypes.EmitEvent]: { 'ui:widget': ActionCard, - content: data => data.eventName, + content: data => ( + <> + {data.eventName || '?'} + <FixedInfo> (Event)</FixedInfo> + </> + ), }, [SDKTypes.HttpRequest]: { - 'ui:widget': ActionCard, - content: data => data.url, + 'ui:widget': CardTemplate, + header: { + 'ui:widget': ActionHeader, + }, + body: data => ( + <SingleLineDiv> + <FixedInfo>{data.method} </FixedInfo> + {data.url} + </SingleLineDiv> + ), + footer: data => + data.resultProperty ? ( + <> + {data.resultProperty} + <FixedInfo> = Result property</FixedInfo> + </> + ) : null, }, [SDKTypes.TraceActivity]: { 'ui:widget': ActionHeader, @@ -209,7 +283,17 @@ export const uiSchema: UISchema = { content: data => data.hostname, }, [SDKTypes.OAuthInput]: { - 'ui:widget': ActionCard, - content: data => data.tokenProperty, + 'ui:widget': CardTemplate, + header: { + 'ui:widget': ActionHeader, + }, + body: data => <SingleLineDiv>{data.connectionName}</SingleLineDiv>, + footer: data => + data.tokenProperty ? ( + <> + {data.tokenProperty} + <FixedInfo> = Token Property</FixedInfo> + </> + ) : null, }, }; diff --git a/Composer/packages/extensions/visual-designer/src/widgets/ActionCard.tsx b/Composer/packages/extensions/visual-designer/src/widgets/ActionCard.tsx index db45d55919..28cb5fc06a 100644 --- a/Composer/packages/extensions/visual-designer/src/widgets/ActionCard.tsx +++ b/Composer/packages/extensions/visual-designer/src/widgets/ActionCard.tsx @@ -2,61 +2,17 @@ // Licensed under the MIT License. import React from 'react'; -import { generateSDKTitle } from '@bfc/shared'; -import { FormCard } from '../components/nodes/templates/FormCard'; import { WidgetContainerProps, WidgetComponent } from '../schema/uischema.types'; -import { ObiColors } from '../constants/ElementColors'; -import { NodeMenu } from '../components/menus/NodeMenu'; +import { CardTemplate } from '../components/nodes/templates/CardTemplate'; + +import { ActionHeader } from './ActionHeader'; export interface ActionCardProps extends WidgetContainerProps { title: string; - disableSDKTitle?: boolean; - icon: string; content: string | number | JSX.Element; - menu?: JSX.Element | string; - children?: JSX.Element; - size?: { - width: number; - - height: number; - }; - colors?: { - theme: string; - icon: string; - }; } -const DefaultCardColor = { - theme: ObiColors.AzureGray3, - icon: ObiColors.AzureGray2, -}; - -export const ActionCard: WidgetComponent<ActionCardProps> = ({ - id, - data, - onEvent, - title, - disableSDKTitle, - icon, - menu, - content, - children, - size, - colors = DefaultCardColor, -}) => { - const header = disableSDKTitle ? title : generateSDKTitle(data, title); - const nodeColors = { themeColor: colors.theme, iconColor: colors.icon }; - return ( - <FormCard - header={header} - corner={menu === 'none' ? null : menu || <NodeMenu id={id} onEvent={onEvent} />} - icon={icon} - label={content} - nodeColors={nodeColors} - styles={{ ...size }} - > - {children} - </FormCard> - ); +export const ActionCard: WidgetComponent<ActionCardProps> = ({ id, data, onEvent, title, content }) => { + return <CardTemplate header={<ActionHeader id={id} data={data} onEvent={onEvent} title={title} />} body={content} />; }; diff --git a/Composer/packages/extensions/visual-designer/src/widgets/ActionHeader.tsx b/Composer/packages/extensions/visual-designer/src/widgets/ActionHeader.tsx index e8b7db1dd6..9483175fb6 100644 --- a/Composer/packages/extensions/visual-designer/src/widgets/ActionHeader.tsx +++ b/Composer/packages/extensions/visual-designer/src/widgets/ActionHeader.tsx @@ -9,12 +9,16 @@ import { WidgetComponent, WidgetContainerProps } from '../schema/uischema.types' import { StandardNodeWidth, HeaderHeight } from '../constants/ElementSizes'; import { ObiColors } from '../constants/ElementColors'; import { NodeMenu } from '../components/menus/NodeMenu'; +import { ElementIcon } from '../utils/obiPropertyResolver'; +import { Icon } from '../components/decorations/icon'; +import { StandardFontCSS, TruncatedCSS } from '../components/elements/sharedCSS'; export interface ActionHeaderProps extends WidgetContainerProps { title: string; disableSDKTitle?: boolean; + icon?: string; menu?: JSX.Element | 'none'; - colors: { + colors?: { theme: string; icon: string; }; @@ -27,19 +31,14 @@ const DefaultColors = { const container = css` cursor: pointer; - font-family: Segoe UI; - font-size: 12px; - line-height: 14px; - color: black; + position: relative; + display: flex; + align-items: center; `; -const header = css` - font-size: 12px; - font-family: Segoe UI; - line-height: 14px; - overflow: hidden; - text-overflow: ellipsis; - whitespace: pre; +const headerText = css` + ${StandardFontCSS}; + ${TruncatedCSS}; `; export const ActionHeader: WidgetComponent<ActionHeaderProps> = ({ @@ -48,6 +47,7 @@ export const ActionHeader: WidgetComponent<ActionHeaderProps> = ({ onEvent, title, disableSDKTitle, + icon, menu, colors = DefaultColors, }) => { @@ -64,16 +64,36 @@ export const ActionHeader: WidgetComponent<ActionHeaderProps> = ({ > <div css={css` - ${header}; width: calc(100% - 40px); padding: 4px 8px; + display: flex; `} > - {headerContent} - </div> - <div css={{ position: 'absolute', top: 4, right: 0 }}> - {menu === 'none' ? null : menu || <NodeMenu id={id} onEvent={onEvent} />} + {icon && icon !== ElementIcon.None && ( + <div + css={{ + width: 16, + height: 16, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + marginRight: '5px', + }} + > + <Icon icon={icon} color={colors.icon} size={16} /> + </div> + )} + <div + css={css` + ${headerText}; + line-height: 16px; + transform: translateY(-1px); + `} + > + {headerContent} + </div> </div> + <div>{menu === 'none' ? null : menu || <NodeMenu id={id} onEvent={onEvent} />}</div> </div> ); }; diff --git a/Composer/packages/extensions/visual-designer/src/widgets/ActivityRenderer.tsx b/Composer/packages/extensions/visual-designer/src/widgets/ActivityRenderer.tsx index 40ba5283a9..e4c62ba83d 100644 --- a/Composer/packages/extensions/visual-designer/src/widgets/ActivityRenderer.tsx +++ b/Composer/packages/extensions/visual-designer/src/widgets/ActivityRenderer.tsx @@ -2,58 +2,22 @@ // Licensed under the MIT License. import React from 'react'; -import { generateSDKTitle } from '@bfc/shared'; import get from 'lodash/get'; -import { ElementIcon } from '../utils/obiPropertyResolver'; -import { NodeMenu } from '../components/menus/NodeMenu'; -import { FormCard } from '../components/nodes/templates/FormCard'; import { useLgTemplate } from '../utils/hooks'; import { WidgetContainerProps } from '../schema/uischema.types'; -import { ObiColors } from '../constants/ElementColors'; export interface ActivityRenderer extends WidgetContainerProps { /** indicates which field contains lg activity. ('activity', 'prompt', 'invalidPropmt'...) */ field: string; - icon: ElementIcon; - title?: string; - disableSDKTitle?: boolean; defaultContent?: string; - colors?: { - theme: string; - icon: string; - }; } -const DefaultThemeColor = { - theme: ObiColors.BlueMagenta20, - icon: ObiColors.BlueMagenta30, -}; - -export const ActivityRenderer: React.FC<ActivityRenderer> = ({ - id, - data, - onEvent, - title, - disableSDKTitle, - field, - defaultContent, - icon = ElementIcon.MessageBot, - colors = DefaultThemeColor, -}) => { +export const ActivityRenderer: React.FC<ActivityRenderer> = ({ data, field, defaultContent }) => { const designerId = get(data, '$designer.id'); const activityTemplate = get(data, field, ''); const templateText = useLgTemplate(activityTemplate, designerId); - const nodeColors = { themeColor: colors.theme, iconColor: colors.icon }; - - return ( - <FormCard - header={disableSDKTitle ? title : generateSDKTitle(data, title)} - label={templateText || defaultContent} - icon={icon} - corner={<NodeMenu id={id} onEvent={onEvent} />} - nodeColors={nodeColors} - /> - ); + const displayedText = templateText || defaultContent; + return <>{displayedText}</>; }; diff --git a/Composer/packages/extensions/visual-designer/src/widgets/DialogRef.tsx b/Composer/packages/extensions/visual-designer/src/widgets/DialogRef.tsx new file mode 100644 index 0000000000..50f53ffdba --- /dev/null +++ b/Composer/packages/extensions/visual-designer/src/widgets/DialogRef.tsx @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** @jsx jsx */ +import { jsx } from '@emotion/core'; +import get from 'lodash/get'; + +import { LinkBtn } from '../components/elements/styledComponents'; +import { WidgetContainerProps, WidgetComponent } from '../schema/uischema.types'; +import { NodeEventTypes } from '../constants/NodeEventTypes'; + +export interface DialogRefCardProps extends WidgetContainerProps { + dialog: string | object; + getRefContent?: (dialogRef: JSX.Element | null) => JSX.Element; +} + +export const DialogRef: WidgetComponent<DialogRefCardProps> = ({ id, onEvent, dialog, getRefContent }) => { + const calleeDialog = typeof dialog === 'object' ? get(dialog, '$ref') : dialog; + const dialogRef = calleeDialog ? ( + <LinkBtn + onClick={e => { + e.stopPropagation(); + onEvent(NodeEventTypes.OpenDialog, { caller: id, callee: calleeDialog }); + }} + > + {calleeDialog} + </LinkBtn> + ) : null; + return typeof getRefContent === 'function' ? getRefContent(dialogRef) : dialogRef; +}; diff --git a/Composer/packages/extensions/visual-designer/src/widgets/DialogRefCard.tsx b/Composer/packages/extensions/visual-designer/src/widgets/DialogRefCard.tsx deleted file mode 100644 index 75a7a97a3d..0000000000 --- a/Composer/packages/extensions/visual-designer/src/widgets/DialogRefCard.tsx +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -/** @jsx jsx */ -import { jsx } from '@emotion/core'; -import { generateSDKTitle } from '@bfc/shared'; -import get from 'lodash/get'; - -import { LinkBtn } from '../components/elements/styledComponents'; -import { FormCard } from '../components/nodes/templates/FormCard'; -import { WidgetContainerProps, WidgetComponent } from '../schema/uischema.types'; -import { ObiColors } from '../constants/ElementColors'; -import { NodeEventTypes } from '../constants/NodeEventTypes'; -import { NodeMenu } from '../components/menus/NodeMenu'; - -export interface DialogRefCardProps extends WidgetContainerProps { - dialog: string | object; - getRefContent?: (dialogRef: JSX.Element) => JSX.Element; - colors?: { - theme: string; - icon: string; - }; -} - -const DefaultCardColor = { - theme: ObiColors.AzureGray3, - icon: ObiColors.AzureGray2, -}; - -export const DialogRefCard: WidgetComponent<DialogRefCardProps> = ({ - id, - data, - onEvent, - dialog, - getRefContent, - colors = DefaultCardColor, -}) => { - const header = generateSDKTitle(data); - const nodeColors = { themeColor: colors.theme, iconColor: colors.icon }; - const calleeDialog = typeof dialog === 'object' ? get(dialog, '$ref') : dialog; - const dialogRef = ( - <LinkBtn - onClick={e => { - e.stopPropagation(); - onEvent(NodeEventTypes.OpenDialog, { caller: id, callee: calleeDialog }); - }} - > - {calleeDialog} - </LinkBtn> - ); - return ( - <FormCard - header={header} - corner={<NodeMenu id={id} onEvent={onEvent} />} - label={typeof getRefContent === 'function' ? getRefContent(dialogRef) : dialogRef} - nodeColors={nodeColors} - /> - ); -}; From 781f387781164c55580df35262e12dfae86dd2bc Mon Sep 17 00:00:00 2001 From: Chris Whitten <christopher.whitten@microsoft.com> Date: Wed, 4 Mar 2020 14:54:36 -0800 Subject: [PATCH 19/40] feat: Added inline lu to prompts (#2159) Co-authored-by: Chris Whitten <christopher.whitten@microsoft.com> --- .../src/Form/fields/PromptField/UserInput.tsx | 16 +++- .../src/Form/fields/PromptField/index.tsx | 53 ++++------- .../src/Form/fields/PromptField/styles.ts | 4 - .../src/Form/widgets/IntentWidget.tsx | 87 +++++++------------ .../src/Form/widgets/LuEditorWidget.tsx | 68 ++++++++------- 5 files changed, 104 insertions(+), 124 deletions(-) diff --git a/Composer/packages/extensions/obiformeditor/src/Form/fields/PromptField/UserInput.tsx b/Composer/packages/extensions/obiformeditor/src/Form/fields/PromptField/UserInput.tsx index 44aac5ef1f..97553ed0f0 100644 --- a/Composer/packages/extensions/obiformeditor/src/Form/fields/PromptField/UserInput.tsx +++ b/Composer/packages/extensions/obiformeditor/src/Form/fields/PromptField/UserInput.tsx @@ -10,11 +10,13 @@ import { JSONSchema6 } from 'json-schema'; import { SDKTypes, MicrosoftInputDialog, ChoiceInput, ConfirmInput } from '@bfc/shared'; import { TextWidget, SelectWidget } from '../../widgets'; +import { LuEditorWidget } from '../../widgets/LuEditorWidget'; import { field } from './styles'; import { GetSchema, PromptFieldChangeHandler } from './types'; import { ChoiceInputSettings } from './ChoiceInput'; import { ConfirmInputSettings } from './ConfirmInput'; +import { DialogInfo } from '@bfc/indexers/lib/type'; const getOptions = (enumSchema: JSONSchema6) => { if (!enumSchema || !enumSchema.enum || !Array.isArray(enumSchema.enum)) { @@ -24,16 +26,28 @@ const getOptions = (enumSchema: JSONSchema6) => { return enumSchema.enum.map(o => ({ label: o as string, value: o as string })); }; +const usesLuisRecognizer = ({ content }: DialogInfo) => { + return typeof content?.recognizer === 'string'; +}; + interface UserInputProps extends FieldProps<MicrosoftInputDialog> { onChange: PromptFieldChangeHandler; getSchema: GetSchema; } export const UserInput: React.FC<UserInputProps> = props => { - const { onChange, getSchema, idSchema, formData, errorSchema } = props; + const { formContext, onChange, getSchema, idSchema, formData, errorSchema } = props; + const { const: type } = getSchema('$type'); + const [, promptType] = (type as string).split('.'); + const intentName = `${promptType}.response-${formData?.$designer?.id}`; return ( <Fragment> + {usesLuisRecognizer(formContext.currentDialog) && ( + <div css={field}> + <LuEditorWidget name={intentName} formContext={formContext} /> + </div> + )} <div css={field}> <TextWidget onChange={onChange('property')} diff --git a/Composer/packages/extensions/obiformeditor/src/Form/fields/PromptField/index.tsx b/Composer/packages/extensions/obiformeditor/src/Form/fields/PromptField/index.tsx index e1c1a6a4b8..5aaf5df06d 100644 --- a/Composer/packages/extensions/obiformeditor/src/Form/fields/PromptField/index.tsx +++ b/Composer/packages/extensions/obiformeditor/src/Form/fields/PromptField/index.tsx @@ -4,7 +4,6 @@ /** @jsx jsx */ import { jsx } from '@emotion/core'; import React from 'react'; -import { IdSchema } from '@bfcomposer/react-jsonschema-form'; import formatMessage from 'format-message'; import { Pivot, PivotLinkSize, PivotItem } from 'office-ui-fabric-react/lib/Pivot'; import get from 'lodash/get'; @@ -13,7 +12,7 @@ import { PromptTab } from '@bfc/shared'; import { BaseField } from '../BaseField'; import { BFDFieldProps } from '../../types'; -import { tabs, tabsContainer, settingsContainer } from './styles'; +import { tabs } from './styles'; import { BotAsks } from './BotAsks'; import { UserInput } from './UserInput'; import { Other } from './Other'; @@ -23,7 +22,6 @@ import { GetSchema, PromptFieldChangeHandler } from './types'; export const PromptField: React.FC<BFDFieldProps> = props => { const { formContext } = props; const { shellApi, focusedTab, focusedSteps } = formContext; - const promptSettingsIdSchema = ({ __id: props.idSchema.__id + 'promptSettings' } as unknown) as IdSchema; const getSchema: GetSchema = field => { const fieldSchema = get(props.schema, ['properties', field]); @@ -43,38 +41,23 @@ export const PromptField: React.FC<BFDFieldProps> = props => { return ( <BaseField {...props}> - <div css={tabsContainer}> - <Pivot - linkSize={PivotLinkSize.large} - styles={tabs} - selectedKey={focusedTab || PromptTab.BOT_ASKS} - onLinkClick={handleTabChange} - > - <PivotItem headerText={formatMessage('Bot Asks')} itemKey={PromptTab.BOT_ASKS}> - <BotAsks {...props} onChange={updateField} getSchema={getSchema} /> - </PivotItem> - <PivotItem headerText={formatMessage('User Input')} itemKey={PromptTab.USER_INPUT}> - <UserInput {...props} onChange={updateField} getSchema={getSchema} /> - </PivotItem> - <PivotItem headerText={formatMessage('Other')} itemKey={PromptTab.OTHER}> - <Other {...props} onChange={updateField} getSchema={getSchema} /> - </PivotItem> - </Pivot> - </div> - - <div className="ObjectItem ObjectItemContainer" css={settingsContainer}> - <div className="ObjectItemField"> - <BaseField - formContext={props.formContext} - formData={props.formData} - idSchema={promptSettingsIdSchema} - title={formatMessage('Prompt settings')} - schema={{}} - > - <PromptSettings {...props} onChange={updateField} getSchema={getSchema} /> - </BaseField> - </div> - </div> + <Pivot + linkSize={PivotLinkSize.large} + styles={tabs} + selectedKey={focusedTab || PromptTab.BOT_ASKS} + onLinkClick={handleTabChange} + > + <PivotItem headerText={formatMessage('Bot Asks')} itemKey={PromptTab.BOT_ASKS}> + <BotAsks {...props} onChange={updateField} getSchema={getSchema} /> + </PivotItem> + <PivotItem headerText={formatMessage('User Input')} itemKey={PromptTab.USER_INPUT}> + <UserInput {...props} onChange={updateField} getSchema={getSchema} /> + </PivotItem> + <PivotItem headerText={formatMessage('Other')} itemKey={PromptTab.OTHER}> + <Other {...props} onChange={updateField} getSchema={getSchema} /> + <PromptSettings {...props} onChange={updateField} getSchema={getSchema} /> + </PivotItem> + </Pivot> </BaseField> ); }; diff --git a/Composer/packages/extensions/obiformeditor/src/Form/fields/PromptField/styles.ts b/Composer/packages/extensions/obiformeditor/src/Form/fields/PromptField/styles.ts index aecd3cf0cf..6cc18679b6 100644 --- a/Composer/packages/extensions/obiformeditor/src/Form/fields/PromptField/styles.ts +++ b/Composer/packages/extensions/obiformeditor/src/Form/fields/PromptField/styles.ts @@ -21,10 +21,6 @@ export const tabs: Partial<IPivotStyles> = { }, }; -export const tabsContainer = css` - border-bottom: 1px solid #c8c6c4; -`; - export const validationItemInput = css` display: flex; align-items: center; diff --git a/Composer/packages/extensions/obiformeditor/src/Form/widgets/IntentWidget.tsx b/Composer/packages/extensions/obiformeditor/src/Form/widgets/IntentWidget.tsx index bc3579bb6d..e5121ab40c 100644 --- a/Composer/packages/extensions/obiformeditor/src/Form/widgets/IntentWidget.tsx +++ b/Composer/packages/extensions/obiformeditor/src/Form/widgets/IntentWidget.tsx @@ -4,7 +4,6 @@ import React from 'react'; import { Dropdown, ResponsiveMode, IDropdownOption } from 'office-ui-fabric-react/lib/Dropdown'; import formatMessage from 'format-message'; -import { RegexRecognizer } from '@bfc/shared'; import { DialogInfo } from '@bfc/indexers'; import { BFDWidgetProps } from '../types'; @@ -19,58 +18,35 @@ enum RecognizerType { 'luis', } -function recognizerType(currentDialog: DialogInfo): RecognizerType | null { - const recognizer = currentDialog.content.recognizer; - if (!recognizer) { - return null; - } +function recognizerType({ content }: DialogInfo): RecognizerType | null { + const { recognizer } = content; - if (typeof recognizer === 'object' && recognizer.$type === 'Microsoft.RegexRecognizer') { - return RecognizerType.regex; - } else if (typeof recognizer === 'string') { - return RecognizerType.luis; + if (recognizer) { + if (typeof recognizer === 'object' && recognizer.$type === 'Microsoft.RegexRecognizer') { + return RecognizerType.regex; + } else if (typeof recognizer === 'string') { + return RecognizerType.luis; + } } return null; } -function regexIntentOptions(currentDialog: DialogInfo): IDropdownOption[] { - const recognizer = currentDialog.content.recognizer as RegexRecognizer; - let options: IDropdownOption[] = [EMPTY_OPTION]; - - if (!recognizer) { - return options; - } - - if (recognizer.intents) { - options = options.concat(recognizer.intents.map(i => ({ key: i.intent, text: i.intent }))); - } - - return options; +function regexIntentOptions({ content }: DialogInfo): IDropdownOption[] { + const { recognizer } = content; + return (recognizer?.intents || []).reduce( + (acc, { intent }) => (intent ? [...acc, { key: intent, text: intent }] : acc), + [EMPTY_OPTION] + ); } export const IntentWidget: React.FC<BFDWidgetProps> = props => { const { disabled, onChange, id, onFocus, onBlur, value, formContext, placeholder, label, schema } = props; const { description } = schema; const { currentDialog } = formContext; - let options: IDropdownOption[] = []; - let widgetLabel = label; - let isLuisSelected = false; - switch (recognizerType(currentDialog)) { - case RecognizerType.regex: - options = regexIntentOptions(currentDialog); - isLuisSelected = false; - break; - case RecognizerType.luis: - widgetLabel = `Trigger phrases(intent name: #${value || ''})`; - isLuisSelected = true; - break; - default: - options = [EMPTY_OPTION]; - isLuisSelected = false; - break; - } + const type = recognizerType(currentDialog); + const options: IDropdownOption[] = type === RecognizerType.regex ? regexIntentOptions(currentDialog) : [EMPTY_OPTION]; const handleChange = (_e, option): void => { if (option) { @@ -80,21 +56,24 @@ export const IntentWidget: React.FC<BFDWidgetProps> = props => { return ( <> - <WidgetLabel label={widgetLabel} description={description} id={id} /> - {!isLuisSelected && ( - <Dropdown - id={id.replace(/\.|#/g, '')} - onBlur={() => onBlur && onBlur(id, value)} - onChange={handleChange} - onFocus={() => onFocus && onFocus(id, value)} - options={options} - selectedKey={value || null} - responsiveMode={ResponsiveMode.large} - disabled={disabled || options.length === 1} - placeholder={options.length > 1 ? placeholder : formatMessage('No intents configured for this dialog')} - /> + {type === RecognizerType.luis ? ( + <LuEditorWidget formContext={formContext} name={value} height={316} /> + ) : ( + <> + <WidgetLabel label={label} description={description} id={id} /> + <Dropdown + id={id} + onBlur={() => onBlur && onBlur(id, value)} + onChange={handleChange} + onFocus={() => onFocus && onFocus(id, value)} + options={options} + selectedKey={value || null} + responsiveMode={ResponsiveMode.large} + disabled={disabled || options.length === 1} + placeholder={options.length > 1 ? placeholder : formatMessage('No intents configured for this dialog')} + /> + </> )} - {isLuisSelected && <LuEditorWidget formContext={formContext} onChange={onChange} name={value} height={316} />} </> ); }; diff --git a/Composer/packages/extensions/obiformeditor/src/Form/widgets/LuEditorWidget.tsx b/Composer/packages/extensions/obiformeditor/src/Form/widgets/LuEditorWidget.tsx index 0d05aac956..c551e5bdab 100644 --- a/Composer/packages/extensions/obiformeditor/src/Form/widgets/LuEditorWidget.tsx +++ b/Composer/packages/extensions/obiformeditor/src/Form/widgets/LuEditorWidget.tsx @@ -2,18 +2,20 @@ // Licensed under the MIT License. import React from 'react'; -import { LuEditor } from '@bfc/code-editor'; import debounce from 'lodash/debounce'; -import { LuIntentSection } from '@bfc/shared'; +import formatMessage from 'format-message'; +import { LuEditor } from '@bfc/code-editor'; import { LuFile, filterSectionDiagnostics } from '@bfc/indexers'; +import { LuIntentSection } from '@bfc/shared'; import { FormContext } from '../types'; +import { WidgetLabel } from './WidgetLabel'; interface LuEditorWidgetProps { formContext: FormContext; name: string; height?: number | string; - onChange: (template?: string) => void; + onChange?: (template?: string) => void; } export class LuEditorWidget extends React.Component<LuEditorWidgetProps> { @@ -33,7 +35,7 @@ export class LuEditorWidget extends React.Component<LuEditorWidgetProps> { formContext: FormContext; name: string; luFileId: string; - luFile: LuFile | null; + luFile?: LuFile; luIntent: LuIntentSection; state = { localValue: '' }; debounceUpdate; @@ -64,44 +66,50 @@ export class LuEditorWidget extends React.Component<LuEditorWidgetProps> { }); if (this.luFileId) { if (body) { - this.updateLuIntent(body); + this.debounceUpdate(body); } else { this.formContext.shellApi.removeLuIntent(this.luFileId, this.name); } } }; + render() { - const diagnostic = this.luFile && filterSectionDiagnostics(this.luFile.diagnostics, this.luIntent)[0]; + const { height = 250 } = this.props; + const { luFile, luFileId, luIntent, name } = this; + const diagnostic = luFile && filterSectionDiagnostics(luFile.diagnostics, luIntent)[0]; const errorMsg = diagnostic ? diagnostic.message.split('error message: ')[diagnostic.message.split('error message: ').length - 1] : ''; - const luOption = { - fileId: this.luFileId, - sectionId: this.luIntent?.Name, - }; - const height = this.props.height || 250; + + const label = formatMessage('Trigger phrases (intent: #{name})', { name }); return ( - <LuEditor - onChange={this.onChange} - value={this.state.localValue} - errorMsg={errorMsg} - hidePlaceholder={true} - luOption={luOption} - options={{ - lineNumbers: 'off', - minimap: { - enabled: false, - }, - lineDecorationsWidth: 10, - lineNumbersMinChars: 0, - glyphMargin: false, - folding: false, - renderLineHighlight: 'none', - }} - height={height} - /> + <> + <WidgetLabel label={label} /> + <LuEditor + onChange={this.onChange} + value={this.state.localValue} + errorMsg={errorMsg} + hidePlaceholder={true} + luOption={{ + fileId: luFileId, + sectionId: luIntent?.Name, + }} + options={{ + lineNumbers: 'off', + minimap: { + enabled: false, + }, + lineDecorationsWidth: 10, + lineNumbersMinChars: 0, + glyphMargin: false, + folding: false, + renderLineHighlight: 'none', + }} + height={height} + /> + </> ); } } From e51dc5a22966f5978d1eeb521a43442a507147a3 Mon Sep 17 00:00:00 2001 From: Chris Whitten <christopher.whitten@microsoft.com> Date: Thu, 5 Mar 2020 15:09:05 -0800 Subject: [PATCH 20/40] feat: Add cross train before luis publish (#2069) * Add cross train before luis publish * update the publish flow * create cross train config * fix some bug * update the lubuild version * update the bf-lu version and remove the composer's file update check * fix bug: the file add error * remove the status test * fix unit test * update the bf-lu * update the function * update the crosstrain config * fix lint * add some comments --- Composer/packages/client/package.json | 2 +- .../src/Form/fields/PromptField/UserInput.tsx | 2 +- .../src/Form/widgets/LuEditorWidget.tsx | 1 + Composer/packages/lib/indexers/package.json | 2 +- .../lib/indexers/src/dialogIndexer.ts | 9 +- .../src/dialogUtils/extractIntentTriggers.ts | 44 +++ .../lib/indexers/src/dialogUtils/types.ts | 5 + .../packages/lib/indexers/src/luIndexer.ts | 2 +- Composer/packages/lib/indexers/src/type.ts | 2 + .../lib/indexers/src/types/bf-lu.d.ts | 2 +- .../packages/lib/indexers/src/utils/luUtil.ts | 2 +- .../models/bot/luisPublisher.test.ts | 68 ---- Composer/packages/server/package.json | 2 +- .../server/src/models/bot/botProject.ts | 56 +-- .../server/src/models/bot/interface.ts | 7 - .../server/src/models/bot/luPublisher.ts | 319 +++++++++++------- .../server/src/models/storage/interface.ts | 2 +- .../src/models/storage/localDiskStorage.ts | 2 +- Composer/packages/server/src/types/bf-lu.d.ts | 35 -- .../language-understanding/package.json | 4 +- Composer/yarn.lock | 146 ++++++-- 21 files changed, 395 insertions(+), 319 deletions(-) create mode 100644 Composer/packages/lib/indexers/src/dialogUtils/extractIntentTriggers.ts delete mode 100644 Composer/packages/server/__tests__/models/bot/luisPublisher.test.ts delete mode 100644 Composer/packages/server/src/types/bf-lu.d.ts diff --git a/Composer/packages/client/package.json b/Composer/packages/client/package.json index 5c8da74fd9..842169d718 100644 --- a/Composer/packages/client/package.json +++ b/Composer/packages/client/package.json @@ -20,7 +20,7 @@ "@bfc/extensions": "*", "@bfc/indexers": "*", "@bfc/shared": "*", - "@bfcomposer/bf-lu": "^1.2.5", + "@bfcomposer/bf-lu": "^1.2.9", "@emotion/core": "^10.0.7", "@reach/router": "^1.2.1", "axios": "^0.18.0", diff --git a/Composer/packages/extensions/obiformeditor/src/Form/fields/PromptField/UserInput.tsx b/Composer/packages/extensions/obiformeditor/src/Form/fields/PromptField/UserInput.tsx index 97553ed0f0..d2550e546d 100644 --- a/Composer/packages/extensions/obiformeditor/src/Form/fields/PromptField/UserInput.tsx +++ b/Composer/packages/extensions/obiformeditor/src/Form/fields/PromptField/UserInput.tsx @@ -8,6 +8,7 @@ import { FieldProps } from '@bfcomposer/react-jsonschema-form'; import formatMessage from 'format-message'; import { JSONSchema6 } from 'json-schema'; import { SDKTypes, MicrosoftInputDialog, ChoiceInput, ConfirmInput } from '@bfc/shared'; +import { DialogInfo } from '@bfc/indexers/lib/type'; import { TextWidget, SelectWidget } from '../../widgets'; import { LuEditorWidget } from '../../widgets/LuEditorWidget'; @@ -16,7 +17,6 @@ import { field } from './styles'; import { GetSchema, PromptFieldChangeHandler } from './types'; import { ChoiceInputSettings } from './ChoiceInput'; import { ConfirmInputSettings } from './ConfirmInput'; -import { DialogInfo } from '@bfc/indexers/lib/type'; const getOptions = (enumSchema: JSONSchema6) => { if (!enumSchema || !enumSchema.enum || !Array.isArray(enumSchema.enum)) { diff --git a/Composer/packages/extensions/obiformeditor/src/Form/widgets/LuEditorWidget.tsx b/Composer/packages/extensions/obiformeditor/src/Form/widgets/LuEditorWidget.tsx index c551e5bdab..dba358ad9f 100644 --- a/Composer/packages/extensions/obiformeditor/src/Form/widgets/LuEditorWidget.tsx +++ b/Composer/packages/extensions/obiformeditor/src/Form/widgets/LuEditorWidget.tsx @@ -9,6 +9,7 @@ import { LuFile, filterSectionDiagnostics } from '@bfc/indexers'; import { LuIntentSection } from '@bfc/shared'; import { FormContext } from '../types'; + import { WidgetLabel } from './WidgetLabel'; interface LuEditorWidgetProps { diff --git a/Composer/packages/lib/indexers/package.json b/Composer/packages/lib/indexers/package.json index 461948af75..b0102f5096 100644 --- a/Composer/packages/lib/indexers/package.json +++ b/Composer/packages/lib/indexers/package.json @@ -29,7 +29,7 @@ }, "dependencies": { "@bfc/shared": "*", - "@bfcomposer/bf-lu": "^1.2.5", + "@bfcomposer/bf-lu": "^1.2.9", "botbuilder-lg": "^4.8.0-preview.106823", "botframework-expressions": "^4.8.0-preview.106476", "lodash": "^4.17.15" diff --git a/Composer/packages/lib/indexers/src/dialogIndexer.ts b/Composer/packages/lib/indexers/src/dialogIndexer.ts index eb36ee6ae6..fe89692f22 100644 --- a/Composer/packages/lib/indexers/src/dialogIndexer.ts +++ b/Composer/packages/lib/indexers/src/dialogIndexer.ts @@ -12,7 +12,7 @@ import { JsonWalk, VisitorFunc } from './utils/jsonWalk'; import { getBaseName } from './utils/help'; import { Diagnostic } from './diagnostic'; import ExtractMemoryPaths from './dialogUtils/extractMemoryPaths'; - +import ExtractIntentTriggers from './dialogUtils/extractIntentTriggers'; // find out all lg templates given dialog function ExtractLgTemplates(id, dialog): LgTemplateJsonPath[] { const templates: LgTemplateJsonPath[] = []; @@ -95,7 +95,7 @@ function ExtractLuIntents(dialog, id: string): ReferredLuIntents[] { // find out all triggers given dialog function ExtractTriggers(dialog): ITrigger[] { - const trigers: ITrigger[] = []; + const triggers: ITrigger[] = []; /** * * @param path , jsonPath string * @param value , current node value * @@ -118,7 +118,7 @@ function ExtractTriggers(dialog): ITrigger[] { } else if (trigger.isIntent && has(rule, 'intent')) { trigger.displayName = rule.intent; } - trigers.push(trigger); + triggers.push(trigger); } }); return true; @@ -126,7 +126,7 @@ function ExtractTriggers(dialog): ITrigger[] { return false; }; JsonWalk('$', dialog, visitor); - return trigers; + return triggers; } // find out all referred dialog @@ -204,6 +204,7 @@ function parse(id: string, content: any, schema: any) { luFile: getBaseName(luFile, '.lu'), lgFile: getBaseName(lgFile, '.lg'), triggers: ExtractTriggers(content), + intentTriggers: ExtractIntentTriggers(content), }; } diff --git a/Composer/packages/lib/indexers/src/dialogUtils/extractIntentTriggers.ts b/Composer/packages/lib/indexers/src/dialogUtils/extractIntentTriggers.ts new file mode 100644 index 0000000000..ff5e4bf01d --- /dev/null +++ b/Composer/packages/lib/indexers/src/dialogUtils/extractIntentTriggers.ts @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +import { SDKTypes } from '@bfc/shared'; + +import { VisitorFunc, JsonWalk } from '../utils/jsonWalk'; + +import { IIntentTrigger } from './types'; + +// find out all properties from given dialog +function ExtractIntentTriggers(value: any): IIntentTrigger[] { + const triggers: IIntentTrigger[] = []; + + const visitor: VisitorFunc = (path: string, value: any): boolean => { + if (value?.$type === SDKTypes.OnIntent) { + if (value.intent) { + const dialogs: string[] = []; + + const visitor: VisitorFunc = (path: string, value: any): boolean => { + if (value?.$type === SDKTypes.BeginDialog) { + if (value.dialog) { + dialogs.push(value.dialog); + } + return true; + } + return false; + }; + JsonWalk('$', value, visitor); + if (dialogs.length) { + triggers.push({ + intent: value.intent, + dialogs, + }); + } + } + return true; + } + return false; + }; + JsonWalk('$', value, visitor); + + return triggers; +} + +export default ExtractIntentTriggers; diff --git a/Composer/packages/lib/indexers/src/dialogUtils/types.ts b/Composer/packages/lib/indexers/src/dialogUtils/types.ts index 4e7960f655..974ccef47f 100644 --- a/Composer/packages/lib/indexers/src/dialogUtils/types.ts +++ b/Composer/packages/lib/indexers/src/dialogUtils/types.ts @@ -23,4 +23,9 @@ export interface IExpressionProperties { }; } +export interface IIntentTrigger { + intent: string; + dialogs: string[]; +} + export type CheckerFunc = (path: string, value: any, type: string, schema: any) => Diagnostic[] | null; // error msg diff --git a/Composer/packages/lib/indexers/src/luIndexer.ts b/Composer/packages/lib/indexers/src/luIndexer.ts index 164ea2d89c..2a17c1dae9 100644 --- a/Composer/packages/lib/indexers/src/luIndexer.ts +++ b/Composer/packages/lib/indexers/src/luIndexer.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { sectionHandler } from '@bfcomposer/bf-lu/lib/parser'; +import { sectionHandler } from '@bfcomposer/bf-lu/lib/parser/composerindex'; import get from 'lodash/get'; import { LuIntentSection } from '@bfc/shared'; diff --git a/Composer/packages/lib/indexers/src/type.ts b/Composer/packages/lib/indexers/src/type.ts index 5b47641a57..db7c525bc7 100644 --- a/Composer/packages/lib/indexers/src/type.ts +++ b/Composer/packages/lib/indexers/src/type.ts @@ -4,6 +4,7 @@ import { LuIntentSection } from '@bfc/shared'; import { Diagnostic } from './diagnostic'; +import { IIntentTrigger } from './dialogUtils/types'; export interface FileInfo { name: string; @@ -38,6 +39,7 @@ export interface DialogInfo { relativePath: string; userDefinedVariables: string[]; triggers: ITrigger[]; + intentTriggers: IIntentTrigger[]; } export interface LgTemplateJsonPath { diff --git a/Composer/packages/lib/indexers/src/types/bf-lu.d.ts b/Composer/packages/lib/indexers/src/types/bf-lu.d.ts index 2383096793..f8df414a4c 100644 --- a/Composer/packages/lib/indexers/src/types/bf-lu.d.ts +++ b/Composer/packages/lib/indexers/src/types/bf-lu.d.ts @@ -1,7 +1,7 @@ /// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -declare module '@bfcomposer/bf-lu/lib/parser' { +declare module '@bfcomposer/bf-lu/lib/parser/composerindex' { namespace parser { function parseFile(fileContent: any, log: any, locale: any): any; function validateLUISBlob(LUISJSONBlob: any): any; diff --git a/Composer/packages/lib/indexers/src/utils/luUtil.ts b/Composer/packages/lib/indexers/src/utils/luUtil.ts index 0baac955ff..c4127f8b88 100644 --- a/Composer/packages/lib/indexers/src/utils/luUtil.ts +++ b/Composer/packages/lib/indexers/src/utils/luUtil.ts @@ -7,7 +7,7 @@ * for more usage detail, please check client/__tests__/utils/luUtil.test.ts */ -import { sectionHandler } from '@bfcomposer/bf-lu/lib/parser'; +import { sectionHandler } from '@bfcomposer/bf-lu/lib/parser/composerindex'; import isEmpty from 'lodash/isEmpty'; import { LuIntentSection } from '@bfc/shared'; diff --git a/Composer/packages/server/__tests__/models/bot/luisPublisher.test.ts b/Composer/packages/server/__tests__/models/bot/luisPublisher.test.ts deleted file mode 100644 index 45c32fcefe..0000000000 --- a/Composer/packages/server/__tests__/models/bot/luisPublisher.test.ts +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -import { FileUpdateType } from '../../../src/models/bot/interface'; -import { Path } from '../../../src/utility/path'; - -import { LuPublisher } from './../../../src/models/bot/luPublisher'; -import service from './../../../src/services/storage'; - -const botDir = Path.join(__dirname, '../../mocks/samplebots/bot1'); -const storage = service.getStorageClient('default'); - -describe('luis status management', () => { - it('will get luis status', async () => { - const luPublisher = new LuPublisher(botDir, storage); - const status = await luPublisher.loadStatus(['bot1/a.lu', 'bot1/b.lu', 'bot1/Main.lu']); - expect(status['bot1/a.lu'].lastUpdateTime).toBe(1); - expect(status['bot1/a.lu'].lastPublishTime).toBe(0); - }); - - it('can update luis status', async () => { - const luPublisher = new LuPublisher(botDir, storage); - - await luPublisher.loadStatus(['bot1/a.lu', 'bot1/b.lu', 'bot1/Main.lu']); - const oldUpdateTime = luPublisher.status['bot1/a.lu'].lastUpdateTime; - - await luPublisher.onFileChange('bot1/a.lu', FileUpdateType.UPDATE); - const newUpdateTime = luPublisher.status['bot1/a.lu'].lastUpdateTime; - // update should increase the update time - expect(newUpdateTime).toBeGreaterThan(oldUpdateTime); - }); -}); - -describe('get unpublishedFiles', () => { - it('will get unpublished files', async () => { - const lufiles = [ - { - diagnostics: [], - id: 'a', - relativePath: 'bot1/a.lu', - content: '', - intents: [], - }, - { - diagnostics: [], - id: 'b', - relativePath: 'bot1/b.lu', - content: '', - intents: [], - }, - ]; - - const luPublisher = new LuPublisher(botDir, storage); - await luPublisher.loadStatus(['bot1/a.lu', 'bot1/b.lu']); // relative path is key - - let files = luPublisher.getUnpublisedFiles(lufiles); - expect(files.length).toBe(2); - const curTime = Date.now(); - luPublisher.status['bot1/a.lu'].lastPublishTime = curTime; // assumming we publish a.lu - luPublisher.status['bot1/b.lu'].lastPublishTime = curTime; // and b.lu - files = luPublisher.getUnpublisedFiles(lufiles); - expect(files.length).toBe(0); - - await luPublisher.onFileChange('bot1/a.lu', FileUpdateType.UPDATE); - files = luPublisher.getUnpublisedFiles(lufiles); - expect(files.length).toBe(1); - }); -}); diff --git a/Composer/packages/server/package.json b/Composer/packages/server/package.json index 08e07b6842..aab1124761 100644 --- a/Composer/packages/server/package.json +++ b/Composer/packages/server/package.json @@ -57,7 +57,7 @@ "@bfc/lg-languageserver": "*", "@bfc/lu-languageserver": "*", "@bfc/shared": "*", - "@bfcomposer/bf-lu": "^1.2.5", + "@bfcomposer/bf-lu": "^1.2.9", "archiver": "^3.0.0", "axios": "^0.18.0", "azure-storage": "^2.10.3", diff --git a/Composer/packages/server/src/models/bot/botProject.ts b/Composer/packages/server/src/models/bot/botProject.ts index 22e86e5579..036b7b6d64 100644 --- a/Composer/packages/server/src/models/bot/botProject.ts +++ b/Composer/packages/server/src/models/bot/botProject.ts @@ -23,7 +23,7 @@ import { ISettingManager, OBFUSCATED_VALUE } from '../settings'; import log from '../../logger'; import { IFileStorage } from './../storage/interface'; -import { LocationRef, LuisStatus, FileUpdateType } from './interface'; +import { LocationRef } from './interface'; import { LuPublisher } from './luPublisher'; import { DialogSetting } from './interface'; @@ -89,7 +89,6 @@ export class BotProject { if (this.settings) { await this.luPublisher.setLuisConfig(this.settings.luis); } - await this.luPublisher.loadStatus(this.luFiles.map(f => f.relativePath)); }; public getIndexes = () => { @@ -99,7 +98,7 @@ export class BotProject { location: this.dir, dialogs: this.dialogs, lgFiles: this.lgFiles, - luFiles: this.mergeLuStatus(this.luFiles, this.luPublisher.status), + luFiles: this.luFiles, schemas: this.getSchemas(), botEnvironment: this.environment.getEnvironmentName(this.name), settings: this.settings, @@ -133,25 +132,6 @@ export class BotProject { await this.luPublisher.setLuisConfig(config.luis); }; - // merge the status managed by luPublisher to the LuFile structure to keep a - // unified interface - private mergeLuStatus = ( - luFiles: LuFile[], - luStatus: { - [key: string]: LuisStatus; - } - ) => { - return luFiles.map(x => { - if (!luStatus[x.relativePath]) { - throw new Error(`No luis status for lu file ${x.relativePath}`); - } - return { - ...x, - status: luStatus[x.relativePath], - }; - }); - }; - public getSchemas = () => { let editorSchema = this.defaultEditorSchema; let sdkSchema = this.defaultSDKSchema; @@ -305,9 +285,8 @@ export class BotProject { } await this._updateFile(luFile.relativePath, content); - await this.luPublisher.onFileChange(luFile.relativePath, FileUpdateType.UPDATE); - return this.mergeLuStatus(this.luFiles, this.luPublisher.status); + return this.luFiles; }; public createLuFile = async (id: string, content: string, dir: string = this.defaultDir(id)): Promise<LuFile[]> => { @@ -319,8 +298,7 @@ export class BotProject { // TODO: validate before save await this._createFile(relativePath, content); - await this.luPublisher.onFileChange(relativePath, FileUpdateType.CREATE); // let publisher know that some files changed - return this.mergeLuStatus(this.luFiles, this.luPublisher.status); // return a merged LUFile always + return this.luFiles; // return a merged LUFile always }; public removeLuFile = async (id: string): Promise<LuFile[]> => { @@ -331,41 +309,31 @@ export class BotProject { await this._removeFile(luFile.relativePath); - await this.luPublisher.onFileChange(luFile.relativePath, FileUpdateType.DELETE); await this._cleanUp(luFile.relativePath); - return this.mergeLuStatus(this.luFiles, this.luPublisher.status); + return this.luFiles; }; public publishLuis = async (authoringKey: string) => { this.luPublisher.setAuthoringKey(authoringKey); const referred = this.luFiles.filter(this.isReferred); - const unpublished = this.luPublisher.getUnpublisedFiles(referred); - const invalidLuFile = unpublished.filter(file => file.diagnostics.length !== 0); + const invalidLuFile = referred.filter(file => file.diagnostics.length !== 0); if (invalidLuFile.length !== 0) { const msg = this.generateErrorMessage(invalidLuFile); throw new Error(`The Following LuFile(s) are invalid: \n` + msg); } - const emptyLuFiles = unpublished.filter(this.isLuFileEmpty); + const emptyLuFiles = referred.filter(this.isLuFileEmpty); if (emptyLuFiles.length !== 0) { const msg = emptyLuFiles.map(file => file.id).join(' '); throw new Error(`You have the following empty LuFile(s): ` + msg); } - if (unpublished.length > 0) { - await this.luPublisher.publish(unpublished); + if (referred.length > 0) { + this.luPublisher.createCrossTrainConfig(this.dialogs, referred); + await this.luPublisher.publish(referred); } - return this.mergeLuStatus(this.luFiles, this.luPublisher.status); - }; - - public checkLuisPublished = () => { - const referredLuFiles = this.luFiles.filter(this.isReferred); - if (referredLuFiles.length <= 0) { - return true; - } else { - return this.luPublisher.checkLuisPublised(referredLuFiles); - } + return this.luFiles; }; public cloneFiles = async (locationRef: LocationRef): Promise<LocationRef> => { @@ -522,7 +490,7 @@ export class BotProject { // load only from the data dir, otherwise may get "build" versions from // deployment process const root = this.dataDir; - const paths = await this.fileStorage.glob(pattern, root); + const paths = await this.fileStorage.glob([pattern, '!(generated/**)'], root); for (const filePath of paths.sort()) { const realFilePath: string = Path.join(root, filePath); diff --git a/Composer/packages/server/src/models/bot/interface.ts b/Composer/packages/server/src/models/bot/interface.ts index 92c75875fd..4cfa754c16 100644 --- a/Composer/packages/server/src/models/bot/interface.ts +++ b/Composer/packages/server/src/models/bot/interface.ts @@ -14,11 +14,6 @@ export interface ILuisSettings { }; } -export interface LuisStatus { - lastUpdateTime: number; - lastPublishTime: number; -} - // we will probably also use this interface to consolidate the processing of lu\lg\dialog export enum FileUpdateType { CREATE = 'create', @@ -40,8 +35,6 @@ export interface IOperationLUFile { relativePath?: string; content?: string; intents: []; - lastUpdateTime?: number; - lastPublishTime?: number; [key: string]: any; } diff --git a/Composer/packages/server/src/models/bot/luPublisher.ts b/Composer/packages/server/src/models/bot/luPublisher.ts index 12fcc28a09..67c589a260 100644 --- a/Composer/packages/server/src/models/bot/luPublisher.ts +++ b/Composer/packages/server/src/models/bot/luPublisher.ts @@ -2,30 +2,43 @@ // Licensed under the MIT License. import isEqual from 'lodash/isEqual'; -import { luBuild } from '@bfcomposer/bf-lu/lib/parser/lubuild'; -import { LuFile } from '@bfc/indexers'; +import keys from 'lodash/keys'; +import { LuFile, DialogInfo } from '@bfc/indexers'; import { Path } from './../../utility/path'; import { IFileStorage } from './../storage/interface'; -import { ILuisConfig, LuisStatus, FileUpdateType } from './interface'; +import { ILuisConfig } from './interface'; import log from './../../logger'; -const GENERATEDFOLDER = 'ComposerDialogs/generated'; -const LU_STATUS_FILE = 'luis.status.json'; -const DEFAULT_STATUS = { - lastUpdateTime: 1, - lastPublishTime: 0, // means unpublished -}; +// eslint-disable-next-line @typescript-eslint/no-var-requires +const crossTrainer = require('@bfcomposer/bf-lu/lib/parser/cross-train/crossTrainer.js'); +// eslint-disable-next-line @typescript-eslint/no-var-requires +const luBuild = require('@bfcomposer/bf-lu/lib/parser/lubuild/builder.js'); + +const DIALOGS_FOLDER = 'ComposerDialogs'; +const GENERATEDFOLDER = 'generated'; +const INTERUPTION = 'interuption'; + +interface ICrossTrainConfig { + rootIds: string[]; + triggerRules: { [key: string]: any }; + intentName: string; + verbose: boolean; +} + export class LuPublisher { public botDir: string; + public dialogsDir: string; public generatedFolderPath: string; - public statusFile: string; + public interuptionFolderPath: string; public storage: IFileStorage; public config: ILuisConfig | null = null; - - // key: filePath relative to bot dir - // value: lastUpdateTime && lastPublishTime - public status: { [key: string]: LuisStatus } = {}; + public crossTrainMapRule: ICrossTrainConfig = { + rootIds: [], + triggerRules: {}, + intentName: '_Interruption', + verbose: true, + }; private builder = new luBuild.Builder(message => { log(message); @@ -33,159 +46,221 @@ export class LuPublisher { constructor(path: string, storage: IFileStorage) { this.botDir = path; - this.generatedFolderPath = Path.join(this.botDir, GENERATEDFOLDER); - this.statusFile = Path.join(this.generatedFolderPath, LU_STATUS_FILE); + this.dialogsDir = Path.join(this.botDir, DIALOGS_FOLDER); + this.generatedFolderPath = Path.join(this.dialogsDir, GENERATEDFOLDER); + this.interuptionFolderPath = Path.join(this.generatedFolderPath, INTERUPTION); this.storage = storage; } - // load luis status from luis.status.json - public loadStatus = async (files: string[] = []) => { - if (await this.storage.exists(this.statusFile)) { - const content = await this.storage.readFile(this.statusFile); - this.status = JSON.parse(content); - } + public publish = async (luFiles: LuFile[]) => { + try { + await this._createGeneratedDir(); - // make sure all LU file have an initial value - files.forEach(f => { - if (!this.status[f]) { - this.status[f] = { ...DEFAULT_STATUS }; // use ... ensure don't referred to the same object - } - }); - return this.status; - }; + //do cross train before publish + await this._crossTrain(luFiles); + + await this._runBuild(luFiles); - // reset status when config changed, because status don't represent the current config - public resetStatus = () => { - for (const key in this.status) { - this.status[key] = { ...DEFAULT_STATUS }; + //remove the cross train result + await this._cleanCrossTrain(); + } catch (error) { + throw new Error(error.message ?? error.text ?? 'Error publishing to LUIS.'); } }; - public saveStatus = async () => { - if (!(await this.storage.exists(this.generatedFolderPath))) { - await this.storage.mkDir(this.generatedFolderPath); + public getLuisConfig = () => this.config; + + public setLuisConfig = async (config: ILuisConfig) => { + if (!isEqual(config, this.config)) { + this.config = config; + await this._deleteDir(this.generatedFolderPath); } - await this.storage.writeFile(this.statusFile, JSON.stringify(this.status, null, 2)); }; - public onFileChange = async (relativePath: string, type: FileUpdateType) => { - switch (type) { - case FileUpdateType.CREATE: - this.status[relativePath] = { - lastUpdateTime: Date.now(), - lastPublishTime: 0, // unpublished - }; - break; - case FileUpdateType.UPDATE: - this.status[relativePath].lastUpdateTime = Date.now(); - break; - case FileUpdateType.DELETE: - delete this.status[relativePath]; - break; + public setAuthoringKey = (key: string) => { + if (this.config) { + this.config.authoringKey = key; } - await this.saveStatus(); }; - public publish = async (luFiles: LuFile[]) => { - if (!luFiles.length) { - throw new Error('No luis file exist'); - } - const config = this._getConfig(); - const curTime = Date.now(); - try { - const loadResult = await this._loadLuConatents(luFiles); - const buildResult = await this.builder.build( - loadResult.luContents, - loadResult.recognizers, - config.authoringKey, - config.region, - config.botName, - config.suffix, - config.fallbackLocal, - false, - loadResult.multiRecognizers, - loadResult.settings - ); - - // update pubish status after sucessfully published - luFiles.forEach(f => { - this.status[f.relativePath].lastPublishTime = curTime; - }); - await this.saveStatus(); - await this.builder.writeDialogAssets(buildResult, true, this.generatedFolderPath); - } catch (error) { - throw new Error(error.body?.error?.message || error.message || 'Error publishing to LUIS.'); + //generate the cross-train config + /* the config is like + { + rootIds: [ + 'main.lu', + 'main.fr-fr.lu' + ], + triggerRules: { + 'main.lu': { + 'dia1.lu': 'dia1_trigger', + 'dia2.lu': 'dia2_trigger' + }, + 'dia2.lu': { + 'dia3.lu': 'dia3_trigger', + 'dia4.lu': 'dia4_trigger' + }, + 'main.fr-fr.lu': { + 'dia1.fr-fr.lu': 'dia1_trigger' + } + }, + intentName: '_Interruption', + verbose: true } - }; + */ + public createCrossTrainConfig = (dialogs: DialogInfo[], luFiles: LuFile[]) => { + const triggerRules = {}; + const countMap = {}; - public getUnpublisedFiles = (files: LuFile[]) => { - // unpublished means either - // 1. there is no status tracking - // 2. the status shows that lastPublishTime < lastUpdateTime - return files.filter(f => { - return ( - !this.status[f.relativePath] || - this.status[f.relativePath].lastPublishTime <= this.status[f.relativePath].lastUpdateTime - ); + //map all referred lu files + luFiles.forEach(file => { + countMap[file.id] = 0; }); - }; - public checkLuisPublised = (files: LuFile[]) => { - const unpublished = this.getUnpublisedFiles(files); - return unpublished.length === 0; + dialogs.forEach(dialog => { + const { intentTriggers } = dialog; + const fileId = this._createConfigId(dialog.id); + if (intentTriggers.length) { + //find the trigger's dialog that use a recognizer + intentTriggers.forEach(item => { + const used = item.dialogs.filter(dialog => { + if (typeof countMap[dialog] === 'number') { + countMap[dialog]++; + return true; + } + return false; + }); + if (used.length) { + const result = used.reduce((result, temp) => { + const id = this._createConfigId(temp); + result[id] = item.intent; + return result; + }, {}); + triggerRules[fileId] = { ...triggerRules[fileId], ...result }; + } + }); + } + }); + + this.crossTrainMapRule.rootIds = keys(countMap) + .filter(key => (countMap[key] === 0 || key === 'Main') && triggerRules[this._createConfigId(key)]) + .map(item => this._createConfigId(item)); + this.crossTrainMapRule.triggerRules = triggerRules; }; - public getLuisConfig = () => this.config; + private _createConfigId(fileId) { + return `${fileId}.lu`; + } - public setLuisConfig = async (config: ILuisConfig) => { - if (!isEqual(config, this.config)) { - this.config = config; - await this._deleteGenerated(this.generatedFolderPath); - this.resetStatus(); + private async _createGeneratedDir() { + if (!(await this.storage.exists(this.generatedFolderPath))) { + await this.storage.mkDir(this.generatedFolderPath); } - }; + } - public setAuthoringKey = (key: string) => { - if (this.config) { - this.config.authoringKey = key; + private _needCrossTrain() { + return !!this.crossTrainMapRule.rootIds.length; + } + + private async _crossTrain(luFiles: LuFile[]) { + if (!this._needCrossTrain()) return; + const luContents = luFiles.map(file => { + return { content: file.content, id: this._createConfigId(file.id) }; + }); + + const result = await crossTrainer.crossTrain(luContents, [], this.crossTrainMapRule); + + await this._writeFiles(result.luResult); + } + + private async _writeFiles(crossTrainResult) { + if (!(await this.storage.exists(this.interuptionFolderPath))) { + await this.storage.mkDir(this.interuptionFolderPath); } - }; + for (const key of crossTrainResult.keys()) { + const fileName = Path.basename(key); + const newFileId = Path.join(this.interuptionFolderPath, fileName); + await this.storage.writeFile(newFileId, crossTrainResult.get(key).Content); + } + } + + private async _runBuild(luFiles: LuFile[]) { + const config = await this._getConfig(luFiles); + if (config.models.length === 0) { + throw new Error('No luis file exist'); + } + const loadResult = await this._loadLuConatents(config.models); + const buildResult = await this.builder.build( + loadResult.luContents, + loadResult.recognizers, + config.authoringKey, + config.region, + config.botName, + config.suffix, + config.fallbackLocal, + false, + loadResult.multiRecognizers, + loadResult.settings + ); + await this.builder.writeDialogAssets(buildResult, true, this.generatedFolderPath); + } + //delete files in generated folder - private async _deleteGenerated(path: string) { + private async _deleteDir(path: string) { if (await this.storage.exists(path)) { const files = await this.storage.readDir(path); for (const file of files) { const curPath = Path.join(path, file); if ((await this.storage.stat(curPath)).isDir) { - await this._deleteGenerated(curPath); + await this._deleteDir(curPath); } else { await this.storage.removeFile(curPath); } } + await this.storage.rmDir(path); } } - private _getConfig = () => { - const luConfig = { - authoringKey: this.config?.authoringKey || '', - region: this.config?.authoringRegion || '', - botName: this.config?.name || '', - suffix: this.config?.environment || '', - fallbackLocal: this.config?.defaultLanguage || 'en-us', + private _getConfig = async (luFiles: LuFile[]) => { + if (!this.config) { + throw new Error('Please complete your Luis settings'); + } + + const luConfig: any = { + authoringKey: this.config.authoringKey || '', + region: this.config.authoringRegion || '', + botName: this.config.name || '', + suffix: this.config.environment || '', + fallbackLocal: this.config.defaultLanguage || 'en-us', }; - return luConfig; - }; - private _loadLuConatents = async (luFiles: LuFile[]) => { - const pathList = luFiles.map(file => { - return Path.resolve(this.botDir, file.relativePath); + luConfig.models = []; + //add all lu file after cross train + let paths: string[] = []; + if (this._needCrossTrain()) { + paths = await this.storage.glob('**/*.lu', this.interuptionFolderPath); + luConfig.models = paths.map(filePath => Path.join(this.interuptionFolderPath, filePath)); + } + + //add the lu file that are not in interuption folder. + luFiles.forEach(file => { + if (!~paths.indexOf(`${file.id}.lu`)) { + luConfig.models.push(Path.resolve(this.botDir, file.relativePath)); + } }); + return luConfig; + }; + private _loadLuConatents = async (paths: string[]) => { return await this.builder.loadContents( - pathList, + paths, this.config?.defaultLanguage || '', this.config?.environment || '', this.config?.authoringRegion || '' ); }; + + private async _cleanCrossTrain() { + if (!this._needCrossTrain()) return; + await this._deleteDir(this.interuptionFolderPath); + } } diff --git a/Composer/packages/server/src/models/storage/interface.ts b/Composer/packages/server/src/models/storage/interface.ts index bfefa3b4cc..6c5077b589 100644 --- a/Composer/packages/server/src/models/storage/interface.ts +++ b/Composer/packages/server/src/models/storage/interface.ts @@ -28,7 +28,7 @@ export interface IFileStorage { removeFile(path: string): Promise<void>; mkDir(path: string, options?: MakeDirectoryOptions): Promise<void>; rmDir(path: string): Promise<void>; - glob(pattern: string, path: string): Promise<string[]>; + glob(pattern: string | string[], path: string): Promise<string[]>; copyFile(src: string, dest: string): Promise<void>; rename(oldPath: string, newPath: string): Promise<void>; } diff --git a/Composer/packages/server/src/models/storage/localDiskStorage.ts b/Composer/packages/server/src/models/storage/localDiskStorage.ts index 72bc116d0e..9213486128 100644 --- a/Composer/packages/server/src/models/storage/localDiskStorage.ts +++ b/Composer/packages/server/src/models/storage/localDiskStorage.ts @@ -63,7 +63,7 @@ export class LocalDiskStorage implements IFileStorage { await rmDir(path); } - async glob(pattern: string, path: string): Promise<string[]> { + async glob(pattern: string | string[], path: string): Promise<string[]> { return await glob(pattern, { cwd: path, dot: true }); } diff --git a/Composer/packages/server/src/types/bf-lu.d.ts b/Composer/packages/server/src/types/bf-lu.d.ts deleted file mode 100644 index 1eff54c908..0000000000 --- a/Composer/packages/server/src/types/bf-lu.d.ts +++ /dev/null @@ -1,35 +0,0 @@ -/// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -declare module '@bfcomposer/bf-lu/lib/parser/lubuild' { - namespace luBuild { - class Builder { - private readonly handler: (input: string) => any; - - constructor(handler: any); - - loadContents( - files: string[], - culture: string, - suffix: string, - region: string, - stdin?: string - ): { luContents: any[]; recognizers: Map<string, any>; multiRecognizers: Map<string, any>; settings: any }; - - build( - luContents: any[], - recognizers: Map<string, any>, - authoringKey: string, - region: string, - botName: string, - suffix: string, - fallbackLocale: string, - deleteOldVersion: boolean, - multiRecognizers: Map<string, any>, - settings: any - ): any[]; - - writeDialogAssets(contents: any[], force: boolean, out: string): void; - } - } -} diff --git a/Composer/packages/tools/language-servers/language-understanding/package.json b/Composer/packages/tools/language-servers/language-understanding/package.json index 02bc7a28fa..f74756c548 100644 --- a/Composer/packages/tools/language-servers/language-understanding/package.json +++ b/Composer/packages/tools/language-servers/language-understanding/package.json @@ -18,8 +18,8 @@ "start:server": "cd demo && ts-node ./src/server.ts" }, "dependencies": { - "@microsoft/bf-cli-command": "https://botbuilder.myget.org/F/botbuilder-declarative/npm/@microsoft/bf-cli-command/-/@microsoft/bf-cli-command-1.0.0.tgz", - "@bfcomposer/bf-lu": "^1.2.5", + "@microsoft/bf-cli-command": "https://botbuilder.myget.org/F/botbuilder-declarative/npm/@microsoft/bf-cli-command/-/@microsoft/bf-cli-command-1.0.1.tgz", + "@bfcomposer/bf-lu": "^1.2.9", "@types/node": "^12.0.4", "express": "^4.15.2", "monaco-editor-core": "^0.17.0", diff --git a/Composer/yarn.lock b/Composer/yarn.lock index 3f746b86fe..262a651ef3 100644 --- a/Composer/yarn.lock +++ b/Composer/yarn.lock @@ -2120,17 +2120,13 @@ lodash "^4.17.13" to-fast-properties "^2.0.0" -"@bfcomposer/bf-lu@^1.2.5": - version "1.2.5" - resolved "https://botbuilder.myget.org/F/botbuilder-declarative/npm/@bfcomposer/bf-lu/-/@bfcomposer/bf-lu-1.2.5.tgz#4d3707746518d013e19aa6f26467c94342874459" - integrity sha1-TTcHdGUY0BPhmqbyZGfJQ0KHRFk= +"@bfcomposer/bf-lu@^1.2.9": + version "1.2.9" + resolved "https://botbuilder.myget.org/F/botbuilder-declarative/npm/@bfcomposer/bf-lu/-/@bfcomposer/bf-lu-1.2.9.tgz#b2fec0d8f1c3d4d0e526a5bdbf9e5c9b3ab5b71d" + integrity sha1-sv7A2PHD1NDlJqW9v55cmzq1tx0= dependencies: "@azure/cognitiveservices-luis-authoring" "3.0.1" "@azure/ms-rest-azure-js" "2.0.1" - "@microsoft/bf-cli-command" "1.0.0" - "@oclif/command" "~1.5.19" - "@oclif/config" "~1.13.3" - "@oclif/errors" "~1.2.2" antlr4 "^4.7.2" chalk "2.4.1" console-stream "^0.1.1" @@ -2138,6 +2134,7 @@ delay "^4.3.0" fs-extra "^8.1.0" get-stdin "^6.0.0" + globby "^10.0.1" intercept-stdout "^0.1.2" lodash "^4.17.15" node-fetch "^2.1.2" @@ -2724,19 +2721,19 @@ "@types/istanbul-reports" "^1.1.1" "@types/yargs" "^13.0.0" -"@microsoft/bf-cli-command@1.0.0", "@microsoft/bf-cli-command@https://botbuilder.myget.org/F/botbuilder-declarative/npm/@microsoft/bf-cli-command/-/@microsoft/bf-cli-command-1.0.0.tgz": - version "1.0.0" - resolved "https://botbuilder.myget.org/F/botbuilder-declarative/npm/@microsoft/bf-cli-command/-/@microsoft/bf-cli-command-1.0.0.tgz#0d681e93690c4e872bf1181a3ff02f094830929e" - integrity sha1-DWgek2kMTocr8RgaP/AvCUgwkp4= +"@microsoft/bf-cli-command@https://botbuilder.myget.org/F/botbuilder-declarative/npm/@microsoft/bf-cli-command/-/@microsoft/bf-cli-command-1.0.1.tgz": + version "1.0.1" + resolved "https://botbuilder.myget.org/F/botbuilder-declarative/npm/@microsoft/bf-cli-command/-/@microsoft/bf-cli-command-1.0.1.tgz#5c7f650015f839d80a7739bc70f8cd5fa90a1e48" dependencies: - "@oclif/command" "~1.5.13" - "@oclif/config" "~1.12.12" + "@oclif/command" "~1.5.19" + "@oclif/config" "~1.13.3" "@oclif/errors" "~1.2.2" applicationinsights "^1.0.8" chalk "2.4.1" cli-ux "~4.9.3" debug "^4.1.1" fs-extra "^7.0.1" + tslib "~1.10.0" "@microsoft/load-themed-styles@^1.7.13": version "1.9.5" @@ -2756,15 +2753,36 @@ call-me-maybe "^1.0.1" glob-to-regexp "^0.3.0" +"@nodelib/fs.scandir@2.1.3": + version "2.1.3" + resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz#3a582bdb53804c6ba6d146579c46e52130cf4a3b" + integrity sha512-eGmwYQn3gxo4r7jdQnkrrN6bY478C3P+a/y72IJukF8LjB6ZHeB3c+Ehacj3sYeSmUXGlnA67/PmbM9CVwL7Dw== + dependencies: + "@nodelib/fs.stat" "2.0.3" + run-parallel "^1.1.9" + +"@nodelib/fs.stat@2.0.3", "@nodelib/fs.stat@^2.0.2": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.3.tgz#34dc5f4cabbc720f4e60f75a747e7ecd6c175bd3" + integrity sha512-bQBFruR2TAwoevBEd/NWMoAAtNGzTRgdrqnYCc7dhzfoNvqPzLyqlEQnzZ3kVnNrSp25iyxE00/3h2fqGAGArA== + "@nodelib/fs.stat@^1.1.2": version "1.1.3" resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz#2b5a3ab3f918cca48a8c754c08168e3f03eba61b" integrity sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw== -"@oclif/command@^1.5.13", "@oclif/command@~1.5.13", "@oclif/command@~1.5.19": +"@nodelib/fs.walk@^1.2.3": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.4.tgz#011b9202a70a6366e436ca5c065844528ab04976" + integrity sha512-1V9XOY4rDW0rehzbrcqAmHnz8e7SKvX27gh8Gt2WgB0+pdzdiLV83p72kZPU+jvMbS1qU5mauP2iOvO8rhmurQ== + dependencies: + "@nodelib/fs.scandir" "2.1.3" + fastq "^1.6.0" + +"@oclif/command@^1.5.13", "@oclif/command@~1.5.19": version "1.5.19" - resolved "https://botbuilder.myget.org/F/botbuilder-declarative/npm/@oclif/command/-/@oclif/command-1.5.19.tgz#13f472450eb83bd6c6871a164c03eadb5e1a07ed" - integrity sha1-E/RyRQ64O9bGhxoWTAPq214aB+0= + resolved "https://registry.yarnpkg.com/@oclif/command/-/command-1.5.19.tgz#13f472450eb83bd6c6871a164c03eadb5e1a07ed" + integrity sha512-6+iaCMh/JXJaB2QWikqvGE9//wLEVYYwZd5sud8aLoLKog1Q75naZh2vlGVtg5Mq/NqpqGQvdIjJb3Bm+64AUQ== dependencies: "@oclif/config" "^1" "@oclif/errors" "^1.2.2" @@ -2782,14 +2800,6 @@ debug "^4.1.1" tslib "^1.9.3" -"@oclif/config@~1.12.12": - version "1.12.12" - resolved "https://botbuilder.myget.org/F/botbuilder-declarative/npm/@oclif/config/-/@oclif/config-1.12.12.tgz#09e6628fe454214bbd7630ae18d2bee0db6bd533" - integrity sha1-CeZij+RUIUu9djCuGNK+4Ntr1TM= - dependencies: - debug "^4.1.1" - tslib "^1.9.3" - "@oclif/errors@^1.2.2", "@oclif/errors@~1.2.2": version "1.2.2" resolved "https://botbuilder.myget.org/F/botbuilder-declarative/npm/@oclif/errors/-/@oclif/errors-1.2.2.tgz#9d8f269b15f13d70aa93316fed7bebc24688edc2" @@ -4272,6 +4282,11 @@ array-union@^1.0.1, array-union@^1.0.2: dependencies: array-uniq "^1.0.1" +array-union@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" + integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== + array-uniq@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" @@ -7395,6 +7410,13 @@ dir-glob@^2.2.1: dependencies: path-type "^3.0.0" +dir-glob@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" + integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== + dependencies: + path-type "^4.0.0" + dns-equal@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/dns-equal/-/dns-equal-1.0.0.tgz#b39e7f1da6eb0a75ba9c17324b34753c47e0654d" @@ -8335,6 +8357,18 @@ fast-glob@^2.0.2, fast-glob@^2.2.6: merge2 "^1.2.3" micromatch "^3.1.10" +fast-glob@^3.0.3: + version "3.2.2" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.2.tgz#ade1a9d91148965d4bf7c51f72e1ca662d32e63d" + integrity sha512-UDV82o4uQyljznxwMxyVRJgZZt3O5wENYojjzbaGEGZgeOxkLFf+V4cnUD+krzb2F72E18RhamkMZ7AdeggF7A== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.0" + merge2 "^1.3.0" + micromatch "^4.0.2" + picomatch "^2.2.1" + fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" @@ -8345,6 +8379,13 @@ fast-levenshtein@~2.0.4, fast-levenshtein@~2.0.6: resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= +fastq@^1.6.0: + version "1.6.1" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.6.1.tgz#4570c74f2ded173e71cf0beb08ac70bb85826791" + integrity sha512-mpIH5sKYueh3YyeJwqtVo8sORi0CgtmkVbK6kZStpQlZBYQuTzG2CZ7idSiJuA7bY0SFCWUc5WIs+oYumGCQNw== + dependencies: + reusify "^1.0.4" + faye-websocket@^0.10.0: version "0.10.0" resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.10.0.tgz#4e492f8d04dfb6f89003507f6edbf2d501e7c6f4" @@ -8944,7 +8985,7 @@ glob-parent@^3.1.0: is-glob "^3.1.0" path-dirname "^1.0.0" -glob-parent@^5.0.0: +glob-parent@^5.0.0, glob-parent@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.0.tgz#5f4c1d1e748d30cd73ad2944b3577a81b081e8c2" integrity sha512-qjtRgnIVmOfnKUE3NJAQEdk+lKrxfw8t5ke7SXtfMTHcjsBfOfWXCQfdb30zfDoZQ2IRSIiidmjtbHZPZ++Ihw== @@ -9091,6 +9132,20 @@ globby@8.0.2: pify "^3.0.0" slash "^1.0.0" +globby@^10.0.1: + version "10.0.2" + resolved "https://registry.yarnpkg.com/globby/-/globby-10.0.2.tgz#277593e745acaa4646c3ab411289ec47a0392543" + integrity sha512-7dUi7RvCoT/xast/o/dLN53oqND4yk0nsHkhRgn9w65C4PofCLOoJ39iSOg+qVDdWQPIEj+eszMHQ+aLVwwQSg== + dependencies: + "@types/glob" "^7.1.1" + array-union "^2.1.0" + dir-glob "^3.0.1" + fast-glob "^3.0.3" + glob "^7.1.3" + ignore "^5.1.1" + merge2 "^1.2.3" + slash "^3.0.0" + globby@^6.1.0: version "6.1.0" resolved "https://registry.yarnpkg.com/globby/-/globby-6.1.0.tgz#f5a6d70e8395e21c858fb0489d64df02424d506c" @@ -9647,6 +9702,11 @@ ignore@^4.0.3, ignore@^4.0.6: resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== +ignore@^5.1.1: + version "5.1.4" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.4.tgz#84b7b3dbe64552b6ef0eca99f6743dbec6d97adf" + integrity sha512-MzbUSahkTW1u7JpKKjY7LCARd1fU5W2rLdxlM4kdkayuCwZImjkpluF9CM1aLewYJguPDqewLam18Y6AU69A8A== + immer@1.10.0: version "1.10.0" resolved "https://registry.yarnpkg.com/immer/-/immer-1.10.0.tgz#bad67605ba9c810275d91e1c2a47d4582e98286d" @@ -11817,6 +11877,11 @@ merge2@^1.2.3: resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.2.3.tgz#7ee99dbd69bb6481689253f018488a1b902b0ed5" integrity sha512-gdUU1Fwj5ep4kplwcmftruWofEFt6lfpkkr3h860CXbAB9c3hGb55EOL2ali0Td5oebvW0E1+3Sr+Ur7XfKpRA== +merge2@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.3.0.tgz#5b366ee83b2f1582c48f87e47cf1a9352103ca81" + integrity sha512-2j4DAdlBOkiSZIsaXk4mTE3sRS02yBHAtfy127xRV3bQUFqXkjHCHLW6Scv7DwNRbIWNHH8zpnz9zMaKXIdvYw== + methods@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" @@ -11846,7 +11911,7 @@ micromatch@^3.0.4, micromatch@^3.1.10, micromatch@^3.1.4, micromatch@^3.1.8: snapdragon "^0.8.1" to-regex "^3.0.2" -micromatch@^4.0.0: +micromatch@^4.0.0, micromatch@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.2.tgz#4fcb0999bf9fbc2fcbdd212f6d629b9a56c39259" integrity sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q== @@ -13178,6 +13243,11 @@ path-type@^3.0.0: dependencies: pify "^3.0.0" +path-type@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" + integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== + pbkdf2@^3.0.3: version "3.0.17" resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.0.17.tgz#976c206530617b14ebb32114239f7b09336e93a6" @@ -13209,6 +13279,11 @@ picomatch@^2.0.5: resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.0.7.tgz#514169d8c7cd0bdbeecc8a2609e34a7163de69f6" integrity sha512-oLHIdio3tZ0qH76NybpeneBhYVj0QFTfXEFTc/B3zKQspYfYYkWYgFsmzo+4kvId/bQRcNkVeguI3y+CD22BtA== +picomatch@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.1.tgz#21bac888b6ed8601f831ce7816e335bc779f0a4a" + integrity sha512-ISBaA8xQNmwELC7eOjqFKMESB2VIqt4PPDD0nsS95b/9dZXvVKOlz9keMSnoGGKcOHXfTvDD6WMaRoSc9UuhRA== + pify@^2.0.0, pify@^2.2.0: version "2.3.0" resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" @@ -14984,6 +15059,11 @@ retry@^0.12.0: resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" integrity sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs= +reusify@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" + integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== + rgb-regex@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/rgb-regex/-/rgb-regex-1.0.1.tgz#c0e0d6882df0e23be254a475e8edd41915feaeb1" @@ -15033,6 +15113,11 @@ run-node@^1.0.0: resolved "https://registry.yarnpkg.com/run-node/-/run-node-1.0.0.tgz#46b50b946a2aa2d4947ae1d886e9856fd9cabe5e" integrity sha512-kc120TBlQ3mih1LSzdAJXo4xn/GWS2ec0l3S+syHDXP9uRr0JAT8Qd3mdMuyjqCzeZktgP3try92cEgf9Nks8A== +run-parallel@^1.1.9: + version "1.1.9" + resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.1.9.tgz#c9dd3a7cf9f4b2c4b6244e173a6ed866e61dd679" + integrity sha512-DEqnSRTDw/Tc3FXf49zedI638Z9onwUotBMiUFKmrO2sdFKIbXamXGQ3Axd4qgphxKB4kw/qP1w5kTxnfU1B9Q== + run-queue@^1.0.0, run-queue@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/run-queue/-/run-queue-1.0.3.tgz#e848396f057d223f24386924618e25694161ec47" @@ -15397,6 +15482,11 @@ slash@^2.0.0: resolved "https://registry.yarnpkg.com/slash/-/slash-2.0.0.tgz#de552851a1759df3a8f206535442f5ec4ddeab44" integrity sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A== +slash@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" + integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== + slice-ansi@0.0.4: version "0.0.4" resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-0.0.4.tgz#edbf8903f66f7ce2f8eafd6ceed65e264c831b35" @@ -16506,7 +16596,7 @@ tsconfig-paths@^3.4.0: minimist "^1.2.0" strip-bom "^3.0.0" -tslib@^1.10.0, tslib@^1.9.2, tslib@^1.9.3: +tslib@^1.10.0, tslib@^1.9.2, tslib@^1.9.3, tslib@~1.10.0: version "1.10.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a" integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ== From 8a17c929be28fb97c00304d3b315103f8af7a71d Mon Sep 17 00:00:00 2001 From: Chris Whitten <christopher.whitten@microsoft.com> Date: Thu, 5 Mar 2020 15:37:03 -0800 Subject: [PATCH 21/40] a11y: Use header tag for trigger in visual editor (#2128) * Add header tag * use header tag for trigger Co-authored-by: zeye <2295905420@qq.com> Co-authored-by: Chris Whitten <christopher.whitten@microsoft.com> --- .../visual-designer/src/components/nodes/Trigger.tsx | 7 +++++-- .../visual-designer/src/components/nodes/triggerStyles.ts | 5 +++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/Composer/packages/extensions/visual-designer/src/components/nodes/Trigger.tsx b/Composer/packages/extensions/visual-designer/src/components/nodes/Trigger.tsx index 82435f8dbd..2747730c35 100644 --- a/Composer/packages/extensions/visual-designer/src/components/nodes/Trigger.tsx +++ b/Composer/packages/extensions/visual-designer/src/components/nodes/Trigger.tsx @@ -14,6 +14,7 @@ import { titleStyle, subtitleStyle, triggerIconStyle, + titleContentStyle, } from './triggerStyles'; function getLabel(data: any): string { @@ -39,9 +40,11 @@ export const Trigger = ({ data, onClick = () => {} }): JSX.Element => { <div css={triggerContentStyle}> <div css={titleStyle}> <Icon iconName="Flow" style={triggerIconStyle} /> - <span>{name}</span> + <h4 css={titleContentStyle}>{name}</h4> + </div> + <div css={subtitleStyle} className="trigger__content-label"> + {label} </div> - <div css={subtitleStyle}>{label}</div> </div> </div> ); diff --git a/Composer/packages/extensions/visual-designer/src/components/nodes/triggerStyles.ts b/Composer/packages/extensions/visual-designer/src/components/nodes/triggerStyles.ts index 392068703a..57cc3b154f 100644 --- a/Composer/packages/extensions/visual-designer/src/components/nodes/triggerStyles.ts +++ b/Composer/packages/extensions/visual-designer/src/components/nodes/triggerStyles.ts @@ -28,8 +28,13 @@ export const titleStyle: any = { fontSize: '18px', lineHeight: '24px', fontWeight: 600, + display: 'flex', }; +export const titleContentStyle = { + fontWeight: 600, + margin: '0px', +}; export const subtitleStyle: any = { whiteSpace: 'nowrap', color: ObiColors.LightGray, From 1bea213a7b40fc8bf6c1b4ff4a8f1fb127219cd8 Mon Sep 17 00:00:00 2001 From: Chris Whitten <christopher.whitten@microsoft.com> Date: Thu, 5 Mar 2020 15:52:23 -0800 Subject: [PATCH 22/40] add name for nodeMenu, arrow and endNode (#2131) Co-authored-by: Chris Whitten <christopher.whitten@microsoft.com> --- .../visual-designer/src/components/lib/EdgeUtil.tsx | 9 ++++++++- .../visual-designer/src/components/menus/NodeMenu.tsx | 3 ++- .../visual-designer/src/editors/StepEditor.tsx | 1 + 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/Composer/packages/extensions/visual-designer/src/components/lib/EdgeUtil.tsx b/Composer/packages/extensions/visual-designer/src/components/lib/EdgeUtil.tsx index 192990b7a5..d4b24335ab 100644 --- a/Composer/packages/extensions/visual-designer/src/components/lib/EdgeUtil.tsx +++ b/Composer/packages/extensions/visual-designer/src/components/lib/EdgeUtil.tsx @@ -126,7 +126,14 @@ export const drawSVGEdge = ( const [p1, p2] = calculateArrowPoints(endPoint, direction); const points = [p1, endPoint, p2].map(p => `${p.x},${p.y}`).join(' '); const arrow = ( - <polyline key={`edge-${id}__arrow`} points={points} {...strokeProps} fill="none" strokeDasharray="none" /> + <polyline + key={`edge-${id}__arrow`} + name={id} + points={points} + {...strokeProps} + fill="none" + strokeDasharray="none" + /> ); elements.push(arrow); } diff --git a/Composer/packages/extensions/visual-designer/src/components/menus/NodeMenu.tsx b/Composer/packages/extensions/visual-designer/src/components/menus/NodeMenu.tsx index c05d8bd5ac..a562e37ab3 100644 --- a/Composer/packages/extensions/visual-designer/src/components/menus/NodeMenu.tsx +++ b/Composer/packages/extensions/visual-designer/src/components/menus/NodeMenu.tsx @@ -19,7 +19,7 @@ const declareElementAttributes = (id: string) => { [AttrNames.SelectedId]: `${id}${MenuTypes.NodeMenu}`, }; }; -export const NodeMenu = ({ id, onEvent }): JSX.Element => { +export const NodeMenu = ({ id, onEvent }) => { const menuItems = [ { key: 'delete', @@ -35,6 +35,7 @@ export const NodeMenu = ({ id, onEvent }): JSX.Element => { return ( <div + data-name="nodeMenu" css={{ marginRight: '1px', }} diff --git a/Composer/packages/extensions/visual-designer/src/editors/StepEditor.tsx b/Composer/packages/extensions/visual-designer/src/editors/StepEditor.tsx index 0cc9a5a9d1..c3a51f73ba 100644 --- a/Composer/packages/extensions/visual-designer/src/editors/StepEditor.tsx +++ b/Composer/packages/extensions/visual-designer/src/editors/StepEditor.tsx @@ -83,6 +83,7 @@ export const StepEditor = ({ id, data, onEvent, trigger, addCoachMarkRef }): JSX { directed: true } )} <circle + name="editor__end" r={TerminatorSize.height / 2 - 1} cx={editorAxisX} cy={contentBoundary.height + HeadSize.height + ElementInterval.y / 2 + TerminatorSize.height / 2} From c17b59e0940f04cdaa6e9662be0ac8aff0c23b45 Mon Sep 17 00:00:00 2001 From: Chris Whitten <christopher.whitten@microsoft.com> Date: Thu, 5 Mar 2020 16:37:40 -0800 Subject: [PATCH 23/40] fix: remove edit button in lu all up view (#2146) * fix some known issues * fix e2e test * handle comments Co-authored-by: Chris Whitten <christopher.whitten@microsoft.com> --- Composer/cypress/integration/LUPage.spec.ts | 7 ++----- .../pages/language-understanding/index.tsx | 21 ++++++++++--------- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/Composer/cypress/integration/LUPage.spec.ts b/Composer/cypress/integration/LUPage.spec.ts index f989d4dce7..950e35157a 100644 --- a/Composer/cypress/integration/LUPage.spec.ts +++ b/Composer/cypress/integration/LUPage.spec.ts @@ -14,10 +14,7 @@ context('LU Page', () => { cy.contains('ToDoBotWithLuisSample.Main'); cy.contains('All'); - cy.get('.toggleEditMode button').as('switchButton'); - - // all multiple file, edit mode button is disabled. - cy.get('@switchButton').should('be.disabled'); + cy.get('.toggleEditMode button').should('not.exist'); // by default is table view cy.findByTestId('LUEditor') @@ -28,7 +25,7 @@ context('LU Page', () => { cy.findByTestId('LUEditor').within(() => { cy.findByText('__TestToDoBotWithLuisSample.Main').click(); }); - + cy.get('.toggleEditMode button').as('switchButton'); // goto edit-mode cy.get('@switchButton').click(); cy.findByTestId('LUEditor') diff --git a/Composer/packages/client/src/pages/language-understanding/index.tsx b/Composer/packages/client/src/pages/language-understanding/index.tsx index 960a8730a7..95ee7bd2da 100644 --- a/Composer/packages/client/src/pages/language-understanding/index.tsx +++ b/Composer/packages/client/src/pages/language-understanding/index.tsx @@ -91,16 +91,17 @@ const LUPage: React.FC<LUPageProps> = props => { <div css={ContentHeaderStyle}> <div css={HeaderText}>{formatMessage('User Input')}</div> <div css={flexContent}> - <Toggle - className={'toggleEditMode'} - css={actionButton} - onText={formatMessage('Edit mode')} - offText={formatMessage('Edit mode')} - defaultChecked={false} - checked={!!edit} - disabled={isRoot && edit === false} - onChange={onToggleEditMode} - /> + {(!isRoot || edit) && ( + <Toggle + className={'toggleEditMode'} + css={actionButton} + onText={formatMessage('Edit mode')} + offText={formatMessage('Edit mode')} + defaultChecked={false} + checked={!!edit} + onChange={onToggleEditMode} + /> + )} </div> </div> <div css={ContentStyle} data-testid="LUEditor"> From ae6111e0affc089467010560acc390d4c6b9d7e1 Mon Sep 17 00:00:00 2001 From: TJ Durnford <tjdford@gmail.com> Date: Thu, 5 Mar 2020 18:15:29 -0800 Subject: [PATCH 24/40] feat: Remove input LU when deconstructing prompts (#2180) * feat: Remove input LU when deconstructing prompts * Fixed lint issue Co-authored-by: Chris Whitten <christopher.whitten@microsoft.com> --- .../visual-designer/src/editors/ObiEditor.tsx | 12 +++++-- .../extensions/visual-designer/src/index.tsx | 2 ++ .../src/store/NodeRendererContext.ts | 4 ++- .../lib/shared/src/deleteUtils/index.ts | 36 +++++++++++++++++-- .../packages/lib/shared/src/dialogFactory.ts | 16 ++++++--- 5 files changed, 60 insertions(+), 10 deletions(-) diff --git a/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx b/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx index aafe981915..8c1c35a255 100644 --- a/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx +++ b/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx @@ -44,7 +44,7 @@ export const ObiEditor: FC<ObiEditorProps> = ({ }): JSX.Element | null => { let divRef; - const { focusedId, focusedEvent, clipboardActions, copyLgTemplate, removeLgTemplates } = useContext( + const { focusedId, focusedEvent, clipboardActions, copyLgTemplate, removeLgTemplates, removeLuIntent } = useContext( NodeRendererContext ); @@ -59,6 +59,10 @@ export const ObiEditor: FC<ObiEditorProps> = ({ return removeLgTemplates(lgFileId, normalizedLgTemplates); }; + const deleteLuIntents = (luIntents: string[]) => { + return Promise.all(luIntents.map(intent => removeLuIntent(path, intent))); + }; + const dispatchEvent = (eventName: NodeEventTypes, eventData: any): any => { let handler; switch (eventName) { @@ -80,7 +84,7 @@ export const ObiEditor: FC<ObiEditorProps> = ({ break; case NodeEventTypes.Delete: handler = e => { - onChange(deleteNode(data, e.id, node => deleteAction(node, deleteLgTemplates))); + onChange(deleteNode(data, e.id, node => deleteAction(node, deleteLgTemplates, deleteLuIntents))); onFocusSteps([]); }; break; @@ -138,7 +142,9 @@ export const ObiEditor: FC<ObiEditorProps> = ({ break; case NodeEventTypes.DeleteSelection: handler = e => { - const dialog = deleteNodes(data, e.actionIds, nodes => deleteActions(nodes, deleteLgTemplates)); + const dialog = deleteNodes(data, e.actionIds, nodes => + deleteActions(nodes, deleteLgTemplates, deleteLuIntents) + ); onChange(dialog); onFocusSteps([]); }; diff --git a/Composer/packages/extensions/visual-designer/src/index.tsx b/Composer/packages/extensions/visual-designer/src/index.tsx index 9cfb246659..974b59904f 100644 --- a/Composer/packages/extensions/visual-designer/src/index.tsx +++ b/Composer/packages/extensions/visual-designer/src/index.tsx @@ -62,6 +62,7 @@ const VisualDesigner: React.FC<VisualDesignerProps> = ({ copyLgTemplate, removeLgTemplate, removeLgTemplates, + removeLuIntent, undo, redo, } = shellApi; @@ -78,6 +79,7 @@ const VisualDesigner: React.FC<VisualDesignerProps> = ({ copyLgTemplate, removeLgTemplate, removeLgTemplates, + removeLuIntent, }; return ( diff --git a/Composer/packages/extensions/visual-designer/src/store/NodeRendererContext.ts b/Composer/packages/extensions/visual-designer/src/store/NodeRendererContext.ts index 36aa1abd0f..49c9201a3f 100644 --- a/Composer/packages/extensions/visual-designer/src/store/NodeRendererContext.ts +++ b/Composer/packages/extensions/visual-designer/src/store/NodeRendererContext.ts @@ -9,7 +9,8 @@ type ShellApiFuncs = | 'copyLgTemplate' | 'removeLgTemplate' | 'removeLgTemplates' - | 'updateLgTemplate'; + | 'updateLgTemplate' + | 'removeLuIntent'; interface NodeRendererContextValue extends Pick<ShellApi, ShellApiFuncs> { focusedId?: string; @@ -28,4 +29,5 @@ export const NodeRendererContext = React.createContext<NodeRendererContextValue> removeLgTemplate: () => Promise.resolve(), removeLgTemplates: () => Promise.resolve(), updateLgTemplate: () => Promise.resolve(), + removeLuIntent: () => Promise.resolve(), }); diff --git a/Composer/packages/lib/shared/src/deleteUtils/index.ts b/Composer/packages/lib/shared/src/deleteUtils/index.ts index 0c94f765c0..14d10ddfec 100644 --- a/Composer/packages/lib/shared/src/deleteUtils/index.ts +++ b/Composer/packages/lib/shared/src/deleteUtils/index.ts @@ -25,19 +25,51 @@ const collectLgTemplates = (action: any, outputTemplates: string[]) => { } }; -export const deleteAdaptiveAction = (data: MicrosoftIDialog, deleteLgTemplates: (lgTemplates: string[]) => any) => { +const collectLuIntents = (action: any, outputTemplates: string[]) => { + if (typeof action === 'string') return; + if (!action || !action.$type) return; + + switch (action.$type) { + case SDKTypes.AttachmentInput: + case SDKTypes.ChoiceInput: + case SDKTypes.ConfirmInput: + case SDKTypes.DateTimeInput: + case SDKTypes.NumberInput: + case SDKTypes.TextInput: { + const [, promptType] = action.$type.split('.'); + const intentName = `${promptType}.response-${action?.$designer?.id}`; + promptType && intentName && outputTemplates.push(intentName); + break; + } + } +}; + +export const deleteAdaptiveAction = ( + data: MicrosoftIDialog, + deleteLgTemplates: (lgTemplates: string[]) => any, + deleteLuIntents: (luIntents: string[]) => any +) => { const lgTemplates: string[] = []; + const luIntents: string[] = []; + walkAdaptiveAction(data, action => collectLgTemplates(action, lgTemplates)); + walkAdaptiveAction(data, action => collectLuIntents(action, luIntents)); deleteLgTemplates(lgTemplates.filter(activity => !!activity)); + deleteLuIntents(luIntents); }; export const deleteAdaptiveActionList = ( data: MicrosoftIDialog[], - deleteLgTemplates: (lgTemplates: string[]) => any + deleteLgTemplates: (lgTemplates: string[]) => any, + deleteLuIntents: (luIntents: string[]) => any ) => { const lgTemplates: string[] = []; + const luIntents: string[] = []; + walkAdaptiveActionList(data, action => collectLgTemplates(action, lgTemplates)); + walkAdaptiveAction(data, action => collectLuIntents(action, luIntents)); deleteLgTemplates(lgTemplates.filter(activity => !!activity)); + deleteLuIntents(luIntents); }; diff --git a/Composer/packages/lib/shared/src/dialogFactory.ts b/Composer/packages/lib/shared/src/dialogFactory.ts index ef9b76243f..585b5a6aed 100644 --- a/Composer/packages/lib/shared/src/dialogFactory.ts +++ b/Composer/packages/lib/shared/src/dialogFactory.ts @@ -118,12 +118,20 @@ export const deepCopyAction = async ( }); }; -export const deleteAction = (data: MicrosoftIDialog, deleteLgTemplates: (templates: string[]) => any) => { - return deleteAdaptiveAction(data, deleteLgTemplates); +export const deleteAction = ( + data: MicrosoftIDialog, + deleteLgTemplates: (templates: string[]) => any, + deleteLuIntents: (luIntents: string[]) => any +) => { + return deleteAdaptiveAction(data, deleteLgTemplates, deleteLuIntents); }; -export const deleteActions = (inputs: MicrosoftIDialog[], deleteLgTemplates: (templates: string[]) => any) => { - return deleteAdaptiveActionList(inputs, deleteLgTemplates); +export const deleteActions = ( + inputs: MicrosoftIDialog[], + deleteLgTemplates: (templates: string[]) => any, + deleteLuIntents: (luIntents: string[]) => any +) => { + return deleteAdaptiveActionList(inputs, deleteLgTemplates, deleteLuIntents); }; export const seedNewDialog = ( From 964b8d668d0390d5265146ab9f0a375fa32f0f3d Mon Sep 17 00:00:00 2001 From: Chris Whitten <christopher.whitten@microsoft.com> Date: Thu, 5 Mar 2020 18:47:29 -0800 Subject: [PATCH 25/40] Default ActivityProcessed to true (bool) (#2189) --- Composer/packages/lib/shared/src/appschema.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Composer/packages/lib/shared/src/appschema.ts b/Composer/packages/lib/shared/src/appschema.ts index 5330d03cfa..f52fb02d27 100644 --- a/Composer/packages/lib/shared/src/appschema.ts +++ b/Composer/packages/lib/shared/src/appschema.ts @@ -260,12 +260,11 @@ export const appschema: OBISchema = { additionalProperties: true, }, activityProcessed: { - $role: 'expression', - type: 'string', + type: 'boolean', title: 'Activity Processed', description: 'When set to false, the dialog that is called can process the current activity.', - default: 'true', - examples: ['true'], + default: true, + examples: [true], }, resultProperty: { $role: 'expression', From fed25157c197095caf51fb95405dbf5b069d2e5b Mon Sep 17 00:00:00 2001 From: Long Alan <julong@microsoft.com> Date: Fri, 6 Mar 2020 23:13:48 +0800 Subject: [PATCH 26/40] fix: lg template display wrong in visual & form editor (#2191) * activity content truncated in visual editor * show template in visual editor * fix data in toDobotWithLuis sample bot * refactor --- .../src/components/elements/styledComponents.tsx | 1 + .../visual-designer/src/utils/hooks.ts | 5 +++-- .../src/utils/normalizeLgTemplate.ts | 16 ++++++++++++++++ .../ComposerDialogs/Main/Main.dialog | 8 ++++---- 4 files changed, 24 insertions(+), 6 deletions(-) create mode 100644 Composer/packages/extensions/visual-designer/src/utils/normalizeLgTemplate.ts diff --git a/Composer/packages/extensions/visual-designer/src/components/elements/styledComponents.tsx b/Composer/packages/extensions/visual-designer/src/components/elements/styledComponents.tsx index 1bdc8fa122..e8a3e6b08d 100644 --- a/Composer/packages/extensions/visual-designer/src/components/elements/styledComponents.tsx +++ b/Composer/packages/extensions/visual-designer/src/components/elements/styledComponents.tsx @@ -47,6 +47,7 @@ export const SingleLineDiv = styled.div<DivProps>` export const TextDiv = styled.div` ${StandardFontCSS}; ${MultilineCSS}; + white-space: pre-wrap; line-height: 16px; display: inline-block; `; diff --git a/Composer/packages/extensions/visual-designer/src/utils/hooks.ts b/Composer/packages/extensions/visual-designer/src/utils/hooks.ts index acf156af71..0ac62702d2 100644 --- a/Composer/packages/extensions/visual-designer/src/utils/hooks.ts +++ b/Composer/packages/extensions/visual-designer/src/utils/hooks.ts @@ -7,6 +7,8 @@ import { LgTemplateRef } from '@bfc/shared'; import { NodeRendererContext } from '../store/NodeRendererContext'; +import { normalizeLgTemplate } from './normalizeLgTemplate'; + export const useLgTemplate = (str?: string, dialogId?: string) => { const { getLgTemplates } = useContext(NodeRendererContext); const [templateText, setTemplateText] = useState(''); @@ -33,8 +35,7 @@ export const useLgTemplate = (str?: string, dialogId?: string) => { } if (template && template.body) { - const [firstLine] = template.body.split('\n'); - setTemplateText(firstLine.startsWith('-') ? firstLine.substring(1) : firstLine); + setTemplateText(normalizeLgTemplate(template)); } else { setTemplateText(''); } diff --git a/Composer/packages/extensions/visual-designer/src/utils/normalizeLgTemplate.ts b/Composer/packages/extensions/visual-designer/src/utils/normalizeLgTemplate.ts new file mode 100644 index 0000000000..06ff15a07b --- /dev/null +++ b/Composer/packages/extensions/visual-designer/src/utils/normalizeLgTemplate.ts @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { LGTemplate } from 'botbuilder-lg'; + +export function normalizeLgTemplate(template: LGTemplate): string { + const templateTexts = template.body.split('\n').map(line => (line.startsWith('-') ? line.substring(1) : line)); + let showText = ''; + + if (templateTexts[0] && templateTexts[0].trim() === '[Activity') { + showText = templateTexts.find(text => text.includes('Text = '))?.split('Text = ')[1] || ''; + } else { + showText = templateTexts.join('\n'); + } + return showText; +} diff --git a/Composer/packages/server/assets/projects/ToDoBotWithLuisSample/ComposerDialogs/Main/Main.dialog b/Composer/packages/server/assets/projects/ToDoBotWithLuisSample/ComposerDialogs/Main/Main.dialog index d760ed5083..2145b05a40 100644 --- a/Composer/packages/server/assets/projects/ToDoBotWithLuisSample/ComposerDialogs/Main/Main.dialog +++ b/Composer/packages/server/assets/projects/ToDoBotWithLuisSample/ComposerDialogs/Main/Main.dialog @@ -314,7 +314,7 @@ { "$type": "Microsoft.SendActivity", "$designer": { - "id": "684852", + "id": "212971", "name": "Send a response" }, "activity": "${bfdactivity-212971()}" @@ -322,7 +322,7 @@ { "$type": "Microsoft.TextInput", "$designer": { - "id": "038757", + "id": "879447", "name": "Text input" }, "prompt": "${bfdprompt-879447()}", @@ -346,7 +346,7 @@ { "$type": "Microsoft.SendActivity", "$designer": { - "id": "593087", + "id": "056974", "name": "Send a response" }, "activity": "${bfdactivity-056974()}" @@ -356,7 +356,7 @@ { "$type": "Microsoft.SendActivity", "$designer": { - "id": "253146", + "id": "084161", "name": "Send a response" }, "activity": "${bfdactivity-084161()}" From c2d766575262dd1df2354190f0da868f778575ab Mon Sep 17 00:00:00 2001 From: Long Alan <julong@microsoft.com> Date: Fri, 6 Mar 2020 23:44:23 +0800 Subject: [PATCH 27/40] fix: Double scroll bars in dialog's properties pane (#2163) * double scroll * fix side bar horizontal scroll Co-authored-by: zeye <2295905420@qq.com> Co-authored-by: Chris Whitten <christopher.whitten@microsoft.com> --- Composer/packages/client/public/extensionContainer.html | 3 +++ Composer/packages/client/src/components/ProjectTree/styles.ts | 1 + Composer/packages/client/src/pages/design/styles.js | 1 + 3 files changed, 5 insertions(+) diff --git a/Composer/packages/client/public/extensionContainer.html b/Composer/packages/client/public/extensionContainer.html index 1e74464e72..56d4f64b40 100644 --- a/Composer/packages/client/public/extensionContainer.html +++ b/Composer/packages/client/public/extensionContainer.html @@ -47,6 +47,9 @@ width: 100%; height: 100%; } + #root { + overflow: auto; + } </style> <body> <noscript>You need to enable JavaScript to run this app.</noscript> diff --git a/Composer/packages/client/src/components/ProjectTree/styles.ts b/Composer/packages/client/src/components/ProjectTree/styles.ts index 0d53763b9a..a1c4b050f9 100644 --- a/Composer/packages/client/src/components/ProjectTree/styles.ts +++ b/Composer/packages/client/src/components/ProjectTree/styles.ts @@ -28,6 +28,7 @@ export const root = css` border-right: 1px solid #c4c4c4; box-sizing: border-box; overflow-y: auto; + overflow-x: hidden; .ms-List-cell { min-height: 36px; } diff --git a/Composer/packages/client/src/pages/design/styles.js b/Composer/packages/client/src/pages/design/styles.js index a155b225fd..8961319754 100644 --- a/Composer/packages/client/src/pages/design/styles.js +++ b/Composer/packages/client/src/pages/design/styles.js @@ -62,6 +62,7 @@ export const editorWrapper = css` display: flex; flex-direction: row; flex-grow: 1; + overflow: hidden; `; export const visualPanel = css` From 4da36a2aaa4447b7f4c30b6b55b48f0cfdfa93ec Mon Sep 17 00:00:00 2001 From: Lu Han <32191031+luhan2017@users.noreply.github.com> Date: Fri, 6 Mar 2020 23:59:21 +0800 Subject: [PATCH 28/40] Update HttpRequest sample (#2161) Co-authored-by: Chris Whitten <christopher.whitten@microsoft.com> --- .../HttpRequest/HttpRequest.dialog | 6 +++--- .../ComposerDialogs/OAuthInput/OAuthInput.dialog | 2 +- .../ComposerDialogs/OAuthInput/OAuthInput.lg | 15 ++++++++++++++- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/HttpRequest/HttpRequest.dialog b/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/HttpRequest/HttpRequest.dialog index 1870dd309b..cce25d6cc9 100644 --- a/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/HttpRequest/HttpRequest.dialog +++ b/Composer/packages/server/assets/projects/ActionsSample/ComposerDialogs/HttpRequest/HttpRequest.dialog @@ -49,12 +49,12 @@ "method": "POST", "url": "http://petstore.swagger.io/v2/pet", "body": { - "id": "{user.petid}", + "id": "${user.petid}", "category": { "id": 0, "name": "string" }, - "name": "{user.petname}", + "name": "${user.petname}", "photoUrls": [ "string" ], @@ -96,7 +96,7 @@ "id": "705959" }, "method": "GET", - "url": "http://petstore.swagger.io/v2/pet/{user.id}", + "url": "http://petstore.swagger.io/v2/pet/${user.id}", "resultProperty": "user.getResponse" }, { diff --git a/Composer/packages/server/assets/projects/AskingQuestionsSample/ComposerDialogs/OAuthInput/OAuthInput.dialog b/Composer/packages/server/assets/projects/AskingQuestionsSample/ComposerDialogs/OAuthInput/OAuthInput.dialog index fd8d7f04c0..bbcf0003e0 100644 --- a/Composer/packages/server/assets/projects/AskingQuestionsSample/ComposerDialogs/OAuthInput/OAuthInput.dialog +++ b/Composer/packages/server/assets/projects/AskingQuestionsSample/ComposerDialogs/OAuthInput/OAuthInput.dialog @@ -39,7 +39,7 @@ "method": "GET", "url": "https://graph.microsoft.com/beta/me/mailFolders/inbox/messages", "headers": { - "Authorization": "Bearer {user.token.token}" + "Authorization": "Bearer ${user.token.token}" }, "resultProperty": "user.getGraphEmails" }, diff --git a/Composer/packages/server/assets/projects/AskingQuestionsSample/ComposerDialogs/OAuthInput/OAuthInput.lg b/Composer/packages/server/assets/projects/AskingQuestionsSample/ComposerDialogs/OAuthInput/OAuthInput.lg index 550972f230..3b34c92815 100644 --- a/Composer/packages/server/assets/projects/AskingQuestionsSample/ComposerDialogs/OAuthInput/OAuthInput.lg +++ b/Composer/packages/server/assets/projects/AskingQuestionsSample/ComposerDialogs/OAuthInput/OAuthInput.lg @@ -1,4 +1,17 @@ [import](common.lg) # bfdactivity-991558 --${ShowEmailSummary(user)} \ No newline at end of file +-${ShowEmailSummary(user)} + +# ShowEmailSummary(user) +- IF: ${count(user.getGraphEmails.value) == 1} + - You have ${count(user.getGraphEmails.value)} email. This email is ${ShowEmail(user.getGraphEmails.value[0])}. +- ELSEIF: ${count(user.getGraphEmails.value) >= 2} + - You have ${count(user.getGraphEmails.value)} emails, the first email is ${ShowEmail(user.getGraphEmails.value[0])}. +- ELSEIF: ${count(user.getGraphEmails.value) == 0} + - You don't have any email. +- ELSE: + - You should not be here. + +# ShowEmail(email) +- ${email.subject} \ No newline at end of file From a05a5fc87146bfe7d08e4f85ceb6a743a7afde32 Mon Sep 17 00:00:00 2001 From: Qi Kang <kangqidev@gmail.com> Date: Sat, 7 Mar 2020 00:26:53 +0800 Subject: [PATCH 29/40] fix: deployment script, decouple debugging and deployment settings (#2153) * Fix deployment script. Use appsettings.Deployment.json for local debug, use appsettings.Production.json for azure deployment. * update production name to deployment * fix typo * use nightly build package * remove version * update to 3.1 Co-authored-by: Qi Kang <qika@microsoft.com> Co-authored-by: Lu Han <32191031+luhan2017@users.noreply.github.com> Co-authored-by: Chris Whitten <christopher.whitten@microsoft.com> --- BotProject/Templates/CSharp/BotProject.csproj | 2 +- BotProject/Templates/CSharp/Program.cs | 21 +++++++-- .../Templates/CSharp/Scripts/create.ps1 | 22 ++++----- .../Templates/CSharp/Scripts/deploy.ps1 | 46 +++++++------------ .../CSharp/appsettings.Deployment.json | 3 ++ .../CSharp/appsettings.Development.json | 8 +--- .../models/connector/csharpBotConnector.ts | 2 + 7 files changed, 50 insertions(+), 54 deletions(-) create mode 100644 BotProject/Templates/CSharp/appsettings.Deployment.json diff --git a/BotProject/Templates/CSharp/BotProject.csproj b/BotProject/Templates/CSharp/BotProject.csproj index 3cc5c22939..060d2341fe 100644 --- a/BotProject/Templates/CSharp/BotProject.csproj +++ b/BotProject/Templates/CSharp/BotProject.csproj @@ -30,7 +30,7 @@ <PackageReference Include="Microsoft.Bot.Builder.Integration.ApplicationInsights.Core" Version="4.8.0-preview-200227-109209" /> <PackageReference Include="Microsoft.Bot.Builder.Integration.AspNet.Core" Version="4.8.0-preview-200227-109209" /> <PackageReference Include="Microsoft.Bot.Builder.Dialogs" Version="4.8.0-preview-200227-109209" /> - <PackageReference Include="Microsoft.Bot.Connector" Version="4.8.0-preview-95744" /> + <PackageReference Include="Microsoft.Bot.Connector" Version="4.8.0-preview-200227-109209" /> <PackageReference Include="MSTest.TestFramework" Version="1.4.0" /> <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.66"> <PrivateAssets>all</PrivateAssets> diff --git a/BotProject/Templates/CSharp/Program.cs b/BotProject/Templates/CSharp/Program.cs index c5869104f3..434f5a21d2 100644 --- a/BotProject/Templates/CSharp/Program.cs +++ b/BotProject/Templates/CSharp/Program.cs @@ -24,12 +24,25 @@ public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .ConfigureAppConfiguration((hostingContext, builder) => { - builder.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) - .AddJsonFile($"ComposerDialogs/settings/appsettings.json", optional: true, reloadOnChange: true) - .UseLuisConfigAdaptor() - .UseLuisSettings(); var env = hostingContext.HostingEnvironment; + + builder.AddJsonFile($"ComposerDialogs/settings/appsettings.json", optional: true, reloadOnChange: true) + .AddJsonFile("appsetting.json", optional: true, reloadOnChange: true) + .UseLuisConfigAdaptor() + .UseLuisSettings(); + if (env.IsDevelopment()) + { + // Local Debug + builder.AddJsonFile("appsettings.development.json", optional: true, reloadOnChange: true); + } + else + { + //Azure Deploy + builder.AddJsonFile("appsettings.deployment.json", optional: true, reloadOnChange: true); + } + + if (!env.IsDevelopment()) { builder.AddUserSecrets<Startup>(); } diff --git a/BotProject/Templates/CSharp/Scripts/create.ps1 b/BotProject/Templates/CSharp/Scripts/create.ps1 index 20ca8ddc86..d3ac6cac73 100644 --- a/BotProject/Templates/CSharp/Scripts/create.ps1 +++ b/BotProject/Templates/CSharp/Scripts/create.ps1 @@ -24,9 +24,9 @@ else { New-Item -Path $logFile | Out-Null } -if (-not (Test-Path (Join-Path $projDir 'appsettings.json'))) +if (-not (Test-Path (Join-Path $projDir 'appsettings.deployment.json'))) { - Write-Host "! Could not find an 'appsettings.json' file in the current directory." -ForegroundColor DarkRed + Write-Host "! Could not find an 'appsettings.deployment.json' file in the current directory." -ForegroundColor DarkRed Write-Host "+ Please re-run this script from your project directory." -ForegroundColor Magenta Break } @@ -152,24 +152,22 @@ if ($outputs) $outputMap = @{} $outputs.PSObject.Properties | Foreach-Object { $outputMap[$_.Name] = $_.Value } - # Update appsettings.json - Write-Host "> Updating appsettings.json ..." - if (Test-Path $(Join-Path $projDir appsettings.json)) { - $settings = Get-Content $(Join-Path $projDir appsettings.json) | ConvertFrom-Json + # Update appsettings.deployment.json + Write-Host "> Updating appsettings.deployment.json ..." + if (Test-Path $(Join-Path $projDir appsettings.deployment.json)) { + $settings = Get-Content $(Join-Path $projDir appsettings.deployment.json) | ConvertFrom-Json } else { $settings = New-Object PSObject } - $settings | Add-Member -Type NoteProperty -Force -Name 'microsoftAppId' -Value $appId - - dotnet user-secrets init --project $projDir - dotnet user-secrets set "MicrosoftAppPassword" $appPassword --project $projDir + $settings | Add-Member -Type NoteProperty -Force -Name 'MicrosoftAppId' -Value $appId + $settings | Add-Member -Type NoteProperty -Force -Name 'MicrosoftAppPassword' -Value $appPassword $settings | Add-Member -Type NoteProperty -Force -Name 'bot' -Value "ComposerDialogs" foreach ($key in $outputMap.Keys) { $settings | Add-Member -Type NoteProperty -Force -Name $key -Value $outputMap[$key].value } - $settings | ConvertTo-Json -depth 100 | Out-File $(Join-Path $projDir appsettings.json) + $settings | ConvertTo-Json -depth 100 | Out-File $(Join-Path $projDir appsettings.deployment.json) Write-Host "> Done." Write-Host "- App Id: $appId" @@ -210,4 +208,4 @@ else Write-Host "+ To delete this resource group, run 'az group delete -g $($resourceGroup) --no-wait'" -ForegroundColor Magenta Break -} \ No newline at end of file +} diff --git a/BotProject/Templates/CSharp/Scripts/deploy.ps1 b/BotProject/Templates/CSharp/Scripts/deploy.ps1 index 8d6c86ac60..faab0667c4 100644 --- a/BotProject/Templates/CSharp/Scripts/deploy.ps1 +++ b/BotProject/Templates/CSharp/Scripts/deploy.ps1 @@ -1,4 +1,4 @@ -Param( +Param( [string] $name, [string] $environment, [string] $luisAuthoringKey, @@ -56,7 +56,7 @@ if (Test-Path $zipPath) { dotnet user-secrets init # Perform dotnet publish step ahead of zipping up -$publishFolder = $(Join-Path $projFolder 'bin\Release\netcoreapp2.2') +$publishFolder = $(Join-Path $projFolder 'bin\Release\netcoreapp3.1') dotnet publish -c release -o $publishFolder -v q > $logFile @@ -73,26 +73,8 @@ else { Copy-Item -Path $localBotPath -Recurse -Destination $publishFolder -Container -Force } -# Merge from custom config files -$customConfigFiles = Get-ChildItem -Path $remoteBotPath -Include "appsettings.json" -Recurse -Force -if ($customConfigFiles) { - if (Test-Path $(Join-Path $publishFolder appsettings.json)) { - $settings = Get-Content $(Join-Path $publishFolder appsettings.json) | ConvertFrom-Json - } - else { - $settings = New-Object PSObject - } - - $customConfig = @{ } - $customSetting = Get-Content $customConfigFiles.FullName | ConvertFrom-Json - $customSetting.PSObject.Properties | Foreach-Object { $customConfig[$_.Name] = $_.Value } - foreach ($key in $customConfig.Keys) { $settings | Add-Member -Type NoteProperty -Force -Name $key -Value $customConfig[$key] } - - $settings | ConvertTo-Json -depth 100 | Out-File $(Join-Path $publishFolder appsettings.json) -} - # Try to get luis config from appsettings -$settings = Get-Content $(Join-Path $projFolder appsettings.json) | ConvertFrom-Json +$settings = Get-Content $(Join-Path $projFolder appsettings.deployment.json) | ConvertFrom-Json $luisSettings = $settings.luis if (-not $luisAuthoringKey) { @@ -131,13 +113,17 @@ if ($luisAuthoringKey -and $luisAuthoringRegion) { $luconfigjson | ConvertTo-Json -Depth 100 | Out-File $(Join-Path $remoteBotPath luconfig.json) - # Execute lubuild command - if (Get-Command lubuild -errorAction SilentlyContinue) { - lubuild --authoringKey $luisAuthoringKey + # Execute bf luis:build command + if (Get-Command bf -errorAction SilentlyContinue) { + $customizedSettings = Get-Content $(Join-Path $remoteBotPath settings appsettings.json) | ConvertFrom-Json + $customizedEnv = $customizedSettings.luis.environment + bf luis:build --in .\ --botName $name --authoringKey $luisAuthoringKey --dialog --out .\generated --suffix $customizedEnv -f } else { - Write-Host "lubuild does not exist, use the following command to install lubuild:" - Write-Host "npm install -g https://botbuilder.myget.org/F/botbuilder-declarative/npm/lubuild/-/1.0.3-preview.tgz" + Write-Host "bf luis:build does not exist, use the following command to install:" + Write-Host "1. npm config set registry https://botbuilder.myget.org/F/botframework-cli/npm/" + Write-Host "2. npm install -g @microsoft/botframework-cli" + Write-Host "3. npm config set registry http://registry.npmjs.org" Break } @@ -152,8 +138,8 @@ if ($luisAuthoringKey -and $luisAuthoringRegion) { Set-Location -Path $projFolder # change setting file in publish folder - if (Test-Path $(Join-Path $publishFolder appsettings.json)) { - $settings = Get-Content $(Join-Path $publishFolder appsettings.json) | ConvertFrom-Json + if (Test-Path $(Join-Path $publishFolder appsettings.deployment.json)) { + $settings = Get-Content $(Join-Path $publishFolder appsettings.deployment.json) | ConvertFrom-Json } else { $settings = New-Object PSObject @@ -180,7 +166,7 @@ if ($luisAuthoringKey -and $luisAuthoringRegion) { $settings | Add-Member -Type NoteProperty -Force -Name 'luis' -Value $luisConfig - $settings | ConvertTo-Json -depth 100 | Out-File $(Join-Path $publishFolder appsettings.json) + $settings | ConvertTo-Json -depth 100 | Out-File $(Join-Path $publishFolder appsettings.deployment.json) $tokenResponse = (az account get-access-token) | ConvertFrom-Json $token = $tokenResponse.accessToken @@ -251,4 +237,4 @@ if ($?) { else { Write-Host "! Could not deploy automatically to Azure. Review the log for more information." -ForegroundColor DarkRed Write-Host "! Log: $($logFile)" -ForegroundColor DarkRed -} \ No newline at end of file +} diff --git a/BotProject/Templates/CSharp/appsettings.Deployment.json b/BotProject/Templates/CSharp/appsettings.Deployment.json new file mode 100644 index 0000000000..1797133380 --- /dev/null +++ b/BotProject/Templates/CSharp/appsettings.Deployment.json @@ -0,0 +1,3 @@ +{ + +} diff --git a/BotProject/Templates/CSharp/appsettings.Development.json b/BotProject/Templates/CSharp/appsettings.Development.json index e203e9407e..0db3279e44 100644 --- a/BotProject/Templates/CSharp/appsettings.Development.json +++ b/BotProject/Templates/CSharp/appsettings.Development.json @@ -1,9 +1,3 @@ { - "Logging": { - "LogLevel": { - "Default": "Debug", - "System": "Information", - "Microsoft": "Information" - } - } + } diff --git a/Composer/packages/server/src/models/connector/csharpBotConnector.ts b/Composer/packages/server/src/models/connector/csharpBotConnector.ts index f97dadb2a4..3e8b105a05 100644 --- a/Composer/packages/server/src/models/connector/csharpBotConnector.ts +++ b/Composer/packages/server/src/models/connector/csharpBotConnector.ts @@ -152,6 +152,8 @@ export class CSharpBotConnector implements IBotConnector { `bin/Debug/${envSettings.runtimeFrameworkVersion}/BotProject.dll`, `--urls`, this.endpoint, + `--environment`, + `development`, ...this.getConnectorConfig(config), ], { From 3d646592cd4e9d02345d943a6ba2f6e138e26e29 Mon Sep 17 00:00:00 2001 From: Andy Brown <asbrown002@gmail.com> Date: Fri, 6 Mar 2020 14:21:32 -0800 Subject: [PATCH 30/40] fix: load schema files when loading bot project (#2170) * fix: correctly fetch the bot project schema * do not throw error if schemas directory does not exist * fix type imports Co-authored-by: Chris Whitten <christopher.whitten@microsoft.com> --- .../server/src/models/bot/botProject.ts | 54 +++++++++++++++---- 1 file changed, 44 insertions(+), 10 deletions(-) diff --git a/Composer/packages/server/src/models/bot/botProject.ts b/Composer/packages/server/src/models/bot/botProject.ts index 036b7b6d64..bfba48e2c9 100644 --- a/Composer/packages/server/src/models/bot/botProject.ts +++ b/Composer/packages/server/src/models/bot/botProject.ts @@ -485,7 +485,7 @@ export class BotProject { } const fileList: FileInfo[] = []; - const patterns = ['**/*.dialog', '**/*.lg', '**/*.lu', '**/*.schema']; + const patterns = ['**/*.dialog', '**/*.lg', '**/*.lu']; for (const pattern of patterns) { // load only from the data dir, otherwise may get "build" versions from // deployment process @@ -494,22 +494,56 @@ export class BotProject { for (const filePath of paths.sort()) { const realFilePath: string = Path.join(root, filePath); - // skip lg files for now - if ((await this.fileStorage.stat(realFilePath)).isFile) { - const content: string = await this.fileStorage.readFile(realFilePath); - fileList.push({ - name: Path.basename(filePath), - content: content, - path: realFilePath, - relativePath: Path.relative(this.dir, realFilePath), - }); + const fileInfo = await this._getFileInfo(realFilePath); + if (fileInfo) { + fileList.push(fileInfo); } } } + const schemas = await this._getSchemas(); + fileList.push(...schemas); + return fileList; }; + private _getSchemas = async (): Promise<FileInfo[]> => { + if (!(await this.exists())) { + throw new Error(`${this.dir} is not a valid path`); + } + + const schemasDir = Path.join(this.dir, 'Schemas'); + + if (!(await this.fileStorage.exists(schemasDir))) { + debug('No schemas directory found.'); + return []; + } + + const schemas: FileInfo[] = []; + const paths = await this.fileStorage.glob('*.schema', schemasDir); + + for (const path of paths) { + const fileInfo = await this._getFileInfo(Path.join(schemasDir, path)); + if (fileInfo) { + schemas.push(fileInfo); + } + } + + return schemas; + }; + + private _getFileInfo = async (path: string): Promise<FileInfo | undefined> => { + if ((await this.fileStorage.stat(path)).isFile) { + const content: string = await this.fileStorage.readFile(path); + return { + name: Path.basename(path), + content: content, + path: path, + relativePath: Path.relative(this.dir, path), + }; + } + }; + // check project stracture is valid or not, if not, try fix it. private _checkProjectStructure = async () => { const dialogs: DialogInfo[] = this.dialogs; From a98200165458e5b6ca8a2470716fbecc8e56d1fe Mon Sep 17 00:00:00 2001 From: TJ Durnford <tjdford@gmail.com> Date: Fri, 6 Mar 2020 19:18:43 -0800 Subject: [PATCH 31/40] fix: Moved value field to user tab and removed inline lu from attachment input (#2194) --- .../src/Form/fields/PromptField/Other.tsx | 14 +------------ .../src/Form/fields/PromptField/UserInput.tsx | 21 ++++++++++++++----- .../src/Form/widgets/LuEditorWidget.tsx | 5 ++++- 3 files changed, 21 insertions(+), 19 deletions(-) diff --git a/Composer/packages/extensions/obiformeditor/src/Form/fields/PromptField/Other.tsx b/Composer/packages/extensions/obiformeditor/src/Form/fields/PromptField/Other.tsx index 2e36c41bb7..581957d1c4 100644 --- a/Composer/packages/extensions/obiformeditor/src/Form/fields/PromptField/Other.tsx +++ b/Composer/packages/extensions/obiformeditor/src/Form/fields/PromptField/Other.tsx @@ -10,7 +10,6 @@ import { MicrosoftInputDialog } from '@bfc/shared'; import { WidgetLabel } from '../../widgets/WidgetLabel'; import { LgEditorWidget } from '../../widgets/LgEditorWidget'; -import { TextWidget } from '../../widgets'; import { Validations } from './Validations'; import { field } from './styles'; @@ -22,7 +21,7 @@ interface OtherProps extends FieldProps<MicrosoftInputDialog> { } export const Other: React.FC<OtherProps> = props => { - const { onChange, getSchema, idSchema, formData, errorSchema } = props; + const { onChange, getSchema, idSchema, formData } = props; return ( <React.Fragment> @@ -56,17 +55,6 @@ export const Other: React.FC<OtherProps> = props => { height={125} /> </div> - <div css={field}> - <TextWidget - onChange={onChange('value')} - schema={getSchema('value')} - id={idSchema.value.__id} - value={formData.value} - label={formatMessage('Value')} - formContext={props.formContext} - rawErrors={errorSchema.value && errorSchema.value.__errors} - /> - </div> <div css={field}> <WidgetLabel label={formatMessage('Default value response')} diff --git a/Composer/packages/extensions/obiformeditor/src/Form/fields/PromptField/UserInput.tsx b/Composer/packages/extensions/obiformeditor/src/Form/fields/PromptField/UserInput.tsx index d2550e546d..8a83eb5dda 100644 --- a/Composer/packages/extensions/obiformeditor/src/Form/fields/PromptField/UserInput.tsx +++ b/Composer/packages/extensions/obiformeditor/src/Form/fields/PromptField/UserInput.tsx @@ -43,11 +43,6 @@ export const UserInput: React.FC<UserInputProps> = props => { return ( <Fragment> - {usesLuisRecognizer(formContext.currentDialog) && ( - <div css={field}> - <LuEditorWidget name={intentName} formContext={formContext} /> - </div> - )} <div css={field}> <TextWidget onChange={onChange('property')} @@ -72,6 +67,22 @@ export const UserInput: React.FC<UserInputProps> = props => { /> </div> )} + <div css={field}> + <TextWidget + onChange={onChange('value')} + schema={getSchema('value')} + id={idSchema.value.__id} + value={formData.value} + label={formatMessage('Value')} + formContext={props.formContext} + rawErrors={errorSchema.value && errorSchema.value.__errors} + /> + </div> + {usesLuisRecognizer(formContext.currentDialog) && type !== SDKTypes.AttachmentInput && ( + <div css={field}> + <LuEditorWidget name={intentName} formContext={formContext} prompt /> + </div> + )} {getSchema('defaultLocale') && ( <div css={field}> <TextWidget diff --git a/Composer/packages/extensions/obiformeditor/src/Form/widgets/LuEditorWidget.tsx b/Composer/packages/extensions/obiformeditor/src/Form/widgets/LuEditorWidget.tsx index dba358ad9f..658419708e 100644 --- a/Composer/packages/extensions/obiformeditor/src/Form/widgets/LuEditorWidget.tsx +++ b/Composer/packages/extensions/obiformeditor/src/Form/widgets/LuEditorWidget.tsx @@ -17,6 +17,7 @@ interface LuEditorWidgetProps { name: string; height?: number | string; onChange?: (template?: string) => void; + prompt?: boolean; } export class LuEditorWidget extends React.Component<LuEditorWidgetProps> { @@ -83,7 +84,9 @@ export class LuEditorWidget extends React.Component<LuEditorWidgetProps> { ? diagnostic.message.split('error message: ')[diagnostic.message.split('error message: ').length - 1] : ''; - const label = formatMessage('Trigger phrases (intent: #{name})', { name }); + const label = prompt + ? formatMessage('Expected responses (intent: {name})', { name }) + : formatMessage('Trigger phrases (intent: {name})', { name }); return ( <> From 3eea8212cab0e71c7b4a26078f10c00a70feb7b2 Mon Sep 17 00:00:00 2001 From: leileizhang <leilzh@microsoft.com> Date: Sun, 8 Mar 2020 00:35:18 +0800 Subject: [PATCH 32/40] fix: use nightly build to replace the private bf-lu package (#2190) * use nightly build replace the private bf-lu * replace all * update the lock Co-authored-by: Dong Lei <donglei@microsoft.com> --- Composer/packages/client/package.json | 2 +- .../lib/indexers/__tests__/luUtil.test.ts | 2 +- Composer/packages/lib/indexers/package.json | 2 +- .../packages/lib/indexers/src/luIndexer.ts | 2 +- .../lib/indexers/src/types/bf-lu.d.ts | 2 +- .../packages/lib/indexers/src/utils/luUtil.ts | 2 +- Composer/packages/server/package.json | 2 +- .../server/src/models/bot/luPublisher.ts | 4 +- .../language-understanding/package.json | 2 +- .../language-understanding/src/LUServer.ts | 2 +- Composer/yarn.lock | 42 +++++++++---------- 11 files changed, 32 insertions(+), 32 deletions(-) diff --git a/Composer/packages/client/package.json b/Composer/packages/client/package.json index 842169d718..4b125b00bf 100644 --- a/Composer/packages/client/package.json +++ b/Composer/packages/client/package.json @@ -20,7 +20,7 @@ "@bfc/extensions": "*", "@bfc/indexers": "*", "@bfc/shared": "*", - "@bfcomposer/bf-lu": "^1.2.9", + "@microsoft/bf-lu": "4.8.0-preview.110925", "@emotion/core": "^10.0.7", "@reach/router": "^1.2.1", "axios": "^0.18.0", diff --git a/Composer/packages/lib/indexers/__tests__/luUtil.test.ts b/Composer/packages/lib/indexers/__tests__/luUtil.test.ts index 916061b82d..c900a5366a 100644 --- a/Composer/packages/lib/indexers/__tests__/luUtil.test.ts +++ b/Composer/packages/lib/indexers/__tests__/luUtil.test.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { sectionHandler } from '@bfcomposer/bf-lu/lib/parser'; +import { sectionHandler } from '@microsoft/bf-lu/lib/parser'; import { updateIntent, addIntent, removeIntent } from '../src/utils/luUtil'; diff --git a/Composer/packages/lib/indexers/package.json b/Composer/packages/lib/indexers/package.json index b0102f5096..5cdba6f35b 100644 --- a/Composer/packages/lib/indexers/package.json +++ b/Composer/packages/lib/indexers/package.json @@ -29,7 +29,7 @@ }, "dependencies": { "@bfc/shared": "*", - "@bfcomposer/bf-lu": "^1.2.9", + "@microsoft/bf-lu": "4.8.0-preview.110925", "botbuilder-lg": "^4.8.0-preview.106823", "botframework-expressions": "^4.8.0-preview.106476", "lodash": "^4.17.15" diff --git a/Composer/packages/lib/indexers/src/luIndexer.ts b/Composer/packages/lib/indexers/src/luIndexer.ts index 2a17c1dae9..8dc778af09 100644 --- a/Composer/packages/lib/indexers/src/luIndexer.ts +++ b/Composer/packages/lib/indexers/src/luIndexer.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { sectionHandler } from '@bfcomposer/bf-lu/lib/parser/composerindex'; +import { sectionHandler } from '@microsoft/bf-lu/lib/parser/composerindex'; import get from 'lodash/get'; import { LuIntentSection } from '@bfc/shared'; diff --git a/Composer/packages/lib/indexers/src/types/bf-lu.d.ts b/Composer/packages/lib/indexers/src/types/bf-lu.d.ts index f8df414a4c..13498d4a76 100644 --- a/Composer/packages/lib/indexers/src/types/bf-lu.d.ts +++ b/Composer/packages/lib/indexers/src/types/bf-lu.d.ts @@ -1,7 +1,7 @@ /// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -declare module '@bfcomposer/bf-lu/lib/parser/composerindex' { +declare module '@microsoft/bf-lu/lib/parser/composerindex' { namespace parser { function parseFile(fileContent: any, log: any, locale: any): any; function validateLUISBlob(LUISJSONBlob: any): any; diff --git a/Composer/packages/lib/indexers/src/utils/luUtil.ts b/Composer/packages/lib/indexers/src/utils/luUtil.ts index c4127f8b88..351d779267 100644 --- a/Composer/packages/lib/indexers/src/utils/luUtil.ts +++ b/Composer/packages/lib/indexers/src/utils/luUtil.ts @@ -7,7 +7,7 @@ * for more usage detail, please check client/__tests__/utils/luUtil.test.ts */ -import { sectionHandler } from '@bfcomposer/bf-lu/lib/parser/composerindex'; +import { sectionHandler } from '@microsoft/bf-lu/lib/parser/composerindex'; import isEmpty from 'lodash/isEmpty'; import { LuIntentSection } from '@bfc/shared'; diff --git a/Composer/packages/server/package.json b/Composer/packages/server/package.json index aab1124761..ce17df22c7 100644 --- a/Composer/packages/server/package.json +++ b/Composer/packages/server/package.json @@ -57,7 +57,7 @@ "@bfc/lg-languageserver": "*", "@bfc/lu-languageserver": "*", "@bfc/shared": "*", - "@bfcomposer/bf-lu": "^1.2.9", + "@microsoft/bf-lu": "4.8.0-preview.110925", "archiver": "^3.0.0", "axios": "^0.18.0", "azure-storage": "^2.10.3", diff --git a/Composer/packages/server/src/models/bot/luPublisher.ts b/Composer/packages/server/src/models/bot/luPublisher.ts index 67c589a260..4bdc5b3b95 100644 --- a/Composer/packages/server/src/models/bot/luPublisher.ts +++ b/Composer/packages/server/src/models/bot/luPublisher.ts @@ -11,9 +11,9 @@ import { ILuisConfig } from './interface'; import log from './../../logger'; // eslint-disable-next-line @typescript-eslint/no-var-requires -const crossTrainer = require('@bfcomposer/bf-lu/lib/parser/cross-train/crossTrainer.js'); +const crossTrainer = require('@microsoft/bf-lu/lib/parser/cross-train/crossTrainer.js'); // eslint-disable-next-line @typescript-eslint/no-var-requires -const luBuild = require('@bfcomposer/bf-lu/lib/parser/lubuild/builder.js'); +const luBuild = require('@microsoft/bf-lu/lib/parser/lubuild/builder.js'); const DIALOGS_FOLDER = 'ComposerDialogs'; const GENERATEDFOLDER = 'generated'; diff --git a/Composer/packages/tools/language-servers/language-understanding/package.json b/Composer/packages/tools/language-servers/language-understanding/package.json index f74756c548..3957f83c7f 100644 --- a/Composer/packages/tools/language-servers/language-understanding/package.json +++ b/Composer/packages/tools/language-servers/language-understanding/package.json @@ -19,7 +19,7 @@ }, "dependencies": { "@microsoft/bf-cli-command": "https://botbuilder.myget.org/F/botbuilder-declarative/npm/@microsoft/bf-cli-command/-/@microsoft/bf-cli-command-1.0.1.tgz", - "@bfcomposer/bf-lu": "^1.2.9", + "@microsoft/bf-lu": "4.8.0-preview.110925", "@types/node": "^12.0.4", "express": "^4.15.2", "monaco-editor-core": "^0.17.0", diff --git a/Composer/packages/tools/language-servers/language-understanding/src/LUServer.ts b/Composer/packages/tools/language-servers/language-understanding/src/LUServer.ts index b2aab572b0..5d83d0fcac 100644 --- a/Composer/packages/tools/language-servers/language-understanding/src/LUServer.ts +++ b/Composer/packages/tools/language-servers/language-understanding/src/LUServer.ts @@ -20,7 +20,7 @@ import { import { TextDocumentPositionParams, DocumentOnTypeFormattingParams } from 'vscode-languageserver-protocol'; import { updateIntent, isValid, checkSection } from '@bfc/indexers/lib/utils/luUtil'; import { luIndexer } from '@bfc/indexers'; -import { parser } from '@bfcomposer/bf-lu/lib/parser'; +import { parser } from '@microsoft/bf-lu/lib/parser'; import { EntityTypesObj, LineState } from './entityEnum'; import * as util from './matchingPattern'; diff --git a/Composer/yarn.lock b/Composer/yarn.lock index 262a651ef3..f161cf854a 100644 --- a/Composer/yarn.lock +++ b/Composer/yarn.lock @@ -2120,27 +2120,6 @@ lodash "^4.17.13" to-fast-properties "^2.0.0" -"@bfcomposer/bf-lu@^1.2.9": - version "1.2.9" - resolved "https://botbuilder.myget.org/F/botbuilder-declarative/npm/@bfcomposer/bf-lu/-/@bfcomposer/bf-lu-1.2.9.tgz#b2fec0d8f1c3d4d0e526a5bdbf9e5c9b3ab5b71d" - integrity sha1-sv7A2PHD1NDlJqW9v55cmzq1tx0= - dependencies: - "@azure/cognitiveservices-luis-authoring" "3.0.1" - "@azure/ms-rest-azure-js" "2.0.1" - antlr4 "^4.7.2" - chalk "2.4.1" - console-stream "^0.1.1" - deep-equal "^1.0.1" - delay "^4.3.0" - fs-extra "^8.1.0" - get-stdin "^6.0.0" - globby "^10.0.1" - intercept-stdout "^0.1.2" - lodash "^4.17.15" - node-fetch "^2.1.2" - semver "^5.5.1" - tslib "^1.10.0" - "@bfcomposer/monaco-editor-webpack-plugin@^1.7.2": version "1.7.2" resolved "https://botbuilder.myget.org/F/botbuilder-declarative/npm/@bfcomposer/monaco-editor-webpack-plugin/-/@bfcomposer/monaco-editor-webpack-plugin-1.7.2.tgz#f00ac5cec496dc3d44713d9142956b3336033eab" @@ -2735,6 +2714,27 @@ fs-extra "^7.0.1" tslib "~1.10.0" +"@microsoft/bf-lu@4.8.0-preview.110925": + version "4.8.0-preview.110925" + uid "8905960d5858aa9f4d94c150fdc80ac4b6947e94" + resolved "https://botbuilder.myget.org/F/botframework-cli/npm/@microsoft/bf-lu/-/@microsoft/bf-lu-4.8.0-preview.110925.tgz#8905960d5858aa9f4d94c150fdc80ac4b6947e94" + dependencies: + "@azure/cognitiveservices-luis-authoring" "3.0.1" + "@azure/ms-rest-azure-js" "2.0.1" + antlr4 "^4.7.2" + chalk "2.4.1" + console-stream "^0.1.1" + deep-equal "^1.0.1" + delay "^4.3.0" + fs-extra "^8.1.0" + get-stdin "^6.0.0" + globby "^10.0.1" + intercept-stdout "^0.1.2" + lodash "^4.17.15" + node-fetch "^2.1.2" + semver "^5.5.1" + tslib "^1.10.0" + "@microsoft/load-themed-styles@^1.7.13": version "1.9.5" resolved "https://registry.yarnpkg.com/@microsoft/load-themed-styles/-/load-themed-styles-1.9.5.tgz#b9f063bafa568e356b4d99a5b522d0bab60bda85" From d8f84418c1bb8c8b42133bc3191aef006af347bd Mon Sep 17 00:00:00 2001 From: Vadim Kacherov <vkacherov@gmail.com> Date: Sat, 7 Mar 2020 12:14:00 -0500 Subject: [PATCH 33/40] Update setup-yarn.md (#2160) Looks like the 3.1 is now required to test the bot Co-authored-by: Chris Whitten <christopher.whitten@microsoft.com> --- docs/setup-yarn.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/setup-yarn.md b/docs/setup-yarn.md index a9f4aa09d9..fd65c9a6c7 100644 --- a/docs/setup-yarn.md +++ b/docs/setup-yarn.md @@ -9,7 +9,7 @@ Bot Framework Composer is designed to be a hosted web app. Currently, you need t - [Node.js](https://nodejs.org/dist/v12.13.0/): version 12.13.0 - [Yarn](https://yarnpkg.com/en/docs/install): latest stable version - [Bot Framework Emulator](https://github.com/microsoft/BotFramework-Emulator/releases/latest): latest stable version -- [.NET Core SDK 2.2](https://dotnet.microsoft.com/download/dotnet-core/2.2): required to test your bot +- [.NET Core SDK 3.1](https://dotnet.microsoft.com/download/dotnet-core/current): required to test your bot ## Installation instructions 1. To start, open a terminal and clone the Composer GitHub repository. You will use this terminal for the rest of the steps in this section. From 8cd8859ddde83ac4d9e0714aa0a8ae4fbfbb317a Mon Sep 17 00:00:00 2001 From: Dara Oladapo <dara.oladapo@outlook.com> Date: Sat, 7 Mar 2020 20:27:30 +0300 Subject: [PATCH 34/40] Update setup-yarn.md (#2145) The documentation confused me for a bit. I ran the yarn and yarn build command before remembering I first needed to go into the BotFramework-Composer folder. I believe this makes it easier. Co-authored-by: Andy Brown <asbrown002@gmail.com> Co-authored-by: Chris Whitten <christopher.whitten@microsoft.com> --- docs/setup-yarn.md | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/docs/setup-yarn.md b/docs/setup-yarn.md index fd65c9a6c7..bd0e3c8ca0 100644 --- a/docs/setup-yarn.md +++ b/docs/setup-yarn.md @@ -18,14 +18,20 @@ Bot Framework Composer is designed to be a hosted web app. Currently, you need t git clone https://github.com/microsoft/BotFramework-Composer.git ``` -2. After cloning the repository, navigate to the **Bot Framework Composer** folder. Then run the following commands to navigate to the **Composer** folder and get all required packages: +2. After cloning the repository, navigate to the **Bot Framework Composer** folder. + + ``` + cd BotFramework-Composer + ``` + +3. Run the following commands to navigate to the **Composer** folder and get all required packages: ``` cd Composer yarn ``` -3. Next, run the following command to build the Composer application, this command can take several minutes to finish: +4. Next, run the following command to build the Composer application, this command can take several minutes to finish: ``` yarn build @@ -34,13 +40,13 @@ Bot Framework Composer is designed to be a hosted web app. Currently, you need t > [!NOTE] > If you are having trouble installing or building Composer run `yarn tableflip`. This will remove all of the Composer application's dependencies (node_modules) and then it reinstalls and rebuilds all of its dependencies. Once completed, run `yarn install` and `yarn build` again. This process generally takes 5-10 minutes. -4. Again using Yarn, start the Composer authoring application and the bot runtime: +5. Again using Yarn, start the Composer authoring application and the bot runtime: ``` yarn startall ``` -5. Once you see **Composer now running at:** appear in your terminal, you can run Composer in your browser using the address http://localhost:3000. +6. Once you see **Composer now running at:** appear in your terminal, you can run Composer in your browser using the address http://localhost:3000. ![browser address](./media/setup-yarn/address.png) From 27a4fdb2cecea823fd41a4f434dd50b6cb484b2e Mon Sep 17 00:00:00 2001 From: leileizhang <leilzh@microsoft.com> Date: Mon, 9 Mar 2020 23:41:05 +0800 Subject: [PATCH 35/40] fix: lu build bug when training empty intents (#2201) --- Composer/packages/client/package.json | 2 +- Composer/packages/lib/indexers/package.json | 2 +- Composer/packages/lib/indexers/src/luIndexer.ts | 2 +- Composer/packages/server/package.json | 2 +- .../language-servers/language-understanding/package.json | 2 +- Composer/yarn.lock | 8 ++++---- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Composer/packages/client/package.json b/Composer/packages/client/package.json index 4b125b00bf..ea80ad3363 100644 --- a/Composer/packages/client/package.json +++ b/Composer/packages/client/package.json @@ -20,7 +20,7 @@ "@bfc/extensions": "*", "@bfc/indexers": "*", "@bfc/shared": "*", - "@microsoft/bf-lu": "4.8.0-preview.110925", + "@microsoft/bf-lu": "4.8.0-preview.111952", "@emotion/core": "^10.0.7", "@reach/router": "^1.2.1", "axios": "^0.18.0", diff --git a/Composer/packages/lib/indexers/package.json b/Composer/packages/lib/indexers/package.json index 5cdba6f35b..953c0f7fc6 100644 --- a/Composer/packages/lib/indexers/package.json +++ b/Composer/packages/lib/indexers/package.json @@ -29,7 +29,7 @@ }, "dependencies": { "@bfc/shared": "*", - "@microsoft/bf-lu": "4.8.0-preview.110925", + "@microsoft/bf-lu": "4.8.0-preview.111952", "botbuilder-lg": "^4.8.0-preview.106823", "botframework-expressions": "^4.8.0-preview.106476", "lodash": "^4.17.15" diff --git a/Composer/packages/lib/indexers/src/luIndexer.ts b/Composer/packages/lib/indexers/src/luIndexer.ts index 8dc778af09..207707730c 100644 --- a/Composer/packages/lib/indexers/src/luIndexer.ts +++ b/Composer/packages/lib/indexers/src/luIndexer.ts @@ -15,7 +15,7 @@ const { luParser } = sectionHandler; function convertLuDiagnostic(d: any, source: string): Diagnostic { const severityMap = { ERROR: DiagnosticSeverity.Error, - WARNING: DiagnosticSeverity.Warning, + WARN: DiagnosticSeverity.Warning, INFORMATION: DiagnosticSeverity.Information, HINT: DiagnosticSeverity.Hint, }; diff --git a/Composer/packages/server/package.json b/Composer/packages/server/package.json index ce17df22c7..b55679aba0 100644 --- a/Composer/packages/server/package.json +++ b/Composer/packages/server/package.json @@ -57,7 +57,7 @@ "@bfc/lg-languageserver": "*", "@bfc/lu-languageserver": "*", "@bfc/shared": "*", - "@microsoft/bf-lu": "4.8.0-preview.110925", + "@microsoft/bf-lu": "4.8.0-preview.111952", "archiver": "^3.0.0", "axios": "^0.18.0", "azure-storage": "^2.10.3", diff --git a/Composer/packages/tools/language-servers/language-understanding/package.json b/Composer/packages/tools/language-servers/language-understanding/package.json index 3957f83c7f..cfd4d243c6 100644 --- a/Composer/packages/tools/language-servers/language-understanding/package.json +++ b/Composer/packages/tools/language-servers/language-understanding/package.json @@ -19,7 +19,7 @@ }, "dependencies": { "@microsoft/bf-cli-command": "https://botbuilder.myget.org/F/botbuilder-declarative/npm/@microsoft/bf-cli-command/-/@microsoft/bf-cli-command-1.0.1.tgz", - "@microsoft/bf-lu": "4.8.0-preview.110925", + "@microsoft/bf-lu": "4.8.0-preview.111952", "@types/node": "^12.0.4", "express": "^4.15.2", "monaco-editor-core": "^0.17.0", diff --git a/Composer/yarn.lock b/Composer/yarn.lock index f161cf854a..df1dcb8af2 100644 --- a/Composer/yarn.lock +++ b/Composer/yarn.lock @@ -2714,10 +2714,10 @@ fs-extra "^7.0.1" tslib "~1.10.0" -"@microsoft/bf-lu@4.8.0-preview.110925": - version "4.8.0-preview.110925" - uid "8905960d5858aa9f4d94c150fdc80ac4b6947e94" - resolved "https://botbuilder.myget.org/F/botframework-cli/npm/@microsoft/bf-lu/-/@microsoft/bf-lu-4.8.0-preview.110925.tgz#8905960d5858aa9f4d94c150fdc80ac4b6947e94" +"@microsoft/bf-lu@4.8.0-preview.111952": + version "4.8.0-preview.111952" + uid d1707a5f150ab204e67122f8b1d5796ad7f656c9 + resolved "https://botbuilder.myget.org/F/botframework-cli/npm/@microsoft/bf-lu/-/@microsoft/bf-lu-4.8.0-preview.111952.tgz#d1707a5f150ab204e67122f8b1d5796ad7f656c9" dependencies: "@azure/cognitiveservices-luis-authoring" "3.0.1" "@azure/ms-rest-azure-js" "2.0.1" From 66e5fb4f68fa0f810cf41c4c741bc0d6dc405b50 Mon Sep 17 00:00:00 2001 From: liweitian <liweitian93@outlook.com> Date: Tue, 10 Mar 2020 01:46:19 +0800 Subject: [PATCH 36/40] fix: trigger creation bug (#2151) * fix bug * save tmp code * add regex intent back * update * update label text * rename onMessageActivity to onMessageReceivedMessage * update trigger type validation function * remove commented code * rename onMessageActivity to onMessageReceivedMessage & update trigger type validation function * update regex field * remove commented code * remove commented code * revert auto-saved file * update label text * fix bug * save tmp code * add regex intent back * update * update label text * rename onMessageActivity to onMessageReceivedMessage * update trigger type validation function * remove commented code * update regex field * remove commented code * remove commented code * revert auto-saved file * update label text * update label text * update regEx api * add shellapi updateRegExIntentHandler * shell api * inline regex in form editor * create new inline regex intent when no intent Co-authored-by: Long Alan <julong@microsoft.com> Co-authored-by: Chris Whitten <christopher.whitten@microsoft.com> --- Composer/packages/client/src/ShellApi.ts | 10 +++ .../ProjectTree/TriggerCreationModal.tsx | 86 ++++++++++++++----- .../ExtensionContainer.tsx | 4 + .../client/src/pages/design/index.tsx | 18 ++-- .../packages/client/src/utils/dialogUtil.ts | 58 ++++++++++--- .../obiformeditor/demo/src/index.tsx | 1 + .../src/Form/fields/RecognizerField/index.tsx | 10 --- .../src/Form/widgets/IntentWidget.tsx | 31 ++++--- .../src/Form/widgets/RegexEditorWidget.tsx | 53 ++++++++++++ .../obiformeditor/src/schema/uischema.ts | 2 +- Composer/packages/lib/shared/src/appschema.ts | 9 +- Composer/packages/lib/shared/src/labelMap.ts | 4 + .../packages/lib/shared/src/types/schema.ts | 1 + .../packages/lib/shared/src/types/shell.ts | 1 + Composer/packages/lib/shared/src/viewUtils.ts | 2 +- .../packages/server/schemas/editor.schema | 4 +- Composer/packages/server/schemas/sdk.schema | 14 +-- 17 files changed, 230 insertions(+), 78 deletions(-) create mode 100644 Composer/packages/extensions/obiformeditor/src/Form/widgets/RegexEditorWidget.tsx diff --git a/Composer/packages/client/src/ShellApi.ts b/Composer/packages/client/src/ShellApi.ts index ed3dd20dea..11614ba51a 100644 --- a/Composer/packages/client/src/ShellApi.ts +++ b/Composer/packages/client/src/ShellApi.ts @@ -9,6 +9,7 @@ import get from 'lodash/get'; import { isExpression } from './utils'; import * as lgUtil from './utils/lgUtil'; import * as luUtil from './utils/luUtil'; +import { updateRegExIntent } from './utils/dialogUtil'; import { StoreContext } from './store'; import ApiClient from './messenger/ApiClient'; import { getDialogData, setDialogData, sanitizeDialogData } from './utils'; @@ -243,6 +244,14 @@ export const ShellApi: React.FC = () => { return await updateLuFile({ id, content }); } + async function updateRegExIntentHandler({ id, intentName, pattern }, event) { + if (isEventSourceValid(event) === false) return false; + const dialog = dialogs.find(dialog => dialog.id === id); + if (!dialog) throw new Error(`dialog ${dialogId} not found`); + const newDialog = updateRegExIntent(dialog, intentName, pattern); + return await updateDialog({ id, content: newDialog.content }); + } + async function fileHandler(fileTargetType, fileChangeType, { id, content }, event) { if (isEventSourceValid(event) === false) return false; @@ -345,6 +354,7 @@ export const ShellApi: React.FC = () => { apiClient.registerApi('addLuIntent', addLuIntentHandler); apiClient.registerApi('updateLuIntent', updateLuIntentHandler); apiClient.registerApi('removeLuIntent', removeLuIntentHandler); + apiClient.registerApi('updateRegExIntent', updateRegExIntentHandler); apiClient.registerApi('navTo', navTo); apiClient.registerApi('onFocusEvent', focusEvent); apiClient.registerApi('onFocusSteps', focusSteps); diff --git a/Composer/packages/client/src/components/ProjectTree/TriggerCreationModal.tsx b/Composer/packages/client/src/components/ProjectTree/TriggerCreationModal.tsx index 472df83534..a9e70383b3 100644 --- a/Composer/packages/client/src/components/ProjectTree/TriggerCreationModal.tsx +++ b/Composer/packages/client/src/components/ProjectTree/TriggerCreationModal.tsx @@ -17,7 +17,7 @@ import get from 'lodash/get'; import { LuEditor } from '@bfc/code-editor'; import { - addNewTrigger, + generateNewDialog, getTriggerTypes, TriggerFormData, TriggerFormDataErrors, @@ -28,6 +28,7 @@ import { getEventTypes, getActivityTypes, getMessageTypes, + regexRecognizerKey, } from '../../utils/dialogUtil'; import { addIntent } from '../../utils/luUtil'; import { StoreContext } from '../../store'; @@ -35,9 +36,13 @@ import { StoreContext } from '../../store'; import { styles, dropdownStyles, dialogWindow, intent } from './styles'; const nameRegex = /^[a-zA-Z0-9-_.]+$/; -const validateForm = (data: TriggerFormData): TriggerFormDataErrors => { +const validateForm = ( + data: TriggerFormData, + isRegEx: boolean, + regExIntents: [{ intent: string; pattern: string }] +): TriggerFormDataErrors => { const errors: TriggerFormDataErrors = {}; - const { $type, specifiedType, intent, triggerPhrases } = data; + const { $type, specifiedType, intent, triggerPhrases, regexEx } = data; if ($type === eventTypeKey && !specifiedType) { errors.specifiedType = formatMessage('Please select a event type'); @@ -47,17 +52,29 @@ const validateForm = (data: TriggerFormData): TriggerFormDataErrors => { errors.specifiedType = formatMessage('Please select an activity type'); } + if ($type === messageTypeKey && !specifiedType) { + errors.specifiedType = formatMessage('Please select a message type'); + } + if (!$type) { errors.$type = formatMessage('Please select a trigger type'); } - if (!intent || !nameRegex.test(intent)) { + if ($type === intentTypeKey && (!intent || !nameRegex.test(intent))) { errors.intent = formatMessage( 'Spaces and special characters are not allowed. Use letters, numbers, -, or _., numbers, -, and _' ); } - if (!triggerPhrases) { + if ($type === intentTypeKey && isRegEx && regExIntents.find(ri => ri.intent === intent)) { + errors.intent = `regEx ${intent} is already defined`; + } + + if ($type === intentTypeKey && isRegEx && !regexEx) { + errors.regexEx = formatMessage('Please input regEx pattern'); + } + + if ($type === intentTypeKey && !isRegEx && !triggerPhrases) { errors.triggerPhrases = formatMessage('Please input trigger phrases'); } if (data.errors.triggerPhrases) { @@ -66,7 +83,7 @@ const validateForm = (data: TriggerFormData): TriggerFormDataErrors => { return errors; }; -interface LuFilePayload { +export interface LuFilePayload { id: string; content: string; } @@ -75,7 +92,7 @@ interface TriggerCreationModalProps { dialogId: string; isOpen: boolean; onDismiss: () => void; - onSubmit: (dialog: DialogInfo, luFilePayload: LuFilePayload) => void; + onSubmit: (dialog: DialogInfo, luFilePayload?: LuFilePayload) => void; } const initialFormData: TriggerFormData = { @@ -84,6 +101,7 @@ const initialFormData: TriggerFormData = { specifiedType: '', intent: '', triggerPhrases: '', + regexEx: '', }; const triggerTypeOptions: IDropdownOption[] = getTriggerTypes(); @@ -94,10 +112,12 @@ export const TriggerCreationModal: React.FC<TriggerCreationModalProps> = props = const { state } = useContext(StoreContext); const { dialogs, luFiles } = state; const luFile = luFiles.find(lu => lu.id === dialogId); - + const dialogFile = dialogs.find(dialog => dialog.id === dialogId); + const isRegEx = get(dialogFile, 'content.recognizer.$type', '') === regexRecognizerKey; + const regexIntents = get(dialogFile, 'content.recognizer.intents', []); const onClickSubmitButton = e => { e.preventDefault(); - const errors = validateForm(formData); + const errors = validateForm(formData, isRegEx, regexIntents); if (Object.keys(errors).length) { setFormData({ @@ -108,13 +128,17 @@ export const TriggerCreationModal: React.FC<TriggerCreationModalProps> = props = } const content = get(luFile, 'content', ''); - const newContent = addIntent(content, { Name: formData.intent, Body: formData.triggerPhrases }); - const updateLuFile = { - id: dialogId, - content: newContent, - }; - const newDialog = addNewTrigger(dialogs, dialogId, formData); - onSubmit(newDialog, updateLuFile); + const newDialog = generateNewDialog(dialogs, dialogId, formData); + if (formData.$type === intentTypeKey && !isRegEx) { + const newContent = addIntent(content, { Name: formData.intent, Body: formData.triggerPhrases }); + const updateLuFile = { + id: dialogId, + content: newContent, + }; + onSubmit(newDialog, updateLuFile); + } else { + onSubmit(newDialog); + } onDismiss(); }; @@ -130,6 +154,10 @@ export const TriggerCreationModal: React.FC<TriggerCreationModalProps> = props = setFormData({ ...formData, intent: name }); }; + const onChangeRegEx = (e, pattern) => { + setFormData({ ...formData, regexEx: pattern }); + }; + const onTriggerPhrasesChange = (body: string) => { const errors = formData.errors; const content = '#' + formData.intent + '\n' + body; @@ -142,7 +170,9 @@ export const TriggerCreationModal: React.FC<TriggerCreationModalProps> = props = const activityTypes: IDropdownOption[] = getActivityTypes(); const messageTypes: IDropdownOption[] = getMessageTypes(); - const showIntentFields = formData.$type === intentTypeKey; + const showIntentName = formData.$type === intentTypeKey; + const showRegExDropDown = formData.$type === intentTypeKey && isRegEx; + const showTriggerPhrase = formData.$type === intentTypeKey && !isRegEx; const showEventDropDown = formData.$type === eventTypeKey; const showActivityDropDown = formData.$type === activityTypeKey; const showMessageDropDown = formData.$type === messageTypeKey; @@ -172,7 +202,6 @@ export const TriggerCreationModal: React.FC<TriggerCreationModalProps> = props = data-testid={'triggerTypeDropDown'} defaultSelectedKey={intentTypeKey} /> - {showEventDropDown && ( <Dropdown placeholder={formatMessage('Select a event type')} @@ -206,17 +235,30 @@ export const TriggerCreationModal: React.FC<TriggerCreationModalProps> = props = data-testid={'messageTypeDropDown'} /> )} - {showIntentFields && ( + {showIntentName && ( <TextField - label={formatMessage('What is the name of this trigger')} + label={ + isRegEx + ? formatMessage('What is the name of this trigger (RegEx)') + : formatMessage('What is the name of this trigger (Luis)') + } styles={intent} onChange={onNameChange} errorMessage={formData.errors.intent} data-testid="TriggerName" /> )} - {showIntentFields && <Label>{formatMessage('Trigger Phrases')}</Label>} - {showIntentFields && ( + + {showRegExDropDown && ( + <TextField + label={formatMessage('Please input regex pattern')} + onChange={onChangeRegEx} + errorMessage={formData.errors.regexEx} + data-testid={'RegExDropDown'} + /> + )} + {showTriggerPhrase && <Label>{formatMessage('Trigger phrases')}</Label>} + {showTriggerPhrase && ( <LuEditor onChange={onTriggerPhrasesChange} value={formData.triggerPhrases} diff --git a/Composer/packages/client/src/extension-container/ExtensionContainer.tsx b/Composer/packages/client/src/extension-container/ExtensionContainer.tsx index 9ad680cfd9..79fc535df8 100644 --- a/Composer/packages/client/src/extension-container/ExtensionContainer.tsx +++ b/Composer/packages/client/src/extension-container/ExtensionContainer.tsx @@ -110,6 +110,10 @@ const shellApi: ShellApi = { return apiClient.apiCall('removeLuIntent', { id, intentName }); }, + updateRegExIntent: (id, intentName, pattern) => { + return apiClient.apiCall('updateRegExIntent', { id, intentName, pattern }); + }, + createDialog: () => { return apiClient.apiCall('createDialog'); }, diff --git a/Composer/packages/client/src/pages/design/index.tsx b/Composer/packages/client/src/pages/design/index.tsx index 25655f874f..b0b36a2020 100644 --- a/Composer/packages/client/src/pages/design/index.tsx +++ b/Composer/packages/client/src/pages/design/index.tsx @@ -12,12 +12,13 @@ import { globalHistory } from '@reach/router'; import get from 'lodash/get'; import { PromptTab } from '@bfc/shared'; import { getNewDesigner, seedNewDialog } from '@bfc/shared'; +import { DialogInfo } from '@bfc/indexers'; import { VisualEditorAPI } from '../../messenger/FrameAPI'; import { TestController } from '../../TestController'; import { BASEPATH, DialogDeleting } from '../../constants'; import { createSelectedPath, deleteTrigger, getbreadcrumbLabel } from '../../utils'; -import { TriggerCreationModal } from '../../components/ProjectTree/TriggerCreationModal'; +import { TriggerCreationModal, LuFilePayload } from '../../components/ProjectTree/TriggerCreationModal'; import { Conversation } from '../../components/Conversation'; import { DialogStyle } from '../../components/Modal/styles'; import { OpenConfirmModal } from '../../components/Modal/Confirm'; @@ -171,18 +172,21 @@ function DesignPage(props) { setTriggerModalVisibility(true); }; - const onTriggerCreationSubmit = (dialog, luFile) => { + const onTriggerCreationSubmit = (dialog: DialogInfo, luFile?: LuFilePayload) => { const dialogPayload = { id: dialog.id, content: dialog.content, }; - const luFilePayload = { - id: luFile.id, - content: luFile.content, - }; + if (luFile) { + const luFilePayload = { + id: luFile.id, + content: luFile.content, + }; + actions.updateLuFile(luFilePayload); + } + const index = get(dialog, 'content.triggers', []).length - 1; actions.selectTo(`triggers[${index}]`); - actions.updateLuFile(luFilePayload); actions.updateDialog(dialogPayload); }; diff --git a/Composer/packages/client/src/utils/dialogUtil.ts b/Composer/packages/client/src/utils/dialogUtil.ts index e54934034c..ded58597d9 100644 --- a/Composer/packages/client/src/utils/dialogUtil.ts +++ b/Composer/packages/client/src/utils/dialogUtil.ts @@ -24,6 +24,7 @@ export interface TriggerFormData { specifiedType: string; intent: string; triggerPhrases: string; + regexEx: string; } export interface TriggerFormDataErrors { @@ -31,6 +32,7 @@ export interface TriggerFormDataErrors { intent?: string; specifiedType?: string; triggerPhrases?: string; + regexEx?: string; } export function getDialog(dialogs: DialogInfo[], dialogId: string) { @@ -60,8 +62,15 @@ export function getFriendlyName(data) { return data.$type; } -export function insert(content, path: string, position: number | undefined, data: TriggerFormData) { +export function insert(content, path: string, position: number | undefined, data: any) { const current = get(content, path, []); + const insertAt = typeof position === 'undefined' ? current.length : position; + current.splice(insertAt, 0, data); + set(content, path, current); + return content; +} + +export function generateNewTrigger(data: TriggerFormData) { const optionalAttributes: { intent?: string; event?: string } = {}; if (data.specifiedType) { data.$type = data.specifiedType; @@ -73,23 +82,52 @@ export function insert(content, path: string, position: number | undefined, data $type: data.$type, ...seedNewDialog(data.$type, {}, optionalAttributes), }; + return newStep; +} - const insertAt = typeof position === 'undefined' ? current.length : position; - - current.splice(insertAt, 0, newStep); +export function generateRegexExpression(intent: string, pattern: string) { + return { intent, pattern }; +} - set(content, path, current); +export function createNewTrigger(dialog: DialogInfo, data: TriggerFormData): DialogInfo { + const dialogCopy = cloneDeep(dialog); + const trigger = generateNewTrigger(data); + insert(dialogCopy.content, 'triggers', undefined, trigger); + return dialogCopy; +} - return content; +export function createRegExIntent(dialog: DialogInfo, intent: string, pattern: string): DialogInfo { + const regex = generateRegexExpression(intent, pattern); + const dialogCopy = cloneDeep(dialog); + insert(dialogCopy.content, 'recognizer.intents', undefined, regex); + return dialogCopy; } -export function addNewTrigger(dialogs: DialogInfo[], dialogId: string, data: TriggerFormData): DialogInfo { - const dialogCopy = getDialog(dialogs, dialogId); - if (!dialogCopy) throw new Error(`dialog ${dialogId} does not exist`); - insert(dialogCopy.content, 'triggers', undefined, data); +export function updateRegExIntent(dialog: DialogInfo, intent: string, pattern: string): DialogInfo { + let dialogCopy = cloneDeep(dialog); + const regexIntents = get(dialogCopy, 'content.recognizer.intents', []); + const targetIntent = regexIntents.find(ri => ri.intent === intent); + if (!targetIntent) { + dialogCopy = createRegExIntent(dialog, intent, pattern); + } else { + targetIntent.pattern = pattern; + } return dialogCopy; } +export function generateNewDialog(dialogs: DialogInfo[], dialogId: string, data: TriggerFormData): DialogInfo { + //add new trigger + const dialog = dialogs.find(dialog => dialog.id === dialogId); + if (!dialog) throw new Error(`dialog ${dialogId} does not exist`); + let updatedDialog = createNewTrigger(dialog, data); + + //add regex expression + if (data.regexEx) { + updatedDialog = createRegExIntent(updatedDialog, data.intent, data.regexEx); + } + return updatedDialog; +} + export function createSelectedPath(selected: number) { return `triggers[${selected}]`; } diff --git a/Composer/packages/extensions/obiformeditor/demo/src/index.tsx b/Composer/packages/extensions/obiformeditor/demo/src/index.tsx index a399e293d7..5178b5d728 100644 --- a/Composer/packages/extensions/obiformeditor/demo/src/index.tsx +++ b/Composer/packages/extensions/obiformeditor/demo/src/index.tsx @@ -176,6 +176,7 @@ const mockShellApi = [ 'addLuIntent', 'updateLuIntent', 'removeLuIntent', + 'updateRegExIntent', 'validateExpression', 'onFocusSteps', 'onFocusEvent', diff --git a/Composer/packages/extensions/obiformeditor/src/Form/fields/RecognizerField/index.tsx b/Composer/packages/extensions/obiformeditor/src/Form/fields/RecognizerField/index.tsx index 5a490cfda5..70c27c0f64 100644 --- a/Composer/packages/extensions/obiformeditor/src/Form/fields/RecognizerField/index.tsx +++ b/Composer/packages/extensions/obiformeditor/src/Form/fields/RecognizerField/index.tsx @@ -11,9 +11,6 @@ import { LuFile } from '@bfc/indexers'; import { BaseField } from '../BaseField'; -import ToggleEditor from './ToggleEditor'; -import RegexEditor from './RegexEditor'; - import './styles.css'; export const RecognizerField: React.FC<FieldProps<MicrosoftIRecognizer>> = props => { @@ -127,13 +124,6 @@ export const RecognizerField: React.FC<FieldProps<MicrosoftIRecognizer>> = props responsiveMode={ResponsiveMode.large} onRenderTitle={onRenderTitle} /> - <ToggleEditor key={getSelectedType()} title={'regular expression editor'} loaded={Boolean(isRegex)}> - {() => { - if (isRegex) { - return <RegexEditor {...props} />; - } - }} - </ToggleEditor> </BaseField> </div> ); diff --git a/Composer/packages/extensions/obiformeditor/src/Form/widgets/IntentWidget.tsx b/Composer/packages/extensions/obiformeditor/src/Form/widgets/IntentWidget.tsx index e5121ab40c..b659fb552f 100644 --- a/Composer/packages/extensions/obiformeditor/src/Form/widgets/IntentWidget.tsx +++ b/Composer/packages/extensions/obiformeditor/src/Form/widgets/IntentWidget.tsx @@ -10,10 +10,12 @@ import { BFDWidgetProps } from '../types'; import { WidgetLabel } from './WidgetLabel'; import { LuEditorWidget } from './LuEditorWidget'; +import { RegexEditorWidget } from './RegexEditorWidget'; const EMPTY_OPTION = { key: '', text: '' }; enum RecognizerType { + 'none', 'regex', 'luis', } @@ -29,15 +31,7 @@ function recognizerType({ content }: DialogInfo): RecognizerType | null { } } - return null; -} - -function regexIntentOptions({ content }: DialogInfo): IDropdownOption[] { - const { recognizer } = content; - return (recognizer?.intents || []).reduce( - (acc, { intent }) => (intent ? [...acc, { key: intent, text: intent }] : acc), - [EMPTY_OPTION] - ); + return RecognizerType.none; } export const IntentWidget: React.FC<BFDWidgetProps> = props => { @@ -46,7 +40,16 @@ export const IntentWidget: React.FC<BFDWidgetProps> = props => { const { currentDialog } = formContext; const type = recognizerType(currentDialog); - const options: IDropdownOption[] = type === RecognizerType.regex ? regexIntentOptions(currentDialog) : [EMPTY_OPTION]; + let options: IDropdownOption[] = []; + + switch (type) { + case RecognizerType.regex: + case RecognizerType.luis: + break; + default: + options = [EMPTY_OPTION]; + break; + } const handleChange = (_e, option): void => { if (option) { @@ -56,13 +59,11 @@ export const IntentWidget: React.FC<BFDWidgetProps> = props => { return ( <> - {type === RecognizerType.luis ? ( - <LuEditorWidget formContext={formContext} name={value} height={316} /> - ) : ( + {type === RecognizerType.none && ( <> <WidgetLabel label={label} description={description} id={id} /> <Dropdown - id={id} + id={id.replace(/\.|#/g, '')} onBlur={() => onBlur && onBlur(id, value)} onChange={handleChange} onFocus={() => onFocus && onFocus(id, value)} @@ -74,6 +75,8 @@ export const IntentWidget: React.FC<BFDWidgetProps> = props => { /> </> )} + {type === RecognizerType.luis && <LuEditorWidget formContext={formContext} name={value} height={316} />} + {type === RecognizerType.regex && <RegexEditorWidget formContext={formContext} name={value} />} </> ); }; diff --git a/Composer/packages/extensions/obiformeditor/src/Form/widgets/RegexEditorWidget.tsx b/Composer/packages/extensions/obiformeditor/src/Form/widgets/RegexEditorWidget.tsx new file mode 100644 index 0000000000..6909cb0585 --- /dev/null +++ b/Composer/packages/extensions/obiformeditor/src/Form/widgets/RegexEditorWidget.tsx @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** @jsx jsx */ +import { jsx } from '@emotion/core'; +import formatMessage from 'format-message'; +import { RegexRecognizer } from '@bfc/shared'; +import { DialogInfo } from '@bfc/indexers'; +import { useState } from 'react'; + +import { FormContext } from '../types'; + +import { TextWidget } from '.'; + +interface RegexEditorWidgetProps { + formContext: FormContext; + name: string; + onChange?: (template?: string) => void; +} + +function getRegexIntentPattern(currentDialog: DialogInfo, intent: string): string | null { + const recognizer = currentDialog.content.recognizer as RegexRecognizer; + let pattern: string | null = null; + + if (!recognizer) { + return null; + } + + if (recognizer.intents) { + pattern = recognizer.intents.find(i => i.intent === intent)?.pattern || null; + } + + return pattern; +} +export const RegexEditorWidget: React.FC<RegexEditorWidgetProps> = props => { + const { formContext, name } = props; + const { currentDialog } = formContext; + const label = formatMessage('Trigger phrases (intent: #{name})', { name }); + const [localValue, setLocalValue] = useState(getRegexIntentPattern(currentDialog, name)); + const handleIntentchange = (pattern): void => { + setLocalValue(pattern); + formContext.shellApi.updateRegExIntent(currentDialog.id, name, pattern); + }; + return ( + <TextWidget + id="regIntent" + label={label} + onChange={handleIntentchange} + formContext={formContext} + value={localValue} + /> + ); +}; diff --git a/Composer/packages/extensions/obiformeditor/src/schema/uischema.ts b/Composer/packages/extensions/obiformeditor/src/schema/uischema.ts index 01fdd5ed71..54b5796491 100644 --- a/Composer/packages/extensions/obiformeditor/src/schema/uischema.ts +++ b/Composer/packages/extensions/obiformeditor/src/schema/uischema.ts @@ -159,7 +159,7 @@ export const uiSchema: { [key in SDKTypes]?: UiSchema } = { [SDKTypes.OnInvokeActivity]: { ...triggerUiSchema, }, - [SDKTypes.OnMessageActivity]: { + [SDKTypes.OnMessageReceivedActivity]: { ...triggerUiSchema, }, [SDKTypes.OnMessageDeleteActivity]: { diff --git a/Composer/packages/lib/shared/src/appschema.ts b/Composer/packages/lib/shared/src/appschema.ts index f52fb02d27..3d9ff52432 100644 --- a/Composer/packages/lib/shared/src/appschema.ts +++ b/Composer/packages/lib/shared/src/appschema.ts @@ -2324,13 +2324,14 @@ export const appschema: OBISchema = { }, }, }, - 'Microsoft.OnMessageActivity': { + 'Microsoft.OnMessageReceivedActivity': { $role: 'unionType(Microsoft.ITriggerCondition)', - title: 'On Message activity', - description: "Actions to perform on receipt of an activity with type 'Message'. Overrides Intent trigger.", + title: 'On MessageRecieved activity', + description: + "Actions to perform on receipt of an activity with type 'MessageRecieved'. Overrides Intent trigger.", type: 'object', properties: { - ...$properties(SDKTypes.OnMessageActivity), + ...$properties(SDKTypes.OnMessageReceivedActivity), condition: { $role: 'expression', title: 'Condition', diff --git a/Composer/packages/lib/shared/src/labelMap.ts b/Composer/packages/lib/shared/src/labelMap.ts index b49e7a4714..f1a03fd2b0 100644 --- a/Composer/packages/lib/shared/src/labelMap.ts +++ b/Composer/packages/lib/shared/src/labelMap.ts @@ -175,6 +175,10 @@ export const ConceptLabels: { [key in ConceptLabelKey]?: LabelOverride } = { title: formatMessage('Message events'), subtitle: formatMessage('Message recieved activity'), }, + [SDKTypes.OnMessageReceivedActivity]: { + title: formatMessage('Message received'), + subtitle: formatMessage('Message recieved activity'), + }, [SDKTypes.OnMessageDeleteActivity]: { title: formatMessage('Message deleted'), subtitle: formatMessage('Message deleted activity'), diff --git a/Composer/packages/lib/shared/src/types/schema.ts b/Composer/packages/lib/shared/src/types/schema.ts index 6bcd5b2818..fa1330a206 100644 --- a/Composer/packages/lib/shared/src/types/schema.ts +++ b/Composer/packages/lib/shared/src/types/schema.ts @@ -74,6 +74,7 @@ export enum SDKTypes { OnIntent = 'Microsoft.OnIntent', OnInvokeActivity = 'Microsoft.OnInvokeActivity', OnMessageActivity = 'Microsoft.OnMessageActivity', + OnMessageReceivedActivity = 'Microsoft.OnMessageReceivedActivity', OnMessageDeleteActivity = 'Microsoft.OnMessageDeleteActivity', OnMessageReactionActivity = 'Microsoft.OnMessageReactionActivity', OnMessageUpdateActivity = 'Microsoft.OnMessageUpdateActivity', diff --git a/Composer/packages/lib/shared/src/types/shell.ts b/Composer/packages/lib/shared/src/types/shell.ts index 521d3600a1..cde567c97a 100644 --- a/Composer/packages/lib/shared/src/types/shell.ts +++ b/Composer/packages/lib/shared/src/types/shell.ts @@ -72,6 +72,7 @@ export interface ShellApi { removeLgTemplates: (id: string, templateNames: string[]) => Promise<void>; addLuIntent: (id: string, intent: LuIntentSection | null) => Promise<void>; updateLuIntent: (id: string, intentName: string, intent: LuIntentSection | null) => Promise<void>; + updateRegExIntent: (id: string, intentName: string, pattern: string) => Promise<void>; removeLuIntent: (id: string, intentName: string) => Promise<void>; createDialog: () => Promise<string>; validateExpression: (expression?: string) => Promise<boolean>; diff --git a/Composer/packages/lib/shared/src/viewUtils.ts b/Composer/packages/lib/shared/src/viewUtils.ts index 923c54b228..31c92478b4 100644 --- a/Composer/packages/lib/shared/src/viewUtils.ts +++ b/Composer/packages/lib/shared/src/viewUtils.ts @@ -129,7 +129,7 @@ export const dialogGroups: DialogGroupsMap = { [DialogGroup.MESSAGE_EVENTS]: { label: 'Message events', types: [ - SDKTypes.OnMessageActivity, + SDKTypes.OnMessageReceivedActivity, SDKTypes.OnMessageDeleteActivity, SDKTypes.OnMessageReactionActivity, SDKTypes.OnMessageUpdateActivity, diff --git a/Composer/packages/server/schemas/editor.schema b/Composer/packages/server/schemas/editor.schema index 3d4c987261..672349bee7 100644 --- a/Composer/packages/server/schemas/editor.schema +++ b/Composer/packages/server/schemas/editor.schema @@ -199,8 +199,8 @@ "title": "Conversation invoked", "subtitle": "Invoke activity" }, - "Microsoft.OnMessageActivity": { - "title": "Message events", + "Microsoft.OnMessageReceivedActivity": { + "title": "Message recieved", "subtitle": "Message recieved activity" }, "Microsoft.OnMessageDeleteActivity": { diff --git a/Composer/packages/server/schemas/sdk.schema b/Composer/packages/server/schemas/sdk.schema index a1c8661f27..be2633ce59 100644 --- a/Composer/packages/server/schemas/sdk.schema +++ b/Composer/packages/server/schemas/sdk.schema @@ -351,9 +351,9 @@ "$ref": "#/definitions/Microsoft.OnInvokeActivity" }, { - "title": "Microsoft.OnMessageActivity", - "description": "Actions to perform on receipt of an activity with type 'Message'. Overrides Intent trigger.", - "$ref": "#/definitions/Microsoft.OnMessageActivity" + "title": "Microsoft.OnMessageReceivedActivity", + "description": "Actions to perform on receipt of an activity with type 'MessageReceived'. Overrides Intent trigger.", + "$ref": "#/definitions/Microsoft.OnMessageReceivedActivity" }, { "title": "Microsoft.OnMessageDeleteActivity", @@ -4891,9 +4891,9 @@ "$ref": "#/definitions/Microsoft.OnInvokeActivity" }, { - "title": "Microsoft.OnMessageActivity", + "title": "Microsoft.OnMessageReceivedActivity", "description": "Actions to perform on receipt of an activity with type 'Message'. Overrides Intent trigger.", - "$ref": "#/definitions/Microsoft.OnMessageActivity" + "$ref": "#/definitions/Microsoft.OnMessageReceivedActivity" }, { "title": "Microsoft.OnMessageDeleteActivity", @@ -8184,7 +8184,7 @@ } ] }, - "Microsoft.OnMessageActivity": { + "Microsoft.OnMessageReceivedActivity": { "$role": "union(Microsoft.ITriggerCondition)", "title": "On Message activity", "description": "Actions to perform on receipt of an activity with type 'Message'. Overrides Intent trigger.", @@ -8195,7 +8195,7 @@ "description": "Defines the valid properties for the component you are configuring (from a dialog .schema file)", "type": "string", "pattern": "^[a-zA-Z][a-zA-Z0-9.]*$", - "const": "Microsoft.OnMessageActivity" + "const": "Microsoft.OnMessageReceivedActivity" }, "$copy": { "title": "$copy", From 79f9040fc0a1d6c13900b892a0be106519ba0a67 Mon Sep 17 00:00:00 2001 From: zeye <2295905420@qq.com> Date: Tue, 10 Mar 2020 04:50:08 +0800 Subject: [PATCH 37/40] defense lg copy API failure (#2198) Co-authored-by: Chris Whitten <christopher.whitten@microsoft.com> --- Composer/packages/extensions/visual-designer/src/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Composer/packages/extensions/visual-designer/src/index.tsx b/Composer/packages/extensions/visual-designer/src/index.tsx index 974b59904f..8896a1c83d 100644 --- a/Composer/packages/extensions/visual-designer/src/index.tsx +++ b/Composer/packages/extensions/visual-designer/src/index.tsx @@ -76,7 +76,7 @@ const VisualDesigner: React.FC<VisualDesignerProps> = ({ clipboardActions: clipboardActions || [], updateLgTemplate, getLgTemplates, - copyLgTemplate, + copyLgTemplate: (id: string, from: string, to?: string) => copyLgTemplate(id, from, to).catch(() => ''), removeLgTemplate, removeLgTemplates, removeLuIntent, From ceb8186eca00c81f74d31a633d40dfdf455259f2 Mon Sep 17 00:00:00 2001 From: Andy Brown <asbrown002@gmail.com> Date: Mon, 9 Mar 2020 14:59:50 -0700 Subject: [PATCH 38/40] release: update changelog (#2204) * add a11y to tags and add additional memebers * update changelog --- CHANGELOG.md | 51 +++++++++++++++++++++++++++++++++++ scripts/generate-changelog.js | 26 +++++++++--------- 2 files changed, 65 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d0f67e1b60..aee5732f2f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,57 @@ ## Releases +### 03-09-2020 + +#### Added + +- feat: Remove input LU when deconstructing prompts ([#2180](https://github.com/microsoft/BotFramework-Composer/pull/2180)) ([@tdurnford](https://github.com/tdurnford)) +- feat: Add cross train before luis publish ([#2069](https://github.com/microsoft/BotFramework-Composer/pull/2069)) ([@cwhitten](https://github.com/cwhitten)) +- feat: Added inline lu to prompts ([#2159](https://github.com/microsoft/BotFramework-Composer/pull/2159)) ([@cwhitten](https://github.com/cwhitten)) +- feat: implement new action design to improve readability ([#2136](https://github.com/microsoft/BotFramework-Composer/pull/2136)) ([@cwhitten](https://github.com/cwhitten)) +- feat: repaint ui for setProperties in visual editor ([#2017](https://github.com/microsoft/BotFramework-Composer/pull/2017)) ([@alanlong9278](https://github.com/alanlong9278)) +- feat: Update package and schema to 200216 ([#1997](https://github.com/microsoft/BotFramework-Composer/pull/1997)) ([@luhan2017](https://github.com/luhan2017)) +- feat: update new trigger modal according to design ([#1786](https://github.com/microsoft/BotFramework-Composer/pull/1786)) ([@liweitian](https://github.com/liweitian)) +- feat: display 6 actions as contentless node ([#2108](https://github.com/microsoft/BotFramework-Composer/pull/2108)) ([@yeze322](https://github.com/yeze322)) +- feat: support multi-line node block in Visual Editor ([#2005](https://github.com/microsoft/BotFramework-Composer/pull/2005)) ([@yeze322](https://github.com/yeze322)) +- feat: add more help links ([#2070](https://github.com/microsoft/BotFramework-Composer/pull/2070)) ([@a-b-r-o-w-n](https://github.com/a-b-r-o-w-n)) +- feat: support inline LU section editing ([#1994](https://github.com/microsoft/BotFramework-Composer/pull/1994)) ([@zhixzhan](https://github.com/zhixzhan)) + +#### Fixed + +- fix: trigger creation bug ([#2151](https://github.com/microsoft/BotFramework-Composer/pull/2151)) ([@liweitian](https://github.com/liweitian)) +- fix: lu build bug when training empty intents ([#2201](https://github.com/microsoft/BotFramework-Composer/pull/2201)) ([@lei9444](https://github.com/lei9444)) +- fix: use nightly build to replace the private bf-lu package ([#2190](https://github.com/microsoft/BotFramework-Composer/pull/2190)) ([@lei9444](https://github.com/lei9444)) +- fix: Moved value field to user tab and removed inline lu from attachment input ([#2194](https://github.com/microsoft/BotFramework-Composer/pull/2194)) ([@tdurnford](https://github.com/tdurnford)) +- fix: load schema files when loading bot project ([#2170](https://github.com/microsoft/BotFramework-Composer/pull/2170)) ([@a-b-r-o-w-n](https://github.com/a-b-r-o-w-n)) +- fix: deployment script, decouple debugging and deployment settings ([#2153](https://github.com/microsoft/BotFramework-Composer/pull/2153)) ([@zidaneymar](https://github.com/zidaneymar)) +- fix: Double scroll bars in dialog's properties pane ([#2163](https://github.com/microsoft/BotFramework-Composer/pull/2163)) ([@alanlong9278](https://github.com/alanlong9278)) +- fix: lg template display wrong in visual & form editor ([#2191](https://github.com/microsoft/BotFramework-Composer/pull/2191)) ([@alanlong9278](https://github.com/alanlong9278)) +- fix: remove edit button in lu all up view ([#2146](https://github.com/microsoft/BotFramework-Composer/pull/2146)) ([@cwhitten](https://github.com/cwhitten)) +- a11y: Use header tag for trigger in visual editor ([#2128](https://github.com/microsoft/BotFramework-Composer/pull/2128)) ([@cwhitten](https://github.com/cwhitten)) +- fix: update lu format link ([#2107](https://github.com/microsoft/BotFramework-Composer/pull/2107)) ([@liweitian](https://github.com/liweitian)) +- fix: resolve known bugs in LU LSP. ([#2098](https://github.com/microsoft/BotFramework-Composer/pull/2098)) ([@cosmicshuai](https://github.com/cosmicshuai)) +- fix: no longer show duplicate lg error notifications ([#2100](https://github.com/microsoft/BotFramework-Composer/pull/2100)) ([@zhixzhan](https://github.com/zhixzhan)) +- fix: replace animated screenshots with a static screenshot ([#2045](https://github.com/microsoft/BotFramework-Composer/pull/2045)) ([@benbrown](https://github.com/benbrown)) +- fix: support copy actions across dialogs ([#2198](https://github.com/microsoft/BotFramework-Composer/pull/2198)) ([@yeze322](https://github.com/yeze322)) +- fix: Default ActivityProcessed to true (bool) ([#2189](https://github.com/microsoft/BotFramework-Composer/pull/2189)) ([@cwhitten](https://github.com/cwhitten)) +- a11y: add name for nodeMenu, arrow and endNode ([#2131](https://github.com/microsoft/BotFramework-Composer/pull/2131)) ([@cwhitten](https://github.com/cwhitten)) +- a11y: add role, name, posinset for paste in edgeMenu ([#2126](https://github.com/microsoft/BotFramework-Composer/pull/2126)) ([@alanlong9278](https://github.com/alanlong9278)) +- fix: fix 'sort()' function of steps and cases in visual editor ([#2166](https://github.com/microsoft/BotFramework-Composer/pull/2166)) ([@yeze322](https://github.com/yeze322)) + +#### Changed + +- style: fix hover state nodes ui ([#2065](https://github.com/microsoft/BotFramework-Composer/pull/2065)) ([@alanlong9278](https://github.com/alanlong9278)) + +#### Other + +- ci: add a11y pr title prefix for accessibility prs ([#2171](https://github.com/microsoft/BotFramework-Composer/pull/2171)) ([@a-b-r-o-w-n](https://github.com/a-b-r-o-w-n)) +- docs: update docs to fix LU file format link ([#2071](https://github.com/microsoft/BotFramework-Composer/pull/2071)) ([@vishwacsena](https://github.com/vishwacsena)) +- chore: merge stable release into master ([@a-b-r-o-w-n](https://github.com/a-b-r-o-w-n)) +- docs: clarify setup ([#2145](https://github.com/microsoft/BotFramework-Composer/pull/2145)) ([@DaraOladapo](https://github.com/DaraOladapo)) +- docs: update dotnet requirement in setup docs ([#2160](https://github.com/microsoft/BotFramework-Composer/pull/2160)) ([@vkacherov](https://github.com/vkacherov)) +- samples: Update HttpRequest sample ([#2161](https://github.com/microsoft/BotFramework-Composer/pull/2161)) ([@luhan2017](https://github.com/luhan2017)) + ### 02-21-2020 #### Added diff --git a/scripts/generate-changelog.js b/scripts/generate-changelog.js index 90ab14515f..aed65cec88 100644 --- a/scripts/generate-changelog.js +++ b/scripts/generate-changelog.js @@ -5,24 +5,26 @@ const PULL_URL = "https://github.com/microsoft/BotFramework-Composer/pull"; const AUTHORS = { "Andy Brown": "a-b-r-o-w-n", + "Ben Brown": "benbrown", "Chris Whitten": "cwhitten", - "Kamran Iqbal": "Kaiqb", "Dong Lei": "boydc2014", + "Hongyang Du (hond)": "Danieladu", + "Kamran Iqbal": "Kaiqb", "Long Alan": "alanlong9278", + "Lu Han": "luhan2017", + "Pooja Nagpal": "p-nagpal", + "Qi Kang": "zidaneymar", + "Shuai Wang": "cosmicshuai", "TJ Durnford": "tdurnford", - leileizhang: "lei9444", - zeye: "yeze322", + "Vishwac Sena Kannan": "vishwacsena", + "Weitian Li": "liweitian", "Zhixiang Zhan": "zhixzhan", - zhixzhan: "zhixzhan", - "Shuai Wang": "cosmicshuai", + leileizhang: "lei9444", liweitian: "liweitian", - "Ben Brown": "benbrown", - "Pooja Nagpal": "p-nagpal", - xieofxie: "xieofxie", - "Lu Han": "luhan2017", VanyLaw: "VanyLaw", - "Hongyang Du (hond)": "Danieladu", - "Weitian Li": "liweitian" + xieofxie: "xieofxie", + zeye: "yeze322", + zhixzhan: "zhixzhan" }; const getLog = () => @@ -37,7 +39,7 @@ const getDate = () => const SECTIONS = { Added: ["feat"], - Fixed: ["fix"], + Fixed: ["fix", "a11y"], Changed: ["refactor", "style"], Other: [] }; From 256fffbc23de75857953f956444f48a92c6d666e Mon Sep 17 00:00:00 2001 From: Chris Whitten <christopher.whitten@microsoft.com> Date: Tue, 10 Mar 2020 09:12:56 -0700 Subject: [PATCH 39/40] Switch to actions/checkout@v2 (#2221) --- .github/workflows/main.yml | 6 +++--- .github/workflows/validate-pr.yml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e2f664b4f2..2fdb51fd8e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -18,7 +18,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v1 + uses: actions/checkout@v2 - name: Set Node Version uses: actions/setup-node@v1 with: @@ -56,7 +56,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v1 + uses: actions/checkout@v2 - name: Set Dotnet Version uses: actions/setup-dotnet@v1 with: @@ -75,7 +75,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v1 + uses: actions/checkout@v2 - name: docker-compose build run: docker-compose build - name: Health check diff --git a/.github/workflows/validate-pr.yml b/.github/workflows/validate-pr.yml index b8fbbe8fc3..3d6f84f147 100644 --- a/.github/workflows/validate-pr.yml +++ b/.github/workflows/validate-pr.yml @@ -10,7 +10,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v1 + uses: actions/checkout@v2 - name: Cache Node Modules uses: actions/cache@preview with: From d5f1776ab5a1b14bae9ecda6f78556760887f11b Mon Sep 17 00:00:00 2001 From: liweitian <liweitian93@outlook.com> Date: Wed, 11 Mar 2020 03:19:04 +0800 Subject: [PATCH 40/40] fix: regEx trigger creation bug (#2214) Co-authored-by: Chris Whitten <christopher.whitten@microsoft.com> --- .../ProjectTree/TriggerCreationModal.tsx | 30 ++++++++++--------- .../packages/client/src/utils/dialogUtil.ts | 29 ++++++++++++++---- 2 files changed, 39 insertions(+), 20 deletions(-) diff --git a/Composer/packages/client/src/components/ProjectTree/TriggerCreationModal.tsx b/Composer/packages/client/src/components/ProjectTree/TriggerCreationModal.tsx index a9e70383b3..3cf5124191 100644 --- a/Composer/packages/client/src/components/ProjectTree/TriggerCreationModal.tsx +++ b/Composer/packages/client/src/components/ProjectTree/TriggerCreationModal.tsx @@ -95,26 +95,25 @@ interface TriggerCreationModalProps { onSubmit: (dialog: DialogInfo, luFilePayload?: LuFilePayload) => void; } -const initialFormData: TriggerFormData = { - errors: {}, - $type: intentTypeKey, - specifiedType: '', - intent: '', - triggerPhrases: '', - regexEx: '', -}; - -const triggerTypeOptions: IDropdownOption[] = getTriggerTypes(); - export const TriggerCreationModal: React.FC<TriggerCreationModalProps> = props => { const { isOpen, onDismiss, onSubmit, dialogId } = props; - const [formData, setFormData] = useState(initialFormData); const { state } = useContext(StoreContext); const { dialogs, luFiles } = state; const luFile = luFiles.find(lu => lu.id === dialogId); const dialogFile = dialogs.find(dialog => dialog.id === dialogId); const isRegEx = get(dialogFile, 'content.recognizer.$type', '') === regexRecognizerKey; const regexIntents = get(dialogFile, 'content.recognizer.intents', []); + const isNone = !get(dialogFile, 'content.recognizer'); + const initialFormData: TriggerFormData = { + errors: {}, + $type: isNone ? '' : intentTypeKey, + specifiedType: '', + intent: '', + triggerPhrases: '', + regexEx: '', + }; + const [formData, setFormData] = useState(initialFormData); + const onClickSubmitButton = e => { e.preventDefault(); const errors = validateForm(formData, isRegEx, regexIntents); @@ -169,7 +168,10 @@ export const TriggerCreationModal: React.FC<TriggerCreationModalProps> = props = const eventTypes: IDropdownOption[] = getEventTypes(); const activityTypes: IDropdownOption[] = getActivityTypes(); const messageTypes: IDropdownOption[] = getMessageTypes(); - + let triggerTypeOptions: IDropdownOption[] = getTriggerTypes(); + if (isNone) { + triggerTypeOptions = triggerTypeOptions.filter(t => t.key !== intentTypeKey); + } const showIntentName = formData.$type === intentTypeKey; const showRegExDropDown = formData.$type === intentTypeKey && isRegEx; const showTriggerPhrase = formData.$type === intentTypeKey && !isRegEx; @@ -200,7 +202,7 @@ export const TriggerCreationModal: React.FC<TriggerCreationModalProps> = props = onChange={onSelectTriggerType} errorMessage={formData.errors.$type} data-testid={'triggerTypeDropDown'} - defaultSelectedKey={intentTypeKey} + defaultSelectedKey={formData.$type} /> {showEventDropDown && ( <Dropdown diff --git a/Composer/packages/client/src/utils/dialogUtil.ts b/Composer/packages/client/src/utils/dialogUtil.ts index ded58597d9..de8fb2216d 100644 --- a/Composer/packages/client/src/utils/dialogUtil.ts +++ b/Composer/packages/client/src/utils/dialogUtil.ts @@ -89,7 +89,7 @@ export function generateRegexExpression(intent: string, pattern: string) { return { intent, pattern }; } -export function createNewTrigger(dialog: DialogInfo, data: TriggerFormData): DialogInfo { +export function createTrigger(dialog: DialogInfo, data: TriggerFormData): DialogInfo { const dialogCopy = cloneDeep(dialog); const trigger = generateNewTrigger(data); insert(dialogCopy.content, 'triggers', undefined, trigger); @@ -115,11 +115,23 @@ export function updateRegExIntent(dialog: DialogInfo, intent: string, pattern: s return dialogCopy; } +//it is possible that we cannot find a RegEx. Because it will clear all regEx when we +//switch to another recognizer type +export function deleteRegExIntent(dialog: DialogInfo, intent: string): DialogInfo { + const dialogCopy = cloneDeep(dialog); + const regexIntents = get(dialogCopy, 'content.recognizer.intents', []); + const index = regexIntents.findIndex(ri => ri.intent === intent); + if (index > -1) { + regexIntents.splice(index, 1); + } + return dialogCopy; +} + export function generateNewDialog(dialogs: DialogInfo[], dialogId: string, data: TriggerFormData): DialogInfo { //add new trigger const dialog = dialogs.find(dialog => dialog.id === dialogId); if (!dialog) throw new Error(`dialog ${dialogId} does not exist`); - let updatedDialog = createNewTrigger(dialog, data); + let updatedDialog = createTrigger(dialog, data); //add regex expression if (data.regexEx) { @@ -137,11 +149,16 @@ export function createFocusedPath(selected: number, focused: number) { } export function deleteTrigger(dialogs: DialogInfo[], dialogId: string, index: number) { - const dialogCopy = getDialog(dialogs, dialogId); + let dialogCopy = getDialog(dialogs, dialogId); if (!dialogCopy) return null; - const content = dialogCopy.content; - content.triggers.splice(index, 1); - return content; + const isRegEx = get(dialogCopy, 'content.recognizer.$type', '') === regexRecognizerKey; + if (isRegEx) { + const regExIntent = get(dialogCopy, `content.triggers[${index}].intent`, ''); + dialogCopy = deleteRegExIntent(dialogCopy, regExIntent); + } + const triggers = get(dialogCopy, 'content.triggers'); + triggers.splice(index, 1); + return dialogCopy.content; } export function getTriggerTypes(): IDropdownOption[] {

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 sDiL22< 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%0Atz5CkZ=^cNm-b@xx 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 z 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