2
0

Introduce a new high-performing standalone chat API (#1200)

Closes #1154

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
	- Added authentication functionality for user sessions in chat API.
- Introduced chat-related API endpoints for starting, previewing, and
continuing chat sessions, and streaming messages.
- Implemented WhatsApp API webhook handling for receiving and processing
messages.
- Added environment variable `NEXT_PUBLIC_CHAT_API_URL` for chat API URL
configuration.

- **Bug Fixes**
	- Adjusted file upload logic to correctly determine the API host.
	- Fixed message streaming URL in chat integration with OpenAI.

- **Documentation**
- Updated guides for creating blocks, local installation, self-hosting,
and deployment to use `bun` instead of `pnpm`.

- **Refactor**
	- Refactored chat API functionalities to use modular architecture.
- Simplified client log saving and session update functionalities by
using external functions.
	- Transitioned package management and workflow commands to use `bun`.

- **Chores**
- Switched to `bun` for package management in Dockerfiles and GitHub
workflows.
	- Added new Dockerfile for chat API service setup with Bun framework.
	- Updated `.prettierignore` and documentation with new commands.

- **Style**
	- No visible changes to end-users.

- **Tests**
	- No visible changes to end-users.

- **Revert**
	- No reverts in this release.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Baptiste Arnaud
2024-03-21 10:23:23 +01:00
committed by GitHub
parent 5b9176708c
commit 2fcf83c529
51 changed files with 1446 additions and 494 deletions

View File

@@ -1,3 +1,3 @@
emojiList.json
iconNames.ts
reporters
reporters

View File

@@ -21,6 +21,12 @@ const injectViewerUrlIfVercelPreview = (val) => {
process.env.VERCEL_BUILDER_PROJECT_NAME,
process.env.NEXT_PUBLIC_VERCEL_VIEWER_PROJECT_NAME
)
if (process.env.NEXT_PUBLIC_CHAT_API_URL.includes('{{pr_id}}'))
process.env.NEXT_PUBLIC_CHAT_API_URL =
process.env.NEXT_PUBLIC_CHAT_API_URL.replace(
'{{pr_id}}',
process.env.VERCEL_GIT_PULL_REQUEST_ID
)
}
injectViewerUrlIfVercelPreview(process.env.NEXT_PUBLIC_VIEWER_URL)

View File

@@ -75,7 +75,7 @@
"next": "14.1.0",
"next-auth": "4.22.1",
"nextjs-cors": "2.1.2",
"nodemailer": "6.9.3",
"nodemailer": "6.9.8",
"nprogress": "0.2.0",
"openai": "4.28.4",
"papaparse": "5.4.1",
@@ -114,7 +114,7 @@
"@types/jsonwebtoken": "9.0.2",
"@types/micro-cors": "0.1.3",
"@types/node": "20.4.2",
"@types/nodemailer": "6.4.8",
"@types/nodemailer": "6.4.14",
"@types/nprogress": "0.2.0",
"@types/papaparse": "5.3.7",
"@types/prettier": "2.7.3",

View File

