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