Browse Source

Feat/loop break node (#17268)

tags/1.2.0
zxhlyh 7 months ago
parent
commit
713902dc47
No account linked to committer's email address
64 changed files with 1397 additions and 139 deletions
  1. 1
    1
      web/app/account/account-page/index.tsx
  2. 1
    1
      web/app/account/avatar.tsx
  3. 5
    0
      web/app/components/base/icons/assets/vender/workflow/loop-end.svg
  4. 9
    5
      web/app/components/base/icons/src/public/education/Triangle.tsx
  5. 38
    0
      web/app/components/base/icons/src/vender/workflow/LoopEnd.json
  6. 20
    0
      web/app/components/base/icons/src/vender/workflow/LoopEnd.tsx
  7. 1
    0
      web/app/components/base/icons/src/vender/workflow/index.ts
  8. 157
    0
      web/app/components/base/select/pure.tsx
  9. 1
    1
      web/app/components/header/account-dropdown/index.tsx
  10. 2
    2
      web/app/components/header/plan-badge/index.tsx
  11. 3
    0
      web/app/components/workflow/block-icon.tsx
  12. 10
    1
      web/app/components/workflow/block-selector/blocks.tsx
  13. 6
    0
      web/app/components/workflow/block-selector/constants.tsx
  14. 16
    0
      web/app/components/workflow/constants.ts
  15. 7
    0
      web/app/components/workflow/hooks/use-nodes-data.ts
  16. 17
    14
      web/app/components/workflow/hooks/use-nodes-interactions.ts
  17. 14
    0
      web/app/components/workflow/hooks/use-workflow-run-event/use-workflow-node-iteration-finished.ts
  18. 14
    3
      web/app/components/workflow/hooks/use-workflow-run-event/use-workflow-node-loop-finished.ts
  19. 10
    10
      web/app/components/workflow/hooks/use-workflow-run-event/use-workflow-node-loop-next.ts
  20. 0
    3
      web/app/components/workflow/hooks/use-workflow-run-event/use-workflow-node-loop-started.ts
  21. 3
    0
      web/app/components/workflow/index.tsx
  22. 3
    0
      web/app/components/workflow/nodes/_base/components/help-link.tsx
  23. 1
    1
      web/app/components/workflow/nodes/_base/components/panel-operator/panel-operator-popup.tsx
  24. 2
    1
      web/app/components/workflow/nodes/_base/components/variable-tag.tsx
  25. 141
    14
      web/app/components/workflow/nodes/_base/components/variable/utils.ts
  26. 1
    1
      web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.tsx
  27. 7
    2
      web/app/components/workflow/nodes/_base/components/variable/var-reference-vars.tsx
  28. 3
    3
      web/app/components/workflow/nodes/_base/hooks/use-available-var-list.ts
  29. 7
    6
      web/app/components/workflow/nodes/_base/hooks/use-node-help-link.ts
  30. 27
    7
      web/app/components/workflow/nodes/_base/node.tsx
  31. 4
    1
      web/app/components/workflow/nodes/assigner/components/var-list/index.tsx
  32. 0
    1
      web/app/components/workflow/nodes/assigner/node.tsx
  33. 7
    7
      web/app/components/workflow/nodes/assigner/use-config.ts
  34. 5
    1
      web/app/components/workflow/nodes/iteration/use-interactions.ts
  35. 47
    0
      web/app/components/workflow/nodes/llm/types.ts
  36. 23
    0
      web/app/components/workflow/nodes/loop-end/default.ts
  37. 0
    3
      web/app/components/workflow/nodes/loop/components/condition-wrap.tsx
  38. 13
    0
      web/app/components/workflow/nodes/loop/components/loop-variables/empty.tsx
  39. 144
    0
      web/app/components/workflow/nodes/loop/components/loop-variables/form-item.tsx
  40. 28
    0
      web/app/components/workflow/nodes/loop/components/loop-variables/index.tsx
  41. 37
    0
      web/app/components/workflow/nodes/loop/components/loop-variables/input-mode-selec.tsx
  42. 78
    0
      web/app/components/workflow/nodes/loop/components/loop-variables/item.tsx
  43. 51
    0
      web/app/components/workflow/nodes/loop/components/loop-variables/variable-type-select.tsx
  44. 5
    0
      web/app/components/workflow/nodes/loop/default.ts
  45. 27
    2
      web/app/components/workflow/nodes/loop/panel.tsx
  46. 18
    0
      web/app/components/workflow/nodes/loop/types.ts
  47. 52
    4
      web/app/components/workflow/nodes/loop/use-config.ts
  48. 5
    2
      web/app/components/workflow/nodes/loop/use-interactions.ts
  49. 65
    26
      web/app/components/workflow/nodes/variable-assigner/components/node-variable-item.tsx
  50. 2
    0
      web/app/components/workflow/operator/add-block.tsx
  51. 6
    1
      web/app/components/workflow/run/hooks.ts
  52. 7
    2
      web/app/components/workflow/run/loop-log/loop-log-trigger.tsx
  53. 19
    1
      web/app/components/workflow/run/loop-log/loop-result-panel.tsx
  54. 20
    5
      web/app/components/workflow/run/node.tsx
  55. 4
    0
      web/app/components/workflow/run/special-result-panel.tsx
  56. 2
    0
      web/app/components/workflow/run/tracing-panel.tsx
  57. 1
    0
      web/app/components/workflow/simple-node/constants.ts
  58. 148
    0
      web/app/components/workflow/simple-node/index.tsx
  59. 3
    0
      web/app/components/workflow/simple-node/types.ts
  60. 12
    2
      web/app/components/workflow/types.ts
  61. 11
    5
      web/app/components/workflow/utils.ts
  62. 12
    0
      web/i18n/en-US/workflow.ts
  63. 12
    0
      web/i18n/zh-Hans/workflow.ts
  64. 2
    0
      web/types/workflow.ts

+ 1
- 1
web/app/account/account-page/index.tsx View File

{userProfile.name} {userProfile.name}
{isEducationAccount && ( {isEducationAccount && (
<PremiumBadge size='s' color='blue' className='ml-1 !px-2'> <PremiumBadge size='s' color='blue' className='ml-1 !px-2'>
<RiGraduationCapFill className='w-3 h-3 mr-1' />
<RiGraduationCapFill className='mr-1 h-3 w-3' />
<span className='system-2xs-medium'>EDU</span> <span className='system-2xs-medium'>EDU</span>
</PremiumBadge> </PremiumBadge>
)} )}

+ 1
- 1
web/app/account/avatar.tsx View File

{userProfile.name} {userProfile.name}
{isEducationAccount && ( {isEducationAccount && (
<PremiumBadge size='s' color='blue' className='ml-1 !px-2'> <PremiumBadge size='s' color='blue' className='ml-1 !px-2'>
<RiGraduationCapFill className='w-3 h-3 mr-1' />
<RiGraduationCapFill className='mr-1 h-3 w-3' />
<span className='system-2xs-medium'>EDU</span> <span className='system-2xs-medium'>EDU</span>
</PremiumBadge> </PremiumBadge>
)} )}

+ 5
- 0
web/app/components/base/icons/assets/vender/workflow/loop-end.svg View File

<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="ongoing">
<path id="Vector" fill-rule="evenodd" clip-rule="evenodd" d="M8 2.75C5.10051 2.75 2.75 5.10051 2.75 8C2.75 10.8995 5.1005 13.25 8 13.25C8.41421 13.25 8.75 13.5858 8.75 14C8.75 14.4142 8.41421 14.75 8 14.75C4.27208 14.75 1.25 11.7279 1.25 8C1.25 4.27208 4.27208 1.25 8 1.25C8.41421 1.25 8.75 1.58579 8.75 2C8.75 2.41421 8.41421 2.75 8 2.75ZM10.3508 2.42715C10.5582 2.06861 11.017 1.94608 11.3755 2.15349C11.9971 2.51301 12.5556 2.96859 13.0311 3.49984C13.3073 3.8085 13.281 4.28264 12.9724 4.55887C12.6637 4.8351 12.1896 4.80882 11.9133 4.50016C11.5429 4.08625 11.1079 3.73153 10.6245 3.4519C10.2659 3.2445 10.1434 2.7857 10.3508 2.42715ZM8.13634 5.46967C8.42923 5.17678 8.9041 5.17678 9.197 5.46967L11.197 7.46967C11.4899 7.76256 11.4899 8.23744 11.197 8.53033L9.197 10.5303C8.9041 10.8232 8.42923 10.8232 8.13634 10.5303C7.84344 10.2374 7.84344 9.76256 8.13634 9.46967L8.85601 8.75H5.33333C4.91912 8.75 4.58333 8.41421 4.58333 8C4.58333 7.58579 4.91912 7.25 5.33333 7.25H8.85601L8.13634 6.53033C7.84344 6.23744 7.84344 5.76256 8.13634 5.46967ZM13.7414 6.09691C14.1478 6.01676 14.5422 6.28123 14.6224 6.68762C14.7062 7.1128 14.75 7.55166 14.75 8C14.75 8.44834 14.7062 8.88721 14.6224 9.31234C14.5422 9.71872 14.1478 9.98318 13.7414 9.90302C13.335 9.82287 13.0706 9.42845 13.1507 9.02206C13.2158 8.69213 13.25 8.35046 13.25 8C13.25 7.64954 13.2158 7.30787 13.1507 6.97785C13.0706 6.57146 13.335 6.17705 13.7414 6.09691ZM12.9723 11.4411C13.281 11.7173 13.3073 12.1915 13.0311 12.5002C12.5556 13.0314 11.9971 13.487 11.3756 13.8465C11.017 14.0539 10.5582 13.9314 10.3508 13.5729C10.1434 13.2143 10.2659 12.7556 10.6244 12.5481C11.1079 12.2685 11.5429 11.9138 11.9133 11.4999C12.1895 11.1912 12.6637 11.1649 12.9723 11.4411Z" fill="white"/>
</g>
</svg>

+ 9
- 5
web/app/components/base/icons/src/public/education/Triangle.tsx View File

import * as React from 'react' import * as React from 'react'
import data from './Triangle.json' import data from './Triangle.json'
import IconBase from '@/app/components/base/icons/IconBase' import IconBase from '@/app/components/base/icons/IconBase'
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
import type { IconData } from '@/app/components/base/icons/IconBase'


const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
props,
ref,
) => <IconBase {...props} ref={ref} data={data as IconData} />)
const Icon = (
{
ref,
...props
}: React.SVGProps<SVGSVGElement> & {
ref?: React.RefObject<React.MutableRefObject<HTMLOrSVGElement>>;
},
) => <IconBase {...props} ref={ref} data={data as IconData} />


Icon.displayName = 'Triangle' Icon.displayName = 'Triangle'



+ 38
- 0
web/app/components/base/icons/src/vender/workflow/LoopEnd.json View File

{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "16",
"height": "16",
"viewBox": "0 0 16 16",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "g",
"attributes": {
"id": "ongoing"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"id": "Vector",
"fill-rule": "evenodd",
"clip-rule": "evenodd",
"d": "M8 2.75C5.10051 2.75 2.75 5.10051 2.75 8C2.75 10.8995 5.1005 13.25 8 13.25C8.41421 13.25 8.75 13.5858 8.75 14C8.75 14.4142 8.41421 14.75 8 14.75C4.27208 14.75 1.25 11.7279 1.25 8C1.25 4.27208 4.27208 1.25 8 1.25C8.41421 1.25 8.75 1.58579 8.75 2C8.75 2.41421 8.41421 2.75 8 2.75ZM10.3508 2.42715C10.5582 2.06861 11.017 1.94608 11.3755 2.15349C11.9971 2.51301 12.5556 2.96859 13.0311 3.49984C13.3073 3.8085 13.281 4.28264 12.9724 4.55887C12.6637 4.8351 12.1896 4.80882 11.9133 4.50016C11.5429 4.08625 11.1079 3.73153 10.6245 3.4519C10.2659 3.2445 10.1434 2.7857 10.3508 2.42715ZM8.13634 5.46967C8.42923 5.17678 8.9041 5.17678 9.197 5.46967L11.197 7.46967C11.4899 7.76256 11.4899 8.23744 11.197 8.53033L9.197 10.5303C8.9041 10.8232 8.42923 10.8232 8.13634 10.5303C7.84344 10.2374 7.84344 9.76256 8.13634 9.46967L8.85601 8.75H5.33333C4.91912 8.75 4.58333 8.41421 4.58333 8C4.58333 7.58579 4.91912 7.25 5.33333 7.25H8.85601L8.13634 6.53033C7.84344 6.23744 7.84344 5.76256 8.13634 5.46967ZM13.7414 6.09691C14.1478 6.01676 14.5422 6.28123 14.6224 6.68762C14.7062 7.1128 14.75 7.55166 14.75 8C14.75 8.44834 14.7062 8.88721 14.6224 9.31234C14.5422 9.71872 14.1478 9.98318 13.7414 9.90302C13.335 9.82287 13.0706 9.42845 13.1507 9.02206C13.2158 8.69213 13.25 8.35046 13.25 8C13.25 7.64954 13.2158 7.30787 13.1507 6.97785C13.0706 6.57146 13.335 6.17705 13.7414 6.09691ZM12.9723 11.4411C13.281 11.7173 13.3073 12.1915 13.0311 12.5002C12.5556 13.0314 11.9971 13.487 11.3756 13.8465C11.017 14.0539 10.5582 13.9314 10.3508 13.5729C10.1434 13.2143 10.2659 12.7556 10.6244 12.5481C11.1079 12.2685 11.5429 11.9138 11.9133 11.4999C12.1895 11.1912 12.6637 11.1649 12.9723 11.4411Z",
"fill": "currentColor"
},
"children": []
}
]
}
]
},
"name": "LoopEnd"
}

