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

View File

@@ -1,12 +1,22 @@
import { useCallback, useState } from 'react'
import produce from 'immer'
import { useBoolean } from 'ahooks'
import { type OutputVar } from '../../code/types'
import type { ValueSelector } from '@/app/components/workflow/types'
import { VarType } from '@/app/components/workflow/types'
import type {
CodeNodeType,
OutputVar,
} from '../../code/types'
import type {
ValueSelector,
} from '@/app/components/workflow/types'
import {
BlockEnum,
VarType,
} from '@/app/components/workflow/types'
import {
useWorkflow,
} from '@/app/components/workflow/hooks'
import { ErrorHandleTypeEnum } from '@/app/components/workflow/nodes/_base/components/error-handle/types'
import { getDefaultValue } from '@/app/components/workflow/nodes/_base/components/error-handle/utils'
type Params<T> = {
id: string
@@ -29,6 +39,9 @@ function useOutputVarList<T>({
const handleVarsChange = useCallback((newVars: OutputVar, changedIndex?: number, newKey?: string) => {
const newInputs = produce(inputs, (draft: any) => {
draft[varKey] = newVars
if ((inputs as CodeNodeType).type === BlockEnum.Code && (inputs as CodeNodeType).error_strategy === ErrorHandleTypeEnum.defaultValue && varKey === 'outputs')
draft.default_value = getDefaultValue(draft as any)
})
setInputs(newInputs)
@@ -59,6 +72,9 @@ function useOutputVarList<T>({
children: null,
},
}
if ((inputs as CodeNodeType).type === BlockEnum.Code && (inputs as CodeNodeType).error_strategy === ErrorHandleTypeEnum.defaultValue && varKey === 'outputs')
draft.default_value = getDefaultValue(draft as any)
})
setInputs(newInputs)
onOutputKeyOrdersChange([...outputKeyOrders, newKey])
@@ -84,6 +100,9 @@ function useOutputVarList<T>({
const newInputs = produce(inputs, (draft: any) => {
delete draft[varKey][key]
if ((inputs as CodeNodeType).type === BlockEnum.Code && (inputs as CodeNodeType).error_strategy === ErrorHandleTypeEnum.defaultValue && varKey === 'outputs')
draft.default_value = getDefaultValue(draft as any)
})
setInputs(newInputs)
onOutputKeyOrdersChange(outputKeyOrders.filter((_, i) => i !== index))

View File

@@ -10,8 +10,9 @@ import {
useRef,
} from 'react'
import {
RiCheckboxCircleLine,
RiErrorWarningLine,
RiAlertFill,
RiCheckboxCircleFill,
RiErrorWarningFill,
RiLoader2Line,
} from '@remixicon/react'
import { useTranslation } from 'react-i18next'
@@ -24,6 +25,7 @@ import {
useNodesReadOnly,
useToolIcon,
} from '../../hooks'
import { hasErrorHandleNode } from '../../utils'
import { useNodeIterationInteractions } from '../iteration/use-interactions'
import type { IterationNodeType } from '../iteration/types'
import {
@@ -32,6 +34,7 @@ import {
} from './components/node-handle'
import NodeResizer from './components/node-resizer'
import NodeControl from './components/node-control'
import ErrorHandleOnNode from './components/error-handle/error-handle-on-node'
import AddVariablePopupWithPosition from './components/add-variable-popup-with-position'
import cn from '@/utils/classnames'
import BlockIcon from '@/app/components/workflow/block-icon'
@@ -71,11 +74,13 @@ const BaseNode: FC<BaseNodeProps> = ({
showRunningBorder,
showSuccessBorder,
showFailedBorder,
showExceptionBorder,
} = useMemo(() => {
return {
showRunningBorder: data._runningStatus === NodeRunningStatus.Running && !showSelectedBorder,
showSuccessBorder: data._runningStatus === NodeRunningStatus.Succeeded && !showSelectedBorder,
showFailedBorder: data._runningStatus === NodeRunningStatus.Failed && !showSelectedBorder,
showExceptionBorder: data._runningStatus === NodeRunningStatus.Exception && !showSelectedBorder,
}
}, [data._runningStatus, showSelectedBorder])
@@ -85,6 +90,7 @@ const BaseNode: FC<BaseNodeProps> = ({
'flex border-[2px] rounded-2xl',
showSelectedBorder ? 'border-components-option-card-option-selected-border' : 'border-transparent',
!showSelectedBorder && data._inParallelHovering && 'border-workflow-block-border-highlight',
data._waitingRun && 'opacity-70',
)}
ref={nodeRef}
style={{
@@ -99,9 +105,10 @@ const BaseNode: FC<BaseNodeProps> = ({
data.type !== BlockEnum.Iteration && 'w-[240px] bg-workflow-block-bg',
data.type === BlockEnum.Iteration && 'flex flex-col w-full h-full bg-[#fcfdff]/80',
!data._runningStatus && 'hover:shadow-lg',
showRunningBorder && '!border-primary-500',
showSuccessBorder && '!border-[#12B76A]',
showFailedBorder && '!border-[#F04438]',
showRunningBorder && '!border-state-accent-solid',
showSuccessBorder && '!border-state-success-solid',
showFailedBorder && '!border-state-destructive-solid',
showExceptionBorder && '!border-state-warning-solid',
data._isBundled && '!shadow-lg',
)}
>
@@ -192,24 +199,29 @@ const BaseNode: FC<BaseNodeProps> = ({
</div>
{
data._iterationLength && data._iterationIndex && data._runningStatus === NodeRunningStatus.Running && (
<div className='mr-1.5 text-xs font-medium text-primary-600'>
<div className='mr-1.5 text-xs font-medium text-text-accent'>
{data._iterationIndex > data._iterationLength ? data._iterationLength : data._iterationIndex}/{data._iterationLength}
</div>
)
}
{
(data._runningStatus === NodeRunningStatus.Running || data._singleRunningStatus === NodeRunningStatus.Running) && (
<RiLoader2Line className='w-3.5 h-3.5 text-primary-600 animate-spin' />
<RiLoader2Line className='w-3.5 h-3.5 text-text-accent animate-spin' />
)
}
{
data._runningStatus === NodeRunningStatus.Succeeded && (
<RiCheckboxCircleLine className='w-3.5 h-3.5 text-[#12B76A]' />
<RiCheckboxCircleFill className='w-3.5 h-3.5 text-text-success' />
)
}
{
data._runningStatus === NodeRunningStatus.Failed && (
<RiErrorWarningLine className='w-3.5 h-3.5 text-[#F04438]' />
<RiErrorWarningFill className='w-3.5 h-3.5 text-text-destructive' />
)
}
{
data._runningStatus === NodeRunningStatus.Exception && (
<RiAlertFill className='w-3.5 h-3.5 text-text-warning-secondary' />
)
}
</div>
@@ -225,6 +237,14 @@ const BaseNode: FC<BaseNodeProps> = ({
</div>
)
}
{
hasErrorHandleNode(data.type) && (
<ErrorHandleOnNode
id={id}
data={data}
/>
)
}
{
data.desc && data.type !== BlockEnum.Iteration && (
<div className='px-3 pt-1 pb-2 system-xs-regular text-text-tertiary whitespace-pre-line break-words'>

View File

@@ -20,6 +20,7 @@ import {
DescriptionInput,
TitleInput,
} from './components/title-description-input'
import ErrorHandleOnPanel from './components/error-handle/error-handle-on-panel'
import { useResizePanel } from './hooks/use-resize-panel'
import cn from '@/utils/classnames'
import BlockIcon from '@/app/components/workflow/block-icon'
@@ -34,7 +35,10 @@ import {
useWorkflow,
useWorkflowHistory,
} from '@/app/components/workflow/hooks'
import { canRunBySingle } from '@/app/components/workflow/utils'
import {
canRunBySingle,
hasErrorHandleNode,
} from '@/app/components/workflow/utils'
import Tooltip from '@/app/components/base/tooltip'
import type { Node } from '@/app/components/workflow/types'
import { useStore as useAppStore } from '@/app/components/app/store'
@@ -161,9 +165,17 @@ const BasePanel: FC<BasePanelProps> = ({
/>
</div>
</div>
<div className='py-2'>
<div>
{cloneElement(children, { id, data })}
</div>
{
hasErrorHandleNode(data.type) && (
<ErrorHandleOnPanel
id={id}
data={data}
/>
)
}
{
!!availableNextBlocks.length && (
<div className='p-4 border-t-[0.5px] border-t-black/5'>

View File

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

View File

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

View File

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

View File

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

View File

@@ -18,7 +18,6 @@ import Switch from '@/app/components/base/switch'
import Select from '@/app/components/base/select'
import Slider from '@/app/components/base/slider'
import Input from '@/app/components/base/input'
import Divider from '@/app/components/base/divider'
const i18nPrefix = 'workflow.nodes.iteration'
@@ -72,7 +71,7 @@ const Panel: FC<NodePanelProps<IterationNodeType>> = ({
} = useConfig(id, data)
return (
<div className='mt-2'>
<div className='pt-2 pb-2'>
<div className='px-4 pb-4 space-y-4'>
<Field
title={t(`${i18nPrefix}.input`)}
@@ -131,9 +130,7 @@ const Panel: FC<NodePanelProps<IterationNodeType>> = ({
</Field>
</div>)
}
<div className='px-4 py-2'>
<Divider className='h-[1px]'/>
</div>
<Split />
<div className='px-4 py-2'>
<Field title={t(`${i18nPrefix}.errorResponseMethod`)} >

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -14,6 +14,7 @@ import BeforeRunForm from '@/app/components/workflow/nodes/_base/components/befo
import ResultPanel from '@/app/components/workflow/run/result-panel'
import Split from '@/app/components/workflow/nodes/_base/components/split'
import OutputVars, { VarItem } from '@/app/components/workflow/nodes/_base/components/output-vars'
import { FieldCollapse } from '@/app/components/workflow/nodes/_base/components/collapse'
const i18nPrefix = 'workflow.nodes.questionClassifiers'
@@ -55,8 +56,8 @@ const Panel: FC<NodePanelProps<QuestionClassifierNodeType>> = ({
const model = inputs.model
return (
<div className='mt-2'>
<div className='px-4 pb-4 space-y-4'>
<div className='pt-2'>
<div className='px-4 space-y-4'>
<Field
title={t(`${i18nPrefix}.model`)}
>
@@ -107,27 +108,27 @@ const Panel: FC<NodePanelProps<QuestionClassifierNodeType>> = ({
readonly={readOnly}
/>
</Field>
<Field
title={t(`${i18nPrefix}.advancedSetting`)}
supportFold
>
<AdvancedSetting
hideMemorySetting={!isChatMode}
instruction={inputs.instruction}
onInstructionChange={handleInstructionChange}
memory={inputs.memory}
onMemoryChange={handleMemoryChange}
readonly={readOnly}
isChatApp={isChatMode}
isChatModel={isChatModel}
hasSetBlockStatus={hasSetBlockStatus}
nodesOutputVars={availableVars}
availableNodes={availableNodesWithParent}
/>
</Field>
<Split />
</div>
<FieldCollapse
title={t(`${i18nPrefix}.advancedSetting`)}
>
<AdvancedSetting
hideMemorySetting={!isChatMode}
instruction={inputs.instruction}
onInstructionChange={handleInstructionChange}
memory={inputs.memory}
onMemoryChange={handleMemoryChange}
readonly={readOnly}
isChatApp={isChatMode}
isChatModel={isChatModel}
hasSetBlockStatus={hasSetBlockStatus}
nodesOutputVars={availableVars}
availableNodes={availableNodesWithParent}
/>
</FieldCollapse>
<Split />
<div className='px-4 pt-4 pb-2'>
<div>
<OutputVars>
<>
<VarItem

View File

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

View File

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

View File

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

View File

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