import { startChatQuery } from '@/queries/startChatQuery' import { BotContext, OutgoingLog } from '@/types' import { setBotContainerHeight } from '@/utils/botContainerHeightSignal' import { setBotContainer } from '@/utils/botContainerSignal' import { CorsError } from '@/utils/CorsError' import { injectFont } from '@/utils/injectFont' import { setIsMobile } from '@/utils/isMobileSignal' import { persist } from '@/utils/persist' import { setCssVariablesValue } from '@/utils/setCssVariablesValue' import { getExistingResultIdFromStorage, getInitialChatReplyFromStorage, setInitialChatReplyInStorage, setResultInStorage, wipeExistingChatStateInStorage, } from '@/utils/storage' import { toaster } from '@/utils/toaster' import { Toast, Toaster } from '@ark-ui/solid' import { isDefined, isNotDefined, isNotEmpty } from '@indite.io/lib' import { Font, InputBlock, StartChatResponse, StartFrom, } from '@indite.io/schemas' import { defaultSettings } from '@indite.io/schemas/features/bot/settings/constants' import { defaultFontFamily, defaultFontType, defaultProgressBarPosition, } from '@indite.io/schemas/features/bot/theme/constants' import { clsx } from 'clsx' import { HTTPError } from 'ky' import { createEffect, createSignal, onCleanup, onMount, Show } from 'solid-js' import { Portal } from 'solid-js/web' import immutableCss from '../assets/immutable.css' import { ConversationContainer } from './ConversationContainer' import { ErrorMessage } from './ErrorMessage' import { CloseIcon } from './icons/CloseIcon' import { LiteBadge } from './LiteBadge' import { ProgressBar } from './ProgressBar' export type BotProps = { source?: string botName?: string | undefined // eslint-disable-next-line @typescript-eslint/no-explicit-any bot: string | any isPreview?: boolean resultId?: string prefilledVariables?: Record apiHost?: string font?: Font progressBarRef?: HTMLDivElement startFrom?: StartFrom sessionId?: string onNewInputBlock?: (inputBlock: InputBlock) => void onAnswer?: (answer: { message: string; blockId: string }) => void onInit?: () => void onEnd?: () => void onNewLogs?: (logs: OutgoingLog[]) => void onChatStatePersisted?: (isEnabled: boolean) => void } // utils/sharedValue.ts type SharedValue = { value: T; getValue: () => T; updateValue: (newValue: T) => void; }; type RichTextChild = { text: string; }; type RichTextElement = { type: 'p'; // Assuming only paragraphs are used for now children: RichTextChild[]; }; type MessageContent = { type: 'richText'; richText: RichTextElement[]; }; type Message = { id: string; type: 'text'; text: string; // Add the required 'text' property attachedFileUrls?: string[]; // Optional property to match the expected structure content: MessageContent; }; type Bot = { id: string; theme: Record; // Adjust as necessary based on your theme structure settings: Record; // Adjust as necessary based on your settings structure }; export type ConversationData = { sessionId: string; bot: Bot; messages: Message[]; input: InputBlock; }; export function createSharedValue(initialValue: T): SharedValue { let currentValue = initialValue; return { get value() { return currentValue; }, getValue: () => currentValue, updateValue: (newValue: T) => { currentValue = newValue; }, }; } // Initialize a shared instance export const sharedStringValue = createSharedValue(null); export const Bot = (props: BotProps & { class?: string, isTemplateModalView?: boolean, source?: string }) => { const [initialChatReply, setInitialChatReply] = createSignal< StartChatResponse | 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 botIdFromProps = typeof props.bot === 'string' ? props.bot : undefined const isPreview = typeof props.bot !== 'string' || (props.isPreview ?? false) const resultIdInStorage = getExistingResultIdFromStorage(botIdFromProps) console.log('props', props); const { data, error } = await startChatQuery({ stripeRedirectStatus: urlParams.get('redirect_status') ?? undefined, bot: props.bot, apiHost: props.apiHost, source: props.source, isPreview, resultId: isNotEmpty(props.resultId) ? props.resultId : resultIdInStorage, prefilledVariables: { ...prefilledVariables, ...props.prefilledVariables, }, startFrom: props.startFrom, sessionId: props.sessionId, }) if (data && typeof data === 'object' && 'messages' in data && 'bot' in data && 'sessionId' in data && 'input' in data) { sharedStringValue.updateValue(data as ConversationData); } else { console.error('Invalid data format:', data); } 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 (error instanceof CorsError) { return setError(new Error(error.message)) } 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 && botIdFromProps && (data.bot.settings.general?.rememberUser?.isEnabled ?? defaultSettings.general.rememberUser.isEnabled) ) { if (resultIdInStorage && resultIdInStorage !== data.resultId) wipeExistingChatStateInStorage(data.bot.id) const storage = data.bot.settings.general?.rememberUser?.storage ?? defaultSettings.general.rememberUser.storage setResultInStorage(storage)(botIdFromProps, data.resultId) const initialChatInStorage = getInitialChatReplyFromStorage( data.bot.id ) if ( initialChatInStorage && initialChatInStorage.bot.publishedAt && data.bot.publishedAt ) { if ( new Date(initialChatInStorage.bot.publishedAt).getTime() === new Date(data.bot.publishedAt).getTime() ) { setInitialChatReply(initialChatInStorage) } else { // Restart chat by resetting remembered state wipeExistingChatStateInStorage(data.bot.id) setInitialChatReply(data) setInitialChatReplyInStorage(data, { botId: data.bot.id, storage, }) } } else { setInitialChatReply(data) setInitialChatReplyInStorage(data, { botId: data.bot.id, storage, }) } props.onChatStatePersisted?.(true) } else { wipeExistingChatStateInStorage(data.bot.id) setInitialChatReply(data) if (data.input?.id && props.onNewInputBlock) props.onNewInputBlock(data.input) if (data.logs) props.onNewLogs?.(data.logs) props.onChatStatePersisted?.(false) } setCustomCss(data.bot.theme.customCss ?? '') } createEffect(() => { if (isNotDefined(props.bot) || isInitialized()) return initializeBot().then() }) createEffect(() => { if (isNotDefined(props.bot) || typeof props.bot === 'string') return setCustomCss(props.bot.theme.customCss ?? '') if ( props.bot.theme.general?.progressBar?.isEnabled && initialChatReply() && !initialChatReply()?.bot.theme.general?.progressBar?.isEnabled ) { setIsInitialized(false) initializeBot().then() } }) onCleanup(() => { setIsInitialized(false) }) return ( <> {(error) => } {(initialChatReply) => ( )} ) } type BotContentProps = { botName?: string | undefined initialChatReply: StartChatResponse 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] = persist( createSignal(props.initialChatReply.progress), { storage: props.context.storage, key: `bot-${props.context.bot.id}-progressValue`, } ) const [botContainerWidth, setBotContainerWidth] = createSignal('100%') let botContainerElement: HTMLDivElement | undefined const resizeObserver = new ResizeObserver((entries) => { setBotContainerWidth(`${entries[0].contentRect.width - 60}px`) return setIsMobile(entries[0].target.clientWidth < 400) }) onMount(() => { if (!botContainerElement) return setBotContainer(botContainerElement) resizeObserver.observe(botContainerElement) setBotContainerWidth(`${botContainerElement.clientWidth - 60}px`) setBotContainerHeight(`${botContainerElement.clientHeight}px`) }) createEffect(() => { injectFont( props.initialChatReply.bot.theme.general?.font ?? { type: defaultFontType, family: defaultFontFamily, } ) if (!botContainerElement) return setCssVariablesValue( props.initialChatReply.bot.theme, botContainerElement, props.context.isPreview ) }) onCleanup(() => { if (!botContainerElement) return resizeObserver.unobserve(botContainerElement) }) return (

{props.botName ?? 'Bot'}

} >
{(toast) => ( {toast().title} {toast().description} )}
) }