+ 20
- 0
web/app/components/base/icons/src/vender/workflow/LoopEnd.tsx View File

// GENERATE BY script
// DON NOT EDIT IT MANUALLY

import * as React from 'react'
import data from './LoopEnd.json'
import IconBase from '@/app/components/base/icons/IconBase'
import type { IconData } from '@/app/components/base/icons/IconBase'

const Icon = (
{
ref,
...props
}: React.SVGProps<SVGSVGElement> & {
ref?: React.RefObject<React.MutableRefObject<HTMLOrSVGElement>>;
},
) => <IconBase {...props} ref={ref} data={data as IconData} />

Icon.displayName = 'LoopEnd'

export default Icon

+ 1
- 0
web/app/components/base/icons/src/vender/workflow/index.ts View File

export { default as KnowledgeRetrieval } from './KnowledgeRetrieval' export { default as KnowledgeRetrieval } from './KnowledgeRetrieval'
export { default as ListFilter } from './ListFilter' export { default as ListFilter } from './ListFilter'
export { default as Llm } from './Llm' export { default as Llm } from './Llm'
export { default as LoopEnd } from './LoopEnd'
export { default as Loop } from './Loop' export { default as Loop } from './Loop'
export { default as ParameterExtractor } from './ParameterExtractor' export { default as ParameterExtractor } from './ParameterExtractor'
export { default as QuestionClassifier } from './QuestionClassifier' export { default as QuestionClassifier } from './QuestionClassifier'

+ 157
- 0
web/app/components/base/select/pure.tsx View File

import {
useCallback,
useState,
} from 'react'
import { useTranslation } from 'react-i18next'
import {
RiArrowDownSLine,
RiCheckLine,
} from '@remixicon/react'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import type {
PortalToFollowElemOptions,
} from '@/app/components/base/portal-to-follow-elem'
import cn from '@/utils/classnames'

type Option = {
label: string
value: string
}

type PureSelectProps = {
options: Option[]
value?: string
onChange?: (value: string) => void
containerProps?: PortalToFollowElemOptions & {
open?: boolean
onOpenChange?: (open: boolean) => void
}
triggerProps?: {
className?: string
},
popupProps?: {
wrapperClassName?: string
className?: string
itemClassName?: string
title?: string
},
}
const PureSelect = ({
options,
value,
onChange,
containerProps,
triggerProps,
popupProps,
}: PureSelectProps) => {
const { t } = useTranslation()
const {
open,
onOpenChange,
placement,
offset,
} = containerProps || {}
const {
className: triggerClassName,
} = triggerProps || {}
const {
wrapperClassName: popupWrapperClassName,
className: popupClassName,
itemClassName: popupItemClassName,
title: popupTitle,
} = popupProps || {}

const [localOpen, setLocalOpen] = useState(false)
const mergedOpen = open ?? localOpen

const handleOpenChange = useCallback((openValue: boolean) => {
onOpenChange?.(openValue)
setLocalOpen(openValue)
}, [onOpenChange])

const selectedOption = options.find(option => option.value === value)
const triggerText = selectedOption?.label || t('common.placeholder.select')

return (
<PortalToFollowElem
placement={placement || 'bottom-start'}
offset={offset || 4}
open={mergedOpen}
onOpenChange={handleOpenChange}
>
<PortalToFollowElemTrigger
onClick={() => handleOpenChange(!mergedOpen)}
asChild
>
<div
className={cn(
'system-sm-regular group flex h-8 cursor-pointer items-center rounded-lg bg-components-input-bg-normal px-2 text-components-input-text-filled hover:bg-state-base-hover-alt',
mergedOpen && 'bg-state-base-hover-alt',
triggerClassName,
)}
>
<div
className='grow'
title={triggerText}
>
{triggerText}
</div>
<RiArrowDownSLine
className={cn(
'h-4 w-4 shrink-0 text-text-quaternary group-hover:text-text-secondary',
mergedOpen && 'text-text-secondary',
)}
/>
</div>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className={cn(
'z-10',
popupWrapperClassName,
)}>
<div
className={cn(
'rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-1 shadow-lg',
popupClassName,
)}
>
{
popupTitle && (
<div className='system-xs-medium-uppercase flex h-[22px] items-center px-3 text-text-tertiary'>
{popupTitle}
</div>
)
}
{
options.map(option => (
<div
key={option.value}
className={cn(
'system-sm-medium flex h-8 cursor-pointer items-center rounded-lg px-2 text-text-secondary hover:bg-state-base-hover',
popupItemClassName,
)}
title={option.label}
onClick={() => {
onChange?.(option.value)
handleOpenChange(false)
}}
>
<div className='mr-1 grow truncate px-1'>
{option.label}
</div>
{
value === option.value && <RiCheckLine className='h-4 w-4 shrink-0 text-text-accent' />
}
</div>
))
}
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
)
}

export default PureSelect

+ 1
- 1
web/app/components/header/account-dropdown/index.tsx View File

{userProfile.name} {userProfile.name}
{isEducationAccount && ( {isEducationAccount && (
<PremiumBadge size='s' color='blue' className='ml-1 !px-2'> <PremiumBadge size='s' color='blue' className='ml-1 !px-2'>
<RiGraduationCapFill className='w-3 h-3 mr-1' />
<RiGraduationCapFill className='mr-1 h-3 w-3' />
<span className='system-2xs-medium'>EDU</span> <span className='system-2xs-medium'>EDU</span>
</PremiumBadge> </PremiumBadge>
)} )}

+ 2
- 2
web/app/components/header/plan-badge/index.tsx View File

if (plan === Plan.professional) { if (plan === Plan.professional) {
return <PremiumBadge className='select-none' size='s' color='blue' allowHover={allowHover} onClick={onClick}> return <PremiumBadge className='select-none' size='s' color='blue' allowHover={allowHover} onClick={onClick}>
<div className='system-2xs-medium-uppercase'> <div className='system-2xs-medium-uppercase'>
<span className='p-1 inline-flex items-center gap-1'>
{isEducationWorkspace && <RiGraduationCapFill className='w-3 h-3' />}
<span className='inline-flex items-center gap-1 p-1'>
{isEducationWorkspace && <RiGraduationCapFill className='h-3 w-3' />}
pro pro
</span> </span>
</div> </div>

+ 3
- 0
web/app/components/workflow/block-icon.tsx View File

ListFilter, ListFilter,
Llm, Llm,
Loop, Loop,
LoopEnd,
ParameterExtractor, ParameterExtractor,
QuestionClassifier, QuestionClassifier,
TemplatingTransform, TemplatingTransform,
[BlockEnum.Iteration]: <Iteration className={className} />, [BlockEnum.Iteration]: <Iteration className={className} />,
[BlockEnum.LoopStart]: <VariableX className={className} />, [BlockEnum.LoopStart]: <VariableX className={className} />,
[BlockEnum.Loop]: <Loop className={className} />, [BlockEnum.Loop]: <Loop className={className} />,
[BlockEnum.LoopEnd]: <LoopEnd className={className} />,
[BlockEnum.ParameterExtractor]: <ParameterExtractor className={className} />, [BlockEnum.ParameterExtractor]: <ParameterExtractor className={className} />,
[BlockEnum.DocExtractor]: <DocsExtractor className={className} />, [BlockEnum.DocExtractor]: <DocsExtractor className={className} />,
[BlockEnum.ListFilter]: <ListFilter className={className} />, [BlockEnum.ListFilter]: <ListFilter className={className} />,
[BlockEnum.IfElse]: 'bg-util-colors-cyan-cyan-500', [BlockEnum.IfElse]: 'bg-util-colors-cyan-cyan-500',
[BlockEnum.Iteration]: 'bg-util-colors-cyan-cyan-500', [BlockEnum.Iteration]: 'bg-util-colors-cyan-cyan-500',
[BlockEnum.Loop]: 'bg-util-colors-cyan-cyan-500', [BlockEnum.Loop]: 'bg-util-colors-cyan-cyan-500',
[BlockEnum.LoopEnd]: 'bg-util-colors-warning-warning-500',
[BlockEnum.HttpRequest]: 'bg-util-colors-violet-violet-500', [BlockEnum.HttpRequest]: 'bg-util-colors-violet-violet-500',
[BlockEnum.Answer]: 'bg-util-colors-warning-warning-500', [BlockEnum.Answer]: 'bg-util-colors-warning-warning-500',
[BlockEnum.KnowledgeRetrieval]: 'bg-util-colors-green-green-500', [BlockEnum.KnowledgeRetrieval]: 'bg-util-colors-green-green-500',

+ 10
- 1
web/app/components/workflow/block-selector/blocks.tsx View File

import { useBlocks } from './hooks' import { useBlocks } from './hooks'
import type { ToolDefaultValue } from './types' import type { ToolDefaultValue } from './types'
import Tooltip from '@/app/components/base/tooltip' import Tooltip from '@/app/components/base/tooltip'
import Badge from '@/app/components/base/badge'


type BlocksProps = { type BlocksProps = {
searchText: string searchText: string
className='mr-2 shrink-0' className='mr-2 shrink-0'
type={block.type} type={block.type}
/> />
<div className='text-sm text-text-secondary'>{block.title}</div>
<div className='grow text-sm text-text-secondary'>{block.title}</div>
{
block.type === BlockEnum.LoopEnd && (
<Badge
text={t('workflow.nodes.loop.loopNode')}
className='ml-2 shrink-0'
/>
)
}
</div> </div>
</Tooltip> </Tooltip>
)) ))

+ 6
- 0
web/app/components/workflow/block-selector/constants.tsx View File

type: BlockEnum.IfElse, type: BlockEnum.IfElse,
title: 'IF/ELSE', title: 'IF/ELSE',
}, },
{
classification: BlockClassificationEnum.Logic,
type: BlockEnum.LoopEnd,
title: 'Exit Loop',
description: '',
},
{ {
classification: BlockClassificationEnum.Logic, classification: BlockClassificationEnum.Logic,
type: BlockEnum.Iteration, type: BlockEnum.Iteration,

+ 16
- 0
web/app/components/workflow/constants.ts View File

import IterationStartDefault from './nodes/iteration-start/default' import IterationStartDefault from './nodes/iteration-start/default'
import AgentDefault from './nodes/agent/default' import AgentDefault from './nodes/agent/default'
import LoopStartDefault from './nodes/loop-start/default' import LoopStartDefault from './nodes/loop-start/default'
import LoopEndDefault from './nodes/loop-end/default'


type NodesExtraData = { type NodesExtraData = {
author: string author: string
getAvailableNextNodes: LoopStartDefault.getAvailableNextNodes, getAvailableNextNodes: LoopStartDefault.getAvailableNextNodes,
checkValid: LoopStartDefault.checkValid, checkValid: LoopStartDefault.checkValid,
}, },
[BlockEnum.LoopEnd]: {
author: 'Dify',
about: '',
availablePrevNodes: [],
availableNextNodes: [],
getAvailablePrevNodes: LoopEndDefault.getAvailablePrevNodes,
getAvailableNextNodes: LoopEndDefault.getAvailableNextNodes,
checkValid: LoopEndDefault.checkValid,
},
[BlockEnum.Code]: { [BlockEnum.Code]: {
author: 'Dify', author: 'Dify',
about: '', about: '',
desc: '', desc: '',
...LoopStartDefault.defaultValue, ...LoopStartDefault.defaultValue,
}, },
[BlockEnum.LoopEnd]: {
type: BlockEnum.LoopEnd,
title: '',
desc: '',
...LoopEndDefault.defaultValue,
},
[BlockEnum.Code]: { [BlockEnum.Code]: {
type: BlockEnum.Code, type: BlockEnum.Code,
title: '', title: '',

+ 7
- 0
web/app/components/workflow/hooks/use-nodes-data.ts View File

const availableNextBlocks = useMemo(() => { const availableNextBlocks = useMemo(() => {
if (!nodeType) if (!nodeType)
return [] return []

return nodesExtraData[nodeType].availableNextNodes || [] return nodesExtraData[nodeType].availableNextNodes || []
}, [nodeType, nodesExtraData]) }, [nodeType, nodesExtraData])


if (isInLoop && (nType === BlockEnum.Iteration || nType === BlockEnum.Loop || nType === BlockEnum.End)) if (isInLoop && (nType === BlockEnum.Iteration || nType === BlockEnum.Loop || nType === BlockEnum.End))
return false return false


if (!isInLoop && nType === BlockEnum.LoopEnd)
return false

return true return true
}), }),
availableNextBlocks: availableNextBlocks.filter((nType) => { availableNextBlocks: availableNextBlocks.filter((nType) => {
if (isInLoop && (nType === BlockEnum.Iteration || nType === BlockEnum.Loop || nType === BlockEnum.End)) if (isInLoop && (nType === BlockEnum.Iteration || nType === BlockEnum.Loop || nType === BlockEnum.End))
return false return false


if (!isInLoop && nType === BlockEnum.LoopEnd)
return false

return true return true
}), }),
} }

