This commit is contained in:
2025-12-01 17:21:38 +08:00
parent 32fee2b8ab
commit fab8c13cb3
7511 changed files with 996300 additions and 0 deletions

View File

@@ -0,0 +1,89 @@
import {
memo,
useCallback,
useState,
} from 'react'
import { useVariableAssigner } from '../../hooks'
import type { VariableAssignerNodeType } from '../../types'
import cn from '@/utils/classnames'
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 = {
variableAssignerNodeId: string
variableAssignerNodeData: VariableAssignerNodeType
availableVars: NodeOutPutVar[]
handleId?: string
}
const AddVariable = ({
availableVars,
variableAssignerNodeId,
variableAssignerNodeData,
handleId,
}: AddVariableProps) => {
const [open, setOpen] = useState(false)
const { handleAssignVariableValueChange } = useVariableAssigner()
const handleSelectVariable = useCallback((v: ValueSelector, varDetail: Var) => {
handleAssignVariableValueChange(
variableAssignerNodeId,
v,
varDetail,
handleId,
)
setOpen(false)
}, [handleAssignVariableValueChange, variableAssignerNodeId, handleId, setOpen])
return (
<div className={cn(
open && '!flex',
variableAssignerNodeData.selected && '!flex',
)}>
<PortalToFollowElem
placement={'right'}
offset={4}
open={open}
onOpenChange={setOpen}
>
<PortalToFollowElemTrigger
onClick={() => setOpen(!open)}
>
<div
className={cn(
'group/addvariable flex items-center justify-center',
'h-4 w-4 cursor-pointer',
'hover:rounded-full hover:bg-primary-600',
open && '!rounded-full !bg-primary-600',
)}
>
<Plus02
className={cn(
'h-2.5 w-2.5 text-text-tertiary',
'group-hover/addvariable:text-text-primary',
open && '!text-text-primary',
)}
/>
</div>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-[1000]'>
<AddVariablePopup
onSelect={handleSelectVariable}
availableVars={availableVars}
/>
</PortalToFollowElemContent>
</PortalToFollowElem>
</div>
)
}
export default memo(AddVariable)

View File

@@ -0,0 +1,155 @@
import {
memo,
useMemo,
} from 'react'
import { useTranslation } from 'react-i18next'
import useNodes from '@/app/components/workflow/store/workflow/use-nodes'
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 AddVariable from './add-variable'
import { isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils'
import cn from '@/utils/classnames'
import { isExceptionVariable } from '@/app/components/workflow/utils'
import {
VariableLabelInNode,
} from '@/app/components/workflow/nodes/_base/components/variable/variable-label'
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 groupEnabled = item.groupEnabled
const outputType = useMemo(() => {
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, groupEnabled])
const availableVars = getAvailableVars(item.variableAssignerNodeId, item.targetHandleId, filterVar(outputType as VarType), true)
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 rounded-lg border-[1.5px] border-transparent px-1.5 pb-1.5 pt-1',
showSelectionBorder && '!border-dashed !border-divider-subtle bg-state-base-hover',
showSelectedBorder && '!border-text-accent !bg-util-colors-blue-blue-50',
)}
onMouseEnter={() => groupEnabled && handleGroupItemMouseEnter(item.targetHandleId)}
onMouseLeave={handleGroupItemMouseLeave}
>
<div className='flex h-4 items-center justify-between text-[10px] font-medium text-text-tertiary'>
<span
className={cn(
'grow truncate uppercase',
showSelectedBorder && 'text-text-accent',
)}
title={item.title}
>
{item.title}
</span>
<div className='flex items-center'>
<span className='ml-2 shrink-0'>{item.type}</span>
<div className='ml-2 mr-1 h-2.5 w-[1px] bg-divider-regular'></div>
<AddVariable
availableVars={availableVars}
variableAssignerNodeId={item.variableAssignerNodeId}
variableAssignerNodeData={item.variableAssignerNodeData}
handleId={item.targetHandleId}
/>
</div>
</div>
{
!item.variables.length && (
<div
className={cn(
'relative flex h-[22px] items-center justify-between space-x-1 rounded-md bg-workflow-block-parma-bg px-1 text-[10px] font-normal uppercase text-text-tertiary',
(showSelectedBorder || showSelectionBorder) && '!bg-black/[0.02]',
)}
>
{t(`${i18nPrefix}.varNotSet`)}
</div>
)
}
{
!!item.variables.length && (
<div className='space-y-0.5'>
{
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('.')
const isException = isExceptionVariable(varName, node?.data.type)
return (
<VariableLabelInNode
key={index}
variables={variable}
nodeType={node?.data.type}
nodeTitle={node?.data.title}
isExceptionVariable={isException}
/>
)
})
}
</div>
)
}
</div>
)
}
export default memo(NodeGroupItem)

