diff --git a/apps/builder/components/settings/SettingsContent.tsx b/apps/builder/components/settings/SettingsContent.tsx
new file mode 100644
index 000000000..c138c545f
--- /dev/null
+++ b/apps/builder/components/settings/SettingsContent.tsx
@@ -0,0 +1,26 @@
+import { Flex, Stack } from '@chakra-ui/react'
+import { TypingEmulationSettings } from 'bot-engine'
+import { useTypebot } from 'contexts/TypebotContext'
+import React from 'react'
+import { TypingEmulation } from './TypingEmulation'
+
+export const SettingsContent = () => {
+ const { typebot, updateSettings } = useTypebot()
+
+ const handleTypingEmulationUpdate = (
+ typingEmulation: TypingEmulationSettings
+ ) => {
+ if (!typebot) return
+ updateSettings({ ...typebot.settings, typingEmulation })
+ }
+ return (
+
+
+
+
+
+ )
+}
diff --git a/apps/builder/components/settings/SmartNumberInput.tsx b/apps/builder/components/settings/SmartNumberInput.tsx
new file mode 100644
index 000000000..073ac4739
--- /dev/null
+++ b/apps/builder/components/settings/SmartNumberInput.tsx
@@ -0,0 +1,39 @@
+import {
+ NumberInputProps,
+ NumberInput,
+ NumberInputField,
+ NumberInputStepper,
+ NumberIncrementStepper,
+ NumberDecrementStepper,
+} from '@chakra-ui/react'
+import { useState, useEffect } from 'react'
+
+export const SmartNumberInput = ({
+ initialValue,
+ onValueChange,
+ ...props
+}: {
+ initialValue: number
+ onValueChange: (value: number) => void
+} & NumberInputProps) => {
+ const [value, setValue] = useState(initialValue.toString())
+
+ useEffect(() => {
+ if (value.endsWith('.') || value.endsWith(',')) return
+ if (value === '') onValueChange(0)
+ const newValue = parseFloat(value)
+ if (isNaN(newValue)) return
+ onValueChange(newValue)
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [value])
+
+ return (
+
+
+
+
+
+
+
+ )
+}
diff --git a/apps/builder/components/settings/TypingEmulation.tsx b/apps/builder/components/settings/TypingEmulation.tsx
new file mode 100644
index 000000000..df64afc9e
--- /dev/null
+++ b/apps/builder/components/settings/TypingEmulation.tsx
@@ -0,0 +1,63 @@
+import { Flex, Stack, Switch, Text } from '@chakra-ui/react'
+import { TypingEmulationSettings } from 'bot-engine'
+import React from 'react'
+import { SmartNumberInput } from './SmartNumberInput'
+
+type TypingEmulationProps = {
+ typingEmulation?: TypingEmulationSettings
+ onUpdate: (typingEmulation: TypingEmulationSettings) => void
+}
+
+export const TypingEmulation = ({
+ typingEmulation,
+ onUpdate,
+}: TypingEmulationProps) => {
+ const handleSwitchChange = () => {
+ if (!typingEmulation) return
+ onUpdate({ ...typingEmulation, enabled: !typingEmulation.enabled })
+ }
+
+ const handleSpeedChange = (speed: number) => {
+ if (!typingEmulation) return
+ onUpdate({ ...typingEmulation, speed })
+ }
+
+ const handleMaxDelayChange = (maxDelay: number) => {
+ if (!typingEmulation) return
+ onUpdate({ ...typingEmulation, maxDelay: maxDelay })
+ }
+
+ return (
+
+
+ Typing emulation
+
+
+ {typingEmulation?.enabled && (
+
+
+ Words per minutes:
+
+
+
+ Max delay (in seconds):
+
+
+
+ )}
+
+ )
+}
diff --git a/apps/builder/components/shared/TypebotHeader/TypebotHeader.tsx b/apps/builder/components/shared/TypebotHeader/TypebotHeader.tsx
index 70756c9ae..34f3a85c3 100644
--- a/apps/builder/components/shared/TypebotHeader/TypebotHeader.tsx
+++ b/apps/builder/components/shared/TypebotHeader/TypebotHeader.tsx
@@ -53,9 +53,9 @@ export const TypebotHeader = () => {
diff --git a/apps/builder/contexts/TypebotContext.tsx b/apps/builder/contexts/TypebotContext.tsx
index ce5723f59..cc2360a48 100644
--- a/apps/builder/contexts/TypebotContext.tsx
+++ b/apps/builder/contexts/TypebotContext.tsx
@@ -1,5 +1,13 @@
import { useToast } from '@chakra-ui/react'
-import { Block, Step, StepType, Target, Theme, Typebot } from 'bot-engine'
+import {
+ Block,
+ Settings,
+ Step,
+ StepType,
+ Target,
+ Theme,
+ Typebot,
+} from 'bot-engine'
import { useRouter } from 'next/router'
import {
createContext,
@@ -47,6 +55,7 @@ const typebotContext = createContext<{
}) => void
undo: () => void
updateTheme: (theme: Theme) => void
+ updateSettings: (settings: Settings) => void
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
}>({})
@@ -270,6 +279,11 @@ export const TypebotContext = ({
setLocalTypebot({ ...localTypebot, theme })
}
+ const updateSettings = (settings: Settings) => {
+ if (!localTypebot) return
+ setLocalTypebot({ ...localTypebot, settings })
+ }
+
return (
{children}
diff --git a/apps/builder/pages/api/typebots.ts b/apps/builder/pages/api/typebots.ts
index 0fd716807..8a3eb2e09 100644
--- a/apps/builder/pages/api/typebots.ts
+++ b/apps/builder/pages/api/typebots.ts
@@ -1,4 +1,10 @@
-import { BackgroundType, StartBlock, StepType, Theme } from 'bot-engine'
+import {
+ BackgroundType,
+ Settings,
+ StartBlock,
+ StepType,
+ Theme,
+} from 'bot-engine'
import { Typebot, User } from 'db'
import prisma from 'libs/prisma'
import { NextApiRequest, NextApiResponse } from 'next'
@@ -44,8 +50,15 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
background: { type: BackgroundType.NONE, content: '#ffffff' },
},
}
+ const settings: Settings = {
+ typingEmulation: {
+ enabled: true,
+ speed: 300,
+ maxDelay: 1.5,
+ },
+ }
const typebot = await prisma.typebot.create({
- data: { ...data, ownerId: user.id, startBlock, theme },
+ data: { ...data, ownerId: user.id, startBlock, theme, settings },
})
return res.send(typebot)
}
diff --git a/apps/builder/pages/typebots/[id]/settings.tsx b/apps/builder/pages/typebots/[id]/settings.tsx
new file mode 100644
index 000000000..4d2d486eb
--- /dev/null
+++ b/apps/builder/pages/typebots/[id]/settings.tsx
@@ -0,0 +1,23 @@
+import { Flex } from '@chakra-ui/layout'
+import withAuth from 'components/HOC/withUser'
+import { Seo } from 'components/Seo'
+import { SettingsContent } from 'components/settings/SettingsContent'
+import { TypebotHeader } from 'components/shared/TypebotHeader'
+import { TypebotContext } from 'contexts/TypebotContext'
+import { useRouter } from 'next/router'
+import React from 'react'
+
+const SettingsPage = () => {
+ const { query } = useRouter()
+ return (
+
+
+
+
+
+
+
+ )
+}
+
+export default withAuth(SettingsPage)
diff --git a/apps/builder/services/typebots.ts b/apps/builder/services/typebots.ts
index 89222cf10..1441caa57 100644
--- a/apps/builder/services/typebots.ts
+++ b/apps/builder/services/typebots.ts
@@ -156,4 +156,5 @@ export const parseTypebotToPublicTypebot = (
startBlock: typebot.startBlock,
typebotId: typebot.id,
theme: typebot.theme,
+ settings: typebot.settings,
})
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 dd2d17dd5..32235de99 100644
--- a/packages/bot-engine/src/components/ChatBlock/ChatStep/bubbles/HostMessageBubble.tsx
+++ b/packages/bot-engine/src/components/ChatBlock/ChatStep/bubbles/HostMessageBubble.tsx
@@ -1,6 +1,8 @@
import React, { useEffect, useRef, useState } from 'react'
import { useHostAvatars } from '../../../../contexts/HostAvatarsContext'
+import { useTypebot } from '../../../../contexts/TypebotContext'
import { StepType, TextStep } from '../../../../models'
+import { computeTypingTimeout } from '../../../../services/chat'
import { TypingContent } from './TypingContent'
type HostMessageBubbleProps = {
@@ -16,15 +18,18 @@ export const HostMessageBubble = ({
step,
onTransitionEnd,
}: HostMessageBubbleProps) => {
+ const { typebot } = useTypebot()
+ const { typingEmulation } = typebot.settings
const { updateLastAvatarOffset } = useHostAvatars()
const messageContainer = useRef(null)
const [isTyping, setIsTyping] = useState(true)
useEffect(() => {
- const wordCount = step.content.plainText.match(/(\w+)/g)?.length ?? 0
- const typedWordsPerMinute = 250
- const typingTimeout = (wordCount / typedWordsPerMinute) * 60000
sendAvatarOffset()
+ const typingTimeout = computeTypingTimeout(
+ step.content.plainText,
+ typingEmulation
+ )
setTimeout(() => {
onTypingEnd()
}, typingTimeout)
diff --git a/packages/bot-engine/src/models/publicTypebot.ts b/packages/bot-engine/src/models/publicTypebot.ts
index 7bf95583d..d0f4802b4 100644
--- a/packages/bot-engine/src/models/publicTypebot.ts
+++ b/packages/bot-engine/src/models/publicTypebot.ts
@@ -1,11 +1,12 @@
import { PublicTypebot as PublicTypebotFromPrisma } from 'db'
-import { Block, StartBlock, Theme } from '.'
+import { Block, Settings, StartBlock, Theme } from '.'
export type PublicTypebot = Omit<
PublicTypebotFromPrisma,
- 'blocks' | 'startBlock' | 'theme'
+ 'blocks' | 'startBlock' | 'theme' | 'settings'
> & {
blocks: Block[]
startBlock: StartBlock
theme: Theme
+ settings: Settings
}
diff --git a/packages/bot-engine/src/models/typebot.ts b/packages/bot-engine/src/models/typebot.ts
index 20c49990e..a2969c857 100644
--- a/packages/bot-engine/src/models/typebot.ts
+++ b/packages/bot-engine/src/models/typebot.ts
@@ -2,11 +2,12 @@ import { Typebot as TypebotFromPrisma } from 'db'
export type Typebot = Omit<
TypebotFromPrisma,
- 'blocks' | 'startBlock' | 'theme'
+ 'blocks' | 'startBlock' | 'theme' | 'settings'
> & {
blocks: Block[]
startBlock: StartBlock
theme: Theme
+ settings: Settings
}
export type StartBlock = {
@@ -74,3 +75,13 @@ export type Background = {
type: BackgroundType
content: string
}
+
+export type Settings = {
+ typingEmulation: TypingEmulationSettings
+}
+
+export type TypingEmulationSettings = {
+ enabled: boolean
+ speed: number
+ maxDelay: number
+}
diff --git a/packages/bot-engine/src/services/chat.ts b/packages/bot-engine/src/services/chat.ts
new file mode 100644
index 000000000..7b13bbdc5
--- /dev/null
+++ b/packages/bot-engine/src/services/chat.ts
@@ -0,0 +1,15 @@
+import { TypingEmulationSettings } from '../models'
+
+export const computeTypingTimeout = (
+ bubbleContent: string,
+ typingSettings: TypingEmulationSettings
+) => {
+ const wordCount = bubbleContent.match(/(\w+)/g)?.length ?? 0
+ const typedWordsPerMinute = typingSettings.speed
+ let typingTimeout = typingSettings.enabled
+ ? (wordCount / typedWordsPerMinute) * 60000
+ : 0
+ if (typingTimeout > typingSettings.maxDelay * 1000)
+ typingTimeout = typingSettings.maxDelay * 1000
+ return typingTimeout
+}
diff --git a/packages/db/prisma/migrations/20211223124908_add_settings/migration.sql b/packages/db/prisma/migrations/20211223124908_add_settings/migration.sql
new file mode 100644
index 000000000..fb3f6f55c
--- /dev/null
+++ b/packages/db/prisma/migrations/20211223124908_add_settings/migration.sql
@@ -0,0 +1,12 @@
+/*
+ Warnings:
+
+ - Added the required column `settings` to the `PublicTypebot` table without a default value. This is not possible if the table is not empty.
+ - Added the required column `settings` to the `Typebot` table without a default value. This is not possible if the table is not empty.
+
+*/
+-- AlterTable
+ALTER TABLE "PublicTypebot" ADD COLUMN "settings" JSONB NOT NULL;
+
+-- AlterTable
+ALTER TABLE "Typebot" ADD COLUMN "settings" JSONB NOT NULL;
diff --git a/packages/db/prisma/schema.prisma b/packages/db/prisma/schema.prisma
index 21ff6535b..b2c75aa20 100644
--- a/packages/db/prisma/schema.prisma
+++ b/packages/db/prisma/schema.prisma
@@ -90,6 +90,7 @@ model Typebot {
blocks Json[]
startBlock Json
theme Json
+ settings Json
}
model PublicTypebot {
@@ -100,6 +101,7 @@ model PublicTypebot {
blocks Json[]
startBlock Json
theme Json
+ settings Json
}
model Result {