feat: workflow variable aggregator support group (#4811)

Co-authored-by: Yeuoly <admin@srmxy.cn>
This commit is contained in:
zxhlyh
2024-05-30 18:54:58 +08:00
committed by GitHub
parent 18ab63bd37
commit 4b91383efc
17 changed files with 147 additions and 276 deletions

View File

@@ -43,7 +43,9 @@ const AddVariablePopupWithPosition = ({
if (!showAssignVariablePopup)
return ''
if (showAssignVariablePopup.variableAssignerNodeHandleId === 'target')
const groupEnabled = showAssignVariablePopup.variableAssignerNodeData.advanced_settings?.group_enabled
if (!groupEnabled)
return showAssignVariablePopup.variableAssignerNodeData.output_type
const group = showAssignVariablePopup.variableAssignerNodeData.advanced_settings?.groups.find(group => group.groupId === showAssignVariablePopup.variableAssignerNodeHandleId)

View File

@@ -113,13 +113,11 @@ export const NodeSourceHandle = memo(({
nodeSelectorClassName,
}: NodeHandleProps) => {
const notInitialWorkflow = useStore(s => s.notInitialWorkflow)
const connectingNodePayload = useStore(s => s.connectingNodePayload)
const [open, setOpen] = useState(false)
const { handleNodeAdd } = useNodesInteractions()
const { getNodesReadOnly } = useNodesReadOnly()
const { availableNextBlocks } = useAvailableBlocks(data.type, data.isInIteration)
const isUnConnectable = !availableNextBlocks.length || ((connectingNodePayload?.nodeType === BlockEnum.VariableAssigner || connectingNodePayload?.nodeType === BlockEnum.VariableAggregator) && connectingNodePayload?.handleType === 'target')
const isConnectable = !isUnConnectable
const isConnectable = !!availableNextBlocks.length
const connected = data._connectedSourceHandleIds?.includes(handleId)
const handleOpenChange = useCallback((v: boolean) => {

View File

@@ -117,7 +117,7 @@ const BaseNode: FC<BaseNodeProps> = ({
)
}
{
data.type !== BlockEnum.VariableAssigner && data.type !== BlockEnum.VariableAggregator && !data._isCandidate && (
!data._isCandidate && (
<NodeTargetHandle
id={id}
data={data}

View File

@@ -1,6 +1,7 @@
import {
memo,
useCallback,
useState,
} from 'react'
import cn from 'classnames'
import { useVariableAssigner } from '../../hooks'
@@ -19,21 +20,18 @@ import type {
} from '@/app/components/workflow/types'
export type AddVariableProps = {
open: boolean
onOpenChange: (open: boolean) => void
variableAssignerNodeId: string
variableAssignerNodeData: VariableAssignerNodeType
availableVars: NodeOutPutVar[]
handleId?: string
}
const AddVariable = ({
open,
onOpenChange,
availableVars,
variableAssignerNodeId,
variableAssignerNodeData,
handleId,
}: AddVariableProps) => {
const [open, setOpen] = useState(false)
const { handleAssignVariableValueChange } = useVariableAssigner()
const handleSelectVariable = useCallback((v: ValueSelector, varDetail: Var) => {
@@ -43,34 +41,38 @@ const AddVariable = ({
varDetail,
handleId,
)
onOpenChange(false)
}, [handleAssignVariableValueChange, variableAssignerNodeId, handleId, onOpenChange])
setOpen(false)
}, [handleAssignVariableValueChange, variableAssignerNodeId, handleId, setOpen])
return (
<div className={cn(
'hidden group-hover:flex absolute top-0 left-0 z-10 pointer-events-none',
open && '!flex',
variableAssignerNodeData.selected && '!flex',
)}>
<PortalToFollowElem
placement={'left-start'}
offset={{
mainAxis: 4,
crossAxis: -60,
}}
placement={'right'}
offset={4}
open={open}
onOpenChange={onOpenChange}
onOpenChange={setOpen}
>
<PortalToFollowElemTrigger
onClick={() => onOpenChange(!open)}
onClick={() => setOpen(!open)}
>
<div
className={cn(
'flex items-center justify-center',
'w-4 h-4 rounded-full bg-primary-600 cursor-pointer z-10',
'group/addvariable flex items-center justify-center',
'w-4 h-4 cursor-pointer',
'hover:rounded-full hover:bg-primary-600',
open && '!rounded-full !bg-primary-600',
)}
>
<Plus02 className='w-2.5 h-2.5 text-white' />
<Plus02
className={cn(
'w-2.5 h-2.5 text-gray-500',
'group-hover/addvariable:text-white',
open && '!text-white',
)}
/>
</div>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-[1000]'>

View File

@@ -18,7 +18,7 @@ import {
useVariableAssigner,
} from '../hooks'
import { filterVar } from '../utils'
import NodeHandle from './node-handle'
import AddVariable from './add-variable'
import NodeVariableItem from './node-variable-item'
import { isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils'
@@ -47,40 +47,75 @@ const NodeGroupItem = ({
handleGroupItemMouseLeave,
} = useVariableAssigner()
const getAvailableVars = useGetAvailableVars()
const groupEnabled = item.groupEnabled
const outputType = useMemo(() => {
if (item.targetHandleId === 'target')
if (!groupEnabled)
return item.variableAssignerNodeData.output_type
const group = item.variableAssignerNodeData.advanced_settings?.groups.find(group => group.groupId === item.targetHandleId)
return group?.output_type || ''
}, [item.variableAssignerNodeData, item.targetHandleId])
}, [item.variableAssignerNodeData, item.targetHandleId, groupEnabled])
const availableVars = getAvailableVars(item.variableAssignerNodeId, item.targetHandleId, filterVar(outputType as VarType))
const showSelectionBorder = enteringNodePayload?.nodeId === item.variableAssignerNodeId && item.groupEnabled && hoveringAssignVariableGroupId === item.targetHandleId
const connected = item.variableAssignerNodeData._connectedTargetHandleIds?.includes(item.targetHandleId)
const showSelectionBorder = useMemo(() => {
if (groupEnabled && enteringNodePayload?.nodeId === item.variableAssignerNodeId) {
if (hoveringAssignVariableGroupId)
return hoveringAssignVariableGroupId !== item.targetHandleId
else
return enteringNodePayload?.nodeData.advanced_settings?.groups[0].groupId !== item.targetHandleId
}
return false
}, [enteringNodePayload, groupEnabled, hoveringAssignVariableGroupId, item.targetHandleId, item.variableAssignerNodeId])
const showSelectedBorder = useMemo(() => {
if (groupEnabled && enteringNodePayload?.nodeId === item.variableAssignerNodeId) {
if (hoveringAssignVariableGroupId)
return hoveringAssignVariableGroupId === item.targetHandleId
else
return enteringNodePayload?.nodeData.advanced_settings?.groups[0].groupId === item.targetHandleId
}
return false
}, [enteringNodePayload, groupEnabled, hoveringAssignVariableGroupId, item.targetHandleId, item.variableAssignerNodeId])
return (
<div
className={cn(
'relative pt-1 px-1.5 pb-1.5 rounded-lg border border-transparent',
showSelectionBorder && '!border-primary-600',
'relative pt-1 px-1.5 pb-1.5 rounded-lg border-[1.5px] border-transparent',
showSelectionBorder && '!border-gray-300 !border-dashed bg-black/[0.02]',
showSelectedBorder && '!border-primary-600 !bg-primary-50',
)}
onMouseEnter={() => handleGroupItemMouseEnter(item.targetHandleId)}
onMouseEnter={() => groupEnabled && handleGroupItemMouseEnter(item.targetHandleId)}
onMouseLeave={handleGroupItemMouseLeave}
>
<div className='flex items-center justify-between h-4 text-[10px] font-medium text-gray-500'>
<NodeHandle
connected={connected}
variableAssignerNodeId={item.variableAssignerNodeId}
variableAssignerNodeData={item.variableAssignerNodeData}
handleId={item.targetHandleId}
availableVars={availableVars}
/>
<span className='grow uppercase truncate' title={item.title}>{item.title}</span>
<span className='shrink-0 ml-2'>{item.type}</span>
<span
className={cn(
'grow uppercase truncate',
showSelectedBorder && 'text-primary-600',
)}
title={item.title}
>
{item.title}
</span>
<div className='flex items-center'>
<span className='shrink-0 ml-2'>{item.type}</span>
<div className='ml-2 mr-1 w-[1px] h-2.5 bg-gray-200'></div>
<AddVariable
availableVars={availableVars}
variableAssignerNodeId={item.variableAssignerNodeId}
variableAssignerNodeData={item.variableAssignerNodeData}
handleId={item.targetHandleId}
/>
</div>
</div>
{
!item.variables.length && (
<div className='relative flex items-center px-1 h-[22px] justify-between bg-gray-100 rounded-md space-x-1 text-[10px] font-normal text-gray-400 uppercase'>
<div
className={cn(
'relative flex items-center px-1 h-[22px] justify-between bg-gray-100 rounded-md space-x-1 text-[10px] font-normal text-gray-400 uppercase',
(showSelectedBorder || showSelectionBorder) && '!bg-black/[0.02]',
)}
>
{t(`${i18nPrefix}.varNotSet`)}
</div>
)
@@ -96,6 +131,7 @@ const NodeGroupItem = ({
key={index}
node={node as Node}
varName={varName}
showBorder={showSelectedBorder || showSelectionBorder}
/>
)
})

View File

@@ -1,69 +0,0 @@
import type { MouseEvent } from 'react'
import {
memo,
useCallback,
useState,
} from 'react'
import cn from 'classnames'
import {
Handle,
Position,
} from 'reactflow'
import type { VariableAssignerNodeType } from '../types'
import AddVariable from './add-variable'
import type { NodeOutPutVar } from '@/app/components/workflow/types'
import { useStore } from '@/app/components/workflow/store'
type NodeHandleProps = {
handleId?: string
connected?: boolean
variableAssignerNodeId: string
availableVars: NodeOutPutVar[]
variableAssignerNodeData: VariableAssignerNodeType
}
const NodeHandle = ({
connected,
variableAssignerNodeId,
handleId = 'target',
availableVars,
variableAssignerNodeData,
}: NodeHandleProps) => {
const [open, setOpen] = useState(false)
const connectingNodePayload = useStore(s => s.connectingNodePayload)
const isUnConnectable = connectingNodePayload?.handleType === 'source'
const handleOpenChange = useCallback((v: boolean) => {
setOpen(v)
}, [])
const handleHandleClick = useCallback((e: MouseEvent) => {
e.stopPropagation()
setOpen(v => !v)
}, [])
return (
<Handle
id={handleId}
type='target'
onClick={handleHandleClick}
position={Position.Left}
isConnectable={!isUnConnectable}
className={cn(
'!-left-[13px] !top-1 !w-4 !h-4 !bg-transparent !rounded-none !outline-none !border-none z-[1] !transform-none',
'after:absolute after:w-0.5 after:h-2 after:left-[5px] after:top-1 after:bg-primary-500 pointer-events-none',
!connected && 'after:opacity-0',
)}
>
<AddVariable
open={open}
onOpenChange={handleOpenChange}
variableAssignerNodeId={variableAssignerNodeId}
variableAssignerNodeData={variableAssignerNodeData}
handleId={handleId}
availableVars={availableVars}
/>
</Handle>
)
}
export default memo(NodeHandle)

View File

@@ -1,4 +1,5 @@
import { memo } from 'react'
import cn from 'classnames'
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'
@@ -8,13 +9,18 @@ import { BlockEnum } from '@/app/components/workflow/types'
type NodeVariableItemProps = {
node: Node
varName: string
showBorder?: boolean
}
const NodeVariableItem = ({
node,
varName,
showBorder,
}: NodeVariableItemProps) => {
return (
<div className='relative flex items-center mt-0.5 h-6 bg-gray-100 rounded-md px-1 text-xs font-normal text-gray-700' >
<div className={cn(
'relative flex items-center mt-0.5 h-6 bg-gray-100 rounded-md px-1 text-xs font-normal text-gray-700',
showBorder && '!bg-black/[0.02]',
)}>
<div className='flex items-center'>
<div className='p-[1px]'>
<VarBlockIcon

View File

@@ -1,6 +1,5 @@
import { useCallback } from 'react'
import {
useEdges,
useNodes,
useStoreApi,
} from 'reactflow'
@@ -12,9 +11,7 @@ import {
useNodeDataUpdate,
useWorkflow,
} from '../../hooks'
import { getNodesConnectedSourceOrTargetHandleIdsMap } from '../../utils'
import type {
Edge,
Node,
ValueSelector,
Var,
@@ -99,82 +96,6 @@ export const useVariableAssigner = () => {
handleAssignVariableValueChange(variableAssignerNodeId, value, varDetail, variableAssignerNodeHandleId)
}, [store, workflowStore, handleAssignVariableValueChange])
const handleRemoveEdges = useCallback((nodeId: string, enabled: boolean) => {
const {
getNodes,
setNodes,
edges,
setEdges,
} = store.getState()
const nodes = getNodes()
const needDeleteEdges = edges.filter(edge => edge.target === nodeId)
if (!needDeleteEdges.length)
return
const currentNode = nodes.find(node => node.id === nodeId)!
const groups = currentNode.data.advanced_settings?.groups || []
let shouldKeepEdges: Edge[] = []
if (enabled) {
shouldKeepEdges = edges.filter((edge) => {
return edge.target === nodeId && edge.targetHandle === 'target'
}).map((edge) => {
return {
...edge,
targetHandle: groups[0].groupId,
}
})
}
else {
shouldKeepEdges = edges.filter((edge) => {
return edge.target === nodeId && edge.targetHandle === groups[0].groupId
}).map((edge) => {
return {
...edge,
targetHandle: 'target',
}
})
}
const nodesConnectedSourceOrTargetHandleIdsMap = getNodesConnectedSourceOrTargetHandleIdsMap(
[
...needDeleteEdges.map((needDeleteEdge) => {
return {
type: 'remove',
edge: needDeleteEdge,
}
}),
...shouldKeepEdges.map((shouldKeepEdge) => {
return {
type: 'add',
edge: shouldKeepEdge,
}
}),
],
nodes,
)
const newNodes = produce(nodes, (draft) => {
draft.forEach((node) => {
if (nodesConnectedSourceOrTargetHandleIdsMap[node.id]) {
node.data = {
...node.data,
...nodesConnectedSourceOrTargetHandleIdsMap[node.id],
}
}
})
})
setNodes(newNodes)
const newEdges = produce(edges, (draft) => {
draft = draft.filter(edge => edge.target !== nodeId)
draft.push(...shouldKeepEdges)
return draft
})
setEdges(newEdges)
}, [store])
const handleGroupItemMouseEnter = useCallback((groupId: string) => {
const {
setHoveringAssignVariableGroupId,
@@ -195,7 +116,6 @@ export const useVariableAssigner = () => {
return {
handleAddVariableInAddVariablePopupWithPosition,
handleRemoveEdges,
handleGroupItemMouseEnter,
handleGroupItemMouseLeave,
handleAssignVariableValueChange,
@@ -205,8 +125,7 @@ export const useVariableAssigner = () => {
export const useGetAvailableVars = () => {
const { t } = useTranslation()
const nodes: Node[] = useNodes()
const edges: Edge[] = useEdges()
const { getBeforeNodesInSameBranch } = useWorkflow()
const { getBeforeNodesInSameBranchIncludeParent } = useWorkflow()
const isChatMode = useIsChatMode()
const getAvailableVars = useCallback((nodeId: string, handleId: string, filterVar: (v: Var) => boolean) => {
const availableNodes: Node[] = []
@@ -214,21 +133,10 @@ export const useGetAvailableVars = () => {
if (!currentNode)
return []
const beforeNodes = getBeforeNodesInSameBranchIncludeParent(nodeId)
availableNodes.push(...beforeNodes)
const parentNode = nodes.find(node => node.id === currentNode.parentId)
const connectedEdges = edges.filter(edge => edge.target === nodeId && edge.targetHandle === handleId)
if (parentNode && !connectedEdges.length) {
const beforeNodes = getBeforeNodesInSameBranch(parentNode.id)
availableNodes.push(...beforeNodes)
}
else {
connectedEdges.forEach((connectedEdge) => {
const beforeNodes = getBeforeNodesInSameBranch(connectedEdge.source)
const connectedNode = nodes.find(node => node.id === connectedEdge.source)!
availableNodes.push(connectedNode, ...beforeNodes)
})
}
return toNodeAvailableVars({
parentNode,
@@ -237,7 +145,7 @@ export const useGetAvailableVars = () => {
isChatMode,
filterVar,
})
}, [nodes, edges, t, isChatMode, getBeforeNodesInSameBranch])
}, [nodes, t, isChatMode, getBeforeNodesInSameBranchIncludeParent])
return getAvailableVars
}

View File

@@ -43,7 +43,7 @@ const Node: FC<NodeProps<VariableAssignerNodeType>> = (props) => {
}, [t, advanced_settings, data, id])
return (
<div className='relative mb-1 px-1' ref={ref}>
<div className='relative mb-1 px-1 space-y-0.5' ref={ref}>
{
groups.map((item) => {
return (

View File

@@ -1,16 +1,16 @@
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
// import cn from 'classnames'
// import Field from '../_base/components/field'
import cn from 'classnames'
import Field from '../_base/components/field'
import RemoveEffectVarConfirm from '../_base/components/remove-effect-var-confirm'
import useConfig from './use-config'
import type { VariableAssignerNodeType } from './types'
import VarGroupItem from './components/var-group-item'
import { type NodePanelProps } 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'
// import Switch from '@/app/components/base/switch'
import OutputVars, { VarItem } from '@/app/components/workflow/nodes/_base/components/output-vars'
import Switch from '@/app/components/base/switch'
import AddButton from '@/app/components/workflow/nodes/_base/components/add-button'
const i18nPrefix = 'workflow.nodes.variableAssigner'
@@ -25,7 +25,7 @@ const Panel: FC<NodePanelProps<VariableAssignerNodeType>> = ({
inputs,
handleListOrTypeChange,
isEnableGroup,
// handleGroupEnabledChange,
handleGroupEnabledChange,
handleAddGroup,
handleListOrTypeChangeInGroup,
handleGroupRemoved,
@@ -81,8 +81,8 @@ const Panel: FC<NodePanelProps<VariableAssignerNodeType>> = ({
/>
</div>)}
</div>
{/* <Split /> */}
{/* <div className={cn('px-4 pt-4', isEnableGroup ? 'pb-4' : 'pb-2')}>
<Split />
<div className={cn('px-4 pt-4', isEnableGroup ? 'pb-4' : 'pb-2')}>
<Field
title={t(`${i18nPrefix}.aggregationGroup`)}
tooltip={t(`${i18nPrefix}.aggregationGroupTip`)!}
@@ -95,8 +95,8 @@ const Panel: FC<NodePanelProps<VariableAssignerNodeType>> = ({
/>
}
/>
</div> */}
{/* {isEnableGroup && (
</div>
{isEnableGroup && (
<>
<Split />
<div className='px-4 pt-4 pb-2'>
@@ -116,7 +116,7 @@ const Panel: FC<NodePanelProps<VariableAssignerNodeType>> = ({
</OutputVars>
</div>
</>
)} */}
)}
<RemoveEffectVarConfirm
isShow={isShowRemoveVarConfirm}
onCancel={hideRemoveVarConfirm}

View File

@@ -5,7 +5,7 @@ import { v4 as uuid4 } from 'uuid'
import type { ValueSelector, Var } from '../../types'
import { VarType } from '../../types'
import type { VarGroupItem, VariableAssignerNodeType } from './types'
import { useGetAvailableVars, useVariableAssigner } from './hooks'
import { useGetAvailableVars } from './hooks'
import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud'
import {
@@ -19,7 +19,6 @@ const useConfig = (id: string, payload: VariableAssignerNodeType) => {
const { inputs, setInputs } = useNodeCrud<VariableAssignerNodeType>(id, payload)
const isEnableGroup = !!inputs.advanced_settings?.group_enabled
const { handleRemoveEdges } = useVariableAssigner()
// Not Enable Group
const handleListOrTypeChange = useCallback((payload: VarGroupItem) => {
@@ -114,8 +113,7 @@ const useConfig = (id: string, payload: VariableAssignerNodeType) => {
draft.advanced_settings.group_enabled = enabled
})
setInputs(newInputs)
handleRemoveEdges(id, enabled)
}, [handleOutVarRenameChange, id, inputs, isVarUsedInNodes, setInputs, showRemoveVarConfirm, handleRemoveEdges])
}, [handleOutVarRenameChange, id, inputs, isVarUsedInNodes, setInputs, showRemoveVarConfirm])
const handleAddGroup = useCallback(() => {
let maxInGroupName = 1