@@ -2,7 +2,6 @@ import {
|
||||
SessionState,
|
||||
VariableWithValue,
|
||||
ChoiceInputBlock,
|
||||
ItemType,
|
||||
} from '@typebot.io/schemas'
|
||||
import { isDefined } from '@typebot.io/lib'
|
||||
import { filterChoiceItems } from './filterChoiceItems'
|
||||
@@ -14,10 +13,10 @@ export const injectVariableValuesInButtonsInputBlock =
|
||||
(state: SessionState) =>
|
||||
(block: ChoiceInputBlock): ChoiceInputBlock => {
|
||||
const { variables } = state.typebotsQueue[0].typebot
|
||||
if (block.options.dynamicVariableId) {
|
||||
if (block.options?.dynamicVariableId) {
|
||||
const variable = variables.find(
|
||||
(variable) =>
|
||||
variable.id === block.options.dynamicVariableId &&
|
||||
variable.id === block.options?.dynamicVariableId &&
|
||||
isDefined(variable.value)
|
||||
) as VariableWithValue | undefined
|
||||
if (!variable) return block
|
||||
@@ -26,7 +25,6 @@ export const injectVariableValuesInButtonsInputBlock =
|
||||
...block,
|
||||
items: value.filter(isDefined).map((item, idx) => ({
|
||||
id: idx.toString(),
|
||||
type: ItemType.BUTTON,
|
||||
blockId: block.id,
|
||||
content: item,
|
||||
})),
|
||||
|
||||
@@ -7,7 +7,7 @@ export const parseButtonsReply =
|
||||
(inputValue: string, block: ChoiceInputBlock): ParsedReply => {
|
||||
const displayedItems =
|
||||
injectVariableValuesInButtonsInputBlock(state)(block).items
|
||||
if (block.options.isMultipleChoice) {
|
||||
if (block.options?.isMultipleChoice) {
|
||||
const longestItemsFirst = [...displayedItems].sort(
|
||||
(a, b) => (b.content?.length ?? 0) - (a.content?.length ?? 0)
|
||||
)
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
import { getPrefilledInputValue } from '../../../getPrefilledValue'
|
||||
import {
|
||||
DateInputBlock,
|
||||
DateInputOptions,
|
||||
SessionState,
|
||||
Variable,
|
||||
} from '@typebot.io/schemas'
|
||||
import { DateInputBlock, SessionState, Variable } from '@typebot.io/schemas'
|
||||
import { deepParseVariables } from '../../../variables/deepParseVariables'
|
||||
import { parseVariables } from '../../../variables/parseVariables'
|
||||
|
||||
export const parseDateInput =
|
||||
(state: SessionState) => (block: DateInputBlock) => {
|
||||
if (!block.options) return block
|
||||
return {
|
||||
...block,
|
||||
options: {
|
||||
@@ -34,8 +30,10 @@ export const parseDateInput =
|
||||
}
|
||||
|
||||
const parseDateLimit = (
|
||||
limit: DateInputOptions['min'] | DateInputOptions['max'],
|
||||
hasTime: DateInputOptions['hasTime'],
|
||||
limit:
|
||||
| NonNullable<DateInputBlock['options']>['min']
|
||||
| NonNullable<DateInputBlock['options']>['max'],
|
||||
hasTime: NonNullable<DateInputBlock['options']>['hasTime'],
|
||||
variables: Variable[]
|
||||
) => {
|
||||
if (!limit) return
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { isDefined } from '@typebot.io/lib'
|
||||
import { ParsedReply } from '../../../types'
|
||||
import { DateInputBlock } from '@typebot.io/schemas'
|
||||
import { parse as chronoParse } from 'chrono-node'
|
||||
import { format } from 'date-fns'
|
||||
import { defaultDateInputOptions } from '@typebot.io/schemas/features/blocks/inputs/date/constants'
|
||||
|
||||
export const parseDateReply = (
|
||||
reply: string,
|
||||
@@ -10,8 +12,10 @@ export const parseDateReply = (
|
||||
const parsedDate = chronoParse(reply)
|
||||
if (parsedDate.length === 0) return { status: 'fail' }
|
||||
const formatString =
|
||||
block.options.format ??
|
||||
(block.options.hasTime ? 'dd/MM/yyyy HH:mm' : 'dd/MM/yyyy')
|
||||
block.options?.format ??
|
||||
(block.options?.hasTime
|
||||
? defaultDateInputOptions.formatWithTime
|
||||
: defaultDateInputOptions.format)
|
||||
|
||||
const detectedStartDate = parseDateWithNeutralTimezone(
|
||||
parsedDate[0].start.date()
|
||||
@@ -25,25 +29,27 @@ export const parseDateReply = (
|
||||
? format(detectedEndDate, formatString)
|
||||
: undefined
|
||||
|
||||
if (block.options.isRange && !endDate) return { status: 'fail' }
|
||||
if (block.options?.isRange && !endDate) return { status: 'fail' }
|
||||
|
||||
const max = block.options?.max
|
||||
if (
|
||||
block.options.max &&
|
||||
(detectedStartDate > new Date(block.options.max) ||
|
||||
(detectedEndDate && detectedEndDate > new Date(block.options.max)))
|
||||
isDefined(max) &&
|
||||
(detectedStartDate > new Date(max) ||
|
||||
(detectedEndDate && detectedEndDate > new Date(max)))
|
||||
)
|
||||
return { status: 'fail' }
|
||||
|
||||
const min = block.options?.min
|
||||
if (
|
||||
block.options.min &&
|
||||
(detectedStartDate < new Date(block.options.min) ||
|
||||
(detectedEndDate && detectedEndDate < new Date(block.options.min)))
|
||||
isDefined(min) &&
|
||||
(detectedStartDate < new Date(min) ||
|
||||
(detectedEndDate && detectedEndDate < new Date(min)))
|
||||
)
|
||||
return { status: 'fail' }
|
||||
|
||||
return {
|
||||
status: 'success',
|
||||
reply: block.options.isRange ? `${startDate} to ${endDate}` : startDate,
|
||||
reply: block.options?.isRange ? `${startDate} to ${endDate}` : startDate,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { TRPCError } from '@trpc/server'
|
||||
import {
|
||||
PaymentInputOptions,
|
||||
PaymentInputBlock,
|
||||
PaymentInputRuntimeOptions,
|
||||
SessionState,
|
||||
StripeCredentials,
|
||||
@@ -9,20 +9,23 @@ import Stripe from 'stripe'
|
||||
import { decrypt } from '@typebot.io/lib/api/encryption/decrypt'
|
||||
import { parseVariables } from '../../../variables/parseVariables'
|
||||
import prisma from '@typebot.io/lib/prisma'
|
||||
import { defaultPaymentInputOptions } from '@typebot.io/schemas/features/blocks/inputs/payment/constants'
|
||||
|
||||
export const computePaymentInputRuntimeOptions =
|
||||
(state: SessionState) => (options: PaymentInputOptions) =>
|
||||
(state: SessionState) => (options: PaymentInputBlock['options']) =>
|
||||
createStripePaymentIntent(state)(options)
|
||||
|
||||
const createStripePaymentIntent =
|
||||
(state: SessionState) =>
|
||||
async (options: PaymentInputOptions): Promise<PaymentInputRuntimeOptions> => {
|
||||
async (
|
||||
options: PaymentInputBlock['options']
|
||||
): Promise<PaymentInputRuntimeOptions> => {
|
||||
const {
|
||||
resultId,
|
||||
typebot: { variables },
|
||||
} = state.typebotsQueue[0]
|
||||
const isPreview = !resultId
|
||||
if (!options.credentialsId)
|
||||
if (!options?.credentialsId)
|
||||
throw new TRPCError({
|
||||
code: 'BAD_REQUEST',
|
||||
message: 'Missing credentialsId',
|
||||
@@ -39,9 +42,10 @@ const createStripePaymentIntent =
|
||||
: stripeKeys.live.secretKey,
|
||||
{ apiVersion: '2022-11-15' }
|
||||
)
|
||||
const currency = options?.currency ?? defaultPaymentInputOptions.currency
|
||||
const amount = Math.round(
|
||||
Number(parseVariables(variables)(options.amount)) *
|
||||
(isZeroDecimalCurrency(options.currency) ? 1 : 100)
|
||||
(isZeroDecimalCurrency(currency) ? 1 : 100)
|
||||
)
|
||||
if (isNaN(amount))
|
||||
throw new TRPCError({
|
||||
@@ -55,7 +59,7 @@ const createStripePaymentIntent =
|
||||
)
|
||||
const paymentIntent = await stripe.paymentIntents.create({
|
||||
amount,
|
||||
currency: options.currency,
|
||||
currency,
|
||||
receipt_email: receiptEmail === '' ? undefined : receiptEmail,
|
||||
description: options.additionalInformation?.description,
|
||||
automatic_payment_methods: {
|
||||
@@ -84,7 +88,7 @@ const createStripePaymentIntent =
|
||||
? stripeKeys.test.publicKey
|
||||
: stripeKeys.live.publicKey,
|
||||
amountLabel: priceFormatter.format(
|
||||
amount / (isZeroDecimalCurrency(options.currency) ? 1 : 100)
|
||||
amount / (isZeroDecimalCurrency(currency) ? 1 : 100)
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import {
|
||||
VariableWithValue,
|
||||
ItemType,
|
||||
PictureChoiceBlock,
|
||||
Variable,
|
||||
} from '@typebot.io/schemas'
|
||||
@@ -12,19 +11,19 @@ export const injectVariableValuesInPictureChoiceBlock =
|
||||
(variables: Variable[]) =>
|
||||
(block: PictureChoiceBlock): PictureChoiceBlock => {
|
||||
if (
|
||||
block.options.dynamicItems?.isEnabled &&
|
||||
block.options?.dynamicItems?.isEnabled &&
|
||||
block.options.dynamicItems.pictureSrcsVariableId
|
||||
) {
|
||||
const pictureSrcsVariable = variables.find(
|
||||
(variable) =>
|
||||
variable.id === block.options.dynamicItems?.pictureSrcsVariableId &&
|
||||
variable.id === block.options?.dynamicItems?.pictureSrcsVariableId &&
|
||||
isDefined(variable.value)
|
||||
) as VariableWithValue | undefined
|
||||
if (!pictureSrcsVariable) return block
|
||||
const titlesVariable = block.options.dynamicItems.titlesVariableId
|
||||
? (variables.find(
|
||||
(variable) =>
|
||||
variable.id === block.options.dynamicItems?.titlesVariableId &&
|
||||
variable.id === block.options?.dynamicItems?.titlesVariableId &&
|
||||
isDefined(variable.value)
|
||||
) as VariableWithValue | undefined)
|
||||
: undefined
|
||||
@@ -37,7 +36,7 @@ export const injectVariableValuesInPictureChoiceBlock =
|
||||
? (variables.find(
|
||||
(variable) =>
|
||||
variable.id ===
|
||||
block.options.dynamicItems?.descriptionsVariableId &&
|
||||
block.options?.dynamicItems?.descriptionsVariableId &&
|
||||
isDefined(variable.value)
|
||||
) as VariableWithValue | undefined)
|
||||
: undefined
|
||||
@@ -55,7 +54,6 @@ export const injectVariableValuesInPictureChoiceBlock =
|
||||
...block,
|
||||
items: variableValues.filter(isDefined).map((pictureSrc, idx) => ({
|
||||
id: idx.toString(),
|
||||
type: ItemType.PICTURE_CHOICE,
|
||||
blockId: block.id,
|
||||
pictureSrc,
|
||||
title: titlesVariableValues?.[idx] ?? '',
|
||||
|
||||
@@ -8,7 +8,7 @@ export const parsePictureChoicesReply =
|
||||
const displayedItems = injectVariableValuesInPictureChoiceBlock(
|
||||
state.typebotsQueue[0].typebot.variables
|
||||
)(block).items
|
||||
if (block.options.isMultipleChoice) {
|
||||
if (block.options?.isMultipleChoice) {
|
||||
const longestItemsFirst = [...displayedItems].sort(
|
||||
(a, b) => (b.title?.length ?? 0) - (a.title?.length ?? 0)
|
||||
)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { RatingInputBlock } from '@typebot.io/schemas'
|
||||
import { defaultRatingInputOptions } from '@typebot.io/schemas/features/blocks/inputs/rating/constants'
|
||||
|
||||
export const validateRatingReply = (reply: string, block: RatingInputBlock) =>
|
||||
Number(reply) <= block.options.length
|
||||
Number(reply) <= (block.options?.length ?? defaultRatingInputOptions.length)
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { ExecuteIntegrationResponse } from '../../../types'
|
||||
import { env } from '@typebot.io/env'
|
||||
import { isDefined } from '@typebot.io/lib'
|
||||
import {
|
||||
ChatwootBlock,
|
||||
ChatwootOptions,
|
||||
SessionState,
|
||||
} from '@typebot.io/schemas'
|
||||
import { ChatwootBlock, SessionState } from '@typebot.io/schemas'
|
||||
import { extractVariablesFromText } from '../../../variables/extractVariablesFromText'
|
||||
import { parseGuessedValueType } from '../../../variables/parseGuessedValueType'
|
||||
import { parseVariables } from '../../../variables/parseVariables'
|
||||
import { defaultChatwootOptions } from '@typebot.io/schemas/features/blocks/integrations/chatwoot/constants'
|
||||
|
||||
const parseSetUserCode = (user: ChatwootOptions['user'], resultId: string) =>
|
||||
const parseSetUserCode = (
|
||||
user: NonNullable<ChatwootBlock['options']>['user'],
|
||||
resultId: string
|
||||
) =>
|
||||
user?.email || user?.id
|
||||
? `
|
||||
window.$chatwoot.setUser(${user?.id ?? `"${resultId}"`}, {
|
||||
@@ -27,7 +27,7 @@ const parseChatwootOpenCode = ({
|
||||
user,
|
||||
resultId,
|
||||
typebotId,
|
||||
}: ChatwootOptions & { typebotId: string; resultId: string }) => {
|
||||
}: ChatwootBlock['options'] & { typebotId: string; resultId: string }) => {
|
||||
const openChatwoot = `${parseSetUserCode(user, resultId)}
|
||||
if(window.Typebot?.unmount) window.Typebot.unmount();
|
||||
window.$chatwoot.setCustomAttributes({
|
||||
@@ -46,7 +46,7 @@ const parseChatwootOpenCode = ({
|
||||
if (window.$chatwoot) {${openChatwoot}}
|
||||
else {
|
||||
(function (d, t) {
|
||||
var BASE_URL = "${baseUrl}";
|
||||
var BASE_URL = "${baseUrl ?? defaultChatwootOptions.baseUrl}";
|
||||
var g = d.createElement(t),
|
||||
s = d.getElementsByTagName(t)[0];
|
||||
g.src = BASE_URL + "/packs/js/sdk.js";
|
||||
@@ -78,7 +78,7 @@ export const executeChatwootBlock = (
|
||||
if (state.whatsApp) return { outgoingEdgeId: block.outgoingEdgeId }
|
||||
const { typebot, resultId } = state.typebotsQueue[0]
|
||||
const chatwootCode =
|
||||
block.options.task === 'Close widget'
|
||||
block.options?.task === 'Close widget'
|
||||
? chatwootCloseCode
|
||||
: isDefined(resultId)
|
||||
? parseChatwootOpenCode({
|
||||
@@ -87,6 +87,7 @@ export const executeChatwootBlock = (
|
||||
resultId,
|
||||
})
|
||||
: ''
|
||||
|
||||
return {
|
||||
outgoingEdgeId: block.outgoingEdgeId,
|
||||
clientSideActions: [
|
||||
|
||||
@@ -7,7 +7,7 @@ export const executeGoogleAnalyticsBlock = (
|
||||
block: GoogleAnalyticsBlock
|
||||
): ExecuteIntegrationResponse => {
|
||||
const { typebot, resultId } = state.typebotsQueue[0]
|
||||
if (!resultId || state.whatsApp)
|
||||
if (!resultId || state.whatsApp || !block.options)
|
||||
return { outgoingEdgeId: block.outgoingEdgeId }
|
||||
const googleAnalytics = deepParseVariables(typebot.variables, {
|
||||
guessCorrectTypes: true,
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
import {
|
||||
GoogleSheetsBlock,
|
||||
GoogleSheetsAction,
|
||||
SessionState,
|
||||
} from '@typebot.io/schemas'
|
||||
import { GoogleSheetsBlock, SessionState } from '@typebot.io/schemas'
|
||||
import { insertRow } from './insertRow'
|
||||
import { updateRow } from './updateRow'
|
||||
import { getRow } from './getRow'
|
||||
import { ExecuteIntegrationResponse } from '../../../types'
|
||||
import { GoogleSheetsAction } from '@typebot.io/schemas/features/blocks/integrations/googleSheets/constants'
|
||||
|
||||
export const executeGoogleSheetBlock = async (
|
||||
state: SessionState,
|
||||
block: GoogleSheetsBlock
|
||||
): Promise<ExecuteIntegrationResponse> => {
|
||||
if (!block.options) return { outgoingEdgeId: block.outgoingEdgeId }
|
||||
const action = block.options.action
|
||||
if (!action) return { outgoingEdgeId: block.outgoingEdgeId }
|
||||
switch (action) {
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
VariableWithValue,
|
||||
ReplyLog,
|
||||
} from '@typebot.io/schemas'
|
||||
import { isNotEmpty, byId } from '@typebot.io/lib'
|
||||
import { isNotEmpty, byId, isDefined } from '@typebot.io/lib'
|
||||
import { getAuthenticatedGoogleDoc } from './helpers/getAuthenticatedGoogleDoc'
|
||||
import { ExecuteIntegrationResponse } from '../../../types'
|
||||
import { matchFilter } from './helpers/matchFilter'
|
||||
@@ -20,7 +20,7 @@ export const getRow = async (
|
||||
): Promise<ExecuteIntegrationResponse> => {
|
||||
const logs: ReplyLog[] = []
|
||||
const { variables } = state.typebotsQueue[0].typebot
|
||||
const { sheetId, cellsToExtract, referenceCell, filter } =
|
||||
const { sheetId, cellsToExtract, filter, ...parsedOptions } =
|
||||
deepParseVariables(variables)(options)
|
||||
if (!sheetId) return { outgoingEdgeId }
|
||||
|
||||
@@ -36,8 +36,9 @@ export const getRow = async (
|
||||
const filteredRows = getTotalRows(
|
||||
options.totalRowsToExtract,
|
||||
rows.filter((row) =>
|
||||
referenceCell
|
||||
? row.get(referenceCell.column as string) === referenceCell.value
|
||||
'referenceCell' in parsedOptions && parsedOptions.referenceCell
|
||||
? row.get(parsedOptions.referenceCell?.column as string) ===
|
||||
parsedOptions.referenceCell?.value
|
||||
: matchFilter(row, filter)
|
||||
)
|
||||
)
|
||||
@@ -50,17 +51,19 @@ export const getRow = async (
|
||||
return { outgoingEdgeId, logs }
|
||||
}
|
||||
const extractingColumns = cellsToExtract
|
||||
.map((cell) => cell.column)
|
||||
?.map((cell) => cell.column)
|
||||
.filter(isNotEmpty)
|
||||
const selectedRows = filteredRows.map((row) =>
|
||||
extractingColumns.reduce<{ [key: string]: string }>(
|
||||
(obj, column) => ({ ...obj, [column]: row.get(column) }),
|
||||
{}
|
||||
const selectedRows = filteredRows
|
||||
.map((row) =>
|
||||
extractingColumns?.reduce<{ [key: string]: string }>(
|
||||
(obj, column) => ({ ...obj, [column]: row.get(column) }),
|
||||
{}
|
||||
)
|
||||
)
|
||||
)
|
||||
.filter(isDefined)
|
||||
if (!selectedRows) return { outgoingEdgeId }
|
||||
|
||||
const newVariables = options.cellsToExtract.reduce<VariableWithValue[]>(
|
||||
const newVariables = options.cellsToExtract?.reduce<VariableWithValue[]>(
|
||||
(newVariables, cell) => {
|
||||
const existingVariable = variables.find(byId(cell.variableId))
|
||||
const value = selectedRows.map((row) => row[cell.column ?? ''])
|
||||
@@ -75,6 +78,7 @@ export const getRow = async (
|
||||
},
|
||||
[]
|
||||
)
|
||||
if (!newVariables) return { outgoingEdgeId }
|
||||
const newSessionState = updateVariablesInSession(state)(newVariables)
|
||||
return {
|
||||
outgoingEdgeId,
|
||||
|
||||
@@ -3,11 +3,11 @@ import { env } from '@typebot.io/env'
|
||||
import { encrypt } from '@typebot.io/lib/api/encryption/encrypt'
|
||||
import { decrypt } from '@typebot.io/lib/api/encryption/decrypt'
|
||||
import { isDefined } from '@typebot.io/lib/utils'
|
||||
import { GoogleSheetsCredentials } from '@typebot.io/schemas/features/blocks/integrations/googleSheets/schemas'
|
||||
import { Credentials as CredentialsFromDb } from '@typebot.io/prisma'
|
||||
import { GoogleSpreadsheet } from 'google-spreadsheet'
|
||||
import { OAuth2Client, Credentials } from 'google-auth-library'
|
||||
import prisma from '@typebot.io/lib/prisma'
|
||||
import { GoogleSheetsCredentials } from '@typebot.io/schemas'
|
||||
|
||||
export const getAuthenticatedGoogleDoc = async ({
|
||||
credentialsId,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { isDefined } from '@typebot.io/lib'
|
||||
import { GoogleSheetsGetOptions } from '@typebot.io/schemas'
|
||||
import {
|
||||
GoogleSheetsGetOptions,
|
||||
LogicalOperator,
|
||||
ComparisonOperators,
|
||||
} from '@typebot.io/schemas'
|
||||
} from '@typebot.io/schemas/features/blocks/logic/condition/constants'
|
||||
import { GoogleSpreadsheetRow } from 'google-spreadsheet'
|
||||
|
||||
export const matchFilter = (
|
||||
@@ -12,7 +12,7 @@ export const matchFilter = (
|
||||
) => {
|
||||
if (!filter) return true
|
||||
return filter.logicalOperator === LogicalOperator.AND
|
||||
? filter.comparisons.every(
|
||||
? filter.comparisons?.every(
|
||||
(comparison) =>
|
||||
comparison.column &&
|
||||
matchComparison(
|
||||
@@ -21,7 +21,7 @@ export const matchFilter = (
|
||||
comparison.value
|
||||
)
|
||||
)
|
||||
: filter.comparisons.some(
|
||||
: filter.comparisons?.some(
|
||||
(comparison) =>
|
||||
comparison.column &&
|
||||
matchComparison(
|
||||
|
||||
@@ -17,8 +17,14 @@ export const updateRow = async (
|
||||
}: { outgoingEdgeId?: string; options: GoogleSheetsUpdateRowOptions }
|
||||
): Promise<ExecuteIntegrationResponse> => {
|
||||
const { variables } = state.typebotsQueue[0].typebot
|
||||
const { sheetId, referenceCell, filter } =
|
||||
const { sheetId, filter, ...parsedOptions } =
|
||||
deepParseVariables(variables)(options)
|
||||
|
||||
const referenceCell =
|
||||
'referenceCell' in parsedOptions && parsedOptions.referenceCell
|
||||
? parsedOptions.referenceCell
|
||||
: null
|
||||
|
||||
if (!options.cellsToUpsert || !sheetId || (!referenceCell && !filter))
|
||||
return { outgoingEdgeId }
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import {
|
||||
Block,
|
||||
BubbleBlockType,
|
||||
Credentials,
|
||||
SessionState,
|
||||
TypebotInSession,
|
||||
@@ -8,7 +7,6 @@ import {
|
||||
import {
|
||||
ChatCompletionOpenAIOptions,
|
||||
OpenAICredentials,
|
||||
chatCompletionMessageRoles,
|
||||
} from '@typebot.io/schemas/features/blocks/integrations/openai'
|
||||
import { byId, isEmpty } from '@typebot.io/lib'
|
||||
import { decrypt } from '@typebot.io/lib/api/encryption/decrypt'
|
||||
@@ -20,6 +18,11 @@ import prisma from '@typebot.io/lib/prisma'
|
||||
import { ExecuteIntegrationResponse } from '../../../types'
|
||||
import { parseVariableNumber } from '../../../variables/parseVariableNumber'
|
||||
import { updateVariablesInSession } from '../../../variables/updateVariablesInSession'
|
||||
import {
|
||||
chatCompletionMessageRoles,
|
||||
defaultOpenAIOptions,
|
||||
} from '@typebot.io/schemas/features/blocks/integrations/openai/constants'
|
||||
import { BubbleBlockType } from '@typebot.io/schemas/features/blocks/bubbles/constants'
|
||||
|
||||
export const createChatCompletionOpenAI = async (
|
||||
state: SessionState,
|
||||
@@ -38,6 +41,7 @@ export const createChatCompletionOpenAI = async (
|
||||
status: 'error',
|
||||
description: 'Make sure to select an OpenAI account',
|
||||
}
|
||||
|
||||
if (!options.credentialsId) {
|
||||
return {
|
||||
outgoingEdgeId,
|
||||
@@ -74,7 +78,7 @@ export const createChatCompletionOpenAI = async (
|
||||
|
||||
const assistantMessageVariableName = typebot.variables.find(
|
||||
(variable) =>
|
||||
options.responseMapping.find(
|
||||
options.responseMapping?.find(
|
||||
(m) => m.valueToExtract === 'Message content'
|
||||
)?.variableId === variable.id
|
||||
)?.name
|
||||
@@ -109,7 +113,7 @@ export const createChatCompletionOpenAI = async (
|
||||
const { chatCompletion, logs } = await executeChatCompletionOpenAIRequest({
|
||||
apiKey,
|
||||
messages,
|
||||
model: options.model,
|
||||
model: options.model ?? defaultOpenAIOptions.model,
|
||||
temperature,
|
||||
baseUrl: options.baseUrl,
|
||||
apiVersion: options.apiVersion,
|
||||
@@ -140,8 +144,8 @@ const isNextBubbleMessageWithAssistantMessage =
|
||||
if (!nextBlock) return false
|
||||
return (
|
||||
nextBlock.type === BubbleBlockType.TEXT &&
|
||||
nextBlock.content.richText?.length > 0 &&
|
||||
nextBlock.content.richText?.at(0)?.children.at(0).text ===
|
||||
(nextBlock.content?.richText?.length ?? 0) > 0 &&
|
||||
nextBlock.content?.richText?.at(0)?.children.at(0).text ===
|
||||
`{{${assistantVariableName}}}`
|
||||
)
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ type Props = Pick<
|
||||
temperature: number | undefined
|
||||
currentLogs?: ChatReply['logs']
|
||||
isRetrying?: boolean
|
||||
} & Pick<OpenAIBlock['options'], 'apiVersion' | 'baseUrl'>
|
||||
} & Pick<NonNullable<OpenAIBlock['options']>, 'apiVersion' | 'baseUrl'>
|
||||
|
||||
export const executeChatCompletionOpenAIRequest = async ({
|
||||
apiKey,
|
||||
|
||||
@@ -7,7 +7,7 @@ export const executeOpenAIBlock = async (
|
||||
state: SessionState,
|
||||
block: OpenAIBlock
|
||||
): Promise<ExecuteIntegrationResponse> => {
|
||||
switch (block.options.task) {
|
||||
switch (block.options?.task) {
|
||||
case 'Create chat completion':
|
||||
return createChatCompletionOpenAI(state, {
|
||||
options: block.options,
|
||||
|
||||
@@ -9,6 +9,7 @@ import { SessionState } from '@typebot.io/schemas/features/chat/sessionState'
|
||||
import { OpenAIStream } from 'ai'
|
||||
import { parseVariableNumber } from '../../../variables/parseVariableNumber'
|
||||
import { ClientOptions, OpenAI } from 'openai'
|
||||
import { defaultOpenAIOptions } from '@typebot.io/schemas/features/blocks/integrations/openai/constants'
|
||||
|
||||
export const getChatCompletionStream =
|
||||
(conn: Connection) =>
|
||||
@@ -53,7 +54,7 @@ export const getChatCompletionStream =
|
||||
const openai = new OpenAI(config)
|
||||
|
||||
const response = await openai.chat.completions.create({
|
||||
model: options.model,
|
||||
model: options.model ?? defaultOpenAIOptions.model,
|
||||
temperature,
|
||||
stream: true,
|
||||
messages,
|
||||
|
||||
@@ -15,7 +15,7 @@ export const parseChatCompletionMessages =
|
||||
} => {
|
||||
const variablesTransformedToList: VariableWithValue[] = []
|
||||
const parsedMessages = messages
|
||||
.flatMap((message) => {
|
||||
?.flatMap((message) => {
|
||||
if (!message.role) return
|
||||
if (message.role === 'Messages sequence ✨') {
|
||||
if (
|
||||
@@ -71,6 +71,29 @@ export const parseChatCompletionMessages =
|
||||
|
||||
return allMessages
|
||||
}
|
||||
if (message.role === 'Dialogue') {
|
||||
if (!message.dialogueVariableId) return
|
||||
const dialogue = (variables.find(
|
||||
(variable) => variable.id === message.dialogueVariableId
|
||||
)?.value ?? []) as string[]
|
||||
|
||||
return dialogue.map<OpenAI.Chat.ChatCompletionMessageParam>(
|
||||
(dialogueItem, index) => {
|
||||
if (index === 0 && message.startsBy === 'assistant')
|
||||
return {
|
||||
role: 'assistant',
|
||||
content: dialogueItem,
|
||||
}
|
||||
return {
|
||||
role:
|
||||
index % (message.startsBy === 'assistant' ? 1 : 2) === 0
|
||||
? 'user'
|
||||
: 'assistant',
|
||||
content: dialogueItem,
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
return {
|
||||
role: message.role,
|
||||
content: parseVariables(variables)(message.content),
|
||||
@@ -83,6 +106,8 @@ export const parseChatCompletionMessages =
|
||||
(message) => isNotEmpty(message?.role) && isNotEmpty(message?.content)
|
||||
) as OpenAI.Chat.ChatCompletionMessageParam[]
|
||||
|
||||
console.log('parsedMessages', parsedMessages)
|
||||
|
||||
return {
|
||||
variablesTransformedToList,
|
||||
messages: parsedMessages,
|
||||
|
||||
@@ -19,7 +19,7 @@ export const resumeChatCompletion =
|
||||
) =>
|
||||
async (message: string, totalTokens?: number) => {
|
||||
let newSessionState = state
|
||||
const newVariables = options.responseMapping.reduce<
|
||||
const newVariables = options.responseMapping?.reduce<
|
||||
VariableWithUnknowValue[]
|
||||
>((newVariables, mapping) => {
|
||||
const { typebot } = newSessionState.typebotsQueue[0]
|
||||
@@ -41,7 +41,7 @@ export const resumeChatCompletion =
|
||||
}
|
||||
return newVariables
|
||||
}, [])
|
||||
if (newVariables.length > 0)
|
||||
if (newVariables && newVariables.length > 0)
|
||||
newSessionState = updateVariablesInSession(newSessionState)(newVariables)
|
||||
return {
|
||||
outgoingEdgeId,
|
||||
|
||||
@@ -9,7 +9,7 @@ export const executePixelBlock = (
|
||||
const { typebot, resultId } = state.typebotsQueue[0]
|
||||
if (
|
||||
!resultId ||
|
||||
!block.options.pixelId ||
|
||||
!block.options?.pixelId ||
|
||||
!block.options.eventType ||
|
||||
state.whatsApp
|
||||
)
|
||||
|
||||
@@ -3,7 +3,6 @@ import {
|
||||
AnswerInSessionState,
|
||||
ReplyLog,
|
||||
SendEmailBlock,
|
||||
SendEmailOptions,
|
||||
SessionState,
|
||||
SmtpCredentials,
|
||||
TypebotInSession,
|
||||
@@ -20,6 +19,7 @@ import { env } from '@typebot.io/env'
|
||||
import { ExecuteIntegrationResponse } from '../../../types'
|
||||
import prisma from '@typebot.io/lib/prisma'
|
||||
import { parseVariables } from '../../../variables/parseVariables'
|
||||
import { defaultSendEmailOptions } from '@typebot.io/schemas/features/blocks/integrations/sendEmail/constants'
|
||||
|
||||
export const executeSendEmailBlock = async (
|
||||
state: SessionState,
|
||||
@@ -41,24 +41,34 @@ export const executeSendEmailBlock = async (
|
||||
}
|
||||
|
||||
const bodyUniqueVariable = findUniqueVariableValue(typebot.variables)(
|
||||
options.body
|
||||
options?.body
|
||||
)
|
||||
const body = bodyUniqueVariable
|
||||
? stringifyUniqueVariableValueAsHtml(bodyUniqueVariable)
|
||||
: parseVariables(typebot.variables, { isInsideHtml: true })(
|
||||
options.body ?? ''
|
||||
options?.body ?? ''
|
||||
)
|
||||
|
||||
if (!options?.recipients)
|
||||
return { outgoingEdgeId: block.outgoingEdgeId, logs }
|
||||
|
||||
try {
|
||||
const sendEmailLogs = await sendEmail({
|
||||
typebot,
|
||||
answers,
|
||||
credentialsId: options.credentialsId,
|
||||
credentialsId:
|
||||
options.credentialsId ?? defaultSendEmailOptions.credentialsId,
|
||||
recipients: options.recipients.map(parseVariables(typebot.variables)),
|
||||
subject: parseVariables(typebot.variables)(options.subject ?? ''),
|
||||
subject: options.subject
|
||||
? parseVariables(typebot.variables)(options?.subject)
|
||||
: undefined,
|
||||
body,
|
||||
cc: (options.cc ?? []).map(parseVariables(typebot.variables)),
|
||||
bcc: (options.bcc ?? []).map(parseVariables(typebot.variables)),
|
||||
cc: options.cc
|
||||
? options.cc.map(parseVariables(typebot.variables))
|
||||
: undefined,
|
||||
bcc: options.bcc
|
||||
? options.bcc.map(parseVariables(typebot.variables))
|
||||
: undefined,
|
||||
replyTo: options.replyTo
|
||||
? parseVariables(typebot.variables)(options.replyTo)
|
||||
: undefined,
|
||||
@@ -91,7 +101,16 @@ const sendEmail = async ({
|
||||
isBodyCode,
|
||||
isCustomBody,
|
||||
fileUrls,
|
||||
}: SendEmailOptions & {
|
||||
}: {
|
||||
credentialsId: string
|
||||
recipients: string[]
|
||||
body: string | undefined
|
||||
subject: string | undefined
|
||||
cc: string[] | undefined
|
||||
bcc: string[] | undefined
|
||||
replyTo: string | undefined
|
||||
isBodyCode: boolean | undefined
|
||||
isCustomBody: boolean | undefined
|
||||
typebot: TypebotInSession
|
||||
answers: AnswerInSessionState[]
|
||||
fileUrls?: string | string[]
|
||||
@@ -216,9 +235,10 @@ const getEmailBody = async ({
|
||||
}: {
|
||||
typebot: TypebotInSession
|
||||
answersInSession: AnswerInSessionState[]
|
||||
} & Pick<SendEmailOptions, 'isCustomBody' | 'isBodyCode' | 'body'>): Promise<
|
||||
{ html?: string; text?: string } | undefined
|
||||
> => {
|
||||
} & Pick<
|
||||
NonNullable<SendEmailBlock['options']>,
|
||||
'isCustomBody' | 'isBodyCode' | 'body'
|
||||
>): Promise<{ html?: string; text?: string } | undefined> => {
|
||||
if (isCustomBody || (isNotDefined(isCustomBody) && !isEmpty(body)))
|
||||
return {
|
||||
html: isBodyCode ? body : undefined,
|
||||
|
||||
@@ -7,22 +7,23 @@ import {
|
||||
Webhook,
|
||||
Variable,
|
||||
WebhookResponse,
|
||||
WebhookOptions,
|
||||
defaultWebhookAttributes,
|
||||
KeyValue,
|
||||
ReplyLog,
|
||||
ExecutableWebhook,
|
||||
AnswerInSessionState,
|
||||
} from '@typebot.io/schemas'
|
||||
import { stringify } from 'qs'
|
||||
import { omit } from '@typebot.io/lib'
|
||||
import { isDefined, isEmpty, omit } from '@typebot.io/lib'
|
||||
import { getDefinedVariables, parseAnswers } from '@typebot.io/lib/results'
|
||||
import got, { Method, HTTPError, OptionsInit } from 'got'
|
||||
import { resumeWebhookExecution } from './resumeWebhookExecution'
|
||||
import { HttpMethod } from '@typebot.io/schemas/features/blocks/integrations/webhook/enums'
|
||||
import { ExecuteIntegrationResponse } from '../../../types'
|
||||
import { parseVariables } from '../../../variables/parseVariables'
|
||||
import prisma from '@typebot.io/lib/prisma'
|
||||
import {
|
||||
HttpMethod,
|
||||
defaultWebhookAttributes,
|
||||
} from '@typebot.io/schemas/features/blocks/integrations/webhook/constants'
|
||||
|
||||
type ParsedWebhook = ExecutableWebhook & {
|
||||
basicAuth: { username?: string; password?: string }
|
||||
@@ -35,22 +36,17 @@ export const executeWebhookBlock = async (
|
||||
): Promise<ExecuteIntegrationResponse> => {
|
||||
const logs: ReplyLog[] = []
|
||||
const webhook =
|
||||
block.options.webhook ??
|
||||
((await prisma.webhook.findUnique({
|
||||
where: { id: block.webhookId },
|
||||
})) as Webhook | null)
|
||||
if (!webhook) {
|
||||
logs.push({
|
||||
status: 'error',
|
||||
description: `Couldn't find webhook with id ${block.webhookId}`,
|
||||
})
|
||||
return { outgoingEdgeId: block.outgoingEdgeId, logs }
|
||||
}
|
||||
const preparedWebhook = prepareWebhookAttributes(webhook, block.options)
|
||||
block.options?.webhook ??
|
||||
('webhookId' in block
|
||||
? ((await prisma.webhook.findUnique({
|
||||
where: { id: block.webhookId },
|
||||
})) as Webhook | null)
|
||||
: null)
|
||||
if (!webhook) return { outgoingEdgeId: block.outgoingEdgeId }
|
||||
const parsedWebhook = await parseWebhookAttributes(
|
||||
state,
|
||||
state.typebotsQueue[0].answers
|
||||
)(preparedWebhook)
|
||||
)({ webhook, isCustomBody: block.options?.isCustomBody })
|
||||
if (!parsedWebhook) {
|
||||
logs.push({
|
||||
status: 'error',
|
||||
@@ -58,7 +54,7 @@ export const executeWebhookBlock = async (
|
||||
})
|
||||
return { outgoingEdgeId: block.outgoingEdgeId, logs }
|
||||
}
|
||||
if (block.options.isExecutedOnClient && !state.whatsApp)
|
||||
if (block.options?.isExecutedOnClient && !state.whatsApp)
|
||||
return {
|
||||
outgoingEdgeId: block.outgoingEdgeId,
|
||||
clientSideActions: [
|
||||
@@ -78,40 +74,36 @@ export const executeWebhookBlock = async (
|
||||
})
|
||||
}
|
||||
|
||||
const prepareWebhookAttributes = (
|
||||
webhook: Webhook,
|
||||
options: WebhookOptions
|
||||
): Webhook => {
|
||||
if (options.isAdvancedConfig === false) {
|
||||
return { ...webhook, body: '{{state}}', ...defaultWebhookAttributes }
|
||||
} else if (options.isCustomBody === false) {
|
||||
return { ...webhook, body: '{{state}}' }
|
||||
}
|
||||
return webhook
|
||||
}
|
||||
|
||||
const checkIfBodyIsAVariable = (body: string) => /^{{.+}}$/.test(body)
|
||||
|
||||
const parseWebhookAttributes =
|
||||
(state: SessionState, answers: AnswerInSessionState[]) =>
|
||||
async (webhook: Webhook): Promise<ParsedWebhook | undefined> => {
|
||||
async ({
|
||||
webhook,
|
||||
isCustomBody,
|
||||
}: {
|
||||
webhook: Webhook
|
||||
isCustomBody?: boolean
|
||||
}): Promise<ParsedWebhook | undefined> => {
|
||||
if (!webhook.url || !webhook.method) return
|
||||
const { typebot } = state.typebotsQueue[0]
|
||||
const basicAuth: { username?: string; password?: string } = {}
|
||||
const basicAuthHeaderIdx = webhook.headers.findIndex(
|
||||
const basicAuthHeaderIdx = webhook.headers?.findIndex(
|
||||
(h) =>
|
||||
h.key?.toLowerCase() === 'authorization' &&
|
||||
h.value?.toLowerCase()?.includes('basic')
|
||||
)
|
||||
const isUsernamePasswordBasicAuth =
|
||||
basicAuthHeaderIdx !== -1 &&
|
||||
webhook.headers[basicAuthHeaderIdx].value?.includes(':')
|
||||
isDefined(basicAuthHeaderIdx) &&
|
||||
webhook.headers?.at(basicAuthHeaderIdx)?.value?.includes(':')
|
||||
if (isUsernamePasswordBasicAuth) {
|
||||
const [username, password] =
|
||||
webhook.headers[basicAuthHeaderIdx].value?.slice(6).split(':') ?? []
|
||||
webhook.headers?.at(basicAuthHeaderIdx)?.value?.slice(6).split(':') ??
|
||||
[]
|
||||
basicAuth.username = username
|
||||
basicAuth.password = password
|
||||
webhook.headers.splice(basicAuthHeaderIdx, 1)
|
||||
webhook.headers?.splice(basicAuthHeaderIdx, 1)
|
||||
}
|
||||
const headers = convertKeyValueTableToObject(
|
||||
webhook.headers,
|
||||
@@ -124,9 +116,11 @@ const parseWebhookAttributes =
|
||||
body: webhook.body,
|
||||
answers,
|
||||
variables: typebot.variables,
|
||||
isCustomBody,
|
||||
})
|
||||
const method = webhook.method ?? defaultWebhookAttributes.method
|
||||
const { data: body, isJson } =
|
||||
bodyContent && webhook.method !== HttpMethod.GET
|
||||
bodyContent && method !== HttpMethod.GET
|
||||
? safeJsonParse(
|
||||
parseVariables(typebot.variables, {
|
||||
isInsideJson: !checkIfBodyIsAVariable(bodyContent),
|
||||
@@ -139,7 +133,7 @@ const parseWebhookAttributes =
|
||||
webhook.url + (queryParams !== '' ? `?${queryParams}` : '')
|
||||
),
|
||||
basicAuth,
|
||||
method: webhook.method,
|
||||
method,
|
||||
headers,
|
||||
body,
|
||||
isJson,
|
||||
@@ -156,7 +150,7 @@ export const executeWebhook = async (
|
||||
const request = {
|
||||
url,
|
||||
method: method as Method,
|
||||
headers,
|
||||
headers: headers ?? {},
|
||||
...(basicAuth ?? {}),
|
||||
json:
|
||||
!contentType?.includes('x-www-form-urlencoded') && body && isJson
|
||||
@@ -222,20 +216,21 @@ const getBodyContent = async ({
|
||||
body,
|
||||
answers,
|
||||
variables,
|
||||
isCustomBody,
|
||||
}: {
|
||||
body?: string | null
|
||||
answers: AnswerInSessionState[]
|
||||
variables: Variable[]
|
||||
isCustomBody?: boolean
|
||||
}): Promise<string | undefined> => {
|
||||
if (!body) return
|
||||
return body === '{{state}}'
|
||||
return isEmpty(body) && isCustomBody !== true
|
||||
? JSON.stringify(
|
||||
parseAnswers({
|
||||
answers,
|
||||
variables: getDefinedVariables(variables),
|
||||
})
|
||||
)
|
||||
: body
|
||||
: body ?? undefined
|
||||
}
|
||||
|
||||
const convertKeyValueTableToObject = (
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import {
|
||||
InputBlock,
|
||||
InputBlockType,
|
||||
LogicBlockType,
|
||||
PublicTypebot,
|
||||
ResultHeaderCell,
|
||||
Block,
|
||||
@@ -11,6 +9,8 @@ import {
|
||||
} from '@typebot.io/schemas'
|
||||
import { isInputBlock, byId, isNotDefined } from '@typebot.io/lib'
|
||||
import { parseResultHeader } from '@typebot.io/lib/results'
|
||||
import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants'
|
||||
import { LogicBlockType } from '@typebot.io/schemas/features/blocks/logic/constants'
|
||||
|
||||
export const parseSampleResult =
|
||||
(
|
||||
@@ -58,11 +58,11 @@ const extractLinkedInputBlocks =
|
||||
extractLinkedInputBlocks(
|
||||
linkedTypebots.find((t) =>
|
||||
'typebotId' in t
|
||||
? t.typebotId === linkedBot.options.typebotId
|
||||
: t.id === linkedBot.options.typebotId
|
||||
? t.typebotId === linkedBot.options?.typebotId
|
||||
: t.id === linkedBot.options?.typebotId
|
||||
) as Typebot | PublicTypebot,
|
||||
linkedTypebots
|
||||
)(linkedBot.options.groupId, 'forward')
|
||||
)(linkedBot.options?.groupId, 'forward')
|
||||
)
|
||||
)
|
||||
: []
|
||||
@@ -117,7 +117,7 @@ const parseResultSample = (
|
||||
const getSampleValue = (block: InputBlock): string => {
|
||||
switch (block.type) {
|
||||
case InputBlockType.CHOICE:
|
||||
return block.options.isMultipleChoice
|
||||
return block.options?.isMultipleChoice
|
||||
? block.items.map((item) => item.content).join(', ')
|
||||
: block.items[0]?.content ?? 'Item'
|
||||
case InputBlockType.DATE:
|
||||
@@ -139,7 +139,7 @@ const getSampleValue = (block: InputBlock): string => {
|
||||
case InputBlockType.PAYMENT:
|
||||
return 'Success'
|
||||
case InputBlockType.PICTURE_CHOICE:
|
||||
return block.options.isMultipleChoice
|
||||
return block.options?.isMultipleChoice
|
||||
? block.items.map((item) => item.title ?? item.pictureSrc).join(', ')
|
||||
: block.items[0]?.title ?? block.items[0]?.pictureSrc ?? 'Item'
|
||||
}
|
||||
@@ -178,16 +178,21 @@ const getGroupIds =
|
||||
) =>
|
||||
(groupId: string): string[] => {
|
||||
const groups = typebot.edges.reduce<string[]>((groupIds, edge) => {
|
||||
const fromGroupId = typebot.groups.find((g) =>
|
||||
g.blocks.some(
|
||||
(b) => 'blockId' in edge.from && b.id === edge.from.blockId
|
||||
)
|
||||
)?.id
|
||||
if (!fromGroupId) return groupIds
|
||||
if (direction === 'forward')
|
||||
return (!existingGroupIds ||
|
||||
!existingGroupIds?.includes(edge.to.groupId)) &&
|
||||
edge.from.groupId === groupId
|
||||
fromGroupId === groupId
|
||||
? [...groupIds, edge.to.groupId]
|
||||
: groupIds
|
||||
return (!existingGroupIds ||
|
||||
!existingGroupIds.includes(edge.from.groupId)) &&
|
||||
return (!existingGroupIds || !existingGroupIds.includes(fromGroupId)) &&
|
||||
edge.to.groupId === groupId
|
||||
? [...groupIds, edge.from.groupId]
|
||||
? [...groupIds, fromGroupId]
|
||||
: groupIds
|
||||
}, [])
|
||||
const newGroups = [...(existingGroupIds ?? []), ...groups]
|
||||
|
||||
@@ -49,7 +49,7 @@ export const resumeWebhookExecution = ({
|
||||
}
|
||||
)
|
||||
|
||||
const newVariables = block.options.responseVariableMapping.reduce<
|
||||
const newVariables = block.options?.responseVariableMapping?.reduce<
|
||||
VariableWithUnknowValue[]
|
||||
>((newVariables, varMapping) => {
|
||||
if (!varMapping?.bodyPath || !varMapping.variableId) return newVariables
|
||||
@@ -66,7 +66,7 @@ export const resumeWebhookExecution = ({
|
||||
return newVariables
|
||||
}
|
||||
}, [])
|
||||
if (newVariables.length > 0) {
|
||||
if (newVariables && newVariables.length > 0) {
|
||||
const newSessionState = updateVariablesInSession(state)(newVariables)
|
||||
return {
|
||||
outgoingEdgeId: block.outgoingEdgeId,
|
||||
|
||||
@@ -20,26 +20,26 @@ export const executeZemanticAiBlock = async (
|
||||
): Promise<ExecuteIntegrationResponse> => {
|
||||
let newSessionState = state
|
||||
|
||||
const noCredentialsError = {
|
||||
status: 'error',
|
||||
description: 'Make sure to select a Zemantic AI account',
|
||||
}
|
||||
|
||||
const zemanticRequestError = {
|
||||
status: 'error',
|
||||
description: 'Could not execute Zemantic AI request',
|
||||
}
|
||||
if (!block.options?.credentialsId)
|
||||
return {
|
||||
outgoingEdgeId: block.outgoingEdgeId,
|
||||
}
|
||||
|
||||
const credentials = await prisma.credentials.findUnique({
|
||||
where: {
|
||||
id: block.options.credentialsId,
|
||||
id: block.options?.credentialsId,
|
||||
},
|
||||
})
|
||||
|
||||
if (!credentials) {
|
||||
console.error('Could not find credentials in database')
|
||||
return {
|
||||
outgoingEdgeId: block.outgoingEdgeId,
|
||||
logs: [noCredentialsError],
|
||||
logs: [
|
||||
{
|
||||
status: 'error',
|
||||
description: 'Make sure to select a Zemantic AI account',
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
const { apiKey } = (await decrypt(
|
||||
@@ -109,7 +109,12 @@ export const executeZemanticAiBlock = async (
|
||||
console.error(e)
|
||||
return {
|
||||
outgoingEdgeId: block.outgoingEdgeId,
|
||||
logs: [zemanticRequestError],
|
||||
logs: [
|
||||
{
|
||||
status: 'error',
|
||||
description: 'Could not execute Zemantic AI request',
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { AbTestBlock, SessionState } from '@typebot.io/schemas'
|
||||
import { ExecuteLogicResponse } from '../../../types'
|
||||
import { defaultAbTestOptions } from '@typebot.io/schemas/features/blocks/logic/abTest/constants'
|
||||
|
||||
export const executeAbTest = (
|
||||
_: SessionState,
|
||||
@@ -7,7 +8,10 @@ export const executeAbTest = (
|
||||
): ExecuteLogicResponse => {
|
||||
const aEdgeId = block.items[0].outgoingEdgeId
|
||||
const random = Math.random() * 100
|
||||
if (random < block.options.aPercent && aEdgeId) {
|
||||
if (
|
||||
random < (block.options?.aPercent ?? defaultAbTestOptions.aPercent) &&
|
||||
aEdgeId
|
||||
) {
|
||||
return { outgoingEdgeId: aEdgeId }
|
||||
}
|
||||
const bEdgeId = block.items[1].outgoingEdgeId
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
import { isNotDefined, isDefined } from '@typebot.io/lib'
|
||||
import {
|
||||
Comparison,
|
||||
ComparisonOperators,
|
||||
Condition,
|
||||
LogicalOperator,
|
||||
Variable,
|
||||
} from '@typebot.io/schemas'
|
||||
import { Comparison, Condition, Variable } from '@typebot.io/schemas'
|
||||
import { findUniqueVariableValue } from '../../../variables/findUniqueVariableValue'
|
||||
import { parseVariables } from '../../../variables/parseVariables'
|
||||
import {
|
||||
LogicalOperator,
|
||||
ComparisonOperators,
|
||||
defaultConditionItemContent,
|
||||
} from '@typebot.io/schemas/features/blocks/logic/condition/constants'
|
||||
|
||||
export const executeCondition =
|
||||
(variables: Variable[]) =>
|
||||
(condition: Condition): boolean =>
|
||||
condition.logicalOperator === LogicalOperator.AND
|
||||
(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))
|
||||
}
|
||||
|
||||
const executeComparison =
|
||||
(variables: Variable[]) =>
|
||||
|
||||
@@ -7,8 +7,8 @@ export const executeConditionBlock = (
|
||||
block: ConditionBlock
|
||||
): ExecuteLogicResponse => {
|
||||
const { variables } = state.typebotsQueue[0].typebot
|
||||
const passedCondition = block.items.find((item) =>
|
||||
executeCondition(variables)(item.content)
|
||||
const passedCondition = block.items.find(
|
||||
(item) => item.content && executeCondition(variables)(item.content)
|
||||
)
|
||||
return {
|
||||
outgoingEdgeId: passedCondition
|
||||
|
||||
@@ -6,22 +6,23 @@ import { JumpBlock } from '@typebot.io/schemas/features/blocks/logic/jump'
|
||||
|
||||
export const executeJumpBlock = (
|
||||
state: SessionState,
|
||||
{ groupId, blockId }: JumpBlock['options']
|
||||
{ groupId, blockId }: JumpBlock['options'] = {}
|
||||
): ExecuteLogicResponse => {
|
||||
if (!groupId) return { outgoingEdgeId: undefined }
|
||||
const { typebot } = state.typebotsQueue[0]
|
||||
const groupToJumpTo = typebot.groups.find((group) => group.id === groupId)
|
||||
const blockToJumpTo =
|
||||
groupToJumpTo?.blocks.find((block) => block.id === blockId) ??
|
||||
groupToJumpTo?.blocks[0]
|
||||
|
||||
if (!blockToJumpTo?.groupId)
|
||||
if (!blockToJumpTo)
|
||||
throw new TRPCError({
|
||||
code: 'INTERNAL_SERVER_ERROR',
|
||||
message: 'Block to jump to is not found',
|
||||
})
|
||||
|
||||
const portalEdge = createPortalEdge({
|
||||
to: { groupId: blockToJumpTo?.groupId, blockId: blockToJumpTo?.id },
|
||||
to: { groupId, blockId: blockToJumpTo?.id },
|
||||
})
|
||||
const newSessionState = addEdgeToTypebot(state, portalEdge)
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ export const executeScript = (
|
||||
block: ScriptBlock
|
||||
): ExecuteLogicResponse => {
|
||||
const { variables } = state.typebotsQueue[0].typebot
|
||||
if (!block.options.content || state.whatsApp)
|
||||
if (!block.options?.content || state.whatsApp)
|
||||
return { outgoingEdgeId: block.outgoingEdgeId }
|
||||
|
||||
const scriptToExecute = parseScriptToExecuteClientSideAction(
|
||||
|
||||
@@ -76,7 +76,7 @@ const evaluateSetVariableExpression =
|
||||
const getExpressionToEvaluate =
|
||||
(state: SessionState) =>
|
||||
(options: SetVariableBlock['options']): string | null => {
|
||||
switch (options.type) {
|
||||
switch (options?.type) {
|
||||
case 'Contact name':
|
||||
return state.whatsApp?.contact.name ?? null
|
||||
case 'Phone number': {
|
||||
@@ -102,6 +102,12 @@ const getExpressionToEvaluate =
|
||||
return `const itemIndex = ${options.mapListItemParams?.baseListVariableId}.indexOf(${options.mapListItemParams?.baseItemVariableId})
|
||||
return ${options.mapListItemParams?.targetListVariableId}.at(itemIndex)`
|
||||
}
|
||||
case 'Append value(s)': {
|
||||
return `if(!${options.item}) return ${options.variableId};
|
||||
if(!${options.variableId}) return [${options.item}];
|
||||
if(!Array.isArray(${options.variableId})) return [${options.variableId}, ${options.item}];
|
||||
return (${options.variableId}).concat(${options.item});`
|
||||
}
|
||||
case 'Empty': {
|
||||
return null
|
||||
}
|
||||
@@ -117,7 +123,7 @@ const getExpressionToEvaluate =
|
||||
}
|
||||
case 'Custom':
|
||||
case undefined: {
|
||||
return options.expressionToEvaluate ?? null
|
||||
return options?.expressionToEvaluate ?? null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,19 +14,21 @@ import { isNotDefined } from '@typebot.io/lib/utils'
|
||||
import { createResultIfNotExist } from '../../../queries/createResultIfNotExist'
|
||||
import { executeJumpBlock } from '../jump/executeJumpBlock'
|
||||
import prisma from '@typebot.io/lib/prisma'
|
||||
import { defaultTypebotLinkOptions } from '@typebot.io/schemas/features/blocks/logic/typebotLink/constants'
|
||||
import { saveVisitedEdges } from '../../../queries/saveVisitedEdges'
|
||||
|
||||
export const executeTypebotLink = async (
|
||||
state: SessionState,
|
||||
block: TypebotLinkBlock
|
||||
): Promise<ExecuteLogicResponse> => {
|
||||
const logs: ReplyLog[] = []
|
||||
const typebotId = block.options.typebotId
|
||||
const typebotId = block.options?.typebotId
|
||||
if (
|
||||
typebotId === 'current' ||
|
||||
typebotId === state.typebotsQueue[0].typebot.id
|
||||
) {
|
||||
return executeJumpBlock(state, {
|
||||
groupId: block.options.groupId,
|
||||
groupId: block.options?.groupId,
|
||||
})
|
||||
}
|
||||
if (!typebotId) {
|
||||
@@ -42,7 +44,7 @@ export const executeTypebotLink = async (
|
||||
logs.push({
|
||||
status: 'error',
|
||||
description: `Failed to link typebot`,
|
||||
details: `Typebot with ID ${block.options.typebotId} not found`,
|
||||
details: `Typebot with ID ${block.options?.typebotId} not found`,
|
||||
})
|
||||
return { outgoingEdgeId: block.outgoingEdgeId, logs }
|
||||
}
|
||||
@@ -53,7 +55,7 @@ export const executeTypebotLink = async (
|
||||
)
|
||||
|
||||
const nextGroupId =
|
||||
block.options.groupId ??
|
||||
block.options?.groupId ??
|
||||
linkedTypebot.groups.find((group) =>
|
||||
group.blocks.some((block) => block.type === 'start')
|
||||
)?.id
|
||||
@@ -61,7 +63,7 @@ export const executeTypebotLink = async (
|
||||
logs.push({
|
||||
status: 'error',
|
||||
description: `Failed to link typebot`,
|
||||
details: `Group with ID "${block.options.groupId}" not found`,
|
||||
details: `Group with ID "${block.options?.groupId}" not found`,
|
||||
})
|
||||
return { outgoingEdgeId: block.outgoingEdgeId, logs }
|
||||
}
|
||||
@@ -96,12 +98,14 @@ const addLinkedTypebotToState = async (
|
||||
}
|
||||
: currentTypebotInQueue
|
||||
|
||||
const shouldMergeResults = block.options.mergeResults !== false
|
||||
const shouldMergeResults =
|
||||
currentTypebotInQueue.typebot.version === '6'
|
||||
? block.options?.mergeResults ?? defaultTypebotLinkOptions.mergeResults
|
||||
: block.options?.mergeResults !== false
|
||||
|
||||
if (
|
||||
currentTypebotInQueue.resultId &&
|
||||
currentTypebotInQueue.answers.length === 0 &&
|
||||
shouldMergeResults
|
||||
currentTypebotInQueue.answers.length === 0
|
||||
) {
|
||||
await createResultIfNotExist({
|
||||
resultId: currentTypebotInQueue.resultId,
|
||||
@@ -159,11 +163,10 @@ const createResumeEdgeIfNecessary = (
|
||||
return {
|
||||
id: createId(),
|
||||
from: {
|
||||
groupId: '',
|
||||
blockId: '',
|
||||
},
|
||||
to: {
|
||||
groupId: nextBlockInGroup.groupId,
|
||||
groupId: currentGroup.id,
|
||||
blockId: nextBlockInGroup.id,
|
||||
},
|
||||
}
|
||||
@@ -196,6 +199,7 @@ const fetchTypebot = async (state: SessionState, typebotId: string) => {
|
||||
edges: true,
|
||||
groups: true,
|
||||
variables: true,
|
||||
events: true,
|
||||
},
|
||||
})
|
||||
return typebotInSessionStateSchema.parse(typebot)
|
||||
@@ -208,6 +212,7 @@ const fetchTypebot = async (state: SessionState, typebotId: string) => {
|
||||
edges: true,
|
||||
groups: true,
|
||||
variables: true,
|
||||
events: true,
|
||||
},
|
||||
})
|
||||
if (!typebot) return null
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { User } from '@typebot.io/prisma'
|
||||
import {
|
||||
LogicBlockType,
|
||||
Block,
|
||||
PublicTypebot,
|
||||
Typebot,
|
||||
TypebotLinkBlock,
|
||||
} from '@typebot.io/schemas'
|
||||
import { isDefined } from '@typebot.io/lib'
|
||||
import { fetchLinkedTypebots } from './fetchLinkedTypebots'
|
||||
import { LogicBlockType } from '@typebot.io/schemas/features/blocks/logic/constants'
|
||||
|
||||
type Props = {
|
||||
typebots: Pick<PublicTypebot, 'groups'>[]
|
||||
@@ -23,18 +24,18 @@ export const getPreviouslyLinkedTypebots =
|
||||
.flatMap((typebot) =>
|
||||
(
|
||||
typebot.groups
|
||||
.flatMap((group) => group.blocks)
|
||||
.flatMap<Block>((group) => group.blocks)
|
||||
.filter(
|
||||
(block) =>
|
||||
block.type === LogicBlockType.TYPEBOT_LINK &&
|
||||
isDefined(block.options.typebotId) &&
|
||||
isDefined(block.options?.typebotId) &&
|
||||
!capturedLinkedBots.some(
|
||||
(bot) =>
|
||||
('typebotId' in bot ? bot.typebotId : bot.id) ===
|
||||
block.options.typebotId
|
||||
block.options?.typebotId
|
||||
)
|
||||
) as TypebotLinkBlock[]
|
||||
).map((s) => s.options.typebotId)
|
||||
).map((b) => b.options?.typebotId)
|
||||
)
|
||||
.filter(isDefined)
|
||||
if (linkedTypebotIds.length === 0) return capturedLinkedBots
|
||||
|
||||
@@ -1,20 +1,27 @@
|
||||
import { ExecuteLogicResponse } from '../../../types'
|
||||
import { SessionState, WaitBlock } from '@typebot.io/schemas'
|
||||
import { parseVariables } from '../../../variables/parseVariables'
|
||||
import { isNotDefined } from '@typebot.io/lib'
|
||||
|
||||
export const executeWait = (
|
||||
state: SessionState,
|
||||
block: WaitBlock
|
||||
): ExecuteLogicResponse => {
|
||||
const { variables } = state.typebotsQueue[0].typebot
|
||||
if (!block.options?.secondsToWaitFor)
|
||||
return { outgoingEdgeId: block.outgoingEdgeId }
|
||||
|
||||
const parsedSecondsToWaitFor = safeParseInt(
|
||||
parseVariables(variables)(block.options.secondsToWaitFor)
|
||||
)
|
||||
|
||||
if (isNotDefined(parsedSecondsToWaitFor))
|
||||
return { outgoingEdgeId: block.outgoingEdgeId }
|
||||
|
||||
return {
|
||||
outgoingEdgeId: block.outgoingEdgeId,
|
||||
clientSideActions:
|
||||
parsedSecondsToWaitFor || block.options.shouldPause
|
||||
parsedSecondsToWaitFor || block.options?.shouldPause
|
||||
? [
|
||||
{
|
||||
wait: { secondsToWaitFor: parsedSecondsToWaitFor ?? 0 },
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
import {
|
||||
TypingEmulation,
|
||||
defaultSettings,
|
||||
} from '@typebot.io/schemas/features/typebot/settings'
|
||||
import { Settings } from '@typebot.io/schemas/features/typebot/settings'
|
||||
import { defaultSettings } from '@typebot.io/schemas/features/typebot/settings/constants'
|
||||
|
||||
type Props = {
|
||||
bubbleContent: string
|
||||
typingSettings?: TypingEmulation
|
||||
typingSettings?: Settings['typingEmulation']
|
||||
}
|
||||
|
||||
export const computeTypingDuration = ({
|
||||
bubbleContent,
|
||||
typingSettings = defaultSettings({ isBrandingEnabled: false })
|
||||
.typingEmulation,
|
||||
typingSettings,
|
||||
}: Props) => {
|
||||
let wordCount = bubbleContent.match(/(\w+)/g)?.length ?? 0
|
||||
if (wordCount === 0) wordCount = bubbleContent.length
|
||||
const typedWordsPerMinute = typingSettings.speed
|
||||
let typingTimeout = typingSettings.enabled
|
||||
? (wordCount / typedWordsPerMinute) * 60000
|
||||
: 0
|
||||
if (typingTimeout > typingSettings.maxDelay * 1000)
|
||||
typingTimeout = typingSettings.maxDelay * 1000
|
||||
const { enabled, speed, maxDelay } = {
|
||||
enabled: typingSettings?.enabled ?? defaultSettings.typingEmulation.enabled,
|
||||
speed: typingSettings?.speed ?? defaultSettings.typingEmulation.speed,
|
||||
maxDelay:
|
||||
typingSettings?.maxDelay ?? defaultSettings.typingEmulation.maxDelay,
|
||||
}
|
||||
const typedWordsPerMinute = speed
|
||||
let typingTimeout = enabled ? (wordCount / typedWordsPerMinute) * 60000 : 0
|
||||
if (typingTimeout > maxDelay * 1000) typingTimeout = maxDelay * 1000
|
||||
return typingTimeout
|
||||
}
|
||||
|
||||
@@ -1,15 +1,10 @@
|
||||
import {
|
||||
AnswerInSessionState,
|
||||
Block,
|
||||
BubbleBlockType,
|
||||
ChatReply,
|
||||
Group,
|
||||
InputBlock,
|
||||
InputBlockType,
|
||||
IntegrationBlockType,
|
||||
LogicBlockType,
|
||||
SessionState,
|
||||
defaultPaymentInputOptions,
|
||||
invalidEmailDefaultRetryMessage,
|
||||
} from '@typebot.io/schemas'
|
||||
import { isInputBlock, byId } from '@typebot.io/lib'
|
||||
import { executeGroup, parseInput } from './executeGroup'
|
||||
@@ -31,6 +26,17 @@ import { updateVariablesInSession } from './variables/updateVariablesInSession'
|
||||
import { startBotFlow } from './startBotFlow'
|
||||
import { TRPCError } from '@trpc/server'
|
||||
import { parseNumber } from './blocks/inputs/number/parseNumber'
|
||||
import { BubbleBlockType } from '@typebot.io/schemas/features/blocks/bubbles/constants'
|
||||
import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants'
|
||||
import { defaultPaymentInputOptions } from '@typebot.io/schemas/features/blocks/inputs/payment/constants'
|
||||
import { IntegrationBlockType } from '@typebot.io/schemas/features/blocks/integrations/constants'
|
||||
import { LogicBlockType } from '@typebot.io/schemas/features/blocks/logic/constants'
|
||||
import { defaultEmailInputOptions } from '@typebot.io/schemas/features/blocks/inputs/email/constants'
|
||||
import { defaultChoiceInputOptions } from '@typebot.io/schemas/features/blocks/inputs/choice/constants'
|
||||
import { defaultPictureChoiceOptions } from '@typebot.io/schemas/features/blocks/inputs/pictureChoice/constants'
|
||||
import { defaultFileInputOptions } from '@typebot.io/schemas/features/blocks/inputs/file/constants'
|
||||
import { VisitedEdge } from '@typebot.io/prisma'
|
||||
import { getBlockById } from '@typebot.io/lib/getBlockById'
|
||||
|
||||
type Params = {
|
||||
version: 1 | 2
|
||||
@@ -39,23 +45,21 @@ type Params = {
|
||||
export const continueBotFlow = async (
|
||||
reply: string | undefined,
|
||||
{ state, version }: Params
|
||||
): Promise<ChatReply & { newSessionState: SessionState }> => {
|
||||
): Promise<
|
||||
ChatReply & { newSessionState: SessionState; visitedEdges: VisitedEdge[] }
|
||||
> => {
|
||||
let firstBubbleWasStreamed = false
|
||||
let newSessionState = { ...state }
|
||||
const visitedEdges: VisitedEdge[] = []
|
||||
|
||||
if (!newSessionState.currentBlock) return startBotFlow({ state, version })
|
||||
if (!newSessionState.currentBlockId) return startBotFlow({ state, version })
|
||||
|
||||
const group = state.typebotsQueue[0].typebot.groups.find(
|
||||
(group) => group.id === state.currentBlock?.groupId
|
||||
const { block, group, blockIndex } = getBlockById(
|
||||
newSessionState.currentBlockId,
|
||||
state.typebotsQueue[0].typebot.groups
|
||||
)
|
||||
const blockIndex =
|
||||
group?.blocks.findIndex(
|
||||
(block) => block.id === state.currentBlock?.blockId
|
||||
) ?? -1
|
||||
|
||||
const block = blockIndex >= 0 ? group?.blocks[blockIndex ?? 0] : null
|
||||
|
||||
if (!block || !group)
|
||||
if (!block)
|
||||
throw new TRPCError({
|
||||
code: 'INTERNAL_SERVER_ERROR',
|
||||
message: 'Group / block not found',
|
||||
@@ -63,7 +67,7 @@ export const continueBotFlow = async (
|
||||
|
||||
if (block.type === LogicBlockType.SET_VARIABLE) {
|
||||
const existingVariable = state.typebotsQueue[0].typebot.variables.find(
|
||||
byId(block.options.variableId)
|
||||
byId(block.options?.variableId)
|
||||
)
|
||||
if (existingVariable && reply) {
|
||||
const newVariable = {
|
||||
@@ -81,7 +85,7 @@ export const continueBotFlow = async (
|
||||
if (result.newSessionState) newSessionState = result.newSessionState
|
||||
} else if (
|
||||
block.type === IntegrationBlockType.OPEN_AI &&
|
||||
block.options.task === 'Create chat completion'
|
||||
block.options?.task === 'Create chat completion'
|
||||
) {
|
||||
firstBubbleWasStreamed = true
|
||||
if (reply) {
|
||||
@@ -102,20 +106,12 @@ export const continueBotFlow = async (
|
||||
return {
|
||||
...(await parseRetryMessage(newSessionState)(block)),
|
||||
newSessionState,
|
||||
visitedEdges: [],
|
||||
}
|
||||
|
||||
formattedReply =
|
||||
'reply' in parsedReplyResult ? parsedReplyResult.reply : undefined
|
||||
const nextEdgeId = getOutgoingEdgeId(newSessionState)(block, formattedReply)
|
||||
const itemId = nextEdgeId
|
||||
? newSessionState.typebotsQueue[0].typebot.edges.find(byId(nextEdgeId))
|
||||
?.from.itemId
|
||||
: undefined
|
||||
newSessionState = await processAndSaveAnswer(
|
||||
state,
|
||||
block,
|
||||
itemId
|
||||
)(formattedReply)
|
||||
newSessionState = await processAndSaveAnswer(state, block)(formattedReply)
|
||||
}
|
||||
|
||||
const groupHasMoreBlocks = blockIndex < group.blocks.length - 1
|
||||
@@ -127,8 +123,8 @@ export const continueBotFlow = async (
|
||||
{
|
||||
...group,
|
||||
blocks: group.blocks.slice(blockIndex + 1),
|
||||
},
|
||||
{ version, state: newSessionState, firstBubbleWasStreamed }
|
||||
} as Group,
|
||||
{ version, state: newSessionState, visitedEdges, firstBubbleWasStreamed }
|
||||
)
|
||||
return {
|
||||
...chatReply,
|
||||
@@ -143,10 +139,13 @@ export const continueBotFlow = async (
|
||||
newSessionState,
|
||||
lastMessageNewFormat:
|
||||
formattedReply !== reply ? formattedReply : undefined,
|
||||
visitedEdges,
|
||||
}
|
||||
|
||||
const nextGroup = await getNextGroup(newSessionState)(nextEdgeId)
|
||||
|
||||
if (nextGroup.visitedEdge) visitedEdges.push(nextGroup.visitedEdge)
|
||||
|
||||
newSessionState = nextGroup.newSessionState
|
||||
|
||||
if (!nextGroup.group)
|
||||
@@ -155,12 +154,14 @@ export const continueBotFlow = async (
|
||||
newSessionState,
|
||||
lastMessageNewFormat:
|
||||
formattedReply !== reply ? formattedReply : undefined,
|
||||
visitedEdges,
|
||||
}
|
||||
|
||||
const chatReply = await executeGroup(nextGroup.group, {
|
||||
version,
|
||||
state: newSessionState,
|
||||
firstBubbleWasStreamed,
|
||||
visitedEdges,
|
||||
})
|
||||
|
||||
return {
|
||||
@@ -170,10 +171,10 @@ export const continueBotFlow = async (
|
||||
}
|
||||
|
||||
const processAndSaveAnswer =
|
||||
(state: SessionState, block: InputBlock, itemId?: string) =>
|
||||
(state: SessionState, block: InputBlock) =>
|
||||
async (reply: string | undefined): Promise<SessionState> => {
|
||||
if (!reply) return state
|
||||
let newState = await saveAnswer(state, block, itemId)(reply)
|
||||
let newState = await saveAnswer(state, block)(reply)
|
||||
newState = saveVariableValueIfAny(newState, block)(reply)
|
||||
return newState
|
||||
}
|
||||
@@ -181,9 +182,9 @@ const processAndSaveAnswer =
|
||||
const saveVariableValueIfAny =
|
||||
(state: SessionState, block: InputBlock) =>
|
||||
(reply: string): SessionState => {
|
||||
if (!block.options.variableId) return state
|
||||
if (!block.options?.variableId) return state
|
||||
const foundVariable = state.typebotsQueue[0].typebot.variables.find(
|
||||
(variable) => variable.id === block.options.variableId
|
||||
(variable) => variable.id === block.options?.variableId
|
||||
)
|
||||
if (!foundVariable) return state
|
||||
|
||||
@@ -203,6 +204,7 @@ const parseRetryMessage =
|
||||
(state: SessionState) =>
|
||||
async (block: InputBlock): Promise<Pick<ChatReply, 'messages' | 'input'>> => {
|
||||
const retryMessage =
|
||||
block.options &&
|
||||
'retryMessageContent' in block.options &&
|
||||
block.options.retryMessageContent
|
||||
? block.options.retryMessageContent
|
||||
@@ -224,34 +226,35 @@ const parseRetryMessage =
|
||||
const parseDefaultRetryMessage = (block: InputBlock): string => {
|
||||
switch (block.type) {
|
||||
case InputBlockType.EMAIL:
|
||||
return invalidEmailDefaultRetryMessage
|
||||
return defaultEmailInputOptions.retryMessageContent
|
||||
case InputBlockType.PAYMENT:
|
||||
return defaultPaymentInputOptions.retryMessageContent as string
|
||||
return defaultPaymentInputOptions.retryMessageContent
|
||||
default:
|
||||
return 'Invalid message. Please, try again.'
|
||||
}
|
||||
}
|
||||
|
||||
const saveAnswer =
|
||||
(state: SessionState, block: InputBlock, itemId?: string) =>
|
||||
(state: SessionState, block: InputBlock) =>
|
||||
async (reply: string): Promise<SessionState> => {
|
||||
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({
|
||||
block,
|
||||
answer: {
|
||||
blockId: block.id,
|
||||
itemId,
|
||||
groupId: block.groupId,
|
||||
groupId,
|
||||
content: reply,
|
||||
variableId: block.options.variableId,
|
||||
variableId: block.options?.variableId,
|
||||
},
|
||||
reply,
|
||||
state,
|
||||
itemId,
|
||||
})
|
||||
|
||||
const key = block.options.variableId
|
||||
const key = block.options?.variableId
|
||||
? state.typebotsQueue[0].typebot.variables.find(
|
||||
(variable) => variable.id === block.options.variableId
|
||||
(variable) => variable.id === block.options?.variableId
|
||||
)?.name
|
||||
: state.typebotsQueue[0].typebot.groups.find((group) =>
|
||||
group.blocks.find((blockInGroup) => blockInGroup.id === block.id)
|
||||
@@ -289,7 +292,10 @@ const getOutgoingEdgeId =
|
||||
const variables = state.typebotsQueue[0].typebot.variables
|
||||
if (
|
||||
block.type === InputBlockType.CHOICE &&
|
||||
!block.options.isMultipleChoice &&
|
||||
!(
|
||||
block.options?.isMultipleChoice ??
|
||||
defaultChoiceInputOptions.isMultipleChoice
|
||||
) &&
|
||||
reply
|
||||
) {
|
||||
const matchedItem = block.items.find(
|
||||
@@ -301,7 +307,10 @@ const getOutgoingEdgeId =
|
||||
}
|
||||
if (
|
||||
block.type === InputBlockType.PICTURE_CHOICE &&
|
||||
!block.options.isMultipleChoice &&
|
||||
!(
|
||||
block.options?.isMultipleChoice ??
|
||||
defaultPictureChoiceOptions.isMultipleChoice
|
||||
) &&
|
||||
reply
|
||||
) {
|
||||
const matchedItem = block.items.find(
|
||||
@@ -328,7 +337,7 @@ const parseReply =
|
||||
if (!inputValue) return { status: 'fail' }
|
||||
const formattedPhone = formatPhoneNumber(
|
||||
inputValue,
|
||||
block.options.defaultCountryCode
|
||||
block.options?.defaultCountryCode
|
||||
)
|
||||
if (!formattedPhone) return { status: 'fail' }
|
||||
return { status: 'success', reply: formattedPhone }
|
||||
@@ -358,7 +367,7 @@ const parseReply =
|
||||
}
|
||||
case InputBlockType.FILE: {
|
||||
if (!inputValue)
|
||||
return block.options.isRequired
|
||||
return block.options?.isRequired ?? defaultFileInputOptions.isRequired
|
||||
? { status: 'fail' }
|
||||
: { status: 'skip' }
|
||||
return { status: 'success', reply: inputValue }
|
||||
|
||||
@@ -2,7 +2,6 @@ import {
|
||||
ChatReply,
|
||||
Group,
|
||||
InputBlock,
|
||||
InputBlockType,
|
||||
RuntimeOptions,
|
||||
SessionState,
|
||||
} from '@typebot.io/schemas'
|
||||
@@ -22,7 +21,12 @@ import { injectVariableValuesInPictureChoiceBlock } from './blocks/inputs/pictur
|
||||
import { getPrefilledInputValue } from './getPrefilledValue'
|
||||
import { parseDateInput } from './blocks/inputs/date/parseDateInput'
|
||||
import { deepParseVariables } from './variables/deepParseVariables'
|
||||
import { parseBubbleBlock } from './parseBubbleBlock'
|
||||
import {
|
||||
BubbleBlockWithDefinedContent,
|
||||
parseBubbleBlock,
|
||||
} from './parseBubbleBlock'
|
||||
import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants'
|
||||
import { VisitedEdge } from '@typebot.io/prisma'
|
||||
|
||||
type ContextProps = {
|
||||
version: 1 | 2
|
||||
@@ -30,6 +34,7 @@ type ContextProps = {
|
||||
currentReply?: ChatReply
|
||||
currentLastBubbleId?: string
|
||||
firstBubbleWasStreamed?: boolean
|
||||
visitedEdges: VisitedEdge[]
|
||||
}
|
||||
|
||||
export const executeGroup = async (
|
||||
@@ -37,11 +42,14 @@ export const executeGroup = async (
|
||||
{
|
||||
version,
|
||||
state,
|
||||
visitedEdges,
|
||||
currentReply,
|
||||
currentLastBubbleId,
|
||||
firstBubbleWasStreamed,
|
||||
}: ContextProps
|
||||
): Promise<ChatReply & { newSessionState: SessionState }> => {
|
||||
): Promise<
|
||||
ChatReply & { newSessionState: SessionState; visitedEdges: VisitedEdge[] }
|
||||
> => {
|
||||
const messages: ChatReply['messages'] = currentReply?.messages ?? []
|
||||
let clientSideActions: ChatReply['clientSideActions'] =
|
||||
currentReply?.clientSideActions
|
||||
@@ -57,11 +65,12 @@ export const executeGroup = async (
|
||||
nextEdgeId = block.outgoingEdgeId
|
||||
|
||||
if (isBubbleBlock(block)) {
|
||||
if (firstBubbleWasStreamed && index === 0) continue
|
||||
if (!block.content || (firstBubbleWasStreamed && index === 0)) continue
|
||||
messages.push(
|
||||
parseBubbleBlock(block, {
|
||||
parseBubbleBlock(block as BubbleBlockWithDefinedContent, {
|
||||
version,
|
||||
variables: newSessionState.typebotsQueue[0].typebot.variables,
|
||||
typebotVersion: newSessionState.typebotsQueue[0].typebot.version,
|
||||
})
|
||||
)
|
||||
lastBubbleBlockId = block.id
|
||||
@@ -74,13 +83,11 @@ export const executeGroup = async (
|
||||
input: await parseInput(newSessionState)(block),
|
||||
newSessionState: {
|
||||
...newSessionState,
|
||||
currentBlock: {
|
||||
groupId: group.id,
|
||||
blockId: block.id,
|
||||
},
|
||||
currentBlockId: block.id,
|
||||
},
|
||||
clientSideActions,
|
||||
logs,
|
||||
visitedEdges,
|
||||
}
|
||||
const executionResponse = isLogicBlock(block)
|
||||
? await executeLogic(newSessionState)(block)
|
||||
@@ -113,13 +120,11 @@ export const executeGroup = async (
|
||||
messages,
|
||||
newSessionState: {
|
||||
...newSessionState,
|
||||
currentBlock: {
|
||||
groupId: group.id,
|
||||
blockId: block.id,
|
||||
},
|
||||
currentBlockId: block.id,
|
||||
},
|
||||
clientSideActions,
|
||||
logs,
|
||||
visitedEdges,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -131,19 +136,22 @@ export const executeGroup = async (
|
||||
}
|
||||
|
||||
if (!nextEdgeId && newSessionState.typebotsQueue.length === 1)
|
||||
return { messages, newSessionState, clientSideActions, logs }
|
||||
return { messages, newSessionState, clientSideActions, logs, visitedEdges }
|
||||
|
||||
const nextGroup = await getNextGroup(newSessionState)(nextEdgeId ?? undefined)
|
||||
|
||||
newSessionState = nextGroup.newSessionState
|
||||
|
||||
if (nextGroup.visitedEdge) visitedEdges.push(nextGroup.visitedEdge)
|
||||
|
||||
if (!nextGroup.group) {
|
||||
return { messages, newSessionState, clientSideActions, logs }
|
||||
return { messages, newSessionState, clientSideActions, logs, visitedEdges }
|
||||
}
|
||||
|
||||
return executeGroup(nextGroup.group, {
|
||||
version,
|
||||
state: newSessionState,
|
||||
visitedEdges,
|
||||
currentReply: {
|
||||
messages,
|
||||
clientSideActions,
|
||||
@@ -188,14 +196,14 @@ export const parseInput =
|
||||
...parsedBlock,
|
||||
options: {
|
||||
...parsedBlock.options,
|
||||
min: isNotEmpty(parsedBlock.options.min as string)
|
||||
? Number(parsedBlock.options.min)
|
||||
min: isNotEmpty(parsedBlock.options?.min as string)
|
||||
? Number(parsedBlock.options?.min)
|
||||
: undefined,
|
||||
max: isNotEmpty(parsedBlock.options.max as string)
|
||||
? Number(parsedBlock.options.max)
|
||||
max: isNotEmpty(parsedBlock.options?.max as string)
|
||||
? Number(parsedBlock.options?.max)
|
||||
: undefined,
|
||||
step: isNotEmpty(parsedBlock.options.step as string)
|
||||
? Number(parsedBlock.options.step)
|
||||
step: isNotEmpty(parsedBlock.options?.step as string)
|
||||
? Number(parsedBlock.options?.step)
|
||||
: undefined,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -6,12 +6,9 @@ import { executeGoogleAnalyticsBlock } from './blocks/integrations/googleAnalyti
|
||||
import { executeGoogleSheetBlock } from './blocks/integrations/googleSheets/executeGoogleSheetBlock'
|
||||
import { executePixelBlock } from './blocks/integrations/pixel/executePixelBlock'
|
||||
import { executeZemanticAiBlock } from './blocks/integrations/zemanticAi/executeZemanticAiBlock'
|
||||
import {
|
||||
IntegrationBlock,
|
||||
IntegrationBlockType,
|
||||
SessionState,
|
||||
} from '@typebot.io/schemas'
|
||||
import { IntegrationBlock, SessionState } from '@typebot.io/schemas'
|
||||
import { ExecuteIntegrationResponse } from './types'
|
||||
import { IntegrationBlockType } from '@typebot.io/schemas/features/blocks/integrations/constants'
|
||||
|
||||
export const executeIntegration =
|
||||
(state: SessionState) =>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { executeWait } from './blocks/logic/wait/executeWait'
|
||||
import { LogicBlock, LogicBlockType, SessionState } from '@typebot.io/schemas'
|
||||
import { LogicBlock, SessionState } from '@typebot.io/schemas'
|
||||
import { ExecuteLogicResponse } from './types'
|
||||
import { executeScript } from './blocks/logic/script/executeScript'
|
||||
import { executeJumpBlock } from './blocks/logic/jump/executeJumpBlock'
|
||||
@@ -8,6 +8,7 @@ import { executeConditionBlock } from './blocks/logic/condition/executeCondition
|
||||
import { executeSetVariable } from './blocks/logic/setVariable/executeSetVariable'
|
||||
import { executeTypebotLink } from './blocks/logic/typebotLink/executeTypebotLink'
|
||||
import { executeAbTest } from './blocks/logic/abTest/executeAbTest'
|
||||
import { LogicBlockType } from '@typebot.io/schemas/features/blocks/logic/constants'
|
||||
|
||||
export const executeLogic =
|
||||
(state: SessionState) =>
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { byId, isDefined, isNotDefined } from '@typebot.io/lib'
|
||||
import { Group, SessionState, VariableWithValue } from '@typebot.io/schemas'
|
||||
import { upsertResult } from './queries/upsertResult'
|
||||
import { VisitedEdge } from '@typebot.io/prisma'
|
||||
|
||||
export type NextGroup = {
|
||||
group?: Group
|
||||
newSessionState: SessionState
|
||||
visitedEdge?: VisitedEdge
|
||||
}
|
||||
|
||||
export const getNextGroup =
|
||||
@@ -93,11 +95,23 @@ export const getNextGroup =
|
||||
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,
|
||||
},
|
||||
newSessionState: state,
|
||||
visitedEdge: resultId
|
||||
? {
|
||||
index: currentVisitedEdgeIndex,
|
||||
edgeId: nextEdge.id,
|
||||
resultId,
|
||||
}
|
||||
: undefined,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { isDefined } from '@typebot.io/lib/utils'
|
||||
import { InputBlock } from '@typebot.io/schemas/features/blocks/schemas'
|
||||
import { InputBlock } from '@typebot.io/schemas'
|
||||
import { Variable } from '@typebot.io/schemas/features/typebot/variable'
|
||||
|
||||
export const getPrefilledInputValue =
|
||||
(variables: Variable[]) => (block: InputBlock) => {
|
||||
const variableValue = variables.find(
|
||||
(variable) =>
|
||||
variable.id === block.options.variableId && isDefined(variable.value)
|
||||
variable.id === block.options?.variableId && isDefined(variable.value)
|
||||
)?.value
|
||||
if (!variableValue || Array.isArray(variableValue)) return
|
||||
return variableValue
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
import { parseVideoUrl } from '@typebot.io/lib/parseVideoUrl'
|
||||
import {
|
||||
BubbleBlock,
|
||||
Variable,
|
||||
ChatReply,
|
||||
BubbleBlockType,
|
||||
} from '@typebot.io/schemas'
|
||||
import { BubbleBlock, Variable, ChatReply, Typebot } from '@typebot.io/schemas'
|
||||
import { deepParseVariables } from './variables/deepParseVariables'
|
||||
import { isEmpty, isNotEmpty } from '@typebot.io/lib/utils'
|
||||
import {
|
||||
@@ -16,31 +11,42 @@ import {
|
||||
createDeserializeMdPlugin,
|
||||
deserializeMd,
|
||||
} from '@udecode/plate-serializer-md'
|
||||
import { BubbleBlockType } from '@typebot.io/schemas/features/blocks/bubbles/constants'
|
||||
import { defaultVideoBubbleContent } from '@typebot.io/schemas/features/blocks/bubbles/video/constants'
|
||||
|
||||
type Params = {
|
||||
version: 1 | 2
|
||||
typebotVersion: Typebot['version']
|
||||
variables: Variable[]
|
||||
}
|
||||
|
||||
export type BubbleBlockWithDefinedContent = BubbleBlock & {
|
||||
content: NonNullable<BubbleBlock['content']>
|
||||
}
|
||||
|
||||
export const parseBubbleBlock = (
|
||||
block: BubbleBlock,
|
||||
{ version, variables }: Params
|
||||
block: BubbleBlockWithDefinedContent,
|
||||
{ version, variables, typebotVersion }: Params
|
||||
): ChatReply['messages'][0] => {
|
||||
switch (block.type) {
|
||||
case BubbleBlockType.TEXT: {
|
||||
if (version === 1)
|
||||
return deepParseVariables(
|
||||
variables,
|
||||
{},
|
||||
{ takeLatestIfList: true }
|
||||
)(block)
|
||||
return {
|
||||
...block,
|
||||
content: {
|
||||
...block.content,
|
||||
richText: (block.content?.richText ?? []).map(
|
||||
deepParseVariables(variables)
|
||||
),
|
||||
},
|
||||
}
|
||||
return {
|
||||
...block,
|
||||
content: {
|
||||
...block.content,
|
||||
richText: parseVariablesInRichText(block.content.richText, {
|
||||
richText: parseVariablesInRichText(block.content?.richText ?? [], {
|
||||
variables,
|
||||
takeLatestIfList: true,
|
||||
takeLatestIfList: typebotVersion !== '6',
|
||||
}),
|
||||
},
|
||||
}
|
||||
@@ -53,22 +59,24 @@ export const parseBubbleBlock = (
|
||||
content: {
|
||||
...message.content,
|
||||
height:
|
||||
typeof message.content.height === 'string'
|
||||
typeof message.content?.height === 'string'
|
||||
? parseFloat(message.content.height)
|
||||
: message.content.height,
|
||||
: message.content?.height,
|
||||
},
|
||||
}
|
||||
}
|
||||
case BubbleBlockType.VIDEO: {
|
||||
const parsedContent = deepParseVariables(variables)(block.content)
|
||||
const parsedContent = block.content
|
||||
? deepParseVariables(variables)(block.content)
|
||||
: undefined
|
||||
return {
|
||||
...block,
|
||||
content: {
|
||||
...(parsedContent.url ? parseVideoUrl(parsedContent.url) : {}),
|
||||
...(parsedContent?.url ? parseVideoUrl(parsedContent.url) : {}),
|
||||
height:
|
||||
typeof parsedContent.height === 'string'
|
||||
typeof parsedContent?.height === 'string'
|
||||
? parseFloat(parsedContent.height)
|
||||
: parsedContent.height,
|
||||
: defaultVideoBubbleContent.height,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ type Props = {
|
||||
hasStarted: boolean
|
||||
isCompleted: boolean
|
||||
}
|
||||
|
||||
export const createResultIfNotExist = async ({
|
||||
resultId,
|
||||
typebot,
|
||||
|
||||
@@ -10,6 +10,7 @@ export const findPublicTypebot = ({ publicId }: Props) =>
|
||||
select: {
|
||||
version: true,
|
||||
groups: true,
|
||||
events: true,
|
||||
edges: true,
|
||||
settings: true,
|
||||
theme: true,
|
||||
|
||||
@@ -12,6 +12,7 @@ export const findTypebot = ({ id, userId }: Props) =>
|
||||
version: true,
|
||||
id: true,
|
||||
groups: true,
|
||||
events: true,
|
||||
edges: true,
|
||||
settings: true,
|
||||
theme: true,
|
||||
|
||||
8
packages/bot-engine/queries/saveVisitedEdges.ts
Normal file
8
packages/bot-engine/queries/saveVisitedEdges.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import prisma from '@typebot.io/lib/prisma'
|
||||
import { VisitedEdge } from '@typebot.io/prisma'
|
||||
|
||||
export const saveVisitedEdges = (visitedEdges: VisitedEdge[]) =>
|
||||
prisma.visitedEdge.createMany({
|
||||
data: visitedEdges,
|
||||
skipDuplicates: true,
|
||||
})
|
||||
@@ -4,18 +4,16 @@ import { InputBlock, SessionState } from '@typebot.io/schemas'
|
||||
|
||||
type Props = {
|
||||
answer: Omit<Prisma.AnswerUncheckedCreateInput, 'resultId'>
|
||||
block: InputBlock
|
||||
reply: string
|
||||
itemId?: string
|
||||
state: SessionState
|
||||
}
|
||||
export const upsertAnswer = async ({ answer, block, state }: Props) => {
|
||||
export const upsertAnswer = async ({ answer, state }: Props) => {
|
||||
const resultId = state.typebotsQueue[0].resultId
|
||||
if (!resultId) return
|
||||
const where = {
|
||||
resultId,
|
||||
blockId: block.id,
|
||||
groupId: block.groupId,
|
||||
blockId: answer.blockId,
|
||||
groupId: answer.groupId,
|
||||
}
|
||||
const existingAnswer = await prisma.answer.findUnique({
|
||||
where: {
|
||||
@@ -28,7 +26,6 @@ export const upsertAnswer = async ({ answer, block, state }: Props) => {
|
||||
where,
|
||||
data: {
|
||||
content: answer.content,
|
||||
itemId: answer.itemId,
|
||||
},
|
||||
})
|
||||
return prisma.answer.createMany({
|
||||
|
||||
@@ -6,12 +6,15 @@ 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 { VisitedEdge } from '@typebot.io/prisma'
|
||||
|
||||
type Props = {
|
||||
session: Pick<ChatSession, 'state'> & { id?: string }
|
||||
input: ChatReply['input']
|
||||
logs: ChatReply['logs']
|
||||
clientSideActions: ChatReply['clientSideActions']
|
||||
visitedEdges: VisitedEdge[]
|
||||
forceCreateSession?: boolean
|
||||
}
|
||||
|
||||
@@ -21,6 +24,7 @@ export const saveStateToDatabase = async ({
|
||||
logs,
|
||||
clientSideActions,
|
||||
forceCreateSession,
|
||||
visitedEdges,
|
||||
}: Props) => {
|
||||
const containsSetVariableClientSideAction = clientSideActions?.some(
|
||||
(action) => action.expectsDedicatedReply
|
||||
@@ -67,5 +71,7 @@ export const saveStateToDatabase = async ({
|
||||
Sentry.captureException(e)
|
||||
}
|
||||
|
||||
if (visitedEdges.length > 0) await saveVisitedEdges(visitedEdges)
|
||||
|
||||
return session
|
||||
}
|
||||
|
||||
@@ -1,36 +1,71 @@
|
||||
import { TRPCError } from '@trpc/server'
|
||||
import { ChatReply, SessionState } from '@typebot.io/schemas'
|
||||
import { ChatReply, SessionState, StartElementId } from '@typebot.io/schemas'
|
||||
import { executeGroup } from './executeGroup'
|
||||
import { getNextGroup } from './getNextGroup'
|
||||
import { VisitedEdge } from '@typebot.io/prisma'
|
||||
|
||||
type Props = {
|
||||
version: 1 | 2
|
||||
state: SessionState
|
||||
startGroupId?: string
|
||||
}
|
||||
} & StartElementId
|
||||
|
||||
export const startBotFlow = async ({
|
||||
version,
|
||||
state,
|
||||
startGroupId,
|
||||
}: Props): Promise<ChatReply & { newSessionState: SessionState }> => {
|
||||
...props
|
||||
}: Props): Promise<
|
||||
ChatReply & { newSessionState: SessionState; visitedEdges: VisitedEdge[] }
|
||||
> => {
|
||||
let newSessionState = state
|
||||
if (startGroupId) {
|
||||
const visitedEdges: VisitedEdge[] = []
|
||||
if ('startGroupId' in props) {
|
||||
const group = state.typebotsQueue[0].typebot.groups.find(
|
||||
(group) => group.id === startGroupId
|
||||
(group) => group.id === props.startGroupId
|
||||
)
|
||||
if (!group)
|
||||
throw new TRPCError({
|
||||
code: 'BAD_REQUEST',
|
||||
message: "startGroupId doesn't exist",
|
||||
message: "Start group doesn't exist",
|
||||
})
|
||||
return executeGroup(group, { version, state: newSessionState })
|
||||
return executeGroup(group, {
|
||||
version,
|
||||
state: newSessionState,
|
||||
visitedEdges,
|
||||
})
|
||||
}
|
||||
const firstEdgeId =
|
||||
newSessionState.typebotsQueue[0].typebot.groups[0].blocks[0].outgoingEdgeId
|
||||
if (!firstEdgeId) return { messages: [], newSessionState }
|
||||
const firstEdgeId = getFirstEdgeId({
|
||||
state: newSessionState,
|
||||
startEventId: 'startEventId' in props ? props.startEventId : undefined,
|
||||
})
|
||||
if (!firstEdgeId) return { messages: [], newSessionState, visitedEdges: [] }
|
||||
const nextGroup = await getNextGroup(newSessionState)(firstEdgeId)
|
||||
newSessionState = nextGroup.newSessionState
|
||||
if (!nextGroup.group) return { messages: [], newSessionState }
|
||||
return executeGroup(nextGroup.group, { version, state: newSessionState })
|
||||
if (nextGroup.visitedEdge) visitedEdges.push(nextGroup.visitedEdge)
|
||||
if (!nextGroup.group) return { messages: [], newSessionState, visitedEdges }
|
||||
return executeGroup(nextGroup.group, {
|
||||
version,
|
||||
state: newSessionState,
|
||||
visitedEdges,
|
||||
})
|
||||
}
|
||||
|
||||
const getFirstEdgeId = ({
|
||||
state,
|
||||
startEventId,
|
||||
}: {
|
||||
state: SessionState
|
||||
startEventId: string | undefined
|
||||
}) => {
|
||||
const { typebot } = state.typebotsQueue[0]
|
||||
if (startEventId) {
|
||||
const event = typebot.events?.find((e) => e.id === startEventId)
|
||||
if (!event)
|
||||
throw new TRPCError({
|
||||
code: 'BAD_REQUEST',
|
||||
message: "Start event doesn't exist",
|
||||
})
|
||||
return event.outgoingEdgeId
|
||||
}
|
||||
if (typebot.version === '6') return typebot.events[0].outgoingEdgeId
|
||||
return typebot.groups[0].blocks[0].outgoingEdgeId
|
||||
}
|
||||
|
||||
@@ -5,10 +5,11 @@ import {
|
||||
Variable,
|
||||
VariableWithValue,
|
||||
Theme,
|
||||
IntegrationBlockType,
|
||||
GoogleAnalyticsBlock,
|
||||
PixelBlock,
|
||||
SessionState,
|
||||
TypebotInSession,
|
||||
Block,
|
||||
} from '@typebot.io/schemas'
|
||||
import {
|
||||
ChatReply,
|
||||
@@ -30,6 +31,10 @@ import { getNextGroup } from './getNextGroup'
|
||||
import { upsertResult } from './queries/upsertResult'
|
||||
import { continueBotFlow } from './continueBotFlow'
|
||||
import { parseVariables } from './variables/parseVariables'
|
||||
import { defaultSettings } from '@typebot.io/schemas/features/typebot/settings/constants'
|
||||
import { IntegrationBlockType } from '@typebot.io/schemas/features/blocks/integrations/constants'
|
||||
import { defaultTheme } from '@typebot.io/schemas/features/typebot/theme/constants'
|
||||
import { VisitedEdge } from '@typebot.io/prisma'
|
||||
|
||||
type Props = {
|
||||
version: 1 | 2
|
||||
@@ -45,7 +50,9 @@ export const startSession = async ({
|
||||
startParams,
|
||||
userId,
|
||||
initialSessionState,
|
||||
}: Props): Promise<ChatReply & { newSessionState: SessionState }> => {
|
||||
}: Props): Promise<
|
||||
ChatReply & { newSessionState: SessionState; visitedEdges: VisitedEdge[] }
|
||||
> => {
|
||||
if (!startParams)
|
||||
throw new TRPCError({
|
||||
code: 'BAD_REQUEST',
|
||||
@@ -64,10 +71,10 @@ export const startSession = async ({
|
||||
typebotId: typebot.id,
|
||||
prefilledVariables,
|
||||
isRememberUserEnabled:
|
||||
typebot.settings.general.rememberUser?.isEnabled ??
|
||||
(isDefined(typebot.settings.general.isNewResultOnRefreshEnabled)
|
||||
? !typebot.settings.general.isNewResultOnRefreshEnabled
|
||||
: false),
|
||||
typebot.settings.general?.rememberUser?.isEnabled ??
|
||||
(isDefined(typebot.settings.general?.isNewResultOnRefreshEnabled)
|
||||
? !typebot.settings.general?.isNewResultOnRefreshEnabled
|
||||
: defaultSettings.general.rememberUser.isEnabled),
|
||||
})
|
||||
|
||||
const startVariables =
|
||||
@@ -75,22 +82,21 @@ export const startSession = async ({
|
||||
? injectVariablesFromExistingResult(prefilledVariables, result.variables)
|
||||
: prefilledVariables
|
||||
|
||||
const typebotInSession = convertStartTypebotToTypebotInSession(
|
||||
typebot,
|
||||
startVariables
|
||||
)
|
||||
|
||||
const initialState: SessionState = {
|
||||
version: '2',
|
||||
version: '3',
|
||||
typebotsQueue: [
|
||||
{
|
||||
resultId: result?.id,
|
||||
typebot: {
|
||||
version: typebot.version,
|
||||
id: typebot.id,
|
||||
groups: typebot.groups,
|
||||
edges: typebot.edges,
|
||||
variables: startVariables,
|
||||
},
|
||||
typebot: typebotInSession,
|
||||
answers: result
|
||||
? result.answers.map((answer) => {
|
||||
const block = typebot.groups
|
||||
.flatMap((group) => group.blocks)
|
||||
.flatMap<Block>((group) => group.blocks)
|
||||
.find((block) => block.id === answer.blockId)
|
||||
if (!block || !isInputBlock(block))
|
||||
return {
|
||||
@@ -98,9 +104,9 @@ export const startSession = async ({
|
||||
value: answer.content,
|
||||
}
|
||||
const key =
|
||||
(block.options.variableId
|
||||
(block.options?.variableId
|
||||
? startVariables.find(
|
||||
(variable) => variable.id === block.options.variableId
|
||||
(variable) => variable.id === block.options?.variableId
|
||||
)?.name
|
||||
: typebot.groups.find((group) =>
|
||||
group.blocks.find(
|
||||
@@ -135,13 +141,18 @@ export const startSession = async ({
|
||||
},
|
||||
dynamicTheme: parseDynamicTheme(initialState),
|
||||
messages: [],
|
||||
visitedEdges: [],
|
||||
}
|
||||
}
|
||||
|
||||
let chatReply = await startBotFlow({
|
||||
version,
|
||||
state: initialState,
|
||||
startGroupId: startParams.startGroupId,
|
||||
...('startGroupId' in startParams
|
||||
? { startGroupId: startParams.startGroupId }
|
||||
: 'startEventId' in startParams
|
||||
? { startEventId: startParams.startEventId }
|
||||
: {}),
|
||||
})
|
||||
|
||||
// If params has message and first block is an input block, we can directly continue the bot flow
|
||||
@@ -165,7 +176,7 @@ export const startSession = async ({
|
||||
version,
|
||||
state: {
|
||||
...newSessionState,
|
||||
currentBlock: { groupId: firstBlock.groupId, blockId: firstBlock.id },
|
||||
currentBlockId: firstBlock.id,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -177,6 +188,7 @@ export const startSession = async ({
|
||||
clientSideActions: startFlowClientActions,
|
||||
newSessionState,
|
||||
logs,
|
||||
visitedEdges,
|
||||
} = chatReply
|
||||
|
||||
const clientSideActions = startFlowClientActions ?? []
|
||||
@@ -188,12 +200,12 @@ export const startSession = async ({
|
||||
if (isDefined(startClientSideAction)) {
|
||||
if (!result) {
|
||||
if ('startPropsToInject' in startClientSideAction) {
|
||||
const { customHeadCode, googleAnalyticsId, pixelId, pixelIds, gtmId } =
|
||||
const { customHeadCode, googleAnalyticsId, pixelIds, gtmId } =
|
||||
startClientSideAction.startPropsToInject
|
||||
let toolsList = ''
|
||||
if (customHeadCode) toolsList += 'Custom head code, '
|
||||
if (googleAnalyticsId) toolsList += 'Google Analytics, '
|
||||
if (pixelId || pixelIds) toolsList += 'Pixel, '
|
||||
if (pixelIds) toolsList += 'Pixel, '
|
||||
if (gtmId) toolsList += 'Google Tag Manager, '
|
||||
toolsList = toolsList.slice(0, -2)
|
||||
startLogs.push({
|
||||
@@ -229,6 +241,7 @@ export const startSession = async ({
|
||||
},
|
||||
dynamicTheme: parseDynamicTheme(newSessionState),
|
||||
logs: startLogs.length > 0 ? startLogs : undefined,
|
||||
visitedEdges,
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -249,6 +262,7 @@ export const startSession = async ({
|
||||
clientSideActions.length > 0 ? clientSideActions : undefined,
|
||||
dynamicTheme: parseDynamicTheme(newSessionState),
|
||||
logs: startLogs.length > 0 ? startLogs : undefined,
|
||||
visitedEdges,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -341,12 +355,13 @@ const getResult = async ({
|
||||
|
||||
const parseDynamicThemeInState = (theme: Theme) => {
|
||||
const hostAvatarUrl =
|
||||
theme.chat.hostAvatar?.isEnabled ?? true
|
||||
? theme.chat.hostAvatar?.url
|
||||
theme.chat?.hostAvatar?.isEnabled ?? defaultTheme.chat.hostAvatar.isEnabled
|
||||
? theme.chat?.hostAvatar?.url
|
||||
: undefined
|
||||
const guestAvatarUrl =
|
||||
theme.chat.guestAvatar?.isEnabled ?? false
|
||||
? theme.chat.guestAvatar?.url
|
||||
theme.chat?.guestAvatar?.isEnabled ??
|
||||
defaultTheme.chat.guestAvatar.isEnabled
|
||||
? theme.chat?.guestAvatar?.url
|
||||
: undefined
|
||||
if (!hostAvatarUrl?.startsWith('{{') && !guestAvatarUrl?.startsWith('{{'))
|
||||
return
|
||||
@@ -361,28 +376,30 @@ const parseDynamicThemeInState = (theme: Theme) => {
|
||||
const parseStartClientSideAction = (
|
||||
typebot: StartTypebot
|
||||
): NonNullable<ChatReply['clientSideActions']>[number] | undefined => {
|
||||
const blocks = typebot.groups.flatMap((group) => group.blocks)
|
||||
const blocks = typebot.groups.flatMap<Block>((group) => group.blocks)
|
||||
const pixelBlocks = (
|
||||
blocks.filter(
|
||||
(block) =>
|
||||
block.type === IntegrationBlockType.PIXEL &&
|
||||
isNotEmpty(block.options.pixelId) &&
|
||||
block.options.isInitSkip !== true
|
||||
isNotEmpty(block.options?.pixelId) &&
|
||||
block.options?.isInitSkip !== true
|
||||
) as PixelBlock[]
|
||||
).map((pixelBlock) => pixelBlock.options.pixelId as string)
|
||||
).map((pixelBlock) => pixelBlock.options?.pixelId as string)
|
||||
|
||||
const startPropsToInject = {
|
||||
customHeadCode: isNotEmpty(typebot.settings.metadata.customHeadCode)
|
||||
? sanitizeAndParseHeadCode(typebot.settings.metadata.customHeadCode)
|
||||
customHeadCode: isNotEmpty(typebot.settings.metadata?.customHeadCode)
|
||||
? sanitizeAndParseHeadCode(
|
||||
typebot.settings.metadata?.customHeadCode as string
|
||||
)
|
||||
: undefined,
|
||||
gtmId: typebot.settings.metadata.googleTagManagerId,
|
||||
gtmId: typebot.settings.metadata?.googleTagManagerId,
|
||||
googleAnalyticsId: (
|
||||
blocks.find(
|
||||
(block) =>
|
||||
block.type === IntegrationBlockType.GOOGLE_ANALYTICS &&
|
||||
block.options.trackingId
|
||||
block.options?.trackingId
|
||||
) as GoogleAnalyticsBlock | undefined
|
||||
)?.options.trackingId,
|
||||
)?.options?.trackingId,
|
||||
pixelIds: pixelBlocks.length > 0 ? pixelBlocks : undefined,
|
||||
}
|
||||
|
||||
@@ -403,8 +420,10 @@ const sanitizeAndParseTheme = (
|
||||
theme: Theme,
|
||||
{ variables }: { variables: Variable[] }
|
||||
): Theme => ({
|
||||
general: deepParseVariables(variables)(theme.general),
|
||||
chat: deepParseVariables(variables)(theme.chat),
|
||||
general: theme.general
|
||||
? deepParseVariables(variables)(theme.general)
|
||||
: undefined,
|
||||
chat: theme.chat ? deepParseVariables(variables)(theme.chat) : undefined,
|
||||
customCss: theme.customCss
|
||||
? removeLiteBadgeCss(parseVariables(variables)(theme.customCss))
|
||||
: undefined,
|
||||
@@ -421,3 +440,25 @@ const removeLiteBadgeCss = (code: string) => {
|
||||
const liteBadgeCssRegex = /.*#lite-badge.*{[\s\S][^{]*}/gm
|
||||
return code.replace(liteBadgeCssRegex, '')
|
||||
}
|
||||
|
||||
const convertStartTypebotToTypebotInSession = (
|
||||
typebot: StartTypebot,
|
||||
startVariables: Variable[]
|
||||
): TypebotInSession =>
|
||||
typebot.version === '6'
|
||||
? {
|
||||
version: typebot.version,
|
||||
id: typebot.id,
|
||||
groups: typebot.groups,
|
||||
edges: typebot.edges,
|
||||
variables: startVariables,
|
||||
events: typebot.events,
|
||||
}
|
||||
: {
|
||||
version: typebot.version,
|
||||
id: typebot.id,
|
||||
groups: typebot.groups,
|
||||
edges: typebot.edges,
|
||||
variables: startVariables,
|
||||
events: typebot.events,
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { safeStringify } from '@typebot.io/lib/safeStringify'
|
||||
import { isDefined } from '@typebot.io/lib/utils'
|
||||
import { isDefined, isNotDefined } from '@typebot.io/lib/utils'
|
||||
import { Variable, VariableWithValue } from '@typebot.io/schemas'
|
||||
import { parseGuessedValueType } from './parseGuessedValueType'
|
||||
|
||||
export type ParseVariablesOptions = {
|
||||
fieldToParse?: 'value' | 'id'
|
||||
@@ -16,6 +17,12 @@ export const defaultParseVariablesOptions: ParseVariablesOptions = {
|
||||
isInsideHtml: false,
|
||||
}
|
||||
|
||||
// {{= inline code =}}
|
||||
const inlineCodeRegex = /\{\{=(.+?)=\}\}/g
|
||||
|
||||
// {{variable}} and ${{{variable}}}
|
||||
const variableRegex = /\{\{([^{}]+)\}\}|(\$)\{\{([^{}]+)\}\}/g
|
||||
|
||||
export const parseVariables =
|
||||
(
|
||||
variables: Variable[],
|
||||
@@ -23,10 +30,14 @@ export const parseVariables =
|
||||
) =>
|
||||
(text: string | undefined): string => {
|
||||
if (!text || text === '') return ''
|
||||
// Capture {{variable}} and ${{{variable}}} (variables in template litterals)
|
||||
const pattern = /\{\{([^{}]+)\}\}|(\$)\{\{([^{}]+)\}\}/g
|
||||
return text.replace(
|
||||
pattern,
|
||||
const textWithInlineCodeParsed = text.replace(
|
||||
inlineCodeRegex,
|
||||
(_full, inlineCodeToEvaluate) =>
|
||||
evaluateInlineCode(inlineCodeToEvaluate, { variables })
|
||||
)
|
||||
|
||||
return textWithInlineCodeParsed.replace(
|
||||
variableRegex,
|
||||
(_full, nameInCurlyBraces, _dollarSign, nameInTemplateLitteral) => {
|
||||
const dollarSign = (_dollarSign ?? '') as string
|
||||
const matchedVarName = nameInCurlyBraces ?? nameInTemplateLitteral
|
||||
@@ -55,6 +66,22 @@ export const parseVariables =
|
||||
)
|
||||
}
|
||||
|
||||
const evaluateInlineCode = (
|
||||
code: string,
|
||||
{ variables }: { variables: Variable[] }
|
||||
) => {
|
||||
const evaluating = parseVariables(variables, { fieldToParse: 'id' })(
|
||||
code.includes('return ') ? code : `return ${code}`
|
||||
)
|
||||
try {
|
||||
const func = Function(...variables.map((v) => v.id), evaluating)
|
||||
return func(...variables.map((v) => parseGuessedValueType(v.value)))
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
return parseVariables(variables)(code)
|
||||
}
|
||||
}
|
||||
|
||||
type VariableToParseInformation = {
|
||||
startIndex: number
|
||||
endIndex: number
|
||||
@@ -69,10 +96,34 @@ export const getVariablesToParseInfoInText = (
|
||||
takeLatestIfList,
|
||||
}: { variables: Variable[]; takeLatestIfList?: boolean }
|
||||
): VariableToParseInformation[] => {
|
||||
const pattern = /\{\{([^{}]+)\}\}|(\$)\{\{([^{}]+)\}\}/g
|
||||
const variablesToParseInfo: VariableToParseInformation[] = []
|
||||
let match
|
||||
while ((match = pattern.exec(text)) !== null) {
|
||||
const inlineCodeMatches = [...text.matchAll(inlineCodeRegex)]
|
||||
inlineCodeMatches.forEach((match) => {
|
||||
if (isNotDefined(match.index) || !match[0].length) return
|
||||
const inlineCodeToEvaluate = match[1]
|
||||
const evaluatedValue = evaluateInlineCode(inlineCodeToEvaluate, {
|
||||
variables,
|
||||
})
|
||||
variablesToParseInfo.push({
|
||||
startIndex: match.index,
|
||||
endIndex: match.index + match[0].length,
|
||||
textToReplace: match[0],
|
||||
value:
|
||||
safeStringify(
|
||||
takeLatestIfList && Array.isArray(evaluatedValue)
|
||||
? evaluatedValue[evaluatedValue.length - 1]
|
||||
: evaluatedValue
|
||||
) ?? '',
|
||||
})
|
||||
})
|
||||
const textWithInlineCodeParsed = text.replace(
|
||||
inlineCodeRegex,
|
||||
(_full, inlineCodeToEvaluate) =>
|
||||
evaluateInlineCode(inlineCodeToEvaluate, { variables })
|
||||
)
|
||||
const variableMatches = [...textWithInlineCodeParsed.matchAll(variableRegex)]
|
||||
variableMatches.forEach((match) => {
|
||||
if (isNotDefined(match.index) || !match[0].length) return
|
||||
const matchedVarName = match[1] ?? match[3]
|
||||
const variable = variables.find((variable) => {
|
||||
return matchedVarName === variable.name && isDefined(variable.value)
|
||||
@@ -88,7 +139,7 @@ export const getVariablesToParseInfoInText = (
|
||||
: variable?.value
|
||||
) ?? '',
|
||||
})
|
||||
}
|
||||
})
|
||||
return variablesToParseInfo
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import {
|
||||
BubbleBlockType,
|
||||
ButtonItem,
|
||||
ChatReply,
|
||||
InputBlockType,
|
||||
} from '@typebot.io/schemas'
|
||||
import { ButtonItem, ChatReply } from '@typebot.io/schemas'
|
||||
import { WhatsAppSendingMessage } from '@typebot.io/schemas/features/whatsapp'
|
||||
import { convertRichTextToWhatsAppText } from './convertRichTextToWhatsAppText'
|
||||
import { isDefined, isEmpty } from '@typebot.io/lib/utils'
|
||||
import { BubbleBlockType } from '@typebot.io/schemas/features/blocks/bubbles/constants'
|
||||
import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants'
|
||||
import { defaultPictureChoiceOptions } from '@typebot.io/schemas/features/blocks/inputs/pictureChoice/constants'
|
||||
import { defaultChoiceInputOptions } from '@typebot.io/schemas/features/blocks/inputs/choice/constants'
|
||||
|
||||
export const convertInputToWhatsAppMessages = (
|
||||
input: NonNullable<ChatReply['input']>,
|
||||
@@ -14,7 +13,7 @@ export const convertInputToWhatsAppMessages = (
|
||||
): WhatsAppSendingMessage[] => {
|
||||
const lastMessageText =
|
||||
lastMessage?.type === BubbleBlockType.TEXT
|
||||
? convertRichTextToWhatsAppText(lastMessage.content.richText)
|
||||
? convertRichTextToWhatsAppText(lastMessage.content.richText ?? [])
|
||||
: undefined
|
||||
switch (input.type) {
|
||||
case InputBlockType.DATE:
|
||||
@@ -28,7 +27,10 @@ export const convertInputToWhatsAppMessages = (
|
||||
case InputBlockType.TEXT:
|
||||
return []
|
||||
case InputBlockType.PICTURE_CHOICE: {
|
||||
if (input.options.isMultipleChoice)
|
||||
if (
|
||||
input.options?.isMultipleChoice ??
|
||||
defaultPictureChoiceOptions.isMultipleChoice
|
||||
)
|
||||
return input.items.flatMap((item, idx) => {
|
||||
let bodyText = ''
|
||||
if (item.title) bodyText += `*${item.title}*`
|
||||
@@ -88,7 +90,10 @@ export const convertInputToWhatsAppMessages = (
|
||||
})
|
||||
}
|
||||
case InputBlockType.CHOICE: {
|
||||
if (input.options.isMultipleChoice)
|
||||
if (
|
||||
input.options?.isMultipleChoice ??
|
||||
defaultChoiceInputOptions.isMultipleChoice
|
||||
)
|
||||
return [
|
||||
{
|
||||
type: 'text',
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import {
|
||||
BubbleBlockType,
|
||||
ChatReply,
|
||||
VideoBubbleContentType,
|
||||
} from '@typebot.io/schemas'
|
||||
import { ChatReply } from '@typebot.io/schemas'
|
||||
import { WhatsAppSendingMessage } from '@typebot.io/schemas/features/whatsapp'
|
||||
import { convertRichTextToWhatsAppText } from './convertRichTextToWhatsAppText'
|
||||
import { isSvgSrc } from '@typebot.io/lib/utils'
|
||||
import { BubbleBlockType } from '@typebot.io/schemas/features/blocks/bubbles/constants'
|
||||
import { VideoBubbleContentType } from '@typebot.io/schemas/features/blocks/bubbles/video/constants'
|
||||
|
||||
const mp4HttpsUrlRegex = /^https:\/\/.*\.mp4$/
|
||||
|
||||
|
||||
@@ -84,8 +84,14 @@ export const resumeWhatsAppFlow = async ({
|
||||
}
|
||||
}
|
||||
|
||||
const { input, logs, newSessionState, messages, clientSideActions } =
|
||||
resumeResponse
|
||||
const {
|
||||
input,
|
||||
logs,
|
||||
newSessionState,
|
||||
messages,
|
||||
clientSideActions,
|
||||
visitedEdges,
|
||||
} = resumeResponse
|
||||
|
||||
await sendChatReplyToWhatsApp({
|
||||
to: receivedMessage.from,
|
||||
@@ -106,9 +112,10 @@ export const resumeWhatsAppFlow = async ({
|
||||
id: sessionId,
|
||||
state: {
|
||||
...newSessionState,
|
||||
currentBlock: !input ? undefined : newSessionState.currentBlock,
|
||||
currentBlockId: !input ? undefined : newSessionState.currentBlockId,
|
||||
},
|
||||
},
|
||||
visitedEdges,
|
||||
})
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
import {
|
||||
ChatReply,
|
||||
InputBlockType,
|
||||
SessionState,
|
||||
Settings,
|
||||
} from '@typebot.io/schemas'
|
||||
import { ChatReply, SessionState, Settings } from '@typebot.io/schemas'
|
||||
import {
|
||||
WhatsAppCredentials,
|
||||
WhatsAppSendingMessage,
|
||||
@@ -16,6 +11,7 @@ import { convertInputToWhatsAppMessages } from './convertInputToWhatsAppMessage'
|
||||
import { isNotDefined } from '@typebot.io/lib/utils'
|
||||
import { computeTypingDuration } from '../computeTypingDuration'
|
||||
import { continueBotFlow } from '../continueBotFlow'
|
||||
import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants'
|
||||
|
||||
// Media can take some time to be delivered. This make sure we don't send a message before the media is delivered.
|
||||
const messageAfterMediaTimeout = 5000
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import prisma from '@typebot.io/lib/prisma'
|
||||
import {
|
||||
ChatReply,
|
||||
ComparisonOperators,
|
||||
LogicalOperator,
|
||||
PublicTypebot,
|
||||
SessionState,
|
||||
Settings,
|
||||
@@ -14,6 +12,11 @@ import {
|
||||
} from '@typebot.io/schemas/features/whatsapp'
|
||||
import { isNotDefined } from '@typebot.io/lib/utils'
|
||||
import { startSession } from '../startSession'
|
||||
import {
|
||||
LogicalOperator,
|
||||
ComparisonOperators,
|
||||
} from '@typebot.io/schemas/features/blocks/logic/condition/constants'
|
||||
import { VisitedEdge } from '@typebot.io/prisma'
|
||||
|
||||
type Props = {
|
||||
incomingMessage?: string
|
||||
@@ -30,6 +33,7 @@ export const startWhatsAppSession = async ({
|
||||
}: Props): Promise<
|
||||
| (ChatReply & {
|
||||
newSessionState: SessionState
|
||||
visitedEdges: VisitedEdge[]
|
||||
})
|
||||
| { error: string }
|
||||
> => {
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
"version": "0.1.0",
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/index.mjs",
|
||||
"types": "dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "pnpm tsc --noEmit && tsup",
|
||||
"dev": "tsup --watch"
|
||||
@@ -41,7 +40,7 @@
|
||||
"@typebot.io/tsconfig": "workspace:*",
|
||||
"tsup": "6.5.0",
|
||||
"typebot-js": "workspace:*",
|
||||
"typescript": "4.9.4",
|
||||
"typescript": "5.2.2",
|
||||
"@typebot.io/lib": "workspace:*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState } from 'react'
|
||||
import { useAnswers } from '../../../providers/AnswersProvider'
|
||||
import { InputBlock, InputBlockType } from '@typebot.io/schemas'
|
||||
import { InputBlock } from '@typebot.io/schemas'
|
||||
import { GuestBubble } from './bubbles/GuestBubble'
|
||||
import { byId } from '@typebot.io/lib'
|
||||
import { InputSubmitContent } from '@/types'
|
||||
@@ -17,6 +17,9 @@ import { ChoiceForm } from '@/features/blocks/inputs/buttons'
|
||||
import { PaymentForm } from '@/features/blocks/inputs/payment'
|
||||
import { RatingForm } from '@/features/blocks/inputs/rating'
|
||||
import { FileUploadForm } from '@/features/blocks/inputs/fileUpload'
|
||||
import { defaultSettings } from '@typebot.io/schemas/features/typebot/settings/constants'
|
||||
import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants'
|
||||
import { getBlockById } from '@typebot.io/lib/getBlockById'
|
||||
|
||||
export const InputChatBlock = ({
|
||||
block,
|
||||
@@ -39,9 +42,11 @@ export const InputChatBlock = ({
|
||||
const [answer, setAnswer] = useState<string>()
|
||||
const [isEditting, setIsEditting] = useState(false)
|
||||
|
||||
const { variableId } = block.options
|
||||
const { variableId } = block.options ?? {}
|
||||
const defaultValue =
|
||||
(typebot.settings.general.isInputPrefillEnabled ?? true) && variableId
|
||||
(typebot.settings.general?.isInputPrefillEnabled ??
|
||||
defaultSettings.general.isInputPrefillEnabled) &&
|
||||
variableId
|
||||
? typebot.variables.find(
|
||||
(variable) =>
|
||||
variable.name === typebot.variables.find(byId(variableId))?.name
|
||||
@@ -51,14 +56,17 @@ export const InputChatBlock = ({
|
||||
const handleSubmit = async ({ label, value, itemId }: InputSubmitContent) => {
|
||||
setAnswer(label ?? value)
|
||||
const isRetry = !isInputValid(value, block.type)
|
||||
if (!isRetry && addAnswer)
|
||||
if (!isRetry && addAnswer) {
|
||||
const { group } = getBlockById(block.id, typebot.groups)
|
||||
await addAnswer(typebot.variables)({
|
||||
blockId: block.id,
|
||||
groupId: block.groupId,
|
||||
groupId: group.id,
|
||||
content: value,
|
||||
variableId,
|
||||
uploadedFiles: block.type === InputBlockType.FILE,
|
||||
})
|
||||
}
|
||||
|
||||
if (!isEditting) onTransitionEnd({ label, value, itemId }, isRetry)
|
||||
setIsEditting(false)
|
||||
}
|
||||
@@ -66,11 +74,11 @@ export const InputChatBlock = ({
|
||||
if (isLoading) return null
|
||||
|
||||
if (answer) {
|
||||
const avatarUrl = typebot.theme.chat.guestAvatar?.url
|
||||
const avatarUrl = typebot.theme.chat?.guestAvatar?.url
|
||||
return (
|
||||
<GuestBubble
|
||||
message={answer}
|
||||
showAvatar={typebot.theme.chat.guestAvatar?.isEnabled ?? false}
|
||||
showAvatar={typebot.theme.chat?.guestAvatar?.isEnabled ?? false}
|
||||
avatarSrc={avatarUrl && parseVariables(typebot.variables)(avatarUrl)}
|
||||
/>
|
||||
)
|
||||
@@ -160,7 +168,7 @@ const Input = ({
|
||||
<PaymentForm
|
||||
options={block.options}
|
||||
onSuccess={() =>
|
||||
onSubmit({ value: block.options.labels.success ?? 'Success' })
|
||||
onSubmit({ value: block.options?.labels?.success ?? 'Success' })
|
||||
}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -3,7 +3,8 @@ import { EmbedBubble } from '@/features/blocks/bubbles/embed'
|
||||
import { ImageBubble } from '@/features/blocks/bubbles/image'
|
||||
import { TextBubble } from '@/features/blocks/bubbles/textBubble'
|
||||
import { VideoBubble } from '@/features/blocks/bubbles/video'
|
||||
import { BubbleBlock, BubbleBlockType } from '@typebot.io/schemas'
|
||||
import { BubbleBlock } from '@typebot.io/schemas'
|
||||
import { BubbleBlockType } from '@typebot.io/schemas/features/blocks/bubbles/constants'
|
||||
|
||||
type Props = {
|
||||
block: BubbleBlock
|
||||
@@ -23,7 +24,7 @@ export const HostBubble = ({ block, onTransitionEnd }: Props) => {
|
||||
case BubbleBlockType.AUDIO:
|
||||
return (
|
||||
<AudioBubble
|
||||
url={block.content.url}
|
||||
url={block.content?.url}
|
||||
onTransitionEnd={onTransitionEnd}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
import {
|
||||
BubbleBlock,
|
||||
InputBlock,
|
||||
LogicBlockType,
|
||||
PublicTypebot,
|
||||
Block,
|
||||
} from '@typebot.io/schemas'
|
||||
@@ -30,6 +29,8 @@ import { executeIntegration } from '@/utils/executeIntegration'
|
||||
import { executeLogic } from '@/utils/executeLogic'
|
||||
import { blockCanBeRetried, parseRetryBlock } from '@/utils/inputs'
|
||||
import { PopupBlockedToast } from '../PopupBlockedToast'
|
||||
import { LogicBlockType } from '@typebot.io/schemas/features/blocks/logic/constants'
|
||||
import { getBlockById } from '@typebot.io/lib/getBlockById'
|
||||
|
||||
type ChatGroupProps = {
|
||||
blocks: Block[]
|
||||
@@ -143,19 +144,20 @@ export const ChatGroup = ({
|
||||
if (blockedPopupUrl) setBlockedPopupUrl(blockedPopupUrl)
|
||||
const isRedirecting =
|
||||
currentBlock.type === LogicBlockType.REDIRECT &&
|
||||
currentBlock.options.isNewTab === false
|
||||
currentBlock.options?.isNewTab === false
|
||||
if (isRedirecting) return
|
||||
nextEdgeId
|
||||
? onGroupEnd({ edgeId: nextEdgeId, updatedTypebot: linkedTypebot })
|
||||
: displayNextBlock()
|
||||
}
|
||||
if (isIntegrationBlock(currentBlock)) {
|
||||
const { group } = getBlockById(currentBlock.id, typebot.groups)
|
||||
const nextEdgeId = await executeIntegration({
|
||||
block: currentBlock,
|
||||
context: {
|
||||
apiHost,
|
||||
typebotId: currentTypebotId,
|
||||
groupId: currentBlock.groupId,
|
||||
groupId: group.id,
|
||||
blockId: currentBlock.id,
|
||||
variables: typebot.variables,
|
||||
isPreview,
|
||||
@@ -181,10 +183,12 @@ export const ChatGroup = ({
|
||||
scroll()
|
||||
const currentBlock = [...processedBlocks].pop()
|
||||
if (currentBlock) {
|
||||
if (isRetry && blockCanBeRetried(currentBlock))
|
||||
if (isRetry && blockCanBeRetried(currentBlock)) {
|
||||
const { group } = getBlockById(currentBlock.id, typebot.groups)
|
||||
return insertBlockInStack(
|
||||
parseRetryBlock(currentBlock, typebot.variables, createEdge)
|
||||
parseRetryBlock(currentBlock, group.id, typebot.variables, createEdge)
|
||||
)
|
||||
}
|
||||
if (
|
||||
isInputBlock(currentBlock) &&
|
||||
currentBlock.options?.variableId &&
|
||||
@@ -196,7 +200,7 @@ export const ChatGroup = ({
|
||||
)
|
||||
}
|
||||
const isSingleChoiceBlock =
|
||||
isChoiceInput(currentBlock) && !currentBlock.options.isMultipleChoice
|
||||
isChoiceInput(currentBlock) && !currentBlock.options?.isMultipleChoice
|
||||
if (isSingleChoiceBlock) {
|
||||
const nextEdgeId = currentBlock.items.find(
|
||||
byId(answerContent?.itemId)
|
||||
@@ -214,7 +218,7 @@ export const ChatGroup = ({
|
||||
nextBlock ? insertBlockInStack(nextBlock) : onGroupEnd({})
|
||||
}
|
||||
|
||||
const avatarSrc = typebot.theme.chat.hostAvatar?.url
|
||||
const avatarSrc = typebot.theme.chat?.hostAvatar?.url
|
||||
|
||||
return (
|
||||
<div className="flex w-full" data-group-name={groupTitle}>
|
||||
@@ -224,10 +228,10 @@ export const ChatGroup = ({
|
||||
key={idx}
|
||||
displayChunk={chunk}
|
||||
hostAvatar={{
|
||||
isEnabled: typebot.theme.chat.hostAvatar?.isEnabled ?? true,
|
||||
isEnabled: typebot.theme.chat?.hostAvatar?.isEnabled ?? true,
|
||||
src: avatarSrc && parseVariables(typebot.variables)(avatarSrc),
|
||||
}}
|
||||
hasGuestAvatar={typebot.theme.chat.guestAvatar?.isEnabled ?? false}
|
||||
hasGuestAvatar={typebot.theme.chat?.guestAvatar?.isEnabled ?? false}
|
||||
onDisplayNextBlock={displayNextBlock}
|
||||
keepShowingHostAvatar={keepShowingHostAvatar}
|
||||
blockedPopupUrl={blockedPopupUrl}
|
||||
|
||||
@@ -57,7 +57,7 @@ export const ConversationContainer = ({
|
||||
if (!nextGroup) return
|
||||
onNewGroupVisible({
|
||||
id: 'edgeId',
|
||||
from: { groupId: 'block', blockId: 'block' },
|
||||
from: { blockId: 'block' },
|
||||
to: { groupId },
|
||||
})
|
||||
return setDisplayedGroups([
|
||||
|
||||
@@ -7,7 +7,6 @@ import { ConversationContainer } from './ConversationContainer'
|
||||
import { AnswersProvider } from '../providers/AnswersProvider'
|
||||
import {
|
||||
AnswerInput,
|
||||
BackgroundType,
|
||||
Edge,
|
||||
PublicTypebot,
|
||||
VariableWithValue,
|
||||
@@ -16,6 +15,7 @@ import { Log } from '@typebot.io/prisma'
|
||||
import { LiteBadge } from './LiteBadge'
|
||||
import { isNotEmpty } from '@typebot.io/lib'
|
||||
import { getViewerUrl } from '@typebot.io/lib/getViewerUrl'
|
||||
import { BackgroundType } from '@typebot.io/schemas/features/typebot/theme/constants'
|
||||
|
||||
export type TypebotViewerProps = {
|
||||
typebot: Omit<PublicTypebot, 'updatedAt' | 'createdAt'>
|
||||
@@ -78,7 +78,7 @@ export const TypebotViewer = ({
|
||||
<style
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `@import url('https://fonts.googleapis.com/css2?family=${
|
||||
typebot.theme.general.font ?? 'Open Sans'
|
||||
typebot.theme.general?.font ?? 'Open Sans'
|
||||
}:ital,wght@0,300;0,400;0,600;1,300;1,400;1,600&display=swap');`,
|
||||
}}
|
||||
/>
|
||||
@@ -112,7 +112,7 @@ export const TypebotViewer = ({
|
||||
startGroupId={startGroupId}
|
||||
/>
|
||||
</div>
|
||||
{typebot.settings.general.isBrandingEnabled && <LiteBadge />}
|
||||
{typebot.settings.general?.isBrandingEnabled && <LiteBadge />}
|
||||
</div>
|
||||
</AnswersProvider>
|
||||
</TypebotProvider>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useTypebot } from '@/providers/TypebotProvider'
|
||||
import { AudioBubbleContent } from '@typebot.io/schemas'
|
||||
import { TypingBubble } from '@/components/TypingBubble'
|
||||
import { parseVariables } from '@/features/variables'
|
||||
import { AudioBubbleBlock } from '@typebot.io/schemas'
|
||||
|
||||
type Props = {
|
||||
url: AudioBubbleContent['url']
|
||||
url: NonNullable<AudioBubbleBlock['content']>['url']
|
||||
onTransitionEnd: () => void
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ export const EmbedBubble = ({ block, onTransitionEnd }: Props) => {
|
||||
}
|
||||
}, [isLoading, isTyping, onTypingEnd])
|
||||
|
||||
const height = block.content.height
|
||||
const height = block.content?.height
|
||||
? typeof block.content.height === 'string'
|
||||
? parseVariables(typebot.variables)(block.content.height) + 'px'
|
||||
: block.content.height
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useTypebot } from '@/providers/TypebotProvider'
|
||||
import { BubbleBlockType, TextBubbleBlock } from '@typebot.io/schemas'
|
||||
import { TextBubbleBlock } from '@typebot.io/schemas'
|
||||
import { computeTypingDuration } from '../utils/computeTypingDuration'
|
||||
import { parseVariables } from '@/features/variables'
|
||||
import { TypingBubble } from '@/components/TypingBubble'
|
||||
import { BubbleBlockType } from '@typebot.io/schemas/features/blocks/bubbles/constants'
|
||||
|
||||
type Props = {
|
||||
block: TextBubbleBlock
|
||||
@@ -12,19 +13,13 @@ type Props = {
|
||||
|
||||
export const showAnimationDuration = 400
|
||||
|
||||
const defaultTypingEmulation = {
|
||||
enabled: true,
|
||||
speed: 300,
|
||||
maxDelay: 1.5,
|
||||
}
|
||||
|
||||
export const TextBubble = ({ block, onTransitionEnd }: Props) => {
|
||||
const { typebot, isLoading } = useTypebot()
|
||||
const messageContainer = useRef<HTMLDivElement | null>(null)
|
||||
const [isTyping, setIsTyping] = useState(true)
|
||||
|
||||
const [content] = useState(
|
||||
parseVariables(typebot.variables)(block.content.html)
|
||||
parseVariables(typebot.variables)(block.content?.html)
|
||||
)
|
||||
|
||||
const onTypingEnd = useCallback(() => {
|
||||
@@ -37,10 +32,10 @@ export const TextBubble = ({ block, onTransitionEnd }: Props) => {
|
||||
useEffect(() => {
|
||||
if (!isTyping || isLoading) return
|
||||
|
||||
const typingTimeout = computeTypingDuration(
|
||||
block.content.plainText ?? '',
|
||||
typebot.settings?.typingEmulation ?? defaultTypingEmulation
|
||||
)
|
||||
const typingTimeout = computeTypingDuration({
|
||||
bubbleContent: block.content?.plainText ?? '',
|
||||
typingSettings: typebot.settings?.typingEmulation,
|
||||
})
|
||||
const timeout = setTimeout(() => {
|
||||
onTypingEnd()
|
||||
}, typingTimeout)
|
||||
@@ -49,7 +44,7 @@ export const TextBubble = ({ block, onTransitionEnd }: Props) => {
|
||||
clearTimeout(timeout)
|
||||
}
|
||||
}, [
|
||||
block.content.plainText,
|
||||
block.content?.plainText,
|
||||
isLoading,
|
||||
isTyping,
|
||||
onTypingEnd,
|
||||
|
||||
@@ -1,16 +1,25 @@
|
||||
import { TypingEmulation } from '@typebot.io/schemas'
|
||||
import { Settings } from '@typebot.io/schemas'
|
||||
import { defaultSettings } from '@typebot.io/schemas/features/typebot/settings/constants'
|
||||
|
||||
export const computeTypingDuration = (
|
||||
bubbleContent: string,
|
||||
typingSettings: TypingEmulation
|
||||
) => {
|
||||
type Props = {
|
||||
bubbleContent: string
|
||||
typingSettings?: Settings['typingEmulation']
|
||||
}
|
||||
|
||||
export const computeTypingDuration = ({
|
||||
bubbleContent,
|
||||
typingSettings,
|
||||
}: Props) => {
|
||||
let wordCount = bubbleContent.match(/(\w+)/g)?.length ?? 0
|
||||
if (wordCount === 0) wordCount = bubbleContent.length
|
||||
const typedWordsPerMinute = typingSettings.speed
|
||||
let typingTimeout = typingSettings.enabled
|
||||
? (wordCount / typedWordsPerMinute) * 60000
|
||||
: 0
|
||||
if (typingTimeout > typingSettings.maxDelay * 1000)
|
||||
typingTimeout = typingSettings.maxDelay * 1000
|
||||
const { enabled, speed, maxDelay } = {
|
||||
enabled: typingSettings?.enabled ?? defaultSettings.typingEmulation.enabled,
|
||||
speed: typingSettings?.speed ?? defaultSettings.typingEmulation.speed,
|
||||
maxDelay:
|
||||
typingSettings?.maxDelay ?? defaultSettings.typingEmulation.maxDelay,
|
||||
}
|
||||
const typedWordsPerMinute = speed
|
||||
let typingTimeout = enabled ? (wordCount / typedWordsPerMinute) * 60000 : 0
|
||||
if (typingTimeout > maxDelay * 1000) typingTimeout = maxDelay * 1000
|
||||
return typingTimeout
|
||||
}
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useTypebot } from '@/providers/TypebotProvider'
|
||||
import {
|
||||
Variable,
|
||||
VideoBubbleContent,
|
||||
VideoBubbleContentType,
|
||||
VideoBubbleBlock,
|
||||
} from '@typebot.io/schemas'
|
||||
import { Variable, VideoBubbleBlock } from '@typebot.io/schemas'
|
||||
import { TypingBubble } from '@/components/TypingBubble'
|
||||
import { parseVariables } from '@/features/variables'
|
||||
import { VideoBubbleContentType } from '@typebot.io/schemas/features/blocks/bubbles/video/constants'
|
||||
|
||||
type Props = {
|
||||
block: VideoBubbleBlock
|
||||
@@ -71,7 +67,7 @@ const VideoContent = ({
|
||||
isTyping,
|
||||
variables,
|
||||
}: {
|
||||
content?: VideoBubbleContent
|
||||
content?: VideoBubbleBlock['content']
|
||||
isTyping: boolean
|
||||
variables: Variable[]
|
||||
}) => {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { SendButton } from '@/components/SendButton'
|
||||
import { InputSubmitContent } from '@/types'
|
||||
import { DateInputOptions } from '@typebot.io/schemas'
|
||||
import { useState } from 'react'
|
||||
import { parseReadableDate } from '../utils/parseReadableDate'
|
||||
import { DateInputBlock } from '@typebot.io/schemas'
|
||||
|
||||
type DateInputProps = {
|
||||
onSubmit: (inputValue: InputSubmitContent) => void
|
||||
options?: DateInputOptions
|
||||
options: DateInputBlock['options']
|
||||
}
|
||||
|
||||
export const DateForm = ({
|
||||
|
||||
@@ -2,9 +2,10 @@ import { Spinner, SendButton } from '@/components/SendButton'
|
||||
import { useAnswers } from '@/providers/AnswersProvider'
|
||||
import { useTypebot } from '@/providers/TypebotProvider'
|
||||
import { InputSubmitContent } from '@/types'
|
||||
import { defaultFileInputOptions, FileInputBlock } from '@typebot.io/schemas'
|
||||
import { FileInputBlock } from '@typebot.io/schemas'
|
||||
import React, { ChangeEvent, FormEvent, useState, DragEvent } from 'react'
|
||||
import { uploadFiles } from '../helpers/uploadFiles'
|
||||
import { defaultFileInputOptions } from '@typebot.io/schemas/features/blocks/inputs/file/constants'
|
||||
|
||||
type Props = {
|
||||
block: FileInputBlock
|
||||
@@ -13,13 +14,13 @@ type Props = {
|
||||
}
|
||||
|
||||
export const FileUploadForm = ({
|
||||
block: {
|
||||
id,
|
||||
options: { isMultipleAllowed, labels, sizeLimit, isRequired },
|
||||
},
|
||||
block: { id, options },
|
||||
onSubmit,
|
||||
onSkip,
|
||||
}: Props) => {
|
||||
const { isMultipleAllowed, labels, isRequired } = options ?? {}
|
||||
const sizeLimit =
|
||||
options && 'sizeLimit' in options ? options?.sizeLimit : undefined
|
||||
const { isPreview, currentTypebotId } = useTypebot()
|
||||
const { resultId } = useAnswers()
|
||||
const [selectedFiles, setSelectedFiles] = useState<File[]>([])
|
||||
@@ -160,7 +161,7 @@ export const FileUploadForm = ({
|
||||
)}
|
||||
<p
|
||||
className="text-sm text-gray-500 text-center"
|
||||
dangerouslySetInnerHTML={{ __html: labels.placeholder }}
|
||||
dangerouslySetInnerHTML={{ __html: labels?.placeholder ?? '' }}
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
@@ -181,7 +182,7 @@ export const FileUploadForm = ({
|
||||
}
|
||||
onClick={onSkip}
|
||||
>
|
||||
{labels.skip ?? defaultFileInputOptions.labels.skip}
|
||||
{labels?.skip ?? defaultFileInputOptions.labels.skip}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
@@ -195,17 +196,17 @@ export const FileUploadForm = ({
|
||||
}
|
||||
onClick={clearFiles}
|
||||
>
|
||||
{labels.clear ?? defaultFileInputOptions.labels.clear}
|
||||
{labels?.clear ?? defaultFileInputOptions.labels.clear}
|
||||
</button>
|
||||
)}
|
||||
<SendButton
|
||||
type="submit"
|
||||
label={
|
||||
labels.button === defaultFileInputOptions.labels.button
|
||||
labels?.button === defaultFileInputOptions.labels.button
|
||||
? `${labels.button} ${selectedFiles.length} file${
|
||||
selectedFiles.length > 1 ? 's' : ''
|
||||
}`
|
||||
: labels.button
|
||||
: labels?.button ?? ''
|
||||
}
|
||||
disableIcon
|
||||
/>
|
||||
|
||||
@@ -23,7 +23,6 @@ export const uploadFiles = async ({
|
||||
i += 1
|
||||
const { data } = await sendRequest<{
|
||||
presignedUrl: string
|
||||
formData: Record<string, string>
|
||||
hasReachedStorageLimit: boolean
|
||||
}>(
|
||||
`${basePath}/storage/upload-url?filePath=${encodeURIComponent(
|
||||
@@ -36,14 +35,9 @@ export const uploadFiles = async ({
|
||||
const url = data.presignedUrl
|
||||
if (data.hasReachedStorageLimit) urls.push(null)
|
||||
else {
|
||||
const formData = new FormData()
|
||||
Object.entries(data.formData).forEach(([key, value]) => {
|
||||
formData.append(key, value)
|
||||
})
|
||||
formData.append('file', file)
|
||||
const upload = await fetch(data.presignedUrl, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
const upload = await fetch(url, {
|
||||
method: 'PUT',
|
||||
body: file,
|
||||
})
|
||||
|
||||
if (!upload.ok) continue
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { PaymentInputOptions, PaymentProvider } from '@typebot.io/schemas'
|
||||
import React from 'react'
|
||||
import { PaymentInputBlock } from '@typebot.io/schemas'
|
||||
import { StripePaymentForm } from './StripePaymentForm'
|
||||
import { PaymentProvider } from '@typebot.io/schemas/features/blocks/inputs/payment/constants'
|
||||
|
||||
type Props = {
|
||||
onSuccess: () => void
|
||||
options: PaymentInputOptions
|
||||
options: PaymentInputBlock['options']
|
||||
}
|
||||
|
||||
export const PaymentForm = ({ onSuccess, options }: Props): JSX.Element => {
|
||||
switch (options.provider) {
|
||||
switch (options?.provider) {
|
||||
case undefined:
|
||||
case PaymentProvider.STRIPE:
|
||||
return <StripePaymentForm onSuccess={onSuccess} options={options} />
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { FormEvent, useEffect, useState } from 'react'
|
||||
import { useStripe, useElements, PaymentElement } from '@stripe/react-stripe-js'
|
||||
import { Elements } from '@stripe/react-stripe-js'
|
||||
import { PaymentInputOptions, Variable } from '@typebot.io/schemas'
|
||||
import { PaymentInputBlock, Variable } from '@typebot.io/schemas'
|
||||
import { SendButton, Spinner } from '@/components/SendButton'
|
||||
import { initStripe } from '@/lib/stripe'
|
||||
import { parseVariables } from '@/features/variables'
|
||||
@@ -11,7 +11,7 @@ import { createPaymentIntentQuery } from '../../queries/createPaymentIntentQuery
|
||||
import { Stripe } from '@stripe/stripe-js'
|
||||
|
||||
type Props = {
|
||||
options: PaymentInputOptions
|
||||
options: PaymentInputBlock['options']
|
||||
onSuccess: () => void
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ const CheckoutForm = ({
|
||||
onSuccess: () => void
|
||||
clientSecret: string
|
||||
amountLabel: string
|
||||
options: PaymentInputOptions
|
||||
options: PaymentInputBlock['options']
|
||||
variables: Variable[]
|
||||
viewerHost: string
|
||||
}) => {
|
||||
@@ -131,13 +131,13 @@ const CheckoutForm = ({
|
||||
return_url: viewerHost,
|
||||
payment_method_data: {
|
||||
billing_details: {
|
||||
name: options.additionalInformation?.name
|
||||
? parseVariables(variables)(options.additionalInformation?.name)
|
||||
name: options?.additionalInformation?.name
|
||||
? parseVariables(variables)(options.additionalInformation.name)
|
||||
: undefined,
|
||||
email: options.additionalInformation?.email
|
||||
email: options?.additionalInformation?.email
|
||||
? parseVariables(variables)(options.additionalInformation?.email)
|
||||
: undefined,
|
||||
phone: options.additionalInformation?.phoneNumber
|
||||
phone: options?.additionalInformation?.phoneNumber
|
||||
? parseVariables(variables)(
|
||||
options.additionalInformation?.phoneNumber
|
||||
)
|
||||
@@ -172,7 +172,7 @@ const CheckoutForm = ({
|
||||
/>
|
||||
{isPayButtonVisible && (
|
||||
<SendButton
|
||||
label={`${options.labels.button} ${amountLabel}`}
|
||||
label={`${options?.labels?.button} ${amountLabel}`}
|
||||
isDisabled={isLoading || !stripe || !elements}
|
||||
isLoading={isLoading}
|
||||
className="mt-4 w-full max-w-lg"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { PaymentInputOptions, Variable } from '@typebot.io/schemas'
|
||||
import { PaymentInputBlock, Variable } from '@typebot.io/schemas'
|
||||
import { sendRequest } from '@typebot.io/lib'
|
||||
|
||||
export const createPaymentIntentQuery = ({
|
||||
@@ -7,7 +7,7 @@ export const createPaymentIntentQuery = ({
|
||||
inputOptions,
|
||||
variables,
|
||||
}: {
|
||||
inputOptions: PaymentInputOptions
|
||||
inputOptions: PaymentInputBlock['options']
|
||||
apiHost: string
|
||||
variables: Variable[]
|
||||
isPreview: boolean
|
||||
|
||||
@@ -52,8 +52,10 @@ export const PhoneInput = ({
|
||||
ref={inputRef}
|
||||
value={inputValue}
|
||||
onChange={handleChange}
|
||||
placeholder={block.options.labels.placeholder ?? 'Your phone number...'}
|
||||
defaultCountry={block.options.defaultCountryCode as Country}
|
||||
placeholder={
|
||||
block.options?.labels?.placeholder ?? 'Your phone number...'
|
||||
}
|
||||
defaultCountry={block.options?.defaultCountryCode as Country}
|
||||
autoFocus={!isMobile}
|
||||
/>
|
||||
<SendButton
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { InputSubmitContent } from '@/types'
|
||||
import { RatingInputOptions, RatingInputBlock } from '@typebot.io/schemas'
|
||||
import { RatingInputBlock } from '@typebot.io/schemas'
|
||||
import React, { FormEvent, useState } from 'react'
|
||||
import { isDefined, isEmpty, isNotDefined } from '@typebot.io/lib'
|
||||
import { SendButton } from '../../../../../components/SendButton'
|
||||
import { defaultRatingInputOptions } from '@typebot.io/schemas/features/blocks/inputs/rating/constants'
|
||||
|
||||
type Props = {
|
||||
block: RatingInputBlock
|
||||
@@ -19,14 +20,14 @@ export const RatingForm = ({ block, onSubmit }: Props) => {
|
||||
}
|
||||
|
||||
const handleClick = (rating: number) => {
|
||||
if (block.options.isOneClickSubmitEnabled)
|
||||
if (block.options?.isOneClickSubmitEnabled)
|
||||
onSubmit({ value: rating.toString() })
|
||||
setRating(rating)
|
||||
}
|
||||
|
||||
return (
|
||||
<form className="flex flex-col" onSubmit={handleSubmit}>
|
||||
{block.options.labels.left && (
|
||||
{block.options?.labels?.left && (
|
||||
<span className="text-sm w-full mb-2 rating-label">
|
||||
{block.options.labels.left}
|
||||
</span>
|
||||
@@ -34,20 +35,20 @@ export const RatingForm = ({ block, onSubmit }: Props) => {
|
||||
<div className="flex flex-wrap justify-center">
|
||||
{Array.from(
|
||||
Array(
|
||||
block.options.length +
|
||||
(block.options.buttonType === 'Numbers' ? 1 : 0)
|
||||
(block.options?.length ?? defaultRatingInputOptions.length) +
|
||||
(block.options?.buttonType === 'Numbers' ? 1 : 0)
|
||||
)
|
||||
).map((_, idx) => (
|
||||
<RatingButton
|
||||
{...block.options}
|
||||
key={idx}
|
||||
rating={rating}
|
||||
idx={idx + (block.options.buttonType === 'Numbers' ? 0 : 1)}
|
||||
idx={idx + (block.options?.buttonType === 'Numbers' ? 0 : 1)}
|
||||
onClick={handleClick}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{block.options.labels.right && (
|
||||
{block.options?.labels?.right && (
|
||||
<span className="text-sm w-full text-right mb-2 pr-2 rating-label">
|
||||
{block.options.labels.right}
|
||||
</span>
|
||||
@@ -56,7 +57,7 @@ export const RatingForm = ({ block, onSubmit }: Props) => {
|
||||
<div className="flex justify-end mr-2">
|
||||
{isDefined(rating) && (
|
||||
<SendButton
|
||||
label={block.options?.labels.button ?? 'Send'}
|
||||
label={block.options?.labels?.button ?? 'Send'}
|
||||
disableIcon
|
||||
/>
|
||||
)}
|
||||
@@ -71,7 +72,10 @@ const RatingButton = ({
|
||||
buttonType,
|
||||
customIcon,
|
||||
onClick,
|
||||
}: Pick<RatingInputOptions, 'buttonType' | 'customIcon'> & {
|
||||
}: Pick<
|
||||
NonNullable<RatingInputBlock['options']>,
|
||||
'buttonType' | 'customIcon'
|
||||
> & {
|
||||
rating: number | undefined
|
||||
idx: number
|
||||
onClick: (idx: number) => void
|
||||
@@ -100,7 +104,7 @@ const RatingButton = ({
|
||||
onClick={() => onClick(idx)}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html:
|
||||
customIcon.isEnabled && !isEmpty(customIcon.svg)
|
||||
customIcon?.isEnabled && !isEmpty(customIcon.svg)
|
||||
? customIcon.svg
|
||||
: defaultIcon,
|
||||
}}
|
||||
|
||||
@@ -2,9 +2,11 @@ import { parseVariables } from '@/features/variables'
|
||||
import { IntegrationState } from '@/types'
|
||||
import { sendEventToParent } from '@/utils/chat'
|
||||
import { isEmbedded } from '@/utils/helpers'
|
||||
import { ChatwootBlock, ChatwootOptions } from '@typebot.io/schemas'
|
||||
import { ChatwootBlock } from '@typebot.io/schemas'
|
||||
|
||||
const parseSetUserCode = (user: ChatwootOptions['user']) => `
|
||||
const parseSetUserCode = (
|
||||
user: NonNullable<ChatwootBlock['options']>['user']
|
||||
) => `
|
||||
window.$chatwoot.setUser("${user?.id ?? ''}", {
|
||||
email: ${user?.email ? `"${user.email}"` : 'undefined'},
|
||||
name: ${user?.name ? `"${user.name}"` : 'undefined'},
|
||||
@@ -17,7 +19,7 @@ const parseChatwootOpenCode = ({
|
||||
baseUrl,
|
||||
websiteToken,
|
||||
user,
|
||||
}: ChatwootOptions) => `
|
||||
}: ChatwootBlock['options'] = {}) => `
|
||||
if (window.$chatwoot) {
|
||||
if(${Boolean(user)}) {
|
||||
${parseSetUserCode(user)}
|
||||
|
||||
@@ -3,7 +3,6 @@ import { IntegrationState } from '@/types'
|
||||
import { parseLog } from '@/utils/helpers'
|
||||
import {
|
||||
GoogleSheetsBlock,
|
||||
GoogleSheetsAction,
|
||||
GoogleSheetsInsertRowOptions,
|
||||
GoogleSheetsUpdateRowOptions,
|
||||
GoogleSheetsGetOptions,
|
||||
@@ -12,12 +11,14 @@ import {
|
||||
Variable,
|
||||
} from '@typebot.io/schemas'
|
||||
import { sendRequest, byId } from '@typebot.io/lib'
|
||||
import { GoogleSheetsAction } from '@typebot.io/schemas/features/blocks/integrations/googleSheets/constants'
|
||||
|
||||
export const executeGoogleSheetBlock = async (
|
||||
block: GoogleSheetsBlock,
|
||||
context: IntegrationState
|
||||
) => {
|
||||
if (!('action' in block.options)) return block.outgoingEdgeId
|
||||
if (!block.options || !('action' in block.options))
|
||||
return block.outgoingEdgeId
|
||||
switch (block.options.action) {
|
||||
case GoogleSheetsAction.INSERT_ROW:
|
||||
insertRowInGoogleSheets(block.options, context)
|
||||
@@ -68,7 +69,7 @@ const updateRowInGoogleSheets = (
|
||||
options: GoogleSheetsUpdateRowOptions,
|
||||
{ variables, apiHost, onNewLog, resultId }: IntegrationState
|
||||
) => {
|
||||
if (!options.cellsToUpsert || !options.referenceCell) return
|
||||
if (!options.cellsToUpsert || !('referenceCell' in options)) return
|
||||
sendRequest({
|
||||
url: `${apiHost}/api/integrations/google-sheets/spreadsheets/${options.spreadsheetId}/sheets/${options.sheetId}`,
|
||||
method: 'POST',
|
||||
@@ -78,8 +79,8 @@ const updateRowInGoogleSheets = (
|
||||
values: parseCellValues(options.cellsToUpsert, variables),
|
||||
resultId,
|
||||
referenceCell: {
|
||||
column: options.referenceCell.column,
|
||||
value: parseVariables(variables)(options.referenceCell.value ?? ''),
|
||||
column: options.referenceCell?.column,
|
||||
value: parseVariables(variables)(options.referenceCell?.value ?? ''),
|
||||
},
|
||||
},
|
||||
}).then(({ error }) => {
|
||||
@@ -113,15 +114,18 @@ const getRowFromGoogleSheets = async (
|
||||
body: {
|
||||
action: GoogleSheetsAction.GET,
|
||||
credentialsId: options.credentialsId,
|
||||
referenceCell: options.referenceCell
|
||||
? {
|
||||
column: options.referenceCell.column,
|
||||
value: parseVariables(variables)(options.referenceCell.value ?? ''),
|
||||
}
|
||||
: undefined,
|
||||
referenceCell:
|
||||
'referenceCell' in options
|
||||
? {
|
||||
column: options.referenceCell?.column,
|
||||
value: parseVariables(variables)(
|
||||
options.referenceCell?.value ?? ''
|
||||
),
|
||||
}
|
||||
: undefined,
|
||||
filter: options.filter
|
||||
? {
|
||||
comparisons: options.filter.comparisons.map((comparison) => ({
|
||||
comparisons: options.filter.comparisons?.map((comparison) => ({
|
||||
...comparison,
|
||||
value: parseVariables(variables)(comparison.value),
|
||||
})),
|
||||
|
||||
@@ -29,18 +29,18 @@ export const executeSendEmailBlock = (
|
||||
url: `${apiHost}/api/typebots/${typebotId}/integrations/email?resultId=${resultId}`,
|
||||
method: 'POST',
|
||||
body: {
|
||||
credentialsId: options.credentialsId,
|
||||
recipients: options.recipients.map(parseVariables(variables)),
|
||||
subject: parseVariables(variables)(options.subject ?? ''),
|
||||
body: parseVariables(variables)(options.body ?? ''),
|
||||
cc: (options.cc ?? []).map(parseVariables(variables)),
|
||||
bcc: (options.bcc ?? []).map(parseVariables(variables)),
|
||||
replyTo: options.replyTo
|
||||
credentialsId: options?.credentialsId,
|
||||
recipients: options?.recipients?.map(parseVariables(variables)),
|
||||
subject: parseVariables(variables)(options?.subject ?? ''),
|
||||
body: parseVariables(variables)(options?.body ?? ''),
|
||||
cc: (options?.cc ?? []).map(parseVariables(variables)),
|
||||
bcc: (options?.bcc ?? []).map(parseVariables(variables)),
|
||||
replyTo: options?.replyTo
|
||||
? parseVariables(variables)(options.replyTo)
|
||||
: undefined,
|
||||
fileUrls: variables.find(byId(options.attachmentsVariableId))?.value,
|
||||
isCustomBody: options.isCustomBody,
|
||||
isBodyCode: options.isBodyCode,
|
||||
fileUrls: variables.find(byId(options?.attachmentsVariableId))?.value,
|
||||
isCustomBody: options?.isCustomBody,
|
||||
isBodyCode: options?.isBodyCode,
|
||||
resultValues,
|
||||
},
|
||||
}).then(({ error }) => {
|
||||
|
||||
@@ -48,7 +48,7 @@ export const executeWebhook = async (
|
||||
: 'Webhook successfuly executed',
|
||||
details: JSON.stringify(error ?? data, null, 2).substring(0, 1000),
|
||||
})
|
||||
const newVariables = block.options.responseVariableMapping.reduce<
|
||||
const newVariables = block.options?.responseVariableMapping?.reduce<
|
||||
VariableWithUnknowValue[]
|
||||
>((newVariables, varMapping) => {
|
||||
if (!varMapping?.bodyPath || !varMapping.variableId) return newVariables
|
||||
@@ -66,6 +66,6 @@ export const executeWebhook = async (
|
||||
return newVariables
|
||||
}
|
||||
}, [])
|
||||
updateVariables(newVariables)
|
||||
if (newVariables) updateVariables(newVariables)
|
||||
return block.outgoingEdgeId
|
||||
}
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import { parseVariables } from '@/features/variables'
|
||||
import { EdgeId, LogicState } from '@/types'
|
||||
import {
|
||||
Comparison,
|
||||
ComparisonOperators,
|
||||
ConditionBlock,
|
||||
LogicalOperator,
|
||||
Variable,
|
||||
} from '@typebot.io/schemas'
|
||||
import { Comparison, ConditionBlock, Variable } from '@typebot.io/schemas'
|
||||
import { isNotDefined, isDefined } from '@typebot.io/lib'
|
||||
import {
|
||||
LogicalOperator,
|
||||
ComparisonOperators,
|
||||
} from '@typebot.io/schemas/features/blocks/logic/condition/constants'
|
||||
|
||||
export const executeCondition = (
|
||||
block: ConditionBlock,
|
||||
@@ -16,9 +14,9 @@ export const executeCondition = (
|
||||
const passedCondition = block.items.find((item) => {
|
||||
const { content } = item
|
||||
const isConditionPassed =
|
||||
content.logicalOperator === LogicalOperator.AND
|
||||
? content.comparisons.every(executeComparison(variables))
|
||||
: content.comparisons.some(executeComparison(variables))
|
||||
content?.logicalOperator === LogicalOperator.AND
|
||||
? content.comparisons?.every(executeComparison(variables))
|
||||
: content?.comparisons?.some(executeComparison(variables))
|
||||
return isConditionPassed
|
||||
})
|
||||
return passedCondition ? passedCondition.outgoingEdgeId : block.outgoingEdgeId
|
||||
|
||||
@@ -8,7 +8,7 @@ export const executeScript = async (
|
||||
block: ScriptBlock,
|
||||
{ typebot: { variables } }: LogicState
|
||||
) => {
|
||||
if (!block.options.content) return
|
||||
if (!block.options?.content) return
|
||||
if (block.options.shouldExecuteInParentContext && isEmbedded) {
|
||||
sendEventToParent({
|
||||
codeToExecute: parseVariables(variables)(block.options.content),
|
||||
|
||||
@@ -8,6 +8,8 @@ export const executeSetVariable = (
|
||||
{ typebot: { variables }, updateVariableValue, updateVariables }: LogicState
|
||||
): EdgeId | undefined => {
|
||||
if (!block.options?.variableId) return block.outgoingEdgeId
|
||||
if (block.options.type !== undefined && block.options.type !== 'Custom')
|
||||
return block.outgoingEdgeId
|
||||
const evaluatedExpression = block.options.expressionToEvaluate
|
||||
? evaluateSetVariableExpression(variables)(
|
||||
block.options.expressionToEvaluate
|
||||
|
||||
@@ -9,10 +9,10 @@ export const fetchAndInjectTypebot = async (
|
||||
): Promise<LinkedTypebot | undefined> => {
|
||||
const { data, error } = isPreview
|
||||
? await sendRequest<{ typebot: Typebot }>(
|
||||
`/api/typebots/${block.options.typebotId}`
|
||||
`/api/typebots/${block.options?.typebotId}`
|
||||
)
|
||||
: await sendRequest<{ typebot: PublicTypebot }>(
|
||||
`${apiHost}/api/publicTypebots/${block.options.typebotId}`
|
||||
`${apiHost}/api/publicTypebots/${block.options?.typebotId}`
|
||||
)
|
||||
if (!data || error) return
|
||||
return injectLinkedTypebot(data.typebot)
|
||||
|
||||
@@ -21,12 +21,12 @@ export const executeTypebotLink = async (
|
||||
currentTypebotId,
|
||||
} = context
|
||||
const linkedTypebot = (
|
||||
block.options.typebotId === 'current'
|
||||
block.options?.typebotId === 'current'
|
||||
? typebot
|
||||
: [typebot, ...linkedTypebots].find((typebot) =>
|
||||
'typebotId' in typebot
|
||||
? typebot.typebotId === block.options.typebotId
|
||||
: typebot.id === block.options.typebotId
|
||||
? typebot.typebotId === block.options?.typebotId
|
||||
: typebot.id === block.options?.typebotId
|
||||
) ?? (await fetchAndInjectTypebot(block, context))
|
||||
) as PublicTypebot | LinkedTypebot | undefined
|
||||
if (!linkedTypebot) {
|
||||
@@ -47,13 +47,13 @@ export const executeTypebotLink = async (
|
||||
'typebotId' in linkedTypebot ? linkedTypebot.typebotId : linkedTypebot.id
|
||||
)
|
||||
const nextGroupId =
|
||||
block.options.groupId ??
|
||||
block.options?.groupId ??
|
||||
linkedTypebot.groups.find((b) => b.blocks.some((s) => s.type === 'start'))
|
||||
?.id
|
||||
if (!nextGroupId) return { nextEdgeId: block.outgoingEdgeId }
|
||||
const newEdge: Edge = {
|
||||
id: (Math.random() * 1000).toString(),
|
||||
from: { blockId: '', groupId: '' },
|
||||
from: { blockId: '' },
|
||||
to: {
|
||||
groupId: nextGroupId,
|
||||
},
|
||||
|
||||
@@ -6,7 +6,7 @@ export const executeWait = async (
|
||||
block: WaitBlock,
|
||||
{ typebot: { variables } }: LogicState
|
||||
) => {
|
||||
if (!block.options.secondsToWaitFor) return block.outgoingEdgeId
|
||||
if (!block.options?.secondsToWaitFor) return block.outgoingEdgeId
|
||||
const parsedSecondsToWaitFor = parseVariables(variables)(
|
||||
block.options.secondsToWaitFor
|
||||
)
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import {
|
||||
Background,
|
||||
BackgroundType,
|
||||
ChatTheme,
|
||||
ContainerColors,
|
||||
GeneralTheme,
|
||||
InputColors,
|
||||
Theme,
|
||||
} from '@typebot.io/schemas'
|
||||
import { BackgroundType } from '@typebot.io/schemas/features/typebot/theme/constants'
|
||||
|
||||
const cssVariableNames = {
|
||||
general: {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { GoogleAnalyticsOptions } from '@typebot.io/schemas'
|
||||
import { GoogleAnalyticsBlock } from '@typebot.io/schemas'
|
||||
|
||||
declare const gtag: (
|
||||
type: string,
|
||||
@@ -33,7 +33,7 @@ const initGoogleAnalytics = (id: string): Promise<void> =>
|
||||
if (existingScript) resolve()
|
||||
})
|
||||
|
||||
export const sendGaEvent = (options: GoogleAnalyticsOptions) => {
|
||||
export const sendGaEvent = (options: GoogleAnalyticsBlock['options']) => {
|
||||
if (!options) return
|
||||
gtag('event', options.action, {
|
||||
event_category: options.category,
|
||||
|
||||
@@ -133,7 +133,7 @@ export const TypebotProvider = ({
|
||||
groups: [...localTypebot.groups, ...typebotToInject.groups],
|
||||
variables: [...localTypebot.variables, ...typebotToInject.variables],
|
||||
edges: [...localTypebot.edges, ...typebotToInject.edges],
|
||||
}
|
||||
} as TypebotViewerProps['typebot']
|
||||
setLocalTypebot(updatedTypebot)
|
||||
return typebotToInject
|
||||
}
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
import {
|
||||
BubbleBlock,
|
||||
BubbleBlockType,
|
||||
InputBlock,
|
||||
InputBlockType,
|
||||
Block,
|
||||
} from '@typebot.io/schemas'
|
||||
import { BubbleBlock, InputBlock, Block } from '@typebot.io/schemas'
|
||||
import { isBubbleBlock, isInputBlock } from '@typebot.io/lib'
|
||||
import type { TypebotPostMessageData } from 'typebot-js'
|
||||
import { BubbleBlockType } from '@typebot.io/schemas/features/blocks/bubbles/constants'
|
||||
import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants'
|
||||
|
||||
export const getLastChatBlockType = (
|
||||
blocks: Block[]
|
||||
|
||||
@@ -4,7 +4,8 @@ import { executeGoogleSheetBlock } from '@/features/blocks/integrations/googleSh
|
||||
import { executeSendEmailBlock } from '@/features/blocks/integrations/sendEmail'
|
||||
import { executeWebhook } from '@/features/blocks/integrations/webhook'
|
||||
import { IntegrationState } from '@/types'
|
||||
import { IntegrationBlock, IntegrationBlockType } from '@typebot.io/schemas'
|
||||
import { IntegrationBlock } from '@typebot.io/schemas'
|
||||
import { IntegrationBlockType } from '@typebot.io/schemas/features/blocks/integrations/constants'
|
||||
|
||||
export const executeIntegration = ({
|
||||
block,
|
||||
|
||||
@@ -6,8 +6,9 @@ import { executeTypebotLink } from '@/features/blocks/logic/typebotLink'
|
||||
import { executeWait } from '@/features/blocks/logic/wait'
|
||||
import { LinkedTypebot } from '@/providers/TypebotProvider'
|
||||
import { EdgeId, LogicState } from '@/types'
|
||||
import { LogicBlock, LogicBlockType } from '@typebot.io/schemas'
|
||||
import { LogicBlock } from '@typebot.io/schemas'
|
||||
import { executeScript } from '@/features/blocks/logic/script/executeScript'
|
||||
import { LogicBlockType } from '@typebot.io/schemas/features/blocks/logic/constants'
|
||||
|
||||
export const executeLogic = async (
|
||||
block: LogicBlock,
|
||||
|
||||
@@ -4,16 +4,16 @@ import { validateUrl } from '@/features/blocks/inputs/url'
|
||||
import { parseVariables } from '@/features/variables'
|
||||
import {
|
||||
BubbleBlock,
|
||||
BubbleBlockType,
|
||||
Edge,
|
||||
EmailInputBlock,
|
||||
InputBlockType,
|
||||
PhoneNumberInputBlock,
|
||||
Block,
|
||||
UrlInputBlock,
|
||||
Variable,
|
||||
} from '@typebot.io/schemas'
|
||||
import { isInputBlock } from '@typebot.io/lib'
|
||||
import { isDefined, isInputBlock } from '@typebot.io/lib'
|
||||
import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants'
|
||||
import { BubbleBlockType } from '@typebot.io/schemas/features/blocks/bubbles/constants'
|
||||
|
||||
export const isInputValid = (
|
||||
inputValue: string,
|
||||
@@ -33,23 +33,25 @@ export const isInputValid = (
|
||||
export const blockCanBeRetried = (
|
||||
block: Block
|
||||
): block is EmailInputBlock | UrlInputBlock | PhoneNumberInputBlock =>
|
||||
isInputBlock(block) && 'retryMessageContent' in block.options
|
||||
isInputBlock(block) &&
|
||||
isDefined(block.options) &&
|
||||
'retryMessageContent' in block.options
|
||||
|
||||
export const parseRetryBlock = (
|
||||
block: EmailInputBlock | UrlInputBlock | PhoneNumberInputBlock,
|
||||
groupId: string,
|
||||
variables: Variable[],
|
||||
createEdge: (edge: Edge) => void
|
||||
): BubbleBlock => {
|
||||
const content = parseVariables(variables)(block.options.retryMessageContent)
|
||||
const content = parseVariables(variables)(block.options?.retryMessageContent)
|
||||
const newBlockId = block.id + Math.random() * 1000
|
||||
const newEdge: Edge = {
|
||||
id: (Math.random() * 1000).toString(),
|
||||
from: { blockId: newBlockId, groupId: block.groupId },
|
||||
to: { groupId: block.groupId, blockId: block.id },
|
||||
from: { blockId: newBlockId },
|
||||
to: { groupId, blockId: block.id },
|
||||
}
|
||||
createEdge(newEdge)
|
||||
return {
|
||||
groupId: block.groupId,
|
||||
id: newBlockId,
|
||||
type: BubbleBlockType.TEXT,
|
||||
content: {
|
||||
|
||||
@@ -4,7 +4,6 @@ export default defineConfig((options) => ({
|
||||
entry: ['src/index.ts'],
|
||||
sourcemap: true,
|
||||
minify: !options.watch,
|
||||
dts: true,
|
||||
format: ['esm', 'cjs'],
|
||||
loader: {
|
||||
'.css': 'text',
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
"jest-environment-jsdom": "29.4.1",
|
||||
"prettier": "2.8.3",
|
||||
"ts-jest": "29.0.5",
|
||||
"typescript": "4.9.4",
|
||||
"typescript": "5.2.2",
|
||||
"@typebot.io/tsconfig": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,6 +43,6 @@
|
||||
"rollup-plugin-postcss": "4.0.2",
|
||||
"rollup-plugin-typescript-paths": "1.4.0",
|
||||
"tailwindcss": "3.3.3",
|
||||
"typescript": "5.1.6"
|
||||
"typescript": "5.2.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,21 +12,22 @@ import {
|
||||
} from '@/utils/storage'
|
||||
import { setCssVariablesValue } from '@/utils/setCssVariablesValue'
|
||||
import immutableCss from '../assets/immutable.css'
|
||||
import { InputBlock, StartElementId } from '@typebot.io/schemas'
|
||||
import { defaultTheme } from '@typebot.io/schemas/features/typebot/theme/constants'
|
||||
|
||||
export type BotProps = {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
typebot: string | any
|
||||
isPreview?: boolean
|
||||
resultId?: string
|
||||
startGroupId?: string
|
||||
prefilledVariables?: Record<string, unknown>
|
||||
apiHost?: string
|
||||
onNewInputBlock?: (ids: { id: string; groupId: string }) => void
|
||||
onNewInputBlock?: (inputBlock: InputBlock) => void
|
||||
onAnswer?: (answer: { message: string; blockId: string }) => void
|
||||
onInit?: () => void
|
||||
onEnd?: () => void
|
||||
onNewLogs?: (logs: OutgoingLog[]) => void
|
||||
}
|
||||
} & StartElementId
|
||||
|
||||
export const Bot = (props: BotProps & { class?: string }) => {
|
||||
const [initialChatReply, setInitialChatReply] = createSignal<
|
||||
@@ -54,11 +55,15 @@ export const Bot = (props: BotProps & { class?: string }) => {
|
||||
resultId: isNotEmpty(props.resultId)
|
||||
? props.resultId
|
||||
: getExistingResultIdFromStorage(typebotIdFromProps),
|
||||
startGroupId: props.startGroupId,
|
||||
prefilledVariables: {
|
||||
...prefilledVariables,
|
||||
...props.prefilledVariables,
|
||||
},
|
||||
...('startGroupId' in props
|
||||
? { startGroupId: props.startGroupId }
|
||||
: 'startEventId' in props
|
||||
? { startEventId: props.startEventId }
|
||||
: {}),
|
||||
})
|
||||
if (error && 'code' in error && typeof error.code === 'string') {
|
||||
if (typeof props.typebot !== 'string' || (props.isPreview ?? false)) {
|
||||
@@ -80,7 +85,7 @@ export const Bot = (props: BotProps & { class?: string }) => {
|
||||
}
|
||||
|
||||
if (data.resultId && typebotIdFromProps)
|
||||
setResultInStorage(data.typebot.settings.general.rememberUser?.storage)(
|
||||
setResultInStorage(data.typebot.settings.general?.rememberUser?.storage)(
|
||||
typebotIdFromProps,
|
||||
data.resultId
|
||||
)
|
||||
@@ -88,10 +93,7 @@ export const Bot = (props: BotProps & { class?: string }) => {
|
||||
setCustomCss(data.typebot.theme.customCss ?? '')
|
||||
|
||||
if (data.input?.id && props.onNewInputBlock)
|
||||
props.onNewInputBlock({
|
||||
id: data.input.id,
|
||||
groupId: data.input.groupId,
|
||||
})
|
||||
props.onNewInputBlock(data.input)
|
||||
if (data.logs) props.onNewLogs?.(data.logs)
|
||||
}
|
||||
|
||||
@@ -157,7 +159,7 @@ type BotContentProps = {
|
||||
initialChatReply: InitialChatReply
|
||||
context: BotContext
|
||||
class?: string
|
||||
onNewInputBlock?: (block: { id: string; groupId: string }) => void
|
||||
onNewInputBlock?: (inputBlock: InputBlock) => void
|
||||
onAnswer?: (answer: { message: string; blockId: string }) => void
|
||||
onEnd?: () => void
|
||||
onNewLogs?: (logs: OutgoingLog[]) => void
|
||||
@@ -176,13 +178,15 @@ const BotContent = (props: BotContentProps) => {
|
||||
existingFont
|
||||
?.getAttribute('href')
|
||||
?.includes(
|
||||
props.initialChatReply.typebot?.theme?.general?.font ?? 'Open Sans'
|
||||
props.initialChatReply.typebot?.theme?.general?.font ??
|
||||
defaultTheme.general.font
|
||||
)
|
||||
)
|
||||
return
|
||||
const font = document.createElement('link')
|
||||
font.href = `https://fonts.bunny.net/css2?family=${
|
||||
props.initialChatReply.typebot?.theme?.general?.font ?? 'Open Sans'
|
||||
props.initialChatReply.typebot?.theme?.general?.font ??
|
||||
defaultTheme.general.font
|
||||
}:ital,wght@0,300;0,400;0,600;1,300;1,400;1,600&display=swap');')`
|
||||
font.rel = 'stylesheet'
|
||||
font.id = 'bot-font'
|
||||
@@ -224,7 +228,9 @@ const BotContent = (props: BotContentProps) => {
|
||||
/>
|
||||
</div>
|
||||
<Show
|
||||
when={props.initialChatReply.typebot.settings.general.isBrandingEnabled}
|
||||
when={
|
||||
props.initialChatReply.typebot.settings.general?.isBrandingEnabled
|
||||
}
|
||||
>
|
||||
<LiteBadge botContainer={botContainer} />
|
||||
</Show>
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { BotContext, ChatChunk as ChatChunkType } from '@/types'
|
||||
import { isMobile } from '@/utils/isMobileSignal'
|
||||
import type { ChatReply, Settings, Theme } from '@typebot.io/schemas'
|
||||
import { ChatReply, Settings, Theme } from '@typebot.io/schemas'
|
||||
import { createSignal, For, onMount, Show } from 'solid-js'
|
||||
import { HostBubble } from '../bubbles/HostBubble'
|
||||
import { InputChatBlock } from '../InputChatBlock'
|
||||
import { AvatarSideContainer } from './AvatarSideContainer'
|
||||
import { StreamingBubble } from '../bubbles/StreamingBubble'
|
||||
import { defaultTheme } from '@typebot.io/schemas/features/typebot/theme/constants'
|
||||
import { defaultSettings } from '@typebot.io/schemas/features/typebot/settings/constants'
|
||||
|
||||
type Props = Pick<ChatReply, 'messages' | 'input'> & {
|
||||
theme: Theme
|
||||
@@ -56,12 +58,13 @@ export const ChatChunk = (props: Props) => {
|
||||
<div class={'flex' + (isMobile() ? ' gap-1' : ' gap-2')}>
|
||||
<Show
|
||||
when={
|
||||
props.theme.chat.hostAvatar?.isEnabled &&
|
||||
(props.theme.chat?.hostAvatar?.isEnabled ??
|
||||
defaultTheme.chat.hostAvatar.isEnabled) &&
|
||||
props.messages.length > 0
|
||||
}
|
||||
>
|
||||
<AvatarSideContainer
|
||||
hostAvatarSrc={props.theme.chat.hostAvatar?.url}
|
||||
hostAvatarSrc={props.theme.chat?.hostAvatar?.url}
|
||||
hideAvatar={props.hideAvatar}
|
||||
/>
|
||||
</Show>
|
||||
@@ -69,13 +72,15 @@ export const ChatChunk = (props: Props) => {
|
||||
<div
|
||||
class="flex flex-col flex-1 gap-2"
|
||||
style={{
|
||||
'max-width': props.theme.chat.guestAvatar?.isEnabled
|
||||
? isMobile()
|
||||
? 'calc(100% - 32px - 32px)'
|
||||
: 'calc(100% - 48px - 48px)'
|
||||
: isMobile()
|
||||
? 'calc(100% - 32px)'
|
||||
: 'calc(100% - 48px)',
|
||||
'max-width':
|
||||
props.theme.chat?.guestAvatar?.isEnabled ??
|
||||
defaultTheme.chat.guestAvatar.isEnabled
|
||||
? isMobile()
|
||||
? 'calc(100% - 32px - 32px)'
|
||||
: 'calc(100% - 48px - 48px)'
|
||||
: isMobile()
|
||||
? 'calc(100% - 32px)'
|
||||
: 'calc(100% - 48px)',
|
||||
}}
|
||||
>
|
||||
<For each={props.messages.slice(0, displayedMessageIndex() + 1)}>
|
||||
@@ -95,11 +100,15 @@ export const ChatChunk = (props: Props) => {
|
||||
ref={inputRef}
|
||||
block={props.input}
|
||||
inputIndex={props.inputIndex}
|
||||
hasHostAvatar={props.theme.chat.hostAvatar?.isEnabled ?? false}
|
||||
guestAvatar={props.theme.chat.guestAvatar}
|
||||
hasHostAvatar={
|
||||
props.theme.chat?.hostAvatar?.isEnabled ??
|
||||
defaultTheme.chat.hostAvatar.isEnabled
|
||||
}
|
||||
guestAvatar={props.theme.chat?.guestAvatar}
|
||||
context={props.context}
|
||||
isInputPrefillEnabled={
|
||||
props.settings.general.isInputPrefillEnabled ?? true
|
||||
props.settings.general?.isInputPrefillEnabled ??
|
||||
defaultSettings.general.isInputPrefillEnabled
|
||||
}
|
||||
hasError={props.hasError}
|
||||
onSubmit={props.onSubmit}
|
||||
@@ -109,9 +118,14 @@ export const ChatChunk = (props: Props) => {
|
||||
<Show when={props.streamingMessageId} keyed>
|
||||
{(streamingMessageId) => (
|
||||
<div class={'flex' + (isMobile() ? ' gap-1' : ' gap-2')}>
|
||||
<Show when={props.theme.chat.hostAvatar?.isEnabled}>
|
||||
<Show
|
||||
when={
|
||||
props.theme.chat?.hostAvatar?.isEnabled ??
|
||||
defaultTheme.chat.hostAvatar.isEnabled
|
||||
}
|
||||
>
|
||||
<AvatarSideContainer
|
||||
hostAvatarSrc={props.theme.chat.hostAvatar?.url}
|
||||
hostAvatarSrc={props.theme.chat?.hostAvatar?.url}
|
||||
hideAvatar={props.hideAvatar}
|
||||
/>
|
||||
</Show>
|
||||
@@ -119,13 +133,15 @@ export const ChatChunk = (props: Props) => {
|
||||
<div
|
||||
class="flex flex-col flex-1 gap-2"
|
||||
style={{
|
||||
'max-width': props.theme.chat.guestAvatar?.isEnabled
|
||||
? isMobile()
|
||||
? 'calc(100% - 32px - 32px)'
|
||||
: 'calc(100% - 48px - 48px)'
|
||||
: isMobile()
|
||||
? 'calc(100% - 32px)'
|
||||
: 'calc(100% - 48px)',
|
||||
'max-width':
|
||||
props.theme.chat?.hostAvatar?.isEnabled ??
|
||||
defaultTheme.chat.hostAvatar.isEnabled
|
||||
? isMobile()
|
||||
? 'calc(100% - 32px - 32px)'
|
||||
: 'calc(100% - 48px - 48px)'
|
||||
: isMobile()
|
||||
? 'calc(100% - 32px)'
|
||||
: 'calc(100% - 48px)',
|
||||
}}
|
||||
>
|
||||
<StreamingBubble streamingMessageId={streamingMessageId} />
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { ChatReply, SendMessageInput, Theme } from '@typebot.io/schemas'
|
||||
import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/enums'
|
||||
import {
|
||||
ChatReply,
|
||||
InputBlock,
|
||||
SendMessageInput,
|
||||
Theme,
|
||||
} from '@typebot.io/schemas'
|
||||
import {
|
||||
createEffect,
|
||||
createSignal,
|
||||
@@ -25,6 +29,7 @@ import {
|
||||
formattedMessages,
|
||||
setFormattedMessages,
|
||||
} from '@/utils/formattedMessagesSignal'
|
||||
import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants'
|
||||
|
||||
const parseDynamicTheme = (
|
||||
initialTheme: Theme,
|
||||
@@ -34,26 +39,26 @@ const parseDynamicTheme = (
|
||||
chat: {
|
||||
...initialTheme.chat,
|
||||
hostAvatar:
|
||||
initialTheme.chat.hostAvatar && dynamicTheme?.hostAvatarUrl
|
||||
initialTheme.chat?.hostAvatar && dynamicTheme?.hostAvatarUrl
|
||||
? {
|
||||
...initialTheme.chat.hostAvatar,
|
||||
url: dynamicTheme.hostAvatarUrl,
|
||||
}
|
||||
: initialTheme.chat.hostAvatar,
|
||||
: initialTheme.chat?.hostAvatar,
|
||||
guestAvatar:
|
||||
initialTheme.chat.guestAvatar && dynamicTheme?.guestAvatarUrl
|
||||
initialTheme.chat?.guestAvatar && dynamicTheme?.guestAvatarUrl
|
||||
? {
|
||||
...initialTheme.chat.guestAvatar,
|
||||
url: dynamicTheme?.guestAvatarUrl,
|
||||
}
|
||||
: initialTheme.chat.guestAvatar,
|
||||
: initialTheme.chat?.guestAvatar,
|
||||
},
|
||||
})
|
||||
|
||||
type Props = {
|
||||
initialChatReply: InitialChatReply
|
||||
context: BotContext
|
||||
onNewInputBlock?: (ids: { id: string; groupId: string }) => void
|
||||
onNewInputBlock?: (inputBlock: InputBlock) => void
|
||||
onAnswer?: (answer: { message: string; blockId: string }) => void
|
||||
onEnd?: () => void
|
||||
onNewLogs?: (logs: OutgoingLog[]) => void
|
||||
@@ -178,11 +183,8 @@ export const ConversationContainer = (props: Props) => {
|
||||
}
|
||||
if (data.logs) props.onNewLogs?.(data.logs)
|
||||
if (data.dynamicTheme) setDynamicTheme(data.dynamicTheme)
|
||||
if (data.input?.id && props.onNewInputBlock) {
|
||||
props.onNewInputBlock({
|
||||
id: data.input.id,
|
||||
groupId: data.input.groupId,
|
||||
})
|
||||
if (data.input && props.onNewInputBlock) {
|
||||
props.onNewInputBlock(data.input)
|
||||
}
|
||||
if (data.clientSideActions) {
|
||||
const actionsBeforeFirstBubble = data.clientSideActions.filter((action) =>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user