@ -91,6 +91,7 @@
|
||||
"tinycolor2": "1.6.0",
|
||||
"unsplash-js": "7.0.18",
|
||||
"use-debounce": "9.0.4",
|
||||
"zod-validation-error": "3.0.3",
|
||||
"zustand": "4.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@ -18,7 +18,7 @@ import {
|
||||
|
||||
export interface ContextMenuProps<T extends HTMLElement> {
|
||||
onOpen?: () => void
|
||||
renderMenu: () => JSX.Element | null
|
||||
renderMenu: ({ onClose }: { onClose: () => void }) => JSX.Element | null
|
||||
children: (
|
||||
ref: MutableRefObject<T | null>,
|
||||
isOpened: boolean
|
||||
@ -101,7 +101,7 @@ export function ContextMenu<T extends HTMLElement = HTMLElement>(
|
||||
}}
|
||||
{...props.menuButtonProps}
|
||||
/>
|
||||
{props.renderMenu()}
|
||||
{props.renderMenu({ onClose: onCloseHandler })}
|
||||
</Menu>
|
||||
</Portal>
|
||||
)}
|
||||
|
@ -662,3 +662,12 @@ export const UnlinkIcon = (props: IconProps) => (
|
||||
<line x1="19" x2="22" y1="16" y2="16" />
|
||||
</Icon>
|
||||
)
|
||||
|
||||
export const RepeatIcon = (props: IconProps) => (
|
||||
<Icon viewBox="0 0 24 24" {...featherIconsBaseProps} {...props}>
|
||||
<path d="m2 9 3-3 3 3" />
|
||||
<path d="M13 18H7a2 2 0 0 1-2-2V6" />
|
||||
<path d="m22 15-3 3-3-3" />
|
||||
<path d="M11 6h6a2 2 0 0 1 2 2v10" />
|
||||
</Icon>
|
||||
)
|
||||
|
@ -0,0 +1,95 @@
|
||||
import {
|
||||
MenuList,
|
||||
MenuItem,
|
||||
useDisclosure,
|
||||
Menu,
|
||||
MenuButton,
|
||||
HStack,
|
||||
Text,
|
||||
} from '@chakra-ui/react'
|
||||
import { RepeatIcon, ChevronRightIcon } from '@/components/icons'
|
||||
import { useDebouncedCallback } from 'use-debounce'
|
||||
import { useForgedBlock } from '@/features/forge/hooks/useForgedBlock'
|
||||
import { ForgedBlockIcon } from '@/features/forge/ForgedBlockIcon'
|
||||
import { ForgedBlock } from '@typebot.io/forge-schemas'
|
||||
import { TurnableIntoParam } from '@typebot.io/forge'
|
||||
import { ZodObject } from 'zod'
|
||||
import { BlockV6 } from '@typebot.io/schemas'
|
||||
|
||||
type Props = {
|
||||
block: BlockV6
|
||||
onTurnIntoClick: (
|
||||
params: TurnableIntoParam,
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
blockSchema: ZodObject<any>
|
||||
) => void
|
||||
}
|
||||
|
||||
export const ForgedBlockTurnIntoMenu = ({ block, onTurnIntoClick }: Props) => {
|
||||
const { actionDef } = useForgedBlock(
|
||||
block.type,
|
||||
'options' in block ? block.options?.action : undefined
|
||||
)
|
||||
const { onClose, onOpen, isOpen } = useDisclosure()
|
||||
const debounceSubMenuClose = useDebouncedCallback(onClose, 200)
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
debounceSubMenuClose.cancel()
|
||||
onOpen()
|
||||
}
|
||||
|
||||
if (
|
||||
!actionDef ||
|
||||
!actionDef?.turnableInto ||
|
||||
actionDef?.turnableInto.length === 0
|
||||
)
|
||||
return null
|
||||
return (
|
||||
<Menu isOpen={isOpen} placement="right" offset={[0, 0]} onClose={onClose}>
|
||||
<MenuButton
|
||||
as={MenuItem}
|
||||
onClick={onOpen}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={debounceSubMenuClose}
|
||||
icon={<RepeatIcon />}
|
||||
>
|
||||
<HStack justifyContent="space-between">
|
||||
<Text>Turn into</Text>
|
||||
<ChevronRightIcon />
|
||||
</HStack>
|
||||
</MenuButton>
|
||||
<MenuList
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={debounceSubMenuClose}
|
||||
>
|
||||
{actionDef.turnableInto.map((params) => (
|
||||
<TurnIntoMenuItem
|
||||
key={params.blockType}
|
||||
blockType={params.blockType}
|
||||
onClick={(blockSchema) => onTurnIntoClick(params, blockSchema)}
|
||||
/>
|
||||
))}
|
||||
</MenuList>
|
||||
</Menu>
|
||||
)
|
||||
}
|
||||
|
||||
const TurnIntoMenuItem = ({
|
||||
blockType,
|
||||
onClick,
|
||||
}: {
|
||||
blockType: ForgedBlock['type']
|
||||
onClick: (blockSchema: ZodObject<any>) => void
|
||||
}) => {
|
||||
const { blockDef, blockSchema } = useForgedBlock(blockType)
|
||||
|
||||
if (!blockDef || !blockSchema) return null
|
||||
return (
|
||||
<MenuItem
|
||||
icon={<ForgedBlockIcon type={blockType} />}
|
||||
onClick={() => onClick(blockSchema)}
|
||||
>
|
||||
{blockDef.name}
|
||||
</MenuItem>
|
||||
)
|
||||
}
|
@ -1,12 +1,9 @@
|
||||
import { useMemo } from 'react'
|
||||
import { forgedBlockSchemas, forgedBlocks } from '@typebot.io/forge-schemas'
|
||||
import { enabledBlocks } from '@typebot.io/forge-repository'
|
||||
import { BlockWithOptions } from '@typebot.io/schemas'
|
||||
import { BlockV6 } from '@typebot.io/schemas'
|
||||
|
||||
export const useForgedBlock = (
|
||||
blockType: BlockWithOptions['type'],
|
||||
action?: string
|
||||
) =>
|
||||
export const useForgedBlock = (blockType: BlockV6['type'], action?: string) =>
|
||||
useMemo(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
if ((enabledBlocks as any).includes(blockType) === false) return {}
|
||||
|
@ -45,6 +45,10 @@ import { SettingsModal } from './SettingsModal'
|
||||
import { TElement } from '@udecode/plate-common'
|
||||
import { LogicBlockType } from '@typebot.io/schemas/features/blocks/logic/constants'
|
||||
import { useGroupsStore } from '@/features/graph/hooks/useGroupsStore'
|
||||
import { TurnableIntoParam } from '@typebot.io/forge'
|
||||
import { ZodError, ZodObject } from 'zod'
|
||||
import { toast } from 'sonner'
|
||||
import { fromZodError } from 'zod-validation-error'
|
||||
|
||||
export const BlockNode = ({
|
||||
block,
|
||||
@ -186,6 +190,42 @@ export const BlockNode = ({
|
||||
}
|
||||
}, [])
|
||||
|
||||
const convertBlock = (
|
||||
turnIntoParams: TurnableIntoParam,
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
targetBlockSchema: ZodObject<any>
|
||||
) => {
|
||||
if (!('options' in block) || !block.options) return
|
||||
|
||||
const convertedBlockOptions = turnIntoParams.customMapping
|
||||
? turnIntoParams.customMapping(block.options)
|
||||
: block.options
|
||||
try {
|
||||
updateBlock(
|
||||
indices,
|
||||
targetBlockSchema.parse({
|
||||
...block,
|
||||
type: turnIntoParams.blockType,
|
||||
options: {
|
||||
...convertedBlockOptions,
|
||||
credentialsId: undefined,
|
||||
},
|
||||
} as Block)
|
||||
)
|
||||
setOpenedBlockId(block.id)
|
||||
} catch (error) {
|
||||
if (error instanceof ZodError) {
|
||||
const validationError = fromZodError(error)
|
||||
console.error(validationError)
|
||||
toast.error('Could not convert block', {
|
||||
description: validationError.toString(),
|
||||
})
|
||||
} else {
|
||||
toast.error('An error occured while converting the block')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const hasIcomingEdge = typebot?.edges.some((edge) => {
|
||||
return edge.to.blockId === block.id
|
||||
})
|
||||
@ -198,7 +238,16 @@ export const BlockNode = ({
|
||||
/>
|
||||
) : (
|
||||
<ContextMenu<HTMLDivElement>
|
||||
renderMenu={() => <BlockNodeContextMenu indices={indices} />}
|
||||
renderMenu={({ onClose }) => (
|
||||
<BlockNodeContextMenu
|
||||
indices={indices}
|
||||
block={block}
|
||||
onTurnIntoClick={(params, schema) => {
|
||||
convertBlock(params, schema)
|
||||
onClose()
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
>
|
||||
{(ref, isContextMenuOpened) => (
|
||||
<Popover
|
||||
|
@ -1,11 +1,24 @@
|
||||
import { MenuList, MenuItem } from '@chakra-ui/react'
|
||||
import { CopyIcon, TrashIcon } from '@/components/icons'
|
||||
import { useTypebot } from '@/features/editor/providers/TypebotProvider'
|
||||
import { BlockIndices } from '@typebot.io/schemas'
|
||||
import { BlockIndices, BlockV6 } from '@typebot.io/schemas'
|
||||
import { useTranslate } from '@tolgee/react'
|
||||
import { ForgedBlockTurnIntoMenu } from '@/features/forge/components/ForgedBlockTurnIntoMenu'
|
||||
import { TurnableIntoParam } from '@typebot.io/forge'
|
||||
import { ZodObject } from 'zod'
|
||||
|
||||
type Props = { indices: BlockIndices }
|
||||
export const BlockNodeContextMenu = ({ indices }: Props) => {
|
||||
type Props = {
|
||||
indices: BlockIndices
|
||||
block: BlockV6
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
onTurnIntoClick: (params: TurnableIntoParam, schema: ZodObject<any>) => void
|
||||
}
|
||||
|
||||
export const BlockNodeContextMenu = ({
|
||||
indices,
|
||||
block,
|
||||
onTurnIntoClick,
|
||||
}: Props) => {
|
||||
const { t } = useTranslate()
|
||||
const { deleteBlock, duplicateBlock } = useTypebot()
|
||||
|
||||
@ -15,6 +28,10 @@ export const BlockNodeContextMenu = ({ indices }: Props) => {
|
||||
|
||||
return (
|
||||
<MenuList>
|
||||
<ForgedBlockTurnIntoMenu
|
||||
block={block}
|
||||
onTurnIntoClick={onTurnIntoClick}
|
||||
/>
|
||||
<MenuItem icon={<CopyIcon />} onClick={handleDuplicateClick}>
|
||||
{t('duplicate')}
|
||||
</MenuItem>
|
||||
|
@ -8,6 +8,7 @@ import { ChatNodeResponse } from '../types'
|
||||
export const sendMessage = createAction({
|
||||
auth,
|
||||
name: 'Send Message',
|
||||
turnableInto: undefined,
|
||||
options: option.object({
|
||||
botId: option.string.layout({
|
||||
label: 'Bot ID',
|
||||
|
@ -67,6 +67,15 @@ export const createChatCompletion = createAction({
|
||||
name: 'Create chat completion',
|
||||
auth,
|
||||
options,
|
||||
turnableInto: [
|
||||
{
|
||||
blockType: 'openai',
|
||||
},
|
||||
{
|
||||
blockType: 'together-ai',
|
||||
},
|
||||
{ blockType: 'open-router' },
|
||||
],
|
||||
getSetVariableIds: (options) =>
|
||||
options.responseMapping?.map((res) => res.variableId).filter(isDefined) ??
|
||||
[],
|
||||
|
@ -9,7 +9,7 @@ export const auth = {
|
||||
isRequired: true,
|
||||
inputType: 'password',
|
||||
helperText:
|
||||
'You can generate an API key [here](https://console.mistral.ai/user/api-keys/).',
|
||||
'You can generate an API key [here](https://console.mistral.ai/api-keys).',
|
||||
}),
|
||||
}),
|
||||
} satisfies AuthDefinition
|
||||
|
@ -12,6 +12,15 @@ import { ModelsResponse } from '../types'
|
||||
export const createChatCompletion = createAction({
|
||||
name: 'Create chat completion',
|
||||
auth,
|
||||
turnableInto: [
|
||||
{
|
||||
blockType: 'openai',
|
||||
},
|
||||
{
|
||||
blockType: 'together-ai',
|
||||
},
|
||||
{ blockType: 'mistral' },
|
||||
],
|
||||
options: parseChatCompletionOptions({
|
||||
modelFetchId: 'fetchModels',
|
||||
}),
|
||||
|
@ -19,6 +19,15 @@ export const createChatCompletion = createAction({
|
||||
modelFetchId: 'fetchModels',
|
||||
}),
|
||||
getSetVariableIds: getChatCompletionSetVarIds,
|
||||
turnableInto: [
|
||||
{
|
||||
blockType: 'open-router',
|
||||
},
|
||||
{
|
||||
blockType: 'together-ai',
|
||||
},
|
||||
{ blockType: 'mistral' },
|
||||
],
|
||||
fetchers: [
|
||||
{
|
||||
id: 'fetchModels',
|
||||
|
@ -14,6 +14,15 @@ export const createChatCompletion = createAction({
|
||||
modelHelperText:
|
||||
'You can find the list of all the models available [here](https://docs.together.ai/docs/inference-models#chat-models). Copy the model string for API.',
|
||||
}),
|
||||
turnableInto: [
|
||||
{
|
||||
blockType: 'openai',
|
||||
},
|
||||
{
|
||||
blockType: 'open-router',
|
||||
},
|
||||
{ blockType: 'mistral' },
|
||||
],
|
||||
getSetVariableIds: getChatCompletionSetVarIds,
|
||||
run: {
|
||||
server: (params) =>
|
||||
|
@ -11,6 +11,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@typebot.io/tsconfig": "workspace:*",
|
||||
"@types/react": "18.2.15"
|
||||
"@types/react": "18.2.15",
|
||||
"@typebot.io/forge-repository": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { SVGProps } from 'react'
|
||||
import { z } from './zod'
|
||||
import { ZodRawShape } from 'zod'
|
||||
import { enabledBlocks } from '@typebot.io/forge-repository'
|
||||
|
||||
export type VariableStore = {
|
||||
get: (variableId: string) => string | (string | null)[] | null | undefined
|
||||
@ -32,6 +33,14 @@ export type FunctionToExecute = {
|
||||
|
||||
export type ReadOnlyVariableStore = Omit<VariableStore, 'set'>
|
||||
|
||||
export type TurnableIntoParam<T = {}> = {
|
||||
blockType: (typeof enabledBlocks)[number]
|
||||
/**
|
||||
* If defined will be used to convert the existing block options into the new block options.
|
||||
*/
|
||||
customMapping?: (options: T) => any
|
||||
}
|
||||
|
||||
export type ActionDefinition<
|
||||
A extends AuthDefinition,
|
||||
BaseOptions extends z.ZodObject<ZodRawShape> = z.ZodObject<{}>,
|
||||
@ -40,6 +49,7 @@ export type ActionDefinition<
|
||||
name: string
|
||||
fetchers?: FetcherDefinition<A, z.infer<BaseOptions> & z.infer<Options>>[]
|
||||
options?: Options
|
||||
turnableInto?: TurnableIntoParam<z.infer<Options>>[]
|
||||
getSetVariableIds?: (options: z.infer<Options>) => string[]
|
||||
run?: {
|
||||
server?: (params: {
|
||||
|
15
pnpm-lock.yaml
generated
15
pnpm-lock.yaml
generated
@ -263,6 +263,9 @@ importers:
|
||||
use-debounce:
|
||||
specifier: 9.0.4
|
||||
version: 9.0.4(react@18.2.0)
|
||||
zod-validation-error:
|
||||
specifier: 3.0.3
|
||||
version: 3.0.3(zod@3.22.4)
|
||||
zustand:
|
||||
specifier: 4.5.0
|
||||
version: 4.5.0(@types/react@18.2.15)(immer@10.0.2)(react@18.2.0)
|
||||
@ -1414,6 +1417,9 @@ importers:
|
||||
specifier: 3.22.4
|
||||
version: 3.22.4
|
||||
devDependencies:
|
||||
'@typebot.io/forge-repository':
|
||||
specifier: workspace:*
|
||||
version: link:../repository
|
||||
'@typebot.io/tsconfig':
|
||||
specifier: workspace:*
|
||||
version: link:../../tsconfig
|
||||
@ -22787,6 +22793,15 @@ packages:
|
||||
zod: 3.22.4
|
||||
dev: true
|
||||
|
||||
/zod-validation-error@3.0.3(zod@3.22.4):
|
||||
resolution: {integrity: sha512-cETTrcMq3Ze58vhdR0zD37uJm/694I6mAxcf/ei5bl89cC++fBNxrC2z8lkFze/8hVMPwrbtrwXHR2LB50fpHw==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
peerDependencies:
|
||||
zod: ^3.18.0
|
||||
dependencies:
|
||||
zod: 3.22.4
|
||||
dev: false
|
||||
|
||||
/zod@3.22.4:
|
||||
resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==}
|
||||
|
||||
|
Reference in New Issue
Block a user