From 805de1519f54a53976e6d70f52648ab27cb309c3 Mon Sep 17 00:00:00 2001 From: liweitian Date: Mon, 10 Aug 2020 18:47:56 +0800 Subject: [PATCH 1/2] fix bug --- .../components/CreationFlow/CreationFlow.tsx | 5 ++++- .../src/recoilModel/dispatchers/project.ts | 20 +++++++++++-------- .../projects/QnASample/qnasample.dialog | 3 +-- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/Composer/packages/client/src/components/CreationFlow/CreationFlow.tsx b/Composer/packages/client/src/components/CreationFlow/CreationFlow.tsx index bd59cff27f..781d4e05ae 100644 --- a/Composer/packages/client/src/components/CreationFlow/CreationFlow.tsx +++ b/Composer/packages/client/src/components/CreationFlow/CreationFlow.tsx @@ -20,6 +20,7 @@ import { } from '../../recoilModel'; import Home from '../../pages/home/Home'; import ImportQnAFromUrlModal from '../../pages/qna/ImportQnAFromUrlModal'; +import { QnABotTemplateId } from '../../constants'; import { CreateOptions } from './CreateOptions'; import { OpenProject } from './OpenProject'; @@ -96,7 +97,9 @@ const CreationFlow: React.FC = () => { }; const handleCreateQnA = async (urls: string[], knowledgeBaseName: string) => { - await handleSubmit(formData, 'QnASample'); + saveTemplateId(QnABotTemplateId); + setCreationFlowStatus(CreationFlowStatus.CLOSE); + await handleCreateNew(formData, QnABotTemplateId); for (let i = 0; i < urls.length; i++) { if (!urls[i]) continue; await importQnAFromUrl({ diff --git a/Composer/packages/client/src/recoilModel/dispatchers/project.ts b/Composer/packages/client/src/recoilModel/dispatchers/project.ts index 8e5f170580..bcd4ace5cd 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/project.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/project.ts @@ -23,6 +23,7 @@ import filePersistence from '../persistence/FilePersistence'; import { navigateTo } from '../../utils/navigation'; import languageStorage from '../../utils/languageStorage'; import { designPageLocationState } from '../atoms/botState'; +import { QnABotTemplateId } from '../../constants'; import { skillManifestsState, @@ -133,7 +134,7 @@ const initQnaFilesStatus = (projectId: string, qnaFiles: QnAFile[], dialogs: Dia return updateQnaFilesStatus(projectId, qnaFiles); }; export const projectDispatcher = () => { - const initBotState = async (callbackHelpers: CallbackInterface, data: any, jumpToMain: boolean) => { + const initBotState = async (callbackHelpers: CallbackInterface, data: any, jump: boolean, templateId: string) => { const { snapshot, gotoSnapshot } = callbackHelpers; const curLocation = await snapshot.getPromise(locationState); const { files, botName, botEnvironment, location, schemas, settings, id: projectId, diagnostics, skills } = data; @@ -188,9 +189,12 @@ export const projectDispatcher = () => { set(settingsState, mergedSettings); }); gotoSnapshot(newSnapshot); - if (jumpToMain && projectId) { - const mainUrl = `/bot/${projectId}/dialogs/${mainDialog}`; - navigateTo(mainUrl); + if (jump && projectId) { + let url = `/bot/${projectId}/dialogs/${mainDialog}`; + if (templateId === QnABotTemplateId) { + url = `/bot/${projectId}/qna/${mainDialog}`; + } + navigateTo(url); } } catch (err) { callbackHelpers.set(botOpeningState, false); @@ -224,7 +228,7 @@ export const projectDispatcher = () => { try { await setBotOpeningStatus(callbackHelpers); const response = await httpClient.put(`/projects/open`, { path, storageId }); - await initBotState(callbackHelpers, response.data, true); + await initBotState(callbackHelpers, response.data, true, ''); return response.data.id; } catch (ex) { @@ -237,7 +241,7 @@ export const projectDispatcher = () => { const fetchProjectById = useRecoilCallback((callbackHelpers: CallbackInterface) => async (projectId: string) => { try { const response = await httpClient.get(`/projects/${projectId}`); - await initBotState(callbackHelpers, response.data, false); + await initBotState(callbackHelpers, response.data, false, ''); } catch (ex) { handleProjectFailure(callbackHelpers, ex); navigateTo('/home'); @@ -266,7 +270,7 @@ export const projectDispatcher = () => { if (settingStorage.get(projectId)) { settingStorage.remove(projectId); } - await initBotState(callbackHelpers, response.data, true); + await initBotState(callbackHelpers, response.data, true, templateId); return projectId; } catch (ex) { handleProjectFailure(callbackHelpers, ex); @@ -310,7 +314,7 @@ export const projectDispatcher = () => { description, location, }); - await initBotState(callbackHelpers, response.data, true); + await initBotState(callbackHelpers, response.data, true, ''); return response.data.id; } catch (ex) { handleProjectFailure(callbackHelpers, ex); diff --git a/Composer/plugins/samples/assets/projects/QnASample/qnasample.dialog b/Composer/plugins/samples/assets/projects/QnASample/qnasample.dialog index 3b9e27d9b2..14acc09e7e 100644 --- a/Composer/plugins/samples/assets/projects/QnASample/qnasample.dialog +++ b/Composer/plugins/samples/assets/projects/QnASample/qnasample.dialog @@ -98,6 +98,5 @@ ], "$schema": "https://raw.githubusercontent.com/microsoft/BotFramework-Composer/stable/Composer/packages/server/schemas/sdk.schema", "generator": "qnasample.lg", - "recognizer": "qnasample.lu.qna", - "id": "qnasample" + "recognizer": "qnasample.lu.qna" } From 287df5c16a8da99741365e1206efe7f97b34f1ce Mon Sep 17 00:00:00 2001 From: liweitian Date: Wed, 12 Aug 2020 18:31:40 +0800 Subject: [PATCH 2/2] update import QNA UI --- .../components/CreationFlow/CreationFlow.tsx | 4 +- Composer/packages/client/src/constants.ts | 6 ++ .../client/src/pages/design/DesignPage.tsx | 4 +- .../src/pages/qna/FailedImportQnAModal.tsx | 77 +++++++++++++++++++ .../src/pages/qna/ImportQnAFromUrlModal.tsx | 48 ++++++------ .../packages/client/src/pages/qna/index.tsx | 19 ++++- .../client/src/recoilModel/dispatchers/qna.ts | 32 +++----- .../packages/client/src/recoilModel/types.ts | 1 + 8 files changed, 140 insertions(+), 51 deletions(-) create mode 100644 Composer/packages/client/src/pages/qna/FailedImportQnAModal.tsx diff --git a/Composer/packages/client/src/components/CreationFlow/CreationFlow.tsx b/Composer/packages/client/src/components/CreationFlow/CreationFlow.tsx index cb634a1deb..24aca7e567 100644 --- a/Composer/packages/client/src/components/CreationFlow/CreationFlow.tsx +++ b/Composer/packages/client/src/components/CreationFlow/CreationFlow.tsx @@ -96,13 +96,13 @@ const CreationFlow: React.FC = () => { saveProjectAs(projectId, formData.name, formData.description, formData.location); }; - const handleCreateQnA = async (urls: string[], knowledgeBaseName: string) => { + const handleCreateQnA = async (urls: string[]) => { saveTemplateId(QnABotTemplateId); setCreationFlowStatus(CreationFlowStatus.CLOSE); await handleCreateNew(formData, QnABotTemplateId); for (let i = 0; i < urls.length; i++) { if (!urls[i]) continue; - await importQnAFromUrl({ id: `${formData.name.toLocaleLowerCase()}.${locale}`, knowledgeBaseName, url: urls[i] }); + await importQnAFromUrl({ id: `${formData.name.toLocaleLowerCase()}.${locale}`, url: urls[i] }); } }; diff --git a/Composer/packages/client/src/constants.ts b/Composer/packages/client/src/constants.ts index fb238f4fd0..120c299bfe 100644 --- a/Composer/packages/client/src/constants.ts +++ b/Composer/packages/client/src/constants.ts @@ -110,6 +110,12 @@ export const DialogCreationCopy = { title: formatMessage('Set destination folder'), subText: formatMessage('Choose a location for your new bot project.'), }, + IMPORT_QNA: { + title: formatMessage('Create New Knowledge Base'), + subText: formatMessage( + 'Extract question-and-answer pairs from an online FAQ, product manuals, or other files. Supported formats are .tsv, .pdf, .doc, .docx, .xlsx, containing questions and answers in sequence. Learn more about knowledge base sources. Skip this step to add questions and answers manually after creation. The number of sources and file size you can add depends on the QnA service SKU you choose. Learn more about QnA Maker SKUs.' + ), + }, }; export const DialogDeleting = { diff --git a/Composer/packages/client/src/pages/design/DesignPage.tsx b/Composer/packages/client/src/pages/design/DesignPage.tsx index 746b6762fc..034da1767c 100644 --- a/Composer/packages/client/src/pages/design/DesignPage.tsx +++ b/Composer/packages/client/src/pages/design/DesignPage.tsx @@ -654,7 +654,7 @@ const DesignPage: React.FC { + const handleCreateQnA = async (urls: string[]) => { cancelImportQnAModal(); const lgTemplateId1 = generateDesignerId(); const lgTemplateId2 = generateDesignerId(); @@ -693,7 +693,7 @@ SuggestedActions = ${foreach(turn.recognized.answers[0].context.prompts, x, x.di onTriggerCreationSubmit(newDialog, undefined, lgFilePayload); for (let i = 0; i < urls.length; i++) { if (!urls[i]) continue; - await importQnAFromUrl({ id: `${dialogId}.${locale}`, knowledgeBaseName, url: urls[i] }); + await importQnAFromUrl({ id: `${dialogId}.${locale}`, url: urls[i] }); } } }; diff --git a/Composer/packages/client/src/pages/qna/FailedImportQnAModal.tsx b/Composer/packages/client/src/pages/qna/FailedImportQnAModal.tsx new file mode 100644 index 0000000000..ee6fdebfa3 --- /dev/null +++ b/Composer/packages/client/src/pages/qna/FailedImportQnAModal.tsx @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** @jsx jsx */ +import { jsx, css } from '@emotion/core'; +import { Dialog, DialogType, DialogFooter } from 'office-ui-fabric-react/lib/Dialog'; +import { PrimaryButton } from 'office-ui-fabric-react/lib/Button'; +import formatMessage from 'format-message'; + +// -------------------- Styles -------------------- // + +const normalStyle = css` + padding: 15px; + margin-bottom: 20px; + white-space: pre-line; +`; + +const consoleStyle = css` + background: #000; + max-height: 90px; + overflow-y: auto; + font-size: 16px; + line-height: 23px; + color: #fff; + padding: 10px 15px; + margin-bottom: 20px; + white-space: pre-line; +`; + +const dialogStyle = { + normal: 'NORMAL', + console: 'CONSOLE', +}; + +export const builtInStyles = { + [dialogStyle.normal]: normalStyle, + [dialogStyle.console]: consoleStyle, +}; + +// -------------------- AlertDialog -------------------- // +type Props = { + setting: { + title: string; + subtitle?: string; + confirmText?: string; + style?: string; + }; + onConfirm: () => void; +}; +export const FailedImportQnAModal = (props: Props) => { + const { setting, onConfirm } = props; + const { title, subtitle = '', confirmText = formatMessage('Ok'), style = dialogStyle.normal } = setting; + + return ( + + ); +}; diff --git a/Composer/packages/client/src/pages/qna/ImportQnAFromUrlModal.tsx b/Composer/packages/client/src/pages/qna/ImportQnAFromUrlModal.tsx index 05739489eb..97bc85702c 100644 --- a/Composer/packages/client/src/pages/qna/ImportQnAFromUrlModal.tsx +++ b/Composer/packages/client/src/pages/qna/ImportQnAFromUrlModal.tsx @@ -39,7 +39,7 @@ const dialogWindow = css` display: flex; flex-direction: column; width: 400px; - min-height: 300px; + min-height: 200px; `; const textField = { @@ -78,12 +78,11 @@ interface ImportQnAFromUrlModalProps isCreatingBot: boolean; subscriptionKey?: string; onDismiss: () => void; - onSubmit: (urls: string[], knowledgeBaseName: string) => void; + onSubmit: (urls: string[]) => void; } interface ImportQnAFromUrlModalFormData { urls: string[]; - knowledgeBaseName: string; } export const ImportQnAFromUrlModal: React.FC = (props) => { @@ -94,27 +93,34 @@ export const ImportQnAFromUrlModal: React.FC = (prop required: true, defaultValue: [''], }, - knowledgeBaseName: { - defaultValue: '', - }, }; const { formData, updateField, hasErrors } = useForm(formConfig); const isQnAFileselected = !(dialogId === 'all'); const disabled = !isQnAFileselected || hasErrors || urlErrors.some((e) => !!e) || formData.urls.some((url) => !url); const validateUrls = (urls: string[]) => { - return urls.map((url) => { - if (!url.startsWith('http://') && !url.startsWith('https://')) { - return formatMessage('A valid url should start with http:// or https://'); - } else { - return ''; + const res = Array(urls.length).fill(''); + for (let i = 0; i < urls.length; i++) { + if (!urls[i].startsWith('http://') && !urls[i].startsWith('https://')) { + res[i] = formatMessage('A valid url should start with http:// or https://'); + } + } + + for (let i = 0; i < urls.length; i++) { + for (let j = 0; j < urls.length; j++) { + if (urls[i] === urls[j] && i !== j) { + res[i] = formatMessage('This url is duplicated'); + res[j] = formatMessage('This url is duplicated'); + } } - }); + } + return res; }; const addNewUrl = () => { const urls = cloneDeep(formData.urls); urls.splice(urls.length, 0, ''); updateField('urls', urls); + setUrlErrors(validateUrls(urls)); }; const updateUrl = (index: number, url: string | undefined) => { @@ -129,13 +135,14 @@ export const ImportQnAFromUrlModal: React.FC = (prop const urls = cloneDeep(formData.urls); urls.splice(index, 1); updateField('urls', urls); + setUrlErrors(validateUrls(urls)); }; return ( = (prop >
- updateField('knowledgeBaseName', knowledgeBaseName)} - /> {formData.urls.map((l, index) => { return (
= (prop ); })} - {{formatMessage('Add')}} + {{formatMessage('Add URL')}} {!isQnAFileselected && (
{formatMessage('please select a specific qna file to import QnA')}
@@ -198,7 +198,7 @@ export const ImportQnAFromUrlModal: React.FC = (prop if (hasErrors) { return; } - onSubmit([], formData.knowledgeBaseName); + onSubmit([]); }} /> )} @@ -211,7 +211,7 @@ export const ImportQnAFromUrlModal: React.FC = (prop if (hasErrors) { return; } - onSubmit(formData.urls, formData.knowledgeBaseName); + onSubmit(formData.urls); }} /> diff --git a/Composer/packages/client/src/pages/qna/index.tsx b/Composer/packages/client/src/pages/qna/index.tsx index 1431c72701..14b7f29e18 100644 --- a/Composer/packages/client/src/pages/qna/index.tsx +++ b/Composer/packages/client/src/pages/qna/index.tsx @@ -21,6 +21,7 @@ import { QnAAllUpViewStatus } from '../../recoilModel/types'; import TableView from './table-view'; import { ImportQnAFromUrlModal } from './ImportQnAFromUrlModal'; +import { FailedImportQnAModal } from './FailedImportQnAModal'; const CodeEditor = React.lazy(() => import('./code-editor')); @@ -132,14 +133,17 @@ const QnAPage: React.FC = (props) => { setImportQnAFromUrlModalVisiability(false); }; - const onSubmit = async (urls: string[], knowledgeBaseName: string) => { + const onSubmit = async (urls: string[]) => { onDismiss(); for (let i = 0; i < urls.length; i++) { if (!urls[i]) continue; - await actions.importQnAFromUrl({ id: `${dialogId}.${locale}`, knowledgeBaseName, url: urls[i] }); + await actions.importQnAFromUrl({ id: `${dialogId}.${locale}`, url: urls[i] }); } }; + const onConfirm = () => { + actions.updateQnAAllUpViewStatus({ status: QnAAllUpViewStatus.Success }); + }; return ( = (props) => { }> - {qnaAllUpViewStatus === QnAAllUpViewStatus.Success && } + {qnaAllUpViewStatus !== QnAAllUpViewStatus.Loading && } {qnaAllUpViewStatus === QnAAllUpViewStatus.Loading && } + {qnaAllUpViewStatus === QnAAllUpViewStatus.Failed && ( + + )} {importQnAFromUrlModalVisiability && ( )} diff --git a/Composer/packages/client/src/recoilModel/dispatchers/qna.ts b/Composer/packages/client/src/recoilModel/dispatchers/qna.ts index 514a6389f6..584e916fe9 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/qna.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/qna.ts @@ -6,7 +6,6 @@ import { useRecoilCallback, CallbackInterface } from 'recoil'; import qnaWorker from '../parsers/qnaWorker'; import { qnaFilesState, qnaAllUpViewStatusState, projectIdState, localeState, settingsState } from '../atoms/botState'; -import { applicationErrorState } from '../atoms'; import { QnAAllUpViewStatus } from '../types'; import qnaFileStatusStorage from '../../utils/qnaFileStatusStorage'; import { getBaseName } from '../../utils/fileUtil'; @@ -88,15 +87,7 @@ export const qnaDispatcher = () => { } ); const importQnAFromUrl = useRecoilCallback( - (callbackHelpers: CallbackInterface) => async ({ - id, - knowledgeBaseName, - url, - }: { - id: string; - knowledgeBaseName: string; - url: string; - }) => { + (callbackHelpers: CallbackInterface) => async ({ id, url }: { id: string; url: string }) => { const { set, snapshot } = callbackHelpers; const qnaFiles = await snapshot.getPromise(qnaFilesState); const qnaFile = qnaFiles.find((f) => f.id === id); @@ -105,20 +96,20 @@ export const qnaDispatcher = () => { const response = await httpClient.get(`/qnaContent`, { params: { url }, }); - if (!knowledgeBaseName) { - knowledgeBaseName = 'default knowledge base'; - } - const appendedContent = `> knowledge base name: ${knowledgeBaseName}\n` + response.data; - const content = qnaFile ? qnaFile.content + '\n' + appendedContent : appendedContent; + const content = qnaFile ? qnaFile.content + '\n' + response.data : response.data; await updateQnAFileState(callbackHelpers, { id, content }); + set(qnaAllUpViewStatusState, QnAAllUpViewStatus.Success); } catch (err) { - set(applicationErrorState, { - message: err.message, - summary: `Failed to import QnA`, - }); + set(qnaAllUpViewStatusState, QnAAllUpViewStatus.Failed); } - set(qnaAllUpViewStatusState, QnAAllUpViewStatus.Success); + } + ); + + const updateQnAAllUpViewStatus = useRecoilCallback( + (callbackHelpers: CallbackInterface) => async ({ status }: { status: QnAAllUpViewStatus }) => { + const { set } = callbackHelpers; + set(qnaAllUpViewStatusState, status); } ); @@ -126,5 +117,6 @@ export const qnaDispatcher = () => { createQnAFile, updateQnAFile, importQnAFromUrl, + updateQnAAllUpViewStatus, }; }; diff --git a/Composer/packages/client/src/recoilModel/types.ts b/Composer/packages/client/src/recoilModel/types.ts index c7ada34dfd..c9f8d189f8 100644 --- a/Composer/packages/client/src/recoilModel/types.ts +++ b/Composer/packages/client/src/recoilModel/types.ts @@ -110,4 +110,5 @@ export type BoilerplateVersion = { export enum QnAAllUpViewStatus { Loading, Success, + Failed, }