diff --git a/apps/builder/src/features/settings/components/SettingsSideMenu.tsx b/apps/builder/src/features/settings/components/SettingsSideMenu.tsx index 0e424e950..694565a19 100644 --- a/apps/builder/src/features/settings/components/SettingsSideMenu.tsx +++ b/apps/builder/src/features/settings/components/SettingsSideMenu.tsx @@ -84,7 +84,7 @@ export const SettingsSideMenu = () => { - Typing emulation + Typing diff --git a/apps/builder/src/features/settings/components/TypingEmulationForm.tsx b/apps/builder/src/features/settings/components/TypingEmulationForm.tsx index 6726bfa1f..d3e160880 100644 --- a/apps/builder/src/features/settings/components/TypingEmulationForm.tsx +++ b/apps/builder/src/features/settings/components/TypingEmulationForm.tsx @@ -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 ( - - {isEnabled && ( - + > + + - - - )} + seconds + + + + + + + seconds + ) } diff --git a/apps/builder/src/features/whatsapp/startWhatsAppPreview.ts b/apps/builder/src/features/whatsapp/startWhatsAppPreview.ts index 2ea1903f7..f7fbf377e 100644 --- a/apps/builder/src/features/whatsapp/startWhatsAppPreview.ts +++ b/apps/builder/src/features/whatsapp/startWhatsAppPreview.ts @@ -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, diff --git a/apps/docs/openapi/builder.json b/apps/docs/openapi/builder.json index f73b77e43..9aa6f32ff 100644 --- a/apps/docs/openapi/builder.json +++ b/apps/docs/openapi/builder.json @@ -27386,6 +27386,14 @@ }, "maxDelay": { "type": "number" + }, + "delayBetweenBubbles": { + "type": "number", + "minimum": 0, + "maximum": 5 + }, + "isDisabledOnFirstMessage": { + "type": "boolean" } } }, diff --git a/apps/docs/openapi/viewer.json b/apps/docs/openapi/viewer.json index 7151fac8d..3ae8d4aeb 100644 --- a/apps/docs/openapi/viewer.json +++ b/apps/docs/openapi/viewer.json @@ -7623,6 +7623,14 @@ }, "maxDelay": { "type": "number" + }, + "delayBetweenBubbles": { + "type": "number", + "minimum": 0, + "maximum": 5 + }, + "isDisabledOnFirstMessage": { + "type": "boolean" } } }, diff --git a/apps/docs/settings/overview.mdx b/apps/docs/settings/overview.mdx index b7e395700..4d5255212 100644 --- a/apps/docs/settings/overview.mdx +++ b/apps/docs/settings/overview.mdx @@ -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 diff --git a/packages/bot-engine/whatsapp/resumeWhatsAppFlow.ts b/packages/bot-engine/whatsapp/resumeWhatsAppFlow.ts index 0b00827c5..53ab07c74 100644 --- a/packages/bot-engine/whatsapp/resumeWhatsAppFlow.ts +++ b/packages/bot-engine/whatsapp/resumeWhatsAppFlow.ts @@ -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, diff --git a/packages/bot-engine/whatsapp/sendChatReplyToWhatsApp.ts b/packages/bot-engine/whatsapp/sendChatReplyToWhatsApp.ts index bbf7a016f..4193e850c 100644 --- a/packages/bot-engine/whatsapp/sendChatReplyToWhatsApp.ts +++ b/packages/bot-engine/whatsapp/sendChatReplyToWhatsApp.ts @@ -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, diff --git a/packages/embeds/js/package.json b/packages/embeds/js/package.json index aa49af6f6..8866f407f 100644 --- a/packages/embeds/js/package.json +++ b/packages/embeds/js/package.json @@ -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", diff --git a/packages/embeds/js/src/components/ConversationContainer/ChatChunk.tsx b/packages/embeds/js/src/components/ConversationContainer/ChatChunk.tsx index f73e757f9..55b2f72c0 100644 --- a/packages/embeds/js/src/components/ConversationContainer/ChatChunk.tsx +++ b/packages/embeds/js/src/components/ConversationContainer/ChatChunk.tsx @@ -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) => { }} > - {(message) => ( + {(message, idx) => ( diff --git a/packages/embeds/js/src/components/bubbles/HostBubble.tsx b/packages/embeds/js/src/components/bubbles/HostBubble.tsx index 78da5ad9d..e5ff96094 100644 --- a/packages/embeds/js/src/components/bubbles/HostBubble.tsx +++ b/packages/embeds/js/src/components/bubbles/HostBubble.tsx @@ -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) => { diff --git a/packages/embeds/js/src/features/blocks/bubbles/textBubble/components/TextBubble.tsx b/packages/embeds/js/src/features/blocks/bubbles/textBubble/components/TextBubble.tsx index 2a59bfffe..0b39f7d72 100644 --- a/packages/embeds/js/src/features/blocks/bubbles/textBubble/components/TextBubble.tsx +++ b/packages/embeds/js/src/features/blocks/bubbles/textBubble/components/TextBubble.tsx @@ -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, diff --git a/packages/embeds/nextjs/package.json b/packages/embeds/nextjs/package.json index 9b987180e..6126c4251 100644 --- a/packages/embeds/nextjs/package.json +++ b/packages/embeds/nextjs/package.json @@ -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", diff --git a/packages/embeds/react/package.json b/packages/embeds/react/package.json index 8fede1244..e64c9a239 100644 --- a/packages/embeds/react/package.json +++ b/packages/embeds/react/package.json @@ -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", diff --git a/packages/schemas/features/typebot/settings/constants.ts b/packages/schemas/features/typebot/settings/constants.ts index 9f55293ad..113338cc1 100644 --- a/packages/schemas/features/typebot/settings/constants.ts +++ b/packages/schemas/features/typebot/settings/constants.ts @@ -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.', diff --git a/packages/schemas/features/typebot/settings/schema.ts b/packages/schemas/features/typebot/settings/schema.ts index 3982c3801..5152549b0 100644 --- a/packages/schemas/features/typebot/settings/schema.ts +++ b/packages/schemas/features/typebot/settings/schema.ts @@ -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({