♻️ Introduce typebot v6 with events (#1013)

Closes #885
This commit is contained in:
Baptiste Arnaud
2023-11-08 15:34:16 +01:00
committed by GitHub
parent 68e4fc71fb
commit 35300eaf34
634 changed files with 58971 additions and 31449 deletions

View File

@@ -12,21 +12,22 @@ import {
} from '@/utils/storage'
import { setCssVariablesValue } from '@/utils/setCssVariablesValue'
import immutableCss from '../assets/immutable.css'
import { InputBlock, StartElementId } from '@typebot.io/schemas'
import { defaultTheme } from '@typebot.io/schemas/features/typebot/theme/constants'
export type BotProps = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
typebot: string | any
isPreview?: boolean
resultId?: string
startGroupId?: string
prefilledVariables?: Record<string, unknown>
apiHost?: string
onNewInputBlock?: (ids: { id: string; groupId: string }) => void
onNewInputBlock?: (inputBlock: InputBlock) => void
onAnswer?: (answer: { message: string; blockId: string }) => void
onInit?: () => void
onEnd?: () => void
onNewLogs?: (logs: OutgoingLog[]) => void
}
} & StartElementId
export const Bot = (props: BotProps & { class?: string }) => {
const [initialChatReply, setInitialChatReply] = createSignal<
@@ -54,11 +55,15 @@ export const Bot = (props: BotProps & { class?: string }) => {
resultId: isNotEmpty(props.resultId)
? props.resultId
: getExistingResultIdFromStorage(typebotIdFromProps),
startGroupId: props.startGroupId,
prefilledVariables: {
...prefilledVariables,
...props.prefilledVariables,
},
...('startGroupId' in props
? { startGroupId: props.startGroupId }
: 'startEventId' in props
? { startEventId: props.startEventId }
: {}),
})
if (error && 'code' in error && typeof error.code === 'string') {
if (typeof props.typebot !== 'string' || (props.isPreview ?? false)) {
@@ -80,7 +85,7 @@ export const Bot = (props: BotProps & { class?: string }) => {
}
if (data.resultId && typebotIdFromProps)
setResultInStorage(data.typebot.settings.general.rememberUser?.storage)(
setResultInStorage(data.typebot.settings.general?.rememberUser?.storage)(
typebotIdFromProps,
data.resultId
)
@@ -88,10 +93,7 @@ export const Bot = (props: BotProps & { class?: string }) => {
setCustomCss(data.typebot.theme.customCss ?? '')
if (data.input?.id && props.onNewInputBlock)
props.onNewInputBlock({
id: data.input.id,
groupId: data.input.groupId,
})
props.onNewInputBlock(data.input)
if (data.logs) props.onNewLogs?.(data.logs)
}
@@ -157,7 +159,7 @@ type BotContentProps = {
initialChatReply: InitialChatReply
context: BotContext
class?: string
onNewInputBlock?: (block: { id: string; groupId: string }) => void
onNewInputBlock?: (inputBlock: InputBlock) => void
onAnswer?: (answer: { message: string; blockId: string }) => void
onEnd?: () => void
onNewLogs?: (logs: OutgoingLog[]) => void
@@ -176,13 +178,15 @@ const BotContent = (props: BotContentProps) => {
existingFont
?.getAttribute('href')
?.includes(
props.initialChatReply.typebot?.theme?.general?.font ?? 'Open Sans'
props.initialChatReply.typebot?.theme?.general?.font ??
defaultTheme.general.font
)
)
return
const font = document.createElement('link')
font.href = `https://fonts.bunny.net/css2?family=${
props.initialChatReply.typebot?.theme?.general?.font ?? 'Open Sans'
props.initialChatReply.typebot?.theme?.general?.font ??
defaultTheme.general.font
}:ital,wght@0,300;0,400;0,600;1,300;1,400;1,600&display=swap');')`
font.rel = 'stylesheet'
font.id = 'bot-font'
@@ -224,7 +228,9 @@ const BotContent = (props: BotContentProps) => {
/>
</div>
<Show
when={props.initialChatReply.typebot.settings.general.isBrandingEnabled}
when={
props.initialChatReply.typebot.settings.general?.isBrandingEnabled
}
>
<LiteBadge botContainer={botContainer} />
</Show>

View File

@@ -1,11 +1,13 @@
import { BotContext, ChatChunk as ChatChunkType } from '@/types'
import { isMobile } from '@/utils/isMobileSignal'
import type { ChatReply, Settings, Theme } from '@typebot.io/schemas'
import { 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'
import { defaultTheme } from '@typebot.io/schemas/features/typebot/theme/constants'
import { defaultSettings } from '@typebot.io/schemas/features/typebot/settings/constants'
type Props = Pick<ChatReply, 'messages' | 'input'> & {
theme: Theme
@@ -56,12 +58,13 @@ export const ChatChunk = (props: Props) => {
<div class={'flex' + (isMobile() ? ' gap-1' : ' gap-2')}>
<Show
when={
props.theme.chat.hostAvatar?.isEnabled &&
(props.theme.chat?.hostAvatar?.isEnabled ??
defaultTheme.chat.hostAvatar.isEnabled) &&
props.messages.length > 0
}
>
<AvatarSideContainer
hostAvatarSrc={props.theme.chat.hostAvatar?.url}
hostAvatarSrc={props.theme.chat?.hostAvatar?.url}
hideAvatar={props.hideAvatar}
/>
</Show>
@@ -69,13 +72,15 @@ export const ChatChunk = (props: Props) => {
<div
class="flex flex-col flex-1 gap-2"
style={{
'max-width': props.theme.chat.guestAvatar?.isEnabled
? isMobile()
? 'calc(100% - 32px - 32px)'
: 'calc(100% - 48px - 48px)'
: isMobile()
? 'calc(100% - 32px)'
: 'calc(100% - 48px)',
'max-width':
props.theme.chat?.guestAvatar?.isEnabled ??
defaultTheme.chat.guestAvatar.isEnabled
? isMobile()
? 'calc(100% - 32px - 32px)'
: 'calc(100% - 48px - 48px)'
: isMobile()
? 'calc(100% - 32px)'
: 'calc(100% - 48px)',
}}
>
<For each={props.messages.slice(0, displayedMessageIndex() + 1)}>
@@ -95,11 +100,15 @@ export const ChatChunk = (props: Props) => {
ref={inputRef}
block={props.input}
inputIndex={props.inputIndex}
hasHostAvatar={props.theme.chat.hostAvatar?.isEnabled ?? false}
guestAvatar={props.theme.chat.guestAvatar}
hasHostAvatar={
props.theme.chat?.hostAvatar?.isEnabled ??
defaultTheme.chat.hostAvatar.isEnabled
}
guestAvatar={props.theme.chat?.guestAvatar}
context={props.context}
isInputPrefillEnabled={
props.settings.general.isInputPrefillEnabled ?? true
props.settings.general?.isInputPrefillEnabled ??
defaultSettings.general.isInputPrefillEnabled
}
hasError={props.hasError}
onSubmit={props.onSubmit}
@@ -109,9 +118,14 @@ export const ChatChunk = (props: Props) => {
<Show when={props.streamingMessageId} keyed>
{(streamingMessageId) => (
<div class={'flex' + (isMobile() ? ' gap-1' : ' gap-2')}>
<Show when={props.theme.chat.hostAvatar?.isEnabled}>
<Show
when={
props.theme.chat?.hostAvatar?.isEnabled ??
defaultTheme.chat.hostAvatar.isEnabled
}
>
<AvatarSideContainer
hostAvatarSrc={props.theme.chat.hostAvatar?.url}
hostAvatarSrc={props.theme.chat?.hostAvatar?.url}
hideAvatar={props.hideAvatar}
/>
</Show>
@@ -119,13 +133,15 @@ export const ChatChunk = (props: Props) => {
<div
class="flex flex-col flex-1 gap-2"
style={{
'max-width': props.theme.chat.guestAvatar?.isEnabled
? isMobile()
? 'calc(100% - 32px - 32px)'
: 'calc(100% - 48px - 48px)'
: isMobile()
? 'calc(100% - 32px)'
: 'calc(100% - 48px)',
'max-width':
props.theme.chat?.hostAvatar?.isEnabled ??
defaultTheme.chat.hostAvatar.isEnabled
? isMobile()
? 'calc(100% - 32px - 32px)'
: 'calc(100% - 48px - 48px)'
: isMobile()
? 'calc(100% - 32px)'
: 'calc(100% - 48px)',
}}
>
<StreamingBubble streamingMessageId={streamingMessageId} />

View File

@@ -1,5 +1,9 @@
import { ChatReply, SendMessageInput, Theme } from '@typebot.io/schemas'
import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/enums'
import {
ChatReply,
InputBlock,
SendMessageInput,
Theme,
} from '@typebot.io/schemas'
import {
createEffect,
createSignal,
@@ -25,6 +29,7 @@ import {
formattedMessages,
setFormattedMessages,
} from '@/utils/formattedMessagesSignal'
import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants'
const parseDynamicTheme = (
initialTheme: Theme,
@@ -34,26 +39,26 @@ const parseDynamicTheme = (
chat: {
...initialTheme.chat,
hostAvatar:
initialTheme.chat.hostAvatar && dynamicTheme?.hostAvatarUrl
initialTheme.chat?.hostAvatar && dynamicTheme?.hostAvatarUrl
? {
...initialTheme.chat.hostAvatar,
url: dynamicTheme.hostAvatarUrl,
}
: initialTheme.chat.hostAvatar,
: initialTheme.chat?.hostAvatar,
guestAvatar:
initialTheme.chat.guestAvatar && dynamicTheme?.guestAvatarUrl
initialTheme.chat?.guestAvatar && dynamicTheme?.guestAvatarUrl
? {
...initialTheme.chat.guestAvatar,
url: dynamicTheme?.guestAvatarUrl,
}
: initialTheme.chat.guestAvatar,
: initialTheme.chat?.guestAvatar,
},
})
type Props = {
initialChatReply: InitialChatReply
context: BotContext
onNewInputBlock?: (ids: { id: string; groupId: string }) => void
onNewInputBlock?: (inputBlock: InputBlock) => void
onAnswer?: (answer: { message: string; blockId: string }) => void
onEnd?: () => void
onNewLogs?: (logs: OutgoingLog[]) => void
@@ -178,11 +183,8 @@ export const ConversationContainer = (props: Props) => {
}
if (data.logs) props.onNewLogs?.(data.logs)
if (data.dynamicTheme) setDynamicTheme(data.dynamicTheme)
if (data.input?.id && props.onNewInputBlock) {
props.onNewInputBlock({
id: data.input.id,
groupId: data.input.groupId,
})
if (data.input && props.onNewInputBlock) {
props.onNewInputBlock(data.input)
}
if (data.clientSideActions) {
const actionsBeforeFirstBubble = data.clientSideActions.filter((action) =>

View File

@@ -2,6 +2,7 @@ import { Theme } from '@typebot.io/schemas'
import { Show } from 'solid-js'
import { LoadingBubble } from '../bubbles/LoadingBubble'
import { AvatarSideContainer } from './AvatarSideContainer'
import { defaultTheme } from '@typebot.io/schemas/features/typebot/theme/constants'
type Props = {
theme: Theme
@@ -11,9 +12,14 @@ export const LoadingChunk = (props: Props) => (
<div class="flex w-full">
<div class="flex flex-col w-full min-w-0">
<div class="flex gap-2">
<Show when={props.theme.chat.hostAvatar?.isEnabled}>
<Show
when={
props.theme.chat?.hostAvatar?.isEnabled ??
defaultTheme.chat.hostAvatar.isEnabled
}
>
<AvatarSideContainer
hostAvatarSrc={props.theme.chat.hostAvatar?.url}
hostAvatarSrc={props.theme.chat?.hostAvatar?.url}
/>
</Show>
<LoadingBubble />

View File

@@ -1,11 +1,9 @@
import type {
ChatReply,
ChoiceInputBlock,
DateInputOptions,
EmailInputBlock,
FileInputBlock,
NumberInputBlock,
PaymentInputOptions,
PhoneNumberInputBlock,
RatingInputBlock,
RuntimeOptions,
@@ -13,8 +11,9 @@ import type {
Theme,
UrlInputBlock,
PictureChoiceBlock,
PaymentInputBlock,
DateInputBlock,
} from '@typebot.io/schemas'
import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/enums'
import { GuestBubble } from './bubbles/GuestBubble'
import { BotContext, InputSubmitContent } from '@/types'
import { TextInput } from '@/features/blocks/inputs/textInput'
@@ -34,12 +33,15 @@ import { Buttons } from '@/features/blocks/inputs/buttons/components/Buttons'
import { SinglePictureChoice } from '@/features/blocks/inputs/pictureChoice/SinglePictureChoice'
import { MultiplePictureChoice } from '@/features/blocks/inputs/pictureChoice/MultiplePictureChoice'
import { formattedMessages } from '@/utils/formattedMessagesSignal'
import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants'
import { defaultPaymentInputOptions } from '@typebot.io/schemas/features/blocks/inputs/payment/constants'
import { defaultTheme } from '@typebot.io/schemas/features/typebot/theme/constants'
type Props = {
ref: HTMLDivElement | undefined
block: NonNullable<ChatReply['input']>
hasHostAvatar: boolean
guestAvatar?: Theme['chat']['guestAvatar']
guestAvatar?: NonNullable<Theme['chat']>['guestAvatar']
inputIndex: number
context: BotContext
isInputPrefillEnabled: boolean
@@ -74,7 +76,10 @@ export const InputChatBlock = (props: Props) => {
<Match when={answer() && !props.hasError}>
<GuestBubble
message={formattedMessage() ?? (answer() as string)}
showAvatar={props.guestAvatar?.isEnabled ?? false}
showAvatar={
props.guestAvatar?.isEnabled ??
defaultTheme.chat.guestAvatar.isEnabled
}
avatarSrc={props.guestAvatar?.url && props.guestAvatar.url}
/>
</Match>
@@ -122,8 +127,8 @@ const Input = (props: {
const submitPaymentSuccess = () =>
props.onSubmit({
value:
(props.block.options as PaymentInputOptions).labels.success ??
'Success',
(props.block.options as PaymentInputBlock['options'])?.labels
?.success ?? defaultPaymentInputOptions.labels.success,
})
return (
@@ -158,9 +163,9 @@ const Input = (props: {
</Match>
<Match when={props.block.type === InputBlockType.PHONE}>
<PhoneInput
labels={(props.block as PhoneNumberInputBlock).options.labels}
labels={(props.block as PhoneNumberInputBlock).options?.labels}
defaultCountryCode={
(props.block as PhoneNumberInputBlock).options.defaultCountryCode
(props.block as PhoneNumberInputBlock).options?.defaultCountryCode
}
defaultValue={getPrefilledValue()}
onSubmit={onSubmit}
@@ -168,7 +173,7 @@ const Input = (props: {
</Match>
<Match when={props.block.type === InputBlockType.DATE}>
<DateForm
options={props.block.options as DateInputOptions}
options={props.block.options as DateInputBlock['options']}
defaultValue={getPrefilledValue()}
onSubmit={onSubmit}
/>
@@ -176,7 +181,7 @@ const Input = (props: {
<Match when={isButtonsBlock(props.block)} keyed>
{(block) => (
<Switch>
<Match when={!block.options.isMultipleChoice}>
<Match when={!block.options?.isMultipleChoice}>
<Buttons
inputIndex={props.inputIndex}
defaultItems={block.items}
@@ -184,7 +189,7 @@ const Input = (props: {
onSubmit={onSubmit}
/>
</Match>
<Match when={block.options.isMultipleChoice}>
<Match when={block.options?.isMultipleChoice}>
<MultipleChoicesForm
inputIndex={props.inputIndex}
defaultItems={block.items}
@@ -198,14 +203,14 @@ const Input = (props: {
<Match when={isPictureChoiceBlock(props.block)} keyed>
{(block) => (
<Switch>
<Match when={!block.options.isMultipleChoice}>
<Match when={!block.options?.isMultipleChoice}>
<SinglePictureChoice
defaultItems={block.items}
options={block.options}
onSubmit={onSubmit}
/>
</Match>
<Match when={block.options.isMultipleChoice}>
<Match when={block.options?.isMultipleChoice}>
<MultiplePictureChoice
defaultItems={block.items}
options={block.options}
@@ -237,7 +242,7 @@ const Input = (props: {
{
...props.block.options,
...props.block.runtimeOptions,
} as PaymentInputOptions & RuntimeOptions
} as PaymentInputBlock['options'] & RuntimeOptions
}
onSuccess={submitPaymentSuccess}
/>

View File

@@ -4,20 +4,20 @@ import { ImageBubble } from '@/features/blocks/bubbles/image'
import { TextBubble } from '@/features/blocks/bubbles/textBubble'
import { VideoBubble } from '@/features/blocks/bubbles/video'
import type {
AudioBubbleContent,
AudioBubbleBlock,
ChatMessage,
EmbedBubbleContent,
ImageBubbleContent,
TextBubbleContent,
TypingEmulation,
VideoBubbleContent,
EmbedBubbleBlock,
ImageBubbleBlock,
Settings,
TextBubbleBlock,
VideoBubbleBlock,
} from '@typebot.io/schemas'
import { BubbleBlockType } from '@typebot.io/schemas/features/blocks/bubbles/enums'
import { BubbleBlockType } from '@typebot.io/schemas/features/blocks/bubbles/constants'
import { Match, Switch } from 'solid-js'
type Props = {
message: ChatMessage
typingEmulation: TypingEmulation
typingEmulation: Settings['typingEmulation']
onTransitionEnd: (offsetTop?: number) => void
}
@@ -30,32 +30,32 @@ export const HostBubble = (props: Props) => {
<Switch>
<Match when={props.message.type === BubbleBlockType.TEXT}>
<TextBubble
content={props.message.content as TextBubbleContent}
content={props.message.content as TextBubbleBlock['content']}
typingEmulation={props.typingEmulation}
onTransitionEnd={onTransitionEnd}
/>
</Match>
<Match when={props.message.type === BubbleBlockType.IMAGE}>
<ImageBubble
content={props.message.content as ImageBubbleContent}
content={props.message.content as ImageBubbleBlock['content']}
onTransitionEnd={onTransitionEnd}
/>
</Match>
<Match when={props.message.type === BubbleBlockType.VIDEO}>
<VideoBubble
content={props.message.content as VideoBubbleContent}
content={props.message.content as VideoBubbleBlock['content']}
onTransitionEnd={onTransitionEnd}
/>
</Match>
<Match when={props.message.type === BubbleBlockType.EMBED}>
<EmbedBubble
content={props.message.content as EmbedBubbleContent}
content={props.message.content as EmbedBubbleBlock['content']}
onTransitionEnd={onTransitionEnd}
/>
</Match>
<Match when={props.message.type === BubbleBlockType.AUDIO}>
<AudioBubble
content={props.message.content as AudioBubbleContent}
content={props.message.content as AudioBubbleBlock['content']}
onTransitionEnd={onTransitionEnd}
/>
</Match>