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

@@ -1,4 +1,3 @@
/* eslint-disable solid/reactivity */
import { initGoogleAnalytics } from '@/lib/gtag'
import { gtmBodyElement } from '@/lib/gtm'
import { initPixel } from '@/lib/pixel'

View 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

View File

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