mirror of
https://git.mirrors.martin98.com/https://github.com/langgenius/dify.git
synced 2025-04-22 13:49:45 +08:00
feat: workflow continue on error (#11474)
This commit is contained in:
parent
86dfdcb8ec
commit
bec5451f12
@ -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]' />
|
||||
|
@ -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
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
@ -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
|
@ -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}
|
||||
|
@ -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>
|
||||
|
@ -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')}
|
||||
|
||||
<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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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')}
|
||||
|
||||
<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
|
@ -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,
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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 []
|
||||
}
|
@ -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
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
|
@ -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}
|
||||
>
|
||||
|
@ -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 = {
|
||||
|
@ -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}
|
||||
|
@ -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, (() => {
|
||||
|
@ -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>
|
||||
|
@ -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>))
|
||||
|
@ -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))
|
||||
|
@ -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'>
|
||||
|
@ -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'>
|
||||
|
@ -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'
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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}
|
||||
>
|
||||
|
@ -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`)} >
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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}
|
||||
|
@ -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) => (
|
||||
|
@ -26,6 +26,16 @@ const nodeDefault: NodeDefault<QuestionClassifierNodeType> = {
|
||||
name: '',
|
||||
},
|
||||
],
|
||||
_targetBranches: [
|
||||
{
|
||||
id: '1',
|
||||
name: '',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: '',
|
||||
},
|
||||
],
|
||||
vision: {
|
||||
enabled: false,
|
||||
},
|
||||
|
@ -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
|
||||
|
@ -95,7 +95,7 @@ const Panel: FC<NodePanelProps<TemplateTransformNodeType>> = ({
|
||||
/>
|
||||
</div>
|
||||
<Split />
|
||||
<div className='px-4 pt-4 pb-2'>
|
||||
<div>
|
||||
<OutputVars>
|
||||
<>
|
||||
<VarItem
|
||||
|
@ -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
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
|
@ -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 && (
|
||||
|
@ -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' && (
|
||||
|
@ -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>
|
||||
)}
|
||||
|
@ -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>
|
||||
)}
|
||||
|
@ -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>
|
||||
|
@ -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',
|
||||
)}
|
||||
>
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
@ -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[]
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -102,6 +102,8 @@ const translation = {
|
||||
addParallelNode: 'Add Parallel Node',
|
||||
parallel: 'PARALLEL',
|
||||
branch: 'BRANCH',
|
||||
onFailure: 'On Failure',
|
||||
addFailureBranch: 'Add Fail Branch',
|
||||
},
|
||||
env: {
|
||||
envPanelTitle: 'Environment Variables',
|
||||
@ -302,6 +304,31 @@ const translation = {
|
||||
tip: 'Chat memory',
|
||||
builtIn: 'Built-in',
|
||||
},
|
||||
errorHandle: {
|
||||
title: 'Error Handling',
|
||||
tip: 'Exception handling strategy, triggered when a node encounters an exception.',
|
||||
none: {
|
||||
title: 'None',
|
||||
desc: 'The node will stop running if an exception occurs and is not handled',
|
||||
},
|
||||
defaultValue: {
|
||||
title: 'Default Value',
|
||||
desc: 'When an error occurs, specify a static output content.',
|
||||
tip: 'On error, will return below value.',
|
||||
inLog: 'Node exception, outputting according to default values.',
|
||||
output: 'Output Default Value',
|
||||
},
|
||||
failBranch: {
|
||||
title: 'Fail Branch',
|
||||
desc: 'When an error occurs, it will execute the exception branch',
|
||||
customize: 'Go to the canvas to customize the fail branch logic.',
|
||||
customizeTip: 'When the fail branch is activated, exceptions thrown by nodes will not terminate the process. Instead, it will automatically execute the predefined fail branch, allowing you to flexibly provide error messages, reports, fixes, or skip actions.',
|
||||
inLog: 'Node exception, will automatically execute the fail branch. The node output will return an error type and error message and pass them to downstream.',
|
||||
},
|
||||
partialSucceeded: {
|
||||
tip: 'There are {{num}} nodes in the process running abnormally, please go to tracing to check the logs.',
|
||||
},
|
||||
},
|
||||
},
|
||||
start: {
|
||||
required: 'required',
|
||||
|
@ -101,6 +101,8 @@ const translation = {
|
||||
addParallelNode: '添加并行节点',
|
||||
parallel: '并行',
|
||||
branch: '分支',
|
||||
onFailure: '异常时',
|
||||
addFailureBranch: '添加异常分支',
|
||||
},
|
||||
env: {
|
||||
envPanelTitle: '环境变量',
|
||||
@ -301,6 +303,31 @@ const translation = {
|
||||
tip: '聊天记忆',
|
||||
builtIn: '内置',
|
||||
},
|
||||
errorHandle: {
|
||||
title: '异常处理',
|
||||
tip: '配置异常处理策略,当节点发生异常时触发。',
|
||||
none: {
|
||||
title: '无',
|
||||
desc: '当发生异常且未处理时,节点将停止运行',
|
||||
},
|
||||
defaultValue: {
|
||||
title: '默认值',
|
||||
desc: '当发生异常时,指定默认输出内容。',
|
||||
tip: '当发生异常时,将返回以下值。',
|
||||
inLog: '节点异常,根据默认值输出。',
|
||||
output: '输出默认值',
|
||||
},
|
||||
failBranch: {
|
||||
title: '异常分支',
|
||||
desc: '当发生异常时,将执行异常分支',
|
||||
customize: '在画布自定义失败分支逻辑。',
|
||||
customizeTip: '当节点发生异常时,将自动执行失败分支。失败分支允许您灵活地提供错误消息、报告、修复或跳过操作。',
|
||||
inLog: '节点异常,将自动执行失败分支。节点输出将返回错误类型和错误信息,并传递给下游。',
|
||||
},
|
||||
partialSucceeded: {
|
||||
tip: '流程中有 {{num}} 个节点运行异常,请前往追踪查看日志。',
|
||||
},
|
||||
},
|
||||
},
|
||||
start: {
|
||||
required: '必填',
|
||||
|
@ -297,6 +297,7 @@ export type WorkflowRunDetailResponse = {
|
||||
created_by_end_user?: EndUserInfo
|
||||
created_at: number
|
||||
finished_at: number
|
||||
exceptions_count?: number
|
||||
}
|
||||
|
||||
export type AgentLogMeta = {
|
||||
|
@ -7,6 +7,7 @@ import type {
|
||||
Node,
|
||||
} from '@/app/components/workflow/types'
|
||||
import type { TransferMethod } from '@/types/app'
|
||||
import type { ErrorHandleTypeEnum } from '@/app/components/workflow/nodes/_base/components/error-handle/types'
|
||||
|
||||
export type NodeTracing = {
|
||||
id: string
|
||||
@ -22,7 +23,7 @@ export type NodeTracing = {
|
||||
parallel_run_id?: string
|
||||
error?: string
|
||||
elapsed_time: number
|
||||
execution_metadata: {
|
||||
execution_metadata?: {
|
||||
total_tokens: number
|
||||
total_price: number
|
||||
currency: string
|
||||
@ -34,6 +35,7 @@ export type NodeTracing = {
|
||||
parent_parallel_start_node_id?: string
|
||||
parallel_mode_run_id?: string
|
||||
iteration_duration_map?: IterationDurationMap
|
||||
error_strategy?: ErrorHandleTypeEnum
|
||||
}
|
||||
metadata: {
|
||||
iterator_length: number
|
||||
@ -172,6 +174,7 @@ export type NodeFinishedResponse = {
|
||||
iteration_index?: number
|
||||
iteration_id?: string
|
||||
parallel_mode_run_id: string
|
||||
error_strategy?: ErrorHandleTypeEnum
|
||||
}
|
||||
created_at: number
|
||||
files?: FileResponse[]
|
||||
|
Loading…
x
Reference in New Issue
Block a user