Skip to content

Commit

Permalink
🚸 (engine) Improve engine v2 client loading and timings
Browse files Browse the repository at this point in the history
Client actions are triggered after the correct bubble block. If the send message request is longer than 1s we show a loading chunk

Closes #276
  • Loading branch information
baptisteArno committed Jan 27, 2023
1 parent a738897 commit 4f78dda
Show file tree
Hide file tree
Showing 16 changed files with 400 additions and 254 deletions.
429 changes: 229 additions & 200 deletions apps/docs/openapi/chat/_spec_.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,15 @@ if (window.$chatwoot) {

export const executeChatwootBlock = (
{ typebot: { variables }, isPreview }: SessionState,
block: ChatwootBlock
block: ChatwootBlock,
lastBubbleBlockId?: string
): ExecuteIntegrationResponse => {
const chatwootCode = parseChatwootOpenCode(block.options)
return {
outgoingEdgeId: block.outgoingEdgeId,
clientSideActions: [
{
lastBubbleBlockId,
chatwoot: {
codeToExecute: {
content: parseVariables(variables, { fieldToParse: 'id' })(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ import { GoogleAnalyticsBlock, SessionState } from 'models'

export const executeGoogleAnalyticsBlock = (
{ typebot: { variables } }: SessionState,
block: GoogleAnalyticsBlock
block: GoogleAnalyticsBlock,
lastBubbleBlockId?: string
): ExecuteIntegrationResponse => ({
outgoingEdgeId: block.outgoingEdgeId,
clientSideActions: [
{
googleAnalytics: deepParseVariable(variables)(block.options),
lastBubbleBlockId,
},
],
})
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import { CodeBlock, SessionState } from 'models'

export const executeCode = (
{ typebot: { variables } }: SessionState,
block: CodeBlock
block: CodeBlock,
lastBubbleBlockId?: string
): ExecuteLogicResponse => {
if (!block.options.content) return { outgoingEdgeId: block.outgoingEdgeId }

Expand All @@ -30,6 +31,7 @@ export const executeCode = (
content,
args,
},
lastBubbleBlockId,
},
],
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ import { sanitizeUrl } from 'utils'

export const executeRedirect = (
{ typebot: { variables } }: SessionState,
block: RedirectBlock
block: RedirectBlock,
lastBubbleBlockId?: string
): ExecuteLogicResponse => {
if (!block.options?.url) return { outgoingEdgeId: block.outgoingEdgeId }
const formattedUrl = sanitizeUrl(parseVariables(variables)(block.options.url))
return {
clientSideActions: [
{
lastBubbleBlockId,
redirect: { url: formattedUrl, isNewTab: block.options.isNewTab },
},
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,29 @@ import { SessionState, WaitBlock } from 'models'

export const executeWait = async (
{ typebot: { variables } }: SessionState,
block: WaitBlock
block: WaitBlock,
lastBubbleBlockId?: string
): Promise<ExecuteLogicResponse> => {
if (!block.options.secondsToWaitFor)
return { outgoingEdgeId: block.outgoingEdgeId }
const parsedSecondsToWaitFor = parseVariables(variables)(
block.options.secondsToWaitFor
const parsedSecondsToWaitFor = safeParseInt(
parseVariables(variables)(block.options.secondsToWaitFor)
)

return {
outgoingEdgeId: block.outgoingEdgeId,
// @ts-expect-error isNaN can be used with strings
clientSideActions: isNaN(parsedSecondsToWaitFor)
? undefined
: [{ wait: { secondsToWaitFor: parsedSecondsToWaitFor } }],
clientSideActions: parsedSecondsToWaitFor
? [
{
wait: { secondsToWaitFor: parsedSecondsToWaitFor },
lastBubbleBlockId,
},
]
: undefined,
}
}

const safeParseInt = (value: string) => {
const parsedValue = parseInt(value)
return isNaN(parsedValue) ? undefined : parsedValue
}
1 change: 1 addition & 0 deletions apps/viewer/src/features/chat/api/utils/continueBotFlow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ const parseRetryMessage = (
return {
messages: [
{
id: block.id,
type: BubbleBlockType.TEXT,
content: {
plainText: retryMessage,
Expand Down
6 changes: 4 additions & 2 deletions apps/viewer/src/features/chat/api/utils/executeGroup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export const executeGroup =
currentReply?.clientSideActions
let logs: ChatReply['logs'] = currentReply?.logs
let nextEdgeId = null
let lastBubbleBlockId: string | undefined

let newSessionState = state

Expand All @@ -39,6 +40,7 @@ export const executeGroup =
messages.push(
deepParseVariable(newSessionState.typebot.variables)(block)
)
lastBubbleBlockId = block.id
continue
}

Expand All @@ -63,9 +65,9 @@ export const executeGroup =
logs,
}
const executionResponse = isLogicBlock(block)
? await executeLogic(newSessionState)(block)
? await executeLogic(newSessionState, lastBubbleBlockId)(block)
: isIntegrationBlock(block)
? await executeIntegration(newSessionState)(block)
? await executeIntegration(newSessionState, lastBubbleBlockId)(block)
: null

if (!executionResponse) continue
Expand Down
6 changes: 3 additions & 3 deletions apps/viewer/src/features/chat/api/utils/executeIntegration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@ import { IntegrationBlock, IntegrationBlockType, SessionState } from 'models'
import { ExecuteIntegrationResponse } from '../../types'

export const executeIntegration =
(state: SessionState) =>
(state: SessionState, lastBubbleBlockId?: string) =>
async (block: IntegrationBlock): Promise<ExecuteIntegrationResponse> => {
switch (block.type) {
case IntegrationBlockType.GOOGLE_SHEETS:
return executeGoogleSheetBlock(state, block)
case IntegrationBlockType.CHATWOOT:
return executeChatwootBlock(state, block)
return executeChatwootBlock(state, block, lastBubbleBlockId)
case IntegrationBlockType.GOOGLE_ANALYTICS:
return executeGoogleAnalyticsBlock(state, block)
return executeGoogleAnalyticsBlock(state, block, lastBubbleBlockId)
case IntegrationBlockType.EMAIL:
return executeSendEmailBlock(state, block)
case IntegrationBlockType.WEBHOOK:
Expand Down
8 changes: 4 additions & 4 deletions apps/viewer/src/features/chat/api/utils/executeLogic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,20 @@ import { LogicBlock, LogicBlockType, SessionState } from 'models'
import { ExecuteLogicResponse } from '../../types'

export const executeLogic =
(state: SessionState) =>
(state: SessionState, lastBubbleBlockId?: string) =>
async (block: LogicBlock): Promise<ExecuteLogicResponse> => {
switch (block.type) {
case LogicBlockType.SET_VARIABLE:
return executeSetVariable(state, block)
case LogicBlockType.CONDITION:
return executeCondition(state, block)
case LogicBlockType.REDIRECT:
return executeRedirect(state, block)
return executeRedirect(state, block, lastBubbleBlockId)
case LogicBlockType.CODE:
return executeCode(state, block)
return executeCode(state, block, lastBubbleBlockId)
case LogicBlockType.TYPEBOT_LINK:
return executeTypebotLink(state, block)
case LogicBlockType.WAIT:
return executeWait(state, block)
return executeWait(state, block, lastBubbleBlockId)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ type Props = Pick<ChatReply, 'messages' | 'input'> & {
settings: Settings
inputIndex: number
context: BotContext
isLoadingBubbleDisplayed: boolean
onNewBubbleDisplayed: (blockId: string) => Promise<void>
onScrollToBottom: () => void
onSubmit: (input: string) => void
onSkip: () => void
Expand All @@ -26,7 +28,9 @@ export const ChatChunk = (props: Props) => {
props.onScrollToBottom()
})

const displayNextMessage = () => {
const displayNextMessage = async () => {
const lastBubbleBlockId = props.messages[displayedMessageIndex()].id
await props.onNewBubbleDisplayed(lastBubbleBlockId)
setDisplayedMessageIndex(
displayedMessageIndex() === props.messages.length
? displayedMessageIndex()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import type { ChatReply, Theme } from 'models'
import { createEffect, createSignal, For } from 'solid-js'
import { createEffect, createSignal, For, Show } from 'solid-js'
import { sendMessageQuery } from '@/queries/sendMessageQuery'
import { ChatChunk } from './ChatChunk'
import { BotContext, InitialChatReply } from '@/types'
import { isNotDefined } from 'utils'
import { executeClientSideAction } from '@/utils/executeClientSideActions'
import { LoadingChunk } from './LoadingChunk'

const parseDynamicTheme = (
initialTheme: Theme,
Expand Down Expand Up @@ -55,6 +56,7 @@ export const ConversationContainer = (props: Props) => {
ChatReply['dynamicTheme']
>(props.initialChatReply.dynamicTheme)
const [theme, setTheme] = createSignal(props.initialChatReply.typebot.theme)
const [isSending, setIsSending] = createSignal(false)

createEffect(() => {
setTheme(
Expand All @@ -66,11 +68,16 @@ export const ConversationContainer = (props: Props) => {
const currentBlockId = chatChunks().at(-1)?.input?.id
if (currentBlockId && props.onAnswer)
props.onAnswer({ message, blockId: currentBlockId })
const longRequest = setTimeout(() => {
setIsSending(true)
}, 1000)
const data = await sendMessageQuery({
apiHost: props.context.apiHost,
sessionId: props.initialChatReply.sessionId,
message,
})
clearTimeout(longRequest)
setIsSending(false)
if (!data) return
if (data.logs) props.onNewLogs?.(data.logs)
if (data.dynamicTheme) setDynamicTheme(data.dynamicTheme)
Expand Down Expand Up @@ -101,7 +108,10 @@ export const ConversationContainer = (props: Props) => {
const lastChunk = chatChunks().at(-1)
if (!lastChunk) return
if (lastChunk.clientSideActions) {
for (const action of lastChunk.clientSideActions) {
const actionsToExecute = lastChunk.clientSideActions.filter((action) =>
isNotDefined(action.lastBubbleBlockId)
)
for (const action of actionsToExecute) {
await executeClientSideAction(action)
}
}
Expand All @@ -110,6 +120,19 @@ export const ConversationContainer = (props: Props) => {
}
}

const handleNewBubbleDisplayed = async (blockId: string) => {
const lastChunk = chatChunks().at(-1)
if (!lastChunk) return
if (lastChunk.clientSideActions) {
const actionsToExecute = lastChunk.clientSideActions.filter(
(action) => action.lastBubbleBlockId === blockId
)
for (const action of actionsToExecute) {
await executeClientSideAction(action)
}
}
}

return (
<div
ref={chatContainer}
Expand All @@ -123,6 +146,8 @@ export const ConversationContainer = (props: Props) => {
input={chatChunk.input}
theme={theme()}
settings={props.initialChatReply.typebot.settings}
isLoadingBubbleDisplayed={isSending()}
onNewBubbleDisplayed={handleNewBubbleDisplayed}
onAllBubblesDisplayed={handleAllBubblesDisplayed}
onSubmit={sendMessage}
onScrollToBottom={autoScrollToBottom}
Expand All @@ -133,6 +158,9 @@ export const ConversationContainer = (props: Props) => {
/>
)}
</For>
<Show when={isSending()}>
<LoadingChunk theme={theme()} />
</Show>
<BottomSpacer ref={bottomSpacer} />
</div>
)
Expand Down
32 changes: 32 additions & 0 deletions packages/js/src/components/ConversationContainer/LoadingChunk.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Theme } from 'models'
import { Show } from 'solid-js'
import { LoadingBubble } from '../bubbles/LoadingBubble'
import { AvatarSideContainer } from './AvatarSideContainer'

type Props = {
theme: Theme
}

export const LoadingChunk = (props: Props) => (
<div class="flex w-full">
<div class="flex flex-col w-full min-w-0">
<div class="flex">
<Show when={props.theme.chat.hostAvatar?.isEnabled}>
<AvatarSideContainer
hostAvatarSrc={props.theme.chat.hostAvatar?.url}
/>
</Show>
<div
class="flex-1"
style={{
'margin-right': props.theme.chat.guestAvatar?.isEnabled
? '50px'
: '0.5rem',
}}
>
<LoadingBubble />
</div>
</div>
</div>
</div>
)
25 changes: 25 additions & 0 deletions packages/js/src/components/bubbles/LoadingBubble.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { TypingBubble } from '@/components'

export const LoadingBubble = () => (
<div class="flex flex-col animate-fade-in">
<div class="flex mb-2 w-full items-center">
<div class={'flex relative items-start typebot-host-bubble'}>
<div
class="flex items-center absolute px-4 py-2 rounded-lg bubble-typing "
style={{
width: '4rem',
height: '2rem',
}}
data-testid="host-bubble"
>
<TypingBubble />
</div>
<p
class={
'overflow-hidden text-fade-in mx-4 my-2 whitespace-pre-wrap slate-html-container relative opacity-0 h-6 text-ellipsis'
}
/>
</div>
</div>
</div>
)
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,8 @@ export const TextBubble = (props: Props) => {
{isTyping() && <TypingBubble />}
</div>
<p
style={{
'text-overflow': 'ellipsis',
}}
class={
'overflow-hidden text-fade-in mx-4 my-2 whitespace-pre-wrap slate-html-container relative ' +
'overflow-hidden text-fade-in mx-4 my-2 whitespace-pre-wrap slate-html-container relative text-ellipsis ' +
(isTyping() ? 'opacity-0 h-6' : 'opacity-100 h-full')
}
innerHTML={props.content.html}
Expand Down
Loading

4 comments on commit 4f78dda

@vercel
Copy link

@vercel vercel bot commented on 4f78dda Jan 27, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vercel
Copy link

@vercel vercel bot commented on 4f78dda Jan 27, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

viewer-v2 – ./apps/viewer

an.nigerias.io
app.yvon.earth
ar.nigerias.io
bot.enreso.org
bot.rslabs.pro
bots.bridge.ai
chat.hayuri.id
chicken.cr8.ai
gollum.riku.ai
gsbulletin.com
panther.cr7.ai
panther.cr8.ai
penguin.cr8.ai
talk.gocare.io
ticketfute.com
unicorn.cr8.ai
apo.nigerias.io
start.taxtree.io
typebot.aloe.bot
voicehelp.cr8.ai
zap.fundviser.in
app.chatforms.net
bot.hostnation.de
bot.maitempah.com
bot.phuonghub.com
bot.reviewzer.com
bot.rihabilita.it
cares.urlabout.me
fmm.wpwakanda.com
gentleman-shop.fr
k1.kandabrand.com
lb.ticketfute.com
ov1.wpwakanda.com
ov2.wpwakanda.com
ov3.wpwakanda.com
viewer.typebot.io
1988.bouclidom.com
andreimayer.com.br
bot.danyservice.it
bot.iconicbrows.it
bot.megafox.com.br
bot.neferlopez.com
bots.robomotion.io
cadu.uninta.edu.br
dicanatural.online
digitalhelp.com.au
goalsettingbot.com
pant.maxbot.com.br
positivobra.com.br
survey.digienge.io
this-is-a-test.com
zap.techadviser.in
bot.boston-voip.com
bot.cabinpromos.com
bot.digitalbled.com
bot.dsignagency.com
bot.eventhub.com.au
bot.jepierre.com.br
bot.viralsangat.com
bot.winglabs.com.br
carsalesenquiry.com
tarian.theiofoundation.org
ted.meujalecobrasil.com.br
type.dericsoncalari.com.br
bot.pinpointinteractive.com
bot.polychromes-project.com
bot.seidinembroseanchetu.it
chatbot.berbelanjabiz.trade
designguide.techyscouts.com
liveconvert2.kandalearn.com
presente.empresarias.com.mx
sell.sellthemotorhome.co.uk
anamnese.odontopavani.com.br
austin.channelautomation.com
bot.marketingplusmindset.com
bot.seidibergamoseanchetu.it
desabafe.sergiolimajr.com.br
piazzatorre.barrettamario.it
type.cookieacademyonline.com
bot.brigadeirosemdrama.com.br
forms.escoladeautomacao.com.br
onboarding.libertydreamcare.ie
type.talitasouzamarques.com.br
agendamento.sergiolimajr.com.br
anamnese.clinicamegasjdr.com.br
bookings.littlepartymonkeys.com
bot.comercializadoraomicron.com
elevateyourmind.groovepages.com
viewer-v2-typebot-io.vercel.app
yourfeedback.comebackreward.com
gerador.verificadordehospedes.com
personal-trainer.barrettamario.it
preagendamento.sergiolimajr.com.br
studiotecnicoimmobiliaremerelli.it
download.thailandmicespecialist.com
register.thailandmicespecialist.com
bot.studiotecnicoimmobiliaremerelli.it
pesquisa.escolamodacomproposito.com.br
anamnese.clinicaramosodontologia.com.br
viewer-v2-git-main-typebot-io.vercel.app

@vercel
Copy link

@vercel vercel bot commented on 4f78dda Jan 27, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

builder-v2 – ./apps/builder

builder-v2-typebot-io.vercel.app
app.typebot.io
builder-v2-git-main-typebot-io.vercel.app

@vercel
Copy link

@vercel vercel bot commented on 4f78dda Jan 27, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

docs – ./apps/docs

docs-typebot-io.vercel.app
docs.typebot.io
docs-git-main-typebot-io.vercel.app

Please sign in to comment.