feat: workflow continue on error (#11474)

This commit is contained in:
zxhlyh
2024-12-11 14:21:38 +08:00
committed by GitHub
parent 86dfdcb8ec
commit bec5451f12
60 changed files with 1481 additions and 282 deletions

View File

@@ -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 = ({
</div>
)}
<div className='flex items-center text-primary-600'>
{!isEnv && !isChatVar && <Variable02 className='shrink-0 w-3.5 h-3.5' />}
{!isEnv && !isChatVar && <Variable02 className={cn('shrink-0 w-3.5 h-3.5', isException && 'text-text-warning')} />}
{isEnv && <Env className='shrink-0 w-3.5 h-3.5 text-util-colors-violet-violet-600' />}
{isChatVar && <BubbleX className='w-3.5 h-3.5 text-util-colors-teal-teal-700' />}
<div className={cn('shrink-0 ml-0.5 text-xs font-medium truncate', (isEnv || isChatVar) && 'text-gray-900')} title={varName}>{varName}</div>
<div className={cn('shrink-0 ml-0.5 text-xs font-medium truncate', (isEnv || isChatVar) && 'text-gray-900', isException && 'text-text-warning')} title={varName}>{varName}</div>
{
!node && !isEnv && !isChatVar && (
<RiErrorWarningFill className='ml-0.5 w-3 h-3 text-[#D92D20]' />

View File

@@ -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 (
<defs>
<linearGradient
id={id}
gradientUnits='userSpaceOnUse'
x1={x1}
y1={y1}
x2={x2}
y2={y2}
>
<stop
offset='0%'
style={{
stopColor: startColor,
stopOpacity: 1,
}}
/>
<stop
offset='100%'
style={{
stopColor,
stopOpacity: 1,
}}
/>
</linearGradient>
</defs>
)
}
export default CustomEdgeLinearGradientRender

View File

@@ -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 && (
<CustomEdgeLinearGradientRender
id={linearGradientId}
startColor={getEdgeColor(_sourceRunningStatus)}
stopColor={getEdgeColor(_targetRunningStatus)}
position={{
x1: sourceX,
y1: sourceY,
x2: targetX,
y2: targetY,
}}
/>
)
}
<BaseEdge
id={id}
path={edgePath}
style={{
stroke: (selected || data?._connectedNodeIsHovering || data?._run) ? '#2970FF' : '#D0D5DD',
stroke,
strokeWidth: 2,
opacity: data._waitingRun ? 0.7 : 1,
}}
/>
<EdgeLabelRenderer>
@@ -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,
}}
>
<BlockSelector

View File

