✨ (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,
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user