Skip to content

Commit

Permalink
feat(skills): modules can register multiple skills
Browse files Browse the repository at this point in the history
  • Loading branch information
allardy committed Nov 8, 2018
1 parent a372f19 commit 8012f1f
Show file tree
Hide file tree
Showing 10 changed files with 282 additions and 276 deletions.
16 changes: 6 additions & 10 deletions modules/skill-choice/package.json
Expand Up @@ -4,10 +4,9 @@
"description": "Botpress Skill that allows users to pick an option",
"main": "dist/backend/index.js",
"botpress": {
"menuText": "Choice",
"menuIcon": "view_module",
"webBundle": "bin/web.bundle.js",
"noInterface": true
"liteViews": {
"skill-choice": "./src/views/skill-choice.jsx"
}
},
"webpack": {
"externals": {
Expand All @@ -21,19 +20,16 @@
"build": "./node_modules/.bin/module-builder build",
"watch": "./node_modules/.bin/module-builder watch"
},
"publishConfig": {
"access": "public"
},
"author": "Botpress Team",
"license": "AGPL-3.0-only",
"devDependencies": {
"@types/node": "^10.11.0",
"classnames": "^2.2.5",
"react-sortable-hoc": "^0.6.8",
"react-tag-input": "^5.0.2"
"react-tag-input": "^5.0.2",
"module-builder": "../../build/module-builder"
},
"dependencies": {
"bluebird": "^3.4.6",
"module-builder": "../../build/module-builder"
"bluebird": "^3.4.6"
}
}
13 changes: 7 additions & 6 deletions modules/skill-choice/src/backend/index.ts
Expand Up @@ -17,19 +17,20 @@ const onServerReady = async (bp: SDK) => {
await setup(bp)
}