+ 17
- 14
web/app/components/workflow/hooks/use-nodes-interactions.ts View File

import { import {
genNewNodeTitleFromOld, genNewNodeTitleFromOld,
generateNewNode, generateNewNode,
getNodeCustomTypeByNodeDataType,
getNodesConnectedSourceOrTargetHandleIdsMap, getNodesConnectedSourceOrTargetHandleIdsMap,
getTopLeftNodePosition, getTopLeftNodePosition,
} from '../utils' } from '../utils'
} }


if (node.id === currentNode.parentId) if (node.id === currentNode.parentId)
node.data._children = node.data._children?.filter(child => child !== nodeId)
node.data._children = node.data._children?.filter(child => child.nodeId !== nodeId)
}) })
draft.splice(currentNodeIndex, 1) draft.splice(currentNodeIndex, 1)
}) })
newIterationStartNode, newIterationStartNode,
newLoopStartNode, newLoopStartNode,
} = generateNewNode({ } = generateNewNode({
type: getNodeCustomTypeByNodeDataType(nodeType),
data: { data: {
...NODES_INITIAL_DATA[nodeType], ...NODES_INITIAL_DATA[nodeType],
title: nodesWithSameType.length > 0 ? `${t(`workflow.blocks.${nodeType}`)} ${nodesWithSameType.length + 1}` : t(`workflow.blocks.${nodeType}`), title: nodesWithSameType.length > 0 ? `${t(`workflow.blocks.${nodeType}`)} ${nodesWithSameType.length + 1}` : t(`workflow.blocks.${nodeType}`),
} }


if (node.data.type === BlockEnum.Iteration && prevNode.parentId === node.id) if (node.data.type === BlockEnum.Iteration && prevNode.parentId === node.id)
node.data._children?.push(newNode.id)
node.data._children?.push({ nodeId: newNode.id, nodeType: newNode.data.type })


if (node.data.type === BlockEnum.Loop && prevNode.parentId === node.id) if (node.data.type === BlockEnum.Loop && prevNode.parentId === node.id)
node.data._children?.push(newNode.id)
node.data._children?.push({ nodeId: newNode.id, nodeType: newNode.data.type })
}) })
draft.push(newNode) draft.push(newNode)




let newEdge let newEdge


if ((nodeType !== BlockEnum.IfElse) && (nodeType !== BlockEnum.QuestionClassifier)) {
if ((nodeType !== BlockEnum.IfElse) && (nodeType !== BlockEnum.QuestionClassifier) && (nodeType !== BlockEnum.LoopEnd)) {
newEdge = { newEdge = {
id: `${newNode.id}-${sourceHandle}-${nextNodeId}-${nextNodeTargetHandle}`, id: `${newNode.id}-${sourceHandle}-${nextNodeId}-${nextNodeTargetHandle}`,
type: CUSTOM_EDGE, type: CUSTOM_EDGE,
} }


if (node.data.type === BlockEnum.Iteration && nextNode.parentId === node.id) if (node.data.type === BlockEnum.Iteration && nextNode.parentId === node.id)
node.data._children?.push(newNode.id)
node.data._children?.push({ nodeId: newNode.id, nodeType: newNode.data.type })


if (node.data.type === BlockEnum.Iteration && node.data.start_node_id === nextNodeId) { if (node.data.type === BlockEnum.Iteration && node.data.start_node_id === nextNodeId) {
node.data.start_node_id = newNode.id node.data.start_node_id = newNode.id
} }


if (node.data.type === BlockEnum.Loop && nextNode.parentId === node.id) if (node.data.type === BlockEnum.Loop && nextNode.parentId === node.id)
node.data._children?.push(newNode.id)
node.data._children?.push({ nodeId: newNode.id, nodeType: newNode.data.type })


if (node.data.type === BlockEnum.Loop && node.data.start_node_id === nextNodeId) { if (node.data.type === BlockEnum.Loop && node.data.start_node_id === nextNodeId) {
node.data.start_node_id = newNode.id node.data.start_node_id = newNode.id
const isNextNodeInIteration = !!nextNodeParentNode && nextNodeParentNode.data.type === BlockEnum.Iteration const isNextNodeInIteration = !!nextNodeParentNode && nextNodeParentNode.data.type === BlockEnum.Iteration
const isNextNodeInLoop = !!nextNodeParentNode && nextNodeParentNode.data.type === BlockEnum.Loop const isNextNodeInLoop = !!nextNodeParentNode && nextNodeParentNode.data.type === BlockEnum.Loop


if (nodeType !== BlockEnum.IfElse && nodeType !== BlockEnum.QuestionClassifier) {
if (nodeType !== BlockEnum.IfElse && nodeType !== BlockEnum.QuestionClassifier && nodeType !== BlockEnum.LoopEnd) {
newNextEdge = { newNextEdge = {
id: `${newNode.id}-${sourceHandle}-${nextNodeId}-${nextNodeTargetHandle}`, id: `${newNode.id}-${sourceHandle}-${nextNodeId}-${nextNodeTargetHandle}`,
type: CUSTOM_EDGE, type: CUSTOM_EDGE,
node.position.x += NODE_WIDTH_X_OFFSET node.position.x += NODE_WIDTH_X_OFFSET


if (node.data.type === BlockEnum.Iteration && prevNode.parentId === node.id) if (node.data.type === BlockEnum.Iteration && prevNode.parentId === node.id)
node.data._children?.push(newNode.id)
node.data._children?.push({ nodeId: newNode.id, nodeType: newNode.data.type })
if (node.data.type === BlockEnum.Loop && prevNode.parentId === node.id) if (node.data.type === BlockEnum.Loop && prevNode.parentId === node.id)
node.data._children?.push(newNode.id)
node.data._children?.push({ nodeId: newNode.id, nodeType: newNode.data.type })
}) })
draft.push(newNode) draft.push(newNode)
if (newIterationStartNode) if (newIterationStartNode)
newIterationStartNode, newIterationStartNode,
newLoopStartNode, newLoopStartNode,
} = generateNewNode({ } = generateNewNode({
type: getNodeCustomTypeByNodeDataType(nodeType),
data: { data: {
...NODES_INITIAL_DATA[nodeType], ...NODES_INITIAL_DATA[nodeType],
title: nodesWithSameType.length > 0 ? `${t(`workflow.blocks.${nodeType}`)} ${nodesWithSameType.length + 1}` : t(`workflow.blocks.${nodeType}`), title: nodesWithSameType.length > 0 ? `${t(`workflow.blocks.${nodeType}`)} ${nodesWithSameType.length + 1}` : t(`workflow.blocks.${nodeType}`),
if (nodeId) { if (nodeId) {
// If nodeId is provided, copy that specific node // If nodeId is provided, copy that specific node
const nodeToCopy = nodes.find(node => node.id === nodeId && node.data.type !== BlockEnum.Start const nodeToCopy = nodes.find(node => node.id === nodeId && node.data.type !== BlockEnum.Start
&& node.type !== CUSTOM_ITERATION_START_NODE && node.type !== CUSTOM_LOOP_START_NODE)
&& node.type !== CUSTOM_ITERATION_START_NODE && node.type !== CUSTOM_LOOP_START_NODE && node.data.type !== BlockEnum.LoopEnd)
if (nodeToCopy) if (nodeToCopy)
setClipboardElements([nodeToCopy]) setClipboardElements([nodeToCopy])
} }
return return
} }


const selectedNode = nodes.find(node => node.data.selected && node.data.type !== BlockEnum.Start)
const selectedNode = nodes.find(node => node.data.selected && node.data.type !== BlockEnum.Start && node.data.type !== BlockEnum.LoopEnd)


if (selectedNode) if (selectedNode)
setClipboardElements([selectedNode]) setClipboardElements([selectedNode])
newChildren = copyChildren newChildren = copyChildren
idMapping = newIdMapping idMapping = newIdMapping
newChildren.forEach((child) => { newChildren.forEach((child) => {
newNode.data._children?.push(child.id)
newNode.data._children?.push({ nodeId: child.id, nodeType: child.data.type })
}) })
newChildren.push(newIterationStartNode!) newChildren.push(newIterationStartNode!)
} }


newChildren = handleNodeLoopChildrenCopy(nodeToPaste.id, newNode.id) newChildren = handleNodeLoopChildrenCopy(nodeToPaste.id, newNode.id)
newChildren.forEach((child) => { newChildren.forEach((child) => {
newNode.data._children?.push(child.id)
newNode.data._children?.push({ nodeId: child.id, nodeType: child.data.type })
}) })
newChildren.push(newLoopStartNode!) newChildren.push(newLoopStartNode!)
} }


const nodes = getNodes() const nodes = getNodes()
const currentNode = nodes.find(n => n.id === nodeId)! const currentNode = nodes.find(n => n.id === nodeId)!
const childrenNodes = nodes.filter(n => currentNode.data._children?.includes(n.id))
const childrenNodes = nodes.filter(n => currentNode.data._children?.find((c: any) => c.nodeId === n.id))
let rightNode: Node let rightNode: Node
let bottomNode: Node let bottomNode: Node



+ 14
- 0
web/app/components/workflow/hooks/use-workflow-run-event/use-workflow-node-iteration-finished.ts View File

const { const {
getNodes, getNodes,
setNodes, setNodes,
edges,
setEdges,
} = store.getState() } = store.getState()
const nodes = getNodes() const nodes = getNodes()
setWorkflowRunningData(produce(workflowRunningData!, (draft) => { setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
currentNode.data._runningStatus = data.status currentNode.data._runningStatus = data.status
}) })
setNodes(newNodes) setNodes(newNodes)
const newEdges = produce(edges, (draft) => {
const incomeEdges = draft.filter((edge) => {
return edge.target === data.node_id
})
incomeEdges.forEach((edge) => {
edge.data = {
...edge.data,
_targetRunningStatus: data.status as any,
}
})
})
setEdges(newEdges)
}, [workflowStore, store]) }, [workflowStore, store])


return { return {

+ 14
- 3
web/app/components/workflow/hooks/use-workflow-run-event/use-workflow-node-loop-finished.ts View File

import produce from 'immer' import produce from 'immer'
import type { LoopFinishedResponse } from '@/types/workflow' import type { LoopFinishedResponse } from '@/types/workflow'
import { useWorkflowStore } from '@/app/components/workflow/store' import { useWorkflowStore } from '@/app/components/workflow/store'
import { DEFAULT_LOOP_TIMES } from '@/app/components/workflow/constants'


export const useWorkflowNodeLoopFinished = () => { export const useWorkflowNodeLoopFinished = () => {
const store = useStoreApi() const store = useStoreApi()
const { const {
workflowRunningData, workflowRunningData,
setWorkflowRunningData, setWorkflowRunningData,
setLoopTimes,
} = workflowStore.getState() } = workflowStore.getState()
const { const {
getNodes, getNodes,
setNodes, setNodes,
edges,
setEdges,
} = store.getState() } = store.getState()
const nodes = getNodes() const nodes = getNodes()
setWorkflowRunningData(produce(workflowRunningData!, (draft) => { setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
} }
} }
})) }))
setLoopTimes(DEFAULT_LOOP_TIMES)
const newNodes = produce(nodes, (draft) => { const newNodes = produce(nodes, (draft) => {
const currentNode = draft.find(node => node.id === data.node_id)! const currentNode = draft.find(node => node.id === data.node_id)!


currentNode.data._runningStatus = data.status currentNode.data._runningStatus = data.status
}) })
setNodes(newNodes) setNodes(newNodes)
const newEdges = produce(edges, (draft) => {
const incomeEdges = draft.filter((edge) => {
return edge.target === data.node_id
})
incomeEdges.forEach((edge) => {
edge.data = {
...edge.data,
_targetRunningStatus: data.status as any,
}
})
})
setEdges(newEdges)
}, [workflowStore, store]) }, [workflowStore, store])


