feat(engine): ✨ Add retry bubbles
This commit is contained in:
@ -14,6 +14,7 @@ import {
|
||||
} from 'utils'
|
||||
import { executeLogic } from 'services/logic'
|
||||
import { executeIntegration } from 'services/integration'
|
||||
import { parseRetryStep, stepCanBeRetried } from 'services/inputs'
|
||||
|
||||
type ChatBlockProps = {
|
||||
steps: PublicStep[]
|
||||
@ -30,7 +31,7 @@ export const ChatBlock = ({
|
||||
onScroll,
|
||||
onBlockEnd,
|
||||
}: ChatBlockProps) => {
|
||||
const { typebot, updateVariableValue } = useTypebot()
|
||||
const { typebot, updateVariableValue, createEdge } = useTypebot()
|
||||
const [displayedSteps, setDisplayedSteps] = useState<PublicStep[]>([])
|
||||
|
||||
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()
|
||||
console.log(currentStep)
|
||||
if (currentStep) {
|
||||
if (isRetry && stepCanBeRetried(currentStep))
|
||||
return setDisplayedSteps([
|
||||
...displayedSteps,
|
||||
parseRetryStep(currentStep, typebot.variables, createEdge),
|
||||
])
|
||||
if (
|
||||
isInputStep(currentStep) &&
|
||||
currentStep.options?.variableId &&
|
||||
|
@ -1,26 +1,27 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useAnswers } from '../../../contexts/AnswersContext'
|
||||
import { useHostAvatars } from '../../../contexts/HostAvatarsContext'
|
||||
import { InputStep, InputStepType, PublicStep, Step } from 'models'
|
||||
import { InputStep, InputStepType, PublicStep } from 'models'
|
||||
import { GuestBubble } from './bubbles/GuestBubble'
|
||||
import { TextForm } from './inputs/TextForm'
|
||||
import { isBubbleStep, isInputStep } from 'utils'
|
||||
import { DateForm } from './inputs/DateForm'
|
||||
import { ChoiceForm } from './inputs/ChoiceForm'
|
||||
import { HostBubble } from './bubbles/HostBubble'
|
||||
import { isInputValid } from 'services/inputs'
|
||||
|
||||
export const ChatStep = ({
|
||||
step,
|
||||
onTransitionEnd,
|
||||
}: {
|
||||
step: PublicStep
|
||||
onTransitionEnd: (answerContent?: string) => void
|
||||
onTransitionEnd: (answerContent?: string, isRetry?: boolean) => void
|
||||
}) => {
|
||||
const { addAnswer } = useAnswers()
|
||||
|
||||
const handleInputSubmit = (content: string) => {
|
||||
addAnswer({ stepId: step.id, blockId: step.blockId, content })
|
||||
onTransitionEnd(content)
|
||||
const handleInputSubmit = (content: string, isRetry: boolean) => {
|
||||
if (!isRetry) addAnswer({ stepId: step.id, blockId: step.blockId, content })
|
||||
onTransitionEnd(content, isRetry)
|
||||
}
|
||||
|
||||
if (isBubbleStep(step))
|
||||
@ -35,7 +36,7 @@ const InputChatStep = ({
|
||||
onSubmit,
|
||||
}: {
|
||||
step: InputStep
|
||||
onSubmit: (value: string) => void
|
||||
onSubmit: (value: string, isRetry: boolean) => void
|
||||
}) => {
|
||||
const { addNewAvatarOffset } = useHostAvatars()
|
||||
const [answer, setAnswer] = useState<string>()
|
||||
@ -47,7 +48,7 @@ const InputChatStep = ({
|
||||
|
||||
const handleSubmit = (value: string) => {
|
||||
setAnswer(value)
|
||||
onSubmit(value)
|
||||
onSubmit(value, !isInputValid(value, step.type))
|
||||
}
|
||||
|
||||
if (answer) {
|
||||
|
@ -5,22 +5,24 @@ import { useFrame } from 'react-frame-component'
|
||||
import { setCssVariablesValue } from '../services/theme'
|
||||
import { useAnswers } from '../contexts/AnswersContext'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import { Answer, Edge, PublicBlock, PublicTypebot } from 'models'
|
||||
import { Answer, Edge, PublicBlock, Theme } from 'models'
|
||||
import { byId } from 'utils'
|
||||
import { animateScroll as scroll } from 'react-scroll'
|
||||
import { useTypebot } from 'contexts/TypebotContext'
|
||||
|
||||
type Props = {
|
||||
typebot: PublicTypebot
|
||||
theme: Theme
|
||||
onNewBlockVisible: (edge: Edge) => void
|
||||
onNewAnswer: (answer: Answer) => void
|
||||
onCompleted: () => void
|
||||
}
|
||||
export const ConversationContainer = ({
|
||||
typebot,
|
||||
theme,
|
||||
onNewBlockVisible,
|
||||
onNewAnswer,
|
||||
onCompleted,
|
||||
}: Props) => {
|
||||
const { typebot } = useTypebot()
|
||||
const { document: frameDocument } = useFrame()
|
||||
const [displayedBlocks, setDisplayedBlocks] = useState<
|
||||
{ block: PublicBlock; startStepIndex: number }[]
|
||||
@ -51,8 +53,8 @@ export const ConversationContainer = ({
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
setCssVariablesValue(typebot.theme, frameDocument.body.style)
|
||||
}, [typebot.theme, frameDocument])
|
||||
setCssVariablesValue(theme, frameDocument.body.style)
|
||||
}, [theme, frameDocument])
|
||||
|
||||
useEffect(() => {
|
||||
const answer = [...answers].pop()
|
||||
|
@ -74,7 +74,7 @@ export const TypebotViewer = ({
|
||||
>
|
||||
<div className="flex w-full h-full justify-center">
|
||||
<ConversationContainer
|
||||
typebot={typebot}
|
||||
theme={typebot.theme}
|
||||
onNewBlockVisible={handleNewBlockVisible}
|
||||
onNewAnswer={handleNewAnswer}
|
||||
onCompleted={handleCompleted}
|
||||
|
@ -1,9 +1,10 @@
|
||||
import { PublicTypebot } from 'models'
|
||||
import { Edge, PublicTypebot } from 'models'
|
||||
import React, { createContext, ReactNode, useContext, useState } from 'react'
|
||||
|
||||
const typebotContext = createContext<{
|
||||
typebot: PublicTypebot
|
||||
updateVariableValue: (variableId: string, value: string) => void
|
||||
createEdge: (edge: Edge) => void
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
}>({})
|
||||
@ -25,11 +26,20 @@ export const TypebotContext = ({
|
||||
),
|
||||
}))
|
||||
}
|
||||
|
||||
const createEdge = (edge: Edge) => {
|
||||
setLocalTypebot((typebot) => ({
|
||||
...typebot,
|
||||
edges: [...typebot.edges, edge],
|
||||
}))
|
||||
}
|
||||
|
||||
return (
|
||||
<typebotContext.Provider
|
||||
value={{
|
||||
typebot: localTypebot,
|
||||
updateVariableValue,
|
||||
createEdge,
|
||||
}}
|
||||
>
|
||||
{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,
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user