2
0

Introducing The Forge (#1072)

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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