79
apps/viewer/src/features/chat/api/continueChat.ts
Normal file
79
apps/viewer/src/features/chat/api/continueChat.ts
Normal 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,
|
||||
}
|
||||
})
|
||||
@@ -1,8 +1,8 @@
|
||||
import { publicProcedure } from '@/helpers/server/trpc'
|
||||
import {
|
||||
chatReplySchema,
|
||||
sendMessageInputSchema,
|
||||
} from '@typebot.io/schemas/features/chat/schema'
|
||||
chatReplySchema,
|
||||
} from '@typebot.io/schemas/features/chat/legacy/schema'
|
||||
import { TRPCError } from '@trpc/server'
|
||||
import { getSession } from '@typebot.io/bot-engine/queries/getSession'
|
||||
import { startSession } from '@typebot.io/bot-engine/startSession'
|
||||
@@ -16,10 +16,12 @@ export const sendMessageV1 = publicProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: 'POST',
|
||||
path: '/sendMessage',
|
||||
path: '/v1/sendMessage',
|
||||
summary: 'Send a message',
|
||||
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.',
|
||||
tags: ['Deprecated'],
|
||||
deprecated: true,
|
||||
},
|
||||
})
|
||||
.input(sendMessageInputSchema)
|
||||
@@ -60,8 +62,45 @@ export const sendMessageV1 = publicProcedure
|
||||
visitedEdges,
|
||||
} = await startSession({
|
||||
version: 1,
|
||||
startParams,
|
||||
userId: user?.id,
|
||||
startParams:
|
||||
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,
|
||||
})
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -1,8 +1,4 @@
|
||||
import { publicProcedure } from '@/helpers/server/trpc'
|
||||
import {
|
||||
chatReplySchema,
|
||||
sendMessageInputSchema,
|
||||
} from '@typebot.io/schemas/features/chat/schema'
|
||||
import { TRPCError } from '@trpc/server'
|
||||
import { getSession } from '@typebot.io/bot-engine/queries/getSession'
|
||||
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 { parseDynamicTheme } from '@typebot.io/bot-engine/parseDynamicTheme'
|
||||
import { isDefined } from '@typebot.io/lib/utils'
|
||||
import {
|
||||
chatReplySchema,
|
||||
sendMessageInputSchema,
|
||||
} from '@typebot.io/schemas/features/chat/legacy/schema'
|
||||
|
||||
export const sendMessageV2 = publicProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: 'POST',
|
||||
path: '/sendMessage',
|
||||
path: '/v2/sendMessage',
|
||||
summary: 'Send a message',
|
||||
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.',
|
||||
tags: ['Deprecated'],
|
||||
deprecated: true,
|
||||
},
|
||||
})
|
||||
.input(sendMessageInputSchema)
|
||||
@@ -60,8 +62,45 @@ export const sendMessageV2 = publicProcedure
|
||||
visitedEdges,
|
||||
} = await startSession({
|
||||
version: 2,
|
||||
startParams,
|
||||
userId: user?.id,
|
||||
startParams:
|
||||
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,
|
||||
})
|
||||
|
||||
@@ -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
|
||||
}
|
||||
63
apps/viewer/src/features/chat/api/saveClientLogs.ts
Normal file
63
apps/viewer/src/features/chat/api/saveClientLogs.ts
Normal 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.',
|
||||
})
|
||||
}
|
||||
})
|
||||
83
apps/viewer/src/features/chat/api/startChat.ts
Normal file
83
apps/viewer/src/features/chat/api/startChat.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
)
|
||||
83
apps/viewer/src/features/chat/api/startChatPreview.ts
Normal file
83
apps/viewer/src/features/chat/api/startChatPreview.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -1,4 +1,3 @@
|
||||
import { publicProcedure } from '@/helpers/server/trpc'
|
||||
import { TRPCError } from '@trpc/server'
|
||||
import { z } from 'zod'
|
||||
import { getSession } from '@typebot.io/bot-engine/queries/getSession'
|
||||
@@ -9,12 +8,13 @@ import {
|
||||
Variable,
|
||||
} from '@typebot.io/schemas'
|
||||
import prisma from '@typebot.io/lib/prisma'
|
||||
import { authenticatedProcedure } from '@/helpers/server/trpc'
|
||||
|
||||
export const updateTypebotInSession = publicProcedure
|
||||
export const updateTypebotInSession = authenticatedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: 'POST',
|
||||
path: '/sessions/{sessionId}/updateTypebot',
|
||||
path: '/v1/sessions/{sessionId}/updateTypebot',
|
||||
summary: 'Update typebot in session',
|
||||
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.',
|
||||
@@ -28,8 +28,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' })
|
||||
|
||||
@@ -19,10 +19,11 @@ export const getUploadUrl = publicProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
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',
|
||||
description: 'Used for the web client to get the bucket upload file.',
|
||||
deprecated: true,
|
||||
tags: ['Deprecated'],
|
||||
},
|
||||
})
|
||||
.input(
|
||||
|
||||
@@ -13,7 +13,7 @@ export const generateUploadUrl = publicProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: 'POST',
|
||||
path: '/generate-upload-url',
|
||||
path: '/v1/generate-upload-url',
|
||||
summary: 'Generate upload URL',
|
||||
description: 'Used to upload anything from the client to S3 bucket',
|
||||
},
|
||||
|
||||
@@ -8,7 +8,7 @@ export const receiveMessage = publicProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: 'POST',
|
||||
path: '/workspaces/{workspaceId}/whatsapp/{credentialsId}/webhook',
|
||||
path: '/v1/workspaces/{workspaceId}/whatsapp/{credentialsId}/webhook',
|
||||
summary: 'Message webhook',
|
||||
tags: ['WhatsApp'],
|
||||
},
|
||||
|
||||
@@ -7,7 +7,7 @@ export const subscribeWebhook = publicProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: 'GET',
|
||||
path: '/workspaces/{workspaceId}/whatsapp/{credentialsId}/webhook',
|
||||
path: '/v1/workspaces/{workspaceId}/whatsapp/{credentialsId}/webhook',
|
||||
summary: 'Subscribe webhook',
|
||||
tags: ['WhatsApp'],
|
||||
protect: true,
|
||||
|
||||
Reference in New Issue
Block a user