2
0

(typebotLink) Better typebot link with merge option

Closes #675
This commit is contained in:
Baptiste Arnaud
2023-08-24 07:48:30 +02:00
parent 0acede92ef
commit ee3b94c35d
59 changed files with 1147 additions and 696 deletions

View File

@@ -180,7 +180,8 @@
"type": "Typebot link", "type": "Typebot link",
"options": { "options": {
"typebotId": "current", "typebotId": "current",
"groupId": "vLUAPaxKwPF49iZhg4XZYa" "groupId": "vLUAPaxKwPF49iZhg4XZYa",
"mergeResults": false
} }
} }
], ],
@@ -305,7 +306,8 @@
"groupId": "1GvxCAAEysxJMxrVngud3X", "groupId": "1GvxCAAEysxJMxrVngud3X",
"options": { "options": {
"groupId": "vLUAPaxKwPF49iZhg4XZYa", "groupId": "vLUAPaxKwPF49iZhg4XZYa",
"typebotId": "current" "typebotId": "current",
"mergeResults": false
} }
} }
], ],

View File

@@ -282,7 +282,8 @@
"type": "Typebot link", "type": "Typebot link",
"options": { "options": {
"typebotId": "current", "typebotId": "current",
"groupId": "cl96ns9qr00043b6ii07bo25o" "groupId": "cl96ns9qr00043b6ii07bo25o",
"mergeResults": false
} }
} }
] ]

View File

@@ -618,7 +618,8 @@
"groupId": "cl1r15f68005f2e6dvdtal7cp", "groupId": "cl1r15f68005f2e6dvdtal7cp",
"options": { "options": {
"groupId": "cl1r09bc6000h2e6dqml18p4p", "groupId": "cl1r09bc6000h2e6dqml18p4p",
"typebotId": "current" "typebotId": "current",
"mergeResults": false
} }
} }
], ],

View File

@@ -65,7 +65,8 @@ export const getLinkedTypebots = authenticatedProcedure
(typebotIds, block) => (typebotIds, block) =>
block.type === LogicBlockType.TYPEBOT_LINK && block.type === LogicBlockType.TYPEBOT_LINK &&
isDefined(block.options.typebotId) && isDefined(block.options.typebotId) &&
!typebotIds.includes(block.options.typebotId) !typebotIds.includes(block.options.typebotId) &&
block.options.mergeResults !== false
? [...typebotIds, block.options.typebotId] ? [...typebotIds, block.options.typebotId]
: typebotIds, : typebotIds,
[] []

View File

@@ -1,10 +1,11 @@
import { Stack } from '@chakra-ui/react' import { Stack } from '@chakra-ui/react'
import { useTypebot } from '@/features/editor/providers/TypebotProvider' import { useTypebot } from '@/features/editor/providers/TypebotProvider'
import { TypebotLinkOptions } from '@typebot.io/schemas' import { TypebotLinkOptions } from '@typebot.io/schemas'
import { byId } from '@typebot.io/lib'
import { GroupsDropdown } from './GroupsDropdown' import { GroupsDropdown } from './GroupsDropdown'
import { TypebotsDropdown } from './TypebotsDropdown' import { TypebotsDropdown } from './TypebotsDropdown'
import { useEffect, useState } from 'react' import { trpc } from '@/lib/trpc'
import { isNotEmpty } from '@typebot.io/lib'
import { SwitchWithLabel } from '@/components/inputs/SwitchWithLabel'
type Props = { type Props = {
options: TypebotLinkOptions options: TypebotLinkOptions
@@ -12,21 +13,30 @@ type Props = {
} }
export const TypebotLinkForm = ({ options, onOptionsChange }: Props) => { export const TypebotLinkForm = ({ options, onOptionsChange }: Props) => {
const { linkedTypebots, typebot, save } = useTypebot() const { typebot } = useTypebot()
const [linkedTypebotId, setLinkedTypebotId] = useState(options.typebotId)
const handleTypebotIdChange = async ( const handleTypebotIdChange = async (
typebotId: string | 'current' | undefined typebotId: string | 'current' | undefined
) => onOptionsChange({ ...options, typebotId }) ) => onOptionsChange({ ...options, typebotId, groupId: undefined })
const { data: linkedTypebotData } = trpc.typebot.getTypebot.useQuery(
{
typebotId: options.typebotId as string,
},
{
enabled: isNotEmpty(options.typebotId) && options.typebotId !== 'current',
}
)
const handleGroupIdChange = (groupId: string | undefined) => const handleGroupIdChange = (groupId: string | undefined) =>
onOptionsChange({ ...options, groupId }) onOptionsChange({ ...options, groupId })
useEffect(() => { const updateMergeResults = (mergeResults: boolean) =>
if (linkedTypebotId === options.typebotId) return onOptionsChange({ ...options, mergeResults })
setLinkedTypebotId(options.typebotId)
save().then() const isCurrentTypebotSelected =
}, [linkedTypebotId, options.typebotId, save]) (typebot && options.typebotId === typebot.id) ||
options.typebotId === 'current'
return ( return (
<Stack> <Stack>
@@ -40,22 +50,30 @@ export const TypebotLinkForm = ({ options, onOptionsChange }: Props) => {
)} )}
{options.typebotId && ( {options.typebotId && (
<GroupsDropdown <GroupsDropdown
key={options.typebotId}
groups={ groups={
typebot && typebot && isCurrentTypebotSelected
(options.typebotId === typebot.id ||
options.typebotId === 'current')
? typebot.groups ? typebot.groups
: linkedTypebots?.find(byId(options.typebotId))?.groups ?? [] : linkedTypebotData?.typebot?.groups ?? []
} }
groupId={options.groupId} groupId={options.groupId}
onGroupIdSelected={handleGroupIdChange} onGroupIdSelected={handleGroupIdChange}
isLoading={ isLoading={
linkedTypebots === undefined && linkedTypebotData?.typebot === undefined &&
options.typebotId !== 'current' &&
typebot && typebot &&
typebot.id !== options.typebotId typebot.id !== options.typebotId
} }
/> />
)} )}
{!isCurrentTypebotSelected && (
<SwitchWithLabel
label="Merge answers"
moreInfoContent="If enabled, the answers collected in the linked typebot will be merged with the results of the current typebot."
initialValue={options.mergeResults ?? true}
onCheckChange={updateMergeResults}
/>
)}
</Stack> </Stack>
) )
} }

View File

