2
0

Add Wait block

Closes #142
This commit is contained in:
Baptiste Arnaud
2023-01-26 18:23:09 +01:00
parent ee864d9729
commit fa9e4b7b67
29 changed files with 621 additions and 313 deletions

View File

@@ -25,9 +25,7 @@ import { useParentModal } from '@/features/graph/providers/ParentModalProvider'
type Props = { type Props = {
initialVariableId?: string initialVariableId?: string
autoFocus?: boolean autoFocus?: boolean
onSelectVariable: ( onSelectVariable: (variable: Pick<Variable, 'id' | 'name'>) => void
variable: Pick<Variable, 'id' | 'name'> | undefined
) => void
} & InputProps } & InputProps
export const VariableSearchInput = ({ export const VariableSearchInput = ({
@@ -70,7 +68,6 @@ export const VariableSearchInput = ({
const onInputChange = (e: ChangeEvent<HTMLInputElement>) => { const onInputChange = (e: ChangeEvent<HTMLInputElement>) => {
setInputValue(e.target.value) setInputValue(e.target.value)
if (e.target.value === '') { if (e.target.value === '') {
onSelectVariable(undefined)
setFilteredItems([...variables.slice(0, 50)]) setFilteredItems([...variables.slice(0, 50)])
return return
} }

View File

@@ -10,16 +10,19 @@ import { useEffect, useState } from 'react'
import { useDebouncedCallback } from 'use-debounce' import { useDebouncedCallback } from 'use-debounce'
import { env } from 'utils' import { env } from 'utils'
type Props = {
value?: number
debounceTimeout?: number
withVariableButton?: boolean
onValueChange: (value?: number) => void
} & NumberInputProps
export const SmartNumberInput = ({ export const SmartNumberInput = ({
value, value,
onValueChange, onValueChange,
debounceTimeout = 1000, debounceTimeout = 1000,
...props ...props
}: { }: Props) => {
value?: number
debounceTimeout?: number
onValueChange: (value?: number) => void
} & NumberInputProps) => {
const [currentValue, setCurrentValue] = useState(value?.toString() ?? '') const [currentValue, setCurrentValue] = useState(value?.toString() ?? '')
const debounced = useDebouncedCallback( const debounced = useDebouncedCallback(
onValueChange, onValueChange,

View File

@@ -0,0 +1,14 @@
import { featherIconsBaseProps } from '@/components/icons'
import { Icon, IconProps, useColorModeValue } from '@chakra-ui/react'
export const WaitIcon = (props: IconProps) => (
<Icon
viewBox="0 0 24 24"
color={useColorModeValue('purple.500', 'purple.300')}
{...featherIconsBaseProps}
{...props}
>
<circle cx="12" cy="12" r="10"></circle>
<polyline points="12 6 12 12 16 14"></polyline>
</Icon>
)

View File

@@ -0,0 +1,13 @@
import { Text } from '@chakra-ui/react'
import { WaitOptions } from 'models'
import React from 'react'
type Props = {
options: WaitOptions
}
export const WaitNodeContent = ({ options: { secondsToWaitFor } }: Props) => (
<Text color={secondsToWaitFor ? 'currentcolor' : 'gray.500'} noOfLines={1}>
{secondsToWaitFor ? `Wait for ${secondsToWaitFor}s` : 'Configure...'}
</Text>
)

View File

@@ -0,0 +1,26 @@
import { Stack } from '@chakra-ui/react'
import { WaitOptions } from 'models'
import React from 'react'
import { Input } from '@/components/inputs'
type Props = {
options: WaitOptions
onOptionsChange: (options: WaitOptions) => void
}
export const WaitSettings = ({ options, onOptionsChange }: Props) => {
const handleSecondsChange = (secondsToWaitFor: string | undefined) => {
onOptionsChange({ ...options, secondsToWaitFor })
}
return (
<Stack spacing={4}>
<Input
label="Seconds to wait for:"
defaultValue={options.secondsToWaitFor}
onChange={handleSecondsChange}
placeholder="0"
/>
</Stack>
)
}

View File

@@ -0,0 +1,26 @@
import test, { expect } from '@playwright/test'
import { typebotViewer } from 'utils/playwright/testHelpers'
import { importTypebotInDatabase } from 'utils/playwright/databaseActions'
import cuid from 'cuid'
import { getTestAsset } from '@/test/utils/playwright'
const typebotId = cuid()
test.describe('Wait block', () => {
test('wait should trigger', async ({ page }) => {
await importTypebotInDatabase(getTestAsset('typebots/logic/wait.json'), {
id: typebotId,
})
await page.goto(`/typebots/${typebotId}/edit`)
await page.click('text=Configure...')
await page.getByRole('textbox', { name: 'Seconds to wait for:' }).fill('3')
await page.click('text=Preview')
await typebotViewer(page).locator('text=Wait now').click()
await page.waitForTimeout(1000)
await expect(typebotViewer(page).locator('text="Hi there!"')).toBeHidden()
await page.waitForTimeout(3000)
await expect(typebotViewer(page).locator('text="Hi there!"')).toBeVisible()
})
})

View File

@@ -36,6 +36,7 @@ import { TextInputIcon } from '@/features/blocks/inputs/textInput'
import { EmbedBubbleIcon } from '@/features/blocks/bubbles/embed' import { EmbedBubbleIcon } from '@/features/blocks/bubbles/embed'
import { GoogleAnalyticsLogo } from '@/features/blocks/integrations/googleAnalytics' import { GoogleAnalyticsLogo } from '@/features/blocks/integrations/googleAnalytics'
import { AudioBubbleIcon } from '@/features/blocks/bubbles/audio' import { AudioBubbleIcon } from '@/features/blocks/bubbles/audio'
import { WaitIcon } from '@/features/blocks/logic/wait/components/WaitIcon'
type BlockIconProps = { type: BlockType } & IconProps type BlockIconProps = { type: BlockType } & IconProps
@@ -82,6 +83,8 @@ export const BlockIcon = ({ type, ...props }: BlockIconProps) => {
return <RedirectIcon color={purple} {...props} /> return <RedirectIcon color={purple} {...props} />
case LogicBlockType.CODE: case LogicBlockType.CODE:
return <CodeIcon color={purple} {...props} /> return <CodeIcon color={purple} {...props} />
case LogicBlockType.WAIT:
return <WaitIcon color={purple} {...props} />
case LogicBlockType.TYPEBOT_LINK: case LogicBlockType.TYPEBOT_LINK:
return <TypebotLinkIcon color={purple} {...props} /> return <TypebotLinkIcon color={purple} {...props} />
case IntegrationBlockType.GOOGLE_SHEETS: case IntegrationBlockType.GOOGLE_SHEETS:

View File

@@ -77,6 +77,8 @@ export const BlockTypeLabel = ({ type }: Props): JSX.Element => {
<Text>Typebot</Text> <Text>Typebot</Text>
</Tooltip> </Tooltip>
) )
case LogicBlockType.WAIT:
return <Text>Wait</Text>
case IntegrationBlockType.GOOGLE_SHEETS: case IntegrationBlockType.GOOGLE_SHEETS:
return ( return (
<Tooltip label="Google Sheets"> <Tooltip label="Google Sheets">

View File

@@ -37,6 +37,7 @@ import { SendEmailContent } from '@/features/blocks/integrations/sendEmail'
import { isInputBlock, isChoiceInput, blockHasItems } from 'utils' import { isInputBlock, isChoiceInput, blockHasItems } from 'utils'
import { MakeComContent } from '@/features/blocks/integrations/makeCom' import { MakeComContent } from '@/features/blocks/integrations/makeCom'
import { AudioBubbleNode } from '@/features/blocks/bubbles/audio' import { AudioBubbleNode } from '@/features/blocks/bubbles/audio'
import { WaitNodeContent } from '@/features/blocks/logic/wait/components/WaitNodeContent'
type Props = { type Props = {
block: Block | StartBlock block: Block | StartBlock
@@ -120,6 +121,9 @@ export const BlockNodeContent = ({ block, indices }: Props): JSX.Element => {
/> />
) )
} }
case LogicBlockType.WAIT: {
return <WaitNodeContent options={block.options} />
}
case LogicBlockType.TYPEBOT_LINK: case LogicBlockType.TYPEBOT_LINK:
return <TypebotLinkNode block={block} /> return <TypebotLinkNode block={block} />

View File

@@ -40,6 +40,8 @@ const getHelpDocUrl = (blockType: BlockWithOptions['type']): string | null => {
return 'https://docs.typebot.io/editor/blocks/logic/redirect' return 'https://docs.typebot.io/editor/blocks/logic/redirect'
case LogicBlockType.CODE: case LogicBlockType.CODE:
return 'https://docs.typebot.io/editor/blocks/logic/code' return 'https://docs.typebot.io/editor/blocks/logic/code'
case LogicBlockType.WAIT:
return 'https://docs.typebot.io/editor/blocks/logic/wait'
case InputBlockType.TEXT: case InputBlockType.TEXT:
return 'https://docs.typebot.io/editor/blocks/inputs/text' return 'https://docs.typebot.io/editor/blocks/inputs/text'
case InputBlockType.NUMBER: case InputBlockType.NUMBER:

View File

@@ -41,6 +41,7 @@ import { ButtonsOptionsForm } from '@/features/blocks/inputs/buttons'
import { ChatwootSettingsForm } from '@/features/blocks/integrations/chatwoot' import { ChatwootSettingsForm } from '@/features/blocks/integrations/chatwoot'
import { MakeComSettings } from '@/features/blocks/integrations/makeCom' import { MakeComSettings } from '@/features/blocks/integrations/makeCom'
import { HelpDocButton } from './HelpDocButton' import { HelpDocButton } from './HelpDocButton'
import { WaitSettings } from '@/features/blocks/logic/wait/components/WaitSettings'
type Props = { type Props = {
block: BlockWithOptions block: BlockWithOptions
@@ -212,6 +213,14 @@ export const BlockSettings = ({
/> />
) )
} }
case LogicBlockType.WAIT: {
return (
<WaitSettings
options={block.options}
onOptionsChange={handleOptionsChange}
/>
)
}
case IntegrationBlockType.GOOGLE_SHEETS: { case IntegrationBlockType.GOOGLE_SHEETS: {
return ( return (
<GoogleSheetsSettingsBody <GoogleSheetsSettingsBody

View File

@@ -37,6 +37,7 @@ import {
Item, Item,
ItemType, ItemType,
LogicBlockType, LogicBlockType,
defaultWaitOptions,
} from 'models' } from 'models'
import { import {
stubLength, stubLength,
@@ -434,6 +435,8 @@ const parseDefaultBlockOptions = (type: BlockWithOptionsType): BlockOptions => {
return defaultRedirectOptions return defaultRedirectOptions
case LogicBlockType.CODE: case LogicBlockType.CODE:
return defaultCodeOptions return defaultCodeOptions
case LogicBlockType.WAIT:
return defaultWaitOptions
case LogicBlockType.TYPEBOT_LINK: case LogicBlockType.TYPEBOT_LINK:
return {} return {}
case IntegrationBlockType.GOOGLE_SHEETS: case IntegrationBlockType.GOOGLE_SHEETS:

View File

@@ -13,9 +13,7 @@ import React from 'react'
import { VariableSearchInput } from '@/components/VariableSearchInput' import { VariableSearchInput } from '@/components/VariableSearchInput'
type Props = { type Props = {
onSelectVariable: ( onSelectVariable: (variable: Pick<Variable, 'name' | 'id'>) => void
variable: Pick<Variable, 'name' | 'id'> | undefined
) => void
} & Omit<IconButtonProps, 'aria-label'> } & Omit<IconButtonProps, 'aria-label'>
export const VariablesButton = ({ onSelectVariable, ...props }: Props) => { export const VariablesButton = ({ onSelectVariable, ...props }: Props) => {

View File

@@ -0,0 +1 @@
{"id":"clddbpdus00011asqclyl0vlw","createdAt":"2023-01-26T16:42:16.803Z","updatedAt":"2023-01-26T16:48:53.712Z","icon":null,"name":"My typebot","publishedTypebotId":null,"folderId":null,"groups":[{"id":"clddbpduq002ssq1a2h622bw0","title":"Start","blocks":[{"id":"clddbpduq002tsq1aghl4dh31","type":"start","label":"Start","groupId":"clddbpduq002ssq1a2h622bw0","outgoingEdgeId":"clddbq3yp000l3b6ss15lu48v"}],"graphCoordinates":{"x":0,"y":0}},{"id":"clddbpito000e3b6spz6qqipa","title":"Group #1","blocks":[{"id":"clddbpito000f3b6srs0cizys","type":"choice input","items":[{"id":"clddbpito000g3b6snvioor95","type":0,"blockId":"clddbpito000f3b6srs0cizys","content":"Wait now"}],"groupId":"clddbpito000e3b6spz6qqipa","options":{"buttonLabel":"Send","isMultipleChoice":false}},{"id":"clddbw55x000m3b6svz34enqq","groupId":"clddbpito000e3b6spz6qqipa","type":"Wait","options":{"secondsToWaitFor":""},"outgoingEdgeId":"clddbw6l4000n3b6st6le0hu9"}],"graphCoordinates":{"x":357.49609375,"y":148.40625}},{"id":"clddbpwh9000i3b6s9e1vdjrd","title":"Group #2","blocks":[{"id":"clddbpwh9000j3b6srnzdpbcx","type":"text","content":{"html":"<div>Hi there!</div>","richText":[{"type":"p","children":[{"text":"Hi there!"}]}],"plainText":"Hi there!"},"groupId":"clddbpwh9000i3b6s9e1vdjrd"}],"graphCoordinates":{"x":545.25,"y":447.36328125}}],"variables":[],"edges":[{"id":"clddbq3yp000l3b6ss15lu48v","to":{"groupId":"clddbpito000e3b6spz6qqipa"},"from":{"blockId":"clddbpduq002tsq1aghl4dh31","groupId":"clddbpduq002ssq1a2h622bw0"}},{"from":{"groupId":"clddbpito000e3b6spz6qqipa","blockId":"clddbw55x000m3b6svz34enqq"},"to":{"groupId":"clddbpwh9000i3b6s9e1vdjrd"},"id":"clddbw6l4000n3b6st6le0hu9"}],"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"}}},"settings":{"general":{"isBrandingEnabled":false,"isInputPrefillEnabled":true,"isResultSavingEnabled":true,"isHideQueryParamsEnabled":true,"isNewResultOnRefreshEnabled":false},"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}

View File

@@ -14,14 +14,6 @@ The "Code" block allows you to execute Javascript code. If you want to set a var
window.location.reload() window.location.reload()
``` ```
### Wait for 3 seconds
By default, Promises will be awaited. So something like this will work:
```js
return new Promise((res) => setTimeout(res, 3000))
```
### Post a message to parent ### Post a message to parent
```js ```js

View File

@@ -0,0 +1,9 @@
# Wait
The "Wait" block allows you to pause the conversation for a certain amount of seconds.
This can be useful if you want the bot to emphasize on what's been said or to wait before a redirection for example to make sure the user has read everything.
:::caution
This should be used wisely. If you want the bot to write slower or faster in a more general sense, you need to check the [Typing emulation settings](/editor/settings#typing-emulation)
:::

View File

@@ -1446,59 +1446,193 @@
{ {
"anyOf": [ "anyOf": [
{ {
"allOf": [ "anyOf": [
{ {
"type": "object", "allOf": [
"properties": { {
"id": {
"type": "string"
},
"groupId": {
"type": "string"
},
"outgoingEdgeId": {
"type": "string"
}
},
"required": [
"id",
"groupId"
],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"Code"
]
},
"options": {
"type": "object", "type": "object",
"properties": { "properties": {
"name": { "id": {
"type": "string" "type": "string"
}, },
"content": { "groupId": {
"type": "string" "type": "string"
}, },
"shouldExecuteInParentContext": { "outgoingEdgeId": {
"type": "boolean" "type": "string"
} }
}, },
"required": [ "required": [
"name" "id",
"groupId"
],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"Code"
]
},
"options": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"content": {
"type": "string"
},
"shouldExecuteInParentContext": {
"type": "boolean"
}
},
"required": [
"name"
],
"additionalProperties": false
}
},
"required": [
"type",
"options"
], ],
"additionalProperties": false "additionalProperties": false
} }
}, ]
"required": [ },
"type", {
"options" "allOf": [
], {
"additionalProperties": false "type": "object",
"properties": {
"id": {
"type": "string"
},
"groupId": {
"type": "string"
},
"outgoingEdgeId": {
"type": "string"
}
},
"required": [
"id",
"groupId"
],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"Condition"
]
},
"items": {
"type": "array",
"items": {
"allOf": [
{
"type": "object",
"properties": {
"id": {
"type": "string"
},
"blockId": {
"type": "string"
},
"outgoingEdgeId": {
"type": "string"
}
},
"required": [
"id",
"blockId"
],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"type": {
"type": "number",
"enum": [
1
]
},
"content": {
"type": "object",
"properties": {
"logicalOperator": {
"type": "string",
"enum": [
"OR",
"AND"
]
},
"comparisons": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"variableId": {
"type": "string"
},
"comparisonOperator": {
"type": "string",
"enum": [
"Equal to",
"Not equal",
"Contains",
"Greater than",
"Less than",
"Is set"
]
},
"value": {
"type": "string"
}
},
"required": [
"id"
],
"additionalProperties": false
}
}
},
"required": [
"logicalOperator",
"comparisons"
],
"additionalProperties": false
}
},
"required": [
"type",
"content"
],
"additionalProperties": false
}
]
}
}
},
"required": [
"type",
"items"
],
"additionalProperties": false
}
]
} }
] ]
}, },
@@ -1529,104 +1663,28 @@
"type": { "type": {
"type": "string", "type": "string",
"enum": [ "enum": [
"Condition" "Redirect"
] ]
}, },
"items": { "options": {
"type": "array", "type": "object",
"items": { "properties": {
"allOf": [ "url": {
{ "type": "string"
"type": "object", },
"properties": { "isNewTab": {
"id": { "type": "boolean"
"type": "string" }
}, },
"blockId": { "required": [
"type": "string" "isNewTab"
}, ],
"outgoingEdgeId": { "additionalProperties": false
"type": "string"
}
},
"required": [
"id",
"blockId"
],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"type": {
"type": "number",
"enum": [
1
]
},
"content": {
"type": "object",
"properties": {
"logicalOperator": {
"type": "string",
"enum": [
"OR",
"AND"
]
},
"comparisons": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"variableId": {
"type": "string"
},
"comparisonOperator": {
"type": "string",
"enum": [
"Equal to",
"Not equal",
"Contains",
"Greater than",
"Less than",
"Is set"
]
},
"value": {
"type": "string"
}
},
"required": [
"id"
],
"additionalProperties": false
}
}
},
"required": [
"logicalOperator",
"comparisons"
],
"additionalProperties": false
}
},
"required": [
"type",
"content"
],
"additionalProperties": false
}
]
}
} }
}, },
"required": [ "required": [
"type", "type",
"items" "options"
], ],
"additionalProperties": false "additionalProperties": false
} }
@@ -1661,22 +1719,19 @@
"type": { "type": {
"type": "string", "type": "string",
"enum": [ "enum": [
"Redirect" "Typebot link"
] ]
}, },
"options": { "options": {
"type": "object", "type": "object",
"properties": { "properties": {
"url": { "typebotId": {
"type": "string" "type": "string"
}, },
"isNewTab": { "groupId": {
"type": "boolean" "type": "string"
} }
}, },
"required": [
"isNewTab"
],
"additionalProperties": false "additionalProperties": false
} }
}, },
@@ -1717,17 +1772,20 @@
"type": { "type": {
"type": "string", "type": "string",
"enum": [ "enum": [
"Typebot link" "Set variable"
] ]
}, },
"options": { "options": {
"type": "object", "type": "object",
"properties": { "properties": {
"typebotId": { "variableId": {
"type": "string" "type": "string"
}, },
"groupId": { "expressionToEvaluate": {
"type": "string" "type": "string"
},
"isCode": {
"type": "boolean"
} }
}, },
"additionalProperties": false "additionalProperties": false
@@ -1770,20 +1828,14 @@
"type": { "type": {
"type": "string", "type": "string",
"enum": [ "enum": [
"Set variable" "Wait"
] ]
}, },
"options": { "options": {
"type": "object", "type": "object",
"properties": { "properties": {
"variableId": { "secondsToWaitFor": {
"type": "string" "type": "string"
},
"expressionToEvaluate": {
"type": "string"
},
"isCode": {
"type": "boolean"
} }
}, },
"additionalProperties": false "additionalProperties": false
@@ -4406,168 +4458,232 @@
} }
] ]
}, },
"logic": { "clientSideActions": {
"type": "object", "type": "array",
"properties": { "items": {
"redirect": { "anyOf": [
"type": "object", {
"properties": { "anyOf": [
"url": { {
"type": "string" "anyOf": [
}, {
"isNewTab": {
"type": "boolean"
}
},
"required": [
"isNewTab"
],
"additionalProperties": false
},
"codeToExecute": {
"type": "object",
"properties": {
"content": {
"type": "string"
},
"args": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"value": {
"anyOf": [ "anyOf": [
{ {
"not": {} "type": "object",
}, "properties": {
{ "codeToExecute": {
"anyOf": [ "type": "object",
{ "properties": {
"anyOf": [ "content": {
{
"type": "string" "type": "string"
}, },
{ "args": {
"type": "number" "type": "array",
} "items": {
] "type": "object",
}, "properties": {
{ "id": {
"type": "boolean" "type": "string"
} },
] "value": {
} "anyOf": [
], {
"nullable": true "not": {}
} },
}, {
"required": [ "anyOf": [
"id" {
], "anyOf": [
"additionalProperties": false {
} "type": "string"
} },
}, {
"required": [ "type": "number"
"content", }
"args" ]
], },
"additionalProperties": false {
} "type": "boolean"
}, }
"additionalProperties": false ]
}, }
"integrations": { ],
"type": "object", "nullable": true
"properties": { }
"chatwoot": {
"type": "object",
"properties": {
"codeToExecute": {
"type": "object",
"properties": {
"content": {
"type": "string"
},
"args": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"value": {
"anyOf": [
{
"not": {}
},
{
"anyOf": [
{
"anyOf": [
{
"type": "string"
}, },
{ "required": [
"type": "number" "id"
} ],
] "additionalProperties": false
}
}
},
"required": [
"content",
"args"
],
"additionalProperties": false
}
},
"required": [
"codeToExecute"
],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"redirect": {
"type": "object",
"properties": {
"url": {
"type": "string"
}, },
{ "isNewTab": {
"type": "boolean" "type": "boolean"
} }
] },
"required": [
"isNewTab"
],
"additionalProperties": false
} }
},
"required": [
"redirect"
], ],
"nullable": true "additionalProperties": false
}
]
},
{
"type": "object",
"properties": {
"chatwoot": {
"type": "object",
"properties": {
"codeToExecute": {
"type": "object",
"properties": {
"content": {
"type": "string"
},
"args": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"value": {
"anyOf": [
{
"not": {}
},
{
"anyOf": [
{
"anyOf": [
{
"type": "string"
},
{
"type": "number"
}
]
},
{
"type": "boolean"
}
]
}
],
"nullable": true
}
},
"required": [
"id"
],
"additionalProperties": false
}
}
},
"required": [
"content",
"args"
],
"additionalProperties": false
}
},
"required": [
"codeToExecute"
],
"additionalProperties": false
} }
}, },
"required": [ "required": [
"id" "chatwoot"
], ],
"additionalProperties": false "additionalProperties": false
} }
} ]
}, },
"required": [ {
"content", "type": "object",
"args" "properties": {
], "googleAnalytics": {
"additionalProperties": false "type": "object",
} "properties": {
"trackingId": {
"type": "string"
},
"category": {
"type": "string"
},
"action": {
"type": "string"
},
"label": {
"type": "string"
},
"value": {
"type": "number"
}
},
"additionalProperties": false
}
},
"required": [
"googleAnalytics"
],
"additionalProperties": false
}
]
}, },
"required": [ {
"codeToExecute" "type": "object",
], "properties": {
"additionalProperties": false "wait": {
}, "type": "object",
"googleAnalytics": { "properties": {
"type": "object", "secondsToWaitFor": {
"properties": { "type": "number"
"trackingId": { }
"type": "string" },
"required": [
"secondsToWaitFor"
],
"additionalProperties": false
}
}, },
"category": { "required": [
"type": "string" "wait"
}, ],
"action": { "additionalProperties": false
"type": "string" }
}, ]
"label": { }
"type": "string"
},
"value": {
"type": "number"
}
},
"additionalProperties": false
}
},
"additionalProperties": false
}, },
"sessionId": { "sessionId": {
"type": "string" "type": "string"

View File

@@ -0,0 +1,22 @@
import { ExecuteLogicResponse } from '@/features/chat'
import { parseVariables } from '@/features/variables'
import { SessionState, WaitBlock } from 'models'
export const executeWait = async (
{ typebot: { variables } }: SessionState,
block: WaitBlock
): Promise<ExecuteLogicResponse> => {
if (!block.options.secondsToWaitFor)
return { outgoingEdgeId: block.outgoingEdgeId }
const parsedSecondsToWaitFor = parseVariables(variables)(
block.options.secondsToWaitFor
)
return {
outgoingEdgeId: block.outgoingEdgeId,
// @ts-expect-error isNaN can be used with strings
clientSideActions: isNaN(parsedSecondsToWaitFor)
? undefined
: [{ wait: { secondsToWaitFor: parsedSecondsToWaitFor } }],
}
}

View File

@@ -3,6 +3,7 @@ import { executeCondition } from '@/features/blocks/logic/condition/api'
import { executeRedirect } from '@/features/blocks/logic/redirect/api' import { executeRedirect } from '@/features/blocks/logic/redirect/api'
import { executeSetVariable } from '@/features/blocks/logic/setVariable/api' import { executeSetVariable } from '@/features/blocks/logic/setVariable/api'
import { executeTypebotLink } from '@/features/blocks/logic/typebotLink/api' import { executeTypebotLink } from '@/features/blocks/logic/typebotLink/api'
import { executeWait } from '@/features/blocks/logic/wait/api/utils/executeWait'
import { LogicBlock, LogicBlockType, SessionState } from 'models' import { LogicBlock, LogicBlockType, SessionState } from 'models'
import { ExecuteLogicResponse } from '../../types' import { ExecuteLogicResponse } from '../../types'
@@ -20,5 +21,7 @@ export const executeLogic =
return executeCode(state, block) return executeCode(state, block)
case LogicBlockType.TYPEBOT_LINK: case LogicBlockType.TYPEBOT_LINK:
return executeTypebotLink(state, block) return executeTypebotLink(state, block)
case LogicBlockType.WAIT:
return executeWait(state, block)
} }
} }