return { return {

+ 10
- 10
web/app/components/workflow/hooks/use-workflow-run-event/use-workflow-node-loop-next.ts View File

import { useStoreApi } from 'reactflow' import { useStoreApi } from 'reactflow'
import produce from 'immer' import produce from 'immer'
import type { LoopNextResponse } from '@/types/workflow' import type { LoopNextResponse } from '@/types/workflow'
import { useWorkflowStore } from '@/app/components/workflow/store'
import { NodeRunningStatus } from '@/app/components/workflow/types'


export const useWorkflowNodeLoopNext = () => { export const useWorkflowNodeLoopNext = () => {
const store = useStoreApi() const store = useStoreApi()
const workflowStore = useWorkflowStore()


const handleWorkflowNodeLoopNext = useCallback((params: LoopNextResponse) => { const handleWorkflowNodeLoopNext = useCallback((params: LoopNextResponse) => {
const {
loopTimes,
setLoopTimes,
} = workflowStore.getState()

const { data } = params const { data } = params
const { const {
getNodes, getNodes,
const nodes = getNodes() const nodes = getNodes()
const newNodes = produce(nodes, (draft) => { const newNodes = produce(nodes, (draft) => {
const currentNode = draft.find(node => node.id === data.node_id)! const currentNode = draft.find(node => node.id === data.node_id)!
currentNode.data._loopIndex = loopTimes
setLoopTimes(loopTimes + 1)
currentNode.data._loopIndex = data.index

draft.forEach((node) => {
if (node.parentId === data.node_id) {
node.data._waitingRun = true
node.data._runningStatus = NodeRunningStatus.Waiting
}
})
}) })
setNodes(newNodes) setNodes(newNodes)
}, [workflowStore, store])
}, [store])


return { return {
handleWorkflowNodeLoopNext, handleWorkflowNodeLoopNext,

+ 0
- 3
web/app/components/workflow/hooks/use-workflow-run-event/use-workflow-node-loop-started.ts View File

import { useWorkflowStore } from '@/app/components/workflow/store' import { useWorkflowStore } from '@/app/components/workflow/store'
import type { LoopStartedResponse } from '@/types/workflow' import type { LoopStartedResponse } from '@/types/workflow'
import { NodeRunningStatus } from '@/app/components/workflow/types' import { NodeRunningStatus } from '@/app/components/workflow/types'
import { DEFAULT_LOOP_TIMES } from '@/app/components/workflow/constants'


export const useWorkflowNodeLoopStarted = () => { export const useWorkflowNodeLoopStarted = () => {
const store = useStoreApi() const store = useStoreApi()
const { const {
workflowRunningData, workflowRunningData,
setWorkflowRunningData, setWorkflowRunningData,
setLoopTimes,
} = workflowStore.getState() } = workflowStore.getState()
const { const {
getNodes, getNodes,
status: NodeRunningStatus.Running, status: NodeRunningStatus.Running,
}) })
})) }))
setLoopTimes(DEFAULT_LOOP_TIMES)


const { const {
setViewport, setViewport,

+ 3
- 0
web/app/components/workflow/index.tsx View File

import { CUSTOM_ITERATION_START_NODE } from './nodes/iteration-start/constants' import { CUSTOM_ITERATION_START_NODE } from './nodes/iteration-start/constants'
import CustomLoopStartNode from './nodes/loop-start' import CustomLoopStartNode from './nodes/loop-start'
import { CUSTOM_LOOP_START_NODE } from './nodes/loop-start/constants' import { CUSTOM_LOOP_START_NODE } from './nodes/loop-start/constants'
import CustomSimpleNode from './simple-node'
import { CUSTOM_SIMPLE_NODE } from './simple-node/constants'
import Operator from './operator' import Operator from './operator'
import CustomEdge from './custom-edge' import CustomEdge from './custom-edge'
import CustomConnectionLine from './custom-connection-line' import CustomConnectionLine from './custom-connection-line'
const nodeTypes = { const nodeTypes = {
[CUSTOM_NODE]: CustomNode, [CUSTOM_NODE]: CustomNode,
[CUSTOM_NOTE_NODE]: CustomNoteNode, [CUSTOM_NOTE_NODE]: CustomNoteNode,
[CUSTOM_SIMPLE_NODE]: CustomSimpleNode,
[CUSTOM_ITERATION_START_NODE]: CustomIterationStartNode, [CUSTOM_ITERATION_START_NODE]: CustomIterationStartNode,
[CUSTOM_LOOP_START_NODE]: CustomLoopStartNode, [CUSTOM_LOOP_START_NODE]: CustomLoopStartNode,
} }

+ 3
- 0
web/app/components/workflow/nodes/_base/components/help-link.tsx View File

const { t } = useTranslation() const { t } = useTranslation()
const link = useNodeHelpLink(nodeType) const link = useNodeHelpLink(nodeType)


if (!link)
return null

return ( return (
<TooltipPlus <TooltipPlus
popupContent={t('common.userProfile.helpCenter')} popupContent={t('common.userProfile.helpCenter')}

+ 1
- 1
web/app/components/workflow/nodes/_base/components/panel-operator/panel-operator-popup.tsx View File

) )
} }
{ {
showHelpLink && (
showHelpLink && link && (
<> <>
<div className='p-1'> <div className='p-1'>
<a <a

+ 2
- 1
web/app/components/workflow/nodes/_base/components/variable-tag.tsx View File

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

+ 141
- 14
web/app/components/workflow/nodes/_base/components/variable/utils.ts View File

import type { CodeNodeType } from '../../../code/types' import type { CodeNodeType } from '../../../code/types'
import type { EndNodeType } from '../../../end/types' import type { EndNodeType } from '../../../end/types'
import type { AnswerNodeType } from '../../../answer/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 { KnowledgeRetrievalNodeType } from '../../../knowledge-retrieval/types'
import type { IfElseNodeType } from '../../../if-else/types' import type { IfElseNodeType } from '../../../if-else/types'
import type { TemplateTransformNodeType } from '../../../template-transform/types' import type { TemplateTransformNodeType } from '../../../template-transform/types'
import type { StartNodeType } from '@/app/components/workflow/nodes/start/types' 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 { ConversationVariable, EnvironmentVariable, Node, NodeOutPutVar, ValueSelector, Var } from '@/app/components/workflow/types'
import type { VariableAssignerNodeType } from '@/app/components/workflow/nodes/variable-assigner/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 { import {
HTTP_REQUEST_OUTPUT_STRUCT, HTTP_REQUEST_OUTPUT_STRUCT,
KNOWLEDGE_RETRIEVAL_OUTPUT_STRUCT, KNOWLEDGE_RETRIEVAL_OUTPUT_STRUCT,
} as any)[type] || VarType.string } 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 findExceptVarInObject = (obj: any, filterVar: (payload: Var, selector: ValueSelector) => boolean, value_selector: ValueSelector, isFile?: boolean): Var => {
const { children } = obj const { children } = obj
const isStructuredOutput = !!(children as StructuredOutput)?.schema?.properties

const res: Var = { const res: Var = {
variable: obj.variable, variable: obj.variable,
type: isFile ? VarType.file : VarType.object, type: isFile ? VarType.file : VarType.object,
children: children.filter((item: Var) => {
children: isStructuredOutput ? findExceptVarInStructuredOutput(children, filterVar) : children.filter((item: Var) => {
const { children } = item const { children } = item
const currSelector = [...value_selector, item.variable] const currSelector = [...value_selector, item.variable]
if (!children) if (!children)
return filterVar(item, currSelector) return filterVar(item, currSelector)

const obj = findExceptVarInObject(item, filterVar, currSelector, false) // File doesn't contains file children 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 return res
} }


case BlockEnum.LLM: { 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 break
} }

case BlockEnum.KnowledgeRetrieval: { case BlockEnum.KnowledgeRetrieval: {
res.vars = KNOWLEDGE_RETRIEVAL_OUTPUT_STRUCT res.vars = KNOWLEDGE_RETRIEVAL_OUTPUT_STRUCT
break break
break 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: { case BlockEnum.DocExtractor: {
res.vars = [ res.vars = [
{ {
return false return false


const obj = findExceptVarInObject(isFile ? { ...v, children } : v, filterVar, selector, isFile) 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) => { }).map((v) => {
const isFile = v.type === VarType.file const isFile = v.type === VarType.file


}, },
} }
const res = [ 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] : []), ...(environmentVariables.length > 0 ? [ENV_NODE] : []),
...((isChatMode && conversationVariables.length > 0) ? [CHAT_VAR_NODE] : []), ...((isChatMode && conversationVariables.length > 0) ? [CHAT_VAR_NODE] : []),
].map((node) => { ].map((node) => {
isConstant, isConstant,
environmentVariables = [], environmentVariables = [],
conversationVariables = [], conversationVariables = [],
}:
{
}: {
valueSelector: ValueSelector valueSelector: ValueSelector
parentNode?: Node | null parentNode?: Node | null
isIterationItem?: boolean isIterationItem?: boolean
const isEnv = isENV(valueSelector) const isEnv = isENV(valueSelector)
const isChatVar = isConversationVar(valueSelector) const isChatVar = isConversationVar(valueSelector)
const startNode = availableNodes.find((node: any) => { 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] const targetVarNodeId = isSystem ? startNode?.id : valueSelector[0]


let type: VarType = VarType.string let type: VarType = VarType.string
let curr: any = targetVar.vars let curr: any = targetVar.vars

if (isSystem || isEnv || isChatVar) { if (isSystem || isEnv || isChatVar) {
return curr.find((v: any) => v.variable === (valueSelector as ValueSelector).join('.'))?.type return curr.find((v: any) => v.variable === (valueSelector as ValueSelector).join('.'))?.type
} }
else { 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) => { (valueSelector as ValueSelector).slice(1).forEach((key, i) => {
const isLast = i === valueSelector.length - 2 const isLast = i === valueSelector.length - 2
if (Array.isArray(curr)) if (Array.isArray(curr))
}, },
], ],
} }
const iterationIndex = beforeNodesOutputVars.findIndex(v => v.nodeId === iterationNode?.id)
if (iterationIndex > -1)
beforeNodesOutputVars.splice(iterationIndex, 1)
beforeNodesOutputVars.unshift(iterationVar) beforeNodesOutputVars.unshift(iterationVar)
} }
return beforeNodesOutputVars return beforeNodesOutputVars
}) })
return newNode return newNode
} }

const varToValueSelectorList = (v: Var, parentValueSelector: ValueSelector, res: ValueSelector[]) => { const varToValueSelectorList = (v: Var, parentValueSelector: ValueSelector, res: ValueSelector[]) => {
if (!v.variable) if (!v.variable)
return return


res.push([...parentValueSelector, v.variable]) 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) 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[]) => { const varsToValueSelectorList = (vars: Var | Var[], parentValueSelector: ValueSelector, res: ValueSelector[]) => {
} }


case BlockEnum.LLM: { 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 break
} }



+ 1
- 1
web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.tsx View File

const isChatMode = useIsChatMode() const isChatMode = useIsChatMode()


const { getCurrentVariableType } = useWorkflowVariables() const { getCurrentVariableType } = useWorkflowVariables()
const { availableNodes, availableVars } = useAvailableVarList(nodeId, {
const { availableVars, availableNodesWithParent: availableNodes } = useAvailableVarList(nodeId, {
onlyLeafNodeVar, onlyLeafNodeVar,
passedInAvailableNodes, passedInAvailableNodes,
filterVar, filterVar,

+ 7
- 2
web/app/components/workflow/nodes/_base/components/variable/var-reference-vars.tsx View File

import { BubbleX, Env } from '@/app/components/base/icons/src/vender/line/others' import { BubbleX, Env } from '@/app/components/base/icons/src/vender/line/others'
import { checkKeys } from '@/utils/var' import { checkKeys } from '@/utils/var'
import { FILE_STRUCT } from '@/app/components/workflow/constants' import { FILE_STRUCT } from '@/app/components/workflow/constants'
import { Loop } from '@/app/components/base/icons/src/vender/workflow'


type ObjectChildrenProps = { type ObjectChildrenProps = {
nodeId: string nodeId: string
itemWidth?: number itemWidth?: number
isSupportFileVar?: boolean isSupportFileVar?: boolean
isException?: boolean isException?: boolean
isLoopVar?: boolean
} }


const Item: FC<ItemProps> = ({ const Item: FC<ItemProps> = ({
itemWidth, itemWidth,
isSupportFileVar, isSupportFileVar,
isException, isException,
isLoopVar,
}) => { }) => {
const isFile = itemData.type === VarType.file const isFile = itemData.type === VarType.file
const isObj = ([VarType.object, VarType.file].includes(itemData.type) && itemData.children && itemData.children.length > 0) const isObj = ([VarType.object, VarType.file].includes(itemData.type) && itemData.children && itemData.children.length > 0)
onMouseDown={e => e.preventDefault()} onMouseDown={e => e.preventDefault()}
> >
<div className='flex w-0 grow items-center'> <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' />} {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 && ( {!isEnv && !isChatVar && (
<div title={itemData.variable} className='system-sm-medium ml-1 w-0 grow truncate text-text-secondary'>{itemData.variable}</div> <div title={itemData.variable} className='system-sm-medium ml-1 w-0 grow truncate text-text-secondary'>{itemData.variable}</div>
)} )}
itemWidth={itemWidth} itemWidth={itemWidth}
isSupportFileVar={isSupportFileVar} isSupportFileVar={isSupportFileVar}
isException={v.isException} isException={v.isException}
isLoopVar={item.isLoop}
/> />
))} ))}
</div>)) </div>))

