⚡ Restore chat state when user is remembered (#1333)
Closes #993 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Added a detailed explanation page for the "Remember user" setting in the app documentation. - Introduced persistence of chat state across sessions, with options for local or session storage. - Enhanced bot functionality to store and retrieve initial chat replies and manage bot open state with improved storage handling. - Added a new callback for chat state persistence to bot component props. - **Improvements** - Updated the general settings form to clarify the description of the "Remember user" feature. - Enhanced custom CSS handling and progress value persistence in bot components. - Added conditional transition disabling in various components for smoother user experiences. - Simplified the handling of `onTransitionEnd` across multiple bubble components. - **Refactor** - Renamed `inputIndex` to `chunkIndex` or `index` in various components for consistency. - Removed unused ESLint disable comments related to reactivity rules. - Adjusted import statements and cleaned up code across several files. - **Bug Fixes** - Fixed potential issues with undefined callbacks by introducing optional chaining in component props. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@ -85,7 +85,7 @@ export const GeneralSettingsForm = ({
|
|||||||
/>
|
/>
|
||||||
<SwitchWithRelatedSettings
|
<SwitchWithRelatedSettings
|
||||||
label={'Remember user'}
|
label={'Remember user'}
|
||||||
moreInfoContent="If enabled, user previous variables will be prefilled and his new answers will override the previous ones."
|
moreInfoContent="If enabled, the chat state will be restored if the user comes back after exiting."
|
||||||
initialValue={
|
initialValue={
|
||||||
generalSettings?.rememberUser?.isEnabled ??
|
generalSettings?.rememberUser?.isEnabled ??
|
||||||
(isDefined(generalSettings?.isNewResultOnRefreshEnabled)
|
(isDefined(generalSettings?.isNewResultOnRefreshEnabled)
|
||||||
@ -112,7 +112,7 @@ export const GeneralSettingsForm = ({
|
|||||||
<Tag size="sm" bgColor={keyBg}>
|
<Tag size="sm" bgColor={keyBg}>
|
||||||
local
|
local
|
||||||
</Tag>{' '}
|
</Tag>{' '}
|
||||||
to remember the user forever.
|
to remember the user forever on the same device.
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
</MoreInfoTooltip>
|
</MoreInfoTooltip>
|
||||||
|
@ -142,7 +142,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"group": "Settings",
|
"group": "Settings",
|
||||||
"pages": ["settings/overview"]
|
"pages": ["settings/overview", "settings/remember-user"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"group": "Deploy",
|
"group": "Deploy",
|
||||||
|
@ -14,7 +14,7 @@ The general settings represent the general behaviors of your typebot.
|
|||||||
|
|
||||||
- **Prefill input**: If enabled, the inputs will be automatically pre-filled whenever their associated variable has a value.
|
- **Prefill input**: If enabled, the inputs will be automatically pre-filled whenever their associated variable has a value.
|
||||||
- **Hide query params on bot start**: If enabled, the query params will be hidden when the bot starts.
|
- **Hide query params on bot start**: If enabled, the query params will be hidden when the bot starts.
|
||||||
- **Remember user**: If enabled, user previous variables will be prefilled and his new answers will override the previous ones.
|
- [**Remember user**](./remember-user)
|
||||||
|
|
||||||
## Typing emulation
|
## Typing emulation
|
||||||
|
|
||||||
|
13
apps/docs/settings/remember-user.mdx
Normal file
13
apps/docs/settings/remember-user.mdx
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
---
|
||||||
|
title: Remember user
|
||||||
|
icon: bookmark
|
||||||
|
---
|
||||||
|
|
||||||
|
Head over to the `Settings` tab of your typebot, under the `General` section you can find the `Remember user` setting.
|
||||||
|
|
||||||
|
This setting allows you to save the chat session state into the user's web browser storage. It means that if he answers a question and then closes the chat, the next time he opens it, the chat will be in the same state as it was before.
|
||||||
|
|
||||||
|
There are 2 storage options:
|
||||||
|
|
||||||
|
- **Local storage**: The chat state will be saved in the user's web browser. It will be available only on the same device and web browser.
|
||||||
|
- **Session storage**: The chat state will be saved in the user's web browser. It will be available only on the same device and web browser, but it will be deleted when the user closes the current tab or the web browser.
|
@ -6,7 +6,7 @@ type Props = {
|
|||||||
}
|
}
|
||||||
export const findResult = ({ id }: Props) =>
|
export const findResult = ({ id }: Props) =>
|
||||||
prisma.result.findFirst({
|
prisma.result.findFirst({
|
||||||
where: { id },
|
where: { id, isArchived: { not: true } },
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
variables: true,
|
variables: true,
|
||||||
|
@ -6,5 +6,6 @@ module.exports = {
|
|||||||
'@next/next/no-img-element': 'off',
|
'@next/next/no-img-element': 'off',
|
||||||
'@next/next/no-html-link-for-pages': 'off',
|
'@next/next/no-html-link-for-pages': 'off',
|
||||||
'solid/no-innerhtml': 'off',
|
'solid/no-innerhtml': 'off',
|
||||||
|
'solid/reactivity': 'off',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@typebot.io/js",
|
"name": "@typebot.io/js",
|
||||||
"version": "0.2.48",
|
"version": "0.2.49",
|
||||||
"description": "Javascript library to display typebots on your website",
|
"description": "Javascript library to display typebots on your website",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
|
@ -8,7 +8,10 @@ import { BotContext, InitialChatReply, OutgoingLog } from '@/types'
|
|||||||
import { ErrorMessage } from './ErrorMessage'
|
import { ErrorMessage } from './ErrorMessage'
|
||||||
import {
|
import {
|
||||||
getExistingResultIdFromStorage,
|
getExistingResultIdFromStorage,
|
||||||
|
getInitialChatReplyFromStorage,
|
||||||
|
setInitialChatReplyInStorage,
|
||||||
setResultInStorage,
|
setResultInStorage,
|
||||||
|
wipeExistingChatStateInStorage,
|
||||||
} from '@/utils/storage'
|
} from '@/utils/storage'
|
||||||
import { setCssVariablesValue } from '@/utils/setCssVariablesValue'
|
import { setCssVariablesValue } from '@/utils/setCssVariablesValue'
|
||||||
import immutableCss from '../assets/immutable.css'
|
import immutableCss from '../assets/immutable.css'
|
||||||
@ -20,6 +23,8 @@ import { HTTPError } from 'ky'
|
|||||||
import { injectFont } from '@/utils/injectFont'
|
import { injectFont } from '@/utils/injectFont'
|
||||||
import { ProgressBar } from './ProgressBar'
|
import { ProgressBar } from './ProgressBar'
|
||||||
import { Portal } from 'solid-js/web'
|
import { Portal } from 'solid-js/web'
|
||||||
|
import { defaultSettings } from '@typebot.io/schemas/features/typebot/settings/constants'
|
||||||
|
import { persist } from '@/utils/persist'
|
||||||
|
|
||||||
export type BotProps = {
|
export type BotProps = {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
@ -35,6 +40,7 @@ export type BotProps = {
|
|||||||
onInit?: () => void
|
onInit?: () => void
|
||||||
onEnd?: () => void
|
onEnd?: () => void
|
||||||
onNewLogs?: (logs: OutgoingLog[]) => void
|
onNewLogs?: (logs: OutgoingLog[]) => void
|
||||||
|
onChatStatePersisted?: (isEnabled: boolean) => void
|
||||||
startFrom?: StartFrom
|
startFrom?: StartFrom
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -59,14 +65,13 @@ export const Bot = (props: BotProps & { class?: string }) => {
|
|||||||
typeof props.typebot === 'string' ? props.typebot : undefined
|
typeof props.typebot === 'string' ? props.typebot : undefined
|
||||||
const isPreview =
|
const isPreview =
|
||||||
typeof props.typebot !== 'string' || (props.isPreview ?? false)
|
typeof props.typebot !== 'string' || (props.isPreview ?? false)
|
||||||
|
const resultIdInStorage = getExistingResultIdFromStorage(typebotIdFromProps)
|
||||||
const { data, error } = await startChatQuery({
|
const { data, error } = await startChatQuery({
|
||||||
stripeRedirectStatus: urlParams.get('redirect_status') ?? undefined,
|
stripeRedirectStatus: urlParams.get('redirect_status') ?? undefined,
|
||||||
typebot: props.typebot,
|
typebot: props.typebot,
|
||||||
apiHost: props.apiHost,
|
apiHost: props.apiHost,
|
||||||
isPreview,
|
isPreview,
|
||||||
resultId: isNotEmpty(props.resultId)
|
resultId: isNotEmpty(props.resultId) ? props.resultId : resultIdInStorage,
|
||||||
? props.resultId
|
|
||||||
: getExistingResultIdFromStorage(typebotIdFromProps),
|
|
||||||
prefilledVariables: {
|
prefilledVariables: {
|
||||||
...prefilledVariables,
|
...prefilledVariables,
|
||||||
...props.prefilledVariables,
|
...props.prefilledVariables,
|
||||||
@ -111,17 +116,40 @@ export const Bot = (props: BotProps & { class?: string }) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.resultId && typebotIdFromProps)
|
if (
|
||||||
setResultInStorage(data.typebot.settings.general?.rememberUser?.storage)(
|
data.resultId &&
|
||||||
typebotIdFromProps,
|
typebotIdFromProps &&
|
||||||
data.resultId
|
(data.typebot.settings.general?.rememberUser?.isEnabled ??
|
||||||
|
defaultSettings.general.rememberUser.isEnabled)
|
||||||
|
) {
|
||||||
|
if (resultIdInStorage && resultIdInStorage !== data.resultId)
|
||||||
|
wipeExistingChatStateInStorage(data.typebot.id)
|
||||||
|
const storage =
|
||||||
|
data.typebot.settings.general?.rememberUser?.storage ??
|
||||||
|
defaultSettings.general.rememberUser.storage
|
||||||
|
setResultInStorage(storage)(typebotIdFromProps, data.resultId)
|
||||||
|
const initialChatInStorage = getInitialChatReplyFromStorage(
|
||||||
|
data.typebot.id
|
||||||
)
|
)
|
||||||
|
if (initialChatInStorage) {
|
||||||
|
setInitialChatReply(initialChatInStorage)
|
||||||
|
} else {
|
||||||
|
setInitialChatReply(data)
|
||||||
|
setInitialChatReplyInStorage(data, {
|
||||||
|
typebotId: data.typebot.id,
|
||||||
|
storage,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
props.onChatStatePersisted?.(true)
|
||||||
|
} else {
|
||||||
setInitialChatReply(data)
|
setInitialChatReply(data)
|
||||||
setCustomCss(data.typebot.theme.customCss ?? '')
|
|
||||||
|
|
||||||
if (data.input?.id && props.onNewInputBlock)
|
if (data.input?.id && props.onNewInputBlock)
|
||||||
props.onNewInputBlock(data.input)
|
props.onNewInputBlock(data.input)
|
||||||
if (data.logs) props.onNewLogs?.(data.logs)
|
if (data.logs) props.onNewLogs?.(data.logs)
|
||||||
|
props.onChatStatePersisted?.(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
setCustomCss(data.typebot.theme.customCss ?? '')
|
||||||
}
|
}
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
@ -178,6 +206,16 @@ export const Bot = (props: BotProps & { class?: string }) => {
|
|||||||
resultId: initialChatReply.resultId,
|
resultId: initialChatReply.resultId,
|
||||||
sessionId: initialChatReply.sessionId,
|
sessionId: initialChatReply.sessionId,
|
||||||
typebot: initialChatReply.typebot,
|
typebot: initialChatReply.typebot,
|
||||||
|
storage:
|
||||||
|
initialChatReply.typebot.settings.general?.rememberUser
|
||||||
|
?.isEnabled &&
|
||||||
|
!(
|
||||||
|
typeof props.typebot !== 'string' ||
|
||||||
|
(props.isPreview ?? false)
|
||||||
|
)
|
||||||
|
? initialChatReply.typebot.settings.general?.rememberUser
|
||||||
|
?.storage ?? defaultSettings.general.rememberUser.storage
|
||||||
|
: undefined,
|
||||||
}}
|
}}
|
||||||
progressBarRef={props.progressBarRef}
|
progressBarRef={props.progressBarRef}
|
||||||
onNewInputBlock={props.onNewInputBlock}
|
onNewInputBlock={props.onNewInputBlock}
|
||||||
@ -203,8 +241,12 @@ type BotContentProps = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const BotContent = (props: BotContentProps) => {
|
const BotContent = (props: BotContentProps) => {
|
||||||
const [progressValue, setProgressValue] = createSignal<number | undefined>(
|
const [progressValue, setProgressValue] = persist(
|
||||||
props.initialChatReply.progress
|
createSignal<number | undefined>(props.initialChatReply.progress),
|
||||||
|
{
|
||||||
|
storage: props.context.storage,
|
||||||
|
key: `typebot-${props.context.typebot.id}-progressValue`,
|
||||||
|
}
|
||||||
)
|
)
|
||||||
let botContainer: HTMLDivElement | undefined
|
let botContainer: HTMLDivElement | undefined
|
||||||
|
|
||||||
|
@ -2,7 +2,11 @@ import { createSignal, onCleanup, onMount } from 'solid-js'
|
|||||||
import { isMobile } from '@/utils/isMobileSignal'
|
import { isMobile } from '@/utils/isMobileSignal'
|
||||||
import { Avatar } from '../avatars/Avatar'
|
import { Avatar } from '../avatars/Avatar'
|
||||||
|
|
||||||
type Props = { hostAvatarSrc?: string; hideAvatar?: boolean }
|
type Props = {
|
||||||
|
hostAvatarSrc?: string
|
||||||
|
hideAvatar?: boolean
|
||||||
|
isTransitionDisabled?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
export const AvatarSideContainer = (props: Props) => {
|
export const AvatarSideContainer = (props: Props) => {
|
||||||
let avatarContainer: HTMLDivElement | undefined
|
let avatarContainer: HTMLDivElement | undefined
|
||||||
@ -40,7 +44,9 @@ export const AvatarSideContainer = (props: Props) => {
|
|||||||
}
|
}
|
||||||
style={{
|
style={{
|
||||||
top: `${top()}px`,
|
top: `${top()}px`,
|
||||||
transition: 'top 350ms ease-out, opacity 250ms ease-out',
|
transition: props.isTransitionDisabled
|
||||||
|
? undefined
|
||||||
|
: 'top 350ms ease-out, opacity 250ms ease-out',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Avatar initialAvatarSrc={props.hostAvatarSrc} />
|
<Avatar initialAvatarSrc={props.hostAvatarSrc} />
|
||||||
|
@ -12,11 +12,12 @@ import { defaultSettings } from '@typebot.io/schemas/features/typebot/settings/c
|
|||||||
type Props = Pick<ContinueChatResponse, 'messages' | 'input'> & {
|
type Props = Pick<ContinueChatResponse, 'messages' | 'input'> & {
|
||||||
theme: Theme
|
theme: Theme
|
||||||
settings: Settings
|
settings: Settings
|
||||||
inputIndex: number
|
index: number
|
||||||
context: BotContext
|
context: BotContext
|
||||||
hasError: boolean
|
hasError: boolean
|
||||||
hideAvatar: boolean
|
hideAvatar: boolean
|
||||||
streamingMessageId: ChatChunkType['streamingMessageId']
|
streamingMessageId: ChatChunkType['streamingMessageId']
|
||||||
|
isTransitionDisabled?: boolean
|
||||||
onNewBubbleDisplayed: (blockId: string) => Promise<void>
|
onNewBubbleDisplayed: (blockId: string) => Promise<void>
|
||||||
onScrollToBottom: (top?: number) => void
|
onScrollToBottom: (top?: number) => void
|
||||||
onSubmit: (input?: string) => void
|
onSubmit: (input?: string) => void
|
||||||
@ -26,7 +27,9 @@ type Props = Pick<ContinueChatResponse, 'messages' | 'input'> & {
|
|||||||
|
|
||||||
export const ChatChunk = (props: Props) => {
|
export const ChatChunk = (props: Props) => {
|
||||||
let inputRef: HTMLDivElement | undefined
|
let inputRef: HTMLDivElement | undefined
|
||||||
const [displayedMessageIndex, setDisplayedMessageIndex] = createSignal(0)
|
const [displayedMessageIndex, setDisplayedMessageIndex] = createSignal(
|
||||||
|
props.isTransitionDisabled ? props.messages.length : 0
|
||||||
|
)
|
||||||
const [lastBubbleOffsetTop, setLastBubbleOffsetTop] = createSignal<number>()
|
const [lastBubbleOffsetTop, setLastBubbleOffsetTop] = createSignal<number>()
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
@ -45,7 +48,6 @@ export const ChatChunk = (props: Props) => {
|
|||||||
defaultSettings.typingEmulation.delayBetweenBubbles) > 0 &&
|
defaultSettings.typingEmulation.delayBetweenBubbles) > 0 &&
|
||||||
displayedMessageIndex() < props.messages.length - 1
|
displayedMessageIndex() < props.messages.length - 1
|
||||||
) {
|
) {
|
||||||
// eslint-disable-next-line solid/reactivity
|
|
||||||
await new Promise((resolve) =>
|
await new Promise((resolve) =>
|
||||||
setTimeout(
|
setTimeout(
|
||||||
resolve,
|
resolve,
|
||||||
@ -82,6 +84,7 @@ export const ChatChunk = (props: Props) => {
|
|||||||
<AvatarSideContainer
|
<AvatarSideContainer
|
||||||
hostAvatarSrc={props.theme.chat?.hostAvatar?.url}
|
hostAvatarSrc={props.theme.chat?.hostAvatar?.url}
|
||||||
hideAvatar={props.hideAvatar}
|
hideAvatar={props.hideAvatar}
|
||||||
|
isTransitionDisabled={props.isTransitionDisabled}
|
||||||
/>
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
@ -108,10 +111,12 @@ export const ChatChunk = (props: Props) => {
|
|||||||
(props.settings.typingEmulation?.isDisabledOnFirstMessage ??
|
(props.settings.typingEmulation?.isDisabledOnFirstMessage ??
|
||||||
defaultSettings.typingEmulation
|
defaultSettings.typingEmulation
|
||||||
.isDisabledOnFirstMessage) &&
|
.isDisabledOnFirstMessage) &&
|
||||||
props.inputIndex === 0 &&
|
props.index === 0 &&
|
||||||
idx() === 0
|
idx() === 0
|
||||||
}
|
}
|
||||||
onTransitionEnd={displayNextMessage}
|
onTransitionEnd={
|
||||||
|
props.isTransitionDisabled ? undefined : displayNextMessage
|
||||||
|
}
|
||||||
onCompleted={props.onSubmit}
|
onCompleted={props.onSubmit}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@ -123,7 +128,7 @@ export const ChatChunk = (props: Props) => {
|
|||||||
<InputChatBlock
|
<InputChatBlock
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
block={props.input}
|
block={props.input}
|
||||||
inputIndex={props.inputIndex}
|
chunkIndex={props.index}
|
||||||
hasHostAvatar={
|
hasHostAvatar={
|
||||||
props.theme.chat?.hostAvatar?.isEnabled ??
|
props.theme.chat?.hostAvatar?.isEnabled ??
|
||||||
defaultTheme.chat.hostAvatar.isEnabled
|
defaultTheme.chat.hostAvatar.isEnabled
|
||||||
|
@ -32,6 +32,7 @@ import {
|
|||||||
import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants'
|
import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants'
|
||||||
import { saveClientLogsQuery } from '@/queries/saveClientLogsQuery'
|
import { saveClientLogsQuery } from '@/queries/saveClientLogsQuery'
|
||||||
import { HTTPError } from 'ky'
|
import { HTTPError } from 'ky'
|
||||||
|
import { persist } from '@/utils/persist'
|
||||||
|
|
||||||
const parseDynamicTheme = (
|
const parseDynamicTheme = (
|
||||||
initialTheme: Theme,
|
initialTheme: Theme,
|
||||||
@ -69,13 +70,19 @@ type Props = {
|
|||||||
|
|
||||||
export const ConversationContainer = (props: Props) => {
|
export const ConversationContainer = (props: Props) => {
|
||||||
let chatContainer: HTMLDivElement | undefined
|
let chatContainer: HTMLDivElement | undefined
|
||||||
const [chatChunks, setChatChunks] = createSignal<ChatChunkType[]>([
|
const [chatChunks, setChatChunks] = persist(
|
||||||
|
createSignal<ChatChunkType[]>([
|
||||||
{
|
{
|
||||||
input: props.initialChatReply.input,
|
input: props.initialChatReply.input,
|
||||||
messages: props.initialChatReply.messages,
|
messages: props.initialChatReply.messages,
|
||||||
clientSideActions: props.initialChatReply.clientSideActions,
|
clientSideActions: props.initialChatReply.clientSideActions,
|
||||||
},
|
},
|
||||||
])
|
]),
|
||||||
|
{
|
||||||
|
key: `typebot-${props.context.typebot.id}-chatChunks`,
|
||||||
|
storage: props.context.storage,
|
||||||
|
}
|
||||||
|
)
|
||||||
const [dynamicTheme, setDynamicTheme] = createSignal<
|
const [dynamicTheme, setDynamicTheme] = createSignal<
|
||||||
ContinueChatResponse['dynamicTheme']
|
ContinueChatResponse['dynamicTheme']
|
||||||
>(props.initialChatReply.dynamicTheme)
|
>(props.initialChatReply.dynamicTheme)
|
||||||
@ -276,7 +283,7 @@ export const ConversationContainer = (props: Props) => {
|
|||||||
<For each={chatChunks()}>
|
<For each={chatChunks()}>
|
||||||
{(chatChunk, index) => (
|
{(chatChunk, index) => (
|
||||||
<ChatChunk
|
<ChatChunk
|
||||||
inputIndex={index()}
|
index={index()}
|
||||||
messages={chatChunk.messages}
|
messages={chatChunk.messages}
|
||||||
input={chatChunk.input}
|
input={chatChunk.input}
|
||||||
theme={theme()}
|
theme={theme()}
|
||||||
@ -290,6 +297,7 @@ export const ConversationContainer = (props: Props) => {
|
|||||||
(chatChunk.messages.length > 0 && isSending()))
|
(chatChunk.messages.length > 0 && isSending()))
|
||||||
}
|
}
|
||||||
hasError={hasError() && index() === chatChunks().length - 1}
|
hasError={hasError() && index() === chatChunks().length - 1}
|
||||||
|
isTransitionDisabled={index() !== chatChunks().length - 1}
|
||||||
onNewBubbleDisplayed={handleNewBubbleDisplayed}
|
onNewBubbleDisplayed={handleNewBubbleDisplayed}
|
||||||
onAllBubblesDisplayed={handleAllBubblesDisplayed}
|
onAllBubblesDisplayed={handleAllBubblesDisplayed}
|
||||||
onSubmit={sendMessage}
|
onSubmit={sendMessage}
|
||||||
|
@ -36,13 +36,14 @@ import { formattedMessages } from '@/utils/formattedMessagesSignal'
|
|||||||
import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants'
|
import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants'
|
||||||
import { defaultPaymentInputOptions } from '@typebot.io/schemas/features/blocks/inputs/payment/constants'
|
import { defaultPaymentInputOptions } from '@typebot.io/schemas/features/blocks/inputs/payment/constants'
|
||||||
import { defaultTheme } from '@typebot.io/schemas/features/typebot/theme/constants'
|
import { defaultTheme } from '@typebot.io/schemas/features/typebot/theme/constants'
|
||||||
|
import { persist } from '@/utils/persist'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
ref: HTMLDivElement | undefined
|
ref: HTMLDivElement | undefined
|
||||||
block: NonNullable<ContinueChatResponse['input']>
|
block: NonNullable<ContinueChatResponse['input']>
|
||||||
hasHostAvatar: boolean
|
hasHostAvatar: boolean
|
||||||
guestAvatar?: NonNullable<Theme['chat']>['guestAvatar']
|
guestAvatar?: NonNullable<Theme['chat']>['guestAvatar']
|
||||||
inputIndex: number
|
chunkIndex: number
|
||||||
context: BotContext
|
context: BotContext
|
||||||
isInputPrefillEnabled: boolean
|
isInputPrefillEnabled: boolean
|
||||||
hasError: boolean
|
hasError: boolean
|
||||||
@ -52,7 +53,10 @@ type Props = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const InputChatBlock = (props: Props) => {
|
export const InputChatBlock = (props: Props) => {
|
||||||
const [answer, setAnswer] = createSignal<string>()
|
const [answer, setAnswer] = persist(createSignal<string>(), {
|
||||||
|
key: `typebot-${props.context.typebot.id}-input-${props.chunkIndex}`,
|
||||||
|
storage: props.context.storage,
|
||||||
|
})
|
||||||
const [formattedMessage, setFormattedMessage] = createSignal<string>()
|
const [formattedMessage, setFormattedMessage] = createSignal<string>()
|
||||||
|
|
||||||
const handleSubmit = async ({ label, value }: InputSubmitContent) => {
|
const handleSubmit = async ({ label, value }: InputSubmitContent) => {
|
||||||
@ -67,7 +71,7 @@ export const InputChatBlock = (props: Props) => {
|
|||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const formattedMessage = formattedMessages().findLast(
|
const formattedMessage = formattedMessages().findLast(
|
||||||
(message) => props.inputIndex === message.inputIndex
|
(message) => props.chunkIndex === message.inputIndex
|
||||||
)?.formattedMessage
|
)?.formattedMessage
|
||||||
if (formattedMessage) setFormattedMessage(formattedMessage)
|
if (formattedMessage) setFormattedMessage(formattedMessage)
|
||||||
})
|
})
|
||||||
@ -101,7 +105,7 @@ export const InputChatBlock = (props: Props) => {
|
|||||||
<Input
|
<Input
|
||||||
context={props.context}
|
context={props.context}
|
||||||
block={props.block}
|
block={props.block}
|
||||||
inputIndex={props.inputIndex}
|
chunkIndex={props.chunkIndex}
|
||||||
isInputPrefillEnabled={props.isInputPrefillEnabled}
|
isInputPrefillEnabled={props.isInputPrefillEnabled}
|
||||||
existingAnswer={props.hasError ? answer() : undefined}
|
existingAnswer={props.hasError ? answer() : undefined}
|
||||||
onTransitionEnd={props.onTransitionEnd}
|
onTransitionEnd={props.onTransitionEnd}
|
||||||
@ -117,7 +121,7 @@ export const InputChatBlock = (props: Props) => {
|
|||||||
const Input = (props: {
|
const Input = (props: {
|
||||||
context: BotContext
|
context: BotContext
|
||||||
block: NonNullable<ContinueChatResponse['input']>
|
block: NonNullable<ContinueChatResponse['input']>
|
||||||
inputIndex: number
|
chunkIndex: number
|
||||||
isInputPrefillEnabled: boolean
|
isInputPrefillEnabled: boolean
|
||||||
existingAnswer?: string
|
existingAnswer?: string
|
||||||
onTransitionEnd: () => void
|
onTransitionEnd: () => void
|
||||||
@ -189,7 +193,7 @@ const Input = (props: {
|
|||||||
<Switch>
|
<Switch>
|
||||||
<Match when={!block.options?.isMultipleChoice}>
|
<Match when={!block.options?.isMultipleChoice}>
|
||||||
<Buttons
|
<Buttons
|
||||||
inputIndex={props.inputIndex}
|
chunkIndex={props.chunkIndex}
|
||||||
defaultItems={block.items}
|
defaultItems={block.items}
|
||||||
options={block.options}
|
options={block.options}
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
@ -197,7 +201,6 @@ const Input = (props: {
|
|||||||
</Match>
|
</Match>
|
||||||
<Match when={block.options?.isMultipleChoice}>
|
<Match when={block.options?.isMultipleChoice}>
|
||||||
<MultipleChoicesForm
|
<MultipleChoicesForm
|
||||||
inputIndex={props.inputIndex}
|
|
||||||
defaultItems={block.items}
|
defaultItems={block.items}
|
||||||
options={block.options}
|
options={block.options}
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
|
@ -21,60 +21,50 @@ type Props = {
|
|||||||
message: ChatMessage
|
message: ChatMessage
|
||||||
typingEmulation: Settings['typingEmulation']
|
typingEmulation: Settings['typingEmulation']
|
||||||
isTypingSkipped: boolean
|
isTypingSkipped: boolean
|
||||||
onTransitionEnd: (offsetTop?: number) => void
|
onTransitionEnd?: (offsetTop?: number) => void
|
||||||
onCompleted: (reply?: string) => void
|
onCompleted: (reply?: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const HostBubble = (props: Props) => {
|
export const HostBubble = (props: Props) => (
|
||||||
const onTransitionEnd = (offsetTop?: number) => {
|
|
||||||
props.onTransitionEnd(offsetTop)
|
|
||||||
}
|
|
||||||
|
|
||||||
const onCompleted = (reply?: string) => {
|
|
||||||
props.onCompleted(reply)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={props.message.type === BubbleBlockType.TEXT}>
|
<Match when={props.message.type === BubbleBlockType.TEXT}>
|
||||||
<TextBubble
|
<TextBubble
|
||||||
content={props.message.content as TextBubbleBlock['content']}
|
content={props.message.content as TextBubbleBlock['content']}
|
||||||
isTypingSkipped={props.isTypingSkipped}
|
isTypingSkipped={props.isTypingSkipped}
|
||||||
typingEmulation={props.typingEmulation}
|
typingEmulation={props.typingEmulation}
|
||||||
onTransitionEnd={onTransitionEnd}
|
onTransitionEnd={props.onTransitionEnd}
|
||||||
/>
|
/>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={props.message.type === BubbleBlockType.IMAGE}>
|
<Match when={props.message.type === BubbleBlockType.IMAGE}>
|
||||||
<ImageBubble
|
<ImageBubble
|
||||||
content={props.message.content as ImageBubbleBlock['content']}
|
content={props.message.content as ImageBubbleBlock['content']}
|
||||||
onTransitionEnd={onTransitionEnd}
|
onTransitionEnd={props.onTransitionEnd}
|
||||||
/>
|
/>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={props.message.type === BubbleBlockType.VIDEO}>
|
<Match when={props.message.type === BubbleBlockType.VIDEO}>
|
||||||
<VideoBubble
|
<VideoBubble
|
||||||
content={props.message.content as VideoBubbleBlock['content']}
|
content={props.message.content as VideoBubbleBlock['content']}
|
||||||
onTransitionEnd={onTransitionEnd}
|
onTransitionEnd={props.onTransitionEnd}
|
||||||
/>
|
/>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={props.message.type === BubbleBlockType.EMBED}>
|
<Match when={props.message.type === BubbleBlockType.EMBED}>
|
||||||
<EmbedBubble
|
<EmbedBubble
|
||||||
content={props.message.content as EmbedBubbleBlock['content']}
|
content={props.message.content as EmbedBubbleBlock['content']}
|
||||||
onTransitionEnd={onTransitionEnd}
|
onTransitionEnd={props.onTransitionEnd}
|
||||||
/>
|
/>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={props.message.type === 'custom-embed'}>
|
<Match when={props.message.type === 'custom-embed'}>
|
||||||
<CustomEmbedBubble
|
<CustomEmbedBubble
|
||||||
content={props.message.content as CustomEmbedBubbleProps['content']}
|
content={props.message.content as CustomEmbedBubbleProps['content']}
|
||||||
onTransitionEnd={onTransitionEnd}
|
onTransitionEnd={props.onTransitionEnd}
|
||||||
onCompleted={onCompleted}
|
onCompleted={props.onCompleted}
|
||||||
/>
|
/>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={props.message.type === BubbleBlockType.AUDIO}>
|
<Match when={props.message.type === BubbleBlockType.AUDIO}>
|
||||||
<AudioBubble
|
<AudioBubble
|
||||||
content={props.message.content as AudioBubbleBlock['content']}
|
content={props.message.content as AudioBubbleBlock['content']}
|
||||||
onTransitionEnd={onTransitionEnd}
|
onTransitionEnd={props.onTransitionEnd}
|
||||||
/>
|
/>
|
||||||
</Match>
|
</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
@ -3,10 +3,11 @@ import { isMobile } from '@/utils/isMobileSignal'
|
|||||||
import { AudioBubbleBlock } from '@typebot.io/schemas'
|
import { AudioBubbleBlock } from '@typebot.io/schemas'
|
||||||
import { createSignal, onCleanup, onMount } from 'solid-js'
|
import { createSignal, onCleanup, onMount } from 'solid-js'
|
||||||
import { defaultAudioBubbleContent } from '@typebot.io/schemas/features/blocks/bubbles/audio/constants'
|
import { defaultAudioBubbleContent } from '@typebot.io/schemas/features/blocks/bubbles/audio/constants'
|
||||||
|
import clsx from 'clsx'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
content: AudioBubbleBlock['content']
|
content: AudioBubbleBlock['content']
|
||||||
onTransitionEnd: (offsetTop?: number) => void
|
onTransitionEnd?: (offsetTop?: number) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const showAnimationDuration = 400
|
const showAnimationDuration = 400
|
||||||
@ -18,7 +19,9 @@ export const AudioBubble = (props: Props) => {
|
|||||||
let isPlayed = false
|
let isPlayed = false
|
||||||
let ref: HTMLDivElement | undefined
|
let ref: HTMLDivElement | undefined
|
||||||
let audioElement: HTMLAudioElement | undefined
|
let audioElement: HTMLAudioElement | undefined
|
||||||
const [isTyping, setIsTyping] = createSignal(true)
|
const [isTyping, setIsTyping] = createSignal(
|
||||||
|
props.onTransitionEnd ? true : false
|
||||||
|
)
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
typingTimeout = setTimeout(() => {
|
typingTimeout = setTimeout(() => {
|
||||||
@ -26,7 +29,7 @@ export const AudioBubble = (props: Props) => {
|
|||||||
isPlayed = true
|
isPlayed = true
|
||||||
setIsTyping(false)
|
setIsTyping(false)
|
||||||
setTimeout(
|
setTimeout(
|
||||||
() => props.onTransitionEnd(ref?.offsetTop),
|
() => props.onTransitionEnd?.(ref?.offsetTop),
|
||||||
showAnimationDuration
|
showAnimationDuration
|
||||||
)
|
)
|
||||||
}, typingDuration)
|
}, typingDuration)
|
||||||
@ -37,7 +40,13 @@ export const AudioBubble = (props: Props) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="flex flex-col animate-fade-in" ref={ref}>
|
<div
|
||||||
|
class={clsx(
|
||||||
|
'flex flex-col',
|
||||||
|
props.onTransitionEnd ? 'animate-fade-in' : undefined
|
||||||
|
)}
|
||||||
|
ref={ref}
|
||||||
|
>
|
||||||
<div class="flex w-full items-center">
|
<div class="flex w-full items-center">
|
||||||
<div class="flex relative z-10 items-start typebot-host-bubble max-w-full">
|
<div class="flex relative z-10 items-start typebot-host-bubble max-w-full">
|
||||||
<div
|
<div
|
||||||
@ -53,8 +62,10 @@ export const AudioBubble = (props: Props) => {
|
|||||||
ref={audioElement}
|
ref={audioElement}
|
||||||
src={props.content?.url}
|
src={props.content?.url}
|
||||||
autoplay={
|
autoplay={
|
||||||
props.content?.isAutoplayEnabled ??
|
props.onTransitionEnd
|
||||||
|
? props.content?.isAutoplayEnabled ??
|
||||||
defaultAudioBubbleContent.isAutoplayEnabled
|
defaultAudioBubbleContent.isAutoplayEnabled
|
||||||
|
: false
|
||||||
}
|
}
|
||||||
class={
|
class={
|
||||||
'z-10 text-fade-in ' +
|
'z-10 text-fade-in ' +
|
||||||
|
@ -7,7 +7,7 @@ import { executeCode } from '@/features/blocks/logic/script/executeScript'
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
content: CustomEmbedBubbleProps['content']
|
content: CustomEmbedBubbleProps['content']
|
||||||
onTransitionEnd: (offsetTop?: number) => void
|
onTransitionEnd?: (offsetTop?: number) => void
|
||||||
onCompleted: (reply?: string) => void
|
onCompleted: (reply?: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -17,7 +17,9 @@ export const showAnimationDuration = 400
|
|||||||
|
|
||||||
export const CustomEmbedBubble = (props: Props) => {
|
export const CustomEmbedBubble = (props: Props) => {
|
||||||
let ref: HTMLDivElement | undefined
|
let ref: HTMLDivElement | undefined
|
||||||
const [isTyping, setIsTyping] = createSignal(true)
|
const [isTyping, setIsTyping] = createSignal(
|
||||||
|
props.onTransitionEnd ? true : false
|
||||||
|
)
|
||||||
let containerRef: HTMLDivElement | undefined
|
let containerRef: HTMLDivElement | undefined
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
@ -41,7 +43,7 @@ export const CustomEmbedBubble = (props: Props) => {
|
|||||||
typingTimeout = setTimeout(() => {
|
typingTimeout = setTimeout(() => {
|
||||||
setIsTyping(false)
|
setIsTyping(false)
|
||||||
setTimeout(
|
setTimeout(
|
||||||
() => props.onTransitionEnd(ref?.offsetTop),
|
() => props.onTransitionEnd?.(ref?.offsetTop),
|
||||||
showAnimationDuration
|
showAnimationDuration
|
||||||
)
|
)
|
||||||
}, 2000)
|
}, 2000)
|
||||||
@ -52,7 +54,13 @@ export const CustomEmbedBubble = (props: Props) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="flex flex-col w-full animate-fade-in" ref={ref}>
|
<div
|
||||||
|
class={clsx(
|
||||||
|
'flex flex-col w-full',
|
||||||
|
props.onTransitionEnd ? 'animate-fade-in' : undefined
|
||||||
|
)}
|
||||||
|
ref={ref}
|
||||||
|
>
|
||||||
<div class="flex w-full items-center">
|
<div class="flex w-full items-center">
|
||||||
<div class="flex relative z-10 items-start typebot-host-bubble w-full max-w-full">
|
<div class="flex relative z-10 items-start typebot-host-bubble w-full max-w-full">
|
||||||
<div
|
<div
|
||||||
|
@ -7,7 +7,7 @@ import { defaultEmbedBubbleContent } from '@typebot.io/schemas/features/blocks/b
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
content: EmbedBubbleBlock['content']
|
content: EmbedBubbleBlock['content']
|
||||||
onTransitionEnd: (offsetTop?: number) => void
|
onTransitionEnd?: (offsetTop?: number) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
let typingTimeout: NodeJS.Timeout
|
let typingTimeout: NodeJS.Timeout
|
||||||
@ -16,13 +16,15 @@ export const showAnimationDuration = 400
|
|||||||
|
|
||||||
export const EmbedBubble = (props: Props) => {
|
export const EmbedBubble = (props: Props) => {
|
||||||
let ref: HTMLDivElement | undefined
|
let ref: HTMLDivElement | undefined
|
||||||
const [isTyping, setIsTyping] = createSignal(true)
|
const [isTyping, setIsTyping] = createSignal(
|
||||||
|
props.onTransitionEnd ? true : false
|
||||||
|
)
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
typingTimeout = setTimeout(() => {
|
typingTimeout = setTimeout(() => {
|
||||||
setIsTyping(false)
|
setIsTyping(false)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
props.onTransitionEnd(ref?.offsetTop)
|
props.onTransitionEnd?.(ref?.offsetTop)
|
||||||
}, showAnimationDuration)
|
}, showAnimationDuration)
|
||||||
}, 2000)
|
}, 2000)
|
||||||
})
|
})
|
||||||
@ -32,7 +34,13 @@ export const EmbedBubble = (props: Props) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="flex flex-col w-full animate-fade-in" ref={ref}>
|
<div
|
||||||
|
class={clsx(
|
||||||
|
'flex flex-col w-full',
|
||||||
|
props.onTransitionEnd ? 'animate-fade-in' : undefined
|
||||||
|
)}
|
||||||
|
ref={ref}
|
||||||
|
>
|
||||||
<div class="flex w-full items-center">
|
<div class="flex w-full items-center">
|
||||||
<div class="flex relative z-10 items-start typebot-host-bubble w-full max-w-full">
|
<div class="flex relative z-10 items-start typebot-host-bubble w-full max-w-full">
|
||||||
<div
|
<div
|
||||||
|
@ -7,7 +7,7 @@ import { defaultImageBubbleContent } from '@typebot.io/schemas/features/blocks/b
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
content: ImageBubbleBlock['content']
|
content: ImageBubbleBlock['content']
|
||||||
onTransitionEnd: (offsetTop?: number) => void
|
onTransitionEnd?: (offsetTop?: number) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const showAnimationDuration = 400
|
export const showAnimationDuration = 400
|
||||||
@ -19,13 +19,15 @@ let typingTimeout: NodeJS.Timeout
|
|||||||
export const ImageBubble = (props: Props) => {
|
export const ImageBubble = (props: Props) => {
|
||||||
let ref: HTMLDivElement | undefined
|
let ref: HTMLDivElement | undefined
|
||||||
let image: HTMLImageElement | undefined
|
let image: HTMLImageElement | undefined
|
||||||
const [isTyping, setIsTyping] = createSignal(true)
|
const [isTyping, setIsTyping] = createSignal(
|
||||||
|
props.onTransitionEnd ? true : false
|
||||||
|
)
|
||||||
|
|
||||||
const onTypingEnd = () => {
|
const onTypingEnd = () => {
|
||||||
if (!isTyping()) return
|
if (!isTyping()) return
|
||||||
setIsTyping(false)
|
setIsTyping(false)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
props.onTransitionEnd(ref?.offsetTop)
|
props.onTransitionEnd?.(ref?.offsetTop)
|
||||||
}, showAnimationDuration)
|
}, showAnimationDuration)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -49,9 +51,11 @@ export const ImageBubble = (props: Props) => {
|
|||||||
alt={
|
alt={
|
||||||
props.content?.clickLink?.alt ?? defaultImageBubbleContent.clickLink.alt
|
props.content?.clickLink?.alt ?? defaultImageBubbleContent.clickLink.alt
|
||||||
}
|
}
|
||||||
class={
|
class={clsx(
|
||||||
'text-fade-in w-full ' + (isTyping() ? 'opacity-0' : 'opacity-100')
|
'w-full',
|
||||||
}
|
isTyping() ? 'opacity-0' : 'opacity-100',
|
||||||
|
props.onTransitionEnd ? 'text-fade-in' : undefined
|
||||||
|
)}
|
||||||
style={{
|
style={{
|
||||||
'max-height': '512px',
|
'max-height': '512px',
|
||||||
height: isTyping() ? '32px' : 'auto',
|
height: isTyping() ? '32px' : 'auto',
|
||||||
@ -62,7 +66,13 @@ export const ImageBubble = (props: Props) => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="flex flex-col animate-fade-in" ref={ref}>
|
<div
|
||||||
|
class={clsx(
|
||||||
|
'flex flex-col',
|
||||||
|
props.onTransitionEnd ? 'animate-fade-in' : undefined
|
||||||
|
)}
|
||||||
|
ref={ref}
|
||||||
|
>
|
||||||
<div class="flex w-full items-center">
|
<div class="flex w-full items-center">
|
||||||
<div class="flex relative z-10 items-start typebot-host-bubble max-w-full">
|
<div class="flex relative z-10 items-start typebot-host-bubble max-w-full">
|
||||||
<div
|
<div
|
||||||
|
@ -11,7 +11,7 @@ type Props = {
|
|||||||
content: TextBubbleBlock['content']
|
content: TextBubbleBlock['content']
|
||||||
typingEmulation: Settings['typingEmulation']
|
typingEmulation: Settings['typingEmulation']
|
||||||
isTypingSkipped: boolean
|
isTypingSkipped: boolean
|
||||||
onTransitionEnd: (offsetTop?: number) => void
|
onTransitionEnd?: (offsetTop?: number) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const showAnimationDuration = 400
|
export const showAnimationDuration = 400
|
||||||
@ -20,13 +20,15 @@ let typingTimeout: NodeJS.Timeout
|
|||||||
|
|
||||||
export const TextBubble = (props: Props) => {
|
export const TextBubble = (props: Props) => {
|
||||||
let ref: HTMLDivElement | undefined
|
let ref: HTMLDivElement | undefined
|
||||||
const [isTyping, setIsTyping] = createSignal(true)
|
const [isTyping, setIsTyping] = createSignal(
|
||||||
|
props.onTransitionEnd ? true : false
|
||||||
|
)
|
||||||
|
|
||||||
const onTypingEnd = () => {
|
const onTypingEnd = () => {
|
||||||
if (!isTyping()) return
|
if (!isTyping()) return
|
||||||
setIsTyping(false)
|
setIsTyping(false)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
props.onTransitionEnd(ref?.offsetTop)
|
props.onTransitionEnd?.(ref?.offsetTop)
|
||||||
}, showAnimationDuration)
|
}, showAnimationDuration)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,7 +52,13 @@ export const TextBubble = (props: Props) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="flex flex-col animate-fade-in" ref={ref}>
|
<div
|
||||||
|
class={clsx(
|
||||||
|
'flex flex-col',
|
||||||
|
props.onTransitionEnd ? 'animate-fade-in' : undefined
|
||||||
|
)}
|
||||||
|
ref={ref}
|
||||||
|
>
|
||||||
<div class="flex w-full items-center">
|
<div class="flex w-full items-center">
|
||||||
<div class="flex relative items-start typebot-host-bubble max-w-full">
|
<div class="flex relative items-start typebot-host-bubble max-w-full">
|
||||||
<div
|
<div
|
||||||
|
@ -15,7 +15,7 @@ import {
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
content: VideoBubbleBlock['content']
|
content: VideoBubbleBlock['content']
|
||||||
onTransitionEnd: (offsetTop?: number) => void
|
onTransitionEnd?: (offsetTop?: number) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const showAnimationDuration = 400
|
export const showAnimationDuration = 400
|
||||||
@ -23,7 +23,9 @@ let typingTimeout: NodeJS.Timeout
|
|||||||
|
|
||||||
export const VideoBubble = (props: Props) => {
|
export const VideoBubble = (props: Props) => {
|
||||||
let ref: HTMLDivElement | undefined
|
let ref: HTMLDivElement | undefined
|
||||||
const [isTyping, setIsTyping] = createSignal(true)
|
const [isTyping, setIsTyping] = createSignal(
|
||||||
|
props.onTransitionEnd ? true : false
|
||||||
|
)
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
const typingDuration =
|
const typingDuration =
|
||||||
@ -37,7 +39,7 @@ export const VideoBubble = (props: Props) => {
|
|||||||
if (!isTyping()) return
|
if (!isTyping()) return
|
||||||
setIsTyping(false)
|
setIsTyping(false)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
props.onTransitionEnd(ref?.offsetTop)
|
props.onTransitionEnd?.(ref?.offsetTop)
|
||||||
}, showAnimationDuration)
|
}, showAnimationDuration)
|
||||||
}, typingDuration)
|
}, typingDuration)
|
||||||
})
|
})
|
||||||
@ -47,7 +49,13 @@ export const VideoBubble = (props: Props) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="flex flex-col w-full animate-fade-in" ref={ref}>
|
<div
|
||||||
|
class={clsx(
|
||||||
|
'flex flex-col w-full',
|
||||||
|
props.onTransitionEnd ? 'animate-fade-in' : undefined
|
||||||
|
)}
|
||||||
|
ref={ref}
|
||||||
|
>
|
||||||
<div class="flex w-full items-center">
|
<div class="flex w-full items-center">
|
||||||
<div class="flex relative z-10 items-start typebot-host-bubble overflow-hidden w-full max-w-full">
|
<div class="flex relative z-10 items-start typebot-host-bubble overflow-hidden w-full max-w-full">
|
||||||
<div
|
<div
|
||||||
@ -69,7 +77,7 @@ export const VideoBubble = (props: Props) => {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<video
|
<video
|
||||||
autoplay
|
autoplay={props.onTransitionEnd ? false : true}
|
||||||
src={props.content?.url}
|
src={props.content?.url}
|
||||||
controls
|
controls
|
||||||
class={
|
class={
|
||||||
|
@ -7,7 +7,7 @@ import { For, Show, createSignal, onMount } from 'solid-js'
|
|||||||
import { defaultChoiceInputOptions } from '@typebot.io/schemas/features/blocks/inputs/choice/constants'
|
import { defaultChoiceInputOptions } from '@typebot.io/schemas/features/blocks/inputs/choice/constants'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
inputIndex: number
|
chunkIndex: number
|
||||||
defaultItems: ChoiceInputBlock['items']
|
defaultItems: ChoiceInputBlock['items']
|
||||||
options: ChoiceInputBlock['options']
|
options: ChoiceInputBlock['options']
|
||||||
onSubmit: (value: InputSubmitContent) => void
|
onSubmit: (value: InputSubmitContent) => void
|
||||||
@ -66,7 +66,7 @@ export const Buttons = (props: Props) => {
|
|||||||
>
|
>
|
||||||
{item.content}
|
{item.content}
|
||||||
</Button>
|
</Button>
|
||||||
{props.inputIndex === 0 && props.defaultItems.length === 1 && (
|
{props.chunkIndex === 0 && props.defaultItems.length === 1 && (
|
||||||
<span class="flex h-3 w-3 absolute top-0 right-0 -mt-1 -mr-1 ping">
|
<span class="flex h-3 w-3 absolute top-0 right-0 -mt-1 -mr-1 ping">
|
||||||
<span class="animate-ping absolute inline-flex h-full w-full rounded-full brightness-200 opacity-75" />
|
<span class="animate-ping absolute inline-flex h-full w-full rounded-full brightness-200 opacity-75" />
|
||||||
<span class="relative inline-flex rounded-full h-3 w-3 brightness-150" />
|
<span class="relative inline-flex rounded-full h-3 w-3 brightness-150" />
|
||||||
|
@ -8,7 +8,6 @@ import { SearchInput } from '@/components/inputs/SearchInput'
|
|||||||
import { defaultChoiceInputOptions } from '@typebot.io/schemas/features/blocks/inputs/choice/constants'
|
import { defaultChoiceInputOptions } from '@typebot.io/schemas/features/blocks/inputs/choice/constants'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
inputIndex: number
|
|
||||||
defaultItems: ChoiceInputBlock['items']
|
defaultItems: ChoiceInputBlock['items']
|
||||||
options: ChoiceInputBlock['options']
|
options: ChoiceInputBlock['options']
|
||||||
onSubmit: (value: InputSubmitContent) => void
|
onSubmit: (value: InputSubmitContent) => void
|
||||||
|
@ -17,7 +17,6 @@ export const NumberInput = (props: NumberInputProps) => {
|
|||||||
const [inputValue, setInputValue] = createSignal<string | number>(
|
const [inputValue, setInputValue] = createSignal<string | number>(
|
||||||
props.defaultValue ?? ''
|
props.defaultValue ?? ''
|
||||||
)
|
)
|
||||||
// eslint-disable-next-line solid/reactivity
|
|
||||||
const [staticValue, bindValue, targetValue] = numberInputHelper(() =>
|
const [staticValue, bindValue, targetValue] = numberInputHelper(() =>
|
||||||
inputValue()
|
inputValue()
|
||||||
)
|
)
|
||||||
|
@ -14,6 +14,11 @@ import { isDefined } from '@typebot.io/lib'
|
|||||||
import { BubbleParams } from '../types'
|
import { BubbleParams } from '../types'
|
||||||
import { Bot, BotProps } from '../../../components/Bot'
|
import { Bot, BotProps } from '../../../components/Bot'
|
||||||
import { getPaymentInProgressInStorage } from '@/features/blocks/inputs/payment/helpers/paymentInProgressStorage'
|
import { getPaymentInProgressInStorage } from '@/features/blocks/inputs/payment/helpers/paymentInProgressStorage'
|
||||||
|
import {
|
||||||
|
getBotOpenedStateFromStorage,
|
||||||
|
removeBotOpenedStateInStorage,
|
||||||
|
setBotOpenedStateInStorage,
|
||||||
|
} from '@/utils/storage'
|
||||||
|
|
||||||
export type BubbleProps = BotProps &
|
export type BubbleProps = BotProps &
|
||||||
BubbleParams & {
|
BubbleParams & {
|
||||||
@ -33,7 +38,6 @@ export const Bubble = (props: BubbleProps) => {
|
|||||||
])
|
])
|
||||||
const [isMounted, setIsMounted] = createSignal(true)
|
const [isMounted, setIsMounted] = createSignal(true)
|
||||||
const [prefilledVariables, setPrefilledVariables] = createSignal(
|
const [prefilledVariables, setPrefilledVariables] = createSignal(
|
||||||
// eslint-disable-next-line solid/reactivity
|
|
||||||
botProps.prefilledVariables
|
botProps.prefilledVariables
|
||||||
)
|
)
|
||||||
const [isPreviewMessageDisplayed, setIsPreviewMessageDisplayed] =
|
const [isPreviewMessageDisplayed, setIsPreviewMessageDisplayed] =
|
||||||
@ -48,7 +52,6 @@ export const Bubble = (props: BubbleProps) => {
|
|||||||
const [isBotOpened, setIsBotOpened] = createSignal(false)
|
const [isBotOpened, setIsBotOpened] = createSignal(false)
|
||||||
const [isBotStarted, setIsBotStarted] = createSignal(false)
|
const [isBotStarted, setIsBotStarted] = createSignal(false)
|
||||||
const [buttonSize, setButtonSize] = createSignal(
|
const [buttonSize, setButtonSize] = createSignal(
|
||||||
// eslint-disable-next-line solid/reactivity
|
|
||||||
parseButtonSize(bubbleProps.theme?.button?.size ?? 'medium')
|
parseButtonSize(bubbleProps.theme?.button?.size ?? 'medium')
|
||||||
)
|
)
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
@ -62,8 +65,8 @@ export const Bubble = (props: BubbleProps) => {
|
|||||||
const autoShowDelay = bubbleProps.autoShowDelay
|
const autoShowDelay = bubbleProps.autoShowDelay
|
||||||
const previewMessageAutoShowDelay =
|
const previewMessageAutoShowDelay =
|
||||||
bubbleProps.previewMessage?.autoShowDelay
|
bubbleProps.previewMessage?.autoShowDelay
|
||||||
const paymentInProgress = getPaymentInProgressInStorage()
|
if (getBotOpenedStateFromStorage() || getPaymentInProgressInStorage())
|
||||||
if (paymentInProgress) openBot()
|
openBot()
|
||||||
if (isDefined(autoShowDelay)) {
|
if (isDefined(autoShowDelay)) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
openBot()
|
openBot()
|
||||||
@ -113,6 +116,7 @@ export const Bubble = (props: BubbleProps) => {
|
|||||||
|
|
||||||
const closeBot = () => {
|
const closeBot = () => {
|
||||||
setIsBotOpened(false)
|
setIsBotOpened(false)
|
||||||
|
removeBotOpenedStateInStorage()
|
||||||
if (isBotOpened()) bubbleProps.onClose?.()
|
if (isBotOpened()) bubbleProps.onClose?.()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -146,6 +150,11 @@ export const Bubble = (props: BubbleProps) => {
|
|||||||
} else setIsMounted(false)
|
} else setIsMounted(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleOnChatStatePersisted = (isPersisted: boolean) => {
|
||||||
|
botProps.onChatStatePersisted?.(isPersisted)
|
||||||
|
if (isPersisted) setBotOpenedStateInStorage()
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Show when={isMounted()}>
|
<Show when={isMounted()}>
|
||||||
<style>{styles}</style>
|
<style>{styles}</style>
|
||||||
@ -195,6 +204,7 @@ export const Bubble = (props: BubbleProps) => {
|
|||||||
<Show when={isBotStarted()}>
|
<Show when={isBotStarted()}>
|
||||||
<Bot
|
<Bot
|
||||||
{...botProps}
|
{...botProps}
|
||||||
|
onChatStatePersisted={handleOnChatStatePersisted}
|
||||||
prefilledVariables={prefilledVariables()}
|
prefilledVariables={prefilledVariables()}
|
||||||
class="rounded-lg"
|
class="rounded-lg"
|
||||||
progressBarRef={progressBarContainerRef}
|
progressBarRef={progressBarContainerRef}
|
||||||
|
@ -12,6 +12,11 @@ import { isDefined, isNotDefined } from '@typebot.io/lib'
|
|||||||
import { PopupParams } from '../types'
|
import { PopupParams } from '../types'
|
||||||
import { Bot, BotProps } from '../../../components/Bot'
|
import { Bot, BotProps } from '../../../components/Bot'
|
||||||
import { getPaymentInProgressInStorage } from '@/features/blocks/inputs/payment/helpers/paymentInProgressStorage'
|
import { getPaymentInProgressInStorage } from '@/features/blocks/inputs/payment/helpers/paymentInProgressStorage'
|
||||||
|
import {
|
||||||
|
getBotOpenedStateFromStorage,
|
||||||
|
removeBotOpenedStateInStorage,
|
||||||
|
setBotOpenedStateInStorage,
|
||||||
|
} from '@/utils/storage'
|
||||||
|
|
||||||
export type PopupProps = BotProps &
|
export type PopupProps = BotProps &
|
||||||
PopupParams & {
|
PopupParams & {
|
||||||
@ -32,18 +37,18 @@ export const Popup = (props: PopupProps) => {
|
|||||||
])
|
])
|
||||||
|
|
||||||
const [prefilledVariables, setPrefilledVariables] = createSignal(
|
const [prefilledVariables, setPrefilledVariables] = createSignal(
|
||||||
// eslint-disable-next-line solid/reactivity
|
|
||||||
botProps.prefilledVariables
|
botProps.prefilledVariables
|
||||||
)
|
)
|
||||||
|
|
||||||
const [isBotOpened, setIsBotOpened] = createSignal(
|
const [isBotOpened, setIsBotOpened] = createSignal(popupProps.isOpen ?? false)
|
||||||
// eslint-disable-next-line solid/reactivity
|
|
||||||
popupProps.isOpen ?? false
|
|
||||||
)
|
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
const paymentInProgress = getPaymentInProgressInStorage()
|
if (
|
||||||
if (popupProps.defaultOpen || paymentInProgress) openBot()
|
popupProps.defaultOpen ||
|
||||||
|
getPaymentInProgressInStorage() ||
|
||||||
|
getBotOpenedStateFromStorage()
|
||||||
|
)
|
||||||
|
openBot()
|
||||||
window.addEventListener('message', processIncomingEvent)
|
window.addEventListener('message', processIncomingEvent)
|
||||||
const autoShowDelay = popupProps.autoShowDelay
|
const autoShowDelay = popupProps.autoShowDelay
|
||||||
if (isDefined(autoShowDelay)) {
|
if (isDefined(autoShowDelay)) {
|
||||||
@ -99,12 +104,18 @@ export const Popup = (props: PopupProps) => {
|
|||||||
popupProps.onClose?.()
|
popupProps.onClose?.()
|
||||||
document.body.style.overflow = 'auto'
|
document.body.style.overflow = 'auto'
|
||||||
document.removeEventListener('pointerdown', closeBot)
|
document.removeEventListener('pointerdown', closeBot)
|
||||||
|
removeBotOpenedStateInStorage()
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggleBot = () => {
|
const toggleBot = () => {
|
||||||
isBotOpened() ? closeBot() : openBot()
|
isBotOpened() ? closeBot() : openBot()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleOnChatStatePersisted = (isPersisted: boolean) => {
|
||||||
|
botProps.onChatStatePersisted?.(isPersisted)
|
||||||
|
if (isPersisted) setBotOpenedStateInStorage()
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Show when={isBotOpened()}>
|
<Show when={isBotOpened()}>
|
||||||
<style>{styles}</style>
|
<style>{styles}</style>
|
||||||
@ -136,7 +147,11 @@ export const Popup = (props: PopupProps) => {
|
|||||||
}}
|
}}
|
||||||
on:pointerdown={stopPropagation}
|
on:pointerdown={stopPropagation}
|
||||||
>
|
>
|
||||||
<Bot {...botProps} prefilledVariables={prefilledVariables()} />
|
<Bot
|
||||||
|
{...botProps}
|
||||||
|
prefilledVariables={prefilledVariables()}
|
||||||
|
onChatStatePersisted={handleOnChatStatePersisted}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -11,6 +11,7 @@ export type BotContext = {
|
|||||||
isPreview: boolean
|
isPreview: boolean
|
||||||
apiHost?: string
|
apiHost?: string
|
||||||
sessionId: string
|
sessionId: string
|
||||||
|
storage: 'local' | 'session' | undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
export type InitialChatReply = StartChatResponse & {
|
export type InitialChatReply = StartChatResponse & {
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
/* eslint-disable solid/reactivity */
|
|
||||||
import { initGoogleAnalytics } from '@/lib/gtag'
|
import { initGoogleAnalytics } from '@/lib/gtag'
|
||||||
import { gtmBodyElement } from '@/lib/gtm'
|
import { gtmBodyElement } from '@/lib/gtm'
|
||||||
import { initPixel } from '@/lib/pixel'
|
import { initPixel } from '@/lib/pixel'
|
||||||
|
54
packages/embeds/js/src/utils/persist.ts
Normal file
54
packages/embeds/js/src/utils/persist.ts
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
// Copied from https://github.com/solidjs-community/solid-primitives/blob/main/packages/storage/src/types.ts
|
||||||
|
// Simplifying and adding a `isEnabled` prop
|
||||||
|
|
||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
import { defaultSettings } from '@typebot.io/schemas/features/typebot/settings/constants'
|
||||||
|
import type { Setter, Signal } from 'solid-js'
|
||||||
|
import { untrack } from 'solid-js'
|
||||||
|
import { reconcile } from 'solid-js/store'
|
||||||
|
|
||||||
|
type Params = {
|
||||||
|
key: string
|
||||||
|
storage: 'local' | 'session' | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export function persist<T>(signal: Signal<T>, params: Params): Signal<T> {
|
||||||
|
if (!params.storage) return signal
|
||||||
|
|
||||||
|
const storage = parseRememberUserStorage(
|
||||||
|
params.storage || defaultSettings.general.rememberUser.storage
|
||||||
|
)
|
||||||
|
const serialize: (data: T) => string = JSON.stringify.bind(JSON)
|
||||||
|
const deserialize: (data: string) => T = JSON.parse.bind(JSON)
|
||||||
|
const init = storage.getItem(params.key)
|
||||||
|
const set =
|
||||||
|
typeof signal[0] === 'function'
|
||||||
|
? (data: string) => (signal[1] as any)(() => deserialize(data))
|
||||||
|
: (data: string) => (signal[1] as any)(reconcile(deserialize(data)))
|
||||||
|
|
||||||
|
if (init) set(init)
|
||||||
|
|
||||||
|
return [
|
||||||
|
signal[0],
|
||||||
|
typeof signal[0] === 'function'
|
||||||
|
? (value?: T | ((prev: T) => T)) => {
|
||||||
|
const output = (signal[1] as Setter<T>)(value as any)
|
||||||
|
|
||||||
|
if (value) storage.setItem(params.key, serialize(output))
|
||||||
|
else storage.removeItem(params.key)
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
: (...args: any[]) => {
|
||||||
|
;(signal[1] as any)(...args)
|
||||||
|
const value = serialize(untrack(() => signal[0] as any))
|
||||||
|
storage.setItem(params.key, value)
|
||||||
|
},
|
||||||
|
] as typeof signal
|
||||||
|
}
|
||||||
|
|
||||||
|
const parseRememberUserStorage = (
|
||||||
|
storage: 'local' | 'session' | undefined
|
||||||
|
): typeof localStorage | typeof sessionStorage =>
|
||||||
|
(storage ?? defaultSettings.general.rememberUser.storage) === 'session'
|
||||||
|
? sessionStorage
|
||||||
|
: localStorage
|
@ -1,11 +1,14 @@
|
|||||||
const sessionStorageKey = 'resultId'
|
import { InitialChatReply } from '@/types'
|
||||||
|
import { defaultSettings } from '@typebot.io/schemas/features/typebot/settings/constants'
|
||||||
|
|
||||||
|
const storageResultIdKey = 'resultId'
|
||||||
|
|
||||||
export const getExistingResultIdFromStorage = (typebotId?: string) => {
|
export const getExistingResultIdFromStorage = (typebotId?: string) => {
|
||||||
if (!typebotId) return
|
if (!typebotId) return
|
||||||
try {
|
try {
|
||||||
return (
|
return (
|
||||||
sessionStorage.getItem(`${sessionStorageKey}-${typebotId}`) ??
|
sessionStorage.getItem(`${storageResultIdKey}-${typebotId}`) ??
|
||||||
localStorage.getItem(`${sessionStorageKey}-${typebotId}`) ??
|
localStorage.getItem(`${storageResultIdKey}-${typebotId}`) ??
|
||||||
undefined
|
undefined
|
||||||
)
|
)
|
||||||
} catch {
|
} catch {
|
||||||
@ -17,13 +20,86 @@ export const setResultInStorage =
|
|||||||
(storageType: 'local' | 'session' = 'session') =>
|
(storageType: 'local' | 'session' = 'session') =>
|
||||||
(typebotId: string, resultId: string) => {
|
(typebotId: string, resultId: string) => {
|
||||||
try {
|
try {
|
||||||
;(storageType === 'session' ? localStorage : sessionStorage).removeItem(
|
parseRememberUserStorage(storageType).setItem(
|
||||||
`${sessionStorageKey}-${typebotId}`
|
`${storageResultIdKey}-${typebotId}`,
|
||||||
|
resultId
|
||||||
)
|
)
|
||||||
return (
|
|
||||||
storageType === 'session' ? sessionStorage : localStorage
|
|
||||||
).setItem(`${sessionStorageKey}-${typebotId}`, resultId)
|
|
||||||
} catch {
|
} catch {
|
||||||
/* empty */
|
/* empty */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getInitialChatReplyFromStorage = (
|
||||||
|
typebotId: string | undefined
|
||||||
|
) => {
|
||||||
|
if (!typebotId) return
|
||||||
|
try {
|
||||||
|
const rawInitialChatReply =
|
||||||
|
sessionStorage.getItem(`typebot-${typebotId}-initialChatReply`) ??
|
||||||
|
localStorage.getItem(`typebot-${typebotId}-initialChatReply`)
|
||||||
|
if (!rawInitialChatReply) return
|
||||||
|
return JSON.parse(rawInitialChatReply) as InitialChatReply
|
||||||
|
} catch {
|
||||||
|
/* empty */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export const setInitialChatReplyInStorage = (
|
||||||
|
initialChatReply: InitialChatReply,
|
||||||
|
{
|
||||||
|
typebotId,
|
||||||
|
storage,
|
||||||
|
}: {
|
||||||
|
typebotId: string
|
||||||
|
storage?: 'local' | 'session'
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const rawInitialChatReply = JSON.stringify(initialChatReply)
|
||||||
|
parseRememberUserStorage(storage).setItem(
|
||||||
|
`typebot-${typebotId}-initialChatReply`,
|
||||||
|
rawInitialChatReply
|
||||||
|
)
|
||||||
|
} catch {
|
||||||
|
/* empty */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const setBotOpenedStateInStorage = () => {
|
||||||
|
try {
|
||||||
|
sessionStorage.setItem(`typebot-botOpened`, 'true')
|
||||||
|
} catch {
|
||||||
|
/* empty */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const removeBotOpenedStateInStorage = () => {
|
||||||
|
try {
|
||||||
|
sessionStorage.removeItem(`typebot-botOpened`)
|
||||||
|
} catch {
|
||||||
|
/* empty */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getBotOpenedStateFromStorage = () => {
|
||||||
|
try {
|
||||||
|
return sessionStorage.getItem(`typebot-botOpened`) === 'true'
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const parseRememberUserStorage = (
|
||||||
|
storage: 'local' | 'session' | undefined
|
||||||
|
): typeof localStorage | typeof sessionStorage =>
|
||||||
|
(storage ?? defaultSettings.general.rememberUser.storage) === 'session'
|
||||||
|
? sessionStorage
|
||||||
|
: localStorage
|
||||||
|
|
||||||
|
export const wipeExistingChatStateInStorage = (typebotId: string) => {
|
||||||
|
Object.keys(localStorage).forEach((key) => {
|
||||||
|
if (key.startsWith(`typebot-${typebotId}`)) localStorage.removeItem(key)
|
||||||
|
})
|
||||||
|
Object.keys(sessionStorage).forEach((key) => {
|
||||||
|
if (key.startsWith(`typebot-${typebotId}`)) sessionStorage.removeItem(key)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
/* eslint-disable solid/reactivity */
|
|
||||||
import { BubbleProps } from './features/bubble'
|
import { BubbleProps } from './features/bubble'
|
||||||
import { PopupProps } from './features/popup'
|
import { PopupProps } from './features/popup'
|
||||||
import { BotProps } from './components/Bot'
|
import { BotProps } from './components/Bot'
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@typebot.io/nextjs",
|
"name": "@typebot.io/nextjs",
|
||||||
"version": "0.2.48",
|
"version": "0.2.49",
|
||||||
"description": "Convenient library to display typebots on your Next.js website",
|
"description": "Convenient library to display typebots on your Next.js website",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@typebot.io/react",
|
"name": "@typebot.io/react",
|
||||||
"version": "0.2.48",
|
"version": "0.2.49",
|
||||||
"description": "Convenient library to display typebots on your React app",
|
"description": "Convenient library to display typebots on your React app",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
|
Reference in New Issue
Block a user