feat(inputs): ✨ Add Set variable step
This commit is contained in:
@ -4,9 +4,19 @@ import { TransitionGroup, CSSTransition } from 'react-transition-group'
|
||||
import { ChatStep } from './ChatStep'
|
||||
import { AvatarSideContainer } from './AvatarSideContainer'
|
||||
import { HostAvatarsContext } from '../../contexts/HostAvatarsContext'
|
||||
import { ChoiceInputStep, Step } from 'models'
|
||||
import { ChoiceInputStep, LogicStep, Step } from 'models'
|
||||
import { useTypebot } from '../../contexts/TypebotContext'
|
||||
import { isChoiceInput } from 'utils'
|
||||
import {
|
||||
isChoiceInput,
|
||||
isInputStep,
|
||||
isLogicStep,
|
||||
isTextBubbleStep,
|
||||
} from 'utils'
|
||||
import {
|
||||
evaluateExpression,
|
||||
isMathFormula,
|
||||
parseVariables,
|
||||
} from 'services/variable'
|
||||
|
||||
type ChatBlockProps = {
|
||||
stepIds: string[]
|
||||
@ -14,15 +24,20 @@ type ChatBlockProps = {
|
||||
}
|
||||
|
||||
export const ChatBlock = ({ stepIds, onBlockEnd }: ChatBlockProps) => {
|
||||
const { typebot } = useTypebot()
|
||||
const { typebot, updateVariableValue } = useTypebot()
|
||||
const [displayedSteps, setDisplayedSteps] = useState<Step[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
setDisplayedSteps([typebot.steps.byId[stepIds[0]]])
|
||||
displayNextStep()
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
autoScrollToBottom()
|
||||
const currentStep = [...displayedSteps].pop()
|
||||
if (currentStep && isLogicStep(currentStep)) {
|
||||
executeLogic(currentStep)
|
||||
displayNextStep()
|
||||
}
|
||||
}, [displayedSteps])
|
||||
|
||||
const autoScrollToBottom = () => {
|
||||
@ -34,20 +49,37 @@ export const ChatBlock = ({ stepIds, onBlockEnd }: ChatBlockProps) => {
|
||||
|
||||
const displayNextStep = (answerContent?: string) => {
|
||||
const currentStep = [...displayedSteps].pop()
|
||||
if (!currentStep) throw new Error('currentStep should exist')
|
||||
const isSingleChoiceStep =
|
||||
isChoiceInput(currentStep) && !currentStep.options.isMultipleChoice
|
||||
if (isSingleChoiceStep)
|
||||
return onBlockEnd(getSingleChoiceTargetId(currentStep, answerContent))
|
||||
if (
|
||||
currentStep?.target?.blockId ||
|
||||
displayedSteps.length === stepIds.length
|
||||
)
|
||||
return onBlockEnd(currentStep?.target?.blockId)
|
||||
if (currentStep) {
|
||||
if (
|
||||
isInputStep(currentStep) &&
|
||||
currentStep.options?.variableId &&
|
||||
answerContent
|
||||
) {
|
||||
updateVariableValue(currentStep.options.variableId, answerContent)
|
||||
}
|
||||
const isSingleChoiceStep =
|
||||
isChoiceInput(currentStep) && !currentStep.options.isMultipleChoice
|
||||
if (isSingleChoiceStep)
|
||||
return onBlockEnd(getSingleChoiceTargetId(currentStep, answerContent))
|
||||
if (
|
||||
currentStep?.target?.blockId ||
|
||||
displayedSteps.length === stepIds.length
|
||||
)
|
||||
return onBlockEnd(currentStep?.target?.blockId)
|
||||
}
|
||||
const nextStep = typebot.steps.byId[stepIds[displayedSteps.length]]
|
||||
if (nextStep) setDisplayedSteps([...displayedSteps, nextStep])
|
||||
}
|
||||
|
||||
const executeLogic = (step: LogicStep) => {
|
||||
if (!step.options?.variableId || !step.options.expressionToEvaluate) return
|
||||
const expression = step.options.expressionToEvaluate
|
||||
const evaluatedExpression = isMathFormula(expression)
|
||||
? evaluateExpression(parseVariables(expression, typebot.variables))
|
||||
: expression
|
||||
updateVariableValue(step.options.variableId, evaluatedExpression)
|
||||
}
|
||||
|
||||
const getSingleChoiceTargetId = (
|
||||
currentStep: ChoiceInputStep,
|
||||
answerContent?: string
|
||||
@ -68,16 +100,18 @@ export const ChatBlock = ({ stepIds, onBlockEnd }: ChatBlockProps) => {
|
||||
<AvatarSideContainer />
|
||||
<div className="flex flex-col w-full">
|
||||
<TransitionGroup>
|
||||
{displayedSteps.map((step) => (
|
||||
<CSSTransition
|
||||
key={step.id}
|
||||
classNames="bubble"
|
||||
timeout={500}
|
||||
unmountOnExit
|
||||
>
|
||||
<ChatStep step={step} onTransitionEnd={displayNextStep} />
|
||||
</CSSTransition>
|
||||
))}
|
||||
{displayedSteps
|
||||
.filter((step) => isInputStep(step) || isTextBubbleStep(step))
|
||||
.map((step) => (
|
||||
<CSSTransition
|
||||
key={step.id}
|
||||
classNames="bubble"
|
||||
timeout={500}
|
||||
unmountOnExit
|
||||
>
|
||||
<ChatStep step={step} onTransitionEnd={displayNextStep} />
|
||||
</CSSTransition>
|
||||
))}
|
||||
</TransitionGroup>
|
||||
</div>
|
||||
</HostAvatarsContext>
|
||||
|
@ -1,9 +1,10 @@
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import { useHostAvatars } from '../../../../contexts/HostAvatarsContext'
|
||||
import { useTypebot } from '../../../../contexts/TypebotContext'
|
||||
import { BubbleStepType, StepType, TextStep } from 'models'
|
||||
import { computeTypingTimeout } from '../../../../services/chat'
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useHostAvatars } from 'contexts/HostAvatarsContext'
|
||||
import { useTypebot } from 'contexts/TypebotContext'
|
||||
import { BubbleStepType, TextStep } from 'models'
|
||||
import { computeTypingTimeout } from 'services/chat'
|
||||
import { TypingContent } from './TypingContent'
|
||||
import { parseVariables } from 'services/variable'
|
||||
|
||||
type HostMessageBubbleProps = {
|
||||
step: TextStep
|
||||
@ -24,6 +25,11 @@ export const HostMessageBubble = ({
|
||||
const messageContainer = useRef<HTMLDivElement | null>(null)
|
||||
const [isTyping, setIsTyping] = useState(true)
|
||||
|
||||
const content = useMemo(
|
||||
() => parseVariables(step.content.html, typebot.variables),
|
||||
[typebot.variables]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
sendAvatarOffset()
|
||||
const typingTimeout = computeTypingTimeout(
|
||||
@ -72,7 +78,7 @@ export const HostMessageBubble = ({
|
||||
(isTyping ? 'opacity-0 h-6' : 'opacity-100 h-full')
|
||||
}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: step.content.html,
|
||||
__html: content,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
@ -1,8 +1,9 @@
|
||||
import { PublicTypebot } from 'models'
|
||||
import React, { createContext, ReactNode, useContext } from 'react'
|
||||
import React, { createContext, ReactNode, useContext, useState } from 'react'
|
||||
|
||||
const typebotContext = createContext<{
|
||||
typebot: PublicTypebot
|
||||
updateVariableValue: (variableId: string, value: string) => void
|
||||
//@ts-ignore
|
||||
}>({})
|
||||
|
||||
@ -13,10 +14,25 @@ export const TypebotContext = ({
|
||||
children: ReactNode
|
||||
typebot: PublicTypebot
|
||||
}) => {
|
||||
const [localTypebot, setLocalTypebot] = useState<PublicTypebot>(typebot)
|
||||
|
||||
const updateVariableValue = (variableId: string, value: string) => {
|
||||
setLocalTypebot((typebot) => ({
|
||||
...typebot,
|
||||
variables: {
|
||||
...typebot.variables,
|
||||
byId: {
|
||||
...typebot.variables.byId,
|
||||
[variableId]: { ...typebot.variables.byId[variableId], value },
|
||||
},
|
||||
},
|
||||
}))
|
||||
}
|
||||
return (
|
||||
<typebotContext.Provider
|
||||
value={{
|
||||
typebot,
|
||||
typebot: localTypebot,
|
||||
updateVariableValue,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
46
packages/bot-engine/src/services/variable.ts
Normal file
46
packages/bot-engine/src/services/variable.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import { Table, Variable } from 'models'
|
||||
import { isDefined } from 'utils'
|
||||
|
||||
const safeEval = eval
|
||||
|
||||
export const stringContainsVariable = (str: string): boolean =>
|
||||
/\{\{(.*?)\}\}/g.test(str)
|
||||
|
||||
export const parseVariables = (
|
||||
text: string,
|
||||
variables: Table<Variable>
|
||||
): string => {
|
||||
if (text === '') return text
|
||||
return text.replace(/\{\{(.*?)\}\}/g, (_, fullVariableString) => {
|
||||
const matchedVarName = fullVariableString.replace(/{{|}}/g, '')
|
||||
const matchedVariableId = variables.allIds.find((variableId) => {
|
||||
const variable = variables.byId[variableId]
|
||||
return matchedVarName === variable.name && isDefined(variable.value)
|
||||
})
|
||||
return variables.byId[matchedVariableId ?? '']?.value ?? ''
|
||||
})
|
||||
}
|
||||
|
||||
export const isMathFormula = (str?: string) =>
|
||||
['*', '/', '+', '-'].some((val) => str && str.includes(val))
|
||||
|
||||
export const evaluateExpression = (str: string) => {
|
||||
let result = replaceCommasWithDots(str)
|
||||
try {
|
||||
const evaluatedNumber = safeEval(result) as number
|
||||
if (countDecimals(evaluatedNumber) > 2) {
|
||||
return evaluatedNumber.toFixed(2)
|
||||
}
|
||||
return evaluatedNumber.toString()
|
||||
} catch (err) {
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
const replaceCommasWithDots = (str: string) =>
|
||||
str.replace(new RegExp(/(\d+)(,)(\d+)/, 'g'), '$1.$3')
|
||||
|
||||
const countDecimals = (value: number) => {
|
||||
if (value % 1 != 0) return value.toString().split('.')[1].length
|
||||
return 0
|
||||
}
|
Reference in New Issue
Block a user