feat: workflow continue on error (#11474)

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

View File

@@ -59,7 +59,7 @@ const BeforeRunForm: FC<BeforeRunFormProps> = ({
}) => {
const { t } = useTranslation()
const isFinished = runningStatus === NodeRunningStatus.Succeeded || runningStatus === NodeRunningStatus.Failed
const isFinished = runningStatus === NodeRunningStatus.Succeeded || runningStatus === NodeRunningStatus.Failed || runningStatus === NodeRunningStatus.Exception
const isRunning = runningStatus === NodeRunningStatus.Running
const isFileLoaded = (() => {
// system files

View File

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

View File

@@ -0,0 +1,56 @@
import { useState } from 'react'
import { RiArrowDropRightLine } from '@remixicon/react'
import cn from '@/utils/classnames'
export { default as FieldCollapse } from './field-collapse'
type CollapseProps = {
disabled?: boolean
trigger: JSX.Element
children: JSX.Element
collapsed?: boolean
onCollapse?: (collapsed: boolean) => void
}
const Collapse = ({
disabled,
trigger,
children,
collapsed,
onCollapse,
}: CollapseProps) => {
const [collapsedLocal, setCollapsedLocal] = useState(true)
const collapsedMerged = collapsed !== undefined ? collapsed : collapsedLocal
return (
<>
<div
className='flex items-center'
onClick={() => {
if (!disabled) {
setCollapsedLocal(!collapsedMerged)
onCollapse?.(!collapsedMerged)
}
}}
>
<div className='shrink-0 w-4 h-4'>
{
!disabled && (
<RiArrowDropRightLine
className={cn(
'w-4 h-4 text-text-tertiary',
!collapsedMerged && 'transform rotate-90',
)}
/>
)
}
</div>
{trigger}
</div>
{
!collapsedMerged && children
}
</>
)
}
export default Collapse

View File

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

View File

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

View File

@@ -0,0 +1,89 @@
import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import type { DefaultValueForm } from './types'
import Input from '@/app/components/base/input'
import { VarType } from '@/app/components/workflow/types'
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
type DefaultValueProps = {
forms: DefaultValueForm[]
onFormChange: (form: DefaultValueForm) => void
}
const DefaultValue = ({
forms,
onFormChange,
}: DefaultValueProps) => {
const { t } = useTranslation()
const getFormChangeHandler = useCallback(({ key, type }: DefaultValueForm) => {
return (payload: any) => {
let value
if (type === VarType.string || type === VarType.number)
value = payload.target.value
if (type === VarType.array || type === VarType.arrayNumber || type === VarType.arrayString || type === VarType.arrayObject || type === VarType.arrayFile || type === VarType.object)
value = payload
onFormChange({ key, type, value })
}
}, [onFormChange])
return (
<div className='px-4 pt-2'>
<div className='mb-2 body-xs-regular text-text-tertiary'>
{t('workflow.nodes.common.errorHandle.defaultValue.desc')}
&nbsp;
<a
href='https://docs.dify.ai/guides/workflow/error-handling'
target='_blank'
className='text-text-accent'
>
{t('workflow.common.learnMore')}
</a>
</div>
<div className='space-y-1'>
{
forms.map((form, index) => {
return (
<div
key={index}
className='py-1'
>
<div className='flex items-center mb-1'>
<div className='mr-1 system-sm-medium text-text-primary'>{form.key}</div>
<div className='system-xs-regular text-text-tertiary'>{form.type}</div>
</div>
{
(form.type === VarType.string || form.type === VarType.number) && (
<Input
type={form.type}
value={form.value || (form.type === VarType.string ? '' : 0)}
onChange={getFormChangeHandler({ key: form.key, type: form.type })}
/>
)
}
{
(
form.type === VarType.array
|| form.type === VarType.arrayNumber
|| form.type === VarType.arrayString
|| form.type === VarType.arrayObject
|| form.type === VarType.object
) && (
<CodeEditor
language={CodeLanguage.json}
value={form.value}
onChange={getFormChangeHandler({ key: form.key, type: form.type })}
/>
)
}
</div>
)
})
}
</div>
</div>
)
}
export default DefaultValue

View File

@@ -0,0 +1,67 @@
import { useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import { useUpdateNodeInternals } from 'reactflow'
import { NodeSourceHandle } from '../node-handle'
import { ErrorHandleTypeEnum } from './types'
import type { Node } from '@/app/components/workflow/types'
import { NodeRunningStatus } from '@/app/components/workflow/types'
import cn from '@/utils/classnames'
type ErrorHandleOnNodeProps = Pick<Node, 'id' | 'data'>
const ErrorHandleOnNode = ({
id,
data,
}: ErrorHandleOnNodeProps) => {
const { t } = useTranslation()
const { error_strategy } = data
const updateNodeInternals = useUpdateNodeInternals()
useEffect(() => {
if (error_strategy === ErrorHandleTypeEnum.failBranch)
updateNodeInternals(id)
}, [error_strategy, id, updateNodeInternals])
if (!error_strategy)
return null
return (
<div className='relative pt-1 pb-2 px-3'>
<div className={cn(
'relative flex items-center justify-between px-[5px] h-6 bg-workflow-block-parma-bg rounded-md',
data._runningStatus === NodeRunningStatus.Exception && 'border-[0.5px] border-components-badge-status-light-warning-halo bg-state-warning-hover',
)}>
<div className='system-xs-medium-uppercase text-text-tertiary'>
{t('workflow.common.onFailure')}
</div>
<div className={cn(
'system-xs-medium text-text-secondary',
data._runningStatus === NodeRunningStatus.Exception && 'text-text-warning',
)}>
{
error_strategy === ErrorHandleTypeEnum.defaultValue && (
t('workflow.nodes.common.errorHandle.defaultValue.output')
)
}
{
error_strategy === ErrorHandleTypeEnum.failBranch && (
t('workflow.nodes.common.errorHandle.failBranch.title')
)
}
</div>
{
error_strategy === ErrorHandleTypeEnum.failBranch && (
<NodeSourceHandle
id={id}
data={data}
handleId={ErrorHandleTypeEnum.failBranch}
handleClassName='!top-1/2 !-right-[21px] !-translate-y-1/2 after:!bg-workflow-link-line-failure-button-bg'
nodeSelectorClassName='!bg-workflow-link-line-failure-button-bg'
/>
)
}
</div>
</div>
)
}
export default ErrorHandleOnNode

View File

@@ -0,0 +1,90 @@
import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import Collapse from '../collapse'
import { ErrorHandleTypeEnum } from './types'
import ErrorHandleTypeSelector from './error-handle-type-selector'
import FailBranchCard from './fail-branch-card'
import DefaultValue from './default-value'
import {
useDefaultValue,
useErrorHandle,
} from './hooks'
import type { DefaultValueForm } from './types'
import type {
CommonNodeType,
Node,
} from '@/app/components/workflow/types'
import Split from '@/app/components/workflow/nodes/_base/components/split'
import Tooltip from '@/app/components/base/tooltip'
type ErrorHandleProps = Pick<Node, 'id' | 'data'>
const ErrorHandle = ({
id,
data,
}: ErrorHandleProps) => {
const { t } = useTranslation()
const { error_strategy, default_value } = data
const {
collapsed,
setCollapsed,
handleErrorHandleTypeChange,
} = useErrorHandle(id, data)
const { handleFormChange } = useDefaultValue(id)
const getHandleErrorHandleTypeChange = useCallback((data: CommonNodeType) => {
return (value: ErrorHandleTypeEnum) => {
handleErrorHandleTypeChange(value, data)
}
}, [handleErrorHandleTypeChange])
const getHandleFormChange = useCallback((data: CommonNodeType) => {
return (v: DefaultValueForm) => {
handleFormChange(v, data)
}
}, [handleFormChange])
return (
<>
<Split />
<div className='py-4'>
<Collapse
disabled={!error_strategy}
collapsed={collapsed}
onCollapse={setCollapsed}
trigger={
<div className='grow flex items-center justify-between pr-4'>
<div className='flex items-center'>
<div className='mr-0.5 system-sm-semibold-uppercase text-text-secondary'>
{t('workflow.nodes.common.errorHandle.title')}
</div>
<Tooltip popupContent={t('workflow.nodes.common.errorHandle.tip')} />
</div>
<ErrorHandleTypeSelector
value={error_strategy || ErrorHandleTypeEnum.none}
onSelected={getHandleErrorHandleTypeChange(data)}
/>
</div>
}
>
<>
{
error_strategy === ErrorHandleTypeEnum.failBranch && !collapsed && (
<FailBranchCard />
)
}
{
error_strategy === ErrorHandleTypeEnum.defaultValue && !collapsed && !!default_value?.length && (
<DefaultValue
forms={default_value}
onFormChange={getHandleFormChange(data)}
/>
)
}
</>
</Collapse>
</div>
</>
)
}
export default ErrorHandle

View File

@@ -0,0 +1,43 @@
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { RiAlertFill } from '@remixicon/react'
import { ErrorHandleTypeEnum } from './types'
type ErrorHandleTipProps = {
type?: ErrorHandleTypeEnum
}
const ErrorHandleTip = ({
type,
}: ErrorHandleTipProps) => {
const { t } = useTranslation()
const text = useMemo(() => {
if (type === ErrorHandleTypeEnum.failBranch)
return t('workflow.nodes.common.errorHandle.failBranch.inLog')
if (type === ErrorHandleTypeEnum.defaultValue)
return t('workflow.nodes.common.errorHandle.defaultValue.inLog')
}, [])
if (!type)
return null
return (
<div
className='relative flex p-2 pr-[52px] rounded-lg border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-xs'
>
<div
className='absolute inset-0 opacity-40 rounded-lg'
style={{
background: 'linear-gradient(92deg, rgba(247, 144, 9, 0.25) 0%, rgba(255, 255, 255, 0.00) 100%)',
}}
></div>
<RiAlertFill className='shrink-0 mr-1 w-4 h-4 text-text-warning-secondary' />
<div className='grow system-xs-medium text-text-primary'>
{text}
</div>
</div>
)
}
export default ErrorHandleTip

View File

@@ -0,0 +1,95 @@
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import {
RiArrowDownSLine,
RiCheckLine,
} from '@remixicon/react'
import { ErrorHandleTypeEnum } from './types'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import Button from '@/app/components/base/button'
type ErrorHandleTypeSelectorProps = {
value: ErrorHandleTypeEnum
onSelected: (value: ErrorHandleTypeEnum) => void
}
const ErrorHandleTypeSelector = ({
value,
onSelected,
}: ErrorHandleTypeSelectorProps) => {
const { t } = useTranslation()
const [open, setOpen] = useState(false)
const options = [
{
value: ErrorHandleTypeEnum.none,
label: t('workflow.nodes.common.errorHandle.none.title'),
description: t('workflow.nodes.common.errorHandle.none.desc'),
},
{
value: ErrorHandleTypeEnum.defaultValue,
label: t('workflow.nodes.common.errorHandle.defaultValue.title'),
description: t('workflow.nodes.common.errorHandle.defaultValue.desc'),
},
{
value: ErrorHandleTypeEnum.failBranch,
label: t('workflow.nodes.common.errorHandle.failBranch.title'),
description: t('workflow.nodes.common.errorHandle.failBranch.desc'),
},
]
const selectedOption = options.find(option => option.value === value)
return (
<PortalToFollowElem
open={open}
onOpenChange={setOpen}
placement='bottom-end'
offset={4}
>
<PortalToFollowElemTrigger onClick={(e) => {
e.stopPropagation()
setOpen(v => !v)
}}>
<Button
size='small'
>
{selectedOption?.label}
<RiArrowDownSLine className='w-3.5 h-3.5' />
</Button>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-[11]'>
<div className='p-1 w-[280px] border-[0.5px] border-components-panel-border rounded-xl bg-components-panel-bg-blur shadow-lg'>
{
options.map(option => (
<div
key={option.value}
className='flex p-2 pr-3 rounded-lg hover:bg-state-base-hover cursor-pointer'
onClick={(e) => {
e.stopPropagation()
onSelected(option.value)
setOpen(false)
}}
>
<div className='mr-1 w-4 shrink-0'>
{
value === option.value && (
<RiCheckLine className='w-4 h-4 text-text-accent' />
)
}
</div>
<div className='grow'>
<div className='mb-0.5 system-sm-semibold text-text-secondary'>{option.label}</div>
<div className='system-xs-regular text-text-tertiary'>{option.description}</div>
</div>
</div>
))
}
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
)
}
export default ErrorHandleTypeSelector

View File

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

View File

@@ -0,0 +1,123 @@
import {
useCallback,
useMemo,
useState,
} from 'react'
import { ErrorHandleTypeEnum } from './types'
import type { DefaultValueForm } from './types'
import { getDefaultValue } from './utils'
import type {
CommonNodeType,
} from '@/app/components/workflow/types'
import {
useEdgesInteractions,
useNodeDataUpdate,
} from '@/app/components/workflow/hooks'
export const useDefaultValue = (
id: string,
) => {
const { handleNodeDataUpdateWithSyncDraft } = useNodeDataUpdate()
const handleFormChange = useCallback((
{
key,
value,
type,
}: DefaultValueForm,
data: CommonNodeType,
) => {
const default_value = data.default_value || []
const index = default_value.findIndex(form => form.key === key)
if (index > -1) {
const newDefaultValue = [...default_value]
newDefaultValue[index].value = value
handleNodeDataUpdateWithSyncDraft({
id,
data: {
default_value: newDefaultValue,
},
})
return
}
handleNodeDataUpdateWithSyncDraft({
id,
data: {
default_value: [
...default_value,
{
key,
value,
type,
},
],
},
})
}, [handleNodeDataUpdateWithSyncDraft, id])
return {
handleFormChange,
}
}
export const useErrorHandle = (
id: string,
data: CommonNodeType,
) => {
const initCollapsed = useMemo(() => {
if (data.error_strategy === ErrorHandleTypeEnum.none)
return true
return false
}, [data.error_strategy])
const [collapsed, setCollapsed] = useState(initCollapsed)
const { handleNodeDataUpdateWithSyncDraft } = useNodeDataUpdate()
const { handleEdgeDeleteByDeleteBranch } = useEdgesInteractions()
const handleErrorHandleTypeChange = useCallback((value: ErrorHandleTypeEnum, data: CommonNodeType) => {
if (data.error_strategy === value)
return
if (value === ErrorHandleTypeEnum.none) {
handleNodeDataUpdateWithSyncDraft({
id,
data: {
error_strategy: undefined,
default_value: undefined,
},
})
setCollapsed(true)
handleEdgeDeleteByDeleteBranch(id, ErrorHandleTypeEnum.failBranch)
}
if (value === ErrorHandleTypeEnum.failBranch) {
handleNodeDataUpdateWithSyncDraft({
id,
data: {
error_strategy: value,
default_value: undefined,
},
})
setCollapsed(false)
}
if (value === ErrorHandleTypeEnum.defaultValue) {
handleNodeDataUpdateWithSyncDraft({
id,
data: {
error_strategy: value,
default_value: getDefaultValue(data),
},
})
setCollapsed(false)
handleEdgeDeleteByDeleteBranch(id, ErrorHandleTypeEnum.failBranch)
}
}, [id, handleNodeDataUpdateWithSyncDraft, handleEdgeDeleteByDeleteBranch])
return {
collapsed,
setCollapsed,
handleErrorHandleTypeChange,
}
}

View File

@@ -0,0 +1,13 @@
import type { VarType } from '@/app/components/workflow/types'
export enum ErrorHandleTypeEnum {
none = 'none',
failBranch = 'fail-branch',
defaultValue = 'default-value',
}
export type DefaultValueForm = {
key: string
type: VarType
value?: any
}

View File

@@ -0,0 +1,83 @@
import type { CommonNodeType } from '@/app/components/workflow/types'
import {
BlockEnum,
VarType,
} from '@/app/components/workflow/types'
import type { CodeNodeType } from '@/app/components/workflow/nodes/code/types'
const getDefaultValueByType = (type: VarType) => {
if (type === VarType.string)
return ''
if (type === VarType.number)
return 0
if (type === VarType.object)
return '{}'
if (type === VarType.arrayObject || type === VarType.arrayString || type === VarType.arrayNumber || type === VarType.arrayFile)
return '[]'
return ''
}
export const getDefaultValue = (data: CommonNodeType) => {
const { type } = data
if (type === BlockEnum.LLM) {
return [{
key: 'text',
type: VarType.string,
value: getDefaultValueByType(VarType.string),
}]
}
if (type === BlockEnum.HttpRequest) {
return [
{
key: 'body',
type: VarType.string,
value: getDefaultValueByType(VarType.string),
},
{
key: 'status_code',
type: VarType.number,
value: getDefaultValueByType(VarType.number),
},
{
key: 'headers',
type: VarType.object,
value: getDefaultValueByType(VarType.object),
},
]
}
if (type === BlockEnum.Tool) {
return [
{
key: 'text',
type: VarType.string,
value: getDefaultValueByType(VarType.string),
},
{
key: 'json',
type: VarType.arrayObject,
value: getDefaultValueByType(VarType.arrayObject),
},
]
}
if (type === BlockEnum.Code) {
const { outputs } = data as CodeNodeType
return Object.keys(outputs).map((key) => {
return {
key,
type: outputs[key].type,
value: getDefaultValueByType(outputs[key].type),
}
})
}
return []
}

View File

@@ -1,6 +1,7 @@
import {
memo,
useCallback,
useMemo,
useState,
} from 'react'
import { useTranslation } from 'react-i18next'
@@ -24,12 +25,14 @@ type AddProps = {
nodeData: CommonNodeType
sourceHandle: string
isParallel?: boolean
isFailBranch?: boolean
}
const Add = ({
nodeId,
nodeData,
sourceHandle,
isParallel,
isFailBranch,
}: AddProps) => {
const { t } = useTranslation()
const [open, setOpen] = useState(false)
@@ -58,6 +61,15 @@ const Add = ({
setOpen(newOpen)
}, [checkParallelLimit, nodeId, sourceHandle])
const tip = useMemo(() => {
if (isFailBranch)
return t('workflow.common.addFailureBranch')
if (isParallel)
return t('workflow.common.addParallelNode')
return t('workflow.panel.selectNextStep')
}, [isFailBranch, isParallel, t])
const renderTrigger = useCallback((open: boolean) => {
return (
<div
@@ -72,15 +84,11 @@ const Add = ({
<RiAddLine className='w-3 h-3' />
</div>
<div className='flex items-center uppercase'>
{
isParallel
? t('workflow.common.addParallelNode')
: t('workflow.panel.selectNextStep')
}
{tip}
</div>
</div>
)
}, [t, nodesReadOnly, isParallel])
}, [nodesReadOnly, tip])
return (
<BlockSelector

View File

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

View File

@@ -14,6 +14,8 @@ import type {
import { BlockEnum } from '../../../../types'
import Line from './line'
import Container from './container'
import { hasErrorHandleNode } from '@/app/components/workflow/utils'
import { ErrorHandleTypeEnum } from '@/app/components/workflow/nodes/_base/components/error-handle/types'
type NextStepProps = {
selectedNode: Node
@@ -28,25 +30,54 @@ const NextStep = ({
const branches = useMemo(() => {
return data._targetBranches || []
}, [data])
const nodeWithBranches = data.type === BlockEnum.IfElse || data.type === BlockEnum.QuestionClassifier
const edges = useEdges()
const outgoers = getOutgoers(selectedNode as Node, store.getState().getNodes(), edges)
const connectedEdges = getConnectedEdges([selectedNode] as Node[], edges).filter(edge => edge.source === selectedNode!.id)
const branchesOutgoers = useMemo(() => {
if (!branches?.length)
return []
const list = useMemo(() => {
let items = []
if (branches?.length) {
items = branches.map((branch, index) => {
const connected = connectedEdges.filter(edge => edge.sourceHandle === branch.id)
const nextNodes = connected.map(edge => outgoers.find(outgoer => outgoer.id === edge.target)!)
return branches.map((branch) => {
const connected = connectedEdges.filter(edge => edge.sourceHandle === branch.id)
return {
branch: {
...branch,
name: data.type === BlockEnum.QuestionClassifier ? `${t('workflow.nodes.questionClassifiers.class')} ${index + 1}` : branch.name,
},
nextNodes,
}
})
}
else {
const connected = connectedEdges.filter(edge => edge.sourceHandle === 'source')
const nextNodes = connected.map(edge => outgoers.find(outgoer => outgoer.id === edge.target)!)
return {
branch,
items = [{
branch: {
id: '',
name: '',
},
nextNodes,
}]
if (data.error_strategy === ErrorHandleTypeEnum.failBranch && hasErrorHandleNode(data.type)) {
const connected = connectedEdges.filter(edge => edge.sourceHandle === ErrorHandleTypeEnum.failBranch)
const nextNodes = connected.map(edge => outgoers.find(outgoer => outgoer.id === edge.target)!)
items.push({
branch: {
id: ErrorHandleTypeEnum.failBranch,
name: t('workflow.common.onFailure'),
},
nextNodes,
})
}
})
}, [branches, connectedEdges, outgoers])
}
return items
}, [branches, connectedEdges, data.error_strategy, data.type, outgoers, t])
return (
<div className='flex py-1'>
@@ -57,34 +88,23 @@ const NextStep = ({
/>
</div>
<Line
list={nodeWithBranches ? branchesOutgoers.map(item => item.nextNodes.length + 1) : [1]}
list={list.length ? list.map(item => item.nextNodes.length + 1) : [1]}
/>
<div className='grow space-y-2'>
{
!nodeWithBranches && (
<Container
nodeId={selectedNode!.id}
nodeData={selectedNode!.data}
sourceHandle='source'
nextNodes={outgoers}
/>
)
}
{
nodeWithBranches && (
branchesOutgoers.map((item, index) => {
return (
<Container
key={item.branch.id}
nodeId={selectedNode!.id}
nodeData={selectedNode!.data}
sourceHandle={item.branch.id}
nextNodes={item.nextNodes}
branchName={item.branch.name || `${t('workflow.nodes.questionClassifiers.class')} ${index + 1}`}
/>
)
})
)
list.map((item, index) => {
return (
<Container
key={index}
nodeId={selectedNode!.id}
nodeData={selectedNode!.data}
sourceHandle={item.branch.id}
nextNodes={item.nextNodes}
branchName={item.branch.name}
isFailBranch={item.branch.id === ErrorHandleTypeEnum.failBranch}
/>
)
})
}
</div>
</div>

View File

@@ -10,7 +10,10 @@ import {
Position,
} from 'reactflow'
import { useTranslation } from 'react-i18next'
import { BlockEnum } from '../../../types'
import {
BlockEnum,
NodeRunningStatus,
} from '../../../types'
import type { Node } from '../../../types'
import BlockSelector from '../../../block-selector'
import type { ToolDefaultValue } from '../../../block-selector/types'
@@ -24,11 +27,13 @@ import {
import {
useStore,
} from '../../../store'
import cn from '@/utils/classnames'
type NodeHandleProps = {
handleId: string
handleClassName?: string
nodeSelectorClassName?: string
showExceptionStatus?: boolean
} & Pick<Node, 'id' | 'data'>
export const NodeTargetHandle = memo(({
@@ -72,14 +77,17 @@ export const NodeTargetHandle = memo(({
id={handleId}
type='target'
position={Position.Left}
className={`
!w-4 !h-4 !bg-transparent !rounded-none !outline-none !border-none z-[1]
after:absolute after:w-0.5 after:h-2 after:left-1.5 after:top-1 after:bg-primary-500
hover:scale-125 transition-all
${!connected && 'after:opacity-0'}
${data.type === BlockEnum.Start && 'opacity-0'}
${handleClassName}
`}
className={cn(
'!w-4 !h-4 !bg-transparent !rounded-none !outline-none !border-none z-[1]',
'after:absolute after:w-0.5 after:h-2 after:left-1.5 after:top-1 after:bg-workflow-link-line-handle',
'hover:scale-125 transition-all',
data._runningStatus === NodeRunningStatus.Succeeded && 'after:bg-workflow-link-line-success-handle',
data._runningStatus === NodeRunningStatus.Failed && 'after:bg-workflow-link-line-error-handle',
data._runningStatus === NodeRunningStatus.Exception && 'after:bg-workflow-link-line-failure-handle',
!connected && 'after:opacity-0',
data.type === BlockEnum.Start && 'opacity-0',
handleClassName,
)}
isConnectable={isConnectable}
onClick={handleHandleClick}
>
@@ -114,6 +122,7 @@ export const NodeSourceHandle = memo(({
handleId,
handleClassName,
nodeSelectorClassName,
showExceptionStatus,
}: NodeHandleProps) => {
const { t } = useTranslation()
const notInitialWorkflow = useStore(s => s.notInitialWorkflow)
@@ -157,13 +166,16 @@ export const NodeSourceHandle = memo(({
id={handleId}
type='source'
position={Position.Right}
className={`
group/handle !w-4 !h-4 !bg-transparent !rounded-none !outline-none !border-none z-[1]
after:absolute after:w-0.5 after:h-2 after:right-1.5 after:top-1 after:bg-primary-500
hover:scale-125 transition-all
${!connected && 'after:opacity-0'}
${handleClassName}
`}
className={cn(
'group/handle !w-4 !h-4 !bg-transparent !rounded-none !outline-none !border-none z-[1]',
'after:absolute after:w-0.5 after:h-2 after:right-1.5 after:top-1 after:bg-workflow-link-line-handle',
'hover:scale-125 transition-all',
data._runningStatus === NodeRunningStatus.Succeeded && 'after:bg-workflow-link-line-success-handle',
data._runningStatus === NodeRunningStatus.Failed && 'after:bg-workflow-link-line-error-handle',
showExceptionStatus && data._runningStatus === NodeRunningStatus.Exception && 'after:bg-workflow-link-line-failure-handle',
!connected && 'after:opacity-0',
handleClassName,
)}
isConnectable={isConnectable}
onClick={handleHandleClick}
>

View File

@@ -2,11 +2,7 @@
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { useBoolean } from 'ahooks'
import {
RiArrowDownSLine,
} from '@remixicon/react'
import cn from '@/utils/classnames'
import { FieldCollapse } from '@/app/components/workflow/nodes/_base/components/collapse'
type Props = {
className?: string
@@ -15,28 +11,14 @@ type Props = {
}
const OutputVars: FC<Props> = ({
className,
title,
children,
}) => {
const { t } = useTranslation()
const [isFold, {
toggle: toggleFold,
}] = useBoolean(true)
return (
<div>
<div
onClick={toggleFold}
className={cn(className, 'flex justify-between system-sm-semibold-uppercase text-text-secondary cursor-pointer')}>
<div>{title || t('workflow.nodes.common.outputVars')}</div>
<RiArrowDownSLine className='w-4 h-4 text-text-tertiary transform transition-transform' style={{ transform: isFold ? 'rotate(-90deg)' : 'rotate(0deg)' }} />
</div>
{!isFold && (
<div className='mt-2 space-y-1'>
{children}
</div>
)}
</div>
<FieldCollapse title={title || t('workflow.nodes.common.outputVars')}>
{children}
</FieldCollapse>
)
}
type VarItemProps = {

View File

@@ -17,6 +17,7 @@ import { BubbleX, Env } from '@/app/components/base/icons/src/vender/line/others
import { getNodeInfoById, isConversationVar, isENV, isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils'
import Tooltip from '@/app/components/base/tooltip'
import cn from '@/utils/classnames'
import { isExceptionVariable } from '@/app/components/workflow/utils'
type VariableTagProps = {
valueSelector: ValueSelector
@@ -45,6 +46,7 @@ const VariableTag = ({
const isValid = Boolean(node) || isEnv || isChatVar
const variableName = isSystemVar(valueSelector) ? valueSelector.slice(0).join('.') : valueSelector.slice(1).join('.')
const isException = isExceptionVariable(variableName, node?.data.type)
const { t } = useTranslation()
return (
@@ -67,12 +69,12 @@ const VariableTag = ({
</>
)}
<Line3 className='shrink-0 mx-0.5' />
<Variable02 className='shrink-0 mr-0.5 w-3.5 h-3.5 text-text-accent' />
<Variable02 className={cn('shrink-0 mr-0.5 w-3.5 h-3.5 text-text-accent', isException && 'text-text-warning')} />
</>)}
{isEnv && <Env className='shrink-0 mr-0.5 w-3.5 h-3.5 text-util-colors-violet-violet-600' />}
{isChatVar && <BubbleX className='w-3.5 h-3.5 text-util-colors-teal-teal-700' />}
<div
className={cn('truncate ml-0.5 text-text-accent font-medium', (isEnv || isChatVar) && 'text-text-secondary')}
className={cn('truncate ml-0.5 text-text-accent font-medium', (isEnv || isChatVar) && 'text-text-secondary', isException && 'text-text-warning')}
title={variableName}
>
{variableName}

View File

@@ -315,6 +315,24 @@ const formatItem = (
}
}
const { error_strategy } = data
if (error_strategy) {
res.vars = [
...res.vars,
{
variable: 'error_message',
type: VarType.string,
isException: true,
},
{
variable: 'error_type',
type: VarType.string,
isException: true,
},
]
}
const selector = [id]
res.vars = res.vars.filter((v) => {
const isCurrentMatched = filterVar(v, (() => {

View File

@@ -36,6 +36,7 @@ import TypeSelector from '@/app/components/workflow/nodes/_base/components/selec
import AddButton from '@/app/components/base/button/add-button'
import Badge from '@/app/components/base/badge'
import Tooltip from '@/app/components/base/tooltip'
import { isExceptionVariable } from '@/app/components/workflow/utils'
const TRIGGER_DEFAULT_WIDTH = 227
@@ -224,16 +225,18 @@ const VarReferencePicker: FC<Props> = ({
isConstant: !!isConstant,
})
const { isEnv, isChatVar, isValidVar } = useMemo(() => {
const { isEnv, isChatVar, isValidVar, isException } = useMemo(() => {
const isEnv = isENV(value as ValueSelector)
const isChatVar = isConversationVar(value as ValueSelector)
const isValidVar = Boolean(outputVarNode) || isEnv || isChatVar
const isException = isExceptionVariable(varName, outputVarNode?.type)
return {
isEnv,
isChatVar,
isValidVar,
isException,
}
}, [value, outputVarNode])
}, [value, outputVarNode, varName])
// 8(left/right-padding) + 14(icon) + 4 + 14 + 2 = 42 + 17 buff
const availableWidth = triggerWidth - 56
@@ -335,7 +338,7 @@ const VarReferencePicker: FC<Props> = ({
{!hasValue && <Variable02 className='w-3.5 h-3.5' />}
{isEnv && <Env className='w-3.5 h-3.5 text-util-colors-violet-violet-600' />}
{isChatVar && <BubbleX className='w-3.5 h-3.5 text-util-colors-teal-teal-700' />}
<div className={cn('ml-0.5 text-xs font-medium truncate', isEnv && '!text-text-secondary', isChatVar && 'text-util-colors-teal-teal-700')} title={varName} style={{
<div className={cn('ml-0.5 text-xs font-medium truncate', isEnv && '!text-text-secondary', isChatVar && 'text-util-colors-teal-teal-700', isException && 'text-text-warning')} title={varName} style={{
maxWidth: maxVarNameWidth,
}}>{varName}</div>
</div>

View File

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