@@ -114,6 +114,7 @@ export const startWhatsAppPreview = authenticatedProcedure
typebotId,
startFrom,
userId: user.id,
isStreamEnabled: false,
},
initialSessionState: {
whatsApp: (existingSession?.state as SessionState | undefined)

View File

@@ -0,0 +1,36 @@
{
"name": "chat-api",
"version": "1.0.0",
"license": "AGPL-3.0-or-later",
"private": true,
"scripts": {
"dev": "dotenv -e ./.env -e ../../.env -- bun --hot src/index.ts",
"build": "dotenv -e ./.env -e ../../.env -- bun build --target=bun ./src/index.ts --outdir ./dist",
"start": "bun src/index.ts"
},
"dependencies": {
"@hono/prometheus": "1.0.0",
"@hono/sentry": "1.0.1",
"@hono/typebox-validator": "0.2.2",
"@sinclair/typebox": "0.32.5",
"@trpc/server": "10.40.0",
"@typebot.io/bot-engine": "workspace:*",
"@typebot.io/env": "workspace:*",
"@typebot.io/forge": "workspace:*",
"@typebot.io/forge-repository": "workspace:*",
"@typebot.io/lib": "workspace:*",
"@typebot.io/prisma": "workspace:*",
"@typebot.io/schemas": "workspace:*",
"@typebot.io/variables": "workspace:*",
"ai": "3.0.12",
"hono": "4.0.5",
"openai": "4.28.4",
"prom-client": "15.1.0"
},
"devDependencies": {
"dotenv-cli": "7.2.1",
"@typebot.io/tsconfig": "workspace:*",
"@types/react": "18.2.15",
"react": "18.2.0"
}
}

30
apps/chat-api/src/auth.ts Normal file
View File

@@ -0,0 +1,30 @@
import { env } from '@typebot.io/env'
import { mockedUser } from '@typebot.io/lib/mockedUser'
import prisma from '@typebot.io/lib/prisma'
export const getAuthenticatedUserId = async (
authorizationHeaderValue: string | undefined
): Promise<string | undefined> => {
if (env.NEXT_PUBLIC_E2E_TEST) return mockedUser.id
const bearerToken = extractBearerToken(authorizationHeaderValue)
if (!bearerToken) return
return authenticateByToken(bearerToken)
}
const authenticateByToken = async (
token: string
): Promise<string | undefined> => {
if (typeof window !== 'undefined') return
const apiToken = await prisma.apiToken.findFirst({
where: {
token,
},
select: {
ownerId: true,
},
})
return apiToken?.ownerId
}
const extractBearerToken = (authorizationHeaderValue: string | undefined) =>
authorizationHeaderValue?.slice(7)

View File

@@ -0,0 +1,30 @@
import { Hono } from 'hono'
import { webRuntime } from './runtimes/web'
import { whatsAppRuntime } from './runtimes/whatsapp'
import { prometheus } from '@hono/prometheus'
import { sentry } from '@hono/sentry'
import { env } from '@typebot.io/env'
const app = new Hono()
app.use(
'*',
sentry({
environment: env.NODE_ENV,
dsn: env.NEXT_PUBLIC_SENTRY_DSN,
release: process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA + '-chat-api',
})
)
const { printMetrics, registerMetrics } = prometheus()
app.use('*', registerMetrics)
app.get('/metrics', printMetrics)
app.get('/ping', (c) => c.json({ status: 'ok' }, 200))
app.route('/', webRuntime)
app.route('/', whatsAppRuntime)
export default {
port: process.env.PORT ?? 3002,
fetch: app.fetch,
}

View File

@@ -0,0 +1,132 @@
import { startChat } from '@typebot.io/bot-engine/apiHandlers/startChat'
import { continueChat } from '@typebot.io/bot-engine/apiHandlers/continueChat'
import { startChatPreview } from '@typebot.io/bot-engine/apiHandlers/startChatPreview'
import { getMessageStream } from '@typebot.io/bot-engine/apiHandlers/getMessageStream'
import { Hono } from 'hono'
import { cors } from 'hono/cors'
import { tbValidator } from '@hono/typebox-validator'
import { Type as t } from '@sinclair/typebox'
import { getAuthenticatedUserId } from '../auth'
export const webRuntime = new Hono()
webRuntime.use('*', cors())
webRuntime.post(
'/api/v1/typebots/:publicId/startChat',
tbValidator(
'json',
t.Object({
message: t.Optional(t.String()),
isStreamEnabled: t.Optional(t.Boolean()),
resultId: t.Optional(t.String()),
isOnlyRegistering: t.Optional(t.Boolean()),
prefilledVariables: t.Optional(t.Record(t.String(), t.Unknown())),
}),
(result, c) => {
if (!result.success) return c.json({ message: 'Invalid input' }, 400)
}
),
async (c) => {
const data = c.req.valid('json')
const { corsOrigin, ...response } = await startChat({
...data,
publicId: c.req.param('publicId'),
isStreamEnabled: data.isStreamEnabled ?? true,
isOnlyRegistering: data.isOnlyRegistering ?? false,
origin: c.req.header('origin'),
})
if (corsOrigin) c.res.headers.set('Access-Control-Allow-Origin', corsOrigin)
return c.json(response)
}
)
webRuntime.post(
'/api/v1/typebots/:id/preview/startChat',
tbValidator(
'json',
t.Object({
message: t.Optional(t.String()),
isStreamEnabled: t.Optional(t.Boolean()),
resultId: t.Optional(t.String()),
isOnlyRegistering: t.Optional(t.Boolean()),
prefilledVariables: t.Optional(t.Record(t.String(), t.Unknown())),
startFrom: t.Optional(
t.Union([
t.Object({
type: t.Literal('group'),
groupId: t.String(),
}),
t.Object({
type: t.Literal('event'),
eventId: t.String(),
}),
])
),
typebot: t.Optional(t.Any()),
}),
(result, c) => {
if (!result.success) return c.json({ message: 'Invalid input' }, 400)
}
),
async (c) => {
const data = c.req.valid('json')
const userId = !data.typebot
? await getAuthenticatedUserId(c.req.header('Authorization'))
: undefined
return c.json(
await startChatPreview({
...data,
typebotId: c.req.param('id'),
userId,
isStreamEnabled: data.isStreamEnabled ?? true,
isOnlyRegistering: data.isOnlyRegistering ?? false,
})
)
}
)
webRuntime.post(
'/api/v1/sessions/:sessionId/continueChat',
tbValidator(
'json',
t.Object({
message: t.Optional(t.String()),
}),
(result, c) => {
if (!result.success) return c.json({ message: 'Invalid input' }, 400)
}
),
async (c) => {
const data = c.req.valid('json')
const { corsOrigin, ...response } = await continueChat({
...data,
sessionId: c.req.param('sessionId'),
origin: c.req.header('origin'),
})
if (corsOrigin) c.res.headers.set('Access-Control-Allow-Origin', corsOrigin)
return c.json(response)
}
)
webRuntime.post(
'/api/v1/sessions/:sessionId/streamMessage',
tbValidator(
'json',
t.Object({
messages: t.Optional(t.Array(t.Any())),
}),
(result, c) => {
if (!result.success) return c.json({ message: 'Invalid input' }, 400)
}
),
async (c) => {
const data = c.req.valid('json')
const { stream, status, message } = await getMessageStream({
sessionId: c.req.param('sessionId'),
messages: data.messages,
})
if (!stream) return c.json({ message }, (status ?? 400) as ResponseInit)
return new Response(stream)
}
)

View File

@@ -0,0 +1,51 @@
import { receiveMessage } from '@typebot.io/bot-engine/apiHandlers/receiveMessage'
import { receiveMessagePreview } from '@typebot.io/bot-engine/apiHandlers/receiveMessagePreview'
import { tbValidator } from '@hono/typebox-validator'
import { Hono } from 'hono'
import { Type as t } from '@sinclair/typebox'
export const whatsAppRuntime = new Hono()
whatsAppRuntime.post(
'/api/v1/workspaces/:workspaceId/whatsapp/:credentialsId/webhook',
tbValidator(
'json',
t.Object({
object: t.String(),
entry: t.Any(),
}),
(result, c) => {
if (!result.success) return c.json({ message: 'Invalid input' }, 400)
}
),
async (c) => {
const data = c.req.valid('json')
receiveMessage({
workspaceId: c.req.param('workspaceId'),
credentialsId: c.req.param('credentialsId'),
...data,
})
return c.json({ message: 'Webhook received' }, 200)
}
)
whatsAppRuntime.post(
'/api/v1/whatsapp/preview/webhook',
tbValidator(
'json',
t.Object({
object: t.String(),
entry: t.Any(),
}),
(result, c) => {
if (!result.success) return c.json({ message: 'Invalid input' }, 400)
}
),
async (c) => {
const data = c.req.valid('json')
receiveMessagePreview({
...data,
})
return c.json({ message: 'Webhook received' }, 200)
}
)

View File

@@ -0,0 +1,8 @@
{
"extends": "@typebot.io/tsconfig/base.json",
"include": ["src/**/*.ts"],
"exclude": ["node_modules"],
"compilerOptions": {
"jsx": "react"
}
}

View File

@@ -1772,14 +1772,16 @@
"type": "object",
"properties": {
"isStreamEnabled": {
"type": "boolean"
"type": "boolean",
"default": false
},
"message": {
"type": "string"
},
"isOnlyRegistering": {
"type": "boolean",
"description": "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."
"description": "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.",
"default": false
},
"typebot": {
"oneOf": [

View File

@@ -2,7 +2,7 @@
"name": "landing-page",
"version": "1.0.0",
"scripts": {
"dev": "dotenv -e ./.env -e ../../.env -- next dev -p 3002",
"dev": "dotenv -e ./.env -e ../../.env -- next dev -p 3003",
"start": "dotenv -e ./.env -e ../../.env -- next start",
"build": "dotenv -e ./.env -e ../../.env -- next build",
"lint": "next lint",

View File

@@ -17,6 +17,12 @@ const injectViewerUrlIfVercelPreview = (val) => {
)
return
process.env.NEXT_PUBLIC_VIEWER_URL = `https://${process.env.VERCEL_BRANCH_URL}`
if (process.env.NEXT_PUBLIC_CHAT_API_URL.includes('{{pr_id}}'))
process.env.NEXT_PUBLIC_CHAT_API_URL =
process.env.NEXT_PUBLIC_CHAT_API_URL.replace(
'{{pr_id}}',
process.env.VERCEL_GIT_PULL_REQUEST_ID
)
}
injectViewerUrlIfVercelPreview(process.env.NEXT_PUBLIC_VIEWER_URL)

View File

@@ -28,7 +28,7 @@
"got": "12.6.0",
"next": "14.1.0",
"nextjs-cors": "2.1.2",
"nodemailer": "6.9.3",
"nodemailer": "6.9.8",
"openai": "4.28.4",
"qs": "6.11.2",
"react": "18.2.0",
@@ -50,7 +50,7 @@
"@typebot.io/variables": "workspace:*",
"@types/cors": "2.8.13",
"@types/node": "20.4.2",
"@types/nodemailer": "6.4.8",
"@types/nodemailer": "6.4.14",
"@types/papaparse": "5.3.7",
"@types/qs": "6.9.7",
"@types/react": "18.2.15",

View File

@@ -0,0 +1,45 @@
import { getMessageStream } from '@typebot.io/bot-engine/apiHandlers/getMessageStream'
import { StreamingTextResponse } from 'ai'
import { NextResponse } from 'next/server'
export const dynamic = 'force-dynamic'
const responseHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'POST, OPTIONS',
'Access-Control-Expose-Headers': 'Content-Length, X-JSON',
'Access-Control-Allow-Headers': '*',
}
export async function OPTIONS() {
return new Response('ok', {
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'POST',
'Access-Control-Expose-Headers': 'Content-Length, X-JSON',
'Access-Control-Allow-Headers': '*',
},
})
}
export async function POST(
req: Request,
{ params }: { params: { sessionId: string } }
) {
if (process.env.VERCEL_ENV)
return NextResponse.json(
{ message: "Can't get streaming if hosted on Vercel" },
{ status: 400, headers: responseHeaders }
)
const messages =
typeof req.body === 'string' ? JSON.parse(req.body) : req.body
const { stream, status, message } = await getMessageStream({
sessionId: params.sessionId,
messages,
})
if (!stream)
return NextResponse.json({ message }, { status, headers: responseHeaders })
return new StreamingTextResponse(stream, {
headers: responseHeaders,
})
}

View File

@@ -1,14 +1,7 @@
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, isNotDefined } from '@typebot.io/lib/utils'
import { z } from 'zod'
import { filterPotentiallySensitiveLogs } from '@typebot.io/bot-engine/logs/filterPotentiallySensitiveLogs'
import { computeCurrentProgress } from '@typebot.io/bot-engine/computeCurrentProgress'
import { continueChat as continueChatFn } from '@typebot.io/bot-engine/apiHandlers/continueChat'
export const continueChat = publicProcedure
.meta({
@@ -29,92 +22,12 @@ export const continueChat = publicProcedure
})
)
.output(continueChatResponseSchema)
.mutation(async ({ input: { sessionId, message }, ctx: { res, origin } }) => {
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.',
})
if (
session?.state.allowedOrigins &&
session.state.allowedOrigins.length > 0
) {
if (origin && session.state.allowedOrigins.includes(origin))
res.setHeader('Access-Control-Allow-Origin', origin)
else
res.setHeader(
'Access-Control-Allow-Origin',
session.state.allowedOrigins[0]
)
}
const {
messages,
input,
clientSideActions,
newSessionState,
logs,
lastMessageNewFormat,
visitedEdges,
} = await continueBotFlow(message, {
version: 2,
state: session.state,
startTime: Date.now(),
.mutation(async ({ input: { sessionId, message }, ctx: { origin, res } }) => {
const { corsOrigin, ...response } = await continueChatFn({
origin,
sessionId,
message,
})
if (newSessionState)
await saveStateToDatabase({
session: {
id: session.id,
state: newSessionState,
},
input,
logs,
clientSideActions,
visitedEdges,
hasCustomEmbedBubble: messages.some(
(message) => message.type === 'custom-embed'
),
})
const isPreview = isNotDefined(session.state.typebotsQueue[0].resultId)
const isEnded =
newSessionState.progressMetadata &&
!input?.id &&
(clientSideActions?.filter((c) => c.expectsDedicatedReply).length ??
0) === 0
return {
messages,
input,
clientSideActions,
dynamicTheme: parseDynamicTheme(newSessionState),
logs: isPreview ? logs : logs?.filter(filterPotentiallySensitiveLogs),
lastMessageNewFormat,
progress: newSessionState.progressMetadata
? isEnded
? 100
: computeCurrentProgress({
typebotsQueue: newSessionState.typebotsQueue,
progressMetadata: newSessionState.progressMetadata,
currentInputBlockId: input?.id,
})
: undefined,
}
if (corsOrigin) res.setHeader('Access-Control-Allow-Origin', corsOrigin)
return response
})

View File

@@ -1,11 +1,7 @@
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'
import { saveClientLogs as saveClientLogsFn } from '@typebot.io/bot-engine/apiHandlers/saveClientLogs'
export const saveClientLogs = publicProcedure
.meta({
@@ -22,42 +18,6 @@ export const saveClientLogs = publicProcedure
})
)
.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.',
})
}
})
.mutation(({ input: { sessionId, clientLogs } }) =>
saveClientLogsFn({ sessionId, clientLogs })
)

