FEAT: NEW WORKFLOW ENGINE (#3160)

Co-authored-by: Joel <iamjoel007@gmail.com>
Co-authored-by: Yeuoly <admin@srmxy.cn>
Co-authored-by: JzoNg <jzongcode@gmail.com>
Co-authored-by: StyleZhang <jasonapring2015@outlook.com>
Co-authored-by: jyong <jyong@dify.ai>
Co-authored-by: nite-knite <nkCoding@gmail.com>
Co-authored-by: jyong <718720800@qq.com>
This commit is contained in:
takatost
2024-04-08 18:51:46 +08:00
committed by GitHub
parent 2fb9850af5
commit 7753ba2d37
1161 changed files with 103836 additions and 10327 deletions

View File

@@ -0,0 +1,120 @@
import {
memo,
useCallback,
useMemo,
} from 'react'
import { useTranslation } from 'react-i18next'
import { groupBy } from 'lodash-es'
import BlockIcon from '../block-icon'
import { BlockEnum } from '../types'
import {
useIsChatMode,
useNodesExtraData,
} from '../hooks'
import { BLOCK_CLASSIFICATIONS } from './constants'
import { useBlocks } from './hooks'
import type { ToolDefaultValue } from './types'
import Tooltip from '@/app/components/base/tooltip'
type BlocksProps = {
searchText: string
onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void
availableBlocksTypes?: BlockEnum[]
}
const Blocks = ({
searchText,
onSelect,
availableBlocksTypes = [],
}: BlocksProps) => {
const { t } = useTranslation()
const isChatMode = useIsChatMode()
const nodesExtraData = useNodesExtraData()
const blocks = useBlocks()
const groups = useMemo(() => {
return BLOCK_CLASSIFICATIONS.reduce((acc, classification) => {
const list = groupBy(blocks, 'classification')[classification].filter((block) => {
if (block.type === BlockEnum.Answer && !isChatMode)
return false
return block.title.toLowerCase().includes(searchText.toLowerCase()) && availableBlocksTypes.includes(block.type)
})
return {
...acc,
[classification]: list,
}
}, {} as Record<string, typeof blocks>)
}, [blocks, isChatMode, searchText, availableBlocksTypes])
const isEmpty = Object.values(groups).every(list => !list.length)
const renderGroup = useCallback((classification: string) => {
const list = groups[classification]
return (
<div
key={classification}
className='mb-1 last-of-type:mb-0'
>
{
classification !== '-' && !!list.length && (
<div className='flex items-start px-3 h-[22px] text-xs font-medium text-gray-500'>
{t(`workflow.tabs.${classification}`)}
</div>
)
}
{
list.map(block => (
<Tooltip
key={block.type}
selector={`workflow-block-${block.type}`}
position='right'
className='!p-0 !px-3 !py-2.5 !w-[200px] !leading-[18px] !text-xs !text-gray-700 !border-[0.5px] !border-black/5 !bg-transparent !rounded-xl !shadow-lg'
htmlContent={(
<div>
<div className='flex items-center mb-2'>
<BlockIcon
size='md'
className='mr-2'
type={block.type}
/>
<div className='text-sm text-gray-900'>{block.title}</div>
</div>
{nodesExtraData[block.type].about}
</div>
)}
noArrow
>
<div
key={block.type}
className='flex items-center px-3 w-full h-8 rounded-lg hover:bg-gray-50 cursor-pointer'
onClick={() => onSelect(block.type)}
>
<BlockIcon
className='mr-2'
type={block.type}
/>
<div className='text-sm text-gray-900'>{block.title}</div>
</div>
</Tooltip>
))
}
</div>
)
}, [groups, nodesExtraData, onSelect, t])
return (
<div className='p-1'>
{
isEmpty && (
<div className='flex items-center px-3 h-[22px] text-xs font-medium text-gray-500'>{t('workflow.tabs.noResult')}</div>
)
}
{
!isEmpty && BLOCK_CLASSIFICATIONS.map(renderGroup)
}
</div>
)
}
export default memo(Blocks)

View File

@@ -0,0 +1,70 @@
import type { Block } from '../types'
import { BlockEnum } from '../types'
import { BlockClassificationEnum } from './types'
export const BLOCKS: Block[] = [
{
classification: BlockClassificationEnum.Default,
type: BlockEnum.Start,
title: 'Start',
description: '',
},
{
classification: BlockClassificationEnum.Default,
type: BlockEnum.LLM,
title: 'LLM',
},
{
classification: BlockClassificationEnum.Default,
type: BlockEnum.KnowledgeRetrieval,
title: 'Knowledge Retrieval',
},
{
classification: BlockClassificationEnum.Default,
type: BlockEnum.End,
title: 'End',
},
{
classification: BlockClassificationEnum.Default,
type: BlockEnum.Answer,
title: 'Direct Answer',
},
{
classification: BlockClassificationEnum.QuestionUnderstand,
type: BlockEnum.QuestionClassifier,
title: 'Question Classifier',
},
{
classification: BlockClassificationEnum.Logic,
type: BlockEnum.IfElse,
title: 'IF/ELSE',
},
{
classification: BlockClassificationEnum.Transform,
type: BlockEnum.Code,
title: 'Code',
},
{
classification: BlockClassificationEnum.Transform,
type: BlockEnum.TemplateTransform,
title: 'Templating Transform',
},
{
classification: BlockClassificationEnum.Transform,
type: BlockEnum.VariableAssigner,
title: 'Variable Assigner',
},
{
classification: BlockClassificationEnum.Utilities,
type: BlockEnum.HttpRequest,
title: 'HTTP Request',
},
]
export const BLOCK_CLASSIFICATIONS: string[] = [
BlockClassificationEnum.Default,
BlockClassificationEnum.QuestionUnderstand,
BlockClassificationEnum.Logic,
BlockClassificationEnum.Transform,
BlockClassificationEnum.Utilities,
]

View File

@@ -0,0 +1,33 @@
import { useTranslation } from 'react-i18next'
import { BLOCKS } from './constants'
import { TabsEnum } from './types'
export const useBlocks = () => {
const { t } = useTranslation()
return BLOCKS.map((block) => {
return {
...block,
title: t(`workflow.blocks.${block.type}`),
}
})
}
export const useTabs = () => {
const { t } = useTranslation()
return [
{
key: TabsEnum.Blocks,
name: t('workflow.tabs.blocks'),
},
{
key: TabsEnum.BuiltInTool,
name: t('workflow.tabs.builtInTool'),
},
{
key: TabsEnum.CustomTool,
name: t('workflow.tabs.customTool'),
},
]
}

View File

@@ -0,0 +1,146 @@
import type {
FC,
MouseEventHandler,
} from 'react'
import {
memo,
useCallback,
useState,
} from 'react'
import { useTranslation } from 'react-i18next'
import type {
OffsetOptions,
Placement,
} from '@floating-ui/react'
import type { BlockEnum, OnSelectBlock } from '../types'
import Tabs from './tabs'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import {
Plus02,
SearchLg,
} from '@/app/components/base/icons/src/vender/line/general'
import { XCircle } from '@/app/components/base/icons/src/vender/solid/general'
type NodeSelectorProps = {
open?: boolean
onOpenChange?: (open: boolean) => void
onSelect: OnSelectBlock
trigger?: (open: boolean) => React.ReactNode
placement?: Placement
offset?: OffsetOptions
triggerStyle?: React.CSSProperties
triggerClassName?: (open: boolean) => string
triggerInnerClassName?: string
popupClassName?: string
asChild?: boolean
availableBlocksTypes?: BlockEnum[]
disabled?: boolean
}
const NodeSelector: FC<NodeSelectorProps> = ({
open: openFromProps,
onOpenChange,
onSelect,
trigger,
placement = 'right',
offset = 6,
triggerClassName,
triggerInnerClassName,
triggerStyle,
popupClassName,
asChild,
availableBlocksTypes,
disabled,
}) => {
const { t } = useTranslation()
const [searchText, setSearchText] = useState('')
const [localOpen, setLocalOpen] = useState(false)
const open = openFromProps === undefined ? localOpen : openFromProps
const handleOpenChange = useCallback((newOpen: boolean) => {
setLocalOpen(newOpen)
if (onOpenChange)
onOpenChange(newOpen)
}, [onOpenChange])
const handleTrigger = useCallback<MouseEventHandler<HTMLDivElement>>((e) => {
if (disabled)
return
e.stopPropagation()
handleOpenChange(!open)
}, [handleOpenChange, open, disabled])
const handleSelect = useCallback<OnSelectBlock>((type, toolDefaultValue) => {
handleOpenChange(false)
onSelect(type, toolDefaultValue)
}, [handleOpenChange, onSelect])
return (
<PortalToFollowElem
placement={placement}
offset={offset}
open={open}
onOpenChange={handleOpenChange}
>
<PortalToFollowElemTrigger
asChild={asChild}
onClick={handleTrigger}
className={triggerInnerClassName}
>
{
trigger
? trigger(open)
: (
<div
className={`
flex items-center justify-center
w-4 h-4 rounded-full bg-primary-600 cursor-pointer z-10
${triggerClassName?.(open)}
`}
style={triggerStyle}
>
<Plus02 className='w-2.5 h-2.5 text-white' />
</div>
)
}
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-[1000]'>
<div className={`rounded-lg border-[0.5px] border-gray-200 bg-white shadow-lg ${popupClassName}`}>
<div className='px-2 pt-2'>
<div
className='flex items-center px-2 rounded-lg bg-gray-100'
onClick={e => e.stopPropagation()}
>
<SearchLg className='shrink-0 ml-[1px] mr-[5px] w-3.5 h-3.5 text-gray-400' />
<input
value={searchText}
className='grow px-0.5 py-[7px] text-[13px] text-gray-700 bg-transparent appearance-none outline-none caret-primary-600 placeholder:text-gray-400'
placeholder={t('workflow.tabs.searchBlock') || ''}
onChange={e => setSearchText(e.target.value)}
autoFocus
/>
{
searchText && (
<div
className='flex items-center justify-center ml-[5px] w-[18px] h-[18px] cursor-pointer'
onClick={() => setSearchText('')}
>
<XCircle className='w-[14px] h-[14px] text-gray-400' />
</div>
)
}
</div>
</div>
<Tabs
onSelect={handleSelect}
searchText={searchText}
availableBlocksTypes={availableBlocksTypes}
/>
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
)
}
export default memo(NodeSelector)

View File

@@ -0,0 +1,76 @@
import type { FC } from 'react'
import {
memo,
useState,
} from 'react'
import type { BlockEnum } from '../types'
import { useTabs } from './hooks'
import type { ToolDefaultValue } from './types'
import { TabsEnum } from './types'
import Tools from './tools'
import Blocks from './blocks'
export type TabsProps = {
searchText: string
onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void
availableBlocksTypes?: BlockEnum[]
}
const Tabs: FC<TabsProps> = ({
searchText,
onSelect,
availableBlocksTypes,
}) => {
const tabs = useTabs()
const [activeTab, setActiveTab] = useState(tabs[0].key)
return (
<div onClick={e => e.stopPropagation()}>
<div className='flex items-center px-3 border-b-[0.5px] border-b-black/5'>
{
tabs.map(tab => (
<div
key={tab.key}
className={`
relative mr-4 h-[34px] leading-[34px] text-[13px] font-medium cursor-pointer
${activeTab === tab.key
? 'text-gray-700 after:absolute after:bottom-0 after:left-0 after:h-0.5 after:w-full after:bg-primary-600'
: 'text-gray-500'}
`}
onClick={() => setActiveTab(tab.key)}
>
{tab.name}
</div>
))
}
</div>
{
activeTab === TabsEnum.Blocks && (
<Blocks
searchText={searchText}
onSelect={onSelect}
availableBlocksTypes={availableBlocksTypes}
/>
)
}
{
activeTab === TabsEnum.BuiltInTool && (
<Tools
onSelect={onSelect}
searchText={searchText}
/>
)
}
{
activeTab === TabsEnum.CustomTool && (
<Tools
isCustom
searchText={searchText}
onSelect={onSelect}
/>
)
}
</div>
)
}
export default memo(Tabs)

View File

@@ -0,0 +1,113 @@
import {
memo,
useCallback,
useMemo,
} from 'react'
import { useTranslation } from 'react-i18next'
import BlockIcon from '../block-icon'
import { BlockEnum } from '../types'
import type { ToolWithProvider } from '../types'
import { useStore } from '../store'
import type { ToolDefaultValue } from './types'
import Tooltip from '@/app/components/base/tooltip'
import { useGetLanguage } from '@/context/i18n'
type ToolsProps = {
isCustom?: boolean
onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void
searchText: string
}
const Blocks = ({
isCustom,
searchText,
onSelect,
}: ToolsProps) => {
const { t } = useTranslation()
const language = useGetLanguage()
const buildInTools = useStore(s => s.buildInTools)
const customTools = useStore(s => s.customTools)
const tools = useMemo(() => {
const currentTools = isCustom ? customTools : buildInTools
return currentTools.filter((toolWithProvider) => {
return toolWithProvider.tools.some((tool) => {
return tool.label[language].toLowerCase().includes(searchText.toLowerCase())
})
})
}, [isCustom, customTools, buildInTools, searchText, language])
const renderGroup = useCallback((toolWithProvider: ToolWithProvider) => {
const list = toolWithProvider.tools
return (
<div
key={toolWithProvider.id}
className='mb-1 last-of-type:mb-0'
>
<div className='flex items-start px-3 h-[22px] text-xs font-medium text-gray-500'>
{toolWithProvider.label[language]}
</div>
{
list.map(tool => (
<Tooltip
key={tool.name}
selector={`workflow-block-tool-${tool.name}`}
position='right'
className='!p-0 !px-3 !py-2.5 !w-[200px] !leading-[18px] !text-xs !text-gray-700 !border-[0.5px] !border-black/5 !bg-transparent !rounded-xl !shadow-lg'
htmlContent={(
<div>
<div className='flex items-center mb-2'>
<BlockIcon
size='md'
className='mr-2'
type={BlockEnum.Tool}
toolIcon={toolWithProvider.icon}
/>
<div className='text-sm text-gray-900'>{tool.label[language]}</div>
</div>
{tool.description[language]}
</div>
)}
noArrow
>
<div
className='flex items-center px-3 w-full h-8 rounded-lg hover:bg-gray-50 cursor-pointer'
onClick={() => onSelect(BlockEnum.Tool, {
provider_id: toolWithProvider.id,
provider_type: toolWithProvider.type,
provider_name: toolWithProvider.name,
tool_name: tool.name,
tool_label: tool.label[language],
title: tool.label[language],
})}
>
<BlockIcon
className='mr-2'
type={BlockEnum.Tool}
toolIcon={toolWithProvider.icon}
/>
<div className='text-sm text-gray-900'>{tool.label[language]}</div>
</div>
</Tooltip>
))
}
</div>
)
}, [onSelect, language])
return (
<div className='p-1 max-h-[464px] overflow-y-auto'>
{
!tools.length && (
<div className='flex items-center px-3 h-[22px] text-xs font-medium text-gray-500'>{t('workflow.tabs.noResult')}</div>
)
}
{
!!tools.length && tools.map(renderGroup)
}
</div>
)
}
export default memo(Blocks)

View File

@@ -0,0 +1,22 @@
export enum TabsEnum {
Blocks = 'blocks',
BuiltInTool = 'built-in-tool',
CustomTool = 'custom-tool',
}
export enum BlockClassificationEnum {
Default = '-',
QuestionUnderstand = 'question-understand',
Logic = 'logic',
Transform = 'transform',
Utilities = 'utilities',
}
export type ToolDefaultValue = {
provider_id: string
provider_type: string
provider_name: string
tool_name: string
tool_label: string
title: string
}