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

@@ -3231,7 +3231,19 @@
"messages": { "messages": {
"type": "array", "type": "array",
"items": { "items": {
"anyOf": [ "allOf": [
{
"type": "object",
"properties": {
"id": {
"type": "string"
}
},
"required": [
"id"
],
"additionalProperties": false
},
{ {
"anyOf": [ "anyOf": [
{ {
@@ -3239,36 +3251,65 @@
{ {
"anyOf": [ "anyOf": [
{ {
"type": "object", "anyOf": [
"properties": { {
"type": {
"type": "string",
"enum": [
"text"
]
},
"content": {
"type": "object", "type": "object",
"properties": { "properties": {
"html": { "type": {
"type": "string" "type": "string",
"enum": [
"text"
]
}, },
"plainText": { "content": {
"type": "string" "type": "object",
"properties": {
"html": {
"type": "string"
},
"plainText": {
"type": "string"
}
},
"required": [
"html",
"plainText"
],
"additionalProperties": false
} }
}, },
"required": [ "required": [
"html", "type",
"plainText" "content"
],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"image"
]
},
"content": {
"type": "object",
"properties": {
"url": {
"type": "string"
}
},
"additionalProperties": false
}
},
"required": [
"type",
"content"
], ],
"additionalProperties": false "additionalProperties": false
} }
}, ]
"required": [
"type",
"content"
],
"additionalProperties": false
}, },
{ {
"type": "object", "type": "object",
@@ -3276,7 +3317,7 @@
"type": { "type": {
"type": "string", "type": "string",
"enum": [ "enum": [
"image" "video"
] ]
}, },
"content": { "content": {
@@ -3284,6 +3325,17 @@
"properties": { "properties": {
"url": { "url": {
"type": "string" "type": "string"
},
"id": {
"type": "string"
},
"type": {
"type": "string",
"enum": [
"url",
"youtube",
"vimeo"
]
} }
}, },
"additionalProperties": false "additionalProperties": false
@@ -3303,7 +3355,7 @@
"type": { "type": {
"type": "string", "type": "string",
"enum": [ "enum": [
"video" "audio"
] ]
}, },
"content": { "content": {
@@ -3311,17 +3363,6 @@
"properties": { "properties": {
"url": { "url": {
"type": "string" "type": "string"
},
"id": {
"type": "string"
},
"type": {
"type": "string",
"enum": [
"url",
"youtube",
"vimeo"
]
} }
}, },
"additionalProperties": false "additionalProperties": false
@@ -3341,7 +3382,7 @@
"type": { "type": {
"type": "string", "type": "string",
"enum": [ "enum": [
"audio" "embed"
] ]
}, },
"content": { "content": {
@@ -3349,8 +3390,14 @@
"properties": { "properties": {
"url": { "url": {
"type": "string" "type": "string"
},
"height": {
"type": "number"
} }
}, },
"required": [
"height"
],
"additionalProperties": false "additionalProperties": false
} }
}, },
@@ -3361,37 +3408,6 @@
"additionalProperties": false "additionalProperties": false
} }
] ]
},
{
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"embed"
]
},
"content": {
"type": "object",
"properties": {
"url": {
"type": "string"
},
"height": {
"type": "number"
}
},
"required": [
"height"
],
"additionalProperties": false
}
},
"required": [
"type",
"content"
],
"additionalProperties": false
} }
] ]
} }
@@ -4461,7 +4477,16 @@
"clientSideActions": { "clientSideActions": {
"type": "array", "type": "array",
"items": { "items": {
"anyOf": [ "allOf": [
{
"type": "object",
"properties": {
"lastBubbleBlockId": {
"type": "string"
}
},
"additionalProperties": false
},
{ {
"anyOf": [ "anyOf": [
{ {
@@ -4469,88 +4494,163 @@
{ {
"anyOf": [ "anyOf": [
{ {
"type": "object", "anyOf": [
"properties": { {
"codeToExecute": {
"type": "object", "type": "object",
"properties": { "properties": {
"content": { "codeToExecute": {
"type": "string" "type": "object",
}, "properties": {
"args": { "content": {
"type": "array", "type": "string"
"items": { },
"type": "object", "args": {
"properties": { "type": "array",
"id": { "items": {
"type": "string" "type": "object",
}, "properties": {
"value": { "id": {
"anyOf": [ "type": "string"
{
"not": {}
}, },
{ "value": {
"anyOf": [ "anyOf": [
{
"not": {}
},
{ {
"anyOf": [ "anyOf": [
{ {
"type": "string" "anyOf": [
{
"type": "string"
},
{
"type": "number"
}
]
}, },
{ {
"type": "number" "type": "boolean"
} }
] ]
},
{
"type": "boolean"
} }
] ],
"nullable": true
} }
},
"required": [
"id"
], ],
"nullable": true "additionalProperties": false
} }
}, }
"required": [ },
"id" "required": [
], "content",
"additionalProperties": false "args"
} ],
"additionalProperties": false
} }
}, },
"required": [ "required": [
"content", "codeToExecute"
"args" ],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"redirect": {
"type": "object",
"properties": {
"url": {
"type": "string"
},
"isNewTab": {
"type": "boolean"
}
},
"required": [
"isNewTab"
],
"additionalProperties": false
}
},
"required": [
"redirect"
], ],
"additionalProperties": false "additionalProperties": false
} }
}, ]
"required": [
"codeToExecute"
],
"additionalProperties": false
}, },
{ {
"type": "object", "type": "object",
"properties": { "properties": {
"redirect": { "chatwoot": {
"type": "object", "type": "object",
"properties": { "properties": {
"url": { "codeToExecute": {
"type": "string" "type": "object",
}, "properties": {
"isNewTab": { "content": {
"type": "boolean" "type": "string"
},
"args": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"value": {
"anyOf": [
{
"not": {}
},
{
"anyOf": [
{
"anyOf": [
{
"type": "string"
},
{
"type": "number"
}
]
},
{
"type": "boolean"
}
]
}
],
"nullable": true
}
},
"required": [
"id"
],
"additionalProperties": false
}
}
},
"required": [
"content",
"args"
],
"additionalProperties": false
} }
}, },
"required": [ "required": [
"isNewTab" "codeToExecute"
], ],
"additionalProperties": false "additionalProperties": false
} }
}, },
"required": [ "required": [
"redirect" "chatwoot"
], ],
"additionalProperties": false "additionalProperties": false
} }
@@ -4559,71 +4659,30 @@
{ {
"type": "object", "type": "object",
"properties": { "properties": {
"chatwoot": { "googleAnalytics": {
"type": "object", "type": "object",
"properties": { "properties": {
"codeToExecute": { "trackingId": {
"type": "object", "type": "string"
"properties": { },
"content": { "category": {
"type": "string" "type": "string"
}, },
"args": { "action": {
"type": "array", "type": "string"
"items": { },
"type": "object", "label": {
"properties": { "type": "string"
"id": { },
"type": "string" "value": {
}, "type": "number"
"value": {
"anyOf": [
{
"not": {}
},
{
"anyOf": [
{
"anyOf": [
{
"type": "string"
},
{
"type": "number"
}
]
},
{
"type": "boolean"
}
]
}
],
"nullable": true
}
},
"required": [
"id"
],
"additionalProperties": false
}
}
},
"required": [
"content",
"args"
],
"additionalProperties": false
} }
}, },
"required": [
"codeToExecute"
],
"additionalProperties": false "additionalProperties": false
} }
}, },
"required": [ "required": [
"chatwoot" "googleAnalytics"
], ],
"additionalProperties": false "additionalProperties": false
} }
@@ -4632,55 +4691,25 @@
{ {
"type": "object", "type": "object",
"properties": { "properties": {
"googleAnalytics": { "wait": {
"type": "object", "type": "object",
"properties": { "properties": {
"trackingId": { "secondsToWaitFor": {
"type": "string"
},
"category": {
"type": "string"
},
"action": {
"type": "string"
},
"label": {
"type": "string"
},
"value": {
"type": "number" "type": "number"
} }
}, },
"required": [
"secondsToWaitFor"
],
"additionalProperties": false "additionalProperties": false
} }
}, },
"required": [ "required": [
"googleAnalytics" "wait"
], ],
"additionalProperties": false "additionalProperties": false
} }
] ]
},
{
"type": "object",
"properties": {
"wait": {
"type": "object",
"properties": {
"secondsToWaitFor": {
"type": "number"
}
},
"required": [
"secondsToWaitFor"
],
"additionalProperties": false
}
},
"required": [
"wait"
],
"additionalProperties": false
} }
] ]
} }

View File

@@ -51,13 +51,15 @@ if (window.$chatwoot) {
export const executeChatwootBlock = ( export const executeChatwootBlock = (
{ typebot: { variables }, isPreview }: SessionState, { typebot: { variables }, isPreview }: SessionState,
block: ChatwootBlock block: ChatwootBlock,
lastBubbleBlockId?: string
): ExecuteIntegrationResponse => { ): ExecuteIntegrationResponse => {
const chatwootCode = parseChatwootOpenCode(block.options) const chatwootCode = parseChatwootOpenCode(block.options)
return { return {
outgoingEdgeId: block.outgoingEdgeId, outgoingEdgeId: block.outgoingEdgeId,
clientSideActions: [ clientSideActions: [
{ {
lastBubbleBlockId,
chatwoot: { chatwoot: {
codeToExecute: { codeToExecute: {
content: parseVariables(variables, { fieldToParse: 'id' })( content: parseVariables(variables, { fieldToParse: 'id' })(

View File

@@ -4,12 +4,14 @@ import { GoogleAnalyticsBlock, SessionState } from 'models'
export const executeGoogleAnalyticsBlock = ( export const executeGoogleAnalyticsBlock = (
{ typebot: { variables } }: SessionState, { typebot: { variables } }: SessionState,
block: GoogleAnalyticsBlock block: GoogleAnalyticsBlock,
lastBubbleBlockId?: string
): ExecuteIntegrationResponse => ({ ): ExecuteIntegrationResponse => ({
outgoingEdgeId: block.outgoingEdgeId, outgoingEdgeId: block.outgoingEdgeId,
clientSideActions: [ clientSideActions: [
{ {
googleAnalytics: deepParseVariable(variables)(block.options), googleAnalytics: deepParseVariable(variables)(block.options),
lastBubbleBlockId,
}, },
], ],
}) })

View File

@@ -8,7 +8,8 @@ import { CodeBlock, SessionState } from 'models'
export const executeCode = ( export const executeCode = (
{ typebot: { variables } }: SessionState, { typebot: { variables } }: SessionState,
block: CodeBlock block: CodeBlock,
lastBubbleBlockId?: string
): ExecuteLogicResponse => { ): ExecuteLogicResponse => {
if (!block.options.content) return { outgoingEdgeId: block.outgoingEdgeId } if (!block.options.content) return { outgoingEdgeId: block.outgoingEdgeId }
@@ -30,6 +31,7 @@ export const executeCode = (
content, content,
args, args,
}, },
lastBubbleBlockId,
}, },
], ],
} }

View File

@@ -5,13 +5,15 @@ import { sanitizeUrl } from 'utils'
export const executeRedirect = ( export const executeRedirect = (
{ typebot: { variables } }: SessionState, { typebot: { variables } }: SessionState,
block: RedirectBlock block: RedirectBlock,
lastBubbleBlockId?: string
): ExecuteLogicResponse => { ): ExecuteLogicResponse => {
if (!block.options?.url) return { outgoingEdgeId: block.outgoingEdgeId } if (!block.options?.url) return { outgoingEdgeId: block.outgoingEdgeId }
const formattedUrl = sanitizeUrl(parseVariables(variables)(block.options.url)) const formattedUrl = sanitizeUrl(parseVariables(variables)(block.options.url))
return { return {
clientSideActions: [ clientSideActions: [
{ {
lastBubbleBlockId,
redirect: { url: formattedUrl, isNewTab: block.options.isNewTab }, redirect: { url: formattedUrl, isNewTab: block.options.isNewTab },
}, },
], ],

View File

@@ -4,19 +4,29 @@ import { SessionState, WaitBlock } from 'models'
export const executeWait = async ( export const executeWait = async (
{ typebot: { variables } }: SessionState, { typebot: { variables } }: SessionState,
block: WaitBlock block: WaitBlock,
lastBubbleBlockId?: string
): Promise<ExecuteLogicResponse> => { ): Promise<ExecuteLogicResponse> => {
if (!block.options.secondsToWaitFor) if (!block.options.secondsToWaitFor)
return { outgoingEdgeId: block.outgoingEdgeId } return { outgoingEdgeId: block.outgoingEdgeId }
const parsedSecondsToWaitFor = parseVariables(variables)( const parsedSecondsToWaitFor = safeParseInt(
block.options.secondsToWaitFor parseVariables(variables)(block.options.secondsToWaitFor)
) )
return { return {
outgoingEdgeId: block.outgoingEdgeId, outgoingEdgeId: block.outgoingEdgeId,
// @ts-expect-error isNaN can be used with strings clientSideActions: parsedSecondsToWaitFor
clientSideActions: isNaN(parsedSecondsToWaitFor) ? [
? undefined {
: [{ wait: { secondsToWaitFor: parsedSecondsToWaitFor } }], wait: { secondsToWaitFor: parsedSecondsToWaitFor },
lastBubbleBlockId,
},
]
: undefined,
} }
} }
const safeParseInt = (value: string) => {
const parsedValue = parseInt(value)
return isNaN(parsedValue) ? undefined : parsedValue
}

View File

@@ -132,6 +132,7 @@ const parseRetryMessage = (
return { return {
messages: [ messages: [
{ {
id: block.id,
type: BubbleBlockType.TEXT, type: BubbleBlockType.TEXT,
content: { content: {
plainText: retryMessage, plainText: retryMessage,

View File

@@ -29,6 +29,7 @@ export const executeGroup =
currentReply?.clientSideActions currentReply?.clientSideActions
let logs: ChatReply['logs'] = currentReply?.logs let logs: ChatReply['logs'] = currentReply?.logs
let nextEdgeId = null let nextEdgeId = null
let lastBubbleBlockId: string | undefined
let newSessionState = state let newSessionState = state
@@ -39,6 +40,7 @@ export const executeGroup =
messages.push( messages.push(
deepParseVariable(newSessionState.typebot.variables)(block) deepParseVariable(newSessionState.typebot.variables)(block)
) )
lastBubbleBlockId = block.id
continue continue
} }
@@ -63,9 +65,9 @@ export const executeGroup =
logs, logs,
} }
const executionResponse = isLogicBlock(block) const executionResponse = isLogicBlock(block)
? await executeLogic(newSessionState)(block) ? await executeLogic(newSessionState, lastBubbleBlockId)(block)
: isIntegrationBlock(block) : isIntegrationBlock(block)
? await executeIntegration(newSessionState)(block) ? await executeIntegration(newSessionState, lastBubbleBlockId)(block)
: null : null
if (!executionResponse) continue if (!executionResponse) continue

View File

@@ -7,15 +7,15 @@ import { IntegrationBlock, IntegrationBlockType, SessionState } from 'models'
import { ExecuteIntegrationResponse } from '../../types' import { ExecuteIntegrationResponse } from '../../types'
export const executeIntegration = export const executeIntegration =
(state: SessionState) => (state: SessionState, lastBubbleBlockId?: string) =>
async (block: IntegrationBlock): Promise<ExecuteIntegrationResponse> => { async (block: IntegrationBlock): Promise<ExecuteIntegrationResponse> => {
switch (block.type) { switch (block.type) {
case IntegrationBlockType.GOOGLE_SHEETS: case IntegrationBlockType.GOOGLE_SHEETS:
return executeGoogleSheetBlock(state, block) return executeGoogleSheetBlock(state, block)
case IntegrationBlockType.CHATWOOT: case IntegrationBlockType.CHATWOOT:
return executeChatwootBlock(state, block) return executeChatwootBlock(state, block, lastBubbleBlockId)
case IntegrationBlockType.GOOGLE_ANALYTICS: case IntegrationBlockType.GOOGLE_ANALYTICS:
return executeGoogleAnalyticsBlock(state, block) return executeGoogleAnalyticsBlock(state, block, lastBubbleBlockId)
case IntegrationBlockType.EMAIL: case IntegrationBlockType.EMAIL:
return executeSendEmailBlock(state, block) return executeSendEmailBlock(state, block)
case IntegrationBlockType.WEBHOOK: case IntegrationBlockType.WEBHOOK:

View File

@@ -8,7 +8,7 @@ import { LogicBlock, LogicBlockType, SessionState } from 'models'
import { ExecuteLogicResponse } from '../../types' import { ExecuteLogicResponse } from '../../types'
export const executeLogic = export const executeLogic =
(state: SessionState) => (state: SessionState, lastBubbleBlockId?: string) =>
async (block: LogicBlock): Promise<ExecuteLogicResponse> => { async (block: LogicBlock): Promise<ExecuteLogicResponse> => {
switch (block.type) { switch (block.type) {
case LogicBlockType.SET_VARIABLE: case LogicBlockType.SET_VARIABLE:
@@ -16,12 +16,12 @@ export const executeLogic =
case LogicBlockType.CONDITION: case LogicBlockType.CONDITION:
return executeCondition(state, block) return executeCondition(state, block)
case LogicBlockType.REDIRECT: case LogicBlockType.REDIRECT:
return executeRedirect(state, block) return executeRedirect(state, block, lastBubbleBlockId)
case LogicBlockType.CODE: case LogicBlockType.CODE:
return executeCode(state, block) return executeCode(state, block, lastBubbleBlockId)
case LogicBlockType.TYPEBOT_LINK: case LogicBlockType.TYPEBOT_LINK:
return executeTypebotLink(state, block) return executeTypebotLink(state, block)
case LogicBlockType.WAIT: case LogicBlockType.WAIT:
return executeWait(state, block) return executeWait(state, block, lastBubbleBlockId)
} }
} }

View File

@@ -10,6 +10,8 @@ type Props = Pick<ChatReply, 'messages' | 'input'> & {
settings: Settings settings: Settings
inputIndex: number inputIndex: number
context: BotContext context: BotContext
isLoadingBubbleDisplayed: boolean
onNewBubbleDisplayed: (blockId: string) => Promise<void>
onScrollToBottom: () => void onScrollToBottom: () => void
onSubmit: (input: string) => void onSubmit: (input: string) => void
onSkip: () => void onSkip: () => void
@@ -26,7 +28,9 @@ export const ChatChunk = (props: Props) => {
props.onScrollToBottom() props.onScrollToBottom()
}) })
const displayNextMessage = () => { const displayNextMessage = async () => {
const lastBubbleBlockId = props.messages[displayedMessageIndex()].id
await props.onNewBubbleDisplayed(lastBubbleBlockId)
setDisplayedMessageIndex( setDisplayedMessageIndex(
displayedMessageIndex() === props.messages.length displayedMessageIndex() === props.messages.length
? displayedMessageIndex() ? displayedMessageIndex()

View File

@@ -1,10 +1,11 @@
import type { ChatReply, Theme } from 'models' 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 { sendMessageQuery } from '@/queries/sendMessageQuery'
import { ChatChunk } from './ChatChunk' import { ChatChunk } from './ChatChunk'
import { BotContext, InitialChatReply } from '@/types' import { BotContext, InitialChatReply } from '@/types'
import { isNotDefined } from 'utils' import { isNotDefined } from 'utils'
import { executeClientSideAction } from '@/utils/executeClientSideActions' import { executeClientSideAction } from '@/utils/executeClientSideActions'
import { LoadingChunk } from './LoadingChunk'
const parseDynamicTheme = ( const parseDynamicTheme = (
initialTheme: Theme, initialTheme: Theme,
@@ -55,6 +56,7 @@ export const ConversationContainer = (props: Props) => {
ChatReply['dynamicTheme'] ChatReply['dynamicTheme']
>(props.initialChatReply.dynamicTheme) >(props.initialChatReply.dynamicTheme)
const [theme, setTheme] = createSignal(props.initialChatReply.typebot.theme) const [theme, setTheme] = createSignal(props.initialChatReply.typebot.theme)
const [isSending, setIsSending] = createSignal(false)
createEffect(() => { createEffect(() => {
setTheme( setTheme(
@@ -66,11 +68,16 @@ export const ConversationContainer = (props: Props) => {
const currentBlockId = chatChunks().at(-1)?.input?.id const currentBlockId = chatChunks().at(-1)?.input?.id
if (currentBlockId && props.onAnswer) if (currentBlockId && props.onAnswer)
props.onAnswer({ message, blockId: currentBlockId }) props.onAnswer({ message, blockId: currentBlockId })
const longRequest = setTimeout(() => {
setIsSending(true)
}, 1000)
const data = await sendMessageQuery({ const data = await sendMessageQuery({
apiHost: props.context.apiHost, apiHost: props.context.apiHost,
sessionId: props.initialChatReply.sessionId, sessionId: props.initialChatReply.sessionId,
message, message,
}) })
clearTimeout(longRequest)
setIsSending(false)
if (!data) return if (!data) return
if (data.logs) props.onNewLogs?.(data.logs) if (data.logs) props.onNewLogs?.(data.logs)
if (data.dynamicTheme) setDynamicTheme(data.dynamicTheme) if (data.dynamicTheme) setDynamicTheme(data.dynamicTheme)
@@ -101,7 +108,10 @@ export const ConversationContainer = (props: Props) => {
const lastChunk = chatChunks().at(-1) const lastChunk = chatChunks().at(-1)
if (!lastChunk) return if (!lastChunk) return
if (lastChunk.clientSideActions) { 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) 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 ( return (
<div <div
ref={chatContainer} ref={chatContainer}
@@ -123,6 +146,8 @@ export const ConversationContainer = (props: Props) => {
input={chatChunk.input} input={chatChunk.input}
theme={theme()} theme={theme()}
settings={props.initialChatReply.typebot.settings} settings={props.initialChatReply.typebot.settings}
isLoadingBubbleDisplayed={isSending()}
onNewBubbleDisplayed={handleNewBubbleDisplayed}
onAllBubblesDisplayed={handleAllBubblesDisplayed} onAllBubblesDisplayed={handleAllBubblesDisplayed}
onSubmit={sendMessage} onSubmit={sendMessage}
onScrollToBottom={autoScrollToBottom} onScrollToBottom={autoScrollToBottom}
@@ -133,6 +158,9 @@ export const ConversationContainer = (props: Props) => {
/> />
)} )}
</For> </For>
<Show when={isSending()}>
<LoadingChunk theme={theme()} />
</Show>
<BottomSpacer ref={bottomSpacer} /> <BottomSpacer ref={bottomSpacer} />
</div> </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 />} {isTyping() && <TypingBubble />}
</div> </div>
<p <p
style={{
'text-overflow': 'ellipsis',
}}
class={ 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') (isTyping() ? 'opacity-0 h-6' : 'opacity-100 h-full')
} }
innerHTML={props.content.html} innerHTML={props.content.html}

View File

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