View File

@@ -3,11 +3,7 @@ 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'
import { filterPotentiallySensitiveLogs } from '@typebot.io/bot-engine/logs/filterPotentiallySensitiveLogs'
import { computeCurrentProgress } from '@typebot.io/bot-engine/computeCurrentProgress'
import { startChat as startChatFn } from '@typebot.io/bot-engine/apiHandlers/startChat'
export const startChat = publicProcedure
.meta({
@@ -19,99 +15,11 @@ export const startChat = publicProcedure
})
.input(startChatInputSchema)
.output(startChatResponseSchema)
.mutation(
async ({
input: {
message,
isOnlyRegistering,
publicId,
isStreamEnabled,
prefilledVariables,
resultId: startResultId,
},
ctx: { origin, res },
}) => {
const {
typebot,
messages,
input,
resultId,
dynamicTheme,
logs,
clientSideActions,
newSessionState,
visitedEdges,
} = await startSession({
version: 2,
startParams: {
type: 'live',
isOnlyRegistering,
isStreamEnabled,
publicId,
prefilledVariables,
resultId: startResultId,
},
message,
})
if (
newSessionState.allowedOrigins &&
newSessionState.allowedOrigins.length > 0
) {
if (origin && newSessionState.allowedOrigins.includes(origin))
res.setHeader('Access-Control-Allow-Origin', origin)
else
res.setHeader(
'Access-Control-Allow-Origin',
newSessionState.allowedOrigins[0]
)
}
const session = isOnlyRegistering
? await restartSession({
state: newSessionState,
})
: await saveStateToDatabase({
session: {
state: newSessionState,
},
input,
logs,
clientSideActions,
visitedEdges,
hasCustomEmbedBubble: messages.some(
(message) => message.type === 'custom-embed'
),
})
const isEnded =
newSessionState.progressMetadata &&
!input?.id &&
(clientSideActions?.filter((c) => c.expectsDedicatedReply).length ??
0) === 0
return {
sessionId: session.id,
typebot: {
id: typebot.id,
theme: typebot.theme,
settings: typebot.settings,
},
messages,
input,
resultId,
dynamicTheme,
logs: logs?.filter(filterPotentiallySensitiveLogs),
clientSideActions,
progress: newSessionState.progressMetadata
? isEnded
? 100
: computeCurrentProgress({
typebotsQueue: newSessionState.typebotsQueue,
progressMetadata: newSessionState.progressMetadata,
currentInputBlockId: input?.id,
})
: undefined,
}
}
)
.mutation(async ({ input, ctx: { origin, res } }) => {
const { corsOrigin, ...response } = await startChatFn({
...input,
origin,
})
if (corsOrigin) res.setHeader('Access-Control-Allow-Origin', corsOrigin)
return response
})

