Skip to content

Commit

Permalink
fix(nlu): no more weird debounce that override other intents (#4317)
Browse files Browse the repository at this point in the history
* fix(nlu): no more weird debounce that override other intents

* added typings in all nlu frontend
  • Loading branch information
franklevasseur committed Dec 16, 2020
1 parent 50bbaf2 commit 632c6ac
Show file tree
Hide file tree
Showing 15 changed files with 148 additions and 102 deletions.
51 changes: 19 additions & 32 deletions modules/nlu/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,45 +2,32 @@ import axios, { AxiosInstance } from 'axios'
import { NLU } from 'botpress/sdk'
import * as sdk from 'botpress/sdk'

export interface NLUApi {
fetchContexts: () => Promise<string[]>
fetchIntentsWithQNAs: () => Promise<NLU.IntentDefinition[]>
fetchIntents: () => Promise<NLU.IntentDefinition[]>
fetchIntent: (x: string) => Promise<NLU.IntentDefinition>
createIntent: (x: Partial<NLU.IntentDefinition>) => Promise<any>
updateIntent: (targetIntent: string, intent: Partial<NLU.IntentDefinition>, updateTopics?: boolean) => Promise<any>
syncIntentTopics: (intentNames?: string[]) => Promise<void>
deleteIntent: (x: string) => Promise<any>
fetchEntities: () => Promise<NLU.EntityDefinition[]>
fetchEntity: (x: string) => Promise<NLU.EntityDefinition>
createEntity: (x: NLU.EntityDefinition) => Promise<any>
updateEntity: (targetEntityId: string, x: NLU.EntityDefinition) => Promise<any>
deleteEntity: (x: string) => Promise<any>
train: () => Promise<void>
cancelTraining: () => Promise<void>
}
export type NLUApi = ReturnType<typeof makeApi>

export const makeApi = (bp: { axios: AxiosInstance }): NLUApi => ({
fetchContexts: () => bp.axios.get('/nlu/contexts').then(res => res.data),
fetchIntentsWithQNAs: () => bp.axios.get('/nlu/intents').then(res => res.data),
fetchIntents: async () => {
export const makeApi = (bp: { axios: AxiosInstance }) => ({
fetchContexts: (): Promise<string[]> => bp.axios.get('/nlu/contexts').then(res => res.data),
fetchIntentsWithQNAs: (): Promise<NLU.IntentDefinition[]> => bp.axios.get('/nlu/intents').then(res => res.data),
fetchIntents: async (): Promise<NLU.IntentDefinition[]> => {
const { data } = await bp.axios.get('/nlu/intents')
return data.filter(x => !x.name.startsWith('__qna__'))
},
fetchIntent: (intentName: string) => bp.axios.get(`/nlu/intents/${intentName}`).then(res => res.data),
fetchIntent: (intentName: string): Promise<NLU.IntentDefinition> =>
bp.axios.get(`/nlu/intents/${intentName}`).then(res => res.data),
createIntent: (intent: Partial<NLU.IntentDefinition>) => bp.axios.post('/nlu/intents', intent),
updateIntent: (targetIntent: string, intent: Partial<NLU.IntentDefinition>) =>
updateIntent: (targetIntent: string, intent: Partial<NLU.IntentDefinition>): Promise<void> =>
bp.axios.post(`/nlu/intents/${targetIntent}`, intent),
deleteIntent: (name: string) => bp.axios.post(`/nlu/intents/${name}/delete`),
syncIntentTopics: (intentNames?: string[]) => bp.axios.post('/nlu/sync/intents/topics', { intentNames }),
fetchEntities: () => bp.axios.get('/nlu/entities').then(res => res.data),
fetchEntity: (entityName: string) => bp.axios.get(`/nlu/entities/${entityName}`).then(res => res.data),
createEntity: (entity: NLU.EntityDefinition) => bp.axios.post('/nlu/entities/', entity),
updateEntity: (targetEntityId: string, entity: NLU.EntityDefinition) =>
deleteIntent: (name: string): Promise<void> => bp.axios.post(`/nlu/intents/${name}/delete`),
syncIntentTopics: (intentNames?: string[]): Promise<void> =>
bp.axios.post('/nlu/sync/intents/topics', { intentNames }),
fetchEntities: (): Promise<NLU.EntityDefinition[]> => bp.axios.get('/nlu/entities').then(res => res.data),
fetchEntity: (entityName: string): Promise<NLU.EntityDefinition> =>
bp.axios.get(`/nlu/entities/${entityName}`).then(res => res.data),
createEntity: (entity: NLU.EntityDefinition): Promise<void> => bp.axios.post('/nlu/entities/', entity),
updateEntity: (targetEntityId: string, entity: NLU.EntityDefinition): Promise<void> =>
bp.axios.post(`/nlu/entities/${targetEntityId}`, entity),
deleteEntity: (entityId: string) => bp.axios.post(`/nlu/entities/${entityId}/delete`),
train: () => bp.axios.post('/mod/nlu/train'),
cancelTraining: () => bp.axios.post('/mod/nlu/train/delete')
deleteEntity: (entityId: string): Promise<void> => bp.axios.post(`/nlu/entities/${entityId}/delete`),
train: (): Promise<void> => bp.axios.post('/mod/nlu/train'),
cancelTraining: (): Promise<void> => bp.axios.post('/mod/nlu/train/delete')
})

export const createApi = async (bp: typeof sdk, botId: string) => {
Expand Down
8 changes: 1 addition & 7 deletions modules/nlu/src/views/full/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -146,13 +146,7 @@ const NLU: FC<Props> = props => {
/>
)}
{!!intents.length && currentItem && currentItem.type === 'intent' && (
<IntentEditor
intent={currentItem.name}
api={api}
contentLang={props.contentLang}
showSlotPanel
axios={props.bp.axios} // to be removed for api, requires a lot of refactoring
/>
<IntentEditor intent={currentItem.name} api={api} contentLang={props.contentLang} showSlotPanel />
)}
{currentItem && currentItem.type === 'entity' && (
<EntityEditor
Expand Down
23 changes: 11 additions & 12 deletions modules/nlu/src/views/full/intents/FullEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { NLU } from 'botpress/sdk'
import { utils } from 'botpress/shared'
import cx from 'classnames'
import _ from 'lodash'
import React, { FC, useEffect, useState } from 'react'
import React, { FC, useEffect, useRef, useState } from 'react'

import { NLUApi } from '../../../api'

Expand All @@ -19,19 +19,24 @@ interface Props {
api: NLUApi
contentLang: string
showSlotPanel?: boolean
axios: AxiosInstance
liteEditor?: boolean
}

export const IntentEditor: FC<Props> = props => {
const [intent, setIntent] = useState<NLU.IntentDefinition>()

const debouncedApiSaveIntent = useRef(
_.debounce((newIntent: NLU.IntentDefinition) => props.api.createIntent(newIntent), 2500)
)

useEffect(() => {
// tslint:disable-next-line: no-floating-promises
props.api.fetchIntent(props.intent).then(intent => {
setIntent(intent)
utils.inspect(intent)
})

return () => debouncedApiSaveIntent.current.flush()
}, [props.intent])

if (!intent) {
Expand All @@ -47,7 +52,8 @@ export const IntentEditor: FC<Props> = props => {

const handleUtterancesChange = (newUtterances: string[]) => {
const newIntent = { ...intent, utterances: { ...intent.utterances, [props.contentLang]: newUtterances } }
saveIntent(newIntent)
setIntent(newIntent)
debouncedApiSaveIntent.current(newIntent)
}

const handleSlotsChange = (slots: NLU.SlotDefinition[], { operation, name, oldName }) => {
Expand Down Expand Up @@ -75,12 +81,7 @@ export const IntentEditor: FC<Props> = props => {
api={props.api}
/>
)}
<IntentHint
intent={intent}
liteEditor={props.liteEditor}
contentLang={props.contentLang}
axios={props.axios}
/>
<IntentHint intent={intent} liteEditor={props.liteEditor} contentLang={props.contentLang} />
</div>
<UtterancesEditor
intentName={intent.name}
Expand All @@ -89,9 +90,7 @@ export const IntentEditor: FC<Props> = props => {
slots={intent.slots}
/>
</div>
{props.showSlotPanel && (
<Slots slots={intent.slots} api={props.api} axios={props.axios} onSlotsChanged={handleSlotsChange} />
)}
{props.showSlotPanel && <Slots slots={intent.slots} api={props.api} onSlotsChanged={handleSlotsChange} />}
</div>
)
}
1 change: 0 additions & 1 deletion modules/nlu/src/views/full/intents/IntentHint.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import style from './style.scss'
interface Props {
intent: sdk.NLU.IntentDefinition
contentLang: string
axios: AxiosInstance
liteEditor: boolean
}

Expand Down
10 changes: 1 addition & 9 deletions modules/nlu/src/views/full/intents/LiteEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,15 +92,7 @@ export const LiteEditor: FC<Props> = props => {
onSubmit={createIntent}
title={lang.tr('module.nlu.intents.createLabel')}
/>
{currentIntent && (
<IntentEditor
liteEditor
intent={currentIntent}
api={api}
contentLang={props.contentLang}
axios={props.bp.axios} // to be removed for api, requires a lot of refactoring
/>
)}
{currentIntent && <IntentEditor liteEditor intent={currentIntent} api={api} contentLang={props.contentLang} />}

<div className={style.chooseContainer}>
<ControlGroup>
Expand Down
45 changes: 30 additions & 15 deletions modules/nlu/src/views/full/intents/UtterancesEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,20 @@ interface Props {
onChange: (x: string[]) => void
}

export class UtterancesEditor extends React.Component<Props> {
interface Selected {
utterance: number
block: number
from: number
to: number
}

interface State {
selection: Selected
value: Value
showSlotMenu: boolean
}

export class UtterancesEditor extends React.Component<Props, State> {
state = {
selection: { utterance: -1, block: -1, from: -1, to: -1 },
value: utterancesToValue([]),
Expand Down Expand Up @@ -109,7 +122,6 @@ export class UtterancesEditor extends React.Component<Props> {
}

render() {

return (
<Editor
value={this.state.value}
Expand Down Expand Up @@ -139,7 +151,9 @@ export class UtterancesEditor extends React.Component<Props> {
show={this.state.showSlotMenu}
onSlotClicked={this.tag.bind(this, editor)}
/>
<div className={style.utterances} onCopy={this.onCopy}>{children}</div>
<div className={style.utterances} onCopy={this.onCopy}>
{children}
</div>
</div>
)
}
Expand All @@ -152,9 +166,10 @@ export class UtterancesEditor extends React.Component<Props> {
lines = valueToUtterances(this.state.value)
} else {
// Partial selection, we remove the heading numbers and empty lines
lines = selection.split('\n').map(txt =>
txt.replace(/^\d{1,4}$/, '')
).filter(x => x.length)
lines = selection
.split('\n')
.map(txt => txt.replace(/^\d{1,4}$/, ''))
.filter(x => x.length)
}

event.clipboardData.setData('text/plain', lines.join('\n'))
Expand Down Expand Up @@ -193,23 +208,24 @@ export class UtterancesEditor extends React.Component<Props> {
}
}

dispatchChanges = _.debounce(value => {
dispatchChanges = (value: Value) => {
this.props.onChange(valueToUtterances(value))
}, 2500)
}

dispatchNeeded = operations => {
return operations
.map(x => x.get('type'))
.filter(x => ['insert_text', 'remove_text', 'add_mark', 'remove_mark', 'split_node'].includes(x)).size
}

onChange = ({ value, operations }) => {
let selectionState = {}
onChange = ({ value, operations }: { value: Value; operations: any }) => {
let selection: Selected | undefined
if (operations.filter(x => x.get('type') === 'set_selection').size) {
selectionState = this.onSelectionChanged(value)
selection = this.onSelectionChanged(value)
}

this.setState({ value, ...selectionState })
const newState: Partial<State> = selection ? { value, selection } : { value }
this.setState(newState as State)

if (this.dispatchNeeded(operations)) {
this.dispatchChanges(value)
Expand Down Expand Up @@ -289,7 +305,7 @@ export class UtterancesEditor extends React.Component<Props> {
)
}

onSelectionChanged = (value: Value) => {
onSelectionChanged = (value: Value): Selected => {
const selection: Selection = value.get('selection').toJS()

let from = -1
Expand All @@ -302,7 +318,6 @@ export class UtterancesEditor extends React.Component<Props> {
from = Math.min(selection.anchor.offset, selection.focus.offset)
to = Math.max(selection.anchor.offset, selection.focus.offset)


if (from !== to) {
if (selection.isFocused) {
this.showSlotPopover()
Expand All @@ -318,6 +333,6 @@ export class UtterancesEditor extends React.Component<Props> {
this.hideSlotPopover()
}

return { selection: { utterance, block, from, to } }
return { utterance, block, from, to }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ interface EntityOption {
type: string
name: string
}

interface Props {
entities: string[]
api: NLUApi
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
import React from 'react'
import style from '../style.scss'
import { Tag } from '@blueprintjs/core'
import { NLU } from 'botpress/sdk'
import { confirmDialog, lang } from 'botpress/shared'
import React from 'react'

import style from '../style.scss'

interface State {}
interface Props {
key: string
slot: NLU.SlotDefinition
onDelete: (slot: NLU.SlotDefinition) => void
onEdit: (slot: NLU.SlotDefinition) => void
}

export default class SlotItem extends React.Component {
export default class SlotItem extends React.Component<Props, State> {
handleDeleteClicked = async e => {
e.preventDefault()

Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,43 @@
import React from 'react'
import nanoid from 'nanoid'
import { Button, Classes, Dialog, FormGroup, Intent } from '@blueprintjs/core'
import { NLU } from 'botpress/sdk'
import { lang } from 'botpress/shared'
import _ from 'lodash'
import random from 'lodash/random'
import nanoid from 'nanoid'
import React from 'react'

import { NLUApi } from '../../../../api'

import { Dialog, Button, Classes, FormGroup, Intent } from '@blueprintjs/core'
import { SlotOperation } from './typings'
import { EntitySelector } from './EntitySelector'
import { lang } from 'botpress/shared'

const N_COLORS = 12
const INITIAL_STATE = {
const INITIAL_STATE: State = {
id: null,
name: '',
entities: [],
editing: false,
color: false
color: 0
}

interface State extends NLU.SlotDefinition {
id: null | string
name: string
entities: string[]
editing: boolean
color: number
}

export default class SlotModal extends React.Component {
interface Props {
api: NLUApi
slot: NLU.SlotDefinition
slots: NLU.SlotDefinition[]
show: boolean
onSlotSave: (slot: NLU.SlotDefinition, operation: SlotOperation) => void
onHide: () => void
}

export default class SlotModal extends React.Component<Props, State> {
nameInput = null
state = { ...INITIAL_STATE }

Expand All @@ -40,15 +62,17 @@ export default class SlotModal extends React.Component {
initializeFromProps = () => {
if (this.props.slot) {
this.setState({ ...this.props.slot, editing: true })
} else this.resetState()
} else {
this.resetState()
}
}

resetState = () => this.setState({ ...INITIAL_STATE })

getNextAvailableColor = () => {
const maxColor = _.get(_.maxBy(this.props.slots, 'color'), 'color') || 0

//if no more colors available, we return a random color
// if no more colors available, we return a random color
return maxColor <= N_COLORS ? maxColor + 1 : random(1, N_COLORS)
}

Expand Down Expand Up @@ -84,7 +108,7 @@ export default class SlotModal extends React.Component {
<div className={Classes.DIALOG_BODY}>
<FormGroup label={lang.tr('name')}>
<input
tabIndex="1"
tabIndex={1}
ref={el => (this.nameInput = el)}
className={`${Classes.INPUT} ${Classes.FILL}`}
value={this.state.name}
Expand Down

0 comments on commit 632c6ac

Please sign in to comment.