✨ (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,
|
||||
} from '@chakra-ui/react'
|
||||
import { useTranslate } from '@tolgee/react'
|
||||
import React, { ChangeEvent, useState } from 'react'
|
||||
import React, { useState } from 'react'
|
||||
import tinyColor from 'tinycolor2'
|
||||
import { useDebouncedCallback } from 'use-debounce'
|
||||
|
||||
const colorsSelection: `#${string}`[] = [
|
||||
'#666460',
|
||||
@ -42,9 +43,9 @@ export const ColorPicker = ({ value, defaultValue, onColorChange }: Props) => {
|
||||
const [color, setColor] = useState(defaultValue ?? '')
|
||||
const displayedValue = value ?? color
|
||||
|
||||
const handleColorChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
setColor(e.target.value)
|
||||
onColorChange(e.target.value)
|
||||
const handleColorChange = (color: string) => {
|
||||
setColor(color)
|
||||
onColorChange(color)
|
||||
}
|
||||
|
||||
const handleClick = (color: string) => () => {
|
||||
@ -103,7 +104,7 @@ export const ColorPicker = ({ value, defaultValue, onColorChange }: Props) => {
|
||||
aria-label={t('colorPicker.colorValue.ariaLabel')}
|
||||
size="sm"
|
||||
value={displayedValue}
|
||||
onChange={handleColorChange}
|
||||
onChange={(e) => handleColorChange(e.target.value)}
|
||||
/>
|
||||
<NativeColorPicker
|
||||
size="sm"
|
||||
@ -124,8 +125,12 @@ const NativeColorPicker = ({
|
||||
...props
|
||||
}: {
|
||||
color: string
|
||||
onColorChange: (e: ChangeEvent<HTMLInputElement>) => void
|
||||
onColorChange: (color: string) => void
|
||||
} & ButtonProps) => {
|
||||
const debouncedOnColorChange = useDebouncedCallback((color: string) => {
|
||||
onColorChange(color)
|
||||
}, 200)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button as="label" htmlFor="native-picker" {...props}>
|
||||
@ -136,7 +141,7 @@ const NativeColorPicker = ({
|
||||
display="none"
|
||||
id="native-picker"
|
||||
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 { TypebotNotFoundPage } from '@/features/editor/components/TypebotNotFoundPage'
|
||||
import { env } from '@typebot.io/env'
|
||||
import { headerHeight } from '@/features/editor/constants'
|
||||
|
||||
export const SettingsPage = () => {
|
||||
const { typebot, is404 } = useTypebot()
|
||||
@ -15,7 +16,7 @@ export const SettingsPage = () => {
|
||||
<Flex overflow="hidden" h="100vh" flexDir="column">
|
||||
<Seo title={typebot?.name ? `${typebot.name} | Settings` : 'Settings'} />
|
||||
<TypebotHeader />
|
||||
<Flex h="full" w="full">
|
||||
<Flex height={`calc(100vh - ${headerHeight}px)`} w="full">
|
||||
<SettingsSideMenu />
|
||||
<Flex flex="1">
|
||||
{typebot && (
|
||||
|
@ -20,7 +20,6 @@ import { GeneralSettingsForm } from './GeneralSettingsForm'
|
||||
import { MetadataForm } from './MetadataForm'
|
||||
import { TypingEmulationForm } from './TypingEmulationForm'
|
||||
import { useTypebot } from '@/features/editor/providers/TypebotProvider'
|
||||
import { headerHeight } from '@/features/editor/constants'
|
||||
import { SecurityForm } from './SecurityForm'
|
||||
|
||||
export const SettingsSideMenu = () => {
|
||||
@ -52,7 +51,7 @@ export const SettingsSideMenu = () => {
|
||||
<Stack
|
||||
flex="1"
|
||||
maxW="400px"
|
||||
height={`calc(100vh - ${headerHeight}px)`}
|
||||
height="full"
|
||||
borderRightWidth={1}
|
||||
pt={10}
|
||||
spacing={10}
|
||||
|
@ -5,6 +5,7 @@ import { Flex } from '@chakra-ui/react'
|
||||
import { Standard } from '@typebot.io/nextjs'
|
||||
import { ThemeSideMenu } from './ThemeSideMenu'
|
||||
import { TypebotNotFoundPage } from '@/features/editor/components/TypebotNotFoundPage'
|
||||
import { headerHeight } from '@/features/editor/constants'
|
||||
|
||||
export const ThemePage = () => {
|
||||
const { typebot, is404 } = useTypebot()
|
||||
@ -14,7 +15,7 @@ export const ThemePage = () => {
|
||||
<Flex overflow="hidden" h="100vh" flexDir="column">
|
||||
<Seo title={typebot?.name ? `${typebot.name} | Theme` : 'Theme'} />
|
||||
<TypebotHeader />
|
||||
<Flex h="full" w="full">
|
||||
<Flex w="full" height={`calc(100vh - ${headerHeight}px)`}>
|
||||
<ThemeSideMenu />
|
||||
<Flex flex="1">
|
||||
{typebot && (
|
||||
|
@ -13,7 +13,6 @@ import { ChatTheme, GeneralTheme, ThemeTemplate } from '@typebot.io/schemas'
|
||||
import React from 'react'
|
||||
import { CustomCssSettings } from './CustomCssSettings'
|
||||
import { useTypebot } from '@/features/editor/providers/TypebotProvider'
|
||||
import { headerHeight } from '@/features/editor/constants'
|
||||
import { ChatThemeSettings } from './chat/ChatThemeSettings'
|
||||
import { GeneralSettings } from './general/GeneralSettings'
|
||||
import { ThemeTemplates } from './ThemeTemplates'
|
||||
@ -61,7 +60,7 @@ export const ThemeSideMenu = () => {
|
||||
<Stack
|
||||
flex="1"
|
||||
maxW="400px"
|
||||
height={`calc(100vh - ${headerHeight}px)`}
|
||||
h="full"
|
||||
borderRightWidth={1}
|
||||
pt={10}
|
||||
spacing={10}
|
||||
|
@ -34,56 +34,61 @@ export const BackgroundContent = ({
|
||||
const handleContentChange = (content: string) =>
|
||||
onBackgroundContentChange(content)
|
||||
|
||||
switch (background?.type) {
|
||||
case 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>
|
||||
)
|
||||
case BackgroundType.IMAGE:
|
||||
if (!typebot) return null
|
||||
return (
|
||||
<Popover isLazy placement="top">
|
||||
<PopoverTrigger>
|
||||
{isNotEmpty(background.content) ? (
|
||||
<Image
|
||||
src={background.content}
|
||||
alt={t('theme.sideMenu.global.background.image.alt')}
|
||||
cursor="pointer"
|
||||
_hover={{ filter: 'brightness(.9)' }}
|
||||
transition="filter 200ms"
|
||||
rounded="md"
|
||||
maxH="200px"
|
||||
objectFit="cover"
|
||||
/>
|
||||
) : (
|
||||
<Button>
|
||||
{t('theme.sideMenu.global.background.image.button')}
|
||||
</Button>
|
||||
)}
|
||||
</PopoverTrigger>
|
||||
<Portal>
|
||||
<PopoverContent p="4" w="500px">
|
||||
<ImageUploadContent
|
||||
uploadFileProps={{
|
||||
workspaceId: typebot.workspaceId,
|
||||
typebotId: typebot.id,
|
||||
fileName: 'background',
|
||||
}}
|
||||
defaultUrl={background.content}
|
||||
onSubmit={handleContentChange}
|
||||
excludedTabs={['giphy', 'icon']}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Portal>
|
||||
</Popover>
|
||||
)
|
||||
if (
|
||||
(background?.type ?? defaultTheme.general.background.type) ===
|
||||
BackgroundType.IMAGE
|
||||
) {
|
||||
if (!typebot) return null
|
||||
return (
|
||||
<Popover isLazy placement="top">
|
||||
<PopoverTrigger>
|
||||
{isNotEmpty(background?.content) ? (
|
||||
<Image
|
||||
src={background?.content}
|
||||
alt={t('theme.sideMenu.global.background.image.alt')}
|
||||
cursor="pointer"
|
||||
_hover={{ filter: 'brightness(.9)' }}
|
||||
transition="filter 200ms"
|
||||
rounded="md"
|
||||
maxH="200px"
|
||||
objectFit="cover"
|
||||
/>
|
||||
) : (
|
||||
<Button>
|
||||
{t('theme.sideMenu.global.background.image.button')}
|
||||
</Button>
|
||||
)}
|
||||
</PopoverTrigger>
|
||||
<Portal>
|
||||
<PopoverContent p="4" w="500px">
|
||||
<ImageUploadContent
|
||||
uploadFileProps={{
|
||||
workspaceId: typebot.workspaceId,
|
||||
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 React from 'react'
|
||||
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'
|
||||
|
||||
type Props = {
|
||||
@ -11,8 +14,6 @@ type Props = {
|
||||
onBackgroundChange: (newBackground: Background) => void
|
||||
}
|
||||
|
||||
const defaultBackgroundType = BackgroundType.NONE
|
||||
|
||||
export const BackgroundSelector = ({
|
||||
background,
|
||||
onBackgroundChange,
|
||||
@ -20,11 +21,10 @@ export const BackgroundSelector = ({
|
||||
const { t } = useTranslate()
|
||||
|
||||
const handleBackgroundTypeChange = (type: BackgroundType) =>
|
||||
background &&
|
||||
onBackgroundChange({ ...background, type, content: undefined })
|
||||
|
||||
const handleBackgroundContentChange = (content: string) =>
|
||||
background && onBackgroundChange({ ...background, content })
|
||||
onBackgroundChange({ ...background, content })
|
||||
|
||||
return (
|
||||
<Stack spacing={4}>
|
||||
@ -44,7 +44,7 @@ export const BackgroundSelector = ({
|
||||
value: BackgroundType.NONE,
|
||||
},
|
||||
]}
|
||||
value={background?.type ?? defaultBackgroundType}
|
||||
value={background?.type ?? defaultTheme.general.background.type}
|
||||
onSelect={handleBackgroundTypeChange}
|
||||
/>
|
||||
<BackgroundContent
|
||||
|
@ -3,13 +3,14 @@ import { GoogleFontForm } from './GoogleFontForm'
|
||||
import { CustomFontForm } from './CustomFontForm'
|
||||
|
||||
type Props = {
|
||||
font: Font
|
||||
font: Font | undefined
|
||||
onFontChange: (font: Font) => void
|
||||
}
|
||||
|
||||
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} />
|
||||
if (font.type === 'Custom')
|
||||
return <CustomFontForm font={font} onFontChange={onFontChange} />
|
||||
return null
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ import {
|
||||
useDisclosure,
|
||||
Text,
|
||||
} 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 { BackgroundSelector } from './BackgroundSelector'
|
||||
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 { RadioButtons } from '@/components/inputs/RadioButtons'
|
||||
import { FontForm } from './FontForm'
|
||||
import { ProgressBarForm } from './ProgressBarForm'
|
||||
|
||||
type Props = {
|
||||
isBrandingEnabled: boolean
|
||||
@ -63,6 +64,9 @@ export const GeneralSettings = ({
|
||||
const handleBackgroundChange = (background: Background) =>
|
||||
onGeneralThemeChange({ ...generalTheme, background })
|
||||
|
||||
const updateProgressBar = (progressBar: ProgressBar) =>
|
||||
onGeneralThemeChange({ ...generalTheme, progressBar })
|
||||
|
||||
const updateBranding = () => {
|
||||
if (isBrandingEnabled && isWorkspaceFreePlan) return
|
||||
if (
|
||||
@ -118,15 +122,16 @@ export const GeneralSettings = ({
|
||||
defaultValue={fontType}
|
||||
onSelect={updateFontType}
|
||||
/>
|
||||
<FontForm
|
||||
font={generalTheme?.font ?? defaultTheme.general.font}
|
||||
onFontChange={updateFont}
|
||||
/>
|
||||
<FontForm font={generalTheme?.font} onFontChange={updateFont} />
|
||||
</Stack>
|
||||
<BackgroundSelector
|
||||
background={generalTheme?.background ?? defaultTheme.general.background}
|
||||
background={generalTheme?.background}
|
||||
onBackgroundChange={handleBackgroundChange}
|
||||
/>
|
||||
<ProgressBarForm
|
||||
progressBar={generalTheme?.progressBar}
|
||||
onProgressBarChange={updateProgressBar}
|
||||
/>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
@ -1,16 +1,18 @@
|
||||
import { Select } from '@/components/inputs/Select'
|
||||
import { env } from '@typebot.io/env'
|
||||
import { GoogleFont } from '@typebot.io/schemas'
|
||||
import { defaultTheme } from '@typebot.io/schemas/features/typebot/theme/constants'
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
type Props = {
|
||||
font: GoogleFont | string
|
||||
font: GoogleFont | string | undefined
|
||||
onFontChange: (font: GoogleFont) => void
|
||||
}
|
||||
|
||||
export const GoogleFontForm = ({ font, onFontChange }: Props) => {
|
||||
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[]>([])
|
||||
|
||||
|
@ -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 { z } from 'zod'
|
||||
import { filterPotentiallySensitiveLogs } from '@typebot.io/bot-engine/logs/filterPotentiallySensitiveLogs'
|
||||
import { computeCurrentProgress } from '@typebot.io/bot-engine/computeCurrentProgress'
|
||||
|
||||
export const continueChat = publicProcedure
|
||||
.meta({
|
||||
@ -93,6 +94,12 @@ export const continueChat = publicProcedure
|
||||
|
||||
const isPreview = isNotDefined(session.state.typebotsQueue[0].resultId)
|
||||
|
||||
const isEnded =
|
||||
newSessionState.progressMetadata &&
|
||||
!input?.id &&
|
||||
(clientSideActions?.filter((c) => c.expectsDedicatedReply).length ??
|
||||
0) === 0
|
||||
|
||||
return {
|
||||
messages,
|
||||
input,
|
||||
@ -100,5 +107,14 @@ export const continueChat = publicProcedure
|
||||
dynamicTheme: parseDynamicTheme(newSessionState),
|
||||
logs: isPreview ? logs : logs?.filter(filterPotentiallySensitiveLogs),
|
||||
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,
|
||||
logs: logs?.filter(filterPotentiallySensitiveLogs),
|
||||
clientSideActions,
|
||||
progress: newSessionState.progressMetadata ? 0 : undefined,
|
||||
}
|
||||
}
|
||||
)
|
||||
|
@ -83,6 +83,7 @@ export const startChatPreview = publicProcedure
|
||||
dynamicTheme,
|
||||
logs,
|
||||
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 {
|
||||
...state,
|
||||
progressMetadata: state.progressMetadata
|
||||
? { totalAnswers: state.progressMetadata.totalAnswers + 1 }
|
||||
: undefined,
|
||||
typebotsQueue: state.typebotsQueue.map((typebot, index) =>
|
||||
index === 0
|
||||
? {
|
||||
|
@ -70,6 +70,13 @@ export const getNextGroup =
|
||||
...state.typebotsQueue.slice(2),
|
||||
],
|
||||
} satisfies SessionState
|
||||
if (state.progressMetadata)
|
||||
newSessionState.progressMetadata = {
|
||||
...state.progressMetadata,
|
||||
totalAnswers:
|
||||
state.progressMetadata.totalAnswers +
|
||||
state.typebotsQueue[0].answers.length,
|
||||
}
|
||||
const nextGroup = await getNextGroup(newSessionState)(nextEdgeId)
|
||||
newSessionState = nextGroup.newSessionState
|
||||
if (!nextGroup)
|
||||
|
@ -137,6 +137,11 @@ export const startSession = async ({
|
||||
startParams.type === 'preview'
|
||||
? undefined
|
||||
: typebot.settings.security?.allowedOrigins,
|
||||
progressMetadata: initialSessionState?.whatsApp
|
||||
? undefined
|
||||
: typebot.theme.general?.progressBar?.isEnabled
|
||||
? { totalAnswers: 0 }
|
||||
: undefined,
|
||||
...initialSessionState,
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@typebot.io/js",
|
||||
"version": "0.2.42",
|
||||
"version": "0.2.43",
|
||||
"description": "Javascript library to display typebots on your website",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
|
@ -31,6 +31,13 @@
|
||||
|
||||
--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 */
|
||||
--PhoneInputCountryFlag-borderColor: transparent;
|
||||
--PhoneInput-color--focus: transparent;
|
||||
@ -400,3 +407,21 @@ select option {
|
||||
color: var(--typebot-input-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 { 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 { ConversationContainer } from './ConversationContainer'
|
||||
import { setIsMobile } from '@/utils/isMobileSignal'
|
||||
@ -18,6 +18,7 @@ import { defaultTheme } from '@typebot.io/schemas/features/typebot/theme/constan
|
||||
import { clsx } from 'clsx'
|
||||
import { HTTPError } from 'ky'
|
||||
import { injectFont } from '@/utils/injectFont'
|
||||
import { ProgressBar } from './ProgressBar'
|
||||
|
||||
export type BotProps = {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
@ -129,6 +130,14 @@ export const Bot = (props: BotProps & { class?: string }) => {
|
||||
createEffect(() => {
|
||||
if (isNotDefined(props.typebot) || typeof props.typebot === 'string') return
|
||||
setCustomCss(props.typebot.theme.customCss ?? '')
|
||||
if (
|
||||
props.typebot.theme.general?.progressBar?.isEnabled &&
|
||||
initialChatReply() &&
|
||||
!initialChatReply()?.typebot.theme.general?.progressBar?.isEnabled
|
||||
) {
|
||||
setIsInitialized(false)
|
||||
initializeBot().then()
|
||||
}
|
||||
})
|
||||
|
||||
onCleanup(() => {
|
||||
@ -190,6 +199,9 @@ type BotContentProps = {
|
||||
}
|
||||
|
||||
const BotContent = (props: BotContentProps) => {
|
||||
const [progressValue, setProgressValue] = createSignal<number | undefined>(
|
||||
props.initialChatReply.progress
|
||||
)
|
||||
let botContainer: HTMLDivElement | undefined
|
||||
|
||||
const resizeObserver = new ResizeObserver((entries) => {
|
||||
@ -207,7 +219,11 @@ const BotContent = (props: BotContentProps) => {
|
||||
defaultTheme.general.font
|
||||
)
|
||||
if (!botContainer) return
|
||||
setCssVariablesValue(props.initialChatReply.typebot.theme, botContainer)
|
||||
setCssVariablesValue(
|
||||
props.initialChatReply.typebot.theme,
|
||||
botContainer,
|
||||
props.context.isPreview
|
||||
)
|
||||
})
|
||||
|
||||
onCleanup(() => {
|
||||
@ -223,6 +239,14 @@ const BotContent = (props: BotContentProps) => {
|
||||
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">
|
||||
<ConversationContainer
|
||||
context={props.context}
|
||||
@ -231,6 +255,7 @@ const BotContent = (props: BotContentProps) => {
|
||||
onAnswer={props.onAnswer}
|
||||
onEnd={props.onEnd}
|
||||
onNewLogs={props.onNewLogs}
|
||||
onProgressUpdate={setProgressValue}
|
||||
/>
|
||||
</div>
|
||||
<Show
|
||||
|
@ -64,6 +64,7 @@ type Props = {
|
||||
onAnswer?: (answer: { message: string; blockId: string }) => void
|
||||
onEnd?: () => void
|
||||
onNewLogs?: (logs: OutgoingLog[]) => void
|
||||
onProgressUpdate?: (progress: number) => void
|
||||
}
|
||||
|
||||
export const ConversationContainer = (props: Props) => {
|
||||
@ -172,6 +173,7 @@ export const ConversationContainer = (props: Props) => {
|
||||
return
|
||||
}
|
||||
if (!data) return
|
||||
if (data.progress) props.onProgressUpdate?.(data.progress)
|
||||
if (data.lastMessageNewFormat) {
|
||||
setFormattedMessages([
|
||||
...formattedMessages(),
|
||||
@ -269,7 +271,7 @@ export const ConversationContainer = (props: Props) => {
|
||||
return (
|
||||
<div
|
||||
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()}>
|
||||
{(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',
|
||||
fontFamily: '--typebot-container-font-family',
|
||||
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: {
|
||||
hostBubbles: {
|
||||
@ -49,18 +57,24 @@ const cssVariableNames = {
|
||||
|
||||
export const setCssVariablesValue = (
|
||||
theme: Theme | undefined,
|
||||
container: HTMLDivElement
|
||||
container: HTMLDivElement,
|
||||
isPreview?: boolean
|
||||
) => {
|
||||
if (!theme) return
|
||||
const documentStyle = container?.style
|
||||
if (!documentStyle) return
|
||||
setGeneralTheme(theme.general ?? defaultTheme.general, documentStyle)
|
||||
setGeneralTheme(
|
||||
theme.general ?? defaultTheme.general,
|
||||
documentStyle,
|
||||
isPreview
|
||||
)
|
||||
setChatTheme(theme.chat ?? defaultTheme.chat, documentStyle)
|
||||
}
|
||||
|
||||
const setGeneralTheme = (
|
||||
generalTheme: GeneralTheme,
|
||||
documentStyle: CSSStyleDeclaration
|
||||
documentStyle: CSSStyleDeclaration,
|
||||
isPreview?: boolean
|
||||
) => {
|
||||
setTypebotBackground(
|
||||
generalTheme.background ?? defaultTheme.general.background,
|
||||
@ -72,6 +86,51 @@ const setGeneralTheme = (
|
||||
? generalTheme.font
|
||||
: 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 = (
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@typebot.io/nextjs",
|
||||
"version": "0.2.42",
|
||||
"version": "0.2.43",
|
||||
"description": "Convenient library to display typebots on your Next.js website",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@typebot.io/react",
|
||||
"version": "0.2.42",
|
||||
"version": "0.2.43",
|
||||
"description": "Convenient library to display typebots on your React app",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
|
@ -310,6 +310,12 @@ const chatResponseBaseSchema = z.object({
|
||||
.describe(
|
||||
'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
|
||||
|
@ -81,6 +81,11 @@ const sessionStateSchemaV2 = z.object({
|
||||
.describe('Expiry timeout in milliseconds'),
|
||||
typingEmulation: settingsSchema.shape.typingEmulation.optional(),
|
||||
currentVisitedEdgeIndex: z.number().optional(),
|
||||
progressMetadata: z
|
||||
.object({
|
||||
totalAnswers: z.number(),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
|
||||
const sessionStateSchemaV3 = sessionStateSchemaV2
|
||||
|
@ -8,6 +8,9 @@ export enum BackgroundType {
|
||||
|
||||
export const fontTypes = ['Google', 'Custom'] as const
|
||||
|
||||
export const progressBarPlacements = ['Top', 'Bottom'] as const
|
||||
export const progressBarPositions = ['fixed', 'absolute'] as const
|
||||
|
||||
export const defaultTheme = {
|
||||
chat: {
|
||||
roundness: 'medium',
|
||||
@ -32,5 +35,13 @@ export const defaultTheme = {
|
||||
family: 'Open Sans',
|
||||
},
|
||||
background: { type: BackgroundType.COLOR, content: '#ffffff' },
|
||||
progressBar: {
|
||||
isEnabled: false,
|
||||
color: '#0042DA',
|
||||
backgroundColor: '#e0edff',
|
||||
thickness: 4,
|
||||
position: 'fixed',
|
||||
placement: 'Top',
|
||||
},
|
||||
},
|
||||
} as const satisfies Theme
|
||||
|
@ -1,6 +1,11 @@
|
||||
import { ThemeTemplate as ThemeTemplatePrisma } from '@typebot.io/prisma'
|
||||
import { z } from '../../../zod'
|
||||
import { BackgroundType, fontTypes } from './constants'
|
||||
import {
|
||||
BackgroundType,
|
||||
fontTypes,
|
||||
progressBarPlacements,
|
||||
progressBarPositions,
|
||||
} from './constants'
|
||||
|
||||
const avatarPropsSchema = z.object({
|
||||
isEnabled: z.boolean().optional(),
|
||||
@ -51,9 +56,20 @@ export const fontSchema = z
|
||||
.or(z.discriminatedUnion('type', [googleFontSchema, customFontSchema]))
|
||||
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({
|
||||
font: fontSchema.optional(),
|
||||
background: backgroundSchema.optional(),
|
||||
progressBar: progressBarSchema.optional(),
|
||||
})
|
||||
|
||||
export const themeSchema = z
|
||||
|
Reference in New Issue
Block a user