2
0

⚗️ Implement bot v2 MVP (#194)

Closes #190
This commit is contained in:
Baptiste Arnaud
2022-12-22 17:02:34 +01:00
committed by GitHub
parent e55823e011
commit 1a3869ae6d
202 changed files with 8060 additions and 1152 deletions

View File

@@ -1,11 +1,16 @@
import { validateButtonInput } from '@/features/blocks/inputs/buttons/api'
import { validateEmail } from '@/features/blocks/inputs/email/api'
import { validatePhoneNumber } from '@/features/blocks/inputs/phone/api'
import {
formatPhoneNumber,
validatePhoneNumber,
} from '@/features/blocks/inputs/phone/api'
import { validateUrl } from '@/features/blocks/inputs/url/api'
import { parseVariables } from '@/features/variables'
import prisma from '@/lib/prisma'
import { TRPCError } from '@trpc/server'
import {
Block,
BlockType,
BubbleBlockType,
ChatReply,
InputBlock,
@@ -20,7 +25,7 @@ import { getNextGroup } from './getNextGroup'
export const continueBotFlow =
(state: SessionState) =>
async (
reply: string
reply?: string
): Promise<ChatReply & { newSessionState?: SessionState }> => {
const group = state.typebot.groups.find(
(group) => group.id === state.currentBlock?.groupId
@@ -30,7 +35,7 @@ export const continueBotFlow =
(block) => block.id === state.currentBlock?.blockId
) ?? -1
const block = blockIndex > 0 ? group?.blocks[blockIndex ?? 0] : null
const block = blockIndex >= 0 ? group?.blocks[blockIndex ?? 0] : null
if (!block || !group)
throw new TRPCError({
@@ -44,9 +49,15 @@ export const continueBotFlow =
message: 'Current block is not an input block',
})
if (!isInputValid(reply, block)) return parseRetryMessage(block)
const formattedReply = formatReply(reply, block.type)
const newVariables = await processAndSaveAnswer(state, block)(reply)
if (!formattedReply || !isReplyValid(formattedReply, block))
return parseRetryMessage(block)
const newVariables = await processAndSaveAnswer(
state,
block
)(formattedReply)
const newSessionState = {
...state,
@@ -58,15 +69,15 @@ export const continueBotFlow =
const groupHasMoreBlocks = blockIndex < group.blocks.length - 1
if (groupHasMoreBlocks) {
const nextEdgeId = getOutgoingEdgeId(newSessionState)(block, formattedReply)
if (groupHasMoreBlocks && !nextEdgeId) {
return executeGroup(newSessionState)({
...group,
blocks: group.blocks.slice(blockIndex + 1),
})
}
const nextEdgeId = block.outgoingEdgeId
if (!nextEdgeId && state.linkedTypebots.queue.length === 0)
return { messages: [] }
@@ -80,7 +91,7 @@ export const continueBotFlow =
const processAndSaveAnswer =
(state: Pick<SessionState, 'result' | 'typebot'>, block: InputBlock) =>
async (reply: string): Promise<Variable[]> => {
await saveAnswer(state.result.id, block)(reply)
state.result && (await saveAnswer(state.result.id, block)(reply))
const newVariables = saveVariableValueIfAny(state, block)(reply)
return newVariables
}
@@ -105,22 +116,26 @@ const saveVariableValueIfAny =
]
}
const parseRetryMessage = (block: InputBlock) => ({
messages: [
{
type: BubbleBlockType.TEXT,
content: {
plainText:
'retryMessageContent' in block.options
? block.options.retryMessageContent
: 'Invalid message. Please, try again.',
richText: [],
html: '',
const parseRetryMessage = (
block: InputBlock
): Pick<ChatReply, 'messages' | 'input'> => {
const retryMessage =
'retryMessageContent' in block.options && block.options.retryMessageContent
? block.options.retryMessageContent
: 'Invalid message. Please, try again.'
return {
messages: [
{
type: BubbleBlockType.TEXT,
content: {
plainText: retryMessage,
html: `<div>${retryMessage}</div>`,
},
},
},
],
input: block,
})
],
input: block,
}
}
const saveAnswer =
(resultId: string, block: InputBlock) => async (reply: string) => {
@@ -135,7 +150,35 @@ const saveAnswer =
})
}
export const isInputValid = (inputValue: string, block: Block): boolean => {
const getOutgoingEdgeId =
({ typebot: { variables } }: Pick<SessionState, 'typebot'>) =>
(block: InputBlock, reply?: string) => {
if (
block.type === InputBlockType.CHOICE &&
!block.options.isMultipleChoice &&
reply
) {
const matchedItem = block.items.find(
(item) => parseVariables(variables)(item.content) === reply
)
if (matchedItem?.outgoingEdgeId) return matchedItem.outgoingEdgeId
}
return block.outgoingEdgeId
}
export const formatReply = (
inputValue: string | undefined,
blockType: BlockType
): string | null => {
if (!inputValue) return null
switch (blockType) {
case InputBlockType.PHONE:
return formatPhoneNumber(inputValue)
}
return inputValue
}
export const isReplyValid = (inputValue: string, block: Block): boolean => {
switch (block.type) {
case InputBlockType.EMAIL:
return validateEmail(inputValue)

View File

@@ -2,13 +2,17 @@ import { parseVariables } from '@/features/variables'
import {
BubbleBlock,
BubbleBlockType,
ChatMessageContent,
ChatMessage,
ChatReply,
Group,
InputBlock,
InputBlockType,
RuntimeOptions,
SessionState,
} from 'models'
import {
isBubbleBlock,
isDefined,
isInputBlock,
isIntegrationBlock,
isLogicBlock,
@@ -16,6 +20,7 @@ import {
import { executeLogic } from './executeLogic'
import { getNextGroup } from './getNextGroup'
import { executeIntegration } from './executeIntegration'
import { computePaymentInputRuntimeOptions } from '@/features/blocks/inputs/payment/api'
export const executeGroup =
(state: SessionState, currentReply?: ChatReply) =>
@@ -33,17 +38,20 @@ export const executeGroup =
nextEdgeId = block.outgoingEdgeId
if (isBubbleBlock(block)) {
messages.push({
type: block.type,
content: parseBubbleBlockContent(newSessionState)(block),
})
messages.push(parseBubbleBlockContent(newSessionState)(block))
continue
}
if (isInputBlock(block))
return {
messages,
input: block,
input: {
...block,
runtimeOptions: await computeRuntimeOptions(newSessionState)(block),
prefilledValue: getPrefilledInputValue(
newSessionState.typebot.variables
)(block),
},
newSessionState: {
...newSessionState,
currentBlock: {
@@ -53,9 +61,9 @@ export const executeGroup =
},
}
const executionResponse = isLogicBlock(block)
? await executeLogic(state)(block)
? await executeLogic(newSessionState)(block)
: isIntegrationBlock(block)
? await executeIntegration(state)(block)
? await executeIntegration(newSessionState)(block)
: null
if (!executionResponse) continue
@@ -84,30 +92,50 @@ export const executeGroup =
)
}
const computeRuntimeOptions =
(state: SessionState) =>
(block: InputBlock): Promise<RuntimeOptions> | undefined => {
switch (block.type) {
case InputBlockType.PAYMENT: {
return computePaymentInputRuntimeOptions(state)(block.options)
}
}
}
const parseBubbleBlockContent =
({ typebot: { variables } }: SessionState) =>
(block: BubbleBlock): ChatMessageContent => {
(block: BubbleBlock): ChatMessage => {
switch (block.type) {
case BubbleBlockType.TEXT: {
const plainText = parseVariables(variables)(block.content.plainText)
const html = parseVariables(variables)(block.content.html)
return { plainText, html }
return { type: block.type, content: { plainText, html } }
}
case BubbleBlockType.IMAGE: {
const url = parseVariables(variables)(block.content.url)
return { url }
return { type: block.type, content: { ...block.content, url } }
}
case BubbleBlockType.VIDEO: {
const url = parseVariables(variables)(block.content.url)
return { url }
return { type: block.type, content: { ...block.content, url } }
}
case BubbleBlockType.AUDIO: {
const url = parseVariables(variables)(block.content.url)
return { url }
return { type: block.type, content: { ...block.content, url } }
}
case BubbleBlockType.EMBED: {
const url = parseVariables(variables)(block.content.url)
return { url }
return { type: block.type, content: { ...block.content, url } }
}
}
}
const getPrefilledInputValue =
(variables: SessionState['typebot']['variables']) => (block: InputBlock) => {
return (
variables.find(
(variable) =>
variable.id === block.options.variableId && isDefined(variable.value)
)?.value ?? undefined
)
}