2
0

feat(engine): Link typebot step

This commit is contained in:
Baptiste Arnaud
2022-03-09 15:12:00 +01:00
parent 1bcc8aee10
commit 7e61ab19eb
61 changed files with 1272 additions and 245 deletions

View File

@ -1,7 +1,7 @@
import React, { useEffect, useRef, useState } from 'react'
import { TransitionGroup, CSSTransition } from 'react-transition-group'
import { AvatarSideContainer } from './AvatarSideContainer'
import { useTypebot } from '../../contexts/TypebotContext'
import { LinkedTypebot, useTypebot } from '../../contexts/TypebotContext'
import {
isBubbleStep,
isBubbleStepType,
@ -14,18 +14,21 @@ import {
import { executeLogic } from 'services/logic'
import { executeIntegration } from 'services/integration'
import { parseRetryStep, stepCanBeRetried } from 'services/inputs'
import { parseVariables } from 'index'
import { parseVariables } from '../../services/variable'
import { useAnswers } from 'contexts/AnswersContext'
import { BubbleStep, InputStep, Step } from 'models'
import { BubbleStep, InputStep, PublicTypebot, Step } from 'models'
import { HostBubble } from './ChatStep/bubbles/HostBubble'
import { InputChatStep } from './ChatStep/InputChatStep'
import { getLastChatStepType } from 'services/chat'
import { getLastChatStepType } from '../../services/chat'
type ChatBlockProps = {
steps: Step[]
startStepIndex: number
onScroll: () => void
onBlockEnd: (edgeId?: string) => void
onBlockEnd: (
edgeId?: string,
updatedTypebot?: PublicTypebot | LinkedTypebot
) => void
}
type ChatDisplayChunk = { bubbles: BubbleStep[]; input?: InputStep }
@ -43,6 +46,8 @@ export const ChatBlock = ({
apiHost,
isPreview,
onNewLog,
injectLinkedTypebot,
linkedTypebots,
} = useTypebot()
const { resultValues } = useAnswers()
const [processedSteps, setProcessedSteps] = useState<Step[]>([])
@ -93,12 +98,17 @@ export const ChatBlock = ({
const currentStep = [...processedSteps].pop()
if (!currentStep) return
if (isLogicStep(currentStep)) {
const nextEdgeId = executeLogic(
currentStep,
typebot.variables,
updateVariableValue
)
nextEdgeId ? onBlockEnd(nextEdgeId) : displayNextStep()
const { nextEdgeId, linkedTypebot } = await executeLogic(currentStep, {
isPreview,
apiHost,
typebot,
linkedTypebots,
updateVariableValue,
injectLinkedTypebot,
onNewLog,
createEdge,
})
nextEdgeId ? onBlockEnd(nextEdgeId, linkedTypebot) : displayNextStep()
}
if (isIntegrationStep(currentStep)) {
const nextEdgeId = await executeIntegration({
@ -118,6 +128,7 @@ export const ChatBlock = ({
})
nextEdgeId ? onBlockEnd(nextEdgeId) : displayNextStep()
}
if (currentStep.type === 'start') onBlockEnd(currentStep.outgoingEdgeId)
}
const displayNextStep = (answerContent?: string, isRetry?: boolean) => {

View File

@ -7,7 +7,7 @@ import { byId } from 'utils'
import { DateForm } from './inputs/DateForm'
import { ChoiceForm } from './inputs/ChoiceForm'
import { useTypebot } from 'contexts/TypebotContext'
import { parseVariables } from 'index'
import { parseVariables } from '../../../services/variable'
import { isInputValid } from 'services/inputs'
export const InputChatStep = ({

View File

@ -4,10 +4,10 @@ import { ChatBlock } from './ChatBlock/ChatBlock'
import { useFrame } from 'react-frame-component'
import { setCssVariablesValue } from '../services/theme'
import { useAnswers } from '../contexts/AnswersContext'
import { Block, Edge, Theme, VariableWithValue } from 'models'
import { Block, Edge, PublicTypebot, Theme, VariableWithValue } from 'models'
import { byId, isNotDefined } from 'utils'
import { animateScroll as scroll } from 'react-scroll'
import { useTypebot } from 'contexts/TypebotContext'
import { LinkedTypebot, useTypebot } from 'contexts/TypebotContext'
type Props = {
theme: Theme
@ -30,10 +30,14 @@ export const ConversationContainer = ({
const bottomAnchor = useRef<HTMLDivElement | null>(null)
const scrollableContainer = useRef<HTMLDivElement | null>(null)
const displayNextBlock = (edgeId?: string) => {
const nextEdge = typebot.edges.find(byId(edgeId))
const displayNextBlock = (
edgeId?: string,
updatedTypebot?: PublicTypebot | LinkedTypebot
) => {
const currentTypebot = updatedTypebot ?? typebot
const nextEdge = currentTypebot.edges.find(byId(edgeId))
if (!nextEdge) return onCompleted()
const nextBlock = typebot.blocks.find(byId(nextEdge.to.blockId))
const nextBlock = currentTypebot.blocks.find(byId(nextEdge.to.blockId))
if (!nextBlock) return onCompleted()
const startStepIndex = nextEdge.to.stepId
? nextBlock.steps.findIndex(byId(nextEdge.to.stepId))

View File

@ -1,5 +1,5 @@
import { Log } from 'db'
import { Edge, PublicTypebot } from 'models'
import { Edge, PublicTypebot, Typebot } from 'models'
import React, {
createContext,
ReactNode,
@ -8,12 +8,18 @@ import React, {
useState,
} from 'react'
export type LinkedTypebot = Pick<
PublicTypebot | Typebot,
'id' | 'blocks' | 'variables' | 'edges'
>
const typebotContext = createContext<{
typebot: PublicTypebot
linkedTypebots: LinkedTypebot[]
apiHost: string
isPreview: boolean
updateVariableValue: (variableId: string, value: string) => void
createEdge: (edge: Edge) => void
injectLinkedTypebot: (typebot: Typebot | PublicTypebot) => LinkedTypebot
onNewLog: (log: Omit<Log, 'id' | 'createdAt' | 'resultId'>) => void
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
@ -33,6 +39,7 @@ export const TypebotContext = ({
onNewLog: (log: Omit<Log, 'id' | 'createdAt' | 'resultId'>) => void
}) => {
const [localTypebot, setLocalTypebot] = useState<PublicTypebot>(typebot)
const [linkedTypebots, setLinkedTypebots] = useState<LinkedTypebot[]>([])
useEffect(() => {
setLocalTypebot((localTypebot) => ({
@ -59,14 +66,34 @@ export const TypebotContext = ({
}))
}
const injectLinkedTypebot = (typebot: Typebot | PublicTypebot) => {
const typebotToInject = {
id: typebot.id,
blocks: typebot.blocks,
edges: typebot.edges,
variables: typebot.variables,
}
setLinkedTypebots((typebots) => [...typebots, typebotToInject])
const updatedTypebot = {
...localTypebot,
blocks: [...localTypebot.blocks, ...typebotToInject.blocks],
variables: [...localTypebot.variables, ...typebotToInject.variables],
edges: [...localTypebot.edges, ...typebotToInject.edges],
}
setLocalTypebot(updatedTypebot)
return typebotToInject
}
return (
<typebotContext.Provider
value={{
typebot: localTypebot,
linkedTypebots,
apiHost,
isPreview,
updateVariableValue,
createEdge,
injectLinkedTypebot,
onNewLog,
}}
>

View File

@ -1,3 +1,5 @@
import { LinkedTypebot } from 'contexts/TypebotContext'
import { Log } from 'db'
import {
LogicStep,
LogicStepType,
@ -9,34 +11,52 @@ import {
RedirectStep,
Comparison,
CodeStep,
TypebotLinkStep,
PublicTypebot,
Typebot,
Edge,
} from 'models'
import { isDefined, isNotDefined } from 'utils'
import { byId, isDefined, isNotDefined, sendRequest } from 'utils'
import { sanitizeUrl } from './utils'
import { evaluateExpression, parseVariables } from './variable'
type EdgeId = string
export const executeLogic = (
type LogicContext = {
isPreview: boolean
apiHost: string
typebot: PublicTypebot
linkedTypebots: LinkedTypebot[]
updateVariableValue: (variableId: string, value: string) => void
injectLinkedTypebot: (typebot: Typebot | PublicTypebot) => LinkedTypebot
onNewLog: (log: Omit<Log, 'id' | 'createdAt' | 'resultId'>) => void
createEdge: (edge: Edge) => void
}
export const executeLogic = async (
step: LogicStep,
variables: Variable[],
updateVariableValue: (variableId: string, expression: string) => void
): EdgeId | undefined => {
context: LogicContext
): Promise<{
nextEdgeId?: EdgeId
linkedTypebot?: PublicTypebot | LinkedTypebot
}> => {
switch (step.type) {
case LogicStepType.SET_VARIABLE:
return executeSetVariable(step, variables, updateVariableValue)
return { nextEdgeId: executeSetVariable(step, context) }
case LogicStepType.CONDITION:
return executeCondition(step, variables)
return { nextEdgeId: executeCondition(step, context) }
case LogicStepType.REDIRECT:
return executeRedirect(step, variables)
return { nextEdgeId: executeRedirect(step, context) }
case LogicStepType.CODE:
return executeCode(step)
return { nextEdgeId: executeCode(step) }
case LogicStepType.TYPEBOT_LINK:
return await executeTypebotLink(step, context)
}
}
const executeSetVariable = (
step: SetVariableStep,
variables: Variable[],
updateVariableValue: (variableId: string, expression: string) => void
{ typebot: { variables }, updateVariableValue }: LogicContext
): EdgeId | undefined => {
if (!step.options?.variableId || !step.options.expressionToEvaluate)
return step.outgoingEdgeId
@ -50,7 +70,7 @@ const executeSetVariable = (
const executeCondition = (
step: ConditionStep,
variables: Variable[]
{ typebot: { variables } }: LogicContext
): EdgeId | undefined => {
const { content } = step.items[0]
const isConditionPassed =
@ -91,7 +111,7 @@ const executeComparison =
const executeRedirect = (
step: RedirectStep,
variables: Variable[]
{ typebot: { variables } }: LogicContext
): EdgeId | undefined => {
if (!step.options?.url) return step.outgoingEdgeId
window.open(
@ -106,3 +126,59 @@ const executeCode = (step: CodeStep) => {
Function(step.options.content)()
return step.outgoingEdgeId
}
const executeTypebotLink = async (
step: TypebotLinkStep,
context: LogicContext
): Promise<{
nextEdgeId?: EdgeId
linkedTypebot?: PublicTypebot | LinkedTypebot
}> => {
const { typebot, linkedTypebots, onNewLog, createEdge } = context
const linkedTypebot =
[typebot, ...linkedTypebots].find(byId(step.options.typebotId)) ??
(await fetchAndInjectTypebot(step, context))
if (!linkedTypebot) {
onNewLog({
status: 'error',
description: 'Failed to link typebot',
details: '',
})
return { nextEdgeId: step.outgoingEdgeId }
}
const nextBlockId =
step.options.blockId ??
linkedTypebot.blocks.find((b) => b.steps.some((s) => s.type === 'start'))
?.id
if (!nextBlockId) return { nextEdgeId: step.outgoingEdgeId }
const newEdge: Edge = {
id: (Math.random() * 1000).toString(),
from: { stepId: '', blockId: '' },
to: {
blockId: nextBlockId,
},
}
createEdge(newEdge)
return {
nextEdgeId: newEdge.id,
linkedTypebot: {
...linkedTypebot,
edges: [...linkedTypebot.edges, newEdge],
},
}
}
const fetchAndInjectTypebot = async (
step: TypebotLinkStep,
{ apiHost, injectLinkedTypebot, isPreview }: LogicContext
): Promise<LinkedTypebot | undefined> => {
const { data, error } = isPreview
? await sendRequest<{ typebot: Typebot }>(
`/api/typebots/${step.options.typebotId}`
)
: await sendRequest<{ typebot: PublicTypebot }>(
`${apiHost}/api/publicTypebots/${step.options.typebotId}`
)
if (!data || error) return
return injectLinkedTypebot(data.typebot)
}

View File

@ -20,7 +20,7 @@ export const parseVariables =
export const evaluateExpression = (str: string) => {
try {
const evaluatedResult = Function('return' + str)()
const evaluatedResult = Function('return ' + str)()
return isNotDefined(evaluatedResult) ? '' : evaluatedResult.toString()
} catch (err) {
console.log(err)