2
0

📝 Add Contribute docs

This commit is contained in:
Baptiste Arnaud
2024-01-03 16:29:41 +01:00
parent b3957295bd
commit 65f4fb0d7a
66 changed files with 1453 additions and 519 deletions

View File

@ -1,109 +1,3 @@
# Contributing to Typebot
You are considering contributing to Typebot. I thank you for this 🙏.
Any contributions you make are **greatly appreciated**. It can be anything from typo fixes to new features.
Let's [discuss](https://github.com/baptisteArno/typebot.io/discussions/new) about what you want to implement before creating a PR if you are unsure about the requirements or the vision of Typebot.
Typebot is a Monorepo powered by [Turborepo](https://turborepo.org/). It is composed of 2 main applications:
- the builder ([`./apps/builder`](apps/builder)), where you build your typebots
- the viewer ([`./apps/viewer`](./apps/viewer)), where your user answer the typebot
These apps are built with awesome web technologies including [Typescript](https://www.typescriptlang.org/), [Next.js](https://nextjs.org/), [Prisma](https://www.prisma.io/), [Chakra UI](https://chakra-ui.com/), [Tailwind CSS](https://tailwindcss.com/).
## Get started
1. [Fork](https://help.github.com/articles/fork-a-repo/) this repository to your
own GitHub account and then
[clone](https://help.github.com/articles/cloning-a-repository/) it to your local device.
2. Create a new branch:
```sh
git checkout -b MY_BRANCH_NAME
```
## Running the project locally
1. Install dependencies
```sh
cd typebot.io
pnpm i
```
2. Set up environment variables
Copy [`.env.dev.example`](./.env.dev.example) to `.env`
Check out the [Configuration guide](https://docs.typebot.io/self-hosting/configuration) if you want to enable more options
3. Make sure you have [Docker](https://docs.docker.com/compose/install/) running
4. Make sure you have Node.js installed. I suggest you use [`nvm`](https://github.com/nvm-sh/nvm) allowing you to manage different versions. Once you installed nvm, you can install and use the latest version of Node.js: `nvm install && nvm use`
5. Start the builder and viewer
```sh
pnpm dev
```
Builder is available at [`http://localhost:3000`](http://localhost:3000)
Viewer is available at [`http://localhost:3001`](http://localhost:3001)
Database inspector is available at [`http://localhost:5555`](http://localhost:5555)
By default, you can easily authenticate in the builder using the "Github Sign In" button. For other options, check out the [Configuration guide](https://docs.typebot.io/self-hosting/configuration)
6. (Optionnal) Start the landing page
Copy [`apps/landing-page/.env.local.example`](apps/landing-page/.env.local.example) to `apps/landing-page/.env.local`
```sh
cd apps/landing-page
pnpm dev
```
7. (Optionnal) Start the docs
```sh
cd apps/docs
pnpm start
```
I know the project can be a bit hard to understand at first. I'm working on improving the documentation and the codebase to make it easier to contribute. If you have any questions, feel free to [open a discussion](https://github.com/baptisteArno/typebot.io/discussions/new)
## How to create a new integration block
The first step to create a new Typebot block is to define its schema. For this you need to
1. Add your integration in the enum `IntegrationBlockType` in [`packages/schemas/features/blocks/integrations/enums.ts`](packages/schemas/features/blocks/integrations/enums.ts)
2. Create a new file in [`packages/schemas/features/blocks/integrations`](packages/schemas/features/blocks/integrations).
Your schema should look like:
```ts
import { z } from 'zod'
import { blockBaseSchema } from '../baseSchemas'
export const myIntegrationBlockSchema = blockBaseSchema.merge(
z.object({
type: z.enum([IntegrationBlockType.MY_INTEGRATION]),
options: z.object({
//...
}),
})
)
export type MyIntegrationBlock = z.infer<typeof myIntegrationBlockSchema>
```
3. Add `myIntegrationBlockSchema` to `blockSchema` in `packages/schemas/features/blocks/schemas.ts`
As soon as you have defined your schema, you can start implementing your block in the builder and the viewer.
Since the code is strictly typed, you should see exactly where you need to add your integration-specific code.
To sum it up you need to create a folder in [`apps/builder/src/features/blocks/integrations`](apps/builder/src/features/blocks/integrations) and in [`apps/viewer/src/features/blocks/integrations`](apps/viewer/src/features/blocks/integrations)
All the content has been moved [here](https://docs.typebot.io/contribute/overview). ❤️

View File

@ -90,24 +90,15 @@ You'll find a lot of resources to help you get started with Typebot in the [docu
## Self-hosting
Interested in self-hosting Typebot on your server? Take a look at the [self-hosting installation instructions](https://docs.typebot.io/self-hosting).
Interested in self-hosting Typebot on your server? Take a look at the [self-hosting installation instructions](https://docs.typebot.io/self-hosting/get-started).
## How to Contribute
You are awesome, lets build great software together. Head over to the [Contribute guidelines](https://github.com/baptisteArno/typebot.io/blob/main/CONTRIBUTING.md) to get started. 💪
### Claim these open bounties 👇
<a href="https://console.algora.io/org/typebot/bounties?status=open">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://console.algora.io/api/og/typebot/bounties.png?p=0&status=open&theme=dark">
<img alt="Bounties of typebot" src="https://console.algora.io/api/og/typebot/bounties.png?p=0&status=open&theme=light">
</picture>
</a>
You are awesome, lets build great software together. Head over to the [Contribute docs](https://docs.typebot.io/contribute/overview) to get started. 💪
## Run the project locally
Follow the [Get started](https://github.com/baptisteArno/typebot.io/blob/main/CONTRIBUTING.md#get-started) section of the Contributing guide.
Follow the [Local installation](https://docs.typebot.io/contribute/guides/local-installation) section of in the Contributing docs.
### Top contributors

View File

@ -51,7 +51,7 @@ export const DropdownList = <T extends readonly any[]>({
isRequired={isRequired}
as={direction === 'column' ? Stack : HStack}
justifyContent="space-between"
width={label ? 'full' : 'auto'}
width={props.width === 'full' || label ? 'full' : 'auto'}
spacing={direction === 'column' ? 2 : 3}
>
{label && (

View File

@ -0,0 +1,135 @@
import { Box, Button, Fade, Flex, IconButton, Stack } from '@chakra-ui/react'
import { TrashIcon, PlusIcon } from '@/components/icons'
import React, { useEffect, useState } from 'react'
import { createId } from '@paralleldrive/cuid2'
type ItemWithId<T extends number | string | boolean> = { id: string; value?: T }
export type TableListItemProps<T> = {
item: T
onItemChange: (item: T) => void
}
type Props<T extends number | string | boolean> = {
initialItems?: T[]
addLabel?: string
newItemDefaultProps?: Partial<T>
hasDefaultItem?: boolean
ComponentBetweenItems?: (props: unknown) => JSX.Element
onItemsChange: (items: T[]) => void
children: (props: TableListItemProps<T>) => JSX.Element
}
const addIdToItems = <T extends number | string | boolean>(
items: T[]
): ItemWithId<T>[] => items.map((item) => ({ id: createId(), value: item }))
const removeIdFromItems = <T extends number | string | boolean>(
items: ItemWithId<T>[]
): T[] => items.map((item) => item.value as T)
export const PrimitiveList = <T extends number | string | boolean>({
initialItems,
addLabel = 'Add',
hasDefaultItem,
children,
ComponentBetweenItems,
onItemsChange,
}: Props<T>) => {
const [items, setItems] = useState<ItemWithId<T>[]>()
const [showDeleteIndex, setShowDeleteIndex] = useState<number | null>(null)
useEffect(() => {
if (items) return
if (initialItems) {
setItems(addIdToItems(initialItems))
} else if (hasDefaultItem) {
setItems(addIdToItems([]))
} else {
setItems([])
}
}, [hasDefaultItem, initialItems, items])
const createItem = () => {
if (!items) return
const newItems = [...items, { id: createId() }]
setItems(newItems)
onItemsChange(removeIdFromItems(newItems))
}
const updateItem = (itemIndex: number, newValue: T) => {
if (!items) return
const newItems = items.map((item, idx) =>
idx === itemIndex ? { ...item, value: newValue } : item
)
setItems(newItems)
onItemsChange(removeIdFromItems(newItems))
}
const deleteItem = (itemIndex: number) => () => {
if (!items) return
const newItems = [...items]
newItems.splice(itemIndex, 1)
setItems([...newItems])
onItemsChange(removeIdFromItems([...newItems]))
}
const handleMouseEnter = (itemIndex: number) => () =>
setShowDeleteIndex(itemIndex)
const handleCellChange = (itemIndex: number) => (item: T) =>
updateItem(itemIndex, item)
const handleMouseLeave = () => setShowDeleteIndex(null)
return (
<Stack spacing={0}>
{items?.map((item, itemIndex) => (
<Box key={item.id}>
{itemIndex !== 0 && ComponentBetweenItems && (
<ComponentBetweenItems />
)}
<Flex
pos="relative"
onMouseEnter={handleMouseEnter(itemIndex)}
onMouseLeave={handleMouseLeave}
mt={itemIndex !== 0 && ComponentBetweenItems ? 4 : 0}
justifyContent="center"
pb="4"
>
{children({
item: item.value as T,
onItemChange: handleCellChange(itemIndex),
})}
<Fade
in={showDeleteIndex === itemIndex}
style={{
position: 'absolute',
left: '-15px',
top: '-15px',
zIndex: 1,
}}
unmountOnExit
>
<IconButton
icon={<TrashIcon />}
aria-label="Remove cell"
onClick={deleteItem(itemIndex)}
size="sm"
shadow="md"
/>
</Fade>
</Flex>
</Box>
))}
<Button
leftIcon={<PlusIcon />}
onClick={createItem}
flexShrink={0}
colorScheme="blue"
>
{addLabel}
</Button>
</Stack>
)
}

View File

@ -109,7 +109,7 @@ export const NumberInput = <HasVariable extends boolean>({
as={direction === 'column' ? Stack : HStack}
isRequired={isRequired}
justifyContent="space-between"
width={label ? 'full' : 'auto'}
width={label || props.width === 'full' ? 'full' : 'auto'}
spacing={direction === 'column' ? 2 : 3}
>
{label && (

View File

@ -35,6 +35,7 @@ export type TextInputProps = {
placeholder?: string
isDisabled?: boolean
direction?: 'row' | 'column'
width?: 'full'
} & Pick<
InputProps,
| 'autoComplete'
@ -66,6 +67,7 @@ export const TextInput = forwardRef(function TextInput(
size,
maxWidth,
direction = 'column',
width,
}: TextInputProps,
ref
) {
@ -141,7 +143,7 @@ export const TextInput = forwardRef(function TextInput(
isRequired={isRequired}
as={direction === 'column' ? Stack : HStack}
justifyContent="space-between"
width={label ? 'full' : 'auto'}
width={label || width === 'full' ? 'full' : 'auto'}
spacing={direction === 'column' ? 2 : 3}
>
{label && (

View File

@ -28,7 +28,7 @@ type Props = {
helperText?: ReactNode
onChange: (value: string) => void
direction?: 'row' | 'column'
} & Pick<TextareaProps, 'minH'>
} & Pick<TextareaProps, 'minH' | 'width'>
export const Textarea = ({
id,
@ -43,6 +43,7 @@ export const Textarea = ({
minH,
helperText,
direction = 'column',
width,
}: Props) => {
const inputRef = useRef<HTMLTextAreaElement | null>(null)
const [isTouched, setIsTouched] = useState(false)
@ -108,7 +109,7 @@ export const Textarea = ({
isRequired={isRequired}
as={direction === 'column' ? Stack : HStack}
justifyContent="space-between"
width={label ? 'full' : 'auto'}
width={label || width === 'full' ? 'full' : 'auto'}
spacing={direction === 'column' ? 2 : 3}
>
{label && (

View File

@ -46,6 +46,7 @@ type Props = {
helperText?: ReactNode
moreInfoTooltip?: string
direction?: 'row' | 'column'
width?: 'full'
} & Omit<InputProps, 'placeholder'>
export const VariableSearchInput = ({
@ -58,6 +59,7 @@ export const VariableSearchInput = ({
moreInfoTooltip,
direction = 'column',
isRequired,
width,
...inputProps
}: Props) => {
const focusedItemBgColor = useColorModeValue('gray.200', 'gray.700')
@ -196,7 +198,7 @@ export const VariableSearchInput = ({
isRequired={isRequired}
as={direction === 'column' ? Stack : HStack}
justifyContent="space-between"
width={label ? 'full' : 'auto'}
width={label || width === 'full' ? 'full' : 'auto'}
spacing={direction === 'column' ? 2 : 3}
>
{label && (

View File

@ -25,6 +25,7 @@ type Props = {
moreInfoTooltip?: string
direction?: 'row' | 'column'
isRequired?: boolean
width?: 'full'
onChange: (value: string | undefined) => void
}
export const ForgeSelectInput = ({
@ -38,6 +39,7 @@ export const ForgeSelectInput = ({
moreInfoTooltip,
isRequired,
direction = 'column',
width,
onChange,
}: Props) => {
const { workspace } = useWorkspace()
@ -83,7 +85,7 @@ export const ForgeSelectInput = ({
isRequired={isRequired}
as={direction === 'column' ? Stack : HStack}
justifyContent="space-between"
width={label ? 'full' : 'auto'}
width={label || width === 'full' ? 'full' : 'auto'}
spacing={direction === 'column' ? 2 : 3}
>
{label && (

View File

@ -31,10 +31,8 @@ export const ZodDiscriminatedUnionLayout = ({
currentItem={data?.[discriminant]}
onItemSelect={(item) => onDataChange({ ...data, [discriminant]: item })}
items={
[...schema._def.optionsMap.keys()].filter(
(key) =>
isDefined(key) &&
!schema._def.optionsMap.get(key)?._def.layout?.isHidden
[...schema._def.optionsMap.keys()].filter((key) =>
isDefined(key)
) as string[]
}
placeholder={dropdownPlaceholder}

View File

@ -13,12 +13,15 @@ import {
AccordionIcon,
AccordionItem,
AccordionPanel,
FormLabel,
Stack,
Text,
} from '@chakra-ui/react'
import { VariableSearchInput } from '@/components/inputs/VariableSearchInput'
import { DropdownList } from '@/components/DropdownList'
import { ForgedBlockDefinition, ForgedBlock } from '@typebot.io/forge-schemas'
import { PrimitiveList } from '@/components/PrimitiveList'
import { SwitchWithLabel } from '@/components/inputs/SwitchWithLabel'
const mdComponents = {
a: ({ href, children }) => (
@ -40,6 +43,8 @@ export const ZodFieldLayout = ({
isInAccordion,
blockDef,
blockOptions,
width,
propName,
onDataChange,
}: {
data: any
@ -47,6 +52,8 @@ export const ZodFieldLayout = ({
isInAccordion?: boolean
blockDef?: ForgedBlockDefinition
blockOptions?: ForgedBlock['options']
width?: 'full'
propName?: string
onDataChange: (val: any) => void
}) => {
const layout = schema._def.layout as ZodLayoutMetadata<ZodTypeAny> | undefined
@ -54,7 +61,6 @@ export const ZodFieldLayout = ({
? schema._def.innerType._def.typeName
: schema._def.typeName
if (layout?.isHidden) return null
switch (type) {
case 'ZodObject':
return (
@ -125,6 +131,7 @@ export const ZodFieldLayout = ({
moreInfoTooltip={layout?.moreInfoTooltip}
placeholder={layout?.placeholder}
direction={layout?.direction}
width={width}
/>
)
}
@ -143,6 +150,18 @@ export const ZodFieldLayout = ({
isRequired={layout?.isRequired}
moreInfoTooltip={layout?.moreInfoTooltip}
onValueChange={onDataChange}
direction={layout?.direction}
width={width}
/>
)
}
case 'ZodBoolean': {
return (
<SwitchWithLabel
label={layout?.label ?? propName ?? ''}
initialValue={data ?? layout?.defaultValue}
onCheckChange={onDataChange}
moreInfoContent={layout?.moreInfoTooltip}
/>
)
}
@ -166,10 +185,11 @@ export const ZodFieldLayout = ({
}
moreInfoTooltip={layout?.moreInfoTooltip}
onChange={onDataChange}
width={width}
/>
)
}
if (layout?.input === 'variableDropdown') {
if (layout?.inputType === 'variableDropdown') {
return (
<VariableSearchInput
initialVariableId={data}
@ -184,10 +204,11 @@ export const ZodFieldLayout = ({
</Markdown>
) : undefined
}
width={width}
/>
)
}
if (layout?.input === 'textarea') {
if (layout?.inputType === 'textarea') {
return (
<Textarea
defaultValue={data ?? layout?.defaultValue}
@ -204,6 +225,7 @@ export const ZodFieldLayout = ({
withVariableButton={layout?.withVariableButton}
moreInfoTooltip={layout.moreInfoTooltip}
onChange={onDataChange}
width={width}
/>
)
}
@ -217,11 +239,12 @@ export const ZodFieldLayout = ({
<Markdown components={mdComponents}>{layout.helperText}</Markdown>
) : undefined
}
type={layout?.input === 'password' ? 'password' : undefined}
type={layout?.inputType === 'password' ? 'password' : undefined}
isRequired={layout?.isRequired}
withVariableButton={layout?.withVariableButton}
moreInfoTooltip={layout?.moreInfoTooltip}
onChange={onDataChange}
width={width}
/>
)
}
@ -240,7 +263,36 @@ const ZodArrayContent = ({
layout: ZodLayoutMetadata<ZodTypeAny> | undefined
isInAccordion?: boolean
onDataChange: (val: any) => void
}) => (
}) => {
const type = schema._def.innerType
? schema._def.innerType._def.typeName
: schema._def.typeName
if (type === 'ZodString' || type === 'ZodNumber' || type === 'ZodEnum')
return (
<Stack spacing={0}>
{layout?.label && <FormLabel>{layout.label}</FormLabel>}
<Stack p="4" rounded="md" flex="1" borderWidth="1px">
<PrimitiveList
onItemsChange={(items) => {
onDataChange(items)
}}
initialItems={data}
addLabel={`Add ${layout?.itemLabel ?? ''}`}
>
{({ item, onItemChange }) => (
<ZodFieldLayout
schema={schema._def.innerType._def.type}
data={item}
isInAccordion={isInAccordion}
onDataChange={onItemChange}
width="full"
/>
)}
</PrimitiveList>
</Stack>
</Stack>
)
return (
<TableList
onItemsChange={(items) => {
onDataChange(items)
@ -261,3 +313,4 @@ const ZodArrayContent = ({
)}
</TableList>
)
}

View File

@ -98,6 +98,7 @@ export const ZodObjectLayout = ({
data={data?.[key]}
blockDef={blockDef}
blockOptions={blockOptions}
propName={key}
onDataChange={(val) => onDataChange({ ...data, [key]: val })}
/>,
],

View File

@ -1,84 +0,0 @@
---
title: "Create User"
api: "POST https://api.mintlify.com/api/user"
description: "This endpoint creates a new user"
---
### Body
<ParamField body="current_token" type="string">
This is the current user group token you have for the user group that you want
to rotate.
</ParamField>
### Response
<ResponseField name="success" type="number">
Indicates whether the call was successful. 1 if successful, 0 if not.
</ResponseField>
<ResponseField name="user_group" type="object">
The contents of the user group
<Expandable title="Toggle object">
<ResponseField name="team_id" type="number">
This is the internal ID for this user group. You don't need to record this
information, since you will not need to use it.
</ResponseField>
<ResponseField name="token" type="string">
This is the user group token (userGroupToken or USER_GROUP_TOKEN) that will be
used to identify which user group is viewing the dashboard. You should save
this on your end to use when rendering an embedded dashboard.
</ResponseField>
<ResponseField name="name" type="string">
This is the name of the user group provided in the request body.
</ResponseField>
<ResponseField name="provided_id" type="string">
This is the user_group_id provided in the request body.
</ResponseField>
<ResponseField name="api_environment_tag" type="JSON or null">
This is the environment tag of the user group. Possible values are 'Customer'
and 'Testing'. User group id's must be unique to each environment, so you can
not create multiple user groups with with same id. If you have a production
customer and a test user group with the same id, you will be required to label
one as 'Customer' and another as 'Testing'
</ResponseField>
</Expandable>
</ResponseField>
<RequestExample>
```bash Example Request
curl --location --request POST 'https://api.mintlify.com/api/user' \
--header 'Content-Type: application/json' \
--header 'Authorization: Token <token>' \
--data-raw '{
"current_token": ""
}'
```
</RequestExample>
<ResponseExample>
```json Response
{
"success": 1,
"user_group": {
"team_id": 3,
"token": "<user_group_token_to_auth_dashboard>",
"name": "Example 1",
"provided_id": "example_1"
}
}
```
</ResponseExample>

View File

@ -1,47 +0,0 @@
---
title: "Delete User"
api: "DELETE https://api.mintlify.com/api/user"
description: "This endpoint deletes an existing user."
---
### Body
<ParamField body="data_source_provided_id" type="string">
The data source ID provided in the data tab may be used to identify the data
source for the user group
</ParamField>
<ParamField body="current_token" type="string">
This is the current user group token you have for the user group you want to
delete
</ParamField>
### Response
<ResponseField name="success" type="number">
Indicates whether the call was successful. 1 if successful, 0 if not.
</ResponseField>
<RequestExample>
```bash Example Request
curl --location --request DELETE 'https://api.mintlify.com/api/user' \
--header 'Content-Type: application/json' \
--header 'Authorization: Token <token>' \
--data-raw '{
"user_group_id": "example_1"
"current_token": "abcdef"
}'
```
</RequestExample>
<ResponseExample>
```json Response
{
"success": 1
}
```
</ResponseExample>

View File

@ -1,101 +0,0 @@
---
title: "Get User"
api: "GET https://api.mintlify.com/api/user"
description: "This endpoint gets or creates a new user."
---
### Body
<ParamField body="name" type="string">
This is the name of the user group.
</ParamField>
<ParamField body="user_group_id" type="string">
This is the ID you use to identify this user group in your database.
</ParamField>
<ParamField body="mapping" type="object">
This is a JSON mapping of schema id to either the data source that this user group should be
associated with or id of the datasource you provided when creating it.
</ParamField>
<ParamField body="properties" type="object">
This is a JSON object for properties assigned to this user group. These will be accessible through
variables in the dashboards and SQL editor
</ParamField>
### Response
<ResponseField name="success" type="number">
Indicates whether the call was successful. 1 if successful, 0 if not.
</ResponseField>
<ResponseField name="new_user_group" type="boolean">
Indicates whether a new user group was created.
</ResponseField>
<ResponseField name="user_group" type="object">
The contents of the user group
<Expandable title="Toggle object">
<ResponseField name="team_id" type="number">
This is the internal ID for this user group. You don't need to record this information, since
you will not need to use it.
</ResponseField>
<ResponseField name="token" type="string">
This is the user group token (userGroupToken or USER_GROUP_TOKEN) that will be used to identify
which user group is viewing the dashboard. You should save this on your end to use when rendering
an embedded dashboard.
</ResponseField>
<ResponseField name="name" type="string">
This is the name of the user group provided in the request body.
</ResponseField>
<ResponseField name="provided_id" type="string">
This is the user_group_id provided in the request body.
</ResponseField>
<ResponseField name="properties" type="JSON or null">
This is the properties object if it was provided in the request body
</ResponseField>
</Expandable>
</ResponseField>
<RequestExample>
```bash Example Request
curl --location --request GET 'https://api.mintlify.com/api/user' \
--header 'Content-Type: application/json' \
--header 'Authorization: Token <token>' \
--data-raw '{
"user_group_id": "example_1",
"name": "Example 1",
"mapping": {"40": "213", "134": "386"},
"properties": {"filterValue": "value"}
}'
```
</RequestExample>
<ResponseExample>
```json Response
{
"success": 1,
"new_user_group": true,
"user_group": {
"team_id": 3,
"token": "<user_group_token_to_auth_dashboard>",
"name": "Example 1",
"provided_id": "example_1"
}
}
```
</ResponseExample>

View File

@ -1,101 +0,0 @@
---
title: "Update User"
api: "PUT https://api.mintlify.com/api/user"
description: "This endpoint updates an existing user."
---
### Body
<ParamField body="name" type="string">
This is the name of the user group.
</ParamField>
<ParamField body="user_group_id" type="string">
This is the ID you use to identify this user group in your database.
</ParamField>
<ParamField body="mapping" type="object">
This is a JSON mapping of schema id to either the data source that this user
group should be associated with or id of the datasource you provided when
creating it.
</ParamField>
<ParamField body="properties" type="object">
This is a JSON object for properties assigned to this user group. These will
be accessible through variables in the dashboards and SQL editor
</ParamField>
### Response
<ResponseField name="success" type="number">
Indicates whether the call was successful. 1 if successful, 0 if not.
</ResponseField>
<ResponseField name="user_group" type="object">
The contents of the user group
<Expandable title="Toggle object">
<ResponseField name="team_id" type="number">
Indicates whether a new user group was created.
</ResponseField>
<ResponseField name="token" type="string">
This is the user group token (userGroupToken or USER_GROUP_TOKEN) that will be
used to identify which user group is viewing the dashboard. You should save
this on your end to use when rendering an embedded dashboard.
</ResponseField>
<ResponseField name="name" type="string">
This is the name of the user group provided in the request body.
</ResponseField>
<ResponseField name="provided_id" type="string">
This is the user_group_id provided in the request body.
</ResponseField>
<ResponseField name="properties" type="JSON | Null">
This is the properties object if it was provided in the request body
</ResponseField>
<ResponseField name="api_environment_tag" type="JSON or null">
This is the environment tag of the user group. Possible values are 'Customer'
and 'Testing'
</ResponseField>
</Expandable>
</ResponseField>
<RequestExample>
```bash Example Request
curl --location --request PUT 'https://api.mintlify.com/api/user' \
--header 'Content-Type: application/json' \
--header 'Authorization: Token <token>' \
--data-raw '{
"user_group_id": "example_1",
"name": "Example 1",
"mapping": {"40": "213", "134": "386"},
"properties": {"filterValue": "value"}
}'
```
</RequestExample>
<ResponseExample>
```json Response
{
"success": 1,
"user_group": {
"team_id": 113,
"token": "<user_group_token_to_auth_dashboard>",
"name": "ok",
"provided_id": "6"
}
}
```
</ResponseExample>

View File

@ -25,7 +25,6 @@ title: Breaking changes
>
<iframe
src="https://www.loom.com/embed/df5d64dd01ca47daa5b7acd18b05a725?sid=e6df2f5b-643c-4175-8351-03e1726b2749"
mozallowfullscreen
allowFullScreen
style={{
position: 'absolute',

View File

@ -0,0 +1,59 @@
---
title: Create a new block
icon: screwdriver-wrench
---
<Frame style={{ maxWidth: '400px' }}>
<img
src="/images/contribute/forging-robot.png"
alt="A blue robot forging a new block"
/>
</Frame>
Creating a new block on Typebot is pretty easy and straightforward using our in-house framework [The Forge](../the-forge/overview).
1. [Install the project locally](./local-installation)
2. Create a new branch:
```sh
git checkout -b MY_BRANCH_NAME
```
3. Create your new block using the [Forge CLI](../the-forge/overview#forge-cli):
```sh
pnpm run create-new-block
```
4. The files should be generated in `packages/forge/blocks/YOUR_BLOCK_NAME`
5. Add the block SVG logo in `packages/forge/blocks/YOUR_BLOCK_NAME/logo.tsx`
6. Right away you should be able to [run the application](./local-installation#running-the-project-locally) and see your newly created logo in the sidebar of the editor.
7. Create a new action in the `packages/forge/blocks/YOUR_BLOCK_NAME/actions` folder. See [Action](../the-forge/action) for more information.
8. List this action in the `actions` array in `packages/forge/blocks/YOUR_BLOCK_NAME/index.tsx`
9. To go further, check out the [Forge documentation](../the-forge/overview).
Make sure to check out other blocks implementations in the [packages/forge/blocks](https://github.com/baptisteArno/typebot.io/tree/main/packages/forge/blocks) folder.
## Live tutorials
<div
style={{
position: 'relative',
paddingBottom: '64.63195691202873%',
height: 0,
}}
>
<iframe
src="https://www.loom.com/embed/c49ced0c2c394751b860458b7eb904a4?sid=58e36bc9-f715-434c-b85c-e5acf6c96454"
allowFullScreen
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
}}
></iframe>
</div>
Make sure to join our [Discord community](https://typebot.io/discord) to participate to these weekly office hours.

View File

@ -0,0 +1,53 @@
---
title: 'Contribute to the documentation'
sidebarTitle: 'Documentation'
image: '/images/contribute/writing-robot.png'
icon: 'book'
---
<Frame style={{ maxWidth: '400px' }}>
<img
src="/images/contribute/writing-robot.png"
alt="A blue robot writing on a paper"
/>
</Frame>
## For a quick fix
If the documentation is missing something, or you found a typo you can quickly edit the documentation:
1. Go to the documentation page you want to edit.
2. Click on the "Suggest edits" button at the bottom of the page.
3. You are redirected to a Github page where you can edit the content of the doc.
4. If you did not already have a fork of the repository, you will be prompted to create one.
5. Edit the content of the doc.
6. Hit "Commit changes...".
7. Click on "Create pull request".
8. Add a title and a description to describe your changes.
9. Click on "Create pull request".
It will be reviewed and merged if approved!
## For a bigger modification
If you'd like to add a new page or add a new section to the documentation:
1. [Install the project locally](./local-installation)
2. Create a new branch:
```sh
git checkout -b MY_BRANCH_NAME
```
3. Run the docs in dev mode
```sh
cd apps/docs
pnpm dev
```
4. All your docs modification will be displayed in real time.
5. Once you are done, commit your changes and push your branch.
6. Create a pull request on the [Github repository](https://github.com/baptisteArno/typebot.io).
It will be reviewed and merged if approved!

View File

@ -0,0 +1,61 @@
---
title: 'Install the project locally'
sidebarTitle: 'Local installation'
image: '/images/contribute/writing-robot.png'
icon: 'laptop'
---
## Get started
1. [Fork](https://help.github.com/articles/fork-a-repo/) this repository to your
own GitHub account and then
[clone](https://help.github.com/articles/cloning-a-repository/) it to your local device.
## Running the project locally
1. Install dependencies
```sh
cd typebot.io
pnpm i
```
2. Set up environment variables
Copy `.env.dev.example` to `.env`
Check out the [Configuration guide](https://docs.typebot.io/self-hosting/configuration) if you want to enable more options
3. Make sure you have [Docker](https://docs.docker.com/compose/install/) running
4. Make sure you have Node.js installed. I suggest you use [`nvm`](https://github.com/nvm-sh/nvm) allowing you to manage different versions. Once you installed nvm, you can install and use the latest version of Node.js: `nvm install && nvm use`
5. Start the builder and viewer
```sh
pnpm dev
```
Builder is available at [`http://localhost:3000`](http://localhost:3000)
Viewer is available at [`http://localhost:3001`](http://localhost:3001)
Database inspector is available at [`http://localhost:5555`](http://localhost:5555)
By default, you can easily authenticate in the builder using the "Github Sign In" button. For other options, check out the [Configuration guide](https://docs.typebot.io/self-hosting/configuration)
6. (Optionnal) Start the landing page
Copy `apps/landing-page/.env.local.example` to `apps/landing-page/.env.local`
```sh
cd apps/landing-page
pnpm dev
```
7. (Optionnal) Start the docs
```sh
cd apps/docs
pnpm start
```

View File

@ -0,0 +1,25 @@
---
title: 'Translate'
image: '/images/contribute/writing-robot.png'
icon: 'globe'
---
<Frame style={{ maxWidth: '400px' }}>
<img
src="/images/contribute/translating-robot.png"
alt="A blue robot translating on a paper"
/>
</Frame>
You speak a language other than English? You can help us translate the application! And it's easy! We use [Tolgee](https://tolgee.io/) to manage translations. It's a great tool that allows you to translate directly in the browser.
All you need to do is to join the [Discord server](https://discord.gg/xjyQczWAXV) and ask for an access to Tolgee in the [#contributors](https://discord.com/channels/1155799591220953138/1155883114455900190) channel.
You'll have a granted access and will be able to translate existing keys directly in the Tolgee platform:
<Frame>
<img
src="/images/contribute/tolgee.png"
alt="Tolgee translate page preview"
/>
</Frame>

View File

@ -0,0 +1,71 @@
---
title: Contribute
---
You are considering contributing to Typebot. I and the Open-source community thank you for this 🙏.
Any contributions you make are **greatly appreciated**. There are many ways to contribute, from improving the documentation, submitting bug reports and feature requests or writing code which can be incorporated into Typebot itself.
<Card
title="Implement a new block"
icon="screwdriver-wrench"
iconType="duotone"
href="./the-forge/overview"
>
Use the Forge. Our in-house framework to easily add new blocks on Typebot.
Help us have a gigantic library of third-party native integrations on Typebot.
</Card>
<Card
title="Translate Typebot"
icon="globe"
iconType="duotone"
color="#822B55"
href="./guides/translation"
>
Use our framework "The Forge" to easily implement a new block. Help us have a
gigantic library of third-party native integrations on Typebot.
</Card>
<Card
title="Improve the documentation"
icon="book"
iconType="duotone"
href="./guides/documentation"
color="#97A0B1"
>
Help us improve the documentation by fixing typos, adding missing information
or proposing new sections.
</Card>
<Card
title="Report issues"
icon="bug"
iconType="duotone"
color="#F63838"
href="https://github.com/baptisteArno/typebot.io/issues/new"
>
Report issues you encounter while using Typebot. This will help us improve the
product and make it even better.
</Card>
<Card
title="Engage and help others on Discord"
icon="discord"
color="#5765F2"
href="https://typebot.io/discord"
>
Engaging with the community and helping others is a great way to contribute to
the project. We'd love to see you on our Discord server. ❤️
</Card>
<Card
title="Sponsor the project"
icon="money-bill"
iconType="duotone"
color="#5AB034"
href="https://github.com/sponsors/baptisteArno"
>
If you don't have time to contribute to the project. You can still show your
appreciation for the project and sponsor my work on Github.
</Card>

View File

@ -0,0 +1,125 @@
---
title: Action
---
An action is what a block can do when it is executed with Typebot. A [block](./block) can have multiple actions.
Here is the `sendMessage` action of the Telegram block:
```ts
import { createAction, option } from '@typebot.io/forge'
import { auth } from '../auth'
import { got } from 'got'
export const sendMessage = createAction({
auth,
name: 'Send message',
options: option.object({
chatId: option.string.layout({
label: 'Chat ID',
placeholder: '@username',
}),
text: option.string.layout({
label: 'Message text',
input: 'textarea',
}),
}),
run: {
server: async ({ credentials: { token }, options: { chatId, text } }) => {
try {
await got.post(`https://api.telegram.org/bot${token}/sendMessage`, {
json: {
chat_id: chatId,
text,
},
})
} catch (error) {
console.log('ERROR', error.response.body)
}
},
},
})
```
<Frame>
<img src="/images/contribute/action-example.png" alt="Action example" />
</Frame>
## Props
<ResponseField name="name" type="string" required>
The name of the action.
</ResponseField>
<ResponseField name="auth" type="Auth">
If the block requires authentication, the auth object needs to be passed to
the action.
</ResponseField>
<ResponseField name="baseOptions" type="z.ZodObject<any>">
If the block has options defined (see [block props](./block#props)), this
needs to be provided here.
</ResponseField>
<ResponseField name="options" type="z.ZodObject<any>">
The action configuration options. See <a href="./options">Options</a> for more
information.
</ResponseField>
<ResponseField name="run">
Check out the <a href="./run">Run</a> documentation for more information on how this can be configured depending of your scenario.
<Expandable title="properties">
<ResponseField name="server" type="ServerRunFunction">
The function to execute on the server when the block is triggered. See{' '}
<a href="./run">Run</a> for more information.
</ResponseField>
<ResponseField name="stream">
<Expandable title="properties">
<ResponseField
name="getStreamVariableId"
type="(options) => string | undefined"
>
A function that returns the variable ID to stream.
</ResponseField>
<ResponseField name="run" type="StreamRunFunction">
The function to execute to stream the variable.
</ResponseField>
</Expandable>
</ResponseField>
<ResponseField name="web">
<Expandable title="properties">
<ResponseField
name="displayEmbedBubble"
type="(options) => string | undefined"
>
<Expandable title="properties">
<ResponseField
name="parseInitFunction"
type="WebFunctionParser"
>
See <a href="./run#client-function">Client function</a> for more information.
</ResponseField>
<ResponseField name="waitForEvent">
<Expandable title="properties">
<ResponseField
name="getSaveVariableId"
type="(options) => string | undefined"
>
A function that returns the variable ID to save as the result of the `waitForEvent` function.
</ResponseField>
<ResponseField name="parseFunction" type="WebFunctionParser">
See <a href="./run#client-function">Client function</a> for more information.
</ResponseField>
</Expandable>
</ResponseField>
</Expandable>
</ResponseField>
<ResponseField name="parseFunction" type="WebFunctionParser">
A function that parses the function to execute on the client. See <a href="./run#client-function">Client function</a> for more information.
</ResponseField>
</Expandable>
</ResponseField>
</Expandable>
</ResponseField>

View File

@ -0,0 +1,32 @@
---
title: Auth
---
## Encrypted credentials
<ResponseField name="type" type="'encryptedCredentials'" required>
The authentication type.
</ResponseField>
<ResponseField name="name" type="string" required>
The name of the credentials. I.e. `Twitter account`, `OpenAI account`, `Stripe
keys`, etc.
</ResponseField>
<ResponseField name="schema" type="z.ZodObject<any>" required>
The schema of the data that needs to be stored. See [Options](./options) for
more information.
Example:
```ts
option.object({
apiKey: option.string.layout({
isRequired: true,
label: 'API key',
withVariableButton: false,
}),
})
```
</ResponseField>

View File

@ -0,0 +1,77 @@
---
title: Block
---
After using the [CLI](./cli) to create your new block. The `index.ts` file contains the block definition. It should look like:
<Frame>
<img
src="/images/contribute/block-name-logo-match.png"
alt="Block name and logo match"
/>
</Frame>
## Props
<ResponseField name="id" type="string" required>
The block ID. It should be unique all the other blocks.
</ResponseField>
<ResponseField name="name" type="string" required>
A concise name for the block. Should be short enough to fit into a small block
card.
</ResponseField>
<ResponseField
name="LightLogo"
type="(props: React.SVGProps<SVGSVGElement>) => React.JSX.Element"
required
></ResponseField>
<ResponseField
name="DarkLogo"
type="(props: React.SVGProps<SVGSVGElement>) => React.JSX.Element"
>
Logo used on the dark mode.
</ResponseField>
<ResponseField name="fullName" type="string">
The full name that will be displayed as a tooltip when the mouse hovers the
block card.
</ResponseField>
<ResponseField name="tags" type="string[]">
List of strings describing the block. Used for block searching.
</ResponseField>
<ResponseField name="auth" type="Auth">
See <a href="./auth">Auth</a> for more information.
</ResponseField>
<ResponseField name="actions" type="Action[]">
A list of all the possible actions that this block provides. See{' '}
<a href="./action">Action</a> for more information
</ResponseField>
<ResponseField name="docsUrl" type="string">
The dedicated documentation URL. (i.e.
`https://docs.typebot.io/editor/blocks/integrations/openai`)
</ResponseField>
<ResponseField name="options" type="z.ZodObject<any>">
Provide it if all the block actions share the same properties. See{' '}
<a href="./options">Options</a> for more information. In the block settings it
will then be displayed between the auth and the actions:
{' '}
<Frame style={{ maxWidth: '600px' }}>
<img src="/images/contribute/base-options.png" alt="Block options" />
</Frame>
</ResponseField>
<ResponseField name="fetchers" type="Fetcher[]">
A list of fetchers used in the provided <code>options</code>. See{' '}
<a href="./fetcher">Fetcher</a> for more information.
</ResponseField>

View File

@ -0,0 +1,27 @@
---
title: CLI
---
The CLI allows you to bootstrap a new block, creating all the necessary files and folders to get started.
It asks you a few questions about your block, and then creates the files and folders for you.
<div
style={{
position: 'relative',
paddingBottom: '64.63195691202873%',
height: 0,
}}
>
<iframe
src="https://www.loom.com/embed/5773d3a1b3eb4613bd373612f84fde31?sid=ba9fb00c-0ca6-4a7c-a4c1-5abb6e54cdec"
allowFullScreen
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
}}
></iframe>
</div>

View File

@ -0,0 +1,53 @@
---
title: Fetcher
---
Your action can have a `fetchers` property that is an array of fetcher objects.
Example:
```ts
export const createChatCompletion = createAction({
//...
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) ?? []
)
},
},
],
})
```
A fetcher object has the following properties: `id`, `dependencies`, and `fetch`.
`id` is a string that uniquely identifies the fetcher. When you want an option field to be populated by the fetcher, you can use the fetcher's id as the value of the option field.
`dependencies` is an array of strings that are the names of the option fields that the fetcher depends on. When any of the option fields in the `dependencies` array changes, the fetcher will be re-run.
`fetch` is an async function that takes an object with the following properties: `credentials`, `options` and returns a list of strings. It can also return a list of objects with the following properties: `value`, `label`. The `value` property is the value of the option field and the `label` property is the label of the option field. (`Promise<(string | { label: string; value: string })[]>`)

View File

@ -0,0 +1,32 @@
---
title: Layout
---
## Props
<ResponseField name="label" type="string">
The label of the field. It is displayed right on top of the input.
<Frame style={{ maxWidth: '400px' }}>
<img src="/images/contribute/layout-label.png" alt="Latout label" />
</Frame>
</ResponseField>
<ResponseField name="placeholder" type="string">
The placeholder of the associated input.
<Frame style={{ maxWidth: '400px' }}>
<img
src="/images/contribute/layout-placeholder.png"
alt="Layout placeholder"
/>
</Frame>
</ResponseField>
<ResponseField name="helperText" type="string">
The placeholder of the associated input.
<Frame style={{ maxWidth: '400px' }}>
<img
src="/images/contribute/layout-placeholder.png"
alt="Layout placeholder"
/>
</Frame>
</ResponseField>

View File

View File

@ -0,0 +1,295 @@
---
title: Options
---
The Forge library extends the [Zod](https://github.com/colinhacks/zod) library. This means that you can use any Zod schema to validate the data that you want to store for your block. The Forge extends the zod schemas with the `layout` function property that allows you to define the layout of the data in the Typebot editor.
Options extends the `z.ZodObject<any>` schema.
The Forge provide convenient functions to create the options schema for you. Even though you could provide straight Zod schemas, we highly recommend using `option` functions to create the options schema.
Here is an example of how a schema can be created using the `option` and `layout` functions:
```ts
option.object({
token: option.string.layout({
label: 'Token',
isRequired: true,
placeholder: 'Type your token...',
helperText: 'You can find your token [here](https://).',
}),
role: option.enum(['user', 'admin']).layout({
defaultValue: 'user',
label: 'Role',
}),
phoneNumber: option.string.layout({
accordion: 'Advanced settings',
label: 'Phone number',
placeholder: 'Type your phone number...',
}),
address: option.string.layout({
accordion: 'Advanced settings',
label: 'Address',
placeholder: 'Type your address...',
}),
isTestModeEnabled: option.boolean.layout({
label: 'Test mode',
defaultValue: true,
helperText: 'Enable test mode to use a test account.',
}),
})
```
<Frame>
<img src="/images/contribute/layout-example.png" alt="Layout label" />
</Frame>
## Object
```ts
option.object({
//...
})
```
## String
Example:
```ts
option.string.layout({
label: 'Name',
placeholder: 'Type a name...',
withVariableButton: false,
})
```
<Frame>
<img
src="/images/contribute/layout-string-example.png"
alt="Layout string example"
/>
</Frame>
## Number
Example:
```ts
option.number.layout({
label: 'Temperature',
defaultValue: 1,
direction: 'row',
})
```
<Frame>
<img
src="/images/contribute/layout-number-example.png"
alt="Layout number example"
/>
</Frame>
## Boolean
```ts
option.boolean.layout({
label: 'Test mode',
moreInfoTooltip: 'Enable test mode to use a test account.',
})
```
{' '}
<Frame>
<img
src="/images/contribute/layout-boolean-example.png"
alt="Layout boolean example"
/>
</Frame>
## Enum
Example:
```ts
option.enum(['user', 'admin']).layout({
label: 'Role',
defaultValue: 'user',
})
```
<Frame>
<img
src="/images/contribute/layout-enum-example.png"
alt="Layout enum example"
/>
</Frame>
## Discriminated unions
Example:
```ts
option.discriminatedUnion('type', [
option.object({
type: option.literal('user'),
name: option.string.layout({ placeholder: 'Type a name...' }),
}),
option.object({
type: option.literal('admin'),
name: option.string.layout({ placeholder: 'Type a name...' }),
phoneNumber: option.string.layout({
placeholder: 'Type a phone number...',
}),
}),
])
```
<Frame>
<img
src="/images/contribute/layout-discriminated-union-example.png"
alt="Layout discriminated union example"
/>
</Frame>
## Literal
Used mainly for [discriminated unions](./options#discriminated-unions). It is not visible on the Typebot editor.
Example:
```ts
option.literal('user')
```
## Array
Use this to collect a list of values.
Example:
- A list of names
```ts
option.array(option.string.layout({ placeholder: 'Type a name...' })).layout({
label: 'Names',
itemLabel: 'name',
})
```
<Frame>
<img
src="/images/contribute/layout-array-example.png"
alt="Layout array example"
/>
</Frame>
## Helpers
### Save Response Array
Use this to save the response of an array of options in variables.
For example if you want your user to be able to save the response of an HTTP request to variables:
```ts
option.saveResponseArray(['Message content', 'Total tokens']).layout({
accordion: 'Save response',
})
```
You provide the list of all the possible response values to save.
<Frame>
<img
src="/images/contribute/layout-save-response-example.png"
alt="Layout save response array example"
/>
</Frame>
## Layout props
<ResponseField name="label" type="string">
The label of the option. Will often be displayed right above the input.
</ResponseField>
<ResponseField name="placeholder" type="string">
The placeholder of the input.
</ResponseField>
<ResponseField name="helperText" type="string">
The helper text of the input. Will often be displayed below the input.
</ResponseField>
<ResponseField name="accordion" type="string">
The name of the accordion where the option will be displayed in. For example if you'd like to group 2 properties in the same accordion named "Advanced settings", you can write:
```ts
option.object({
temperature: option.number.layout({
accordion: 'Advanced settings',
}),
humidity: option.number.layout({
accordion: 'Advanced settings',
}),
})
```
</ResponseField>
<ResponseField name="direction" type="'row' | 'column'" default="column">
The direction of the input. If set to `row`, the label will be displayed on
the left of the input.
</ResponseField>
<ResponseField name="defaultValue" type="any">
The default value of the input.
</ResponseField>
<ResponseField name="moreInfoTooltip" type="string">
The tooltip that will be displayed when the user hovers the info icon of the
input.
</ResponseField>
<ResponseField name="withVariableButton" type="boolean" default="true">
Whether or not to display the variable button next to the input.
</ResponseField>
<ResponseField
name="inputType"
type="'variableDropdown' | 'textarea' | 'password'"
>
The type of the input to display.
</ResponseField>
<ResponseField name="isRequired" type="boolean" default="false">
Whether or not the input is required. Will display a red star next to the
label if set to `true`.
</ResponseField>
<ResponseField name="fetcher" type="string">
Set this if you'd like your input to provide a dropdown of dynamically fetched items.
```ts
option.string.layout({
fetcher: 'fetchModels',
})
```
`fetchModels` should match the `id` of the fetcher that you defined in the
`fetchers` prop of the action. See [Fetcher](./fetcher) for more information.
</ResponseField>
### Array only
<ResponseField name="itemLabel" type="string">
The label of the items of an array option. Will be displayed next to the "Add"
label of the add button.
</ResponseField>
<ResponseField name="isOrdered" type="boolean" default="false">
Whether or not the order of the items in an array schema matters. Will display
"plus" buttons above and below when hovering on an item.
</ResponseField>

View File

@ -0,0 +1,14 @@
---
title: Overview
---
<Frame style={{ maxWidth: '400px' }}>
<img
src="/images/contribute/the-forge.png"
alt="A retro future illustration of a forge"
/>
</Frame>
The Forge is a framework built by Typebot, for Typebot. It allows you to easily implement a new block.
This section goes through the different concepts of this framework and how to use it.

View File

@ -0,0 +1,252 @@
---
title: Run
---
An action can do one of the following things:
- Execute a function on the server (most common)
- If block is followed by a streamable variable, stream the variable on the client. Otherwise, execute a function on the server.
- Execute a function on the client
- Display a custom embed bubble
## Server function
The most common action is to execute a function on the server. This is done by simply declaring that function in the action block.
Example:
```ts
export const sendMessage = createAction({
// ...
run: {
server: async ({
credentials: { apiKey },
options: { botId, message, responseMapping, threadId },
variables,
logs,
}) => {
const res: ChatNodeResponse = await got
.post(apiBaseUrl + botId, {
headers: {
Authorization: `Bearer ${apiKey}`,
},
json: {
message,
chat_session_id: isEmpty(threadId) ? undefined : threadId,
},
})
.json()
if (res.error)
logs.add({
status: 'error',
description: res.error,
})
if (res.error)
logs.responseMapping?.forEach((mapping) => {
if (!mapping.variableId) return
const item = mapping.item ?? 'Message'
if (item === 'Message') variables.set(mapping.variableId, res.message)
if (item === 'Thread ID')
variables.set(mapping.variableId, res.chat_session_id)
})
},
},
})
```
As you can see the function takes `credentials`, `options`, `variables` and `logs` as arguments.
The `credentials` are the credentials that the user has entered in the credentials block.
The `options` are the options that the user has entered in the options block.
The `variables` object contains helper to save and get variables if necessary.
The `logs` allows you to log anything during the function execution. These logs will be displayed as toast in the preview mode or in the Results tab on production.
## Server function + stream
If your block can stream a message (like OpenAI), you can implement a stream object to handle it.
```ts
export const createChatCompletion = createAction({
// ...
run: {
server: async (params) => {
//...
},
stream: {
getStreamVariableId: (options) =>
options.responseMapping?.find(
(res) => res.item === 'Message content' || !res.item
)?.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)
},
},
},
})
```
The `getStreamVariableId` function defines which variable should be streamed.
The `run` works the same as the [server function](./run#server-function). It needs to return `Promise<ReadableStream<any> | undefined>`.
## Client function
If you want to execute a function on the client instead of the server, you can use the `client` object.
<Info>
This makes your block only compatible with the Web runtime. It won't work on
WhatsApp for example, the block will simply be skipped.
</Info>
Example:
```ts
export const shoutName = createAction({
// ...
run: {
web: ({ options }) => {
return {
args: {
name: options.name ?? null,
},
content: `alert('Hello ' + name)`,
}
},
},
})
```
The web function needs to return an object with `args` and `content`.
`args` is an object with arguments that are passed to the `content` context. Note that the arguments can't be `undefined`. If you want to pass an not defined argument, you need to pass `null` instead.
`content` is the code that will be executed on the client. It can call the arguments passed in `args`.
## Display embed bubble
If you want to display a custom embed bubble, you can use the `displayEmbedBubble` object. See [Cal.com
block](https://github.com/baptisteArno/typebot.io/blob/main/packages/forge/blocks/calCom/actions/bookEvent.ts)
as an example.
Example:
```ts
export const bookEvent = createAction({
// ...
run: {
web: {
displayEmbedBubble: {
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});`,
}
},
},
waitForEvent: {
getSaveVariableId: ({ saveBookedDateInVariableId }) =>
saveBookedDateInVariableId,
parseFunction: () => {
return {
args: {},
content: `Cal("on", {
action: "bookingSuccessful",
callback: (e) => {
continueFlow(e.detail.data.date)
}
})`,
}
},
},
},
},
})
```
The `displayEmbedBubble` accepts a `parseInitFunction` function. This function needs to return the same object as the [`web` function](./run#client-function). The function content can use the `typebotElement` variable to get the DOM element where the block is rendered.
Optionally you can also define a `waitForEvent` object. This object accepts a `getSaveVariableId` function that returns the variable id where the event data should be saved. It also accepts a `parseFunction` function that returns the same object as the [`web` function](./run#client-function). The function content can use the `continueFlow` function to continue the flow with the event data.

View File

@ -48,7 +48,6 @@ The generated audio URLs are temporary and expire after 7 days. If you need to s
>
<iframe
src="https://www.loom.com/embed/ccca6cbf16ed4d01b513836775db06a3?sid=22a54baa-000b-435a-b770-bfaf32bfe453"
mozallowfullscreen
allowFullScreen
style={{
position: 'absolute',
@ -78,8 +77,7 @@ I also demonstrate how formatting can be affected by the presence of text before
>
<iframe
src="https://www.loom.com/embed/35dc8af6b9244762acc4a5acf275fb43?sid=3723aa59-13ac-49f2-a95b-5608807ac76d"
mozallowfullscreen
allowfullscreen
allowFullScreen
style={{
position: 'absolute',
top: 0,

View File

@ -54,7 +54,6 @@ I demonstrate how to configure the webhook block, including the URL, method, and
>
<iframe
src="https://www.loom.com/embed/d9aef6a37e0c43759b31be7d69c4dd6d?sid=2a24096c-9e2a-48da-aa2a-61a89877afe7"
mozallowfullscreen
allowFullScreen
style={{
position: 'absolute',

View File

@ -15,11 +15,7 @@ This is the Typebot documentation. It's a great place to find most answers. Plea
<Card title="Deploy your bots" icon="comment" href="/deploy/web/overview">
Explore the different ways you can deploy your typebot.
</Card>
<Card
title="Contribute"
icon="laptop"
href="https://github.com/baptisteArno/typebot.io/blob/main/CONTRIBUTING.md"
>
<Card title="Contribute" icon="laptop" href="/contribute/overview">
Interested in creating a block or improve the app? Learn how to contribute.
</Card>
<Card title="Self-hosting" icon="server" href="/self-hosting/get-started">

View File

@ -19,8 +19,6 @@ https://redirect-site.com?utm_source={{utm_source}}&utm_value={{utm_value}}
>
<iframe
src="https://www.loom.com/embed/9b6cb65aff0a485e9e021b42310b207c?sid=2c61af7c-6aef-443e-b63e-941a079f2031"
mozallowfullscreen=""
allowFullScreen=""
style={{
position: 'absolute',
top: 0,

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

@ -229,7 +229,29 @@
},
{
"group": "Get started",
"pages": ["contributing/overview"]
"pages": ["contribute/overview"]
},
{
"group": "Guides",
"pages": [
"contribute/guides/local-installation",
"contribute/guides/create-block",
"contribute/guides/documentation",
"contribute/guides/translation"
]
},
{
"group": "The Forge",
"pages": [
"contribute/the-forge/overview",
"contribute/the-forge/cli",
"contribute/the-forge/block",
"contribute/the-forge/auth",
"contribute/the-forge/action",
"contribute/the-forge/run",
"contribute/the-forge/options",
"contribute/the-forge/fetcher"
]
},
{
"group": "Chat",

View File

@ -3,7 +3,7 @@
"version": "0.0.0",
"private": true,
"scripts": {
"dev": "mintlify dev",
"dev": "mintlify dev --port 3004",
"build": "pnpm api:generate && mintlify broken-links",
"api:generate": "dotenv -e ./.env -e ../../.env -- tsx --tsconfig ../builder/tsconfig.json ../builder/src/helpers/server/generateOpenApi.ts && dotenv -e ./.env -e ../../.env -- tsx --tsconfig ../viewer/openapi.tsconfig.json ../viewer/src/helpers/server/generateOpenApi.ts"
},

View File

@ -131,10 +131,7 @@ export const executeForgedBlock = async (
const clientSideActions: ExecuteIntegrationResponse['clientSideActions'] = []
if (
action?.run?.web?.parseFunction &&
(state.typebotsQueue[0].resultId || !blockDef.isDisabledInPreview)
) {
if (action?.run?.web?.parseFunction) {
clientSideActions.push({
type: 'codeToExecute',
codeToExecute: action?.run?.web?.parseFunction({

View File

@ -25,7 +25,7 @@ export const bookEvent = createAction({
}),
saveBookedDateInVariableId: option.string.layout({
label: 'Save booked date',
input: 'variableDropdown',
inputType: 'variableDropdown',
}),
}),
getSetVariableIds: ({ saveBookedDateInVariableId }) =>

View File

@ -23,7 +23,7 @@ export const sendMessage = createAction({
message: option.string.layout({
label: 'Message',
placeholder: 'Hi, what can I do with ChatNode',
input: 'textarea',
inputType: 'textarea',
}),
responseMapping: option.saveResponseArray(['Message', 'Thread ID']).layout({
accordion: 'Save response',

View File

@ -9,7 +9,8 @@ export const auth = {
isRequired: true,
helperText:
'You can generate an API key [here](https://go.chatnode.ai/typebot).',
input: 'password',
inputType: 'password',
withVariableButton: false,
}),
}),
} satisfies AuthDefinition

View File

@ -8,7 +8,10 @@ import { auth } from '../auth'
import { baseOptions } from '../baseOptions'
const nativeMessageContentSchema = {
content: option.string.layout({ input: 'textarea', placeholder: 'Content' }),
content: option.string.layout({
inputType: 'textarea',
placeholder: 'Content',
}),
}
const systemMessageItemSchema = option
@ -32,7 +35,7 @@ const assistantMessageItemSchema = option
const dialogueMessageItemSchema = option.object({
role: option.literal('Dialogue'),
dialogueVariableId: option.string.layout({
input: 'variableDropdown',
inputType: 'variableDropdown',
placeholder: 'Dialogue variable',
}),
startsBy: option.enum(['user', 'assistant']).layout({
@ -75,6 +78,7 @@ export const createChatCompletion = createAction({
name: 'Create chat completion',
auth,
baseOptions,
options,
getSetVariableIds: (options) =>
options.responseMapping?.map((res) => res.variableId).filter(isDefined) ??
[],
@ -177,5 +181,4 @@ export const createChatCompletion = createAction({
},
},
},
options,
})

View File

@ -19,14 +19,14 @@ export const createSpeech = createAction({
}),
input: option.string.layout({
label: 'Input',
input: 'textarea',
inputType: 'textarea',
}),
voice: option.enum(openAIVoices).layout({
label: 'Voice',
placeholder: 'Select a voice',
}),
saveUrlInVariableId: option.string.layout({
input: 'variableDropdown',
inputType: 'variableDropdown',
label: 'Save URL in variable',
}),
}),

View File

@ -29,13 +29,13 @@ export const searchDocuments = createAction({
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',
inputType: 'textarea',
}),
prompt: option.string.layout({
accordion: 'Advanced settings',
label: 'Prompt',
moreInfoTooltip: 'Prompt to send to the summarization LLM.',
input: 'textarea',
inputType: 'textarea',
}),
responseMapping: option
.saveResponseArray([

View File

@ -95,6 +95,7 @@ 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(),
boolean: z.boolean().optional(),
enum: <T extends string>(values: readonly [T, ...T[]]) =>
z.enum(values).optional(),
number: z.number().or(variableStringSchema).optional(),
@ -123,7 +124,7 @@ export const option = {
defaultValue: items[0],
}),
variableId: z.string().optional().layout({
input: 'variableDropdown',
inputType: 'variableDropdown',
}),
})
)

View File

@ -117,7 +117,6 @@ export type BlockDefinition<
auth?: Auth
options?: Options | undefined
fetchers?: FetcherDefinition<Auth, Options>[]
isDisabledInPreview?: boolean
actions: ActionDefinition<Auth, Options>[]
}

View File

@ -8,7 +8,7 @@ export interface ZodLayoutMetadata<
> {
accordion?: string
label?: string
input?: 'variableDropdown' | 'textarea' | 'password'
inputType?: 'variableDropdown' | 'textarea' | 'password'
defaultValue?: T extends ZodDate ? string : TInferred
placeholder?: string
helperText?: string
@ -18,7 +18,6 @@ export interface ZodLayoutMetadata<
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
}