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:
zxhlyh
2024-05-27 21:57:08 +08:00
committed by GitHub
parent 444fdb79dc
commit 45deaee762
210 changed files with 9951 additions and 2223 deletions

View File

@@ -0,0 +1,87 @@
import {
memo,
useCallback,
} from 'react'
import cn from 'classnames'
import { useVariableAssigner } from '../../hooks'
import type { VariableAssignerNodeType } from '../../types'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import { Plus02 } from '@/app/components/base/icons/src/vender/line/general'
import AddVariablePopup from '@/app/components/workflow/nodes/_base/components/add-variable-popup'
import type {
NodeOutPutVar,
ValueSelector,
Var,
} 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 { handleAssignVariableValueChange } = useVariableAssigner()
const handleSelectVariable = useCallback((v: ValueSelector, varDetail: Var) => {
handleAssignVariableValueChange(
variableAssignerNodeId,
v,
varDetail,
handleId,
)
onOpenChange(false)
}, [handleAssignVariableValueChange, variableAssignerNodeId, handleId, onOpenChange])
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,
}}
open={open}
onOpenChange={onOpenChange}
>
<PortalToFollowElemTrigger
onClick={() => onOpenChange(!open)}
>
<div
className={cn(
'flex items-center justify-center',
'w-4 h-4 rounded-full bg-primary-600 cursor-pointer z-10',
)}
>
<Plus02 className='w-2.5 h-2.5 text-white' />
</div>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-[1000]'>
<AddVariablePopup
onSelect={handleSelectVariable}
availableVars={availableVars}
/>
</PortalToFollowElemContent>
</PortalToFollowElem>
</div>
)
}
export default memo(AddVariable)

View File

@@ -0,0 +1,107 @@
import {
memo,
useMemo,
} from 'react'
import cn from 'classnames'
import { useTranslation } from 'react-i18next'
import { useNodes } from 'reactflow'
import { useStore } from '../../../store'
import { BlockEnum } from '../../../types'
import type {
Node,
ValueSelector,
VarType,
} from '../../../types'
import type { VariableAssignerNodeType } from '../types'
import {
useGetAvailableVars,
useVariableAssigner,
} from '../hooks'
import { filterVar } from '../utils'
import NodeHandle from './node-handle'
import NodeVariableItem from './node-variable-item'
import { isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils'
const i18nPrefix = 'workflow.nodes.variableAssigner'
type GroupItem = {
groupEnabled: boolean
targetHandleId: string
title: string
type: string
variables: ValueSelector[]
variableAssignerNodeId: string
variableAssignerNodeData: VariableAssignerNodeType
}
type NodeGroupItemProps = {
item: GroupItem
}
const NodeGroupItem = ({
item,
}: NodeGroupItemProps) => {
const { t } = useTranslation()
const enteringNodePayload = useStore(s => s.enteringNodePayload)
const hoveringAssignVariableGroupId = useStore(s => s.hoveringAssignVariableGroupId)
const nodes: Node[] = useNodes()
const {
handleGroupItemMouseEnter,
handleGroupItemMouseLeave,
} = useVariableAssigner()
const getAvailableVars = useGetAvailableVars()
const outputType = useMemo(() => {
if (item.targetHandleId === 'target')
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])
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)
return (
<div
className={cn(
'relative pt-1 px-1.5 pb-1.5 rounded-lg border border-transparent',
showSelectionBorder && '!border-primary-600',
)}
onMouseEnter={() => 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>
</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'>
{t(`${i18nPrefix}.varNotSet`)}
</div>
)
}
{
!!item.variables.length && item.variables.map((variable = [], index) => {
const isSystem = isSystemVar(variable)
const node = isSystem ? nodes.find(node => node.data.type === BlockEnum.Start) : nodes.find(node => node.id === variable[0])
const varName = isSystem ? `sys.${variable[variable.length - 1]}` : variable.slice(1).join('.')
return (
<NodeVariableItem
key={index}
node={node as Node}
varName={varName}
/>
)
})
}
</div>
)
}
export default memo(NodeGroupItem)

View File

@@ -0,0 +1,69 @@
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