@@ -63,25 +63,29 @@ export const useEdgesInteractions = () => {
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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -59,7 +59,7 @@ const BeforeRunForm: FC<BeforeRunFormProps> = ({
}) => {
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

View File

@@ -0,0 +1,26 @@
import Collapse from '.'
type FieldCollapseProps = {
title: string
children: JSX.Element
}
const FieldCollapse = ({
title,
children,
}: FieldCollapseProps) => {
return (
<div className='py-4'>
<Collapse
trigger={
<div className='flex items-center h-6 system-sm-semibold-uppercase text-text-secondary cursor-pointer'>{title}</div>
}
>
<div className='px-4'>
{children}
</div>
</Collapse>
</div>
)
}
export default FieldCollapse

View File

@@ -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 (
<>
<div
className='flex items-center'
onClick={() => {
if (!disabled) {
setCollapsedLocal(!collapsedMerged)
onCollapse?.(!collapsedMerged)
}
}}
>
<div className='shrink-0 w-4 h-4'>
{
!disabled && (
<RiArrowDropRightLine
className={cn(
'w-4 h-4 text-text-tertiary',
!collapsedMerged && 'transform rotate-90',
)}
/>
)
}
</div>
{trigger}
</div>
{
!collapsedMerged && children
}
</>
)
}
export default Collapse

View File

@@ -33,6 +33,7 @@ type Props = {
}[]
showFileList?: boolean
showCodeGenerator?: boolean
tip?: JSX.Element
}
const Base: FC<Props> = ({
@@ -49,6 +50,7 @@ const Base: FC<Props> = ({
fileList = [],
showFileList,
showCodeGenerator = false,
tip,
}) => {
const ref = useRef<HTMLDivElement>(null)
const {
@@ -100,6 +102,7 @@ const Base: FC<Props> = ({
</div>
</div>
</div>
{tip && <div className='px-1 py-0.5'>{tip}</div>}
<PromptEditorHeightResizeWrap
height={isExpand ? editorExpandHeight : editorContentHeight}
minHeight={editorContentMinHeight}

View File

@@ -34,6 +34,7 @@ export type Props = {
onGenerated?: (value: string) => void
showCodeGenerator?: boolean
className?: string
tip?: JSX.Element
}
export const languageMap = {
@@ -69,6 +70,7 @@ const CodeEditor: FC<Props> = ({
onGenerated,
showCodeGenerator = false,
className,
tip,
}) => {
const [isFocus, setIsFocus] = React.useState(false)
const [isMounted, setIsMounted] = React.useState(false)
@@ -211,6 +213,7 @@ const CodeEditor: FC<Props> = ({
fileList={fileList as any}
showFileList={showFileList}
showCodeGenerator={showCodeGenerator}
tip={tip}
>
{main}
</Base>

View File

@@ -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 (
<div className='px-4 pt-2'>
<div className='mb-2 body-xs-regular text-text-tertiary'>
{t('workflow.nodes.common.errorHandle.defaultValue.desc')}
&nbsp;
<a
href='https://docs.dify.ai/guides/workflow/error-handling'
target='_blank'
className='text-text-accent'
>
{t('workflow.common.learnMore')}
</a>
</div>
<div className='space-y-1'>
{
forms.map((form, index) => {
return (
<div
key={index}
className='py-1'
>
<div className='flex items-center mb-1'>
<div className='mr-1 system-sm-medium text-text-primary'>{form.key}</div>
<div className='system-xs-regular text-text-tertiary'>{form.type}</div>
</div>
{
(form.type === VarType.string || form.type === VarType.number) && (
<Input
type={form.type}
value={form.value || (form.type === VarType.string ? '' : 0)}
onChange={getFormChangeHandler({ key: form.key, type: form.type })}
/>
)
}
{
(
form.type === VarType.array
|| form.type === VarType.arrayNumber
|| form.type === VarType.arrayString
|| form.type === VarType.arrayObject
|| form.type === VarType.object
) && (
<CodeEditor
language={CodeLanguage.json}
value={form.value}
onChange={getFormChangeHandler({ key: form.key, type: form.type })}
/>
)
}
</div>
)
})
}
</div>
</div>
)
}
export default DefaultValue

View File

@@ -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<Node, 'id' | 'data'>
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 (
<div className='relative pt-1 pb-2 px-3'>
<div className={cn(
'relative flex items-center justify-between px-[5px] h-6 bg-workflow-block-parma-bg rounded-md',
data._runningStatus === NodeRunningStatus.Exception && 'border-[0.5px] border-components-badge-status-light-warning-halo bg-state-warning-hover',
)}>
<div className='system-xs-medium-uppercase text-text-tertiary'>
{t('workflow.common.onFailure')}
</div>
<div className={cn(
'system-xs-medium text-text-secondary',
data._runningStatus === NodeRunningStatus.Exception && 'text-text-warning',
)}>
{
error_strategy === ErrorHandleTypeEnum.defaultValue && (
t('workflow.nodes.common.errorHandle.defaultValue.output')
)
}
{
error_strategy === ErrorHandleTypeEnum.failBranch && (
t('workflow.nodes.common.errorHandle.failBranch.title')
)
}
</div>
{
error_strategy === ErrorHandleTypeEnum.failBranch && (
<NodeSourceHandle
id={id}
data={data}
handleId={ErrorHandleTypeEnum.failBranch}
handleClassName='!top-1/2 !-right-[21px] !-translate-y-1/2 after:!bg-workflow-link-line-failure-button-bg'
nodeSelectorClassName='!bg-workflow-link-line-failure-button-bg'
/>
)
}
</div>
</div>
)
}
export default ErrorHandleOnNode

View File

@@ -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<Node, 'id' | 'data'>
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 (
<>
<Split />
<div className='py-4'>
<Collapse
disabled={!error_strategy}
collapsed={collapsed}
onCollapse={setCollapsed}
trigger={
<div className='grow flex items-center justify-between pr-4'>
<div className='flex items-center'>
<div className='mr-0.5 system-sm-semibold-uppercase text-text-secondary'>
{t('workflow.nodes.common.errorHandle.title')}
</div>
<Tooltip popupContent={t('workflow.nodes.common.errorHandle.tip')} />
</div>
<ErrorHandleTypeSelector
value={error_strategy || ErrorHandleTypeEnum.none}
onSelected={getHandleErrorHandleTypeChange(data)}
/>
</div>
}
>
<>
{
error_strategy === ErrorHandleTypeEnum.failBranch && !collapsed && (
<FailBranchCard />
)
}
{
error_strategy === ErrorHandleTypeEnum.defaultValue && !collapsed && !!default_value?.length && (
<DefaultValue
forms={default_value}
onFormChange={getHandleFormChange(data)}
/>
)
}
</>
</Collapse>
</div>
</>
)
}
export default ErrorHandle

View File

@@ -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 (
<div
className='relative flex p-2 pr-[52px] rounded-lg border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-xs'
>
<div
className='absolute inset-0 opacity-40 rounded-lg'
style={{
background: 'linear-gradient(92deg, rgba(247, 144, 9, 0.25) 0%, rgba(255, 255, 255, 0.00) 100%)',
}}
></div>
<RiAlertFill className='shrink-0 mr-1 w-4 h-4 text-text-warning-secondary' />
<div className='grow system-xs-medium text-text-primary'>
{text}
</div>
</div>
)
}
export default ErrorHandleTip

View File

@@ -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 (
<PortalToFollowElem
open={open}
onOpenChange={setOpen}
placement='bottom-end'
offset={4}
>
<PortalToFollowElemTrigger onClick={(e) => {
e.stopPropagation()
setOpen(v => !v)
}}>
<Button
size='small'
>
{selectedOption?.label}
<RiArrowDownSLine className='w-3.5 h-3.5' />
</Button>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-[11]'>
<div className='p-1 w-[280px] border-[0.5px] border-components-panel-border rounded-xl bg-components-panel-bg-blur shadow-lg'>
{
options.map(option => (
<div
key={option.value}
className='flex p-2 pr-3 rounded-lg hover:bg-state-base-hover cursor-pointer'
onClick={(e) => {
e.stopPropagation()
onSelected(option.value)
setOpen(false)
}}
>
<div className='mr-1 w-4 shrink-0'>
{
value === option.value && (
<RiCheckLine className='w-4 h-4 text-text-accent' />
)
}
</div>
<div className='grow'>
<div className='mb-0.5 system-sm-semibold text-text-secondary'>{option.label}</div>
<div className='system-xs-regular text-text-tertiary'>{option.description}</div>
</div>
</div>
))
}
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
)
}
export default ErrorHandleTypeSelector

View File

@@ -0,0 +1,32 @@
import { RiMindMap } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
const FailBranchCard = () => {
const { t } = useTranslation()
return (
<div className='pt-2 px-4'>
<div className='p-4 rounded-[10px] bg-workflow-process-bg'>
<div className='flex items-center justify-center mb-2 w-8 h-8 rounded-[10px] border-[0.5px] bg-components-card-bg shadow-lg'>
<RiMindMap className='w-5 h-5 text-text-tertiary' />
</div>
<div className='mb-1 system-sm-medium text-text-secondary'>
{t('workflow.nodes.common.errorHandle.failBranch.customize')}
</div>
<div className='system-xs-regular text-text-tertiary'>
{t('workflow.nodes.common.errorHandle.failBranch.customizeTip')}
&nbsp;
<a
href='https://docs.dify.ai/guides/workflow/error-handling'
target='_blank'
className='text-text-accent'
>
{t('workflow.common.learnMore')}
</a>
</div>
</div>
</div>
)
}
export default FailBranchCard

View File

@@ -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,
}
}

View File

@@ -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
}

View File

@@ -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 []
}

View File

@@ -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 (
<div
@@ -72,15 +84,11 @@ const Add = ({
<RiAddLine className='w-3 h-3' />
</div>
<div className='flex items-center uppercase'>
{
isParallel
? t('workflow.common.addParallelNode')
: t('workflow.panel.selectNextStep')
}
{tip}
</div>
</div>
)
}, [t, nodesReadOnly, isParallel])
}, [nodesReadOnly, tip])
return (
<BlockSelector

View File

@@ -4,6 +4,7 @@ import type {
CommonNodeType,
Node,
} from '@/app/components/workflow/types'
import cn from '@/utils/classnames'
type ContainerProps = {
nodeId: string
@@ -11,6 +12,7 @@ type ContainerProps = {
sourceHandle: string
nextNodes: Node[]
branchName?: string
isFailBranch?: boolean
}
const Container = ({
@@ -19,13 +21,20 @@ const Container = ({
sourceHandle,
nextNodes,
branchName,
isFailBranch,
}: ContainerProps) => {
return (
<div className='p-0.5 space-y-0.5 rounded-[10px] bg-background-section-burn'>
<div className={cn(
'p-0.5 space-y-0.5 rounded-[10px] bg-background-section-burn',
isFailBranch && 'border-[0.5px] border-state-warning-hover-alt bg-state-warning-hover',
)}>
{
branchName && (
<div
className='flex items-center px-2 system-2xs-semibold-uppercase text-text-tertiary truncate'
className={cn(
'flex items-center px-2 system-2xs-semibold-uppercase text-text-tertiary truncate',
isFailBranch && 'text-text-warning',
)}
title={branchName}
>
{branchName}
@@ -44,6 +53,7 @@ const Container = ({
}
<Add
isParallel={!!nextNodes.length}
isFailBranch={isFailBranch}
nodeId={nodeId}
nodeData={nodeData}
sourceHandle={sourceHandle}

View File

@@ -14,6 +14,8 @@ import type {
import { BlockEnum } from '../../../../types'
import Line from './line'
import Container from './container'
import { hasErrorHandleNode } from '@/app/components/workflow/utils'
import { ErrorHandleTypeEnum } from '@/app/components/workflow/nodes/_base/components/error-handle/types'
type NextStepProps = {
selectedNode: Node
@@ -28,25 +30,54 @@ const NextStep = ({
const branches = useMemo(() => {
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 (
<div className='flex py-1'>
@@ -57,34 +88,23 @@ const NextStep = ({
/>
</div>
<Line
list={nodeWithBranches ? branchesOutgoers.map(item => item.nextNodes.length + 1) : [1]}
list={list.length ? list.map(item => item.nextNodes.length + 1) : [1]}
/>
<div className='grow space-y-2'>
{
!nodeWithBranches && (
<Container
nodeId={selectedNode!.id}
nodeData={selectedNode!.data}
sourceHandle='source'
nextNodes={outgoers}
/>
)
}
{
nodeWithBranches && (
branchesOutgoers.map((item, index) => {
return (
<Container
key={item.branch.id}
nodeId={selectedNode!.id}
nodeData={selectedNode!.data}
sourceHandle={item.branch.id}
nextNodes={item.nextNodes}
branchName={item.branch.name || `${t('workflow.nodes.questionClassifiers.class')} ${index + 1}`}
/>
)
})
)
list.map((item, index) => {
return (
<Container
key={index}
nodeId={selectedNode!.id}
nodeData={selectedNode!.data}
sourceHandle={item.branch.id}
nextNodes={item.nextNodes}
branchName={item.branch.name}
isFailBranch={item.branch.id === ErrorHandleTypeEnum.failBranch}
/>
)
})
}
</div>
</div>

View File

@@ -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<Node, 'id' | 'data'>
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}
>

View File

@@ -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<Props> = ({
className,
title,
children,
}) => {
const { t } = useTranslation()
const [isFold, {
toggle: toggleFold,
}] = useBoolean(true)
return (
<div>
<div
onClick={toggleFold}
className={cn(className, 'flex justify-between system-sm-semibold-uppercase text-text-secondary cursor-pointer')}>
<div>{title || t('workflow.nodes.common.outputVars')}</div>
<RiArrowDownSLine className='w-4 h-4 text-text-tertiary transform transition-transform' style={{ transform: isFold ? 'rotate(-90deg)' : 'rotate(0deg)' }} />
</div>
{!isFold && (
<div className='mt-2 space-y-1'>
{children}
</div>
)}
</div>
<FieldCollapse title={title || t('workflow.nodes.common.outputVars')}>
{children}
</FieldCollapse>
)
}
type VarItemProps = {

View File

@@ -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 = ({
</>
)}
<Line3 className='shrink-0 mx-0.5' />
<Variable02 className='shrink-0 mr-0.5 w-3.5 h-3.5 text-text-accent' />
<Variable02 className={cn('shrink-0 mr-0.5 w-3.5 h-3.5 text-text-accent', isException && 'text-text-warning')} />
</>)}
{isEnv && <Env className='shrink-0 mr-0.5 w-3.5 h-3.5 text-util-colors-violet-violet-600' />}
{isChatVar && <BubbleX className='w-3.5 h-3.5 text-util-colors-teal-teal-700' />}
<div
className={cn('truncate ml-0.5 text-text-accent font-medium', (isEnv || isChatVar) && 'text-text-secondary')}
className={cn('truncate ml-0.5 text-text-accent font-medium', (isEnv || isChatVar) && 'text-text-secondary', isException && 'text-text-warning')}
title={variableName}
>
{variableName}

View File

@@ -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, (() => {

View File

@@ -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<Props> = ({
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<Props> = ({
{!hasValue && <Variable02 className='w-3.5 h-3.5' />}
{isEnv && <Env className='w-3.5 h-3.5 text-util-colors-violet-violet-600' />}
{isChatVar && <BubbleX className='w-3.5 h-3.5 text-util-colors-teal-teal-700' />}
<div className={cn('ml-0.5 text-xs font-medium truncate', isEnv && '!text-text-secondary', isChatVar && 'text-util-colors-teal-teal-700')} title={varName} style={{
<div className={cn('ml-0.5 text-xs font-medium truncate', isEnv && '!text-text-secondary', isChatVar && 'text-util-colors-teal-teal-700', isException && 'text-text-warning')} title={varName} style={{
maxWidth: maxVarNameWidth,
}}>{varName}</div>
</div>

View File

@@ -37,6 +37,7 @@ type ItemProps = {
onHovering?: (value: boolean) => void
itemWidth?: number
isSupportFileVar?: boolean
isException?: boolean
}
const Item: FC<ItemProps> = ({
@@ -48,6 +49,7 @@ const Item: FC<ItemProps> = ({
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<ItemProps> = ({
onClick={handleChosen}
>
<div className='flex items-center w-0 grow'>
{!isEnv && !isChatVar && <Variable02 className='shrink-0 w-3.5 h-3.5 text-text-accent' />}
{!isEnv && !isChatVar && <Variable02 className={cn('shrink-0 w-3.5 h-3.5 text-text-accent', isException && 'text-text-warning')} />}
{isEnv && <Env className='shrink-0 w-3.5 h-3.5 text-util-colors-violet-violet-600' />}
{isChatVar && <BubbleX className='w-3.5 h-3.5 text-util-colors-teal-teal-700' />}
{!isEnv && !isChatVar && (
@@ -216,6 +218,7 @@ const ObjectChildren: FC<ObjectChildrenProps> = ({
onChange={onChange}
onHovering={setIsChildrenHovering}
isSupportFileVar={isSupportFileVar}
isException={v.isException}
/>
))
}
@@ -312,6 +315,7 @@ const VarReferenceVars: FC<Props> = ({
onChange={onChange}
itemWidth={itemWidth}
isSupportFileVar={isSupportFileVar}
isException={v.isException}
/>
))}
</div>))

View File

@@ -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<T> = {
id: string
@@ -29,6 +39,9 @@ function useOutputVarList<T>({
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<T>({
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<T>({
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))

View File

@@ -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<BaseNodeProps> = ({
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<BaseNodeProps> = ({
'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<BaseNodeProps> = ({
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<BaseNodeProps> = ({
</div>
{
data._iterationLength && data._iterationIndex && data._runningStatus === NodeRunningStatus.Running && (
<div className='mr-1.5 text-xs font-medium text-primary-600'>
<div className='mr-1.5 text-xs font-medium text-text-accent'>
{data._iterationIndex > data._iterationLength ? data._iterationLength : data._iterationIndex}/{data._iterationLength}
</div>
)
}
{
(data._runningStatus === NodeRunningStatus.Running || data._singleRunningStatus === NodeRunningStatus.Running) && (
<RiLoader2Line className='w-3.5 h-3.5 text-primary-600 animate-spin' />
<RiLoader2Line className='w-3.5 h-3.5 text-text-accent animate-spin' />
)
}
{
data._runningStatus === NodeRunningStatus.Succeeded && (
<RiCheckboxCircleLine className='w-3.5 h-3.5 text-[#12B76A]' />
<RiCheckboxCircleFill className='w-3.5 h-3.5 text-text-success' />
)
}
{
data._runningStatus === NodeRunningStatus.Failed && (
<RiErrorWarningLine className='w-3.5 h-3.5 text-[#F04438]' />
<RiErrorWarningFill className='w-3.5 h-3.5 text-text-destructive' />
)
}
{
data._runningStatus === NodeRunningStatus.Exception && (
<RiAlertFill className='w-3.5 h-3.5 text-text-warning-secondary' />
)
}
</div>
@@ -225,6 +237,14 @@ const BaseNode: FC<BaseNodeProps> = ({
</div>
)
}
{
hasErrorHandleNode(data.type) && (
<ErrorHandleOnNode
id={id}
data={data}
/>
)
}
{
data.desc && data.type !== BlockEnum.Iteration && (
<div className='px-3 pt-1 pb-2 system-xs-regular text-text-tertiary whitespace-pre-line break-words'>

View File

@@ -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<BasePanelProps> = ({
/>
</div>
</div>
<div className='py-2'>
<div>
{cloneElement(children, { id, data })}
</div>
{
hasErrorHandleNode(data.type) && (
<ErrorHandleOnPanel
id={id}
data={data}
/>
)
}
{
!!availableNextBlocks.length && (
<div className='p-4 border-t-[0.5px] border-t-black/5'>

View File

@@ -72,7 +72,7 @@ const Panel: FC<NodePanelProps<DocExtractorNodeType>> = ({
</Field>
</div>
<Split />
<div className='px-4 pt-4 pb-2'>
<div>
<OutputVars>
<VarItem
name='text'

View File

@@ -2,11 +2,9 @@
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { useBoolean } from 'ahooks'
import type { Timeout as TimeoutPayloadType } from '../../types'
import cn from '@/utils/classnames'
import Input from '@/app/components/base/input'
import { ChevronRight } from '@/app/components/base/icons/src/vender/line/arrows'
import { FieldCollapse } from '@/app/components/workflow/nodes/_base/components/collapse'
type Props = {
readonly: boolean
@@ -53,58 +51,43 @@ const Timeout: FC<Props> = ({ 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 (
<>
<div>
<div
onClick={toggleFold}
className={cn('flex justify-between leading-[18px] text-[13px] font-semibold text-gray-700 uppercase cursor-pointer')}>
<div>{t(`${i18nPrefix}.timeout.title`)}</div>
<ChevronRight className='w-4 h-4 text-gray-500 transform transition-transform' style={{ transform: isFold ? 'rotate(0deg)' : 'rotate(90deg)' }} />
<FieldCollapse title={t(`${i18nPrefix}.timeout.title`)}>
<div className='mt-2 space-y-1'>
<div className="space-y-3">
<InputField
title={t('workflow.nodes.http.timeout.connectLabel')!}
description={t('workflow.nodes.http.timeout.connectPlaceholder')!}
placeholder={t('workflow.nodes.http.timeout.connectPlaceholder')!}
readOnly={readonly}
value={connect}
onChange={v => onChange?.({ ...payload, connect: v })}
min={1}
max={max_connect_timeout || 300}
/>
<InputField
title={t('workflow.nodes.http.timeout.readLabel')!}
description={t('workflow.nodes.http.timeout.readPlaceholder')!}
placeholder={t('workflow.nodes.http.timeout.readPlaceholder')!}
readOnly={readonly}
value={read}
onChange={v => onChange?.({ ...payload, read: v })}
min={1}
max={max_read_timeout || 600}
/>
<InputField
title={t('workflow.nodes.http.timeout.writeLabel')!}
description={t('workflow.nodes.http.timeout.writePlaceholder')!}
placeholder={t('workflow.nodes.http.timeout.writePlaceholder')!}
readOnly={readonly}
value={write}
onChange={v => onChange?.({ ...payload, write: v })}
min={1}
max={max_write_timeout || 600}
/>
</div>
{!isFold && (
<div className='mt-2 space-y-1'>
<div className="space-y-3">
<InputField
title={t('workflow.nodes.http.timeout.connectLabel')!}
description={t('workflow.nodes.http.timeout.connectPlaceholder')!}
placeholder={t('workflow.nodes.http.timeout.connectPlaceholder')!}
readOnly={readonly}
value={connect}
onChange={v => onChange?.({ ...payload, connect: v })}
min={1}
max={max_connect_timeout || 300}
/>
<InputField
title={t('workflow.nodes.http.timeout.readLabel')!}
description={t('workflow.nodes.http.timeout.readPlaceholder')!}
placeholder={t('workflow.nodes.http.timeout.readPlaceholder')!}
readOnly={readonly}
value={read}
onChange={v => onChange?.({ ...payload, read: v })}
min={1}
max={max_read_timeout || 600}
/>
<InputField
title={t('workflow.nodes.http.timeout.writeLabel')!}
description={t('workflow.nodes.http.timeout.writePlaceholder')!}
placeholder={t('workflow.nodes.http.timeout.writePlaceholder')!}
readOnly={readonly}
value={write}
onChange={v => onChange?.({ ...payload, write: v })}
min={1}
max={max_write_timeout || 600}
/>
</div>
</div>
)}
</div>
</>
</FieldCollapse>
)
}
export default React.memo(Timeout)

View File

@@ -65,7 +65,7 @@ const Panel: FC<NodePanelProps<HttpNodeType>> = ({
return null
return (
<div className='mt-2'>
<div className='pt-2'>
<div className='px-4 pb-4 space-y-4'>
<Field
title={t(`${i18nPrefix}.api`)}
@@ -136,14 +136,12 @@ const Panel: FC<NodePanelProps<HttpNodeType>> = ({
</Field>
</div>
<Split />
<div className='px-4 pt-4 pb-4'>
<Timeout
nodeId={id}
readonly={readOnly}
payload={inputs.timeout}
onChange={setTimeout}
/>
</div>
<Timeout
nodeId={id}
readonly={readOnly}
payload={inputs.timeout}
onChange={setTimeout}
/>
{(isShowAuthorization && !readOnly) && (
<AuthorizationModal
nodeId={id}
@@ -154,7 +152,7 @@ const Panel: FC<NodePanelProps<HttpNodeType>> = ({
/>
)}
<Split />
<div className='px-4 pt-4 pb-2'>
<div className=''>
<OutputVars>
<>
<VarItem

View File

@@ -3,6 +3,7 @@ import {
useMemo,
} from 'react'
import { useTranslation } from 'react-i18next'
import { useNodes } from 'reactflow'
import { ComparisonOperator } from '../types'
import {
comparisonOperatorNotRequireValue,
@@ -13,6 +14,11 @@ import { Variable02 } from '@/app/components/base/icons/src/vender/solid/develop
import { BubbleX, Env } from '@/app/components/base/icons/src/vender/line/others'
import cn from '@/utils/classnames'
import { isConversationVar, isENV, isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils'
import { isExceptionVariable } from '@/app/components/workflow/utils'
import type {
CommonNodeType,
Node,
} from '@/app/components/workflow/types'
type ConditionValueProps = {
variableSelector: string[]
@@ -27,11 +33,14 @@ const ConditionValue = ({
value,
}: ConditionValueProps) => {
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<CommonNodeType> | undefined = nodes.find(n => n.id === variableSelector[0]) as Node<CommonNodeType>
const isException = isExceptionVariable(variableName, node?.data.type)
const formatValue = useMemo(() => {
if (notHasValue)
return ''
@@ -67,7 +76,7 @@ const ConditionValue = ({
return (
<div className='flex items-center px-1 h-6 rounded-md bg-workflow-block-parma-bg'>
{!isEnvVar && !isChatVar && <Variable02 className='shrink-0 mr-1 w-3.5 h-3.5 text-text-accent' />}
{!isEnvVar && !isChatVar && <Variable02 className={cn('shrink-0 mr-1 w-3.5 h-3.5 text-text-accent', isException && 'text-text-warning')} />}
{isEnvVar && <Env className='shrink-0 mr-1 w-3.5 h-3.5 text-util-colors-violet-violet-600' />}
{isChatVar && <BubbleX className='w-3.5 h-3.5 text-util-colors-teal-teal-700' />}
@@ -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}
>

View File

@@ -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<NodePanelProps<IterationNodeType>> = ({
} = useConfig(id, data)
return (
<div className='mt-2'>
<div className='pt-2 pb-2'>
<div className='px-4 pb-4 space-y-4'>
<Field
title={t(`${i18nPrefix}.input`)}
@@ -131,9 +130,7 @@ const Panel: FC<NodePanelProps<IterationNodeType>> = ({
</Field>
</div>)
}
<div className='px-4 py-2'>
<Divider className='h-[1px]'/>
</div>
<Split />
<div className='px-4 py-2'>
<Field title={t(`${i18nPrefix}.errorResponseMethod`)} >

View File

@@ -53,7 +53,7 @@ const Panel: FC<NodePanelProps<KnowledgeRetrievalNodeType>> = ({
}, [setRerankModelOpen])
return (
<div className='mt-2'>
<div className='pt-2'>
<div className='px-4 pb-4 space-y-4'>
{/* {JSON.stringify(inputs, null, 2)} */}
<Field
@@ -108,7 +108,7 @@ const Panel: FC<NodePanelProps<KnowledgeRetrievalNodeType>> = ({
</div>
<Split />
<div className='px-4 pt-4 pb-2'>
<div>
<OutputVars>
<>
<VarItem

View File

@@ -42,8 +42,8 @@ const Panel: FC<NodePanelProps<ListFilterNodeType>> = ({
} = useConfig(id, data)
return (
<div className='mt-2'>
<div className='px-4 pb-4 space-y-4'>
<div className='pt-2'>
<div className='px-4 space-y-4'>
<Field
title={t(`${i18nPrefix}.inputVar`)}
>
@@ -157,7 +157,7 @@ const Panel: FC<NodePanelProps<ListFilterNodeType>> = ({
</Field>
<Split />
</div>
<div className='px-4 pt-4 pb-2'>
<div>
<OutputVars>
<>
<VarItem

View File

@@ -270,17 +270,15 @@ const Panel: FC<NodePanelProps<LLMNodeType>> = ({
/>
</div>
<Split />
<div className='px-4 pt-4 pb-2'>
<OutputVars>
<>
<VarItem
name='text'
type='string'
description={t(`${i18nPrefix}.outputVars.output`)}
/>
</>
</OutputVars>
</div>
<OutputVars>
<>
<VarItem
name='text'
type='string'
description={t(`${i18nPrefix}.outputVars.output`)}
/>
</>
</OutputVars>
{isShowSingleRun && (
<BeforeRunForm
nodeName={inputs.title}

View File

@@ -20,6 +20,7 @@ import { InputVarType, type NodePanelProps } from '@/app/components/workflow/typ
import Tooltip from '@/app/components/base/tooltip'
import BeforeRunForm from '@/app/components/workflow/nodes/_base/components/before-run-form'
import { VarType } from '@/app/components/workflow/types'
import { FieldCollapse } from '@/app/components/workflow/nodes/_base/components/collapse'
const i18nPrefix = 'workflow.nodes.parameterExtractor'
const i18nCommonPrefix = 'workflow.common'
@@ -67,8 +68,8 @@ const Panel: FC<NodePanelProps<ParameterExtractorNodeType>> = ({
const model = inputs.model
return (
<div className='mt-2'>
<div className='px-4 pb-4 space-y-4'>
<div className='pt-2'>
<div className='px-4 space-y-4'>
<Field
title={t(`${i18nCommonPrefix}.model`)}
>
@@ -157,38 +158,33 @@ const Panel: FC<NodePanelProps<ParameterExtractorNodeType>> = ({
nodesOutputVars={availableVars}
availableNodes={availableNodesWithParent}
/>
<Field
title={t(`${i18nPrefix}.advancedSetting`)}
supportFold
>
<>
{/* Memory */}
{isChatMode && (
<div className='mt-4'>
<MemoryConfig
readonly={readOnly}
config={{ data: inputs.memory }}
onChange={handleMemoryChange}
canSetRoleName={isCompletionModel}
/>
</div>
)}
{isSupportFunctionCall && (
<div className='mt-2'>
<ReasoningModePicker
type={inputs.reasoning_mode}
onChange={handleReasoningModeChange}
/>
</div>
)}
</>
</Field>
</div>
<FieldCollapse title={t(`${i18nPrefix}.advancedSetting`)}>
<>
{/* Memory */}
{isChatMode && (
<div className='mt-4'>
<MemoryConfig
readonly={readOnly}
config={{ data: inputs.memory }}
onChange={handleMemoryChange}
canSetRoleName={isCompletionModel}
/>
</div>
)}
{isSupportFunctionCall && (
<div className='mt-2'>
<ReasoningModePicker
type={inputs.reasoning_mode}
onChange={handleReasoningModeChange}
/>
</div>
)}
</>
</FieldCollapse>
{inputs.parameters?.length > 0 && (<>
<Split />
<div className='px-4 pt-4 pb-2'>
<div>
<OutputVars>
<>
{inputs.parameters.map((param, index) => (

View File

@@ -26,6 +26,16 @@ const nodeDefault: NodeDefault<QuestionClassifierNodeType> = {
name: '',
},
],
_targetBranches: [
{
id: '1',
name: '',
},
{
id: '2',
name: '',
},
],
vision: {
enabled: false,
},

View File

@@ -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<NodePanelProps<QuestionClassifierNodeType>> = ({
const model = inputs.model
return (
<div className='mt-2'>
<div className='px-4 pb-4 space-y-4'>
<div className='pt-2'>
<div className='px-4 space-y-4'>
<Field
title={t(`${i18nPrefix}.model`)}
>
@@ -107,27 +108,27 @@ const Panel: FC<NodePanelProps<QuestionClassifierNodeType>> = ({
readonly={readOnly}
/>
</Field>
<Field
title={t(`${i18nPrefix}.advancedSetting`)}
supportFold
>
<AdvancedSetting
hideMemorySetting={!isChatMode}
instruction={inputs.instruction}
onInstructionChange={handleInstructionChange}
memory={inputs.memory}
onMemoryChange={handleMemoryChange}
readonly={readOnly}
isChatApp={isChatMode}
isChatModel={isChatModel}
hasSetBlockStatus={hasSetBlockStatus}
nodesOutputVars={availableVars}
availableNodes={availableNodesWithParent}
/>
</Field>
<Split />
</div>
<FieldCollapse
title={t(`${i18nPrefix}.advancedSetting`)}
>
<AdvancedSetting
hideMemorySetting={!isChatMode}
instruction={inputs.instruction}
onInstructionChange={handleInstructionChange}
memory={inputs.memory}
onMemoryChange={handleMemoryChange}
readonly={readOnly}
isChatApp={isChatMode}
isChatModel={isChatModel}
hasSetBlockStatus={hasSetBlockStatus}
nodesOutputVars={availableVars}
availableNodes={availableNodesWithParent}
/>
</FieldCollapse>
<Split />
<div className='px-4 pt-4 pb-2'>
<div>
<OutputVars>
<>
<VarItem

View File

@@ -95,7 +95,7 @@ const Panel: FC<NodePanelProps<TemplateTransformNodeType>> = ({
/>
</div>
<Split />
<div className='px-4 pt-4 pb-2'>
<div>
<OutputVars>
<>
<VarItem

View File

@@ -56,10 +56,10 @@ const Panel: FC<NodePanelProps<ToolNodeType>> = ({
}
return (
<div className='mt-2'>
<div className='pt-2'>
{!readOnly && isShowAuthBtn && (
<>
<div className='px-4 pb-3'>
<div className='px-4'>
<Button
variant='primary'
className='w-full'
@@ -71,7 +71,7 @@ const Panel: FC<NodePanelProps<ToolNodeType>> = ({
</>
)}
{!isShowAuthBtn && <>
<div className='px-4 pb-4 space-y-4'>
<div className='px-4 space-y-4'>
{toolInputVarSchema.length > 0 && (
<Field
title={t(`${i18nPrefix}.inputVars`)}
@@ -118,7 +118,7 @@ const Panel: FC<NodePanelProps<ToolNodeType>> = ({
/>
)}
<div className='px-4 pt-4 pb-2'>
<div>
<OutputVars>
<>
<VarItem

View File

@@ -21,6 +21,7 @@ import AddVariable from './add-variable'
import NodeVariableItem from './node-variable-item'
import { isConversationVar, isENV, isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils'
import cn from '@/utils/classnames'
import { isExceptionVariable } from '@/app/components/workflow/utils'
const i18nPrefix = 'workflow.nodes.variableAssigner'
type GroupItem = {
@@ -128,12 +129,14 @@ const NodeGroupItem = ({
const node = isSystem ? nodes.find(node => node.data.type === BlockEnum.Start) : nodes.find(node => node.id === variable[0])
const varName = isSystem ? `sys.${variable[variable.length - 1]}` : variable.slice(1).join('.')
const isException = isExceptionVariable(varName, node?.data.type)
return (
<NodeVariableItem
key={index}
isEnv={isEnv}
isChatVar={isChatVar}
isException={isException}
node={node as Node}
varName={varName}
showBorder={showSelectedBorder || showSelectionBorder}

View File

@@ -17,6 +17,7 @@ type NodeVariableItemProps = {
writeMode?: string
showBorder?: boolean
className?: string
isException?: boolean
}
const i18nPrefix = 'workflow.nodes.assigner'
@@ -29,6 +30,7 @@ const NodeVariableItem = ({
writeMode,
showBorder,
className,
isException,
}: NodeVariableItemProps) => {
const { t } = useTranslation()
return (
@@ -50,14 +52,14 @@ const NodeVariableItem = ({
</div>
)}
<div className='flex items-center text-primary-600 w-full'>
{!isEnv && !isChatVar && <Variable02 className='shrink-0 w-3.5 h-3.5 text-primary-500' />}
{!isEnv && !isChatVar && <Variable02 className={cn('shrink-0 w-3.5 h-3.5 text-primary-500', isException && 'text-text-warning')} />}
{isEnv && <Env className='shrink-0 w-3.5 h-3.5 text-util-colors-violet-violet-600' />}
{!isChatVar && <div className={cn('max-w-[75px] truncate ml-0.5 system-xs-medium overflow-hidden text-ellipsis', isEnv && 'text-gray-900')} title={varName}>{varName}</div>}
{!isChatVar && <div className={cn('max-w-[75px] truncate ml-0.5 system-xs-medium overflow-hidden text-ellipsis', isEnv && 'text-gray-900', isException && 'text-text-warning')} title={varName}>{varName}</div>}
{isChatVar
&& <div className='flex items-center w-full gap-1'>
<div className='flex h-[18px] min-w-[18px] items-center gap-0.5 flex-1'>
<BubbleX className='w-3.5 h-3.5 text-util-colors-teal-teal-700' />
<div className='max-w-[75px] truncate ml-0.5 system-xs-medium overflow-hidden text-ellipsis text-util-colors-teal-teal-700'>{varName}</div>
<div className={cn('max-w-[75px] truncate ml-0.5 system-xs-medium overflow-hidden text-ellipsis text-util-colors-teal-teal-700')}>{varName}</div>
</div>
{writeMode && <Badge className='shrink-0' text={t(`${i18nPrefix}.operations.${writeMode}`)} />}
</div>

View File

@@ -193,6 +193,7 @@ const WorkflowPreview = () => {
created_at={workflowRunningData?.result?.created_at}
created_by={(workflowRunningData?.result?.created_by as any)?.name}
steps={workflowRunningData?.result?.total_steps}
exceptionCounts={workflowRunningData?.result?.exceptions_count}
/>
)}
{currentTab === 'DETAIL' && !workflowRunningData?.result && (

View File

@@ -258,6 +258,7 @@ const RunPanel: FC<RunProps> = ({ hideResult, activeTab = 'RESULT', runID, getRe
created_at={runDetail.created_at}
created_by={executor}
steps={runDetail.total_steps}
exceptionCounts={runDetail.exceptions_count}
/>
)}
{!loading && currentTab === 'TRACING' && (

View File

@@ -38,6 +38,12 @@ const MetaData: FC<Props> = ({
{status === 'succeeded' && (
<span>SUCCESS</span>
)}
{status === 'partial-succeeded' && (
<span>PARTIAL SUCCESS</span>
)}
{status === 'exception' && (
<span>EXCEPTION</span>
)}
{status === 'failed' && (
<span>FAIL</span>
)}

View File

@@ -19,6 +19,7 @@ import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/
import Button from '@/app/components/base/button'
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
import type { IterationDurationMap, NodeTracing } from '@/types/workflow'
import ErrorHandleTip from '@/app/components/workflow/nodes/_base/components/error-handle/error-handle-tip'
type Props = {
className?: string
@@ -128,6 +129,9 @@ const NodePanel: FC<Props> = ({
{nodeInfo.status === 'stopped' && (
<RiAlertFill className={cn('shrink-0 ml-2 w-4 h-4 text-text-warning-secondary', inMessage && 'w-3.5 h-3.5')} />
)}
{nodeInfo.status === 'exception' && (
<RiAlertFill className={cn('shrink-0 ml-2 w-4 h-4 text-text-warning-secondary', inMessage && 'w-3.5 h-3.5')} />
)}
{nodeInfo.status === 'running' && (
<div className='shrink-0 flex items-center text-text-accent text-[13px] leading-[16px] font-medium'>
<span className='mr-2 text-xs font-normal'>Running</span>
@@ -165,12 +169,24 @@ const NodePanel: FC<Props> = ({
<Split className='mt-2' />
</div>
)}
<div className={cn('px-[10px]', hideInfo && '!px-2 !py-0.5')}>
{nodeInfo.status === 'stopped' && (
<div className={cn('mb-1', hideInfo && '!px-2 !py-0.5')}>
{(nodeInfo.status === 'stopped') && (
<StatusContainer status='stopped'>
{t('workflow.tracing.stopBy', { user: nodeInfo.created_by ? nodeInfo.created_by.name : 'N/A' })}
</StatusContainer>
)}
{(nodeInfo.status === 'exception') && (
<StatusContainer status='stopped'>
{nodeInfo.error}
<a
href='https://docs.dify.ai/guides/workflow/error-handling/predefined-nodes-failure-logic'
target='_blank'
className='text-text-accent'
>
{t('workflow.common.learnMore')}
</a>
</StatusContainer>
)}
{nodeInfo.status === 'failed' && (
<StatusContainer status='failed'>
{nodeInfo.error}
@@ -207,6 +223,7 @@ const NodePanel: FC<Props> = ({
language={CodeLanguage.json}
value={nodeInfo.outputs}
isJSONStringifyBeauty
tip={<ErrorHandleTip type={nodeInfo.execution_metadata?.error_strategy} />}
/>
</div>
)}

View File

@@ -5,6 +5,7 @@ import StatusPanel from './status'
import MetaData from './meta'
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
import ErrorHandleTip from '@/app/components/workflow/nodes/_base/components/error-handle/error-handle-tip'
type ResultPanelProps = {
inputs?: string
@@ -19,6 +20,8 @@ type ResultPanelProps = {
finished_at?: number
steps?: number
showSteps?: boolean
exceptionCounts?: number
execution_metadata?: any
}
const ResultPanel: FC<ResultPanelProps> = ({
@@ -33,6 +36,8 @@ const ResultPanel: FC<ResultPanelProps> = ({
created_by,
steps,
showSteps,
exceptionCounts,
execution_metadata,
}) => {
const { t } = useTranslation()
return (
@@ -43,6 +48,7 @@ const ResultPanel: FC<ResultPanelProps> = ({
time={elapsed_time}
tokens={total_tokens}
error={error}
exceptionCounts={exceptionCounts}
/>
</div>
<div className='px-4 py-2 flex flex-col gap-2'>
@@ -69,6 +75,7 @@ const ResultPanel: FC<ResultPanelProps> = ({
language={CodeLanguage.json}
value={outputs}
isJSONStringifyBeauty
tip={<ErrorHandleTip type={execution_metadata?.error_strategy} />}
/>
)}
</div>

View File

@@ -14,10 +14,12 @@ const StatusContainer: FC<Props> = ({
return (
<div
className={cn(
'relative px-3 py-2.5 rounded-lg border system-xs-regular',
'relative px-3 py-2.5 rounded-lg border system-xs-regular break-all',
status === 'succeeded' && 'border-[rgba(23,178,106,0.8)] bg-workflow-display-success-bg bg-[url(~@/app/components/workflow/run/assets/bg-line-success.svg)] shadow-[inset_2px_2px_0_0_rgba(255,255,255,0.5),inset_0_1px_3px_0_rgba(0,0,0,0.12),inset_0_2px_24px_0_rgba(23,178,106,0.2),0_1px_2px_0_rgba(9,9,11,0.05),0_0_0_1px_rgba(0,0,0,0.05)] text-text-success',
status === 'partial-succeeded' && 'border-[rgba(23,178,106,0.8)] bg-workflow-display-success-bg bg-[url(~@/app/components/workflow/run/assets/bg-line-success.svg)] shadow-[inset_2px_2px_0_0_rgba(255,255,255,0.5),inset_0_1px_3px_0_rgba(0,0,0,0.12),inset_0_2px_24px_0_rgba(23,178,106,0.2),0_1px_2px_0_rgba(9,9,11,0.05),0_0_0_1px_rgba(0,0,0,0.05)] text-text-success',
status === 'failed' && 'border-[rgba(240,68,56,0.8)] bg-workflow-display-error-bg bg-[url(~@/app/components/workflow/run/assets/bg-line-error.svg)] shadow-[inset_2px_2px_0_0_rgba(255,255,255,0.5),inset_0_1px_3px_0_rgba(0,0,0,0.12),inset_0_2px_24px_0_rgba(240,68,56,0.2),0_1px_2px_0_rgba(9,9,11,0.05),0_0_0_1px_rgba(0,0,0,0.05)] text-text-warning',
status === 'stopped' && 'border-[rgba(247,144,9,0.8)] bg-workflow-display-warning-bg bg-[url(~@/app/components/workflow/run/assets/bg-line-warning.svg)] shadow-[inset_2px_2px_0_0_rgba(255,255,255,0.5),inset_0_1px_3px_0_rgba(0,0,0,0.12),inset_0_2px_24px_0_rgba(247,144,9,0.2),0_1px_2px_0_rgba(9,9,11,0.05),0_0_0_1px_rgba(0,0,0,0.05)] text-text-destructive',
status === 'exception' && 'border-[rgba(247,144,9,0.8)] bg-workflow-display-warning-bg bg-[url(~@/app/components/workflow/run/assets/bg-line-warning.svg)] shadow-[inset_2px_2px_0_0_rgba(255,255,255,0.5),inset_0_1px_3px_0_rgba(0,0,0,0.12),inset_0_2px_24px_0_rgba(247,144,9,0.2),0_1px_2px_0_rgba(9,9,11,0.05),0_0_0_1px_rgba(0,0,0,0.05)] text-text-destructive',
status === 'running' && 'border-[rgba(11,165,236,0.8)] bg-workflow-display-normal-bg bg-[url(~@/app/components/workflow/run/assets/bg-line-running.svg)] shadow-[inset_2px_2px_0_0_rgba(255,255,255,0.5),inset_0_1px_3px_0_rgba(0,0,0,0.12),inset_0_2px_24px_0_rgba(11,165,236,0.2),0_1px_2px_0_rgba(9,9,11,0.05),0_0_0_1px_rgba(0,0,0,0.05)] text-util-colors-blue-light-blue-light-600',
)}
>

View File

@@ -10,6 +10,7 @@ type ResultProps = {
time?: number
tokens?: number
error?: string
exceptionCounts?: number
}
const StatusPanel: FC<ResultProps> = ({
@@ -17,18 +18,23 @@ const StatusPanel: FC<ResultProps> = ({
time,
tokens,
error,
exceptionCounts,
}) => {
const { t } = useTranslation()
return (
<StatusContainer status={status}>
<div className='flex'>
<div className='flex-[33%] max-w-[120px]'>
<div className={cn(
'flex-[33%] max-w-[120px]',
status === 'partial-succeeded' && 'min-w-[140px]',
)}>
<div className='mb-1 text-text-tertiary system-2xs-medium-uppercase'>{t('runLog.resultPanel.status')}</div>
<div
className={cn(
'flex items-center gap-1 system-xs-semibold-uppercase',
status === 'succeeded' && 'text-util-colors-green-green-600',
status === 'partial-succeeded' && 'text-util-colors-green-green-600',
status === 'failed' && 'text-util-colors-red-red-600',
status === 'stopped' && 'text-util-colors-warning-warning-600',
status === 'running' && 'text-util-colors-blue-light-blue-light-600',
@@ -46,6 +52,18 @@ const StatusPanel: FC<ResultProps> = ({
<span>SUCCESS</span>
</>
)}
{status === 'partial-succeeded' && (
<>
<Indicator color={'green'} />
<span>PARTIAL SUCCESS</span>
</>
)}
{status === 'exception' && (
<>
<Indicator color={'yellow'} />
<span>EXCEPTION</span>
</>
)}
{status === 'failed' && (
<>
<Indicator color={'red'} />
@@ -87,8 +105,45 @@ const StatusPanel: FC<ResultProps> = ({
<>
<div className='my-2 h-[0.5px] bg-divider-subtle'/>
<div className='system-xs-regular text-text-destructive'>{error}</div>
{
!!exceptionCounts && (
<>
<div className='my-2 h-[0.5px] bg-divider-subtle'/>
<div className='system-xs-regular text-text-destructive'>
{t('workflow.nodes.common.errorHandle.partialSucceeded.tip', { num: exceptionCounts })}
</div>
</>
)
}
</>
)}
{
status === 'partial-succeeded' && !!exceptionCounts && (
<>
<div className='my-2 h-[0.5px] bg-divider-deep'/>
<div className='system-xs-medium text-text-warning'>
{t('workflow.nodes.common.errorHandle.partialSucceeded.tip', { num: exceptionCounts })}
</div>
</>
)
}
{
status === 'exception' && (
<>
<div className='my-2 h-[0.5px] bg-divider-deep'/>
<div className='system-xs-medium text-text-warning'>
{error}
<a
href='https://docs.dify.ai/guides/workflow/error-handling/predefined-nodes-failure-logic'
target='_blank'
className='text-text-accent'
>
{t('workflow.common.learnMore')}
</a>
</div>
</>
)
}
</StatusContainer>
)
}

View File

@@ -9,6 +9,10 @@ import type { VarType as VarKindType } from '@/app/components/workflow/nodes/too
import type { FileResponse, NodeTracing } from '@/types/workflow'
import type { Collection, Tool } from '@/app/components/tools/types'
import type { ChatVarType } from '@/app/components/workflow/panel/chat-variable-panel/type'
import type {
DefaultValueForm,
ErrorHandleTypeEnum,
} from '@/app/components/workflow/nodes/_base/components/error-handle/types'
export enum BlockEnum {
Start = 'start',
@@ -52,6 +56,7 @@ export type CommonNodeType<T = {}> = {
_targetBranches?: Branch[]
_isSingleRun?: boolean
_runningStatus?: NodeRunningStatus
_runningBranchId?: string
_singleRunningStatus?: NodeRunningStatus
_isCandidate?: boolean
_isBundled?: boolean
@@ -62,6 +67,7 @@ export type CommonNodeType<T = {}> = {
_iterationLength?: number
_iterationIndex?: number
_inParallelHovering?: boolean
_waitingRun?: boolean
isInIteration?: boolean
iteration_id?: string
selected?: boolean
@@ -70,14 +76,18 @@ export type CommonNodeType<T = {}> = {
type: BlockEnum
width?: number
height?: number
error_strategy?: ErrorHandleTypeEnum
default_value?: DefaultValueForm[]
} & T & Partial<Pick<ToolDefaultValue, 'provider_id' | 'provider_type' | 'provider_name' | 'tool_name'>>
export type CommonEdgeType = {
_hovering?: boolean
_connectedNodeIsHovering?: boolean
_connectedNodeIsSelected?: boolean
_run?: boolean
_isBundled?: boolean
_sourceRunningStatus?: NodeRunningStatus
_targetRunningStatus?: NodeRunningStatus
_waitingRun?: boolean
isInIteration?: boolean
iteration_id?: string
sourceType: BlockEnum
@@ -242,6 +252,7 @@ export type Var = {
options?: string[]
required?: boolean
des?: string
isException?: boolean
}
export type NodeOutPutVar = {
@@ -281,6 +292,7 @@ export enum NodeRunningStatus {
Running = 'running',
Succeeded = 'succeeded',
Failed = 'failed',
Exception = 'exception',
}
export type OnNodeAdd = (
@@ -331,6 +343,7 @@ export type WorkflowRunningData = {
showSteps?: boolean
total_steps?: number
files?: FileResponse[]
exceptions_count?: number
}
tracing?: NodeTracing[]
}

View File

@@ -19,7 +19,11 @@ import type {
ToolWithProvider,
ValueSelector,
} from './types'
import { BlockEnum, ErrorHandleMode } from './types'
import {
BlockEnum,
ErrorHandleMode,
NodeRunningStatus,
} from './types'
import {
CUSTOM_NODE,
ITERATION_CHILDREN_Z_INDEX,
@@ -761,3 +765,34 @@ export const getParallelInfo = (nodes: Node[], edges: Edge[], parentNodeId?: str
hasAbnormalEdges,
}
}
export const hasErrorHandleNode = (nodeType?: BlockEnum) => {
return nodeType === BlockEnum.LLM || nodeType === BlockEnum.Tool || nodeType === BlockEnum.HttpRequest || nodeType === BlockEnum.Code
}
export const getEdgeColor = (nodeRunningStatus?: NodeRunningStatus, isFailBranch?: boolean) => {
if (nodeRunningStatus === NodeRunningStatus.Succeeded)
return 'var(--color-workflow-link-line-success-handle)'
if (nodeRunningStatus === NodeRunningStatus.Failed)
return 'var(--color-workflow-link-line-error-handle)'
if (nodeRunningStatus === NodeRunningStatus.Exception)
return 'var(--color-workflow-link-line-failure-handle)'
if (nodeRunningStatus === NodeRunningStatus.Running) {
if (isFailBranch)
return 'var(--color-workflow-link-line-failure-handle)'
return 'var(--color-workflow-link-line-handle)'
}
return 'var(--color-workflow-link-line-normal)'
}
export const isExceptionVariable = (variable: string, nodeType?: BlockEnum) => {
if ((variable === 'error_message' || variable === 'error_type') && hasErrorHandleNode(nodeType))
return true
return false
}