2
0

🧑‍💻 (chat) Introduce startChat and continueChat endpoints

Closes #1030
This commit is contained in:
Baptiste Arnaud
2023-11-13 15:27:36 +01:00
parent 63233eb7ee
commit 084588a086
74 changed files with 28426 additions and 645 deletions

View File

@ -4,8 +4,8 @@ import * as Sentry from '@sentry/nextjs'
import { User } from '@typebot.io/prisma' import { User } from '@typebot.io/prisma'
import { NextApiRequest, NextApiResponse } from 'next' import { NextApiRequest, NextApiResponse } from 'next'
import { getServerSession } from 'next-auth' import { getServerSession } from 'next-auth'
import { mockedUser } from '../mockedUser'
import { env } from '@typebot.io/env' import { env } from '@typebot.io/env'
import { mockedUser } from '@typebot.io/lib/mockedUser'
export const getAuthenticatedUser = async ( export const getAuthenticatedUser = async (
req: NextApiRequest, req: NextApiRequest,

View File

@ -12,8 +12,6 @@ export const AudioBubbleNode = ({ url }: Props) => {
return isDefined(url) ? ( return isDefined(url) ? (
<audio src={url} controls /> <audio src={url} controls />
) : ( ) : (
<Text color={'gray.500'}> <Text color={'gray.500'}>{t('clickToEdit')}</Text>
{t('editor.blocks.bubbles.audio.node.clickToEdit.text')}
</Text>
) )
} }

View File

@ -9,10 +9,6 @@ type Props = {
export const EmbedBubbleContent = ({ block }: Props) => { export const EmbedBubbleContent = ({ block }: Props) => {
const { t } = useTranslate() const { t } = useTranslate()
if (!block.content?.url) if (!block.content?.url)
return ( return <Text color="gray.500">{t('clickToEdit')}</Text>
<Text color="gray.500">
{t('editor.blocks.bubbles.embed.node.clickToEdit.text')}
</Text>
)
return <Text>{t('editor.blocks.bubbles.embed.node.show.text')}</Text> return <Text>{t('editor.blocks.bubbles.embed.node.show.text')}</Text>
} }

View File

@ -11,9 +11,7 @@ export const ImageBubbleContent = ({ block }: Props) => {
const containsVariables = const containsVariables =
block.content?.url?.includes('{{') && block.content.url.includes('}}') block.content?.url?.includes('{{') && block.content.url.includes('}}')
return !block.content?.url ? ( return !block.content?.url ? (
<Text color={'gray.500'}> <Text color={'gray.500'}>{t('clickToEdit')}</Text>
{t('editor.blocks.bubbles.image.node.clickToEdit.text')}
</Text>
) : ( ) : (
<Box w="full"> <Box w="full">
<Image <Image

View File

@ -13,11 +13,7 @@ type Props = {
export const VideoBubbleContent = ({ block }: Props) => { export const VideoBubbleContent = ({ block }: Props) => {
const { t } = useTranslate() const { t } = useTranslate()
if (!block.content?.url || !block.content.type) if (!block.content?.url || !block.content.type)
return ( return <Text color="gray.500">{t('clickToEdit')}</Text>
<Text color="gray.500">
{t('editor.blocks.bubbles.video.node.clickToEdit.text')}
</Text>
)
const containsVariables = const containsVariables =
block.content?.url?.includes('{{') && block.content.url.includes('}}') block.content?.url?.includes('{{') && block.content.url.includes('}}')
switch (block.content.type) { switch (block.content.type) {

View File

@ -32,7 +32,7 @@ test('should be configurable', async ({ page }) => {
await expect(page.getByTestId('selected-item-label').first()).toHaveText( await expect(page.getByTestId('selected-item-label').first()).toHaveText(
'My link typebot 2' 'My link typebot 2'
) )
await page.click('input[placeholder="Select a block"]') await page.click('input[placeholder="Select a group"]')
await page.click('text=Group #2') await page.click('text=Group #2')
await page.click('text=Preview') await page.click('text=Preview')

View File

@ -4,7 +4,7 @@ import { useTypebot } from '@/features/editor/providers/TypebotProvider'
import { useGraph } from '@/features/graph/providers/GraphProvider' import { useGraph } from '@/features/graph/providers/GraphProvider'
import { useToast } from '@/hooks/useToast' import { useToast } from '@/hooks/useToast'
import { Standard } from '@typebot.io/nextjs' import { Standard } from '@typebot.io/nextjs'
import { ChatReply } from '@typebot.io/schemas' import { ContinueChatResponse } from '@typebot.io/schemas'
export const WebPreview = () => { export const WebPreview = () => {
const { typebot } = useTypebot() const { typebot } = useTypebot()
@ -13,7 +13,7 @@ export const WebPreview = () => {
const { showToast } = useToast() const { showToast } = useToast()
const handleNewLogs = (logs: ChatReply['logs']) => { const handleNewLogs = (logs: ContinueChatResponse['logs']) => {
logs?.forEach((log) => { logs?.forEach((log) => {
showToast({ showToast({
icon: <WebhookIcon />, icon: <WebhookIcon />,
@ -40,8 +40,13 @@ export const WebPreview = () => {
<Standard <Standard
key={`web-preview${startPreviewAtGroup ?? ''}`} key={`web-preview${startPreviewAtGroup ?? ''}`}
typebot={typebot} typebot={typebot}
startGroupId={startPreviewAtGroup} startFrom={
startEventId={startPreviewAtEvent} startPreviewAtGroup
? { type: 'group', groupId: startPreviewAtGroup }
: startPreviewAtEvent
? { type: 'event', eventId: startPreviewAtEvent }
: undefined
}
onNewInputBlock={(block) => onNewInputBlock={(block) =>
setPreviewingBlock({ setPreviewingBlock({
id: block.id, id: block.id,

View File

@ -24,7 +24,7 @@ import { BuoyIcon, ExternalLinkIcon } from '@/components/icons'
export const WhatsAppPreviewInstructions = (props: StackProps) => { export const WhatsAppPreviewInstructions = (props: StackProps) => {
const { typebot, save } = useTypebot() const { typebot, save } = useTypebot()
const { startPreviewAtGroup } = useEditor() const { startPreviewAtGroup, startPreviewAtEvent } = useEditor()
const [phoneNumber, setPhoneNumber] = useState( const [phoneNumber, setPhoneNumber] = useState(
getPhoneNumberFromLocalStorage() ?? '' getPhoneNumberFromLocalStorage() ?? ''
) )
@ -56,7 +56,11 @@ export const WhatsAppPreviewInstructions = (props: StackProps) => {
mutate({ mutate({
to: phoneNumber, to: phoneNumber,
typebotId: typebot.id, typebotId: typebot.id,
startGroupId: startPreviewAtGroup, startFrom: startPreviewAtGroup
? { type: 'group', groupId: startPreviewAtGroup }
: startPreviewAtEvent
? { type: 'event', eventId: startPreviewAtEvent }
: undefined,
}) })
} }

View File

@ -10,7 +10,7 @@ import { restartSession } from '@typebot.io/bot-engine/queries/restartSession'
import { sendChatReplyToWhatsApp } from '@typebot.io/bot-engine/whatsapp/sendChatReplyToWhatsApp' import { sendChatReplyToWhatsApp } from '@typebot.io/bot-engine/whatsapp/sendChatReplyToWhatsApp'
import { sendWhatsAppMessage } from '@typebot.io/bot-engine/whatsapp/sendWhatsAppMessage' import { sendWhatsAppMessage } from '@typebot.io/bot-engine/whatsapp/sendWhatsAppMessage'
import { isReadTypebotForbidden } from '../typebot/helpers/isReadTypebotForbidden' import { isReadTypebotForbidden } from '../typebot/helpers/isReadTypebotForbidden'
import { SessionState } from '@typebot.io/schemas' import { SessionState, startFromSchema } from '@typebot.io/schemas'
export const startWhatsAppPreview = authenticatedProcedure export const startWhatsAppPreview = authenticatedProcedure
.meta({ .meta({
@ -31,7 +31,7 @@ export const startWhatsAppPreview = authenticatedProcedure
value.replace(/\s/g, '').replace(/\+/g, '').replace(/-/g, '') value.replace(/\s/g, '').replace(/\+/g, '').replace(/-/g, '')
), ),
typebotId: z.string(), typebotId: z.string(),
startGroupId: z.string().optional(), startFrom: startFromSchema.optional(),
}) })
) )
.output( .output(
@ -39,135 +39,133 @@ export const startWhatsAppPreview = authenticatedProcedure
message: z.string(), message: z.string(),
}) })
) )
.mutation( .mutation(async ({ input: { to, typebotId, startFrom }, ctx: { user } }) => {
async ({ input: { to, typebotId, startGroupId }, ctx: { user } }) => { if (
if ( !env.WHATSAPP_PREVIEW_FROM_PHONE_NUMBER_ID ||
!env.WHATSAPP_PREVIEW_FROM_PHONE_NUMBER_ID || !env.META_SYSTEM_USER_TOKEN ||
!env.META_SYSTEM_USER_TOKEN || !env.WHATSAPP_PREVIEW_TEMPLATE_NAME
!env.WHATSAPP_PREVIEW_TEMPLATE_NAME )
) throw new TRPCError({
throw new TRPCError({ code: 'BAD_REQUEST',
code: 'BAD_REQUEST', message:
message: 'Missing WHATSAPP_PREVIEW_FROM_PHONE_NUMBER_ID or META_SYSTEM_USER_TOKEN or WHATSAPP_PREVIEW_TEMPLATE_NAME env variables',
'Missing WHATSAPP_PREVIEW_FROM_PHONE_NUMBER_ID or META_SYSTEM_USER_TOKEN or WHATSAPP_PREVIEW_TEMPLATE_NAME env variables', })
})
const existingTypebot = await prisma.typebot.findFirst({ const existingTypebot = await prisma.typebot.findFirst({
where: { where: {
id: typebotId, id: typebotId,
}, },
select: { select: {
id: true, id: true,
workspaceId: true, workspaceId: true,
collaborators: { collaborators: {
select: { select: {
userId: true, userId: true,
},
}, },
}, },
}) },
if ( })
!existingTypebot?.id || if (
(await isReadTypebotForbidden(existingTypebot, user)) !existingTypebot?.id ||
) (await isReadTypebotForbidden(existingTypebot, user))
throw new TRPCError({ code: 'NOT_FOUND', message: 'Typebot not found' }) )
throw new TRPCError({ code: 'NOT_FOUND', message: 'Typebot not found' })
const sessionId = `wa-preview-${to}` const sessionId = `wa-preview-${to}`
const existingSession = await prisma.chatSession.findFirst({ const existingSession = await prisma.chatSession.findFirst({
where: { where: {
id: sessionId, id: sessionId,
}, },
select: { select: {
updatedAt: true, updatedAt: true,
state: true, state: true,
}, },
}) })
// For users that did not interact with the bot in the last 24 hours, we need to send a template message. // For users that did not interact with the bot in the last 24 hours, we need to send a template message.
const canSendDirectMessagesToUser = const canSendDirectMessagesToUser =
(existingSession?.updatedAt.getTime() ?? 0) > (existingSession?.updatedAt.getTime() ?? 0) >
Date.now() - 24 * 60 * 60 * 1000 Date.now() - 24 * 60 * 60 * 1000
const { const {
newSessionState, newSessionState,
messages,
input,
clientSideActions,
logs,
visitedEdges,
} = await startSession({
version: 2,
message: undefined,
startParams: {
isOnlyRegistering: !canSendDirectMessagesToUser,
type: 'preview',
typebotId,
startFrom,
userId: user.id,
},
initialSessionState: {
whatsApp: (existingSession?.state as SessionState | undefined)
?.whatsApp,
},
})
if (canSendDirectMessagesToUser) {
await sendChatReplyToWhatsApp({
to,
typingEmulation: newSessionState.typingEmulation,
messages, messages,
input, input,
clientSideActions, clientSideActions,
logs, credentials: {
visitedEdges, phoneNumberId: env.WHATSAPP_PREVIEW_FROM_PHONE_NUMBER_ID,
} = await startSession({ systemUserAccessToken: env.META_SYSTEM_USER_TOKEN,
version: 2,
message: undefined,
startParams: {
isOnlyRegistering: !canSendDirectMessagesToUser,
typebot: typebotId,
isPreview: true,
startGroupId,
},
userId: user.id,
initialSessionState: {
whatsApp: (existingSession?.state as SessionState | undefined)
?.whatsApp,
}, },
state: newSessionState,
}) })
await saveStateToDatabase({
if (canSendDirectMessagesToUser) { clientSideActions: [],
await sendChatReplyToWhatsApp({ input,
logs,
session: {
id: sessionId,
state: newSessionState,
},
visitedEdges,
})
} else {
await restartSession({
state: newSessionState,
id: sessionId,
})
try {
await sendWhatsAppMessage({
to, to,
typingEmulation: newSessionState.typingEmulation, message: {
messages, type: 'template',
input, template: {
clientSideActions, language: {
code: env.WHATSAPP_PREVIEW_TEMPLATE_LANG,
},
name: env.WHATSAPP_PREVIEW_TEMPLATE_NAME,
},
},
credentials: { credentials: {
phoneNumberId: env.WHATSAPP_PREVIEW_FROM_PHONE_NUMBER_ID, phoneNumberId: env.WHATSAPP_PREVIEW_FROM_PHONE_NUMBER_ID,
systemUserAccessToken: env.META_SYSTEM_USER_TOKEN, systemUserAccessToken: env.META_SYSTEM_USER_TOKEN,
}, },
state: newSessionState,
}) })
await saveStateToDatabase({ } catch (err) {
clientSideActions: [], if (err instanceof HTTPError) console.log(err.response.body)
input, throw new TRPCError({
logs, code: 'INTERNAL_SERVER_ERROR',
session: { message: 'Request to Meta to send preview message failed',
id: sessionId, cause: err,
state: newSessionState,
},
visitedEdges,
}) })
} else {
await restartSession({
state: newSessionState,
id: sessionId,
})
try {
await sendWhatsAppMessage({
to,
message: {
type: 'template',
template: {
language: {
code: env.WHATSAPP_PREVIEW_TEMPLATE_LANG,
},
name: env.WHATSAPP_PREVIEW_TEMPLATE_NAME,
},
},
credentials: {
phoneNumberId: env.WHATSAPP_PREVIEW_FROM_PHONE_NUMBER_ID,
systemUserAccessToken: env.META_SYSTEM_USER_TOKEN,
},
})
} catch (err) {
if (err instanceof HTTPError) console.log(err.response.body)
throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message: 'Request to Meta to send preview message failed',
cause: err,
})
}
}
return {
message: 'success',
} }
} }
) return {
message: 'success',
}
})

View File

@ -11,7 +11,7 @@ import { NextApiRequest, NextApiResponse } from 'next'
import { customAdapter } from '../../../features/auth/api/customAdapter' import { customAdapter } from '../../../features/auth/api/customAdapter'
import { User } from '@typebot.io/prisma' import { User } from '@typebot.io/prisma'
import { getAtPath, isDefined } from '@typebot.io/lib' import { getAtPath, isDefined } from '@typebot.io/lib'
import { mockedUser } from '@/features/auth/mockedUser' import { mockedUser } from '@typebot.io/lib/mockedUser'
import { getNewUserInvitations } from '@/features/auth/helpers/getNewUserInvitations' import { getNewUserInvitations } from '@/features/auth/helpers/getNewUserInvitations'
import { sendVerificationRequest } from '@/features/auth/helpers/sendVerificationRequest' import { sendVerificationRequest } from '@/features/auth/helpers/sendVerificationRequest'
import { Ratelimit } from '@upstash/ratelimit' import { Ratelimit } from '@upstash/ratelimit'

View File

@ -624,7 +624,9 @@
"enum": [ "enum": [
"url", "url",
"youtube", "youtube",
"vimeo" "vimeo",
"tiktok",
"gumlet"
] ]
}, },
"height": { "height": {
@ -634,6 +636,12 @@
}, },
{} {}
] ]
},
"aspectRatio": {
"type": "string"
},
"maxWidth": {
"type": "string"
} }
}, },
"additionalProperties": false "additionalProperties": false
@ -3929,7 +3937,9 @@
"enum": [ "enum": [
"url", "url",
"youtube", "youtube",
"vimeo" "vimeo",
"tiktok",
"gumlet"
] ]
}, },
"height": { "height": {
@ -3939,6 +3949,12 @@
}, },
{} {}
] ]
},
"aspectRatio": {
"type": "string"
},
"maxWidth": {
"type": "string"
} }
}, },
"additionalProperties": false "additionalProperties": false
@ -8007,7 +8023,9 @@
"enum": [ "enum": [
"url", "url",
"youtube", "youtube",
"vimeo" "vimeo",
"tiktok",
"gumlet"
] ]
}, },
"height": { "height": {
@ -8017,6 +8035,12 @@
}, },
{} {}
] ]
},
"aspectRatio": {
"type": "string"
},
"maxWidth": {
"type": "string"
} }
}, },
"additionalProperties": false "additionalProperties": false
@ -11708,7 +11732,9 @@
"enum": [ "enum": [
"url", "url",
"youtube", "youtube",
"vimeo" "vimeo",
"tiktok",
"gumlet"
] ]
}, },
"height": { "height": {
@ -11718,6 +11744,12 @@
}, },
{} {}
] ]
},
"aspectRatio": {
"type": "string"
},
"maxWidth": {
"type": "string"
} }
}, },
"additionalProperties": false "additionalProperties": false
@ -15497,7 +15529,9 @@
"enum": [ "enum": [
"url", "url",
"youtube", "youtube",
"vimeo" "vimeo",
"tiktok",
"gumlet"
] ]
}, },
"height": { "height": {
@ -15507,6 +15541,12 @@
}, },
{} {}
] ]
},
"aspectRatio": {
"type": "string"
},
"maxWidth": {
"type": "string"
} }
}, },
"additionalProperties": false "additionalProperties": false
@ -19195,7 +19235,9 @@
"enum": [ "enum": [
"url", "url",
"youtube", "youtube",
"vimeo" "vimeo",
"tiktok",
"gumlet"
] ]
}, },
"height": { "height": {
@ -19205,6 +19247,12 @@
}, },
{} {}
] ]
},
"aspectRatio": {
"type": "string"
},
"maxWidth": {
"type": "string"
} }
}, },
"additionalProperties": false "additionalProperties": false
@ -22918,7 +22966,9 @@
"enum": [ "enum": [
"url", "url",
"youtube", "youtube",
"vimeo" "vimeo",
"tiktok",
"gumlet"
] ]
}, },
"height": { "height": {
@ -22928,6 +22978,12 @@
}, },
{} {}
] ]
},
"aspectRatio": {
"type": "string"
},
"maxWidth": {
"type": "string"
} }
}, },
"additionalProperties": false "additionalProperties": false
@ -26647,7 +26703,9 @@
"enum": [ "enum": [
"url", "url",
"youtube", "youtube",
"vimeo" "vimeo",
"tiktok",
"gumlet"
] ]
}, },
"height": { "height": {
@ -26657,6 +26715,12 @@
}, },
{} {}
] ]
},
"aspectRatio": {
"type": "string"
},
"maxWidth": {
"type": "string"
} }
}, },
"additionalProperties": false "additionalProperties": false
@ -30203,7 +30267,6 @@
"version", "version",
"id", "id",
"name", "name",
"events",
"groups", "groups",
"edges", "edges",
"variables", "variables",
@ -30425,7 +30488,9 @@
"enum": [ "enum": [
"url", "url",
"youtube", "youtube",
"vimeo" "vimeo",
"tiktok",
"gumlet"
] ]
}, },
"height": { "height": {
@ -30435,6 +30500,12 @@
}, },
{} {}
] ]
},
"aspectRatio": {
"type": "string"
},
"maxWidth": {
"type": "string"
} }
}, },
"additionalProperties": false "additionalProperties": false
@ -34213,7 +34284,9 @@
"enum": [ "enum": [
"url", "url",
"youtube", "youtube",
"vimeo" "vimeo",
"tiktok",
"gumlet"
] ]
}, },
"height": { "height": {
@ -34223,6 +34296,12 @@
}, },
{} {}
] ]
},
"aspectRatio": {
"type": "string"
},
"maxWidth": {
"type": "string"
} }
}, },
"additionalProperties": false "additionalProperties": false
@ -37878,7 +37957,9 @@
"enum": [ "enum": [
"url", "url",
"youtube", "youtube",
"vimeo" "vimeo",
"tiktok",
"gumlet"
] ]
}, },
"height": { "height": {
@ -37888,6 +37969,12 @@
}, },
{} {}
] ]
},
"aspectRatio": {
"type": "string"
},
"maxWidth": {
"type": "string"
} }
}, },
"additionalProperties": false "additionalProperties": false
@ -41678,7 +41765,9 @@
"enum": [ "enum": [
"url", "url",
"youtube", "youtube",
"vimeo" "vimeo",
"tiktok",
"gumlet"
] ]
}, },
"height": { "height": {
@ -41688,6 +41777,12 @@
}, },
{} {}
] ]
},
"aspectRatio": {
"type": "string"
},
"maxWidth": {
"type": "string"
} }
}, },
"additionalProperties": false "additionalProperties": false
@ -45166,10 +45261,6 @@
"type": "string", "type": "string",
"nullable": true "nullable": true
}, },
"publicId": {
"type": "string",
"nullable": true
},
"resultsTablePreferences": { "resultsTablePreferences": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -45208,15 +45299,13 @@
"required": [ "required": [
"version", "version",
"name", "name",
"events",
"groups", "groups",
"edges", "edges",
"variables", "variables",
"theme", "theme",
"settings", "settings",
"icon", "icon",
"folderId", "folderId"
"publicId"
], ],
"additionalProperties": false "additionalProperties": false
}, },
@ -45418,7 +45507,9 @@
"enum": [ "enum": [
"url", "url",
"youtube", "youtube",
"vimeo" "vimeo",
"tiktok",
"gumlet"
] ]
}, },
"height": { "height": {
@ -45428,6 +45519,12 @@
}, },
{} {}
] ]
},
"aspectRatio": {
"type": "string"
},
"maxWidth": {
"type": "string"
} }
}, },
"additionalProperties": false "additionalProperties": false
@ -48816,10 +48913,6 @@
"type": "string", "type": "string",
"nullable": true "nullable": true
}, },
"publicId": {
"type": "string",
"nullable": true
},
"resultsTablePreferences": { "resultsTablePreferences": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -48865,8 +48958,7 @@
"theme", "theme",
"settings", "settings",
"icon", "icon",
"folderId", "folderId"
"publicId"
], ],
"additionalProperties": false "additionalProperties": false
} }
@ -49092,7 +49184,9 @@
"enum": [ "enum": [
"url", "url",
"youtube", "youtube",
"vimeo" "vimeo",
"tiktok",
"gumlet"
] ]
}, },
"height": { "height": {
@ -49102,6 +49196,12 @@
}, },
{} {}
] ]
},
"aspectRatio": {
"type": "string"
},
"maxWidth": {
"type": "string"
} }
}, },
"additionalProperties": false "additionalProperties": false
@ -55954,8 +56054,47 @@
"type": "string", "type": "string",
"minLength": 1 "minLength": 1
}, },
"startGroupId": { "startFrom": {
"type": "string" "anyOf": [
{
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"group"
]
},
"groupId": {
"type": "string"
}
},
"required": [
"type",
"groupId"
],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"event"
]
},
"eventId": {
"type": "string"
}
},
"required": [
"type",
"eventId"
],
"additionalProperties": false
}
]
} }
}, },
"required": [ "required": [

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,79 @@
import { publicProcedure } from '@/helpers/server/trpc'
import { continueChatResponseSchema } from '@typebot.io/schemas/features/chat/schema'
import { TRPCError } from '@trpc/server'
import { getSession } from '@typebot.io/bot-engine/queries/getSession'
import { saveStateToDatabase } from '@typebot.io/bot-engine/saveStateToDatabase'
import { continueBotFlow } from '@typebot.io/bot-engine/continueBotFlow'
import { parseDynamicTheme } from '@typebot.io/bot-engine/parseDynamicTheme'
import { isDefined } from '@typebot.io/lib/utils'
import { z } from 'zod'
export const continueChat = publicProcedure
.meta({
openapi: {
method: 'POST',
path: '/v1/sessions/{sessionId}/continueChat',
summary: 'Continue chat',
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(
z.object({
message: z.string().optional(),
sessionId: z.string(),
})
)
.output(continueChatResponseSchema)
.mutation(async ({ input: { sessionId, message } }) => {
const session = await getSession(sessionId)
if (!session) {
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Session not found.',
})
}
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.',
})
const {
messages,
input,
clientSideActions,
newSessionState,
logs,
lastMessageNewFormat,
visitedEdges,
} = await continueBotFlow(message, { version: 2, state: session.state })
if (newSessionState)
await saveStateToDatabase({
session: {
id: session.id,
state: newSessionState,
},
input,
logs,
clientSideActions,
visitedEdges,
})
return {
messages,
input,
clientSideActions,
dynamicTheme: parseDynamicTheme(newSessionState),
logs,
lastMessageNewFormat,
}
})

View File

@ -1,8 +1,8 @@
import { publicProcedure } from '@/helpers/server/trpc' import { publicProcedure } from '@/helpers/server/trpc'
import { import {
chatReplySchema,
sendMessageInputSchema, sendMessageInputSchema,
} from '@typebot.io/schemas/features/chat/schema' chatReplySchema,
} from '@typebot.io/schemas/features/chat/legacy/schema'
import { TRPCError } from '@trpc/server' import { TRPCError } from '@trpc/server'
import { getSession } from '@typebot.io/bot-engine/queries/getSession' import { getSession } from '@typebot.io/bot-engine/queries/getSession'
import { startSession } from '@typebot.io/bot-engine/startSession' import { startSession } from '@typebot.io/bot-engine/startSession'
@ -16,10 +16,12 @@ export const sendMessageV1 = publicProcedure
.meta({ .meta({
openapi: { openapi: {
method: 'POST', method: 'POST',
path: '/sendMessage', path: '/v1/sendMessage',
summary: 'Send a message', summary: 'Send a message',
description: 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.', '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.',
tags: ['Deprecated'],
deprecated: true,
}, },
}) })
.input(sendMessageInputSchema) .input(sendMessageInputSchema)
@ -60,8 +62,45 @@ export const sendMessageV1 = publicProcedure
visitedEdges, visitedEdges,
} = await startSession({ } = await startSession({
version: 1, version: 1,
startParams, startParams:
userId: user?.id, startParams.isPreview || typeof startParams.typebot !== 'string'
? {
type: 'preview',
isOnlyRegistering: startParams.isOnlyRegistering ?? false,
isStreamEnabled: startParams.isStreamEnabled,
startFrom:
'startGroupId' in startParams && startParams.startGroupId
? {
type: 'group',
groupId: startParams.startGroupId,
}
: 'startEventId' in startParams &&
startParams.startEventId
? {
type: 'event',
eventId: startParams.startEventId,
}
: undefined,
typebotId:
typeof startParams.typebot === 'string'
? startParams.typebot
: startParams.typebot.id,
typebot:
typeof startParams.typebot === 'string'
? undefined
: startParams.typebot,
message,
userId: parseUserId(user?.id),
}
: {
type: 'live',
isOnlyRegistering: startParams.isOnlyRegistering ?? false,
isStreamEnabled: startParams.isStreamEnabled,
publicId: startParams.typebot,
prefilledVariables: startParams.prefilledVariables,
resultId: startParams.resultId,
message,
},
message, message,
}) })
@ -133,3 +172,13 @@ export const sendMessageV1 = publicProcedure
} }
} }
) )
const parseUserId = (userId?: string): string => {
if (!userId)
throw new TRPCError({
code: 'UNAUTHORIZED',
message: 'You need to be authenticated to perform this action',
})
return userId
}

