import { LiteBadge } from './LiteBadge' import { createEffect, createSignal, onCleanup, onMount, Show } from 'solid-js' import { isDefined, isNotDefined, isNotEmpty } from '@typebot.io/lib' import { startChatQuery } from '@/queries/startChatQuery' import { ConversationContainer } from './ConversationContainer' import { setIsMobile } from '@/utils/isMobileSignal' import { BotContext, InitialChatReply, OutgoingLog } from '@/types' import { ErrorMessage } from './ErrorMessage' import { getExistingResultIdFromStorage, setResultInStorage, } from '@/utils/storage' import { setCssVariablesValue } from '@/utils/setCssVariablesValue' import immutableCss from '../assets/immutable.css' import { Font, InputBlock } from '@typebot.io/schemas' import { StartFrom } from '@typebot.io/schemas' import { defaultTheme } from '@typebot.io/schemas/features/typebot/theme/constants' import { clsx } from 'clsx' import { HTTPError } from 'ky' import { injectFont } from '@/utils/injectFont' import { ProgressBar } from './ProgressBar' import { Portal } from 'solid-js/web' export type BotProps = { // eslint-disable-next-line @typescript-eslint/no-explicit-any typebot: string | any isPreview?: boolean resultId?: string prefilledVariables?: Record apiHost?: string font?: Font progressBarRef?: HTMLDivElement onNewInputBlock?: (inputBlock: InputBlock) => void onAnswer?: (answer: { message: string; blockId: string }) => void onInit?: () => void onEnd?: () => void onNewLogs?: (logs: OutgoingLog[]) => void startFrom?: StartFrom } export const Bot = (props: BotProps & { class?: string }) => { const [initialChatReply, setInitialChatReply] = createSignal< InitialChatReply | undefined >() const [customCss, setCustomCss] = createSignal('') const [isInitialized, setIsInitialized] = createSignal(false) const [error, setError] = createSignal() const initializeBot = async () => { if (props.font) injectFont(props.font) setIsInitialized(true) const urlParams = new URLSearchParams(location.search) props.onInit?.() const prefilledVariables: { [key: string]: string } = {} urlParams.forEach((value, key) => { prefilledVariables[key] = value }) const typebotIdFromProps = typeof props.typebot === 'string' ? props.typebot : undefined const isPreview = typeof props.typebot !== 'string' || (props.isPreview ?? false) const { data, error } = await startChatQuery({ stripeRedirectStatus: urlParams.get('redirect_status') ?? undefined, typebot: props.typebot, apiHost: props.apiHost, isPreview, resultId: isNotEmpty(props.resultId) ? props.resultId : getExistingResultIdFromStorage(typebotIdFromProps), prefilledVariables: { ...prefilledVariables, ...props.prefilledVariables, }, startFrom: props.startFrom, }) if (error instanceof HTTPError) { if (isPreview) { return setError( new Error(`An error occurred while loading the bot.`, { cause: { status: error.response.status, body: await error.response.json(), }, }) ) } if (error.response.status === 400 || error.response.status === 403) return setError(new Error('This bot is now closed.')) if (error.response.status === 404) return setError(new Error("The bot you're looking for doesn't exist.")) return setError( new Error( `Error! Couldn't initiate the chat. (${error.response.statusText})` ) ) } if (!data) { if (error) { console.error(error) if (isPreview) { return setError( new Error(`Error! Could not reach server. Check your connection.`, { cause: error, }) ) } } return setError( new Error('Error! Could not reach server. Check your connection.') ) } if (data.resultId && typebotIdFromProps) setResultInStorage(data.typebot.settings.general?.rememberUser?.storage)( typebotIdFromProps, data.resultId ) setInitialChatReply(data) setCustomCss(data.typebot.theme.customCss ?? '') if (data.input?.id && props.onNewInputBlock) props.onNewInputBlock(data.input) if (data.logs) props.onNewLogs?.(data.logs) } createEffect(() => { if (isNotDefined(props.typebot) || isInitialized()) return initializeBot().then() }) createEffect(() => { if (isNotDefined(props.typebot) || typeof props.typebot === 'string') return setCustomCss(props.typebot.theme.customCss ?? '') if ( props.typebot.theme.general?.progressBar?.isEnabled && initialChatReply() && !initialChatReply()?.typebot.theme.general?.progressBar?.isEnabled ) { setIsInitialized(false) initializeBot().then() } }) onCleanup(() => { setIsInitialized(false) }) return ( <> {(error) => } {(initialChatReply) => ( )} ) } type BotContentProps = { initialChatReply: InitialChatReply context: BotContext class?: string progressBarRef?: HTMLDivElement onNewInputBlock?: (inputBlock: InputBlock) => void onAnswer?: (answer: { message: string; blockId: string }) => void onEnd?: () => void onNewLogs?: (logs: OutgoingLog[]) => void } const BotContent = (props: BotContentProps) => { const [progressValue, setProgressValue] = createSignal( props.initialChatReply.progress ) let botContainer: HTMLDivElement | undefined const resizeObserver = new ResizeObserver((entries) => { return setIsMobile(entries[0].target.clientWidth < 400) }) onMount(() => { if (!botContainer) return resizeObserver.observe(botContainer) }) createEffect(() => { injectFont( props.initialChatReply.typebot.theme.general?.font ?? defaultTheme.general.font ) if (!botContainer) return setCssVariablesValue( props.initialChatReply.typebot.theme, botContainer, props.context.isPreview ) }) onCleanup(() => { if (!botContainer) return resizeObserver.unobserve(botContainer) }) return (
} >
) }