View File

@@ -0,0 +1 @@
export { executeWait } from './utils/executeWait'

View File

@@ -0,0 +1,19 @@
import { parseVariables } from '@/features/variables'
import { LogicState } from '@/types'
import { WaitBlock } from 'models'
export const executeWait = async (
block: WaitBlock,
{ typebot: { variables } }: LogicState
) => {
if (!block.options.secondsToWaitFor) return block.outgoingEdgeId
const parsedSecondsToWaitFor = parseVariables(variables)(
block.options.secondsToWaitFor
)
// @ts-expect-error isNaN can be used with strings
if (isNaN(parsedSecondsToWaitFor)) return block.outgoingEdgeId
await new Promise((resolve) =>
setTimeout(resolve, parseInt(parsedSecondsToWaitFor) * 1000)
)
return block.outgoingEdgeId
}

View File

@@ -4,6 +4,7 @@ import { executeCondition } from '@/features/blocks/logic/condition'
import { executeRedirect } from '@/features/blocks/logic/redirect' import { executeRedirect } from '@/features/blocks/logic/redirect'
import { executeSetVariable } from '@/features/blocks/logic/setVariable' import { executeSetVariable } from '@/features/blocks/logic/setVariable'
import { executeTypebotLink } from '@/features/blocks/logic/typebotLink' import { executeTypebotLink } from '@/features/blocks/logic/typebotLink'
import { executeWait } from '@/features/blocks/logic/wait'
import { LinkedTypebot } from '@/providers/TypebotProvider' import { LinkedTypebot } from '@/providers/TypebotProvider'
import { EdgeId, LogicState } from '@/types' import { EdgeId, LogicState } from '@/types'
import { LogicBlock, LogicBlockType } from 'models' import { LogicBlock, LogicBlockType } from 'models'
@@ -26,5 +27,7 @@ export const executeLogic = async (
return { nextEdgeId: await executeCode(block, context) } return { nextEdgeId: await executeCode(block, context) }
case LogicBlockType.TYPEBOT_LINK: case LogicBlockType.TYPEBOT_LINK:
return executeTypebotLink(block, context) return executeTypebotLink(block, context)
case LogicBlockType.WAIT:
return { nextEdgeId: await executeWait(block, context) }
} }
} }