View File

@ -1,8 +1,4 @@
import { publicProcedure } from '@/helpers/server/trpc' import { publicProcedure } from '@/helpers/server/trpc'
import {
chatReplySchema,
sendMessageInputSchema,
} from '@typebot.io/schemas/features/chat/schema'
import { TRPCError } from '@trpc/server' import { TRPCError } from '@trpc/server'
import { getSession } from '@typebot.io/bot-engine/queries/getSession' import { getSession } from '@typebot.io/bot-engine/queries/getSession'
import { startSession } from '@typebot.io/bot-engine/startSession' import { startSession } from '@typebot.io/bot-engine/startSession'
@ -11,15 +7,21 @@ import { restartSession } from '@typebot.io/bot-engine/queries/restartSession'
import { continueBotFlow } from '@typebot.io/bot-engine/continueBotFlow' 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'
import {
chatReplySchema,
sendMessageInputSchema,
} from '@typebot.io/schemas/features/chat/legacy/schema'
export const sendMessageV2 = publicProcedure export const sendMessageV2 = publicProcedure
.meta({ .meta({
openapi: { openapi: {
method: 'POST', method: 'POST',
path: '/sendMessage', path: '/v2/sendMessage',
summary: 'Send a message', summary: 'Send a message',
description: 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.', '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.',
tags: ['Deprecated'],
deprecated: true,
}, },
}) })
.input(sendMessageInputSchema) .input(sendMessageInputSchema)
@ -60,8 +62,45 @@ export const sendMessageV2 = publicProcedure
visitedEdges, visitedEdges,
} = await startSession({ } = await startSession({
version: 2, version: 2,
startParams, startParams:
userId: user?.id, startParams.isPreview || typeof startParams.typebot !== 'string'
? {
type: 'preview',
isOnlyRegistering: startParams.isOnlyRegistering ?? false,
isStreamEnabled: startParams.isStreamEnabled,
startFrom:
'startGroupId' in startParams && startParams.startGroupId
? {
type: 'group',
groupId: startParams.startGroupId,
}
: 'startEventId' in startParams &&
startParams.startEventId
? {
type: 'event',
eventId: startParams.startEventId,
}
: undefined,
typebotId:
typeof startParams.typebot === 'string'
? startParams.typebot
: startParams.typebot.id,
typebot:
typeof startParams.typebot === 'string'
? undefined
: startParams.typebot,
message,
userId: parseUserId(user?.id),
}
: {
type: 'live',
isOnlyRegistering: startParams.isOnlyRegistering ?? false,
isStreamEnabled: startParams.isStreamEnabled,
publicId: startParams.typebot,
prefilledVariables: startParams.prefilledVariables,
resultId: startParams.resultId,
message,
},
message, message,
}) })
@ -133,3 +172,13 @@ export const sendMessageV2 = publicProcedure
} }
} }
) )
const parseUserId = (userId?: string): string => {
if (!userId)
throw new TRPCError({
code: 'UNAUTHORIZED',
message: 'You need to be authenticated to perform this action',
})
return userId
}

