Skip to content

Commit

Permalink
fix(hitl2): allow multilang transfer/assign messages (#4428)
Browse files Browse the repository at this point in the history
* fix(hitl2): allow multilang transfer/assign messages

* make use of '@' prefix on content id

* add better jsdoc to the configs

* improved the way translated prop are extracted

* typo

* converted config examples into default values

* small fix
  • Loading branch information
laurentlp committed Jan 28, 2021
1 parent da35caa commit f5ff84a
Show file tree
Hide file tree
Showing 10 changed files with 66 additions and 42 deletions.
45 changes: 28 additions & 17 deletions modules/hitlnext/src/backend/api.ts
Expand Up @@ -6,7 +6,9 @@ import { Request, Response } from 'express'
import Joi from 'joi'
import _ from 'lodash'

import { Config } from '../config'
import { MODULE_NAME } from '../constants'
import { agentName } from '../helper'

import { HandoffType, IAgent, IComment, IHandoff } from './../types'
import { UnprocessableEntityError } from './errors'
Expand Down Expand Up @@ -157,6 +159,8 @@ export default async (bp: typeof sdk, state: StateType) => {
return res.sendStatus(200)
}

const configs: Config = await bp.config.getModuleConfigForBot(MODULE_NAME, req.params.botId)

const handoff = await repository.createHandoff(req.params.botId, payload).then(handoff => {
state.cacheHandoff(req.params.botId, handoff.userThreadId, handoff)
return handoff
Expand All @@ -169,14 +173,12 @@ export default async (bp: typeof sdk, state: StateType) => {
channel: handoff.userChannel
}

bp.events.replyToEvent(
eventDestination,
await bp.cms.renderElement(
'builtin_text',
{ type: 'text', text: 'You are being transfered to an agent.' },
eventDestination
if (configs.transferMessage) {
bp.events.replyToEvent(
eventDestination,
await bp.cms.renderElement('@builtin_text', { type: 'text', text: configs.transferMessage }, eventDestination)
)
)
}

realtime.sendPayload(req.params.botId, {
resource: 'handoff',
Expand Down Expand Up @@ -221,6 +223,7 @@ export default async (bp: typeof sdk, state: StateType) => {
const { email, strategy } = req.tokenUser!

const agentId = makeAgentId(strategy, email)
const agent = await repository.getCurrentAgent(req as BPRequest, req.params.botId, agentId)

let handoff = await repository.findHandoff(req.params.botId, req.params.id)

Expand All @@ -246,6 +249,8 @@ export default async (bp: typeof sdk, state: StateType) => {

await extendAgentSession(repository, realtime, req.params.botId, agentId)

const configs: Config = await bp.config.getModuleConfigForBot(MODULE_NAME, req.params.botId)

const baseCustomEventPayload: Partial<sdk.IO.EventCtorArgs> = {
botId: handoff.botId,
direction: 'outgoing',
Expand All @@ -256,17 +261,23 @@ export default async (bp: typeof sdk, state: StateType) => {
}
}

// custom event to user
await bp.events.sendEvent(
bp.IO.Event(
_.merge(_.cloneDeep(baseCustomEventPayload), {
target: handoff.userId,
threadId: handoff.userThreadId,
channel: handoff.userChannel,
payload: { from: 'bot', component: 'HandoffAssignedForUser' }
}) as sdk.IO.EventCtorArgs
const eventDestination = {
botId: req.params.botId,
target: handoff.userId,
threadId: handoff.userThreadId,
channel: handoff.userChannel
}

if (configs.assignMessage) {
bp.events.replyToEvent(
eventDestination,
await bp.cms.renderElement(
'@builtin_text',
{ type: 'text', text: configs.assignMessage, agentName: agentName(agent) },
eventDestination
)
)
)
}

const recentEvents = await bp.events.findEvents(
{ botId, threadId: handoff.userThreadId },
Expand Down
17 changes: 17 additions & 0 deletions modules/hitlnext/src/config.ts
Expand Up @@ -38,6 +38,23 @@ export interface Config {
* @default false
*/
enableConversationDeletion: boolean

/**
* @param transferMessage The message sent to the user when he is being transferred to an agent. E.g. ̀`{ "lang": "message"}`.
* @default { "en": "You are being transferred to an agent.", "fr": "Vous êtes transféré à un agent."}
*/
transferMessage: {
[Key: string]: string
}

/**
* @param assignMessage The message sent to the user when he has been assigned to an agent.
* @argument agentName It is possible to specify the agent name as an argument to the message. See the example below.
* @default { "en": "You have been assigned to our agent {{agentName}}.", "fr": "Vous avez été assigné à notre agent(e) {{agentName}}."}
*/
assignMessage: {
[Key: string]: string
}
}

export interface IShortcut {
Expand Down
@@ -1,4 +1,4 @@
import { IAgent } from '../../../types'
import { IAgent } from './types'

export function agentName(agent: IAgent) {
const { firstname, lastname } = agent.attributes || {}
Expand Down
Expand Up @@ -4,8 +4,8 @@ import _, { Dictionary } from 'lodash'
import React, { FC } from 'react'
import { Initial } from 'react-initial'

import { agentName } from '../../../../helper'
import { IAgent } from '../../../../types'
import { agentName } from '../../shared/helper'

import styles from './../../style.scss'

Expand Down
2 changes: 1 addition & 1 deletion modules/hitlnext/src/views/full/app/components/Comment.tsx
Expand Up @@ -2,8 +2,8 @@ import { ContentSection, lang } from 'botpress/shared'
import moment from 'moment'
import React, { FC, useContext } from 'react'

import { agentName } from '../../../../helper'
import { IComment } from '../../../../types'
import { agentName } from '../../shared/helper'
import style from '../../style.scss'
import { Context } from '../Store'

Expand Down
Expand Up @@ -6,8 +6,8 @@ import moment from 'moment'
import ms from 'ms'
import React, { FC, useContext, useEffect, useState } from 'react'

import { agentName } from '../../../../helper'
import { IHandoff } from '../../../../types'
import { agentName } from '../../shared/helper'
import style from '../../style.scss'
import { generateUsername, getOrSet } from '../utils'
import { Context } from '../Store'
Expand Down
@@ -1,8 +1,8 @@
import { lang } from 'botpress/shared'
import React, { FC } from 'react'

import { agentName } from '../../../../helper'
import { IAgent } from '../../../../types'
import { agentName } from '../../shared/helper'

const AgentItem: FC<IAgent> = props => {
return (
Expand Down
12 changes: 0 additions & 12 deletions modules/hitlnext/src/views/lite/HandoffAssigned.tsx
Expand Up @@ -28,15 +28,3 @@ export const HandoffAssignedForAgent = props => {
</Fragment>
)
}

export const HandoffAssignedForUser = () => {
const [isLangInit, setLangInit] = useState(false)

useEffect(() => {
// tslint:disable-next-line: no-floating-promises
initLang().then(() => setLangInit(true))
}, [])

// TODO render agent name
return <Fragment>{isLangInit && <span>{lang.tr('module.hitlnext.handoff.assignedToAgent')}</span>}</Fragment>
}
4 changes: 2 additions & 2 deletions modules/hitlnext/src/views/lite/index.jsx
@@ -1,12 +1,12 @@
import React from 'react'

import ShortcutComposer from './ShortcutComposer'
import { HandoffAssignedForAgent, HandoffAssignedForUser } from './HandoffAssigned'
import { HandoffAssignedForAgent } from './HandoffAssigned'

export class LiteView extends React.Component {
render() {
return null
}
}

export { ShortcutComposer, HandoffAssignedForAgent, HandoffAssignedForUser }
export { ShortcutComposer, HandoffAssignedForAgent }
20 changes: 14 additions & 6 deletions src/bp/core/services/cms.ts
Expand Up @@ -580,11 +580,19 @@ export class CMSService implements IDisposeOnExit {
private getOriginalProps(formData: object, contentType: ContentType, lang: string, defaultLang?: string) {
const originalProps = Object.keys(_.get(contentType, 'jsonSchema.properties'))

// When data is accessible through a single key containing the '$' separator. e.g. { 'text$en': '...' }
const separatorExtraction = (prop: string) =>
formData[`${prop}$${lang}`] || (defaultLang && formData[`${prop}$${defaultLang}`])

// When data is accessible through keys of a nested dictionary. e.g. { 'text': { 'en': '...' } }
const nestedDictExtraction = (prop: string) =>
formData[prop] && (formData[prop][lang] || (defaultLang && formData[prop][defaultLang]))

if (originalProps) {
return originalProps.reduce((result, key) => {
result[key] = formData[`${key}$${lang}`] || (defaultLang && formData[`${key}$${defaultLang}`])
return result
}, {})
return originalProps.reduce(
(result, prop) => ((result[prop] = separatorExtraction(prop) || nestedDictExtraction(prop)), result),
{}
)
} else {
return formData
}
Expand All @@ -610,9 +618,9 @@ export class CMSService implements IDisposeOnExit {

const translateFormData = async (formData: object): Promise<object> => {
const defaultLang = (await this.configProvider.getBotConfig(eventDestination.botId)).defaultLanguage
const lang = _.get(args, 'event.state.user.language')
const userLang = _.get(args, 'event.state.user.language')

return this.getOriginalProps(formData, contentTypeRenderer, lang, defaultLang)
return this.getOriginalProps(formData, contentTypeRenderer, userLang, defaultLang)
}

if (contentId.startsWith('!')) {
Expand Down

0 comments on commit f5ff84a

Please sign in to comment.