feat(engine): ✨ Add retry bubbles
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -9,3 +9,4 @@ yarn-error.log
|
|||||||
authenticatedState.json
|
authenticatedState.json
|
||||||
playwright-report
|
playwright-report
|
||||||
dist
|
dist
|
||||||
|
test-results
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { FormLabel, Stack } from '@chakra-ui/react'
|
import { FormLabel, Stack } from '@chakra-ui/react'
|
||||||
import { DebouncedInput } from 'components/shared/DebouncedInput'
|
import { DebouncedInput } from 'components/shared/DebouncedInput'
|
||||||
|
import { InputWithVariableButton } from 'components/shared/TextboxWithVariableButton'
|
||||||
import { VariableSearchInput } from 'components/shared/VariableSearchInput'
|
import { VariableSearchInput } from 'components/shared/VariableSearchInput'
|
||||||
import { EmailInputOptions, Variable } from 'models'
|
import { EmailInputOptions, Variable } from 'models'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
@@ -19,6 +20,8 @@ export const EmailInputSettingsBody = ({
|
|||||||
onOptionsChange({ ...options, labels: { ...options.labels, button } })
|
onOptionsChange({ ...options, labels: { ...options.labels, button } })
|
||||||
const handleVariableChange = (variable?: Variable) =>
|
const handleVariableChange = (variable?: Variable) =>
|
||||||
onOptionsChange({ ...options, variableId: variable?.id })
|
onOptionsChange({ ...options, variableId: variable?.id })
|
||||||
|
const handleRetryMessageChange = (retryMessageContent: string) =>
|
||||||
|
onOptionsChange({ ...options, retryMessageContent })
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack spacing={4}>
|
<Stack spacing={4}>
|
||||||
@@ -42,6 +45,16 @@ export const EmailInputSettingsBody = ({
|
|||||||
onChange={handleButtonLabelChange}
|
onChange={handleButtonLabelChange}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
<Stack>
|
||||||
|
<FormLabel mb="0" htmlFor="retry">
|
||||||
|
Retry message:
|
||||||
|
</FormLabel>
|
||||||
|
<InputWithVariableButton
|
||||||
|
id="retry"
|
||||||
|
initialValue={options.retryMessageContent}
|
||||||
|
onChange={handleRetryMessageChange}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
<Stack>
|
<Stack>
|
||||||
<FormLabel mb="0" htmlFor="variable">
|
<FormLabel mb="0" htmlFor="variable">
|
||||||
Save answer in a variable:
|
Save answer in a variable:
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { FormLabel, Stack } from '@chakra-ui/react'
|
import { FormLabel, Stack } from '@chakra-ui/react'
|
||||||
import { DebouncedInput } from 'components/shared/DebouncedInput'
|
import { DebouncedInput } from 'components/shared/DebouncedInput'
|
||||||
|
import { InputWithVariableButton } from 'components/shared/TextboxWithVariableButton'
|
||||||
import { VariableSearchInput } from 'components/shared/VariableSearchInput'
|
import { VariableSearchInput } from 'components/shared/VariableSearchInput'
|
||||||
import { EmailInputOptions, Variable } from 'models'
|
import { EmailInputOptions, Variable } from 'models'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
@@ -19,6 +20,8 @@ export const PhoneNumberSettingsBody = ({
|
|||||||
onOptionsChange({ ...options, labels: { ...options.labels, button } })
|
onOptionsChange({ ...options, labels: { ...options.labels, button } })
|
||||||
const handleVariableChange = (variable?: Variable) =>
|
const handleVariableChange = (variable?: Variable) =>
|
||||||
onOptionsChange({ ...options, variableId: variable?.id })
|
onOptionsChange({ ...options, variableId: variable?.id })
|
||||||
|
const handleRetryMessageChange = (retryMessageContent: string) =>
|
||||||
|
onOptionsChange({ ...options, retryMessageContent })
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack spacing={4}>
|
<Stack spacing={4}>
|
||||||
@@ -42,6 +45,16 @@ export const PhoneNumberSettingsBody = ({
|
|||||||
onChange={handleButtonLabelChange}
|
onChange={handleButtonLabelChange}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
<Stack>
|
||||||
|
<FormLabel mb="0" htmlFor="retry">
|
||||||
|
Retry message:
|
||||||
|
</FormLabel>
|
||||||
|
<InputWithVariableButton
|
||||||
|
id="retry"
|
||||||
|
initialValue={options.retryMessageContent}
|
||||||
|
onChange={handleRetryMessageChange}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
<Stack>
|
<Stack>
|
||||||
<FormLabel mb="0" htmlFor="variable">
|
<FormLabel mb="0" htmlFor="variable">
|
||||||
Save answer in a variable:
|
Save answer in a variable:
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { FormLabel, Stack } from '@chakra-ui/react'
|
import { FormLabel, Stack } from '@chakra-ui/react'
|
||||||
import { DebouncedInput } from 'components/shared/DebouncedInput'
|
import { DebouncedInput } from 'components/shared/DebouncedInput'
|
||||||
|
import { InputWithVariableButton } from 'components/shared/TextboxWithVariableButton'
|
||||||
import { VariableSearchInput } from 'components/shared/VariableSearchInput'
|
import { VariableSearchInput } from 'components/shared/VariableSearchInput'
|
||||||
import { UrlInputOptions, Variable } from 'models'
|
import { UrlInputOptions, Variable } from 'models'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
@@ -19,6 +20,8 @@ export const UrlInputSettingsBody = ({
|
|||||||
onOptionsChange({ ...options, labels: { ...options.labels, button } })
|
onOptionsChange({ ...options, labels: { ...options.labels, button } })
|
||||||
const handleVariableChange = (variable?: Variable) =>
|
const handleVariableChange = (variable?: Variable) =>
|
||||||
onOptionsChange({ ...options, variableId: variable?.id })
|
onOptionsChange({ ...options, variableId: variable?.id })
|
||||||
|
const handleRetryMessageChange = (retryMessageContent: string) =>
|
||||||
|
onOptionsChange({ ...options, retryMessageContent })
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack spacing={4}>
|
<Stack spacing={4}>
|
||||||
@@ -42,6 +45,16 @@ export const UrlInputSettingsBody = ({
|
|||||||
onChange={handleButtonLabelChange}
|
onChange={handleButtonLabelChange}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
<Stack>
|
||||||
|
<FormLabel mb="0" htmlFor="retry">
|
||||||
|
Retry message:
|
||||||
|
</FormLabel>
|
||||||
|
<InputWithVariableButton
|
||||||
|
id="retry"
|
||||||
|
initialValue={options.retryMessageContent}
|
||||||
|
onChange={handleRetryMessageChange}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
<Stack>
|
<Stack>
|
||||||
<FormLabel mb="0" htmlFor="variable">
|
<FormLabel mb="0" htmlFor="variable">
|
||||||
Save answer in a variable:
|
Save answer in a variable:
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ const createCredentials = () => {
|
|||||||
'ya29.A0ARrdaM--PV_87ebjywDJpXKb77NBFJl16meVUapYdfNv6W6ZzqqC47fNaPaRjbDbOIIcp6f49cMaX5ndK9TAFnKwlVqz3nrK9nLKqgyDIhYsIq47smcAIZkK56SWPx3X3DwAFqRu2UPojpd2upWwo-3uJrod',
|
'ya29.A0ARrdaM--PV_87ebjywDJpXKb77NBFJl16meVUapYdfNv6W6ZzqqC47fNaPaRjbDbOIIcp6f49cMaX5ndK9TAFnKwlVqz3nrK9nLKqgyDIhYsIq47smcAIZkK56SWPx3X3DwAFqRu2UPojpd2upWwo-3uJrod',
|
||||||
// This token is linked to a mock Google account (typebot.test.user@gmail.com)
|
// This token is linked to a mock Google account (typebot.test.user@gmail.com)
|
||||||
refresh_token:
|
refresh_token:
|
||||||
'1//0379tIHBxszeXCgYIARAAGAMSNwF-L9Ir0zhkzhblwXqn3_jYqRP3pajcUpqkjRU3fKZZ_eQakOa28amUHSQ-Q9fMzk89MpRTvkc',
|
'1//03NRE9V8T-aayCgYIARAAGAMSNwF-L9Ir6zVzF-wm30psz0lbDJj5Y9OgqTO0cvBISODMW4QTR0VK40BLnOQgcHCHkb9c769TAhQ',
|
||||||
})
|
})
|
||||||
return prisma.credentials.createMany({
|
return prisma.credentials.createMany({
|
||||||
data: [
|
data: [
|
||||||
|
|||||||
@@ -34,10 +34,25 @@ test.describe('Email input step', () => {
|
|||||||
await page.fill('#placeholder', 'Your email...')
|
await page.fill('#placeholder', 'Your email...')
|
||||||
await expect(page.locator('text=Your email...')).toBeVisible()
|
await expect(page.locator('text=Your email...')).toBeVisible()
|
||||||
await page.fill('#button', 'Go')
|
await page.fill('#button', 'Go')
|
||||||
|
await page.fill(
|
||||||
|
`input[value="${defaultEmailInputOptions.retryMessageContent}"]`,
|
||||||
|
'Try again bro'
|
||||||
|
)
|
||||||
|
|
||||||
await page.click('text=Restart')
|
await page.click('text=Restart')
|
||||||
|
await typebotViewer(page)
|
||||||
|
.locator(`input[placeholder="Your email..."]`)
|
||||||
|
.fill('test@test')
|
||||||
|
await typebotViewer(page).locator('text=Go').click()
|
||||||
await expect(
|
await expect(
|
||||||
typebotViewer(page).locator(`input[placeholder="Your email..."]`)
|
typebotViewer(page).locator('text=Try again bro')
|
||||||
|
).toBeVisible()
|
||||||
|
await typebotViewer(page)
|
||||||
|
.locator(`input[placeholder="Your email..."]`)
|
||||||
|
.fill('test@test.com')
|
||||||
|
await typebotViewer(page).locator('text=Go').click()
|
||||||
|
await expect(
|
||||||
|
typebotViewer(page).locator('text=test@test.com')
|
||||||
).toBeVisible()
|
).toBeVisible()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -33,16 +33,27 @@ test.describe('Phone input step', () => {
|
|||||||
await page.click(`text=${defaultPhoneInputOptions.labels.placeholder}`)
|
await page.click(`text=${defaultPhoneInputOptions.labels.placeholder}`)
|
||||||
await page.fill('#placeholder', '+33 XX XX XX XX')
|
await page.fill('#placeholder', '+33 XX XX XX XX')
|
||||||
await page.fill('#button', 'Go')
|
await page.fill('#button', 'Go')
|
||||||
|
await page.fill(
|
||||||
|
`input[value="${defaultPhoneInputOptions.retryMessageContent}"]`,
|
||||||
|
'Try again bro'
|
||||||
|
)
|
||||||
|
|
||||||
await page.click('text=Restart')
|
await page.click('text=Restart')
|
||||||
await typebotViewer(page)
|
await typebotViewer(page)
|
||||||
.locator(`input[placeholder="+33 XX XX XX XX"]`)
|
.locator(`input[placeholder="+33 XX XX XX XX"]`)
|
||||||
.fill('+33 6 73 18 45 36')
|
.fill('+33 6 73')
|
||||||
await expect(typebotViewer(page).locator(`img`)).toHaveAttribute(
|
await expect(typebotViewer(page).locator(`img`)).toHaveAttribute(
|
||||||
'alt',
|
'alt',
|
||||||
'France'
|
'France'
|
||||||
)
|
)
|
||||||
await typebotViewer(page).locator('text="Go"').click()
|
await typebotViewer(page).locator('button >> text="Go"').click()
|
||||||
await expect(typebotViewer(page).locator('text=+33673184536')).toBeVisible()
|
await expect(
|
||||||
|
typebotViewer(page).locator('text=Try again bro')
|
||||||
|
).toBeVisible()
|
||||||
|
await typebotViewer(page)
|
||||||
|
.locator(`input[placeholder="+33 XX XX XX XX"]`)
|
||||||
|
.fill('+33 6 73 54 45 67')
|
||||||
|
await typebotViewer(page).locator('button >> text="Go"').click()
|
||||||
|
await expect(typebotViewer(page).locator('text=+33673544567')).toBeVisible()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -34,10 +34,25 @@ test.describe('Url input step', () => {
|
|||||||
await page.fill('#placeholder', 'Your URL...')
|
await page.fill('#placeholder', 'Your URL...')
|
||||||
await expect(page.locator('text=Your URL...')).toBeVisible()
|
await expect(page.locator('text=Your URL...')).toBeVisible()
|
||||||
await page.fill('#button', 'Go')
|
await page.fill('#button', 'Go')
|
||||||
|
await page.fill(
|
||||||
|
`input[value="${defaultUrlInputOptions.retryMessageContent}"]`,
|
||||||
|
'Try again bro'
|
||||||
|
)
|
||||||
|
|
||||||
await page.click('text=Restart')
|
await page.click('text=Restart')
|
||||||
|
await typebotViewer(page)
|
||||||
|
.locator(`input[placeholder="Your URL..."]`)
|
||||||
|
.fill('gg://test.com')
|
||||||
|
await typebotViewer(page).locator('button >> text="Go"').click()
|
||||||
await expect(
|
await expect(
|
||||||
typebotViewer(page).locator(`input[placeholder="Your URL..."]`)
|
typebotViewer(page).locator('text=Try again bro')
|
||||||
|
).toBeVisible()
|
||||||
|
await typebotViewer(page)
|
||||||
|
.locator(`input[placeholder="Your URL..."]`)
|
||||||
|
.fill('https://website.com')
|
||||||
|
await typebotViewer(page).locator('button >> text="Go"').click()
|
||||||
|
await expect(
|
||||||
|
typebotViewer(page).locator('text=https://website.com')
|
||||||
).toBeVisible()
|
).toBeVisible()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
} from 'utils'
|
} from 'utils'
|
||||||
import { executeLogic } from 'services/logic'
|
import { executeLogic } from 'services/logic'
|
||||||
import { executeIntegration } from 'services/integration'
|
import { executeIntegration } from 'services/integration'
|
||||||
|
import { parseRetryStep, stepCanBeRetried } from 'services/inputs'
|
||||||
|
|
||||||
type ChatBlockProps = {
|
type ChatBlockProps = {
|
||||||
steps: PublicStep[]
|
steps: PublicStep[]
|
||||||
@@ -30,7 +31,7 @@ export const ChatBlock = ({
|
|||||||
onScroll,
|
onScroll,
|
||||||
onBlockEnd,
|
onBlockEnd,
|
||||||
}: ChatBlockProps) => {
|
}: ChatBlockProps) => {
|
||||||
const { typebot, updateVariableValue } = useTypebot()
|
const { typebot, updateVariableValue, createEdge } = useTypebot()
|
||||||
const [displayedSteps, setDisplayedSteps] = useState<PublicStep[]>([])
|
const [displayedSteps, setDisplayedSteps] = useState<PublicStep[]>([])
|
||||||
|
|
||||||
const currentStepIndex = displayedSteps.length - 1
|
const currentStepIndex = displayedSteps.length - 1
|
||||||
@@ -70,9 +71,15 @@ export const ChatBlock = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const displayNextStep = (answerContent?: string) => {
|
const displayNextStep = (answerContent?: string, isRetry?: boolean) => {
|
||||||
const currentStep = [...displayedSteps].pop()
|
const currentStep = [...displayedSteps].pop()
|
||||||
|
console.log(currentStep)
|
||||||
if (currentStep) {
|
if (currentStep) {
|
||||||
|
if (isRetry && stepCanBeRetried(currentStep))
|
||||||
|
return setDisplayedSteps([
|
||||||
|
...displayedSteps,
|
||||||
|
parseRetryStep(currentStep, typebot.variables, createEdge),
|
||||||
|
])
|
||||||
if (
|
if (
|
||||||
isInputStep(currentStep) &&
|
isInputStep(currentStep) &&
|
||||||
currentStep.options?.variableId &&
|
currentStep.options?.variableId &&
|
||||||
|
|||||||
@@ -1,26 +1,27 @@
|
|||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { useAnswers } from '../../../contexts/AnswersContext'
|
import { useAnswers } from '../../../contexts/AnswersContext'
|
||||||
import { useHostAvatars } from '../../../contexts/HostAvatarsContext'
|
import { useHostAvatars } from '../../../contexts/HostAvatarsContext'
|
||||||
import { InputStep, InputStepType, PublicStep, Step } from 'models'
|
import { InputStep, InputStepType, PublicStep } from 'models'
|
||||||
import { GuestBubble } from './bubbles/GuestBubble'
|
import { GuestBubble } from './bubbles/GuestBubble'
|
||||||
import { TextForm } from './inputs/TextForm'
|
import { TextForm } from './inputs/TextForm'
|
||||||
import { isBubbleStep, isInputStep } from 'utils'
|
import { isBubbleStep, isInputStep } from 'utils'
|
||||||
import { DateForm } from './inputs/DateForm'
|
import { DateForm } from './inputs/DateForm'
|
||||||
import { ChoiceForm } from './inputs/ChoiceForm'
|
import { ChoiceForm } from './inputs/ChoiceForm'
|
||||||
import { HostBubble } from './bubbles/HostBubble'
|
import { HostBubble } from './bubbles/HostBubble'
|
||||||
|
import { isInputValid } from 'services/inputs'
|
||||||
|
|
||||||
export const ChatStep = ({
|
export const ChatStep = ({
|
||||||
step,
|
step,
|
||||||
onTransitionEnd,
|
onTransitionEnd,
|
||||||
}: {
|
}: {
|
||||||
step: PublicStep
|
step: PublicStep
|
||||||
onTransitionEnd: (answerContent?: string) => void
|
onTransitionEnd: (answerContent?: string, isRetry?: boolean) => void
|
||||||
}) => {
|
}) => {
|
||||||
const { addAnswer } = useAnswers()
|
const { addAnswer } = useAnswers()
|
||||||
|
|
||||||
const handleInputSubmit = (content: string) => {
|
const handleInputSubmit = (content: string, isRetry: boolean) => {
|
||||||
addAnswer({ stepId: step.id, blockId: step.blockId, content })
|
if (!isRetry) addAnswer({ stepId: step.id, blockId: step.blockId, content })
|
||||||
onTransitionEnd(content)
|
onTransitionEnd(content, isRetry)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isBubbleStep(step))
|
if (isBubbleStep(step))
|
||||||
@@ -35,7 +36,7 @@ const InputChatStep = ({
|
|||||||
onSubmit,
|
onSubmit,
|
||||||
}: {
|
}: {
|
||||||
step: InputStep
|
step: InputStep
|
||||||
onSubmit: (value: string) => void
|
onSubmit: (value: string, isRetry: boolean) => void
|
||||||
}) => {
|
}) => {
|
||||||
const { addNewAvatarOffset } = useHostAvatars()
|
const { addNewAvatarOffset } = useHostAvatars()
|
||||||
const [answer, setAnswer] = useState<string>()
|
const [answer, setAnswer] = useState<string>()
|
||||||
@@ -47,7 +48,7 @@ const InputChatStep = ({
|
|||||||
|
|
||||||
const handleSubmit = (value: string) => {
|
const handleSubmit = (value: string) => {
|
||||||
setAnswer(value)
|
setAnswer(value)
|
||||||
onSubmit(value)
|
onSubmit(value, !isInputValid(value, step.type))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (answer) {
|
if (answer) {
|
||||||
|
|||||||
@@ -5,22 +5,24 @@ import { useFrame } from 'react-frame-component'
|
|||||||
import { setCssVariablesValue } from '../services/theme'
|
import { setCssVariablesValue } from '../services/theme'
|
||||||
import { useAnswers } from '../contexts/AnswersContext'
|
import { useAnswers } from '../contexts/AnswersContext'
|
||||||
import { deepEqual } from 'fast-equals'
|
import { deepEqual } from 'fast-equals'
|
||||||
import { Answer, Edge, PublicBlock, PublicTypebot } from 'models'
|
import { Answer, Edge, PublicBlock, Theme } from 'models'
|
||||||
import { byId } from 'utils'
|
import { byId } from 'utils'
|
||||||
import { animateScroll as scroll } from 'react-scroll'
|
import { animateScroll as scroll } from 'react-scroll'
|
||||||
|
import { useTypebot } from 'contexts/TypebotContext'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
typebot: PublicTypebot
|
theme: Theme
|
||||||
onNewBlockVisible: (edge: Edge) => void
|
onNewBlockVisible: (edge: Edge) => void
|
||||||
onNewAnswer: (answer: Answer) => void
|
onNewAnswer: (answer: Answer) => void
|
||||||
onCompleted: () => void
|
onCompleted: () => void
|
||||||
}
|
}
|
||||||
export const ConversationContainer = ({
|
export const ConversationContainer = ({
|
||||||
typebot,
|
theme,
|
||||||
onNewBlockVisible,
|
onNewBlockVisible,
|
||||||
onNewAnswer,
|
onNewAnswer,
|
||||||
onCompleted,
|
onCompleted,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
|
const { typebot } = useTypebot()
|
||||||
const { document: frameDocument } = useFrame()
|
const { document: frameDocument } = useFrame()
|
||||||
const [displayedBlocks, setDisplayedBlocks] = useState<
|
const [displayedBlocks, setDisplayedBlocks] = useState<
|
||||||
{ block: PublicBlock; startStepIndex: number }[]
|
{ block: PublicBlock; startStepIndex: number }[]
|
||||||
@@ -51,8 +53,8 @@ export const ConversationContainer = ({
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setCssVariablesValue(typebot.theme, frameDocument.body.style)
|
setCssVariablesValue(theme, frameDocument.body.style)
|
||||||
}, [typebot.theme, frameDocument])
|
}, [theme, frameDocument])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const answer = [...answers].pop()
|
const answer = [...answers].pop()
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ export const TypebotViewer = ({
|
|||||||
>
|
>
|
||||||
<div className="flex w-full h-full justify-center">
|
<div className="flex w-full h-full justify-center">
|
||||||
<ConversationContainer
|
<ConversationContainer
|
||||||
typebot={typebot}
|
theme={typebot.theme}
|
||||||
onNewBlockVisible={handleNewBlockVisible}
|
onNewBlockVisible={handleNewBlockVisible}
|
||||||
onNewAnswer={handleNewAnswer}
|
onNewAnswer={handleNewAnswer}
|
||||||
onCompleted={handleCompleted}
|
onCompleted={handleCompleted}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { PublicTypebot } from 'models'
|
import { Edge, PublicTypebot } from 'models'
|
||||||
import React, { createContext, ReactNode, useContext, useState } from 'react'
|
import React, { createContext, ReactNode, useContext, useState } from 'react'
|
||||||
|
|
||||||
const typebotContext = createContext<{
|
const typebotContext = createContext<{
|
||||||
typebot: PublicTypebot
|
typebot: PublicTypebot
|
||||||
updateVariableValue: (variableId: string, value: string) => void
|
updateVariableValue: (variableId: string, value: string) => void
|
||||||
|
createEdge: (edge: Edge) => void
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
}>({})
|
}>({})
|
||||||
@@ -25,11 +26,20 @@ export const TypebotContext = ({
|
|||||||
),
|
),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const createEdge = (edge: Edge) => {
|
||||||
|
setLocalTypebot((typebot) => ({
|
||||||
|
...typebot,
|
||||||
|
edges: [...typebot.edges, edge],
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<typebotContext.Provider
|
<typebotContext.Provider
|
||||||
value={{
|
value={{
|
||||||
typebot: localTypebot,
|
typebot: localTypebot,
|
||||||
updateVariableValue,
|
updateVariableValue,
|
||||||
|
createEdge,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
65
packages/bot-engine/src/services/inputs.ts
Normal file
65
packages/bot-engine/src/services/inputs.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import {
|
||||||
|
BubbleStep,
|
||||||
|
BubbleStepType,
|
||||||
|
Edge,
|
||||||
|
EmailInputStep,
|
||||||
|
InputStepType,
|
||||||
|
PhoneNumberInputStep,
|
||||||
|
PublicStep,
|
||||||
|
UrlInputStep,
|
||||||
|
Variable,
|
||||||
|
} from 'models'
|
||||||
|
import { isPossiblePhoneNumber } from 'react-phone-number-input'
|
||||||
|
import { isInputStep } from 'utils'
|
||||||
|
import { parseVariables } from './variable'
|
||||||
|
|
||||||
|
const emailRegex =
|
||||||
|
/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
|
||||||
|
const urlRegex =
|
||||||
|
/((([A-Za-z]{3,9}:(?:\/\/)?)(?:[\-;:&=\+\$,\w]+@)?[A-Za-z0-9\.\-]+|(?:www\.|[\-;:&=\+\$,\w]+@)[A-Za-z0-9\.\-]+)((?:\/[\+~%\/\.\w\-_]*)?\??(?:[\-\+=&;%@\.\w_]*)#?(?:[\.\!\/\\\w]*))?)/
|
||||||
|
|
||||||
|
export const isInputValid = (
|
||||||
|
inputValue: string,
|
||||||
|
type: InputStepType
|
||||||
|
): boolean => {
|
||||||
|
switch (type) {
|
||||||
|
case InputStepType.EMAIL:
|
||||||
|
return emailRegex.test(inputValue)
|
||||||
|
case InputStepType.PHONE:
|
||||||
|
return isPossiblePhoneNumber(inputValue)
|
||||||
|
case InputStepType.URL:
|
||||||
|
return urlRegex.test(inputValue)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
export const stepCanBeRetried = (
|
||||||
|
step: PublicStep
|
||||||
|
): step is EmailInputStep | UrlInputStep | PhoneNumberInputStep =>
|
||||||
|
isInputStep(step) && 'retryMessageContent' in step.options
|
||||||
|
|
||||||
|
export const parseRetryStep = (
|
||||||
|
step: EmailInputStep | UrlInputStep | PhoneNumberInputStep,
|
||||||
|
variables: Variable[],
|
||||||
|
createEdge: (edge: Edge) => void
|
||||||
|
): BubbleStep => {
|
||||||
|
const content = parseVariables(variables)(step.options.retryMessageContent)
|
||||||
|
const newStepId = step.id + Math.random() * 1000
|
||||||
|
const newEdge: Edge = {
|
||||||
|
id: (Math.random() * 1000).toString(),
|
||||||
|
from: { stepId: newStepId, blockId: step.blockId },
|
||||||
|
to: { blockId: step.blockId, stepId: step.id },
|
||||||
|
}
|
||||||
|
createEdge(newEdge)
|
||||||
|
return {
|
||||||
|
blockId: step.blockId,
|
||||||
|
id: newStepId,
|
||||||
|
type: BubbleStepType.TEXT,
|
||||||
|
content: {
|
||||||
|
html: `<div>${content}</div>`,
|
||||||
|
richText: [],
|
||||||
|
plainText: content,
|
||||||
|
},
|
||||||
|
outgoingEdgeId: newEdge.id,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -56,11 +56,9 @@ export type DateInputStep = StepBase & {
|
|||||||
|
|
||||||
export type PhoneNumberInputStep = StepBase & {
|
export type PhoneNumberInputStep = StepBase & {
|
||||||
type: InputStepType.PHONE
|
type: InputStepType.PHONE
|
||||||
options: OptionBase & InputTextOptionsBase
|
options: PhoneNumberInputOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PhoneNumberInputOptions = OptionBase & InputTextOptionsBase
|
|
||||||
|
|
||||||
export type ChoiceInputStep = StepBase & {
|
export type ChoiceInputStep = StepBase & {
|
||||||
type: InputStepType.CHOICE
|
type: InputStepType.CHOICE
|
||||||
items: ButtonItem[]
|
items: ButtonItem[]
|
||||||
@@ -73,6 +71,7 @@ export type ButtonItem = ItemBase & {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type OptionBase = { variableId?: string }
|
type OptionBase = { variableId?: string }
|
||||||
|
|
||||||
type InputTextOptionsBase = {
|
type InputTextOptionsBase = {
|
||||||
labels: { placeholder: string; button: string }
|
labels: { placeholder: string; button: string }
|
||||||
}
|
}
|
||||||
@@ -88,9 +87,20 @@ export type DateInputOptions = OptionBase & {
|
|||||||
isRange: boolean
|
isRange: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type EmailInputOptions = OptionBase & InputTextOptionsBase
|
export type EmailInputOptions = OptionBase & {
|
||||||
|
labels: { placeholder: string; button: string }
|
||||||
|
retryMessageContent: string
|
||||||
|
}
|
||||||
|
|
||||||
export type UrlInputOptions = OptionBase & InputTextOptionsBase
|
export type UrlInputOptions = OptionBase & {
|
||||||
|
labels: { placeholder: string; button: string }
|
||||||
|
retryMessageContent: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PhoneNumberInputOptions = OptionBase & {
|
||||||
|
labels: { placeholder: string; button: string }
|
||||||
|
retryMessageContent: string
|
||||||
|
}
|
||||||
|
|
||||||
export type TextInputOptions = OptionBase &
|
export type TextInputOptions = OptionBase &
|
||||||
InputTextOptionsBase & {
|
InputTextOptionsBase & {
|
||||||
@@ -116,11 +126,21 @@ export const defaultNumberInputOptions: NumberInputOptions = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const defaultEmailInputOptions: EmailInputOptions = {
|
export const defaultEmailInputOptions: EmailInputOptions = {
|
||||||
labels: { button: defaultButtonLabel, placeholder: 'Type your email...' },
|
labels: {
|
||||||
|
button: defaultButtonLabel,
|
||||||
|
placeholder: 'Type your email...',
|
||||||
|
},
|
||||||
|
retryMessageContent:
|
||||||
|
"This email doesn't seem to be valid. Can you type it again?",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const defaultUrlInputOptions: UrlInputOptions = {
|
export const defaultUrlInputOptions: UrlInputOptions = {
|
||||||
labels: { button: defaultButtonLabel, placeholder: 'Type a URL...' },
|
labels: {
|
||||||
|
button: defaultButtonLabel,
|
||||||
|
placeholder: 'Type a URL...',
|
||||||
|
},
|
||||||
|
retryMessageContent:
|
||||||
|
"This email doesn't seem to be valid. Can you type it again?",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const defaultDateInputOptions: DateInputOptions = {
|
export const defaultDateInputOptions: DateInputOptions = {
|
||||||
@@ -134,6 +154,8 @@ export const defaultPhoneInputOptions: PhoneNumberInputOptions = {
|
|||||||
button: defaultButtonLabel,
|
button: defaultButtonLabel,
|
||||||
placeholder: 'Type your phone number...',
|
placeholder: 'Type your phone number...',
|
||||||
},
|
},
|
||||||
|
retryMessageContent:
|
||||||
|
"This email doesn't seem to be valid. Can you type it again?",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const defaultChoiceInputOptions: ChoiceInputOptions = {
|
export const defaultChoiceInputOptions: ChoiceInputOptions = {
|
||||||
|
|||||||
Reference in New Issue
Block a user