434 lines
13 KiB
TypeScript
434 lines
13 KiB
TypeScript
import {
|
|
Answer,
|
|
ContinueChatResponse,
|
|
Edge,
|
|
Group,
|
|
InputBlock,
|
|
TypebotInSession,
|
|
Variable,
|
|
} from '@typebot.io/schemas'
|
|
import { SetVariableHistoryItem } from '@typebot.io/schemas/features/result'
|
|
import { isBubbleBlock, isInputBlock } from '@typebot.io/schemas/helpers'
|
|
import { BubbleBlockType } from '@typebot.io/schemas/features/blocks/bubbles/constants'
|
|
import { LogicBlockType } from '@typebot.io/schemas/features/blocks/logic/constants'
|
|
import { createId } from '@typebot.io/lib/createId'
|
|
import { executeCondition } from './executeCondition'
|
|
import {
|
|
parseBubbleBlock,
|
|
BubbleBlockWithDefinedContent,
|
|
} from '../bot-engine/parseBubbleBlock'
|
|
import { defaultChoiceInputOptions } from '@typebot.io/schemas/features/blocks/inputs/choice/constants'
|
|
import { defaultPictureChoiceOptions } from '@typebot.io/schemas/features/blocks/inputs/pictureChoice/constants'
|
|
import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants'
|
|
import { parseVariables } from '@typebot.io/variables/parseVariables'
|
|
|
|
type TranscriptMessage =
|
|
| {
|
|
role: 'bot' | 'user'
|
|
} & (
|
|
| { type: 'text'; text: string }
|
|
| { type: 'image'; image: string }
|
|
| { type: 'video'; video: string }
|
|
| { type: 'audio'; audio: string }
|
|
)
|
|
|
|
export const parseTranscriptMessageText = (
|
|
message: TranscriptMessage
|
|
): string => {
|
|
switch (message.type) {
|
|
case 'text':
|
|
return message.text
|
|
case 'image':
|
|
return message.image
|
|
case 'video':
|
|
return message.video
|
|
case 'audio':
|
|
return message.audio
|
|
}
|
|
}
|
|
|
|
export const computeResultTranscript = ({
|
|
typebot,
|
|
answers,
|
|
setVariableHistory,
|
|
visitedEdges,
|
|
stopAtBlockId,
|
|
}: {
|
|
typebot: TypebotInSession
|
|
answers: Pick<Answer, 'blockId' | 'content' | 'attachedFileUrls'>[]
|
|
setVariableHistory: Pick<
|
|
SetVariableHistoryItem,
|
|
'blockId' | 'variableId' | 'value'
|
|
>[]
|
|
visitedEdges: string[]
|
|
stopAtBlockId?: string
|
|
}): TranscriptMessage[] => {
|
|
const firstEdgeId = getFirstEdgeId(typebot)
|
|
if (!firstEdgeId) return []
|
|
const firstEdge = typebot.edges.find((edge) => edge.id === firstEdgeId)
|
|
if (!firstEdge) return []
|
|
const firstGroup = getNextGroup(typebot, firstEdgeId)
|
|
if (!firstGroup) return []
|
|
return executeGroup({
|
|
typebotsQueue: [{ typebot }],
|
|
nextGroup: firstGroup,
|
|
currentTranscript: [],
|
|
answers,
|
|
setVariableHistory,
|
|
visitedEdges,
|
|
stopAtBlockId,
|
|
})
|
|
}
|
|
|
|
const getFirstEdgeId = (typebot: TypebotInSession) => {
|
|
if (typebot.version === '6') return typebot.events?.[0].outgoingEdgeId
|
|
return typebot.groups.at(0)?.blocks.at(0)?.outgoingEdgeId
|
|
}
|
|
|
|
const getNextGroup = (
|
|
typebot: TypebotInSession,
|
|
edgeId: string
|
|
): { group: Group; blockIndex?: number } | undefined => {
|
|
const edge = typebot.edges.find((edge) => edge.id === edgeId)
|
|
if (!edge) return
|
|
const group = typebot.groups.find((group) => group.id === edge.to.groupId)
|
|
if (!group) return
|
|
const blockIndex = edge.to.blockId
|
|
? group.blocks.findIndex((block) => block.id === edge.to.blockId)
|
|
: undefined
|
|
return { group, blockIndex }
|
|
}
|
|
|
|
const executeGroup = ({
|
|
currentTranscript,
|
|
typebotsQueue,
|
|
answers,
|
|
nextGroup,
|
|
setVariableHistory,
|
|
visitedEdges,
|
|
stopAtBlockId,
|
|
}: {
|
|
currentTranscript: TranscriptMessage[]
|
|
nextGroup:
|
|
| {
|
|
group: Group
|
|
blockIndex?: number | undefined
|
|
}
|
|
| undefined
|
|
typebotsQueue: {
|
|
typebot: TypebotInSession
|
|
resumeEdgeId?: string
|
|
}[]
|
|
answers: Pick<Answer, 'blockId' | 'content' | 'attachedFileUrls'>[]
|
|
setVariableHistory: Pick<
|
|
SetVariableHistoryItem,
|
|
'blockId' | 'variableId' | 'value'
|
|
>[]
|
|
visitedEdges: string[]
|
|
stopAtBlockId?: string
|
|
}): TranscriptMessage[] => {
|
|
if (!nextGroup) return currentTranscript
|
|
for (const block of nextGroup?.group.blocks.slice(
|
|
nextGroup.blockIndex ?? 0
|
|
)) {
|
|
if (stopAtBlockId && block.id === stopAtBlockId) return currentTranscript
|
|
if (setVariableHistory.at(0)?.blockId === block.id)
|
|
typebotsQueue[0].typebot.variables = applySetVariable(
|
|
setVariableHistory.shift(),
|
|
typebotsQueue[0].typebot
|
|
)
|
|
let nextEdgeId = block.outgoingEdgeId
|
|
if (isBubbleBlock(block)) {
|
|
if (!block.content) continue
|
|
const parsedBubbleBlock = parseBubbleBlock(
|
|
block as BubbleBlockWithDefinedContent,
|
|
{
|
|
version: 2,
|
|
variables: typebotsQueue[0].typebot.variables,
|
|
typebotVersion: typebotsQueue[0].typebot.version,
|
|
textBubbleContentFormat: 'markdown',
|
|
}
|
|
)
|
|
const newMessage =
|
|
convertChatMessageToTranscriptMessage(parsedBubbleBlock)
|
|
if (newMessage) currentTranscript.push(newMessage)
|
|
} else if (isInputBlock(block)) {
|
|
const answer = answers.shift()
|
|
if (!answer) break
|
|
if (block.options?.variableId) {
|
|
const replyVariable = typebotsQueue[0].typebot.variables.find(
|
|
(variable) => variable.id === block.options?.variableId
|
|
)
|
|
if (replyVariable) {
|
|
typebotsQueue[0].typebot.variables =
|
|
typebotsQueue[0].typebot.variables.map((v) =>
|
|
v.id === replyVariable.id ? { ...v, value: answer.content } : v
|
|
)
|
|
}
|
|
}
|
|
if (
|
|
block.type === InputBlockType.TEXT &&
|
|
block.options?.attachments?.isEnabled &&
|
|
block.options?.attachments?.saveVariableId &&
|
|
answer.attachedFileUrls &&
|
|
answer.attachedFileUrls?.length > 0
|
|
) {
|
|
const variable = typebotsQueue[0].typebot.variables.find(
|
|
(variable) =>
|
|
variable.id === block.options?.attachments?.saveVariableId
|
|
)
|
|
if (variable) {
|
|
typebotsQueue[0].typebot.variables =
|
|
typebotsQueue[0].typebot.variables.map((v) =>
|
|
v.id === variable.id
|
|
? {
|
|
...v,
|
|
value: Array.isArray(variable.value)
|
|
? variable.value.concat(answer.attachedFileUrls!)
|
|
: answer.attachedFileUrls!.length === 1
|
|
? answer.attachedFileUrls![0]
|
|
: answer.attachedFileUrls,
|
|
}
|
|
: v
|
|
)
|
|
}
|
|
}
|
|
currentTranscript.push({
|
|
role: 'user',
|
|
type: 'text',
|
|
text:
|
|
(answer.attachedFileUrls?.length ?? 0) > 0
|
|
? `${answer.attachedFileUrls?.join(', ')}\n\n${answer.content}`
|
|
: answer.content,
|
|
})
|
|
const outgoingEdge = getOutgoingEdgeId({
|
|
block,
|
|
answer: answer.content,
|
|
variables: typebotsQueue[0].typebot.variables,
|
|
})
|
|
if (outgoingEdge.isOffDefaultPath) visitedEdges.shift()
|
|
nextEdgeId = outgoingEdge.edgeId
|
|
} else if (block.type === LogicBlockType.CONDITION) {
|
|
const passedCondition = block.items.find(
|
|
(item) =>
|
|
item.content &&
|
|
executeCondition({
|
|
variables: typebotsQueue[0].typebot.variables,
|
|
condition: item.content,
|
|
})
|
|
)
|
|
if (passedCondition) {
|
|
visitedEdges.shift()
|
|
nextEdgeId = passedCondition.outgoingEdgeId
|
|
}
|
|
} else if (block.type === LogicBlockType.AB_TEST) {
|
|
nextEdgeId = visitedEdges.shift() ?? nextEdgeId
|
|
} else if (block.type === LogicBlockType.JUMP) {
|
|
if (!block.options?.groupId) continue
|
|
const groupToJumpTo = typebotsQueue[0].typebot.groups.find(
|
|
(group) => group.id === block.options?.groupId
|
|
)
|
|
const blockToJumpTo =
|
|
groupToJumpTo?.blocks.find((b) => b.id === block.options?.blockId) ??
|
|
groupToJumpTo?.blocks[0]
|
|
|
|
if (!blockToJumpTo) continue
|
|
|
|
const portalEdge = {
|
|
id: createId(),
|
|
from: { blockId: '', groupId: '' },
|
|
to: { groupId: block.options.groupId, blockId: blockToJumpTo.id },
|
|
}
|
|
typebotsQueue[0].typebot.edges.push(portalEdge)
|
|
visitedEdges.shift()
|
|
nextEdgeId = portalEdge.id
|
|
} else if (block.type === LogicBlockType.TYPEBOT_LINK) {
|
|
const isLinkingSameTypebot =
|
|
block.options &&
|
|
(block.options.typebotId === 'current' ||
|
|
block.options.typebotId === typebotsQueue[0].typebot.id)
|
|
|
|
const linkedGroup = typebotsQueue[0].typebot.groups.find(
|
|
(g) => g.id === block.options?.groupId
|
|
)
|
|
if (!isLinkingSameTypebot || !linkedGroup) continue
|
|
let resumeEdge: Edge | undefined
|
|
if (!block.outgoingEdgeId) {
|
|
const currentBlockIndex = nextGroup.group.blocks.findIndex(
|
|
(b) => b.id === block.id
|
|
)
|
|
const nextBlockInGroup =
|
|
currentBlockIndex === -1
|
|
? undefined
|
|
: nextGroup.group.blocks.at(currentBlockIndex + 1)
|
|
if (nextBlockInGroup)
|
|
resumeEdge = {
|
|
id: createId(),
|
|
from: {
|
|
blockId: '',
|
|
},
|
|
to: {
|
|
groupId: nextGroup.group.id,
|
|
blockId: nextBlockInGroup.id,
|
|
},
|
|
}
|
|
}
|
|
return executeGroup({
|
|
typebotsQueue: [
|
|
{
|
|
typebot: typebotsQueue[0].typebot,
|
|
resumeEdgeId: resumeEdge ? resumeEdge.id : block.outgoingEdgeId,
|
|
},
|
|
{
|
|
typebot: resumeEdge
|
|
? {
|
|
...typebotsQueue[0].typebot,
|
|
edges: typebotsQueue[0].typebot.edges.concat([resumeEdge]),
|
|
}
|
|
: typebotsQueue[0].typebot,
|
|
},
|
|
],
|
|
answers,
|
|
setVariableHistory,
|
|
currentTranscript,
|
|
nextGroup: {
|
|
group: linkedGroup,
|
|
},
|
|
visitedEdges,
|
|
stopAtBlockId,
|
|
})
|
|
}
|
|
if (nextEdgeId) {
|
|
const nextGroup = getNextGroup(typebotsQueue[0].typebot, nextEdgeId)
|
|
if (nextGroup) {
|
|
return executeGroup({
|
|
typebotsQueue,
|
|
answers,
|
|
setVariableHistory,
|
|
currentTranscript,
|
|
nextGroup,
|
|
visitedEdges,
|
|
stopAtBlockId,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
if (typebotsQueue.length > 1 && typebotsQueue[0].resumeEdgeId) {
|
|
return executeGroup({
|
|
typebotsQueue: typebotsQueue.slice(1),
|
|
answers,
|
|
setVariableHistory,
|
|
currentTranscript,
|
|
nextGroup: getNextGroup(
|
|
typebotsQueue[1].typebot,
|
|
typebotsQueue[0].resumeEdgeId
|
|
),
|
|
visitedEdges: visitedEdges.slice(1),
|
|
stopAtBlockId,
|
|
})
|
|
}
|
|
return currentTranscript
|
|
}
|
|
|
|
const applySetVariable = (
|
|
setVariable:
|
|
| Pick<SetVariableHistoryItem, 'blockId' | 'variableId' | 'value'>
|
|
| undefined,
|
|
typebot: TypebotInSession
|
|
): Variable[] => {
|
|
if (!setVariable) return typebot.variables
|
|
const variable = typebot.variables.find(
|
|
(variable) => variable.id === setVariable.variableId
|
|
)
|
|
if (!variable) return typebot.variables
|
|
return typebot.variables.map((v) =>
|
|
v.id === variable.id ? { ...v, value: setVariable.value } : v
|
|
)
|
|
}
|
|
|
|
const convertChatMessageToTranscriptMessage = (
|
|
chatMessage: ContinueChatResponse['messages'][0]
|
|
): TranscriptMessage | null => {
|
|
switch (chatMessage.type) {
|
|
case BubbleBlockType.TEXT: {
|
|
if (chatMessage.content.type === 'richText') return null
|
|
return {
|
|
role: 'bot',
|
|
type: 'text',
|
|
text: chatMessage.content.markdown,
|
|
}
|
|
}
|
|
case BubbleBlockType.IMAGE: {
|
|
if (!chatMessage.content.url) return null
|
|
return {
|
|
role: 'bot',
|
|
type: 'image',
|
|
image: chatMessage.content.url,
|
|
}
|
|
}
|
|
case BubbleBlockType.VIDEO: {
|
|
if (!chatMessage.content.url) return null
|
|
return {
|
|
role: 'bot',
|
|
type: 'video',
|
|
video: chatMessage.content.url,
|
|
}
|
|
}
|
|
case BubbleBlockType.AUDIO: {
|
|
if (!chatMessage.content.url) return null
|
|
return {
|
|
role: 'bot',
|
|
type: 'audio',
|
|
audio: chatMessage.content.url,
|
|
}
|
|
}
|
|
case 'custom-embed':
|
|
case BubbleBlockType.EMBED: {
|
|
return null
|
|
}
|
|
}
|
|
}
|
|
|
|
const getOutgoingEdgeId = ({
|
|
block,
|
|
answer,
|
|
variables,
|
|
}: {
|
|
block: InputBlock
|
|
answer: string | undefined
|
|
variables: Variable[]
|
|
}): { edgeId: string | undefined; isOffDefaultPath: boolean } => {
|
|
if (
|
|
block.type === InputBlockType.CHOICE &&
|
|
!(
|
|
block.options?.isMultipleChoice ??
|
|
defaultChoiceInputOptions.isMultipleChoice
|
|
) &&
|
|
answer
|
|
) {
|
|
const matchedItem = block.items.find(
|
|
(item) =>
|
|
parseVariables(variables)(item.content).normalize() ===
|
|
answer.normalize()
|
|
)
|
|
if (matchedItem?.outgoingEdgeId)
|
|
return { edgeId: matchedItem.outgoingEdgeId, isOffDefaultPath: true }
|
|
}
|
|
if (
|
|
block.type === InputBlockType.PICTURE_CHOICE &&
|
|
!(
|
|
block.options?.isMultipleChoice ??
|
|
defaultPictureChoiceOptions.isMultipleChoice
|
|
) &&
|
|
answer
|
|
) {
|
|
const matchedItem = block.items.find(
|
|
(item) =>
|
|
parseVariables(variables)(item.title).normalize() === answer.normalize()
|
|
)
|
|
if (matchedItem?.outgoingEdgeId)
|
|
return { edgeId: matchedItem.outgoingEdgeId, isOffDefaultPath: true }
|
|
}
|
|
return { edgeId: block.outgoingEdgeId, isOffDefaultPath: false }
|
|
}
|