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:
Baptiste Arnaud
2024-03-07 15:39:09 +01:00
committed by GitHub
parent 583294f90c
commit 0dc276c18f
31 changed files with 427 additions and 154 deletions

View File

@@ -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 ' +

View File

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

View File

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

View File

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

View File

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

View File

@@ -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={

View File

@@ -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" />

View File

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

View File

@@ -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()
)

View File

@@ -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}

View File

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