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

@ -4,11 +4,12 @@ import {
ContinueChatResponse,
Group,
InputBlock,
Message,
SessionState,
SetVariableHistoryItem,
Variable,
} from '@typebot.io/schemas'
import { byId } from '@typebot.io/lib'
import { byId, isDefined } from '@typebot.io/lib'
import { isInputBlock } from '@typebot.io/schemas/helpers'
import { executeGroup, parseInput } from './executeGroup'
import { getNextGroup } from './getNextGroup'
@ -42,10 +43,9 @@ import { ForgedBlock } from '@typebot.io/forge-repository/types'
import { forgedBlocks } from '@typebot.io/forge-repository/definitions'
import { resumeChatCompletion } from './blocks/integrations/legacy/openai/resumeChatCompletion'
import { env } from '@typebot.io/env'
import { downloadMedia } from './whatsapp/downloadMedia'
import { uploadFileToBucket } from '@typebot.io/lib/s3/uploadFileToBucket'
import { isURL } from '@typebot.io/lib/validators/isURL'
import { isForgedBlockType } from '@typebot.io/schemas/features/blocks/forged/helpers'
import { resetSessionState } from './resetSessionState'
type Params = {
version: 1 | 2
@ -69,7 +69,11 @@ export const continueBotFlow = async (
const setVariableHistory: SetVariableHistoryItem[] = []
if (!newSessionState.currentBlockId)
return startBotFlow({ state, version, textBubbleContentFormat })
return startBotFlow({
state: resetSessionState(newSessionState),
version,
textBubbleContentFormat,
})
const { block, group, blockIndex } = getBlockById(
newSessionState.currentBlockId,
@ -161,7 +165,7 @@ export const continueBotFlow = async (
newVariables: [
{
...variableToUpdate,
value: safeJsonParse(reply as string),
value: reply?.text ? safeJsonParse(reply?.text) : undefined,
},
],
})
@ -187,7 +191,14 @@ export const continueBotFlow = async (
formattedReply =
'reply' in parsedReplyResult ? parsedReplyResult.reply : undefined
newSessionState = await processAndSaveAnswer(state, block)(formattedReply)
newSessionState = await processAndSaveAnswer(
state,
block
)(
isDefined(formattedReply)
? { ...reply, type: 'text', text: formattedReply }
: undefined
)
}
const groupHasMoreBlocks = blockIndex < group.blocks.length - 1
@ -267,37 +278,92 @@ export const continueBotFlow = async (
const processAndSaveAnswer =
(state: SessionState, block: InputBlock) =>
async (reply: string | undefined): Promise<SessionState> => {
async (reply: Message | undefined): Promise<SessionState> => {
if (!reply) return state
let newState = await saveAnswerInDb(state, block)(reply)
return newState
return saveAnswerInDb(state, block)(reply)
}
const saveVariableValueIfAny =
const saveVariablesValueIfAny =
(state: SessionState, block: InputBlock) =>
(reply: string): SessionState => {
(reply: Message): SessionState => {
if (!block.options?.variableId) return state
const foundVariable = state.typebotsQueue[0].typebot.variables.find(
(variable) => variable.id === block.options?.variableId
)
if (!foundVariable) return state
const { updatedState } = updateVariablesInSession({
newVariables: [
{
...foundVariable,
value: Array.isArray(foundVariable.value)
? foundVariable.value.concat(reply)
: reply,
},
],
currentBlockId: undefined,
state,
})
return updatedState
const newSessionState = saveAttachmentsVarIfAny({ block, reply, state })
return saveInputVarIfAny({ block, reply, state: newSessionState })
}
const saveAttachmentsVarIfAny = ({
block,
reply,
state,
}: {
block: InputBlock
reply: Message
state: SessionState
}): SessionState => {
if (
block.type !== InputBlockType.TEXT ||
!block.options?.attachments?.isEnabled ||
!block.options?.attachments?.saveVariableId ||
!reply.attachedFileUrls ||
reply.attachedFileUrls.length === 0
)
return state
const variable = state.typebotsQueue[0].typebot.variables.find(
(variable) => variable.id === block.options?.attachments?.saveVariableId
)
if (!variable) return state
const { updatedState } = updateVariablesInSession({
newVariables: [
{
id: variable.id,
name: variable.name,
value: Array.isArray(variable.value)
? variable.value.concat(reply.attachedFileUrls)
: reply.attachedFileUrls.length === 1
? reply.attachedFileUrls[0]
: reply.attachedFileUrls,
},
],
currentBlockId: undefined,
state,
})
return updatedState
}
const saveInputVarIfAny = ({
block,
reply,
state,
}: {
block: InputBlock
reply: Message
state: SessionState
}): SessionState => {
const foundVariable = state.typebotsQueue[0].typebot.variables.find(
(variable) => variable.id === block.options?.variableId
)
if (!foundVariable) return state
const { updatedState } = updateVariablesInSession({
newVariables: [
{
...foundVariable,
value:
Array.isArray(foundVariable.value) && reply.text
? foundVariable.value.concat(reply.text)
: reply.text,
},
],
currentBlockId: undefined,
state,
})
return updatedState
}
const parseRetryMessage =
(state: SessionState) =>
async (
@ -346,26 +412,27 @@ const parseDefaultRetryMessage = (block: InputBlock): string => {
const saveAnswerInDb =
(state: SessionState, block: InputBlock) =>
async (reply: string): Promise<SessionState> => {
async (reply: Message): Promise<SessionState> => {
let newSessionState = state
await saveAnswer({
answer: {
blockId: block.id,
content: reply,
content: reply.text,
attachedFileUrls: reply.attachedFileUrls,
},
reply,
state,
})
newSessionState = {
...saveVariableValueIfAny(newSessionState, block)(reply),
...saveVariablesValueIfAny(newSessionState, block)(reply),
previewMetadata: state.typebotsQueue[0].resultId
? newSessionState.previewMetadata
: {
...newSessionState.previewMetadata,
answers: (newSessionState.previewMetadata?.answers ?? []).concat({
blockId: block.id,
content: reply,
content: reply.text,
attachedFileUrls: reply.attachedFileUrls,
}),
},
}
@ -378,7 +445,10 @@ const saveAnswerInDb =
return setNewAnswerInState(newSessionState)({
key: key ?? block.id,
value: reply,
value:
(reply.attachedFileUrls ?? []).length > 0
? `${reply.attachedFileUrls!.join(', ')}\n\n${reply.text}`
: reply.text,
})
}
@ -465,41 +535,17 @@ const getOutgoingEdgeId =
const parseReply =
(state: SessionState) =>
async (reply: Reply, block: InputBlock): Promise<ParsedReply> => {
if (reply && typeof reply !== 'string') {
if (block.type !== InputBlockType.FILE) return { status: 'fail' }
if (block.options?.visibility !== 'Public') {
return {
status: 'success',
reply:
env.NEXTAUTH_URL +
`/api/typebots/${state.typebotsQueue[0].typebot.id}/whatsapp/media/${reply.mediaId}`,
}
}
const { file, mimeType } = await downloadMedia({
mediaId: reply.mediaId,
systemUserAccessToken: reply.accessToken,
})
const url = await uploadFileToBucket({
file,
key: `public/workspaces/${reply.workspaceId}/typebots/${state.typebotsQueue[0].typebot.id}/results/${state.typebotsQueue[0].resultId}/${reply.mediaId}`,
mimeType,
})
return {
status: 'success',
reply: url,
}
}
switch (block.type) {
case InputBlockType.EMAIL: {
if (!reply) return { status: 'fail' }
const formattedEmail = formatEmail(reply)
const formattedEmail = formatEmail(reply.text)
if (!formattedEmail) return { status: 'fail' }
return { status: 'success', reply: formattedEmail }
}
case InputBlockType.PHONE: {
if (!reply) return { status: 'fail' }
const formattedPhone = formatPhoneNumber(
reply,
reply.text,
block.options?.defaultCountryCode
)
if (!formattedPhone) return { status: 'fail' }
@ -507,58 +553,60 @@ const parseReply =
}
case InputBlockType.URL: {
if (!reply) return { status: 'fail' }
const isValid = isURL(reply, { require_protocol: false })
const isValid = isURL(reply.text, { require_protocol: false })
if (!isValid) return { status: 'fail' }
return { status: 'success', reply: reply }
return { status: 'success', reply: reply.text }
}
case InputBlockType.CHOICE: {
if (!reply) return { status: 'fail' }
return parseButtonsReply(state)(reply, block)
return parseButtonsReply(state)(reply.text, block)
}
case InputBlockType.NUMBER: {
if (!reply) return { status: 'fail' }
const isValid = validateNumber(reply, {
const isValid = validateNumber(reply.text, {
options: block.options,
variables: state.typebotsQueue[0].typebot.variables,
})
if (!isValid) return { status: 'fail' }
return { status: 'success', reply: parseNumber(reply) }
return { status: 'success', reply: parseNumber(reply.text) }
}
case InputBlockType.DATE: {
if (!reply) return { status: 'fail' }
return parseDateReply(reply, block)
return parseDateReply(reply.text, block)
}
case InputBlockType.FILE: {
if (!reply)
return block.options?.isRequired ?? defaultFileInputOptions.isRequired
? { status: 'fail' }
: { status: 'skip' }
const urls = reply.split(', ')
const urls = reply.text.split(', ')
const status = urls.some((url) =>
isURL(url, { require_tld: env.S3_ENDPOINT !== 'localhost' })
)
? 'success'
: 'fail'
return { status, reply: reply }
if (!block.options?.isMultipleAllowed && urls.length > 1)
return { status, reply: reply.text.split(',')[0] }
return { status, reply: reply.text }
}
case InputBlockType.PAYMENT: {
if (!reply) return { status: 'fail' }
if (reply === 'fail') return { status: 'fail' }
return { status: 'success', reply: reply }
if (reply.text === 'fail') return { status: 'fail' }
return { status: 'success', reply: reply.text }
}
case InputBlockType.RATING: {
if (!reply) return { status: 'fail' }
const isValid = validateRatingReply(reply, block)
const isValid = validateRatingReply(reply.text, block)
if (!isValid) return { status: 'fail' }
return { status: 'success', reply: reply }
return { status: 'success', reply: reply.text }
}
case InputBlockType.PICTURE_CHOICE: {
if (!reply) return { status: 'fail' }
return parsePictureChoicesReply(state)(reply, block)
return parsePictureChoicesReply(state)(reply.text, block)
}
case InputBlockType.TEXT: {
if (!reply) return { status: 'fail' }
return { status: 'success', reply: reply }
return { status: 'success', reply: reply.text }
}
}
}