2
0

Merge branch 'main' into internal/add-vercel-functions-whatsapp-webhook

This commit is contained in:
Baptiste Arnaud
2024-04-15 12:04:54 +02:00
77 changed files with 2339 additions and 631 deletions

View File

@ -1,4 +1,3 @@
# Make sure to change this to your own random string of 32 characters (https://docs.typebot.io/self-hosting/docker#2-add-the-required-configuration)
ENCRYPTION_SECRET=H+KbL/OFrqbEuDy/1zX8bsPG+spXri3S
DATABASE_URL=postgresql://postgres:typebot@localhost:5432/typebot

View File

@ -12,9 +12,10 @@
"editor.tabSize": 2,
"typescript.updateImportsOnFileMove.enabled": "always",
"playwright.env": {
"DATABASE_URL": "postgresql://postgres:typebot@localhost:5432/typebot",
"DATABASE_URL": "postgresql://postgres:typebot@127.0.0.1:5432/typebot",
"NEXT_PUBLIC_VIEWER_URL": "http://localhost:3001",
"NEXTAUTH_URL": "http://localhost:3000"
"NEXTAUTH_URL": "http://localhost:3000",
"ENCRYPTION_SECRET": "H+KbL/OFrqbEuDy/1zX8bsPG+spXri3S"
},
"[prisma]": {
"editor.defaultFormatter": "Prisma.prisma"

View File

@ -13,6 +13,7 @@
"format:check": "prettier --check ./src --ignore-path ../../.prettierignore"
},
"dependencies": {
"@typebot.io/theme": "workspace:*",
"@braintree/sanitize-url": "7.0.1",
"@chakra-ui/anatomy": "2.1.1",
"@chakra-ui/react": "2.7.1",

View File

@ -35,10 +35,16 @@ const colorsSelection: `#${string}`[] = [
type Props = {
value?: string
defaultValue?: string
isDisabled?: boolean
onColorChange: (color: string) => void
}
export const ColorPicker = ({ value, defaultValue, onColorChange }: Props) => {
export const ColorPicker = ({
value,
defaultValue,
isDisabled,
onColorChange,
}: Props) => {
const { t } = useTranslate()
const [color, setColor] = useState(defaultValue ?? '')
const displayedValue = value ?? color
@ -63,6 +69,7 @@ export const ColorPicker = ({ value, defaultValue, onColorChange }: Props) => {
padding={0}
borderRadius={3}
borderWidth={1}
isDisabled={isDisabled}
>
<Box rounded="full" boxSize="14px" bgColor={displayedValue} />
</Button>

View File

@ -141,6 +141,8 @@ export const UnsplashPicker = ({ imageSize, onImageSelect }: Props) => {
fetchNewImages(query, 0)
}}
withVariableButton={false}
debounceTimeout={500}
forceDebounce
/>
<Link
isExternal

View File

@ -24,6 +24,7 @@ import { env } from '@typebot.io/env'
import { MoreInfoTooltip } from '../MoreInfoTooltip'
export type TextInputProps = {
forceDebounce?: boolean
defaultValue?: string
onChange?: (value: string) => void
debounceTimeout?: number
@ -62,6 +63,7 @@ export const TextInput = forwardRef(function TextInput(
autoComplete,
isDisabled,
autoFocus,
forceDebounce,
onChange: _onChange,
onFocus,
onKeyUp,
@ -83,7 +85,7 @@ export const TextInput = forwardRef(function TextInput(
const onChange = useDebouncedCallback(
// eslint-disable-next-line @typescript-eslint/no-empty-function
_onChange ?? (() => {}),
env.NEXT_PUBLIC_E2E_TEST ? 0 : debounceTimeout
env.NEXT_PUBLIC_E2E_TEST && !forceDebounce ? 0 : debounceTimeout
)
useEffect(() => {

View File

@ -1,6 +1,7 @@
import { Text } from '@chakra-ui/react'
import { useTranslate } from '@tolgee/react'
import { PaymentInputBlock } from '@typebot.io/schemas'
import { defaultPaymentInputOptions } from '@typebot.io/schemas/features/blocks/inputs/payment/constants'
type Props = {
block: PaymentInputBlock
@ -9,11 +10,7 @@ type Props = {
export const PaymentInputContent = ({ block }: Props) => {
const { t } = useTranslate()
if (
!block.options?.amount ||
!block.options.credentialsId ||
!block.options.currency
)
if (!block.options?.amount || !block.options.credentialsId)
return (
<Text color="gray.500">
{t('blocks.inputs.payment.placeholder.label')}
@ -22,7 +19,7 @@ export const PaymentInputContent = ({ block }: Props) => {
return (
<Text noOfLines={1} pr="6">
{t('blocks.inputs.payment.collect.label')} {block.options.amount}{' '}
{block.options.currency}
{block.options.currency ?? defaultPaymentInputOptions.currency}
</Text>
)
}

View File

@ -163,6 +163,7 @@ export const StripeConfigModal = ({
placeholder="sk_test_..."
withVariableButton={false}
debounceTimeout={0}
type="password"
/>
</HStack>
</Stack>
@ -187,6 +188,7 @@ export const StripeConfigModal = ({
placeholder="sk_live_..."
withVariableButton={false}
debounceTimeout={0}
type="password"
/>
</FormControl>
</HStack>

View File

@ -70,7 +70,7 @@ export const CountryCodeSelect = ({ countryCode, onSelect }: Props) => {
<option value="DK">Denmark (+45)</option>
<option value="DJ">Djibouti (+253)</option>
<option value="DM">Dominica (+1809)</option>
<option value="DO">Dominican Republic (+1809)</option>
<option value="DO">Dominican Republic (+18XX)</option>
<option value="EC">Ecuador (+593)</option>
<option value="EG">Egypt (+20)</option>
<option value="SV">El Salvador (+503)</option>

View File

@ -1,19 +1,16 @@
import { Text } from '@chakra-ui/react'
import { MakeComBlock } from '@typebot.io/schemas'
import { isNotDefined } from '@typebot.io/lib'
type Props = {
block: MakeComBlock
}
export const MakeComContent = ({ block }: Props) => {
const webhook = block.options?.webhook
if (isNotDefined(webhook?.body))
if (!block.options?.webhook?.url)
return <Text color="gray.500">Configure...</Text>
return (
<Text noOfLines={1} pr="6">
{webhook?.url ? 'Trigger scenario' : 'Disabled'}
Trigger scenario
</Text>
)
}

View File

@ -1,19 +1,16 @@
import { Text } from '@chakra-ui/react'
import { PabblyConnectBlock } from '@typebot.io/schemas'
import { isNotDefined } from '@typebot.io/lib'
type Props = {
block: PabblyConnectBlock
}
export const PabblyConnectContent = ({ block }: Props) => {
const webhook = block.options?.webhook
if (isNotDefined(webhook?.body))
if (!block.options?.webhook?.url)
return <Text color="gray.500">Configure...</Text>
return (
<Text noOfLines={1} pr="6">
{webhook?.url ? 'Trigger scenario' : 'Disabled'}
Trigger scenario
</Text>
)
}

View File

@ -1,19 +1,16 @@
import { Text } from '@chakra-ui/react'
import { ZapierBlock } from '@typebot.io/schemas'
import { isNotDefined } from '@typebot.io/lib'
type Props = {
block: ZapierBlock
}
export const ZapierContent = ({ block }: Props) => {
const webhook = block.options?.webhook
if (isNotDefined(webhook?.body))
if (!block.options?.webhook?.url)
return <Text color="gray.500">Configure...</Text>
return (
<Text noOfLines={1} pr="6">
{webhook?.url ? 'Trigger zap' : 'Disabled'}
Trigger zap
</Text>
)
}

View File

@ -26,7 +26,7 @@ import { useRouter } from 'next/router'
export const BoardMenuButton = (props: FlexProps) => {
const { query } = useRouter()
const { typebot } = useTypebot()
const { typebot, currentUserMode } = useTypebot()
const { user } = useUser()
const [isDownloading, setIsDownloading] = useState(false)
const { isOpen, onOpen, onClose } = useDisclosure()
@ -78,9 +78,11 @@ export const BoardMenuButton = (props: FlexProps) => {
<MenuItem icon={<SettingsIcon />} onClick={onOpen}>
{t('editor.graph.menu.editorSettingsItem.label')}
</MenuItem>
<MenuItem icon={<DownloadIcon />} onClick={downloadFlow}>
{t('editor.graph.menu.exportFlowItem.label')}
</MenuItem>
{currentUserMode !== 'guest' ? (
<MenuItem icon={<DownloadIcon />} onClick={downloadFlow}>
{t('editor.graph.menu.exportFlowItem.label')}
</MenuItem>
) : null}
</MenuList>
<EditorSettingsModal isOpen={isOpen} onClose={onClose} />
</Menu>

View File

@ -73,7 +73,6 @@ export const PreviewDrawer = () => {
right="0"
top={`0`}
h={`100%`}
w={`${width}px`}
bgColor={useColorModeValue('white', 'gray.900')}
borderLeftWidth={'1px'}
shadow="lg"
@ -82,6 +81,7 @@ export const PreviewDrawer = () => {
onMouseLeave={() => setIsResizeHandleVisible(false)}
p="6"
zIndex={10}
style={{ width: `${width}px` }}
>
<Fade in={isResizeHandleVisible}>
<ResizeHandle

View File

@ -5,13 +5,13 @@ import { Typebot } from '@typebot.io/schemas'
import { useState } from 'react'
import { BubbleSettings } from '../../../settings/BubbleSettings/BubbleSettings'
import { JavascriptBubbleSnippet } from '../JavascriptBubbleSnippet'
import { defaultTheme } from '@typebot.io/schemas/features/typebot/theme/constants'
import { defaultButtonsBackgroundColor } from '@typebot.io/schemas/features/typebot/theme/constants'
export const parseDefaultBubbleTheme = (typebot?: Typebot) => ({
button: {
backgroundColor:
typebot?.theme.chat?.buttons?.backgroundColor ??
defaultTheme.chat.buttons.backgroundColor,
defaultButtonsBackgroundColor,
},
})

View File

@ -158,6 +158,10 @@ export const WhatsAppCredentialsModal = ({
setIsVerifying(false)
showToast({
description: 'Could not get system info',
details:
err instanceof Error
? { content: err.message, lang: 'json' }
: undefined,
})
return false
}
@ -204,7 +208,10 @@ export const WhatsAppCredentialsModal = ({
setIsVerifying(false)
showToast({
description: 'Could not get phone number info',
details: { content: JSON.stringify(err), lang: 'json' },
details:
err instanceof Error
? { content: err.message, lang: 'json' }
: undefined,
})
return false
}

View File

@ -8,12 +8,17 @@ import {
Link,
Stack,
Text,
Code,
} from '@chakra-ui/react'
import { BubbleProps } from '@typebot.io/nextjs'
import { useState } from 'react'
import { BubbleSettings } from '../../../settings/BubbleSettings/BubbleSettings'
import { parseApiHostValue, parseInitBubbleCode } from '../../../snippetParsers'
import { parseDefaultBubbleTheme } from '../../Javascript/instructions/JavascriptBubbleInstructions'
import packageJson from '../../../../../../../../../../packages/embeds/js/package.json'
import { isCloudProdInstance } from '@/helpers/isCloudProdInstance'
const typebotCloudLibraryVersion = '0.2'
type Props = {
publicId: string
@ -52,6 +57,14 @@ export const WordpressBubbleInstructions = ({ publicId }: Props) => {
<ExternalLinkIcon mx="2px" />
</Link>
</ListItem>
<ListItem>
Set <Code>Library version</Code> to{' '}
<Code>
{isCloudProdInstance()
? typebotCloudLibraryVersion
: packageJson.version}
</Code>
</ListItem>
<ListItem>
<Stack spacing={4}>
<BubbleSettings

View File

@ -7,11 +7,16 @@ import {
Link,
Stack,
Text,
Code,
} from '@chakra-ui/react'
import { useState } from 'react'
import { PopupSettings } from '../../../settings/PopupSettings'
import { parseInitPopupCode } from '../../../snippetParsers/popup'
import { parseApiHostValue } from '../../../snippetParsers'
import { isCloudProdInstance } from '@/helpers/isCloudProdInstance'
import packageJson from '../../../../../../../../../../packages/embeds/js/package.json'
const typebotCloudLibraryVersion = '0.2'
type Props = {
publicId: string
@ -42,6 +47,14 @@ export const WordpressPopupInstructions = ({
<ExternalLinkIcon mx="2px" />
</Link>
</ListItem>
<ListItem>
Set <Code>Library version</Code> to{' '}
<Code>
{isCloudProdInstance()
? typebotCloudLibraryVersion
: packageJson.version}
</Code>
</ListItem>
<ListItem>
<Stack spacing={4}>
<PopupSettings

View File

@ -8,6 +8,7 @@ export const DefaultAvatar = (props: IconProps) => {
fill="none"
xmlns="http://www.w3.org/2000/svg"
boxSize="40px"
borderRadius="full"
data-testid="default-avatar"
{...props}
>

View File

@ -35,7 +35,7 @@ export const ThemeSideMenu = () => {
typebot &&
updateTypebot({ updates: { theme: { ...typebot.theme, customCss } } })
const selectedTemplate = (
const selectTemplate = (
selectedTemplate: Partial<Pick<ThemeTemplate, 'id' | 'theme'>>
) => {
if (!typebot) return
@ -56,6 +56,8 @@ export const ThemeSideMenu = () => {
},
})
const templateId = typebot?.selectedThemeTemplateId ?? undefined
return (
<Stack
flex="1"
@ -84,12 +86,10 @@ export const ThemeSideMenu = () => {
<AccordionPanel pb={12}>
{typebot && (
<ThemeTemplates
selectedTemplateId={
typebot.selectedThemeTemplateId ?? undefined
}
selectedTemplateId={templateId}
currentTheme={typebot.theme}
workspaceId={typebot.workspaceId}
onTemplateSelect={selectedTemplate}
onTemplateSelect={selectTemplate}
/>
)}
</AccordionPanel>
@ -106,6 +106,7 @@ export const ThemeSideMenu = () => {
<AccordionPanel pb={4}>
{typebot && (
<GeneralSettings
key={templateId}
isBrandingEnabled={
typebot.settings.general?.isBrandingEnabled ??
defaultSettings.general.isBrandingEnabled
@ -128,9 +129,11 @@ export const ThemeSideMenu = () => {
<AccordionPanel pb={4}>
{typebot && (
<ChatThemeSettings
key={templateId}
workspaceId={typebot.workspaceId}
typebotId={typebot.id}
chatTheme={typebot.theme.chat}
generalBackground={typebot.theme.general?.background}
onChatThemeChange={updateChatTheme}
/>
)}
@ -147,6 +150,7 @@ export const ThemeSideMenu = () => {
<AccordionPanel pb={4}>
{typebot && (
<CustomCssSettings
key={templateId}
customCss={typebot.theme.customCss}
onCustomCssChange={updateCustomCss}
/>

View File

@ -19,8 +19,13 @@ import { Theme, ThemeTemplate } from '@typebot.io/schemas'
import { useState } from 'react'
import { DefaultAvatar } from './DefaultAvatar'
import {
defaultButtonsBackgroundColor,
BackgroundType,
defaultTheme,
defaultGuestAvatarIsEnabled,
defaultGuestBubblesBackgroundColor,
defaultHostAvatarIsEnabled,
defaultBackgroundColor,
defaultHostBubblesBackgroundColor,
} from '@typebot.io/schemas/features/typebot/theme/constants'
import { useTranslate } from '@tolgee/react'
@ -71,28 +76,28 @@ export const ThemeTemplateCard = ({
const hostAvatar = {
isEnabled:
themeTemplate.theme.chat?.hostAvatar?.isEnabled ??
defaultTheme.chat.hostAvatar.isEnabled,
defaultHostAvatarIsEnabled,
url: themeTemplate.theme.chat?.hostAvatar?.url,
}
const hostBubbleBgColor =
themeTemplate.theme.chat?.hostBubbles?.backgroundColor ??
defaultTheme.chat.hostBubbles.backgroundColor
defaultHostBubblesBackgroundColor
const guestAvatar = {
isEnabled:
themeTemplate.theme.chat?.guestAvatar?.isEnabled ??
defaultTheme.chat.guestAvatar.isEnabled,
defaultGuestAvatarIsEnabled,
url: themeTemplate.theme.chat?.guestAvatar?.url,
}
const guestBubbleBgColor =
themeTemplate.theme.chat?.guestBubbles?.backgroundColor ??
defaultTheme.chat.guestBubbles.backgroundColor
defaultGuestBubblesBackgroundColor
const buttonBgColor =
themeTemplate.theme.chat?.buttons?.backgroundColor ??
defaultTheme.chat.buttons.backgroundColor
defaultButtonsBackgroundColor
return (
<Stack
@ -197,8 +202,7 @@ const parseBackground = (
case undefined:
case BackgroundType.COLOR:
return {
backgroundColor:
background?.content ?? defaultTheme.general.background.content,
backgroundColor: background?.content ?? defaultBackgroundColor,
}
case BackgroundType.IMAGE:
return { backgroundImage: `url(${background.content})` }

View File

@ -0,0 +1,134 @@
import { FormLabel, HStack, Stack } from '@chakra-ui/react'
import { ChatTheme, GeneralTheme } from '@typebot.io/schemas'
import React from 'react'
import {
defaultBlur,
defaultContainerBackgroundColor,
defaultContainerMaxHeight,
defaultContainerMaxWidth,
defaultDarkTextColor,
defaultLightTextColor,
defaultOpacity,
defaultRoundness,
} from '@typebot.io/schemas/features/typebot/theme/constants'
import { ContainerThemeForm } from './ContainerThemeForm'
import { NumberInput } from '@/components/inputs'
import { DropdownList } from '@/components/DropdownList'
import { isChatContainerLight } from '@typebot.io/theme/isChatContainerLight'
type Props = {
generalBackground: GeneralTheme['background']
container: ChatTheme['container']
onContainerChange: (container: ChatTheme['container'] | undefined) => void
}
export const ChatContainerForm = ({
generalBackground,
container,
onContainerChange,
}: Props) => {
const updateMaxWidth = (maxWidth?: number) =>
updateDimension('maxWidth', maxWidth, maxWidthUnit)
const updateMaxWidthUnit = (unit: string) =>
updateDimension('maxWidth', maxWidth, unit)
const updateMaxHeight = (maxHeight?: number) =>
updateDimension('maxHeight', maxHeight, maxHeightUnit)
const updateMaxHeightUnit = (unit: string) =>
updateDimension('maxHeight', maxHeight, unit)
const updateDimension = (
dimension: 'maxWidth' | 'maxHeight',
value: number | undefined,
unit: string
) =>
onContainerChange({
...container,
[dimension]: `${value}${unit}`,
})
const { value: maxWidth, unit: maxWidthUnit } = parseValueAndUnit(
container?.maxWidth ?? defaultContainerMaxWidth
)
const { value: maxHeight, unit: maxHeightUnit } = parseValueAndUnit(
container?.maxHeight ?? defaultContainerMaxHeight
)
return (
<Stack>
<HStack justifyContent="space-between">
<FormLabel mb="0" mr="0">
Max width:
</FormLabel>
<HStack>
<NumberInput
size="sm"
width="100px"
defaultValue={maxWidth}
min={0}
step={10}
withVariableButton={false}
onValueChange={updateMaxWidth}
/>
<DropdownList
size="sm"
items={['px', '%', 'vh', 'vw']}
currentItem={maxWidthUnit}
onItemSelect={updateMaxWidthUnit}
/>
</HStack>
</HStack>
<HStack justifyContent="space-between">
<FormLabel mb="0" mr="0">
Max height:
</FormLabel>
<HStack>
<NumberInput
size="sm"
width="100px"
defaultValue={maxHeight}
min={0}
step={10}
onValueChange={updateMaxHeight}
withVariableButton={false}
/>
<DropdownList
size="sm"
items={['px', '%', 'vh', 'vw']}
currentItem={maxHeightUnit}
onItemSelect={updateMaxHeightUnit}
/>
</HStack>
</HStack>
<ContainerThemeForm
theme={container}
defaultTheme={{
backgroundColor: defaultContainerBackgroundColor,
border: {
roundeness: defaultRoundness,
},
blur: defaultBlur,
opacity: defaultOpacity,
color: isChatContainerLight({
chatContainer: container,
generalBackground,
})
? defaultLightTextColor
: defaultDarkTextColor,
}}
onThemeChange={onContainerChange}
/>
</Stack>
)
}
const parseValueAndUnit = (valueWithUnit: string) => {
const value = parseFloat(valueWithUnit)
const unit = valueWithUnit.replace(value.toString(), '')
return { value, unit }
}

View File

@ -1,23 +1,36 @@
import {
LargeRadiusIcon,
MediumRadiusIcon,
NoRadiusIcon,
} from '@/components/icons'
import { RadioButtons } from '@/components/inputs/RadioButtons'
import { Heading, Stack } from '@chakra-ui/react'
import { AvatarProps, ChatTheme, Theme } from '@typebot.io/schemas'
import {
AvatarProps,
ChatTheme,
GeneralTheme,
Theme,
} from '@typebot.io/schemas'
import React from 'react'
import { AvatarForm } from './AvatarForm'
import { ButtonsTheme } from './ButtonsTheme'
import { GuestBubbles } from './GuestBubbles'
import { HostBubbles } from './HostBubbles'
import { InputsTheme } from './InputsTheme'
import { defaultTheme } from '@typebot.io/schemas/features/typebot/theme/constants'
import { useTranslate } from '@tolgee/react'
import { ChatContainerForm } from './ChatContainerForm'
import { ContainerThemeForm } from './ContainerThemeForm'
import {
defaultButtonsBackgroundColor,
defaultButtonsColor,
defaultButtonsBorderThickness,
defaultGuestBubblesBackgroundColor,
defaultGuestBubblesColor,
defaultHostBubblesBackgroundColor,
defaultHostBubblesColor,
defaultInputsBackgroundColor,
defaultInputsColor,
defaultInputsPlaceholderColor,
defaultInputsShadow,
defaultOpacity,
defaultBlur,
defaultRoundness,
} from '@typebot.io/schemas/features/typebot/theme/constants'
type Props = {
workspaceId: string
typebotId: string
generalBackground: GeneralTheme['background']
chatTheme: Theme['chat']
onChatThemeChange: (chatTheme: ChatTheme) => void
}
@ -26,6 +39,7 @@ export const ChatThemeSettings = ({
workspaceId,
typebotId,
chatTheme,
generalBackground,
onChatThemeChange,
}: Props) => {
const { t } = useTranslate()
@ -44,6 +58,16 @@ export const ChatThemeSettings = ({
const updateInputs = (inputs: NonNullable<Theme['chat']>['inputs']) =>
onChatThemeChange({ ...chatTheme, inputs })
const updateChatContainer = (
container: NonNullable<Theme['chat']>['container']
) => onChatThemeChange({ ...chatTheme, container })
const updateInputsPlaceholderColor = (placeholderColor: string) =>
onChatThemeChange({
...chatTheme,
inputs: { ...chatTheme?.inputs, placeholderColor },
})
const updateHostAvatar = (hostAvatar: AvatarProps) =>
onChatThemeChange({ ...chatTheme, hostAvatar })
@ -52,6 +76,14 @@ export const ChatThemeSettings = ({
return (
<Stack spacing={6}>
<Stack borderWidth={1} rounded="md" p="4" spacing={4}>
<Heading fontSize="lg">Container</Heading>
<ChatContainerForm
generalBackground={generalBackground}
container={chatTheme?.container}
onContainerChange={updateChatContainer}
/>
</Stack>
<AvatarForm
uploadFileProps={{
workspaceId,
@ -59,7 +91,7 @@ export const ChatThemeSettings = ({
fileName: 'hostAvatar',
}}
title={t('theme.sideMenu.chat.botAvatar')}
avatarProps={chatTheme?.hostAvatar ?? defaultTheme.chat.hostAvatar}
avatarProps={chatTheme?.hostAvatar}
isDefaultCheck
onAvatarChange={updateHostAvatar}
/>
@ -70,63 +102,83 @@ export const ChatThemeSettings = ({
fileName: 'guestAvatar',
}}
title={t('theme.sideMenu.chat.userAvatar')}
avatarProps={chatTheme?.guestAvatar ?? defaultTheme.chat.guestAvatar}
avatarProps={chatTheme?.guestAvatar}
onAvatarChange={updateGuestAvatar}
/>
<Stack borderWidth={1} rounded="md" p="4" spacing={4}>
<Heading fontSize="lg">{t('theme.sideMenu.chat.botBubbles')}</Heading>
<HostBubbles
hostBubbles={chatTheme?.hostBubbles ?? defaultTheme.chat.hostBubbles}
onHostBubblesChange={updateHostBubbles}
<ContainerThemeForm
testId="hostBubblesTheme"
theme={chatTheme?.hostBubbles}
onThemeChange={updateHostBubbles}
defaultTheme={{
backgroundColor: defaultHostBubblesBackgroundColor,
color: defaultHostBubblesColor,
opacity: defaultOpacity,
blur: defaultBlur,
border: {
roundeness: defaultRoundness,
},
}}
/>
</Stack>
<Stack borderWidth={1} rounded="md" p="4" spacing={4}>
<Heading fontSize="lg">{t('theme.sideMenu.chat.userBubbles')}</Heading>
<GuestBubbles
guestBubbles={
chatTheme?.guestBubbles ?? defaultTheme.chat.guestBubbles
}
onGuestBubblesChange={updateGuestBubbles}
<ContainerThemeForm
testId="guestBubblesTheme"
theme={chatTheme?.guestBubbles}
onThemeChange={updateGuestBubbles}
defaultTheme={{
backgroundColor: defaultGuestBubblesBackgroundColor,
color: defaultGuestBubblesColor,
opacity: defaultOpacity,
blur: defaultBlur,
border: {
roundeness: defaultRoundness,
},
}}
/>
</Stack>
<Stack borderWidth={1} rounded="md" p="4" spacing={4}>
<Heading fontSize="lg">{t('theme.sideMenu.chat.buttons')}</Heading>
<ButtonsTheme
buttons={chatTheme?.buttons ?? defaultTheme.chat.buttons}
onButtonsChange={updateButtons}
<ContainerThemeForm
testId="buttonsTheme"
theme={chatTheme?.buttons}
onThemeChange={updateButtons}
defaultTheme={{
backgroundColor: defaultButtonsBackgroundColor,
color: defaultButtonsColor,
opacity: defaultOpacity,
blur: defaultBlur,
border: {
roundeness: defaultRoundness,
thickness: defaultButtonsBorderThickness,
color:
chatTheme?.buttons?.backgroundColor ??
defaultButtonsBackgroundColor,
},
}}
/>
</Stack>
<Stack borderWidth={1} rounded="md" p="4" spacing={4}>
<Heading fontSize="lg">{t('theme.sideMenu.chat.inputs')}</Heading>
<InputsTheme
inputs={chatTheme?.inputs ?? defaultTheme.chat.inputs}
onInputsChange={updateInputs}
/>
</Stack>
<Stack borderWidth={1} rounded="md" p="4" spacing={4}>
<Heading fontSize="lg">
{t('theme.sideMenu.chat.cornersRoundness')}
</Heading>
<RadioButtons
options={[
{
label: <NoRadiusIcon />,
value: 'none',
<ContainerThemeForm
testId="inputsTheme"
theme={chatTheme?.inputs}
onThemeChange={updateInputs}
onPlaceholderColorChange={updateInputsPlaceholderColor}
defaultTheme={{
backgroundColor: defaultInputsBackgroundColor,
color: defaultInputsColor,
placeholderColor: defaultInputsPlaceholderColor,
shadow: defaultInputsShadow,
opacity: defaultOpacity,
blur: defaultBlur,
border: {
roundeness: defaultRoundness,
},
{
label: <MediumRadiusIcon />,
value: 'medium',
},
{
label: <LargeRadiusIcon />,
value: 'large',
},
]}
value={chatTheme?.roundness ?? defaultTheme.chat.roundness}
onSelect={(roundness) =>
onChatThemeChange({ ...chatTheme, roundness })
}
}}
/>
</Stack>
</Stack>

View File

@ -0,0 +1,297 @@
import {
Stack,
FormLabel,
HStack,
Switch,
Accordion,
AccordionButton,
AccordionIcon,
AccordionItem,
AccordionPanel,
} from '@chakra-ui/react'
import {
ContainerTheme,
ContainerBorderTheme,
InputTheme,
} from '@typebot.io/schemas'
import React from 'react'
import { ColorPicker } from '../../../../components/ColorPicker'
import { useTranslate } from '@tolgee/react'
import { NumberInput } from '@/components/inputs'
import { DropdownList } from '@/components/DropdownList'
import {
borderRoundness,
defaultOpacity,
shadows,
} from '@typebot.io/schemas/features/typebot/theme/constants'
type Props<T extends ((placeholder: string) => void) | undefined> = {
theme: (T extends undefined ? ContainerTheme : InputTheme) | undefined
defaultTheme: T extends undefined ? ContainerTheme : InputTheme
placeholderColor?: T extends undefined ? never : string
testId?: string
onThemeChange: (
theme: T extends undefined ? ContainerTheme : InputTheme
) => void
onPlaceholderColorChange?: T
}
export const ContainerThemeForm = <
T extends ((placeholder: string) => void) | undefined
>({
theme,
testId,
defaultTheme,
onPlaceholderColorChange,
onThemeChange,
}: Props<T>) => {
const { t } = useTranslate()
const updateBackgroundColor = (backgroundColor: string) =>
onThemeChange({ ...theme, backgroundColor })
const toggleBackgroundColor = () =>
onThemeChange({
...theme,
backgroundColor:
backgroundColor === 'transparent' ? '#ffffff' : 'transparent',
})
const updateTextColor = (color: string) => onThemeChange({ ...theme, color })
const updateShadow = (shadow?: ContainerTheme['shadow']) =>
onThemeChange({ ...theme, shadow })
const updateBlur = (blur?: number) => onThemeChange({ ...theme, blur })
const updateOpacity = (opacity?: number) =>
onThemeChange({ ...theme, opacity })
const updateBorder = (border: ContainerBorderTheme) =>
onThemeChange({ ...theme, border })
const updatePlaceholderColor = (color: string) =>
onThemeChange({ ...theme, placeholderColor: color } as InputTheme)
const backgroundColor =
theme?.backgroundColor ?? defaultTheme?.backgroundColor
const shadow = theme?.shadow ?? defaultTheme?.shadow ?? 'none'
return (
<Stack spacing={4} data-testid={testId}>
<HStack justify="space-between">
<FormLabel mb="0" mr="0">
{t('theme.sideMenu.chat.theme.background')}
</FormLabel>
<HStack>
<Switch
defaultChecked={backgroundColor !== 'transparent'}
onChange={toggleBackgroundColor}
/>
<ColorPicker
isDisabled={backgroundColor === 'transparent'}
value={backgroundColor}
onColorChange={updateBackgroundColor}
/>
</HStack>
</HStack>
<HStack justify="space-between">
<FormLabel mb="0" mr="0">
{t('theme.sideMenu.chat.theme.text')}
</FormLabel>
<ColorPicker
value={theme?.color ?? defaultTheme?.color}
onColorChange={updateTextColor}
/>
</HStack>
{onPlaceholderColorChange && (
<HStack justify="space-between">
<FormLabel mb="0" mr="0">
{t('theme.sideMenu.chat.theme.placeholder')}
</FormLabel>
<ColorPicker
value={
theme && 'placeholderColor' in theme
? theme.placeholderColor
: defaultTheme && 'placeholderColor' in defaultTheme
? defaultTheme.placeholderColor
: undefined
}
onColorChange={updatePlaceholderColor}
/>
</HStack>
)}
<Accordion allowToggle>
<AccordionItem>
<AccordionButton justifyContent="space-between">
Border
<AccordionIcon />
</AccordionButton>
<AccordionPanel>
<BorderThemeForm
border={theme?.border}
defaultBorder={defaultTheme.border}
onBorderChange={updateBorder}
/>
</AccordionPanel>
</AccordionItem>
<AccordionItem>
<AccordionButton justifyContent="space-between">
Advanced
<AccordionIcon />
</AccordionButton>
<AccordionPanel as={Stack}>
{backgroundColor !== 'transparent' && (
<>
<NumberInput
size="sm"
direction="row"
label="Opacity:"
width="100px"
min={0}
max={1}
step={0.1}
defaultValue={theme?.opacity ?? defaultTheme?.opacity}
onValueChange={updateOpacity}
withVariableButton={false}
/>
{(theme?.opacity ?? defaultTheme?.opacity) !== 1 && (
<NumberInput
size="sm"
direction="row"
label="Blur:"
suffix="px"
width="100px"
min={0}
defaultValue={theme?.blur ?? defaultTheme?.blur}
onValueChange={updateBlur}
withVariableButton={false}
/>
)}
</>
)}
<HStack justify="space-between">
<FormLabel mb="0" mr="0">
Shadow:
</FormLabel>
<HStack>
<DropdownList
currentItem={shadow}
onItemSelect={updateShadow}
items={shadows}
size="sm"
/>
</HStack>
</HStack>
</AccordionPanel>
</AccordionItem>
</Accordion>
</Stack>
)
}
const BorderThemeForm = ({
border,
defaultBorder,
onBorderChange,
}: {
border: ContainerBorderTheme | undefined
defaultBorder: ContainerBorderTheme | undefined
onBorderChange: (border: ContainerBorderTheme) => void
}) => {
const updateRoundness = (roundeness: (typeof borderRoundness)[number]) => {
onBorderChange({ ...border, roundeness })
}
const updateCustomRoundeness = (customRoundeness: number | undefined) => {
onBorderChange({ ...border, customRoundeness })
}
const updateThickness = (thickness: number | undefined) => {
onBorderChange({ ...border, thickness })
}
const updateColor = (color: string | undefined) => {
onBorderChange({ ...border, color })
}
const updateOpacity = (opacity: number | undefined) => {
onBorderChange({ ...border, opacity })
}
const thickness = border?.thickness ?? defaultBorder?.thickness ?? 0
return (
<Stack>
<HStack justifyContent="space-between">
<FormLabel mb="0" mr="0">
Roundness:
</FormLabel>
<HStack>
<DropdownList
currentItem={border?.roundeness ?? defaultBorder?.roundeness}
onItemSelect={updateRoundness}
items={borderRoundness}
placeholder="md"
size="sm"
/>
{(border?.roundeness ?? defaultBorder?.roundeness) === 'custom' && (
<NumberInput
size="sm"
suffix="px"
width="60px"
min={0}
defaultValue={border?.customRoundeness}
onValueChange={updateCustomRoundeness}
withVariableButton={false}
/>
)}
</HStack>
</HStack>
<HStack justifyContent="space-between">
<FormLabel mb="0" mr="0">
Thickness:
</FormLabel>
<NumberInput
size="sm"
suffix="px"
width="60px"
min={0}
defaultValue={thickness}
onValueChange={updateThickness}
withVariableButton={false}
/>
</HStack>
{thickness > 0 && (
<>
<HStack justifyContent="space-between">
<FormLabel mb="0" mr="0">
Color:
</FormLabel>
<ColorPicker
value={border?.color ?? defaultBorder?.color}
onColorChange={updateColor}
/>
</HStack>
<NumberInput
size="sm"
direction="row"
label="Opacity:"
width="100px"
min={0}
max={1}
step={0.1}
defaultValue={border?.opacity ?? defaultOpacity}
onValueChange={updateOpacity}
withVariableButton={false}
/>
</>
)}
</Stack>
)
}

View File

@ -1,13 +1,16 @@
import { Stack, Flex, Text } from '@chakra-ui/react'
import { ContainerColors } from '@typebot.io/schemas'
import { ContainerTheme } from '@typebot.io/schemas'
import React from 'react'
import { ColorPicker } from '../../../../components/ColorPicker'
import { defaultTheme } from '@typebot.io/schemas/features/typebot/theme/constants'
import { useTranslate } from '@tolgee/react'
import {
defaultGuestBubblesBackgroundColor,
defaultGuestBubblesColor,
} from '@typebot.io/schemas/features/typebot/theme/constants'
type Props = {
guestBubbles: ContainerColors | undefined
onGuestBubblesChange: (hostBubbles: ContainerColors | undefined) => void
guestBubbles: ContainerTheme | undefined
onGuestBubblesChange: (hostBubbles: ContainerTheme | undefined) => void
}
export const GuestBubbles = ({ guestBubbles, onGuestBubblesChange }: Props) => {
@ -25,8 +28,7 @@ export const GuestBubbles = ({ guestBubbles, onGuestBubblesChange }: Props) => {
<Text>{t('theme.sideMenu.chat.theme.background')}</Text>
<ColorPicker
value={
guestBubbles?.backgroundColor ??
defaultTheme.chat.guestBubbles.backgroundColor
guestBubbles?.backgroundColor ?? defaultGuestBubblesBackgroundColor
}
onColorChange={updateBackground}
/>
@ -34,7 +36,7 @@ export const GuestBubbles = ({ guestBubbles, onGuestBubblesChange }: Props) => {
<Flex justify="space-between" align="center">
<Text>{t('theme.sideMenu.chat.theme.text')}</Text>
<ColorPicker
value={guestBubbles?.color ?? defaultTheme.chat.guestBubbles.color}
value={guestBubbles?.color ?? defaultGuestBubblesColor}
onColorChange={updateText}
/>
</Flex>

View File

@ -1,13 +1,16 @@
import { Stack, Flex, Text } from '@chakra-ui/react'
import { ContainerColors } from '@typebot.io/schemas'
import { ContainerTheme } from '@typebot.io/schemas'
import React from 'react'
import { ColorPicker } from '../../../../components/ColorPicker'
import { defaultTheme } from '@typebot.io/schemas/features/typebot/theme/constants'
import { useTranslate } from '@tolgee/react'
import {
defaultHostBubblesBackgroundColor,
defaultHostBubblesColor,
} from '@typebot.io/schemas/features/typebot/theme/constants'
type Props = {
hostBubbles: ContainerColors | undefined
onHostBubblesChange: (hostBubbles: ContainerColors | undefined) => void
hostBubbles: ContainerTheme | undefined
onHostBubblesChange: (hostBubbles: ContainerTheme | undefined) => void
}
export const HostBubbles = ({ hostBubbles, onHostBubblesChange }: Props) => {
@ -24,8 +27,7 @@ export const HostBubbles = ({ hostBubbles, onHostBubblesChange }: Props) => {
<Text>{t('theme.sideMenu.chat.theme.background')}</Text>
<ColorPicker
value={
hostBubbles?.backgroundColor ??
defaultTheme.chat.hostBubbles.backgroundColor
hostBubbles?.backgroundColor ?? defaultHostBubblesBackgroundColor
}
onColorChange={handleBackgroundChange}
/>
@ -33,7 +35,7 @@ export const HostBubbles = ({ hostBubbles, onHostBubblesChange }: Props) => {
<Flex justify="space-between" align="center">
<Text>{t('theme.sideMenu.chat.theme.text')}</Text>
<ColorPicker
value={hostBubbles?.color ?? defaultTheme.chat.hostBubbles.color}
value={hostBubbles?.color ?? defaultHostBubblesColor}
onColorChange={handleTextChange}
/>
</Flex>

View File

@ -1,44 +0,0 @@
import { Stack, Flex, Text } from '@chakra-ui/react'
import { InputColors, Theme } from '@typebot.io/schemas'
import React from 'react'
import { ColorPicker } from '../../../../components/ColorPicker'
import { useTranslate } from '@tolgee/react'
type Props = {
inputs: NonNullable<Theme['chat']>['inputs']
onInputsChange: (buttons: InputColors) => void
}
export const InputsTheme = ({ inputs, onInputsChange }: Props) => {
const { t } = useTranslate()
const handleBackgroundChange = (backgroundColor: string) =>
onInputsChange({ ...inputs, backgroundColor })
const handleTextChange = (color: string) =>
onInputsChange({ ...inputs, color })
const handlePlaceholderChange = (placeholderColor: string) =>
onInputsChange({ ...inputs, placeholderColor })
return (
<Stack data-testid="inputs-theme">
<Flex justify="space-between" align="center">
<Text>{t('theme.sideMenu.chat.theme.background')}</Text>
<ColorPicker
value={inputs?.backgroundColor}
onColorChange={handleBackgroundChange}
/>
</Flex>
<Flex justify="space-between" align="center">
<Text>{t('theme.sideMenu.chat.theme.text')}</Text>
<ColorPicker value={inputs?.color} onColorChange={handleTextChange} />
</Flex>
<Flex justify="space-between" align="center">
<Text>{t('theme.sideMenu.chat.theme.placeholder')}</Text>
<ColorPicker
value={inputs?.placeholderColor}
onColorChange={handlePlaceholderChange}
/>
</Flex>
</Stack>
)
}

View File

@ -16,7 +16,8 @@ import React from 'react'
import { ColorPicker } from '../../../../components/ColorPicker'
import {
BackgroundType,
defaultTheme,
defaultBackgroundColor,
defaultBackgroundType,
} from '@typebot.io/schemas/features/typebot/theme/constants'
import { useTranslate } from '@tolgee/react'
@ -34,10 +35,7 @@ export const BackgroundContent = ({
const handleContentChange = (content: string) =>
onBackgroundContentChange(content)
if (
(background?.type ?? defaultTheme.general.background.type) ===
BackgroundType.IMAGE
) {
if ((background?.type ?? defaultBackgroundType) === BackgroundType.IMAGE) {
if (!typebot) return null
return (
<Popover isLazy placement="top">
@ -76,15 +74,12 @@ export const BackgroundContent = ({
</Popover>
)
}
if (
(background?.type ?? defaultTheme.general.background.type) ===
BackgroundType.COLOR
) {
if ((background?.type ?? defaultBackgroundType) === 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}
value={background?.content ?? defaultBackgroundColor}
onColorChange={handleContentChange}
/>
</Flex>

View File

@ -1,11 +1,11 @@
import { RadioButtons } from '@/components/inputs/RadioButtons'
import { Stack, Text } from '@chakra-ui/react'
import { Stack } from '@chakra-ui/react'
import { Background } from '@typebot.io/schemas'
import React from 'react'
import { BackgroundContent } from './BackgroundContent'
import {
BackgroundType,
defaultTheme,
defaultBackgroundType,
} from '@typebot.io/schemas/features/typebot/theme/constants'
import { useTranslate } from '@tolgee/react'
@ -28,7 +28,6 @@ export const BackgroundSelector = ({
return (
<Stack spacing={4}>
<Text>{t('theme.sideMenu.global.background')}</Text>
<RadioButtons
options={[
{
@ -44,7 +43,7 @@ export const BackgroundSelector = ({
value: BackgroundType.NONE,
},
]}
value={background?.type ?? defaultTheme.general.background.type}
value={background?.type ?? defaultBackgroundType}
onSelect={handleBackgroundTypeChange}
/>
<BackgroundContent

View File

@ -4,7 +4,11 @@ import {
Stack,
Switch,
useDisclosure,
Text,
Accordion,
AccordionItem,
AccordionButton,
AccordionPanel,
AccordionIcon,
} from '@chakra-ui/react'
import { Background, Font, ProgressBar, Theme } from '@typebot.io/schemas'
import React from 'react'
@ -16,7 +20,7 @@ import { useWorkspace } from '@/features/workspace/WorkspaceProvider'
import { ChangePlanModal } from '@/features/billing/components/ChangePlanModal'
import { useTranslate } from '@tolgee/react'
import {
defaultTheme,
defaultFontType,
fontTypes,
} from '@typebot.io/schemas/features/typebot/theme/constants'
import { trpc } from '@/lib/trpc'
@ -91,7 +95,7 @@ export const GeneralSettings = ({
const fontType =
(typeof generalTheme?.font === 'string'
? 'Google'
: generalTheme?.font?.type) ?? defaultTheme.general.font.type
: generalTheme?.font?.type) ?? defaultFontType
return (
<Stack spacing={6}>
@ -115,23 +119,46 @@ export const GeneralSettings = ({
onChange={updateBranding}
/>
</Flex>
<ProgressBarForm
progressBar={generalTheme?.progressBar}
onProgressBarChange={updateProgressBar}
/>
<Stack>
<Text>{t('theme.sideMenu.global.font')}</Text>
<RadioButtons
options={fontTypes}
defaultValue={fontType}
onSelect={updateFontType}
/>
<FontForm font={generalTheme?.font} onFontChange={updateFont} />
</Stack>
<BackgroundSelector
background={generalTheme?.background}
onBackgroundChange={handleBackgroundChange}
/>
<Accordion allowToggle>
<AccordionItem>
<AccordionButton justifyContent="space-between">
Progress Bar
<AccordionIcon />
</AccordionButton>
<AccordionPanel>
<ProgressBarForm
progressBar={generalTheme?.progressBar}
onProgressBarChange={updateProgressBar}
/>
</AccordionPanel>
</AccordionItem>
<AccordionItem>
<AccordionButton justifyContent="space-between">
{t('theme.sideMenu.global.font')}
<AccordionIcon />
</AccordionButton>
<AccordionPanel as={Stack}>
<RadioButtons
options={fontTypes}
defaultValue={fontType}
onSelect={updateFontType}
/>
<FontForm font={generalTheme?.font} onFontChange={updateFont} />
</AccordionPanel>
</AccordionItem>
<AccordionItem>
<AccordionButton justifyContent="space-between">
{t('theme.sideMenu.global.background')}
<AccordionIcon />
</AccordionButton>
<AccordionPanel>
<BackgroundSelector
background={generalTheme?.background}
onBackgroundChange={handleBackgroundChange}
/>
</AccordionPanel>
</AccordionItem>
</Accordion>
</Stack>
)
}

View File

@ -1,7 +1,7 @@
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 { defaultFontFamily } from '@typebot.io/schemas/features/typebot/theme/constants'
import { useState, useEffect } from 'react'
type Props = {
@ -11,8 +11,7 @@ type Props = {
export const GoogleFontForm = ({ font, onFontChange }: Props) => {
const [currentFont, setCurrentFont] = useState(
(typeof font === 'string' ? font : font?.family) ??
defaultTheme.general.font.family
(typeof font === 'string' ? font : font?.family) ?? defaultFontFamily
)
const [googleFonts, setGoogleFonts] = useState<string[]>([])

View File

@ -5,7 +5,12 @@ import { NumberInput } from '@/components/inputs'
import { FormLabel, HStack } from '@chakra-ui/react'
import { ProgressBar } from '@typebot.io/schemas'
import {
defaultTheme,
defaultProgressBarBackgroundColor,
defaultProgressBarColor,
defaultProgressBarIsEnabled,
defaultProgressBarPlacement,
defaultProgressBarPosition,
defaultProgressBarThickness,
progressBarPlacements,
progressBarPositions,
} from '@typebot.io/schemas/features/typebot/theme/constants'
@ -34,33 +39,41 @@ export const ProgressBarForm = ({
const updateThickness = (thickness?: number) =>
onProgressBarChange({ ...progressBar, thickness })
const updateBackgroundColor = (backgroundColor: string) =>
onProgressBarChange({ ...progressBar, backgroundColor })
return (
<SwitchWithRelatedSettings
label={'Enable progress bar?'}
initialValue={
progressBar?.isEnabled ?? defaultTheme.general.progressBar.isEnabled
}
initialValue={progressBar?.isEnabled ?? defaultProgressBarIsEnabled}
onCheckChange={updateEnabled}
>
<DropdownList
size="sm"
direction="row"
label="Placement:"
currentItem={
progressBar?.placement ?? defaultTheme.general.progressBar.placement
}
currentItem={progressBar?.placement ?? defaultProgressBarPlacement}
onItemSelect={updatePlacement}
items={progressBarPlacements}
/>
<HStack justifyContent="space-between">
<FormLabel mb="0" mr="0">
Color:
Background color:
</FormLabel>
<ColorPicker
defaultValue={
progressBar?.color ?? defaultTheme.general.progressBar.color
progressBar?.backgroundColor ?? defaultProgressBarBackgroundColor
}
onColorChange={updateBackgroundColor}
/>
</HStack>
<HStack justifyContent="space-between">
<FormLabel mb="0" mr="0">
Color:
</FormLabel>
<ColorPicker
defaultValue={progressBar?.color ?? defaultProgressBarColor}
onColorChange={updateColor}
/>
</HStack>
@ -69,9 +82,7 @@ export const ProgressBarForm = ({
direction="row"
withVariableButton={false}
maxW="100px"
defaultValue={
progressBar?.thickness ?? defaultTheme.general.progressBar.thickness
}
defaultValue={progressBar?.thickness ?? defaultProgressBarThickness}
onValueChange={updateThickness}
size="sm"
/>
@ -80,9 +91,7 @@ export const ProgressBarForm = ({
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
}
currentItem={progressBar?.position ?? defaultProgressBarPosition}
onItemSelect={updatePosition}
items={progressBarPositions}
/>

View File

@ -3,6 +3,10 @@ import test, { expect } from '@playwright/test'
import { createId } from '@paralleldrive/cuid2'
import { importTypebotInDatabase } from '@typebot.io/playwright/databaseActions'
import { freeWorkspaceId } from '@typebot.io/playwright/databaseSetup'
import {
defaultContainerMaxHeight,
defaultContainerMaxWidth,
} from '@typebot.io/schemas/features/typebot/theme/constants'
const hostAvatarUrl =
'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1760&q=80'
@ -30,6 +34,7 @@ test.describe.parallel('Theme page', () => {
await expect(page.locator('a:has-text("Made with Typebot")')).toBeHidden()
// Font
await page.getByRole('button', { name: 'Font' }).click()
await page.getByRole('textbox').fill('Roboto Slab')
await page.getByRole('menuitem', { name: 'Roboto Slab' }).click()
await expect(page.locator('.typebot-container')).toHaveCSS(
@ -42,6 +47,7 @@ test.describe.parallel('Theme page', () => {
'background-color',
'rgba(0, 0, 0, 0)'
)
await page.getByRole('button', { name: 'Background' }).click()
await page.click('text=Color')
await page.waitForTimeout(100)
await page.getByRole('button', { name: 'Pick a color' }).click()
@ -82,6 +88,38 @@ test.describe.parallel('Theme page', () => {
await expect(page.getByRole('button', { name: 'Go' })).toBeVisible()
await page.click('button:has-text("Chat")')
// Container
await expect(page.locator('.typebot-chat-view')).toHaveCSS(
'max-width',
defaultContainerMaxWidth
)
await page
.locator('div')
.filter({ hasText: /^Max width:px$/ })
.getByRole('spinbutton')
.fill('600')
await expect(page.locator('.typebot-chat-view')).toHaveCSS(
'max-width',
'600px'
)
await expect(page.locator('.typebot-chat-view')).toHaveCSS(
'max-height',
defaultContainerMaxHeight
)
await page
.locator('div')
.filter({ hasText: /^Max height:%$/ })
.getByRole('spinbutton')
.fill('80')
await expect(page.locator('.typebot-chat-view')).toHaveCSS(
'max-height',
'80%'
)
await expect(page.locator('.typebot-chat-view')).toHaveCSS(
'color',
'rgb(48, 50, 53)'
)
// Host avatar
await expect(
page.locator('[data-testid="default-avatar"]').nth(1)
@ -102,39 +140,13 @@ test.describe.parallel('Theme page', () => {
await expect(page.locator('.typebot-container img')).toBeHidden()
// Roundness
await expect(page.getByRole('button', { name: 'Go' })).toHaveCSS(
'border-radius',
'6px'
)
await page
.getByRole('region', { name: 'Chat' })
.getByRole('radiogroup')
.locator('div')
.first()
.click()
await expect(page.getByRole('button', { name: 'Go' })).toHaveCSS(
'border-radius',
'0px'
)
await page
.getByRole('region', { name: 'Chat' })
.getByRole('radiogroup')
.locator('div')
.nth(2)
.click()
await expect(page.getByRole('button', { name: 'Go' })).toHaveCSS(
'border-radius',
'20px'
)
// Host bubbles
await page.click(
'[data-testid="host-bubbles-theme"] >> [aria-label="Pick a color"] >> nth=0'
'[data-testid="hostBubblesTheme"] >> [aria-label="Pick a color"] >> nth=0'
)
await page.fill('input[value="#F7F8FF"]', '#2a9d8f')
await page.click(
'[data-testid="host-bubbles-theme"] >> [aria-label="Pick a color"] >> nth=1'
'[data-testid="hostBubblesTheme"] >> [aria-label="Pick a color"] >> nth=1'
)
await page.fill('input[value="#303235"]', '#ffffff')
const hostBubble = page.locator('[data-testid="host-bubble"] >> nth=-1')
@ -146,11 +158,11 @@ test.describe.parallel('Theme page', () => {
// Buttons
await page.click(
'[data-testid="buttons-theme"] >> [aria-label="Pick a color"] >> nth=0'
'[data-testid="buttonsTheme"] >> [aria-label="Pick a color"] >> nth=0'
)
await page.fill('input[value="#0042DA"]', '#7209b7')
await page.click(
'[data-testid="buttons-theme"] >> [aria-label="Pick a color"] >> nth=1'
'[data-testid="buttonsTheme"] >> [aria-label="Pick a color"] >> nth=1'
)
await page.fill('input[value="#FFFFFF"]', '#e9c46a')
const button = page.getByRole('button', { name: 'Go' })
@ -159,11 +171,11 @@ test.describe.parallel('Theme page', () => {
// Guest bubbles
await page.click(
'[data-testid="guest-bubbles-theme"] >> [aria-label="Pick a color"] >> nth=0'
'[data-testid="guestBubblesTheme"] >> [aria-label="Pick a color"] >> nth=0'
)
await page.fill('input[value="#FF8E21"]', '#d8f3dc')
await page.click(
'[data-testid="guest-bubbles-theme"] >> [aria-label="Pick a color"] >> nth=1'
'[data-testid="guestBubblesTheme"] >> [aria-label="Pick a color"] >> nth=1'
)
await page.fill('input[value="#FFFFFF"]', '#264653')
await page.getByRole('button', { name: 'Go' }).click()
@ -192,11 +204,11 @@ test.describe.parallel('Theme page', () => {
await page.waitForTimeout(1000)
// Input
await page.click(
'[data-testid="inputs-theme"] >> [aria-label="Pick a color"] >> nth=0'
'[data-testid="inputsTheme"] >> [aria-label="Pick a color"] >> nth=0'
)
await page.fill('input[value="#FFFFFF"]', '#ffe8d6')
await page.click(
'[data-testid="inputs-theme"] >> [aria-label="Pick a color"] >> nth=1'
'[data-testid="inputsTheme"] >> [aria-label="Pick a color"] >> nth=1'
)
await page.fill('input[value="#303235"]', '#023e8a')
const input = page.locator('.typebot-input')

View File

@ -143,7 +143,7 @@
"blocks.inputs.file.settings.skip.label": "Skip button label:",
"blocks.inputs.fileUpload.blockCard.tooltip": "Upload Files",
"blocks.inputs.number.settings.step.label": "Step:",
"blocks.inputs.payment.collect.label": "Coletar",
"blocks.inputs.payment.collect.label": "Collect",
"blocks.inputs.payment.placeholder.label": "Configure...",
"blocks.inputs.payment.settings.account.label": "Account:",
"blocks.inputs.payment.settings.accountText.label": "{provider} account",

View File

@ -173,7 +173,12 @@ export const getAuthOptions = ({
if (disposableEmailDomains.includes(user.email.split('@')[1]))
return false
}
if (env.DISABLE_SIGNUP && isNewUser && user.email) {
if (
env.DISABLE_SIGNUP &&
isNewUser &&
user.email &&
!env.ADMIN_EMAIL?.includes(user.email)
) {
const { invitations, workspaceInvitations } =
await getNewUserInvitations(prisma, user.email)
if (invitations.length === 0 && workspaceInvitations.length === 0)

View File

@ -21342,6 +21342,70 @@
"chat": {
"type": "object",
"properties": {
"container": {
"type": "object",
"properties": {
"maxWidth": {
"type": "string"
},
"maxHeight": {
"type": "string"
},
"backgroundColor": {
"type": "string"
},
"color": {
"type": "string"
},
"blur": {
"type": "number"
},
"opacity": {
"type": "number",
"minimum": 0,
"maximum": 1
},
"shadow": {
"type": "string",
"enum": [
"none",
"sm",
"md",
"lg",
"xl",
"2xl"
]
},
"border": {
"type": "object",
"properties": {
"thickness": {
"type": "number"
},
"color": {
"type": "string"
},
"roundeness": {
"type": "string",
"enum": [
"none",
"medium",
"large",
"custom"
]
},
"customRoundeness": {
"type": "number"
},
"opacity": {
"type": "number",
"minimum": 0,
"maximum": 1
}
}
}
}
},
"hostAvatar": {
"type": "object",
"properties": {
@ -21372,6 +21436,53 @@
},
"color": {
"type": "string"
},
"blur": {
"type": "number"
},
"opacity": {
"type": "number",
"minimum": 0,
"maximum": 1
},
"shadow": {
"type": "string",
"enum": [
"none",
"sm",
"md",
"lg",
"xl",
"2xl"
]
},
"border": {
"type": "object",
"properties": {
"thickness": {
"type": "number"
},
"color": {
"type": "string"
},
"roundeness": {
"type": "string",
"enum": [
"none",
"medium",
"large",
"custom"
]
},
"customRoundeness": {
"type": "number"
},
"opacity": {
"type": "number",
"minimum": 0,
"maximum": 1
}
}
}
}
},
@ -21383,6 +21494,53 @@
},
"color": {
"type": "string"
},
"blur": {
"type": "number"
},
"opacity": {
"type": "number",
"minimum": 0,
"maximum": 1
},
"shadow": {
"type": "string",
"enum": [
"none",
"sm",
"md",
"lg",
"xl",
"2xl"
]
},
"border": {
"type": "object",
"properties": {
"thickness": {
"type": "number"
},
"color": {
"type": "string"
},
"roundeness": {
"type": "string",
"enum": [
"none",
"medium",
"large",
"custom"
]
},
"customRoundeness": {
"type": "number"
},
"opacity": {
"type": "number",
"minimum": 0,
"maximum": 1
}
}
}
}
},
@ -21394,6 +21552,53 @@
},
"color": {
"type": "string"
},
"blur": {
"type": "number"
},
"opacity": {
"type": "number",
"minimum": 0,
"maximum": 1
},
"shadow": {
"type": "string",
"enum": [
"none",
"sm",
"md",
"lg",
"xl",
"2xl"
]
},
"border": {
"type": "object",
"properties": {
"thickness": {
"type": "number"
},
"color": {
"type": "string"
},
"roundeness": {
"type": "string",
"enum": [
"none",
"medium",
"large",
"custom"
]
},
"customRoundeness": {
"type": "number"
},
"opacity": {
"type": "number",
"minimum": 0,
"maximum": 1
}
}
}
}
},
@ -21406,6 +21611,53 @@
"color": {
"type": "string"
},
"blur": {
"type": "number"
},
"opacity": {
"type": "number",
"minimum": 0,
"maximum": 1
},
"shadow": {
"type": "string",
"enum": [
"none",
"sm",
"md",
"lg",
"xl",
"2xl"
]
},
"border": {
"type": "object",
"properties": {
"thickness": {
"type": "number"
},
"color": {
"type": "string"
},
"roundeness": {
"type": "string",
"enum": [
"none",
"medium",
"large",
"custom"
]
},
"customRoundeness": {
"type": "number"
},
"opacity": {
"type": "number",
"minimum": 0,
"maximum": 1
}
}
},
"placeholderColor": {
"type": "string"
}
@ -21417,7 +21669,8 @@
"none",
"medium",
"large"
]
],
"description": "Deprecated, use `container.border.roundeness` instead"
}
}
},

View File

@ -6821,6 +6821,70 @@
"chat": {
"type": "object",
"properties": {
"container": {
"type": "object",
"properties": {
"maxWidth": {
"type": "string"
},
"maxHeight": {
"type": "string"
},
"backgroundColor": {
"type": "string"
},
"color": {
"type": "string"
},
"blur": {
"type": "number"
},
"opacity": {
"type": "number",
"minimum": 0,
"maximum": 1
},
"shadow": {
"type": "string",
"enum": [
"none",
"sm",
"md",
"lg",
"xl",
"2xl"
]
},
"border": {
"type": "object",
"properties": {
"thickness": {
"type": "number"
},
"color": {
"type": "string"
},
"roundeness": {
"type": "string",
"enum": [
"none",
"medium",
"large",
"custom"
]
},
"customRoundeness": {
"type": "number"
},
"opacity": {
"type": "number",
"minimum": 0,
"maximum": 1
}
}
}
}
},
"hostAvatar": {
"type": "object",
"properties": {
@ -6851,6 +6915,53 @@
},
"color": {
"type": "string"
},
"blur": {
"type": "number"
},
"opacity": {
"type": "number",
"minimum": 0,
"maximum": 1
},
"shadow": {
"type": "string",
"enum": [
"none",
"sm",
"md",
"lg",
"xl",
"2xl"
]
},
"border": {
"type": "object",
"properties": {
"thickness": {
"type": "number"
},
"color": {
"type": "string"
},
"roundeness": {
"type": "string",
"enum": [
"none",
"medium",
"large",
"custom"
]
},
"customRoundeness": {
"type": "number"
},
"opacity": {
"type": "number",
"minimum": 0,
"maximum": 1
}
}
}
}
},
@ -6862,6 +6973,53 @@
},
"color": {
"type": "string"
},
"blur": {
"type": "number"
},
"opacity": {
"type": "number",
"minimum": 0,
"maximum": 1
},
"shadow": {
"type": "string",
"enum": [
"none",
"sm",
"md",
"lg",
"xl",
"2xl"
]
},
"border": {
"type": "object",
"properties": {
"thickness": {
"type": "number"
},
"color": {
"type": "string"
},
"roundeness": {
"type": "string",
"enum": [
"none",
"medium",
"large",
"custom"
]
},
"customRoundeness": {
"type": "number"
},
"opacity": {
"type": "number",
"minimum": 0,
"maximum": 1
}
}
}
}
},
@ -6873,6 +7031,53 @@
},
"color": {
"type": "string"
},
"blur": {
"type": "number"
},
"opacity": {
"type": "number",
"minimum": 0,
"maximum": 1
},
"shadow": {
"type": "string",
"enum": [
"none",
"sm",
"md",
"lg",
"xl",
"2xl"
]
},
"border": {
"type": "object",
"properties": {
"thickness": {
"type": "number"
},
"color": {
"type": "string"
},
"roundeness": {
"type": "string",
"enum": [
"none",
"medium",
"large",
"custom"
]
},
"customRoundeness": {
"type": "number"
},
"opacity": {
"type": "number",
"minimum": 0,
"maximum": 1
}
}
}
}
},
@ -6885,6 +7090,53 @@
"color": {
"type": "string"
},
"blur": {
"type": "number"
},
"opacity": {
"type": "number",
"minimum": 0,
"maximum": 1
},
"shadow": {
"type": "string",
"enum": [
"none",
"sm",
"md",
"lg",
"xl",
"2xl"
]
},
"border": {
"type": "object",
"properties": {
"thickness": {
"type": "number"
},
"color": {
"type": "string"
},
"roundeness": {
"type": "string",
"enum": [
"none",
"medium",
"large",
"custom"
]
},
"customRoundeness": {
"type": "number"
},
"opacity": {
"type": "number",
"minimum": 0,
"maximum": 1
}
}
},
"placeholderColor": {
"type": "string"
}
@ -6896,7 +7148,8 @@
"none",
"medium",
"large"
]
],
"description": "Deprecated, use `container.border.roundeness` instead"
}
}
},

View File

@ -43,7 +43,7 @@ cp .env.example .env
<Note>
The database user should have the `SUPERUSER` role. You can setup and migrate
the database with the `pnpm prisma generate && pnpm db:migrate` command.
the database with the `pnpm db:migrate` command.
</Note>
3. Install dependencies

View File

@ -7,8 +7,11 @@ import { TypebotPageProps, TypebotPageV2 } from '@/components/TypebotPageV2'
import { TypebotPageV3, TypebotV3PageProps } from '@/components/TypebotPageV3'
import { env } from '@typebot.io/env'
import prisma from '@typebot.io/lib/prisma'
import { defaultTheme } from '@typebot.io/schemas/features/typebot/theme/constants'
import { defaultSettings } from '@typebot.io/schemas/features/typebot/settings/constants'
import {
defaultBackgroundColor,
defaultBackgroundType,
} from '@typebot.io/schemas/features/typebot/theme/constants'
// Browsers that doesn't support ES modules and/or web components
const incompatibleBrowsers = [
@ -109,9 +112,10 @@ const getTypebotFromPublicId = async (publicId?: string) => {
? ({
name: publishedTypebot.typebot.name,
publicId: publishedTypebot.typebot.publicId ?? null,
background:
publishedTypebot.theme.general?.background ??
defaultTheme.general.background,
background: publishedTypebot.theme.general?.background ?? {
type: defaultBackgroundType,
content: defaultBackgroundColor,
},
isHideQueryParamsEnabled:
publishedTypebot.settings.general?.isHideQueryParamsEnabled ??
defaultSettings.general.isHideQueryParamsEnabled,
@ -156,9 +160,10 @@ const getTypebotFromCustomDomain = async (customDomain: string) => {
? ({
name: publishedTypebot.typebot.name,
publicId: publishedTypebot.typebot.publicId ?? null,
background:
publishedTypebot.theme.general?.background ??
defaultTheme.general.background,
background: publishedTypebot.theme.general?.background ?? {
type: defaultBackgroundType,
content: defaultBackgroundColor,
},
isHideQueryParamsEnabled:
publishedTypebot.settings.general?.isHideQueryParamsEnabled ??
defaultSettings.general.isHideQueryParamsEnabled,
@ -233,7 +238,10 @@ const App = ({
defaultSettings.general.isHideQueryParamsEnabled
}
background={
publishedTypebot.background ?? defaultTheme.general.background
publishedTypebot.background ?? {
type: defaultBackgroundType,
content: defaultBackgroundColor,
}
}
metadata={publishedTypebot.metadata ?? {}}
font={publishedTypebot.font}

View File

@ -24,7 +24,7 @@ export const injectVariableValuesInButtonsInputBlock =
return {
...deepParseVariables(variables)(block),
items: value.filter(isDefined).map((item, idx) => ({
id: idx.toString(),
id: 'choice' + idx.toString(),
blockId: block.id,
content: item,
})),

View File

@ -71,8 +71,7 @@ export const parseButtonsReply =
const matchedItem = longestItemsFirst.find(
(item) =>
item.id === inputValue ||
(item.content &&
inputValue.toLowerCase().trim() === item.content.toLowerCase().trim())
(item.content && inputValue.trim() === item.content.trim())
)
if (!matchedItem) return { status: 'fail' }
return {

View File

@ -77,7 +77,7 @@ const createStripePaymentIntent =
options.currency === 'EUR' ? 'fr-FR' : undefined,
{
style: 'currency',
currency: options.currency,
currency,
}
)

View File

@ -13,7 +13,6 @@ import { decrypt } from '@typebot.io/lib/api/encryption/decrypt'
import { resumeChatCompletion } from './resumeChatCompletion'
import { parseChatCompletionMessages } from './parseChatCompletionMessages'
import { executeChatCompletionOpenAIRequest } from './executeChatCompletionOpenAIRequest'
import { isPlaneteScale } from '@typebot.io/lib/isPlanetScale'
import prisma from '@typebot.io/lib/prisma'
import { ExecuteIntegrationResponse } from '../../../../types'
import { parseVariableNumber } from '@typebot.io/variables/parseVariableNumber'
@ -23,6 +22,7 @@ import {
defaultOpenAIOptions,
} from '@typebot.io/schemas/features/blocks/integrations/openai/constants'
import { BubbleBlockType } from '@typebot.io/schemas/features/blocks/bubbles/constants'
import { isPlaneteScale } from '@typebot.io/lib/isPlanetScale'
export const createChatCompletionOpenAI = async (
state: SessionState,
@ -90,7 +90,8 @@ export const createChatCompletionOpenAI = async (
blockId,
assistantMessageVariableName
) &&
!process.env.VERCEL_ENV
(!process.env.VERCEL_ENV ||
(isPlaneteScale() && credentials && isCredentialsV2(credentials)))
) {
return {
clientSideActions: [
@ -101,6 +102,7 @@ export const createChatCompletionOpenAI = async (
content?: string
role: (typeof chatCompletionMessageRoles)[number]
}[],
runtime: process.env.VERCEL_ENV ? 'edge' : 'nodejs',
},
expectsDedicatedReply: true,
},

View File

@ -15,7 +15,7 @@ import {
} from '@typebot.io/schemas'
import { stringify } from 'qs'
import { isDefined, isEmpty, isNotDefined, omit } from '@typebot.io/lib'
import ky, { HTTPError, Options } from 'ky'
import ky, { HTTPError, Options, TimeoutError } from 'ky'
import { resumeWebhookExecution } from './resumeWebhookExecution'
import { ExecuteIntegrationResponse } from '../../../types'
import { parseVariables } from '@typebot.io/variables/parseVariables'
@ -250,19 +250,18 @@ export const executeWebhook = async (
})
return { response, logs, startTimeShouldBeUpdated: true }
}
if (
typeof error === 'object' &&
error &&
'code' in error &&
error.code === 'ETIMEDOUT'
) {
if (error instanceof TimeoutError) {
const response = {
statusCode: 408,
data: { message: `Request timed out.` },
data: {
message: `Request timed out. (${(request.timeout ?? 0) / 1000}ms)`,
},
}
logs.push({
status: 'error',
description: `Webhook request timed out. (${request.timeout}ms)`,
description: `Webhook request timed out. (${
(request.timeout ?? 0) / 1000
}s)`,
details: {
response,
request,

View File

@ -32,6 +32,14 @@ const executeComparison =
if (isNotDefined(comparison.comparisonOperator)) return false
switch (comparison.comparisonOperator) {
case ComparisonOperators.CONTAINS: {
if (Array.isArray(inputValue)) {
const equal = (a: string | null, b: string | null) => {
if (typeof a === 'string' && typeof b === 'string')
return a.normalize() === b.normalize()
return a !== b
}
return compare(equal, inputValue, value, 'some')
}
const contains = (a: string | null, b: string | null) => {
if (b === '' || !b || !a) return false
return a
@ -43,6 +51,14 @@ const executeComparison =
return compare(contains, inputValue, value, 'some')
}
case ComparisonOperators.NOT_CONTAINS: {
if (Array.isArray(inputValue)) {
const notEqual = (a: string | null, b: string | null) => {
if (typeof a === 'string' && typeof b === 'string')
return a.normalize() !== b.normalize()
return a !== b
}
return compare(notEqual, inputValue, value)
}
const notContains = (a: string | null, b: string | null) => {
if (b === '' || !b || !a) return true
return !a

View File

@ -59,11 +59,8 @@ export const executeForgedBlock = async (
) &&
state.isStreamEnabled &&
!state.whatsApp &&
// TODO: Enable once chat api is rolling
isPlaneteScale() &&
credentials &&
isCredentialsV2(credentials)
// !process.env.VERCEL_ENV
(!process.env.VERCEL_ENV ||
(isPlaneteScale() && credentials && isCredentialsV2(credentials)))
) {
return {
outgoingEdgeId: block.outgoingEdgeId,
@ -72,6 +69,7 @@ export const executeForgedBlock = async (
type: 'stream',
expectsDedicatedReply: true,
stream: true,
runtime: process.env.VERCEL_ENV ? 'edge' : 'nodejs',
},
],
}

View File

@ -34,11 +34,14 @@ import { continueBotFlow } from './continueBotFlow'
import { parseVariables } from '@typebot.io/variables/parseVariables'
import { defaultSettings } from '@typebot.io/schemas/features/typebot/settings/constants'
import { IntegrationBlockType } from '@typebot.io/schemas/features/blocks/integrations/constants'
import { defaultTheme } from '@typebot.io/schemas/features/typebot/theme/constants'
import { VisitedEdge } from '@typebot.io/prisma'
import { env } from '@typebot.io/env'
import { getFirstEdgeId } from './getFirstEdgeId'
import { Reply } from './types'
import {
defaultGuestAvatarIsEnabled,
defaultHostAvatarIsEnabled,
} from '@typebot.io/schemas/features/typebot/theme/constants'
type StartParams =
| ({
@ -385,12 +388,11 @@ const getResult = async ({
const parseDynamicThemeInState = (theme: Theme) => {
const hostAvatarUrl =
theme.chat?.hostAvatar?.isEnabled ?? defaultTheme.chat.hostAvatar.isEnabled
theme.chat?.hostAvatar?.isEnabled ?? defaultHostAvatarIsEnabled
? theme.chat?.hostAvatar?.url
: undefined
const guestAvatarUrl =
theme.chat?.guestAvatar?.isEnabled ??
defaultTheme.chat.guestAvatar.isEnabled
theme.chat?.guestAvatar?.isEnabled ?? defaultGuestAvatarIsEnabled
? theme.chat?.guestAvatar?.url
: undefined
if (!hostAvatarUrl?.startsWith('{{') && !guestAvatarUrl?.startsWith('{{'))

View File

@ -1,9 +1,9 @@
import {
Background,
ChatTheme,
ContainerColors,
ContainerTheme,
GeneralTheme,
InputColors,
InputTheme,
Theme,
} from '@typebot.io/schemas'
import { BackgroundType } from '@typebot.io/schemas/features/typebot/theme/constants'
@ -66,7 +66,7 @@ const setChatTheme = (
}
const setHostBubbles = (
hostBubbles: ContainerColors,
hostBubbles: ContainerTheme,
documentStyle: CSSStyleDeclaration
) => {
if (hostBubbles.backgroundColor)
@ -82,7 +82,7 @@ const setHostBubbles = (
}
const setGuestBubbles = (
guestBubbles: ContainerColors,
guestBubbles: any,
documentStyle: CSSStyleDeclaration
) => {
if (guestBubbles.backgroundColor)
@ -98,7 +98,7 @@ const setGuestBubbles = (
}
const setButtons = (
buttons: ContainerColors,
buttons: ContainerTheme,
documentStyle: CSSStyleDeclaration
) => {
if (buttons.backgroundColor)
@ -113,7 +113,7 @@ const setButtons = (
)
}
const setInputs = (inputs: InputColors, documentStyle: CSSStyleDeclaration) => {
const setInputs = (inputs: InputTheme, documentStyle: CSSStyleDeclaration) => {
if (inputs.backgroundColor)
documentStyle.setProperty(
cssVariableNames.chat.inputs.bgColor,

View File

@ -1,6 +1,6 @@
{
"name": "@typebot.io/js",
"version": "0.2.64",
"version": "0.2.73",
"description": "Javascript library to display typebots on your website",
"type": "module",
"main": "dist/index.js",
@ -31,6 +31,7 @@
"@typebot.io/env": "workspace:*",
"@typebot.io/lib": "workspace:*",
"@typebot.io/schemas": "workspace:*",
"@typebot.io/theme": "workspace:*",
"@typebot.io/tsconfig": "workspace:*",
"@types/dompurify": "3.0.3",
"autoprefixer": "10.4.14",

View File

@ -2,47 +2,6 @@
@tailwind components;
@tailwind utilities;
:host {
--typebot-container-bg-image: none;
--typebot-container-bg-color: transparent;
--typebot-container-font-family: 'Open Sans';
--typebot-container-color: #303235;
--typebot-button-bg-color: #0042da;
--typebot-button-bg-color-rgb: 0, 66, 218;
--typebot-button-color: #ffffff;
--typebot-checkbox-bg-color: #ffffff;
--typebot-host-bubble-bg-color: #f7f8ff;
--typebot-host-bubble-color: #303235;
--typebot-guest-bubble-bg-color: #ff8e21;
--typebot-guest-bubble-color: #ffffff;
--typebot-input-bg-color: #ffffff;
--typebot-input-color: #303235;
--typebot-input-placeholder-color: #9095a0;
--typebot-header-bg-color: #ffffff;
--typebot-header-color: #303235;
--selectable-base-alpha: 0;
--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;
}
/* Hide scrollbar for Chrome, Safari and Opera */
.scrollable-container::-webkit-scrollbar {
display: none;
@ -161,67 +120,100 @@ pre {
.typebot-container {
background-image: var(--typebot-container-bg-image);
background-color: var(--typebot-container-bg-color);
background-position: center;
background-size: cover;
font-family: var(--typebot-container-font-family), -apple-system,
BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif,
'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
container-type: inline-size;
}
.typebot-chat-view {
max-width: var(--typebot-chat-container-max-width);
background-color: rgba(
var(--typebot-chat-container-bg-rgb),
var(--typebot-chat-container-opacity)
);
color: rgb(var(--typebot-chat-container-color));
min-height: 100%;
backdrop-filter: blur(var(--typebot-chat-container-blur));
border-width: var(--typebot-chat-container-border-width);
border-color: rgba(
var(--typebot-chat-container-border-rgb),
var(--typebot-chat-container-border-opacity)
);
padding-left: 20px;
padding-right: 20px;
box-shadow: var(--typebot-chat-container-box-shadow);
}
@container (min-width: 480px) {
.typebot-chat-view {
min-height: var(--typebot-chat-container-max-height);
max-height: var(--typebot-chat-container-max-height);
border-radius: var(--typebot-chat-container-border-radius);
}
}
.typebot-button {
color: var(--typebot-button-color);
background-color: var(--typebot-button-bg-color);
border: 1px solid var(--typebot-button-bg-color);
border-radius: var(--typebot-border-radius);
background-color: rgba(
var(--typebot-button-bg-rgb),
var(--typebot-button-opacity)
);
border-width: var(--typebot-button-border-width);
border-color: rgba(
var(--typebot-button-border-rgb),
var(--typebot-button-border-opacity)
);
border-radius: var(--typebot-button-border-radius);
box-shadow: var(--typebot-button-box-shadow);
backdrop-filter: blur(var(--typebot-button-blur));
transition: all 0.3s ease;
}
.typebot-button.selectable {
color: var(--typebot-host-bubble-color);
background-color: var(--typebot-host-bubble-bg-color);
border: 1px solid var(--typebot-button-bg-color);
}
.typebot-selectable {
border: 1px solid
rgba(
var(--typebot-button-bg-color-rgb),
calc(var(--selectable-base-alpha) + 0.25)
);
border-radius: var(--typebot-border-radius);
color: var(--typebot-container-color);
border-width: var(--typebot-button-border-width);
border-color: rgba(
var(--typebot-button-border-rgb),
calc(var(--selectable-alpha-ratio) * 0.25)
);
border-radius: var(--typebot-button-border-radius);
color: rgb(var(--typebot-chat-container-color));
background-color: rgba(
var(--typebot-button-bg-color-rgb),
calc(var(--selectable-base-alpha) + 0.08)
var(--typebot-button-bg-rgb),
calc(var(--selectable-alpha-ratio) * 0.08)
);
transition: all 0.3s ease;
backdrop-filter: blur(2px);
}
.typebot-selectable:hover {
background-color: rgba(
var(--typebot-button-bg-color-rgb),
calc(var(--selectable-base-alpha) + 0.12)
var(--typebot-button-bg-rgb),
calc(var(--selectable-alpha-ratio) * 0.12)
);
border-color: rgba(
var(--typebot-button-bg-color-rgb),
calc(var(--selectable-base-alpha) + 0.3)
var(--typebot-button-border-rgb),
calc(var(--selectable-alpha-ratio) * 0.3)
);
}
.typebot-selectable.selected {
background-color: rgba(
var(--typebot-button-bg-color-rgb),
calc(var(--selectable-base-alpha) + 0.18)
var(--typebot-button-bg-rgb),
calc(var(--selectable-alpha-ratio) * 0.18)
);
border-color: rgba(
var(--typebot-button-bg-color-rgb),
calc(var(--selectable-base-alpha) + 0.35)
var(--typebot-button-border-rgb),
calc(var(--selectable-alpha-ratio) * 0.35)
);
}
.typebot-checkbox {
border: 1px solid var(--typebot-button-bg-color);
border-radius: var(--typebot-border-radius);
background-color: var(--typebot-checkbox-bg-color);
border: 1px solid
rgba(var(--typebot-button-bg-rgb), var(--typebot-button-opacity));
border-radius: var(--typebot-button-border-radius);
background-color: rgba(var(--typebot-checkbox-bg-rgb));
color: var(--typebot-button-color);
padding: 1px;
border-radius: 2px;
@ -229,7 +221,7 @@ pre {
}
.typebot-checkbox.checked {
background-color: var(--typebot-button-bg-color);
background-color: rgb(var(--typebot-button-bg-rgb));
}
.typebot-host-bubble {
@ -237,22 +229,56 @@ pre {
}
.typebot-host-bubble > .bubble-typing {
background-color: var(--typebot-host-bubble-bg-color);
border: var(--typebot-host-bubble-border);
background-color: rgba(
var(--typebot-host-bubble-bg-rgb),
var(--typebot-host-bubble-opacity)
);
border-width: var(--typebot-host-bubble-border-width);
border-color: rgba(
var(--typebot-host-bubble-border-rgb),
var(--typebot-host-bubble-border-opacity)
);
border-radius: var(--typebot-host-bubble-border-radius);
box-shadow: var(--typebot-host-bubble-box-shadow);
backdrop-filter: blur(var(--typebot-host-bubble-blur));
}
.typebot-host-bubble img,
.typebot-host-bubble video,
.typebot-host-bubble iframe {
border-radius: 6px;
}
.typebot-guest-bubble {
color: var(--typebot-guest-bubble-color);
background-color: var(--typebot-guest-bubble-bg-color);
border-radius: 6px;
background-color: rgba(
var(--typebot-guest-bubble-bg-rgb),
var(--typebot-guest-bubble-opacity)
);
border-width: var(--typebot-guest-bubble-border-width);
border-color: rgba(
var(--typebot-guest-bubble-border-rgb),
var(--typebot-guest-bubble-border-opacity)
);
border-radius: var(--typebot-guest-bubble-border-radius);
box-shadow: var(--typebot-guest-bubble-box-shadow);
backdrop-filter: blur(var(--typebot-guest-bubble-blur));
}
.typebot-input {
color: var(--typebot-input-color);
background-color: var(--typebot-input-bg-color);
box-shadow: 0 2px 6px -1px rgba(0, 0, 0, 0.1);
border-radius: var(--typebot-border-radius);
background-color: rgba(
var(--typebot-input-bg-rgb),
var(--typebot-input-opacity)
);
border-width: var(--typebot-input-border-width);
border-color: rgba(
var(--typebot-input-border-rgb),
var(--typebot-input-border-opacity)
);
border-radius: var(--typebot-input-border-radius);
box-shadow: var(--typebot-input-box-shadow);
backdrop-filter: blur(var(--typebot-input-blur));
}
.typebot-input-error-message {
@ -263,24 +289,20 @@ pre {
fill: var(--typebot-button-color);
}
.typebot-chat-view {
max-width: 900px;
}
.ping span {
background-color: var(--typebot-button-bg-color);
background-color: rgb(var(--typebot-button-bg-rgb));
}
.rating-icon-container svg {
width: 42px;
height: 42px;
stroke: var(--typebot-button-bg-color);
stroke: rgb(var(--typebot-button-bg-rgb));
fill: var(--typebot-host-bubble-bg-color);
transition: fill 100ms ease-out;
}
.rating-icon-container.selected svg {
fill: var(--typebot-button-bg-color);
fill: rgb(var(--typebot-button-bg-rgb));
}
.rating-icon-container:hover svg {
@ -292,59 +314,60 @@ pre {
}
.upload-progress-bar {
background-color: var(--typebot-button-bg-color);
border-radius: var(--typebot-border-radius);
background-color: rgb(var(--typebot-button-bg-rgb));
border-radius: var(--typebot-input-border-radius);
}
.total-files-indicator {
background-color: var(--typebot-button-bg-color);
background-color: rgb(var(--typebot-button-bg-rgb));
color: var(--typebot-button-color);
font-size: 10px;
}
.typebot-upload-input {
transition: border-color 100ms ease-out;
border-radius: var(--typebot-border-radius);
border-radius: var(--typebot-input-border-radius);
}
.typebot-upload-input.dragging-over {
border-color: var(--typebot-button-bg-color);
border-color: rgb(var(--typebot-button-bg-rgb));
}
.secondary-button {
background-color: var(--typebot-host-bubble-bg-color);
color: var(--typebot-host-bubble-color);
border-radius: var(--typebot-border-radius);
border-radius: var(--typebot-button-border-radius);
}
.typebot-country-select {
color: var(--typebot-input-color);
background-color: var(--typebot-input-bg-color);
border-radius: var(--typebot-border-radius);
border-radius: var(--typebot-button-border-radius);
}
.typebot-date-input {
color-scheme: light;
color: var(--typebot-input-color);
background-color: var(--typebot-input-bg-color);
border-radius: var(--typebot-border-radius);
border-radius: var(--typebot-input-border-radius);
}
.typebot-popup-blocked-toast {
border-radius: var(--typebot-border-radius);
border-radius: var(--typebot-input-border-radius);
}
.typebot-picture-button {
color: var(--typebot-button-color);
background-color: var(--typebot-button-bg-color);
border-radius: var(--typebot-border-radius);
background-color: rgb(var(--typebot-button-bg-rgb));
border-radius: var(--typebot-button-border-radius);
transition: all 0.3s ease;
width: 236px;
}
.typebot-picture-button > img,
.typebot-selectable-picture > img {
border-radius: var(--typebot-border-radius) var(--typebot-border-radius) 0 0;
border-radius: var(--typebot-button-border-radius)
var(--typebot-button-border-radius) 0 0;
min-width: 200px;
width: 100%;
max-height: 200px;
@ -362,14 +385,14 @@ pre {
.typebot-selectable-picture {
border: 1px solid
rgba(
var(--typebot-button-bg-color-rgb),
calc(var(--selectable-base-alpha) + 0.25)
var(--typebot-button-bg-rgb),
calc(var(--selectable-alpha-ratio) * 0.25)
);
border-radius: var(--typebot-border-radius);
color: var(--typebot-container-color);
border-radius: var(--typebot-button-border-radius);
color: rgb(var(--typebot-chat-container-color));
background-color: rgba(
var(--typebot-button-bg-color-rgb),
calc(var(--selectable-base-alpha) + 0.08)
var(--typebot-button-bg-rgb),
calc(var(--selectable-alpha-ratio) * 0.08)
);
transition: all 0.3s ease;
width: 236px;
@ -377,23 +400,23 @@ pre {
.typebot-selectable-picture:hover {
background-color: rgba(
var(--typebot-button-bg-color-rgb),
calc(var(--selectable-base-alpha) + 0.12)
var(--typebot-button-bg-rgb),
calc(var(--selectable-alpha-ratio) * 0.12)
);
border-color: rgba(
var(--typebot-button-bg-color-rgb),
calc(var(--selectable-base-alpha) + 0.3)
var(--typebot-button-bg-rgb),
calc(var(--selectable-alpha-ratio) * 0.3)
);
}
.typebot-selectable-picture.selected {
background-color: rgba(
var(--typebot-button-bg-color-rgb),
calc(var(--selectable-base-alpha) + 0.18)
var(--typebot-button-bg-rgb),
calc(var(--selectable-alpha-ratio) * 0.18)
);
border-color: rgba(
var(--typebot-button-bg-color-rgb),
calc(var(--selectable-base-alpha) + 0.35)
var(--typebot-button-bg-rgb),
calc(var(--selectable-alpha-ratio) * 0.35)
);
}
@ -404,8 +427,8 @@ select option {
.typebot-progress-bar-container {
background-color: rgba(
var(--typebot-button-bg-color-rgb),
calc(var(--selectable-base-alpha) + 0.12)
var(--typebot-progress-bar-bg-rgb),
calc(var(--selectable-alpha-ratio) * 0.12)
);
height: var(--typebot-progress-bar-height);

View File

@ -16,7 +16,6 @@ import {
import { setCssVariablesValue } from '@/utils/setCssVariablesValue'
import immutableCss from '../assets/immutable.css'
import { Font, InputBlock, StartFrom } from '@typebot.io/schemas'
import { defaultTheme } from '@typebot.io/schemas/features/typebot/theme/constants'
import { clsx } from 'clsx'
import { HTTPError } from 'ky'
import { injectFont } from '@/utils/injectFont'
@ -25,6 +24,11 @@ import { Portal } from 'solid-js/web'
import { defaultSettings } from '@typebot.io/schemas/features/typebot/settings/constants'
import { persist } from '@/utils/persist'
import { setBotContainerHeight } from '@/utils/botContainerHeightSignal'
import {
defaultFontFamily,
defaultFontType,
defaultProgressBarPosition,
} from '@typebot.io/schemas/features/typebot/theme/constants'
export type BotProps = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@ -142,6 +146,7 @@ export const Bot = (props: BotProps & { class?: string }) => {
}
props.onChatStatePersisted?.(true)
} else {
wipeExistingChatStateInStorage(data.typebot.id)
setInitialChatReply(data)
if (data.input?.id && props.onNewInputBlock)
props.onNewInputBlock(data.input)
@ -262,8 +267,10 @@ const BotContent = (props: BotContentProps) => {
createEffect(() => {
injectFont(
props.initialChatReply.typebot.theme.general?.font ??
defaultTheme.general.font
props.initialChatReply.typebot.theme.general?.font ?? {
type: defaultFontType,
family: defaultFontFamily,
}
)
if (!botContainer) return
setCssVariablesValue(
@ -282,7 +289,7 @@ const BotContent = (props: BotContentProps) => {
<div
ref={botContainer}
class={clsx(
'relative flex w-full h-full text-base overflow-hidden bg-cover bg-center flex-col items-center typebot-container @container',
'relative flex w-full h-full text-base overflow-hidden flex-col justify-center items-center typebot-container',
props.class
)}
>
@ -296,8 +303,7 @@ const BotContent = (props: BotContentProps) => {
when={
props.progressBarRef &&
(props.initialChatReply.typebot.theme.general?.progressBar
?.position ?? defaultTheme.general.progressBar.position) ===
'fixed'
?.position ?? defaultProgressBarPosition) === 'fixed'
}
fallback={<ProgressBar value={progressValue() as number} />}
>
@ -306,17 +312,15 @@ const BotContent = (props: BotContentProps) => {
</Portal>
</Show>
</Show>
<div class="flex w-full h-full justify-center">
<ConversationContainer
context={props.context}
initialChatReply={props.initialChatReply}
onNewInputBlock={props.onNewInputBlock}
onAnswer={props.onAnswer}
onEnd={props.onEnd}
onNewLogs={props.onNewLogs}
onProgressUpdate={setProgressValue}
/>
</div>
<ConversationContainer
context={props.context}
initialChatReply={props.initialChatReply}
onNewInputBlock={props.onNewInputBlock}
onAnswer={props.onAnswer}
onEnd={props.onEnd}
onNewLogs={props.onNewLogs}
onProgressUpdate={setProgressValue}
/>
<Show
when={
props.initialChatReply.typebot.settings.general?.isBrandingEnabled

View File

@ -6,8 +6,11 @@ import { HostBubble } from '../bubbles/HostBubble'
import { InputChatBlock } from '../InputChatBlock'
import { AvatarSideContainer } from './AvatarSideContainer'
import { StreamingBubble } from '../bubbles/StreamingBubble'
import { defaultTheme } from '@typebot.io/schemas/features/typebot/theme/constants'
import { defaultSettings } from '@typebot.io/schemas/features/typebot/settings/constants'
import {
defaultGuestAvatarIsEnabled,
defaultHostAvatarIsEnabled,
} from '@typebot.io/schemas/features/typebot/theme/constants'
type Props = Pick<ContinueChatResponse, 'messages' | 'input'> & {
theme: Theme
@ -77,7 +80,7 @@ export const ChatChunk = (props: Props) => {
<Show
when={
(props.theme.chat?.hostAvatar?.isEnabled ??
defaultTheme.chat.hostAvatar.isEnabled) &&
defaultHostAvatarIsEnabled) &&
props.messages.length > 0
}
>
@ -93,7 +96,7 @@ export const ChatChunk = (props: Props) => {
style={{
'max-width':
props.theme.chat?.guestAvatar?.isEnabled ??
defaultTheme.chat.guestAvatar.isEnabled
defaultGuestAvatarIsEnabled
? isMobile()
? 'calc(100% - 32px - 32px)'
: 'calc(100% - 48px - 48px)'
@ -131,7 +134,7 @@ export const ChatChunk = (props: Props) => {
chunkIndex={props.index}
hasHostAvatar={
props.theme.chat?.hostAvatar?.isEnabled ??
defaultTheme.chat.hostAvatar.isEnabled
defaultHostAvatarIsEnabled
}
guestAvatar={props.theme.chat?.guestAvatar}
context={props.context}
@ -151,7 +154,7 @@ export const ChatChunk = (props: Props) => {
<Show
when={
props.theme.chat?.hostAvatar?.isEnabled ??
defaultTheme.chat.hostAvatar.isEnabled
defaultHostAvatarIsEnabled
}
>
<AvatarSideContainer
@ -165,7 +168,7 @@ export const ChatChunk = (props: Props) => {
style={{
'max-width':
props.theme.chat?.hostAvatar?.isEnabled ??
defaultTheme.chat.hostAvatar.isEnabled
defaultHostAvatarIsEnabled
? isMobile()
? 'calc(100% - 32px - 32px)'
: 'calc(100% - 48px - 48px)'

View File

@ -154,6 +154,7 @@ export const ConversationContainer = (props: Props) => {
const longRequest = setTimeout(() => {
setIsSending(true)
}, 1000)
autoScrollToBottom()
const { data, error } = await continueChatQuery({
apiHost: props.context.apiHost,
sessionId: props.initialChatReply.sessionId,
@ -205,6 +206,13 @@ export const ConversationContainer = (props: Props) => {
isNotDefined(action.lastBubbleBlockId)
)
await processClientSideActions(actionsBeforeFirstBubble)
if (
data.clientSideActions.length === 1 &&
data.clientSideActions[0].type === 'stream' &&
data.messages.length === 0 &&
data.input === undefined
)
return
}
setChatChunks((displayedChunks) => [
...displayedChunks,
@ -284,7 +292,7 @@ export const ConversationContainer = (props: Props) => {
return (
<div
ref={chatContainer}
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"
class="flex flex-col overflow-y-auto w-full px-3 pt-10 relative scrollable-container typebot-chat-view scroll-smooth gap-2"
>
<For each={chatChunks()}>
{(chatChunk, index) => (

View File

@ -2,7 +2,7 @@ import { Theme } from '@typebot.io/schemas'
import { Show } from 'solid-js'
import { LoadingBubble } from '../bubbles/LoadingBubble'
import { AvatarSideContainer } from './AvatarSideContainer'
import { defaultTheme } from '@typebot.io/schemas/features/typebot/theme/constants'
import { defaultHostAvatarIsEnabled } from '@typebot.io/schemas/features/typebot/theme/constants'
type Props = {
theme: Theme
@ -15,7 +15,7 @@ export const LoadingChunk = (props: Props) => (
<Show
when={
props.theme.chat?.hostAvatar?.isEnabled ??
defaultTheme.chat.hostAvatar.isEnabled
defaultHostAvatarIsEnabled
}
>
<AvatarSideContainer

View File

@ -35,8 +35,8 @@ import { MultiplePictureChoice } from '@/features/blocks/inputs/pictureChoice/Mu
import { formattedMessages } from '@/utils/formattedMessagesSignal'
import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants'
import { defaultPaymentInputOptions } from '@typebot.io/schemas/features/blocks/inputs/payment/constants'
import { defaultTheme } from '@typebot.io/schemas/features/typebot/theme/constants'
import { persist } from '@/utils/persist'
import { defaultGuestAvatarIsEnabled } from '@typebot.io/schemas/features/typebot/theme/constants'
type Props = {
ref: HTMLDivElement | undefined
@ -82,8 +82,7 @@ export const InputChatBlock = (props: Props) => {
<GuestBubble
message={formattedMessage() ?? (answer() as string)}
showAvatar={
props.guestAvatar?.isEnabled ??
defaultTheme.chat.guestAvatar.isEnabled
props.guestAvatar?.isEnabled ?? defaultGuestAvatarIsEnabled
}
avatarSrc={props.guestAvatar?.url && props.guestAvatar.url}
/>

View File

@ -1,14 +1,15 @@
import { streamingMessage } from '@/utils/streamingMessageSignal'
import { createEffect, createSignal } from 'solid-js'
import { For, createEffect, createSignal } from 'solid-js'
import { marked } from 'marked'
import domPurify from 'dompurify'
import { isNotEmpty } from '@typebot.io/lib'
type Props = {
streamingMessageId: string
}
export const StreamingBubble = (props: Props) => {
const [content, setContent] = createSignal<string>('')
const [content, setContent] = createSignal<string[]>([])
marked.use({
renderer: {
@ -19,12 +20,28 @@ export const StreamingBubble = (props: Props) => {
})
createEffect(() => {
if (streamingMessage()?.id === props.streamingMessageId)
setContent(
domPurify.sanitize(marked.parse(streamingMessage()?.content ?? ''), {
ADD_ATTR: ['target'],
if (streamingMessage()?.id !== props.streamingMessageId) return []
setContent(
streamingMessage()
?.content.split('```')
.map((block, index) => {
if (index % 2 === 0) {
return block.split('\n\n').map((line) =>
domPurify.sanitize(marked.parse(line), {
ADD_ATTR: ['target'],
})
)
} else {
return [
domPurify.sanitize(marked.parse('```' + block + '```'), {
ADD_ATTR: ['target'],
}),
]
}
})
)
.flat()
.filter(isNotEmpty) ?? []
)
})
return (
@ -43,8 +60,9 @@ export const StreamingBubble = (props: Props) => {
class={
'flex flex-col overflow-hidden text-fade-in mx-4 my-2 relative text-ellipsis h-full gap-6'
}
innerHTML={content()}
/>
>
<For each={content()}>{(line) => <span innerHTML={line} />}</For>
</div>
</div>
</div>
</div>

View File

@ -1,7 +1,7 @@
import { SendButton } from '@/components/SendButton'
import { InputSubmitContent } from '@/types'
import type { RatingInputBlock } from '@typebot.io/schemas'
import { createSignal, For, Match, Switch } from 'solid-js'
import { createSignal, For, Match, Switch, Show } from 'solid-js'
import { isDefined, isEmpty, isNotDefined } from '@typebot.io/lib'
import { Button } from '@/components/Button'
import { defaultRatingInputOptions } from '@typebot.io/schemas/features/blocks/inputs/rating/constants'
@ -107,17 +107,24 @@ const RatingButton = (props: RatingButtonProps) => {
'Numbers'
}
>
<Button
on:click={handleClick}
class={
props.isOneClickSubmitEnabled ||
(isDefined(props.rating) && props.idx <= props.rating)
? ''
: 'selectable'
}
>
{props.idx}
</Button>
<Show when={props.isOneClickSubmitEnabled}>
<Button on:click={handleClick}>{props.idx}</Button>
</Show>
<Show when={!props.isOneClickSubmitEnabled}>
<div
role="checkbox"
aria-checked={isDefined(props.rating) && props.idx <= props.rating}
on:click={handleClick}
class={
'py-2 px-4 font-semibold focus:outline-none cursor-pointer select-none typebot-selectable' +
(isDefined(props.rating) && props.idx <= props.rating
? ' selected'
: '')
}
>
{props.idx}
</div>
</Show>
</Match>
<Match
when={

View File

@ -6,6 +6,7 @@ import { isMobile } from '@/utils/isMobileSignal'
import type { TextInputBlock } from '@typebot.io/schemas'
import { createSignal, onCleanup, onMount } from 'solid-js'
import { defaultTextInputOptions } from '@typebot.io/schemas/features/blocks/inputs/text/constants'
import clsx from 'clsx'
type Props = {
block: TextInputBlock
@ -55,7 +56,10 @@ export const TextInput = (props: Props) => {
return (
<div
class={'flex items-end justify-between pr-2 typebot-input w-full'}
class={clsx(
'flex justify-between pr-2 typebot-input w-full',
props.block.options?.isLong ? 'items-end' : 'items-center'
)}
data-testid="input"
style={{
'max-width': props.block.options?.isLong ? undefined : '350px',

View File

@ -7,16 +7,22 @@ let abortController: AbortController | null = null
const secondsToWaitBeforeRetries = 3
const maxRetryAttempts = 3
const edgeRuntimePath = '/api/integrations/openai/streamer'
const nodejsRuntimePath = (sessionId: string) =>
`/api/v1/sessions/${sessionId}/streamMessage`
export const streamChat =
(context: ClientSideActionContext & { retryAttempt?: number }) =>
async ({
messages,
runtime,
onMessageStream,
}: {
messages?: {
content?: string | undefined
role?: 'system' | 'user' | 'assistant' | undefined
}[]
runtime: 'edge' | 'nodejs'
onMessageStream?: (props: { id: string; message: string }) => void
}): Promise<{ message?: string; error?: object }> => {
try {
@ -25,9 +31,12 @@ export const streamChat =
const apiHost = context.apiHost
const res = await fetch(
`${
isNotEmpty(apiHost) ? apiHost : guessApiHost()
}/api/integrations/openai/streamer`,
isNotEmpty(apiHost)
? apiHost
: guessApiHost() +
(runtime === 'edge'
? edgeRuntimePath
: nodejsRuntimePath(context.sessionId)),
{
method: 'POST',
headers: {
@ -35,7 +44,7 @@ export const streamChat =
},
body: JSON.stringify({
messages,
sessionId: context.sessionId,
sessionId: runtime === 'edge' ? context.sessionId : undefined,
}),
signal: abortController.signal,
}
@ -52,7 +61,7 @@ export const streamChat =
return streamChat({
...context,
retryAttempt: (context.retryAttempt ?? 0) + 1,
})({ messages, onMessageStream })
})({ messages, onMessageStream, runtime })
}
return {
error: (await res.json()) || 'Failed to fetch the chat response.',

View File

@ -54,12 +54,17 @@ export const executeClientSideAction = async ({
'streamOpenAiChatCompletion' in clientSideAction ||
'stream' in clientSideAction
) {
const runtime =
'streamOpenAiChatCompletion' in clientSideAction
? clientSideAction.streamOpenAiChatCompletion.runtime
: clientSideAction.runtime
const { error, message } = await streamChat(context)({
messages:
'streamOpenAiChatCompletion' in clientSideAction
? clientSideAction.streamOpenAiChatCompletion?.messages
: undefined,
onMessageStream,
runtime,
})
if (error)
return {

View File

@ -1,6 +1,6 @@
import { isNotEmpty } from '@typebot.io/lib'
import { Font } from '@typebot.io/schemas'
import { defaultTheme } from '@typebot.io/schemas/features/typebot/theme/constants'
import { defaultFontFamily } from '@typebot.io/schemas/features/typebot/theme/constants'
const googleFontCdnBaseUrl = 'https://fonts.bunny.net/css2'
const elementId = 'typebot-font'
@ -10,8 +10,7 @@ export const injectFont = (font: Font) => {
if (typeof font === 'string' || font.type === 'Google') {
const fontFamily =
(typeof font === 'string' ? font : font.family) ??
defaultTheme.general.font.family
(typeof font === 'string' ? font : font.family) ?? defaultFontFamily
if (existingFont?.getAttribute('href')?.includes(fontFamily)) return
existingFont?.remove()
const fontElement = document.createElement('link')

View File

@ -1,56 +1,122 @@
import {
Background,
ChatTheme,
ContainerColors,
ContainerBorderTheme,
ContainerTheme,
GeneralTheme,
InputColors,
InputTheme,
Theme,
} from '@typebot.io/schemas'
import { isLight, hexToRgb } from '@typebot.io/lib/hexToRgb'
import { isNotEmpty } from '@typebot.io/lib'
import { isDefined, isEmpty } from '@typebot.io/lib'
import {
BackgroundType,
defaultTheme,
defaultBackgroundColor,
defaultBackgroundType,
defaultButtonsBackgroundColor,
defaultButtonsColor,
defaultButtonsBorderThickness,
defaultContainerBackgroundColor,
defaultContainerMaxHeight,
defaultContainerMaxWidth,
defaultDarkTextColor,
defaultFontFamily,
defaultGuestBubblesBackgroundColor,
defaultGuestBubblesColor,
defaultHostBubblesBackgroundColor,
defaultHostBubblesColor,
defaultInputsBackgroundColor,
defaultInputsColor,
defaultInputsPlaceholderColor,
defaultLightTextColor,
defaultProgressBarBackgroundColor,
defaultProgressBarColor,
defaultProgressBarPlacement,
defaultProgressBarPosition,
defaultProgressBarThickness,
defaultInputsShadow,
defaultOpacity,
defaultBlur,
defaultRoundness,
} from '@typebot.io/schemas/features/typebot/theme/constants'
import { isChatContainerLight } from '@typebot.io/theme/isChatContainerLight'
const cssVariableNames = {
general: {
bgImage: '--typebot-container-bg-image',
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',
colorRgb: '--typebot-progress-bar-color-rgb',
colorRgb: '--typebot-progress-bar-bg-rgb',
height: '--typebot-progress-bar-height',
top: '--typebot-progress-bar-top',
bottom: '--typebot-progress-bar-bottom',
},
},
chat: {
container: {
maxWidth: '--typebot-chat-container-max-width',
maxHeight: '--typebot-chat-container-max-height',
bgColor: '--typebot-chat-container-bg-rgb',
color: '--typebot-chat-container-color',
borderRadius: '--typebot-chat-container-border-radius',
borderWidth: '--typebot-chat-container-border-width',
borderColor: '--typebot-chat-container-border-rgb',
borderOpacity: '--typebot-chat-container-border-opacity',
opacity: '--typebot-chat-container-opacity',
blur: '--typebot-chat-container-blur',
boxShadow: '--typebot-chat-container-box-shadow',
},
hostBubbles: {
bgColor: '--typebot-host-bubble-bg-color',
bgColor: '--typebot-host-bubble-bg-rgb',
color: '--typebot-host-bubble-color',
borderRadius: '--typebot-host-bubble-border-radius',
borderWidth: '--typebot-host-bubble-border-width',
borderColor: '--typebot-host-bubble-border-rgb',
borderOpacity: '--typebot-host-bubble-border-opacity',
opacity: '--typebot-host-bubble-opacity',
blur: '--typebot-host-bubble-blur',
boxShadow: '--typebot-host-bubble-box-shadow',
},
guestBubbles: {
bgColor: '--typebot-guest-bubble-bg-color',
bgColor: '--typebot-guest-bubble-bg-rgb',
color: '--typebot-guest-bubble-color',
borderRadius: '--typebot-guest-bubble-border-radius',
borderWidth: '--typebot-guest-bubble-border-width',
borderColor: '--typebot-guest-bubble-border-rgb',
borderOpacity: '--typebot-guest-bubble-border-opacity',
opacity: '--typebot-guest-bubble-opacity',
blur: '--typebot-guest-bubble-blur',
boxShadow: '--typebot-guest-bubble-box-shadow',
},
inputs: {
bgColor: '--typebot-input-bg-color',
bgColor: '--typebot-input-bg-rgb',
color: '--typebot-input-color',
placeholderColor: '--typebot-input-placeholder-color',
borderRadius: '--typebot-input-border-radius',
borderWidth: '--typebot-input-border-width',
borderColor: '--typebot-input-border-rgb',
borderOpacity: '--typebot-input-border-opacity',
opacity: '--typebot-input-opacity',
blur: '--typebot-input-blur',
boxShadow: '--typebot-input-box-shadow',
},
buttons: {
bgColor: '--typebot-button-bg-color',
bgColorRgb: '--typebot-button-bg-color-rgb',
bgRgb: '--typebot-button-bg-rgb',
color: '--typebot-button-color',
borderRadius: '--typebot-button-border-radius',
borderWidth: '--typebot-button-border-width',
borderColor: '--typebot-button-border-rgb',
borderOpacity: '--typebot-button-border-opacity',
opacity: '--typebot-button-opacity',
blur: '--typebot-button-blur',
boxShadow: '--typebot-button-box-shadow',
},
checkbox: {
bgColor: '--typebot-checkbox-bg-color',
color: '--typebot-checkbox-color',
baseAlpha: '--selectable-base-alpha',
bgRgb: '--typebot-checkbox-bg-rgb',
alphaRatio: '--selectable-alpha-ratio',
},
},
} as const
@ -63,43 +129,31 @@ export const setCssVariablesValue = (
if (!theme) return
const documentStyle = container?.style
if (!documentStyle) return
setGeneralTheme(
theme.general ?? defaultTheme.general,
documentStyle,
isPreview
)
setChatTheme(theme.chat ?? defaultTheme.chat, documentStyle)
setGeneralTheme(theme.general, documentStyle, isPreview)
setChatTheme(theme.chat, theme.general?.background, documentStyle)
}
const setGeneralTheme = (
generalTheme: GeneralTheme,
generalTheme: GeneralTheme | undefined,
documentStyle: CSSStyleDeclaration,
isPreview?: boolean
) => {
setTypebotBackground(
generalTheme.background ?? defaultTheme.general.background,
documentStyle
)
setGeneralBackground(generalTheme?.background, documentStyle)
documentStyle.setProperty(
cssVariableNames.general.fontFamily,
(typeof generalTheme.font === 'string'
(typeof generalTheme?.font === 'string'
? generalTheme.font
: generalTheme.font?.family) ?? defaultTheme.general.font.family
)
setProgressBar(
generalTheme.progressBar ?? defaultTheme.general.progressBar,
documentStyle,
isPreview
: generalTheme?.font?.family) ?? defaultFontFamily
)
setProgressBar(generalTheme?.progressBar, documentStyle, isPreview)
}
const setProgressBar = (
progressBar: NonNullable<GeneralTheme['progressBar']>,
progressBar: GeneralTheme['progressBar'],
documentStyle: CSSStyleDeclaration,
isPreview?: boolean
) => {
const position =
progressBar.position ?? defaultTheme.general.progressBar.position
const position = progressBar?.position ?? defaultProgressBarPosition
documentStyle.setProperty(
cssVariableNames.general.progressBar.position,
@ -107,22 +161,20 @@ const setProgressBar = (
)
documentStyle.setProperty(
cssVariableNames.general.progressBar.color,
progressBar.color ?? defaultTheme.general.progressBar.color
progressBar?.color ?? defaultProgressBarColor
)
documentStyle.setProperty(
cssVariableNames.general.progressBar.colorRgb,
hexToRgb(
progressBar.backgroundColor ??
defaultTheme.general.progressBar.backgroundColor
progressBar?.backgroundColor ?? defaultProgressBarBackgroundColor
).join(', ')
)
documentStyle.setProperty(
cssVariableNames.general.progressBar.height,
`${progressBar.thickness ?? defaultTheme.general.progressBar.thickness}px`
`${progressBar?.thickness ?? defaultProgressBarThickness}px`
)
const placement =
progressBar.placement ?? defaultTheme.general.progressBar.placement
const placement = progressBar?.placement ?? defaultProgressBarPlacement
documentStyle.setProperty(
cssVariableNames.general.progressBar.top,
@ -136,123 +188,436 @@ const setProgressBar = (
}
const setChatTheme = (
chatTheme: ChatTheme,
chatTheme: ChatTheme | undefined,
generalBackground: GeneralTheme['background'],
documentStyle: CSSStyleDeclaration
) => {
setHostBubbles(
chatTheme.hostBubbles ?? defaultTheme.chat.hostBubbles,
documentStyle
setChatContainer(
chatTheme?.container,
generalBackground,
documentStyle,
chatTheme?.roundness
)
setGuestBubbles(
chatTheme.guestBubbles ?? defaultTheme.chat.guestBubbles,
documentStyle
setHostBubbles(chatTheme?.hostBubbles, documentStyle, chatTheme?.roundness)
setGuestBubbles(chatTheme?.guestBubbles, documentStyle, chatTheme?.roundness)
setButtons(chatTheme?.buttons, documentStyle, chatTheme?.roundness)
setInputs(chatTheme?.inputs, documentStyle, chatTheme?.roundness)
setCheckbox(chatTheme?.container, generalBackground, documentStyle)
}
const setChatContainer = (
container: ChatTheme['container'],
generalBackground: GeneralTheme['background'],
documentStyle: CSSStyleDeclaration,
legacyRoundness?: ChatTheme['roundness']
) => {
const chatContainerBgColor =
container?.backgroundColor ?? defaultContainerBackgroundColor
const isBgDisabled =
chatContainerBgColor === 'transparent' || isEmpty(chatContainerBgColor)
documentStyle.setProperty(
cssVariableNames.chat.container.bgColor,
isBgDisabled ? '0, 0, 0' : hexToRgb(chatContainerBgColor).join(', ')
)
setButtons(chatTheme.buttons ?? defaultTheme.chat.buttons, documentStyle)
setInputs(chatTheme.inputs ?? defaultTheme.chat.inputs, documentStyle)
setRoundness(
chatTheme.roundness ?? defaultTheme.chat.roundness,
documentStyle
documentStyle.setProperty(
cssVariableNames.chat.container.color,
hexToRgb(
container?.color ??
(isChatContainerLight({
chatContainer: container,
generalBackground,
})
? defaultLightTextColor
: defaultDarkTextColor)
).join(', ')
)
documentStyle.setProperty(
cssVariableNames.chat.container.maxWidth,
container?.maxWidth ?? defaultContainerMaxWidth
)
documentStyle.setProperty(
cssVariableNames.chat.container.maxHeight,
container?.maxHeight ?? defaultContainerMaxHeight
)
const opacity = isBgDisabled
? '1'
: (container?.opacity ?? defaultOpacity).toString()
documentStyle.setProperty(
cssVariableNames.chat.container.opacity,
isBgDisabled ? '0' : (container?.opacity ?? defaultOpacity).toString()
)
documentStyle.setProperty(
cssVariableNames.chat.container.blur,
opacity === '1' || isBgDisabled
? '0xp'
: `${container?.blur ?? defaultBlur}px`
)
setShadow(
container?.shadow,
documentStyle,
cssVariableNames.chat.container.boxShadow
)
setBorderRadius(
container?.border ?? {
roundeness: legacyRoundness ?? defaultRoundness,
},
documentStyle,
cssVariableNames.chat.container.borderRadius
)
documentStyle.setProperty(
cssVariableNames.chat.container.borderWidth,
isDefined(container?.border?.thickness)
? `${container?.border?.thickness}px`
: '0'
)
documentStyle.setProperty(
cssVariableNames.chat.container.borderOpacity,
isDefined(container?.border?.opacity)
? container.border.opacity.toString()
: defaultOpacity.toString()
)
documentStyle.setProperty(
cssVariableNames.chat.container.borderColor,
hexToRgb(container?.border?.color ?? '').join(', ')
)
}
const setHostBubbles = (
hostBubbles: ContainerColors,
documentStyle: CSSStyleDeclaration
hostBubbles: ContainerTheme | undefined,
documentStyle: CSSStyleDeclaration,
legacyRoundness?: ChatTheme['roundness']
) => {
documentStyle.setProperty(
cssVariableNames.chat.hostBubbles.bgColor,
hostBubbles.backgroundColor ?? defaultTheme.chat.hostBubbles.backgroundColor
hexToRgb(
hostBubbles?.backgroundColor ?? defaultHostBubblesBackgroundColor
).join(', ')
)
documentStyle.setProperty(
cssVariableNames.chat.hostBubbles.color,
hostBubbles.color ?? defaultTheme.chat.hostBubbles.color
hostBubbles?.color ?? defaultHostBubblesColor
)
setBorderRadius(
hostBubbles?.border ?? {
roundeness: legacyRoundness ?? defaultRoundness,
},
documentStyle,
cssVariableNames.chat.hostBubbles.borderRadius
)
documentStyle.setProperty(
cssVariableNames.chat.hostBubbles.borderWidth,
isDefined(hostBubbles?.border?.thickness)
? `${hostBubbles?.border?.thickness}px`
: '0'
)
documentStyle.setProperty(
cssVariableNames.chat.hostBubbles.borderColor,
hexToRgb(hostBubbles?.border?.color ?? '').join(', ')
)
documentStyle.setProperty(
cssVariableNames.chat.hostBubbles.opacity,
hostBubbles?.backgroundColor === 'transparent'
? '0'
: isDefined(hostBubbles?.opacity)
? hostBubbles.opacity.toString()
: defaultOpacity.toString()
)
documentStyle.setProperty(
cssVariableNames.chat.hostBubbles.borderOpacity,
isDefined(hostBubbles?.border?.opacity)
? hostBubbles.border.opacity.toString()
: defaultOpacity.toString()
)
documentStyle.setProperty(
cssVariableNames.chat.hostBubbles.blur,
isDefined(hostBubbles?.blur)
? `${hostBubbles.blur ?? 0}px`
: defaultBlur.toString()
)
setShadow(
hostBubbles?.shadow,
documentStyle,
cssVariableNames.chat.hostBubbles.boxShadow
)
}
const setGuestBubbles = (
guestBubbles: ContainerColors,
documentStyle: CSSStyleDeclaration
guestBubbles: ContainerTheme | undefined,
documentStyle: CSSStyleDeclaration,
legacyRoundness?: ChatTheme['roundness']
) => {
documentStyle.setProperty(
cssVariableNames.chat.guestBubbles.bgColor,
guestBubbles.backgroundColor ??
defaultTheme.chat.guestBubbles.backgroundColor
hexToRgb(
guestBubbles?.backgroundColor ?? defaultGuestBubblesBackgroundColor
).join(', ')
)
documentStyle.setProperty(
cssVariableNames.chat.guestBubbles.color,
guestBubbles.color ?? defaultTheme.chat.guestBubbles.color
guestBubbles?.color ?? defaultGuestBubblesColor
)
setBorderRadius(
guestBubbles?.border ?? {
roundeness: legacyRoundness ?? defaultRoundness,
},
documentStyle,
cssVariableNames.chat.guestBubbles.borderRadius
)
documentStyle.setProperty(
cssVariableNames.chat.guestBubbles.borderWidth,
isDefined(guestBubbles?.border?.thickness)
? `${guestBubbles?.border?.thickness}px`
: '0'
)
documentStyle.setProperty(
cssVariableNames.chat.guestBubbles.borderColor,
hexToRgb(guestBubbles?.border?.color ?? '').join(', ')
)
documentStyle.setProperty(
cssVariableNames.chat.guestBubbles.borderOpacity,
isDefined(guestBubbles?.border?.opacity)
? guestBubbles.border.opacity.toString()
: defaultOpacity.toString()
)
documentStyle.setProperty(
cssVariableNames.chat.guestBubbles.opacity,
guestBubbles?.backgroundColor === 'transparent'
? '0'
: isDefined(guestBubbles?.opacity)
? guestBubbles.opacity.toString()
: defaultOpacity.toString()
)
documentStyle.setProperty(
cssVariableNames.chat.guestBubbles.blur,
isDefined(guestBubbles?.blur)
? `${guestBubbles.blur ?? 0}px`
: defaultBlur.toString()
)
setShadow(
guestBubbles?.shadow,
documentStyle,
cssVariableNames.chat.guestBubbles.boxShadow
)
}
const setButtons = (
buttons: ContainerColors,
documentStyle: CSSStyleDeclaration
buttons: ContainerTheme | undefined,
documentStyle: CSSStyleDeclaration,
legacyRoundness?: ChatTheme['roundness']
) => {
const bgColor =
buttons.backgroundColor ?? defaultTheme.chat.buttons.backgroundColor
documentStyle.setProperty(cssVariableNames.chat.buttons.bgColor, bgColor)
const bgColor = buttons?.backgroundColor ?? defaultButtonsBackgroundColor
documentStyle.setProperty(
cssVariableNames.chat.buttons.bgColorRgb,
cssVariableNames.chat.buttons.bgRgb,
hexToRgb(bgColor).join(', ')
)
documentStyle.setProperty(
cssVariableNames.chat.buttons.bgRgb,
hexToRgb(bgColor).join(', ')
)
documentStyle.setProperty(
cssVariableNames.chat.buttons.color,
buttons.color ?? defaultTheme.chat.buttons.color
buttons?.color ?? defaultButtonsColor
)
setBorderRadius(
buttons?.border ?? {
roundeness: legacyRoundness ?? defaultRoundness,
},
documentStyle,
cssVariableNames.chat.buttons.borderRadius
)
documentStyle.setProperty(
cssVariableNames.chat.buttons.borderWidth,
isDefined(buttons?.border?.thickness)
? `${buttons?.border?.thickness}px`
: `${defaultButtonsBorderThickness}px`
)
documentStyle.setProperty(
cssVariableNames.chat.buttons.borderColor,
hexToRgb(
buttons?.border?.color ??
buttons?.backgroundColor ??
defaultButtonsBackgroundColor
).join(', ')
)
documentStyle.setProperty(
cssVariableNames.chat.buttons.borderOpacity,
isDefined(buttons?.border?.opacity)
? buttons.border.opacity.toString()
: defaultOpacity.toString()
)
documentStyle.setProperty(
cssVariableNames.chat.buttons.opacity,
buttons?.backgroundColor === 'transparent'
? '0'
: isDefined(buttons?.opacity)
? buttons.opacity.toString()
: defaultOpacity.toString()
)
documentStyle.setProperty(
cssVariableNames.chat.buttons.blur,
isDefined(buttons?.blur) ? `${buttons.blur ?? 0}px` : defaultBlur.toString()
)
setShadow(
buttons?.shadow,
documentStyle,
cssVariableNames.chat.buttons.boxShadow
)
}
const setInputs = (inputs: InputColors, documentStyle: CSSStyleDeclaration) => {
const setInputs = (
inputs: InputTheme | undefined,
documentStyle: CSSStyleDeclaration,
legacyRoundness?: ChatTheme['roundness']
) => {
documentStyle.setProperty(
cssVariableNames.chat.inputs.bgColor,
inputs.backgroundColor ?? defaultTheme.chat.inputs.backgroundColor
hexToRgb(inputs?.backgroundColor ?? defaultInputsBackgroundColor).join(', ')
)
documentStyle.setProperty(
cssVariableNames.chat.inputs.color,
inputs.color ?? defaultTheme.chat.inputs.color
inputs?.color ?? defaultInputsColor
)
documentStyle.setProperty(
cssVariableNames.chat.inputs.placeholderColor,
inputs.placeholderColor ?? defaultTheme.chat.inputs.placeholderColor
inputs?.placeholderColor ?? defaultInputsPlaceholderColor
)
setBorderRadius(
inputs?.border ?? {
roundeness: legacyRoundness ?? defaultRoundness,
},
documentStyle,
cssVariableNames.chat.inputs.borderRadius
)
documentStyle.setProperty(
cssVariableNames.chat.inputs.borderWidth,
isDefined(inputs?.border?.thickness)
? `${inputs?.border?.thickness}px`
: '0'
)
documentStyle.setProperty(
cssVariableNames.chat.inputs.borderColor,
hexToRgb(inputs?.border?.color ?? '').join(', ')
)
documentStyle.setProperty(
cssVariableNames.chat.inputs.opacity,
inputs?.backgroundColor === 'transparent'
? '0'
: isDefined(inputs?.opacity)
? inputs.opacity.toString()
: defaultOpacity.toString()
)
documentStyle.setProperty(
cssVariableNames.chat.inputs.blur,
isDefined(inputs?.blur) ? `${inputs.blur ?? 0}px` : defaultBlur.toString()
)
setShadow(
inputs?.shadow ?? defaultInputsShadow,
documentStyle,
cssVariableNames.chat.inputs.boxShadow
)
}
const setTypebotBackground = (
background: Background,
const setCheckbox = (
container: ChatTheme['container'],
generalBackground: GeneralTheme['background'],
documentStyle: CSSStyleDeclaration
) => {
const chatContainerBgColor =
container?.backgroundColor ?? defaultContainerBackgroundColor
const isChatBgTransparent =
chatContainerBgColor === 'transparent' ||
isEmpty(chatContainerBgColor) ||
(container?.opacity ?? defaultOpacity) <= 0.2
if (isChatBgTransparent) {
const bgType = generalBackground?.type ?? defaultBackgroundType
documentStyle.setProperty(
cssVariableNames.chat.checkbox.bgRgb,
bgType === BackgroundType.IMAGE
? 'rgba(255, 255, 255, 0.75)'
: hexToRgb(
(bgType === BackgroundType.COLOR
? generalBackground?.content
: '#ffffff') ?? '#ffffff'
).join(', ')
)
if (bgType === BackgroundType.IMAGE) {
documentStyle.setProperty(cssVariableNames.chat.checkbox.alphaRatio, '3')
} else {
documentStyle.setProperty(
cssVariableNames.chat.checkbox.alphaRatio,
generalBackground?.content && isLight(generalBackground?.content)
? '1'
: '2'
)
}
} else {
documentStyle.setProperty(
cssVariableNames.chat.checkbox.bgRgb,
hexToRgb(chatContainerBgColor)
.concat(container?.opacity ?? 1)
.join(', ')
)
documentStyle.setProperty(
cssVariableNames.chat.checkbox.alphaRatio,
isLight(chatContainerBgColor) ? '1' : '2'
)
}
}
const setGeneralBackground = (
background: Background | undefined,
documentStyle: CSSStyleDeclaration
) => {
documentStyle.setProperty(cssVariableNames.general.bgImage, null)
documentStyle.setProperty(cssVariableNames.general.bgColor, null)
documentStyle.setProperty(
background?.type === BackgroundType.IMAGE
(background?.type ?? defaultBackgroundType) === BackgroundType.IMAGE
? cssVariableNames.general.bgImage
: cssVariableNames.general.bgColor,
parseBackgroundValue(background)
parseBackgroundValue({
type: background?.type ?? defaultBackgroundType,
content: background?.content ?? defaultBackgroundColor,
})
)
documentStyle.setProperty(
cssVariableNames.chat.checkbox.bgColor,
background?.type === BackgroundType.IMAGE
? 'rgba(255, 255, 255, 0.75)'
: (background?.type === BackgroundType.COLOR
? background.content
: '#ffffff') ?? '#ffffff'
)
const backgroundColor =
background.type === BackgroundType.IMAGE
? '#000000'
: background?.type === BackgroundType.COLOR &&
isNotEmpty(background.content)
? background.content
: '#ffffff'
documentStyle.setProperty(
cssVariableNames.general.color,
isLight(backgroundColor) ? '#303235' : '#ffffff'
)
if (background.type === BackgroundType.IMAGE) {
documentStyle.setProperty(cssVariableNames.chat.checkbox.baseAlpha, '0.40')
} else {
documentStyle.setProperty(cssVariableNames.chat.checkbox.baseAlpha, '0')
}
}
const parseBackgroundValue = ({ type, content }: Background = {}) => {
@ -261,25 +626,80 @@ const parseBackgroundValue = ({ type, content }: Background = {}) => {
return 'transparent'
case undefined:
case BackgroundType.COLOR:
return content ?? defaultTheme.general.background.content
return content ?? defaultBackgroundColor
case BackgroundType.IMAGE:
return `url(${content})`
}
}
const setRoundness = (
roundness: NonNullable<ChatTheme['roundness']>,
documentStyle: CSSStyleDeclaration
const setBorderRadius = (
border: ContainerBorderTheme,
documentStyle: CSSStyleDeclaration,
variableName: string
) => {
switch (roundness) {
switch (border?.roundeness ?? defaultRoundness) {
case 'none': {
documentStyle.setProperty(variableName, '0')
break
}
case 'medium': {
documentStyle.setProperty(variableName, '6px')
break
}
case 'large': {
documentStyle.setProperty(variableName, '20px')
break
}
case 'custom': {
documentStyle.setProperty(
variableName,
`${border.customRoundeness ?? 6}px`
)
break
}
}
}
// Props taken from https://tailwindcss.com/docs/box-shadow
const setShadow = (
shadow: ContainerTheme['shadow'],
documentStyle: CSSStyleDeclaration,
variableName: string
) => {
if (shadow === undefined) {
documentStyle.setProperty(variableName, '0 0 #0000')
return
}
switch (shadow) {
case 'none':
documentStyle.setProperty('--typebot-border-radius', '0')
documentStyle.setProperty(variableName, '0 0 #0000')
break
case 'medium':
documentStyle.setProperty('--typebot-border-radius', '6px')
case 'sm':
documentStyle.setProperty(variableName, '0 1px 2px 0 rgb(0 0 0 / 0.05)')
break
case 'large':
documentStyle.setProperty('--typebot-border-radius', '20px')
case 'md':
documentStyle.setProperty(
variableName,
'0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)'
)
break
case 'lg':
documentStyle.setProperty(
variableName,
'0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)'
)
break
case 'xl':
documentStyle.setProperty(
variableName,
'0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1)'
)
break
case '2xl':
documentStyle.setProperty(
variableName,
'0 25px 50px -12px rgb(0 0 0 / 0.25)'
)
break
}
}

View File

@ -1,6 +1,6 @@
{
"name": "@typebot.io/nextjs",
"version": "0.2.64",
"version": "0.2.73",
"description": "Convenient library to display typebots on your Next.js website",
"main": "dist/index.js",
"types": "dist/index.d.ts",

View File

@ -1,6 +1,6 @@
{
"name": "@typebot.io/react",
"version": "0.2.64",
"version": "0.2.73",
"description": "Convenient library to display typebots on your React app",
"main": "dist/index.js",
"types": "dist/index.d.ts",

View File

@ -1,13 +1,13 @@
{
"name": "@typebot.io/wordpress",
"version": "3.5.0",
"version": "3.6.0",
"main": "index.js",
"repository": "https://github.com/baptisteArno/typebot.io",
"author": "baptisteArno",
"license": "AGPL-3.0-or-later",
"scripts": {
"deploy": "pnpm copy && pnpm commit",
"copy": "svn copy ./trunk ./tags/3.5.0",
"commit": "svn ci -m 'Add lib_version attr in shortcode'"
"copy": "svn copy ./trunk ./tags/3.6.0",
"commit": "svn ci -m 'Add lib_version attr in admin panel'"
}
}

View File

@ -2,10 +2,10 @@
Contributors: baptisteArno
Tags: typebot, forms, surveys, quizzes, form builder, survey builder, quiz builder, custom forms, mobile forms, payment forms, order forms, feedback forms, enquiry forms, stripe, dropbox, google sheets, mailchimp, salesforce, hubspot, activecampaign, infusionsoft, asana, hipchat, slack, trello, zendesk
Requires at least: 5.0
Tested up to: 6.0
Tested up to: 6.5
License: GPL 2.0
License URI: http://www.gnu.org/licenses/gpl-2.0.txt
Stable Tag: 3.5.0
Stable Tag: 3.6.0
== Description ==
Collect 4x more responses with conversational apps using Typebot.
@ -23,6 +23,10 @@ This plugin relies on Typebot which is a tool that allows you to create conversa
2. Activate the plugin through the Plugins menu in WordPress
3. Activate your Typebot with the "Typebot" admin button located in the sidebar
== Changelog ==
= 3.6.0 =
* Add the lib_version attribute to wp admin panel
== Changelog ==
= 3.5.0 =
* Add the lib_version attribute in shortcode

View File

@ -25,6 +25,7 @@ class Typebot_Admin
public function register_typebot_settings()
{
register_setting('typebot', 'lib_version');
register_setting('typebot', 'init_snippet');
register_setting('typebot', 'excluded_pages');
}

View File

@ -9,6 +9,11 @@
settings_fields('typebot');
do_settings_sections('typebot');
?>
<div style="display: flex; flex-direction: column">
<label>Library version:</label>
<input name="lib_version" value="<?php echo esc_attr(get_option('lib_version') !== null && get_option('lib_version') !== '' ? get_option('lib_version') : '0.2'); ?>" style="padding: .5rem" />
</div>
<div style="display: flex; flex-direction: column">
<label>If embedding as <strong>Popup</strong> or <strong>Bubble</strong>, paste the initialization snippet here:</label>
<textarea name="init_snippet" style="min-height: 150px; padding: 0.5rem; margin-top: 1rem"><?php echo esc_attr(get_option('init_snippet')); ?></textarea>

View File

@ -40,7 +40,8 @@ class Typebot_Public
function typebot_script()
{
echo '<script type="module">import Typebot from "https://cdn.jsdelivr.net/npm/@typebot.io/js@0.2.15/dist/web.js";';
$lib_version = get_option('lib_version') !== null && get_option('lib_version') !== '' ? get_option('lib_version') : '0.2';
echo '<script type="module">import Typebot from "https://cdn.jsdelivr.net/npm/@typebot.io/js@'.$lib_version.'/dist/web.js";';
if (
get_option('excluded_pages') !== null &&
get_option('excluded_pages') !== ''

View File

@ -3,7 +3,7 @@
/**
* Plugin Name: Typebot
* Description: Convert more with conversational forms
* Version: 3.5.0
* Version: 3.6.0
* Author: Typebot
* Author URI: http://typebot.io/
* License: GPL-2.0+
@ -16,7 +16,7 @@ if (!defined('WPINC')) {
die();
}
define('TYPEBOT_VERSION', '3.5.0');
define('TYPEBOT_VERSION', '3.6.0');
function activate_typebot()
{

View File

@ -377,6 +377,18 @@ export const phoneCountries = [
code: 'DO',
dial_code: '+1849',
},
{
name: 'Dominican Republic',
flag: '🇩🇴',
code: 'DO',
dial_code: '+1829',
},
{
name: 'Dominican Republic',
flag: '🇩🇴',
code: 'DO',
dial_code: '+1809',
},
{
name: 'Ecuador',
flag: '🇪🇨',

View File

@ -110,6 +110,7 @@ export const clientSideActionSchema = z.discriminatedUnion('type', [
messages: z.array(
nativeMessageSchema.pick({ content: true, role: true })
),
runtime: z.enum(['edge', 'nodejs']),
}),
})
.merge(clientSideActionBaseSchema)
@ -151,6 +152,7 @@ export const clientSideActionSchema = z.discriminatedUnion('type', [
.object({
type: z.literal('stream'),
stream: z.literal(true),
runtime: z.enum(['edge', 'nodejs']),
})
.merge(clientSideActionBaseSchema)
.openapi({

View File

@ -1,5 +1,3 @@
import { Theme } from './schema'
export enum BackgroundType {
COLOR = 'Color',
IMAGE = 'Image',
@ -11,37 +9,63 @@ 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',
hostBubbles: { backgroundColor: '#F7F8FF', color: '#303235' },
guestBubbles: { backgroundColor: '#FF8E21', color: '#FFFFFF' },
buttons: { backgroundColor: '#0042DA', color: '#FFFFFF' },
inputs: {
backgroundColor: '#FFFFFF',
color: '#303235',
placeholderColor: '#9095A0',
},
hostAvatar: {
isEnabled: true,
},
guestAvatar: {
isEnabled: false,
},
},
general: {
font: {
type: 'Google',
family: 'Open Sans',
},
background: { type: BackgroundType.COLOR, content: '#ffffff' },
progressBar: {
isEnabled: false,
color: '#0042DA',
backgroundColor: '#e0edff',
thickness: 4,
position: 'absolute',
placement: 'Top',
},
},
} as const satisfies Theme
export const shadows = ['none', 'sm', 'md', 'lg', 'xl', '2xl'] as const
export const borderRoundness = ['none', 'medium', 'large', 'custom'] as const
export const defaultLightTextColor = '#303235'
export const defaultDarkTextColor = '#FFFFFF'
/*---- General ----*/
// Font
export const defaultFontType = 'Google'
export const defaultFontFamily = 'Open Sans'
// Background
export const defaultBackgroundType = BackgroundType.COLOR
export const defaultBackgroundColor = '#ffffff'
// Progress bar
export const defaultProgressBarIsEnabled = false
export const defaultProgressBarColor = '#0042DA'
export const defaultProgressBarBackgroundColor = '#e0edff'
export const defaultProgressBarThickness = 4
export const defaultProgressBarPosition = 'absolute'
export const defaultProgressBarPlacement = 'Top'
export const defaultRoundness = 'medium'
export const defaultOpacity = 1
export const defaultBlur = 0
/*---- Chat ----*/
// Container
export const defaultContainerMaxWidth = '800px'
export const defaultContainerMaxHeight = '100%'
export const defaultContainerBackgroundColor = 'transparent'
export const defaultContainerColor = '#27272A'
// Host bubbles
export const defaultHostBubblesBackgroundColor = '#F7F8FF'
export const defaultHostBubblesColor = defaultLightTextColor
// Guest bubbles
export const defaultGuestBubblesBackgroundColor = '#FF8E21'
export const defaultGuestBubblesColor = defaultDarkTextColor
// Buttons
export const defaultButtonsBackgroundColor = '#0042DA'
export const defaultButtonsColor = defaultDarkTextColor
export const defaultButtonsBorderThickness = 1
// Inputs
export const defaultInputsBackgroundColor = '#FFFFFF'
export const defaultInputsColor = defaultLightTextColor
export const defaultInputsPlaceholderColor = '#9095A0'
export const defaultInputsShadow = 'md'
// Host avatar
export const defaultHostAvatarIsEnabled = true
// Guest avatar
export const defaultGuestAvatarIsEnabled = false

View File

@ -2,9 +2,11 @@ import { ThemeTemplate as ThemeTemplatePrisma } from '@typebot.io/prisma'
import { z } from '../../../zod'
import {
BackgroundType,
borderRoundness,
fontTypes,
progressBarPlacements,
progressBarPositions,
shadows,
} from './constants'
const avatarPropsSchema = z.object({
@ -12,25 +14,48 @@ const avatarPropsSchema = z.object({
url: z.string().optional(),
})
const containerColorsSchema = z.object({
backgroundColor: z.string().optional(),
const containerBorderThemeSchema = z.object({
thickness: z.number().optional(),
color: z.string().optional(),
roundeness: z.enum(borderRoundness).optional(),
customRoundeness: z.number().optional(),
opacity: z.number().min(0).max(1).optional(),
})
const inputColorsSchema = containerColorsSchema.merge(
z.object({
placeholderColor: z.string().optional(),
export type ContainerBorderTheme = z.infer<typeof containerBorderThemeSchema>
const containerThemeSchema = z.object({
backgroundColor: z.string().optional(),
color: z.string().optional(),
blur: z.number().optional(),
opacity: z.number().min(0).max(1).optional(),
shadow: z.enum(shadows).optional(),
border: containerBorderThemeSchema.optional(),
})
const inputThemeSchema = containerThemeSchema.extend({
placeholderColor: z.string().optional(),
})
const chatContainerSchema = z
.object({
maxWidth: z.string().optional(),
maxHeight: z.string().optional(),
})
)
.merge(containerThemeSchema)
export const chatThemeSchema = z.object({
container: chatContainerSchema.optional(),
hostAvatar: avatarPropsSchema.optional(),
guestAvatar: avatarPropsSchema.optional(),
hostBubbles: containerColorsSchema.optional(),
guestBubbles: containerColorsSchema.optional(),
buttons: containerColorsSchema.optional(),
inputs: inputColorsSchema.optional(),
roundness: z.enum(['none', 'medium', 'large']).optional(),
hostBubbles: containerThemeSchema.optional(),
guestBubbles: containerThemeSchema.optional(),
buttons: containerThemeSchema.optional(),
inputs: inputThemeSchema.optional(),
roundness: z
.enum(['none', 'medium', 'large'])
.optional()
.describe('Deprecated, use `container.border.roundeness` instead'),
})
const backgroundSchema = z.object({
@ -98,6 +123,6 @@ export type ChatTheme = z.infer<typeof chatThemeSchema>
export type AvatarProps = z.infer<typeof avatarPropsSchema>
export type GeneralTheme = z.infer<typeof generalThemeSchema>
export type Background = z.infer<typeof backgroundSchema>
export type ContainerColors = z.infer<typeof containerColorsSchema>
export type InputColors = z.infer<typeof inputColorsSchema>
export type ContainerTheme = z.infer<typeof containerThemeSchema>
export type InputTheme = z.infer<typeof inputThemeSchema>
export type ThemeTemplate = z.infer<typeof themeTemplateSchema>

View File

@ -0,0 +1,41 @@
import { isLight } from '@typebot.io/lib/hexToRgb'
import { ContainerTheme, GeneralTheme } from '@typebot.io/schemas'
import {
BackgroundType,
defaultBackgroundColor,
defaultBackgroundType,
defaultContainerBackgroundColor,
defaultOpacity,
} from '@typebot.io/schemas/features/typebot/theme/constants'
import { isEmpty, isNotEmpty } from '@typebot.io/lib'
type Props = {
chatContainer: ContainerTheme | undefined
generalBackground: GeneralTheme['background']
}
export const isChatContainerLight = ({
chatContainer,
generalBackground,
}: Props): boolean => {
const chatContainerBgColor =
chatContainer?.backgroundColor ?? defaultContainerBackgroundColor
const ignoreChatBackground =
(chatContainer?.opacity ?? defaultOpacity) <= 0.3 ||
chatContainerBgColor === 'transparent' ||
isEmpty(chatContainerBgColor)
if (ignoreChatBackground) {
const bgType = generalBackground?.type ?? defaultBackgroundType
const backgroundColor =
bgType === BackgroundType.IMAGE
? '#000000'
: bgType === BackgroundType.COLOR &&
isNotEmpty(generalBackground?.content)
? generalBackground.content
: '#ffffff'
return isLight(backgroundColor)
}
return isLight(chatContainer?.backgroundColor ?? defaultBackgroundColor)
}

View File

@ -0,0 +1,13 @@
{
"name": "@typebot.io/theme",
"version": "1.0.0",
"description": "",
"scripts": {},
"keywords": [],
"author": "Baptiste Arnaud",
"license": "ISC",
"dependencies": {
"@typebot.io/schemas": "workspace:*",
"@typebot.io/lib": "workspace:*"
}
}

15
pnpm-lock.yaml generated
View File

@ -122,6 +122,9 @@ importers:
'@typebot.io/nextjs':
specifier: workspace:*
version: link:../../packages/embeds/nextjs
'@typebot.io/theme':
specifier: workspace:*
version: link:../../packages/theme
'@udecode/cn':
specifier: 29.0.1
version: 29.0.1(@types/react@18.2.15)(class-variance-authority@0.7.0)(react-dom@18.2.0)(react@18.2.0)(tailwind-merge@2.2.1)
@ -1054,6 +1057,9 @@ importers:
'@typebot.io/schemas':
specifier: workspace:*
version: link:../../schemas
'@typebot.io/theme':
specifier: workspace:*
version: link:../../theme
'@typebot.io/tsconfig':
specifier: workspace:*
version: link:../../tsconfig
@ -1931,6 +1937,15 @@ importers:
specifier: workspace:*
version: link:../tsconfig
packages/theme:
dependencies:
'@typebot.io/lib':
specifier: workspace:*
version: link:../lib
'@typebot.io/schemas':
specifier: workspace:*
version: link:../schemas
packages/transactional:
dependencies:
'@react-email/components':