✨ Introducing The Forge (#1072)
The Forge allows anyone to easily create their own Typebot Block. Closes #380
This commit is contained in:
@ -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])
|
||||
|
@ -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) => {
|
||||
|
@ -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,
|
||||
|
@ -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'
|
||||
|
||||
|
@ -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[]) =>
|
||||
|
@ -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 = (
|
||||
|
@ -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,
|
||||
|
@ -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[]) =>
|
||||
|
@ -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,
|
||||
|
@ -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,
|
@ -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,
|
@ -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,
|
@ -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 (
|
@ -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'
|
||||
|
@ -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,
|
||||
])
|
@ -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 =
|
||||
(
|
@ -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,
|
||||
|
@ -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 (
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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'
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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 = (
|
||||
|
@ -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 = (
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
207
packages/bot-engine/forge/executeForgedBlock.ts
Normal file
207
packages/bot-engine/forge/executeForgedBlock.ts
Normal 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
|
@ -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:*"
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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 = (
|
||||
|
@ -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 =
|
||||
|
@ -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",
|
||||
|
@ -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 <></>
|
||||
|
@ -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>
|
||||
)
|
||||
|
@ -4,7 +4,8 @@
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
"@/*": ["src/*"],
|
||||
"react": ["./node_modules/@types/react"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -263,7 +263,7 @@ pre {
|
||||
}
|
||||
|
||||
.typebot-chat-view {
|
||||
max-width: 800px;
|
||||
max-width: 900px;
|
||||
}
|
||||
|
||||
.ping span {
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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']}
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
@ -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.',
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -38,7 +38,7 @@
|
||||
"typescript": "5.3.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"next": "12.x || 13.x",
|
||||
"next": "12.x || 13.x || 14.x",
|
||||
"react": "18.x"
|
||||
}
|
||||
}
|
||||
|
126
packages/forge/blocks/calCom/actions/bookEvent.ts
Normal file
126
packages/forge/blocks/calCom/actions/bookEvent.ts
Normal 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'
|
||||
}
|
||||
}
|
11
packages/forge/blocks/calCom/baseOptions.ts
Normal file
11
packages/forge/blocks/calCom/baseOptions.ts
Normal 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',
|
||||
}),
|
||||
})
|
1
packages/forge/blocks/calCom/constants.ts
Normal file
1
packages/forge/blocks/calCom/constants.ts
Normal file
@ -0,0 +1 @@
|
||||
export const defaultBaseUrl = 'https://app.cal.com'
|
13
packages/forge/blocks/calCom/index.ts
Normal file
13
packages/forge/blocks/calCom/index.ts
Normal 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],
|
||||
})
|
16
packages/forge/blocks/calCom/logo.tsx
Normal file
16
packages/forge/blocks/calCom/logo.tsx
Normal 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>
|
||||
)
|
15
packages/forge/blocks/calCom/package.json
Normal file
15
packages/forge/blocks/calCom/package.json
Normal 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:*"
|
||||
}
|
||||
}
|
10
packages/forge/blocks/calCom/tsconfig.json
Normal file
10
packages/forge/blocks/calCom/tsconfig.json
Normal 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"
|
||||
}
|
||||
}
|
63
packages/forge/blocks/chatNode/actions/sendMessage.ts
Normal file
63
packages/forge/blocks/chatNode/actions/sendMessage.ts
Normal 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)
|
||||
})
|
||||
},
|
||||
},
|
||||
})
|
15
packages/forge/blocks/chatNode/auth.ts
Normal file
15
packages/forge/blocks/chatNode/auth.ts
Normal 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
|
1
packages/forge/blocks/chatNode/constants.ts
Normal file
1
packages/forge/blocks/chatNode/constants.ts
Normal file
@ -0,0 +1 @@
|
||||
export const apiBaseUrl = 'https://api.public.chatnode.ai/v1/'
|
13
packages/forge/blocks/chatNode/index.ts
Normal file
13
packages/forge/blocks/chatNode/index.ts
Normal 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],
|
||||
})
|
10
packages/forge/blocks/chatNode/logo.tsx
Normal file
10
packages/forge/blocks/chatNode/logo.tsx
Normal 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>
|
||||
)
|
16
packages/forge/blocks/chatNode/package.json
Normal file
16
packages/forge/blocks/chatNode/package.json
Normal 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"
|
||||
}
|
||||
}
|
10
packages/forge/blocks/chatNode/tsconfig.json
Normal file
10
packages/forge/blocks/chatNode/tsconfig.json
Normal 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"
|
||||
}
|
||||
}
|
4
packages/forge/blocks/chatNode/types.ts
Normal file
4
packages/forge/blocks/chatNode/types.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export type ChatNodeResponse = {
|
||||
message: string
|
||||
chat_session_id: string
|
||||
}
|
180
packages/forge/blocks/openai/actions/createChatCompletion.tsx
Normal file
180
packages/forge/blocks/openai/actions/createChatCompletion.tsx
Normal 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,
|
||||
})
|
105
packages/forge/blocks/openai/actions/createSpeech.tsx
Normal file
105
packages/forge/blocks/openai/actions/createSpeech.tsx
Normal 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)
|
||||
},
|
||||
},
|
||||
})
|
16
packages/forge/blocks/openai/auth.ts
Normal file
16
packages/forge/blocks/openai/auth.ts
Normal 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,
|
||||
}),
|
||||
}),
|
||||
})
|
14
packages/forge/blocks/openai/baseOptions.ts
Normal file
14
packages/forge/blocks/openai/baseOptions.ts
Normal 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',
|
||||
}),
|
||||
})
|
15
packages/forge/blocks/openai/constants.ts
Normal file
15
packages/forge/blocks/openai/constants.ts
Normal 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
|
@ -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
|
||||
}
|
17
packages/forge/blocks/openai/index.ts
Normal file
17
packages/forge/blocks/openai/index.ts
Normal 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],
|
||||
})
|
13
packages/forge/blocks/openai/logo.tsx
Normal file
13
packages/forge/blocks/openai/logo.tsx
Normal 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>
|
||||
)
|
20
packages/forge/blocks/openai/package.json
Normal file
20
packages/forge/blocks/openai/package.json
Normal 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:*"
|
||||
}
|
||||
}
|
10
packages/forge/blocks/openai/tsconfig.json
Normal file
10
packages/forge/blocks/openai/tsconfig.json
Normal 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"
|
||||
}
|
||||
}
|
110
packages/forge/blocks/zemanticAi/actions/searchDocuments.ts
Normal file
110
packages/forge/blocks/zemanticAi/actions/searchDocuments.ts
Normal 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)
|
||||
})
|
||||
},
|
||||
},
|
||||
})
|
15
packages/forge/blocks/zemanticAi/auth.ts
Normal file
15
packages/forge/blocks/zemanticAi/auth.ts
Normal 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
|
8
packages/forge/blocks/zemanticAi/baseOptions.ts
Normal file
8
packages/forge/blocks/zemanticAi/baseOptions.ts
Normal 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',
|
||||
}),
|
||||
})
|
1
packages/forge/blocks/zemanticAi/constants.ts
Normal file
1
packages/forge/blocks/zemanticAi/constants.ts
Normal file
@ -0,0 +1 @@
|
||||
export const apiBaseUrl = 'https://api.zemantic.ai/v1/search-documents'
|
43
packages/forge/blocks/zemanticAi/index.ts
Normal file
43
packages/forge/blocks/zemanticAi/index.ts
Normal 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],
|
||||
})
|
21
packages/forge/blocks/zemanticAi/logo.tsx
Normal file
21
packages/forge/blocks/zemanticAi/logo.tsx
Normal 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>
|
||||
)
|
16
packages/forge/blocks/zemanticAi/package.json
Normal file
16
packages/forge/blocks/zemanticAi/package.json
Normal 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"
|
||||
}
|
||||
}
|
10
packages/forge/blocks/zemanticAi/tsconfig.json
Normal file
10
packages/forge/blocks/zemanticAi/tsconfig.json
Normal 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"
|
||||
}
|
||||
}
|
4
packages/forge/blocks/zemanticAi/types.ts
Normal file
4
packages/forge/blocks/zemanticAi/types.ts
Normal 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
295
packages/forge/cli/index.ts
Normal 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)
|
||||
})
|
19
packages/forge/cli/package.json
Normal file
19
packages/forge/cli/package.json
Normal 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"
|
||||
}
|
||||
}
|
9
packages/forge/cli/tsconfig.json
Normal file
9
packages/forge/cli/tsconfig.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "@typebot.io/tsconfig/base.json",
|
||||
"include": ["./**/*.ts"],
|
||||
"exclude": ["node_modules"],
|
||||
"compilerOptions": {
|
||||
"lib": ["ESNext"],
|
||||
"resolveJsonModule": true
|
||||
}
|
||||
}
|
133
packages/forge/core/index.ts
Normal file
133
packages/forge/core/index.ts
Normal 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'
|
16
packages/forge/core/package.json
Normal file
16
packages/forge/core/package.json
Normal 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"
|
||||
}
|
||||
}
|
5
packages/forge/core/tsconfig.json
Normal file
5
packages/forge/core/tsconfig.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": "@typebot.io/tsconfig/base.json",
|
||||
"include": ["**/*.ts", "**/*.tsx"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
133
packages/forge/core/types.ts
Normal file
133
packages/forge/core/types.ts
Normal 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
|
48
packages/forge/core/zod/extendWithTypebotLayout.ts
Normal file
48
packages/forge/core/zod/extendWithTypebotLayout.ts
Normal 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
|
||||
}
|
||||
}
|
10
packages/forge/core/zod/index.ts
Normal file
10
packages/forge/core/zod/index.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { z } from 'zod'
|
||||
import {
|
||||
extendWithTypebotLayout,
|
||||
ZodLayoutMetadata,
|
||||
} from './extendWithTypebotLayout'
|
||||
|
||||
extendWithTypebotLayout(z)
|
||||
|
||||
export { z }
|
||||
export type { ZodLayoutMetadata }
|
7
packages/forge/repository/index.ts
Normal file
7
packages/forge/repository/index.ts
Normal file
@ -0,0 +1,7 @@
|
||||
// Do not edit this file manually
|
||||
export const enabledBlocks = [
|
||||
'openai',
|
||||
'zemantic-ai',
|
||||
'cal-com',
|
||||
'chat-node',
|
||||
] as const
|
9
packages/forge/repository/package.json
Normal file
9
packages/forge/repository/package.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "@typebot.io/forge-repository",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.ts",
|
||||
"keywords": [],
|
||||
"author": "Baptiste Arnaud",
|
||||
"license": "ISC"
|
||||
}
|
28
packages/forge/schemas/index.ts
Normal file
28
packages/forge/schemas/index.ts
Normal 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)
|
17
packages/forge/schemas/package.json
Normal file
17
packages/forge/schemas/package.json
Normal 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:*"
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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'
|
||||
|
@ -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)
|
||||
|
@ -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
Reference in New Issue
Block a user