// TODO: One module can export multiple skills. Many changes in Studio
const flowGenerator: sdk.FlowGenerator[] = [
const skillsToRegister: sdk.Skill[] = [
{
flowName: 'skill-choice',
generator: skill_choice.generateFlow
id: 'skill-choice',
name: 'Choice',
flowGenerator: skill_choice.generateFlow
}
]

const serveFile = async (filePath: string): Promise<Buffer> => {
filePath = filePath.toLowerCase()

const mapping = {
'index.js': path.join(__dirname, '../web/web.bundle.js')
'index.js': path.join(__dirname, '../web/web.bundle.js'),
'skill-choice.js': path.join(__dirname, '../web/skill-choice.bundle.js')
}

// Web views
Expand Down Expand Up @@ -85,7 +86,7 @@ const entryPoint: sdk.ModuleEntryPoint = {
plugins: [],
moduleView: { stretched: true }
},
flowGenerator
skills: skillsToRegister
}

export default entryPoint
231 changes: 1 addition & 230 deletions modules/skill-choice/src/views/index.jsx
@@ -1,236 +1,7 @@
import React from 'react'
import _ from 'lodash'

import { Alert, Tabs, Tab } from 'react-bootstrap'

import { WithContext as ReactTags } from 'react-tag-input'

import ContentPickerWidget from 'botpress/content-picker'

import style from './style.scss'

export default class TemplateModule extends React.Component {
state = {
keywords: {},
contentId: '',
config: {}
}

componentDidMount() {
this.props.resizeBuilderWindow && this.props.resizeBuilderWindow('small')
const getOrDefault = (propsKey, stateKey) => this.props.initialData[propsKey] || this.state[stateKey]

if (this.props.initialData) {
this.setState(
{
contentId: getOrDefault('contentId', 'contentId'),
keywords: getOrDefault('keywords', 'keywords'),
config: getOrDefault('config', 'config')
},
() => this.refreshContent()
)
}

this.fetchDefaultConfig()
}

async refreshContent() {
const id = this.state.contentId
const res = await this.props.bp.axios.get(`/api/content/items/${id}`)
return this.onContentChanged(res.data, true)
}

componentDidUpdate() {
this.updateParent()
}

updateParent = () => {
this.props.onDataChanged &&
this.props.onDataChanged({
contentId: this.state.contentId,
keywords: this.state.keywords,
config: this.state.config
})
if (this.choices && this.choices.length > 1) {
this.props.onValidChanged && this.props.onValidChanged(true)
}
}

fetchDefaultConfig = async () => {
const res = await this.props.bp.axios.get('/mod/skill-choice/config')
this.setState({ defaultConfig: res.data })
}

onMaxRetriesChanged = event => {
const config = { ...this.state.config, nbMaxRetries: isNaN(event.target.value) ? 1 : event.target.value }
this.setState({ config })
}

onBlocNameChanged = key => event => {
let blocName = event.target.value

if (!blocName.startsWith('#')) {
blocName = '#' + blocName
}

this.setState({ [key]: blocName })
}

onNameOfQuestionBlocChanged = this.onBlocNameChanged('nameOfQuestionBloc')
onNameOfInvalidBlocChanged = this.onBlocNameChanged('nameOfInvalidBloc')

onContentChanged = (element, force = false) => {
if (element && (force || element.id !== this.state.contentId)) {
this.choices = _.get(element, 'formData.choices') || [] //CHANGED
const initialKeywords = element.id === this.state.contentId ? this.state.keywords : {}
const keywords = this.choices.reduce((acc, v) => {
if (!acc[v.value]) {
acc[v.value] = _.uniq([v.value, v.title])
}
return acc
}, initialKeywords)
this.setState({ contentId: element.id, keywords: keywords })
}
}

handleMatchAddition = choiceValue => tag => {
const newTags = [...(this.state.keywords[choiceValue] || []), tag.text]
const keywords = { ...this.state.keywords, [choiceValue]: newTags }
this.setState({ keywords: keywords })
}

handleMatchDeletion = choiceValue => index => {
const newTags = this.state.keywords[choiceValue] || []
_.pullAt(newTags, index)
const keywords = { ...this.state.keywords, [choiceValue]: newTags }
this.setState({ keywords: keywords })
}

renderMatchingSection() {
return this.choices.map(choice => {
const keywordsEntry = this.state.keywords[choice.value] || []
const tags = keywordsEntry.map(x => ({ id: x, text: x }))
return (
<div className={style.keywords}>
<h4>
{choice.title} <small>({choice.value})</small>
</h4>
<ReactTags
inline
tags={tags}
suggestions={[]}
handleDelete={this.handleMatchDeletion(choice.value)}
handleAddition={this.handleMatchAddition(choice.value)}
/>
</div>
)
})
}

renderBasic() {
const matchingSection =
this.choices && this.choices.length ? (
this.renderMatchingSection()
) : (
<Alert bsStyle="warning">No choices available. Pick a content element that contains choices.</Alert>
)

const contentPickerProps = {}
const contentElement = this.getContentElement()
if (contentElement && contentElement.length) {
contentPickerProps.categoryId = contentElement
}

return (
<div className={style.content}>
<p>
<strong>Change the question and choices</strong>
</p>
<div>
<ContentPickerWidget
{...contentPickerProps}
itemId={this.state.contentId}
onChange={this.onContentChanged}
placeholder="Pick content (question and choices)"
/>
</div>
<p>
<strong>Define how choices are matched</strong>
</p>
<div>{matchingSection}</div>
</div>
)
}

getContentElement() {
return typeof this.state.config.contentElement === 'string'
? this.state.config.contentElement
: this.state.defaultConfig && this.state.defaultConfig.defaultContentElement
}

getNbRetries() {
return (
this.state.config.nbMaxRetries || (this.state.defaultConfig && this.state.defaultConfig.defaultMaxAttempts) || 0
)
}

getInvalidText() {
return this.state.config.invalidText || ''
}

handleConfigTextChanged = name => event => {
const config = { ...this.state.config, [name]: event.target.value }
this.setState({ config })
}

renderAdvanced() {
return (
<div className={style.content}>
<div>
<label htmlFor="inputMaxRetries">Max number of retries</label>
<input
id="inputMaxRetries"
type="number"
name="quantity"
min="0"
max="1000"
value={this.getNbRetries()}
onChange={this.onMaxRetriesChanged}
/>
</div>
<div>
<label htmlFor="invalidText">On invalid choice, say this instead of repeating question:</label>
<div>
<textarea
id="invalidText"
value={this.getInvalidText()}
onChange={this.handleConfigTextChanged('invalidText')}
/>
</div>
</div>
<div>
<label htmlFor="contentElementName">Content Element to use:</label>
<input
id="contentElementName"
type="text"
value={this.getContentElement()}
onChange={this.handleConfigTextChanged('contentElement')}
/>
</div>
</div>
)
}

render() {
return (
<Tabs defaultActiveKey={1} id="add-option-skill-tabs" animation={false}>
<Tab eventKey={1} title="Basic">
{this.renderBasic()}
</Tab>
<Tab eventKey={2} title="Advanced">
{this.renderAdvanced()}
</Tab>
</Tabs>
)
return null
}
}

0 comments on commit 8012f1f

Please sign in to comment.