View File

@@ -0,0 +1,7 @@
type Props = {
secondsToWaitFor: number
}
export const executeWait = async ({ secondsToWaitFor }: Props) => {
await new Promise((resolve) => setTimeout(resolve, secondsToWaitFor * 1000))
}

View File

@@ -2,6 +2,7 @@ import { executeChatwoot } from '@/features/blocks/integrations/chatwoot'
import { executeGoogleAnalyticsBlock } from '@/features/blocks/integrations/googleAnalytics/utils/executeGoogleAnalytics' import { executeGoogleAnalyticsBlock } from '@/features/blocks/integrations/googleAnalytics/utils/executeGoogleAnalytics'
import { executeCode } from '@/features/blocks/logic/code' import { executeCode } from '@/features/blocks/logic/code'
import { executeRedirect } from '@/features/blocks/logic/redirect' import { executeRedirect } from '@/features/blocks/logic/redirect'
import { executeWait } from '@/features/blocks/logic/wait/utils/executeWait'
import type { ChatReply } from 'models' import type { ChatReply } from 'models'
export const executeClientSideAction = async ( export const executeClientSideAction = async (
@@ -19,4 +20,7 @@ export const executeClientSideAction = async (
if ('redirect' in clientSideAction) { if ('redirect' in clientSideAction) {
executeRedirect(clientSideAction.redirect) executeRedirect(clientSideAction.redirect)
} }
if ('wait' in clientSideAction) {
await executeWait(clientSideAction.wait)
}
} }

View File

@@ -4,4 +4,5 @@ export enum LogicBlockType {
REDIRECT = 'Redirect', REDIRECT = 'Redirect',
CODE = 'Code', CODE = 'Code',
TYPEBOT_LINK = 'Typebot link', TYPEBOT_LINK = 'Typebot link',
WAIT = 'Wait',
} }