View File

@@ -0,0 +1,127 @@
import {
memo,
useMemo,
} from 'react'
import { useTranslation } from 'react-i18next'
import cn from '@/utils/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'
import { BubbleX, Env } from '@/app/components/base/icons/src/vender/line/others'
import Badge from '@/app/components/base/badge'
import type { Node, ValueSelector } from '@/app/components/workflow/types'
import { isConversationVar, isENV, isRagVariableVar, isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils'
import { InputField } from '@/app/components/base/icons/src/vender/pipeline'
type NodeVariableItemProps = {
node: Node
variable: ValueSelector
writeMode?: string
showBorder?: boolean
className?: string
isException?: boolean
}
const i18nPrefix = 'workflow.nodes.assigner'
const NodeVariableItem = ({
node,
variable,
writeMode,
showBorder,
className,
isException,
}: NodeVariableItemProps) => {
const { t } = useTranslation()
const isSystem = isSystemVar(variable)
const isEnv = isENV(variable)
const isChatVar = isConversationVar(variable)
const isRagVar = isRagVariableVar(variable)
const varName = useMemo(() => {
if(isSystem)
return `sys.${variable[variable.length - 1]}`
if(isRagVar)
return variable[variable.length - 1]
return variable.slice(1).join('.')
}, [isRagVar, isSystem, variable])
const VariableIcon = useMemo(() => {
if (isEnv) {
return (
<Env className='h-3.5 w-3.5 shrink-0 text-util-colors-violet-violet-600' />
)
}
if (isChatVar) {
return (
<BubbleX className='h-3.5 w-3.5 shrink-0 text-util-colors-teal-teal-700' />
)
}
if(isRagVar) {
return (
<InputField className='h-3.5 w-3.5 shrink-0 text-text-accent' />
)
}
return (
<Variable02
className={cn(
'h-3.5 w-3.5 shrink-0 text-text-accent',
isException && 'text-text-warning',
)}
/>
)
}, [isEnv, isChatVar, isRagVar, isException])
const VariableName = useMemo(() => {
return (
<div
className={cn(
'system-xs-medium ml-0.5 shrink truncate text-text-accent',
isEnv && 'text-text-primary',
isException && 'text-text-warning',
isChatVar && 'text-util-colors-teal-teal-700',
)}
title={varName}
>
{varName}
</div>
)
}, [isEnv, isChatVar, varName, isException])
return (
<div className={cn(
'relative flex items-center gap-1 self-stretch rounded-md bg-workflow-block-parma-bg p-[3px] pl-[5px]',
showBorder && '!bg-state-base-hover',
className,
)}>
<div className='flex w-0 grow items-center'>
{
node && (
<>
<div className='shrink-0 p-[1px]'>
<VarBlockIcon
className='!text-text-primary'
type={node.data.type}
/>
</div>
<div
className='mx-0.5 shrink-[1000] truncate text-xs font-medium text-text-secondary'
title={node?.data.title}
>
{node?.data.title}
</div>
<Line3 className='mr-0.5 shrink-0'></Line3>
</>
)
}
{VariableIcon}
{VariableName}
</div>
{writeMode && <Badge className='shrink-0' text={t(`${i18nPrefix}.operations.${writeMode}`)} />}
</div>
)
}
export default memo(NodeVariableItem)

View File

@@ -0,0 +1,179 @@
'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 {
RiDeleteBinLine,
} from '@remixicon/react'
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 { Folder } from '@/app/components/base/icons/src/vender/line/files'
import { checkKeys, replaceSpaceWithUnderscoreInVarNameInput } 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>) => {
replaceSpaceWithUnderscoreInVarNameInput(e.target)
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 h-3.5 w-3.5' />
{(!isEditGroupName)
? (
<div className='system-sm-semibold flex h-6 cursor-text items-center rounded-lg px-1 text-text-secondary hover:bg-gray-100' onClick={setEditGroupName}>
{payload.group_name}
</div>
)
: (
<input
type='text'
className='h-6 rounded-lg border border-gray-300 bg-white px-1 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='ml-0.5 hidden cursor-pointer rounded-md p-1 text-text-tertiary hover:bg-state-destructive-hover hover:text-text-destructive group-hover:block'
onClick={onRemove}
>
<RiDeleteBinLine
className='h-4 w-4'
/>
</div>
)}
</div>
: t(`${i18nPrefix}.title`)!}
operations={
<div className='flex h-6 items-center space-x-2'>
{payload.variables.length > 0 && (
<div className='system-2xs-medium-uppercase flex h-[18px] items-center rounded-[5px] border border-divider-deep px-1 text-text-tertiary'>{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

@@ -0,0 +1,86 @@
'use client'
import type { FC } from 'react'
import { useTranslation } from 'react-i18next'
import React, { useCallback } from 'react'
import { produce } from 'immer'
import RemoveButton from '../../../_base/components/remove-button'
import 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'
import { noop } from 'lodash-es'
type Props = {
readonly: boolean
nodeId: string
list: ValueSelector[]
onChange: (list: ValueSelector[], value?: ValueSelector) => void
onOpen?: (index: number) => void
filterVar?: (payload: Var, valueSelector: ValueSelector) => boolean
}
const VarList: FC<Props> = ({
readonly,
nodeId,
list,
onChange,
onOpen = noop,
filterVar,
}) => {
const { t } = useTranslation()
const handleVarReferenceChange = useCallback((index: number) => {
return (value: ValueSelector | string) => {
const newList = produce(list, (draft) => {
draft[index] = value as ValueSelector
})
onChange(newList, value as ValueSelector)
}
}, [list, onChange])
const handleVarRemove = useCallback((index: number) => {
return () => {
const newList = produce(list, (draft) => {
draft.splice(index, 1)
})
onChange(newList)
}
}, [list, onChange])
const handleOpen = useCallback((index: number) => {
return () => onOpen(index)
}, [onOpen])
if (list.length === 0) {
return (
<ListNoDataPlaceholder>
{t('workflow.nodes.variableAssigner.noVarTip')}
</ListNoDataPlaceholder>
)
}
return (
<div className='space-y-2'>
{list.map((item, index) => (
<div className='flex items-center space-x-1' key={index}>
<VarReferencePicker
readonly={readonly}
nodeId={nodeId}
isShowNodeName
className='grow'
value={item}
onChange={handleVarReferenceChange(index)}
onOpen={handleOpen(index)}
filterVar={filterVar}
defaultVarKindType={VarKindType.variable}
/>
{!readonly && (
<RemoveButton
onClick={handleVarRemove(index)}
/>
)}
</div>
))}
</div>
)
}
export default React.memo(VarList)

View File

@@ -0,0 +1,34 @@
import { useCallback } from 'react'
import { produce } from 'immer'
import type { VariableAssignerNodeType } from '../../types'
import type { ValueSelector } from '@/app/components/workflow/types'
type Params = {
id: string
inputs: VariableAssignerNodeType
setInputs: (newInputs: VariableAssignerNodeType) => void
}
function useVarList({
inputs,
setInputs,
}: Params) {
const handleVarListChange = useCallback((newList: ValueSelector[]) => {
const newInputs = produce(inputs, (draft) => {
draft.variables = newList
})
setInputs(newInputs)
}, [inputs, setInputs])
const handleAddVariable = useCallback(() => {
const newInputs = produce(inputs, (draft) => {
draft.variables.push([])
})
setInputs(newInputs)
}, [inputs, setInputs])
return {
handleVarListChange,
handleAddVariable,
}
}
export default useVarList

View File

@@ -0,0 +1,56 @@
import { type NodeDefault, VarType } from '../../types'
import type { VariableAssignerNodeType } from './types'
import { genNodeMetaData } from '@/app/components/workflow/utils'
import { BlockEnum } from '@/app/components/workflow/types'
import { BlockClassificationEnum } from '@/app/components/workflow/block-selector/types'
const i18nPrefix = 'workflow'
const metaData = genNodeMetaData({
classification: BlockClassificationEnum.Transform,
sort: 3,
type: BlockEnum.VariableAggregator,
})
const nodeDefault: NodeDefault<VariableAssignerNodeType> = {
metaData,
defaultValue: {
output_type: VarType.any,
variables: [],
},
checkValid(payload: VariableAssignerNodeType, t: any) {
let errorMessages = ''
const { variables, advanced_settings } = payload
const { group_enabled = false, groups = [] } = advanced_settings || {}
// enable group
const validateVariables = (variables: any[], field: string) => {
variables.forEach((variable) => {
if (!variable || variable.length === 0)
errorMessages = t(`${i18nPrefix}.errorMsg.fieldRequired`, { field: t(field) })
})
}
if (group_enabled) {
if (!groups || groups.length === 0) {
errorMessages = t(`${i18nPrefix}.errorMsg.fieldRequired`, { field: t(`${i18nPrefix}.nodes.variableAssigner.title`) })
}
else if (!errorMessages) {
groups.forEach((group) => {
validateVariables(group.variables || [], `${i18nPrefix}.errorMsg.fields.variableValue`)
})
}
}
else {
if (!variables || variables.length === 0)
errorMessages = t(`${i18nPrefix}.errorMsg.fieldRequired`, { field: t(`${i18nPrefix}.nodes.variableAssigner.title`) })
else if (!errorMessages)
validateVariables(variables, `${i18nPrefix}.errorMsg.fields.variableValue`)
}
return {
isValid: !errorMessages,
errorMessage: errorMessages,
}
},
}
export default nodeDefault

View File

@@ -0,0 +1,165 @@
import { useCallback } from 'react'
import {
useStoreApi,
} from 'reactflow'
import useNodes from '@/app/components/workflow/store/workflow/use-nodes'
import { uniqBy } from 'lodash-es'
import { produce } from 'immer'
import {
useIsChatMode,
useNodeDataUpdate,
useWorkflow,
useWorkflowVariables,
} from '../../hooks'
import type {
Node,
ValueSelector,
Var,
} from '../../types'
import { useWorkflowStore } from '../../store'
import type {
VarGroupItem,
VariableAssignerNodeType,
} from './types'
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 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,
handleGroupItemMouseEnter,
handleGroupItemMouseLeave,
handleAssignVariableValueChange,
}
}
export const useGetAvailableVars = () => {
const nodes: Node[] = useNodes()
const { getBeforeNodesInSameBranchIncludeParent } = useWorkflow()
const { getNodeAvailableVars } = useWorkflowVariables()
const isChatMode = useIsChatMode()
const getAvailableVars = useCallback((nodeId: string, handleId: string, filterVar: (v: Var) => boolean, hideEnv = false) => {
const availableNodes: Node[] = []
const currentNode = nodes.find(node => node.id === nodeId)!
if (!currentNode)
return []
const beforeNodes = getBeforeNodesInSameBranchIncludeParent(nodeId)
availableNodes.push(...beforeNodes)
const parentNode = nodes.find(node => node.id === currentNode.parentId)
if (hideEnv) {
return getNodeAvailableVars({
parentNode,
beforeNodes: uniqBy(availableNodes, 'id').filter(node => node.id !== nodeId),
isChatMode,
hideEnv,
hideChatVar: false,
filterVar,
})
.map(node => ({
...node,
vars: node.isStartNode ? node.vars.filter(v => !v.variable.startsWith('sys.')) : node.vars,
}))
.filter(item => item.vars.length > 0)
}
return getNodeAvailableVars({
parentNode,
beforeNodes: uniqBy(availableNodes, 'id').filter(node => node.id !== nodeId),
isChatMode,
filterVar,
})
}, [nodes, getBeforeNodesInSameBranchIncludeParent, getNodeAvailableVars, isChatMode])
return getAvailableVars
}

View File

@@ -0,0 +1,61 @@
import type { FC } from 'react'
import {
memo,
useMemo,
useRef,
} from 'react'
import type { NodeProps } from 'reactflow'
import { useTranslation } from 'react-i18next'
import NodeGroupItem from './components/node-group-item'
import type { VariableAssignerNodeType } from './types'
const i18nPrefix = 'workflow.nodes.variableAssigner'
const Node: FC<NodeProps<VariableAssignerNodeType>> = (props) => {
const { t } = useTranslation()
const ref = useRef<HTMLDivElement>(null)
const { id, data } = props
const { advanced_settings } = data
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='relative mb-1 space-y-0.5 px-1' ref={ref}>
{
groups.map((item) => {
return (
<NodeGroupItem
key={item.title}
item={item}
/>
)
})
}
</div >
)
}
export default memo(Node)

View File

@@ -0,0 +1,127 @@
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
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 cn from '@/utils/classnames'
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 AddButton from '@/app/components/workflow/nodes/_base/components/add-button'
const i18nPrefix = 'workflow.nodes.variableAssigner'
const Panel: FC<NodePanelProps<VariableAssignerNodeType>> = ({
id,
data,
}) => {
const { t } = useTranslation()
const {
readOnly,
inputs,
handleListOrTypeChange,
isEnableGroup,
handleGroupEnabledChange,
handleAddGroup,
handleListOrTypeChangeInGroup,
handleGroupRemoved,
handleVarGroupNameChange,
isShowRemoveVarConfirm,
hideRemoveVarConfirm,
onRemoveVarConfirm,
getAvailableVars,
filterVar,
} = useConfig(id, data)
return (
<div className='mt-2'>
<div className='space-y-4 px-4 pb-4'>
{!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), true)}
/>
)
: (<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), true)}
/>
{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 />
<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>
</>
)}
<RemoveEffectVarConfirm
isShow={isShowRemoveVarConfirm}
onCancel={hideRemoveVarConfirm}
onConfirm={onRemoveVarConfirm}
/>
</div>
)
}
export default React.memo(Panel)

