feat(inputs): ✨ Add Condition step
This commit is contained in:
@ -4,10 +4,20 @@ import { TransitionGroup, CSSTransition } from 'react-transition-group'
|
||||
import { ChatStep } from './ChatStep'
|
||||
import { AvatarSideContainer } from './AvatarSideContainer'
|
||||
import { HostAvatarsContext } from '../../contexts/HostAvatarsContext'
|
||||
import { ChoiceInputStep, LogicStep, Step } from 'models'
|
||||
import {
|
||||
ChoiceInputStep,
|
||||
ComparisonOperators,
|
||||
ConditionStep,
|
||||
LogicalOperator,
|
||||
LogicStep,
|
||||
LogicStepType,
|
||||
Step,
|
||||
Target,
|
||||
} from 'models'
|
||||
import { useTypebot } from '../../contexts/TypebotContext'
|
||||
import {
|
||||
isChoiceInput,
|
||||
isDefined,
|
||||
isInputStep,
|
||||
isLogicStep,
|
||||
isTextBubbleStep,
|
||||
@ -20,23 +30,30 @@ import {
|
||||
|
||||
type ChatBlockProps = {
|
||||
stepIds: string[]
|
||||
onBlockEnd: (nextBlockId?: string) => void
|
||||
startStepId?: string
|
||||
onBlockEnd: (target?: Target) => void
|
||||
}
|
||||
|
||||
export const ChatBlock = ({ stepIds, onBlockEnd }: ChatBlockProps) => {
|
||||
export const ChatBlock = ({
|
||||
stepIds,
|
||||
startStepId,
|
||||
onBlockEnd,
|
||||
}: ChatBlockProps) => {
|
||||
const { typebot, updateVariableValue } = useTypebot()
|
||||
const [displayedSteps, setDisplayedSteps] = useState<Step[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
displayNextStep()
|
||||
const nextStep =
|
||||
typebot.steps.byId[startStepId ?? stepIds[displayedSteps.length]]
|
||||
if (nextStep) setDisplayedSteps([...displayedSteps, nextStep])
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
autoScrollToBottom()
|
||||
const currentStep = [...displayedSteps].pop()
|
||||
if (currentStep && isLogicStep(currentStep)) {
|
||||
executeLogic(currentStep)
|
||||
displayNextStep()
|
||||
const target = executeLogic(currentStep)
|
||||
target ? onBlockEnd(target) : displayNextStep()
|
||||
}
|
||||
}, [displayedSteps])
|
||||
|
||||
@ -65,33 +82,71 @@ export const ChatBlock = ({ stepIds, onBlockEnd }: ChatBlockProps) => {
|
||||
currentStep?.target?.blockId ||
|
||||
displayedSteps.length === stepIds.length
|
||||
)
|
||||
return onBlockEnd(currentStep?.target?.blockId)
|
||||
return onBlockEnd(currentStep?.target)
|
||||
}
|
||||
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 executeLogic = (step: LogicStep): Target | undefined => {
|
||||
switch (step.type) {
|
||||
case LogicStepType.SET_VARIABLE: {
|
||||
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)
|
||||
return
|
||||
}
|
||||
case LogicStepType.CONDITION: {
|
||||
const isConditionPassed =
|
||||
step.options?.logicalOperator === LogicalOperator.AND
|
||||
? step.options?.comparisons.allIds.every(executeComparison(step))
|
||||
: step.options?.comparisons.allIds.some(executeComparison(step))
|
||||
return isConditionPassed ? step.trueTarget : step.falseTarget
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const executeComparison = (step: ConditionStep) => (comparisonId: string) => {
|
||||
const comparison = step.options?.comparisons.byId[comparisonId]
|
||||
if (!comparison?.variableId) return false
|
||||
const inputValue = typebot.variables.byId[comparison.variableId].value ?? ''
|
||||
const { value } = comparison
|
||||
if (!isDefined(value)) return false
|
||||
switch (comparison.comparisonOperator) {
|
||||
case ComparisonOperators.CONTAINS: {
|
||||
return inputValue.includes(value)
|
||||
}
|
||||
case ComparisonOperators.EQUAL: {
|
||||
return inputValue === value
|
||||
}
|
||||
case ComparisonOperators.NOT_EQUAL: {
|
||||
return inputValue !== value
|
||||
}
|
||||
case ComparisonOperators.GREATER: {
|
||||
return parseFloat(inputValue) >= parseFloat(value)
|
||||
}
|
||||
case ComparisonOperators.LESS: {
|
||||
return parseFloat(inputValue) <= parseFloat(value)
|
||||
}
|
||||
case ComparisonOperators.IS_SET: {
|
||||
return isDefined(inputValue) && inputValue.length > 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const getSingleChoiceTargetId = (
|
||||
currentStep: ChoiceInputStep,
|
||||
answerContent?: string
|
||||
) => {
|
||||
): Target | undefined => {
|
||||
const itemId = currentStep.options.itemIds.find(
|
||||
(itemId) => typebot.choiceItems.byId[itemId].content === answerContent
|
||||
)
|
||||
if (!itemId) throw new Error('itemId should exist')
|
||||
const targetId =
|
||||
typebot.choiceItems.byId[itemId].target?.blockId ??
|
||||
currentStep.target?.blockId
|
||||
return targetId
|
||||
return typebot.choiceItems.byId[itemId].target ?? currentStep.target
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -5,8 +5,7 @@ import { useFrame } from 'react-frame-component'
|
||||
import { setCssVariablesValue } from '../services/theme'
|
||||
import { useAnswers } from '../contexts/AnswersContext'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import { Answer, Block, PublicTypebot } from 'models'
|
||||
import { filterTable } from 'utils'
|
||||
import { Answer, Block, PublicTypebot, Target } from 'models'
|
||||
|
||||
type Props = {
|
||||
typebot: PublicTypebot
|
||||
@ -21,25 +20,29 @@ export const ConversationContainer = ({
|
||||
onCompleted,
|
||||
}: Props) => {
|
||||
const { document: frameDocument } = useFrame()
|
||||
const [displayedBlocks, setDisplayedBlocks] = useState<Block[]>([])
|
||||
const [displayedBlocks, setDisplayedBlocks] = useState<
|
||||
{ block: Block; startStepId?: string }[]
|
||||
>([])
|
||||
const [localAnswer, setLocalAnswer] = useState<Answer | undefined>()
|
||||
const { answers } = useAnswers()
|
||||
const bottomAnchor = useRef<HTMLDivElement | null>(null)
|
||||
|
||||
const displayNextBlock = (blockId?: string) => {
|
||||
if (!blockId) return onCompleted()
|
||||
const nextBlock = typebot.blocks.byId[blockId]
|
||||
const displayNextBlock = (target?: Target) => {
|
||||
if (!target) return onCompleted()
|
||||
const nextBlock = {
|
||||
block: typebot.blocks.byId[target.blockId],
|
||||
startStepId: target.stepId,
|
||||
}
|
||||
if (!nextBlock) return onCompleted()
|
||||
onNewBlockVisible(blockId)
|
||||
onNewBlockVisible(target.blockId)
|
||||
setDisplayedBlocks([...displayedBlocks, nextBlock])
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const blocks = typebot.blocks
|
||||
const firstBlockId =
|
||||
const firstTarget =
|
||||
typebot.steps.byId[blocks.byId[blocks.allIds[0]].stepIds[0]].target
|
||||
?.blockId
|
||||
if (firstBlockId) displayNextBlock(firstBlockId)
|
||||
if (firstTarget) displayNextBlock(firstTarget)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
@ -58,10 +61,11 @@ export const ConversationContainer = ({
|
||||
className="overflow-y-scroll w-full lg:w-3/4 min-h-full rounded lg:px-5 px-3 pt-10 relative scrollable-container typebot-chat-view"
|
||||
id="scrollable-container"
|
||||
>
|
||||
{displayedBlocks.map((block, idx) => (
|
||||
{displayedBlocks.map((displayedBlock, idx) => (
|
||||
<ChatBlock
|
||||
key={block.id + idx}
|
||||
stepIds={block.stepIds}
|
||||
key={displayedBlock.block.id + idx}
|
||||
stepIds={displayedBlock.block.stepIds}
|
||||
startStepId={displayedBlock.startStepId}
|
||||
onBlockEnd={displayNextBlock}
|
||||
/>
|
||||
))}
|
||||
|
@ -1,9 +1,11 @@
|
||||
import { StepBase } from '.'
|
||||
import { StepBase, Target } from '.'
|
||||
import { Table } from '../..'
|
||||
|
||||
export type LogicStep = SetVariableStep
|
||||
export type LogicStep = SetVariableStep | ConditionStep
|
||||
|
||||
export enum LogicStepType {
|
||||
SET_VARIABLE = 'Set variable',
|
||||
CONDITION = 'Condition',
|
||||
}
|
||||
|
||||
export type SetVariableStep = StepBase & {
|
||||
@ -11,6 +13,39 @@ export type SetVariableStep = StepBase & {
|
||||
options?: SetVariableOptions
|
||||
}
|
||||
|
||||
export enum LogicalOperator {
|
||||
OR = 'OR',
|
||||
AND = 'AND',
|
||||
}
|
||||
|
||||
export enum ComparisonOperators {
|
||||
EQUAL = 'Equal to',
|
||||
NOT_EQUAL = 'Not equal',
|
||||
CONTAINS = 'Contains',
|
||||
GREATER = 'Greater than',
|
||||
LESS = 'Less than',
|
||||
IS_SET = 'Is set',
|
||||
}
|
||||
|
||||
export type ConditionStep = StepBase & {
|
||||
type: LogicStepType.CONDITION
|
||||
options: ConditionOptions
|
||||
trueTarget?: Target
|
||||
falseTarget?: Target
|
||||
}
|
||||
|
||||
export type ConditionOptions = {
|
||||
comparisons: Table<Comparison>
|
||||
logicalOperator?: LogicalOperator
|
||||
}
|
||||
|
||||
export type Comparison = {
|
||||
id: string
|
||||
variableId?: string
|
||||
comparisonOperator: ComparisonOperators
|
||||
value?: string
|
||||
}
|
||||
|
||||
export type SetVariableOptions = {
|
||||
variableId?: string
|
||||
expressionToEvaluate?: string
|
||||
|
@ -2,6 +2,7 @@ import {
|
||||
BubbleStep,
|
||||
BubbleStepType,
|
||||
ChoiceInputStep,
|
||||
ConditionStep,
|
||||
InputStep,
|
||||
InputStepType,
|
||||
LogicStep,
|
||||
@ -36,9 +37,9 @@ export const sendRequest = async <ResponseData>({
|
||||
}
|
||||
}
|
||||
|
||||
export const isDefined = <T>(value: T | undefined | null): value is T => {
|
||||
return <T>value !== undefined && <T>value !== null
|
||||
}
|
||||
export const isDefined = <T>(
|
||||
value: T | undefined | null
|
||||
): value is NonNullable<T> => value !== undefined && value !== null
|
||||
|
||||
export const filterTable = <T>(ids: string[], table: Table<T>): Table<T> => ({
|
||||
byId: ids.reduce((acc, id) => ({ ...acc, [id]: table.byId[id] }), {}),
|
||||
@ -65,3 +66,6 @@ export const isChoiceInput = (step: Step): step is ChoiceInputStep =>
|
||||
|
||||
export const isSingleChoiceInput = (step: Step): step is ChoiceInputStep =>
|
||||
step.type === InputStepType.CHOICE && !step.options.isMultipleChoice
|
||||
|
||||
export const isConditionStep = (step: Step): step is ConditionStep =>
|
||||
step.type === LogicStepType.CONDITION
|
||||
|
Reference in New Issue
Block a user