feat: workflow new nodes (#4683)
Co-authored-by: Joel <iamjoel007@gmail.com> Co-authored-by: Patryk Garstecki <patryk20120@yahoo.pl> Co-authored-by: Sebastian.W <thiner@gmail.com> Co-authored-by: 呆萌闷油瓶 <253605712@qq.com> Co-authored-by: takatost <takatost@users.noreply.github.com> Co-authored-by: rechardwang <wh_goodjob@163.com> Co-authored-by: Nite Knite <nkCoding@gmail.com> Co-authored-by: Chenhe Gu <guchenhe@gmail.com> Co-authored-by: Joshua <138381132+joshua20231026@users.noreply.github.com> Co-authored-by: Weaxs <459312872@qq.com> Co-authored-by: Ikko Eltociear Ashimine <eltociear@gmail.com> Co-authored-by: leejoo0 <81673835+leejoo0@users.noreply.github.com> Co-authored-by: JzoNg <jzongcode@gmail.com> Co-authored-by: sino <sino2322@gmail.com> Co-authored-by: Vikey Chen <vikeytk@gmail.com> Co-authored-by: wanghl <Wang-HL@users.noreply.github.com> Co-authored-by: Haolin Wang-汪皓临 <haolin.wang@atlaslovestravel.com> Co-authored-by: Zixuan Cheng <61724187+Theysua@users.noreply.github.com> Co-authored-by: crazywoola <100913391+crazywoola@users.noreply.github.com> Co-authored-by: Bowen Liang <bowenliang@apache.org> Co-authored-by: Bowen Liang <liangbowen@gf.com.cn> Co-authored-by: fanghongtai <42790567+fanghongtai@users.noreply.github.com> Co-authored-by: wxfanghongtai <wxfanghongtai@gf.com.cn> Co-authored-by: Matri <qjp@bithuman.io> Co-authored-by: Benjamin <benjaminx@gmail.com>
This commit is contained in:
128
web/app/components/workflow/nodes/iteration/add-block.tsx
Normal file
128
web/app/components/workflow/nodes/iteration/add-block.tsx
Normal file
@@ -0,0 +1,128 @@
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
} from 'react'
|
||||
import produce from 'immer'
|
||||
import cn from 'classnames'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
generateNewNode,
|
||||
} from '../../utils'
|
||||
import {
|
||||
useAvailableBlocks,
|
||||
useNodesReadOnly,
|
||||
} from '../../hooks'
|
||||
import { NODES_INITIAL_DATA } from '../../constants'
|
||||
import InsertBlock from './insert-block'
|
||||
import type { IterationNodeType } from './types'
|
||||
import BlockSelector from '@/app/components/workflow/block-selector'
|
||||
import { Plus } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import { IterationStart } from '@/app/components/base/icons/src/vender/workflow'
|
||||
import type {
|
||||
OnSelectBlock,
|
||||
} from '@/app/components/workflow/types'
|
||||
import {
|
||||
BlockEnum,
|
||||
} from '@/app/components/workflow/types'
|
||||
import TooltipPlus from '@/app/components/base/tooltip-plus'
|
||||
|
||||
type AddBlockProps = {
|
||||
iterationNodeId: string
|
||||
iterationNodeData: IterationNodeType
|
||||
}
|
||||
const AddBlock = ({
|
||||
iterationNodeId,
|
||||
iterationNodeData,
|
||||
}: AddBlockProps) => {
|
||||
const { t } = useTranslation()
|
||||
const store = useStoreApi()
|
||||
const { nodesReadOnly } = useNodesReadOnly()
|
||||
const { availableNextBlocks } = useAvailableBlocks(BlockEnum.Start, true)
|
||||
const { availablePrevBlocks } = useAvailableBlocks(iterationNodeData.startNodeType, true)
|
||||
|
||||
const handleSelect = useCallback<OnSelectBlock>((type, toolDefaultValue) => {
|
||||
const {
|
||||
getNodes,
|
||||
setNodes,
|
||||
} = store.getState()
|
||||
const nodes = getNodes()
|
||||
const nodesWithSameType = nodes.filter(node => node.data.type === type)
|
||||
const newNode = generateNewNode({
|
||||
data: {
|
||||
...NODES_INITIAL_DATA[type],
|
||||
title: nodesWithSameType.length > 0 ? `${t(`workflow.blocks.${type}`)} ${nodesWithSameType.length + 1}` : t(`workflow.blocks.${type}`),
|
||||
...(toolDefaultValue || {}),
|
||||
isIterationStart: true,
|
||||
isInIteration: true,
|
||||
iteration_id: iterationNodeId,
|
||||
},
|
||||
position: {
|
||||
x: 117,
|
||||
y: 85,
|
||||
},
|
||||
zIndex: 1001,
|
||||
parentId: iterationNodeId,
|
||||
extent: 'parent',
|
||||
})
|
||||
const newNodes = produce(nodes, (draft) => {
|
||||
draft.forEach((node) => {
|
||||
if (node.id === iterationNodeId) {
|
||||
node.data._children = [newNode.id]
|
||||
node.data.start_node_id = newNode.id
|
||||
node.data.startNodeType = newNode.data.type
|
||||
}
|
||||
})
|
||||
draft.push(newNode)
|
||||
})
|
||||
setNodes(newNodes)
|
||||
}, [store, t, iterationNodeId])
|
||||
|
||||
const renderTriggerElement = useCallback((open: boolean) => {
|
||||
return (
|
||||
<div className={cn(
|
||||
'relative inline-flex items-center px-3 h-8 rounded-lg border-[0.5px] border-gray-50 bg-white shadow-xs cursor-pointer hover:bg-gray-200 text-[13px] font-medium text-gray-700',
|
||||
`${nodesReadOnly && '!cursor-not-allowed opacity-50'}`,
|
||||
open && '!bg-gray-50',
|
||||
)}>
|
||||
<Plus className='mr-1 w-4 h-4' />
|
||||
{t('workflow.common.addBlock')}
|
||||
</div>
|
||||
)
|
||||
}, [nodesReadOnly, t])
|
||||
|
||||
return (
|
||||
<div className='absolute top-12 left-6 flex items-center h-8 z-10'>
|
||||
<TooltipPlus popupContent={t('workflow.blocks.iteration-start')}>
|
||||
<div className='flex items-center justify-center w-6 h-6 rounded-full border-[0.5px] border-black/[0.02] shadow-md bg-primary-500'>
|
||||
<IterationStart className='w-4 h-4 text-white' />
|
||||
</div>
|
||||
</TooltipPlus>
|
||||
<div className='group/insert relative w-16 h-0.5 bg-gray-300'>
|
||||
{
|
||||
iterationNodeData.startNodeType && (
|
||||
<InsertBlock
|
||||
startNodeId={iterationNodeData.start_node_id}
|
||||
availableBlocksTypes={availablePrevBlocks}
|
||||
/>
|
||||
)
|
||||
}
|
||||
<div className='absolute right-0 top-1/2 -translate-y-1/2 w-0.5 h-2 bg-primary-500'></div>
|
||||
</div>
|
||||
{
|
||||
!iterationNodeData.startNodeType && (
|
||||
<BlockSelector
|
||||
disabled={nodesReadOnly}
|
||||
onSelect={handleSelect}
|
||||
trigger={renderTriggerElement}
|
||||
triggerInnerClassName='inline-flex'
|
||||
popupClassName='!min-w-[256px]'
|
||||
availableBlocksTypes={availableNextBlocks}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(AddBlock)
|
39
web/app/components/workflow/nodes/iteration/default.ts
Normal file
39
web/app/components/workflow/nodes/iteration/default.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { BlockEnum } from '../../types'
|
||||
import type { NodeDefault } from '../../types'
|
||||
import type { IterationNodeType } from './types'
|
||||
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/constants'
|
||||
const i18nPrefix = 'workflow'
|
||||
|
||||
const nodeDefault: NodeDefault<IterationNodeType> = {
|
||||
defaultValue: {
|
||||
start_node_id: '',
|
||||
iterator_selector: [],
|
||||
output_selector: [],
|
||||
},
|
||||
getAvailablePrevNodes(isChatMode: boolean) {
|
||||
const nodes = isChatMode
|
||||
? ALL_CHAT_AVAILABLE_BLOCKS
|
||||
: ALL_COMPLETION_AVAILABLE_BLOCKS.filter(type => type !== BlockEnum.End)
|
||||
return nodes
|
||||
},
|
||||
getAvailableNextNodes(isChatMode: boolean) {
|
||||
const nodes = isChatMode ? ALL_CHAT_AVAILABLE_BLOCKS : ALL_COMPLETION_AVAILABLE_BLOCKS
|
||||
return nodes
|
||||
},
|
||||
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
|
61
web/app/components/workflow/nodes/iteration/insert-block.tsx
Normal file
61
web/app/components/workflow/nodes/iteration/insert-block.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
useState,
|
||||
} from 'react'
|
||||
import cn from 'classnames'
|
||||
import { useNodesInteractions } from '../../hooks'
|
||||
import type {
|
||||
BlockEnum,
|
||||
OnSelectBlock,
|
||||
} from '../../types'
|
||||
import BlockSelector from '../../block-selector'
|
||||
|
||||
type InsertBlockProps = {
|
||||
startNodeId: string
|
||||
availableBlocksTypes: BlockEnum[]
|
||||
}
|
||||
const InsertBlock = ({
|
||||
startNodeId,
|
||||
availableBlocksTypes,
|
||||
}: InsertBlockProps) => {
|
||||
const [open, setOpen] = useState(false)
|
||||
const { handleNodeAdd } = useNodesInteractions()
|
||||
|
||||
const handleOpenChange = useCallback((v: boolean) => {
|
||||
setOpen(v)
|
||||
}, [])
|
||||
const handleInsert = useCallback<OnSelectBlock>((nodeType, toolDefaultValue) => {
|
||||
handleNodeAdd(
|
||||
{
|
||||
nodeType,
|
||||
toolDefaultValue,
|
||||
},
|
||||
{
|
||||
nextNodeId: startNodeId,
|
||||
nextNodeTargetHandle: 'target',
|
||||
},
|
||||
)
|
||||
}, [startNodeId, handleNodeAdd])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'nopan nodrag',
|
||||
'hidden group-hover/insert:block absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2',
|
||||
open && '!block',
|
||||
)}
|
||||
>
|
||||
<BlockSelector
|
||||
open={open}
|
||||
onOpenChange={handleOpenChange}
|
||||
asChild
|
||||
onSelect={handleInsert}
|
||||
availableBlocksTypes={availableBlocksTypes}
|
||||
triggerClassName={() => 'hover:scale-125 transition-all'}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(InsertBlock)
|
49
web/app/components/workflow/nodes/iteration/node.tsx
Normal file
49
web/app/components/workflow/nodes/iteration/node.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import type { FC } from 'react'
|
||||
import {
|
||||
memo,
|
||||
useEffect,
|
||||
} from 'react'
|
||||
import {
|
||||
Background,
|
||||
useNodesInitialized,
|
||||
useViewport,
|
||||
} from 'reactflow'
|
||||
import cn from 'classnames'
|
||||
import { useNodeIterationInteractions } from './use-interactions'
|
||||
import type { IterationNodeType } from './types'
|
||||
import AddBlock from './add-block'
|
||||
import type { NodeProps } from '@/app/components/workflow/types'
|
||||
|
||||
const Node: FC<NodeProps<IterationNodeType>> = ({
|
||||
id,
|
||||
data,
|
||||
}) => {
|
||||
const { zoom } = useViewport()
|
||||
const nodesInitialized = useNodesInitialized()
|
||||
const { handleNodeIterationRerender } = useNodeIterationInteractions()
|
||||
|
||||
useEffect(() => {
|
||||
if (nodesInitialized)
|
||||
handleNodeIterationRerender(id)
|
||||
}, [nodesInitialized, id, handleNodeIterationRerender])
|
||||
|
||||
return (
|
||||
<div className={cn(
|
||||
'relative min-w-[258px] min-h-[118px] w-full h-full rounded-2xl bg-[#F0F2F7]/90',
|
||||
)}>
|
||||
<Background
|
||||
id={`iteration-background-${id}`}
|
||||
className='rounded-2xl !z-0'
|
||||
gap={[14 / zoom, 14 / zoom]}
|
||||
size={2 / zoom}
|
||||
color='#E4E5E7'
|
||||
/>
|
||||
<AddBlock
|
||||
iterationNodeId={id}
|
||||
iterationNodeData={data}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(Node)
|
136
web/app/components/workflow/nodes/iteration/panel.tsx
Normal file
136
web/app/components/workflow/nodes/iteration/panel.tsx
Normal file
@@ -0,0 +1,136 @@
|
||||
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 ResultPanel from '../../run/result-panel'
|
||||
import IterationResultPanel from '../../run/iteration-result-panel'
|
||||
import type { IterationNodeType } from './types'
|
||||
import useConfig from './use-config'
|
||||
import { InputVarType, type NodePanelProps } from '@/app/components/workflow/types'
|
||||
import Field from '@/app/components/workflow/nodes/_base/components/field'
|
||||
import BeforeRunForm from '@/app/components/workflow/nodes/_base/components/before-run-form'
|
||||
import { ArrowNarrowRight } from '@/app/components/base/icons/src/vender/line/arrows'
|
||||
|
||||
const i18nPrefix = 'workflow.nodes.iteration'
|
||||
|
||||
const Panel: FC<NodePanelProps<IterationNodeType>> = ({
|
||||
id,
|
||||
data,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const {
|
||||
readOnly,
|
||||
inputs,
|
||||
filterInputVar,
|
||||
handleInputChange,
|
||||
childrenNodeVars,
|
||||
iterationChildrenNodes,
|
||||
handleOutputVarChange,
|
||||
isShowSingleRun,
|
||||
hideSingleRun,
|
||||
isShowIterationDetail,
|
||||
backToSingleRun,
|
||||
showIterationDetail,
|
||||
hideIterationDetail,
|
||||
runningStatus,
|
||||
handleRun,
|
||||
handleStop,
|
||||
runResult,
|
||||
inputVarValues,
|
||||
setInputVarValues,
|
||||
usedOutVars,
|
||||
iterator,
|
||||
setIterator,
|
||||
iteratorInputKey,
|
||||
iterationRunResult,
|
||||
} = useConfig(id, data)
|
||||
|
||||
return (
|
||||
<div className='mt-2'>
|
||||
<div className='px-4 pb-4 space-y-4'>
|
||||
<Field
|
||||
title={t(`${i18nPrefix}.input`)}
|
||||
>
|
||||
<VarReferencePicker
|
||||
readonly={readOnly}
|
||||
nodeId={id}
|
||||
isShowNodeName
|
||||
value={inputs.iterator_selector || []}
|
||||
onChange={handleInputChange}
|
||||
filterVar={filterInputVar}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
<Split />
|
||||
<div className='mt-2 px-4 pb-4 space-y-4'>
|
||||
<Field
|
||||
title={t(`${i18nPrefix}.output`)}
|
||||
operations={(
|
||||
<div className='flex items-center h-[18px] px-1 border border-black/8 rounded-[5px] text-xs font-medium text-gray-500 capitalize'>Array</div>
|
||||
)}
|
||||
>
|
||||
<VarReferencePicker
|
||||
readonly={readOnly}
|
||||
nodeId={id}
|
||||
isShowNodeName
|
||||
value={inputs.output_selector || []}
|
||||
onChange={handleOutputVarChange}
|
||||
availableNodes={iterationChildrenNodes}
|
||||
availableVars={childrenNodeVars}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
{isShowSingleRun && (
|
||||
<BeforeRunForm
|
||||
nodeName={inputs.title}
|
||||
onHide={hideSingleRun}
|
||||
forms={[
|
||||
{
|
||||
inputs: [...usedOutVars],
|
||||
values: inputVarValues,
|
||||
onChange: setInputVarValues,
|
||||
},
|
||||
{
|
||||
label: t(`${i18nPrefix}.input`)!,
|
||||
inputs: [{
|
||||
label: '',
|
||||
variable: iteratorInputKey,
|
||||
type: InputVarType.iterator,
|
||||
required: false,
|
||||
}],
|
||||
values: { [iteratorInputKey]: iterator },
|
||||
onChange: keyValue => setIterator((keyValue as any)[iteratorInputKey]),
|
||||
},
|
||||
]}
|
||||
runningStatus={runningStatus}
|
||||
onRun={handleRun}
|
||||
onStop={handleStop}
|
||||
result={
|
||||
<div className='mt-3'>
|
||||
<div className='px-4'>
|
||||
<div className='flex items-center h-[34px] justify-between px-3 bg-gray-100 border-[0.5px] border-gray-200 rounded-lg cursor-pointer' onClick={showIterationDetail}>
|
||||
<div className='leading-[18px] text-[13px] font-medium text-gray-700'>{t(`${i18nPrefix}.iteration`, { count: iterationRunResult.length })}</div>
|
||||
<ArrowNarrowRight className='w-3.5 h-3.5 text-gray-500' />
|
||||
</div>
|
||||
<Split className='mt-3' />
|
||||
</div>
|
||||
<ResultPanel {...runResult} showSteps={false} />
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{isShowIterationDetail && (
|
||||
<IterationResultPanel
|
||||
onBack={backToSingleRun}
|
||||
onHide={hideIterationDetail}
|
||||
list={iterationRunResult}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(Panel)
|
15
web/app/components/workflow/nodes/iteration/types.ts
Normal file
15
web/app/components/workflow/nodes/iteration/types.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import type {
|
||||
BlockEnum,
|
||||
CommonNodeType,
|
||||
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
|
||||
output_selector: ValueSelector
|
||||
output_type: VarType // output type.
|
||||
}
|
215
web/app/components/workflow/nodes/iteration/use-config.ts
Normal file
215
web/app/components/workflow/nodes/iteration/use-config.ts
Normal file
@@ -0,0 +1,215 @@
|
||||
import { useCallback } from 'react'
|
||||
import produce from 'immer'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import {
|
||||
useIsChatMode,
|
||||
useIsNodeInIteration,
|
||||
useNodesReadOnly,
|
||||
useWorkflow,
|
||||
} from '../../hooks'
|
||||
import { VarType } from '../../types'
|
||||
import type { ValueSelector, Var } from '../../types'
|
||||
import useNodeCrud from '../_base/hooks/use-node-crud'
|
||||
import { getNodeInfoById, getNodeUsedVarPassToServerKey, getNodeUsedVars, isSystemVar, toNodeOutputVars } from '../_base/components/variable/utils'
|
||||
import useOneStepRun from '../_base/hooks/use-one-step-run'
|
||||
import type { IterationNodeType } from './types'
|
||||
import type { VarType as VarKindType } from '@/app/components/workflow/nodes/tool/types'
|
||||
|
||||
const DELIMITER = '@@@@@'
|
||||
const useConfig = (id: string, payload: IterationNodeType) => {
|
||||
const { nodesReadOnly: readOnly } = useNodesReadOnly()
|
||||
const { isNodeInIteration } = useIsNodeInIteration(id)
|
||||
const isChatMode = useIsChatMode()
|
||||
|
||||
const { inputs, setInputs } = useNodeCrud<IterationNodeType>(id, payload)
|
||||
|
||||
const filterInputVar = useCallback((varPayload: Var) => {
|
||||
return [VarType.array, VarType.arrayString, VarType.arrayNumber, VarType.arrayObject].includes(varPayload.type)
|
||||
}, [])
|
||||
|
||||
const handleInputChange = useCallback((input: ValueSelector | string) => {
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
draft.iterator_selector = input as ValueSelector || []
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}, [inputs, setInputs])
|
||||
|
||||
// output
|
||||
const { getIterationNodeChildren, getBeforeNodesInSameBranch } = useWorkflow()
|
||||
const beforeNodes = getBeforeNodesInSameBranch(id)
|
||||
const iterationChildrenNodes = getIterationNodeChildren(id)
|
||||
const canChooseVarNodes = [...beforeNodes, ...iterationChildrenNodes]
|
||||
const childrenNodeVars = toNodeOutputVars(iterationChildrenNodes, isChatMode)
|
||||
|
||||
const handleOutputVarChange = useCallback((output: ValueSelector | string, _varKindType: VarKindType, varInfo?: Var) => {
|
||||
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,
|
||||
} as Record<VarType, VarType>)[outputItemType] || VarType.arrayString
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}, [inputs, setInputs])
|
||||
|
||||
// single run
|
||||
const iteratorInputKey = `${id}.input_selector`
|
||||
const {
|
||||
isShowSingleRun,
|
||||
showSingleRun,
|
||||
hideSingleRun,
|
||||
toVarInputs,
|
||||
runningStatus,
|
||||
handleRun: doHandleRun,
|
||||
handleStop,
|
||||
runInputData,
|
||||
setRunInputData,
|
||||
runResult,
|
||||
iterationRunResult,
|
||||
} = useOneStepRun<IterationNodeType>({
|
||||
id,
|
||||
data: inputs,
|
||||
iteratorInputKey,
|
||||
defaultRunInputData: {
|
||||
[iteratorInputKey]: [''],
|
||||
},
|
||||
})
|
||||
|
||||
const [isShowIterationDetail, {
|
||||
setTrue: doShowIterationDetail,
|
||||
setFalse: doHideIterationDetail,
|
||||
}] = useBoolean(false)
|
||||
|
||||
const hideIterationDetail = useCallback(() => {
|
||||
hideSingleRun()
|
||||
doHideIterationDetail()
|
||||
}, [doHideIterationDetail, hideSingleRun])
|
||||
|
||||
const showIterationDetail = useCallback(() => {
|
||||
doShowIterationDetail()
|
||||
}, [doShowIterationDetail])
|
||||
|
||||
const backToSingleRun = useCallback(() => {
|
||||
hideIterationDetail()
|
||||
showSingleRun()
|
||||
}, [hideIterationDetail, showSingleRun])
|
||||
|
||||
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 handleRun = useCallback((data: Record<string, any>) => {
|
||||
const formattedData: Record<string, any> = {}
|
||||
Object.keys(allVarObject).forEach((key) => {
|
||||
const [varSectorStr, nodeId] = key.split(DELIMITER)
|
||||
formattedData[`${nodeId}.${allVarObject[key].inSingleRunPassedKey}`] = data[varSectorStr]
|
||||
})
|
||||
formattedData[iteratorInputKey] = data[iteratorInputKey]
|
||||
doHandleRun(formattedData)
|
||||
}, [allVarObject, doHandleRun, iteratorInputKey])
|
||||
|
||||
const inputVarValues = (() => {
|
||||
const vars: Record<string, any> = {}
|
||||
Object.keys(runInputData)
|
||||
.filter(key => ![iteratorInputKey].includes(key))
|
||||
.forEach((key) => {
|
||||
vars[key] = runInputData[key]
|
||||
})
|
||||
return vars
|
||||
})()
|
||||
|
||||
const setInputVarValues = useCallback((newPayload: Record<string, any>) => {
|
||||
const newVars = {
|
||||
...newPayload,
|
||||
[iteratorInputKey]: runInputData[iteratorInputKey],
|
||||
}
|
||||
setRunInputData(newVars)
|
||||
}, [iteratorInputKey, runInputData, setRunInputData])
|
||||
|
||||
const iterator = runInputData[iteratorInputKey]
|
||||
const setIterator = useCallback((newIterator: string[]) => {
|
||||
setRunInputData({
|
||||
...runInputData,
|
||||
[iteratorInputKey]: newIterator,
|
||||
})
|
||||
}, [iteratorInputKey, runInputData, setRunInputData])
|
||||
|
||||
return {
|
||||
readOnly,
|
||||
inputs,
|
||||
filterInputVar,
|
||||
handleInputChange,
|
||||
childrenNodeVars,
|
||||
iterationChildrenNodes,
|
||||
handleOutputVarChange,
|
||||
isShowSingleRun,
|
||||
showSingleRun,
|
||||
hideSingleRun,
|
||||
isShowIterationDetail,
|
||||
showIterationDetail,
|
||||
hideIterationDetail,
|
||||
backToSingleRun,
|
||||
runningStatus,
|
||||
handleRun,
|
||||
handleStop,
|
||||
runResult,
|
||||
inputVarValues,
|
||||
setInputVarValues,
|
||||
usedOutVars,
|
||||
iterator,
|
||||
setIterator,
|
||||
iteratorInputKey,
|
||||
iterationRunResult,
|
||||
}
|
||||
}
|
||||
|
||||
export default useConfig
|
142
web/app/components/workflow/nodes/iteration/use-interactions.ts
Normal file
142
web/app/components/workflow/nodes/iteration/use-interactions.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
import { useCallback } from 'react'
|
||||
import produce from 'immer'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
import type {
|
||||
BlockEnum,
|
||||
Node,
|
||||
} from '../../types'
|
||||
import { generateNewNode } from '../../utils'
|
||||
import {
|
||||
ITERATION_PADDING,
|
||||
NODES_INITIAL_DATA,
|
||||
} from '../../constants'
|
||||
|
||||
export const useNodeIterationInteractions = () => {
|
||||
const { t } = useTranslation()
|
||||
const store = useStoreApi()
|
||||
|
||||
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) => {
|
||||
const { getNodes } = store.getState()
|
||||
const nodes = getNodes()
|
||||
const childrenNodes = nodes.filter(n => n.parentId === nodeId)
|
||||
|
||||
return childrenNodes.map((child, index) => {
|
||||
const childNodeType = child.data.type as BlockEnum
|
||||
const nodesWithSameType = nodes.filter(node => node.data.type === childNodeType)
|
||||
const newNode = generateNewNode({
|
||||
data: {
|
||||
...NODES_INITIAL_DATA[childNodeType],
|
||||
...child.data,
|
||||
selected: false,
|
||||
_isBundled: false,
|
||||
_connectedSourceHandleIds: [],
|
||||
_connectedTargetHandleIds: [],
|
||||
title: nodesWithSameType.length > 0 ? `${t(`workflow.blocks.${childNodeType}`)} ${nodesWithSameType.length + 1}` : t(`workflow.blocks.${childNodeType}`),
|
||||
},
|
||||
position: child.position,
|
||||
positionAbsolute: child.positionAbsolute,
|
||||
parentId: newNodeId,
|
||||
extent: child.extent,
|
||||
zIndex: child.zIndex,
|
||||
})
|
||||
newNode.id = `${newNodeId}${newNode.id + index}`
|
||||
return newNode
|
||||
})
|
||||
}, [store, t])
|
||||
|
||||
return {
|
||||
handleNodeIterationRerender,
|
||||
handleNodeIterationChildDrag,
|
||||
handleNodeIterationChildSizeChange,
|
||||
handleNodeIterationChildrenCopy,
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user