2
0

(settings) Add delay between bubbles option and typing disabling on first message

This commit is contained in:
Baptiste Arnaud
2024-01-24 12:03:41 +01:00
parent e80f297e74
commit f052b4c805
16 changed files with 159 additions and 37 deletions

View File

@ -84,7 +84,7 @@ export const SettingsSideMenu = () => {
<AccordionButton py={6}>
<HStack flex="1" pl={2}>
<ChatIcon />
<Heading fontSize="lg">Typing emulation</Heading>
<Heading fontSize="lg">Typing</Heading>
</HStack>
<AccordionIcon />
</AccordionButton>

View File

@ -1,10 +1,11 @@
import { Stack } from '@chakra-ui/react'
import { HStack, Stack, Text } from '@chakra-ui/react'
import { Settings } from '@typebot.io/schemas'
import React from 'react'
import { isDefined } from '@typebot.io/lib'
import { NumberInput } from '@/components/inputs'
import { SwitchWithLabel } from '@/components/inputs/SwitchWithLabel'
import { defaultSettings } from '@typebot.io/schemas/features/typebot/settings/constants'
import { SwitchWithRelatedSettings } from '@/components/SwitchWithRelatedSettings'
import { isDefined } from '@typebot.io/lib'
type Props = {
typingEmulation: Settings['typingEmulation']
@ -18,51 +19,92 @@ export const TypingEmulationForm = ({ typingEmulation, onUpdate }: Props) => {
enabled,
})
const handleSpeedChange = (speed?: number) =>
isDefined(speed) && onUpdate({ ...typingEmulation, speed })
const updateSpeed = (speed?: number) =>
onUpdate({ ...typingEmulation, speed })
const handleMaxDelayChange = (maxDelay?: number) =>
isDefined(maxDelay) && onUpdate({ ...typingEmulation, maxDelay })
const updateMaxDelay = (maxDelay?: number) =>
onUpdate({
...typingEmulation,
maxDelay: isDefined(maxDelay)
? Math.max(Math.min(maxDelay, 5), 0)
: undefined,
})
const isEnabled =
typingEmulation?.enabled ?? defaultSettings.typingEmulation.enabled
const updateIsDisabledOnFirstMessage = (isDisabledOnFirstMessage: boolean) =>
onUpdate({
...typingEmulation,
isDisabledOnFirstMessage,
})
const updateDelayBetweenBubbles = (delayBetweenBubbles?: number) =>
onUpdate({ ...typingEmulation, delayBetweenBubbles })
return (
<Stack spacing={6}>
<SwitchWithLabel
<SwitchWithRelatedSettings
label={'Typing emulation'}
initialValue={isEnabled}
initialValue={
typingEmulation?.enabled ?? defaultSettings.typingEmulation.enabled
}
onCheckChange={updateIsEnabled}
/>
{isEnabled && (
<Stack pl={10}>
>
<NumberInput
label="Words per minutes:"
data-testid="speed"
defaultValue={
typingEmulation?.speed ?? defaultSettings.typingEmulation.speed
}
onValueChange={updateSpeed}
withVariableButton={false}
maxW="100px"
step={30}
direction="row"
/>
<HStack>
<NumberInput
label="Words per minutes:"
data-testid="speed"
defaultValue={
typingEmulation?.speed ?? defaultSettings.typingEmulation.speed
}
onValueChange={handleSpeedChange}
withVariableButton={false}
maxW="100px"
step={30}
direction="row"
/>
<NumberInput
label="Max delay (in seconds):"
label="Max delay:"
data-testid="max-delay"
defaultValue={
typingEmulation?.maxDelay ??
defaultSettings.typingEmulation.maxDelay
}
onValueChange={handleMaxDelayChange}
onValueChange={updateMaxDelay}
withVariableButton={false}
maxW="100px"
step={0.1}
direction="row"
size="sm"
/>
</Stack>
)}
<Text>seconds</Text>
</HStack>
<SwitchWithLabel
label={'Disable on first message'}
moreInfoContent="When checked, typing emulation will be disabled for the first message sent by the bot."
onCheckChange={updateIsDisabledOnFirstMessage}
initialValue={
typingEmulation?.isDisabledOnFirstMessage ??
defaultSettings.typingEmulation.isDisabledOnFirstMessage
}
/>
</SwitchWithRelatedSettings>
<HStack>
<NumberInput
label="Delay between messages:"
defaultValue={
typingEmulation?.delayBetweenBubbles ??
defaultSettings.typingEmulation.delayBetweenBubbles
}
withVariableButton={false}
onValueChange={updateDelayBetweenBubbles}
direction="row"
maxW={'100px'}
min={0}
max={5}
size="sm"
/>
<Text>seconds</Text>
</HStack>
</Stack>
)
}

