2
0

(setVariable) Add Transcription system var (#1507)

Closes #1484
This commit is contained in:
Baptiste Arnaud
2024-05-15 14:24:55 +02:00
committed by GitHub
parent ec7ff8d9ca
commit 40f21203b5
102 changed files with 2911 additions and 986 deletions

View File

@ -0,0 +1,137 @@
import prisma from '@typebot.io/lib/prisma'
import { authenticatedProcedure } from '@/helpers/server/trpc'
import { TRPCError } from '@trpc/server'
import { z } from 'zod'
import { canReadTypebots } from '@/helpers/databaseRules'
import { totalAnswersSchema } from '@typebot.io/schemas/features/analytics'
import { parseGroups } from '@typebot.io/schemas'
import { isInputBlock } from '@typebot.io/schemas/helpers'
import { defaultTimeFilter, timeFilterValues } from '../constants'
import {
parseFromDateFromTimeFilter,
parseToDateFromTimeFilter,
} from '../helpers/parseDateFromTimeFilter'
export const getInDepthAnalyticsData = authenticatedProcedure
.meta({
openapi: {
method: 'GET',
path: '/v1/typebots/{typebotId}/analytics/inDepthData',
protect: true,
summary:
'List total answers in blocks and off-default paths visited edges',
tags: ['Analytics'],
},
})
.input(
z.object({
typebotId: z.string(),
timeFilter: z.enum(timeFilterValues).default(defaultTimeFilter),
timeZone: z.string().optional(),
})
)
.output(
z.object({
totalAnswers: z.array(totalAnswersSchema),
offDefaultPathVisitedEdges: z.array(
z.object({ edgeId: z.string(), total: z.number() })
),
})
)
.query(
async ({ input: { typebotId, timeFilter, timeZone }, ctx: { user } }) => {
const typebot = await prisma.typebot.findFirst({
where: canReadTypebots(typebotId, user),
select: { publishedTypebot: true },
})
if (!typebot?.publishedTypebot)
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Published typebot not found',
})
const fromDate = parseFromDateFromTimeFilter(timeFilter, timeZone)
const toDate = parseToDateFromTimeFilter(timeFilter, timeZone)
const totalAnswersPerBlock = await prisma.answer.groupBy({
by: ['blockId', 'resultId'],
where: {
result: {
typebotId: typebot.publishedTypebot.typebotId,
createdAt: fromDate
? {
gte: fromDate,
lte: toDate ?? undefined,
}
: undefined,
},
blockId: {
in: parseGroups(typebot.publishedTypebot.groups, {
typebotVersion: typebot.publishedTypebot.version,
}).flatMap((group) =>
group.blocks.filter(isInputBlock).map((block) => block.id)
),
},
},
})
const totalAnswersV2PerBlock = await prisma.answerV2.groupBy({
by: ['blockId', 'resultId'],
where: {
result: {
typebotId: typebot.publishedTypebot.typebotId,
createdAt: fromDate
? {
gte: fromDate,
lte: toDate ?? undefined,
}
: undefined,
},
blockId: {
in: parseGroups(typebot.publishedTypebot.groups, {
typebotVersion: typebot.publishedTypebot.version,
}).flatMap((group) =>
group.blocks.filter(isInputBlock).map((block) => block.id)
),
},
},
})
const uniqueCounts = totalAnswersPerBlock
.concat(totalAnswersV2PerBlock)
.reduce<{
[key: string]: Set<string>
}>((acc, { blockId, resultId }) => {
acc[blockId] = acc[blockId] || new Set()
acc[blockId].add(resultId)
return acc
}, {})
const offDefaultPathVisitedEdges = await prisma.visitedEdge.groupBy({
by: ['edgeId'],
where: {
result: {
typebotId: typebot.publishedTypebot.typebotId,
createdAt: fromDate
? {
gte: fromDate,
lte: toDate ?? undefined,
}
: undefined,
},
},
_count: { resultId: true },
})
return {
totalAnswers: Object.keys(uniqueCounts).map((blockId) => ({
blockId,
total: uniqueCounts[blockId].size,
})),
offDefaultPathVisitedEdges: offDefaultPathVisitedEdges.map((e) => ({
edgeId: e.edgeId,
total: e._count.resultId,
})),
}
}
)

View File

@ -1,78 +0,0 @@
import prisma from '@typebot.io/lib/prisma'
import { authenticatedProcedure } from '@/helpers/server/trpc'
import { TRPCError } from '@trpc/server'
import { z } from 'zod'
import { canReadTypebots } from '@/helpers/databaseRules'
import { totalAnswersSchema } from '@typebot.io/schemas/features/analytics'
import { parseGroups } from '@typebot.io/schemas'
import { isInputBlock } from '@typebot.io/schemas/helpers'
import { defaultTimeFilter, timeFilterValues } from '../constants'
import {
parseFromDateFromTimeFilter,
parseToDateFromTimeFilter,
} from '../helpers/parseDateFromTimeFilter'
export const getTotalAnswers = authenticatedProcedure
.meta({
openapi: {
method: 'GET',
path: '/v1/typebots/{typebotId}/analytics/totalAnswersInBlocks',
protect: true,
summary: 'List total answers in blocks',
tags: ['Analytics'],
},
})
.input(
z.object({
typebotId: z.string(),
timeFilter: z.enum(timeFilterValues).default(defaultTimeFilter),
timeZone: z.string().optional(),
})
)
.output(z.object({ totalAnswers: z.array(totalAnswersSchema) }))
.query(
async ({ input: { typebotId, timeFilter, timeZone }, ctx: { user } }) => {
const typebot = await prisma.typebot.findFirst({
where: canReadTypebots(typebotId, user),
select: { publishedTypebot: true },
})
if (!typebot?.publishedTypebot)
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Published typebot not found',
})
const fromDate = parseFromDateFromTimeFilter(timeFilter, timeZone)
const toDate = parseToDateFromTimeFilter(timeFilter, timeZone)
const totalAnswersPerBlock = await prisma.answer.groupBy({
by: ['blockId'],
where: {
result: {
typebotId: typebot.publishedTypebot.typebotId,
createdAt: fromDate
? {
gte: fromDate,
lte: toDate ?? undefined,
}
: undefined,
},
blockId: {
in: parseGroups(typebot.publishedTypebot.groups, {
typebotVersion: typebot.publishedTypebot.version,
}).flatMap((group) =>
group.blocks.filter(isInputBlock).map((block) => block.id)
),
},
},
_count: { _all: true },
})
return {
totalAnswers: totalAnswersPerBlock.map((a) => ({
blockId: a.blockId,
total: a._count._all,
})),
}
}
)

View File

@ -1,10 +1,8 @@
import { router } from '@/helpers/server/trpc' import { router } from '@/helpers/server/trpc'
import { getTotalAnswers } from './getTotalAnswers'
import { getTotalVisitedEdges } from './getTotalVisitedEdges'
import { getStats } from './getStats' import { getStats } from './getStats'
import { getInDepthAnalyticsData } from './getInDepthAnalyticsData'
export const analyticsRouter = router({ export const analyticsRouter = router({
getTotalAnswers, getInDepthAnalyticsData,
getTotalVisitedEdges,
getStats, getStats,
}) })

View File

