♻️ (api) Auto start bot if starting with input
Closes #877, closes #884
This commit is contained in:
@ -103,7 +103,7 @@ test.describe.parallel('Google sheets integration', () => {
|
|||||||
await page.click('text=Select an operation')
|
await page.click('text=Select an operation')
|
||||||
await page.click('text=Get data from sheet')
|
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.getByRole('button', { name: 'Add filter rule' }).click()
|
||||||
await page.click('text=Select a column')
|
await page.click('text=Select a column')
|
||||||
await page.click('button >> text="Email"')
|
await page.click('button >> text="Email"')
|
||||||
|
@ -34,7 +34,7 @@ test.describe('Send email block', () => {
|
|||||||
)
|
)
|
||||||
await page.fill('[placeholder="John Smith"]', 'John Smith')
|
await page.fill('[placeholder="John Smith"]', 'John Smith')
|
||||||
await page.fill('[placeholder="mail.provider.com"]', env.SMTP_HOST)
|
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('[type="password"]', env.SMTP_PASSWORD)
|
||||||
await page.fill('input[role="spinbutton"]', env.SMTP_PORT.toString())
|
await page.fill('input[role="spinbutton"]', env.SMTP_PORT.toString())
|
||||||
await expect(createButton).toBeEnabled()
|
await expect(createButton).toBeEnabled()
|
||||||
|
@ -57,7 +57,9 @@ test.describe('Starter workspace', () => {
|
|||||||
},
|
},
|
||||||
])
|
])
|
||||||
await page.goto(`/typebots/${typebotId}/share`)
|
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 page.click('text=Add my domain')
|
||||||
await expect(
|
await expect(
|
||||||
page.locator(
|
page.locator(
|
||||||
|
@ -91,6 +91,7 @@ export const startWhatsAppPreview = authenticatedProcedure
|
|||||||
|
|
||||||
const { newSessionState, messages, input, clientSideActions, logs } =
|
const { newSessionState, messages, input, clientSideActions, logs } =
|
||||||
await startSession({
|
await startSession({
|
||||||
|
message: undefined,
|
||||||
startParams: {
|
startParams: {
|
||||||
isOnlyRegistering: !canSendDirectMessagesToUser,
|
isOnlyRegistering: !canSendDirectMessagesToUser,
|
||||||
typebot: typebotId,
|
typebot: typebotId,
|
||||||
|
@ -57,7 +57,7 @@ export const sendMessage = publicProcedure
|
|||||||
logs,
|
logs,
|
||||||
clientSideActions,
|
clientSideActions,
|
||||||
newSessionState,
|
newSessionState,
|
||||||
} = await startSession({ startParams, userId: user?.id })
|
} = await startSession({ startParams, userId: user?.id, message })
|
||||||
|
|
||||||
const allLogs = clientLogs ? [...(logs ?? []), ...clientLogs] : logs
|
const allLogs = clientLogs ? [...(logs ?? []), ...clientLogs] : logs
|
||||||
|
|
||||||
|
102
apps/viewer/src/test/assets/typebots/chat/startingWithInput.json
Normal file
102
apps/viewer/src/test/assets/typebots/chat/startingWithInput.json
Normal 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
|
||||||
|
}
|
@ -27,6 +27,13 @@ test('API chat execution should work on preview bot', async ({ request }) => {
|
|||||||
id: 'chat-sub-bot',
|
id: 'chat-sub-bot',
|
||||||
publicId: 'chat-sub-bot-public',
|
publicId: 'chat-sub-bot-public',
|
||||||
})
|
})
|
||||||
|
await importTypebotInDatabase(
|
||||||
|
getTestAsset('typebots/chat/startingWithInput.json'),
|
||||||
|
{
|
||||||
|
id: 'starting-with-input',
|
||||||
|
publicId: 'starting-with-input-public',
|
||||||
|
}
|
||||||
|
)
|
||||||
await createWebhook(typebotId, {
|
await createWebhook(typebotId, {
|
||||||
id: 'chat-webhook-id',
|
id: 'chat-webhook-id',
|
||||||
method: HttpMethod.GET,
|
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)
|
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',
|
||||||
|
},
|
||||||
|
])
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@ -28,6 +28,7 @@ import { validateRatingReply } from './blocks/inputs/rating/validateRatingReply'
|
|||||||
import { parsePictureChoicesReply } from './blocks/inputs/pictureChoice/parsePictureChoicesReply'
|
import { parsePictureChoicesReply } from './blocks/inputs/pictureChoice/parsePictureChoicesReply'
|
||||||
import { parseVariables } from './variables/parseVariables'
|
import { parseVariables } from './variables/parseVariables'
|
||||||
import { updateVariablesInSession } from './variables/updateVariablesInSession'
|
import { updateVariablesInSession } from './variables/updateVariablesInSession'
|
||||||
|
import { startBotFlow } from './startBotFlow'
|
||||||
import { TRPCError } from '@trpc/server'
|
import { TRPCError } from '@trpc/server'
|
||||||
|
|
||||||
export const continueBotFlow =
|
export const continueBotFlow =
|
||||||
@ -36,6 +37,9 @@ export const continueBotFlow =
|
|||||||
reply?: string
|
reply?: string
|
||||||
): Promise<ChatReply & { newSessionState: SessionState }> => {
|
): Promise<ChatReply & { newSessionState: SessionState }> => {
|
||||||
let newSessionState = { ...state }
|
let newSessionState = { ...state }
|
||||||
|
|
||||||
|
if (!newSessionState.currentBlock) return startBotFlow(state)
|
||||||
|
|
||||||
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
|
||||||
)
|
)
|
||||||
|
@ -26,14 +26,19 @@ import { startBotFlow } from './startBotFlow'
|
|||||||
import { prefillVariables } from './variables/prefillVariables'
|
import { prefillVariables } from './variables/prefillVariables'
|
||||||
import { deepParseVariables } from './variables/deepParseVariables'
|
import { deepParseVariables } from './variables/deepParseVariables'
|
||||||
import { injectVariablesFromExistingResult } from './variables/injectVariablesFromExistingResult'
|
import { injectVariablesFromExistingResult } from './variables/injectVariablesFromExistingResult'
|
||||||
|
import { getNextGroup } from './getNextGroup'
|
||||||
|
import { upsertResult } from './queries/upsertResult'
|
||||||
|
import { continueBotFlow } from './continueBotFlow'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
message: string | undefined
|
||||||
startParams: StartParams
|
startParams: StartParams
|
||||||
userId: string | undefined
|
userId: string | undefined
|
||||||
initialSessionState?: Pick<SessionState, 'whatsApp' | 'expiryTimeout'>
|
initialSessionState?: Pick<SessionState, 'whatsApp' | 'expiryTimeout'>
|
||||||
}
|
}
|
||||||
|
|
||||||
export const startSession = async ({
|
export const startSession = async ({
|
||||||
|
message,
|
||||||
startParams,
|
startParams,
|
||||||
userId,
|
userId,
|
||||||
initialSessionState,
|
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 {
|
const {
|
||||||
messages,
|
messages,
|
||||||
input,
|
input,
|
||||||
clientSideActions: startFlowClientActions,
|
clientSideActions: startFlowClientActions,
|
||||||
newSessionState,
|
newSessionState,
|
||||||
logs,
|
logs,
|
||||||
} = await startBotFlow(initialState, startParams.startGroupId)
|
} = chatReply
|
||||||
|
|
||||||
const clientSideActions = startFlowClientActions ?? []
|
const clientSideActions = startFlowClientActions ?? []
|
||||||
|
|
||||||
|
@ -12,7 +12,6 @@ import { decrypt } from '@typebot.io/lib/api'
|
|||||||
import { saveStateToDatabase } from '../saveStateToDatabase'
|
import { saveStateToDatabase } from '../saveStateToDatabase'
|
||||||
import prisma from '@typebot.io/lib/prisma'
|
import prisma from '@typebot.io/lib/prisma'
|
||||||
import { isDefined } from '@typebot.io/lib/utils'
|
import { isDefined } from '@typebot.io/lib/utils'
|
||||||
import { startBotFlow } from '../startBotFlow'
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
receivedMessage: WhatsAppIncomingMessage
|
receivedMessage: WhatsAppIncomingMessage
|
||||||
@ -65,9 +64,9 @@ export const resumeWhatsAppFlow = async ({
|
|||||||
|
|
||||||
const resumeResponse =
|
const resumeResponse =
|
||||||
session && !isSessionExpired
|
session && !isSessionExpired
|
||||||
? session.state.currentBlock
|
? await continueBotFlow({ ...session.state, whatsApp: { contact } })(
|
||||||
? await continueBotFlow(session.state)(messageContent)
|
messageContent
|
||||||
: await startBotFlow({ ...session.state, whatsApp: { contact } })
|
)
|
||||||
: workspaceId
|
: workspaceId
|
||||||
? await startWhatsAppSession({
|
? await startWhatsAppSession({
|
||||||
incomingMessage: messageContent,
|
incomingMessage: messageContent,
|
||||||
|
@ -12,11 +12,8 @@ import {
|
|||||||
WhatsAppCredentials,
|
WhatsAppCredentials,
|
||||||
defaultSessionExpiryTimeout,
|
defaultSessionExpiryTimeout,
|
||||||
} from '@typebot.io/schemas/features/whatsapp'
|
} 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 { startSession } from '../startSession'
|
||||||
import { getNextGroup } from '../getNextGroup'
|
|
||||||
import { continueBotFlow } from '../continueBotFlow'
|
|
||||||
import { upsertResult } from '../queries/upsertResult'
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
incomingMessage?: string
|
incomingMessage?: string
|
||||||
@ -80,7 +77,8 @@ export const startWhatsAppSession = async ({
|
|||||||
publicTypebot.settings.whatsApp?.sessionExpiryTimeout ??
|
publicTypebot.settings.whatsApp?.sessionExpiryTimeout ??
|
||||||
defaultSessionExpiryTimeout
|
defaultSessionExpiryTimeout
|
||||||
|
|
||||||
let chatReply = await startSession({
|
return startSession({
|
||||||
|
message: incomingMessage,
|
||||||
startParams: {
|
startParams: {
|
||||||
typebot: publicTypebot.typebot.publicId as string,
|
typebot: publicTypebot.typebot.publicId as string,
|
||||||
},
|
},
|
||||||
@ -92,31 +90,6 @@ export const startWhatsAppSession = async ({
|
|||||||
expiryTimeout: sessionExpiryTimeoutHours * 60 * 60 * 1000,
|
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 = (
|
export const messageMatchStartCondition = (
|
||||||
|
Reference in New Issue
Block a user