View File

@@ -2,11 +2,8 @@ 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'
import { publicProcedure } from '@/helpers/server/trpc'
import { computeCurrentProgress } from '@typebot.io/bot-engine/computeCurrentProgress'
import { startChatPreview as startChatPreviewFn } from '@typebot.io/bot-engine/apiHandlers/startChatPreview'
export const startChatPreview = publicProcedure
.meta({
@@ -32,75 +29,15 @@ export const startChatPreview = publicProcedure
prefilledVariables,
},
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,
prefilledVariables,
},
}) =>
startChatPreviewFn({
message,
isOnlyRegistering,
isStreamEnabled,
startFrom,
typebotId,
typebot: startTypebot,
userId: user?.id,
prefilledVariables,
})
const session = isOnlyRegistering
? await restartSession({
state: newSessionState,
})
: await saveStateToDatabase({
session: {
state: newSessionState,
},
input,
logs,
clientSideActions,
visitedEdges,
hasCustomEmbedBubble: messages.some(
(message) => message.type === 'custom-embed'
),
})
const isEnded =
newSessionState.progressMetadata &&
!input?.id &&
(clientSideActions?.filter((c) => c.expectsDedicatedReply).length ??
0) === 0
return {
sessionId: session.id,
typebot: {
id: typebot.id,
theme: typebot.theme,
settings: typebot.settings,
},
messages,
input,
dynamicTheme,
logs,
clientSideActions,
progress: newSessionState.progressMetadata
? isEnded
? 100
: computeCurrentProgress({
typebotsQueue: newSessionState.typebotsQueue,
progressMetadata: newSessionState.progressMetadata,
currentInputBlockId: input?.id,
})
: undefined,
}
}
)

