Skip to content

Commit

Permalink
feat(qna): added "text_redirect" action type
Browse files Browse the repository at this point in the history
  • Loading branch information
slvnperron committed Jul 13, 2018
1 parent caab49d commit d8e6825
Show file tree
Hide file tree
Showing 4 changed files with 109 additions and 34 deletions.
34 changes: 26 additions & 8 deletions packages/functionals/botpress-qna/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ module.exports = {
const questionsToSave = typeof questions === 'string' ? parsers[`${format}Parse`](questions) : questions
return Promise.each(questionsToSave, question => storage.saveQuestion({ ...question, enabled: true }))
},

/**
* @async
* Fetches questions and represents them as json
Expand All @@ -56,15 +57,32 @@ module.exports = {
* @returns {Array.<{questions: Array, question: String, action: String, answer: String}>}
*/
async export({ flat = false } = {}) {
return (await storage.getQuestions()).flatMap(
({ data: { questions, answer: textAnswer, action, redirectFlow, redirectNode } }) => {
const answer = action === 'text' ? textAnswer : [redirectFlow, redirectNode].filter(Boolean).join('#')
if (!flat) {
return { questions, action, answer }
const qnas = await storage.getQuestions()

return qnas.flatMap(question => {
const { data } = question
const { questions, answer: textAnswer, action, redirectNode, redirectFlow } = data

let answer = textAnswer
let answer2 = null

if (action === 'redirect') {
answer = redirectFlow
if (redirectNode) {
answer += '#' + redirectNode
}
return questions.map(question => ({ question, action, answer }))
} else if (action === 'text_redirect') {
answer2 = redirectFlow
if (redirectNode) {
answer2 += '#' + redirectNode
}
}

if (!flat) {
return { questions, action, answer, answer2 }
}
)
return questions.map(question => ({ question, action, answer, answer2 }))
})
}
}

Expand Down Expand Up @@ -112,7 +130,7 @@ module.exports = {
router.get('/csv', async (req, res) => {
res.setHeader('Content-Type', 'text/csv')
res.setHeader('Content-disposition', `attachment; filename=qna_${moment().format('DD-MM-YYYY')}.csv`)
const json2csvParser = new Json2csvParser({ fields: ['question', 'action', 'answer'], header: false })
const json2csvParser = new Json2csvParser({ fields: ['question', 'action', 'answer', 'answer2'], header: true })
res.end(json2csvParser.parse(await bp.qna.export({ flat: true })))
})

Expand Down
11 changes: 8 additions & 3 deletions packages/functionals/botpress-qna/src/middleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,17 @@ export const processEvent = async (event, { bp, storage, logger, config }) => {
return false
}

if (data.action === 'text') {
if (data.action.includes('text')) {
logger.debug('QnA: replying to recognized question with plain text answer', id)
event.reply(config.textRenderer, { text: data.answer })
// return `true` to prevent further middlewares from capturing the message
return true
} else if (data.action === 'redirect') {

if (data.action === 'text') {
return true
}
}

if (data.action.includes('redirect')) {
logger.debug('QnA: replying to recognized question with redirect', id)
// TODO: This is used as the `stateId` by the bot template
// Not sure if it's universal enough for every use-case but
Expand Down
45 changes: 35 additions & 10 deletions packages/functionals/botpress-qna/src/parsers.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import get from 'lodash/get'
import parseCsvToJson from 'csv-parse/lib/sync'

const parseFlow = str => {
Expand All @@ -6,22 +7,46 @@ const parseFlow = str => {
}

export const jsonParse = jsonContent =>
jsonContent.map(({ questions, answer: instruction, action }) => {
if (!['text', 'redirect'].includes(action)) {
throw new Error('Failed to process CSV-row: action should be either "text" or "redirect"')
jsonContent.map(({ questions, answer: instruction, answer2, action }) => {
if (!['text', 'redirect', 'text_redirect'].includes(action)) {
throw new Error('Failed to process CSV-row: action should be either "text", "redirect" or "text_redirect"')
}
const answer = action === 'text' ? instruction : ''
const flowParams = action === 'redirect' ? parseFlow(instruction) : { redirectFlow: '', redirectNode: '' }
return { questions, action, answer, ...flowParams }

let redirectInstruction = null
let textAnswer = ''

if (action === 'text') {
textAnswer = instruction
} else if (action === 'redirect') {
redirectInstruction = instruction
} else if (action === 'text_redirect') {
textAnswer = instruction
redirectInstruction = answer2
}

const flowParams = redirectInstruction ? parseFlow(redirectInstruction) : { redirectFlow: '', redirectNode: '' }
return { questions, action, answer: textAnswer, ...flowParams }
})

export const csvParse = csvContent => {
const mergeRows = (acc, { question, answer, action }) => {
const mergeRows = (acc, { question, answer, answer2, action }) => {
const [prevRow] = acc.slice(-1)
if (prevRow && prevRow.answer === answer) {
const isSameAnswer = prevRow && (prevRow.answer === answer && (!answer2 || answer2 === prevRow.answer2))
if (isSameAnswer) {
return [...acc.slice(0, acc.length - 1), { ...prevRow, questions: [...prevRow.questions, question] }]
}
return [...acc, { answer, action, questions: [question] }]
return [...acc, { answer, answer2, action, questions: [question] }]
}
return jsonParse(parseCsvToJson(csvContent, { columns: ['question', 'action', 'answer'] }).reduce(mergeRows, []))

const rows = parseCsvToJson(csvContent, { columns: ['question', 'action', 'answer', 'answer2'] }).reduce(
mergeRows,
[]
)

// We trim the header if detected in the first row
if (get(rows, '0.action') === 'action') {
rows.splice(0, 1)
}

return jsonParse(rows)
}
53 changes: 40 additions & 13 deletions packages/functionals/botpress-qna/src/views/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import Select from 'react-select'
import classnames from 'classnames'
import find from 'lodash/find'
import some from 'lodash/some'
import get from 'lodash/get'

import ArrayEditor from './ArrayEditor'
import QuestionsEditor from './QuestionsEditor'
Expand Down Expand Up @@ -50,7 +51,8 @@ const cleanupQuestions = questions => questions.map(q => q.trim()).filter(Boolea

const ACTIONS = {
TEXT: 'text',
REDIRECT: 'redirect'
REDIRECT: 'redirect',
TEXT_REDIRECT: 'text_redirect'
}

export default class QnaAdmin extends Component {
Expand Down Expand Up @@ -163,6 +165,32 @@ export default class QnaAdmin extends Component {
!!cleanupQuestions(data.questions).length &&
(data.action === ACTIONS.TEXT ? !!data.answer : !!data.redirectFlow && !!data.redirectNode)

renderTextAndRedirectSelect(index, onChange) {
return (
<div>
{this.renderTextInput(index, onChange)}
{this.renderRedirectSelect(index, onChange)}
</div>
)
}

renderTextInput = (index, onChange) => {
const item = index === null ? 'newItem' : `items.${index}`
const answer = get(this.state, `${item}.data.answer`, '')

return (
<FormGroup controlId={this.getFormControlId(index, 'answer')}>
<ControlLabel>Answer:</ControlLabel>
<FormControl
componentClass="textarea"
placeholder="Answer"
value={answer}
onChange={this.onInputChange(index, 'answer', onChange)}
/>
</FormGroup>
)
}

renderRedirectSelect(index, onChange) {
const { flows } = this.state
if (!flows) {
Expand Down Expand Up @@ -299,21 +327,20 @@ export default class QnaAdmin extends Component {
>
redirect to flow node
</Radio>
<Radio
name={this.getFormControlId(index, 'action')}
value={ACTIONS.TEXT_REDIRECT}
checked={data.action === ACTIONS.TEXT_REDIRECT}
onChange={this.onInputChange(index, 'action', onChange)}
inline
>
text answer and redirect to flow node
</Radio>
</FormGroup>

{data.action === ACTIONS.TEXT && (
<FormGroup controlId={this.getFormControlId(index, 'answer')}>
<ControlLabel>Answer:</ControlLabel>
<FormControl
componentClass="textarea"
placeholder="Answer"
value={data.answer}
onChange={this.onInputChange(index, 'answer', onChange)}
/>
</FormGroup>
)}

{data.action === ACTIONS.TEXT && this.renderTextInput(index, onChange)}
{data.action === ACTIONS.REDIRECT && this.renderRedirectSelect(index, onChange)}
{data.action === ACTIONS.TEXT_REDIRECT && this.renderTextAndRedirectSelect(index, onChange)}

<ButtonToolbar>
<Button type="button" onClick={() => onReset(index)} disabled={!isDirty}>
Expand Down

0 comments on commit d8e6825

Please sign in to comment.