@@ -0,0 +1,36 @@
import { memo } from 'react'
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 type { Node } from '@/app/components/workflow/types'
import { BlockEnum } from '@/app/components/workflow/types'
type NodeVariableItemProps = {
node: Node
varName: string
}
const NodeVariableItem = ({
node,
varName,
}: 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='flex items-center'>
<div className='p-[1px]'>
<VarBlockIcon
className='!text-gray-900'
type={node?.data.type || BlockEnum.Start}
/>
</div>
<div className='max-w-[85px] truncate mx-0.5 text-xs font-medium text-gray-700' title={node?.data.title}>{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='max-w-[75px] truncate ml-0.5 text-xs font-medium' title={varName}>{varName}</div>
</div>
</div>
)
}
export default memo(NodeVariableItem)

View File

@@ -0,0 +1,176 @@
'use client'
import React, { useCallback } from 'react'
import type { ChangeEvent, FC } from 'react'
import { useTranslation } from 'react-i18next'
import produce from 'immer'
import { useBoolean } from 'ahooks'
import type { VarGroupItem as VarGroupItemType } from '../types'
import VarReferencePicker from '../../_base/components/variable/var-reference-picker'
import VarList from '../components/var-list'
import Field from '@/app/components/workflow/nodes/_base/components/field'
import { VarType } from '@/app/components/workflow/types'
import type { NodeOutPutVar, ValueSelector, Var } from '@/app/components/workflow/types'
import { VarType as VarKindType } from '@/app/components/workflow/nodes/tool/types'
import { Trash03 } from '@/app/components/base/icons/src/vender/line/general'
import { Folder } from '@/app/components/base/icons/src/vender/line/files'
import { checkKeys } from '@/utils/var'
import Toast from '@/app/components/base/toast'
const i18nPrefix = 'workflow.nodes.variableAssigner'
type Payload = VarGroupItemType & {
group_name?: string
}
type Props = {
readOnly: boolean
nodeId: string
payload: Payload
onChange: (newPayload: Payload) => void
groupEnabled: boolean
onGroupNameChange?: (value: string) => void
canRemove?: boolean
onRemove?: () => void
availableVars: NodeOutPutVar[]
}
const VarGroupItem: FC<Props> = ({
readOnly,
nodeId,
payload,
onChange,
groupEnabled,
onGroupNameChange,
canRemove,
onRemove,
availableVars,
}) => {
const { t } = useTranslation()
const handleAddVariable = useCallback((value: ValueSelector | string, _varKindType: VarKindType, varInfo?: Var) => {
const chosenVariables = payload.variables
if (chosenVariables.some(item => item.join('.') === (value as ValueSelector).join('.')))
return
const newPayload = produce(payload, (draft: Payload) => {
draft.variables.push(value as ValueSelector)
if (varInfo && varInfo.type !== VarType.any)
draft.output_type = varInfo.type
})
onChange(newPayload)
}, [onChange, payload])
const handleListChange = useCallback((newList: ValueSelector[], changedItem?: ValueSelector) => {
if (changedItem) {
const chosenVariables = payload.variables
if (chosenVariables.some(item => item.join('.') === (changedItem as ValueSelector).join('.')))
return
}
const newPayload = produce(payload, (draft) => {
draft.variables = newList
if (newList.length === 0)
draft.output_type = VarType.any
})
onChange(newPayload)
}, [onChange, payload])
const filterVar = useCallback((varPayload: Var) => {
if (payload.output_type === VarType.any)
return true
return varPayload.type === payload.output_type
}, [payload.output_type])
const [isEditGroupName, {
setTrue: setEditGroupName,
setFalse: setNotEditGroupName,
}] = useBoolean(false)
const handleGroupNameChange = useCallback((e: ChangeEvent<any>) => {
const value = e.target.value
const { isValid, errorKey, errorMessageKey } = checkKeys([value], false)
if (!isValid) {
Toast.notify({
type: 'error',
message: t(`appDebug.varKeyError.${errorMessageKey}`, { key: errorKey }),
})
return
}
onGroupNameChange?.(value)
}, [onGroupNameChange, t])
return (
<Field
className='group'
title={groupEnabled
? <div className='flex items-center'>
<div className='flex items-center !normal-case'>
<Folder className='mr-0.5 w-3.5 h-3.5' />
{(!isEditGroupName)
? (
<div className='flex items-center h-6 px-1 rounded-lg cursor-text hover:bg-gray-100' onClick={setEditGroupName}>
{payload.group_name}
</div>
)
: (
<input
type='text'
className='h-6 px-1 rounded-lg bg-white border border-gray-300 focus:outline-none'
// style={{
// width: `${((payload.group_name?.length || 0) + 1) / 2}em`,
// }}
size={payload.group_name?.length} // to fit the input width
autoFocus
value={payload.group_name}
onChange={handleGroupNameChange}
onBlur={setNotEditGroupName}
maxLength={30}
/>)}
</div>
{canRemove && (
<div
className='group-hover:block hidden ml-0.5 p-1 rounded-md text-gray-500 cursor-pointer hover:bg-[#FEE4E2] hover:text-[#D92D20]'
onClick={onRemove}
>
<Trash03
className='w-4 h-4'
/>
</div>
)}
</div>
: t(`${i18nPrefix}.title`)!}
operations={
<div className='flex items-center h-6 space-x-2'>
{payload.variables.length > 0 && (
<div className='flex items-center h-[18px] px-1 border border-black/8 rounded-[5px] text-xs font-medium text-gray-500 capitalize'>{payload.output_type}</div>
)}
{
!readOnly
? <VarReferencePicker
isAddBtnTrigger
readonly={false}
nodeId={nodeId}
isShowNodeName
value={[]}
onChange={handleAddVariable}
defaultVarKindType={VarKindType.variable}
filterVar={filterVar}
availableVars={availableVars}
/>
: undefined
}
</div>
}
>
<VarList
readonly={readOnly}
nodeId={nodeId}
list={payload.variables}
onChange={handleListChange}
filterVar={filterVar}
/>
</Field>
)
}
export default React.memo(VarGroupItem)

