Stream bubble content if placed right after Op…

Closes #617
This commit is contained in:
Baptiste Arnaud
2023-07-19 11:20:44 +02:00
parent 6c540657a6
commit 3952ae2755
14 changed files with 231 additions and 34 deletions

View File

@@ -1,10 +1,11 @@
import { BotContext } from '@/types'
import { BotContext, ChatChunk as ChatChunkType } from '@/types'
import { isMobile } from '@/utils/isMobileSignal'
import type { ChatReply, Settings, Theme } from '@typebot.io/schemas'
import { createSignal, For, onMount, Show } from 'solid-js'
import { HostBubble } from '../bubbles/HostBubble'
import { InputChatBlock } from '../InputChatBlock'
import { AvatarSideContainer } from './AvatarSideContainer'
import { StreamingBubble } from '../bubbles/StreamingBubble'
type Props = Pick<ChatReply, 'messages' | 'input'> & {
theme: Theme
@@ -13,6 +14,7 @@ type Props = Pick<ChatReply, 'messages' | 'input'> & {
context: BotContext
hasError: boolean
hideAvatar: boolean
streamingMessageId: ChatChunkType['streamingMessageId']
onNewBubbleDisplayed: (blockId: string) => Promise<void>
onScrollToBottom: (top?: number) => void
onSubmit: (input: string) => void
@@ -25,6 +27,7 @@ export const ChatChunk = (props: Props) => {
const [displayedMessageIndex, setDisplayedMessageIndex] = createSignal(0)
onMount(() => {
if (props.streamingMessageId) return
if (props.messages.length === 0) {
props.onAllBubblesDisplayed()
}
@@ -101,6 +104,31 @@ export const ChatChunk = (props: Props) => {
hasError={props.hasError}
/>
)}
<Show when={props.streamingMessageId} keyed>
{(streamingMessageId) => (
<div class={'flex' + (isMobile() ? ' gap-1' : ' gap-2')}>
<Show when={props.theme.chat.hostAvatar?.isEnabled}>
<AvatarSideContainer
hostAvatarSrc={props.theme.chat.hostAvatar?.url}
hideAvatar={props.hideAvatar}
/>
</Show>
<div
class="flex flex-col flex-1 gap-2"
style={{
'margin-right': props.theme.chat.guestAvatar?.isEnabled
? isMobile()
? '32px'
: '48px'
: undefined,
}}
>
<StreamingBubble streamingMessageId={streamingMessageId} />
</div>
</div>
)}
</Show>
</div>
)
}

View File

@@ -1,13 +1,26 @@
import { ChatReply, SendMessageInput, Theme } from '@typebot.io/schemas'
import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/enums'
import { createEffect, createSignal, For, onMount, Show } from 'solid-js'
import {
createEffect,
createSignal,
createUniqueId,
For,
onMount,
Show,
} from 'solid-js'
import { sendMessageQuery } from '@/queries/sendMessageQuery'
import { ChatChunk } from './ChatChunk'
import { BotContext, InitialChatReply, OutgoingLog } from '@/types'
import {
BotContext,
ChatChunk as ChatChunkType,
InitialChatReply,
OutgoingLog,
} from '@/types'
import { isNotDefined } from '@typebot.io/lib'
import { executeClientSideAction } from '@/utils/executeClientSideActions'
import { LoadingChunk } from './LoadingChunk'
import { PopupBlockedToast } from './PopupBlockedToast'
import { setStreamingMessage } from '@/utils/streamingMessageSignal'
const parseDynamicTheme = (
initialTheme: Theme,
@@ -44,9 +57,7 @@ type Props = {
export const ConversationContainer = (props: Props) => {
let chatContainer: HTMLDivElement | undefined
const [chatChunks, setChatChunks] = createSignal<
Pick<ChatReply, 'messages' | 'input' | 'clientSideActions'>[]
>([
const [chatChunks, setChatChunks] = createSignal<ChatChunkType[]>([
{
input: props.initialChatReply.input,
messages: props.initialChatReply.messages,
@@ -74,9 +85,13 @@ export const ConversationContainer = (props: Props) => {
'webhookToExecute' in action
)
setIsSending(true)
const response = await executeClientSideAction(action, {
apiHost: props.context.apiHost,
sessionId: props.initialChatReply.sessionId,
const response = await executeClientSideAction({
clientSideAction: action,
context: {
apiHost: props.context.apiHost,
sessionId: props.initialChatReply.sessionId,
},
onMessageStream: streamMessage,
})
if (response && 'replyToSend' in response) {
sendMessage(response.replyToSend, response.logs)
@@ -89,6 +104,22 @@ export const ConversationContainer = (props: Props) => {
})()
})
const streamMessage = (content: string) => {
setIsSending(false)
const lastChunk = chatChunks().at(-1)
if (!lastChunk) return
const id = lastChunk.streamingMessageId ?? createUniqueId()
if (!lastChunk.streamingMessageId)
setChatChunks((displayedChunks) => [
...displayedChunks,
{
messages: [],
streamingMessageId: id,
},
])
setStreamingMessage({ id, content })
}
createEffect(() => {
setTheme(
parseDynamicTheme(props.initialChatReply.typebot.theme, dynamicTheme())
@@ -151,9 +182,13 @@ export const ConversationContainer = (props: Props) => {
'webhookToExecute' in action
)
setIsSending(true)
const response = await executeClientSideAction(action, {
apiHost: props.context.apiHost,
sessionId: props.initialChatReply.sessionId,
const response = await executeClientSideAction({
clientSideAction: action,
context: {
apiHost: props.context.apiHost,
sessionId: props.initialChatReply.sessionId,
},
onMessageStream: streamMessage,
})
if (response && 'replyToSend' in response) {
sendMessage(response.replyToSend, response.logs)
@@ -167,7 +202,9 @@ export const ConversationContainer = (props: Props) => {
...displayedChunks,
{
input: data.input,
messages: data.messages,
messages: chatChunks().at(-1)?.streamingMessageId
? data.messages.slice(1)
: data.messages,
clientSideActions: data.clientSideActions,
},
])
@@ -200,9 +237,13 @@ export const ConversationContainer = (props: Props) => {
'webhookToExecute' in action
)
setIsSending(true)
const response = await executeClientSideAction(action, {
apiHost: props.context.apiHost,
sessionId: props.initialChatReply.sessionId,
const response = await executeClientSideAction({
clientSideAction: action,
context: {
apiHost: props.context.apiHost,
sessionId: props.initialChatReply.sessionId,
},
onMessageStream: streamMessage,
})
if (response && 'replyToSend' in response) {
sendMessage(response.replyToSend, response.logs)
@@ -229,14 +270,19 @@ export const ConversationContainer = (props: Props) => {
input={chatChunk.input}
theme={theme()}
settings={props.initialChatReply.typebot.settings}
streamingMessageId={chatChunk.streamingMessageId}
context={props.context}
hideAvatar={
!chatChunk.input &&
!chatChunk.streamingMessageId &&
index() < chatChunks().length - 1
}
hasError={hasError() && index() === chatChunks().length - 1}
onNewBubbleDisplayed={handleNewBubbleDisplayed}
onAllBubblesDisplayed={handleAllBubblesDisplayed}
onSubmit={sendMessage}
onScrollToBottom={autoScrollToBottom}
onSkip={handleSkip}
context={props.context}
hasError={hasError() && index() === chatChunks().length - 1}
hideAvatar={!chatChunk.input && index() < chatChunks().length - 1}
/>
)}
</For>

View File

@@ -0,0 +1,40 @@
import { streamingMessage } from '@/utils/streamingMessageSignal'
import { createEffect, createSignal } from 'solid-js'
type Props = {
streamingMessageId: string
}
export const StreamingBubble = (props: Props) => {
let ref: HTMLDivElement | undefined
const [content, setContent] = createSignal<string>('')
createEffect(() => {
if (streamingMessage()?.id === props.streamingMessageId)
setContent(streamingMessage()?.content ?? '')
})
return (
<div class="flex flex-col animate-fade-in" ref={ref}>
<div class="flex w-full items-center">
<div class="flex relative items-start typebot-host-bubble">
<div
class="flex items-center absolute px-4 py-2 bubble-typing "
style={{
width: '100%',
height: '100%',
}}
data-testid="host-bubble"
/>
<div
class={
'overflow-hidden text-fade-in mx-4 my-2 whitespace-pre-wrap slate-html-container relative text-ellipsis opacity-100 h-full'
}
>
{content()}
</div>
</div>
</div>
</div>
)
}