diff --git a/apps/builder/public/templates/customer-support.json b/apps/builder/public/templates/customer-support.json
index 85229781b..8d8819260 100644
--- a/apps/builder/public/templates/customer-support.json
+++ b/apps/builder/public/templates/customer-support.json
@@ -180,7 +180,8 @@
"type": "Typebot link",
"options": {
"typebotId": "current",
- "groupId": "vLUAPaxKwPF49iZhg4XZYa"
+ "groupId": "vLUAPaxKwPF49iZhg4XZYa",
+ "mergeResults": false
}
}
],
@@ -305,7 +306,8 @@
"groupId": "1GvxCAAEysxJMxrVngud3X",
"options": {
"groupId": "vLUAPaxKwPF49iZhg4XZYa",
- "typebotId": "current"
+ "typebotId": "current",
+ "mergeResults": false
}
}
],
diff --git a/apps/builder/public/templates/faq.json b/apps/builder/public/templates/faq.json
index 219d4fca2..7c904a86a 100644
--- a/apps/builder/public/templates/faq.json
+++ b/apps/builder/public/templates/faq.json
@@ -282,7 +282,8 @@
"type": "Typebot link",
"options": {
"typebotId": "current",
- "groupId": "cl96ns9qr00043b6ii07bo25o"
+ "groupId": "cl96ns9qr00043b6ii07bo25o",
+ "mergeResults": false
}
}
]
diff --git a/apps/builder/public/templates/quiz.json b/apps/builder/public/templates/quiz.json
index 878c3066f..819660239 100644
--- a/apps/builder/public/templates/quiz.json
+++ b/apps/builder/public/templates/quiz.json
@@ -618,7 +618,8 @@
"groupId": "cl1r15f68005f2e6dvdtal7cp",
"options": {
"groupId": "cl1r09bc6000h2e6dqml18p4p",
- "typebotId": "current"
+ "typebotId": "current",
+ "mergeResults": false
}
}
],
diff --git a/apps/builder/src/features/blocks/logic/typebotLink/api/getLinkedTypebots.ts b/apps/builder/src/features/blocks/logic/typebotLink/api/getLinkedTypebots.ts
index eddc52bda..9bdfdfa18 100644
--- a/apps/builder/src/features/blocks/logic/typebotLink/api/getLinkedTypebots.ts
+++ b/apps/builder/src/features/blocks/logic/typebotLink/api/getLinkedTypebots.ts
@@ -65,7 +65,8 @@ export const getLinkedTypebots = authenticatedProcedure
(typebotIds, block) =>
block.type === LogicBlockType.TYPEBOT_LINK &&
isDefined(block.options.typebotId) &&
- !typebotIds.includes(block.options.typebotId)
+ !typebotIds.includes(block.options.typebotId) &&
+ block.options.mergeResults !== false
? [...typebotIds, block.options.typebotId]
: typebotIds,
[]
diff --git a/apps/builder/src/features/blocks/logic/typebotLink/components/TypebotLinkForm.tsx b/apps/builder/src/features/blocks/logic/typebotLink/components/TypebotLinkForm.tsx
index bb062836c..4b87c29d5 100644
--- a/apps/builder/src/features/blocks/logic/typebotLink/components/TypebotLinkForm.tsx
+++ b/apps/builder/src/features/blocks/logic/typebotLink/components/TypebotLinkForm.tsx
@@ -1,10 +1,11 @@
import { Stack } from '@chakra-ui/react'
import { useTypebot } from '@/features/editor/providers/TypebotProvider'
import { TypebotLinkOptions } from '@typebot.io/schemas'
-import { byId } from '@typebot.io/lib'
import { GroupsDropdown } from './GroupsDropdown'
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 = {
options: TypebotLinkOptions
@@ -12,21 +13,30 @@ type Props = {
}
export const TypebotLinkForm = ({ options, onOptionsChange }: Props) => {
- const { linkedTypebots, typebot, save } = useTypebot()
- const [linkedTypebotId, setLinkedTypebotId] = useState(options.typebotId)
+ const { typebot } = useTypebot()
const handleTypebotIdChange = async (
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) =>
onOptionsChange({ ...options, groupId })
- useEffect(() => {
- if (linkedTypebotId === options.typebotId) return
- setLinkedTypebotId(options.typebotId)
- save().then()
- }, [linkedTypebotId, options.typebotId, save])
+ const updateMergeResults = (mergeResults: boolean) =>
+ onOptionsChange({ ...options, mergeResults })
+
+ const isCurrentTypebotSelected =
+ (typebot && options.typebotId === typebot.id) ||
+ options.typebotId === 'current'
return (
@@ -40,22 +50,30 @@ export const TypebotLinkForm = ({ options, onOptionsChange }: Props) => {
)}
{options.typebotId && (
)}
+ {!isCurrentTypebotSelected && (
+
+ )}
)
}
diff --git a/apps/builder/src/features/blocks/logic/typebotLink/components/TypebotLinkNode.tsx b/apps/builder/src/features/blocks/logic/typebotLink/components/TypebotLinkNode.tsx
index 47dad2de7..4d6e2d0c2 100644
--- a/apps/builder/src/features/blocks/logic/typebotLink/components/TypebotLinkNode.tsx
+++ b/apps/builder/src/features/blocks/logic/typebotLink/components/TypebotLinkNode.tsx
@@ -2,24 +2,36 @@ import { TypebotLinkBlock } from '@typebot.io/schemas'
import React from 'react'
import { Tag, Text } from '@chakra-ui/react'
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 = {
block: TypebotLinkBlock
}
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 =
typebot &&
(block.options.typebotId === typebot.id ||
block.options.typebotId === 'current')
- const linkedTypebot = isCurrentTypebot
- ? typebot
- : linkedTypebots?.find(byId(block.options.typebotId))
+ const linkedTypebot = isCurrentTypebot ? typebot : linkedTypebotData?.typebot
const blockTitle = linkedTypebot?.groups.find(
byId(block.options.groupId)
)?.title
+
if (!block.options.typebotId)
return Configure...
return (
diff --git a/apps/builder/src/features/blocks/logic/typebotLink/components/TypebotsDropdown.tsx b/apps/builder/src/features/blocks/logic/typebotLink/components/TypebotsDropdown.tsx
index 0267268cd..dfa787fdc 100644
--- a/apps/builder/src/features/blocks/logic/typebotLink/components/TypebotsDropdown.tsx
+++ b/apps/builder/src/features/blocks/logic/typebotLink/components/TypebotsDropdown.tsx
@@ -61,7 +61,17 @@ export const TypebotsDropdown = ({
aria-label="Navigate to typebot"
icon={}
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 ?? [],
+ },
+ }}
/>
)}
diff --git a/apps/builder/src/features/editor/components/TypebotHeader.tsx b/apps/builder/src/features/editor/components/TypebotHeader.tsx
index 405461e4e..352de1581 100644
--- a/apps/builder/src/features/editor/components/TypebotHeader.tsx
+++ b/apps/builder/src/features/editor/components/TypebotHeader.tsx
@@ -156,13 +156,22 @@ export const TypebotHeader = () => {
as={Link}
aria-label="Navigate back"
icon={}
- href={
- router.query.parentId
- ? `/typebots/${router.query.parentId}/edit`
+ href={{
+ pathname: router.query.parentId
+ ? '/typebots/[typebotId]/edit'
: typebot?.folderId
- ? `/typebots/folders/${typebot.folderId}`
- : '/typebots'
- }
+ ? '/typebots/folders/[folderId]'
+ : '/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"
/>
diff --git a/apps/builder/src/features/editor/providers/TypebotProvider.tsx b/apps/builder/src/features/editor/providers/TypebotProvider.tsx
index 3a46d1d0d..cf2de545f 100644
--- a/apps/builder/src/features/editor/providers/TypebotProvider.tsx
+++ b/apps/builder/src/features/editor/providers/TypebotProvider.tsx
@@ -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 {
createContext,
@@ -49,7 +49,6 @@ const typebotContext = createContext<
{
typebot?: Typebot
publishedTypebot?: PublicTypebot
- linkedTypebots?: Pick[]
isReadOnly?: boolean
isPublished: boolean
isSavingLoading: boolean
@@ -132,36 +131,6 @@ export const TypebotProvider = ({
{ redo, undo, flush, canRedo, canUndo, set: setLocalTypebot },
] = useUndo(undefined)
- const linkedTypebotIds = useMemo(
- () =>
- typebot?.groups
- .flatMap((group) => group.blocks)
- .reduce(
- (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(() => {
if (!typebot && isDefined(localTypebot)) setLocalTypebot(undefined)
if (isFetchingTypebot) return
@@ -270,7 +239,6 @@ export const TypebotProvider = ({
value={{
typebot: localTypebot,
publishedTypebot,
- linkedTypebots: linkedTypebotsData?.typebots ?? [],
isReadOnly: typebotData?.isReadOnly,
isSavingLoading: isSaving,
save: saveTypebot,
diff --git a/apps/builder/src/features/results/ResultsProvider.tsx b/apps/builder/src/features/results/ResultsProvider.tsx
index 2b1e3d05a..22ef938ac 100644
--- a/apps/builder/src/features/results/ResultsProvider.tsx
+++ b/apps/builder/src/features/results/ResultsProvider.tsx
@@ -1,11 +1,17 @@
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 { parseResultHeader } from '@typebot.io/lib/results'
import { useTypebot } from '../editor/providers/TypebotProvider'
import { useResultsQuery } from './hooks/useResultsQuery'
import { TableData } from './types'
import { convertResultsToTableData } from './helpers/convertResultsToTableData'
+import { trpc } from '@/lib/trpc'
+import { isDefined } from '@typebot.io/lib/utils'
const resultsContext = createContext<{
resultsList: { results: ResultWithAnswers[] }[] | undefined
@@ -32,7 +38,7 @@ export const ResultsProvider = ({
totalResults: number
onDeleteResults: (totalResultsDeleted: number) => void
}) => {
- const { publishedTypebot, linkedTypebots } = useTypebot()
+ const { publishedTypebot } = useTypebot()
const { showToast } = useToast()
const { data, fetchNextPage, hasNextPage, refetch } = useResultsQuery({
typebotId,
@@ -41,6 +47,29 @@ export const ResultsProvider = ({
},
})
+ const linkedTypebotIds =
+ publishedTypebot?.groups
+ .flatMap((group) => group.blocks)
+ .reduce(
+ (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(
() => data?.flatMap((d) => d.results) ?? [],
[data]
@@ -49,9 +78,9 @@ export const ResultsProvider = ({
const resultHeader = useMemo(
() =>
publishedTypebot
- ? parseResultHeader(publishedTypebot, linkedTypebots)
+ ? parseResultHeader(publishedTypebot, linkedTypebotsData?.typebots)
: [],
- [linkedTypebots, publishedTypebot]
+ [linkedTypebotsData?.typebots, publishedTypebot]
)
const tableData = useMemo(
diff --git a/apps/builder/src/features/results/components/table/ExportAllResultsModal.tsx b/apps/builder/src/features/results/components/table/ExportAllResultsModal.tsx
index c6def732f..bb3fe90c8 100644
--- a/apps/builder/src/features/results/components/table/ExportAllResultsModal.tsx
+++ b/apps/builder/src/features/results/components/table/ExportAllResultsModal.tsx
@@ -23,6 +23,7 @@ import { useResults } from '../../ResultsProvider'
import { parseColumnOrder } from '../../helpers/parseColumnsOrder'
import { convertResultsToTableData } from '../../helpers/convertResultsToTableData'
import { parseAccessor } from '../../helpers/parseAccessor'
+import { isDefined } from '@typebot.io/lib'
type Props = {
isOpen: boolean
@@ -30,7 +31,7 @@ type Props = {
}
export const ExportAllResultsModal = ({ isOpen, onClose }: Props) => {
- const { typebot, publishedTypebot, linkedTypebots } = useTypebot()
+ const { typebot, publishedTypebot } = useTypebot()
const workspaceId = typebot?.workspaceId
const typebotId = typebot?.id
const { showToast } = useToast()
@@ -41,6 +42,15 @@ export const ExportAllResultsModal = ({ isOpen, onClose }: Props) => {
const [areDeletedBlocksIncluded, setAreDeletedBlocksIncluded] =
useState(false)
+ const { data: linkedTypebotsData } = trpc.getLinkedTypebots.useQuery(
+ {
+ typebotId: typebotId as string,
+ },
+ {
+ enabled: isDefined(typebotId),
+ }
+ )
+
const getAllResults = async () => {
if (!workspaceId || !typebotId) return []
const allResults = []
@@ -71,7 +81,11 @@ export const ExportAllResultsModal = ({ isOpen, onClose }: Props) => {
const results = await getAllResults()
const resultHeader = areDeletedBlocksIncluded
- ? parseResultHeader(publishedTypebot, linkedTypebots, results)
+ ? parseResultHeader(
+ publishedTypebot,
+ linkedTypebotsData?.typebots,
+ results
+ )
: existingResultHeader
const dataToUnparse = convertResultsToTableData(results, resultHeader)
diff --git a/apps/builder/src/features/typebot/helpers/parseNewBlock.ts b/apps/builder/src/features/typebot/helpers/parseNewBlock.ts
index a648f0bba..8833e30e1 100644
--- a/apps/builder/src/features/typebot/helpers/parseNewBlock.ts
+++ b/apps/builder/src/features/typebot/helpers/parseNewBlock.ts
@@ -43,6 +43,7 @@ import {
LogicBlockType,
defaultAbTestOptions,
BlockWithItems,
+ defaultTypebotLinkOptions,
} from '@typebot.io/schemas'
import { defaultPictureChoiceOptions } from '@typebot.io/schemas/features/blocks/inputs/pictureChoice'
@@ -122,7 +123,7 @@ const parseDefaultBlockOptions = (type: BlockWithOptionsType): BlockOptions => {
case LogicBlockType.JUMP:
return {}
case LogicBlockType.TYPEBOT_LINK:
- return {}
+ return defaultTypebotLinkOptions
case LogicBlockType.AB_TEST:
return defaultAbTestOptions
case IntegrationBlockType.GOOGLE_SHEETS:
diff --git a/apps/viewer/src/features/blocks/inputs/buttons/injectVariableValuesInButtonsInputBlock.ts b/apps/viewer/src/features/blocks/inputs/buttons/injectVariableValuesInButtonsInputBlock.ts
index 3e88445c1..313fe5278 100644
--- a/apps/viewer/src/features/blocks/inputs/buttons/injectVariableValuesInButtonsInputBlock.ts
+++ b/apps/viewer/src/features/blocks/inputs/buttons/injectVariableValuesInButtonsInputBlock.ts
@@ -13,8 +13,9 @@ import { filterChoiceItems } from './filterChoiceItems'
export const injectVariableValuesInButtonsInputBlock =
(state: SessionState) =>
(block: ChoiceInputBlock): ChoiceInputBlock => {
+ const { variables } = state.typebotsQueue[0].typebot
if (block.options.dynamicVariableId) {
- const variable = state.typebot.variables.find(
+ const variable = variables.find(
(variable) =>
variable.id === block.options.dynamicVariableId &&
isDefined(variable.value)
@@ -31,18 +32,17 @@ export const injectVariableValuesInButtonsInputBlock =
})),
}
}
- return deepParseVariables(state.typebot.variables)(
- filterChoiceItems(state.typebot.variables)(block)
- )
+ return deepParseVariables(variables)(filterChoiceItems(variables)(block))
}
const getVariableValue =
(state: SessionState) =>
(variable: VariableWithValue): (string | null)[] => {
if (!Array.isArray(variable.value)) {
- const [transformedVariable] = transformStringVariablesToList(
- state.typebot.variables
- )([variable.id])
+ const { variables } = state.typebotsQueue[0].typebot
+ const [transformedVariable] = transformStringVariablesToList(variables)([
+ variable.id,
+ ])
updateVariables(state)([transformedVariable])
return transformedVariable.value as string[]
}
diff --git a/apps/viewer/src/features/blocks/inputs/payment/computePaymentInputRuntimeOptions.ts b/apps/viewer/src/features/blocks/inputs/payment/computePaymentInputRuntimeOptions.ts
index 48dd884cb..20549d27e 100644
--- a/apps/viewer/src/features/blocks/inputs/payment/computePaymentInputRuntimeOptions.ts
+++ b/apps/viewer/src/features/blocks/inputs/payment/computePaymentInputRuntimeOptions.ts
@@ -11,18 +11,17 @@ import Stripe from 'stripe'
import { decrypt } from '@typebot.io/lib/api/encryption'
export const computePaymentInputRuntimeOptions =
- (state: Pick) =>
- (options: PaymentInputOptions) =>
+ (state: SessionState) => (options: PaymentInputOptions) =>
createStripePaymentIntent(state)(options)
const createStripePaymentIntent =
- (state: Pick) =>
+ (state: SessionState) =>
async (options: PaymentInputOptions): Promise => {
const {
- result,
+ resultId,
typebot: { variables },
- } = state
- const isPreview = !result.id
+ } = state.typebotsQueue[0]
+ const isPreview = !resultId
if (!options.credentialsId)
throw new TRPCError({
code: 'BAD_REQUEST',
diff --git a/apps/viewer/src/features/blocks/inputs/pictureChoice/injectVariableValuesInPictureChoiceBlock.ts b/apps/viewer/src/features/blocks/inputs/pictureChoice/injectVariableValuesInPictureChoiceBlock.ts
index 00341935f..d5fd257ce 100644
--- a/apps/viewer/src/features/blocks/inputs/pictureChoice/injectVariableValuesInPictureChoiceBlock.ts
+++ b/apps/viewer/src/features/blocks/inputs/pictureChoice/injectVariableValuesInPictureChoiceBlock.ts
@@ -1,15 +1,15 @@
import {
- SessionState,
VariableWithValue,
ItemType,
PictureChoiceBlock,
+ Variable,
} from '@typebot.io/schemas'
import { isDefined } from '@typebot.io/lib'
import { deepParseVariables } from '@/features/variables/deepParseVariable'
import { filterPictureChoiceItems } from './filterPictureChoiceItems'
export const injectVariableValuesInPictureChoiceBlock =
- (variables: SessionState['typebot']['variables']) =>
+ (variables: Variable[]) =>
(block: PictureChoiceBlock): PictureChoiceBlock => {
if (
block.options.dynamicItems?.isEnabled &&
diff --git a/apps/viewer/src/features/blocks/integrations/chatwoot/executeChatwootBlock.ts b/apps/viewer/src/features/blocks/integrations/chatwoot/executeChatwootBlock.ts
index 2760cc5aa..c0ecc0b3b 100644
--- a/apps/viewer/src/features/blocks/integrations/chatwoot/executeChatwootBlock.ts
+++ b/apps/viewer/src/features/blocks/integrations/chatwoot/executeChatwootBlock.ts
@@ -70,17 +70,18 @@ if (window.$chatwoot) {
`
export const executeChatwootBlock = (
- { typebot, result }: SessionState,
+ state: SessionState,
block: ChatwootBlock
): ExecuteIntegrationResponse => {
+ const { typebot, resultId } = state.typebotsQueue[0]
const chatwootCode =
block.options.task === 'Close widget'
? chatwootCloseCode
- : isDefined(result.id)
+ : isDefined(resultId)
? parseChatwootOpenCode({
...block.options,
typebotId: typebot.id,
- resultId: result.id,
+ resultId,
})
: ''
return {
diff --git a/apps/viewer/src/features/blocks/integrations/googleAnalytics/executeGoogleAnalyticsBlock.ts b/apps/viewer/src/features/blocks/integrations/googleAnalytics/executeGoogleAnalyticsBlock.ts
index 9ef6637e6..06a2506b3 100644
--- a/apps/viewer/src/features/blocks/integrations/googleAnalytics/executeGoogleAnalyticsBlock.ts
+++ b/apps/viewer/src/features/blocks/integrations/googleAnalytics/executeGoogleAnalyticsBlock.ts
@@ -3,11 +3,12 @@ import { deepParseVariables } from '@/features/variables/deepParseVariable'
import { GoogleAnalyticsBlock, SessionState } from '@typebot.io/schemas'
export const executeGoogleAnalyticsBlock = (
- { typebot: { variables }, result }: SessionState,
+ state: SessionState,
block: GoogleAnalyticsBlock
): ExecuteIntegrationResponse => {
- if (!result) return { outgoingEdgeId: block.outgoingEdgeId }
- const googleAnalytics = deepParseVariables(variables, {
+ const { typebot, resultId } = state.typebotsQueue[0]
+ if (!resultId) return { outgoingEdgeId: block.outgoingEdgeId }
+ const googleAnalytics = deepParseVariables(typebot.variables, {
guessCorrectTypes: true,
removeEmptyStrings: true,
})(block.options)
diff --git a/apps/viewer/src/features/blocks/integrations/googleSheets/getRow.ts b/apps/viewer/src/features/blocks/integrations/googleSheets/getRow.ts
index 5e2b0a4e6..82b67ea12 100644
--- a/apps/viewer/src/features/blocks/integrations/googleSheets/getRow.ts
+++ b/apps/viewer/src/features/blocks/integrations/googleSheets/getRow.ts
@@ -19,13 +19,11 @@ export const getRow = async (
}: { outgoingEdgeId?: string; options: GoogleSheetsGetOptions }
): Promise => {
const logs: ReplyLog[] = []
- const { sheetId, cellsToExtract, referenceCell, filter } = deepParseVariables(
- state.typebot.variables
- )(options)
+ const { variables } = state.typebotsQueue[0].typebot
+ const { sheetId, cellsToExtract, referenceCell, filter } =
+ deepParseVariables(variables)(options)
if (!sheetId) return { outgoingEdgeId }
- const variables = state.typebot.variables
-
const doc = await getAuthenticatedGoogleDoc({
credentialsId: options.credentialsId,
spreadsheetId: options.spreadsheetId,
diff --git a/apps/viewer/src/features/blocks/integrations/googleSheets/insertRow.ts b/apps/viewer/src/features/blocks/integrations/googleSheets/insertRow.ts
index 0055f4b15..bbdff7216 100644
--- a/apps/viewer/src/features/blocks/integrations/googleSheets/insertRow.ts
+++ b/apps/viewer/src/features/blocks/integrations/googleSheets/insertRow.ts
@@ -8,12 +8,13 @@ import { getAuthenticatedGoogleDoc } from './helpers/getAuthenticatedGoogleDoc'
import { ExecuteIntegrationResponse } from '@/features/chat/types'
export const insertRow = async (
- { typebot: { variables } }: SessionState,
+ state: SessionState,
{
outgoingEdgeId,
options,
}: { outgoingEdgeId?: string; options: GoogleSheetsInsertRowOptions }
): Promise => {
+ const { variables } = state.typebotsQueue[0].typebot
if (!options.cellsToInsert || !options.sheetId) return { outgoingEdgeId }
const logs: ReplyLog[] = []
diff --git a/apps/viewer/src/features/blocks/integrations/googleSheets/updateRow.ts b/apps/viewer/src/features/blocks/integrations/googleSheets/updateRow.ts
index c75b80951..6b5f16052 100644
--- a/apps/viewer/src/features/blocks/integrations/googleSheets/updateRow.ts
+++ b/apps/viewer/src/features/blocks/integrations/googleSheets/updateRow.ts
@@ -10,12 +10,13 @@ import { ExecuteIntegrationResponse } from '@/features/chat/types'
import { matchFilter } from './helpers/matchFilter'
export const updateRow = async (
- { typebot: { variables } }: SessionState,
+ state: SessionState,
{
outgoingEdgeId,
options,
}: { outgoingEdgeId?: string; options: GoogleSheetsUpdateRowOptions }
): Promise => {
+ const { variables } = state.typebotsQueue[0].typebot
const { sheetId, referenceCell, filter } =
deepParseVariables(variables)(options)
if (!options.cellsToUpsert || !sheetId || (!referenceCell && !filter))
diff --git a/apps/viewer/src/features/blocks/integrations/openai/createChatCompletionOpenAI.ts b/apps/viewer/src/features/blocks/integrations/openai/createChatCompletionOpenAI.ts
index b407d7b0e..248368411 100644
--- a/apps/viewer/src/features/blocks/integrations/openai/createChatCompletionOpenAI.ts
+++ b/apps/viewer/src/features/blocks/integrations/openai/createChatCompletionOpenAI.ts
@@ -1,6 +1,11 @@
import { ExecuteIntegrationResponse } from '@/features/chat/types'
import prisma from '@/lib/prisma'
-import { Block, BubbleBlockType, SessionState } from '@typebot.io/schemas'
+import {
+ Block,
+ BubbleBlockType,
+ SessionState,
+ TypebotInSession,
+} from '@typebot.io/schemas'
import {
ChatCompletionOpenAIOptions,
OpenAICredentials,
@@ -51,13 +56,16 @@ export const createChatCompletionOpenAI = async (
credentials.data,
credentials.iv
)) as OpenAICredentials['data']
+
+ const { typebot } = newSessionState.typebotsQueue[0]
+
const { variablesTransformedToList, messages } = parseChatCompletionMessages(
- newSessionState.typebot.variables
+ typebot.variables
)(options.messages)
if (variablesTransformedToList.length > 0)
newSessionState = updateVariables(state)(variablesTransformedToList)
- const temperature = parseVariableNumber(newSessionState.typebot.variables)(
+ const temperature = parseVariableNumber(typebot.variables)(
options.advancedSettings?.temperature
)
@@ -66,7 +74,7 @@ export const createChatCompletionOpenAI = async (
isCredentialsV2(credentials) &&
newSessionState.isStreamEnabled
) {
- const assistantMessageVariableName = state.typebot.variables.find(
+ const assistantMessageVariableName = typebot.variables.find(
(variable) =>
options.responseMapping.find(
(m) => m.valueToExtract === 'Message content'
@@ -81,9 +89,10 @@ export const createChatCompletionOpenAI = async (
content?: string
role: (typeof chatCompletionMessageRoles)[number]
}[],
- displayStream: isNextBubbleMessageWithAssistantMessage(
- state.typebot
- )(blockId, assistantMessageVariableName),
+ displayStream: isNextBubbleMessageWithAssistantMessage(typebot)(
+ blockId,
+ assistantMessageVariableName
+ ),
},
},
],
@@ -117,7 +126,7 @@ export const createChatCompletionOpenAI = async (
}
const isNextBubbleMessageWithAssistantMessage =
- (typebot: SessionState['typebot']) =>
+ (typebot: TypebotInSession) =>
(blockId: string, assistantVariableName?: string): boolean => {
if (!assistantVariableName) return false
const nextBlock = getNextBlock(typebot)(blockId)
@@ -131,7 +140,7 @@ const isNextBubbleMessageWithAssistantMessage =
}
const getNextBlock =
- (typebot: SessionState['typebot']) =>
+ (typebot: TypebotInSession) =>
(blockId: string): Block | undefined => {
const group = typebot.groups.find((group) =>
group.blocks.find(byId(blockId))
diff --git a/apps/viewer/src/features/blocks/integrations/openai/getChatCompletionStream.ts b/apps/viewer/src/features/blocks/integrations/openai/getChatCompletionStream.ts
index fb3c5d3bb..55bccc595 100644
--- a/apps/viewer/src/features/blocks/integrations/openai/getChatCompletionStream.ts
+++ b/apps/viewer/src/features/blocks/integrations/openai/getChatCompletionStream.ts
@@ -5,7 +5,7 @@ import {
ChatCompletionOpenAIOptions,
OpenAICredentials,
} 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 {
ChatCompletionRequestMessage,
@@ -35,7 +35,8 @@ export const getChatCompletionStream =
credentials.iv
)) as OpenAICredentials['data']
- const temperature = parseVariableNumber(state.typebot.variables)(
+ const { typebot } = state.typebotsQueue[0]
+ const temperature = parseVariableNumber(typebot.variables)(
options.advancedSettings?.temperature
)
diff --git a/apps/viewer/src/features/blocks/integrations/openai/resumeChatCompletion.ts b/apps/viewer/src/features/blocks/integrations/openai/resumeChatCompletion.ts
index 113237962..79eb99038 100644
--- a/apps/viewer/src/features/blocks/integrations/openai/resumeChatCompletion.ts
+++ b/apps/viewer/src/features/blocks/integrations/openai/resumeChatCompletion.ts
@@ -22,9 +22,8 @@ export const resumeChatCompletion =
const newVariables = options.responseMapping.reduce<
VariableWithUnknowValue[]
>((newVariables, mapping) => {
- const existingVariable = newSessionState.typebot.variables.find(
- byId(mapping.variableId)
- )
+ const { typebot } = newSessionState.typebotsQueue[0]
+ const existingVariable = typebot.variables.find(byId(mapping.variableId))
if (!existingVariable) return newVariables
if (mapping.valueToExtract === 'Message content') {
newVariables.push({
diff --git a/apps/viewer/src/features/blocks/integrations/pixel/executePixelBlock.ts b/apps/viewer/src/features/blocks/integrations/pixel/executePixelBlock.ts
index bdfd0989b..b0074378b 100644
--- a/apps/viewer/src/features/blocks/integrations/pixel/executePixelBlock.ts
+++ b/apps/viewer/src/features/blocks/integrations/pixel/executePixelBlock.ts
@@ -3,12 +3,13 @@ import { deepParseVariables } from '@/features/variables/deepParseVariable'
import { PixelBlock, SessionState } from '@typebot.io/schemas'
export const executePixelBlock = (
- { typebot: { variables }, result }: SessionState,
+ state: SessionState,
block: PixelBlock
): 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 }
- const pixel = deepParseVariables(variables, {
+ const pixel = deepParseVariables(typebot.variables, {
guessCorrectTypes: true,
removeEmptyStrings: true,
})(block.options)
diff --git a/apps/viewer/src/features/blocks/integrations/sendEmail/executeSendEmailBlock.tsx b/apps/viewer/src/features/blocks/integrations/sendEmail/executeSendEmailBlock.tsx
index a93e418e3..219bb410a 100644
--- a/apps/viewer/src/features/blocks/integrations/sendEmail/executeSendEmailBlock.tsx
+++ b/apps/viewer/src/features/blocks/integrations/sendEmail/executeSendEmailBlock.tsx
@@ -3,32 +3,32 @@ import prisma from '@/lib/prisma'
import { render } from '@faire/mjml-react/utils/render'
import { DefaultBotNotificationEmail } from '@typebot.io/emails'
import {
- PublicTypebot,
+ AnswerInSessionState,
ReplyLog,
- ResultInSession,
SendEmailBlock,
SendEmailOptions,
SessionState,
SmtpCredentials,
+ TypebotInSession,
Variable,
} from '@typebot.io/schemas'
import { createTransport } from 'nodemailer'
import Mail from 'nodemailer/lib/mailer'
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 { defaultFrom, defaultTransportOptions } from './constants'
import { ExecuteIntegrationResponse } from '@/features/chat/types'
import { findUniqueVariableValue } from '../../../variables/findUniqueVariableValue'
export const executeSendEmailBlock = async (
- { result, typebot }: SessionState,
+ state: SessionState,
block: SendEmailBlock
): Promise => {
const logs: ReplyLog[] = []
const { options } = block
- const { variables } = typebot
- const isPreview = !result.id
+ const { typebot, resultId, answers } = state.typebotsQueue[0]
+ const isPreview = !resultId
if (isPreview)
return {
outgoingEdgeId: block.outgoingEdgeId,
@@ -41,23 +41,23 @@ export const executeSendEmailBlock = async (
}
const body =
- findUniqueVariableValue(variables)(options.body)?.toString() ??
- parseVariables(variables, { escapeHtml: true })(options.body ?? '')
+ findUniqueVariableValue(typebot.variables)(options.body)?.toString() ??
+ parseVariables(typebot.variables, { escapeHtml: true })(options.body ?? '')
try {
const sendEmailLogs = await sendEmail({
- typebotId: typebot.id,
- result,
+ typebot,
+ answers,
credentialsId: options.credentialsId,
- recipients: options.recipients.map(parseVariables(variables)),
- subject: parseVariables(variables)(options.subject ?? ''),
+ recipients: options.recipients.map(parseVariables(typebot.variables)),
+ subject: parseVariables(typebot.variables)(options.subject ?? ''),
body,
- cc: (options.cc ?? []).map(parseVariables(variables)),
- bcc: (options.bcc ?? []).map(parseVariables(variables)),
+ cc: (options.cc ?? []).map(parseVariables(typebot.variables)),
+ bcc: (options.bcc ?? []).map(parseVariables(typebot.variables)),
replyTo: options.replyTo
- ? parseVariables(variables)(options.replyTo)
+ ? parseVariables(typebot.variables)(options.replyTo)
: undefined,
- fileUrls: getFileUrls(variables)(options.attachmentsVariableId),
+ fileUrls: getFileUrls(typebot.variables)(options.attachmentsVariableId),
isCustomBody: options.isCustomBody,
isBodyCode: options.isBodyCode,
})
@@ -74,8 +74,8 @@ export const executeSendEmailBlock = async (
}
const sendEmail = async ({
- typebotId,
- result,
+ typebot,
+ answers,
credentialsId,
recipients,
body,
@@ -87,8 +87,8 @@ const sendEmail = async ({
isCustomBody,
fileUrls,
}: SendEmailOptions & {
- typebotId: string
- result: ResultInSession
+ typebot: TypebotInSession
+ answers: AnswerInSessionState[]
fileUrls?: string | string[]
}): Promise => {
const logs: ReplyLog[] = []
@@ -112,8 +112,8 @@ const sendEmail = async ({
body,
isCustomBody,
isBodyCode,
- typebotId,
- result,
+ typebot,
+ answersInSession: answers,
})
if (!emailBody) {
@@ -206,11 +206,11 @@ const getEmailBody = async ({
body,
isCustomBody,
isBodyCode,
- typebotId,
- result,
+ typebot,
+ answersInSession,
}: {
- typebotId: string
- result: ResultInSession
+ typebot: TypebotInSession
+ answersInSession: AnswerInSessionState[]
} & Pick): Promise<
{ html?: string; text?: string } | undefined
> => {
@@ -219,11 +219,10 @@ const getEmailBody = async ({
html: isBodyCode ? body : undefined,
text: !isBodyCode ? body : undefined,
}
- const typebot = (await prisma.publicTypebot.findUnique({
- where: { typebotId },
- })) as unknown as PublicTypebot
- if (!typebot) return
- const answers = parseAnswers(typebot, [])(result)
+ const answers = parseAnswers({
+ variables: getDefinedVariables(typebot.variables),
+ answers: answersInSession,
+ })
return {
html: render(
=> {
- const { typebot, result } = state
const logs: ReplyLog[] = []
const webhook =
block.options.webhook ??
@@ -52,9 +48,8 @@ export const executeWebhookBlock = async (
}
const preparedWebhook = prepareWebhookAttributes(webhook, block.options)
const parsedWebhook = await parseWebhookAttributes(
- typebot,
- block.groupId,
- result
+ state,
+ state.typebotsQueue[0].answers
)(preparedWebhook)
if (!parsedWebhook) {
logs.push({
@@ -97,14 +92,10 @@ const prepareWebhookAttributes = (
const checkIfBodyIsAVariable = (body: string) => /^{{.+}}$/.test(body)
const parseWebhookAttributes =
- (
- typebot: SessionState['typebot'],
- groupId: string,
- result: ResultInSession
- ) =>
+ (state: SessionState, answers: AnswerInSessionState[]) =>
async (webhook: Webhook): Promise => {
if (!webhook.url || !webhook.method) return
- const { variables } = typebot
+ const { typebot } = state.typebotsQueue[0]
const basicAuth: { username?: string; password?: string } = {}
const basicAuthHeaderIdx = webhook.headers.findIndex(
(h) =>
@@ -121,32 +112,29 @@ const parseWebhookAttributes =
basicAuth.password = password
webhook.headers.splice(basicAuthHeaderIdx, 1)
}
- const headers = convertKeyValueTableToObject(webhook.headers, variables) as
- | ExecutableWebhook['headers']
- | undefined
+ const headers = convertKeyValueTableToObject(
+ webhook.headers,
+ typebot.variables
+ ) as ExecutableWebhook['headers'] | undefined
const queryParams = stringify(
- convertKeyValueTableToObject(webhook.queryParams, variables)
+ convertKeyValueTableToObject(webhook.queryParams, typebot.variables)
)
- const bodyContent = await getBodyContent(
- typebot,
- []
- )({
+ const bodyContent = await getBodyContent({
body: webhook.body,
- result,
- groupId,
- variables,
+ answers,
+ variables: typebot.variables,
})
const { data: body, isJson } =
bodyContent && webhook.method !== HttpMethod.GET
? safeJsonParse(
- parseVariables(variables, {
+ parseVariables(typebot.variables, {
escapeForJson: !checkIfBodyIsAVariable(bodyContent),
})(bodyContent)
)
: { data: undefined, isJson: false }
return {
- url: parseVariables(variables)(
+ url: parseVariables(typebot.variables)(
webhook.url + (queryParams !== '' ? `?${queryParams}` : '')
),
basicAuth,
@@ -229,34 +217,25 @@ export const executeWebhook = async (
}
}
-const getBodyContent =
- (
- typebot: Pick,
- linkedTypebots: (Typebot | PublicTypebot)[]
- ) =>
- async ({
- body,
- result,
- groupId,
- variables,
- }: {
- body?: string | null
- result?: ResultInSession
- groupId: string
- variables: Variable[]
- }): Promise => {
- if (!body) return
- return body === '{{state}}'
- ? JSON.stringify(
- result
- ? parseAnswers(typebot, linkedTypebots)(result)
- : await parseSampleResult(typebot, linkedTypebots)(
- groupId,
- variables
- )
- )
- : body
- }
+const getBodyContent = async ({
+ body,
+ answers,
+ variables,
+}: {
+ body?: string | null
+ answers: AnswerInSessionState[]
+ variables: Variable[]
+}): Promise => {
+ if (!body) return
+ return body === '{{state}}'
+ ? JSON.stringify(
+ parseAnswers({
+ answers,
+ variables: getDefinedVariables(variables),
+ })
+ )
+ : body
+}
const convertKeyValueTableToObject = (
keyValues: KeyValue[] | undefined,
diff --git a/apps/viewer/src/features/blocks/integrations/webhook/resumeWebhookExecution.ts b/apps/viewer/src/features/blocks/integrations/webhook/resumeWebhookExecution.ts
index 753a870a6..295ab2321 100644
--- a/apps/viewer/src/features/blocks/integrations/webhook/resumeWebhookExecution.ts
+++ b/apps/viewer/src/features/blocks/integrations/webhook/resumeWebhookExecution.ts
@@ -5,11 +5,12 @@ import { byId } from '@typebot.io/lib'
import {
MakeComBlock,
PabblyConnectBlock,
+ ReplyLog,
VariableWithUnknowValue,
WebhookBlock,
ZapierBlock,
} from '@typebot.io/schemas'
-import { ReplyLog, SessionState } from '@typebot.io/schemas/features/chat'
+import { SessionState } from '@typebot.io/schemas/features/chat/sessionState'
type Props = {
state: SessionState
@@ -27,7 +28,7 @@ export const resumeWebhookExecution = ({
logs = [],
response,
}: Props): ExecuteIntegrationResponse => {
- const { typebot } = state
+ const { typebot } = state.typebotsQueue[0]
const status = response.statusCode.toString()
const isError = status.startsWith('4') || status.startsWith('5')
diff --git a/apps/viewer/src/features/blocks/logic/condition/executeConditionBlock.ts b/apps/viewer/src/features/blocks/logic/condition/executeConditionBlock.ts
index b42aa835e..289541086 100644
--- a/apps/viewer/src/features/blocks/logic/condition/executeConditionBlock.ts
+++ b/apps/viewer/src/features/blocks/logic/condition/executeConditionBlock.ts
@@ -3,9 +3,10 @@ import { ExecuteLogicResponse } from '@/features/chat/types'
import { executeCondition } from './executeCondition'
export const executeConditionBlock = (
- { typebot: { variables } }: SessionState,
+ state: SessionState,
block: ConditionBlock
): ExecuteLogicResponse => {
+ const { variables } = state.typebotsQueue[0].typebot
const passedCondition = block.items.find((item) =>
executeCondition(variables)(item.content)
)
diff --git a/apps/viewer/src/features/blocks/logic/jump/executeJumpBlock.ts b/apps/viewer/src/features/blocks/logic/jump/executeJumpBlock.ts
index 8a7b93a11..f46d0ab17 100644
--- a/apps/viewer/src/features/blocks/logic/jump/executeJumpBlock.ts
+++ b/apps/viewer/src/features/blocks/logic/jump/executeJumpBlock.ts
@@ -11,9 +11,8 @@ export const executeJumpBlock = (
state: SessionState,
{ groupId, blockId }: JumpBlock['options']
): ExecuteLogicResponse => {
- const groupToJumpTo = state.typebot.groups.find(
- (group) => group.id === groupId
- )
+ const { typebot } = state.typebotsQueue[0]
+ const groupToJumpTo = typebot.groups.find((group) => group.id === groupId)
const blockToJumpTo =
groupToJumpTo?.blocks.find((block) => block.id === blockId) ??
groupToJumpTo?.blocks[0]
diff --git a/apps/viewer/src/features/blocks/logic/redirect/executeRedirect.ts b/apps/viewer/src/features/blocks/logic/redirect/executeRedirect.ts
index 1e676f845..96a2a842a 100644
--- a/apps/viewer/src/features/blocks/logic/redirect/executeRedirect.ts
+++ b/apps/viewer/src/features/blocks/logic/redirect/executeRedirect.ts
@@ -4,9 +4,10 @@ import { sanitizeUrl } from '@typebot.io/lib'
import { ExecuteLogicResponse } from '@/features/chat/types'
export const executeRedirect = (
- { typebot: { variables } }: SessionState,
+ state: SessionState,
block: RedirectBlock
): ExecuteLogicResponse => {
+ const { variables } = state.typebotsQueue[0].typebot
if (!block.options?.url) return { outgoingEdgeId: block.outgoingEdgeId }
const formattedUrl = sanitizeUrl(parseVariables(variables)(block.options.url))
return {
diff --git a/apps/viewer/src/features/blocks/logic/script/executeScript.ts b/apps/viewer/src/features/blocks/logic/script/executeScript.ts
index 0adb36a8b..9d3ab4dd9 100644
--- a/apps/viewer/src/features/blocks/logic/script/executeScript.ts
+++ b/apps/viewer/src/features/blocks/logic/script/executeScript.ts
@@ -5,9 +5,10 @@ import { parseVariables } from '@/features/variables/parseVariables'
import { ScriptBlock, SessionState, Variable } from '@typebot.io/schemas'
export const executeScript = (
- { typebot: { variables } }: SessionState,
+ state: SessionState,
block: ScriptBlock
): ExecuteLogicResponse => {
+ const { variables } = state.typebotsQueue[0].typebot
if (!block.options.content) return { outgoingEdgeId: block.outgoingEdgeId }
const scriptToExecute = parseScriptToExecuteClientSideAction(
diff --git a/apps/viewer/src/features/blocks/logic/setVariable/executeSetVariable.ts b/apps/viewer/src/features/blocks/logic/setVariable/executeSetVariable.ts
index 570e13889..0828b18fb 100644
--- a/apps/viewer/src/features/blocks/logic/setVariable/executeSetVariable.ts
+++ b/apps/viewer/src/features/blocks/logic/setVariable/executeSetVariable.ts
@@ -10,14 +10,14 @@ export const executeSetVariable = (
state: SessionState,
block: SetVariableBlock
): ExecuteLogicResponse => {
- const { variables } = state.typebot
+ const { variables } = state.typebotsQueue[0].typebot
if (!block.options?.variableId)
return {
outgoingEdgeId: block.outgoingEdgeId,
}
- const expressionToEvaluate = getExpressionToEvaluate(state.result.id)(
- block.options
- )
+ const expressionToEvaluate = getExpressionToEvaluate(
+ state.typebotsQueue[0].resultId
+ )(block.options)
const isCustomValue = !block.options.type || block.options.type === 'Custom'
if (
expressionToEvaluate &&
@@ -25,7 +25,7 @@ export const executeSetVariable = (
block.options.type === 'Moment of the day')
) {
const scriptToExecute = parseScriptToExecuteClientSideAction(
- state.typebot.variables,
+ variables,
expressionToEvaluate
)
return {
diff --git a/apps/viewer/src/features/blocks/logic/typebotLink/executeTypebotLink.ts b/apps/viewer/src/features/blocks/logic/typebotLink/executeTypebotLink.ts
index 058d26fb3..3d0f83799 100644
--- a/apps/viewer/src/features/blocks/logic/typebotLink/executeTypebotLink.ts
+++ b/apps/viewer/src/features/blocks/logic/typebotLink/executeTypebotLink.ts
@@ -6,19 +6,24 @@ import prisma from '@/lib/prisma'
import {
TypebotLinkBlock,
SessionState,
- TypebotInSession,
Variable,
ReplyLog,
+ Typebot,
+ VariableWithValue,
+ Edge,
} from '@typebot.io/schemas'
-import { byId } from '@typebot.io/lib'
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 (
state: SessionState,
block: TypebotLinkBlock
): Promise => {
const logs: ReplyLog[] = []
- if (!block.options.typebotId) {
+ const typebotId = block.options.typebotId
+ if (!typebotId) {
logs.push({
status: 'error',
description: `Failed to link typebot`,
@@ -26,7 +31,7 @@ export const executeTypebotLink = async (
})
return { outgoingEdgeId: block.outgoingEdgeId, logs }
}
- const linkedTypebot = await getLinkedTypebot(state, block.options.typebotId)
+ const linkedTypebot = await fetchTypebot(state, typebotId)
if (!linkedTypebot) {
logs.push({
status: 'error',
@@ -35,12 +40,17 @@ export const executeTypebotLink = async (
})
return { outgoingEdgeId: block.outgoingEdgeId, logs }
}
- let newSessionState = addLinkedTypebotToState(state, block, linkedTypebot)
+ let newSessionState = await addLinkedTypebotToState(
+ state,
+ block,
+ linkedTypebot
+ )
const nextGroupId =
block.options.groupId ??
- linkedTypebot.groups.find((b) => b.blocks.some((s) => s.type === 'start'))
- ?.id
+ linkedTypebot.groups.find((group) =>
+ group.blocks.some((block) => block.type === 'start')
+ )?.id
if (!nextGroupId) {
logs.push({
status: 'error',
@@ -60,76 +70,123 @@ export const executeTypebotLink = async (
}
}
-const addLinkedTypebotToState = (
+const addLinkedTypebotToState = async (
state: SessionState,
block: TypebotLinkBlock,
- linkedTypebot: TypebotInSession
-): SessionState => {
- const incomingVariables = fillVariablesWithExistingValues(
- linkedTypebot.variables,
- state.typebot.variables
- )
+ linkedTypebot: Pick
+): Promise => {
+ const currentTypebotInQueue = state.typebotsQueue[0]
+ const isPreview = isNotDefined(currentTypebotInQueue.resultId)
+
+ 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 {
...state,
- typebot: {
- ...state.typebot,
- groups: [...state.typebot.groups, ...linkedTypebot.groups],
- variables: [...state.typebot.variables, ...incomingVariables],
- edges: [...state.typebot.edges, ...linkedTypebot.edges],
+ typebotsQueue: [
+ {
+ typebot: {
+ ...linkedTypebot,
+ 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: {
- typebots: [
- ...state.linkedTypebots.typebots.filter(
- (existingTypebots) => existingTypebots.id !== linkedTypebot.id
- ),
- ],
- queue: block.outgoingEdgeId
- ? [
- ...state.linkedTypebots.queue,
- { edgeId: block.outgoingEdgeId, typebotId: state.currentTypebotId },
- ]
- : state.linkedTypebots.queue,
+ to: {
+ groupId: nextBlockInGroup.groupId,
+ blockId: nextBlockInGroup.id,
},
- currentTypebotId: linkedTypebot.id,
}
}
const fillVariablesWithExistingValues = (
variables: Variable[],
variablesWithValues: Variable[]
-): Variable[] =>
- variables.map((variable) => {
- const matchedVariable = variablesWithValues.find(
- (variableWithValue) => variableWithValue.name === variable.name
- )
+): VariableWithValue[] =>
+ variables
+ .map((variable) => {
+ const matchedVariable = variablesWithValues.find(
+ (variableWithValue) => variableWithValue.name === variable.name
+ )
- return {
- ...variable,
- value: matchedVariable?.value ?? variable.value,
- }
- })
+ return {
+ ...variable,
+ value: matchedVariable?.value,
+ }
+ })
+ .filter((variable) => isDefined(variable.value)) as VariableWithValue[]
-const getLinkedTypebot = async (
- state: SessionState,
- typebotId: string
-): Promise => {
- 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 => {
+const fetchTypebot = async (state: SessionState, typebotId: string) => {
+ const { typebot: typebotInState, resultId } = state.typebotsQueue[0]
+ const isPreview = !resultId
+ if (typebotId === 'current') return typebotInState
if (isPreview) {
const typebot = await prisma.typebot.findUnique({
where: { id: typebotId },
@@ -140,7 +197,7 @@ const fetchTypebot = async (
variables: true,
},
})
- return typebot as TypebotInSession
+ return typebot as Pick
}
const typebot = await prisma.publicTypebot.findUnique({
where: { typebotId },
@@ -155,5 +212,5 @@ const fetchTypebot = async (
return {
...typebot,
id: typebotId,
- } as TypebotInSession
+ } as Pick
}
diff --git a/apps/viewer/src/features/blocks/logic/typebotLink/typebotLink.spec.ts b/apps/viewer/src/features/blocks/logic/typebotLink/typebotLink.spec.ts
index c1e06e888..4383ecbc5 100644
--- a/apps/viewer/src/features/blocks/logic/typebotLink/typebotLink.spec.ts
+++ b/apps/viewer/src/features/blocks/logic/typebotLink/typebotLink.spec.ts
@@ -3,6 +3,7 @@ import test, { expect } from '@playwright/test'
import { importTypebotInDatabase } from '@typebot.io/lib/playwright/databaseActions'
const typebotId = 'cl0ibhi7s0018n21aarlmg0cm'
+const typebotWithMergeDisabledId = 'cl0ibhi7s0018n21aarlag0cm'
const linkedTypebotId = 'cl0ibhv8d0130n21aw8doxhj5'
test.beforeAll(async () => {
@@ -11,6 +12,13 @@ test.beforeAll(async () => {
getTestAsset('typebots/linkTypebots/1.json'),
{ id: typebotId, publicId: `${typebotId}-public` }
)
+ await importTypebotInDatabase(
+ getTestAsset('typebots/linkTypebots/1-merge-disabled.json'),
+ {
+ id: typebotWithMergeDisabledId,
+ publicId: `${typebotWithMergeDisabledId}-public`,
+ }
+ )
await importTypebotInDatabase(
getTestAsset('typebots/linkTypebots/2.json'),
{ 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 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()
+ })
+})
diff --git a/apps/viewer/src/features/blocks/logic/wait/executeWait.ts b/apps/viewer/src/features/blocks/logic/wait/executeWait.ts
index 0a02b7bfa..95e83d368 100644
--- a/apps/viewer/src/features/blocks/logic/wait/executeWait.ts
+++ b/apps/viewer/src/features/blocks/logic/wait/executeWait.ts
@@ -3,9 +3,10 @@ import { parseVariables } from '@/features/variables/parseVariables'
import { SessionState, WaitBlock } from '@typebot.io/schemas'
export const executeWait = (
- { typebot: { variables } }: SessionState,
+ state: SessionState,
block: WaitBlock
): ExecuteLogicResponse => {
+ const { variables } = state.typebotsQueue[0].typebot
if (!block.options.secondsToWaitFor)
return { outgoingEdgeId: block.outgoingEdgeId }
const parsedSecondsToWaitFor = safeParseInt(
diff --git a/apps/viewer/src/features/chat/api/sendMessage.ts b/apps/viewer/src/features/chat/api/sendMessage.ts
index 462a43b5f..a183ec47f 100644
--- a/apps/viewer/src/features/chat/api/sendMessage.ts
+++ b/apps/viewer/src/features/chat/api/sendMessage.ts
@@ -7,7 +7,6 @@ import {
IntegrationBlockType,
PixelBlock,
ReplyLog,
- ResultInSession,
sendMessageInputSchema,
SessionState,
StartParams,
@@ -148,22 +147,19 @@ const startSession = async (
: prefilledVariables
const initialState: SessionState = {
- typebot: {
- id: typebot.id,
- groups: typebot.groups,
- edges: typebot.edges,
- variables: startVariables,
- },
- linkedTypebots: {
- typebots: [],
- queue: [],
- },
- result: {
- id: result?.id,
- variables: result?.variables ?? [],
- answers: result?.answers ?? [],
- },
- currentTypebotId: typebot.id,
+ version: '2',
+ typebotsQueue: [
+ {
+ resultId: result?.id,
+ typebot: {
+ id: typebot.id,
+ groups: typebot.groups,
+ edges: typebot.edges,
+ variables: startVariables,
+ },
+ answers: [],
+ },
+ ],
dynamicTheme: parseDynamicThemeInState(typebot.theme),
isStreamEnabled: startParams.isStreamEnabled,
}
@@ -212,12 +208,12 @@ const startSession = async (
startClientSideAction.length > 0 ? startClientSideAction : undefined,
typebot: {
id: typebot.id,
- settings: deepParseVariables(newSessionState.typebot.variables)(
- typebot.settings
- ),
- theme: deepParseVariables(newSessionState.typebot.variables)(
- typebot.theme
- ),
+ settings: deepParseVariables(
+ newSessionState.typebotsQueue[0].typebot.variables
+ )(typebot.settings),
+ theme: deepParseVariables(
+ newSessionState.typebotsQueue[0].typebot.variables
+ )(typebot.theme),
},
dynamicTheme: parseDynamicThemeReply(newSessionState),
logs: startLogs.length > 0 ? startLogs : undefined,
@@ -239,12 +235,12 @@ const startSession = async (
sessionId: session.id,
typebot: {
id: typebot.id,
- settings: deepParseVariables(newSessionState.typebot.variables)(
- typebot.settings
- ),
- theme: deepParseVariables(newSessionState.typebot.variables)(
- typebot.theme
- ),
+ settings: deepParseVariables(
+ newSessionState.typebotsQueue[0].typebot.variables
+ )(typebot.settings),
+ theme: deepParseVariables(
+ newSessionState.typebotsQueue[0].typebot.variables
+ )(typebot.theme),
},
messages,
input,
@@ -319,7 +315,7 @@ const getResult = async ({
if (isPreview) return
const existingResult =
resultId && isRememberUserEnabled
- ? ((await findResult({ id: resultId })) as ResultInSession)
+ ? await findResult({ id: resultId })
: undefined
const prefilledVariableWithValue = prefilledVariables.filter(
@@ -341,7 +337,7 @@ const getResult = async ({
return {
id: existingResult?.id ?? createId(),
variables: updatedResult.variables,
- answers: existingResult?.answers,
+ answers: existingResult?.answers ?? [],
}
}
@@ -369,10 +365,10 @@ const parseDynamicThemeReply = (
): ChatReply['dynamicTheme'] => {
if (!state?.dynamicTheme) return
return {
- hostAvatarUrl: parseVariables(state?.typebot.variables)(
+ hostAvatarUrl: parseVariables(state.typebotsQueue[0].typebot.variables)(
state.dynamicTheme.hostAvatarUrl
),
- guestAvatarUrl: parseVariables(state?.typebot.variables)(
+ guestAvatarUrl: parseVariables(state.typebotsQueue[0].typebot.variables)(
state.dynamicTheme.guestAvatarUrl
),
}
diff --git a/apps/viewer/src/features/chat/api/updateTypebotInSession.ts b/apps/viewer/src/features/chat/api/updateTypebotInSession.ts
index 1289fb9f5..f199f4ce5 100644
--- a/apps/viewer/src/features/chat/api/updateTypebotInSession.ts
+++ b/apps/viewer/src/features/chat/api/updateTypebotInSession.ts
@@ -3,7 +3,12 @@ import { TRPCError } from '@trpc/server'
import { z } from 'zod'
import { getSession } from '../queries/getSession'
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
.meta({
@@ -32,7 +37,7 @@ export const updateTypebotInSession = publicProcedure
const publicTypebot = (await prisma.publicTypebot.findFirst({
where: {
typebot: {
- id: session.state.typebot.id,
+ id: session.state.typebotsQueue[0].typebot.id,
OR: [
{
workspace: {
@@ -74,21 +79,28 @@ const updateSessionState = (
newTypebot: Pick
): SessionState => ({
...currentState,
- typebot: {
- ...currentState.typebot,
- edges: newTypebot.edges,
- variables: updateVariablesInSession(
- currentState.typebot.variables,
- newTypebot.variables
- ),
- groups: newTypebot.groups,
- },
+ typebotsQueue: currentState.typebotsQueue.map((typebotInQueue, index) =>
+ index === 0
+ ? {
+ ...typebotInQueue,
+ typebot: {
+ ...typebotInQueue.typebot,
+ edges: newTypebot.edges,
+ groups: newTypebot.groups,
+ variables: updateVariablesInSession(
+ typebotInQueue.typebot.variables,
+ newTypebot.variables
+ ),
+ },
+ }
+ : typebotInQueue
+ ),
})
const updateVariablesInSession = (
- currentVariables: SessionState['typebot']['variables'],
+ currentVariables: Variable[],
newVariables: Typebot['variables']
-): SessionState['typebot']['variables'] => [
+): Variable[] => [
...currentVariables,
...newVariables.filter(
(newVariable) =>
diff --git a/apps/viewer/src/features/chat/helpers/addEdgeToTypebot.ts b/apps/viewer/src/features/chat/helpers/addEdgeToTypebot.ts
index a83489ca5..83c12a519 100644
--- a/apps/viewer/src/features/chat/helpers/addEdgeToTypebot.ts
+++ b/apps/viewer/src/features/chat/helpers/addEdgeToTypebot.ts
@@ -6,10 +6,17 @@ export const addEdgeToTypebot = (
edge: Edge
): SessionState => ({
...state,
- typebot: {
- ...state.typebot,
- edges: [...state.typebot.edges, edge],
- },
+ typebotsQueue: state.typebotsQueue.map((typebot, index) =>
+ index === 0
+ ? {
+ ...typebot,
+ typebot: {
+ ...typebot.typebot,
+ edges: [...typebot.typebot.edges, edge],
+ },
+ }
+ : typebot
+ ),
})
export const createPortalEdge = ({ to }: Pick) => ({
diff --git a/apps/viewer/src/features/chat/helpers/continueBotFlow.ts b/apps/viewer/src/features/chat/helpers/continueBotFlow.ts
index 6a439dfef..d607f472b 100644
--- a/apps/viewer/src/features/chat/helpers/continueBotFlow.ts
+++ b/apps/viewer/src/features/chat/helpers/continueBotFlow.ts
@@ -1,5 +1,6 @@
import { TRPCError } from '@trpc/server'
import {
+ AnswerInSessionState,
Block,
BlockType,
BubbleBlockType,
@@ -8,7 +9,6 @@ import {
InputBlockType,
IntegrationBlockType,
LogicBlockType,
- ResultInSession,
SessionState,
SetVariableBlock,
WebhookBlock,
@@ -35,7 +35,7 @@ export const continueBotFlow =
reply?: string
): Promise => {
let newSessionState = { ...state }
- const group = state.typebot.groups.find(
+ const group = state.typebotsQueue[0].typebot.groups.find(
(group) => group.id === state.currentBlock?.groupId
)
const blockIndex =
@@ -52,7 +52,7 @@ export const continueBotFlow =
})
if (block.type === LogicBlockType.SET_VARIABLE) {
- const existingVariable = state.typebot.variables.find(
+ const existingVariable = state.typebotsQueue[0].typebot.variables.find(
byId(block.options.variableId)
)
if (existingVariable && reply) {
@@ -103,7 +103,8 @@ export const continueBotFlow =
formattedReply
)
const itemId = nextEdgeId
- ? state.typebot.edges.find(byId(nextEdgeId))?.from.itemId
+ ? newSessionState.typebotsQueue[0].typebot.edges.find(byId(nextEdgeId))
+ ?.from.itemId
: undefined
newSessionState = await processAndSaveAnswer(
state,
@@ -128,7 +129,7 @@ export const continueBotFlow =
}
}
- if (!nextEdgeId && state.linkedTypebots.queue.length === 0)
+ if (!nextEdgeId && state.typebotsQueue.length === 1)
return {
messages: [],
newSessionState,
@@ -138,7 +139,9 @@ export const continueBotFlow =
const nextGroup = getNextGroup(newSessionState)(nextEdgeId)
- if (!nextGroup)
+ newSessionState = nextGroup.newSessionState
+
+ if (!nextGroup.group)
return {
messages: [],
newSessionState,
@@ -168,7 +171,7 @@ const saveVariableValueIfAny =
(state: SessionState, block: InputBlock) =>
(reply: string): SessionState => {
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
)
if (!foundVariable) return state
@@ -235,34 +238,47 @@ const saveAnswer =
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)({
- blockId: block.id,
- variableId: block.options.variableId ?? null,
- content: reply,
+ key: key ?? block.id,
+ value: reply,
})
}
const setNewAnswerInState =
- (state: SessionState) => (newAnswer: ResultInSession['answers'][number]) => {
- const newAnswers = state.result.answers
- .filter((answer) => answer.blockId !== newAnswer.blockId)
+ (state: SessionState) => (newAnswer: AnswerInSessionState) => {
+ const answers = state.typebotsQueue[0].answers
+ const newAnswers = answers
+ .filter((answer) => answer.key !== newAnswer.key)
.concat(newAnswer)
return {
...state,
- result: {
- ...state.result,
- answers: newAnswers,
- },
+ typebotsQueue: state.typebotsQueue.map((typebot, index) =>
+ index === 0
+ ? {
+ ...typebot,
+ answers: newAnswers,
+ }
+ : typebot
+ ),
} satisfies SessionState
}
const getOutgoingEdgeId =
- ({ typebot: { variables } }: Pick) =>
+ (state: Pick) =>
(
block: InputBlock | SetVariableBlock | OpenAIBlock | WebhookBlock,
reply: string | undefined
) => {
+ const variables = state.typebotsQueue[0].typebot.variables
if (
block.type === InputBlockType.CHOICE &&
!block.options.isMultipleChoice &&
diff --git a/apps/viewer/src/features/chat/helpers/executeGroup.ts b/apps/viewer/src/features/chat/helpers/executeGroup.ts
index 45e8899f2..30e6e12e6 100644
--- a/apps/viewer/src/features/chat/helpers/executeGroup.ts
+++ b/apps/viewer/src/features/chat/helpers/executeGroup.ts
@@ -7,6 +7,7 @@ import {
InputBlockType,
RuntimeOptions,
SessionState,
+ Variable,
} from '@typebot.io/schemas'
import {
isBubbleBlock,
@@ -47,7 +48,9 @@ export const executeGroup =
if (isBubbleBlock(block)) {
messages.push(
- parseBubbleBlock(newSessionState.typebot.variables)(block)
+ parseBubbleBlock(newSessionState.typebotsQueue[0].typebot.variables)(
+ block
+ )
)
lastBubbleBlockId = block.id
continue
@@ -118,14 +121,14 @@ export const executeGroup =
}
}
- if (!nextEdgeId)
+ if (!nextEdgeId && state.typebotsQueue.length === 1)
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 }
}
@@ -141,7 +144,7 @@ export const executeGroup =
}
const computeRuntimeOptions =
- (state: Pick) =>
+ (state: SessionState) =>
(block: InputBlock): Promise | undefined => {
switch (block.type) {
case InputBlockType.PAYMENT: {
@@ -151,7 +154,7 @@ const computeRuntimeOptions =
}
const getPrefilledInputValue =
- (variables: SessionState['typebot']['variables']) => (block: InputBlock) => {
+ (variables: Variable[]) => (block: InputBlock) => {
const variableValue = variables.find(
(variable) =>
variable.id === block.options.variableId && isDefined(variable.value)
@@ -161,7 +164,7 @@ const getPrefilledInputValue =
}
const parseBubbleBlock =
- (variables: SessionState['typebot']['variables']) =>
+ (variables: Variable[]) =>
(block: BubbleBlock): ChatReply['messages'][0] => {
switch (block.type) {
case BubbleBlockType.TEXT:
@@ -197,15 +200,17 @@ const parseInput =
}
case InputBlockType.PICTURE_CHOICE: {
return injectVariableValuesInPictureChoiceBlock(
- state.typebot.variables
+ state.typebotsQueue[0].typebot.variables
)(block)
}
case InputBlockType.NUMBER: {
- const parsedBlock = deepParseVariables(state.typebot.variables)({
+ const parsedBlock = deepParseVariables(
+ state.typebotsQueue[0].typebot.variables
+ )({
...block,
- prefilledValue: getPrefilledInputValue(state.typebot.variables)(
- block
- ),
+ prefilledValue: getPrefilledInputValue(
+ state.typebotsQueue[0].typebot.variables
+ )(block),
})
return {
...parsedBlock,
@@ -224,12 +229,12 @@ const parseInput =
}
}
default: {
- return deepParseVariables(state.typebot.variables)({
+ return deepParseVariables(state.typebotsQueue[0].typebot.variables)({
...block,
runtimeOptions: await computeRuntimeOptions(state)(block),
- prefilledValue: getPrefilledInputValue(state.typebot.variables)(
- block
- ),
+ prefilledValue: getPrefilledInputValue(
+ state.typebotsQueue[0].typebot.variables
+ )(block),
})
}
}
diff --git a/apps/viewer/src/features/chat/helpers/getNextGroup.ts b/apps/viewer/src/features/chat/helpers/getNextGroup.ts
index fd8747313..2b2cb4618 100644
--- a/apps/viewer/src/features/chat/helpers/getNextGroup.ts
+++ b/apps/viewer/src/features/chat/helpers/getNextGroup.ts
@@ -2,37 +2,81 @@ import { byId } from '@typebot.io/lib'
import { Group, SessionState } from '@typebot.io/schemas'
export type NextGroup = {
- group: Group
- updatedContext?: SessionState
+ group?: Group
+ newSessionState: SessionState
}
export const getNextGroup =
(state: SessionState) =>
- (edgeId?: string): NextGroup | null => {
- const { typebot } = state
- const nextEdge = typebot.edges.find(byId(edgeId))
+ (edgeId?: string): NextGroup => {
+ const nextEdge = state.typebotsQueue[0].typebot.edges.find(byId(edgeId))
if (!nextEdge) {
- if (state.linkedTypebots.queue.length > 0) {
- const nextEdgeId = state.linkedTypebots.queue[0].edgeId
- const updatedContext = {
+ if (state.typebotsQueue.length > 1) {
+ const nextEdgeId = state.typebotsQueue[0].edgeIdToTriggerWhenDone
+ const isMergingWithParent = state.typebotsQueue[0].isMergingWithParent
+ const newSessionState = {
...state,
- linkedBotQueue: state.linkedTypebots.queue.slice(1),
- }
- const nextGroup = getNextGroup(updatedContext)(nextEdgeId)
- if (!nextGroup) return null
+ typebotsQueue: [
+ {
+ ...state.typebotsQueue[1],
+ 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 {
...nextGroup,
- updatedContext,
+ newSessionState,
}
}
- return null
+ return {
+ newSessionState: state,
+ }
}
- const nextGroup = typebot.groups.find(byId(nextEdge.to.groupId))
- if (!nextGroup) return null
+ const nextGroup = state.typebotsQueue[0].typebot.groups.find(
+ byId(nextEdge.to.groupId)
+ )
+ if (!nextGroup)
+ return {
+ newSessionState: state,
+ }
const startBlockIndex = nextEdge.to.blockId
? nextGroup.blocks.findIndex(byId(nextEdge.to.blockId))
: 0
return {
- group: { ...nextGroup, blocks: nextGroup.blocks.slice(startBlockIndex) },
+ group: {
+ ...nextGroup,
+ blocks: nextGroup.blocks.slice(startBlockIndex),
+ },
+ newSessionState: state,
}
}
diff --git a/apps/viewer/src/features/chat/helpers/saveStateToDatabase.ts b/apps/viewer/src/features/chat/helpers/saveStateToDatabase.ts
index 4bb9c5913..fc38ed72a 100644
--- a/apps/viewer/src/features/chat/helpers/saveStateToDatabase.ts
+++ b/apps/viewer/src/features/chat/helpers/saveStateToDatabase.ts
@@ -11,6 +11,7 @@ type Props = {
logs: ChatReply['logs']
clientSideActions: ChatReply['clientSideActions']
}
+
export const saveStateToDatabase = async ({
session: { state, id },
input,
@@ -21,25 +22,30 @@ export const saveStateToDatabase = async ({
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(
(action) => 'setVariable' in action
)
+
+ const answers = state.typebotsQueue[0].answers
+
await upsertResult({
- state,
+ resultId,
+ typebot: state.typebotsQueue[0].typebot,
isCompleted: Boolean(
- !input &&
- !containsSetVariableClientSideAction &&
- state.result.answers.length > 0
+ !input && !containsSetVariableClientSideAction && answers.length > 0
),
+ hasStarted: answers.length > 0,
})
if (logs && logs.length > 0)
await saveLogs(
logs.map((log) => ({
...log,
- resultId: state.result.id as string,
+ resultId,
details: formatLogDetails(log.details),
}))
)
diff --git a/apps/viewer/src/features/chat/helpers/startBotFlow.ts b/apps/viewer/src/features/chat/helpers/startBotFlow.ts
index be8c5fdb4..055b45ac7 100644
--- a/apps/viewer/src/features/chat/helpers/startBotFlow.ts
+++ b/apps/viewer/src/features/chat/helpers/startBotFlow.ts
@@ -8,7 +8,7 @@ export const startBotFlow = async (
startGroupId?: string
): Promise => {
if (startGroupId) {
- const group = state.typebot.groups.find(
+ const group = state.typebotsQueue[0].typebot.groups.find(
(group) => group.id === startGroupId
)
if (!group)
@@ -18,9 +18,10 @@ export const startBotFlow = async (
})
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 }
const nextGroup = getNextGroup(state)(firstEdgeId)
- if (!nextGroup) return { messages: [], newSessionState: state }
+ if (!nextGroup.group) return { messages: [], newSessionState: state }
return executeGroup(state)(nextGroup.group)
}
diff --git a/apps/viewer/src/features/chat/queries/createResultIfNotExist.ts b/apps/viewer/src/features/chat/queries/createResultIfNotExist.ts
new file mode 100644
index 000000000..ffc087b67
--- /dev/null
+++ b/apps/viewer/src/features/chat/queries/createResultIfNotExist.ts
@@ -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),
+ },
+ ],
+ })
+}
diff --git a/apps/viewer/src/features/chat/queries/findResult.ts b/apps/viewer/src/features/chat/queries/findResult.ts
index d261d6358..b0ace7ed9 100644
--- a/apps/viewer/src/features/chat/queries/findResult.ts
+++ b/apps/viewer/src/features/chat/queries/findResult.ts
@@ -1,4 +1,5 @@
import prisma from '@/lib/prisma'
+import { Answer, Result } from '@typebot.io/schemas'
type Props = {
id: string
@@ -9,6 +10,18 @@ export const findResult = ({ id }: Props) =>
select: {
id: true,
variables: true,
- answers: { select: { blockId: true, variableId: true, content: true } },
+ hasStarted: true,
+ answers: {
+ select: {
+ content: true,
+ blockId: true,
+ variableId: true,
+ },
+ },
},
- })
+ }) as Promise<
+ | (Pick & {
+ answers: Pick[]
+ })
+ | null
+ >
diff --git a/apps/viewer/src/features/chat/queries/getSession.ts b/apps/viewer/src/features/chat/queries/getSession.ts
index 2ac9d29c0..ef20425e6 100644
--- a/apps/viewer/src/features/chat/queries/getSession.ts
+++ b/apps/viewer/src/features/chat/queries/getSession.ts
@@ -1,12 +1,13 @@
import prisma from '@/lib/prisma'
-import { ChatSession } from '@typebot.io/schemas'
+import { ChatSession, sessionStateSchema } from '@typebot.io/schemas'
export const getSession = async (
sessionId: string
): Promise | null> => {
- const session = (await prisma.chatSession.findUnique({
+ const session = await prisma.chatSession.findUnique({
where: { id: sessionId },
select: { id: true, state: true },
- })) as Pick | null
- return session
+ })
+ if (!session) return null
+ return { ...session, state: sessionStateSchema.parse(session.state) }
}
diff --git a/apps/viewer/src/features/chat/queries/upsertAnswer.ts b/apps/viewer/src/features/chat/queries/upsertAnswer.ts
index 9b1ca7e18..76a5c8ba3 100644
--- a/apps/viewer/src/features/chat/queries/upsertAnswer.ts
+++ b/apps/viewer/src/features/chat/queries/upsertAnswer.ts
@@ -12,12 +12,13 @@ type Props = {
state: SessionState
}
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) {
answer.storageUsed = await computeStorageUsed(reply)
}
const where = {
- resultId: state.result.id,
+ resultId,
blockId: block.id,
groupId: block.groupId,
}
@@ -37,7 +38,7 @@ export const upsertAnswer = async ({ answer, reply, block, state }: Props) => {
},
})
return prisma.answer.createMany({
- data: [{ ...answer, resultId: state.result.id }],
+ data: [{ ...answer, resultId }],
})
}
diff --git a/apps/viewer/src/features/chat/queries/upsertResult.ts b/apps/viewer/src/features/chat/queries/upsertResult.ts
index cdbc15d9c..88a713686 100644
--- a/apps/viewer/src/features/chat/queries/upsertResult.ts
+++ b/apps/viewer/src/features/chat/queries/upsertResult.ts
@@ -1,33 +1,43 @@
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 = {
- state: SessionState
+ resultId: string
+ typebot: TypebotInSession
+ hasStarted: 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({
- where: { id: state.result.id },
+ where: { id: resultId },
select: { id: true },
})
+ const variablesWithValue = getDefinedVariables(typebot.variables)
+
if (existingResult) {
return prisma.result.updateMany({
- where: { id: state.result.id },
+ where: { id: resultId },
data: {
isCompleted: isCompleted ? true : undefined,
- hasStarted: state.result.answers.length > 0 ? true : undefined,
- variables: state.result.variables,
+ hasStarted,
+ variables: variablesWithValue,
},
})
}
return prisma.result.createMany({
data: [
{
- id: state.result.id,
- typebotId: state.typebot.id,
+ id: resultId,
+ typebotId: typebot.id,
isCompleted: isCompleted ? true : false,
- hasStarted: state.result.answers.length > 0 ? true : undefined,
- variables: state.result.variables,
+ hasStarted,
+ variables: variablesWithValue,
},
],
})
diff --git a/apps/viewer/src/features/variables/updateVariables.ts b/apps/viewer/src/features/variables/updateVariables.ts
index 35050950d..6bfab1934 100644
--- a/apps/viewer/src/features/variables/updateVariables.ts
+++ b/apps/viewer/src/features/variables/updateVariables.ts
@@ -1,8 +1,6 @@
-import { isDefined } from '@typebot.io/lib'
import {
SessionState,
VariableWithUnknowValue,
- VariableWithValue,
Variable,
} from '@typebot.io/schemas'
import { safeStringify } from '@typebot.io/lib/safeStringify'
@@ -11,40 +9,23 @@ export const updateVariables =
(state: SessionState) =>
(newVariables: VariableWithUnknowValue[]): SessionState => ({
...state,
- typebot: {
- ...state.typebot,
- variables: updateTypebotVariables(state)(newVariables),
- },
- result: {
- ...state.result,
- variables: updateResultVariables(state)(newVariables),
- },
+ typebotsQueue: state.typebotsQueue.map((typebotInQueue, index) =>
+ index === 0
+ ? {
+ ...typebotInQueue,
+ typebot: {
+ ...typebotInQueue.typebot,
+ variables: updateTypebotVariables(typebotInQueue.typebot)(
+ newVariables
+ ),
+ },
+ }
+ : typebotInQueue
+ ),
})
-const updateResultVariables =
- ({ result }: Pick) =>
- (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 =
- ({ typebot }: Pick) =>
+ (typebot: { variables: Variable[] }) =>
(newVariables: VariableWithUnknowValue[]): Variable[] => {
const serializedNewVariables = newVariables.map((variable) => ({
...variable,
diff --git a/apps/viewer/src/pages/api/integrations/openai/streamer.ts b/apps/viewer/src/pages/api/integrations/openai/streamer.ts
index b15a3d14d..c6bd98e08 100644
--- a/apps/viewer/src/pages/api/integrations/openai/streamer.ts
+++ b/apps/viewer/src/pages/api/integrations/openai/streamer.ts
@@ -41,7 +41,7 @@ const handler = async (req: Request) => {
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
)
const blockIndex =
diff --git a/apps/viewer/src/pages/api/typebots/[typebotId]/blocks/[blockId]/executeWebhook.ts b/apps/viewer/src/pages/api/typebots/[typebotId]/blocks/[blockId]/executeWebhook.ts
index 6f9afbab4..0879edabd 100644
--- a/apps/viewer/src/pages/api/typebots/[typebotId]/blocks/[blockId]/executeWebhook.ts
+++ b/apps/viewer/src/pages/api/typebots/[typebotId]/blocks/[blockId]/executeWebhook.ts
@@ -85,131 +85,116 @@ const checkIfBodyIsAVariable = (body: string) => /^{{.+}}$/.test(body)
export const executeWebhook =
(typebot: Typebot) =>
- async ({
- webhook,
- variables,
- groupId,
- resultValues,
- resultId,
- parentTypebotIds = [],
- }: {
- webhook: Webhook
- variables: Variable[]
- groupId: string
- resultValues?: ResultValues
- resultId?: string
- parentTypebotIds: string[]
- }): Promise => {
- if (!webhook.url || !webhook.method)
- return {
- statusCode: 400,
- 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)
+ async ({
+ webhook,
+ variables,
+ groupId,
+ resultValues,
+ resultId,
+ parentTypebotIds = [],
+ }: {
+ webhook: Webhook
+ variables: Variable[]
+ groupId: string
+ resultValues?: ResultValues
+ resultId?: string
+ parentTypebotIds: string[]
+ }): Promise => {
+ if (!webhook.url || !webhook.method)
+ return {
+ statusCode: 400,
+ data: { message: `Webhook doesn't have url or method` },
}
- const headers = convertKeyValueTableToObject(webhook.headers, variables) as
- | Headers
- | undefined
- const queryParams = stringify(
- convertKeyValueTableToObject(webhook.queryParams, variables)
- )
- const contentType = headers ? headers['Content-Type'] : undefined
- const linkedTypebotsParents = await fetchLinkedTypebots({
- isPreview: !('typebotId' in typebot),
- typebotIds: parentTypebotIds,
- })
- 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(
+ 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
+ | Headers
+ | undefined
+ const queryParams = stringify(
+ convertKeyValueTableToObject(webhook.queryParams, variables)
+ )
+ const contentType = headers ? headers['Content-Type'] : undefined
+ const linkedTypebotsParents = await fetchLinkedTypebots({
+ isPreview: !('typebotId' in typebot),
+ typebotIds: parentTypebotIds,
+ })
+ 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, {
escapeForJson: !checkIfBodyIsAVariable(bodyContent),
})(bodyContent)
)
- : { data: undefined, isJson: false }
+ : { data: undefined, isJson: false }
- const request = {
- url: parseVariables(variables)(
- webhook.url + (queryParams !== '' ? `?${queryParams}` : '')
- ),
- method: webhook.method as Method,
- headers,
- ...basicAuth,
- json:
- !contentType?.includes('x-www-form-urlencoded') && body && isJson
- ? body
- : undefined,
- form:
- contentType?.includes('x-www-form-urlencoded') && body
- ? body
- : undefined,
- body: body && !isJson ? body : undefined,
- }
- try {
- const response = await got(request.url, omit(request, 'url'))
- await saveSuccessLog({
- resultId,
- message: 'Webhook successfuly executed.',
- details: {
- statusCode: response.statusCode,
- request,
- response: safeJsonParse(response.body).data,
- },
- })
- return {
+ const request = {
+ url: parseVariables(variables)(
+ webhook.url + (queryParams !== '' ? `?${queryParams}` : '')
+ ),
+ method: webhook.method as Method,
+ headers,
+ ...basicAuth,
+ json:
+ !contentType?.includes('x-www-form-urlencoded') && body && isJson
+ ? body
+ : undefined,
+ form:
+ contentType?.includes('x-www-form-urlencoded') && body
+ ? body
+ : undefined,
+ body: body && !isJson ? body : undefined,
+ }
+ try {
+ const response = await got(request.url, omit(request, 'url'))
+ await saveSuccessLog({
+ resultId,
+ message: 'Webhook successfuly executed.',
+ details: {
statusCode: response.statusCode,
- data: safeJsonParse(response.body).data,
- }
- } catch (error) {
- if (error instanceof HTTPError) {
- const response = {
- statusCode: error.response.statusCode,
- data: safeJsonParse(error.response.body as string).data,
- }
- await saveErrorLog({
- resultId,
- message: 'Webhook returned an error',
- details: {
- request,
- response,
- },
- })
- return response
- }
+ request,
+ response: safeJsonParse(response.body).data,
+ },
+ })
+ return {
+ statusCode: response.statusCode,
+ data: safeJsonParse(response.body).data,
+ }
+ } catch (error) {
+ if (error instanceof HTTPError) {
const response = {
- statusCode: 500,
- data: { message: `Error from Typebot server: ${error}` },
+ statusCode: error.response.statusCode,
+ data: safeJsonParse(error.response.body as string).data,
}
- console.error(error)
await saveErrorLog({
resultId,
- message: 'Webhook failed to execute',
+ message: 'Webhook returned an error',
details: {
request,
response,
@@ -217,36 +202,66 @@ export const executeWebhook =
})
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 =
(
typebot: Pick,
linkedTypebots: (Typebot | PublicTypebot)[]
) =>
- async ({
- body,
- resultValues,
- groupId,
- variables,
- }: {
- body?: string | null
- resultValues?: ResultValues
- groupId: string
- variables: Variable[]
- }): Promise => {
- if (!body) return
- return body === '{{state}}'
- ? JSON.stringify(
+ async ({
+ body,
+ resultValues,
+ groupId,
+ variables,
+ }: {
+ body?: string | null
+ resultValues?: ResultValues
+ groupId: string
+ variables: Variable[]
+ }): Promise => {
+ if (!body) return
+ return body === '{{state}}'
+ ? JSON.stringify(
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)(
- groupId,
- variables
- )
+ groupId,
+ variables
+ )
)
- : body
- }
+ : body
+ }
const convertKeyValueTableToObject = (
keyValues: KeyValue[] | undefined,
diff --git a/apps/viewer/src/pages/api/typebots/[typebotId]/integrations/email.tsx b/apps/viewer/src/pages/api/typebots/[typebotId]/integrations/email.tsx
index 56a7cc19e..8d3708927 100644
--- a/apps/viewer/src/pages/api/typebots/[typebotId]/integrations/email.tsx
+++ b/apps/viewer/src/pages/api/typebots/[typebotId]/integrations/email.tsx
@@ -15,7 +15,6 @@ import Mail from 'nodemailer/lib/mailer'
import { DefaultBotNotificationEmail } from '@typebot.io/emails'
import { render } from '@faire/mjml-react/utils/render'
import prisma from '@/lib/prisma'
-import { getPreviouslyLinkedTypebots } from '@/features/blocks/logic/typebotLink/getPreviouslyLinkedTypebots'
import { saveErrorLog } from '@/features/logs/saveErrorLog'
import { saveSuccessLog } from '@/features/logs/saveSuccessLog'
@@ -197,10 +196,20 @@ const getEmailBody = async ({
where: { typebotId },
})) as unknown as PublicTypebot
if (!typebot) return
- const linkedTypebots = await getPreviouslyLinkedTypebots({
- typebots: [typebot],
- })([])
- const answers = parseAnswers(typebot, linkedTypebots)(resultValues)
+ const answers = 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,
+ })
return {
html: render(
,
- linkedTypebots: Pick[] | undefined
- ) =>
- ({
- createdAt,
- answers,
- variables: resultVariables,
- }: Omit & { createdAt?: Date | string }): {
- [key: string]: string
- } => {
- const header = parseResultHeader(typebot, linkedTypebots)
- return {
- submittedAt: !createdAt
- ? new Date().toISOString()
- : typeof createdAt === 'string'
- ? createdAt
- : createdAt.toISOString(),
- ...[...answers, ...resultVariables].reduce<{
- [key: string]: string
- }>((o, answerOrVariable) => {
- const isVariable = !('blockId' in answerOrVariable)
- if (isVariable) {
- 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 parseAnswers = ({
+ answers,
+ variables: resultVariables,
+}: {
+ answers: AnswerInSessionState[]
+ variables: VariableWithValue[]
+}): {
+ [key: string]: string
+} => {
+ return {
+ submittedAt: new Date().toISOString(),
+ ...[...answers, ...resultVariables].reduce<{
+ [key: string]: string
+ }>((o, answerOrVariable) => {
+ if ('id' in answerOrVariable) {
+ const variable = answerOrVariable
+ if (variable.value === null) return o
+ return { ...o, [variable.name]: variable.value.toString() }
+ }
+ const answer = answerOrVariable as AnswerInSessionState
+ if (isEmpty(answer.key)) return o
+ return {
+ ...o,
+ [answer.key]: answer.value,
+ }
+ }, {}),
}
+}
+
+export const getDefinedVariables = (variables: Variable[]) =>
+ variables.filter((variable) =>
+ isDefined(variable.value)
+ ) as VariableWithValue[]
diff --git a/packages/schemas/features/blocks/logic/typebotLink.ts b/packages/schemas/features/blocks/logic/typebotLink.ts
index cb600d038..842dda350 100644
--- a/packages/schemas/features/blocks/logic/typebotLink.ts
+++ b/packages/schemas/features/blocks/logic/typebotLink.ts
@@ -5,6 +5,7 @@ import { LogicBlockType } from './enums'
export const typebotLinkOptionsSchema = z.object({
typebotId: z.string().optional(),
groupId: z.string().optional(),
+ mergeResults: z.boolean().optional(),
})
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
export type TypebotLinkOptions = z.infer
diff --git a/packages/schemas/features/chat.ts b/packages/schemas/features/chat/schema.ts
similarity index 79%
rename from packages/schemas/features/chat.ts
rename to packages/schemas/features/chat/schema.ts
index da27f8b52..c86afcaea 100644
--- a/packages/schemas/features/chat.ts
+++ b/packages/schemas/features/chat/schema.ts
@@ -5,68 +5,21 @@ import {
paymentInputRuntimeOptionsSchema,
pixelOptionsSchema,
redirectOptionsSchema,
-} from './blocks'
-import { publicTypebotSchema } from './publicTypebot'
-import { logSchema, resultSchema } from './result'
-import { listVariableValue, typebotSchema } from './typebot'
+} from '../blocks'
+import { logSchema } from '../result'
+import { listVariableValue, typebotSchema } from '../typebot'
import {
textBubbleContentSchema,
imageBubbleContentSchema,
videoBubbleContentSchema,
audioBubbleContentSchema,
embedBubbleContentSchema,
-} from './blocks/bubbles'
-import { answerSchema } from './answer'
-import { BubbleBlockType } from './blocks/bubbles/enums'
-import { inputBlockSchemas } from './blocks/schemas'
-import { chatCompletionMessageSchema } from './blocks/integrations/openai'
-
-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(),
-})
+} from '../blocks/bubbles'
+import { BubbleBlockType } from '../blocks/bubbles/enums'
+import { inputBlockSchemas } from '../blocks/schemas'
+import { chatCompletionMessageSchema } from '../blocks/integrations/openai'
+import { sessionStateSchema } from './sessionState'
+import { dynamicThemeSchema } from './shared'
const chatSessionSchema = z.object({
id: z.string(),
@@ -301,9 +254,7 @@ export const chatReplySchema = z.object({
})
export type ChatSession = z.infer
-export type SessionState = z.infer
-export type TypebotInSession = z.infer
-export type ResultInSession = z.infer
+
export type ChatReply = z.infer
export type ChatMessage = z.infer
export type SendMessageInput = z.infer
diff --git a/packages/schemas/features/chat/sessionState.ts b/packages/schemas/features/chat/sessionState.ts
new file mode 100644
index 000000000..f2c9d92f9
--- /dev/null
+++ b/packages/schemas/features/chat/sessionState.ts
@@ -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
+
+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
+
+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,
+ }
+ })
diff --git a/packages/schemas/features/chat/shared.ts b/packages/schemas/features/chat/shared.ts
new file mode 100644
index 000000000..c05813d06
--- /dev/null
+++ b/packages/schemas/features/chat/shared.ts
@@ -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
+
+export const dynamicThemeSchema = z.object({
+ hostAvatarUrl: z.string().optional(),
+ guestAvatarUrl: z.string().optional(),
+})
diff --git a/packages/schemas/index.ts b/packages/schemas/index.ts
index 4c298d0df..4811b0085 100644
--- a/packages/schemas/index.ts
+++ b/packages/schemas/index.ts
@@ -5,6 +5,8 @@ export * from './features/result'
export * from './features/answer'
export * from './features/utils'
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/items'