Skip to content

Commit 9e6a1f7

Browse files
committed
♻️ (api) Auto start bot if starting with input
Closes #877, closes #884
1 parent 2bc9dfb commit 9e6a1f7

File tree

11 files changed

+180
-39
lines changed

11 files changed

+180
-39
lines changed

apps/builder/src/features/blocks/integrations/googleSheets/googleSheets.spec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ test.describe.parallel('Google sheets integration', () => {
103103
await page.click('text=Select an operation')
104104
await page.click('text=Get data from sheet')
105105

106-
await page.getByRole('button', { name: 'Rows to filter' }).click()
106+
await page.getByRole('button', { name: 'Select row(s)' }).click()
107107
await page.getByRole('button', { name: 'Add filter rule' }).click()
108108
await page.click('text=Select a column')
109109
await page.click('button >> text="Email"')

apps/builder/src/features/blocks/integrations/sendEmail/sendEmail.spec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ test.describe('Send email block', () => {
3434
)
3535
await page.fill('[placeholder="John Smith"]', 'John Smith')
3636
await page.fill('[placeholder="mail.provider.com"]', env.SMTP_HOST)
37-
await page.fill('[placeholder="[email protected]"]', env.SMTP_USERNAME)
37+
await page.getByLabel('Username').fill(env.SMTP_USERNAME)
3838
await page.fill('[type="password"]', env.SMTP_PASSWORD)
3939
await page.fill('input[role="spinbutton"]', env.SMTP_PORT.toString())
4040
await expect(createButton).toBeEnabled()

apps/builder/src/features/customDomains/customDomains.spec.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,9 @@ test.describe('Starter workspace', () => {
5757
},
5858
])
5959
await page.goto(`/typebots/${typebotId}/share`)
60-
await expect(page.locator('[data-testid="pro-lock-tag"]')).toBeVisible()
60+
await expect(
61+
page.locator('[data-testid="pro-lock-tag"]').nth(0)
62+
).toBeVisible()
6163
await page.click('text=Add my domain')
6264
await expect(
6365
page.locator(

apps/builder/src/features/whatsapp/startWhatsAppPreview.ts

+1
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ export const startWhatsAppPreview = authenticatedProcedure
9191

9292
const { newSessionState, messages, input, clientSideActions, logs } =
9393
await startSession({
94+
message: undefined,
9495
startParams: {
9596
isOnlyRegistering: !canSendDirectMessagesToUser,
9697
typebot: typebotId,

apps/viewer/src/features/chat/api/sendMessage.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ export const sendMessage = publicProcedure
5757
logs,
5858
clientSideActions,
5959
newSessionState,
60-
} = await startSession({ startParams, userId: user?.id })
60+
} = await startSession({ startParams, userId: user?.id, message })
6161

6262
const allLogs = clientLogs ? [...(logs ?? []), ...clientLogs] : logs
6363

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
{
2+
"version": "5",
3+
"id": "clnbugp6a00011ackz0k3zfkp",
4+
"name": "My typebot",
5+
"groups": [
6+
{
7+
"id": "k2nokn9v0zyhae0wqcxsbqa7",
8+
"title": "Start",
9+
"graphCoordinates": { "x": 0, "y": 0 },
10+
"blocks": [
11+
{
12+
"id": "sx4xmdbosubnxkhcg6x521p1",
13+
"groupId": "k2nokn9v0zyhae0wqcxsbqa7",
14+
"outgoingEdgeId": "fj2ga89lctnuwcdsshwtxmhp",
15+
"type": "start",
16+
"label": "Start"
17+
}
18+
]
19+
},
20+
{
21+
"id": "g8kdars2ahr3cyz2qf1f7w4i",
22+
"title": "Group #1",
23+
"graphCoordinates": { "x": 235, "y": 170 },
24+
"blocks": [
25+
{
26+
"id": "prh6snup7cbmoxtf5vox8kjw",
27+
"groupId": "g8kdars2ahr3cyz2qf1f7w4i",
28+
"type": "text input",
29+
"options": {
30+
"labels": {
31+
"placeholder": "Type your answer...",
32+
"button": "Send"
33+
},
34+
"isLong": false
35+
}
36+
},
37+
{
38+
"id": "dpyyb38amnwwl4q461el2uf6",
39+
"groupId": "g8kdars2ahr3cyz2qf1f7w4i",
40+
"type": "text",
41+
"content": {
42+
"richText": [
43+
{ "type": "p", "children": [{ "text": "That's nice!" }] }
44+
]
45+
}
46+
}
47+
]
48+
}
49+
],
50+
"edges": [
51+
{
52+
"id": "fj2ga89lctnuwcdsshwtxmhp",
53+
"from": {
54+
"groupId": "k2nokn9v0zyhae0wqcxsbqa7",
55+
"blockId": "sx4xmdbosubnxkhcg6x521p1"
56+
},
57+
"to": { "groupId": "g8kdars2ahr3cyz2qf1f7w4i" }
58+
}
59+
],
60+
"variables": [],
61+
"theme": {
62+
"general": {
63+
"font": "Open Sans",
64+
"background": { "type": "Color", "content": "#ffffff" }
65+
},
66+
"chat": {
67+
"hostAvatar": { "isEnabled": true },
68+
"hostBubbles": { "backgroundColor": "#F7F8FF", "color": "#303235" },
69+
"guestBubbles": { "backgroundColor": "#FF8E21", "color": "#FFFFFF" },
70+
"buttons": { "backgroundColor": "#0042DA", "color": "#FFFFFF" },
71+
"inputs": {
72+
"backgroundColor": "#FFFFFF",
73+
"color": "#303235",
74+
"placeholderColor": "#9095A0"
75+
}
76+
}
77+
},
78+
"selectedThemeTemplateId": null,
79+
"settings": {
80+
"general": {
81+
"isBrandingEnabled": false,
82+
"isInputPrefillEnabled": true,
83+
"isHideQueryParamsEnabled": true,
84+
"rememberUser": { "isEnabled": false }
85+
},
86+
"typingEmulation": { "enabled": true, "speed": 300, "maxDelay": 1.5 },
87+
"metadata": {
88+
"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."
89+
}
90+
},
91+
"createdAt": "2023-10-04T14:28:55.282Z",
92+
"updatedAt": "2023-10-04T14:29:11.949Z",
93+
"icon": null,
94+
"folderId": null,
95+
"publicId": null,
96+
"customDomain": null,
97+
"workspaceId": "proWorkspace",
98+
"resultsTablePreferences": null,
99+
"isArchived": false,
100+
"isClosed": false,
101+
"whatsAppCredentialsId": null
102+
}

apps/viewer/src/test/chat.spec.ts

+29
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,13 @@ test('API chat execution should work on preview bot', async ({ request }) => {
2727
id: 'chat-sub-bot',
2828
publicId: 'chat-sub-bot-public',
2929
})
30+
await importTypebotInDatabase(
31+
getTestAsset('typebots/chat/startingWithInput.json'),
32+
{
33+
id: 'starting-with-input',
34+
publicId: 'starting-with-input-public',
35+
}
36+
)
3037
await createWebhook(typebotId, {
3138
id: 'chat-webhook-id',
3239
method: HttpMethod.GET,
@@ -218,4 +225,26 @@ test('API chat execution should work on published bot', async ({ request }) => {
218225
])
219226
expect(messages[2].content.richText.length).toBeGreaterThan(0)
220227
})
228+
await test.step('Starting with a message when typebot starts with input should proceed', async () => {
229+
const { messages } = await (
230+
await request.post(`/api/v1/sendMessage`, {
231+
data: {
232+
message: 'Hey',
233+
startParams: {
234+
typebot: 'starting-with-input-public',
235+
},
236+
} satisfies SendMessageInput,
237+
})
238+
).json()
239+
expect(messages[0].content.richText).toStrictEqual([
240+
{
241+
children: [
242+
{
243+
text: "That's nice!",
244+
},
245+
],
246+
type: 'p',
247+
},
248+
])
249+
})
221250
})

packages/bot-engine/continueBotFlow.ts

+4
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { validateRatingReply } from './blocks/inputs/rating/validateRatingReply'
2828
import { parsePictureChoicesReply } from './blocks/inputs/pictureChoice/parsePictureChoicesReply'
2929
import { parseVariables } from './variables/parseVariables'
3030
import { updateVariablesInSession } from './variables/updateVariablesInSession'
31+
import { startBotFlow } from './startBotFlow'
3132
import { TRPCError } from '@trpc/server'
3233

3334
export const continueBotFlow =
@@ -36,6 +37,9 @@ export const continueBotFlow =
3637
reply?: string
3738
): Promise<ChatReply & { newSessionState: SessionState }> => {
3839
let newSessionState = { ...state }
40+
41+
if (!newSessionState.currentBlock) return startBotFlow(state)
42+
3943
const group = state.typebotsQueue[0].typebot.groups.find(
4044
(group) => group.id === state.currentBlock?.groupId
4145
)

packages/bot-engine/startSession.ts

+32-1
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,19 @@ import { startBotFlow } from './startBotFlow'
2626
import { prefillVariables } from './variables/prefillVariables'
2727
import { deepParseVariables } from './variables/deepParseVariables'
2828
import { injectVariablesFromExistingResult } from './variables/injectVariablesFromExistingResult'
29+
import { getNextGroup } from './getNextGroup'
30+
import { upsertResult } from './queries/upsertResult'
31+
import { continueBotFlow } from './continueBotFlow'
2932

3033
type Props = {
34+
message: string | undefined
3135
startParams: StartParams
3236
userId: string | undefined
3337
initialSessionState?: Pick<SessionState, 'whatsApp' | 'expiryTimeout'>
3438
}
3539

3640
export const startSession = async ({
41+
message,
3742
startParams,
3843
userId,
3944
initialSessionState,
@@ -130,13 +135,39 @@ export const startSession = async ({
130135
}
131136
}
132137

138+
let chatReply = await startBotFlow(initialState, startParams.startGroupId)
139+
140+
// If params has message and first block is an input block, we can directly continue the bot flow
141+
if (message) {
142+
const firstEdgeId =
143+
chatReply.newSessionState.typebotsQueue[0].typebot.groups[0].blocks[0]
144+
.outgoingEdgeId
145+
const nextGroup = await getNextGroup(chatReply.newSessionState)(firstEdgeId)
146+
const newSessionState = nextGroup.newSessionState
147+
const firstBlock = nextGroup.group?.blocks.at(0)
148+
if (firstBlock && isInputBlock(firstBlock)) {
149+
const resultId = newSessionState.typebotsQueue[0].resultId
150+
if (resultId)
151+
await upsertResult({
152+
hasStarted: true,
153+
isCompleted: false,
154+
resultId,
155+
typebot: newSessionState.typebotsQueue[0].typebot,
156+
})
157+
chatReply = await continueBotFlow({
158+
...newSessionState,
159+
currentBlock: { groupId: firstBlock.groupId, blockId: firstBlock.id },
160+
})(message)
161+
}
162+
}
163+
133164
const {
134165
messages,
135166
input,
136167
clientSideActions: startFlowClientActions,
137168
newSessionState,
138169
logs,
139-
} = await startBotFlow(initialState, startParams.startGroupId)
170+
} = chatReply
140171

141172
const clientSideActions = startFlowClientActions ?? []
142173

packages/bot-engine/whatsapp/resumeWhatsAppFlow.ts

+3-4
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import { decrypt } from '@typebot.io/lib/api'
1212
import { saveStateToDatabase } from '../saveStateToDatabase'
1313
import prisma from '@typebot.io/lib/prisma'
1414
import { isDefined } from '@typebot.io/lib/utils'
15-
import { startBotFlow } from '../startBotFlow'
1615

1716
type Props = {
1817
receivedMessage: WhatsAppIncomingMessage
@@ -65,9 +64,9 @@ export const resumeWhatsAppFlow = async ({
6564

6665
const resumeResponse =
6766
session && !isSessionExpired
68-
? session.state.currentBlock
69-
? await continueBotFlow(session.state)(messageContent)
70-
: await startBotFlow({ ...session.state, whatsApp: { contact } })
67+
? await continueBotFlow({ ...session.state, whatsApp: { contact } })(
68+
messageContent
69+
)
7170
: workspaceId
7271
? await startWhatsAppSession({
7372
incomingMessage: messageContent,

packages/bot-engine/whatsapp/startWhatsAppSession.ts

+3-30
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,8 @@ import {
1212
WhatsAppCredentials,
1313
defaultSessionExpiryTimeout,
1414
} from '@typebot.io/schemas/features/whatsapp'
15-
import { isInputBlock, isNotDefined } from '@typebot.io/lib/utils'
15+
import { isNotDefined } from '@typebot.io/lib/utils'
1616
import { startSession } from '../startSession'
17-
import { getNextGroup } from '../getNextGroup'
18-
import { continueBotFlow } from '../continueBotFlow'
19-
import { upsertResult } from '../queries/upsertResult'
2017

2118
type Props = {
2219
incomingMessage?: string
@@ -80,7 +77,8 @@ export const startWhatsAppSession = async ({
8077
publicTypebot.settings.whatsApp?.sessionExpiryTimeout ??
8178
defaultSessionExpiryTimeout
8279

83-
let chatReply = await startSession({
80+
return startSession({
81+
message: incomingMessage,
8482
startParams: {
8583
typebot: publicTypebot.typebot.publicId as string,
8684
},
@@ -92,31 +90,6 @@ export const startWhatsAppSession = async ({
9290
expiryTimeout: sessionExpiryTimeoutHours * 60 * 60 * 1000,
9391
},
9492
})
95-
96-
let sessionState: SessionState = chatReply.newSessionState
97-
98-
// If first block is an input block, we can directly continue the bot flow
99-
const firstEdgeId =
100-
sessionState.typebotsQueue[0].typebot.groups[0].blocks[0].outgoingEdgeId
101-
const nextGroup = await getNextGroup(sessionState)(firstEdgeId)
102-
sessionState = nextGroup.newSessionState
103-
const firstBlock = nextGroup.group?.blocks.at(0)
104-
if (firstBlock && isInputBlock(firstBlock)) {
105-
const resultId = sessionState.typebotsQueue[0].resultId
106-
if (resultId)
107-
await upsertResult({
108-
hasStarted: true,
109-
isCompleted: false,
110-
resultId,
111-
typebot: sessionState.typebotsQueue[0].typebot,
112-
})
113-
chatReply = await continueBotFlow({
114-
...sessionState,
115-
currentBlock: { groupId: firstBlock.groupId, blockId: firstBlock.id },
116-
})(incomingMessage)
117-
}
118-
119-
return chatReply
12093
}
12194

12295
export const messageMatchStartCondition = (

0 commit comments

Comments
 (0)