From 53e4bc2b759ada4f43175493f5333dbff917dea9 Mon Sep 17 00:00:00 2001 From: Baptiste Arnaud Date: Fri, 4 Aug 2023 14:53:49 +0200 Subject: [PATCH] :zap: Add API endpoint to update the typebot in ongoing chat session --- apps/docs/openapi/chat/_spec_.json | 99 ++++++++++++++----- .../src/features/chat/api/sendMessage.ts | 3 +- .../chat/api/updateTypebotInSession.ts | 99 +++++++++++++++++++ .../src/helpers/server/routers/v1/_app.ts | 2 + packages/schemas/features/chat.ts | 2 +- 5 files changed, 175 insertions(+), 30 deletions(-) create mode 100644 apps/viewer/src/features/chat/api/updateTypebotInSession.ts diff --git a/apps/docs/openapi/chat/_spec_.json b/apps/docs/openapi/chat/_spec_.json index 0f2dff833..8f40410d6 100644 --- a/apps/docs/openapi/chat/_spec_.json +++ b/apps/docs/openapi/chat/_spec_.json @@ -15,11 +15,6 @@ "operationId": "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.", - "security": [ - { - "Authorization": [] - } - ], "requestBody": { "required": true, "content": { @@ -35,27 +30,6 @@ "type": "string", "description": "Session ID that you get from the initial chat request to a bot. If not provided, it will create a new session." }, - "clientLogs": { - "type": "array", - "items": { - "type": "object", - "properties": { - "status": { - "type": "string" - }, - "description": { - "type": "string" - }, - "details": {} - }, - "required": [ - "status", - "description" - ], - "additionalProperties": false - }, - "description": "Logs while executing client side actions" - }, "startParams": { "type": "object", "properties": { @@ -3566,13 +3540,35 @@ "description": "[More info about prefilled variables.](https://docs.typebot.io/editor/variables#prefilled-variables)" }, "isStreamEnabled": { - "type": "boolean" + "type": "boolean", + "description": "Set this to `true` if you intend to stream OpenAI completions on a client." } }, "required": [ "typebot" ], "additionalProperties": false + }, + "clientLogs": { + "type": "array", + "items": { + "type": "object", + "properties": { + "status": { + "type": "string" + }, + "description": { + "type": "string" + }, + "details": {} + }, + "required": [ + "status", + "description" + ], + "additionalProperties": false + }, + "description": "Logs while executing client side actions" } }, "additionalProperties": false @@ -5811,6 +5807,55 @@ } } } + }, + "/session/{sessionId}/updateTypebot": { + "post": { + "operationId": "updateTypebotInSession", + "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.", + "security": [ + { + "Authorization": [] + } + ], + "parameters": [ + { + "name": "sessionId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "enum": [ + "success" + ] + } + }, + "required": [ + "message" + ], + "additionalProperties": false + } + } + } + }, + "default": { + "$ref": "#/components/responses/error" + } + } + } } }, "components": { diff --git a/apps/viewer/src/features/chat/api/sendMessage.ts b/apps/viewer/src/features/chat/api/sendMessage.ts index 37b92a528..a115a2a21 100644 --- a/apps/viewer/src/features/chat/api/sendMessage.ts +++ b/apps/viewer/src/features/chat/api/sendMessage.ts @@ -40,12 +40,11 @@ export const sendMessage = publicProcedure 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.', - protect: true, }, }) .input(sendMessageInputSchema) .output(chatReplySchema) - .query( + .mutation( async ({ input: { sessionId, message, startParams, clientLogs }, ctx: { user }, diff --git a/apps/viewer/src/features/chat/api/updateTypebotInSession.ts b/apps/viewer/src/features/chat/api/updateTypebotInSession.ts new file mode 100644 index 000000000..1289fb9f5 --- /dev/null +++ b/apps/viewer/src/features/chat/api/updateTypebotInSession.ts @@ -0,0 +1,99 @@ +import { publicProcedure } from '@/helpers/server/trpc' +import { TRPCError } from '@trpc/server' +import { z } from 'zod' +import { getSession } from '../queries/getSession' +import prisma from '@/lib/prisma' +import { PublicTypebot, SessionState, Typebot } from '@typebot.io/schemas' + +export const updateTypebotInSession = publicProcedure + .meta({ + openapi: { + method: 'POST', + path: '/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.', + protect: true, + }, + }) + .input( + z.object({ + sessionId: z.string(), + }) + ) + .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.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 | 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 +): SessionState => ({ + ...currentState, + typebot: { + ...currentState.typebot, + edges: newTypebot.edges, + variables: updateVariablesInSession( + currentState.typebot.variables, + newTypebot.variables + ), + groups: newTypebot.groups, + }, +}) + +const updateVariablesInSession = ( + currentVariables: SessionState['typebot']['variables'], + newVariables: Typebot['variables'] +): SessionState['typebot']['variables'] => [ + ...currentVariables, + ...newVariables.filter( + (newVariable) => + !currentVariables.find( + (currentVariable) => currentVariable.id === newVariable.id + ) + ), +] diff --git a/apps/viewer/src/helpers/server/routers/v1/_app.ts b/apps/viewer/src/helpers/server/routers/v1/_app.ts index b2d73a7fe..37f2ada96 100644 --- a/apps/viewer/src/helpers/server/routers/v1/_app.ts +++ b/apps/viewer/src/helpers/server/routers/v1/_app.ts @@ -1,10 +1,12 @@ import { getUploadUrl } from '@/features/blocks/inputs/fileUpload/api/getUploadUrl' import { sendMessage } from '@/features/chat/api/sendMessage' import { router } from '../../trpc' +import { updateTypebotInSession } from '@/features/chat/api/updateTypebotInSession' export const appRouter = router({ sendMessage, getUploadUrl, + updateTypebotInSession, }) export type AppRouter = typeof appRouter diff --git a/packages/schemas/features/chat.ts b/packages/schemas/features/chat.ts index ac108939f..fcb11145c 100644 --- a/packages/schemas/features/chat.ts +++ b/packages/schemas/features/chat.ts @@ -150,7 +150,7 @@ const startParamsSchema = z.object({ .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 for this to work." + "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()