Introduce bot v2 in builder (#328)

Also, the new engine is the default for updated typebots for viewer

Closes #211
This commit is contained in:
Baptiste Arnaud
2023-02-21 15:25:14 +01:00
committed by GitHub
parent 527dc8a5b1
commit debdac12ff
208 changed files with 4462 additions and 5236 deletions

View File

@@ -1,161 +0,0 @@
import { TypebotViewer } from 'bot-engine'
import { AnswerInput, PublicTypebot, Typebot, VariableWithValue } from 'models'
import { useRouter } from 'next/router'
import React, { useEffect, useState } from 'react'
import {
injectCustomHeadCode,
isDefined,
isNotDefined,
isNotEmpty,
} from 'utils'
import { SEO } from './Seo'
import { ErrorPage } from './ErrorPage'
import { createResultQuery, updateResultQuery } from '@/features/results'
import { upsertAnswerQuery } from '@/features/answers'
import { gtmBodyElement } from '@/lib/google-tag-manager'
import {
getExistingResultFromSession,
setResultInSession,
} from '@/utils/sessionStorage'
export type TypebotPageProps = {
publishedTypebot: Omit<PublicTypebot, 'createdAt' | 'updatedAt'> & {
typebot: Pick<Typebot, 'name' | 'isClosed' | 'isArchived'>
}
url: string
isIE: boolean
customHeadCode: string | null
}
export const TypebotPage = ({
publishedTypebot,
isIE,
url,
customHeadCode,
}: TypebotPageProps) => {
const { asPath, push } = useRouter()
const [showTypebot, setShowTypebot] = useState(false)
const [predefinedVariables, setPredefinedVariables] = useState<{
[key: string]: string
}>()
const [error, setError] = useState<Error | undefined>(
isIE ? new Error('Internet explorer is not supported') : undefined
)
const [resultId, setResultId] = useState<string | undefined>()
const [variableUpdateQueue, setVariableUpdateQueue] = useState<
VariableWithValue[][]
>([])
const [chatStarted, setChatStarted] = useState(false)
useEffect(() => {
setShowTypebot(true)
const urlParams = new URLSearchParams(location.search)
clearQueryParams()
const predefinedVariables: { [key: string]: string } = {}
urlParams.forEach((value, key) => {
predefinedVariables[key] = value
})
setPredefinedVariables(predefinedVariables)
initializeResult().then()
if (isDefined(customHeadCode)) injectCustomHeadCode(customHeadCode)
const gtmId = publishedTypebot.settings.metadata.googleTagManagerId
if (isNotEmpty(gtmId)) document.body.prepend(gtmBodyElement(gtmId))
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
const clearQueryParams = () => {
const hasQueryParams = asPath.includes('?')
if (
hasQueryParams &&
publishedTypebot.settings.general.isHideQueryParamsEnabled !== false
)
push(asPath.split('?')[0], undefined, { shallow: true })
}
const initializeResult = async () => {
const resultIdFromSession = getExistingResultFromSession()
if (resultIdFromSession) setResultId(resultIdFromSession)
else {
const { error, data } = await createResultQuery(
publishedTypebot.typebotId
)
if (error) return setError(error)
if (data?.hasReachedLimit)
return setError(new Error('This bot is now closed.'))
if (data?.result) {
setResultId(data.result.id)
if (
publishedTypebot.settings.general.isNewResultOnRefreshEnabled !== true
)
setResultInSession(data.result.id)
}
}
}
useEffect(() => {
if (!resultId || variableUpdateQueue.length === 0) return
Promise.all(variableUpdateQueue.map(sendNewVariables(resultId))).then()
setVariableUpdateQueue([])
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [resultId])
const handleNewVariables = async (variables: VariableWithValue[]) => {
if (!resultId)
return setVariableUpdateQueue([...variableUpdateQueue, variables])
await sendNewVariables(resultId)(variables)
}
const sendNewVariables =
(resultId: string) => async (variables: VariableWithValue[]) => {
if (publishedTypebot.settings.general.isResultSavingEnabled === false)
return
const { error } = await updateResultQuery(resultId, { variables })
if (error) setError(error)
}
const handleNewAnswer = async (
answer: AnswerInput & { uploadedFiles: boolean }
) => {
if (!resultId) return setError(new Error('Error: result was not created'))
if (publishedTypebot.settings.general.isResultSavingEnabled !== false) {
const { error } = await upsertAnswerQuery({ ...answer, resultId })
if (error) setError(error)
}
if (chatStarted) return
updateResultQuery(resultId, {
hasStarted: true,
}).then(({ error }) => (error ? setError(error) : setChatStarted(true)))
}
const handleCompleted = async () => {
if (publishedTypebot.settings.general.isResultSavingEnabled === false)
return
if (!resultId) return setError(new Error('Error: result was not created'))
const { error } = await updateResultQuery(resultId, { isCompleted: true })
if (error) setError(error)
}
if (error) {
return <ErrorPage error={error} />
}
return (
<div style={{ height: '100vh' }}>
<SEO
url={url}
typebotName={publishedTypebot.typebot.name}
metadata={publishedTypebot.settings.metadata}
/>
{showTypebot && (
<TypebotViewer
typebot={publishedTypebot}
resultId={resultId}
predefinedVariables={predefinedVariables}
onNewAnswer={handleNewAnswer}
onCompleted={handleCompleted}
onVariablesUpdated={handleNewVariables}
isLoading={isNotDefined(resultId)}
/>
)}
</div>
)
}

View File

@@ -1,50 +1,161 @@
import { Standard } from '@typebot.io/react'
import { BackgroundType, Typebot } from 'models'
import { TypebotViewer } from 'bot-engine'
import { AnswerInput, PublicTypebot, Typebot, VariableWithValue } from 'models'
import { useRouter } from 'next/router'
import React, { useEffect, useState } from 'react'
import {
injectCustomHeadCode,
isDefined,
isNotDefined,
isNotEmpty,
} from 'utils'
import { SEO } from './Seo'
import { ErrorPage } from './ErrorPage'
import { createResultQuery, updateResultQuery } from '@/features/results'
import { upsertAnswerQuery } from '@/features/answers'
import { gtmBodyElement } from '@/lib/google-tag-manager'
import {
getExistingResultFromSession,
setResultInSession,
} from '@/utils/sessionStorage'
export type TypebotPageProps = {
publishedTypebot: Omit<PublicTypebot, 'createdAt' | 'updatedAt'> & {
typebot: Pick<Typebot, 'name' | 'isClosed' | 'isArchived' | 'publicId'>
}
url: string
typebot?: Pick<Typebot, 'settings' | 'theme' | 'name' | 'publicId'>
isIE: boolean
customHeadCode: string | null
}
export const TypebotPage = ({ url, typebot }: TypebotPageProps) => {
const { asPath, push, query } = useRouter()
export const TypebotPageV2 = ({
publishedTypebot,
isIE,
url,
customHeadCode,
}: TypebotPageProps) => {
const { asPath, push } = useRouter()
const [showTypebot, setShowTypebot] = useState(false)
const [predefinedVariables, setPredefinedVariables] = useState<{
[key: string]: string
}>()
const [error, setError] = useState<Error | undefined>(
isIE ? new Error('Internet explorer is not supported') : undefined
)
const [resultId, setResultId] = useState<string | undefined>()
const [variableUpdateQueue, setVariableUpdateQueue] = useState<
VariableWithValue[][]
>([])
const [chatStarted, setChatStarted] = useState(false)
const background = typebot?.theme.general.background
useEffect(() => {
setShowTypebot(true)
const urlParams = new URLSearchParams(location.search)
clearQueryParams()
const predefinedVariables: { [key: string]: string } = {}
urlParams.forEach((value, key) => {
predefinedVariables[key] = value
})
setPredefinedVariables(predefinedVariables)
initializeResult().then()
if (isDefined(customHeadCode)) injectCustomHeadCode(customHeadCode)
const gtmId = publishedTypebot.settings.metadata.googleTagManagerId
if (isNotEmpty(gtmId)) document.body.prepend(gtmBodyElement(gtmId))
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
const clearQueryParamsIfNecessary = () => {
const clearQueryParams = () => {
const hasQueryParams = asPath.includes('?')
if (
!hasQueryParams ||
!(typebot?.settings.general.isHideQueryParamsEnabled ?? true)
hasQueryParams &&
publishedTypebot.settings.general.isHideQueryParamsEnabled !== false
)
return
push(asPath.split('?')[0], undefined, { shallow: true })
push(asPath.split('?')[0], undefined, { shallow: true })
}
const initializeResult = async () => {
const resultIdFromSession = getExistingResultFromSession()
if (resultIdFromSession) setResultId(resultIdFromSession)
else {
const { error, data } = await createResultQuery(
publishedTypebot.typebotId
)
if (error) return setError(error)
if (data?.hasReachedLimit)
return setError(new Error('This bot is now closed.'))
if (data?.result) {
setResultId(data.result.id)
if (
publishedTypebot.settings.general.isNewResultOnRefreshEnabled !== true
)
setResultInSession(data.result.id)
}
}
}
useEffect(() => {
if (!resultId || variableUpdateQueue.length === 0) return
Promise.all(variableUpdateQueue.map(sendNewVariables(resultId))).then()
setVariableUpdateQueue([])
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [resultId])
const handleNewVariables = async (variables: VariableWithValue[]) => {
if (!resultId)
return setVariableUpdateQueue([...variableUpdateQueue, variables])
await sendNewVariables(resultId)(variables)
}
const sendNewVariables =
(resultId: string) => async (variables: VariableWithValue[]) => {
if (publishedTypebot.settings.general.isResultSavingEnabled === false)
return
const { error } = await updateResultQuery(resultId, { variables })
if (error) setError(error)
}
const handleNewAnswer = async (
answer: AnswerInput & { uploadedFiles: boolean }
) => {
if (!resultId) return setError(new Error('Error: result was not created'))
if (publishedTypebot.settings.general.isResultSavingEnabled !== false) {
const { error } = await upsertAnswerQuery({ ...answer, resultId })
if (error) setError(error)
}
if (chatStarted) return
updateResultQuery(resultId, {
hasStarted: true,
}).then(({ error }) => (error ? setError(error) : setChatStarted(true)))
}
const handleCompleted = async () => {
if (publishedTypebot.settings.general.isResultSavingEnabled === false)
return
if (!resultId) return setError(new Error('Error: result was not created'))
const { error } = await updateResultQuery(resultId, { isCompleted: true })
if (error) setError(error)
}
if (error) {
return <ErrorPage error={error} />
}
return (
<div
style={{
height: '100vh',
// Set background color to avoid SSR flash
backgroundColor:
background?.type === BackgroundType.COLOR
? background?.content
: 'white',
}}
>
{typebot && (
<SEO
url={url}
typebotName={typebot.name}
metadata={typebot.settings.metadata}
<div style={{ height: '100vh' }}>
<SEO
url={url}
typebotName={publishedTypebot.typebot.name}
metadata={publishedTypebot.settings.metadata}
/>
{showTypebot && (
<TypebotViewer
typebot={publishedTypebot}
resultId={resultId}
predefinedVariables={predefinedVariables}
onNewAnswer={handleNewAnswer}
onCompleted={handleCompleted}
onVariablesUpdated={handleNewVariables}
isLoading={isNotDefined(resultId)}
/>
)}
<Standard
typebot={typebot?.publicId ?? query.publicId?.toString() ?? 'n'}
onInit={clearQueryParamsIfNecessary}
/>
</div>
)
}

View File

@@ -0,0 +1,50 @@
import { Standard } from '@typebot.io/react'
import { BackgroundType, Typebot } from 'models'
import { useRouter } from 'next/router'
import { SEO } from './Seo'
export type TypebotPageProps = {
url: string
typebot?: Pick<Typebot, 'settings' | 'theme' | 'name' | 'publicId'>
}
export const TypebotPageV3 = ({ url, typebot }: TypebotPageProps) => {
const { asPath, push, query } = useRouter()
const background = typebot?.theme.general.background
const clearQueryParamsIfNecessary = () => {
const hasQueryParams = asPath.includes('?')
if (
!hasQueryParams ||
!(typebot?.settings.general.isHideQueryParamsEnabled ?? true)
)
return
push(asPath.split('?')[0], undefined, { shallow: true })
}
return (
<div
style={{
height: '100vh',
// Set background color to avoid SSR flash
backgroundColor:
background?.type === BackgroundType.COLOR
? background?.content
: 'white',
}}
>
{typebot && (
<SEO
url={url}
typebotName={typebot.name}
metadata={typebot.settings.metadata}
/>
)}
<Standard
typebot={typebot?.publicId ?? query.publicId?.toString() ?? 'n'}
onInit={clearQueryParamsIfNecessary}
/>
</div>
)
}