2
0

feat(inputs): Add Condition step

This commit is contained in:
Baptiste Arnaud
2022-01-15 17:30:20 +01:00
parent 4ccb7bca49
commit 2814a352b2
30 changed files with 1178 additions and 243 deletions

View File

@ -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 (

View File

@ -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}
/>
))}

View File

@ -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

View File

@ -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