([])
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) => {
- {displayedSteps.map((step) => (
-
-
-
- ))}
+ {displayedSteps
+ .filter((step) => isInputStep(step) || isTextBubbleStep(step))
+ .map((step) => (
+
+
+
+ ))}
diff --git a/packages/bot-engine/src/components/ChatBlock/ChatStep/bubbles/HostMessageBubble.tsx b/packages/bot-engine/src/components/ChatBlock/ChatStep/bubbles/HostMessageBubble.tsx
index fad80bbd7..768a3b8a4 100644
--- a/packages/bot-engine/src/components/ChatBlock/ChatStep/bubbles/HostMessageBubble.tsx
+++ b/packages/bot-engine/src/components/ChatBlock/ChatStep/bubbles/HostMessageBubble.tsx
@@ -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(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,
}}
/>
)}
diff --git a/packages/bot-engine/src/contexts/TypebotContext.tsx b/packages/bot-engine/src/contexts/TypebotContext.tsx
index 55f3ee114..78644a436 100644
--- a/packages/bot-engine/src/contexts/TypebotContext.tsx
+++ b/packages/bot-engine/src/contexts/TypebotContext.tsx
@@ -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(typebot)
+
+ const updateVariableValue = (variableId: string, value: string) => {
+ setLocalTypebot((typebot) => ({
+ ...typebot,
+ variables: {
+ ...typebot.variables,
+ byId: {
+ ...typebot.variables.byId,
+ [variableId]: { ...typebot.variables.byId[variableId], value },
+ },
+ },
+ }))
+ }
return (
{children}
diff --git a/packages/bot-engine/src/services/variable.ts b/packages/bot-engine/src/services/variable.ts
new file mode 100644
index 000000000..418f89b53
--- /dev/null
+++ b/packages/bot-engine/src/services/variable.ts
@@ -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
+): 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
+}
diff --git a/packages/bot-engine/tsconfig.json b/packages/bot-engine/tsconfig.json
index c4e91ca6b..2292756c5 100644
--- a/packages/bot-engine/tsconfig.json
+++ b/packages/bot-engine/tsconfig.json
@@ -13,6 +13,7 @@
"outDir": "dist",
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
- "emitDeclarationOnly": true
+ "emitDeclarationOnly": true,
+ "baseUrl": "./src"
}
}
diff --git a/packages/db/prisma/schema.draft.prisma b/packages/db/prisma/schema.draft.prisma
deleted file mode 100644
index d95c6a640..000000000
--- a/packages/db/prisma/schema.draft.prisma
+++ /dev/null
@@ -1,87 +0,0 @@
-datasource db {
- url = env("DATABASE_URL")
- provider = "postgresql"
-}
-
-generator client {
- provider = "prisma-client-js"
-}
-
-model User {
- id String @id
- createdAt DateTime @default(now())
- email String @unique
- name String?
- avatarUrl String?
- redeemedCoupon Boolean?
- oAuthCredentials Json?
- referralId String?
- domains String[]
- onboarding_data Json?
- settings Json
- typebots Typebot[] @relation("Owner")
- sharedTypebots Typebot[] @relation("Collaborators")
- dashboardFolders DashboardFolder[]
-}
-
-model DashboardFolder {
- id BigInt @id @default(autoincrement())
- createdAt DateTime @default(now())
- name String
- owner User @relation(fields: [ownerId], references: [id])
- ownerId String
- parentFolderId BigInt
- parentFolder DashboardFolder @relation("ParentChild", fields: [parentFolderId], references: [id])
- childrenFolder DashboardFolder[] @relation("ParentChild")
-}
-
-model Typebot {
- id BigInt @id @default(autoincrement())
- createdAt DateTime @default(now())
- updatedAt DateTime @default(now())
- steps Json[]
- publishedTypebotId BigInt @unique
- publishedTypebot PublicTypebot @relation(fields: [publishedTypebotId], references: [id])
- connectors Json[]
- name String
- ownerId String
- owner User @relation("Owner", fields: [ownerId], references: [id])
- conditions Json
- startConditions Json
- theme Json
- settings Json
- collaborators User[] @relation("Collaborators")
- customDomains String[]
- shareSettings Json
- variables Json
- checkedConversionRules String[]
- results Result[]
- httpRequests Json[]
- credentials Json[]
-}
-
-model PublicTypebot {
- id BigInt @id @default(autoincrement())
- typebot Typebot?
- steps Json[]
- name String
- conditions Json
- startConditions Json
- theme Json
- settings Json
- connectors Json
- customDomains String[]
- shareSettings Json
- variables Json
-}
-
-model Result {
- id BigInt @id @default(autoincrement())
- createdAt DateTime @default(now())
- updatedAt DateTime @default(now())
- typebotId BigInt
- typebot Typebot @relation(fields: [typebotId], references: [id])
- variables Json[]
- isCompleted Boolean
- answers Json[]
-}
diff --git a/packages/db/prisma/schema.prisma b/packages/db/prisma/schema.prisma
index 9497aa47d..413f31fb4 100644
--- a/packages/db/prisma/schema.prisma
+++ b/packages/db/prisma/schema.prisma
@@ -93,6 +93,7 @@ model Typebot {
blocks Json
steps Json
choiceItems Json
+ variables Json
theme Json
settings Json
publicId String? @unique
@@ -106,6 +107,7 @@ model PublicTypebot {
blocks Json
steps Json
choiceItems Json
+ variables Json
theme Json
settings Json
publicId String? @unique
diff --git a/packages/models/src/publicTypebot.ts b/packages/models/src/publicTypebot.ts
index a6ae61b84..26df2227e 100644
--- a/packages/models/src/publicTypebot.ts
+++ b/packages/models/src/publicTypebot.ts
@@ -1,14 +1,22 @@
import { PublicTypebot as PublicTypebotFromPrisma } from 'db'
import { Block, ChoiceItem, Settings, Step, Theme } from './typebot'
+import { Variable } from './typebot/variable'
import { Table } from './utils'
export type PublicTypebot = Omit<
PublicTypebotFromPrisma,
- 'blocks' | 'startBlock' | 'theme' | 'settings' | 'steps'
+ | 'blocks'
+ | 'startBlock'
+ | 'theme'
+ | 'settings'
+ | 'steps'
+ | 'choiceItems'
+ | 'variables'
> & {
blocks: Table
steps: Table
choiceItems: Table
+ variables: Table
theme: Theme
settings: Settings
}
diff --git a/packages/models/src/typebot/index.ts b/packages/models/src/typebot/index.ts
index 4a09896f2..466582a5c 100644
--- a/packages/models/src/typebot/index.ts
+++ b/packages/models/src/typebot/index.ts
@@ -2,3 +2,4 @@ export * from './typebot'
export * from './steps'
export * from './theme'
export * from './settings'
+export * from './variable'
diff --git a/packages/models/src/typebot/steps/index.ts b/packages/models/src/typebot/steps/index.ts
index c9ce17275..7a8bb723f 100644
--- a/packages/models/src/typebot/steps/index.ts
+++ b/packages/models/src/typebot/steps/index.ts
@@ -1,2 +1,3 @@
export * from './steps'
export * from './inputs'
+export * from './logic'
diff --git a/packages/models/src/typebot/steps/inputs.ts b/packages/models/src/typebot/steps/inputs.ts
index d36389b38..404ee0ec8 100644
--- a/packages/models/src/typebot/steps/inputs.ts
+++ b/packages/models/src/typebot/steps/inputs.ts
@@ -47,7 +47,7 @@ export type DateInputStep = StepBase & {
export type PhoneNumberInputStep = StepBase & {
type: InputStepType.PHONE
- options?: InputTextOptionsBase
+ options?: OptionBase & InputTextOptionsBase
}
export type ChoiceInputStep = StepBase & {
@@ -55,12 +55,6 @@ export type ChoiceInputStep = StepBase & {
options: ChoiceInputOptions
}
-export type ChoiceInputOptions = {
- itemIds: string[]
- isMultipleChoice?: boolean
- buttonLabel?: string
-}
-
export type ChoiceItem = {
id: string
stepId: string
@@ -68,26 +62,35 @@ export type ChoiceItem = {
target?: Target
}
-export type DateInputOptions = {
+type OptionBase = { variableId?: string }
+type InputTextOptionsBase = {
+ labels?: { placeholder?: string; button?: string }
+}
+
+export type ChoiceInputOptions = OptionBase & {
+ itemIds: string[]
+ isMultipleChoice?: boolean
+ buttonLabel?: string
+}
+
+export type DateInputOptions = OptionBase & {
labels?: { button?: string; from?: string; to?: string }
hasTime?: boolean
isRange?: boolean
}
-export type EmailInputOptions = InputTextOptionsBase
+export type EmailInputOptions = OptionBase & InputTextOptionsBase
-export type UrlInputOptions = InputTextOptionsBase
+export type UrlInputOptions = OptionBase & InputTextOptionsBase
-type InputTextOptionsBase = {
- labels?: { placeholder?: string; button?: string }
-}
+export type TextInputOptions = OptionBase &
+ InputTextOptionsBase & {
+ isLong?: boolean
+ }
-export type TextInputOptions = InputTextOptionsBase & {
- isLong?: boolean
-}
-
-export type NumberInputOptions = InputTextOptionsBase & {
- min?: number
- max?: number
- step?: number
-}
+export type NumberInputOptions = OptionBase &
+ InputTextOptionsBase & {
+ min?: number
+ max?: number
+ step?: number
+ }
diff --git a/packages/models/src/typebot/steps/logic.ts b/packages/models/src/typebot/steps/logic.ts
new file mode 100644
index 000000000..89363ea6a
--- /dev/null
+++ b/packages/models/src/typebot/steps/logic.ts
@@ -0,0 +1,17 @@
+import { StepBase } from '.'
+
+export type LogicStep = SetVariableStep
+
+export enum LogicStepType {
+ SET_VARIABLE = 'Set variable',
+}
+
+export type SetVariableStep = StepBase & {
+ type: LogicStepType.SET_VARIABLE
+ options?: SetVariableOptions
+}
+
+export type SetVariableOptions = {
+ variableId?: string
+ expressionToEvaluate?: string
+}
diff --git a/packages/models/src/typebot/steps/steps.ts b/packages/models/src/typebot/steps/steps.ts
index 41b3866c3..0eacd45cf 100644
--- a/packages/models/src/typebot/steps/steps.ts
+++ b/packages/models/src/typebot/steps/steps.ts
@@ -1,10 +1,11 @@
import { InputStep, InputStepType } from './inputs'
+import { LogicStep, LogicStepType } from './logic'
-export type Step = StartStep | BubbleStep | InputStep
+export type Step = StartStep | BubbleStep | InputStep | LogicStep
export type BubbleStep = TextStep
-export type StepType = 'start' | BubbleStepType | InputStepType
+export type StepType = 'start' | BubbleStepType | InputStepType | LogicStepType
export enum BubbleStepType {
TEXT = 'text',
diff --git a/packages/models/src/typebot/typebot.ts b/packages/models/src/typebot/typebot.ts
index e843005df..3a4e3c38a 100644
--- a/packages/models/src/typebot/typebot.ts
+++ b/packages/models/src/typebot/typebot.ts
@@ -4,14 +4,16 @@ import { Table } from '../utils'
import { Settings } from './settings'
import { Step } from './steps/steps'
import { Theme } from './theme'
+import { Variable } from './variable'
export type Typebot = Omit<
TypebotFromPrisma,
- 'blocks' | 'theme' | 'settings' | 'steps'
+ 'blocks' | 'theme' | 'settings' | 'steps' | 'choiceItems' | 'variables'
> & {
blocks: Table
steps: Table
choiceItems: Table
+ variables: Table
theme: Theme
settings: Settings
}
diff --git a/packages/models/src/typebot/variable.ts b/packages/models/src/typebot/variable.ts
new file mode 100644
index 000000000..f4ad5d454
--- /dev/null
+++ b/packages/models/src/typebot/variable.ts
@@ -0,0 +1,5 @@
+export type Variable = {
+ id: string
+ name: string
+ value?: string
+}
diff --git a/packages/utils/src/utils.ts b/packages/utils/src/utils.ts
index a1cf2d714..71911429a 100644
--- a/packages/utils/src/utils.ts
+++ b/packages/utils/src/utils.ts
@@ -1,8 +1,11 @@
import {
+ BubbleStep,
BubbleStepType,
ChoiceInputStep,
InputStep,
InputStepType,
+ LogicStep,
+ LogicStepType,
Step,
Table,
TextInputStep,
@@ -45,6 +48,12 @@ export const filterTable = (ids: string[], table: Table): Table => ({
export const isInputStep = (step: Step): step is InputStep =>
(Object.values(InputStepType) as string[]).includes(step.type)
+export const isBubbleStep = (step: Step): step is BubbleStep =>
+ (Object.values(BubbleStepType) as string[]).includes(step.type)
+
+export const isLogicStep = (step: Step): step is LogicStep =>
+ (Object.values(LogicStepType) as string[]).includes(step.type)
+
export const isTextBubbleStep = (step: Step): step is TextStep =>
step.type === BubbleStepType.TEXT