+ 3
- 3
web/app/components/workflow/nodes/_base/hooks/use-available-var-list.ts View File

onlyLeafNodeVar: false, onlyLeafNodeVar: false,
filterVar: () => true, filterVar: () => true,
}) => { }) => {
const { getTreeLeafNodes, getBeforeNodesInSameBranch } = useWorkflow()
const { getTreeLeafNodes, getBeforeNodesInSameBranchIncludeParent } = useWorkflow()
const { getNodeAvailableVars } = useWorkflowVariables() const { getNodeAvailableVars } = useWorkflowVariables()
const isChatMode = useIsChatMode() const isChatMode = useIsChatMode()


const availableNodes = passedInAvailableNodes || (onlyLeafNodeVar ? getTreeLeafNodes(nodeId) : getBeforeNodesInSameBranch(nodeId))
const availableNodes = passedInAvailableNodes || (onlyLeafNodeVar ? getTreeLeafNodes(nodeId) : getBeforeNodesInSameBranchIncludeParent(nodeId))


const { const {
parentNode: iterationNode, parentNode: iterationNode,
return { return {
availableVars, availableVars,
availableNodes, availableNodes,
availableNodesWithParent: iterationNode ? [...availableNodes, iterationNode] : availableNodes,
availableNodesWithParent: availableNodes,
} }
} }



+ 7
- 6
web/app/components/workflow/nodes/_base/hooks/use-node-help-link.ts View File

[BlockEnum.VariableAggregator]: 'variable-aggregator', [BlockEnum.VariableAggregator]: 'variable-aggregator',
[BlockEnum.Assigner]: 'variable-assigner', [BlockEnum.Assigner]: 'variable-assigner',
[BlockEnum.Iteration]: 'iteration', [BlockEnum.Iteration]: 'iteration',
[BlockEnum.IterationStart]: 'iteration',
[BlockEnum.Loop]: 'loop', [BlockEnum.Loop]: 'loop',
[BlockEnum.LoopStart]: 'loop',
[BlockEnum.ParameterExtractor]: 'parameter-extractor', [BlockEnum.ParameterExtractor]: 'parameter-extractor',
[BlockEnum.HttpRequest]: 'http-request', [BlockEnum.HttpRequest]: 'http-request',
[BlockEnum.Tool]: 'tools', [BlockEnum.Tool]: 'tools',
[BlockEnum.VariableAggregator]: 'variable-aggregator', [BlockEnum.VariableAggregator]: 'variable-aggregator',
[BlockEnum.Assigner]: 'variable-assigner', [BlockEnum.Assigner]: 'variable-assigner',
[BlockEnum.Iteration]: 'iteration', [BlockEnum.Iteration]: 'iteration',
[BlockEnum.IterationStart]: 'iteration',
[BlockEnum.Loop]: 'loop', [BlockEnum.Loop]: 'loop',
[BlockEnum.LoopStart]: 'loop',
[BlockEnum.ParameterExtractor]: 'parameter-extractor', [BlockEnum.ParameterExtractor]: 'parameter-extractor',
[BlockEnum.HttpRequest]: 'http-request', [BlockEnum.HttpRequest]: 'http-request',
[BlockEnum.Tool]: 'tools', [BlockEnum.Tool]: 'tools',
[BlockEnum.ListFilter]: 'list-operator', [BlockEnum.ListFilter]: 'list-operator',
[BlockEnum.Agent]: 'agent', [BlockEnum.Agent]: 'agent',
} }
}, [language])
}, [language]) as Record<string, string>

const link = linkMap[nodeType]

if (!link)
return ''


return `${prefixLink}${linkMap[nodeType]}`
return `${prefixLink}${link}`
} }

+ 27
- 7
web/app/components/workflow/nodes/_base/node.tsx View File

