diff --git a/apps/builder/src/components/ColorPicker.tsx b/apps/builder/src/components/ColorPicker.tsx index 48f9779453..5dfa8b679f 100644 --- a/apps/builder/src/components/ColorPicker.tsx +++ b/apps/builder/src/components/ColorPicker.tsx @@ -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) => { - 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)} /> ) => void + onColorChange: (color: string) => void } & ButtonProps) => { + const debouncedOnColorChange = useDebouncedCallback((color: string) => { + onColorChange(color) + }, 200) + return ( <> - )} - - - - - - - - ) + if ( + (background?.type ?? defaultTheme.general.background.type) === + BackgroundType.IMAGE + ) { + if (!typebot) return null + return ( + + + {isNotEmpty(background?.content) ? ( + {t('theme.sideMenu.global.background.image.alt')} + ) : ( + + )} + + + + + + + + ) } + if ( + (background?.type ?? defaultTheme.general.background.type) === + BackgroundType.COLOR + ) { + return ( + + {t('theme.sideMenu.global.background.color')} + + + ) + } + return null } diff --git a/apps/builder/src/features/theme/components/general/BackgroundSelector.tsx b/apps/builder/src/features/theme/components/general/BackgroundSelector.tsx index e3b32d5be0..2bbb4b3221 100644 --- a/apps/builder/src/features/theme/components/general/BackgroundSelector.tsx +++ b/apps/builder/src/features/theme/components/general/BackgroundSelector.tsx @@ -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 ( @@ -44,7 +44,7 @@ export const BackgroundSelector = ({ value: BackgroundType.NONE, }, ]} - value={background?.type ?? defaultBackgroundType} + value={background?.type ?? defaultTheme.general.background.type} onSelect={handleBackgroundTypeChange} /> void } export const FontForm = ({ font, onFontChange }: Props) => { - if (typeof font === 'string' || font.type === 'Google') + if (!font || typeof font === 'string' || font?.type === 'Google') return if (font.type === 'Custom') return + return null } diff --git a/apps/builder/src/features/theme/components/general/GeneralSettings.tsx b/apps/builder/src/features/theme/components/general/GeneralSettings.tsx index a4bec14a7e..3a07c51165 100644 --- a/apps/builder/src/features/theme/components/general/GeneralSettings.tsx +++ b/apps/builder/src/features/theme/components/general/GeneralSettings.tsx @@ -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} /> - + + ) } diff --git a/apps/builder/src/features/theme/components/general/GoogleFontForm.tsx b/apps/builder/src/features/theme/components/general/GoogleFontForm.tsx index 5963859b01..b601139e5e 100644 --- a/apps/builder/src/features/theme/components/general/GoogleFontForm.tsx +++ b/apps/builder/src/features/theme/components/general/GoogleFontForm.tsx @@ -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([]) diff --git a/apps/builder/src/features/theme/components/general/ProgressBarForm.tsx b/apps/builder/src/features/theme/components/general/ProgressBarForm.tsx new file mode 100644 index 0000000000..f11ce2b7d8 --- /dev/null +++ b/apps/builder/src/features/theme/components/general/ProgressBarForm.tsx @@ -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 ( + + + + + Color: + + + + Background color: + + + + Thickness: + + + + ) +} diff --git a/apps/viewer/src/features/chat/api/continueChat.ts b/apps/viewer/src/features/chat/api/continueChat.ts index 768a7374ad..fc85c8b912 100644 --- a/apps/viewer/src/features/chat/api/continueChat.ts +++ b/apps/viewer/src/features/chat/api/continueChat.ts @@ -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, } }) diff --git a/apps/viewer/src/features/chat/api/startChat.ts b/apps/viewer/src/features/chat/api/startChat.ts index 48a145de3f..95a436490a 100644 --- a/apps/viewer/src/features/chat/api/startChat.ts +++ b/apps/viewer/src/features/chat/api/startChat.ts @@ -96,6 +96,7 @@ export const startChat = publicProcedure dynamicTheme, logs: logs?.filter(filterPotentiallySensitiveLogs), clientSideActions, + progress: newSessionState.progressMetadata ? 0 : undefined, } } ) diff --git a/apps/viewer/src/features/chat/api/startChatPreview.ts b/apps/viewer/src/features/chat/api/startChatPreview.ts index d58cb83030..2b74039427 100644 --- a/apps/viewer/src/features/chat/api/startChatPreview.ts +++ b/apps/viewer/src/features/chat/api/startChatPreview.ts @@ -83,6 +83,7 @@ export const startChatPreview = publicProcedure dynamicTheme, logs, clientSideActions, + progress: newSessionState.progressMetadata ? 0 : undefined, } } ) diff --git a/packages/bot-engine/computeCurrentProgress.ts b/packages/bot-engine/computeCurrentProgress.ts new file mode 100644 index 0000000000..1c0760ca7f --- /dev/null +++ b/packages/bot-engine/computeCurrentProgress.ts @@ -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 + 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 +} diff --git a/packages/bot-engine/continueBotFlow.ts b/packages/bot-engine/continueBotFlow.ts index 1bc997c7d0..6892476412 100644 --- a/packages/bot-engine/continueBotFlow.ts +++ b/packages/bot-engine/continueBotFlow.ts @@ -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 ? { diff --git a/packages/bot-engine/getNextGroup.ts b/packages/bot-engine/getNextGroup.ts index 00d52b4c68..63399504f2 100644 --- a/packages/bot-engine/getNextGroup.ts +++ b/packages/bot-engine/getNextGroup.ts @@ -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) diff --git a/packages/bot-engine/startSession.ts b/packages/bot-engine/startSession.ts index 8fbe86d4fe..a8d3461537 100644 --- a/packages/bot-engine/startSession.ts +++ b/packages/bot-engine/startSession.ts @@ -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, } diff --git a/packages/embeds/js/package.json b/packages/embeds/js/package.json index cde9da2d0c..3a2cac2d87 100644 --- a/packages/embeds/js/package.json +++ b/packages/embeds/js/package.json @@ -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", diff --git a/packages/embeds/js/src/assets/index.css b/packages/embeds/js/src/assets/index.css index de40417dd0..2d304faf2d 100644 --- a/packages/embeds/js/src/assets/index.css +++ b/packages/embeds/js/src/assets/index.css @@ -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; +} diff --git a/packages/embeds/js/src/components/Bot.tsx b/packages/embeds/js/src/components/Bot.tsx index c821480c6e..82c5d8a0a6 100644 --- a/packages/embeds/js/src/components/Bot.tsx +++ b/packages/embeds/js/src/components/Bot.tsx @@ -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( + 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 )} > + + +
{ onAnswer={props.onAnswer} onEnd={props.onEnd} onNewLogs={props.onNewLogs} + onProgressUpdate={setProgressValue} />
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 (
{(chatChunk, index) => ( diff --git a/packages/embeds/js/src/components/ProgressBar.tsx b/packages/embeds/js/src/components/ProgressBar.tsx new file mode 100644 index 0000000000..3f78a05bd3 --- /dev/null +++ b/packages/embeds/js/src/components/ProgressBar.tsx @@ -0,0 +1,14 @@ +type Props = { + value: number +} + +export const ProgressBar = (props: Props) => ( +
+
+
+) diff --git a/packages/embeds/js/src/utils/setCssVariablesValue.ts b/packages/embeds/js/src/utils/setCssVariablesValue.ts index efd9668768..30f2757499 100644 --- a/packages/embeds/js/src/utils/setCssVariablesValue.ts +++ b/packages/embeds/js/src/utils/setCssVariablesValue.ts @@ -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, + 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 = ( diff --git a/packages/embeds/nextjs/package.json b/packages/embeds/nextjs/package.json index dc72b30276..115c015486 100644 --- a/packages/embeds/nextjs/package.json +++ b/packages/embeds/nextjs/package.json @@ -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", diff --git a/packages/embeds/react/package.json b/packages/embeds/react/package.json index 13c1fbe964..bf8249055c 100644 --- a/packages/embeds/react/package.json +++ b/packages/embeds/react/package.json @@ -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", diff --git a/packages/schemas/features/chat/schema.ts b/packages/schemas/features/chat/schema.ts index 9d5147efea..09166dcf8e 100644 --- a/packages/schemas/features/chat/schema.ts +++ b/packages/schemas/features/chat/schema.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 diff --git a/packages/schemas/features/chat/sessionState.ts b/packages/schemas/features/chat/sessionState.ts index 9ef4bb72bf..f5cdee408a 100644 --- a/packages/schemas/features/chat/sessionState.ts +++ b/packages/schemas/features/chat/sessionState.ts @@ -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 diff --git a/packages/schemas/features/typebot/theme/constants.ts b/packages/schemas/features/typebot/theme/constants.ts index 6827d1f6c6..e6c88f8ec8 100644 --- a/packages/schemas/features/typebot/theme/constants.ts +++ b/packages/schemas/features/typebot/theme/constants.ts @@ -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 diff --git a/packages/schemas/features/typebot/theme/schema.ts b/packages/schemas/features/typebot/theme/schema.ts index dc7a62ff87..be42fe846b 100644 --- a/packages/schemas/features/typebot/theme/schema.ts +++ b/packages/schemas/features/typebot/theme/schema.ts @@ -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 +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 + const generalThemeSchema = z.object({ font: fontSchema.optional(), background: backgroundSchema.optional(), + progressBar: progressBarSchema.optional(), }) export const themeSchema = z