View File

@ -0,0 +1,63 @@
import { publicProcedure } from '@/helpers/server/trpc'
import { chatLogSchema } from '@typebot.io/schemas/features/chat/schema'
import { TRPCError } from '@trpc/server'
import { getSession } from '@typebot.io/bot-engine/queries/getSession'
import { z } from 'zod'
import { saveLogs } from '@typebot.io/bot-engine/queries/saveLogs'
import { formatLogDetails } from '@typebot.io/bot-engine/logs/helpers/formatLogDetails'
import * as Sentry from '@sentry/nextjs'
export const saveClientLogs = publicProcedure
.meta({
openapi: {
method: 'POST',
path: '/v1/sessions/{sessionId}/clientLogs',
summary: 'Save client logs',
},
})
.input(
z.object({
sessionId: z.string(),
clientLogs: z.array(chatLogSchema),
})
)
.output(z.object({ message: z.string() }))
.mutation(async ({ input: { sessionId, clientLogs } }) => {
const session = await getSession(sessionId)
if (!session) {
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Session not found.',
})
}
const resultId = session.state.typebotsQueue[0].resultId
if (!resultId) {
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Result not found.',
})
}
try {
await saveLogs(
clientLogs.map((log) => ({
...log,
resultId,
details: formatLogDetails(log.details),
}))
)
return {
message: 'Logs successfully saved.',
}
} catch (e) {
console.error('Failed to save logs', e)
Sentry.captureException(e)
throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message: 'Failed to save logs.',
})
}
})

View File

@ -0,0 +1,83 @@
import { publicProcedure } from '@/helpers/server/trpc'
import {
startChatInputSchema,
startChatResponseSchema,
} from '@typebot.io/schemas/features/chat/schema'
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'
export const startChat = publicProcedure
.meta({
openapi: {
method: 'POST',
path: '/v1/typebots/{publicId}/startChat',
summary: 'Start chat',
},
})
.input(startChatInputSchema)
.output(startChatResponseSchema)
.mutation(
async ({
input: {
message,
isOnlyRegistering,
publicId,
isStreamEnabled,
prefilledVariables,
resultId: startResultId,
},
}) => {
const {
typebot,
messages,
input,
resultId,
dynamicTheme,
logs,
clientSideActions,
newSessionState,
visitedEdges,
} = await startSession({
version: 2,
startParams: {
type: 'live',
isOnlyRegistering,
isStreamEnabled,
publicId,
prefilledVariables,
resultId: startResultId,
},
message,
})
const session = isOnlyRegistering
? await restartSession({
state: newSessionState,
})
: await saveStateToDatabase({
session: {
state: newSessionState,
},
input,
logs,
clientSideActions,
visitedEdges,
})
return {
sessionId: session.id,
typebot: {
id: typebot.id,
theme: typebot.theme,
settings: typebot.settings,
},
messages,
input,
resultId,
dynamicTheme,
logs,
clientSideActions,
}
}
)

View File

@ -0,0 +1,83 @@
import { authenticatedProcedure } from '@/helpers/server/trpc'
import {
startPreviewChatInputSchema,
startPreviewChatResponseSchema,
} from '@typebot.io/schemas/features/chat/schema'
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'
export const startChatPreview = authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/v1/typebots/{typebotId}/preview/startChat',
summary: 'Start preview chat',
},
})
.input(startPreviewChatInputSchema)
.output(startPreviewChatResponseSchema)
.mutation(
async ({
input: {
message,
isOnlyRegistering,
isStreamEnabled,
startFrom,
typebotId,
typebot: startTypebot,
},
ctx: { user },
}) => {
const {
typebot,
messages,
input,
dynamicTheme,
logs,
clientSideActions,
newSessionState,
visitedEdges,
} = await startSession({
version: 2,
startParams: {
type: 'preview',
isOnlyRegistering,
isStreamEnabled,
startFrom,
typebotId,
typebot: startTypebot,
userId: user.id,
},
message,
})
const session = isOnlyRegistering
? await restartSession({
state: newSessionState,
})
: await saveStateToDatabase({
session: {
state: newSessionState,
},
input,
logs,
clientSideActions,
visitedEdges,
})
return {
sessionId: session.id,
typebot: {
id: typebot.id,
theme: typebot.theme,
settings: typebot.settings,
},
messages,
input,
dynamicTheme,
logs,
clientSideActions,
}
}
)

View File

