2
0

🚸 (engine) Improve engine v2 client loading and timings

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
This commit is contained in:
Baptiste Arnaud
2023-01-27 10:54:59 +01:00
parent a738897dbb
commit 4f78dda640
16 changed files with 408 additions and 262 deletions

View File

@ -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
@ -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()

View File

@ -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,
@ -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(
@ -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)
@ -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)
}
}
@ -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}
@ -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}
@ -133,6 +158,9 @@ export const ConversationContainer = (props: Props) => {
/>
)}
</For>
<Show when={isSending()}>
<LoadingChunk theme={theme()} />
</Show>
<BottomSpacer ref={bottomSpacer} />
</div>
)

View File

@ -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>
)

View File

@ -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>
)

View File

@ -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}

View File

@ -87,11 +87,15 @@ const embedMessageSchema = z.object({
content: embedBubbleContentSchema,
})
const chatMessageSchema = textMessageSchema
.or(imageMessageSchema)
.or(videoMessageSchema)
.or(audioMessageSchema)
.or(embedMessageSchema)
const chatMessageSchema = z
.object({ id: z.string() })
.and(
textMessageSchema
.or(imageMessageSchema)
.or(videoMessageSchema)
.or(audioMessageSchema)
.or(embedMessageSchema)
)
const codeToExecuteSchema = z.object({
content: z.string(),
@ -167,29 +171,35 @@ const replyLogSchema = logSchema
const clientSideActionSchema = z
.object({
codeToExecute: codeToExecuteSchema,
lastBubbleBlockId: z.string().optional(),
})
.or(
z.object({
redirect: redirectOptionsSchema,
})
)
.or(
z.object({
chatwoot: z.object({ codeToExecute: codeToExecuteSchema }),
})
)
.or(
z.object({
googleAnalytics: googleAnalyticsOptionsSchema,
})
)
.or(
z.object({
wait: z.object({
secondsToWaitFor: z.number(),
}),
})
.and(
z
.object({
codeToExecute: codeToExecuteSchema,
})
.or(
z.object({
redirect: redirectOptionsSchema,
})
)
.or(
z.object({
chatwoot: z.object({ codeToExecute: codeToExecuteSchema }),
})
)
.or(
z.object({
googleAnalytics: googleAnalyticsOptionsSchema,
})
)
.or(
z.object({
wait: z.object({
secondsToWaitFor: z.number(),
}),
})
)
)
export const chatReplySchema = z.object({