View File

@ -128,6 +128,7 @@ export const startWhatsAppPreview = authenticatedProcedure
messages,
input,
clientSideActions,
isFirstChatChunk: true,
credentials: {
phoneNumberId: env.WHATSAPP_PREVIEW_FROM_PHONE_NUMBER_ID,
systemUserAccessToken: env.META_SYSTEM_USER_TOKEN,

View File

@ -27386,6 +27386,14 @@
},
"maxDelay": {
"type": "number"
},
"delayBetweenBubbles": {
"type": "number",
"minimum": 0,
"maximum": 5
},
"isDisabledOnFirstMessage": {
"type": "boolean"
}
}
},

View File

@ -7623,6 +7623,14 @@
},
"maxDelay": {
"type": "number"
},
"delayBetweenBubbles": {
"type": "number",
"minimum": 0,
"maximum": 5
},
"isDisabledOnFirstMessage": {
"type": "boolean"
}
}
},

View File

@ -28,6 +28,9 @@ You can customize this typing speed in the settings:
The goal of a typebot is not to pretend that the bot is a real human. So we suggest not setting the typing speed too low.
The `Disable on first message` allows you to disable the typing emulation on the first message. This is useful if you want to lower the first message display time since the site can take some time to load first.
The `Delay between messages` by default is 0 and you can increase it up to 5 seconds if you want to add a delay between **every** messages sent by the typebot.
Sometimes you want to pause the bot for a few seconds between one message and another, regardless of the typing speed. You can achieve this by adding a Code block with the following content:
```js

View File

@ -93,10 +93,12 @@ export const resumeWhatsAppFlow = async ({
visitedEdges,
} = resumeResponse
const isFirstChatChunk = (!session || isSessionExpired) ?? false
await sendChatReplyToWhatsApp({
to: receivedMessage.from,
messages,
input,
isFirstChatChunk,
typingEmulation: newSessionState.typingEmulation,
clientSideActions,
credentials,

View File

@ -16,12 +16,14 @@ import { isNotDefined } from '@typebot.io/lib/utils'
import { computeTypingDuration } from '../computeTypingDuration'
import { continueBotFlow } from '../continueBotFlow'
import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants'
import { defaultSettings } from '@typebot.io/schemas/features/typebot/settings/constants'
// Media can take some time to be delivered. This make sure we don't send a message before the media is delivered.
const messageAfterMediaTimeout = 5000
type Props = {
to: string
isFirstChatChunk: boolean
typingEmulation: SessionState['typingEmulation']
credentials: WhatsAppCredentials['data']
state: SessionState
@ -30,6 +32,7 @@ type Props = {
export const sendChatReplyToWhatsApp = async ({
to,
typingEmulation,
isFirstChatChunk,
messages,
input,
clientSideActions,
@ -57,6 +60,7 @@ export const sendChatReplyToWhatsApp = async ({
to,
messages,
input,
isFirstChatChunk: false,
typingEmulation: newSessionState.typingEmulation,
clientSideActions,
credentials,
@ -64,19 +68,40 @@ export const sendChatReplyToWhatsApp = async ({
})
}
let i = -1
for (const message of messagesBeforeInput) {
i += 1
if (
i > 0 &&
(typingEmulation?.delayBetweenBubbles ??
defaultSettings.typingEmulation.delayBetweenBubbles) > 0
) {
await new Promise((resolve) =>
setTimeout(
resolve,
(typingEmulation?.delayBetweenBubbles ??
defaultSettings.typingEmulation.delayBetweenBubbles) * 1000
)
)
}
const whatsAppMessage = convertMessageToWhatsAppMessage(message)
if (isNotDefined(whatsAppMessage)) continue
const lastSentMessageIsMedia = ['audio', 'video', 'image'].includes(
sentMessages.at(-1)?.type ?? ''
)
const typingDuration = lastSentMessageIsMedia
? messageAfterMediaTimeout
: isFirstChatChunk &&
i === 0 &&
(typingEmulation?.isDisabledOnFirstMessage ??
defaultSettings.typingEmulation.isDisabledOnFirstMessage)
? 0
: getTypingDuration({
message: whatsAppMessage,
typingEmulation,
})
if (typingDuration)
if ((typingDuration ?? 0) > 0)
await new Promise((resolve) => setTimeout(resolve, typingDuration))
try {
await sendWhatsAppMessage({
@ -101,6 +126,7 @@ export const sendChatReplyToWhatsApp = async ({
to,
messages,
input,
isFirstChatChunk: false,
typingEmulation: newSessionState.typingEmulation,
clientSideActions,
credentials,

View File

@ -1,6 +1,6 @@
{
"name": "@typebot.io/js",
"version": "0.2.35",
"version": "0.2.36",
"description": "Javascript library to display typebots on your website",
"type": "module",
"main": "dist/index.js",

View File

@ -40,6 +40,20 @@ export const ChatChunk = (props: Props) => {
})
const displayNextMessage = async (bubbleOffsetTop?: number) => {
if (
(props.settings.typingEmulation?.delayBetweenBubbles ??
defaultSettings.typingEmulation.delayBetweenBubbles) > 0 &&
displayedMessageIndex() < props.messages.length - 1
) {
// eslint-disable-next-line solid/reactivity
await new Promise((resolve) =>
setTimeout(
resolve,
(props.settings.typingEmulation?.delayBetweenBubbles ??
defaultSettings.typingEmulation.delayBetweenBubbles) * 1000
)
)
}
const lastBubbleBlockId = props.messages[displayedMessageIndex()].id
await props.onNewBubbleDisplayed(lastBubbleBlockId)
setDisplayedMessageIndex(
@ -86,10 +100,17 @@ export const ChatChunk = (props: Props) => {
}}
>
<For each={props.messages.slice(0, displayedMessageIndex() + 1)}>
{(message) => (
{(message, idx) => (
<HostBubble
message={message}
typingEmulation={props.settings.typingEmulation}
isTypingSkipped={
(props.settings.typingEmulation?.isDisabledOnFirstMessage ??
defaultSettings.typingEmulation
.isDisabledOnFirstMessage) &&
props.inputIndex === 0 &&
idx() === 0
}
onTransitionEnd={displayNextMessage}
onCompleted={props.onSubmit}
/>

View File

@ -20,6 +20,7 @@ import { Match, Switch } from 'solid-js'
type Props = {
message: ChatMessage
typingEmulation: Settings['typingEmulation']
isTypingSkipped: boolean
onTransitionEnd: (offsetTop?: number) => void
onCompleted: (reply?: string) => void
}
@ -38,6 +39,7 @@ export const HostBubble = (props: Props) => {
<Match when={props.message.type === BubbleBlockType.TEXT}>
<TextBubble
content={props.message.content as TextBubbleBlock['content']}
isTypingSkipped={props.isTypingSkipped}
typingEmulation={props.typingEmulation}
onTransitionEnd={onTransitionEnd}
/>

View File

@ -10,6 +10,7 @@ import { computeTypingDuration } from '@typebot.io/bot-engine/computeTypingDurat
type Props = {
content: TextBubbleBlock['content']
typingEmulation: Settings['typingEmulation']
isTypingSkipped: boolean
onTransitionEnd: (offsetTop?: number) => void
}
@ -35,7 +36,7 @@ export const TextBubble = (props: Props) => {
? computePlainText(props.content.richText)
: ''
const typingDuration =
props.typingEmulation?.enabled === false
props.typingEmulation?.enabled === false || props.isTypingSkipped
? 0
: computeTypingDuration({
bubbleContent: plainText,

View File

@ -1,6 +1,6 @@
{
"name": "@typebot.io/nextjs",
"version": "0.2.35",
"version": "0.2.36",
"description": "Convenient library to display typebots on your Next.js website",
"main": "dist/index.js",
"types": "dist/index.d.ts",

View File

@ -1,6 +1,6 @@
{
"name": "@typebot.io/react",
"version": "0.2.35",
"version": "0.2.36",
"description": "Convenient library to display typebots on your React app",
"main": "dist/index.js",
"types": "dist/index.d.ts",

View File

@ -10,7 +10,13 @@ export const defaultSettings = {
isBrandingEnabled: false,
isTypingEmulationEnabled: true,
},
typingEmulation: { enabled: true, speed: 300, maxDelay: 1.5 },
typingEmulation: {
enabled: true,
speed: 400,
maxDelay: 3,
delayBetweenBubbles: 0,
isDisabledOnFirstMessage: true,
},
metadata: {
description:
'Build beautiful conversational forms and embed them directly in your applications without a line of code. Triple your response rate and collect answers that has more value compared to a traditional form.',

View File

@ -20,6 +20,8 @@ const typingEmulation = z.object({
enabled: z.boolean().optional(),
speed: z.number().optional(),
maxDelay: z.number().optional(),
delayBetweenBubbles: z.number().min(0).max(5).optional(),
isDisabledOnFirstMessage: z.boolean().optional(),
})
const metadataSchema = z.object({