View File

@@ -0,0 +1,15 @@
import type { CommonNodeType, ValueSelector, VarType } from '@/app/components/workflow/types'
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

@@ -0,0 +1,214 @@
import { useCallback, useRef, useState } from 'react'
import { produce } from 'immer'
import { useBoolean, useDebounceFn } 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 } from './hooks'
import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud'
import {
useNodesReadOnly,
useWorkflow,
} from '@/app/components/workflow/hooks'
import useInspectVarsCrud from '../../hooks/use-inspect-vars-crud'
const useConfig = (id: string, payload: VariableAssignerNodeType) => {
const {
deleteNodeInspectorVars,
renameInspectVarName,
} = useInspectVarsCrud()
const { nodesReadOnly: readOnly } = useNodesReadOnly()
const { handleOutVarRenameChange, isVarUsedInNodes, removeUsedVarInNodes } = useWorkflow()
const { inputs, setInputs } = useNodeCrud<VariableAssignerNodeType>(id, payload)
const isEnableGroup = !!inputs.advanced_settings?.group_enabled
// 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)
}
}, [inputs, setInputs])
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)
deleteNodeInspectorVars(id)
}, [deleteNodeInspectorVars, handleOutVarRenameChange, id, inputs, isVarUsedInNodes, setInputs, showRemoveVarConfirm])
const handleAddGroup = useCallback(() => {
let maxInGroupName = 1
inputs.advanced_settings.groups.forEach((item) => {
const match = /(\d+)$/.exec(item.group_name)
if (match) {
const num = Number.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)
deleteNodeInspectorVars(id)
}, [deleteNodeInspectorVars, id, inputs, setInputs])
// record the first old name value
const oldNameRecord = useRef<Record<string, string>>({})
const {
run: renameInspectNameWithDebounce,
} = useDebounceFn(
(id: string, newName: string) => {
const oldName = oldNameRecord.current[id]
renameInspectVarName(id, oldName, newName)
delete oldNameRecord.current[id]
},
{ wait: 500 },
)
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)
if(!(id in oldNameRecord.current))
oldNameRecord.current[id] = inputs.advanced_settings.groups[index].group_name
renameInspectNameWithDebounce(id, name)
}
}, [handleOutVarRenameChange, id, inputs, renameInspectNameWithDebounce, setInputs])
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])
return {
readOnly,
inputs,
handleListOrTypeChange,
isEnableGroup,
handleGroupEnabledChange,
handleAddGroup,
handleListOrTypeChangeInGroup,
handleGroupRemoved,
handleVarGroupNameChange,
isShowRemoveVarConfirm,
hideRemoveVarConfirm,
onRemoveVarConfirm,
getAvailableVars,
filterVar,
}
}
export default useConfig