View File

@@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next'
import React, { useCallback } from 'react'
import produce from 'immer'
import RemoveButton from '../../../_base/components/remove-button'
import ListNoDataPlaceholder from '../../../_base/components/list-no-data-placeholder'
import VarReferencePicker from '@/app/components/workflow/nodes/_base/components/variable/var-reference-picker'
import type { ValueSelector, Var } from '@/app/components/workflow/types'
import { VarType as VarKindType } from '@/app/components/workflow/nodes/tool/types'
@@ -12,9 +13,8 @@ type Props = {
readonly: boolean
nodeId: string
list: ValueSelector[]
onChange: (list: ValueSelector[]) => void
onChange: (list: ValueSelector[], value?: ValueSelector) => void
onOpen?: (index: number) => void
onlyLeafNodeVar?: boolean
filterVar?: (payload: Var, valueSelector: ValueSelector) => boolean
}
@@ -24,7 +24,6 @@ const VarList: FC<Props> = ({
list,
onChange,
onOpen = () => { },
onlyLeafNodeVar,
filterVar,
}) => {
const { t } = useTranslation()
@@ -33,7 +32,7 @@ const VarList: FC<Props> = ({
const newList = produce(list, (draft) => {
draft[index] = value as ValueSelector
})
onChange(newList)
onChange(newList, value as ValueSelector)
}
}, [list, onChange])
@@ -52,9 +51,9 @@ const VarList: FC<Props> = ({
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'>
<ListNoDataPlaceholder>
{t('workflow.nodes.variableAssigner.noVarTip')}
</div>
</ListNoDataPlaceholder>
)
}
@@ -70,7 +69,6 @@ const VarList: FC<Props> = ({
value={item}
onChange={handleVarReferenceChange(index)}
onOpen={handleOpen(index)}
onlyLeafNodeVar={onlyLeafNodeVar}
filterVar={filterVar}
defaultVarKindType={VarKindType.variable}
/>

View File

@@ -2,7 +2,6 @@ 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
@@ -10,18 +9,15 @@ type Params = {
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])
}, [inputs, setInputs])
const handleAddVariable = useCallback(() => {
const newInputs = produce(inputs, (draft) => {

View File

@@ -7,14 +7,14 @@ const i18nPrefix = 'workflow'
const nodeDefault: NodeDefault<VariableAssignerNodeType> = {
defaultValue: {
output_type: VarType.string,
output_type: VarType.any,
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)
return nodes
},
getAvailableNextNodes(isChatMode: boolean) {
const nodes = isChatMode ? ALL_CHAT_AVAILABLE_BLOCKS : ALL_COMPLETION_AVAILABLE_BLOCKS

View File

@@ -0,0 +1,243 @@
import { useCallback } from 'react'
import {
useEdges,
useNodes,
useStoreApi,
} from 'reactflow'
import { useTranslation } from 'react-i18next'
import { uniqBy } from 'lodash-es'
import produce from 'immer'
import {
useIsChatMode,
useNodeDataUpdate,
useWorkflow,
} from '../../hooks'
import { getNodesConnectedSourceOrTargetHandleIdsMap } from '../../utils'
import type {
Edge,
Node,
ValueSelector,
Var,
} from '../../types'
import { useWorkflowStore } from '../../store'
import type {
VarGroupItem,
VariableAssignerNodeType,
} from './types'
import { toNodeAvailableVars } from '@/app/components/workflow/nodes/_base/components/variable/utils'
export const useVariableAssigner = () => {
const store = useStoreApi()
const workflowStore = useWorkflowStore()
const { handleNodeDataUpdate } = useNodeDataUpdate()
const handleAssignVariableValueChange = useCallback((nodeId: string, value: ValueSelector, varDetail: Var, groupId?: string) => {
const { getNodes } = store.getState()
const nodes = getNodes()
const node: Node<VariableAssignerNodeType> = nodes.find(node => node.id === nodeId)!
let payload
if (groupId && groupId !== 'target') {
payload = {
advanced_settings: {
...node.data.advanced_settings,
groups: node.data.advanced_settings?.groups.map((group: VarGroupItem & { groupId: string }) => {
if (group.groupId === groupId && !group.variables.some(item => item.join('.') === (value as ValueSelector).join('.'))) {
return {
...group,
variables: [...group.variables, value],
output_type: varDetail.type,
}
}
return group
}),
},
}
}
else {
if (node.data.variables.some(item => item.join('.') === (value as ValueSelector).join('.')))
return
payload = {
variables: [...node.data.variables, value],
output_type: varDetail.type,
}
}
handleNodeDataUpdate({
id: nodeId,
data: payload,
})
}, [store, handleNodeDataUpdate])
const handleAddVariableInAddVariablePopupWithPosition = useCallback((
nodeId: string,
variableAssignerNodeId: string,
variableAssignerNodeHandleId: string,
value: ValueSelector,
varDetail: Var,
) => {
const {
getNodes,
setNodes,
} = store.getState()
const {
setShowAssignVariablePopup,
} = workflowStore.getState()
const newNodes = produce(getNodes(), (draft) => {
draft.forEach((node) => {
if (node.id === nodeId || node.id === variableAssignerNodeId) {
node.data = {
...node.data,
_showAddVariablePopup: false,
_holdAddVariablePopup: false,
}
}
})
})
setNodes(newNodes)
setShowAssignVariablePopup(undefined)
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,
} = workflowStore.getState()
setHoveringAssignVariableGroupId(groupId)
}, [workflowStore])
const handleGroupItemMouseLeave = useCallback(() => {
const {
connectingNodePayload,
setHoveringAssignVariableGroupId,
} = workflowStore.getState()
if (connectingNodePayload)
setHoveringAssignVariableGroupId(undefined)
}, [workflowStore])
return {
handleAddVariableInAddVariablePopupWithPosition,
handleRemoveEdges,
handleGroupItemMouseEnter,
handleGroupItemMouseLeave,
handleAssignVariableValueChange,
}
}
export const useGetAvailableVars = () => {
const { t } = useTranslation()
const nodes: Node[] = useNodes()
const edges: Edge[] = useEdges()
const { getBeforeNodesInSameBranch } = useWorkflow()
const isChatMode = useIsChatMode()
const getAvailableVars = useCallback((nodeId: string, handleId: string, filterVar: (v: Var) => boolean) => {
const availableNodes: Node[] = []
const currentNode = nodes.find(node => node.id === nodeId)!
if (!currentNode)
return []
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,
t,
beforeNodes: uniqBy(availableNodes, 'id').filter(node => node.id !== nodeId),
isChatMode,
filterVar,
})
}, [nodes, edges, t, isChatMode, getBeforeNodesInSameBranch])
return getAvailableVars
}

View File

@@ -1,91 +1,61 @@
import type { FC } from 'react'
import React from 'react'
import {
memo,
useMemo,
useRef,
} from 'react'
import type { NodeProps } from 'reactflow'
import { useTranslation } from 'react-i18next'
import { NodeTargetHandle } from '../_base/components/node-handle'
import { BlockEnum } from '../../types'
import NodeGroupItem from './components/node-group-item'
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 ref = useRef<HTMLDivElement>(null)
const { id, data } = props
const { variables: originVariables, output_type } = data
const { getTreeLeafNodes } = useWorkflow()
const { advanced_settings } = data
const availableNodes = getTreeLeafNodes(id)
const variables = originVariables.filter(item => item.length > 0)
const groups = useMemo(() => {
if (!advanced_settings?.group_enabled) {
return [{
groupEnabled: false,
targetHandleId: 'target',
title: t(`${i18nPrefix}.title`),
type: data.output_type,
variables: data.variables,
variableAssignerNodeId: id,
variableAssignerNodeData: data,
}]
}
return advanced_settings.groups.map((group) => {
return {
groupEnabled: true,
targetHandleId: group.groupId,
title: group.group_name,
type: group.output_type,
variables: group.variables,
variableAssignerNodeId: id,
variableAssignerNodeData: data,
}
})
}, [t, advanced_settings, data, id])
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>
<div className='relative mb-1 px-1' ref={ref}>
{
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]'
groups.map((item) => {
return (
<NodeGroupItem
key={item.title}
item={item}
/>
</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='max-w-[85px] truncate mx-0.5 text-xs font-medium text-gray-700' title={node?.data.title}>{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='max-w-[75px] truncate ml-0.5 text-xs font-medium' title={varName}>{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)
export default memo(Node)

View File

@@ -1,17 +1,17 @@
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 RemoveEffectVarConfirm from '../_base/components/remove-effect-var-confirm'
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 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 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'
const Panel: FC<NodePanelProps<VariableAssignerNodeType>> = ({
@@ -23,70 +23,105 @@ const Panel: FC<NodePanelProps<VariableAssignerNodeType>> = ({
const {
readOnly,
inputs,
handleOutputTypeChange,
handleVarListChange,
handleAddVariable,
handleOnVarOpen,
handleListOrTypeChange,
isEnableGroup,
// handleGroupEnabledChange,
handleAddGroup,
handleListOrTypeChangeInGroup,
handleGroupRemoved,
handleVarGroupNameChange,
isShowRemoveVarConfirm,
hideRemoveVarConfirm,
onRemoveVarConfirm,
getAvailableVars,
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`)}
{!isEnableGroup
? (
<VarGroupItem
readOnly={readOnly}
nodeId={id}
payload={{
output_type: inputs.output_type,
variables: inputs.variables,
}}
onChange={handleListOrTypeChange}
groupEnabled={false}
availableVars={getAvailableVars(id, 'target', filterVar(inputs.output_type))}
/>
</>
</OutputVars>
)
: (<div>
<div className='space-y-2'>
{inputs.advanced_settings?.groups.map((item, index) => (
<div key={item.groupId}>
<VarGroupItem
readOnly={readOnly}
nodeId={id}
payload={item}
onChange={handleListOrTypeChangeInGroup(item.groupId)}
groupEnabled
canRemove={!readOnly && inputs.advanced_settings?.groups.length > 1}
onRemove={handleGroupRemoved(item.groupId)}
onGroupNameChange={handleVarGroupNameChange(item.groupId)}
availableVars={getAvailableVars(id, item.groupId, filterVar(item.output_type))}
/>
{index !== inputs.advanced_settings?.groups.length - 1 && <Split className='my-4' />}
</div>
))}
</div>
<AddButton
className='mt-2'
text={t(`${i18nPrefix}.addGroup`)}
onClick={handleAddGroup}
/>
</div>)}
</div>
{/* <Split /> */}
{/* <div className={cn('px-4 pt-4', isEnableGroup ? 'pb-4' : 'pb-2')}>
<Field
title={t(`${i18nPrefix}.aggregationGroup`)}
tooltip={t(`${i18nPrefix}.aggregationGroupTip`)!}
operations={
<Switch
defaultValue={isEnableGroup}
onChange={handleGroupEnabledChange}
size='md'
disabled={readOnly}
/>
}
/>
</div> */}
{/* {isEnableGroup && (
<>
<Split />
<div className='px-4 pt-4 pb-2'>
<OutputVars>
<>
{inputs.advanced_settings?.groups.map((item, index) => (
<VarItem
key={index}
name={`${item.group_name}.output`}
type={item.output_type}
description={t(`${i18nPrefix}.outputVars.varDescribe`, {
groupName: item.group_name,
})}
/>
))}
</>
</OutputVars>
</div>
</>
)} */}
<RemoveEffectVarConfirm
isShow={isShowRemoveVarConfirm}
onCancel={hideRemoveVarConfirm}
onConfirm={onRemoveVarConfirm}
/>
</div>
)
}

View File

@@ -1,6 +1,15 @@
import type { CommonNodeType, ValueSelector, VarType } from '@/app/components/workflow/types'
export type VariableAssignerNodeType = CommonNodeType & {
export type VarGroupItem = {
output_type: VarType
variables: ValueSelector[]
}
export type VariableAssignerNodeType = CommonNodeType & VarGroupItem & {
advanced_settings: {
group_enabled: boolean
groups: ({
group_name: string
groupId: string
} & VarGroupItem)[]
}
}

View File

@@ -1,10 +1,13 @@
import { useCallback, useEffect, useState } from 'react'
import { useCallback, useState } from 'react'
import produce from 'immer'
import useVarList from './components/var-list/use-var-list'
import type { VariableAssignerNodeType } from './types'
import { useBoolean } from 'ahooks'
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 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,
@@ -12,68 +15,176 @@ import {
const useConfig = (id: string, payload: VariableAssignerNodeType) => {
const { nodesReadOnly: readOnly } = useNodesReadOnly()
const { inputs, setInputs } = useNodeCrud<VariableAssignerNodeType>(id, payload)
const { getBeforeNodeById } = useWorkflow()
const beforeNodes = getBeforeNodeById(id)
const { handleOutVarRenameChange, isVarUsedInNodes, removeUsedVarInNodes } = useWorkflow()
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']
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) => {
setInputs({
...inputs,
...payload,
})
}, [inputs, setInputs])
const handleListOrTypeChangeInGroup = useCallback((groupId: string) => {
return (payload: VarGroupItem) => {
const index = inputs.advanced_settings.groups.findIndex(item => item.groupId === groupId)
const newInputs = produce(inputs, (draft) => {
draft.advanced_settings.groups[index] = {
...draft.advanced_settings.groups[index],
...payload,
}
})
setInputs(newInputs)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [beforeNodes, inputs.variables])
}, [inputs, setInputs])
const handleOutputTypeChange = useCallback((outputType: string) => {
const newInputs = produce(inputs, (draft: VariableAssignerNodeType) => {
draft.output_type = outputType as VarType
const getAvailableVars = useGetAvailableVars()
const filterVar = (varType: VarType) => {
return (v: Var) => {
if (varType === VarType.any)
return true
if (v.type === VarType.any)
return true
return v.type === varType
}
}
const [isShowRemoveVarConfirm, {
setTrue: showRemoveVarConfirm,
setFalse: hideRemoveVarConfirm,
}] = useBoolean(false)
const [removedVars, setRemovedVars] = useState<ValueSelector[]>([])
const [removeType, setRemoveType] = useState<'group' | 'enableChanged'>('group')
const [removedGroupIndex, setRemovedGroupIndex] = useState<number>(-1)
const handleGroupRemoved = useCallback((groupId: string) => {
return () => {
const index = inputs.advanced_settings.groups.findIndex(item => item.groupId === groupId)
if (isVarUsedInNodes([id, inputs.advanced_settings.groups[index].group_name, 'output'])) {
showRemoveVarConfirm()
setRemovedVars([[id, inputs.advanced_settings.groups[index].group_name, 'output']])
setRemoveType('group')
setRemovedGroupIndex(index)
return
}
const newInputs = produce(inputs, (draft) => {
draft.advanced_settings.groups.splice(index, 1)
})
setInputs(newInputs)
}
}, [id, inputs, isVarUsedInNodes, setInputs, showRemoveVarConfirm])
const handleGroupEnabledChange = useCallback((enabled: boolean) => {
const newInputs = produce(inputs, (draft) => {
if (!draft.advanced_settings)
draft.advanced_settings = { group_enabled: false, groups: [] }
if (enabled) {
if (draft.advanced_settings.groups.length === 0) {
const DEFAULT_GROUP_NAME = 'Group1'
draft.advanced_settings.groups = [{
output_type: draft.output_type,
variables: draft.variables,
group_name: DEFAULT_GROUP_NAME,
groupId: uuid4(),
}]
handleOutVarRenameChange(id, [id, 'output'], [id, DEFAULT_GROUP_NAME, 'output'])
}
}
else {
if (draft.advanced_settings.groups.length > 0) {
if (draft.advanced_settings.groups.length > 1) {
const useVars = draft.advanced_settings.groups.filter((item, index) => index > 0 && isVarUsedInNodes([id, item.group_name, 'output']))
if (useVars.length > 0) {
showRemoveVarConfirm()
setRemovedVars(useVars.map(item => [id, item.group_name, 'output']))
setRemoveType('enableChanged')
return
}
}
draft.output_type = draft.advanced_settings.groups[0].output_type
draft.variables = draft.advanced_settings.groups[0].variables
handleOutVarRenameChange(id, [id, draft.advanced_settings.groups[0].group_name, 'output'], [id, 'output'])
}
}
draft.advanced_settings.group_enabled = enabled
})
setInputs(newInputs)
handleRemoveEdges(id, enabled)
}, [handleOutVarRenameChange, id, inputs, isVarUsedInNodes, setInputs, showRemoveVarConfirm, handleRemoveEdges])
const handleAddGroup = useCallback(() => {
let maxInGroupName = 1
inputs.advanced_settings.groups.forEach((item) => {
const match = item.group_name.match(/(\d+)$/)
if (match) {
const num = parseInt(match[1], 10)
if (num > maxInGroupName)
maxInGroupName = num
}
})
const newInputs = produce(inputs, (draft) => {
draft.advanced_settings.groups.push({
output_type: VarType.any,
variables: [],
group_name: `Group${maxInGroupName + 1}`,
groupId: uuid4(),
})
})
setInputs(newInputs)
}, [inputs, setInputs])
const { handleVarListChange, handleAddVariable } = useVarList({
id,
inputs,
setInputs,
})
const handleVarGroupNameChange = useCallback((groupId: string) => {
return (name: string) => {
const index = inputs.advanced_settings.groups.findIndex(item => item.groupId === groupId)
const newInputs = produce(inputs, (draft) => {
draft.advanced_settings.groups[index].group_name = name
})
handleOutVarRenameChange(id, [id, inputs.advanced_settings.groups[index].group_name, 'output'], [id, name, 'output'])
setInputs(newInputs)
}
}, [handleOutVarRenameChange, 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
const onRemoveVarConfirm = useCallback(() => {
removedVars.forEach((v) => {
removeUsedVarInNodes(v)
})
hideRemoveVarConfirm()
if (removeType === 'group') {
const newInputs = produce(inputs, (draft) => {
draft.advanced_settings.groups.splice(removedGroupIndex, 1)
})
setInputs(newInputs)
}
else {
// removeType === 'enableChanged' to enabled
const newInputs = produce(inputs, (draft) => {
draft.advanced_settings.group_enabled = false
draft.output_type = draft.advanced_settings.groups[0].output_type
draft.variables = draft.advanced_settings.groups[0].variables
})
setInputs(newInputs)
}
}, [removedVars, hideRemoveVarConfirm, removeType, removeUsedVarInNodes, inputs, setInputs, removedGroupIndex])
// 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,
handleListOrTypeChange,
isEnableGroup,
handleGroupEnabledChange,
handleAddGroup,
handleListOrTypeChangeInGroup,
handleGroupRemoved,
handleVarGroupNameChange,
isShowRemoveVarConfirm,
hideRemoveVarConfirm,
onRemoveVarConfirm,
getAvailableVars,
filterVar,
}
}

View File

@@ -1,5 +1,16 @@
import type { VariableAssignerNodeType } from './types'
import type { Var } from '../../types'
import { VarType } from '../../types'
export const checkNodeValid = (payload: VariableAssignerNodeType) => {
export const checkNodeValid = () => {
return true
}
export const filterVar = (varType: VarType) => {
return (v: Var) => {
if (varType === VarType.any)
return true
if (v.type === VarType.any)
return true
return v.type === varType
}
}