feat(engine): ✨ Link typebot step
This commit is contained in:
@ -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) => {
|
||||
|
@ -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 = ({
|
||||
|
@ -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))
|
||||
|
@ -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,
|
||||
}}
|
||||
>
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
|
Reference in New Issue
Block a user