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:
@@ -0,0 +1,86 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import React, { useCallback } from 'react'
|
||||
import produce from 'immer'
|
||||
import RemoveButton from '../../../_base/components/remove-button'
|
||||
import VarReferencePicker from '@/app/components/workflow/nodes/_base/components/variable/var-reference-picker'
|
||||
import type { ValueSelector, Var } from '@/app/components/workflow/types'
|
||||
|
||||
type Props = {
|
||||
readonly: boolean
|
||||
nodeId: string
|
||||
list: ValueSelector[]
|
||||
onChange: (list: ValueSelector[]) => void
|
||||
onOpen?: (index: number) => void
|
||||
onlyLeafNodeVar?: boolean
|
||||
filterVar?: (payload: Var, valueSelector: ValueSelector) => boolean
|
||||
}
|
||||
|
||||
const VarList: FC<Props> = ({
|
||||
readonly,
|
||||
nodeId,
|
||||
list,
|
||||
onChange,
|
||||
onOpen = () => { },
|
||||
onlyLeafNodeVar,
|
||||
filterVar,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const handleVarReferenceChange = useCallback((index: number) => {
|
||||
return (value: ValueSelector | string) => {
|
||||
const newList = produce(list, (draft) => {
|
||||
draft[index] = value as ValueSelector
|
||||
})
|
||||
onChange(newList)
|
||||
}
|
||||
}, [list, onChange])
|
||||
|
||||
const handleVarRemove = useCallback((index: number) => {
|
||||
return () => {
|
||||
const newList = produce(list, (draft) => {
|
||||
draft.splice(index, 1)
|
||||
})
|
||||
onChange(newList)
|
||||
}
|
||||
}, [list, onChange])
|
||||
|
||||
const handleOpen = useCallback((index: number) => {
|
||||
return () => onOpen(index)
|
||||
}, [onOpen])
|
||||
|
||||
if (list.length === 0) {
|
||||
return (
|
||||
<div className='flex rounded-md bg-gray-50 items-center h-[42px] justify-center leading-[18px] text-xs font-normal text-gray-500'>
|
||||
{t('workflow.nodes.variableAssigner.noVarTip')}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='space-y-2'>
|
||||
{list.map((item, index) => (
|
||||
<div className='flex items-center space-x-1' key={index}>
|
||||
<VarReferencePicker
|
||||
readonly={readonly}
|
||||
nodeId={nodeId}
|
||||
isShowNodeName
|
||||
className='grow'
|
||||
value={item}
|
||||
onChange={handleVarReferenceChange(index)}
|
||||
onOpen={handleOpen(index)}
|
||||
onlyLeafNodeVar={onlyLeafNodeVar}
|
||||
filterVar={filterVar}
|
||||
/>
|
||||
{!readonly && (
|
||||
<RemoveButton
|
||||
className='!p-2 !bg-gray-100 hover:!bg-gray-200'
|
||||
onClick={handleVarRemove(index)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(VarList)
|
||||
@@ -0,0 +1,38 @@
|
||||
import { useCallback } from 'react'
|
||||
import produce from 'immer'
|
||||
import type { VariableAssignerNodeType } from '../../types'
|
||||
import type { ValueSelector } from '@/app/components/workflow/types'
|
||||
import { useEdgesInteractions } from '@/app/components/workflow/hooks'
|
||||
|
||||
type Params = {
|
||||
id: string
|
||||
inputs: VariableAssignerNodeType
|
||||
setInputs: (newInputs: VariableAssignerNodeType) => void
|
||||
}
|
||||
function useVarList({
|
||||
id,
|
||||
inputs,
|
||||
setInputs,
|
||||
}: Params) {
|
||||
const { handleVariableAssignerEdgesChange } = useEdgesInteractions()
|
||||
const handleVarListChange = useCallback((newList: ValueSelector[]) => {
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
draft.variables = newList
|
||||
})
|
||||
setInputs(newInputs)
|
||||
handleVariableAssignerEdgesChange(id, newList)
|
||||
}, [inputs, setInputs, id, handleVariableAssignerEdgesChange])
|
||||
|
||||
const handleAddVariable = useCallback(() => {
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
draft.variables.push([])
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}, [inputs, setInputs])
|
||||
return {
|
||||
handleVarListChange,
|
||||
handleAddVariable,
|
||||
}
|
||||
}
|
||||
|
||||
export default useVarList
|
||||
@@ -0,0 +1,42 @@
|
||||
import { type NodeDefault, VarType } from '../../types'
|
||||
import { BlockEnum } from '../../types'
|
||||
import type { VariableAssignerNodeType } from './types'
|
||||
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/constants'
|
||||
|
||||
const i18nPrefix = 'workflow'
|
||||
|
||||
const nodeDefault: NodeDefault<VariableAssignerNodeType> = {
|
||||
defaultValue: {
|
||||
output_type: VarType.string,
|
||||
variables: [],
|
||||
},
|
||||
getAvailablePrevNodes(isChatMode: boolean) {
|
||||
const nodes = isChatMode
|
||||
? ALL_CHAT_AVAILABLE_BLOCKS
|
||||
: ALL_COMPLETION_AVAILABLE_BLOCKS.filter(type => type !== BlockEnum.End)
|
||||
return nodes.filter(type => type !== BlockEnum.IfElse && type !== BlockEnum.QuestionClassifier)
|
||||
},
|
||||
getAvailableNextNodes(isChatMode: boolean) {
|
||||
const nodes = isChatMode ? ALL_CHAT_AVAILABLE_BLOCKS : ALL_COMPLETION_AVAILABLE_BLOCKS
|
||||
return nodes
|
||||
},
|
||||
checkValid(payload: VariableAssignerNodeType, t: any) {
|
||||
let errorMessages = ''
|
||||
const { variables } = payload
|
||||
if (!variables || variables.length === 0)
|
||||
errorMessages = t(`${i18nPrefix}.errorMsg.fieldRequired`, { field: t(`${i18nPrefix}.nodes.variableAssigner.title`) })
|
||||
if (!errorMessages) {
|
||||
variables.forEach((variable) => {
|
||||
if (!variable || variable.length === 0)
|
||||
errorMessages = t(`${i18nPrefix}.errorMsg.fieldRequired`, { field: t(`${i18nPrefix}.errorMsg.fields.variableValue`) })
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
isValid: !errorMessages,
|
||||
errorMessage: errorMessages,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export default nodeDefault
|
||||
91
web/app/components/workflow/nodes/variable-assigner/node.tsx
Normal file
91
web/app/components/workflow/nodes/variable-assigner/node.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import type { NodeProps } from 'reactflow'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { NodeTargetHandle } from '../_base/components/node-handle'
|
||||
import { BlockEnum } from '../../types'
|
||||
import type { VariableAssignerNodeType } from './types'
|
||||
import { VarBlockIcon } from '@/app/components/workflow/block-icon'
|
||||
import { Line3 } from '@/app/components/base/icons/src/public/common'
|
||||
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
|
||||
import {
|
||||
useWorkflow,
|
||||
} from '@/app/components/workflow/hooks'
|
||||
|
||||
const i18nPrefix = 'workflow.nodes.variableAssigner'
|
||||
|
||||
const Node: FC<NodeProps<VariableAssignerNodeType>> = (props) => {
|
||||
const { t } = useTranslation()
|
||||
const { id, data } = props
|
||||
const { variables: originVariables, output_type } = data
|
||||
const { getTreeLeafNodes } = useWorkflow()
|
||||
|
||||
const availableNodes = getTreeLeafNodes(id)
|
||||
const variables = originVariables.filter(item => item.length > 0)
|
||||
|
||||
return (
|
||||
<div className='mb-1 px-3 py-1'>
|
||||
<div className='mb-0.5 leading-4 text-xs font-medium text-gray-500 uppercase'>{t(`${i18nPrefix}.title`)}</div>
|
||||
{
|
||||
variables.length === 0 && (
|
||||
<div className='relative flex items-center h-6 justify-between bg-gray-100 rounded-md px-1 space-x-1 text-xs font-normal text-gray-400 uppercase'>
|
||||
{t(`${i18nPrefix}.varNotSet`)}
|
||||
<NodeTargetHandle
|
||||
{...props}
|
||||
handleId='varNotSet'
|
||||
handleClassName='!top-1/2 !-translate-y-1/2 !-left-[21px]'
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{variables.length > 0 && (
|
||||
<>
|
||||
<div className='space-y-0.5'>
|
||||
{variables.map((item, index) => {
|
||||
const node = availableNodes.find(node => node.id === item[0])
|
||||
const varName = item[item.length - 1]
|
||||
|
||||
return (
|
||||
<div key={index} className='relative flex items-center h-6 bg-gray-100 rounded-md px-1 text-xs font-normal text-gray-700' >
|
||||
<NodeTargetHandle
|
||||
{...props}
|
||||
handleId={item[0]}
|
||||
handleClassName='!top-1/2 !-translate-y-1/2 !-left-[21px]'
|
||||
/>
|
||||
<div className='flex items-center'>
|
||||
<div className='p-[1px]'>
|
||||
<VarBlockIcon
|
||||
className='!text-gray-900'
|
||||
type={(node?.data.type as BlockEnum) || BlockEnum.Start}
|
||||
/>
|
||||
</div>
|
||||
<div className='mx-0.5 text-xs font-medium text-gray-700'>{node?.data.title}</div>
|
||||
<Line3 className='mr-0.5'></Line3>
|
||||
</div>
|
||||
<div className='flex items-center text-primary-600'>
|
||||
<Variable02 className='w-3.5 h-3.5' />
|
||||
<div className='ml-0.5 text-xs font-medium'>{varName}</div>
|
||||
</div>
|
||||
{/* <div className='ml-0.5 text-xs font-normal text-gray-500'>{output_type}</div> */}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
|
||||
)}
|
||||
</div>
|
||||
<div className='mt-2 flex items-center h-6 justify-between bg-gray-100 rounded-md px-1 space-x-1 text-xs font-normal text-gray-700'>
|
||||
<div className='text-xs font-medium text-gray-500 uppercase'>
|
||||
{t(`${i18nPrefix}.outputType`)}
|
||||
</div>
|
||||
<div className='text-xs font-normal text-gray-700'>
|
||||
{t(`${i18nPrefix}.type.${output_type}`)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
</div >
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(Node)
|
||||
@@ -0,0 +1,94 @@
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import useConfig from './use-config'
|
||||
import VarList from './components/var-list'
|
||||
import type { VariableAssignerNodeType } from './types'
|
||||
import Field from '@/app/components/workflow/nodes/_base/components/field'
|
||||
import Selector from '@/app/components/workflow/nodes/_base/components/selector'
|
||||
import AddButton from '@/app/components/base/button/add-button'
|
||||
import { ChevronDown } from '@/app/components/base/icons/src/vender/line/arrows'
|
||||
import type { NodePanelProps } from '@/app/components/workflow/types'
|
||||
import { VarType } from '@/app/components/workflow/types'
|
||||
import Split from '@/app/components/workflow/nodes/_base/components/split'
|
||||
import OutputVars, { VarItem } from '@/app/components/workflow/nodes/_base/components/output-vars'
|
||||
|
||||
const i18nPrefix = 'workflow.nodes.variableAssigner'
|
||||
const Panel: FC<NodePanelProps<VariableAssignerNodeType>> = ({
|
||||
id,
|
||||
data,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const {
|
||||
readOnly,
|
||||
inputs,
|
||||
handleOutputTypeChange,
|
||||
handleVarListChange,
|
||||
handleAddVariable,
|
||||
handleOnVarOpen,
|
||||
filterVar,
|
||||
} = useConfig(id, data)
|
||||
|
||||
const typeOptions = [
|
||||
{ label: t(`${i18nPrefix}.type.string`), value: VarType.string },
|
||||
{ label: t(`${i18nPrefix}.type.number`), value: VarType.number },
|
||||
{ label: t(`${i18nPrefix}.type.object`), value: VarType.object },
|
||||
{ label: t(`${i18nPrefix}.type.array`), value: VarType.array },
|
||||
]
|
||||
|
||||
return (
|
||||
<div className='mt-2'>
|
||||
<div className='px-4 pb-4 space-y-4'>
|
||||
<Field
|
||||
title={t(`${i18nPrefix}.outputVarType`)}
|
||||
>
|
||||
<Selector
|
||||
readonly={readOnly}
|
||||
value={inputs.output_type}
|
||||
options={typeOptions}
|
||||
onChange={handleOutputTypeChange}
|
||||
trigger={
|
||||
<div className='flex items-center h-8 justify-between px-2.5 rounded-lg bg-gray-100 capitalize'>
|
||||
<div className='text-[13px] font-normal text-gray-900'>{inputs.output_type}</div>
|
||||
{!readOnly && <ChevronDown className='w-3.5 h-3.5 text-gray-700' />}
|
||||
</div>
|
||||
}
|
||||
popupClassName='!top-[36px] !w-[387px]'
|
||||
showChecked
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
title={t(`${i18nPrefix}.title`)}
|
||||
operations={
|
||||
!readOnly ? <AddButton onClick={handleAddVariable} /> : undefined
|
||||
}
|
||||
>
|
||||
<VarList
|
||||
readonly={readOnly}
|
||||
nodeId={id}
|
||||
list={inputs.variables}
|
||||
onChange={handleVarListChange}
|
||||
onOpen={handleOnVarOpen}
|
||||
onlyLeafNodeVar
|
||||
filterVar={filterVar}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
<Split />
|
||||
<div className='px-4 pt-4 pb-2'>
|
||||
<OutputVars>
|
||||
<>
|
||||
<VarItem
|
||||
name='output'
|
||||
type={inputs.output_type}
|
||||
description={t(`${i18nPrefix}.outputVars.output`)}
|
||||
/>
|
||||
</>
|
||||
</OutputVars>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(Panel)
|
||||
@@ -0,0 +1,6 @@
|
||||
import type { CommonNodeType, ValueSelector, VarType } from '@/app/components/workflow/types'
|
||||
|
||||
export type VariableAssignerNodeType = CommonNodeType & {
|
||||
output_type: VarType
|
||||
variables: ValueSelector[]
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import produce from 'immer'
|
||||
import useVarList from './components/var-list/use-var-list'
|
||||
import type { VariableAssignerNodeType } from './types'
|
||||
import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud'
|
||||
import type { ValueSelector, Var } from '@/app/components/workflow/types'
|
||||
import { BlockEnum, VarType } from '@/app/components/workflow/types'
|
||||
import {
|
||||
useNodesReadOnly,
|
||||
useWorkflow,
|
||||
} from '@/app/components/workflow/hooks'
|
||||
|
||||
const useConfig = (id: string, payload: VariableAssignerNodeType) => {
|
||||
const { nodesReadOnly: readOnly } = useNodesReadOnly()
|
||||
const { inputs, setInputs } = useNodeCrud<VariableAssignerNodeType>(id, payload)
|
||||
const { getBeforeNodeById } = useWorkflow()
|
||||
const beforeNodes = getBeforeNodeById(id)
|
||||
|
||||
useEffect(() => {
|
||||
if (beforeNodes.length !== 1 || inputs.variables.length > 0)
|
||||
return
|
||||
const beforeNode = beforeNodes[0]
|
||||
if (beforeNode.data.type === BlockEnum.KnowledgeRetrieval) {
|
||||
const newInputs = produce(inputs, (draft: VariableAssignerNodeType) => {
|
||||
draft.output_type = VarType.array
|
||||
draft.variables[0] = [beforeNode.id, 'result']
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [beforeNodes, inputs.variables])
|
||||
|
||||
const handleOutputTypeChange = useCallback((outputType: string) => {
|
||||
const newInputs = produce(inputs, (draft: VariableAssignerNodeType) => {
|
||||
draft.output_type = outputType as VarType
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}, [inputs, setInputs])
|
||||
|
||||
const { handleVarListChange, handleAddVariable } = useVarList({
|
||||
id,
|
||||
inputs,
|
||||
setInputs,
|
||||
})
|
||||
|
||||
const { variables } = inputs
|
||||
const [currVarIndex, setCurrVarIndex] = useState(-1)
|
||||
const currVar = variables[currVarIndex]
|
||||
const handleOnVarOpen = useCallback((index: number) => {
|
||||
setCurrVarIndex(index)
|
||||
}, [])
|
||||
const filterVar = useCallback((varPayload: Var, valueSelector: ValueSelector) => {
|
||||
const type = varPayload.type
|
||||
if ((inputs.output_type !== VarType.array && type !== inputs.output_type) || (
|
||||
inputs.output_type === VarType.array && ![VarType.array, VarType.arrayString, VarType.arrayNumber, VarType.arrayObject].includes(type)
|
||||
))
|
||||
return false
|
||||
|
||||
// can not choose the same node
|
||||
if (!currVar)
|
||||
return true
|
||||
|
||||
const selectNodeId = valueSelector[0]
|
||||
|
||||
if (selectNodeId !== currVar[0] && variables.find(v => v[0] === selectNodeId))
|
||||
return false
|
||||
|
||||
return true
|
||||
}, [currVar, inputs.output_type, variables])
|
||||
return {
|
||||
readOnly,
|
||||
inputs,
|
||||
handleOutputTypeChange,
|
||||
handleVarListChange,
|
||||
handleAddVariable,
|
||||
handleOnVarOpen,
|
||||
filterVar,
|
||||
}
|
||||
}
|
||||
|
||||
export default useConfig
|
||||
@@ -0,0 +1,5 @@
|
||||
import type { VariableAssignerNodeType } from './types'
|
||||
|
||||
export const checkNodeValid = (payload: VariableAssignerNodeType) => {
|
||||
return true
|
||||
}
|
||||
Reference in New Issue
Block a user