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..."]')
.press('Enter')
await expect(
page.locator('typebot-standard').locator('text=Your name is:')
).toHaveText(`Your name is: Georges2 Last name`)
page.locator('typebot-standard').locator('text=Georges2')
).toBeVisible()
})
})

View File

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

View File

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

View File

@ -43,7 +43,7 @@ export const parseReactBotProps = ({ typebot, apiHost }: BotProps) => {
}
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'`
export const parseInlineScript = (script: string) =>

View File

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

View File

@ -12,7 +12,7 @@ There, you can change the container dimensions. Here is a code example:
```html
<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: 'my-typebot',
@ -32,7 +32,7 @@ Here is an example:
```html
<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: 'my-typebot',
@ -72,7 +72,7 @@ If you have different bots on the same page you will have to make them distinct
```html
<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({
id: 'bot1'
@ -104,7 +104,7 @@ Here is an example:
```html
<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: 'my-typebot',

View File

@ -22,7 +22,7 @@ It should look like:
```html
<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: 'my-typebot',

View File

@ -2,17 +2,17 @@
"openapi": "3.0.3",
"info": {
"title": "Chat API",
"version": "1.0.0"
"version": "2.0.0"
},
"servers": [
{
"url": "https://typebot.io/api/v1"
"url": "https://typebot.io/api/v2"
}
],
"paths": {
"/sendMessage": {
"post": {
"operationId": "sendMessage",
"operationId": "sendMessageV2",
"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.",
"requestBody": {

View File

@ -12,7 +12,7 @@ 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 sendMessage = publicProcedure
export const sendMessageV1 = publicProcedure
.meta({
openapi: {
method: 'POST',
@ -57,7 +57,12 @@ export const sendMessage = publicProcedure
logs,
clientSideActions,
newSessionState,
} = await startSession({ startParams, userId: user?.id, message })
} = await startSession({
version: 1,
startParams,
userId: user?.id,
message,
})
const allLogs = clientLogs ? [...(logs ?? []), ...clientLogs] : logs
@ -98,7 +103,7 @@ export const sendMessage = publicProcedure
newSessionState,
logs,
lastMessageNewFormat,
} = await continueBotFlow(session.state)(message)
} = await continueBotFlow(message, { version: 1, state: session.state })
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 { writeFileSync } from 'fs'
import { appRouter } from './routers/v1/_app'
import { appRouter } from './routers/appRouterV2'
const openApiDocument = generateOpenApiDocument(appRouter, {
title: 'Chat API',
version: '1.0.0',
baseUrl: 'https://typebot.io/api/v1',
version: '2.0.0',
baseUrl: 'https://typebot.io/api/v2',
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 { router } from '../../trpc'
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({
sendMessage,
sendMessageV1,
getUploadUrl,
generateUploadUrl,
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 { createOpenApiNextHandler } from 'trpc-openapi'
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 () => {
const { sessionId, messages, input, resultId } = await (
await request.post(`/api/v1/sendMessage`, {
await request.post(`/api/v2/sendMessage`, {
data: {
startParams: {
typebot: typebotId,
@ -83,7 +83,7 @@ test('API chat execution should work on published bot', async ({ request }) => {
await test.step('Start the chat', async () => {
const { sessionId, messages, input, resultId } = await (
await request.post(`/api/v1/sendMessage`, {
await request.post(`/api/v2/sendMessage`, {
data: {
startParams: {
typebot: publicId,
@ -111,12 +111,30 @@ test('API chat execution should work on published bot', async ({ request }) => {
await test.step('Answer Name question', async () => {
const { messages, input } = await (
await request.post(`/api/v1/sendMessage`, {
await request.post(`/api/v2/sendMessage`, {
data: { message: 'John', sessionId: chatSessionId },
})
).json()
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(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 () => {
const { messages, input } = await (
await request.post(`/api/v1/sendMessage`, {
await request.post(`/api/v2/sendMessage`, {
data: { message: '24', sessionId: chatSessionId },
})
).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' },
])
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([
{
@ -145,7 +181,7 @@ test('API chat execution should work on published bot', async ({ request }) => {
await test.step('Answer Rating question', async () => {
const { messages, input } = await (
await request.post(`/api/v1/sendMessage`, {
await request.post(`/api/v2/sendMessage`, {
data: { message: '8', sessionId: chatSessionId },
})
).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 () => {
const { messages, input } = await (
await request.post(`/api/v1/sendMessage`, {
await request.post(`/api/v2/sendMessage`, {
data: { message: 'invalid email', sessionId: chatSessionId },
})
).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 () => {
const { messages, input } = await (
await request.post(`/api/v1/sendMessage`, {
await request.post(`/api/v2/sendMessage`, {
data: { message: 'typebot@email.com', sessionId: chatSessionId },
})
).json()
@ -189,7 +225,7 @@ test('API chat execution should work on published bot', async ({ request }) => {
await test.step('Answer URL question', async () => {
const { messages, input } = await (
await request.post(`/api/v1/sendMessage`, {
await request.post(`/api/v2/sendMessage`, {
data: { message: 'https://typebot.io', sessionId: chatSessionId },
})
).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 () => {
const { messages } = await (
await request.post(`/api/v1/sendMessage`, {
await request.post(`/api/v2/sendMessage`, {
data: { message: 'Yes', sessionId: chatSessionId },
})
).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 () => {
const { messages } = await (
await request.post(`/api/v1/sendMessage`, {
await request.post(`/api/v2/sendMessage`, {
data: {
message: 'Hey',
startParams: {

View File

@ -12,7 +12,7 @@ test('should correctly be injected', async ({ page }) => {
await page.goto(`/${typebotId}-public`)
await expect(page.locator('text="Your name is"')).toBeVisible()
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(
'email@test.com'
)

View File

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

View File

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

View File

@ -31,14 +31,17 @@ import { updateVariablesInSession } from './variables/updateVariablesInSession'
import { startBotFlow } from './startBotFlow'
import { TRPCError } from '@trpc/server'
export const continueBotFlow =
(state: SessionState) =>
async (
reply?: string
type Params = {
version: 1 | 2
state: SessionState
}
export const continueBotFlow = async (
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(
(group) => group.id === state.currentBlock?.groupId
@ -100,10 +103,7 @@ export const continueBotFlow =
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
@ -120,10 +120,13 @@ export const continueBotFlow =
const nextEdgeId = getOutgoingEdgeId(newSessionState)(block, formattedReply)
if (groupHasMoreBlocks && !nextEdgeId) {
const chatReply = await executeGroup(newSessionState)({
const chatReply = await executeGroup(
{
...group,
blocks: group.blocks.slice(blockIndex + 1),
})
},
{ version, state: newSessionState }
)
return {
...chatReply,
lastMessageNewFormat:
@ -151,12 +154,14 @@ export const continueBotFlow =
formattedReply !== reply ? formattedReply : undefined,
}
const chatReply = await executeGroup(newSessionState)(nextGroup.group)
const chatReply = await executeGroup(nextGroup.group, {
version,
state: newSessionState,
})
return {
...chatReply,
lastMessageNewFormat:
formattedReply !== reply ? formattedReply : undefined,
lastMessageNewFormat: formattedReply !== reply ? formattedReply : undefined,
}
}

View File

@ -1,17 +1,13 @@
import {
BubbleBlock,
BubbleBlockType,
ChatReply,
Group,
InputBlock,
InputBlockType,
RuntimeOptions,
SessionState,
Variable,
} from '@typebot.io/schemas'
import {
isBubbleBlock,
isEmpty,
isInputBlock,
isIntegrationBlock,
isLogicBlock,
@ -26,22 +22,18 @@ import { injectVariableValuesInPictureChoiceBlock } from './blocks/inputs/pictur
import { getPrefilledInputValue } from './getPrefilledValue'
import { parseDateInput } from './blocks/inputs/date/parseDateInput'
import { deepParseVariables } from './variables/deepParseVariables'
import { parseVideoUrl } from '@typebot.io/lib/parseVideoUrl'
import { TDescendant, createPlateEditor } from '@udecode/plate-common'
import {
createDeserializeMdPlugin,
deserializeMd,
} from '@udecode/plate-serializer-md'
import { getVariablesToParseInfoInText } from './variables/parseVariables'
import { parseBubbleBlock } from './parseBubbleBlock'
export const executeGroup =
(
state: SessionState,
currentReply?: ChatReply,
type ContextProps = {
version: 1 | 2
state: SessionState
currentReply?: ChatReply
currentLastBubbleId?: string
) =>
async (
group: Group
}
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'] =
@ -57,9 +49,10 @@ export const executeGroup =
if (isBubbleBlock(block)) {
messages.push(
parseBubbleBlock(newSessionState.typebotsQueue[0].typebot.variables)(
block
)
parseBubbleBlock(block, {
version,
variables: newSessionState.typebotsQueue[0].typebot.variables,
})
)
lastBubbleBlockId = block.id
continue
@ -130,9 +123,7 @@ export const executeGroup =
if (!nextEdgeId && newSessionState.typebotsQueue.length === 1)
return { messages, newSessionState, clientSideActions, logs }
const nextGroup = await getNextGroup(newSessionState)(
nextEdgeId ?? undefined
)
const nextGroup = await getNextGroup(newSessionState)(nextEdgeId ?? undefined)
newSessionState = nextGroup.newSessionState
@ -140,15 +131,16 @@ export const executeGroup =
return { messages, newSessionState, clientSideActions, logs }
}
return executeGroup(
newSessionState,
{
return executeGroup(nextGroup.group, {
version,
state: newSessionState,
currentReply: {
messages,
clientSideActions,
logs,
},
lastBubbleBlockId
)(nextGroup.group)
currentLastBubbleId: lastBubbleBlockId,
})
}
const computeRuntimeOptions =
@ -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 =
(state: SessionState) =>
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 { getNextGroup } from './getNextGroup'
export const startBotFlow = async (
state: SessionState,
type Props = {
version: 1 | 2
state: SessionState
startGroupId?: string
): Promise<ChatReply & { newSessionState: SessionState }> => {
}
export const startBotFlow = async ({
version,
state,
startGroupId,
}: Props): Promise<ChatReply & { newSessionState: SessionState }> => {
let newSessionState = state
if (startGroupId) {
const group = state.typebotsQueue[0].typebot.groups.find(
@ -17,7 +24,7 @@ export const startBotFlow = async (
code: 'BAD_REQUEST',
message: "startGroupId doesn't exist",
})
return executeGroup(newSessionState)(group)
return executeGroup(group, { version, state: newSessionState })
}
const firstEdgeId =
newSessionState.typebotsQueue[0].typebot.groups[0].blocks[0].outgoingEdgeId
@ -25,5 +32,5 @@ export const startBotFlow = async (
const nextGroup = await getNextGroup(newSessionState)(firstEdgeId)
newSessionState = nextGroup.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'
type Props = {
version: 1 | 2
message: string | undefined
startParams: StartParams
userId: string | undefined
@ -38,6 +39,7 @@ type Props = {
}
export const startSession = async ({
version,
message,
startParams,
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 (message) {
@ -154,10 +160,13 @@ export const startSession = async ({
resultId,
typebot: newSessionState.typebotsQueue[0].typebot,
})
chatReply = await continueBotFlow({
chatReply = await continueBotFlow(message, {
version,
state: {
...newSessionState,
currentBlock: { groupId: firstBlock.groupId, blockId: firstBlock.id },
})(message)
},
})
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -16,7 +16,7 @@ npm install @typebot.io/js
```
<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: 'my-typebot',
@ -34,7 +34,7 @@ There, you can change the container dimensions. Here is a code example:
```html
<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: 'my-typebot',
@ -54,7 +54,7 @@ Here is an example:
```html
<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: 'my-typebot',
@ -96,7 +96,7 @@ Here is an example:
```html
<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: 'my-typebot',

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -40,7 +40,7 @@ class Typebot_Public
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 (
get_option('excluded_pages') !== null &&
get_option('excluded_pages') !== ''
@ -91,7 +91,7 @@ class Typebot_Public
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%';
$height = '500px';
$api_host = 'https://viewer.typebot.io';