2
0

Introducing The Forge (#1072)

The Forge allows anyone to easily create their own Typebot Block.

Closes #380
This commit is contained in:
Baptiste Arnaud
2023-12-13 10:22:02 +01:00
committed by GitHub
parent c373108b55
commit 5e019bbb22
184 changed files with 42659 additions and 37411 deletions

View File

@ -5,9 +5,9 @@ import {
} from '@typebot.io/schemas'
import { isDefined } from '@typebot.io/lib'
import { filterChoiceItems } from './filterChoiceItems'
import { deepParseVariables } from '../../../variables/deepParseVariables'
import { transformStringVariablesToList } from '../../../variables/transformVariablesToList'
import { updateVariablesInSession } from '../../../variables/updateVariablesInSession'
import { deepParseVariables } from '@typebot.io/variables/deepParseVariables'
import { transformVariablesToList } from '@typebot.io/variables/transformVariablesToList'
import { updateVariablesInSession } from '@typebot.io/variables/updateVariablesInSession'
export const injectVariableValuesInButtonsInputBlock =
(state: SessionState) =>
@ -38,7 +38,7 @@ const getVariableValue =
(variable: VariableWithValue): (string | null)[] => {
if (!Array.isArray(variable.value)) {
const { variables } = state.typebotsQueue[0].typebot
const [transformedVariable] = transformStringVariablesToList(variables)([
const [transformedVariable] = transformVariablesToList(variables)([
variable.id,
])
updateVariablesInSession(state)([transformedVariable])

View File

@ -1,7 +1,7 @@
import { getPrefilledInputValue } from '../../../getPrefilledValue'
import { DateInputBlock, SessionState, Variable } from '@typebot.io/schemas'
import { deepParseVariables } from '../../../variables/deepParseVariables'
import { parseVariables } from '../../../variables/parseVariables'
import { deepParseVariables } from '@typebot.io/variables/deepParseVariables'
import { parseVariables } from '@typebot.io/variables/parseVariables'
export const parseDateInput =
(state: SessionState) => (block: DateInputBlock) => {

View File

@ -1,6 +1,6 @@
import { isNotDefined } from '@typebot.io/lib'
import { NumberInputBlock, Variable } from '@typebot.io/schemas'
import { parseVariables } from '../../../variables/parseVariables'
import { parseVariables } from '@typebot.io/variables/parseVariables'
export const validateNumber = (
inputValue: string,

View File

@ -7,7 +7,7 @@ import {
} from '@typebot.io/schemas'
import Stripe from 'stripe'
import { decrypt } from '@typebot.io/lib/api/encryption/decrypt'
import { parseVariables } from '../../../variables/parseVariables'
import { parseVariables } from '@typebot.io/variables/parseVariables'
import prisma from '@typebot.io/lib/prisma'
import { defaultPaymentInputOptions } from '@typebot.io/schemas/features/blocks/inputs/payment/constants'

View File

@ -5,7 +5,7 @@ import {
} from '@typebot.io/schemas'
import { isDefined } from '@typebot.io/lib'
import { filterPictureChoiceItems } from './filterPictureChoiceItems'
import { deepParseVariables } from '../../../variables/deepParseVariables'
import { deepParseVariables } from '@typebot.io/variables/deepParseVariables'
export const injectVariableValuesInPictureChoiceBlock =
(variables: Variable[]) =>

View File

@ -2,9 +2,9 @@ import { ExecuteIntegrationResponse } from '../../../types'
import { env } from '@typebot.io/env'
import { isDefined } from '@typebot.io/lib'
import { ChatwootBlock, SessionState } from '@typebot.io/schemas'
import { extractVariablesFromText } from '../../../variables/extractVariablesFromText'
import { parseGuessedValueType } from '../../../variables/parseGuessedValueType'
import { parseVariables } from '../../../variables/parseVariables'
import { extractVariablesFromText } from '@typebot.io/variables/extractVariablesFromText'
import { parseGuessedValueType } from '@typebot.io/variables/parseGuessedValueType'
import { parseVariables } from '@typebot.io/variables/parseVariables'
import { defaultChatwootOptions } from '@typebot.io/schemas/features/blocks/integrations/chatwoot/constants'
const parseSetUserCode = (

View File

@ -8,8 +8,8 @@ import { isNotEmpty, byId, isDefined } from '@typebot.io/lib'
import { getAuthenticatedGoogleDoc } from './helpers/getAuthenticatedGoogleDoc'
import { ExecuteIntegrationResponse } from '../../../types'
import { matchFilter } from './helpers/matchFilter'
import { deepParseVariables } from '../../../variables/deepParseVariables'
import { updateVariablesInSession } from '../../../variables/updateVariablesInSession'
import { deepParseVariables } from '@typebot.io/variables/deepParseVariables'
import { updateVariablesInSession } from '@typebot.io/variables/updateVariablesInSession'
export const getRow = async (
state: SessionState,

View File

@ -1,5 +1,5 @@
import { Variable, Cell } from '@typebot.io/schemas'
import { parseVariables } from '../../../../variables/parseVariables'
import { parseVariables } from '@typebot.io/variables/parseVariables'
export const parseCellValues =
(variables: Variable[]) =>

View File

@ -7,7 +7,7 @@ import { parseCellValues } from './helpers/parseCellValues'
import { getAuthenticatedGoogleDoc } from './helpers/getAuthenticatedGoogleDoc'
import { ExecuteIntegrationResponse } from '../../../types'
import { matchFilter } from './helpers/matchFilter'
import { deepParseVariables } from '../../../variables/deepParseVariables'
import { deepParseVariables } from '@typebot.io/variables/deepParseVariables'
export const updateRow = async (
state: SessionState,

View File

@ -1,6 +1,6 @@
import { ExecuteIntegrationResponse } from '../../../types'
import { ExecuteIntegrationResponse } from '../../../../types'
import { GoogleAnalyticsBlock, SessionState } from '@typebot.io/schemas'
import { deepParseVariables } from '../../../variables/deepParseVariables'
import { deepParseVariables } from '@typebot.io/variables/deepParseVariables'
export const executeGoogleAnalyticsBlock = (
state: SessionState,

View File

@ -7,12 +7,12 @@ import { isNotEmpty } from '@typebot.io/lib'
import { decrypt } from '@typebot.io/lib/api/encryption/decrypt'
import prisma from '@typebot.io/lib/prisma'
import { defaultOpenAIOptions } from '@typebot.io/schemas/features/blocks/integrations/openai/constants'
import { ExecuteIntegrationResponse } from '../../../../types'
import { ExecuteIntegrationResponse } from '../../../../../types'
import OpenAI, { ClientOptions } from 'openai'
import { uploadFileToBucket } from '@typebot.io/lib/s3/uploadFileToBucket'
import { updateVariablesInSession } from '../../../../variables/updateVariablesInSession'
import { updateVariablesInSession } from '@typebot.io/variables/updateVariablesInSession'
import { createId } from '@paralleldrive/cuid2'
import { parseVariables } from '../../../../variables/parseVariables'
import { parseVariables } from '@typebot.io/variables/parseVariables'
export const createSpeechOpenAI = async (
state: SessionState,

View File

@ -15,9 +15,9 @@ import { parseChatCompletionMessages } from './parseChatCompletionMessages'
import { executeChatCompletionOpenAIRequest } from './executeChatCompletionOpenAIRequest'
import { isPlaneteScale } from '@typebot.io/lib/isPlanetScale'
import prisma from '@typebot.io/lib/prisma'
import { ExecuteIntegrationResponse } from '../../../types'
import { parseVariableNumber } from '../../../variables/parseVariableNumber'
import { updateVariablesInSession } from '../../../variables/updateVariablesInSession'
import { ExecuteIntegrationResponse } from '../../../../types'
import { parseVariableNumber } from '@typebot.io/variables/parseVariableNumber'
import { updateVariablesInSession } from '@typebot.io/variables/updateVariablesInSession'
import {
chatCompletionMessageRoles,
defaultOpenAIOptions,

View File

@ -1,7 +1,7 @@
import { SessionState } from '@typebot.io/schemas'
import { OpenAIBlock } from '@typebot.io/schemas/features/blocks/integrations/openai'
import { createChatCompletionOpenAI } from './createChatCompletionOpenAI'
import { ExecuteIntegrationResponse } from '../../../types'
import { ExecuteIntegrationResponse } from '../../../../types'
import { createSpeechOpenAI } from './audio/createSpeechOpenAI'
export const executeOpenAIBlock = async (

View File

@ -7,7 +7,7 @@ import {
} from '@typebot.io/schemas/features/blocks/integrations/openai'
import { SessionState } from '@typebot.io/schemas/features/chat/sessionState'
import { OpenAIStream } from 'ai'
import { parseVariableNumber } from '../../../variables/parseVariableNumber'
import { parseVariableNumber } from '@typebot.io/variables/parseVariableNumber'
import { ClientOptions, OpenAI } from 'openai'
import { defaultOpenAIOptions } from '@typebot.io/schemas/features/blocks/integrations/openai/constants'

View File

@ -2,8 +2,8 @@ import { byId, isNotEmpty } from '@typebot.io/lib'
import { Variable, VariableWithValue } from '@typebot.io/schemas'
import { ChatCompletionOpenAIOptions } from '@typebot.io/schemas/features/blocks/integrations/openai'
import type { OpenAI } from 'openai'
import { parseVariables } from '../../../variables/parseVariables'
import { transformStringVariablesToList } from '../../../variables/transformVariablesToList'
import { parseVariables } from '@typebot.io/variables/parseVariables'
import { transformVariablesToList } from '@typebot.io/variables/transformVariablesToList'
export const parseChatCompletionMessages =
(variables: Variable[]) =>
@ -24,7 +24,7 @@ export const parseChatCompletionMessages =
)
return
variablesTransformedToList.push(
...transformStringVariablesToList(variables)([
...transformVariablesToList(variables)([
message.content.assistantMessagesVariableId,
message.content.userMessagesVariableId,
])

View File

@ -2,7 +2,7 @@ import { byId, isDefined } from '@typebot.io/lib'
import { ContinueChatResponse, SessionState } from '@typebot.io/schemas'
import { ChatCompletionOpenAIOptions } from '@typebot.io/schemas/features/blocks/integrations/openai'
import { VariableWithUnknowValue } from '@typebot.io/schemas/features/typebot/variable'
import { updateVariablesInSession } from '../../../variables/updateVariablesInSession'
import { updateVariablesInSession } from '@typebot.io/variables/updateVariablesInSession'
export const resumeChatCompletion =
(

View File

@ -1,6 +1,6 @@
import { PixelBlock, SessionState } from '@typebot.io/schemas'
import { ExecuteIntegrationResponse } from '../../../types'
import { deepParseVariables } from '../../../variables/deepParseVariables'
import { deepParseVariables } from '@typebot.io/variables/deepParseVariables'
export const executePixelBlock = (
state: SessionState,

View File

@ -14,11 +14,11 @@ import { byId, isDefined, isEmpty, isNotDefined, omit } from '@typebot.io/lib'
import { getDefinedVariables, parseAnswers } from '@typebot.io/lib/results'
import { decrypt } from '@typebot.io/lib/api/encryption/decrypt'
import { defaultFrom, defaultTransportOptions } from './constants'
import { findUniqueVariableValue } from '../../../variables/findUniqueVariableValue'
import { findUniqueVariableValue } from '@typebot.io/variables/findUniqueVariableValue'
import { env } from '@typebot.io/env'
import { ExecuteIntegrationResponse } from '../../../types'
import prisma from '@typebot.io/lib/prisma'
import { parseVariables } from '../../../variables/parseVariables'
import { parseVariables } from '@typebot.io/variables/parseVariables'
import { defaultSendEmailOptions } from '@typebot.io/schemas/features/blocks/integrations/sendEmail/constants'
export const executeSendEmailBlock = async (

View File

@ -18,7 +18,7 @@ import { getDefinedVariables, parseAnswers } from '@typebot.io/lib/results'
import got, { Method, HTTPError, OptionsInit } from 'got'
import { resumeWebhookExecution } from './resumeWebhookExecution'
import { ExecuteIntegrationResponse } from '../../../types'
import { parseVariables } from '../../../variables/parseVariables'
import { parseVariables } from '@typebot.io/variables/parseVariables'
import prisma from '@typebot.io/lib/prisma'
import {
HttpMethod,

View File

@ -9,8 +9,8 @@ import {
} from '@typebot.io/schemas'
import { SessionState } from '@typebot.io/schemas/features/chat/sessionState'
import { ExecuteIntegrationResponse } from '../../../types'
import { parseVariables } from '../../../variables/parseVariables'
import { updateVariablesInSession } from '../../../variables/updateVariablesInSession'
import { parseVariables } from '@typebot.io/variables/parseVariables'
import { updateVariablesInSession } from '@typebot.io/variables/updateVariablesInSession'
type Props = {
state: SessionState

View File

@ -10,7 +10,7 @@ import { byId, isDefined, isEmpty } from '@typebot.io/lib'
import { getDefinedVariables, parseAnswers } from '@typebot.io/lib/results'
import prisma from '@typebot.io/lib/prisma'
import { ExecuteIntegrationResponse } from '../../../types'
import { updateVariablesInSession } from '../../../variables/updateVariablesInSession'
import { updateVariablesInSession } from '@typebot.io/variables/updateVariablesInSession'
const URL = 'https://api.zemantic.ai/v1/search-documents'

View File

@ -1,7 +1,7 @@
import { isNotDefined, isDefined } from '@typebot.io/lib'
import { Comparison, Condition, Variable } from '@typebot.io/schemas'
import { findUniqueVariableValue } from '../../../variables/findUniqueVariableValue'
import { parseVariables } from '../../../variables/parseVariables'
import { findUniqueVariableValue } from '@typebot.io/variables/findUniqueVariableValue'
import { parseVariables } from '@typebot.io/variables/parseVariables'
import {
LogicalOperator,
ComparisonOperators,

View File

@ -1,7 +1,7 @@
import { RedirectBlock, SessionState } from '@typebot.io/schemas'
import { sanitizeUrl } from '@typebot.io/lib'
import { ExecuteLogicResponse } from '../../../types'
import { parseVariables } from '../../../variables/parseVariables'
import { parseVariables } from '@typebot.io/variables/parseVariables'
export const executeRedirect = (
state: SessionState,

View File

@ -1,8 +1,8 @@
import { ExecuteLogicResponse } from '../../../types'
import { ScriptBlock, SessionState, Variable } from '@typebot.io/schemas'
import { extractVariablesFromText } from '../../../variables/extractVariablesFromText'
import { parseGuessedValueType } from '../../../variables/parseGuessedValueType'
import { parseVariables } from '../../../variables/parseVariables'
import { extractVariablesFromText } from '@typebot.io/variables/extractVariablesFromText'
import { parseGuessedValueType } from '@typebot.io/variables/parseGuessedValueType'
import { parseVariables } from '@typebot.io/variables/parseVariables'
export const executeScript = (
state: SessionState,

View File

@ -2,9 +2,9 @@ import { SessionState, SetVariableBlock, Variable } from '@typebot.io/schemas'
import { byId } from '@typebot.io/lib'
import { ExecuteLogicResponse } from '../../../types'
import { parseScriptToExecuteClientSideAction } from '../script/executeScript'
import { parseGuessedValueType } from '../../../variables/parseGuessedValueType'
import { parseVariables } from '../../../variables/parseVariables'
import { updateVariablesInSession } from '../../../variables/updateVariablesInSession'
import { parseGuessedValueType } from '@typebot.io/variables/parseGuessedValueType'
import { parseVariables } from '@typebot.io/variables/parseVariables'
import { updateVariablesInSession } from '@typebot.io/variables/updateVariablesInSession'
import { createId } from '@paralleldrive/cuid2'
export const executeSetVariable = (

View File

@ -1,6 +1,6 @@
import { ExecuteLogicResponse } from '../../../types'
import { SessionState, WaitBlock } from '@typebot.io/schemas'
import { parseVariables } from '../../../variables/parseVariables'
import { parseVariables } from '@typebot.io/variables/parseVariables'
import { isNotDefined } from '@typebot.io/lib'
export const executeWait = (

View File

@ -12,7 +12,6 @@ import { getNextGroup } from './getNextGroup'
import { validateEmail } from './blocks/inputs/email/validateEmail'
import { formatPhoneNumber } from './blocks/inputs/phone/formatPhoneNumber'
import { validateUrl } from './blocks/inputs/url/validateUrl'
import { resumeChatCompletion } from './blocks/integrations/openai/resumeChatCompletion'
import { resumeWebhookExecution } from './blocks/integrations/webhook/resumeWebhookExecution'
import { upsertAnswer } from './queries/upsertAnswer'
import { parseButtonsReply } from './blocks/inputs/buttons/parseButtonsReply'
@ -21,8 +20,8 @@ import { validateNumber } from './blocks/inputs/number/validateNumber'
import { parseDateReply } from './blocks/inputs/date/parseDateReply'
import { validateRatingReply } from './blocks/inputs/rating/validateRatingReply'
import { parsePictureChoicesReply } from './blocks/inputs/pictureChoice/parsePictureChoicesReply'
import { parseVariables } from './variables/parseVariables'
import { updateVariablesInSession } from './variables/updateVariablesInSession'
import { parseVariables } from '@typebot.io/variables/parseVariables'
import { updateVariablesInSession } from '@typebot.io/variables/updateVariablesInSession'
import { startBotFlow } from './startBotFlow'
import { TRPCError } from '@trpc/server'
import { parseNumber } from './blocks/inputs/number/parseNumber'
@ -37,6 +36,9 @@ import { defaultPictureChoiceOptions } from '@typebot.io/schemas/features/blocks
import { defaultFileInputOptions } from '@typebot.io/schemas/features/blocks/inputs/file/constants'
import { VisitedEdge } from '@typebot.io/prisma'
import { getBlockById } from '@typebot.io/lib/getBlockById'
import { ForgedBlock, forgedBlocks } from '@typebot.io/forge-schemas'
import { enabledBlocks } from '@typebot.io/forge-repository'
import { resumeChatCompletion } from './blocks/integrations/legacy/openai/resumeChatCompletion'
type Params = {
version: 1 | 2
@ -80,14 +82,9 @@ export const continueBotFlow = async (
}
newSessionState = updateVariablesInSession(state)([newVariable])
}
} else if (reply && block.type === IntegrationBlockType.WEBHOOK) {
const result = resumeWebhookExecution({
state,
block,
response: JSON.parse(reply),
})
if (result.newSessionState) newSessionState = result.newSessionState
} else if (
}
// Legacy
else if (
block.type === IntegrationBlockType.OPEN_AI &&
block.options?.task === 'Create chat completion'
) {
@ -99,6 +96,58 @@ export const continueBotFlow = async (
})(reply)
newSessionState = result.newSessionState
}
} else if (reply && block.type === IntegrationBlockType.WEBHOOK) {
const result = resumeWebhookExecution({
state,
block,
response: JSON.parse(reply),
})
if (result.newSessionState) newSessionState = result.newSessionState
} else if (
enabledBlocks.includes(block.type as (typeof enabledBlocks)[number])
) {
if (reply) {
const options = (block as ForgedBlock).options
const action = forgedBlocks
.find((b) => b.id === block.type)
?.actions.find((a) => a.name === options?.action)
if (action) {
if (action.run?.stream?.getStreamVariableId) {
firstBubbleWasStreamed = true
const variableToUpdate =
state.typebotsQueue[0].typebot.variables.find(
(v) => v.id === action?.run?.stream?.getStreamVariableId(options)
)
if (variableToUpdate)
newSessionState = updateVariablesInSession(state)([
{
...variableToUpdate,
value: reply,
},
])
}
if (
action.run?.web?.displayEmbedBubble?.waitForEvent?.getSaveVariableId
) {
const variableToUpdate =
state.typebotsQueue[0].typebot.variables.find(
(v) =>
v.id ===
action?.run?.web?.displayEmbedBubble?.waitForEvent?.getSaveVariableId?.(
options
)
)
if (variableToUpdate)
newSessionState = updateVariablesInSession(state)([
{
...variableToUpdate,
value: reply,
},
])
}
}
}
}
let formattedReply: string | undefined

View File

@ -6,6 +6,7 @@ import {
SessionState,
} from '@typebot.io/schemas'
import {
createId,
isBubbleBlock,
isInputBlock,
isIntegrationBlock,
@ -20,7 +21,7 @@ import { injectVariableValuesInButtonsInputBlock } from './blocks/inputs/buttons
import { injectVariableValuesInPictureChoiceBlock } from './blocks/inputs/pictureChoice/injectVariableValuesInPictureChoiceBlock'
import { getPrefilledInputValue } from './getPrefilledValue'
import { parseDateInput } from './blocks/inputs/date/parseDateInput'
import { deepParseVariables } from './variables/deepParseVariables'
import { deepParseVariables } from '@typebot.io/variables/deepParseVariables'
import {
BubbleBlockWithDefinedContent,
parseBubbleBlock,
@ -139,10 +140,21 @@ export const executeGroup = async (
lastBubbleBlockId,
})),
]
if (
'customEmbedBubble' in executionResponse &&
executionResponse.customEmbedBubble
) {
messages.push({
id: createId(),
...executionResponse.customEmbedBubble,
})
}
if (
executionResponse.clientSideActions?.find(
(action) => action.expectsDedicatedReply
)
) ||
('customEmbedBubble' in executionResponse &&
executionResponse.customEmbedBubble)
) {
return {
messages,

View File

@ -1,14 +1,15 @@
import { executeOpenAIBlock } from './blocks/integrations/openai/executeOpenAIBlock'
import { executeSendEmailBlock } from './blocks/integrations/sendEmail/executeSendEmailBlock'
import { executeWebhookBlock } from './blocks/integrations/webhook/executeWebhookBlock'
import { executeChatwootBlock } from './blocks/integrations/chatwoot/executeChatwootBlock'
import { executeGoogleAnalyticsBlock } from './blocks/integrations/googleAnalytics/executeGoogleAnalyticsBlock'
import { executeGoogleAnalyticsBlock } from './blocks/integrations/legacy/googleAnalytics/executeGoogleAnalyticsBlock'
import { executeGoogleSheetBlock } from './blocks/integrations/googleSheets/executeGoogleSheetBlock'
import { executePixelBlock } from './blocks/integrations/pixel/executePixelBlock'
import { executeZemanticAiBlock } from './blocks/integrations/zemanticAi/executeZemanticAiBlock'
import { IntegrationBlock, SessionState } from '@typebot.io/schemas'
import { ExecuteIntegrationResponse } from './types'
import { IntegrationBlockType } from '@typebot.io/schemas/features/blocks/integrations/constants'
import { executeOpenAIBlock } from './blocks/integrations/legacy/openai/executeOpenAIBlock'
import { executeForgedBlock } from './forge/executeForgedBlock'
export const executeIntegration =
(state: SessionState) =>
@ -33,5 +34,7 @@ export const executeIntegration =
return executePixelBlock(state, block)
case IntegrationBlockType.ZEMANTIC_AI:
return executeZemanticAiBlock(state, block)
default:
return executeForgedBlock(state, block)
}
}

View File

@ -0,0 +1,207 @@
import { VariableStore, LogsStore } from '@typebot.io/forge'
import { ForgedBlock, forgedBlocks } from '@typebot.io/forge-schemas'
import { decrypt } from '@typebot.io/lib/api/encryption/decrypt'
import { isPlaneteScale } from '@typebot.io/lib/isPlanetScale'
import prisma from '@typebot.io/lib/prisma'
import {
SessionState,
ContinueChatResponse,
Block,
TypebotInSession,
} from '@typebot.io/schemas'
import { deepParseVariables } from '@typebot.io/variables/deepParseVariables'
import {
ParseVariablesOptions,
parseVariables,
} from '@typebot.io/variables/parseVariables'
import { updateVariablesInSession } from '@typebot.io/variables/updateVariablesInSession'
import { ExecuteIntegrationResponse } from '../types'
import { byId } from '@typebot.io/lib'
import { BubbleBlockType } from '@typebot.io/schemas/features/blocks/bubbles/constants'
export const executeForgedBlock = async (
state: SessionState,
block: ForgedBlock
): Promise<ExecuteIntegrationResponse> => {
const blockDef = forgedBlocks.find((b) => b.id === block.type)
if (!blockDef) return { outgoingEdgeId: block.outgoingEdgeId }
const action = blockDef.actions.find((a) => a.name === block.options.action)
const noCredentialsError = {
status: 'error',
description: 'Credentials not provided for integration',
}
let credentials: { data: string; iv: string } | null = null
if (blockDef.auth) {
if (!block.options.credentialsId) {
return {
outgoingEdgeId: block.outgoingEdgeId,
logs: [noCredentialsError],
}
}
credentials = await prisma.credentials.findUnique({
where: {
id: block.options.credentialsId,
},
})
if (!credentials) {
console.error('Could not find credentials in database')
return {
outgoingEdgeId: block.outgoingEdgeId,
logs: [noCredentialsError],
}
}
}
const typebot = state.typebotsQueue[0].typebot
if (
action?.run?.stream &&
isPlaneteScale() &&
credentials &&
isCredentialsV2(credentials) &&
state.isStreamEnabled &&
!state.whatsApp &&
isNextBubbleTextWithStreamingVar(typebot)(
block.id,
action.run.stream.getStreamVariableId(block.options)
)
) {
return {
outgoingEdgeId: block.outgoingEdgeId,
clientSideActions: [
{
expectsDedicatedReply: true,
stream: true,
},
],
}
}
let newSessionState = state
const variables: VariableStore = {
get: (id: string) => {
const variable = newSessionState.typebotsQueue[0].typebot.variables.find(
(variable) => variable.id === id
)
return variable?.value
},
set: (id: string, value: unknown) => {
const variable = newSessionState.typebotsQueue[0].typebot.variables.find(
(variable) => variable.id === id
)
if (!variable) return
newSessionState = updateVariablesInSession(newSessionState)([
{ ...variable, value },
])
},
parse: (text: string, params?: ParseVariablesOptions) =>
parseVariables(
newSessionState.typebotsQueue[0].typebot.variables,
params
)(text),
}
let logs: NonNullable<ContinueChatResponse['logs']> = []
const logsStore: LogsStore = {
add: (log) => {
if (typeof log === 'string') {
logs.push({
status: 'error',
description: log,
})
return
}
logs.push(log)
},
}
const credentialsData = credentials
? await decrypt(credentials.data, credentials.iv)
: undefined
const parsedOptions = deepParseVariables(
state.typebotsQueue[0].typebot.variables
)(block.options)
await action?.run?.server?.({
credentials: credentialsData ?? {},
options: parsedOptions,
variables,
logs: logsStore,
})
const clientSideActions: ExecuteIntegrationResponse['clientSideActions'] = []
if (
action?.run?.web?.parseFunction &&
(state.typebotsQueue[0].resultId || !blockDef.isDisabledInPreview)
) {
clientSideActions.push({
codeToExecute: action?.run?.web?.parseFunction({
options: parsedOptions,
}),
})
}
return {
newSessionState,
outgoingEdgeId: block.outgoingEdgeId,
logs,
clientSideActions,
customEmbedBubble: action?.run?.web?.displayEmbedBubble
? {
type: 'custom-embed',
content: {
initFunction: action.run.web.displayEmbedBubble.parseInitFunction({
options: parsedOptions,
}),
waitForEventFunction:
action.run.web.displayEmbedBubble.waitForEvent?.parseFunction?.({
options: parsedOptions,
}),
},
}
: undefined,
}
}
const isNextBubbleTextWithStreamingVar =
(typebot: TypebotInSession) =>
(blockId: string, streamVariableId?: string): boolean => {
const streamVariable = typebot.variables.find(
(variable) => variable.id === streamVariableId
)
if (!streamVariable) return false
const nextBlock = getNextBlock(typebot)(blockId)
if (!nextBlock) return false
return (
nextBlock.type === BubbleBlockType.TEXT &&
(nextBlock.content?.richText?.length ?? 0) > 0 &&
nextBlock.content?.richText?.at(0)?.children.at(0).text ===
`{{${streamVariable.name}}}`
)
}
const getNextBlock =
(typebot: TypebotInSession) =>
(blockId: string): Block | undefined => {
const group = typebot.groups.find((group) =>
group.blocks.find(byId(blockId))
)
if (!group) return
const blockIndex = group.blocks.findIndex(byId(blockId))
const nextBlockInGroup = group.blocks.at(blockIndex + 1)
if (nextBlockInGroup) return nextBlockInGroup
const outgoingEdgeId = group.blocks.at(blockIndex)?.outgoingEdgeId
if (!outgoingEdgeId) return
const outgoingEdge = typebot.edges.find(byId(outgoingEdgeId))
if (!outgoingEdge) return
const connectedGroup = typebot.groups.find(byId(outgoingEdge?.to.groupId))
if (!connectedGroup) return
return outgoingEdge.to.blockId
? connectedGroup.blocks.find(
(block) => block.id === outgoingEdge.to.blockId
)
: connectedGroup?.blocks.at(0)
}
const isCredentialsV2 = (credentials: { iv: string }) =>
credentials.iv.length === 24

View File

@ -16,6 +16,7 @@
"@typebot.io/prisma": "workspace:*",
"@typebot.io/schemas": "workspace:*",
"@typebot.io/tsconfig": "workspace:*",
"@typebot.io/variables": "workspace:*",
"@udecode/plate-common": "21.1.5",
"@udecode/plate-serializer-md": "24.4.0",
"ai": "2.2.24",
@ -34,6 +35,9 @@
},
"devDependencies": {
"@types/nodemailer": "6.4.8",
"@types/qs": "6.9.7"
"@types/qs": "6.9.7",
"@typebot.io/forge-schemas": "workspace:*",
"@typebot.io/forge": "workspace:*",
"@typebot.io/forge-repository": "workspace:*"
}
}

View File

@ -5,12 +5,12 @@ import {
ContinueChatResponse,
Typebot,
} from '@typebot.io/schemas'
import { deepParseVariables } from './variables/deepParseVariables'
import { deepParseVariables } from '@typebot.io/variables/deepParseVariables'
import { isEmpty, isNotEmpty } from '@typebot.io/lib/utils'
import {
getVariablesToParseInfoInText,
parseVariables,
} from './variables/parseVariables'
} from '@typebot.io/variables/parseVariables'
import { TDescendant, createPlateEditor } from '@udecode/plate-common'
import {
createDeserializeMdPlugin,

View File

@ -1,5 +1,5 @@
import { SessionState, ContinueChatResponse } from '@typebot.io/schemas'
import { parseVariables } from './variables/parseVariables'
import { parseVariables } from '@typebot.io/variables/parseVariables'
export const parseDynamicTheme = (
state: SessionState | undefined

View File

@ -24,18 +24,20 @@ import { findTypebot } from './queries/findTypebot'
import { findPublicTypebot } from './queries/findPublicTypebot'
import { findResult } from './queries/findResult'
import { startBotFlow } from './startBotFlow'
import { prefillVariables } from './variables/prefillVariables'
import { deepParseVariables } from './variables/deepParseVariables'
import { injectVariablesFromExistingResult } from './variables/injectVariablesFromExistingResult'
import { prefillVariables } from '@typebot.io/variables/prefillVariables'
import { deepParseVariables } from '@typebot.io/variables/deepParseVariables'
import { injectVariablesFromExistingResult } from '@typebot.io/variables/injectVariablesFromExistingResult'
import { getNextGroup } from './getNextGroup'
import { upsertResult } from './queries/upsertResult'
import { continueBotFlow } from './continueBotFlow'
import { parseVariables } from './variables/parseVariables'
import { parseVariables } from '@typebot.io/variables/parseVariables'
import { defaultSettings } from '@typebot.io/schemas/features/typebot/settings/constants'
import { IntegrationBlockType } from '@typebot.io/schemas/features/blocks/integrations/constants'
import { defaultTheme } from '@typebot.io/schemas/features/typebot/theme/constants'
import { VisitedEdge } from '@typebot.io/prisma'
import { env } from '@typebot.io/env'
import { forgedBlocks } from '@typebot.io/forge-schemas'
import { FunctionToExecute } from '@typebot.io/forge'
type StartParams =
| ({
@ -425,9 +427,7 @@ const parseStartClientSideAction = (
)
return
return {
startPropsToInject,
}
return { startPropsToInject }
}
const sanitizeAndParseTheme = (

View File

@ -1,4 +1,8 @@
import { ContinueChatResponse, SessionState } from '@typebot.io/schemas'
import {
ContinueChatResponse,
CustomEmbedBubble,
SessionState,
} from '@typebot.io/schemas'
export type EdgeId = string
@ -11,6 +15,7 @@ export type ExecuteIntegrationResponse = {
outgoingEdgeId: EdgeId | undefined
newSessionState?: SessionState
startTimeShouldBeUpdated?: boolean
customEmbedBubble?: CustomEmbedBubble
} & Pick<ContinueChatResponse, 'clientSideActions' | 'logs'>
export type ParsedReply =

View File

@ -22,7 +22,7 @@
"devDependencies": {
"@types/node": "18.11.18",
"@types/qs": "6.9.7",
"@types/react": "18.0.27",
"@types/react": "18.2.15",
"@types/react-phone-number-input": "3.0.14",
"@types/react-scroll": "1.8.6",
"@types/react-transition-group": "4.4.5",

View File

@ -2,7 +2,7 @@ import React, { useState } from 'react'
import { isDefined } from '@typebot.io/lib'
import { DefaultAvatar } from './DefaultAvatar'
export const Avatar = ({ avatarSrc }: { avatarSrc?: string }): JSX.Element => {
export const Avatar = ({ avatarSrc }: { avatarSrc?: string }) => {
const [currentAvatarSrc] = useState(avatarSrc)
if (currentAvatarSrc === '') return <></>

View File

@ -1,46 +1,42 @@
import React from 'react'
export const DefaultAvatar = (): JSX.Element => {
return (
<figure
className={
'flex justify-center items-center rounded-full text-white w-6 h-6 text-sm relative xs:w-10 xs:h-10 xs:text-xl'
}
data-testid="default-avatar"
export const DefaultAvatar = () => (
<figure
className={
'flex justify-center items-center rounded-full text-white w-6 h-6 text-sm relative xs:w-10 xs:h-10 xs:text-xl'
}
data-testid="default-avatar"
>
<svg
width="75"
height="75"
viewBox="0 0 75 75"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={'absolute top-0 left-0 w-6 h-6 xs:w-full xs:h-full xs:text-xl'}
>
<svg
width="75"
height="75"
viewBox="0 0 75 75"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={
'absolute top-0 left-0 w-6 h-6 xs:w-full xs:h-full xs:text-xl'
}
>
<mask id="mask0" x="0" y="0" mask-type="alpha">
<circle cx="37.5" cy="37.5" r="37.5" fill="#0042DA" />
</mask>
<g mask="url(#mask0)">
<rect x="-30" y="-43" width="131" height="154" fill="#0042DA" />
<rect
x="2.50413"
y="120.333"
width="81.5597"
height="86.4577"
rx="2.5"
transform="rotate(-52.6423 2.50413 120.333)"
stroke="#FED23D"
strokeWidth="5"
/>
<circle cx="76.5" cy="-1.5" r="29" stroke="#FF8E20" strokeWidth="5" />
<path
d="M-49.8224 22L-15.5 -40.7879L18.8224 22H-49.8224Z"
stroke="#F7F8FF"
strokeWidth="5"
/>
</g>
</svg>
</figure>
)
}
<mask id="mask0" x="0" y="0" mask-type="alpha">
<circle cx="37.5" cy="37.5" r="37.5" fill="#0042DA" />
</mask>
<g mask="url(#mask0)">
<rect x="-30" y="-43" width="131" height="154" fill="#0042DA" />
<rect
x="2.50413"
y="120.333"
width="81.5597"
height="86.4577"
rx="2.5"
transform="rotate(-52.6423 2.50413 120.333)"
stroke="#FED23D"
strokeWidth="5"
/>
<circle cx="76.5" cy="-1.5" r="29" stroke="#FF8E20" strokeWidth="5" />
<path
d="M-49.8224 22L-15.5 -40.7879L18.8224 22H-49.8224Z"
stroke="#F7F8FF"
strokeWidth="5"
/>
</g>
</svg>
</figure>
)

View File

@ -4,7 +4,8 @@
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
"@/*": ["src/*"],
"react": ["./node_modules/@types/react"]
}
}
}

View File

@ -263,7 +263,7 @@ pre {
}
.typebot-chat-view {
max-width: 800px;
max-width: 900px;
}
.ping span {

View File

@ -15,6 +15,7 @@ import immutableCss from '../assets/immutable.css'
import { InputBlock } from '@typebot.io/schemas'
import { StartFrom } from '@typebot.io/schemas'
import { defaultTheme } from '@typebot.io/schemas/features/typebot/theme/constants'
import { clsx } from 'clsx'
export type BotProps = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@ -213,10 +214,10 @@ const BotContent = (props: BotContentProps) => {
return (
<div
ref={botContainer}
class={
'relative flex w-full h-full text-base overflow-hidden bg-cover bg-center flex-col items-center typebot-container ' +
class={clsx(
'relative flex w-full h-full text-base overflow-hidden bg-cover bg-center flex-col items-center typebot-container @container',
props.class
}
)}
>
<div class="flex w-full h-full justify-center">
<ConversationContainer

View File

@ -19,7 +19,7 @@ type Props = Pick<ContinueChatResponse, 'messages' | 'input'> & {
streamingMessageId: ChatChunkType['streamingMessageId']
onNewBubbleDisplayed: (blockId: string) => Promise<void>
onScrollToBottom: (top?: number) => void
onSubmit: (input: string) => void
onSubmit: (input?: string) => void
onSkip: () => void
onAllBubblesDisplayed: () => void
}
@ -91,6 +91,7 @@ export const ChatChunk = (props: Props) => {
message={message}
typingEmulation={props.settings.typingEmulation}
onTransitionEnd={displayNextMessage}
onCompleted={props.onSubmit}
/>
)}
</For>

View File

@ -1,11 +1,13 @@
import { AudioBubble } from '@/features/blocks/bubbles/audio'
import { EmbedBubble } from '@/features/blocks/bubbles/embed'
import { CustomEmbedBubble } from '@/features/blocks/bubbles/embed/components/CustomEmbedBubble'
import { ImageBubble } from '@/features/blocks/bubbles/image'
import { TextBubble } from '@/features/blocks/bubbles/textBubble'
import { VideoBubble } from '@/features/blocks/bubbles/video'
import type {
AudioBubbleBlock,
ChatMessage,
CustomEmbedBubble as CustomEmbedBubbleProps,
EmbedBubbleBlock,
ImageBubbleBlock,
Settings,
@ -19,6 +21,7 @@ type Props = {
message: ChatMessage
typingEmulation: Settings['typingEmulation']
onTransitionEnd: (offsetTop?: number) => void
onCompleted: (reply?: string) => void
}
export const HostBubble = (props: Props) => {
@ -26,6 +29,10 @@ export const HostBubble = (props: Props) => {
props.onTransitionEnd(offsetTop)
}
const onCompleted = (reply?: string) => {
props.onCompleted(reply)
}
return (
<Switch>
<Match when={props.message.type === BubbleBlockType.TEXT}>
@ -53,6 +60,13 @@ export const HostBubble = (props: Props) => {
onTransitionEnd={onTransitionEnd}
/>
</Match>
<Match when={props.message.type === 'custom-embed'}>
<CustomEmbedBubble
content={props.message.content as CustomEmbedBubbleProps['content']}
onTransitionEnd={onTransitionEnd}
onCompleted={onCompleted}
/>
</Match>
<Match when={props.message.type === BubbleBlockType.AUDIO}>
<AudioBubble
content={props.message.content as AudioBubbleBlock['content']}

View File

@ -0,0 +1,86 @@
import { TypingBubble } from '@/components'
import { isMobile } from '@/utils/isMobileSignal'
import { createSignal, onCleanup, onMount } from 'solid-js'
import { clsx } from 'clsx'
import { CustomEmbedBubble as CustomEmbedBubbleProps } from '@typebot.io/schemas'
import { executeCode } from '@/features/blocks/logic/script/executeScript'
type Props = {
content: CustomEmbedBubbleProps['content']
onTransitionEnd: (offsetTop?: number) => void
onCompleted: (reply?: string) => void
}
let typingTimeout: NodeJS.Timeout
export const showAnimationDuration = 400
export const CustomEmbedBubble = (props: Props) => {
let ref: HTMLDivElement | undefined
const [isTyping, setIsTyping] = createSignal(true)
let containerRef: HTMLDivElement | undefined
onMount(() => {
console.log(
props.content.initFunction.content,
props.content.initFunction.args
)
executeCode({
args: {
...props.content.initFunction.args,
typebotElement: containerRef,
},
content: props.content.initFunction.content,
})
if (props.content.waitForEventFunction)
executeCode({
args: {
...props.content.waitForEventFunction.args,
continueFlow: props.onCompleted,
},
content: props.content.waitForEventFunction.content,
})
typingTimeout = setTimeout(() => {
setIsTyping(false)
setTimeout(
() => props.onTransitionEnd(ref?.offsetTop),
showAnimationDuration
)
}, 2000)
})
onCleanup(() => {
if (typingTimeout) clearTimeout(typingTimeout)
})
return (
<div class="flex flex-col w-full animate-fade-in" ref={ref}>
<div class="flex w-full items-center">
<div class="flex relative z-10 items-start typebot-host-bubble w-full max-w-full">
<div
class="flex items-center absolute px-4 py-2 bubble-typing z-10 "
style={{
width: isTyping() ? '64px' : '100%',
height: isTyping() ? '32px' : '100%',
}}
>
{isTyping() && <TypingBubble />}
</div>
<div
class={clsx(
'p-2 z-20 text-fade-in w-full',
isTyping() ? 'opacity-0' : 'opacity-100'
)}
style={{
height: isTyping() ? (isMobile() ? '32px' : '36px') : undefined,
}}
>
<div class="w-full h-full overflow-scroll" ref={containerRef} />
</div>
</div>
</div>
</div>
)
}

View File

@ -9,15 +9,16 @@ const maxRetryAttempts = 3
export const streamChat =
(context: ClientSideActionContext & { retryAttempt?: number }) =>
async (
messages: {
async ({
messages,
onMessageStream,
}: {
messages?: {
content?: string | undefined
role?: 'system' | 'user' | 'assistant' | undefined
}[],
{
onMessageStream,
}: { onMessageStream?: (props: { id: string; message: string }) => void }
): Promise<{ message?: string; error?: object }> => {
}[]
onMessageStream?: (props: { id: string; message: string }) => void
}): Promise<{ message?: string; error?: object }> => {
try {
abortController = new AbortController()
@ -51,7 +52,7 @@ export const streamChat =
return streamChat({
...context,
retryAttempt: (context.retryAttempt ?? 0) + 1,
})(messages, { onMessageStream })
})({ messages, onMessageStream })
}
return {
error: (await res.json()) || 'Failed to fetch the chat response.',

View File

@ -21,3 +21,18 @@ const parseContent = (content: string) => {
.replace(/<\/script>/g, '')
return contentWithoutScriptTags
}
export const executeCode = async ({
args,
content,
}: {
content: string
args: Record<string, unknown>
}) => {
try {
const func = AsyncFunction(...Object.keys(args), content)
await func(...Object.keys(args).map((key) => args[key]))
} catch (err) {
console.warn('Script threw an error:', err)
}
}

View File

@ -2,7 +2,10 @@ import { executeChatwoot } from '@/features/blocks/integrations/chatwoot'
import { executeGoogleAnalyticsBlock } from '@/features/blocks/integrations/googleAnalytics/utils/executeGoogleAnalytics'
import { streamChat } from '@/features/blocks/integrations/openai/streamChat'
import { executeRedirect } from '@/features/blocks/logic/redirect'
import { executeScript } from '@/features/blocks/logic/script/executeScript'
import {
executeScript,
executeCode,
} from '@/features/blocks/logic/script/executeScript'
import { executeSetVariable } from '@/features/blocks/logic/setVariable/executeSetVariable'
import { executeWait } from '@/features/blocks/logic/wait/utils/executeWait'
import { executeWebhook } from '@/features/blocks/integrations/webhook/executeWebhook'
@ -47,20 +50,24 @@ export const executeClientSideAction = async ({
if ('setVariable' in clientSideAction) {
return executeSetVariable(clientSideAction.setVariable.scriptToExecute)
}
if ('streamOpenAiChatCompletion' in clientSideAction) {
const { error, message } = await streamChat(context)(
clientSideAction.streamOpenAiChatCompletion.messages,
{
onMessageStream,
}
)
if (
'streamOpenAiChatCompletion' in clientSideAction ||
'stream' in clientSideAction
) {
const { error, message } = await streamChat(context)({
messages:
'streamOpenAiChatCompletion' in clientSideAction
? clientSideAction.streamOpenAiChatCompletion?.messages
: undefined,
onMessageStream,
})
if (error)
return {
replyToSend: undefined,
logs: [
{
status: 'error',
description: 'OpenAI returned an error',
description: 'Message streaming returned an error',
details: JSON.stringify(error, null, 2),
},
],
@ -77,4 +84,7 @@ export const executeClientSideAction = async ({
if ('pixel' in clientSideAction) {
return executePixel(clientSideAction.pixel)
}
if ('codeToExecute' in clientSideAction) {
return executeCode(clientSideAction.codeToExecute)
}
}

View File

@ -38,7 +38,7 @@
"typescript": "5.3.2"
},
"peerDependencies": {
"next": "12.x || 13.x",
"next": "12.x || 13.x || 14.x",
"react": "18.x"
}
}

View File

@ -0,0 +1,126 @@
import { createAction, option } from '@typebot.io/forge'
import { baseOptions } from '../baseOptions'
import { defaultBaseUrl } from '../constants'
export const bookEvent = createAction({
name: 'Book event',
baseOptions,
options: option.object({
link: option.string.layout({
label: 'Event link',
placeholder: 'https://cal.com/...',
}),
layout: option
.enum(['Month', 'Weekly', 'Columns'])
.layout({ label: 'Layout:', defaultValue: 'Month', direction: 'row' }),
name: option.string.layout({
accordion: 'Prefill information',
label: 'Name',
placeholder: 'John Doe',
}),
email: option.string.layout({
accordion: 'Prefill information',
label: 'Email',
placeholder: 'johndoe@gmail.com',
}),
saveBookedDateInVariableId: option.string.layout({
label: 'Save booked date',
input: 'variableDropdown',
}),
}),
getSetVariableIds: ({ saveBookedDateInVariableId }) =>
saveBookedDateInVariableId ? [saveBookedDateInVariableId] : [],
run: {
web: {
displayEmbedBubble: {
waitForEvent: {
getSaveVariableId: ({ saveBookedDateInVariableId }) =>
saveBookedDateInVariableId,
parseFunction: () => {
return {
args: {},
content: `Cal("on", {
action: "bookingSuccessful",
callback: (e) => {
continueFlow(e.detail.data.date)
}
})`,
}
},
},
parseInitFunction: ({ options }) => {
if (!options.link) throw new Error('Missing link')
const baseUrl = options.baseUrl ?? defaultBaseUrl
const link = options.link?.startsWith('http')
? options.link.replace(/http.+:\/\/[^\/]+\//, '')
: options.link
return {
args: {
baseUrl,
link: link ?? '',
name: options.name ?? null,
email: options.email ?? null,
layout: parseLayoutAttr(options.layout),
},
content: `(function (C, A, L) {
let p = function (a, ar) {
a.q.push(ar);
};
let d = C.document;
C.Cal =
C.Cal ||
function () {
let cal = C.Cal;
let ar = arguments;
if (!cal.loaded) {
cal.ns = {};
cal.q = cal.q || [];
d.head.appendChild(d.createElement("script")).src = A;
cal.loaded = true;
}
if (ar[0] === L) {
const api = function () {
p(api, arguments);
};
const namespace = ar[1];
api.q = api.q || [];
typeof namespace === "string"
? (cal.ns[namespace] = api) && p(api, ar)
: p(cal, ar);
return;
}
p(cal, ar);
};
})(window, baseUrl + "/embed/embed.js", "init");
Cal("init", { origin: baseUrl });
Cal("inline", {
elementOrSelector: typebotElement,
calLink: link,
layout,
config: {
name: name ?? undefined,
email: email ?? undefined,
}
});
Cal("ui", {"hideEventTypeDetails":false,layout});`,
}
},
},
},
},
})
const parseLayoutAttr = (
layout?: 'Month' | 'Weekly' | 'Columns'
): 'month_view' | 'week_view' | 'column_view' => {
switch (layout) {
case 'Weekly':
return 'week_view'
case 'Columns':
return 'column_view'
default:
return 'month_view'
}
}

View File

@ -0,0 +1,11 @@
import { option } from '@typebot.io/forge'
import { defaultBaseUrl } from './constants'
export const baseOptions = option.object({
baseUrl: option.string.layout({
label: 'Base origin',
placeholder: 'https://cal.com',
defaultValue: defaultBaseUrl,
accordion: 'Customize host',
}),
})

View File

@ -0,0 +1 @@
export const defaultBaseUrl = 'https://app.cal.com'

View File

@ -0,0 +1,13 @@
import { createBlock } from '@typebot.io/forge'
import { CalComLogo } from './logo'
import { bookEvent } from './actions/bookEvent'
import { baseOptions } from './baseOptions'
export const calCom = createBlock({
id: 'cal-com',
name: 'Cal.com',
tags: ['calendar', 'scheduling', 'meetings'],
LightLogo: CalComLogo,
options: baseOptions,
actions: [bookEvent],
})

View File

@ -0,0 +1,16 @@
import React from 'react'
export const CalComLogo = (props: React.SVGProps<SVGSVGElement>) => (
<svg viewBox="0 0 31 31" {...props}>
<rect width="31" height="31" rx="3" fill="#2B2F33" />
<path
d="M9.40968 21.7097C5.75369 21.7097 3 18.9373 3 15.5145C3 12.0804 5.61308 9.28516 9.40968 9.28516C11.4251 9.28516 12.8195 9.87843 13.9093 11.2361L12.1516 12.6395C11.4134 11.8864 10.5228 11.5099 9.40968 11.5099C6.9372 11.5099 5.57792 13.324 5.57792 15.5145C5.57792 17.7051 7.06609 19.4849 9.40968 19.4849C10.5111 19.4849 11.4486 19.1085 12.1868 18.3555L13.921 19.8158C12.8782 21.1164 11.4486 21.7097 9.40968 21.7097Z"
fill="white"
/>
<path
d="M21.4917 12.595H23.8586V21.4941H21.4917V20.1935C20.9995 21.1176 20.1792 21.7337 18.6091 21.7337C16.1015 21.7337 14.0977 19.6459 14.0977 17.0788C14.0977 14.5117 16.1015 12.4238 18.6091 12.4238C20.1676 12.4238 20.9995 13.0399 21.4917 13.9641V12.595ZM21.5619 17.0788C21.5619 15.6869 20.5659 14.5345 18.9958 14.5345C17.4841 14.5345 16.4998 15.6982 16.4998 17.0788C16.4998 18.425 17.4841 19.623 18.9958 19.623C20.5542 19.623 21.5619 18.4593 21.5619 17.0788Z"
fill="white"
/>
<path d="M25.5332 9H27.9002V21.4816H25.5332V9Z" fill="white" />
</svg>
)

View File

@ -0,0 +1,15 @@
{
"name": "@typebot.io/cal-com-block",
"version": "1.0.0",
"description": "",
"main": "index.ts",
"keywords": [],
"license": "ISC",
"devDependencies": {
"@typebot.io/forge": "workspace:*",
"@typebot.io/tsconfig": "workspace:*",
"@types/react": "18.2.15",
"typescript": "5.3.2",
"@typebot.io/lib": "workspace:*"
}
}

View File

@ -0,0 +1,10 @@
{
"extends": "@typebot.io/tsconfig/base.json",
"include": ["**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"],
"compilerOptions": {
"lib": ["ESNext", "DOM"],
"noEmit": true,
"jsx": "react"
}
}

View File

@ -0,0 +1,63 @@
import { createAction, option } from '@typebot.io/forge'
import { isDefined, isEmpty } from '@typebot.io/lib'
import { got } from 'got'
import { apiBaseUrl } from '../constants'
import { auth } from '../auth'
import { ChatNodeResponse } from '../types'
export const sendMessage = createAction({
auth,
name: 'Send Message',
options: option.object({
botId: option.string.layout({
label: 'Bot ID',
placeholder: '68c052c5c3680f63',
moreInfoTooltip:
'The bot_id you want to ask question to. You can find it at the end of your ChatBot URl in your dashboard',
}),
threadId: option.string.layout({
label: 'Thread ID',
moreInfoTooltip:
'Used to remember the conversation with the user. If empty, a new thread is created.',
}),
message: option.string.layout({
label: 'Message',
placeholder: 'Hi, what can I do with ChatNode',
input: 'textarea',
}),
responseMapping: option.saveResponseArray(['Message', 'Thread ID']).layout({
accordion: 'Save response',
}),
}),
getSetVariableIds: ({ responseMapping }) =>
responseMapping?.map((r) => r.variableId).filter(isDefined) ?? [],
run: {
server: async ({
credentials: { apiKey },
options: { botId, message, responseMapping, threadId },
variables,
}) => {
const res: ChatNodeResponse = await got
.post(apiBaseUrl + botId, {
headers: {
Authorization: `Bearer ${apiKey}`,
},
json: {
message,
chat_session_id: isEmpty(threadId) ? undefined : threadId,
},
})
.json()
responseMapping?.forEach((mapping) => {
if (!mapping.variableId || !mapping.item) return
if (mapping.item === 'Message')
variables.set(mapping.variableId, res.message)
if (mapping.item === 'Thread ID')
variables.set(mapping.variableId, res.chat_session_id)
})
},
},
})

View File

@ -0,0 +1,15 @@
import { option, AuthDefinition } from '@typebot.io/forge'
export const auth = {
type: 'encryptedCredentials',
name: 'ChatNode account',
schema: option.object({
apiKey: option.string.layout({
label: 'API key',
isRequired: true,
helperText:
'You can generate an API key [here](https://www.chatnode.ai/account/settings).',
input: 'password',
}),
}),
} satisfies AuthDefinition

View File

@ -0,0 +1 @@
export const apiBaseUrl = 'https://api.public.chatnode.ai/v1/'

View File

@ -0,0 +1,13 @@
import { createBlock } from '@typebot.io/forge'
import { ChatNodeLogo } from './logo'
import { auth } from './auth'
import { sendMessage } from './actions/sendMessage'
export const chatNode = createBlock({
id: 'chat-node',
name: 'ChatNode',
tags: ['ai', 'openai', 'document', 'url'],
LightLogo: ChatNodeLogo,
auth,
actions: [sendMessage],
})

View File

@ -0,0 +1,10 @@
import React from 'react'
export const ChatNodeLogo = (props: React.SVGProps<SVGSVGElement>) => (
<svg viewBox="0 0 25 25" xmlns="http://www.w3.org/2000/svg" {...props}>
<path
d="M17.5659 9.95206C17.5659 10.6394 17.0087 11.1966 16.3214 11.1966H15.9065C15.2192 11.1966 14.6619 10.6394 14.6619 9.95206L14.6619 2.36802C14.6619 2.03123 14.459 1.726 14.1432 1.60895C12.3471 0.943268 10.4043 0.57959 8.37666 0.57959H2.00814C1.43534 0.57959 0.970996 1.04394 0.970996 1.61674C0.970996 3.63734 0.93996 5.66077 0.964561 7.68162C0.968691 8.02086 1.2456 8.29263 1.58487 8.29263L7.60934 8.29263C8.2967 8.29263 8.85392 8.84985 8.85392 9.53721V9.95206C8.85392 10.6394 8.2967 11.1966 7.60934 11.1966L2.03766 11.1966C1.64592 11.1966 1.35104 11.5545 1.44396 11.9351C1.82599 13.4998 2.42987 14.9774 3.22037 16.333C3.46819 16.7579 3.93069 17.0047 4.42262 17.0047H16.3214C17.0087 17.0047 17.5659 17.5619 17.5659 18.2492V18.6641C17.5659 19.3514 17.0087 19.9087 16.3214 19.9087H7.04882C6.67598 19.9087 6.4984 20.3561 6.78168 20.5985C9.68205 23.0805 13.4486 24.5796 17.5653 24.5796H24.971V17.1739C24.971 11.6855 22.3066 6.81948 18.2007 3.79866C17.9338 3.60233 17.5659 3.7977 17.5659 4.12899L17.5659 9.95206Z"
fill="#818CF8"
></path>
</svg>
)

View File

@ -0,0 +1,16 @@
{
"name": "@typebot.io/chat-node-block",
"version": "1.0.0",
"description": "",
"main": "index.ts",
"keywords": [],
"license": "ISC",
"devDependencies": {
"@typebot.io/forge": "workspace:*",
"@typebot.io/lib": "workspace:*",
"@typebot.io/tsconfig": "workspace:*",
"@types/react": "18.2.15",
"typescript": "5.3.2",
"got": "12.6.0"
}
}

View File

@ -0,0 +1,10 @@
{
"extends": "@typebot.io/tsconfig/base.json",
"include": ["**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"],
"compilerOptions": {
"lib": ["ESNext", "DOM"],
"noEmit": true,
"jsx": "react"
}
}

View File

@ -0,0 +1,4 @@
export type ChatNodeResponse = {
message: string
chat_session_id: string
}

View File

@ -0,0 +1,180 @@
import { option, createAction } from '@typebot.io/forge'
import OpenAI, { ClientOptions } from 'openai'
import { defaultOpenAIOptions } from '../constants'
import { OpenAIStream } from 'ai'
import { parseChatCompletionMessages } from '../helpers/parseChatCompletionMessages'
import { isDefined } from '@typebot.io/lib'
import { auth } from '../auth'
import { baseOptions } from '../baseOptions'
const nativeMessageContentSchema = {
content: option.string.layout({ input: 'textarea', placeholder: 'Content' }),
}
const systemMessageItemSchema = option
.object({
role: option.literal('system'),
})
.extend(nativeMessageContentSchema)
const userMessageItemSchema = option
.object({
role: option.literal('user'),
})
.extend(nativeMessageContentSchema)
const assistantMessageItemSchema = option
.object({
role: option.literal('assistant'),
})
.extend(nativeMessageContentSchema)
const dialogueMessageItemSchema = option.object({
role: option.literal('Dialogue'),
dialogueVariableId: option.string.layout({
input: 'variableDropdown',
placeholder: 'Dialogue variable',
}),
startsBy: option.enum(['user', 'assistant']).layout({
label: 'starts by',
direction: 'row',
defaultValue: 'user',
}),
})
export const options = option.object({
model: option.string.layout({
placeholder: 'Select a model',
defaultValue: defaultOpenAIOptions.model,
fetcher: 'fetchModels',
}),
messages: option
.array(
option.discriminatedUnion('role', [
systemMessageItemSchema,
userMessageItemSchema,
assistantMessageItemSchema,
dialogueMessageItemSchema,
])
)
.layout({ accordion: 'Messages', itemLabel: 'message', isOrdered: true }),
temperature: option.number.layout({
accordion: 'Advanced settings',
label: 'Temperature',
direction: 'row',
defaultValue: defaultOpenAIOptions.temperature,
}),
responseMapping: option
.saveResponseArray(['Message content', 'Total tokens'])
.layout({
accordion: 'Save response',
}),
})
export const createChatCompletion = createAction({
name: 'Create chat completion',
auth,
baseOptions,
getSetVariableIds: (options) =>
options.responseMapping?.map((res) => res.variableId).filter(isDefined) ??
[],
fetchers: [
{
id: 'fetchModels',
dependencies: ['baseUrl', 'apiVersion'],
fetch: async ({ credentials, options }) => {
const baseUrl = options?.baseUrl ?? defaultOpenAIOptions.baseUrl
const config = {
apiKey: credentials.apiKey,
baseURL: baseUrl ?? defaultOpenAIOptions.baseUrl,
defaultHeaders: {
'api-key': credentials.apiKey,
},
defaultQuery: options?.apiVersion
? {
'api-version': options.apiVersion,
}
: undefined,
} satisfies ClientOptions
const openai = new OpenAI(config)
const models = await openai.models.list()
return (
models.data
.filter((model) => model.id.includes('gpt'))
.sort((a, b) => b.created - a.created)
.map((model) => model.id) ?? []
)
},
},
],
run: {
server: async ({ credentials: { apiKey }, options, variables }) => {
const config = {
apiKey,
baseURL: options.baseUrl,
defaultHeaders: {
'api-key': apiKey,
},
defaultQuery: options.apiVersion
? {
'api-version': options.apiVersion,
}
: undefined,
} satisfies ClientOptions
const openai = new OpenAI(config)
const response = await openai.chat.completions.create({
model: options.model ?? defaultOpenAIOptions.model,
temperature: options.temperature
? Number(options.temperature)
: undefined,
messages: parseChatCompletionMessages({ options, variables }),
})
options.responseMapping?.forEach((mapping) => {
if (!mapping.variableId) return
if (!mapping.item || mapping.item === 'Message content')
variables.set(mapping.variableId, response.choices[0].message.content)
if (mapping.item === 'Total tokens')
variables.set(mapping.variableId, response.usage?.total_tokens)
})
},
stream: {
getStreamVariableId: (options) =>
options.responseMapping?.find((res) => res.item === 'Message content')
?.variableId,
run: async ({ credentials: { apiKey }, options, variables }) => {
const config = {
apiKey,
baseURL: options.baseUrl,
defaultHeaders: {
'api-key': apiKey,
},
defaultQuery: options.apiVersion
? {
'api-version': options.apiVersion,
}
: undefined,
} satisfies ClientOptions
const openai = new OpenAI(config)
const response = await openai.chat.completions.create({
model: options.model ?? defaultOpenAIOptions.model,
temperature: options.temperature
? Number(options.temperature)
: undefined,
stream: true,
messages: parseChatCompletionMessages({ options, variables }),
})
return OpenAIStream(response)
},
},
},
options,
})

View File

@ -0,0 +1,105 @@
import { option, createAction } from '@typebot.io/forge'
import { defaultOpenAIOptions, openAIVoices } from '../constants'
import OpenAI, { ClientOptions } from 'openai'
import { isNotEmpty, createId } from '@typebot.io/lib'
import { uploadFileToBucket } from '@typebot.io/lib/s3/uploadFileToBucket'
import { auth } from '../auth'
import { baseOptions } from '../baseOptions'
export const createSpeech = createAction({
name: 'Create speech',
auth,
baseOptions,
options: option.object({
model: option.string.layout({
fetcher: 'fetchSpeechModels',
defaultValue: 'tts-1',
placeholder: 'Select a model',
}),
input: option.string.layout({
label: 'Input',
input: 'textarea',
}),
voice: option.enum(openAIVoices).layout({
label: 'Voice',
placeholder: 'Select a voice',
}),
saveUrlInVariableId: option.string.layout({
input: 'variableDropdown',
label: 'Save URL in variable',
}),
}),
getSetVariableIds: (options) =>
options.saveUrlInVariableId ? [options.saveUrlInVariableId] : [],
fetchers: [
{
id: 'fetchSpeechModels',
dependencies: ['baseUrl', 'apiVersion'],
fetch: async ({ credentials, options }) => {
const baseUrl = options?.baseUrl ?? defaultOpenAIOptions.baseUrl
const config = {
apiKey: credentials.apiKey,
baseURL: baseUrl ?? defaultOpenAIOptions.baseUrl,
defaultHeaders: {
'api-key': credentials.apiKey,
},
defaultQuery: options?.apiVersion
? {
'api-version': options.apiVersion,
}
: undefined,
} satisfies ClientOptions
const openai = new OpenAI(config)
const models = await openai.models.list()
return (
models.data
.filter((model) => model.id.includes('tts'))
.sort((a, b) => b.created - a.created)
.map((model) => model.id) ?? []
)
},
},
],
run: {
server: async ({ credentials: { apiKey }, options, variables, logs }) => {
if (!options.input) return logs.add('Create speech input is empty')
if (!options.voice) return logs.add('Create speech voice is empty')
if (!options.saveUrlInVariableId)
return logs.add('Create speech save variable is empty')
const config = {
apiKey,
baseURL: options.baseUrl ?? defaultOpenAIOptions.baseUrl,
defaultHeaders: {
'api-key': apiKey,
},
defaultQuery: isNotEmpty(options.apiVersion)
? {
'api-version': options.apiVersion,
}
: undefined,
} satisfies ClientOptions
const openai = new OpenAI(config)
const model = options.model ?? defaultOpenAIOptions.voiceModel
const rawAudio = (await openai.audio.speech.create({
input: options.input,
voice: options.voice,
model,
})) as any
const url = await uploadFileToBucket({
file: Buffer.from((await rawAudio.arrayBuffer()) as ArrayBuffer),
key: `tmp/openai/audio/${createId() + createId()}.mp3`,
mimeType: 'audio/mpeg',
})
variables.set(options.saveUrlInVariableId, url)
},
},
})

View File

@ -0,0 +1,16 @@
import { createAuth, option } from '@typebot.io/forge'
export const auth = createAuth({
type: 'encryptedCredentials',
name: 'OpenAI account',
schema: option.object({
apiKey: option.string.layout({
isRequired: true,
label: 'API key',
placeholder: 'sk-...',
helperText:
'You can generate an API key [here](https://platform.openai.com/account/api-keys)',
withVariableButton: false,
}),
}),
})

View File

@ -0,0 +1,14 @@
import { option } from '@typebot.io/forge'
import { defaultOpenAIOptions } from './constants'
export const baseOptions = option.object({
baseUrl: option.string.layout({
accordion: 'Customize provider',
label: 'Base URL',
defaultValue: defaultOpenAIOptions.baseUrl,
}),
apiVersion: option.string.layout({
accordion: 'Customize provider',
label: 'API version',
}),
})

View File

@ -0,0 +1,15 @@
export const openAIVoices = [
'alloy',
'echo',
'fable',
'onyx',
'nova',
'shimmer',
] as const
export const defaultOpenAIOptions = {
baseUrl: 'https://api.openai.com/v1',
model: 'gpt-3.5-turbo',
voiceModel: 'tts-1',
temperature: 1,
} as const

View File

@ -0,0 +1,54 @@
import type { OpenAI } from 'openai'
import { options as createChatCompletionOption } from '../actions/createChatCompletion'
import { ReadOnlyVariableStore } from '@typebot.io/forge'
import { isNotEmpty } from '@typebot.io/lib'
import { z } from '@typebot.io/forge/zod'
export const parseChatCompletionMessages = ({
options: { messages },
variables,
}: {
options: Pick<z.infer<typeof createChatCompletionOption>, 'messages'>
variables: ReadOnlyVariableStore
}): OpenAI.Chat.ChatCompletionMessageParam[] => {
const parsedMessages = messages
?.flatMap((message) => {
if (!message.role) return
if (message.role === 'Dialogue') {
if (!message.dialogueVariableId) return
const dialogue = variables.get(message.dialogueVariableId) ?? []
const dialogueArr = Array.isArray(dialogue) ? dialogue : [dialogue]
return dialogueArr.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,
}
}
)
}
if (!message.content) return
return {
role: message.role,
content: variables.parse(message.content),
} satisfies OpenAI.Chat.ChatCompletionMessageParam
})
.filter(
(message) =>
isNotEmpty(message?.role) && isNotEmpty(message?.content?.toString())
) as OpenAI.Chat.ChatCompletionMessageParam[]
return parsedMessages
}

View File

@ -0,0 +1,17 @@
import { OpenAILightLogo, OpenAIDarkLogo } from './logo'
import { createChatCompletion } from './actions/createChatCompletion'
import { createSpeech } from './actions/createSpeech'
import { createBlock } from '@typebot.io/forge'
import { auth } from './auth'
import { baseOptions } from './baseOptions'
export const openAIBlock = createBlock({
id: 'openai' as const,
name: 'OpenAI',
tags: ['openai'],
LightLogo: OpenAILightLogo,
DarkLogo: OpenAIDarkLogo,
auth,
options: baseOptions,
actions: [createChatCompletion, createSpeech],
})

View File

@ -0,0 +1,13 @@
import React from 'react'
export const OpenAILightLogo = (props: React.SVGProps<SVGSVGElement>) => (
<svg viewBox="0 0 24 24" fill="#000100" {...props}>
<path d="M22.282 9.821a5.985 5.985 0 0 0-.516-4.91 6.046 6.046 0 0 0-6.51-2.9A6.065 6.065 0 0 0 4.981 4.18a5.985 5.985 0 0 0-3.998 2.9 6.046 6.046 0 0 0 .743 7.097 5.98 5.98 0 0 0 .51 4.911 6.051 6.051 0 0 0 6.515 2.9A5.985 5.985 0 0 0 13.26 24a6.056 6.056 0 0 0 5.772-4.206 5.99 5.99 0 0 0 3.997-2.9 6.056 6.056 0 0 0-.747-7.073zM13.26 22.43a4.476 4.476 0 0 1-2.876-1.04l.141-.081 4.779-2.758a.795.795 0 0 0 .392-.681v-6.737l2.02 1.168a.071.071 0 0 1 .038.052v5.583a4.504 4.504 0 0 1-4.494 4.494zM3.6 18.304a4.47 4.47 0 0 1-.535-3.014l.142.085 4.783 2.759a.771.771 0 0 0 .78 0l5.843-3.369v2.332a.08.08 0 0 1-.033.062L9.74 19.95a4.5 4.5 0 0 1-6.14-1.646zM2.34 7.896a4.485 4.485 0 0 1 2.366-1.973V11.6a.766.766 0 0 0 .388.676l5.815 3.355-2.02 1.168a.076.076 0 0 1-.071 0l-4.83-2.786A4.504 4.504 0 0 1 2.34 7.872zm16.597 3.855l-5.833-3.387L15.119 7.2a.076.076 0 0 1 .071 0l4.83 2.791a4.494 4.494 0 0 1-.676 8.105v-5.678a.79.79 0 0 0-.407-.667zm2.01-3.023l-.141-.085-4.774-2.782a.776.776 0 0 0-.785 0L9.409 9.23V6.897a.066.066 0 0 1 .028-.061l4.83-2.787a4.5 4.5 0 0 1 6.68 4.66zm-12.64 4.135l-2.02-1.164a.08.08 0 0 1-.038-.057V6.075a4.5 4.5 0 0 1 7.375-3.453l-.142.08L8.704 5.46a.795.795 0 0 0-.393.681zm1.097-2.365l2.602-1.5 2.607 1.5v2.999l-2.597 1.5-2.607-1.5z" />
</svg>
)
export const OpenAIDarkLogo = (props: React.SVGProps<SVGSVGElement>) => (
<svg viewBox="0 0 24 24" fill="#FEFFFE" {...props}>
<path d="M22.282 9.821a5.985 5.985 0 0 0-.516-4.91 6.046 6.046 0 0 0-6.51-2.9A6.065 6.065 0 0 0 4.981 4.18a5.985 5.985 0 0 0-3.998 2.9 6.046 6.046 0 0 0 .743 7.097 5.98 5.98 0 0 0 .51 4.911 6.051 6.051 0 0 0 6.515 2.9A5.985 5.985 0 0 0 13.26 24a6.056 6.056 0 0 0 5.772-4.206 5.99 5.99 0 0 0 3.997-2.9 6.056 6.056 0 0 0-.747-7.073zM13.26 22.43a4.476 4.476 0 0 1-2.876-1.04l.141-.081 4.779-2.758a.795.795 0 0 0 .392-.681v-6.737l2.02 1.168a.071.071 0 0 1 .038.052v5.583a4.504 4.504 0 0 1-4.494 4.494zM3.6 18.304a4.47 4.47 0 0 1-.535-3.014l.142.085 4.783 2.759a.771.771 0 0 0 .78 0l5.843-3.369v2.332a.08.08 0 0 1-.033.062L9.74 19.95a4.5 4.5 0 0 1-6.14-1.646zM2.34 7.896a4.485 4.485 0 0 1 2.366-1.973V11.6a.766.766 0 0 0 .388.676l5.815 3.355-2.02 1.168a.076.076 0 0 1-.071 0l-4.83-2.786A4.504 4.504 0 0 1 2.34 7.872zm16.597 3.855l-5.833-3.387L15.119 7.2a.076.076 0 0 1 .071 0l4.83 2.791a4.494 4.494 0 0 1-.676 8.105v-5.678a.79.79 0 0 0-.407-.667zm2.01-3.023l-.141-.085-4.774-2.782a.776.776 0 0 0-.785 0L9.409 9.23V6.897a.066.066 0 0 1 .028-.061l4.83-2.787a4.5 4.5 0 0 1 6.68 4.66zm-12.64 4.135l-2.02-1.164a.08.08 0 0 1-.038-.057V6.075a4.5 4.5 0 0 1 7.375-3.453l-.142.08L8.704 5.46a.795.795 0 0 0-.393.681zm1.097-2.365l2.602-1.5 2.607 1.5v2.999l-2.597 1.5-2.607-1.5z" />
</svg>
)

View File

@ -0,0 +1,20 @@
{
"name": "@typebot.io/openai-block",
"version": "1.0.0",
"description": "",
"main": "index.ts",
"keywords": [],
"author": "Baptiste Arnaud",
"license": "ISC",
"dependencies": {
"ai": "2.2.24",
"openai": "4.19.0"
},
"devDependencies": {
"@typebot.io/forge": "workspace:*",
"@typebot.io/tsconfig": "workspace:*",
"@types/react": "18.2.15",
"typescript": "5.3.2",
"@typebot.io/lib": "workspace:*"
}
}

View File

@ -0,0 +1,10 @@
{
"extends": "@typebot.io/tsconfig/base.json",
"include": ["**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"],
"compilerOptions": {
"lib": ["ESNext", "DOM"],
"noEmit": true,
"jsx": "react"
}
}

View File

@ -0,0 +1,110 @@
import { createAction, option } from '@typebot.io/forge'
import { isDefined } from '@typebot.io/lib'
import { ZemanticAiResponse } from '../types'
import { got } from 'got'
import { apiBaseUrl } from '../constants'
import { auth } from '../auth'
import { baseOptions } from '../baseOptions'
export const searchDocuments = createAction({
baseOptions,
auth,
name: 'Search documents',
options: option.object({
query: option.string.layout({
label: 'Query',
placeholder: 'Content',
moreInfoTooltip:
'The question you want to ask or search against the documents in the project.',
}),
maxResults: option.number.layout({
label: 'Max results',
placeholder: 'i.e. 3',
defaultValue: 3,
moreInfoTooltip:
'The maximum number of document chunk results to return from your search.',
}),
systemPrompt: option.string.layout({
accordion: 'Advanced settings',
label: 'System prompt',
moreInfoTooltip:
'System prompt to send to the summarization LLM. This is prepended to the prompt and helps guide system behavior.',
input: 'textarea',
}),
prompt: option.string.layout({
accordion: 'Advanced settings',
label: 'Prompt',
moreInfoTooltip: 'Prompt to send to the summarization LLM.',
input: 'textarea',
}),
responseMapping: option
.saveResponseArray([
'Summary',
'Document IDs',
'Texts',
'Scores',
] as const)
.layout({
accordion: 'Save response',
}),
}),
getSetVariableIds: ({ responseMapping }) =>
responseMapping?.map((r) => r.variableId).filter(isDefined) ?? [],
run: {
server: async ({
credentials: { apiKey },
options: {
maxResults,
projectId,
prompt,
query,
responseMapping,
systemPrompt,
},
variables,
}) => {
const res: ZemanticAiResponse = await got
.post(apiBaseUrl, {
headers: {
Authorization: `Bearer ${apiKey}`,
},
json: {
projectId,
query,
maxResults,
summarize: true,
summaryOptions: {
system_prompt: systemPrompt,
prompt: prompt,
},
},
})
.json()
responseMapping?.forEach((mapping) => {
if (!mapping.variableId || !mapping.item) return
if (mapping.item === 'Document IDs')
variables.set(
mapping.variableId,
res.results.map((r) => r.documentId)
)
if (mapping.item === 'Texts')
variables.set(
mapping.variableId,
res.results.map((r) => r.text)
)
if (mapping.item === 'Scores')
variables.set(
mapping.variableId,
res.results.map((r) => r.score)
)
if (mapping.item === 'Summary')
variables.set(mapping.variableId, res.summary)
})
},
},
})

View File

@ -0,0 +1,15 @@
import { AuthDefinition, option } from '@typebot.io/forge'
export const auth = {
type: 'encryptedCredentials',
name: 'Zemantic AI account',
schema: option.object({
apiKey: option.string.layout({
label: 'API key',
isRequired: true,
placeholder: 'ze...',
helperText:
'You can generate an API key [here](https://zemantic.ai/dashboard/settings).',
}),
}),
} satisfies AuthDefinition

View File

@ -0,0 +1,8 @@
import { option } from '@typebot.io/forge'
export const baseOptions = option.object({
projectId: option.string.layout({
placeholder: 'Select a project',
fetcher: 'fetchProjects',
}),
})

View File

@ -0,0 +1 @@
export const apiBaseUrl = 'https://api.zemantic.ai/v1/search-documents'

View File

@ -0,0 +1,43 @@
import { createBlock } from '@typebot.io/forge'
import { ZemanticAiLogo } from './logo'
import { got } from 'got'
import { searchDocuments } from './actions/searchDocuments'
import { auth } from './auth'
import { baseOptions } from './baseOptions'
export const zemanticAi = createBlock({
id: 'zemantic-ai',
name: 'Zemantic AI',
tags: [],
LightLogo: ZemanticAiLogo,
auth,
options: baseOptions,
fetchers: [
{
id: 'fetchProjects',
dependencies: [],
fetch: async ({ credentials: { apiKey } }) => {
const url = 'https://api.zemantic.ai/v1/projects'
const response = await got
.get(url, {
headers: {
Authorization: `Bearer ${apiKey}`,
},
})
.json()
const projectsData = response as {
id: string
name: string
}[]
return projectsData.map((project) => ({
label: project.name,
value: project.id,
}))
},
},
],
actions: [searchDocuments],
})

View File

@ -0,0 +1,21 @@
import React from 'react'
export const ZemanticAiLogo = (props: React.SVGProps<SVGSVGElement>) => (
<svg viewBox="0 0 24 24" {...props}>
<g transform="matrix(.049281 0 0 .064343 -.27105 -3.4424)">
<path
d="m99.5 205.5v221h-94v-373h94v152z"
fill="#8771b1"
opacity=".991"
/>
<path
d="m284.5 426.5v-221-152h94v373h-94z"
fill="#f05b4e"
opacity=".99"
/>
<path d="m99.5 205.5h93v221h-93v-221z" fill="#ec9896" />
<path d="m192.5 205.5h92v221h-92v-221z" fill="#efe894" />
<path d="m398.5 298.5h94v128h-94v-128z" fill="#46bb91" opacity=".989" />
</g>
</svg>
)

View File

@ -0,0 +1,16 @@
{
"name": "@typebot.io/zemantic-ai-block",
"version": "1.0.0",
"description": "",
"main": "index.ts",
"keywords": [],
"license": "ISC",
"devDependencies": {
"@typebot.io/forge": "workspace:*",
"@typebot.io/tsconfig": "workspace:*",
"@types/react": "18.2.15",
"typescript": "5.3.2",
"@typebot.io/lib": "workspace:*",
"got": "12.6.0"
}
}

View File

@ -0,0 +1,10 @@
{
"extends": "@typebot.io/tsconfig/base.json",
"include": ["**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"],
"compilerOptions": {
"lib": ["ESNext", "DOM"],
"noEmit": true,
"jsx": "react"
}
}

View File

@ -0,0 +1,4 @@
export type ZemanticAiResponse = {
results: { documentId: string; text: string; score: number }[]
summary: string
}

295
packages/forge/cli/index.ts Normal file
View File

@ -0,0 +1,295 @@
import * as p from '@clack/prompts'
import { spinner } from '@clack/prompts'
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'
import { join } from 'path'
import prettier from 'prettier'
import { spawn } from 'child_process'
import builderPackageJson from '../../../apps/builder/package.json'
const slugRegex = /^[a-z0-9]+(?:-[a-z0-9]+)*$/
const prettierRc = {
trailingComma: 'es5',
tabWidth: 2,
semi: false,
singleQuote: true,
} as const
type PromptResult = {
name: string
id: string
auth: 'apiKey' | 'encryptedData' | 'none'
camelCaseId: string
}
const main = async () => {
p.intro('Create a new Typebot integration block')
const { name, id } = await p.group(
{
name: () =>
p.text({
message: 'Integration name?',
placeholder: 'Short name like: Sheets, Analytics, Cal.com',
}),
id: ({ results }) =>
p.text({
message:
'Integration ID? (should be a slug like: cal-com, openai...)',
placeholder: 'my-integration',
initialValue: slugify(results.name ?? ''),
validate: (val) => {
if (!slugRegex.test(val))
return 'Invalid ID. Must be a slug, like: "google-sheets".'
},
}),
},
{
onCancel: () => {
p.cancel('Operation cancelled.')
process.exit(0)
},
}
)
const auth = (await p.select({
message: 'Does this integration require authentication to work?',
options: [
{ value: 'apiKey', label: 'API key or token' },
{ value: 'encryptedData', label: 'Custom encrypted data' },
{ value: 'none', label: 'None' },
],
})) as 'apiKey' | 'encryptedData' | 'none'
const prompt: PromptResult = {
name,
id: id as string,
auth,
camelCaseId: camelize(id as string),
}
const s = spinner()
s.start('Creating files...')
const newBlockPath = join(process.cwd(), `../blocks/${prompt.camelCaseId}`)
if (existsSync(newBlockPath)) {
s.stop('Creating files...')
p.log.error(
`An integration with the ID "${prompt.id}" already exists. Please choose a different ID.`
)
process.exit(1)
}
mkdirSync(newBlockPath)
await createPackageJson(newBlockPath, prompt)
await createTsConfig(newBlockPath)
await createIndexFile(newBlockPath, prompt)
await createLogoFile(newBlockPath, prompt)
if (prompt.auth !== 'none') await createAuthFile(newBlockPath, prompt)
await addNewIntegrationToRepository(prompt)
s.stop('Creating files...')
s.start('Installing dependencies...')
await new Promise<void>((resolve, reject) => {
const ls = spawn('pnpm', ['install'])
ls.stderr.on('data', (data) => {
reject(data)
})
ls.on('error', (error) => {
reject(error)
})
ls.on('close', () => {
resolve()
})
})
s.stop('Installing dependencies...')
p.outro(
`Done! 🎉 Head over to packages/forge/blocks/${prompt.camelCaseId} and start coding!`
)
}
const slugify = (str: string): string => {
return String(str)
.normalize('NFKD') // split accented characters into their base characters and diacritical marks
.replace(/[\u0300-\u036f]/g, '') // remove all the accents, which happen to be all in the \u03xx UNICODE block.
.trim() // trim leading or trailing whitespace
.toLowerCase() // convert to lowercase
.replace(/[^a-z0-9\. -]/g, '') // remove non-alphanumeric characters
.replace(/[\s\.]+/g, '-') // replace spaces and dots with hyphens
.replace(/-+/g, '-') // remove consecutive hyphens
}
const camelize = (str: string) =>
str.replace(/-([a-z])/g, function (g) {
return g[1].toUpperCase()
})
const capitalize = (str: string) => {
const [fst] = str
return `${fst.toUpperCase()}${str.slice(1)}`
}
const createIndexFile = async (
path: string,
{ id, camelCaseId, name, auth }: PromptResult
) => {
const camelCaseName = camelize(id as string)
writeFileSync(
join(path, 'index.ts'),
await prettier.format(
`import { createBlock } from '@typebot.io/forge'
import { ${capitalize(camelCaseId)}Logo } from './logo'
${auth !== 'none' ? `import { auth } from './auth'` : ''}
export const ${camelCaseName} = createBlock({
id: '${id}',
name: '${name}',
tags: [],
LightLogo: ${capitalize(camelCaseName)}Logo,${auth !== 'none' ? `auth,` : ''}
actions: [],
})
`,
{ parser: 'typescript', ...prettierRc }
)
)
}
const createPackageJson = async (path: string, { id }: { id: unknown }) => {
writeFileSync(
join(path, 'package.json'),
await prettier.format(
JSON.stringify({
name: `@typebot.io/${id}-block`,
version: '1.0.0',
description: '',
main: 'index.ts',
keywords: [],
license: 'ISC',
devDependencies: {
'@typebot.io/forge': 'workspace:*',
'@typebot.io/tsconfig': 'workspace:*',
'@types/react': builderPackageJson.devDependencies['@types/react'],
typescript: builderPackageJson.devDependencies['typescript'],
'@typebot.io/lib': 'workspace:*',
},
}),
{ parser: 'json', ...prettierRc }
)
)
}
const addNewIntegrationToRepository = async ({
camelCaseId,
id,
}: {
camelCaseId: string
id: string
}) => {
const schemasPath = join(process.cwd(), `../schemas`)
const packageJson = require(join(schemasPath, 'package.json'))
packageJson.devDependencies[`@typebot.io/${id}-block`] = 'workspace:*'
writeFileSync(
join(schemasPath, 'package.json'),
await prettier.format(JSON.stringify(packageJson, null, 2), {
parser: 'json',
...prettierRc,
})
)
const repoIndexFile = readFileSync(join(schemasPath, 'index.ts')).toString()
writeFileSync(
join(schemasPath, 'index.ts'),
await prettier.format(
repoIndexFile
.replace(
'] as BlockDefinition<(typeof enabledBlocks)[number], any, any>[]',
`${camelCaseId},] as BlockDefinition<(typeof enabledBlocks)[number], any, any>[]`
)
.replace(
'// Do not edit this file manually',
`// Do not edit this file manually\nimport {${camelCaseId}} from '@typebot.io/${id}-block'`
),
{ parser: 'typescript', ...prettierRc }
)
)
const repoPath = join(process.cwd(), `../repository`)
const enabledIndexFile = readFileSync(join(repoPath, 'index.ts')).toString()
writeFileSync(
join(repoPath, 'index.ts'),
await prettier.format(
enabledIndexFile.replace('] as const', `'${id}'] as const`),
{ parser: 'typescript', ...prettierRc }
)
)
}
const createTsConfig = async (path: string) => {
writeFileSync(
join(path, 'tsconfig.json'),
await prettier.format(
JSON.stringify({
extends: '@typebot.io/tsconfig/base.json',
include: ['**/*.ts', '**/*.tsx'],
exclude: ['node_modules'],
compilerOptions: {
lib: ['ESNext', 'DOM'],
noEmit: true,
jsx: 'react',
},
}),
{ parser: 'json', ...prettierRc }
)
)
}
const createLogoFile = async (
path: string,
{ camelCaseId }: { camelCaseId: string }
) => {
writeFileSync(
join(path, 'logo.tsx'),
await prettier.format(
`import React from 'react'
export const ${capitalize(
camelCaseId
)}Logo = (props: React.SVGProps<SVGSVGElement>) => <svg></svg>
`,
{ parser: 'typescript', ...prettierRc }
)
)
}
const createAuthFile = async (
path: string,
{ name, auth }: { name: string; auth: 'apiKey' | 'encryptedData' | 'none' }
) =>
writeFileSync(
join(path, 'auth.ts'),
await prettier.format(
`import { option, AuthDefinition } from '@typebot.io/forge'
export const auth = {
type: 'encryptedCredentials',
name: '${name} account',
${
auth === 'apiKey'
? `schema: option.object({
apiKey: option.string.layout({
label: 'API key',
isRequired: true,
input: 'password',
helperText:
'You can generate an API key [here](<INSERT_URL>).',
}),
}),`
: ''
}
} satisfies AuthDefinition`,
{ parser: 'typescript', ...prettierRc }
)
)
main()
.then()
.catch((err) => {
console.error(err)
process.exit(1)
})

View File

@ -0,0 +1,19 @@
{
"name": "forge-cli",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "tsx ./index.ts"
},
"keywords": [],
"author": "Baptiste Arnaud",
"license": "ISC",
"devDependencies": {
"@clack/prompts": "^0.7.0",
"@typebot.io/tsconfig": "workspace:*",
"@types/node": "^20.10.1",
"tsx": "^4.6.1",
"prettier": "3.0.0"
}
}

View File

@ -0,0 +1,9 @@
{
"extends": "@typebot.io/tsconfig/base.json",
"include": ["./**/*.ts"],
"exclude": ["node_modules"],
"compilerOptions": {
"lib": ["ESNext"],
"resolveJsonModule": true
}
}

View File

@ -0,0 +1,133 @@
import { AuthDefinition, BlockDefinition, ActionDefinition } from './types'
import { z } from './zod'
export const variableStringSchema = z.custom<`{{${string}}}`>((val) =>
/^{{.+}}$/g.test(val as string)
)
export const createAuth = <A extends AuthDefinition>(authDefinition: A) =>
authDefinition
export const createBlock = <
I extends string,
A extends AuthDefinition,
O extends z.ZodObject<any>
>(
blockDefinition: BlockDefinition<I, A, O>
): BlockDefinition<I, A, O> => blockDefinition
export const createAction = <
A extends AuthDefinition,
BaseOptions extends z.ZodObject<any>,
O extends z.ZodObject<any>
>(
actionDefinition: {
auth?: A
baseOptions?: BaseOptions
} & ActionDefinition<A, BaseOptions, O>
) => actionDefinition
export const parseBlockSchema = <
I extends string,
A extends AuthDefinition,
O extends z.ZodObject<any>
>(
blockDefinition: BlockDefinition<I, A, O>
) => {
const options = z.discriminatedUnion('action', [
blockDefinition.options
? blockDefinition.options.extend({
credentialsId: z.string().optional(),
action: z.undefined(),
})
: z.object({
credentialsId: z.string().optional(),
action: z.undefined(),
}),
...blockDefinition.actions.map((action) =>
blockDefinition.options
? (blockDefinition.options
.extend({
credentialsId: z.string().optional(),
})
.extend({
action: z.literal(action.name),
})
.merge(action.options ?? z.object({})) as any)
: z
.object({
credentialsId: z.string().optional(),
})
.extend({
action: z.literal(action.name),
})
.merge(action.options ?? z.object({}))
),
])
return z.object({
id: z.string(),
outgoingEdgeId: z.string().optional(),
type: z.literal(blockDefinition.id),
options: options.optional(),
})
}
export const parseBlockCredentials = <
I extends string,
A extends AuthDefinition,
O extends z.ZodObject<any>
>(
blockDefinition: BlockDefinition<I, A, O>
) => {
if (!blockDefinition.auth) throw new Error('Block has no auth definition')
return z.object({
id: z.string(),
type: z.literal(blockDefinition.id),
createdAt: z.date(),
workspaceId: z.string(),
name: z.string(),
iv: z.string(),
data: blockDefinition.auth.schema,
})
}
export const option = {
object: <T extends z.ZodRawShape>(schema: T) => z.object(schema),
literal: <T extends string>(value: T) => z.literal(value),
string: z.string().optional(),
enum: <T extends string>(values: readonly [T, ...T[]]) =>
z.enum(values).optional(),
number: z.number().or(variableStringSchema).optional(),
array: <T extends z.ZodTypeAny>(schema: T) => z.array(schema).optional(),
discriminatedUnion: <
T extends string,
J extends [
z.ZodDiscriminatedUnionOption<T>,
...z.ZodDiscriminatedUnionOption<T>[]
]
>(
field: T,
schemas: J
) =>
// @ts-expect-error
z.discriminatedUnion<T, J>(field, [
z.object({ [field]: z.undefined() }),
...schemas,
]),
saveResponseArray: <I extends readonly [string, ...string[]]>(items: I) =>
z
.array(
z.object({
item: z.enum(items).optional().layout({
placeholder: 'Select a response',
defaultValue: items[0],
}),
variableId: z.string().optional().layout({
input: 'variableDropdown',
}),
})
)
.optional(),
}
export type * from './types'

View File

@ -0,0 +1,16 @@
{
"name": "@typebot.io/forge",
"version": "1.0.0",
"description": "",
"main": "index.ts",
"keywords": [],
"author": "Baptiste Arnaud",
"license": "ISC",
"dependencies": {
"zod": "3.22.4"
},
"devDependencies": {
"@typebot.io/tsconfig": "workspace:*",
"@types/react": "18.2.15"
}
}

View File

@ -0,0 +1,5 @@
{
"extends": "@typebot.io/tsconfig/base.json",
"include": ["**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}

View File

@ -0,0 +1,133 @@
import { SVGProps } from 'react'
import { z } from './zod'
export type VariableStore = {
get: (variableId: string) => string | (string | null)[] | null | undefined
set: (variableId: string, value: unknown) => void
parse: (value: string) => string
}
export type LogsStore = {
add: (
log:
| string
| {
status: 'error' | 'success' | 'info'
description: string
details?: unknown
}
) => void
}
export type FunctionToExecute = {
args: Record<string, string | number | null>
content: string
}
export type ReadOnlyVariableStore = Omit<VariableStore, 'set'>
export type ActionDefinition<
A extends AuthDefinition,
BaseOptions extends z.ZodObject<any>,
Options extends z.ZodObject<any> = z.ZodObject<{}>
> = {
name: string
fetchers?: FetcherDefinition<A, z.infer<BaseOptions> & z.infer<Options>>[]
options?: Options
getSetVariableIds?: (options: z.infer<Options>) => string[]
run?: {
server?: (params: {
credentials: CredentialsFromAuthDef<A>
options: z.infer<BaseOptions> & z.infer<Options>
variables: VariableStore
logs: LogsStore
}) => Promise<void> | void
/**
* Used to stream a text bubble. Will only be used if the block following the integration block is a text bubble containing the variable returned by `getStreamVariableId`.
*/
stream?: {
getStreamVariableId: (options: z.infer<Options>) => string | undefined
run: (params: {
credentials: CredentialsFromAuthDef<A>
options: z.infer<BaseOptions> & z.infer<Options>
variables: ReadOnlyVariableStore
}) => Promise<ReadableStream<any> | undefined>
}
web?: {
displayEmbedBubble?: {
waitForEvent?: {
getSaveVariableId?: (
options: z.infer<BaseOptions> & z.infer<Options>
) => string | undefined
parseFunction: (params: {
options: z.infer<BaseOptions> & z.infer<Options>
}) => FunctionToExecute
}
parseInitFunction: (params: {
options: z.infer<BaseOptions> & z.infer<Options>
}) => FunctionToExecute
}
parseFunction?: (params: {
options: z.infer<BaseOptions> & z.infer<Options>
}) => FunctionToExecute
}
}
}
export type FetcherDefinition<A extends AuthDefinition, T = {}> = {
id: string
/**
* List of option keys to determine if the fetcher should be re-executed whenever these options are updated.
*/
dependencies: (keyof T)[]
fetch: (params: {
credentials: CredentialsFromAuthDef<A>
options: T
}) => Promise<(string | { label: string; value: string })[]>
}
export type AuthDefinition = {
type: 'encryptedCredentials'
name: string
schema: z.ZodObject<any>
}
export type CredentialsFromAuthDef<A extends AuthDefinition> = A extends {
type: 'encryptedCredentials'
schema: infer S extends z.ZodObject<any>
}
? z.infer<S>
: never
export type BlockDefinition<
Id extends string,
Auth extends AuthDefinition,
Options extends z.ZodObject<any>
> = {
id: Id
name: string
fullName?: string
/**
* Keywords used when searching for a block.
*/
tags?: string[]
LightLogo: (props: SVGProps<SVGSVGElement>) => JSX.Element
DarkLogo?: (props: SVGProps<SVGSVGElement>) => JSX.Element
docsUrl?: string
auth?: Auth
options?: Options | undefined
fetchers?: FetcherDefinition<Auth, Options>[]
isDisabledInPreview?: boolean
actions: ActionDefinition<Auth, Options>[]
}
export type FetchItemsParams<T> = T extends ActionDefinition<
infer A,
infer BaseOptions,
infer Options
>
? {
credentials: CredentialsFromAuthDef<A>
options: BaseOptions & Options
}
: never

View File

@ -0,0 +1,48 @@
import { ZodArray, ZodDate, ZodOptional, ZodString, ZodTypeAny, z } from 'zod'
type OptionableZodType<T extends ZodTypeAny> = T | ZodOptional<T>
export interface ZodLayoutMetadata<
T extends ZodTypeAny,
TInferred = z.input<T> | z.output<T>
> {
accordion?: string
label?: string
input?: 'variableDropdown' | 'textarea' | 'password'
defaultValue?: T extends ZodDate ? string : TInferred
placeholder?: string
helperText?: string
direction?: 'row' | 'column'
isRequired?: boolean
withVariableButton?: boolean
fetcher?: T extends OptionableZodType<ZodString> ? string : never
itemLabel?: T extends OptionableZodType<ZodArray<any>> ? string : never
isOrdered?: T extends OptionableZodType<ZodArray<any>> ? boolean : never
isHidden?: boolean
moreInfoTooltip?: string
}
declare module 'zod' {
interface ZodType<Output, Def extends ZodTypeDef, Input = Output> {
layout<T extends ZodTypeAny>(this: T, metadata: ZodLayoutMetadata<T>): T
}
interface ZodTypeDef {
layout?: ZodLayoutMetadata<ZodTypeAny>
}
}
export const extendWithTypebotLayout = (zod: typeof z) => {
if (typeof zod.ZodType.prototype.layout !== 'undefined') {
return
}
zod.ZodType.prototype.layout = function (layout) {
const result = new (this as any).constructor({
...this._def,
layout,
})
return result
}
}

View File

@ -0,0 +1,10 @@
import { z } from 'zod'
import {
extendWithTypebotLayout,
ZodLayoutMetadata,
} from './extendWithTypebotLayout'
extendWithTypebotLayout(z)
export { z }
export type { ZodLayoutMetadata }

View File

@ -0,0 +1,7 @@
// Do not edit this file manually
export const enabledBlocks = [
'openai',
'zemantic-ai',
'cal-com',
'chat-node',
] as const

View File

@ -0,0 +1,9 @@
{
"name": "@typebot.io/forge-repository",
"version": "1.0.0",
"description": "",
"main": "index.ts",
"keywords": [],
"author": "Baptiste Arnaud",
"license": "ISC"
}

View File

@ -0,0 +1,28 @@
// Do not edit this file manually
import { chatNode } from '@typebot.io/chat-node-block'
import { calCom } from '@typebot.io/cal-com-block'
import { zemanticAi } from '@typebot.io/zemantic-ai-block'
import { openAIBlock } from '@typebot.io/openai-block'
import {
BlockDefinition,
parseBlockCredentials,
parseBlockSchema,
} from '@typebot.io/forge'
import { enabledBlocks } from '@typebot.io/forge-repository'
import { z } from '@typebot.io/forge/zod'
export const forgedBlocks = [
openAIBlock,
zemanticAi,
calCom,
chatNode,
] as BlockDefinition<(typeof enabledBlocks)[number], any, any>[]
export type ForgedBlockDefinition = (typeof forgedBlocks)[number]
export const forgedBlockSchemas = forgedBlocks.map(parseBlockSchema)
export type ForgedBlock = z.infer<(typeof forgedBlockSchemas)[number]>
export const forgedCredentialsSchemas = forgedBlocks
.filter((b) => b.auth)
.map(parseBlockCredentials)

View File

@ -0,0 +1,17 @@
{
"name": "@typebot.io/forge-schemas",
"version": "1.0.0",
"description": "",
"main": "index.ts",
"keywords": [],
"author": "Baptiste Arnaud",
"license": "ISC",
"devDependencies": {
"@typebot.io/forge": "workspace:*",
"@typebot.io/forge-repository": "workspace:*",
"@typebot.io/openai-block": "workspace:*",
"@typebot.io/zemantic-ai-block": "workspace:*",
"@typebot.io/cal-com-block": "workspace:*",
"@typebot.io/chat-node-block": "workspace:*"
}
}

View File

@ -13,12 +13,13 @@
"@typebot.io/schemas": "workspace:*",
"@typebot.io/tsconfig": "workspace:*",
"@types/nodemailer": "6.4.8",
"next": "13.5.4",
"next": "14.0.3",
"nodemailer": "6.9.3",
"typescript": "5.3.2"
"typescript": "5.3.2",
"@typebot.io/forge-repository": "workspace:*"
},
"peerDependencies": {
"next": "13.0.0",
"next": "14.0.0",
"nodemailer": "6.7.8"
},
"dependencies": {
@ -28,6 +29,7 @@
"got": "12.6.0",
"minio": "7.1.3",
"remark-slate": "1.8.6",
"stripe": "12.13.0"
"stripe": "12.13.0",
"zod": "3.22.4"
}
}

View File

@ -19,6 +19,7 @@ import { PictureChoiceBlock } from '@typebot.io/schemas/features/blocks/inputs/p
import { IntegrationBlockType } from '@typebot.io/schemas/features/blocks/integrations/constants'
import { LogicBlockType } from '@typebot.io/schemas/features/blocks/logic/constants'
import { defaultChoiceInputOptions } from '@typebot.io/schemas/features/blocks/inputs/choice/constants'
import { enabledBlocks } from '@typebot.io/forge-repository'
export const sendRequest = async <ResponseData>(
params:
@ -110,7 +111,11 @@ export const isConditionBlock = (block: Block): block is ConditionBlock =>
block.type === LogicBlockType.CONDITION
export const isIntegrationBlock = (block: Block): block is IntegrationBlock =>
(Object.values(IntegrationBlockType) as string[]).includes(block.type)
(
Object.values(IntegrationBlockType).concat(
enabledBlocks as readonly any[]
) as any[]
).includes(block.type)
export const isWebhookBlock = (block: Block): block is WebhookBlock =>
[
@ -252,3 +257,5 @@ export const getAtPath = <T>(obj: T, path: string): unknown => {
export const isSvgSrc = (src: string | undefined) =>
src?.startsWith('data:image/svg') || src?.endsWith('.svg')
export { createId } from '@paralleldrive/cuid2'

View File

@ -1,11 +1,12 @@
import { z } from 'zod'
import { blockBaseSchema } from './shared'
import { startBlockSchema } from './start/schemas'
import { Item, ItemV6 } from '../items/schema'
import { ItemV6 } from '../items/schema'
import { bubbleBlockSchemas } from './bubbles/schema'
import { LogicBlock, logicBlockSchemas } from './logic/schema'
import { InputBlock, inputBlockSchemas } from './inputs/schema'
import { IntegrationBlock, integrationBlockSchemas } from './integrations'
import { enabledBlocks } from '@typebot.io/forge-repository'
export type BlockWithOptions = Extract<Block, { options?: any }>
@ -27,12 +28,21 @@ export const blockSchemaV5 = z.discriminatedUnion('type', [
])
export type BlockV5 = z.infer<typeof blockSchemaV5>
export const blockSchemaV6 = z.discriminatedUnion('type', [
...bubbleBlockSchemas,
...inputBlockSchemas.v6,
...logicBlockSchemas.v6,
...integrationBlockSchemas.v6,
])
export const blockSchemaV6 = z
.discriminatedUnion('type', [
...bubbleBlockSchemas,
...inputBlockSchemas.v6,
...logicBlockSchemas.v6,
...integrationBlockSchemas.v6,
])
.or(
blockBaseSchema.merge(
z.object({
type: z.enum(enabledBlocks),
options: z.any().optional(),
})
)
)
export type BlockV6 = z.infer<typeof blockSchemaV6>
const blockSchema = blockSchemaV5.or(blockSchemaV6)

View File

@ -60,6 +60,24 @@ const embedMessageSchema = z.object({
.merge(z.object({ height: z.number().optional() })),
})
const displayEmbedBubbleSchema = z.object({
waitForEventFunction: z
.object({
args: z.record(z.string(), z.unknown()),
content: z.string(),
})
.optional(),
initFunction: z.object({
args: z.record(z.string(), z.unknown()),
content: z.string(),
}),
})
const customEmbedSchema = z.object({
type: z.literal('custom-embed'),
content: displayEmbedBubbleSchema,
})
export type CustomEmbedBubble = z.infer<typeof customEmbedSchema>
export const chatMessageSchema = z
.object({ id: z.string() })
.and(
@ -69,6 +87,7 @@ export const chatMessageSchema = z
videoMessageSchema,
audioMessageSchema,
embedMessageSchema,
customEmbedSchema,
])
)
export type ChatMessage = z.infer<typeof chatMessageSchema>
@ -242,6 +261,19 @@ export const clientSideActionSchema = z
pixel: pixelOptionsSchema,
})
)
.or(
z.object({
stream: z.literal(true),
})
)
.or(
z.object({
codeToExecute: z.object({
args: z.record(z.string(), z.unknown()),
content: z.string(),
}),
})
)
)
const typebotInChatReplyPick = {

Some files were not shown because too many files have changed in this diff Show More