⚡ (chat) Improve chat API compatibility with preview mode
This commit is contained in:
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@typebot.io/js",
|
||||
"version": "0.0.0",
|
||||
"description": "",
|
||||
"version": "0.0.1",
|
||||
"description": "Javascript library to display typebots on your website",
|
||||
"main": "dist/index.mjs",
|
||||
"types": "dist/index.d.ts",
|
||||
"scripts": {
|
||||
|
@ -2,8 +2,7 @@
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
:host,
|
||||
:root {
|
||||
:host {
|
||||
--typebot-container-bg-image: none;
|
||||
--typebot-container-bg-color: transparent;
|
||||
--typebot-container-font-family: 'Open Sans';
|
||||
|
@ -1,83 +1,128 @@
|
||||
import { LiteBadge } from './LiteBadge'
|
||||
import { createSignal, onCleanup, onMount, Show } from 'solid-js'
|
||||
import {
|
||||
getViewerUrl,
|
||||
injectCustomHeadCode,
|
||||
isDefined,
|
||||
isNotEmpty,
|
||||
} from 'utils'
|
||||
import { createEffect, createSignal, onCleanup, onMount, Show } from 'solid-js'
|
||||
import { getViewerUrl, injectCustomHeadCode, isEmpty, isNotEmpty } from 'utils'
|
||||
import { getInitialChatReplyQuery } from '@/queries/getInitialChatReplyQuery'
|
||||
import { ConversationContainer } from './ConversationContainer'
|
||||
import css from '../assets/index.css'
|
||||
import { InitialChatReply, StartParams } from 'models'
|
||||
import { StartParams } from 'models'
|
||||
import { setIsMobile } from '@/utils/isMobileSignal'
|
||||
import { BotContext } from '@/types'
|
||||
import { BotContext, InitialChatReply } from '@/types'
|
||||
import { ErrorMessage } from './ErrorMessage'
|
||||
import {
|
||||
getExistingResultIdFromSession,
|
||||
setResultInSession,
|
||||
} from '@/utils/sessionStorage'
|
||||
import { setCssVariablesValue } from '@/utils/setCssVariablesValue'
|
||||
|
||||
export type BotProps = StartParams & {
|
||||
initialChatReply?: InitialChatReply
|
||||
apiHost?: string
|
||||
onNewInputBlock?: (ids: { id: string; groupId: string }) => void
|
||||
onAnswer?: (answer: { message: string; blockId: string }) => void
|
||||
onInit?: () => void
|
||||
onEnd?: () => void
|
||||
}
|
||||
|
||||
export const Bot = (props: BotProps) => {
|
||||
const [initialChatReply, setInitialChatReply] = createSignal<
|
||||
InitialChatReply | undefined
|
||||
>(props.initialChatReply)
|
||||
>()
|
||||
const [error, setError] = createSignal<Error | undefined>(
|
||||
// eslint-disable-next-line solid/reactivity
|
||||
isEmpty(isEmpty(props.apiHost) ? getViewerUrl() : props.apiHost)
|
||||
? new Error('process.env.NEXT_PUBLIC_VIEWER_URL is missing in env')
|
||||
: undefined
|
||||
)
|
||||
|
||||
const initializeBot = async () => {
|
||||
const urlParams = new URLSearchParams(location.search)
|
||||
props.onInit?.()
|
||||
const prefilledVariables: { [key: string]: string } = {}
|
||||
urlParams.forEach((value, key) => {
|
||||
prefilledVariables[key] = value
|
||||
})
|
||||
const { data, error } = await getInitialChatReplyQuery({
|
||||
typebot: props.typebot,
|
||||
apiHost: props.apiHost,
|
||||
isPreview: props.isPreview ?? false,
|
||||
resultId: isNotEmpty(props.resultId)
|
||||
? props.resultId
|
||||
: getExistingResultIdFromSession(),
|
||||
startGroupId: props.startGroupId,
|
||||
prefilledVariables: {
|
||||
...prefilledVariables,
|
||||
...props.prefilledVariables,
|
||||
},
|
||||
})
|
||||
if (error && 'code' in error && typeof error.code === 'string') {
|
||||
if (['BAD_REQUEST', 'FORBIDDEN'].includes(error.code))
|
||||
setError(new Error('This bot is now closed.'))
|
||||
if (error.code === 'NOT_FOUND') setError(new Error('Typebot not found.'))
|
||||
return
|
||||
}
|
||||
|
||||
if (!data) return setError(new Error("Couldn't initiate the chat"))
|
||||
|
||||
if (data.resultId) setResultInSession(data.resultId)
|
||||
setInitialChatReply(data)
|
||||
|
||||
if (data.input?.id && props.onNewInputBlock)
|
||||
props.onNewInputBlock({
|
||||
id: data.input.id,
|
||||
groupId: data.input.groupId,
|
||||
})
|
||||
const customHeadCode = data.typebot.settings.metadata.customHeadCode
|
||||
if (customHeadCode) injectCustomHeadCode(customHeadCode)
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (!props.typebotId) return
|
||||
const initialChatReplyValue = initialChatReply()
|
||||
if (isDefined(initialChatReplyValue)) {
|
||||
const customHeadCode =
|
||||
initialChatReplyValue.typebot.settings.metadata.customHeadCode
|
||||
if (customHeadCode) injectCustomHeadCode(customHeadCode)
|
||||
} else {
|
||||
const urlParams = new URLSearchParams(location.search)
|
||||
const prefilledVariables: { [key: string]: string } = {}
|
||||
urlParams.forEach((value, key) => {
|
||||
prefilledVariables[key] = value
|
||||
})
|
||||
getInitialChatReplyQuery({
|
||||
typebotId: props.typebotId,
|
||||
apiHost: props.apiHost,
|
||||
isPreview: props.isPreview ?? false,
|
||||
resultId: props.resultId,
|
||||
prefilledVariables: {
|
||||
...prefilledVariables,
|
||||
...props.prefilledVariables,
|
||||
},
|
||||
}).then((initialChatReply) => {
|
||||
setInitialChatReply(initialChatReply)
|
||||
})
|
||||
}
|
||||
initializeBot().then()
|
||||
})
|
||||
|
||||
return (
|
||||
<Show
|
||||
when={isNotEmpty(props.apiHost ?? getViewerUrl())}
|
||||
fallback={() => (
|
||||
<p>process.env.NEXT_PUBLIC_VIEWER_URL is missing in env</p>
|
||||
)}
|
||||
>
|
||||
<>
|
||||
<style>{css}</style>
|
||||
<Show when={error()} keyed>
|
||||
{(error) => <ErrorMessage error={error} />}
|
||||
</Show>
|
||||
<Show when={initialChatReply()} keyed>
|
||||
{(initialChatReply) => (
|
||||
<BotContent
|
||||
initialChatReply={initialChatReply}
|
||||
initialChatReply={{
|
||||
...initialChatReply,
|
||||
typebot: {
|
||||
...initialChatReply.typebot,
|
||||
settings:
|
||||
typeof props.typebot === 'string'
|
||||
? initialChatReply.typebot?.settings
|
||||
: props.typebot?.settings,
|
||||
theme:
|
||||
typeof props.typebot === 'string'
|
||||
? initialChatReply.typebot?.theme
|
||||
: props.typebot?.theme,
|
||||
},
|
||||
}}
|
||||
context={{
|
||||
apiHost: props.apiHost,
|
||||
isPreview: props.isPreview ?? false,
|
||||
typebotId: props.typebotId as string,
|
||||
typebotId: initialChatReply.typebot.id,
|
||||
resultId: initialChatReply.resultId,
|
||||
}}
|
||||
onNewInputBlock={props.onNewInputBlock}
|
||||
onAnswer={props.onAnswer}
|
||||
onEnd={props.onEnd}
|
||||
/>
|
||||
)}
|
||||
</Show>
|
||||
</Show>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
type BotContentProps = {
|
||||
initialChatReply: InitialChatReply
|
||||
context: BotContext
|
||||
onNewInputBlock?: (ids: { id: string; groupId: string }) => void
|
||||
onAnswer?: (answer: { message: string; blockId: string }) => void
|
||||
onEnd?: () => void
|
||||
}
|
||||
|
||||
const BotContent = (props: BotContentProps) => {
|
||||
@ -98,38 +143,39 @@ const BotContent = (props: BotContentProps) => {
|
||||
|
||||
onMount(() => {
|
||||
injectCustomFont()
|
||||
if (botContainer) {
|
||||
resizeObserver.observe(botContainer)
|
||||
}
|
||||
if (!botContainer) return
|
||||
resizeObserver.observe(botContainer)
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
if (!botContainer) return
|
||||
setCssVariablesValue(props.initialChatReply.typebot.theme, botContainer)
|
||||
})
|
||||
|
||||
onCleanup(() => {
|
||||
if (botContainer) {
|
||||
resizeObserver.unobserve(botContainer)
|
||||
}
|
||||
if (!botContainer) return
|
||||
resizeObserver.unobserve(botContainer)
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<style>{css}</style>
|
||||
<div
|
||||
ref={botContainer}
|
||||
class="flex w-full h-full text-base overflow-hidden bg-cover flex-col items-center typebot-container"
|
||||
>
|
||||
<div class="flex w-full h-full justify-center">
|
||||
<ConversationContainer
|
||||
context={props.context}
|
||||
initialChatReply={props.initialChatReply}
|
||||
/>
|
||||
</div>
|
||||
<Show
|
||||
when={
|
||||
props.initialChatReply.typebot.settings.general.isBrandingEnabled
|
||||
}
|
||||
>
|
||||
<LiteBadge botContainer={botContainer} />
|
||||
</Show>
|
||||
<div
|
||||
ref={botContainer}
|
||||
class="relative flex w-full h-full text-base overflow-hidden bg-cover flex-col items-center typebot-container"
|
||||
>
|
||||
<div class="flex w-full h-full justify-center">
|
||||
<ConversationContainer
|
||||
context={props.context}
|
||||
initialChatReply={props.initialChatReply}
|
||||
onNewInputBlock={props.onNewInputBlock}
|
||||
onAnswer={props.onAnswer}
|
||||
onEnd={props.onEnd}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
<Show
|
||||
when={props.initialChatReply.typebot.settings.general.isBrandingEnabled}
|
||||
>
|
||||
<LiteBadge botContainer={botContainer} />
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -10,7 +10,9 @@ type Props = Pick<ChatReply, 'messages' | 'input'> & {
|
||||
settings: Settings
|
||||
inputIndex: number
|
||||
context: BotContext
|
||||
onScrollToBottom: () => void
|
||||
onSubmit: (input: string) => void
|
||||
onEnd?: () => void
|
||||
onSkip: () => void
|
||||
}
|
||||
|
||||
@ -23,6 +25,9 @@ export const ChatChunk = (props: Props) => {
|
||||
? displayedMessageIndex()
|
||||
: displayedMessageIndex() + 1
|
||||
)
|
||||
props.onScrollToBottom()
|
||||
if (!props.input && displayedMessageIndex() === props.messages.length)
|
||||
return props.onEnd?.()
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -1,31 +1,73 @@
|
||||
import { ChatReply, InitialChatReply } from 'models'
|
||||
import { ChatReply, Theme } from 'models'
|
||||
import { createSignal, For } from 'solid-js'
|
||||
import { sendMessageQuery } from '@/queries/sendMessageQuery'
|
||||
import { ChatChunk } from './ChatChunk'
|
||||
import { BotContext } from '@/types'
|
||||
import { BotContext, InitialChatReply } from '@/types'
|
||||
import { executeIntegrations } from '@/utils/executeIntegrations'
|
||||
import { executeLogic } from '@/utils/executeLogic'
|
||||
|
||||
const parseDynamicTheme = (
|
||||
theme: Theme,
|
||||
dynamicTheme: ChatReply['dynamicTheme']
|
||||
): Theme => ({
|
||||
...theme,
|
||||
chat: {
|
||||
...theme.chat,
|
||||
hostAvatar: theme.chat.hostAvatar
|
||||
? {
|
||||
...theme.chat.hostAvatar,
|
||||
url: dynamicTheme?.hostAvatarUrl,
|
||||
}
|
||||
: undefined,
|
||||
guestAvatar: theme.chat.guestAvatar
|
||||
? {
|
||||
...theme.chat.guestAvatar,
|
||||
url: dynamicTheme?.guestAvatarUrl,
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
})
|
||||
|
||||
type Props = {
|
||||
initialChatReply: InitialChatReply
|
||||
context: BotContext
|
||||
onNewInputBlock?: (ids: { id: string; groupId: string }) => void
|
||||
onAnswer?: (answer: { message: string; blockId: string }) => void
|
||||
onEnd?: () => void
|
||||
}
|
||||
|
||||
export const ConversationContainer = (props: Props) => {
|
||||
let bottomSpacer: HTMLDivElement | undefined
|
||||
const [chatChunks, setChatChunks] = createSignal<ChatReply[]>([
|
||||
{
|
||||
input: props.initialChatReply.input,
|
||||
messages: props.initialChatReply.messages,
|
||||
},
|
||||
])
|
||||
const [theme, setTheme] = createSignal(
|
||||
parseDynamicTheme(
|
||||
props.initialChatReply.typebot.theme,
|
||||
props.initialChatReply.dynamicTheme
|
||||
)
|
||||
)
|
||||
|
||||
const sendMessage = async (message: string) => {
|
||||
const currentBlockId = chatChunks().at(-1)?.input?.id
|
||||
if (currentBlockId && props.onAnswer)
|
||||
props.onAnswer({ message, blockId: currentBlockId })
|
||||
const data = await sendMessageQuery({
|
||||
apiHost: props.context.apiHost,
|
||||
sessionId: props.initialChatReply.sessionId,
|
||||
message,
|
||||
})
|
||||
if (!data) return
|
||||
if (data.dynamicTheme) applyDynamicTheme(data.dynamicTheme)
|
||||
if (data.input?.id && props.onNewInputBlock) {
|
||||
props.onNewInputBlock({
|
||||
id: data.input.id,
|
||||
groupId: data.input.groupId,
|
||||
})
|
||||
}
|
||||
if (data.integrations) {
|
||||
executeIntegrations(data.integrations)
|
||||
}
|
||||
@ -41,6 +83,17 @@ export const ConversationContainer = (props: Props) => {
|
||||
])
|
||||
}
|
||||
|
||||
const applyDynamicTheme = (dynamicTheme: ChatReply['dynamicTheme']) => {
|
||||
setTheme((theme) => parseDynamicTheme(theme, dynamicTheme))
|
||||
}
|
||||
|
||||
const autoScrollToBottom = () => {
|
||||
if (!bottomSpacer) return
|
||||
setTimeout(() => {
|
||||
bottomSpacer?.scrollIntoView({ behavior: 'smooth' })
|
||||
}, 200)
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="overflow-y-scroll w-full min-h-full rounded px-3 pt-10 relative scrollable-container typebot-chat-view">
|
||||
<For each={chatChunks()}>
|
||||
@ -49,16 +102,26 @@ export const ConversationContainer = (props: Props) => {
|
||||
inputIndex={index()}
|
||||
messages={chatChunk.messages}
|
||||
input={chatChunk.input}
|
||||
theme={props.initialChatReply.typebot.theme}
|
||||
theme={theme()}
|
||||
settings={props.initialChatReply.typebot.settings}
|
||||
onSubmit={sendMessage}
|
||||
onScrollToBottom={autoScrollToBottom}
|
||||
onSkip={() => {
|
||||
// TODO: implement skip
|
||||
}}
|
||||
onEnd={props.onEnd}
|
||||
context={props.context}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
<BottomSpacer ref={bottomSpacer} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
type BottomSpacerProps = {
|
||||
ref: HTMLDivElement | undefined
|
||||
}
|
||||
const BottomSpacer = (props: BottomSpacerProps) => {
|
||||
return <div ref={props.ref} class="w-full h-32" />
|
||||
}
|
||||
|
10
packages/js/src/components/ErrorMessage.tsx
Normal file
10
packages/js/src/components/ErrorMessage.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
type Props = {
|
||||
error: Error
|
||||
}
|
||||
export const ErrorMessage = (props: Props) => {
|
||||
return (
|
||||
<div class="h-full flex justify-center items-center flex-col">
|
||||
<p class="text-5xl">{props.error.message}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -44,7 +44,7 @@ export const InputChatBlock = (props: Props) => {
|
||||
|
||||
const handleSubmit = async ({ label, value }: InputSubmitContent) => {
|
||||
setAnswer(label ?? value)
|
||||
props.onSubmit(value)
|
||||
props.onSubmit(value ?? label)
|
||||
}
|
||||
|
||||
return (
|
||||
@ -96,37 +96,40 @@ const Input = (props: {
|
||||
<Switch>
|
||||
<Match when={props.block.type === InputBlockType.TEXT}>
|
||||
<TextInput
|
||||
block={props.block as TextInputBlock & { prefilledValue?: string }}
|
||||
block={props.block as TextInputBlock}
|
||||
defaultValue={props.block.prefilledValue}
|
||||
onSubmit={onSubmit}
|
||||
hasGuestAvatar={props.hasGuestAvatar}
|
||||
/>
|
||||
</Match>
|
||||
<Match when={props.block.type === InputBlockType.NUMBER}>
|
||||
<NumberInput
|
||||
block={props.block as NumberInputBlock & { prefilledValue?: string }}
|
||||
block={props.block as NumberInputBlock}
|
||||
defaultValue={props.block.prefilledValue}
|
||||
onSubmit={onSubmit}
|
||||
hasGuestAvatar={props.hasGuestAvatar}
|
||||
/>
|
||||
</Match>
|
||||
<Match when={props.block.type === InputBlockType.EMAIL}>
|
||||
<EmailInput
|
||||
block={props.block as EmailInputBlock & { prefilledValue?: string }}
|
||||
block={props.block as EmailInputBlock}
|
||||
defaultValue={props.block.prefilledValue}
|
||||
onSubmit={onSubmit}
|
||||
hasGuestAvatar={props.hasGuestAvatar}
|
||||
/>
|
||||
</Match>
|
||||
<Match when={props.block.type === InputBlockType.URL}>
|
||||
<UrlInput
|
||||
block={props.block as UrlInputBlock & { prefilledValue?: string }}
|
||||
block={props.block as UrlInputBlock}
|
||||
defaultValue={props.block.prefilledValue}
|
||||
onSubmit={onSubmit}
|
||||
hasGuestAvatar={props.hasGuestAvatar}
|
||||
/>
|
||||
</Match>
|
||||
<Match when={props.block.type === InputBlockType.PHONE}>
|
||||
<PhoneInput
|
||||
block={
|
||||
props.block as PhoneNumberInputBlock & { prefilledValue?: string }
|
||||
}
|
||||
block={props.block as PhoneNumberInputBlock}
|
||||
defaultValue={props.block.prefilledValue}
|
||||
onSubmit={onSubmit}
|
||||
hasGuestAvatar={props.hasGuestAvatar}
|
||||
/>
|
||||
@ -146,7 +149,8 @@ const Input = (props: {
|
||||
</Match>
|
||||
<Match when={props.block.type === InputBlockType.RATING}>
|
||||
<RatingForm
|
||||
block={props.block as RatingInputBlock & { prefilledValue?: string }}
|
||||
block={props.block as RatingInputBlock}
|
||||
defaultValue={props.block.prefilledValue}
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
</Match>
|
||||
|
@ -38,7 +38,7 @@ export const LiteBadge = (props: Props) => {
|
||||
href={'https://www.typebot.io/?utm_source=litebadge'}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="fixed py-1 px-2 bg-white z-50 rounded shadow-md lite-badge"
|
||||
class="absolute py-1 px-2 bg-white z-50 rounded shadow-md lite-badge text-gray-900"
|
||||
style={{ bottom: '20px' }}
|
||||
id="lite-badge"
|
||||
>
|
||||
|
@ -1,24 +1,25 @@
|
||||
import { isMobile } from '@/utils/isMobileSignal'
|
||||
import { Show } from 'solid-js'
|
||||
import { isNotEmpty } from 'utils'
|
||||
import { DefaultAvatar } from './DefaultAvatar'
|
||||
|
||||
export const Avatar = (props: { avatarSrc?: string }) => (
|
||||
<Show when={props.avatarSrc !== ''}>
|
||||
<Show when={props.avatarSrc} keyed fallback={() => <DefaultAvatar />}>
|
||||
{(currentAvatarSrc) => (
|
||||
<figure
|
||||
class={
|
||||
'flex justify-center items-center rounded-full text-white relative animate-fade-in ' +
|
||||
(isMobile() ? 'w-6 h-6 text-sm' : 'w-10 h-10 text-xl')
|
||||
}
|
||||
>
|
||||
<img
|
||||
src={currentAvatarSrc}
|
||||
alt="Bot avatar"
|
||||
class="rounded-full object-cover w-full h-full"
|
||||
/>
|
||||
</figure>
|
||||
)}
|
||||
</Show>
|
||||
<Show
|
||||
when={isNotEmpty(props.avatarSrc)}
|
||||
keyed
|
||||
fallback={() => <DefaultAvatar />}
|
||||
>
|
||||
<figure
|
||||
class={
|
||||
'flex justify-center items-center rounded-full text-white relative animate-fade-in ' +
|
||||
(isMobile() ? 'w-6 h-6 text-sm' : 'w-10 h-10 text-xl')
|
||||
}
|
||||
>
|
||||
<img
|
||||
src={props.avatarSrc}
|
||||
alt="Bot avatar"
|
||||
class="rounded-full object-cover w-full h-full"
|
||||
/>
|
||||
</figure>
|
||||
</Show>
|
||||
)
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { isMobile } from '@/utils/isMobileSignal'
|
||||
import { splitProps } from 'solid-js'
|
||||
import { JSX } from 'solid-js/jsx-runtime'
|
||||
|
||||
@ -9,13 +8,13 @@ type ShortTextInputProps = {
|
||||
|
||||
export const ShortTextInput = (props: ShortTextInputProps) => {
|
||||
const [local, others] = splitProps(props, ['ref', 'onInput'])
|
||||
|
||||
return (
|
||||
<input
|
||||
ref={local.ref}
|
||||
ref={props.ref}
|
||||
class="focus:outline-none bg-transparent px-4 py-4 flex-1 w-full text-input"
|
||||
type="text"
|
||||
style={{ 'font-size': '16px' }}
|
||||
autofocus={!isMobile()}
|
||||
onInput={(e) => local.onInput(e.currentTarget.value)}
|
||||
{...others}
|
||||
/>
|
||||
|
@ -3,9 +3,6 @@ import type { Component } from 'solid-js'
|
||||
|
||||
export const App: Component = () => {
|
||||
return (
|
||||
<Bot
|
||||
typebotId="clbm11cku000t3b6o01ug8awh"
|
||||
apiHost="http://localhost:3001"
|
||||
/>
|
||||
<Bot typebot="clbm11cku000t3b6o01ug8awh" apiHost="http://localhost:3001" />
|
||||
)
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ type Props = {
|
||||
export const DateForm = (props: Props) => {
|
||||
const [inputValues, setInputValues] = createSignal({ from: '', to: '' })
|
||||
return (
|
||||
<div class="flex flex-col w-full lg:w-4/6">
|
||||
<div class="flex flex-col">
|
||||
<div class="flex items-center">
|
||||
<form
|
||||
class={'flex justify-between rounded-lg typebot-input pr-2 items-end'}
|
||||
|
@ -20,7 +20,6 @@ export const parseReadableDate = ({
|
||||
const fromReadable = new Date(
|
||||
hasTime ? from : from.replace(/-/g, '/')
|
||||
).toLocaleString(currentLocale, formatOptions)
|
||||
console.log(to, to.replace(/-/g, '/'))
|
||||
const toReadable = new Date(
|
||||
hasTime ? to : to.replace(/-/g, '/')
|
||||
).toLocaleString(currentLocale, formatOptions)
|
||||
|
@ -1,20 +1,19 @@
|
||||
import { ShortTextInput } from '@/components/inputs'
|
||||
import { SendButton } from '@/components/SendButton'
|
||||
import { InputSubmitContent } from '@/types'
|
||||
import { isMobile } from '@/utils/isMobileSignal'
|
||||
import { EmailInputBlock } from 'models'
|
||||
import { createSignal } from 'solid-js'
|
||||
import { createSignal, onMount } from 'solid-js'
|
||||
|
||||
type Props = {
|
||||
block: EmailInputBlock & { prefilledValue?: string }
|
||||
onSubmit: (value: InputSubmitContent) => void
|
||||
block: EmailInputBlock
|
||||
defaultValue?: string
|
||||
hasGuestAvatar: boolean
|
||||
onSubmit: (value: InputSubmitContent) => void
|
||||
}
|
||||
|
||||
export const EmailInput = (props: Props) => {
|
||||
const [inputValue, setInputValue] = createSignal(
|
||||
// eslint-disable-next-line solid/reactivity
|
||||
props.block.prefilledValue ?? ''
|
||||
)
|
||||
const [inputValue, setInputValue] = createSignal(props.defaultValue ?? '')
|
||||
let inputRef: HTMLInputElement | undefined
|
||||
|
||||
const handleInput = (inputValue: string) => setInputValue(inputValue)
|
||||
@ -30,6 +29,10 @@ export const EmailInput = (props: Props) => {
|
||||
if (e.key === 'Enter') submit()
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (!isMobile() && inputRef) inputRef.focus()
|
||||
})
|
||||
|
||||
return (
|
||||
<div
|
||||
class={
|
||||
|
@ -1,20 +1,19 @@
|
||||
import { ShortTextInput } from '@/components/inputs'
|
||||
import { SendButton } from '@/components/SendButton'
|
||||
import { InputSubmitContent } from '@/types'
|
||||
import { isMobile } from '@/utils/isMobileSignal'
|
||||
import { NumberInputBlock } from 'models'
|
||||
import { createSignal } from 'solid-js'
|
||||
import { createSignal, onMount } from 'solid-js'
|
||||
|
||||
type NumberInputProps = {
|
||||
block: NumberInputBlock & { prefilledValue?: string }
|
||||
block: NumberInputBlock
|
||||
defaultValue?: string
|
||||
onSubmit: (value: InputSubmitContent) => void
|
||||
hasGuestAvatar: boolean
|
||||
}
|
||||
|
||||
export const NumberInput = (props: NumberInputProps) => {
|
||||
const [inputValue, setInputValue] = createSignal(
|
||||
// eslint-disable-next-line solid/reactivity
|
||||
props.block.prefilledValue ?? ''
|
||||
)
|
||||
const [inputValue, setInputValue] = createSignal(props.defaultValue ?? '')
|
||||
let inputRef: HTMLInputElement | undefined
|
||||
|
||||
const handleInput = (inputValue: string) => setInputValue(inputValue)
|
||||
@ -30,6 +29,10 @@ export const NumberInput = (props: NumberInputProps) => {
|
||||
if (e.key === 'Enter') submit()
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (!isMobile() && inputRef) inputRef.focus()
|
||||
})
|
||||
|
||||
return (
|
||||
<div
|
||||
class={
|
||||
|
@ -3,21 +3,19 @@ import { SendButton } from '@/components/SendButton'
|
||||
import { InputSubmitContent } from '@/types'
|
||||
import { isMobile } from '@/utils/isMobileSignal'
|
||||
import type { PhoneNumberInputBlock } from 'models'
|
||||
import { createSignal, For } from 'solid-js'
|
||||
import { createSignal, For, onMount } from 'solid-js'
|
||||
import { phoneCountries } from 'utils/phoneCountries'
|
||||
|
||||
type PhoneInputProps = {
|
||||
block: PhoneNumberInputBlock & { prefilledValue?: string }
|
||||
block: PhoneNumberInputBlock
|
||||
defaultValue?: string
|
||||
onSubmit: (value: InputSubmitContent) => void
|
||||
hasGuestAvatar: boolean
|
||||
}
|
||||
|
||||
export const PhoneInput = (props: PhoneInputProps) => {
|
||||
const [selectedCountryCode, setSelectedCountryCode] = createSignal('INT')
|
||||
const [inputValue, setInputValue] = createSignal(
|
||||
// eslint-disable-next-line solid/reactivity
|
||||
props.block.prefilledValue ?? ''
|
||||
)
|
||||
const [inputValue, setInputValue] = createSignal(props.defaultValue ?? '')
|
||||
let inputRef: HTMLInputElement | undefined
|
||||
|
||||
const handleInput = (inputValue: string | undefined) => {
|
||||
@ -47,11 +45,13 @@ export const PhoneInput = (props: PhoneInputProps) => {
|
||||
setSelectedCountryCode(event.currentTarget.value)
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (!isMobile() && inputRef) inputRef.focus()
|
||||
})
|
||||
|
||||
return (
|
||||
<div
|
||||
class={
|
||||
'flex items-end justify-between rounded-lg pr-2 typebot-input w-full'
|
||||
}
|
||||
class={'flex items-end justify-between rounded-lg pr-2 typebot-input'}
|
||||
data-testid="input"
|
||||
style={{
|
||||
'margin-right': props.hasGuestAvatar ? '50px' : '0.5rem',
|
||||
|
@ -5,20 +5,21 @@ import { createSignal, For, Match, Switch } from 'solid-js'
|
||||
import { isDefined, isEmpty, isNotDefined } from 'utils'
|
||||
|
||||
type Props = {
|
||||
block: RatingInputBlock & { prefilledValue?: string }
|
||||
block: RatingInputBlock
|
||||
defaultValue?: string
|
||||
onSubmit: (value: InputSubmitContent) => void
|
||||
}
|
||||
|
||||
export const RatingForm = (props: Props) => {
|
||||
const [rating, setRating] = createSignal<number | undefined>(
|
||||
// eslint-disable-next-line solid/reactivity
|
||||
props.block.prefilledValue ? Number(props.block.prefilledValue) : undefined
|
||||
props.defaultValue ? Number(props.defaultValue) : undefined
|
||||
)
|
||||
|
||||
const handleSubmit = (e: SubmitEvent) => {
|
||||
e.preventDefault()
|
||||
if (isNotDefined(rating)) return
|
||||
props.onSubmit({ value: rating.toString() })
|
||||
const selectedRating = rating()
|
||||
if (isNotDefined(selectedRating)) return
|
||||
props.onSubmit({ value: selectedRating.toString() })
|
||||
}
|
||||
|
||||
const handleClick = (rating: number) => {
|
||||
|
@ -1,20 +1,19 @@
|
||||
import { Textarea, ShortTextInput } from '@/components/inputs'
|
||||
import { SendButton } from '@/components/SendButton'
|
||||
import { InputSubmitContent } from '@/types'
|
||||
import { isMobile } from '@/utils/isMobileSignal'
|
||||
import { TextInputBlock } from 'models'
|
||||
import { createSignal } from 'solid-js'
|
||||
import { createSignal, onMount } from 'solid-js'
|
||||
|
||||
type Props = {
|
||||
block: TextInputBlock & { prefilledValue?: string }
|
||||
onSubmit: (value: InputSubmitContent) => void
|
||||
block: TextInputBlock
|
||||
defaultValue?: string
|
||||
hasGuestAvatar: boolean
|
||||
onSubmit: (value: InputSubmitContent) => void
|
||||
}
|
||||
|
||||
export const TextInput = (props: Props) => {
|
||||
const [inputValue, setInputValue] = createSignal(
|
||||
// eslint-disable-next-line solid/reactivity
|
||||
props.block.prefilledValue ?? ''
|
||||
)
|
||||
const [inputValue, setInputValue] = createSignal(props.defaultValue ?? '')
|
||||
let inputRef: HTMLInputElement | HTMLTextAreaElement | undefined
|
||||
|
||||
const handleInput = (inputValue: string) => setInputValue(inputValue)
|
||||
@ -31,6 +30,10 @@ export const TextInput = (props: Props) => {
|
||||
if (e.key === 'Enter') submit()
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (!isMobile() && inputRef) inputRef.focus()
|
||||
})
|
||||
|
||||
return (
|
||||
<div
|
||||
class={
|
||||
|
@ -1,20 +1,19 @@
|
||||
import { ShortTextInput } from '@/components/inputs'
|
||||
import { SendButton } from '@/components/SendButton'
|
||||
import { InputSubmitContent } from '@/types'
|
||||
import { isMobile } from '@/utils/isMobileSignal'
|
||||
import { UrlInputBlock } from 'models'
|
||||
import { createSignal } from 'solid-js'
|
||||
import { createSignal, onMount } from 'solid-js'
|
||||
|
||||
type Props = {
|
||||
block: UrlInputBlock & { prefilledValue?: string }
|
||||
block: UrlInputBlock
|
||||
defaultValue?: string
|
||||
onSubmit: (value: InputSubmitContent) => void
|
||||
hasGuestAvatar: boolean
|
||||
}
|
||||
|
||||
export const UrlInput = (props: Props) => {
|
||||
const [inputValue, setInputValue] = createSignal(
|
||||
// eslint-disable-next-line solid/reactivity
|
||||
props.block.prefilledValue ?? ''
|
||||
)
|
||||
const [inputValue, setInputValue] = createSignal(props.defaultValue ?? '')
|
||||
let inputRef: HTMLInputElement | HTMLTextAreaElement | undefined
|
||||
|
||||
const handleInput = (inputValue: string) => {
|
||||
@ -36,6 +35,10 @@ export const UrlInput = (props: Props) => {
|
||||
if (e.key === 'Enter') submit()
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (!isMobile() && inputRef) inputRef.focus()
|
||||
})
|
||||
|
||||
return (
|
||||
<div
|
||||
class={
|
||||
|
@ -20,7 +20,7 @@ export const Bubble = (props: BubbleProps) => {
|
||||
'onClose',
|
||||
'previewMessage',
|
||||
'onPreviewMessageClick',
|
||||
'button',
|
||||
'theme',
|
||||
])
|
||||
const [prefilledVariables, setPrefilledVariables] = createSignal(
|
||||
// eslint-disable-next-line solid/reactivity
|
||||
@ -106,13 +106,13 @@ export const Bubble = (props: BubbleProps) => {
|
||||
<Show when={isPreviewMessageDisplayed()}>
|
||||
<PreviewMessage
|
||||
{...previewMessage()}
|
||||
button={bubbleProps.button}
|
||||
previewMessageTheme={bubbleProps.theme?.previewMessage}
|
||||
onClick={handlePreviewMessageClick}
|
||||
onCloseClick={hideMessage}
|
||||
/>
|
||||
</Show>
|
||||
<BubbleButton
|
||||
{...bubbleProps.button}
|
||||
{...bubbleProps.theme?.button}
|
||||
toggleBot={toggleBot}
|
||||
isBotOpened={isBotOpened()}
|
||||
/>
|
||||
@ -126,7 +126,7 @@ export const Bubble = (props: BubbleProps) => {
|
||||
'box-shadow': 'rgb(0 0 0 / 16%) 0px 5px 40px',
|
||||
}}
|
||||
class={
|
||||
'absolute bottom-20 sm:right-4 rounded-lg bg-white w-full sm:w-[400px] max-h-[704px] ' +
|
||||
'absolute bottom-20 sm:right-4 rounded-lg w-full sm:w-[400px] max-h-[704px] ' +
|
||||
(isBotOpened() ? 'opacity-1' : 'opacity-0 pointer-events-none')
|
||||
}
|
||||
>
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Show } from 'solid-js'
|
||||
import { ButtonParams } from '../types'
|
||||
import { ButtonTheme } from '../types'
|
||||
|
||||
type Props = ButtonParams & {
|
||||
type Props = ButtonTheme & {
|
||||
isBotOpened: boolean
|
||||
toggleBot: () => void
|
||||
}
|
||||
|
@ -1,14 +1,14 @@
|
||||
import { createSignal } from 'solid-js'
|
||||
import { BubbleParams, PreviewMessageParams } from '../types'
|
||||
import { PreviewMessageParams, PreviewMessageTheme } from '../types'
|
||||
|
||||
export type PreviewMessageProps = Pick<
|
||||
PreviewMessageParams,
|
||||
'avatarUrl' | 'message' | 'style'
|
||||
> &
|
||||
Pick<BubbleParams, 'button'> & {
|
||||
onClick: () => void
|
||||
onCloseClick: () => void
|
||||
}
|
||||
'avatarUrl' | 'message'
|
||||
> & {
|
||||
previewMessageTheme?: PreviewMessageTheme
|
||||
onClick: () => void
|
||||
onCloseClick: () => void
|
||||
}
|
||||
|
||||
const defaultFontFamily =
|
||||
"-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'"
|
||||
@ -23,9 +23,11 @@ export const PreviewMessage = (props: PreviewMessageProps) => {
|
||||
onClick={props.onClick}
|
||||
class="absolute bottom-20 right-4 w-64 rounded-md duration-200 flex items-center gap-4 shadow-md animate-fade-in cursor-pointer hover:shadow-lg p-4"
|
||||
style={{
|
||||
'font-family': props.style?.fontFamily ?? defaultFontFamily,
|
||||
'background-color': props.style?.backgroundColor ?? '#F7F8FF',
|
||||
color: props.style?.color ?? '#303235',
|
||||
'font-family':
|
||||
props.previewMessageTheme?.fontFamily ?? defaultFontFamily,
|
||||
'background-color':
|
||||
props.previewMessageTheme?.backgroundColor ?? '#F7F8FF',
|
||||
color: props.previewMessageTheme?.color ?? '#303235',
|
||||
}}
|
||||
onMouseEnter={() => setIsPreviewMessageHovered(true)}
|
||||
onMouseLeave={() => setIsPreviewMessageHovered(false)}
|
||||
@ -40,8 +42,9 @@ export const PreviewMessage = (props: PreviewMessageProps) => {
|
||||
return props.onCloseClick()
|
||||
}}
|
||||
style={{
|
||||
'background-color': props.style?.closeButtonBgColor ?? '#F7F8FF',
|
||||
color: props.style?.closeButtonColor ?? '#303235',
|
||||
'background-color':
|
||||
props.previewMessageTheme?.closeButtonBgColor ?? '#F7F8FF',
|
||||
color: props.previewMessageTheme?.closeButtonColor ?? '#303235',
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
|
@ -1,9 +1,14 @@
|
||||
export type BubbleParams = {
|
||||
button: ButtonParams
|
||||
previewMessage: PreviewMessageParams
|
||||
theme?: BubbleTheme
|
||||
previewMessage?: PreviewMessageParams
|
||||
}
|
||||
|
||||
export type ButtonParams = {
|
||||
export type BubbleTheme = {
|
||||
button?: ButtonTheme
|
||||
previewMessage?: PreviewMessageTheme
|
||||
}
|
||||
|
||||
export type ButtonTheme = {
|
||||
backgroundColor?: string
|
||||
icon?: {
|
||||
color?: string
|
||||
@ -15,13 +20,12 @@ export type PreviewMessageParams = {
|
||||
avatarUrl?: string
|
||||
message: string
|
||||
autoShowDelay?: number
|
||||
style?: PreviewMessageStyle
|
||||
}
|
||||
|
||||
type PreviewMessageStyle = Partial<{
|
||||
backgroundColor: string
|
||||
color: string
|
||||
fontFamily: string
|
||||
closeButtonBgColor: string
|
||||
closeButtonColor: string
|
||||
}>
|
||||
export type PreviewMessageTheme = {
|
||||
backgroundColor?: string
|
||||
color?: string
|
||||
fontFamily?: string
|
||||
closeButtonBgColor?: string
|
||||
closeButtonColor?: string
|
||||
}
|
||||
|
@ -1,5 +1,12 @@
|
||||
import styles from '../../../assets/index.css'
|
||||
import { createSignal, onMount, Show, splitProps, onCleanup } from 'solid-js'
|
||||
import {
|
||||
createSignal,
|
||||
onMount,
|
||||
Show,
|
||||
splitProps,
|
||||
onCleanup,
|
||||
createEffect,
|
||||
} from 'solid-js'
|
||||
import { Bot, BotProps } from '../../../components/Bot'
|
||||
import { CommandData } from '@/features/commands'
|
||||
import { isDefined } from 'utils'
|
||||
@ -7,6 +14,8 @@ import { PopupParams } from '../types'
|
||||
|
||||
export type PopupProps = BotProps &
|
||||
PopupParams & {
|
||||
defaultOpen?: boolean
|
||||
isOpen?: boolean
|
||||
onOpen?: () => void
|
||||
onClose?: () => void
|
||||
}
|
||||
@ -18,7 +27,9 @@ export const Popup = (props: PopupProps) => {
|
||||
'onOpen',
|
||||
'onClose',
|
||||
'autoShowDelay',
|
||||
'style',
|
||||
'theme',
|
||||
'isOpen',
|
||||
'defaultOpen',
|
||||
])
|
||||
|
||||
const [prefilledVariables, setPrefilledVariables] = createSignal(
|
||||
@ -26,10 +37,14 @@ export const Popup = (props: PopupProps) => {
|
||||
botProps.prefilledVariables
|
||||
)
|
||||
|
||||
const [isBotOpened, setIsBotOpened] = createSignal(false)
|
||||
const [isBotOpened, setIsBotOpened] = createSignal(
|
||||
// eslint-disable-next-line solid/reactivity
|
||||
popupProps.isOpen ?? popupProps.defaultOpen ?? false
|
||||
)
|
||||
|
||||
onMount(() => {
|
||||
window.addEventListener('click', processWindowClick)
|
||||
document.addEventListener('pointerdown', processWindowClick)
|
||||
botContainer?.addEventListener('pointerdown', stopPropagation)
|
||||
window.addEventListener('message', processIncomingEvent)
|
||||
const autoShowDelay = popupProps.autoShowDelay
|
||||
if (isDefined(autoShowDelay)) {
|
||||
@ -39,16 +54,25 @@ export const Popup = (props: PopupProps) => {
|
||||
}
|
||||
})
|
||||
|
||||
onCleanup(() => {
|
||||
window.removeEventListener('message', processIncomingEvent)
|
||||
window.removeEventListener('click', processWindowClick)
|
||||
createEffect(() => {
|
||||
const isOpen = popupProps.isOpen
|
||||
if (isDefined(isOpen)) setIsBotOpened(isOpen)
|
||||
})
|
||||
|
||||
const processWindowClick = (event: MouseEvent) => {
|
||||
if (!botContainer || botContainer.contains(event.target as Node)) return
|
||||
onCleanup(() => {
|
||||
document.removeEventListener('pointerdown', processWindowClick)
|
||||
botContainer?.removeEventListener('pointerdown', stopPropagation)
|
||||
window.removeEventListener('message', processIncomingEvent)
|
||||
})
|
||||
|
||||
const processWindowClick = () => {
|
||||
setIsBotOpened(false)
|
||||
}
|
||||
|
||||
const stopPropagation = (event: MouseEvent) => {
|
||||
event.stopPropagation()
|
||||
}
|
||||
|
||||
const processIncomingEvent = (event: MessageEvent<CommandData>) => {
|
||||
const { data } = event
|
||||
if (!data.isFromTypebot) return
|
||||
@ -63,16 +87,19 @@ export const Popup = (props: PopupProps) => {
|
||||
}
|
||||
|
||||
const openBot = () => {
|
||||
setIsBotOpened(true)
|
||||
if (isBotOpened()) popupProps.onOpen?.()
|
||||
if (isDefined(props.isOpen)) return
|
||||
setIsBotOpened(true)
|
||||
}
|
||||
|
||||
const closeBot = () => {
|
||||
setIsBotOpened(false)
|
||||
if (isBotOpened()) popupProps.onClose?.()
|
||||
if (isDefined(props.isOpen)) return
|
||||
setIsBotOpened(false)
|
||||
}
|
||||
|
||||
const toggleBot = () => {
|
||||
if (isDefined(props.isOpen)) return
|
||||
isBotOpened() ? closeBot() : openBot()
|
||||
}
|
||||
|
||||
@ -85,15 +112,11 @@ export const Popup = (props: PopupProps) => {
|
||||
aria-modal="true"
|
||||
>
|
||||
<style>{styles}</style>
|
||||
<div class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity animate-fade-in" />
|
||||
<div class="fixed inset-0 bg-black bg-opacity-50 transition-opacity animate-fade-in" />
|
||||
<div class="fixed inset-0 z-10 overflow-y-auto">
|
||||
<div class="flex min-h-full items-center justify-center p-4 text-center sm:p-0">
|
||||
<div
|
||||
class="relative h-[80vh] transform overflow-hidden rounded-lg text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg"
|
||||
style={{
|
||||
width: popupProps.style?.width ?? '100%',
|
||||
'background-color': popupProps.style?.backgroundColor ?? '#fff',
|
||||
}}
|
||||
ref={botContainer}
|
||||
>
|
||||
<Bot {...botProps} prefilledVariables={prefilledVariables()} />
|
||||
|
@ -1,6 +1,6 @@
|
||||
export type PopupParams = {
|
||||
autoShowDelay?: number
|
||||
style?: {
|
||||
theme?: {
|
||||
width?: string
|
||||
backgroundColor?: string
|
||||
}
|
||||
|
@ -1,28 +1,31 @@
|
||||
import { InitialChatReply, SendMessageInput, StartParams } from 'models'
|
||||
import { getViewerUrl, sendRequest } from 'utils'
|
||||
import { InitialChatReply } from '@/types'
|
||||
import { SendMessageInput, StartParams } from 'models'
|
||||
import { getViewerUrl, isEmpty, sendRequest } from 'utils'
|
||||
|
||||
export async function getInitialChatReplyQuery({
|
||||
typebotId,
|
||||
typebot,
|
||||
isPreview,
|
||||
apiHost,
|
||||
prefilledVariables,
|
||||
startGroupId,
|
||||
resultId,
|
||||
}: StartParams & {
|
||||
apiHost?: string
|
||||
}) {
|
||||
if (!typebotId)
|
||||
if (!typebot)
|
||||
throw new Error('Typebot ID is required to get initial messages')
|
||||
|
||||
const response = await sendRequest<InitialChatReply>({
|
||||
return sendRequest<InitialChatReply>({
|
||||
method: 'POST',
|
||||
url: `${apiHost ?? getViewerUrl()}/api/v1/sendMessage`,
|
||||
url: `${isEmpty(apiHost) ? getViewerUrl() : apiHost}/api/v1/sendMessage`,
|
||||
body: {
|
||||
startParams: {
|
||||
isPreview,
|
||||
typebotId,
|
||||
typebot,
|
||||
prefilledVariables,
|
||||
startGroupId,
|
||||
resultId,
|
||||
},
|
||||
} satisfies SendMessageInput,
|
||||
})
|
||||
|
||||
return response.data
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { ChatReply, SendMessageInput } from 'models'
|
||||
import { getViewerUrl, sendRequest } from 'utils'
|
||||
import { getViewerUrl, isEmpty, sendRequest } from 'utils'
|
||||
|
||||
export async function sendMessageQuery({
|
||||
apiHost,
|
||||
@ -7,7 +7,7 @@ export async function sendMessageQuery({
|
||||
}: SendMessageInput & { apiHost?: string }) {
|
||||
const response = await sendRequest<ChatReply>({
|
||||
method: 'POST',
|
||||
url: `${apiHost ?? getViewerUrl()}/api/v1/sendMessage`,
|
||||
url: `${isEmpty(apiHost) ? getViewerUrl() : apiHost}/api/v1/sendMessage`,
|
||||
body,
|
||||
})
|
||||
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { ChatReply } from 'models'
|
||||
|
||||
export type InputSubmitContent = {
|
||||
label?: string
|
||||
value: string
|
||||
@ -5,7 +7,12 @@ export type InputSubmitContent = {
|
||||
|
||||
export type BotContext = {
|
||||
typebotId: string
|
||||
resultId: string
|
||||
resultId?: string
|
||||
isPreview: boolean
|
||||
apiHost?: string
|
||||
}
|
||||
|
||||
export type InitialChatReply = ChatReply & {
|
||||
typebot: NonNullable<ChatReply['typebot']>
|
||||
sessionId: NonNullable<ChatReply['sessionId']>
|
||||
}
|
||||
|
17
packages/js/src/utils/sessionStorage.ts
Normal file
17
packages/js/src/utils/sessionStorage.ts
Normal file
@ -0,0 +1,17 @@
|
||||
const sessionStorageKey = 'resultId'
|
||||
|
||||
export const getExistingResultIdFromSession = () => {
|
||||
try {
|
||||
return sessionStorage.getItem(sessionStorageKey) ?? undefined
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
}
|
||||
|
||||
export const setResultInSession = (resultId: string) => {
|
||||
try {
|
||||
return sessionStorage.setItem(sessionStorageKey, resultId)
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
}
|
144
packages/js/src/utils/setCssVariablesValue.ts
Normal file
144
packages/js/src/utils/setCssVariablesValue.ts
Normal file
@ -0,0 +1,144 @@
|
||||
import {
|
||||
Background,
|
||||
BackgroundType,
|
||||
ChatTheme,
|
||||
ContainerColors,
|
||||
GeneralTheme,
|
||||
InputColors,
|
||||
Theme,
|
||||
} from 'models'
|
||||
|
||||
const cssVariableNames = {
|
||||
general: {
|
||||
bgImage: '--typebot-container-bg-image',
|
||||
bgColor: '--typebot-container-bg-color',
|
||||
fontFamily: '--typebot-container-font-family',
|
||||
},
|
||||
chat: {
|
||||
hostBubbles: {
|
||||
bgColor: '--typebot-host-bubble-bg-color',
|
||||
color: '--typebot-host-bubble-color',
|
||||
},
|
||||
guestBubbles: {
|
||||
bgColor: '--typebot-guest-bubble-bg-color',
|
||||
color: '--typebot-guest-bubble-color',
|
||||
},
|
||||
inputs: {
|
||||
bgColor: '--typebot-input-bg-color',
|
||||
color: '--typebot-input-color',
|
||||
placeholderColor: '--typebot-input-placeholder-color',
|
||||
},
|
||||
buttons: {
|
||||
bgColor: '--typebot-button-bg-color',
|
||||
color: '--typebot-button-color',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const setCssVariablesValue = (
|
||||
theme: Theme | undefined,
|
||||
container: HTMLDivElement
|
||||
) => {
|
||||
if (!theme) return
|
||||
const documentStyle = container?.style
|
||||
if (!documentStyle) return
|
||||
if (theme.general) setGeneralTheme(theme.general, documentStyle)
|
||||
if (theme.chat) setChatTheme(theme.chat, documentStyle)
|
||||
}
|
||||
|
||||
const setGeneralTheme = (
|
||||
generalTheme: GeneralTheme,
|
||||
documentStyle: CSSStyleDeclaration
|
||||
) => {
|
||||
const { background, font } = generalTheme
|
||||
if (background) setTypebotBackground(background, documentStyle)
|
||||
if (font) documentStyle.setProperty(cssVariableNames.general.fontFamily, font)
|
||||
}
|
||||
|
||||
const setChatTheme = (
|
||||
chatTheme: ChatTheme,
|
||||
documentStyle: CSSStyleDeclaration
|
||||
) => {
|
||||
const { hostBubbles, guestBubbles, buttons, inputs } = chatTheme
|
||||
if (hostBubbles) setHostBubbles(hostBubbles, documentStyle)
|
||||
if (guestBubbles) setGuestBubbles(guestBubbles, documentStyle)
|
||||
if (buttons) setButtons(buttons, documentStyle)
|
||||
if (inputs) setInputs(inputs, documentStyle)
|
||||
}
|
||||
|
||||
const setHostBubbles = (
|
||||
hostBubbles: ContainerColors,
|
||||
documentStyle: CSSStyleDeclaration
|
||||
) => {
|
||||
if (hostBubbles.backgroundColor)
|
||||
documentStyle.setProperty(
|
||||
cssVariableNames.chat.hostBubbles.bgColor,
|
||||
hostBubbles.backgroundColor
|
||||
)
|
||||
if (hostBubbles.color)
|
||||
documentStyle.setProperty(
|
||||
cssVariableNames.chat.hostBubbles.color,
|
||||
hostBubbles.color
|
||||
)
|
||||
}
|
||||
|
||||
const setGuestBubbles = (
|
||||
guestBubbles: ContainerColors,
|
||||
documentStyle: CSSStyleDeclaration
|
||||
) => {
|
||||
if (guestBubbles.backgroundColor)
|
||||
documentStyle.setProperty(
|
||||
cssVariableNames.chat.guestBubbles.bgColor,
|
||||
guestBubbles.backgroundColor
|
||||
)
|
||||
if (guestBubbles.color)
|
||||
documentStyle.setProperty(
|
||||
cssVariableNames.chat.guestBubbles.color,
|
||||
guestBubbles.color
|
||||
)
|
||||
}
|
||||
|
||||
const setButtons = (
|
||||
buttons: ContainerColors,
|
||||
documentStyle: CSSStyleDeclaration
|
||||
) => {
|
||||
if (buttons.backgroundColor)
|
||||
documentStyle.setProperty(
|
||||
cssVariableNames.chat.buttons.bgColor,
|
||||
buttons.backgroundColor
|
||||
)
|
||||
if (buttons.color)
|
||||
documentStyle.setProperty(
|
||||
cssVariableNames.chat.buttons.color,
|
||||
buttons.color
|
||||
)
|
||||
}
|
||||
|
||||
const setInputs = (inputs: InputColors, documentStyle: CSSStyleDeclaration) => {
|
||||
if (inputs.backgroundColor)
|
||||
documentStyle.setProperty(
|
||||
cssVariableNames.chat.inputs.bgColor,
|
||||
inputs.backgroundColor
|
||||
)
|
||||
if (inputs.color)
|
||||
documentStyle.setProperty(cssVariableNames.chat.inputs.color, inputs.color)
|
||||
if (inputs.placeholderColor)
|
||||
documentStyle.setProperty(
|
||||
cssVariableNames.chat.inputs.placeholderColor,
|
||||
inputs.placeholderColor
|
||||
)
|
||||
}
|
||||
|
||||
const setTypebotBackground = (
|
||||
background: Background,
|
||||
documentStyle: CSSStyleDeclaration
|
||||
) => {
|
||||
documentStyle.setProperty(
|
||||
background?.type === BackgroundType.IMAGE
|
||||
? cssVariableNames.general.bgImage
|
||||
: cssVariableNames.general.bgColor,
|
||||
background.type === BackgroundType.NONE
|
||||
? 'transparent'
|
||||
: background.content ?? '#ffffff'
|
||||
)
|
||||
}
|
Reference in New Issue
Block a user