From 40f21203b50e57c4cdd2b4cc70a6625d35b9cb45 Mon Sep 17 00:00:00 2001 From: Baptiste Arnaud Date: Wed, 15 May 2024 14:24:55 +0200 Subject: [PATCH] :sparkles: (setVariable) Add Transcription system var (#1507) Closes #1484 --- .../analytics/api/getInDepthAnalyticsData.ts | 137 ++++++ .../features/analytics/api/getTotalAnswers.ts | 78 ---- .../src/features/analytics/api/router.ts | 6 +- .../components/AnalyticsGraphContainer.tsx | 122 +++++- .../components/SetVariableContent.tsx | 1 + .../components/SetVariableSettings.tsx | 19 +- .../logic/setVariable/setVariable.spec.ts | 46 +- .../logic/typebotLink/typebotLink.spec.ts | 4 +- .../src/features/editor/editor.spec.ts | 52 --- .../src/features/editor/hooks/useUndo.ts | 12 +- .../graph/components/edges/DropOffEdge.tsx | 4 +- .../preview/components/VariablesDrawer.tsx | 118 ++++-- .../preview/components/WebPreview.tsx | 3 + .../src/features/results/ResultsProvider.tsx | 14 +- .../src/features/results/api/getResult.ts | 24 +- .../src/features/results/api/getResults.ts | 21 +- .../table/ExportAllResultsModal.tsx | 7 +- .../src/features/typebot/api/createTypebot.ts | 12 +- .../src/features/typebot/api/importTypebot.ts | 15 +- .../src/features/typebot/api/updateTypebot.ts | 17 +- .../features/typebot/helpers/sanitizers.ts | 38 ++ .../features/whatsapp/startWhatsAppPreview.ts | 2 + .../assets/typebots/logic/linkTypebots/2.json | 100 +++-- .../assets/typebots/logic/setVariable2.json | 339 +++++++++++++++ apps/docs/openapi/builder.json | 204 ++------- apps/docs/openapi/viewer.json | 10 +- apps/viewer/playwright.config.ts | 2 +- .../features/chat/api/legacy/sendMessageV1.ts | 4 + .../features/chat/api/legacy/sendMessageV2.ts | 4 + .../src/features/chat/api/startChatPreview.ts | 2 + .../blocks/[blockId]/executeWebhook.ts | 5 +- .../[typebotId]/integrations/email.tsx | 3 +- .../[typebotId]/results/[resultId]/answers.ts | 30 +- .../src/test/assets/typebots/transcript.json | 180 ++++++++ apps/viewer/src/test/transcript.spec.ts | 34 ++ packages/bot-engine/addEdgeToTypebot.ts | 2 +- .../bot-engine/apiHandlers/continueChat.ts | 2 + .../apiHandlers/getMessageStream.ts | 13 +- packages/bot-engine/apiHandlers/startChat.ts | 2 + .../apiHandlers/startChatPreview.ts | 6 + .../inputs/buttons/filterChoiceItems.ts | 7 +- ...injectVariableValuesInButtonsInputBlock.ts | 1 - .../pictureChoice/filterPictureChoiceItems.ts | 7 +- .../googleSheets/executeGoogleSheetBlock.ts | 1 + .../integrations/googleSheets/getRow.ts | 16 +- .../legacy/openai/audio/createSpeechOpenAI.ts | 16 +- .../openai/createChatCompletionOpenAI.ts | 9 +- .../legacy/openai/resumeChatCompletion.ts | 6 +- .../webhook/resumeWebhookExecution.ts | 9 +- .../zemanticAi/executeZemanticAiBlock.ts | 24 +- .../logic/condition/executeConditionBlock.ts | 6 +- .../blocks/logic/script/executeScript.ts | 17 +- .../logic/setVariable/executeSetVariable.ts | 151 ++++++- packages/bot-engine/continueBotFlow.ts | 140 +++--- packages/bot-engine/executeGroup.ts | 65 ++- .../bot-engine/forge/executeForgedBlock.ts | 14 +- packages/bot-engine/getFirstEdgeId.ts | 9 +- packages/bot-engine/getNextGroup.ts | 218 +++++----- packages/bot-engine/package.json | 1 + packages/bot-engine/parseBubbleBlock.ts | 31 +- packages/bot-engine/queries/createSession.ts | 21 +- packages/bot-engine/queries/findResult.ts | 47 ++- packages/bot-engine/queries/saveAnswer.ts | 16 + .../queries/saveSetVariableHistoryItems.ts | 16 + packages/bot-engine/queries/upsertAnswer.ts | 34 -- packages/bot-engine/queries/upsertResult.ts | 58 ++- packages/bot-engine/saveStateToDatabase.ts | 36 +- packages/bot-engine/startBotFlow.ts | 25 +- packages/bot-engine/startSession.ts | 80 +++- packages/bot-engine/types.ts | 3 + .../bot-engine/whatsapp/resumeWhatsAppFlow.ts | 2 + .../whatsapp/startWhatsAppSession.ts | 2 + packages/embeds/js/package.json | 2 +- packages/embeds/js/src/components/Bot.tsx | 4 +- packages/embeds/js/src/constants.ts | 1 + .../embeds/js/src/queries/startChatQuery.ts | 3 + packages/embeds/nextjs/package.json | 2 +- packages/embeds/react/package.json | 2 +- packages/logic/computeResultTranscript.ts | 397 ++++++++++++++++++ .../condition => logic}/executeCondition.ts | 21 +- packages/logic/package.json | 18 + packages/logic/tsconfig.json | 12 + packages/playwright/databaseActions.ts | 3 +- packages/prisma/mysql/schema.prisma | 66 ++- .../migration.sql | 47 +++ packages/prisma/postgresql/schema.prisma | 64 ++- packages/results/archiveResults.ts | 26 ++ packages/results/convertResultsToTableData.ts | 36 +- packages/results/parseBlockIdVariableIdMap.ts | 19 + packages/results/parseResultHeader.ts | 27 +- packages/schemas/features/answer.ts | 17 +- .../blocks/logic/setVariable/constants.ts | 3 + .../blocks/logic/setVariable/schema.ts | 1 + packages/schemas/features/chat/schema.ts | 6 + .../schemas/features/chat/sessionState.ts | 92 ++-- packages/schemas/features/result.ts | 15 +- packages/scripts/exportResults.ts | 52 ++- packages/variables/package.json | 3 +- packages/variables/parseVariables.ts | 2 + packages/variables/types.ts | 5 +- .../variables/updateVariablesInSession.ts | 105 ++++- pnpm-lock.yaml | 64 ++- 102 files changed, 2911 insertions(+), 986 deletions(-) create mode 100644 apps/builder/src/features/analytics/api/getInDepthAnalyticsData.ts delete mode 100644 apps/builder/src/features/analytics/api/getTotalAnswers.ts create mode 100644 apps/builder/src/test/assets/typebots/logic/setVariable2.json create mode 100644 apps/viewer/src/test/assets/typebots/transcript.json create mode 100644 apps/viewer/src/test/transcript.spec.ts create mode 100644 packages/bot-engine/queries/saveAnswer.ts create mode 100644 packages/bot-engine/queries/saveSetVariableHistoryItems.ts delete mode 100644 packages/bot-engine/queries/upsertAnswer.ts create mode 100644 packages/logic/computeResultTranscript.ts rename packages/{bot-engine/blocks/logic/condition => logic}/executeCondition.ts (94%) create mode 100644 packages/logic/package.json create mode 100644 packages/logic/tsconfig.json create mode 100644 packages/prisma/postgresql/migrations/20240515062409_add_answerv2_table/migration.sql create mode 100644 packages/results/parseBlockIdVariableIdMap.ts diff --git a/apps/builder/src/features/analytics/api/getInDepthAnalyticsData.ts b/apps/builder/src/features/analytics/api/getInDepthAnalyticsData.ts new file mode 100644 index 0000000000..280ffa9171 --- /dev/null +++ b/apps/builder/src/features/analytics/api/getInDepthAnalyticsData.ts @@ -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 + }>((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, + })), + } + } + ) diff --git a/apps/builder/src/features/analytics/api/getTotalAnswers.ts b/apps/builder/src/features/analytics/api/getTotalAnswers.ts deleted file mode 100644 index 99da05ee2c..0000000000 --- a/apps/builder/src/features/analytics/api/getTotalAnswers.ts +++ /dev/null @@ -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, - })), - } - } - ) diff --git a/apps/builder/src/features/analytics/api/router.ts b/apps/builder/src/features/analytics/api/router.ts index 937d9c1749..8b1252ef0b 100644 --- a/apps/builder/src/features/analytics/api/router.ts +++ b/apps/builder/src/features/analytics/api/router.ts @@ -1,10 +1,8 @@ import { router } from '@/helpers/server/trpc' -import { getTotalAnswers } from './getTotalAnswers' -import { getTotalVisitedEdges } from './getTotalVisitedEdges' import { getStats } from './getStats' +import { getInDepthAnalyticsData } from './getInDepthAnalyticsData' export const analyticsRouter = router({ - getTotalAnswers, - getTotalVisitedEdges, + getInDepthAnalyticsData, getStats, }) diff --git a/apps/builder/src/features/analytics/components/AnalyticsGraphContainer.tsx b/apps/builder/src/features/analytics/components/AnalyticsGraphContainer.tsx index bd21f1ad89..00957e20a3 100644 --- a/apps/builder/src/features/analytics/components/AnalyticsGraphContainer.tsx +++ b/apps/builder/src/features/analytics/components/AnalyticsGraphContainer.tsx @@ -5,8 +5,14 @@ import { useDisclosure, } from '@chakra-ui/react' import { useTypebot } from '@/features/editor/providers/TypebotProvider' -import { Stats } from '@typebot.io/schemas' -import React from 'react' +import { + Edge, + GroupV6, + Stats, + TotalAnswers, + TotalVisitedEdges, +} from '@typebot.io/schemas' +import React, { useMemo } from 'react' import { StatsCards } from './StatsCards' import { ChangePlanModal } from '@/features/billing/components/ChangePlanModal' import { Graph } from '@/features/graph/components/Graph' @@ -16,6 +22,7 @@ import { trpc } from '@/lib/trpc' import { isDefined } from '@typebot.io/lib' import { EventsCoordinatesProvider } from '@/features/graph/providers/EventsCoordinateProvider' import { timeFilterValues } from '../constants' +import { blockHasItems, isInputBlock } from '@typebot.io/schemas/helpers' const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone @@ -33,7 +40,7 @@ export const AnalyticsGraphContainer = ({ const { t } = useTranslate() const { isOpen, onOpen, onClose } = useDisclosure() const { typebot, publishedTypebot } = useTypebot() - const { data } = trpc.analytics.getTotalAnswers.useQuery( + const { data } = trpc.analytics.getInDepthAnalyticsData.useQuery( { typebotId: typebot?.id as string, timeFilter, @@ -42,14 +49,36 @@ export const AnalyticsGraphContainer = ({ { enabled: isDefined(publishedTypebot) } ) - const { data: edgesData } = trpc.analytics.getTotalVisitedEdges.useQuery( - { - typebotId: typebot?.id as string, - timeFilter, - timeZone, - }, - { enabled: isDefined(publishedTypebot) } - ) + const totalVisitedEdges = useMemo(() => { + if ( + !publishedTypebot?.edges || + !publishedTypebot.groups || + !publishedTypebot.events || + !data?.totalAnswers || + !stats?.totalViews + ) + return + const firstEdgeId = publishedTypebot.events[0].outgoingEdgeId + if (!firstEdgeId) return + return populateEdgesWithVisitData({ + edgeId: firstEdgeId, + edges: publishedTypebot.edges, + groups: publishedTypebot.groups, + currentTotalUsers: stats.totalViews, + totalVisitedEdges: data.offDefaultPathVisitedEdges + ? [...data.offDefaultPathVisitedEdges] + : [], + totalAnswers: data.totalAnswers, + edgeVisitHistory: [], + }) + }, [ + data?.offDefaultPathVisitedEdges, + data?.totalAnswers, + publishedTypebot?.edges, + publishedTypebot?.groups, + publishedTypebot?.events, + stats?.totalViews, + ]) return ( @@ -102,3 +131,72 @@ export const AnalyticsGraphContainer = ({ ) } + +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 +} diff --git a/apps/builder/src/features/blocks/logic/setVariable/components/SetVariableContent.tsx b/apps/builder/src/features/blocks/logic/setVariable/components/SetVariableContent.tsx index 3c40f9cebb..6f1e126b77 100644 --- a/apps/builder/src/features/blocks/logic/setVariable/components/SetVariableContent.tsx +++ b/apps/builder/src/features/blocks/logic/setVariable/components/SetVariableContent.tsx @@ -71,6 +71,7 @@ const Expression = ({ case 'Result ID': case 'Moment of the day': case 'Environment name': + case 'Transcript': case 'Yesterday': { return ( diff --git a/apps/builder/src/features/blocks/logic/setVariable/components/SetVariableSettings.tsx b/apps/builder/src/features/blocks/logic/setVariable/components/SetVariableSettings.tsx index 7499ebb9b0..d2c736e5b7 100644 --- a/apps/builder/src/features/blocks/logic/setVariable/components/SetVariableSettings.tsx +++ b/apps/builder/src/features/blocks/logic/setVariable/components/SetVariableSettings.tsx @@ -8,11 +8,13 @@ import { WhatsAppLogo } from '@/components/logos/WhatsAppLogo' import { defaultSetVariableOptions, hiddenTypes, + sessionOnlySetVariableOptions, valueTypes, } from '@typebot.io/schemas/features/blocks/logic/setVariable/constants' import { TextInput } from '@/components/inputs' import { isDefined } from '@typebot.io/lib' import { useTypebot } from '@/features/editor/providers/TypebotProvider' +import { isInputBlock } from '@typebot.io/schemas/helpers' type Props = { options: SetVariableBlock['options'] @@ -48,6 +50,20 @@ export const SetVariableSettings = ({ options, onOptionsChange }: Props) => { }) } + const isSessionOnly = + options?.type && + sessionOnlySetVariableOptions.includes( + options.type as (typeof sessionOnlySetVariableOptions)[number] + ) + + const isLinkedToAnswer = + options?.variableId && + typebot?.groups.some((g) => + g.blocks.some( + (b) => isInputBlock(b) && b.options?.variableId === options.variableId + ) + ) + return ( @@ -80,7 +96,7 @@ export const SetVariableSettings = ({ options, onOptionsChange }: Props) => { /> - {selectedVariable && ( + {selectedVariable && !isSessionOnly && !isLinkedToAnswer && ( { test('its configuration should work', async ({ page }) => { + const typebotId = createId() await importTypebotInDatabase( getTestAsset('typebots/logic/setVariable.json'), { @@ -70,4 +71,47 @@ test.describe('Set variable block', () => { page.locator('typebot-standard').locator('text=Addition: 366000') ).toBeVisible() }) + + test('Transcription variable setting should work in preview', async ({ + page, + }) => { + const typebotId = createId() + await importTypebotInDatabase( + getTestAsset('typebots/logic/setVariable2.json'), + { + id: typebotId, + } + ) + + await page.goto(`/typebots/${typebotId}/edit`) + await page.getByText('Transcription =').click() + await expect(page.getByText('Save in results?')).toBeVisible() + await page.locator('input[type="text"]').click() + await page.getByRole('menuitem', { name: 'Transcript' }).click() + await expect(page.getByText('Save in results?')).toBeHidden() + await expect(page.getByText('System.Transcript')).toBeVisible() + + await page.getByRole('button', { name: 'Test' }).click() + await page.getByRole('button', { name: 'There is a bug 🐛' }).click() + await page.getByTestId('textarea').fill('Hello!!') + await page.getByTestId('input').getByRole('button').click() + await page + .locator('typebot-standard') + .getByRole('button', { name: 'Restart' }) + .click() + await page.getByRole('button', { name: 'I have a question 💭' }).click() + await page.getByTestId('textarea').fill('How are you?') + await page.getByTestId('input').getByRole('button').click() + await page.getByRole('button', { name: 'Transcription' }).click() + + await expect( + page.getByText('Assistant: "Hey friend 👋 How').first() + ).toBeVisible() + await expect( + page.getByText( + 'Assistant: "https://media0.giphy.com/media/rhgwg4qBu97ISgbfni/giphy-downsized.gif?cid=fe3852a3wimy48e55djt23j44uto7gdlu8ksytylafisvr0q&rid=giphy-downsized.gif&ct=g"' + ) + ).toBeVisible() + await expect(page.getByText('User: "How are you?"')).toBeVisible() + }) }) diff --git a/apps/builder/src/features/blocks/logic/typebotLink/typebotLink.spec.ts b/apps/builder/src/features/blocks/logic/typebotLink/typebotLink.spec.ts index fb83ba9c13..366d79a02b 100644 --- a/apps/builder/src/features/blocks/logic/typebotLink/typebotLink.spec.ts +++ b/apps/builder/src/features/blocks/logic/typebotLink/typebotLink.spec.ts @@ -43,7 +43,7 @@ test('should be configurable', async ({ page }) => { await page.click('[aria-label="Close"]') await page.click('text=Jump to Group #2 in My link typebot 2') await page.getByTestId('selected-item-label').nth(1).click({ force: true }) - await page.click('button >> text=Start') + await page.getByLabel('Clear').click() await page.click('text=Test') await page.locator('typebot-standard').locator('input').fill('Hello there!') @@ -53,7 +53,7 @@ test('should be configurable', async ({ page }) => { ).toBeVisible() await page.click('[aria-label="Close"]') - await page.click('text=Jump to Start in My link typebot 2') + await page.click('text=Jump in My link typebot 2') await page.waitForTimeout(1000) await page.getByTestId('selected-item-label').first().click({ force: true }) await page.click('button >> text=Current typebot') diff --git a/apps/builder/src/features/editor/editor.spec.ts b/apps/builder/src/features/editor/editor.spec.ts index 530fd9f9f5..23cad0ea2f 100644 --- a/apps/builder/src/features/editor/editor.spec.ts +++ b/apps/builder/src/features/editor/editor.spec.ts @@ -61,58 +61,6 @@ test('Edges connection should work', async ({ page }) => { const total = await page.locator('[data-testid="edge"]').count() expect(total).toBe(1) }) -test('Drag and drop blocks and items should work', async ({ page }) => { - const typebotId = createId() - await importTypebotInDatabase( - getTestAsset('typebots/editor/buttonsDnd.json'), - { - id: typebotId, - } - ) - - // Blocks dnd - await page.goto(`/typebots/${typebotId}/edit`) - await expect(page.locator('[data-testid="block"] >> nth=0')).toHaveText( - 'Hello!' - ) - await page.dragAndDrop('text=Hello', '[data-testid="block"] >> nth=2', { - targetPosition: { x: 100, y: 0 }, - }) - await expect(page.locator('[data-testid="block"] >> nth=1')).toHaveText( - 'Hello!' - ) - await page.dragAndDrop('text=Hello', 'text=Group #2') - await expect(page.locator('[data-testid="block"] >> nth=2')).toHaveText( - 'Hello!' - ) - - // Items dnd - await expect(page.locator('[data-testid="item"] >> nth=0')).toHaveText( - 'Item 1' - ) - await page.dragAndDrop('text=Item 1', 'text=Item 3') - await expect(page.locator('[data-testid="item"] >> nth=2')).toHaveText( - 'Item 1' - ) - await expect(page.locator('[data-testid="item"] >> nth=1')).toHaveText( - 'Item 3' - ) - await page.dragAndDrop('text=Item 3', 'text=Item 2-3') - await expect(page.locator('[data-testid="item"] >> nth=7')).toHaveText( - 'Item 3' - ) - - await expect(page.locator('[data-testid="item"] >> nth=2')).toHaveText( - 'Name=John' - ) - await page.dragAndDrop( - '[data-testid="item"] >> nth=2', - '[data-testid="item"] >> nth=3' - ) - await expect(page.locator('[data-testid="item"] >> nth=3')).toHaveText( - 'Name=John' - ) -}) test('Rename and icon change should work', async ({ page }) => { const typebotId = createId() diff --git a/apps/builder/src/features/editor/hooks/useUndo.ts b/apps/builder/src/features/editor/hooks/useUndo.ts index d7e24af3ad..7380bb03eb 100644 --- a/apps/builder/src/features/editor/hooks/useUndo.ts +++ b/apps/builder/src/features/editor/hooks/useUndo.ts @@ -110,10 +110,14 @@ export const useUndo = ( const setUpdateDate = useCallback( (updatedAt: Date) => { - set((current) => ({ - ...current, - updatedAt, - })) + set((current) => + current + ? { + ...current, + updatedAt, + } + : current + ) }, [set] ) diff --git a/apps/builder/src/features/graph/components/edges/DropOffEdge.tsx b/apps/builder/src/features/graph/components/edges/DropOffEdge.tsx index a95ad47459..2d571b937b 100644 --- a/apps/builder/src/features/graph/components/edges/DropOffEdge.tsx +++ b/apps/builder/src/features/graph/components/edges/DropOffEdge.tsx @@ -18,7 +18,7 @@ import { TotalVisitedEdges, } from '@typebot.io/schemas/features/analytics' import { computeTotalUsersAtBlock } from '@/features/analytics/helpers/computeTotalUsersAtBlock' -import { byId } from '@typebot.io/lib' +import { byId, isNotDefined } from '@typebot.io/lib' import { blockHasItems } from '@typebot.io/schemas/helpers' import { groupWidth } from '../../constants' import { getTotalAnswersAtBlock } from '@/features/analytics/helpers/getTotalAnswersAtBlock' @@ -130,7 +130,7 @@ export const DropOffEdge = ({ return lastBlock?.id === currentBlockId }, [publishedTypebot, currentBlockId]) - if (!endpointCoordinates) return null + if (!endpointCoordinates || isNotDefined(dropOffRate)) return null return ( <> diff --git a/apps/builder/src/features/preview/components/VariablesDrawer.tsx b/apps/builder/src/features/preview/components/VariablesDrawer.tsx index 2637a91cc5..2f7bef1f9e 100644 --- a/apps/builder/src/features/preview/components/VariablesDrawer.tsx +++ b/apps/builder/src/features/preview/components/VariablesDrawer.tsx @@ -22,7 +22,7 @@ import { FormEvent, useState } from 'react' import { headerHeight } from '../../editor/constants' import { useDrag } from '@use-gesture/react' import { ResizeHandle } from './ResizeHandle' -import { Variable } from '@typebot.io/schemas' +import { InputBlock, SetVariableBlock, Variable } from '@typebot.io/schemas' import { CheckIcon, MoreHorizontalIcon, @@ -32,6 +32,9 @@ import { import { SwitchWithLabel } from '@/components/inputs/SwitchWithLabel' import { isNotEmpty } from '@typebot.io/lib' import { createId } from '@paralleldrive/cuid2' +import { isInputBlock } from '@typebot.io/schemas/helpers' +import { LogicBlockType } from '@typebot.io/schemas/features/blocks/logic/constants' +import { sessionOnlySetVariableOptions } from '@typebot.io/schemas/features/blocks/logic/setVariable/constants' type Props = { onClose: () => void @@ -70,6 +73,14 @@ export const VariablesDrawer = ({ onClose }: Props) => { }) } + const setVariableAndInputBlocks = + typebot?.groups.flatMap( + (g) => + g.blocks.filter( + (b) => b.type === LogicBlockType.SET_VARIABLE || isInputBlock(b) + ) as (InputBlock | SetVariableBlock)[] + ) ?? [] + return ( { variable={variable} onChange={(changes) => updateVariable(variable.id, changes)} onDelete={() => deleteVariable(variable.id)} + setVariableAndInputBlocks={setVariableAndInputBlocks} /> ))} @@ -144,58 +156,76 @@ const VariableItem = ({ variable, onChange, onDelete, + setVariableAndInputBlocks, }: { variable: Variable onChange: (variable: Partial) => void onDelete: () => void -}) => ( - - onChange({ name })} - > - - - + setVariableAndInputBlocks: (InputBlock | SetVariableBlock)[] +}) => { + const isSessionOnly = setVariableAndInputBlocks.some( + (b) => + b.type === LogicBlockType.SET_VARIABLE && + sessionOnlySetVariableOptions.includes( + b.options?.type as (typeof sessionOnlySetVariableOptions)[number] + ) + ) - - - - } - aria-label={'Settings'} - size="sm" - /> - - - - - onChange({ - ...variable, - isSessionVariable: !variable.isSessionVariable, - }) - } - /> - - + const isLinkedToAnswer = setVariableAndInputBlocks.some( + (b) => isInputBlock(b) && b.options?.variableId === variable.id + ) + + return ( + + onChange({ name })} + > + + + + + + {!isSessionOnly && !isLinkedToAnswer && ( + + + } + aria-label={'Settings'} + size="sm" + /> + + + + + onChange({ + ...variable, + isSessionVariable: !variable.isSessionVariable, + }) + } + /> + + + + )} } onClick={onDelete} aria-label="Delete" size="sm" /> - + - -) + ) +} diff --git a/apps/builder/src/features/preview/components/WebPreview.tsx b/apps/builder/src/features/preview/components/WebPreview.tsx index a2e975e7d3..4d8cb98967 100644 --- a/apps/builder/src/features/preview/components/WebPreview.tsx +++ b/apps/builder/src/features/preview/components/WebPreview.tsx @@ -1,4 +1,5 @@ import { WebhookIcon } from '@/components/icons' +import { useUser } from '@/features/account/hooks/useUser' import { useEditor } from '@/features/editor/providers/EditorProvider' import { useTypebot } from '@/features/editor/providers/TypebotProvider' import { useGraph } from '@/features/graph/providers/GraphProvider' @@ -7,6 +8,7 @@ import { Standard } from '@typebot.io/nextjs' import { ContinueChatResponse } from '@typebot.io/schemas' export const WebPreview = () => { + const { user } = useUser() const { typebot } = useTypebot() const { startPreviewAtGroup, startPreviewAtEvent } = useEditor() const { setPreviewingBlock } = useGraph() @@ -40,6 +42,7 @@ export const WebPreview = () => { publishedTypebot - ? convertResultsToTableData( - data?.flatMap((d) => d.results) ?? [], - resultHeader, - parseCellContent - ) + ? convertResultsToTableData({ + results: data?.flatMap((d) => d.results) ?? [], + headerCells: resultHeader, + cellParser: parseCellContent, + blockIdVariableIdMap: parseBlockIdVariableIdMap( + publishedTypebot.groups + ), + }) : [], [publishedTypebot, data, resultHeader] ) diff --git a/apps/builder/src/features/results/api/getResult.ts b/apps/builder/src/features/results/api/getResult.ts index fd0bcedb71..2114007681 100644 --- a/apps/builder/src/features/results/api/getResult.ts +++ b/apps/builder/src/features/results/api/getResult.ts @@ -71,11 +71,31 @@ export const getResult = authenticatedProcedure orderBy: { createdAt: 'desc', }, - include: { answers: true }, + include: { + answers: { + select: { + blockId: true, + content: true, + }, + }, + answersV2: { + select: { + blockId: true, + content: true, + }, + }, + }, }) if (results.length === 0) throw new TRPCError({ code: 'NOT_FOUND', message: 'Result not found' }) - return { result: resultWithAnswersSchema.parse(results[0]) } + const { answers, answersV2, ...result } = results[0] + + return { + result: resultWithAnswersSchema.parse({ + ...result, + answers: answers.concat(answersV2), + }), + } }) diff --git a/apps/builder/src/features/results/api/getResults.ts b/apps/builder/src/features/results/api/getResults.ts index 301dbb9caf..de7e336343 100644 --- a/apps/builder/src/features/results/api/getResults.ts +++ b/apps/builder/src/features/results/api/getResults.ts @@ -104,7 +104,20 @@ export const getResults = authenticatedProcedure orderBy: { createdAt: 'desc', }, - include: { answers: true }, + include: { + answers: { + select: { + blockId: true, + content: true, + }, + }, + answersV2: { + select: { + blockId: true, + content: true, + }, + }, + }, }) let nextCursor: typeof cursor | undefined @@ -114,7 +127,11 @@ export const getResults = authenticatedProcedure } return { - results: z.array(resultWithAnswersSchema).parse(results), + results: z + .array(resultWithAnswersSchema) + .parse( + results.map((r) => ({ ...r, answers: r.answersV2.concat(r.answers) })) + ), nextCursor, } }) diff --git a/apps/builder/src/features/results/components/table/ExportAllResultsModal.tsx b/apps/builder/src/features/results/components/table/ExportAllResultsModal.tsx index e11ef76cbe..012b70275d 100644 --- a/apps/builder/src/features/results/components/table/ExportAllResultsModal.tsx +++ b/apps/builder/src/features/results/components/table/ExportAllResultsModal.tsx @@ -27,6 +27,7 @@ import { parseUniqueKey } from '@typebot.io/lib/parseUniqueKey' import { useResults } from '../../ResultsProvider' import { byId, isDefined } from '@typebot.io/lib' import { Typebot } from '@typebot.io/schemas' +import { parseBlockIdVariableIdMap } from '@typebot.io/results/parseBlockIdVariableIdMap' type Props = { isOpen: boolean @@ -101,7 +102,11 @@ export const ExportAllResultsModal = ({ isOpen, onClose }: Props) => { ) : existingResultHeader - const dataToUnparse = convertResultsToTableData(results, resultHeader) + const dataToUnparse = convertResultsToTableData({ + results, + headerCells: resultHeader, + blockIdVariableIdMap: parseBlockIdVariableIdMap(typebot?.groups), + }) const headerIds = parseColumnsOrder( typebot?.resultsTablePreferences?.columnsOrder, diff --git a/apps/builder/src/features/typebot/api/createTypebot.ts b/apps/builder/src/features/typebot/api/createTypebot.ts index 6c1588dc2f..d45dd43a32 100644 --- a/apps/builder/src/features/typebot/api/createTypebot.ts +++ b/apps/builder/src/features/typebot/api/createTypebot.ts @@ -10,6 +10,7 @@ import { isPublicIdNotAvailable, sanitizeGroups, sanitizeSettings, + sanitizeVariables, } from '../helpers/sanitizers' import { createId } from '@paralleldrive/cuid2' import { EventType } from '@typebot.io/schemas/features/events/constants' @@ -92,6 +93,9 @@ export const createTypebot = authenticatedProcedure if (!existingFolder) typebot.folderId = null } + const groups = ( + typebot.groups ? await sanitizeGroups(workspaceId)(typebot.groups) : [] + ) as TypebotV6['groups'] const newTypebot = await prisma.typebot.create({ data: { version: '6', @@ -99,9 +103,7 @@ export const createTypebot = authenticatedProcedure name: typebot.name ?? 'My typebot', icon: typebot.icon, selectedThemeTemplateId: typebot.selectedThemeTemplateId, - groups: (typebot.groups - ? await sanitizeGroups(workspaceId)(typebot.groups) - : []) as TypebotV6['groups'], + groups, events: typebot.events ?? [ { type: EventType.START, @@ -118,7 +120,9 @@ export const createTypebot = authenticatedProcedure } : {}, folderId: typebot.folderId, - variables: typebot.variables ?? [], + variables: typebot.variables + ? sanitizeVariables({ variables: typebot.variables, groups }) + : [], edges: typebot.edges ?? [], resultsTablePreferences: typebot.resultsTablePreferences ?? undefined, publicId: typebot.publicId ?? undefined, diff --git a/apps/builder/src/features/typebot/api/importTypebot.ts b/apps/builder/src/features/typebot/api/importTypebot.ts index a56f2cd57c..02083752d2 100644 --- a/apps/builder/src/features/typebot/api/importTypebot.ts +++ b/apps/builder/src/features/typebot/api/importTypebot.ts @@ -15,6 +15,7 @@ import { sanitizeFolderId, sanitizeGroups, sanitizeSettings, + sanitizeVariables, } from '../helpers/sanitizers' import { preprocessTypebot } from '@typebot.io/schemas/features/typebot/helpers/preprocessTypebot' import { migrateTypebot } from '@typebot.io/migrations/migrateTypebot' @@ -122,6 +123,12 @@ export const importTypebot = authenticatedProcedure const migratedTypebot = await migrateImportingTypebot(typebot) + const groups = ( + migratedTypebot.groups + ? await sanitizeGroups(workspaceId)(migratedTypebot.groups) + : [] + ) as TypebotV6['groups'] + const newTypebot = await prisma.typebot.create({ data: { version: '6', @@ -129,9 +136,7 @@ export const importTypebot = authenticatedProcedure name: migratedTypebot.name, icon: migratedTypebot.icon, selectedThemeTemplateId: migratedTypebot.selectedThemeTemplateId, - groups: (migratedTypebot.groups - ? await sanitizeGroups(workspaceId)(migratedTypebot.groups) - : []) as TypebotV6['groups'], + groups, events: migratedTypebot.events ?? undefined, theme: migratedTypebot.theme ? migratedTypebot.theme : {}, settings: migratedTypebot.settings @@ -147,7 +152,9 @@ export const importTypebot = authenticatedProcedure folderId: migratedTypebot.folderId, workspaceId: workspace.id, }), - variables: migratedTypebot.variables ?? [], + variables: migratedTypebot.variables + ? sanitizeVariables({ variables: migratedTypebot.variables, groups }) + : [], edges: migratedTypebot.edges ?? [], resultsTablePreferences: migratedTypebot.resultsTablePreferences ?? undefined, diff --git a/apps/builder/src/features/typebot/api/updateTypebot.ts b/apps/builder/src/features/typebot/api/updateTypebot.ts index 0e104bf50d..f598711288 100644 --- a/apps/builder/src/features/typebot/api/updateTypebot.ts +++ b/apps/builder/src/features/typebot/api/updateTypebot.ts @@ -13,6 +13,7 @@ import { sanitizeCustomDomain, sanitizeGroups, sanitizeSettings, + sanitizeVariables, } from '../helpers/sanitizers' import { isWriteTypebotForbidden } from '../helpers/isWriteTypebotForbidden' import { isCloudProdInstance } from '@/helpers/isCloudProdInstance' @@ -156,6 +157,10 @@ export const updateTypebot = authenticatedProcedure }) } + const groups = typebot.groups + ? await sanitizeGroups(existingTypebot.workspace.id)(typebot.groups) + : undefined + const newTypebot = await prisma.typebot.update({ where: { id: existingTypebot.id, @@ -166,9 +171,7 @@ export const updateTypebot = authenticatedProcedure icon: typebot.icon, selectedThemeTemplateId: typebot.selectedThemeTemplateId, events: typebot.events ?? undefined, - groups: typebot.groups - ? await sanitizeGroups(existingTypebot.workspace.id)(typebot.groups) - : undefined, + groups, theme: typebot.theme ? typebot.theme : undefined, settings: typebot.settings ? sanitizeSettings( @@ -178,7 +181,13 @@ export const updateTypebot = authenticatedProcedure ) : undefined, folderId: typebot.folderId, - variables: typebot.variables, + variables: + typebot.variables && groups + ? sanitizeVariables({ + variables: typebot.variables, + groups, + }) + : undefined, edges: typebot.edges, resultsTablePreferences: typebot.resultsTablePreferences === null diff --git a/apps/builder/src/features/typebot/helpers/sanitizers.ts b/apps/builder/src/features/typebot/helpers/sanitizers.ts index cf42077b2f..10d6722654 100644 --- a/apps/builder/src/features/typebot/helpers/sanitizers.ts +++ b/apps/builder/src/features/typebot/helpers/sanitizers.ts @@ -4,6 +4,9 @@ import { Plan } from '@typebot.io/prisma' import { Block, Typebot } from '@typebot.io/schemas' import { IntegrationBlockType } from '@typebot.io/schemas/features/blocks/integrations/constants' import { defaultSendEmailOptions } from '@typebot.io/schemas/features/blocks/integrations/sendEmail/constants' +import { LogicBlockType } from '@typebot.io/schemas/features/blocks/logic/constants' +import { sessionOnlySetVariableOptions } from '@typebot.io/schemas/features/blocks/logic/setVariable/constants' +import { isInputBlock } from '@typebot.io/schemas/helpers' export const sanitizeSettings = ( settings: Typebot['settings'], @@ -160,3 +163,38 @@ export const sanitizeCustomDomain = async ({ }) return domainCount === 0 ? null : customDomain } + +export const sanitizeVariables = ({ + variables, + groups, +}: Pick): Typebot['variables'] => { + 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 + }) +} diff --git a/apps/builder/src/features/whatsapp/startWhatsAppPreview.ts b/apps/builder/src/features/whatsapp/startWhatsAppPreview.ts index 7256932de9..54e18ab7ea 100644 --- a/apps/builder/src/features/whatsapp/startWhatsAppPreview.ts +++ b/apps/builder/src/features/whatsapp/startWhatsAppPreview.ts @@ -105,6 +105,7 @@ export const startWhatsAppPreview = authenticatedProcedure clientSideActions, logs, visitedEdges, + setVariableHistory, } = await startSession({ version: 2, message: undefined, @@ -145,6 +146,7 @@ export const startWhatsAppPreview = authenticatedProcedure state: newSessionState, }, visitedEdges, + setVariableHistory, }) } else { await restartSession({ diff --git a/apps/builder/src/test/assets/typebots/logic/linkTypebots/2.json b/apps/builder/src/test/assets/typebots/logic/linkTypebots/2.json index 1fe457c73a..5abc3accba 100644 --- a/apps/builder/src/test/assets/typebots/logic/linkTypebots/2.json +++ b/apps/builder/src/test/assets/typebots/logic/linkTypebots/2.json @@ -1,101 +1,99 @@ { - "id": "cl0iecee90042961arm5kb0f0", - "createdAt": "2022-03-08T17:18:50.337Z", - "updatedAt": "2022-03-08T21:05:28.825Z", - "name": "Another typebot", - "folderId": null, - "groups": [ + "version": "6", + "id": "qk6zz1ag2jnm3yny7fzqrbwn", + "name": "My link typebot 2", + "events": [ { "id": "p4ByLVoKiDRyRoPHKmcTfw", - "blocks": [ - { - "id": "rw6smEWEJzHKbiVKLUKFvZ", - "type": "start", - "label": "Start", - "groupId": "p4ByLVoKiDRyRoPHKmcTfw", - "outgoingEdgeId": "1z3pfiatTUHbraD2uSoA3E" - } - ], - "title": "Start", - "graphCoordinates": { "x": 0, "y": 0 } - }, + "outgoingEdgeId": "1z3pfiatTUHbraD2uSoA3E", + "graphCoordinates": { "x": 0, "y": 0 }, + "type": "start" + } + ], + "groups": [ { "id": "bg4QEJseUsTP496H27j5k2", + "title": "Group #1", + "graphCoordinates": { "x": 366, "y": 191 }, "blocks": [ { "id": "s8ZeBL9p5za77eBmdKECLYq", + "outgoingEdgeId": "aEBnubX4EMx4Cse6xPAR1m", "type": "text input", - "groupId": "bg4QEJseUsTP496H27j5k2", "options": { - "isLong": false, - "labels": { "button": "Send", "placeholder": "Type your answer..." } - }, - "outgoingEdgeId": "aEBnubX4EMx4Cse6xPAR1m" + "labels": { + "placeholder": "Type your answer...", + "button": "Send" + }, + "isLong": false + } } - ], - "title": "Group #1", - "graphCoordinates": { "x": 366, "y": 191 } + ] }, { "id": "uhqCZSNbsYVFxop7Gc8xvn", + "title": "Group #2", + "graphCoordinates": { "x": 793, "y": 99 }, "blocks": [ { "id": "smyHyeS6yaFaHHU44BNmN4n", "type": "text", - "groupId": "uhqCZSNbsYVFxop7Gc8xvn", "content": { "richText": [ { "type": "p", "children": [{ "text": "Second block" }] } ] } } - ], - "title": "Group #2", - "graphCoordinates": { "x": 793, "y": 99 } + ] } ], - "variables": [], "edges": [ { "id": "1z3pfiatTUHbraD2uSoA3E", - "to": { "groupId": "bg4QEJseUsTP496H27j5k2" }, - "from": { - "blockId": "rw6smEWEJzHKbiVKLUKFvZ", - "groupId": "p4ByLVoKiDRyRoPHKmcTfw" - } + "from": { "eventId": "p4ByLVoKiDRyRoPHKmcTfw" }, + "to": { "groupId": "bg4QEJseUsTP496H27j5k2" } }, { "id": "aEBnubX4EMx4Cse6xPAR1m", - "to": { "groupId": "uhqCZSNbsYVFxop7Gc8xvn" }, - "from": { - "blockId": "s8ZeBL9p5za77eBmdKECLYq", - "groupId": "bg4QEJseUsTP496H27j5k2" - } + "from": { "blockId": "s8ZeBL9p5za77eBmdKECLYq" }, + "to": { "groupId": "uhqCZSNbsYVFxop7Gc8xvn" } } ], + "variables": [], "theme": { + "general": { "font": "Open Sans", "background": { "type": "None" } }, "chat": { + "hostBubbles": { "backgroundColor": "#F7F8FF", "color": "#303235" }, + "guestBubbles": { "backgroundColor": "#FF8E21", "color": "#FFFFFF" }, + "buttons": { "backgroundColor": "#0042DA", "color": "#FFFFFF" }, "inputs": { - "color": "#303235", "backgroundColor": "#FFFFFF", + "color": "#303235", "placeholderColor": "#9095A0" - }, - "buttons": { "color": "#FFFFFF", "backgroundColor": "#0042DA" }, - "hostBubbles": { "color": "#303235", "backgroundColor": "#F7F8FF" }, - "guestBubbles": { "color": "#FFFFFF", "backgroundColor": "#FF8E21" } - }, - "general": { "font": "Open Sans", "background": { "type": "None" } } + } + } }, + "selectedThemeTemplateId": null, "settings": { "general": { "isBrandingEnabled": true, "isNewResultOnRefreshEnabled": false }, + "typingEmulation": { "enabled": true, "speed": 300, "maxDelay": 1.5 }, "metadata": { "description": "Build beautiful conversational forms and embed them directly in your applications without a line of code. Triple your response rate and collect answers that has more value compared to a traditional form." - }, - "typingEmulation": { "speed": 300, "enabled": true, "maxDelay": 1.5 } + } }, + "createdAt": "2022-03-08T17:18:50.337Z", + "updatedAt": "2022-03-08T21:05:28.825Z", + "icon": null, + "folderId": null, "publicId": null, - "customDomain": null + "customDomain": null, + "workspaceId": "proWorkspace", + "resultsTablePreferences": null, + "isArchived": false, + "isClosed": false, + "whatsAppCredentialsId": null, + "riskLevel": null } diff --git a/apps/builder/src/test/assets/typebots/logic/setVariable2.json b/apps/builder/src/test/assets/typebots/logic/setVariable2.json new file mode 100644 index 0000000000..0b7dd2fedb --- /dev/null +++ b/apps/builder/src/test/assets/typebots/logic/setVariable2.json @@ -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 +} diff --git a/apps/docs/openapi/builder.json b/apps/docs/openapi/builder.json index 7617011a42..9e90dc2da6 100644 --- a/apps/docs/openapi/builder.json +++ b/apps/docs/openapi/builder.json @@ -587,7 +587,8 @@ "Result ID", "Random ID", "Phone number", - "Contact name" + "Contact name", + "Transcript" ] } }, @@ -2679,10 +2680,10 @@ } } }, - "/v1/typebots/{typebotId}/analytics/totalAnswersInBlocks": { + "/v1/typebots/{typebotId}/analytics/inDepthData": { "get": { - "operationId": "analytics-getTotalAnswers", - "summary": "List total answers in blocks", + "operationId": "analytics-getInDepthAnalyticsData", + "summary": "List total answers in blocks and off-default paths visited edges", "tags": [ "Analytics" ], @@ -2753,123 +2754,8 @@ "total" ] } - } - }, - "required": [ - "totalAnswers" - ] - } - } - } - }, - "400": { - "description": "Invalid input data", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/error.BAD_REQUEST" - } - } - } - }, - "401": { - "description": "Authorization not provided", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/error.UNAUTHORIZED" - } - } - } - }, - "403": { - "description": "Insufficient access", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/error.FORBIDDEN" - } - } - } - }, - "404": { - "description": "Not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/error.NOT_FOUND" - } - } - } - }, - "500": { - "description": "Internal server error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/error.INTERNAL_SERVER_ERROR" - } - } - } - } - } - } - }, - "/v1/typebots/{typebotId}/analytics/totalVisitedEdges": { - "get": { - "operationId": "analytics-getTotalVisitedEdges", - "summary": "List total edges used in results", - "tags": [ - "Analytics" - ], - "security": [ - { - "Authorization": [] - } - ], - "parameters": [ - { - "in": "path", - "name": "typebotId", - "schema": { - "type": "string" - }, - "required": true - }, - { - "in": "query", - "name": "timeFilter", - "schema": { - "type": "string", - "enum": [ - "today", - "last7Days", - "last30Days", - "monthToDate", - "lastMonth", - "yearToDate", - "allTime" - ], - "default": "last7Days" - } - }, - { - "in": "query", - "name": "timeZone", - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "Successful response", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "totalVisitedEdges": { + }, + "offDefaultPathVisitedEdges": { "type": "array", "items": { "type": "object", @@ -2889,7 +2775,8 @@ } }, "required": [ - "totalVisitedEdges" + "totalAnswers", + "offDefaultPathVisitedEdges" ] } } @@ -5022,7 +4909,8 @@ "Result ID", "Random ID", "Phone number", - "Contact name" + "Contact name", + "Transcript" ] } }, @@ -8480,7 +8368,8 @@ "Result ID", "Random ID", "Phone number", - "Contact name" + "Contact name", + "Transcript" ] } }, @@ -11203,43 +11092,25 @@ "type": "boolean", "nullable": true }, + "lastChatSessionId": { + "type": "string", + "nullable": true + }, "answers": { "type": "array", "items": { "type": "object", "properties": { - "createdAt": { - "type": "string" - }, - "resultId": { - "type": "string" - }, "blockId": { "type": "string" }, - "groupId": { - "type": "string" - }, - "variableId": { - "type": "string", - "nullable": true - }, "content": { "type": "string" - }, - "storageUsed": { - "type": "number", - "nullable": true } }, "required": [ - "createdAt", - "resultId", "blockId", - "groupId", - "variableId", - "content", - "storageUsed" + "content" ] } } @@ -11252,6 +11123,7 @@ "isCompleted", "hasStarted", "isArchived", + "lastChatSessionId", "answers" ] } @@ -11515,43 +11387,25 @@ "type": "boolean", "nullable": true }, + "lastChatSessionId": { + "type": "string", + "nullable": true + }, "answers": { "type": "array", "items": { "type": "object", "properties": { - "createdAt": { - "type": "string" - }, - "resultId": { - "type": "string" - }, "blockId": { "type": "string" }, - "groupId": { - "type": "string" - }, - "variableId": { - "type": "string", - "nullable": true - }, "content": { "type": "string" - }, - "storageUsed": { - "type": "number", - "nullable": true } }, "required": [ - "createdAt", - "resultId", "blockId", - "groupId", - "variableId", - "content", - "storageUsed" + "content" ] } } @@ -11564,6 +11418,7 @@ "isCompleted", "hasStarted", "isArchived", + "lastChatSessionId", "answers" ] } @@ -17011,7 +16866,8 @@ "Result ID", "Random ID", "Phone number", - "Contact name" + "Contact name", + "Transcript" ] } }, @@ -22665,7 +22521,8 @@ "Result ID", "Random ID", "Phone number", - "Contact name" + "Contact name", + "Transcript" ] } }, @@ -25492,7 +25349,8 @@ "Result ID", "Random ID", "Phone number", - "Contact name" + "Contact name", + "Transcript" ] } }, diff --git a/apps/docs/openapi/viewer.json b/apps/docs/openapi/viewer.json index 5941c0ef7d..941ebda642 100644 --- a/apps/docs/openapi/viewer.json +++ b/apps/docs/openapi/viewer.json @@ -1851,6 +1851,10 @@ "First name": "John", "Email": "john@gmail.com" } + }, + "sessionId": { + "type": "string", + "description": "If provided, will be used as the session ID and will overwrite any existing session with the same ID." } } } @@ -3675,7 +3679,8 @@ "Result ID", "Random ID", "Phone number", - "Contact name" + "Contact name", + "Transcript" ] } }, @@ -7818,7 +7823,8 @@ "Result ID", "Random ID", "Phone number", - "Contact name" + "Contact name", + "Transcript" ] } }, diff --git a/apps/viewer/playwright.config.ts b/apps/viewer/playwright.config.ts index f8eb434e32..8ba2ccb1c4 100644 --- a/apps/viewer/playwright.config.ts +++ b/apps/viewer/playwright.config.ts @@ -29,7 +29,7 @@ export default defineConfig({ use: { trace: 'on-first-retry', locale: 'en-US', - baseURL: process.env.NEXT_PUBLIC_VIEWER_URL, + baseURL: process.env.NEXT_PUBLIC_VIEWER_URL?.split(',')[0], }, projects: [ { diff --git a/apps/viewer/src/features/chat/api/legacy/sendMessageV1.ts b/apps/viewer/src/features/chat/api/legacy/sendMessageV1.ts index 1cec97cbb6..2d99e0d1d9 100644 --- a/apps/viewer/src/features/chat/api/legacy/sendMessageV1.ts +++ b/apps/viewer/src/features/chat/api/legacy/sendMessageV1.ts @@ -60,6 +60,7 @@ export const sendMessageV1 = publicProcedure clientSideActions, newSessionState, visitedEdges, + setVariableHistory, } = await startSession({ version: 1, startParams: @@ -136,6 +137,7 @@ export const sendMessageV1 = publicProcedure hasCustomEmbedBubble: messages.some( (message) => message.type === 'custom-embed' ), + setVariableHistory, }) return { @@ -176,6 +178,7 @@ export const sendMessageV1 = publicProcedure logs, lastMessageNewFormat, visitedEdges, + setVariableHistory, } = await continueBotFlow(message, { version: 1, state: session.state }) const allLogs = clientLogs ? [...(logs ?? []), ...clientLogs] : logs @@ -193,6 +196,7 @@ export const sendMessageV1 = publicProcedure hasCustomEmbedBubble: messages.some( (message) => message.type === 'custom-embed' ), + setVariableHistory, }) return { diff --git a/apps/viewer/src/features/chat/api/legacy/sendMessageV2.ts b/apps/viewer/src/features/chat/api/legacy/sendMessageV2.ts index ca4c386a49..2ef11b9c1c 100644 --- a/apps/viewer/src/features/chat/api/legacy/sendMessageV2.ts +++ b/apps/viewer/src/features/chat/api/legacy/sendMessageV2.ts @@ -60,6 +60,7 @@ export const sendMessageV2 = publicProcedure clientSideActions, newSessionState, visitedEdges, + setVariableHistory, } = await startSession({ version: 2, startParams: @@ -136,6 +137,7 @@ export const sendMessageV2 = publicProcedure hasCustomEmbedBubble: messages.some( (message) => message.type === 'custom-embed' ), + setVariableHistory, }) return { @@ -175,6 +177,7 @@ export const sendMessageV2 = publicProcedure logs, lastMessageNewFormat, visitedEdges, + setVariableHistory, } = await continueBotFlow(message, { version: 2, state: session.state }) const allLogs = clientLogs ? [...(logs ?? []), ...clientLogs] : logs @@ -192,6 +195,7 @@ export const sendMessageV2 = publicProcedure hasCustomEmbedBubble: messages.some( (message) => message.type === 'custom-embed' ), + setVariableHistory, }) return { diff --git a/apps/viewer/src/features/chat/api/startChatPreview.ts b/apps/viewer/src/features/chat/api/startChatPreview.ts index 991d5c42aa..f24873e392 100644 --- a/apps/viewer/src/features/chat/api/startChatPreview.ts +++ b/apps/viewer/src/features/chat/api/startChatPreview.ts @@ -27,6 +27,7 @@ export const startChatPreview = publicProcedure typebotId, typebot: startTypebot, prefilledVariables, + sessionId, }, ctx: { user }, }) => @@ -39,5 +40,6 @@ export const startChatPreview = publicProcedure typebot: startTypebot, userId: user?.id, prefilledVariables, + sessionId, }) ) diff --git a/apps/viewer/src/pages/api/typebots/[typebotId]/blocks/[blockId]/executeWebhook.ts b/apps/viewer/src/pages/api/typebots/[typebotId]/blocks/[blockId]/executeWebhook.ts index 27d099c774..8f20df407a 100644 --- a/apps/viewer/src/pages/api/typebots/[typebotId]/blocks/[blockId]/executeWebhook.ts +++ b/apps/viewer/src/pages/api/typebots/[typebotId]/blocks/[blockId]/executeWebhook.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ import { ResultValues, Typebot, @@ -36,7 +37,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { const { resultValues, variables, parentTypebotIds } = ( typeof req.body === 'string' ? JSON.parse(req.body) : req.body ) as { - resultValues: ResultValues | undefined + resultValues: ResultValues variables: Variable[] parentTypebotIds: string[] } @@ -76,7 +77,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { const linkedTypebots = [...linkedTypebotsParents, ...linkedTypebotsChildren] const answers = resultValues - ? resultValues.answers.map((answer) => ({ + ? resultValues.answers.map((answer: any) => ({ key: (answer.variableId ? typebot.variables.find( diff --git a/apps/viewer/src/pages/api/typebots/[typebotId]/integrations/email.tsx b/apps/viewer/src/pages/api/typebots/[typebotId]/integrations/email.tsx index d018bf3df8..bcfc7c16e6 100644 --- a/apps/viewer/src/pages/api/typebots/[typebotId]/integrations/email.tsx +++ b/apps/viewer/src/pages/api/typebots/[typebotId]/integrations/email.tsx @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ import { PublicTypebot, ResultValues, @@ -203,7 +204,7 @@ const getEmailBody = async ({ })) as unknown as PublicTypebot if (!typebot) return const answers = parseAnswers({ - answers: resultValues.answers.map((answer) => ({ + answers: (resultValues as any).answers.map((answer: any) => ({ key: (answer.variableId ? typebot.variables.find( diff --git a/apps/viewer/src/pages/api/typebots/[typebotId]/results/[resultId]/answers.ts b/apps/viewer/src/pages/api/typebots/[typebotId]/results/[resultId]/answers.ts index 4affeeb316..8ddd3e39a1 100644 --- a/apps/viewer/src/pages/api/typebots/[typebotId]/results/[resultId]/answers.ts +++ b/apps/viewer/src/pages/api/typebots/[typebotId]/results/[resultId]/answers.ts @@ -1,38 +1,16 @@ import prisma from '@typebot.io/lib/prisma' import { Answer } from '@typebot.io/prisma' -import { got } from 'got' import { NextApiRequest, NextApiResponse } from 'next' -import { isNotDefined } from '@typebot.io/lib' import { methodNotAllowed } from '@typebot.io/lib/api' const handler = async (req: NextApiRequest, res: NextApiResponse) => { if (req.method === 'PUT') { + // eslint-disable-next-line @typescript-eslint/no-unused-vars const { uploadedFiles, ...answer } = ( typeof req.body === 'string' ? JSON.parse(req.body) : req.body - ) as Answer & { uploadedFiles?: boolean } - let storageUsed = 0 - if (uploadedFiles && answer.content.includes('http')) { - const fileUrls = answer.content.split(', ') - const hasReachedStorageLimit = fileUrls[0] === null - if (!hasReachedStorageLimit) { - for (const url of fileUrls) { - const { headers } = await got(url) - const size = headers['content-length'] - if (isNotDefined(size)) return - storageUsed += parseInt(size, 10) - } - } - } - const result = await prisma.answer.upsert({ - where: { - resultId_blockId_groupId: { - resultId: answer.resultId, - groupId: answer.groupId, - blockId: answer.blockId, - }, - }, - create: { ...answer, storageUsed: storageUsed > 0 ? storageUsed : null }, - update: { ...answer, storageUsed: storageUsed > 0 ? storageUsed : null }, + ) as Answer & { uploadedFiles: string[] } + const result = await prisma.answer.createMany({ + data: [{ ...answer }], }) return res.send(result) } diff --git a/apps/viewer/src/test/assets/typebots/transcript.json b/apps/viewer/src/test/assets/typebots/transcript.json new file mode 100644 index 0000000000..e52fc6103b --- /dev/null +++ b/apps/viewer/src/test/assets/typebots/transcript.json @@ -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 +} diff --git a/apps/viewer/src/test/transcript.spec.ts b/apps/viewer/src/test/transcript.spec.ts new file mode 100644 index 0000000000..c9ae7882fd --- /dev/null +++ b/apps/viewer/src/test/transcript.spec.ts @@ -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() +}) diff --git a/packages/bot-engine/addEdgeToTypebot.ts b/packages/bot-engine/addEdgeToTypebot.ts index 83c12a5199..77c3dc1fbe 100644 --- a/packages/bot-engine/addEdgeToTypebot.ts +++ b/packages/bot-engine/addEdgeToTypebot.ts @@ -20,7 +20,7 @@ export const addEdgeToTypebot = ( }) export const createPortalEdge = ({ to }: Pick) => ({ - id: createId(), + id: 'virtual-' + createId(), from: { blockId: '', groupId: '' }, to, }) diff --git a/packages/bot-engine/apiHandlers/continueChat.ts b/packages/bot-engine/apiHandlers/continueChat.ts index 86aa41ba26..778c2ff06e 100644 --- a/packages/bot-engine/apiHandlers/continueChat.ts +++ b/packages/bot-engine/apiHandlers/continueChat.ts @@ -52,6 +52,7 @@ export const continueChat = async ({ origin, sessionId, message }: Props) => { logs, lastMessageNewFormat, visitedEdges, + setVariableHistory, } = await continueBotFlow(message, { version: 2, state: session.state, @@ -68,6 +69,7 @@ export const continueChat = async ({ origin, sessionId, message }: Props) => { logs, clientSideActions, visitedEdges, + setVariableHistory, hasCustomEmbedBubble: messages.some( (message) => message.type === 'custom-embed' ), diff --git a/packages/bot-engine/apiHandlers/getMessageStream.ts b/packages/bot-engine/apiHandlers/getMessageStream.ts index 57d5902004..a3edbaa3a2 100644 --- a/packages/bot-engine/apiHandlers/getMessageStream.ts +++ b/packages/bot-engine/apiHandlers/getMessageStream.ts @@ -16,6 +16,7 @@ import { isForgedBlockType } from '@typebot.io/schemas/features/blocks/forged/he import { updateVariablesInSession } from '@typebot.io/variables/updateVariablesInSession' import { updateSession } from '../queries/updateSession' import { deepParseVariables } from '@typebot.io/variables/deepParseVariables' +import { saveSetVariableHistoryItems } from '../queries/saveSetVariableHistoryItems' type Props = { sessionId: string @@ -114,11 +115,17 @@ export const getMessageStream = async ({ sessionId, messages }: Props) => { (variable) => variable.id === id ) 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({ id: session.id, - state: updateVariablesInSession(session.state)([ - { ...variable, value }, - ]), + state: updatedState, isReplying: undefined, }) }, diff --git a/packages/bot-engine/apiHandlers/startChat.ts b/packages/bot-engine/apiHandlers/startChat.ts index 2cd6bfc9ee..a7558fd064 100644 --- a/packages/bot-engine/apiHandlers/startChat.ts +++ b/packages/bot-engine/apiHandlers/startChat.ts @@ -33,6 +33,7 @@ export const startChat = async ({ clientSideActions, newSessionState, visitedEdges, + setVariableHistory, } = await startSession({ version: 2, startParams: { @@ -69,6 +70,7 @@ export const startChat = async ({ logs, clientSideActions, visitedEdges, + setVariableHistory, hasCustomEmbedBubble: messages.some( (message) => message.type === 'custom-embed' ), diff --git a/packages/bot-engine/apiHandlers/startChatPreview.ts b/packages/bot-engine/apiHandlers/startChatPreview.ts index 406b52ce63..82d2f65dbf 100644 --- a/packages/bot-engine/apiHandlers/startChatPreview.ts +++ b/packages/bot-engine/apiHandlers/startChatPreview.ts @@ -13,6 +13,7 @@ type Props = { typebot?: StartTypebot userId?: string prefilledVariables?: Record + sessionId?: string } export const startChatPreview = async ({ @@ -24,6 +25,7 @@ export const startChatPreview = async ({ typebot: startTypebot, userId, prefilledVariables, + sessionId, }: Props) => { const { typebot, @@ -34,6 +36,7 @@ export const startChatPreview = async ({ clientSideActions, newSessionState, visitedEdges, + setVariableHistory, } = await startSession({ version: 2, startParams: { @@ -45,6 +48,7 @@ export const startChatPreview = async ({ typebot: startTypebot, userId, prefilledVariables, + sessionId, }, message, }) @@ -61,9 +65,11 @@ export const startChatPreview = async ({ logs, clientSideActions, visitedEdges, + setVariableHistory, hasCustomEmbedBubble: messages.some( (message) => message.type === 'custom-embed' ), + initialSessionId: sessionId, }) const isEnded = diff --git a/packages/bot-engine/blocks/inputs/buttons/filterChoiceItems.ts b/packages/bot-engine/blocks/inputs/buttons/filterChoiceItems.ts index 7ad20984fb..b744afdf28 100644 --- a/packages/bot-engine/blocks/inputs/buttons/filterChoiceItems.ts +++ b/packages/bot-engine/blocks/inputs/buttons/filterChoiceItems.ts @@ -1,12 +1,15 @@ +import { executeCondition } from '@typebot.io/logic/executeCondition' import { ChoiceInputBlock, Variable } from '@typebot.io/schemas' -import { executeCondition } from '../../logic/condition/executeCondition' export const filterChoiceItems = (variables: Variable[]) => (block: ChoiceInputBlock): ChoiceInputBlock => { const filteredItems = block.items.filter((item) => { if (item.displayCondition?.isEnabled && item.displayCondition?.condition) - return executeCondition(variables)(item.displayCondition.condition) + return executeCondition({ + variables, + condition: item.displayCondition.condition, + }) return true }) diff --git a/packages/bot-engine/blocks/inputs/buttons/injectVariableValuesInButtonsInputBlock.ts b/packages/bot-engine/blocks/inputs/buttons/injectVariableValuesInButtonsInputBlock.ts index 63bb7b2790..b7288ae2ec 100644 --- a/packages/bot-engine/blocks/inputs/buttons/injectVariableValuesInButtonsInputBlock.ts +++ b/packages/bot-engine/blocks/inputs/buttons/injectVariableValuesInButtonsInputBlock.ts @@ -41,7 +41,6 @@ const getVariableValue = const [transformedVariable] = transformVariablesToList(variables)([ variable.id, ]) - updateVariablesInSession(state)([transformedVariable]) return transformedVariable.value as string[] } return variable.value diff --git a/packages/bot-engine/blocks/inputs/pictureChoice/filterPictureChoiceItems.ts b/packages/bot-engine/blocks/inputs/pictureChoice/filterPictureChoiceItems.ts index 704761a99b..9007c393c7 100644 --- a/packages/bot-engine/blocks/inputs/pictureChoice/filterPictureChoiceItems.ts +++ b/packages/bot-engine/blocks/inputs/pictureChoice/filterPictureChoiceItems.ts @@ -1,12 +1,15 @@ +import { executeCondition } from '@typebot.io/logic/executeCondition' import { PictureChoiceBlock, Variable } from '@typebot.io/schemas' -import { executeCondition } from '../../logic/condition/executeCondition' export const filterPictureChoiceItems = (variables: Variable[]) => (block: PictureChoiceBlock): PictureChoiceBlock => { const filteredItems = block.items.filter((item) => { if (item.displayCondition?.isEnabled && item.displayCondition?.condition) - return executeCondition(variables)(item.displayCondition.condition) + return executeCondition({ + variables, + condition: item.displayCondition.condition, + }) return true }) diff --git a/packages/bot-engine/blocks/integrations/googleSheets/executeGoogleSheetBlock.ts b/packages/bot-engine/blocks/integrations/googleSheets/executeGoogleSheetBlock.ts index a41b4cd800..42f0f91548 100644 --- a/packages/bot-engine/blocks/integrations/googleSheets/executeGoogleSheetBlock.ts +++ b/packages/bot-engine/blocks/integrations/googleSheets/executeGoogleSheetBlock.ts @@ -25,6 +25,7 @@ export const executeGoogleSheetBlock = async ( }) case GoogleSheetsAction.GET: return getRow(state, { + blockId: block.id, options: block.options, outgoingEdgeId: block.outgoingEdgeId, }) diff --git a/packages/bot-engine/blocks/integrations/googleSheets/getRow.ts b/packages/bot-engine/blocks/integrations/googleSheets/getRow.ts index 6783784c8c..c6f610dd0b 100644 --- a/packages/bot-engine/blocks/integrations/googleSheets/getRow.ts +++ b/packages/bot-engine/blocks/integrations/googleSheets/getRow.ts @@ -14,9 +14,14 @@ import { updateVariablesInSession } from '@typebot.io/variables/updateVariablesI export const getRow = async ( state: SessionState, { + blockId, outgoingEdgeId, options, - }: { outgoingEdgeId?: string; options: GoogleSheetsGetOptions } + }: { + blockId: string + outgoingEdgeId?: string + options: GoogleSheetsGetOptions + } ): Promise => { const logs: ChatLog[] = [] const { variables } = state.typebotsQueue[0].typebot @@ -79,10 +84,15 @@ export const getRow = async ( [] ) if (!newVariables) return { outgoingEdgeId } - const newSessionState = updateVariablesInSession(state)(newVariables) + const { updatedState, newSetVariableHistory } = updateVariablesInSession({ + state, + newVariables, + currentBlockId: blockId, + }) return { outgoingEdgeId, - newSessionState, + newSessionState: updatedState, + newSetVariableHistory, } } catch (err) { logs.push({ diff --git a/packages/bot-engine/blocks/integrations/legacy/openai/audio/createSpeechOpenAI.ts b/packages/bot-engine/blocks/integrations/legacy/openai/audio/createSpeechOpenAI.ts index 0bb42dc33a..2cbb0a16f0 100644 --- a/packages/bot-engine/blocks/integrations/legacy/openai/audio/createSpeechOpenAI.ts +++ b/packages/bot-engine/blocks/integrations/legacy/openai/audio/createSpeechOpenAI.ts @@ -107,12 +107,16 @@ export const createSpeechOpenAI = async ( mimeType: 'audio/mpeg', }) - newSessionState = updateVariablesInSession(newSessionState)([ - { - ...saveUrlInVariable, - value: url, - }, - ]) + newSessionState = updateVariablesInSession({ + newVariables: [ + { + ...saveUrlInVariable, + value: url, + }, + ], + state: newSessionState, + currentBlockId: undefined, + }).updatedState return { startTimeShouldBeUpdated: true, diff --git a/packages/bot-engine/blocks/integrations/legacy/openai/createChatCompletionOpenAI.ts b/packages/bot-engine/blocks/integrations/legacy/openai/createChatCompletionOpenAI.ts index 6e142399c4..9ab7deaae3 100644 --- a/packages/bot-engine/blocks/integrations/legacy/openai/createChatCompletionOpenAI.ts +++ b/packages/bot-engine/blocks/integrations/legacy/openai/createChatCompletionOpenAI.ts @@ -22,7 +22,6 @@ import { defaultOpenAIOptions, } from '@typebot.io/schemas/features/blocks/integrations/openai/constants' import { BubbleBlockType } from '@typebot.io/schemas/features/blocks/bubbles/constants' -import { isPlaneteScale } from '@typebot.io/lib/isPlanetScale' export const createChatCompletionOpenAI = async ( state: SessionState, @@ -68,9 +67,11 @@ export const createChatCompletionOpenAI = async ( typebot.variables )(options.messages) if (variablesTransformedToList.length > 0) - newSessionState = updateVariablesInSession(state)( - variablesTransformedToList - ) + newSessionState = updateVariablesInSession({ + state, + newVariables: variablesTransformedToList, + currentBlockId: undefined, + }).updatedState const temperature = parseVariableNumber(typebot.variables)( options.advancedSettings?.temperature diff --git a/packages/bot-engine/blocks/integrations/legacy/openai/resumeChatCompletion.ts b/packages/bot-engine/blocks/integrations/legacy/openai/resumeChatCompletion.ts index bb6f341f91..fae6be0e97 100644 --- a/packages/bot-engine/blocks/integrations/legacy/openai/resumeChatCompletion.ts +++ b/packages/bot-engine/blocks/integrations/legacy/openai/resumeChatCompletion.ts @@ -42,7 +42,11 @@ export const resumeChatCompletion = return newVariables }, []) if (newVariables && newVariables.length > 0) - newSessionState = updateVariablesInSession(newSessionState)(newVariables) + newSessionState = updateVariablesInSession({ + newVariables, + state: newSessionState, + currentBlockId: undefined, + }).updatedState return { outgoingEdgeId, newSessionState, diff --git a/packages/bot-engine/blocks/integrations/webhook/resumeWebhookExecution.ts b/packages/bot-engine/blocks/integrations/webhook/resumeWebhookExecution.ts index ca7b8ec5f9..8de36c64f7 100644 --- a/packages/bot-engine/blocks/integrations/webhook/resumeWebhookExecution.ts +++ b/packages/bot-engine/blocks/integrations/webhook/resumeWebhookExecution.ts @@ -70,10 +70,15 @@ export const resumeWebhookExecution = ({ } }, []) if (newVariables && newVariables.length > 0) { - const newSessionState = updateVariablesInSession(state)(newVariables) + const { updatedState, newSetVariableHistory } = updateVariablesInSession({ + newVariables, + state, + currentBlockId: block.id, + }) return { outgoingEdgeId: block.outgoingEdgeId, - newSessionState, + newSessionState: updatedState, + newSetVariableHistory, logs, } } diff --git a/packages/bot-engine/blocks/integrations/zemanticAi/executeZemanticAiBlock.ts b/packages/bot-engine/blocks/integrations/zemanticAi/executeZemanticAiBlock.ts index 01f70e6b93..40c37f875d 100644 --- a/packages/bot-engine/blocks/integrations/zemanticAi/executeZemanticAiBlock.ts +++ b/packages/bot-engine/blocks/integrations/zemanticAi/executeZemanticAiBlock.ts @@ -19,6 +19,7 @@ export const executeZemanticAiBlock = async ( block: ZemanticAiBlock ): Promise => { let newSessionState = state + let setVariableHistory = [] if (!block.options?.credentialsId) return { @@ -82,24 +83,34 @@ export const executeZemanticAiBlock = async ( for (const r of block.options.responseMapping || []) { const variable = typebot.variables.find(byId(r.variableId)) + let newVariables = [] switch (r.valueToExtract) { case 'Summary': if (isDefined(variable) && !isEmpty(res.summary)) { - newSessionState = updateVariablesInSession(newSessionState)([ - { ...variable, value: res.summary }, - ]) + newVariables.push({ ...variable, value: res.summary }) } break case 'Results': if (isDefined(variable) && res.results.length) { - newSessionState = updateVariablesInSession(newSessionState)([ - { ...variable, value: JSON.stringify(res.results) }, - ]) + newVariables.push({ + ...variable, + value: JSON.stringify(res.results), + }) } break default: break } + if (newVariables.length > 0) { + const { newSetVariableHistory, updatedState } = + updateVariablesInSession({ + newVariables, + state: newSessionState, + currentBlockId: block.id, + }) + newSessionState = updatedState + setVariableHistory.push(...newSetVariableHistory) + } } } catch (e) { console.error(e) @@ -112,6 +123,7 @@ export const executeZemanticAiBlock = async ( description: 'Could not execute Zemantic AI request', }, ], + newSetVariableHistory: setVariableHistory, } } diff --git a/packages/bot-engine/blocks/logic/condition/executeConditionBlock.ts b/packages/bot-engine/blocks/logic/condition/executeConditionBlock.ts index 85a2f9ca26..e58cfaf56b 100644 --- a/packages/bot-engine/blocks/logic/condition/executeConditionBlock.ts +++ b/packages/bot-engine/blocks/logic/condition/executeConditionBlock.ts @@ -1,14 +1,14 @@ import { ConditionBlock, SessionState } from '@typebot.io/schemas' import { ExecuteLogicResponse } from '../../../types' -import { executeCondition } from './executeCondition' - +import { executeCondition } from '@typebot.io/logic/executeCondition' export const executeConditionBlock = ( state: SessionState, block: ConditionBlock ): ExecuteLogicResponse => { const { variables } = state.typebotsQueue[0].typebot const passedCondition = block.items.find( - (item) => item.content && executeCondition(variables)(item.content) + (item) => + item.content && executeCondition({ variables, condition: item.content }) ) return { outgoingEdgeId: passedCondition diff --git a/packages/bot-engine/blocks/logic/script/executeScript.ts b/packages/bot-engine/blocks/logic/script/executeScript.ts index 8ea7ce4ef3..cba609e32d 100644 --- a/packages/bot-engine/blocks/logic/script/executeScript.ts +++ b/packages/bot-engine/blocks/logic/script/executeScript.ts @@ -24,14 +24,25 @@ export const executeScript = async ( body: block.options.content, }) - const newSessionState = newVariables - ? updateVariablesInSession(state)(newVariables) - : state + const updateVarResults = newVariables + ? updateVariablesInSession({ + newVariables, + state, + currentBlockId: block.id, + }) + : undefined + + let newSessionState = state + + if (updateVarResults) { + newSessionState = updateVarResults.updatedState + } return { outgoingEdgeId: block.outgoingEdgeId, logs: error ? [{ status: 'error', description: error }] : [], newSessionState, + newSetVariableHistory: updateVarResults?.newSetVariableHistory, } } diff --git a/packages/bot-engine/blocks/logic/setVariable/executeSetVariable.ts b/packages/bot-engine/blocks/logic/setVariable/executeSetVariable.ts index ea45b1cf68..8e6fcd7271 100644 --- a/packages/bot-engine/blocks/logic/setVariable/executeSetVariable.ts +++ b/packages/bot-engine/blocks/logic/setVariable/executeSetVariable.ts @@ -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 { ExecuteLogicResponse } from '../../../types' import { parseScriptToExecuteClientSideAction } from '../script/executeScript' @@ -7,18 +13,27 @@ import { parseVariables } from '@typebot.io/variables/parseVariables' import { updateVariablesInSession } from '@typebot.io/variables/updateVariablesInSession' import { createId } from '@paralleldrive/cuid2' 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' -export const executeSetVariable = ( +export const executeSetVariable = async ( state: SessionState, block: SetVariableBlock -): ExecuteLogicResponse => { +): Promise => { const { variables } = state.typebotsQueue[0].typebot if (!block.options?.variableId) return { 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' if ( expressionToEvaluate && @@ -52,10 +67,25 @@ export const executeSetVariable = ( ...existingVariable, 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 { outgoingEdgeId: block.outgoingEdgeId, - newSessionState, + newSessionState: updatedState, + newSetVariableHistory, } } @@ -85,7 +115,10 @@ const evaluateSetVariableExpression = const getExpressionToEvaluate = (state: SessionState) => - (options: SetVariableBlock['options']): string | null => { + async ( + options: SetVariableBlock['options'], + blockId: string + ): Promise => { switch (options?.type) { case 'Contact name': return state.whatsApp?.contact.name ?? null @@ -149,6 +182,34 @@ const getExpressionToEvaluate = case 'Environment name': { 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 undefined: { return options?.expressionToEvaluate ?? null @@ -160,3 +221,79 @@ const toISOWithTz = (date: Date, timeZone: string) => { const zonedDate = utcToZonedTime(date, timeZone) return tzFormat(zonedDate, "yyyy-MM-dd'T'HH:mm:ssXXX", { timeZone }) } + +type ParsedTranscriptProps = { + answers: Pick[] + setVariableHistory: Pick< + SetVariableHistoryItem, + 'blockId' | 'variableId' | 'value' + >[] + visitedEdges: string[] +} + +const parseTranscriptProps = async ( + state: SessionState +): Promise => { + if (!state.typebotsQueue[0].resultId) + return parsePreviewTranscriptProps(state) + return parseResultTranscriptProps(state) +} + +const parsePreviewTranscriptProps = async ( + state: SessionState +): Promise => { + if (!state.previewMetadata) return + return { + answers: state.previewMetadata.answers ?? [], + setVariableHistory: state.previewMetadata.setVariableHistory ?? [], + visitedEdges: state.previewMetadata.visitedEdges ?? [], + } +} + +const parseResultTranscriptProps = async ( + state: SessionState +): Promise => { + 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), + } +} diff --git a/packages/bot-engine/continueBotFlow.ts b/packages/bot-engine/continueBotFlow.ts index 8358b5cb33..e6af026823 100644 --- a/packages/bot-engine/continueBotFlow.ts +++ b/packages/bot-engine/continueBotFlow.ts @@ -5,6 +5,7 @@ import { Group, InputBlock, SessionState, + SetVariableHistoryItem, } from '@typebot.io/schemas' import { byId } from '@typebot.io/lib' import { isInputBlock } from '@typebot.io/schemas/helpers' @@ -13,7 +14,7 @@ import { getNextGroup } from './getNextGroup' import { validateEmail } from './blocks/inputs/email/validateEmail' import { formatPhoneNumber } from './blocks/inputs/phone/formatPhoneNumber' 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 { ParsedReply, Reply } from './types' import { validateNumber } from './blocks/inputs/number/validateNumber' @@ -57,11 +58,13 @@ export const continueBotFlow = async ( ContinueChatResponse & { newSessionState: SessionState visitedEdges: VisitedEdge[] + setVariableHistory: SetVariableHistoryItem[] } > => { let firstBubbleWasStreamed = false let newSessionState = { ...state } const visitedEdges: VisitedEdge[] = [] + const setVariableHistory: SetVariableHistoryItem[] = [] if (!newSessionState.currentBlockId) return startBotFlow({ state, version }) @@ -76,16 +79,17 @@ export const continueBotFlow = async ( message: 'Group / block not found', }) + let variableToUpdate + if (block.type === LogicBlockType.SET_VARIABLE) { const existingVariable = state.typebotsQueue[0].typebot.variables.find( byId(block.options?.variableId) ) if (existingVariable && reply && typeof reply === 'string') { - const newVariable = { + variableToUpdate = { ...existingVariable, value: safeJsonParse(reply), } - newSessionState = updateVariablesInSession(state)([newVariable]) } } // Legacy @@ -121,42 +125,41 @@ export const continueBotFlow = async ( if (action) { if (action.run?.stream?.getStreamVariableId) { firstBubbleWasStreamed = true - const variableToUpdate = - state.typebotsQueue[0].typebot.variables.find( - (v) => v.id === action?.run?.stream?.getStreamVariableId(options) - ) - if (variableToUpdate) - newSessionState = updateVariablesInSession(state)([ - { - ...variableToUpdate, - value: reply, - }, - ]) + variableToUpdate = state.typebotsQueue[0].typebot.variables.find( + (v) => v.id === action?.run?.stream?.getStreamVariableId(options) + ) } if ( action.run?.web?.displayEmbedBubble?.waitForEvent?.getSaveVariableId ) { - const variableToUpdate = - state.typebotsQueue[0].typebot.variables.find( - (v) => - v.id === - action?.run?.web?.displayEmbedBubble?.waitForEvent?.getSaveVariableId?.( - options - ) - ) - if (variableToUpdate) - newSessionState = updateVariablesInSession(state)([ - { - ...variableToUpdate, - value: reply, - }, - ]) + variableToUpdate = state.typebotsQueue[0].typebot.variables.find( + (v) => + v.id === + action?.run?.web?.displayEmbedBubble?.waitForEvent?.getSaveVariableId?.( + options + ) + ) } } } } + if (variableToUpdate) { + const { newSetVariableHistory, updatedState } = updateVariablesInSession({ + state: newSessionState, + currentBlockId: block.id, + newVariables: [ + { + ...variableToUpdate, + value: reply, + }, + ], + }) + newSessionState = updatedState + setVariableHistory.push(...newSetVariableHistory) + } + let formattedReply: string | undefined if (isInputBlock(block)) { @@ -167,6 +170,7 @@ export const continueBotFlow = async ( ...(await parseRetryMessage(newSessionState)(block)), newSessionState, visitedEdges: [], + setVariableHistory: [], } formattedReply = @@ -176,7 +180,9 @@ export const continueBotFlow = async ( const groupHasMoreBlocks = blockIndex < group.blocks.length - 1 - const nextEdgeId = getOutgoingEdgeId(newSessionState)(block, formattedReply) + const { edgeId: nextEdgeId, isOffDefaultPath } = getOutgoingEdgeId( + newSessionState + )(block, formattedReply) if (groupHasMoreBlocks && !nextEdgeId) { const chatReply = await executeGroup( @@ -188,6 +194,7 @@ export const continueBotFlow = async ( version, state: newSessionState, visitedEdges, + setVariableHistory, firstBubbleWasStreamed, startTime, } @@ -206,9 +213,14 @@ export const continueBotFlow = async ( lastMessageNewFormat: formattedReply !== reply ? formattedReply : undefined, visitedEdges, + setVariableHistory, } - const nextGroup = await getNextGroup(newSessionState)(nextEdgeId) + const nextGroup = await getNextGroup({ + state: newSessionState, + edgeId: nextEdgeId, + isOffDefaultPath, + }) if (nextGroup.visitedEdge) visitedEdges.push(nextGroup.visitedEdge) @@ -221,6 +233,7 @@ export const continueBotFlow = async ( lastMessageNewFormat: formattedReply !== reply ? formattedReply : undefined, visitedEdges, + setVariableHistory, } const chatReply = await executeGroup(nextGroup.group, { @@ -228,6 +241,7 @@ export const continueBotFlow = async ( state: newSessionState, firstBubbleWasStreamed, visitedEdges, + setVariableHistory, startTime, }) @@ -241,8 +255,7 @@ const processAndSaveAnswer = (state: SessionState, block: InputBlock) => async (reply: string | undefined): Promise => { if (!reply) return state - let newState = await saveAnswer(state, block)(reply) - newState = saveVariableValueIfAny(newState, block)(reply) + let newState = await saveAnswerInDb(state, block)(reply) return newState } @@ -255,16 +268,20 @@ const saveVariableValueIfAny = ) if (!foundVariable) return state - const newSessionState = updateVariablesInSession(state)([ - { - ...foundVariable, - value: Array.isArray(foundVariable.value) - ? foundVariable.value.concat(reply) - : reply, - }, - ]) + const { updatedState } = updateVariablesInSession({ + newVariables: [ + { + ...foundVariable, + value: Array.isArray(foundVariable.value) + ? foundVariable.value.concat(reply) + : reply, + }, + ], + currentBlockId: undefined, + state, + }) - return newSessionState + return updatedState } const parseRetryMessage = @@ -305,31 +322,43 @@ const parseDefaultRetryMessage = (block: InputBlock): string => { } } -const saveAnswer = +const saveAnswerInDb = (state: SessionState, block: InputBlock) => async (reply: string): Promise => { + let newSessionState = state const groupId = state.typebotsQueue[0].typebot.groups.find((group) => group.blocks.some((blockInGroup) => blockInGroup.id === block.id) )?.id if (!groupId) throw new Error('saveAnswer: Group not found') - await upsertAnswer({ + await saveAnswer({ answer: { blockId: block.id, - groupId, content: reply, - variableId: block.options?.variableId, }, reply, 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 - ? state.typebotsQueue[0].typebot.variables.find( + ? newSessionState.typebotsQueue[0].typebot.variables.find( (variable) => variable.id === block.options?.variableId )?.name - : parseGroupKey(block.id, { state }) + : parseGroupKey(block.id, { state: newSessionState }) - return setNewAnswerInState(state)({ + return setNewAnswerInState(newSessionState)({ key: key ?? block.id, value: reply, }) @@ -375,7 +404,10 @@ const setNewAnswerInState = const getOutgoingEdgeId = (state: Pick) => - (block: Block, reply: string | undefined) => { + ( + block: Block, + reply: string | undefined + ): { edgeId: string | undefined; isOffDefaultPath: boolean } => { const variables = state.typebotsQueue[0].typebot.variables if ( block.type === InputBlockType.CHOICE && @@ -390,7 +422,8 @@ const getOutgoingEdgeId = parseVariables(variables)(item.content).normalize() === reply.normalize() ) - if (matchedItem?.outgoingEdgeId) return matchedItem.outgoingEdgeId + if (matchedItem?.outgoingEdgeId) + return { edgeId: matchedItem.outgoingEdgeId, isOffDefaultPath: true } } if ( block.type === InputBlockType.PICTURE_CHOICE && @@ -405,9 +438,10 @@ const getOutgoingEdgeId = parseVariables(variables)(item.title).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 = diff --git a/packages/bot-engine/executeGroup.ts b/packages/bot-engine/executeGroup.ts index cf12f3122a..285a33ba2c 100644 --- a/packages/bot-engine/executeGroup.ts +++ b/packages/bot-engine/executeGroup.ts @@ -4,6 +4,7 @@ import { InputBlock, RuntimeOptions, SessionState, + SetVariableHistoryItem, } from '@typebot.io/schemas' import { isNotEmpty } from '@typebot.io/lib' import { @@ -21,16 +22,16 @@ import { injectVariableValuesInPictureChoiceBlock } from './blocks/inputs/pictur import { getPrefilledInputValue } from './getPrefilledValue' import { parseDateInput } from './blocks/inputs/date/parseDateInput' import { deepParseVariables } from '@typebot.io/variables/deepParseVariables' -import { - BubbleBlockWithDefinedContent, - parseBubbleBlock, -} from './parseBubbleBlock' import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants' import { VisitedEdge } from '@typebot.io/prisma' import { env } from '@typebot.io/env' import { TRPCError } from '@trpc/server' import { ExecuteIntegrationResponse, ExecuteLogicResponse } from './types' import { createId } from '@paralleldrive/cuid2' +import { + BubbleBlockWithDefinedContent, + parseBubbleBlock, +} from './parseBubbleBlock' type ContextProps = { version: 1 | 2 @@ -39,6 +40,7 @@ type ContextProps = { currentLastBubbleId?: string firstBubbleWasStreamed?: boolean visitedEdges: VisitedEdge[] + setVariableHistory: SetVariableHistoryItem[] startTime?: number } @@ -48,6 +50,7 @@ export const executeGroup = async ( version, state, visitedEdges, + setVariableHistory, currentReply, currentLastBubbleId, firstBubbleWasStreamed, @@ -56,6 +59,7 @@ export const executeGroup = async ( ): Promise< ContinueChatResponse & { newSessionState: SessionState + setVariableHistory: SetVariableHistoryItem[] visitedEdges: VisitedEdge[] } > => { @@ -70,6 +74,7 @@ export const executeGroup = async ( let newSessionState = state + let isNextEdgeOffDefaultPath = false let index = -1 for (const block of group.blocks) { if ( @@ -110,6 +115,7 @@ export const executeGroup = async ( clientSideActions, logs, visitedEdges, + setVariableHistory, } const executionResponse = ( isLogicBlock(block) @@ -120,6 +126,29 @@ export const executeGroup = async ( ) as ExecuteLogicResponse | ExecuteIntegrationResponse | null 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 ( 'startTimeShouldBeUpdated' in executionResponse && executionResponse.startTimeShouldBeUpdated @@ -165,33 +194,55 @@ export const executeGroup = async ( clientSideActions, logs, visitedEdges, + setVariableHistory, } } } if (executionResponse.outgoingEdgeId) { + isNextEdgeOffDefaultPath = + block.outgoingEdgeId !== executionResponse.outgoingEdgeId nextEdgeId = executionResponse.outgoingEdgeId break } } 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 if (nextGroup.visitedEdge) visitedEdges.push(nextGroup.visitedEdge) if (!nextGroup.group) { - return { messages, newSessionState, clientSideActions, logs, visitedEdges } + return { + messages, + newSessionState, + clientSideActions, + logs, + visitedEdges, + setVariableHistory, + } } return executeGroup(nextGroup.group, { version, state: newSessionState, visitedEdges, + setVariableHistory, currentReply: { messages, clientSideActions, diff --git a/packages/bot-engine/forge/executeForgedBlock.ts b/packages/bot-engine/forge/executeForgedBlock.ts index 993b091f16..05d004f91d 100644 --- a/packages/bot-engine/forge/executeForgedBlock.ts +++ b/packages/bot-engine/forge/executeForgedBlock.ts @@ -2,12 +2,12 @@ import { VariableStore, LogsStore } from '@typebot.io/forge' import { forgedBlocks } from '@typebot.io/forge-repository/definitions' import { ForgedBlock } from '@typebot.io/forge-repository/types' import { decrypt } from '@typebot.io/lib/api/encryption/decrypt' -import { isPlaneteScale } from '@typebot.io/lib/isPlanetScale' import { SessionState, ContinueChatResponse, Block, TypebotInSession, + SetVariableHistoryItem, } from '@typebot.io/schemas' import { deepParseVariables } from '@typebot.io/variables/deepParseVariables' import { @@ -73,6 +73,7 @@ export const executeForgedBlock = async ( } let newSessionState = state + let setVariableHistory: SetVariableHistoryItem[] = [] const variables: VariableStore = { get: (id: string) => { @@ -86,9 +87,13 @@ export const executeForgedBlock = async ( (variable) => variable.id === id ) if (!variable) return - newSessionState = updateVariablesInSession(newSessionState)([ - { ...variable, value }, - ]) + const { newSetVariableHistory, updatedState } = updateVariablesInSession({ + newVariables: [{ ...variable, value }], + state: newSessionState, + currentBlockId: block.id, + }) + newSessionState = updatedState + setVariableHistory.push(...newSetVariableHistory) }, parse: (text: string, params?: ParseVariablesOptions) => parseVariables( @@ -159,6 +164,7 @@ export const executeForgedBlock = async ( }, } : undefined, + newSetVariableHistory: setVariableHistory, } } diff --git a/packages/bot-engine/getFirstEdgeId.ts b/packages/bot-engine/getFirstEdgeId.ts index 8c290fa8bf..c63fa71220 100644 --- a/packages/bot-engine/getFirstEdgeId.ts +++ b/packages/bot-engine/getFirstEdgeId.ts @@ -1,14 +1,13 @@ import { TRPCError } from '@trpc/server' -import { SessionState } from '@typebot.io/schemas' +import { TypebotInSession } from '@typebot.io/schemas' export const getFirstEdgeId = ({ - state, + typebot, startEventId, }: { - state: SessionState + typebot: Pick startEventId: string | undefined }) => { - const { typebot } = state.typebotsQueue[0] if (startEventId) { const event = typebot.events?.find((e) => e.id === startEventId) if (!event) @@ -18,6 +17,6 @@ export const getFirstEdgeId = ({ }) 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 } diff --git a/packages/bot-engine/getNextGroup.ts b/packages/bot-engine/getNextGroup.ts index 63399504f2..5a4883cdeb 100644 --- a/packages/bot-engine/getNextGroup.ts +++ b/packages/bot-engine/getNextGroup.ts @@ -9,116 +9,138 @@ export type NextGroup = { visitedEdge?: VisitedEdge } -export const getNextGroup = - (state: SessionState) => - async (edgeId?: string): Promise => { - const nextEdge = state.typebotsQueue[0].typebot.edges.find(byId(edgeId)) - if (!nextEdge) { - if (state.typebotsQueue.length > 1) { - const nextEdgeId = state.typebotsQueue[0].edgeIdToTriggerWhenDone - const isMergingWithParent = state.typebotsQueue[0].isMergingWithParent - const currentResultId = state.typebotsQueue[0].resultId - if (!isMergingWithParent && currentResultId) - await upsertResult({ - resultId: currentResultId, - typebot: state.typebotsQueue[0].typebot, - isCompleted: true, - hasStarted: state.typebotsQueue[0].answers.length > 0, - }) - let newSessionState = { - ...state, - typebotsQueue: [ - { - ...state.typebotsQueue[1], - typebot: isMergingWithParent - ? { - ...state.typebotsQueue[1].typebot, - variables: state.typebotsQueue[1].typebot.variables - .map((variable) => ({ - ...variable, - value: - state.typebotsQueue[0].typebot.variables.find( - (v) => v.name === variable.name - )?.value ?? variable.value, - })) - .concat( - state.typebotsQueue[0].typebot.variables.filter( - (variable) => - isDefined(variable.value) && - isNotDefined( - state.typebotsQueue[1].typebot.variables.find( - (v) => v.name === variable.name - ) +export const getNextGroup = async ({ + state, + edgeId, + isOffDefaultPath, +}: { + state: SessionState + edgeId?: string + isOffDefaultPath: boolean +}): Promise => { + const nextEdge = state.typebotsQueue[0].typebot.edges.find(byId(edgeId)) + if (!nextEdge) { + if (state.typebotsQueue.length > 1) { + const nextEdgeId = state.typebotsQueue[0].edgeIdToTriggerWhenDone + const isMergingWithParent = state.typebotsQueue[0].isMergingWithParent + const currentResultId = state.typebotsQueue[0].resultId + if (!isMergingWithParent && currentResultId) + await upsertResult({ + resultId: currentResultId, + typebot: state.typebotsQueue[0].typebot, + isCompleted: true, + hasStarted: state.typebotsQueue[0].answers.length > 0, + }) + let newSessionState = { + ...state, + typebotsQueue: [ + { + ...state.typebotsQueue[1], + typebot: isMergingWithParent + ? { + ...state.typebotsQueue[1].typebot, + variables: state.typebotsQueue[1].typebot.variables + .map((variable) => ({ + ...variable, + value: + state.typebotsQueue[0].typebot.variables.find( + (v) => v.name === variable.name + )?.value ?? variable.value, + })) + .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[] - ), - } - : state.typebotsQueue[1].typebot, - answers: isMergingWithParent - ? [ - ...state.typebotsQueue[1].answers.filter( - (incomingAnswer) => - !state.typebotsQueue[0].answers.find( - (currentAnswer) => - currentAnswer.key === incomingAnswer.key - ) + ) + ) as VariableWithValue[] ), - ...state.typebotsQueue[0].answers, - ] - : state.typebotsQueue[1].answers, - }, - ...state.typebotsQueue.slice(2), - ], - } satisfies SessionState - if (state.progressMetadata) - newSessionState.progressMetadata = { - ...state.progressMetadata, - totalAnswers: - state.progressMetadata.totalAnswers + - state.typebotsQueue[0].answers.length, - } - const nextGroup = await getNextGroup(newSessionState)(nextEdgeId) - newSessionState = nextGroup.newSessionState - if (!nextGroup) - return { - newSessionState, - } + } + : state.typebotsQueue[1].typebot, + answers: isMergingWithParent + ? [ + ...state.typebotsQueue[1].answers.filter( + (incomingAnswer) => + !state.typebotsQueue[0].answers.find( + (currentAnswer) => + currentAnswer.key === incomingAnswer.key + ) + ), + ...state.typebotsQueue[0].answers, + ] + : state.typebotsQueue[1].answers, + }, + ...state.typebotsQueue.slice(2), + ], + } satisfies SessionState + 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 { - ...nextGroup, newSessionState, } - } 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 { - group: { - ...nextGroup, - blocks: nextGroup.blocks.slice(startBlockIndex), - } as Group, - newSessionState: { - ...state, - currentVisitedEdgeIndex, - }, - visitedEdge: resultId + newSessionState: state, + } + } + 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 = 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, resultId, } : undefined, - } } +} diff --git a/packages/bot-engine/package.json b/packages/bot-engine/package.json index 446d724e17..8b5fc1c2e5 100644 --- a/packages/bot-engine/package.json +++ b/packages/bot-engine/package.json @@ -19,6 +19,7 @@ "@typebot.io/tsconfig": "workspace:*", "@typebot.io/variables": "workspace:*", "@udecode/plate-common": "30.4.5", + "@typebot.io/logic": "workspace:*", "ai": "3.0.31", "chrono-node": "2.7.5", "date-fns": "2.30.0", diff --git a/packages/bot-engine/parseBubbleBlock.ts b/packages/bot-engine/parseBubbleBlock.ts index e7e77544b8..74da1c4a21 100644 --- a/packages/bot-engine/parseBubbleBlock.ts +++ b/packages/bot-engine/parseBubbleBlock.ts @@ -6,7 +6,7 @@ import { Typebot, } from '@typebot.io/schemas' 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 { getVariablesToParseInfoInText, parseVariables, @@ -49,7 +49,7 @@ export const parseBubbleBlock = ( richText: parseVariablesInRichText(block.content?.richText ?? [], { variables, takeLatestIfList: typebotVersion !== '6', - }), + }).parsedElements, }, } } @@ -93,14 +93,15 @@ export const parseBubbleBlock = ( } } -const parseVariablesInRichText = ( +export const parseVariablesInRichText = ( elements: TDescendant[], { variables, takeLatestIfList, }: { variables: Variable[]; takeLatestIfList?: boolean } -): TDescendant[] => { +): { parsedElements: TDescendant[]; parsedVariableIds: string[] } => { const parsedElements: TDescendant[] = [] + const parsedVariableIds: string[] = [] for (const element of elements) { if ('text' in element) { const text = element.text as string @@ -112,6 +113,9 @@ const parseVariablesInRichText = ( variables, takeLatestIfList, }) + parsedVariableIds.push( + ...variablesInText.map((v) => v.variableId).filter(isDefined) + ) if (variablesInText.length === 0) { parsedElements.push(element) continue @@ -185,19 +189,28 @@ const parseVariablesInRichText = ( ? 'variable' : element.type + const { + parsedElements: parsedChildren, + parsedVariableIds: parsedChildrenVariableIds, + } = parseVariablesInRichText(element.children as TDescendant[], { + variables, + takeLatestIfList, + }) + + parsedVariableIds.push(...parsedChildrenVariableIds) parsedElements.push({ ...element, url: element.url ? parseVariables(variables)(element.url as string) : undefined, type, - children: parseVariablesInRichText(element.children as TDescendant[], { - variables, - takeLatestIfList, - }), + children: parsedChildren, }) } - return parsedElements + return { + parsedElements, + parsedVariableIds, + } } const applyElementStyleToDescendants = ( diff --git a/packages/bot-engine/queries/createSession.ts b/packages/bot-engine/queries/createSession.ts index dd826e1459..f603af1bb4 100644 --- a/packages/bot-engine/queries/createSession.ts +++ b/packages/bot-engine/queries/createSession.ts @@ -12,11 +12,26 @@ export const createSession = ({ id, state, isReplying, -}: Props): Prisma.PrismaPromise => - prisma.chatSession.create({ - data: { +}: Props): Prisma.PrismaPromise => { + if (!id) { + return prisma.chatSession.create({ + data: { + id, + state, + isReplying, + }, + }) + } + return prisma.chatSession.upsert({ + where: { id }, + update: { + state, + isReplying, + }, + create: { id, state, isReplying, }, }) +} diff --git a/packages/bot-engine/queries/findResult.ts b/packages/bot-engine/queries/findResult.ts index 7d7315e690..7220714383 100644 --- a/packages/bot-engine/queries/findResult.ts +++ b/packages/bot-engine/queries/findResult.ts @@ -4,24 +4,33 @@ import { Answer, Result } from '@typebot.io/schemas' type Props = { id: string } -export const findResult = ({ id }: Props) => - prisma.result.findFirst({ - where: { id, isArchived: { not: true } }, - select: { - id: true, - variables: true, - hasStarted: true, - answers: { - select: { - content: true, - blockId: true, - variableId: true, +export const findResult = async ({ id }: Props) => { + const { answers, answersV2, ...result } = + (await prisma.result.findFirst({ + where: { id, isArchived: { not: true } }, + select: { + id: true, + variables: true, + hasStarted: true, + answers: { + select: { + content: true, + blockId: true, + }, + }, + answersV2: { + select: { + content: true, + blockId: true, + }, }, }, - }, - }) as Promise< - | (Pick & { - answers: Pick[] - }) - | null - > + })) ?? {} + if (!result) return null + return { + ...result, + answers: (answersV2 ?? []).concat(answers ?? []), + } as Pick & { + answers: Pick[] + } +} diff --git a/packages/bot-engine/queries/saveAnswer.ts b/packages/bot-engine/queries/saveAnswer.ts new file mode 100644 index 0000000000..d80f23cac9 --- /dev/null +++ b/packages/bot-engine/queries/saveAnswer.ts @@ -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 + 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 }], + }) +} diff --git a/packages/bot-engine/queries/saveSetVariableHistoryItems.ts b/packages/bot-engine/queries/saveSetVariableHistoryItems.ts new file mode 100644 index 0000000000..137eb19aaf --- /dev/null +++ b/packages/bot-engine/queries/saveSetVariableHistoryItems.ts @@ -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, + }) diff --git a/packages/bot-engine/queries/upsertAnswer.ts b/packages/bot-engine/queries/upsertAnswer.ts deleted file mode 100644 index 9c95511ed4..0000000000 --- a/packages/bot-engine/queries/upsertAnswer.ts +++ /dev/null @@ -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 - 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 }], - }) -} diff --git a/packages/bot-engine/queries/upsertResult.ts b/packages/bot-engine/queries/upsertResult.ts index 9947bd112d..3f6bdd0657 100644 --- a/packages/bot-engine/queries/upsertResult.ts +++ b/packages/bot-engine/queries/upsertResult.ts @@ -1,29 +1,79 @@ import prisma from '@typebot.io/lib/prisma' -import { Prisma } from '@typebot.io/prisma' -import { TypebotInSession } from '@typebot.io/schemas' +import { Prisma, SetVariableHistoryItem, VisitedEdge } from '@typebot.io/prisma' +import { ContinueChatResponse, TypebotInSession } from '@typebot.io/schemas' import { filterNonSessionVariablesWithValues } from '@typebot.io/variables/filterVariablesWithValues' +import { formatLogDetails } from '../logs/helpers/formatLogDetails' type Props = { resultId: string typebot: TypebotInSession hasStarted: boolean isCompleted: boolean + lastChatSessionId?: string + logs?: ContinueChatResponse['logs'] + visitedEdges?: VisitedEdge[] + setVariableHistory?: SetVariableHistoryItem[] } export const upsertResult = ({ resultId, typebot, hasStarted, isCompleted, + lastChatSessionId, + logs, + visitedEdges, + setVariableHistory, }: Props): Prisma.PrismaPromise => { const variablesWithValue = filterNonSessionVariablesWithValues( 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({ where: { id: resultId }, update: { isCompleted: isCompleted ? true : undefined, hasStarted, variables: variablesWithValue, + lastChatSessionId, + logs: logsToCreate, + setVariableHistory: setVariableHistoryToCreate, + edges: visitedEdgesToCreate, }, create: { id: resultId, @@ -31,6 +81,10 @@ export const upsertResult = ({ isCompleted: isCompleted ? true : false, hasStarted, variables: variablesWithValue, + lastChatSessionId, + logs: logsToCreate, + setVariableHistory: setVariableHistoryToCreate, + edges: visitedEdgesToCreate, }, select: { id: true }, }) diff --git a/packages/bot-engine/saveStateToDatabase.ts b/packages/bot-engine/saveStateToDatabase.ts index b2985e7f9e..0241deddca 100644 --- a/packages/bot-engine/saveStateToDatabase.ts +++ b/packages/bot-engine/saveStateToDatabase.ts @@ -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 { saveLogs } from './queries/saveLogs' import { updateSession } from './queries/updateSession' -import { formatLogDetails } from './logs/helpers/formatLogDetails' import { createSession } from './queries/createSession' 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 from '@typebot.io/lib/prisma' @@ -16,7 +16,9 @@ type Props = { logs: ContinueChatResponse['logs'] clientSideActions: ContinueChatResponse['clientSideActions'] visitedEdges: VisitedEdge[] + setVariableHistory: SetVariableHistoryItem[] hasCustomEmbedBubble?: boolean + initialSessionId?: string } export const saveStateToDatabase = async ({ @@ -25,7 +27,9 @@ export const saveStateToDatabase = async ({ logs, clientSideActions, visitedEdges, + setVariableHistory, hasCustomEmbedBubble, + initialSessionId, }: Props) => { const containsSetVariableClientSideAction = clientSideActions?.some( (action) => action.expectsDedicatedReply @@ -46,7 +50,7 @@ export const saveStateToDatabase = async ({ const session = id ? { state, id } - : await createSession({ id, state, isReplying: false }) + : await createSession({ id: initialSessionId, state, isReplying: false }) if (!resultId) { if (queries.length > 0) await prisma.$transaction(queries) @@ -63,25 +67,13 @@ export const saveStateToDatabase = async ({ !input && !containsSetVariableClientSideAction && 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) return session diff --git a/packages/bot-engine/startBotFlow.ts b/packages/bot-engine/startBotFlow.ts index eb00514c60..1f9a18f56a 100644 --- a/packages/bot-engine/startBotFlow.ts +++ b/packages/bot-engine/startBotFlow.ts @@ -2,6 +2,7 @@ import { TRPCError } from '@trpc/server' import { ContinueChatResponse, SessionState, + SetVariableHistoryItem, StartFrom, } from '@typebot.io/schemas' import { executeGroup } from './executeGroup' @@ -25,10 +26,12 @@ export const startBotFlow = async ({ ContinueChatResponse & { newSessionState: SessionState visitedEdges: VisitedEdge[] + setVariableHistory: SetVariableHistoryItem[] } > => { let newSessionState = state const visitedEdges: VisitedEdge[] = [] + const setVariableHistory: SetVariableHistoryItem[] = [] if (startFrom?.type === 'group') { const group = state.typebotsQueue[0].typebot.groups.find( (group) => group.id === startFrom.groupId @@ -42,22 +45,34 @@ export const startBotFlow = async ({ version, state: newSessionState, visitedEdges, + setVariableHistory, startTime, }) } const firstEdgeId = getFirstEdgeId({ - state: newSessionState, + typebot: newSessionState.typebotsQueue[0].typebot, startEventId: startFrom?.type === 'event' ? startFrom.eventId : undefined, }) - if (!firstEdgeId) return { messages: [], newSessionState, visitedEdges: [] } - const nextGroup = await getNextGroup(newSessionState)(firstEdgeId) + if (!firstEdgeId) + return { + messages: [], + newSessionState, + setVariableHistory: [], + visitedEdges: [], + } + const nextGroup = await getNextGroup({ + state: newSessionState, + edgeId: firstEdgeId, + isOffDefaultPath: false, + }) newSessionState = nextGroup.newSessionState - if (nextGroup.visitedEdge) visitedEdges.push(nextGroup.visitedEdge) - if (!nextGroup.group) return { messages: [], newSessionState, visitedEdges } + if (!nextGroup.group) + return { messages: [], newSessionState, visitedEdges, setVariableHistory } return executeGroup(nextGroup.group, { version, state: newSessionState, visitedEdges, + setVariableHistory, startTime, }) } diff --git a/packages/bot-engine/startSession.ts b/packages/bot-engine/startSession.ts index 371a36dc88..05e89c11ab 100644 --- a/packages/bot-engine/startSession.ts +++ b/packages/bot-engine/startSession.ts @@ -11,6 +11,7 @@ import { SessionState, TypebotInSession, Block, + SetVariableHistoryItem, } from '@typebot.io/schemas' import { StartChatInput, @@ -31,7 +32,10 @@ import { injectVariablesFromExistingResult } from '@typebot.io/variables/injectV import { getNextGroup } from './getNextGroup' import { upsertResult } from './queries/upsertResult' 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 { IntegrationBlockType } from '@typebot.io/schemas/features/blocks/integrations/constants' import { VisitedEdge } from '@typebot.io/prisma' @@ -42,6 +46,9 @@ import { defaultGuestAvatarIsEnabled, defaultHostAvatarIsEnabled, } 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 = | ({ @@ -68,6 +75,7 @@ export const startSession = async ({ Omit & { newSessionState: SessionState visitedEdges: VisitedEdge[] + setVariableHistory: SetVariableHistoryItem[] resultId?: string } > => { @@ -145,6 +153,8 @@ export const startSession = async ({ : typebot.theme.general?.progressBar?.isEnabled ? { totalAnswers: 0 } : undefined, + setVariableIdsForHistory: + extractVariableIdsUsedForTranscript(typebotInSession), ...initialSessionState, } @@ -164,6 +174,7 @@ export const startSession = async ({ dynamicTheme: parseDynamicTheme(initialState), messages: [], 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 (message) { const firstEdgeId = getFirstEdgeId({ - state: chatReply.newSessionState, + typebot: chatReply.newSessionState.typebotsQueue[0].typebot, startEventId: startParams.type === 'preview' && startParams.startFrom?.type === 'event' ? startParams.startFrom.eventId : undefined, }) - const nextGroup = await getNextGroup(chatReply.newSessionState)(firstEdgeId) + const nextGroup = await getNextGroup({ + state: chatReply.newSessionState, + edgeId: firstEdgeId, + isOffDefaultPath: false, + }) const newSessionState = nextGroup.newSessionState const firstBlock = nextGroup.group?.blocks.at(0) if (firstBlock && isInputBlock(firstBlock)) { @@ -214,6 +229,7 @@ export const startSession = async ({ newSessionState, logs, visitedEdges, + setVariableHistory, } = chatReply const clientSideActions = startFlowClientActions ?? [] @@ -268,6 +284,7 @@ export const startSession = async ({ dynamicTheme: parseDynamicTheme(newSessionState), logs: startLogs.length > 0 ? startLogs : undefined, visitedEdges, + setVariableHistory, } return { @@ -290,6 +307,7 @@ export const startSession = async ({ dynamicTheme: parseDynamicTheme(newSessionState), logs: startLogs.length > 0 ? startLogs : undefined, visitedEdges, + setVariableHistory, } } @@ -497,3 +515,59 @@ const convertStartTypebotToTypebotInSession = ( variables: startVariables, events: typebot.events, } + +const extractVariableIdsUsedForTranscript = ( + typebot: TypebotInSession +): string[] => { + const variableIds: Set = 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] +} diff --git a/packages/bot-engine/types.ts b/packages/bot-engine/types.ts index 6e273f1db4..36a529c154 100644 --- a/packages/bot-engine/types.ts +++ b/packages/bot-engine/types.ts @@ -2,6 +2,7 @@ import { ContinueChatResponse, CustomEmbedBubble, SessionState, + SetVariableHistoryItem, } from '@typebot.io/schemas' export type EdgeId = string @@ -9,6 +10,7 @@ export type EdgeId = string export type ExecuteLogicResponse = { outgoingEdgeId: EdgeId | undefined newSessionState?: SessionState + newSetVariableHistory?: SetVariableHistoryItem[] } & Pick export type ExecuteIntegrationResponse = { @@ -16,6 +18,7 @@ export type ExecuteIntegrationResponse = { newSessionState?: SessionState startTimeShouldBeUpdated?: boolean customEmbedBubble?: CustomEmbedBubble + newSetVariableHistory?: SetVariableHistoryItem[] } & Pick type WhatsAppMediaMessage = { diff --git a/packages/bot-engine/whatsapp/resumeWhatsAppFlow.ts b/packages/bot-engine/whatsapp/resumeWhatsAppFlow.ts index 5b05e76e71..6c04befd5e 100644 --- a/packages/bot-engine/whatsapp/resumeWhatsAppFlow.ts +++ b/packages/bot-engine/whatsapp/resumeWhatsAppFlow.ts @@ -114,6 +114,7 @@ export const resumeWhatsAppFlow = async ({ messages, clientSideActions, visitedEdges, + setVariableHistory, } = resumeResponse const isFirstChatChunk = (!session || isSessionExpired) ?? false @@ -140,6 +141,7 @@ export const resumeWhatsAppFlow = async ({ }, }, visitedEdges, + setVariableHistory, }) return { diff --git a/packages/bot-engine/whatsapp/startWhatsAppSession.ts b/packages/bot-engine/whatsapp/startWhatsAppSession.ts index 8a4a4096a2..c257343f97 100644 --- a/packages/bot-engine/whatsapp/startWhatsAppSession.ts +++ b/packages/bot-engine/whatsapp/startWhatsAppSession.ts @@ -3,6 +3,7 @@ import { ContinueChatResponse, PublicTypebot, SessionState, + SetVariableHistoryItem, Settings, Typebot, } from '@typebot.io/schemas' @@ -35,6 +36,7 @@ export const startWhatsAppSession = async ({ | (ContinueChatResponse & { newSessionState: SessionState visitedEdges: VisitedEdge[] + setVariableHistory: SetVariableHistoryItem[] }) | { error: string } > => { diff --git a/packages/embeds/js/package.json b/packages/embeds/js/package.json index 591734bb12..8bd462f02f 100644 --- a/packages/embeds/js/package.json +++ b/packages/embeds/js/package.json @@ -1,6 +1,6 @@ { "name": "@typebot.io/js", - "version": "0.2.81", + "version": "0.2.82", "description": "Javascript library to display typebots on your website", "type": "module", "main": "dist/index.js", diff --git a/packages/embeds/js/src/components/Bot.tsx b/packages/embeds/js/src/components/Bot.tsx index 53837048e9..dca0e93b09 100644 --- a/packages/embeds/js/src/components/Bot.tsx +++ b/packages/embeds/js/src/components/Bot.tsx @@ -39,13 +39,14 @@ export type BotProps = { apiHost?: string font?: Font progressBarRef?: HTMLDivElement + startFrom?: StartFrom + sessionId?: string onNewInputBlock?: (inputBlock: InputBlock) => void onAnswer?: (answer: { message: string; blockId: string }) => void onInit?: () => void onEnd?: () => void onNewLogs?: (logs: OutgoingLog[]) => void onChatStatePersisted?: (isEnabled: boolean) => void - startFrom?: StartFrom } export const Bot = (props: BotProps & { class?: string }) => { @@ -81,6 +82,7 @@ export const Bot = (props: BotProps & { class?: string }) => { ...props.prefilledVariables, }, startFrom: props.startFrom, + sessionId: props.sessionId, }) if (error instanceof HTTPError) { if (isPreview) { diff --git a/packages/embeds/js/src/constants.ts b/packages/embeds/js/src/constants.ts index 34d7f0d1de..8f543b1dc6 100644 --- a/packages/embeds/js/src/constants.ts +++ b/packages/embeds/js/src/constants.ts @@ -14,6 +14,7 @@ export const defaultBotProps: BotProps = { prefilledVariables: undefined, apiHost: undefined, resultId: undefined, + sessionId: undefined, } export const defaultPopupProps: PopupProps = { diff --git a/packages/embeds/js/src/queries/startChatQuery.ts b/packages/embeds/js/src/queries/startChatQuery.ts index 36b0dedfcf..67ec82b07c 100644 --- a/packages/embeds/js/src/queries/startChatQuery.ts +++ b/packages/embeds/js/src/queries/startChatQuery.ts @@ -21,6 +21,7 @@ type Props = { isPreview: boolean prefilledVariables?: Record resultId?: string + sessionId?: string } export async function startChatQuery({ @@ -31,6 +32,7 @@ export async function startChatQuery({ resultId, stripeRedirectStatus, startFrom, + sessionId, }: Props) { if (isNotDefined(typebot)) throw new Error('Typebot ID is required to get initial messages') @@ -83,6 +85,7 @@ export async function startChatQuery({ startFrom, typebot, prefilledVariables, + sessionId, } satisfies Omit< StartPreviewChatInput, 'typebotId' | 'isOnlyRegistering' diff --git a/packages/embeds/nextjs/package.json b/packages/embeds/nextjs/package.json index 17c9ceeb56..d54ecc6cc9 100644 --- a/packages/embeds/nextjs/package.json +++ b/packages/embeds/nextjs/package.json @@ -1,6 +1,6 @@ { "name": "@typebot.io/nextjs", - "version": "0.2.81", + "version": "0.2.82", "description": "Convenient library to display typebots on your Next.js website", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/embeds/react/package.json b/packages/embeds/react/package.json index 4c72be93d5..a61edb383c 100644 --- a/packages/embeds/react/package.json +++ b/packages/embeds/react/package.json @@ -1,6 +1,6 @@ { "name": "@typebot.io/react", - "version": "0.2.81", + "version": "0.2.82", "description": "Convenient library to display typebots on your React app", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/logic/computeResultTranscript.ts b/packages/logic/computeResultTranscript.ts new file mode 100644 index 0000000000..4ab1465d91 --- /dev/null +++ b/packages/logic/computeResultTranscript.ts @@ -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[] + 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[] + 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 + | 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 } +} diff --git a/packages/bot-engine/blocks/logic/condition/executeCondition.ts b/packages/logic/executeCondition.ts similarity index 94% rename from packages/bot-engine/blocks/logic/condition/executeCondition.ts rename to packages/logic/executeCondition.ts index d817c7ec3c..e303aaa815 100644 --- a/packages/bot-engine/blocks/logic/condition/executeCondition.ts +++ b/packages/logic/executeCondition.ts @@ -8,15 +8,18 @@ import { defaultConditionItemContent, } from '@typebot.io/schemas/features/blocks/logic/condition/constants' -export const executeCondition = - (variables: Variable[]) => - (condition: Condition): boolean => { - if (!condition.comparisons) return false - return (condition.logicalOperator ?? - defaultConditionItemContent.logicalOperator) === LogicalOperator.AND - ? condition.comparisons.every(executeComparison(variables)) - : condition.comparisons.some(executeComparison(variables)) - } +type Props = { + condition: Condition + variables: Variable[] +} + +export const executeCondition = ({ condition, variables }: Props): boolean => { + if (!condition.comparisons) return false + return (condition.logicalOperator ?? + defaultConditionItemContent.logicalOperator) === LogicalOperator.AND + ? condition.comparisons.every(executeComparison(variables)) + : condition.comparisons.some(executeComparison(variables)) +} const executeComparison = (variables: Variable[]) => diff --git a/packages/logic/package.json b/packages/logic/package.json new file mode 100644 index 0000000000..028191af18 --- /dev/null +++ b/packages/logic/package.json @@ -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" + } +} diff --git a/packages/logic/tsconfig.json b/packages/logic/tsconfig.json new file mode 100644 index 0000000000..adb22d5013 --- /dev/null +++ b/packages/logic/tsconfig.json @@ -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"] + } +} diff --git a/packages/playwright/databaseActions.ts b/packages/playwright/databaseActions.ts index 2541d80690..3528c85ef2 100644 --- a/packages/playwright/databaseActions.ts +++ b/packages/playwright/databaseActions.ts @@ -58,13 +58,12 @@ const createAnswers = ({ count, resultIdPrefix, }: { resultIdPrefix: string } & Pick) => { - return prisma.answer.createMany({ + return prisma.answerV2.createMany({ data: [ ...Array.from(Array(count)).map((_, idx) => ({ resultId: `${resultIdPrefix}-result${idx}`, content: `content${idx}`, blockId: 'block1', - groupId: 'group1', })), ], }) diff --git a/packages/prisma/mysql/schema.prisma b/packages/prisma/mysql/schema.prisma index 4decbff5ed..1ad3dc928e 100644 --- a/packages/prisma/mysql/schema.prisma +++ b/packages/prisma/mysql/schema.prisma @@ -255,22 +255,36 @@ model PublicTypebot { } model Result { - id String @id @default(cuid()) - createdAt DateTime @default(now()) - typebotId String - variables Json - isCompleted Boolean - hasStarted Boolean? - isArchived Boolean? @default(false) - typebot Typebot @relation(fields: [typebotId], references: [id], onDelete: Cascade) - answers Answer[] - logs Log[] - edges VisitedEdge[] + id String @id @default(cuid()) + createdAt DateTime @default(now()) + typebotId String + variables Json + isCompleted Boolean + hasStarted Boolean? + isArchived Boolean? @default(false) + lastChatSessionId String? + typebot Typebot @relation(fields: [typebotId], references: [id], onDelete: Cascade) + answers Answer[] + logs Log[] + edges VisitedEdge[] + setVariableHistory SetVariableHistoryItem[] + answersV2 AnswerV2[] @@index([typebotId, isArchived, hasStarted, createdAt(sort: Desc)]) @@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 { result Result @relation(fields: [resultId], references: [id], onDelete: Cascade) resultId String @@ -292,20 +306,28 @@ model Log { @@index([resultId]) } +// TODO: gradually remove variableId and groupId model Answer { - createdAt DateTime @default(now()) @updatedAt - resultId String - blockId String - itemId String? - groupId String - variableId String? - content String @db.Text - storageUsed Int? - result Result @relation(fields: [resultId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) @updatedAt + resultId String + blockId String + groupId String + variableId String? + content String @db.Text + result Result @relation(fields: [resultId], references: [id], onDelete: Cascade) @@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 { diff --git a/packages/prisma/postgresql/migrations/20240515062409_add_answerv2_table/migration.sql b/packages/prisma/postgresql/migrations/20240515062409_add_answerv2_table/migration.sql new file mode 100644 index 0000000000..a9920673b6 --- /dev/null +++ b/packages/prisma/postgresql/migrations/20240515062409_add_answerv2_table/migration.sql @@ -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; diff --git a/packages/prisma/postgresql/schema.prisma b/packages/prisma/postgresql/schema.prisma index f6fab45728..def5470eb3 100644 --- a/packages/prisma/postgresql/schema.prisma +++ b/packages/prisma/postgresql/schema.prisma @@ -236,22 +236,36 @@ model PublicTypebot { } model Result { - id String @id @default(cuid()) - createdAt DateTime @default(now()) - typebotId String - variables Json - isCompleted Boolean - hasStarted Boolean? - isArchived Boolean? @default(false) - typebot Typebot @relation(fields: [typebotId], references: [id], onDelete: Cascade) - answers Answer[] - logs Log[] - edges VisitedEdge[] + id String @id @default(cuid()) + createdAt DateTime @default(now()) + typebotId String + variables Json + isCompleted Boolean + hasStarted Boolean? + isArchived Boolean? @default(false) + lastChatSessionId String? + typebot Typebot @relation(fields: [typebotId], references: [id], onDelete: Cascade) + answers Answer[] + answersV2 AnswerV2[] + logs Log[] + edges VisitedEdge[] + setVariableHistory SetVariableHistoryItem[] @@index([typebotId, hasStarted, createdAt(sort: Desc)]) @@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 { result Result @relation(fields: [resultId], references: [id], onDelete: Cascade) resultId String @@ -274,19 +288,25 @@ model Log { } model Answer { - createdAt DateTime @default(now()) @updatedAt - resultId String - blockId String - itemId String? - groupId String - variableId String? - content String - storageUsed Int? - result Result @relation(fields: [resultId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) @updatedAt + resultId String + blockId String + groupId String + variableId String? + content String + result Result @relation(fields: [resultId], references: [id], onDelete: Cascade) @@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 { diff --git a/packages/results/archiveResults.ts b/packages/results/archiveResults.ts index b19db6b9fd..99f10b229a 100644 --- a/packages/results/archiveResults.ts +++ b/packages/results/archiveResults.ts @@ -2,6 +2,7 @@ import { Prisma, PrismaClient } from '@typebot.io/prisma' import { Block, Typebot } from '@typebot.io/schemas' import { deleteFilesFromBucket } from '@typebot.io/lib/s3/deleteFilesFromBucket' import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants' +import { isDefined } from '@typebot.io/lib' type ArchiveResultsProps = { typebot: Pick @@ -42,6 +43,7 @@ export const archiveResults = }, select: { id: true, + lastChatSessionId: true, }, take: batchSize, }) @@ -76,6 +78,30 @@ export const archiveResults = 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({ where: { id: { in: resultIds }, diff --git a/packages/results/convertResultsToTableData.ts b/packages/results/convertResultsToTableData.ts index 57153903dc..0746eb10d7 100644 --- a/packages/results/convertResultsToTableData.ts +++ b/packages/results/convertResultsToTableData.ts @@ -24,11 +24,19 @@ const defaultCellParser: CellParser = (content, blockType) => { : { plainText: content.toString() } } -export const convertResultsToTableData = ( - results: ResultWithAnswers[] | undefined, - headerCells: ResultHeaderCell[], - cellParser: CellParser = defaultCellParser -): TableData[] => +type Props = { + results: ResultWithAnswers[] | undefined + headerCells: ResultHeaderCell[] + cellParser?: CellParser + blockIdVariableIdMap: Record +} + +export const convertResultsToTableData = ({ + results, + headerCells, + cellParser = defaultCellParser, + blockIdVariableIdMap, +}: Props): TableData[] => (results ?? []).map((result) => ({ id: { plainText: result.id }, date: { @@ -37,23 +45,23 @@ export const convertResultsToTableData = ( ...[...result.answers, ...result.variables].reduce<{ [key: string]: { element?: JSX.Element; plainText: string } }>((tableData, answerOrVariable) => { - if ('groupId' in answerOrVariable) { - const answer = answerOrVariable satisfies Answer - const header = answer.variableId + if ('blockId' in answerOrVariable) { + const answer = answerOrVariable satisfies Pick< + Answer, + 'blockId' | 'content' + > + const answerVariableId = blockIdVariableIdMap[answer.blockId] + const header = answerVariableId ? headerCells.find((headerCell) => - headerCell.variableIds?.includes(answer.variableId as string) + headerCell.variableIds?.includes(answerVariableId) ) : headerCells.find((headerCell) => headerCell.blocks?.some((block) => block.id === answer.blockId) ) 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 { ...tableData, - [header.id]: cellParser(content, header.blockType), + [header.id]: cellParser(answer.content, header.blockType), } } const variable = answerOrVariable satisfies VariableWithValue diff --git a/packages/results/parseBlockIdVariableIdMap.ts b/packages/results/parseBlockIdVariableIdMap.ts new file mode 100644 index 0000000000..6559ee5263 --- /dev/null +++ b/packages/results/parseBlockIdVariableIdMap.ts @@ -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 +} diff --git a/packages/results/parseResultHeader.ts b/packages/results/parseResultHeader.ts index d88b9c28ef..44e9decb6a 100644 --- a/packages/results/parseResultHeader.ts +++ b/packages/results/parseResultHeader.ts @@ -35,7 +35,11 @@ export const parseResultHeader = ( { label: 'Submitted at', id: 'date' }, ...inputsResultHeader, ...parseVariablesHeaders(parsedVariables, inputsResultHeader), - ...parseResultsFromPreviousBotVersions(results ?? [], inputsResultHeader), + ...parseResultsFromPreviousBotVersions({ + results: results ?? [], + existingInputResultHeaders: inputsResultHeader, + groups: parsedGroups, + }), ] } @@ -176,19 +180,22 @@ const parseVariablesHeaders = ( return [...existingHeaders, newHeaderCell] }, []) -const parseResultsFromPreviousBotVersions = ( - results: ResultWithAnswers[], +const parseResultsFromPreviousBotVersions = ({ + results, + existingInputResultHeaders, + groups, +}: { + results: ResultWithAnswers[] existingInputResultHeaders: ResultHeaderCell[] -): ResultHeaderCell[] => + groups: Group[] +}): ResultHeaderCell[] => results .flatMap((result) => result.answers) .filter( (answer) => - !answer.variableId && existingInputResultHeaders.every( (header) => header.id !== answer.blockId - ) && - isNotEmpty(answer.content) + ) && isNotEmpty(answer.content) ) .reduce((existingHeaders, answer) => { if ( @@ -197,6 +204,10 @@ const parseResultsFromPreviousBotVersions = ( ) ) return existingHeaders + const groupId = + groups.find((group) => + group.blocks.some((block) => block.id === answer.blockId) + )?.id ?? '' return [ ...existingHeaders, { @@ -205,7 +216,7 @@ const parseResultsFromPreviousBotVersions = ( blocks: [ { id: answer.blockId, - groupId: answer.groupId, + groupId, }, ], blockType: InputBlockType.TEXT, diff --git a/packages/schemas/features/answer.ts b/packages/schemas/features/answer.ts index 5bbe50caa1..90dace7c09 100644 --- a/packages/schemas/features/answer.ts +++ b/packages/schemas/features/answer.ts @@ -1,27 +1,28 @@ 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(), resultId: z.string(), blockId: z.string(), groupId: z.string(), variableId: z.string().nullable(), content: z.string(), - storageUsed: z.number().nullable(), - // TO-DO: remove once itemId is removed from database schema -}) satisfies z.ZodType> +}) satisfies z.ZodType + +export const answerSchema = z.object({ + blockId: z.string(), + content: z.string(), +}) -export const answerInputSchema = answerSchema +export const answerInputSchema = answerV1Schema .omit({ createdAt: true, resultId: true, variableId: true, - storageUsed: true, }) .extend({ variableId: z.string().nullish(), - storageUsed: z.number().nullish(), }) satisfies z.ZodType export const statsSchema = z.object({ diff --git a/packages/schemas/features/blocks/logic/setVariable/constants.ts b/packages/schemas/features/blocks/logic/setVariable/constants.ts index 556eb87ece..bceb1a0fb5 100644 --- a/packages/schemas/features/blocks/logic/setVariable/constants.ts +++ b/packages/schemas/features/blocks/logic/setVariable/constants.ts @@ -5,6 +5,7 @@ export const valueTypes = [ 'Empty', 'Append value(s)', 'Environment name', + 'Transcript', 'User ID', 'Result ID', 'Now', @@ -20,6 +21,8 @@ export const valueTypes = [ export const hiddenTypes = ['Today', 'User ID'] as const +export const sessionOnlySetVariableOptions = ['Transcript'] as const + export const defaultSetVariableOptions = { type: 'Custom', isExecutedOnClient: false, diff --git a/packages/schemas/features/blocks/logic/setVariable/schema.ts b/packages/schemas/features/blocks/logic/setVariable/schema.ts index 8471b23b16..94dc36967a 100644 --- a/packages/schemas/features/blocks/logic/setVariable/schema.ts +++ b/packages/schemas/features/blocks/logic/setVariable/schema.ts @@ -21,6 +21,7 @@ const basicSetVariableOptionsSchema = baseOptions.extend({ 'Random ID', 'Phone number', 'Contact name', + 'Transcript', ]), }) diff --git a/packages/schemas/features/chat/schema.ts b/packages/schemas/features/chat/schema.ts index 114178b6a9..cee1f0e424 100644 --- a/packages/schemas/features/chat/schema.ts +++ b/packages/schemas/features/chat/schema.ts @@ -260,6 +260,12 @@ export const startPreviewChatInputSchema = z.object({ 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 diff --git a/packages/schemas/features/chat/sessionState.ts b/packages/schemas/features/chat/sessionState.ts index f5cdee408a..b8b8ff53b3 100644 --- a/packages/schemas/features/chat/sessionState.ts +++ b/packages/schemas/features/chat/sessionState.ts @@ -1,14 +1,9 @@ import { z } from '../../zod' import { answerSchema } from '../answer' -import { resultSchema } from '../result' +import { resultSchema, setVariableHistoryItemSchema } from '../result' import { typebotInSessionStateSchema, dynamicThemeSchema } from './shared' import { settingsSchema } from '../typebot/settings' - -const answerInSessionStateSchema = answerSchema.pick({ - content: true, - blockId: true, - variableId: true, -}) +import { isInputBlock } from '../../helpers' const answerInSessionStateSchemaV2 = z.object({ key: z.string(), @@ -23,7 +18,7 @@ const resultInSessionStateSchema = resultSchema }) .merge( z.object({ - answers: z.array(answerInSessionStateSchema), + answers: z.array(answerSchema), id: z.string().optional(), }) ) @@ -94,6 +89,23 @@ const sessionStateSchemaV3 = sessionStateSchemaV2 version: z.literal('3'), currentBlockId: 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 @@ -119,17 +131,27 @@ const migrateFromV1ToV2 = ( { typebot: state.typebot, resultId: state.result.id, - answers: state.result.answers.map((answer) => ({ - key: - (answer.variableId - ? state.typebot.variables.find( - (variable) => variable.id === answer.variableId - )?.name - : state.typebot.groups.find((group) => - group.blocks.find((block) => block.id === answer.blockId) - )?.title) ?? '', - value: answer.content, - })), + answers: state.result.answers.map((answer) => { + let answerVariableId: string | undefined + state.typebot.groups.forEach((group) => { + group.blocks.forEach((block) => { + if (isInputBlock(block) && block.id === answer.blockId) { + answerVariableId = block.options?.variableId + } + }) + }) + 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, edgeIdToTriggerWhenDone: state.linkedTypebots.queue.length > 0 @@ -141,17 +163,27 @@ const migrateFromV1ToV2 = ( ({ typebot, resultId: state.result.id, - answers: state.result.answers.map((answer) => ({ - key: - (answer.variableId - ? state.typebot.variables.find( - (variable) => variable.id === answer.variableId - )?.name - : state.typebot.groups.find((group) => - group.blocks.find((block) => block.id === answer.blockId) - )?.title) ?? '', - value: answer.content, - })), + answers: state.result.answers.map((answer) => { + let answerVariableId: string | undefined + typebot.groups.forEach((group) => { + group.blocks.forEach((block) => { + if (isInputBlock(block) && block.id === answer.blockId) { + answerVariableId = block.options?.variableId + } + }) + }) + 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) ?.edgeId, } satisfies SessionState['typebotsQueue'][number]) diff --git a/packages/schemas/features/result.ts b/packages/schemas/features/result.ts index 009730d34a..e6a67e2f1b 100644 --- a/packages/schemas/features/result.ts +++ b/packages/schemas/features/result.ts @@ -1,9 +1,10 @@ import { z } from '../zod' import { answerInputSchema, answerSchema } from './answer' -import { variableWithValueSchema } from './typebot/variable' +import { listVariableValue, variableWithValueSchema } from './typebot/variable' import { Result as ResultPrisma, Log as LogPrisma, + SetVariableHistoryItem as SetVariableHistoryItemPrisma, VisitedEdge, } from '@typebot.io/prisma' import { InputBlockType } from './blocks/inputs/constants' @@ -16,6 +17,7 @@ export const resultSchema = z.object({ isCompleted: z.boolean(), hasStarted: z.boolean().nullable(), isArchived: z.boolean().nullable(), + lastChatSessionId: z.string().nullable(), }) satisfies z.ZodType export const resultWithAnswersSchema = resultSchema.merge( @@ -78,3 +80,14 @@ export type CellValueType = { element?: JSX.Element; plainText: string } export type TableData = { id: Pick } & Record + +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 +export type SetVariableHistoryItem = z.infer< + typeof setVariableHistoryItemSchema +> diff --git a/packages/scripts/exportResults.ts b/packages/scripts/exportResults.ts index 9cf7963da7..a13940f121 100644 --- a/packages/scripts/exportResults.ts +++ b/packages/scripts/exportResults.ts @@ -5,12 +5,13 @@ import cliProgress from 'cli-progress' import { writeFileSync } from 'fs' import { ResultWithAnswers, - Typebot, + TypebotV6, resultWithAnswersSchema, } from '@typebot.io/schemas' import { byId } from '@typebot.io/lib' import { parseResultHeader } from '@typebot.io/results/parseResultHeader' import { convertResultsToTableData } from '@typebot.io/results/convertResultsToTableData' +import { parseBlockIdVariableIdMap } from '@typebot.io/results/parseBlockIdVariableIdMap' import { parseColumnsOrder } from '@typebot.io/results/parseColumnsOrder' import { parseUniqueKey } from '@typebot.io/lib/parseUniqueKey' import { unparse } from 'papaparse' @@ -39,7 +40,7 @@ const exportResults = async () => { where: { id: typebotId, }, - })) as Typebot | null + })) as TypebotV6 | null if (!typebot) { console.log('No typebot found') @@ -61,19 +62,34 @@ const exportResults = async () => { for (let skip = 0; skip < totalResultsToExport; skip += 50) { results.push( ...z.array(resultWithAnswersSchema).parse( - await prisma.result.findMany({ - take: 50, - skip, - where: { - typebotId, - hasStarted: true, - isArchived: false, - }, - orderBy: { - createdAt: 'desc', - }, - include: { answers: true }, - }) + ( + await prisma.result.findMany({ + take: 50, + skip, + where: { + typebotId, + hasStarted: true, + isArchived: false, + }, + orderBy: { + createdAt: 'desc', + }, + 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) @@ -85,7 +101,11 @@ const exportResults = async () => { const resultHeader = parseResultHeader(typebot, []) - const dataToUnparse = convertResultsToTableData(results, resultHeader) + const dataToUnparse = convertResultsToTableData({ + results, + headerCells: resultHeader, + blockIdVariableIdMap: parseBlockIdVariableIdMap(typebot?.groups), + }) const headerIds = parseColumnsOrder( typebot?.resultsTablePreferences?.columnsOrder, diff --git a/packages/variables/package.json b/packages/variables/package.json index 91a5a0e92d..ef827e564b 100644 --- a/packages/variables/package.json +++ b/packages/variables/package.json @@ -4,6 +4,7 @@ "license": "AGPL-3.0-or-later", "private": true, "dependencies": { - "@typebot.io/lib": "workspace:*" + "@typebot.io/lib": "workspace:*", + "@typebot.io/tsconfig": "workspace:*" } } diff --git a/packages/variables/parseVariables.ts b/packages/variables/parseVariables.ts index 2b37023ed3..bdbc20cef5 100644 --- a/packages/variables/parseVariables.ts +++ b/packages/variables/parseVariables.ts @@ -95,6 +95,7 @@ type VariableToParseInformation = { endIndex: number textToReplace: string value: string + variableId?: string } export const getVariablesToParseInfoInText = ( @@ -146,6 +147,7 @@ export const getVariablesToParseInfoInText = ( ? variable?.value[variable?.value.length - 1] : variable?.value ) ?? '', + variableId: variable?.id, }) }) return variablesToParseInfo.sort((a, b) => a.startIndex - b.startIndex) diff --git a/packages/variables/types.ts b/packages/variables/types.ts index 415bbd9a89..9c72f0a1f2 100644 --- a/packages/variables/types.ts +++ b/packages/variables/types.ts @@ -2,12 +2,13 @@ export type Variable = { id: string name: string value?: string | (string | null)[] | null | undefined + isSessionVariable?: boolean } -export type VariableWithValue = Pick & { +export type VariableWithValue = Omit & { value: string | (string | null)[] } -export type VariableWithUnknowValue = Pick & { +export type VariableWithUnknowValue = Omit & { value?: unknown } diff --git a/packages/variables/updateVariablesInSession.ts b/packages/variables/updateVariablesInSession.ts index 6ec79c0d7c..ddc8dfb289 100644 --- a/packages/variables/updateVariablesInSession.ts +++ b/packages/variables/updateVariablesInSession.ts @@ -1,41 +1,102 @@ import { safeStringify } from '@typebot.io/lib/safeStringify' import { Variable, VariableWithUnknowValue } from './types' +import { SessionState, SetVariableHistoryItem } from '../schemas' -export const updateVariablesInSession = - (state: any) => (newVariables: VariableWithUnknowValue[]) => ({ - ...state, - typebotsQueue: state.typebotsQueue.map( - (typebotInQueue: { typebot: { variables: Variable[] } }, index: number) => +type Props = { + state: SessionState + newVariables: VariableWithUnknowValue[] + currentBlockId: string | undefined +} +export const updateVariablesInSession = ({ + state, + newVariables, + currentBlockId, +}: Props): { + updatedState: SessionState + newSetVariableHistory: SetVariableHistoryItem[] +} => { + const { updatedVariables, newSetVariableHistory, setVariableHistoryIndex } = + updateTypebotVariables({ + state, + newVariables, + currentBlockId, + }) + + return { + updatedState: { + ...state, + currentSetVariableHistoryIndex: setVariableHistoryIndex, + typebotsQueue: state.typebotsQueue.map((typebotInQueue, index: number) => index === 0 ? { ...typebotInQueue, typebot: { ...typebotInQueue.typebot, - variables: updateTypebotVariables(typebotInQueue.typebot)( - newVariables - ), + variables: updatedVariables, }, } : typebotInQueue - ), - }) + ), + previewMetadata: state.typebotsQueue[0].resultId + ? state.previewMetadata + : { + ...state.previewMetadata, + setVariableHistory: ( + state.previewMetadata?.setVariableHistory ?? [] + ).concat(newSetVariableHistory), + }, + }, + newSetVariableHistory, + } +} -const updateTypebotVariables = - (typebot: { variables: Variable[] }) => - (newVariables: VariableWithUnknowValue[]): Variable[] => { - const serializedNewVariables = newVariables.map((variable) => ({ - ...variable, - value: Array.isArray(variable.value) - ? variable.value.map(safeStringify) - : safeStringify(variable.value), - })) +const updateTypebotVariables = ({ + state, + newVariables, + currentBlockId, +}: { + state: SessionState + newVariables: VariableWithUnknowValue[] + currentBlockId: string | undefined +}): { + updatedVariables: Variable[] + newSetVariableHistory: SetVariableHistoryItem[] + setVariableHistoryIndex: number +} => { + const serializedNewVariables = newVariables.map((variable) => ({ + ...variable, + value: Array.isArray(variable.value) + ? variable.value.map(safeStringify) + : safeStringify(variable.value), + })) + + let setVariableHistoryIndex = state.currentSetVariableHistoryIndex ?? 0 + const setVariableHistory: SetVariableHistoryItem[] = [] + if (currentBlockId) { + serializedNewVariables + .filter((v) => state.setVariableIdsForHistory?.includes(v.id)) + .forEach((newVariable) => { + setVariableHistory.push({ + resultId: state.typebotsQueue[0].resultId as string, + index: setVariableHistoryIndex, + blockId: currentBlockId, + variableId: newVariable.id, + value: newVariable.value, + }) + setVariableHistoryIndex += 1 + }) + } - return [ - ...typebot.variables.filter((existingVariable) => + return { + updatedVariables: [ + ...state.typebotsQueue[0].typebot.variables.filter((existingVariable) => serializedNewVariables.every( (newVariable) => existingVariable.id !== newVariable.id ) ), ...serializedNewVariables, - ] + ], + newSetVariableHistory: setVariableHistory, + setVariableHistoryIndex, } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1d4de5c8e8..9438d08f5c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -772,6 +772,9 @@ importers: '@typebot.io/lib': specifier: workspace:* version: link:../lib + '@typebot.io/logic': + specifier: workspace:* + version: link:../logic '@typebot.io/prisma': specifier: workspace:* version: link:../prisma @@ -1737,6 +1740,25 @@ importers: specifier: 5.4.5 version: 5.4.5 + packages/logic: + dependencies: + '@typebot.io/lib': + specifier: workspace:* + version: link:../lib + '@typebot.io/schemas': + specifier: workspace:* + version: link:../schemas + '@typebot.io/variables': + specifier: workspace:* + version: link:../variables + devDependencies: + '@typebot.io/tsconfig': + specifier: workspace:* + version: link:../tsconfig + '@udecode/plate-common': + specifier: 30.4.5 + version: 30.4.5(@types/react@18.2.15)(immer@10.0.2)(react-dom@18.2.0)(react@18.2.0)(scheduler@0.23.0)(slate-history@0.100.0)(slate-hyperscript@0.100.0)(slate-react@0.102.0)(slate@0.102.0) + packages/migrations: dependencies: '@typebot.io/lib': @@ -1984,6 +2006,9 @@ importers: '@typebot.io/lib': specifier: workspace:* version: link:../lib + '@typebot.io/tsconfig': + specifier: workspace:* + version: link:../tsconfig packages: @@ -7160,7 +7185,6 @@ packages: /@juggle/resize-observer@3.4.0: resolution: {integrity: sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==} - dev: false /@ladle/react-context@1.0.1(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-xVQ8siyOEQG6e4Knibes1uA3PTyXnqiMmfSmd5pIbkzeDty8NCBtYHhTXSlfmcDNEsw/G8OzNWo4VbyQAVDl2A==} @@ -8090,7 +8114,6 @@ packages: '@babel/runtime': 7.24.0 '@types/react': 18.2.15 react: 18.2.0 - dev: false /@radix-ui/react-context@1.0.1(@types/react@18.2.15)(react@18.2.0): resolution: {integrity: sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==} @@ -8368,7 +8391,6 @@ packages: '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.15)(react@18.2.0) '@types/react': 18.2.15 react: 18.2.0 - dev: false /@radix-ui/react-toggle-group@1.0.4(@types/react-dom@18.2.15)(@types/react@18.2.15)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-Uaj/M/cMyiyT9Bx6fOZO0SAG4Cls0GptBWiBmBxofmDbNVnYYoyRWj/2M/6VCi/7qcXFWnHhRUfdfZFvvkuu8A==} @@ -9688,7 +9710,6 @@ packages: /@types/is-hotkey@0.1.10: resolution: {integrity: sha512-RvC8KMw5BCac1NvRRyaHgMMEtBaZ6wh0pyPTBu7izn4Sj/AX9Y4aXU5c7rX8PnM/knsuUpC1IeoBkANtxBypsQ==} - dev: false /@types/istanbul-lib-coverage@2.0.6: resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} @@ -9754,7 +9775,6 @@ packages: /@types/lodash@4.17.0: resolution: {integrity: sha512-t7dhREVv6dbNj0q17X12j7yDG4bD/DHYX7o5/DbDxobP0HnGPgpRz2Ej77aL7TZT3DSw13fqUTj8J4mMnqa7WA==} - dev: false /@types/mdast@3.0.15: resolution: {integrity: sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==} @@ -10311,7 +10331,6 @@ packages: - immer - react-native - scheduler - dev: false /@udecode/plate-core@30.4.5(@types/react@18.2.15)(immer@10.0.2)(react-dom@18.2.0)(react@18.2.0)(scheduler@0.23.0)(slate-history@0.100.0)(slate-hyperscript@0.100.0)(slate-react@0.102.0)(slate@0.102.0): resolution: {integrity: sha512-x/X0dCLoWFyC7wEI9hTcVMR8C/xiTkF0w9I5fyhCMg1mXz/y4DB0CMute+hYT0Wz7rqgj9DYT4v8ryrB9fEu9A==} @@ -10350,7 +10369,6 @@ packages: - immer - react-native - scheduler - dev: false /@udecode/plate-floating@30.5.3(@udecode/plate-common@30.4.5)(react-dom@18.2.0)(react@18.2.0)(slate-history@0.100.0)(slate-hyperscript@0.100.0)(slate-react@0.102.0)(slate@0.102.0): resolution: {integrity: sha512-9KxpZdKLy45a3Z+MJqSGmuJKQrl7CrNsLyUdjKD4Iqd1DIdBwl65dGqTmgI1EycF2jUsWIrgGE3W71f7E5/JdA==} @@ -10569,7 +10587,6 @@ packages: - immer - react-native - scheduler - dev: false /@udecode/react-utils@29.0.1(@types/react@18.2.15)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-+bFJFTDsWArFaC4AZFap0VdCvEbu5ZA16avj4xjjdBBho4TiHOZ7RMDliwTUetA3DOm5LG02dmZ1U4ORNC0m3w==} @@ -10584,7 +10601,6 @@ packages: react-dom: 18.2.0(react@18.2.0) transitivePeerDependencies: - '@types/react' - dev: false /@udecode/slate-react@29.0.1(@types/react@18.2.15)(react-dom@18.2.0)(react@18.2.0)(slate-history@0.100.0)(slate-react@0.102.0)(slate@0.102.0): resolution: {integrity: sha512-DOiGXxfL43tVyNg0LneTQGQBW/HkF2srwIM8b0Al/x082HHfo2PP2WkFqPqTh1uGUAa2RBRh9xFKmNkKeuyWSw==} @@ -10605,7 +10621,6 @@ packages: slate-react: 0.102.0(react-dom@18.2.0)(react@18.2.0)(slate@0.102.0) transitivePeerDependencies: - '@types/react' - dev: false /@udecode/slate-utils@25.0.0(slate-history@0.100.0)(slate@0.102.0): resolution: {integrity: sha512-H8dECl5Tu44Nt946rkSXCJ1yzsc2R9GXSoA9oNIBmcyNo3jTHZOyG/Ocn3RGgfzAK996A43GBD/keNabJEPtQg==} @@ -10618,7 +10633,6 @@ packages: lodash: 4.17.21 slate: 0.102.0 slate-history: 0.100.0(slate@0.102.0) - dev: false /@udecode/slate@25.0.0(slate-history@0.100.0)(slate@0.102.0): resolution: {integrity: sha512-mGb9nMDwIygLqERwJ8kTOfo3wIxMQ0xLJEPKn09jrshEIxUCyO3mYj8y/5vOMcrzj6yexOsgQ6VNX8ylS3lnIQ==} @@ -10629,11 +10643,9 @@ packages: '@udecode/utils': 24.3.0 slate: 0.102.0 slate-history: 0.100.0(slate@0.102.0) - dev: false /@udecode/utils@24.3.0: resolution: {integrity: sha512-/Y2lh/Ih1wx4zN35Ky2Z1G1/5f7cSAS7F6dkhrcbJUnDF0srTidoEIRabK+og/yIK/MCEFfOsQGetoV7Ert5hg==} - dev: false /@uiw/codemirror-extensions-basic-setup@4.21.24(@codemirror/autocomplete@6.14.0)(@codemirror/commands@6.3.3)(@codemirror/language@6.10.1)(@codemirror/lint@6.5.0)(@codemirror/search@6.5.6)(@codemirror/state@6.4.1)(@codemirror/view@6.25.1): resolution: {integrity: sha512-TJYKlPxNAVJNclW1EGumhC7I02jpdMgBon4jZvb5Aju9+tUzS44IwORxUx8BD8ZtH2UHmYS+04rE3kLk/BtnCQ==} @@ -12273,7 +12285,6 @@ packages: /clsx@1.2.1: resolution: {integrity: sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==} engines: {node: '>=6'} - dev: false /clsx@2.0.0: resolution: {integrity: sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==} @@ -12432,7 +12443,6 @@ packages: /compute-scroll-into-view@3.1.0: resolution: {integrity: sha512-rj8l8pD4bJ1nx+dAkMhV1xB5RuZEyVysfxJqB1pRchh1KVvwOv9b7CGB8ZfjTImVv2oF+sYMUkMZq6Na5Ftmbg==} - dev: false /concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -13102,7 +13112,6 @@ packages: /direction@1.0.4: resolution: {integrity: sha512-GYqKi1aH7PJXxdhTeZBFrg8vUBeKXi+cNprXsC1kpJcbcVnV9wBsrOu1cQEdG0WeQwlfHiy3XvnKfIrJ2R0NzQ==} hasBin: true - dev: false /dlv@1.1.3: resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} @@ -15508,11 +15517,9 @@ packages: /immer@10.0.2: resolution: {integrity: sha512-Rx3CqeqQ19sxUtYV9CU911Vhy8/721wRFnJv3REVGWUmoAcIwzifTsdmJte/MV+0/XpM35LZdQMBGkRIoLPwQA==} - dev: false /immer@10.0.4: resolution: {integrity: sha512-cuBuGK40P/sk5IzWa9QPUaAdvPHjkk1c+xYsd9oZw+YQQEV+10G0P5uMpGctZZKnyQ+ibRO08bD25nWLmYi2pw==} - dev: false /import-cwd@3.0.0: resolution: {integrity: sha512-4pnzH16plW+hgvRECbDWpQl3cqtvSofHWh44met7ESfZ8UZOWWddm8hEyDTqREJ9RbYHY8gi8DqmaelApoOGMg==} @@ -15841,7 +15848,6 @@ packages: /is-hotkey@0.2.0: resolution: {integrity: sha512-UknnZK4RakDmTgz4PI1wIph5yxSs/mvChWs9ifnlXsKuXgWmOkY/hAE0H/k2MIqH0RlRye0i1oC07MCRSD28Mw==} - dev: false /is-html@2.0.0: resolution: {integrity: sha512-S+OpgB5i7wzIue/YSE5hg0e5ZYfG3hhpNh9KGl6ayJ38p7ED6wxQLd1TV91xHpcTvw90KMJ9EwN3F/iNflHBVg==} @@ -16562,7 +16568,6 @@ packages: dependencies: jotai: 2.7.0(@types/react@18.2.15)(react@18.2.0) optics-ts: 2.4.1 - dev: false /jotai-x@1.2.2(@types/react@18.2.15)(jotai@2.7.0)(react@18.2.0): resolution: {integrity: sha512-HaFl3O4aKdBdeTyuzzcvnBWvicXkxl0DBINsqasqWrL7mZov4AAuXUSAsAY817UDwMe1+k77uBazUCFlaiyU3A==} @@ -16579,7 +16584,6 @@ packages: '@types/react': 18.2.15 jotai: 2.7.0(@types/react@18.2.15)(react@18.2.0) react: 18.2.0 - dev: false /jotai@2.7.0(@types/react@18.2.15)(react@18.2.0): resolution: {integrity: sha512-4qsyFKu4MprI39rj2uoItyhu24NoCHzkOV7z70PQr65SpzV6CSyhQvVIfbNlNqOIOspNMdf5OK+kTXLvqe63Jw==} @@ -16595,7 +16599,6 @@ packages: dependencies: '@types/react': 18.2.15 react: 18.2.0 - dev: false /joycon@3.1.1: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} @@ -16986,7 +16989,6 @@ packages: /lodash.mapvalues@4.6.0: resolution: {integrity: sha512-JPFqXFeZQ7BfS00H58kClY7SPVeHertPE0lNuCyZ26/XlN8TvakYD7b9bGyNmXbT/D3BbtPAAmq90gPWqLkxlQ==} - dev: false /lodash.memoize@4.1.2: resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} @@ -19003,7 +19005,6 @@ packages: /optics-ts@2.4.1: resolution: {integrity: sha512-HaYzMHvC80r7U/LqAd4hQyopDezC60PO2qF5GuIwALut2cl5rK1VWHsqTp0oqoJJWjiv6uXKqsO+Q2OO0C3MmQ==} - dev: false /optionator@0.9.3: resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==} @@ -19982,7 +19983,6 @@ packages: /proxy-compare@2.6.0: resolution: {integrity: sha512-8xuCeM3l8yqdmbPoYeLbrAXCBWu19XEYc5/F28f5qOaoAIMyfmBUkl5axiK+x9olUvRlcekvnm98AP9RDngOIw==} - dev: false /proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} @@ -20229,7 +20229,6 @@ packages: dependencies: react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - dev: false /react-inspector@6.0.2(react@18.2.0): resolution: {integrity: sha512-x+b7LxhmHXjHoU/VrFAzw5iutsILRoYyDq97EDYdFpPLcvqtEzk4ZSZSQjnFPbr5T57tLXnHcqFYoN1pI6u8uQ==} @@ -20399,7 +20398,6 @@ packages: react-dom: 18.2.0(react@18.2.0) scheduler: 0.23.0 use-context-selector: 1.4.4(react-dom@18.2.0)(react@18.2.0)(scheduler@0.23.0) - dev: false /react-transition-group@4.4.5(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==} @@ -20975,7 +20973,6 @@ packages: resolution: {integrity: sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==} dependencies: compute-scroll-into-view: 3.1.0 - dev: false /section-matter@1.0.0: resolution: {integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==} @@ -21190,7 +21187,6 @@ packages: dependencies: is-plain-object: 5.0.0 slate: 0.102.0 - dev: false /slate-hyperscript@0.100.0(slate@0.102.0): resolution: {integrity: sha512-fb2KdAYg6RkrQGlqaIi4wdqz3oa0S4zKNBJlbnJbNOwa23+9FLD6oPVx9zUGqCSIpy+HIpOeqXrg0Kzwh/Ii4A==} @@ -21199,7 +21195,6 @@ packages: dependencies: is-plain-object: 5.0.0 slate: 0.102.0 - dev: false /slate-react@0.102.0(react-dom@18.2.0)(react@18.2.0)(slate@0.102.0): resolution: {integrity: sha512-SAcFsK5qaOxXjm0hr/t2pvIxfRv6HJGzmWkG58TdH4LdJCsgKS1n6hQOakHPlRVCwPgwvngB6R+t3pPjv8MqwA==} @@ -21220,7 +21215,6 @@ packages: scroll-into-view-if-needed: 3.1.0 slate: 0.102.0 tiny-invariant: 1.3.1 - dev: false /slate@0.102.0: resolution: {integrity: sha512-RT+tHgqOyZVB1oFV9Pv99ajwh4OUCN9p28QWdnDTIzaN/kZxMsHeQN39UNAgtkZTVVVygFqeg7/R2jiptCvfyA==} @@ -21228,7 +21222,6 @@ packages: immer: 10.0.4 is-plain-object: 5.0.0 tiny-warning: 1.0.3 - dev: false /slice-ansi@1.0.0: resolution: {integrity: sha512-POqxBK6Lb3q6s047D/XsDVNPnF9Dl8JSaqe9h9lURl0OdNqy/ujDrOiIHtsqXMGbWWTIomRzAMaTyawAU//Reg==} @@ -22073,7 +22066,6 @@ packages: /tiny-invariant@1.3.1: resolution: {integrity: sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==} - dev: false /tiny-invariant@1.3.3: resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} @@ -22081,7 +22073,6 @@ packages: /tiny-warning@1.0.3: resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==} - dev: false /tinycolor2@1.6.0: resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==} @@ -22824,7 +22815,6 @@ packages: react: 18.2.0 react-dom: 18.2.0(react@18.2.0) scheduler: 0.23.0 - dev: false /use-debounce@9.0.4(react@18.2.0): resolution: {integrity: sha512-6X8H/mikbrt0XE8e+JXRtZ8yYVvKkdYRfmIhWZYsP8rcNs9hk3APV8Ua2mFkKRLcJKVdnX2/Vwrmg2GWKUQEaQ==} @@ -22842,7 +22832,6 @@ packages: dependencies: dequal: 2.0.3 react: 18.2.0 - dev: false /use-sidecar@1.1.2(@types/react@18.2.15)(react@18.2.0): resolution: {integrity: sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==} @@ -22866,7 +22855,6 @@ packages: react: ^16.8.0 || ^17.0.0 || ^18.0.0 dependencies: react: 18.2.0 - dev: false /util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -23519,7 +23507,6 @@ packages: - react-dom - react-native - scheduler - dev: false /zustand@4.5.0(@types/react@18.2.15)(immer@10.0.2)(react@18.2.0): resolution: {integrity: sha512-zlVFqS5TQ21nwijjhJlx4f9iGrXSL0o/+Dpy4txAP22miJ8Ti6c1Ol1RLNN98BMib83lmDH/2KmLwaNXpjrO1A==} @@ -23540,7 +23527,6 @@ packages: immer: 10.0.2 react: 18.2.0 use-sync-external-store: 1.2.0(react@18.2.0) - dev: false /zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}