mirror of
http://112.124.100.131/huang.ze/ebiz-dify-ai.git
synced 2025-12-10 19:36:53 +08:00
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:
120
web/app/components/workflow/block-selector/blocks.tsx
Normal file
120
web/app/components/workflow/block-selector/blocks.tsx
Normal 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)
|
||||
70
web/app/components/workflow/block-selector/constants.tsx
Normal file
70
web/app/components/workflow/block-selector/constants.tsx
Normal 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,
|
||||
]
|
||||
33
web/app/components/workflow/block-selector/hooks.ts
Normal file
33
web/app/components/workflow/block-selector/hooks.ts
Normal 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'),
|
||||
},
|
||||
]
|
||||
}
|
||||
146
web/app/components/workflow/block-selector/index.tsx
Normal file
146
web/app/components/workflow/block-selector/index.tsx
Normal 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)
|
||||
76
web/app/components/workflow/block-selector/tabs.tsx
Normal file
76
web/app/components/workflow/block-selector/tabs.tsx
Normal 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)
|
||||
113
web/app/components/workflow/block-selector/tools.tsx
Normal file
113
web/app/components/workflow/block-selector/tools.tsx
Normal 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)
|
||||
22
web/app/components/workflow/block-selector/types.ts
Normal file
22
web/app/components/workflow/block-selector/types.ts
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user