2
0

♻️ (api) Auto start bot if starting with input

Closes #877, closes #884
This commit is contained in:
Baptiste Arnaud
2023-10-04 16:47:58 +02:00
parent 2bc9dfb503
commit 9e6a1f7dc0
11 changed files with 180 additions and 39 deletions

View File

@ -103,7 +103,7 @@ test.describe.parallel('Google sheets integration', () => {
await page.click('text=Select an operation')
await page.click('text=Get data from sheet')
await page.getByRole('button', { name: 'Rows to filter' }).click()
await page.getByRole('button', { name: 'Select row(s)' }).click()
await page.getByRole('button', { name: 'Add filter rule' }).click()
await page.click('text=Select a column')
await page.click('button >> text="Email"')

View File

@ -34,7 +34,7 @@ test.describe('Send email block', () => {
)
await page.fill('[placeholder="John Smith"]', 'John Smith')
await page.fill('[placeholder="mail.provider.com"]', env.SMTP_HOST)
await page.fill('[placeholder="user@provider.com"]', env.SMTP_USERNAME)
await page.getByLabel('Username').fill(env.SMTP_USERNAME)
await page.fill('[type="password"]', env.SMTP_PASSWORD)
await page.fill('input[role="spinbutton"]', env.SMTP_PORT.toString())
await expect(createButton).toBeEnabled()

View File

@ -57,7 +57,9 @@ test.describe('Starter workspace', () => {
},
])
await page.goto(`/typebots/${typebotId}/share`)
await expect(page.locator('[data-testid="pro-lock-tag"]')).toBeVisible()
await expect(
page.locator('[data-testid="pro-lock-tag"]').nth(0)
).toBeVisible()
await page.click('text=Add my domain')
await expect(
page.locator(

View File

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

View File

@ -57,7 +57,7 @@ export const sendMessage = publicProcedure
logs,
clientSideActions,
newSessionState,
} = await startSession({ startParams, userId: user?.id })
} = await startSession({ startParams, userId: user?.id, message })
const allLogs = clientLogs ? [...(logs ?? []), ...clientLogs] : logs

View File

@ -0,0 +1,102 @@
{
"version": "5",
"id": "clnbugp6a00011ackz0k3zfkp",
"name": "My typebot",
"groups": [
{
"id": "k2nokn9v0zyhae0wqcxsbqa7",
"title": "Start",
"graphCoordinates": { "x": 0, "y": 0 },
"blocks": [
{
"id": "sx4xmdbosubnxkhcg6x521p1",
"groupId": "k2nokn9v0zyhae0wqcxsbqa7",
"outgoingEdgeId": "fj2ga89lctnuwcdsshwtxmhp",
"type": "start",
"label": "Start"
}
]
},
{
"id": "g8kdars2ahr3cyz2qf1f7w4i",
"title": "Group #1",
"graphCoordinates": { "x": 235, "y": 170 },
"blocks": [
{
"id": "prh6snup7cbmoxtf5vox8kjw",
"groupId": "g8kdars2ahr3cyz2qf1f7w4i",
"type": "text input",
"options": {
"labels": {
"placeholder": "Type your answer...",
"button": "Send"
},
"isLong": false
}
},
{
"id": "dpyyb38amnwwl4q461el2uf6",
"groupId": "g8kdars2ahr3cyz2qf1f7w4i",
"type": "text",
"content": {
"richText": [
{ "type": "p", "children": [{ "text": "That's nice!" }] }
]
}
}
]
}
],
"edges": [
{
"id": "fj2ga89lctnuwcdsshwtxmhp",
"from": {
"groupId": "k2nokn9v0zyhae0wqcxsbqa7",
"blockId": "sx4xmdbosubnxkhcg6x521p1"
},
"to": { "groupId": "g8kdars2ahr3cyz2qf1f7w4i" }
}
],
"variables": [],
"theme": {
"general": {
"font": "Open Sans",
"background": { "type": "Color", "content": "#ffffff" }
},
"chat": {
"hostAvatar": { "isEnabled": true },
"hostBubbles": { "backgroundColor": "#F7F8FF", "color": "#303235" },
"guestBubbles": { "backgroundColor": "#FF8E21", "color": "#FFFFFF" },
"buttons": { "backgroundColor": "#0042DA", "color": "#FFFFFF" },
"inputs": {
"backgroundColor": "#FFFFFF",
"color": "#303235",
"placeholderColor": "#9095A0"
}
}
},
"selectedThemeTemplateId": null,
"settings": {
"general": {
"isBrandingEnabled": false,
"isInputPrefillEnabled": true,
"isHideQueryParamsEnabled": true,
"rememberUser": { "isEnabled": 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."
}
},
"createdAt": "2023-10-04T14:28:55.282Z",
"updatedAt": "2023-10-04T14:29:11.949Z",
"icon": null,
"folderId": null,
"publicId": null,
"customDomain": null,
"workspaceId": "proWorkspace",
"resultsTablePreferences": null,
"isArchived": false,
"isClosed": false,
"whatsAppCredentialsId": null
}

View File

@ -27,6 +27,13 @@ test('API chat execution should work on preview bot', async ({ request }) => {
id: 'chat-sub-bot',
publicId: 'chat-sub-bot-public',
})
await importTypebotInDatabase(
getTestAsset('typebots/chat/startingWithInput.json'),
{
id: 'starting-with-input',
publicId: 'starting-with-input-public',
}
)
await createWebhook(typebotId, {
id: 'chat-webhook-id',
method: HttpMethod.GET,
@ -218,4 +225,26 @@ test('API chat execution should work on published bot', async ({ request }) => {
])
expect(messages[2].content.richText.length).toBeGreaterThan(0)
})
await test.step('Starting with a message when typebot starts with input should proceed', async () => {
const { messages } = await (
await request.post(`/api/v1/sendMessage`, {
data: {
message: 'Hey',
startParams: {
typebot: 'starting-with-input-public',
},
} satisfies SendMessageInput,
})
).json()
expect(messages[0].content.richText).toStrictEqual([
{
children: [
{
text: "That's nice!",
},
],
type: 'p',
},
])
})
})

View File

@ -28,6 +28,7 @@ import { validateRatingReply } from './blocks/inputs/rating/validateRatingReply'
import { parsePictureChoicesReply } from './blocks/inputs/pictureChoice/parsePictureChoicesReply'
import { parseVariables } from './variables/parseVariables'
import { updateVariablesInSession } from './variables/updateVariablesInSession'
import { startBotFlow } from './startBotFlow'
import { TRPCError } from '@trpc/server'
export const continueBotFlow =
@ -36,6 +37,9 @@ export const continueBotFlow =
reply?: string
): Promise<ChatReply & { newSessionState: SessionState }> => {
let newSessionState = { ...state }
if (!newSessionState.currentBlock) return startBotFlow(state)
const group = state.typebotsQueue[0].typebot.groups.find(
(group) => group.id === state.currentBlock?.groupId
)

View File

@ -26,14 +26,19 @@ import { startBotFlow } from './startBotFlow'
import { prefillVariables } from './variables/prefillVariables'
import { deepParseVariables } from './variables/deepParseVariables'
import { injectVariablesFromExistingResult } from './variables/injectVariablesFromExistingResult'
import { getNextGroup } from './getNextGroup'
import { upsertResult } from './queries/upsertResult'
import { continueBotFlow } from './continueBotFlow'
type Props = {
message: string | undefined
startParams: StartParams
userId: string | undefined
initialSessionState?: Pick<SessionState, 'whatsApp' | 'expiryTimeout'>
}
export const startSession = async ({
message,
startParams,
userId,
initialSessionState,
@ -130,13 +135,39 @@ export const startSession = async ({
}
}
let chatReply = await startBotFlow(initialState, startParams.startGroupId)
// If params has message and first block is an input block, we can directly continue the bot flow
if (message) {
const firstEdgeId =
chatReply.newSessionState.typebotsQueue[0].typebot.groups[0].blocks[0]
.outgoingEdgeId
const nextGroup = await getNextGroup(chatReply.newSessionState)(firstEdgeId)
const newSessionState = nextGroup.newSessionState
const firstBlock = nextGroup.group?.blocks.at(0)
if (firstBlock && isInputBlock(firstBlock)) {
const resultId = newSessionState.typebotsQueue[0].resultId
if (resultId)
await upsertResult({
hasStarted: true,
isCompleted: false,
resultId,
typebot: newSessionState.typebotsQueue[0].typebot,
})
chatReply = await continueBotFlow({
...newSessionState,
currentBlock: { groupId: firstBlock.groupId, blockId: firstBlock.id },
})(message)
}
}
const {
messages,
input,
clientSideActions: startFlowClientActions,
newSessionState,
logs,
} = await startBotFlow(initialState, startParams.startGroupId)
} = chatReply
const clientSideActions = startFlowClientActions ?? []

View File

@ -12,7 +12,6 @@ import { decrypt } from '@typebot.io/lib/api'
import { saveStateToDatabase } from '../saveStateToDatabase'
import prisma from '@typebot.io/lib/prisma'
import { isDefined } from '@typebot.io/lib/utils'
import { startBotFlow } from '../startBotFlow'
type Props = {
receivedMessage: WhatsAppIncomingMessage
@ -65,9 +64,9 @@ export const resumeWhatsAppFlow = async ({
const resumeResponse =
session && !isSessionExpired
? session.state.currentBlock
? await continueBotFlow(session.state)(messageContent)
: await startBotFlow({ ...session.state, whatsApp: { contact } })
? await continueBotFlow({ ...session.state, whatsApp: { contact } })(
messageContent
)
: workspaceId
? await startWhatsAppSession({
incomingMessage: messageContent,

View File

@ -12,11 +12,8 @@ import {
WhatsAppCredentials,
defaultSessionExpiryTimeout,
} from '@typebot.io/schemas/features/whatsapp'
import { isInputBlock, isNotDefined } from '@typebot.io/lib/utils'
import { isNotDefined } from '@typebot.io/lib/utils'
import { startSession } from '../startSession'
import { getNextGroup } from '../getNextGroup'
import { continueBotFlow } from '../continueBotFlow'
import { upsertResult } from '../queries/upsertResult'
type Props = {
incomingMessage?: string
@ -80,7 +77,8 @@ export const startWhatsAppSession = async ({
publicTypebot.settings.whatsApp?.sessionExpiryTimeout ??
defaultSessionExpiryTimeout
let chatReply = await startSession({
return startSession({
message: incomingMessage,
startParams: {
typebot: publicTypebot.typebot.publicId as string,
},
@ -92,31 +90,6 @@ export const startWhatsAppSession = async ({
expiryTimeout: sessionExpiryTimeoutHours * 60 * 60 * 1000,
},
})
let sessionState: SessionState = chatReply.newSessionState
// If first block is an input block, we can directly continue the bot flow
const firstEdgeId =
sessionState.typebotsQueue[0].typebot.groups[0].blocks[0].outgoingEdgeId
const nextGroup = await getNextGroup(sessionState)(firstEdgeId)
sessionState = nextGroup.newSessionState
const firstBlock = nextGroup.group?.blocks.at(0)
if (firstBlock && isInputBlock(firstBlock)) {
const resultId = sessionState.typebotsQueue[0].resultId
if (resultId)
await upsertResult({
hasStarted: true,
isCompleted: false,
resultId,
typebot: sessionState.typebotsQueue[0].typebot,
})
chatReply = await continueBotFlow({
...sessionState,
currentBlock: { groupId: firstBlock.groupId, blockId: firstBlock.id },
})(incomingMessage)
}
return chatReply
}
export const messageMatchStartCondition = (