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 @@ -23,7 +23,13 @@ import { IGroupedListStyles } from 'office-ui-fabric-react/lib/GroupedList';
import { ISearchBoxStyles } from 'office-ui-fabric-react/lib/SearchBox';

import { dispatcherState, userSettingsState } from '../../recoilModel';
import { createSelectedPath, getFriendlyName } from '../../utils/dialogUtil';
import {
createSelectedPath,
getFriendlyName,
regexRecognizerKey,
onChooseIntentKey,
qnaMatcherKey,
} from '../../utils/dialogUtil';

import { TreeItem } from './treeItem';

Expand Down Expand Up @@ -59,6 +65,9 @@ const root = css`
// -------------------- ProjectTree -------------------- //

function createGroupItem(dialog: DialogInfo, currentId: string, position: number) {
const isRegEx = (dialog.content?.recognizer?.$kind ?? '') === regexRecognizerKey;
const isNotSupported =
isRegEx && dialog.triggers.some((t) => t.type === qnaMatcherKey || t.type === onChooseIntentKey);
return {
key: dialog.id,
name: dialog.displayName,
Expand All @@ -67,14 +76,15 @@ function createGroupItem(dialog: DialogInfo, currentId: string, position: number
count: dialog.triggers.length,
hasMoreData: true,
isCollapsed: dialog.id !== currentId,
data: dialog,
data: { ...dialog, warning: isNotSupported },
};
}

function createItem(trigger: ITrigger, index: number) {
function createItem(trigger: ITrigger, index: number, isNotSupported?: boolean) {
return {
...trigger,
index,
warning: isNotSupported,
displayName: trigger.displayName || getFriendlyName({ $kind: trigger.type }),
};
}
Expand Down Expand Up @@ -106,8 +116,10 @@ function createItemsAndGroups(
(result: { items: any[]; groups: IGroup[] }, dialog) => {
result.groups.push(createGroupItem(dialog, dialogId, position));
position += dialog.triggers.length;
const isRegEx = (dialog.content?.recognizer?.$kind ?? '') === regexRecognizerKey;
dialog.triggers.forEach((item, index) => {
result.items.push(createItem(item, index));
const isNotSupported = isRegEx && (item.type === qnaMatcherKey || item.type === onChooseIntentKey);
result.items.push(createItem(item, index, isNotSupported));
});
return result;
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

/** @jsx jsx */
import { jsx, css } from '@emotion/core';
import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
import formatMessage from 'format-message';
import { Dialog, DialogType, DialogFooter } from 'office-ui-fabric-react/lib/Dialog';
import { PrimaryButton, DefaultButton } from 'office-ui-fabric-react/lib/Button';
Expand All @@ -15,11 +15,12 @@ import { TextField } from 'office-ui-fabric-react/lib/TextField';
import { luIndexer, combineMessage } from '@bfc/indexers';
import { PlaceHolderSectionName } from '@bfc/indexers/lib/utils/luUtil';
import { DialogInfo, SDKKinds } from '@bfc/shared';
import { LuEditor, inlineModePlaceholder } from '@bfc/code-editor';
import { LuEditor, inlineModePlaceholder, defaultQnAPlaceholder } from '@bfc/code-editor';
import { IComboBoxOption } from 'office-ui-fabric-react/lib/ComboBox';
import { useRecoilValue } from 'recoil';
import { FontWeights } from '@uifabric/styling';
import { FontSizes } from '@uifabric/fluent-theme';
import get from 'lodash/get';

import {
generateNewDialog,
Expand All @@ -33,6 +34,9 @@ import {
getEventTypes,
getActivityTypes,
regexRecognizerKey,
qnaMatcherKey,
onChooseIntentKey,
adaptiveCardKey,
} from '../../utils/dialogUtil';
import { addIntent } from '../../utils/luUtil';
import {
Expand All @@ -41,6 +45,7 @@ import {
localeState,
projectIdState,
schemasState,
qnaFilesState,
} from '../../recoilModel/atoms/botState';
import { userSettingsState } from '../../recoilModel';
import { nameRegex } from '../../constants';
Expand Down Expand Up @@ -92,6 +97,12 @@ const intent = {
},
};

const optionRow = {
display: 'flex',
height: 15,
fontSize: 15,
};

// -------------------- Validation Helpers -------------------- //

const initialFormDataErrors = {
Expand All @@ -103,6 +114,11 @@ const initialFormDataErrors = {
activity: '',
};

const getQnADiagnostics = (content: string) => {
const { diagnostics } = luIndexer.parse(content);
return combineMessage(diagnostics);
};

const getLuDiagnostics = (intent: string, triggerPhrases: string) => {
const content = `#${intent}\n${triggerPhrases}`;
const { diagnostics } = luIndexer.parse(content);
Expand Down Expand Up @@ -200,52 +216,74 @@ export interface LuFilePayload {
content: string;
}

export interface QnAFilePayload {
id: string;
content: string;
}

