⚡ 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:
@ -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