View File

@@ -0,0 +1,92 @@
import type { RefObject } from 'react'
import type { InputVar, ValueSelector, Variable } from '@/app/components/workflow/types'
import { useCallback } from 'react'
import type { VariableAssignerNodeType } from './types'
type Params = {
id: string,
payload: VariableAssignerNodeType,
runInputData: Record<string, any>
runInputDataRef: RefObject<Record<string, any>>
getInputVars: (textList: string[]) => InputVar[]
setRunInputData: (data: Record<string, any>) => void
toVarInputs: (variables: Variable[]) => InputVar[]
varSelectorsToVarInputs: (variables: ValueSelector[]) => InputVar[]
}
const useSingleRunFormParams = ({
payload,
runInputData,
setRunInputData,
varSelectorsToVarInputs,
}: Params) => {
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 = (() => {
const allInputs: ValueSelector[] = []
const isGroupEnabled = !!payload.advanced_settings?.group_enabled
if (!isGroupEnabled && payload.variables && payload.variables.length) {
payload.variables.forEach((varSelector) => {
allInputs.push(varSelector)
})
}
if (isGroupEnabled && payload.advanced_settings && payload.advanced_settings.groups && payload.advanced_settings.groups.length) {
payload.advanced_settings.groups.forEach((group) => {
group.variables?.forEach((varSelector) => {
allInputs.push(varSelector)
})
})
}
const varInputs = varSelectorsToVarInputs(allInputs)
// remove duplicate inputs
const existVarsKey: Record<string, boolean> = {}
const uniqueVarInputs: InputVar[] = []
varInputs.forEach((input) => {
if(!input)
return
if (!existVarsKey[input.variable]) {
existVarsKey[input.variable] = true
uniqueVarInputs.push({
...input,
required: false, // just one of the inputs is required
})
}
})
return [
{
inputs: uniqueVarInputs,
values: inputVarValues,
onChange: setInputVarValues,
},
]
})()
const getDependentVars = () => {
if(payload.advanced_settings?.group_enabled) {
const vars: ValueSelector[][] = []
payload.advanced_settings.groups.forEach((group) => {
if(group.variables)
vars.push([...group.variables])
})
return vars
}
return [payload.variables]
}
return {
forms,
getDependentVars,
}
}
export default useSingleRunFormParams

View File

@@ -0,0 +1,16 @@
import type { Var } from '../../types'
import { VarType } from '../../types'
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
}
}