@ -5,8 +5,14 @@ import {
useDisclosure, useDisclosure,
} from '@chakra-ui/react' } from '@chakra-ui/react'
import { useTypebot } from '@/features/editor/providers/TypebotProvider' import { useTypebot } from '@/features/editor/providers/TypebotProvider'
import { Stats } from '@typebot.io/schemas' import {
import React from 'react' Edge,
GroupV6,
Stats,
TotalAnswers,
TotalVisitedEdges,
} from '@typebot.io/schemas'
import React, { useMemo } from 'react'
import { StatsCards } from './StatsCards' import { StatsCards } from './StatsCards'
import { ChangePlanModal } from '@/features/billing/components/ChangePlanModal' import { ChangePlanModal } from '@/features/billing/components/ChangePlanModal'
import { Graph } from '@/features/graph/components/Graph' import { Graph } from '@/features/graph/components/Graph'
@ -16,6 +22,7 @@ import { trpc } from '@/lib/trpc'
import { isDefined } from '@typebot.io/lib' import { isDefined } from '@typebot.io/lib'
import { EventsCoordinatesProvider } from '@/features/graph/providers/EventsCoordinateProvider' import { EventsCoordinatesProvider } from '@/features/graph/providers/EventsCoordinateProvider'
import { timeFilterValues } from '../constants' import { timeFilterValues } from '../constants'
import { blockHasItems, isInputBlock } from '@typebot.io/schemas/helpers'
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone
@ -33,7 +40,7 @@ export const AnalyticsGraphContainer = ({
const { t } = useTranslate() const { t } = useTranslate()
const { isOpen, onOpen, onClose } = useDisclosure() const { isOpen, onOpen, onClose } = useDisclosure()
const { typebot, publishedTypebot } = useTypebot() const { typebot, publishedTypebot } = useTypebot()
const { data } = trpc.analytics.getTotalAnswers.useQuery( const { data } = trpc.analytics.getInDepthAnalyticsData.useQuery(
{ {
typebotId: typebot?.id as string, typebotId: typebot?.id as string,
timeFilter, timeFilter,
@ -42,14 +49,36 @@ export const AnalyticsGraphContainer = ({
{ enabled: isDefined(publishedTypebot) } { enabled: isDefined(publishedTypebot) }
) )
const { data: edgesData } = trpc.analytics.getTotalVisitedEdges.useQuery( const totalVisitedEdges = useMemo(() => {
{ if (
typebotId: typebot?.id as string, !publishedTypebot?.edges ||
timeFilter, !publishedTypebot.groups ||
timeZone, !publishedTypebot.events ||
}, !data?.totalAnswers ||
{ enabled: isDefined(publishedTypebot) } !stats?.totalViews
) )
return
const firstEdgeId = publishedTypebot.events[0].outgoingEdgeId
if (!firstEdgeId) return
return populateEdgesWithVisitData({
edgeId: firstEdgeId,
edges: publishedTypebot.edges,
groups: publishedTypebot.groups,
currentTotalUsers: stats.totalViews,
totalVisitedEdges: data.offDefaultPathVisitedEdges
? [...data.offDefaultPathVisitedEdges]
: [],
totalAnswers: data.totalAnswers,
edgeVisitHistory: [],
})
}, [
data?.offDefaultPathVisitedEdges,
data?.totalAnswers,
publishedTypebot?.edges,
publishedTypebot?.groups,
publishedTypebot?.events,
stats?.totalViews,
])
return ( return (
<Flex <Flex
@ -73,7 +102,7 @@ export const AnalyticsGraphContainer = ({
typebot={publishedTypebot} typebot={publishedTypebot}
onUnlockProPlanClick={onOpen} onUnlockProPlanClick={onOpen}
totalAnswers={data?.totalAnswers} totalAnswers={data?.totalAnswers}
totalVisitedEdges={edgesData?.totalVisitedEdges} totalVisitedEdges={totalVisitedEdges}
/> />
</EventsCoordinatesProvider> </EventsCoordinatesProvider>
</GraphProvider> </GraphProvider>
@ -102,3 +131,72 @@ export const AnalyticsGraphContainer = ({
</Flex> </Flex>
) )
} }
const populateEdgesWithVisitData = ({
edgeId,
edges,
groups,
currentTotalUsers,
totalVisitedEdges,
totalAnswers,
edgeVisitHistory,
}: {
edgeId: string
edges: Edge[]
groups: GroupV6[]
currentTotalUsers: number
totalVisitedEdges: TotalVisitedEdges[]
totalAnswers: TotalAnswers[]
edgeVisitHistory: string[]
}): TotalVisitedEdges[] => {
if (edgeVisitHistory.find((e) => e === edgeId)) return totalVisitedEdges
totalVisitedEdges.push({
edgeId,
total: currentTotalUsers,
})
edgeVisitHistory.push(edgeId)
const edge = edges.find((edge) => edge.id === edgeId)
if (!edge) return totalVisitedEdges
const group = groups.find((group) => edge?.to.groupId === group.id)
if (!group) return totalVisitedEdges
for (const block of edge.to.blockId
? group.blocks.slice(
group.blocks.findIndex((b) => b.id === edge.to.blockId)
)
: group.blocks) {
if (blockHasItems(block)) {
for (const item of block.items) {
if (item.outgoingEdgeId) {
totalVisitedEdges = populateEdgesWithVisitData({
edgeId: item.outgoingEdgeId,
edges,
groups,
currentTotalUsers:
totalVisitedEdges.find(
(tve) => tve.edgeId === item.outgoingEdgeId
)?.total ?? 0,
totalVisitedEdges,
totalAnswers,
edgeVisitHistory,
})
}
}
}
if (block.outgoingEdgeId) {
const totalUsers = isInputBlock(block)
? totalAnswers.find((a) => a.blockId === block.id)?.total
: currentTotalUsers
totalVisitedEdges = populateEdgesWithVisitData({
edgeId: block.outgoingEdgeId,
edges,
groups,
currentTotalUsers: totalUsers ?? 0,
totalVisitedEdges,
totalAnswers,
edgeVisitHistory,
})
}
}
return totalVisitedEdges
}

View File

@ -71,6 +71,7 @@ const Expression = ({
case 'Result ID': case 'Result ID':
case 'Moment of the day': case 'Moment of the day':
case 'Environment name': case 'Environment name':
case 'Transcript':
case 'Yesterday': { case 'Yesterday': {
return ( return (
<Text as="span"> <Text as="span">

View File

@ -8,11 +8,13 @@ import { WhatsAppLogo } from '@/components/logos/WhatsAppLogo'
import { import {
defaultSetVariableOptions, defaultSetVariableOptions,
hiddenTypes, hiddenTypes,
sessionOnlySetVariableOptions,
valueTypes, valueTypes,
} from '@typebot.io/schemas/features/blocks/logic/setVariable/constants' } from '@typebot.io/schemas/features/blocks/logic/setVariable/constants'
import { TextInput } from '@/components/inputs' import { TextInput } from '@/components/inputs'
import { isDefined } from '@typebot.io/lib' import { isDefined } from '@typebot.io/lib'
import { useTypebot } from '@/features/editor/providers/TypebotProvider' import { useTypebot } from '@/features/editor/providers/TypebotProvider'
import { isInputBlock } from '@typebot.io/schemas/helpers'
type Props = { type Props = {
options: SetVariableBlock['options'] options: SetVariableBlock['options']
@ -48,6 +50,20 @@ export const SetVariableSettings = ({ options, onOptionsChange }: Props) => {
}) })
} }
const isSessionOnly =
options?.type &&
sessionOnlySetVariableOptions.includes(
options.type as (typeof sessionOnlySetVariableOptions)[number]
)
const isLinkedToAnswer =
options?.variableId &&
typebot?.groups.some((g) =>
g.blocks.some(
(b) => isInputBlock(b) && b.options?.variableId === options.variableId
)
)
return ( return (
<Stack spacing={4}> <Stack spacing={4}>
<Stack> <Stack>
@ -80,7 +96,7 @@ export const SetVariableSettings = ({ options, onOptionsChange }: Props) => {
/> />
</Stack> </Stack>
{selectedVariable && ( {selectedVariable && !isSessionOnly && !isLinkedToAnswer && (
<SwitchWithLabel <SwitchWithLabel
key={selectedVariable.id} key={selectedVariable.id}
label="Save in results?" label="Save in results?"
@ -244,6 +260,7 @@ const SetVariableValue = ({
case 'Today': case 'Today':
case 'Result ID': case 'Result ID':
case 'Empty': case 'Empty':
case 'Transcript':
return null return null
} }
} }

View File

@ -3,10 +3,11 @@ import { importTypebotInDatabase } from '@typebot.io/playwright/databaseActions'
import { createId } from '@paralleldrive/cuid2' import { createId } from '@paralleldrive/cuid2'
import { getTestAsset } from '@/test/utils/playwright' import { getTestAsset } from '@/test/utils/playwright'
const typebotId = createId() test.describe.configure({ mode: 'parallel' })
test.describe('Set variable block', () => { test.describe('Set variable block', () => {
test('its configuration should work', async ({ page }) => { test('its configuration should work', async ({ page }) => {
const typebotId = createId()
await importTypebotInDatabase( await importTypebotInDatabase(
getTestAsset('typebots/logic/setVariable.json'), getTestAsset('typebots/logic/setVariable.json'),
{ {
@ -70,4 +71,47 @@ test.describe('Set variable block', () => {
page.locator('typebot-standard').locator('text=Addition: 366000') page.locator('typebot-standard').locator('text=Addition: 366000')
).toBeVisible() ).toBeVisible()
}) })
test('Transcription variable setting should work in preview', async ({
page,
}) => {
const typebotId = createId()
await importTypebotInDatabase(
getTestAsset('typebots/logic/setVariable2.json'),
{
id: typebotId,
}
)
await page.goto(`/typebots/${typebotId}/edit`)
await page.getByText('Transcription =').click()
await expect(page.getByText('Save in results?')).toBeVisible()
await page.locator('input[type="text"]').click()
await page.getByRole('menuitem', { name: 'Transcript' }).click()
await expect(page.getByText('Save in results?')).toBeHidden()
await expect(page.getByText('System.Transcript')).toBeVisible()
await page.getByRole('button', { name: 'Test' }).click()
await page.getByRole('button', { name: 'There is a bug 🐛' }).click()
await page.getByTestId('textarea').fill('Hello!!')
await page.getByTestId('input').getByRole('button').click()
await page
.locator('typebot-standard')
.getByRole('button', { name: 'Restart' })
.click()
await page.getByRole('button', { name: 'I have a question 💭' }).click()
await page.getByTestId('textarea').fill('How are you?')
await page.getByTestId('input').getByRole('button').click()
await page.getByRole('button', { name: 'Transcription' }).click()
await expect(
page.getByText('Assistant: "Hey friend 👋 How').first()
).toBeVisible()
await expect(
page.getByText(
'Assistant: "https://media0.giphy.com/media/rhgwg4qBu97ISgbfni/giphy-downsized.gif?cid=fe3852a3wimy48e55djt23j44uto7gdlu8ksytylafisvr0q&rid=giphy-downsized.gif&ct=g"'
)
).toBeVisible()
await expect(page.getByText('User: "How are you?"')).toBeVisible()
})
}) })

View File

@ -43,7 +43,7 @@ test('should be configurable', async ({ page }) => {
await page.click('[aria-label="Close"]') await page.click('[aria-label="Close"]')
await page.click('text=Jump to Group #2 in My link typebot 2') await page.click('text=Jump to Group #2 in My link typebot 2')
await page.getByTestId('selected-item-label').nth(1).click({ force: true }) await page.getByTestId('selected-item-label').nth(1).click({ force: true })
await page.click('button >> text=Start') await page.getByLabel('Clear').click()
await page.click('text=Test') await page.click('text=Test')
await page.locator('typebot-standard').locator('input').fill('Hello there!') await page.locator('typebot-standard').locator('input').fill('Hello there!')
@ -53,7 +53,7 @@ test('should be configurable', async ({ page }) => {
).toBeVisible() ).toBeVisible()
await page.click('[aria-label="Close"]') await page.click('[aria-label="Close"]')
await page.click('text=Jump to Start in My link typebot 2') await page.click('text=Jump in My link typebot 2')
await page.waitForTimeout(1000) await page.waitForTimeout(1000)
await page.getByTestId('selected-item-label').first().click({ force: true }) await page.getByTestId('selected-item-label').first().click({ force: true })
await page.click('button >> text=Current typebot') await page.click('button >> text=Current typebot')

View File

@ -61,58 +61,6 @@ test('Edges connection should work', async ({ page }) => {
const total = await page.locator('[data-testid="edge"]').count() const total = await page.locator('[data-testid="edge"]').count()
expect(total).toBe(1) expect(total).toBe(1)
}) })
test('Drag and drop blocks and items should work', async ({ page }) => {
const typebotId = createId()
await importTypebotInDatabase(
getTestAsset('typebots/editor/buttonsDnd.json'),
{
id: typebotId,
}
)
// Blocks dnd
await page.goto(`/typebots/${typebotId}/edit`)
await expect(page.locator('[data-testid="block"] >> nth=0')).toHaveText(
'Hello!'
)
await page.dragAndDrop('text=Hello', '[data-testid="block"] >> nth=2', {
targetPosition: { x: 100, y: 0 },
})
await expect(page.locator('[data-testid="block"] >> nth=1')).toHaveText(
'Hello!'
)
await page.dragAndDrop('text=Hello', 'text=Group #2')
await expect(page.locator('[data-testid="block"] >> nth=2')).toHaveText(
'Hello!'
)
// Items dnd
await expect(page.locator('[data-testid="item"] >> nth=0')).toHaveText(
'Item 1'
)
await page.dragAndDrop('text=Item 1', 'text=Item 3')
await expect(page.locator('[data-testid="item"] >> nth=2')).toHaveText(
'Item 1'
)
await expect(page.locator('[data-testid="item"] >> nth=1')).toHaveText(
'Item 3'
)
await page.dragAndDrop('text=Item 3', 'text=Item 2-3')
await expect(page.locator('[data-testid="item"] >> nth=7')).toHaveText(
'Item 3'
)
await expect(page.locator('[data-testid="item"] >> nth=2')).toHaveText(
'Name=John'
)
await page.dragAndDrop(
'[data-testid="item"] >> nth=2',
'[data-testid="item"] >> nth=3'
)
await expect(page.locator('[data-testid="item"] >> nth=3')).toHaveText(
'Name=John'
)
})
test('Rename and icon change should work', async ({ page }) => { test('Rename and icon change should work', async ({ page }) => {
const typebotId = createId() const typebotId = createId()

View File

@ -110,10 +110,14 @@ export const useUndo = <T extends { updatedAt: Date }>(
const setUpdateDate = useCallback( const setUpdateDate = useCallback(
(updatedAt: Date) => { (updatedAt: Date) => {
set((current) => ({ set((current) =>
...current, current
updatedAt, ? {
})) ...current,
updatedAt,
}
: current
)
}, },
[set] [set]
) )

View File

@ -18,7 +18,7 @@ import {
TotalVisitedEdges, TotalVisitedEdges,
} from '@typebot.io/schemas/features/analytics' } from '@typebot.io/schemas/features/analytics'
import { computeTotalUsersAtBlock } from '@/features/analytics/helpers/computeTotalUsersAtBlock' import { computeTotalUsersAtBlock } from '@/features/analytics/helpers/computeTotalUsersAtBlock'
import { byId } from '@typebot.io/lib' import { byId, isNotDefined } from '@typebot.io/lib'
import { blockHasItems } from '@typebot.io/schemas/helpers' import { blockHasItems } from '@typebot.io/schemas/helpers'
import { groupWidth } from '../../constants' import { groupWidth } from '../../constants'
import { getTotalAnswersAtBlock } from '@/features/analytics/helpers/getTotalAnswersAtBlock' import { getTotalAnswersAtBlock } from '@/features/analytics/helpers/getTotalAnswersAtBlock'
@ -130,7 +130,7 @@ export const DropOffEdge = ({
return lastBlock?.id === currentBlockId return lastBlock?.id === currentBlockId
}, [publishedTypebot, currentBlockId]) }, [publishedTypebot, currentBlockId])
if (!endpointCoordinates) return null if (!endpointCoordinates || isNotDefined(dropOffRate)) return null
return ( return (
<> <>

View File

@ -22,7 +22,7 @@ import { FormEvent, useState } from 'react'
import { headerHeight } from '../../editor/constants' import { headerHeight } from '../../editor/constants'
import { useDrag } from '@use-gesture/react' import { useDrag } from '@use-gesture/react'
import { ResizeHandle } from './ResizeHandle' import { ResizeHandle } from './ResizeHandle'
import { Variable } from '@typebot.io/schemas' import { InputBlock, SetVariableBlock, Variable } from '@typebot.io/schemas'
import { import {
CheckIcon, CheckIcon,
MoreHorizontalIcon, MoreHorizontalIcon,
@ -32,6 +32,9 @@ import {
import { SwitchWithLabel } from '@/components/inputs/SwitchWithLabel' import { SwitchWithLabel } from '@/components/inputs/SwitchWithLabel'
import { isNotEmpty } from '@typebot.io/lib' import { isNotEmpty } from '@typebot.io/lib'
import { createId } from '@paralleldrive/cuid2' import { createId } from '@paralleldrive/cuid2'
import { isInputBlock } from '@typebot.io/schemas/helpers'
import { LogicBlockType } from '@typebot.io/schemas/features/blocks/logic/constants'
import { sessionOnlySetVariableOptions } from '@typebot.io/schemas/features/blocks/logic/setVariable/constants'
type Props = { type Props = {
onClose: () => void onClose: () => void
@ -70,6 +73,14 @@ export const VariablesDrawer = ({ onClose }: Props) => {
}) })
} }
const setVariableAndInputBlocks =
typebot?.groups.flatMap(
(g) =>
g.blocks.filter(
(b) => b.type === LogicBlockType.SET_VARIABLE || isInputBlock(b)
) as (InputBlock | SetVariableBlock)[]
) ?? []
return ( return (
<Flex <Flex
pos="absolute" pos="absolute"
@ -132,6 +143,7 @@ export const VariablesDrawer = ({ onClose }: Props) => {
variable={variable} variable={variable}
onChange={(changes) => updateVariable(variable.id, changes)} onChange={(changes) => updateVariable(variable.id, changes)}
onDelete={() => deleteVariable(variable.id)} onDelete={() => deleteVariable(variable.id)}
setVariableAndInputBlocks={setVariableAndInputBlocks}
/> />
))} ))}
</Stack> </Stack>
@ -144,58 +156,76 @@ const VariableItem = ({
variable, variable,
onChange, onChange,
onDelete, onDelete,
setVariableAndInputBlocks,
}: { }: {
variable: Variable variable: Variable
onChange: (variable: Partial<Variable>) => void onChange: (variable: Partial<Variable>) => void
onDelete: () => void onDelete: () => void
}) => ( setVariableAndInputBlocks: (InputBlock | SetVariableBlock)[]
<HStack justifyContent="space-between"> }) => {
<Editable const isSessionOnly = setVariableAndInputBlocks.some(
defaultValue={variable.name} (b) =>
onSubmit={(name) => onChange({ name })} b.type === LogicBlockType.SET_VARIABLE &&
> sessionOnlySetVariableOptions.includes(
<EditablePreview b.options?.type as (typeof sessionOnlySetVariableOptions)[number]
px="2" )
noOfLines={1} )
cursor="text"
_hover={{
bg: useColorModeValue('gray.100', 'gray.700'),
}}
/>
<EditableInput ml="1" pl="1" />
</Editable>
<HStack> const isLinkedToAnswer = setVariableAndInputBlocks.some(
<Popover> (b) => isInputBlock(b) && b.options?.variableId === variable.id
<PopoverTrigger> )
<IconButton
icon={<MoreHorizontalIcon />} return (
aria-label={'Settings'} <HStack justifyContent="space-between">
size="sm" <Editable
/> defaultValue={variable.name}
</PopoverTrigger> onSubmit={(name) => onChange({ name })}
<PopoverContent> >
<PopoverBody> <EditablePreview
<SwitchWithLabel px="2"
label="Save in results?" noOfLines={1}
moreInfoContent="Check this option if you want to save the variable value in the typebot Results table." cursor="text"
initialValue={!variable.isSessionVariable} _hover={{
onCheckChange={() => bg: useColorModeValue('gray.100', 'gray.700'),
onChange({ }}
...variable, />
isSessionVariable: !variable.isSessionVariable, <EditableInput ml="1" pl="1" />
}) </Editable>
}
/> <HStack>
</PopoverBody> {!isSessionOnly && !isLinkedToAnswer && (
</PopoverContent> <Popover>
<PopoverTrigger>
<IconButton
icon={<MoreHorizontalIcon />}
aria-label={'Settings'}
size="sm"
/>
</PopoverTrigger>
<PopoverContent>
<PopoverBody>
<SwitchWithLabel
label="Save in results?"
moreInfoContent="Check this option if you want to save the variable value in the typebot Results table."
initialValue={!variable.isSessionVariable}
onCheckChange={() =>
onChange({
...variable,
isSessionVariable: !variable.isSessionVariable,
})
}
/>
</PopoverBody>
</PopoverContent>
</Popover>
)}
<IconButton <IconButton
icon={<TrashIcon />} icon={<TrashIcon />}
onClick={onDelete} onClick={onDelete}
aria-label="Delete" aria-label="Delete"
size="sm" size="sm"
/> />
</Popover> </HStack>
</HStack> </HStack>
</HStack> )
) }

View File

@ -1,4 +1,5 @@
import { WebhookIcon } from '@/components/icons' import { WebhookIcon } from '@/components/icons'
import { useUser } from '@/features/account/hooks/useUser'
import { useEditor } from '@/features/editor/providers/EditorProvider' import { useEditor } from '@/features/editor/providers/EditorProvider'
import { useTypebot } from '@/features/editor/providers/TypebotProvider' import { useTypebot } from '@/features/editor/providers/TypebotProvider'
import { useGraph } from '@/features/graph/providers/GraphProvider' import { useGraph } from '@/features/graph/providers/GraphProvider'
@ -7,6 +8,7 @@ import { Standard } from '@typebot.io/nextjs'
import { ContinueChatResponse } from '@typebot.io/schemas' import { ContinueChatResponse } from '@typebot.io/schemas'
export const WebPreview = () => { export const WebPreview = () => {
const { user } = useUser()
const { typebot } = useTypebot() const { typebot } = useTypebot()
const { startPreviewAtGroup, startPreviewAtEvent } = useEditor() const { startPreviewAtGroup, startPreviewAtEvent } = useEditor()
const { setPreviewingBlock } = useGraph() const { setPreviewingBlock } = useGraph()
@ -40,6 +42,7 @@ export const WebPreview = () => {
<Standard <Standard
key={`web-preview${startPreviewAtGroup ?? ''}`} key={`web-preview${startPreviewAtGroup ?? ''}`}
typebot={typebot} typebot={typebot}
sessionId={user ? `${typebot.id}-${user.id}` : undefined}
startFrom={ startFrom={
startPreviewAtGroup startPreviewAtGroup
? { type: 'group', groupId: startPreviewAtGroup } ? { type: 'group', groupId: startPreviewAtGroup }

View File

@ -15,6 +15,7 @@ import { parseResultHeader } from '@typebot.io/results/parseResultHeader'
import { convertResultsToTableData } from '@typebot.io/results/convertResultsToTableData' import { convertResultsToTableData } from '@typebot.io/results/convertResultsToTableData'
import { parseCellContent } from './helpers/parseCellContent' import { parseCellContent } from './helpers/parseCellContent'
import { timeFilterValues } from '../analytics/constants' import { timeFilterValues } from '../analytics/constants'
import { parseBlockIdVariableIdMap } from '@typebot.io/results/parseBlockIdVariableIdMap'
const resultsContext = createContext<{ const resultsContext = createContext<{
resultsList: { results: ResultWithAnswers[] }[] | undefined resultsList: { results: ResultWithAnswers[] }[] | undefined
@ -97,11 +98,14 @@ export const ResultsProvider = ({
const tableData = useMemo( const tableData = useMemo(
() => () =>
publishedTypebot publishedTypebot
? convertResultsToTableData( ? convertResultsToTableData({
data?.flatMap((d) => d.results) ?? [], results: data?.flatMap((d) => d.results) ?? [],
resultHeader, headerCells: resultHeader,
parseCellContent cellParser: parseCellContent,
) blockIdVariableIdMap: parseBlockIdVariableIdMap(
publishedTypebot.groups
),
})
: [], : [],
[publishedTypebot, data, resultHeader] [publishedTypebot, data, resultHeader]
) )

View File

@ -71,11 +71,31 @@ export const getResult = authenticatedProcedure
orderBy: { orderBy: {
createdAt: 'desc', createdAt: 'desc',
}, },
include: { answers: true }, include: {
answers: {
select: {
blockId: true,
content: true,
},
},
answersV2: {
select: {
blockId: true,
content: true,
},
},
},
}) })
if (results.length === 0) if (results.length === 0)
throw new TRPCError({ code: 'NOT_FOUND', message: 'Result not found' }) throw new TRPCError({ code: 'NOT_FOUND', message: 'Result not found' })
return { result: resultWithAnswersSchema.parse(results[0]) } const { answers, answersV2, ...result } = results[0]
return {
result: resultWithAnswersSchema.parse({
...result,
answers: answers.concat(answersV2),
}),
}
}) })

View File

@ -104,7 +104,20 @@ export const getResults = authenticatedProcedure
orderBy: { orderBy: {
createdAt: 'desc', createdAt: 'desc',
}, },
include: { answers: true }, include: {
answers: {
select: {
blockId: true,
content: true,
},
},
answersV2: {
select: {
blockId: true,
content: true,
},
},
},
}) })
let nextCursor: typeof cursor | undefined let nextCursor: typeof cursor | undefined
@ -114,7 +127,11 @@ export const getResults = authenticatedProcedure
} }
return { return {
results: z.array(resultWithAnswersSchema).parse(results), results: z
.array(resultWithAnswersSchema)
.parse(
results.map((r) => ({ ...r, answers: r.answersV2.concat(r.answers) }))
),
nextCursor, nextCursor,
} }
}) })

View File

@ -27,6 +27,7 @@ import { parseUniqueKey } from '@typebot.io/lib/parseUniqueKey'
import { useResults } from '../../ResultsProvider' import { useResults } from '../../ResultsProvider'
import { byId, isDefined } from '@typebot.io/lib' import { byId, isDefined } from '@typebot.io/lib'
import { Typebot } from '@typebot.io/schemas' import { Typebot } from '@typebot.io/schemas'
import { parseBlockIdVariableIdMap } from '@typebot.io/results/parseBlockIdVariableIdMap'
type Props = { type Props = {
isOpen: boolean isOpen: boolean
@ -101,7 +102,11 @@ export const ExportAllResultsModal = ({ isOpen, onClose }: Props) => {
) )
: existingResultHeader : existingResultHeader
const dataToUnparse = convertResultsToTableData(results, resultHeader) const dataToUnparse = convertResultsToTableData({
results,
headerCells: resultHeader,
blockIdVariableIdMap: parseBlockIdVariableIdMap(typebot?.groups),
})
const headerIds = parseColumnsOrder( const headerIds = parseColumnsOrder(
typebot?.resultsTablePreferences?.columnsOrder, typebot?.resultsTablePreferences?.columnsOrder,

View File

@ -10,6 +10,7 @@ import {
isPublicIdNotAvailable, isPublicIdNotAvailable,
sanitizeGroups, sanitizeGroups,
sanitizeSettings, sanitizeSettings,
sanitizeVariables,
} from '../helpers/sanitizers' } from '../helpers/sanitizers'
import { createId } from '@paralleldrive/cuid2' import { createId } from '@paralleldrive/cuid2'
import { EventType } from '@typebot.io/schemas/features/events/constants' import { EventType } from '@typebot.io/schemas/features/events/constants'
@ -92,6 +93,9 @@ export const createTypebot = authenticatedProcedure
if (!existingFolder) typebot.folderId = null if (!existingFolder) typebot.folderId = null
} }
const groups = (
typebot.groups ? await sanitizeGroups(workspaceId)(typebot.groups) : []
) as TypebotV6['groups']
const newTypebot = await prisma.typebot.create({ const newTypebot = await prisma.typebot.create({
data: { data: {
version: '6', version: '6',
@ -99,9 +103,7 @@ export const createTypebot = authenticatedProcedure
name: typebot.name ?? 'My typebot', name: typebot.name ?? 'My typebot',
icon: typebot.icon, icon: typebot.icon,
selectedThemeTemplateId: typebot.selectedThemeTemplateId, selectedThemeTemplateId: typebot.selectedThemeTemplateId,
groups: (typebot.groups groups,
? await sanitizeGroups(workspaceId)(typebot.groups)
: []) as TypebotV6['groups'],
events: typebot.events ?? [ events: typebot.events ?? [
{ {
type: EventType.START, type: EventType.START,
@ -118,7 +120,9 @@ export const createTypebot = authenticatedProcedure
} }
: {}, : {},
folderId: typebot.folderId, folderId: typebot.folderId,
variables: typebot.variables ?? [], variables: typebot.variables
? sanitizeVariables({ variables: typebot.variables, groups })
: [],
edges: typebot.edges ?? [], edges: typebot.edges ?? [],
resultsTablePreferences: typebot.resultsTablePreferences ?? undefined, resultsTablePreferences: typebot.resultsTablePreferences ?? undefined,
publicId: typebot.publicId ?? undefined, publicId: typebot.publicId ?? undefined,

View File

@ -15,6 +15,7 @@ import {
sanitizeFolderId, sanitizeFolderId,
sanitizeGroups, sanitizeGroups,
sanitizeSettings, sanitizeSettings,
sanitizeVariables,
} from '../helpers/sanitizers' } from '../helpers/sanitizers'
import { preprocessTypebot } from '@typebot.io/schemas/features/typebot/helpers/preprocessTypebot' import { preprocessTypebot } from '@typebot.io/schemas/features/typebot/helpers/preprocessTypebot'
import { migrateTypebot } from '@typebot.io/migrations/migrateTypebot' import { migrateTypebot } from '@typebot.io/migrations/migrateTypebot'
@ -122,6 +123,12 @@ export const importTypebot = authenticatedProcedure
const migratedTypebot = await migrateImportingTypebot(typebot) const migratedTypebot = await migrateImportingTypebot(typebot)
const groups = (
migratedTypebot.groups
? await sanitizeGroups(workspaceId)(migratedTypebot.groups)
: []
) as TypebotV6['groups']
const newTypebot = await prisma.typebot.create({ const newTypebot = await prisma.typebot.create({
data: { data: {
version: '6', version: '6',
@ -129,9 +136,7 @@ export const importTypebot = authenticatedProcedure
name: migratedTypebot.name, name: migratedTypebot.name,
icon: migratedTypebot.icon, icon: migratedTypebot.icon,
selectedThemeTemplateId: migratedTypebot.selectedThemeTemplateId, selectedThemeTemplateId: migratedTypebot.selectedThemeTemplateId,
groups: (migratedTypebot.groups groups,
? await sanitizeGroups(workspaceId)(migratedTypebot.groups)
: []) as TypebotV6['groups'],
events: migratedTypebot.events ?? undefined, events: migratedTypebot.events ?? undefined,
theme: migratedTypebot.theme ? migratedTypebot.theme : {}, theme: migratedTypebot.theme ? migratedTypebot.theme : {},
settings: migratedTypebot.settings settings: migratedTypebot.settings
@ -147,7 +152,9 @@ export const importTypebot = authenticatedProcedure
folderId: migratedTypebot.folderId, folderId: migratedTypebot.folderId,
workspaceId: workspace.id, workspaceId: workspace.id,
}), }),
variables: migratedTypebot.variables ?? [], variables: migratedTypebot.variables
? sanitizeVariables({ variables: migratedTypebot.variables, groups })
: [],
edges: migratedTypebot.edges ?? [], edges: migratedTypebot.edges ?? [],
resultsTablePreferences: resultsTablePreferences:
migratedTypebot.resultsTablePreferences ?? undefined, migratedTypebot.resultsTablePreferences ?? undefined,

View File

@ -13,6 +13,7 @@ import {
sanitizeCustomDomain, sanitizeCustomDomain,
sanitizeGroups, sanitizeGroups,
sanitizeSettings, sanitizeSettings,
sanitizeVariables,
} from '../helpers/sanitizers' } from '../helpers/sanitizers'
import { isWriteTypebotForbidden } from '../helpers/isWriteTypebotForbidden' import { isWriteTypebotForbidden } from '../helpers/isWriteTypebotForbidden'
import { isCloudProdInstance } from '@/helpers/isCloudProdInstance' import { isCloudProdInstance } from '@/helpers/isCloudProdInstance'
@ -156,6 +157,10 @@ export const updateTypebot = authenticatedProcedure
}) })
} }
const groups = typebot.groups
? await sanitizeGroups(existingTypebot.workspace.id)(typebot.groups)
: undefined
const newTypebot = await prisma.typebot.update({ const newTypebot = await prisma.typebot.update({
where: { where: {
id: existingTypebot.id, id: existingTypebot.id,
@ -166,9 +171,7 @@ export const updateTypebot = authenticatedProcedure
icon: typebot.icon, icon: typebot.icon,
selectedThemeTemplateId: typebot.selectedThemeTemplateId, selectedThemeTemplateId: typebot.selectedThemeTemplateId,
events: typebot.events ?? undefined, events: typebot.events ?? undefined,
groups: typebot.groups groups,
? await sanitizeGroups(existingTypebot.workspace.id)(typebot.groups)
: undefined,
theme: typebot.theme ? typebot.theme : undefined, theme: typebot.theme ? typebot.theme : undefined,
settings: typebot.settings settings: typebot.settings
? sanitizeSettings( ? sanitizeSettings(
@ -178,7 +181,13 @@ export const updateTypebot = authenticatedProcedure
) )
: undefined, : undefined,
folderId: typebot.folderId, folderId: typebot.folderId,
variables: typebot.variables, variables:
typebot.variables && groups
? sanitizeVariables({
variables: typebot.variables,
groups,
})
: undefined,
edges: typebot.edges, edges: typebot.edges,
resultsTablePreferences: resultsTablePreferences:
typebot.resultsTablePreferences === null typebot.resultsTablePreferences === null

View File

@ -4,6 +4,9 @@ import { Plan } from '@typebot.io/prisma'
import { Block, Typebot } from '@typebot.io/schemas' import { Block, Typebot } from '@typebot.io/schemas'
import { IntegrationBlockType } from '@typebot.io/schemas/features/blocks/integrations/constants' import { IntegrationBlockType } from '@typebot.io/schemas/features/blocks/integrations/constants'
import { defaultSendEmailOptions } from '@typebot.io/schemas/features/blocks/integrations/sendEmail/constants' import { defaultSendEmailOptions } from '@typebot.io/schemas/features/blocks/integrations/sendEmail/constants'
import { LogicBlockType } from '@typebot.io/schemas/features/blocks/logic/constants'
import { sessionOnlySetVariableOptions } from '@typebot.io/schemas/features/blocks/logic/setVariable/constants'
import { isInputBlock } from '@typebot.io/schemas/helpers'
export const sanitizeSettings = ( export const sanitizeSettings = (
settings: Typebot['settings'], settings: Typebot['settings'],
@ -160,3 +163,38 @@ export const sanitizeCustomDomain = async ({
}) })
return domainCount === 0 ? null : customDomain return domainCount === 0 ? null : customDomain
} }
export const sanitizeVariables = ({
variables,
groups,
}: Pick<Typebot, 'variables' | 'groups'>): Typebot['variables'] => {
const blocks = groups
.flatMap((group) => group.blocks as Block[])
.filter((b) => isInputBlock(b) || b.type === LogicBlockType.SET_VARIABLE)
return variables.map((variable) => {
if (variable.isSessionVariable) return variable
const isVariableLinkedToInputBlock = blocks.some(
(block) =>
isInputBlock(block) && block.options?.variableId === variable.id
)
if (isVariableLinkedToInputBlock)
return {
...variable,
isSessionVariable: true,
}
const isVariableSetToForbiddenResultVar = blocks.some(
(block) =>
block.type === LogicBlockType.SET_VARIABLE &&
block.options?.variableId === variable.id &&
sessionOnlySetVariableOptions.includes(
block.options.type as (typeof sessionOnlySetVariableOptions)[number]
)
)
if (isVariableSetToForbiddenResultVar)
return {
...variable,
isSessionVariable: true,
}
return variable
})
}

View File

@ -105,6 +105,7 @@ export const startWhatsAppPreview = authenticatedProcedure
clientSideActions, clientSideActions,
logs, logs,
visitedEdges, visitedEdges,
setVariableHistory,
} = await startSession({ } = await startSession({
version: 2, version: 2,
message: undefined, message: undefined,
@ -145,6 +146,7 @@ export const startWhatsAppPreview = authenticatedProcedure
state: newSessionState, state: newSessionState,
}, },
visitedEdges, visitedEdges,
setVariableHistory,
}) })
} else { } else {
await restartSession({ await restartSession({

View File

@ -1,101 +1,99 @@
{ {
"id": "cl0iecee90042961arm5kb0f0", "version": "6",
"createdAt": "2022-03-08T17:18:50.337Z", "id": "qk6zz1ag2jnm3yny7fzqrbwn",
"updatedAt": "2022-03-08T21:05:28.825Z", "name": "My link typebot 2",
"name": "Another typebot", "events": [
"folderId": null,
"groups": [
{ {
"id": "p4ByLVoKiDRyRoPHKmcTfw", "id": "p4ByLVoKiDRyRoPHKmcTfw",
"blocks": [ "outgoingEdgeId": "1z3pfiatTUHbraD2uSoA3E",
{ "graphCoordinates": { "x": 0, "y": 0 },
"id": "rw6smEWEJzHKbiVKLUKFvZ", "type": "start"
"type": "start", }
"label": "Start", ],
"groupId": "p4ByLVoKiDRyRoPHKmcTfw", "groups": [
"outgoingEdgeId": "1z3pfiatTUHbraD2uSoA3E"
}
],
"title": "Start",
"graphCoordinates": { "x": 0, "y": 0 }
},
{ {
"id": "bg4QEJseUsTP496H27j5k2", "id": "bg4QEJseUsTP496H27j5k2",
"title": "Group #1",
"graphCoordinates": { "x": 366, "y": 191 },
"blocks": [ "blocks": [
{ {
"id": "s8ZeBL9p5za77eBmdKECLYq", "id": "s8ZeBL9p5za77eBmdKECLYq",
"outgoingEdgeId": "aEBnubX4EMx4Cse6xPAR1m",
"type": "text input", "type": "text input",
"groupId": "bg4QEJseUsTP496H27j5k2",
"options": { "options": {
"isLong": false, "labels": {
"labels": { "button": "Send", "placeholder": "Type your answer..." } "placeholder": "Type your answer...",
}, "button": "Send"
"outgoingEdgeId": "aEBnubX4EMx4Cse6xPAR1m" },
"isLong": false
}
} }
], ]
"title": "Group #1",
"graphCoordinates": { "x": 366, "y": 191 }
}, },
{ {
"id": "uhqCZSNbsYVFxop7Gc8xvn", "id": "uhqCZSNbsYVFxop7Gc8xvn",
"title": "Group #2",
"graphCoordinates": { "x": 793, "y": 99 },
"blocks": [ "blocks": [
{ {
"id": "smyHyeS6yaFaHHU44BNmN4n", "id": "smyHyeS6yaFaHHU44BNmN4n",
"type": "text", "type": "text",
"groupId": "uhqCZSNbsYVFxop7Gc8xvn",
"content": { "content": {
"richText": [ "richText": [
{ "type": "p", "children": [{ "text": "Second block" }] } { "type": "p", "children": [{ "text": "Second block" }] }
] ]
} }
} }
], ]
"title": "Group #2",
"graphCoordinates": { "x": 793, "y": 99 }
} }
], ],
"variables": [],
"edges": [ "edges": [
{ {
"id": "1z3pfiatTUHbraD2uSoA3E", "id": "1z3pfiatTUHbraD2uSoA3E",
"to": { "groupId": "bg4QEJseUsTP496H27j5k2" }, "from": { "eventId": "p4ByLVoKiDRyRoPHKmcTfw" },
"from": { "to": { "groupId": "bg4QEJseUsTP496H27j5k2" }
"blockId": "rw6smEWEJzHKbiVKLUKFvZ",
"groupId": "p4ByLVoKiDRyRoPHKmcTfw"
}
}, },
{ {
"id": "aEBnubX4EMx4Cse6xPAR1m", "id": "aEBnubX4EMx4Cse6xPAR1m",
"to": { "groupId": "uhqCZSNbsYVFxop7Gc8xvn" }, "from": { "blockId": "s8ZeBL9p5za77eBmdKECLYq" },
"from": { "to": { "groupId": "uhqCZSNbsYVFxop7Gc8xvn" }
"blockId": "s8ZeBL9p5za77eBmdKECLYq",
"groupId": "bg4QEJseUsTP496H27j5k2"
}
} }
], ],
"variables": [],
"theme": { "theme": {
"general": { "font": "Open Sans", "background": { "type": "None" } },
"chat": { "chat": {
"hostBubbles": { "backgroundColor": "#F7F8FF", "color": "#303235" },
"guestBubbles": { "backgroundColor": "#FF8E21", "color": "#FFFFFF" },
"buttons": { "backgroundColor": "#0042DA", "color": "#FFFFFF" },
"inputs": { "inputs": {
"color": "#303235",
"backgroundColor": "#FFFFFF", "backgroundColor": "#FFFFFF",
"color": "#303235",
"placeholderColor": "#9095A0" "placeholderColor": "#9095A0"
}, }
"buttons": { "color": "#FFFFFF", "backgroundColor": "#0042DA" }, }
"hostBubbles": { "color": "#303235", "backgroundColor": "#F7F8FF" },
"guestBubbles": { "color": "#FFFFFF", "backgroundColor": "#FF8E21" }
},
"general": { "font": "Open Sans", "background": { "type": "None" } }
}, },
"selectedThemeTemplateId": null,
"settings": { "settings": {
"general": { "general": {
"isBrandingEnabled": true, "isBrandingEnabled": true,
"isNewResultOnRefreshEnabled": false "isNewResultOnRefreshEnabled": false
}, },
"typingEmulation": { "enabled": true, "speed": 300, "maxDelay": 1.5 },
"metadata": { "metadata": {
"description": "Build beautiful conversational forms and embed them directly in your applications without a line of code. Triple your response rate and collect answers that has more value compared to a traditional form." "description": "Build beautiful conversational forms and embed them directly in your applications without a line of code. Triple your response rate and collect answers that has more value compared to a traditional form."
}, }
"typingEmulation": { "speed": 300, "enabled": true, "maxDelay": 1.5 }
}, },
"createdAt": "2022-03-08T17:18:50.337Z",
"updatedAt": "2022-03-08T21:05:28.825Z",
"icon": null,
"folderId": null,
"publicId": null, "publicId": null,
"customDomain": null "customDomain": null,
"workspaceId": "proWorkspace",
"resultsTablePreferences": null,
"isArchived": false,
"isClosed": false,
"whatsAppCredentialsId": null,
"riskLevel": null
} }

View File

@ -0,0 +1,339 @@
{
"version": "6",
"id": "w8pny2noxxejt2kq36q3udll",
"name": "Customer Support",
"events": [
{
"id": "uG1tt8JdDyu2nju3oJ4wc1",
"outgoingEdgeId": "2dzxChB1qm9WGfzNF91tfg",
"graphCoordinates": { "x": -281, "y": -89 },
"type": "start"
}
],
"groups": [
{
"id": "vLUAPaxKwPF49iZhg4XZYa",
"title": "Menu",
"graphCoordinates": { "x": -268.18, "y": -40.15 },
"blocks": [
{
"id": "spud6U3K1omh2dZG8yN2CW4",
"type": "text",
"content": {
"richText": [
{ "type": "p", "children": [{ "text": "Hey friend 👋" }] },
{ "type": "p", "children": [{ "text": "How can I help you?" }] }
]
}
},
{
"id": "s6kp2Z4igeY3kL7B64qBdUg",
"type": "choice input",
"items": [
{
"id": "fQ8oLDnKmDBuPDK7riJ2kt",
"outgoingEdgeId": "dhniFxrsH5r54aEE5JXwK2",
"content": "I have a feature request ✨"
},
{
"id": "h2rFDX2UnKS4Kdu3Eyuqq3",
"outgoingEdgeId": "2C4mhU5o2Hdm7dztR9xNE9",
"content": "There is a bug 🐛"
},
{
"id": "hcUFBPeQA3gSyXRprRk2v9",
"outgoingEdgeId": "bTo6CZD1YapDDyVdvJgFDV",
"content": "I have a question 💭"
}
]
}
]
},
{
"id": "7MuqF6nen1ZTwGB53Mz8VY",
"title": "Bug",
"graphCoordinates": { "x": 57.55, "y": -57.03 },
"blocks": [
{
"id": "sjsECyfSBMkUnoWaEnBTmJX",
"type": "text",
"content": {
"richText": [{ "type": "p", "children": [{ "text": "Shoot! 🤪" }] }]
}
},
{
"id": "seomQsnPWgiMzQVeZ3us7x2",
"type": "text",
"content": {
"richText": [
{
"type": "p",
"children": [
{
"text": "Can you describe the bug with as many details as possible?"
}
]
}
]
}
},
{
"id": "s3LYyyYtjdQ88jkMMV5DSW7",
"outgoingEdgeId": "s6i6m1vmx9vl0rniev5iymp1",
"type": "text input",
"options": {
"labels": { "placeholder": "Describe the bug..." },
"variableId": "v51BcuecnB6kRU1tsttaGyR",
"isLong": true
}
}
]
},
{
"id": "kyK8JQ77NodUYaz3JLS88A",
"title": "Feature request",
"graphCoordinates": { "x": 364.36, "y": -517.93 },
"blocks": [
{
"id": "s9bgHcWdobb8Z5cTbrnTz6R",
"type": "text",
"content": {
"richText": [{ "type": "p", "children": [{ "text": "Awesome!" }] }]
}
},
{
"id": "s2NbNaBGKhMvdEUdVPXKZjy",
"type": "text",
"content": {
"richText": [
{
"type": "p",
"children": [{ "text": "Head up to the feedback board." }]
},
{ "type": "p", "children": [{ "text": "" }] },
{
"type": "p",
"children": [
{ "text": "👉 " },
{
"url": "https://app.typebot.io/feedback",
"type": "a",
"children": [{ "text": "https://app.typebot.io/feedback" }]
},
{ "text": "" }
]
},
{ "type": "p", "children": [{ "text": "" }] },
{
"type": "p",
"children": [
{
"text": "There, you'll be able to check existing feature requests and submit yours if it's missing 💪"
}
]
}
]
}
},
{
"id": "cl16lb3b300092e6dh4h01vxw",
"type": "choice input",
"items": [
{
"id": "cl16lb3b3000a2e6dy8zdhzpz",
"outgoingEdgeId": "wsbg8ht5das922mkojzjh0yy",
"content": "Restart"
}
]
},
{
"id": "j08qxg0h804rngfroedblt5f",
"type": "Jump",
"options": { "groupId": "vLUAPaxKwPF49iZhg4XZYa" }
}
]
},
{
"id": "puWCBhGWSQRbqTkVH89RCf",
"title": "Question",
"graphCoordinates": { "x": -234.07, "y": 457.59 },
"blocks": [
{
"id": "sm4iHhLQs9yNdRG3b7xqV8Y",
"type": "text",
"content": {
"richText": [
{
"type": "p",
"children": [
{ "text": "First, don't forget to check out the " },
{
"url": "https://docs.typebot.io/",
"type": "a",
"children": [{ "text": "Documentation 🙏" }]
}
]
}
]
}
},
{
"id": "sreX6rwMevEmbTpnkGCtp3k",
"type": "text",
"content": {
"richText": [
{
"type": "p",
"children": [{ "text": "Otherwise, I'm all ears!" }]
}
]
}
},
{
"id": "so4GiKFWWjKCjXgmMJYCGbe",
"type": "image",
"content": {
"url": "https://media0.giphy.com/media/rhgwg4qBu97ISgbfni/giphy-downsized.gif?cid=fe3852a3wimy48e55djt23j44uto7gdlu8ksytylafisvr0q&rid=giphy-downsized.gif&ct=g"
}
},
{
"id": "sjd4qACugMarB7gJC8nMhb3",
"outgoingEdgeId": "vojurfd82lye9yhtag3rie62",
"type": "text input",
"options": { "variableId": "v51BcuecnB6kRU1tsttaGyR", "isLong": true }
}
]
},
{
"id": "1GvxCAAEysxJMxrVngud3X",
"title": "Bye",
"graphCoordinates": { "x": 143.62, "y": 360.18 },
"blocks": [
{
"id": "s4JATFkBxzmcqqEKQB2xFfa",
"type": "choice input",
"items": [
{ "id": "jqm8wZa5yYb73493n5s3Uc", "content": "Restart" },
{
"id": "iszohxs8m1yfe0o1q6skmqo5",
"outgoingEdgeId": "cqcldkfg50a3lxw8kf6bze2e",
"content": "Transcription"
}
]
},
{
"id": "igdnc34rcmiyamazghr8s708",
"type": "Jump",
"options": { "groupId": "vLUAPaxKwPF49iZhg4XZYa" }
}
]
},
{
"id": "lhs4apmv49e4zn4vshbqnk0n",
"title": "Group #6",
"graphCoordinates": { "x": 460.78, "y": 359.03 },
"blocks": [
{
"id": "m1s6w5baydn76trkl145iokz",
"type": "Set variable",
"options": { "variableId": "vpdyhwqidox0fu265z5r1pxr4" }
},
{
"id": "sdv8sulyi6pg2z2qybzjqmbs",
"type": "text",
"content": {
"richText": [
{ "type": "p", "children": [{ "text": "{{Transcription}}" }] }
]
}
}
]
}
],
"edges": [
{
"id": "2dzxChB1qm9WGfzNF91tfg",
"from": { "eventId": "uG1tt8JdDyu2nju3oJ4wc1" },
"to": { "groupId": "vLUAPaxKwPF49iZhg4XZYa" }
},
{
"id": "dhniFxrsH5r54aEE5JXwK2",
"from": {
"blockId": "s6kp2Z4igeY3kL7B64qBdUg",
"itemId": "fQ8oLDnKmDBuPDK7riJ2kt"
},
"to": { "groupId": "kyK8JQ77NodUYaz3JLS88A" }
},
{
"id": "2C4mhU5o2Hdm7dztR9xNE9",
"from": {
"blockId": "s6kp2Z4igeY3kL7B64qBdUg",
"itemId": "h2rFDX2UnKS4Kdu3Eyuqq3"
},
"to": { "groupId": "7MuqF6nen1ZTwGB53Mz8VY" }
},
{
"id": "bTo6CZD1YapDDyVdvJgFDV",
"from": {
"blockId": "s6kp2Z4igeY3kL7B64qBdUg",
"itemId": "hcUFBPeQA3gSyXRprRk2v9"
},
"to": { "groupId": "puWCBhGWSQRbqTkVH89RCf" }
},
{
"id": "cl1571xtc00042e6dcptam5jw",
"from": { "blockId": "s5Fh7zHUw3j4zDM5xjzwsXB" },
"to": { "groupId": "1GvxCAAEysxJMxrVngud3X" }
},
{
"id": "vojurfd82lye9yhtag3rie62",
"from": { "blockId": "sjd4qACugMarB7gJC8nMhb3" },
"to": { "groupId": "1GvxCAAEysxJMxrVngud3X" }
},
{
"id": "s6i6m1vmx9vl0rniev5iymp1",
"from": { "blockId": "s3LYyyYtjdQ88jkMMV5DSW7" },
"to": { "groupId": "1GvxCAAEysxJMxrVngud3X" }
},
{
"id": "wsbg8ht5das922mkojzjh0yy",
"from": {
"blockId": "cl16lb3b300092e6dh4h01vxw",
"itemId": "cl16lb3b3000a2e6dy8zdhzpz"
},
"to": { "groupId": "1GvxCAAEysxJMxrVngud3X" }
},
{
"id": "cqcldkfg50a3lxw8kf6bze2e",
"from": {
"blockId": "s4JATFkBxzmcqqEKQB2xFfa",
"itemId": "iszohxs8m1yfe0o1q6skmqo5"
},
"to": { "groupId": "lhs4apmv49e4zn4vshbqnk0n" }
}
],
"variables": [
{ "id": "t2k6cj3uYfNdJX13APA4b9", "name": "Email" },
{ "id": "v51BcuecnB6kRU1tsttaGyR", "name": "Content" },
{
"id": "vpdyhwqidox0fu265z5r1pxr4",
"name": "Transcription",
"isSessionVariable": true
}
],
"theme": {},
"selectedThemeTemplateId": null,
"settings": { "typingEmulation": { "enabled": false } },
"createdAt": "2024-05-04T09:07:37.028Z",
"updatedAt": "2024-05-04T09:09:56.735Z",
"icon": "😍",
"folderId": null,
"publicId": null,
"customDomain": null,
"workspaceId": "proWorkspace",
"resultsTablePreferences": null,
"isArchived": false,
"isClosed": false,
"whatsAppCredentialsId": null,
"riskLevel": null
}

View File

@ -587,7 +587,8 @@
"Result ID", "Result ID",
"Random ID", "Random ID",
"Phone number", "Phone number",
"Contact name" "Contact name",
"Transcript"
] ]
} }
}, },
@ -2679,10 +2680,10 @@
} }
} }
}, },
"/v1/typebots/{typebotId}/analytics/totalAnswersInBlocks": { "/v1/typebots/{typebotId}/analytics/inDepthData": {
"get": { "get": {
"operationId": "analytics-getTotalAnswers", "operationId": "analytics-getInDepthAnalyticsData",
"summary": "List total answers in blocks", "summary": "List total answers in blocks and off-default paths visited edges",
"tags": [ "tags": [
"Analytics" "Analytics"
], ],
@ -2753,123 +2754,8 @@
"total" "total"
] ]
} }
} },
}, "offDefaultPathVisitedEdges": {
"required": [
"totalAnswers"
]
}
}
}
},
"400": {
"description": "Invalid input data",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/error.BAD_REQUEST"
}
}
}
},
"401": {
"description": "Authorization not provided",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/error.UNAUTHORIZED"
}
}
}
},
"403": {
"description": "Insufficient access",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/error.FORBIDDEN"
}
}
}
},
"404": {
"description": "Not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/error.NOT_FOUND"
}
}
}
},
"500": {
"description": "Internal server error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/error.INTERNAL_SERVER_ERROR"
}
}
}
}
}
}
},
"/v1/typebots/{typebotId}/analytics/totalVisitedEdges": {
"get": {
"operationId": "analytics-getTotalVisitedEdges",
"summary": "List total edges used in results",
"tags": [
"Analytics"
],
"security": [
{
"Authorization": []
}
],
"parameters": [
{
"in": "path",
"name": "typebotId",
"schema": {
"type": "string"
},
"required": true
},
{
"in": "query",
"name": "timeFilter",
"schema": {
"type": "string",
"enum": [
"today",
"last7Days",
"last30Days",
"monthToDate",
"lastMonth",
"yearToDate",
"allTime"
],
"default": "last7Days"
}
},
{
"in": "query",
"name": "timeZone",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Successful response",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"totalVisitedEdges": {
"type": "array", "type": "array",
"items": { "items": {
"type": "object", "type": "object",
@ -2889,7 +2775,8 @@
} }
}, },
"required": [ "required": [
"totalVisitedEdges" "totalAnswers",
"offDefaultPathVisitedEdges"
] ]
} }
} }
@ -5022,7 +4909,8 @@
"Result ID", "Result ID",
"Random ID", "Random ID",
"Phone number", "Phone number",
"Contact name" "Contact name",
"Transcript"
] ]
} }
}, },
@ -8480,7 +8368,8 @@
"Result ID", "Result ID",
"Random ID", "Random ID",
"Phone number", "Phone number",
"Contact name" "Contact name",
"Transcript"
] ]
} }
}, },
@ -11203,43 +11092,25 @@
"type": "boolean", "type": "boolean",
"nullable": true "nullable": true
}, },
"lastChatSessionId": {
"type": "string",
"nullable": true
},
"answers": { "answers": {
"type": "array", "type": "array",
"items": { "items": {
"type": "object", "type": "object",
"properties": { "properties": {
"createdAt": {
"type": "string"
},
"resultId": {
"type": "string"
},
"blockId": { "blockId": {
"type": "string" "type": "string"
}, },
"groupId": {
"type": "string"
},
"variableId": {
"type": "string",
"nullable": true
},
"content": { "content": {
"type": "string" "type": "string"
},
"storageUsed": {
"type": "number",
"nullable": true
} }
}, },
"required": [ "required": [
"createdAt",
"resultId",
"blockId", "blockId",
"groupId", "content"
"variableId",
"content",
"storageUsed"
] ]
} }
} }
@ -11252,6 +11123,7 @@
"isCompleted", "isCompleted",
"hasStarted", "hasStarted",
"isArchived", "isArchived",
"lastChatSessionId",
"answers" "answers"
] ]
} }
@ -11515,43 +11387,25 @@
"type": "boolean", "type": "boolean",
"nullable": true "nullable": true
}, },
"lastChatSessionId": {
"type": "string",
"nullable": true
},
"answers": { "answers": {
"type": "array", "type": "array",
"items": { "items": {
"type": "object", "type": "object",
"properties": { "properties": {
"createdAt": {
"type": "string"
},
"resultId": {
"type": "string"
},
"blockId": { "blockId": {
"type": "string" "type": "string"
}, },
"groupId": {
"type": "string"
},
"variableId": {
"type": "string",
"nullable": true
},
"content": { "content": {
"type": "string" "type": "string"
},
"storageUsed": {
"type": "number",
"nullable": true
} }
}, },
"required": [ "required": [
"createdAt",
"resultId",
"blockId", "blockId",
"groupId", "content"
"variableId",
"content",
"storageUsed"
] ]
} }
} }
@ -11564,6 +11418,7 @@
"isCompleted", "isCompleted",
"hasStarted", "hasStarted",
"isArchived", "isArchived",
"lastChatSessionId",
"answers" "answers"
] ]
} }
@ -17011,7 +16866,8 @@
"Result ID", "Result ID",
"Random ID", "Random ID",
"Phone number", "Phone number",
"Contact name" "Contact name",
"Transcript"
] ]
} }
}, },
@ -22665,7 +22521,8 @@
"Result ID", "Result ID",
"Random ID", "Random ID",
"Phone number", "Phone number",
"Contact name" "Contact name",
"Transcript"
] ]
} }
}, },
@ -25492,7 +25349,8 @@
"Result ID", "Result ID",
"Random ID", "Random ID",
"Phone number", "Phone number",
"Contact name" "Contact name",
"Transcript"
] ]
} }
}, },

