feat: knowledge pipeline (#25360)

Signed-off-by: -LAN- <laipz8200@outlook.com>
Co-authored-by: twwu <twwu@dify.ai>
Co-authored-by: crazywoola <100913391+crazywoola@users.noreply.github.com>
Co-authored-by: jyong <718720800@qq.com>
Co-authored-by: Wu Tianwei <30284043+WTW0313@users.noreply.github.com>
Co-authored-by: QuantumGhost <obelisk.reg+git@gmail.com>
Co-authored-by: lyzno1 <yuanyouhuilyz@gmail.com>
Co-authored-by: quicksand <quicksandzn@gmail.com>
Co-authored-by: Jyong <76649700+JohnJyong@users.noreply.github.com>
Co-authored-by: lyzno1 <92089059+lyzno1@users.noreply.github.com>
Co-authored-by: zxhlyh <jasonapring2015@outlook.com>
Co-authored-by: Yongtao Huang <yongtaoh2022@gmail.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Joel <iamjoel007@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: nite-knite <nkCoding@gmail.com>
Co-authored-by: Hanqing Zhao <sherry9277@gmail.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: Harry <xh001x@hotmail.com>
This commit is contained in:
-LAN-
2025-09-18 12:49:10 +08:00
committed by GitHub
parent 7dadb33003
commit 85cda47c70
1772 changed files with 102407 additions and 31710 deletions

View File

@@ -17,7 +17,6 @@ import Textarea from '@/app/components/base/textarea'
import TextGenerationImageUploader from '@/app/components/base/image-uploader/text-generation-image-uploader'
import { FileUploaderInAttachmentWrapper } from '@/app/components/base/file-uploader'
import { Resolution, TransferMethod } from '@/types/app'
import { useFeatures } from '@/app/components/base/features/hooks'
import { VarBlockIcon } from '@/app/components/workflow/block-icon'
import { Line3 } from '@/app/components/base/icons/src/public/common'
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
@@ -26,6 +25,7 @@ import { FILE_EXTS } from '@/app/components/base/prompt-editor/constants'
import cn from '@/utils/classnames'
import type { FileEntity } from '@/app/components/base/file-uploader/types'
import BoolInput from './bool-input'
import { useHooksStore } from '@/app/components/workflow/hooks-store'
type Props = {
payload: InputVar
@@ -46,7 +46,8 @@ const FormItem: FC<Props> = ({
}) => {
const { t } = useTranslation()
const { type } = payload
const fileSettings = useFeatures(s => s.features.file)
const fileSettings = useHooksStore(s => s.configsMap?.fileSettings)
const handleArrayItemChange = useCallback((index: number) => {
return (newValue: any) => {
const newValues = produce(value, (draft: any) => {
@@ -187,7 +188,7 @@ const FormItem: FC<Props> = ({
/>
)
}
{ type === InputVarType.jsonObject && (
{type === InputVarType.jsonObject && (
<CodeEditor
value={value}
language={CodeLanguage.json}

View File

@@ -39,6 +39,7 @@ type Props = {
tip?: React.JSX.Element
nodesOutputVars?: NodeOutPutVar[]
availableNodes?: Node[]
footer?: React.ReactNode
}
const Base: FC<Props> = ({
@@ -57,6 +58,7 @@ const Base: FC<Props> = ({
showFileList,
showCodeGenerator = false,
tip,
footer,
}) => {
const ref = useRef<HTMLDivElement>(null)
const {
@@ -128,6 +130,7 @@ const Base: FC<Props> = ({
{showFileList && fileList.length > 0 && (
<FileListInLog fileList={fileList} />
)}
{footer}
</div>
</Wrap>
)

View File

@@ -39,6 +39,7 @@ export type Props = {
showCodeGenerator?: boolean
className?: string
tip?: React.JSX.Element
footer?: React.ReactNode
}
export const languageMap = {
@@ -67,6 +68,7 @@ const CodeEditor: FC<Props> = ({
showCodeGenerator = false,
className,
tip,
footer,
}) => {
const [isFocus, setIsFocus] = React.useState(false)
const [isMounted, setIsMounted] = React.useState(false)
@@ -191,6 +193,7 @@ const CodeEditor: FC<Props> = ({
showFileList={showFileList}
showCodeGenerator={showCodeGenerator}
tip={tip}
footer={footer}
>
{main}
</Base>

View File

@@ -17,7 +17,7 @@ const ErrorHandleTip = ({
if (type === ErrorHandleTypeEnum.defaultValue)
return t('workflow.nodes.common.errorHandle.defaultValue.inLog')
}, [])
}, [t, type])
if (!type)
return null

View File

@@ -47,7 +47,7 @@ const FileTypeItem: FC<Props> = ({
? (
<div>
<div className='flex items-center border-b border-divider-subtle p-3 pb-2'>
<FileTypeIcon className='shrink-0' type={type} size='md' />
<FileTypeIcon className='shrink-0' type={type} size='lg' />
<div className='system-sm-medium mx-2 grow text-text-primary'>{t(`appDebug.variableConfig.file.${type}.name`)}</div>
<Checkbox className='shrink-0' checked={selected} />
</div>
@@ -62,7 +62,7 @@ const FileTypeItem: FC<Props> = ({
)
: (
<div className='flex items-center'>
<FileTypeIcon className='shrink-0' type={type} size='md' />
<FileTypeIcon className='shrink-0' type={type} size='lg' />
<div className='mx-2 grow'>
<div className='system-sm-medium text-text-primary'>{t(`appDebug.variableConfig.file.${type}.name`)}</div>
<div className='system-2xs-regular-uppercase mt-1 text-text-tertiary'>{type !== SupportUploadFileTypes.custom ? FILE_EXTS[type].join(', ') : t('appDebug.variableConfig.file.custom.description')}</div>

View File

@@ -31,6 +31,8 @@ type Props = {
inPanel?: boolean
currentTool?: Tool
currentProvider?: ToolWithProvider
showManageInputField?: boolean
onManageInputField?: () => void
}
const FormInputItem: FC<Props> = ({
@@ -42,6 +44,8 @@ const FormInputItem: FC<Props> = ({
inPanel,
currentTool,
currentProvider,
showManageInputField,
onManageInputField,
}) => {
const language = useLanguage()
@@ -64,7 +68,7 @@ const FormInputItem: FC<Props> = ({
const isSelect = type === FormTypeEnum.select || type === FormTypeEnum.dynamicSelect
const isAppSelector = type === FormTypeEnum.appSelector
const isModelSelector = type === FormTypeEnum.modelSelector
const showTypeSwitch = isNumber || isBoolean || isObject || isArray
const showTypeSwitch = isNumber || isBoolean || isObject || isArray || isSelect
const isConstant = varInput?.type === VarKindType.constant || !varInput?.type
const showVariableSelector = isFile || varInput?.type === VarKindType.variable
@@ -84,8 +88,8 @@ const FormInputItem: FC<Props> = ({
return VarType.arrayFile
else if (type === FormTypeEnum.file)
return VarType.file
// else if (isSelect)
// return VarType.select
else if (isSelect)
return VarType.string
// else if (isAppSelector)
// return VarType.appSelector
// else if (isModelSelector)
@@ -192,6 +196,8 @@ const FormInputItem: FC<Props> = ({
onChange={handleValueChange}
nodesOutputVars={availableVars}
availableNodes={availableNodesWithParent}
showManageInputField={showManageInputField}
onManageInputField={onManageInputField}
/>
)}
{isNumber && isConstant && (
@@ -209,7 +215,7 @@ const FormInputItem: FC<Props> = ({
onChange={handleValueChange}
/>
)}
{isSelect && (
{isSelect && isConstant && (
<SimpleSelect
wrapperClassName='h-8 grow'
disabled={readOnly}
@@ -271,6 +277,7 @@ const FormInputItem: FC<Props> = ({
valueTypePlaceHolder={targetVarType()}
currentTool={currentTool}
currentProvider={currentProvider}
isFilterFileVar={isBoolean}
/>
)}
</div>

View File

@@ -0,0 +1,12 @@
import { RiAddLine } from '@remixicon/react'
import ActionButton from '@/app/components/base/action-button'
const Add = () => {
return (
<ActionButton>
<RiAddLine className='h-4 w-4' />
</ActionButton>
)
}
export default Add

View File

@@ -0,0 +1,24 @@
import { BoxGroupField } from '@/app/components/workflow/nodes/_base/components/layout'
import Add from './add'
const InputField = () => {
return (
<BoxGroupField
fieldProps={{
supportCollapse: true,
fieldTitleProps: {
title: 'input field',
operation: <Add />,
},
}}
boxGroupProps={{
boxProps: {
withBorderBottom: true,
},
}}
>
input field
</BoxGroupField>
)
}
export default InputField

View File

@@ -3,7 +3,7 @@ import type { FC } from 'react'
import React, { useCallback } from 'react'
import Slider from '@/app/components/base/slider'
type Props = {
export type InputNumberWithSliderProps = {
value: number
defaultValue?: number
min?: number
@@ -12,7 +12,7 @@ type Props = {
onChange: (value: number) => void
}
const InputNumberWithSlider: FC<Props> = ({
const InputNumberWithSlider: FC<InputNumberWithSliderProps> = ({
value,
defaultValue = 0,
min,

View File

@@ -13,6 +13,7 @@ import PromptEditor from '@/app/components/base/prompt-editor'
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
import Tooltip from '@/app/components/base/tooltip'
import { noop } from 'lodash-es'
import { useStore } from '@/app/components/workflow/store'
type Props = {
instanceId?: string
@@ -55,6 +56,9 @@ const Editor: FC<Props> = ({
onFocusChange?.(isFocus)
}, [isFocus])
const pipelineId = useStore(s => s.pipelineId)
const setShowInputFieldPanel = useStore(s => s.setShowInputFieldPanel)
return (
<div className={cn(className, 'relative')}>
<>
@@ -102,6 +106,8 @@ const Editor: FC<Props> = ({
}
return acc
}, {} as any),
showManageInputField: !!pipelineId,
onManageInputField: () => setShowInputFieldPanel?.(true),
}}
onChange={onChange}
editable={!readOnly}

View File

@@ -1,7 +1,16 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import { RiAlignLeft, RiBracesLine, RiCheckboxLine, RiCheckboxMultipleLine, RiFileCopy2Line, RiFileList2Line, RiHashtag, RiTextSnippet } from '@remixicon/react'
import {
RiAlignLeft,
RiBracesLine,
RiCheckboxLine,
RiCheckboxMultipleLine,
RiFileCopy2Line,
RiFileList2Line,
RiHashtag,
RiTextSnippet,
} from '@remixicon/react'
import { InputVarType } from '../../../types'
type Props = {
@@ -19,6 +28,7 @@ const getIcon = (type: InputVarType) => {
[InputVarType.jsonObject]: RiBracesLine,
[InputVarType.singleFile]: RiFileList2Line,
[InputVarType.multiFiles]: RiFileCopy2Line,
[InputVarType.checkbox]: RiCheckboxLine,
} as any)[type] || RiTextSnippet
}

View File

@@ -0,0 +1,29 @@
import type { ReactNode } from 'react'
import { memo } from 'react'
import type {
BoxGroupProps,
FieldProps,
} from '.'
import {
BoxGroup,
Field,
} from '.'
type BoxGroupFieldProps = {
children?: ReactNode
boxGroupProps?: Omit<BoxGroupProps, 'children'>
fieldProps?: Omit<FieldProps, 'children'>
}
export const BoxGroupField = memo(({
children,
fieldProps,
boxGroupProps,
}: BoxGroupFieldProps) => {
return (
<BoxGroup {...boxGroupProps}>
<Field {...fieldProps}>
{children}
</Field>
</BoxGroup>
)
})

View File

@@ -0,0 +1,29 @@
import type { ReactNode } from 'react'
import { memo } from 'react'
import {
Box,
Group,
} from '.'
import type {
BoxProps,
GroupProps,
} from '.'
export type BoxGroupProps = {
children?: ReactNode
boxProps?: Omit<BoxProps, 'children'>
groupProps?: Omit<GroupProps, 'children'>
}
export const BoxGroup = memo(({
children,
boxProps,
groupProps,
}: BoxGroupProps) => {
return (
<Box {...boxProps}>
<Group {...groupProps}>
{children}
</Group>
</Box>
)
})

View File

@@ -0,0 +1,25 @@
import type { ReactNode } from 'react'
import { memo } from 'react'
import cn from '@/utils/classnames'
export type BoxProps = {
className?: string
children?: ReactNode
withBorderBottom?: boolean
}
export const Box = memo(({
className,
children,
withBorderBottom,
}: BoxProps) => {
return (
<div
className={cn(
'py-2',
withBorderBottom && 'border-b border-divider-subtle',
className,
)}>
{children}
</div>
)
})

View File

@@ -0,0 +1,72 @@
import type { ReactNode } from 'react'
import {
memo,
useState,
} from 'react'
import { ArrowDownRoundFill } from '@/app/components/base/icons/src/vender/solid/general'
import Tooltip from '@/app/components/base/tooltip'
import cn from '@/utils/classnames'
export type FieldTitleProps = {
title?: string
operation?: ReactNode
subTitle?: string | ReactNode
tooltip?: string
showArrow?: boolean
disabled?: boolean
collapsed?: boolean
onCollapse?: (collapsed: boolean) => void
}
export const FieldTitle = memo(({
title,
operation,
subTitle,
tooltip,
showArrow,
disabled,
collapsed,
onCollapse,
}: FieldTitleProps) => {
const [collapsedLocal, setCollapsedLocal] = useState(true)
const collapsedMerged = collapsed !== undefined ? collapsed : collapsedLocal
return (
<div className={cn('mb-0.5', !!subTitle && 'mb-1')}>
<div
className='group/collapse flex items-center justify-between py-1'
onClick={() => {
if (!disabled) {
setCollapsedLocal(!collapsedMerged)
onCollapse?.(!collapsedMerged)
}
}}
>
<div className='system-sm-semibold-uppercase flex items-center text-text-secondary'>
{title}
{
showArrow && (
<ArrowDownRoundFill
className={cn(
'h-4 w-4 cursor-pointer text-text-quaternary group-hover/collapse:text-text-secondary',
collapsedMerged && 'rotate-[270deg]',
)}
/>
)
}
{
tooltip && (
<Tooltip
popupContent={tooltip}
triggerClassName='w-4 h-4 ml-1'
/>
)
}
</div>
{operation}
</div>
{
subTitle
}
</div>
)
})

View File

@@ -0,0 +1,36 @@
import type { ReactNode } from 'react'
import {
memo,
useState,
} from 'react'
import type { FieldTitleProps } from '.'
import { FieldTitle } from '.'
export type FieldProps = {
fieldTitleProps?: FieldTitleProps
children?: ReactNode
disabled?: boolean
supportCollapse?: boolean
}
export const Field = memo(({
fieldTitleProps,
children,
supportCollapse,
disabled,
}: FieldProps) => {
const [collapsed, setCollapsed] = useState(false)
return (
<div>
<FieldTitle
{...fieldTitleProps}
collapsed={collapsed}
onCollapse={setCollapsed}
showArrow={supportCollapse}
disabled={disabled}
/>
{supportCollapse && !collapsed && children}
{!supportCollapse && children}
</div>
)
})

View File

@@ -0,0 +1,29 @@
import type { ReactNode } from 'react'
import { memo } from 'react'
import type {
FieldProps,
GroupProps,
} from '.'
import {
Field,
Group,
} from '.'
type GroupFieldProps = {
children?: ReactNode
groupProps?: Omit<GroupProps, 'children'>
fieldProps?: Omit<FieldProps, 'children'>
}
export const GroupField = memo(({
children,
fieldProps,
groupProps,
}: GroupFieldProps) => {
return (
<Group {...groupProps}>
<Field {...fieldProps}>
{children}
</Field>
</Group>
)
})

View File

@@ -0,0 +1,25 @@
import type { ReactNode } from 'react'
import { memo } from 'react'
import cn from '@/utils/classnames'
export type GroupProps = {
className?: string
children?: ReactNode
withBorderBottom?: boolean
}
export const Group = memo(({
className,
children,
withBorderBottom,
}: GroupProps) => {
return (
<div
className={cn(
'px-4 py-2',
withBorderBottom && 'border-b border-divider-subtle',
className,
)}>
{children}
</div>
)
})

View File

@@ -0,0 +1,7 @@
export * from './box'
export * from './group'
export * from './box-group'
export * from './field-title'
export * from './field'
export * from './group-field'
export * from './box-group-field'

View File

@@ -38,7 +38,7 @@ const Add = ({
const [open, setOpen] = useState(false)
const { handleNodeAdd } = useNodesInteractions()
const { nodesReadOnly } = useNodesReadOnly()
const { availableNextBlocks } = useAvailableBlocks(nodeData.type, nodeData.isInIteration, nodeData.isInLoop)
const { availableNextBlocks } = useAvailableBlocks(nodeData.type, nodeData.isInIteration || nodeData.isInLoop)
const { checkParallelLimit } = useWorkflow()
const handleSelect = useCallback<OnSelectBlock>((type, toolDefaultValue) => {
@@ -80,7 +80,7 @@ const Add = ({
${nodesReadOnly && '!cursor-not-allowed'}
`}
>
<div className='bg-background-default-dimm mr-1.5 flex h-5 w-5 items-center justify-center rounded-[5px]'>
<div className='mr-1.5 flex h-5 w-5 items-center justify-center rounded-[5px] bg-background-default-dimmed'>
<RiAddLine className='h-3 w-3' />
</div>
<div className='flex items-center uppercase'>

View File

@@ -36,7 +36,7 @@ const ChangeItem = ({
const {
availablePrevBlocks,
availableNextBlocks,
} = useAvailableBlocks(data.type, data.isInIteration, data.isInLoop)
} = useAvailableBlocks(data.type, data.isInIteration || data.isInLoop)
const handleSelect = useCallback<OnSelectBlock>((type, toolDefaultValue) => {
handleNodeChange(nodeId, type, sourceHandle, toolDefaultValue)

View File

@@ -47,7 +47,7 @@ export const NodeTargetHandle = memo(({
const { handleNodeAdd } = useNodesInteractions()
const { getNodesReadOnly } = useNodesReadOnly()
const connected = data._connectedTargetHandleIds?.includes(handleId)
const { availablePrevBlocks } = useAvailableBlocks(data.type, data.isInIteration, data.isInLoop)
const { availablePrevBlocks } = useAvailableBlocks(data.type, data.isInIteration || data.isInLoop)
const isConnectable = !!availablePrevBlocks.length
const handleOpenChange = useCallback((v: boolean) => {
@@ -129,7 +129,7 @@ export const NodeSourceHandle = memo(({
const [open, setOpen] = useState(false)
const { handleNodeAdd } = useNodesInteractions()
const { getNodesReadOnly } = useNodesReadOnly()
const { availableNextBlocks } = useAvailableBlocks(data.type, data.isInIteration, data.isInLoop)
const { availableNextBlocks } = useAvailableBlocks(data.type, data.isInIteration || data.isInLoop)
const isConnectable = !!availableNextBlocks.length
const isChatMode = useIsChatMode()
const { checkParallelLimit } = useWorkflow()

View File

@@ -30,7 +30,7 @@ const ChangeBlock = ({
const {
availablePrevBlocks,
availableNextBlocks,
} = useAvailableBlocks(nodeData.type, nodeData.isInIteration, nodeData.isInLoop)
} = useAvailableBlocks(nodeData.type, nodeData.isInIteration || nodeData.isInLoop)
const availableNodes = useMemo(() => {
if (availablePrevBlocks.length && availableNextBlocks.length)

View File

@@ -31,7 +31,6 @@ const PanelOperator = ({
crossAxis: 53,
},
onOpenChange,
inNode,
showHelpLink = true,
}: PanelOperatorProps) => {
const [open, setOpen] = useState(false)

View File

@@ -1,28 +1,21 @@
import {
memo,
useMemo,
} from 'react'
import { useTranslation } from 'react-i18next'
import { useEdges } from 'reactflow'
import { useNodeHelpLink } from '../../hooks/use-node-help-link'
import ChangeBlock from './change-block'
import {
canRunBySingle,
} from '@/app/components/workflow/utils'
import { useStore } from '@/app/components/workflow/store'
import {
useNodeDataUpdate,
useNodesExtraData,
useNodeMetaData,
useNodesInteractions,
useNodesReadOnly,
useNodesSyncDraft,
} from '@/app/components/workflow/hooks'
import ShortcutsName from '@/app/components/workflow/shortcuts-name'
import type { Node } from '@/app/components/workflow/types'
import { BlockEnum } from '@/app/components/workflow/types'
import { useGetLanguage } from '@/context/i18n'
import { CollectionType } from '@/app/components/tools/types'
import { canFindTool } from '@/utils'
type PanelOperatorPopupProps = {
id: string
@@ -37,7 +30,6 @@ const PanelOperatorPopup = ({
showHelpLink,
}: PanelOperatorPopupProps) => {
const { t } = useTranslation()
const language = useGetLanguage()
const edges = useEdges()
const {
handleNodeDelete,
@@ -48,41 +40,9 @@ const PanelOperatorPopup = ({
const { handleNodeDataUpdate } = useNodeDataUpdate()
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
const { nodesReadOnly } = useNodesReadOnly()
const nodesExtraData = useNodesExtraData()
const buildInTools = useStore(s => s.buildInTools)
const customTools = useStore(s => s.customTools)
const workflowTools = useStore(s => s.workflowTools)
const edge = edges.find(edge => edge.target === id)
const author = useMemo(() => {
if (data.type !== BlockEnum.Tool)
return nodesExtraData[data.type].author
if (data.provider_type === CollectionType.builtIn)
return buildInTools.find(toolWithProvider => canFindTool(toolWithProvider.id, data.provider_id))?.author
if (data.provider_type === CollectionType.workflow)
return workflowTools.find(toolWithProvider => toolWithProvider.id === data.provider_id)?.author
return customTools.find(toolWithProvider => toolWithProvider.id === data.provider_id)?.author
}, [data, nodesExtraData, buildInTools, customTools, workflowTools])
const about = useMemo(() => {
if (data.type !== BlockEnum.Tool)
return nodesExtraData[data.type].about
if (data.provider_type === CollectionType.builtIn)
return buildInTools.find(toolWithProvider => canFindTool(toolWithProvider.id, data.provider_id))?.description[language]
if (data.provider_type === CollectionType.workflow)
return workflowTools.find(toolWithProvider => toolWithProvider.id === data.provider_id)?.description[language]
return customTools.find(toolWithProvider => toolWithProvider.id === data.provider_id)?.description[language]
}, [data, nodesExtraData, language, buildInTools, customTools, workflowTools])
const showChangeBlock = data.type !== BlockEnum.Start && !nodesReadOnly && data.type !== BlockEnum.Iteration && data.type !== BlockEnum.Loop
const link = useNodeHelpLink(data.type)
const nodeMetaData = useNodeMetaData({ id, data } as Node)
const showChangeBlock = !nodeMetaData.isTypeFixed && !nodesReadOnly
const isChildNode = !!(data.isInIteration || data.isInLoop)
return (
@@ -124,53 +84,65 @@ const PanelOperatorPopup = ({
)
}
{
data.type !== BlockEnum.Start && !nodesReadOnly && (
!nodesReadOnly && (
<>
<div className='p-1'>
<div
className='flex h-8 cursor-pointer items-center justify-between rounded-lg px-3 text-sm text-text-secondary hover:bg-state-base-hover'
onClick={() => {
onClosePopup()
handleNodesCopy(id)
}}
>
{t('workflow.common.copy')}
<ShortcutsName keys={['ctrl', 'c']} />
</div>
<div
className='flex h-8 cursor-pointer items-center justify-between rounded-lg px-3 text-sm text-text-secondary hover:bg-state-base-hover'
onClick={() => {
onClosePopup()
handleNodesDuplicate(id)
}}
>
{t('workflow.common.duplicate')}
<ShortcutsName keys={['ctrl', 'd']} />
</div>
</div>
<div className='h-px bg-divider-regular'></div>
<div className='p-1'>
<div
className={`
flex h-8 cursor-pointer items-center justify-between rounded-lg px-3 text-sm text-text-secondary
hover:bg-state-destructive-hover hover:text-red-500
`}
onClick={() => handleNodeDelete(id)}
>
{t('common.operation.delete')}
<ShortcutsName keys={['del']} />
</div>
</div>
<div className='h-px bg-divider-regular'></div>
{
!nodeMetaData.isSingleton && (
<>
<div className='p-1'>
<div
className='flex h-8 cursor-pointer items-center justify-between rounded-lg px-3 text-sm text-text-secondary hover:bg-state-base-hover'
onClick={() => {
onClosePopup()
handleNodesCopy(id)
}}
>
{t('workflow.common.copy')}
<ShortcutsName keys={['ctrl', 'c']} />
</div>
<div
className='flex h-8 cursor-pointer items-center justify-between rounded-lg px-3 text-sm text-text-secondary hover:bg-state-base-hover'
onClick={() => {
onClosePopup()
handleNodesDuplicate(id)
}}
>
{t('workflow.common.duplicate')}
<ShortcutsName keys={['ctrl', 'd']} />
</div>
</div>
<div className='h-px bg-divider-regular'></div>
</>
)
}
{
!nodeMetaData.isUndeletable && (
<>
<div className='p-1'>
<div
className={`
flex h-8 cursor-pointer items-center justify-between rounded-lg px-3 text-sm text-text-secondary
hover:bg-state-destructive-hover hover:text-text-destructive
`}
onClick={() => handleNodeDelete(id)}
>
{t('common.operation.delete')}
<ShortcutsName keys={['del']} />
</div>
</div>
<div className='h-px bg-divider-regular'></div>
</>
)
}
</>
)
}
{
showHelpLink && link && (
showHelpLink && nodeMetaData.helpLinkUri && (
<>
<div className='p-1'>
<a
href={link}
href={nodeMetaData.helpLinkUri}
target='_blank'
className='flex h-8 cursor-pointer items-center rounded-lg px-3 text-sm text-text-secondary hover:bg-state-base-hover'
>
@@ -186,9 +158,9 @@ const PanelOperatorPopup = ({
<div className='mb-1 flex h-[22px] items-center font-medium'>
{t('workflow.panel.about').toLocaleUpperCase()}
</div>
<div className='mb-1 leading-[18px] text-text-secondary'>{about}</div>
<div className='mb-1 leading-[18px] text-text-secondary'>{nodeMetaData.description}</div>
<div className='leading-[18px]'>
{t('workflow.panel.createdBy')} {author}
{t('workflow.panel.createdBy')} {nodeMetaData.author}
</div>
</div>
</div>

View File

@@ -150,6 +150,8 @@ const Editor: FC<Props> = ({
}
const getVarType = useWorkflowVariableType()
const pipelineId = useStore(s => s.pipelineId)
const setShowInputFieldPanel = useStore(s => s.setShowInputFieldPanel)
return (
<Wrap className={cn(className, wrapClassName)} style={wrapStyle} isInNode isExpand={isExpand}>
@@ -264,7 +266,7 @@ const Editor: FC<Props> = ({
workflowVariableBlock={{
show: true,
variables: nodesOutputVars || [],
getVarType,
getVarType: getVarType as any,
workflowNodesMap: availableNodes.reduce((acc, node) => {
acc[node.id] = {
title: node.data.title,
@@ -281,6 +283,8 @@ const Editor: FC<Props> = ({
}
return acc
}, {} as any),
showManageInputField: !!pipelineId,
onManageInputField: () => setShowInputFieldPanel?.(true),
}}
onChange={onChange}
onBlur={setBlur}

View File

@@ -8,7 +8,7 @@ import type {
VarType,
} from '@/app/components/workflow/types'
import { BlockEnum } from '@/app/components/workflow/types'
import { getNodeInfoById, isConversationVar, isENV, isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils'
import { getNodeInfoById, isConversationVar, isENV, isRagVariableVar, isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils'
import { isExceptionVariable } from '@/app/components/workflow/utils'
import {
VariableLabelInSelect,
@@ -27,18 +27,19 @@ const VariableTag = ({
availableNodes,
}: VariableTagProps) => {
const nodes = useNodes<CommonNodeType>()
const isRagVar = isRagVariableVar(valueSelector)
const node = useMemo(() => {
if (isSystemVar(valueSelector)) {
const startNode = availableNodes?.find(n => n.data.type === BlockEnum.Start)
if (startNode)
return startNode
}
return getNodeInfoById(availableNodes || nodes, valueSelector[0])
}, [nodes, valueSelector, availableNodes])
return getNodeInfoById(availableNodes || nodes, isRagVar ? valueSelector[1] : valueSelector[0])
}, [nodes, valueSelector, availableNodes, isRagVar])
const isEnv = isENV(valueSelector)
const isChatVar = isConversationVar(valueSelector)
const isValid = Boolean(node) || isEnv || isChatVar
const isValid = Boolean(node) || isEnv || isChatVar || isRagVar
const variableName = isSystemVar(valueSelector) ? valueSelector.slice(0).join('.') : valueSelector.slice(1).join('.')
const isException = isExceptionVariable(variableName, node?.data.type)

View File

@@ -0,0 +1,38 @@
import { useTranslation } from 'react-i18next'
import { RiAddLine } from '@remixicon/react'
type ManageInputFieldProps = {
onManage: () => void
}
const ManageInputField = ({
onManage,
}: ManageInputFieldProps) => {
const { t } = useTranslation()
return (
<div className='flex items-center border-t border-divider-subtle pt-1'>
<div
className='flex h-8 grow cursor-pointer items-center px-3'
onClick={onManage}
>
<RiAddLine className='mr-1 h-4 w-4 text-text-tertiary' />
<div
className='system-xs-medium truncate text-text-tertiary'
title='Create user input field'
>
{t('pipeline.inputField.create')}
</div>
</div>
<div className='mx-1 h-3 w-[1px] shrink-0 bg-divider-regular'></div>
<div
className='system-xs-medium flex h-8 shrink-0 cursor-pointer items-center justify-center px-3 text-text-tertiary'
onClick={onManage}
>
{t('pipeline.inputField.manage')}
</div>
</div>
)
}
export default ManageInputField

View File

@@ -0,0 +1,162 @@
import matchTheSchemaType from './match-schema-type'
describe('match the schema type', () => {
it('should return true for identical primitive types', () => {
expect(matchTheSchemaType({ type: 'string' }, { type: 'string' })).toBe(true)
expect(matchTheSchemaType({ type: 'number' }, { type: 'number' })).toBe(true)
})
it('should return false for different primitive types', () => {
expect(matchTheSchemaType({ type: 'string' }, { type: 'number' })).toBe(false)
})
it('should ignore values and only compare types', () => {
expect(matchTheSchemaType({ type: 'string', value: 'hello' }, { type: 'string', value: 'world' })).toBe(true)
expect(matchTheSchemaType({ type: 'number', value: 42 }, { type: 'number', value: 100 })).toBe(true)
})
it('should return true for structural differences but no types', () => {
expect(matchTheSchemaType({ type: 'string', other: { b: 'xxx' } }, { type: 'string', other: 'xxx' })).toBe(true)
expect(matchTheSchemaType({ type: 'string', other: { b: 'xxx' } }, { type: 'string' })).toBe(true)
})
it('should handle nested objects with same structure and types', () => {
const obj1 = {
type: 'object',
properties: {
name: { type: 'string' },
age: { type: 'number' },
address: {
type: 'object',
properties: {
street: { type: 'string' },
city: { type: 'string' },
},
},
},
}
const obj2 = {
type: 'object',
properties: {
name: { type: 'string', value: 'Alice' },
age: { type: 'number', value: 30 },
address: {
type: 'object',
properties: {
street: { type: 'string', value: '123 Main St' },
city: { type: 'string', value: 'Wonderland' },
},
},
},
}
expect(matchTheSchemaType(obj1, obj2)).toBe(true)
})
it('should return false for nested objects with different structures', () => {
const obj1 = {
type: 'object',
properties: {
name: { type: 'string' },
age: { type: 'number' },
},
}
const obj2 = {
type: 'object',
properties: {
name: { type: 'string' },
address: { type: 'string' },
},
}
expect(matchTheSchemaType(obj1, obj2)).toBe(false)
})
it('file struct should match file type', () => {
const fileSchema = {
$id: 'https://dify.ai/schemas/v1/file.json',
$schema: 'http://json-schema.org/draft-07/schema#',
version: '1.0.0',
type: 'object',
title: 'File Schema',
description: 'Schema for file objects (v1)',
properties: {
name: {
type: 'string',
description: 'file name',
},
size: {
type: 'number',
description: 'file size',
},
extension: {
type: 'string',
description: 'file extension',
},
type: {
type: 'string',
description: 'file type',
},
mime_type: {
type: 'string',
description: 'file mime type',
},
transfer_method: {
type: 'string',
description: 'file transfer method',
},
url: {
type: 'string',
description: 'file url',
},
related_id: {
type: 'string',
description: 'file related id',
},
},
required: [
'name',
],
}
const file = {
type: 'object',
title: 'File',
description: 'Schema for file objects (v1)',
properties: {
name: {
type: 'string',
description: 'file name',
},
size: {
type: 'number',
description: 'file size',
},
extension: {
type: 'string',
description: 'file extension',
},
type: {
type: 'string',
description: 'file type',
},
mime_type: {
type: 'string',
description: 'file mime type',
},
transfer_method: {
type: 'string',
description: 'file transfer method',
},
url: {
type: 'string',
description: 'file url',
},
related_id: {
type: 'string',
description: 'file related id',
},
},
required: [
'name',
],
}
expect(matchTheSchemaType(fileSchema, file)).toBe(true)
})
})

View File

@@ -0,0 +1,42 @@
export type AnyObj = Record<string, any> | null
const isObj = (x: any): x is object => x !== null && typeof x === 'object'
// only compare type in object
function matchTheSchemaType(scheme: AnyObj, target: AnyObj): boolean {
const isMatch = (schema: AnyObj, t: AnyObj): boolean => {
const oSchema = isObj(schema)
const oT = isObj(t)
if(!oSchema)
return true
if (!oT) { // ignore the object without type
// deep find oSchema has type
for (const key in schema) {
if (key === 'type')
return false
if (isObj((schema as any)[key]) && !isMatch((schema as any)[key], null))
return false
}
return true
}
// check current `type`
const tx = (schema as any).type
const ty = (t as any).type
const isTypeValueObj = isObj(tx)
if(!isTypeValueObj) // caution: type can be object, so that it would not be compare by value
if (tx !== ty) return false
// recurse into all keys
const keys = new Set([...Object.keys(schema as object), ...Object.keys(t as object)])
for (const k of keys) {
if (k === 'type' && !isTypeValueObj) continue // already checked
if (!isMatch((schema as any)[k], (t as any)[k])) return false
}
return true
}
return isMatch(scheme, target)
}
export default matchTheSchemaType

View File

@@ -9,7 +9,7 @@ import type { ValueSelector } from '@/app/components/workflow/types'
type Props = {
className?: string
root: { nodeId?: string, nodeName?: string, attrName: string }
root: { nodeId?: string, nodeName?: string, attrName: string, attrAlias?: string }
payload: StructuredOutput
readonly?: boolean
onSelect?: (valueSelector: ValueSelector) => void
@@ -52,8 +52,7 @@ export const PickerPanelMain: FC<Props> = ({
)}
<div className='system-sm-medium text-text-secondary'>{root.attrName}</div>
</div>
{/* It must be object */}
<div className='system-xs-regular ml-2 shrink-0 text-text-tertiary'>object</div>
<div className='system-xs-regular ml-2 truncate text-text-tertiary' title={root.attrAlias || 'object'}>{root.attrAlias || 'object'}</div>
</div>
{fieldNames.map(name => (
<Field

View File

@@ -44,7 +44,7 @@ const Field: FC<Props> = ({
/>
)}
<div className={cn('system-sm-medium ml-[7px] h-6 truncate leading-6 text-text-secondary', isRoot && rootClassName)}>{name}</div>
<div className='system-xs-regular ml-3 shrink-0 leading-6 text-text-tertiary'>{getFieldType(payload)}</div>
<div className='system-xs-regular ml-3 shrink-0 leading-6 text-text-tertiary'>{getFieldType(payload)}{(payload.schemaType && payload.schemaType !== 'file' && ` (${payload.schemaType})`)}</div>
{required && <div className='system-2xs-medium-uppercase ml-3 leading-6 text-text-warning'>{t('app.structOutput.required')}</div>}
</div>
{payload.description && (

View File

@@ -0,0 +1,21 @@
import type { SchemaTypeDefinition } from '@/service/use-common'
import { useSchemaTypeDefinitions } from '@/service/use-common'
import type { AnyObj } from './match-schema-type'
import matchTheSchemaType from './match-schema-type'
export const getMatchedSchemaType = (obj: AnyObj, schemaTypeDefinitions?: SchemaTypeDefinition[]): string => {
if(!schemaTypeDefinitions) return ''
const matched = schemaTypeDefinitions.find(def => matchTheSchemaType(obj, def.schema))
return matched ? matched.name : ''
}
const useMatchSchemaType = () => {
const { data: schemaTypeDefinitions, isLoading } = useSchemaTypeDefinitions()
return {
isLoading,
schemaTypeDefinitions,
}
}
export default useMatchSchemaType

View File

@@ -10,14 +10,18 @@ import {
RiMoreLine,
} from '@remixicon/react'
import produce from 'immer'
import { useReactFlow, useStoreApi } from 'reactflow'
import {
useNodes,
useReactFlow,
useStoreApi,
} from 'reactflow'
import RemoveButton from '../remove-button'
import useAvailableVarList from '../../hooks/use-available-var-list'
import VarReferencePopup from './var-reference-popup'
import { getNodeInfoById, isConversationVar, isENV, isSystemVar, varTypeToStructType } from './utils'
import { getNodeInfoById, isConversationVar, isENV, isRagVariableVar, isSystemVar, removeFileVars, varTypeToStructType } from './utils'
import ConstantField from './constant-field'
import cn from '@/utils/classnames'
import type { Node, NodeOutPutVar, ToolWithProvider, ValueSelector, Var } from '@/app/components/workflow/types'
import type { CommonNodeType, Node, NodeOutPutVar, ToolWithProvider, ValueSelector, Var } from '@/app/components/workflow/types'
import type { CredentialFormSchemaSelect } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { type CredentialFormSchema, type FormOption, FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { BlockEnum } from '@/app/components/workflow/types'
@@ -59,6 +63,7 @@ type Props = {
defaultVarKindType?: VarKindType
onlyLeafNodeVar?: boolean
filterVar?: (payload: Var, valueSelector: ValueSelector) => boolean
isFilterFileVar?: boolean
availableNodes?: Node[]
availableVars?: NodeOutPutVar[]
isAddBtnTrigger?: boolean
@@ -74,6 +79,7 @@ type Props = {
zIndex?: number
currentTool?: Tool
currentProvider?: ToolWithProvider
preferSchemaType?: boolean
}
const DEFAULT_VALUE_SELECTOR: Props['value'] = []
@@ -90,6 +96,7 @@ const VarReferencePicker: FC<Props> = ({
defaultVarKindType = VarKindType.constant,
onlyLeafNodeVar,
filterVar = () => true,
isFilterFileVar,
availableNodes: passedInAvailableNodes,
availableVars: passedInAvailableVars,
isAddBtnTrigger,
@@ -105,14 +112,12 @@ const VarReferencePicker: FC<Props> = ({
zIndex,
currentTool,
currentProvider,
preferSchemaType,
}) => {
const { t } = useTranslation()
const store = useStoreApi()
const {
getNodes,
} = store.getState()
const nodes = useNodes<CommonNodeType>()
const isChatMode = useIsChatMode()
const { getCurrentVariableType } = useWorkflowVariables()
const { availableVars, availableNodesWithParent: availableNodes } = useAvailableVarList(nodeId, {
onlyLeafNodeVar,
@@ -126,12 +131,12 @@ const VarReferencePicker: FC<Props> = ({
return node.data.type === BlockEnum.Start
})
const node = getNodes().find(n => n.id === nodeId)
const isInIteration = !!node?.data.isInIteration
const iterationNode = isInIteration ? getNodes().find(n => n.id === node.parentId) : null
const node = nodes.find(n => n.id === nodeId)
const isInIteration = !!(node?.data as any)?.isInIteration
const iterationNode = isInIteration ? nodes.find(n => n.id === node?.parentId) : null
const isInLoop = !!node?.data.isInLoop
const loopNode = isInLoop ? getNodes().find(n => n.id === node.parentId) : null
const isInLoop = !!(node?.data as any)?.isInLoop
const loopNode = isInLoop ? nodes.find(n => n.id === node?.parentId) : null
const triggerRef = useRef<HTMLDivElement>(null)
const [triggerWidth, setTriggerWidth] = useState(TRIGGER_DEFAULT_WIDTH)
@@ -143,7 +148,10 @@ const VarReferencePicker: FC<Props> = ({
const [varKindType, setVarKindType] = useState<VarKindType>(defaultVarKindType)
const isConstant = isSupportConstantValue && varKindType === VarKindType.constant
const outputVars = useMemo(() => (passedInAvailableVars || availableVars), [passedInAvailableVars, availableVars])
const outputVars = useMemo(() => {
const results = passedInAvailableVars || availableVars
return isFilterFileVar ? removeFileVars(results) : results
}, [passedInAvailableVars, availableVars, isFilterFileVar])
const [open, setOpen] = useState(false)
useEffect(() => {
@@ -190,7 +198,7 @@ const VarReferencePicker: FC<Props> = ({
}
}, [value, hasValue, isConstant, isIterationVar, iterationNode, availableNodes, outputVarNodeId, startNode, isLoopVar, loopNode])
const isShowAPart = (value as ValueSelector).length > 2
const isShowAPart = (value as ValueSelector).length > 2 && !isRagVariableVar((value as ValueSelector))
const varName = useMemo(() => {
if (!hasValue)
@@ -275,21 +283,24 @@ const VarReferencePicker: FC<Props> = ({
}, [availableNodes, reactflow, store])
const type = getCurrentVariableType({
parentNode: isInIteration ? iterationNode : loopNode,
parentNode: (isInIteration ? iterationNode : loopNode) as any,
valueSelector: value as ValueSelector,
availableNodes,
isChatMode,
isConstant: !!isConstant,
preferSchemaType,
})
const { isEnv, isChatVar, isValidVar, isException } = useMemo(() => {
const { isEnv, isChatVar, isRagVar, isValidVar, isException } = useMemo(() => {
const isEnv = isENV(value as ValueSelector)
const isChatVar = isConversationVar(value as ValueSelector)
const isValidVar = Boolean(outputVarNode) || isEnv || isChatVar
const isRagVar = isRagVariableVar(value as ValueSelector)
const isValidVar = Boolean(outputVarNode) || isEnv || isChatVar || isRagVar
const isException = isExceptionVariable(varName, outputVarNode?.type)
return {
isEnv,
isChatVar,
isRagVar,
isValidVar,
isException,
}
@@ -382,8 +393,9 @@ const VarReferencePicker: FC<Props> = ({
if (isEnv) return 'environment'
if (isChatVar) return 'conversation'
if (isLoopVar) return 'loop'
if (isRagVar) return 'rag'
return 'system'
}, [isEnv, isChatVar, isLoopVar])
}, [isEnv, isChatVar, isLoopVar, isRagVar])
return (
<div className={cn(className, !readonly && 'cursor-pointer')}>
@@ -455,7 +467,7 @@ const VarReferencePicker: FC<Props> = ({
{hasValue
? (
<>
{isShowNodeName && !isEnv && !isChatVar && (
{isShowNodeName && !isEnv && !isChatVar && !isRagVar && (
<div className='flex items-center' onClick={(e) => {
if (e.metaKey || e.ctrlKey) {
e.stopPropagation()
@@ -553,6 +565,7 @@ const VarReferencePicker: FC<Props> = ({
itemWidth={isAddBtnTrigger ? 260 : (minWidth || triggerWidth)}
isSupportFileVar={isSupportFileVar}
zIndex={zIndex}
preferSchemaType={preferSchemaType}
/>
)}
</PortalToFollowElemContent>

View File

@@ -1,10 +1,11 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import React, { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import VarReferenceVars from './var-reference-vars'
import type { NodeOutPutVar, ValueSelector, Var } from '@/app/components/workflow/types'
import ListEmpty from '@/app/components/base/list-empty'
import { useStore } from '@/app/components/workflow/store'
import { useDocLink } from '@/context/i18n'
type Props = {
@@ -14,6 +15,7 @@ type Props = {
itemWidth?: number
isSupportFileVar?: boolean
zIndex?: number
preferSchemaType?: boolean
}
const VarReferencePopup: FC<Props> = ({
vars,
@@ -22,8 +24,12 @@ const VarReferencePopup: FC<Props> = ({
itemWidth,
isSupportFileVar = true,
zIndex,
preferSchemaType,
}) => {
const { t } = useTranslation()
const pipelineId = useStore(s => s.pipelineId)
const showManageRagInputFields = useMemo(() => !!pipelineId, [pipelineId])
const setShowInputFieldPanel = useStore(s => s.setShowInputFieldPanel)
const docLink = useDocLink()
// max-h-[300px] overflow-y-auto todo: use portal to handle long list
return (
@@ -63,6 +69,9 @@ const VarReferencePopup: FC<Props> = ({
itemWidth={itemWidth}
isSupportFileVar={isSupportFileVar}
zIndex={zIndex}
showManageInputField={showManageRagInputFields}
onManageInputField={() => setShowInputFieldPanel?.(true)}
preferSchemaType={preferSchemaType}
/>
}
</div >

View File

@@ -16,11 +16,12 @@ import { checkKeys } from '@/utils/var'
import type { StructuredOutput } from '../../../llm/types'
import { Type } from '../../../llm/types'
import PickerStructurePanel from '@/app/components/workflow/nodes/_base/components/variable/object-child-tree-panel/picker'
import { varTypeToStructType } from './utils'
import { isSpecialVar, varTypeToStructType } from './utils'
import type { Field } from '@/app/components/workflow/nodes/llm/types'
import { FILE_STRUCT } from '@/app/components/workflow/constants'
import { noop } from 'lodash-es'
import { CodeAssistant, MagicEdit } from '@/app/components/base/icons/src/vender/line/general'
import ManageInputField from './manage-input-field'
import { VariableIconWithColor } from '@/app/components/workflow/nodes/_base/components/variable/variable-label'
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
@@ -33,6 +34,7 @@ type ObjectChildrenProps = {
onHovering?: (value: boolean) => void
itemWidth?: number
isSupportFileVar?: boolean
preferSchemaType?: boolean
}
type ItemProps = {
@@ -50,6 +52,7 @@ type ItemProps = {
isInCodeGeneratorInstructionEditor?: boolean
zIndex?: number
className?: string
preferSchemaType?: boolean
}
const objVarTypes = [VarType.object, VarType.file]
@@ -68,6 +71,7 @@ const Item: FC<ItemProps> = ({
isInCodeGeneratorInstructionEditor,
zIndex,
className,
preferSchemaType,
}) => {
const isStructureOutput = itemData.type === VarType.object && (itemData.children as StructuredOutput)?.schema?.properties
const isFile = itemData.type === VarType.file && !isStructureOutput
@@ -75,6 +79,7 @@ const Item: FC<ItemProps> = ({
const isSys = itemData.variable.startsWith('sys.')
const isEnv = itemData.variable.startsWith('env.')
const isChatVar = itemData.variable.startsWith('conversation.')
const isRagVariable = itemData.isRagVariable
const flatVarIcon = useMemo(() => {
if (!isFlat)
return null
@@ -156,7 +161,7 @@ const Item: FC<ItemProps> = ({
if (isFlat) {
onChange([itemData.variable], itemData)
}
else if (isSys || isEnv || isChatVar) { // system variable | environment variable | conversation variable
else if (isSys || isEnv || isChatVar || isRagVariable) { // system variable | environment variable | conversation variable
onChange([...objPath, ...itemData.variable.split('.')], itemData)
}
else {
@@ -167,8 +172,9 @@ const Item: FC<ItemProps> = ({
if (isEnv) return 'environment'
if (isChatVar) return 'conversation'
if (isLoopVar) return 'loop'
if (isRagVariable) return 'rag'
return 'system'
}, [isEnv, isChatVar, isSys, isLoopVar])
}, [isEnv, isChatVar, isSys, isLoopVar, isRagVariable])
return (
<PortalToFollowElem
open={open}
@@ -195,7 +201,7 @@ const Item: FC<ItemProps> = ({
/>}
{isFlat && flatVarIcon}
{!isEnv && !isChatVar && (
{!isEnv && !isChatVar && !isRagVariable && (
<div title={itemData.variable} className='system-sm-medium ml-1 w-0 grow truncate text-text-secondary'>{varName}</div>
)}
{isEnv && (
@@ -204,8 +210,11 @@ const Item: FC<ItemProps> = ({
{isChatVar && (
<div title={itemData.des} className='system-sm-medium ml-1 w-0 grow truncate text-text-secondary'>{itemData.variable.replace('conversation.', '')}</div>
)}
{isRagVariable && (
<div title={itemData.des} className='system-sm-medium ml-1 w-0 grow truncate text-text-secondary'>{itemData.variable.split('.').slice(-1)[0]}</div>
)}
</div>
<div className='ml-1 shrink-0 text-xs font-normal capitalize text-text-tertiary'>{itemData.type}</div>
<div className='ml-1 shrink-0 text-xs font-normal capitalize text-text-tertiary'>{(preferSchemaType && itemData.schemaType) ? itemData.schemaType : itemData.type}</div>
{
(isObj || isStructureOutput) && (
<ChevronRight className={cn('ml-0.5 h-3 w-3 text-text-quaternary', isHovering && 'text-text-tertiary')} />
@@ -218,7 +227,7 @@ const Item: FC<ItemProps> = ({
}}>
{(isStructureOutput || isObj) && (
<PickerStructurePanel
root={{ nodeId, nodeName: title, attrName: itemData.variable }}
root={{ nodeId, nodeName: title, attrName: itemData.variable, attrAlias: itemData.schemaType }}
payload={structuredOutput!}
onHovering={setIsChildrenHovering}
onSelect={(valueSelector) => {
@@ -240,6 +249,7 @@ const ObjectChildren: FC<ObjectChildrenProps> = ({
onHovering,
itemWidth,
isSupportFileVar,
preferSchemaType,
}) => {
const currObjPath = objPath
const itemRef = useRef<HTMLDivElement>(null)
@@ -284,6 +294,7 @@ const ObjectChildren: FC<ObjectChildrenProps> = ({
onHovering={setIsChildrenHovering}
isSupportFileVar={isSupportFileVar}
isException={v.isException}
preferSchemaType={preferSchemaType}
/>
))
}
@@ -303,7 +314,10 @@ type Props = {
onBlur?: () => void
zIndex?: number
isInCodeGeneratorInstructionEditor?: boolean
showManageInputField?: boolean
onManageInputField?: () => void
autoFocus?: boolean
preferSchemaType?: boolean
}
const VarReferenceVars: FC<Props> = ({
hideSearch,
@@ -317,7 +331,10 @@ const VarReferenceVars: FC<Props> = ({
onBlur,
zIndex,
isInCodeGeneratorInstructionEditor,
showManageInputField,
onManageInputField,
autoFocus = true,
preferSchemaType,
}) => {
const { t } = useTranslation()
const [searchText, setSearchText] = useState('')
@@ -330,7 +347,7 @@ const VarReferenceVars: FC<Props> = ({
}
const filteredVars = vars.filter((v) => {
const children = v.vars.filter(v => checkKeys([v.variable], false).isValid || v.variable.startsWith('sys.') || v.variable.startsWith('env.') || v.variable.startsWith('conversation.'))
const children = v.vars.filter(v => checkKeys([v.variable], false).isValid || isSpecialVar(v.variable.split('.')[0]))
return children.length > 0
}).filter((node) => {
if (!searchText)
@@ -341,7 +358,7 @@ const VarReferenceVars: FC<Props> = ({
})
return children.length > 0
}).map((node) => {
let vars = node.vars.filter(v => checkKeys([v.variable], false).isValid || v.variable.startsWith('sys.') || v.variable.startsWith('env.') || v.variable.startsWith('conversation.'))
let vars = node.vars.filter(v => checkKeys([v.variable], false).isValid || isSpecialVar(v.variable.split('.')[0]))
if (searchText) {
const searchTextLower = searchText.toLowerCase()
if (!node.title.toLowerCase().includes(searchTextLower))
@@ -407,6 +424,7 @@ const VarReferenceVars: FC<Props> = ({
isFlat={item.isFlat}
isInCodeGeneratorInstructionEditor={isInCodeGeneratorInstructionEditor}
zIndex={zIndex}
preferSchemaType={preferSchemaType}
/>
))}
{item.isFlat && !filteredVars[i + 1]?.isFlat && !!filteredVars.find(item => !item.isFlat) && (
@@ -420,6 +438,13 @@ const VarReferenceVars: FC<Props> = ({
}
</div>
: <div className='mt-2 pl-3 text-xs font-medium uppercase leading-[18px] text-gray-500'>{t('workflow.common.noVar')}</div>}
{
showManageInputField && (
<ManageInputField
onManage={onManageInputField || noop}
/>
)
}
</>
)
}

View File

@@ -2,9 +2,11 @@ import { useMemo } from 'react'
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
import { BubbleX, Env } from '@/app/components/base/icons/src/vender/line/others'
import { Loop } from '@/app/components/base/icons/src/vender/workflow'
import { InputField } from '@/app/components/base/icons/src/vender/pipeline'
import {
isConversationVar,
isENV,
isRagVariableVar,
isSystemVar,
} from '../utils'
import { VarInInspectType } from '@/types/workflow'
@@ -13,6 +15,9 @@ export const useVarIcon = (variables: string[], variableCategory?: VarInInspectT
if (variableCategory === 'loop')
return Loop
if (variableCategory === 'rag' || isRagVariableVar(variables))
return InputField
if (isENV(variables) || variableCategory === VarInInspectType.environment || variableCategory === 'environment')
return Env
@@ -41,7 +46,11 @@ export const useVarColor = (variables: string[], isExceptionVariable?: boolean,
}
export const useVarName = (variables: string[], notShowFullPath?: boolean) => {
const variableFullPathName = variables.slice(1).join('.')
let variableFullPathName = variables.slice(1).join('.')
if (isRagVariableVar(variables))
variableFullPathName = variables.slice(2).join('.')
const variablesLength = variables.length
const varName = useMemo(() => {
const isSystem = isSystemVar(variables)

View File

@@ -2,7 +2,7 @@ import type {
FC,
ReactNode,
} from 'react'
import {
import React, {
cloneElement,
memo,
useCallback,
@@ -36,6 +36,7 @@ import {
useAvailableBlocks,
useNodeDataUpdate,
useNodesInteractions,
useNodesMetaData,
useNodesReadOnly,
useToolIcon,
useWorkflowHistory,
@@ -44,6 +45,7 @@ import {
canRunBySingle,
hasErrorHandleNode,
hasRetryNode,
isSupportCustomRunForm,
} from '@/app/components/workflow/utils'
import Tooltip from '@/app/components/base/tooltip'
import { BlockEnum, type Node, NodeRunningStatus } from '@/app/components/workflow/types'
@@ -54,18 +56,35 @@ import LastRun from './last-run'
import useLastRun from './last-run/use-last-run'
import BeforeRunForm from '../before-run-form'
import { debounce } from 'lodash-es'
import { NODES_EXTRA_DATA } from '@/app/components/workflow/constants'
import { useLogs } from '@/app/components/workflow/run/hooks'
import PanelWrap from '../before-run-form/panel-wrap'
import SpecialResultPanel from '@/app/components/workflow/run/special-result-panel'
import { Stop } from '@/app/components/base/icons/src/vender/line/mediaAndDevices'
import { useHooksStore } from '@/app/components/workflow/hooks-store'
import { FlowType } from '@/types/common'
import {
AuthorizedInDataSourceNode,
AuthorizedInNode,
PluginAuth,
PluginAuthInDataSourceNode,
} from '@/app/components/plugins/plugin-auth'
import { AuthCategory } from '@/app/components/plugins/plugin-auth'
import { canFindTool } from '@/utils'
import type { CustomRunFormProps } from '@/app/components/workflow/nodes/data-source/types'
import { DataSourceClassification } from '@/app/components/workflow/nodes/data-source/types'
import { useModalContext } from '@/context/modal-context'
import DataSourceBeforeRunForm from '@/app/components/workflow/nodes/data-source/before-run-form'
import useInspectVarsCrud from '@/app/components/workflow/hooks/use-inspect-vars-crud'
const getCustomRunForm = (params: CustomRunFormProps): React.JSX.Element => {
const nodeType = params.payload.type
switch (nodeType) {
case BlockEnum.DataSource:
return <DataSourceBeforeRunForm {...params} />
default:
return <div>Custom Run Form: {nodeType} not found</div>
}
}
type BasePanelProps = {
children: ReactNode
id: Node['id']
@@ -142,7 +161,7 @@ const BasePanel: FC<BasePanelProps> = ({
const { handleNodeSelect } = useNodesInteractions()
const { nodesReadOnly } = useNodesReadOnly()
const { availableNextBlocks } = useAvailableBlocks(data.type, data.isInIteration, data.isInLoop)
const { availableNextBlocks } = useAvailableBlocks(data.type, data.isInIteration || data.isInLoop)
const toolIcon = useToolIcon(data)
const { saveStateToHistory } = useWorkflowHistory()
@@ -169,11 +188,11 @@ const BasePanel: FC<BasePanelProps> = ({
const [isPaused, setIsPaused] = useState(false)
useEffect(() => {
if(data._singleRunningStatus === NodeRunningStatus.Running) {
if (data._singleRunningStatus === NodeRunningStatus.Running) {
hasClickRunning.current = true
setIsPaused(false)
}
else if(data._isSingleRun && data._singleRunningStatus === undefined && hasClickRunning) {
else if (data._isSingleRun && data._singleRunningStatus === undefined && hasClickRunning) {
setIsPaused(true)
hasClickRunning.current = false
}
@@ -190,10 +209,13 @@ const BasePanel: FC<BasePanelProps> = ({
}, [handleNodeDataUpdate, id, data])
useEffect(() => {
// console.log(`id changed: ${id}, hasClickRunning: ${hasClickRunning.current}`)
hasClickRunning.current = false
}, [id])
const {
nodesMap,
} = useNodesMetaData()
const configsMap = useHooksStore(s => s.configsMap)
const {
isShowSingleRun,
hideSingleRun,
@@ -201,11 +223,14 @@ const BasePanel: FC<BasePanelProps> = ({
runInputData,
runInputDataRef,
runResult,
setRunResult,
getInputVars,
toVarInputs,
tabType,
isRunAfterSingleRun,
setIsRunAfterSingleRun,
setTabType,
handleAfterCustomSingleRun,
singleRunParams,
nodeInfo,
setRunInputData,
@@ -215,8 +240,10 @@ const BasePanel: FC<BasePanelProps> = ({
getFilteredExistVarForms,
} = useLastRun<typeof data>({
id,
flowId: configsMap?.flowId || '',
flowType: configsMap?.flowType || FlowType.appFlow,
data,
defaultRunInputData: NODES_EXTRA_DATA[data.type]?.defaultRunInputData || {},
defaultRunInputData: nodesMap?.[data.type]?.defaultRunInputData || {},
isPaused,
})
@@ -239,6 +266,11 @@ const BasePanel: FC<BasePanelProps> = ({
const showPluginAuth = useMemo(() => {
return data.type === BlockEnum.Tool && currCollection?.allow_delete
}, [currCollection, data.type])
const dataSourceList = useStore(s => s.dataSourceList)
const currentDataSource = useMemo(() => {
if (data.type === BlockEnum.DataSource && data.provider_type !== DataSourceClassification.localFile)
return dataSourceList?.find(item => item.plugin_id === data.plugin_id)
}, [dataSourceList, data.plugin_id, data.type, data.provider_type])
const handleAuthorizationItemClick = useCallback((credential_id: string) => {
handleNodeDataUpdateWithSyncDraft({
id,
@@ -247,8 +279,16 @@ const BasePanel: FC<BasePanelProps> = ({
},
})
}, [handleNodeDataUpdateWithSyncDraft, id])
const { setShowAccountSettingModal } = useModalContext()
const handleJumpToDataSourcePage = useCallback(() => {
setShowAccountSettingModal({ payload: 'data-source' })
}, [setShowAccountSettingModal])
if(logParams.showSpecialResultPanel) {
const {
appendNodeInspectVars,
} = useInspectVarsCrud()
if (logParams.showSpecialResultPanel) {
return (
<div className={cn(
'relative mr-1 h-full',
@@ -274,6 +314,20 @@ const BasePanel: FC<BasePanelProps> = ({
}
if (isShowSingleRun) {
const form = getCustomRunForm({
nodeId: id,
flowId: configsMap?.flowId || '',
flowType: configsMap?.flowType || FlowType.appFlow,
payload: data,
setRunResult,
setIsRunAfterSingleRun,
isPaused,
isRunAfterSingleRun,
onSuccess: handleAfterCustomSingleRun,
onCancel: hideSingleRun,
appendNodeInspectVars,
})
return (
<div className={cn(
'relative mr-1 h-full',
@@ -285,26 +339,36 @@ const BasePanel: FC<BasePanelProps> = ({
width: `${nodePanelWidth}px`,
}}
>
<BeforeRunForm
nodeName={data.title}
nodeType={data.type}
onHide={hideSingleRun}
onRun={handleRunWithParams}
{...singleRunParams!}
{...passedLogParams}
existVarValuesInForms={getExistVarValuesInForms(singleRunParams?.forms as any)}
filteredExistVarForms={getFilteredExistVarForms(singleRunParams?.forms as any)}
/>
{isSupportCustomRunForm(data.type) ? (
form
) : (
<BeforeRunForm
nodeName={data.title}
nodeType={data.type}
onHide={hideSingleRun}
onRun={handleRunWithParams}
{...singleRunParams!}
{...passedLogParams}
existVarValuesInForms={getExistVarValuesInForms(singleRunParams?.forms as any)}
filteredExistVarForms={getFilteredExistVarForms(singleRunParams?.forms as any)}
/>
)}
</div>
</div>
)
}
return (
<div className={cn(
'relative mr-1 h-full',
showMessageLogModal && '!absolute -top-[5px] right-[416px] z-0 !mr-0 w-[384px] overflow-hidden rounded-2xl border-[0.5px] border-components-panel-border shadow-lg transition-all',
)}>
<div
className={cn(
'relative mr-1 h-full',
showMessageLogModal && 'absolute z-0 mr-2 w-[400px] overflow-hidden rounded-2xl border-[0.5px] border-components-panel-border shadow-lg transition-all',
)}
style={{
right: !showMessageLogModal ? '0' : `${otherPanelWidth}px`,
}}
>
<div
ref={triggerRef}
className='absolute -left-1 top-0 flex h-full w-1 cursor-col-resize resize-x items-center justify-center'>
@@ -312,7 +376,7 @@ const BasePanel: FC<BasePanelProps> = ({
</div>
<div
ref={containerRef}
className={cn('flex h-full flex-col rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg', showSingleRunPanel ? 'overflow-hidden' : 'overflow-y-auto')}
className={cn('flex h-full flex-col rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg transition-[width] ease-linear', showSingleRunPanel ? 'overflow-hidden' : 'overflow-y-auto')}
style={{
width: `${nodePanelWidth}px`,
}}
@@ -340,7 +404,7 @@ const BasePanel: FC<BasePanelProps> = ({
<div
className='mr-1 flex h-6 w-6 cursor-pointer items-center justify-center rounded-md hover:bg-state-base-hover'
onClick={() => {
if(isSingleRunning) {
if (isSingleRunning) {
handleNodeDataUpdate({
id,
data: {
@@ -407,7 +471,26 @@ const BasePanel: FC<BasePanelProps> = ({
)
}
{
!showPluginAuth && (
!!currentDataSource && (
<PluginAuthInDataSourceNode
onJumpToDataSourcePage={handleJumpToDataSourcePage}
isAuthorized={currentDataSource.is_authorized}
>
<div className='flex items-center justify-between pl-4 pr-3'>
<Tab
value={tabType}
onChange={setTabType}
/>
<AuthorizedInDataSourceNode
onJumpToDataSourcePage={handleJumpToDataSourcePage}
authorizationsNum={3}
/>
</div>
</PluginAuthInDataSourceNode>
)
}
{
!showPluginAuth && !currentDataSource && (
<div className='flex items-center justify-between pl-4 pr-3'>
<Tab
value={tabType}

View File

@@ -8,6 +8,8 @@ import NoData from './no-data'
import { useLastRun } from '@/service/use-workflow'
import { RiLoader2Line } from '@remixicon/react'
import type { NodeTracing } from '@/types/workflow'
import { useHooksStore } from '@/app/components/workflow/hooks-store'
import { FlowType } from '@/types/common'
type Props = {
appId: string
@@ -35,6 +37,7 @@ const LastRun: FC<Props> = ({
isPaused,
...otherResultPanelProps
}) => {
const configsMap = useHooksStore(s => s.configsMap)
const isOneStepRunSucceed = oneStepRunRunningStatus === NodeRunningStatus.Succeeded
const isOneStepRunFailed = oneStepRunRunningStatus === NodeRunningStatus.Failed
// hide page and return to page would lost the oneStepRunRunningStatus
@@ -44,7 +47,7 @@ const LastRun: FC<Props> = ({
const hidePageOneStepRunFinished = [NodeRunningStatus.Succeeded, NodeRunningStatus.Failed].includes(hidePageOneStepFinishedStatus!)
const canRunLastRun = !isRunAfterSingleRun || isOneStepRunSucceed || isOneStepRunFailed || (pageHasHide && hidePageOneStepRunFinished)
const { data: lastRunResult, isFetching, error } = useLastRun(appId, nodeId, canRunLastRun)
const { data: lastRunResult, isFetching, error } = useLastRun(configsMap?.flowType || FlowType.appFlow, configsMap?.flowId || '', nodeId, canRunLastRun)
const isRunning = useMemo(() => {
if(isPaused)
return false

View File

@@ -19,6 +19,7 @@ import useLoopSingleRunFormParams from '@/app/components/workflow/nodes/loop/use
import useIfElseSingleRunFormParams from '@/app/components/workflow/nodes/if-else/use-single-run-form-params'
import useVariableAggregatorSingleRunFormParams from '@/app/components/workflow/nodes/variable-assigner/use-single-run-form-params'
import useVariableAssignerSingleRunFormParams from '@/app/components/workflow/nodes/assigner/use-single-run-form-params'
import useKnowledgeBaseSingleRunFormParams from '@/app/components/workflow/nodes/knowledge-base/use-single-run-form-params'
import useToolGetDataForCheckMore from '@/app/components/workflow/nodes/tool/use-get-data-for-check-more'
import { VALUE_SELECTOR_DELIMITER as DELIMITER } from '@/config'
@@ -32,6 +33,7 @@ import {
import useInspectVarsCrud from '@/app/components/workflow/hooks/use-inspect-vars-crud'
import { useInvalidLastRun } from '@/service/use-workflow'
import { useStore, useWorkflowStore } from '@/app/components/workflow/store'
import { isSupportCustomRunForm } from '@/app/components/workflow/utils'
const singleRunFormParamsHooks: Record<BlockEnum, any> = {
[BlockEnum.LLM]: useLLMSingleRunFormParams,
@@ -50,6 +52,7 @@ const singleRunFormParamsHooks: Record<BlockEnum, any> = {
[BlockEnum.IfElse]: useIfElseSingleRunFormParams,
[BlockEnum.VariableAggregator]: useVariableAggregatorSingleRunFormParams,
[BlockEnum.Assigner]: useVariableAssignerSingleRunFormParams,
[BlockEnum.KnowledgeBase]: useKnowledgeBaseSingleRunFormParams,
[BlockEnum.VariableAssigner]: undefined,
[BlockEnum.End]: undefined,
[BlockEnum.Answer]: undefined,
@@ -57,6 +60,8 @@ const singleRunFormParamsHooks: Record<BlockEnum, any> = {
[BlockEnum.IterationStart]: undefined,
[BlockEnum.LoopStart]: undefined,
[BlockEnum.LoopEnd]: undefined,
[BlockEnum.DataSource]: undefined,
[BlockEnum.DataSourceEmpty]: undefined,
}
const useSingleRunFormParamsHooks = (nodeType: BlockEnum) => {
@@ -89,6 +94,9 @@ const getDataForCheckMoreHooks: Record<BlockEnum, any> = {
[BlockEnum.Assigner]: undefined,
[BlockEnum.LoopStart]: undefined,
[BlockEnum.LoopEnd]: undefined,
[BlockEnum.DataSource]: undefined,
[BlockEnum.DataSourceEmpty]: undefined,
[BlockEnum.KnowledgeBase]: undefined,
}
const useGetDataForCheckMoreHooks = <T>(nodeType: BlockEnum) => {
@@ -111,6 +119,7 @@ const useLastRun = <T>({
const isIterationNode = blockType === BlockEnum.Iteration
const isLoopNode = blockType === BlockEnum.Loop
const isAggregatorNode = blockType === BlockEnum.VariableAggregator
const isCustomRunNode = isSupportCustomRunForm(blockType)
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
const {
getData: getDataForCheckMore,
@@ -119,6 +128,8 @@ const useLastRun = <T>({
const {
id,
flowId,
flowType,
data,
} = oneStepRunParams
const oneStepRunRes = useOneStepRun({
@@ -129,7 +140,6 @@ const useLastRun = <T>({
})
const {
appId,
hideSingleRun,
handleRun: doCallRunApi,
getInputVars,
@@ -164,7 +174,7 @@ const useLastRun = <T>({
})
const toSubmitData = useCallback((data: Record<string, any>) => {
if(!isIterationNode && !isLoopNode)
if (!isIterationNode && !isLoopNode)
return data
const allVarObject = singleRunParams?.allVarObject || {}
@@ -173,7 +183,7 @@ const useLastRun = <T>({
const [varSectorStr, nodeId] = key.split(DELIMITER)
formattedData[`${nodeId}.${allVarObject[key].inSingleRunPassedKey}`] = data[varSectorStr]
})
if(isIterationNode) {
if (isIterationNode) {
const iteratorInputKey = `${id}.input_selector`
formattedData[iteratorInputKey] = data[iteratorInputKey]
}
@@ -193,16 +203,16 @@ const useLastRun = <T>({
const initShowLastRunTab = useStore(s => s.initShowLastRunTab)
const [tabType, setTabType] = useState<TabType>(initShowLastRunTab ? TabType.lastRun : TabType.settings)
useEffect(() => {
if(initShowLastRunTab)
if (initShowLastRunTab)
setTabType(TabType.lastRun)
setInitShowLastRunTab(false)
}, [initShowLastRunTab])
const invalidLastRun = useInvalidLastRun(appId!, id)
const invalidLastRun = useInvalidLastRun(flowType, flowId, id)
const handleRunWithParams = async (data: Record<string, any>) => {
const { isValid } = checkValid()
if(!isValid)
if (!isValid)
return
setNodeRunning()
setIsRunAfterSingleRun(true)
@@ -226,14 +236,14 @@ const useLastRun = <T>({
const values: Record<string, boolean> = {}
form.inputs.forEach(({ variable, getVarValueFromDependent }) => {
const isGetValueFromDependent = getVarValueFromDependent || !variable.includes('.')
if(isGetValueFromDependent && !singleRunParams?.getDependentVar)
if (isGetValueFromDependent && !singleRunParams?.getDependentVar)
return
const selector = isGetValueFromDependent ? (singleRunParams?.getDependentVar(variable) || []) : variable.slice(1, -1).split('.')
if(!selector || selector.length === 0)
if (!selector || selector.length === 0)
return
const [nodeId, varName] = selector.slice(0, 2)
if(!isStartNode && nodeId === id) { // inner vars like loop vars
if (!isStartNode && nodeId === id) { // inner vars like loop vars
values[variable] = true
return
}
@@ -247,7 +257,7 @@ const useLastRun = <T>({
}
const isAllVarsHasValue = (vars?: ValueSelector[]) => {
if(!vars || vars.length === 0)
if (!vars || vars.length === 0)
return true
return vars.every((varItem) => {
const [nodeId, varName] = varItem.slice(0, 2)
@@ -257,7 +267,7 @@ const useLastRun = <T>({
}
const isSomeVarsHasValue = (vars?: ValueSelector[]) => {
if(!vars || vars.length === 0)
if (!vars || vars.length === 0)
return true
return vars.some((varItem) => {
const [nodeId, varName] = varItem.slice(0, 2)
@@ -284,7 +294,7 @@ const useLastRun = <T>({
}
const checkAggregatorVarsSet = (vars: ValueSelector[][]) => {
if(!vars || vars.length === 0)
if (!vars || vars.length === 0)
return true
// in each group, at last one set is ok
return vars.every((varItem) => {
@@ -292,10 +302,20 @@ const useLastRun = <T>({
})
}
const handleAfterCustomSingleRun = () => {
invalidLastRun()
setTabType(TabType.lastRun)
hideSingleRun()
}
const handleSingleRun = () => {
const { isValid } = checkValid()
if(!isValid)
if (!isValid)
return
if (isCustomRunNode) {
showSingleRun()
return
}
const vars = singleRunParams?.getDependentVars?.()
// no need to input params
if (isAggregatorNode ? checkAggregatorVarsSet(vars) : isAllVarsHasValue(vars)) {
@@ -315,7 +335,9 @@ const useLastRun = <T>({
...oneStepRunRes,
tabType,
isRunAfterSingleRun,
setIsRunAfterSingleRun,
setTabType: handleTabClicked,
handleAfterCustomSingleRun,
singleRunParams,
nodeInfo,
setRunInputData,