📝 Add Contribute docs
108
CONTRIBUTING.md
@ -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). ❤️
|
||||
|
15
README.md
@ -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
|
||||
|
||||
|
@ -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 && (
|
||||
|
135
apps/builder/src/components/PrimitiveList.tsx
Normal 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>
|
||||
)
|
||||
}
|
@ -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 && (
|
||||
|
@ -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 && (
|
||||
|
@ -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 && (
|
||||
|
@ -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 && (
|
||||
|
@ -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 && (
|
||||
|
@ -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}
|
||||
|
@ -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,24 +263,54 @@ const ZodArrayContent = ({
|
||||
layout: ZodLayoutMetadata<ZodTypeAny> | undefined
|
||||
isInAccordion?: boolean
|
||||
onDataChange: (val: any) => void
|
||||
}) => (
|
||||
<TableList
|
||||
onItemsChange={(items) => {
|
||||
onDataChange(items)
|
||||
}}
|
||||
initialItems={data}
|
||||
addLabel={`Add ${layout?.itemLabel ?? ''}`}
|
||||
isOrdered={layout?.isOrdered}
|
||||
>
|
||||
{({ item, onItemChange }) => (
|
||||
<Stack p="4" rounded="md" flex="1" borderWidth="1px">
|
||||
<ZodFieldLayout
|
||||
schema={schema._def.innerType._def.type}
|
||||
data={item}
|
||||
isInAccordion={isInAccordion}
|
||||
onDataChange={onItemChange}
|
||||
/>
|
||||
}) => {
|
||||
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>
|
||||
)}
|
||||
</TableList>
|
||||
)
|
||||
)
|
||||
return (
|
||||
<TableList
|
||||
onItemsChange={(items) => {
|
||||
onDataChange(items)
|
||||
}}
|
||||
initialItems={data}
|
||||
addLabel={`Add ${layout?.itemLabel ?? ''}`}
|
||||
isOrdered={layout?.isOrdered}
|
||||
>
|
||||
{({ item, onItemChange }) => (
|
||||
<Stack p="4" rounded="md" flex="1" borderWidth="1px">
|
||||
<ZodFieldLayout
|
||||
schema={schema._def.innerType._def.type}
|
||||
data={item}
|
||||
isInAccordion={isInAccordion}
|
||||
onDataChange={onItemChange}
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
</TableList>
|
||||
)
|
||||
}
|
||||
|
@ -98,6 +98,7 @@ export const ZodObjectLayout = ({
|
||||
data={data?.[key]}
|
||||
blockDef={blockDef}
|
||||
blockOptions={blockOptions}
|
||||
propName={key}
|
||||
onDataChange={(val) => onDataChange({ ...data, [key]: val })}
|
||||
/>,
|
||||
],
|
||||
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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',
|
||||
|
59
apps/docs/contribute/guides/create-block.mdx
Normal 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.
|
53
apps/docs/contribute/guides/documentation.mdx
Normal 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!
|
61
apps/docs/contribute/guides/local-installation.mdx
Normal 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
|
||||
```
|
25
apps/docs/contribute/guides/translation.mdx
Normal 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>
|
71
apps/docs/contribute/overview.mdx
Normal 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>
|
125
apps/docs/contribute/the-forge/action.mdx
Normal 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>
|
32
apps/docs/contribute/the-forge/auth.mdx
Normal 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>
|
77
apps/docs/contribute/the-forge/block.mdx
Normal 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>
|
27
apps/docs/contribute/the-forge/cli.mdx
Normal 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>
|
53
apps/docs/contribute/the-forge/fetcher.mdx
Normal 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 })[]>`)
|
32
apps/docs/contribute/the-forge/layout.mdx
Normal 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>
|
0
apps/docs/contribute/the-forge/logo.mdx
Normal file
295
apps/docs/contribute/the-forge/options.mdx
Normal 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>
|
14
apps/docs/contribute/the-forge/overview.mdx
Normal 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.
|
252
apps/docs/contribute/the-forge/run.mdx
Normal 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.
|
@ -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,
|
||||
|
@ -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',
|
||||
|
@ -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">
|
||||
|
@ -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,
|
||||
|
BIN
apps/docs/images/contribute/action-example.png
Normal file
After Width: | Height: | Size: 70 KiB |
BIN
apps/docs/images/contribute/base-options.png
Normal file
After Width: | Height: | Size: 108 KiB |
BIN
apps/docs/images/contribute/block-name-logo-match.png
Normal file
After Width: | Height: | Size: 128 KiB |
BIN
apps/docs/images/contribute/forging-robot.png
Normal file
After Width: | Height: | Size: 1.1 MiB |
BIN
apps/docs/images/contribute/layout-array-example.png
Normal file
After Width: | Height: | Size: 34 KiB |
BIN
apps/docs/images/contribute/layout-boolean-example.png
Normal file
After Width: | Height: | Size: 32 KiB |
After Width: | Height: | Size: 25 KiB |
BIN
apps/docs/images/contribute/layout-enum-example.png
Normal file
After Width: | Height: | Size: 24 KiB |
BIN
apps/docs/images/contribute/layout-example.png
Normal file
After Width: | Height: | Size: 88 KiB |
BIN
apps/docs/images/contribute/layout-label.png
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
apps/docs/images/contribute/layout-number-example.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
apps/docs/images/contribute/layout-placeholder.png
Normal file
After Width: | Height: | Size: 19 KiB |
BIN
apps/docs/images/contribute/layout-save-response-example.png
Normal file
After Width: | Height: | Size: 36 KiB |
BIN
apps/docs/images/contribute/layout-string-example.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
apps/docs/images/contribute/the-forge.png
Normal file
After Width: | Height: | Size: 1.5 MiB |
BIN
apps/docs/images/contribute/tolgee.png
Normal file
After Width: | Height: | Size: 171 KiB |
BIN
apps/docs/images/contribute/translating-robot.png
Normal file
After Width: | Height: | Size: 1.5 MiB |
BIN
apps/docs/images/contribute/writing-robot.png
Normal file
After Width: | Height: | Size: 1.1 MiB |
@ -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",
|
||||
|
@ -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"
|
||||
},
|
||||
|
@ -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({
|
||||
|
@ -25,7 +25,7 @@ export const bookEvent = createAction({
|
||||
}),
|
||||
saveBookedDateInVariableId: option.string.layout({
|
||||
label: 'Save booked date',
|
||||
input: 'variableDropdown',
|
||||
inputType: 'variableDropdown',
|
||||
}),
|
||||
}),
|
||||
getSetVariableIds: ({ saveBookedDateInVariableId }) =>
|
||||
|
@ -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',
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
})
|
||||
|
@ -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',
|
||||
}),
|
||||
}),
|
||||
|
@ -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([
|
||||
|
@ -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',
|
||||
}),
|
||||
})
|
||||
)
|
||||
|
@ -117,7 +117,6 @@ export type BlockDefinition<
|
||||
auth?: Auth
|
||||
options?: Options | undefined
|
||||
fetchers?: FetcherDefinition<Auth, Options>[]
|
||||
isDisabledInPreview?: boolean
|
||||
actions: ActionDefinition<Auth, Options>[]
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|