@ -594,3 +594,13 @@ export const TableIcon = (props: IconProps) => (
|
||||
<path d="M9 3H5a2 2 0 0 0-2 2v4m6-6h10a2 2 0 0 1 2 2v4M9 3v18m0 0h10a2 2 0 0 0 2-2V9M9 21H5a2 2 0 0 1-2-2V9m0 0h18"></path>
|
||||
</Icon>
|
||||
)
|
||||
|
||||
export const ShuffleIcon = (props: IconProps) => (
|
||||
<Icon viewBox="0 0 24 24" {...featherIconsBaseProps} {...props}>
|
||||
<polyline points="16 3 21 3 21 8"></polyline>
|
||||
<line x1="4" y1="20" x2="21" y2="3"></line>
|
||||
<polyline points="21 16 21 21 16 21"></polyline>
|
||||
<line x1="15" y1="15" x2="21" y2="21"></line>
|
||||
<line x1="4" y1="4" x2="9" y2="9"></line>
|
||||
</Icon>
|
||||
)
|
||||
|
@ -9,6 +9,7 @@ import {
|
||||
HStack,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
Stack,
|
||||
} from '@chakra-ui/react'
|
||||
import { Variable, VariableString } from '@typebot.io/schemas'
|
||||
import { useEffect, useState } from 'react'
|
||||
@ -27,6 +28,7 @@ type Props<HasVariable extends boolean> = {
|
||||
label?: string
|
||||
moreInfoTooltip?: string
|
||||
isRequired?: boolean
|
||||
direction?: 'row' | 'column'
|
||||
onValueChange: (value?: Value<HasVariable>) => void
|
||||
} & Omit<NumberInputProps, 'defaultValue' | 'value' | 'onChange' | 'isRequired'>
|
||||
|
||||
@ -38,6 +40,7 @@ export const NumberInput = <HasVariable extends boolean>({
|
||||
label,
|
||||
moreInfoTooltip,
|
||||
isRequired,
|
||||
direction,
|
||||
...props
|
||||
}: Props<HasVariable>) => {
|
||||
const [value, setValue] = useState(defaultValue?.toString() ?? '')
|
||||
@ -90,7 +93,7 @@ export const NumberInput = <HasVariable extends boolean>({
|
||||
|
||||
return (
|
||||
<FormControl
|
||||
as={HStack}
|
||||
as={direction === 'column' ? Stack : HStack}
|
||||
isRequired={isRequired}
|
||||
justifyContent="space-between"
|
||||
width={label ? 'full' : 'auto'}
|
||||
|
@ -8,7 +8,7 @@ import {
|
||||
} from '@chakra-ui/react'
|
||||
import { PlusIcon } from '@/components/icons'
|
||||
import { useTypebot } from '@/features/editor/providers/TypebotProvider'
|
||||
import { ButtonItem, ItemIndices, ItemType } from '@typebot.io/schemas'
|
||||
import { ButtonItem, Item, ItemIndices, ItemType } from '@typebot.io/schemas'
|
||||
import React, { useRef, useState } from 'react'
|
||||
import { isNotDefined } from '@typebot.io/lib'
|
||||
|
||||
@ -26,7 +26,9 @@ export const ButtonsItemNode = ({ item, indices, isMouseOver }: Props) => {
|
||||
const handleInputSubmit = () => {
|
||||
if (itemValue === '') deleteItem(indices)
|
||||
else
|
||||
updateItem(indices, { content: itemValue === '' ? undefined : itemValue })
|
||||
updateItem(indices, {
|
||||
content: itemValue === '' ? undefined : itemValue,
|
||||
} as Item)
|
||||
}
|
||||
|
||||
const handleKeyPress = (e: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
|
24
apps/builder/src/features/blocks/logic/abTest/abTest.spec.ts
Normal file
24
apps/builder/src/features/blocks/logic/abTest/abTest.spec.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import test, { expect } from '@playwright/test'
|
||||
import { importTypebotInDatabase } from '@typebot.io/lib/playwright/databaseActions'
|
||||
import { createId } from '@paralleldrive/cuid2'
|
||||
import { getTestAsset } from '@/test/utils/playwright'
|
||||
|
||||
const typebotId = createId()
|
||||
|
||||
test.describe('AB Test block', () => {
|
||||
test('its configuration should work', async ({ page }) => {
|
||||
await importTypebotInDatabase(getTestAsset('typebots/logic/abTest.json'), {
|
||||
id: typebotId,
|
||||
})
|
||||
|
||||
await page.goto(`/typebots/${typebotId}/edit`)
|
||||
await page.getByText('A 50%').click()
|
||||
await page.getByLabel('Percent of users to follow A:').fill('100')
|
||||
await expect(page.getByText('A 100%')).toBeVisible()
|
||||
await expect(page.getByText('B 0%')).toBeVisible()
|
||||
await page.getByRole('button', { name: 'Preview' }).click()
|
||||
await expect(
|
||||
page.locator('typebot-standard').getByText('How are you?')
|
||||
).toBeVisible()
|
||||
})
|
||||
})
|
@ -0,0 +1,7 @@
|
||||
import { ShuffleIcon } from '@/components/icons'
|
||||
import { IconProps } from '@chakra-ui/react'
|
||||
import React from 'react'
|
||||
|
||||
export const AbTestIcon = (props: IconProps) => (
|
||||
<ShuffleIcon color="purple.500" {...props} />
|
||||
)
|
@ -0,0 +1,66 @@
|
||||
import React from 'react'
|
||||
import { Flex, Stack, useColorModeValue, Text, Tag } from '@chakra-ui/react'
|
||||
import { AbTestBlock } from '@typebot.io/schemas'
|
||||
import { SourceEndpoint } from '@/features/graph/components/endpoints/SourceEndpoint'
|
||||
|
||||
type Props = {
|
||||
block: AbTestBlock
|
||||
}
|
||||
|
||||
export const AbTestNodeBody = ({ block }: Props) => {
|
||||
const borderColor = useColorModeValue('gray.200', 'gray.700')
|
||||
const bg = useColorModeValue('white', undefined)
|
||||
|
||||
return (
|
||||
<Stack spacing={2} w="full">
|
||||
<Flex
|
||||
pos="relative"
|
||||
align="center"
|
||||
shadow="sm"
|
||||
rounded="md"
|
||||
bg={bg}
|
||||
borderWidth={'1px'}
|
||||
borderColor={borderColor}
|
||||
w="full"
|
||||
>
|
||||
<Text p="3">
|
||||
A <Tag>{block.options.aPercent}%</Tag>
|
||||
</Text>
|
||||
<SourceEndpoint
|
||||
source={{
|
||||
groupId: block.groupId,
|
||||
blockId: block.id,
|
||||
itemId: block.items[0].id,
|
||||
}}
|
||||
pos="absolute"
|
||||
right="-49px"
|
||||
pointerEvents="all"
|
||||
/>
|
||||
</Flex>
|
||||
<Flex
|
||||
pos="relative"
|
||||
align="center"
|
||||
shadow="sm"
|
||||
rounded="md"
|
||||
bg={bg}
|
||||
borderWidth={'1px'}
|
||||
borderColor={borderColor}
|
||||
w="full"
|
||||
>
|
||||
<Text p="3">
|
||||
B <Tag>{100 - block.options.aPercent}%</Tag>
|
||||
</Text>
|
||||
<SourceEndpoint
|
||||
source={{
|
||||
groupId: block.groupId,
|
||||
blockId: block.id,
|
||||
itemId: block.items[1].id,
|
||||
}}
|
||||
pos="absolute"
|
||||
right="-49px"
|
||||
pointerEvents="all"
|
||||
/>
|
||||
</Flex>
|
||||
</Stack>
|
||||
)
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
import { Stack } from '@chakra-ui/react'
|
||||
import React from 'react'
|
||||
import { isDefined } from '@typebot.io/lib'
|
||||
import { AbTestBlock } from '@typebot.io/schemas'
|
||||
import { NumberInput } from '@/components/inputs'
|
||||
|
||||
type Props = {
|
||||
options: AbTestBlock['options']
|
||||
onOptionsChange: (options: AbTestBlock['options']) => void
|
||||
}
|
||||
|
||||
export const AbTestSettings = ({ options, onOptionsChange }: Props) => {
|
||||
const updateAPercent = (aPercent?: number) =>
|
||||
isDefined(aPercent) ? onOptionsChange({ ...options, aPercent }) : null
|
||||
|
||||
return (
|
||||
<Stack spacing={4}>
|
||||
<NumberInput
|
||||
defaultValue={options.aPercent}
|
||||
onValueChange={updateAPercent}
|
||||
withVariableButton={false}
|
||||
label="Percent of users to follow A:"
|
||||
direction="column"
|
||||
max={100}
|
||||
min={0}
|
||||
/>
|
||||
</Stack>
|
||||
)
|
||||
}
|
@ -37,6 +37,7 @@ import { ConditionIcon } from '@/features/blocks/logic/condition/components/Cond
|
||||
import { RedirectIcon } from '@/features/blocks/logic/redirect/components/RedirectIcon'
|
||||
import { SetVariableIcon } from '@/features/blocks/logic/setVariable/components/SetVariableIcon'
|
||||
import { TypebotLinkIcon } from '@/features/blocks/logic/typebotLink/components/TypebotLinkIcon'
|
||||
import { AbTestIcon } from '@/features/blocks/logic/abTest/components/AbTestIcon'
|
||||
|
||||
type BlockIconProps = { type: BlockType } & IconProps
|
||||
|
||||
@ -91,6 +92,8 @@ export const BlockIcon = ({ type, ...props }: BlockIconProps): JSX.Element => {
|
||||
return <JumpIcon color={purple} {...props} />
|
||||
case LogicBlockType.TYPEBOT_LINK:
|
||||
return <TypebotLinkIcon color={purple} {...props} />
|
||||
case LogicBlockType.AB_TEST:
|
||||
return <AbTestIcon color={purple} {...props} />
|
||||
case IntegrationBlockType.GOOGLE_SHEETS:
|
||||
return <GoogleSheetsLogo {...props} />
|
||||
case IntegrationBlockType.GOOGLE_ANALYTICS:
|
||||
|
@ -57,6 +57,8 @@ export const BlockLabel = ({ type }: Props): JSX.Element => {
|
||||
return <Text>Wait</Text>
|
||||
case LogicBlockType.JUMP:
|
||||
return <Text>Jump</Text>
|
||||
case LogicBlockType.AB_TEST:
|
||||
return <Text>AB Test</Text>
|
||||
case IntegrationBlockType.GOOGLE_SHEETS:
|
||||
return <Text>Sheets</Text>
|
||||
case IntegrationBlockType.GOOGLE_ANALYTICS:
|
||||
|
@ -7,6 +7,8 @@ import {
|
||||
Block,
|
||||
LogicBlockType,
|
||||
InputBlockType,
|
||||
ConditionItem,
|
||||
ButtonItem,
|
||||
} from '@typebot.io/schemas'
|
||||
import { SetTypebot } from '../TypebotProvider'
|
||||
import produce from 'immer'
|
||||
@ -15,7 +17,11 @@ import { byId, blockHasItems } from '@typebot.io/lib'
|
||||
import { createId } from '@paralleldrive/cuid2'
|
||||
import { WritableDraft } from 'immer/dist/types/types-external'
|
||||
|
||||
type NewItem = Pick<Item, 'blockId' | 'outgoingEdgeId' | 'type'> & Partial<Item>
|
||||
type NewItem = Pick<
|
||||
ConditionItem | ButtonItem,
|
||||
'blockId' | 'outgoingEdgeId' | 'type'
|
||||
> &
|
||||
Partial<ConditionItem | ButtonItem>
|
||||
|
||||
export type ItemsActions = {
|
||||
createItem: (item: NewItem, indices: ItemIndices) => void
|
||||
|
@ -39,6 +39,7 @@ import { TypebotLinkNode } from '@/features/blocks/logic/typebotLink/components/
|
||||
import { ItemNodesList } from '../item/ItemNodesList'
|
||||
import { GoogleAnalyticsNodeBody } from '@/features/blocks/integrations/googleAnalytics/components/GoogleAnalyticsNodeBody'
|
||||
import { ChatwootNodeBody } from '@/features/blocks/integrations/chatwoot/components/ChatwootNodeBody'
|
||||
import { AbTestNodeBody } from '@/features/blocks/logic/abTest/components/AbTestNodeBody'
|
||||
|
||||
type Props = {
|
||||
block: Block | StartBlock
|
||||
@ -142,6 +143,9 @@ export const BlockNodeContent = ({ block, indices }: Props): JSX.Element => {
|
||||
case LogicBlockType.JUMP: {
|
||||
return <JumpNodeBody options={block.options} />
|
||||
}
|
||||
case LogicBlockType.AB_TEST: {
|
||||
return <AbTestNodeBody block={block} />
|
||||
}
|
||||
case LogicBlockType.TYPEBOT_LINK:
|
||||
return <TypebotLinkNode block={block} />
|
||||
case LogicBlockType.CONDITION:
|
||||
|
@ -45,6 +45,7 @@ import { DateInputSettings } from '@/features/blocks/inputs/date/components/Date
|
||||
import { PhoneInputSettings } from '@/features/blocks/inputs/phone/components/PhoneInputSettings'
|
||||
import { GoogleSheetsSettings } from '@/features/blocks/integrations/googleSheets/components/GoogleSheetsSettings'
|
||||
import { ChatwootSettings } from '@/features/blocks/integrations/chatwoot/components/ChatwootSettings'
|
||||
import { AbTestSettings } from '@/features/blocks/logic/abTest/components/AbTestSettings'
|
||||
|
||||
type Props = {
|
||||
block: BlockWithOptions
|
||||
@ -229,6 +230,14 @@ export const BlockSettings = ({
|
||||
/>
|
||||
)
|
||||
}
|
||||
case LogicBlockType.AB_TEST: {
|
||||
return (
|
||||
<AbTestSettings
|
||||
options={block.options}
|
||||
onOptionsChange={updateOptions}
|
||||
/>
|
||||
)
|
||||
}
|
||||
case IntegrationBlockType.GOOGLE_SHEETS: {
|
||||
return (
|
||||
<GoogleSheetsSettings
|
||||
|
@ -1,6 +1,11 @@
|
||||
import { Flex, useColorModeValue } from '@chakra-ui/react'
|
||||
import { useTypebot } from '@/features/editor/providers/TypebotProvider'
|
||||
import { ChoiceInputBlock, Item, ItemIndices } from '@typebot.io/schemas'
|
||||
import {
|
||||
ChoiceInputBlock,
|
||||
Item,
|
||||
ItemIndices,
|
||||
ItemType,
|
||||
} from '@typebot.io/schemas'
|
||||
import React, { useRef, useState } from 'react'
|
||||
import { SourceEndpoint } from '../../endpoints/SourceEndpoint'
|
||||
import { ItemNodeContent } from './ItemNodeContent'
|
||||
@ -9,6 +14,7 @@ import { ContextMenu } from '@/components/ContextMenu'
|
||||
import { isDefined } from '@typebot.io/lib'
|
||||
import { Coordinates } from '@/features/graph/types'
|
||||
import {
|
||||
DraggabbleItem,
|
||||
NodePosition,
|
||||
useDragDistance,
|
||||
} from '@/features/graph/providers/GraphDndProvider'
|
||||
@ -20,7 +26,7 @@ type Props = {
|
||||
indices: ItemIndices
|
||||
onMouseDown?: (
|
||||
blockNodePosition: { absolute: Coordinates; relative: Coordinates },
|
||||
item: Item
|
||||
item: DraggabbleItem
|
||||
) => void
|
||||
connectionDisabled?: boolean
|
||||
}
|
||||
@ -33,7 +39,7 @@ export const ItemNode = ({
|
||||
}: Props) => {
|
||||
const previewingBorderColor = useColorModeValue('blue.400', 'blue.300')
|
||||
const borderColor = useColorModeValue('gray.200', 'gray.700')
|
||||
const bg = useColorModeValue('white', undefined)
|
||||
const bg = useColorModeValue('white', 'gray.850')
|
||||
const { typebot } = useTypebot()
|
||||
const { previewingEdge } = useGraph()
|
||||
const [isMouseOver, setIsMouseOver] = useState(false)
|
||||
@ -48,7 +54,7 @@ export const ItemNode = ({
|
||||
| undefined
|
||||
)?.options?.isMultipleChoice
|
||||
const onDrag = (position: NodePosition) => {
|
||||
if (!onMouseDown) return
|
||||
if (!onMouseDown || item.type === ItemType.AB_TEST) return
|
||||
onMouseDown(position, item)
|
||||
}
|
||||
useDragDistance({
|
||||
|
@ -28,5 +28,7 @@ export const ItemNodeContent = ({ item, indices, isMouseOver }: Props) => {
|
||||
indices={indices}
|
||||
/>
|
||||
)
|
||||
case ItemType.AB_TEST:
|
||||
return <></>
|
||||
}
|
||||
}
|
||||
|
@ -11,7 +11,6 @@ import {
|
||||
BlockIndices,
|
||||
BlockWithItems,
|
||||
LogicBlockType,
|
||||
Item,
|
||||
} from '@typebot.io/schemas'
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import { ItemNode } from './ItemNode'
|
||||
@ -20,6 +19,7 @@ import { isDefined } from '@typebot.io/lib'
|
||||
import {
|
||||
useBlockDnd,
|
||||
computeNearestPlaceholderIndex,
|
||||
DraggabbleItem,
|
||||
} from '@/features/graph/providers/GraphDndProvider'
|
||||
import { useGraph } from '@/features/graph/providers/GraphProvider'
|
||||
import { Coordinates } from '@dnd-kit/utilities'
|
||||
@ -107,7 +107,7 @@ export const ItemNodesList = ({
|
||||
(itemIndex: number) =>
|
||||
(
|
||||
{ absolute, relative }: { absolute: Coordinates; relative: Coordinates },
|
||||
item: Item
|
||||
item: DraggabbleItem
|
||||
) => {
|
||||
if (!typebot || block.items.length <= 1) return
|
||||
placeholderRefs.current.splice(itemIndex + 1, 1)
|
||||
|
@ -1,5 +1,10 @@
|
||||
import { useEventListener } from '@chakra-ui/react'
|
||||
import { DraggableBlock, DraggableBlockType, Item } from '@typebot.io/schemas'
|
||||
import {
|
||||
AbTestBlock,
|
||||
DraggableBlock,
|
||||
DraggableBlockType,
|
||||
Item,
|
||||
} from '@typebot.io/schemas'
|
||||
import {
|
||||
createContext,
|
||||
Dispatch,
|
||||
@ -18,13 +23,15 @@ type NodeElement = {
|
||||
element: HTMLDivElement
|
||||
}
|
||||
|
||||
export type DraggabbleItem = Exclude<Item, AbTestBlock['items'][number]>
|
||||
|
||||
const graphDndContext = createContext<{
|
||||
draggedBlockType?: DraggableBlockType
|
||||
setDraggedBlockType: Dispatch<SetStateAction<DraggableBlockType | undefined>>
|
||||
draggedBlock?: DraggableBlock
|
||||
setDraggedBlock: Dispatch<SetStateAction<DraggableBlock | undefined>>
|
||||
draggedItem?: Item
|
||||
setDraggedItem: Dispatch<SetStateAction<Item | undefined>>
|
||||
draggedItem?: DraggabbleItem
|
||||
setDraggedItem: Dispatch<SetStateAction<DraggabbleItem | undefined>>
|
||||
mouseOverGroup?: NodeElement
|
||||
setMouseOverGroup: (node: NodeElement | undefined) => void
|
||||
mouseOverBlock?: NodeElement
|
||||
@ -40,7 +47,7 @@ export const GraphDndProvider = ({ children }: { children: ReactNode }) => {
|
||||
const [draggedBlockType, setDraggedBlockType] = useState<
|
||||
DraggableBlockType | undefined
|
||||
>()
|
||||
const [draggedItem, setDraggedItem] = useState<Item | undefined>()
|
||||
const [draggedItem, setDraggedItem] = useState<DraggabbleItem | undefined>()
|
||||
const [mouseOverGroup, _setMouseOverGroup] = useState<NodeElement>()
|
||||
const [mouseOverBlock, _setMouseOverBlock] = useState<NodeElement>()
|
||||
|
||||
|
@ -1,7 +1,9 @@
|
||||
import { isChoiceInput, isConditionBlock, isDefined } from '@typebot.io/lib'
|
||||
import { Block, InputBlockType } from '@typebot.io/schemas'
|
||||
import { Block, InputBlockType, LogicBlockType } from '@typebot.io/schemas'
|
||||
|
||||
export const hasDefaultConnector = (block: Block) =>
|
||||
(!isChoiceInput(block) && !isConditionBlock(block)) ||
|
||||
(!isChoiceInput(block) &&
|
||||
!isConditionBlock(block) &&
|
||||
block.type !== LogicBlockType.AB_TEST) ||
|
||||
(block.type === InputBlockType.CHOICE &&
|
||||
isDefined(block.options.dynamicVariableId))
|
||||
|
@ -42,10 +42,14 @@ import {
|
||||
Item,
|
||||
ItemType,
|
||||
LogicBlockType,
|
||||
defaultAbTestOptions,
|
||||
} from '@typebot.io/schemas'
|
||||
|
||||
const parseDefaultItems = (
|
||||
type: LogicBlockType.CONDITION | InputBlockType.CHOICE,
|
||||
type:
|
||||
| LogicBlockType.CONDITION
|
||||
| InputBlockType.CHOICE
|
||||
| LogicBlockType.AB_TEST,
|
||||
blockId: string
|
||||
): Item[] => {
|
||||
switch (type) {
|
||||
@ -60,6 +64,11 @@ const parseDefaultItems = (
|
||||
content: defaultConditionContent,
|
||||
},
|
||||
]
|
||||
case LogicBlockType.AB_TEST:
|
||||
return [
|
||||
{ id: createId(), blockId, type: ItemType.AB_TEST, path: 'a' },
|
||||
{ id: createId(), blockId, type: ItemType.AB_TEST, path: 'b' },
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@ -112,6 +121,8 @@ const parseDefaultBlockOptions = (type: BlockWithOptionsType): BlockOptions => {
|
||||
return {}
|
||||
case LogicBlockType.TYPEBOT_LINK:
|
||||
return {}
|
||||
case LogicBlockType.AB_TEST:
|
||||
return defaultAbTestOptions
|
||||
case IntegrationBlockType.GOOGLE_SHEETS:
|
||||
return defaultGoogleSheetsOptions
|
||||
case IntegrationBlockType.GOOGLE_ANALYTICS:
|
||||
|
164
apps/builder/src/test/assets/typebots/logic/abTest.json
Normal file
164
apps/builder/src/test/assets/typebots/logic/abTest.json
Normal file
@ -0,0 +1,164 @@
|
||||
{
|
||||
"id": "clgkx6u4n00021ait8o4c1nvs",
|
||||
"version": "3",
|
||||
"createdAt": "2023-04-17T14:17:13.223Z",
|
||||
"updatedAt": "2023-04-17T14:17:13.223Z",
|
||||
"icon": null,
|
||||
"name": "My typebot",
|
||||
"folderId": null,
|
||||
"groups": [
|
||||
{
|
||||
"id": "bb5ad8yx3j52fecxdnztlap5",
|
||||
"title": "Start",
|
||||
"blocks": [
|
||||
{
|
||||
"id": "q20ww00q5nnv4icmi3rhbtda",
|
||||
"type": "start",
|
||||
"label": "Start",
|
||||
"groupId": "bb5ad8yx3j52fecxdnztlap5",
|
||||
"outgoingEdgeId": "qr2ebzm62bcz21jh2wmv1odd"
|
||||
}
|
||||
],
|
||||
"graphCoordinates": { "x": 0, "y": 0 }
|
||||
},
|
||||
{
|
||||
"id": "wv63xqdlwj6ub5vb3ulbmzd3",
|
||||
"graphCoordinates": { "x": 310, "y": 206 },
|
||||
"title": "Group #1",
|
||||
"blocks": [
|
||||
{
|
||||
"id": "ice9jqqw4glrx5kblisez4lp",
|
||||
"groupId": "wv63xqdlwj6ub5vb3ulbmzd3",
|
||||
"type": "text",
|
||||
"content": {
|
||||
"richText": [{ "type": "p", "children": [{ "text": "Hey" }] }]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "rorajfbwc2evvumo837ulwmy",
|
||||
"groupId": "wv63xqdlwj6ub5vb3ulbmzd3",
|
||||
"type": "AB test",
|
||||
"options": { "aPercent": 50 },
|
||||
"items": [
|
||||
{
|
||||
"id": "rcw6vr8xfcb5dmfx2ts04rb0",
|
||||
"blockId": "rorajfbwc2evvumo837ulwmy",
|
||||
"type": 2,
|
||||
"path": "a",
|
||||
"outgoingEdgeId": "lmof3n7qyjo5bkw9kaksgrzh"
|
||||
},
|
||||
{
|
||||
"id": "i9shyze7fyd5wfxfq0svg2lg",
|
||||
"blockId": "rorajfbwc2evvumo837ulwmy",
|
||||
"type": 2,
|
||||
"path": "b",
|
||||
"outgoingEdgeId": "kvaqhcb1df8cmvekwtpd1irr"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "ybebe0sxg6zu307a4ruwhmb6",
|
||||
"graphCoordinates": { "x": 705.96875, "y": 166.57421875 },
|
||||
"title": "Group #2",
|
||||
"blocks": [
|
||||
{
|
||||
"id": "jqqjruceaqwqvm74isgoxbez",
|
||||
"groupId": "ybebe0sxg6zu307a4ruwhmb6",
|
||||
"type": "text",
|
||||
"content": {
|
||||
"richText": [
|
||||
{ "type": "p", "children": [{ "text": "How are you?" }] }
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "d66ppqavqy5mac7fy9jo5lib",
|
||||
"graphCoordinates": { "x": 702.44921875, "y": 350.9921875 },
|
||||
"title": "Group #2 copy",
|
||||
"blocks": [
|
||||
{
|
||||
"id": "x74mxf3hby8hw7zo3k8b5vc1",
|
||||
"groupId": "d66ppqavqy5mac7fy9jo5lib",
|
||||
"type": "text",
|
||||
"content": {
|
||||
"richText": [
|
||||
{ "type": "p", "children": [{ "text": "What's up?" }] }
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"variables": [],
|
||||
"edges": [
|
||||
{
|
||||
"from": {
|
||||
"groupId": "wv63xqdlwj6ub5vb3ulbmzd3",
|
||||
"blockId": "rorajfbwc2evvumo837ulwmy",
|
||||
"itemId": "rcw6vr8xfcb5dmfx2ts04rb0"
|
||||
},
|
||||
"to": { "groupId": "ybebe0sxg6zu307a4ruwhmb6" },
|
||||
"id": "lmof3n7qyjo5bkw9kaksgrzh"
|
||||
},
|
||||
{
|
||||
"from": {
|
||||
"groupId": "wv63xqdlwj6ub5vb3ulbmzd3",
|
||||
"blockId": "rorajfbwc2evvumo837ulwmy",
|
||||
"itemId": "i9shyze7fyd5wfxfq0svg2lg"
|
||||
},
|
||||
"to": { "groupId": "d66ppqavqy5mac7fy9jo5lib" },
|
||||
"id": "kvaqhcb1df8cmvekwtpd1irr"
|
||||
},
|
||||
{
|
||||
"from": {
|
||||
"groupId": "bb5ad8yx3j52fecxdnztlap5",
|
||||
"blockId": "q20ww00q5nnv4icmi3rhbtda"
|
||||
},
|
||||
"to": { "groupId": "wv63xqdlwj6ub5vb3ulbmzd3" },
|
||||
"id": "qr2ebzm62bcz21jh2wmv1odd"
|
||||
}
|
||||
],
|
||||
"theme": {
|
||||
"chat": {
|
||||
"inputs": {
|
||||
"color": "#303235",
|
||||
"backgroundColor": "#FFFFFF",
|
||||
"placeholderColor": "#9095A0"
|
||||
},
|
||||
"buttons": { "color": "#FFFFFF", "backgroundColor": "#0042DA" },
|
||||
"hostAvatar": {
|
||||
"url": "https://avatars.githubusercontent.com/u/16015833?v=4",
|
||||
"isEnabled": true
|
||||
},
|
||||
"hostBubbles": { "color": "#303235", "backgroundColor": "#F7F8FF" },
|
||||
"guestBubbles": { "color": "#FFFFFF", "backgroundColor": "#FF8E21" }
|
||||
},
|
||||
"general": {
|
||||
"font": "Open Sans",
|
||||
"background": { "type": "Color", "content": "#ffffff" }
|
||||
}
|
||||
},
|
||||
"selectedThemeTemplateId": null,
|
||||
"settings": {
|
||||
"general": {
|
||||
"isBrandingEnabled": false,
|
||||
"isInputPrefillEnabled": true,
|
||||
"isHideQueryParamsEnabled": true,
|
||||
"isNewResultOnRefreshEnabled": true
|
||||
},
|
||||
"metadata": {
|
||||
"description": "Build beautiful conversational forms and embed them directly in your applications without a line of code. Triple your response rate and collect answers that has more value compared to a traditional form."
|
||||
},
|
||||
"typingEmulation": { "speed": 300, "enabled": true, "maxDelay": 1.5 }
|
||||
},
|
||||
"publicId": null,
|
||||
"customDomain": null,
|
||||
"workspaceId": "proWorkspace",
|
||||
"resultsTablePreferences": null,
|
||||
"isArchived": false,
|
||||
"isClosed": false
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
import { AbTestBlock, SessionState } from '@typebot.io/schemas'
|
||||
import { ExecuteLogicResponse } from '@/features/chat/types'
|
||||
|
||||
export const executeAbTest = (
|
||||
_: SessionState,
|
||||
block: AbTestBlock
|
||||
): ExecuteLogicResponse => {
|
||||
const aEdgeId = block.items[0].outgoingEdgeId
|
||||
const random = Math.random() * 100
|
||||
if (random < block.options.aPercent && aEdgeId) {
|
||||
return { outgoingEdgeId: aEdgeId }
|
||||
}
|
||||
const bEdgeId = block.items[1].outgoingEdgeId
|
||||
if (bEdgeId) return { outgoingEdgeId: bEdgeId }
|
||||
return { outgoingEdgeId: block.outgoingEdgeId }
|
||||
}
|
@ -7,6 +7,7 @@ import { executeRedirect } from '@/features/blocks/logic/redirect/executeRedirec
|
||||
import { executeCondition } from '@/features/blocks/logic/condition/executeCondition'
|
||||
import { executeSetVariable } from '@/features/blocks/logic/setVariable/executeSetVariable'
|
||||
import { executeTypebotLink } from '@/features/blocks/logic/typebotLink/executeTypebotLink'
|
||||
import { executeAbTest } from '@/features/blocks/logic/abTest/executeAbTest'
|
||||
|
||||
export const executeLogic =
|
||||
(state: SessionState, lastBubbleBlockId?: string) =>
|
||||
@ -26,5 +27,7 @@ export const executeLogic =
|
||||
return executeWait(state, block, lastBubbleBlockId)
|
||||
case LogicBlockType.JUMP:
|
||||
return executeJumpBlock(state, block.options)
|
||||
case LogicBlockType.AB_TEST:
|
||||
return executeAbTest(state, block)
|
||||
}
|
||||
}
|
||||
|
@ -132,8 +132,13 @@ export const blockTypeHasWebhook = (
|
||||
|
||||
export const blockTypeHasItems = (
|
||||
type: BlockType
|
||||
): type is LogicBlockType.CONDITION | InputBlockType.CHOICE =>
|
||||
type === LogicBlockType.CONDITION || type === InputBlockType.CHOICE
|
||||
): type is
|
||||
| LogicBlockType.CONDITION
|
||||
| InputBlockType.CHOICE
|
||||
| LogicBlockType.AB_TEST =>
|
||||
type === LogicBlockType.CONDITION ||
|
||||
type === InputBlockType.CHOICE ||
|
||||
type === LogicBlockType.AB_TEST
|
||||
|
||||
export const blockHasItems = (
|
||||
block: Block
|
||||
|
31
packages/schemas/features/blocks/logic/abTest.ts
Normal file
31
packages/schemas/features/blocks/logic/abTest.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { z } from 'zod'
|
||||
import { blockBaseSchema } from '../baseSchemas'
|
||||
import { LogicBlockType } from './enums'
|
||||
import { itemBaseSchema } from '../../items/baseSchemas'
|
||||
import { ItemType } from '../../items/enums'
|
||||
|
||||
export const aItemSchema = itemBaseSchema.extend({
|
||||
type: z.literal(ItemType.AB_TEST),
|
||||
path: z.literal('a'),
|
||||
})
|
||||
|
||||
export const bItemSchema = itemBaseSchema.extend({
|
||||
type: z.literal(ItemType.AB_TEST),
|
||||
path: z.literal('b'),
|
||||
})
|
||||
|
||||
export const abTestBlockSchema = blockBaseSchema.merge(
|
||||
z.object({
|
||||
type: z.enum([LogicBlockType.AB_TEST]),
|
||||
items: z.tuple([aItemSchema, bItemSchema]),
|
||||
options: z.object({
|
||||
aPercent: z.number().min(0).max(100),
|
||||
}),
|
||||
})
|
||||
)
|
||||
|
||||
export const defaultAbTestOptions = {
|
||||
aPercent: 50,
|
||||
}
|
||||
|
||||
export type AbTestBlock = z.infer<typeof abTestBlockSchema>
|
@ -6,4 +6,5 @@ export enum LogicBlockType {
|
||||
TYPEBOT_LINK = 'Typebot link',
|
||||
WAIT = 'Wait',
|
||||
JUMP = 'Jump',
|
||||
AB_TEST = 'AB test',
|
||||
}
|
||||
|
@ -5,3 +5,4 @@ export * from './redirect'
|
||||
export * from './setVariable'
|
||||
export * from './typebotLink'
|
||||
export * from './wait'
|
||||
export * from './abTest'
|
||||
|
@ -42,6 +42,8 @@ import {
|
||||
setVariableBlockSchema,
|
||||
typebotLinkBlockSchema,
|
||||
waitBlockSchema,
|
||||
abTestBlockSchema,
|
||||
AbTestBlock,
|
||||
} from './logic'
|
||||
import { jumpBlockSchema } from './logic/jump'
|
||||
|
||||
@ -79,7 +81,7 @@ export type BlockOptions =
|
||||
| LogicBlockOptions
|
||||
| IntegrationBlockOptions
|
||||
|
||||
export type BlockWithItems = ConditionBlock | ChoiceInputBlock
|
||||
export type BlockWithItems = ConditionBlock | ChoiceInputBlock | AbTestBlock
|
||||
|
||||
export type BlockBase = z.infer<typeof blockBaseSchema>
|
||||
|
||||
@ -123,6 +125,7 @@ export const logicBlockSchema = z.discriminatedUnion('type', [
|
||||
typebotLinkBlockSchema,
|
||||
waitBlockSchema,
|
||||
jumpBlockSchema,
|
||||
abTestBlockSchema,
|
||||
])
|
||||
|
||||
export type LogicBlock = z.infer<typeof logicBlockSchema>
|
||||
|
@ -1,4 +1,5 @@
|
||||
export enum ItemType {
|
||||
BUTTON,
|
||||
CONDITION,
|
||||
AB_TEST,
|
||||
}
|
||||
|
@ -1,7 +1,11 @@
|
||||
import { z } from 'zod'
|
||||
import { buttonItemSchema } from '../blocks/inputs/choice'
|
||||
import { conditionItemSchema } from '../blocks/logic/condition'
|
||||
import { aItemSchema, bItemSchema } from '../blocks'
|
||||
|
||||
const itemSchema = buttonItemSchema.or(conditionItemSchema)
|
||||
const itemSchema = buttonItemSchema
|
||||
.or(conditionItemSchema)
|
||||
.or(aItemSchema)
|
||||
.or(bItemSchema)
|
||||
|
||||
export type Item = z.infer<typeof itemSchema>
|
||||
|
Reference in New Issue
Block a user