mirror of
https://git.mirrors.martin98.com/https://github.com/langgenius/dify.git
synced 2025-08-12 02:29:03 +08:00
Feat/loop break node (#17268)
This commit is contained in:
parent
627a9e2ce1
commit
713902dc47
@ -144,7 +144,7 @@ export default function AccountPage() {
|
||||
{userProfile.name}
|
||||
{isEducationAccount && (
|
||||
<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>
|
||||
</PremiumBadge>
|
||||
)}
|
||||
|
@ -78,7 +78,7 @@ export default function AppSelector() {
|
||||
{userProfile.name}
|
||||
{isEducationAccount && (
|
||||
<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>
|
||||
</PremiumBadge>
|
||||
)}
|
||||
|
@ -0,0 +1,5 @@
|
||||
<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>
|
After Width: | Height: | Size: 1.8 KiB |
@ -4,12 +4,16 @@
|
||||
import * as React from 'react'
|
||||
import data from './Triangle.json'
|
||||
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'
|
||||
|
||||
|
@ -0,0 +1,38 @@
|
||||
{
|
||||
"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"
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
// 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
|
@ -13,6 +13,7 @@ export { default as Jinja } from './Jinja'
|
||||
export { default as KnowledgeRetrieval } from './KnowledgeRetrieval'
|
||||
export { default as ListFilter } from './ListFilter'
|
||||
export { default as Llm } from './Llm'
|
||||
export { default as LoopEnd } from './LoopEnd'
|
||||
export { default as Loop } from './Loop'
|
||||
export { default as ParameterExtractor } from './ParameterExtractor'
|
||||
export { default as QuestionClassifier } from './QuestionClassifier'
|
||||
|
157
web/app/components/base/select/pure.tsx
Normal file
157
web/app/components/base/select/pure.tsx
Normal file
@ -0,0 +1,157 @@
|
||||
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
|
@ -94,7 +94,7 @@ export default function AppSelector() {
|
||||
{userProfile.name}
|
||||
{isEducationAccount && (
|
||||
<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>
|
||||
</PremiumBadge>
|
||||
)}
|
||||
|
@ -42,8 +42,8 @@ const PlanBadge: FC<PlanBadgeProps> = ({ plan, allowHover, sandboxAsUpgrade = fa
|
||||
if (plan === Plan.professional) {
|
||||
return <PremiumBadge className='select-none' size='s' color='blue' allowHover={allowHover} onClick={onClick}>
|
||||
<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
|
||||
</span>
|
||||
</div>
|
||||
|
@ -16,6 +16,7 @@ import {
|
||||
ListFilter,
|
||||
Llm,
|
||||
Loop,
|
||||
LoopEnd,
|
||||
ParameterExtractor,
|
||||
QuestionClassifier,
|
||||
TemplatingTransform,
|
||||
@ -54,6 +55,7 @@ const getIcon = (type: BlockEnum, className: string) => {
|
||||
[BlockEnum.Iteration]: <Iteration className={className} />,
|
||||
[BlockEnum.LoopStart]: <VariableX className={className} />,
|
||||
[BlockEnum.Loop]: <Loop className={className} />,
|
||||
[BlockEnum.LoopEnd]: <LoopEnd className={className} />,
|
||||
[BlockEnum.ParameterExtractor]: <ParameterExtractor className={className} />,
|
||||
[BlockEnum.DocExtractor]: <DocsExtractor className={className} />,
|
||||
[BlockEnum.ListFilter]: <ListFilter className={className} />,
|
||||
@ -68,6 +70,7 @@ const ICON_CONTAINER_BG_COLOR_MAP: Record<string, string> = {
|
||||
[BlockEnum.IfElse]: 'bg-util-colors-cyan-cyan-500',
|
||||
[BlockEnum.Iteration]: '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.Answer]: 'bg-util-colors-warning-warning-500',
|
||||
[BlockEnum.KnowledgeRetrieval]: 'bg-util-colors-green-green-500',
|
||||
|
@ -15,6 +15,7 @@ import { BLOCK_CLASSIFICATIONS } from './constants'
|
||||
import { useBlocks } from './hooks'
|
||||
import type { ToolDefaultValue } from './types'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import Badge from '@/app/components/base/badge'
|
||||
|
||||
type BlocksProps = {
|
||||
searchText: string
|
||||
@ -90,7 +91,15 @@ const Blocks = ({
|
||||
className='mr-2 shrink-0'
|
||||
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>
|
||||
</Tooltip>
|
||||
))
|
||||
|
@ -39,6 +39,12 @@ export const BLOCKS: Block[] = [
|
||||
type: BlockEnum.IfElse,
|
||||
title: 'IF/ELSE',
|
||||
},
|
||||
{
|
||||
classification: BlockClassificationEnum.Logic,
|
||||
type: BlockEnum.LoopEnd,
|
||||
title: 'Exit Loop',
|
||||
description: '',
|
||||
},
|
||||
{
|
||||
classification: BlockClassificationEnum.Logic,
|
||||
type: BlockEnum.Iteration,
|
||||
|
@ -21,6 +21,7 @@ import ListFilterDefault from './nodes/list-operator/default'
|
||||
import IterationStartDefault from './nodes/iteration-start/default'
|
||||
import AgentDefault from './nodes/agent/default'
|
||||
import LoopStartDefault from './nodes/loop-start/default'
|
||||
import LoopEndDefault from './nodes/loop-end/default'
|
||||
|
||||
type NodesExtraData = {
|
||||
author: string
|
||||
@ -122,6 +123,15 @@ export const NODES_EXTRA_DATA: Record<BlockEnum, NodesExtraData> = {
|
||||
getAvailableNextNodes: LoopStartDefault.getAvailableNextNodes,
|
||||
checkValid: LoopStartDefault.checkValid,
|
||||
},
|
||||
[BlockEnum.LoopEnd]: {
|
||||
author: 'Dify',
|
||||
about: '',
|
||||
availablePrevNodes: [],
|
||||
availableNextNodes: [],
|
||||
getAvailablePrevNodes: LoopEndDefault.getAvailablePrevNodes,
|
||||
getAvailableNextNodes: LoopEndDefault.getAvailableNextNodes,
|
||||
checkValid: LoopEndDefault.checkValid,
|
||||
},
|
||||
[BlockEnum.Code]: {
|
||||
author: 'Dify',
|
||||
about: '',
|
||||
@ -297,6 +307,12 @@ export const NODES_INITIAL_DATA = {
|
||||
desc: '',
|
||||
...LoopStartDefault.defaultValue,
|
||||
},
|
||||
[BlockEnum.LoopEnd]: {
|
||||
type: BlockEnum.LoopEnd,
|
||||
title: '',
|
||||
desc: '',
|
||||
...LoopEndDefault.defaultValue,
|
||||
},
|
||||
[BlockEnum.Code]: {
|
||||
type: BlockEnum.Code,
|
||||
title: '',
|
||||
|
@ -42,6 +42,7 @@ export const useAvailableBlocks = (nodeType?: BlockEnum, isInIteration?: boolean
|
||||
const availableNextBlocks = useMemo(() => {
|
||||
if (!nodeType)
|
||||
return []
|
||||
|
||||
return nodesExtraData[nodeType].availableNextNodes || []
|
||||
}, [nodeType, nodesExtraData])
|
||||
|
||||
@ -54,6 +55,9 @@ export const useAvailableBlocks = (nodeType?: BlockEnum, isInIteration?: boolean
|
||||
if (isInLoop && (nType === BlockEnum.Iteration || nType === BlockEnum.Loop || nType === BlockEnum.End))
|
||||
return false
|
||||
|
||||
if (!isInLoop && nType === BlockEnum.LoopEnd)
|
||||
return false
|
||||
|
||||
return true
|
||||
}),
|
||||
availableNextBlocks: availableNextBlocks.filter((nType) => {
|
||||
@ -63,6 +67,9 @@ export const useAvailableBlocks = (nodeType?: BlockEnum, isInIteration?: boolean
|
||||
if (isInLoop && (nType === BlockEnum.Iteration || nType === BlockEnum.Loop || nType === BlockEnum.End))
|
||||
return false
|
||||
|
||||
if (!isInLoop && nType === BlockEnum.LoopEnd)
|
||||
return false
|
||||
|
||||
return true
|
||||
}),
|
||||
}
|
||||
|
@ -39,6 +39,7 @@ import {
|
||||
import {
|
||||
genNewNodeTitleFromOld,
|
||||
generateNewNode,
|
||||
getNodeCustomTypeByNodeDataType,
|
||||
getNodesConnectedSourceOrTargetHandleIdsMap,
|
||||
getTopLeftNodePosition,
|
||||
} from '../utils'
|
||||
@ -638,7 +639,7 @@ export const useNodesInteractions = () => {
|
||||
}
|
||||
|
||||
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)
|
||||
})
|
||||
@ -686,6 +687,7 @@ export const useNodesInteractions = () => {
|
||||
newIterationStartNode,
|
||||
newLoopStartNode,
|
||||
} = generateNewNode({
|
||||
type: getNodeCustomTypeByNodeDataType(nodeType),
|
||||
data: {
|
||||
...NODES_INITIAL_DATA[nodeType],
|
||||
title: nodesWithSameType.length > 0 ? `${t(`workflow.blocks.${nodeType}`)} ${nodesWithSameType.length + 1}` : t(`workflow.blocks.${nodeType}`),
|
||||
@ -775,10 +777,10 @@ export const useNodesInteractions = () => {
|
||||
}
|
||||
|
||||
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)
|
||||
node.data._children?.push(newNode.id)
|
||||
node.data._children?.push({ nodeId: newNode.id, nodeType: newNode.data.type })
|
||||
})
|
||||
draft.push(newNode)
|
||||
|
||||
@ -853,7 +855,7 @@ export const useNodesInteractions = () => {
|
||||
|
||||
let newEdge
|
||||
|
||||
if ((nodeType !== BlockEnum.IfElse) && (nodeType !== BlockEnum.QuestionClassifier)) {
|
||||
if ((nodeType !== BlockEnum.IfElse) && (nodeType !== BlockEnum.QuestionClassifier) && (nodeType !== BlockEnum.LoopEnd)) {
|
||||
newEdge = {
|
||||
id: `${newNode.id}-${sourceHandle}-${nextNodeId}-${nextNodeTargetHandle}`,
|
||||
type: CUSTOM_EDGE,
|
||||
@ -901,7 +903,7 @@ export const useNodesInteractions = () => {
|
||||
}
|
||||
|
||||
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) {
|
||||
node.data.start_node_id = newNode.id
|
||||
@ -909,7 +911,7 @@ export const useNodesInteractions = () => {
|
||||
}
|
||||
|
||||
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) {
|
||||
node.data.start_node_id = newNode.id
|
||||
@ -1004,7 +1006,7 @@ export const useNodesInteractions = () => {
|
||||
const isNextNodeInIteration = !!nextNodeParentNode && nextNodeParentNode.data.type === BlockEnum.Iteration
|
||||
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 = {
|
||||
id: `${newNode.id}-${sourceHandle}-${nextNodeId}-${nextNodeTargetHandle}`,
|
||||
type: CUSTOM_EDGE,
|
||||
@ -1049,9 +1051,9 @@ export const useNodesInteractions = () => {
|
||||
node.position.x += NODE_WIDTH_X_OFFSET
|
||||
|
||||
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)
|
||||
node.data._children?.push(newNode.id)
|
||||
node.data._children?.push({ nodeId: newNode.id, nodeType: newNode.data.type })
|
||||
})
|
||||
draft.push(newNode)
|
||||
if (newIterationStartNode)
|
||||
@ -1117,6 +1119,7 @@ export const useNodesInteractions = () => {
|
||||
newIterationStartNode,
|
||||
newLoopStartNode,
|
||||
} = generateNewNode({
|
||||
type: getNodeCustomTypeByNodeDataType(nodeType),
|
||||
data: {
|
||||
...NODES_INITIAL_DATA[nodeType],
|
||||
title: nodesWithSameType.length > 0 ? `${t(`workflow.blocks.${nodeType}`)} ${nodesWithSameType.length + 1}` : t(`workflow.blocks.${nodeType}`),
|
||||
@ -1240,7 +1243,7 @@ export const useNodesInteractions = () => {
|
||||
if (nodeId) {
|
||||
// If nodeId is provided, copy that specific node
|
||||
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)
|
||||
setClipboardElements([nodeToCopy])
|
||||
}
|
||||
@ -1254,7 +1257,7 @@ export const useNodesInteractions = () => {
|
||||
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)
|
||||
setClipboardElements([selectedNode])
|
||||
@ -1328,7 +1331,7 @@ export const useNodesInteractions = () => {
|
||||
newChildren = copyChildren
|
||||
idMapping = newIdMapping
|
||||
newChildren.forEach((child) => {
|
||||
newNode.data._children?.push(child.id)
|
||||
newNode.data._children?.push({ nodeId: child.id, nodeType: child.data.type })
|
||||
})
|
||||
newChildren.push(newIterationStartNode!)
|
||||
}
|
||||
@ -1339,7 +1342,7 @@ export const useNodesInteractions = () => {
|
||||
|
||||
newChildren = handleNodeLoopChildrenCopy(nodeToPaste.id, newNode.id)
|
||||
newChildren.forEach((child) => {
|
||||
newNode.data._children?.push(child.id)
|
||||
newNode.data._children?.push({ nodeId: child.id, nodeType: child.data.type })
|
||||
})
|
||||
newChildren.push(newLoopStartNode!)
|
||||
}
|
||||
@ -1424,7 +1427,7 @@ export const useNodesInteractions = () => {
|
||||
|
||||
const nodes = getNodes()
|
||||
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 bottomNode: Node
|
||||
|
||||
|
@ -19,6 +19,8 @@ export const useWorkflowNodeIterationFinished = () => {
|
||||
const {
|
||||
getNodes,
|
||||
setNodes,
|
||||
edges,
|
||||
setEdges,
|
||||
} = store.getState()
|
||||
const nodes = getNodes()
|
||||
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
|
||||
@ -38,6 +40,18 @@ export const useWorkflowNodeIterationFinished = () => {
|
||||
currentNode.data._runningStatus = data.status
|
||||
})
|
||||
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])
|
||||
|
||||
return {
|
||||
|
@ -3,7 +3,6 @@ import { useStoreApi } from 'reactflow'
|
||||
import produce from 'immer'
|
||||
import type { LoopFinishedResponse } from '@/types/workflow'
|
||||
import { useWorkflowStore } from '@/app/components/workflow/store'
|
||||
import { DEFAULT_LOOP_TIMES } from '@/app/components/workflow/constants'
|
||||
|
||||
export const useWorkflowNodeLoopFinished = () => {
|
||||
const store = useStoreApi()
|
||||
@ -14,11 +13,12 @@ export const useWorkflowNodeLoopFinished = () => {
|
||||
const {
|
||||
workflowRunningData,
|
||||
setWorkflowRunningData,
|
||||
setLoopTimes,
|
||||
} = workflowStore.getState()
|
||||
const {
|
||||
getNodes,
|
||||
setNodes,
|
||||
edges,
|
||||
setEdges,
|
||||
} = store.getState()
|
||||
const nodes = getNodes()
|
||||
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
|
||||
@ -31,13 +31,24 @@ export const useWorkflowNodeLoopFinished = () => {
|
||||
}
|
||||
}
|
||||
}))
|
||||
setLoopTimes(DEFAULT_LOOP_TIMES)
|
||||
const newNodes = produce(nodes, (draft) => {
|
||||
const currentNode = draft.find(node => node.id === data.node_id)!
|
||||
|
||||
currentNode.data._runningStatus = data.status
|
||||
})
|
||||
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])
|
||||
|
||||
return {
|
||||
|
@ -2,18 +2,12 @@ import { useCallback } from 'react'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
import produce from 'immer'
|
||||
import type { LoopNextResponse } from '@/types/workflow'
|
||||
import { useWorkflowStore } from '@/app/components/workflow/store'
|
||||
import { NodeRunningStatus } from '@/app/components/workflow/types'
|
||||
|
||||
export const useWorkflowNodeLoopNext = () => {
|
||||
const store = useStoreApi()
|
||||
const workflowStore = useWorkflowStore()
|
||||
|
||||
const handleWorkflowNodeLoopNext = useCallback((params: LoopNextResponse) => {
|
||||
const {
|
||||
loopTimes,
|
||||
setLoopTimes,
|
||||
} = workflowStore.getState()
|
||||
|
||||
const { data } = params
|
||||
const {
|
||||
getNodes,
|
||||
@ -23,11 +17,17 @@ export const useWorkflowNodeLoopNext = () => {
|
||||
const nodes = getNodes()
|
||||
const newNodes = produce(nodes, (draft) => {
|
||||
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)
|
||||
}, [workflowStore, store])
|
||||
}, [store])
|
||||
|
||||
return {
|
||||
handleWorkflowNodeLoopNext,
|
||||
|
@ -7,7 +7,6 @@ import produce from 'immer'
|
||||
import { useWorkflowStore } from '@/app/components/workflow/store'
|
||||
import type { LoopStartedResponse } from '@/types/workflow'
|
||||
import { NodeRunningStatus } from '@/app/components/workflow/types'
|
||||
import { DEFAULT_LOOP_TIMES } from '@/app/components/workflow/constants'
|
||||
|
||||
export const useWorkflowNodeLoopStarted = () => {
|
||||
const store = useStoreApi()
|
||||
@ -25,7 +24,6 @@ export const useWorkflowNodeLoopStarted = () => {
|
||||
const {
|
||||
workflowRunningData,
|
||||
setWorkflowRunningData,
|
||||
setLoopTimes,
|
||||
} = workflowStore.getState()
|
||||
const {
|
||||
getNodes,
|
||||
@ -41,7 +39,6 @@ export const useWorkflowNodeLoopStarted = () => {
|
||||
status: NodeRunningStatus.Running,
|
||||
})
|
||||
}))
|
||||
setLoopTimes(DEFAULT_LOOP_TIMES)
|
||||
|
||||
const {
|
||||
setViewport,
|
||||
|
@ -61,6 +61,8 @@ import CustomIterationStartNode from './nodes/iteration-start'
|
||||
import { CUSTOM_ITERATION_START_NODE } from './nodes/iteration-start/constants'
|
||||
import CustomLoopStartNode from './nodes/loop-start'
|
||||
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 CustomEdge from './custom-edge'
|
||||
import CustomConnectionLine from './custom-connection-line'
|
||||
@ -104,6 +106,7 @@ import DatasetsDetailProvider from './datasets-detail-store/provider'
|
||||
const nodeTypes = {
|
||||
[CUSTOM_NODE]: CustomNode,
|
||||
[CUSTOM_NOTE_NODE]: CustomNoteNode,
|
||||
[CUSTOM_SIMPLE_NODE]: CustomSimpleNode,
|
||||
[CUSTOM_ITERATION_START_NODE]: CustomIterationStartNode,
|
||||
[CUSTOM_LOOP_START_NODE]: CustomLoopStartNode,
|
||||
}
|
||||
|
@ -14,6 +14,9 @@ const HelpLink = ({
|
||||
const { t } = useTranslation()
|
||||
const link = useNodeHelpLink(nodeType)
|
||||
|
||||
if (!link)
|
||||
return null
|
||||
|
||||
return (
|
||||
<TooltipPlus
|
||||
popupContent={t('common.userProfile.helpCenter')}
|
||||
|
@ -164,7 +164,7 @@ const PanelOperatorPopup = ({
|
||||
)
|
||||
}
|
||||
{
|
||||
showHelpLink && (
|
||||
showHelpLink && link && (
|
||||
<>
|
||||
<div className='p-1'>
|
||||
<a
|
||||
|
@ -58,7 +58,8 @@ const VariableTag = ({
|
||||
{node && (
|
||||
<>
|
||||
<VarBlockIcon
|
||||
type={BlockEnum.Start}
|
||||
type={node.data.type || BlockEnum.Start}
|
||||
className='mr-0.5'
|
||||
/>
|
||||
<div
|
||||
className='max-w-[60px] truncate font-medium text-text-secondary'
|
||||
|
@ -3,7 +3,7 @@ import { isArray, uniq } from 'lodash-es'
|
||||
import type { CodeNodeType } from '../../../code/types'
|
||||
import type { EndNodeType } from '../../../end/types'
|
||||
import type { AnswerNodeType } from '../../../answer/types'
|
||||
import type { LLMNodeType } from '../../../llm/types'
|
||||
import { type LLMNodeType, type StructuredOutput, Type } from '../../../llm/types'
|
||||
import type { KnowledgeRetrievalNodeType } from '../../../knowledge-retrieval/types'
|
||||
import type { IfElseNodeType } from '../../../if-else/types'
|
||||
import type { TemplateTransformNodeType } from '../../../template-transform/types'
|
||||
@ -21,6 +21,8 @@ import { BlockEnum, InputVarType, VarType } from '@/app/components/workflow/type
|
||||
import type { StartNodeType } from '@/app/components/workflow/nodes/start/types'
|
||||
import type { ConversationVariable, EnvironmentVariable, Node, NodeOutPutVar, ValueSelector, Var } from '@/app/components/workflow/types'
|
||||
import type { VariableAssignerNodeType } from '@/app/components/workflow/nodes/variable-assigner/types'
|
||||
import type { Field as StructField } from '@/app/components/workflow/nodes/llm/types'
|
||||
|
||||
import {
|
||||
HTTP_REQUEST_OUTPUT_STRUCT,
|
||||
KNOWLEDGE_RETRIEVAL_OUTPUT_STRUCT,
|
||||
@ -55,19 +57,81 @@ const inputVarTypeToVarType = (type: InputVarType): VarType => {
|
||||
} as any)[type] || VarType.string
|
||||
}
|
||||
|
||||
const structTypeToVarType = (type: Type): VarType => {
|
||||
return ({
|
||||
[Type.string]: VarType.string,
|
||||
[Type.number]: VarType.number,
|
||||
[Type.boolean]: VarType.boolean,
|
||||
[Type.object]: VarType.object,
|
||||
[Type.array]: VarType.array,
|
||||
} as any)[type] || VarType.string
|
||||
}
|
||||
|
||||
export const varTypeToStructType = (type: VarType): Type => {
|
||||
return ({
|
||||
[VarType.string]: Type.string,
|
||||
[VarType.number]: Type.number,
|
||||
[VarType.boolean]: Type.boolean,
|
||||
[VarType.object]: Type.object,
|
||||
[VarType.array]: Type.array,
|
||||
} as any)[type] || Type.string
|
||||
}
|
||||
|
||||
const findExceptVarInStructuredProperties = (properties: Record<string, StructField>, filterVar: (payload: Var, selector: ValueSelector) => boolean): Record<string, StructField> => {
|
||||
const res = produce(properties, (draft) => {
|
||||
Object.keys(properties).forEach((key) => {
|
||||
const item = properties[key]
|
||||
const isObj = item.type === Type.object
|
||||
if (!isObj && !filterVar({
|
||||
variable: key,
|
||||
type: structTypeToVarType(item.type),
|
||||
}, [key])) {
|
||||
delete properties[key]
|
||||
return
|
||||
}
|
||||
if (item.type === Type.object && item.properties)
|
||||
item.properties = findExceptVarInStructuredProperties(item.properties, filterVar)
|
||||
})
|
||||
return draft
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
const findExceptVarInStructuredOutput = (structuredOutput: StructuredOutput, filterVar: (payload: Var, selector: ValueSelector) => boolean): StructuredOutput => {
|
||||
const res = produce(structuredOutput, (draft) => {
|
||||
const properties = draft.schema.properties
|
||||
Object.keys(properties).forEach((key) => {
|
||||
const item = properties[key]
|
||||
const isObj = item.type === Type.object
|
||||
if (!isObj && !filterVar({
|
||||
variable: key,
|
||||
type: structTypeToVarType(item.type),
|
||||
}, [key])) {
|
||||
delete properties[key]
|
||||
return
|
||||
}
|
||||
if (item.type === Type.object && item.properties)
|
||||
item.properties = findExceptVarInStructuredProperties(item.properties, filterVar)
|
||||
})
|
||||
return draft
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
const findExceptVarInObject = (obj: any, filterVar: (payload: Var, selector: ValueSelector) => boolean, value_selector: ValueSelector, isFile?: boolean): Var => {
|
||||
const { children } = obj
|
||||
const isStructuredOutput = !!(children as StructuredOutput)?.schema?.properties
|
||||
|
||||
const res: Var = {
|
||||
variable: obj.variable,
|
||||
type: isFile ? VarType.file : VarType.object,
|
||||
children: children.filter((item: Var) => {
|
||||
children: isStructuredOutput ? findExceptVarInStructuredOutput(children, filterVar) : children.filter((item: Var) => {
|
||||
const { children } = item
|
||||
const currSelector = [...value_selector, item.variable]
|
||||
if (!children)
|
||||
return filterVar(item, currSelector)
|
||||
|
||||
const obj = findExceptVarInObject(item, filterVar, currSelector, false) // File doesn't contains file children
|
||||
return obj.children && obj.children?.length > 0
|
||||
return obj.children && (obj.children as Var[])?.length > 0
|
||||
}),
|
||||
}
|
||||
return res
|
||||
@ -139,10 +203,17 @@ const formatItem = (
|
||||
}
|
||||
|
||||
case BlockEnum.LLM: {
|
||||
res.vars = LLM_OUTPUT_STRUCT
|
||||
res.vars = [...LLM_OUTPUT_STRUCT]
|
||||
if (data.structured_output_enabled && data.structured_output?.schema?.properties && Object.keys(data.structured_output.schema.properties).length > 0) {
|
||||
res.vars.push({
|
||||
variable: 'structured_output',
|
||||
type: VarType.object,
|
||||
children: data.structured_output,
|
||||
})
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case BlockEnum.KnowledgeRetrieval: {
|
||||
res.vars = KNOWLEDGE_RETRIEVAL_OUTPUT_STRUCT
|
||||
break
|
||||
@ -286,6 +357,21 @@ const formatItem = (
|
||||
break
|
||||
}
|
||||
|
||||
case BlockEnum.Loop: {
|
||||
const { loop_variables } = data as LoopNodeType
|
||||
res.isLoop = true
|
||||
res.vars = loop_variables?.map((v) => {
|
||||
return {
|
||||
variable: v.label,
|
||||
type: v.var_type,
|
||||
isLoopVariable: true,
|
||||
nodeId: res.nodeId,
|
||||
}
|
||||
}) || []
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case BlockEnum.DocExtractor: {
|
||||
res.vars = [
|
||||
{
|
||||
@ -405,7 +491,7 @@ const formatItem = (
|
||||
return false
|
||||
|
||||
const obj = findExceptVarInObject(isFile ? { ...v, children } : v, filterVar, selector, isFile)
|
||||
return obj?.children && obj?.children.length > 0
|
||||
return obj?.children && ((obj?.children as Var[]).length > 0 || Object.keys((obj?.children as StructuredOutput)?.schema?.properties || {}).length > 0)
|
||||
}).map((v) => {
|
||||
const isFile = v.type === VarType.file
|
||||
|
||||
@ -457,7 +543,7 @@ export const toNodeOutputVars = (
|
||||
},
|
||||
}
|
||||
const res = [
|
||||
...nodes.filter(node => SUPPORT_OUTPUT_VARS_NODE.includes(node.data.type)),
|
||||
...nodes.filter(node => SUPPORT_OUTPUT_VARS_NODE.includes(node?.data?.type)),
|
||||
...(environmentVariables.length > 0 ? [ENV_NODE] : []),
|
||||
...((isChatMode && conversationVariables.length > 0) ? [CHAT_VAR_NODE] : []),
|
||||
].map((node) => {
|
||||
@ -579,8 +665,7 @@ export const getVarType = ({
|
||||
isConstant,
|
||||
environmentVariables = [],
|
||||
conversationVariables = [],
|
||||
}:
|
||||
{
|
||||
}: {
|
||||
valueSelector: ValueSelector
|
||||
parentNode?: Node | null
|
||||
isIterationItem?: boolean
|
||||
@ -644,7 +729,7 @@ export const getVarType = ({
|
||||
const isEnv = isENV(valueSelector)
|
||||
const isChatVar = isConversationVar(valueSelector)
|
||||
const startNode = availableNodes.find((node: any) => {
|
||||
return node.data.type === BlockEnum.Start
|
||||
return node?.data.type === BlockEnum.Start
|
||||
})
|
||||
|
||||
const targetVarNodeId = isSystem ? startNode?.id : valueSelector[0]
|
||||
@ -655,10 +740,30 @@ export const getVarType = ({
|
||||
|
||||
let type: VarType = VarType.string
|
||||
let curr: any = targetVar.vars
|
||||
|
||||
if (isSystem || isEnv || isChatVar) {
|
||||
return curr.find((v: any) => v.variable === (valueSelector as ValueSelector).join('.'))?.type
|
||||
}
|
||||
else {
|
||||
const targetVar = curr.find((v: any) => v.variable === valueSelector[1])
|
||||
if (!targetVar)
|
||||
return VarType.string
|
||||
|
||||
const isStructuredOutputVar = !!targetVar.children?.schema?.properties
|
||||
if (isStructuredOutputVar) {
|
||||
let currProperties = targetVar.children.schema;
|
||||
(valueSelector as ValueSelector).slice(2).forEach((key, i) => {
|
||||
const isLast = i === valueSelector.length - 3
|
||||
if (!currProperties)
|
||||
return
|
||||
|
||||
currProperties = currProperties.properties[key]
|
||||
if (isLast)
|
||||
type = structTypeToVarType(currProperties?.type)
|
||||
})
|
||||
return type
|
||||
}
|
||||
|
||||
(valueSelector as ValueSelector).slice(1).forEach((key, i) => {
|
||||
const isLast = i === valueSelector.length - 2
|
||||
if (Array.isArray(curr))
|
||||
@ -741,6 +846,9 @@ export const toNodeAvailableVars = ({
|
||||
},
|
||||
],
|
||||
}
|
||||
const iterationIndex = beforeNodesOutputVars.findIndex(v => v.nodeId === iterationNode?.id)
|
||||
if (iterationIndex > -1)
|
||||
beforeNodesOutputVars.splice(iterationIndex, 1)
|
||||
beforeNodesOutputVars.unshift(iterationVar)
|
||||
}
|
||||
return beforeNodesOutputVars
|
||||
@ -1181,17 +1289,27 @@ export const updateNodeVars = (oldNode: Node, oldVarSelector: ValueSelector, new
|
||||
})
|
||||
return newNode
|
||||
}
|
||||
|
||||
const varToValueSelectorList = (v: Var, parentValueSelector: ValueSelector, res: ValueSelector[]) => {
|
||||
if (!v.variable)
|
||||
return
|
||||
|
||||
res.push([...parentValueSelector, v.variable])
|
||||
const isStructuredOutput = !!(v.children as StructuredOutput)?.schema?.properties
|
||||
|
||||
if (v.children && v.children.length > 0) {
|
||||
v.children.forEach((child) => {
|
||||
if ((v.children as Var[])?.length > 0) {
|
||||
(v.children as Var[]).forEach((child) => {
|
||||
varToValueSelectorList(child, [...parentValueSelector, v.variable], res)
|
||||
})
|
||||
}
|
||||
if (isStructuredOutput) {
|
||||
Object.keys((v.children as StructuredOutput)?.schema?.properties || {}).forEach((key) => {
|
||||
varToValueSelectorList({
|
||||
variable: key,
|
||||
type: structTypeToVarType((v.children as StructuredOutput)?.schema?.properties[key].type),
|
||||
}, [...parentValueSelector, v.variable], res)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const varsToValueSelectorList = (vars: Var | Var[], parentValueSelector: ValueSelector, res: ValueSelector[]) => {
|
||||
@ -1225,7 +1343,16 @@ export const getNodeOutputVars = (node: Node, isChatMode: boolean): ValueSelecto
|
||||
}
|
||||
|
||||
case BlockEnum.LLM: {
|
||||
varsToValueSelectorList(LLM_OUTPUT_STRUCT, [id], res)
|
||||
const vars = [...LLM_OUTPUT_STRUCT]
|
||||
const llmNodeData = data as LLMNodeType
|
||||
if (llmNodeData.structured_output_enabled && llmNodeData.structured_output?.schema?.properties && Object.keys(llmNodeData.structured_output.schema.properties).length > 0) {
|
||||
vars.push({
|
||||
variable: 'structured_output',
|
||||
type: VarType.object,
|
||||
children: llmNodeData.structured_output,
|
||||
})
|
||||
}
|
||||
varsToValueSelectorList(vars, [id], res)
|
||||
break
|
||||
}
|
||||
|
||||
|
@ -101,7 +101,7 @@ const VarReferencePicker: FC<Props> = ({
|
||||
const isChatMode = useIsChatMode()
|
||||
|
||||
const { getCurrentVariableType } = useWorkflowVariables()
|
||||
const { availableNodes, availableVars } = useAvailableVarList(nodeId, {
|
||||
const { availableVars, availableNodesWithParent: availableNodes } = useAvailableVarList(nodeId, {
|
||||
onlyLeafNodeVar,
|
||||
passedInAvailableNodes,
|
||||
filterVar,
|
||||
|
@ -16,6 +16,7 @@ import Input from '@/app/components/base/input'
|
||||
import { BubbleX, Env } from '@/app/components/base/icons/src/vender/line/others'
|
||||
import { checkKeys } from '@/utils/var'
|
||||
import { FILE_STRUCT } from '@/app/components/workflow/constants'
|
||||
import { Loop } from '@/app/components/base/icons/src/vender/workflow'
|
||||
|
||||
type ObjectChildrenProps = {
|
||||
nodeId: string
|
||||
@ -38,6 +39,7 @@ type ItemProps = {
|
||||
itemWidth?: number
|
||||
isSupportFileVar?: boolean
|
||||
isException?: boolean
|
||||
isLoopVar?: boolean
|
||||
}
|
||||
|
||||
const Item: FC<ItemProps> = ({
|
||||
@ -50,6 +52,7 @@ const Item: FC<ItemProps> = ({
|
||||
itemWidth,
|
||||
isSupportFileVar,
|
||||
isException,
|
||||
isLoopVar,
|
||||
}) => {
|
||||
const isFile = itemData.type === VarType.file
|
||||
const isObj = ([VarType.object, VarType.file].includes(itemData.type) && itemData.children && itemData.children.length > 0)
|
||||
@ -112,9 +115,10 @@ const Item: FC<ItemProps> = ({
|
||||
onMouseDown={e => e.preventDefault()}
|
||||
>
|
||||
<div className='flex w-0 grow items-center'>
|
||||
{!isEnv && !isChatVar && <Variable02 className={cn('h-3.5 w-3.5 shrink-0 text-text-accent', isException && 'text-text-warning')} />}
|
||||
{!isEnv && !isChatVar && !isLoopVar && <Variable02 className={cn('h-3.5 w-3.5 shrink-0 text-text-accent', isException && 'text-text-warning')} />}
|
||||
{isEnv && <Env className='h-3.5 w-3.5 shrink-0 text-util-colors-violet-violet-600' />}
|
||||
{isChatVar && <BubbleX className='h-3.5 w-3.5 text-util-colors-teal-teal-700' />}
|
||||
{isChatVar && <BubbleX className='h-3.5 w-3.5 shrink-0 text-util-colors-teal-teal-700' />}
|
||||
{isLoopVar && <Loop className='h-3.5 w-3.5 shrink-0 text-util-colors-cyan-cyan-500' />}
|
||||
{!isEnv && !isChatVar && (
|
||||
<div title={itemData.variable} className='system-sm-medium ml-1 w-0 grow truncate text-text-secondary'>{itemData.variable}</div>
|
||||
)}
|
||||
@ -317,6 +321,7 @@ const VarReferenceVars: FC<Props> = ({
|
||||
itemWidth={itemWidth}
|
||||
isSupportFileVar={isSupportFileVar}
|
||||
isException={v.isException}
|
||||
isLoopVar={item.isLoop}
|
||||
/>
|
||||
))}
|
||||
</div>))
|
||||
|
@ -24,11 +24,11 @@ const useAvailableVarList = (nodeId: string, {
|
||||
onlyLeafNodeVar: false,
|
||||
filterVar: () => true,
|
||||
}) => {
|
||||
const { getTreeLeafNodes, getBeforeNodesInSameBranch } = useWorkflow()
|
||||
const { getTreeLeafNodes, getBeforeNodesInSameBranchIncludeParent } = useWorkflow()
|
||||
const { getNodeAvailableVars } = useWorkflowVariables()
|
||||
const isChatMode = useIsChatMode()
|
||||
|
||||
const availableNodes = passedInAvailableNodes || (onlyLeafNodeVar ? getTreeLeafNodes(nodeId) : getBeforeNodesInSameBranch(nodeId))
|
||||
const availableNodes = passedInAvailableNodes || (onlyLeafNodeVar ? getTreeLeafNodes(nodeId) : getBeforeNodesInSameBranchIncludeParent(nodeId))
|
||||
|
||||
const {
|
||||
parentNode: iterationNode,
|
||||
@ -46,7 +46,7 @@ const useAvailableVarList = (nodeId: string, {
|
||||
return {
|
||||
availableVars,
|
||||
availableNodes,
|
||||
availableNodesWithParent: iterationNode ? [...availableNodes, iterationNode] : availableNodes,
|
||||
availableNodesWithParent: availableNodes,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -26,9 +26,7 @@ export const useNodeHelpLink = (nodeType: BlockEnum) => {
|
||||
[BlockEnum.VariableAggregator]: 'variable-aggregator',
|
||||
[BlockEnum.Assigner]: 'variable-assigner',
|
||||
[BlockEnum.Iteration]: 'iteration',
|
||||
[BlockEnum.IterationStart]: 'iteration',
|
||||
[BlockEnum.Loop]: 'loop',
|
||||
[BlockEnum.LoopStart]: 'loop',
|
||||
[BlockEnum.ParameterExtractor]: 'parameter-extractor',
|
||||
[BlockEnum.HttpRequest]: 'http-request',
|
||||
[BlockEnum.Tool]: 'tools',
|
||||
@ -52,9 +50,7 @@ export const useNodeHelpLink = (nodeType: BlockEnum) => {
|
||||
[BlockEnum.VariableAggregator]: 'variable-aggregator',
|
||||
[BlockEnum.Assigner]: 'variable-assigner',
|
||||
[BlockEnum.Iteration]: 'iteration',
|
||||
[BlockEnum.IterationStart]: 'iteration',
|
||||
[BlockEnum.Loop]: 'loop',
|
||||
[BlockEnum.LoopStart]: 'loop',
|
||||
[BlockEnum.ParameterExtractor]: 'parameter-extractor',
|
||||
[BlockEnum.HttpRequest]: 'http-request',
|
||||
[BlockEnum.Tool]: 'tools',
|
||||
@ -62,7 +58,12 @@ export const useNodeHelpLink = (nodeType: BlockEnum) => {
|
||||
[BlockEnum.ListFilter]: 'list-operator',
|
||||
[BlockEnum.Agent]: 'agent',
|
||||
}
|
||||
}, [language])
|
||||
}, [language]) as Record<string, string>
|
||||
|
||||
return `${prefixLink}${linkMap[nodeType]}`
|
||||
const link = linkMap[nodeType]
|
||||
|
||||
if (!link)
|
||||
return ''
|
||||
|
||||
return `${prefixLink}${link}`
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import type {
|
||||
FC,
|
||||
ReactNode,
|
||||
ReactElement,
|
||||
} from 'react'
|
||||
import {
|
||||
cloneElement,
|
||||
@ -46,7 +46,7 @@ import BlockIcon from '@/app/components/workflow/block-icon'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
|
||||
type BaseNodeProps = {
|
||||
children: ReactNode
|
||||
children: ReactElement
|
||||
} & NodeProps
|
||||
|
||||
const BaseNode: FC<BaseNodeProps> = ({
|
||||
@ -104,6 +104,30 @@ const BaseNode: FC<BaseNodeProps> = ({
|
||||
}
|
||||
}, [data._runningStatus, showSelectedBorder])
|
||||
|
||||
const LoopIndex = useMemo(() => {
|
||||
let text = ''
|
||||
|
||||
if (data._runningStatus === NodeRunningStatus.Running)
|
||||
text = t('workflow.nodes.loop.currentLoopCount', { count: data._loopIndex })
|
||||
if (data._runningStatus === NodeRunningStatus.Succeeded || data._runningStatus === NodeRunningStatus.Failed)
|
||||
text = t('workflow.nodes.loop.totalLoopCount', { count: data._loopIndex })
|
||||
|
||||
if (text) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'system-xs-medium mr-2 text-text-tertiary',
|
||||
data._runningStatus === NodeRunningStatus.Running && 'text-text-accent',
|
||||
)}
|
||||
>
|
||||
{text}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}, [data._loopIndex, data._runningStatus, t])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
@ -233,11 +257,7 @@ const BaseNode: FC<BaseNodeProps> = ({
|
||||
)
|
||||
}
|
||||
{
|
||||
data._loopLength && data._loopIndex && data._runningStatus === NodeRunningStatus.Running && (
|
||||
<div className='mr-1.5 text-xs font-medium text-primary-600'>
|
||||
{data._loopIndex > data._loopLength ? data._loopLength : data._loopIndex}/{data._loopLength}
|
||||
</div>
|
||||
)
|
||||
data.type === BlockEnum.Loop && data._loopIndex && LoopIndex
|
||||
}
|
||||
{
|
||||
(data._runningStatus === NodeRunningStatus.Running || data._singleRunningStatus === NodeRunningStatus.Running) && (
|
||||
|
@ -95,10 +95,13 @@ const VarList: FC<Props> = ({
|
||||
}, [onOpen])
|
||||
|
||||
const handleFilterToAssignedVar = useCallback((index: number) => {
|
||||
return (payload: Var, valueSelector: ValueSelector) => {
|
||||
return (payload: Var) => {
|
||||
const item = list[index]
|
||||
const assignedVarType = item.variable_selector ? getAssignedVarType?.(item.variable_selector) : undefined
|
||||
|
||||
if (item.variable_selector.join('.') === `${payload.nodeId}.${payload.variable}`)
|
||||
return false
|
||||
|
||||
if (!filterToAssignedVar || !item.variable_selector || !assignedVarType || !item.operation)
|
||||
return true
|
||||
|
||||
|
@ -13,7 +13,6 @@ const NodeComponent: FC<NodeProps<AssignerNodeType>> = ({
|
||||
data,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const nodes: Node[] = useNodes()
|
||||
if (data.version === '2') {
|
||||
const { items: operationItems } = data
|
||||
|
@ -31,7 +31,7 @@ const useConfig = (id: string, rawPayload: AssignerNodeType) => {
|
||||
}
|
||||
|
||||
const store = useStoreApi()
|
||||
const { getBeforeNodesInSameBranch } = useWorkflow()
|
||||
const { getBeforeNodesInSameBranchIncludeParent } = useWorkflow()
|
||||
|
||||
const {
|
||||
getNodes,
|
||||
@ -39,11 +39,9 @@ const useConfig = (id: string, rawPayload: AssignerNodeType) => {
|
||||
const currentNode = getNodes().find(n => n.id === id)
|
||||
const isInIteration = payload.isInIteration
|
||||
const iterationNode = isInIteration ? getNodes().find(n => n.id === currentNode!.parentId) : null
|
||||
const isInLoop = payload.isInLoop
|
||||
const loopNode = isInLoop ? getNodes().find(n => n.id === currentNode!.parentId) : null
|
||||
const availableNodes = useMemo(() => {
|
||||
return getBeforeNodesInSameBranch(id)
|
||||
}, [getBeforeNodesInSameBranch, id])
|
||||
return getBeforeNodesInSameBranchIncludeParent(id)
|
||||
}, [getBeforeNodesInSameBranchIncludeParent, id])
|
||||
const { inputs, setInputs } = useNodeCrud<AssignerNodeType>(id, payload)
|
||||
const newSetInputs = useCallback((newInputs: AssignerNodeType) => {
|
||||
const finalInputs = produce(newInputs, (draft) => {
|
||||
@ -56,13 +54,13 @@ const useConfig = (id: string, rawPayload: AssignerNodeType) => {
|
||||
const { getCurrentVariableType } = useWorkflowVariables()
|
||||
const getAssignedVarType = useCallback((valueSelector: ValueSelector) => {
|
||||
return getCurrentVariableType({
|
||||
parentNode: isInIteration ? iterationNode : loopNode,
|
||||
parentNode: isInIteration ? iterationNode : null,
|
||||
valueSelector: valueSelector || [],
|
||||
availableNodes,
|
||||
isChatMode,
|
||||
isConstant: false,
|
||||
})
|
||||
}, [getCurrentVariableType, isInIteration, iterationNode, loopNode, availableNodes, isChatMode])
|
||||
}, [getCurrentVariableType, isInIteration, iterationNode, availableNodes, isChatMode])
|
||||
|
||||
const handleOperationListChanges = useCallback((items: AssignerNodeOperation[]) => {
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
@ -91,6 +89,8 @@ const useConfig = (id: string, rawPayload: AssignerNodeType) => {
|
||||
}, [])
|
||||
|
||||
const filterAssignedVar = useCallback((varPayload: Var, selector: ValueSelector) => {
|
||||
if (varPayload.isLoopVariable)
|
||||
return true
|
||||
return selector.join('.').startsWith('conversation')
|
||||
}, [])
|
||||
|
||||
|
@ -6,7 +6,10 @@ import type {
|
||||
BlockEnum,
|
||||
Node,
|
||||
} from '../../types'
|
||||
import { generateNewNode } from '../../utils'
|
||||
import {
|
||||
generateNewNode,
|
||||
getNodeCustomTypeByNodeDataType,
|
||||
} from '../../utils'
|
||||
import {
|
||||
ITERATION_PADDING,
|
||||
NODES_INITIAL_DATA,
|
||||
@ -115,6 +118,7 @@ export const useNodeIterationInteractions = () => {
|
||||
const childNodeType = child.data.type as BlockEnum
|
||||
const nodesWithSameType = nodes.filter(node => node.data.type === childNodeType)
|
||||
const { newNode } = generateNewNode({
|
||||
type: getNodeCustomTypeByNodeDataType(childNodeType),
|
||||
data: {
|
||||
...NODES_INITIAL_DATA[childNodeType],
|
||||
...child.data,
|
||||
|
@ -15,4 +15,51 @@ export type LLMNodeType = CommonNodeType & {
|
||||
enabled: boolean
|
||||
configs?: VisionSetting
|
||||
}
|
||||
structured_output_enabled?: boolean
|
||||
structured_output?: StructuredOutput
|
||||
}
|
||||
|
||||
export enum Type {
|
||||
string = 'string',
|
||||
number = 'number',
|
||||
boolean = 'boolean',
|
||||
object = 'object',
|
||||
array = 'array',
|
||||
}
|
||||
|
||||
export enum ArrayType {
|
||||
string = 'array[string]',
|
||||
number = 'array[number]',
|
||||
boolean = 'array[boolean]',
|
||||
object = 'array[object]',
|
||||
}
|
||||
|
||||
export type TypeWithArray = Type | ArrayType
|
||||
|
||||
type ArrayItemType = Exclude<Type, Type.array>
|
||||
export type ArrayItems = Omit<Field, 'type'> & { type: ArrayItemType }
|
||||
|
||||
export type SchemaEnumType = string[] | number[]
|
||||
|
||||
export type Field = {
|
||||
type: Type
|
||||
properties?: { // Object has properties
|
||||
[key: string]: Field
|
||||
}
|
||||
required?: string[] // Key of required properties in object
|
||||
description?: string
|
||||
items?: ArrayItems // Array has items. Define the item type
|
||||
enum?: SchemaEnumType // Enum values
|
||||
additionalProperties?: false // Required in object by api. Just set false
|
||||
}
|
||||
|
||||
export type StructuredOutput = {
|
||||
schema: SchemaRoot
|
||||
}
|
||||
|
||||
export type SchemaRoot = {
|
||||
type: Type.object
|
||||
properties: Record<string, Field>
|
||||
required?: string[]
|
||||
additionalProperties: false
|
||||
}
|
||||
|
23
web/app/components/workflow/nodes/loop-end/default.ts
Normal file
23
web/app/components/workflow/nodes/loop-end/default.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import type { NodeDefault } from '../../types'
|
||||
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/blocks'
|
||||
import type {
|
||||
SimpleNodeType,
|
||||
} from '@/app/components/workflow/simple-node/types'
|
||||
|
||||
const nodeDefault: NodeDefault<SimpleNodeType> = {
|
||||
defaultValue: {},
|
||||
getAvailablePrevNodes(isChatMode: boolean) {
|
||||
const nodes = isChatMode ? ALL_CHAT_AVAILABLE_BLOCKS : ALL_COMPLETION_AVAILABLE_BLOCKS
|
||||
return nodes
|
||||
},
|
||||
getAvailableNextNodes() {
|
||||
return []
|
||||
},
|
||||
checkValid() {
|
||||
return {
|
||||
isValid: true,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export default nodeDefault
|
@ -138,9 +138,6 @@ const ConditionWrap: FC<Props> = ({
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{!isSubVariable && (
|
||||
<div className='mx-3 my-2 h-[1px] bg-divider-subtle'></div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
@ -0,0 +1,13 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const Empty = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className='system-xs-regular flex h-10 items-center justify-center rounded-[10px] bg-background-section text-text-tertiary'>
|
||||
{t('workflow.nodes.loop.setLoopVariables')}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Empty
|
@ -0,0 +1,144 @@
|
||||
import {
|
||||
useCallback,
|
||||
useMemo,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import VarReferencePicker from '@/app/components/workflow/nodes/_base/components/variable/var-reference-picker'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Textarea from '@/app/components/base/textarea'
|
||||
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
|
||||
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
|
||||
import type {
|
||||
LoopVariable,
|
||||
} from '@/app/components/workflow/nodes/loop/types'
|
||||
import type {
|
||||
Var,
|
||||
} from '@/app/components/workflow/types'
|
||||
import {
|
||||
ValueType,
|
||||
VarType,
|
||||
} from '@/app/components/workflow/types'
|
||||
|
||||
const objectPlaceholder = `# example
|
||||
# {
|
||||
# "name": "ray",
|
||||
# "age": 20
|
||||
# }`
|
||||
const arrayStringPlaceholder = `# example
|
||||
# [
|
||||
# "value1",
|
||||
# "value2"
|
||||
# ]`
|
||||
const arrayNumberPlaceholder = `# example
|
||||
# [
|
||||
# 100,
|
||||
# 200
|
||||
# ]`
|
||||
const arrayObjectPlaceholder = `# example
|
||||
# [
|
||||
# {
|
||||
# "name": "ray",
|
||||
# "age": 20
|
||||
# },
|
||||
# {
|
||||
# "name": "lily",
|
||||
# "age": 18
|
||||
# }
|
||||
# ]`
|
||||
|
||||
type FormItemProps = {
|
||||
nodeId: string
|
||||
item: LoopVariable
|
||||
onChange: (value: any) => void
|
||||
}
|
||||
const FormItem = ({
|
||||
nodeId,
|
||||
item,
|
||||
onChange,
|
||||
}: FormItemProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { value_type, var_type, value } = item
|
||||
|
||||
const handleInputChange = useCallback((e: any) => {
|
||||
onChange(e.target.value)
|
||||
}, [onChange])
|
||||
|
||||
const handleChange = useCallback((value: any) => {
|
||||
onChange(value)
|
||||
}, [onChange])
|
||||
|
||||
const filterVar = useCallback((variable: Var) => {
|
||||
return variable.type === var_type
|
||||
}, [var_type])
|
||||
|
||||
const editorMinHeight = useMemo(() => {
|
||||
if (var_type === VarType.arrayObject)
|
||||
return '240px'
|
||||
return '120px'
|
||||
}, [var_type])
|
||||
const placeholder = useMemo(() => {
|
||||
if (var_type === VarType.arrayString)
|
||||
return arrayStringPlaceholder
|
||||
if (var_type === VarType.arrayNumber)
|
||||
return arrayNumberPlaceholder
|
||||
if (var_type === VarType.arrayObject)
|
||||
return arrayObjectPlaceholder
|
||||
return objectPlaceholder
|
||||
}, [var_type])
|
||||
|
||||
return (
|
||||
<div>
|
||||
{
|
||||
value_type === ValueType.variable && (
|
||||
<VarReferencePicker
|
||||
readonly={false}
|
||||
nodeId={nodeId}
|
||||
isShowNodeName
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
filterVar={filterVar}
|
||||
placeholder={t('workflow.nodes.assigner.setParameter') as string}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
value_type === ValueType.constant && var_type === VarType.string && (
|
||||
<Textarea
|
||||
value={value}
|
||||
onChange={handleInputChange}
|
||||
className='min-h-12 w-full'
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
value_type === ValueType.constant && var_type === VarType.number && (
|
||||
<Input
|
||||
type="number"
|
||||
value={value}
|
||||
onChange={handleInputChange}
|
||||
className='w-full'
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
value_type === ValueType.constant
|
||||
&& (var_type === VarType.object || var_type === VarType.arrayString || var_type === VarType.arrayNumber || var_type === VarType.arrayObject)
|
||||
&& (
|
||||
<div className='w-full rounded-[10px] bg-components-input-bg-normal py-2 pl-3 pr-1' style={{ height: editorMinHeight }}>
|
||||
<CodeEditor
|
||||
value={value}
|
||||
isExpand
|
||||
noWrapper
|
||||
language={CodeLanguage.json}
|
||||
onChange={handleChange}
|
||||
className='w-full'
|
||||
placeholder={<div className='whitespace-pre'>{placeholder}</div>}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default FormItem
|
@ -0,0 +1,28 @@
|
||||
import Empty from './empty'
|
||||
import Item from './item'
|
||||
import type {
|
||||
LoopVariable,
|
||||
LoopVariablesComponentShape,
|
||||
} from '@/app/components/workflow/nodes/loop/types'
|
||||
|
||||
type LoopVariableProps = {
|
||||
variables?: LoopVariable[]
|
||||
} & LoopVariablesComponentShape
|
||||
|
||||
const LoopVariableComponent = ({
|
||||
variables = [],
|
||||
...restProps
|
||||
}: LoopVariableProps) => {
|
||||
if (!variables.length)
|
||||
return <Empty />
|
||||
|
||||
return variables.map(variable => (
|
||||
<Item
|
||||
key={variable.id}
|
||||
item={variable}
|
||||
{...restProps}
|
||||
/>
|
||||
))
|
||||
}
|
||||
|
||||
export default LoopVariableComponent
|
@ -0,0 +1,37 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import PureSelect from '@/app/components/base/select/pure'
|
||||
|
||||
type InputModeSelectProps = {
|
||||
value?: string
|
||||
onChange: (value: string) => void
|
||||
}
|
||||
const InputModeSelect = ({
|
||||
value,
|
||||
onChange,
|
||||
}: InputModeSelectProps) => {
|
||||
const { t } = useTranslation()
|
||||
const options = [
|
||||
{
|
||||
label: 'Variable',
|
||||
value: 'variable',
|
||||
},
|
||||
{
|
||||
label: 'Constant',
|
||||
value: 'constant',
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<PureSelect
|
||||
options={options}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
popupProps={{
|
||||
title: t('workflow.nodes.loop.inputMode'),
|
||||
className: 'w-[132px]',
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default InputModeSelect
|
@ -0,0 +1,78 @@
|
||||
import { useCallback } from 'react'
|
||||
import { RiDeleteBinLine } from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import InputModeSelect from './input-mode-selec'
|
||||
import VariableTypeSelect from './variable-type-select'
|
||||
import FormItem from './form-item'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import Input from '@/app/components/base/input'
|
||||
import type {
|
||||
LoopVariable,
|
||||
LoopVariablesComponentShape,
|
||||
} from '@/app/components/workflow/nodes/loop/types'
|
||||
|
||||
type ItemProps = {
|
||||
item: LoopVariable
|
||||
} & LoopVariablesComponentShape
|
||||
const Item = ({
|
||||
nodeId,
|
||||
item,
|
||||
handleRemoveLoopVariable,
|
||||
handleUpdateLoopVariable,
|
||||
}: ItemProps) => {
|
||||
const { t } = useTranslation()
|
||||
const handleUpdateItemLabel = useCallback((e: any) => {
|
||||
handleUpdateLoopVariable(item.id, { label: e.target.value })
|
||||
}, [item.id, handleUpdateLoopVariable])
|
||||
|
||||
const handleUpdateItemVarType = useCallback((value: any) => {
|
||||
handleUpdateLoopVariable(item.id, { var_type: value, value: undefined })
|
||||
}, [item.id, handleUpdateLoopVariable])
|
||||
|
||||
const handleUpdateItemValueType = useCallback((value: any) => {
|
||||
handleUpdateLoopVariable(item.id, { value_type: value, value: undefined })
|
||||
}, [item.id, handleUpdateLoopVariable])
|
||||
|
||||
const handleUpdateItemValue = useCallback((value: any) => {
|
||||
handleUpdateLoopVariable(item.id, { value })
|
||||
}, [item.id, handleUpdateLoopVariable])
|
||||
|
||||
return (
|
||||
<div className='mb-4 flex last-of-type:mb-0'>
|
||||
<div className='w-0 grow'>
|
||||
<div className='mb-1 grid grid-cols-3 gap-1'>
|
||||
<Input
|
||||
value={item.label}
|
||||
onChange={handleUpdateItemLabel}
|
||||
autoFocus={!item.label}
|
||||
placeholder={t('workflow.nodes.loop.variableName')}
|
||||
/>
|
||||
<VariableTypeSelect
|
||||
value={item.var_type}
|
||||
onChange={handleUpdateItemVarType}
|
||||
/>
|
||||
<InputModeSelect
|
||||
value={item.value_type}
|
||||
onChange={handleUpdateItemValueType}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<FormItem
|
||||
nodeId={nodeId}
|
||||
item={item}
|
||||
onChange={handleUpdateItemValue}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ActionButton
|
||||
className='shrink-0'
|
||||
size='l'
|
||||
onClick={() => handleRemoveLoopVariable(item.id)}
|
||||
>
|
||||
<RiDeleteBinLine className='h-4 w-4 text-text-tertiary' />
|
||||
</ActionButton>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Item
|
@ -0,0 +1,51 @@
|
||||
import PureSelect from '@/app/components/base/select/pure'
|
||||
import { VarType } from '@/app/components/workflow/types'
|
||||
|
||||
type VariableTypeSelectProps = {
|
||||
value?: string
|
||||
onChange: (value: string) => void
|
||||
}
|
||||
const VariableTypeSelect = ({
|
||||
value,
|
||||
onChange,
|
||||
}: VariableTypeSelectProps) => {
|
||||
const options = [
|
||||
{
|
||||
label: 'String',
|
||||
value: VarType.string,
|
||||
},
|
||||
{
|
||||
label: 'Number',
|
||||
value: VarType.number,
|
||||
},
|
||||
{
|
||||
label: 'Object',
|
||||
value: VarType.object,
|
||||
},
|
||||
{
|
||||
label: 'Array[string]',
|
||||
value: VarType.arrayString,
|
||||
},
|
||||
{
|
||||
label: 'Array[number]',
|
||||
value: VarType.arrayNumber,
|
||||
},
|
||||
{
|
||||
label: 'Array[object]',
|
||||
value: VarType.arrayObject,
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<PureSelect
|
||||
options={options}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
popupProps={{
|
||||
className: 'w-[132px]',
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default VariableTypeSelect
|
@ -28,6 +28,11 @@ const nodeDefault: NodeDefault<LoopNodeType> = {
|
||||
checkValid(payload: LoopNodeType, t: any) {
|
||||
let errorMessages = ''
|
||||
|
||||
payload.loop_variables?.forEach((variable) => {
|
||||
if (!variable.label)
|
||||
errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t(`${i18nPrefix}.fields.variable`) })
|
||||
})
|
||||
|
||||
payload.break_conditions!.forEach((condition) => {
|
||||
if (!errorMessages && (!condition.variable_selector || condition.variable_selector.length === 0))
|
||||
errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t(`${i18nPrefix}.fields.variable`) })
|
||||
|
@ -1,13 +1,14 @@
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { RiAddLine } from '@remixicon/react'
|
||||
import Split from '../_base/components/split'
|
||||
import ResultPanel from '../../run/result-panel'
|
||||
import InputNumberWithSlider from '../_base/components/input-number-with-slider'
|
||||
import type { LoopNodeType } from './types'
|
||||
import useConfig from './use-config'
|
||||
import ConditionWrap from './components/condition-wrap'
|
||||
import LoopVariable from './components/loop-variables'
|
||||
import type { NodePanelProps } from '@/app/components/workflow/types'
|
||||
import Field from '@/app/components/workflow/nodes/_base/components/field'
|
||||
import BeforeRunForm from '@/app/components/workflow/nodes/_base/components/before-run-form'
|
||||
@ -45,6 +46,9 @@ const Panel: FC<NodePanelProps<LoopNodeType>> = ({
|
||||
handleUpdateSubVariableCondition,
|
||||
handleToggleSubVariableConditionLogicalOperator,
|
||||
handleUpdateLoopCount,
|
||||
handleAddLoopVariable,
|
||||
handleRemoveLoopVariable,
|
||||
handleUpdateLoopVariable,
|
||||
} = useConfig(id, data)
|
||||
|
||||
const nodeInfo = formatTracing(loopRunResult, t)[0]
|
||||
@ -53,6 +57,27 @@ const Panel: FC<NodePanelProps<LoopNodeType>> = ({
|
||||
return (
|
||||
<div className='mt-2'>
|
||||
<div>
|
||||
<Field
|
||||
title={<div className='pl-3'>{t('workflow.nodes.loop.loopVariables')}</div>}
|
||||
operations={
|
||||
<div
|
||||
className='mr-4 flex h-5 w-5 cursor-pointer items-center justify-center'
|
||||
onClick={handleAddLoopVariable}
|
||||
>
|
||||
<RiAddLine className='h-4 w-4 text-text-tertiary' />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className='px-4'>
|
||||
<LoopVariable
|
||||
variables={inputs.loop_variables}
|
||||
nodeId={id}
|
||||
handleRemoveLoopVariable={handleRemoveLoopVariable}
|
||||
handleUpdateLoopVariable={handleUpdateLoopVariable}
|
||||
/>
|
||||
</div>
|
||||
</Field>
|
||||
<Split className='my-2' />
|
||||
<Field
|
||||
title={<div className='pl-3'>{t(`${i18nPrefix}.breakCondition`)}</div>}
|
||||
tooltip={t(`${i18nPrefix}.breakConditionTip`)}
|
||||
@ -74,7 +99,7 @@ const Panel: FC<NodePanelProps<LoopNodeType>> = ({
|
||||
logicalOperator={inputs.logical_operator!}
|
||||
/>
|
||||
</Field>
|
||||
<Split />
|
||||
<Split className='mt-2' />
|
||||
<div className='mt-2'>
|
||||
<Field
|
||||
title={<div className='pl-3'>{t(`${i18nPrefix}.loopMaxCount`)}</div>}
|
||||
|
@ -4,6 +4,7 @@ import type {
|
||||
CommonNodeType,
|
||||
ErrorHandleMode,
|
||||
ValueSelector,
|
||||
ValueType,
|
||||
Var,
|
||||
VarType,
|
||||
} from '@/app/components/workflow/types'
|
||||
@ -65,6 +66,13 @@ export type handleRemoveSubVariableCondition = (conditionId: string, subConditio
|
||||
export type HandleUpdateSubVariableCondition = (conditionId: string, subConditionId: string, newSubCondition: Condition) => void
|
||||
export type HandleToggleSubVariableConditionLogicalOperator = (conditionId: string) => void
|
||||
|
||||
export type LoopVariable = {
|
||||
id: string
|
||||
label: string
|
||||
var_type: VarType
|
||||
value_type: ValueType
|
||||
value: any
|
||||
}
|
||||
export type LoopNodeType = CommonNodeType & {
|
||||
startNodeType?: BlockEnum
|
||||
start_node_id: string
|
||||
@ -73,4 +81,14 @@ export type LoopNodeType = CommonNodeType & {
|
||||
break_conditions?: Condition[]
|
||||
loop_count: number
|
||||
error_handle_mode: ErrorHandleMode // how to handle error in the iteration
|
||||
loop_variables?: LoopVariable[]
|
||||
}
|
||||
|
||||
export type HandleUpdateLoopVariable = (id: string, updateData: Partial<LoopVariable>) => void
|
||||
export type HandleRemoveLoopVariable = (id: string) => void
|
||||
|
||||
export type LoopVariablesComponentShape = {
|
||||
nodeId: string
|
||||
handleRemoveLoopVariable: HandleRemoveLoopVariable
|
||||
handleUpdateLoopVariable: HandleUpdateLoopVariable
|
||||
}
|
||||
|
@ -1,14 +1,17 @@
|
||||
import { useCallback } from 'react'
|
||||
import {
|
||||
useCallback,
|
||||
useRef,
|
||||
} from 'react'
|
||||
import produce from 'immer'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import { uuid4 } from '@sentry/utils'
|
||||
import { v4 as uuid4 } from 'uuid'
|
||||
import {
|
||||
useIsChatMode,
|
||||
useIsNodeInLoop,
|
||||
useNodesReadOnly,
|
||||
useWorkflow,
|
||||
} from '../../hooks'
|
||||
import { VarType } from '../../types'
|
||||
import { ValueType, VarType } from '../../types'
|
||||
import type { ErrorHandleMode, ValueSelector, Var } from '../../types'
|
||||
import useNodeCrud from '../_base/hooks/use-node-crud'
|
||||
import { getNodeInfoById, getNodeUsedVarPassToServerKey, getNodeUsedVars, isSystemVar, toNodeOutputVars } from '../_base/components/variable/utils'
|
||||
@ -27,6 +30,11 @@ const useConfig = (id: string, payload: LoopNodeType) => {
|
||||
const conversationVariables = useStore(s => s.conversationVariables)
|
||||
|
||||
const { inputs, setInputs } = useNodeCrud<LoopNodeType>(id, payload)
|
||||
const inputsRef = useRef(inputs)
|
||||
const handleInputsChange = useCallback((newInputs: LoopNodeType) => {
|
||||
inputsRef.current = newInputs
|
||||
setInputs(newInputs)
|
||||
}, [setInputs])
|
||||
|
||||
const filterInputVar = useCallback((varPayload: Var) => {
|
||||
return [VarType.array, VarType.arrayString, VarType.arrayNumber, VarType.arrayObject, VarType.arrayFile].includes(varPayload.type)
|
||||
@ -35,7 +43,7 @@ const useConfig = (id: string, payload: LoopNodeType) => {
|
||||
// output
|
||||
const { getLoopNodeChildren, getBeforeNodesInSameBranch } = useWorkflow()
|
||||
const beforeNodes = getBeforeNodesInSameBranch(id)
|
||||
const loopChildrenNodes = getLoopNodeChildren(id)
|
||||
const loopChildrenNodes = [{ id, data: payload } as any, ...getLoopNodeChildren(id)]
|
||||
const canChooseVarNodes = [...beforeNodes, ...loopChildrenNodes]
|
||||
const childrenNodeVars = toNodeOutputVars(loopChildrenNodes, isChatMode, undefined, [], conversationVariables)
|
||||
|
||||
@ -291,6 +299,43 @@ const useConfig = (id: string, payload: LoopNodeType) => {
|
||||
setInputs(newInputs)
|
||||
}, [inputs, setInputs])
|
||||
|
||||
const handleAddLoopVariable = useCallback(() => {
|
||||
const newInputs = produce(inputsRef.current, (draft) => {
|
||||
if (!draft.loop_variables)
|
||||
draft.loop_variables = []
|
||||
|
||||
draft.loop_variables.push({
|
||||
id: uuid4(),
|
||||
label: '',
|
||||
var_type: VarType.string,
|
||||
value_type: ValueType.constant,
|
||||
value: '',
|
||||
})
|
||||
})
|
||||
handleInputsChange(newInputs)
|
||||
}, [handleInputsChange])
|
||||
|
||||
const handleRemoveLoopVariable = useCallback((id: string) => {
|
||||
const newInputs = produce(inputsRef.current, (draft) => {
|
||||
draft.loop_variables = draft.loop_variables?.filter(item => item.id !== id)
|
||||
})
|
||||
handleInputsChange(newInputs)
|
||||
}, [handleInputsChange])
|
||||
|
||||
const handleUpdateLoopVariable = useCallback((id: string, updateData: any) => {
|
||||
const loopVariables = inputsRef.current.loop_variables || []
|
||||
const index = loopVariables.findIndex(item => item.id === id)
|
||||
const newInputs = produce(inputsRef.current, (draft) => {
|
||||
if (index > -1) {
|
||||
draft.loop_variables![index] = {
|
||||
...draft.loop_variables![index],
|
||||
...updateData,
|
||||
}
|
||||
}
|
||||
})
|
||||
handleInputsChange(newInputs)
|
||||
}, [handleInputsChange])
|
||||
|
||||
return {
|
||||
readOnly,
|
||||
inputs,
|
||||
@ -325,6 +370,9 @@ const useConfig = (id: string, payload: LoopNodeType) => {
|
||||
handleToggleSubVariableConditionLogicalOperator,
|
||||
handleUpdateLoopCount,
|
||||
changeErrorResponseMode,
|
||||
handleAddLoopVariable,
|
||||
handleRemoveLoopVariable,
|
||||
handleUpdateLoopVariable,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -6,7 +6,10 @@ import type {
|
||||
BlockEnum,
|
||||
Node,
|
||||
} from '../../types'
|
||||
import { generateNewNode } from '../../utils'
|
||||
import {
|
||||
generateNewNode,
|
||||
getNodeCustomTypeByNodeDataType,
|
||||
} from '../../utils'
|
||||
import {
|
||||
LOOP_PADDING,
|
||||
NODES_INITIAL_DATA,
|
||||
@ -114,7 +117,7 @@ export const useNodeLoopInteractions = () => {
|
||||
const childNodeType = child.data.type as BlockEnum
|
||||
const nodesWithSameType = nodes.filter(node => node.data.type === childNodeType)
|
||||
const { newNode } = generateNewNode({
|
||||
|
||||
type: getNodeCustomTypeByNodeDataType(childNodeType),
|
||||
data: {
|
||||
...NODES_INITIAL_DATA[childNodeType],
|
||||
...child.data,
|
||||
|
@ -1,4 +1,7 @@
|
||||
import { memo } from 'react'
|
||||
import {
|
||||
memo,
|
||||
useMemo,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import cn from '@/utils/classnames'
|
||||
import { VarBlockIcon } from '@/app/components/workflow/block-icon'
|
||||
@ -7,7 +10,6 @@ import { Variable02 } from '@/app/components/base/icons/src/vender/solid/develop
|
||||
import { BubbleX, Env } from '@/app/components/base/icons/src/vender/line/others'
|
||||
import Badge from '@/app/components/base/badge'
|
||||
import type { Node } from '@/app/components/workflow/types'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
|
||||
type NodeVariableItemProps = {
|
||||
isEnv: boolean
|
||||
@ -33,38 +35,75 @@ const NodeVariableItem = ({
|
||||
isException,
|
||||
}: NodeVariableItemProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const VariableIcon = useMemo(() => {
|
||||
if (isEnv) {
|
||||
return (
|
||||
<Env className='h-3.5 w-3.5 shrink-0 text-util-colors-violet-violet-600' />
|
||||
)
|
||||
}
|
||||
|
||||
if (isChatVar) {
|
||||
return (
|
||||
<BubbleX className='h-3.5 w-3.5 shrink-0 text-util-colors-teal-teal-700' />
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Variable02
|
||||
className={cn(
|
||||
'h-3.5 w-3.5 shrink-0 text-text-accent',
|
||||
isException && 'text-text-warning',
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}, [isEnv, isChatVar, isException])
|
||||
|
||||
const VariableName = useMemo(() => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'system-xs-medium ml-0.5 shrink truncate text-text-accent',
|
||||
isEnv && 'text-gray-900',
|
||||
isException && 'text-text-warning',
|
||||
isChatVar && 'text-util-colors-teal-teal-700',
|
||||
)}
|
||||
title={varName}
|
||||
>
|
||||
{varName}
|
||||
</div>
|
||||
)
|
||||
}, [isEnv, isChatVar, varName, isException])
|
||||
return (
|
||||
<div className={cn(
|
||||
'relative flex items-center gap-1 self-stretch rounded-md bg-workflow-block-parma-bg p-[3px] pl-[5px]',
|
||||
showBorder && '!bg-black/[0.02]',
|
||||
className,
|
||||
)}>
|
||||
{!isEnv && !isChatVar && (
|
||||
<div className='flex items-center'>
|
||||
<div className='p-[1px]'>
|
||||
<VarBlockIcon
|
||||
className='!text-gray-900'
|
||||
type={node?.data.type || BlockEnum.Start}
|
||||
/>
|
||||
</div>
|
||||
<div className='mx-0.5 max-w-[85px] truncate text-xs font-medium text-gray-700' title={node?.data.title}>{node?.data.title}</div>
|
||||
<Line3 className='mr-0.5'></Line3>
|
||||
</div>
|
||||
)}
|
||||
<div className='flex w-full items-center text-primary-600'>
|
||||
{!isEnv && !isChatVar && <Variable02 className={cn('h-3.5 w-3.5 shrink-0 text-primary-500', isException && 'text-text-warning')} />}
|
||||
{isEnv && <Env className='h-3.5 w-3.5 shrink-0 text-util-colors-violet-violet-600' />}
|
||||
{!isChatVar && <div className={cn('system-xs-medium ml-0.5 max-w-[75px] overflow-hidden truncate text-ellipsis', isEnv && 'text-gray-900', isException && 'text-text-warning')} title={varName}>{varName}</div>}
|
||||
{isChatVar
|
||||
&& <div className='flex w-full items-center gap-1'>
|
||||
<div className='flex h-[18px] min-w-[18px] flex-1 items-center gap-0.5'>
|
||||
<BubbleX className='h-3.5 w-3.5 text-util-colors-teal-teal-700' />
|
||||
<div className={cn('system-xs-medium ml-0.5 max-w-[75px] overflow-hidden truncate text-ellipsis text-util-colors-teal-teal-700')}>{varName}</div>
|
||||
</div>
|
||||
{writeMode && <Badge className='shrink-0' text={t(`${i18nPrefix}.operations.${writeMode}`)} />}
|
||||
</div>
|
||||
<div className='flex w-0 grow items-center'>
|
||||
{
|
||||
node && (
|
||||
<>
|
||||
<div className='shrink-0 p-[1px]'>
|
||||
<VarBlockIcon
|
||||
className='!text-gray-900'
|
||||
type={node.data.type}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className='mx-0.5 shrink-[1000] truncate text-xs font-medium text-gray-700'
|
||||
title={node?.data.title}
|
||||
>
|
||||
{node?.data.title}
|
||||
</div>
|
||||
<Line3 className='mr-0.5 shrink-0'></Line3>
|
||||
</>
|
||||
)
|
||||
}
|
||||
{VariableIcon}
|
||||
{VariableName}
|
||||
</div>
|
||||
{writeMode && <Badge className='shrink-0' text={t(`${i18nPrefix}.operations.${writeMode}`)} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import type { OffsetOptions } from '@floating-ui/react'
|
||||
import {
|
||||
generateNewNode,
|
||||
getNodeCustomTypeByNodeDataType,
|
||||
} from '../utils'
|
||||
import {
|
||||
useAvailableBlocks,
|
||||
@ -56,6 +57,7 @@ const AddBlock = ({
|
||||
const nodes = getNodes()
|
||||
const nodesWithSameType = nodes.filter(node => node.data.type === type)
|
||||
const { newNode } = generateNewNode({
|
||||
type: getNodeCustomTypeByNodeDataType(type),
|
||||
data: {
|
||||
...NODES_INITIAL_DATA[type],
|
||||
title: nodesWithSameType.length > 0 ? `${t(`workflow.blocks.${type}`)} ${nodesWithSameType.length + 1}` : t(`workflow.blocks.${type}`),
|
||||
|
@ -8,6 +8,7 @@ import type {
|
||||
AgentLogItemWithChildren,
|
||||
IterationDurationMap,
|
||||
LoopDurationMap,
|
||||
LoopVariableMap,
|
||||
NodeTracing,
|
||||
} from '@/types/workflow'
|
||||
|
||||
@ -40,10 +41,12 @@ export const useLogs = () => {
|
||||
}] = useBoolean(false)
|
||||
const [loopResultList, setLoopResultList] = useState<NodeTracing[][]>([])
|
||||
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()
|
||||
setLoopResultList(detail)
|
||||
setLoopResultDurationMap(loopDurationMap)
|
||||
setLoopResultVariableMap(loopVariableMap)
|
||||
}, [setShowLoopingDetailTrue, setLoopResultList, setLoopResultDurationMap])
|
||||
|
||||
const [agentOrToolLogItemStack, setAgentOrToolLogItemStack] = useState<AgentLogItemWithChildren[]>([])
|
||||
@ -101,6 +104,8 @@ export const useLogs = () => {
|
||||
setLoopResultList,
|
||||
loopResultDurationMap,
|
||||
setLoopResultDurationMap,
|
||||
loopResultVariableMap,
|
||||
setLoopResultVariableMap,
|
||||
handleShowLoopResultList,
|
||||
|
||||
agentOrToolLogItemStack,
|
||||
|
@ -3,13 +3,14 @@ import { RiArrowRightSLine } from '@remixicon/react'
|
||||
import Button from '@/app/components/base/button'
|
||||
import type {
|
||||
LoopDurationMap,
|
||||
LoopVariableMap,
|
||||
NodeTracing,
|
||||
} from '@/types/workflow'
|
||||
import { Loop } from '@/app/components/base/icons/src/vender/workflow'
|
||||
|
||||
type LoopLogTriggerProps = {
|
||||
nodeInfo: NodeTracing
|
||||
onShowLoopResultList: (loopResultList: NodeTracing[][], loopResultDurationMap: LoopDurationMap) => void
|
||||
onShowLoopResultList: (loopResultList: NodeTracing[][], loopResultDurationMap: LoopDurationMap, loopVariableMap: LoopVariableMap) => void
|
||||
}
|
||||
const LoopLogTrigger = ({
|
||||
nodeInfo,
|
||||
@ -35,7 +36,11 @@ const LoopLogTrigger = ({
|
||||
const handleOnShowLoopDetail = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation()
|
||||
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 (
|
||||
<Button
|
||||
|
@ -12,19 +12,23 @@ import { NodeRunningStatus } from '@/app/components/workflow/types'
|
||||
import TracingPanel from '@/app/components/workflow/run/tracing-panel'
|
||||
import { Loop } from '@/app/components/base/icons/src/vender/workflow'
|
||||
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'
|
||||
|
||||
type Props = {
|
||||
list: NodeTracing[][]
|
||||
onBack: () => void
|
||||
loopDurationMap?: LoopDurationMap
|
||||
loopVariableMap?: LoopVariableMap
|
||||
}
|
||||
|
||||
const LoopResultPanel: FC<Props> = ({
|
||||
list,
|
||||
onBack,
|
||||
loopDurationMap,
|
||||
loopVariableMap,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [expandedLoops, setExpandedLoops] = useState<Record<number, boolean>>({})
|
||||
@ -114,6 +118,20 @@ const LoopResultPanel: FC<Props> = ({
|
||||
'overflow-hidden transition-all duration-200',
|
||||
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
|
||||
list={loop}
|
||||
className='bg-background-section-burn'
|
||||
|
@ -1,7 +1,7 @@
|
||||
'use client'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { FC } from 'react'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import {
|
||||
RiAlertFill,
|
||||
RiArrowRightSLine,
|
||||
@ -23,6 +23,7 @@ import type {
|
||||
AgentLogItemWithChildren,
|
||||
IterationDurationMap,
|
||||
LoopDurationMap,
|
||||
LoopVariableMap,
|
||||
NodeTracing,
|
||||
} from '@/types/workflow'
|
||||
import ErrorHandleTip from '@/app/components/workflow/nodes/_base/components/error-handle/error-handle-tip'
|
||||
@ -35,7 +36,7 @@ type Props = {
|
||||
hideInfo?: boolean
|
||||
hideProcessDetail?: boolean
|
||||
onShowIterationDetail?: (detail: NodeTracing[][], iterDurationMap: IterationDurationMap) => void
|
||||
onShowLoopDetail?: (detail: NodeTracing[][], loopDurationMap: LoopDurationMap) => void
|
||||
onShowLoopDetail?: (detail: NodeTracing[][], loopDurationMap: LoopDurationMap, loopVariableMap: LoopVariableMap) => void
|
||||
onShowRetryDetail?: (detail: NodeTracing[]) => void
|
||||
onShowAgentOrToolLog?: (detail?: AgentLogItemWithChildren) => void
|
||||
notShowIterationNav?: boolean
|
||||
@ -90,6 +91,20 @@ const NodePanel: FC<Props> = ({
|
||||
const isAgentNode = nodeInfo.node_type === BlockEnum.Agent && !!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 (
|
||||
<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'>
|
||||
@ -199,7 +214,7 @@ const NodePanel: FC<Props> = ({
|
||||
<div className={cn('mb-1')}>
|
||||
<CodeEditor
|
||||
readOnly
|
||||
title={<div>{t('workflow.common.input').toLocaleUpperCase()}</div>}
|
||||
title={<div>{inputsTitle}</div>}
|
||||
language={CodeLanguage.json}
|
||||
value={nodeInfo.inputs}
|
||||
isJSONStringifyBeauty
|
||||
@ -210,7 +225,7 @@ const NodePanel: FC<Props> = ({
|
||||
<div className={cn('mb-1')}>
|
||||
<CodeEditor
|
||||
readOnly
|
||||
title={<div>{t('workflow.common.processData').toLocaleUpperCase()}</div>}
|
||||
title={<div>{processDataTitle}</div>}
|
||||
language={CodeLanguage.json}
|
||||
value={nodeInfo.process_data}
|
||||
isJSONStringifyBeauty
|
||||
@ -221,7 +236,7 @@ const NodePanel: FC<Props> = ({
|
||||
<div>
|
||||
<CodeEditor
|
||||
readOnly
|
||||
title={<div>{t('workflow.common.output').toLocaleUpperCase()}</div>}
|
||||
title={<div>{outputTitle}</div>}
|
||||
language={CodeLanguage.json}
|
||||
value={nodeInfo.outputs}
|
||||
isJSONStringifyBeauty
|
||||
|
@ -6,6 +6,7 @@ import type {
|
||||
AgentLogItemWithChildren,
|
||||
IterationDurationMap,
|
||||
LoopDurationMap,
|
||||
LoopVariableMap,
|
||||
NodeTracing,
|
||||
} from '@/types/workflow'
|
||||
|
||||
@ -23,6 +24,7 @@ export type SpecialResultPanelProps = {
|
||||
setShowLoopingDetailFalse?: () => void
|
||||
loopResultList?: NodeTracing[][]
|
||||
loopResultDurationMap?: LoopDurationMap
|
||||
loopResultVariableMap?: LoopVariableMap
|
||||
|
||||
agentOrToolLogItemStack?: AgentLogItemWithChildren[]
|
||||
agentOrToolLogListMap?: Record<string, AgentLogItemWithChildren[]>
|
||||
@ -42,6 +44,7 @@ const SpecialResultPanel = ({
|
||||
setShowLoopingDetailFalse,
|
||||
loopResultList,
|
||||
loopResultDurationMap,
|
||||
loopResultVariableMap,
|
||||
|
||||
agentOrToolLogItemStack,
|
||||
agentOrToolLogListMap,
|
||||
@ -75,6 +78,7 @@ const SpecialResultPanel = ({
|
||||
list={loopResultList}
|
||||
onBack={setShowLoopingDetailFalse}
|
||||
loopDurationMap={loopResultDurationMap}
|
||||
loopVariableMap={loopResultVariableMap}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -86,6 +86,7 @@ const TracingPanel: FC<TracingPanelProps> = ({
|
||||
setShowLoopingDetailFalse,
|
||||
loopResultList,
|
||||
loopResultDurationMap,
|
||||
loopResultVariableMap,
|
||||
handleShowLoopResultList,
|
||||
|
||||
agentOrToolLogItemStack,
|
||||
@ -172,6 +173,7 @@ const TracingPanel: FC<TracingPanelProps> = ({
|
||||
setShowLoopingDetailFalse={setShowLoopingDetailFalse}
|
||||
loopResultList={loopResultList}
|
||||
loopResultDurationMap={loopResultDurationMap}
|
||||
loopResultVariableMap={loopResultVariableMap}
|
||||
|
||||
agentOrToolLogItemStack={agentOrToolLogItemStack}
|
||||
agentOrToolLogListMap={agentOrToolLogListMap}
|
||||
|
1
web/app/components/workflow/simple-node/constants.ts
Normal file
1
web/app/components/workflow/simple-node/constants.ts
Normal file
@ -0,0 +1 @@
|
||||
export const CUSTOM_SIMPLE_NODE = 'custom-simple'
|
148
web/app/components/workflow/simple-node/index.tsx
Normal file
148
web/app/components/workflow/simple-node/index.tsx
Normal file
@ -0,0 +1,148 @@
|
||||
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
web/app/components/workflow/simple-node/types.ts
Normal file
3
web/app/components/workflow/simple-node/types.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import type { CommonNodeType } from '@/app/components/workflow/types'
|
||||
|
||||
export type SimpleNodeType = CommonNodeType
|
@ -14,6 +14,7 @@ import type {
|
||||
ErrorHandleTypeEnum,
|
||||
} from '@/app/components/workflow/nodes/_base/components/error-handle/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 {
|
||||
Start = 'start',
|
||||
@ -38,6 +39,7 @@ export enum BlockEnum {
|
||||
Agent = 'agent',
|
||||
Loop = 'loop',
|
||||
LoopStart = 'loop-start',
|
||||
LoopEnd = 'loop-end',
|
||||
}
|
||||
|
||||
export enum ControlMode {
|
||||
@ -64,7 +66,7 @@ export type CommonNodeType<T = {}> = {
|
||||
_singleRunningStatus?: NodeRunningStatus
|
||||
_isCandidate?: boolean
|
||||
_isBundled?: boolean
|
||||
_children?: string[]
|
||||
_children?: { nodeId: string; nodeType: BlockEnum }[]
|
||||
_isEntering?: boolean
|
||||
_showAddVariablePopup?: boolean
|
||||
_holdAddVariablePopup?: boolean
|
||||
@ -256,16 +258,23 @@ export enum VarType {
|
||||
any = 'any',
|
||||
}
|
||||
|
||||
export enum ValueType {
|
||||
variable = 'variable',
|
||||
constant = 'constant',
|
||||
}
|
||||
|
||||
export type Var = {
|
||||
variable: string
|
||||
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
|
||||
isSelect?: boolean
|
||||
options?: string[]
|
||||
required?: boolean
|
||||
des?: string
|
||||
isException?: boolean
|
||||
isLoopVariable?: boolean
|
||||
nodeId?: string
|
||||
}
|
||||
|
||||
export type NodeOutPutVar = {
|
||||
@ -273,6 +282,7 @@ export type NodeOutPutVar = {
|
||||
title: string
|
||||
vars: Var[]
|
||||
isStartNode?: boolean
|
||||
isLoop?: boolean
|
||||
}
|
||||
|
||||
export type Block = {
|
||||
|
@ -49,6 +49,7 @@ import type { LoopNodeType } from './nodes/loop/types'
|
||||
import { CollectionType } from '@/app/components/tools/types'
|
||||
import { toolParametersToFormSchemas } from '@/app/components/tools/utils/to-form-schema'
|
||||
import { canFindTool, correctModelProvider } from '@/utils'
|
||||
import { CUSTOM_SIMPLE_NODE } from '@/app/components/workflow/simple-node/constants'
|
||||
|
||||
const WHITE = 'WHITE'
|
||||
const GRAY = 'GRAY'
|
||||
@ -165,7 +166,7 @@ export function generateNewNode({ data, position, id, zIndex, type, ...rest }: O
|
||||
if (data.type === BlockEnum.Iteration) {
|
||||
const newIterationStartNode = getIterationStartNode(newNode.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 {
|
||||
newNode,
|
||||
newIterationStartNode,
|
||||
@ -175,7 +176,7 @@ export function generateNewNode({ data, position, id, zIndex, type, ...rest }: O
|
||||
if (data.type === BlockEnum.Loop) {
|
||||
const newLoopStartNode = getLoopStartNode(newNode.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 {
|
||||
newNode,
|
||||
newLoopStartNode,
|
||||
@ -317,12 +318,12 @@ export const initialNodes = (originNodes: Node[], originEdges: Edge[]) => {
|
||||
const iterationOrLoopNodeMap = nodes.reduce((acc, node) => {
|
||||
if (node.parentId) {
|
||||
if (acc[node.parentId])
|
||||
acc[node.parentId].push(node.id)
|
||||
acc[node.parentId].push({ nodeId: node.id, nodeType: node.data.type })
|
||||
else
|
||||
acc[node.parentId] = [node.id]
|
||||
acc[node.parentId] = [{ nodeId: node.id, nodeType: node.data.type }]
|
||||
}
|
||||
return acc
|
||||
}, {} as Record<string, string[]>)
|
||||
}, {} as Record<string, { nodeId: string; nodeType: BlockEnum }[]>)
|
||||
|
||||
return nodes.map((node) => {
|
||||
if (!node.type)
|
||||
@ -1052,3 +1053,8 @@ export const isExceptionVariable = (variable: string, nodeType?: BlockEnum) => {
|
||||
export const hasRetryNode = (nodeType?: BlockEnum) => {
|
||||
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
|
||||
}
|
||||
|
@ -248,6 +248,7 @@ const translation = {
|
||||
'agent': 'Agent',
|
||||
'loop-start': 'Loop Start',
|
||||
'loop': 'Loop',
|
||||
'loop-end': 'Exit Loop',
|
||||
},
|
||||
blocksAbout: {
|
||||
'start': 'Define the initial parameters for launching a workflow',
|
||||
@ -265,6 +266,7 @@ const translation = {
|
||||
'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.',
|
||||
'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.',
|
||||
'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.',
|
||||
@ -712,6 +714,16 @@ const translation = {
|
||||
continueOnError: 'Continue on Error',
|
||||
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: {
|
||||
addNote: 'Add Note',
|
||||
|
@ -249,6 +249,7 @@ const translation = {
|
||||
'agent': 'Agent',
|
||||
'loop-start': '循环开始',
|
||||
'loop': '循环',
|
||||
'loop-end': '退出循环',
|
||||
},
|
||||
blocksAbout: {
|
||||
'start': '定义一个 workflow 流程启动的初始参数',
|
||||
@ -266,6 +267,7 @@ const translation = {
|
||||
'variable-aggregator': '将多路分支的变量聚合为一个变量,以实现下游节点统一配置。',
|
||||
'iteration': '对列表对象执行多次步骤直至输出所有结果。',
|
||||
'loop': '循环执行一段逻辑直到满足结束条件或者到达循环次数上限。',
|
||||
'loop-end': '相当于“break” 此节点没有配置项,当循环体内运行到此节点后循环终止。',
|
||||
'parameter-extractor': '利用 LLM 从自然语言内推理提取出结构化参数,用于后置的工具调用或 HTTP 请求。',
|
||||
'document-extractor': '用于将用户上传的文档解析为 LLM 便于理解的文本内容。',
|
||||
'list-operator': '用于过滤或排序数组内容。',
|
||||
@ -713,6 +715,16 @@ const translation = {
|
||||
continueOnError: '忽略错误并继续',
|
||||
removeAbnormalOutput: '移除错误输出',
|
||||
},
|
||||
loopVariables: '循环变量',
|
||||
initialLoopVariables: '初始循环变量',
|
||||
finalLoopVariables: '最终循环变量',
|
||||
setLoopVariables: '在循环范围内设置变量',
|
||||
variableName: '变量名',
|
||||
inputMode: '输入模式',
|
||||
exitConditionTip: '循环节点至少需要一个退出条件',
|
||||
loopNode: '循环节点',
|
||||
currentLoopCount: '当前循环次数:{{count}}',
|
||||
totalLoopCount: '总循环次数:{{count}}',
|
||||
},
|
||||
note: {
|
||||
addNote: '添加注释',
|
||||
|
@ -61,6 +61,7 @@ export type NodeTracing = {
|
||||
agent_strategy?: string
|
||||
icon?: string
|
||||
}
|
||||
loop_variable_map?: Record<string, any>
|
||||
}
|
||||
metadata: {
|
||||
iterator_length: number
|
||||
@ -334,6 +335,7 @@ export type ConversationVariableResponse = {
|
||||
|
||||
export type IterationDurationMap = Record<string, number>
|
||||
export type LoopDurationMap = Record<string, number>
|
||||
export type LoopVariableMap = Record<string, any>
|
||||
|
||||
export type WorkflowConfigResponse = {
|
||||
parallel_depth_limit: number
|
||||
|
Loading…
x
Reference in New Issue
Block a user