dify
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
} from 'react'
|
||||
import {
|
||||
RiAddLine,
|
||||
} from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
useAvailableBlocks,
|
||||
useNodesInteractions,
|
||||
useNodesReadOnly,
|
||||
} from '../../hooks'
|
||||
import type { IterationNodeType } from './types'
|
||||
import cn from '@/utils/classnames'
|
||||
import BlockSelector from '@/app/components/workflow/block-selector'
|
||||
import type {
|
||||
OnSelectBlock,
|
||||
} from '@/app/components/workflow/types'
|
||||
import {
|
||||
BlockEnum,
|
||||
} from '@/app/components/workflow/types'
|
||||
|
||||
type AddBlockProps = {
|
||||
iterationNodeId: string
|
||||
iterationNodeData: IterationNodeType
|
||||
}
|
||||
const AddBlock = ({
|
||||
iterationNodeData,
|
||||
}: AddBlockProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { nodesReadOnly } = useNodesReadOnly()
|
||||
const { handleNodeAdd } = useNodesInteractions()
|
||||
const { availableNextBlocks } = useAvailableBlocks(BlockEnum.Start, true)
|
||||
|
||||
const handleSelect = useCallback<OnSelectBlock>((type, pluginDefaultValue) => {
|
||||
handleNodeAdd(
|
||||
{
|
||||
nodeType: type,
|
||||
pluginDefaultValue,
|
||||
},
|
||||
{
|
||||
prevNodeId: iterationNodeData.start_node_id,
|
||||
prevNodeSourceHandle: 'source',
|
||||
},
|
||||
)
|
||||
}, [handleNodeAdd, iterationNodeData.start_node_id])
|
||||
|
||||
const renderTriggerElement = useCallback((open: boolean) => {
|
||||
return (
|
||||
<div className={cn(
|
||||
'system-sm-medium relative inline-flex h-8 cursor-pointer items-center rounded-lg border-[0.5px] border-components-button-secondary-border bg-components-button-secondary-bg px-3 text-components-button-secondary-text shadow-xs backdrop-blur-[5px] hover:bg-components-button-secondary-bg-hover',
|
||||
`${nodesReadOnly && '!cursor-not-allowed bg-components-button-secondary-bg-disabled'}`,
|
||||
open && 'bg-components-button-secondary-bg-hover',
|
||||
)}>
|
||||
<RiAddLine className='mr-1 h-4 w-4' />
|
||||
{t('workflow.common.addBlock')}
|
||||
</div>
|
||||
)
|
||||
}, [nodesReadOnly, t])
|
||||
|
||||
return (
|
||||
<div className='absolute left-14 top-7 z-10 flex h-8 items-center'>
|
||||
<div className='group/insert relative h-0.5 w-16 bg-gray-300'>
|
||||
<div className='absolute right-0 top-1/2 h-2 w-0.5 -translate-y-1/2 bg-primary-500'></div>
|
||||
</div>
|
||||
<BlockSelector
|
||||
disabled={nodesReadOnly}
|
||||
onSelect={handleSelect}
|
||||
trigger={renderTriggerElement}
|
||||
triggerInnerClassName='inline-flex'
|
||||
popupClassName='!min-w-[256px]'
|
||||
availableBlocksTypes={availableNextBlocks}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(AddBlock)
|
||||
55
dify/web/app/components/workflow/nodes/iteration/default.ts
Normal file
55
dify/web/app/components/workflow/nodes/iteration/default.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { BlockEnum, ErrorHandleMode } from '../../types'
|
||||
import type { NodeDefault } from '../../types'
|
||||
import type { IterationNodeType } from './types'
|
||||
import { genNodeMetaData } from '@/app/components/workflow/utils'
|
||||
import { BlockClassificationEnum } from '@/app/components/workflow/block-selector/types'
|
||||
const i18nPrefix = 'workflow'
|
||||
|
||||
const metaData = genNodeMetaData({
|
||||
classification: BlockClassificationEnum.Logic,
|
||||
sort: 2,
|
||||
type: BlockEnum.Iteration,
|
||||
isTypeFixed: true,
|
||||
})
|
||||
const nodeDefault: NodeDefault<IterationNodeType> = {
|
||||
metaData,
|
||||
defaultValue: {
|
||||
start_node_id: '',
|
||||
iterator_selector: [],
|
||||
output_selector: [],
|
||||
_children: [],
|
||||
_isShowTips: false,
|
||||
is_parallel: false,
|
||||
parallel_nums: 10,
|
||||
error_handle_mode: ErrorHandleMode.Terminated,
|
||||
flatten_output: true,
|
||||
},
|
||||
checkValid(payload: IterationNodeType, t: any) {
|
||||
let errorMessages = ''
|
||||
|
||||
if (
|
||||
!errorMessages
|
||||
&& (!payload.iterator_selector || payload.iterator_selector.length === 0)
|
||||
) {
|
||||
errorMessages = t(`${i18nPrefix}.errorMsg.fieldRequired`, {
|
||||
field: t(`${i18nPrefix}.nodes.iteration.input`),
|
||||
})
|
||||
}
|
||||
|
||||
if (
|
||||
!errorMessages
|
||||
&& (!payload.output_selector || payload.output_selector.length === 0)
|
||||
) {
|
||||
errorMessages = t(`${i18nPrefix}.errorMsg.fieldRequired`, {
|
||||
field: t(`${i18nPrefix}.nodes.iteration.output`),
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
isValid: !errorMessages,
|
||||
errorMessage: errorMessages,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export default nodeDefault
|
||||
74
dify/web/app/components/workflow/nodes/iteration/node.tsx
Normal file
74
dify/web/app/components/workflow/nodes/iteration/node.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import type { FC } from 'react'
|
||||
import {
|
||||
memo,
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react'
|
||||
import {
|
||||
Background,
|
||||
useNodesInitialized,
|
||||
useViewport,
|
||||
} from 'reactflow'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { IterationStartNodeDumb } from '../iteration-start'
|
||||
import { useNodeIterationInteractions } from './use-interactions'
|
||||
import type { IterationNodeType } from './types'
|
||||
import AddBlock from './add-block'
|
||||
import cn from '@/utils/classnames'
|
||||
import type { NodeProps } from '@/app/components/workflow/types'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
|
||||
const i18nPrefix = 'workflow.nodes.iteration'
|
||||
|
||||
const Node: FC<NodeProps<IterationNodeType>> = ({
|
||||
id,
|
||||
data,
|
||||
}) => {
|
||||
const { zoom } = useViewport()
|
||||
const nodesInitialized = useNodesInitialized()
|
||||
const { handleNodeIterationRerender } = useNodeIterationInteractions()
|
||||
const { t } = useTranslation()
|
||||
const [showTips, setShowTips] = useState(data._isShowTips)
|
||||
|
||||
useEffect(() => {
|
||||
if (nodesInitialized)
|
||||
handleNodeIterationRerender(id)
|
||||
if (data.is_parallel && showTips) {
|
||||
Toast.notify({
|
||||
type: 'warning',
|
||||
message: t(`${i18nPrefix}.answerNodeWarningDesc`),
|
||||
duration: 5000,
|
||||
})
|
||||
setShowTips(false)
|
||||
}
|
||||
}, [nodesInitialized, id, handleNodeIterationRerender, data.is_parallel, showTips, t])
|
||||
|
||||
return (
|
||||
<div className={cn(
|
||||
'relative h-full min-h-[90px] w-full min-w-[240px] rounded-2xl bg-workflow-canvas-workflow-bg',
|
||||
)}>
|
||||
<Background
|
||||
id={`iteration-background-${id}`}
|
||||
className='!z-0 rounded-2xl'
|
||||
gap={[14 / zoom, 14 / zoom]}
|
||||
size={2 / zoom}
|
||||
color='var(--color-workflow-canvas-workflow-dot-color)'
|
||||
/>
|
||||
{
|
||||
data._isCandidate && (
|
||||
<IterationStartNodeDumb />
|
||||
)
|
||||
}
|
||||
{
|
||||
data._children!.length === 1 && (
|
||||
<AddBlock
|
||||
iterationNodeId={id}
|
||||
iterationNodeData={data}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(Node)
|
||||
137
dify/web/app/components/workflow/nodes/iteration/panel.tsx
Normal file
137
dify/web/app/components/workflow/nodes/iteration/panel.tsx
Normal file
@@ -0,0 +1,137 @@
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import VarReferencePicker from '../_base/components/variable/var-reference-picker'
|
||||
import Split from '../_base/components/split'
|
||||
import { MIN_ITERATION_PARALLEL_NUM } from '../../constants'
|
||||
import type { IterationNodeType } from './types'
|
||||
import useConfig from './use-config'
|
||||
import { ErrorHandleMode, type NodePanelProps } from '@/app/components/workflow/types'
|
||||
import Field from '@/app/components/workflow/nodes/_base/components/field'
|
||||
import Switch from '@/app/components/base/switch'
|
||||
import Select from '@/app/components/base/select'
|
||||
import Slider from '@/app/components/base/slider'
|
||||
import Input from '@/app/components/base/input'
|
||||
import { MAX_PARALLEL_LIMIT } from '@/config'
|
||||
|
||||
const i18nPrefix = 'workflow.nodes.iteration'
|
||||
|
||||
const Panel: FC<NodePanelProps<IterationNodeType>> = ({
|
||||
id,
|
||||
data,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const responseMethod = [
|
||||
{
|
||||
value: ErrorHandleMode.Terminated,
|
||||
name: t(`${i18nPrefix}.ErrorMethod.operationTerminated`),
|
||||
},
|
||||
{
|
||||
value: ErrorHandleMode.ContinueOnError,
|
||||
name: t(`${i18nPrefix}.ErrorMethod.continueOnError`),
|
||||
},
|
||||
{
|
||||
value: ErrorHandleMode.RemoveAbnormalOutput,
|
||||
name: t(`${i18nPrefix}.ErrorMethod.removeAbnormalOutput`),
|
||||
},
|
||||
]
|
||||
const {
|
||||
readOnly,
|
||||
inputs,
|
||||
filterInputVar,
|
||||
handleInputChange,
|
||||
childrenNodeVars,
|
||||
iterationChildrenNodes,
|
||||
handleOutputVarChange,
|
||||
changeParallel,
|
||||
changeErrorResponseMode,
|
||||
changeParallelNums,
|
||||
changeFlattenOutput,
|
||||
} = useConfig(id, data)
|
||||
|
||||
return (
|
||||
<div className='pb-2 pt-2'>
|
||||
<div className='space-y-4 px-4 pb-4'>
|
||||
<Field
|
||||
title={t(`${i18nPrefix}.input`)}
|
||||
required
|
||||
operations={(
|
||||
<div className='system-2xs-medium-uppercase flex h-[18px] items-center rounded-[5px] border border-divider-deep px-1 capitalize text-text-tertiary'>Array</div>
|
||||
)}
|
||||
>
|
||||
<VarReferencePicker
|
||||
readonly={readOnly}
|
||||
nodeId={id}
|
||||
isShowNodeName
|
||||
value={inputs.iterator_selector || []}
|
||||
onChange={handleInputChange}
|
||||
filterVar={filterInputVar}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
<Split />
|
||||
<div className='mt-2 space-y-4 px-4 pb-4'>
|
||||
<Field
|
||||
title={t(`${i18nPrefix}.output`)}
|
||||
required
|
||||
operations={(
|
||||
<div className='system-2xs-medium-uppercase flex h-[18px] items-center rounded-[5px] border border-divider-deep px-1 capitalize text-text-tertiary'>Array</div>
|
||||
)}
|
||||
>
|
||||
<VarReferencePicker
|
||||
readonly={readOnly}
|
||||
nodeId={id}
|
||||
isShowNodeName
|
||||
value={inputs.output_selector || []}
|
||||
onChange={handleOutputVarChange}
|
||||
availableNodes={iterationChildrenNodes}
|
||||
availableVars={childrenNodeVars}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
<div className='px-4 pb-2'>
|
||||
<Field title={t(`${i18nPrefix}.parallelMode`)} tooltip={<div className='w-[230px]'>{t(`${i18nPrefix}.parallelPanelDesc`)}</div>} inline>
|
||||
<Switch defaultValue={inputs.is_parallel} onChange={changeParallel} />
|
||||
</Field>
|
||||
</div>
|
||||
{
|
||||
inputs.is_parallel && (<div className='px-4 pb-2'>
|
||||
<Field title={t(`${i18nPrefix}.MaxParallelismTitle`)} isSubTitle tooltip={<div className='w-[230px]'>{t(`${i18nPrefix}.MaxParallelismDesc`)}</div>}>
|
||||
<div className='row flex'>
|
||||
<Input type='number' wrapperClassName='w-18 mr-4 ' max={MAX_PARALLEL_LIMIT} min={MIN_ITERATION_PARALLEL_NUM} value={inputs.parallel_nums} onChange={(e) => { changeParallelNums(Number(e.target.value)) }} />
|
||||
<Slider
|
||||
value={inputs.parallel_nums}
|
||||
onChange={changeParallelNums}
|
||||
max={MAX_PARALLEL_LIMIT}
|
||||
min={MIN_ITERATION_PARALLEL_NUM}
|
||||
className=' mt-4 flex-1 shrink-0'
|
||||
/>
|
||||
</div>
|
||||
|
||||
</Field>
|
||||
</div>)
|
||||
}
|
||||
<Split />
|
||||
|
||||
<div className='px-4 py-2'>
|
||||
<Field title={t(`${i18nPrefix}.errorResponseMethod`)} >
|
||||
<Select items={responseMethod} defaultValue={inputs.error_handle_mode} onSelect={changeErrorResponseMode} allowSearch={false} />
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
<Split />
|
||||
|
||||
<div className='px-4 py-2'>
|
||||
<Field
|
||||
title={t(`${i18nPrefix}.flattenOutput`)}
|
||||
tooltip={<div className='w-[230px]'>{t(`${i18nPrefix}.flattenOutputDesc`)}</div>}
|
||||
inline
|
||||
>
|
||||
<Switch defaultValue={inputs.flatten_output} onChange={changeFlattenOutput} />
|
||||
</Field>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(Panel)
|
||||
22
dify/web/app/components/workflow/nodes/iteration/types.ts
Normal file
22
dify/web/app/components/workflow/nodes/iteration/types.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import type {
|
||||
BlockEnum,
|
||||
CommonNodeType,
|
||||
ErrorHandleMode,
|
||||
ValueSelector,
|
||||
VarType,
|
||||
} from '@/app/components/workflow/types'
|
||||
|
||||
export type IterationNodeType = CommonNodeType & {
|
||||
startNodeType?: BlockEnum
|
||||
start_node_id: string // start node id in the iteration
|
||||
iteration_id?: string
|
||||
iterator_selector: ValueSelector
|
||||
iterator_input_type: VarType
|
||||
output_selector: ValueSelector
|
||||
output_type: VarType // output type.
|
||||
is_parallel: boolean // open the parallel mode or not
|
||||
parallel_nums: number // the numbers of parallel
|
||||
error_handle_mode: ErrorHandleMode // how to handle error in the iteration
|
||||
flatten_output: boolean // whether to flatten the output array if all elements are lists
|
||||
_isShowTips: boolean // when answer node in parallel mode iteration show tips
|
||||
}
|
||||
130
dify/web/app/components/workflow/nodes/iteration/use-config.ts
Normal file
130
dify/web/app/components/workflow/nodes/iteration/use-config.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import { useCallback } from 'react'
|
||||
import { produce } from 'immer'
|
||||
import {
|
||||
useIsChatMode,
|
||||
useNodesReadOnly,
|
||||
useWorkflow,
|
||||
} from '../../hooks'
|
||||
import { VarType } from '../../types'
|
||||
import type { ErrorHandleMode, ValueSelector, Var } from '../../types'
|
||||
import useNodeCrud from '../_base/hooks/use-node-crud'
|
||||
import type { IterationNodeType } from './types'
|
||||
import { toNodeOutputVars } from '../_base/components/variable/utils'
|
||||
import type { VarType as VarKindType } from '@/app/components/workflow/nodes/tool/types'
|
||||
import type { Item } from '@/app/components/base/select'
|
||||
import useInspectVarsCrud from '../../hooks/use-inspect-vars-crud'
|
||||
import { isEqual } from 'lodash-es'
|
||||
import { useStore } from '../../store'
|
||||
import {
|
||||
useAllBuiltInTools,
|
||||
useAllCustomTools,
|
||||
useAllMCPTools,
|
||||
useAllWorkflowTools,
|
||||
} from '@/service/use-tools'
|
||||
|
||||
const useConfig = (id: string, payload: IterationNodeType) => {
|
||||
const {
|
||||
deleteNodeInspectorVars,
|
||||
} = useInspectVarsCrud()
|
||||
const { nodesReadOnly: readOnly } = useNodesReadOnly()
|
||||
const isChatMode = useIsChatMode()
|
||||
|
||||
const { inputs, setInputs } = useNodeCrud<IterationNodeType>(id, payload)
|
||||
|
||||
const filterInputVar = useCallback((varPayload: Var) => {
|
||||
return [VarType.array, VarType.arrayString, VarType.arrayBoolean, VarType.arrayNumber, VarType.arrayObject, VarType.arrayFile].includes(varPayload.type)
|
||||
}, [])
|
||||
|
||||
const handleInputChange = useCallback((input: ValueSelector | string, _varKindType: VarKindType, varInfo?: Var) => {
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
draft.iterator_selector = input as ValueSelector || []
|
||||
draft.iterator_input_type = varInfo?.type || VarType.arrayString
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}, [inputs, setInputs])
|
||||
|
||||
// output
|
||||
const { getIterationNodeChildren } = useWorkflow()
|
||||
const iterationChildrenNodes = getIterationNodeChildren(id)
|
||||
const { data: buildInTools } = useAllBuiltInTools()
|
||||
const { data: customTools } = useAllCustomTools()
|
||||
const { data: workflowTools } = useAllWorkflowTools()
|
||||
const { data: mcpTools } = useAllMCPTools()
|
||||
const dataSourceList = useStore(s => s.dataSourceList)
|
||||
const allPluginInfoList = {
|
||||
buildInTools: buildInTools || [],
|
||||
customTools: customTools || [],
|
||||
workflowTools: workflowTools || [],
|
||||
mcpTools: mcpTools || [],
|
||||
dataSourceList: dataSourceList || [],
|
||||
}
|
||||
const childrenNodeVars = toNodeOutputVars(iterationChildrenNodes, isChatMode, undefined, [], [], [], allPluginInfoList)
|
||||
|
||||
const handleOutputVarChange = useCallback((output: ValueSelector | string, _varKindType: VarKindType, varInfo?: Var) => {
|
||||
if (isEqual(inputs.output_selector, output as ValueSelector))
|
||||
return
|
||||
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
draft.output_selector = output as ValueSelector || []
|
||||
const outputItemType = varInfo?.type || VarType.string
|
||||
|
||||
draft.output_type = ({
|
||||
[VarType.string]: VarType.arrayString,
|
||||
[VarType.number]: VarType.arrayNumber,
|
||||
[VarType.object]: VarType.arrayObject,
|
||||
[VarType.file]: VarType.arrayFile,
|
||||
// list operator node can output array
|
||||
[VarType.array]: VarType.array,
|
||||
[VarType.arrayFile]: VarType.arrayFile,
|
||||
[VarType.arrayString]: VarType.arrayString,
|
||||
[VarType.arrayNumber]: VarType.arrayNumber,
|
||||
[VarType.arrayObject]: VarType.arrayObject,
|
||||
} as Record<VarType, VarType>)[outputItemType] || VarType.arrayString
|
||||
})
|
||||
setInputs(newInputs)
|
||||
deleteNodeInspectorVars(id)
|
||||
}, [deleteNodeInspectorVars, id, inputs, setInputs])
|
||||
|
||||
const changeParallel = useCallback((value: boolean) => {
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
draft.is_parallel = value
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}, [inputs, setInputs])
|
||||
|
||||
const changeErrorResponseMode = useCallback((item: Item) => {
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
draft.error_handle_mode = item.value as ErrorHandleMode
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}, [inputs, setInputs])
|
||||
const changeParallelNums = useCallback((num: number) => {
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
draft.parallel_nums = num
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}, [inputs, setInputs])
|
||||
|
||||
const changeFlattenOutput = useCallback((value: boolean) => {
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
draft.flatten_output = value
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}, [inputs, setInputs])
|
||||
|
||||
return {
|
||||
readOnly,
|
||||
inputs,
|
||||
filterInputVar,
|
||||
handleInputChange,
|
||||
childrenNodeVars,
|
||||
iterationChildrenNodes,
|
||||
handleOutputVarChange,
|
||||
changeParallel,
|
||||
changeErrorResponseMode,
|
||||
changeParallelNums,
|
||||
changeFlattenOutput,
|
||||
}
|
||||
}
|
||||
|
||||
export default useConfig
|
||||
@@ -0,0 +1,165 @@
|
||||
import { useCallback } from 'react'
|
||||
import { produce } from 'immer'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
import type {
|
||||
BlockEnum,
|
||||
ChildNodeTypeCount,
|
||||
Node,
|
||||
} from '../../types'
|
||||
import {
|
||||
generateNewNode,
|
||||
getNodeCustomTypeByNodeDataType,
|
||||
} from '../../utils'
|
||||
import {
|
||||
ITERATION_PADDING,
|
||||
} from '../../constants'
|
||||
import { CUSTOM_ITERATION_START_NODE } from '../iteration-start/constants'
|
||||
import { useNodesMetaData } from '@/app/components/workflow/hooks'
|
||||
|
||||
export const useNodeIterationInteractions = () => {
|
||||
const { t } = useTranslation()
|
||||
const store = useStoreApi()
|
||||
const { nodesMap: nodesMetaDataMap } = useNodesMetaData()
|
||||
|
||||
const handleNodeIterationRerender = useCallback((nodeId: string) => {
|
||||
const {
|
||||
getNodes,
|
||||
setNodes,
|
||||
} = store.getState()
|
||||
|
||||
const nodes = getNodes()
|
||||
const currentNode = nodes.find(n => n.id === nodeId)!
|
||||
const childrenNodes = nodes.filter(n => n.parentId === nodeId)
|
||||
let rightNode: Node
|
||||
let bottomNode: Node
|
||||
|
||||
childrenNodes.forEach((n) => {
|
||||
if (rightNode) {
|
||||
if (n.position.x + n.width! > rightNode.position.x + rightNode.width!)
|
||||
rightNode = n
|
||||
}
|
||||
else {
|
||||
rightNode = n
|
||||
}
|
||||
if (bottomNode) {
|
||||
if (n.position.y + n.height! > bottomNode.position.y + bottomNode.height!)
|
||||
bottomNode = n
|
||||
}
|
||||
else {
|
||||
bottomNode = n
|
||||
}
|
||||
})
|
||||
|
||||
const widthShouldExtend = rightNode! && currentNode.width! < rightNode.position.x + rightNode.width!
|
||||
const heightShouldExtend = bottomNode! && currentNode.height! < bottomNode.position.y + bottomNode.height!
|
||||
|
||||
if (widthShouldExtend || heightShouldExtend) {
|
||||
const newNodes = produce(nodes, (draft) => {
|
||||
draft.forEach((n) => {
|
||||
if (n.id === nodeId) {
|
||||
if (widthShouldExtend) {
|
||||
n.data.width = rightNode.position.x + rightNode.width! + ITERATION_PADDING.right
|
||||
n.width = rightNode.position.x + rightNode.width! + ITERATION_PADDING.right
|
||||
}
|
||||
if (heightShouldExtend) {
|
||||
n.data.height = bottomNode.position.y + bottomNode.height! + ITERATION_PADDING.bottom
|
||||
n.height = bottomNode.position.y + bottomNode.height! + ITERATION_PADDING.bottom
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
setNodes(newNodes)
|
||||
}
|
||||
}, [store])
|
||||
|
||||
const handleNodeIterationChildDrag = useCallback((node: Node) => {
|
||||
const { getNodes } = store.getState()
|
||||
const nodes = getNodes()
|
||||
|
||||
const restrictPosition: { x?: number; y?: number } = { x: undefined, y: undefined }
|
||||
|
||||
if (node.data.isInIteration) {
|
||||
const parentNode = nodes.find(n => n.id === node.parentId)
|
||||
|
||||
if (parentNode) {
|
||||
if (node.position.y < ITERATION_PADDING.top)
|
||||
restrictPosition.y = ITERATION_PADDING.top
|
||||
if (node.position.x < ITERATION_PADDING.left)
|
||||
restrictPosition.x = ITERATION_PADDING.left
|
||||
if (node.position.x + node.width! > parentNode!.width! - ITERATION_PADDING.right)
|
||||
restrictPosition.x = parentNode!.width! - ITERATION_PADDING.right - node.width!
|
||||
if (node.position.y + node.height! > parentNode!.height! - ITERATION_PADDING.bottom)
|
||||
restrictPosition.y = parentNode!.height! - ITERATION_PADDING.bottom - node.height!
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
restrictPosition,
|
||||
}
|
||||
}, [store])
|
||||
|
||||
const handleNodeIterationChildSizeChange = useCallback((nodeId: string) => {
|
||||
const { getNodes } = store.getState()
|
||||
const nodes = getNodes()
|
||||
const currentNode = nodes.find(n => n.id === nodeId)!
|
||||
const parentId = currentNode.parentId
|
||||
|
||||
if (parentId)
|
||||
handleNodeIterationRerender(parentId)
|
||||
}, [store, handleNodeIterationRerender])
|
||||
|
||||
const handleNodeIterationChildrenCopy = useCallback((nodeId: string, newNodeId: string, idMapping: Record<string, string>) => {
|
||||
const { getNodes } = store.getState()
|
||||
const nodes = getNodes()
|
||||
const childrenNodes = nodes.filter(n => n.parentId === nodeId && n.type !== CUSTOM_ITERATION_START_NODE)
|
||||
const newIdMapping = { ...idMapping }
|
||||
const childNodeTypeCount: ChildNodeTypeCount = {}
|
||||
|
||||
const copyChildren = childrenNodes.map((child, index) => {
|
||||
const childNodeType = child.data.type as BlockEnum
|
||||
const nodesWithSameType = nodes.filter(node => node.data.type === childNodeType)
|
||||
|
||||
if(!childNodeTypeCount[childNodeType])
|
||||
childNodeTypeCount[childNodeType] = nodesWithSameType.length + 1
|
||||
else
|
||||
childNodeTypeCount[childNodeType] = childNodeTypeCount[childNodeType] + 1
|
||||
|
||||
const { newNode } = generateNewNode({
|
||||
type: getNodeCustomTypeByNodeDataType(childNodeType),
|
||||
data: {
|
||||
...nodesMetaDataMap![childNodeType].defaultValue,
|
||||
...child.data,
|
||||
selected: false,
|
||||
_isBundled: false,
|
||||
_connectedSourceHandleIds: [],
|
||||
_connectedTargetHandleIds: [],
|
||||
title: nodesWithSameType.length > 0 ? `${t(`workflow.blocks.${childNodeType}`)} ${childNodeTypeCount[childNodeType]}` : t(`workflow.blocks.${childNodeType}`),
|
||||
iteration_id: newNodeId,
|
||||
type: childNodeType,
|
||||
},
|
||||
position: child.position,
|
||||
positionAbsolute: child.positionAbsolute,
|
||||
parentId: newNodeId,
|
||||
extent: child.extent,
|
||||
zIndex: child.zIndex,
|
||||
})
|
||||
newNode.id = `${newNodeId}${newNode.id + index}`
|
||||
newIdMapping[child.id] = newNode.id
|
||||
return newNode
|
||||
})
|
||||
|
||||
return {
|
||||
copyChildren,
|
||||
newIdMapping,
|
||||
}
|
||||
}, [store, t])
|
||||
|
||||
return {
|
||||
handleNodeIterationRerender,
|
||||
handleNodeIterationChildDrag,
|
||||
handleNodeIterationChildSizeChange,
|
||||
handleNodeIterationChildrenCopy,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
import type { RefObject } from 'react'
|
||||
import type { InputVar, ValueSelector, Variable } from '@/app/components/workflow/types'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import type { IterationNodeType } from './types'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useIsNodeInIteration, useWorkflow } from '../../hooks'
|
||||
import { getNodeInfoById, getNodeUsedVarPassToServerKey, getNodeUsedVars, isSystemVar } from '../_base/components/variable/utils'
|
||||
import { InputVarType, VarType } from '@/app/components/workflow/types'
|
||||
import formatTracing from '@/app/components/workflow/run/utils/format-log'
|
||||
import type { NodeTracing } from '@/types/workflow'
|
||||
import { VALUE_SELECTOR_DELIMITER as DELIMITER } from '@/config'
|
||||
|
||||
const i18nPrefix = 'workflow.nodes.iteration'
|
||||
|
||||
type Params = {
|
||||
id: string,
|
||||
payload: IterationNodeType,
|
||||
runInputData: Record<string, any>
|
||||
runInputDataRef: RefObject<Record<string, any>>
|
||||
getInputVars: (textList: string[]) => InputVar[]
|
||||
setRunInputData: (data: Record<string, any>) => void
|
||||
toVarInputs: (variables: Variable[]) => InputVar[]
|
||||
iterationRunResult: NodeTracing[]
|
||||
}
|
||||
const useSingleRunFormParams = ({
|
||||
id,
|
||||
payload,
|
||||
runInputData,
|
||||
toVarInputs,
|
||||
setRunInputData,
|
||||
iterationRunResult,
|
||||
}: Params) => {
|
||||
const { t } = useTranslation()
|
||||
const { isNodeInIteration } = useIsNodeInIteration(id)
|
||||
|
||||
const { getIterationNodeChildren, getBeforeNodesInSameBranch } = useWorkflow()
|
||||
const iterationChildrenNodes = getIterationNodeChildren(id)
|
||||
const beforeNodes = getBeforeNodesInSameBranch(id)
|
||||
const canChooseVarNodes = [...beforeNodes, ...iterationChildrenNodes]
|
||||
|
||||
const iteratorInputKey = `${id}.input_selector`
|
||||
const iterator = runInputData[iteratorInputKey]
|
||||
const setIterator = useCallback((newIterator: string[]) => {
|
||||
setRunInputData({
|
||||
...runInputData,
|
||||
[iteratorInputKey]: newIterator,
|
||||
})
|
||||
}, [iteratorInputKey, runInputData, setRunInputData])
|
||||
|
||||
const { usedOutVars, allVarObject } = (() => {
|
||||
const vars: ValueSelector[] = []
|
||||
const varObjs: Record<string, boolean> = {}
|
||||
const allVarObject: Record<string, {
|
||||
inSingleRunPassedKey: string
|
||||
}> = {}
|
||||
iterationChildrenNodes.forEach((node) => {
|
||||
const nodeVars = getNodeUsedVars(node).filter(item => item && item.length > 0)
|
||||
nodeVars.forEach((varSelector) => {
|
||||
if (varSelector[0] === id) { // skip iteration node itself variable: item, index
|
||||
return
|
||||
}
|
||||
const isInIteration = isNodeInIteration(varSelector[0])
|
||||
if (isInIteration) // not pass iteration inner variable
|
||||
return
|
||||
|
||||
const varSectorStr = varSelector.join('.')
|
||||
if (!varObjs[varSectorStr]) {
|
||||
varObjs[varSectorStr] = true
|
||||
vars.push(varSelector)
|
||||
}
|
||||
let passToServerKeys = getNodeUsedVarPassToServerKey(node, varSelector)
|
||||
if (typeof passToServerKeys === 'string')
|
||||
passToServerKeys = [passToServerKeys]
|
||||
|
||||
passToServerKeys.forEach((key: string, index: number) => {
|
||||
allVarObject[[varSectorStr, node.id, index].join(DELIMITER)] = {
|
||||
inSingleRunPassedKey: key,
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
const res = toVarInputs(vars.map((item) => {
|
||||
const varInfo = getNodeInfoById(canChooseVarNodes, item[0])
|
||||
return {
|
||||
label: {
|
||||
nodeType: varInfo?.data.type,
|
||||
nodeName: varInfo?.data.title || canChooseVarNodes[0]?.data.title, // default start node title
|
||||
variable: isSystemVar(item) ? item.join('.') : item[item.length - 1],
|
||||
},
|
||||
variable: `${item.join('.')}`,
|
||||
value_selector: item,
|
||||
}
|
||||
}))
|
||||
return {
|
||||
usedOutVars: res,
|
||||
allVarObject,
|
||||
}
|
||||
})()
|
||||
|
||||
const setInputVarValues = useCallback((newPayload: Record<string, any>) => {
|
||||
setRunInputData(newPayload)
|
||||
}, [setRunInputData])
|
||||
const inputVarValues = (() => {
|
||||
const vars: Record<string, any> = {}
|
||||
Object.keys(runInputData)
|
||||
.forEach((key) => {
|
||||
vars[key] = runInputData[key]
|
||||
})
|
||||
return vars
|
||||
})()
|
||||
|
||||
const forms = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
inputs: [...usedOutVars],
|
||||
values: inputVarValues,
|
||||
onChange: setInputVarValues,
|
||||
},
|
||||
{
|
||||
label: t(`${i18nPrefix}.input`)!,
|
||||
inputs: [{
|
||||
label: '',
|
||||
variable: iteratorInputKey,
|
||||
type: InputVarType.iterator,
|
||||
required: false,
|
||||
getVarValueFromDependent: true,
|
||||
isFileItem: payload.iterator_input_type === VarType.arrayFile,
|
||||
}],
|
||||
values: { [iteratorInputKey]: iterator },
|
||||
onChange: (keyValue: Record<string, any>) => setIterator(keyValue[iteratorInputKey]),
|
||||
},
|
||||
]
|
||||
}, [inputVarValues, iterator, iteratorInputKey, payload.iterator_input_type, setInputVarValues, setIterator, t, usedOutVars])
|
||||
|
||||
const nodeInfo = formatTracing(iterationRunResult, t)[0]
|
||||
|
||||
const getDependentVars = () => {
|
||||
return [payload.iterator_selector]
|
||||
}
|
||||
const getDependentVar = (variable: string) => {
|
||||
if(variable === iteratorInputKey)
|
||||
return payload.iterator_selector
|
||||
}
|
||||
|
||||
return {
|
||||
forms,
|
||||
nodeInfo,
|
||||
allVarObject,
|
||||
getDependentVars,
|
||||
getDependentVar,
|
||||
}
|
||||
}
|
||||
|
||||
export default useSingleRunFormParams
|
||||
Reference in New Issue
Block a user