import type { import type {
FC, FC,
ReactNode,
ReactElement,
} from 'react' } from 'react'
import { import {
cloneElement, cloneElement,
import Tooltip from '@/app/components/base/tooltip' import Tooltip from '@/app/components/base/tooltip'


type BaseNodeProps = { type BaseNodeProps = {
children: ReactNode
children: ReactElement
} & NodeProps } & NodeProps


const BaseNode: FC<BaseNodeProps> = ({ const BaseNode: FC<BaseNodeProps> = ({
} }
}, [data._runningStatus, showSelectedBorder]) }, [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 ( return (
<div <div
className={cn( className={cn(
) )
} }
{ {
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) && ( (data._runningStatus === NodeRunningStatus.Running || data._singleRunningStatus === NodeRunningStatus.Running) && (

+ 4
- 1
web/app/components/workflow/nodes/assigner/components/var-list/index.tsx View File

}, [onOpen]) }, [onOpen])


const handleFilterToAssignedVar = useCallback((index: number) => { const handleFilterToAssignedVar = useCallback((index: number) => {
return (payload: Var, valueSelector: ValueSelector) => {
return (payload: Var) => {
const item = list[index] const item = list[index]
const assignedVarType = item.variable_selector ? getAssignedVarType?.(item.variable_selector) : undefined 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) if (!filterToAssignedVar || !item.variable_selector || !assignedVarType || !item.operation)
return true return true



+ 0
- 1
web/app/components/workflow/nodes/assigner/node.tsx View File

data, data,
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()

const nodes: Node[] = useNodes() const nodes: Node[] = useNodes()
if (data.version === '2') { if (data.version === '2') {
const { items: operationItems } = data const { items: operationItems } = data

+ 7
- 7
web/app/components/workflow/nodes/assigner/use-config.ts View File

} }


const store = useStoreApi() const store = useStoreApi()
const { getBeforeNodesInSameBranch } = useWorkflow()
const { getBeforeNodesInSameBranchIncludeParent } = useWorkflow()


const { const {
getNodes, getNodes,
const currentNode = getNodes().find(n => n.id === id) const currentNode = getNodes().find(n => n.id === id)
const isInIteration = payload.isInIteration const isInIteration = payload.isInIteration
const iterationNode = isInIteration ? getNodes().find(n => n.id === currentNode!.parentId) : null 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(() => { const availableNodes = useMemo(() => {
return getBeforeNodesInSameBranch(id)
}, [getBeforeNodesInSameBranch, id])
return getBeforeNodesInSameBranchIncludeParent(id)
}, [getBeforeNodesInSameBranchIncludeParent, id])
const { inputs, setInputs } = useNodeCrud<AssignerNodeType>(id, payload) const { inputs, setInputs } = useNodeCrud<AssignerNodeType>(id, payload)
const newSetInputs = useCallback((newInputs: AssignerNodeType) => { const newSetInputs = useCallback((newInputs: AssignerNodeType) => {
const finalInputs = produce(newInputs, (draft) => { const finalInputs = produce(newInputs, (draft) => {
const { getCurrentVariableType } = useWorkflowVariables() const { getCurrentVariableType } = useWorkflowVariables()
const getAssignedVarType = useCallback((valueSelector: ValueSelector) => { const getAssignedVarType = useCallback((valueSelector: ValueSelector) => {
return getCurrentVariableType({ return getCurrentVariableType({
parentNode: isInIteration ? iterationNode : loopNode,
parentNode: isInIteration ? iterationNode : null,
valueSelector: valueSelector || [], valueSelector: valueSelector || [],
availableNodes, availableNodes,
isChatMode, isChatMode,
isConstant: false, isConstant: false,
}) })
}, [getCurrentVariableType, isInIteration, iterationNode, loopNode, availableNodes, isChatMode])
}, [getCurrentVariableType, isInIteration, iterationNode, availableNodes, isChatMode])


const handleOperationListChanges = useCallback((items: AssignerNodeOperation[]) => { const handleOperationListChanges = useCallback((items: AssignerNodeOperation[]) => {
const newInputs = produce(inputs, (draft) => { const newInputs = produce(inputs, (draft) => {
}, []) }, [])


const filterAssignedVar = useCallback((varPayload: Var, selector: ValueSelector) => { const filterAssignedVar = useCallback((varPayload: Var, selector: ValueSelector) => {
if (varPayload.isLoopVariable)
return true
return selector.join('.').startsWith('conversation') return selector.join('.').startsWith('conversation')
}, []) }, [])



+ 5
- 1
web/app/components/workflow/nodes/iteration/use-interactions.ts View File

BlockEnum, BlockEnum,
Node, Node,
} from '../../types' } from '../../types'
import { generateNewNode } from '../../utils'
import {
generateNewNode,
getNodeCustomTypeByNodeDataType,
} from '../../utils'
import { import {
ITERATION_PADDING, ITERATION_PADDING,
NODES_INITIAL_DATA, NODES_INITIAL_DATA,
const childNodeType = child.data.type as BlockEnum const childNodeType = child.data.type as BlockEnum
const nodesWithSameType = nodes.filter(node => node.data.type === childNodeType) const nodesWithSameType = nodes.filter(node => node.data.type === childNodeType)
const { newNode } = generateNewNode({ const { newNode } = generateNewNode({
type: getNodeCustomTypeByNodeDataType(childNodeType),
data: { data: {
...NODES_INITIAL_DATA[childNodeType], ...NODES_INITIAL_DATA[childNodeType],
...child.data, ...child.data,

+ 47
- 0
web/app/components/workflow/nodes/llm/types.ts View File

enabled: boolean enabled: boolean
configs?: VisionSetting 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
} }

+ 23
- 0
web/app/components/workflow/nodes/loop-end/default.ts View File

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

+ 0
- 3
web/app/components/workflow/nodes/loop/components/condition-wrap.tsx View File

)} )}
</div> </div>
</div> </div>
{!isSubVariable && (
<div className='mx-3 my-2 h-[1px] bg-divider-subtle'></div>
)}
</div> </div>
</> </>
) )

+ 13
- 0
web/app/components/workflow/nodes/loop/components/loop-variables/empty.tsx View File

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

+ 144
- 0
web/app/components/workflow/nodes/loop/components/loop-variables/form-item.tsx View File

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

+ 28
- 0
web/app/components/workflow/nodes/loop/components/loop-variables/index.tsx View File

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

+ 37
- 0
web/app/components/workflow/nodes/loop/components/loop-variables/input-mode-selec.tsx View File

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

+ 78
- 0
web/app/components/workflow/nodes/loop/components/loop-variables/item.tsx View File

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

+ 51
- 0
web/app/components/workflow/nodes/loop/components/loop-variables/variable-type-select.tsx View File

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

+ 5
- 0
web/app/components/workflow/nodes/loop/default.ts View File

checkValid(payload: LoopNodeType, t: any) { checkValid(payload: LoopNodeType, t: any) {
let errorMessages = '' let errorMessages = ''


payload.loop_variables?.forEach((variable) => {
if (!variable.label)
errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t(`${i18nPrefix}.fields.variable`) })
})

payload.break_conditions!.forEach((condition) => { payload.break_conditions!.forEach((condition) => {
if (!errorMessages && (!condition.variable_selector || condition.variable_selector.length === 0)) if (!errorMessages && (!condition.variable_selector || condition.variable_selector.length === 0))
errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t(`${i18nPrefix}.fields.variable`) }) errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t(`${i18nPrefix}.fields.variable`) })

+ 27
- 2
web/app/components/workflow/nodes/loop/panel.tsx View File

import type { FC } from 'react' import type { FC } from 'react'
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { RiAddLine } from '@remixicon/react'
import Split from '../_base/components/split' import Split from '../_base/components/split'
import ResultPanel from '../../run/result-panel' import ResultPanel from '../../run/result-panel'
import InputNumberWithSlider from '../_base/components/input-number-with-slider' import InputNumberWithSlider from '../_base/components/input-number-with-slider'
import type { LoopNodeType } from './types' import type { LoopNodeType } from './types'
import useConfig from './use-config' import useConfig from './use-config'
import ConditionWrap from './components/condition-wrap' import ConditionWrap from './components/condition-wrap'
import LoopVariable from './components/loop-variables'
import type { NodePanelProps } from '@/app/components/workflow/types' import type { NodePanelProps } from '@/app/components/workflow/types'
import Field from '@/app/components/workflow/nodes/_base/components/field' import Field from '@/app/components/workflow/nodes/_base/components/field'
import BeforeRunForm from '@/app/components/workflow/nodes/_base/components/before-run-form' import BeforeRunForm from '@/app/components/workflow/nodes/_base/components/before-run-form'
handleUpdateSubVariableCondition, handleUpdateSubVariableCondition,
handleToggleSubVariableConditionLogicalOperator, handleToggleSubVariableConditionLogicalOperator,
handleUpdateLoopCount, handleUpdateLoopCount,
handleAddLoopVariable,
handleRemoveLoopVariable,
handleUpdateLoopVariable,
} = useConfig(id, data) } = useConfig(id, data)


const nodeInfo = formatTracing(loopRunResult, t)[0] const nodeInfo = formatTracing(loopRunResult, t)[0]
return ( return (
<div className='mt-2'> <div className='mt-2'>
<div> <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 <Field
title={<div className='pl-3'>{t(`${i18nPrefix}.breakCondition`)}</div>} title={<div className='pl-3'>{t(`${i18nPrefix}.breakCondition`)}</div>}
tooltip={t(`${i18nPrefix}.breakConditionTip`)} tooltip={t(`${i18nPrefix}.breakConditionTip`)}
logicalOperator={inputs.logical_operator!} logicalOperator={inputs.logical_operator!}
/> />
</Field> </Field>
<Split />
<Split className='mt-2' />
<div className='mt-2'> <div className='mt-2'>
<Field <Field
title={<div className='pl-3'>{t(`${i18nPrefix}.loopMaxCount`)}</div>} title={<div className='pl-3'>{t(`${i18nPrefix}.loopMaxCount`)}</div>}

+ 18
- 0
web/app/components/workflow/nodes/loop/types.ts View File

CommonNodeType, CommonNodeType,
ErrorHandleMode, ErrorHandleMode,
ValueSelector, ValueSelector,
ValueType,
Var, Var,
VarType, VarType,
} from '@/app/components/workflow/types' } from '@/app/components/workflow/types'
export type HandleUpdateSubVariableCondition = (conditionId: string, subConditionId: string, newSubCondition: Condition) => void export type HandleUpdateSubVariableCondition = (conditionId: string, subConditionId: string, newSubCondition: Condition) => void
export type HandleToggleSubVariableConditionLogicalOperator = (conditionId: string) => 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 & { export type LoopNodeType = CommonNodeType & {
startNodeType?: BlockEnum startNodeType?: BlockEnum
start_node_id: string start_node_id: string
break_conditions?: Condition[] break_conditions?: Condition[]
loop_count: number loop_count: number
error_handle_mode: ErrorHandleMode // how to handle error in the iteration 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
} }

+ 52
- 4
web/app/components/workflow/nodes/loop/use-config.ts View File

import { useCallback } from 'react'
import {
useCallback,
useRef,
} from 'react'
import produce from 'immer' import produce from 'immer'
import { useBoolean } from 'ahooks' import { useBoolean } from 'ahooks'
import { uuid4 } from '@sentry/utils'
import { v4 as uuid4 } from 'uuid'
import { import {
useIsChatMode, useIsChatMode,
useIsNodeInLoop, useIsNodeInLoop,
useNodesReadOnly, useNodesReadOnly,
useWorkflow, useWorkflow,
} from '../../hooks' } from '../../hooks'
import { VarType } from '../../types'
import { ValueType, VarType } from '../../types'
import type { ErrorHandleMode, ValueSelector, Var } from '../../types' import type { ErrorHandleMode, ValueSelector, Var } from '../../types'
import useNodeCrud from '../_base/hooks/use-node-crud' import useNodeCrud from '../_base/hooks/use-node-crud'
import { getNodeInfoById, getNodeUsedVarPassToServerKey, getNodeUsedVars, isSystemVar, toNodeOutputVars } from '../_base/components/variable/utils' import { getNodeInfoById, getNodeUsedVarPassToServerKey, getNodeUsedVars, isSystemVar, toNodeOutputVars } from '../_base/components/variable/utils'
const conversationVariables = useStore(s => s.conversationVariables) const conversationVariables = useStore(s => s.conversationVariables)


const { inputs, setInputs } = useNodeCrud<LoopNodeType>(id, payload) 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) => { const filterInputVar = useCallback((varPayload: Var) => {
return [VarType.array, VarType.arrayString, VarType.arrayNumber, VarType.arrayObject, VarType.arrayFile].includes(varPayload.type) return [VarType.array, VarType.arrayString, VarType.arrayNumber, VarType.arrayObject, VarType.arrayFile].includes(varPayload.type)
// output // output
const { getLoopNodeChildren, getBeforeNodesInSameBranch } = useWorkflow() const { getLoopNodeChildren, getBeforeNodesInSameBranch } = useWorkflow()
const beforeNodes = getBeforeNodesInSameBranch(id) const beforeNodes = getBeforeNodesInSameBranch(id)
const loopChildrenNodes = getLoopNodeChildren(id)
const loopChildrenNodes = [{ id, data: payload } as any, ...getLoopNodeChildren(id)]
const canChooseVarNodes = [...beforeNodes, ...loopChildrenNodes] const canChooseVarNodes = [...beforeNodes, ...loopChildrenNodes]
const childrenNodeVars = toNodeOutputVars(loopChildrenNodes, isChatMode, undefined, [], conversationVariables) const childrenNodeVars = toNodeOutputVars(loopChildrenNodes, isChatMode, undefined, [], conversationVariables)


setInputs(newInputs) setInputs(newInputs)
}, [inputs, setInputs]) }, [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 { return {
readOnly, readOnly,
inputs, inputs,
handleToggleSubVariableConditionLogicalOperator, handleToggleSubVariableConditionLogicalOperator,
handleUpdateLoopCount, handleUpdateLoopCount,
changeErrorResponseMode, changeErrorResponseMode,
handleAddLoopVariable,
handleRemoveLoopVariable,
handleUpdateLoopVariable,
} }
} }



+ 5
- 2
web/app/components/workflow/nodes/loop/use-interactions.ts View File

BlockEnum, BlockEnum,
Node, Node,
} from '../../types' } from '../../types'
import { generateNewNode } from '../../utils'
import {
generateNewNode,
getNodeCustomTypeByNodeDataType,
} from '../../utils'
import { import {
LOOP_PADDING, LOOP_PADDING,
NODES_INITIAL_DATA, NODES_INITIAL_DATA,
const childNodeType = child.data.type as BlockEnum const childNodeType = child.data.type as BlockEnum
const nodesWithSameType = nodes.filter(node => node.data.type === childNodeType) const nodesWithSameType = nodes.filter(node => node.data.type === childNodeType)
const { newNode } = generateNewNode({ const { newNode } = generateNewNode({
type: getNodeCustomTypeByNodeDataType(childNodeType),
data: { data: {
...NODES_INITIAL_DATA[childNodeType], ...NODES_INITIAL_DATA[childNodeType],
...child.data, ...child.data,

+ 65
- 26
web/app/components/workflow/nodes/variable-assigner/components/node-variable-item.tsx View File

import { memo } from 'react'
import {
memo,
useMemo,
} from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import { VarBlockIcon } from '@/app/components/workflow/block-icon' import { VarBlockIcon } from '@/app/components/workflow/block-icon'
import { BubbleX, Env } from '@/app/components/base/icons/src/vender/line/others' import { BubbleX, Env } from '@/app/components/base/icons/src/vender/line/others'
import Badge from '@/app/components/base/badge' import Badge from '@/app/components/base/badge'
import type { Node } from '@/app/components/workflow/types' import type { Node } from '@/app/components/workflow/types'
import { BlockEnum } from '@/app/components/workflow/types'


type NodeVariableItemProps = { type NodeVariableItemProps = {
isEnv: boolean isEnv: boolean
isException, isException,
}: NodeVariableItemProps) => { }: NodeVariableItemProps) => {
const { t } = useTranslation() 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 ( return (
<div className={cn( <div className={cn(
'relative flex items-center gap-1 self-stretch rounded-md bg-workflow-block-parma-bg p-[3px] pl-[5px]', 'relative flex items-center gap-1 self-stretch rounded-md bg-workflow-block-parma-bg p-[3px] pl-[5px]',
showBorder && '!bg-black/[0.02]', showBorder && '!bg-black/[0.02]',
className, 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> </div>
{writeMode && <Badge className='shrink-0' text={t(`${i18nPrefix}.operations.${writeMode}`)} />}
</div> </div>
) )
} }

+ 2
- 0
web/app/components/workflow/operator/add-block.tsx View File

import type { OffsetOptions } from '@floating-ui/react' import type { OffsetOptions } from '@floating-ui/react'
import { import {
generateNewNode, generateNewNode,
getNodeCustomTypeByNodeDataType,
} from '../utils' } from '../utils'
import { import {
useAvailableBlocks, useAvailableBlocks,
const nodes = getNodes() const nodes = getNodes()
const nodesWithSameType = nodes.filter(node => node.data.type === type) const nodesWithSameType = nodes.filter(node => node.data.type === type)
const { newNode } = generateNewNode({ const { newNode } = generateNewNode({
type: getNodeCustomTypeByNodeDataType(type),
data: { data: {
...NODES_INITIAL_DATA[type], ...NODES_INITIAL_DATA[type],
title: nodesWithSameType.length > 0 ? `${t(`workflow.blocks.${type}`)} ${nodesWithSameType.length + 1}` : t(`workflow.blocks.${type}`), title: nodesWithSameType.length > 0 ? `${t(`workflow.blocks.${type}`)} ${nodesWithSameType.length + 1}` : t(`workflow.blocks.${type}`),

+ 6
- 1
web/app/components/workflow/run/hooks.ts View File

AgentLogItemWithChildren, AgentLogItemWithChildren,
IterationDurationMap, IterationDurationMap,
LoopDurationMap, LoopDurationMap,
LoopVariableMap,
NodeTracing, NodeTracing,
} from '@/types/workflow' } from '@/types/workflow'


}] = useBoolean(false) }] = useBoolean(false)
const [loopResultList, setLoopResultList] = useState<NodeTracing[][]>([]) const [loopResultList, setLoopResultList] = useState<NodeTracing[][]>([])
const [loopResultDurationMap, setLoopResultDurationMap] = useState<LoopDurationMap>({}) const [loopResultDurationMap, setLoopResultDurationMap] = useState<LoopDurationMap>({})
const handleShowLoopResultList = useCallback((detail: NodeTracing[][], loopDurationMap: LoopDurationMap) => {
const [loopResultVariableMap, setLoopResultVariableMap] = useState<Record<string, any>>({})
const handleShowLoopResultList = useCallback((detail: NodeTracing[][], loopDurationMap: LoopDurationMap, loopVariableMap: LoopVariableMap) => {
setShowLoopingDetailTrue() setShowLoopingDetailTrue()
setLoopResultList(detail) setLoopResultList(detail)
setLoopResultDurationMap(loopDurationMap) setLoopResultDurationMap(loopDurationMap)
setLoopResultVariableMap(loopVariableMap)
}, [setShowLoopingDetailTrue, setLoopResultList, setLoopResultDurationMap]) }, [setShowLoopingDetailTrue, setLoopResultList, setLoopResultDurationMap])


const [agentOrToolLogItemStack, setAgentOrToolLogItemStack] = useState<AgentLogItemWithChildren[]>([]) const [agentOrToolLogItemStack, setAgentOrToolLogItemStack] = useState<AgentLogItemWithChildren[]>([])
setLoopResultList, setLoopResultList,
loopResultDurationMap, loopResultDurationMap,
setLoopResultDurationMap, setLoopResultDurationMap,
loopResultVariableMap,
setLoopResultVariableMap,
handleShowLoopResultList, handleShowLoopResultList,


agentOrToolLogItemStack, agentOrToolLogItemStack,

+ 7
- 2
web/app/components/workflow/run/loop-log/loop-log-trigger.tsx View File

import Button from '@/app/components/base/button' import Button from '@/app/components/base/button'
import type { import type {
LoopDurationMap, LoopDurationMap,
LoopVariableMap,
NodeTracing, NodeTracing,
} from '@/types/workflow' } from '@/types/workflow'
import { Loop } from '@/app/components/base/icons/src/vender/workflow' import { Loop } from '@/app/components/base/icons/src/vender/workflow'


type LoopLogTriggerProps = { type LoopLogTriggerProps = {
nodeInfo: NodeTracing nodeInfo: NodeTracing
onShowLoopResultList: (loopResultList: NodeTracing[][], loopResultDurationMap: LoopDurationMap) => void
onShowLoopResultList: (loopResultList: NodeTracing[][], loopResultDurationMap: LoopDurationMap, loopVariableMap: LoopVariableMap) => void
} }
const LoopLogTrigger = ({ const LoopLogTrigger = ({
nodeInfo, nodeInfo,
const handleOnShowLoopDetail = (e: React.MouseEvent<HTMLButtonElement>) => { const handleOnShowLoopDetail = (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation() e.stopPropagation()
e.nativeEvent.stopImmediatePropagation() e.nativeEvent.stopImmediatePropagation()
onShowLoopResultList(nodeInfo.details || [], nodeInfo?.loopDurationMap || nodeInfo.execution_metadata?.loop_duration_map || {})
onShowLoopResultList(
nodeInfo.details || [],
nodeInfo?.loopDurationMap || nodeInfo.execution_metadata?.loop_duration_map || {},
nodeInfo.execution_metadata?.loop_variable_map || {},
)
} }
return ( return (
<Button <Button

+ 19
- 1
web/app/components/workflow/run/loop-log/loop-result-panel.tsx View File

import TracingPanel from '@/app/components/workflow/run/tracing-panel' import TracingPanel from '@/app/components/workflow/run/tracing-panel'
import { Loop } from '@/app/components/base/icons/src/vender/workflow' import { Loop } from '@/app/components/base/icons/src/vender/workflow'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import type { LoopDurationMap, NodeTracing } from '@/types/workflow'
import type { LoopDurationMap, LoopVariableMap, NodeTracing } from '@/types/workflow'
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
const i18nPrefix = 'workflow.singleRun' const i18nPrefix = 'workflow.singleRun'


type Props = { type Props = {
list: NodeTracing[][] list: NodeTracing[][]
onBack: () => void onBack: () => void
loopDurationMap?: LoopDurationMap loopDurationMap?: LoopDurationMap
loopVariableMap?: LoopVariableMap
} }


const LoopResultPanel: FC<Props> = ({ const LoopResultPanel: FC<Props> = ({
list, list,
onBack, onBack,
loopDurationMap, loopDurationMap,
loopVariableMap,
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
const [expandedLoops, setExpandedLoops] = useState<Record<number, boolean>>({}) const [expandedLoops, setExpandedLoops] = useState<Record<number, boolean>>({})
'overflow-hidden transition-all duration-200', 'overflow-hidden transition-all duration-200',
expandedLoops[index] ? 'max-h-[1000px] opacity-100' : 'max-h-0 opacity-0', expandedLoops[index] ? 'max-h-[1000px] opacity-100' : 'max-h-0 opacity-0',
)}> )}>
{
loopVariableMap?.[index] && (
<div className='p-2 pb-0'>
<CodeEditor
readOnly
title={<div>{t('workflow.nodes.loop.loopVariables').toLocaleUpperCase()}</div>}
language={CodeLanguage.json}
height={112}
value={loopVariableMap[index]}
isJSONStringifyBeauty
/>
</div>
)
}
<TracingPanel <TracingPanel
list={loop} list={loop}
className='bg-background-section-burn' className='bg-background-section-burn'

+ 20
- 5
web/app/components/workflow/run/node.tsx View File

'use client' 'use client'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import type { FC } from 'react' import type { FC } from 'react'
import { useCallback, useEffect, useState } from 'react'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { import {
RiAlertFill, RiAlertFill,
RiArrowRightSLine, RiArrowRightSLine,
AgentLogItemWithChildren, AgentLogItemWithChildren,
IterationDurationMap, IterationDurationMap,
LoopDurationMap, LoopDurationMap,
LoopVariableMap,
NodeTracing, NodeTracing,
} from '@/types/workflow' } from '@/types/workflow'
import ErrorHandleTip from '@/app/components/workflow/nodes/_base/components/error-handle/error-handle-tip' import ErrorHandleTip from '@/app/components/workflow/nodes/_base/components/error-handle/error-handle-tip'
hideInfo?: boolean hideInfo?: boolean
hideProcessDetail?: boolean hideProcessDetail?: boolean
onShowIterationDetail?: (detail: NodeTracing[][], iterDurationMap: IterationDurationMap) => void onShowIterationDetail?: (detail: NodeTracing[][], iterDurationMap: IterationDurationMap) => void
onShowLoopDetail?: (detail: NodeTracing[][], loopDurationMap: LoopDurationMap) => void
onShowLoopDetail?: (detail: NodeTracing[][], loopDurationMap: LoopDurationMap, loopVariableMap: LoopVariableMap) => void
onShowRetryDetail?: (detail: NodeTracing[]) => void onShowRetryDetail?: (detail: NodeTracing[]) => void
onShowAgentOrToolLog?: (detail?: AgentLogItemWithChildren) => void onShowAgentOrToolLog?: (detail?: AgentLogItemWithChildren) => void
notShowIterationNav?: boolean notShowIterationNav?: boolean
const isAgentNode = nodeInfo.node_type === BlockEnum.Agent && !!nodeInfo.agentLog?.length const isAgentNode = nodeInfo.node_type === BlockEnum.Agent && !!nodeInfo.agentLog?.length
const isToolNode = nodeInfo.node_type === BlockEnum.Tool && !!nodeInfo.agentLog?.length const isToolNode = nodeInfo.node_type === BlockEnum.Tool && !!nodeInfo.agentLog?.length


const inputsTitle = useMemo(() => {
let text = t('workflow.common.input')
if (nodeInfo.node_type === BlockEnum.Loop)
text = t('workflow.nodes.loop.initialLoopVariables')
return text.toLocaleUpperCase()
}, [nodeInfo.node_type, t])
const processDataTitle = t('workflow.common.processData').toLocaleUpperCase()
const outputTitle = useMemo(() => {
let text = t('workflow.common.output')
if (nodeInfo.node_type === BlockEnum.Loop)
text = t('workflow.nodes.loop.finalLoopVariables')
return text.toLocaleUpperCase()
}, [nodeInfo.node_type, t])

return ( return (
<div className={cn('px-2 py-1', className)}> <div className={cn('px-2 py-1', className)}>
<div className='group rounded-[10px] border border-components-panel-border bg-background-default shadow-xs transition-all hover:shadow-md'> <div className='group rounded-[10px] border border-components-panel-border bg-background-default shadow-xs transition-all hover:shadow-md'>
<div className={cn('mb-1')}> <div className={cn('mb-1')}>
<CodeEditor <CodeEditor
readOnly readOnly
title={<div>{t('workflow.common.input').toLocaleUpperCase()}</div>}
title={<div>{inputsTitle}</div>}
language={CodeLanguage.json} language={CodeLanguage.json}
value={nodeInfo.inputs} value={nodeInfo.inputs}
isJSONStringifyBeauty isJSONStringifyBeauty
<div className={cn('mb-1')}> <div className={cn('mb-1')}>
<CodeEditor <CodeEditor
readOnly readOnly
title={<div>{t('workflow.common.processData').toLocaleUpperCase()}</div>}
title={<div>{processDataTitle}</div>}
language={CodeLanguage.json} language={CodeLanguage.json}
value={nodeInfo.process_data} value={nodeInfo.process_data}
isJSONStringifyBeauty isJSONStringifyBeauty
<div> <div>
<CodeEditor <CodeEditor
readOnly readOnly
title={<div>{t('workflow.common.output').toLocaleUpperCase()}</div>}
title={<div>{outputTitle}</div>}
language={CodeLanguage.json} language={CodeLanguage.json}
value={nodeInfo.outputs} value={nodeInfo.outputs}
isJSONStringifyBeauty isJSONStringifyBeauty

+ 4
- 0
web/app/components/workflow/run/special-result-panel.tsx View File

AgentLogItemWithChildren, AgentLogItemWithChildren,
IterationDurationMap, IterationDurationMap,
LoopDurationMap, LoopDurationMap,
LoopVariableMap,
NodeTracing, NodeTracing,
} from '@/types/workflow' } from '@/types/workflow'


setShowLoopingDetailFalse?: () => void setShowLoopingDetailFalse?: () => void
loopResultList?: NodeTracing[][] loopResultList?: NodeTracing[][]
loopResultDurationMap?: LoopDurationMap loopResultDurationMap?: LoopDurationMap
loopResultVariableMap?: LoopVariableMap


agentOrToolLogItemStack?: AgentLogItemWithChildren[] agentOrToolLogItemStack?: AgentLogItemWithChildren[]
agentOrToolLogListMap?: Record<string, AgentLogItemWithChildren[]> agentOrToolLogListMap?: Record<string, AgentLogItemWithChildren[]>
setShowLoopingDetailFalse, setShowLoopingDetailFalse,
loopResultList, loopResultList,
loopResultDurationMap, loopResultDurationMap,
loopResultVariableMap,


agentOrToolLogItemStack, agentOrToolLogItemStack,
agentOrToolLogListMap, agentOrToolLogListMap,
list={loopResultList} list={loopResultList}
onBack={setShowLoopingDetailFalse} onBack={setShowLoopingDetailFalse}
loopDurationMap={loopResultDurationMap} loopDurationMap={loopResultDurationMap}
loopVariableMap={loopResultVariableMap}
/> />
) )
} }

+ 2
- 0
web/app/components/workflow/run/tracing-panel.tsx View File

setShowLoopingDetailFalse, setShowLoopingDetailFalse,
loopResultList, loopResultList,
loopResultDurationMap, loopResultDurationMap,
loopResultVariableMap,
handleShowLoopResultList, handleShowLoopResultList,


agentOrToolLogItemStack, agentOrToolLogItemStack,
setShowLoopingDetailFalse={setShowLoopingDetailFalse} setShowLoopingDetailFalse={setShowLoopingDetailFalse}
loopResultList={loopResultList} loopResultList={loopResultList}
loopResultDurationMap={loopResultDurationMap} loopResultDurationMap={loopResultDurationMap}
loopResultVariableMap={loopResultVariableMap}


agentOrToolLogItemStack={agentOrToolLogItemStack} agentOrToolLogItemStack={agentOrToolLogItemStack}
agentOrToolLogListMap={agentOrToolLogListMap} agentOrToolLogListMap={agentOrToolLogListMap}

+ 1
- 0
web/app/components/workflow/simple-node/constants.ts View File

export const CUSTOM_SIMPLE_NODE = 'custom-simple'

+ 148
- 0
web/app/components/workflow/simple-node/index.tsx View File

import type {
FC,
} from 'react'
import {
memo,
useMemo,
} from 'react'
import {
RiAlertFill,
RiCheckboxCircleFill,
RiErrorWarningFill,
RiLoader2Line,
} from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import {
NodeTargetHandle,
} from '@/app/components/workflow/nodes/_base/components/node-handle'
import NodeControl from '@/app/components/workflow/nodes/_base/components/node-control'
import cn from '@/utils/classnames'
import BlockIcon from '@/app/components/workflow/block-icon'
import type {
NodeProps,
} from '@/app/components/workflow/types'
import {
NodeRunningStatus,
} from '@/app/components/workflow/types'
import {
useNodesReadOnly,
} from '@/app/components/workflow/hooks'

type SimpleNodeProps = NodeProps

const SimpleNode: FC<SimpleNodeProps> = ({
id,
data,
}) => {
const { t } = useTranslation()
const { nodesReadOnly } = useNodesReadOnly()

const showSelectedBorder = data.selected || data._isBundled || data._isEntering
const {
showRunningBorder,
showSuccessBorder,
showFailedBorder,
showExceptionBorder,
} = useMemo(() => {
return {
showRunningBorder: data._runningStatus === NodeRunningStatus.Running && !showSelectedBorder,
showSuccessBorder: data._runningStatus === NodeRunningStatus.Succeeded && !showSelectedBorder,
showFailedBorder: data._runningStatus === NodeRunningStatus.Failed && !showSelectedBorder,
showExceptionBorder: data._runningStatus === NodeRunningStatus.Exception && !showSelectedBorder,
}
}, [data._runningStatus, showSelectedBorder])

return (
<div
className={cn(
'flex rounded-2xl border-[2px]',
showSelectedBorder ? 'border-components-option-card-option-selected-border' : 'border-transparent',
!showSelectedBorder && data._inParallelHovering && 'border-workflow-block-border-highlight',
data._waitingRun && 'opacity-70',
)}
style={{
width: 'auto',
height: 'auto',
}}
>
<div
className={cn(
'group relative pb-1 shadow-xs',
'rounded-[15px] border border-transparent',
'w-[240px] bg-workflow-block-bg',
!data._runningStatus && 'hover:shadow-lg',
showRunningBorder && '!border-state-accent-solid',
showSuccessBorder && '!border-state-success-solid',
showFailedBorder && '!border-state-destructive-solid',
showExceptionBorder && '!border-state-warning-solid',
data._isBundled && '!shadow-lg',
)}
>
{
data._inParallelHovering && (
<div className='top system-2xs-medium-uppercase absolute -top-2.5 left-2 z-10 text-text-tertiary'>
{t('workflow.common.parallelRun')}
</div>
)
}
{
!data._isCandidate && (
<NodeTargetHandle
id={id}
data={data}
handleClassName='!top-4 !-left-[9px] !translate-y-0'
handleId='target'
/>
)
}
{
!data._runningStatus && !nodesReadOnly && !data._isCandidate && (
<NodeControl
id={id}
data={data}
/>
)
}
<div className={cn(
'flex items-center rounded-t-2xl px-3 pb-2 pt-3',
)}>
<BlockIcon
className='mr-2 shrink-0'
type={data.type}
size='md'
/>
<div
title={data.title}
className='system-sm-semibold-uppercase mr-1 flex grow items-center truncate text-text-primary'
>
<div>
{data.title}
</div>
</div>
{
(data._runningStatus === NodeRunningStatus.Running || data._singleRunningStatus === NodeRunningStatus.Running) && (
<RiLoader2Line className='h-3.5 w-3.5 animate-spin text-text-accent' />
)
}
{
data._runningStatus === NodeRunningStatus.Succeeded && (
<RiCheckboxCircleFill className='h-3.5 w-3.5 text-text-success' />
)
}
{
data._runningStatus === NodeRunningStatus.Failed && (
<RiErrorWarningFill className='h-3.5 w-3.5 text-text-destructive' />
)
}
{
data._runningStatus === NodeRunningStatus.Exception && (
<RiAlertFill className='h-3.5 w-3.5 text-text-warning-secondary' />
)
}
</div>
</div>
</div>
)
}

export default memo(SimpleNode)

+ 3
- 0
web/app/components/workflow/simple-node/types.ts View File

import type { CommonNodeType } from '@/app/components/workflow/types'

export type SimpleNodeType = CommonNodeType

+ 12
- 2
web/app/components/workflow/types.ts View File

ErrorHandleTypeEnum, ErrorHandleTypeEnum,
} from '@/app/components/workflow/nodes/_base/components/error-handle/types' } from '@/app/components/workflow/nodes/_base/components/error-handle/types'
import type { WorkflowRetryConfig } from '@/app/components/workflow/nodes/_base/components/retry/types' import type { WorkflowRetryConfig } from '@/app/components/workflow/nodes/_base/components/retry/types'
import type { StructuredOutput } from '@/app/components/workflow/nodes/llm/types'


export enum BlockEnum { export enum BlockEnum {
Start = 'start', Start = 'start',
Agent = 'agent', Agent = 'agent',
Loop = 'loop', Loop = 'loop',
LoopStart = 'loop-start', LoopStart = 'loop-start',
LoopEnd = 'loop-end',
} }


export enum ControlMode { export enum ControlMode {
_singleRunningStatus?: NodeRunningStatus _singleRunningStatus?: NodeRunningStatus
_isCandidate?: boolean _isCandidate?: boolean
_isBundled?: boolean _isBundled?: boolean
_children?: string[]
_children?: { nodeId: string; nodeType: BlockEnum }[]
_isEntering?: boolean _isEntering?: boolean
_showAddVariablePopup?: boolean _showAddVariablePopup?: boolean
_holdAddVariablePopup?: boolean _holdAddVariablePopup?: boolean
any = 'any', any = 'any',
} }


export enum ValueType {
variable = 'variable',
constant = 'constant',
}

export type Var = { export type Var = {
variable: string variable: string
type: VarType type: VarType
children?: Var[] // if type is obj, has the children struct
children?: Var[] | StructuredOutput // if type is obj, has the children struct
isParagraph?: boolean isParagraph?: boolean
isSelect?: boolean isSelect?: boolean
options?: string[] options?: string[]
required?: boolean required?: boolean
des?: string des?: string
isException?: boolean isException?: boolean
isLoopVariable?: boolean
nodeId?: string
} }


export type NodeOutPutVar = { export type NodeOutPutVar = {
title: string title: string
vars: Var[] vars: Var[]
isStartNode?: boolean isStartNode?: boolean
isLoop?: boolean
} }


export type Block = { export type Block = {

+ 11
- 5
web/app/components/workflow/utils.ts View File

import { CollectionType } from '@/app/components/tools/types' import { CollectionType } from '@/app/components/tools/types'
import { toolParametersToFormSchemas } from '@/app/components/tools/utils/to-form-schema' import { toolParametersToFormSchemas } from '@/app/components/tools/utils/to-form-schema'
import { canFindTool, correctModelProvider } from '@/utils' import { canFindTool, correctModelProvider } from '@/utils'
import { CUSTOM_SIMPLE_NODE } from '@/app/components/workflow/simple-node/constants'


const WHITE = 'WHITE' const WHITE = 'WHITE'
const GRAY = 'GRAY' const GRAY = 'GRAY'
if (data.type === BlockEnum.Iteration) { if (data.type === BlockEnum.Iteration) {
const newIterationStartNode = getIterationStartNode(newNode.id); const newIterationStartNode = getIterationStartNode(newNode.id);
(newNode.data as IterationNodeType).start_node_id = newIterationStartNode.id; (newNode.data as IterationNodeType).start_node_id = newIterationStartNode.id;
(newNode.data as IterationNodeType)._children = [newIterationStartNode.id]
(newNode.data as IterationNodeType)._children = [{ nodeId: newIterationStartNode.id, nodeType: BlockEnum.IterationStart }]
return { return {
newNode, newNode,
newIterationStartNode, newIterationStartNode,
if (data.type === BlockEnum.Loop) { if (data.type === BlockEnum.Loop) {
const newLoopStartNode = getLoopStartNode(newNode.id); const newLoopStartNode = getLoopStartNode(newNode.id);
(newNode.data as LoopNodeType).start_node_id = newLoopStartNode.id; (newNode.data as LoopNodeType).start_node_id = newLoopStartNode.id;
(newNode.data as LoopNodeType)._children = [newLoopStartNode.id]
(newNode.data as LoopNodeType)._children = [{ nodeId: newLoopStartNode.id, nodeType: BlockEnum.LoopStart }]
return { return {
newNode, newNode,
newLoopStartNode, newLoopStartNode,
const iterationOrLoopNodeMap = nodes.reduce((acc, node) => { const iterationOrLoopNodeMap = nodes.reduce((acc, node) => {
if (node.parentId) { if (node.parentId) {
if (acc[node.parentId]) if (acc[node.parentId])
acc[node.parentId].push(node.id)
acc[node.parentId].push({ nodeId: node.id, nodeType: node.data.type })
else else
acc[node.parentId] = [node.id]
acc[node.parentId] = [{ nodeId: node.id, nodeType: node.data.type }]
} }
return acc return acc
}, {} as Record<string, string[]>)
}, {} as Record<string, { nodeId: string; nodeType: BlockEnum }[]>)


return nodes.map((node) => { return nodes.map((node) => {
if (!node.type) if (!node.type)
export const hasRetryNode = (nodeType?: BlockEnum) => { export const hasRetryNode = (nodeType?: BlockEnum) => {
return nodeType === BlockEnum.LLM || nodeType === BlockEnum.Tool || nodeType === BlockEnum.HttpRequest || nodeType === BlockEnum.Code return nodeType === BlockEnum.LLM || nodeType === BlockEnum.Tool || nodeType === BlockEnum.HttpRequest || nodeType === BlockEnum.Code
} }

export const getNodeCustomTypeByNodeDataType = (nodeType: BlockEnum) => {
if (nodeType === BlockEnum.LoopEnd)
return CUSTOM_SIMPLE_NODE
}

+ 12
- 0
web/i18n/en-US/workflow.ts View File

'agent': 'Agent', 'agent': 'Agent',
'loop-start': 'Loop Start', 'loop-start': 'Loop Start',
'loop': 'Loop', 'loop': 'Loop',
'loop-end': 'Exit Loop',
}, },
blocksAbout: { blocksAbout: {
'start': 'Define the initial parameters for launching a workflow', 'start': 'Define the initial parameters for launching a workflow',
'variable-aggregator': 'Aggregate multi-branch variables into a single variable for unified configuration of downstream nodes.', 'variable-aggregator': 'Aggregate multi-branch variables into a single variable for unified configuration of downstream nodes.',
'iteration': 'Perform multiple steps on a list object until all results are outputted.', 'iteration': 'Perform multiple steps on a list object until all results are outputted.',
'loop': 'Execute a loop of logic until the termination condition is met or the maximum loop count is reached.', 'loop': 'Execute a loop of logic until the termination condition is met or the maximum loop count is reached.',
'loop-end': 'Equivalent to "break". This node has no configuration items. When the loop body reaches this node, the loop terminates.',
'parameter-extractor': 'Use LLM to extract structured parameters from natural language for tool invocations or HTTP requests.', 'parameter-extractor': 'Use LLM to extract structured parameters from natural language for tool invocations or HTTP requests.',
'document-extractor': 'Used to parse uploaded documents into text content that is easily understandable by LLM.', 'document-extractor': 'Used to parse uploaded documents into text content that is easily understandable by LLM.',
'list-operator': 'Used to filter or sort array content.', 'list-operator': 'Used to filter or sort array content.',
continueOnError: 'Continue on Error', continueOnError: 'Continue on Error',
removeAbnormalOutput: 'Remove Abnormal Output', removeAbnormalOutput: 'Remove Abnormal Output',
}, },
loopVariables: 'Loop Variables',
initialLoopVariables: 'Initial Loop Variables',
finalLoopVariables: 'Final Loop Variables',
setLoopVariables: 'Set variables within the loop scope',
variableName: 'Variable Name',
inputMode: 'Input Mode',
exitConditionTip: 'A loop node needs at least one exit condition',
loopNode: 'Loop Node',
currentLoopCount: 'Current loop count: {{count}}',
totalLoopCount: 'Total loop count: {{count}}',
}, },
note: { note: {
addNote: 'Add Note', addNote: 'Add Note',

+ 12
- 0
web/i18n/zh-Hans/workflow.ts View File

'agent': 'Agent', 'agent': 'Agent',
'loop-start': '循环开始', 'loop-start': '循环开始',
'loop': '循环', 'loop': '循环',
'loop-end': '退出循环',
}, },
blocksAbout: { blocksAbout: {
'start': '定义一个 workflow 流程启动的初始参数', 'start': '定义一个 workflow 流程启动的初始参数',
'variable-aggregator': '将多路分支的变量聚合为一个变量,以实现下游节点统一配置。', 'variable-aggregator': '将多路分支的变量聚合为一个变量,以实现下游节点统一配置。',
'iteration': '对列表对象执行多次步骤直至输出所有结果。', 'iteration': '对列表对象执行多次步骤直至输出所有结果。',
'loop': '循环执行一段逻辑直到满足结束条件或者到达循环次数上限。', 'loop': '循环执行一段逻辑直到满足结束条件或者到达循环次数上限。',
'loop-end': '相当于“break” 此节点没有配置项,当循环体内运行到此节点后循环终止。',
'parameter-extractor': '利用 LLM 从自然语言内推理提取出结构化参数,用于后置的工具调用或 HTTP 请求。', 'parameter-extractor': '利用 LLM 从自然语言内推理提取出结构化参数,用于后置的工具调用或 HTTP 请求。',
'document-extractor': '用于将用户上传的文档解析为 LLM 便于理解的文本内容。', 'document-extractor': '用于将用户上传的文档解析为 LLM 便于理解的文本内容。',
'list-operator': '用于过滤或排序数组内容。', 'list-operator': '用于过滤或排序数组内容。',
continueOnError: '忽略错误并继续', continueOnError: '忽略错误并继续',
removeAbnormalOutput: '移除错误输出', removeAbnormalOutput: '移除错误输出',
}, },
loopVariables: '循环变量',
initialLoopVariables: '初始循环变量',
finalLoopVariables: '最终循环变量',
setLoopVariables: '在循环范围内设置变量',
variableName: '变量名',
inputMode: '输入模式',
exitConditionTip: '循环节点至少需要一个退出条件',
loopNode: '循环节点',
currentLoopCount: '当前循环次数:{{count}}',
totalLoopCount: '总循环次数:{{count}}',
}, },
note: { note: {
addNote: '添加注释', addNote: '添加注释',

+ 2
- 0
web/types/workflow.ts View File

agent_strategy?: string agent_strategy?: string
icon?: string icon?: string
} }
loop_variable_map?: Record<string, any>
} }
metadata: { metadata: {
iterator_length: number iterator_length: number


export type IterationDurationMap = Record<string, number> export type IterationDurationMap = Record<string, number>
export type LoopDurationMap = Record<string, number> export type LoopDurationMap = Record<string, number>
export type LoopVariableMap = Record<string, any>


export type WorkflowConfigResponse = { export type WorkflowConfigResponse = {
parallel_depth_limit: number parallel_depth_limit: number

Loading…
Cancel
Save