@@ -2,24 +2,36 @@ import { TypebotLinkBlock } from '@typebot.io/schemas'
import React from 'react' import React from 'react'
import { Tag, Text } from '@chakra-ui/react' import { Tag, Text } from '@chakra-ui/react'
import { useTypebot } from '@/features/editor/providers/TypebotProvider' import { useTypebot } from '@/features/editor/providers/TypebotProvider'
import { byId } from '@typebot.io/lib' import { byId, isNotEmpty } from '@typebot.io/lib'
import { trpc } from '@/lib/trpc'
type Props = { type Props = {
block: TypebotLinkBlock block: TypebotLinkBlock
} }
export const TypebotLinkNode = ({ block }: Props) => { export const TypebotLinkNode = ({ block }: Props) => {
const { linkedTypebots, typebot } = useTypebot() const { typebot } = useTypebot()
const { data: linkedTypebotData } = trpc.typebot.getTypebot.useQuery(
{
typebotId: block.options.typebotId as string,
},
{
enabled:
isNotEmpty(block.options.typebotId) &&
block.options.typebotId !== 'current',
}
)
const isCurrentTypebot = const isCurrentTypebot =
typebot && typebot &&
(block.options.typebotId === typebot.id || (block.options.typebotId === typebot.id ||
block.options.typebotId === 'current') block.options.typebotId === 'current')
const linkedTypebot = isCurrentTypebot const linkedTypebot = isCurrentTypebot ? typebot : linkedTypebotData?.typebot
? typebot
: linkedTypebots?.find(byId(block.options.typebotId))
const blockTitle = linkedTypebot?.groups.find( const blockTitle = linkedTypebot?.groups.find(
byId(block.options.groupId) byId(block.options.groupId)
)?.title )?.title
if (!block.options.typebotId) if (!block.options.typebotId)
return <Text color="gray.500">Configure...</Text> return <Text color="gray.500">Configure...</Text>
return ( return (

View File

@@ -61,7 +61,17 @@ export const TypebotsDropdown = ({
aria-label="Navigate to typebot" aria-label="Navigate to typebot"
icon={<ExternalLinkIcon />} icon={<ExternalLinkIcon />}
as={Link} as={Link}
href={`/typebots/${typebotId}/edit?parentId=${query.typebotId}`} href={{
pathname: '/typebots/[typebotId]/edit',
query: {
typebotId,
parentId: query.parentId
? Array.isArray(query.parentId)
? query.parentId.concat(query.typebotId?.toString() ?? '')
: [query.parentId, query.typebotId?.toString() ?? '']
: query.typebotId ?? [],
},
}}
/> />
)} )}
</HStack> </HStack>

View File

@@ -156,13 +156,22 @@ export const TypebotHeader = () => {
as={Link} as={Link}
aria-label="Navigate back" aria-label="Navigate back"
icon={<ChevronLeftIcon fontSize={25} />} icon={<ChevronLeftIcon fontSize={25} />}
href={ href={{
router.query.parentId pathname: router.query.parentId
? `/typebots/${router.query.parentId}/edit` ? '/typebots/[typebotId]/edit'
: typebot?.folderId : typebot?.folderId
? `/typebots/folders/${typebot.folderId}` ? '/typebots/folders/[folderId]'
: '/typebots' : '/typebots',
} query: {
folderId: typebot?.folderId ?? [],
parentId: Array.isArray(router.query.parentId)
? router.query.parentId.slice(0, -1)
: [],
typebotId: Array.isArray(router.query.parentId)
? [...router.query.parentId].pop()
: router.query.parentId ?? [],
},
}}
size="sm" size="sm"
/> />
<HStack spacing={1}> <HStack spacing={1}>

View File

@@ -1,4 +1,4 @@
import { LogicBlockType, PublicTypebot, Typebot } from '@typebot.io/schemas' import { PublicTypebot, Typebot } from '@typebot.io/schemas'
import { Router, useRouter } from 'next/router' import { Router, useRouter } from 'next/router'
import { import {
createContext, createContext,
@@ -49,7 +49,6 @@ const typebotContext = createContext<
{ {
typebot?: Typebot typebot?: Typebot
publishedTypebot?: PublicTypebot publishedTypebot?: PublicTypebot
linkedTypebots?: Pick<Typebot, 'id' | 'groups' | 'variables' | 'name'>[]
isReadOnly?: boolean isReadOnly?: boolean
isPublished: boolean isPublished: boolean
isSavingLoading: boolean isSavingLoading: boolean
@@ -132,36 +131,6 @@ export const TypebotProvider = ({
{ redo, undo, flush, canRedo, canUndo, set: setLocalTypebot }, { redo, undo, flush, canRedo, canUndo, set: setLocalTypebot },
] = useUndo<Typebot>(undefined) ] = useUndo<Typebot>(undefined)
const linkedTypebotIds = useMemo(
() =>
typebot?.groups
.flatMap((group) => group.blocks)
.reduce<string[]>(
(typebotIds, block) =>
block.type === LogicBlockType.TYPEBOT_LINK &&
isDefined(block.options.typebotId) &&
!typebotIds.includes(block.options.typebotId)
? [...typebotIds, block.options.typebotId]
: typebotIds,
[]
) ?? [],
[typebot?.groups]
)
const { data: linkedTypebotsData } = trpc.getLinkedTypebots.useQuery(
{
typebotId: typebot?.id as string,
},
{
enabled: isDefined(typebot?.id) && linkedTypebotIds.length > 0,
onError: (error) =>
showToast({
title: 'Error while fetching linkedTypebots',
description: error.message,
}),
}
)
useEffect(() => { useEffect(() => {
if (!typebot && isDefined(localTypebot)) setLocalTypebot(undefined) if (!typebot && isDefined(localTypebot)) setLocalTypebot(undefined)
if (isFetchingTypebot) return if (isFetchingTypebot) return
@@ -270,7 +239,6 @@ export const TypebotProvider = ({
value={{ value={{
typebot: localTypebot, typebot: localTypebot,
publishedTypebot, publishedTypebot,
linkedTypebots: linkedTypebotsData?.typebots ?? [],
isReadOnly: typebotData?.isReadOnly, isReadOnly: typebotData?.isReadOnly,
isSavingLoading: isSaving, isSavingLoading: isSaving,
save: saveTypebot, save: saveTypebot,

View File

@@ -1,11 +1,17 @@
import { useToast } from '@/hooks/useToast' import { useToast } from '@/hooks/useToast'
import { ResultHeaderCell, ResultWithAnswers } from '@typebot.io/schemas' import {
LogicBlockType,
ResultHeaderCell,
ResultWithAnswers,
} from '@typebot.io/schemas'
import { createContext, ReactNode, useContext, useMemo } from 'react' import { createContext, ReactNode, useContext, useMemo } from 'react'
import { parseResultHeader } from '@typebot.io/lib/results' import { parseResultHeader } from '@typebot.io/lib/results'
import { useTypebot } from '../editor/providers/TypebotProvider' import { useTypebot } from '../editor/providers/TypebotProvider'
import { useResultsQuery } from './hooks/useResultsQuery' import { useResultsQuery } from './hooks/useResultsQuery'
import { TableData } from './types' import { TableData } from './types'
import { convertResultsToTableData } from './helpers/convertResultsToTableData' import { convertResultsToTableData } from './helpers/convertResultsToTableData'
import { trpc } from '@/lib/trpc'
import { isDefined } from '@typebot.io/lib/utils'
const resultsContext = createContext<{ const resultsContext = createContext<{
resultsList: { results: ResultWithAnswers[] }[] | undefined resultsList: { results: ResultWithAnswers[] }[] | undefined
@@ -32,7 +38,7 @@ export const ResultsProvider = ({
totalResults: number totalResults: number
onDeleteResults: (totalResultsDeleted: number) => void onDeleteResults: (totalResultsDeleted: number) => void
}) => { }) => {
const { publishedTypebot, linkedTypebots } = useTypebot() const { publishedTypebot } = useTypebot()
const { showToast } = useToast() const { showToast } = useToast()
const { data, fetchNextPage, hasNextPage, refetch } = useResultsQuery({ const { data, fetchNextPage, hasNextPage, refetch } = useResultsQuery({
typebotId, typebotId,
@@ -41,6 +47,29 @@ export const ResultsProvider = ({
}, },
}) })
const linkedTypebotIds =
publishedTypebot?.groups
.flatMap((group) => group.blocks)
.reduce<string[]>(
(typebotIds, block) =>
block.type === LogicBlockType.TYPEBOT_LINK &&
isDefined(block.options.typebotId) &&
!typebotIds.includes(block.options.typebotId) &&
block.options.mergeResults !== false
? [...typebotIds, block.options.typebotId]
: typebotIds,
[]
) ?? []
const { data: linkedTypebotsData } = trpc.getLinkedTypebots.useQuery(
{
typebotId,
},
{
enabled: linkedTypebotIds.length > 0,
}
)
const flatResults = useMemo( const flatResults = useMemo(
() => data?.flatMap((d) => d.results) ?? [], () => data?.flatMap((d) => d.results) ?? [],
[data] [data]
@@ -49,9 +78,9 @@ export const ResultsProvider = ({
const resultHeader = useMemo( const resultHeader = useMemo(
() => () =>
publishedTypebot publishedTypebot
? parseResultHeader(publishedTypebot, linkedTypebots) ? parseResultHeader(publishedTypebot, linkedTypebotsData?.typebots)
: [], : [],
[linkedTypebots, publishedTypebot] [linkedTypebotsData?.typebots, publishedTypebot]
) )
const tableData = useMemo( const tableData = useMemo(

View File

@@ -23,6 +23,7 @@ import { useResults } from '../../ResultsProvider'
import { parseColumnOrder } from '../../helpers/parseColumnsOrder' import { parseColumnOrder } from '../../helpers/parseColumnsOrder'
import { convertResultsToTableData } from '../../helpers/convertResultsToTableData' import { convertResultsToTableData } from '../../helpers/convertResultsToTableData'
import { parseAccessor } from '../../helpers/parseAccessor' import { parseAccessor } from '../../helpers/parseAccessor'
import { isDefined } from '@typebot.io/lib'
type Props = { type Props = {
isOpen: boolean isOpen: boolean
@@ -30,7 +31,7 @@ type Props = {
} }
export const ExportAllResultsModal = ({ isOpen, onClose }: Props) => { export const ExportAllResultsModal = ({ isOpen, onClose }: Props) => {
const { typebot, publishedTypebot, linkedTypebots } = useTypebot() const { typebot, publishedTypebot } = useTypebot()
const workspaceId = typebot?.workspaceId const workspaceId = typebot?.workspaceId
const typebotId = typebot?.id const typebotId = typebot?.id
const { showToast } = useToast() const { showToast } = useToast()
@@ -41,6 +42,15 @@ export const ExportAllResultsModal = ({ isOpen, onClose }: Props) => {
const [areDeletedBlocksIncluded, setAreDeletedBlocksIncluded] = const [areDeletedBlocksIncluded, setAreDeletedBlocksIncluded] =
useState(false) useState(false)
const { data: linkedTypebotsData } = trpc.getLinkedTypebots.useQuery(
{
typebotId: typebotId as string,
},
{
enabled: isDefined(typebotId),
}
)
const getAllResults = async () => { const getAllResults = async () => {
if (!workspaceId || !typebotId) return [] if (!workspaceId || !typebotId) return []
const allResults = [] const allResults = []
@@ -71,7 +81,11 @@ export const ExportAllResultsModal = ({ isOpen, onClose }: Props) => {
const results = await getAllResults() const results = await getAllResults()
const resultHeader = areDeletedBlocksIncluded const resultHeader = areDeletedBlocksIncluded
? parseResultHeader(publishedTypebot, linkedTypebots, results) ? parseResultHeader(
publishedTypebot,
linkedTypebotsData?.typebots,
results
)
: existingResultHeader : existingResultHeader
const dataToUnparse = convertResultsToTableData(results, resultHeader) const dataToUnparse = convertResultsToTableData(results, resultHeader)

View File

@@ -43,6 +43,7 @@ import {
LogicBlockType, LogicBlockType,
defaultAbTestOptions, defaultAbTestOptions,
BlockWithItems, BlockWithItems,
defaultTypebotLinkOptions,
} from '@typebot.io/schemas' } from '@typebot.io/schemas'
import { defaultPictureChoiceOptions } from '@typebot.io/schemas/features/blocks/inputs/pictureChoice' import { defaultPictureChoiceOptions } from '@typebot.io/schemas/features/blocks/inputs/pictureChoice'
@@ -122,7 +123,7 @@ const parseDefaultBlockOptions = (type: BlockWithOptionsType): BlockOptions => {
case LogicBlockType.JUMP: case LogicBlockType.JUMP:
return {} return {}
case LogicBlockType.TYPEBOT_LINK: case LogicBlockType.TYPEBOT_LINK:
return {} return defaultTypebotLinkOptions
case LogicBlockType.AB_TEST: case LogicBlockType.AB_TEST:
return defaultAbTestOptions return defaultAbTestOptions
case IntegrationBlockType.GOOGLE_SHEETS: case IntegrationBlockType.GOOGLE_SHEETS:

View File

@@ -13,8 +13,9 @@ import { filterChoiceItems } from './filterChoiceItems'
export const injectVariableValuesInButtonsInputBlock = export const injectVariableValuesInButtonsInputBlock =
(state: SessionState) => (state: SessionState) =>
(block: ChoiceInputBlock): ChoiceInputBlock => { (block: ChoiceInputBlock): ChoiceInputBlock => {
const { variables } = state.typebotsQueue[0].typebot
if (block.options.dynamicVariableId) { if (block.options.dynamicVariableId) {
const variable = state.typebot.variables.find( const variable = variables.find(
(variable) => (variable) =>
variable.id === block.options.dynamicVariableId && variable.id === block.options.dynamicVariableId &&
isDefined(variable.value) isDefined(variable.value)
@@ -31,18 +32,17 @@ export const injectVariableValuesInButtonsInputBlock =
})), })),
} }
} }
return deepParseVariables(state.typebot.variables)( return deepParseVariables(variables)(filterChoiceItems(variables)(block))
filterChoiceItems(state.typebot.variables)(block)
)
} }
const getVariableValue = const getVariableValue =
(state: SessionState) => (state: SessionState) =>
(variable: VariableWithValue): (string | null)[] => { (variable: VariableWithValue): (string | null)[] => {
if (!Array.isArray(variable.value)) { if (!Array.isArray(variable.value)) {
const [transformedVariable] = transformStringVariablesToList( const { variables } = state.typebotsQueue[0].typebot
state.typebot.variables const [transformedVariable] = transformStringVariablesToList(variables)([
)([variable.id]) variable.id,
])
updateVariables(state)([transformedVariable]) updateVariables(state)([transformedVariable])
return transformedVariable.value as string[] return transformedVariable.value as string[]
} }

View File

@@ -11,18 +11,17 @@ import Stripe from 'stripe'
import { decrypt } from '@typebot.io/lib/api/encryption' import { decrypt } from '@typebot.io/lib/api/encryption'
export const computePaymentInputRuntimeOptions = export const computePaymentInputRuntimeOptions =
(state: Pick<SessionState, 'result' | 'typebot'>) => (state: SessionState) => (options: PaymentInputOptions) =>
(options: PaymentInputOptions) =>
createStripePaymentIntent(state)(options) createStripePaymentIntent(state)(options)
const createStripePaymentIntent = const createStripePaymentIntent =
(state: Pick<SessionState, 'result' | 'typebot'>) => (state: SessionState) =>
async (options: PaymentInputOptions): Promise<PaymentInputRuntimeOptions> => { async (options: PaymentInputOptions): Promise<PaymentInputRuntimeOptions> => {
const { const {
result, resultId,
typebot: { variables }, typebot: { variables },
} = state } = state.typebotsQueue[0]
const isPreview = !result.id const isPreview = !resultId
if (!options.credentialsId) if (!options.credentialsId)
throw new TRPCError({ throw new TRPCError({
code: 'BAD_REQUEST', code: 'BAD_REQUEST',

View File

@@ -1,15 +1,15 @@
import { import {
SessionState,
VariableWithValue, VariableWithValue,
ItemType, ItemType,
PictureChoiceBlock, PictureChoiceBlock,
Variable,
} from '@typebot.io/schemas' } from '@typebot.io/schemas'
import { isDefined } from '@typebot.io/lib' import { isDefined } from '@typebot.io/lib'
import { deepParseVariables } from '@/features/variables/deepParseVariable' import { deepParseVariables } from '@/features/variables/deepParseVariable'
import { filterPictureChoiceItems } from './filterPictureChoiceItems' import { filterPictureChoiceItems } from './filterPictureChoiceItems'
export const injectVariableValuesInPictureChoiceBlock = export const injectVariableValuesInPictureChoiceBlock =
(variables: SessionState['typebot']['variables']) => (variables: Variable[]) =>
(block: PictureChoiceBlock): PictureChoiceBlock => { (block: PictureChoiceBlock): PictureChoiceBlock => {
if ( if (
block.options.dynamicItems?.isEnabled && block.options.dynamicItems?.isEnabled &&

View File

@@ -70,17 +70,18 @@ if (window.$chatwoot) {
` `
export const executeChatwootBlock = ( export const executeChatwootBlock = (
{ typebot, result }: SessionState, state: SessionState,
block: ChatwootBlock block: ChatwootBlock
): ExecuteIntegrationResponse => { ): ExecuteIntegrationResponse => {
const { typebot, resultId } = state.typebotsQueue[0]
const chatwootCode = const chatwootCode =
block.options.task === 'Close widget' block.options.task === 'Close widget'
? chatwootCloseCode ? chatwootCloseCode
: isDefined(result.id) : isDefined(resultId)
? parseChatwootOpenCode({ ? parseChatwootOpenCode({
...block.options, ...block.options,
typebotId: typebot.id, typebotId: typebot.id,
resultId: result.id, resultId,
}) })
: '' : ''
return { return {

View File

@@ -3,11 +3,12 @@ import { deepParseVariables } from '@/features/variables/deepParseVariable'
import { GoogleAnalyticsBlock, SessionState } from '@typebot.io/schemas' import { GoogleAnalyticsBlock, SessionState } from '@typebot.io/schemas'
export const executeGoogleAnalyticsBlock = ( export const executeGoogleAnalyticsBlock = (
{ typebot: { variables }, result }: SessionState, state: SessionState,
block: GoogleAnalyticsBlock block: GoogleAnalyticsBlock
): ExecuteIntegrationResponse => { ): ExecuteIntegrationResponse => {
if (!result) return { outgoingEdgeId: block.outgoingEdgeId } const { typebot, resultId } = state.typebotsQueue[0]
const googleAnalytics = deepParseVariables(variables, { if (!resultId) return { outgoingEdgeId: block.outgoingEdgeId }
const googleAnalytics = deepParseVariables(typebot.variables, {
guessCorrectTypes: true, guessCorrectTypes: true,
removeEmptyStrings: true, removeEmptyStrings: true,
})(block.options) })(block.options)

View File

@@ -19,13 +19,11 @@ export const getRow = async (
}: { outgoingEdgeId?: string; options: GoogleSheetsGetOptions } }: { outgoingEdgeId?: string; options: GoogleSheetsGetOptions }
): Promise<ExecuteIntegrationResponse> => { ): Promise<ExecuteIntegrationResponse> => {
const logs: ReplyLog[] = [] const logs: ReplyLog[] = []
const { sheetId, cellsToExtract, referenceCell, filter } = deepParseVariables( const { variables } = state.typebotsQueue[0].typebot
state.typebot.variables const { sheetId, cellsToExtract, referenceCell, filter } =
)(options) deepParseVariables(variables)(options)
if (!sheetId) return { outgoingEdgeId } if (!sheetId) return { outgoingEdgeId }
const variables = state.typebot.variables
const doc = await getAuthenticatedGoogleDoc({ const doc = await getAuthenticatedGoogleDoc({
credentialsId: options.credentialsId, credentialsId: options.credentialsId,
spreadsheetId: options.spreadsheetId, spreadsheetId: options.spreadsheetId,

View File

@@ -8,12 +8,13 @@ import { getAuthenticatedGoogleDoc } from './helpers/getAuthenticatedGoogleDoc'
import { ExecuteIntegrationResponse } from '@/features/chat/types' import { ExecuteIntegrationResponse } from '@/features/chat/types'
export const insertRow = async ( export const insertRow = async (
{ typebot: { variables } }: SessionState, state: SessionState,
{ {
outgoingEdgeId, outgoingEdgeId,
options, options,
}: { outgoingEdgeId?: string; options: GoogleSheetsInsertRowOptions } }: { outgoingEdgeId?: string; options: GoogleSheetsInsertRowOptions }
): Promise<ExecuteIntegrationResponse> => { ): Promise<ExecuteIntegrationResponse> => {
const { variables } = state.typebotsQueue[0].typebot
if (!options.cellsToInsert || !options.sheetId) return { outgoingEdgeId } if (!options.cellsToInsert || !options.sheetId) return { outgoingEdgeId }
const logs: ReplyLog[] = [] const logs: ReplyLog[] = []

View File

@@ -10,12 +10,13 @@ import { ExecuteIntegrationResponse } from '@/features/chat/types'
import { matchFilter } from './helpers/matchFilter' import { matchFilter } from './helpers/matchFilter'
export const updateRow = async ( export const updateRow = async (
{ typebot: { variables } }: SessionState, state: SessionState,
{ {
outgoingEdgeId, outgoingEdgeId,
options, options,
}: { outgoingEdgeId?: string; options: GoogleSheetsUpdateRowOptions } }: { outgoingEdgeId?: string; options: GoogleSheetsUpdateRowOptions }
): Promise<ExecuteIntegrationResponse> => { ): Promise<ExecuteIntegrationResponse> => {
const { variables } = state.typebotsQueue[0].typebot
const { sheetId, referenceCell, filter } = const { sheetId, referenceCell, filter } =
deepParseVariables(variables)(options) deepParseVariables(variables)(options)
if (!options.cellsToUpsert || !sheetId || (!referenceCell && !filter)) if (!options.cellsToUpsert || !sheetId || (!referenceCell && !filter))

View File

@@ -1,6 +1,11 @@
import { ExecuteIntegrationResponse } from '@/features/chat/types' import { ExecuteIntegrationResponse } from '@/features/chat/types'
import prisma from '@/lib/prisma' import prisma from '@/lib/prisma'
import { Block, BubbleBlockType, SessionState } from '@typebot.io/schemas' import {
Block,
BubbleBlockType,
SessionState,
TypebotInSession,
} from '@typebot.io/schemas'
import { import {
ChatCompletionOpenAIOptions, ChatCompletionOpenAIOptions,
OpenAICredentials, OpenAICredentials,
@@ -51,13 +56,16 @@ export const createChatCompletionOpenAI = async (
credentials.data, credentials.data,
credentials.iv credentials.iv
)) as OpenAICredentials['data'] )) as OpenAICredentials['data']
const { typebot } = newSessionState.typebotsQueue[0]
const { variablesTransformedToList, messages } = parseChatCompletionMessages( const { variablesTransformedToList, messages } = parseChatCompletionMessages(
newSessionState.typebot.variables typebot.variables
)(options.messages) )(options.messages)
if (variablesTransformedToList.length > 0) if (variablesTransformedToList.length > 0)
newSessionState = updateVariables(state)(variablesTransformedToList) newSessionState = updateVariables(state)(variablesTransformedToList)
const temperature = parseVariableNumber(newSessionState.typebot.variables)( const temperature = parseVariableNumber(typebot.variables)(
options.advancedSettings?.temperature options.advancedSettings?.temperature
) )
@@ -66,7 +74,7 @@ export const createChatCompletionOpenAI = async (
isCredentialsV2(credentials) && isCredentialsV2(credentials) &&
newSessionState.isStreamEnabled newSessionState.isStreamEnabled
) { ) {
const assistantMessageVariableName = state.typebot.variables.find( const assistantMessageVariableName = typebot.variables.find(
(variable) => (variable) =>
options.responseMapping.find( options.responseMapping.find(
(m) => m.valueToExtract === 'Message content' (m) => m.valueToExtract === 'Message content'
@@ -81,9 +89,10 @@ export const createChatCompletionOpenAI = async (
content?: string content?: string
role: (typeof chatCompletionMessageRoles)[number] role: (typeof chatCompletionMessageRoles)[number]
}[], }[],
displayStream: isNextBubbleMessageWithAssistantMessage( displayStream: isNextBubbleMessageWithAssistantMessage(typebot)(
state.typebot blockId,
)(blockId, assistantMessageVariableName), assistantMessageVariableName
),
}, },
}, },
], ],
@@ -117,7 +126,7 @@ export const createChatCompletionOpenAI = async (
} }
const isNextBubbleMessageWithAssistantMessage = const isNextBubbleMessageWithAssistantMessage =
(typebot: SessionState['typebot']) => (typebot: TypebotInSession) =>
(blockId: string, assistantVariableName?: string): boolean => { (blockId: string, assistantVariableName?: string): boolean => {
if (!assistantVariableName) return false if (!assistantVariableName) return false
const nextBlock = getNextBlock(typebot)(blockId) const nextBlock = getNextBlock(typebot)(blockId)
@@ -131,7 +140,7 @@ const isNextBubbleMessageWithAssistantMessage =
} }
const getNextBlock = const getNextBlock =
(typebot: SessionState['typebot']) => (typebot: TypebotInSession) =>
(blockId: string): Block | undefined => { (blockId: string): Block | undefined => {
const group = typebot.groups.find((group) => const group = typebot.groups.find((group) =>
group.blocks.find(byId(blockId)) group.blocks.find(byId(blockId))

View File

@@ -5,7 +5,7 @@ import {
ChatCompletionOpenAIOptions, ChatCompletionOpenAIOptions,
OpenAICredentials, OpenAICredentials,
} from '@typebot.io/schemas/features/blocks/integrations/openai' } from '@typebot.io/schemas/features/blocks/integrations/openai'
import { SessionState } from '@typebot.io/schemas/features/chat' import { SessionState } from '@typebot.io/schemas/features/chat/sessionState'
import { OpenAIStream } from 'ai' import { OpenAIStream } from 'ai'
import { import {
ChatCompletionRequestMessage, ChatCompletionRequestMessage,
@@ -35,7 +35,8 @@ export const getChatCompletionStream =
credentials.iv credentials.iv
)) as OpenAICredentials['data'] )) as OpenAICredentials['data']
const temperature = parseVariableNumber(state.typebot.variables)( const { typebot } = state.typebotsQueue[0]
const temperature = parseVariableNumber(typebot.variables)(
options.advancedSettings?.temperature options.advancedSettings?.temperature
) )

View File

@@ -22,9 +22,8 @@ export const resumeChatCompletion =
const newVariables = options.responseMapping.reduce< const newVariables = options.responseMapping.reduce<
VariableWithUnknowValue[] VariableWithUnknowValue[]
>((newVariables, mapping) => { >((newVariables, mapping) => {
const existingVariable = newSessionState.typebot.variables.find( const { typebot } = newSessionState.typebotsQueue[0]
byId(mapping.variableId) const existingVariable = typebot.variables.find(byId(mapping.variableId))
)
if (!existingVariable) return newVariables if (!existingVariable) return newVariables
if (mapping.valueToExtract === 'Message content') { if (mapping.valueToExtract === 'Message content') {
newVariables.push({ newVariables.push({

View File

@@ -3,12 +3,13 @@ import { deepParseVariables } from '@/features/variables/deepParseVariable'
import { PixelBlock, SessionState } from '@typebot.io/schemas' import { PixelBlock, SessionState } from '@typebot.io/schemas'
export const executePixelBlock = ( export const executePixelBlock = (
{ typebot: { variables }, result }: SessionState, state: SessionState,
block: PixelBlock block: PixelBlock
): ExecuteIntegrationResponse => { ): ExecuteIntegrationResponse => {
if (!result || !block.options.pixelId || !block.options.eventType) const { typebot, resultId } = state.typebotsQueue[0]
if (!resultId || !block.options.pixelId || !block.options.eventType)
return { outgoingEdgeId: block.outgoingEdgeId } return { outgoingEdgeId: block.outgoingEdgeId }
const pixel = deepParseVariables(variables, { const pixel = deepParseVariables(typebot.variables, {
guessCorrectTypes: true, guessCorrectTypes: true,
removeEmptyStrings: true, removeEmptyStrings: true,
})(block.options) })(block.options)

View File

@@ -3,32 +3,32 @@ import prisma from '@/lib/prisma'
import { render } from '@faire/mjml-react/utils/render' import { render } from '@faire/mjml-react/utils/render'
import { DefaultBotNotificationEmail } from '@typebot.io/emails' import { DefaultBotNotificationEmail } from '@typebot.io/emails'
import { import {
PublicTypebot, AnswerInSessionState,
ReplyLog, ReplyLog,
ResultInSession,
SendEmailBlock, SendEmailBlock,
SendEmailOptions, SendEmailOptions,
SessionState, SessionState,
SmtpCredentials, SmtpCredentials,
TypebotInSession,
Variable, Variable,
} from '@typebot.io/schemas' } from '@typebot.io/schemas'
import { createTransport } from 'nodemailer' import { createTransport } from 'nodemailer'
import Mail from 'nodemailer/lib/mailer' import Mail from 'nodemailer/lib/mailer'
import { byId, isDefined, isEmpty, isNotDefined, omit } from '@typebot.io/lib' import { byId, isDefined, isEmpty, isNotDefined, omit } from '@typebot.io/lib'
import { parseAnswers } from '@typebot.io/lib/results' import { getDefinedVariables, parseAnswers } from '@typebot.io/lib/results'
import { decrypt } from '@typebot.io/lib/api' import { decrypt } from '@typebot.io/lib/api'
import { defaultFrom, defaultTransportOptions } from './constants' import { defaultFrom, defaultTransportOptions } from './constants'
import { ExecuteIntegrationResponse } from '@/features/chat/types' import { ExecuteIntegrationResponse } from '@/features/chat/types'
import { findUniqueVariableValue } from '../../../variables/findUniqueVariableValue' import { findUniqueVariableValue } from '../../../variables/findUniqueVariableValue'
export const executeSendEmailBlock = async ( export const executeSendEmailBlock = async (
{ result, typebot }: SessionState, state: SessionState,
block: SendEmailBlock block: SendEmailBlock
): Promise<ExecuteIntegrationResponse> => { ): Promise<ExecuteIntegrationResponse> => {
const logs: ReplyLog[] = [] const logs: ReplyLog[] = []
const { options } = block const { options } = block
const { variables } = typebot const { typebot, resultId, answers } = state.typebotsQueue[0]
const isPreview = !result.id const isPreview = !resultId
if (isPreview) if (isPreview)
return { return {
outgoingEdgeId: block.outgoingEdgeId, outgoingEdgeId: block.outgoingEdgeId,
@@ -41,23 +41,23 @@ export const executeSendEmailBlock = async (
} }
const body = const body =
findUniqueVariableValue(variables)(options.body)?.toString() ?? findUniqueVariableValue(typebot.variables)(options.body)?.toString() ??
parseVariables(variables, { escapeHtml: true })(options.body ?? '') parseVariables(typebot.variables, { escapeHtml: true })(options.body ?? '')
try { try {
const sendEmailLogs = await sendEmail({ const sendEmailLogs = await sendEmail({
typebotId: typebot.id, typebot,
result, answers,
credentialsId: options.credentialsId, credentialsId: options.credentialsId,
recipients: options.recipients.map(parseVariables(variables)), recipients: options.recipients.map(parseVariables(typebot.variables)),
subject: parseVariables(variables)(options.subject ?? ''), subject: parseVariables(typebot.variables)(options.subject ?? ''),
body, body,
cc: (options.cc ?? []).map(parseVariables(variables)), cc: (options.cc ?? []).map(parseVariables(typebot.variables)),
bcc: (options.bcc ?? []).map(parseVariables(variables)), bcc: (options.bcc ?? []).map(parseVariables(typebot.variables)),
replyTo: options.replyTo replyTo: options.replyTo
? parseVariables(variables)(options.replyTo) ? parseVariables(typebot.variables)(options.replyTo)
: undefined, : undefined,
fileUrls: getFileUrls(variables)(options.attachmentsVariableId), fileUrls: getFileUrls(typebot.variables)(options.attachmentsVariableId),
isCustomBody: options.isCustomBody, isCustomBody: options.isCustomBody,
isBodyCode: options.isBodyCode, isBodyCode: options.isBodyCode,
}) })
@@ -74,8 +74,8 @@ export const executeSendEmailBlock = async (
} }
const sendEmail = async ({ const sendEmail = async ({
typebotId, typebot,
result, answers,
credentialsId, credentialsId,
recipients, recipients,
body, body,
@@ -87,8 +87,8 @@ const sendEmail = async ({
isCustomBody, isCustomBody,
fileUrls, fileUrls,
}: SendEmailOptions & { }: SendEmailOptions & {
typebotId: string typebot: TypebotInSession
result: ResultInSession answers: AnswerInSessionState[]
fileUrls?: string | string[] fileUrls?: string | string[]
}): Promise<ReplyLog[] | undefined> => { }): Promise<ReplyLog[] | undefined> => {
const logs: ReplyLog[] = [] const logs: ReplyLog[] = []
@@ -112,8 +112,8 @@ const sendEmail = async ({
body, body,
isCustomBody, isCustomBody,
isBodyCode, isBodyCode,
typebotId, typebot,
result, answersInSession: answers,
}) })
if (!emailBody) { if (!emailBody) {
@@ -206,11 +206,11 @@ const getEmailBody = async ({
body, body,
isCustomBody, isCustomBody,
isBodyCode, isBodyCode,
typebotId, typebot,
result, answersInSession,
}: { }: {
typebotId: string typebot: TypebotInSession
result: ResultInSession answersInSession: AnswerInSessionState[]
} & Pick<SendEmailOptions, 'isCustomBody' | 'isBodyCode' | 'body'>): Promise< } & Pick<SendEmailOptions, 'isCustomBody' | 'isBodyCode' | 'body'>): Promise<
{ html?: string; text?: string } | undefined { html?: string; text?: string } | undefined
> => { > => {
@@ -219,11 +219,10 @@ const getEmailBody = async ({
html: isBodyCode ? body : undefined, html: isBodyCode ? body : undefined,
text: !isBodyCode ? body : undefined, text: !isBodyCode ? body : undefined,
} }
const typebot = (await prisma.publicTypebot.findUnique({ const answers = parseAnswers({
where: { typebotId }, variables: getDefinedVariables(typebot.variables),
})) as unknown as PublicTypebot answers: answersInSession,
if (!typebot) return })
const answers = parseAnswers(typebot, [])(result)
return { return {
html: render( html: render(
<DefaultBotNotificationEmail <DefaultBotNotificationEmail

View File

@@ -6,22 +6,19 @@ import {
PabblyConnectBlock, PabblyConnectBlock,
SessionState, SessionState,
Webhook, Webhook,
Typebot,
Variable, Variable,
WebhookResponse, WebhookResponse,
WebhookOptions, WebhookOptions,
defaultWebhookAttributes, defaultWebhookAttributes,
PublicTypebot,
KeyValue, KeyValue,
ReplyLog, ReplyLog,
ResultInSession,
ExecutableWebhook, ExecutableWebhook,
AnswerInSessionState,
} from '@typebot.io/schemas' } from '@typebot.io/schemas'
import { stringify } from 'qs' import { stringify } from 'qs'
import { omit } from '@typebot.io/lib' import { omit } from '@typebot.io/lib'
import { parseAnswers } from '@typebot.io/lib/results' import { getDefinedVariables, parseAnswers } from '@typebot.io/lib/results'
import got, { Method, HTTPError, OptionsInit } from 'got' import got, { Method, HTTPError, OptionsInit } from 'got'
import { parseSampleResult } from './parseSampleResult'
import { ExecuteIntegrationResponse } from '@/features/chat/types' import { ExecuteIntegrationResponse } from '@/features/chat/types'
import { parseVariables } from '@/features/variables/parseVariables' import { parseVariables } from '@/features/variables/parseVariables'
import { resumeWebhookExecution } from './resumeWebhookExecution' import { resumeWebhookExecution } from './resumeWebhookExecution'
@@ -36,7 +33,6 @@ export const executeWebhookBlock = async (
state: SessionState, state: SessionState,
block: WebhookBlock | ZapierBlock | MakeComBlock | PabblyConnectBlock block: WebhookBlock | ZapierBlock | MakeComBlock | PabblyConnectBlock
): Promise<ExecuteIntegrationResponse> => { ): Promise<ExecuteIntegrationResponse> => {
const { typebot, result } = state
const logs: ReplyLog[] = [] const logs: ReplyLog[] = []
const webhook = const webhook =
block.options.webhook ?? block.options.webhook ??
@@ -52,9 +48,8 @@ export const executeWebhookBlock = async (
} }
const preparedWebhook = prepareWebhookAttributes(webhook, block.options) const preparedWebhook = prepareWebhookAttributes(webhook, block.options)
const parsedWebhook = await parseWebhookAttributes( const parsedWebhook = await parseWebhookAttributes(
typebot, state,
block.groupId, state.typebotsQueue[0].answers
result
)(preparedWebhook) )(preparedWebhook)
if (!parsedWebhook) { if (!parsedWebhook) {
logs.push({ logs.push({
@@ -97,14 +92,10 @@ const prepareWebhookAttributes = (
const checkIfBodyIsAVariable = (body: string) => /^{{.+}}$/.test(body) const checkIfBodyIsAVariable = (body: string) => /^{{.+}}$/.test(body)
const parseWebhookAttributes = const parseWebhookAttributes =
( (state: SessionState, answers: AnswerInSessionState[]) =>
typebot: SessionState['typebot'],
groupId: string,
result: ResultInSession
) =>
async (webhook: Webhook): Promise<ParsedWebhook | undefined> => { async (webhook: Webhook): Promise<ParsedWebhook | undefined> => {
if (!webhook.url || !webhook.method) return if (!webhook.url || !webhook.method) return
const { variables } = typebot const { typebot } = state.typebotsQueue[0]
const basicAuth: { username?: string; password?: string } = {} const basicAuth: { username?: string; password?: string } = {}
const basicAuthHeaderIdx = webhook.headers.findIndex( const basicAuthHeaderIdx = webhook.headers.findIndex(
(h) => (h) =>
@@ -121,32 +112,29 @@ const parseWebhookAttributes =
basicAuth.password = password basicAuth.password = password
webhook.headers.splice(basicAuthHeaderIdx, 1) webhook.headers.splice(basicAuthHeaderIdx, 1)
} }
const headers = convertKeyValueTableToObject(webhook.headers, variables) as const headers = convertKeyValueTableToObject(
| ExecutableWebhook['headers'] webhook.headers,
| undefined typebot.variables
) as ExecutableWebhook['headers'] | undefined
const queryParams = stringify( const queryParams = stringify(
convertKeyValueTableToObject(webhook.queryParams, variables) convertKeyValueTableToObject(webhook.queryParams, typebot.variables)
) )
const bodyContent = await getBodyContent( const bodyContent = await getBodyContent({
typebot,
[]
)({
body: webhook.body, body: webhook.body,
result, answers,
groupId, variables: typebot.variables,
variables,
}) })
const { data: body, isJson } = const { data: body, isJson } =
bodyContent && webhook.method !== HttpMethod.GET bodyContent && webhook.method !== HttpMethod.GET
? safeJsonParse( ? safeJsonParse(
parseVariables(variables, { parseVariables(typebot.variables, {
escapeForJson: !checkIfBodyIsAVariable(bodyContent), escapeForJson: !checkIfBodyIsAVariable(bodyContent),
})(bodyContent) })(bodyContent)
) )
: { data: undefined, isJson: false } : { data: undefined, isJson: false }
return { return {
url: parseVariables(variables)( url: parseVariables(typebot.variables)(
webhook.url + (queryParams !== '' ? `?${queryParams}` : '') webhook.url + (queryParams !== '' ? `?${queryParams}` : '')
), ),
basicAuth, basicAuth,
@@ -229,34 +217,25 @@ export const executeWebhook = async (
} }
} }
const getBodyContent = const getBodyContent = async ({
( body,
typebot: Pick<Typebot | PublicTypebot, 'groups' | 'variables' | 'edges'>, answers,
linkedTypebots: (Typebot | PublicTypebot)[] variables,
) => }: {
async ({ body?: string | null
body, answers: AnswerInSessionState[]
result, variables: Variable[]
groupId, }): Promise<string | undefined> => {
variables, if (!body) return
}: { return body === '{{state}}'
body?: string | null ? JSON.stringify(
result?: ResultInSession parseAnswers({
groupId: string answers,
variables: Variable[] variables: getDefinedVariables(variables),
}): Promise<string | undefined> => { })
if (!body) return )
return body === '{{state}}' : body
? JSON.stringify( }
result
? parseAnswers(typebot, linkedTypebots)(result)
: await parseSampleResult(typebot, linkedTypebots)(
groupId,
variables
)
)
: body
}
const convertKeyValueTableToObject = ( const convertKeyValueTableToObject = (
keyValues: KeyValue[] | undefined, keyValues: KeyValue[] | undefined,

View File

@@ -5,11 +5,12 @@ import { byId } from '@typebot.io/lib'
import { import {
MakeComBlock, MakeComBlock,
PabblyConnectBlock, PabblyConnectBlock,
ReplyLog,
VariableWithUnknowValue, VariableWithUnknowValue,
WebhookBlock, WebhookBlock,
ZapierBlock, ZapierBlock,
} from '@typebot.io/schemas' } from '@typebot.io/schemas'
import { ReplyLog, SessionState } from '@typebot.io/schemas/features/chat' import { SessionState } from '@typebot.io/schemas/features/chat/sessionState'
type Props = { type Props = {
state: SessionState state: SessionState
@@ -27,7 +28,7 @@ export const resumeWebhookExecution = ({
logs = [], logs = [],
response, response,
}: Props): ExecuteIntegrationResponse => { }: Props): ExecuteIntegrationResponse => {
const { typebot } = state const { typebot } = state.typebotsQueue[0]
const status = response.statusCode.toString() const status = response.statusCode.toString()
const isError = status.startsWith('4') || status.startsWith('5') const isError = status.startsWith('4') || status.startsWith('5')

View File

@@ -3,9 +3,10 @@ import { ExecuteLogicResponse } from '@/features/chat/types'
import { executeCondition } from './executeCondition' import { executeCondition } from './executeCondition'
export const executeConditionBlock = ( export const executeConditionBlock = (
{ typebot: { variables } }: SessionState, state: SessionState,
block: ConditionBlock block: ConditionBlock
): ExecuteLogicResponse => { ): ExecuteLogicResponse => {
const { variables } = state.typebotsQueue[0].typebot
const passedCondition = block.items.find((item) => const passedCondition = block.items.find((item) =>
executeCondition(variables)(item.content) executeCondition(variables)(item.content)
) )

View File

@@ -11,9 +11,8 @@ export const executeJumpBlock = (
state: SessionState, state: SessionState,
{ groupId, blockId }: JumpBlock['options'] { groupId, blockId }: JumpBlock['options']
): ExecuteLogicResponse => { ): ExecuteLogicResponse => {
const groupToJumpTo = state.typebot.groups.find( const { typebot } = state.typebotsQueue[0]
(group) => group.id === groupId const groupToJumpTo = typebot.groups.find((group) => group.id === groupId)
)
const blockToJumpTo = const blockToJumpTo =
groupToJumpTo?.blocks.find((block) => block.id === blockId) ?? groupToJumpTo?.blocks.find((block) => block.id === blockId) ??
groupToJumpTo?.blocks[0] groupToJumpTo?.blocks[0]

View File

@@ -4,9 +4,10 @@ import { sanitizeUrl } from '@typebot.io/lib'
import { ExecuteLogicResponse } from '@/features/chat/types' import { ExecuteLogicResponse } from '@/features/chat/types'
export const executeRedirect = ( export const executeRedirect = (
{ typebot: { variables } }: SessionState, state: SessionState,
block: RedirectBlock block: RedirectBlock
): ExecuteLogicResponse => { ): ExecuteLogicResponse => {
const { variables } = state.typebotsQueue[0].typebot
if (!block.options?.url) return { outgoingEdgeId: block.outgoingEdgeId } if (!block.options?.url) return { outgoingEdgeId: block.outgoingEdgeId }
const formattedUrl = sanitizeUrl(parseVariables(variables)(block.options.url)) const formattedUrl = sanitizeUrl(parseVariables(variables)(block.options.url))
return { return {

View File

@@ -5,9 +5,10 @@ import { parseVariables } from '@/features/variables/parseVariables'
import { ScriptBlock, SessionState, Variable } from '@typebot.io/schemas' import { ScriptBlock, SessionState, Variable } from '@typebot.io/schemas'
export const executeScript = ( export const executeScript = (
{ typebot: { variables } }: SessionState, state: SessionState,
block: ScriptBlock block: ScriptBlock
): ExecuteLogicResponse => { ): ExecuteLogicResponse => {
const { variables } = state.typebotsQueue[0].typebot
if (!block.options.content) return { outgoingEdgeId: block.outgoingEdgeId } if (!block.options.content) return { outgoingEdgeId: block.outgoingEdgeId }
const scriptToExecute = parseScriptToExecuteClientSideAction( const scriptToExecute = parseScriptToExecuteClientSideAction(

View File

@@ -10,14 +10,14 @@ export const executeSetVariable = (
state: SessionState, state: SessionState,
block: SetVariableBlock block: SetVariableBlock
): ExecuteLogicResponse => { ): ExecuteLogicResponse => {
const { variables } = state.typebot const { variables } = state.typebotsQueue[0].typebot
if (!block.options?.variableId) if (!block.options?.variableId)
return { return {
outgoingEdgeId: block.outgoingEdgeId, outgoingEdgeId: block.outgoingEdgeId,
} }
const expressionToEvaluate = getExpressionToEvaluate(state.result.id)( const expressionToEvaluate = getExpressionToEvaluate(
block.options state.typebotsQueue[0].resultId
) )(block.options)
const isCustomValue = !block.options.type || block.options.type === 'Custom' const isCustomValue = !block.options.type || block.options.type === 'Custom'
if ( if (
expressionToEvaluate && expressionToEvaluate &&
@@ -25,7 +25,7 @@ export const executeSetVariable = (
block.options.type === 'Moment of the day') block.options.type === 'Moment of the day')
) { ) {
const scriptToExecute = parseScriptToExecuteClientSideAction( const scriptToExecute = parseScriptToExecuteClientSideAction(
state.typebot.variables, variables,
expressionToEvaluate expressionToEvaluate
) )
return { return {

View File

@@ -6,19 +6,24 @@ import prisma from '@/lib/prisma'
import { import {
TypebotLinkBlock, TypebotLinkBlock,
SessionState, SessionState,
TypebotInSession,
Variable, Variable,
ReplyLog, ReplyLog,
Typebot,
VariableWithValue,
Edge,
} from '@typebot.io/schemas' } from '@typebot.io/schemas'
import { byId } from '@typebot.io/lib'
import { ExecuteLogicResponse } from '@/features/chat/types' import { ExecuteLogicResponse } from '@/features/chat/types'
import { createId } from '@paralleldrive/cuid2'
import { isDefined, isNotDefined } from '@typebot.io/lib/utils'
import { createResultIfNotExist } from '@/features/chat/queries/createResultIfNotExist'
export const executeTypebotLink = async ( export const executeTypebotLink = async (
state: SessionState, state: SessionState,
block: TypebotLinkBlock block: TypebotLinkBlock
): Promise<ExecuteLogicResponse> => { ): Promise<ExecuteLogicResponse> => {
const logs: ReplyLog[] = [] const logs: ReplyLog[] = []
if (!block.options.typebotId) { const typebotId = block.options.typebotId
if (!typebotId) {
logs.push({ logs.push({
status: 'error', status: 'error',
description: `Failed to link typebot`, description: `Failed to link typebot`,
@@ -26,7 +31,7 @@ export const executeTypebotLink = async (
}) })
return { outgoingEdgeId: block.outgoingEdgeId, logs } return { outgoingEdgeId: block.outgoingEdgeId, logs }
} }
const linkedTypebot = await getLinkedTypebot(state, block.options.typebotId) const linkedTypebot = await fetchTypebot(state, typebotId)
if (!linkedTypebot) { if (!linkedTypebot) {
logs.push({ logs.push({
status: 'error', status: 'error',
@@ -35,12 +40,17 @@ export const executeTypebotLink = async (
}) })
return { outgoingEdgeId: block.outgoingEdgeId, logs } return { outgoingEdgeId: block.outgoingEdgeId, logs }
} }
let newSessionState = addLinkedTypebotToState(state, block, linkedTypebot) let newSessionState = await addLinkedTypebotToState(
state,
block,
linkedTypebot
)
const nextGroupId = const nextGroupId =
block.options.groupId ?? block.options.groupId ??
linkedTypebot.groups.find((b) => b.blocks.some((s) => s.type === 'start')) linkedTypebot.groups.find((group) =>
?.id group.blocks.some((block) => block.type === 'start')
)?.id
if (!nextGroupId) { if (!nextGroupId) {
logs.push({ logs.push({
status: 'error', status: 'error',
@@ -60,76 +70,123 @@ export const executeTypebotLink = async (
} }
} }
const addLinkedTypebotToState = ( const addLinkedTypebotToState = async (
state: SessionState, state: SessionState,
block: TypebotLinkBlock, block: TypebotLinkBlock,
linkedTypebot: TypebotInSession linkedTypebot: Pick<Typebot, 'id' | 'edges' | 'groups' | 'variables'>
): SessionState => { ): Promise<SessionState> => {
const incomingVariables = fillVariablesWithExistingValues( const currentTypebotInQueue = state.typebotsQueue[0]
linkedTypebot.variables, const isPreview = isNotDefined(currentTypebotInQueue.resultId)
state.typebot.variables
) const resumeEdge = createResumeEdgeIfNecessary(state, block)
const currentTypebotWithResumeEdge = resumeEdge
? {
...currentTypebotInQueue,
typebot: {
...currentTypebotInQueue.typebot,
edges: [...currentTypebotInQueue.typebot.edges, resumeEdge],
},
}
: currentTypebotInQueue
const shouldMergeResults =
block.options.mergeResults !== false ||
currentTypebotInQueue.typebot.id === linkedTypebot.id ||
block.options.typebotId === 'current'
if (
currentTypebotInQueue.resultId &&
currentTypebotInQueue.answers.length === 0 &&
shouldMergeResults
) {
await createResultIfNotExist({
resultId: currentTypebotInQueue.resultId,
typebot: currentTypebotInQueue.typebot,
hasStarted: false,
isCompleted: false,
})
}
return { return {
...state, ...state,
typebot: { typebotsQueue: [
...state.typebot, {
groups: [...state.typebot.groups, ...linkedTypebot.groups], typebot: {
variables: [...state.typebot.variables, ...incomingVariables], ...linkedTypebot,
edges: [...state.typebot.edges, ...linkedTypebot.edges], variables: fillVariablesWithExistingValues(
linkedTypebot.variables,
currentTypebotInQueue.typebot.variables
),
},
resultId: isPreview
? undefined
: shouldMergeResults
? currentTypebotInQueue.resultId
: createId(),
edgeIdToTriggerWhenDone: block.outgoingEdgeId ?? resumeEdge?.id,
answers: shouldMergeResults ? currentTypebotInQueue.answers : [],
isMergingWithParent: shouldMergeResults,
},
currentTypebotWithResumeEdge,
...state.typebotsQueue.slice(1),
],
}
}
const createResumeEdgeIfNecessary = (
state: SessionState,
block: TypebotLinkBlock
): Edge | undefined => {
const currentTypebotInQueue = state.typebotsQueue[0]
const blockId = block.id
if (block.outgoingEdgeId) return
const currentGroup = currentTypebotInQueue.typebot.groups.find((group) =>
group.blocks.some((block) => block.id === blockId)
)
if (!currentGroup) return
const currentBlockIndex = currentGroup.blocks.findIndex(
(block) => block.id === blockId
)
const nextBlockInGroup =
currentBlockIndex === -1
? undefined
: currentGroup.blocks[currentBlockIndex + 1]
if (!nextBlockInGroup) return
return {
id: createId(),
from: {
groupId: '',
blockId: '',
}, },
linkedTypebots: { to: {
typebots: [ groupId: nextBlockInGroup.groupId,
...state.linkedTypebots.typebots.filter( blockId: nextBlockInGroup.id,
(existingTypebots) => existingTypebots.id !== linkedTypebot.id
),
],
queue: block.outgoingEdgeId
? [
...state.linkedTypebots.queue,
{ edgeId: block.outgoingEdgeId, typebotId: state.currentTypebotId },
]
: state.linkedTypebots.queue,
}, },
currentTypebotId: linkedTypebot.id,
} }
} }
const fillVariablesWithExistingValues = ( const fillVariablesWithExistingValues = (
variables: Variable[], variables: Variable[],
variablesWithValues: Variable[] variablesWithValues: Variable[]
): Variable[] => ): VariableWithValue[] =>
variables.map((variable) => { variables
const matchedVariable = variablesWithValues.find( .map((variable) => {
(variableWithValue) => variableWithValue.name === variable.name const matchedVariable = variablesWithValues.find(
) (variableWithValue) => variableWithValue.name === variable.name
)
return { return {
...variable, ...variable,
value: matchedVariable?.value ?? variable.value, value: matchedVariable?.value,
} }
}) })
.filter((variable) => isDefined(variable.value)) as VariableWithValue[]
const getLinkedTypebot = async ( const fetchTypebot = async (state: SessionState, typebotId: string) => {
state: SessionState, const { typebot: typebotInState, resultId } = state.typebotsQueue[0]
typebotId: string const isPreview = !resultId
): Promise<TypebotInSession | null> => { if (typebotId === 'current') return typebotInState
const { typebot, result } = state
const isPreview = !result.id
if (typebotId === 'current') return typebot
const availableTypebots =
'linkedTypebots' in state
? [typebot, ...state.linkedTypebots.typebots]
: [typebot]
const linkedTypebot =
availableTypebots.find(byId(typebotId)) ??
(await fetchTypebot(isPreview, typebotId))
return linkedTypebot
}
const fetchTypebot = async (
isPreview: boolean,
typebotId: string
): Promise<TypebotInSession | null> => {
if (isPreview) { if (isPreview) {
const typebot = await prisma.typebot.findUnique({ const typebot = await prisma.typebot.findUnique({
where: { id: typebotId }, where: { id: typebotId },
@@ -140,7 +197,7 @@ const fetchTypebot = async (
variables: true, variables: true,
}, },
}) })
return typebot as TypebotInSession return typebot as Pick<Typebot, 'id' | 'edges' | 'groups' | 'variables'>
} }
const typebot = await prisma.publicTypebot.findUnique({ const typebot = await prisma.publicTypebot.findUnique({
where: { typebotId }, where: { typebotId },
@@ -155,5 +212,5 @@ const fetchTypebot = async (
return { return {
...typebot, ...typebot,
id: typebotId, id: typebotId,
} as TypebotInSession } as Pick<Typebot, 'id' | 'edges' | 'groups' | 'variables'>
} }

View File

@@ -3,6 +3,7 @@ import test, { expect } from '@playwright/test'
import { importTypebotInDatabase } from '@typebot.io/lib/playwright/databaseActions' import { importTypebotInDatabase } from '@typebot.io/lib/playwright/databaseActions'
const typebotId = 'cl0ibhi7s0018n21aarlmg0cm' const typebotId = 'cl0ibhi7s0018n21aarlmg0cm'
const typebotWithMergeDisabledId = 'cl0ibhi7s0018n21aarlag0cm'
const linkedTypebotId = 'cl0ibhv8d0130n21aw8doxhj5' const linkedTypebotId = 'cl0ibhv8d0130n21aw8doxhj5'
test.beforeAll(async () => { test.beforeAll(async () => {
@@ -11,6 +12,13 @@ test.beforeAll(async () => {
getTestAsset('typebots/linkTypebots/1.json'), getTestAsset('typebots/linkTypebots/1.json'),
{ id: typebotId, publicId: `${typebotId}-public` } { id: typebotId, publicId: `${typebotId}-public` }
) )
await importTypebotInDatabase(
getTestAsset('typebots/linkTypebots/1-merge-disabled.json'),
{
id: typebotWithMergeDisabledId,
publicId: `${typebotWithMergeDisabledId}-public`,
}
)
await importTypebotInDatabase( await importTypebotInDatabase(
getTestAsset('typebots/linkTypebots/2.json'), getTestAsset('typebots/linkTypebots/2.json'),
{ id: linkedTypebotId, publicId: `${linkedTypebotId}-public` } { id: linkedTypebotId, publicId: `${linkedTypebotId}-public` }
@@ -28,3 +36,21 @@ test('should work as expected', async ({ page }) => {
await page.goto(`${process.env.NEXTAUTH_URL}/typebots/${typebotId}/results`) await page.goto(`${process.env.NEXTAUTH_URL}/typebots/${typebotId}/results`)
await expect(page.locator('text=Hello there!')).toBeVisible() await expect(page.locator('text=Hello there!')).toBeVisible()
}) })
test.describe('Merge disabled', () => {
test('should work as expected', async ({ page }) => {
await page.goto(`/${typebotWithMergeDisabledId}-public`)
await page.locator('input').fill('Hello there!')
await page.locator('input').press('Enter')
await expect(page.getByText('Cheers!')).toBeVisible()
await page.goto(
`${process.env.NEXTAUTH_URL}/typebots/${typebotWithMergeDisabledId}/results`
)
await expect(page.locator('text=Submitted at')).toBeVisible()
await expect(page.locator('text=Hello there!')).toBeHidden()
await page.goto(
`${process.env.NEXTAUTH_URL}/typebots/${linkedTypebotId}/results`
)
await expect(page.locator('text=Hello there!')).toBeVisible()
})
})

View File

@@ -3,9 +3,10 @@ import { parseVariables } from '@/features/variables/parseVariables'
import { SessionState, WaitBlock } from '@typebot.io/schemas' import { SessionState, WaitBlock } from '@typebot.io/schemas'
export const executeWait = ( export const executeWait = (
{ typebot: { variables } }: SessionState, state: SessionState,
block: WaitBlock block: WaitBlock
): ExecuteLogicResponse => { ): ExecuteLogicResponse => {
const { variables } = state.typebotsQueue[0].typebot
if (!block.options.secondsToWaitFor) if (!block.options.secondsToWaitFor)
return { outgoingEdgeId: block.outgoingEdgeId } return { outgoingEdgeId: block.outgoingEdgeId }
const parsedSecondsToWaitFor = safeParseInt( const parsedSecondsToWaitFor = safeParseInt(

View File

@@ -7,7 +7,6 @@ import {
IntegrationBlockType, IntegrationBlockType,
PixelBlock, PixelBlock,
ReplyLog, ReplyLog,
ResultInSession,
sendMessageInputSchema, sendMessageInputSchema,
SessionState, SessionState,
StartParams, StartParams,
@@ -148,22 +147,19 @@ const startSession = async (
: prefilledVariables : prefilledVariables
const initialState: SessionState = { const initialState: SessionState = {
typebot: { version: '2',
id: typebot.id, typebotsQueue: [
groups: typebot.groups, {
edges: typebot.edges, resultId: result?.id,
variables: startVariables, typebot: {
}, id: typebot.id,
linkedTypebots: { groups: typebot.groups,
typebots: [], edges: typebot.edges,
queue: [], variables: startVariables,
}, },
result: { answers: [],
id: result?.id, },
variables: result?.variables ?? [], ],
answers: result?.answers ?? [],
},
currentTypebotId: typebot.id,
dynamicTheme: parseDynamicThemeInState(typebot.theme), dynamicTheme: parseDynamicThemeInState(typebot.theme),
isStreamEnabled: startParams.isStreamEnabled, isStreamEnabled: startParams.isStreamEnabled,
} }
@@ -212,12 +208,12 @@ const startSession = async (
startClientSideAction.length > 0 ? startClientSideAction : undefined, startClientSideAction.length > 0 ? startClientSideAction : undefined,
typebot: { typebot: {
id: typebot.id, id: typebot.id,
settings: deepParseVariables(newSessionState.typebot.variables)( settings: deepParseVariables(
typebot.settings newSessionState.typebotsQueue[0].typebot.variables
), )(typebot.settings),
theme: deepParseVariables(newSessionState.typebot.variables)( theme: deepParseVariables(
typebot.theme newSessionState.typebotsQueue[0].typebot.variables
), )(typebot.theme),
}, },
dynamicTheme: parseDynamicThemeReply(newSessionState), dynamicTheme: parseDynamicThemeReply(newSessionState),
logs: startLogs.length > 0 ? startLogs : undefined, logs: startLogs.length > 0 ? startLogs : undefined,
@@ -239,12 +235,12 @@ const startSession = async (
sessionId: session.id, sessionId: session.id,
typebot: { typebot: {
id: typebot.id, id: typebot.id,
settings: deepParseVariables(newSessionState.typebot.variables)( settings: deepParseVariables(
typebot.settings newSessionState.typebotsQueue[0].typebot.variables
), )(typebot.settings),
theme: deepParseVariables(newSessionState.typebot.variables)( theme: deepParseVariables(
typebot.theme newSessionState.typebotsQueue[0].typebot.variables
), )(typebot.theme),
}, },
messages, messages,
input, input,
@@ -319,7 +315,7 @@ const getResult = async ({
if (isPreview) return if (isPreview) return
const existingResult = const existingResult =
resultId && isRememberUserEnabled resultId && isRememberUserEnabled
? ((await findResult({ id: resultId })) as ResultInSession) ? await findResult({ id: resultId })
: undefined : undefined
const prefilledVariableWithValue = prefilledVariables.filter( const prefilledVariableWithValue = prefilledVariables.filter(
@@ -341,7 +337,7 @@ const getResult = async ({
return { return {
id: existingResult?.id ?? createId(), id: existingResult?.id ?? createId(),
variables: updatedResult.variables, variables: updatedResult.variables,
answers: existingResult?.answers, answers: existingResult?.answers ?? [],
} }
} }
@@ -369,10 +365,10 @@ const parseDynamicThemeReply = (
): ChatReply['dynamicTheme'] => { ): ChatReply['dynamicTheme'] => {
if (!state?.dynamicTheme) return if (!state?.dynamicTheme) return
return { return {
hostAvatarUrl: parseVariables(state?.typebot.variables)( hostAvatarUrl: parseVariables(state.typebotsQueue[0].typebot.variables)(
state.dynamicTheme.hostAvatarUrl state.dynamicTheme.hostAvatarUrl
), ),
guestAvatarUrl: parseVariables(state?.typebot.variables)( guestAvatarUrl: parseVariables(state.typebotsQueue[0].typebot.variables)(
state.dynamicTheme.guestAvatarUrl state.dynamicTheme.guestAvatarUrl
), ),
} }

View File

@@ -3,7 +3,12 @@ import { TRPCError } from '@trpc/server'
import { z } from 'zod' import { z } from 'zod'
import { getSession } from '../queries/getSession' import { getSession } from '../queries/getSession'
import prisma from '@/lib/prisma' import prisma from '@/lib/prisma'
import { PublicTypebot, SessionState, Typebot } from '@typebot.io/schemas' import {
PublicTypebot,
SessionState,
Typebot,
Variable,
} from '@typebot.io/schemas'
export const updateTypebotInSession = publicProcedure export const updateTypebotInSession = publicProcedure
.meta({ .meta({
@@ -32,7 +37,7 @@ export const updateTypebotInSession = publicProcedure
const publicTypebot = (await prisma.publicTypebot.findFirst({ const publicTypebot = (await prisma.publicTypebot.findFirst({
where: { where: {
typebot: { typebot: {
id: session.state.typebot.id, id: session.state.typebotsQueue[0].typebot.id,
OR: [ OR: [
{ {
workspace: { workspace: {
@@ -74,21 +79,28 @@ const updateSessionState = (
newTypebot: Pick<PublicTypebot, 'edges' | 'variables' | 'groups'> newTypebot: Pick<PublicTypebot, 'edges' | 'variables' | 'groups'>
): SessionState => ({ ): SessionState => ({
...currentState, ...currentState,
typebot: { typebotsQueue: currentState.typebotsQueue.map((typebotInQueue, index) =>
...currentState.typebot, index === 0
edges: newTypebot.edges, ? {
variables: updateVariablesInSession( ...typebotInQueue,
currentState.typebot.variables, typebot: {
newTypebot.variables ...typebotInQueue.typebot,
), edges: newTypebot.edges,
groups: newTypebot.groups, groups: newTypebot.groups,
}, variables: updateVariablesInSession(
typebotInQueue.typebot.variables,
newTypebot.variables
),
},
}
: typebotInQueue
),
}) })
const updateVariablesInSession = ( const updateVariablesInSession = (
currentVariables: SessionState['typebot']['variables'], currentVariables: Variable[],
newVariables: Typebot['variables'] newVariables: Typebot['variables']
): SessionState['typebot']['variables'] => [ ): Variable[] => [
...currentVariables, ...currentVariables,
...newVariables.filter( ...newVariables.filter(
(newVariable) => (newVariable) =>

View File

@@ -6,10 +6,17 @@ export const addEdgeToTypebot = (
edge: Edge edge: Edge
): SessionState => ({ ): SessionState => ({
...state, ...state,
typebot: { typebotsQueue: state.typebotsQueue.map((typebot, index) =>
...state.typebot, index === 0
edges: [...state.typebot.edges, edge], ? {
}, ...typebot,
typebot: {
...typebot.typebot,
edges: [...typebot.typebot.edges, edge],
},
}
: typebot
),
}) })
export const createPortalEdge = ({ to }: Pick<Edge, 'to'>) => ({ export const createPortalEdge = ({ to }: Pick<Edge, 'to'>) => ({

View File

@@ -1,5 +1,6 @@
import { TRPCError } from '@trpc/server' import { TRPCError } from '@trpc/server'
import { import {
AnswerInSessionState,
Block, Block,
BlockType, BlockType,
BubbleBlockType, BubbleBlockType,
@@ -8,7 +9,6 @@ import {
InputBlockType, InputBlockType,
IntegrationBlockType, IntegrationBlockType,
LogicBlockType, LogicBlockType,
ResultInSession,
SessionState, SessionState,
SetVariableBlock, SetVariableBlock,
WebhookBlock, WebhookBlock,
@@ -35,7 +35,7 @@ export const continueBotFlow =
reply?: string reply?: string
): Promise<ChatReply & { newSessionState: SessionState }> => { ): Promise<ChatReply & { newSessionState: SessionState }> => {
let newSessionState = { ...state } let newSessionState = { ...state }
const group = state.typebot.groups.find( const group = state.typebotsQueue[0].typebot.groups.find(
(group) => group.id === state.currentBlock?.groupId (group) => group.id === state.currentBlock?.groupId
) )
const blockIndex = const blockIndex =
@@ -52,7 +52,7 @@ export const continueBotFlow =
}) })
if (block.type === LogicBlockType.SET_VARIABLE) { if (block.type === LogicBlockType.SET_VARIABLE) {
const existingVariable = state.typebot.variables.find( const existingVariable = state.typebotsQueue[0].typebot.variables.find(
byId(block.options.variableId) byId(block.options.variableId)
) )
if (existingVariable && reply) { if (existingVariable && reply) {
@@ -103,7 +103,8 @@ export const continueBotFlow =
formattedReply formattedReply
) )
const itemId = nextEdgeId const itemId = nextEdgeId
? state.typebot.edges.find(byId(nextEdgeId))?.from.itemId ? newSessionState.typebotsQueue[0].typebot.edges.find(byId(nextEdgeId))
?.from.itemId
: undefined : undefined
newSessionState = await processAndSaveAnswer( newSessionState = await processAndSaveAnswer(
state, state,
@@ -128,7 +129,7 @@ export const continueBotFlow =
} }
} }
if (!nextEdgeId && state.linkedTypebots.queue.length === 0) if (!nextEdgeId && state.typebotsQueue.length === 1)
return { return {
messages: [], messages: [],
newSessionState, newSessionState,
@@ -138,7 +139,9 @@ export const continueBotFlow =
const nextGroup = getNextGroup(newSessionState)(nextEdgeId) const nextGroup = getNextGroup(newSessionState)(nextEdgeId)
if (!nextGroup) newSessionState = nextGroup.newSessionState
if (!nextGroup.group)
return { return {
messages: [], messages: [],
newSessionState, newSessionState,
@@ -168,7 +171,7 @@ const saveVariableValueIfAny =
(state: SessionState, block: InputBlock) => (state: SessionState, block: InputBlock) =>
(reply: string): SessionState => { (reply: string): SessionState => {
if (!block.options.variableId) return state if (!block.options.variableId) return state
const foundVariable = state.typebot.variables.find( const foundVariable = state.typebotsQueue[0].typebot.variables.find(
(variable) => variable.id === block.options.variableId (variable) => variable.id === block.options.variableId
) )
if (!foundVariable) return state if (!foundVariable) return state
@@ -235,34 +238,47 @@ const saveAnswer =
itemId, itemId,
}) })
const key = block.options.variableId
? state.typebotsQueue[0].typebot.variables.find(
(variable) => variable.id === block.options.variableId
)?.name
: state.typebotsQueue[0].typebot.groups.find((group) =>
group.blocks.find((blockInGroup) => blockInGroup.id === block.id)
)?.title
return setNewAnswerInState(state)({ return setNewAnswerInState(state)({
blockId: block.id, key: key ?? block.id,
variableId: block.options.variableId ?? null, value: reply,
content: reply,
}) })
} }
const setNewAnswerInState = const setNewAnswerInState =
(state: SessionState) => (newAnswer: ResultInSession['answers'][number]) => { (state: SessionState) => (newAnswer: AnswerInSessionState) => {
const newAnswers = state.result.answers const answers = state.typebotsQueue[0].answers
.filter((answer) => answer.blockId !== newAnswer.blockId) const newAnswers = answers
.filter((answer) => answer.key !== newAnswer.key)
.concat(newAnswer) .concat(newAnswer)
return { return {
...state, ...state,
result: { typebotsQueue: state.typebotsQueue.map((typebot, index) =>
...state.result, index === 0
answers: newAnswers, ? {
}, ...typebot,
answers: newAnswers,
}
: typebot
),
} satisfies SessionState } satisfies SessionState
} }
const getOutgoingEdgeId = const getOutgoingEdgeId =
({ typebot: { variables } }: Pick<SessionState, 'typebot'>) => (state: Pick<SessionState, 'typebotsQueue'>) =>
( (
block: InputBlock | SetVariableBlock | OpenAIBlock | WebhookBlock, block: InputBlock | SetVariableBlock | OpenAIBlock | WebhookBlock,
reply: string | undefined reply: string | undefined
) => { ) => {
const variables = state.typebotsQueue[0].typebot.variables
if ( if (
block.type === InputBlockType.CHOICE && block.type === InputBlockType.CHOICE &&
!block.options.isMultipleChoice && !block.options.isMultipleChoice &&

View File

@@ -7,6 +7,7 @@ import {
InputBlockType, InputBlockType,
RuntimeOptions, RuntimeOptions,
SessionState, SessionState,
Variable,
} from '@typebot.io/schemas' } from '@typebot.io/schemas'
import { import {
isBubbleBlock, isBubbleBlock,
@@ -47,7 +48,9 @@ export const executeGroup =
if (isBubbleBlock(block)) { if (isBubbleBlock(block)) {
messages.push( messages.push(
parseBubbleBlock(newSessionState.typebot.variables)(block) parseBubbleBlock(newSessionState.typebotsQueue[0].typebot.variables)(
block
)
) )
lastBubbleBlockId = block.id lastBubbleBlockId = block.id
continue continue
@@ -118,14 +121,14 @@ export const executeGroup =
} }
} }
if (!nextEdgeId) if (!nextEdgeId && state.typebotsQueue.length === 1)
return { messages, newSessionState, clientSideActions, logs } return { messages, newSessionState, clientSideActions, logs }
const nextGroup = getNextGroup(newSessionState)(nextEdgeId) const nextGroup = getNextGroup(newSessionState)(nextEdgeId ?? undefined)
if (nextGroup?.updatedContext) newSessionState = nextGroup.updatedContext newSessionState = nextGroup.newSessionState
if (!nextGroup) { if (!nextGroup.group) {
return { messages, newSessionState, clientSideActions, logs } return { messages, newSessionState, clientSideActions, logs }
} }
@@ -141,7 +144,7 @@ export const executeGroup =
} }
const computeRuntimeOptions = const computeRuntimeOptions =
(state: Pick<SessionState, 'result' | 'typebot'>) => (state: SessionState) =>
(block: InputBlock): Promise<RuntimeOptions> | undefined => { (block: InputBlock): Promise<RuntimeOptions> | undefined => {
switch (block.type) { switch (block.type) {
case InputBlockType.PAYMENT: { case InputBlockType.PAYMENT: {
@@ -151,7 +154,7 @@ const computeRuntimeOptions =
} }
const getPrefilledInputValue = const getPrefilledInputValue =
(variables: SessionState['typebot']['variables']) => (block: InputBlock) => { (variables: Variable[]) => (block: InputBlock) => {
const variableValue = variables.find( const variableValue = variables.find(
(variable) => (variable) =>
variable.id === block.options.variableId && isDefined(variable.value) variable.id === block.options.variableId && isDefined(variable.value)
@@ -161,7 +164,7 @@ const getPrefilledInputValue =
} }
const parseBubbleBlock = const parseBubbleBlock =
(variables: SessionState['typebot']['variables']) => (variables: Variable[]) =>
(block: BubbleBlock): ChatReply['messages'][0] => { (block: BubbleBlock): ChatReply['messages'][0] => {
switch (block.type) { switch (block.type) {
case BubbleBlockType.TEXT: case BubbleBlockType.TEXT:
@@ -197,15 +200,17 @@ const parseInput =
} }
case InputBlockType.PICTURE_CHOICE: { case InputBlockType.PICTURE_CHOICE: {
return injectVariableValuesInPictureChoiceBlock( return injectVariableValuesInPictureChoiceBlock(
state.typebot.variables state.typebotsQueue[0].typebot.variables
)(block) )(block)
} }
case InputBlockType.NUMBER: { case InputBlockType.NUMBER: {
const parsedBlock = deepParseVariables(state.typebot.variables)({ const parsedBlock = deepParseVariables(
state.typebotsQueue[0].typebot.variables
)({
...block, ...block,
prefilledValue: getPrefilledInputValue(state.typebot.variables)( prefilledValue: getPrefilledInputValue(
block state.typebotsQueue[0].typebot.variables
), )(block),
}) })
return { return {
...parsedBlock, ...parsedBlock,
@@ -224,12 +229,12 @@ const parseInput =
} }
} }
default: { default: {
return deepParseVariables(state.typebot.variables)({ return deepParseVariables(state.typebotsQueue[0].typebot.variables)({
...block, ...block,
runtimeOptions: await computeRuntimeOptions(state)(block), runtimeOptions: await computeRuntimeOptions(state)(block),
prefilledValue: getPrefilledInputValue(state.typebot.variables)( prefilledValue: getPrefilledInputValue(
block state.typebotsQueue[0].typebot.variables
), )(block),
}) })
} }
} }

View File

@@ -2,37 +2,81 @@ import { byId } from '@typebot.io/lib'
import { Group, SessionState } from '@typebot.io/schemas' import { Group, SessionState } from '@typebot.io/schemas'
export type NextGroup = { export type NextGroup = {
group: Group group?: Group
updatedContext?: SessionState newSessionState: SessionState
} }
export const getNextGroup = export const getNextGroup =
(state: SessionState) => (state: SessionState) =>
(edgeId?: string): NextGroup | null => { (edgeId?: string): NextGroup => {
const { typebot } = state const nextEdge = state.typebotsQueue[0].typebot.edges.find(byId(edgeId))
const nextEdge = typebot.edges.find(byId(edgeId))
if (!nextEdge) { if (!nextEdge) {
if (state.linkedTypebots.queue.length > 0) { if (state.typebotsQueue.length > 1) {
const nextEdgeId = state.linkedTypebots.queue[0].edgeId const nextEdgeId = state.typebotsQueue[0].edgeIdToTriggerWhenDone
const updatedContext = { const isMergingWithParent = state.typebotsQueue[0].isMergingWithParent
const newSessionState = {
...state, ...state,
linkedBotQueue: state.linkedTypebots.queue.slice(1), typebotsQueue: [
} {
const nextGroup = getNextGroup(updatedContext)(nextEdgeId) ...state.typebotsQueue[1],
if (!nextGroup) return null typebot: isMergingWithParent
? {
...state.typebotsQueue[1].typebot,
variables: state.typebotsQueue[1].typebot.variables.map(
(variable) => ({
...variable,
value: state.typebotsQueue[0].answers.find(
(answer) => answer.key === variable.name
)?.value,
})
),
}
: state.typebotsQueue[1].typebot,
answers: isMergingWithParent
? [
...state.typebotsQueue[1].answers.filter(
(incomingAnswer) =>
!state.typebotsQueue[0].answers.find(
(currentAnswer) =>
currentAnswer.key === incomingAnswer.key
)
),
...state.typebotsQueue[0].answers,
]
: state.typebotsQueue[1].answers,
},
...state.typebotsQueue.slice(2),
],
} satisfies SessionState
const nextGroup = getNextGroup(newSessionState)(nextEdgeId)
if (!nextGroup)
return {
newSessionState,
}
return { return {
...nextGroup, ...nextGroup,
updatedContext, newSessionState,
} }
} }
return null return {
newSessionState: state,
}
} }
const nextGroup = typebot.groups.find(byId(nextEdge.to.groupId)) const nextGroup = state.typebotsQueue[0].typebot.groups.find(
if (!nextGroup) return null byId(nextEdge.to.groupId)
)
if (!nextGroup)
return {
newSessionState: state,
}
const startBlockIndex = nextEdge.to.blockId const startBlockIndex = nextEdge.to.blockId
? nextGroup.blocks.findIndex(byId(nextEdge.to.blockId)) ? nextGroup.blocks.findIndex(byId(nextEdge.to.blockId))
: 0 : 0
return { return {
group: { ...nextGroup, blocks: nextGroup.blocks.slice(startBlockIndex) }, group: {
...nextGroup,
blocks: nextGroup.blocks.slice(startBlockIndex),
},
newSessionState: state,
} }
} }

View File

@@ -11,6 +11,7 @@ type Props = {
logs: ChatReply['logs'] logs: ChatReply['logs']
clientSideActions: ChatReply['clientSideActions'] clientSideActions: ChatReply['clientSideActions']
} }
export const saveStateToDatabase = async ({ export const saveStateToDatabase = async ({
session: { state, id }, session: { state, id },
input, input,
@@ -21,25 +22,30 @@ export const saveStateToDatabase = async ({
const session = id ? { state, id } : await createSession({ state }) const session = id ? { state, id } : await createSession({ state })
if (!state?.result?.id) return session const resultId = state.typebotsQueue[0].resultId
if (!resultId) return session
const containsSetVariableClientSideAction = clientSideActions?.some( const containsSetVariableClientSideAction = clientSideActions?.some(
(action) => 'setVariable' in action (action) => 'setVariable' in action
) )
const answers = state.typebotsQueue[0].answers
await upsertResult({ await upsertResult({
state, resultId,
typebot: state.typebotsQueue[0].typebot,
isCompleted: Boolean( isCompleted: Boolean(
!input && !input && !containsSetVariableClientSideAction && answers.length > 0
!containsSetVariableClientSideAction &&
state.result.answers.length > 0
), ),
hasStarted: answers.length > 0,
}) })
if (logs && logs.length > 0) if (logs && logs.length > 0)
await saveLogs( await saveLogs(
logs.map((log) => ({ logs.map((log) => ({
...log, ...log,
resultId: state.result.id as string, resultId,
details: formatLogDetails(log.details), details: formatLogDetails(log.details),
})) }))
) )

View File

@@ -8,7 +8,7 @@ export const startBotFlow = async (
startGroupId?: string startGroupId?: string
): Promise<ChatReply & { newSessionState: SessionState }> => { ): Promise<ChatReply & { newSessionState: SessionState }> => {
if (startGroupId) { if (startGroupId) {
const group = state.typebot.groups.find( const group = state.typebotsQueue[0].typebot.groups.find(
(group) => group.id === startGroupId (group) => group.id === startGroupId
) )
if (!group) if (!group)
@@ -18,9 +18,10 @@ export const startBotFlow = async (
}) })
return executeGroup(state)(group) return executeGroup(state)(group)
} }
const firstEdgeId = state.typebot.groups[0].blocks[0].outgoingEdgeId const firstEdgeId =
state.typebotsQueue[0].typebot.groups[0].blocks[0].outgoingEdgeId
if (!firstEdgeId) return { messages: [], newSessionState: state } if (!firstEdgeId) return { messages: [], newSessionState: state }
const nextGroup = getNextGroup(state)(firstEdgeId) const nextGroup = getNextGroup(state)(firstEdgeId)
if (!nextGroup) return { messages: [], newSessionState: state } if (!nextGroup.group) return { messages: [], newSessionState: state }
return executeGroup(state)(nextGroup.group) return executeGroup(state)(nextGroup.group)
} }

View File

@@ -0,0 +1,33 @@
import prisma from '@/lib/prisma'
import { getDefinedVariables } from '@typebot.io/lib/results'
import { TypebotInSession } from '@typebot.io/schemas'
type Props = {
resultId: string
typebot: TypebotInSession
hasStarted: boolean
isCompleted: boolean
}
export const createResultIfNotExist = async ({
resultId,
typebot,
hasStarted,
isCompleted,
}: Props) => {
const existingResult = await prisma.result.findUnique({
where: { id: resultId },
select: { id: true },
})
if (existingResult) return
return prisma.result.createMany({
data: [
{
id: resultId,
typebotId: typebot.id,
isCompleted: isCompleted ? true : false,
hasStarted,
variables: getDefinedVariables(typebot.variables),
},
],
})
}

View File

@@ -1,4 +1,5 @@
import prisma from '@/lib/prisma' import prisma from '@/lib/prisma'
import { Answer, Result } from '@typebot.io/schemas'
type Props = { type Props = {
id: string id: string
@@ -9,6 +10,18 @@ export const findResult = ({ id }: Props) =>
select: { select: {
id: true, id: true,
variables: true, variables: true,
answers: { select: { blockId: true, variableId: true, content: true } }, hasStarted: true,
answers: {
select: {
content: true,
blockId: true,
variableId: true,
},
},
}, },
}) }) as Promise<
| (Pick<Result, 'id' | 'variables' | 'hasStarted'> & {
answers: Pick<Answer, 'content' | 'blockId' | 'variableId'>[]
})
| null
>

View File

@@ -1,12 +1,13 @@
import prisma from '@/lib/prisma' import prisma from '@/lib/prisma'
import { ChatSession } from '@typebot.io/schemas' import { ChatSession, sessionStateSchema } from '@typebot.io/schemas'
export const getSession = async ( export const getSession = async (
sessionId: string sessionId: string
): Promise<Pick<ChatSession, 'state' | 'id'> | null> => { ): Promise<Pick<ChatSession, 'state' | 'id'> | null> => {
const session = (await prisma.chatSession.findUnique({ const session = await prisma.chatSession.findUnique({
where: { id: sessionId }, where: { id: sessionId },
select: { id: true, state: true }, select: { id: true, state: true },
})) as Pick<ChatSession, 'state' | 'id'> | null })
return session if (!session) return null
return { ...session, state: sessionStateSchema.parse(session.state) }
} }

View File

@@ -12,12 +12,13 @@ type Props = {
state: SessionState state: SessionState
} }
export const upsertAnswer = async ({ answer, reply, block, state }: Props) => { export const upsertAnswer = async ({ answer, reply, block, state }: Props) => {
if (!state.result?.id) return const resultId = state.typebotsQueue[0].resultId
if (!resultId) return
if (reply.includes('http') && block.type === InputBlockType.FILE) { if (reply.includes('http') && block.type === InputBlockType.FILE) {
answer.storageUsed = await computeStorageUsed(reply) answer.storageUsed = await computeStorageUsed(reply)
} }
const where = { const where = {
resultId: state.result.id, resultId,
blockId: block.id, blockId: block.id,
groupId: block.groupId, groupId: block.groupId,
} }
@@ -37,7 +38,7 @@ export const upsertAnswer = async ({ answer, reply, block, state }: Props) => {
}, },
}) })
return prisma.answer.createMany({ return prisma.answer.createMany({
data: [{ ...answer, resultId: state.result.id }], data: [{ ...answer, resultId }],
}) })
} }

View File

@@ -1,33 +1,43 @@
import prisma from '@/lib/prisma' import prisma from '@/lib/prisma'
import { SessionState } from '@typebot.io/schemas' import { getDefinedVariables } from '@typebot.io/lib/results'
import { TypebotInSession } from '@typebot.io/schemas'
type Props = { type Props = {
state: SessionState resultId: string
typebot: TypebotInSession
hasStarted: boolean
isCompleted: boolean isCompleted: boolean
} }
export const upsertResult = async ({ state, isCompleted }: Props) => { export const upsertResult = async ({
resultId,
typebot,
hasStarted,
isCompleted,
}: Props) => {
const existingResult = await prisma.result.findUnique({ const existingResult = await prisma.result.findUnique({
where: { id: state.result.id }, where: { id: resultId },
select: { id: true }, select: { id: true },
}) })
const variablesWithValue = getDefinedVariables(typebot.variables)
if (existingResult) { if (existingResult) {
return prisma.result.updateMany({ return prisma.result.updateMany({
where: { id: state.result.id }, where: { id: resultId },
data: { data: {
isCompleted: isCompleted ? true : undefined, isCompleted: isCompleted ? true : undefined,
hasStarted: state.result.answers.length > 0 ? true : undefined, hasStarted,
variables: state.result.variables, variables: variablesWithValue,
}, },
}) })
} }
return prisma.result.createMany({ return prisma.result.createMany({
data: [ data: [
{ {
id: state.result.id, id: resultId,
typebotId: state.typebot.id, typebotId: typebot.id,
isCompleted: isCompleted ? true : false, isCompleted: isCompleted ? true : false,
hasStarted: state.result.answers.length > 0 ? true : undefined, hasStarted,
variables: state.result.variables, variables: variablesWithValue,
}, },
], ],
}) })

View File

@@ -1,8 +1,6 @@
import { isDefined } from '@typebot.io/lib'
import { import {
SessionState, SessionState,
VariableWithUnknowValue, VariableWithUnknowValue,
VariableWithValue,
Variable, Variable,
} from '@typebot.io/schemas' } from '@typebot.io/schemas'
import { safeStringify } from '@typebot.io/lib/safeStringify' import { safeStringify } from '@typebot.io/lib/safeStringify'
@@ -11,40 +9,23 @@ export const updateVariables =
(state: SessionState) => (state: SessionState) =>
(newVariables: VariableWithUnknowValue[]): SessionState => ({ (newVariables: VariableWithUnknowValue[]): SessionState => ({
...state, ...state,
typebot: { typebotsQueue: state.typebotsQueue.map((typebotInQueue, index) =>
...state.typebot, index === 0
variables: updateTypebotVariables(state)(newVariables), ? {
}, ...typebotInQueue,
result: { typebot: {
...state.result, ...typebotInQueue.typebot,
variables: updateResultVariables(state)(newVariables), variables: updateTypebotVariables(typebotInQueue.typebot)(
}, newVariables
),
},
}
: typebotInQueue
),
}) })
const updateResultVariables =
({ result }: Pick<SessionState, 'result' | 'typebot'>) =>
(newVariables: VariableWithUnknowValue[]): VariableWithValue[] => {
const serializedNewVariables = newVariables.map((variable) => ({
...variable,
value: Array.isArray(variable.value)
? variable.value.map(safeStringify)
: safeStringify(variable.value),
}))
const updatedVariables = [
...result.variables.filter((existingVariable) =>
serializedNewVariables.every(
(newVariable) => existingVariable.id !== newVariable.id
)
),
...serializedNewVariables,
].filter((variable) => isDefined(variable.value)) as VariableWithValue[]
return updatedVariables
}
const updateTypebotVariables = const updateTypebotVariables =
({ typebot }: Pick<SessionState, 'result' | 'typebot'>) => (typebot: { variables: Variable[] }) =>
(newVariables: VariableWithUnknowValue[]): Variable[] => { (newVariables: VariableWithUnknowValue[]): Variable[] => {
const serializedNewVariables = newVariables.map((variable) => ({ const serializedNewVariables = newVariables.map((variable) => ({
...variable, ...variable,

View File

@@ -41,7 +41,7 @@ const handler = async (req: Request) => {
if (!state) return new Response('No state found', { status: 400 }) if (!state) return new Response('No state found', { status: 400 })
const group = state.typebot.groups.find( const group = state.typebotsQueue[0].typebot.groups.find(
(group) => group.id === state.currentBlock?.groupId (group) => group.id === state.currentBlock?.groupId
) )
const blockIndex = const blockIndex =

View File

@@ -85,131 +85,116 @@ const checkIfBodyIsAVariable = (body: string) => /^{{.+}}$/.test(body)
export const executeWebhook = export const executeWebhook =
(typebot: Typebot) => (typebot: Typebot) =>
async ({ async ({
webhook, webhook,
variables, variables,
groupId, groupId,
resultValues, resultValues,
resultId, resultId,
parentTypebotIds = [], parentTypebotIds = [],
}: { }: {
webhook: Webhook webhook: Webhook
variables: Variable[] variables: Variable[]
groupId: string groupId: string
resultValues?: ResultValues resultValues?: ResultValues
resultId?: string resultId?: string
parentTypebotIds: string[] parentTypebotIds: string[]
}): Promise<WebhookResponse> => { }): Promise<WebhookResponse> => {
if (!webhook.url || !webhook.method) if (!webhook.url || !webhook.method)
return { return {
statusCode: 400, statusCode: 400,
data: { message: `Webhook doesn't have url or method` }, data: { message: `Webhook doesn't have url or method` },
}
const basicAuth: { username?: string; password?: string } = {}
const basicAuthHeaderIdx = webhook.headers.findIndex(
(h) =>
h.key?.toLowerCase() === 'authorization' &&
h.value?.toLowerCase()?.includes('basic')
)
const isUsernamePasswordBasicAuth =
basicAuthHeaderIdx !== -1 &&
webhook.headers[basicAuthHeaderIdx].value?.includes(':')
if (isUsernamePasswordBasicAuth) {
const [username, password] =
webhook.headers[basicAuthHeaderIdx].value?.slice(6).split(':') ?? []
basicAuth.username = username
basicAuth.password = password
webhook.headers.splice(basicAuthHeaderIdx, 1)
} }
const headers = convertKeyValueTableToObject(webhook.headers, variables) as const basicAuth: { username?: string; password?: string } = {}
| Headers const basicAuthHeaderIdx = webhook.headers.findIndex(
| undefined (h) =>
const queryParams = stringify( h.key?.toLowerCase() === 'authorization' &&
convertKeyValueTableToObject(webhook.queryParams, variables) h.value?.toLowerCase()?.includes('basic')
) )
const contentType = headers ? headers['Content-Type'] : undefined const isUsernamePasswordBasicAuth =
const linkedTypebotsParents = await fetchLinkedTypebots({ basicAuthHeaderIdx !== -1 &&
isPreview: !('typebotId' in typebot), webhook.headers[basicAuthHeaderIdx].value?.includes(':')
typebotIds: parentTypebotIds, if (isUsernamePasswordBasicAuth) {
}) const [username, password] =
const linkedTypebotsChildren = await getPreviouslyLinkedTypebots({ webhook.headers[basicAuthHeaderIdx].value?.slice(6).split(':') ?? []
isPreview: !('typebotId' in typebot), basicAuth.username = username
typebots: [typebot], basicAuth.password = password
})([]) webhook.headers.splice(basicAuthHeaderIdx, 1)
const bodyContent = await getBodyContent(typebot, [ }
...linkedTypebotsParents, const headers = convertKeyValueTableToObject(webhook.headers, variables) as
...linkedTypebotsChildren, | Headers
])({ | undefined
body: webhook.body, const queryParams = stringify(
resultValues, convertKeyValueTableToObject(webhook.queryParams, variables)
groupId, )
variables, const contentType = headers ? headers['Content-Type'] : undefined
}) const linkedTypebotsParents = await fetchLinkedTypebots({
const { data: body, isJson } = isPreview: !('typebotId' in typebot),
bodyContent && webhook.method !== HttpMethod.GET typebotIds: parentTypebotIds,
? safeJsonParse( })
const linkedTypebotsChildren = await getPreviouslyLinkedTypebots({
isPreview: !('typebotId' in typebot),
typebots: [typebot],
})([])
const bodyContent = await getBodyContent(typebot, [
...linkedTypebotsParents,
...linkedTypebotsChildren,
])({
body: webhook.body,
resultValues,
groupId,
variables,
})
const { data: body, isJson } =
bodyContent && webhook.method !== HttpMethod.GET
? safeJsonParse(
parseVariables(variables, { parseVariables(variables, {
escapeForJson: !checkIfBodyIsAVariable(bodyContent), escapeForJson: !checkIfBodyIsAVariable(bodyContent),
})(bodyContent) })(bodyContent)
) )
: { data: undefined, isJson: false } : { data: undefined, isJson: false }
const request = { const request = {
url: parseVariables(variables)( url: parseVariables(variables)(
webhook.url + (queryParams !== '' ? `?${queryParams}` : '') webhook.url + (queryParams !== '' ? `?${queryParams}` : '')
), ),
method: webhook.method as Method, method: webhook.method as Method,
headers, headers,
...basicAuth, ...basicAuth,
json: json:
!contentType?.includes('x-www-form-urlencoded') && body && isJson !contentType?.includes('x-www-form-urlencoded') && body && isJson
? body ? body
: undefined, : undefined,
form: form:
contentType?.includes('x-www-form-urlencoded') && body contentType?.includes('x-www-form-urlencoded') && body
? body ? body
: undefined, : undefined,
body: body && !isJson ? body : undefined, body: body && !isJson ? body : undefined,
} }
try { try {
const response = await got(request.url, omit(request, 'url')) const response = await got(request.url, omit(request, 'url'))
await saveSuccessLog({ await saveSuccessLog({
resultId, resultId,
message: 'Webhook successfuly executed.', message: 'Webhook successfuly executed.',
details: { details: {
statusCode: response.statusCode,
request,
response: safeJsonParse(response.body).data,
},
})
return {
statusCode: response.statusCode, statusCode: response.statusCode,
data: safeJsonParse(response.body).data, request,
} response: safeJsonParse(response.body).data,
} catch (error) { },
if (error instanceof HTTPError) { })
const response = { return {
statusCode: error.response.statusCode, statusCode: response.statusCode,
data: safeJsonParse(error.response.body as string).data, data: safeJsonParse(response.body).data,
} }
await saveErrorLog({ } catch (error) {
resultId, if (error instanceof HTTPError) {
message: 'Webhook returned an error',
details: {
request,
response,
},
})
return response
}
const response = { const response = {
statusCode: 500, statusCode: error.response.statusCode,
data: { message: `Error from Typebot server: ${error}` }, data: safeJsonParse(error.response.body as string).data,
} }
console.error(error)
await saveErrorLog({ await saveErrorLog({
resultId, resultId,
message: 'Webhook failed to execute', message: 'Webhook returned an error',
details: { details: {
request, request,
response, response,
@@ -217,36 +202,66 @@ export const executeWebhook =
}) })
return response return response
} }
const response = {
statusCode: 500,
data: { message: `Error from Typebot server: ${error}` },
}
console.error(error)
await saveErrorLog({
resultId,
message: 'Webhook failed to execute',
details: {
request,
response,
},
})
return response
} }
}
const getBodyContent = const getBodyContent =
( (
typebot: Pick<Typebot | PublicTypebot, 'groups' | 'variables' | 'edges'>, typebot: Pick<Typebot | PublicTypebot, 'groups' | 'variables' | 'edges'>,
linkedTypebots: (Typebot | PublicTypebot)[] linkedTypebots: (Typebot | PublicTypebot)[]
) => ) =>
async ({ async ({
body, body,
resultValues, resultValues,
groupId, groupId,
variables, variables,
}: { }: {
body?: string | null body?: string | null
resultValues?: ResultValues resultValues?: ResultValues
groupId: string groupId: string
variables: Variable[] variables: Variable[]
}): Promise<string | undefined> => { }): Promise<string | undefined> => {
if (!body) return if (!body) return
return body === '{{state}}' return body === '{{state}}'
? JSON.stringify( ? JSON.stringify(
resultValues resultValues
? parseAnswers(typebot, linkedTypebots)(resultValues) ? parseAnswers({
answers: resultValues.answers.map((answer) => ({
key:
(answer.variableId
? typebot.variables.find(
(variable) => variable.id === answer.variableId
)?.name
: typebot.groups.find((group) =>
group.blocks.find(
(block) => block.id === answer.blockId
)
)?.title) ?? '',
value: answer.content,
})),
variables: resultValues.variables,
})
: await parseSampleResult(typebot, linkedTypebots)( : await parseSampleResult(typebot, linkedTypebots)(
groupId, groupId,
variables variables
) )
) )
: body : body
} }
const convertKeyValueTableToObject = ( const convertKeyValueTableToObject = (
keyValues: KeyValue[] | undefined, keyValues: KeyValue[] | undefined,

View File

@@ -15,7 +15,6 @@ import Mail from 'nodemailer/lib/mailer'
import { DefaultBotNotificationEmail } from '@typebot.io/emails' import { DefaultBotNotificationEmail } from '@typebot.io/emails'
import { render } from '@faire/mjml-react/utils/render' import { render } from '@faire/mjml-react/utils/render'
import prisma from '@/lib/prisma' import prisma from '@/lib/prisma'
import { getPreviouslyLinkedTypebots } from '@/features/blocks/logic/typebotLink/getPreviouslyLinkedTypebots'
import { saveErrorLog } from '@/features/logs/saveErrorLog' import { saveErrorLog } from '@/features/logs/saveErrorLog'
import { saveSuccessLog } from '@/features/logs/saveSuccessLog' import { saveSuccessLog } from '@/features/logs/saveSuccessLog'
@@ -197,10 +196,20 @@ const getEmailBody = async ({
where: { typebotId }, where: { typebotId },
})) as unknown as PublicTypebot })) as unknown as PublicTypebot
if (!typebot) return if (!typebot) return
const linkedTypebots = await getPreviouslyLinkedTypebots({ const answers = parseAnswers({
typebots: [typebot], answers: resultValues.answers.map((answer) => ({
})([]) key:
const answers = parseAnswers(typebot, linkedTypebots)(resultValues) (answer.variableId
? typebot.variables.find(
(variable) => variable.id === answer.variableId
)?.name
: typebot.groups.find((group) =>
group.blocks.find((block) => block.id === answer.blockId)
)?.title) ?? '',
value: answer.content,
})),
variables: resultValues.variables,
})
return { return {
html: render( html: render(
<DefaultBotNotificationEmail <DefaultBotNotificationEmail

View File

@@ -0,0 +1,75 @@
{
"id": "cl0ibhi7s0018n21aarlag0cm",
"createdAt": "2022-03-08T15:58:49.720Z",
"updatedAt": "2022-03-08T16:07:18.899Z",
"name": "My typebot",
"folderId": null,
"groups": [
{
"id": "1qQrnsLzRim1LqCrhbj1MW",
"blocks": [
{
"id": "8srsGhdBJK8v88Xo1RRS4C",
"type": "start",
"label": "Start",
"groupId": "1qQrnsLzRim1LqCrhbj1MW",
"outgoingEdgeId": "ovUHhwr6THMhqtn8QbkjtA"
}
],
"title": "Start",
"graphCoordinates": { "x": 0, "y": 0 }
},
{
"id": "wSR4VCcDNDTTsD9Szi2xH8",
"blocks": [
{
"id": "sw6nHJfkMsM4pxZxMBB6QqW",
"type": "Typebot link",
"groupId": "wSR4VCcDNDTTsD9Szi2xH8",
"options": {
"typebotId": "cl0ibhv8d0130n21aw8doxhj5",
"mergeResults": false
}
}
],
"title": "Group #1",
"graphCoordinates": { "x": 363, "y": 199 }
}
],
"variables": [],
"edges": [
{
"id": "ovUHhwr6THMhqtn8QbkjtA",
"to": { "groupId": "wSR4VCcDNDTTsD9Szi2xH8" },
"from": {
"blockId": "8srsGhdBJK8v88Xo1RRS4C",
"groupId": "1qQrnsLzRim1LqCrhbj1MW"
}
}
],
"theme": {
"chat": {
"inputs": {
"color": "#303235",
"backgroundColor": "#FFFFFF",
"placeholderColor": "#9095A0"
},
"buttons": { "color": "#FFFFFF", "backgroundColor": "#0042DA" },
"hostBubbles": { "color": "#303235", "backgroundColor": "#F7F8FF" },
"guestBubbles": { "color": "#FFFFFF", "backgroundColor": "#FF8E21" }
},
"general": { "font": "Open Sans", "background": { "type": "None" } }
},
"settings": {
"general": {
"isBrandingEnabled": 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
}

View File

@@ -3,12 +3,11 @@ import {
Variable, Variable,
InputBlock, InputBlock,
ResultHeaderCell, ResultHeaderCell,
Answer,
VariableWithValue, VariableWithValue,
Typebot, Typebot,
ResultWithAnswers, ResultWithAnswers,
InputBlockType, InputBlockType,
ResultInSession, AnswerInSessionState,
} from '@typebot.io/schemas' } from '@typebot.io/schemas'
import { import {
isInputBlock, isInputBlock,
@@ -16,6 +15,7 @@ import {
byId, byId,
isNotEmpty, isNotEmpty,
parseGroupTitle, parseGroupTitle,
isEmpty,
} from './utils' } from './utils'
export const parseResultHeader = ( export const parseResultHeader = (
@@ -216,50 +216,36 @@ const parseResultsFromPreviousBotVersions = (
] ]
}, []) }, [])
export const parseAnswers = export const parseAnswers = ({
( answers,
typebot: Pick<Typebot, 'groups' | 'variables'>, variables: resultVariables,
linkedTypebots: Pick<Typebot, 'groups' | 'variables'>[] | undefined }: {
) => answers: AnswerInSessionState[]
({ variables: VariableWithValue[]
createdAt, }): {
answers, [key: string]: string
variables: resultVariables, } => {
}: Omit<ResultInSession, 'hasStarted'> & { createdAt?: Date | string }): { return {
[key: string]: string submittedAt: new Date().toISOString(),
} => { ...[...answers, ...resultVariables].reduce<{
const header = parseResultHeader(typebot, linkedTypebots) [key: string]: string
return { }>((o, answerOrVariable) => {
submittedAt: !createdAt if ('id' in answerOrVariable) {
? new Date().toISOString() const variable = answerOrVariable
: typeof createdAt === 'string' if (variable.value === null) return o
? createdAt return { ...o, [variable.name]: variable.value.toString() }
: createdAt.toISOString(), }
...[...answers, ...resultVariables].reduce<{ const answer = answerOrVariable as AnswerInSessionState
[key: string]: string if (isEmpty(answer.key)) return o
}>((o, answerOrVariable) => { return {
const isVariable = !('blockId' in answerOrVariable) ...o,
if (isVariable) { [answer.key]: answer.value,
const variable = answerOrVariable as VariableWithValue }
if (variable.value === null) return o }, {}),
return { ...o, [variable.name]: variable.value.toString() }
}
const answer = answerOrVariable as Answer
const key = answer.variableId
? header.find(
(cell) =>
answer.variableId &&
cell.variableIds?.includes(answer.variableId)
)?.label
: header.find((cell) =>
cell.blocks?.some((block) => block.id === answer.blockId)
)?.label
if (!key) return o
if (isDefined(o[key])) return o
return {
...o,
[key]: answer.content.toString(),
}
}, {}),
}
} }
}
export const getDefinedVariables = (variables: Variable[]) =>
variables.filter((variable) =>
isDefined(variable.value)
) as VariableWithValue[]

View File

@@ -5,6 +5,7 @@ import { LogicBlockType } from './enums'
export const typebotLinkOptionsSchema = z.object({ export const typebotLinkOptionsSchema = z.object({
typebotId: z.string().optional(), typebotId: z.string().optional(),
groupId: z.string().optional(), groupId: z.string().optional(),
mergeResults: z.boolean().optional(),
}) })
export const typebotLinkBlockSchema = blockBaseSchema.merge( export const typebotLinkBlockSchema = blockBaseSchema.merge(
@@ -14,7 +15,9 @@ export const typebotLinkBlockSchema = blockBaseSchema.merge(
}) })
) )
export const defaultTypebotLinkOptions: TypebotLinkOptions = {} export const defaultTypebotLinkOptions: TypebotLinkOptions = {
mergeResults: false,
}
export type TypebotLinkBlock = z.infer<typeof typebotLinkBlockSchema> export type TypebotLinkBlock = z.infer<typeof typebotLinkBlockSchema>
export type TypebotLinkOptions = z.infer<typeof typebotLinkOptionsSchema> export type TypebotLinkOptions = z.infer<typeof typebotLinkOptionsSchema>

View File

@@ -5,68 +5,21 @@ import {
paymentInputRuntimeOptionsSchema, paymentInputRuntimeOptionsSchema,
pixelOptionsSchema, pixelOptionsSchema,
redirectOptionsSchema, redirectOptionsSchema,
} from './blocks' } from '../blocks'
import { publicTypebotSchema } from './publicTypebot' import { logSchema } from '../result'
import { logSchema, resultSchema } from './result' import { listVariableValue, typebotSchema } from '../typebot'
import { listVariableValue, typebotSchema } from './typebot'
import { import {
textBubbleContentSchema, textBubbleContentSchema,
imageBubbleContentSchema, imageBubbleContentSchema,
videoBubbleContentSchema, videoBubbleContentSchema,
audioBubbleContentSchema, audioBubbleContentSchema,
embedBubbleContentSchema, embedBubbleContentSchema,
} from './blocks/bubbles' } from '../blocks/bubbles'
import { answerSchema } from './answer' import { BubbleBlockType } from '../blocks/bubbles/enums'
import { BubbleBlockType } from './blocks/bubbles/enums' import { inputBlockSchemas } from '../blocks/schemas'
import { inputBlockSchemas } from './blocks/schemas' import { chatCompletionMessageSchema } from '../blocks/integrations/openai'
import { chatCompletionMessageSchema } from './blocks/integrations/openai' import { sessionStateSchema } from './sessionState'
import { dynamicThemeSchema } from './shared'
const typebotInSessionStateSchema = publicTypebotSchema._def.schema.pick({
id: true,
groups: true,
edges: true,
variables: true,
})
const dynamicThemeSchema = z.object({
hostAvatarUrl: z.string().optional(),
guestAvatarUrl: z.string().optional(),
})
const answerInSessionStateSchema = answerSchema.pick({
content: true,
blockId: true,
variableId: true,
})
const resultInSessionStateSchema = resultSchema
.pick({
variables: true,
})
.merge(
z.object({
answers: z.array(answerInSessionStateSchema),
id: z.string().optional(),
})
)
export const sessionStateSchema = z.object({
typebot: typebotInSessionStateSchema,
dynamicTheme: dynamicThemeSchema.optional(),
linkedTypebots: z.object({
typebots: z.array(typebotInSessionStateSchema),
queue: z.array(z.object({ edgeId: z.string(), typebotId: z.string() })),
}),
currentTypebotId: z.string(),
result: resultInSessionStateSchema,
currentBlock: z
.object({
blockId: z.string(),
groupId: z.string(),
})
.optional(),
isStreamEnabled: z.boolean().optional(),
})
const chatSessionSchema = z.object({ const chatSessionSchema = z.object({
id: z.string(), id: z.string(),
@@ -301,9 +254,7 @@ export const chatReplySchema = z.object({
}) })
export type ChatSession = z.infer<typeof chatSessionSchema> export type ChatSession = z.infer<typeof chatSessionSchema>
export type SessionState = z.infer<typeof sessionStateSchema>
export type TypebotInSession = z.infer<typeof typebotInSessionStateSchema>
export type ResultInSession = z.infer<typeof resultInSessionStateSchema>
export type ChatReply = z.infer<typeof chatReplySchema> export type ChatReply = z.infer<typeof chatReplySchema>
export type ChatMessage = z.infer<typeof chatMessageSchema> export type ChatMessage = z.infer<typeof chatMessageSchema>
export type SendMessageInput = z.infer<typeof sendMessageInputSchema> export type SendMessageInput = z.infer<typeof sendMessageInputSchema>

View File

@@ -0,0 +1,125 @@
import { z } from 'zod'
import { answerSchema } from '../answer'
import { resultSchema } from '../result'
import { typebotInSessionStateSchema, dynamicThemeSchema } from './shared'
const answerInSessionStateSchema = answerSchema.pick({
content: true,
blockId: true,
variableId: true,
})
const answerInSessionStateSchemaV2 = z.object({
key: z.string(),
value: z.string(),
})
export type AnswerInSessionState = z.infer<typeof answerInSessionStateSchemaV2>
const resultInSessionStateSchema = resultSchema
.pick({
variables: true,
})
.merge(
z.object({
answers: z.array(answerInSessionStateSchema),
id: z.string().optional(),
})
)
const sessionStateSchemaV1 = z.object({
typebot: typebotInSessionStateSchema,
dynamicTheme: dynamicThemeSchema.optional(),
linkedTypebots: z.object({
typebots: z.array(typebotInSessionStateSchema),
queue: z.array(z.object({ edgeId: z.string(), typebotId: z.string() })),
}),
currentTypebotId: z.string(),
result: resultInSessionStateSchema,
currentBlock: z
.object({
blockId: z.string(),
groupId: z.string(),
})
.optional(),
isStreamEnabled: z.boolean().optional(),
})
const sessionStateSchemaV2 = z.object({
version: z.literal('2'),
typebotsQueue: z.array(
z.object({
edgeIdToTriggerWhenDone: z.string().optional(),
isMergingWithParent: z.boolean().optional(),
resultId: z.string().optional(),
answers: z.array(answerInSessionStateSchemaV2),
typebot: typebotInSessionStateSchema,
})
),
dynamicTheme: dynamicThemeSchema.optional(),
currentBlock: z
.object({
blockId: z.string(),
groupId: z.string(),
})
.optional(),
isStreamEnabled: z.boolean().optional(),
})
export type SessionState = z.infer<typeof sessionStateSchemaV2>
export const sessionStateSchema = sessionStateSchemaV1
.or(sessionStateSchemaV2)
.transform((state): SessionState => {
if ('version' in state) return state
return {
version: '2',
typebotsQueue: [
{
typebot: state.typebot,
resultId: state.result.id,
answers: state.result.answers.map((answer) => ({
key:
(answer.variableId
? state.typebot.variables.find(
(variable) => variable.id === answer.variableId
)?.name
: state.typebot.groups.find((group) =>
group.blocks.find((block) => block.id === answer.blockId)
)?.title) ?? '',
value: answer.content,
})),
isMergingWithParent: true,
edgeIdToTriggerWhenDone:
state.linkedTypebots.queue.length > 0
? state.linkedTypebots.queue[0].edgeId
: undefined,
},
...state.linkedTypebots.typebots.map(
(typebot, index) =>
({
typebot,
resultId: state.result.id,
answers: state.result.answers.map((answer) => ({
key:
(answer.variableId
? state.typebot.variables.find(
(variable) => variable.id === answer.variableId
)?.name
: state.typebot.groups.find((group) =>
group.blocks.find(
(block) => block.id === answer.blockId
)
)?.title) ?? '',
value: answer.content,
})),
edgeIdToTriggerWhenDone: state.linkedTypebots.queue.at(index + 1)
?.edgeId,
} satisfies SessionState['typebotsQueue'][number])
),
],
dynamicTheme: state.dynamicTheme,
currentBlock: state.currentBlock,
isStreamEnabled: state.isStreamEnabled,
}
})

View File

@@ -0,0 +1,17 @@
import { z } from 'zod'
import { publicTypebotSchema } from '../publicTypebot'
export const typebotInSessionStateSchema = publicTypebotSchema._def.schema.pick(
{
id: true,
groups: true,
edges: true,
variables: true,
}
)
export type TypebotInSession = z.infer<typeof typebotInSessionStateSchema>
export const dynamicThemeSchema = z.object({
hostAvatarUrl: z.string().optional(),
guestAvatarUrl: z.string().optional(),
})

View File

@@ -5,6 +5,8 @@ export * from './features/result'
export * from './features/answer' export * from './features/answer'
export * from './features/utils' export * from './features/utils'
export * from './features/credentials' export * from './features/credentials'
export * from './features/chat' export * from './features/chat/schema'
export * from './features/chat/sessionState'
export * from './features/chat/shared'
export * from './features/workspace' export * from './features/workspace'
export * from './features/items' export * from './features/items'