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, }