// -------------------- TriggerCreationModal -------------------- //

interface TriggerCreationModalProps {
dialogId: string;
isOpen: boolean;
onDismiss: () => void;
onSubmit: (dialog: DialogInfo, luFilePayload?: LuFilePayload) => void;
onSubmit: (dialog: DialogInfo, luFilePayload?: LuFilePayload, QnAFilePayload?: QnAFilePayload) => void;
}

export const TriggerCreationModal: React.FC<TriggerCreationModalProps> = (props) => {
const { isOpen, onDismiss, onSubmit, dialogId } = props;
const dialogs = useRecoilValue(dialogsState);
const luFiles = useRecoilValue(luFilesState);
const qnaFiles = useRecoilValue(qnaFilesState);
const locale = useRecoilValue(localeState);
const projectId = useRecoilValue(projectIdState);
const schemas = useRecoilValue(schemasState);
const userSettings = useRecoilValue(userSettingsState);
const luFile = luFiles.find(({ id }) => id === `${dialogId}.${locale}`);
const dialogFile = dialogs.find((dialog) => dialog.id === dialogId);
const isRegEx = (dialogFile?.content?.recognizer?.$kind ?? '') === regexRecognizerKey;
const recognizer = get(dialogFile, 'content.recognizer', '');
const isLUISnQnA = typeof recognizer === 'string' && recognizer.endsWith('.qna');
const regexIntents = dialogFile?.content?.recognizer?.intents ?? [];
const isNone = !dialogFile?.content?.recognizer;
const qnaFile = qnaFiles.find(({ id }) => id === `${dialogId}.${locale}`);
const initialFormData: TriggerFormData = {
errors: initialFormDataErrors,
$kind: isNone ? '' : intentTypeKey,
$kind: intentTypeKey,
event: '',
intent: '',
triggerPhrases: '',
qnaPhrases: '',
regEx: '',
};
const [formData, setFormData] = useState(initialFormData);
const [selectedType, setSelectedType] = useState(isNone ? '' : intentTypeKey);
const [selectedType, setSelectedType] = useState(intentTypeKey);
const showIntentName = selectedType === intentTypeKey;
const showRegExDropDown = selectedType === intentTypeKey && isRegEx;
const showTriggerPhrase = selectedType === intentTypeKey && !isRegEx;
const showTriggerPhrase = selectedType === intentTypeKey && isLUISnQnA;
const showEventDropDown = selectedType === eventTypeKey;
const showActivityDropDown = selectedType === activityTypeKey;
const showCustomEvent = selectedType === customEventKey;

const showQnAPhrase = selectedType === qnaMatcherKey;
const eventTypes: IComboBoxOption[] = getEventTypes();
const activityTypes: IDropdownOption[] = getActivityTypes();
let triggerTypeOptions: IDropdownOption[] = getTriggerTypes();

if (isNone) {
triggerTypeOptions = triggerTypeOptions.filter((t) => t.key !== intentTypeKey);
if (isRegEx) {
let index = triggerTypeOptions.findIndex((t) => t.key === qnaMatcherKey);
triggerTypeOptions[index].data = { icon: 'Warning' };
index = triggerTypeOptions.findIndex((t) => t.key === onChooseIntentKey);
triggerTypeOptions[index].data = { icon: 'Warning' };
}
if (!isLUISnQnA && !isRegEx) {
triggerTypeOptions = triggerTypeOptions.filter((t) => t.key !== adaptiveCardKey);
}
useEffect(() => {
setFormData({ ...formData, qnaPhrases: qnaFile ? qnaFile.content : '' });
}, [qnaFile]);

const onRenderOption = (option: IDropdownOption) => {
return <div css={optionRow}>{option.text}</div>;
};

const shouldDisable = (errors: TriggerFormDataErrors) => {
for (const key in errors) {
Expand All @@ -270,14 +308,21 @@ export const TriggerCreationModal: React.FC<TriggerCreationModalProps> = (props)
}
const content = luFile?.content ?? '';
const luFileId = luFile?.id || `${dialogId}.${locale}`;
if (formData.$kind === adaptiveCardKey) {
formData.$kind = intentTypeKey;
}
const newDialog = generateNewDialog(dialogs, dialogId, formData, schemas.sdk?.content);
if (formData.$kind === intentTypeKey && !isRegEx) {
if (formData.$kind === intentTypeKey && isLUISnQnA) {
const newContent = addIntent(content, { Name: formData.intent, Body: formData.triggerPhrases });
const updateLuFile = {
id: luFileId,
content: newContent,
};
onSubmit(newDialog, updateLuFile);
} else if (formData.$kind === qnaMatcherKey) {
const qnaFileId = qnaFile?.id || `${dialogId}.${locale}`;
const qnaFilePayload: QnAFilePayload = { id: qnaFileId, content: formData.qnaPhrases };
onSubmit(newDialog, undefined, qnaFilePayload);
} else {
onSubmit(newDialog);
}
Expand Down Expand Up @@ -325,6 +370,12 @@ export const TriggerCreationModal: React.FC<TriggerCreationModalProps> = (props)
setFormData({ ...formData, intent: name, errors: { ...formData.errors, ...errors } });
};

const onQnAPhrasesChange = (body: string) => {
const errors: TriggerFormDataErrors = {};
errors.qnaPhrases = getQnADiagnostics(body);
setFormData({ ...formData, qnaPhrases: body, errors: { ...formData.errors, ...errors } });
};

const onChangeRegEx = (e, pattern) => {
const errors: TriggerFormDataErrors = {};
errors.regEx = validateRegExPattern(selectedType, isRegEx, pattern);
Expand Down Expand Up @@ -363,6 +414,8 @@ export const TriggerCreationModal: React.FC<TriggerCreationModalProps> = (props)
options={triggerTypeOptions}
styles={dropdownStyles}
onChange={onSelectTriggerType}
//@ts-ignore:
onRenderOption={onRenderOption}
/>
{showEventDropDown && (
<Dropdown
Expand Down Expand Up @@ -402,7 +455,7 @@ export const TriggerCreationModal: React.FC<TriggerCreationModalProps> = (props)
label={
isRegEx
? formatMessage('What is the name of this trigger (RegEx)')
: formatMessage('What is the name of this trigger (LUIS)')
: formatMessage('What is the name of this trigger (LUIS + QnA)')
}
styles={intent}
onChange={onNameChange}
Expand Down Expand Up @@ -435,6 +488,19 @@ export const TriggerCreationModal: React.FC<TriggerCreationModalProps> = (props)
/>
</React.Fragment>
)}
{showQnAPhrase && (
<React.Fragment>
<Label>{formatMessage('QnA phrases')}</Label>
<LuEditor
editorSettings={userSettings.codeEditor}
errorMessage={formData.errors.qnaPhrases}
height={225}
placeholder={defaultQnAPlaceholder}
value={formData.qnaPhrases}
onChange={onQnAPhrasesChange}
/>
</React.Fragment>
)}
</Stack>
</div>
<DialogFooter>
Expand Down
25 changes: 23 additions & 2 deletions Composer/packages/client/src/components/ProjectTree/treeItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,14 @@ import { IContextualMenuStyles } from 'office-ui-fabric-react/lib/ContextualMenu
import { ICalloutContentStyles } from 'office-ui-fabric-react/lib/Callout';

// -------------------- Styles -------------------- //

const indent = 16;
const itemText = (depth: number) => css`
outline: none;
:focus {
outline: rgb(102, 102, 102) solid 1px;
z-index: 1;
}
padding-left: ${depth * 16}px;
padding-left: ${depth * indent}px;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
Expand All @@ -42,6 +42,11 @@ const content = css`
label: ProjectTreeItem;
`;

const leftIndent = css`
height: 100%;
width: ${indent}px;
`;

const moreMenu: Partial<ICalloutContentStyles> = {
root: {
marginTop: '-7px',
Expand Down Expand Up @@ -117,6 +122,12 @@ export const overflowSet = css`
justify-content: space-between;
`;

const warningIcon = {
marginRight: 5,
color: '#BE880A',
fontSize: 9,
};

// -------------------- TreeItem -------------------- //

interface ITreeItemProps {
Expand All @@ -129,6 +140,9 @@ interface ITreeItemProps {
}

const onRenderItem = (item: IOverflowSetItemProps) => {
const warningContent = formatMessage(
'This trigger type is not supported by the RegEx recognizer and will not be fired.'
);
return (
<div
data-is-focusable
Expand All @@ -139,6 +153,13 @@ const onRenderItem = (item: IOverflowSetItemProps) => {
onFocus={item.onFocus}
>
<div css={content} tabIndex={-1}>
{item.warning ? (
<TooltipHost content={warningContent} directionalHint={DirectionalHint.bottomLeftEdge}>
<Icon iconName={'Warning'} style={warningIcon} />
</TooltipHost>
) : (
<div css={leftIndent} />
)}
{item.depth !== 0 && (
<Icon
iconName="Flow"
Expand Down
3 changes: 3 additions & 0 deletions Composer/packages/client/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,9 @@ export enum ActionTypes {
ADD_LANGUAGE_DIALOG_END = 'ADD_LANGUAGE_DIALOG_END',
DEL_LANGUAGE_DIALOG_BEGIN = 'DEL_LANGUAGE_DIALOG_BEGIN',
DEL_LANGUAGE_DIALOG_END = 'DEL_LANGUAGE_DIALOG_END',
OPEN_RECOGNIZER_DROPDOWN = 'OPEN_RECOGNIZER_DROPDOWN',
CLOSE_RECOGNIZER_DROPDOWN = 'CLOSE_RECOGNIZER_DROPDOWN',
SET_QNA_UPDATE_STATUS = 'SET_QNA_UPDATE_STATUS',
}

export const Tips = {
Expand Down
Loading