+
{defaultRender({
...props,
+ isCollapsable: false,
onRenderColumnHeaderTooltip: (tooltipHostProps) => ,
})}
-
- {dialogIdRef.current !== 'all' && (
-
-
{
- onCreateNewTemplate();
- actions.setMessage('item added');
- }}
- >
- {formatMessage('Add QnA Pair')}
-
-
- )}
-
);
},
- [dialogIdRef, showQnAPairDetails]
+ [dialogId]
);
- const onRenderRow = (props) => {
- if (props) {
- return
;
- }
- return null;
- };
-
- const onCreateNewTemplate = () => {
- const newQnAPair = generateQnAPair();
- const content = get(fileRef.current, 'content', '');
- const newContent = insertSection(0, content, newQnAPair);
- actions.updateQnAFile({ id: `${dialogIdRef.current}.${localeRef.current}`, content: newContent, projectId });
- const newArray = [false, ...showQnAPairDetails];
- setShowQnAPairDetails(newArray);
- };
+ const onRenderRow = useCallback(
+ (props) => {
+ if (props) {
+ return (
+
+ );
+ }
+ return null;
+ },
+ [dialogId, expandedIndex]
+ );
- const getKeyCallback = useCallback((item) => item.uuid, []);
+ if (qnaFile?.empty) {
+ return (
+
+
+

+
{formatMessage('Create a knowledge base from scratch or import knowledge from a URL or PDF files')}
+
{
+ actions.createQnAFromUrlDialogBegin({ projectId, showFromScratch: true });
+ }}
+ />
+
+
+ );
+ }
return (
= (props) => {
onRenderRow={onRenderRow}
/>
+ {editQnAFile && (
+ {
+ setEditQnAFile(undefined);
+ }}
+ onSubmit={onSubmitEditKB}
+ >
+ )}
);
};
diff --git a/Composer/packages/client/src/pages/language-generation/LGPage.tsx b/Composer/packages/client/src/pages/language-generation/LGPage.tsx
index d786f33b01..66921b4237 100644
--- a/Composer/packages/client/src/pages/language-generation/LGPage.tsx
+++ b/Composer/packages/client/src/pages/language-generation/LGPage.tsx
@@ -5,12 +5,11 @@
import { jsx } from '@emotion/core';
import React, { Fragment, useMemo, useCallback, Suspense, useEffect } from 'react';
import formatMessage from 'format-message';
-import { Toggle } from 'office-ui-fabric-react/lib/Toggle';
+import { ActionButton } from 'office-ui-fabric-react/lib/Button';
import { RouteComponentProps, Router } from '@reach/router';
import { useRecoilValue } from 'recoil';
import { LoadingSpinner } from '../../components/LoadingSpinner';
-import { actionButton } from '../language-understanding/styles';
import { navigateTo } from '../../utils/navigation';
import { TestController } from '../../components/TestController/TestController';
import { INavTreeItem } from '../../components/NavTree';
@@ -74,12 +73,12 @@ const LGPage: React.FC
> = (props: RouteComponen
}, [dialogId, dialogs, projectId]);
const onToggleEditMode = useCallback(
- (_e, checked) => {
+ (_e) => {
let url = `/bot/${projectId}/language-generation/${dialogId}`;
- if (checked) url += `/edit`;
+ if (!edit) url += `/edit`;
navigateTo(url);
},
- [dialogId, projectId]
+ [dialogId, projectId, edit]
);
const toolbarItems = [
@@ -92,14 +91,9 @@ const LGPage: React.FC> = (props: RouteComponen
const onRenderHeaderContent = () => {
return (
-
+
+ {edit ? formatMessage('Hide code') : formatMessage('Show code')}
+
);
};
diff --git a/Composer/packages/client/src/pages/language-generation/table-view.tsx b/Composer/packages/client/src/pages/language-generation/table-view.tsx
index 11dccf3da1..cdead54f70 100644
--- a/Composer/packages/client/src/pages/language-generation/table-view.tsx
+++ b/Composer/packages/client/src/pages/language-generation/table-view.tsx
@@ -21,11 +21,11 @@ import { lgUtil } from '@bfc/indexers';
import { EditableField } from '../../components/EditableField';
import { navigateTo } from '../../utils/navigation';
-import { actionButton, formCell } from '../language-understanding/styles';
+import { actionButton, formCell, editableFieldContainer } from '../language-understanding/styles';
import {
dispatcherState,
- localeState,
lgFilesState,
+ localeState,
settingsState,
validateDialogSelectorFamily,
} from '../../recoilModel';
@@ -57,7 +57,7 @@ const TableView: React.FC = (props) => {
const activeDialog = dialogs.find(({ id }) => id === dialogId);
- const [focusedIndex, setFocusedIndex] = useState(0);
+ //const [focusedIndex, setFocusedIndex] = useState(0);
useEffect(() => {
if (!file || isEmpty(file)) return;
@@ -86,7 +86,7 @@ const TableView: React.FC = (props) => {
} as LgTemplate,
};
createLgTemplate(payload);
- setFocusedIndex(file.templates.length);
+ //setFocusedIndex(file.templates.length);
}
}, [file]);
@@ -99,7 +99,7 @@ const TableView: React.FC = (props) => {
projectId,
};
removeLgTemplate(payload);
- setFocusedIndex(file.templates.findIndex((item) => item.name === name));
+ //setFocusedIndex(file.templates.findIndex((item) => item.name === name));
}
},
[file]
@@ -116,7 +116,7 @@ const TableView: React.FC = (props) => {
projectId,
};
copyLgTemplate(payload);
- setFocusedIndex(file.templates.length);
+ //setFocusedIndex(file.templates.length);
}
},
[file]
@@ -209,6 +209,7 @@ const TableView: React.FC = (props) => {
= (props) => {
= (props) => {
= (props) => {
= (props) => {
columns={getTableColums()}
componentRef={listRef}
getKey={getKeyCallback}
- initialFocusedIndex={focusedIndex}
+ //initialFocusedIndex={focusedIndex}
items={templatesToRender}
// getKey={item => item.name}
layoutMode={DetailsListLayoutMode.justified}
diff --git a/Composer/packages/client/src/pages/language-understanding/LUPage.tsx b/Composer/packages/client/src/pages/language-understanding/LUPage.tsx
index 4cad612016..87ed81d6be 100644
--- a/Composer/packages/client/src/pages/language-understanding/LUPage.tsx
+++ b/Composer/packages/client/src/pages/language-understanding/LUPage.tsx
@@ -4,7 +4,7 @@
import { jsx } from '@emotion/core';
import React, { Fragment, useMemo, Suspense, useCallback, useEffect } from 'react';
import formatMessage from 'format-message';
-import { Toggle } from 'office-ui-fabric-react/lib/Toggle';
+import { ActionButton } from 'office-ui-fabric-react/lib/Button';
import { RouteComponentProps, Router } from '@reach/router';
import { useRecoilValue } from 'recoil';
@@ -16,7 +16,6 @@ import { Page } from '../../components/Page';
import { validateDialogSelectorFamily } from '../../recoilModel';
import TableView from './table-view';
-import { actionButton } from './styles';
const CodeEditor = React.lazy(() => import('./code-editor'));
const LUPage: React.FC {
+ (_e) => {
let url = `/bot/${projectId}/language-understanding/${dialogId}`;
- if (checked) url += `/edit`;
+ if (!edit) url += `/edit`;
navigateTo(url);
},
- [dialogId, projectId]
+ [dialogId, projectId, edit]
);
const toolbarItems = [
@@ -83,19 +82,13 @@ const LUPage: React.FC {
- if (!isRoot || edit) {
+ if (!isRoot) {
return (
-
+
+ {edit ? formatMessage('Hide code') : formatMessage('Show code')}
+
);
}
-
return null;
};
diff --git a/Composer/packages/client/src/pages/language-understanding/styles.ts b/Composer/packages/client/src/pages/language-understanding/styles.ts
index f28f91f98f..879c07e14c 100644
--- a/Composer/packages/client/src/pages/language-understanding/styles.ts
+++ b/Composer/packages/client/src/pages/language-understanding/styles.ts
@@ -105,3 +105,7 @@ export const diffEditorContent = css`
export const dropdown = {
dropdown: { width: '50%', maxWidth: 300, minWidth: 100 },
};
+
+export const editableFieldContainer = css`
+ outline: none;
+`;
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 bdbf22bdfb..9466ec6644 100644
--- a/Composer/packages/client/src/pages/language-understanding/table-view.tsx
+++ b/Composer/packages/client/src/pages/language-understanding/table-view.tsx
@@ -32,7 +32,7 @@ import {
validateDialogSelectorFamily,
} from '../../recoilModel';
-import { formCell, luPhraseCell, tableCell } from './styles';
+import { formCell, luPhraseCell, tableCell, editableFieldContainer } from './styles';
interface TableViewProps extends RouteComponentProps<{ dialogId: string; projectId: string }> {
dialogId: string;
projectId: string;
@@ -177,6 +177,7 @@ const TableView: React.FC = (props) => {
= (props) => {
= (props) => {
= (props) => {
{
- dialog.diagnostics.map((diagnostic) => {
+ dialog.diagnostics.forEach((diagnostic) => {
const location = `${dialog.id}.dialog`;
notifications.push(new DialogNotification(projectId, dialog.id, location, diagnostic));
});
});
getReferredLuFiles(luFiles, dialogs).forEach((lufile) => {
- lufile.diagnostics.map((diagnostic) => {
+ lufile.diagnostics.forEach((diagnostic) => {
const location = `${lufile.id}.lu`;
notifications.push(new LuNotification(projectId, lufile.id, location, diagnostic, lufile, dialogs));
});
});
lgFiles.forEach((lgFile) => {
- lgFile.diagnostics.map((diagnostic) => {
+ lgFile.diagnostics.forEach((diagnostic) => {
const location = `${lgFile.id}.lg`;
notifications.push(new LgNotification(projectId, lgFile.id, location, diagnostic, lgFile, dialogs));
});
});
qnaFiles.forEach((qnaFile) => {
- get(qnaFile, 'diagnostics', []).map((diagnostic) => {
+ get(qnaFile, 'diagnostics', []).forEach((diagnostic) => {
const location = `${qnaFile.id}.qna`;
notifications.push(new QnANotification(projectId, qnaFile.id, location, diagnostic));
});
diff --git a/Composer/packages/client/src/recoilModel/atoms/botState.ts b/Composer/packages/client/src/recoilModel/atoms/botState.ts
index adb1400a1e..43a906615b 100644
--- a/Composer/packages/client/src/recoilModel/atoms/botState.ts
+++ b/Composer/packages/client/src/recoilModel/atoms/botState.ts
@@ -17,7 +17,7 @@ import {
} from '@bfc/shared';
import { atomFamily } from 'recoil';
-import { BotLoadError, DesignPageLocation, QnAAllUpViewStatus } from '../../recoilModel/types';
+import { BotLoadError, DesignPageLocation } from '../../recoilModel/types';
import FilePersistence from '../persistence/FilePersistence';
import { BotStatus } from './../../constants';
@@ -241,9 +241,27 @@ export const designPageLocationState = atomFamily({
},
});
-export const qnaAllUpViewStatusState = atomFamily({
- key: getFullyQualifiedKey('qnaAllUpViewStatusState'),
- default: QnAAllUpViewStatus.Success,
+export const showCreateQnAFromUrlDialogState = atomFamily({
+ key: getFullyQualifiedKey('showCreateQnAFromUrlDialog'),
+ default: false,
+});
+
+export const showCreateQnAFromUrlDialogWithScratchState = atomFamily({
+ key: getFullyQualifiedKey('showCreateQnAFromUrlDialogWithScratch'),
+ default: false,
+});
+
+export const showCreateQnAFromScratchDialogState = atomFamily({
+ key: getFullyQualifiedKey('showCreateQnAFromScratchDialog'),
+ default: false,
+});
+export const onCreateQnAFromUrlDialogCompleteState = atomFamily<{ func: undefined | (() => void) }, string>({
+ key: getFullyQualifiedKey('onCreateQnAFromUrlDialogCompleteState'),
+ default: { func: undefined },
+});
+export const onCreateQnAFromScratchDialogCompleteState = atomFamily<{ func: undefined | (() => void) }, string>({
+ key: getFullyQualifiedKey('onCreateQnAFromScratchDialogCompleteState'),
+ default: { func: undefined },
});
export const isEjectRuntimeExistState = atomFamily({
diff --git a/Composer/packages/client/src/recoilModel/dispatchers/__tests__/dialog.test.tsx b/Composer/packages/client/src/recoilModel/dispatchers/__tests__/dialog.test.tsx
index 6e07c5aca1..7a4984ea88 100644
--- a/Composer/packages/client/src/recoilModel/dispatchers/__tests__/dialog.test.tsx
+++ b/Composer/packages/client/src/recoilModel/dispatchers/__tests__/dialog.test.tsx
@@ -45,6 +45,12 @@ jest.mock('@bfc/indexers', () => {
content,
}),
},
+ qnaIndexer: {
+ parse: (id, content) => ({
+ id,
+ content,
+ }),
+ },
lgUtil: {
parse: (id, content) => ({
id,
@@ -57,6 +63,12 @@ jest.mock('@bfc/indexers', () => {
content,
}),
},
+ qnaUtil: {
+ parse: (id, content) => ({
+ id,
+ content,
+ }),
+ },
};
});
@@ -120,8 +132,9 @@ describe('dialog dispatcher', () => {
states: [
{ recoilState: dialogsState(projectId), initialValue: [{ id: '1' }, { id: '2' }] },
{ recoilState: dialogSchemasState(projectId), initialValue: [{ id: '1' }, { id: '2' }] },
- { recoilState: lgFilesState(projectId), initialValue: [{ id: '1.lg' }, { id: '2' }] },
- { recoilState: luFilesState(projectId), initialValue: [{ id: '1.lu' }, { id: '2' }] },
+ { recoilState: lgFilesState(projectId), initialValue: [{ id: '1.en-us' }, { id: '2.en-us' }] },
+ { recoilState: luFilesState(projectId), initialValue: [{ id: '1.en-us' }, { id: '2.en-us' }] },
+ { recoilState: qnaFilesState(projectId), initialValue: [{ id: '1.en-us' }, { id: '2.en-us' }] },
{ recoilState: schemasState(projectId), initialValue: { sdk: { content: '' } } },
],
dispatcher: {
@@ -137,16 +150,17 @@ describe('dialog dispatcher', () => {
it('removes a dialog file', async () => {
await act(async () => {
- await dispatcher.createDialog({ id: '1', content: 'abcde', projectId });
+ await dispatcher.createDialog({ id: '3', content: 'abcde', projectId });
});
await act(async () => {
- await dispatcher.removeDialog('1', projectId);
+ await dispatcher.removeDialog('3', projectId);
});
- expect(renderedComponent.current.dialogs).toEqual([{ id: '2' }]);
- expect(renderedComponent.current.dialogSchemas).toEqual([{ id: '2' }]);
- expect(renderedComponent.current.lgFiles).toEqual([{ id: '2' }]);
- expect(renderedComponent.current.luFiles).toEqual([{ id: '2' }]);
+ expect(renderedComponent.current.dialogs).toEqual([{ id: '1' }, { id: '2' }]);
+ expect(renderedComponent.current.dialogSchemas).toEqual([{ id: '1' }, { id: '2' }]);
+ expect(renderedComponent.current.lgFiles).toEqual([{ id: '1.en-us' }, { id: '2.en-us' }]);
+ expect(renderedComponent.current.luFiles).toEqual([{ id: '1.en-us' }, { id: '2.en-us' }]);
+ expect(renderedComponent.current.qnaFiles).toEqual([{ id: '1.en-us' }, { id: '2.en-us' }]);
});
it('updates a dialog file', async () => {
diff --git a/Composer/packages/client/src/recoilModel/dispatchers/__tests__/qna.test.tsx b/Composer/packages/client/src/recoilModel/dispatchers/__tests__/qna.test.tsx
new file mode 100644
index 0000000000..306d4f235a
--- /dev/null
+++ b/Composer/packages/client/src/recoilModel/dispatchers/__tests__/qna.test.tsx
@@ -0,0 +1,276 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { useRecoilState } from 'recoil';
+import { QnAFile } from '@bfc/shared';
+import { qnaUtil } from '@bfc/indexers';
+import { useRecoilValue } from 'recoil';
+import { act } from '@bfc/test-utils/lib/hooks';
+
+import { qnaDispatcher } from '../qna';
+import { renderRecoilHook } from '../../../../__tests__/testUtils';
+import { qnaFilesState, currentProjectIdState, localeState } from '../../atoms';
+import { dispatcherState } from '../../../recoilModel/DispatcherWrapper';
+import { Dispatcher } from '..';
+
+jest.mock('../../parsers/qnaWorker', () => {
+ const filterParseResult = (qnaFile: QnAFile) => {
+ const cloned = { ...qnaFile, resource: '' };
+ return cloned;
+ };
+ return {
+ parse: (id, content) => ({ id, content }),
+ removeSection: (projectId, qnaFile, sectionId) =>
+ filterParseResult(require('@bfc/indexers/lib/utils/qnaUtil').removeSection(qnaFile, sectionId)),
+ insertSection: (projectId, qnaFile, position, sectionContent) =>
+ filterParseResult(require('@bfc/indexers/lib/utils/qnaUtil').insertSection(qnaFile, position, sectionContent)),
+ createQnAQuestion: (projectId, qnaFile, sectionId, questionContent) =>
+ filterParseResult(
+ require('@bfc/indexers/lib/utils/qnaUtil').createQnAQuestion(qnaFile, sectionId, questionContent)
+ ),
+ updateQnAQuestion: (projectId, qnaFile, sectionId, questionId, questionContent) =>
+ filterParseResult(
+ require('@bfc/indexers/lib/utils/qnaUtil').updateQnAQuestion(qnaFile, sectionId, questionId, questionContent)
+ ),
+ removeQnAQuestion: (projectId, qnaFile, sectionId, questionId) =>
+ filterParseResult(require('@bfc/indexers/lib/utils/qnaUtil').removeQnAQuestion(qnaFile, sectionId, questionId)),
+ updateQnAAnswer: (projectId, qnaFile, sectionId, answerContent) =>
+ filterParseResult(require('@bfc/indexers/lib/utils/qnaUtil').updateQnAAnswer(qnaFile, sectionId, answerContent)),
+ addImport: (projectId, qnaFile, path) =>
+ filterParseResult(require('@bfc/indexers/lib/utils/qnaUtil').addImport(qnaFile, path)),
+ removeImport: (projectId, qnaFile, path) =>
+ filterParseResult(require('@bfc/indexers/lib/utils/qnaUtil').removeImport(qnaFile, path)),
+ };
+});
+
+const projectId = '123asad.123sad';
+const locale = 'en-us';
+
+const content = `# ? What's your name?
+\`\`\`
+Zoidberg
+\`\`\``;
+
+const qna1 = qnaUtil.parse('common.en-us', content);
+const qnaFiles = [qna1];
+
+describe('QnA dispatcher', () => {
+ let renderedComponent, dispatcher: Dispatcher;
+ beforeEach(() => {
+ const useRecoilTestHook = () => {
+ const [qnaFiles, setQnAFiles] = useRecoilState(qnaFilesState(projectId));
+ const currentDispatcher = useRecoilValue(dispatcherState);
+
+ return {
+ qnaFiles,
+ setQnAFiles,
+ currentDispatcher,
+ };
+ };
+
+ const { result } = renderRecoilHook(useRecoilTestHook, {
+ states: [
+ { recoilState: qnaFilesState(projectId), initialValue: qnaFiles },
+ { recoilState: currentProjectIdState, initialValue: projectId },
+ { recoilState: localeState(projectId), initialValue: locale },
+ ],
+ dispatcher: {
+ recoilState: dispatcherState,
+ initialValue: {
+ qnaDispatcher,
+ },
+ },
+ });
+ renderedComponent = result;
+ dispatcher = renderedComponent.current.currentDispatcher;
+ });
+
+ it('should create a qna pairs', async () => {
+ const content = qnaUtil.generateQnAPair('Test', '-add');
+ await act(async () => {
+ await dispatcher.createQnAPairs({
+ id: 'common.en-us',
+ content,
+ projectId,
+ });
+ });
+
+ expect(renderedComponent.current.qnaFiles[0].content).toContain(content);
+ });
+
+ it('should update a qna file', async () => {
+ const content = qnaUtil.generateQnAPair('Test', '-update');
+ await act(async () => {
+ await dispatcher.updateQnAFile({ id: 'common.en-us', content, projectId });
+ });
+
+ expect(renderedComponent.current.qnaFiles[0].content).toBe(content);
+ });
+
+ it('should update a qna question', async () => {
+ await act(async () => {
+ await dispatcher.updateQnAQuestion({
+ id: 'common.en-us',
+ sectionId: qna1.qnaSections[0].sectionId,
+ questionId: qna1.qnaSections[0].Questions[0].id,
+ content: 'What is your name, my friend?',
+ projectId,
+ });
+ });
+
+ expect(renderedComponent.current.qnaFiles[0].qnaSections[0].Questions[0].content).toContain(
+ 'What is your name, my friend?'
+ );
+ });
+
+ it('should create/remove a qna question', async () => {
+ await act(async () => {
+ await dispatcher.createQnAQuestion({
+ id: 'common.en-us',
+ sectionId: qna1.qnaSections[0].sectionId,
+ content: 'What is your name, my friend?',
+ projectId,
+ });
+ });
+ const section = renderedComponent.current.qnaFiles[0].qnaSections[0];
+
+ expect(section.Questions[1].content).toContain('What is your name, my friend?');
+
+ await act(async () => {
+ await dispatcher.removeQnAQuestion({
+ id: 'common.en-us',
+ sectionId: section.sectionId,
+ questionId: section.Questions[1].id,
+ projectId,
+ });
+ });
+
+ expect(renderedComponent.current.qnaFiles[0].content).not.toContain('What is your name, my friend?');
+ });
+
+ it('should update a qna answer', async () => {
+ await act(async () => {
+ await dispatcher.updateQnAAnswer({
+ id: 'common.en-us',
+ sectionId: qna1.qnaSections[0].sectionId,
+ content: 'Bender',
+ projectId,
+ });
+ });
+
+ expect(renderedComponent.current.qnaFiles[0].qnaSections[0].Answer).toBe('Bender');
+ });
+
+ it('should remove a qna pair', async () => {
+ await act(async () => {
+ await dispatcher.removeQnAPairs({
+ id: 'common.en-us',
+ sectionId: qna1.qnaSections[0].sectionId,
+ projectId,
+ });
+ });
+
+ expect(renderedComponent.current.qnaFiles[0].content).toBe(``);
+ });
+
+ it('should create/remove import', async () => {
+ await act(async () => {
+ await dispatcher.createQnAImport({
+ id: 'common.en-us',
+ sourceId: 'guide.source',
+ projectId,
+ });
+ });
+
+ expect(renderedComponent.current.qnaFiles[0].content).toContain('[import](guide.source.qna)');
+
+ await act(async () => {
+ await dispatcher.removeQnAImport({
+ id: 'common.en-us',
+ sourceId: 'guide.source',
+ projectId,
+ });
+ });
+
+ expect(renderedComponent.current.qnaFiles[0].content).not.toContain('[import](guide.source.qna)');
+ });
+
+ it('should create/remove qna file', async () => {
+ await act(async () => {
+ await dispatcher.createQnAFile({
+ id: 'guide',
+ content: '> guide',
+ projectId,
+ });
+ });
+
+ const createdFile = renderedComponent.current.qnaFiles.find(({ id }) => id === 'guide.en-us');
+ expect(createdFile).not.toBeFalsy();
+ expect(createdFile.content).toBe('> guide');
+
+ await act(async () => {
+ await dispatcher.removeQnAFile({
+ id: 'guide',
+ projectId,
+ });
+ });
+
+ const createdFileAfterRemove = renderedComponent.current.qnaFiles.find(({ id }) => id === 'guide.en-us');
+ expect(createdFileAfterRemove).toBeFalsy();
+ expect(renderedComponent.current.qnaFiles.length).toBe(1);
+ });
+
+ it('should create qna kb from scratch and auto create import', async () => {
+ await act(async () => {
+ await dispatcher.createQnAKBFromScratch({
+ id: 'common.en-us',
+ name: 'guide',
+ projectId,
+ });
+ });
+
+ const createdFile = renderedComponent.current.qnaFiles.find(({ id }) => id === 'guide.source');
+ expect(createdFile).not.toBeFalsy();
+
+ const commonFile = renderedComponent.current.qnaFiles.find(({ id }) => id === 'common.en-us');
+ expect(commonFile.content).toContain('[import](guide.source.qna)');
+ });
+
+ it('should rename qna kb and re-create import', async () => {
+ await act(async () => {
+ await dispatcher.createQnAKBFromScratch({
+ id: 'common.en-us',
+ name: 'guide',
+ projectId,
+ });
+
+ await dispatcher.renameQnAKB({
+ id: 'guide.source',
+ name: 'guide2.source',
+ projectId,
+ });
+
+ await dispatcher.removeQnAImport({
+ id: 'common.en-us',
+ sourceId: 'guide.source',
+ projectId,
+ });
+
+ await dispatcher.createQnAImport({
+ id: 'common.en-us',
+ sourceId: 'guide2.source',
+ projectId,
+ });
+ });
+
+ const createdFile1 = renderedComponent.current.qnaFiles.find(({ id }) => id === 'guide.source');
+ expect(createdFile1).toBeFalsy();
+
+ const createdFile2 = renderedComponent.current.qnaFiles.find(({ id }) => id === 'guide2.source');
+ expect(createdFile2).not.toBeFalsy();
+
+ const commonFile = renderedComponent.current.qnaFiles.find(({ id }) => id === 'common.en-us');
+ expect(commonFile.content).toContain('[import](guide2.source.qna)');
+ expect(commonFile.content).not.toContain('[import](guide.source.qna)');
+ });
+});
diff --git a/Composer/packages/client/src/recoilModel/dispatchers/lg.ts b/Composer/packages/client/src/recoilModel/dispatchers/lg.ts
index dd4d866f25..95107d37fa 100644
--- a/Composer/packages/client/src/recoilModel/dispatchers/lg.ts
+++ b/Composer/packages/client/src/recoilModel/dispatchers/lg.ts
@@ -112,14 +112,18 @@ export const removeLgFileState = async (
callbackHelpers: CallbackInterface,
{ id, projectId }: { id: string; projectId: string }
) => {
- try {
- const { set, snapshot } = callbackHelpers;
- let lgFiles = await snapshot.getPromise(lgFilesState(projectId));
- lgFiles = lgFiles.filter((file) => getBaseName(file.id) !== id);
- set(lgFilesState(projectId), lgFiles);
- } catch (error) {
- setError(callbackHelpers, error);
+ const { set, snapshot } = callbackHelpers;
+ let lgFiles = await snapshot.getPromise(lgFilesState(projectId));
+ const locale = await snapshot.getPromise(localeState(projectId));
+
+ const targetLgFile = lgFiles.find((item) => item.id === id) || lgFiles.find((item) => item.id === `${id}.${locale}`);
+ if (!targetLgFile) {
+ setError(callbackHelpers, new Error(`remove lg file ${id} not exist`));
+ return;
}
+
+ lgFiles = lgFiles.filter((file) => file.id !== targetLgFile.id);
+ set(lgFilesState(projectId), lgFiles);
};
export const lgDispatcher = () => {
diff --git a/Composer/packages/client/src/recoilModel/dispatchers/lu.ts b/Composer/packages/client/src/recoilModel/dispatchers/lu.ts
index 1d236a5d50..a9d66cfb02 100644
--- a/Composer/packages/client/src/recoilModel/dispatchers/lu.ts
+++ b/Composer/packages/client/src/recoilModel/dispatchers/lu.ts
@@ -86,7 +86,8 @@ export const createLuFileState = async (
const createdLuId = `${id}.${locale}`;
const createdLuFile = (await luWorker.parse(id, content, luFeatures)) as LuFile;
if (luFiles.find((lu) => lu.id === createdLuId)) {
- throw new Error('lu file already exist');
+ setError(callbackHelpers, new Error(formatMessage('lu file already exist')));
+ return;
}
const changes: LuFile[] = [];
@@ -109,14 +110,22 @@ export const removeLuFileState = async (
) => {
const { set, snapshot } = callbackHelpers;
let luFiles = await snapshot.getPromise(luFilesState(projectId));
+ const locale = await snapshot.getPromise(localeState(projectId));
+
+ const targetLuFile = luFiles.find((item) => item.id === id) || luFiles.find((item) => item.id === `${id}.${locale}`);
+ if (!targetLuFile) {
+ // eslint-disable-next-line format-message/literal-pattern
+ setError(callbackHelpers, new Error(formatMessage(`remove lu file ${id} not exist`)));
+ return;
+ }
luFiles.forEach((file) => {
- if (getBaseName(file.id) === getBaseName(id)) {
- luFileStatusStorage.removeFileStatus(projectId, id);
+ if (file.id === targetLuFile.id) {
+ luFileStatusStorage.removeFileStatus(projectId, targetLuFile.id);
}
});
- luFiles = luFiles.filter((file) => getBaseName(file.id) !== id);
+ luFiles = luFiles.filter((file) => file.id !== targetLuFile.id);
set(luFilesState(projectId), luFiles);
};
diff --git a/Composer/packages/client/src/recoilModel/dispatchers/qna.ts b/Composer/packages/client/src/recoilModel/dispatchers/qna.ts
index adbbdedfff..8daac100ab 100644
--- a/Composer/packages/client/src/recoilModel/dispatchers/qna.ts
+++ b/Composer/packages/client/src/recoilModel/dispatchers/qna.ts
@@ -3,9 +3,19 @@
/* eslint-disable react-hooks/rules-of-hooks */
import { QnAFile } from '@bfc/shared';
import { useRecoilCallback, CallbackInterface } from 'recoil';
+import { qnaUtil } from '@bfc/indexers';
import qnaWorker from '../parsers/qnaWorker';
-import { qnaFilesState, localeState, settingsState } from '../atoms/botState';
+import {
+ qnaFilesState,
+ localeState,
+ settingsState,
+ showCreateQnAFromScratchDialogState,
+ showCreateQnAFromUrlDialogState,
+ showCreateQnAFromUrlDialogWithScratchState,
+ onCreateQnAFromScratchDialogCompleteState,
+ onCreateQnAFromUrlDialogCompleteState,
+} from '../atoms/botState';
import qnaFileStatusStorage from '../../utils/qnaFileStatusStorage';
import { getBaseName } from '../../utils/fileUtil';
import { navigateTo } from '../../utils/navigation';
@@ -63,24 +73,205 @@ export const createQnAFileState = async (
set(qnaFilesState(projectId), [...qnaFiles, ...changes]);
};
+/**
+ * id can be
+ * 1. dialogId, no locale
+ * 2. qna file id, with locale
+ * 3. source qna file id, no locale
+ */
export const removeQnAFileState = async (
callbackHelpers: CallbackInterface,
{ id, projectId }: { id: string; projectId: string }
) => {
const { set, snapshot } = callbackHelpers;
let qnaFiles = await snapshot.getPromise(qnaFilesState(projectId));
+ const locale = await snapshot.getPromise(localeState(projectId));
+
+ const targetQnAFile =
+ qnaFiles.find((item) => item.id === id) || qnaFiles.find((item) => item.id === `${id}.${locale}`);
+ if (!targetQnAFile) {
+ throw new Error(`remove qna file ${id} not exist`);
+ }
qnaFiles.forEach((file) => {
- if (getBaseName(file.id) === getBaseName(id)) {
- qnaFileStatusStorage.removeFileStatus(projectId, id);
+ if (file.id === targetQnAFile.id) {
+ qnaFileStatusStorage.removeFileStatus(projectId, targetQnAFile.id);
}
});
- qnaFiles = qnaFiles.filter((file) => getBaseName(file.id) !== id);
+ qnaFiles = qnaFiles.filter((file) => file.id !== targetQnAFile.id);
set(qnaFilesState(projectId), qnaFiles);
};
+export const createKBFileState = async (
+ callbackHelpers: CallbackInterface,
+ { id, name, content, projectId }: { id: string; name: string; content: string; projectId: string }
+) => {
+ const { set, snapshot } = callbackHelpers;
+ const qnaFiles = await snapshot.getPromise(qnaFilesState(projectId));
+ const createdSourceQnAId = `${name}.source`;
+
+ if (qnaFiles.find((qna) => qna.id === createdSourceQnAId)) {
+ throw new Error(`source qna file ${createdSourceQnAId}.qna already exist`);
+ }
+
+ const createdQnAFile = (await qnaWorker.parse(createdSourceQnAId, content)) as QnAFile;
+
+ let newQnAFiles = [...qnaFiles];
+
+ // if created on a dialog, need update this dialog's all locale qna ref
+ if (id.includes('.source') === false) {
+ const updatedQnAId = id;
+ if (!qnaFiles.find((f) => f.id === updatedQnAId)) {
+ throw new Error(`update qna file ${updatedQnAId}.qna not exist`);
+ }
+
+ newQnAFiles = qnaFiles.map((file) => {
+ if (!file.id.endsWith('.source') && getBaseName(file.id) === getBaseName(updatedQnAId)) {
+ return qnaUtil.addImport(file, `${createdSourceQnAId}.qna`);
+ }
+ return file;
+ });
+
+ qnaFileStatusStorage.updateFileStatus(projectId, updatedQnAId);
+ }
+
+ qnaFileStatusStorage.updateFileStatus(projectId, createdSourceQnAId);
+ set(qnaFilesState(projectId), [createdQnAFile, ...newQnAFiles]);
+};
+
+export const removeKBFileState = async (
+ callbackHelpers: CallbackInterface,
+ { id, projectId }: { id: string; projectId: string }
+) => {
+ const { set, snapshot } = callbackHelpers;
+ let qnaFiles = await snapshot.getPromise(qnaFilesState(projectId));
+ const locale = await snapshot.getPromise(localeState(projectId));
+
+ const targetQnAFile =
+ qnaFiles.find((item) => item.id === id) || qnaFiles.find((item) => item.id === `${id}.${locale}`);
+ if (!targetQnAFile) {
+ throw new Error(`remove qna container file ${id} not exist`);
+ }
+
+ qnaFiles.forEach((file) => {
+ if (file.id === targetQnAFile.id) {
+ qnaFileStatusStorage.removeFileStatus(projectId, targetQnAFile.id);
+ }
+ });
+
+ qnaFiles = qnaFiles.filter((file) => file.id !== targetQnAFile.id);
+ set(qnaFilesState(projectId), qnaFiles);
+};
+
+export const renameKBFileState = async (
+ callbackHelpers: CallbackInterface,
+ { id, name, projectId }: { id: string; name: string; projectId: string }
+) => {
+ const { set, snapshot } = callbackHelpers;
+ const qnaFiles = await snapshot.getPromise(qnaFilesState(projectId));
+ const locale = await snapshot.getPromise(localeState(projectId));
+
+ const targetQnAFile =
+ qnaFiles.find((item) => item.id === id) || qnaFiles.find((item) => item.id === `${id}.${locale}`);
+ if (!targetQnAFile) {
+ throw new Error(`rename qna container file ${id} not exist`);
+ }
+
+ const existQnAFile =
+ qnaFiles.find((item) => item.id === name) || qnaFiles.find((item) => item.id === `${name}.${locale}`);
+ if (existQnAFile) {
+ throw new Error(`rename qna container file to ${name} already exist`);
+ }
+ qnaFileStatusStorage.removeFileStatus(projectId, targetQnAFile.id);
+
+ const newQnAFiles = qnaFiles.map((file) => {
+ if (file.id === targetQnAFile.id) {
+ return {
+ ...file,
+ id: name,
+ };
+ }
+ return file;
+ });
+
+ set(qnaFilesState(projectId), newQnAFiles);
+};
+
export const qnaDispatcher = () => {
+ const createQnAFromUrlDialogBegin = useRecoilCallback(
+ ({ set }: CallbackInterface) => async ({
+ onComplete,
+ projectId,
+ showFromScratch,
+ }: {
+ onComplete?: () => void;
+ showFromScratch: boolean;
+ projectId: string;
+ }) => {
+ set(showCreateQnAFromUrlDialogState(projectId), true);
+ if (showFromScratch) {
+ set(showCreateQnAFromUrlDialogWithScratchState(projectId), true);
+ } else {
+ set(showCreateQnAFromUrlDialogWithScratchState(projectId), false);
+ }
+ set(onCreateQnAFromUrlDialogCompleteState(projectId), { func: onComplete });
+ }
+ );
+
+ const createQnAFromUrlDialogCancel = useRecoilCallback(
+ ({ set }: CallbackInterface) => ({ projectId }: { projectId: string }) => {
+ set(showCreateQnAFromUrlDialogState(projectId), false);
+ set(onCreateQnAFromUrlDialogCompleteState(projectId), { func: undefined });
+ }
+ );
+
+ const createQnAFromScratchDialogBegin = useRecoilCallback(
+ ({ set }: CallbackInterface) => async ({
+ onComplete,
+ projectId,
+ }: {
+ onComplete?: () => void;
+ projectId: string;
+ }) => {
+ set(showCreateQnAFromScratchDialogState(projectId), true);
+ set(onCreateQnAFromScratchDialogCompleteState(projectId), { func: onComplete });
+ }
+ );
+
+ const createQnAFromScratchDialogCancel = useRecoilCallback(
+ ({ set }: CallbackInterface) => async ({ projectId }: { projectId: string }) => {
+ set(showCreateQnAFromScratchDialogState(projectId), false);
+ set(onCreateQnAFromScratchDialogCompleteState(projectId), { func: undefined });
+ }
+ );
+
+ const createQnAFromUrlDialogSuccess = useRecoilCallback(
+ ({ set, snapshot }: CallbackInterface) => async ({ projectId }: { projectId: string }) => {
+ const onCreateQnAFromUrlDialogComplete = (
+ await snapshot.getPromise(onCreateQnAFromUrlDialogCompleteState(projectId))
+ ).func;
+ if (typeof onCreateQnAFromUrlDialogComplete === 'function') {
+ onCreateQnAFromUrlDialogComplete();
+ }
+ set(showCreateQnAFromUrlDialogState(projectId), false);
+ set(onCreateQnAFromUrlDialogCompleteState(projectId), { func: undefined });
+ }
+ );
+
+ const createQnAFromScratchDialogSuccess = useRecoilCallback(
+ ({ set, snapshot }: CallbackInterface) => async ({ projectId }: { projectId: string }) => {
+ const onCreateQnAFromScratchDialogComplete = (
+ await snapshot.getPromise(onCreateQnAFromScratchDialogCompleteState(projectId))
+ ).func;
+ if (typeof onCreateQnAFromScratchDialogComplete === 'function') {
+ onCreateQnAFromScratchDialogComplete();
+ }
+ set(showCreateQnAFromScratchDialogState(projectId), false);
+ set(onCreateQnAFromScratchDialogCompleteState(projectId), { func: undefined });
+ }
+ );
+
const updateQnAFile = useRecoilCallback(
(callbackHelpers: CallbackInterface) => async ({
id,
@@ -109,28 +300,43 @@ export const qnaDispatcher = () => {
}
);
- const importQnAFromUrls = useRecoilCallback(
+ const removeQnAFile = useRecoilCallback(
+ (callbackHelpers: CallbackInterface) => async ({ id, projectId }: { id: string; projectId: string }) => {
+ await removeQnAFileState(callbackHelpers, { id, projectId });
+ }
+ );
+
+ const dismissCreateQnAModal = useRecoilCallback(
+ ({ set }: CallbackInterface) => async ({ projectId }: { projectId: string }) => {
+ set(showCreateQnAFromUrlDialogState(projectId), false);
+ set(showCreateQnAFromScratchDialogState(projectId), false);
+ }
+ );
+
+ const createQnAKBFromUrl = useRecoilCallback(
(callbackHelpers: CallbackInterface) => async ({
id,
- urls,
+ name,
+ url,
+ multiTurn,
projectId,
}: {
- id: string;
- urls: string[];
+ id: string; // dialogId.locale
+ name: string;
+ url: string;
+ multiTurn: boolean;
projectId: string;
}) => {
- const { snapshot } = callbackHelpers;
- const qnaFiles = await snapshot.getPromise(qnaFilesState(projectId));
- const qnaFile = qnaFiles.find((f) => f.id === id);
-
- const notification = createNotifiction(getQnaPendingNotification(urls));
+ await dismissCreateQnAModal({ projectId });
+ const notification = createNotifiction(getQnaPendingNotification(url));
addNotificationInternal(callbackHelpers, notification);
+ let response;
try {
- const response = await httpClient.get(`/utilities/qna/parse`, {
- params: { urls: encodeURIComponent(urls.join(',')) },
+ response = await httpClient.get(`/utilities/qna/parse`, {
+ params: { url: encodeURIComponent(url), multiTurn },
});
- const content = qnaFile ? qnaFile.content + '\n' + response.data : response.data;
+ const content = response.data;
await updateQnAFileState(callbackHelpers, { id, content, projectId });
const notification = createNotifiction(
@@ -145,15 +351,318 @@ export const qnaDispatcher = () => {
callbackHelpers,
createNotifiction(getQnaFailedNotification(err.response?.data?.message))
);
+ createQnAFromUrlDialogCancel({ projectId });
+ return;
} finally {
deleteNotificationInternal(callbackHelpers, notification.id);
}
+
+ const contentForSourceQnA = `> !# @source.url=${url}
+> !# @source.multiTurn=${multiTurn}
+${response.data}
+`;
+
+ await createKBFileState(callbackHelpers, {
+ id,
+ name,
+ content: contentForSourceQnA,
+ projectId,
+ });
+
+ await createQnAFromUrlDialogSuccess({ projectId });
+ }
+ );
+
+ const createQnAKBFromScratch = useRecoilCallback(
+ (callbackHelpers: CallbackInterface) => async ({
+ id,
+ name,
+ projectId,
+ content = '',
+ }: {
+ id: string; // dialogId.locale
+ name: string;
+ content?: string;
+ projectId: string;
+ }) => {
+ await dismissCreateQnAModal({ projectId });
+
+ await createKBFileState(callbackHelpers, {
+ id,
+ name,
+ content,
+ projectId,
+ });
+ await createQnAFromScratchDialogSuccess({ projectId });
+
+ const notification = createNotifiction(
+ getQnaSuccessNotification(() => {
+ navigateTo(`/bot/${projectId}/knowledge-base/${getBaseName(id)}`);
+ deleteNotificationInternal(callbackHelpers, notification.id);
+ })
+ );
+ addNotificationInternal(callbackHelpers, notification);
+ }
+ );
+
+ const updateQnAQuestion = useRecoilCallback(
+ ({ set, snapshot }: CallbackInterface) => async ({
+ id,
+ sectionId,
+ questionId,
+ content,
+ projectId,
+ }: {
+ id: string;
+ sectionId: string;
+ questionId: string;
+ content: string;
+ projectId: string;
+ }) => {
+ const qnaFiles = await snapshot.getPromise(qnaFilesState(projectId));
+ const qnaFile = qnaFiles.find((temp) => temp.id === id);
+ if (!qnaFile) return qnaFiles;
+
+ // const updatedFile = await updateQnAFileState(callbackHelpers, { id, content });
+ const updatedFile = qnaUtil.updateQnAQuestion(qnaFile, sectionId, questionId, content);
+ set(qnaFilesState(projectId), (qnaFiles) => {
+ return qnaFiles.map((file) => {
+ return file.id === id ? updatedFile : file;
+ });
+ });
+ }
+ );
+
+ const updateQnAAnswer = useRecoilCallback(
+ ({ set, snapshot }: CallbackInterface) => async ({
+ id,
+ sectionId,
+ content,
+ projectId,
+ }: {
+ id: string;
+ sectionId: string;
+ content: string;
+ projectId: string;
+ }) => {
+ const qnaFiles = await snapshot.getPromise(qnaFilesState(projectId));
+ const qnaFile = qnaFiles.find((temp) => temp.id === id);
+ if (!qnaFile) return qnaFiles;
+
+ const updatedFile = qnaUtil.updateQnAAnswer(qnaFile, sectionId, content);
+ set(qnaFilesState(projectId), (qnaFiles) => {
+ return qnaFiles.map((file) => {
+ return file.id === id ? updatedFile : file;
+ });
+ });
+ }
+ );
+
+ const createQnAQuestion = useRecoilCallback(
+ ({ set, snapshot }: CallbackInterface) => async ({
+ id,
+ sectionId,
+ content,
+ projectId,
+ }: {
+ id: string;
+ sectionId: string;
+ content: string;
+ projectId: string;
+ }) => {
+ const qnaFiles = await snapshot.getPromise(qnaFilesState(projectId));
+ const qnaFile = qnaFiles.find((temp) => temp.id === id);
+ if (!qnaFile) return qnaFiles;
+
+ const updatedFile = qnaUtil.createQnAQuestion(qnaFile, sectionId, content);
+ set(qnaFilesState(projectId), (qnaFiles) => {
+ return qnaFiles.map((file) => {
+ return file.id === id ? updatedFile : file;
+ });
+ });
+ }
+ );
+
+ const removeQnAQuestion = useRecoilCallback(
+ ({ set, snapshot }: CallbackInterface) => async ({
+ id,
+ sectionId,
+ questionId,
+ projectId,
+ }: {
+ id: string;
+ sectionId: string;
+ questionId: string;
+ projectId: string;
+ }) => {
+ const qnaFiles = await snapshot.getPromise(qnaFilesState(projectId));
+ const qnaFile = qnaFiles.find((temp) => temp.id === id);
+ if (!qnaFile) return qnaFiles;
+
+ const updatedFile = qnaUtil.removeQnAQuestion(qnaFile, sectionId, questionId);
+ set(qnaFilesState(projectId), (qnaFiles) => {
+ return qnaFiles.map((file) => {
+ return file.id === id ? updatedFile : file;
+ });
+ });
+ }
+ );
+
+ const createQnAPairs = useRecoilCallback(
+ ({ set, snapshot }: CallbackInterface) => async ({
+ id,
+ content,
+ projectId,
+ }: {
+ id: string;
+ content: string;
+ projectId: string;
+ }) => {
+ const qnaFiles = await snapshot.getPromise(qnaFilesState(projectId));
+ const qnaFile = qnaFiles.find((temp) => temp.id === id);
+ if (!qnaFile) return qnaFiles;
+
+ // insert into head, need investigate
+ const updatedFile = qnaUtil.insertSection(qnaFile, 0, content);
+ set(qnaFilesState(projectId), (qnaFiles) => {
+ return qnaFiles.map((file) => {
+ return file.id === id ? updatedFile : file;
+ });
+ });
+ }
+ );
+
+ const removeQnAPairs = useRecoilCallback(
+ ({ set, snapshot }: CallbackInterface) => async ({
+ id,
+ sectionId,
+ projectId,
+ }: {
+ id: string;
+ sectionId: string;
+ projectId: string;
+ }) => {
+ const qnaFiles = await snapshot.getPromise(qnaFilesState(projectId));
+ const qnaFile = qnaFiles.find((temp) => temp.id === id);
+ if (!qnaFile) return qnaFiles;
+
+ const updatedFile = qnaUtil.removeSection(qnaFile, sectionId);
+ set(qnaFilesState(projectId), (qnaFiles) => {
+ return qnaFiles.map((file) => {
+ return file.id === id ? updatedFile : file;
+ });
+ });
+ }
+ );
+
+ const createQnAImport = useRecoilCallback(
+ ({ set, snapshot }: CallbackInterface) => async ({
+ id,
+ sourceId,
+ projectId,
+ }: {
+ id: string;
+ sourceId: string;
+ projectId: string;
+ }) => {
+ const qnaFiles = await snapshot.getPromise(qnaFilesState(projectId));
+ const qnaFile = qnaFiles.find((temp) => temp.id === id);
+ if (!qnaFile) return qnaFiles;
+
+ const updatedFile = qnaUtil.addImport(qnaFile, `${sourceId}.qna`);
+ set(qnaFilesState(projectId), (qnaFiles) => {
+ return qnaFiles.map((file) => {
+ return file.id === id ? updatedFile : file;
+ });
+ });
+ }
+ );
+ const removeQnAImport = useRecoilCallback(
+ ({ set, snapshot }: CallbackInterface) => async ({
+ id,
+ sourceId,
+ projectId,
+ }: {
+ id: string;
+ sourceId: string;
+ projectId: string;
+ }) => {
+ const qnaFiles = await snapshot.getPromise(qnaFilesState(projectId));
+ const qnaFile = qnaFiles.find((temp) => temp.id === id);
+ if (!qnaFile) return qnaFiles;
+
+ const updatedFile = qnaUtil.removeImport(qnaFile, `${sourceId}.qna`);
+ set(qnaFilesState(projectId), (qnaFiles) => {
+ return qnaFiles.map((file) => {
+ return file.id === id ? updatedFile : file;
+ });
+ });
+ }
+ );
+ const updateQnAImport = useRecoilCallback(
+ ({ set, snapshot }: CallbackInterface) => async ({
+ id,
+ sourceId,
+ newSourceId,
+ projectId,
+ }: {
+ id: string;
+ sourceId: string;
+ newSourceId: string;
+ projectId: string;
+ }) => {
+ const qnaFiles = await snapshot.getPromise(qnaFilesState(projectId));
+ const qnaFile = qnaFiles.find((temp) => temp.id === id);
+ if (!qnaFile) return qnaFiles;
+
+ let updatedFile = qnaUtil.removeImport(qnaFile, `${sourceId}.qna`);
+ updatedFile = qnaUtil.addImport(updatedFile, `${newSourceId}.qna`);
+ set(qnaFilesState(projectId), (qnaFiles) => {
+ return qnaFiles.map((file) => {
+ return file.id === id ? updatedFile : file;
+ });
+ });
+ }
+ );
+ const removeQnAKB = useRecoilCallback(
+ (callbackHelpers: CallbackInterface) => async ({ id, projectId }: { id: string; projectId: string }) => {
+ await removeKBFileState(callbackHelpers, { id, projectId });
+ }
+ );
+ const renameQnAKB = useRecoilCallback(
+ (callbackHelpers: CallbackInterface) => async ({
+ id,
+ name,
+ projectId,
+ }: {
+ id: string;
+ name: string;
+ projectId: string;
+ }) => {
+ await renameKBFileState(callbackHelpers, { id, name, projectId });
}
);
return {
+ createQnAImport,
+ removeQnAImport,
+ updateQnAImport,
+ createQnAPairs,
+ removeQnAPairs,
+ createQnAQuestion,
+ removeQnAQuestion,
+ updateQnAQuestion,
+ updateQnAAnswer,
createQnAFile,
+ removeQnAFile,
updateQnAFile,
- importQnAFromUrls,
+ removeQnAKB,
+ renameQnAKB,
+ createQnAKBFromUrl,
+ createQnAKBFromScratch,
+ createQnAFromScratchDialogBegin,
+ createQnAFromScratchDialogCancel,
+ createQnAFromUrlDialogBegin,
+ createQnAFromUrlDialogCancel,
};
};
diff --git a/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts b/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts
index 018cc8fb2d..81ce30ee06 100644
--- a/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts
+++ b/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts
@@ -32,7 +32,7 @@ import luFileStatusStorage from '../../../utils/luFileStatusStorage';
import { getReferredLuFiles } from '../../../utils/luUtil';
import { navigateTo } from '../../../utils/navigation';
import qnaFileStatusStorage from '../../../utils/qnaFileStatusStorage';
-import { getReferredQnaFiles } from '../../../utils/qnaUtil';
+import { getReferredQnaFiles, reformQnAToContainerKB } from '../../../utils/qnaUtil';
import {
botDiagnosticsState,
botDisplayNameState,
@@ -60,6 +60,7 @@ import {
settingsState,
skillManifestsState,
skillsState,
+ showCreateQnAFromUrlDialogState,
} from '../../atoms';
import * as botstates from '../../atoms/botState';
import lgWorker from '../../parsers/lgWorker';
@@ -159,8 +160,13 @@ export const loadProjectData = (response) => {
const storedLocale = languageStorage.get(botName)?.locale;
const locale = settings.languages.includes(storedLocale) ? storedLocale : settings.defaultLanguage;
const indexedFiles = indexer.index(files, botName, locale, skillContent, mergedSettings);
+
+ // migrate script move qna pairs in *.qna to *-manual.source.qna.
+ // TODO: remove after a period of time.
+ const updateQnAFiles = reformQnAToContainerKB(projectId, indexedFiles.qnaFiles);
+
return {
- botFiles: { ...indexedFiles, mergedSettings },
+ botFiles: { ...indexedFiles, qnaFiles: updateQnAFiles, mergedSettings },
projectData: response.data,
error: undefined,
};
@@ -409,7 +415,13 @@ export const createNewBotFromTemplate = async (
}
const currentBotProjectFileIndexed: BotProjectFile = botFiles.botProjectSpaceFiles[0];
set(botProjectFileState(projectId), currentBotProjectFileIndexed);
+
const mainDialog = await initBotState(callbackHelpers, projectData, botFiles);
+ // if create from QnATemplate, continue creation flow.
+ if (templateId === QnABotTemplateId) {
+ set(showCreateQnAFromUrlDialogState(projectId), true);
+ }
+
return { projectId, mainDialog };
};
diff --git a/Composer/packages/client/src/recoilModel/parsers/workers/qnaParser.worker.ts b/Composer/packages/client/src/recoilModel/parsers/workers/qnaParser.worker.ts
index 7dc3481f4a..cc463d5f9d 100644
--- a/Composer/packages/client/src/recoilModel/parsers/workers/qnaParser.worker.ts
+++ b/Composer/packages/client/src/recoilModel/parsers/workers/qnaParser.worker.ts
@@ -1,15 +1,10 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
-import { qnaIndexer } from '@bfc/indexers';
import * as qnaUtil from '@bfc/indexers/lib/utils/qnaUtil';
import { QnAActionType } from './../types';
const ctx: Worker = self as any;
-const parse = (content: string, id: string) => {
- return { id, content, ...qnaIndexer.parse(content, id) };
-};
-
ctx.onmessage = function (msg) {
const { id: msgId, type, payload } = msg.data;
const { content, id, file, indexId } = payload;
@@ -17,7 +12,7 @@ ctx.onmessage = function (msg) {
try {
switch (type) {
case QnAActionType.Parse: {
- result = parse(content, id);
+ result = qnaUtil.parse(id, content);
break;
}
case QnAActionType.AddSection: {
diff --git a/Composer/packages/client/src/recoilModel/persistence/types.ts b/Composer/packages/client/src/recoilModel/persistence/types.ts
index 8cdbf3c736..941f6a119d 100644
--- a/Composer/packages/client/src/recoilModel/persistence/types.ts
+++ b/Composer/packages/client/src/recoilModel/persistence/types.ts
@@ -15,6 +15,7 @@ export enum FileExtensions {
Lu = '.lu',
Lg = '.lg',
QnA = '.qna',
+ SourceQnA = '.source.qna',
Setting = 'appsettings.json',
BotProject = '.botproj',
}
diff --git a/Composer/packages/client/src/recoilModel/types.ts b/Composer/packages/client/src/recoilModel/types.ts
index f5ce133627..4659e14df8 100644
--- a/Composer/packages/client/src/recoilModel/types.ts
+++ b/Composer/packages/client/src/recoilModel/types.ts
@@ -110,10 +110,4 @@ export type BoilerplateVersion = {
updateRequired?: boolean;
};
-export enum QnAAllUpViewStatus {
- Loading,
- Success,
- Failed,
-}
-
export type Notification = CardProps & { id: string };
diff --git a/Composer/packages/client/src/shell/triggerApi.ts b/Composer/packages/client/src/shell/triggerApi.ts
index 79cc9e64f2..4f0b20246b 100644
--- a/Composer/packages/client/src/shell/triggerApi.ts
+++ b/Composer/packages/client/src/shell/triggerApi.ts
@@ -9,7 +9,6 @@ import get from 'lodash/get';
import { useResolvers } from '../hooks/useResolver';
import { onChooseIntentKey, generateNewDialog, intentTypeKey, qnaMatcherKey } from '../utils/dialogUtil';
-import { navigateTo } from '../utils/navigation';
import { schemasState, lgFilesState, dialogsState, localeState } from '../recoilModel';
import { Dispatcher } from '../recoilModel/dispatchers';
@@ -29,7 +28,7 @@ function createTriggerApi(
return value.substring(startIndex + 1, endIndex);
};
- const createTriggerHandler = async (id, formData, url) => {
+ const createTriggerHandler = async (id, formData, autoSelected = true) => {
const luFile = luFileResolver(id);
const lgFile = lgFileResolver(id);
const dialog = dialogResolver(id);
@@ -90,9 +89,7 @@ function createTriggerApi(
content: newDialog.content,
};
await updateDialog(dialogPayload);
- if (url) {
- navigateTo(url);
- } else {
+ if (autoSelected) {
selectTo(projectId, `triggers[${index}]`);
}
};
diff --git a/Composer/packages/client/src/utils/notifications.ts b/Composer/packages/client/src/utils/notifications.ts
index b9b5c2df04..aec925bd4f 100644
--- a/Composer/packages/client/src/utils/notifications.ts
+++ b/Composer/packages/client/src/utils/notifications.ts
@@ -3,11 +3,10 @@
import formatMessage from 'format-message';
import { CardProps } from './../components/NotificationCard';
-
-export const getQnaPendingNotification = (urls: string[]): CardProps => {
+export const getQnaPendingNotification = (url: string): CardProps => {
return {
title: formatMessage('Creating your knowledge base'),
- description: formatMessage('Extracting QNA pairs from {urls}', { urls: urls.join(' ') }),
+ description: formatMessage('Extracting QNA pairs from {url}', { url }),
type: 'pending',
};
};
diff --git a/Composer/packages/client/src/utils/qnaUtil.ts b/Composer/packages/client/src/utils/qnaUtil.ts
index 32144f4e07..2d9a484875 100644
--- a/Composer/packages/client/src/utils/qnaUtil.ts
+++ b/Composer/packages/client/src/utils/qnaUtil.ts
@@ -7,17 +7,80 @@
* for more usage detail, please check client/__tests__/utils/luUtil.test.ts
*/
import { QnAFile, DialogInfo } from '@bfc/shared';
+import { qnaUtil } from '@bfc/indexers';
+
+import { createFile, updateFile } from '../recoilModel/persistence/http';
import { getBaseName, getExtension } from './fileUtil';
-export * from '@bfc/indexers/lib/utils/qnaUtil';
-export function getFileLocale(fileName: string) {
+export const getFileLocale = (fileName: string) => {
//file name = 'a.en-us.qna'
return getExtension(getBaseName(fileName));
-}
-export function getReferredQnaFiles(qnaFiles: QnAFile[], dialogs: DialogInfo[]) {
+};
+export const getReferredQnaFiles = (qnaFiles: QnAFile[], dialogs: DialogInfo[]) => {
return qnaFiles.filter((file) => {
const idWithOutLocale = getBaseName(file.id);
return dialogs.some((dialog) => dialog.qnaFile === idWithOutLocale && !!file.content);
});
-}
+};
+// substring text file by lines
+export const substringTextByLine = (text: string, start?: number, end?: number): string => {
+ return text.split('\n').slice(start, end).join('\n');
+};
+/**
+ * Migrate qna pair in