@ -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,
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
@ -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,
|
|
||||||
})),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
@ -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,
|
||||||
})
|
})
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
@ -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">
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@ -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')
|
||||||
|
@ -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()
|
||||||
|
@ -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]
|
||||||
)
|
)
|
||||||
|
@ -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 (
|
||||||
<>
|
<>
|
||||||
|
@ -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>
|
)
|
||||||
)
|
}
|
||||||
|
@ -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 }
|
||||||
|
@ -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]
|
||||||
)
|
)
|
||||||
|
@ -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),
|
||||||
|
}),
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
@ -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,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@ -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({
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
339
apps/builder/src/test/assets/typebots/logic/setVariable2.json
Normal file
339
apps/builder/src/test/assets/typebots/logic/setVariable2.json
Normal 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
|
||||||
|
}
|
@ -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"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -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"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -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: [
|
||||||
{
|
{
|
||||||
|
@ -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 {
|
||||||
|
@ -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 {
|
||||||
|
@ -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,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
@ -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(
|
||||||
|
@ -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(
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
180
apps/viewer/src/test/assets/typebots/transcript.json
Normal file
180
apps/viewer/src/test/assets/typebots/transcript.json
Normal 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
|
||||||
|
}
|
34
apps/viewer/src/test/transcript.spec.ts
Normal file
34
apps/viewer/src/test/transcript.spec.ts
Normal 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()
|
||||||
|
})
|
@ -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,
|
||||||
})
|
})
|
||||||
|
@ -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'
|
||||||
),
|
),
|
||||||
|
@ -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,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
@ -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'
|
||||||
),
|
),
|
||||||
|
@ -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 =
|
||||||
|
@ -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
|
||||||
})
|
})
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
})
|
})
|
||||||
|
@ -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,
|
||||||
})
|
})
|
||||||
|
@ -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({
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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 =
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
@ -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",
|
||||||
|
@ -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 = (
|
||||||
|
@ -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,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
@ -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'>[]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
16
packages/bot-engine/queries/saveAnswer.ts
Normal file
16
packages/bot-engine/queries/saveAnswer.ts
Normal 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 }],
|
||||||
|
})
|
||||||
|
}
|
16
packages/bot-engine/queries/saveSetVariableHistoryItems.ts
Normal file
16
packages/bot-engine/queries/saveSetVariableHistoryItems.ts
Normal 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,
|
||||||
|
})
|
@ -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 }],
|
|
||||||
})
|
|
||||||
}
|
|
@ -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 },
|
||||||
})
|
})
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -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]
|
||||||
|
}
|
||||||
|
@ -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 = {
|
||||||
|
@ -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 {
|
||||||
|
@ -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 }
|
||||||
> => {
|
> => {
|
||||||
|
@ -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",
|
||||||
|
@ -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) {
|
||||||
|
@ -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 = {
|
||||||
|
@ -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'
|
||||||
|
@ -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",
|
||||||
|
@ -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",
|
||||||
|
397
packages/logic/computeResultTranscript.ts
Normal file
397
packages/logic/computeResultTranscript.ts
Normal 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 }
|
||||||
|
}
|
@ -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[]) =>
|
18
packages/logic/package.json
Normal file
18
packages/logic/package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
12
packages/logic/tsconfig.json
Normal file
12
packages/logic/tsconfig.json
Normal 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"]
|
||||||
|
}
|
||||||
|
}
|
@ -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',
|
|
||||||
})),
|
})),
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
@ -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 {
|
||||||
|
@ -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;
|
@ -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 {
|
||||||
|
@ -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 },
|
||||||
|
@ -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
|
||||||
|
19
packages/results/parseBlockIdVariableIdMap.ts
Normal file
19
packages/results/parseBlockIdVariableIdMap.ts
Normal 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
|
||||||
|
}
|
@ -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,
|
||||||
|
@ -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({
|
||||||
|
@ -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,
|
||||||
|
@ -21,6 +21,7 @@ const basicSetVariableOptionsSchema = baseOptions.extend({
|
|||||||
'Random ID',
|
'Random ID',
|
||||||
'Phone number',
|
'Phone number',
|
||||||
'Contact name',
|
'Contact name',
|
||||||
|
'Transcript',
|
||||||
]),
|
]),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
@ -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])
|
||||||
|
@ -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
|
||||||
|
>
|
||||||
|
@ -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,
|
||||||
|
@ -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:*"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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
Reference in New Issue
Block a user