Feat/loop break node (#17268)

This commit is contained in:
zxhlyh
2025-04-01 16:52:07 +08:00
committed by GitHub
parent 627a9e2ce1
commit 713902dc47
64 changed files with 1397 additions and 139 deletions

View File

@@ -14,6 +14,9 @@ const HelpLink = ({
const { t } = useTranslation()
const link = useNodeHelpLink(nodeType)
if (!link)
return null
return (
<TooltipPlus
popupContent={t('common.userProfile.helpCenter')}

View File

@@ -164,7 +164,7 @@ const PanelOperatorPopup = ({
)
}
{
showHelpLink && (
showHelpLink && link && (
<>
<div className='p-1'>
<a

View File

@@ -58,7 +58,8 @@ const VariableTag = ({
{node && (
<>
<VarBlockIcon
type={BlockEnum.Start}
type={node.data.type || BlockEnum.Start}
className='mr-0.5'
/>
<div
className='max-w-[60px] truncate font-medium text-text-secondary'

View File

@@ -3,7 +3,7 @@ import { isArray, uniq } from 'lodash-es'
import type { CodeNodeType } from '../../../code/types'
import type { EndNodeType } from '../../../end/types'
import type { AnswerNodeType } from '../../../answer/types'
import type { LLMNodeType } from '../../../llm/types'
import { type LLMNodeType, type StructuredOutput, Type } from '../../../llm/types'
import type { KnowledgeRetrievalNodeType } from '../../../knowledge-retrieval/types'
import type { IfElseNodeType } from '../../../if-else/types'
import type { TemplateTransformNodeType } from '../../../template-transform/types'
@@ -21,6 +21,8 @@ import { BlockEnum, InputVarType, VarType } from '@/app/components/workflow/type
import type { StartNodeType } from '@/app/components/workflow/nodes/start/types'
import type { ConversationVariable, EnvironmentVariable, Node, NodeOutPutVar, ValueSelector, Var } from '@/app/components/workflow/types'
import type { VariableAssignerNodeType } from '@/app/components/workflow/nodes/variable-assigner/types'
import type { Field as StructField } from '@/app/components/workflow/nodes/llm/types'
import {
HTTP_REQUEST_OUTPUT_STRUCT,
KNOWLEDGE_RETRIEVAL_OUTPUT_STRUCT,
@@ -55,19 +57,81 @@ const inputVarTypeToVarType = (type: InputVarType): VarType => {
} as any)[type] || VarType.string
}
const structTypeToVarType = (type: Type): VarType => {
return ({
[Type.string]: VarType.string,
[Type.number]: VarType.number,
[Type.boolean]: VarType.boolean,
[Type.object]: VarType.object,
[Type.array]: VarType.array,
} as any)[type] || VarType.string
}
export const varTypeToStructType = (type: VarType): Type => {
return ({
[VarType.string]: Type.string,
[VarType.number]: Type.number,
[VarType.boolean]: Type.boolean,
[VarType.object]: Type.object,
[VarType.array]: Type.array,
} as any)[type] || Type.string
}
const findExceptVarInStructuredProperties = (properties: Record<string, StructField>, filterVar: (payload: Var, selector: ValueSelector) => boolean): Record<string, StructField> => {
const res = produce(properties, (draft) => {
Object.keys(properties).forEach((key) => {
const item = properties[key]
const isObj = item.type === Type.object
if (!isObj && !filterVar({
variable: key,
type: structTypeToVarType(item.type),
}, [key])) {
delete properties[key]
return
}
if (item.type === Type.object && item.properties)
item.properties = findExceptVarInStructuredProperties(item.properties, filterVar)
})
return draft
})
return res
}
const findExceptVarInStructuredOutput = (structuredOutput: StructuredOutput, filterVar: (payload: Var, selector: ValueSelector) => boolean): StructuredOutput => {
const res = produce(structuredOutput, (draft) => {
const properties = draft.schema.properties
Object.keys(properties).forEach((key) => {
const item = properties[key]
const isObj = item.type === Type.object
if (!isObj && !filterVar({
variable: key,
type: structTypeToVarType(item.type),
}, [key])) {
delete properties[key]
return
}
if (item.type === Type.object && item.properties)
item.properties = findExceptVarInStructuredProperties(item.properties, filterVar)
})
return draft
})
return res
}
const findExceptVarInObject = (obj: any, filterVar: (payload: Var, selector: ValueSelector) => boolean, value_selector: ValueSelector, isFile?: boolean): Var => {
const { children } = obj
const isStructuredOutput = !!(children as StructuredOutput)?.schema?.properties
const res: Var = {
variable: obj.variable,
type: isFile ? VarType.file : VarType.object,
children: children.filter((item: Var) => {
children: isStructuredOutput ? findExceptVarInStructuredOutput(children, filterVar) : children.filter((item: Var) => {
const { children } = item
const currSelector = [...value_selector, item.variable]
if (!children)
return filterVar(item, currSelector)
const obj = findExceptVarInObject(item, filterVar, currSelector, false) // File doesn't contains file children
return obj.children && obj.children?.length > 0
return obj.children && (obj.children as Var[])?.length > 0
}),
}
return res
@@ -139,10 +203,17 @@ const formatItem = (
}
case BlockEnum.LLM: {
res.vars = LLM_OUTPUT_STRUCT
res.vars = [...LLM_OUTPUT_STRUCT]
if (data.structured_output_enabled && data.structured_output?.schema?.properties && Object.keys(data.structured_output.schema.properties).length > 0) {
res.vars.push({
variable: 'structured_output',
type: VarType.object,
children: data.structured_output,
})
}
break
}
case BlockEnum.KnowledgeRetrieval: {
res.vars = KNOWLEDGE_RETRIEVAL_OUTPUT_STRUCT
break
@@ -286,6 +357,21 @@ const formatItem = (
break
}
case BlockEnum.Loop: {
const { loop_variables } = data as LoopNodeType
res.isLoop = true
res.vars = loop_variables?.map((v) => {
return {
variable: v.label,
type: v.var_type,
isLoopVariable: true,
nodeId: res.nodeId,
}
}) || []
break
}
case BlockEnum.DocExtractor: {
res.vars = [
{
@@ -405,7 +491,7 @@ const formatItem = (
return false
const obj = findExceptVarInObject(isFile ? { ...v, children } : v, filterVar, selector, isFile)
return obj?.children && obj?.children.length > 0
return obj?.children && ((obj?.children as Var[]).length > 0 || Object.keys((obj?.children as StructuredOutput)?.schema?.properties || {}).length > 0)
}).map((v) => {
const isFile = v.type === VarType.file
@@ -457,7 +543,7 @@ export const toNodeOutputVars = (
},
}
const res = [
...nodes.filter(node => SUPPORT_OUTPUT_VARS_NODE.includes(node.data.type)),
...nodes.filter(node => SUPPORT_OUTPUT_VARS_NODE.includes(node?.data?.type)),
...(environmentVariables.length > 0 ? [ENV_NODE] : []),
...((isChatMode && conversationVariables.length > 0) ? [CHAT_VAR_NODE] : []),
].map((node) => {
@@ -579,8 +665,7 @@ export const getVarType = ({
isConstant,
environmentVariables = [],
conversationVariables = [],
}:
{
}: {
valueSelector: ValueSelector
parentNode?: Node | null
isIterationItem?: boolean
@@ -644,7 +729,7 @@ export const getVarType = ({
const isEnv = isENV(valueSelector)
const isChatVar = isConversationVar(valueSelector)
const startNode = availableNodes.find((node: any) => {
return node.data.type === BlockEnum.Start
return node?.data.type === BlockEnum.Start
})
const targetVarNodeId = isSystem ? startNode?.id : valueSelector[0]
@@ -655,10 +740,30 @@ export const getVarType = ({
let type: VarType = VarType.string
let curr: any = targetVar.vars
if (isSystem || isEnv || isChatVar) {
return curr.find((v: any) => v.variable === (valueSelector as ValueSelector).join('.'))?.type
}
else {
const targetVar = curr.find((v: any) => v.variable === valueSelector[1])
if (!targetVar)
return VarType.string
const isStructuredOutputVar = !!targetVar.children?.schema?.properties
if (isStructuredOutputVar) {
let currProperties = targetVar.children.schema;
(valueSelector as ValueSelector).slice(2).forEach((key, i) => {
const isLast = i === valueSelector.length - 3
if (!currProperties)
return
currProperties = currProperties.properties[key]
if (isLast)
type = structTypeToVarType(currProperties?.type)
})
return type
}
(valueSelector as ValueSelector).slice(1).forEach((key, i) => {
const isLast = i === valueSelector.length - 2
if (Array.isArray(curr))
@@ -741,6 +846,9 @@ export const toNodeAvailableVars = ({
},
],
}
const iterationIndex = beforeNodesOutputVars.findIndex(v => v.nodeId === iterationNode?.id)
if (iterationIndex > -1)
beforeNodesOutputVars.splice(iterationIndex, 1)
beforeNodesOutputVars.unshift(iterationVar)
}
return beforeNodesOutputVars
@@ -1181,17 +1289,27 @@ export const updateNodeVars = (oldNode: Node, oldVarSelector: ValueSelector, new
})
return newNode
}
const varToValueSelectorList = (v: Var, parentValueSelector: ValueSelector, res: ValueSelector[]) => {
if (!v.variable)
return
res.push([...parentValueSelector, v.variable])
const isStructuredOutput = !!(v.children as StructuredOutput)?.schema?.properties
if (v.children && v.children.length > 0) {
v.children.forEach((child) => {
if ((v.children as Var[])?.length > 0) {
(v.children as Var[]).forEach((child) => {
varToValueSelectorList(child, [...parentValueSelector, v.variable], res)
})
}
if (isStructuredOutput) {
Object.keys((v.children as StructuredOutput)?.schema?.properties || {}).forEach((key) => {
varToValueSelectorList({
variable: key,
type: structTypeToVarType((v.children as StructuredOutput)?.schema?.properties[key].type),
}, [...parentValueSelector, v.variable], res)
})
}
}
const varsToValueSelectorList = (vars: Var | Var[], parentValueSelector: ValueSelector, res: ValueSelector[]) => {
@@ -1225,7 +1343,16 @@ export const getNodeOutputVars = (node: Node, isChatMode: boolean): ValueSelecto
}
case BlockEnum.LLM: {
varsToValueSelectorList(LLM_OUTPUT_STRUCT, [id], res)
const vars = [...LLM_OUTPUT_STRUCT]
const llmNodeData = data as LLMNodeType
if (llmNodeData.structured_output_enabled && llmNodeData.structured_output?.schema?.properties && Object.keys(llmNodeData.structured_output.schema.properties).length > 0) {
vars.push({
variable: 'structured_output',
type: VarType.object,
children: llmNodeData.structured_output,
})
}
varsToValueSelectorList(vars, [id], res)
break
}

View File

@@ -101,7 +101,7 @@ const VarReferencePicker: FC<Props> = ({
const isChatMode = useIsChatMode()
const { getCurrentVariableType } = useWorkflowVariables()
const { availableNodes, availableVars } = useAvailableVarList(nodeId, {
const { availableVars, availableNodesWithParent: availableNodes } = useAvailableVarList(nodeId, {
onlyLeafNodeVar,
passedInAvailableNodes,
filterVar,

View File

@@ -16,6 +16,7 @@ import Input from '@/app/components/base/input'
import { BubbleX, Env } from '@/app/components/base/icons/src/vender/line/others'
import { checkKeys } from '@/utils/var'
import { FILE_STRUCT } from '@/app/components/workflow/constants'
import { Loop } from '@/app/components/base/icons/src/vender/workflow'
type ObjectChildrenProps = {
nodeId: string
@@ -38,6 +39,7 @@ type ItemProps = {
itemWidth?: number
isSupportFileVar?: boolean
isException?: boolean
isLoopVar?: boolean
}
const Item: FC<ItemProps> = ({
@@ -50,6 +52,7 @@ const Item: FC<ItemProps> = ({
itemWidth,
isSupportFileVar,
isException,
isLoopVar,
}) => {
const isFile = itemData.type === VarType.file
const isObj = ([VarType.object, VarType.file].includes(itemData.type) && itemData.children && itemData.children.length > 0)
@@ -112,9 +115,10 @@ const Item: FC<ItemProps> = ({
onMouseDown={e => e.preventDefault()}
>
<div className='flex w-0 grow items-center'>
{!isEnv && !isChatVar && <Variable02 className={cn('h-3.5 w-3.5 shrink-0 text-text-accent', isException && 'text-text-warning')} />}
{!isEnv && !isChatVar && !isLoopVar && <Variable02 className={cn('h-3.5 w-3.5 shrink-0 text-text-accent', isException && 'text-text-warning')} />}
{isEnv && <Env className='h-3.5 w-3.5 shrink-0 text-util-colors-violet-violet-600' />}
{isChatVar && <BubbleX className='h-3.5 w-3.5 text-util-colors-teal-teal-700' />}
{isChatVar && <BubbleX className='h-3.5 w-3.5 shrink-0 text-util-colors-teal-teal-700' />}
{isLoopVar && <Loop className='h-3.5 w-3.5 shrink-0 text-util-colors-cyan-cyan-500' />}
{!isEnv && !isChatVar && (
<div title={itemData.variable} className='system-sm-medium ml-1 w-0 grow truncate text-text-secondary'>{itemData.variable}</div>
)}
@@ -317,6 +321,7 @@ const VarReferenceVars: FC<Props> = ({
itemWidth={itemWidth}
isSupportFileVar={isSupportFileVar}
isException={v.isException}
isLoopVar={item.isLoop}
/>
))}
</div>))

View File

@@ -24,11 +24,11 @@ const useAvailableVarList = (nodeId: string, {
onlyLeafNodeVar: false,
filterVar: () => true,
}) => {
const { getTreeLeafNodes, getBeforeNodesInSameBranch } = useWorkflow()
const { getTreeLeafNodes, getBeforeNodesInSameBranchIncludeParent } = useWorkflow()
const { getNodeAvailableVars } = useWorkflowVariables()
const isChatMode = useIsChatMode()
const availableNodes = passedInAvailableNodes || (onlyLeafNodeVar ? getTreeLeafNodes(nodeId) : getBeforeNodesInSameBranch(nodeId))
const availableNodes = passedInAvailableNodes || (onlyLeafNodeVar ? getTreeLeafNodes(nodeId) : getBeforeNodesInSameBranchIncludeParent(nodeId))
const {
parentNode: iterationNode,
@@ -46,7 +46,7 @@ const useAvailableVarList = (nodeId: string, {
return {
availableVars,
availableNodes,
availableNodesWithParent: iterationNode ? [...availableNodes, iterationNode] : availableNodes,
availableNodesWithParent: availableNodes,
}
}

View File

@@ -26,9 +26,7 @@ export const useNodeHelpLink = (nodeType: BlockEnum) => {
[BlockEnum.VariableAggregator]: 'variable-aggregator',
[BlockEnum.Assigner]: 'variable-assigner',
[BlockEnum.Iteration]: 'iteration',
[BlockEnum.IterationStart]: 'iteration',
[BlockEnum.Loop]: 'loop',
[BlockEnum.LoopStart]: 'loop',
[BlockEnum.ParameterExtractor]: 'parameter-extractor',
[BlockEnum.HttpRequest]: 'http-request',
[BlockEnum.Tool]: 'tools',
@@ -52,9 +50,7 @@ export const useNodeHelpLink = (nodeType: BlockEnum) => {
[BlockEnum.VariableAggregator]: 'variable-aggregator',
[BlockEnum.Assigner]: 'variable-assigner',
[BlockEnum.Iteration]: 'iteration',
[BlockEnum.IterationStart]: 'iteration',
[BlockEnum.Loop]: 'loop',
[BlockEnum.LoopStart]: 'loop',
[BlockEnum.ParameterExtractor]: 'parameter-extractor',
[BlockEnum.HttpRequest]: 'http-request',
[BlockEnum.Tool]: 'tools',
@@ -62,7 +58,12 @@ export const useNodeHelpLink = (nodeType: BlockEnum) => {
[BlockEnum.ListFilter]: 'list-operator',
[BlockEnum.Agent]: 'agent',
}
}, [language])
}, [language]) as Record<string, string>
return `${prefixLink}${linkMap[nodeType]}`
const link = linkMap[nodeType]
if (!link)
return ''
return `${prefixLink}${link}`
}

View File

@@ -1,6 +1,6 @@
import type {
FC,
ReactNode,
ReactElement,
} from 'react'
import {
cloneElement,
@@ -46,7 +46,7 @@ import BlockIcon from '@/app/components/workflow/block-icon'
import Tooltip from '@/app/components/base/tooltip'
type BaseNodeProps = {
children: ReactNode
children: ReactElement
} & NodeProps
const BaseNode: FC<BaseNodeProps> = ({
@@ -104,6 +104,30 @@ const BaseNode: FC<BaseNodeProps> = ({
}
}, [data._runningStatus, showSelectedBorder])
const LoopIndex = useMemo(() => {
let text = ''
if (data._runningStatus === NodeRunningStatus.Running)
text = t('workflow.nodes.loop.currentLoopCount', { count: data._loopIndex })
if (data._runningStatus === NodeRunningStatus.Succeeded || data._runningStatus === NodeRunningStatus.Failed)
text = t('workflow.nodes.loop.totalLoopCount', { count: data._loopIndex })
if (text) {
return (
<div
className={cn(
'system-xs-medium mr-2 text-text-tertiary',
data._runningStatus === NodeRunningStatus.Running && 'text-text-accent',
)}
>
{text}
</div>
)
}
return null
}, [data._loopIndex, data._runningStatus, t])
return (
<div
className={cn(
@@ -233,11 +257,7 @@ const BaseNode: FC<BaseNodeProps> = ({
)
}
{
data._loopLength && data._loopIndex && data._runningStatus === NodeRunningStatus.Running && (
<div className='mr-1.5 text-xs font-medium text-primary-600'>
{data._loopIndex > data._loopLength ? data._loopLength : data._loopIndex}/{data._loopLength}
</div>
)
data.type === BlockEnum.Loop && data._loopIndex && LoopIndex
}
{
(data._runningStatus === NodeRunningStatus.Running || data._singleRunningStatus === NodeRunningStatus.Running) && (

View File

@@ -95,10 +95,13 @@ const VarList: FC<Props> = ({
}, [onOpen])
const handleFilterToAssignedVar = useCallback((index: number) => {
return (payload: Var, valueSelector: ValueSelector) => {
return (payload: Var) => {
const item = list[index]
const assignedVarType = item.variable_selector ? getAssignedVarType?.(item.variable_selector) : undefined
if (item.variable_selector.join('.') === `${payload.nodeId}.${payload.variable}`)
return false
if (!filterToAssignedVar || !item.variable_selector || !assignedVarType || !item.operation)
return true

View File

@@ -13,7 +13,6 @@ const NodeComponent: FC<NodeProps<AssignerNodeType>> = ({
data,
}) => {
const { t } = useTranslation()
const nodes: Node[] = useNodes()
if (data.version === '2') {
const { items: operationItems } = data

View File

@@ -31,7 +31,7 @@ const useConfig = (id: string, rawPayload: AssignerNodeType) => {
}
const store = useStoreApi()
const { getBeforeNodesInSameBranch } = useWorkflow()
const { getBeforeNodesInSameBranchIncludeParent } = useWorkflow()
const {
getNodes,
@@ -39,11 +39,9 @@ const useConfig = (id: string, rawPayload: AssignerNodeType) => {
const currentNode = getNodes().find(n => n.id === id)
const isInIteration = payload.isInIteration
const iterationNode = isInIteration ? getNodes().find(n => n.id === currentNode!.parentId) : null
const isInLoop = payload.isInLoop
const loopNode = isInLoop ? getNodes().find(n => n.id === currentNode!.parentId) : null
const availableNodes = useMemo(() => {
return getBeforeNodesInSameBranch(id)
}, [getBeforeNodesInSameBranch, id])
return getBeforeNodesInSameBranchIncludeParent(id)
}, [getBeforeNodesInSameBranchIncludeParent, id])
const { inputs, setInputs } = useNodeCrud<AssignerNodeType>(id, payload)
const newSetInputs = useCallback((newInputs: AssignerNodeType) => {
const finalInputs = produce(newInputs, (draft) => {
@@ -56,13 +54,13 @@ const useConfig = (id: string, rawPayload: AssignerNodeType) => {
const { getCurrentVariableType } = useWorkflowVariables()
const getAssignedVarType = useCallback((valueSelector: ValueSelector) => {
return getCurrentVariableType({
parentNode: isInIteration ? iterationNode : loopNode,
parentNode: isInIteration ? iterationNode : null,
valueSelector: valueSelector || [],
availableNodes,
isChatMode,
isConstant: false,
})
}, [getCurrentVariableType, isInIteration, iterationNode, loopNode, availableNodes, isChatMode])
}, [getCurrentVariableType, isInIteration, iterationNode, availableNodes, isChatMode])
const handleOperationListChanges = useCallback((items: AssignerNodeOperation[]) => {
const newInputs = produce(inputs, (draft) => {
@@ -91,6 +89,8 @@ const useConfig = (id: string, rawPayload: AssignerNodeType) => {
}, [])
const filterAssignedVar = useCallback((varPayload: Var, selector: ValueSelector) => {
if (varPayload.isLoopVariable)
return true
return selector.join('.').startsWith('conversation')
}, [])

View File

@@ -6,7 +6,10 @@ import type {
BlockEnum,
Node,
} from '../../types'
import { generateNewNode } from '../../utils'
import {
generateNewNode,
getNodeCustomTypeByNodeDataType,
} from '../../utils'
import {
ITERATION_PADDING,
NODES_INITIAL_DATA,
@@ -115,6 +118,7 @@ export const useNodeIterationInteractions = () => {
const childNodeType = child.data.type as BlockEnum
const nodesWithSameType = nodes.filter(node => node.data.type === childNodeType)
const { newNode } = generateNewNode({
type: getNodeCustomTypeByNodeDataType(childNodeType),
data: {
...NODES_INITIAL_DATA[childNodeType],
...child.data,

View File

@@ -15,4 +15,51 @@ export type LLMNodeType = CommonNodeType & {
enabled: boolean
configs?: VisionSetting
}
structured_output_enabled?: boolean
structured_output?: StructuredOutput
}
export enum Type {
string = 'string',
number = 'number',
boolean = 'boolean',
object = 'object',
array = 'array',
}
export enum ArrayType {
string = 'array[string]',
number = 'array[number]',
boolean = 'array[boolean]',
object = 'array[object]',
}
export type TypeWithArray = Type | ArrayType
type ArrayItemType = Exclude<Type, Type.array>
export type ArrayItems = Omit<Field, 'type'> & { type: ArrayItemType }
export type SchemaEnumType = string[] | number[]
export type Field = {
type: Type
properties?: { // Object has properties
[key: string]: Field
}
required?: string[] // Key of required properties in object
description?: string
items?: ArrayItems // Array has items. Define the item type
enum?: SchemaEnumType // Enum values
additionalProperties?: false // Required in object by api. Just set false
}
export type StructuredOutput = {
schema: SchemaRoot
}
export type SchemaRoot = {
type: Type.object
properties: Record<string, Field>
required?: string[]
additionalProperties: false
}

View File

@@ -0,0 +1,23 @@
import type { NodeDefault } from '../../types'
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/blocks'
import type {
SimpleNodeType,
} from '@/app/components/workflow/simple-node/types'
const nodeDefault: NodeDefault<SimpleNodeType> = {
defaultValue: {},
getAvailablePrevNodes(isChatMode: boolean) {
const nodes = isChatMode ? ALL_CHAT_AVAILABLE_BLOCKS : ALL_COMPLETION_AVAILABLE_BLOCKS
return nodes
},
getAvailableNextNodes() {
return []
},
checkValid() {
return {
isValid: true,
}
},
}
export default nodeDefault

View File

@@ -138,9 +138,6 @@ const ConditionWrap: FC<Props> = ({
)}
</div>
</div>
{!isSubVariable && (
<div className='mx-3 my-2 h-[1px] bg-divider-subtle'></div>
)}
</div>
</>
)

View File

@@ -0,0 +1,13 @@
import { useTranslation } from 'react-i18next'
const Empty = () => {
const { t } = useTranslation()
return (
<div className='system-xs-regular flex h-10 items-center justify-center rounded-[10px] bg-background-section text-text-tertiary'>
{t('workflow.nodes.loop.setLoopVariables')}
</div>
)
}
export default Empty

View File

@@ -0,0 +1,144 @@
import {
useCallback,
useMemo,
} from 'react'
import { useTranslation } from 'react-i18next'
import VarReferencePicker from '@/app/components/workflow/nodes/_base/components/variable/var-reference-picker'
import Input from '@/app/components/base/input'
import Textarea from '@/app/components/base/textarea'
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
import type {
LoopVariable,
} from '@/app/components/workflow/nodes/loop/types'
import type {
Var,
} from '@/app/components/workflow/types'
import {
ValueType,
VarType,
} from '@/app/components/workflow/types'
const objectPlaceholder = `# example
# {
# "name": "ray",
# "age": 20
# }`
const arrayStringPlaceholder = `# example
# [
# "value1",
# "value2"
# ]`
const arrayNumberPlaceholder = `# example
# [
# 100,
# 200
# ]`
const arrayObjectPlaceholder = `# example
# [
# {
# "name": "ray",
# "age": 20
# },
# {
# "name": "lily",
# "age": 18
# }
# ]`
type FormItemProps = {
nodeId: string
item: LoopVariable
onChange: (value: any) => void
}
const FormItem = ({
nodeId,
item,
onChange,
}: FormItemProps) => {
const { t } = useTranslation()
const { value_type, var_type, value } = item
const handleInputChange = useCallback((e: any) => {
onChange(e.target.value)
}, [onChange])
const handleChange = useCallback((value: any) => {
onChange(value)
}, [onChange])
const filterVar = useCallback((variable: Var) => {
return variable.type === var_type
}, [var_type])
const editorMinHeight = useMemo(() => {
if (var_type === VarType.arrayObject)
return '240px'
return '120px'
}, [var_type])
const placeholder = useMemo(() => {
if (var_type === VarType.arrayString)
return arrayStringPlaceholder
if (var_type === VarType.arrayNumber)
return arrayNumberPlaceholder
if (var_type === VarType.arrayObject)
return arrayObjectPlaceholder
return objectPlaceholder
}, [var_type])
return (
<div>
{
value_type === ValueType.variable && (
<VarReferencePicker
readonly={false}
nodeId={nodeId}
isShowNodeName
value={value}
onChange={handleChange}
filterVar={filterVar}
placeholder={t('workflow.nodes.assigner.setParameter') as string}
/>
)
}
{
value_type === ValueType.constant && var_type === VarType.string && (
<Textarea
value={value}
onChange={handleInputChange}
className='min-h-12 w-full'
/>
)
}
{
value_type === ValueType.constant && var_type === VarType.number && (
<Input
type="number"
value={value}
onChange={handleInputChange}
className='w-full'
/>
)
}
{
value_type === ValueType.constant
&& (var_type === VarType.object || var_type === VarType.arrayString || var_type === VarType.arrayNumber || var_type === VarType.arrayObject)
&& (
<div className='w-full rounded-[10px] bg-components-input-bg-normal py-2 pl-3 pr-1' style={{ height: editorMinHeight }}>
<CodeEditor
value={value}
isExpand
noWrapper
language={CodeLanguage.json}
onChange={handleChange}
className='w-full'
placeholder={<div className='whitespace-pre'>{placeholder}</div>}
/>
</div>
)
}
</div>
)
}
export default FormItem

View File

@@ -0,0 +1,28 @@
import Empty from './empty'
import Item from './item'
import type {
LoopVariable,
LoopVariablesComponentShape,
} from '@/app/components/workflow/nodes/loop/types'
type LoopVariableProps = {
variables?: LoopVariable[]
} & LoopVariablesComponentShape
const LoopVariableComponent = ({
variables = [],
...restProps
}: LoopVariableProps) => {
if (!variables.length)
return <Empty />
return variables.map(variable => (
<Item
key={variable.id}
item={variable}
{...restProps}
/>
))
}
export default LoopVariableComponent

View File

@@ -0,0 +1,37 @@
import { useTranslation } from 'react-i18next'
import PureSelect from '@/app/components/base/select/pure'
type InputModeSelectProps = {
value?: string
onChange: (value: string) => void
}
const InputModeSelect = ({
value,
onChange,
}: InputModeSelectProps) => {
const { t } = useTranslation()
const options = [
{
label: 'Variable',
value: 'variable',
},
{
label: 'Constant',
value: 'constant',
},
]
return (
<PureSelect
options={options}
value={value}
onChange={onChange}
popupProps={{
title: t('workflow.nodes.loop.inputMode'),
className: 'w-[132px]',
}}
/>
)
}
export default InputModeSelect

View File

@@ -0,0 +1,78 @@
import { useCallback } from 'react'
import { RiDeleteBinLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import InputModeSelect from './input-mode-selec'
import VariableTypeSelect from './variable-type-select'
import FormItem from './form-item'
import ActionButton from '@/app/components/base/action-button'
import Input from '@/app/components/base/input'
import type {
LoopVariable,
LoopVariablesComponentShape,
} from '@/app/components/workflow/nodes/loop/types'
type ItemProps = {
item: LoopVariable
} & LoopVariablesComponentShape
const Item = ({
nodeId,
item,
handleRemoveLoopVariable,
handleUpdateLoopVariable,
}: ItemProps) => {
const { t } = useTranslation()
const handleUpdateItemLabel = useCallback((e: any) => {
handleUpdateLoopVariable(item.id, { label: e.target.value })
}, [item.id, handleUpdateLoopVariable])
const handleUpdateItemVarType = useCallback((value: any) => {
handleUpdateLoopVariable(item.id, { var_type: value, value: undefined })
}, [item.id, handleUpdateLoopVariable])
const handleUpdateItemValueType = useCallback((value: any) => {
handleUpdateLoopVariable(item.id, { value_type: value, value: undefined })
}, [item.id, handleUpdateLoopVariable])
const handleUpdateItemValue = useCallback((value: any) => {
handleUpdateLoopVariable(item.id, { value })
}, [item.id, handleUpdateLoopVariable])
return (
<div className='mb-4 flex last-of-type:mb-0'>
<div className='w-0 grow'>
<div className='mb-1 grid grid-cols-3 gap-1'>
<Input
value={item.label}
onChange={handleUpdateItemLabel}
autoFocus={!item.label}
placeholder={t('workflow.nodes.loop.variableName')}
/>
<VariableTypeSelect
value={item.var_type}
onChange={handleUpdateItemVarType}
/>
<InputModeSelect
value={item.value_type}
onChange={handleUpdateItemValueType}
/>
</div>
<div>
<FormItem
nodeId={nodeId}
item={item}
onChange={handleUpdateItemValue}
/>
</div>
</div>
<ActionButton
className='shrink-0'
size='l'
onClick={() => handleRemoveLoopVariable(item.id)}
>
<RiDeleteBinLine className='h-4 w-4 text-text-tertiary' />
</ActionButton>
</div>
)
}
export default Item

View File

@@ -0,0 +1,51 @@
import PureSelect from '@/app/components/base/select/pure'
import { VarType } from '@/app/components/workflow/types'
type VariableTypeSelectProps = {
value?: string
onChange: (value: string) => void
}
const VariableTypeSelect = ({
value,
onChange,
}: VariableTypeSelectProps) => {
const options = [
{
label: 'String',
value: VarType.string,
},
{
label: 'Number',
value: VarType.number,
},
{
label: 'Object',
value: VarType.object,
},
{
label: 'Array[string]',
value: VarType.arrayString,
},
{
label: 'Array[number]',
value: VarType.arrayNumber,
},
{
label: 'Array[object]',
value: VarType.arrayObject,
},
]
return (
<PureSelect
options={options}
value={value}
onChange={onChange}
popupProps={{
className: 'w-[132px]',
}}
/>
)
}
export default VariableTypeSelect

View File

@@ -28,6 +28,11 @@ const nodeDefault: NodeDefault<LoopNodeType> = {
checkValid(payload: LoopNodeType, t: any) {
let errorMessages = ''
payload.loop_variables?.forEach((variable) => {
if (!variable.label)
errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t(`${i18nPrefix}.fields.variable`) })
})
payload.break_conditions!.forEach((condition) => {
if (!errorMessages && (!condition.variable_selector || condition.variable_selector.length === 0))
errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t(`${i18nPrefix}.fields.variable`) })

View File

@@ -1,13 +1,14 @@
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { RiAddLine } from '@remixicon/react'
import Split from '../_base/components/split'
import ResultPanel from '../../run/result-panel'
import InputNumberWithSlider from '../_base/components/input-number-with-slider'
import type { LoopNodeType } from './types'
import useConfig from './use-config'
import ConditionWrap from './components/condition-wrap'
import LoopVariable from './components/loop-variables'
import type { NodePanelProps } from '@/app/components/workflow/types'
import Field from '@/app/components/workflow/nodes/_base/components/field'
import BeforeRunForm from '@/app/components/workflow/nodes/_base/components/before-run-form'
@@ -45,6 +46,9 @@ const Panel: FC<NodePanelProps<LoopNodeType>> = ({
handleUpdateSubVariableCondition,
handleToggleSubVariableConditionLogicalOperator,
handleUpdateLoopCount,
handleAddLoopVariable,
handleRemoveLoopVariable,
handleUpdateLoopVariable,
} = useConfig(id, data)
const nodeInfo = formatTracing(loopRunResult, t)[0]
@@ -53,6 +57,27 @@ const Panel: FC<NodePanelProps<LoopNodeType>> = ({
return (
<div className='mt-2'>
<div>
<Field
title={<div className='pl-3'>{t('workflow.nodes.loop.loopVariables')}</div>}
operations={
<div
className='mr-4 flex h-5 w-5 cursor-pointer items-center justify-center'
onClick={handleAddLoopVariable}
>
<RiAddLine className='h-4 w-4 text-text-tertiary' />
</div>
}
>
<div className='px-4'>
<LoopVariable
variables={inputs.loop_variables}
nodeId={id}
handleRemoveLoopVariable={handleRemoveLoopVariable}
handleUpdateLoopVariable={handleUpdateLoopVariable}
/>
</div>
</Field>
<Split className='my-2' />
<Field
title={<div className='pl-3'>{t(`${i18nPrefix}.breakCondition`)}</div>}
tooltip={t(`${i18nPrefix}.breakConditionTip`)}
@@ -74,7 +99,7 @@ const Panel: FC<NodePanelProps<LoopNodeType>> = ({
logicalOperator={inputs.logical_operator!}
/>
</Field>
<Split />
<Split className='mt-2' />
<div className='mt-2'>
<Field
title={<div className='pl-3'>{t(`${i18nPrefix}.loopMaxCount`)}</div>}

View File

@@ -4,6 +4,7 @@ import type {
CommonNodeType,
ErrorHandleMode,
ValueSelector,
ValueType,
Var,
VarType,
} from '@/app/components/workflow/types'
@@ -65,6 +66,13 @@ export type handleRemoveSubVariableCondition = (conditionId: string, subConditio
export type HandleUpdateSubVariableCondition = (conditionId: string, subConditionId: string, newSubCondition: Condition) => void
export type HandleToggleSubVariableConditionLogicalOperator = (conditionId: string) => void
export type LoopVariable = {
id: string
label: string
var_type: VarType
value_type: ValueType
value: any
}
export type LoopNodeType = CommonNodeType & {
startNodeType?: BlockEnum
start_node_id: string
@@ -73,4 +81,14 @@ export type LoopNodeType = CommonNodeType & {
break_conditions?: Condition[]
loop_count: number
error_handle_mode: ErrorHandleMode // how to handle error in the iteration
loop_variables?: LoopVariable[]
}
export type HandleUpdateLoopVariable = (id: string, updateData: Partial<LoopVariable>) => void
export type HandleRemoveLoopVariable = (id: string) => void
export type LoopVariablesComponentShape = {
nodeId: string
handleRemoveLoopVariable: HandleRemoveLoopVariable
handleUpdateLoopVariable: HandleUpdateLoopVariable
}

View File

@@ -1,14 +1,17 @@
import { useCallback } from 'react'
import {
useCallback,
useRef,
} from 'react'
import produce from 'immer'
import { useBoolean } from 'ahooks'
import { uuid4 } from '@sentry/utils'
import { v4 as uuid4 } from 'uuid'
import {
useIsChatMode,
useIsNodeInLoop,
useNodesReadOnly,
useWorkflow,
} from '../../hooks'
import { VarType } from '../../types'
import { ValueType, VarType } from '../../types'
import type { ErrorHandleMode, ValueSelector, Var } from '../../types'
import useNodeCrud from '../_base/hooks/use-node-crud'
import { getNodeInfoById, getNodeUsedVarPassToServerKey, getNodeUsedVars, isSystemVar, toNodeOutputVars } from '../_base/components/variable/utils'
@@ -27,6 +30,11 @@ const useConfig = (id: string, payload: LoopNodeType) => {
const conversationVariables = useStore(s => s.conversationVariables)
const { inputs, setInputs } = useNodeCrud<LoopNodeType>(id, payload)
const inputsRef = useRef(inputs)
const handleInputsChange = useCallback((newInputs: LoopNodeType) => {
inputsRef.current = newInputs
setInputs(newInputs)
}, [setInputs])
const filterInputVar = useCallback((varPayload: Var) => {
return [VarType.array, VarType.arrayString, VarType.arrayNumber, VarType.arrayObject, VarType.arrayFile].includes(varPayload.type)
@@ -35,7 +43,7 @@ const useConfig = (id: string, payload: LoopNodeType) => {
// output
const { getLoopNodeChildren, getBeforeNodesInSameBranch } = useWorkflow()
const beforeNodes = getBeforeNodesInSameBranch(id)
const loopChildrenNodes = getLoopNodeChildren(id)
const loopChildrenNodes = [{ id, data: payload } as any, ...getLoopNodeChildren(id)]
const canChooseVarNodes = [...beforeNodes, ...loopChildrenNodes]
const childrenNodeVars = toNodeOutputVars(loopChildrenNodes, isChatMode, undefined, [], conversationVariables)
@@ -291,6 +299,43 @@ const useConfig = (id: string, payload: LoopNodeType) => {
setInputs(newInputs)
}, [inputs, setInputs])
const handleAddLoopVariable = useCallback(() => {
const newInputs = produce(inputsRef.current, (draft) => {
if (!draft.loop_variables)
draft.loop_variables = []
draft.loop_variables.push({
id: uuid4(),
label: '',
var_type: VarType.string,
value_type: ValueType.constant,
value: '',
})
})
handleInputsChange(newInputs)
}, [handleInputsChange])
const handleRemoveLoopVariable = useCallback((id: string) => {
const newInputs = produce(inputsRef.current, (draft) => {
draft.loop_variables = draft.loop_variables?.filter(item => item.id !== id)
})
handleInputsChange(newInputs)
}, [handleInputsChange])
const handleUpdateLoopVariable = useCallback((id: string, updateData: any) => {
const loopVariables = inputsRef.current.loop_variables || []
const index = loopVariables.findIndex(item => item.id === id)
const newInputs = produce(inputsRef.current, (draft) => {
if (index > -1) {
draft.loop_variables![index] = {
...draft.loop_variables![index],
...updateData,
}
}
})
handleInputsChange(newInputs)
}, [handleInputsChange])
return {
readOnly,
inputs,
@@ -325,6 +370,9 @@ const useConfig = (id: string, payload: LoopNodeType) => {
handleToggleSubVariableConditionLogicalOperator,
handleUpdateLoopCount,
changeErrorResponseMode,
handleAddLoopVariable,
handleRemoveLoopVariable,
handleUpdateLoopVariable,
}
}

View File

@@ -6,7 +6,10 @@ import type {
BlockEnum,
Node,
} from '../../types'
import { generateNewNode } from '../../utils'
import {
generateNewNode,
getNodeCustomTypeByNodeDataType,
} from '../../utils'
import {
LOOP_PADDING,
NODES_INITIAL_DATA,
@@ -114,7 +117,7 @@ export const useNodeLoopInteractions = () => {
const childNodeType = child.data.type as BlockEnum
const nodesWithSameType = nodes.filter(node => node.data.type === childNodeType)
const { newNode } = generateNewNode({
type: getNodeCustomTypeByNodeDataType(childNodeType),
data: {
...NODES_INITIAL_DATA[childNodeType],
...child.data,

View File

@@ -1,4 +1,7 @@
import { memo } from 'react'
import {
memo,
useMemo,
} from 'react'
import { useTranslation } from 'react-i18next'
import cn from '@/utils/classnames'
import { VarBlockIcon } from '@/app/components/workflow/block-icon'
@@ -7,7 +10,6 @@ import { Variable02 } from '@/app/components/base/icons/src/vender/solid/develop
import { BubbleX, Env } from '@/app/components/base/icons/src/vender/line/others'
import Badge from '@/app/components/base/badge'
import type { Node } from '@/app/components/workflow/types'
import { BlockEnum } from '@/app/components/workflow/types'
type NodeVariableItemProps = {
isEnv: boolean
@@ -33,38 +35,75 @@ const NodeVariableItem = ({
isException,
}: NodeVariableItemProps) => {
const { t } = useTranslation()
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' />
)
}
return (
<Variable02
className={cn(
'h-3.5 w-3.5 shrink-0 text-text-accent',
isException && 'text-text-warning',
)}
/>
)
}, [isEnv, isChatVar, isException])
const VariableName = useMemo(() => {
return (
<div
className={cn(
'system-xs-medium ml-0.5 shrink truncate text-text-accent',
isEnv && 'text-gray-900',
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-black/[0.02]',
className,
)}>
{!isEnv && !isChatVar && (
<div className='flex items-center'>
<div className='p-[1px]'>
<VarBlockIcon
className='!text-gray-900'
type={node?.data.type || BlockEnum.Start}
/>
</div>
<div className='mx-0.5 max-w-[85px] truncate 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 w-full items-center text-primary-600'>
{!isEnv && !isChatVar && <Variable02 className={cn('h-3.5 w-3.5 shrink-0 text-primary-500', isException && 'text-text-warning')} />}
{isEnv && <Env className='h-3.5 w-3.5 shrink-0 text-util-colors-violet-violet-600' />}
{!isChatVar && <div className={cn('system-xs-medium ml-0.5 max-w-[75px] overflow-hidden truncate text-ellipsis', isEnv && 'text-gray-900', isException && 'text-text-warning')} title={varName}>{varName}</div>}
{isChatVar
&& <div className='flex w-full items-center gap-1'>
<div className='flex h-[18px] min-w-[18px] flex-1 items-center gap-0.5'>
<BubbleX className='h-3.5 w-3.5 text-util-colors-teal-teal-700' />
<div className={cn('system-xs-medium ml-0.5 max-w-[75px] overflow-hidden truncate text-ellipsis text-util-colors-teal-teal-700')}>{varName}</div>
</div>
{writeMode && <Badge className='shrink-0' text={t(`${i18nPrefix}.operations.${writeMode}`)} />}
</div>
<div className='flex w-0 grow items-center'>
{
node && (
<>
<div className='shrink-0 p-[1px]'>
<VarBlockIcon
className='!text-gray-900'
type={node.data.type}
/>
</div>
<div
className='mx-0.5 shrink-[1000] truncate text-xs font-medium text-gray-700'
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>
)
}