2
0

Add attachments option to text input (#1608)

Closes #854
This commit is contained in:
Baptiste Arnaud
2024-06-26 10:13:38 +02:00
committed by GitHub
parent 80da7af4f1
commit 6db0464fd7
88 changed files with 2959 additions and 735 deletions

View File

@@ -1,5 +1,8 @@
import { publicProcedure } from '@/helpers/server/trpc'
import { continueChatResponseSchema } from '@typebot.io/schemas/features/chat/schema'
import {
continueChatResponseSchema,
messageSchema,
} from '@typebot.io/schemas/features/chat/schema'
import { z } from 'zod'
import { continueChat as continueChatFn } from '@typebot.io/bot-engine/apiHandlers/continueChat'
@@ -13,7 +16,7 @@ export const continueChat = publicProcedure
})
.input(
z.object({
message: z.string().optional(),
message: messageSchema.optional(),
sessionId: z
.string()
.describe(

View File

@@ -91,7 +91,9 @@ export const sendMessageV1 = publicProcedure
typeof startParams.typebot === 'string'
? undefined
: startParams.typebot,
message,
message: message
? { type: 'text', text: message }
: undefined,
userId: user?.id,
textBubbleContentFormat: 'richText',
}
@@ -102,10 +104,11 @@ export const sendMessageV1 = publicProcedure
publicId: startParams.typebot,
prefilledVariables: startParams.prefilledVariables,
resultId: startParams.resultId,
message,
message: message
? { type: 'text', text: message }
: undefined,
textBubbleContentFormat: 'richText',
},
message,
})
if (startParams.isPreview || typeof startParams.typebot !== 'string') {
@@ -185,11 +188,14 @@ export const sendMessageV1 = publicProcedure
lastMessageNewFormat,
visitedEdges,
setVariableHistory,
} = await continueBotFlow(message, {
version: 1,
state: session.state,
textBubbleContentFormat: 'richText',
})
} = await continueBotFlow(
message ? { type: 'text', text: message } : undefined,
{
version: 1,
state: session.state,
textBubbleContentFormat: 'richText',
}
)
const allLogs = clientLogs ? [...(logs ?? []), ...clientLogs] : logs

View File

@@ -91,7 +91,9 @@ export const sendMessageV2 = publicProcedure
typeof startParams.typebot === 'string'
? undefined
: startParams.typebot,
message,
message: message
? { type: 'text', text: message }
: undefined,
userId: user?.id,
textBubbleContentFormat: 'richText',
}
@@ -102,10 +104,11 @@ export const sendMessageV2 = publicProcedure
publicId: startParams.typebot,
prefilledVariables: startParams.prefilledVariables,
resultId: startParams.resultId,
message,
message: message
? { type: 'text', text: message }
: undefined,
textBubbleContentFormat: 'richText',
},
message,
})
if (startParams.isPreview || typeof startParams.typebot !== 'string') {
@@ -184,11 +187,14 @@ export const sendMessageV2 = publicProcedure
lastMessageNewFormat,
visitedEdges,
setVariableHistory,
} = await continueBotFlow(message, {
version: 2,
state: session.state,
textBubbleContentFormat: 'richText',
})
} = await continueBotFlow(
message ? { type: 'text', text: message } : undefined,
{
version: 2,
state: session.state,
textBubbleContentFormat: 'richText',
}
)
const allLogs = clientLogs ? [...(logs ?? []), ...clientLogs] : logs

View File

