⚡ 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
|
||||
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={
|
||||
generalSettings?.rememberUser?.isEnabled ??
|
||||
(isDefined(generalSettings?.isNewResultOnRefreshEnabled)
|
||||
@ -112,7 +112,7 @@ export const GeneralSettingsForm = ({
|
||||
<Tag size="sm" bgColor={keyBg}>
|
||||
local
|
||||
</Tag>{' '}
|
||||
to remember the user forever.
|
||||
to remember the user forever on the same device.
|
||||
</Text>
|
||||
</Stack>
|
||||
</MoreInfoTooltip>
|
||||
|
@ -142,7 +142,7 @@
|
||||
},
|
||||
{
|
||||
"group": "Settings",
|
||||
"pages": ["settings/overview"]
|
||||
"pages": ["settings/overview", "settings/remember-user"]
|
||||
},
|
||||
{
|
||||
"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.
|
||||
- **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
|
||||
|
||||
|
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) =>
|
||||
prisma.result.findFirst({
|
||||
where: { id },
|
||||
where: { id, isArchived: { not: true } },
|
||||
select: {
|
||||
id: true,
|
||||
variables: true,
|
||||
|
@ -6,5 +6,6 @@ module.exports = {
|
||||
'@next/next/no-img-element': 'off',
|
||||
'@next/next/no-html-link-for-pages': 'off',
|
||||
'solid/no-innerhtml': 'off',
|
||||
'solid/reactivity': 'off',
|
||||
},
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@typebot.io/js",
|
||||
"version": "0.2.48",
|
||||
"version": "0.2.49",
|
||||
"description": "Javascript library to display typebots on your website",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
|
@ -8,7 +8,10 @@ import { BotContext, InitialChatReply, OutgoingLog } from '@/types'
|
||||
import { ErrorMessage } from './ErrorMessage'
|
||||
import {
|
||||
getExistingResultIdFromStorage,
|
||||
getInitialChatReplyFromStorage,
|
||||
setInitialChatReplyInStorage,
|
||||
setResultInStorage,
|
||||
wipeExistingChatStateInStorage,
|
||||
} from '@/utils/storage'
|
||||
import { setCssVariablesValue } from '@/utils/setCssVariablesValue'
|
||||
import immutableCss from '../assets/immutable.css'
|
||||
@ -20,6 +23,8 @@ import { HTTPError } from 'ky'
|
||||
import { injectFont } from '@/utils/injectFont'
|
||||
import { ProgressBar } from './ProgressBar'
|
||||
import { Portal } from 'solid-js/web'
|
||||
import { defaultSettings } from '@typebot.io/schemas/features/typebot/settings/constants'
|
||||
import { persist } from '@/utils/persist'
|
||||
|
||||
export type BotProps = {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
@ -35,6 +40,7 @@ export type BotProps = {
|
||||
onInit?: () => void
|
||||
onEnd?: () => void
|
||||
onNewLogs?: (logs: OutgoingLog[]) => void
|
||||
onChatStatePersisted?: (isEnabled: boolean) => void
|
||||
startFrom?: StartFrom
|
||||
}
|
||||
|
||||
@ -59,14 +65,13 @@ export const Bot = (props: BotProps & { class?: string }) => {
|
||||
typeof props.typebot === 'string' ? props.typebot : undefined
|
||||
const isPreview =
|
||||
typeof props.typebot !== 'string' || (props.isPreview ?? false)
|
||||
const resultIdInStorage = getExistingResultIdFromStorage(typebotIdFromProps)
|
||||
const { data, error } = await startChatQuery({
|
||||
stripeRedirectStatus: urlParams.get('redirect_status') ?? undefined,
|
||||
typebot: props.typebot,
|
||||
apiHost: props.apiHost,
|
||||
isPreview,
|
||||
resultId: isNotEmpty(props.resultId)
|
||||
? props.resultId
|
||||
: getExistingResultIdFromStorage(typebotIdFromProps),
|
||||
resultId: isNotEmpty(props.resultId) ? props.resultId : resultIdInStorage,
|
||||
prefilledVariables: {
|
||||
...prefilledVariables,
|
||||
...props.prefilledVariables,
|
||||
@ -111,17 +116,40 @@ export const Bot = (props: BotProps & { class?: string }) => {
|
||||
)
|
||||
}
|
||||
|
||||
if (data.resultId && typebotIdFromProps)
|
||||
setResultInStorage(data.typebot.settings.general?.rememberUser?.storage)(
|
||||
typebotIdFromProps,
|
||||
data.resultId
|
||||
if (
|
||||
data.resultId &&
|
||||
typebotIdFromProps &&
|
||||
(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
|
||||
)
|
||||
setInitialChatReply(data)
|
||||
setCustomCss(data.typebot.theme.customCss ?? '')
|
||||
if (initialChatInStorage) {
|
||||
setInitialChatReply(initialChatInStorage)
|
||||
} else {
|
||||
setInitialChatReply(data)
|
||||
setInitialChatReplyInStorage(data, {
|
||||
typebotId: data.typebot.id,
|
||||
storage,
|
||||
})
|
||||
}
|
||||
props.onChatStatePersisted?.(true)
|
||||
} else {
|
||||
setInitialChatReply(data)
|
||||
if (data.input?.id && props.onNewInputBlock)
|
||||
props.onNewInputBlock(data.input)
|
||||
if (data.logs) props.onNewLogs?.(data.logs)
|
||||
props.onChatStatePersisted?.(false)
|
||||
}
|
||||
|
||||
if (data.input?.id && props.onNewInputBlock)
|
||||
props.onNewInputBlock(data.input)
|
||||
if (data.logs) props.onNewLogs?.(data.logs)
|
||||
setCustomCss(data.typebot.theme.customCss ?? '')
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
@ -178,6 +206,16 @@ export const Bot = (props: BotProps & { class?: string }) => {
|
||||
resultId: initialChatReply.resultId,
|
||||
sessionId: initialChatReply.sessionId,
|
||||
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}
|
||||
onNewInputBlock={props.onNewInputBlock}
|
||||
@ -203,8 +241,12 @@ type BotContentProps = {
|
||||
}
|
||||
|
||||
const BotContent = (props: BotContentProps) => {
|
||||
const [progressValue, setProgressValue] = createSignal<number | undefined>(
|
||||
props.initialChatReply.progress
|
||||
const [progressValue, setProgressValue] = persist(
|
||||
createSignal<number | undefined>(props.initialChatReply.progress),
|
||||
{
|
||||
storage: props.context.storage,
|
||||
key: `typebot-${props.context.typebot.id}-progressValue`,
|
||||
}
|
||||
)
|
||||
let botContainer: HTMLDivElement | undefined
|
||||
|
||||
|
@ -2,7 +2,11 @@ import { createSignal, onCleanup, onMount } from 'solid-js'
|
||||
import { isMobile } from '@/utils/isMobileSignal'
|
||||
import { Avatar } from '../avatars/Avatar'
|
||||
|
||||
type Props = { hostAvatarSrc?: string; hideAvatar?: boolean }
|
||||
type Props = {
|
||||
hostAvatarSrc?: string
|
||||
hideAvatar?: boolean
|
||||
isTransitionDisabled?: boolean
|
||||
}
|
||||
|
||||
export const AvatarSideContainer = (props: Props) => {
|
||||
let avatarContainer: HTMLDivElement | undefined
|
||||
@ -40,7 +44,9 @@ export const AvatarSideContainer = (props: Props) => {
|
||||
}
|
||||
style={{
|
||||
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} />
|
||||
|
@ -12,11 +12,12 @@ import { defaultSettings } from '@typebot.io/schemas/features/typebot/settings/c
|
||||
type Props = Pick<ContinueChatResponse, 'messages' | 'input'> & {
|
||||
theme: Theme
|
||||
settings: Settings
|
||||
inputIndex: number
|
||||
index: number
|
||||
context: BotContext
|
||||
hasError: boolean
|
||||
hideAvatar: boolean
|
||||
streamingMessageId: ChatChunkType['streamingMessageId']
|
||||
isTransitionDisabled?: boolean
|
||||
onNewBubbleDisplayed: (blockId: string) => Promise<void>
|
||||
onScrollToBottom: (top?: number) => void
|
||||
onSubmit: (input?: string) => void
|
||||
@ -26,7 +27,9 @@ type Props = Pick<ContinueChatResponse, 'messages' | 'input'> & {
|
||||
|
||||
export const ChatChunk = (props: Props) => {
|
||||
let inputRef: HTMLDivElement | undefined
|
||||
const [displayedMessageIndex, setDisplayedMessageIndex] = createSignal(0)
|
||||
const [displayedMessageIndex, setDisplayedMessageIndex] = createSignal(
|
||||
props.isTransitionDisabled ? props.messages.length : 0
|
||||
)
|
||||
const [lastBubbleOffsetTop, setLastBubbleOffsetTop] = createSignal<number>()
|
||||
|
||||
onMount(() => {
|
||||
@ -45,7 +48,6 @@ export const ChatChunk = (props: Props) => {
|
||||
defaultSettings.typingEmulation.delayBetweenBubbles) > 0 &&
|
||||
displayedMessageIndex() < props.messages.length - 1
|
||||
) {
|
||||
// eslint-disable-next-line solid/reactivity
|
||||
await new Promise((resolve) =>
|
||||
setTimeout(
|
||||
resolve,
|
||||
@ -82,6 +84,7 @@ export const ChatChunk = (props: Props) => {
|
||||
<AvatarSideContainer
|
||||
hostAvatarSrc={props.theme.chat?.hostAvatar?.url}
|
||||
hideAvatar={props.hideAvatar}
|
||||
isTransitionDisabled={props.isTransitionDisabled}
|
||||
/>
|
||||
</Show>
|
||||
|
||||
@ -108,10 +111,12 @@ export const ChatChunk = (props: Props) => {
|
||||
(props.settings.typingEmulation?.isDisabledOnFirstMessage ??
|
||||
defaultSettings.typingEmulation
|
||||
.isDisabledOnFirstMessage) &&
|
||||
props.inputIndex === 0 &&
|
||||
props.index === 0 &&
|
||||
idx() === 0
|
||||
}
|
||||
onTransitionEnd={displayNextMessage}
|
||||
onTransitionEnd={
|
||||
props.isTransitionDisabled ? undefined : displayNextMessage
|
||||
}
|
||||
onCompleted={props.onSubmit}
|
||||
/>
|
||||
)}
|
||||
@ -123,7 +128,7 @@ export const ChatChunk = (props: Props) => {
|
||||
<InputChatBlock
|
||||
ref={inputRef}
|
||||
block={props.input}
|
||||
inputIndex={props.inputIndex}
|
||||
chunkIndex={props.index}
|
||||
hasHostAvatar={
|
||||
props.theme.chat?.hostAvatar?.isEnabled ??
|
||||
defaultTheme.chat.hostAvatar.isEnabled
|
||||
|
@ -32,6 +32,7 @@ import {
|
||||
import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants'
|
||||
import { saveClientLogsQuery } from '@/queries/saveClientLogsQuery'
|
||||
import { HTTPError } from 'ky'
|
||||
import { persist } from '@/utils/persist'
|
||||
|
||||
const parseDynamicTheme = (
|
||||
initialTheme: Theme,
|
||||
@ -69,13 +70,19 @@ type Props = {
|
||||
|
||||
export const ConversationContainer = (props: Props) => {
|
||||
let chatContainer: HTMLDivElement | undefined
|
||||
const [chatChunks, setChatChunks] = createSignal<ChatChunkType[]>([
|
||||
const [chatChunks, setChatChunks] = persist(
|
||||
createSignal<ChatChunkType[]>([
|
||||
{
|
||||
input: props.initialChatReply.input,
|
||||
messages: props.initialChatReply.messages,
|
||||
clientSideActions: props.initialChatReply.clientSideActions,
|
||||
},
|
||||
]),
|
||||
{
|
||||
input: props.initialChatReply.input,
|
||||
messages: props.initialChatReply.messages,
|
||||
clientSideActions: props.initialChatReply.clientSideActions,
|
||||
},
|
||||
])
|
||||
key: `typebot-${props.context.typebot.id}-chatChunks`,
|
||||
storage: props.context.storage,
|
||||
}
|
||||
)
|
||||
const [dynamicTheme, setDynamicTheme] = createSignal<
|
||||
ContinueChatResponse['dynamicTheme']
|
||||
>(props.initialChatReply.dynamicTheme)
|
||||
@ -276,7 +283,7 @@ export const ConversationContainer = (props: Props) => {
|
||||
<For each={chatChunks()}>
|
||||
{(chatChunk, index) => (
|
||||
<ChatChunk
|
||||
inputIndex={index()}
|
||||
index={index()}
|
||||
messages={chatChunk.messages}
|
||||
input={chatChunk.input}
|
||||
theme={theme()}
|
||||
@ -290,6 +297,7 @@ export const ConversationContainer = (props: Props) => {
|
||||
(chatChunk.messages.length > 0 && isSending()))
|
||||
}
|
||||
hasError={hasError() && index() === chatChunks().length - 1}
|
||||
isTransitionDisabled={index() !== chatChunks().length - 1}
|
||||
onNewBubbleDisplayed={handleNewBubbleDisplayed}
|
||||
onAllBubblesDisplayed={handleAllBubblesDisplayed}
|
||||
onSubmit={sendMessage}
|
||||
|
@ -36,13 +36,14 @@ import { formattedMessages } from '@/utils/formattedMessagesSignal'
|
||||
import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants'
|
||||
import { defaultPaymentInputOptions } from '@typebot.io/schemas/features/blocks/inputs/payment/constants'
|
||||
import { defaultTheme } from '@typebot.io/schemas/features/typebot/theme/constants'
|
||||
import { persist } from '@/utils/persist'
|
||||
|
||||
type Props = {
|
||||
ref: HTMLDivElement | undefined
|
||||
block: NonNullable<ContinueChatResponse['input']>
|
||||
hasHostAvatar: boolean
|
||||
guestAvatar?: NonNullable<Theme['chat']>['guestAvatar']
|
||||
inputIndex: number
|
||||
chunkIndex: number
|
||||
context: BotContext
|
||||
isInputPrefillEnabled: boolean
|
||||
hasError: boolean
|
||||
@ -52,7 +53,10 @@ type 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 handleSubmit = async ({ label, value }: InputSubmitContent) => {
|
||||
@ -67,7 +71,7 @@ export const InputChatBlock = (props: Props) => {
|
||||
|
||||
createEffect(() => {
|
||||
const formattedMessage = formattedMessages().findLast(
|
||||
(message) => props.inputIndex === message.inputIndex
|
||||
(message) => props.chunkIndex === message.inputIndex
|
||||
)?.formattedMessage
|
||||
if (formattedMessage) setFormattedMessage(formattedMessage)
|
||||
})
|
||||
@ -101,7 +105,7 @@ export const InputChatBlock = (props: Props) => {
|
||||
<Input
|
||||
context={props.context}
|
||||
block={props.block}
|
||||
inputIndex={props.inputIndex}
|
||||
chunkIndex={props.chunkIndex}
|
||||
isInputPrefillEnabled={props.isInputPrefillEnabled}
|
||||
existingAnswer={props.hasError ? answer() : undefined}
|
||||
onTransitionEnd={props.onTransitionEnd}
|
||||
@ -117,7 +121,7 @@ export const InputChatBlock = (props: Props) => {
|
||||
const Input = (props: {
|
||||
context: BotContext
|
||||
block: NonNullable<ContinueChatResponse['input']>
|
||||
inputIndex: number
|
||||
chunkIndex: number
|
||||
isInputPrefillEnabled: boolean
|
||||
existingAnswer?: string
|
||||
onTransitionEnd: () => void
|
||||
@ -189,7 +193,7 @@ const Input = (props: {
|
||||
<Switch>
|
||||
<Match when={!block.options?.isMultipleChoice}>
|
||||
<Buttons
|
||||
inputIndex={props.inputIndex}
|
||||
chunkIndex={props.chunkIndex}
|
||||
defaultItems={block.items}
|
||||
options={block.options}
|
||||
onSubmit={onSubmit}
|
||||
@ -197,7 +201,6 @@ const Input = (props: {
|
||||
</Match>
|
||||
<Match when={block.options?.isMultipleChoice}>
|
||||
<MultipleChoicesForm
|
||||
inputIndex={props.inputIndex}
|
||||
defaultItems={block.items}
|
||||
options={block.options}
|
||||
onSubmit={onSubmit}
|
||||
|
@ -21,60 +21,50 @@ type Props = {
|
||||
message: ChatMessage
|
||||
typingEmulation: Settings['typingEmulation']
|
||||
isTypingSkipped: boolean
|
||||
onTransitionEnd: (offsetTop?: number) => void
|
||||
onTransitionEnd?: (offsetTop?: number) => void
|
||||
onCompleted: (reply?: string) => void
|
||||
}
|
||||
|
||||
export const HostBubble = (props: Props) => {
|
||||
const onTransitionEnd = (offsetTop?: number) => {
|
||||
props.onTransitionEnd(offsetTop)
|
||||
}
|
||||
|
||||
const onCompleted = (reply?: string) => {
|
||||
props.onCompleted(reply)
|
||||
}
|
||||
|
||||
return (
|
||||
<Switch>
|
||||
<Match when={props.message.type === BubbleBlockType.TEXT}>
|
||||
<TextBubble
|
||||
content={props.message.content as TextBubbleBlock['content']}
|
||||
isTypingSkipped={props.isTypingSkipped}
|
||||
typingEmulation={props.typingEmulation}
|
||||
onTransitionEnd={onTransitionEnd}
|
||||
/>
|
||||
</Match>
|
||||
<Match when={props.message.type === BubbleBlockType.IMAGE}>
|
||||
<ImageBubble
|
||||
content={props.message.content as ImageBubbleBlock['content']}
|
||||
onTransitionEnd={onTransitionEnd}
|
||||
/>
|
||||
</Match>
|
||||
<Match when={props.message.type === BubbleBlockType.VIDEO}>
|
||||
<VideoBubble
|
||||
content={props.message.content as VideoBubbleBlock['content']}
|
||||
onTransitionEnd={onTransitionEnd}
|
||||
/>
|
||||
</Match>
|
||||
<Match when={props.message.type === BubbleBlockType.EMBED}>
|
||||
<EmbedBubble
|
||||
content={props.message.content as EmbedBubbleBlock['content']}
|
||||
onTransitionEnd={onTransitionEnd}
|
||||
/>
|
||||
</Match>
|
||||
<Match when={props.message.type === 'custom-embed'}>
|
||||
<CustomEmbedBubble
|
||||
content={props.message.content as CustomEmbedBubbleProps['content']}
|
||||
onTransitionEnd={onTransitionEnd}
|
||||
onCompleted={onCompleted}
|
||||
/>
|
||||
</Match>
|
||||
<Match when={props.message.type === BubbleBlockType.AUDIO}>
|
||||
<AudioBubble
|
||||
content={props.message.content as AudioBubbleBlock['content']}
|
||||
onTransitionEnd={onTransitionEnd}
|
||||
/>
|
||||
</Match>
|
||||
</Switch>
|
||||
)
|
||||
}
|
||||
export const HostBubble = (props: Props) => (
|
||||
<Switch>
|
||||
<Match when={props.message.type === BubbleBlockType.TEXT}>
|
||||
<TextBubble
|
||||
content={props.message.content as TextBubbleBlock['content']}
|
||||
isTypingSkipped={props.isTypingSkipped}
|
||||
typingEmulation={props.typingEmulation}
|
||||
onTransitionEnd={props.onTransitionEnd}
|
||||
/>
|
||||
</Match>
|
||||
<Match when={props.message.type === BubbleBlockType.IMAGE}>
|
||||
<ImageBubble
|
||||
content={props.message.content as ImageBubbleBlock['content']}
|
||||
onTransitionEnd={props.onTransitionEnd}
|
||||
/>
|
||||
</Match>
|
||||
<Match when={props.message.type === BubbleBlockType.VIDEO}>
|
||||
<VideoBubble
|
||||
content={props.message.content as VideoBubbleBlock['content']}
|
||||
onTransitionEnd={props.onTransitionEnd}
|
||||
/>
|
||||
</Match>
|
||||
<Match when={props.message.type === BubbleBlockType.EMBED}>
|
||||
<EmbedBubble
|
||||
content={props.message.content as EmbedBubbleBlock['content']}
|
||||
onTransitionEnd={props.onTransitionEnd}
|
||||
/>
|
||||
</Match>
|
||||
<Match when={props.message.type === 'custom-embed'}>
|
||||
<CustomEmbedBubble
|
||||
content={props.message.content as CustomEmbedBubbleProps['content']}
|
||||
onTransitionEnd={props.onTransitionEnd}
|
||||
onCompleted={props.onCompleted}
|
||||
/>
|
||||
</Match>
|
||||
<Match when={props.message.type === BubbleBlockType.AUDIO}>
|
||||
<AudioBubble
|
||||
content={props.message.content as AudioBubbleBlock['content']}
|
||||
onTransitionEnd={props.onTransitionEnd}
|
||||
/>
|
||||
</Match>
|
||||
</Switch>
|
||||
)
|
||||
|
@ -3,10 +3,11 @@ import { isMobile } from '@/utils/isMobileSignal'
|
||||
import { AudioBubbleBlock } from '@typebot.io/schemas'
|
||||
import { createSignal, onCleanup, onMount } from 'solid-js'
|
||||
import { defaultAudioBubbleContent } from '@typebot.io/schemas/features/blocks/bubbles/audio/constants'
|
||||
import clsx from 'clsx'
|
||||
|
||||
type Props = {
|
||||
content: AudioBubbleBlock['content']
|
||||
onTransitionEnd: (offsetTop?: number) => void
|
||||
onTransitionEnd?: (offsetTop?: number) => void
|
||||
}
|
||||
|
||||
const showAnimationDuration = 400
|
||||
@ -18,7 +19,9 @@ export const AudioBubble = (props: Props) => {
|
||||
let isPlayed = false
|
||||
let ref: HTMLDivElement | undefined
|
||||
let audioElement: HTMLAudioElement | undefined
|
||||
const [isTyping, setIsTyping] = createSignal(true)
|
||||
const [isTyping, setIsTyping] = createSignal(
|
||||
props.onTransitionEnd ? true : false
|
||||
)
|
||||
|
||||
onMount(() => {
|
||||
typingTimeout = setTimeout(() => {
|
||||
@ -26,7 +29,7 @@ export const AudioBubble = (props: Props) => {
|
||||
isPlayed = true
|
||||
setIsTyping(false)
|
||||
setTimeout(
|
||||
() => props.onTransitionEnd(ref?.offsetTop),
|
||||
() => props.onTransitionEnd?.(ref?.offsetTop),
|
||||
showAnimationDuration
|
||||
)
|
||||
}, typingDuration)
|
||||
@ -37,7 +40,13 @@ export const AudioBubble = (props: Props) => {
|
||||
})
|
||||
|
||||
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 relative z-10 items-start typebot-host-bubble max-w-full">
|
||||
<div
|
||||
@ -53,8 +62,10 @@ export const AudioBubble = (props: Props) => {
|
||||
ref={audioElement}
|
||||
src={props.content?.url}
|
||||
autoplay={
|
||||
props.content?.isAutoplayEnabled ??
|
||||
defaultAudioBubbleContent.isAutoplayEnabled
|
||||
props.onTransitionEnd
|
||||
? props.content?.isAutoplayEnabled ??
|
||||
defaultAudioBubbleContent.isAutoplayEnabled
|
||||
: false
|
||||
}
|
||||
class={
|
||||
'z-10 text-fade-in ' +
|
||||
|
@ -7,7 +7,7 @@ import { executeCode } from '@/features/blocks/logic/script/executeScript'
|
||||
|
||||
type Props = {
|
||||
content: CustomEmbedBubbleProps['content']
|
||||
onTransitionEnd: (offsetTop?: number) => void
|
||||
onTransitionEnd?: (offsetTop?: number) => void
|
||||
onCompleted: (reply?: string) => void
|
||||
}
|
||||
|
||||
@ -17,7 +17,9 @@ export const showAnimationDuration = 400
|
||||
|
||||
export const CustomEmbedBubble = (props: Props) => {
|
||||
let ref: HTMLDivElement | undefined
|
||||
const [isTyping, setIsTyping] = createSignal(true)
|
||||
const [isTyping, setIsTyping] = createSignal(
|
||||
props.onTransitionEnd ? true : false
|
||||
)
|
||||
let containerRef: HTMLDivElement | undefined
|
||||
|
||||
onMount(() => {
|
||||
@ -41,7 +43,7 @@ export const CustomEmbedBubble = (props: Props) => {
|
||||
typingTimeout = setTimeout(() => {
|
||||
setIsTyping(false)
|
||||
setTimeout(
|
||||
() => props.onTransitionEnd(ref?.offsetTop),
|
||||
() => props.onTransitionEnd?.(ref?.offsetTop),
|
||||
showAnimationDuration
|
||||
)
|
||||
}, 2000)
|
||||
@ -52,7 +54,13 @@ export const CustomEmbedBubble = (props: Props) => {
|
||||
})
|
||||
|
||||
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 relative z-10 items-start typebot-host-bubble w-full max-w-full">
|
||||
<div
|
||||
|
@ -7,7 +7,7 @@ import { defaultEmbedBubbleContent } from '@typebot.io/schemas/features/blocks/b
|
||||
|
||||
type Props = {
|
||||
content: EmbedBubbleBlock['content']
|
||||
onTransitionEnd: (offsetTop?: number) => void
|
||||
onTransitionEnd?: (offsetTop?: number) => void
|
||||
}
|
||||
|
||||
let typingTimeout: NodeJS.Timeout
|
||||
@ -16,13 +16,15 @@ export const showAnimationDuration = 400
|
||||
|
||||
export const EmbedBubble = (props: Props) => {
|
||||
let ref: HTMLDivElement | undefined
|
||||
const [isTyping, setIsTyping] = createSignal(true)
|
||||
const [isTyping, setIsTyping] = createSignal(
|
||||
props.onTransitionEnd ? true : false
|
||||
)
|
||||
|
||||
onMount(() => {
|
||||
typingTimeout = setTimeout(() => {
|
||||
setIsTyping(false)
|
||||
setTimeout(() => {
|
||||
props.onTransitionEnd(ref?.offsetTop)
|
||||
props.onTransitionEnd?.(ref?.offsetTop)
|
||||
}, showAnimationDuration)
|
||||
}, 2000)
|
||||
})
|
||||
@ -32,7 +34,13 @@ export const EmbedBubble = (props: Props) => {
|
||||
})
|
||||
|
||||
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 relative z-10 items-start typebot-host-bubble w-full max-w-full">
|
||||
<div
|
||||
|
@ -7,7 +7,7 @@ import { defaultImageBubbleContent } from '@typebot.io/schemas/features/blocks/b
|
||||
|
||||
type Props = {
|
||||
content: ImageBubbleBlock['content']
|
||||
onTransitionEnd: (offsetTop?: number) => void
|
||||
onTransitionEnd?: (offsetTop?: number) => void
|
||||
}
|
||||
|
||||
export const showAnimationDuration = 400
|
||||
@ -19,13 +19,15 @@ let typingTimeout: NodeJS.Timeout
|
||||
export const ImageBubble = (props: Props) => {
|
||||
let ref: HTMLDivElement | undefined
|
||||
let image: HTMLImageElement | undefined
|
||||
const [isTyping, setIsTyping] = createSignal(true)
|
||||
const [isTyping, setIsTyping] = createSignal(
|
||||
props.onTransitionEnd ? true : false
|
||||
)
|
||||
|
||||
const onTypingEnd = () => {
|
||||
if (!isTyping()) return
|
||||
setIsTyping(false)
|
||||
setTimeout(() => {
|
||||
props.onTransitionEnd(ref?.offsetTop)
|
||||
props.onTransitionEnd?.(ref?.offsetTop)
|
||||
}, showAnimationDuration)
|
||||
}
|
||||
|
||||
@ -49,9 +51,11 @@ export const ImageBubble = (props: Props) => {
|
||||
alt={
|
||||
props.content?.clickLink?.alt ?? defaultImageBubbleContent.clickLink.alt
|
||||
}
|
||||
class={
|
||||
'text-fade-in w-full ' + (isTyping() ? 'opacity-0' : 'opacity-100')
|
||||
}
|
||||
class={clsx(
|
||||
'w-full',
|
||||
isTyping() ? 'opacity-0' : 'opacity-100',
|
||||
props.onTransitionEnd ? 'text-fade-in' : undefined
|
||||
)}
|
||||
style={{
|
||||
'max-height': '512px',
|
||||
height: isTyping() ? '32px' : 'auto',
|
||||
@ -62,7 +66,13 @@ export const ImageBubble = (props: Props) => {
|
||||
)
|
||||
|
||||
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 relative z-10 items-start typebot-host-bubble max-w-full">
|
||||
<div
|
||||
|
@ -11,7 +11,7 @@ type Props = {
|
||||
content: TextBubbleBlock['content']
|
||||
typingEmulation: Settings['typingEmulation']
|
||||
isTypingSkipped: boolean
|
||||
onTransitionEnd: (offsetTop?: number) => void
|
||||
onTransitionEnd?: (offsetTop?: number) => void
|
||||
}
|
||||
|
||||
export const showAnimationDuration = 400
|
||||
@ -20,13 +20,15 @@ let typingTimeout: NodeJS.Timeout
|
||||
|
||||
export const TextBubble = (props: Props) => {
|
||||
let ref: HTMLDivElement | undefined
|
||||
const [isTyping, setIsTyping] = createSignal(true)
|
||||
const [isTyping, setIsTyping] = createSignal(
|
||||
props.onTransitionEnd ? true : false
|
||||
)
|
||||
|
||||
const onTypingEnd = () => {
|
||||
if (!isTyping()) return
|
||||
setIsTyping(false)
|
||||
setTimeout(() => {
|
||||
props.onTransitionEnd(ref?.offsetTop)
|
||||
props.onTransitionEnd?.(ref?.offsetTop)
|
||||
}, showAnimationDuration)
|
||||
}
|
||||
|
||||
@ -50,7 +52,13 @@ export const TextBubble = (props: Props) => {
|
||||
})
|
||||
|
||||
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 relative items-start typebot-host-bubble max-w-full">
|
||||
<div
|
||||
|
@ -15,7 +15,7 @@ import {
|
||||
|
||||
type Props = {
|
||||
content: VideoBubbleBlock['content']
|
||||
onTransitionEnd: (offsetTop?: number) => void
|
||||
onTransitionEnd?: (offsetTop?: number) => void
|
||||
}
|
||||
|
||||
export const showAnimationDuration = 400
|
||||
@ -23,7 +23,9 @@ let typingTimeout: NodeJS.Timeout
|
||||
|
||||
export const VideoBubble = (props: Props) => {
|
||||
let ref: HTMLDivElement | undefined
|
||||
const [isTyping, setIsTyping] = createSignal(true)
|
||||
const [isTyping, setIsTyping] = createSignal(
|
||||
props.onTransitionEnd ? true : false
|
||||
)
|
||||
|
||||
onMount(() => {
|
||||
const typingDuration =
|
||||
@ -37,7 +39,7 @@ export const VideoBubble = (props: Props) => {
|
||||
if (!isTyping()) return
|
||||
setIsTyping(false)
|
||||
setTimeout(() => {
|
||||
props.onTransitionEnd(ref?.offsetTop)
|
||||
props.onTransitionEnd?.(ref?.offsetTop)
|
||||
}, showAnimationDuration)
|
||||
}, typingDuration)
|
||||
})
|
||||
@ -47,7 +49,13 @@ export const VideoBubble = (props: Props) => {
|
||||
})
|
||||
|
||||
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 relative z-10 items-start typebot-host-bubble overflow-hidden w-full max-w-full">
|
||||
<div
|
||||
@ -69,7 +77,7 @@ export const VideoBubble = (props: Props) => {
|
||||
}
|
||||
>
|
||||
<video
|
||||
autoplay
|
||||
autoplay={props.onTransitionEnd ? false : true}
|
||||
src={props.content?.url}
|
||||
controls
|
||||
class={
|
||||
|
@ -7,7 +7,7 @@ import { For, Show, createSignal, onMount } from 'solid-js'
|
||||
import { defaultChoiceInputOptions } from '@typebot.io/schemas/features/blocks/inputs/choice/constants'
|
||||
|
||||
type Props = {
|
||||
inputIndex: number
|
||||
chunkIndex: number
|
||||
defaultItems: ChoiceInputBlock['items']
|
||||
options: ChoiceInputBlock['options']
|
||||
onSubmit: (value: InputSubmitContent) => void
|
||||
@ -66,7 +66,7 @@ export const Buttons = (props: Props) => {
|
||||
>
|
||||
{item.content}
|
||||
</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="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" />
|
||||
|
@ -8,7 +8,6 @@ import { SearchInput } from '@/components/inputs/SearchInput'
|
||||
import { defaultChoiceInputOptions } from '@typebot.io/schemas/features/blocks/inputs/choice/constants'
|
||||
|
||||
type Props = {
|
||||
inputIndex: number
|
||||
defaultItems: ChoiceInputBlock['items']
|
||||
options: ChoiceInputBlock['options']
|
||||
onSubmit: (value: InputSubmitContent) => void
|
||||
|
@ -17,7 +17,6 @@ export const NumberInput = (props: NumberInputProps) => {
|
||||
const [inputValue, setInputValue] = createSignal<string | number>(
|
||||
props.defaultValue ?? ''
|
||||
)
|
||||
// eslint-disable-next-line solid/reactivity
|
||||
const [staticValue, bindValue, targetValue] = numberInputHelper(() =>
|
||||
inputValue()
|
||||
)
|
||||
|
@ -14,6 +14,11 @@ import { isDefined } from '@typebot.io/lib'
|
||||
import { BubbleParams } from '../types'
|
||||
import { Bot, BotProps } from '../../../components/Bot'
|
||||
import { getPaymentInProgressInStorage } from '@/features/blocks/inputs/payment/helpers/paymentInProgressStorage'
|
||||
import {
|
||||
getBotOpenedStateFromStorage,
|
||||
removeBotOpenedStateInStorage,
|
||||
setBotOpenedStateInStorage,
|
||||
} from '@/utils/storage'
|
||||
|
||||
export type BubbleProps = BotProps &
|
||||
BubbleParams & {
|
||||
@ -33,7 +38,6 @@ export const Bubble = (props: BubbleProps) => {
|
||||
])
|
||||
const [isMounted, setIsMounted] = createSignal(true)
|
||||
const [prefilledVariables, setPrefilledVariables] = createSignal(
|
||||
// eslint-disable-next-line solid/reactivity
|
||||
botProps.prefilledVariables
|
||||
)
|
||||
const [isPreviewMessageDisplayed, setIsPreviewMessageDisplayed] =
|
||||
@ -48,7 +52,6 @@ export const Bubble = (props: BubbleProps) => {
|
||||
const [isBotOpened, setIsBotOpened] = createSignal(false)
|
||||
const [isBotStarted, setIsBotStarted] = createSignal(false)
|
||||
const [buttonSize, setButtonSize] = createSignal(
|
||||
// eslint-disable-next-line solid/reactivity
|
||||
parseButtonSize(bubbleProps.theme?.button?.size ?? 'medium')
|
||||
)
|
||||
createEffect(() => {
|
||||
@ -62,8 +65,8 @@ export const Bubble = (props: BubbleProps) => {
|
||||
const autoShowDelay = bubbleProps.autoShowDelay
|
||||
const previewMessageAutoShowDelay =
|
||||
bubbleProps.previewMessage?.autoShowDelay
|
||||
const paymentInProgress = getPaymentInProgressInStorage()
|
||||
if (paymentInProgress) openBot()
|
||||
if (getBotOpenedStateFromStorage() || getPaymentInProgressInStorage())
|
||||
openBot()
|
||||
if (isDefined(autoShowDelay)) {
|
||||
setTimeout(() => {
|
||||
openBot()
|
||||
@ -113,6 +116,7 @@ export const Bubble = (props: BubbleProps) => {
|
||||
|
||||
const closeBot = () => {
|
||||
setIsBotOpened(false)
|
||||
removeBotOpenedStateInStorage()
|
||||
if (isBotOpened()) bubbleProps.onClose?.()
|
||||
}
|
||||
|
||||
@ -146,6 +150,11 @@ export const Bubble = (props: BubbleProps) => {
|
||||
} else setIsMounted(false)
|
||||
}
|
||||
|
||||
const handleOnChatStatePersisted = (isPersisted: boolean) => {
|
||||
botProps.onChatStatePersisted?.(isPersisted)
|
||||
if (isPersisted) setBotOpenedStateInStorage()
|
||||
}
|
||||
|
||||
return (
|
||||
<Show when={isMounted()}>
|
||||
<style>{styles}</style>
|
||||
@ -195,6 +204,7 @@ export const Bubble = (props: BubbleProps) => {
|
||||
<Show when={isBotStarted()}>
|
||||
<Bot
|
||||
{...botProps}
|
||||
onChatStatePersisted={handleOnChatStatePersisted}
|
||||
prefilledVariables={prefilledVariables()}
|
||||
class="rounded-lg"
|
||||
progressBarRef={progressBarContainerRef}
|
||||
|
@ -12,6 +12,11 @@ import { isDefined, isNotDefined } from '@typebot.io/lib'
|
||||
import { PopupParams } from '../types'
|
||||
import { Bot, BotProps } from '../../../components/Bot'
|
||||
import { getPaymentInProgressInStorage } from '@/features/blocks/inputs/payment/helpers/paymentInProgressStorage'
|
||||
import {
|
||||
getBotOpenedStateFromStorage,
|
||||
removeBotOpenedStateInStorage,
|
||||
setBotOpenedStateInStorage,
|
||||
} from '@/utils/storage'
|
||||
|
||||
export type PopupProps = BotProps &
|
||||
PopupParams & {
|
||||
@ -32,18 +37,18 @@ export const Popup = (props: PopupProps) => {
|
||||
])
|
||||
|
||||
const [prefilledVariables, setPrefilledVariables] = createSignal(
|
||||
// eslint-disable-next-line solid/reactivity
|
||||
botProps.prefilledVariables
|
||||
)
|
||||
|
||||
const [isBotOpened, setIsBotOpened] = createSignal(
|
||||
// eslint-disable-next-line solid/reactivity
|
||||
popupProps.isOpen ?? false
|
||||
)
|
||||
const [isBotOpened, setIsBotOpened] = createSignal(popupProps.isOpen ?? false)
|
||||
|
||||
onMount(() => {
|
||||
const paymentInProgress = getPaymentInProgressInStorage()
|
||||
if (popupProps.defaultOpen || paymentInProgress) openBot()
|
||||
if (
|
||||
popupProps.defaultOpen ||
|
||||
getPaymentInProgressInStorage() ||
|
||||
getBotOpenedStateFromStorage()
|
||||
)
|
||||
openBot()
|
||||
window.addEventListener('message', processIncomingEvent)
|
||||
const autoShowDelay = popupProps.autoShowDelay
|
||||
if (isDefined(autoShowDelay)) {
|
||||
@ -99,12 +104,18 @@ export const Popup = (props: PopupProps) => {
|
||||
popupProps.onClose?.()
|
||||
document.body.style.overflow = 'auto'
|
||||
document.removeEventListener('pointerdown', closeBot)
|
||||
removeBotOpenedStateInStorage()
|
||||
}
|
||||
|
||||
const toggleBot = () => {
|
||||
isBotOpened() ? closeBot() : openBot()
|
||||
}
|
||||
|
||||
const handleOnChatStatePersisted = (isPersisted: boolean) => {
|
||||
botProps.onChatStatePersisted?.(isPersisted)
|
||||
if (isPersisted) setBotOpenedStateInStorage()
|
||||
}
|
||||
|
||||
return (
|
||||
<Show when={isBotOpened()}>
|
||||
<style>{styles}</style>
|
||||
@ -136,7 +147,11 @@ export const Popup = (props: PopupProps) => {
|
||||
}}
|
||||
on:pointerdown={stopPropagation}
|
||||
>
|
||||
<Bot {...botProps} prefilledVariables={prefilledVariables()} />
|
||||
<Bot
|
||||
{...botProps}
|
||||
prefilledVariables={prefilledVariables()}
|
||||
onChatStatePersisted={handleOnChatStatePersisted}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -11,6 +11,7 @@ export type BotContext = {
|
||||
isPreview: boolean
|
||||
apiHost?: string
|
||||
sessionId: string
|
||||
storage: 'local' | 'session' | undefined
|
||||
}
|
||||
|
||||
export type InitialChatReply = StartChatResponse & {
|
||||
|
@ -1,4 +1,3 @@
|
||||
/* eslint-disable solid/reactivity */
|
||||
import { initGoogleAnalytics } from '@/lib/gtag'
|
||||
import { gtmBodyElement } from '@/lib/gtm'
|
||||
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) => {
|
||||
if (!typebotId) return
|
||||
try {
|
||||
return (
|
||||
sessionStorage.getItem(`${sessionStorageKey}-${typebotId}`) ??
|
||||
localStorage.getItem(`${sessionStorageKey}-${typebotId}`) ??
|
||||
sessionStorage.getItem(`${storageResultIdKey}-${typebotId}`) ??
|
||||
localStorage.getItem(`${storageResultIdKey}-${typebotId}`) ??
|
||||
undefined
|
||||
)
|
||||
} catch {
|
||||
@ -17,13 +20,86 @@ export const setResultInStorage =
|
||||
(storageType: 'local' | 'session' = 'session') =>
|
||||
(typebotId: string, resultId: string) => {
|
||||
try {
|
||||
;(storageType === 'session' ? localStorage : sessionStorage).removeItem(
|
||||
`${sessionStorageKey}-${typebotId}`
|
||||
parseRememberUserStorage(storageType).setItem(
|
||||
`${storageResultIdKey}-${typebotId}`,
|
||||
resultId
|
||||
)
|
||||
return (
|
||||
storageType === 'session' ? sessionStorage : localStorage
|
||||
).setItem(`${sessionStorageKey}-${typebotId}`, resultId)
|
||||
} catch {
|
||||
/* 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 { PopupProps } from './features/popup'
|
||||
import { BotProps } from './components/Bot'
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@typebot.io/nextjs",
|
||||
"version": "0.2.48",
|
||||
"version": "0.2.49",
|
||||
"description": "Convenient library to display typebots on your Next.js website",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@typebot.io/react",
|
||||
"version": "0.2.48",
|
||||
"version": "0.2.49",
|
||||
"description": "Convenient library to display typebots on your React app",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
|
Reference in New Issue
Block a user