From 713902dc47973e1ae831cd1a257e254c482d9da0 Mon Sep 17 00:00:00 2001 From: zxhlyh Date: Tue, 1 Apr 2025 16:52:07 +0800 Subject: [PATCH] Feat/loop break node (#17268) --- web/app/account/account-page/index.tsx | 2 +- web/app/account/avatar.tsx | 2 +- .../icons/assets/vender/workflow/loop-end.svg | 5 + .../icons/src/public/education/Triangle.tsx | 14 +- .../icons/src/vender/workflow/LoopEnd.json | 38 +++++ .../icons/src/vender/workflow/LoopEnd.tsx | 20 +++ .../base/icons/src/vender/workflow/index.ts | 1 + web/app/components/base/select/pure.tsx | 157 ++++++++++++++++++ .../header/account-dropdown/index.tsx | 2 +- .../components/header/plan-badge/index.tsx | 4 +- web/app/components/workflow/block-icon.tsx | 3 + .../workflow/block-selector/blocks.tsx | 11 +- .../workflow/block-selector/constants.tsx | 6 + web/app/components/workflow/constants.ts | 16 ++ .../workflow/hooks/use-nodes-data.ts | 7 + .../workflow/hooks/use-nodes-interactions.ts | 31 ++-- .../use-workflow-node-iteration-finished.ts | 14 ++ .../use-workflow-node-loop-finished.ts | 17 +- .../use-workflow-node-loop-next.ts | 20 +-- .../use-workflow-node-loop-started.ts | 3 - web/app/components/workflow/index.tsx | 3 + .../nodes/_base/components/help-link.tsx | 3 + .../panel-operator/panel-operator-popup.tsx | 2 +- .../nodes/_base/components/variable-tag.tsx | 3 +- .../nodes/_base/components/variable/utils.ts | 155 +++++++++++++++-- .../variable/var-reference-picker.tsx | 2 +- .../variable/var-reference-vars.tsx | 9 +- .../_base/hooks/use-available-var-list.ts | 6 +- .../nodes/_base/hooks/use-node-help-link.ts | 13 +- .../components/workflow/nodes/_base/node.tsx | 34 +++- .../assigner/components/var-list/index.tsx | 5 +- .../workflow/nodes/assigner/node.tsx | 1 - .../workflow/nodes/assigner/use-config.ts | 14 +- .../nodes/iteration/use-interactions.ts | 6 +- .../components/workflow/nodes/llm/types.ts | 47 ++++++ .../workflow/nodes/loop-end/default.ts | 23 +++ .../nodes/loop/components/condition-wrap.tsx | 3 - .../loop/components/loop-variables/empty.tsx | 13 ++ .../components/loop-variables/form-item.tsx | 144 ++++++++++++++++ .../loop/components/loop-variables/index.tsx | 28 ++++ .../loop-variables/input-mode-selec.tsx | 37 +++++ .../loop/components/loop-variables/item.tsx | 78 +++++++++ .../loop-variables/variable-type-select.tsx | 51 ++++++ .../components/workflow/nodes/loop/default.ts | 5 + .../components/workflow/nodes/loop/panel.tsx | 29 +++- .../components/workflow/nodes/loop/types.ts | 18 ++ .../workflow/nodes/loop/use-config.ts | 56 ++++++- .../workflow/nodes/loop/use-interactions.ts | 7 +- .../components/node-variable-item.tsx | 91 +++++++--- .../workflow/operator/add-block.tsx | 2 + web/app/components/workflow/run/hooks.ts | 7 +- .../run/loop-log/loop-log-trigger.tsx | 9 +- .../run/loop-log/loop-result-panel.tsx | 20 ++- web/app/components/workflow/run/node.tsx | 25 ++- .../workflow/run/special-result-panel.tsx | 4 + .../components/workflow/run/tracing-panel.tsx | 2 + .../workflow/simple-node/constants.ts | 1 + .../components/workflow/simple-node/index.tsx | 148 +++++++++++++++++ .../components/workflow/simple-node/types.ts | 3 + web/app/components/workflow/types.ts | 14 +- web/app/components/workflow/utils.ts | 16 +- web/i18n/en-US/workflow.ts | 12 ++ web/i18n/zh-Hans/workflow.ts | 12 ++ web/types/workflow.ts | 2 + 64 files changed, 1397 insertions(+), 139 deletions(-) create mode 100644 web/app/components/base/icons/assets/vender/workflow/loop-end.svg create mode 100644 web/app/components/base/icons/src/vender/workflow/LoopEnd.json create mode 100644 web/app/components/base/icons/src/vender/workflow/LoopEnd.tsx create mode 100644 web/app/components/base/select/pure.tsx create mode 100644 web/app/components/workflow/nodes/loop-end/default.ts create mode 100644 web/app/components/workflow/nodes/loop/components/loop-variables/empty.tsx create mode 100644 web/app/components/workflow/nodes/loop/components/loop-variables/form-item.tsx create mode 100644 web/app/components/workflow/nodes/loop/components/loop-variables/index.tsx create mode 100644 web/app/components/workflow/nodes/loop/components/loop-variables/input-mode-selec.tsx create mode 100644 web/app/components/workflow/nodes/loop/components/loop-variables/item.tsx create mode 100644 web/app/components/workflow/nodes/loop/components/loop-variables/variable-type-select.tsx create mode 100644 web/app/components/workflow/simple-node/constants.ts create mode 100644 web/app/components/workflow/simple-node/index.tsx create mode 100644 web/app/components/workflow/simple-node/types.ts diff --git a/web/app/account/account-page/index.tsx b/web/app/account/account-page/index.tsx index d09a8c2cfe..72d2648c23 100644 --- a/web/app/account/account-page/index.tsx +++ b/web/app/account/account-page/index.tsx @@ -144,7 +144,7 @@ export default function AccountPage() { {userProfile.name} {isEducationAccount && ( - + EDU )} diff --git a/web/app/account/avatar.tsx b/web/app/account/avatar.tsx index 63db0f37dc..ea897e639f 100644 --- a/web/app/account/avatar.tsx +++ b/web/app/account/avatar.tsx @@ -78,7 +78,7 @@ export default function AppSelector() { {userProfile.name} {isEducationAccount && ( - + EDU )} diff --git a/web/app/components/base/icons/assets/vender/workflow/loop-end.svg b/web/app/components/base/icons/assets/vender/workflow/loop-end.svg new file mode 100644 index 0000000000..cedacb9c5d --- /dev/null +++ b/web/app/components/base/icons/assets/vender/workflow/loop-end.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/web/app/components/base/icons/src/public/education/Triangle.tsx b/web/app/components/base/icons/src/public/education/Triangle.tsx index 34f2a50666..85aa518ad2 100644 --- a/web/app/components/base/icons/src/public/education/Triangle.tsx +++ b/web/app/components/base/icons/src/public/education/Triangle.tsx @@ -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, Omit>(( - props, - ref, -) => ) +const Icon = ( + { + ref, + ...props + }: React.SVGProps & { + ref?: React.RefObject>; + }, +) => Icon.displayName = 'Triangle' diff --git a/web/app/components/base/icons/src/vender/workflow/LoopEnd.json b/web/app/components/base/icons/src/vender/workflow/LoopEnd.json new file mode 100644 index 0000000000..1427dfdcc5 --- /dev/null +++ b/web/app/components/base/icons/src/vender/workflow/LoopEnd.json @@ -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" +} \ No newline at end of file diff --git a/web/app/components/base/icons/src/vender/workflow/LoopEnd.tsx b/web/app/components/base/icons/src/vender/workflow/LoopEnd.tsx new file mode 100644 index 0000000000..0b8f71d2d8 --- /dev/null +++ b/web/app/components/base/icons/src/vender/workflow/LoopEnd.tsx @@ -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 & { + ref?: React.RefObject>; + }, +) => + +Icon.displayName = 'LoopEnd' + +export default Icon diff --git a/web/app/components/base/icons/src/vender/workflow/index.ts b/web/app/components/base/icons/src/vender/workflow/index.ts index 284be92712..7167b71b44 100644 --- a/web/app/components/base/icons/src/vender/workflow/index.ts +++ b/web/app/components/base/icons/src/vender/workflow/index.ts @@ -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' diff --git a/web/app/components/base/select/pure.tsx b/web/app/components/base/select/pure.tsx new file mode 100644 index 0000000000..81cc2fbadf --- /dev/null +++ b/web/app/components/base/select/pure.tsx @@ -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 ( + + handleOpenChange(!mergedOpen)} + asChild + > +
+
+ {triggerText} +
+ +
+
+ +
+ { + popupTitle && ( +
+ {popupTitle} +
+ ) + } + { + options.map(option => ( +
{ + onChange?.(option.value) + handleOpenChange(false) + }} + > +
+ {option.label} +
+ { + value === option.value && + } +
+ )) + } +
+
+
+ ) +} + +export default PureSelect diff --git a/web/app/components/header/account-dropdown/index.tsx b/web/app/components/header/account-dropdown/index.tsx index 1a0cc96b98..66b61d7ec1 100644 --- a/web/app/components/header/account-dropdown/index.tsx +++ b/web/app/components/header/account-dropdown/index.tsx @@ -94,7 +94,7 @@ export default function AppSelector() { {userProfile.name} {isEducationAccount && ( - + EDU )} diff --git a/web/app/components/header/plan-badge/index.tsx b/web/app/components/header/plan-badge/index.tsx index 37cbe2a710..292025caeb 100644 --- a/web/app/components/header/plan-badge/index.tsx +++ b/web/app/components/header/plan-badge/index.tsx @@ -42,8 +42,8 @@ const PlanBadge: FC = ({ plan, allowHover, sandboxAsUpgrade = fa if (plan === Plan.professional) { return
- - {isEducationWorkspace && } + + {isEducationWorkspace && } pro
diff --git a/web/app/components/workflow/block-icon.tsx b/web/app/components/workflow/block-icon.tsx index fa8b56546a..1e76efc2aa 100644 --- a/web/app/components/workflow/block-icon.tsx +++ b/web/app/components/workflow/block-icon.tsx @@ -16,6 +16,7 @@ import { ListFilter, Llm, Loop, + LoopEnd, ParameterExtractor, QuestionClassifier, TemplatingTransform, @@ -54,6 +55,7 @@ const getIcon = (type: BlockEnum, className: string) => { [BlockEnum.Iteration]: , [BlockEnum.LoopStart]: , [BlockEnum.Loop]: , + [BlockEnum.LoopEnd]: , [BlockEnum.ParameterExtractor]: , [BlockEnum.DocExtractor]: , [BlockEnum.ListFilter]: , @@ -68,6 +70,7 @@ const ICON_CONTAINER_BG_COLOR_MAP: Record = { [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', diff --git a/web/app/components/workflow/block-selector/blocks.tsx b/web/app/components/workflow/block-selector/blocks.tsx index f97c1a261a..4182530a91 100644 --- a/web/app/components/workflow/block-selector/blocks.tsx +++ b/web/app/components/workflow/block-selector/blocks.tsx @@ -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} /> -
{block.title}
+
{block.title}
+ { + block.type === BlockEnum.LoopEnd && ( + + ) + } )) diff --git a/web/app/components/workflow/block-selector/constants.tsx b/web/app/components/workflow/block-selector/constants.tsx index 7e8a7f7a3e..680cbf45b9 100644 --- a/web/app/components/workflow/block-selector/constants.tsx +++ b/web/app/components/workflow/block-selector/constants.tsx @@ -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, diff --git a/web/app/components/workflow/constants.ts b/web/app/components/workflow/constants.ts index fce79cb1df..cdfd963cfa 100644 --- a/web/app/components/workflow/constants.ts +++ b/web/app/components/workflow/constants.ts @@ -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 = { 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: '', diff --git a/web/app/components/workflow/hooks/use-nodes-data.ts b/web/app/components/workflow/hooks/use-nodes-data.ts index c68ce92e34..aeb45ddb93 100644 --- a/web/app/components/workflow/hooks/use-nodes-data.ts +++ b/web/app/components/workflow/hooks/use-nodes-data.ts @@ -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 }), } diff --git a/web/app/components/workflow/hooks/use-nodes-interactions.ts b/web/app/components/workflow/hooks/use-nodes-interactions.ts index 6dc7f7e948..90231cfcc8 100644 --- a/web/app/components/workflow/hooks/use-nodes-interactions.ts +++ b/web/app/components/workflow/hooks/use-nodes-interactions.ts @@ -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 diff --git a/web/app/components/workflow/hooks/use-workflow-run-event/use-workflow-node-iteration-finished.ts b/web/app/components/workflow/hooks/use-workflow-run-event/use-workflow-node-iteration-finished.ts index fdf9e28587..491628cf0b 100644 --- a/web/app/components/workflow/hooks/use-workflow-run-event/use-workflow-node-iteration-finished.ts +++ b/web/app/components/workflow/hooks/use-workflow-run-event/use-workflow-node-iteration-finished.ts @@ -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 { diff --git a/web/app/components/workflow/hooks/use-workflow-run-event/use-workflow-node-loop-finished.ts b/web/app/components/workflow/hooks/use-workflow-run-event/use-workflow-node-loop-finished.ts index 38064e3658..0efe530807 100644 --- a/web/app/components/workflow/hooks/use-workflow-run-event/use-workflow-node-loop-finished.ts +++ b/web/app/components/workflow/hooks/use-workflow-run-event/use-workflow-node-loop-finished.ts @@ -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 { diff --git a/web/app/components/workflow/hooks/use-workflow-run-event/use-workflow-node-loop-next.ts b/web/app/components/workflow/hooks/use-workflow-run-event/use-workflow-node-loop-next.ts index d3c5164dcb..8525e2838b 100644 --- a/web/app/components/workflow/hooks/use-workflow-run-event/use-workflow-node-loop-next.ts +++ b/web/app/components/workflow/hooks/use-workflow-run-event/use-workflow-node-loop-next.ts @@ -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, diff --git a/web/app/components/workflow/hooks/use-workflow-run-event/use-workflow-node-loop-started.ts b/web/app/components/workflow/hooks/use-workflow-run-event/use-workflow-node-loop-started.ts index 533154bc33..1745f43b60 100644 --- a/web/app/components/workflow/hooks/use-workflow-run-event/use-workflow-node-loop-started.ts +++ b/web/app/components/workflow/hooks/use-workflow-run-event/use-workflow-node-loop-started.ts @@ -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, diff --git a/web/app/components/workflow/index.tsx b/web/app/components/workflow/index.tsx index 0c4d5aa671..4c48afb56c 100644 --- a/web/app/components/workflow/index.tsx +++ b/web/app/components/workflow/index.tsx @@ -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, } diff --git a/web/app/components/workflow/nodes/_base/components/help-link.tsx b/web/app/components/workflow/nodes/_base/components/help-link.tsx index 34520f633c..2e7552001b 100644 --- a/web/app/components/workflow/nodes/_base/components/help-link.tsx +++ b/web/app/components/workflow/nodes/_base/components/help-link.tsx @@ -14,6 +14,9 @@ const HelpLink = ({ const { t } = useTranslation() const link = useNodeHelpLink(nodeType) + if (!link) + return null + return (
{ } 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, filterVar: (payload: Var, selector: ValueSelector) => boolean): Record => { + 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 } diff --git a/web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.tsx b/web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.tsx index 0b8e0bbd57..1647642950 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.tsx @@ -101,7 +101,7 @@ const VarReferencePicker: FC = ({ const isChatMode = useIsChatMode() const { getCurrentVariableType } = useWorkflowVariables() - const { availableNodes, availableVars } = useAvailableVarList(nodeId, { + const { availableVars, availableNodesWithParent: availableNodes } = useAvailableVarList(nodeId, { onlyLeafNodeVar, passedInAvailableNodes, filterVar, diff --git a/web/app/components/workflow/nodes/_base/components/variable/var-reference-vars.tsx b/web/app/components/workflow/nodes/_base/components/variable/var-reference-vars.tsx index 6971cf2208..dfd4d19c00 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/var-reference-vars.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable/var-reference-vars.tsx @@ -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 = ({ @@ -50,6 +52,7 @@ const Item: FC = ({ 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 = ({ onMouseDown={e => e.preventDefault()} >
- {!isEnv && !isChatVar && } + {!isEnv && !isChatVar && !isLoopVar && } {isEnv && } - {isChatVar && } + {isChatVar && } + {isLoopVar && } {!isEnv && !isChatVar && (
{itemData.variable}
)} @@ -317,6 +321,7 @@ const VarReferenceVars: FC = ({ itemWidth={itemWidth} isSupportFileVar={isSupportFileVar} isException={v.isException} + isLoopVar={item.isLoop} /> ))}
)) diff --git a/web/app/components/workflow/nodes/_base/hooks/use-available-var-list.ts b/web/app/components/workflow/nodes/_base/hooks/use-available-var-list.ts index 3e13dcb786..e1a6a8bd8d 100644 --- a/web/app/components/workflow/nodes/_base/hooks/use-available-var-list.ts +++ b/web/app/components/workflow/nodes/_base/hooks/use-available-var-list.ts @@ -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, } } diff --git a/web/app/components/workflow/nodes/_base/hooks/use-node-help-link.ts b/web/app/components/workflow/nodes/_base/hooks/use-node-help-link.ts index 0509848101..3c68fbd1fd 100644 --- a/web/app/components/workflow/nodes/_base/hooks/use-node-help-link.ts +++ b/web/app/components/workflow/nodes/_base/hooks/use-node-help-link.ts @@ -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 - return `${prefixLink}${linkMap[nodeType]}` + const link = linkMap[nodeType] + + if (!link) + return '' + + return `${prefixLink}${link}` } diff --git a/web/app/components/workflow/nodes/_base/node.tsx b/web/app/components/workflow/nodes/_base/node.tsx index 177e76e5ab..527b2f094d 100644 --- a/web/app/components/workflow/nodes/_base/node.tsx +++ b/web/app/components/workflow/nodes/_base/node.tsx @@ -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 = ({ @@ -104,6 +104,30 @@ const BaseNode: FC = ({ } }, [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 ( +
+ {text} +
+ ) + } + + return null + }, [data._loopIndex, data._runningStatus, t]) + return (
= ({ ) } { - data._loopLength && data._loopIndex && data._runningStatus === NodeRunningStatus.Running && ( -
- {data._loopIndex > data._loopLength ? data._loopLength : data._loopIndex}/{data._loopLength} -
- ) + data.type === BlockEnum.Loop && data._loopIndex && LoopIndex } { (data._runningStatus === NodeRunningStatus.Running || data._singleRunningStatus === NodeRunningStatus.Running) && ( diff --git a/web/app/components/workflow/nodes/assigner/components/var-list/index.tsx b/web/app/components/workflow/nodes/assigner/components/var-list/index.tsx index 1632072eba..e334c0b281 100644 --- a/web/app/components/workflow/nodes/assigner/components/var-list/index.tsx +++ b/web/app/components/workflow/nodes/assigner/components/var-list/index.tsx @@ -95,10 +95,13 @@ const VarList: FC = ({ }, [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 diff --git a/web/app/components/workflow/nodes/assigner/node.tsx b/web/app/components/workflow/nodes/assigner/node.tsx index 870ba9873c..2dd1ead4f8 100644 --- a/web/app/components/workflow/nodes/assigner/node.tsx +++ b/web/app/components/workflow/nodes/assigner/node.tsx @@ -13,7 +13,6 @@ const NodeComponent: FC> = ({ data, }) => { const { t } = useTranslation() - const nodes: Node[] = useNodes() if (data.version === '2') { const { items: operationItems } = data diff --git a/web/app/components/workflow/nodes/assigner/use-config.ts b/web/app/components/workflow/nodes/assigner/use-config.ts index ad7d066ef1..e7beb1f37a 100644 --- a/web/app/components/workflow/nodes/assigner/use-config.ts +++ b/web/app/components/workflow/nodes/assigner/use-config.ts @@ -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(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') }, []) diff --git a/web/app/components/workflow/nodes/iteration/use-interactions.ts b/web/app/components/workflow/nodes/iteration/use-interactions.ts index c0c005bdf6..c294cfd6aa 100644 --- a/web/app/components/workflow/nodes/iteration/use-interactions.ts +++ b/web/app/components/workflow/nodes/iteration/use-interactions.ts @@ -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, diff --git a/web/app/components/workflow/nodes/llm/types.ts b/web/app/components/workflow/nodes/llm/types.ts index a7774fca2e..a4931f7017 100644 --- a/web/app/components/workflow/nodes/llm/types.ts +++ b/web/app/components/workflow/nodes/llm/types.ts @@ -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 +export type ArrayItems = Omit & { 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 + required?: string[] + additionalProperties: false } diff --git a/web/app/components/workflow/nodes/loop-end/default.ts b/web/app/components/workflow/nodes/loop-end/default.ts new file mode 100644 index 0000000000..c136704123 --- /dev/null +++ b/web/app/components/workflow/nodes/loop-end/default.ts @@ -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 = { + 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 diff --git a/web/app/components/workflow/nodes/loop/components/condition-wrap.tsx b/web/app/components/workflow/nodes/loop/components/condition-wrap.tsx index 7a80dc2812..7aef364658 100644 --- a/web/app/components/workflow/nodes/loop/components/condition-wrap.tsx +++ b/web/app/components/workflow/nodes/loop/components/condition-wrap.tsx @@ -138,9 +138,6 @@ const ConditionWrap: FC = ({ )}
- {!isSubVariable && ( -
- )}
) diff --git a/web/app/components/workflow/nodes/loop/components/loop-variables/empty.tsx b/web/app/components/workflow/nodes/loop/components/loop-variables/empty.tsx new file mode 100644 index 0000000000..6fe4aa0a77 --- /dev/null +++ b/web/app/components/workflow/nodes/loop/components/loop-variables/empty.tsx @@ -0,0 +1,13 @@ +import { useTranslation } from 'react-i18next' + +const Empty = () => { + const { t } = useTranslation() + + return ( +
+ {t('workflow.nodes.loop.setLoopVariables')} +
+ ) +} + +export default Empty diff --git a/web/app/components/workflow/nodes/loop/components/loop-variables/form-item.tsx b/web/app/components/workflow/nodes/loop/components/loop-variables/form-item.tsx new file mode 100644 index 0000000000..4a05e457b3 --- /dev/null +++ b/web/app/components/workflow/nodes/loop/components/loop-variables/form-item.tsx @@ -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 ( +
+ { + value_type === ValueType.variable && ( + + ) + } + { + value_type === ValueType.constant && var_type === VarType.string && ( +