Skip to content
This repository was archived by the owner on Jul 9, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -96,13 +96,13 @@ const CreationFlow: React.FC<CreationFlowProps> = () => {
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] });
}
};

Expand Down
6 changes: 6 additions & 0 deletions Composer/packages/client/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
4 changes: 2 additions & 2 deletions Composer/packages/client/src/pages/design/DesignPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -654,7 +654,7 @@ const DesignPage: React.FC<RouteComponentProps<{ dialogId: string; projectId: st
setImportQnAModalVisibility(false);
};

const handleCreateQnA = async (urls: string[], knowledgeBaseName: string) => {
const handleCreateQnA = async (urls: string[]) => {
cancelImportQnAModal();
const lgTemplateId1 = generateDesignerId();
const lgTemplateId2 = generateDesignerId();
Expand Down Expand Up @@ -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] });
}
}
};
Expand Down
77 changes: 77 additions & 0 deletions Composer/packages/client/src/pages/qna/FailedImportQnAModal.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Dialog
dialogContentProps={{
type: DialogType.normal,
title: title,
// subText: subTitle,
}}
hidden={false}
minWidth={500}
modalProps={{
isBlocking: true,
styles: {
main: { maxWidth: 450 },
},
}}
>
{subtitle && <div css={builtInStyles[style]}>{subtitle}</div>}

<DialogFooter>
<PrimaryButton text={confirmText} onClick={onConfirm} />
</DialogFooter>
</Dialog>
);
};
48 changes: 24 additions & 24 deletions Composer/packages/client/src/pages/qna/ImportQnAFromUrlModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ const dialogWindow = css`
display: flex;
flex-direction: column;
width: 400px;
min-height: 300px;
min-height: 200px;
`;

const textField = {
Expand Down Expand Up @@ -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<ImportQnAFromUrlModalProps> = (props) => {
Expand All @@ -94,27 +93,34 @@ export const ImportQnAFromUrlModal: React.FC<ImportQnAFromUrlModalProps> = (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) => {
Expand All @@ -129,13 +135,14 @@ export const ImportQnAFromUrlModal: React.FC<ImportQnAFromUrlModalProps> = (prop
const urls = cloneDeep(formData.urls);
urls.splice(index, 1);
updateField('urls', urls);
setUrlErrors(validateUrls(urls));
};

return (
<Dialog
dialogContentProps={{
type: DialogType.normal,
title: formatMessage('Create New Knowledge Base'),
title: formatMessage('Populate your KB.'),
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.'
),
Expand All @@ -150,20 +157,13 @@ export const ImportQnAFromUrlModal: React.FC<ImportQnAFromUrlModalProps> = (prop
>
<div css={dialogWindow}>
<Stack>
<TextField
data-testid="knowledgeBaseNameTextField"
label={formatMessage('knowledgebase name')}
styles={textField}
value={formData.knowledgeBaseName}
onChange={(e, knowledgeBaseName) => updateField('knowledgeBaseName', knowledgeBaseName)}
/>
{formData.urls.map((l, index) => {
return (
<div key={index} css={urlContainer}>
<TextField
data-testid="knowledgeLocationTextField"
errorMessage={urlErrors[index]}
label={index === 0 ? formatMessage('knowledge location (URL name)') : ''}
label={index === 0 ? formatMessage('URL') : ''}
placeholder={'http://'}
styles={textField}
value={l}
Expand All @@ -181,7 +181,7 @@ export const ImportQnAFromUrlModal: React.FC<ImportQnAFromUrlModalProps> = (prop
);
})}
<ActionButton css={actionButton} data-testid={'addQnAImportUrl'} iconProps={{ iconName: 'Add' }}>
{<Link onClick={addNewUrl}>{formatMessage('Add')}</Link>}
{<Link onClick={addNewUrl}>{formatMessage('Add URL')}</Link>}
</ActionButton>
{!isQnAFileselected && (
<div css={warning}> {formatMessage('please select a specific qna file to import QnA')}</div>
Expand All @@ -198,7 +198,7 @@ export const ImportQnAFromUrlModal: React.FC<ImportQnAFromUrlModalProps> = (prop
if (hasErrors) {
return;
}
onSubmit([], formData.knowledgeBaseName);
onSubmit([]);
}}
/>
)}
Expand All @@ -211,7 +211,7 @@ export const ImportQnAFromUrlModal: React.FC<ImportQnAFromUrlModalProps> = (prop
if (hasErrors) {
return;
}
onSubmit(formData.urls, formData.knowledgeBaseName);
onSubmit(formData.urls);
}}
/>
</DialogFooter>
Expand Down
19 changes: 16 additions & 3 deletions Composer/packages/client/src/pages/qna/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'));

Expand Down Expand Up @@ -132,14 +133,17 @@ const QnAPage: React.FC<QnAPageProps> = (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 (
<Page
data-testid="QnAPage"
Expand All @@ -153,9 +157,18 @@ const QnAPage: React.FC<QnAPageProps> = (props) => {
<Suspense fallback={<LoadingSpinner />}>
<Router component={Fragment} primary={false}>
<CodeEditor dialogId={dialogId} path="/edit" />
{qnaAllUpViewStatus === QnAAllUpViewStatus.Success && <TableView dialogId={dialogId} path="/" />}
{qnaAllUpViewStatus !== QnAAllUpViewStatus.Loading && <TableView dialogId={dialogId} path="/" />}
</Router>
{qnaAllUpViewStatus === QnAAllUpViewStatus.Loading && <LoadingSpinner message={'Extracting QnA pairs'} />}
{qnaAllUpViewStatus === QnAAllUpViewStatus.Failed && (
<FailedImportQnAModal
setting={{
title: 'Bad Argument',
subtitle: 'Failed to import QnA resource. Your url is invalid.',
}}
onConfirm={onConfirm}
/>
)}
{importQnAFromUrlModalVisiability && (
<ImportQnAFromUrlModal dialogId={dialogId} isCreatingBot={false} onDismiss={onDismiss} onSubmit={onSubmit} />
)}
Expand Down
32 changes: 12 additions & 20 deletions Composer/packages/client/src/recoilModel/dispatchers/qna.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
Expand All @@ -105,26 +96,27 @@ 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);
}
);

return {
createQnAFile,
updateQnAFile,
importQnAFromUrl,
updateQnAAllUpViewStatus,
};
};
1 change: 1 addition & 0 deletions Composer/packages/client/src/recoilModel/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,4 +110,5 @@ export type BoilerplateVersion = {
export enum QnAAllUpViewStatus {
Loading,
Success,
Failed,
}