🚸 (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:
@ -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()
|
||||
|
@ -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>
|
||||
)
|
||||
|
@ -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
packages/js/src/components/bubbles/LoadingBubble.tsx
Normal file
25
packages/js/src/components/bubbles/LoadingBubble.tsx
Normal 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>
|
||||
)
|
@ -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}
|
||||
|
@ -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({
|
||||
|
Reference in New Issue
Block a user