Merge branch 'feat/rag-2' into feat/workflow-draft-var-optimize

This commit is contained in:
QuantumGhost
2025-08-31 15:17:07 +08:00
1456 changed files with 86020 additions and 17662 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,16 +188,16 @@ const FormItem: FC<Props> = ({
/>
)
}
{ type === InputVarType.jsonObject && (
{type === InputVarType.jsonObject && (
<CodeEditor
value={value}
language={CodeLanguage.json}
onChange={onChange}
noWrapper
className='bg h-[80px] overflow-y-auto rounded-[10px] bg-components-input-bg-normal p-1'
placeholder={
<div className='whitespace-pre'>{payload.json_schema}</div>
}
className='bg h-[80px] overflow-y-auto rounded-[10px] bg-components-input-bg-normal p-1'
placeholder={
<div className='whitespace-pre'>{payload.json_schema}</div>
}
/>
)}
{(type === InputVarType.singleFile) && (

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()
@ -192,6 +196,8 @@ const FormInputItem: FC<Props> = ({
onChange={handleValueChange}
nodesOutputVars={availableVars}
availableNodes={availableNodesWithParent}
showManageInputField={showManageInputField}
onManageInputField={onManageInputField}
/>
)}
{isNumber && isConstant && (

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
@ -53,9 +54,11 @@ const Editor: FC<Props> = ({
useEffect(() => {
onFocusChange?.(isFocus)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isFocus])
const pipelineId = useStore(s => s.pipelineId)
const setShowInputFieldPanel = useStore(s => s.setShowInputFieldPanel)
return (
<div className={cn(className, 'relative')}>
<>
@ -103,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,18 +1,15 @@
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,
@ -20,9 +17,6 @@ import {
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 +31,6 @@ const PanelOperatorPopup = ({
showHelpLink,
}: PanelOperatorPopupProps) => {
const { t } = useTranslation()
const language = useGetLanguage()
const edges = useEdges()
const {
handleNodeDelete,
@ -48,41 +41,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 nodeMetaData = useNodeMetaData({ id, data } as Node)
const showChangeBlock = data.type !== BlockEnum.Start && !nodesReadOnly && data.type !== BlockEnum.Iteration && data.type !== BlockEnum.Loop
const link = useNodeHelpLink(data.type)
const isChildNode = !!(data.isInIteration || data.isInLoop)
return (
@ -149,28 +110,34 @@ const PanelOperatorPopup = ({
</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.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-red-500
`}
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 +153,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

@ -148,6 +148,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}>
@ -261,7 +263,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,
@ -278,6 +280,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,17 @@
import { useSchemaTypeDefinitions } from '@/service/use-common'
import type { AnyObj } from './match-schema-type'
import matchTheSchemaType from './match-schema-type'
const useMatchSchemaType = () => {
const { data: schemaTypeDefinitions } = useSchemaTypeDefinitions()
const getMatchedSchemaType = (obj: AnyObj): string => {
if(!schemaTypeDefinitions) return ''
const matched = schemaTypeDefinitions.find(def => matchTheSchemaType(obj, def.schema))
return matched ? matched.name : ''
}
return {
getMatchedSchemaType,
}
}
export default useMatchSchemaType

View File

@ -19,9 +19,10 @@ import { OUTPUT_FILE_SUB_VARIABLES } from '../../../constants'
import type { DocExtractorNodeType } from '../../../document-extractor/types'
import { BlockEnum, InputVarType, VarType } from '@/app/components/workflow/types'
import type { StartNodeType } from '@/app/components/workflow/nodes/start/types'
import type { ConversationVariable, EnvironmentVariable, Node, NodeOutPutVar, ValueSelector, Var } from '@/app/components/workflow/types'
import type { ConversationVariable, EnvironmentVariable, Node, NodeOutPutVar, ToolWithProvider, ValueSelector, Var } from '@/app/components/workflow/types'
import type { VariableAssignerNodeType } from '@/app/components/workflow/nodes/variable-assigner/types'
import type { Field as StructField } from '@/app/components/workflow/nodes/llm/types'
import type { RAGPipelineVariable } from '@/models/pipeline'
import {
AGENT_OUTPUT_STRUCT,
@ -34,6 +35,9 @@ import {
TEMPLATE_TRANSFORM_OUTPUT_STRUCT,
TOOL_OUTPUT_STRUCT,
} from '@/app/components/workflow/constants'
import ToolNodeDefault from '@/app/components/workflow/nodes/tool/default'
import DataSourceNodeDefault from '@/app/components/workflow/nodes/data-source/default'
import type { DataSourceNodeType } from '@/app/components/workflow/nodes/data-source/types'
import type { PromptItem } from '@/models/debug'
import { VAR_REGEX } from '@/config'
import type { AgentNodeType } from '../../../agent/types'
@ -50,6 +54,16 @@ export const isConversationVar = (valueSelector: ValueSelector) => {
return valueSelector[0] === 'conversation'
}
export const isRagVariableVar = (valueSelector: ValueSelector) => {
if (!valueSelector)
return false
return valueSelector[0] === 'rag'
}
export const isSpecialVar = (prefix: string): boolean => {
return ['sys', 'env', 'conversation', 'rag'].includes(prefix)
}
export const hasValidChildren = (children: any): boolean => {
return children && (
(Array.isArray(children) && children.length > 0)
@ -207,6 +221,7 @@ const findExceptVarInObject = (obj: any, filterVar: (payload: Var, selector: Val
variable: obj.variable,
type: isFile ? VarType.file : VarType.object,
children: childrenResult,
schemaType: obj.schemaType,
}
return res
@ -216,6 +231,9 @@ const formatItem = (
item: any,
isChatMode: boolean,
filterVar: (payload: Var, selector: ValueSelector) => boolean,
allPluginInfoList: Record<string, ToolWithProvider[]>,
ragVars?: Var[],
getMatchedSchemaType = (_obj: any) => '',
): NodeOutPutVar => {
const { id, data } = item
@ -397,36 +415,8 @@ const formatItem = (
}
case BlockEnum.Tool: {
const {
output_schema,
} = data as ToolNodeType
if (!output_schema) {
res.vars = TOOL_OUTPUT_STRUCT
}
else {
const outputSchema: any[] = []
Object.keys(output_schema.properties).forEach((outputKey) => {
const output = output_schema.properties[outputKey]
const dataType = output.type
outputSchema.push({
variable: outputKey,
type: dataType === 'array'
? `array[${output.items?.type.slice(0, 1).toLocaleLowerCase()}${output.items?.type.slice(1)}]`
: `${output.type.slice(0, 1).toLocaleLowerCase()}${output.type.slice(1)}`,
description: output.description,
children: output.type === 'object' ? {
schema: {
type: 'object',
properties: output.properties,
},
} : undefined,
})
})
res.vars = [
...TOOL_OUTPUT_STRUCT,
...outputSchema,
]
}
const toolOutputVars = ToolNodeDefault.getOutputVars?.(data as ToolNodeType, allPluginInfoList, [], { getMatchedSchemaType }) || []
res.vars = toolOutputVars
break
}
@ -519,6 +509,13 @@ const formatItem = (
break
}
case BlockEnum.DataSource: {
const payload = data as DataSourceNodeType
const dataSourceVars = DataSourceNodeDefault.getOutputVars?.(payload, allPluginInfoList, ragVars, { getMatchedSchemaType }) || []
res.vars = dataSourceVars
break
}
case 'env': {
res.vars = data.envList.map((env: EnvironmentVariable) => {
return {
@ -540,6 +537,18 @@ const formatItem = (
}) as Var[]
break
}
case 'rag': {
res.vars = data.ragVariables.map((ragVar: RAGPipelineVariable) => {
return {
variable: `rag.shared.${ragVar.variable}`,
type: inputVarTypeToVarType(ragVar.type as any),
des: ragVar.label,
isRagVariable: true,
}
}) as Var[]
break
}
}
const { error_strategy } = data
@ -565,7 +574,7 @@ const formatItem = (
const isCurrentMatched = filterVar(v, (() => {
const variableArr = v.variable.split('.')
const [first] = variableArr
if (first === 'sys' || first === 'env' || first === 'conversation')
if (isSpecialVar(first))
return variableArr
return [...selector, ...variableArr]
@ -592,7 +601,6 @@ const formatItem = (
return obj?.children && ((obj?.children as Var[]).length > 0 || Object.keys((obj?.children as StructuredOutput)?.schema?.properties || {}).length > 0)
}).map((v) => {
const isFile = v.type === VarType.file
const { children } = (() => {
if (isFile) {
return {
@ -615,12 +623,25 @@ const formatItem = (
return res
}
export const removeFileVars = (nodeWithVars: NodeOutPutVar[]) => {
return nodeWithVars.map((item) => {
return {
...item,
vars: item.vars.filter(v => v.type !== VarType.file && v.type !== VarType.arrayFile),
}
}).filter(item => item.vars.length > 0)
}
export const toNodeOutputVars = (
nodes: any[],
isChatMode: boolean,
filterVar = (_payload: Var, _selector: ValueSelector) => true,
environmentVariables: EnvironmentVariable[] = [],
conversationVariables: ConversationVariable[] = [],
ragVariables: RAGPipelineVariable[] = [],
allPluginInfoList: Record<string, ToolWithProvider[]>,
getMatchedSchemaType = (_obj: any) => '',
): NodeOutPutVar[] => {
// ENV_NODE data format
const ENV_NODE = {
@ -640,6 +661,15 @@ export const toNodeOutputVars = (
chatVarList: conversationVariables,
},
}
// RAG_PIPELINE_NODE data format
const RAG_PIPELINE_NODE = {
id: 'rag',
data: {
title: 'SHARED INPUTS',
type: 'rag',
ragVariables: ragVariables.filter(ragVariable => ragVariable.belong_to_node_id === 'shared'),
},
}
// Sort nodes in reverse chronological order (most recent first)
const sortedNodes = [...nodes].sort((a, b) => {
if (a.data.type === BlockEnum.Start) return 1
@ -656,9 +686,20 @@ export const toNodeOutputVars = (
...sortedNodes.filter(node => SUPPORT_OUTPUT_VARS_NODE.includes(node?.data?.type)),
...(environmentVariables.length > 0 ? [ENV_NODE] : []),
...((isChatMode && conversationVariables.length > 0) ? [CHAT_VAR_NODE] : []),
...(RAG_PIPELINE_NODE.data.ragVariables.length > 0 ? [RAG_PIPELINE_NODE] : []),
].map((node) => {
let ragVariablesInDataSource: RAGPipelineVariable[] = []
if (node.data.type === BlockEnum.DataSource)
ragVariablesInDataSource = ragVariables.filter(ragVariable => ragVariable.belong_to_node_id === node.id)
return {
...formatItem(node, isChatMode, filterVar),
...formatItem(node, isChatMode, filterVar, allPluginInfoList, ragVariablesInDataSource.map(
(ragVariable: RAGPipelineVariable) => ({
variable: `rag.${node.id}.${ragVariable.variable}`,
type: inputVarTypeToVarType(ragVariable.type as any),
description: ragVariable.label,
isRagVariable: true,
} as Var),
), getMatchedSchemaType),
isStartNode: node.data.type === BlockEnum.Start,
}
}).filter(item => item.vars.length > 0)
@ -694,9 +735,9 @@ const getIterationItemType = ({
curr = Array.isArray(curr) ? curr.find(v => v.variable === key) : []
if (isLast)
arrayType = curr?.type
arrayType = curr?.type
else if (curr?.type === VarType.object || curr?.type === VarType.file)
curr = curr.children || []
curr = curr.children || []
}
}
@ -781,6 +822,10 @@ export const getVarType = ({
isConstant,
environmentVariables = [],
conversationVariables = [],
ragVariables = [],
allPluginInfoList,
getMatchedSchemaType,
preferSchemaType,
}: {
valueSelector: ValueSelector
parentNode?: Node | null
@ -791,6 +836,10 @@ export const getVarType = ({
isConstant?: boolean
environmentVariables?: EnvironmentVariable[]
conversationVariables?: ConversationVariable[]
ragVariables?: RAGPipelineVariable[]
allPluginInfoList: Record<string, ToolWithProvider[]>
getMatchedSchemaType: (obj: any) => string
preferSchemaType?: boolean
}): VarType => {
if (isConstant)
return VarType.string
@ -801,6 +850,9 @@ export const getVarType = ({
undefined,
environmentVariables,
conversationVariables,
ragVariables,
allPluginInfoList,
getMatchedSchemaType,
)
const isIterationInnerVar = parentNode?.data.type === BlockEnum.Iteration
@ -844,11 +896,20 @@ export const getVarType = ({
const isSystem = isSystemVar(valueSelector)
const isEnv = isENV(valueSelector)
const isChatVar = isConversationVar(valueSelector)
const isSharedRagVariable = isRagVariableVar(valueSelector) && valueSelector[1] === 'shared'
const isInNodeRagVariable = isRagVariableVar(valueSelector) && valueSelector[1] !== 'shared'
const startNode = availableNodes.find((node: any) => {
return node?.data.type === BlockEnum.Start
})
const targetVarNodeId = isSystem ? startNode?.id : valueSelector[0]
const targetVarNodeId = (() => {
if (isSystem)
return startNode?.id
if (isInNodeRagVariable)
return valueSelector[1]
return valueSelector[0]
})()
const targetVar = beforeNodesOutputVars.find(v => v.nodeId === targetVarNodeId)
if (!targetVar)
@ -857,18 +918,25 @@ export const getVarType = ({
let type: VarType = VarType.string
let curr: any = targetVar.vars
if (isSystem || isEnv || isChatVar) {
if (isSystem || isEnv || isChatVar || isSharedRagVariable) {
return curr.find((v: any) => v.variable === (valueSelector as ValueSelector).join('.'))?.type
}
else {
const targetVar = curr.find((v: any) => v.variable === valueSelector[1])
const targetVar = curr.find((v: any) => {
if (isInNodeRagVariable)
return v.variable === valueSelector.join('.')
return v.variable === valueSelector[1]
})
if (!targetVar)
return VarType.string
if (isInNodeRagVariable)
return targetVar.type
const isStructuredOutputVar = !!targetVar.children?.schema?.properties
if (isStructuredOutputVar) {
if (valueSelector.length === 2) { // root
return VarType.object
return (preferSchemaType && targetVar.schemaType) ? targetVar.schemaType : VarType.object
}
let currProperties = targetVar.children.schema;
(valueSelector as ValueSelector).slice(2).forEach((key, i) => {
@ -889,7 +957,7 @@ export const getVarType = ({
curr = curr?.find((v: any) => v.variable === key)
if (isLast) {
type = curr?.type
type = (preferSchemaType && curr?.schemaType) ? curr?.schemaType : curr?.type
}
else {
if (curr?.type === VarType.object || curr?.type === VarType.file)
@ -908,7 +976,10 @@ export const toNodeAvailableVars = ({
isChatMode,
environmentVariables,
conversationVariables,
ragVariables,
filterVar,
allPluginInfoList,
getMatchedSchemaType,
}: {
parentNode?: Node | null
t?: any
@ -919,7 +990,11 @@ export const toNodeAvailableVars = ({
environmentVariables?: EnvironmentVariable[]
// chat var
conversationVariables?: ConversationVariable[]
// rag variables
ragVariables?: RAGPipelineVariable[]
filterVar: (payload: Var, selector: ValueSelector) => boolean
allPluginInfoList: Record<string, ToolWithProvider[]>
getMatchedSchemaType: (obj: any) => string
}): NodeOutPutVar[] => {
const beforeNodesOutputVars = toNodeOutputVars(
beforeNodes,
@ -927,6 +1002,9 @@ export const toNodeAvailableVars = ({
filterVar,
environmentVariables,
conversationVariables,
ragVariables,
allPluginInfoList,
getMatchedSchemaType,
)
const isInIteration = parentNode?.data.type === BlockEnum.Iteration
if (isInIteration) {
@ -939,6 +1017,8 @@ export const toNodeAvailableVars = ({
isChatMode,
environmentVariables,
conversationVariables,
allPluginInfoList,
getMatchedSchemaType,
})
const itemChildren = itemType === VarType.file
? {
@ -1088,6 +1168,13 @@ export const getNodeUsedVars = (node: Node): ValueSelector[] => {
res = [...(mixVars as ValueSelector[]), ...(vars as any)]
break
}
case BlockEnum.DataSource: {
const payload = data as DataSourceNodeType
const mixVars = matchNotSystemVars(Object.keys(payload.datasource_parameters)?.filter(key => payload.datasource_parameters[key].type === ToolVarType.mixed).map(key => payload.datasource_parameters[key].value) as string[])
const vars = Object.keys(payload.datasource_parameters).filter(key => payload.datasource_parameters[key].type === ToolVarType.variable).map(key => payload.datasource_parameters[key].value as string) || []
res = [...(mixVars as ValueSelector[]), ...(vars as any)]
break
}
case BlockEnum.VariableAssigner: {
res = (data as VariableAssignerNodeType)?.variables
@ -1381,6 +1468,30 @@ export const updateNodeVars = (oldNode: Node, oldVarSelector: ValueSelector, new
}
break
}
case BlockEnum.DataSource: {
const payload = data as DataSourceNodeType
const hasShouldRenameVar = Object.keys(payload.datasource_parameters)?.filter(key => payload.datasource_parameters[key].type !== ToolVarType.constant)
if (hasShouldRenameVar) {
Object.keys(payload.datasource_parameters).forEach((key) => {
const value = payload.datasource_parameters[key]
const { type } = value
if (type === ToolVarType.variable) {
payload.datasource_parameters[key] = {
...value,
value: newVarSelector,
}
}
if (type === ToolVarType.mixed) {
payload.datasource_parameters[key] = {
...value,
value: replaceOldVarInText(payload.datasource_parameters[key].value as string, oldVarSelector, newVarSelector),
}
}
})
}
break
}
case BlockEnum.VariableAssigner: {
const payload = data as VariableAssignerNodeType
if (payload.variables) {

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,10 +279,18 @@ 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(
<div className={cn(
'relative mr-1 h-full',
)}>
<div
@ -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: {
@ -356,7 +420,7 @@ const BasePanel: FC<BasePanelProps> = ({
>
{
isSingleRunning ? <Stop className='h-4 w-4 text-text-tertiary' />
: <RiPlayLargeLine className='h-4 w-4 text-text-tertiary' />
: <RiPlayLargeLine className='h-4 w-4 text-text-tertiary' />
}
</div>
</Tooltip>
@ -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,

View File

@ -4,7 +4,11 @@ import {
useWorkflow,
useWorkflowVariables,
} from '@/app/components/workflow/hooks'
import type { Node, ValueSelector, Var } from '@/app/components/workflow/types'
import type { NodeOutPutVar } from '@/app/components/workflow/types'
import { BlockEnum, type Node, type ValueSelector, type Var } from '@/app/components/workflow/types'
import { useStore as useWorkflowStore } from '@/app/components/workflow/store'
import { inputVarTypeToVarType } from '../../data-source/utils'
type Params = {
onlyLeafNodeVar?: boolean
hideEnv?: boolean
@ -24,29 +28,57 @@ const useAvailableVarList = (nodeId: string, {
onlyLeafNodeVar: false,
filterVar: () => true,
}) => {
const { getTreeLeafNodes, getBeforeNodesInSameBranchIncludeParent } = useWorkflow()
const { getTreeLeafNodes, getNodeById, getBeforeNodesInSameBranchIncludeParent } = useWorkflow()
const { getNodeAvailableVars } = useWorkflowVariables()
const isChatMode = useIsChatMode()
const availableNodes = passedInAvailableNodes || (onlyLeafNodeVar ? getTreeLeafNodes(nodeId) : getBeforeNodesInSameBranchIncludeParent(nodeId))
const {
parentNode: iterationNode,
} = useNodeInfo(nodeId)
const availableVars = getNodeAvailableVars({
const currNode = getNodeById(nodeId)
const ragPipelineVariables = useWorkflowStore(s => s.ragPipelineVariables)
const isDataSourceNode = currNode?.data?.type === BlockEnum.DataSource
const dataSourceRagVars: NodeOutPutVar[] = []
if(isDataSourceNode) {
const ragVariablesInDataSource = ragPipelineVariables?.filter(ragVariable => ragVariable.belong_to_node_id === nodeId)
const filterVars = ragVariablesInDataSource?.filter(v => filterVar({
variable: v.variable,
type: inputVarTypeToVarType(v.type),
nodeId,
isRagVariable: true,
}, ['rag', nodeId, v.variable]))
if(filterVars?.length) {
dataSourceRagVars.push({
nodeId,
title: currNode.data?.title,
vars: filterVars.map((v) => {
return {
variable: `rag.${nodeId}.${v.variable}`,
type: inputVarTypeToVarType(v.type),
description: v.label,
isRagVariable: true,
} as Var
}),
})
}
}
const availableVars = [...getNodeAvailableVars({
parentNode: iterationNode,
beforeNodes: availableNodes,
isChatMode,
filterVar,
hideEnv,
hideChatVar,
})
}), ...dataSourceRagVars]
return {
availableVars,
availableNodes,
availableNodesWithParent: availableNodes,
availableNodesWithParent: [
...availableNodes,
...(isDataSourceNode ? [currNode] : []),
],
}
}

View File

@ -1,67 +1,15 @@
import { useMemo } from 'react'
import { useDocLink, useGetLanguage } from '@/context/i18n'
import { BlockEnum } from '@/app/components/workflow/types'
import type { BlockEnum } from '@/app/components/workflow/types'
import { useNodesMetaData } from '@/app/components/workflow/hooks'
export const useNodeHelpLink = (nodeType: BlockEnum) => {
const language = useGetLanguage()
const docLink = useDocLink()
const prefixLink = useMemo(() => {
return docLink('/guides/workflow/node/')
}, [language])
const linkMap = useMemo(() => {
if (language === 'zh_Hans') {
return {
[BlockEnum.Start]: 'start',
[BlockEnum.End]: 'end',
[BlockEnum.Answer]: 'answer',
[BlockEnum.LLM]: 'llm',
[BlockEnum.KnowledgeRetrieval]: 'knowledge-retrieval',
[BlockEnum.QuestionClassifier]: 'question-classifier',
[BlockEnum.IfElse]: 'ifelse',
[BlockEnum.Code]: 'code',
[BlockEnum.TemplateTransform]: 'template',
[BlockEnum.VariableAssigner]: 'variable-assigner',
[BlockEnum.VariableAggregator]: 'variable-aggregator',
[BlockEnum.Assigner]: 'variable-assigner',
[BlockEnum.Iteration]: 'iteration',
[BlockEnum.Loop]: 'loop',
[BlockEnum.ParameterExtractor]: 'parameter-extractor',
[BlockEnum.HttpRequest]: 'http-request',
[BlockEnum.Tool]: 'tools',
[BlockEnum.DocExtractor]: 'doc-extractor',
[BlockEnum.ListFilter]: 'list-operator',
[BlockEnum.Agent]: 'agent',
}
}
const availableNodesMetaData = useNodesMetaData()
return {
[BlockEnum.Start]: 'start',
[BlockEnum.End]: 'end',
[BlockEnum.Answer]: 'answer',
[BlockEnum.LLM]: 'llm',
[BlockEnum.KnowledgeRetrieval]: 'knowledge-retrieval',
[BlockEnum.QuestionClassifier]: 'question-classifier',
[BlockEnum.IfElse]: 'ifelse',
[BlockEnum.Code]: 'code',
[BlockEnum.TemplateTransform]: 'template',
[BlockEnum.VariableAssigner]: 'variable-assigner',
[BlockEnum.VariableAggregator]: 'variable-aggregator',
[BlockEnum.Assigner]: 'variable-assigner',
[BlockEnum.Iteration]: 'iteration',
[BlockEnum.Loop]: 'loop',
[BlockEnum.ParameterExtractor]: 'parameter-extractor',
[BlockEnum.HttpRequest]: 'http-request',
[BlockEnum.Tool]: 'tools',
[BlockEnum.DocExtractor]: 'doc-extractor',
[BlockEnum.ListFilter]: 'list-operator',
[BlockEnum.Agent]: 'agent',
}
}, [language]) as Record<string, string>
const link = useMemo(() => {
const result = availableNodesMetaData?.nodesMap?.[nodeType]?.metaData.helpLinkUri || ''
const link = linkMap[nodeType]
return result
}, [availableNodesMetaData, nodeType])
if (!link)
return ''
return `${prefixLink}${link}`
return link
}

View File

@ -11,7 +11,6 @@ import { getNodeInfoById, isConversationVar, isENV, isSystemVar, toNodeOutputVar
import type { CommonNodeType, InputVar, ValueSelector, Var, Variable } from '@/app/components/workflow/types'
import { BlockEnum, InputVarType, NodeRunningStatus, VarType } from '@/app/components/workflow/types'
import { useStore as useAppStore } from '@/app/components/app/store'
import { useStore, useWorkflowStore } from '@/app/components/workflow/store'
import { fetchNodeInspectVars, getIterationSingleNodeRunUrl, getLoopSingleNodeRunUrl, singleNodeRun } from '@/service/workflow'
import Toast from '@/app/components/base/toast'
@ -52,6 +51,7 @@ import {
} from 'reactflow'
import { useInvalidLastRun } from '@/service/use-workflow'
import useInspectVarsCrud from '../../../hooks/use-inspect-vars-crud'
import type { FlowType } from '@/types/common'
// eslint-disable-next-line ts/no-unsafe-function-type
const checkValidFns: Record<BlockEnum, Function> = {
[BlockEnum.LLM]: checkLLMValid,
@ -72,6 +72,8 @@ const checkValidFns: Record<BlockEnum, Function> = {
export type Params<T> = {
id: string
flowId: string
flowType: FlowType
data: CommonNodeType<T>
defaultRunInputData: Record<string, any>
moreDataForCheckValid?: any
@ -108,6 +110,8 @@ const varTypeToInputVarType = (type: VarType, {
const useOneStepRun = <T>({
id,
flowId,
flowType,
data,
defaultRunInputData,
moreDataForCheckValid,
@ -126,7 +130,19 @@ const useOneStepRun = <T>({
const availableNodes = getBeforeNodesInSameBranch(id)
const availableNodesIncludeParent = getBeforeNodesInSameBranchIncludeParent(id)
const allOutputVars = toNodeOutputVars(availableNodes, isChatMode, undefined, undefined, conversationVariables)
const buildInTools = useStore(s => s.buildInTools)
const customTools = useStore(s => s.customTools)
const workflowTools = useStore(s => s.workflowTools)
const mcpTools = useStore(s => s.mcpTools)
const dataSourceList = useStore(s => s.dataSourceList)
const allPluginInfoList = {
buildInTools,
customTools,
workflowTools,
mcpTools,
dataSourceList: dataSourceList ?? [],
}
const allOutputVars = toNodeOutputVars(availableNodes, isChatMode, undefined, undefined, conversationVariables, [], allPluginInfoList)
const getVar = (valueSelector: ValueSelector): Var | undefined => {
const isSystem = valueSelector[0] === 'sys'
const targetVar = allOutputVars.find(item => isSystem ? !!item.isStartNode : item.nodeId === valueSelector[0])
@ -155,7 +171,6 @@ const useOneStepRun = <T>({
const checkValid = checkValidFns[data.type]
const appId = useAppStore.getState().appDetail?.id
const [runInputData, setRunInputData] = useState<Record<string, any>>(defaultRunInputData || {})
const runInputDataRef = useRef(runInputData)
const handleSetRunInputData = useCallback((data: Record<string, any>) => {
@ -170,7 +185,7 @@ const useOneStepRun = <T>({
const {
setShowSingleRunPanel,
} = workflowStore.getState()
const invalidLastRun = useInvalidLastRun(appId!, id)
const invalidLastRun = useInvalidLastRun(flowType, flowId!, id)
const [runResult, doSetRunResult] = useState<NodeRunResult | null>(null)
const {
appendNodeInspectVars,
@ -197,7 +212,7 @@ const useOneStepRun = <T>({
}
// run fail may also update the inspect vars when the node set the error default output.
const vars = await fetchNodeInspectVars(appId!, id)
const vars = await fetchNodeInspectVars(flowType, flowId!, id)
const { getNodes } = store.getState()
const nodes = getNodes()
appendNodeInspectVars(id, vars, nodes)
@ -207,7 +222,7 @@ const useOneStepRun = <T>({
invalidateSysVarValues()
invalidateConversationVarValues() // loop, iteration, variable assigner node can update the conversation variables, but to simple the logic(some nodes may also can update in the future), all nodes refresh.
}
}, [isRunAfterSingleRun, runningStatus, appId, id, store, appendNodeInspectVars, invalidLastRun, isStartNode, invalidateSysVarValues, invalidateConversationVarValues])
}, [isRunAfterSingleRun, runningStatus, flowId, id, store, appendNodeInspectVars, invalidLastRun, isStartNode, invalidateSysVarValues, invalidateConversationVarValues])
const { handleNodeDataUpdate }: { handleNodeDataUpdate: (data: any) => void } = useNodeDataUpdate()
const setNodeRunning = () => {
@ -306,14 +321,14 @@ const useOneStepRun = <T>({
else {
postData.inputs = submitData
}
res = await singleNodeRun(appId!, id, postData) as any
res = await singleNodeRun(flowType, flowId!, id, postData) as any
}
else if (isIteration) {
setIterationRunResult([])
let _iterationResult: NodeTracing[] = []
let _runResult: any = null
ssePost(
getIterationSingleNodeRunUrl(isChatMode, appId!, id),
getIterationSingleNodeRunUrl(flowType, isChatMode, flowId!, id),
{ body: { inputs: submitData } },
{
onWorkflowStarted: noop,
@ -416,7 +431,7 @@ const useOneStepRun = <T>({
let _loopResult: NodeTracing[] = []
let _runResult: any = null
ssePost(
getLoopSingleNodeRunUrl(isChatMode, appId!, id),
getLoopSingleNodeRunUrl(flowType, isChatMode, flowId!, id),
{ body: { inputs: submitData } },
{
onWorkflowStarted: noop,
@ -634,7 +649,6 @@ const useOneStepRun = <T>({
}
return {
appId,
isShowSingleRun,
hideSingleRun,
showSingleRun,
@ -649,6 +663,7 @@ const useOneStepRun = <T>({
runInputDataRef,
setRunInputData: handleSetRunInputData,
runResult,
setRunResult: doSetRunResult,
iterationRunResult,
loopRunResult,
setNodeRunning,

View File

@ -139,9 +139,8 @@ const BaseNode: FC<BaseNodeProps> = ({
return (
<div
className={cn(
'flex rounded-2xl border-[2px]',
'relative flex rounded-2xl border',
showSelectedBorder ? 'border-components-option-card-option-selected-border' : 'border-transparent',
!showSelectedBorder && data._inParallelHovering && 'border-workflow-block-border-highlight',
data._waitingRun && 'opacity-70',
data._dimmed && 'opacity-30',
)}
@ -151,6 +150,15 @@ const BaseNode: FC<BaseNodeProps> = ({
height: (data.type === BlockEnum.Iteration || data.type === BlockEnum.Loop) ? data.height : 'auto',
}}
>
{
data.type === BlockEnum.DataSource && (
<div className='absolute inset-[-2px] top-[-22px] z-[-1] rounded-[18px] bg-node-data-source-bg p-0.5 backdrop-blur-[6px]'>
<div className='system-2xs-semibold-uppercase flex h-5 items-center px-2.5 text-text-tertiary'>
{t('workflow.blocks.datasource')}
</div>
</div>
)
}
<div
className={cn(
'group relative pb-1 shadow-xs',
@ -165,13 +173,6 @@ const BaseNode: FC<BaseNodeProps> = ({
data._isBundled && '!shadow-lg',
)}
>
{
data._inParallelHovering && (
<div className='top system-2xs-medium-uppercase absolute -top-2.5 left-2 z-10 text-text-tertiary'>
{t('workflow.common.parallelRun')}
</div>
)
}
{
data._showAddVariablePopup && (
<AddVariablePopupWithPosition