2
0

feat(engine): Add retry bubbles

This commit is contained in:
Baptiste Arnaud
2022-02-10 10:25:38 +01:00
parent 276f1c1e90
commit 8c8d77e052
15 changed files with 217 additions and 29 deletions

View File

@ -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 &&

View File

@ -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) {

View File

@ -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()

View File

@ -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}

View File

@ -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}

View 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,
}
}