@@ -5,9 +5,14 @@ import { generatePresignedPostPolicy } from '@typebot.io/lib/s3/generatePresigne
import { env } from '@typebot.io/env'
import prisma from '@typebot.io/lib/prisma'
import { getSession } from '@typebot.io/bot-engine/queries/getSession'
import { parseGroups } from '@typebot.io/schemas'
import {
FileInputBlock,
parseGroups,
TextInputBlock,
} from '@typebot.io/schemas'
import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants'
import { getBlockById } from '@typebot.io/schemas/helpers'
import { PublicTypebot } from '@typebot.io/prisma'
export const generateUploadUrl = publicProcedure
.meta({
@@ -50,27 +55,18 @@ export const generateUploadUrl = publicProcedure
const typebotId = session.state.typebotsQueue[0].typebot.id
const publicTypebot = await prisma.publicTypebot.findFirst({
where: {
typebotId,
},
select: {
version: true,
groups: true,
typebot: {
select: {
workspaceId: true,
},
},
},
})
const isPreview = session.state.typebotsQueue[0].resultId
const workspaceId = publicTypebot?.typebot.workspaceId
const typebot = session.state.typebotsQueue[0].resultId
? await getAndParsePublicTypebot(
session.state.typebotsQueue[0].typebot.id
)
: session.state.typebotsQueue[0].typebot
if (!workspaceId)
if (!typebot?.version)
throw new TRPCError({
code: 'BAD_REQUEST',
message: "Can't find workspaceId",
message: "Can't find typebot",
})
if (session.state.currentBlockId === undefined)
@@ -79,42 +75,93 @@ export const generateUploadUrl = publicProcedure
message: "Can't find currentBlockId in session state",
})
const { block: fileUploadBlock } = getBlockById(
const { block } = getBlockById(
session.state.currentBlockId,
parseGroups(publicTypebot.groups, {
typebotVersion: publicTypebot.version,
parseGroups(typebot.groups, {
typebotVersion: typebot.version,
})
)
if (fileUploadBlock?.type !== InputBlockType.FILE)
if (
block?.type !== InputBlockType.FILE &&
(block.type !== InputBlockType.TEXT ||
!block.options?.attachments?.isEnabled)
)
throw new TRPCError({
code: 'BAD_REQUEST',
message: "Can't find file upload block",
message: 'Current block does not expect file upload',
})
const { visibility, maxFileSize } = parseFileUploadParams(block)
const resultId = session.state.typebotsQueue[0].resultId
const filePath = `${
fileUploadBlock.options?.visibility === 'Private' ? 'private' : 'public'
}/workspaces/${workspaceId}/typebots/${typebotId}/results/${resultId}/${fileName}`
const filePath =
'workspaceId' in typebot && typebot.workspaceId
? `${visibility === 'Private' ? 'private' : 'public'}/workspaces/${
typebot.workspaceId
}/typebots/${typebotId}/results/${resultId}/${fileName}`
: `/public/tmp/${typebotId}/${fileName}`
const presignedPostPolicy = await generatePresignedPostPolicy({
fileType,
filePath,
maxFileSize:
fileUploadBlock.options && 'sizeLimit' in fileUploadBlock.options
? (fileUploadBlock.options.sizeLimit as number)
: env.NEXT_PUBLIC_BOT_FILE_UPLOAD_MAX_SIZE,
maxFileSize,
})
return {
presignedUrl: presignedPostPolicy.postURL,
formData: presignedPostPolicy.formData,
fileUrl:
fileUploadBlock.options?.visibility === 'Private'
visibility === 'Private' && !isPreview
? `${env.NEXTAUTH_URL}/api/typebots/${typebotId}/results/${resultId}/${fileName}`
: env.S3_PUBLIC_CUSTOM_DOMAIN
? `${env.S3_PUBLIC_CUSTOM_DOMAIN}/${filePath}`
: `${presignedPostPolicy.postURL}/${presignedPostPolicy.formData.key}`,
}
})
const getAndParsePublicTypebot = async (typebotId: string) => {
const publicTypebot = (await prisma.publicTypebot.findFirst({
where: {
typebotId,
},
select: {
version: true,
groups: true,
typebot: {
select: {
workspaceId: true,
},
},
},
})) as (PublicTypebot & { typebot: { workspaceId: string } }) | null
return {
...publicTypebot,
workspaceId: publicTypebot?.typebot.workspaceId,
}
}
const parseFileUploadParams = (
block: FileInputBlock | TextInputBlock
): { visibility: 'Public' | 'Private'; maxFileSize: number | undefined } => {
if (block.type === InputBlockType.FILE) {
return {
visibility:
block.options?.visibility === 'Private' ? 'Private' : 'Public',
maxFileSize:
block.options && 'sizeLimit' in block.options
? (block.options.sizeLimit as number)
: env.NEXT_PUBLIC_BOT_FILE_UPLOAD_MAX_SIZE,
}
}
return {
visibility:
block.options?.attachments?.visibility === 'Private'
? 'Private'
: 'Public',
maxFileSize: env.NEXT_PUBLIC_BOT_FILE_UPLOAD_MAX_SIZE,
}
}

View File

@@ -13,7 +13,7 @@ import { StartChatInput, StartPreviewChatInput } from '@typebot.io/schemas'
test.afterEach(async () => {
await deleteWebhooks(['chat-webhook-id'])
await deleteTypebots(['chat-sub-bot'])
await deleteTypebots(['chat-sub-bot', 'starting-with-input'])
})
test('API chat execution should work on preview bot', async ({ request }) => {
@@ -108,6 +108,13 @@ test('API chat execution should work on published bot', async ({ request }) => {
id: 'chat-sub-bot',
publicId: 'chat-sub-bot-public',
})
await importTypebotInDatabase(
getTestAsset('typebots/chat/startingWithInput.json'),
{
id: 'starting-with-input',
publicId: 'starting-with-input-public',
}
)
await createWebhook(typebotId, {
id: 'chat-webhook-id',
method: HttpMethod.GET,
@@ -296,11 +303,12 @@ test('API chat execution should work on published bot', async ({ request }) => {
expect(messages[2].content.richText.length).toBeGreaterThan(0)
})
await test.step('Starting with a message when typebot starts with input should proceed', async () => {
const { messages } = await (
const response = await (
await request.post(
`/api/v1/typebots/starting-with-input-public/startChat`,
{
data: {
//@ts-expect-error We want to test if message is correctly preprocessed by zod
message: 'Hey',
isStreamEnabled: false,
isOnlyRegistering: false,
@@ -309,7 +317,7 @@ test('API chat execution should work on published bot', async ({ request }) => {
}
)
).json()
expect(messages[0].content.richText).toStrictEqual([
expect(response.messages[0].content.richText).toStrictEqual([
{
children: [
{

View File

@@ -14,11 +14,11 @@ test('Transcript set variable should be correctly computed', async ({
await page.goto(`/${typebotId}-public`)
await page.getByPlaceholder('Type your answer...').fill('hey')
await page.getByRole('button').click()
await page.getByLabel('Send').click()
await page.getByPlaceholder('Type your answer...').fill('hey 2')
await page.getByRole('button').click()
await page.getByLabel('Send').click()
await page.getByPlaceholder('Type your answer...').fill('hey 3')
await page.getByRole('button').click()
await page.getByLabel('Send').click()
await expect(
page.getByText('Assistant: "How are you? You said "')
).toBeVisible()

View File

@@ -31,8 +31,8 @@ test.beforeAll(async () => {
test('should work as expected', async ({ page }) => {
await page.goto(`/${typebotId}-public`)
await page.locator('input').fill('Hello there!')
await page.locator('input').press('Enter')
await page.getByPlaceholder('Type your answer...').fill('Hello there!')
await page.getByPlaceholder('Type your answer...').press('Enter')
await expect(page.getByText('Cheers!')).toBeVisible()
await page.goto(`${env.NEXTAUTH_URL}/typebots/${typebotId}/results`)
await expect(page.locator('text=Hello there!')).toBeVisible()
@@ -41,8 +41,8 @@ test('should work as expected', async ({ page }) => {
test.describe('Merge disabled', () => {
test('should work as expected', async ({ page }) => {
await page.goto(`/${typebotWithMergeDisabledId}-public`)
await page.locator('input').fill('Hello there!')
await page.locator('input').press('Enter')
await page.getByPlaceholder('Type your answer...').fill('Hello there!')
await page.getByPlaceholder('Type your answer...').press('Enter')
await expect(page.getByText('Cheers!')).toBeVisible()
await page.goto(
`${process.env.NEXTAUTH_URL}/typebots/${typebotWithMergeDisabledId}/results`