⚡ 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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user