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 {