✨ (theme) Add progress bar option (#1276)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Introduced progress bar functionality across various components for a more interactive user experience. - Added progress tracking and display in chat sessions. - **Enhancements** - Adjusted layout height calculations in the settings and theme pages for consistency. - Improved theme customization options with progress bar styling and placement settings. - **Bug Fixes** - Fixed dynamic height calculation issues in settings and theme side menus by standardizing heights. - **Style** - Added custom styling properties for the progress bar in chat interfaces. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -15,8 +15,9 @@ import {
|
|||||||
Box,
|
Box,
|
||||||
} from '@chakra-ui/react'
|
} from '@chakra-ui/react'
|
||||||
import { useTranslate } from '@tolgee/react'
|
import { useTranslate } from '@tolgee/react'
|
||||||
import React, { ChangeEvent, useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import tinyColor from 'tinycolor2'
|
import tinyColor from 'tinycolor2'
|
||||||
|
import { useDebouncedCallback } from 'use-debounce'
|
||||||
|
|
||||||
const colorsSelection: `#${string}`[] = [
|
const colorsSelection: `#${string}`[] = [
|
||||||
'#666460',
|
'#666460',
|
||||||
@@ -42,9 +43,9 @@ export const ColorPicker = ({ value, defaultValue, onColorChange }: Props) => {
|
|||||||
const [color, setColor] = useState(defaultValue ?? '')
|
const [color, setColor] = useState(defaultValue ?? '')
|
||||||
const displayedValue = value ?? color
|
const displayedValue = value ?? color
|
||||||
|
|
||||||
const handleColorChange = (e: ChangeEvent<HTMLInputElement>) => {
|
const handleColorChange = (color: string) => {
|
||||||
setColor(e.target.value)
|
setColor(color)
|
||||||
onColorChange(e.target.value)
|
onColorChange(color)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleClick = (color: string) => () => {
|
const handleClick = (color: string) => () => {
|
||||||
@@ -103,7 +104,7 @@ export const ColorPicker = ({ value, defaultValue, onColorChange }: Props) => {
|
|||||||
aria-label={t('colorPicker.colorValue.ariaLabel')}
|
aria-label={t('colorPicker.colorValue.ariaLabel')}
|
||||||
size="sm"
|
size="sm"
|
||||||
value={displayedValue}
|
value={displayedValue}
|
||||||
onChange={handleColorChange}
|
onChange={(e) => handleColorChange(e.target.value)}
|
||||||
/>
|
/>
|
||||||
<NativeColorPicker
|
<NativeColorPicker
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -124,8 +125,12 @@ const NativeColorPicker = ({
|
|||||||
...props
|
...props
|
||||||
}: {
|
}: {
|
||||||
color: string
|
color: string
|
||||||
onColorChange: (e: ChangeEvent<HTMLInputElement>) => void
|
onColorChange: (color: string) => void
|
||||||
} & ButtonProps) => {
|
} & ButtonProps) => {
|
||||||
|
const debouncedOnColorChange = useDebouncedCallback((color: string) => {
|
||||||
|
onColorChange(color)
|
||||||
|
}, 200)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Button as="label" htmlFor="native-picker" {...props}>
|
<Button as="label" htmlFor="native-picker" {...props}>
|
||||||
@@ -136,7 +141,7 @@ const NativeColorPicker = ({
|
|||||||
display="none"
|
display="none"
|
||||||
id="native-picker"
|
id="native-picker"
|
||||||
value={color}
|
value={color}
|
||||||
onChange={onColorChange}
|
onChange={(e) => debouncedOnColorChange(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { TypebotHeader } from '@/features/editor/components/TypebotHeader'
|
|||||||
import { useTypebot } from '@/features/editor/providers/TypebotProvider'
|
import { useTypebot } from '@/features/editor/providers/TypebotProvider'
|
||||||
import { TypebotNotFoundPage } from '@/features/editor/components/TypebotNotFoundPage'
|
import { TypebotNotFoundPage } from '@/features/editor/components/TypebotNotFoundPage'
|
||||||
import { env } from '@typebot.io/env'
|
import { env } from '@typebot.io/env'
|
||||||
|
import { headerHeight } from '@/features/editor/constants'
|
||||||
|
|
||||||
export const SettingsPage = () => {
|
export const SettingsPage = () => {
|
||||||
const { typebot, is404 } = useTypebot()
|
const { typebot, is404 } = useTypebot()
|
||||||
@@ -15,7 +16,7 @@ export const SettingsPage = () => {
|
|||||||
<Flex overflow="hidden" h="100vh" flexDir="column">
|
<Flex overflow="hidden" h="100vh" flexDir="column">
|
||||||
<Seo title={typebot?.name ? `${typebot.name} | Settings` : 'Settings'} />
|
<Seo title={typebot?.name ? `${typebot.name} | Settings` : 'Settings'} />
|
||||||
<TypebotHeader />
|
<TypebotHeader />
|
||||||
<Flex h="full" w="full">
|
<Flex height={`calc(100vh - ${headerHeight}px)`} w="full">
|
||||||
<SettingsSideMenu />
|
<SettingsSideMenu />
|
||||||
<Flex flex="1">
|
<Flex flex="1">
|
||||||
{typebot && (
|
{typebot && (
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ import { GeneralSettingsForm } from './GeneralSettingsForm'
|
|||||||
import { MetadataForm } from './MetadataForm'
|
import { MetadataForm } from './MetadataForm'
|
||||||
import { TypingEmulationForm } from './TypingEmulationForm'
|
import { TypingEmulationForm } from './TypingEmulationForm'
|
||||||
import { useTypebot } from '@/features/editor/providers/TypebotProvider'
|
import { useTypebot } from '@/features/editor/providers/TypebotProvider'
|
||||||
import { headerHeight } from '@/features/editor/constants'
|
|
||||||
import { SecurityForm } from './SecurityForm'
|
import { SecurityForm } from './SecurityForm'
|
||||||
|
|
||||||
export const SettingsSideMenu = () => {
|
export const SettingsSideMenu = () => {
|
||||||
@@ -52,7 +51,7 @@ export const SettingsSideMenu = () => {
|
|||||||
<Stack
|
<Stack
|
||||||
flex="1"
|
flex="1"
|
||||||
maxW="400px"
|
maxW="400px"
|
||||||
height={`calc(100vh - ${headerHeight}px)`}
|
height="full"
|
||||||
borderRightWidth={1}
|
borderRightWidth={1}
|
||||||
pt={10}
|
pt={10}
|
||||||
spacing={10}
|
spacing={10}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { Flex } from '@chakra-ui/react'
|
|||||||
import { Standard } from '@typebot.io/nextjs'
|
import { Standard } from '@typebot.io/nextjs'
|
||||||
import { ThemeSideMenu } from './ThemeSideMenu'
|
import { ThemeSideMenu } from './ThemeSideMenu'
|
||||||
import { TypebotNotFoundPage } from '@/features/editor/components/TypebotNotFoundPage'
|
import { TypebotNotFoundPage } from '@/features/editor/components/TypebotNotFoundPage'
|
||||||
|
import { headerHeight } from '@/features/editor/constants'
|
||||||
|
|
||||||
export const ThemePage = () => {
|
export const ThemePage = () => {
|
||||||
const { typebot, is404 } = useTypebot()
|
const { typebot, is404 } = useTypebot()
|
||||||
@@ -14,7 +15,7 @@ export const ThemePage = () => {
|
|||||||
<Flex overflow="hidden" h="100vh" flexDir="column">
|
<Flex overflow="hidden" h="100vh" flexDir="column">
|
||||||
<Seo title={typebot?.name ? `${typebot.name} | Theme` : 'Theme'} />
|
<Seo title={typebot?.name ? `${typebot.name} | Theme` : 'Theme'} />
|
||||||
<TypebotHeader />
|
<TypebotHeader />
|
||||||
<Flex h="full" w="full">
|
<Flex w="full" height={`calc(100vh - ${headerHeight}px)`}>
|
||||||
<ThemeSideMenu />
|
<ThemeSideMenu />
|
||||||
<Flex flex="1">
|
<Flex flex="1">
|
||||||
{typebot && (
|
{typebot && (
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import { ChatTheme, GeneralTheme, ThemeTemplate } from '@typebot.io/schemas'
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { CustomCssSettings } from './CustomCssSettings'
|
import { CustomCssSettings } from './CustomCssSettings'
|
||||||
import { useTypebot } from '@/features/editor/providers/TypebotProvider'
|
import { useTypebot } from '@/features/editor/providers/TypebotProvider'
|
||||||
import { headerHeight } from '@/features/editor/constants'
|
|
||||||
import { ChatThemeSettings } from './chat/ChatThemeSettings'
|
import { ChatThemeSettings } from './chat/ChatThemeSettings'
|
||||||
import { GeneralSettings } from './general/GeneralSettings'
|
import { GeneralSettings } from './general/GeneralSettings'
|
||||||
import { ThemeTemplates } from './ThemeTemplates'
|
import { ThemeTemplates } from './ThemeTemplates'
|
||||||
@@ -61,7 +60,7 @@ export const ThemeSideMenu = () => {
|
|||||||
<Stack
|
<Stack
|
||||||
flex="1"
|
flex="1"
|
||||||
maxW="400px"
|
maxW="400px"
|
||||||
height={`calc(100vh - ${headerHeight}px)`}
|
h="full"
|
||||||
borderRightWidth={1}
|
borderRightWidth={1}
|
||||||
pt={10}
|
pt={10}
|
||||||
spacing={10}
|
spacing={10}
|
||||||
|
|||||||
@@ -34,56 +34,61 @@ export const BackgroundContent = ({
|
|||||||
const handleContentChange = (content: string) =>
|
const handleContentChange = (content: string) =>
|
||||||
onBackgroundContentChange(content)
|
onBackgroundContentChange(content)
|
||||||
|
|
||||||
switch (background?.type) {
|
if (
|
||||||
case BackgroundType.COLOR:
|
(background?.type ?? defaultTheme.general.background.type) ===
|
||||||
return (
|
BackgroundType.IMAGE
|
||||||
<Flex justify="space-between" align="center">
|
) {
|
||||||
<Text>{t('theme.sideMenu.global.background.color')}</Text>
|
if (!typebot) return null
|
||||||
<ColorPicker
|
return (
|
||||||
value={
|
<Popover isLazy placement="top">
|
||||||
background.content ?? defaultTheme.general.background.content
|
<PopoverTrigger>
|
||||||
}
|
{isNotEmpty(background?.content) ? (
|
||||||
onColorChange={handleContentChange}
|
<Image
|
||||||
/>
|
src={background?.content}
|
||||||
</Flex>
|
alt={t('theme.sideMenu.global.background.image.alt')}
|
||||||
)
|
cursor="pointer"
|
||||||
case BackgroundType.IMAGE:
|
_hover={{ filter: 'brightness(.9)' }}
|
||||||
if (!typebot) return null
|
transition="filter 200ms"
|
||||||
return (
|
rounded="md"
|
||||||
<Popover isLazy placement="top">
|
maxH="200px"
|
||||||
<PopoverTrigger>
|
objectFit="cover"
|
||||||
{isNotEmpty(background.content) ? (
|
/>
|
||||||
<Image
|
) : (
|
||||||
src={background.content}
|
<Button>
|
||||||
alt={t('theme.sideMenu.global.background.image.alt')}
|
{t('theme.sideMenu.global.background.image.button')}
|
||||||
cursor="pointer"
|
</Button>
|
||||||
_hover={{ filter: 'brightness(.9)' }}
|
)}
|
||||||
transition="filter 200ms"
|
</PopoverTrigger>
|
||||||
rounded="md"
|
<Portal>
|
||||||
maxH="200px"
|
<PopoverContent p="4" w="500px">
|
||||||
objectFit="cover"
|
<ImageUploadContent
|
||||||
/>
|
uploadFileProps={{
|
||||||
) : (
|
workspaceId: typebot.workspaceId,
|
||||||
<Button>
|
typebotId: typebot.id,
|
||||||
{t('theme.sideMenu.global.background.image.button')}
|
fileName: 'background',
|
||||||
</Button>
|
}}
|
||||||
)}
|
defaultUrl={background?.content}
|
||||||
</PopoverTrigger>
|
onSubmit={handleContentChange}
|
||||||
<Portal>
|
excludedTabs={['giphy', 'icon']}
|
||||||
<PopoverContent p="4" w="500px">
|
/>
|
||||||
<ImageUploadContent
|
</PopoverContent>
|
||||||
uploadFileProps={{
|
</Portal>
|
||||||
workspaceId: typebot.workspaceId,
|
</Popover>
|
||||||
typebotId: typebot.id,
|
)
|
||||||
fileName: 'background',
|
|
||||||
}}
|
|
||||||
defaultUrl={background.content}
|
|
||||||
onSubmit={handleContentChange}
|
|
||||||
excludedTabs={['giphy', 'icon']}
|
|
||||||
/>
|
|
||||||
</PopoverContent>
|
|
||||||
</Portal>
|
|
||||||
</Popover>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
(background?.type ?? defaultTheme.general.background.type) ===
|
||||||
|
BackgroundType.COLOR
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<Flex justify="space-between" align="center">
|
||||||
|
<Text>{t('theme.sideMenu.global.background.color')}</Text>
|
||||||
|
<ColorPicker
|
||||||
|
value={background?.content ?? defaultTheme.general.background.content}
|
||||||
|
onColorChange={handleContentChange}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,10 @@ import { Stack, Text } from '@chakra-ui/react'
|
|||||||
import { Background } from '@typebot.io/schemas'
|
import { Background } from '@typebot.io/schemas'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { BackgroundContent } from './BackgroundContent'
|
import { BackgroundContent } from './BackgroundContent'
|
||||||
import { BackgroundType } from '@typebot.io/schemas/features/typebot/theme/constants'
|
import {
|
||||||
|
BackgroundType,
|
||||||
|
defaultTheme,
|
||||||
|
} from '@typebot.io/schemas/features/typebot/theme/constants'
|
||||||
import { useTranslate } from '@tolgee/react'
|
import { useTranslate } from '@tolgee/react'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -11,8 +14,6 @@ type Props = {
|
|||||||
onBackgroundChange: (newBackground: Background) => void
|
onBackgroundChange: (newBackground: Background) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultBackgroundType = BackgroundType.NONE
|
|
||||||
|
|
||||||
export const BackgroundSelector = ({
|
export const BackgroundSelector = ({
|
||||||
background,
|
background,
|
||||||
onBackgroundChange,
|
onBackgroundChange,
|
||||||
@@ -20,11 +21,10 @@ export const BackgroundSelector = ({
|
|||||||
const { t } = useTranslate()
|
const { t } = useTranslate()
|
||||||
|
|
||||||
const handleBackgroundTypeChange = (type: BackgroundType) =>
|
const handleBackgroundTypeChange = (type: BackgroundType) =>
|
||||||
background &&
|
|
||||||
onBackgroundChange({ ...background, type, content: undefined })
|
onBackgroundChange({ ...background, type, content: undefined })
|
||||||
|
|
||||||
const handleBackgroundContentChange = (content: string) =>
|
const handleBackgroundContentChange = (content: string) =>
|
||||||
background && onBackgroundChange({ ...background, content })
|
onBackgroundChange({ ...background, content })
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack spacing={4}>
|
<Stack spacing={4}>
|
||||||
@@ -44,7 +44,7 @@ export const BackgroundSelector = ({
|
|||||||
value: BackgroundType.NONE,
|
value: BackgroundType.NONE,
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
value={background?.type ?? defaultBackgroundType}
|
value={background?.type ?? defaultTheme.general.background.type}
|
||||||
onSelect={handleBackgroundTypeChange}
|
onSelect={handleBackgroundTypeChange}
|
||||||
/>
|
/>
|
||||||
<BackgroundContent
|
<BackgroundContent
|
||||||
|
|||||||
@@ -3,13 +3,14 @@ import { GoogleFontForm } from './GoogleFontForm'
|
|||||||
import { CustomFontForm } from './CustomFontForm'
|
import { CustomFontForm } from './CustomFontForm'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
font: Font
|
font: Font | undefined
|
||||||
onFontChange: (font: Font) => void
|
onFontChange: (font: Font) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FontForm = ({ font, onFontChange }: Props) => {
|
export const FontForm = ({ font, onFontChange }: Props) => {
|
||||||
if (typeof font === 'string' || font.type === 'Google')
|
if (!font || typeof font === 'string' || font?.type === 'Google')
|
||||||
return <GoogleFontForm font={font} onFontChange={onFontChange} />
|
return <GoogleFontForm font={font} onFontChange={onFontChange} />
|
||||||
if (font.type === 'Custom')
|
if (font.type === 'Custom')
|
||||||
return <CustomFontForm font={font} onFontChange={onFontChange} />
|
return <CustomFontForm font={font} onFontChange={onFontChange} />
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
useDisclosure,
|
useDisclosure,
|
||||||
Text,
|
Text,
|
||||||
} from '@chakra-ui/react'
|
} from '@chakra-ui/react'
|
||||||
import { Background, Font, Theme } from '@typebot.io/schemas'
|
import { Background, Font, ProgressBar, Theme } from '@typebot.io/schemas'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { BackgroundSelector } from './BackgroundSelector'
|
import { BackgroundSelector } from './BackgroundSelector'
|
||||||
import { LockTag } from '@/features/billing/components/LockTag'
|
import { LockTag } from '@/features/billing/components/LockTag'
|
||||||
@@ -24,6 +24,7 @@ import { env } from '@typebot.io/env'
|
|||||||
import { useTypebot } from '@/features/editor/providers/TypebotProvider'
|
import { useTypebot } from '@/features/editor/providers/TypebotProvider'
|
||||||
import { RadioButtons } from '@/components/inputs/RadioButtons'
|
import { RadioButtons } from '@/components/inputs/RadioButtons'
|
||||||
import { FontForm } from './FontForm'
|
import { FontForm } from './FontForm'
|
||||||
|
import { ProgressBarForm } from './ProgressBarForm'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isBrandingEnabled: boolean
|
isBrandingEnabled: boolean
|
||||||
@@ -63,6 +64,9 @@ export const GeneralSettings = ({
|
|||||||
const handleBackgroundChange = (background: Background) =>
|
const handleBackgroundChange = (background: Background) =>
|
||||||
onGeneralThemeChange({ ...generalTheme, background })
|
onGeneralThemeChange({ ...generalTheme, background })
|
||||||
|
|
||||||
|
const updateProgressBar = (progressBar: ProgressBar) =>
|
||||||
|
onGeneralThemeChange({ ...generalTheme, progressBar })
|
||||||
|
|
||||||
const updateBranding = () => {
|
const updateBranding = () => {
|
||||||
if (isBrandingEnabled && isWorkspaceFreePlan) return
|
if (isBrandingEnabled && isWorkspaceFreePlan) return
|
||||||
if (
|
if (
|
||||||
@@ -118,15 +122,16 @@ export const GeneralSettings = ({
|
|||||||
defaultValue={fontType}
|
defaultValue={fontType}
|
||||||
onSelect={updateFontType}
|
onSelect={updateFontType}
|
||||||
/>
|
/>
|
||||||
<FontForm
|
<FontForm font={generalTheme?.font} onFontChange={updateFont} />
|
||||||
font={generalTheme?.font ?? defaultTheme.general.font}
|
|
||||||
onFontChange={updateFont}
|
|
||||||
/>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
<BackgroundSelector
|
<BackgroundSelector
|
||||||
background={generalTheme?.background ?? defaultTheme.general.background}
|
background={generalTheme?.background}
|
||||||
onBackgroundChange={handleBackgroundChange}
|
onBackgroundChange={handleBackgroundChange}
|
||||||
/>
|
/>
|
||||||
|
<ProgressBarForm
|
||||||
|
progressBar={generalTheme?.progressBar}
|
||||||
|
onProgressBarChange={updateProgressBar}
|
||||||
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
import { Select } from '@/components/inputs/Select'
|
import { Select } from '@/components/inputs/Select'
|
||||||
import { env } from '@typebot.io/env'
|
import { env } from '@typebot.io/env'
|
||||||
import { GoogleFont } from '@typebot.io/schemas'
|
import { GoogleFont } from '@typebot.io/schemas'
|
||||||
|
import { defaultTheme } from '@typebot.io/schemas/features/typebot/theme/constants'
|
||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
font: GoogleFont | string
|
font: GoogleFont | string | undefined
|
||||||
onFontChange: (font: GoogleFont) => void
|
onFontChange: (font: GoogleFont) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GoogleFontForm = ({ font, onFontChange }: Props) => {
|
export const GoogleFontForm = ({ font, onFontChange }: Props) => {
|
||||||
const [currentFont, setCurrentFont] = useState(
|
const [currentFont, setCurrentFont] = useState(
|
||||||
typeof font === 'string' ? font : font.family
|
(typeof font === 'string' ? font : font?.family) ??
|
||||||
|
defaultTheme.general.font.family
|
||||||
)
|
)
|
||||||
const [googleFonts, setGoogleFonts] = useState<string[]>([])
|
const [googleFonts, setGoogleFonts] = useState<string[]>([])
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
import { ColorPicker } from '@/components/ColorPicker'
|
||||||
|
import { DropdownList } from '@/components/DropdownList'
|
||||||
|
import { SwitchWithRelatedSettings } from '@/components/SwitchWithRelatedSettings'
|
||||||
|
import { NumberInput } from '@/components/inputs'
|
||||||
|
import { HStack, Text } from '@chakra-ui/react'
|
||||||
|
import { ProgressBar } from '@typebot.io/schemas'
|
||||||
|
import {
|
||||||
|
defaultTheme,
|
||||||
|
progressBarPlacements,
|
||||||
|
progressBarPositions,
|
||||||
|
} from '@typebot.io/schemas/features/typebot/theme/constants'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
progressBar: ProgressBar | undefined
|
||||||
|
onProgressBarChange: (progressBar: ProgressBar) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ProgressBarForm = ({
|
||||||
|
progressBar,
|
||||||
|
onProgressBarChange,
|
||||||
|
}: Props) => {
|
||||||
|
const updateEnabled = (isEnabled: boolean) =>
|
||||||
|
onProgressBarChange({ ...progressBar, isEnabled })
|
||||||
|
|
||||||
|
const updateColor = (color: string) =>
|
||||||
|
onProgressBarChange({ ...progressBar, color })
|
||||||
|
|
||||||
|
const updateBackgroundColor = (backgroundColor: string) =>
|
||||||
|
onProgressBarChange({ ...progressBar, backgroundColor })
|
||||||
|
|
||||||
|
const updatePlacement = (placement: (typeof progressBarPlacements)[number]) =>
|
||||||
|
onProgressBarChange({ ...progressBar, placement })
|
||||||
|
|
||||||
|
const updatePosition = (position: (typeof progressBarPositions)[number]) =>
|
||||||
|
onProgressBarChange({ ...progressBar, position })
|
||||||
|
|
||||||
|
const updateThickness = (thickness?: number) =>
|
||||||
|
onProgressBarChange({ ...progressBar, thickness })
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SwitchWithRelatedSettings
|
||||||
|
label={'Enable progress bar?'}
|
||||||
|
initialValue={
|
||||||
|
progressBar?.isEnabled ?? defaultTheme.general.progressBar.isEnabled
|
||||||
|
}
|
||||||
|
onCheckChange={updateEnabled}
|
||||||
|
>
|
||||||
|
<DropdownList
|
||||||
|
size="sm"
|
||||||
|
direction="row"
|
||||||
|
label="Placement:"
|
||||||
|
currentItem={
|
||||||
|
progressBar?.placement ?? defaultTheme.general.progressBar.placement
|
||||||
|
}
|
||||||
|
onItemSelect={updatePlacement}
|
||||||
|
items={progressBarPlacements}
|
||||||
|
/>
|
||||||
|
<DropdownList
|
||||||
|
size="sm"
|
||||||
|
direction="row"
|
||||||
|
label="Position when embedded:"
|
||||||
|
moreInfoTooltip='Select "fixed" to always position the progress bar at the top of the window even though your bot is embedded. Select "absolute" to position the progress bar at the top of the chat container.'
|
||||||
|
currentItem={
|
||||||
|
progressBar?.position ?? defaultTheme.general.progressBar.position
|
||||||
|
}
|
||||||
|
onItemSelect={updatePosition}
|
||||||
|
items={progressBarPositions}
|
||||||
|
/>
|
||||||
|
<HStack justifyContent="space-between">
|
||||||
|
<Text>Color:</Text>
|
||||||
|
<ColorPicker
|
||||||
|
defaultValue={
|
||||||
|
progressBar?.color ?? defaultTheme.general.progressBar.color
|
||||||
|
}
|
||||||
|
onColorChange={updateColor}
|
||||||
|
/>
|
||||||
|
</HStack>
|
||||||
|
<HStack justifyContent="space-between">
|
||||||
|
<Text>Background color:</Text>
|
||||||
|
<ColorPicker
|
||||||
|
defaultValue={
|
||||||
|
progressBar?.backgroundColor ??
|
||||||
|
defaultTheme.general.progressBar.backgroundColor
|
||||||
|
}
|
||||||
|
onColorChange={updateBackgroundColor}
|
||||||
|
/>
|
||||||
|
</HStack>
|
||||||
|
<HStack justifyContent="space-between">
|
||||||
|
<Text>Thickness:</Text>
|
||||||
|
<NumberInput
|
||||||
|
withVariableButton={false}
|
||||||
|
defaultValue={
|
||||||
|
progressBar?.thickness ?? defaultTheme.general.progressBar.thickness
|
||||||
|
}
|
||||||
|
onValueChange={updateThickness}
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
</HStack>
|
||||||
|
</SwitchWithRelatedSettings>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import { parseDynamicTheme } from '@typebot.io/bot-engine/parseDynamicTheme'
|
|||||||
import { isDefined, isNotDefined } from '@typebot.io/lib/utils'
|
import { isDefined, isNotDefined } from '@typebot.io/lib/utils'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { filterPotentiallySensitiveLogs } from '@typebot.io/bot-engine/logs/filterPotentiallySensitiveLogs'
|
import { filterPotentiallySensitiveLogs } from '@typebot.io/bot-engine/logs/filterPotentiallySensitiveLogs'
|
||||||
|
import { computeCurrentProgress } from '@typebot.io/bot-engine/computeCurrentProgress'
|
||||||
|
|
||||||
export const continueChat = publicProcedure
|
export const continueChat = publicProcedure
|
||||||
.meta({
|
.meta({
|
||||||
@@ -93,6 +94,12 @@ export const continueChat = publicProcedure
|
|||||||
|
|
||||||
const isPreview = isNotDefined(session.state.typebotsQueue[0].resultId)
|
const isPreview = isNotDefined(session.state.typebotsQueue[0].resultId)
|
||||||
|
|
||||||
|
const isEnded =
|
||||||
|
newSessionState.progressMetadata &&
|
||||||
|
!input?.id &&
|
||||||
|
(clientSideActions?.filter((c) => c.expectsDedicatedReply).length ??
|
||||||
|
0) === 0
|
||||||
|
|
||||||
return {
|
return {
|
||||||
messages,
|
messages,
|
||||||
input,
|
input,
|
||||||
@@ -100,5 +107,14 @@ export const continueChat = publicProcedure
|
|||||||
dynamicTheme: parseDynamicTheme(newSessionState),
|
dynamicTheme: parseDynamicTheme(newSessionState),
|
||||||
logs: isPreview ? logs : logs?.filter(filterPotentiallySensitiveLogs),
|
logs: isPreview ? logs : logs?.filter(filterPotentiallySensitiveLogs),
|
||||||
lastMessageNewFormat,
|
lastMessageNewFormat,
|
||||||
|
progress: newSessionState.progressMetadata
|
||||||
|
? isEnded
|
||||||
|
? 100
|
||||||
|
: computeCurrentProgress({
|
||||||
|
typebotsQueue: newSessionState.typebotsQueue,
|
||||||
|
progressMetadata: newSessionState.progressMetadata,
|
||||||
|
currentInputBlockId: input?.id as string,
|
||||||
|
})
|
||||||
|
: undefined,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ export const startChat = publicProcedure
|
|||||||
dynamicTheme,
|
dynamicTheme,
|
||||||
logs: logs?.filter(filterPotentiallySensitiveLogs),
|
logs: logs?.filter(filterPotentiallySensitiveLogs),
|
||||||
clientSideActions,
|
clientSideActions,
|
||||||
|
progress: newSessionState.progressMetadata ? 0 : undefined,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ export const startChatPreview = publicProcedure
|
|||||||
dynamicTheme,
|
dynamicTheme,
|
||||||
logs,
|
logs,
|
||||||
clientSideActions,
|
clientSideActions,
|
||||||
|
progress: newSessionState.progressMetadata ? 0 : undefined,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
130
packages/bot-engine/computeCurrentProgress.ts
Normal file
130
packages/bot-engine/computeCurrentProgress.ts
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
import { blockHasItems, isDefined, isInputBlock, byId } from '@typebot.io/lib'
|
||||||
|
import { getBlockById } from '@typebot.io/lib/getBlockById'
|
||||||
|
import { Block, SessionState } from '@typebot.io/schemas'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
typebotsQueue: SessionState['typebotsQueue']
|
||||||
|
progressMetadata: NonNullable<SessionState['progressMetadata']>
|
||||||
|
currentInputBlockId: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const computeCurrentProgress = ({
|
||||||
|
typebotsQueue,
|
||||||
|
progressMetadata,
|
||||||
|
currentInputBlockId,
|
||||||
|
}: Props) => {
|
||||||
|
if (progressMetadata.totalAnswers === 0) return 0
|
||||||
|
const paths = computePossibleNextInputBlocks({
|
||||||
|
typebotsQueue: typebotsQueue,
|
||||||
|
blockId: currentInputBlockId,
|
||||||
|
visitedBlocks: {
|
||||||
|
[typebotsQueue[0].typebot.id]: [],
|
||||||
|
},
|
||||||
|
currentPath: [],
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
(progressMetadata.totalAnswers /
|
||||||
|
(Math.max(...paths.map((b) => b.length)) +
|
||||||
|
progressMetadata.totalAnswers)) *
|
||||||
|
100
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const computePossibleNextInputBlocks = ({
|
||||||
|
currentPath,
|
||||||
|
typebotsQueue,
|
||||||
|
blockId,
|
||||||
|
visitedBlocks,
|
||||||
|
}: {
|
||||||
|
currentPath: string[]
|
||||||
|
typebotsQueue: SessionState['typebotsQueue']
|
||||||
|
blockId: string
|
||||||
|
visitedBlocks: {
|
||||||
|
[key: string]: string[]
|
||||||
|
}
|
||||||
|
}): string[][] => {
|
||||||
|
if (visitedBlocks[typebotsQueue[0].typebot.id].includes(blockId)) return []
|
||||||
|
visitedBlocks[typebotsQueue[0].typebot.id].push(blockId)
|
||||||
|
|
||||||
|
const possibleNextInputBlocks: string[][] = []
|
||||||
|
|
||||||
|
const { block, group, blockIndex } = getBlockById(
|
||||||
|
blockId,
|
||||||
|
typebotsQueue[0].typebot.groups
|
||||||
|
)
|
||||||
|
|
||||||
|
if (isInputBlock(block)) currentPath.push(block.id)
|
||||||
|
|
||||||
|
const outgoingEdgeIds = getBlockOutgoingEdgeIds(block)
|
||||||
|
|
||||||
|
for (const outgoingEdgeId of outgoingEdgeIds) {
|
||||||
|
const to = typebotsQueue[0].typebot.edges.find(
|
||||||
|
(e) => e.id === outgoingEdgeId
|
||||||
|
)?.to
|
||||||
|
if (!to) continue
|
||||||
|
const blockId =
|
||||||
|
to.blockId ??
|
||||||
|
typebotsQueue[0].typebot.groups.find((g) => g.id === to.groupId)
|
||||||
|
?.blocks[0].id
|
||||||
|
if (!blockId) continue
|
||||||
|
possibleNextInputBlocks.push(
|
||||||
|
...computePossibleNextInputBlocks({
|
||||||
|
typebotsQueue,
|
||||||
|
blockId,
|
||||||
|
visitedBlocks,
|
||||||
|
currentPath,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const block of group.blocks.slice(blockIndex + 1)) {
|
||||||
|
possibleNextInputBlocks.push(
|
||||||
|
...computePossibleNextInputBlocks({
|
||||||
|
typebotsQueue,
|
||||||
|
blockId: block.id,
|
||||||
|
visitedBlocks,
|
||||||
|
currentPath,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (outgoingEdgeIds.length > 0 || group.blocks.length !== blockIndex + 1)
|
||||||
|
return possibleNextInputBlocks
|
||||||
|
|
||||||
|
if (typebotsQueue.length > 1) {
|
||||||
|
const nextEdgeId = typebotsQueue[0].edgeIdToTriggerWhenDone
|
||||||
|
const to = typebotsQueue[1].typebot.edges.find(byId(nextEdgeId))?.to
|
||||||
|
if (!to) return possibleNextInputBlocks
|
||||||
|
const blockId =
|
||||||
|
to.blockId ??
|
||||||
|
typebotsQueue[0].typebot.groups.find((g) => g.id === to.groupId)
|
||||||
|
?.blocks[0].id
|
||||||
|
if (blockId) {
|
||||||
|
possibleNextInputBlocks.push(
|
||||||
|
...computePossibleNextInputBlocks({
|
||||||
|
typebotsQueue: typebotsQueue.slice(1),
|
||||||
|
blockId,
|
||||||
|
visitedBlocks: {
|
||||||
|
...visitedBlocks,
|
||||||
|
[typebotsQueue[1].typebot.id]: [],
|
||||||
|
},
|
||||||
|
currentPath,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
possibleNextInputBlocks.push(currentPath)
|
||||||
|
|
||||||
|
return possibleNextInputBlocks
|
||||||
|
}
|
||||||
|
|
||||||
|
const getBlockOutgoingEdgeIds = (block: Block) => {
|
||||||
|
const edgeIds: string[] = []
|
||||||
|
if (blockHasItems(block)) {
|
||||||
|
edgeIds.push(...block.items.map((i) => i.outgoingEdgeId).filter(isDefined))
|
||||||
|
}
|
||||||
|
if (block.outgoingEdgeId) edgeIds.push(block.outgoingEdgeId)
|
||||||
|
return edgeIds
|
||||||
|
}
|
||||||
@@ -357,6 +357,9 @@ const setNewAnswerInState =
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
|
progressMetadata: state.progressMetadata
|
||||||
|
? { totalAnswers: state.progressMetadata.totalAnswers + 1 }
|
||||||
|
: undefined,
|
||||||
typebotsQueue: state.typebotsQueue.map((typebot, index) =>
|
typebotsQueue: state.typebotsQueue.map((typebot, index) =>
|
||||||
index === 0
|
index === 0
|
||||||
? {
|
? {
|
||||||
|
|||||||
@@ -70,6 +70,13 @@ export const getNextGroup =
|
|||||||
...state.typebotsQueue.slice(2),
|
...state.typebotsQueue.slice(2),
|
||||||
],
|
],
|
||||||
} satisfies SessionState
|
} satisfies SessionState
|
||||||
|
if (state.progressMetadata)
|
||||||
|
newSessionState.progressMetadata = {
|
||||||
|
...state.progressMetadata,
|
||||||
|
totalAnswers:
|
||||||
|
state.progressMetadata.totalAnswers +
|
||||||
|
state.typebotsQueue[0].answers.length,
|
||||||
|
}
|
||||||
const nextGroup = await getNextGroup(newSessionState)(nextEdgeId)
|
const nextGroup = await getNextGroup(newSessionState)(nextEdgeId)
|
||||||
newSessionState = nextGroup.newSessionState
|
newSessionState = nextGroup.newSessionState
|
||||||
if (!nextGroup)
|
if (!nextGroup)
|
||||||
|
|||||||
@@ -137,6 +137,11 @@ export const startSession = async ({
|
|||||||
startParams.type === 'preview'
|
startParams.type === 'preview'
|
||||||
? undefined
|
? undefined
|
||||||
: typebot.settings.security?.allowedOrigins,
|
: typebot.settings.security?.allowedOrigins,
|
||||||
|
progressMetadata: initialSessionState?.whatsApp
|
||||||
|
? undefined
|
||||||
|
: typebot.theme.general?.progressBar?.isEnabled
|
||||||
|
? { totalAnswers: 0 }
|
||||||
|
: undefined,
|
||||||
...initialSessionState,
|
...initialSessionState,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@typebot.io/js",
|
"name": "@typebot.io/js",
|
||||||
"version": "0.2.42",
|
"version": "0.2.43",
|
||||||
"description": "Javascript library to display typebots on your website",
|
"description": "Javascript library to display typebots on your website",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
|
|||||||
@@ -31,6 +31,13 @@
|
|||||||
|
|
||||||
--typebot-border-radius: 6px;
|
--typebot-border-radius: 6px;
|
||||||
|
|
||||||
|
--typebot-progress-bar-position: fixed;
|
||||||
|
--typebot-progress-bar-bg-color: #f7f8ff;
|
||||||
|
--typebot-progress-bar-color: #0042da;
|
||||||
|
--typebot-progress-bar-height: 6px;
|
||||||
|
--typebot-progress-bar-top: 0;
|
||||||
|
--typebot-progress-bar-bottom: auto;
|
||||||
|
|
||||||
/* Phone input */
|
/* Phone input */
|
||||||
--PhoneInputCountryFlag-borderColor: transparent;
|
--PhoneInputCountryFlag-borderColor: transparent;
|
||||||
--PhoneInput-color--focus: transparent;
|
--PhoneInput-color--focus: transparent;
|
||||||
@@ -400,3 +407,21 @@ select option {
|
|||||||
color: var(--typebot-input-color);
|
color: var(--typebot-input-color);
|
||||||
background-color: var(--typebot-input-bg-color);
|
background-color: var(--typebot-input-bg-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.typebot-progress-bar-container {
|
||||||
|
background-color: var(--typebot-progress-bar-bg-color);
|
||||||
|
height: var(--typebot-progress-bar-height);
|
||||||
|
position: var(--typebot-progress-bar-position);
|
||||||
|
top: var(--typebot-progress-bar-top);
|
||||||
|
bottom: var(--typebot-progress-bar-bottom);
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
z-index: 42424242;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typebot-progress-bar-container > .typebot-progress-bar {
|
||||||
|
background-color: var(--typebot-progress-bar-color);
|
||||||
|
position: absolute;
|
||||||
|
height: 100%;
|
||||||
|
transition: width 0.25s ease;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { LiteBadge } from './LiteBadge'
|
import { LiteBadge } from './LiteBadge'
|
||||||
import { createEffect, createSignal, onCleanup, onMount, Show } from 'solid-js'
|
import { createEffect, createSignal, onCleanup, onMount, Show } from 'solid-js'
|
||||||
import { isNotDefined, isNotEmpty } from '@typebot.io/lib'
|
import { isDefined, isNotDefined, isNotEmpty } from '@typebot.io/lib'
|
||||||
import { startChatQuery } from '@/queries/startChatQuery'
|
import { startChatQuery } from '@/queries/startChatQuery'
|
||||||
import { ConversationContainer } from './ConversationContainer'
|
import { ConversationContainer } from './ConversationContainer'
|
||||||
import { setIsMobile } from '@/utils/isMobileSignal'
|
import { setIsMobile } from '@/utils/isMobileSignal'
|
||||||
@@ -18,6 +18,7 @@ import { defaultTheme } from '@typebot.io/schemas/features/typebot/theme/constan
|
|||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { HTTPError } from 'ky'
|
import { HTTPError } from 'ky'
|
||||||
import { injectFont } from '@/utils/injectFont'
|
import { injectFont } from '@/utils/injectFont'
|
||||||
|
import { ProgressBar } from './ProgressBar'
|
||||||
|
|
||||||
export type BotProps = {
|
export type BotProps = {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
@@ -129,6 +130,14 @@ export const Bot = (props: BotProps & { class?: string }) => {
|
|||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (isNotDefined(props.typebot) || typeof props.typebot === 'string') return
|
if (isNotDefined(props.typebot) || typeof props.typebot === 'string') return
|
||||||
setCustomCss(props.typebot.theme.customCss ?? '')
|
setCustomCss(props.typebot.theme.customCss ?? '')
|
||||||
|
if (
|
||||||
|
props.typebot.theme.general?.progressBar?.isEnabled &&
|
||||||
|
initialChatReply() &&
|
||||||
|
!initialChatReply()?.typebot.theme.general?.progressBar?.isEnabled
|
||||||
|
) {
|
||||||
|
setIsInitialized(false)
|
||||||
|
initializeBot().then()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
onCleanup(() => {
|
onCleanup(() => {
|
||||||
@@ -190,6 +199,9 @@ type BotContentProps = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const BotContent = (props: BotContentProps) => {
|
const BotContent = (props: BotContentProps) => {
|
||||||
|
const [progressValue, setProgressValue] = createSignal<number | undefined>(
|
||||||
|
props.initialChatReply.progress
|
||||||
|
)
|
||||||
let botContainer: HTMLDivElement | undefined
|
let botContainer: HTMLDivElement | undefined
|
||||||
|
|
||||||
const resizeObserver = new ResizeObserver((entries) => {
|
const resizeObserver = new ResizeObserver((entries) => {
|
||||||
@@ -207,7 +219,11 @@ const BotContent = (props: BotContentProps) => {
|
|||||||
defaultTheme.general.font
|
defaultTheme.general.font
|
||||||
)
|
)
|
||||||
if (!botContainer) return
|
if (!botContainer) return
|
||||||
setCssVariablesValue(props.initialChatReply.typebot.theme, botContainer)
|
setCssVariablesValue(
|
||||||
|
props.initialChatReply.typebot.theme,
|
||||||
|
botContainer,
|
||||||
|
props.context.isPreview
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
onCleanup(() => {
|
onCleanup(() => {
|
||||||
@@ -223,6 +239,14 @@ const BotContent = (props: BotContentProps) => {
|
|||||||
props.class
|
props.class
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
<Show
|
||||||
|
when={
|
||||||
|
isDefined(progressValue()) &&
|
||||||
|
props.initialChatReply.typebot.theme.general?.progressBar?.isEnabled
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ProgressBar value={progressValue() as number} />
|
||||||
|
</Show>
|
||||||
<div class="flex w-full h-full justify-center">
|
<div class="flex w-full h-full justify-center">
|
||||||
<ConversationContainer
|
<ConversationContainer
|
||||||
context={props.context}
|
context={props.context}
|
||||||
@@ -231,6 +255,7 @@ const BotContent = (props: BotContentProps) => {
|
|||||||
onAnswer={props.onAnswer}
|
onAnswer={props.onAnswer}
|
||||||
onEnd={props.onEnd}
|
onEnd={props.onEnd}
|
||||||
onNewLogs={props.onNewLogs}
|
onNewLogs={props.onNewLogs}
|
||||||
|
onProgressUpdate={setProgressValue}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Show
|
<Show
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ type Props = {
|
|||||||
onAnswer?: (answer: { message: string; blockId: string }) => void
|
onAnswer?: (answer: { message: string; blockId: string }) => void
|
||||||
onEnd?: () => void
|
onEnd?: () => void
|
||||||
onNewLogs?: (logs: OutgoingLog[]) => void
|
onNewLogs?: (logs: OutgoingLog[]) => void
|
||||||
|
onProgressUpdate?: (progress: number) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ConversationContainer = (props: Props) => {
|
export const ConversationContainer = (props: Props) => {
|
||||||
@@ -172,6 +173,7 @@ export const ConversationContainer = (props: Props) => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!data) return
|
if (!data) return
|
||||||
|
if (data.progress) props.onProgressUpdate?.(data.progress)
|
||||||
if (data.lastMessageNewFormat) {
|
if (data.lastMessageNewFormat) {
|
||||||
setFormattedMessages([
|
setFormattedMessages([
|
||||||
...formattedMessages(),
|
...formattedMessages(),
|
||||||
@@ -269,7 +271,7 @@ export const ConversationContainer = (props: Props) => {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={chatContainer}
|
ref={chatContainer}
|
||||||
class="flex flex-col overflow-y-scroll w-full min-h-full px-3 pt-10 relative scrollable-container typebot-chat-view scroll-smooth gap-2"
|
class="flex flex-col overflow-y-auto w-full min-h-full px-3 pt-10 relative scrollable-container typebot-chat-view scroll-smooth gap-2"
|
||||||
>
|
>
|
||||||
<For each={chatChunks()}>
|
<For each={chatChunks()}>
|
||||||
{(chatChunk, index) => (
|
{(chatChunk, index) => (
|
||||||
|
|||||||
14
packages/embeds/js/src/components/ProgressBar.tsx
Normal file
14
packages/embeds/js/src/components/ProgressBar.tsx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
type Props = {
|
||||||
|
value: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ProgressBar = (props: Props) => (
|
||||||
|
<div class="typebot-progress-bar-container">
|
||||||
|
<div
|
||||||
|
class="typebot-progress-bar"
|
||||||
|
style={{
|
||||||
|
width: `${props.value}%`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
@@ -19,6 +19,14 @@ const cssVariableNames = {
|
|||||||
bgColor: '--typebot-container-bg-color',
|
bgColor: '--typebot-container-bg-color',
|
||||||
fontFamily: '--typebot-container-font-family',
|
fontFamily: '--typebot-container-font-family',
|
||||||
color: '--typebot-container-color',
|
color: '--typebot-container-color',
|
||||||
|
progressBar: {
|
||||||
|
position: '--typebot-progress-bar-position',
|
||||||
|
color: '--typebot-progress-bar-color',
|
||||||
|
backgroundColor: '--typebot-progress-bar-bg-color',
|
||||||
|
height: '--typebot-progress-bar-height',
|
||||||
|
top: '--typebot-progress-bar-top',
|
||||||
|
bottom: '--typebot-progress-bar-bottom',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
chat: {
|
chat: {
|
||||||
hostBubbles: {
|
hostBubbles: {
|
||||||
@@ -49,18 +57,24 @@ const cssVariableNames = {
|
|||||||
|
|
||||||
export const setCssVariablesValue = (
|
export const setCssVariablesValue = (
|
||||||
theme: Theme | undefined,
|
theme: Theme | undefined,
|
||||||
container: HTMLDivElement
|
container: HTMLDivElement,
|
||||||
|
isPreview?: boolean
|
||||||
) => {
|
) => {
|
||||||
if (!theme) return
|
if (!theme) return
|
||||||
const documentStyle = container?.style
|
const documentStyle = container?.style
|
||||||
if (!documentStyle) return
|
if (!documentStyle) return
|
||||||
setGeneralTheme(theme.general ?? defaultTheme.general, documentStyle)
|
setGeneralTheme(
|
||||||
|
theme.general ?? defaultTheme.general,
|
||||||
|
documentStyle,
|
||||||
|
isPreview
|
||||||
|
)
|
||||||
setChatTheme(theme.chat ?? defaultTheme.chat, documentStyle)
|
setChatTheme(theme.chat ?? defaultTheme.chat, documentStyle)
|
||||||
}
|
}
|
||||||
|
|
||||||
const setGeneralTheme = (
|
const setGeneralTheme = (
|
||||||
generalTheme: GeneralTheme,
|
generalTheme: GeneralTheme,
|
||||||
documentStyle: CSSStyleDeclaration
|
documentStyle: CSSStyleDeclaration,
|
||||||
|
isPreview?: boolean
|
||||||
) => {
|
) => {
|
||||||
setTypebotBackground(
|
setTypebotBackground(
|
||||||
generalTheme.background ?? defaultTheme.general.background,
|
generalTheme.background ?? defaultTheme.general.background,
|
||||||
@@ -72,6 +86,51 @@ const setGeneralTheme = (
|
|||||||
? generalTheme.font
|
? generalTheme.font
|
||||||
: generalTheme.font?.family) ?? defaultTheme.general.font.family
|
: generalTheme.font?.family) ?? defaultTheme.general.font.family
|
||||||
)
|
)
|
||||||
|
setProgressBar(
|
||||||
|
generalTheme.progressBar ?? defaultTheme.general.progressBar,
|
||||||
|
documentStyle,
|
||||||
|
isPreview
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const setProgressBar = (
|
||||||
|
progressBar: NonNullable<GeneralTheme['progressBar']>,
|
||||||
|
documentStyle: CSSStyleDeclaration,
|
||||||
|
isPreview?: boolean
|
||||||
|
) => {
|
||||||
|
const position =
|
||||||
|
progressBar.position ?? defaultTheme.general.progressBar.position
|
||||||
|
|
||||||
|
documentStyle.setProperty(
|
||||||
|
cssVariableNames.general.progressBar.position,
|
||||||
|
position === 'fixed' ? (isPreview ? 'absolute' : 'fixed') : position
|
||||||
|
)
|
||||||
|
documentStyle.setProperty(
|
||||||
|
cssVariableNames.general.progressBar.color,
|
||||||
|
progressBar.color ?? defaultTheme.general.progressBar.color
|
||||||
|
)
|
||||||
|
documentStyle.setProperty(
|
||||||
|
cssVariableNames.general.progressBar.backgroundColor,
|
||||||
|
progressBar.backgroundColor ??
|
||||||
|
defaultTheme.general.progressBar.backgroundColor
|
||||||
|
)
|
||||||
|
documentStyle.setProperty(
|
||||||
|
cssVariableNames.general.progressBar.height,
|
||||||
|
`${progressBar.thickness ?? defaultTheme.general.progressBar.thickness}px`
|
||||||
|
)
|
||||||
|
|
||||||
|
const placement =
|
||||||
|
progressBar.placement ?? defaultTheme.general.progressBar.placement
|
||||||
|
|
||||||
|
documentStyle.setProperty(
|
||||||
|
cssVariableNames.general.progressBar.top,
|
||||||
|
placement === 'Top' ? '0' : 'auto'
|
||||||
|
)
|
||||||
|
|
||||||
|
documentStyle.setProperty(
|
||||||
|
cssVariableNames.general.progressBar.bottom,
|
||||||
|
placement === 'Bottom' ? '0' : 'auto'
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const setChatTheme = (
|
const setChatTheme = (
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@typebot.io/nextjs",
|
"name": "@typebot.io/nextjs",
|
||||||
"version": "0.2.42",
|
"version": "0.2.43",
|
||||||
"description": "Convenient library to display typebots on your Next.js website",
|
"description": "Convenient library to display typebots on your Next.js website",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@typebot.io/react",
|
"name": "@typebot.io/react",
|
||||||
"version": "0.2.42",
|
"version": "0.2.43",
|
||||||
"description": "Convenient library to display typebots on your React app",
|
"description": "Convenient library to display typebots on your React app",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
|
|||||||
@@ -310,6 +310,12 @@ const chatResponseBaseSchema = z.object({
|
|||||||
.describe(
|
.describe(
|
||||||
'If the typebot contains dynamic avatars, dynamicTheme returns the new avatar URLs whenever their variables are updated.'
|
'If the typebot contains dynamic avatars, dynamicTheme returns the new avatar URLs whenever their variables are updated.'
|
||||||
),
|
),
|
||||||
|
progress: z
|
||||||
|
.number()
|
||||||
|
.optional()
|
||||||
|
.describe(
|
||||||
|
'If progress bar is enabled, this field will return a number between 0 and 100 indicating the current progress based on the longest remaining path of the flow.'
|
||||||
|
),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const startChatResponseSchema = z
|
export const startChatResponseSchema = z
|
||||||
|
|||||||
@@ -81,6 +81,11 @@ const sessionStateSchemaV2 = z.object({
|
|||||||
.describe('Expiry timeout in milliseconds'),
|
.describe('Expiry timeout in milliseconds'),
|
||||||
typingEmulation: settingsSchema.shape.typingEmulation.optional(),
|
typingEmulation: settingsSchema.shape.typingEmulation.optional(),
|
||||||
currentVisitedEdgeIndex: z.number().optional(),
|
currentVisitedEdgeIndex: z.number().optional(),
|
||||||
|
progressMetadata: z
|
||||||
|
.object({
|
||||||
|
totalAnswers: z.number(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
const sessionStateSchemaV3 = sessionStateSchemaV2
|
const sessionStateSchemaV3 = sessionStateSchemaV2
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ export enum BackgroundType {
|
|||||||
|
|
||||||
export const fontTypes = ['Google', 'Custom'] as const
|
export const fontTypes = ['Google', 'Custom'] as const
|
||||||
|
|
||||||
|
export const progressBarPlacements = ['Top', 'Bottom'] as const
|
||||||
|
export const progressBarPositions = ['fixed', 'absolute'] as const
|
||||||
|
|
||||||
export const defaultTheme = {
|
export const defaultTheme = {
|
||||||
chat: {
|
chat: {
|
||||||
roundness: 'medium',
|
roundness: 'medium',
|
||||||
@@ -32,5 +35,13 @@ export const defaultTheme = {
|
|||||||
family: 'Open Sans',
|
family: 'Open Sans',
|
||||||
},
|
},
|
||||||
background: { type: BackgroundType.COLOR, content: '#ffffff' },
|
background: { type: BackgroundType.COLOR, content: '#ffffff' },
|
||||||
|
progressBar: {
|
||||||
|
isEnabled: false,
|
||||||
|
color: '#0042DA',
|
||||||
|
backgroundColor: '#e0edff',
|
||||||
|
thickness: 4,
|
||||||
|
position: 'fixed',
|
||||||
|
placement: 'Top',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
} as const satisfies Theme
|
} as const satisfies Theme
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
import { ThemeTemplate as ThemeTemplatePrisma } from '@typebot.io/prisma'
|
import { ThemeTemplate as ThemeTemplatePrisma } from '@typebot.io/prisma'
|
||||||
import { z } from '../../../zod'
|
import { z } from '../../../zod'
|
||||||
import { BackgroundType, fontTypes } from './constants'
|
import {
|
||||||
|
BackgroundType,
|
||||||
|
fontTypes,
|
||||||
|
progressBarPlacements,
|
||||||
|
progressBarPositions,
|
||||||
|
} from './constants'
|
||||||
|
|
||||||
const avatarPropsSchema = z.object({
|
const avatarPropsSchema = z.object({
|
||||||
isEnabled: z.boolean().optional(),
|
isEnabled: z.boolean().optional(),
|
||||||
@@ -51,9 +56,20 @@ export const fontSchema = z
|
|||||||
.or(z.discriminatedUnion('type', [googleFontSchema, customFontSchema]))
|
.or(z.discriminatedUnion('type', [googleFontSchema, customFontSchema]))
|
||||||
export type Font = z.infer<typeof fontSchema>
|
export type Font = z.infer<typeof fontSchema>
|
||||||
|
|
||||||
|
const progressBarSchema = z.object({
|
||||||
|
isEnabled: z.boolean().optional(),
|
||||||
|
color: z.string().optional(),
|
||||||
|
backgroundColor: z.string().optional(),
|
||||||
|
placement: z.enum(progressBarPlacements).optional(),
|
||||||
|
thickness: z.number().optional(),
|
||||||
|
position: z.enum(progressBarPositions).optional(),
|
||||||
|
})
|
||||||
|
export type ProgressBar = z.infer<typeof progressBarSchema>
|
||||||
|
|
||||||
const generalThemeSchema = z.object({
|
const generalThemeSchema = z.object({
|
||||||
font: fontSchema.optional(),
|
font: fontSchema.optional(),
|
||||||
background: backgroundSchema.optional(),
|
background: backgroundSchema.optional(),
|
||||||
|
progressBar: progressBarSchema.optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const themeSchema = z
|
export const themeSchema = z
|
||||||
|
|||||||
Reference in New Issue
Block a user