View File

@@ -5,3 +5,4 @@ export * from './logicBlock'
export * from './redirect' export * from './redirect'
export * from './setVariable' export * from './setVariable'
export * from './typebotLink' export * from './typebotLink'
export * from './wait'

View File

@@ -4,17 +4,20 @@ import { conditionBlockSchema } from './condition'
import { redirectOptionsSchema, redirectBlockSchema } from './redirect' import { redirectOptionsSchema, redirectBlockSchema } from './redirect'
import { setVariableOptionsSchema, setVariableBlockSchema } from './setVariable' import { setVariableOptionsSchema, setVariableBlockSchema } from './setVariable'
import { typebotLinkOptionsSchema, typebotLinkBlockSchema } from './typebotLink' import { typebotLinkOptionsSchema, typebotLinkBlockSchema } from './typebotLink'
import { waitBlockSchema, waitOptionsSchema } from './wait'
const logicBlockOptionsSchema = codeOptionsSchema const logicBlockOptionsSchema = codeOptionsSchema
.or(redirectOptionsSchema) .or(redirectOptionsSchema)
.or(setVariableOptionsSchema) .or(setVariableOptionsSchema)
.or(typebotLinkOptionsSchema) .or(typebotLinkOptionsSchema)
.or(waitOptionsSchema)
export const logicBlockSchema = codeBlockSchema export const logicBlockSchema = codeBlockSchema
.or(conditionBlockSchema) .or(conditionBlockSchema)
.or(redirectBlockSchema) .or(redirectBlockSchema)
.or(typebotLinkBlockSchema) .or(typebotLinkBlockSchema)
.or(setVariableBlockSchema) .or(setVariableBlockSchema)
.or(waitBlockSchema)
export type LogicBlock = z.infer<typeof logicBlockSchema> export type LogicBlock = z.infer<typeof logicBlockSchema>
export type LogicBlockOptions = z.infer<typeof logicBlockOptionsSchema> export type LogicBlockOptions = z.infer<typeof logicBlockOptionsSchema>

View File

@@ -0,0 +1,19 @@
import { z } from 'zod'
import { blockBaseSchema } from '../baseSchemas'
import { LogicBlockType } from './enums'
export const waitOptionsSchema = z.object({
secondsToWaitFor: z.string().optional(),
})
export const waitBlockSchema = blockBaseSchema.and(
z.object({
type: z.enum([LogicBlockType.WAIT]),
options: waitOptionsSchema,
})
)
export const defaultWaitOptions: WaitOptions = {}
export type WaitBlock = z.infer<typeof waitBlockSchema>
export type WaitOptions = z.infer<typeof waitOptionsSchema>

View File

@@ -184,6 +184,13 @@ const clientSideActionSchema = z
googleAnalytics: googleAnalyticsOptionsSchema, googleAnalytics: googleAnalyticsOptionsSchema,
}) })
) )
.or(
z.object({
wait: z.object({
secondsToWaitFor: z.number(),
}),
})
)
export const chatReplySchema = z.object({ export const chatReplySchema = z.object({
messages: z.array(chatMessageSchema), messages: z.array(chatMessageSchema),