View File

@@ -1,14 +1,6 @@
import { publicProcedure } from '@/helpers/server/trpc'
import { TRPCError } from '@trpc/server'
import { z } from 'zod'
import { getSession } from '@typebot.io/bot-engine/queries/getSession'
import {
PublicTypebot,
SessionState,
Typebot,
Variable,
} from '@typebot.io/schemas'
import prisma from '@typebot.io/lib/prisma'
import { updateTypebotInSession as updateTypebotInSessionFn } from '@typebot.io/bot-engine/apiHandlers/updateTypebotInSession'
export const updateTypebotInSession = publicProcedure
.meta({
@@ -27,85 +19,6 @@ export const updateTypebotInSession = publicProcedure
})
)
.output(z.object({ message: z.literal('success') }))
.mutation(async ({ input: { sessionId }, ctx: { user } }) => {
if (!user)
throw new TRPCError({ code: 'UNAUTHORIZED', message: 'Unauthorized' })
const session = await getSession(sessionId)
if (!session)
throw new TRPCError({ code: 'NOT_FOUND', message: 'Session not found' })
const publicTypebot = (await prisma.publicTypebot.findFirst({
where: {
typebot: {
id: session.state.typebotsQueue[0].typebot.id,
OR: [
{
workspace: {
members: {
some: { userId: user.id, role: { in: ['ADMIN', 'MEMBER'] } },
},
},
},
{
collaborators: {
some: { userId: user.id, type: { in: ['WRITE'] } },
},
},
],
},
},
select: {
edges: true,
groups: true,
variables: true,
},
})) as Pick<PublicTypebot, 'edges' | 'variables' | 'groups'> | null
if (!publicTypebot)
throw new TRPCError({ code: 'UNAUTHORIZED', message: 'Unauthorized' })
const newSessionState = updateSessionState(session.state, publicTypebot)
await prisma.chatSession.updateMany({
where: { id: session.id },
data: { state: newSessionState },
})
return { message: 'success' }
})
const updateSessionState = (
currentState: SessionState,
newTypebot: Pick<PublicTypebot, 'edges' | 'variables' | 'groups'>
): SessionState => ({
...currentState,
typebotsQueue: currentState.typebotsQueue.map((typebotInQueue, index) =>
index === 0
? {
...typebotInQueue,
typebot: {
...typebotInQueue.typebot,
edges: newTypebot.edges,
groups: newTypebot.groups,
variables: updateVariablesInSession(
typebotInQueue.typebot.variables,
newTypebot.variables
),
},
}
: typebotInQueue
) as SessionState['typebotsQueue'],
})
const updateVariablesInSession = (
currentVariables: Variable[],
newVariables: Typebot['variables']
): Variable[] => [
...currentVariables,
...newVariables.filter(
(newVariable) =>
!currentVariables.find(
(currentVariable) => currentVariable.id === newVariable.id
)
),
]
.mutation(({ input: { sessionId }, ctx: { user } }) =>
updateTypebotInSessionFn({ user, sessionId })
)