View File

@ -1851,6 +1851,10 @@
"First name": "John", "First name": "John",
"Email": "john@gmail.com" "Email": "john@gmail.com"
} }
},
"sessionId": {
"type": "string",
"description": "If provided, will be used as the session ID and will overwrite any existing session with the same ID."
} }
} }
} }
@ -3675,7 +3679,8 @@
"Result ID", "Result ID",
"Random ID", "Random ID",
"Phone number", "Phone number",
"Contact name" "Contact name",
"Transcript"
] ]
} }
}, },
@ -7818,7 +7823,8 @@
"Result ID", "Result ID",
"Random ID", "Random ID",
"Phone number", "Phone number",
"Contact name" "Contact name",
"Transcript"
] ]
} }
}, },

View File

@ -29,7 +29,7 @@ export default defineConfig({
use: { use: {
trace: 'on-first-retry', trace: 'on-first-retry',
locale: 'en-US', locale: 'en-US',
baseURL: process.env.NEXT_PUBLIC_VIEWER_URL, baseURL: process.env.NEXT_PUBLIC_VIEWER_URL?.split(',')[0],
}, },
projects: [ projects: [
{ {

View File

@ -60,6 +60,7 @@ export const sendMessageV1 = publicProcedure
clientSideActions, clientSideActions,
newSessionState, newSessionState,
visitedEdges, visitedEdges,
setVariableHistory,
} = await startSession({ } = await startSession({
version: 1, version: 1,
startParams: startParams:
@ -136,6 +137,7 @@ export const sendMessageV1 = publicProcedure
hasCustomEmbedBubble: messages.some( hasCustomEmbedBubble: messages.some(
(message) => message.type === 'custom-embed' (message) => message.type === 'custom-embed'
), ),
setVariableHistory,
}) })
return { return {
@ -176,6 +178,7 @@ export const sendMessageV1 = publicProcedure
logs, logs,
lastMessageNewFormat, lastMessageNewFormat,
visitedEdges, visitedEdges,
setVariableHistory,
} = await continueBotFlow(message, { version: 1, state: session.state }) } = await continueBotFlow(message, { version: 1, state: session.state })
const allLogs = clientLogs ? [...(logs ?? []), ...clientLogs] : logs const allLogs = clientLogs ? [...(logs ?? []), ...clientLogs] : logs
@ -193,6 +196,7 @@ export const sendMessageV1 = publicProcedure
hasCustomEmbedBubble: messages.some( hasCustomEmbedBubble: messages.some(
(message) => message.type === 'custom-embed' (message) => message.type === 'custom-embed'
), ),
setVariableHistory,
}) })
return { return {

View File

@ -60,6 +60,7 @@ export const sendMessageV2 = publicProcedure
clientSideActions, clientSideActions,
newSessionState, newSessionState,
visitedEdges, visitedEdges,
setVariableHistory,
} = await startSession({ } = await startSession({
version: 2, version: 2,
startParams: startParams:
@ -136,6 +137,7 @@ export const sendMessageV2 = publicProcedure
hasCustomEmbedBubble: messages.some( hasCustomEmbedBubble: messages.some(
(message) => message.type === 'custom-embed' (message) => message.type === 'custom-embed'
), ),
setVariableHistory,
}) })
return { return {
@ -175,6 +177,7 @@ export const sendMessageV2 = publicProcedure
logs, logs,
lastMessageNewFormat, lastMessageNewFormat,
visitedEdges, visitedEdges,
setVariableHistory,
} = await continueBotFlow(message, { version: 2, state: session.state }) } = await continueBotFlow(message, { version: 2, state: session.state })
const allLogs = clientLogs ? [...(logs ?? []), ...clientLogs] : logs const allLogs = clientLogs ? [...(logs ?? []), ...clientLogs] : logs
@ -192,6 +195,7 @@ export const sendMessageV2 = publicProcedure
hasCustomEmbedBubble: messages.some( hasCustomEmbedBubble: messages.some(
(message) => message.type === 'custom-embed' (message) => message.type === 'custom-embed'
), ),
setVariableHistory,
}) })
return { return {

View File

@ -27,6 +27,7 @@ export const startChatPreview = publicProcedure
typebotId, typebotId,
typebot: startTypebot, typebot: startTypebot,
prefilledVariables, prefilledVariables,
sessionId,
}, },
ctx: { user }, ctx: { user },
}) => }) =>
@ -39,5 +40,6 @@ export const startChatPreview = publicProcedure
typebot: startTypebot, typebot: startTypebot,
userId: user?.id, userId: user?.id,
prefilledVariables, prefilledVariables,
sessionId,
}) })
) )

View File

