2
0

🐛 New sendMessage version for the new parser

Make sure old client still communicate with old parser
This commit is contained in:
Baptiste Arnaud
2023-10-06 10:14:26 +02:00
parent 6f3e9e9251
commit 3838ac9c3f
35 changed files with 710 additions and 416 deletions

View File

@ -142,8 +142,8 @@ test.describe.parallel('Google sheets integration', () => {
.locator('input[placeholder="Type your email..."]') .locator('input[placeholder="Type your email..."]')
.press('Enter') .press('Enter')
await expect( await expect(
page.locator('typebot-standard').locator('text=Your name is:') page.locator('typebot-standard').locator('text=Georges2')
).toHaveText(`Your name is: Georges2 Last name`) ).toBeVisible()
}) })
}) })

View File

@ -62,7 +62,7 @@ export const ApiPreviewInstructions = (props: StackProps) => {
lang={'shell'} lang={'shell'}
value={`${parseApiHost( value={`${parseApiHost(
typebot?.customDomain typebot?.customDomain
)}/api/v1/sendMessage`} )}/api/v2/sendMessage`}
/> />
<Text>with the following JSON body:</Text> <Text>with the following JSON body:</Text>
<CodeEditor isReadOnly lang={'json'} value={startParamsBody} /> <CodeEditor isReadOnly lang={'json'} value={startParamsBody} />
@ -82,7 +82,7 @@ export const ApiPreviewInstructions = (props: StackProps) => {
lang={'shell'} lang={'shell'}
value={`${parseApiHost( value={`${parseApiHost(
typebot?.customDomain typebot?.customDomain
)}/api/v1/sendMessage`} )}/api/v2/sendMessage`}
/> />
<Text>With the following JSON body:</Text> <Text>With the following JSON body:</Text>
<CodeEditor isReadOnly lang={'json'} value={replyBody} /> <CodeEditor isReadOnly lang={'json'} value={replyBody} />

View File

@ -61,7 +61,7 @@ export const ApiModal = ({
lang={'shell'} lang={'shell'}
value={`${parseApiHost( value={`${parseApiHost(
typebot?.customDomain typebot?.customDomain
)}/api/v1/sendMessage`} )}/api/v2/sendMessage`}
/> />
<Text>with the following JSON body:</Text> <Text>with the following JSON body:</Text>
<CodeEditor isReadOnly lang={'json'} value={startParamsBody} /> <CodeEditor isReadOnly lang={'json'} value={startParamsBody} />
@ -81,7 +81,7 @@ export const ApiModal = ({
lang={'shell'} lang={'shell'}
value={`${parseApiHost( value={`${parseApiHost(
typebot?.customDomain typebot?.customDomain
)}/api/v1/sendMessage`} )}/api/v2/sendMessage`}
/> />
<Text>With the following JSON body:</Text> <Text>With the following JSON body:</Text>
<CodeEditor isReadOnly lang={'json'} value={replyBody} /> <CodeEditor isReadOnly lang={'json'} value={replyBody} />

View File

@ -43,7 +43,7 @@ export const parseReactBotProps = ({ typebot, apiHost }: BotProps) => {
} }
export const typebotImportCode = isCloudProdInstance() export const typebotImportCode = isCloudProdInstance()
? `import Typebot from 'https://cdn.jsdelivr.net/npm/@typebot.io/js@0.1/dist/web.js'` ? `import Typebot from 'https://cdn.jsdelivr.net/npm/@typebot.io/js@0.2/dist/web.js'`
: `import Typebot from 'https://cdn.jsdelivr.net/npm/@typebot.io/js@${packageJson.version}/dist/web.js'` : `import Typebot from 'https://cdn.jsdelivr.net/npm/@typebot.io/js@${packageJson.version}/dist/web.js'`
export const parseInlineScript = (script: string) => export const parseInlineScript = (script: string) =>

View File

@ -91,6 +91,7 @@ export const startWhatsAppPreview = authenticatedProcedure
const { newSessionState, messages, input, clientSideActions, logs } = const { newSessionState, messages, input, clientSideActions, logs } =
await startSession({ await startSession({
version: 2,
message: undefined, message: undefined,
startParams: { startParams: {
isOnlyRegistering: !canSendDirectMessagesToUser, isOnlyRegistering: !canSendDirectMessagesToUser,

View File

@ -12,7 +12,7 @@ There, you can change the container dimensions. Here is a code example:
```html ```html
<script type="module"> <script type="module">
import Typebot from 'https://cdn.jsdelivr.net/npm/@typebot.io/js@0.1/dist/web.js' import Typebot from 'https://cdn.jsdelivr.net/npm/@typebot.io/js@0.2/dist/web.js'
Typebot.initStandard({ Typebot.initStandard({
typebot: 'my-typebot', typebot: 'my-typebot',
@ -32,7 +32,7 @@ Here is an example:
```html ```html
<script type="module"> <script type="module">
import Typebot from 'https://cdn.jsdelivr.net/npm/@typebot.io/js@0.1/dist/web.js' import Typebot from 'https://cdn.jsdelivr.net/npm/@typebot.io/js@0.2/dist/web.js'
Typebot.initPopup({ Typebot.initPopup({
typebot: 'my-typebot', typebot: 'my-typebot',
@ -72,7 +72,7 @@ If you have different bots on the same page you will have to make them distinct
```html ```html
<script type="module"> <script type="module">
import Typebot from 'https://cdn.jsdelivr.net/npm/@typebot.io/js@0.1/dist/web.js' import Typebot from 'https://cdn.jsdelivr.net/npm/@typebot.io/js@0.2/dist/web.js'
Typebot.initStandard({ Typebot.initStandard({
id: 'bot1' id: 'bot1'
@ -104,7 +104,7 @@ Here is an example:
```html ```html
<script type="module"> <script type="module">
import Typebot from 'https://cdn.jsdelivr.net/npm/@typebot.io/js@0.1/dist/web.js' import Typebot from 'https://cdn.jsdelivr.net/npm/@typebot.io/js@0.2/dist/web.js'
Typebot.initBubble({ Typebot.initBubble({
typebot: 'my-typebot', typebot: 'my-typebot',

View File

@ -22,7 +22,7 @@ It should look like:
```html ```html
<script type="module"> <script type="module">
import Typebot from 'https://cdn.jsdelivr.net/npm/@typebot.io/js@0.1/dist/web.js' import Typebot from 'https://cdn.jsdelivr.net/npm/@typebot.io/js@0.2/dist/web.js'
Typebot.initPopup({ Typebot.initPopup({
typebot: 'my-typebot', typebot: 'my-typebot',

View File

@ -2,17 +2,17 @@
"openapi": "3.0.3", "openapi": "3.0.3",
"info": { "info": {
"title": "Chat API", "title": "Chat API",
"version": "1.0.0" "version": "2.0.0"
}, },
"servers": [ "servers": [
{ {
"url": "https://typebot.io/api/v1" "url": "https://typebot.io/api/v2"
} }
], ],
"paths": { "paths": {
"/sendMessage": { "/sendMessage": {
"post": { "post": {
"operationId": "sendMessage", "operationId": "sendMessageV2",
"summary": "Send a message", "summary": "Send a message",
"description": "To initiate a chat, do not provide a `sessionId` nor a `message`.\n\nContinue the conversation by providing the `sessionId` and the `message` that should answer the previous question.\n\nSet the `isPreview` option to `true` to chat with the non-published version of the typebot.", "description": "To initiate a chat, do not provide a `sessionId` nor a `message`.\n\nContinue the conversation by providing the `sessionId` and the `message` that should answer the previous question.\n\nSet the `isPreview` option to `true` to chat with the non-published version of the typebot.",
"requestBody": { "requestBody": {

View File

@ -12,7 +12,7 @@ import { continueBotFlow } from '@typebot.io/bot-engine/continueBotFlow'
import { parseDynamicTheme } from '@typebot.io/bot-engine/parseDynamicTheme' import { parseDynamicTheme } from '@typebot.io/bot-engine/parseDynamicTheme'
import { isDefined } from '@typebot.io/lib/utils' import { isDefined } from '@typebot.io/lib/utils'
export const sendMessage = publicProcedure export const sendMessageV1 = publicProcedure
.meta({ .meta({
openapi: { openapi: {
method: 'POST', method: 'POST',
@ -57,7 +57,12 @@ export const sendMessage = publicProcedure
logs, logs,
clientSideActions, clientSideActions,
newSessionState, newSessionState,
} = await startSession({ startParams, userId: user?.id, message }) } = await startSession({
version: 1,
startParams,
userId: user?.id,
message,
})
const allLogs = clientLogs ? [...(logs ?? []), ...clientLogs] : logs const allLogs = clientLogs ? [...(logs ?? []), ...clientLogs] : logs
@ -98,7 +103,7 @@ export const sendMessage = publicProcedure
newSessionState, newSessionState,
logs, logs,
lastMessageNewFormat, lastMessageNewFormat,
} = await continueBotFlow(session.state)(message) } = await continueBotFlow(message, { version: 1, state: session.state })
const allLogs = clientLogs ? [...(logs ?? []), ...clientLogs] : logs const allLogs = clientLogs ? [...(logs ?? []), ...clientLogs] : logs

View File

@ -0,0 +1,131 @@
import { publicProcedure } from '@/helpers/server/trpc'
import {
chatReplySchema,
sendMessageInputSchema,
} from '@typebot.io/schemas/features/chat/schema'
import { TRPCError } from '@trpc/server'
import { getSession } from '@typebot.io/bot-engine/queries/getSession'
import { startSession } from '@typebot.io/bot-engine/startSession'
import { saveStateToDatabase } from '@typebot.io/bot-engine/saveStateToDatabase'
import { restartSession } from '@typebot.io/bot-engine/queries/restartSession'
import { continueBotFlow } from '@typebot.io/bot-engine/continueBotFlow'
import { parseDynamicTheme } from '@typebot.io/bot-engine/parseDynamicTheme'
import { isDefined } from '@typebot.io/lib/utils'
export const sendMessageV2 = publicProcedure
.meta({
openapi: {
method: 'POST',
path: '/sendMessage',
summary: 'Send a message',
description:
'To initiate a chat, do not provide a `sessionId` nor a `message`.\n\nContinue the conversation by providing the `sessionId` and the `message` that should answer the previous question.\n\nSet the `isPreview` option to `true` to chat with the non-published version of the typebot.',
},
})
.input(sendMessageInputSchema)
.output(chatReplySchema)
.mutation(
async ({
input: { sessionId, message, startParams, clientLogs },
ctx: { user },
}) => {
const session = sessionId ? await getSession(sessionId) : null
const isSessionExpired =
session &&
isDefined(session.state.expiryTimeout) &&
session.updatedAt.getTime() + session.state.expiryTimeout < Date.now()
if (isSessionExpired)
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Session expired. You need to start a new session.',
})
if (!session) {
if (!startParams)
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'Missing startParams',
})
const {
typebot,
messages,
input,
resultId,
dynamicTheme,
logs,
clientSideActions,
newSessionState,
} = await startSession({
version: 2,
startParams,
userId: user?.id,
message,
})
const allLogs = clientLogs ? [...(logs ?? []), ...clientLogs] : logs
const session = startParams?.isOnlyRegistering
? await restartSession({
state: newSessionState,
})
: await saveStateToDatabase({
session: {
state: newSessionState,
},
input,
logs: allLogs,
clientSideActions,
})
return {
sessionId: session.id,
typebot: typebot
? {
id: typebot.id,
theme: typebot.theme,
settings: typebot.settings,
}
: undefined,
messages,
input,
resultId,
dynamicTheme,
logs,
clientSideActions,
}
} else {
const {
messages,
input,
clientSideActions,
newSessionState,
logs,
lastMessageNewFormat,
} = await continueBotFlow(message, { version: 2, state: session.state })
const allLogs = clientLogs ? [...(logs ?? []), ...clientLogs] : logs
if (newSessionState)
await saveStateToDatabase({
session: {
id: session.id,
state: newSessionState,
},
input,
logs: allLogs,
clientSideActions,
})
return {
messages,
input,
clientSideActions,
dynamicTheme: parseDynamicTheme(newSessionState),
logs,
lastMessageNewFormat,
}
}
}
)

View File

@ -1,11 +1,11 @@
import { generateOpenApiDocument } from 'trpc-openapi' import { generateOpenApiDocument } from 'trpc-openapi'
import { writeFileSync } from 'fs' import { writeFileSync } from 'fs'
import { appRouter } from './routers/v1/_app' import { appRouter } from './routers/appRouterV2'
const openApiDocument = generateOpenApiDocument(appRouter, { const openApiDocument = generateOpenApiDocument(appRouter, {
title: 'Chat API', title: 'Chat API',
version: '1.0.0', version: '2.0.0',
baseUrl: 'https://typebot.io/api/v1', baseUrl: 'https://typebot.io/api/v2',
docsUrl: 'https://docs.typebot.io/api', docsUrl: 'https://docs.typebot.io/api',
}) })

View File

@ -1,12 +1,12 @@
import { sendMessage } from '@/features/chat/api/sendMessage' import { sendMessageV1 } from '@/features/chat/api/sendMessageV1'
import { whatsAppRouter } from '@/features/whatsapp/api/router' import { whatsAppRouter } from '@/features/whatsapp/api/router'
import { router } from '../../trpc' import { router } from '../trpc'
import { updateTypebotInSession } from '@/features/chat/api/updateTypebotInSession' import { updateTypebotInSession } from '@/features/chat/api/updateTypebotInSession'
import { getUploadUrl } from '@/features/fileUpload/api/deprecated/getUploadUrl' import { getUploadUrl } from '@/features/fileUpload/api/deprecated/getUploadUrl'
import { generateUploadUrl } from '@/features/fileUpload/api/generateUploadUrl' import { generateUploadUrl } from '@/features/fileUpload/api/generateUploadUrl'
export const appRouter = router({ export const appRouter = router({
sendMessage, sendMessageV1,
getUploadUrl, getUploadUrl,
generateUploadUrl, generateUploadUrl,
updateTypebotInSession, updateTypebotInSession,

View File

@ -0,0 +1,16 @@
import { sendMessageV2 } from '@/features/chat/api/sendMessageV2'
import { whatsAppRouter } from '@/features/whatsapp/api/router'
import { router } from '../trpc'
import { updateTypebotInSession } from '@/features/chat/api/updateTypebotInSession'
import { getUploadUrl } from '@/features/fileUpload/api/deprecated/getUploadUrl'
import { generateUploadUrl } from '@/features/fileUpload/api/generateUploadUrl'
export const appRouter = router({
sendMessageV2,
getUploadUrl,
generateUploadUrl,
updateTypebotInSession,
whatsAppRouter,
})
export type AppRouter = typeof appRouter

View File

@ -1,4 +1,4 @@
import { appRouter } from '@/helpers/server/routers/v1/_app' import { appRouter } from '@/helpers/server/routers/appRouterV1'
import { captureException } from '@sentry/nextjs' import { captureException } from '@sentry/nextjs'
import { createOpenApiNextHandler } from 'trpc-openapi' import { createOpenApiNextHandler } from 'trpc-openapi'
import cors from 'nextjs-cors' import cors from 'nextjs-cors'

View File

@ -0,0 +1,23 @@
import { appRouter } from '@/helpers/server/routers/appRouterV2'
import { captureException } from '@sentry/nextjs'
import { createOpenApiNextHandler } from 'trpc-openapi'
import cors from 'nextjs-cors'
import { NextApiRequest, NextApiResponse } from 'next'
import { createContext } from '@/helpers/server/context'
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
await cors(req, res)
return createOpenApiNextHandler({
router: appRouter,
createContext,
onError({ error }) {
if (error.code === 'INTERNAL_SERVER_ERROR') {
captureException(error)
console.error('Something went wrong', error)
}
},
})(req, res)
}
export default handler

View File

@ -42,7 +42,7 @@ test('API chat execution should work on preview bot', async ({ request }) => {
await test.step('Start the chat', async () => { await test.step('Start the chat', async () => {
const { sessionId, messages, input, resultId } = await ( const { sessionId, messages, input, resultId } = await (
await request.post(`/api/v1/sendMessage`, { await request.post(`/api/v2/sendMessage`, {
data: { data: {
startParams: { startParams: {
typebot: typebotId, typebot: typebotId,
@ -83,7 +83,7 @@ test('API chat execution should work on published bot', async ({ request }) => {
await test.step('Start the chat', async () => { await test.step('Start the chat', async () => {
const { sessionId, messages, input, resultId } = await ( const { sessionId, messages, input, resultId } = await (
await request.post(`/api/v1/sendMessage`, { await request.post(`/api/v2/sendMessage`, {
data: { data: {
startParams: { startParams: {
typebot: publicId, typebot: publicId,
@ -111,12 +111,30 @@ test('API chat execution should work on published bot', async ({ request }) => {
await test.step('Answer Name question', async () => { await test.step('Answer Name question', async () => {
const { messages, input } = await ( const { messages, input } = await (
await request.post(`/api/v1/sendMessage`, { await request.post(`/api/v2/sendMessage`, {
data: { message: 'John', sessionId: chatSessionId }, data: { message: 'John', sessionId: chatSessionId },
}) })
).json() ).json()
expect(messages[0].content.richText).toStrictEqual([ expect(messages[0].content.richText).toStrictEqual([
{ children: [{ text: 'Nice to meet you John' }], type: 'p' }, {
type: 'p',
children: [
{ text: 'Nice to meet you ' },
{
type: 'inline-variable',
children: [
{
type: 'p',
children: [
{
text: 'John',
},
],
},
],
},
],
},
]) ])
expect(messages[1].content.url).toMatch(new RegExp('giphy.com', 'gm')) expect(messages[1].content.url).toMatch(new RegExp('giphy.com', 'gm'))
expect(input.type).toBe('number input') expect(input.type).toBe('number input')
@ -124,7 +142,7 @@ test('API chat execution should work on published bot', async ({ request }) => {
await test.step('Answer Age question', async () => { await test.step('Answer Age question', async () => {
const { messages, input } = await ( const { messages, input } = await (
await request.post(`/api/v1/sendMessage`, { await request.post(`/api/v2/sendMessage`, {
data: { message: '24', sessionId: chatSessionId }, data: { message: '24', sessionId: chatSessionId },
}) })
).json() ).json()
@ -132,7 +150,25 @@ test('API chat execution should work on published bot', async ({ request }) => {
{ children: [{ text: 'Ok, you are an adult then 😁' }], type: 'p' }, { children: [{ text: 'Ok, you are an adult then 😁' }], type: 'p' },
]) ])
expect(messages[1].content.richText).toStrictEqual([ expect(messages[1].content.richText).toStrictEqual([
{ children: [{ text: 'My magic number is 42' }], type: 'p' }, {
children: [
{ text: 'My magic number is ' },
{
type: 'inline-variable',
children: [
{
type: 'p',
children: [
{
text: '42',
},
],
},
],
},
],
type: 'p',
},
]) ])
expect(messages[2].content.richText).toStrictEqual([ expect(messages[2].content.richText).toStrictEqual([
{ {
@ -145,7 +181,7 @@ test('API chat execution should work on published bot', async ({ request }) => {
await test.step('Answer Rating question', async () => { await test.step('Answer Rating question', async () => {
const { messages, input } = await ( const { messages, input } = await (
await request.post(`/api/v1/sendMessage`, { await request.post(`/api/v2/sendMessage`, {
data: { message: '8', sessionId: chatSessionId }, data: { message: '8', sessionId: chatSessionId },
}) })
).json() ).json()
@ -160,7 +196,7 @@ test('API chat execution should work on published bot', async ({ request }) => {
await test.step('Answer Email question with wrong input', async () => { await test.step('Answer Email question with wrong input', async () => {
const { messages, input } = await ( const { messages, input } = await (
await request.post(`/api/v1/sendMessage`, { await request.post(`/api/v2/sendMessage`, {
data: { message: 'invalid email', sessionId: chatSessionId }, data: { message: 'invalid email', sessionId: chatSessionId },
}) })
).json() ).json()
@ -179,7 +215,7 @@ test('API chat execution should work on published bot', async ({ request }) => {
await test.step('Answer Email question with valid input', async () => { await test.step('Answer Email question with valid input', async () => {
const { messages, input } = await ( const { messages, input } = await (
await request.post(`/api/v1/sendMessage`, { await request.post(`/api/v2/sendMessage`, {
data: { message: 'typebot@email.com', sessionId: chatSessionId }, data: { message: 'typebot@email.com', sessionId: chatSessionId },
}) })
).json() ).json()
@ -189,7 +225,7 @@ test('API chat execution should work on published bot', async ({ request }) => {
await test.step('Answer URL question', async () => { await test.step('Answer URL question', async () => {
const { messages, input } = await ( const { messages, input } = await (
await request.post(`/api/v1/sendMessage`, { await request.post(`/api/v2/sendMessage`, {
data: { message: 'https://typebot.io', sessionId: chatSessionId }, data: { message: 'https://typebot.io', sessionId: chatSessionId },
}) })
).json() ).json()
@ -199,7 +235,7 @@ test('API chat execution should work on published bot', async ({ request }) => {
await test.step('Answer Buttons question with invalid choice', async () => { await test.step('Answer Buttons question with invalid choice', async () => {
const { messages } = await ( const { messages } = await (
await request.post(`/api/v1/sendMessage`, { await request.post(`/api/v2/sendMessage`, {
data: { message: 'Yes', sessionId: chatSessionId }, data: { message: 'Yes', sessionId: chatSessionId },
}) })
).json() ).json()
@ -227,7 +263,7 @@ test('API chat execution should work on published bot', async ({ request }) => {
}) })
await test.step('Starting with a message when typebot starts with input should proceed', async () => { await test.step('Starting with a message when typebot starts with input should proceed', async () => {
const { messages } = await ( const { messages } = await (
await request.post(`/api/v1/sendMessage`, { await request.post(`/api/v2/sendMessage`, {
data: { data: {
message: 'Hey', message: 'Hey',
startParams: { startParams: {

View File

@ -12,7 +12,7 @@ test('should correctly be injected', async ({ page }) => {
await page.goto(`/${typebotId}-public`) await page.goto(`/${typebotId}-public`)
await expect(page.locator('text="Your name is"')).toBeVisible() await expect(page.locator('text="Your name is"')).toBeVisible()
await page.goto(`/${typebotId}-public?Name=Baptiste&Email=email@test.com`) await page.goto(`/${typebotId}-public?Name=Baptiste&Email=email@test.com`)
await expect(page.locator('text="Your name is Baptiste"')).toBeVisible() await expect(page.locator('text="Baptiste"')).toBeVisible()
await expect(page.getByPlaceholder('Type your email...')).toHaveValue( await expect(page.getByPlaceholder('Type your email...')).toHaveValue(
'email@test.com' 'email@test.com'
) )

View File

@ -1,11 +1,11 @@
import { generateOpenApiDocument } from 'trpc-openapi' import { generateOpenApiDocument } from 'trpc-openapi'
import { writeFileSync } from 'fs' import { writeFileSync } from 'fs'
import { appRouter } from '@/helpers/server/routers/v1/_app' import { appRouter } from '@/helpers/server/routers/appRouterV2'
const openApiDocument = generateOpenApiDocument(appRouter, { const openApiDocument = generateOpenApiDocument(appRouter, {
title: 'Chat API', title: 'Chat API',
version: '1.0.0', version: '2.0.0',
baseUrl: 'https://typebot.io/api/v1', baseUrl: 'https://typebot.io/api/v2',
docsUrl: 'https://docs.typebot.io/api', docsUrl: 'https://docs.typebot.io/api',
}) })

View File

@ -1,5 +1,8 @@
{ {
"functions": { "functions": {
"src/pages/api/v2/[...trpc].ts": {
"maxDuration": 150
},
"src/pages/api/v1/[...trpc].ts": { "src/pages/api/v1/[...trpc].ts": {
"maxDuration": 150 "maxDuration": 150
}, },

View File

@ -31,128 +31,102 @@ import { updateVariablesInSession } from './variables/updateVariablesInSession'
import { startBotFlow } from './startBotFlow' import { startBotFlow } from './startBotFlow'
import { TRPCError } from '@trpc/server' import { TRPCError } from '@trpc/server'
export const continueBotFlow = type Params = {
(state: SessionState) => version: 1 | 2
async ( state: SessionState
reply?: string }
): Promise<ChatReply & { newSessionState: SessionState }> => { export const continueBotFlow = async (
let newSessionState = { ...state } reply: string | undefined,
{ state, version }: Params
): Promise<ChatReply & { newSessionState: SessionState }> => {
let newSessionState = { ...state }
if (!newSessionState.currentBlock) return startBotFlow(state) if (!newSessionState.currentBlock) return startBotFlow({ state, version })
const group = state.typebotsQueue[0].typebot.groups.find( const group = state.typebotsQueue[0].typebot.groups.find(
(group) => group.id === state.currentBlock?.groupId (group) => group.id === state.currentBlock?.groupId
)
const blockIndex =
group?.blocks.findIndex(
(block) => block.id === state.currentBlock?.blockId
) ?? -1
const block = blockIndex >= 0 ? group?.blocks[blockIndex ?? 0] : null
if (!block || !group)
throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message: 'Group / block not found',
})
if (block.type === LogicBlockType.SET_VARIABLE) {
const existingVariable = state.typebotsQueue[0].typebot.variables.find(
byId(block.options.variableId)
) )
const blockIndex = if (existingVariable && reply) {
group?.blocks.findIndex( const newVariable = {
(block) => block.id === state.currentBlock?.blockId ...existingVariable,
) ?? -1 value: safeJsonParse(reply),
const block = blockIndex >= 0 ? group?.blocks[blockIndex ?? 0] : null
if (!block || !group)
throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message: 'Group / block not found',
})
if (block.type === LogicBlockType.SET_VARIABLE) {
const existingVariable = state.typebotsQueue[0].typebot.variables.find(
byId(block.options.variableId)
)
if (existingVariable && reply) {
const newVariable = {
...existingVariable,
value: safeJsonParse(reply),
}
newSessionState = updateVariablesInSession(state)([newVariable])
} }
} else if (reply && block.type === IntegrationBlockType.WEBHOOK) { newSessionState = updateVariablesInSession(state)([newVariable])
const result = resumeWebhookExecution({ }
state, } else if (reply && block.type === IntegrationBlockType.WEBHOOK) {
block, const result = resumeWebhookExecution({
response: JSON.parse(reply), state,
}) block,
if (result.newSessionState) newSessionState = result.newSessionState response: JSON.parse(reply),
} else if ( })
block.type === IntegrationBlockType.OPEN_AI && if (result.newSessionState) newSessionState = result.newSessionState
block.options.task === 'Create chat completion' } else if (
) { block.type === IntegrationBlockType.OPEN_AI &&
if (reply) { block.options.task === 'Create chat completion'
const result = await resumeChatCompletion(state, { ) {
options: block.options, if (reply) {
outgoingEdgeId: block.outgoingEdgeId, const result = await resumeChatCompletion(state, {
})(reply) options: block.options,
newSessionState = result.newSessionState outgoingEdgeId: block.outgoingEdgeId,
})(reply)
newSessionState = result.newSessionState
}
}
let formattedReply: string | undefined
if (isInputBlock(block)) {
const parsedReplyResult = parseReply(newSessionState)(reply, block)
if (parsedReplyResult.status === 'fail')
return {
...(await parseRetryMessage(newSessionState)(block)),
newSessionState,
} }
}
let formattedReply: string | undefined
if (isInputBlock(block)) {
const parsedReplyResult = parseReply(newSessionState)(reply, block)
if (parsedReplyResult.status === 'fail')
return {
...(await parseRetryMessage(newSessionState)(block)),
newSessionState,
}
formattedReply =
'reply' in parsedReplyResult ? parsedReplyResult.reply : undefined
const nextEdgeId = getOutgoingEdgeId(newSessionState)(
block,
formattedReply
)
const itemId = nextEdgeId
? newSessionState.typebotsQueue[0].typebot.edges.find(byId(nextEdgeId))
?.from.itemId
: undefined
newSessionState = await processAndSaveAnswer(
state,
block,
itemId
)(formattedReply)
}
const groupHasMoreBlocks = blockIndex < group.blocks.length - 1
formattedReply =
'reply' in parsedReplyResult ? parsedReplyResult.reply : undefined
const nextEdgeId = getOutgoingEdgeId(newSessionState)(block, formattedReply) const nextEdgeId = getOutgoingEdgeId(newSessionState)(block, formattedReply)
const itemId = nextEdgeId
? newSessionState.typebotsQueue[0].typebot.edges.find(byId(nextEdgeId))
?.from.itemId
: undefined
newSessionState = await processAndSaveAnswer(
state,
block,
itemId
)(formattedReply)
}
if (groupHasMoreBlocks && !nextEdgeId) { const groupHasMoreBlocks = blockIndex < group.blocks.length - 1
const chatReply = await executeGroup(newSessionState)({
const nextEdgeId = getOutgoingEdgeId(newSessionState)(block, formattedReply)
if (groupHasMoreBlocks && !nextEdgeId) {
const chatReply = await executeGroup(
{
...group, ...group,
blocks: group.blocks.slice(blockIndex + 1), blocks: group.blocks.slice(blockIndex + 1),
}) },
return { { version, state: newSessionState }
...chatReply, )
lastMessageNewFormat:
formattedReply !== reply ? formattedReply : undefined,
}
}
if (!nextEdgeId && state.typebotsQueue.length === 1)
return {
messages: [],
newSessionState,
lastMessageNewFormat:
formattedReply !== reply ? formattedReply : undefined,
}
const nextGroup = await getNextGroup(newSessionState)(nextEdgeId)
newSessionState = nextGroup.newSessionState
if (!nextGroup.group)
return {
messages: [],
newSessionState,
lastMessageNewFormat:
formattedReply !== reply ? formattedReply : undefined,
}
const chatReply = await executeGroup(newSessionState)(nextGroup.group)
return { return {
...chatReply, ...chatReply,
lastMessageNewFormat: lastMessageNewFormat:
@ -160,6 +134,37 @@ export const continueBotFlow =
} }
} }
if (!nextEdgeId && state.typebotsQueue.length === 1)
return {
messages: [],
newSessionState,
lastMessageNewFormat:
formattedReply !== reply ? formattedReply : undefined,
}
const nextGroup = await getNextGroup(newSessionState)(nextEdgeId)
newSessionState = nextGroup.newSessionState
if (!nextGroup.group)
return {
messages: [],
newSessionState,
lastMessageNewFormat:
formattedReply !== reply ? formattedReply : undefined,
}
const chatReply = await executeGroup(nextGroup.group, {
version,
state: newSessionState,
})
return {
...chatReply,
lastMessageNewFormat: formattedReply !== reply ? formattedReply : undefined,
}
}
const processAndSaveAnswer = const processAndSaveAnswer =
(state: SessionState, block: InputBlock, itemId?: string) => (state: SessionState, block: InputBlock, itemId?: string) =>
async (reply: string | undefined): Promise<SessionState> => { async (reply: string | undefined): Promise<SessionState> => {

View File

@ -1,17 +1,13 @@
import { import {
BubbleBlock,
BubbleBlockType,
ChatReply, ChatReply,
Group, Group,
InputBlock, InputBlock,
InputBlockType, InputBlockType,
RuntimeOptions, RuntimeOptions,
SessionState, SessionState,
Variable,
} from '@typebot.io/schemas' } from '@typebot.io/schemas'
import { import {
isBubbleBlock, isBubbleBlock,
isEmpty,
isInputBlock, isInputBlock,
isIntegrationBlock, isIntegrationBlock,
isLogicBlock, isLogicBlock,
@ -26,49 +22,85 @@ import { injectVariableValuesInPictureChoiceBlock } from './blocks/inputs/pictur
import { getPrefilledInputValue } from './getPrefilledValue' import { getPrefilledInputValue } from './getPrefilledValue'
import { parseDateInput } from './blocks/inputs/date/parseDateInput' import { parseDateInput } from './blocks/inputs/date/parseDateInput'
import { deepParseVariables } from './variables/deepParseVariables' import { deepParseVariables } from './variables/deepParseVariables'
import { parseVideoUrl } from '@typebot.io/lib/parseVideoUrl' import { parseBubbleBlock } from './parseBubbleBlock'
import { TDescendant, createPlateEditor } from '@udecode/plate-common'
import {
createDeserializeMdPlugin,
deserializeMd,
} from '@udecode/plate-serializer-md'
import { getVariablesToParseInfoInText } from './variables/parseVariables'
export const executeGroup = type ContextProps = {
( version: 1 | 2
state: SessionState, state: SessionState
currentReply?: ChatReply, currentReply?: ChatReply
currentLastBubbleId?: string currentLastBubbleId?: string
) => }
async (
group: Group
): Promise<ChatReply & { newSessionState: SessionState }> => {
const messages: ChatReply['messages'] = currentReply?.messages ?? []
let clientSideActions: ChatReply['clientSideActions'] =
currentReply?.clientSideActions
let logs: ChatReply['logs'] = currentReply?.logs
let nextEdgeId = null
let lastBubbleBlockId: string | undefined = currentLastBubbleId
let newSessionState = state export const executeGroup = async (
group: Group,
{ version, state, currentReply, currentLastBubbleId }: ContextProps
): Promise<ChatReply & { newSessionState: SessionState }> => {
const messages: ChatReply['messages'] = currentReply?.messages ?? []
let clientSideActions: ChatReply['clientSideActions'] =
currentReply?.clientSideActions
let logs: ChatReply['logs'] = currentReply?.logs
let nextEdgeId = null
let lastBubbleBlockId: string | undefined = currentLastBubbleId
for (const block of group.blocks) { let newSessionState = state
nextEdgeId = block.outgoingEdgeId
if (isBubbleBlock(block)) { for (const block of group.blocks) {
messages.push( nextEdgeId = block.outgoingEdgeId
parseBubbleBlock(newSessionState.typebotsQueue[0].typebot.variables)(
block if (isBubbleBlock(block)) {
) messages.push(
) parseBubbleBlock(block, {
lastBubbleBlockId = block.id version,
continue variables: newSessionState.typebotsQueue[0].typebot.variables,
})
)
lastBubbleBlockId = block.id
continue
}
if (isInputBlock(block))
return {
messages,
input: await parseInput(newSessionState)(block),
newSessionState: {
...newSessionState,
currentBlock: {
groupId: group.id,
blockId: block.id,
},
},
clientSideActions,
logs,
} }
const executionResponse = isLogicBlock(block)
? await executeLogic(newSessionState)(block)
: isIntegrationBlock(block)
? await executeIntegration(newSessionState)(block)
: null
if (isInputBlock(block)) if (!executionResponse) continue
if (executionResponse.logs)
logs = [...(logs ?? []), ...executionResponse.logs]
if (executionResponse.newSessionState)
newSessionState = executionResponse.newSessionState
if (
'clientSideActions' in executionResponse &&
executionResponse.clientSideActions
) {
clientSideActions = [
...(clientSideActions ?? []),
...executionResponse.clientSideActions.map((action) => ({
...action,
lastBubbleBlockId,
})),
]
if (
executionResponse.clientSideActions?.find(
(action) => action.expectsDedicatedReply
)
) {
return { return {
messages, messages,
input: await parseInput(newSessionState)(block),
newSessionState: { newSessionState: {
...newSessionState, ...newSessionState,
currentBlock: { currentBlock: {
@ -79,78 +111,38 @@ export const executeGroup =
clientSideActions, clientSideActions,
logs, logs,
} }
const executionResponse = isLogicBlock(block)
? await executeLogic(newSessionState)(block)
: isIntegrationBlock(block)
? await executeIntegration(newSessionState)(block)
: null
if (!executionResponse) continue
if (executionResponse.logs)
logs = [...(logs ?? []), ...executionResponse.logs]
if (executionResponse.newSessionState)
newSessionState = executionResponse.newSessionState
if (
'clientSideActions' in executionResponse &&
executionResponse.clientSideActions
) {
clientSideActions = [
...(clientSideActions ?? []),
...executionResponse.clientSideActions.map((action) => ({
...action,
lastBubbleBlockId,
})),
]
if (
executionResponse.clientSideActions?.find(
(action) => action.expectsDedicatedReply
)
) {
return {
messages,
newSessionState: {
...newSessionState,
currentBlock: {
groupId: group.id,
blockId: block.id,
},
},
clientSideActions,
logs,
}
}
}
if (executionResponse.outgoingEdgeId) {
nextEdgeId = executionResponse.outgoingEdgeId
break
} }
} }
if (!nextEdgeId && newSessionState.typebotsQueue.length === 1) if (executionResponse.outgoingEdgeId) {
return { messages, newSessionState, clientSideActions, logs } nextEdgeId = executionResponse.outgoingEdgeId
break
const nextGroup = await getNextGroup(newSessionState)(
nextEdgeId ?? undefined
)
newSessionState = nextGroup.newSessionState
if (!nextGroup.group) {
return { messages, newSessionState, clientSideActions, logs }
} }
return executeGroup(
newSessionState,
{
messages,
clientSideActions,
logs,
},
lastBubbleBlockId
)(nextGroup.group)
} }
if (!nextEdgeId && newSessionState.typebotsQueue.length === 1)
return { messages, newSessionState, clientSideActions, logs }
const nextGroup = await getNextGroup(newSessionState)(nextEdgeId ?? undefined)
newSessionState = nextGroup.newSessionState
if (!nextGroup.group) {
return { messages, newSessionState, clientSideActions, logs }
}
return executeGroup(nextGroup.group, {
version,
state: newSessionState,
currentReply: {
messages,
clientSideActions,
logs,
},
currentLastBubbleId: lastBubbleBlockId,
})
}
const computeRuntimeOptions = const computeRuntimeOptions =
(state: SessionState) => (state: SessionState) =>
(block: InputBlock): Promise<RuntimeOptions> | undefined => { (block: InputBlock): Promise<RuntimeOptions> | undefined => {
@ -161,136 +153,6 @@ const computeRuntimeOptions =
} }
} }
const parseBubbleBlock =
(variables: Variable[]) =>
(block: BubbleBlock): ChatReply['messages'][0] => {
switch (block.type) {
case BubbleBlockType.TEXT: {
return {
...block,
content: {
...block.content,
richText: parseVariablesInRichText(
block.content.richText,
variables
),
},
}
}
case BubbleBlockType.EMBED: {
const message = deepParseVariables(variables)(block)
return {
...message,
content: {
...message.content,
height:
typeof message.content.height === 'string'
? parseFloat(message.content.height)
: message.content.height,
},
}
}
case BubbleBlockType.VIDEO: {
const parsedContent = deepParseVariables(variables)(block.content)
return {
...block,
content: parsedContent.url ? parseVideoUrl(parsedContent.url) : {},
}
}
default:
return deepParseVariables(variables)(block)
}
}
const parseVariablesInRichText = (
elements: TDescendant[],
variables: Variable[]
): TDescendant[] => {
const parsedElements: TDescendant[] = []
for (const element of elements) {
if ('text' in element) {
const text = element.text as string
if (isEmpty(text)) {
parsedElements.push(element)
continue
}
const variablesInText = getVariablesToParseInfoInText(text, variables)
if (variablesInText.length === 0) {
parsedElements.push(element)
continue
}
for (const variableInText of variablesInText) {
const textBeforeVariable = text.substring(0, variableInText.startIndex)
const textAfterVariable = text.substring(variableInText.endIndex)
const isStandaloneElement =
isEmpty(textBeforeVariable) && isEmpty(textAfterVariable)
const variableElements = convertMarkdownToRichText(
isStandaloneElement
? variableInText.value
: variableInText.value.replace(/[\n]+/g, ' ')
)
const variableElementsWithStyling = variableElements.map(
(variableElement) => ({
...variableElement,
children: [
...(variableElement.children as TDescendant[]).map((child) => ({
...element,
...child,
})),
],
})
)
if (isStandaloneElement) {
parsedElements.push(...variableElementsWithStyling)
continue
}
const children: TDescendant[] = []
if (isNotEmpty(textBeforeVariable))
children.push({
...element,
text: textBeforeVariable,
})
children.push({
type: 'inline-variable',
children: variableElementsWithStyling,
})
if (isNotEmpty(textAfterVariable))
children.push({
...element,
text: textAfterVariable,
})
parsedElements.push(...children)
}
continue
}
const type =
element.children.length === 1 &&
'text' in element.children[0] &&
(element.children[0].text as string).startsWith('{{') &&
(element.children[0].text as string).endsWith('}}')
? 'variable'
: element.type
parsedElements.push({
...element,
type,
children: parseVariablesInRichText(
element.children as TDescendant[],
variables
),
})
}
return parsedElements
}
const convertMarkdownToRichText = (text: string): TDescendant[] => {
const plugins = [createDeserializeMdPlugin()]
//@ts-ignore
return deserializeMd(createPlateEditor({ plugins }), text)
}
export const parseInput = export const parseInput =
(state: SessionState) => (state: SessionState) =>
async (block: InputBlock): Promise<ChatReply['input']> => { async (block: InputBlock): Promise<ChatReply['input']> => {

View File

@ -0,0 +1,186 @@
import { parseVideoUrl } from '@typebot.io/lib/parseVideoUrl'
import {
BubbleBlock,
Variable,
ChatReply,
BubbleBlockType,
} from '@typebot.io/schemas'
import { deepParseVariables } from './variables/deepParseVariables'
import { isEmpty, isNotEmpty } from '@typebot.io/lib/utils'
import { getVariablesToParseInfoInText } from './variables/parseVariables'
import { TDescendant, createPlateEditor } from '@udecode/plate-common'
import {
createDeserializeMdPlugin,
deserializeMd,
} from '@udecode/plate-serializer-md'
type Params = {
version: 1 | 2
variables: Variable[]
}
export const parseBubbleBlock = (
block: BubbleBlock,
{ version, variables }: Params
): ChatReply['messages'][0] => {
switch (block.type) {
case BubbleBlockType.TEXT: {
if (version === 1)
return deepParseVariables(
variables,
{},
{ takeLatestIfList: true }
)(block)
return {
...block,
content: {
...block.content,
richText: parseVariablesInRichText(block.content.richText, {
variables,
takeLatestIfList: true,
}),
},
}
}
case BubbleBlockType.EMBED: {
const message = deepParseVariables(variables)(block)
return {
...message,
content: {
...message.content,
height:
typeof message.content.height === 'string'
? parseFloat(message.content.height)
: message.content.height,
},
}
}
case BubbleBlockType.VIDEO: {
const parsedContent = deepParseVariables(variables)(block.content)
return {
...block,
content: parsedContent.url ? parseVideoUrl(parsedContent.url) : {},
}
}
default:
return deepParseVariables(variables)(block)
}
}
const parseVariablesInRichText = (
elements: TDescendant[],
{
variables,
takeLatestIfList,
}: { variables: Variable[]; takeLatestIfList?: boolean }
): TDescendant[] => {
const parsedElements: TDescendant[] = []
for (const element of elements) {
if ('text' in element) {
const text = element.text as string
if (isEmpty(text)) {
parsedElements.push(element)
continue
}
const variablesInText = getVariablesToParseInfoInText(text, {
variables,
takeLatestIfList,
})
if (variablesInText.length === 0) {
parsedElements.push(element)
continue
}
let lastTextEndIndex = 0
let index = -1
for (const variableInText of variablesInText) {
index += 1
const textBeforeVariable = text.substring(
lastTextEndIndex,
variableInText.startIndex
)
const textAfterVariable =
index === variablesInText.length - 1
? text.substring(variableInText.endIndex)
: undefined
lastTextEndIndex = variableInText.endIndex
const isStandaloneElement =
isEmpty(textBeforeVariable) && isEmpty(textAfterVariable)
const variableElements = convertMarkdownToRichText(
isStandaloneElement
? variableInText.value
: variableInText.value.replace(/[\n]+/g, ' ')
)
const variableElementsWithStyling = applyElementStyleToDescendants(
variableElements,
{
bold: element.bold,
italic: element.italic,
underline: element.underline,
}
)
if (isStandaloneElement) {
parsedElements.push(...variableElementsWithStyling)
continue
}
const children: TDescendant[] = []
if (isNotEmpty(textBeforeVariable))
children.push({
...element,
text: textBeforeVariable,
})
children.push({
type: 'inline-variable',
children: variableElementsWithStyling,
})
if (isNotEmpty(textAfterVariable))
children.push({
...element,
text: textAfterVariable,
})
parsedElements.push(...children)
}
continue
}
const type =
element.children.length === 1 &&
'text' in element.children[0] &&
(element.children[0].text as string).startsWith('{{') &&
(element.children[0].text as string).endsWith('}}')
? 'variable'
: element.type
parsedElements.push({
...element,
type,
children: parseVariablesInRichText(element.children as TDescendant[], {
variables,
takeLatestIfList,
}),
})
}
return parsedElements
}
const applyElementStyleToDescendants = (
variableElements: TDescendant[],
styles: { bold: unknown; italic: unknown; underline: unknown }
): TDescendant[] =>
variableElements.map((variableElement) => {
if ('text' in variableElement) return { ...styles, ...variableElement }
return {
...variableElement,
children: applyElementStyleToDescendants(
variableElement.children,
styles
),
}
})
const convertMarkdownToRichText = (text: string): TDescendant[] => {
const plugins = [createDeserializeMdPlugin()]
return deserializeMd(createPlateEditor({ plugins }) as unknown as any, text)
}

View File

@ -3,10 +3,17 @@ import { ChatReply, SessionState } from '@typebot.io/schemas'
import { executeGroup } from './executeGroup' import { executeGroup } from './executeGroup'
import { getNextGroup } from './getNextGroup' import { getNextGroup } from './getNextGroup'
export const startBotFlow = async ( type Props = {
state: SessionState, version: 1 | 2
state: SessionState
startGroupId?: string startGroupId?: string
): Promise<ChatReply & { newSessionState: SessionState }> => { }
export const startBotFlow = async ({
version,
state,
startGroupId,
}: Props): Promise<ChatReply & { newSessionState: SessionState }> => {
let newSessionState = state let newSessionState = state
if (startGroupId) { if (startGroupId) {
const group = state.typebotsQueue[0].typebot.groups.find( const group = state.typebotsQueue[0].typebot.groups.find(
@ -17,7 +24,7 @@ export const startBotFlow = async (
code: 'BAD_REQUEST', code: 'BAD_REQUEST',
message: "startGroupId doesn't exist", message: "startGroupId doesn't exist",
}) })
return executeGroup(newSessionState)(group) return executeGroup(group, { version, state: newSessionState })
} }
const firstEdgeId = const firstEdgeId =
newSessionState.typebotsQueue[0].typebot.groups[0].blocks[0].outgoingEdgeId newSessionState.typebotsQueue[0].typebot.groups[0].blocks[0].outgoingEdgeId
@ -25,5 +32,5 @@ export const startBotFlow = async (
const nextGroup = await getNextGroup(newSessionState)(firstEdgeId) const nextGroup = await getNextGroup(newSessionState)(firstEdgeId)
newSessionState = nextGroup.newSessionState newSessionState = nextGroup.newSessionState
if (!nextGroup.group) return { messages: [], newSessionState } if (!nextGroup.group) return { messages: [], newSessionState }
return executeGroup(newSessionState)(nextGroup.group) return executeGroup(nextGroup.group, { version, state: newSessionState })
} }

View File

@ -31,6 +31,7 @@ import { upsertResult } from './queries/upsertResult'
import { continueBotFlow } from './continueBotFlow' import { continueBotFlow } from './continueBotFlow'
type Props = { type Props = {
version: 1 | 2
message: string | undefined message: string | undefined
startParams: StartParams startParams: StartParams
userId: string | undefined userId: string | undefined
@ -38,6 +39,7 @@ type Props = {
} }
export const startSession = async ({ export const startSession = async ({
version,
message, message,
startParams, startParams,
userId, userId,
@ -135,7 +137,11 @@ export const startSession = async ({
} }
} }
let chatReply = await startBotFlow(initialState, startParams.startGroupId) let chatReply = await startBotFlow({
version,
state: initialState,
startGroupId: startParams.startGroupId,
})
// If params has message and first block is an input block, we can directly continue the bot flow // If params has message and first block is an input block, we can directly continue the bot flow
if (message) { if (message) {
@ -154,10 +160,13 @@ export const startSession = async ({
resultId, resultId,
typebot: newSessionState.typebotsQueue[0].typebot, typebot: newSessionState.typebotsQueue[0].typebot,
}) })
chatReply = await continueBotFlow({ chatReply = await continueBotFlow(message, {
...newSessionState, version,
currentBlock: { groupId: firstBlock.groupId, blockId: firstBlock.id }, state: {
})(message) ...newSessionState,
currentBlock: { groupId: firstBlock.groupId, blockId: firstBlock.id },
},
})
} }
} }

View File

@ -64,7 +64,10 @@ type VariableToParseInformation = {
export const getVariablesToParseInfoInText = ( export const getVariablesToParseInfoInText = (
text: string, text: string,
variables: Variable[] {
variables,
takeLatestIfList,
}: { variables: Variable[]; takeLatestIfList?: boolean }
): VariableToParseInformation[] => { ): VariableToParseInformation[] => {
const pattern = /\{\{([^{}]+)\}\}|(\$)\{\{([^{}]+)\}\}/g const pattern = /\{\{([^{}]+)\}\}|(\$)\{\{([^{}]+)\}\}/g
const variablesToParseInfo: VariableToParseInformation[] = [] const variablesToParseInfo: VariableToParseInformation[] = []
@ -78,7 +81,12 @@ export const getVariablesToParseInfoInText = (
startIndex: match.index, startIndex: match.index,
endIndex: match.index + match[0].length, endIndex: match.index + match[0].length,
textToReplace: match[0], textToReplace: match[0],
value: safeStringify(variable?.value) ?? '', value:
safeStringify(
takeLatestIfList && Array.isArray(variable?.value)
? variable?.value[variable?.value.length - 1]
: variable?.value
) ?? '',
}) })
} }
return variablesToParseInfo return variablesToParseInfo

View File

@ -64,9 +64,10 @@ export const resumeWhatsAppFlow = async ({
const resumeResponse = const resumeResponse =
session && !isSessionExpired session && !isSessionExpired
? await continueBotFlow({ ...session.state, whatsApp: { contact } })( ? await continueBotFlow(messageContent, {
messageContent version: 2,
) state: { ...session.state, whatsApp: { contact } },
})
: workspaceId : workspaceId
? await startWhatsAppSession({ ? await startWhatsAppSession({
incomingMessage: messageContent, incomingMessage: messageContent,

View File

@ -51,7 +51,7 @@ export const sendChatReplyToWhatsApp = async ({
const result = await executeClientSideAction({ to, credentials })(action) const result = await executeClientSideAction({ to, credentials })(action)
if (!result) continue if (!result) continue
const { input, newSessionState, messages, clientSideActions } = const { input, newSessionState, messages, clientSideActions } =
await continueBotFlow(state)(result.replyToSend) await continueBotFlow(result.replyToSend, { version: 2, state })
return sendChatReplyToWhatsApp({ return sendChatReplyToWhatsApp({
to, to,
@ -95,7 +95,7 @@ export const sendChatReplyToWhatsApp = async ({
) )
if (!result) continue if (!result) continue
const { input, newSessionState, messages, clientSideActions } = const { input, newSessionState, messages, clientSideActions } =
await continueBotFlow(state)(result.replyToSend) await continueBotFlow(result.replyToSend, { version: 2, state })
return sendChatReplyToWhatsApp({ return sendChatReplyToWhatsApp({
to, to,

View File

@ -78,6 +78,7 @@ export const startWhatsAppSession = async ({
defaultSessionExpiryTimeout defaultSessionExpiryTimeout
return startSession({ return startSession({
version: 2,
message: incomingMessage, message: incomingMessage,
startParams: { startParams: {
typebot: publicTypebot.typebot.publicId as string, typebot: publicTypebot.typebot.publicId as string,

View File

@ -16,7 +16,7 @@ npm install @typebot.io/js
``` ```
<script type="module"> <script type="module">
import Typebot from 'https://cdn.jsdelivr.net/npm/@typebot.io/js@0.1/dist/web.js' import Typebot from 'https://cdn.jsdelivr.net/npm/@typebot.io/js@0.2/dist/web.js'
Typebot.initStandard({ Typebot.initStandard({
typebot: 'my-typebot', typebot: 'my-typebot',
@ -34,7 +34,7 @@ There, you can change the container dimensions. Here is a code example:
```html ```html
<script type="module"> <script type="module">
import Typebot from 'https://cdn.jsdelivr.net/npm/@typebot.io/js@0.1/dist/web.js' import Typebot from 'https://cdn.jsdelivr.net/npm/@typebot.io/js@0.2/dist/web.js'
Typebot.initStandard({ Typebot.initStandard({
typebot: 'my-typebot', typebot: 'my-typebot',
@ -54,7 +54,7 @@ Here is an example:
```html ```html
<script type="module"> <script type="module">
import Typebot from 'https://cdn.jsdelivr.net/npm/@typebot.io/js@0.1/dist/web.js' import Typebot from 'https://cdn.jsdelivr.net/npm/@typebot.io/js@0.2/dist/web.js'
Typebot.initPopup({ Typebot.initPopup({
typebot: 'my-typebot', typebot: 'my-typebot',
@ -96,7 +96,7 @@ Here is an example:
```html ```html
<script type="module"> <script type="module">
import Typebot from 'https://cdn.jsdelivr.net/npm/@typebot.io/js@0.1/dist/web.js' import Typebot from 'https://cdn.jsdelivr.net/npm/@typebot.io/js@0.2/dist/web.js'
Typebot.initBubble({ Typebot.initBubble({
typebot: 'my-typebot', typebot: 'my-typebot',

View File

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

View File

@ -32,7 +32,7 @@ export async function getInitialChatReplyQuery({
if (paymentInProgressState) removePaymentInProgressFromStorage() if (paymentInProgressState) removePaymentInProgressFromStorage()
const { data, error } = await sendRequest<InitialChatReply>({ const { data, error } = await sendRequest<InitialChatReply>({
method: 'POST', method: 'POST',
url: `${isNotEmpty(apiHost) ? apiHost : guessApiHost()}/api/v1/sendMessage`, url: `${isNotEmpty(apiHost) ? apiHost : guessApiHost()}/api/v2/sendMessage`,
body: { body: {
startParams: paymentInProgressState startParams: paymentInProgressState
? undefined ? undefined

View File

@ -8,6 +8,6 @@ export const sendMessageQuery = ({
}: SendMessageInput & { apiHost?: string }) => }: SendMessageInput & { apiHost?: string }) =>
sendRequest<ChatReply>({ sendRequest<ChatReply>({
method: 'POST', method: 'POST',
url: `${isNotEmpty(apiHost) ? apiHost : guessApiHost()}/api/v1/sendMessage`, url: `${isNotEmpty(apiHost) ? apiHost : guessApiHost()}/api/v2/sendMessage`,
body, body,
}) })

View File

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

View File

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

View File

@ -40,7 +40,7 @@ class Typebot_Public
function typebot_script() function typebot_script()
{ {
echo '<script type="module">import Typebot from "https://cdn.jsdelivr.net/npm/@typebot.io/js@0.1/dist/web.js";'; echo '<script type="module">import Typebot from "https://cdn.jsdelivr.net/npm/@typebot.io/js@0.2/dist/web.js";';
if ( if (
get_option('excluded_pages') !== null && get_option('excluded_pages') !== null &&
get_option('excluded_pages') !== '' get_option('excluded_pages') !== ''
@ -91,7 +91,7 @@ class Typebot_Public
public function add_typebot_container($attributes = []) public function add_typebot_container($attributes = [])
{ {
$lib_url = "https://cdn.jsdelivr.net/npm/@typebot.io/js@0.1/dist/web.js"; $lib_url = "https://cdn.jsdelivr.net/npm/@typebot.io/js@0.2/dist/web.js";
$width = '100%'; $width = '100%';
$height = '500px'; $height = '500px';
$api_host = 'https://viewer.typebot.io'; $api_host = 'https://viewer.typebot.io';