Skip to content

Commit

Permalink
✨ (theme) Add progress bar option
Browse files Browse the repository at this point in the history
  • Loading branch information
baptisteArno committed Feb 21, 2024
1 parent eec5041 commit d0f5f75
Show file tree
Hide file tree
Showing 22 changed files with 451 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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 && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = () => {
Expand Down Expand Up @@ -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}
Expand Down
3 changes: 2 additions & 1 deletion apps/builder/src/features/theme/components/ThemePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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 && (
Expand Down
3 changes: 1 addition & 2 deletions apps/builder/src/features/theme/components/ThemeSideMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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
Expand Down Expand Up @@ -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 (
Expand Down Expand Up @@ -127,6 +131,12 @@ export const GeneralSettings = ({
background={generalTheme?.background ?? defaultTheme.general.background}
onBackgroundChange={handleBackgroundChange}
/>
<ProgressBarForm
progressBar={
generalTheme?.progressBar ?? defaultTheme.general.progressBar
}
onProgressBarChange={updateProgressBar}
/>
</Stack>
)
}
101 changes: 101 additions & 0 deletions apps/builder/src/features/theme/components/general/ProgressBarForm.tsx
Original file line number Diff line number Diff line change
@@ -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
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>
)
}
16 changes: 16 additions & 0 deletions apps/viewer/src/features/chat/api/continueChat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -93,12 +94,27 @@ 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,
clientSideActions,
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,
}
})
1 change: 1 addition & 0 deletions apps/viewer/src/features/chat/api/startChat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ export const startChat = publicProcedure
dynamicTheme,
logs: logs?.filter(filterPotentiallySensitiveLogs),
clientSideActions,
progress: newSessionState.progressMetadata ? 0 : undefined,
}
}
)
1 change: 1 addition & 0 deletions apps/viewer/src/features/chat/api/startChatPreview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ export const startChatPreview = publicProcedure
dynamicTheme,
logs,
clientSideActions,
progress: newSessionState.progressMetadata ? 0 : undefined,
}
}
)
130 changes: 130 additions & 0 deletions packages/bot-engine/computeCurrentProgress.ts
Original file line number Diff line number Diff line change
@@ -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
}
Loading

0 comments on commit d0f5f75

Please sign in to comment.