@ -1,4 +1,3 @@
import { publicProcedure } from '@/helpers/server/trpc'
import { TRPCError } from '@trpc/server' import { TRPCError } from '@trpc/server'
import { z } from 'zod' import { z } from 'zod'
import { getSession } from '@typebot.io/bot-engine/queries/getSession' import { getSession } from '@typebot.io/bot-engine/queries/getSession'
@ -9,12 +8,13 @@ import {
Variable, Variable,
} from '@typebot.io/schemas' } from '@typebot.io/schemas'
import prisma from '@typebot.io/lib/prisma' import prisma from '@typebot.io/lib/prisma'
import { authenticatedProcedure } from '@/helpers/server/trpc'
export const updateTypebotInSession = publicProcedure export const updateTypebotInSession = authenticatedProcedure
.meta({ .meta({
openapi: { openapi: {
method: 'POST', method: 'POST',
path: '/sessions/{sessionId}/updateTypebot', path: '/v1/sessions/{sessionId}/updateTypebot',
summary: 'Update typebot in session', summary: 'Update typebot in session',
description: description:
'Update chat session with latest typebot modifications. This is useful when you want to update the typebot in an ongoing session after making changes to it.', 'Update chat session with latest typebot modifications. This is useful when you want to update the typebot in an ongoing session after making changes to it.',
@ -28,8 +28,6 @@ export const updateTypebotInSession = publicProcedure
) )
.output(z.object({ message: z.literal('success') })) .output(z.object({ message: z.literal('success') }))
.mutation(async ({ input: { sessionId }, ctx: { user } }) => { .mutation(async ({ input: { sessionId }, ctx: { user } }) => {
if (!user)
throw new TRPCError({ code: 'UNAUTHORIZED', message: 'Unauthorized' })
const session = await getSession(sessionId) const session = await getSession(sessionId)
if (!session) if (!session)
throw new TRPCError({ code: 'NOT_FOUND', message: 'Session not found' }) throw new TRPCError({ code: 'NOT_FOUND', message: 'Session not found' })

View File

@ -19,10 +19,11 @@ export const getUploadUrl = publicProcedure
.meta({ .meta({
openapi: { openapi: {
method: 'GET', method: 'GET',
path: '/typebots/{typebotId}/blocks/{blockId}/storage/upload-url', path: '/v1/typebots/{typebotId}/blocks/{blockId}/storage/upload-url',
summary: 'Get upload URL for a file', summary: 'Get upload URL for a file',
description: 'Used for the web client to get the bucket upload file.', description: 'Used for the web client to get the bucket upload file.',
deprecated: true, deprecated: true,
tags: ['Deprecated'],
}, },
}) })
.input( .input(

View File

@ -13,7 +13,7 @@ export const generateUploadUrl = publicProcedure
.meta({ .meta({
openapi: { openapi: {
method: 'POST', method: 'POST',
path: '/generate-upload-url', path: '/v1/generate-upload-url',
summary: 'Generate upload URL', summary: 'Generate upload URL',
description: 'Used to upload anything from the client to S3 bucket', description: 'Used to upload anything from the client to S3 bucket',
}, },

View File

@ -8,7 +8,7 @@ export const receiveMessage = publicProcedure
.meta({ .meta({
openapi: { openapi: {
method: 'POST', method: 'POST',
path: '/workspaces/{workspaceId}/whatsapp/{credentialsId}/webhook', path: '/v1/workspaces/{workspaceId}/whatsapp/{credentialsId}/webhook',
summary: 'Message webhook', summary: 'Message webhook',
tags: ['WhatsApp'], tags: ['WhatsApp'],
}, },

View File

@ -7,7 +7,7 @@ export const subscribeWebhook = publicProcedure
.meta({ .meta({
openapi: { openapi: {
method: 'GET', method: 'GET',
path: '/workspaces/{workspaceId}/whatsapp/{credentialsId}/webhook', path: '/v1/workspaces/{workspaceId}/whatsapp/{credentialsId}/webhook',
summary: 'Subscribe webhook', summary: 'Subscribe webhook',
tags: ['WhatsApp'], tags: ['WhatsApp'],
protect: true, protect: true,

View File

@ -0,0 +1,26 @@
import { sendMessageV1 } from '@/features/chat/api/legacy/sendMessageV1'
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'
import { sendMessageV2 } from '@/features/chat/api/legacy/sendMessageV2'
import { continueChat } from '@/features/chat/api/continueChat'
import { saveClientLogs } from '@/features/chat/api/saveClientLogs'
import { startChat } from '@/features/chat/api/startChat'
import { startChatPreview } from '@/features/chat/api/startChatPreview'
export const appRouter = router({
sendMessageV1,
sendMessageV2,
startChat,
continueChat,
startChatPreview: startChatPreview,
getUploadUrl,
generateUploadUrl,
updateTypebotInSession,
whatsAppRouter,
saveClientLogs,
})
export type AppRouter = typeof appRouter

View File

@ -3,6 +3,8 @@ import { inferAsyncReturnType } from '@trpc/server'
import * as trpcNext from '@trpc/server/adapters/next' import * as trpcNext from '@trpc/server/adapters/next'
import { User } from '@typebot.io/prisma' import { User } from '@typebot.io/prisma'
import { NextApiRequest } from 'next' import { NextApiRequest } from 'next'
import { mockedUser } from '@typebot.io/lib/mockedUser'
import { env } from '@typebot.io/env'
export async function createContext(opts: trpcNext.CreateNextContextOptions) { export async function createContext(opts: trpcNext.CreateNextContextOptions) {
const user = await getAuthenticatedUser(opts.req) const user = await getAuthenticatedUser(opts.req)
@ -15,6 +17,7 @@ export async function createContext(opts: trpcNext.CreateNextContextOptions) {
const getAuthenticatedUser = async ( const getAuthenticatedUser = async (
req: NextApiRequest req: NextApiRequest
): Promise<User | undefined> => { ): Promise<User | undefined> => {
if (env.NEXT_PUBLIC_E2E_TEST) return mockedUser
const bearerToken = extractBearerToken(req) const bearerToken = extractBearerToken(req)
if (!bearerToken) return if (!bearerToken) return
return authenticateByToken(bearerToken) return authenticateByToken(bearerToken)

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/appRouterV2' import { appRouter } from './appRouter'
const openApiDocument = generateOpenApiDocument(appRouter, { const openApiDocument = generateOpenApiDocument(appRouter, {
title: 'Chat API', title: 'Chat API',
version: '2.0.0', version: '3.0.0',
baseUrl: 'https://typebot.io/api/v2', baseUrl: 'https://typebot.io/api',
docsUrl: 'https://docs.typebot.io/api', docsUrl: 'https://docs.typebot.io/api',
}) })

View File

@ -1,16 +0,0 @@
import { sendMessageV1 } from '@/features/chat/api/sendMessageV1'
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({
sendMessageV1,
getUploadUrl,
generateUploadUrl,
updateTypebotInSession,
whatsAppRouter,
})
export type AppRouter = typeof appRouter

View File

@ -1,16 +0,0 @@
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 { initTRPC } from '@trpc/server' import { TRPCError, initTRPC } from '@trpc/server'
import { OpenApiMeta } from 'trpc-openapi' import { OpenApiMeta } from 'trpc-openapi'
import superjson from 'superjson' import superjson from 'superjson'
import { Context } from './context' import { Context } from './context'
@ -8,13 +8,23 @@ const t = initTRPC.context<Context>().meta<OpenApiMeta>().create({
transformer: superjson, transformer: superjson,
}) })
export const router = t.router
const sentryMiddleware = t.middleware( const sentryMiddleware = t.middleware(
Sentry.Handlers.trpcMiddleware({ Sentry.Handlers.trpcMiddleware({
attachRpcInput: true, attachRpcInput: true,
}) })
) )
const injectUser = t.middleware(({ next, ctx }) => { export const publicProcedure = t.procedure.use(sentryMiddleware)
const isAuthed = t.middleware(({ next, ctx }) => {
if (!ctx.user?.id) {
throw new TRPCError({
code: 'UNAUTHORIZED',
message: 'You need to be authenticated to perform this action',
})
}
return next({ return next({
ctx: { ctx: {
user: ctx.user, user: ctx.user,
@ -22,10 +32,6 @@ const injectUser = t.middleware(({ next, ctx }) => {
}) })
}) })
const finalMiddleware = sentryMiddleware.unstable_pipe(injectUser) export const authenticatedProcedure = t.procedure.use(
sentryMiddleware.unstable_pipe(isAuthed)
export const middleware = t.middleware )
export const router = t.router
export const publicProcedure = t.procedure.use(finalMiddleware)

View File

@ -1,4 +1,4 @@
import { appRouter } from '@/helpers/server/routers/appRouterV1' import { appRouter } from '@/helpers/server/appRouter'
import * as Sentry from '@sentry/nextjs' import * as Sentry 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

@ -1,23 +0,0 @@
import { appRouter } from '@/helpers/server/routers/appRouterV2'
import * as Sentry 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') {
Sentry.captureException(error)
console.error('Something went wrong', error)
}
},
})(req, res)
}
export default handler

View File

@ -2,7 +2,6 @@ import { getTestAsset } from '@/test/utils/playwright'
import test, { expect } from '@playwright/test' import test, { expect } from '@playwright/test'
import { createId } from '@paralleldrive/cuid2' import { createId } from '@paralleldrive/cuid2'
import prisma from '@typebot.io/lib/prisma' import prisma from '@typebot.io/lib/prisma'
import { SendMessageInput } from '@typebot.io/schemas'
import { import {
createWebhook, createWebhook,
deleteTypebots, deleteTypebots,
@ -10,6 +9,7 @@ import {
importTypebotInDatabase, importTypebotInDatabase,
} from '@typebot.io/lib/playwright/databaseActions' } from '@typebot.io/lib/playwright/databaseActions'
import { HttpMethod } from '@typebot.io/schemas/features/blocks/integrations/webhook/constants' import { HttpMethod } from '@typebot.io/schemas/features/blocks/integrations/webhook/constants'
import { StartChatInput, StartPreviewChatInput } from '@typebot.io/schemas'
test.afterEach(async () => { test.afterEach(async () => {
await deleteWebhooks(['chat-webhook-id']) await deleteWebhooks(['chat-webhook-id'])
@ -40,17 +40,18 @@ test('API chat execution should work on preview bot', async ({ request }) => {
url: 'https://api.chucknorris.io/jokes/random', url: 'https://api.chucknorris.io/jokes/random',
}) })
await test.step('Start the chat', async () => { let chatSessionId: string
await test.step('Can start and continue chat', async () => {
const { sessionId, messages, input, resultId } = await ( const { sessionId, messages, input, resultId } = await (
await request.post(`/api/v2/sendMessage`, { await request.post(`/api/v1/typebots/${typebotId}/preview/startChat`, {
data: { data: {
startParams: { isOnlyRegistering: false,
typebot: typebotId, isStreamEnabled: false,
isPreview: true, } satisfies Omit<StartPreviewChatInput, 'typebotId'>,
},
} satisfies SendMessageInput,
}) })
).json() ).json()
chatSessionId = sessionId
expect(resultId).toBeUndefined() expect(resultId).toBeUndefined()
expect(sessionId).toBeDefined() expect(sessionId).toBeDefined()
expect(messages[0].content.richText).toStrictEqual([ expect(messages[0].content.richText).toStrictEqual([
@ -61,6 +62,38 @@ test('API chat execution should work on preview bot', async ({ request }) => {
]) ])
expect(input.type).toBe('text input') expect(input.type).toBe('text input')
}) })
await test.step('Can answer Name question', async () => {
const { messages, input } = await (
await request.post(`/api/v1/sessions/${chatSessionId}/continueChat`, {
data: {
message: 'John',
},
})
).json()
expect(messages[0].content.richText).toStrictEqual([
{
children: [
{ text: 'Nice to meet you ' },
{
type: 'inline-variable',
children: [
{
type: 'p',
children: [
{
text: 'John',
},
],
},
],
},
],
type: 'p',
},
])
expect(input.type).toBe('number input')
})
}) })
test('API chat execution should work on published bot', async ({ request }) => { test('API chat execution should work on published bot', async ({ request }) => {
@ -83,12 +116,11 @@ 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/v2/sendMessage`, { await request.post(`/api/v1/typebots/${publicId}/startChat`, {
data: { data: {
startParams: { isOnlyRegistering: false,
typebot: publicId, isStreamEnabled: false,
}, } satisfies Omit<StartChatInput, 'publicId'>,
} satisfies SendMessageInput,
}) })
).json() ).json()
chatSessionId = sessionId chatSessionId = sessionId
@ -111,8 +143,8 @@ 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/v2/sendMessage`, { await request.post(`/api/v1/sessions/${chatSessionId}/continueChat`, {
data: { message: 'John', sessionId: chatSessionId }, data: { message: 'John' },
}) })
).json() ).json()
expect(messages[0].content.richText).toStrictEqual([ expect(messages[0].content.richText).toStrictEqual([
@ -142,8 +174,8 @@ 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/v2/sendMessage`, { await request.post(`/api/v1/sessions/${chatSessionId}/continueChat`, {
data: { message: '24', sessionId: chatSessionId }, data: { message: '24' },
}) })
).json() ).json()
expect(messages[0].content.richText).toStrictEqual([ expect(messages[0].content.richText).toStrictEqual([
@ -181,8 +213,8 @@ 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/v2/sendMessage`, { await request.post(`/api/v1/sessions/${chatSessionId}/continueChat`, {
data: { message: '8', sessionId: chatSessionId }, data: { message: '8' },
}) })
).json() ).json()
expect(messages[0].content.richText).toStrictEqual([ expect(messages[0].content.richText).toStrictEqual([
@ -196,8 +228,8 @@ 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/v2/sendMessage`, { await request.post(`/api/v1/sessions/${chatSessionId}/continueChat`, {
data: { message: 'invalid email', sessionId: chatSessionId }, data: { message: 'invalid email' },
}) })
).json() ).json()
expect(messages[0].content.richText).toStrictEqual([ expect(messages[0].content.richText).toStrictEqual([
@ -215,8 +247,8 @@ 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/v2/sendMessage`, { await request.post(`/api/v1/sessions/${chatSessionId}/continueChat`, {
data: { message: 'typebot@email.com', sessionId: chatSessionId }, data: { message: 'typebot@email.com' },
}) })
).json() ).json()
expect(messages.length).toBe(0) expect(messages.length).toBe(0)
@ -225,8 +257,8 @@ 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/v2/sendMessage`, { await request.post(`/api/v1/sessions/${chatSessionId}/continueChat`, {
data: { message: 'https://typebot.io', sessionId: chatSessionId }, data: { message: 'https://typebot.io' },
}) })
).json() ).json()
expect(messages.length).toBe(0) expect(messages.length).toBe(0)
@ -235,8 +267,8 @@ 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/v2/sendMessage`, { await request.post(`/api/v1/sessions/${chatSessionId}/continueChat`, {
data: { message: 'Yes', sessionId: chatSessionId }, data: { message: 'Yes' },
}) })
).json() ).json()
expect(messages[0].content.richText).toStrictEqual([ expect(messages[0].content.richText).toStrictEqual([
@ -263,14 +295,14 @@ 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/v2/sendMessage`, { await request.post(
data: { `/api/v1/typebots/starting-with-input-public/startChat`,
message: 'Hey', {
startParams: { data: {
typebot: 'starting-with-input-public', message: 'Hey',
}, } satisfies Omit<StartChatInput, 'publicId'>,
} satisfies SendMessageInput, }
}) )
).json() ).json()
expect(messages[0].content.richText).toStrictEqual([ expect(messages[0].content.richText).toStrictEqual([
{ {

View File

@ -30,7 +30,7 @@ test('Result should be overwritten on page refresh', async ({ page }) => {
const [, response] = await Promise.all([ const [, response] = await Promise.all([
page.goto(`/${typebotId}-public`), page.goto(`/${typebotId}-public`),
page.waitForResponse(/sendMessage/), page.waitForResponse(/startChat/),
]) ])
const { resultId } = await response.json() const { resultId } = await response.json()
expect(resultId).toBeDefined() expect(resultId).toBeDefined()
@ -38,7 +38,7 @@ test('Result should be overwritten on page refresh', async ({ page }) => {
const [, secondResponse] = await Promise.all([ const [, secondResponse] = await Promise.all([
page.reload(), page.reload(),
page.waitForResponse(/sendMessage/), page.waitForResponse(/startChat/),
]) ])
const { resultId: secondResultId } = await secondResponse.json() const { resultId: secondResultId } = await secondResponse.json()
expect(secondResultId).toBe(resultId) expect(secondResultId).toBe(resultId)
@ -57,7 +57,7 @@ test.describe('Create result on page refresh enabled', () => {
]) ])
const [, response] = await Promise.all([ const [, response] = await Promise.all([
page.goto(`/${typebotId}-public`), page.goto(`/${typebotId}-public`),
page.waitForResponse(/sendMessage/), page.waitForResponse(/startChat/),
]) ])
const { resultId } = await response.json() const { resultId } = await response.json()
expect(resultId).toBeDefined() expect(resultId).toBeDefined()
@ -65,7 +65,7 @@ test.describe('Create result on page refresh enabled', () => {
await expect(page.getByRole('textbox')).toBeVisible() await expect(page.getByRole('textbox')).toBeVisible()
const [, secondResponse] = await Promise.all([ const [, secondResponse] = await Promise.all([
page.reload(), page.reload(),
page.waitForResponse(/sendMessage/), page.waitForResponse(/startChat/),
]) ])
const { resultId: secondResultId } = await secondResponse.json() const { resultId: secondResultId } = await secondResponse.json()
expect(secondResultId).not.toBe(resultId) expect(secondResultId).not.toBe(resultId)

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/appRouterV2' import { appRouter } from '@/helpers/server/appRouter'
const openApiDocument = generateOpenApiDocument(appRouter, { const openApiDocument = generateOpenApiDocument(appRouter, {
title: 'Chat API', title: 'Chat API',
version: '2.0.0', version: '3.0.0',
baseUrl: 'https://typebot.io/api/v2', baseUrl: 'https://typebot.io/api',
docsUrl: 'https://docs.typebot.io/api', docsUrl: 'https://docs.typebot.io/api',
}) })

View File

@ -2,7 +2,7 @@ import {
SessionState, SessionState,
GoogleSheetsGetOptions, GoogleSheetsGetOptions,
VariableWithValue, VariableWithValue,
ReplyLog, ChatLog,
} from '@typebot.io/schemas' } from '@typebot.io/schemas'
import { isNotEmpty, byId, isDefined } from '@typebot.io/lib' import { isNotEmpty, byId, isDefined } from '@typebot.io/lib'
import { getAuthenticatedGoogleDoc } from './helpers/getAuthenticatedGoogleDoc' import { getAuthenticatedGoogleDoc } from './helpers/getAuthenticatedGoogleDoc'
@ -18,7 +18,7 @@ export const getRow = async (
options, options,
}: { outgoingEdgeId?: string; options: GoogleSheetsGetOptions } }: { outgoingEdgeId?: string; options: GoogleSheetsGetOptions }
): Promise<ExecuteIntegrationResponse> => { ): Promise<ExecuteIntegrationResponse> => {
const logs: ReplyLog[] = [] const logs: ChatLog[] = []
const { variables } = state.typebotsQueue[0].typebot const { variables } = state.typebotsQueue[0].typebot
const { sheetId, cellsToExtract, filter, ...parsedOptions } = const { sheetId, cellsToExtract, filter, ...parsedOptions } =
deepParseVariables(variables)(options) deepParseVariables(variables)(options)

View File

@ -1,7 +1,7 @@
import { import {
SessionState, SessionState,
GoogleSheetsInsertRowOptions, GoogleSheetsInsertRowOptions,
ReplyLog, ChatLog,
} from '@typebot.io/schemas' } from '@typebot.io/schemas'
import { parseCellValues } from './helpers/parseCellValues' import { parseCellValues } from './helpers/parseCellValues'
import { getAuthenticatedGoogleDoc } from './helpers/getAuthenticatedGoogleDoc' import { getAuthenticatedGoogleDoc } from './helpers/getAuthenticatedGoogleDoc'
@ -17,7 +17,7 @@ export const insertRow = async (
const { variables } = state.typebotsQueue[0].typebot const { variables } = state.typebotsQueue[0].typebot
if (!options.cellsToInsert || !options.sheetId) return { outgoingEdgeId } if (!options.cellsToInsert || !options.sheetId) return { outgoingEdgeId }
const logs: ReplyLog[] = [] const logs: ChatLog[] = []
const doc = await getAuthenticatedGoogleDoc({ const doc = await getAuthenticatedGoogleDoc({
credentialsId: options.credentialsId, credentialsId: options.credentialsId,

View File

@ -1,7 +1,7 @@
import { import {
SessionState, SessionState,
GoogleSheetsUpdateRowOptions, GoogleSheetsUpdateRowOptions,
ReplyLog, ChatLog,
} from '@typebot.io/schemas' } from '@typebot.io/schemas'
import { parseCellValues } from './helpers/parseCellValues' import { parseCellValues } from './helpers/parseCellValues'
import { getAuthenticatedGoogleDoc } from './helpers/getAuthenticatedGoogleDoc' import { getAuthenticatedGoogleDoc } from './helpers/getAuthenticatedGoogleDoc'
@ -28,7 +28,7 @@ export const updateRow = async (
if (!options.cellsToUpsert || !sheetId || (!referenceCell && !filter)) if (!options.cellsToUpsert || !sheetId || (!referenceCell && !filter))
return { outgoingEdgeId } return { outgoingEdgeId }
const logs: ReplyLog[] = [] const logs: ChatLog[] = []
const doc = await getAuthenticatedGoogleDoc({ const doc = await getAuthenticatedGoogleDoc({
credentialsId: options.credentialsId, credentialsId: options.credentialsId,

View File

@ -1,5 +1,5 @@
import { isNotEmpty } from '@typebot.io/lib/utils' import { isNotEmpty } from '@typebot.io/lib/utils'
import { ChatReply } from '@typebot.io/schemas' import { ContinueChatResponse } from '@typebot.io/schemas'
import { OpenAIBlock } from '@typebot.io/schemas/features/blocks/integrations/openai' import { OpenAIBlock } from '@typebot.io/schemas/features/blocks/integrations/openai'
import { HTTPError } from 'got' import { HTTPError } from 'got'
import { ClientOptions, OpenAI } from 'openai' import { ClientOptions, OpenAI } from 'openai'
@ -10,7 +10,7 @@ type Props = Pick<
> & { > & {
apiKey: string apiKey: string
temperature: number | undefined temperature: number | undefined
currentLogs?: ChatReply['logs'] currentLogs?: ContinueChatResponse['logs']
isRetrying?: boolean isRetrying?: boolean
} & Pick<NonNullable<OpenAIBlock['options']>, 'apiVersion' | 'baseUrl'> } & Pick<NonNullable<OpenAIBlock['options']>, 'apiVersion' | 'baseUrl'>
@ -25,9 +25,9 @@ export const executeChatCompletionOpenAIRequest = async ({
currentLogs = [], currentLogs = [],
}: Props): Promise<{ }: Props): Promise<{
chatCompletion?: OpenAI.Chat.Completions.ChatCompletion chatCompletion?: OpenAI.Chat.Completions.ChatCompletion
logs?: ChatReply['logs'] logs?: ContinueChatResponse['logs']
}> => { }> => {
const logs: ChatReply['logs'] = currentLogs const logs: ContinueChatResponse['logs'] = currentLogs
if (messages.length === 0) return { logs } if (messages.length === 0) return { logs }
try { try {
const config = { const config = {

View File

@ -1,5 +1,5 @@
import { byId, isDefined } from '@typebot.io/lib' import { byId, isDefined } from '@typebot.io/lib'
import { ChatReply, SessionState } from '@typebot.io/schemas' import { ContinueChatResponse, SessionState } from '@typebot.io/schemas'
import { ChatCompletionOpenAIOptions } from '@typebot.io/schemas/features/blocks/integrations/openai' import { ChatCompletionOpenAIOptions } from '@typebot.io/schemas/features/blocks/integrations/openai'
import { VariableWithUnknowValue } from '@typebot.io/schemas/features/typebot/variable' import { VariableWithUnknowValue } from '@typebot.io/schemas/features/typebot/variable'
import { updateVariablesInSession } from '../../../variables/updateVariablesInSession' import { updateVariablesInSession } from '../../../variables/updateVariablesInSession'
@ -14,7 +14,7 @@ export const resumeChatCompletion =
}: { }: {
outgoingEdgeId?: string outgoingEdgeId?: string
options: ChatCompletionOpenAIOptions options: ChatCompletionOpenAIOptions
logs?: ChatReply['logs'] logs?: ContinueChatResponse['logs']
} }
) => ) =>
async (message: string, totalTokens?: number) => { async (message: string, totalTokens?: number) => {

View File

@ -1,7 +1,7 @@
import { DefaultBotNotificationEmail, render } from '@typebot.io/emails' import { DefaultBotNotificationEmail, render } from '@typebot.io/emails'
import { import {
AnswerInSessionState, AnswerInSessionState,
ReplyLog, ChatLog,
SendEmailBlock, SendEmailBlock,
SessionState, SessionState,
SmtpCredentials, SmtpCredentials,
@ -25,7 +25,7 @@ export const executeSendEmailBlock = async (
state: SessionState, state: SessionState,
block: SendEmailBlock block: SendEmailBlock
): Promise<ExecuteIntegrationResponse> => { ): Promise<ExecuteIntegrationResponse> => {
const logs: ReplyLog[] = [] const logs: ChatLog[] = []
const { options } = block const { options } = block
const { typebot, resultId, answers } = state.typebotsQueue[0] const { typebot, resultId, answers } = state.typebotsQueue[0]
const isPreview = !resultId const isPreview = !resultId
@ -114,8 +114,8 @@ const sendEmail = async ({
typebot: TypebotInSession typebot: TypebotInSession
answers: AnswerInSessionState[] answers: AnswerInSessionState[]
fileUrls?: string | string[] fileUrls?: string | string[]
}): Promise<ReplyLog[] | undefined> => { }): Promise<ChatLog[] | undefined> => {
const logs: ReplyLog[] = [] const logs: ChatLog[] = []
const { name: replyToName } = parseEmailRecipient(replyTo) const { name: replyToName } = parseEmailRecipient(replyTo)
const { host, port, isTlsEnabled, username, password, from } = const { host, port, isTlsEnabled, username, password, from } =

View File

@ -8,7 +8,7 @@ import {
Variable, Variable,
WebhookResponse, WebhookResponse,
KeyValue, KeyValue,
ReplyLog, ChatLog,
ExecutableWebhook, ExecutableWebhook,
AnswerInSessionState, AnswerInSessionState,
} from '@typebot.io/schemas' } from '@typebot.io/schemas'
@ -34,7 +34,7 @@ export const executeWebhookBlock = async (
state: SessionState, state: SessionState,
block: WebhookBlock | ZapierBlock | MakeComBlock | PabblyConnectBlock block: WebhookBlock | ZapierBlock | MakeComBlock | PabblyConnectBlock
): Promise<ExecuteIntegrationResponse> => { ): Promise<ExecuteIntegrationResponse> => {
const logs: ReplyLog[] = [] const logs: ChatLog[] = []
const webhook = const webhook =
block.options?.webhook ?? block.options?.webhook ??
('webhookId' in block ('webhookId' in block
@ -142,8 +142,8 @@ const parseWebhookAttributes =
export const executeWebhook = async ( export const executeWebhook = async (
webhook: ParsedWebhook webhook: ParsedWebhook
): Promise<{ response: WebhookResponse; logs?: ReplyLog[] }> => { ): Promise<{ response: WebhookResponse; logs?: ChatLog[] }> => {
const logs: ReplyLog[] = [] const logs: ChatLog[] = []
const { headers, url, method, basicAuth, body, isJson } = webhook const { headers, url, method, basicAuth, body, isJson } = webhook
const contentType = headers ? headers['Content-Type'] : undefined const contentType = headers ? headers['Content-Type'] : undefined

View File

@ -2,7 +2,7 @@ import { byId } from '@typebot.io/lib'
import { import {
MakeComBlock, MakeComBlock,
PabblyConnectBlock, PabblyConnectBlock,
ReplyLog, ChatLog,
VariableWithUnknowValue, VariableWithUnknowValue,
WebhookBlock, WebhookBlock,
ZapierBlock, ZapierBlock,
@ -15,7 +15,7 @@ import { updateVariablesInSession } from '../../../variables/updateVariablesInSe
type Props = { type Props = {
state: SessionState state: SessionState
block: WebhookBlock | ZapierBlock | MakeComBlock | PabblyConnectBlock block: WebhookBlock | ZapierBlock | MakeComBlock | PabblyConnectBlock
logs?: ReplyLog[] logs?: ChatLog[]
response: { response: {
statusCode: number statusCode: number
data?: unknown data?: unknown

View File

@ -3,7 +3,7 @@ import {
TypebotLinkBlock, TypebotLinkBlock,
SessionState, SessionState,
Variable, Variable,
ReplyLog, ChatLog,
Edge, Edge,
typebotInSessionStateSchema, typebotInSessionStateSchema,
TypebotInSession, TypebotInSession,
@ -19,7 +19,7 @@ export const executeTypebotLink = async (
state: SessionState, state: SessionState,
block: TypebotLinkBlock block: TypebotLinkBlock
): Promise<ExecuteLogicResponse> => { ): Promise<ExecuteLogicResponse> => {
const logs: ReplyLog[] = [] const logs: ChatLog[] = []
const typebotId = block.options?.typebotId const typebotId = block.options?.typebotId
if (!typebotId) { if (!typebotId) {
logs.push({ logs.push({

View File

@ -1,7 +1,7 @@
import { import {
AnswerInSessionState, AnswerInSessionState,
Block, Block,
ChatReply, ContinueChatResponse,
Group, Group,
InputBlock, InputBlock,
SessionState, SessionState,
@ -46,7 +46,10 @@ export const continueBotFlow = async (
reply: string | undefined, reply: string | undefined,
{ state, version }: Params { state, version }: Params
): Promise< ): Promise<
ChatReply & { newSessionState: SessionState; visitedEdges: VisitedEdge[] } ContinueChatResponse & {
newSessionState: SessionState
visitedEdges: VisitedEdge[]
}
> => { > => {
let firstBubbleWasStreamed = false let firstBubbleWasStreamed = false
let newSessionState = { ...state } let newSessionState = { ...state }
@ -202,7 +205,9 @@ const saveVariableValueIfAny =
const parseRetryMessage = const parseRetryMessage =
(state: SessionState) => (state: SessionState) =>
async (block: InputBlock): Promise<Pick<ChatReply, 'messages' | 'input'>> => { async (
block: InputBlock
): Promise<Pick<ContinueChatResponse, 'messages' | 'input'>> => {
const retryMessage = const retryMessage =
block.options && block.options &&
'retryMessageContent' in block.options && 'retryMessageContent' in block.options &&

View File

@ -1,5 +1,5 @@
import { import {
ChatReply, ContinueChatResponse,
Group, Group,
InputBlock, InputBlock,
RuntimeOptions, RuntimeOptions,
@ -31,7 +31,7 @@ import { VisitedEdge } from '@typebot.io/prisma'
type ContextProps = { type ContextProps = {
version: 1 | 2 version: 1 | 2
state: SessionState state: SessionState
currentReply?: ChatReply currentReply?: ContinueChatResponse
currentLastBubbleId?: string currentLastBubbleId?: string
firstBubbleWasStreamed?: boolean firstBubbleWasStreamed?: boolean
visitedEdges: VisitedEdge[] visitedEdges: VisitedEdge[]
@ -48,12 +48,16 @@ export const executeGroup = async (
firstBubbleWasStreamed, firstBubbleWasStreamed,
}: ContextProps }: ContextProps
): Promise< ): Promise<
ChatReply & { newSessionState: SessionState; visitedEdges: VisitedEdge[] } ContinueChatResponse & {
newSessionState: SessionState
visitedEdges: VisitedEdge[]
}
> => { > => {
const messages: ChatReply['messages'] = currentReply?.messages ?? [] const messages: ContinueChatResponse['messages'] =
let clientSideActions: ChatReply['clientSideActions'] = currentReply?.messages ?? []
let clientSideActions: ContinueChatResponse['clientSideActions'] =
currentReply?.clientSideActions currentReply?.clientSideActions
let logs: ChatReply['logs'] = currentReply?.logs let logs: ContinueChatResponse['logs'] = currentReply?.logs
let nextEdgeId = null let nextEdgeId = null
let lastBubbleBlockId: string | undefined = currentLastBubbleId let lastBubbleBlockId: string | undefined = currentLastBubbleId
@ -173,7 +177,7 @@ const computeRuntimeOptions =
export const parseInput = export const parseInput =
(state: SessionState) => (state: SessionState) =>
async (block: InputBlock): Promise<ChatReply['input']> => { async (block: InputBlock): Promise<ContinueChatResponse['input']> => {
switch (block.type) { switch (block.type) {
case InputBlockType.CHOICE: { case InputBlockType.CHOICE: {
return injectVariableValuesInButtonsInputBlock(state)(block) return injectVariableValuesInButtonsInputBlock(state)(block)

View File

@ -1,5 +1,10 @@
import { parseVideoUrl } from '@typebot.io/lib/parseVideoUrl' import { parseVideoUrl } from '@typebot.io/lib/parseVideoUrl'
import { BubbleBlock, Variable, ChatReply, Typebot } from '@typebot.io/schemas' import {
BubbleBlock,
Variable,
ContinueChatResponse,
Typebot,
} from '@typebot.io/schemas'
import { deepParseVariables } from './variables/deepParseVariables' import { deepParseVariables } from './variables/deepParseVariables'
import { isEmpty, isNotEmpty } from '@typebot.io/lib/utils' import { isEmpty, isNotEmpty } from '@typebot.io/lib/utils'
import { import {
@ -27,7 +32,7 @@ export type BubbleBlockWithDefinedContent = BubbleBlock & {
export const parseBubbleBlock = ( export const parseBubbleBlock = (
block: BubbleBlockWithDefinedContent, block: BubbleBlockWithDefinedContent,
{ version, variables, typebotVersion }: Params { version, variables, typebotVersion }: Params
): ChatReply['messages'][0] => { ): ContinueChatResponse['messages'][0] => {
switch (block.type) { switch (block.type) {
case BubbleBlockType.TEXT: { case BubbleBlockType.TEXT: {
if (version === 1) if (version === 1)

View File

@ -1,9 +1,9 @@
import { SessionState, ChatReply } from '@typebot.io/schemas' import { SessionState, ContinueChatResponse } from '@typebot.io/schemas'
import { parseVariables } from './variables/parseVariables' import { parseVariables } from './variables/parseVariables'
export const parseDynamicTheme = ( export const parseDynamicTheme = (
state: SessionState | undefined state: SessionState | undefined
): ChatReply['dynamicTheme'] => { ): ContinueChatResponse['dynamicTheme'] => {
if (!state?.dynamicTheme) return if (!state?.dynamicTheme) return
return { return {
hostAvatarUrl: parseVariables(state?.typebotsQueue[0].typebot.variables)( hostAvatarUrl: parseVariables(state?.typebotsQueue[0].typebot.variables)(

View File

@ -1,4 +1,4 @@
import { ChatReply, ChatSession } from '@typebot.io/schemas' import { ContinueChatResponse, ChatSession } from '@typebot.io/schemas'
import { upsertResult } from './queries/upsertResult' import { upsertResult } from './queries/upsertResult'
import { saveLogs } from './queries/saveLogs' import { saveLogs } from './queries/saveLogs'
import { updateSession } from './queries/updateSession' import { updateSession } from './queries/updateSession'
@ -11,9 +11,9 @@ import { VisitedEdge } from '@typebot.io/prisma'
type Props = { type Props = {
session: Pick<ChatSession, 'state'> & { id?: string } session: Pick<ChatSession, 'state'> & { id?: string }
input: ChatReply['input'] input: ContinueChatResponse['input']
logs: ChatReply['logs'] logs: ContinueChatResponse['logs']
clientSideActions: ChatReply['clientSideActions'] clientSideActions: ContinueChatResponse['clientSideActions']
visitedEdges: VisitedEdge[] visitedEdges: VisitedEdge[]
forceCreateSession?: boolean forceCreateSession?: boolean
} }

View File

@ -1,5 +1,9 @@
import { TRPCError } from '@trpc/server' import { TRPCError } from '@trpc/server'
import { ChatReply, SessionState, StartElementId } from '@typebot.io/schemas' import {
ContinueChatResponse,
SessionState,
StartFrom,
} from '@typebot.io/schemas'
import { executeGroup } from './executeGroup' import { executeGroup } from './executeGroup'
import { getNextGroup } from './getNextGroup' import { getNextGroup } from './getNextGroup'
import { VisitedEdge } from '@typebot.io/prisma' import { VisitedEdge } from '@typebot.io/prisma'
@ -7,20 +11,24 @@ import { VisitedEdge } from '@typebot.io/prisma'
type Props = { type Props = {
version: 1 | 2 version: 1 | 2
state: SessionState state: SessionState
} & StartElementId startFrom?: StartFrom
}
export const startBotFlow = async ({ export const startBotFlow = async ({
version, version,
state, state,
...props startFrom,
}: Props): Promise< }: Props): Promise<
ChatReply & { newSessionState: SessionState; visitedEdges: VisitedEdge[] } ContinueChatResponse & {
newSessionState: SessionState
visitedEdges: VisitedEdge[]
}
> => { > => {
let newSessionState = state let newSessionState = state
const visitedEdges: VisitedEdge[] = [] const visitedEdges: VisitedEdge[] = []
if ('startGroupId' in props) { if (startFrom?.type === 'group') {
const group = state.typebotsQueue[0].typebot.groups.find( const group = state.typebotsQueue[0].typebot.groups.find(
(group) => group.id === props.startGroupId (group) => group.id === startFrom.groupId
) )
if (!group) if (!group)
throw new TRPCError({ throw new TRPCError({
@ -35,7 +43,7 @@ export const startBotFlow = async ({
} }
const firstEdgeId = getFirstEdgeId({ const firstEdgeId = getFirstEdgeId({
state: newSessionState, state: newSessionState,
startEventId: 'startEventId' in props ? props.startEventId : undefined, startEventId: startFrom?.type === 'event' ? startFrom.eventId : undefined,
}) })
if (!firstEdgeId) return { messages: [], newSessionState, visitedEdges: [] } if (!firstEdgeId) return { messages: [], newSessionState, visitedEdges: [] }
const nextGroup = await getNextGroup(newSessionState)(firstEdgeId) const nextGroup = await getNextGroup(newSessionState)(firstEdgeId)

View File

@ -12,13 +12,13 @@ import {
Block, Block,
} from '@typebot.io/schemas' } from '@typebot.io/schemas'
import { import {
ChatReply, StartChatInput,
StartParams, StartChatResponse,
StartPreviewChatInput,
StartTypebot, StartTypebot,
startTypebotSchema, startTypebotSchema,
} from '@typebot.io/schemas/features/chat/schema' } from '@typebot.io/schemas/features/chat/schema'
import parse, { NodeType } from 'node-html-parser' import parse, { NodeType } from 'node-html-parser'
import { env } from '@typebot.io/env'
import { parseDynamicTheme } from './parseDynamicTheme' import { parseDynamicTheme } from './parseDynamicTheme'
import { findTypebot } from './queries/findTypebot' import { findTypebot } from './queries/findTypebot'
import { findPublicTypebot } from './queries/findPublicTypebot' import { findPublicTypebot } from './queries/findPublicTypebot'
@ -36,11 +36,19 @@ import { IntegrationBlockType } from '@typebot.io/schemas/features/blocks/integr
import { defaultTheme } from '@typebot.io/schemas/features/typebot/theme/constants' import { defaultTheme } from '@typebot.io/schemas/features/typebot/theme/constants'
import { VisitedEdge } from '@typebot.io/prisma' import { VisitedEdge } from '@typebot.io/prisma'
type StartParams =
| ({
type: 'preview'
userId: string
} & StartPreviewChatInput)
| ({
type: 'live'
} & StartChatInput)
type Props = { type Props = {
version: 1 | 2 version: 1 | 2
message: string | undefined message: string | undefined
startParams: StartParams startParams: StartParams
userId: string | undefined
initialSessionState?: Pick<SessionState, 'whatsApp' | 'expiryTimeout'> initialSessionState?: Pick<SessionState, 'whatsApp' | 'expiryTimeout'>
} }
@ -48,26 +56,24 @@ export const startSession = async ({
version, version,
message, message,
startParams, startParams,
userId,
initialSessionState, initialSessionState,
}: Props): Promise< }: Props): Promise<
ChatReply & { newSessionState: SessionState; visitedEdges: VisitedEdge[] } Omit<StartChatResponse, 'resultId'> & {
newSessionState: SessionState
visitedEdges: VisitedEdge[]
resultId?: string
}
> => { > => {
if (!startParams) const typebot = await getTypebot(startParams)
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'StartParams are missing',
})
const typebot = await getTypebot(startParams, userId) const prefilledVariables =
startParams.type === 'live' && startParams.prefilledVariables
const prefilledVariables = startParams.prefilledVariables ? prefillVariables(typebot.variables, startParams.prefilledVariables)
? prefillVariables(typebot.variables, startParams.prefilledVariables) : typebot.variables
: typebot.variables
const result = await getResult({ const result = await getResult({
...startParams, resultId: startParams.type === 'live' ? startParams.resultId : undefined,
isPreview: startParams.isPreview || typeof startParams.typebot !== 'string', isPreview: startParams.type === 'preview',
typebotId: typebot.id, typebotId: typebot.id,
prefilledVariables, prefilledVariables,
isRememberUserEnabled: isRememberUserEnabled:
@ -148,11 +154,8 @@ export const startSession = async ({
let chatReply = await startBotFlow({ let chatReply = await startBotFlow({
version, version,
state: initialState, state: initialState,
...('startGroupId' in startParams startFrom:
? { startGroupId: startParams.startGroupId } startParams.type === 'preview' ? startParams.startFrom : undefined,
: 'startEventId' in startParams
? { startEventId: startParams.startEventId }
: {}),
}) })
// 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
@ -266,20 +269,16 @@ export const startSession = async ({
} }
} }
const getTypebot = async ( const getTypebot = async (startParams: StartParams): Promise<StartTypebot> => {
{ typebot, isPreview }: Pick<StartParams, 'typebot' | 'isPreview'>, if (startParams.type === 'preview' && startParams.typebot)
userId?: string return startParams.typebot
): Promise<StartTypebot> => { const typebotQuery =
if (typeof typebot !== 'string') return typebot startParams.type === 'preview'
if (isPreview && !userId && !env.NEXT_PUBLIC_E2E_TEST) ? await findTypebot({
throw new TRPCError({ id: startParams.typebotId,
code: 'UNAUTHORIZED', userId: startParams.userId,
message: })
'You need to authenticate the request to start a bot in preview mode.', : await findPublicTypebot({ publicId: startParams.publicId })
})
const typebotQuery = isPreview
? await findTypebot({ id: typebot, userId })
: await findPublicTypebot({ publicId: typebot })
const parsedTypebot = const parsedTypebot =
typebotQuery && 'typebot' in typebotQuery typebotQuery && 'typebot' in typebotQuery
@ -319,7 +318,9 @@ const getResult = async ({
resultId, resultId,
prefilledVariables, prefilledVariables,
isRememberUserEnabled, isRememberUserEnabled,
}: Pick<StartParams, 'isPreview' | 'resultId'> & { }: {
resultId: string | undefined
isPreview: boolean
typebotId: string typebotId: string
prefilledVariables: Variable[] prefilledVariables: Variable[]
isRememberUserEnabled: boolean isRememberUserEnabled: boolean
@ -375,7 +376,7 @@ const parseDynamicThemeInState = (theme: Theme) => {
const parseStartClientSideAction = ( const parseStartClientSideAction = (
typebot: StartTypebot typebot: StartTypebot
): NonNullable<ChatReply['clientSideActions']>[number] | undefined => { ): NonNullable<StartChatResponse['clientSideActions']>[number] | undefined => {
const blocks = typebot.groups.flatMap<Block>((group) => group.blocks) const blocks = typebot.groups.flatMap<Block>((group) => group.blocks)
const pixelBlocks = ( const pixelBlocks = (
blocks.filter( blocks.filter(

View File

@ -1,16 +1,16 @@
import { ChatReply, SessionState } from '@typebot.io/schemas' import { ContinueChatResponse, SessionState } from '@typebot.io/schemas'
export type EdgeId = string export type EdgeId = string
export type ExecuteLogicResponse = { export type ExecuteLogicResponse = {
outgoingEdgeId: EdgeId | undefined outgoingEdgeId: EdgeId | undefined
newSessionState?: SessionState newSessionState?: SessionState
} & Pick<ChatReply, 'clientSideActions' | 'logs'> } & Pick<ContinueChatResponse, 'clientSideActions' | 'logs'>
export type ExecuteIntegrationResponse = { export type ExecuteIntegrationResponse = {
outgoingEdgeId: EdgeId | undefined outgoingEdgeId: EdgeId | undefined
newSessionState?: SessionState newSessionState?: SessionState
} & Pick<ChatReply, 'clientSideActions' | 'logs'> } & Pick<ContinueChatResponse, 'clientSideActions' | 'logs'>
export type ParsedReply = export type ParsedReply =
| { status: 'success'; reply: string } | { status: 'success'; reply: string }

View File

@ -1,9 +1,9 @@
import { safeStringify } from '@typebot.io/lib/safeStringify' import { safeStringify } from '@typebot.io/lib/safeStringify'
import { StartParams, Variable } from '@typebot.io/schemas' import { StartChatInput, Variable } from '@typebot.io/schemas'
export const prefillVariables = ( export const prefillVariables = (
variables: Variable[], variables: Variable[],
prefilledVariables: NonNullable<StartParams['prefilledVariables']> prefilledVariables: NonNullable<StartChatInput['prefilledVariables']>
): Variable[] => ): Variable[] =>
variables.map((variable) => { variables.map((variable) => {
const prefilledVariable = prefilledVariables[variable.name] const prefilledVariable = prefilledVariables[variable.name]

View File

@ -1,4 +1,4 @@
import { ButtonItem, ChatReply } from '@typebot.io/schemas' import { ButtonItem, ContinueChatResponse } from '@typebot.io/schemas'
import { WhatsAppSendingMessage } from '@typebot.io/schemas/features/whatsapp' import { WhatsAppSendingMessage } from '@typebot.io/schemas/features/whatsapp'
import { convertRichTextToWhatsAppText } from './convertRichTextToWhatsAppText' import { convertRichTextToWhatsAppText } from './convertRichTextToWhatsAppText'
import { isDefined, isEmpty } from '@typebot.io/lib/utils' import { isDefined, isEmpty } from '@typebot.io/lib/utils'
@ -8,8 +8,8 @@ import { defaultPictureChoiceOptions } from '@typebot.io/schemas/features/blocks
import { defaultChoiceInputOptions } from '@typebot.io/schemas/features/blocks/inputs/choice/constants' import { defaultChoiceInputOptions } from '@typebot.io/schemas/features/blocks/inputs/choice/constants'
export const convertInputToWhatsAppMessages = ( export const convertInputToWhatsAppMessages = (
input: NonNullable<ChatReply['input']>, input: NonNullable<ContinueChatResponse['input']>,
lastMessage: ChatReply['messages'][number] | undefined lastMessage: ContinueChatResponse['messages'][number] | undefined
): WhatsAppSendingMessage[] => { ): WhatsAppSendingMessage[] => {
const lastMessageText = const lastMessageText =
lastMessage?.type === BubbleBlockType.TEXT lastMessage?.type === BubbleBlockType.TEXT

View File

@ -1,4 +1,4 @@
import { ChatReply } from '@typebot.io/schemas' import { ContinueChatResponse } from '@typebot.io/schemas'
import { WhatsAppSendingMessage } from '@typebot.io/schemas/features/whatsapp' import { WhatsAppSendingMessage } from '@typebot.io/schemas/features/whatsapp'
import { convertRichTextToWhatsAppText } from './convertRichTextToWhatsAppText' import { convertRichTextToWhatsAppText } from './convertRichTextToWhatsAppText'
import { isSvgSrc } from '@typebot.io/lib/utils' import { isSvgSrc } from '@typebot.io/lib/utils'
@ -8,7 +8,7 @@ import { VideoBubbleContentType } from '@typebot.io/schemas/features/blocks/bubb
const mp4HttpsUrlRegex = /^https:\/\/.*\.mp4$/ const mp4HttpsUrlRegex = /^https:\/\/.*\.mp4$/
export const convertMessageToWhatsAppMessage = ( export const convertMessageToWhatsAppMessage = (
message: ChatReply['messages'][number] message: ContinueChatResponse['messages'][number]
): WhatsAppSendingMessage | undefined => { ): WhatsAppSendingMessage | undefined => {
switch (message.type) { switch (message.type) {
case BubbleBlockType.TEXT: { case BubbleBlockType.TEXT: {

View File

@ -1,4 +1,8 @@
import { ChatReply, SessionState, Settings } from '@typebot.io/schemas' import {
ContinueChatResponse,
SessionState,
Settings,
} from '@typebot.io/schemas'
import { import {
WhatsAppCredentials, WhatsAppCredentials,
WhatsAppSendingMessage, WhatsAppSendingMessage,
@ -21,7 +25,7 @@ type Props = {
typingEmulation: SessionState['typingEmulation'] typingEmulation: SessionState['typingEmulation']
credentials: WhatsAppCredentials['data'] credentials: WhatsAppCredentials['data']
state: SessionState state: SessionState
} & Pick<ChatReply, 'messages' | 'input' | 'clientSideActions'> } & Pick<ContinueChatResponse, 'messages' | 'input' | 'clientSideActions'>
export const sendChatReplyToWhatsApp = async ({ export const sendChatReplyToWhatsApp = async ({
to, to,
@ -171,7 +175,9 @@ const getTypingDuration = ({
} }
} }
const isLastMessageIncludedInInput = (input: ChatReply['input']): boolean => { const isLastMessageIncludedInInput = (
input: ContinueChatResponse['input']
): boolean => {
if (isNotDefined(input)) return false if (isNotDefined(input)) return false
return input.type === InputBlockType.CHOICE return input.type === InputBlockType.CHOICE
} }
@ -179,7 +185,9 @@ const isLastMessageIncludedInInput = (input: ChatReply['input']): boolean => {
const executeClientSideAction = const executeClientSideAction =
(context: { to: string; credentials: WhatsAppCredentials['data'] }) => (context: { to: string; credentials: WhatsAppCredentials['data'] }) =>
async ( async (
clientSideAction: NonNullable<ChatReply['clientSideActions']>[number] clientSideAction: NonNullable<
ContinueChatResponse['clientSideActions']
>[number]
): Promise<{ replyToSend: string | undefined } | void> => { ): Promise<{ replyToSend: string | undefined } | void> => {
if ('wait' in clientSideAction) { if ('wait' in clientSideAction) {
await new Promise((resolve) => await new Promise((resolve) =>

View File

@ -1,6 +1,6 @@
import prisma from '@typebot.io/lib/prisma' import prisma from '@typebot.io/lib/prisma'
import { import {
ChatReply, ContinueChatResponse,
PublicTypebot, PublicTypebot,
SessionState, SessionState,
Settings, Settings,
@ -20,7 +20,7 @@ import { VisitedEdge } from '@typebot.io/prisma'
type Props = { type Props = {
incomingMessage?: string incomingMessage?: string
workspaceId?: string workspaceId: string
credentials: WhatsAppCredentials['data'] & Pick<WhatsAppCredentials, 'id'> credentials: WhatsAppCredentials['data'] & Pick<WhatsAppCredentials, 'id'>
contact: NonNullable<SessionState['whatsApp']>['contact'] contact: NonNullable<SessionState['whatsApp']>['contact']
} }
@ -31,7 +31,7 @@ export const startWhatsAppSession = async ({
credentials, credentials,
contact, contact,
}: Props): Promise< }: Props): Promise<
| (ChatReply & { | (ContinueChatResponse & {
newSessionState: SessionState newSessionState: SessionState
visitedEdges: VisitedEdge[] visitedEdges: VisitedEdge[]
}) })
@ -89,9 +89,10 @@ export const startWhatsAppSession = async ({
version: 2, version: 2,
message: incomingMessage, message: incomingMessage,
startParams: { startParams: {
typebot: publicTypebot.typebot.publicId as string, type: 'live',
publicId: publicTypebot.typebot.publicId as string,
isOnlyRegistering: false,
}, },
userId: undefined,
initialSessionState: { initialSessionState: {
whatsApp: { whatsApp: {
contact, contact,

View File

@ -1,6 +1,6 @@
{ {
"name": "@typebot.io/js", "name": "@typebot.io/js",
"version": "0.2.15", "version": "0.2.16",
"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

@ -1,7 +1,7 @@
import { LiteBadge } from './LiteBadge' import { LiteBadge } from './LiteBadge'
import { createEffect, createSignal, onCleanup, onMount, Show } from 'solid-js' import { createEffect, createSignal, onCleanup, onMount, Show } from 'solid-js'
import { isNotDefined, isNotEmpty } from '@typebot.io/lib' import { isNotDefined, isNotEmpty } from '@typebot.io/lib'
import { getInitialChatReplyQuery } from '@/queries/getInitialChatReplyQuery' import { startChatQuery } from '@/queries/startChatQuery'
import { ConversationContainer } from './ConversationContainer' import { ConversationContainer } from './ConversationContainer'
import { setIsMobile } from '@/utils/isMobileSignal' import { setIsMobile } from '@/utils/isMobileSignal'
import { BotContext, InitialChatReply, OutgoingLog } from '@/types' import { BotContext, InitialChatReply, OutgoingLog } from '@/types'
@ -12,7 +12,8 @@ import {
} from '@/utils/storage' } from '@/utils/storage'
import { setCssVariablesValue } from '@/utils/setCssVariablesValue' import { setCssVariablesValue } from '@/utils/setCssVariablesValue'
import immutableCss from '../assets/immutable.css' import immutableCss from '../assets/immutable.css'
import { InputBlock, StartElementId } from '@typebot.io/schemas' import { InputBlock } from '@typebot.io/schemas'
import { StartFrom } from '@typebot.io/schemas'
import { defaultTheme } from '@typebot.io/schemas/features/typebot/theme/constants' import { defaultTheme } from '@typebot.io/schemas/features/typebot/theme/constants'
export type BotProps = { export type BotProps = {
@ -27,7 +28,8 @@ export type BotProps = {
onInit?: () => void onInit?: () => void
onEnd?: () => void onEnd?: () => void
onNewLogs?: (logs: OutgoingLog[]) => void onNewLogs?: (logs: OutgoingLog[]) => void
} & StartElementId startFrom?: StartFrom
}
export const Bot = (props: BotProps & { class?: string }) => { export const Bot = (props: BotProps & { class?: string }) => {
const [initialChatReply, setInitialChatReply] = createSignal< const [initialChatReply, setInitialChatReply] = createSignal<
@ -47,11 +49,13 @@ export const Bot = (props: BotProps & { class?: string }) => {
}) })
const typebotIdFromProps = const typebotIdFromProps =
typeof props.typebot === 'string' ? props.typebot : undefined typeof props.typebot === 'string' ? props.typebot : undefined
const { data, error } = await getInitialChatReplyQuery({ const isPreview =
typeof props.typebot !== 'string' || (props.isPreview ?? false)
const { data, error } = await startChatQuery({
stripeRedirectStatus: urlParams.get('redirect_status') ?? undefined, stripeRedirectStatus: urlParams.get('redirect_status') ?? undefined,
typebot: props.typebot, typebot: props.typebot,
apiHost: props.apiHost, apiHost: props.apiHost,
isPreview: props.isPreview ?? false, isPreview,
resultId: isNotEmpty(props.resultId) resultId: isNotEmpty(props.resultId)
? props.resultId ? props.resultId
: getExistingResultIdFromStorage(typebotIdFromProps), : getExistingResultIdFromStorage(typebotIdFromProps),
@ -59,14 +63,10 @@ export const Bot = (props: BotProps & { class?: string }) => {
...prefilledVariables, ...prefilledVariables,
...props.prefilledVariables, ...props.prefilledVariables,
}, },
...('startGroupId' in props startFrom: props.startFrom,
? { startGroupId: props.startGroupId }
: 'startEventId' in props
? { startEventId: props.startEventId }
: {}),
}) })
if (error && 'code' in error && typeof error.code === 'string') { if (error && 'code' in error && typeof error.code === 'string') {
if (typeof props.typebot !== 'string' || (props.isPreview ?? false)) { if (isPreview) {
return setError( return setError(
new Error('An error occurred while loading the bot.', { new Error('An error occurred while loading the bot.', {
cause: error.message, cause: error.message,

View File

@ -1,6 +1,6 @@
import { BotContext, ChatChunk as ChatChunkType } from '@/types' import { BotContext, ChatChunk as ChatChunkType } from '@/types'
import { isMobile } from '@/utils/isMobileSignal' import { isMobile } from '@/utils/isMobileSignal'
import { ChatReply, Settings, Theme } from '@typebot.io/schemas' import { ContinueChatResponse, Settings, Theme } from '@typebot.io/schemas'
import { createSignal, For, onMount, Show } from 'solid-js' import { createSignal, For, onMount, Show } from 'solid-js'
import { HostBubble } from '../bubbles/HostBubble' import { HostBubble } from '../bubbles/HostBubble'
import { InputChatBlock } from '../InputChatBlock' import { InputChatBlock } from '../InputChatBlock'
@ -9,7 +9,7 @@ import { StreamingBubble } from '../bubbles/StreamingBubble'
import { defaultTheme } from '@typebot.io/schemas/features/typebot/theme/constants' import { defaultTheme } from '@typebot.io/schemas/features/typebot/theme/constants'
import { defaultSettings } from '@typebot.io/schemas/features/typebot/settings/constants' import { defaultSettings } from '@typebot.io/schemas/features/typebot/settings/constants'
type Props = Pick<ChatReply, 'messages' | 'input'> & { type Props = Pick<ContinueChatResponse, 'messages' | 'input'> & {
theme: Theme theme: Theme
settings: Settings settings: Settings
inputIndex: number inputIndex: number

View File

@ -1,8 +1,8 @@
import { import {
ChatReply, ContinueChatResponse,
InputBlock, InputBlock,
SendMessageInput,
Theme, Theme,
ChatLog,
} from '@typebot.io/schemas' } from '@typebot.io/schemas'
import { import {
createEffect, createEffect,
@ -12,7 +12,7 @@ import {
onMount, onMount,
Show, Show,
} from 'solid-js' } from 'solid-js'
import { sendMessageQuery } from '@/queries/sendMessageQuery' import { continueChatQuery } from '@/queries/continueChatQuery'
import { ChatChunk } from './ChatChunk' import { ChatChunk } from './ChatChunk'
import { import {
BotContext, BotContext,
@ -30,10 +30,11 @@ import {
setFormattedMessages, setFormattedMessages,
} from '@/utils/formattedMessagesSignal' } from '@/utils/formattedMessagesSignal'
import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants' import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants'
import { saveClientLogsQuery } from '@/queries/saveClientLogsQuery'
const parseDynamicTheme = ( const parseDynamicTheme = (
initialTheme: Theme, initialTheme: Theme,
dynamicTheme: ChatReply['dynamicTheme'] dynamicTheme: ContinueChatResponse['dynamicTheme']
): Theme => ({ ): Theme => ({
...initialTheme, ...initialTheme,
chat: { chat: {
@ -74,7 +75,7 @@ export const ConversationContainer = (props: Props) => {
}, },
]) ])
const [dynamicTheme, setDynamicTheme] = createSignal< const [dynamicTheme, setDynamicTheme] = createSignal<
ChatReply['dynamicTheme'] ContinueChatResponse['dynamicTheme']
>(props.initialChatReply.dynamicTheme) >(props.initialChatReply.dynamicTheme)
const [theme, setTheme] = createSignal(props.initialChatReply.typebot.theme) const [theme, setTheme] = createSignal(props.initialChatReply.typebot.theme)
const [isSending, setIsSending] = createSignal(false) const [isSending, setIsSending] = createSignal(false)
@ -136,9 +137,16 @@ export const ConversationContainer = (props: Props) => {
const sendMessage = async ( const sendMessage = async (
message: string | undefined, message: string | undefined,
clientLogs?: SendMessageInput['clientLogs'] clientLogs?: ChatLog[]
) => { ) => {
if (clientLogs) props.onNewLogs?.(clientLogs) if (clientLogs) {
props.onNewLogs?.(clientLogs)
await saveClientLogsQuery({
apiHost: props.context.apiHost,
sessionId: props.initialChatReply.sessionId,
clientLogs,
})
}
setHasError(false) setHasError(false)
const currentInputBlock = [...chatChunks()].pop()?.input const currentInputBlock = [...chatChunks()].pop()?.input
if (currentInputBlock?.id && props.onAnswer && message) if (currentInputBlock?.id && props.onAnswer && message)
@ -153,11 +161,10 @@ export const ConversationContainer = (props: Props) => {
const longRequest = setTimeout(() => { const longRequest = setTimeout(() => {
setIsSending(true) setIsSending(true)
}, 1000) }, 1000)
const { data, error } = await sendMessageQuery({ const { data, error } = await continueChatQuery({
apiHost: props.context.apiHost, apiHost: props.context.apiHost,
sessionId: props.initialChatReply.sessionId, sessionId: props.initialChatReply.sessionId,
message, message,
clientLogs,
}) })
clearTimeout(longRequest) clearTimeout(longRequest)
setIsSending(false) setIsSending(false)

View File

@ -1,5 +1,5 @@
import type { import type {
ChatReply, ContinueChatResponse,
ChoiceInputBlock, ChoiceInputBlock,
EmailInputBlock, EmailInputBlock,
FileInputBlock, FileInputBlock,
@ -39,7 +39,7 @@ import { defaultTheme } from '@typebot.io/schemas/features/typebot/theme/constan
type Props = { type Props = {
ref: HTMLDivElement | undefined ref: HTMLDivElement | undefined
block: NonNullable<ChatReply['input']> block: NonNullable<ContinueChatResponse['input']>
hasHostAvatar: boolean hasHostAvatar: boolean
guestAvatar?: NonNullable<Theme['chat']>['guestAvatar'] guestAvatar?: NonNullable<Theme['chat']>['guestAvatar']
inputIndex: number inputIndex: number
@ -113,7 +113,7 @@ export const InputChatBlock = (props: Props) => {
const Input = (props: { const Input = (props: {
context: BotContext context: BotContext
block: NonNullable<ChatReply['input']> block: NonNullable<ContinueChatResponse['input']>
inputIndex: number inputIndex: number
isInputPrefillEnabled: boolean isInputPrefillEnabled: boolean
onSubmit: (answer: InputSubmitContent) => void onSubmit: (answer: InputSubmitContent) => void
@ -252,11 +252,11 @@ const Input = (props: {
} }
const isButtonsBlock = ( const isButtonsBlock = (
block: ChatReply['input'] block: ContinueChatResponse['input']
): ChoiceInputBlock | undefined => ): ChoiceInputBlock | undefined =>
block?.type === InputBlockType.CHOICE ? block : undefined block?.type === InputBlockType.CHOICE ? block : undefined
const isPictureChoiceBlock = ( const isPictureChoiceBlock = (
block: ChatReply['input'] block: ContinueChatResponse['input']
): PictureChoiceBlock | undefined => ): PictureChoiceBlock | undefined =>
block?.type === InputBlockType.PICTURE_CHOICE ? block : undefined block?.type === InputBlockType.PICTURE_CHOICE ? block : undefined

View File

@ -10,7 +10,7 @@ export const defaultBotProps: BotProps = {
onInit: undefined, onInit: undefined,
onNewLogs: undefined, onNewLogs: undefined,
isPreview: undefined, isPreview: undefined,
startGroupId: undefined, startFrom: undefined,
prefilledVariables: undefined, prefilledVariables: undefined,
apiHost: undefined, apiHost: undefined,
resultId: undefined, resultId: undefined,

View File

@ -0,0 +1,22 @@
import { guessApiHost } from '@/utils/guessApiHost'
import { isNotEmpty, sendRequest } from '@typebot.io/lib'
import { ContinueChatResponse } from '@typebot.io/schemas'
export const continueChatQuery = ({
apiHost,
message,
sessionId,
}: {
apiHost?: string
message: string | undefined
sessionId: string
}) =>
sendRequest<ContinueChatResponse>({
method: 'POST',
url: `${
isNotEmpty(apiHost) ? apiHost : guessApiHost()
}/api/v1/sessions/${sessionId}/continueChat`,
body: {
message,
},
})

View File

@ -1,74 +0,0 @@
import { BotContext, InitialChatReply } from '@/types'
import { guessApiHost } from '@/utils/guessApiHost'
import type {
SendMessageInput,
StartElementId,
StartParams,
} from '@typebot.io/schemas'
import { isNotDefined, isNotEmpty, sendRequest } from '@typebot.io/lib'
import {
getPaymentInProgressInStorage,
removePaymentInProgressFromStorage,
} from '@/features/blocks/inputs/payment/helpers/paymentInProgressStorage'
export async function getInitialChatReplyQuery({
typebot,
isPreview,
apiHost,
prefilledVariables,
resultId,
stripeRedirectStatus,
...props
}: StartParams & {
stripeRedirectStatus?: string
apiHost?: string
} & StartElementId) {
if (isNotDefined(typebot))
throw new Error('Typebot ID is required to get initial messages')
const paymentInProgressStateStr = getPaymentInProgressInStorage() ?? undefined
const paymentInProgressState = paymentInProgressStateStr
? (JSON.parse(paymentInProgressStateStr) as {
sessionId: string
typebot: BotContext['typebot']
})
: undefined
if (paymentInProgressState) removePaymentInProgressFromStorage()
const { data, error } = await sendRequest<InitialChatReply>({
method: 'POST',
url: `${isNotEmpty(apiHost) ? apiHost : guessApiHost()}/api/v2/sendMessage`,
body: {
startParams: paymentInProgressState
? undefined
: {
isPreview,
typebot,
prefilledVariables,
resultId,
isStreamEnabled: true,
startGroupId:
'startGroupId' in props ? props.startGroupId : undefined,
startEventId:
'startEventId' in props ? props.startEventId : undefined,
},
sessionId: paymentInProgressState?.sessionId,
message: paymentInProgressState
? stripeRedirectStatus === 'failed'
? 'fail'
: 'Success'
: undefined,
} satisfies SendMessageInput,
})
return {
data: data
? {
...data,
...(paymentInProgressState
? { typebot: paymentInProgressState.typebot }
: {}),
}
: undefined,
error,
}
}

View File

@ -0,0 +1,22 @@
import { guessApiHost } from '@/utils/guessApiHost'
import type { ChatLog } from '@typebot.io/schemas'
import { isNotEmpty, sendRequest } from '@typebot.io/lib'
export const saveClientLogsQuery = ({
apiHost,
sessionId,
clientLogs,
}: {
apiHost?: string
sessionId: string
clientLogs: ChatLog[]
}) =>
sendRequest({
method: 'POST',
url: `${
isNotEmpty(apiHost) ? apiHost : guessApiHost()
}/api/v1/sessions/${sessionId}/clientLogs`,
body: {
clientLogs,
},
})

View File

@ -1,13 +0,0 @@
import { guessApiHost } from '@/utils/guessApiHost'
import type { ChatReply, SendMessageInput } from '@typebot.io/schemas'
import { isNotEmpty, sendRequest } from '@typebot.io/lib'
export const sendMessageQuery = ({
apiHost,
...body
}: SendMessageInput & { apiHost?: string }) =>
sendRequest<ChatReply>({
method: 'POST',
url: `${isNotEmpty(apiHost) ? apiHost : guessApiHost()}/api/v2/sendMessage`,
body,
})

View File

@ -0,0 +1,104 @@
import { BotContext, InitialChatReply } from '@/types'
import { guessApiHost } from '@/utils/guessApiHost'
import { isNotDefined, isNotEmpty, sendRequest } from '@typebot.io/lib'
import {
getPaymentInProgressInStorage,
removePaymentInProgressFromStorage,
} from '@/features/blocks/inputs/payment/helpers/paymentInProgressStorage'
import {
StartChatInput,
StartFrom,
StartPreviewChatInput,
} from '@typebot.io/schemas'
export async function startChatQuery({
typebot,
isPreview,
apiHost,
prefilledVariables,
resultId,
stripeRedirectStatus,
startFrom,
}: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
typebot: string | any
stripeRedirectStatus?: string
apiHost?: string
startFrom?: StartFrom
isPreview: boolean
prefilledVariables?: Record<string, unknown>
resultId?: string
}) {
if (isNotDefined(typebot))
throw new Error('Typebot ID is required to get initial messages')
const paymentInProgressStateStr = getPaymentInProgressInStorage() ?? undefined
const paymentInProgressState = paymentInProgressStateStr
? (JSON.parse(paymentInProgressStateStr) as {
sessionId: string
typebot: BotContext['typebot']
})
: undefined
if (paymentInProgressState) {
removePaymentInProgressFromStorage()
const { data, error } = await sendRequest<InitialChatReply>({
method: 'POST',
url: `${isNotEmpty(apiHost) ? apiHost : guessApiHost()}/api/v1/sessions/${
paymentInProgressState.sessionId
}/continueChat`,
body: {
message: paymentInProgressState
? stripeRedirectStatus === 'failed'
? 'fail'
: 'Success'
: undefined,
},
})
return {
data: data
? {
...data,
...(paymentInProgressState
? { typebot: paymentInProgressState.typebot }
: {}),
}
: undefined,
error,
}
}
const typebotId = typeof typebot === 'string' ? typebot : typebot.id
if (isPreview) {
const { data, error } = await sendRequest<InitialChatReply>({
method: 'POST',
url: `${
isNotEmpty(apiHost) ? apiHost : guessApiHost()
}/api/v1/typebots/${typebotId}/preview/startChat`,
body: {
isStreamEnabled: true,
startFrom,
typebot,
} satisfies Omit<StartPreviewChatInput, 'typebotId'>,
})
return {
data,
error,
}
}
const { data, error } = await sendRequest<InitialChatReply>({
method: 'POST',
url: `${
isNotEmpty(apiHost) ? apiHost : guessApiHost()
}/api/v1/typebots/${typebotId}/startChat`,
body: {
isStreamEnabled: true,
prefilledVariables,
resultId,
} satisfies Omit<StartChatInput, 'publicId'>,
})
return {
data,
error,
}
}

View File

@ -1,4 +1,4 @@
import type { ChatReply } from '@typebot.io/schemas' import { ContinueChatResponse, StartChatResponse } from '@typebot.io/schemas'
export type InputSubmitContent = { export type InputSubmitContent = {
label?: string label?: string
@ -13,9 +13,9 @@ export type BotContext = {
sessionId: string sessionId: string
} }
export type InitialChatReply = ChatReply & { export type InitialChatReply = StartChatResponse & {
typebot: NonNullable<ChatReply['typebot']> typebot: NonNullable<StartChatResponse['typebot']>
sessionId: NonNullable<ChatReply['sessionId']> sessionId: NonNullable<StartChatResponse['sessionId']>
} }
export type OutgoingLog = { export type OutgoingLog = {
@ -30,7 +30,7 @@ export type ClientSideActionContext = {
} }
export type ChatChunk = Pick< export type ChatChunk = Pick<
ChatReply, ContinueChatResponse,
'messages' | 'input' | 'clientSideActions' 'messages' | 'input' | 'clientSideActions'
> & { > & {
streamingMessageId?: string streamingMessageId?: string

View File

@ -8,11 +8,11 @@ import { executeWait } from '@/features/blocks/logic/wait/utils/executeWait'
import { executeWebhook } from '@/features/blocks/integrations/webhook/executeWebhook' import { executeWebhook } from '@/features/blocks/integrations/webhook/executeWebhook'
import { executePixel } from '@/features/blocks/integrations/pixel/executePixel' import { executePixel } from '@/features/blocks/integrations/pixel/executePixel'
import { ClientSideActionContext } from '@/types' import { ClientSideActionContext } from '@/types'
import type { ChatReply, ReplyLog } from '@typebot.io/schemas' import type { ContinueChatResponse, ChatLog } from '@typebot.io/schemas'
import { injectStartProps } from './injectStartProps' import { injectStartProps } from './injectStartProps'
type Props = { type Props = {
clientSideAction: NonNullable<ChatReply['clientSideActions']>[0] clientSideAction: NonNullable<ContinueChatResponse['clientSideActions']>[0]
context: ClientSideActionContext context: ClientSideActionContext
onMessageStream?: (props: { id: string; message: string }) => void onMessageStream?: (props: { id: string; message: string }) => void
} }
@ -23,7 +23,7 @@ export const executeClientSideAction = async ({
onMessageStream, onMessageStream,
}: Props): Promise< }: Props): Promise<
| { blockedPopupUrl: string } | { blockedPopupUrl: string }
| { replyToSend: string | undefined; logs?: ReplyLog[] } | { replyToSend: string | undefined; logs?: ChatLog[] }
| void | void
> => { > => {
if ('chatwoot' in clientSideAction) { if ('chatwoot' in clientSideAction) {

View File

@ -1,6 +1,6 @@
{ {
"name": "@typebot.io/nextjs", "name": "@typebot.io/nextjs",
"version": "0.2.15", "version": "0.2.16",
"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.2.15", "version": "0.2.16",
"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

@ -0,0 +1,123 @@
import { z } from 'zod'
import {
chatLogSchema,
chatMessageSchema,
clientSideActionSchema,
runtimeOptionsSchema,
startTypebotSchema,
} from '../schema'
import { typebotV5Schema, typebotV6Schema } from '../../typebot'
import { dynamicThemeSchema } from '../shared'
import { inputBlockSchemas } from '../../blocks'
export const startElementIdSchema = z.union([
z.object({
startGroupId: z.string().describe('Start chat from a specific group.'),
startEventId: z.never().optional(),
}),
z.object({
startEventId: z.string().describe('Start chat from a specific event.'),
startGroupId: z.never().optional(),
}),
z.object({}),
])
export type StartElementId = z.infer<typeof startElementIdSchema>
const startParamsSchema = z
.object({
typebot: startTypebotSchema
.or(z.string())
.describe(
'Either a Typebot ID or a Typebot object. If you provide a Typebot object, it will be executed in preview mode. ([How can I find my typebot ID?](https://docs.typebot.io/api#how-to-find-my-typebotid)).'
),
isPreview: z
.boolean()
.optional()
.describe(
"If set to `true`, it will start a Preview session with the unpublished bot and it won't be saved in the Results tab. You need to be authenticated with a bearer token for this to work."
),
resultId: z
.string()
.optional()
.describe("Provide it if you'd like to overwrite an existing result."),
prefilledVariables: z
.record(z.unknown())
.optional()
.describe(
'[More info about prefilled variables.](https://docs.typebot.io/editor/variables#prefilled-variables)'
),
isStreamEnabled: z
.boolean()
.optional()
.describe(
'Set this to `true` if you intend to stream OpenAI completions on a client.'
),
isOnlyRegistering: z
.boolean()
.optional()
.describe(
'If set to `true`, it will only register the session and not start the chat. This is used for other chat platform integration as it can require a session to be registered before sending the first message.'
),
})
.and(startElementIdSchema)
export const sendMessageInputSchema = z.object({
message: z
.string()
.optional()
.describe(
'The answer to the previous chat input. Do not provide it if you are starting a new chat.'
),
sessionId: z
.string()
.optional()
.describe(
'Session ID that you get from the initial chat request to a bot. If not provided, it will create a new session.'
),
startParams: startParamsSchema.optional(),
clientLogs: z
.array(chatLogSchema)
.optional()
.describe('Logs while executing client side actions'),
})
export const chatReplySchema = z.object({
messages: z.array(chatMessageSchema),
input: z
.union([
z.discriminatedUnion('type', [...inputBlockSchemas.v5]),
z.discriminatedUnion('type', [...inputBlockSchemas.v6]),
])
.and(
z.object({
prefilledValue: z.string().optional(),
runtimeOptions: runtimeOptionsSchema.optional(),
})
)
.optional(),
clientSideActions: z.array(clientSideActionSchema).optional(),
sessionId: z.string().optional(),
typebot: z
.object({
id: z.string(),
theme: z.union([
typebotV5Schema._def.schema.shape.theme,
typebotV6Schema.shape.theme,
]),
settings: z.union([
typebotV5Schema._def.schema.shape.settings,
typebotV6Schema.shape.settings,
]),
})
.optional(),
resultId: z.string().optional(),
dynamicTheme: dynamicThemeSchema.optional(),
logs: z.array(chatLogSchema).optional(),
lastMessageNewFormat: z
.string()
.optional()
.describe(
'The sent message is validated and formatted on the backend. This is set only if the message differs from the formatted version.'
),
})

View File

@ -29,6 +29,7 @@ const chatSessionSchema = z.object({
updatedAt: z.date(), updatedAt: z.date(),
state: sessionStateSchema, state: sessionStateSchema,
}) })
export type ChatSession = z.infer<typeof chatSessionSchema>
const textMessageSchema = z.object({ const textMessageSchema = z.object({
type: z.literal(BubbleBlockType.TEXT), type: z.literal(BubbleBlockType.TEXT),
@ -59,7 +60,7 @@ const embedMessageSchema = z.object({
.merge(z.object({ height: z.number().optional() })), .merge(z.object({ height: z.number().optional() })),
}) })
const chatMessageSchema = z export const chatMessageSchema = z
.object({ id: z.string() }) .object({ id: z.string() })
.and( .and(
z.discriminatedUnion('type', [ z.discriminatedUnion('type', [
@ -70,6 +71,7 @@ const chatMessageSchema = z
embedMessageSchema, embedMessageSchema,
]) ])
) )
export type ChatMessage = z.infer<typeof chatMessageSchema>
const scriptToExecuteSchema = z.object({ const scriptToExecuteSchema = z.object({
content: z.string(), content: z.string(),
@ -85,6 +87,7 @@ const scriptToExecuteSchema = z.object({
}) })
), ),
}) })
export type ScriptToExecute = z.infer<typeof scriptToExecuteSchema>
const startTypebotPick = { const startTypebotPick = {
version: true, version: true,
@ -103,87 +106,72 @@ export const startTypebotSchema = z.preprocess(
typebotV6Schema.pick(startTypebotPick), typebotV6Schema.pick(startTypebotPick),
]) ])
) )
export type StartTypebot = z.infer<typeof startTypebotSchema>
export const startElementIdSchema = z.union([ export const chatLogSchema = logSchema
z.object({
startGroupId: z.string().describe('Start chat from a specific group.'),
startEventId: z.never().optional(),
}),
z.object({
startEventId: z.string().describe('Start chat from a specific event.'),
startGroupId: z.never().optional(),
}),
z.object({}),
])
export type StartElementId = z.infer<typeof startElementIdSchema>
const startParamsSchema = z
.object({
typebot: startTypebotSchema
.or(z.string())
.describe(
'Either a Typebot ID or a Typebot object. If you provide a Typebot object, it will be executed in preview mode. ([How can I find my typebot ID?](https://docs.typebot.io/api#how-to-find-my-typebotid)).'
),
isPreview: z
.boolean()
.optional()
.describe(
"If set to `true`, it will start a Preview session with the unpublished bot and it won't be saved in the Results tab. You need to be authenticated with a bearer token for this to work."
),
resultId: z
.string()
.optional()
.describe("Provide it if you'd like to overwrite an existing result."),
prefilledVariables: z
.record(z.unknown())
.optional()
.describe(
'[More info about prefilled variables.](https://docs.typebot.io/editor/variables#prefilled-variables)'
),
isStreamEnabled: z
.boolean()
.optional()
.describe(
'Set this to `true` if you intend to stream OpenAI completions on a client.'
),
isOnlyRegistering: z
.boolean()
.optional()
.describe(
'If set to `true`, it will only register the session and not start the chat. This is used for other chat platform integration as it can require a session to be registered before sending the first message.'
),
})
.and(startElementIdSchema)
const replyLogSchema = logSchema
.pick({ .pick({
status: true, status: true,
description: true, description: true,
}) })
.merge(z.object({ details: z.unknown().optional() })) .merge(z.object({ details: z.unknown().optional() }))
export type ChatLog = z.infer<typeof chatLogSchema>
export const sendMessageInputSchema = z.object({ export const startChatInputSchema = z.object({
message: z publicId: z.string(),
isStreamEnabled: z.boolean().optional(),
message: z.string().optional(),
resultId: z
.string() .string()
.optional() .optional()
.describe( .describe("Provide it if you'd like to overwrite an existing result."),
'The answer to the previous chat input. Do not provide it if you are starting a new chat.' isOnlyRegistering: z
), .boolean()
sessionId: z
.string()
.optional() .optional()
.describe( .describe(
'Session ID that you get from the initial chat request to a bot. If not provided, it will create a new session.' 'If set to `true`, it will only register the session and not start the bot. This is used for 3rd party chat platforms as it can require a session to be registered before sending the first message.'
), ),
startParams: startParamsSchema.optional(), prefilledVariables: z
clientLogs: z .record(z.unknown())
.array(replyLogSchema)
.optional() .optional()
.describe('Logs while executing client side actions'), .describe(
'[More info about prefilled variables.](https://docs.typebot.io/editor/variables#prefilled-variables)'
),
}) })
export type StartChatInput = z.infer<typeof startChatInputSchema>
const runtimeOptionsSchema = paymentInputRuntimeOptionsSchema.optional() export const startFromSchema = z.discriminatedUnion('type', [
z.object({
type: z.literal('group'),
groupId: z.string(),
}),
z.object({
type: z.literal('event'),
eventId: z.string(),
}),
])
export type StartFrom = z.infer<typeof startFromSchema>
export const startPreviewChatInputSchema = z.object({
typebotId: z.string(),
isStreamEnabled: z.boolean().optional(),
message: z.string().optional(),
isOnlyRegistering: z
.boolean()
.optional()
.describe(
'If set to `true`, it will only register the session and not start the bot. This is used for 3rd party chat platforms as it can require a session to be registered before sending the first message.'
),
typebot: startTypebotSchema
.optional()
.describe(
'If set, it will override the typebot that is used to start the chat.'
),
startFrom: startFromSchema.optional(),
})
export type StartPreviewChatInput = z.infer<typeof startPreviewChatInputSchema>
export const runtimeOptionsSchema = paymentInputRuntimeOptionsSchema.optional()
export type RuntimeOptions = z.infer<typeof runtimeOptionsSchema>
const startPropsToInjectSchema = z.object({ const startPropsToInjectSchema = z.object({
googleAnalyticsId: z.string().optional(), googleAnalyticsId: z.string().optional(),
@ -191,8 +179,9 @@ const startPropsToInjectSchema = z.object({
gtmId: z.string().optional(), gtmId: z.string().optional(),
customHeadCode: z.string().optional(), customHeadCode: z.string().optional(),
}) })
export type StartPropsToInject = z.infer<typeof startPropsToInjectSchema>
const clientSideActionSchema = z export const clientSideActionSchema = z
.object({ .object({
lastBubbleBlockId: z.string().optional(), lastBubbleBlockId: z.string().optional(),
expectsDedicatedReply: z.boolean().optional(), expectsDedicatedReply: z.boolean().optional(),
@ -271,7 +260,14 @@ export const typebotInChatReply = z.preprocess(
typebotV6Schema.pick(typebotInChatReplyPick), typebotV6Schema.pick(typebotInChatReplyPick),
]) ])
) )
export const chatReplySchema = z.object({
const chatResponseBaseSchema = z.object({
lastMessageNewFormat: z
.string()
.optional()
.describe(
'The sent message is validated and formatted on the backend. This is set only if the message differs from the formatted version.'
),
messages: z.array(chatMessageSchema), messages: z.array(chatMessageSchema),
input: z input: z
.union([ .union([
@ -286,39 +282,30 @@ export const chatReplySchema = z.object({
) )
.optional(), .optional(),
clientSideActions: z.array(clientSideActionSchema).optional(), clientSideActions: z.array(clientSideActionSchema).optional(),
sessionId: z.string().optional(), logs: z.array(chatLogSchema).optional(),
typebot: z
.object({
id: z.string(),
theme: z.union([
typebotV5Schema._def.schema.shape.theme,
typebotV6Schema.shape.theme,
]),
settings: z.union([
typebotV5Schema._def.schema.shape.settings,
typebotV6Schema.shape.settings,
]),
})
.optional(),
resultId: z.string().optional(),
dynamicTheme: dynamicThemeSchema.optional(), dynamicTheme: dynamicThemeSchema.optional(),
logs: z.array(replyLogSchema).optional(),
lastMessageNewFormat: z
.string()
.optional()
.describe(
'The sent message is validated and formatted on the backend. This is set only if the message differs from the formatted version.'
),
}) })
export type ChatSession = z.infer<typeof chatSessionSchema> export const startChatResponseSchema = chatResponseBaseSchema.extend({
sessionId: z.string().optional(),
typebot: z.object({
id: z.string(),
theme: z.union([
typebotV5Schema._def.schema.shape.theme,
typebotV6Schema.shape.theme,
]),
settings: z.union([
typebotV5Schema._def.schema.shape.settings,
typebotV6Schema.shape.settings,
]),
}),
resultId: z.string().optional(),
})
export type StartChatResponse = z.infer<typeof startChatResponseSchema>
export type ChatReply = z.infer<typeof chatReplySchema> export const startPreviewChatResponseSchema = startChatResponseSchema.omit({
export type ChatMessage = z.infer<typeof chatMessageSchema> resultId: true,
export type SendMessageInput = z.infer<typeof sendMessageInputSchema> })
export type ScriptToExecute = z.infer<typeof scriptToExecuteSchema>
export type StartParams = z.infer<typeof startParamsSchema> export const continueChatResponseSchema = chatResponseBaseSchema
export type RuntimeOptions = z.infer<typeof runtimeOptionsSchema> export type ContinueChatResponse = z.infer<typeof continueChatResponseSchema>
export type StartTypebot = z.infer<typeof startTypebotSchema>
export type ReplyLog = z.infer<typeof replyLogSchema>
export type StartPropsToInject = z.infer<typeof startPropsToInjectSchema>