2
0

(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:
Baptiste Arnaud
2024-02-23 08:31:14 +01:00
committed by GitHub
parent f2b21746bc
commit 2d7ccf17c0
30 changed files with 535 additions and 90 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -96,6 +96,7 @@ export const startChat = publicProcedure
dynamicTheme,
logs: logs?.filter(filterPotentiallySensitiveLogs),
clientSideActions,
progress: newSessionState.progressMetadata ? 0 : undefined,
}
}
)

View File

@@ -83,6 +83,7 @@ export const startChatPreview = publicProcedure
dynamicTheme,
logs,
clientSideActions,
progress: newSessionState.progressMetadata ? 0 : undefined,
}
}
)