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 { getTotalAnswers } from './getTotalAnswers'
import { getTotalVisitedEdges } from './getTotalVisitedEdges'
import { getStats } from './getStats'
import { getInDepthAnalyticsData } from './getInDepthAnalyticsData'
export const analyticsRouter = router({
getTotalAnswers,
getTotalVisitedEdges,
getInDepthAnalyticsData,
getStats,
})

View File

@ -5,8 +5,14 @@ import {
useDisclosure,
} from '@chakra-ui/react'
import { useTypebot } from '@/features/editor/providers/TypebotProvider'
import { Stats } from '@typebot.io/schemas'
import React from 'react'
import {
Edge,
GroupV6,
Stats,
TotalAnswers,
TotalVisitedEdges,
} from '@typebot.io/schemas'
import React, { useMemo } from 'react'
import { StatsCards } from './StatsCards'
import { ChangePlanModal } from '@/features/billing/components/ChangePlanModal'
import { Graph } from '@/features/graph/components/Graph'
@ -16,6 +22,7 @@ import { trpc } from '@/lib/trpc'
import { isDefined } from '@typebot.io/lib'
import { EventsCoordinatesProvider } from '@/features/graph/providers/EventsCoordinateProvider'
import { timeFilterValues } from '../constants'
import { blockHasItems, isInputBlock } from '@typebot.io/schemas/helpers'
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone
@ -33,7 +40,7 @@ export const AnalyticsGraphContainer = ({
const { t } = useTranslate()
const { isOpen, onOpen, onClose } = useDisclosure()
const { typebot, publishedTypebot } = useTypebot()
const { data } = trpc.analytics.getTotalAnswers.useQuery(
const { data } = trpc.analytics.getInDepthAnalyticsData.useQuery(
{
typebotId: typebot?.id as string,
timeFilter,
@ -42,14 +49,36 @@ export const AnalyticsGraphContainer = ({
{ enabled: isDefined(publishedTypebot) }
)
const { data: edgesData } = trpc.analytics.getTotalVisitedEdges.useQuery(
{
typebotId: typebot?.id as string,
timeFilter,
timeZone,
},
{ enabled: isDefined(publishedTypebot) }
)
const totalVisitedEdges = useMemo(() => {
if (
!publishedTypebot?.edges ||
!publishedTypebot.groups ||
!publishedTypebot.events ||
!data?.totalAnswers ||
!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 (
<Flex
@ -73,7 +102,7 @@ export const AnalyticsGraphContainer = ({
typebot={publishedTypebot}
onUnlockProPlanClick={onOpen}
totalAnswers={data?.totalAnswers}
totalVisitedEdges={edgesData?.totalVisitedEdges}
totalVisitedEdges={totalVisitedEdges}
/>
</EventsCoordinatesProvider>
</GraphProvider>
@ -102,3 +131,72 @@ export const AnalyticsGraphContainer = ({
</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 'Moment of the day':
case 'Environment name':
case 'Transcript':
case 'Yesterday': {
return (
<Text as="span">

View File

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

View File

@ -3,10 +3,11 @@ import { importTypebotInDatabase } from '@typebot.io/playwright/databaseActions'
import { createId } from '@paralleldrive/cuid2'
import { getTestAsset } from '@/test/utils/playwright'
const typebotId = createId()
test.describe.configure({ mode: 'parallel' })
test.describe('Set variable block', () => {
test('its configuration should work', async ({ page }) => {
const typebotId = createId()
await importTypebotInDatabase(
getTestAsset('typebots/logic/setVariable.json'),
{
@ -70,4 +71,47 @@ test.describe('Set variable block', () => {
page.locator('typebot-standard').locator('text=Addition: 366000')
).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('text=Jump to Group #2 in My link typebot 2')
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.locator('typebot-standard').locator('input').fill('Hello there!')
@ -53,7 +53,7 @@ test('should be configurable', async ({ page }) => {
).toBeVisible()
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.getByTestId('selected-item-label').first().click({ force: true })
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()
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 }) => {
const typebotId = createId()

View File

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

View File

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

View File

@ -22,7 +22,7 @@ import { FormEvent, useState } from 'react'
import { headerHeight } from '../../editor/constants'
import { useDrag } from '@use-gesture/react'
import { ResizeHandle } from './ResizeHandle'
import { Variable } from '@typebot.io/schemas'
import { InputBlock, SetVariableBlock, Variable } from '@typebot.io/schemas'
import {
CheckIcon,
MoreHorizontalIcon,
@ -32,6 +32,9 @@ import {
import { SwitchWithLabel } from '@/components/inputs/SwitchWithLabel'
import { isNotEmpty } from '@typebot.io/lib'
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 = {
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 (
<Flex
pos="absolute"
@ -132,6 +143,7 @@ export const VariablesDrawer = ({ onClose }: Props) => {
variable={variable}
onChange={(changes) => updateVariable(variable.id, changes)}
onDelete={() => deleteVariable(variable.id)}
setVariableAndInputBlocks={setVariableAndInputBlocks}
/>
))}
</Stack>
@ -144,58 +156,76 @@ const VariableItem = ({
variable,
onChange,
onDelete,
setVariableAndInputBlocks,
}: {
variable: Variable
onChange: (variable: Partial<Variable>) => void
onDelete: () => void
}) => (
<HStack justifyContent="space-between">
<Editable
defaultValue={variable.name}
onSubmit={(name) => onChange({ name })}
>
<EditablePreview
px="2"
noOfLines={1}
cursor="text"
_hover={{
bg: useColorModeValue('gray.100', 'gray.700'),
}}
/>
<EditableInput ml="1" pl="1" />
</Editable>
setVariableAndInputBlocks: (InputBlock | SetVariableBlock)[]
}) => {
const isSessionOnly = setVariableAndInputBlocks.some(
(b) =>
b.type === LogicBlockType.SET_VARIABLE &&
sessionOnlySetVariableOptions.includes(
b.options?.type as (typeof sessionOnlySetVariableOptions)[number]
)
)
<HStack>
<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>
const isLinkedToAnswer = setVariableAndInputBlocks.some(
(b) => isInputBlock(b) && b.options?.variableId === variable.id
)
return (
<HStack justifyContent="space-between">
<Editable
defaultValue={variable.name}
onSubmit={(name) => onChange({ name })}
>
<EditablePreview
px="2"
noOfLines={1}
cursor="text"
_hover={{
bg: useColorModeValue('gray.100', 'gray.700'),
}}
/>
<EditableInput ml="1" pl="1" />
</Editable>
<HStack>
{!isSessionOnly && !isLinkedToAnswer && (
<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
icon={<TrashIcon />}
onClick={onDelete}
aria-label="Delete"
size="sm"
/>
</Popover>
</HStack>
</HStack>
</HStack>
)
)
}

View File

@ -1,4 +1,5 @@
import { WebhookIcon } from '@/components/icons'
import { useUser } from '@/features/account/hooks/useUser'
import { useEditor } from '@/features/editor/providers/EditorProvider'
import { useTypebot } from '@/features/editor/providers/TypebotProvider'
import { useGraph } from '@/features/graph/providers/GraphProvider'
@ -7,6 +8,7 @@ import { Standard } from '@typebot.io/nextjs'
import { ContinueChatResponse } from '@typebot.io/schemas'
export const WebPreview = () => {
const { user } = useUser()
const { typebot } = useTypebot()
const { startPreviewAtGroup, startPreviewAtEvent } = useEditor()
const { setPreviewingBlock } = useGraph()
@ -40,6 +42,7 @@ export const WebPreview = () => {
<Standard
key={`web-preview${startPreviewAtGroup ?? ''}`}
typebot={typebot}
sessionId={user ? `${typebot.id}-${user.id}` : undefined}
startFrom={
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 { parseCellContent } from './helpers/parseCellContent'
import { timeFilterValues } from '../analytics/constants'
import { parseBlockIdVariableIdMap } from '@typebot.io/results/parseBlockIdVariableIdMap'
const resultsContext = createContext<{
resultsList: { results: ResultWithAnswers[] }[] | undefined
@ -97,11 +98,14 @@ export const ResultsProvider = ({
const tableData = useMemo(
() =>
publishedTypebot
? convertResultsToTableData(
data?.flatMap((d) => d.results) ?? [],
resultHeader,
parseCellContent
)
? convertResultsToTableData({
results: data?.flatMap((d) => d.results) ?? [],
headerCells: resultHeader,
cellParser: parseCellContent,
blockIdVariableIdMap: parseBlockIdVariableIdMap(
publishedTypebot.groups
),
})
: [],
[publishedTypebot, data, resultHeader]
)

View File

@ -71,11 +71,31 @@ export const getResult = authenticatedProcedure
orderBy: {
createdAt: 'desc',
},
include: { answers: true },
include: {
answers: {
select: {
blockId: true,
content: true,
},
},
answersV2: {
select: {
blockId: true,
content: true,
},
},
},
})
if (results.length === 0)
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: {
createdAt: 'desc',
},
include: { answers: true },
include: {
answers: {
select: {
blockId: true,
content: true,
},
},
answersV2: {
select: {
blockId: true,
content: true,
},
},
},
})
let nextCursor: typeof cursor | undefined
@ -114,7 +127,11 @@ export const getResults = authenticatedProcedure
}
return {
results: z.array(resultWithAnswersSchema).parse(results),
results: z
.array(resultWithAnswersSchema)
.parse(
results.map((r) => ({ ...r, answers: r.answersV2.concat(r.answers) }))
),
nextCursor,
}
})

View File

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

View File

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

View File

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

View File

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

View File

@ -4,6 +4,9 @@ import { Plan } from '@typebot.io/prisma'
import { Block, Typebot } from '@typebot.io/schemas'
import { IntegrationBlockType } from '@typebot.io/schemas/features/blocks/integrations/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 = (
settings: Typebot['settings'],
@ -160,3 +163,38 @@ export const sanitizeCustomDomain = async ({
})
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,
logs,
visitedEdges,
setVariableHistory,
} = await startSession({
version: 2,
message: undefined,
@ -145,6 +146,7 @@ export const startWhatsAppPreview = authenticatedProcedure
state: newSessionState,
},
visitedEdges,
setVariableHistory,
})
} else {
await restartSession({

View File

@ -1,101 +1,99 @@
{
"id": "cl0iecee90042961arm5kb0f0",
"createdAt": "2022-03-08T17:18:50.337Z",
"updatedAt": "2022-03-08T21:05:28.825Z",
"name": "Another typebot",
"folderId": null,
"groups": [
"version": "6",
"id": "qk6zz1ag2jnm3yny7fzqrbwn",
"name": "My link typebot 2",
"events": [
{
"id": "p4ByLVoKiDRyRoPHKmcTfw",
"blocks": [
{
"id": "rw6smEWEJzHKbiVKLUKFvZ",
"type": "start",
"label": "Start",
"groupId": "p4ByLVoKiDRyRoPHKmcTfw",
"outgoingEdgeId": "1z3pfiatTUHbraD2uSoA3E"
}
],
"title": "Start",
"graphCoordinates": { "x": 0, "y": 0 }
},
"outgoingEdgeId": "1z3pfiatTUHbraD2uSoA3E",
"graphCoordinates": { "x": 0, "y": 0 },
"type": "start"
}
],
"groups": [
{
"id": "bg4QEJseUsTP496H27j5k2",
"title": "Group #1",
"graphCoordinates": { "x": 366, "y": 191 },
"blocks": [
{
"id": "s8ZeBL9p5za77eBmdKECLYq",
"outgoingEdgeId": "aEBnubX4EMx4Cse6xPAR1m",
"type": "text input",
"groupId": "bg4QEJseUsTP496H27j5k2",
"options": {
"isLong": false,
"labels": { "button": "Send", "placeholder": "Type your answer..." }
},
"outgoingEdgeId": "aEBnubX4EMx4Cse6xPAR1m"
"labels": {
"placeholder": "Type your answer...",
"button": "Send"
},
"isLong": false
}
}
],
"title": "Group #1",
"graphCoordinates": { "x": 366, "y": 191 }
]
},
{
"id": "uhqCZSNbsYVFxop7Gc8xvn",
"title": "Group #2",
"graphCoordinates": { "x": 793, "y": 99 },
"blocks": [
{
"id": "smyHyeS6yaFaHHU44BNmN4n",
"type": "text",
"groupId": "uhqCZSNbsYVFxop7Gc8xvn",
"content": {
"richText": [
{ "type": "p", "children": [{ "text": "Second block" }] }
]
}
}
],
"title": "Group #2",
"graphCoordinates": { "x": 793, "y": 99 }
]
}
],
"variables": [],
"edges": [
{
"id": "1z3pfiatTUHbraD2uSoA3E",
"to": { "groupId": "bg4QEJseUsTP496H27j5k2" },
"from": {
"blockId": "rw6smEWEJzHKbiVKLUKFvZ",
"groupId": "p4ByLVoKiDRyRoPHKmcTfw"
}
"from": { "eventId": "p4ByLVoKiDRyRoPHKmcTfw" },
"to": { "groupId": "bg4QEJseUsTP496H27j5k2" }
},
{
"id": "aEBnubX4EMx4Cse6xPAR1m",
"to": { "groupId": "uhqCZSNbsYVFxop7Gc8xvn" },
"from": {
"blockId": "s8ZeBL9p5za77eBmdKECLYq",
"groupId": "bg4QEJseUsTP496H27j5k2"
}
"from": { "blockId": "s8ZeBL9p5za77eBmdKECLYq" },
"to": { "groupId": "uhqCZSNbsYVFxop7Gc8xvn" }
}
],
"variables": [],
"theme": {
"general": { "font": "Open Sans", "background": { "type": "None" } },
"chat": {
"hostBubbles": { "backgroundColor": "#F7F8FF", "color": "#303235" },
"guestBubbles": { "backgroundColor": "#FF8E21", "color": "#FFFFFF" },
"buttons": { "backgroundColor": "#0042DA", "color": "#FFFFFF" },
"inputs": {
"color": "#303235",
"backgroundColor": "#FFFFFF",
"color": "#303235",
"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": {
"general": {
"isBrandingEnabled": true,
"isNewResultOnRefreshEnabled": false
},
"typingEmulation": { "enabled": true, "speed": 300, "maxDelay": 1.5 },
"metadata": {
"description": "Build beautiful conversational forms and embed them directly in your applications without a line of code. Triple your response rate and collect answers that has more value compared to a traditional form."
},
"typingEmulation": { "speed": 300, "enabled": true, "maxDelay": 1.5 }
}
},
"createdAt": "2022-03-08T17:18:50.337Z",
"updatedAt": "2022-03-08T21:05:28.825Z",
"icon": null,
"folderId": 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",
"Random ID",
"Phone number",
"Contact name"
"Contact name",
"Transcript"
]
}
},
@ -2679,10 +2680,10 @@
}
}
},
"/v1/typebots/{typebotId}/analytics/totalAnswersInBlocks": {
"/v1/typebots/{typebotId}/analytics/inDepthData": {
"get": {
"operationId": "analytics-getTotalAnswers",
"summary": "List total answers in blocks",
"operationId": "analytics-getInDepthAnalyticsData",
"summary": "List total answers in blocks and off-default paths visited edges",
"tags": [
"Analytics"
],
@ -2753,123 +2754,8 @@
"total"
]
}
}
},
"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": {
},
"offDefaultPathVisitedEdges": {
"type": "array",
"items": {
"type": "object",
@ -2889,7 +2775,8 @@
}
},
"required": [
"totalVisitedEdges"
"totalAnswers",
"offDefaultPathVisitedEdges"
]
}
}
@ -5022,7 +4909,8 @@
"Result ID",
"Random ID",
"Phone number",
"Contact name"
"Contact name",
"Transcript"
]
}
},
@ -8480,7 +8368,8 @@
"Result ID",
"Random ID",
"Phone number",
"Contact name"
"Contact name",
"Transcript"
]
}
},
@ -11203,43 +11092,25 @@
"type": "boolean",
"nullable": true
},
"lastChatSessionId": {
"type": "string",
"nullable": true
},
"answers": {
"type": "array",
"items": {
"type": "object",
"properties": {
"createdAt": {
"type": "string"
},
"resultId": {
"type": "string"
},
"blockId": {
"type": "string"
},
"groupId": {
"type": "string"
},
"variableId": {
"type": "string",
"nullable": true
},
"content": {
"type": "string"
},
"storageUsed": {
"type": "number",
"nullable": true
}
},
"required": [
"createdAt",
"resultId",
"blockId",
"groupId",
"variableId",
"content",
"storageUsed"
"content"
]
}
}
@ -11252,6 +11123,7 @@
"isCompleted",
"hasStarted",
"isArchived",
"lastChatSessionId",
"answers"
]
}
@ -11515,43 +11387,25 @@
"type": "boolean",
"nullable": true
},
"lastChatSessionId": {
"type": "string",
"nullable": true
},
"answers": {
"type": "array",
"items": {
"type": "object",
"properties": {
"createdAt": {
"type": "string"
},
"resultId": {
"type": "string"
},
"blockId": {
"type": "string"
},
"groupId": {
"type": "string"
},
"variableId": {
"type": "string",
"nullable": true
},
"content": {
"type": "string"
},
"storageUsed": {
"type": "number",
"nullable": true
}
},
"required": [
"createdAt",
"resultId",
"blockId",
"groupId",
"variableId",
"content",
"storageUsed"
"content"
]
}
}
@ -11564,6 +11418,7 @@
"isCompleted",
"hasStarted",
"isArchived",
"lastChatSessionId",
"answers"
]
}
@ -17011,7 +16866,8 @@
"Result ID",
"Random ID",
"Phone number",
"Contact name"
"Contact name",
"Transcript"
]
}
},
@ -22665,7 +22521,8 @@
"Result ID",
"Random ID",
"Phone number",
"Contact name"
"Contact name",
"Transcript"
]
}
},
@ -25492,7 +25349,8 @@
"Result ID",
"Random ID",
"Phone number",
"Contact name"
"Contact name",
"Transcript"
]
}
},

View File

@ -1851,6 +1851,10 @@
"First name": "John",
"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",
"Random ID",
"Phone number",
"Contact name"
"Contact name",
"Transcript"
]
}
},
@ -7818,7 +7823,8 @@
"Result ID",
"Random ID",
"Phone number",
"Contact name"
"Contact name",
"Transcript"
]
}
},

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,38 +1,16 @@
import prisma from '@typebot.io/lib/prisma'
import { Answer } from '@typebot.io/prisma'
import { got } from 'got'
import { NextApiRequest, NextApiResponse } from 'next'
import { isNotDefined } from '@typebot.io/lib'
import { methodNotAllowed } from '@typebot.io/lib/api'
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
if (req.method === 'PUT') {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { uploadedFiles, ...answer } = (
typeof req.body === 'string' ? JSON.parse(req.body) : req.body
) as Answer & { uploadedFiles?: boolean }
let storageUsed = 0
if (uploadedFiles && answer.content.includes('http')) {
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 },
) as Answer & { uploadedFiles: string[] }
const result = await prisma.answer.createMany({
data: [{ ...answer }],
})
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()
})