diff --git a/web/app/components/base/prompt-editor/plugins/workflow-variable-block/component.tsx b/web/app/components/base/prompt-editor/plugins/workflow-variable-block/component.tsx index 65f3dad3a21f6e..0073ac300b7a90 100644 --- a/web/app/components/base/prompt-editor/plugins/workflow-variable-block/component.tsx +++ b/web/app/components/base/prompt-editor/plugins/workflow-variable-block/component.tsx @@ -26,6 +26,7 @@ import { VarBlockIcon } from '@/app/components/workflow/block-icon' import { Line3 } from '@/app/components/base/icons/src/public/common' import { isConversationVar, isENV, isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils' import Tooltip from '@/app/components/base/tooltip' +import { isExceptionVariable } from '@/app/components/workflow/utils' type WorkflowVariableBlockComponentProps = { nodeKey: string @@ -53,6 +54,7 @@ const WorkflowVariableBlockComponent = ({ const node = localWorkflowNodesMap![variables[0]] const isEnv = isENV(variables) const isChatVar = isConversationVar(variables) + const isException = isExceptionVariable(varName, node?.type) useEffect(() => { if (!editor.hasNodes([WorkflowVariableBlockNode])) @@ -98,10 +100,10 @@ const WorkflowVariableBlockComponent = ({ )}
- {!isEnv && !isChatVar && } + {!isEnv && !isChatVar && } {isEnv && } {isChatVar && } -
{varName}
+
{varName}
{ !node && !isEnv && !isChatVar && ( diff --git a/web/app/components/workflow/custom-edge-linear-gradient-render.tsx b/web/app/components/workflow/custom-edge-linear-gradient-render.tsx new file mode 100644 index 00000000000000..b799bb36b298d7 --- /dev/null +++ b/web/app/components/workflow/custom-edge-linear-gradient-render.tsx @@ -0,0 +1,53 @@ +type CustomEdgeLinearGradientRenderProps = { + id: string + startColor: string + stopColor: string + position: { + x1: number + x2: number + y1: number + y2: number + } +} +const CustomEdgeLinearGradientRender = ({ + id, + startColor, + stopColor, + position, +}: CustomEdgeLinearGradientRenderProps) => { + const { + x1, + x2, + y1, + y2, + } = position + return ( + + + + + + + ) +} + +export default CustomEdgeLinearGradientRender diff --git a/web/app/components/workflow/custom-edge.tsx b/web/app/components/workflow/custom-edge.tsx index 68e2ef945e8b57..ce95549055540d 100644 --- a/web/app/components/workflow/custom-edge.tsx +++ b/web/app/components/workflow/custom-edge.tsx @@ -1,6 +1,7 @@ import { memo, useCallback, + useMemo, useState, } from 'react' import { intersection } from 'lodash-es' @@ -20,8 +21,12 @@ import type { Edge, OnSelectBlock, } from './types' +import { NodeRunningStatus } from './types' +import { getEdgeColor } from './utils' import { ITERATION_CHILDREN_Z_INDEX } from './constants' +import CustomEdgeLinearGradientRender from './custom-edge-linear-gradient-render' import cn from '@/utils/classnames' +import { ErrorHandleTypeEnum } from '@/app/components/workflow/nodes/_base/components/error-handle/types' const CustomEdge = ({ id, @@ -53,6 +58,26 @@ const CustomEdge = ({ const { handleNodeAdd } = useNodesInteractions() const { availablePrevBlocks } = useAvailableBlocks((data as Edge['data'])!.targetType, (data as Edge['data'])?.isInIteration) const { availableNextBlocks } = useAvailableBlocks((data as Edge['data'])!.sourceType, (data as Edge['data'])?.isInIteration) + const { + _sourceRunningStatus, + _targetRunningStatus, + } = data + + const linearGradientId = useMemo(() => { + if ( + ( + _sourceRunningStatus === NodeRunningStatus.Succeeded + || _sourceRunningStatus === NodeRunningStatus.Failed + || _sourceRunningStatus === NodeRunningStatus.Exception + ) && ( + _targetRunningStatus === NodeRunningStatus.Succeeded + || _targetRunningStatus === NodeRunningStatus.Failed + || _targetRunningStatus === NodeRunningStatus.Exception + || _targetRunningStatus === NodeRunningStatus.Running + ) + ) + return id + }, [_sourceRunningStatus, _targetRunningStatus, id]) const handleOpenChange = useCallback((v: boolean) => { setOpen(v) @@ -73,14 +98,43 @@ const CustomEdge = ({ ) }, [handleNodeAdd, source, sourceHandleId, target, targetHandleId]) + const stroke = useMemo(() => { + if (selected) + return getEdgeColor(NodeRunningStatus.Running) + + if (linearGradientId) + return `url(#${linearGradientId})` + + if (data?._connectedNodeIsHovering) + return getEdgeColor(NodeRunningStatus.Running, sourceHandleId === ErrorHandleTypeEnum.failBranch) + + return getEdgeColor() + }, [data._connectedNodeIsHovering, linearGradientId, selected, sourceHandleId]) + return ( <> + { + linearGradientId && ( + + ) + } @@ -95,6 +149,7 @@ const CustomEdge = ({ position: 'absolute', transform: `translate(-50%, -50%) translate(${labelX}px, ${labelY}px)`, pointerEvents: 'all', + opacity: data._waitingRun ? 0.7 : 1, }} > { edges, setEdges, } = store.getState() - const currentEdgeIndex = edges.findIndex(edge => edge.source === nodeId && edge.sourceHandle === branchId) + const edgeWillBeDeleted = edges.filter(edge => edge.source === nodeId && edge.sourceHandle === branchId) - if (currentEdgeIndex < 0) + if (!edgeWillBeDeleted.length) return - const currentEdge = edges[currentEdgeIndex] - const newNodes = produce(getNodes(), (draft: Node[]) => { - const sourceNode = draft.find(node => node.id === currentEdge.source) - const targetNode = draft.find(node => node.id === currentEdge.target) - - if (sourceNode) - sourceNode.data._connectedSourceHandleIds = sourceNode.data._connectedSourceHandleIds?.filter(handleId => handleId !== currentEdge.sourceHandle) - - if (targetNode) - targetNode.data._connectedTargetHandleIds = targetNode.data._connectedTargetHandleIds?.filter(handleId => handleId !== currentEdge.targetHandle) + const nodes = getNodes() + const nodesConnectedSourceOrTargetHandleIdsMap = getNodesConnectedSourceOrTargetHandleIdsMap( + edgeWillBeDeleted.map(edge => ({ type: 'remove', edge })), + nodes, + ) + const newNodes = produce(nodes, (draft: Node[]) => { + draft.forEach((node) => { + if (nodesConnectedSourceOrTargetHandleIdsMap[node.id]) { + node.data = { + ...node.data, + ...nodesConnectedSourceOrTargetHandleIdsMap[node.id], + } + } + }) }) setNodes(newNodes) const newEdges = produce(edges, (draft) => { - draft.splice(currentEdgeIndex, 1) + return draft.filter(edge => !edgeWillBeDeleted.find(e => e.id === edge.id)) }) setEdges(newEdges) handleSyncWorkflowDraft() @@ -155,7 +159,9 @@ export const useEdgesInteractions = () => { const newEdges = produce(edges, (draft) => { draft.forEach((edge) => { - edge.data._run = false + edge.data._sourceRunningStatus = undefined + edge.data._targetRunningStatus = undefined + edge.data._waitingRun = false }) }) setEdges(newEdges) diff --git a/web/app/components/workflow/hooks/use-nodes-interactions.ts b/web/app/components/workflow/hooks/use-nodes-interactions.ts index 375a269377166a..8962333311d151 100644 --- a/web/app/components/workflow/hooks/use-nodes-interactions.ts +++ b/web/app/components/workflow/hooks/use-nodes-interactions.ts @@ -1033,6 +1033,7 @@ export const useNodesInteractions = () => { const newNodes = produce(nodes, (draft) => { draft.forEach((node) => { node.data._runningStatus = undefined + node.data._waitingRun = false }) }) setNodes(newNodes) diff --git a/web/app/components/workflow/hooks/use-workflow-run.ts b/web/app/components/workflow/hooks/use-workflow-run.ts index 5fbca27791397e..f6a9d24cd336ee 100644 --- a/web/app/components/workflow/hooks/use-workflow-run.ts +++ b/web/app/components/workflow/hooks/use-workflow-run.ts @@ -1,6 +1,5 @@ import { useCallback } from 'react' import { - getIncomers, useReactFlow, useStoreApi, } from 'reactflow' @@ -9,8 +8,8 @@ import { v4 as uuidV4 } from 'uuid' import { usePathname } from 'next/navigation' import { useWorkflowStore } from '../store' import { useNodesSyncDraft } from '../hooks' -import type { Node } from '../types' import { + BlockEnum, NodeRunningStatus, WorkflowRunningStatus, } from '../types' @@ -28,6 +27,7 @@ import { AudioPlayerManager } from '@/app/components/base/audio-btn/audio.player import { getFilesInLogs, } from '@/app/components/base/file-uploader/utils' +import { ErrorHandleTypeEnum } from '@/app/components/workflow/nodes/_base/components/error-handle/types' export const useWorkflowRun = () => { const store = useStoreApi() @@ -174,6 +174,8 @@ export const useWorkflowRun = () => { setIterParallelLogMap, } = workflowStore.getState() const { + getNodes, + setNodes, edges, setEdges, } = store.getState() @@ -186,12 +188,20 @@ export const useWorkflowRun = () => { status: WorkflowRunningStatus.Running, } })) - + const nodes = getNodes() + const newNodes = produce(nodes, (draft) => { + draft.forEach((node) => { + node.data._waitingRun = true + }) + }) + setNodes(newNodes) const newEdges = produce(edges, (draft) => { draft.forEach((edge) => { edge.data = { ...edge.data, - _run: false, + _sourceRunningStatus: undefined, + _targetRunningStatus: undefined, + _waitingRun: true, } }) }) @@ -311,13 +321,27 @@ export const useWorkflowRun = () => { } const newNodes = produce(nodes, (draft) => { draft[currentNodeIndex].data._runningStatus = NodeRunningStatus.Running + draft[currentNodeIndex].data._waitingRun = false }) setNodes(newNodes) - const incomeNodesId = getIncomers({ id: data.node_id } as Node, newNodes, edges).filter(node => node.data._runningStatus === NodeRunningStatus.Succeeded).map(node => node.id) const newEdges = produce(edges, (draft) => { - draft.forEach((edge) => { - if (edge.target === data.node_id && incomeNodesId.includes(edge.source)) - edge.data = { ...edge.data, _run: true } as any + const incomeEdges = draft.filter((edge) => { + return edge.target === data.node_id + }) + + incomeEdges.forEach((edge) => { + const incomeNode = nodes.find(node => node.id === edge.source)! + if ( + (!incomeNode.data._runningBranchId && edge.sourceHandle === 'source') + || (incomeNode.data._runningBranchId && edge.sourceHandle === incomeNode.data._runningBranchId) + ) { + edge.data = { + ...edge.data, + _sourceRunningStatus: incomeNode.data._runningStatus, + _targetRunningStatus: NodeRunningStatus.Running, + _waitingRun: false, + } + } }) }) setEdges(newEdges) @@ -336,6 +360,8 @@ export const useWorkflowRun = () => { const { getNodes, setNodes, + edges, + setEdges, } = store.getState() const nodes = getNodes() const nodeParentId = nodes.find(node => node.id === data.node_id)!.parentId @@ -423,8 +449,31 @@ export const useWorkflowRun = () => { const newNodes = produce(nodes, (draft) => { const currentNode = draft.find(node => node.id === data.node_id)! currentNode.data._runningStatus = data.status as any + if (data.status === NodeRunningStatus.Exception) { + if (data.execution_metadata.error_strategy === ErrorHandleTypeEnum.failBranch) + currentNode.data._runningBranchId = ErrorHandleTypeEnum.failBranch + } + else { + if (data.node_type === BlockEnum.IfElse) + currentNode.data._runningBranchId = data?.outputs?.selected_case_id + + if (data.node_type === BlockEnum.QuestionClassifier) + currentNode.data._runningBranchId = data?.outputs?.class_id + } }) 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) prevNodeId = data.node_id } @@ -474,13 +523,20 @@ export const useWorkflowRun = () => { const newNodes = produce(nodes, (draft) => { draft[currentNodeIndex].data._runningStatus = NodeRunningStatus.Running draft[currentNodeIndex].data._iterationLength = data.metadata.iterator_length + draft[currentNodeIndex].data._waitingRun = false }) setNodes(newNodes) const newEdges = produce(edges, (draft) => { - const edge = draft.find(edge => edge.target === data.node_id && edge.source === prevNodeId) + const incomeEdges = draft.filter(edge => edge.target === data.node_id) - if (edge) - edge.data = { ...edge.data, _run: true } as any + incomeEdges.forEach((edge) => { + edge.data = { + ...edge.data, + _sourceRunningStatus: nodes.find(node => node.id === edge.source)!.data._runningStatus, + _targetRunningStatus: NodeRunningStatus.Running, + _waitingRun: false, + } + }) }) setEdges(newEdges) diff --git a/web/app/components/workflow/nodes/_base/components/before-run-form/index.tsx b/web/app/components/workflow/nodes/_base/components/before-run-form/index.tsx index 79d9c5b4dd8217..92a4deb51381b6 100644 --- a/web/app/components/workflow/nodes/_base/components/before-run-form/index.tsx +++ b/web/app/components/workflow/nodes/_base/components/before-run-form/index.tsx @@ -59,7 +59,7 @@ const BeforeRunForm: FC = ({ }) => { const { t } = useTranslation() - const isFinished = runningStatus === NodeRunningStatus.Succeeded || runningStatus === NodeRunningStatus.Failed + const isFinished = runningStatus === NodeRunningStatus.Succeeded || runningStatus === NodeRunningStatus.Failed || runningStatus === NodeRunningStatus.Exception const isRunning = runningStatus === NodeRunningStatus.Running const isFileLoaded = (() => { // system files diff --git a/web/app/components/workflow/nodes/_base/components/collapse/field-collapse.tsx b/web/app/components/workflow/nodes/_base/components/collapse/field-collapse.tsx new file mode 100644 index 00000000000000..7d2698a6d0956d --- /dev/null +++ b/web/app/components/workflow/nodes/_base/components/collapse/field-collapse.tsx @@ -0,0 +1,26 @@ +import Collapse from '.' + +type FieldCollapseProps = { + title: string + children: JSX.Element +} +const FieldCollapse = ({ + title, + children, +}: FieldCollapseProps) => { + return ( +
+ {title}
+ } + > +
+ {children} +
+ +
+ ) +} + +export default FieldCollapse diff --git a/web/app/components/workflow/nodes/_base/components/collapse/index.tsx b/web/app/components/workflow/nodes/_base/components/collapse/index.tsx new file mode 100644 index 00000000000000..a798ff0a9e8570 --- /dev/null +++ b/web/app/components/workflow/nodes/_base/components/collapse/index.tsx @@ -0,0 +1,56 @@ +import { useState } from 'react' +import { RiArrowDropRightLine } from '@remixicon/react' +import cn from '@/utils/classnames' + +export { default as FieldCollapse } from './field-collapse' + +type CollapseProps = { + disabled?: boolean + trigger: JSX.Element + children: JSX.Element + collapsed?: boolean + onCollapse?: (collapsed: boolean) => void +} +const Collapse = ({ + disabled, + trigger, + children, + collapsed, + onCollapse, +}: CollapseProps) => { + const [collapsedLocal, setCollapsedLocal] = useState(true) + const collapsedMerged = collapsed !== undefined ? collapsed : collapsedLocal + + return ( + <> +
{ + if (!disabled) { + setCollapsedLocal(!collapsedMerged) + onCollapse?.(!collapsedMerged) + } + }} + > +
+ { + !disabled && ( + + ) + } +
+ {trigger} +
+ { + !collapsedMerged && children + } + + ) +} + +export default Collapse diff --git a/web/app/components/workflow/nodes/_base/components/editor/base.tsx b/web/app/components/workflow/nodes/_base/components/editor/base.tsx index 18ec5ea4a354e3..37bae03c996ab5 100644 --- a/web/app/components/workflow/nodes/_base/components/editor/base.tsx +++ b/web/app/components/workflow/nodes/_base/components/editor/base.tsx @@ -33,6 +33,7 @@ type Props = { }[] showFileList?: boolean showCodeGenerator?: boolean + tip?: JSX.Element } const Base: FC = ({ @@ -49,6 +50,7 @@ const Base: FC = ({ fileList = [], showFileList, showCodeGenerator = false, + tip, }) => { const ref = useRef(null) const { @@ -100,6 +102,7 @@ const Base: FC = ({ + {tip &&
{tip}
} void showCodeGenerator?: boolean className?: string + tip?: JSX.Element } export const languageMap = { @@ -69,6 +70,7 @@ const CodeEditor: FC = ({ onGenerated, showCodeGenerator = false, className, + tip, }) => { const [isFocus, setIsFocus] = React.useState(false) const [isMounted, setIsMounted] = React.useState(false) @@ -211,6 +213,7 @@ const CodeEditor: FC = ({ fileList={fileList as any} showFileList={showFileList} showCodeGenerator={showCodeGenerator} + tip={tip} > {main} diff --git a/web/app/components/workflow/nodes/_base/components/error-handle/default-value.tsx b/web/app/components/workflow/nodes/_base/components/error-handle/default-value.tsx new file mode 100644 index 00000000000000..45c23fcc183ec6 --- /dev/null +++ b/web/app/components/workflow/nodes/_base/components/error-handle/default-value.tsx @@ -0,0 +1,89 @@ +import { useCallback } from 'react' +import { useTranslation } from 'react-i18next' +import type { DefaultValueForm } from './types' +import Input from '@/app/components/base/input' +import { VarType } from '@/app/components/workflow/types' +import { CodeLanguage } from '@/app/components/workflow/nodes/code/types' +import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor' + +type DefaultValueProps = { + forms: DefaultValueForm[] + onFormChange: (form: DefaultValueForm) => void +} +const DefaultValue = ({ + forms, + onFormChange, +}: DefaultValueProps) => { + const { t } = useTranslation() + const getFormChangeHandler = useCallback(({ key, type }: DefaultValueForm) => { + return (payload: any) => { + let value + if (type === VarType.string || type === VarType.number) + value = payload.target.value + + if (type === VarType.array || type === VarType.arrayNumber || type === VarType.arrayString || type === VarType.arrayObject || type === VarType.arrayFile || type === VarType.object) + value = payload + + onFormChange({ key, type, value }) + } + }, [onFormChange]) + + return ( +
+
+ {t('workflow.nodes.common.errorHandle.defaultValue.desc')} +   + + {t('workflow.common.learnMore')} + +
+
+ { + forms.map((form, index) => { + return ( +
+
+
{form.key}
+
{form.type}
+
+ { + (form.type === VarType.string || form.type === VarType.number) && ( + + ) + } + { + ( + form.type === VarType.array + || form.type === VarType.arrayNumber + || form.type === VarType.arrayString + || form.type === VarType.arrayObject + || form.type === VarType.object + ) && ( + + ) + } +
+ ) + }) + } +
+
+ ) +} + +export default DefaultValue diff --git a/web/app/components/workflow/nodes/_base/components/error-handle/error-handle-on-node.tsx b/web/app/components/workflow/nodes/_base/components/error-handle/error-handle-on-node.tsx new file mode 100644 index 00000000000000..64ce9ec226bc7c --- /dev/null +++ b/web/app/components/workflow/nodes/_base/components/error-handle/error-handle-on-node.tsx @@ -0,0 +1,67 @@ +import { useEffect } from 'react' +import { useTranslation } from 'react-i18next' +import { useUpdateNodeInternals } from 'reactflow' +import { NodeSourceHandle } from '../node-handle' +import { ErrorHandleTypeEnum } from './types' +import type { Node } from '@/app/components/workflow/types' +import { NodeRunningStatus } from '@/app/components/workflow/types' +import cn from '@/utils/classnames' + +type ErrorHandleOnNodeProps = Pick +const ErrorHandleOnNode = ({ + id, + data, +}: ErrorHandleOnNodeProps) => { + const { t } = useTranslation() + const { error_strategy } = data + const updateNodeInternals = useUpdateNodeInternals() + + useEffect(() => { + if (error_strategy === ErrorHandleTypeEnum.failBranch) + updateNodeInternals(id) + }, [error_strategy, id, updateNodeInternals]) + + if (!error_strategy) + return null + + return ( +
+
+
+ {t('workflow.common.onFailure')} +
+
+ { + error_strategy === ErrorHandleTypeEnum.defaultValue && ( + t('workflow.nodes.common.errorHandle.defaultValue.output') + ) + } + { + error_strategy === ErrorHandleTypeEnum.failBranch && ( + t('workflow.nodes.common.errorHandle.failBranch.title') + ) + } +
+ { + error_strategy === ErrorHandleTypeEnum.failBranch && ( + + ) + } +
+
+ ) +} + +export default ErrorHandleOnNode diff --git a/web/app/components/workflow/nodes/_base/components/error-handle/error-handle-on-panel.tsx b/web/app/components/workflow/nodes/_base/components/error-handle/error-handle-on-panel.tsx new file mode 100644 index 00000000000000..f11f8bd5fb0784 --- /dev/null +++ b/web/app/components/workflow/nodes/_base/components/error-handle/error-handle-on-panel.tsx @@ -0,0 +1,90 @@ +import { useCallback } from 'react' +import { useTranslation } from 'react-i18next' +import Collapse from '../collapse' +import { ErrorHandleTypeEnum } from './types' +import ErrorHandleTypeSelector from './error-handle-type-selector' +import FailBranchCard from './fail-branch-card' +import DefaultValue from './default-value' +import { + useDefaultValue, + useErrorHandle, +} from './hooks' +import type { DefaultValueForm } from './types' +import type { + CommonNodeType, + Node, +} from '@/app/components/workflow/types' +import Split from '@/app/components/workflow/nodes/_base/components/split' +import Tooltip from '@/app/components/base/tooltip' + +type ErrorHandleProps = Pick +const ErrorHandle = ({ + id, + data, +}: ErrorHandleProps) => { + const { t } = useTranslation() + const { error_strategy, default_value } = data + const { + collapsed, + setCollapsed, + handleErrorHandleTypeChange, + } = useErrorHandle(id, data) + const { handleFormChange } = useDefaultValue(id) + + const getHandleErrorHandleTypeChange = useCallback((data: CommonNodeType) => { + return (value: ErrorHandleTypeEnum) => { + handleErrorHandleTypeChange(value, data) + } + }, [handleErrorHandleTypeChange]) + + const getHandleFormChange = useCallback((data: CommonNodeType) => { + return (v: DefaultValueForm) => { + handleFormChange(v, data) + } + }, [handleFormChange]) + + return ( + <> + +
+ +
+
+ {t('workflow.nodes.common.errorHandle.title')} +
+ +
+ +
+ } + > + <> + { + error_strategy === ErrorHandleTypeEnum.failBranch && !collapsed && ( + + ) + } + { + error_strategy === ErrorHandleTypeEnum.defaultValue && !collapsed && !!default_value?.length && ( + + ) + } + + + + + ) +} + +export default ErrorHandle diff --git a/web/app/components/workflow/nodes/_base/components/error-handle/error-handle-tip.tsx b/web/app/components/workflow/nodes/_base/components/error-handle/error-handle-tip.tsx new file mode 100644 index 00000000000000..3e60308ea77da4 --- /dev/null +++ b/web/app/components/workflow/nodes/_base/components/error-handle/error-handle-tip.tsx @@ -0,0 +1,43 @@ +import { useMemo } from 'react' +import { useTranslation } from 'react-i18next' +import { RiAlertFill } from '@remixicon/react' +import { ErrorHandleTypeEnum } from './types' + +type ErrorHandleTipProps = { + type?: ErrorHandleTypeEnum +} +const ErrorHandleTip = ({ + type, +}: ErrorHandleTipProps) => { + const { t } = useTranslation() + + const text = useMemo(() => { + if (type === ErrorHandleTypeEnum.failBranch) + return t('workflow.nodes.common.errorHandle.failBranch.inLog') + + if (type === ErrorHandleTypeEnum.defaultValue) + return t('workflow.nodes.common.errorHandle.defaultValue.inLog') + }, []) + + if (!type) + return null + + return ( +
+
+ +
+ {text} +
+
+ ) +} + +export default ErrorHandleTip diff --git a/web/app/components/workflow/nodes/_base/components/error-handle/error-handle-type-selector.tsx b/web/app/components/workflow/nodes/_base/components/error-handle/error-handle-type-selector.tsx new file mode 100644 index 00000000000000..dadfa8d0b0d317 --- /dev/null +++ b/web/app/components/workflow/nodes/_base/components/error-handle/error-handle-type-selector.tsx @@ -0,0 +1,95 @@ +import { useState } from 'react' +import { useTranslation } from 'react-i18next' +import { + RiArrowDownSLine, + RiCheckLine, +} from '@remixicon/react' +import { ErrorHandleTypeEnum } from './types' +import { + PortalToFollowElem, + PortalToFollowElemContent, + PortalToFollowElemTrigger, +} from '@/app/components/base/portal-to-follow-elem' +import Button from '@/app/components/base/button' + +type ErrorHandleTypeSelectorProps = { + value: ErrorHandleTypeEnum + onSelected: (value: ErrorHandleTypeEnum) => void +} +const ErrorHandleTypeSelector = ({ + value, + onSelected, +}: ErrorHandleTypeSelectorProps) => { + const { t } = useTranslation() + const [open, setOpen] = useState(false) + const options = [ + { + value: ErrorHandleTypeEnum.none, + label: t('workflow.nodes.common.errorHandle.none.title'), + description: t('workflow.nodes.common.errorHandle.none.desc'), + }, + { + value: ErrorHandleTypeEnum.defaultValue, + label: t('workflow.nodes.common.errorHandle.defaultValue.title'), + description: t('workflow.nodes.common.errorHandle.defaultValue.desc'), + }, + { + value: ErrorHandleTypeEnum.failBranch, + label: t('workflow.nodes.common.errorHandle.failBranch.title'), + description: t('workflow.nodes.common.errorHandle.failBranch.desc'), + }, + ] + const selectedOption = options.find(option => option.value === value) + + return ( + + { + e.stopPropagation() + setOpen(v => !v) + }}> + + + +
+ { + options.map(option => ( +
{ + e.stopPropagation() + onSelected(option.value) + setOpen(false) + }} + > +
+ { + value === option.value && ( + + ) + } +
+
+
{option.label}
+
{option.description}
+
+
+ )) + } +
+
+
+ ) +} + +export default ErrorHandleTypeSelector diff --git a/web/app/components/workflow/nodes/_base/components/error-handle/fail-branch-card.tsx b/web/app/components/workflow/nodes/_base/components/error-handle/fail-branch-card.tsx new file mode 100644 index 00000000000000..5dbba10f6c4f38 --- /dev/null +++ b/web/app/components/workflow/nodes/_base/components/error-handle/fail-branch-card.tsx @@ -0,0 +1,32 @@ +import { RiMindMap } from '@remixicon/react' +import { useTranslation } from 'react-i18next' + +const FailBranchCard = () => { + const { t } = useTranslation() + + return ( +
+
+
+ +
+
+ {t('workflow.nodes.common.errorHandle.failBranch.customize')} +
+
+ {t('workflow.nodes.common.errorHandle.failBranch.customizeTip')} +   + + {t('workflow.common.learnMore')} + +
+
+
+ ) +} + +export default FailBranchCard diff --git a/web/app/components/workflow/nodes/_base/components/error-handle/hooks.ts b/web/app/components/workflow/nodes/_base/components/error-handle/hooks.ts new file mode 100644 index 00000000000000..06eb4fc48fbf65 --- /dev/null +++ b/web/app/components/workflow/nodes/_base/components/error-handle/hooks.ts @@ -0,0 +1,123 @@ +import { + useCallback, + useMemo, + useState, +} from 'react' +import { ErrorHandleTypeEnum } from './types' +import type { DefaultValueForm } from './types' +import { getDefaultValue } from './utils' +import type { + CommonNodeType, +} from '@/app/components/workflow/types' +import { + useEdgesInteractions, + useNodeDataUpdate, +} from '@/app/components/workflow/hooks' + +export const useDefaultValue = ( + id: string, +) => { + const { handleNodeDataUpdateWithSyncDraft } = useNodeDataUpdate() + const handleFormChange = useCallback(( + { + key, + value, + type, + }: DefaultValueForm, + data: CommonNodeType, + ) => { + const default_value = data.default_value || [] + const index = default_value.findIndex(form => form.key === key) + + if (index > -1) { + const newDefaultValue = [...default_value] + newDefaultValue[index].value = value + handleNodeDataUpdateWithSyncDraft({ + id, + data: { + default_value: newDefaultValue, + }, + }) + return + } + + handleNodeDataUpdateWithSyncDraft({ + id, + data: { + default_value: [ + ...default_value, + { + key, + value, + type, + }, + ], + }, + }) + }, [handleNodeDataUpdateWithSyncDraft, id]) + + return { + handleFormChange, + } +} + +export const useErrorHandle = ( + id: string, + data: CommonNodeType, +) => { + const initCollapsed = useMemo(() => { + if (data.error_strategy === ErrorHandleTypeEnum.none) + return true + + return false + }, [data.error_strategy]) + const [collapsed, setCollapsed] = useState(initCollapsed) + const { handleNodeDataUpdateWithSyncDraft } = useNodeDataUpdate() + const { handleEdgeDeleteByDeleteBranch } = useEdgesInteractions() + + const handleErrorHandleTypeChange = useCallback((value: ErrorHandleTypeEnum, data: CommonNodeType) => { + if (data.error_strategy === value) + return + + if (value === ErrorHandleTypeEnum.none) { + handleNodeDataUpdateWithSyncDraft({ + id, + data: { + error_strategy: undefined, + default_value: undefined, + }, + }) + setCollapsed(true) + handleEdgeDeleteByDeleteBranch(id, ErrorHandleTypeEnum.failBranch) + } + + if (value === ErrorHandleTypeEnum.failBranch) { + handleNodeDataUpdateWithSyncDraft({ + id, + data: { + error_strategy: value, + default_value: undefined, + }, + }) + setCollapsed(false) + } + + if (value === ErrorHandleTypeEnum.defaultValue) { + handleNodeDataUpdateWithSyncDraft({ + id, + data: { + error_strategy: value, + default_value: getDefaultValue(data), + }, + }) + setCollapsed(false) + handleEdgeDeleteByDeleteBranch(id, ErrorHandleTypeEnum.failBranch) + } + }, [id, handleNodeDataUpdateWithSyncDraft, handleEdgeDeleteByDeleteBranch]) + + return { + collapsed, + setCollapsed, + handleErrorHandleTypeChange, + } +} diff --git a/web/app/components/workflow/nodes/_base/components/error-handle/types.ts b/web/app/components/workflow/nodes/_base/components/error-handle/types.ts new file mode 100644 index 00000000000000..29493641b0be01 --- /dev/null +++ b/web/app/components/workflow/nodes/_base/components/error-handle/types.ts @@ -0,0 +1,13 @@ +import type { VarType } from '@/app/components/workflow/types' + +export enum ErrorHandleTypeEnum { + none = 'none', + failBranch = 'fail-branch', + defaultValue = 'default-value', +} + +export type DefaultValueForm = { + key: string + type: VarType + value?: any +} diff --git a/web/app/components/workflow/nodes/_base/components/error-handle/utils.ts b/web/app/components/workflow/nodes/_base/components/error-handle/utils.ts new file mode 100644 index 00000000000000..eef9677c48e5d4 --- /dev/null +++ b/web/app/components/workflow/nodes/_base/components/error-handle/utils.ts @@ -0,0 +1,83 @@ +import type { CommonNodeType } from '@/app/components/workflow/types' +import { + BlockEnum, + VarType, +} from '@/app/components/workflow/types' +import type { CodeNodeType } from '@/app/components/workflow/nodes/code/types' + +const getDefaultValueByType = (type: VarType) => { + if (type === VarType.string) + return '' + + if (type === VarType.number) + return 0 + + if (type === VarType.object) + return '{}' + + if (type === VarType.arrayObject || type === VarType.arrayString || type === VarType.arrayNumber || type === VarType.arrayFile) + return '[]' + + return '' +} + +export const getDefaultValue = (data: CommonNodeType) => { + const { type } = data + + if (type === BlockEnum.LLM) { + return [{ + key: 'text', + type: VarType.string, + value: getDefaultValueByType(VarType.string), + }] + } + + if (type === BlockEnum.HttpRequest) { + return [ + { + key: 'body', + type: VarType.string, + value: getDefaultValueByType(VarType.string), + }, + { + key: 'status_code', + type: VarType.number, + value: getDefaultValueByType(VarType.number), + }, + { + key: 'headers', + type: VarType.object, + value: getDefaultValueByType(VarType.object), + }, + ] + } + + if (type === BlockEnum.Tool) { + return [ + { + key: 'text', + type: VarType.string, + value: getDefaultValueByType(VarType.string), + }, + { + key: 'json', + type: VarType.arrayObject, + value: getDefaultValueByType(VarType.arrayObject), + }, + ] + } + + if (type === BlockEnum.Code) { + const { outputs } = data as CodeNodeType + + return Object.keys(outputs).map((key) => { + return { + key, + type: outputs[key].type, + value: getDefaultValueByType(outputs[key].type), + } + }) + } + + return [] +} diff --git a/web/app/components/workflow/nodes/_base/components/next-step/add.tsx b/web/app/components/workflow/nodes/_base/components/next-step/add.tsx index 75694983cdcbd8..54ab4b327f0626 100644 --- a/web/app/components/workflow/nodes/_base/components/next-step/add.tsx +++ b/web/app/components/workflow/nodes/_base/components/next-step/add.tsx @@ -1,6 +1,7 @@ import { memo, useCallback, + useMemo, useState, } from 'react' import { useTranslation } from 'react-i18next' @@ -24,12 +25,14 @@ type AddProps = { nodeData: CommonNodeType sourceHandle: string isParallel?: boolean + isFailBranch?: boolean } const Add = ({ nodeId, nodeData, sourceHandle, isParallel, + isFailBranch, }: AddProps) => { const { t } = useTranslation() const [open, setOpen] = useState(false) @@ -58,6 +61,15 @@ const Add = ({ setOpen(newOpen) }, [checkParallelLimit, nodeId, sourceHandle]) + const tip = useMemo(() => { + if (isFailBranch) + return t('workflow.common.addFailureBranch') + + if (isParallel) + return t('workflow.common.addParallelNode') + + return t('workflow.panel.selectNextStep') + }, [isFailBranch, isParallel, t]) const renderTrigger = useCallback((open: boolean) => { return (
- { - isParallel - ? t('workflow.common.addParallelNode') - : t('workflow.panel.selectNextStep') - } + {tip}
) - }, [t, nodesReadOnly, isParallel]) + }, [nodesReadOnly, tip]) return ( { return ( -
+
{ branchName && (
{branchName} @@ -44,6 +53,7 @@ const Container = ({ } { return data._targetBranches || [] }, [data]) - const nodeWithBranches = data.type === BlockEnum.IfElse || data.type === BlockEnum.QuestionClassifier const edges = useEdges() const outgoers = getOutgoers(selectedNode as Node, store.getState().getNodes(), edges) const connectedEdges = getConnectedEdges([selectedNode] as Node[], edges).filter(edge => edge.source === selectedNode!.id) - const branchesOutgoers = useMemo(() => { - if (!branches?.length) - return [] + const list = useMemo(() => { + let items = [] + if (branches?.length) { + items = branches.map((branch, index) => { + const connected = connectedEdges.filter(edge => edge.sourceHandle === branch.id) + const nextNodes = connected.map(edge => outgoers.find(outgoer => outgoer.id === edge.target)!) - return branches.map((branch) => { - const connected = connectedEdges.filter(edge => edge.sourceHandle === branch.id) + return { + branch: { + ...branch, + name: data.type === BlockEnum.QuestionClassifier ? `${t('workflow.nodes.questionClassifiers.class')} ${index + 1}` : branch.name, + }, + nextNodes, + } + }) + } + else { + const connected = connectedEdges.filter(edge => edge.sourceHandle === 'source') const nextNodes = connected.map(edge => outgoers.find(outgoer => outgoer.id === edge.target)!) - return { - branch, + items = [{ + branch: { + id: '', + name: '', + }, nextNodes, + }] + + if (data.error_strategy === ErrorHandleTypeEnum.failBranch && hasErrorHandleNode(data.type)) { + const connected = connectedEdges.filter(edge => edge.sourceHandle === ErrorHandleTypeEnum.failBranch) + const nextNodes = connected.map(edge => outgoers.find(outgoer => outgoer.id === edge.target)!) + + items.push({ + branch: { + id: ErrorHandleTypeEnum.failBranch, + name: t('workflow.common.onFailure'), + }, + nextNodes, + }) } - }) - }, [branches, connectedEdges, outgoers]) + } + + return items + }, [branches, connectedEdges, data.error_strategy, data.type, outgoers, t]) return (
@@ -57,34 +88,23 @@ const NextStep = ({ />
item.nextNodes.length + 1) : [1]} + list={list.length ? list.map(item => item.nextNodes.length + 1) : [1]} />
{ - !nodeWithBranches && ( - - ) - } - { - nodeWithBranches && ( - branchesOutgoers.map((item, index) => { - return ( - - ) - }) - ) + list.map((item, index) => { + return ( + + ) + }) }
diff --git a/web/app/components/workflow/nodes/_base/components/node-handle.tsx b/web/app/components/workflow/nodes/_base/components/node-handle.tsx index 17dca45ebcc696..65798e46107856 100644 --- a/web/app/components/workflow/nodes/_base/components/node-handle.tsx +++ b/web/app/components/workflow/nodes/_base/components/node-handle.tsx @@ -10,7 +10,10 @@ import { Position, } from 'reactflow' import { useTranslation } from 'react-i18next' -import { BlockEnum } from '../../../types' +import { + BlockEnum, + NodeRunningStatus, +} from '../../../types' import type { Node } from '../../../types' import BlockSelector from '../../../block-selector' import type { ToolDefaultValue } from '../../../block-selector/types' @@ -24,11 +27,13 @@ import { import { useStore, } from '../../../store' +import cn from '@/utils/classnames' type NodeHandleProps = { handleId: string handleClassName?: string nodeSelectorClassName?: string + showExceptionStatus?: boolean } & Pick export const NodeTargetHandle = memo(({ @@ -72,14 +77,17 @@ export const NodeTargetHandle = memo(({ id={handleId} type='target' position={Position.Left} - className={` - !w-4 !h-4 !bg-transparent !rounded-none !outline-none !border-none z-[1] - after:absolute after:w-0.5 after:h-2 after:left-1.5 after:top-1 after:bg-primary-500 - hover:scale-125 transition-all - ${!connected && 'after:opacity-0'} - ${data.type === BlockEnum.Start && 'opacity-0'} - ${handleClassName} - `} + className={cn( + '!w-4 !h-4 !bg-transparent !rounded-none !outline-none !border-none z-[1]', + 'after:absolute after:w-0.5 after:h-2 after:left-1.5 after:top-1 after:bg-workflow-link-line-handle', + 'hover:scale-125 transition-all', + data._runningStatus === NodeRunningStatus.Succeeded && 'after:bg-workflow-link-line-success-handle', + data._runningStatus === NodeRunningStatus.Failed && 'after:bg-workflow-link-line-error-handle', + data._runningStatus === NodeRunningStatus.Exception && 'after:bg-workflow-link-line-failure-handle', + !connected && 'after:opacity-0', + data.type === BlockEnum.Start && 'opacity-0', + handleClassName, + )} isConnectable={isConnectable} onClick={handleHandleClick} > @@ -114,6 +122,7 @@ export const NodeSourceHandle = memo(({ handleId, handleClassName, nodeSelectorClassName, + showExceptionStatus, }: NodeHandleProps) => { const { t } = useTranslation() const notInitialWorkflow = useStore(s => s.notInitialWorkflow) @@ -157,13 +166,16 @@ export const NodeSourceHandle = memo(({ id={handleId} type='source' position={Position.Right} - className={` - group/handle !w-4 !h-4 !bg-transparent !rounded-none !outline-none !border-none z-[1] - after:absolute after:w-0.5 after:h-2 after:right-1.5 after:top-1 after:bg-primary-500 - hover:scale-125 transition-all - ${!connected && 'after:opacity-0'} - ${handleClassName} - `} + className={cn( + 'group/handle !w-4 !h-4 !bg-transparent !rounded-none !outline-none !border-none z-[1]', + 'after:absolute after:w-0.5 after:h-2 after:right-1.5 after:top-1 after:bg-workflow-link-line-handle', + 'hover:scale-125 transition-all', + data._runningStatus === NodeRunningStatus.Succeeded && 'after:bg-workflow-link-line-success-handle', + data._runningStatus === NodeRunningStatus.Failed && 'after:bg-workflow-link-line-error-handle', + showExceptionStatus && data._runningStatus === NodeRunningStatus.Exception && 'after:bg-workflow-link-line-failure-handle', + !connected && 'after:opacity-0', + handleClassName, + )} isConnectable={isConnectable} onClick={handleHandleClick} > diff --git a/web/app/components/workflow/nodes/_base/components/output-vars.tsx b/web/app/components/workflow/nodes/_base/components/output-vars.tsx index 4b7f9fc12e1a51..a0d7a25c07d831 100644 --- a/web/app/components/workflow/nodes/_base/components/output-vars.tsx +++ b/web/app/components/workflow/nodes/_base/components/output-vars.tsx @@ -2,11 +2,7 @@ import type { FC } from 'react' import React from 'react' import { useTranslation } from 'react-i18next' -import { useBoolean } from 'ahooks' -import { - RiArrowDownSLine, -} from '@remixicon/react' -import cn from '@/utils/classnames' +import { FieldCollapse } from '@/app/components/workflow/nodes/_base/components/collapse' type Props = { className?: string @@ -15,28 +11,14 @@ type Props = { } const OutputVars: FC = ({ - className, title, children, }) => { const { t } = useTranslation() - const [isFold, { - toggle: toggleFold, - }] = useBoolean(true) return ( -
-
-
{title || t('workflow.nodes.common.outputVars')}
- -
- {!isFold && ( -
- {children} -
- )} -
+ + {children} + ) } type VarItemProps = { diff --git a/web/app/components/workflow/nodes/_base/components/variable-tag.tsx b/web/app/components/workflow/nodes/_base/components/variable-tag.tsx index fc8c1ce9c9a1c6..0c5c3bde4bf171 100644 --- a/web/app/components/workflow/nodes/_base/components/variable-tag.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable-tag.tsx @@ -17,6 +17,7 @@ import { BubbleX, Env } from '@/app/components/base/icons/src/vender/line/others import { getNodeInfoById, isConversationVar, isENV, isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils' import Tooltip from '@/app/components/base/tooltip' import cn from '@/utils/classnames' +import { isExceptionVariable } from '@/app/components/workflow/utils' type VariableTagProps = { valueSelector: ValueSelector @@ -45,6 +46,7 @@ const VariableTag = ({ const isValid = Boolean(node) || isEnv || isChatVar const variableName = isSystemVar(valueSelector) ? valueSelector.slice(0).join('.') : valueSelector.slice(1).join('.') + const isException = isExceptionVariable(variableName, node?.data.type) const { t } = useTranslation() return ( @@ -67,12 +69,12 @@ const VariableTag = ({ )} - + )} {isEnv && } {isChatVar && }
{variableName} diff --git a/web/app/components/workflow/nodes/_base/components/variable/utils.ts b/web/app/components/workflow/nodes/_base/components/variable/utils.ts index 982f4f750030ea..715ad1c7b175ad 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/utils.ts +++ b/web/app/components/workflow/nodes/_base/components/variable/utils.ts @@ -315,6 +315,24 @@ const formatItem = ( } } + const { error_strategy } = data + + if (error_strategy) { + res.vars = [ + ...res.vars, + { + variable: 'error_message', + type: VarType.string, + isException: true, + }, + { + variable: 'error_type', + type: VarType.string, + isException: true, + }, + ] + } + const selector = [id] res.vars = res.vars.filter((v) => { const isCurrentMatched = filterVar(v, (() => { 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 e4d354a615fe9a..3a4cece35c9863 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 @@ -36,6 +36,7 @@ import TypeSelector from '@/app/components/workflow/nodes/_base/components/selec import AddButton from '@/app/components/base/button/add-button' import Badge from '@/app/components/base/badge' import Tooltip from '@/app/components/base/tooltip' +import { isExceptionVariable } from '@/app/components/workflow/utils' const TRIGGER_DEFAULT_WIDTH = 227 @@ -224,16 +225,18 @@ const VarReferencePicker: FC = ({ isConstant: !!isConstant, }) - const { isEnv, isChatVar, isValidVar } = useMemo(() => { + const { isEnv, isChatVar, isValidVar, isException } = useMemo(() => { const isEnv = isENV(value as ValueSelector) const isChatVar = isConversationVar(value as ValueSelector) const isValidVar = Boolean(outputVarNode) || isEnv || isChatVar + const isException = isExceptionVariable(varName, outputVarNode?.type) return { isEnv, isChatVar, isValidVar, + isException, } - }, [value, outputVarNode]) + }, [value, outputVarNode, varName]) // 8(left/right-padding) + 14(icon) + 4 + 14 + 2 = 42 + 17 buff const availableWidth = triggerWidth - 56 @@ -335,7 +338,7 @@ const VarReferencePicker: FC = ({ {!hasValue && } {isEnv && } {isChatVar && } -
{varName}
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 eb28279c0c1cf8..9ac5e4a4e4b6ae 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 @@ -37,6 +37,7 @@ type ItemProps = { onHovering?: (value: boolean) => void itemWidth?: number isSupportFileVar?: boolean + isException?: boolean } const Item: FC = ({ @@ -48,6 +49,7 @@ const Item: FC = ({ onHovering, itemWidth, isSupportFileVar, + isException, }) => { const isFile = itemData.type === VarType.file const isObj = ([VarType.object, VarType.file].includes(itemData.type) && itemData.children && itemData.children.length > 0) @@ -109,7 +111,7 @@ const Item: FC = ({ onClick={handleChosen} >
- {!isEnv && !isChatVar && } + {!isEnv && !isChatVar && } {isEnv && } {isChatVar && } {!isEnv && !isChatVar && ( @@ -216,6 +218,7 @@ const ObjectChildren: FC = ({ onChange={onChange} onHovering={setIsChildrenHovering} isSupportFileVar={isSupportFileVar} + isException={v.isException} /> )) } @@ -312,6 +315,7 @@ const VarReferenceVars: FC = ({ onChange={onChange} itemWidth={itemWidth} isSupportFileVar={isSupportFileVar} + isException={v.isException} /> ))}
)) diff --git a/web/app/components/workflow/nodes/_base/hooks/use-output-var-list.ts b/web/app/components/workflow/nodes/_base/hooks/use-output-var-list.ts index c7bce2ef07a9c0..839cd1402677f5 100644 --- a/web/app/components/workflow/nodes/_base/hooks/use-output-var-list.ts +++ b/web/app/components/workflow/nodes/_base/hooks/use-output-var-list.ts @@ -1,12 +1,22 @@ import { useCallback, useState } from 'react' import produce from 'immer' import { useBoolean } from 'ahooks' -import { type OutputVar } from '../../code/types' -import type { ValueSelector } from '@/app/components/workflow/types' -import { VarType } from '@/app/components/workflow/types' +import type { + CodeNodeType, + OutputVar, +} from '../../code/types' +import type { + ValueSelector, +} from '@/app/components/workflow/types' +import { + BlockEnum, + VarType, +} from '@/app/components/workflow/types' import { useWorkflow, } from '@/app/components/workflow/hooks' +import { ErrorHandleTypeEnum } from '@/app/components/workflow/nodes/_base/components/error-handle/types' +import { getDefaultValue } from '@/app/components/workflow/nodes/_base/components/error-handle/utils' type Params = { id: string @@ -29,6 +39,9 @@ function useOutputVarList({ const handleVarsChange = useCallback((newVars: OutputVar, changedIndex?: number, newKey?: string) => { const newInputs = produce(inputs, (draft: any) => { draft[varKey] = newVars + + if ((inputs as CodeNodeType).type === BlockEnum.Code && (inputs as CodeNodeType).error_strategy === ErrorHandleTypeEnum.defaultValue && varKey === 'outputs') + draft.default_value = getDefaultValue(draft as any) }) setInputs(newInputs) @@ -59,6 +72,9 @@ function useOutputVarList({ children: null, }, } + + if ((inputs as CodeNodeType).type === BlockEnum.Code && (inputs as CodeNodeType).error_strategy === ErrorHandleTypeEnum.defaultValue && varKey === 'outputs') + draft.default_value = getDefaultValue(draft as any) }) setInputs(newInputs) onOutputKeyOrdersChange([...outputKeyOrders, newKey]) @@ -84,6 +100,9 @@ function useOutputVarList({ const newInputs = produce(inputs, (draft: any) => { delete draft[varKey][key] + + if ((inputs as CodeNodeType).type === BlockEnum.Code && (inputs as CodeNodeType).error_strategy === ErrorHandleTypeEnum.defaultValue && varKey === 'outputs') + draft.default_value = getDefaultValue(draft as any) }) setInputs(newInputs) onOutputKeyOrdersChange(outputKeyOrders.filter((_, i) => i !== index)) diff --git a/web/app/components/workflow/nodes/_base/node.tsx b/web/app/components/workflow/nodes/_base/node.tsx index c5b78c5c2140f9..f2da2da35a4fe9 100644 --- a/web/app/components/workflow/nodes/_base/node.tsx +++ b/web/app/components/workflow/nodes/_base/node.tsx @@ -10,8 +10,9 @@ import { useRef, } from 'react' import { - RiCheckboxCircleLine, - RiErrorWarningLine, + RiAlertFill, + RiCheckboxCircleFill, + RiErrorWarningFill, RiLoader2Line, } from '@remixicon/react' import { useTranslation } from 'react-i18next' @@ -24,6 +25,7 @@ import { useNodesReadOnly, useToolIcon, } from '../../hooks' +import { hasErrorHandleNode } from '../../utils' import { useNodeIterationInteractions } from '../iteration/use-interactions' import type { IterationNodeType } from '../iteration/types' import { @@ -32,6 +34,7 @@ import { } from './components/node-handle' import NodeResizer from './components/node-resizer' import NodeControl from './components/node-control' +import ErrorHandleOnNode from './components/error-handle/error-handle-on-node' import AddVariablePopupWithPosition from './components/add-variable-popup-with-position' import cn from '@/utils/classnames' import BlockIcon from '@/app/components/workflow/block-icon' @@ -71,11 +74,13 @@ const BaseNode: FC = ({ 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]) @@ -85,6 +90,7 @@ const BaseNode: FC = ({ 'flex border-[2px] rounded-2xl', showSelectedBorder ? 'border-components-option-card-option-selected-border' : 'border-transparent', !showSelectedBorder && data._inParallelHovering && 'border-workflow-block-border-highlight', + data._waitingRun && 'opacity-70', )} ref={nodeRef} style={{ @@ -99,9 +105,10 @@ const BaseNode: FC = ({ data.type !== BlockEnum.Iteration && 'w-[240px] bg-workflow-block-bg', data.type === BlockEnum.Iteration && 'flex flex-col w-full h-full bg-[#fcfdff]/80', !data._runningStatus && 'hover:shadow-lg', - showRunningBorder && '!border-primary-500', - showSuccessBorder && '!border-[#12B76A]', - showFailedBorder && '!border-[#F04438]', + showRunningBorder && '!border-state-accent-solid', + showSuccessBorder && '!border-state-success-solid', + showFailedBorder && '!border-state-destructive-solid', + showExceptionBorder && '!border-state-warning-solid', data._isBundled && '!shadow-lg', )} > @@ -192,24 +199,29 @@ const BaseNode: FC = ({
{ data._iterationLength && data._iterationIndex && data._runningStatus === NodeRunningStatus.Running && ( -
+
{data._iterationIndex > data._iterationLength ? data._iterationLength : data._iterationIndex}/{data._iterationLength}
) } { (data._runningStatus === NodeRunningStatus.Running || data._singleRunningStatus === NodeRunningStatus.Running) && ( - + ) } { data._runningStatus === NodeRunningStatus.Succeeded && ( - + ) } { data._runningStatus === NodeRunningStatus.Failed && ( - + + ) + } + { + data._runningStatus === NodeRunningStatus.Exception && ( + ) }
@@ -225,6 +237,14 @@ const BaseNode: FC = ({
) } + { + hasErrorHandleNode(data.type) && ( + + ) + } { data.desc && data.type !== BlockEnum.Iteration && (
diff --git a/web/app/components/workflow/nodes/_base/panel.tsx b/web/app/components/workflow/nodes/_base/panel.tsx index 83387621fcedfa..e5cb0862266926 100644 --- a/web/app/components/workflow/nodes/_base/panel.tsx +++ b/web/app/components/workflow/nodes/_base/panel.tsx @@ -20,6 +20,7 @@ import { DescriptionInput, TitleInput, } from './components/title-description-input' +import ErrorHandleOnPanel from './components/error-handle/error-handle-on-panel' import { useResizePanel } from './hooks/use-resize-panel' import cn from '@/utils/classnames' import BlockIcon from '@/app/components/workflow/block-icon' @@ -34,7 +35,10 @@ import { useWorkflow, useWorkflowHistory, } from '@/app/components/workflow/hooks' -import { canRunBySingle } from '@/app/components/workflow/utils' +import { + canRunBySingle, + hasErrorHandleNode, +} from '@/app/components/workflow/utils' import Tooltip from '@/app/components/base/tooltip' import type { Node } from '@/app/components/workflow/types' import { useStore as useAppStore } from '@/app/components/app/store' @@ -161,9 +165,17 @@ const BasePanel: FC = ({ />
-
+
{cloneElement(children, { id, data })}
+ { + hasErrorHandleNode(data.type) && ( + + ) + } { !!availableNextBlocks.length && (
diff --git a/web/app/components/workflow/nodes/document-extractor/panel.tsx b/web/app/components/workflow/nodes/document-extractor/panel.tsx index 52491875cd98bd..1e26fe4c337ee2 100644 --- a/web/app/components/workflow/nodes/document-extractor/panel.tsx +++ b/web/app/components/workflow/nodes/document-extractor/panel.tsx @@ -72,7 +72,7 @@ const Panel: FC> = ({
-
+
= ({ readonly, payload, onChange }) => { const { t } = useTranslation() const { connect, read, write, max_connect_timeout, max_read_timeout, max_write_timeout } = payload ?? {} - const [isFold, { - toggle: toggleFold, - }] = useBoolean(true) - return ( - <> -
-
-
{t(`${i18nPrefix}.timeout.title`)}
- + +
+
+ onChange?.({ ...payload, connect: v })} + min={1} + max={max_connect_timeout || 300} + /> + onChange?.({ ...payload, read: v })} + min={1} + max={max_read_timeout || 600} + /> + onChange?.({ ...payload, write: v })} + min={1} + max={max_write_timeout || 600} + />
- {!isFold && ( -
-
- onChange?.({ ...payload, connect: v })} - min={1} - max={max_connect_timeout || 300} - /> - onChange?.({ ...payload, read: v })} - min={1} - max={max_read_timeout || 600} - /> - onChange?.({ ...payload, write: v })} - min={1} - max={max_write_timeout || 600} - /> -
-
- )}
- - +
) } export default React.memo(Timeout) diff --git a/web/app/components/workflow/nodes/http/panel.tsx b/web/app/components/workflow/nodes/http/panel.tsx index eb9a15e5b4a31e..5c613aa0f35a41 100644 --- a/web/app/components/workflow/nodes/http/panel.tsx +++ b/web/app/components/workflow/nodes/http/panel.tsx @@ -65,7 +65,7 @@ const Panel: FC> = ({ return null return ( -
+
> = ({
-
- -
+ {(isShowAuthorization && !readOnly) && ( > = ({ /> )} -
+
<> { const { t } = useTranslation() + const nodes = useNodes() const variableName = labelName || (isSystemVar(variableSelector) ? variableSelector.slice(0).join('.') : variableSelector.slice(1).join('.')) const operatorName = isComparisonOperatorNeedTranslate(operator) ? t(`workflow.nodes.ifElse.comparisonOperator.${operator}`) : operator const notHasValue = comparisonOperatorNotRequireValue(operator) const isEnvVar = isENV(variableSelector) const isChatVar = isConversationVar(variableSelector) + const node: Node | undefined = nodes.find(n => n.id === variableSelector[0]) as Node + const isException = isExceptionVariable(variableName, node?.data.type) const formatValue = useMemo(() => { if (notHasValue) return '' @@ -67,7 +76,7 @@ const ConditionValue = ({ return (
- {!isEnvVar && !isChatVar && } + {!isEnvVar && !isChatVar && } {isEnvVar && } {isChatVar && } @@ -75,6 +84,7 @@ const ConditionValue = ({ className={cn( 'shrink-0 ml-0.5 truncate text-xs font-medium text-text-accent', !notHasValue && 'max-w-[70px]', + isException && 'text-text-warning', )} title={variableName} > diff --git a/web/app/components/workflow/nodes/iteration/panel.tsx b/web/app/components/workflow/nodes/iteration/panel.tsx index 4ba42d488e81b1..9b6b3d37907792 100644 --- a/web/app/components/workflow/nodes/iteration/panel.tsx +++ b/web/app/components/workflow/nodes/iteration/panel.tsx @@ -18,7 +18,6 @@ import Switch from '@/app/components/base/switch' import Select from '@/app/components/base/select' import Slider from '@/app/components/base/slider' import Input from '@/app/components/base/input' -import Divider from '@/app/components/base/divider' const i18nPrefix = 'workflow.nodes.iteration' @@ -72,7 +71,7 @@ const Panel: FC> = ({ } = useConfig(id, data) return ( -
+
> = ({
) } -
- -
+
diff --git a/web/app/components/workflow/nodes/knowledge-retrieval/panel.tsx b/web/app/components/workflow/nodes/knowledge-retrieval/panel.tsx index 3bfc7c56ed4d2a..bae4217d11fa84 100644 --- a/web/app/components/workflow/nodes/knowledge-retrieval/panel.tsx +++ b/web/app/components/workflow/nodes/knowledge-retrieval/panel.tsx @@ -53,7 +53,7 @@ const Panel: FC> = ({ }, [setRerankModelOpen]) return ( -
+
{/* {JSON.stringify(inputs, null, 2)} */} > = ({
-
+
<> > = ({ } = useConfig(id, data) return ( -
-
+
+
@@ -157,7 +157,7 @@ const Panel: FC> = ({
-
+
<> > = ({ />
-
- - <> - - - -
+ + <> + + + {isShowSingleRun && ( > = ({ const model = inputs.model return ( -
-
+
+
@@ -157,38 +158,33 @@ const Panel: FC> = ({ nodesOutputVars={availableVars} availableNodes={availableNodesWithParent} /> - - <> - - {/* Memory */} - {isChatMode && ( -
- -
- )} - {isSupportFunctionCall && ( -
- -
- )} - -
-
+ + <> + {/* Memory */} + {isChatMode && ( +
+ +
+ )} + {isSupportFunctionCall && ( +
+ +
+ )} + +
{inputs.parameters?.length > 0 && (<> -
+
<> {inputs.parameters.map((param, index) => ( diff --git a/web/app/components/workflow/nodes/question-classifier/default.ts b/web/app/components/workflow/nodes/question-classifier/default.ts index a0936b66e3cdc3..b01db041dab16c 100644 --- a/web/app/components/workflow/nodes/question-classifier/default.ts +++ b/web/app/components/workflow/nodes/question-classifier/default.ts @@ -26,6 +26,16 @@ const nodeDefault: NodeDefault = { name: '', }, ], + _targetBranches: [ + { + id: '1', + name: '', + }, + { + id: '2', + name: '', + }, + ], vision: { enabled: false, }, diff --git a/web/app/components/workflow/nodes/question-classifier/panel.tsx b/web/app/components/workflow/nodes/question-classifier/panel.tsx index 523ec5001996ec..7d27a89d29ddda 100644 --- a/web/app/components/workflow/nodes/question-classifier/panel.tsx +++ b/web/app/components/workflow/nodes/question-classifier/panel.tsx @@ -14,6 +14,7 @@ import BeforeRunForm from '@/app/components/workflow/nodes/_base/components/befo import ResultPanel from '@/app/components/workflow/run/result-panel' import Split from '@/app/components/workflow/nodes/_base/components/split' import OutputVars, { VarItem } from '@/app/components/workflow/nodes/_base/components/output-vars' +import { FieldCollapse } from '@/app/components/workflow/nodes/_base/components/collapse' const i18nPrefix = 'workflow.nodes.questionClassifiers' @@ -55,8 +56,8 @@ const Panel: FC> = ({ const model = inputs.model return ( -
-
+
+
@@ -107,27 +108,27 @@ const Panel: FC> = ({ readonly={readOnly} /> - - - +
+ + + -
+
<> > = ({ />
-
+
<> > = ({ } return ( -
+
{!readOnly && isShowAuthBtn && ( <> -
+