@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { import {
ResultValues, ResultValues,
Typebot, Typebot,
@ -36,7 +37,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const { resultValues, variables, parentTypebotIds } = ( const { resultValues, variables, parentTypebotIds } = (
typeof req.body === 'string' ? JSON.parse(req.body) : req.body typeof req.body === 'string' ? JSON.parse(req.body) : req.body
) as { ) as {
resultValues: ResultValues | undefined resultValues: ResultValues
variables: Variable[] variables: Variable[]
parentTypebotIds: string[] parentTypebotIds: string[]
} }
@ -76,7 +77,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const linkedTypebots = [...linkedTypebotsParents, ...linkedTypebotsChildren] const linkedTypebots = [...linkedTypebotsParents, ...linkedTypebotsChildren]
const answers = resultValues const answers = resultValues
? resultValues.answers.map((answer) => ({ ? resultValues.answers.map((answer: any) => ({
key: key:
(answer.variableId (answer.variableId
? typebot.variables.find( ? typebot.variables.find(

View File

@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { import {
PublicTypebot, PublicTypebot,
ResultValues, ResultValues,
@ -203,7 +204,7 @@ const getEmailBody = async ({
})) as unknown as PublicTypebot })) as unknown as PublicTypebot
if (!typebot) return if (!typebot) return
const answers = parseAnswers({ const answers = parseAnswers({
answers: resultValues.answers.map((answer) => ({ answers: (resultValues as any).answers.map((answer: any) => ({
key: key:
(answer.variableId (answer.variableId
? typebot.variables.find( ? typebot.variables.find(

View File

@ -1,38 +1,16 @@
import prisma from '@typebot.io/lib/prisma' import prisma from '@typebot.io/lib/prisma'
import { Answer } from '@typebot.io/prisma' import { Answer } from '@typebot.io/prisma'
import { got } from 'got'
import { NextApiRequest, NextApiResponse } from 'next' import { NextApiRequest, NextApiResponse } from 'next'
import { isNotDefined } from '@typebot.io/lib'
import { methodNotAllowed } from '@typebot.io/lib/api' import { methodNotAllowed } from '@typebot.io/lib/api'
const handler = async (req: NextApiRequest, res: NextApiResponse) => { const handler = async (req: NextApiRequest, res: NextApiResponse) => {
if (req.method === 'PUT') { if (req.method === 'PUT') {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { uploadedFiles, ...answer } = ( const { uploadedFiles, ...answer } = (
typeof req.body === 'string' ? JSON.parse(req.body) : req.body typeof req.body === 'string' ? JSON.parse(req.body) : req.body
) as Answer & { uploadedFiles?: boolean } ) as Answer & { uploadedFiles: string[] }
let storageUsed = 0 const result = await prisma.answer.createMany({
if (uploadedFiles && answer.content.includes('http')) { data: [{ ...answer }],
const fileUrls = answer.content.split(', ')
const hasReachedStorageLimit = fileUrls[0] === null
if (!hasReachedStorageLimit) {
for (const url of fileUrls) {
const { headers } = await got(url)
const size = headers['content-length']
if (isNotDefined(size)) return
storageUsed += parseInt(size, 10)
}
}
}
const result = await prisma.answer.upsert({
where: {
resultId_blockId_groupId: {
resultId: answer.resultId,
groupId: answer.groupId,
blockId: answer.blockId,
},
},
create: { ...answer, storageUsed: storageUsed > 0 ? storageUsed : null },
update: { ...answer, storageUsed: storageUsed > 0 ? storageUsed : null },
}) })
return res.send(result) return res.send(result)
} }

View File

@ -0,0 +1,180 @@
{
"version": "6",
"id": "clvxf9ent0001tjmcr3e4bypk",
"name": "My typebot",
"events": [
{
"id": "d8r5fbpb2eqsq8egwydygiu2",
"outgoingEdgeId": "eblhvxj5u2tmwr1459lwxrjh",
"graphCoordinates": { "x": 0, "y": 0 },
"type": "start"
}
],
"groups": [
{
"id": "tfsvlygr7lay21s5w475syd8",
"title": "Answer",
"graphCoordinates": { "x": 579.3, "y": -31.43 },
"blocks": [
{
"id": "pkadqaxm0ow0qvt4d6k9eznd",
"type": "text",
"content": {
"richText": [
{
"type": "p",
"children": [{ "text": "How are you? You said {{Answer}}" }]
}
]
}
},
{
"id": "n8b3zi7wd6eory602o006e2a",
"type": "text input",
"options": { "variableId": "vzbq6pomlaf5bhav64ef5wx3d" }
},
{
"id": "byrr3jxa2qh3imilup7yu1bz",
"outgoingEdgeId": "yyc69sbg26ygd7oofetqrmj3",
"type": "Set variable",
"options": {
"variableId": "ve15gxz2fsq004tcqbub0d4m4",
"expressionToEvaluate": "{{Answers count}} + 1"
}
}
]
},
{
"id": "sw6habablg7wmyxzcat99wia",
"title": "Condition",
"graphCoordinates": { "x": 950.69, "y": -30.46 },
"blocks": [
{
"id": "k7hs4zsybbbece1b0080d2pj",
"type": "Condition",
"items": [
{
"id": "ukawer7gc6qdpr4eh0fw2pnv",
"content": {
"comparisons": [
{
"id": "jgb06bu8qz0va8vtnarqxivd",
"variableId": "ve15gxz2fsq004tcqbub0d4m4",
"comparisonOperator": "Equal to",
"value": "3"
}
]
},
"outgoingEdgeId": "hyel5nw6btuiudmt83b25dvu"
}
],
"outgoingEdgeId": "cz2ayuq8nsoqosxlzu8pyebd"
}
]
},
{
"id": "kpmjs3nqbbq88f63us13yqyy",
"title": "Init",
"graphCoordinates": { "x": 235.16, "y": -17.47 },
"blocks": [
{
"id": "w487kr9s9wg3mar7ilfr3tep",
"outgoingEdgeId": "mdcj3y9t8kh4uy8lhoh4avdj",
"type": "Set variable",
"options": {
"variableId": "ve15gxz2fsq004tcqbub0d4m4",
"expressionToEvaluate": "0"
}
}
]
},
{
"id": "wno2kz74jmhzgbi05z4ftjoj",
"title": "Transcript",
"graphCoordinates": { "x": 1308.8, "y": -41 },
"blocks": [
{
"id": "ejy8vk6gnzegn5copktmw74q",
"type": "Set variable",
"options": {
"variableId": "vs2p20vizsf45xcpgwq5ab3rw",
"type": "Transcript"
}
},
{
"id": "qoa74xt647j42sk5b0yyvz9k",
"type": "text",
"content": {
"richText": [
{ "type": "p", "children": [{ "text": "{{Transcript}}" }] }
]
}
}
]
}
],
"edges": [
{
"id": "eblhvxj5u2tmwr1459lwxrjh",
"from": { "eventId": "d8r5fbpb2eqsq8egwydygiu2" },
"to": { "groupId": "kpmjs3nqbbq88f63us13yqyy" }
},
{
"id": "mdcj3y9t8kh4uy8lhoh4avdj",
"from": { "blockId": "w487kr9s9wg3mar7ilfr3tep" },
"to": { "groupId": "tfsvlygr7lay21s5w475syd8" }
},
{
"id": "yyc69sbg26ygd7oofetqrmj3",
"from": { "blockId": "byrr3jxa2qh3imilup7yu1bz" },
"to": { "groupId": "sw6habablg7wmyxzcat99wia" }
},
{
"from": {
"blockId": "k7hs4zsybbbece1b0080d2pj",
"itemId": "ukawer7gc6qdpr4eh0fw2pnv"
},
"to": { "groupId": "wno2kz74jmhzgbi05z4ftjoj" },
"id": "hyel5nw6btuiudmt83b25dvu"
},
{
"from": { "blockId": "k7hs4zsybbbece1b0080d2pj" },
"to": { "groupId": "tfsvlygr7lay21s5w475syd8" },
"id": "cz2ayuq8nsoqosxlzu8pyebd"
}
],
"variables": [
{
"id": "ve15gxz2fsq004tcqbub0d4m4",
"name": "Answers count",
"isSessionVariable": true
},
{
"id": "vs2p20vizsf45xcpgwq5ab3rw",
"name": "Transcript",
"isSessionVariable": true
},
{
"id": "vzbq6pomlaf5bhav64ef5wx3d",
"name": "Answer",
"isSessionVariable": true
}
],
"theme": {},
"selectedThemeTemplateId": null,
"settings": {
"typingEmulation": { "enabled": false }
},
"createdAt": "2024-05-08T06:11:55.385Z",
"updatedAt": "2024-05-08T06:28:18.313Z",
"icon": null,
"folderId": null,
"publicId": null,
"customDomain": null,
"workspaceId": "proWorkspace",
"resultsTablePreferences": null,
"isArchived": false,
"isClosed": false,
"whatsAppCredentialsId": null,
"riskLevel": null
}

View File

@ -0,0 +1,34 @@
import test, { expect } from '@playwright/test'
import { createId } from '@paralleldrive/cuid2'
import { importTypebotInDatabase } from '@typebot.io/playwright/databaseActions'
import { getTestAsset } from './utils/playwright'
test('Transcript set variable should be correctly computed', async ({
page,
}) => {
const typebotId = createId()
await importTypebotInDatabase(getTestAsset('typebots/transcript.json'), {
id: typebotId,
publicId: `${typebotId}-public`,
})
await page.goto(`/${typebotId}-public`)
await page.getByPlaceholder('Type your answer...').fill('hey')
await page.getByRole('button').click()
await page.getByPlaceholder('Type your answer...').fill('hey 2')
await page.getByRole('button').click()
await page.getByPlaceholder('Type your answer...').fill('hey 3')
await page.getByRole('button').click()
await expect(
page.getByText('Assistant: "How are you? You said "')
).toBeVisible()
await expect(
page.getByText('Assistant: "How are you? You said hey"')
).toBeVisible()
await expect(
page.getByText('Assistant: "How are you? You said hey 3"')
).toBeVisible()
await expect(page.getByText('User: "hey"')).toBeVisible()
await expect(page.getByText('User: "hey 2"')).toBeVisible()
await expect(page.getByText('User: "hey 3"')).toBeVisible()
})

View File

@ -20,7 +20,7 @@ export const addEdgeToTypebot = (
}) })
export const createPortalEdge = ({ to }: Pick<Edge, 'to'>) => ({ export const createPortalEdge = ({ to }: Pick<Edge, 'to'>) => ({
id: createId(), id: 'virtual-' + createId(),
from: { blockId: '', groupId: '' }, from: { blockId: '', groupId: '' },
to, to,
}) })

View File

@ -52,6 +52,7 @@ export const continueChat = async ({ origin, sessionId, message }: Props) => {
logs, logs,
lastMessageNewFormat, lastMessageNewFormat,
visitedEdges, visitedEdges,
setVariableHistory,
} = await continueBotFlow(message, { } = await continueBotFlow(message, {
version: 2, version: 2,
state: session.state, state: session.state,
@ -68,6 +69,7 @@ export const continueChat = async ({ origin, sessionId, message }: Props) => {
logs, logs,
clientSideActions, clientSideActions,
visitedEdges, visitedEdges,
setVariableHistory,
hasCustomEmbedBubble: messages.some( hasCustomEmbedBubble: messages.some(
(message) => message.type === 'custom-embed' (message) => message.type === 'custom-embed'
), ),

View File

@ -16,6 +16,7 @@ import { isForgedBlockType } from '@typebot.io/schemas/features/blocks/forged/he
import { updateVariablesInSession } from '@typebot.io/variables/updateVariablesInSession' import { updateVariablesInSession } from '@typebot.io/variables/updateVariablesInSession'
import { updateSession } from '../queries/updateSession' import { updateSession } from '../queries/updateSession'
import { deepParseVariables } from '@typebot.io/variables/deepParseVariables' import { deepParseVariables } from '@typebot.io/variables/deepParseVariables'
import { saveSetVariableHistoryItems } from '../queries/saveSetVariableHistoryItems'
type Props = { type Props = {
sessionId: string sessionId: string
@ -114,11 +115,17 @@ export const getMessageStream = async ({ sessionId, messages }: Props) => {
(variable) => variable.id === id (variable) => variable.id === id
) )
if (!variable) return if (!variable) return
const { updatedState, newSetVariableHistory } =
updateVariablesInSession({
newVariables: [{ ...variable, value }],
state: session.state,
currentBlockId: session.state.currentBlockId,
})
if (newSetVariableHistory.length > 0)
await saveSetVariableHistoryItems(newSetVariableHistory)
await updateSession({ await updateSession({
id: session.id, id: session.id,
state: updateVariablesInSession(session.state)([ state: updatedState,
{ ...variable, value },
]),
isReplying: undefined, isReplying: undefined,
}) })
}, },

View File

@ -33,6 +33,7 @@ export const startChat = async ({
clientSideActions, clientSideActions,
newSessionState, newSessionState,
visitedEdges, visitedEdges,
setVariableHistory,
} = await startSession({ } = await startSession({
version: 2, version: 2,
startParams: { startParams: {
@ -69,6 +70,7 @@ export const startChat = async ({
logs, logs,
clientSideActions, clientSideActions,
visitedEdges, visitedEdges,
setVariableHistory,
hasCustomEmbedBubble: messages.some( hasCustomEmbedBubble: messages.some(
(message) => message.type === 'custom-embed' (message) => message.type === 'custom-embed'
), ),

View File

@ -13,6 +13,7 @@ type Props = {
typebot?: StartTypebot typebot?: StartTypebot
userId?: string userId?: string
prefilledVariables?: Record<string, unknown> prefilledVariables?: Record<string, unknown>
sessionId?: string
} }
export const startChatPreview = async ({ export const startChatPreview = async ({
@ -24,6 +25,7 @@ export const startChatPreview = async ({
typebot: startTypebot, typebot: startTypebot,
userId, userId,
prefilledVariables, prefilledVariables,
sessionId,
}: Props) => { }: Props) => {
const { const {
typebot, typebot,
@ -34,6 +36,7 @@ export const startChatPreview = async ({
clientSideActions, clientSideActions,
newSessionState, newSessionState,
visitedEdges, visitedEdges,
setVariableHistory,
} = await startSession({ } = await startSession({
version: 2, version: 2,
startParams: { startParams: {
@ -45,6 +48,7 @@ export const startChatPreview = async ({
typebot: startTypebot, typebot: startTypebot,
userId, userId,
prefilledVariables, prefilledVariables,
sessionId,
}, },
message, message,
}) })
@ -61,9 +65,11 @@ export const startChatPreview = async ({
logs, logs,
clientSideActions, clientSideActions,
visitedEdges, visitedEdges,
setVariableHistory,
hasCustomEmbedBubble: messages.some( hasCustomEmbedBubble: messages.some(
(message) => message.type === 'custom-embed' (message) => message.type === 'custom-embed'
), ),
initialSessionId: sessionId,
}) })
const isEnded = const isEnded =

View File

@ -1,12 +1,15 @@
import { executeCondition } from '@typebot.io/logic/executeCondition'
import { ChoiceInputBlock, Variable } from '@typebot.io/schemas' import { ChoiceInputBlock, Variable } from '@typebot.io/schemas'
import { executeCondition } from '../../logic/condition/executeCondition'
export const filterChoiceItems = export const filterChoiceItems =
(variables: Variable[]) => (variables: Variable[]) =>
(block: ChoiceInputBlock): ChoiceInputBlock => { (block: ChoiceInputBlock): ChoiceInputBlock => {
const filteredItems = block.items.filter((item) => { const filteredItems = block.items.filter((item) => {
if (item.displayCondition?.isEnabled && item.displayCondition?.condition) if (item.displayCondition?.isEnabled && item.displayCondition?.condition)
return executeCondition(variables)(item.displayCondition.condition) return executeCondition({
variables,
condition: item.displayCondition.condition,
})
return true return true
}) })

View File

@ -41,7 +41,6 @@ const getVariableValue =
const [transformedVariable] = transformVariablesToList(variables)([ const [transformedVariable] = transformVariablesToList(variables)([
variable.id, variable.id,
]) ])
updateVariablesInSession(state)([transformedVariable])
return transformedVariable.value as string[] return transformedVariable.value as string[]
} }
return variable.value return variable.value

View File

@ -1,12 +1,15 @@
import { executeCondition } from '@typebot.io/logic/executeCondition'
import { PictureChoiceBlock, Variable } from '@typebot.io/schemas' import { PictureChoiceBlock, Variable } from '@typebot.io/schemas'
import { executeCondition } from '../../logic/condition/executeCondition'
export const filterPictureChoiceItems = export const filterPictureChoiceItems =
(variables: Variable[]) => (variables: Variable[]) =>
(block: PictureChoiceBlock): PictureChoiceBlock => { (block: PictureChoiceBlock): PictureChoiceBlock => {
const filteredItems = block.items.filter((item) => { const filteredItems = block.items.filter((item) => {
if (item.displayCondition?.isEnabled && item.displayCondition?.condition) if (item.displayCondition?.isEnabled && item.displayCondition?.condition)
return executeCondition(variables)(item.displayCondition.condition) return executeCondition({
variables,
condition: item.displayCondition.condition,
})
return true return true
}) })

View File

@ -25,6 +25,7 @@ export const executeGoogleSheetBlock = async (
}) })
case GoogleSheetsAction.GET: case GoogleSheetsAction.GET:
return getRow(state, { return getRow(state, {
blockId: block.id,
options: block.options, options: block.options,
outgoingEdgeId: block.outgoingEdgeId, outgoingEdgeId: block.outgoingEdgeId,
}) })

View File

@ -14,9 +14,14 @@ import { updateVariablesInSession } from '@typebot.io/variables/updateVariablesI
export const getRow = async ( export const getRow = async (
state: SessionState, state: SessionState,
{ {
blockId,
outgoingEdgeId, outgoingEdgeId,
options, options,
}: { outgoingEdgeId?: string; options: GoogleSheetsGetOptions } }: {
blockId: string
outgoingEdgeId?: string
options: GoogleSheetsGetOptions
}
): Promise<ExecuteIntegrationResponse> => { ): Promise<ExecuteIntegrationResponse> => {
const logs: ChatLog[] = [] const logs: ChatLog[] = []
const { variables } = state.typebotsQueue[0].typebot const { variables } = state.typebotsQueue[0].typebot
@ -79,10 +84,15 @@ export const getRow = async (
[] []
) )
if (!newVariables) return { outgoingEdgeId } if (!newVariables) return { outgoingEdgeId }
const newSessionState = updateVariablesInSession(state)(newVariables) const { updatedState, newSetVariableHistory } = updateVariablesInSession({
state,
newVariables,
currentBlockId: blockId,
})
return { return {
outgoingEdgeId, outgoingEdgeId,
newSessionState, newSessionState: updatedState,
newSetVariableHistory,
} }
} catch (err) { } catch (err) {
logs.push({ logs.push({

View File

@ -107,12 +107,16 @@ export const createSpeechOpenAI = async (
mimeType: 'audio/mpeg', mimeType: 'audio/mpeg',
}) })
newSessionState = updateVariablesInSession(newSessionState)([ newSessionState = updateVariablesInSession({
{ newVariables: [
...saveUrlInVariable, {
value: url, ...saveUrlInVariable,
}, value: url,
]) },
],
state: newSessionState,
currentBlockId: undefined,
}).updatedState
return { return {
startTimeShouldBeUpdated: true, startTimeShouldBeUpdated: true,

View File

@ -22,7 +22,6 @@ import {
defaultOpenAIOptions, defaultOpenAIOptions,
} from '@typebot.io/schemas/features/blocks/integrations/openai/constants' } from '@typebot.io/schemas/features/blocks/integrations/openai/constants'
import { BubbleBlockType } from '@typebot.io/schemas/features/blocks/bubbles/constants' import { BubbleBlockType } from '@typebot.io/schemas/features/blocks/bubbles/constants'
import { isPlaneteScale } from '@typebot.io/lib/isPlanetScale'
export const createChatCompletionOpenAI = async ( export const createChatCompletionOpenAI = async (
state: SessionState, state: SessionState,
@ -68,9 +67,11 @@ export const createChatCompletionOpenAI = async (
typebot.variables typebot.variables
)(options.messages) )(options.messages)
if (variablesTransformedToList.length > 0) if (variablesTransformedToList.length > 0)
newSessionState = updateVariablesInSession(state)( newSessionState = updateVariablesInSession({
variablesTransformedToList state,
) newVariables: variablesTransformedToList,
currentBlockId: undefined,
}).updatedState
const temperature = parseVariableNumber(typebot.variables)( const temperature = parseVariableNumber(typebot.variables)(
options.advancedSettings?.temperature options.advancedSettings?.temperature

View File

@ -42,7 +42,11 @@ export const resumeChatCompletion =
return newVariables return newVariables
}, []) }, [])
if (newVariables && newVariables.length > 0) if (newVariables && newVariables.length > 0)
newSessionState = updateVariablesInSession(newSessionState)(newVariables) newSessionState = updateVariablesInSession({
newVariables,
state: newSessionState,
currentBlockId: undefined,
}).updatedState
return { return {
outgoingEdgeId, outgoingEdgeId,
newSessionState, newSessionState,

View File

@ -70,10 +70,15 @@ export const resumeWebhookExecution = ({
} }
}, []) }, [])
if (newVariables && newVariables.length > 0) { if (newVariables && newVariables.length > 0) {
const newSessionState = updateVariablesInSession(state)(newVariables) const { updatedState, newSetVariableHistory } = updateVariablesInSession({
newVariables,
state,
currentBlockId: block.id,
})
return { return {
outgoingEdgeId: block.outgoingEdgeId, outgoingEdgeId: block.outgoingEdgeId,
newSessionState, newSessionState: updatedState,
newSetVariableHistory,
logs, logs,
} }
} }

View File

@ -19,6 +19,7 @@ export const executeZemanticAiBlock = async (
block: ZemanticAiBlock block: ZemanticAiBlock
): Promise<ExecuteIntegrationResponse> => { ): Promise<ExecuteIntegrationResponse> => {
let newSessionState = state let newSessionState = state
let setVariableHistory = []
if (!block.options?.credentialsId) if (!block.options?.credentialsId)
return { return {
@ -82,24 +83,34 @@ export const executeZemanticAiBlock = async (
for (const r of block.options.responseMapping || []) { for (const r of block.options.responseMapping || []) {
const variable = typebot.variables.find(byId(r.variableId)) const variable = typebot.variables.find(byId(r.variableId))
let newVariables = []
switch (r.valueToExtract) { switch (r.valueToExtract) {
case 'Summary': case 'Summary':
if (isDefined(variable) && !isEmpty(res.summary)) { if (isDefined(variable) && !isEmpty(res.summary)) {
newSessionState = updateVariablesInSession(newSessionState)([ newVariables.push({ ...variable, value: res.summary })
{ ...variable, value: res.summary },
])
} }
break break
case 'Results': case 'Results':
if (isDefined(variable) && res.results.length) { if (isDefined(variable) && res.results.length) {
newSessionState = updateVariablesInSession(newSessionState)([ newVariables.push({
{ ...variable, value: JSON.stringify(res.results) }, ...variable,
]) value: JSON.stringify(res.results),
})
} }
break break
default: default:
break break
} }
if (newVariables.length > 0) {
const { newSetVariableHistory, updatedState } =
updateVariablesInSession({
newVariables,
state: newSessionState,
currentBlockId: block.id,
})
newSessionState = updatedState
setVariableHistory.push(...newSetVariableHistory)
}
} }
} catch (e) { } catch (e) {
console.error(e) console.error(e)
@ -112,6 +123,7 @@ export const executeZemanticAiBlock = async (
description: 'Could not execute Zemantic AI request', description: 'Could not execute Zemantic AI request',
}, },
], ],
newSetVariableHistory: setVariableHistory,
} }
} }

View File

@ -1,14 +1,14 @@
import { ConditionBlock, SessionState } from '@typebot.io/schemas' import { ConditionBlock, SessionState } from '@typebot.io/schemas'
import { ExecuteLogicResponse } from '../../../types' import { ExecuteLogicResponse } from '../../../types'
import { executeCondition } from './executeCondition' import { executeCondition } from '@typebot.io/logic/executeCondition'
export const executeConditionBlock = ( export const executeConditionBlock = (
state: SessionState, state: SessionState,
block: ConditionBlock block: ConditionBlock
): ExecuteLogicResponse => { ): ExecuteLogicResponse => {
const { variables } = state.typebotsQueue[0].typebot const { variables } = state.typebotsQueue[0].typebot
const passedCondition = block.items.find( const passedCondition = block.items.find(
(item) => item.content && executeCondition(variables)(item.content) (item) =>
item.content && executeCondition({ variables, condition: item.content })
) )
return { return {
outgoingEdgeId: passedCondition outgoingEdgeId: passedCondition

View File

@ -24,14 +24,25 @@ export const executeScript = async (
body: block.options.content, body: block.options.content,
}) })
const newSessionState = newVariables const updateVarResults = newVariables
? updateVariablesInSession(state)(newVariables) ? updateVariablesInSession({
: state newVariables,
state,
currentBlockId: block.id,
})
: undefined
let newSessionState = state
if (updateVarResults) {
newSessionState = updateVarResults.updatedState
}
return { return {
outgoingEdgeId: block.outgoingEdgeId, outgoingEdgeId: block.outgoingEdgeId,
logs: error ? [{ status: 'error', description: error }] : [], logs: error ? [{ status: 'error', description: error }] : [],
newSessionState, newSessionState,
newSetVariableHistory: updateVarResults?.newSetVariableHistory,
} }
} }

View File

@ -1,4 +1,10 @@
import { SessionState, SetVariableBlock, Variable } from '@typebot.io/schemas' import {
Answer,
SessionState,
SetVariableBlock,
SetVariableHistoryItem,
Variable,
} from '@typebot.io/schemas'
import { byId, isEmpty } from '@typebot.io/lib' import { byId, isEmpty } from '@typebot.io/lib'
import { ExecuteLogicResponse } from '../../../types' import { ExecuteLogicResponse } from '../../../types'
import { parseScriptToExecuteClientSideAction } from '../script/executeScript' import { parseScriptToExecuteClientSideAction } from '../script/executeScript'
@ -7,18 +13,27 @@ import { parseVariables } from '@typebot.io/variables/parseVariables'
import { updateVariablesInSession } from '@typebot.io/variables/updateVariablesInSession' import { updateVariablesInSession } from '@typebot.io/variables/updateVariablesInSession'
import { createId } from '@paralleldrive/cuid2' import { createId } from '@paralleldrive/cuid2'
import { utcToZonedTime, format as tzFormat } from 'date-fns-tz' import { utcToZonedTime, format as tzFormat } from 'date-fns-tz'
import {
computeResultTranscript,
parseTranscriptMessageText,
} from '@typebot.io/logic/computeResultTranscript'
import prisma from '@typebot.io/lib/prisma'
import { sessionOnlySetVariableOptions } from '@typebot.io/schemas/features/blocks/logic/setVariable/constants'
import vm from 'vm' import vm from 'vm'
export const executeSetVariable = ( export const executeSetVariable = async (
state: SessionState, state: SessionState,
block: SetVariableBlock block: SetVariableBlock
): ExecuteLogicResponse => { ): Promise<ExecuteLogicResponse> => {
const { variables } = state.typebotsQueue[0].typebot const { variables } = state.typebotsQueue[0].typebot
if (!block.options?.variableId) if (!block.options?.variableId)
return { return {
outgoingEdgeId: block.outgoingEdgeId, outgoingEdgeId: block.outgoingEdgeId,
} }
const expressionToEvaluate = getExpressionToEvaluate(state)(block.options) const expressionToEvaluate = await getExpressionToEvaluate(state)(
block.options,
block.id
)
const isCustomValue = !block.options.type || block.options.type === 'Custom' const isCustomValue = !block.options.type || block.options.type === 'Custom'
if ( if (
expressionToEvaluate && expressionToEvaluate &&
@ -52,10 +67,25 @@ export const executeSetVariable = (
...existingVariable, ...existingVariable,
value: evaluatedExpression, value: evaluatedExpression,
} }
const newSessionState = updateVariablesInSession(state)([newVariable]) const { newSetVariableHistory, updatedState } = updateVariablesInSession({
state,
newVariables: [
{
...newVariable,
isSessionVariable: sessionOnlySetVariableOptions.includes(
block.options.type as (typeof sessionOnlySetVariableOptions)[number]
)
? true
: newVariable.isSessionVariable,
},
],
currentBlockId: block.id,
})
return { return {
outgoingEdgeId: block.outgoingEdgeId, outgoingEdgeId: block.outgoingEdgeId,
newSessionState, newSessionState: updatedState,
newSetVariableHistory,
} }
} }
@ -85,7 +115,10 @@ const evaluateSetVariableExpression =
const getExpressionToEvaluate = const getExpressionToEvaluate =
(state: SessionState) => (state: SessionState) =>
(options: SetVariableBlock['options']): string | null => { async (
options: SetVariableBlock['options'],
blockId: string
): Promise<string | null> => {
switch (options?.type) { switch (options?.type) {
case 'Contact name': case 'Contact name':
return state.whatsApp?.contact.name ?? null return state.whatsApp?.contact.name ?? null
@ -149,6 +182,34 @@ const getExpressionToEvaluate =
case 'Environment name': { case 'Environment name': {
return state.whatsApp ? 'whatsapp' : 'web' return state.whatsApp ? 'whatsapp' : 'web'
} }
case 'Transcript': {
const props = await parseTranscriptProps(state)
if (!props) return ''
const typebotWithEmptyVariables = {
...state.typebotsQueue[0].typebot,
variables: state.typebotsQueue[0].typebot.variables.map((v) => ({
...v,
value: undefined,
})),
}
const transcript = computeResultTranscript({
typebot: typebotWithEmptyVariables,
stopAtBlockId: blockId,
...props,
})
return (
'return `' +
transcript
.map(
(message) =>
`${
message.role === 'bot' ? 'Assistant:' : 'User:'
} "${parseTranscriptMessageText(message)}"`
)
.join('\n\n') +
'`'
)
}
case 'Custom': case 'Custom':
case undefined: { case undefined: {
return options?.expressionToEvaluate ?? null return options?.expressionToEvaluate ?? null
@ -160,3 +221,79 @@ const toISOWithTz = (date: Date, timeZone: string) => {
const zonedDate = utcToZonedTime(date, timeZone) const zonedDate = utcToZonedTime(date, timeZone)
return tzFormat(zonedDate, "yyyy-MM-dd'T'HH:mm:ssXXX", { timeZone }) return tzFormat(zonedDate, "yyyy-MM-dd'T'HH:mm:ssXXX", { timeZone })
} }
type ParsedTranscriptProps = {
answers: Pick<Answer, 'blockId' | 'content'>[]
setVariableHistory: Pick<
SetVariableHistoryItem,
'blockId' | 'variableId' | 'value'
>[]
visitedEdges: string[]
}
const parseTranscriptProps = async (
state: SessionState
): Promise<ParsedTranscriptProps | undefined> => {
if (!state.typebotsQueue[0].resultId)
return parsePreviewTranscriptProps(state)
return parseResultTranscriptProps(state)
}
const parsePreviewTranscriptProps = async (
state: SessionState
): Promise<ParsedTranscriptProps | undefined> => {
if (!state.previewMetadata) return
return {
answers: state.previewMetadata.answers ?? [],
setVariableHistory: state.previewMetadata.setVariableHistory ?? [],
visitedEdges: state.previewMetadata.visitedEdges ?? [],
}
}
const parseResultTranscriptProps = async (
state: SessionState
): Promise<ParsedTranscriptProps | undefined> => {
const result = await prisma.result.findUnique({
where: {
id: state.typebotsQueue[0].resultId,
},
select: {
edges: {
select: {
edgeId: true,
index: true,
},
},
answers: {
select: {
blockId: true,
content: true,
},
},
answersV2: {
select: {
blockId: true,
content: true,
},
},
setVariableHistory: {
select: {
blockId: true,
variableId: true,
index: true,
value: true,
},
},
},
})
if (!result) return
return {
answers: result.answersV2.concat(result.answers),
setVariableHistory: (
result.setVariableHistory as SetVariableHistoryItem[]
).sort((a, b) => a.index - b.index),
visitedEdges: result.edges
.sort((a, b) => a.index - b.index)
.map((edge) => edge.edgeId),
}
}

View File

@ -5,6 +5,7 @@ import {
Group, Group,
InputBlock, InputBlock,
SessionState, SessionState,
SetVariableHistoryItem,
} from '@typebot.io/schemas' } from '@typebot.io/schemas'
import { byId } from '@typebot.io/lib' import { byId } from '@typebot.io/lib'
import { isInputBlock } from '@typebot.io/schemas/helpers' import { isInputBlock } from '@typebot.io/schemas/helpers'
@ -13,7 +14,7 @@ import { getNextGroup } from './getNextGroup'
import { validateEmail } from './blocks/inputs/email/validateEmail' import { validateEmail } from './blocks/inputs/email/validateEmail'
import { formatPhoneNumber } from './blocks/inputs/phone/formatPhoneNumber' import { formatPhoneNumber } from './blocks/inputs/phone/formatPhoneNumber'
import { resumeWebhookExecution } from './blocks/integrations/webhook/resumeWebhookExecution' import { resumeWebhookExecution } from './blocks/integrations/webhook/resumeWebhookExecution'
import { upsertAnswer } from './queries/upsertAnswer' import { saveAnswer } from './queries/saveAnswer'
import { parseButtonsReply } from './blocks/inputs/buttons/parseButtonsReply' import { parseButtonsReply } from './blocks/inputs/buttons/parseButtonsReply'
import { ParsedReply, Reply } from './types' import { ParsedReply, Reply } from './types'
import { validateNumber } from './blocks/inputs/number/validateNumber' import { validateNumber } from './blocks/inputs/number/validateNumber'
@ -57,11 +58,13 @@ export const continueBotFlow = async (
ContinueChatResponse & { ContinueChatResponse & {
newSessionState: SessionState newSessionState: SessionState
visitedEdges: VisitedEdge[] visitedEdges: VisitedEdge[]
setVariableHistory: SetVariableHistoryItem[]
} }
> => { > => {
let firstBubbleWasStreamed = false let firstBubbleWasStreamed = false
let newSessionState = { ...state } let newSessionState = { ...state }
const visitedEdges: VisitedEdge[] = [] const visitedEdges: VisitedEdge[] = []
const setVariableHistory: SetVariableHistoryItem[] = []
if (!newSessionState.currentBlockId) return startBotFlow({ state, version }) if (!newSessionState.currentBlockId) return startBotFlow({ state, version })
@ -76,16 +79,17 @@ export const continueBotFlow = async (
message: 'Group / block not found', message: 'Group / block not found',
}) })
let variableToUpdate
if (block.type === LogicBlockType.SET_VARIABLE) { if (block.type === LogicBlockType.SET_VARIABLE) {
const existingVariable = state.typebotsQueue[0].typebot.variables.find( const existingVariable = state.typebotsQueue[0].typebot.variables.find(
byId(block.options?.variableId) byId(block.options?.variableId)
) )
if (existingVariable && reply && typeof reply === 'string') { if (existingVariable && reply && typeof reply === 'string') {
const newVariable = { variableToUpdate = {
...existingVariable, ...existingVariable,
value: safeJsonParse(reply), value: safeJsonParse(reply),
} }
newSessionState = updateVariablesInSession(state)([newVariable])
} }
} }
// Legacy // Legacy
@ -121,42 +125,41 @@ export const continueBotFlow = async (
if (action) { if (action) {
if (action.run?.stream?.getStreamVariableId) { if (action.run?.stream?.getStreamVariableId) {
firstBubbleWasStreamed = true firstBubbleWasStreamed = true
const variableToUpdate = variableToUpdate = state.typebotsQueue[0].typebot.variables.find(
state.typebotsQueue[0].typebot.variables.find( (v) => v.id === action?.run?.stream?.getStreamVariableId(options)
(v) => v.id === action?.run?.stream?.getStreamVariableId(options) )
)
if (variableToUpdate)
newSessionState = updateVariablesInSession(state)([
{
...variableToUpdate,
value: reply,
},
])
} }
if ( if (
action.run?.web?.displayEmbedBubble?.waitForEvent?.getSaveVariableId action.run?.web?.displayEmbedBubble?.waitForEvent?.getSaveVariableId
) { ) {
const variableToUpdate = variableToUpdate = state.typebotsQueue[0].typebot.variables.find(
state.typebotsQueue[0].typebot.variables.find( (v) =>
(v) => v.id ===
v.id === action?.run?.web?.displayEmbedBubble?.waitForEvent?.getSaveVariableId?.(
action?.run?.web?.displayEmbedBubble?.waitForEvent?.getSaveVariableId?.( options
options )
) )
)
if (variableToUpdate)
newSessionState = updateVariablesInSession(state)([
{
...variableToUpdate,
value: reply,
},
])
} }
} }
} }
} }
if (variableToUpdate) {
const { newSetVariableHistory, updatedState } = updateVariablesInSession({
state: newSessionState,
currentBlockId: block.id,
newVariables: [
{
...variableToUpdate,
value: reply,
},
],
})
newSessionState = updatedState
setVariableHistory.push(...newSetVariableHistory)
}
let formattedReply: string | undefined let formattedReply: string | undefined
if (isInputBlock(block)) { if (isInputBlock(block)) {
@ -167,6 +170,7 @@ export const continueBotFlow = async (
...(await parseRetryMessage(newSessionState)(block)), ...(await parseRetryMessage(newSessionState)(block)),
newSessionState, newSessionState,
visitedEdges: [], visitedEdges: [],
setVariableHistory: [],
} }
formattedReply = formattedReply =
@ -176,7 +180,9 @@ export const continueBotFlow = async (
const groupHasMoreBlocks = blockIndex < group.blocks.length - 1 const groupHasMoreBlocks = blockIndex < group.blocks.length - 1
const nextEdgeId = getOutgoingEdgeId(newSessionState)(block, formattedReply) const { edgeId: nextEdgeId, isOffDefaultPath } = getOutgoingEdgeId(
newSessionState
)(block, formattedReply)
if (groupHasMoreBlocks && !nextEdgeId) { if (groupHasMoreBlocks && !nextEdgeId) {
const chatReply = await executeGroup( const chatReply = await executeGroup(
@ -188,6 +194,7 @@ export const continueBotFlow = async (
version, version,
state: newSessionState, state: newSessionState,
visitedEdges, visitedEdges,
setVariableHistory,
firstBubbleWasStreamed, firstBubbleWasStreamed,
startTime, startTime,
} }
@ -206,9 +213,14 @@ export const continueBotFlow = async (
lastMessageNewFormat: lastMessageNewFormat:
formattedReply !== reply ? formattedReply : undefined, formattedReply !== reply ? formattedReply : undefined,
visitedEdges, visitedEdges,
setVariableHistory,
} }
const nextGroup = await getNextGroup(newSessionState)(nextEdgeId) const nextGroup = await getNextGroup({
state: newSessionState,
edgeId: nextEdgeId,
isOffDefaultPath,
})
if (nextGroup.visitedEdge) visitedEdges.push(nextGroup.visitedEdge) if (nextGroup.visitedEdge) visitedEdges.push(nextGroup.visitedEdge)
@ -221,6 +233,7 @@ export const continueBotFlow = async (
lastMessageNewFormat: lastMessageNewFormat:
formattedReply !== reply ? formattedReply : undefined, formattedReply !== reply ? formattedReply : undefined,
visitedEdges, visitedEdges,
setVariableHistory,
} }
const chatReply = await executeGroup(nextGroup.group, { const chatReply = await executeGroup(nextGroup.group, {
@ -228,6 +241,7 @@ export const continueBotFlow = async (
state: newSessionState, state: newSessionState,
firstBubbleWasStreamed, firstBubbleWasStreamed,
visitedEdges, visitedEdges,
setVariableHistory,
startTime, startTime,
}) })
@ -241,8 +255,7 @@ const processAndSaveAnswer =
(state: SessionState, block: InputBlock) => (state: SessionState, block: InputBlock) =>
async (reply: string | undefined): Promise<SessionState> => { async (reply: string | undefined): Promise<SessionState> => {
if (!reply) return state if (!reply) return state
let newState = await saveAnswer(state, block)(reply) let newState = await saveAnswerInDb(state, block)(reply)
newState = saveVariableValueIfAny(newState, block)(reply)
return newState return newState
} }
@ -255,16 +268,20 @@ const saveVariableValueIfAny =
) )
if (!foundVariable) return state if (!foundVariable) return state
const newSessionState = updateVariablesInSession(state)([ const { updatedState } = updateVariablesInSession({
{ newVariables: [
...foundVariable, {
value: Array.isArray(foundVariable.value) ...foundVariable,
? foundVariable.value.concat(reply) value: Array.isArray(foundVariable.value)
: reply, ? foundVariable.value.concat(reply)
}, : reply,
]) },
],
currentBlockId: undefined,
state,
})
return newSessionState return updatedState
} }
const parseRetryMessage = const parseRetryMessage =
@ -305,31 +322,43 @@ const parseDefaultRetryMessage = (block: InputBlock): string => {
} }
} }
const saveAnswer = const saveAnswerInDb =
(state: SessionState, block: InputBlock) => (state: SessionState, block: InputBlock) =>
async (reply: string): Promise<SessionState> => { async (reply: string): Promise<SessionState> => {
let newSessionState = state
const groupId = state.typebotsQueue[0].typebot.groups.find((group) => const groupId = state.typebotsQueue[0].typebot.groups.find((group) =>
group.blocks.some((blockInGroup) => blockInGroup.id === block.id) group.blocks.some((blockInGroup) => blockInGroup.id === block.id)
)?.id )?.id
if (!groupId) throw new Error('saveAnswer: Group not found') if (!groupId) throw new Error('saveAnswer: Group not found')
await upsertAnswer({ await saveAnswer({
answer: { answer: {
blockId: block.id, blockId: block.id,
groupId,
content: reply, content: reply,
variableId: block.options?.variableId,
}, },
reply, reply,
state, state,
}) })
newSessionState = {
...saveVariableValueIfAny(newSessionState, block)(reply),
previewMetadata: state.typebotsQueue[0].resultId
? newSessionState.previewMetadata
: {
...newSessionState.previewMetadata,
answers: (newSessionState.previewMetadata?.answers ?? []).concat({
blockId: block.id,
content: reply,
}),
},
}
const key = block.options?.variableId const key = block.options?.variableId
? state.typebotsQueue[0].typebot.variables.find( ? newSessionState.typebotsQueue[0].typebot.variables.find(
(variable) => variable.id === block.options?.variableId (variable) => variable.id === block.options?.variableId
)?.name )?.name
: parseGroupKey(block.id, { state }) : parseGroupKey(block.id, { state: newSessionState })
return setNewAnswerInState(state)({ return setNewAnswerInState(newSessionState)({
key: key ?? block.id, key: key ?? block.id,
value: reply, value: reply,
}) })
@ -375,7 +404,10 @@ const setNewAnswerInState =
const getOutgoingEdgeId = const getOutgoingEdgeId =
(state: Pick<SessionState, 'typebotsQueue'>) => (state: Pick<SessionState, 'typebotsQueue'>) =>
(block: Block, reply: string | undefined) => { (
block: Block,
reply: string | undefined
): { edgeId: string | undefined; isOffDefaultPath: boolean } => {
const variables = state.typebotsQueue[0].typebot.variables const variables = state.typebotsQueue[0].typebot.variables
if ( if (
block.type === InputBlockType.CHOICE && block.type === InputBlockType.CHOICE &&
@ -390,7 +422,8 @@ const getOutgoingEdgeId =
parseVariables(variables)(item.content).normalize() === parseVariables(variables)(item.content).normalize() ===
reply.normalize() reply.normalize()
) )
if (matchedItem?.outgoingEdgeId) return matchedItem.outgoingEdgeId if (matchedItem?.outgoingEdgeId)
return { edgeId: matchedItem.outgoingEdgeId, isOffDefaultPath: true }
} }
if ( if (
block.type === InputBlockType.PICTURE_CHOICE && block.type === InputBlockType.PICTURE_CHOICE &&
@ -405,9 +438,10 @@ const getOutgoingEdgeId =
parseVariables(variables)(item.title).normalize() === parseVariables(variables)(item.title).normalize() ===
reply.normalize() reply.normalize()
) )
if (matchedItem?.outgoingEdgeId) return matchedItem.outgoingEdgeId if (matchedItem?.outgoingEdgeId)
return { edgeId: matchedItem.outgoingEdgeId, isOffDefaultPath: true }
} }
return block.outgoingEdgeId return { edgeId: block.outgoingEdgeId, isOffDefaultPath: false }
} }
const parseReply = const parseReply =

View File

@ -4,6 +4,7 @@ import {
InputBlock, InputBlock,
RuntimeOptions, RuntimeOptions,
SessionState, SessionState,
SetVariableHistoryItem,
} from '@typebot.io/schemas' } from '@typebot.io/schemas'
import { isNotEmpty } from '@typebot.io/lib' import { isNotEmpty } from '@typebot.io/lib'
import { import {
@ -21,16 +22,16 @@ import { injectVariableValuesInPictureChoiceBlock } from './blocks/inputs/pictur
import { getPrefilledInputValue } from './getPrefilledValue' import { getPrefilledInputValue } from './getPrefilledValue'
import { parseDateInput } from './blocks/inputs/date/parseDateInput' import { parseDateInput } from './blocks/inputs/date/parseDateInput'
import { deepParseVariables } from '@typebot.io/variables/deepParseVariables' import { deepParseVariables } from '@typebot.io/variables/deepParseVariables'
import {
BubbleBlockWithDefinedContent,
parseBubbleBlock,
} from './parseBubbleBlock'
import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants' import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants'
import { VisitedEdge } from '@typebot.io/prisma' import { VisitedEdge } from '@typebot.io/prisma'
import { env } from '@typebot.io/env' import { env } from '@typebot.io/env'
import { TRPCError } from '@trpc/server' import { TRPCError } from '@trpc/server'
import { ExecuteIntegrationResponse, ExecuteLogicResponse } from './types' import { ExecuteIntegrationResponse, ExecuteLogicResponse } from './types'
import { createId } from '@paralleldrive/cuid2' import { createId } from '@paralleldrive/cuid2'
import {
BubbleBlockWithDefinedContent,
parseBubbleBlock,
} from './parseBubbleBlock'
type ContextProps = { type ContextProps = {
version: 1 | 2 version: 1 | 2
@ -39,6 +40,7 @@ type ContextProps = {
currentLastBubbleId?: string currentLastBubbleId?: string
firstBubbleWasStreamed?: boolean firstBubbleWasStreamed?: boolean
visitedEdges: VisitedEdge[] visitedEdges: VisitedEdge[]
setVariableHistory: SetVariableHistoryItem[]
startTime?: number startTime?: number
} }
@ -48,6 +50,7 @@ export const executeGroup = async (
version, version,
state, state,
visitedEdges, visitedEdges,
setVariableHistory,
currentReply, currentReply,
currentLastBubbleId, currentLastBubbleId,
firstBubbleWasStreamed, firstBubbleWasStreamed,
@ -56,6 +59,7 @@ export const executeGroup = async (
): Promise< ): Promise<
ContinueChatResponse & { ContinueChatResponse & {
newSessionState: SessionState newSessionState: SessionState
setVariableHistory: SetVariableHistoryItem[]
visitedEdges: VisitedEdge[] visitedEdges: VisitedEdge[]
} }
> => { > => {
@ -70,6 +74,7 @@ export const executeGroup = async (
let newSessionState = state let newSessionState = state
let isNextEdgeOffDefaultPath = false
let index = -1 let index = -1
for (const block of group.blocks) { for (const block of group.blocks) {
if ( if (
@ -110,6 +115,7 @@ export const executeGroup = async (
clientSideActions, clientSideActions,
logs, logs,
visitedEdges, visitedEdges,
setVariableHistory,
} }
const executionResponse = ( const executionResponse = (
isLogicBlock(block) isLogicBlock(block)
@ -120,6 +126,29 @@ export const executeGroup = async (
) as ExecuteLogicResponse | ExecuteIntegrationResponse | null ) as ExecuteLogicResponse | ExecuteIntegrationResponse | null
if (!executionResponse) continue if (!executionResponse) continue
if (
executionResponse.newSetVariableHistory &&
executionResponse.newSetVariableHistory?.length > 0
) {
if (!newSessionState.typebotsQueue[0].resultId)
newSessionState = {
...newSessionState,
previewMetadata: {
...newSessionState.previewMetadata,
setVariableHistory: (
newSessionState.previewMetadata?.setVariableHistory ?? []
).concat(
executionResponse.newSetVariableHistory.map((item) => ({
blockId: item.blockId,
variableId: item.variableId,
value: item.value,
}))
),
},
}
else setVariableHistory.push(...executionResponse.newSetVariableHistory)
}
if ( if (
'startTimeShouldBeUpdated' in executionResponse && 'startTimeShouldBeUpdated' in executionResponse &&
executionResponse.startTimeShouldBeUpdated executionResponse.startTimeShouldBeUpdated
@ -165,33 +194,55 @@ export const executeGroup = async (
clientSideActions, clientSideActions,
logs, logs,
visitedEdges, visitedEdges,
setVariableHistory,
} }
} }
} }
if (executionResponse.outgoingEdgeId) { if (executionResponse.outgoingEdgeId) {
isNextEdgeOffDefaultPath =
block.outgoingEdgeId !== executionResponse.outgoingEdgeId
nextEdgeId = executionResponse.outgoingEdgeId nextEdgeId = executionResponse.outgoingEdgeId
break break
} }
} }
if (!nextEdgeId && newSessionState.typebotsQueue.length === 1) if (!nextEdgeId && newSessionState.typebotsQueue.length === 1)
return { messages, newSessionState, clientSideActions, logs, visitedEdges } return {
messages,
newSessionState,
clientSideActions,
logs,
visitedEdges,
setVariableHistory,
}
const nextGroup = await getNextGroup(newSessionState)(nextEdgeId ?? undefined) const nextGroup = await getNextGroup({
state: newSessionState,
edgeId: nextEdgeId ?? undefined,
isOffDefaultPath: isNextEdgeOffDefaultPath,
})
newSessionState = nextGroup.newSessionState newSessionState = nextGroup.newSessionState
if (nextGroup.visitedEdge) visitedEdges.push(nextGroup.visitedEdge) if (nextGroup.visitedEdge) visitedEdges.push(nextGroup.visitedEdge)
if (!nextGroup.group) { if (!nextGroup.group) {
return { messages, newSessionState, clientSideActions, logs, visitedEdges } return {
messages,
newSessionState,
clientSideActions,
logs,
visitedEdges,
setVariableHistory,
}
} }
return executeGroup(nextGroup.group, { return executeGroup(nextGroup.group, {
version, version,
state: newSessionState, state: newSessionState,
visitedEdges, visitedEdges,
setVariableHistory,
currentReply: { currentReply: {
messages, messages,
clientSideActions, clientSideActions,

View File

@ -2,12 +2,12 @@ import { VariableStore, LogsStore } from '@typebot.io/forge'
import { forgedBlocks } from '@typebot.io/forge-repository/definitions' import { forgedBlocks } from '@typebot.io/forge-repository/definitions'
import { ForgedBlock } from '@typebot.io/forge-repository/types' import { ForgedBlock } from '@typebot.io/forge-repository/types'
import { decrypt } from '@typebot.io/lib/api/encryption/decrypt' import { decrypt } from '@typebot.io/lib/api/encryption/decrypt'
import { isPlaneteScale } from '@typebot.io/lib/isPlanetScale'
import { import {
SessionState, SessionState,
ContinueChatResponse, ContinueChatResponse,
Block, Block,
TypebotInSession, TypebotInSession,
SetVariableHistoryItem,
} from '@typebot.io/schemas' } from '@typebot.io/schemas'
import { deepParseVariables } from '@typebot.io/variables/deepParseVariables' import { deepParseVariables } from '@typebot.io/variables/deepParseVariables'
import { import {
@ -73,6 +73,7 @@ export const executeForgedBlock = async (
} }
let newSessionState = state let newSessionState = state
let setVariableHistory: SetVariableHistoryItem[] = []
const variables: VariableStore = { const variables: VariableStore = {
get: (id: string) => { get: (id: string) => {
@ -86,9 +87,13 @@ export const executeForgedBlock = async (
(variable) => variable.id === id (variable) => variable.id === id
) )
if (!variable) return if (!variable) return
newSessionState = updateVariablesInSession(newSessionState)([ const { newSetVariableHistory, updatedState } = updateVariablesInSession({
{ ...variable, value }, newVariables: [{ ...variable, value }],
]) state: newSessionState,
currentBlockId: block.id,
})
newSessionState = updatedState
setVariableHistory.push(...newSetVariableHistory)
}, },
parse: (text: string, params?: ParseVariablesOptions) => parse: (text: string, params?: ParseVariablesOptions) =>
parseVariables( parseVariables(
@ -159,6 +164,7 @@ export const executeForgedBlock = async (
}, },
} }
: undefined, : undefined,
newSetVariableHistory: setVariableHistory,
} }
} }

View File

@ -1,14 +1,13 @@
import { TRPCError } from '@trpc/server' import { TRPCError } from '@trpc/server'
import { SessionState } from '@typebot.io/schemas' import { TypebotInSession } from '@typebot.io/schemas'
export const getFirstEdgeId = ({ export const getFirstEdgeId = ({
state, typebot,
startEventId, startEventId,
}: { }: {
state: SessionState typebot: Pick<TypebotInSession, 'events' | 'groups' | 'version'>
startEventId: string | undefined startEventId: string | undefined
}) => { }) => {
const { typebot } = state.typebotsQueue[0]
if (startEventId) { if (startEventId) {
const event = typebot.events?.find((e) => e.id === startEventId) const event = typebot.events?.find((e) => e.id === startEventId)
if (!event) if (!event)
@ -18,6 +17,6 @@ export const getFirstEdgeId = ({
}) })
return event.outgoingEdgeId return event.outgoingEdgeId
} }
if (typebot.version === '6') return typebot.events[0].outgoingEdgeId if (typebot.version === '6') return typebot.events?.[0].outgoingEdgeId
return typebot.groups.at(0)?.blocks.at(0)?.outgoingEdgeId return typebot.groups.at(0)?.blocks.at(0)?.outgoingEdgeId
} }

View File

@ -9,116 +9,138 @@ export type NextGroup = {
visitedEdge?: VisitedEdge visitedEdge?: VisitedEdge
} }
export const getNextGroup = export const getNextGroup = async ({
(state: SessionState) => state,
async (edgeId?: string): Promise<NextGroup> => { edgeId,
const nextEdge = state.typebotsQueue[0].typebot.edges.find(byId(edgeId)) isOffDefaultPath,
if (!nextEdge) { }: {
if (state.typebotsQueue.length > 1) { state: SessionState
const nextEdgeId = state.typebotsQueue[0].edgeIdToTriggerWhenDone edgeId?: string
const isMergingWithParent = state.typebotsQueue[0].isMergingWithParent isOffDefaultPath: boolean
const currentResultId = state.typebotsQueue[0].resultId }): Promise<NextGroup> => {
if (!isMergingWithParent && currentResultId) const nextEdge = state.typebotsQueue[0].typebot.edges.find(byId(edgeId))
await upsertResult({ if (!nextEdge) {
resultId: currentResultId, if (state.typebotsQueue.length > 1) {
typebot: state.typebotsQueue[0].typebot, const nextEdgeId = state.typebotsQueue[0].edgeIdToTriggerWhenDone
isCompleted: true, const isMergingWithParent = state.typebotsQueue[0].isMergingWithParent
hasStarted: state.typebotsQueue[0].answers.length > 0, const currentResultId = state.typebotsQueue[0].resultId
}) if (!isMergingWithParent && currentResultId)
let newSessionState = { await upsertResult({
...state, resultId: currentResultId,
typebotsQueue: [ typebot: state.typebotsQueue[0].typebot,
{ isCompleted: true,
...state.typebotsQueue[1], hasStarted: state.typebotsQueue[0].answers.length > 0,
typebot: isMergingWithParent })
? { let newSessionState = {
...state.typebotsQueue[1].typebot, ...state,
variables: state.typebotsQueue[1].typebot.variables typebotsQueue: [
.map((variable) => ({ {
...variable, ...state.typebotsQueue[1],
value: typebot: isMergingWithParent
state.typebotsQueue[0].typebot.variables.find( ? {
(v) => v.name === variable.name ...state.typebotsQueue[1].typebot,
)?.value ?? variable.value, variables: state.typebotsQueue[1].typebot.variables
})) .map((variable) => ({
.concat( ...variable,
state.typebotsQueue[0].typebot.variables.filter( value:
(variable) => state.typebotsQueue[0].typebot.variables.find(
isDefined(variable.value) && (v) => v.name === variable.name
isNotDefined( )?.value ?? variable.value,
state.typebotsQueue[1].typebot.variables.find( }))
(v) => v.name === variable.name .concat(
) state.typebotsQueue[0].typebot.variables.filter(
(variable) =>
isDefined(variable.value) &&
isNotDefined(
state.typebotsQueue[1].typebot.variables.find(
(v) => v.name === variable.name
) )
) as VariableWithValue[] )
), ) as VariableWithValue[]
}
: 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].typebot,
: state.typebotsQueue[1].answers, answers: isMergingWithParent
}, ? [
...state.typebotsQueue.slice(2), ...state.typebotsQueue[1].answers.filter(
], (incomingAnswer) =>
} satisfies SessionState !state.typebotsQueue[0].answers.find(
if (state.progressMetadata) (currentAnswer) =>
newSessionState.progressMetadata = { currentAnswer.key === incomingAnswer.key
...state.progressMetadata, )
totalAnswers: ),
state.progressMetadata.totalAnswers + ...state.typebotsQueue[0].answers,
state.typebotsQueue[0].answers.length, ]
} : state.typebotsQueue[1].answers,
const nextGroup = await getNextGroup(newSessionState)(nextEdgeId) },
newSessionState = nextGroup.newSessionState ...state.typebotsQueue.slice(2),
if (!nextGroup) ],
return { } satisfies SessionState
newSessionState, if (state.progressMetadata)
} newSessionState.progressMetadata = {
...state.progressMetadata,
totalAnswers:
state.progressMetadata.totalAnswers +
state.typebotsQueue[0].answers.length,
}
const nextGroup = await getNextGroup({
state: newSessionState,
edgeId: nextEdgeId,
isOffDefaultPath,
})
newSessionState = nextGroup.newSessionState
if (!nextGroup)
return { return {
...nextGroup,
newSessionState, newSessionState,
} }
}
return { return {
newSessionState: state, ...nextGroup,
newSessionState,
} }
} }
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
const currentVisitedEdgeIndex = (state.currentVisitedEdgeIndex ?? -1) + 1
const resultId = state.typebotsQueue[0].resultId
return { return {
group: { newSessionState: state,
...nextGroup, }
blocks: nextGroup.blocks.slice(startBlockIndex), }
} as Group, const nextGroup = state.typebotsQueue[0].typebot.groups.find(
newSessionState: { byId(nextEdge.to.groupId)
...state, )
currentVisitedEdgeIndex, if (!nextGroup)
}, return {
visitedEdge: resultId newSessionState: state,
}
const startBlockIndex = nextEdge.to.blockId
? nextGroup.blocks.findIndex(byId(nextEdge.to.blockId))
: 0
const currentVisitedEdgeIndex = isOffDefaultPath
? (state.currentVisitedEdgeIndex ?? -1) + 1
: state.currentVisitedEdgeIndex
const resultId = state.typebotsQueue[0].resultId
return {
group: {
...nextGroup,
blocks: nextGroup.blocks.slice(startBlockIndex),
} as Group,
newSessionState: {
...state,
currentVisitedEdgeIndex,
previewMetadata:
resultId || !isOffDefaultPath
? state.previewMetadata
: {
...state.previewMetadata,
visitedEdges: (state.previewMetadata?.visitedEdges ?? []).concat(
nextEdge.id
),
},
},
visitedEdge:
resultId && isOffDefaultPath && !nextEdge.id.startsWith('virtual-')
? { ? {
index: currentVisitedEdgeIndex, index: currentVisitedEdgeIndex as number,
edgeId: nextEdge.id, edgeId: nextEdge.id,
resultId, resultId,
} }
: undefined, : undefined,
}
} }
}

View File

@ -19,6 +19,7 @@
"@typebot.io/tsconfig": "workspace:*", "@typebot.io/tsconfig": "workspace:*",
"@typebot.io/variables": "workspace:*", "@typebot.io/variables": "workspace:*",
"@udecode/plate-common": "30.4.5", "@udecode/plate-common": "30.4.5",
"@typebot.io/logic": "workspace:*",
"ai": "3.0.31", "ai": "3.0.31",
"chrono-node": "2.7.5", "chrono-node": "2.7.5",
"date-fns": "2.30.0", "date-fns": "2.30.0",

View File

@ -6,7 +6,7 @@ import {
Typebot, Typebot,
} from '@typebot.io/schemas' } from '@typebot.io/schemas'
import { deepParseVariables } from '@typebot.io/variables/deepParseVariables' import { deepParseVariables } from '@typebot.io/variables/deepParseVariables'
import { isEmpty, isNotEmpty } from '@typebot.io/lib/utils' import { isDefined, isEmpty, isNotEmpty } from '@typebot.io/lib/utils'
import { import {
getVariablesToParseInfoInText, getVariablesToParseInfoInText,
parseVariables, parseVariables,
@ -49,7 +49,7 @@ export const parseBubbleBlock = (
richText: parseVariablesInRichText(block.content?.richText ?? [], { richText: parseVariablesInRichText(block.content?.richText ?? [], {
variables, variables,
takeLatestIfList: typebotVersion !== '6', takeLatestIfList: typebotVersion !== '6',
}), }).parsedElements,
}, },
} }
} }
@ -93,14 +93,15 @@ export const parseBubbleBlock = (
} }
} }
const parseVariablesInRichText = ( export const parseVariablesInRichText = (
elements: TDescendant[], elements: TDescendant[],
{ {
variables, variables,
takeLatestIfList, takeLatestIfList,
}: { variables: Variable[]; takeLatestIfList?: boolean } }: { variables: Variable[]; takeLatestIfList?: boolean }
): TDescendant[] => { ): { parsedElements: TDescendant[]; parsedVariableIds: string[] } => {
const parsedElements: TDescendant[] = [] const parsedElements: TDescendant[] = []
const parsedVariableIds: string[] = []
for (const element of elements) { for (const element of elements) {
if ('text' in element) { if ('text' in element) {
const text = element.text as string const text = element.text as string
@ -112,6 +113,9 @@ const parseVariablesInRichText = (
variables, variables,
takeLatestIfList, takeLatestIfList,
}) })
parsedVariableIds.push(
...variablesInText.map((v) => v.variableId).filter(isDefined)
)
if (variablesInText.length === 0) { if (variablesInText.length === 0) {
parsedElements.push(element) parsedElements.push(element)
continue continue
@ -185,19 +189,28 @@ const parseVariablesInRichText = (
? 'variable' ? 'variable'
: element.type : element.type
const {
parsedElements: parsedChildren,
parsedVariableIds: parsedChildrenVariableIds,
} = parseVariablesInRichText(element.children as TDescendant[], {
variables,
takeLatestIfList,
})
parsedVariableIds.push(...parsedChildrenVariableIds)
parsedElements.push({ parsedElements.push({
...element, ...element,
url: element.url url: element.url
? parseVariables(variables)(element.url as string) ? parseVariables(variables)(element.url as string)
: undefined, : undefined,
type, type,
children: parseVariablesInRichText(element.children as TDescendant[], { children: parsedChildren,
variables,
takeLatestIfList,
}),
}) })
} }
return parsedElements return {
parsedElements,
parsedVariableIds,
}
} }
const applyElementStyleToDescendants = ( const applyElementStyleToDescendants = (

View File

@ -12,11 +12,26 @@ export const createSession = ({
id, id,
state, state,
isReplying, isReplying,
}: Props): Prisma.PrismaPromise<any> => }: Props): Prisma.PrismaPromise<any> => {
prisma.chatSession.create({ if (!id) {
data: { return prisma.chatSession.create({
data: {
id,
state,
isReplying,
},
})
}
return prisma.chatSession.upsert({
where: { id },
update: {
state,
isReplying,
},
create: {
id, id,
state, state,
isReplying, isReplying,
}, },
}) })
}

View File

@ -4,24 +4,33 @@ import { Answer, Result } from '@typebot.io/schemas'
type Props = { type Props = {
id: string id: string
} }
export const findResult = ({ id }: Props) => export const findResult = async ({ id }: Props) => {
prisma.result.findFirst({ const { answers, answersV2, ...result } =
where: { id, isArchived: { not: true } }, (await prisma.result.findFirst({
select: { where: { id, isArchived: { not: true } },
id: true, select: {
variables: true, id: true,
hasStarted: true, variables: true,
answers: { hasStarted: true,
select: { answers: {
content: true, select: {
blockId: true, content: true,
variableId: true, blockId: true,
},
},
answersV2: {
select: {
content: true,
blockId: true,
},
}, },
}, },
}, })) ?? {}
}) as Promise< if (!result) return null
| (Pick<Result, 'id' | 'variables' | 'hasStarted'> & { return {
answers: Pick<Answer, 'content' | 'blockId' | 'variableId'>[] ...result,
}) answers: (answersV2 ?? []).concat(answers ?? []),
| null } as Pick<Result, 'id' | 'variables' | 'hasStarted'> & {
> answers: Pick<Answer, 'content' | 'blockId'>[]
}
}

View File

@ -0,0 +1,16 @@
import prisma from '@typebot.io/lib/prisma'
import { Prisma } from '@typebot.io/prisma'
import { SessionState } from '@typebot.io/schemas'
type Props = {
answer: Omit<Prisma.AnswerV2CreateManyInput, 'resultId'>
reply: string
state: SessionState
}
export const saveAnswer = async ({ answer, state }: Props) => {
const resultId = state.typebotsQueue[0].resultId
if (!resultId) return
return prisma.answerV2.createMany({
data: [{ ...answer, resultId }],
})
}

View File

@ -0,0 +1,16 @@
import prisma from '@typebot.io/lib/prisma'
import { Prisma } from '@typebot.io/prisma'
import { SetVariableHistoryItem } from '@typebot.io/schemas'
export const saveSetVariableHistoryItems = (
setVariableHistory: SetVariableHistoryItem[]
) =>
prisma.setVariableHistoryItem.createMany({
data: {
...setVariableHistory.map((item) => ({
...item,
value: item.value === null ? Prisma.JsonNull : item.value,
})),
},
skipDuplicates: true,
})

View File

@ -1,34 +0,0 @@
import prisma from '@typebot.io/lib/prisma'
import { Prisma } from '@typebot.io/prisma'
import { InputBlock, SessionState } from '@typebot.io/schemas'
type Props = {
answer: Omit<Prisma.AnswerUncheckedCreateInput, 'resultId'>
reply: string
state: SessionState
}
export const upsertAnswer = async ({ answer, state }: Props) => {
const resultId = state.typebotsQueue[0].resultId
if (!resultId) return
const where = {
resultId,
blockId: answer.blockId,
groupId: answer.groupId,
}
const existingAnswer = await prisma.answer.findUnique({
where: {
resultId_blockId_groupId: where,
},
select: { resultId: true },
})
if (existingAnswer)
return prisma.answer.updateMany({
where,
data: {
content: answer.content,
},
})
return prisma.answer.createMany({
data: [{ ...answer, resultId }],
})
}

View File

@ -1,29 +1,79 @@
import prisma from '@typebot.io/lib/prisma' import prisma from '@typebot.io/lib/prisma'
import { Prisma } from '@typebot.io/prisma' import { Prisma, SetVariableHistoryItem, VisitedEdge } from '@typebot.io/prisma'
import { TypebotInSession } from '@typebot.io/schemas' import { ContinueChatResponse, TypebotInSession } from '@typebot.io/schemas'
import { filterNonSessionVariablesWithValues } from '@typebot.io/variables/filterVariablesWithValues' import { filterNonSessionVariablesWithValues } from '@typebot.io/variables/filterVariablesWithValues'
import { formatLogDetails } from '../logs/helpers/formatLogDetails'
type Props = { type Props = {
resultId: string resultId: string
typebot: TypebotInSession typebot: TypebotInSession
hasStarted: boolean hasStarted: boolean
isCompleted: boolean isCompleted: boolean
lastChatSessionId?: string
logs?: ContinueChatResponse['logs']
visitedEdges?: VisitedEdge[]
setVariableHistory?: SetVariableHistoryItem[]
} }
export const upsertResult = ({ export const upsertResult = ({
resultId, resultId,
typebot, typebot,
hasStarted, hasStarted,
isCompleted, isCompleted,
lastChatSessionId,
logs,
visitedEdges,
setVariableHistory,
}: Props): Prisma.PrismaPromise<any> => { }: Props): Prisma.PrismaPromise<any> => {
const variablesWithValue = filterNonSessionVariablesWithValues( const variablesWithValue = filterNonSessionVariablesWithValues(
typebot.variables typebot.variables
) )
const logsToCreate =
logs && logs.length > 0
? {
createMany: {
data: logs.map((log) => ({
...log,
details: formatLogDetails(log.details),
})),
},
}
: undefined
const setVariableHistoryToCreate =
setVariableHistory && setVariableHistory.length > 0
? ({
createMany: {
data: setVariableHistory.map((item) => ({
...item,
value: item.value === null ? Prisma.JsonNull : item.value,
resultId: undefined,
})),
},
} as Prisma.SetVariableHistoryItemUpdateManyWithoutResultNestedInput)
: undefined
const visitedEdgesToCreate =
visitedEdges && visitedEdges.length > 0
? {
createMany: {
data: visitedEdges.map((edge) => ({
...edge,
resultId: undefined,
})),
},
}
: undefined
return prisma.result.upsert({ return prisma.result.upsert({
where: { id: resultId }, where: { id: resultId },
update: { update: {
isCompleted: isCompleted ? true : undefined, isCompleted: isCompleted ? true : undefined,
hasStarted, hasStarted,
variables: variablesWithValue, variables: variablesWithValue,
lastChatSessionId,
logs: logsToCreate,
setVariableHistory: setVariableHistoryToCreate,
edges: visitedEdgesToCreate,
}, },
create: { create: {
id: resultId, id: resultId,
@ -31,6 +81,10 @@ export const upsertResult = ({
isCompleted: isCompleted ? true : false, isCompleted: isCompleted ? true : false,
hasStarted, hasStarted,
variables: variablesWithValue, variables: variablesWithValue,
lastChatSessionId,
logs: logsToCreate,
setVariableHistory: setVariableHistoryToCreate,
edges: visitedEdgesToCreate,
}, },
select: { id: true }, select: { id: true },
}) })

View File

@ -1,12 +1,12 @@
import { ContinueChatResponse, ChatSession } from '@typebot.io/schemas' import {
ContinueChatResponse,
ChatSession,
SetVariableHistoryItem,
} from '@typebot.io/schemas'
import { upsertResult } from './queries/upsertResult' import { upsertResult } from './queries/upsertResult'
import { saveLogs } from './queries/saveLogs'
import { updateSession } from './queries/updateSession' import { updateSession } from './queries/updateSession'
import { formatLogDetails } from './logs/helpers/formatLogDetails'
import { createSession } from './queries/createSession' import { createSession } from './queries/createSession'
import { deleteSession } from './queries/deleteSession' import { deleteSession } from './queries/deleteSession'
import * as Sentry from '@sentry/nextjs'
import { saveVisitedEdges } from './queries/saveVisitedEdges'
import { Prisma, VisitedEdge } from '@typebot.io/prisma' import { Prisma, VisitedEdge } from '@typebot.io/prisma'
import prisma from '@typebot.io/lib/prisma' import prisma from '@typebot.io/lib/prisma'
@ -16,7 +16,9 @@ type Props = {
logs: ContinueChatResponse['logs'] logs: ContinueChatResponse['logs']
clientSideActions: ContinueChatResponse['clientSideActions'] clientSideActions: ContinueChatResponse['clientSideActions']
visitedEdges: VisitedEdge[] visitedEdges: VisitedEdge[]
setVariableHistory: SetVariableHistoryItem[]
hasCustomEmbedBubble?: boolean hasCustomEmbedBubble?: boolean
initialSessionId?: string
} }
export const saveStateToDatabase = async ({ export const saveStateToDatabase = async ({
@ -25,7 +27,9 @@ export const saveStateToDatabase = async ({
logs, logs,
clientSideActions, clientSideActions,
visitedEdges, visitedEdges,
setVariableHistory,
hasCustomEmbedBubble, hasCustomEmbedBubble,
initialSessionId,
}: Props) => { }: Props) => {
const containsSetVariableClientSideAction = clientSideActions?.some( const containsSetVariableClientSideAction = clientSideActions?.some(
(action) => action.expectsDedicatedReply (action) => action.expectsDedicatedReply
@ -46,7 +50,7 @@ export const saveStateToDatabase = async ({
const session = id const session = id
? { state, id } ? { state, id }
: await createSession({ id, state, isReplying: false }) : await createSession({ id: initialSessionId, state, isReplying: false })
if (!resultId) { if (!resultId) {
if (queries.length > 0) await prisma.$transaction(queries) if (queries.length > 0) await prisma.$transaction(queries)
@ -63,25 +67,13 @@ export const saveStateToDatabase = async ({
!input && !containsSetVariableClientSideAction && answers.length > 0 !input && !containsSetVariableClientSideAction && answers.length > 0
), ),
hasStarted: answers.length > 0, hasStarted: answers.length > 0,
lastChatSessionId: session.id,
logs,
visitedEdges,
setVariableHistory,
}) })
) )
if (logs && logs.length > 0)
try {
await saveLogs(
logs.map((log) => ({
...log,
resultId,
details: formatLogDetails(log.details),
}))
)
} catch (e) {
console.error('Failed to save logs', e)
Sentry.captureException(e)
}
if (visitedEdges.length > 0) queries.push(saveVisitedEdges(visitedEdges))
await prisma.$transaction(queries) await prisma.$transaction(queries)
return session return session

View File

@ -2,6 +2,7 @@ import { TRPCError } from '@trpc/server'
import { import {
ContinueChatResponse, ContinueChatResponse,
SessionState, SessionState,
SetVariableHistoryItem,
StartFrom, StartFrom,
} from '@typebot.io/schemas' } from '@typebot.io/schemas'
import { executeGroup } from './executeGroup' import { executeGroup } from './executeGroup'
@ -25,10 +26,12 @@ export const startBotFlow = async ({
ContinueChatResponse & { ContinueChatResponse & {
newSessionState: SessionState newSessionState: SessionState
visitedEdges: VisitedEdge[] visitedEdges: VisitedEdge[]
setVariableHistory: SetVariableHistoryItem[]
} }
> => { > => {
let newSessionState = state let newSessionState = state
const visitedEdges: VisitedEdge[] = [] const visitedEdges: VisitedEdge[] = []
const setVariableHistory: SetVariableHistoryItem[] = []
if (startFrom?.type === 'group') { if (startFrom?.type === 'group') {
const group = state.typebotsQueue[0].typebot.groups.find( const group = state.typebotsQueue[0].typebot.groups.find(
(group) => group.id === startFrom.groupId (group) => group.id === startFrom.groupId
@ -42,22 +45,34 @@ export const startBotFlow = async ({
version, version,
state: newSessionState, state: newSessionState,
visitedEdges, visitedEdges,
setVariableHistory,
startTime, startTime,
}) })
} }
const firstEdgeId = getFirstEdgeId({ const firstEdgeId = getFirstEdgeId({
state: newSessionState, typebot: newSessionState.typebotsQueue[0].typebot,
startEventId: startFrom?.type === 'event' ? startFrom.eventId : undefined, startEventId: startFrom?.type === 'event' ? startFrom.eventId : undefined,
}) })
if (!firstEdgeId) return { messages: [], newSessionState, visitedEdges: [] } if (!firstEdgeId)
const nextGroup = await getNextGroup(newSessionState)(firstEdgeId) return {
messages: [],
newSessionState,
setVariableHistory: [],
visitedEdges: [],
}
const nextGroup = await getNextGroup({
state: newSessionState,
edgeId: firstEdgeId,
isOffDefaultPath: false,
})
newSessionState = nextGroup.newSessionState newSessionState = nextGroup.newSessionState
if (nextGroup.visitedEdge) visitedEdges.push(nextGroup.visitedEdge) if (!nextGroup.group)
if (!nextGroup.group) return { messages: [], newSessionState, visitedEdges } return { messages: [], newSessionState, visitedEdges, setVariableHistory }
return executeGroup(nextGroup.group, { return executeGroup(nextGroup.group, {
version, version,
state: newSessionState, state: newSessionState,
visitedEdges, visitedEdges,
setVariableHistory,
startTime, startTime,
}) })
} }

View File

@ -11,6 +11,7 @@ import {
SessionState, SessionState,
TypebotInSession, TypebotInSession,
Block, Block,
SetVariableHistoryItem,
} from '@typebot.io/schemas' } from '@typebot.io/schemas'
import { import {
StartChatInput, StartChatInput,
@ -31,7 +32,10 @@ import { injectVariablesFromExistingResult } from '@typebot.io/variables/injectV
import { getNextGroup } from './getNextGroup' import { getNextGroup } from './getNextGroup'
import { upsertResult } from './queries/upsertResult' import { upsertResult } from './queries/upsertResult'
import { continueBotFlow } from './continueBotFlow' import { continueBotFlow } from './continueBotFlow'
import { parseVariables } from '@typebot.io/variables/parseVariables' import {
getVariablesToParseInfoInText,
parseVariables,
} from '@typebot.io/variables/parseVariables'
import { defaultSettings } from '@typebot.io/schemas/features/typebot/settings/constants' import { defaultSettings } from '@typebot.io/schemas/features/typebot/settings/constants'
import { IntegrationBlockType } from '@typebot.io/schemas/features/blocks/integrations/constants' import { IntegrationBlockType } from '@typebot.io/schemas/features/blocks/integrations/constants'
import { VisitedEdge } from '@typebot.io/prisma' import { VisitedEdge } from '@typebot.io/prisma'
@ -42,6 +46,9 @@ import {
defaultGuestAvatarIsEnabled, defaultGuestAvatarIsEnabled,
defaultHostAvatarIsEnabled, defaultHostAvatarIsEnabled,
} from '@typebot.io/schemas/features/typebot/theme/constants' } from '@typebot.io/schemas/features/typebot/theme/constants'
import { BubbleBlockType } from '@typebot.io/schemas/features/blocks/bubbles/constants'
import { LogicBlockType } from '@typebot.io/schemas/features/blocks/logic/constants'
import { parseVariablesInRichText } from './parseBubbleBlock'
type StartParams = type StartParams =
| ({ | ({
@ -68,6 +75,7 @@ export const startSession = async ({
Omit<StartChatResponse, 'resultId' | 'isStreamEnabled' | 'sessionId'> & { Omit<StartChatResponse, 'resultId' | 'isStreamEnabled' | 'sessionId'> & {
newSessionState: SessionState newSessionState: SessionState
visitedEdges: VisitedEdge[] visitedEdges: VisitedEdge[]
setVariableHistory: SetVariableHistoryItem[]
resultId?: string resultId?: string
} }
> => { > => {
@ -145,6 +153,8 @@ export const startSession = async ({
: typebot.theme.general?.progressBar?.isEnabled : typebot.theme.general?.progressBar?.isEnabled
? { totalAnswers: 0 } ? { totalAnswers: 0 }
: undefined, : undefined,
setVariableIdsForHistory:
extractVariableIdsUsedForTranscript(typebotInSession),
...initialSessionState, ...initialSessionState,
} }
@ -164,6 +174,7 @@ export const startSession = async ({
dynamicTheme: parseDynamicTheme(initialState), dynamicTheme: parseDynamicTheme(initialState),
messages: [], messages: [],
visitedEdges: [], visitedEdges: [],
setVariableHistory: [],
} }
} }
@ -178,14 +189,18 @@ export const startSession = async ({
// If params has message and first block is an input block, we can directly continue the bot flow // If params has message and first block is an input block, we can directly continue the bot flow
if (message) { if (message) {
const firstEdgeId = getFirstEdgeId({ const firstEdgeId = getFirstEdgeId({
state: chatReply.newSessionState, typebot: chatReply.newSessionState.typebotsQueue[0].typebot,
startEventId: startEventId:
startParams.type === 'preview' && startParams.type === 'preview' &&
startParams.startFrom?.type === 'event' startParams.startFrom?.type === 'event'
? startParams.startFrom.eventId ? startParams.startFrom.eventId
: undefined, : undefined,
}) })
const nextGroup = await getNextGroup(chatReply.newSessionState)(firstEdgeId) const nextGroup = await getNextGroup({
state: chatReply.newSessionState,
edgeId: firstEdgeId,
isOffDefaultPath: false,
})
const newSessionState = nextGroup.newSessionState const newSessionState = nextGroup.newSessionState
const firstBlock = nextGroup.group?.blocks.at(0) const firstBlock = nextGroup.group?.blocks.at(0)
if (firstBlock && isInputBlock(firstBlock)) { if (firstBlock && isInputBlock(firstBlock)) {
@ -214,6 +229,7 @@ export const startSession = async ({
newSessionState, newSessionState,
logs, logs,
visitedEdges, visitedEdges,
setVariableHistory,
} = chatReply } = chatReply
const clientSideActions = startFlowClientActions ?? [] const clientSideActions = startFlowClientActions ?? []
@ -268,6 +284,7 @@ export const startSession = async ({
dynamicTheme: parseDynamicTheme(newSessionState), dynamicTheme: parseDynamicTheme(newSessionState),
logs: startLogs.length > 0 ? startLogs : undefined, logs: startLogs.length > 0 ? startLogs : undefined,
visitedEdges, visitedEdges,
setVariableHistory,
} }
return { return {
@ -290,6 +307,7 @@ export const startSession = async ({
dynamicTheme: parseDynamicTheme(newSessionState), dynamicTheme: parseDynamicTheme(newSessionState),
logs: startLogs.length > 0 ? startLogs : undefined, logs: startLogs.length > 0 ? startLogs : undefined,
visitedEdges, visitedEdges,
setVariableHistory,
} }
} }
@ -497,3 +515,59 @@ const convertStartTypebotToTypebotInSession = (
variables: startVariables, variables: startVariables,
events: typebot.events, events: typebot.events,
} }
const extractVariableIdsUsedForTranscript = (
typebot: TypebotInSession
): string[] => {
const variableIds: Set<string> = new Set()
const parseVarParams = {
variables: typebot.variables,
takeLatestIfList: typebot.version !== '6',
}
typebot.groups.forEach((group) => {
group.blocks.forEach((block) => {
if (block.type === BubbleBlockType.TEXT) {
const { parsedVariableIds } = parseVariablesInRichText(
block.content?.richText ?? [],
parseVarParams
)
parsedVariableIds.forEach((variableId) => variableIds.add(variableId))
}
if (
block.type === BubbleBlockType.IMAGE ||
block.type === BubbleBlockType.VIDEO ||
block.type === BubbleBlockType.AUDIO
) {
if (!block.content?.url) return
const variablesInfo = getVariablesToParseInfoInText(
block.content.url,
parseVarParams
)
variablesInfo.forEach((variableInfo) =>
variableInfo.variableId
? variableIds.add(variableInfo.variableId ?? '')
: undefined
)
}
if (block.type === LogicBlockType.CONDITION) {
block.items.forEach((item) =>
item.content?.comparisons?.forEach((comparison) => {
if (comparison.variableId) variableIds.add(comparison.variableId)
if (comparison.value) {
const variableIdsInValue = getVariablesToParseInfoInText(
comparison.value,
parseVarParams
)
variableIdsInValue.forEach((variableInfo) => {
variableInfo.variableId
? variableIds.add(variableInfo.variableId)
: undefined
})
}
})
)
}
})
})
return [...variableIds]
}

View File

@ -2,6 +2,7 @@ import {
ContinueChatResponse, ContinueChatResponse,
CustomEmbedBubble, CustomEmbedBubble,
SessionState, SessionState,
SetVariableHistoryItem,
} from '@typebot.io/schemas' } from '@typebot.io/schemas'
export type EdgeId = string export type EdgeId = string
@ -9,6 +10,7 @@ export type EdgeId = string
export type ExecuteLogicResponse = { export type ExecuteLogicResponse = {
outgoingEdgeId: EdgeId | undefined outgoingEdgeId: EdgeId | undefined
newSessionState?: SessionState newSessionState?: SessionState
newSetVariableHistory?: SetVariableHistoryItem[]
} & Pick<ContinueChatResponse, 'clientSideActions' | 'logs'> } & Pick<ContinueChatResponse, 'clientSideActions' | 'logs'>
export type ExecuteIntegrationResponse = { export type ExecuteIntegrationResponse = {
@ -16,6 +18,7 @@ export type ExecuteIntegrationResponse = {
newSessionState?: SessionState newSessionState?: SessionState
startTimeShouldBeUpdated?: boolean startTimeShouldBeUpdated?: boolean
customEmbedBubble?: CustomEmbedBubble customEmbedBubble?: CustomEmbedBubble
newSetVariableHistory?: SetVariableHistoryItem[]
} & Pick<ContinueChatResponse, 'clientSideActions' | 'logs'> } & Pick<ContinueChatResponse, 'clientSideActions' | 'logs'>
type WhatsAppMediaMessage = { type WhatsAppMediaMessage = {

View File

@ -114,6 +114,7 @@ export const resumeWhatsAppFlow = async ({
messages, messages,
clientSideActions, clientSideActions,
visitedEdges, visitedEdges,
setVariableHistory,
} = resumeResponse } = resumeResponse
const isFirstChatChunk = (!session || isSessionExpired) ?? false const isFirstChatChunk = (!session || isSessionExpired) ?? false
@ -140,6 +141,7 @@ export const resumeWhatsAppFlow = async ({
}, },
}, },
visitedEdges, visitedEdges,
setVariableHistory,
}) })
return { return {

View File

@ -3,6 +3,7 @@ import {
ContinueChatResponse, ContinueChatResponse,
PublicTypebot, PublicTypebot,
SessionState, SessionState,
SetVariableHistoryItem,
Settings, Settings,
Typebot, Typebot,
} from '@typebot.io/schemas' } from '@typebot.io/schemas'
@ -35,6 +36,7 @@ export const startWhatsAppSession = async ({
| (ContinueChatResponse & { | (ContinueChatResponse & {
newSessionState: SessionState newSessionState: SessionState
visitedEdges: VisitedEdge[] visitedEdges: VisitedEdge[]
setVariableHistory: SetVariableHistoryItem[]
}) })
| { error: string } | { error: string }
> => { > => {

View File

@ -1,6 +1,6 @@
{ {
"name": "@typebot.io/js", "name": "@typebot.io/js",
"version": "0.2.81", "version": "0.2.82",
"description": "Javascript library to display typebots on your website", "description": "Javascript library to display typebots on your website",
"type": "module", "type": "module",
"main": "dist/index.js", "main": "dist/index.js",

View File

@ -39,13 +39,14 @@ export type BotProps = {
apiHost?: string apiHost?: string
font?: Font font?: Font
progressBarRef?: HTMLDivElement progressBarRef?: HTMLDivElement
startFrom?: StartFrom
sessionId?: string
onNewInputBlock?: (inputBlock: InputBlock) => void onNewInputBlock?: (inputBlock: InputBlock) => void
onAnswer?: (answer: { message: string; blockId: string }) => void onAnswer?: (answer: { message: string; blockId: string }) => void
onInit?: () => void onInit?: () => void
onEnd?: () => void onEnd?: () => void
onNewLogs?: (logs: OutgoingLog[]) => void onNewLogs?: (logs: OutgoingLog[]) => void
onChatStatePersisted?: (isEnabled: boolean) => void onChatStatePersisted?: (isEnabled: boolean) => void
startFrom?: StartFrom
} }
export const Bot = (props: BotProps & { class?: string }) => { export const Bot = (props: BotProps & { class?: string }) => {
@ -81,6 +82,7 @@ export const Bot = (props: BotProps & { class?: string }) => {
...props.prefilledVariables, ...props.prefilledVariables,
}, },
startFrom: props.startFrom, startFrom: props.startFrom,
sessionId: props.sessionId,
}) })
if (error instanceof HTTPError) { if (error instanceof HTTPError) {
if (isPreview) { if (isPreview) {

View File

@ -14,6 +14,7 @@ export const defaultBotProps: BotProps = {
prefilledVariables: undefined, prefilledVariables: undefined,
apiHost: undefined, apiHost: undefined,
resultId: undefined, resultId: undefined,
sessionId: undefined,
} }
export const defaultPopupProps: PopupProps = { export const defaultPopupProps: PopupProps = {

View File

@ -21,6 +21,7 @@ type Props = {
isPreview: boolean isPreview: boolean
prefilledVariables?: Record<string, unknown> prefilledVariables?: Record<string, unknown>
resultId?: string resultId?: string
sessionId?: string
} }
export async function startChatQuery({ export async function startChatQuery({
@ -31,6 +32,7 @@ export async function startChatQuery({
resultId, resultId,
stripeRedirectStatus, stripeRedirectStatus,
startFrom, startFrom,
sessionId,
}: Props) { }: Props) {
if (isNotDefined(typebot)) if (isNotDefined(typebot))
throw new Error('Typebot ID is required to get initial messages') throw new Error('Typebot ID is required to get initial messages')
@ -83,6 +85,7 @@ export async function startChatQuery({
startFrom, startFrom,
typebot, typebot,
prefilledVariables, prefilledVariables,
sessionId,
} satisfies Omit< } satisfies Omit<
StartPreviewChatInput, StartPreviewChatInput,
'typebotId' | 'isOnlyRegistering' 'typebotId' | 'isOnlyRegistering'

View File

@ -1,6 +1,6 @@
{ {
"name": "@typebot.io/nextjs", "name": "@typebot.io/nextjs",
"version": "0.2.81", "version": "0.2.82",
"description": "Convenient library to display typebots on your Next.js website", "description": "Convenient library to display typebots on your Next.js website",
"main": "dist/index.js", "main": "dist/index.js",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",

View File

@ -1,6 +1,6 @@
{ {
"name": "@typebot.io/react", "name": "@typebot.io/react",
"version": "0.2.81", "version": "0.2.82",
"description": "Convenient library to display typebots on your React app", "description": "Convenient library to display typebots on your React app",
"main": "dist/index.js", "main": "dist/index.js",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",

View File

@ -0,0 +1,397 @@
import {
Answer,
ContinueChatResponse,
Edge,
Group,
InputBlock,
TypebotInSession,
Variable,
} from '@typebot.io/schemas'
import { SetVariableHistoryItem } from '@typebot.io/schemas/features/result'
import { isBubbleBlock, isInputBlock } from '@typebot.io/schemas/helpers'
import { BubbleBlockType } from '@typebot.io/schemas/features/blocks/bubbles/constants'
import { convertRichTextToMarkdown } from '@typebot.io/lib/markdown/convertRichTextToMarkdown'
import { LogicBlockType } from '@typebot.io/schemas/features/blocks/logic/constants'
import { createId } from '@typebot.io/lib/createId'
import { executeCondition } from './executeCondition'
import {
parseBubbleBlock,
BubbleBlockWithDefinedContent,
} from '../bot-engine/parseBubbleBlock'
import { defaultChoiceInputOptions } from '@typebot.io/schemas/features/blocks/inputs/choice/constants'
import { defaultPictureChoiceOptions } from '@typebot.io/schemas/features/blocks/inputs/pictureChoice/constants'
import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants'
import { parseVariables } from '@typebot.io/variables/parseVariables'
type TranscriptMessage =
| {
role: 'bot' | 'user'
} & (
| { type: 'text'; text: string }
| { type: 'image'; image: string }
| { type: 'video'; video: string }
| { type: 'audio'; audio: string }
)
export const parseTranscriptMessageText = (
message: TranscriptMessage
): string => {
switch (message.type) {
case 'text':
return message.text
case 'image':
return message.image
case 'video':
return message.video
case 'audio':
return message.audio
}
}
export const computeResultTranscript = ({
typebot,
answers,
setVariableHistory,
visitedEdges,
stopAtBlockId,
}: {
typebot: TypebotInSession
answers: Pick<Answer, 'blockId' | 'content'>[]
setVariableHistory: Pick<
SetVariableHistoryItem,
'blockId' | 'variableId' | 'value'
>[]
visitedEdges: string[]
stopAtBlockId?: string
}): TranscriptMessage[] => {
const firstEdgeId = getFirstEdgeId(typebot)
if (!firstEdgeId) return []
const firstEdge = typebot.edges.find((edge) => edge.id === firstEdgeId)
if (!firstEdge) return []
const firstGroup = getNextGroup(typebot, firstEdgeId)
if (!firstGroup) return []
return executeGroup({
typebotsQueue: [{ typebot }],
nextGroup: firstGroup,
currentTranscript: [],
answers,
setVariableHistory,
visitedEdges,
stopAtBlockId,
})
}
const getFirstEdgeId = (typebot: TypebotInSession) => {
if (typebot.version === '6') return typebot.events?.[0].outgoingEdgeId
return typebot.groups.at(0)?.blocks.at(0)?.outgoingEdgeId
}
const getNextGroup = (
typebot: TypebotInSession,
edgeId: string
): { group: Group; blockIndex?: number } | undefined => {
const edge = typebot.edges.find((edge) => edge.id === edgeId)
if (!edge) return
const group = typebot.groups.find((group) => group.id === edge.to.groupId)
if (!group) return
const blockIndex = edge.to.blockId
? group.blocks.findIndex((block) => block.id === edge.to.blockId)
: undefined
return { group, blockIndex }
}
const executeGroup = ({
currentTranscript,
typebotsQueue,
answers,
nextGroup,
setVariableHistory,
visitedEdges,
stopAtBlockId,
}: {
currentTranscript: TranscriptMessage[]
nextGroup:
| {
group: Group
blockIndex?: number | undefined
}
| undefined
typebotsQueue: {
typebot: TypebotInSession
resumeEdgeId?: string
}[]
answers: Pick<Answer, 'blockId' | 'content'>[]
setVariableHistory: Pick<
SetVariableHistoryItem,
'blockId' | 'variableId' | 'value'
>[]
visitedEdges: string[]
stopAtBlockId?: string
}): TranscriptMessage[] => {
if (!nextGroup) return currentTranscript
for (const block of nextGroup?.group.blocks.slice(
nextGroup.blockIndex ?? 0
)) {
if (stopAtBlockId && block.id === stopAtBlockId) return currentTranscript
if (setVariableHistory.at(0)?.blockId === block.id)
typebotsQueue[0].typebot.variables = applySetVariable(
setVariableHistory.shift(),
typebotsQueue[0].typebot
)
let nextEdgeId = block.outgoingEdgeId
if (isBubbleBlock(block)) {
if (!block.content) continue
const parsedBubbleBlock = parseBubbleBlock(
block as BubbleBlockWithDefinedContent,
{
version: 2,
variables: typebotsQueue[0].typebot.variables,
typebotVersion: typebotsQueue[0].typebot.version,
}
)
const newMessage =
convertChatMessageToTranscriptMessage(parsedBubbleBlock)
if (newMessage) currentTranscript.push(newMessage)
} else if (isInputBlock(block)) {
const answer = answers.shift()
if (!answer) break
if (block.options?.variableId) {
const variable = typebotsQueue[0].typebot.variables.find(
(variable) => variable.id === block.options?.variableId
)
if (variable) {
typebotsQueue[0].typebot.variables =
typebotsQueue[0].typebot.variables.map((v) =>
v.id === variable.id ? { ...v, value: answer.content } : v
)
}
}
currentTranscript.push({
role: 'user',
type: 'text',
text: answer.content,
})
const outgoingEdge = getOutgoingEdgeId({
block,
answer: answer.content,
variables: typebotsQueue[0].typebot.variables,
})
if (outgoingEdge.isOffDefaultPath) visitedEdges.shift()
nextEdgeId = outgoingEdge.edgeId
} else if (block.type === LogicBlockType.CONDITION) {
const passedCondition = block.items.find(
(item) =>
item.content &&
executeCondition({
variables: typebotsQueue[0].typebot.variables,
condition: item.content,
})
)
if (passedCondition) {
visitedEdges.shift()
nextEdgeId = passedCondition.outgoingEdgeId
}
} else if (block.type === LogicBlockType.AB_TEST) {
nextEdgeId = visitedEdges.shift() ?? nextEdgeId
} else if (block.type === LogicBlockType.JUMP) {
if (!block.options?.groupId) continue
const groupToJumpTo = typebotsQueue[0].typebot.groups.find(
(group) => group.id === block.options?.groupId
)
const blockToJumpTo =
groupToJumpTo?.blocks.find((b) => b.id === block.options?.blockId) ??
groupToJumpTo?.blocks[0]
if (!blockToJumpTo) continue
const portalEdge = {
id: createId(),
from: { blockId: '', groupId: '' },
to: { groupId: block.options.groupId, blockId: blockToJumpTo.id },
}
typebotsQueue[0].typebot.edges.push(portalEdge)
visitedEdges.shift()
nextEdgeId = portalEdge.id
} else if (block.type === LogicBlockType.TYPEBOT_LINK) {
const isLinkingSameTypebot =
block.options &&
(block.options.typebotId === 'current' ||
block.options.typebotId === typebotsQueue[0].typebot.id)
if (!isLinkingSameTypebot) continue
let resumeEdge: Edge | undefined
if (!block.outgoingEdgeId) {
const currentBlockIndex = nextGroup.group.blocks.findIndex(
(b) => b.id === block.id
)
const nextBlockInGroup =
currentBlockIndex === -1
? undefined
: nextGroup.group.blocks.at(currentBlockIndex + 1)
if (nextBlockInGroup)
resumeEdge = {
id: createId(),
from: {
blockId: '',
},
to: {
groupId: nextGroup.group.id,
blockId: nextBlockInGroup.id,
},
}
}
return executeGroup({
typebotsQueue: [
{
typebot: typebotsQueue[0].typebot,
resumeEdgeId: resumeEdge ? resumeEdge.id : block.outgoingEdgeId,
},
{
typebot: resumeEdge
? {
...typebotsQueue[0].typebot,
edges: typebotsQueue[0].typebot.edges.concat([resumeEdge]),
}
: typebotsQueue[0].typebot,
},
],
answers,
setVariableHistory,
currentTranscript,
nextGroup,
visitedEdges,
stopAtBlockId,
})
}
if (nextEdgeId) {
const nextGroup = getNextGroup(typebotsQueue[0].typebot, nextEdgeId)
if (nextGroup) {
return executeGroup({
typebotsQueue,
answers,
setVariableHistory,
currentTranscript,
nextGroup,
visitedEdges,
stopAtBlockId,
})
}
}
}
if (typebotsQueue.length > 1 && typebotsQueue[0].resumeEdgeId) {
return executeGroup({
typebotsQueue: typebotsQueue.slice(1),
answers,
setVariableHistory,
currentTranscript,
nextGroup: getNextGroup(
typebotsQueue[1].typebot,
typebotsQueue[0].resumeEdgeId
),
visitedEdges: visitedEdges.slice(1),
stopAtBlockId,
})
}
return currentTranscript
}
const applySetVariable = (
setVariable:
| Pick<SetVariableHistoryItem, 'blockId' | 'variableId' | 'value'>
| undefined,
typebot: TypebotInSession
): Variable[] => {
if (!setVariable) return typebot.variables
const variable = typebot.variables.find(
(variable) => variable.id === setVariable.variableId
)
if (!variable) return typebot.variables
return typebot.variables.map((v) =>
v.id === variable.id ? { ...v, value: setVariable.value } : v
)
}
const convertChatMessageToTranscriptMessage = (
chatMessage: ContinueChatResponse['messages'][0]
): TranscriptMessage | null => {
switch (chatMessage.type) {
case BubbleBlockType.TEXT: {
if (!chatMessage.content.richText) return null
return {
role: 'bot',
type: 'text',
text: convertRichTextToMarkdown(chatMessage.content.richText),
}
}
case BubbleBlockType.IMAGE: {
if (!chatMessage.content.url) return null
return {
role: 'bot',
type: 'image',
image: chatMessage.content.url,
}
}
case BubbleBlockType.VIDEO: {
if (!chatMessage.content.url) return null
return {
role: 'bot',
type: 'video',
video: chatMessage.content.url,
}
}
case BubbleBlockType.AUDIO: {
if (!chatMessage.content.url) return null
return {
role: 'bot',
type: 'audio',
audio: chatMessage.content.url,
}
}
case 'custom-embed':
case BubbleBlockType.EMBED: {
return null
}
}
}
const getOutgoingEdgeId = ({
block,
answer,
variables,
}: {
block: InputBlock
answer: string | undefined
variables: Variable[]
}): { edgeId: string | undefined; isOffDefaultPath: boolean } => {
if (
block.type === InputBlockType.CHOICE &&
!(
block.options?.isMultipleChoice ??
defaultChoiceInputOptions.isMultipleChoice
) &&
answer
) {
const matchedItem = block.items.find(
(item) =>
parseVariables(variables)(item.content).normalize() ===
answer.normalize()
)
if (matchedItem?.outgoingEdgeId)
return { edgeId: matchedItem.outgoingEdgeId, isOffDefaultPath: true }
}
if (
block.type === InputBlockType.PICTURE_CHOICE &&
!(
block.options?.isMultipleChoice ??
defaultPictureChoiceOptions.isMultipleChoice
) &&
answer
) {
const matchedItem = block.items.find(
(item) =>
parseVariables(variables)(item.title).normalize() === answer.normalize()
)
if (matchedItem?.outgoingEdgeId)
return { edgeId: matchedItem.outgoingEdgeId, isOffDefaultPath: true }
}
return { edgeId: block.outgoingEdgeId, isOffDefaultPath: false }
}

View File

@ -8,15 +8,18 @@ import {
defaultConditionItemContent, defaultConditionItemContent,
} from '@typebot.io/schemas/features/blocks/logic/condition/constants' } from '@typebot.io/schemas/features/blocks/logic/condition/constants'
export const executeCondition = type Props = {
(variables: Variable[]) => condition: Condition
(condition: Condition): boolean => { variables: Variable[]
if (!condition.comparisons) return false }
return (condition.logicalOperator ??
defaultConditionItemContent.logicalOperator) === LogicalOperator.AND export const executeCondition = ({ condition, variables }: Props): boolean => {
? condition.comparisons.every(executeComparison(variables)) if (!condition.comparisons) return false
: condition.comparisons.some(executeComparison(variables)) return (condition.logicalOperator ??
} defaultConditionItemContent.logicalOperator) === LogicalOperator.AND
? condition.comparisons.every(executeComparison(variables))
: condition.comparisons.some(executeComparison(variables))
}
const executeComparison = const executeComparison =
(variables: Variable[]) => (variables: Variable[]) =>

View File

@ -0,0 +1,18 @@
{
"name": "@typebot.io/logic",
"version": "1.0.0",
"description": "",
"scripts": {},
"keywords": [],
"author": "Baptiste Arnaud",
"license": "ISC",
"dependencies": {
"@typebot.io/schemas": "workspace:*",
"@typebot.io/lib": "workspace:*",
"@typebot.io/variables": "workspace:*"
},
"devDependencies": {
"@typebot.io/tsconfig": "workspace:*",
"@udecode/plate-common": "30.4.5"
}
}

View File

@ -0,0 +1,12 @@
{
"extends": "@typebot.io/tsconfig/base.json",
"include": [
"**/*.ts",
"../variables/parseVariables.ts",
"../bot-engine/parseBubbleBlock.ts"
],
"exclude": ["node_modules"],
"compilerOptions": {
"lib": ["ES2021", "DOM"]
}
}

View File

@ -58,13 +58,12 @@ const createAnswers = ({
count, count,
resultIdPrefix, resultIdPrefix,
}: { resultIdPrefix: string } & Pick<CreateFakeResultsProps, 'count'>) => { }: { resultIdPrefix: string } & Pick<CreateFakeResultsProps, 'count'>) => {
return prisma.answer.createMany({ return prisma.answerV2.createMany({
data: [ data: [
...Array.from(Array(count)).map((_, idx) => ({ ...Array.from(Array(count)).map((_, idx) => ({
resultId: `${resultIdPrefix}-result${idx}`, resultId: `${resultIdPrefix}-result${idx}`,
content: `content${idx}`, content: `content${idx}`,
blockId: 'block1', blockId: 'block1',
groupId: 'group1',
})), })),
], ],
}) })

View File

@ -255,22 +255,36 @@ model PublicTypebot {
} }
model Result { model Result {
id String @id @default(cuid()) id String @id @default(cuid())
createdAt DateTime @default(now()) createdAt DateTime @default(now())
typebotId String typebotId String
variables Json variables Json
isCompleted Boolean isCompleted Boolean
hasStarted Boolean? hasStarted Boolean?
isArchived Boolean? @default(false) isArchived Boolean? @default(false)
typebot Typebot @relation(fields: [typebotId], references: [id], onDelete: Cascade) lastChatSessionId String?
answers Answer[] typebot Typebot @relation(fields: [typebotId], references: [id], onDelete: Cascade)
logs Log[] answers Answer[]
edges VisitedEdge[] logs Log[]
edges VisitedEdge[]
setVariableHistory SetVariableHistoryItem[]
answersV2 AnswerV2[]
@@index([typebotId, isArchived, hasStarted, createdAt(sort: Desc)]) @@index([typebotId, isArchived, hasStarted, createdAt(sort: Desc)])
@@index([typebotId, isArchived, isCompleted]) @@index([typebotId, isArchived, isCompleted])
} }
model SetVariableHistoryItem {
result Result @relation(fields: [resultId], references: [id], onDelete: Cascade)
resultId String
index Int
variableId String
blockId String
value Json // string or list of strings
@@unique([resultId, index])
}
model VisitedEdge { model VisitedEdge {
result Result @relation(fields: [resultId], references: [id], onDelete: Cascade) result Result @relation(fields: [resultId], references: [id], onDelete: Cascade)
resultId String resultId String
@ -292,20 +306,28 @@ model Log {
@@index([resultId]) @@index([resultId])
} }
// TODO: gradually remove variableId and groupId
model Answer { model Answer {
createdAt DateTime @default(now()) @updatedAt createdAt DateTime @default(now()) @updatedAt
resultId String resultId String
blockId String blockId String
itemId String? groupId String
groupId String variableId String?
variableId String? content String @db.Text
content String @db.Text result Result @relation(fields: [resultId], references: [id], onDelete: Cascade)
storageUsed Int?
result Result @relation(fields: [resultId], references: [id], onDelete: Cascade)
@@unique([resultId, blockId, groupId]) @@unique([resultId, blockId, groupId])
@@index([blockId, itemId]) }
@@index([storageUsed])
model AnswerV2 {
id Int @id @default(autoincrement())
blockId String
content String
resultId String
result Result @relation(fields: [resultId], references: [id], onDelete: Cascade)
@@index([resultId])
@@index([blockId])
} }
model Coupon { model Coupon {

View File

@ -0,0 +1,47 @@
/*
Warnings:
- You are about to drop the column `itemId` on the `Answer` table. All the data in the column will be lost.
- You are about to drop the column `storageUsed` on the `Answer` table. All the data in the column will be lost.
*/
-- DropIndex
DROP INDEX "Answer_blockId_itemId_idx";
-- AlterTable
ALTER TABLE "Answer" DROP COLUMN "itemId",
DROP COLUMN "storageUsed";
-- AlterTable
ALTER TABLE "Result" ADD COLUMN "lastChatSessionId" TEXT;
-- CreateTable
CREATE TABLE "SetVariableHistoryItem" (
"resultId" TEXT NOT NULL,
"index" INTEGER NOT NULL,
"variableId" TEXT NOT NULL,
"blockId" TEXT NOT NULL,
"value" JSONB NOT NULL
);
-- CreateTable
CREATE TABLE "AnswerV2" (
"id" SERIAL NOT NULL,
"blockId" TEXT NOT NULL,
"content" TEXT NOT NULL,
"resultId" TEXT NOT NULL,
CONSTRAINT "AnswerV2_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "SetVariableHistoryItem_resultId_index_key" ON "SetVariableHistoryItem"("resultId", "index");
-- CreateIndex
CREATE INDEX "AnswerV2_blockId_idx" ON "AnswerV2"("blockId");
-- AddForeignKey
ALTER TABLE "SetVariableHistoryItem" ADD CONSTRAINT "SetVariableHistoryItem_resultId_fkey" FOREIGN KEY ("resultId") REFERENCES "Result"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "AnswerV2" ADD CONSTRAINT "AnswerV2_resultId_fkey" FOREIGN KEY ("resultId") REFERENCES "Result"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -236,22 +236,36 @@ model PublicTypebot {
} }
model Result { model Result {
id String @id @default(cuid()) id String @id @default(cuid())
createdAt DateTime @default(now()) createdAt DateTime @default(now())
typebotId String typebotId String
variables Json variables Json
isCompleted Boolean isCompleted Boolean
hasStarted Boolean? hasStarted Boolean?
isArchived Boolean? @default(false) isArchived Boolean? @default(false)
typebot Typebot @relation(fields: [typebotId], references: [id], onDelete: Cascade) lastChatSessionId String?
answers Answer[] typebot Typebot @relation(fields: [typebotId], references: [id], onDelete: Cascade)
logs Log[] answers Answer[]
edges VisitedEdge[] answersV2 AnswerV2[]
logs Log[]
edges VisitedEdge[]
setVariableHistory SetVariableHistoryItem[]
@@index([typebotId, hasStarted, createdAt(sort: Desc)]) @@index([typebotId, hasStarted, createdAt(sort: Desc)])
@@index([typebotId, isCompleted]) @@index([typebotId, isCompleted])
} }
model SetVariableHistoryItem {
result Result @relation(fields: [resultId], references: [id], onDelete: Cascade)
resultId String
index Int
variableId String
blockId String
value Json // string or list
@@unique([resultId, index])
}
model VisitedEdge { model VisitedEdge {
result Result @relation(fields: [resultId], references: [id], onDelete: Cascade) result Result @relation(fields: [resultId], references: [id], onDelete: Cascade)
resultId String resultId String
@ -274,19 +288,25 @@ model Log {
} }
model Answer { model Answer {
createdAt DateTime @default(now()) @updatedAt createdAt DateTime @default(now()) @updatedAt
resultId String resultId String
blockId String blockId String
itemId String? groupId String
groupId String variableId String?
variableId String? content String
content String result Result @relation(fields: [resultId], references: [id], onDelete: Cascade)
storageUsed Int?
result Result @relation(fields: [resultId], references: [id], onDelete: Cascade)
@@unique([resultId, blockId, groupId]) @@unique([resultId, blockId, groupId])
@@index([blockId, itemId]) }
@@index([storageUsed])
model AnswerV2 {
id Int @id @default(autoincrement())
blockId String
content String
resultId String
result Result @relation(fields: [resultId], references: [id], onDelete: Cascade)
@@index([blockId])
} }
model Coupon { model Coupon {

View File

@ -2,6 +2,7 @@ import { Prisma, PrismaClient } from '@typebot.io/prisma'
import { Block, Typebot } from '@typebot.io/schemas' import { Block, Typebot } from '@typebot.io/schemas'
import { deleteFilesFromBucket } from '@typebot.io/lib/s3/deleteFilesFromBucket' import { deleteFilesFromBucket } from '@typebot.io/lib/s3/deleteFilesFromBucket'
import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants' import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants'
import { isDefined } from '@typebot.io/lib'
type ArchiveResultsProps = { type ArchiveResultsProps = {
typebot: Pick<Typebot, 'groups'> typebot: Pick<Typebot, 'groups'>
@ -42,6 +43,7 @@ export const archiveResults =
}, },
select: { select: {
id: true, id: true,
lastChatSessionId: true,
}, },
take: batchSize, take: batchSize,
}) })
@ -76,6 +78,30 @@ export const archiveResults =
resultId: { in: resultIds }, resultId: { in: resultIds },
}, },
}), }),
prisma.answerV2.deleteMany({
where: {
resultId: { in: resultIds },
},
}),
prisma.visitedEdge.deleteMany({
where: {
resultId: { in: resultIds },
},
}),
prisma.setVariableHistoryItem.deleteMany({
where: {
resultId: { in: resultIds },
},
}),
prisma.chatSession.deleteMany({
where: {
id: {
in: resultsToDelete
.map((r) => r.lastChatSessionId)
.filter(isDefined),
},
},
}),
prisma.result.updateMany({ prisma.result.updateMany({
where: { where: {
id: { in: resultIds }, id: { in: resultIds },

View File

@ -24,11 +24,19 @@ const defaultCellParser: CellParser = (content, blockType) => {
: { plainText: content.toString() } : { plainText: content.toString() }
} }
export const convertResultsToTableData = ( type Props = {
results: ResultWithAnswers[] | undefined, results: ResultWithAnswers[] | undefined
headerCells: ResultHeaderCell[], headerCells: ResultHeaderCell[]
cellParser: CellParser = defaultCellParser cellParser?: CellParser
): TableData[] => blockIdVariableIdMap: Record<string, string>
}
export const convertResultsToTableData = ({
results,
headerCells,
cellParser = defaultCellParser,
blockIdVariableIdMap,
}: Props): TableData[] =>
(results ?? []).map((result) => ({ (results ?? []).map((result) => ({
id: { plainText: result.id }, id: { plainText: result.id },
date: { date: {
@ -37,23 +45,23 @@ export const convertResultsToTableData = (
...[...result.answers, ...result.variables].reduce<{ ...[...result.answers, ...result.variables].reduce<{
[key: string]: { element?: JSX.Element; plainText: string } [key: string]: { element?: JSX.Element; plainText: string }
}>((tableData, answerOrVariable) => { }>((tableData, answerOrVariable) => {
if ('groupId' in answerOrVariable) { if ('blockId' in answerOrVariable) {
const answer = answerOrVariable satisfies Answer const answer = answerOrVariable satisfies Pick<
const header = answer.variableId Answer,
'blockId' | 'content'
>
const answerVariableId = blockIdVariableIdMap[answer.blockId]
const header = answerVariableId
? headerCells.find((headerCell) => ? headerCells.find((headerCell) =>
headerCell.variableIds?.includes(answer.variableId as string) headerCell.variableIds?.includes(answerVariableId)
) )
: headerCells.find((headerCell) => : headerCells.find((headerCell) =>
headerCell.blocks?.some((block) => block.id === answer.blockId) headerCell.blocks?.some((block) => block.id === answer.blockId)
) )
if (!header || !header.blocks || !header.blockType) return tableData if (!header || !header.blocks || !header.blockType) return tableData
const variableValue = result.variables.find(
(variable) => variable.id === answer.variableId
)?.value
const content = variableValue ?? answer.content
return { return {
...tableData, ...tableData,
[header.id]: cellParser(content, header.blockType), [header.id]: cellParser(answer.content, header.blockType),
} }
} }
const variable = answerOrVariable satisfies VariableWithValue const variable = answerOrVariable satisfies VariableWithValue

View File

@ -0,0 +1,19 @@
import { PublicTypebotV6 } from '@typebot.io/schemas'
import { isInputBlock } from '@typebot.io/schemas/helpers'
export const parseBlockIdVariableIdMap = (
groups?: PublicTypebotV6['groups']
): {
[key: string]: string
} => {
if (!groups) return {}
const blockIdVariableIdMap: { [key: string]: string } = {}
groups.forEach((group) => {
group.blocks.forEach((block) => {
if (isInputBlock(block) && block.options?.variableId) {
blockIdVariableIdMap[block.id] = block.options.variableId
}
})
})
return blockIdVariableIdMap
}

View File

@ -35,7 +35,11 @@ export const parseResultHeader = (
{ label: 'Submitted at', id: 'date' }, { label: 'Submitted at', id: 'date' },
...inputsResultHeader, ...inputsResultHeader,
...parseVariablesHeaders(parsedVariables, inputsResultHeader), ...parseVariablesHeaders(parsedVariables, inputsResultHeader),
...parseResultsFromPreviousBotVersions(results ?? [], inputsResultHeader), ...parseResultsFromPreviousBotVersions({
results: results ?? [],
existingInputResultHeaders: inputsResultHeader,
groups: parsedGroups,
}),
] ]
} }
@ -176,19 +180,22 @@ const parseVariablesHeaders = (
return [...existingHeaders, newHeaderCell] return [...existingHeaders, newHeaderCell]
}, []) }, [])
const parseResultsFromPreviousBotVersions = ( const parseResultsFromPreviousBotVersions = ({
results: ResultWithAnswers[], results,
existingInputResultHeaders,
groups,
}: {
results: ResultWithAnswers[]
existingInputResultHeaders: ResultHeaderCell[] existingInputResultHeaders: ResultHeaderCell[]
): ResultHeaderCell[] => groups: Group[]
}): ResultHeaderCell[] =>
results results
.flatMap((result) => result.answers) .flatMap((result) => result.answers)
.filter( .filter(
(answer) => (answer) =>
!answer.variableId &&
existingInputResultHeaders.every( existingInputResultHeaders.every(
(header) => header.id !== answer.blockId (header) => header.id !== answer.blockId
) && ) && isNotEmpty(answer.content)
isNotEmpty(answer.content)
) )
.reduce<ResultHeaderCell[]>((existingHeaders, answer) => { .reduce<ResultHeaderCell[]>((existingHeaders, answer) => {
if ( if (
@ -197,6 +204,10 @@ const parseResultsFromPreviousBotVersions = (
) )
) )
return existingHeaders return existingHeaders
const groupId =
groups.find((group) =>
group.blocks.some((block) => block.id === answer.blockId)
)?.id ?? ''
return [ return [
...existingHeaders, ...existingHeaders,
{ {
@ -205,7 +216,7 @@ const parseResultsFromPreviousBotVersions = (
blocks: [ blocks: [
{ {
id: answer.blockId, id: answer.blockId,
groupId: answer.groupId, groupId,
}, },
], ],
blockType: InputBlockType.TEXT, blockType: InputBlockType.TEXT,

View File

@ -1,27 +1,28 @@
import { z } from '../zod' import { z } from '../zod'
import { Answer as AnswerPrisma, Prisma } from '@typebot.io/prisma' import { Answer as AnswerV1Prisma, Prisma } from '@typebot.io/prisma'
export const answerSchema = z.object({ const answerV1Schema = z.object({
createdAt: z.date(), createdAt: z.date(),
resultId: z.string(), resultId: z.string(),
blockId: z.string(), blockId: z.string(),
groupId: z.string(), groupId: z.string(),
variableId: z.string().nullable(), variableId: z.string().nullable(),
content: z.string(), content: z.string(),
storageUsed: z.number().nullable(), }) satisfies z.ZodType<AnswerV1Prisma>
// TO-DO: remove once itemId is removed from database schema
}) satisfies z.ZodType<Omit<AnswerPrisma, 'itemId'>>
export const answerInputSchema = answerSchema export const answerSchema = z.object({
blockId: z.string(),
content: z.string(),
})
export const answerInputSchema = answerV1Schema
.omit({ .omit({
createdAt: true, createdAt: true,
resultId: true, resultId: true,
variableId: true, variableId: true,
storageUsed: true,
}) })
.extend({ .extend({
variableId: z.string().nullish(), variableId: z.string().nullish(),
storageUsed: z.number().nullish(),
}) satisfies z.ZodType<Prisma.AnswerUncheckedUpdateInput> }) satisfies z.ZodType<Prisma.AnswerUncheckedUpdateInput>
export const statsSchema = z.object({ export const statsSchema = z.object({

View File

@ -5,6 +5,7 @@ export const valueTypes = [
'Empty', 'Empty',
'Append value(s)', 'Append value(s)',
'Environment name', 'Environment name',
'Transcript',
'User ID', 'User ID',
'Result ID', 'Result ID',
'Now', 'Now',
@ -20,6 +21,8 @@ export const valueTypes = [
export const hiddenTypes = ['Today', 'User ID'] as const export const hiddenTypes = ['Today', 'User ID'] as const
export const sessionOnlySetVariableOptions = ['Transcript'] as const
export const defaultSetVariableOptions = { export const defaultSetVariableOptions = {
type: 'Custom', type: 'Custom',
isExecutedOnClient: false, isExecutedOnClient: false,

View File

@ -21,6 +21,7 @@ const basicSetVariableOptionsSchema = baseOptions.extend({
'Random ID', 'Random ID',
'Phone number', 'Phone number',
'Contact name', 'Contact name',
'Transcript',
]), ]),
}) })

View File

@ -260,6 +260,12 @@ export const startPreviewChatInputSchema = z.object({
Email: 'john@gmail.com', Email: 'john@gmail.com',
}, },
}), }),
sessionId: z
.string()
.optional()
.describe(
'If provided, will be used as the session ID and will overwrite any existing session with the same ID.'
),
}) })
export type StartPreviewChatInput = z.infer<typeof startPreviewChatInputSchema> export type StartPreviewChatInput = z.infer<typeof startPreviewChatInputSchema>

View File

@ -1,14 +1,9 @@
import { z } from '../../zod' import { z } from '../../zod'
import { answerSchema } from '../answer' import { answerSchema } from '../answer'
import { resultSchema } from '../result' import { resultSchema, setVariableHistoryItemSchema } from '../result'
import { typebotInSessionStateSchema, dynamicThemeSchema } from './shared' import { typebotInSessionStateSchema, dynamicThemeSchema } from './shared'
import { settingsSchema } from '../typebot/settings' import { settingsSchema } from '../typebot/settings'
import { isInputBlock } from '../../helpers'
const answerInSessionStateSchema = answerSchema.pick({
content: true,
blockId: true,
variableId: true,
})
const answerInSessionStateSchemaV2 = z.object({ const answerInSessionStateSchemaV2 = z.object({
key: z.string(), key: z.string(),
@ -23,7 +18,7 @@ const resultInSessionStateSchema = resultSchema
}) })
.merge( .merge(
z.object({ z.object({
answers: z.array(answerInSessionStateSchema), answers: z.array(answerSchema),
id: z.string().optional(), id: z.string().optional(),
}) })
) )
@ -94,6 +89,23 @@ const sessionStateSchemaV3 = sessionStateSchemaV2
version: z.literal('3'), version: z.literal('3'),
currentBlockId: z.string().optional(), currentBlockId: z.string().optional(),
allowedOrigins: z.array(z.string()).optional(), allowedOrigins: z.array(z.string()).optional(),
setVariableIdsForHistory: z.array(z.string()).optional(),
currentSetVariableHistoryIndex: z.number().optional(),
previewMetadata: z
.object({
answers: z.array(answerSchema).optional(),
visitedEdges: z.array(z.string()).optional(),
setVariableHistory: z
.array(
setVariableHistoryItemSchema.pick({
blockId: true,
variableId: true,
value: true,
})
)
.optional(),
})
.optional(),
}) })
export type SessionState = z.infer<typeof sessionStateSchemaV3> export type SessionState = z.infer<typeof sessionStateSchemaV3>
@ -119,17 +131,27 @@ const migrateFromV1ToV2 = (
{ {
typebot: state.typebot, typebot: state.typebot,
resultId: state.result.id, resultId: state.result.id,
answers: state.result.answers.map((answer) => ({ answers: state.result.answers.map((answer) => {
key: let answerVariableId: string | undefined
(answer.variableId state.typebot.groups.forEach((group) => {
? state.typebot.variables.find( group.blocks.forEach((block) => {
(variable) => variable.id === answer.variableId if (isInputBlock(block) && block.id === answer.blockId) {
)?.name answerVariableId = block.options?.variableId
: state.typebot.groups.find((group) => }
group.blocks.find((block) => block.id === answer.blockId) })
)?.title) ?? '', })
value: answer.content, return {
})), key:
(answerVariableId
? state.typebot.variables.find(
(variable) => variable.id === answerVariableId
)?.name
: state.typebot.groups.find((group) =>
group.blocks.find((block) => block.id === answer.blockId)
)?.title) ?? '',
value: answer.content,
}
}),
isMergingWithParent: true, isMergingWithParent: true,
edgeIdToTriggerWhenDone: edgeIdToTriggerWhenDone:
state.linkedTypebots.queue.length > 0 state.linkedTypebots.queue.length > 0
@ -141,17 +163,27 @@ const migrateFromV1ToV2 = (
({ ({
typebot, typebot,
resultId: state.result.id, resultId: state.result.id,
answers: state.result.answers.map((answer) => ({ answers: state.result.answers.map((answer) => {
key: let answerVariableId: string | undefined
(answer.variableId typebot.groups.forEach((group) => {
? state.typebot.variables.find( group.blocks.forEach((block) => {
(variable) => variable.id === answer.variableId if (isInputBlock(block) && block.id === answer.blockId) {
)?.name answerVariableId = block.options?.variableId
: state.typebot.groups.find((group) => }
group.blocks.find((block) => block.id === answer.blockId) })
)?.title) ?? '', })
value: answer.content, return {
})), key:
(answerVariableId
? state.typebot.variables.find(
(variable) => variable.id === answerVariableId
)?.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) edgeIdToTriggerWhenDone: state.linkedTypebots.queue.at(index + 1)
?.edgeId, ?.edgeId,
} satisfies SessionState['typebotsQueue'][number]) } satisfies SessionState['typebotsQueue'][number])

View File

@ -1,9 +1,10 @@
import { z } from '../zod' import { z } from '../zod'
import { answerInputSchema, answerSchema } from './answer' import { answerInputSchema, answerSchema } from './answer'
import { variableWithValueSchema } from './typebot/variable' import { listVariableValue, variableWithValueSchema } from './typebot/variable'
import { import {
Result as ResultPrisma, Result as ResultPrisma,
Log as LogPrisma, Log as LogPrisma,
SetVariableHistoryItem as SetVariableHistoryItemPrisma,
VisitedEdge, VisitedEdge,
} from '@typebot.io/prisma' } from '@typebot.io/prisma'
import { InputBlockType } from './blocks/inputs/constants' import { InputBlockType } from './blocks/inputs/constants'
@ -16,6 +17,7 @@ export const resultSchema = z.object({
isCompleted: z.boolean(), isCompleted: z.boolean(),
hasStarted: z.boolean().nullable(), hasStarted: z.boolean().nullable(),
isArchived: z.boolean().nullable(), isArchived: z.boolean().nullable(),
lastChatSessionId: z.string().nullable(),
}) satisfies z.ZodType<ResultPrisma> }) satisfies z.ZodType<ResultPrisma>
export const resultWithAnswersSchema = resultSchema.merge( export const resultWithAnswersSchema = resultSchema.merge(
@ -78,3 +80,14 @@ export type CellValueType = { element?: JSX.Element; plainText: string }
export type TableData = { export type TableData = {
id: Pick<CellValueType, 'plainText'> id: Pick<CellValueType, 'plainText'>
} & Record<string, CellValueType> } & Record<string, CellValueType>
export const setVariableHistoryItemSchema = z.object({
resultId: z.string(),
index: z.number(),
blockId: z.string(),
variableId: z.string(),
value: z.string().or(listVariableValue).nullable(),
}) satisfies z.ZodType<SetVariableHistoryItemPrisma>
export type SetVariableHistoryItem = z.infer<
typeof setVariableHistoryItemSchema
>

View File

@ -5,12 +5,13 @@ import cliProgress from 'cli-progress'
import { writeFileSync } from 'fs' import { writeFileSync } from 'fs'
import { import {
ResultWithAnswers, ResultWithAnswers,
Typebot, TypebotV6,
resultWithAnswersSchema, resultWithAnswersSchema,
} from '@typebot.io/schemas' } from '@typebot.io/schemas'
import { byId } from '@typebot.io/lib' import { byId } from '@typebot.io/lib'
import { parseResultHeader } from '@typebot.io/results/parseResultHeader' import { parseResultHeader } from '@typebot.io/results/parseResultHeader'
import { convertResultsToTableData } from '@typebot.io/results/convertResultsToTableData' import { convertResultsToTableData } from '@typebot.io/results/convertResultsToTableData'
import { parseBlockIdVariableIdMap } from '@typebot.io/results/parseBlockIdVariableIdMap'
import { parseColumnsOrder } from '@typebot.io/results/parseColumnsOrder' import { parseColumnsOrder } from '@typebot.io/results/parseColumnsOrder'
import { parseUniqueKey } from '@typebot.io/lib/parseUniqueKey' import { parseUniqueKey } from '@typebot.io/lib/parseUniqueKey'
import { unparse } from 'papaparse' import { unparse } from 'papaparse'
@ -39,7 +40,7 @@ const exportResults = async () => {
where: { where: {
id: typebotId, id: typebotId,
}, },
})) as Typebot | null })) as TypebotV6 | null
if (!typebot) { if (!typebot) {
console.log('No typebot found') console.log('No typebot found')
@ -61,19 +62,34 @@ const exportResults = async () => {
for (let skip = 0; skip < totalResultsToExport; skip += 50) { for (let skip = 0; skip < totalResultsToExport; skip += 50) {
results.push( results.push(
...z.array(resultWithAnswersSchema).parse( ...z.array(resultWithAnswersSchema).parse(
await prisma.result.findMany({ (
take: 50, await prisma.result.findMany({
skip, take: 50,
where: { skip,
typebotId, where: {
hasStarted: true, typebotId,
isArchived: false, hasStarted: true,
}, isArchived: false,
orderBy: { },
createdAt: 'desc', orderBy: {
}, createdAt: 'desc',
include: { answers: true }, },
}) include: {
answers: {
select: {
content: true,
blockId: true,
},
},
answersV2: {
select: {
content: true,
blockId: true,
},
},
},
})
).map((r) => ({ ...r, answers: r.answersV2.concat(r.answers) }))
) )
) )
progressBar.increment(50) progressBar.increment(50)
@ -85,7 +101,11 @@ const exportResults = async () => {
const resultHeader = parseResultHeader(typebot, []) const resultHeader = parseResultHeader(typebot, [])
const dataToUnparse = convertResultsToTableData(results, resultHeader) const dataToUnparse = convertResultsToTableData({
results,
headerCells: resultHeader,
blockIdVariableIdMap: parseBlockIdVariableIdMap(typebot?.groups),
})
const headerIds = parseColumnsOrder( const headerIds = parseColumnsOrder(
typebot?.resultsTablePreferences?.columnsOrder, typebot?.resultsTablePreferences?.columnsOrder,

View File

@ -4,6 +4,7 @@
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@typebot.io/lib": "workspace:*" "@typebot.io/lib": "workspace:*",
"@typebot.io/tsconfig": "workspace:*"
} }
} }

View File

@ -95,6 +95,7 @@ type VariableToParseInformation = {
endIndex: number endIndex: number
textToReplace: string textToReplace: string
value: string value: string
variableId?: string
} }
export const getVariablesToParseInfoInText = ( export const getVariablesToParseInfoInText = (
@ -146,6 +147,7 @@ export const getVariablesToParseInfoInText = (
? variable?.value[variable?.value.length - 1] ? variable?.value[variable?.value.length - 1]
: variable?.value : variable?.value
) ?? '', ) ?? '',
variableId: variable?.id,
}) })
}) })
return variablesToParseInfo.sort((a, b) => a.startIndex - b.startIndex) return variablesToParseInfo.sort((a, b) => a.startIndex - b.startIndex)

View File

@ -2,12 +2,13 @@ export type Variable = {
id: string id: string
name: string name: string
value?: string | (string | null)[] | null | undefined value?: string | (string | null)[] | null | undefined
isSessionVariable?: boolean
} }
export type VariableWithValue = Pick<Variable, 'id' | 'name'> & { export type VariableWithValue = Omit<Variable, 'value'> & {
value: string | (string | null)[] value: string | (string | null)[]
} }
export type VariableWithUnknowValue = Pick<Variable, 'id' | 'name'> & { export type VariableWithUnknowValue = Omit<Variable, 'value'> & {
value?: unknown value?: unknown
} }

Some files were not shown because too many files have changed in this diff Show More