@ -20,7 +20,7 @@ export const addEdgeToTypebot = (
|
||||
})
|
||||
|
||||
export const createPortalEdge = ({ to }: Pick<Edge, 'to'>) => ({
|
||||
id: createId(),
|
||||
id: 'virtual-' + createId(),
|
||||
from: { blockId: '', groupId: '' },
|
||||
to,
|
||||
})
|
||||
|
@ -52,6 +52,7 @@ export const continueChat = async ({ origin, sessionId, message }: Props) => {
|
||||
logs,
|
||||
lastMessageNewFormat,
|
||||
visitedEdges,
|
||||
setVariableHistory,
|
||||
} = await continueBotFlow(message, {
|
||||
version: 2,
|
||||
state: session.state,
|
||||
@ -68,6 +69,7 @@ export const continueChat = async ({ origin, sessionId, message }: Props) => {
|
||||
logs,
|
||||
clientSideActions,
|
||||
visitedEdges,
|
||||
setVariableHistory,
|
||||
hasCustomEmbedBubble: messages.some(
|
||||
(message) => message.type === 'custom-embed'
|
||||
),
|
||||
|
@ -16,6 +16,7 @@ import { isForgedBlockType } from '@typebot.io/schemas/features/blocks/forged/he
|
||||
import { updateVariablesInSession } from '@typebot.io/variables/updateVariablesInSession'
|
||||
import { updateSession } from '../queries/updateSession'
|
||||
import { deepParseVariables } from '@typebot.io/variables/deepParseVariables'
|
||||
import { saveSetVariableHistoryItems } from '../queries/saveSetVariableHistoryItems'
|
||||
|
||||
type Props = {
|
||||
sessionId: string
|
||||
@ -114,11 +115,17 @@ export const getMessageStream = async ({ sessionId, messages }: Props) => {
|
||||
(variable) => variable.id === id
|
||||
)
|
||||
if (!variable) return
|
||||
const { updatedState, newSetVariableHistory } =
|
||||
updateVariablesInSession({
|
||||
newVariables: [{ ...variable, value }],
|
||||
state: session.state,
|
||||
currentBlockId: session.state.currentBlockId,
|
||||
})
|
||||
if (newSetVariableHistory.length > 0)
|
||||
await saveSetVariableHistoryItems(newSetVariableHistory)
|
||||
await updateSession({
|
||||
id: session.id,
|
||||
state: updateVariablesInSession(session.state)([
|
||||
{ ...variable, value },
|
||||
]),
|
||||
state: updatedState,
|
||||
isReplying: undefined,
|
||||
})
|
||||
},
|
||||
|
@ -33,6 +33,7 @@ export const startChat = async ({
|
||||
clientSideActions,
|
||||
newSessionState,
|
||||
visitedEdges,
|
||||
setVariableHistory,
|
||||
} = await startSession({
|
||||
version: 2,
|
||||
startParams: {
|
||||
@ -69,6 +70,7 @@ export const startChat = async ({
|
||||
logs,
|
||||
clientSideActions,
|
||||
visitedEdges,
|
||||
setVariableHistory,
|
||||
hasCustomEmbedBubble: messages.some(
|
||||
(message) => message.type === 'custom-embed'
|
||||
),
|
||||
|
@ -13,6 +13,7 @@ type Props = {
|
||||
typebot?: StartTypebot
|
||||
userId?: string
|
||||
prefilledVariables?: Record<string, unknown>
|
||||
sessionId?: string
|
||||
}
|
||||
|
||||
export const startChatPreview = async ({
|
||||
@ -24,6 +25,7 @@ export const startChatPreview = async ({
|
||||
typebot: startTypebot,
|
||||
userId,
|
||||
prefilledVariables,
|
||||
sessionId,
|
||||
}: Props) => {
|
||||
const {
|
||||
typebot,
|
||||
@ -34,6 +36,7 @@ export const startChatPreview = async ({
|
||||
clientSideActions,
|
||||
newSessionState,
|
||||
visitedEdges,
|
||||
setVariableHistory,
|
||||
} = await startSession({
|
||||
version: 2,
|
||||
startParams: {
|
||||
@ -45,6 +48,7 @@ export const startChatPreview = async ({
|
||||
typebot: startTypebot,
|
||||
userId,
|
||||
prefilledVariables,
|
||||
sessionId,
|
||||
},
|
||||
message,
|
||||
})
|
||||
@ -61,9 +65,11 @@ export const startChatPreview = async ({
|
||||
logs,
|
||||
clientSideActions,
|
||||
visitedEdges,
|
||||
setVariableHistory,
|
||||
hasCustomEmbedBubble: messages.some(
|
||||
(message) => message.type === 'custom-embed'
|
||||
),
|
||||
initialSessionId: sessionId,
|
||||
})
|
||||
|
||||
const isEnded =
|
||||
|
@ -1,12 +1,15 @@
|
||||
import { executeCondition } from '@typebot.io/logic/executeCondition'
|
||||
import { ChoiceInputBlock, Variable } from '@typebot.io/schemas'
|
||||
import { executeCondition } from '../../logic/condition/executeCondition'
|
||||
|
||||
export const filterChoiceItems =
|
||||
(variables: Variable[]) =>
|
||||
(block: ChoiceInputBlock): ChoiceInputBlock => {
|
||||
const filteredItems = block.items.filter((item) => {
|
||||
if (item.displayCondition?.isEnabled && item.displayCondition?.condition)
|
||||
return executeCondition(variables)(item.displayCondition.condition)
|
||||
return executeCondition({
|
||||
variables,
|
||||
condition: item.displayCondition.condition,
|
||||
})
|
||||
|
||||
return true
|
||||
})
|
||||
|
@ -41,7 +41,6 @@ const getVariableValue =
|
||||
const [transformedVariable] = transformVariablesToList(variables)([
|
||||
variable.id,
|
||||
])
|
||||
updateVariablesInSession(state)([transformedVariable])
|
||||
return transformedVariable.value as string[]
|
||||
}
|
||||
return variable.value
|
||||
|
@ -1,12 +1,15 @@
|
||||
import { executeCondition } from '@typebot.io/logic/executeCondition'
|
||||
import { PictureChoiceBlock, Variable } from '@typebot.io/schemas'
|
||||
import { executeCondition } from '../../logic/condition/executeCondition'
|
||||
|
||||
export const filterPictureChoiceItems =
|
||||
(variables: Variable[]) =>
|
||||
(block: PictureChoiceBlock): PictureChoiceBlock => {
|
||||
const filteredItems = block.items.filter((item) => {
|
||||
if (item.displayCondition?.isEnabled && item.displayCondition?.condition)
|
||||
return executeCondition(variables)(item.displayCondition.condition)
|
||||
return executeCondition({
|
||||
variables,
|
||||
condition: item.displayCondition.condition,
|
||||
})
|
||||
|
||||
return true
|
||||
})
|
||||
|
@ -25,6 +25,7 @@ export const executeGoogleSheetBlock = async (
|
||||
})
|
||||
case GoogleSheetsAction.GET:
|
||||
return getRow(state, {
|
||||
blockId: block.id,
|
||||
options: block.options,
|
||||
outgoingEdgeId: block.outgoingEdgeId,
|
||||
})
|
||||
|
@ -14,9 +14,14 @@ import { updateVariablesInSession } from '@typebot.io/variables/updateVariablesI
|
||||
export const getRow = async (
|
||||
state: SessionState,
|
||||
{
|
||||
blockId,
|
||||
outgoingEdgeId,
|
||||
options,
|
||||
}: { outgoingEdgeId?: string; options: GoogleSheetsGetOptions }
|
||||
}: {
|
||||
blockId: string
|
||||
outgoingEdgeId?: string
|
||||
options: GoogleSheetsGetOptions
|
||||
}
|
||||
): Promise<ExecuteIntegrationResponse> => {
|
||||
const logs: ChatLog[] = []
|
||||
const { variables } = state.typebotsQueue[0].typebot
|
||||
@ -79,10 +84,15 @@ export const getRow = async (
|
||||
[]
|
||||
)
|
||||
if (!newVariables) return { outgoingEdgeId }
|
||||
const newSessionState = updateVariablesInSession(state)(newVariables)
|
||||
const { updatedState, newSetVariableHistory } = updateVariablesInSession({
|
||||
state,
|
||||
newVariables,
|
||||
currentBlockId: blockId,
|
||||
})
|
||||
return {
|
||||
outgoingEdgeId,
|
||||
newSessionState,
|
||||
newSessionState: updatedState,
|
||||
newSetVariableHistory,
|
||||
}
|
||||
} catch (err) {
|
||||
logs.push({
|
||||
|
@ -107,12 +107,16 @@ export const createSpeechOpenAI = async (
|
||||
mimeType: 'audio/mpeg',
|
||||
})
|
||||
|
||||
newSessionState = updateVariablesInSession(newSessionState)([
|
||||
{
|
||||
...saveUrlInVariable,
|
||||
value: url,
|
||||
},
|
||||
])
|
||||
newSessionState = updateVariablesInSession({
|
||||
newVariables: [
|
||||
{
|
||||
...saveUrlInVariable,
|
||||
value: url,
|
||||
},
|
||||
],
|
||||
state: newSessionState,
|
||||
currentBlockId: undefined,
|
||||
}).updatedState
|
||||
|
||||
return {
|
||||
startTimeShouldBeUpdated: true,
|
||||
|
@ -22,7 +22,6 @@ import {
|
||||
defaultOpenAIOptions,
|
||||
} from '@typebot.io/schemas/features/blocks/integrations/openai/constants'
|
||||
import { BubbleBlockType } from '@typebot.io/schemas/features/blocks/bubbles/constants'
|
||||
import { isPlaneteScale } from '@typebot.io/lib/isPlanetScale'
|
||||
|
||||
export const createChatCompletionOpenAI = async (
|
||||
state: SessionState,
|
||||
@ -68,9 +67,11 @@ export const createChatCompletionOpenAI = async (
|
||||
typebot.variables
|
||||
)(options.messages)
|
||||
if (variablesTransformedToList.length > 0)
|
||||
newSessionState = updateVariablesInSession(state)(
|
||||
variablesTransformedToList
|
||||
)
|
||||
newSessionState = updateVariablesInSession({
|
||||
state,
|
||||
newVariables: variablesTransformedToList,
|
||||
currentBlockId: undefined,
|
||||
}).updatedState
|
||||
|
||||
const temperature = parseVariableNumber(typebot.variables)(
|
||||
options.advancedSettings?.temperature
|
||||
|
@ -42,7 +42,11 @@ export const resumeChatCompletion =
|
||||
return newVariables
|
||||
}, [])
|
||||
if (newVariables && newVariables.length > 0)
|
||||
newSessionState = updateVariablesInSession(newSessionState)(newVariables)
|
||||
newSessionState = updateVariablesInSession({
|
||||
newVariables,
|
||||
state: newSessionState,
|
||||
currentBlockId: undefined,
|
||||
}).updatedState
|
||||
return {
|
||||
outgoingEdgeId,
|
||||
newSessionState,
|
||||
|
@ -70,10 +70,15 @@ export const resumeWebhookExecution = ({
|
||||
}
|
||||
}, [])
|
||||
if (newVariables && newVariables.length > 0) {
|
||||
const newSessionState = updateVariablesInSession(state)(newVariables)
|
||||
const { updatedState, newSetVariableHistory } = updateVariablesInSession({
|
||||
newVariables,
|
||||
state,
|
||||
currentBlockId: block.id,
|
||||
})
|
||||
return {
|
||||
outgoingEdgeId: block.outgoingEdgeId,
|
||||
newSessionState,
|
||||
newSessionState: updatedState,
|
||||
newSetVariableHistory,
|
||||
logs,
|
||||
}
|
||||
}
|
||||
|
@ -19,6 +19,7 @@ export const executeZemanticAiBlock = async (
|
||||
block: ZemanticAiBlock
|
||||
): Promise<ExecuteIntegrationResponse> => {
|
||||
let newSessionState = state
|
||||
let setVariableHistory = []
|
||||
|
||||
if (!block.options?.credentialsId)
|
||||
return {
|
||||
@ -82,24 +83,34 @@ export const executeZemanticAiBlock = async (
|
||||
|
||||
for (const r of block.options.responseMapping || []) {
|
||||
const variable = typebot.variables.find(byId(r.variableId))
|
||||
let newVariables = []
|
||||
switch (r.valueToExtract) {
|
||||
case 'Summary':
|
||||
if (isDefined(variable) && !isEmpty(res.summary)) {
|
||||
newSessionState = updateVariablesInSession(newSessionState)([
|
||||
{ ...variable, value: res.summary },
|
||||
])
|
||||
newVariables.push({ ...variable, value: res.summary })
|
||||
}
|
||||
break
|
||||
case 'Results':
|
||||
if (isDefined(variable) && res.results.length) {
|
||||
newSessionState = updateVariablesInSession(newSessionState)([
|
||||
{ ...variable, value: JSON.stringify(res.results) },
|
||||
])
|
||||
newVariables.push({
|
||||
...variable,
|
||||
value: JSON.stringify(res.results),
|
||||
})
|
||||
}
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
if (newVariables.length > 0) {
|
||||
const { newSetVariableHistory, updatedState } =
|
||||
updateVariablesInSession({
|
||||
newVariables,
|
||||
state: newSessionState,
|
||||
currentBlockId: block.id,
|
||||
})
|
||||
newSessionState = updatedState
|
||||
setVariableHistory.push(...newSetVariableHistory)
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
@ -112,6 +123,7 @@ export const executeZemanticAiBlock = async (
|
||||
description: 'Could not execute Zemantic AI request',
|
||||
},
|
||||
],
|
||||
newSetVariableHistory: setVariableHistory,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,14 +1,14 @@
|
||||
import { ConditionBlock, SessionState } from '@typebot.io/schemas'
|
||||
import { ExecuteLogicResponse } from '../../../types'
|
||||
import { executeCondition } from './executeCondition'
|
||||
|
||||
import { executeCondition } from '@typebot.io/logic/executeCondition'
|
||||
export const executeConditionBlock = (
|
||||
state: SessionState,
|
||||
block: ConditionBlock
|
||||
): ExecuteLogicResponse => {
|
||||
const { variables } = state.typebotsQueue[0].typebot
|
||||
const passedCondition = block.items.find(
|
||||
(item) => item.content && executeCondition(variables)(item.content)
|
||||
(item) =>
|
||||
item.content && executeCondition({ variables, condition: item.content })
|
||||
)
|
||||
return {
|
||||
outgoingEdgeId: passedCondition
|
||||
|
@ -24,14 +24,25 @@ export const executeScript = async (
|
||||
body: block.options.content,
|
||||
})
|
||||
|
||||
const newSessionState = newVariables
|
||||
? updateVariablesInSession(state)(newVariables)
|
||||
: state
|
||||
const updateVarResults = newVariables
|
||||
? updateVariablesInSession({
|
||||
newVariables,
|
||||
state,
|
||||
currentBlockId: block.id,
|
||||
})
|
||||
: undefined
|
||||
|
||||
let newSessionState = state
|
||||
|
||||
if (updateVarResults) {
|
||||
newSessionState = updateVarResults.updatedState
|
||||
}
|
||||
|
||||
return {
|
||||
outgoingEdgeId: block.outgoingEdgeId,
|
||||
logs: error ? [{ status: 'error', description: error }] : [],
|
||||
newSessionState,
|
||||
newSetVariableHistory: updateVarResults?.newSetVariableHistory,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,10 @@
|
||||
import { SessionState, SetVariableBlock, Variable } from '@typebot.io/schemas'
|
||||
import {
|
||||
Answer,
|
||||
SessionState,
|
||||
SetVariableBlock,
|
||||
SetVariableHistoryItem,
|
||||
Variable,
|
||||
} from '@typebot.io/schemas'
|
||||
import { byId, isEmpty } from '@typebot.io/lib'
|
||||
import { ExecuteLogicResponse } from '../../../types'
|
||||
import { parseScriptToExecuteClientSideAction } from '../script/executeScript'
|
||||
@ -7,18 +13,27 @@ import { parseVariables } from '@typebot.io/variables/parseVariables'
|
||||
import { updateVariablesInSession } from '@typebot.io/variables/updateVariablesInSession'
|
||||
import { createId } from '@paralleldrive/cuid2'
|
||||
import { utcToZonedTime, format as tzFormat } from 'date-fns-tz'
|
||||
import {
|
||||
computeResultTranscript,
|
||||
parseTranscriptMessageText,
|
||||
} from '@typebot.io/logic/computeResultTranscript'
|
||||
import prisma from '@typebot.io/lib/prisma'
|
||||
import { sessionOnlySetVariableOptions } from '@typebot.io/schemas/features/blocks/logic/setVariable/constants'
|
||||
import vm from 'vm'
|
||||
|
||||
export const executeSetVariable = (
|
||||
export const executeSetVariable = async (
|
||||
state: SessionState,
|
||||
block: SetVariableBlock
|
||||
): ExecuteLogicResponse => {
|
||||
): Promise<ExecuteLogicResponse> => {
|
||||
const { variables } = state.typebotsQueue[0].typebot
|
||||
if (!block.options?.variableId)
|
||||
return {
|
||||
outgoingEdgeId: block.outgoingEdgeId,
|
||||
}
|
||||
const expressionToEvaluate = getExpressionToEvaluate(state)(block.options)
|
||||
const expressionToEvaluate = await getExpressionToEvaluate(state)(
|
||||
block.options,
|
||||
block.id
|
||||
)
|
||||
const isCustomValue = !block.options.type || block.options.type === 'Custom'
|
||||
if (
|
||||
expressionToEvaluate &&
|
||||
@ -52,10 +67,25 @@ export const executeSetVariable = (
|
||||
...existingVariable,
|
||||
value: evaluatedExpression,
|
||||
}
|
||||
const newSessionState = updateVariablesInSession(state)([newVariable])
|
||||
const { newSetVariableHistory, updatedState } = updateVariablesInSession({
|
||||
state,
|
||||
newVariables: [
|
||||
{
|
||||
...newVariable,
|
||||
isSessionVariable: sessionOnlySetVariableOptions.includes(
|
||||
block.options.type as (typeof sessionOnlySetVariableOptions)[number]
|
||||
)
|
||||
? true
|
||||
: newVariable.isSessionVariable,
|
||||
},
|
||||
],
|
||||
currentBlockId: block.id,
|
||||
})
|
||||
|
||||
return {
|
||||
outgoingEdgeId: block.outgoingEdgeId,
|
||||
newSessionState,
|
||||
newSessionState: updatedState,
|
||||
newSetVariableHistory,
|
||||
}
|
||||
}
|
||||
|
||||
@ -85,7 +115,10 @@ const evaluateSetVariableExpression =
|
||||
|
||||
const getExpressionToEvaluate =
|
||||
(state: SessionState) =>
|
||||
(options: SetVariableBlock['options']): string | null => {
|
||||
async (
|
||||
options: SetVariableBlock['options'],
|
||||
blockId: string
|
||||
): Promise<string | null> => {
|
||||
switch (options?.type) {
|
||||
case 'Contact name':
|
||||
return state.whatsApp?.contact.name ?? null
|
||||
@ -149,6 +182,34 @@ const getExpressionToEvaluate =
|
||||
case 'Environment name': {
|
||||
return state.whatsApp ? 'whatsapp' : 'web'
|
||||
}
|
||||
case 'Transcript': {
|
||||
const props = await parseTranscriptProps(state)
|
||||
if (!props) return ''
|
||||
const typebotWithEmptyVariables = {
|
||||
...state.typebotsQueue[0].typebot,
|
||||
variables: state.typebotsQueue[0].typebot.variables.map((v) => ({
|
||||
...v,
|
||||
value: undefined,
|
||||
})),
|
||||
}
|
||||
const transcript = computeResultTranscript({
|
||||
typebot: typebotWithEmptyVariables,
|
||||
stopAtBlockId: blockId,
|
||||
...props,
|
||||
})
|
||||
return (
|
||||
'return `' +
|
||||
transcript
|
||||
.map(
|
||||
(message) =>
|
||||
`${
|
||||
message.role === 'bot' ? 'Assistant:' : 'User:'
|
||||
} "${parseTranscriptMessageText(message)}"`
|
||||
)
|
||||
.join('\n\n') +
|
||||
'`'
|
||||
)
|
||||
}
|
||||
case 'Custom':
|
||||
case undefined: {
|
||||
return options?.expressionToEvaluate ?? null
|
||||
@ -160,3 +221,79 @@ const toISOWithTz = (date: Date, timeZone: string) => {
|
||||
const zonedDate = utcToZonedTime(date, timeZone)
|
||||
return tzFormat(zonedDate, "yyyy-MM-dd'T'HH:mm:ssXXX", { timeZone })
|
||||
}
|
||||
|
||||
type ParsedTranscriptProps = {
|
||||
answers: Pick<Answer, 'blockId' | 'content'>[]
|
||||
setVariableHistory: Pick<
|
||||
SetVariableHistoryItem,
|
||||
'blockId' | 'variableId' | 'value'
|
||||
>[]
|
||||
visitedEdges: string[]
|
||||
}
|
||||
|
||||
const parseTranscriptProps = async (
|
||||
state: SessionState
|
||||
): Promise<ParsedTranscriptProps | undefined> => {
|
||||
if (!state.typebotsQueue[0].resultId)
|
||||
return parsePreviewTranscriptProps(state)
|
||||
return parseResultTranscriptProps(state)
|
||||
}
|
||||
|
||||
const parsePreviewTranscriptProps = async (
|
||||
state: SessionState
|
||||
): Promise<ParsedTranscriptProps | undefined> => {
|
||||
if (!state.previewMetadata) return
|
||||
return {
|
||||
answers: state.previewMetadata.answers ?? [],
|
||||
setVariableHistory: state.previewMetadata.setVariableHistory ?? [],
|
||||
visitedEdges: state.previewMetadata.visitedEdges ?? [],
|
||||
}
|
||||
}
|
||||
|
||||
const parseResultTranscriptProps = async (
|
||||
state: SessionState
|
||||
): Promise<ParsedTranscriptProps | undefined> => {
|
||||
const result = await prisma.result.findUnique({
|
||||
where: {
|
||||
id: state.typebotsQueue[0].resultId,
|
||||
},
|
||||
select: {
|
||||
edges: {
|
||||
select: {
|
||||
edgeId: true,
|
||||
index: true,
|
||||
},
|
||||
},
|
||||
answers: {
|
||||
select: {
|
||||
blockId: true,
|
||||
content: true,
|
||||
},
|
||||
},
|
||||
answersV2: {
|
||||
select: {
|
||||
blockId: true,
|
||||
content: true,
|
||||
},
|
||||
},
|
||||
setVariableHistory: {
|
||||
select: {
|
||||
blockId: true,
|
||||
variableId: true,
|
||||
index: true,
|
||||
value: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
if (!result) return
|
||||
return {
|
||||
answers: result.answersV2.concat(result.answers),
|
||||
setVariableHistory: (
|
||||
result.setVariableHistory as SetVariableHistoryItem[]
|
||||
).sort((a, b) => a.index - b.index),
|
||||
visitedEdges: result.edges
|
||||
.sort((a, b) => a.index - b.index)
|
||||
.map((edge) => edge.edgeId),
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import {
|
||||
Group,
|
||||
InputBlock,
|
||||
SessionState,
|
||||
SetVariableHistoryItem,
|
||||
} from '@typebot.io/schemas'
|
||||
import { byId } from '@typebot.io/lib'
|
||||
import { isInputBlock } from '@typebot.io/schemas/helpers'
|
||||
@ -13,7 +14,7 @@ import { getNextGroup } from './getNextGroup'
|
||||
import { validateEmail } from './blocks/inputs/email/validateEmail'
|
||||
import { formatPhoneNumber } from './blocks/inputs/phone/formatPhoneNumber'
|
||||
import { resumeWebhookExecution } from './blocks/integrations/webhook/resumeWebhookExecution'
|
||||
import { upsertAnswer } from './queries/upsertAnswer'
|
||||
import { saveAnswer } from './queries/saveAnswer'
|
||||
import { parseButtonsReply } from './blocks/inputs/buttons/parseButtonsReply'
|
||||
import { ParsedReply, Reply } from './types'
|
||||
import { validateNumber } from './blocks/inputs/number/validateNumber'
|
||||
@ -57,11 +58,13 @@ export const continueBotFlow = async (
|
||||
ContinueChatResponse & {
|
||||
newSessionState: SessionState
|
||||
visitedEdges: VisitedEdge[]
|
||||
setVariableHistory: SetVariableHistoryItem[]
|
||||
}
|
||||
> => {
|
||||
let firstBubbleWasStreamed = false
|
||||
let newSessionState = { ...state }
|
||||
const visitedEdges: VisitedEdge[] = []
|
||||
const setVariableHistory: SetVariableHistoryItem[] = []
|
||||
|
||||
if (!newSessionState.currentBlockId) return startBotFlow({ state, version })
|
||||
|
||||
@ -76,16 +79,17 @@ export const continueBotFlow = async (
|
||||
message: 'Group / block not found',
|
||||
})
|
||||
|
||||
let variableToUpdate
|
||||
|
||||
if (block.type === LogicBlockType.SET_VARIABLE) {
|
||||
const existingVariable = state.typebotsQueue[0].typebot.variables.find(
|
||||
byId(block.options?.variableId)
|
||||
)
|
||||
if (existingVariable && reply && typeof reply === 'string') {
|
||||
const newVariable = {
|
||||
variableToUpdate = {
|
||||
...existingVariable,
|
||||
value: safeJsonParse(reply),
|
||||
}
|
||||
newSessionState = updateVariablesInSession(state)([newVariable])
|
||||
}
|
||||
}
|
||||
// Legacy
|
||||
@ -121,42 +125,41 @@ export const continueBotFlow = async (
|
||||
if (action) {
|
||||
if (action.run?.stream?.getStreamVariableId) {
|
||||
firstBubbleWasStreamed = true
|
||||
const variableToUpdate =
|
||||
state.typebotsQueue[0].typebot.variables.find(
|
||||
(v) => v.id === action?.run?.stream?.getStreamVariableId(options)
|
||||
)
|
||||
if (variableToUpdate)
|
||||
newSessionState = updateVariablesInSession(state)([
|
||||
{
|
||||
...variableToUpdate,
|
||||
value: reply,
|
||||
},
|
||||
])
|
||||
variableToUpdate = state.typebotsQueue[0].typebot.variables.find(
|
||||
(v) => v.id === action?.run?.stream?.getStreamVariableId(options)
|
||||
)
|
||||
}
|
||||
|
||||
if (
|
||||
action.run?.web?.displayEmbedBubble?.waitForEvent?.getSaveVariableId
|
||||
) {
|
||||
const variableToUpdate =
|
||||
state.typebotsQueue[0].typebot.variables.find(
|
||||
(v) =>
|
||||
v.id ===
|
||||
action?.run?.web?.displayEmbedBubble?.waitForEvent?.getSaveVariableId?.(
|
||||
options
|
||||
)
|
||||
)
|
||||
if (variableToUpdate)
|
||||
newSessionState = updateVariablesInSession(state)([
|
||||
{
|
||||
...variableToUpdate,
|
||||
value: reply,
|
||||
},
|
||||
])
|
||||
variableToUpdate = state.typebotsQueue[0].typebot.variables.find(
|
||||
(v) =>
|
||||
v.id ===
|
||||
action?.run?.web?.displayEmbedBubble?.waitForEvent?.getSaveVariableId?.(
|
||||
options
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (variableToUpdate) {
|
||||
const { newSetVariableHistory, updatedState } = updateVariablesInSession({
|
||||
state: newSessionState,
|
||||
currentBlockId: block.id,
|
||||
newVariables: [
|
||||
{
|
||||
...variableToUpdate,
|
||||
value: reply,
|
||||
},
|
||||
],
|
||||
})
|
||||
newSessionState = updatedState
|
||||
setVariableHistory.push(...newSetVariableHistory)
|
||||
}
|
||||
|
||||
let formattedReply: string | undefined
|
||||
|
||||
if (isInputBlock(block)) {
|
||||
@ -167,6 +170,7 @@ export const continueBotFlow = async (
|
||||
...(await parseRetryMessage(newSessionState)(block)),
|
||||
newSessionState,
|
||||
visitedEdges: [],
|
||||
setVariableHistory: [],
|
||||
}
|
||||
|
||||
formattedReply =
|
||||
@ -176,7 +180,9 @@ export const continueBotFlow = async (
|
||||
|
||||
const groupHasMoreBlocks = blockIndex < group.blocks.length - 1
|
||||
|
||||
const nextEdgeId = getOutgoingEdgeId(newSessionState)(block, formattedReply)
|
||||
const { edgeId: nextEdgeId, isOffDefaultPath } = getOutgoingEdgeId(
|
||||
newSessionState
|
||||
)(block, formattedReply)
|
||||
|
||||
if (groupHasMoreBlocks && !nextEdgeId) {
|
||||
const chatReply = await executeGroup(
|
||||
@ -188,6 +194,7 @@ export const continueBotFlow = async (
|
||||
version,
|
||||
state: newSessionState,
|
||||
visitedEdges,
|
||||
setVariableHistory,
|
||||
firstBubbleWasStreamed,
|
||||
startTime,
|
||||
}
|
||||
@ -206,9 +213,14 @@ export const continueBotFlow = async (
|
||||
lastMessageNewFormat:
|
||||
formattedReply !== reply ? formattedReply : undefined,
|
||||
visitedEdges,
|
||||
setVariableHistory,
|
||||
}
|
||||
|
||||
const nextGroup = await getNextGroup(newSessionState)(nextEdgeId)
|
||||
const nextGroup = await getNextGroup({
|
||||
state: newSessionState,
|
||||
edgeId: nextEdgeId,
|
||||
isOffDefaultPath,
|
||||
})
|
||||
|
||||
if (nextGroup.visitedEdge) visitedEdges.push(nextGroup.visitedEdge)
|
||||
|
||||
@ -221,6 +233,7 @@ export const continueBotFlow = async (
|
||||
lastMessageNewFormat:
|
||||
formattedReply !== reply ? formattedReply : undefined,
|
||||
visitedEdges,
|
||||
setVariableHistory,
|
||||
}
|
||||
|
||||
const chatReply = await executeGroup(nextGroup.group, {
|
||||
@ -228,6 +241,7 @@ export const continueBotFlow = async (
|
||||
state: newSessionState,
|
||||
firstBubbleWasStreamed,
|
||||
visitedEdges,
|
||||
setVariableHistory,
|
||||
startTime,
|
||||
})
|
||||
|
||||
@ -241,8 +255,7 @@ const processAndSaveAnswer =
|
||||
(state: SessionState, block: InputBlock) =>
|
||||
async (reply: string | undefined): Promise<SessionState> => {
|
||||
if (!reply) return state
|
||||
let newState = await saveAnswer(state, block)(reply)
|
||||
newState = saveVariableValueIfAny(newState, block)(reply)
|
||||
let newState = await saveAnswerInDb(state, block)(reply)
|
||||
return newState
|
||||
}
|
||||
|
||||
@ -255,16 +268,20 @@ const saveVariableValueIfAny =
|
||||
)
|
||||
if (!foundVariable) return state
|
||||
|
||||
const newSessionState = updateVariablesInSession(state)([
|
||||
{
|
||||
...foundVariable,
|
||||
value: Array.isArray(foundVariable.value)
|
||||
? foundVariable.value.concat(reply)
|
||||
: reply,
|
||||
},
|
||||
])
|
||||
const { updatedState } = updateVariablesInSession({
|
||||
newVariables: [
|
||||
{
|
||||
...foundVariable,
|
||||
value: Array.isArray(foundVariable.value)
|
||||
? foundVariable.value.concat(reply)
|
||||
: reply,
|
||||
},
|
||||
],
|
||||
currentBlockId: undefined,
|
||||
state,
|
||||
})
|
||||
|
||||
return newSessionState
|
||||
return updatedState
|
||||
}
|
||||
|
||||
const parseRetryMessage =
|
||||
@ -305,31 +322,43 @@ const parseDefaultRetryMessage = (block: InputBlock): string => {
|
||||
}
|
||||
}
|
||||
|
||||
const saveAnswer =
|
||||
const saveAnswerInDb =
|
||||
(state: SessionState, block: InputBlock) =>
|
||||
async (reply: string): Promise<SessionState> => {
|
||||
let newSessionState = state
|
||||
const groupId = state.typebotsQueue[0].typebot.groups.find((group) =>
|
||||
group.blocks.some((blockInGroup) => blockInGroup.id === block.id)
|
||||
)?.id
|
||||
if (!groupId) throw new Error('saveAnswer: Group not found')
|
||||
await upsertAnswer({
|
||||
await saveAnswer({
|
||||
answer: {
|
||||
blockId: block.id,
|
||||
groupId,
|
||||
content: reply,
|
||||
variableId: block.options?.variableId,
|
||||
},
|
||||
reply,
|
||||
state,
|
||||
})
|
||||
|
||||
newSessionState = {
|
||||
...saveVariableValueIfAny(newSessionState, block)(reply),
|
||||
previewMetadata: state.typebotsQueue[0].resultId
|
||||
? newSessionState.previewMetadata
|
||||
: {
|
||||
...newSessionState.previewMetadata,
|
||||
answers: (newSessionState.previewMetadata?.answers ?? []).concat({
|
||||
blockId: block.id,
|
||||
content: reply,
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
const key = block.options?.variableId
|
||||
? state.typebotsQueue[0].typebot.variables.find(
|
||||
? newSessionState.typebotsQueue[0].typebot.variables.find(
|
||||
(variable) => variable.id === block.options?.variableId
|
||||
)?.name
|
||||
: parseGroupKey(block.id, { state })
|
||||
: parseGroupKey(block.id, { state: newSessionState })
|
||||
|
||||
return setNewAnswerInState(state)({
|
||||
return setNewAnswerInState(newSessionState)({
|
||||
key: key ?? block.id,
|
||||
value: reply,
|
||||
})
|
||||
@ -375,7 +404,10 @@ const setNewAnswerInState =
|
||||
|
||||
const getOutgoingEdgeId =
|
||||
(state: Pick<SessionState, 'typebotsQueue'>) =>
|
||||
(block: Block, reply: string | undefined) => {
|
||||
(
|
||||
block: Block,
|
||||
reply: string | undefined
|
||||
): { edgeId: string | undefined; isOffDefaultPath: boolean } => {
|
||||
const variables = state.typebotsQueue[0].typebot.variables
|
||||
if (
|
||||
block.type === InputBlockType.CHOICE &&
|
||||
@ -390,7 +422,8 @@ const getOutgoingEdgeId =
|
||||
parseVariables(variables)(item.content).normalize() ===
|
||||
reply.normalize()
|
||||
)
|
||||
if (matchedItem?.outgoingEdgeId) return matchedItem.outgoingEdgeId
|
||||
if (matchedItem?.outgoingEdgeId)
|
||||
return { edgeId: matchedItem.outgoingEdgeId, isOffDefaultPath: true }
|
||||
}
|
||||
if (
|
||||
block.type === InputBlockType.PICTURE_CHOICE &&
|
||||
@ -405,9 +438,10 @@ const getOutgoingEdgeId =
|
||||
parseVariables(variables)(item.title).normalize() ===
|
||||
reply.normalize()
|
||||
)
|
||||
if (matchedItem?.outgoingEdgeId) return matchedItem.outgoingEdgeId
|
||||
if (matchedItem?.outgoingEdgeId)
|
||||
return { edgeId: matchedItem.outgoingEdgeId, isOffDefaultPath: true }
|
||||
}
|
||||
return block.outgoingEdgeId
|
||||
return { edgeId: block.outgoingEdgeId, isOffDefaultPath: false }
|
||||
}
|
||||
|
||||
const parseReply =
|
||||
|
@ -4,6 +4,7 @@ import {
|
||||
InputBlock,
|
||||
RuntimeOptions,
|
||||
SessionState,
|
||||
SetVariableHistoryItem,
|
||||
} from '@typebot.io/schemas'
|
||||
import { isNotEmpty } from '@typebot.io/lib'
|
||||
import {
|
||||
@ -21,16 +22,16 @@ import { injectVariableValuesInPictureChoiceBlock } from './blocks/inputs/pictur
|
||||
import { getPrefilledInputValue } from './getPrefilledValue'
|
||||
import { parseDateInput } from './blocks/inputs/date/parseDateInput'
|
||||
import { deepParseVariables } from '@typebot.io/variables/deepParseVariables'
|
||||
import {
|
||||
BubbleBlockWithDefinedContent,
|
||||
parseBubbleBlock,
|
||||
} from './parseBubbleBlock'
|
||||
import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants'
|
||||
import { VisitedEdge } from '@typebot.io/prisma'
|
||||
import { env } from '@typebot.io/env'
|
||||
import { TRPCError } from '@trpc/server'
|
||||
import { ExecuteIntegrationResponse, ExecuteLogicResponse } from './types'
|
||||
import { createId } from '@paralleldrive/cuid2'
|
||||
import {
|
||||
BubbleBlockWithDefinedContent,
|
||||
parseBubbleBlock,
|
||||
} from './parseBubbleBlock'
|
||||
|
||||
type ContextProps = {
|
||||
version: 1 | 2
|
||||
@ -39,6 +40,7 @@ type ContextProps = {
|
||||
currentLastBubbleId?: string
|
||||
firstBubbleWasStreamed?: boolean
|
||||
visitedEdges: VisitedEdge[]
|
||||
setVariableHistory: SetVariableHistoryItem[]
|
||||
startTime?: number
|
||||
}
|
||||
|
||||
@ -48,6 +50,7 @@ export const executeGroup = async (
|
||||
version,
|
||||
state,
|
||||
visitedEdges,
|
||||
setVariableHistory,
|
||||
currentReply,
|
||||
currentLastBubbleId,
|
||||
firstBubbleWasStreamed,
|
||||
@ -56,6 +59,7 @@ export const executeGroup = async (
|
||||
): Promise<
|
||||
ContinueChatResponse & {
|
||||
newSessionState: SessionState
|
||||
setVariableHistory: SetVariableHistoryItem[]
|
||||
visitedEdges: VisitedEdge[]
|
||||
}
|
||||
> => {
|
||||
@ -70,6 +74,7 @@ export const executeGroup = async (
|
||||
|
||||
let newSessionState = state
|
||||
|
||||
let isNextEdgeOffDefaultPath = false
|
||||
let index = -1
|
||||
for (const block of group.blocks) {
|
||||
if (
|
||||
@ -110,6 +115,7 @@ export const executeGroup = async (
|
||||
clientSideActions,
|
||||
logs,
|
||||
visitedEdges,
|
||||
setVariableHistory,
|
||||
}
|
||||
const executionResponse = (
|
||||
isLogicBlock(block)
|
||||
@ -120,6 +126,29 @@ export const executeGroup = async (
|
||||
) as ExecuteLogicResponse | ExecuteIntegrationResponse | null
|
||||
|
||||
if (!executionResponse) continue
|
||||
if (
|
||||
executionResponse.newSetVariableHistory &&
|
||||
executionResponse.newSetVariableHistory?.length > 0
|
||||
) {
|
||||
if (!newSessionState.typebotsQueue[0].resultId)
|
||||
newSessionState = {
|
||||
...newSessionState,
|
||||
previewMetadata: {
|
||||
...newSessionState.previewMetadata,
|
||||
setVariableHistory: (
|
||||
newSessionState.previewMetadata?.setVariableHistory ?? []
|
||||
).concat(
|
||||
executionResponse.newSetVariableHistory.map((item) => ({
|
||||
blockId: item.blockId,
|
||||
variableId: item.variableId,
|
||||
value: item.value,
|
||||
}))
|
||||
),
|
||||
},
|
||||
}
|
||||
else setVariableHistory.push(...executionResponse.newSetVariableHistory)
|
||||
}
|
||||
|
||||
if (
|
||||
'startTimeShouldBeUpdated' in executionResponse &&
|
||||
executionResponse.startTimeShouldBeUpdated
|
||||
@ -165,33 +194,55 @@ export const executeGroup = async (
|
||||
clientSideActions,
|
||||
logs,
|
||||
visitedEdges,
|
||||
setVariableHistory,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (executionResponse.outgoingEdgeId) {
|
||||
isNextEdgeOffDefaultPath =
|
||||
block.outgoingEdgeId !== executionResponse.outgoingEdgeId
|
||||
nextEdgeId = executionResponse.outgoingEdgeId
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (!nextEdgeId && newSessionState.typebotsQueue.length === 1)
|
||||
return { messages, newSessionState, clientSideActions, logs, visitedEdges }
|
||||
return {
|
||||
messages,
|
||||
newSessionState,
|
||||
clientSideActions,
|
||||
logs,
|
||||
visitedEdges,
|
||||
setVariableHistory,
|
||||
}
|
||||
|
||||
const nextGroup = await getNextGroup(newSessionState)(nextEdgeId ?? undefined)
|
||||
const nextGroup = await getNextGroup({
|
||||
state: newSessionState,
|
||||
edgeId: nextEdgeId ?? undefined,
|
||||
isOffDefaultPath: isNextEdgeOffDefaultPath,
|
||||
})
|
||||
|
||||
newSessionState = nextGroup.newSessionState
|
||||
|
||||
if (nextGroup.visitedEdge) visitedEdges.push(nextGroup.visitedEdge)
|
||||
|
||||
if (!nextGroup.group) {
|
||||
return { messages, newSessionState, clientSideActions, logs, visitedEdges }
|
||||
return {
|
||||
messages,
|
||||
newSessionState,
|
||||
clientSideActions,
|
||||
logs,
|
||||
visitedEdges,
|
||||
setVariableHistory,
|
||||
}
|
||||
}
|
||||
|
||||
return executeGroup(nextGroup.group, {
|
||||
version,
|
||||
state: newSessionState,
|
||||
visitedEdges,
|
||||
setVariableHistory,
|
||||
currentReply: {
|
||||
messages,
|
||||
clientSideActions,
|
||||
|
@ -2,12 +2,12 @@ import { VariableStore, LogsStore } from '@typebot.io/forge'
|
||||
import { forgedBlocks } from '@typebot.io/forge-repository/definitions'
|
||||
import { ForgedBlock } from '@typebot.io/forge-repository/types'
|
||||
import { decrypt } from '@typebot.io/lib/api/encryption/decrypt'
|
||||
import { isPlaneteScale } from '@typebot.io/lib/isPlanetScale'
|
||||
import {
|
||||
SessionState,
|
||||
ContinueChatResponse,
|
||||
Block,
|
||||
TypebotInSession,
|
||||
SetVariableHistoryItem,
|
||||
} from '@typebot.io/schemas'
|
||||
import { deepParseVariables } from '@typebot.io/variables/deepParseVariables'
|
||||
import {
|
||||
@ -73,6 +73,7 @@ export const executeForgedBlock = async (
|
||||
}
|
||||
|
||||
let newSessionState = state
|
||||
let setVariableHistory: SetVariableHistoryItem[] = []
|
||||
|
||||
const variables: VariableStore = {
|
||||
get: (id: string) => {
|
||||
@ -86,9 +87,13 @@ export const executeForgedBlock = async (
|
||||
(variable) => variable.id === id
|
||||
)
|
||||
if (!variable) return
|
||||
newSessionState = updateVariablesInSession(newSessionState)([
|
||||
{ ...variable, value },
|
||||
])
|
||||
const { newSetVariableHistory, updatedState } = updateVariablesInSession({
|
||||
newVariables: [{ ...variable, value }],
|
||||
state: newSessionState,
|
||||
currentBlockId: block.id,
|
||||
})
|
||||
newSessionState = updatedState
|
||||
setVariableHistory.push(...newSetVariableHistory)
|
||||
},
|
||||
parse: (text: string, params?: ParseVariablesOptions) =>
|
||||
parseVariables(
|
||||
@ -159,6 +164,7 @@ export const executeForgedBlock = async (
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
newSetVariableHistory: setVariableHistory,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,14 +1,13 @@
|
||||
import { TRPCError } from '@trpc/server'
|
||||
import { SessionState } from '@typebot.io/schemas'
|
||||
import { TypebotInSession } from '@typebot.io/schemas'
|
||||
|
||||
export const getFirstEdgeId = ({
|
||||
state,
|
||||
typebot,
|
||||
startEventId,
|
||||
}: {
|
||||
state: SessionState
|
||||
typebot: Pick<TypebotInSession, 'events' | 'groups' | 'version'>
|
||||
startEventId: string | undefined
|
||||
}) => {
|
||||
const { typebot } = state.typebotsQueue[0]
|
||||
if (startEventId) {
|
||||
const event = typebot.events?.find((e) => e.id === startEventId)
|
||||
if (!event)
|
||||
@ -18,6 +17,6 @@ export const getFirstEdgeId = ({
|
||||
})
|
||||
return event.outgoingEdgeId
|
||||
}
|
||||
if (typebot.version === '6') return typebot.events[0].outgoingEdgeId
|
||||
if (typebot.version === '6') return typebot.events?.[0].outgoingEdgeId
|
||||
return typebot.groups.at(0)?.blocks.at(0)?.outgoingEdgeId
|
||||
}
|
||||
|
@ -9,116 +9,138 @@ export type NextGroup = {
|
||||
visitedEdge?: VisitedEdge
|
||||
}
|
||||
|
||||
export const getNextGroup =
|
||||
(state: SessionState) =>
|
||||
async (edgeId?: string): Promise<NextGroup> => {
|
||||
const nextEdge = state.typebotsQueue[0].typebot.edges.find(byId(edgeId))
|
||||
if (!nextEdge) {
|
||||
if (state.typebotsQueue.length > 1) {
|
||||
const nextEdgeId = state.typebotsQueue[0].edgeIdToTriggerWhenDone
|
||||
const isMergingWithParent = state.typebotsQueue[0].isMergingWithParent
|
||||
const currentResultId = state.typebotsQueue[0].resultId
|
||||
if (!isMergingWithParent && currentResultId)
|
||||
await upsertResult({
|
||||
resultId: currentResultId,
|
||||
typebot: state.typebotsQueue[0].typebot,
|
||||
isCompleted: true,
|
||||
hasStarted: state.typebotsQueue[0].answers.length > 0,
|
||||
})
|
||||
let newSessionState = {
|
||||
...state,
|
||||
typebotsQueue: [
|
||||
{
|
||||
...state.typebotsQueue[1],
|
||||
typebot: isMergingWithParent
|
||||
? {
|
||||
...state.typebotsQueue[1].typebot,
|
||||
variables: state.typebotsQueue[1].typebot.variables
|
||||
.map((variable) => ({
|
||||
...variable,
|
||||
value:
|
||||
state.typebotsQueue[0].typebot.variables.find(
|
||||
(v) => v.name === variable.name
|
||||
)?.value ?? variable.value,
|
||||
}))
|
||||
.concat(
|
||||
state.typebotsQueue[0].typebot.variables.filter(
|
||||
(variable) =>
|
||||
isDefined(variable.value) &&
|
||||
isNotDefined(
|
||||
state.typebotsQueue[1].typebot.variables.find(
|
||||
(v) => v.name === variable.name
|
||||
)
|
||||
export const getNextGroup = async ({
|
||||
state,
|
||||
edgeId,
|
||||
isOffDefaultPath,
|
||||
}: {
|
||||
state: SessionState
|
||||
edgeId?: string
|
||||
isOffDefaultPath: boolean
|
||||
}): Promise<NextGroup> => {
|
||||
const nextEdge = state.typebotsQueue[0].typebot.edges.find(byId(edgeId))
|
||||
if (!nextEdge) {
|
||||
if (state.typebotsQueue.length > 1) {
|
||||
const nextEdgeId = state.typebotsQueue[0].edgeIdToTriggerWhenDone
|
||||
const isMergingWithParent = state.typebotsQueue[0].isMergingWithParent
|
||||
const currentResultId = state.typebotsQueue[0].resultId
|
||||
if (!isMergingWithParent && currentResultId)
|
||||
await upsertResult({
|
||||
resultId: currentResultId,
|
||||
typebot: state.typebotsQueue[0].typebot,
|
||||
isCompleted: true,
|
||||
hasStarted: state.typebotsQueue[0].answers.length > 0,
|
||||
})
|
||||
let newSessionState = {
|
||||
...state,
|
||||
typebotsQueue: [
|
||||
{
|
||||
...state.typebotsQueue[1],
|
||||
typebot: isMergingWithParent
|
||||
? {
|
||||
...state.typebotsQueue[1].typebot,
|
||||
variables: state.typebotsQueue[1].typebot.variables
|
||||
.map((variable) => ({
|
||||
...variable,
|
||||
value:
|
||||
state.typebotsQueue[0].typebot.variables.find(
|
||||
(v) => v.name === variable.name
|
||||
)?.value ?? variable.value,
|
||||
}))
|
||||
.concat(
|
||||
state.typebotsQueue[0].typebot.variables.filter(
|
||||
(variable) =>
|
||||
isDefined(variable.value) &&
|
||||
isNotDefined(
|
||||
state.typebotsQueue[1].typebot.variables.find(
|
||||
(v) => v.name === variable.name
|
||||
)
|
||||
) as VariableWithValue[]
|
||||
),
|
||||
}
|
||||
: state.typebotsQueue[1].typebot,
|
||||
answers: isMergingWithParent
|
||||
? [
|
||||
...state.typebotsQueue[1].answers.filter(
|
||||
(incomingAnswer) =>
|
||||
!state.typebotsQueue[0].answers.find(
|
||||
(currentAnswer) =>
|
||||
currentAnswer.key === incomingAnswer.key
|
||||
)
|
||||
)
|
||||
) as VariableWithValue[]
|
||||
),
|
||||
...state.typebotsQueue[0].answers,
|
||||
]
|
||||
: state.typebotsQueue[1].answers,
|
||||
},
|
||||
...state.typebotsQueue.slice(2),
|
||||
],
|
||||
} satisfies SessionState
|
||||
if (state.progressMetadata)
|
||||
newSessionState.progressMetadata = {
|
||||
...state.progressMetadata,
|
||||
totalAnswers:
|
||||
state.progressMetadata.totalAnswers +
|
||||
state.typebotsQueue[0].answers.length,
|
||||
}
|
||||
const nextGroup = await getNextGroup(newSessionState)(nextEdgeId)
|
||||
newSessionState = nextGroup.newSessionState
|
||||
if (!nextGroup)
|
||||
return {
|
||||
newSessionState,
|
||||
}
|
||||
}
|
||||
: state.typebotsQueue[1].typebot,
|
||||
answers: isMergingWithParent
|
||||
? [
|
||||
...state.typebotsQueue[1].answers.filter(
|
||||
(incomingAnswer) =>
|
||||
!state.typebotsQueue[0].answers.find(
|
||||
(currentAnswer) =>
|
||||
currentAnswer.key === incomingAnswer.key
|
||||
)
|
||||
),
|
||||
...state.typebotsQueue[0].answers,
|
||||
]
|
||||
: state.typebotsQueue[1].answers,
|
||||
},
|
||||
...state.typebotsQueue.slice(2),
|
||||
],
|
||||
} satisfies SessionState
|
||||
if (state.progressMetadata)
|
||||
newSessionState.progressMetadata = {
|
||||
...state.progressMetadata,
|
||||
totalAnswers:
|
||||
state.progressMetadata.totalAnswers +
|
||||
state.typebotsQueue[0].answers.length,
|
||||
}
|
||||
const nextGroup = await getNextGroup({
|
||||
state: newSessionState,
|
||||
edgeId: nextEdgeId,
|
||||
isOffDefaultPath,
|
||||
})
|
||||
newSessionState = nextGroup.newSessionState
|
||||
if (!nextGroup)
|
||||
return {
|
||||
...nextGroup,
|
||||
newSessionState,
|
||||
}
|
||||
}
|
||||
return {
|
||||
newSessionState: state,
|
||||
...nextGroup,
|
||||
newSessionState,
|
||||
}
|
||||
}
|
||||
const nextGroup = state.typebotsQueue[0].typebot.groups.find(
|
||||
byId(nextEdge.to.groupId)
|
||||
)
|
||||
if (!nextGroup)
|
||||
return {
|
||||
newSessionState: state,
|
||||
}
|
||||
const startBlockIndex = nextEdge.to.blockId
|
||||
? nextGroup.blocks.findIndex(byId(nextEdge.to.blockId))
|
||||
: 0
|
||||
const currentVisitedEdgeIndex = (state.currentVisitedEdgeIndex ?? -1) + 1
|
||||
const resultId = state.typebotsQueue[0].resultId
|
||||
return {
|
||||
group: {
|
||||
...nextGroup,
|
||||
blocks: nextGroup.blocks.slice(startBlockIndex),
|
||||
} as Group,
|
||||
newSessionState: {
|
||||
...state,
|
||||
currentVisitedEdgeIndex,
|
||||
},
|
||||
visitedEdge: resultId
|
||||
newSessionState: state,
|
||||
}
|
||||
}
|
||||
const nextGroup = state.typebotsQueue[0].typebot.groups.find(
|
||||
byId(nextEdge.to.groupId)
|
||||
)
|
||||
if (!nextGroup)
|
||||
return {
|
||||
newSessionState: state,
|
||||
}
|
||||
const startBlockIndex = nextEdge.to.blockId
|
||||
? nextGroup.blocks.findIndex(byId(nextEdge.to.blockId))
|
||||
: 0
|
||||
const currentVisitedEdgeIndex = isOffDefaultPath
|
||||
? (state.currentVisitedEdgeIndex ?? -1) + 1
|
||||
: state.currentVisitedEdgeIndex
|
||||
const resultId = state.typebotsQueue[0].resultId
|
||||
return {
|
||||
group: {
|
||||
...nextGroup,
|
||||
blocks: nextGroup.blocks.slice(startBlockIndex),
|
||||
} as Group,
|
||||
newSessionState: {
|
||||
...state,
|
||||
currentVisitedEdgeIndex,
|
||||
previewMetadata:
|
||||
resultId || !isOffDefaultPath
|
||||
? state.previewMetadata
|
||||
: {
|
||||
...state.previewMetadata,
|
||||
visitedEdges: (state.previewMetadata?.visitedEdges ?? []).concat(
|
||||
nextEdge.id
|
||||
),
|
||||
},
|
||||
},
|
||||
visitedEdge:
|
||||
resultId && isOffDefaultPath && !nextEdge.id.startsWith('virtual-')
|
||||
? {
|
||||
index: currentVisitedEdgeIndex,
|
||||
index: currentVisitedEdgeIndex as number,
|
||||
edgeId: nextEdge.id,
|
||||
resultId,
|
||||
}
|
||||
: undefined,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -19,6 +19,7 @@
|
||||
"@typebot.io/tsconfig": "workspace:*",
|
||||
"@typebot.io/variables": "workspace:*",
|
||||
"@udecode/plate-common": "30.4.5",
|
||||
"@typebot.io/logic": "workspace:*",
|
||||
"ai": "3.0.31",
|
||||
"chrono-node": "2.7.5",
|
||||
"date-fns": "2.30.0",
|
||||
|
@ -6,7 +6,7 @@ import {
|
||||
Typebot,
|
||||
} from '@typebot.io/schemas'
|
||||
import { deepParseVariables } from '@typebot.io/variables/deepParseVariables'
|
||||
import { isEmpty, isNotEmpty } from '@typebot.io/lib/utils'
|
||||
import { isDefined, isEmpty, isNotEmpty } from '@typebot.io/lib/utils'
|
||||
import {
|
||||
getVariablesToParseInfoInText,
|
||||
parseVariables,
|
||||
@ -49,7 +49,7 @@ export const parseBubbleBlock = (
|
||||
richText: parseVariablesInRichText(block.content?.richText ?? [], {
|
||||
variables,
|
||||
takeLatestIfList: typebotVersion !== '6',
|
||||
}),
|
||||
}).parsedElements,
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -93,14 +93,15 @@ export const parseBubbleBlock = (
|
||||
}
|
||||
}
|
||||
|
||||
const parseVariablesInRichText = (
|
||||
export const parseVariablesInRichText = (
|
||||
elements: TDescendant[],
|
||||
{
|
||||
variables,
|
||||
takeLatestIfList,
|
||||
}: { variables: Variable[]; takeLatestIfList?: boolean }
|
||||
): TDescendant[] => {
|
||||
): { parsedElements: TDescendant[]; parsedVariableIds: string[] } => {
|
||||
const parsedElements: TDescendant[] = []
|
||||
const parsedVariableIds: string[] = []
|
||||
for (const element of elements) {
|
||||
if ('text' in element) {
|
||||
const text = element.text as string
|
||||
@ -112,6 +113,9 @@ const parseVariablesInRichText = (
|
||||
variables,
|
||||
takeLatestIfList,
|
||||
})
|
||||
parsedVariableIds.push(
|
||||
...variablesInText.map((v) => v.variableId).filter(isDefined)
|
||||
)
|
||||
if (variablesInText.length === 0) {
|
||||
parsedElements.push(element)
|
||||
continue
|
||||
@ -185,19 +189,28 @@ const parseVariablesInRichText = (
|
||||
? 'variable'
|
||||
: element.type
|
||||
|
||||
const {
|
||||
parsedElements: parsedChildren,
|
||||
parsedVariableIds: parsedChildrenVariableIds,
|
||||
} = parseVariablesInRichText(element.children as TDescendant[], {
|
||||
variables,
|
||||
takeLatestIfList,
|
||||
})
|
||||
|
||||
parsedVariableIds.push(...parsedChildrenVariableIds)
|
||||
parsedElements.push({
|
||||
...element,
|
||||
url: element.url
|
||||
? parseVariables(variables)(element.url as string)
|
||||
: undefined,
|
||||
type,
|
||||
children: parseVariablesInRichText(element.children as TDescendant[], {
|
||||
variables,
|
||||
takeLatestIfList,
|
||||
}),
|
||||
children: parsedChildren,
|
||||
})
|
||||
}
|
||||
return parsedElements
|
||||
return {
|
||||
parsedElements,
|
||||
parsedVariableIds,
|
||||
}
|
||||
}
|
||||
|
||||
const applyElementStyleToDescendants = (
|
||||
|
@ -12,11 +12,26 @@ export const createSession = ({
|
||||
id,
|
||||
state,
|
||||
isReplying,
|
||||
}: Props): Prisma.PrismaPromise<any> =>
|
||||
prisma.chatSession.create({
|
||||
data: {
|
||||
}: Props): Prisma.PrismaPromise<any> => {
|
||||
if (!id) {
|
||||
return prisma.chatSession.create({
|
||||
data: {
|
||||
id,
|
||||
state,
|
||||
isReplying,
|
||||
},
|
||||
})
|
||||
}
|
||||
return prisma.chatSession.upsert({
|
||||
where: { id },
|
||||
update: {
|
||||
state,
|
||||
isReplying,
|
||||
},
|
||||
create: {
|
||||
id,
|
||||
state,
|
||||
isReplying,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
@ -4,24 +4,33 @@ import { Answer, Result } from '@typebot.io/schemas'
|
||||
type Props = {
|
||||
id: string
|
||||
}
|
||||
export const findResult = ({ id }: Props) =>
|
||||
prisma.result.findFirst({
|
||||
where: { id, isArchived: { not: true } },
|
||||
select: {
|
||||
id: true,
|
||||
variables: true,
|
||||
hasStarted: true,
|
||||
answers: {
|
||||
select: {
|
||||
content: true,
|
||||
blockId: true,
|
||||
variableId: true,
|
||||
export const findResult = async ({ id }: Props) => {
|
||||
const { answers, answersV2, ...result } =
|
||||
(await prisma.result.findFirst({
|
||||
where: { id, isArchived: { not: true } },
|
||||
select: {
|
||||
id: true,
|
||||
variables: true,
|
||||
hasStarted: true,
|
||||
answers: {
|
||||
select: {
|
||||
content: true,
|
||||
blockId: true,
|
||||
},
|
||||
},
|
||||
answersV2: {
|
||||
select: {
|
||||
content: true,
|
||||
blockId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}) as Promise<
|
||||
| (Pick<Result, 'id' | 'variables' | 'hasStarted'> & {
|
||||
answers: Pick<Answer, 'content' | 'blockId' | 'variableId'>[]
|
||||
})
|
||||
| null
|
||||
>
|
||||
})) ?? {}
|
||||
if (!result) return null
|
||||
return {
|
||||
...result,
|
||||
answers: (answersV2 ?? []).concat(answers ?? []),
|
||||
} as Pick<Result, 'id' | 'variables' | 'hasStarted'> & {
|
||||
answers: Pick<Answer, 'content' | 'blockId'>[]
|
||||
}
|
||||
}
|
||||
|
16
packages/bot-engine/queries/saveAnswer.ts
Normal file
16
packages/bot-engine/queries/saveAnswer.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import prisma from '@typebot.io/lib/prisma'
|
||||
import { Prisma } from '@typebot.io/prisma'
|
||||
import { SessionState } from '@typebot.io/schemas'
|
||||
|
||||
type Props = {
|
||||
answer: Omit<Prisma.AnswerV2CreateManyInput, 'resultId'>
|
||||
reply: string
|
||||
state: SessionState
|
||||
}
|
||||
export const saveAnswer = async ({ answer, state }: Props) => {
|
||||
const resultId = state.typebotsQueue[0].resultId
|
||||
if (!resultId) return
|
||||
return prisma.answerV2.createMany({
|
||||
data: [{ ...answer, resultId }],
|
||||
})
|
||||
}
|
16
packages/bot-engine/queries/saveSetVariableHistoryItems.ts
Normal file
16
packages/bot-engine/queries/saveSetVariableHistoryItems.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import prisma from '@typebot.io/lib/prisma'
|
||||
import { Prisma } from '@typebot.io/prisma'
|
||||
import { SetVariableHistoryItem } from '@typebot.io/schemas'
|
||||
|
||||
export const saveSetVariableHistoryItems = (
|
||||
setVariableHistory: SetVariableHistoryItem[]
|
||||
) =>
|
||||
prisma.setVariableHistoryItem.createMany({
|
||||
data: {
|
||||
...setVariableHistory.map((item) => ({
|
||||
...item,
|
||||
value: item.value === null ? Prisma.JsonNull : item.value,
|
||||
})),
|
||||
},
|
||||
skipDuplicates: true,
|
||||
})
|
@ -1,34 +0,0 @@
|
||||
import prisma from '@typebot.io/lib/prisma'
|
||||
import { Prisma } from '@typebot.io/prisma'
|
||||
import { InputBlock, SessionState } from '@typebot.io/schemas'
|
||||
|
||||
type Props = {
|
||||
answer: Omit<Prisma.AnswerUncheckedCreateInput, 'resultId'>
|
||||
reply: string
|
||||
state: SessionState
|
||||
}
|
||||
export const upsertAnswer = async ({ answer, state }: Props) => {
|
||||
const resultId = state.typebotsQueue[0].resultId
|
||||
if (!resultId) return
|
||||
const where = {
|
||||
resultId,
|
||||
blockId: answer.blockId,
|
||||
groupId: answer.groupId,
|
||||
}
|
||||
const existingAnswer = await prisma.answer.findUnique({
|
||||
where: {
|
||||
resultId_blockId_groupId: where,
|
||||
},
|
||||
select: { resultId: true },
|
||||
})
|
||||
if (existingAnswer)
|
||||
return prisma.answer.updateMany({
|
||||
where,
|
||||
data: {
|
||||
content: answer.content,
|
||||
},
|
||||
})
|
||||
return prisma.answer.createMany({
|
||||
data: [{ ...answer, resultId }],
|
||||
})
|
||||
}
|
@ -1,29 +1,79 @@
|
||||
import prisma from '@typebot.io/lib/prisma'
|
||||
import { Prisma } from '@typebot.io/prisma'
|
||||
import { TypebotInSession } from '@typebot.io/schemas'
|
||||
import { Prisma, SetVariableHistoryItem, VisitedEdge } from '@typebot.io/prisma'
|
||||
import { ContinueChatResponse, TypebotInSession } from '@typebot.io/schemas'
|
||||
import { filterNonSessionVariablesWithValues } from '@typebot.io/variables/filterVariablesWithValues'
|
||||
import { formatLogDetails } from '../logs/helpers/formatLogDetails'
|
||||
|
||||
type Props = {
|
||||
resultId: string
|
||||
typebot: TypebotInSession
|
||||
hasStarted: boolean
|
||||
isCompleted: boolean
|
||||
lastChatSessionId?: string
|
||||
logs?: ContinueChatResponse['logs']
|
||||
visitedEdges?: VisitedEdge[]
|
||||
setVariableHistory?: SetVariableHistoryItem[]
|
||||
}
|
||||
export const upsertResult = ({
|
||||
resultId,
|
||||
typebot,
|
||||
hasStarted,
|
||||
isCompleted,
|
||||
lastChatSessionId,
|
||||
logs,
|
||||
visitedEdges,
|
||||
setVariableHistory,
|
||||
}: Props): Prisma.PrismaPromise<any> => {
|
||||
const variablesWithValue = filterNonSessionVariablesWithValues(
|
||||
typebot.variables
|
||||
)
|
||||
const logsToCreate =
|
||||
logs && logs.length > 0
|
||||
? {
|
||||
createMany: {
|
||||
data: logs.map((log) => ({
|
||||
...log,
|
||||
details: formatLogDetails(log.details),
|
||||
})),
|
||||
},
|
||||
}
|
||||
: undefined
|
||||
|
||||
const setVariableHistoryToCreate =
|
||||
setVariableHistory && setVariableHistory.length > 0
|
||||
? ({
|
||||
createMany: {
|
||||
data: setVariableHistory.map((item) => ({
|
||||
...item,
|
||||
value: item.value === null ? Prisma.JsonNull : item.value,
|
||||
resultId: undefined,
|
||||
})),
|
||||
},
|
||||
} as Prisma.SetVariableHistoryItemUpdateManyWithoutResultNestedInput)
|
||||
: undefined
|
||||
|
||||
const visitedEdgesToCreate =
|
||||
visitedEdges && visitedEdges.length > 0
|
||||
? {
|
||||
createMany: {
|
||||
data: visitedEdges.map((edge) => ({
|
||||
...edge,
|
||||
resultId: undefined,
|
||||
})),
|
||||
},
|
||||
}
|
||||
: undefined
|
||||
|
||||
return prisma.result.upsert({
|
||||
where: { id: resultId },
|
||||
update: {
|
||||
isCompleted: isCompleted ? true : undefined,
|
||||
hasStarted,
|
||||
variables: variablesWithValue,
|
||||
lastChatSessionId,
|
||||
logs: logsToCreate,
|
||||
setVariableHistory: setVariableHistoryToCreate,
|
||||
edges: visitedEdgesToCreate,
|
||||
},
|
||||
create: {
|
||||
id: resultId,
|
||||
@ -31,6 +81,10 @@ export const upsertResult = ({
|
||||
isCompleted: isCompleted ? true : false,
|
||||
hasStarted,
|
||||
variables: variablesWithValue,
|
||||
lastChatSessionId,
|
||||
logs: logsToCreate,
|
||||
setVariableHistory: setVariableHistoryToCreate,
|
||||
edges: visitedEdgesToCreate,
|
||||
},
|
||||
select: { id: true },
|
||||
})
|
||||
|
@ -1,12 +1,12 @@
|
||||
import { ContinueChatResponse, ChatSession } from '@typebot.io/schemas'
|
||||
import {
|
||||
ContinueChatResponse,
|
||||
ChatSession,
|
||||
SetVariableHistoryItem,
|
||||
} from '@typebot.io/schemas'
|
||||
import { upsertResult } from './queries/upsertResult'
|
||||
import { saveLogs } from './queries/saveLogs'
|
||||
import { updateSession } from './queries/updateSession'
|
||||
import { formatLogDetails } from './logs/helpers/formatLogDetails'
|
||||
import { createSession } from './queries/createSession'
|
||||
import { deleteSession } from './queries/deleteSession'
|
||||
import * as Sentry from '@sentry/nextjs'
|
||||
import { saveVisitedEdges } from './queries/saveVisitedEdges'
|
||||
import { Prisma, VisitedEdge } from '@typebot.io/prisma'
|
||||
import prisma from '@typebot.io/lib/prisma'
|
||||
|
||||
@ -16,7 +16,9 @@ type Props = {
|
||||
logs: ContinueChatResponse['logs']
|
||||
clientSideActions: ContinueChatResponse['clientSideActions']
|
||||
visitedEdges: VisitedEdge[]
|
||||
setVariableHistory: SetVariableHistoryItem[]
|
||||
hasCustomEmbedBubble?: boolean
|
||||
initialSessionId?: string
|
||||
}
|
||||
|
||||
export const saveStateToDatabase = async ({
|
||||
@ -25,7 +27,9 @@ export const saveStateToDatabase = async ({
|
||||
logs,
|
||||
clientSideActions,
|
||||
visitedEdges,
|
||||
setVariableHistory,
|
||||
hasCustomEmbedBubble,
|
||||
initialSessionId,
|
||||
}: Props) => {
|
||||
const containsSetVariableClientSideAction = clientSideActions?.some(
|
||||
(action) => action.expectsDedicatedReply
|
||||
@ -46,7 +50,7 @@ export const saveStateToDatabase = async ({
|
||||
|
||||
const session = id
|
||||
? { state, id }
|
||||
: await createSession({ id, state, isReplying: false })
|
||||
: await createSession({ id: initialSessionId, state, isReplying: false })
|
||||
|
||||
if (!resultId) {
|
||||
if (queries.length > 0) await prisma.$transaction(queries)
|
||||
@ -63,25 +67,13 @@ export const saveStateToDatabase = async ({
|
||||
!input && !containsSetVariableClientSideAction && answers.length > 0
|
||||
),
|
||||
hasStarted: answers.length > 0,
|
||||
lastChatSessionId: session.id,
|
||||
logs,
|
||||
visitedEdges,
|
||||
setVariableHistory,
|
||||
})
|
||||
)
|
||||
|
||||
if (logs && logs.length > 0)
|
||||
try {
|
||||
await saveLogs(
|
||||
logs.map((log) => ({
|
||||
...log,
|
||||
resultId,
|
||||
details: formatLogDetails(log.details),
|
||||
}))
|
||||
)
|
||||
} catch (e) {
|
||||
console.error('Failed to save logs', e)
|
||||
Sentry.captureException(e)
|
||||
}
|
||||
|
||||
if (visitedEdges.length > 0) queries.push(saveVisitedEdges(visitedEdges))
|
||||
|
||||
await prisma.$transaction(queries)
|
||||
|
||||
return session
|
||||
|
@ -2,6 +2,7 @@ import { TRPCError } from '@trpc/server'
|
||||
import {
|
||||
ContinueChatResponse,
|
||||
SessionState,
|
||||
SetVariableHistoryItem,
|
||||
StartFrom,
|
||||
} from '@typebot.io/schemas'
|
||||
import { executeGroup } from './executeGroup'
|
||||
@ -25,10 +26,12 @@ export const startBotFlow = async ({
|
||||
ContinueChatResponse & {
|
||||
newSessionState: SessionState
|
||||
visitedEdges: VisitedEdge[]
|
||||
setVariableHistory: SetVariableHistoryItem[]
|
||||
}
|
||||
> => {
|
||||
let newSessionState = state
|
||||
const visitedEdges: VisitedEdge[] = []
|
||||
const setVariableHistory: SetVariableHistoryItem[] = []
|
||||
if (startFrom?.type === 'group') {
|
||||
const group = state.typebotsQueue[0].typebot.groups.find(
|
||||
(group) => group.id === startFrom.groupId
|
||||
@ -42,22 +45,34 @@ export const startBotFlow = async ({
|
||||
version,
|
||||
state: newSessionState,
|
||||
visitedEdges,
|
||||
setVariableHistory,
|
||||
startTime,
|
||||
})
|
||||
}
|
||||
const firstEdgeId = getFirstEdgeId({
|
||||
state: newSessionState,
|
||||
typebot: newSessionState.typebotsQueue[0].typebot,
|
||||
startEventId: startFrom?.type === 'event' ? startFrom.eventId : undefined,
|
||||
})
|
||||
if (!firstEdgeId) return { messages: [], newSessionState, visitedEdges: [] }
|
||||
const nextGroup = await getNextGroup(newSessionState)(firstEdgeId)
|
||||
if (!firstEdgeId)
|
||||
return {
|
||||
messages: [],
|
||||
newSessionState,
|
||||
setVariableHistory: [],
|
||||
visitedEdges: [],
|
||||
}
|
||||
const nextGroup = await getNextGroup({
|
||||
state: newSessionState,
|
||||
edgeId: firstEdgeId,
|
||||
isOffDefaultPath: false,
|
||||
})
|
||||
newSessionState = nextGroup.newSessionState
|
||||
if (nextGroup.visitedEdge) visitedEdges.push(nextGroup.visitedEdge)
|
||||
if (!nextGroup.group) return { messages: [], newSessionState, visitedEdges }
|
||||
if (!nextGroup.group)
|
||||
return { messages: [], newSessionState, visitedEdges, setVariableHistory }
|
||||
return executeGroup(nextGroup.group, {
|
||||
version,
|
||||
state: newSessionState,
|
||||
visitedEdges,
|
||||
setVariableHistory,
|
||||
startTime,
|
||||
})
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ import {
|
||||
SessionState,
|
||||
TypebotInSession,
|
||||
Block,
|
||||
SetVariableHistoryItem,
|
||||
} from '@typebot.io/schemas'
|
||||
import {
|
||||
StartChatInput,
|
||||
@ -31,7 +32,10 @@ import { injectVariablesFromExistingResult } from '@typebot.io/variables/injectV
|
||||
import { getNextGroup } from './getNextGroup'
|
||||
import { upsertResult } from './queries/upsertResult'
|
||||
import { continueBotFlow } from './continueBotFlow'
|
||||
import { parseVariables } from '@typebot.io/variables/parseVariables'
|
||||
import {
|
||||
getVariablesToParseInfoInText,
|
||||
parseVariables,
|
||||
} from '@typebot.io/variables/parseVariables'
|
||||
import { defaultSettings } from '@typebot.io/schemas/features/typebot/settings/constants'
|
||||
import { IntegrationBlockType } from '@typebot.io/schemas/features/blocks/integrations/constants'
|
||||
import { VisitedEdge } from '@typebot.io/prisma'
|
||||
@ -42,6 +46,9 @@ import {
|
||||
defaultGuestAvatarIsEnabled,
|
||||
defaultHostAvatarIsEnabled,
|
||||
} from '@typebot.io/schemas/features/typebot/theme/constants'
|
||||
import { BubbleBlockType } from '@typebot.io/schemas/features/blocks/bubbles/constants'
|
||||
import { LogicBlockType } from '@typebot.io/schemas/features/blocks/logic/constants'
|
||||
import { parseVariablesInRichText } from './parseBubbleBlock'
|
||||
|
||||
type StartParams =
|
||||
| ({
|
||||
@ -68,6 +75,7 @@ export const startSession = async ({
|
||||
Omit<StartChatResponse, 'resultId' | 'isStreamEnabled' | 'sessionId'> & {
|
||||
newSessionState: SessionState
|
||||
visitedEdges: VisitedEdge[]
|
||||
setVariableHistory: SetVariableHistoryItem[]
|
||||
resultId?: string
|
||||
}
|
||||
> => {
|
||||
@ -145,6 +153,8 @@ export const startSession = async ({
|
||||
: typebot.theme.general?.progressBar?.isEnabled
|
||||
? { totalAnswers: 0 }
|
||||
: undefined,
|
||||
setVariableIdsForHistory:
|
||||
extractVariableIdsUsedForTranscript(typebotInSession),
|
||||
...initialSessionState,
|
||||
}
|
||||
|
||||
@ -164,6 +174,7 @@ export const startSession = async ({
|
||||
dynamicTheme: parseDynamicTheme(initialState),
|
||||
messages: [],
|
||||
visitedEdges: [],
|
||||
setVariableHistory: [],
|
||||
}
|
||||
}
|
||||
|
||||
@ -178,14 +189,18 @@ export const startSession = async ({
|
||||
// If params has message and first block is an input block, we can directly continue the bot flow
|
||||
if (message) {
|
||||
const firstEdgeId = getFirstEdgeId({
|
||||
state: chatReply.newSessionState,
|
||||
typebot: chatReply.newSessionState.typebotsQueue[0].typebot,
|
||||
startEventId:
|
||||
startParams.type === 'preview' &&
|
||||
startParams.startFrom?.type === 'event'
|
||||
? startParams.startFrom.eventId
|
||||
: undefined,
|
||||
})
|
||||
const nextGroup = await getNextGroup(chatReply.newSessionState)(firstEdgeId)
|
||||
const nextGroup = await getNextGroup({
|
||||
state: chatReply.newSessionState,
|
||||
edgeId: firstEdgeId,
|
||||
isOffDefaultPath: false,
|
||||
})
|
||||
const newSessionState = nextGroup.newSessionState
|
||||
const firstBlock = nextGroup.group?.blocks.at(0)
|
||||
if (firstBlock && isInputBlock(firstBlock)) {
|
||||
@ -214,6 +229,7 @@ export const startSession = async ({
|
||||
newSessionState,
|
||||
logs,
|
||||
visitedEdges,
|
||||
setVariableHistory,
|
||||
} = chatReply
|
||||
|
||||
const clientSideActions = startFlowClientActions ?? []
|
||||
@ -268,6 +284,7 @@ export const startSession = async ({
|
||||
dynamicTheme: parseDynamicTheme(newSessionState),
|
||||
logs: startLogs.length > 0 ? startLogs : undefined,
|
||||
visitedEdges,
|
||||
setVariableHistory,
|
||||
}
|
||||
|
||||
return {
|
||||
@ -290,6 +307,7 @@ export const startSession = async ({
|
||||
dynamicTheme: parseDynamicTheme(newSessionState),
|
||||
logs: startLogs.length > 0 ? startLogs : undefined,
|
||||
visitedEdges,
|
||||
setVariableHistory,
|
||||
}
|
||||
}
|
||||
|
||||
@ -497,3 +515,59 @@ const convertStartTypebotToTypebotInSession = (
|
||||
variables: startVariables,
|
||||
events: typebot.events,
|
||||
}
|
||||
|
||||
const extractVariableIdsUsedForTranscript = (
|
||||
typebot: TypebotInSession
|
||||
): string[] => {
|
||||
const variableIds: Set<string> = new Set()
|
||||
const parseVarParams = {
|
||||
variables: typebot.variables,
|
||||
takeLatestIfList: typebot.version !== '6',
|
||||
}
|
||||
typebot.groups.forEach((group) => {
|
||||
group.blocks.forEach((block) => {
|
||||
if (block.type === BubbleBlockType.TEXT) {
|
||||
const { parsedVariableIds } = parseVariablesInRichText(
|
||||
block.content?.richText ?? [],
|
||||
parseVarParams
|
||||
)
|
||||
parsedVariableIds.forEach((variableId) => variableIds.add(variableId))
|
||||
}
|
||||
if (
|
||||
block.type === BubbleBlockType.IMAGE ||
|
||||
block.type === BubbleBlockType.VIDEO ||
|
||||
block.type === BubbleBlockType.AUDIO
|
||||
) {
|
||||
if (!block.content?.url) return
|
||||
const variablesInfo = getVariablesToParseInfoInText(
|
||||
block.content.url,
|
||||
parseVarParams
|
||||
)
|
||||
variablesInfo.forEach((variableInfo) =>
|
||||
variableInfo.variableId
|
||||
? variableIds.add(variableInfo.variableId ?? '')
|
||||
: undefined
|
||||
)
|
||||
}
|
||||
if (block.type === LogicBlockType.CONDITION) {
|
||||
block.items.forEach((item) =>
|
||||
item.content?.comparisons?.forEach((comparison) => {
|
||||
if (comparison.variableId) variableIds.add(comparison.variableId)
|
||||
if (comparison.value) {
|
||||
const variableIdsInValue = getVariablesToParseInfoInText(
|
||||
comparison.value,
|
||||
parseVarParams
|
||||
)
|
||||
variableIdsInValue.forEach((variableInfo) => {
|
||||
variableInfo.variableId
|
||||
? variableIds.add(variableInfo.variableId)
|
||||
: undefined
|
||||
})
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
return [...variableIds]
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ import {
|
||||
ContinueChatResponse,
|
||||
CustomEmbedBubble,
|
||||
SessionState,
|
||||
SetVariableHistoryItem,
|
||||
} from '@typebot.io/schemas'
|
||||
|
||||
export type EdgeId = string
|
||||
@ -9,6 +10,7 @@ export type EdgeId = string
|
||||
export type ExecuteLogicResponse = {
|
||||
outgoingEdgeId: EdgeId | undefined
|
||||
newSessionState?: SessionState
|
||||
newSetVariableHistory?: SetVariableHistoryItem[]
|
||||
} & Pick<ContinueChatResponse, 'clientSideActions' | 'logs'>
|
||||
|
||||
export type ExecuteIntegrationResponse = {
|
||||
@ -16,6 +18,7 @@ export type ExecuteIntegrationResponse = {
|
||||
newSessionState?: SessionState
|
||||
startTimeShouldBeUpdated?: boolean
|
||||
customEmbedBubble?: CustomEmbedBubble
|
||||
newSetVariableHistory?: SetVariableHistoryItem[]
|
||||
} & Pick<ContinueChatResponse, 'clientSideActions' | 'logs'>
|
||||
|
||||
type WhatsAppMediaMessage = {
|
||||
|
@ -114,6 +114,7 @@ export const resumeWhatsAppFlow = async ({
|
||||
messages,
|
||||
clientSideActions,
|
||||
visitedEdges,
|
||||
setVariableHistory,
|
||||
} = resumeResponse
|
||||
|
||||
const isFirstChatChunk = (!session || isSessionExpired) ?? false
|
||||
@ -140,6 +141,7 @@ export const resumeWhatsAppFlow = async ({
|
||||
},
|
||||
},
|
||||
visitedEdges,
|
||||
setVariableHistory,
|
||||
})
|
||||
|
||||
return {
|
||||
|
@ -3,6 +3,7 @@ import {
|
||||
ContinueChatResponse,
|
||||
PublicTypebot,
|
||||
SessionState,
|
||||
SetVariableHistoryItem,
|
||||
Settings,
|
||||
Typebot,
|
||||
} from '@typebot.io/schemas'
|
||||
@ -35,6 +36,7 @@ export const startWhatsAppSession = async ({
|
||||
| (ContinueChatResponse & {
|
||||
newSessionState: SessionState
|
||||
visitedEdges: VisitedEdge[]
|
||||
setVariableHistory: SetVariableHistoryItem[]
|
||||
})
|
||||
| { error: string }
|
||||
> => {
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@typebot.io/js",
|
||||
"version": "0.2.81",
|
||||
"version": "0.2.82",
|
||||
"description": "Javascript library to display typebots on your website",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
|
@ -39,13 +39,14 @@ export type BotProps = {
|
||||
apiHost?: string
|
||||
font?: Font
|
||||
progressBarRef?: HTMLDivElement
|
||||
startFrom?: StartFrom
|
||||
sessionId?: string
|
||||
onNewInputBlock?: (inputBlock: InputBlock) => void
|
||||
onAnswer?: (answer: { message: string; blockId: string }) => void
|
||||
onInit?: () => void
|
||||
onEnd?: () => void
|
||||
onNewLogs?: (logs: OutgoingLog[]) => void
|
||||
onChatStatePersisted?: (isEnabled: boolean) => void
|
||||
startFrom?: StartFrom
|
||||
}
|
||||
|
||||
export const Bot = (props: BotProps & { class?: string }) => {
|
||||
@ -81,6 +82,7 @@ export const Bot = (props: BotProps & { class?: string }) => {
|
||||
...props.prefilledVariables,
|
||||
},
|
||||
startFrom: props.startFrom,
|
||||
sessionId: props.sessionId,
|
||||
})
|
||||
if (error instanceof HTTPError) {
|
||||
if (isPreview) {
|
||||
|
@ -14,6 +14,7 @@ export const defaultBotProps: BotProps = {
|
||||
prefilledVariables: undefined,
|
||||
apiHost: undefined,
|
||||
resultId: undefined,
|
||||
sessionId: undefined,
|
||||
}
|
||||
|
||||
export const defaultPopupProps: PopupProps = {
|
||||
|
@ -21,6 +21,7 @@ type Props = {
|
||||
isPreview: boolean
|
||||
prefilledVariables?: Record<string, unknown>
|
||||
resultId?: string
|
||||
sessionId?: string
|
||||
}
|
||||
|
||||
export async function startChatQuery({
|
||||
@ -31,6 +32,7 @@ export async function startChatQuery({
|
||||
resultId,
|
||||
stripeRedirectStatus,
|
||||
startFrom,
|
||||
sessionId,
|
||||
}: Props) {
|
||||
if (isNotDefined(typebot))
|
||||
throw new Error('Typebot ID is required to get initial messages')
|
||||
@ -83,6 +85,7 @@ export async function startChatQuery({
|
||||
startFrom,
|
||||
typebot,
|
||||
prefilledVariables,
|
||||
sessionId,
|
||||
} satisfies Omit<
|
||||
StartPreviewChatInput,
|
||||
'typebotId' | 'isOnlyRegistering'
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@typebot.io/nextjs",
|
||||
"version": "0.2.81",
|
||||
"version": "0.2.82",
|
||||
"description": "Convenient library to display typebots on your Next.js website",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@typebot.io/react",
|
||||
"version": "0.2.81",
|
||||
"version": "0.2.82",
|
||||
"description": "Convenient library to display typebots on your React app",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
|
397
packages/logic/computeResultTranscript.ts
Normal file
397
packages/logic/computeResultTranscript.ts
Normal file
@ -0,0 +1,397 @@
|
||||
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 { convertRichTextToMarkdown } from '@typebot.io/lib/markdown/convertRichTextToMarkdown'
|
||||
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'>[]
|
||||
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'>[]
|
||||
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,
|
||||
}
|
||||
)
|
||||
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 variable = typebotsQueue[0].typebot.variables.find(
|
||||
(variable) => variable.id === block.options?.variableId
|
||||
)
|
||||
if (variable) {
|
||||
typebotsQueue[0].typebot.variables =
|
||||
typebotsQueue[0].typebot.variables.map((v) =>
|
||||
v.id === variable.id ? { ...v, value: answer.content } : v
|
||||
)
|
||||
}
|
||||
}
|
||||
currentTranscript.push({
|
||||
role: 'user',
|
||||
type: 'text',
|
||||
text: 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)
|
||||
if (!isLinkingSameTypebot) 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,
|
||||
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.richText) return null
|
||||
return {
|
||||
role: 'bot',
|
||||
type: 'text',
|
||||
text: convertRichTextToMarkdown(chatMessage.content.richText),
|
||||
}
|
||||
}
|
||||
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 }
|
||||
}
|
@ -8,15 +8,18 @@ import {
|
||||
defaultConditionItemContent,
|
||||
} from '@typebot.io/schemas/features/blocks/logic/condition/constants'
|
||||
|
||||
export const executeCondition =
|
||||
(variables: Variable[]) =>
|
||||
(condition: Condition): boolean => {
|
||||
if (!condition.comparisons) return false
|
||||
return (condition.logicalOperator ??
|
||||
defaultConditionItemContent.logicalOperator) === LogicalOperator.AND
|
||||
? condition.comparisons.every(executeComparison(variables))
|
||||
: condition.comparisons.some(executeComparison(variables))
|
||||
}
|
||||
type Props = {
|
||||
condition: Condition
|
||||
variables: Variable[]
|
||||
}
|
||||
|
||||
export const executeCondition = ({ condition, variables }: Props): boolean => {
|
||||
if (!condition.comparisons) return false
|
||||
return (condition.logicalOperator ??
|
||||
defaultConditionItemContent.logicalOperator) === LogicalOperator.AND
|
||||
? condition.comparisons.every(executeComparison(variables))
|
||||
: condition.comparisons.some(executeComparison(variables))
|
||||
}
|
||||
|
||||
const executeComparison =
|
||||
(variables: Variable[]) =>
|
18
packages/logic/package.json
Normal file
18
packages/logic/package.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "@typebot.io/logic",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"scripts": {},
|
||||
"keywords": [],
|
||||
"author": "Baptiste Arnaud",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@typebot.io/schemas": "workspace:*",
|
||||
"@typebot.io/lib": "workspace:*",
|
||||
"@typebot.io/variables": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@typebot.io/tsconfig": "workspace:*",
|
||||
"@udecode/plate-common": "30.4.5"
|
||||
}
|
||||
}
|
12
packages/logic/tsconfig.json
Normal file
12
packages/logic/tsconfig.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": "@typebot.io/tsconfig/base.json",
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"../variables/parseVariables.ts",
|
||||
"../bot-engine/parseBubbleBlock.ts"
|
||||
],
|
||||
"exclude": ["node_modules"],
|
||||
"compilerOptions": {
|
||||
"lib": ["ES2021", "DOM"]
|
||||
}
|
||||
}
|
@ -58,13 +58,12 @@ const createAnswers = ({
|
||||
count,
|
||||
resultIdPrefix,
|
||||
}: { resultIdPrefix: string } & Pick<CreateFakeResultsProps, 'count'>) => {
|
||||
return prisma.answer.createMany({
|
||||
return prisma.answerV2.createMany({
|
||||
data: [
|
||||
...Array.from(Array(count)).map((_, idx) => ({
|
||||
resultId: `${resultIdPrefix}-result${idx}`,
|
||||
content: `content${idx}`,
|
||||
blockId: 'block1',
|
||||
groupId: 'group1',
|
||||
})),
|
||||
],
|
||||
})
|
||||
|
@ -255,22 +255,36 @@ model PublicTypebot {
|
||||
}
|
||||
|
||||
model Result {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
typebotId String
|
||||
variables Json
|
||||
isCompleted Boolean
|
||||
hasStarted Boolean?
|
||||
isArchived Boolean? @default(false)
|
||||
typebot Typebot @relation(fields: [typebotId], references: [id], onDelete: Cascade)
|
||||
answers Answer[]
|
||||
logs Log[]
|
||||
edges VisitedEdge[]
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
typebotId String
|
||||
variables Json
|
||||
isCompleted Boolean
|
||||
hasStarted Boolean?
|
||||
isArchived Boolean? @default(false)
|
||||
lastChatSessionId String?
|
||||
typebot Typebot @relation(fields: [typebotId], references: [id], onDelete: Cascade)
|
||||
answers Answer[]
|
||||
logs Log[]
|
||||
edges VisitedEdge[]
|
||||
setVariableHistory SetVariableHistoryItem[]
|
||||
answersV2 AnswerV2[]
|
||||
|
||||
@@index([typebotId, isArchived, hasStarted, createdAt(sort: Desc)])
|
||||
@@index([typebotId, isArchived, isCompleted])
|
||||
}
|
||||
|
||||
model SetVariableHistoryItem {
|
||||
result Result @relation(fields: [resultId], references: [id], onDelete: Cascade)
|
||||
resultId String
|
||||
index Int
|
||||
variableId String
|
||||
blockId String
|
||||
value Json // string or list of strings
|
||||
|
||||
@@unique([resultId, index])
|
||||
}
|
||||
|
||||
model VisitedEdge {
|
||||
result Result @relation(fields: [resultId], references: [id], onDelete: Cascade)
|
||||
resultId String
|
||||
@ -292,20 +306,28 @@ model Log {
|
||||
@@index([resultId])
|
||||
}
|
||||
|
||||
// TODO: gradually remove variableId and groupId
|
||||
model Answer {
|
||||
createdAt DateTime @default(now()) @updatedAt
|
||||
resultId String
|
||||
blockId String
|
||||
itemId String?
|
||||
groupId String
|
||||
variableId String?
|
||||
content String @db.Text
|
||||
storageUsed Int?
|
||||
result Result @relation(fields: [resultId], references: [id], onDelete: Cascade)
|
||||
createdAt DateTime @default(now()) @updatedAt
|
||||
resultId String
|
||||
blockId String
|
||||
groupId String
|
||||
variableId String?
|
||||
content String @db.Text
|
||||
result Result @relation(fields: [resultId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([resultId, blockId, groupId])
|
||||
@@index([blockId, itemId])
|
||||
@@index([storageUsed])
|
||||
}
|
||||
|
||||
model AnswerV2 {
|
||||
id Int @id @default(autoincrement())
|
||||
blockId String
|
||||
content String
|
||||
resultId String
|
||||
result Result @relation(fields: [resultId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([resultId])
|
||||
@@index([blockId])
|
||||
}
|
||||
|
||||
model Coupon {
|
||||
|
@ -0,0 +1,47 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `itemId` on the `Answer` table. All the data in the column will be lost.
|
||||
- You are about to drop the column `storageUsed` on the `Answer` table. All the data in the column will be lost.
|
||||
|
||||
*/
|
||||
-- DropIndex
|
||||
DROP INDEX "Answer_blockId_itemId_idx";
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Answer" DROP COLUMN "itemId",
|
||||
DROP COLUMN "storageUsed";
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Result" ADD COLUMN "lastChatSessionId" TEXT;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "SetVariableHistoryItem" (
|
||||
"resultId" TEXT NOT NULL,
|
||||
"index" INTEGER NOT NULL,
|
||||
"variableId" TEXT NOT NULL,
|
||||
"blockId" TEXT NOT NULL,
|
||||
"value" JSONB NOT NULL
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "AnswerV2" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"blockId" TEXT NOT NULL,
|
||||
"content" TEXT NOT NULL,
|
||||
"resultId" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "AnswerV2_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "SetVariableHistoryItem_resultId_index_key" ON "SetVariableHistoryItem"("resultId", "index");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "AnswerV2_blockId_idx" ON "AnswerV2"("blockId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "SetVariableHistoryItem" ADD CONSTRAINT "SetVariableHistoryItem_resultId_fkey" FOREIGN KEY ("resultId") REFERENCES "Result"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "AnswerV2" ADD CONSTRAINT "AnswerV2_resultId_fkey" FOREIGN KEY ("resultId") REFERENCES "Result"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
@ -236,22 +236,36 @@ model PublicTypebot {
|
||||
}
|
||||
|
||||
model Result {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
typebotId String
|
||||
variables Json
|
||||
isCompleted Boolean
|
||||
hasStarted Boolean?
|
||||
isArchived Boolean? @default(false)
|
||||
typebot Typebot @relation(fields: [typebotId], references: [id], onDelete: Cascade)
|
||||
answers Answer[]
|
||||
logs Log[]
|
||||
edges VisitedEdge[]
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
typebotId String
|
||||
variables Json
|
||||
isCompleted Boolean
|
||||
hasStarted Boolean?
|
||||
isArchived Boolean? @default(false)
|
||||
lastChatSessionId String?
|
||||
typebot Typebot @relation(fields: [typebotId], references: [id], onDelete: Cascade)
|
||||
answers Answer[]
|
||||
answersV2 AnswerV2[]
|
||||
logs Log[]
|
||||
edges VisitedEdge[]
|
||||
setVariableHistory SetVariableHistoryItem[]
|
||||
|
||||
@@index([typebotId, hasStarted, createdAt(sort: Desc)])
|
||||
@@index([typebotId, isCompleted])
|
||||
}
|
||||
|
||||
model SetVariableHistoryItem {
|
||||
result Result @relation(fields: [resultId], references: [id], onDelete: Cascade)
|
||||
resultId String
|
||||
index Int
|
||||
variableId String
|
||||
blockId String
|
||||
value Json // string or list
|
||||
|
||||
@@unique([resultId, index])
|
||||
}
|
||||
|
||||
model VisitedEdge {
|
||||
result Result @relation(fields: [resultId], references: [id], onDelete: Cascade)
|
||||
resultId String
|
||||
@ -274,19 +288,25 @@ model Log {
|
||||
}
|
||||
|
||||
model Answer {
|
||||
createdAt DateTime @default(now()) @updatedAt
|
||||
resultId String
|
||||
blockId String
|
||||
itemId String?
|
||||
groupId String
|
||||
variableId String?
|
||||
content String
|
||||
storageUsed Int?
|
||||
result Result @relation(fields: [resultId], references: [id], onDelete: Cascade)
|
||||
createdAt DateTime @default(now()) @updatedAt
|
||||
resultId String
|
||||
blockId String
|
||||
groupId String
|
||||
variableId String?
|
||||
content String
|
||||
result Result @relation(fields: [resultId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([resultId, blockId, groupId])
|
||||
@@index([blockId, itemId])
|
||||
@@index([storageUsed])
|
||||
}
|
||||
|
||||
model AnswerV2 {
|
||||
id Int @id @default(autoincrement())
|
||||
blockId String
|
||||
content String
|
||||
resultId String
|
||||
result Result @relation(fields: [resultId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([blockId])
|
||||
}
|
||||
|
||||
model Coupon {
|
||||
|
@ -2,6 +2,7 @@ import { Prisma, PrismaClient } from '@typebot.io/prisma'
|
||||
import { Block, Typebot } from '@typebot.io/schemas'
|
||||
import { deleteFilesFromBucket } from '@typebot.io/lib/s3/deleteFilesFromBucket'
|
||||
import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants'
|
||||
import { isDefined } from '@typebot.io/lib'
|
||||
|
||||
type ArchiveResultsProps = {
|
||||
typebot: Pick<Typebot, 'groups'>
|
||||
@ -42,6 +43,7 @@ export const archiveResults =
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
lastChatSessionId: true,
|
||||
},
|
||||
take: batchSize,
|
||||
})
|
||||
@ -76,6 +78,30 @@ export const archiveResults =
|
||||
resultId: { in: resultIds },
|
||||
},
|
||||
}),
|
||||
prisma.answerV2.deleteMany({
|
||||
where: {
|
||||
resultId: { in: resultIds },
|
||||
},
|
||||
}),
|
||||
prisma.visitedEdge.deleteMany({
|
||||
where: {
|
||||
resultId: { in: resultIds },
|
||||
},
|
||||
}),
|
||||
prisma.setVariableHistoryItem.deleteMany({
|
||||
where: {
|
||||
resultId: { in: resultIds },
|
||||
},
|
||||
}),
|
||||
prisma.chatSession.deleteMany({
|
||||
where: {
|
||||
id: {
|
||||
in: resultsToDelete
|
||||
.map((r) => r.lastChatSessionId)
|
||||
.filter(isDefined),
|
||||
},
|
||||
},
|
||||
}),
|
||||
prisma.result.updateMany({
|
||||
where: {
|
||||
id: { in: resultIds },
|
||||
|
@ -24,11 +24,19 @@ const defaultCellParser: CellParser = (content, blockType) => {
|
||||
: { plainText: content.toString() }
|
||||
}
|
||||
|
||||
export const convertResultsToTableData = (
|
||||
results: ResultWithAnswers[] | undefined,
|
||||
headerCells: ResultHeaderCell[],
|
||||
cellParser: CellParser = defaultCellParser
|
||||
): TableData[] =>
|
||||
type Props = {
|
||||
results: ResultWithAnswers[] | undefined
|
||||
headerCells: ResultHeaderCell[]
|
||||
cellParser?: CellParser
|
||||
blockIdVariableIdMap: Record<string, string>
|
||||
}
|
||||
|
||||
export const convertResultsToTableData = ({
|
||||
results,
|
||||
headerCells,
|
||||
cellParser = defaultCellParser,
|
||||
blockIdVariableIdMap,
|
||||
}: Props): TableData[] =>
|
||||
(results ?? []).map((result) => ({
|
||||
id: { plainText: result.id },
|
||||
date: {
|
||||
@ -37,23 +45,23 @@ export const convertResultsToTableData = (
|
||||
...[...result.answers, ...result.variables].reduce<{
|
||||
[key: string]: { element?: JSX.Element; plainText: string }
|
||||
}>((tableData, answerOrVariable) => {
|
||||
if ('groupId' in answerOrVariable) {
|
||||
const answer = answerOrVariable satisfies Answer
|
||||
const header = answer.variableId
|
||||
if ('blockId' in answerOrVariable) {
|
||||
const answer = answerOrVariable satisfies Pick<
|
||||
Answer,
|
||||
'blockId' | 'content'
|
||||
>
|
||||
const answerVariableId = blockIdVariableIdMap[answer.blockId]
|
||||
const header = answerVariableId
|
||||
? headerCells.find((headerCell) =>
|
||||
headerCell.variableIds?.includes(answer.variableId as string)
|
||||
headerCell.variableIds?.includes(answerVariableId)
|
||||
)
|
||||
: headerCells.find((headerCell) =>
|
||||
headerCell.blocks?.some((block) => block.id === answer.blockId)
|
||||
)
|
||||
if (!header || !header.blocks || !header.blockType) return tableData
|
||||
const variableValue = result.variables.find(
|
||||
(variable) => variable.id === answer.variableId
|
||||
)?.value
|
||||
const content = variableValue ?? answer.content
|
||||
return {
|
||||
...tableData,
|
||||
[header.id]: cellParser(content, header.blockType),
|
||||
[header.id]: cellParser(answer.content, header.blockType),
|
||||
}
|
||||
}
|
||||
const variable = answerOrVariable satisfies VariableWithValue
|
||||
|
19
packages/results/parseBlockIdVariableIdMap.ts
Normal file
19
packages/results/parseBlockIdVariableIdMap.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { PublicTypebotV6 } from '@typebot.io/schemas'
|
||||
import { isInputBlock } from '@typebot.io/schemas/helpers'
|
||||
|
||||
export const parseBlockIdVariableIdMap = (
|
||||
groups?: PublicTypebotV6['groups']
|
||||
): {
|
||||
[key: string]: string
|
||||
} => {
|
||||
if (!groups) return {}
|
||||
const blockIdVariableIdMap: { [key: string]: string } = {}
|
||||
groups.forEach((group) => {
|
||||
group.blocks.forEach((block) => {
|
||||
if (isInputBlock(block) && block.options?.variableId) {
|
||||
blockIdVariableIdMap[block.id] = block.options.variableId
|
||||
}
|
||||
})
|
||||
})
|
||||
return blockIdVariableIdMap
|
||||
}
|
@ -35,7 +35,11 @@ export const parseResultHeader = (
|
||||
{ label: 'Submitted at', id: 'date' },
|
||||
...inputsResultHeader,
|
||||
...parseVariablesHeaders(parsedVariables, inputsResultHeader),
|
||||
...parseResultsFromPreviousBotVersions(results ?? [], inputsResultHeader),
|
||||
...parseResultsFromPreviousBotVersions({
|
||||
results: results ?? [],
|
||||
existingInputResultHeaders: inputsResultHeader,
|
||||
groups: parsedGroups,
|
||||
}),
|
||||
]
|
||||
}
|
||||
|
||||
@ -176,19 +180,22 @@ const parseVariablesHeaders = (
|
||||
return [...existingHeaders, newHeaderCell]
|
||||
}, [])
|
||||
|
||||
const parseResultsFromPreviousBotVersions = (
|
||||
results: ResultWithAnswers[],
|
||||
const parseResultsFromPreviousBotVersions = ({
|
||||
results,
|
||||
existingInputResultHeaders,
|
||||
groups,
|
||||
}: {
|
||||
results: ResultWithAnswers[]
|
||||
existingInputResultHeaders: ResultHeaderCell[]
|
||||
): ResultHeaderCell[] =>
|
||||
groups: Group[]
|
||||
}): ResultHeaderCell[] =>
|
||||
results
|
||||
.flatMap((result) => result.answers)
|
||||
.filter(
|
||||
(answer) =>
|
||||
!answer.variableId &&
|
||||
existingInputResultHeaders.every(
|
||||
(header) => header.id !== answer.blockId
|
||||
) &&
|
||||
isNotEmpty(answer.content)
|
||||
) && isNotEmpty(answer.content)
|
||||
)
|
||||
.reduce<ResultHeaderCell[]>((existingHeaders, answer) => {
|
||||
if (
|
||||
@ -197,6 +204,10 @@ const parseResultsFromPreviousBotVersions = (
|
||||
)
|
||||
)
|
||||
return existingHeaders
|
||||
const groupId =
|
||||
groups.find((group) =>
|
||||
group.blocks.some((block) => block.id === answer.blockId)
|
||||
)?.id ?? ''
|
||||
return [
|
||||
...existingHeaders,
|
||||
{
|
||||
@ -205,7 +216,7 @@ const parseResultsFromPreviousBotVersions = (
|
||||
blocks: [
|
||||
{
|
||||
id: answer.blockId,
|
||||
groupId: answer.groupId,
|
||||
groupId,
|
||||
},
|
||||
],
|
||||
blockType: InputBlockType.TEXT,
|
||||
|
@ -1,27 +1,28 @@
|
||||
import { z } from '../zod'
|
||||
import { Answer as AnswerPrisma, Prisma } from '@typebot.io/prisma'
|
||||
import { Answer as AnswerV1Prisma, Prisma } from '@typebot.io/prisma'
|
||||
|
||||
export const answerSchema = z.object({
|
||||
const answerV1Schema = z.object({
|
||||
createdAt: z.date(),
|
||||
resultId: z.string(),
|
||||
blockId: z.string(),
|
||||
groupId: z.string(),
|
||||
variableId: z.string().nullable(),
|
||||
content: z.string(),
|
||||
storageUsed: z.number().nullable(),
|
||||
// TO-DO: remove once itemId is removed from database schema
|
||||
}) satisfies z.ZodType<Omit<AnswerPrisma, 'itemId'>>
|
||||
}) satisfies z.ZodType<AnswerV1Prisma>
|
||||
|
||||
export const answerInputSchema = answerSchema
|
||||
export const answerSchema = z.object({
|
||||
blockId: z.string(),
|
||||
content: z.string(),
|
||||
})
|
||||
|
||||
export const answerInputSchema = answerV1Schema
|
||||
.omit({
|
||||
createdAt: true,
|
||||
resultId: true,
|
||||
variableId: true,
|
||||
storageUsed: true,
|
||||
})
|
||||
.extend({
|
||||
variableId: z.string().nullish(),
|
||||
storageUsed: z.number().nullish(),
|
||||
}) satisfies z.ZodType<Prisma.AnswerUncheckedUpdateInput>
|
||||
|
||||
export const statsSchema = z.object({
|
||||
|
@ -5,6 +5,7 @@ export const valueTypes = [
|
||||
'Empty',
|
||||
'Append value(s)',
|
||||
'Environment name',
|
||||
'Transcript',
|
||||
'User ID',
|
||||
'Result ID',
|
||||
'Now',
|
||||
@ -20,6 +21,8 @@ export const valueTypes = [
|
||||
|
||||
export const hiddenTypes = ['Today', 'User ID'] as const
|
||||
|
||||
export const sessionOnlySetVariableOptions = ['Transcript'] as const
|
||||
|
||||
export const defaultSetVariableOptions = {
|
||||
type: 'Custom',
|
||||
isExecutedOnClient: false,
|
||||
|
@ -21,6 +21,7 @@ const basicSetVariableOptionsSchema = baseOptions.extend({
|
||||
'Random ID',
|
||||
'Phone number',
|
||||
'Contact name',
|
||||
'Transcript',
|
||||
]),
|
||||
})
|
||||
|
||||
|
@ -260,6 +260,12 @@ export const startPreviewChatInputSchema = z.object({
|
||||
Email: 'john@gmail.com',
|
||||
},
|
||||
}),
|
||||
sessionId: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
'If provided, will be used as the session ID and will overwrite any existing session with the same ID.'
|
||||
),
|
||||
})
|
||||
export type StartPreviewChatInput = z.infer<typeof startPreviewChatInputSchema>
|
||||
|
||||
|
@ -1,14 +1,9 @@
|
||||
import { z } from '../../zod'
|
||||
import { answerSchema } from '../answer'
|
||||
import { resultSchema } from '../result'
|
||||
import { resultSchema, setVariableHistoryItemSchema } from '../result'
|
||||
import { typebotInSessionStateSchema, dynamicThemeSchema } from './shared'
|
||||
import { settingsSchema } from '../typebot/settings'
|
||||
|
||||
const answerInSessionStateSchema = answerSchema.pick({
|
||||
content: true,
|
||||
blockId: true,
|
||||
variableId: true,
|
||||
})
|
||||
import { isInputBlock } from '../../helpers'
|
||||
|
||||
const answerInSessionStateSchemaV2 = z.object({
|
||||
key: z.string(),
|
||||
@ -23,7 +18,7 @@ const resultInSessionStateSchema = resultSchema
|
||||
})
|
||||
.merge(
|
||||
z.object({
|
||||
answers: z.array(answerInSessionStateSchema),
|
||||
answers: z.array(answerSchema),
|
||||
id: z.string().optional(),
|
||||
})
|
||||
)
|
||||
@ -94,6 +89,23 @@ const sessionStateSchemaV3 = sessionStateSchemaV2
|
||||
version: z.literal('3'),
|
||||
currentBlockId: z.string().optional(),
|
||||
allowedOrigins: z.array(z.string()).optional(),
|
||||
setVariableIdsForHistory: z.array(z.string()).optional(),
|
||||
currentSetVariableHistoryIndex: z.number().optional(),
|
||||
previewMetadata: z
|
||||
.object({
|
||||
answers: z.array(answerSchema).optional(),
|
||||
visitedEdges: z.array(z.string()).optional(),
|
||||
setVariableHistory: z
|
||||
.array(
|
||||
setVariableHistoryItemSchema.pick({
|
||||
blockId: true,
|
||||
variableId: true,
|
||||
value: true,
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
|
||||
export type SessionState = z.infer<typeof sessionStateSchemaV3>
|
||||
@ -119,17 +131,27 @@ const migrateFromV1ToV2 = (
|
||||
{
|
||||
typebot: state.typebot,
|
||||
resultId: state.result.id,
|
||||
answers: state.result.answers.map((answer) => ({
|
||||
key:
|
||||
(answer.variableId
|
||||
? state.typebot.variables.find(
|
||||
(variable) => variable.id === answer.variableId
|
||||
)?.name
|
||||
: state.typebot.groups.find((group) =>
|
||||
group.blocks.find((block) => block.id === answer.blockId)
|
||||
)?.title) ?? '',
|
||||
value: answer.content,
|
||||
})),
|
||||
answers: state.result.answers.map((answer) => {
|
||||
let answerVariableId: string | undefined
|
||||
state.typebot.groups.forEach((group) => {
|
||||
group.blocks.forEach((block) => {
|
||||
if (isInputBlock(block) && block.id === answer.blockId) {
|
||||
answerVariableId = block.options?.variableId
|
||||
}
|
||||
})
|
||||
})
|
||||
return {
|
||||
key:
|
||||
(answerVariableId
|
||||
? state.typebot.variables.find(
|
||||
(variable) => variable.id === answerVariableId
|
||||
)?.name
|
||||
: state.typebot.groups.find((group) =>
|
||||
group.blocks.find((block) => block.id === answer.blockId)
|
||||
)?.title) ?? '',
|
||||
value: answer.content,
|
||||
}
|
||||
}),
|
||||
isMergingWithParent: true,
|
||||
edgeIdToTriggerWhenDone:
|
||||
state.linkedTypebots.queue.length > 0
|
||||
@ -141,17 +163,27 @@ const migrateFromV1ToV2 = (
|
||||
({
|
||||
typebot,
|
||||
resultId: state.result.id,
|
||||
answers: state.result.answers.map((answer) => ({
|
||||
key:
|
||||
(answer.variableId
|
||||
? state.typebot.variables.find(
|
||||
(variable) => variable.id === answer.variableId
|
||||
)?.name
|
||||
: state.typebot.groups.find((group) =>
|
||||
group.blocks.find((block) => block.id === answer.blockId)
|
||||
)?.title) ?? '',
|
||||
value: answer.content,
|
||||
})),
|
||||
answers: state.result.answers.map((answer) => {
|
||||
let answerVariableId: string | undefined
|
||||
typebot.groups.forEach((group) => {
|
||||
group.blocks.forEach((block) => {
|
||||
if (isInputBlock(block) && block.id === answer.blockId) {
|
||||
answerVariableId = block.options?.variableId
|
||||
}
|
||||
})
|
||||
})
|
||||
return {
|
||||
key:
|
||||
(answerVariableId
|
||||
? state.typebot.variables.find(
|
||||
(variable) => variable.id === answerVariableId
|
||||
)?.name
|
||||
: state.typebot.groups.find((group) =>
|
||||
group.blocks.find((block) => block.id === answer.blockId)
|
||||
)?.title) ?? '',
|
||||
value: answer.content,
|
||||
}
|
||||
}),
|
||||
edgeIdToTriggerWhenDone: state.linkedTypebots.queue.at(index + 1)
|
||||
?.edgeId,
|
||||
} satisfies SessionState['typebotsQueue'][number])
|
||||
|
@ -1,9 +1,10 @@
|
||||
import { z } from '../zod'
|
||||
import { answerInputSchema, answerSchema } from './answer'
|
||||
import { variableWithValueSchema } from './typebot/variable'
|
||||
import { listVariableValue, variableWithValueSchema } from './typebot/variable'
|
||||
import {
|
||||
Result as ResultPrisma,
|
||||
Log as LogPrisma,
|
||||
SetVariableHistoryItem as SetVariableHistoryItemPrisma,
|
||||
VisitedEdge,
|
||||
} from '@typebot.io/prisma'
|
||||
import { InputBlockType } from './blocks/inputs/constants'
|
||||
@ -16,6 +17,7 @@ export const resultSchema = z.object({
|
||||
isCompleted: z.boolean(),
|
||||
hasStarted: z.boolean().nullable(),
|
||||
isArchived: z.boolean().nullable(),
|
||||
lastChatSessionId: z.string().nullable(),
|
||||
}) satisfies z.ZodType<ResultPrisma>
|
||||
|
||||
export const resultWithAnswersSchema = resultSchema.merge(
|
||||
@ -78,3 +80,14 @@ export type CellValueType = { element?: JSX.Element; plainText: string }
|
||||
export type TableData = {
|
||||
id: Pick<CellValueType, 'plainText'>
|
||||
} & Record<string, CellValueType>
|
||||
|
||||
export const setVariableHistoryItemSchema = z.object({
|
||||
resultId: z.string(),
|
||||
index: z.number(),
|
||||
blockId: z.string(),
|
||||
variableId: z.string(),
|
||||
value: z.string().or(listVariableValue).nullable(),
|
||||
}) satisfies z.ZodType<SetVariableHistoryItemPrisma>
|
||||
export type SetVariableHistoryItem = z.infer<
|
||||
typeof setVariableHistoryItemSchema
|
||||
>
|
||||
|
@ -5,12 +5,13 @@ import cliProgress from 'cli-progress'
|
||||
import { writeFileSync } from 'fs'
|
||||
import {
|
||||
ResultWithAnswers,
|
||||
Typebot,
|
||||
TypebotV6,
|
||||
resultWithAnswersSchema,
|
||||
} from '@typebot.io/schemas'
|
||||
import { byId } from '@typebot.io/lib'
|
||||
import { parseResultHeader } from '@typebot.io/results/parseResultHeader'
|
||||
import { convertResultsToTableData } from '@typebot.io/results/convertResultsToTableData'
|
||||
import { parseBlockIdVariableIdMap } from '@typebot.io/results/parseBlockIdVariableIdMap'
|
||||
import { parseColumnsOrder } from '@typebot.io/results/parseColumnsOrder'
|
||||
import { parseUniqueKey } from '@typebot.io/lib/parseUniqueKey'
|
||||
import { unparse } from 'papaparse'
|
||||
@ -39,7 +40,7 @@ const exportResults = async () => {
|
||||
where: {
|
||||
id: typebotId,
|
||||
},
|
||||
})) as Typebot | null
|
||||
})) as TypebotV6 | null
|
||||
|
||||
if (!typebot) {
|
||||
console.log('No typebot found')
|
||||
@ -61,19 +62,34 @@ const exportResults = async () => {
|
||||
for (let skip = 0; skip < totalResultsToExport; skip += 50) {
|
||||
results.push(
|
||||
...z.array(resultWithAnswersSchema).parse(
|
||||
await prisma.result.findMany({
|
||||
take: 50,
|
||||
skip,
|
||||
where: {
|
||||
typebotId,
|
||||
hasStarted: true,
|
||||
isArchived: false,
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
include: { answers: true },
|
||||
})
|
||||
(
|
||||
await prisma.result.findMany({
|
||||
take: 50,
|
||||
skip,
|
||||
where: {
|
||||
typebotId,
|
||||
hasStarted: true,
|
||||
isArchived: false,
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
include: {
|
||||
answers: {
|
||||
select: {
|
||||
content: true,
|
||||
blockId: true,
|
||||
},
|
||||
},
|
||||
answersV2: {
|
||||
select: {
|
||||
content: true,
|
||||
blockId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
).map((r) => ({ ...r, answers: r.answersV2.concat(r.answers) }))
|
||||
)
|
||||
)
|
||||
progressBar.increment(50)
|
||||
@ -85,7 +101,11 @@ const exportResults = async () => {
|
||||
|
||||
const resultHeader = parseResultHeader(typebot, [])
|
||||
|
||||
const dataToUnparse = convertResultsToTableData(results, resultHeader)
|
||||
const dataToUnparse = convertResultsToTableData({
|
||||
results,
|
||||
headerCells: resultHeader,
|
||||
blockIdVariableIdMap: parseBlockIdVariableIdMap(typebot?.groups),
|
||||
})
|
||||
|
||||
const headerIds = parseColumnsOrder(
|
||||
typebot?.resultsTablePreferences?.columnsOrder,
|
||||
|
@ -4,6 +4,7 @@
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@typebot.io/lib": "workspace:*"
|
||||
"@typebot.io/lib": "workspace:*",
|
||||
"@typebot.io/tsconfig": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
@ -95,6 +95,7 @@ type VariableToParseInformation = {
|
||||
endIndex: number
|
||||
textToReplace: string
|
||||
value: string
|
||||
variableId?: string
|
||||
}
|
||||
|
||||
export const getVariablesToParseInfoInText = (
|
||||
@ -146,6 +147,7 @@ export const getVariablesToParseInfoInText = (
|
||||
? variable?.value[variable?.value.length - 1]
|
||||
: variable?.value
|
||||
) ?? '',
|
||||
variableId: variable?.id,
|
||||
})
|
||||
})
|
||||
return variablesToParseInfo.sort((a, b) => a.startIndex - b.startIndex)
|
||||
|
@ -2,12 +2,13 @@ export type Variable = {
|
||||
id: string
|
||||
name: string
|
||||
value?: string | (string | null)[] | null | undefined
|
||||
isSessionVariable?: boolean
|
||||
}
|
||||
|
||||
export type VariableWithValue = Pick<Variable, 'id' | 'name'> & {
|
||||
export type VariableWithValue = Omit<Variable, 'value'> & {
|
||||
value: string | (string | null)[]
|
||||
}
|
||||
|
||||
export type VariableWithUnknowValue = Pick<Variable, 'id' | 'name'> & {
|
||||
export type VariableWithUnknowValue = Omit<Variable, 'value'> & {
|
||||
value?: unknown
|
||||
}
|
||||
|
@ -1,41 +1,102 @@
|
||||
import { safeStringify } from '@typebot.io/lib/safeStringify'
|
||||
import { Variable, VariableWithUnknowValue } from './types'
|
||||
import { SessionState, SetVariableHistoryItem } from '../schemas'
|
||||
|
||||
export const updateVariablesInSession =
|
||||
(state: any) => (newVariables: VariableWithUnknowValue[]) => ({
|
||||
...state,
|
||||
typebotsQueue: state.typebotsQueue.map(
|
||||
(typebotInQueue: { typebot: { variables: Variable[] } }, index: number) =>
|
||||
type Props = {
|
||||
state: SessionState
|
||||
newVariables: VariableWithUnknowValue[]
|
||||
currentBlockId: string | undefined
|
||||
}
|
||||
export const updateVariablesInSession = ({
|
||||
state,
|
||||
newVariables,
|
||||
currentBlockId,
|
||||
}: Props): {
|
||||
updatedState: SessionState
|
||||
newSetVariableHistory: SetVariableHistoryItem[]
|
||||
} => {
|
||||
const { updatedVariables, newSetVariableHistory, setVariableHistoryIndex } =
|
||||
updateTypebotVariables({
|
||||
state,
|
||||
newVariables,
|
||||
currentBlockId,
|
||||
})
|
||||
|
||||
return {
|
||||
updatedState: {
|
||||
...state,
|
||||
currentSetVariableHistoryIndex: setVariableHistoryIndex,
|
||||
typebotsQueue: state.typebotsQueue.map((typebotInQueue, index: number) =>
|
||||
index === 0
|
||||
? {
|
||||
...typebotInQueue,
|
||||
typebot: {
|
||||
...typebotInQueue.typebot,
|
||||
variables: updateTypebotVariables(typebotInQueue.typebot)(
|
||||
newVariables
|
||||
),
|
||||
variables: updatedVariables,
|
||||
},
|
||||
}
|
||||
: typebotInQueue
|
||||
),
|
||||
})
|
||||
),
|
||||
previewMetadata: state.typebotsQueue[0].resultId
|
||||
? state.previewMetadata
|
||||
: {
|
||||
...state.previewMetadata,
|
||||
setVariableHistory: (
|
||||
state.previewMetadata?.setVariableHistory ?? []
|
||||
).concat(newSetVariableHistory),
|
||||
},
|
||||
},
|
||||
newSetVariableHistory,
|
||||
}
|
||||
}
|
||||
|
||||
const updateTypebotVariables =
|
||||
(typebot: { variables: Variable[] }) =>
|
||||
(newVariables: VariableWithUnknowValue[]): Variable[] => {
|
||||
const serializedNewVariables = newVariables.map((variable) => ({
|
||||
...variable,
|
||||
value: Array.isArray(variable.value)
|
||||
? variable.value.map(safeStringify)
|
||||
: safeStringify(variable.value),
|
||||
}))
|
||||
const updateTypebotVariables = ({
|
||||
state,
|
||||
newVariables,
|
||||
currentBlockId,
|
||||
}: {
|
||||
state: SessionState
|
||||
newVariables: VariableWithUnknowValue[]
|
||||
currentBlockId: string | undefined
|
||||
}): {
|
||||
updatedVariables: Variable[]
|
||||
newSetVariableHistory: SetVariableHistoryItem[]
|
||||
setVariableHistoryIndex: number
|
||||
} => {
|
||||
const serializedNewVariables = newVariables.map((variable) => ({
|
||||
...variable,
|
||||
value: Array.isArray(variable.value)
|
||||
? variable.value.map(safeStringify)
|
||||
: safeStringify(variable.value),
|
||||
}))
|
||||
|
||||
return [
|
||||
...typebot.variables.filter((existingVariable) =>
|
||||
let setVariableHistoryIndex = state.currentSetVariableHistoryIndex ?? 0
|
||||
const setVariableHistory: SetVariableHistoryItem[] = []
|
||||
if (currentBlockId) {
|
||||
serializedNewVariables
|
||||
.filter((v) => state.setVariableIdsForHistory?.includes(v.id))
|
||||
.forEach((newVariable) => {
|
||||
setVariableHistory.push({
|
||||
resultId: state.typebotsQueue[0].resultId as string,
|
||||
index: setVariableHistoryIndex,
|
||||
blockId: currentBlockId,
|
||||
variableId: newVariable.id,
|
||||
value: newVariable.value,
|
||||
})
|
||||
setVariableHistoryIndex += 1
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
updatedVariables: [
|
||||
...state.typebotsQueue[0].typebot.variables.filter((existingVariable) =>
|
||||
serializedNewVariables.every(
|
||||
(newVariable) => existingVariable.id !== newVariable.id
|
||||
)
|
||||
),
|
||||
...serializedNewVariables,
|
||||
]
|
||||
],
|
||||
newSetVariableHistory: setVariableHistory,
|
||||
setVariableHistoryIndex,
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user