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
commit 85cda47c70
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
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,

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,8 @@ import {
} from 'reactflow'
import { useInvalidLastRun } from '@/service/use-workflow'
import useInspectVarsCrud from '../../../hooks/use-inspect-vars-crud'
import type { FlowType } from '@/types/common'
import useMatchSchemaType from '../components/variable/use-match-schema-type'
// eslint-disable-next-line ts/no-unsafe-function-type
const checkValidFns: Record<BlockEnum, Function> = {
[BlockEnum.LLM]: checkLLMValid,
@ -72,6 +73,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 +111,8 @@ const varTypeToInputVarType = (type: VarType, {
const useOneStepRun = <T>({
id,
flowId,
flowType,
data,
defaultRunInputData,
moreDataForCheckValid,
@ -126,9 +131,26 @@ const useOneStepRun = <T>({
const availableNodes = getBeforeNodesInSameBranch(id)
const availableNodesIncludeParent = getBeforeNodesInSameBranchIncludeParent(id)
const allOutputVars = toNodeOutputVars(availableNodes, isChatMode, undefined, undefined, conversationVariables)
const workflowStore = useWorkflowStore()
const { schemaTypeDefinitions } = useMatchSchemaType()
const getVar = (valueSelector: ValueSelector): Var | undefined => {
const isSystem = valueSelector[0] === 'sys'
const {
buildInTools,
customTools,
workflowTools,
mcpTools,
dataSourceList,
} = workflowStore.getState()
const allPluginInfoList = {
buildInTools,
customTools,
workflowTools,
mcpTools,
dataSourceList: dataSourceList ?? [],
}
const allOutputVars = toNodeOutputVars(availableNodes, isChatMode, undefined, undefined, conversationVariables, [], allPluginInfoList, schemaTypeDefinitions)
const targetVar = allOutputVars.find(item => isSystem ? !!item.isStartNode : item.nodeId === valueSelector[0])
if (!targetVar)
return undefined
@ -155,7 +177,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>) => {
@ -166,11 +187,10 @@ const useOneStepRun = <T>({
const loopTimes = loopInputKey ? runInputData[loopInputKey]?.length : 0
const store = useStoreApi()
const workflowStore = useWorkflowStore()
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 +217,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 +227,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 +326,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 +436,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 +654,6 @@ const useOneStepRun = <T>({
}
return {
appId,
isShowSingleRun,
hideSingleRun,
showSingleRun,
@ -649,6 +668,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

View file

@ -1,24 +1,20 @@
import type { StrategyDetail, StrategyPluginDetail } from '@/app/components/plugins/types'
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/blocks'
import type { NodeDefault } from '../../types'
import { BlockEnum, type NodeDefault } from '../../types'
import type { AgentNodeType } from './types'
import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { genNodeMetaData } from '../../utils'
const metaData = genNodeMetaData({
sort: 3,
type: BlockEnum.Agent,
})
import { renderI18nObject } from '@/i18n-config'
const nodeDefault: NodeDefault<AgentNodeType> = {
metaData,
defaultValue: {
tool_node_version: '2',
},
getAvailablePrevNodes(isChatMode) {
return isChatMode
? ALL_CHAT_AVAILABLE_BLOCKS
: ALL_COMPLETION_AVAILABLE_BLOCKS
},
getAvailableNextNodes(isChatMode) {
return isChatMode
? ALL_CHAT_AVAILABLE_BLOCKS
: ALL_COMPLETION_AVAILABLE_BLOCKS
},
checkValid(payload, t, moreDataForCheckValid: {
strategyProvider?: StrategyPluginDetail,
strategy?: StrategyDetail

View file

@ -181,7 +181,7 @@ const useConfig = (id: string, payload: AgentNodeType) => {
const outputSchema = useMemo(() => {
const res: any[] = []
if (!inputs.output_schema)
if (!inputs.output_schema || !inputs.output_schema.properties)
return []
Object.keys(inputs.output_schema.properties).forEach((outputKey) => {
const output = inputs.output_schema.properties[outputKey]

View file

@ -1,25 +1,19 @@
import { BlockEnum } from '../../types'
import type { NodeDefault } from '../../types'
import type { AnswerNodeType } from './types'
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/blocks'
import { genNodeMetaData } from '@/app/components/workflow/utils'
import { BlockEnum } from '@/app/components/workflow/types'
const metaData = genNodeMetaData({
sort: 2.1,
type: BlockEnum.Answer,
isRequired: true,
})
const nodeDefault: NodeDefault<AnswerNodeType> = {
metaData,
defaultValue: {
variables: [],
answer: '',
},
getAvailablePrevNodes(isChatMode: boolean) {
const nodes = isChatMode
? ALL_CHAT_AVAILABLE_BLOCKS
: ALL_COMPLETION_AVAILABLE_BLOCKS.filter(type => type !== BlockEnum.End)
return nodes
},
getAvailableNextNodes(isChatMode: boolean) {
const nodes = isChatMode
? ALL_CHAT_AVAILABLE_BLOCKS
: ALL_COMPLETION_AVAILABLE_BLOCKS
return nodes
},
checkValid(payload: AnswerNodeType, t: any) {
let errorMessages = ''
const { answer } = payload

View file

@ -1,24 +1,22 @@
import { BlockEnum } from '../../types'
import type { NodeDefault } from '../../types'
import { type AssignerNodeType, WriteMode } from './types'
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/blocks'
import { genNodeMetaData } from '@/app/components/workflow/utils'
import { BlockEnum } from '@/app/components/workflow/types'
import { BlockClassificationEnum } from '@/app/components/workflow/block-selector/types'
const i18nPrefix = 'workflow.errorMsg'
const metaData = genNodeMetaData({
classification: BlockClassificationEnum.Transform,
sort: 5,
type: BlockEnum.Assigner,
helpLinkUri: 'variable-assigner',
})
const nodeDefault: NodeDefault<AssignerNodeType> = {
metaData,
defaultValue: {
version: '2',
items: [],
},
getAvailablePrevNodes(isChatMode: boolean) {
const nodes = isChatMode
? ALL_CHAT_AVAILABLE_BLOCKS
: ALL_COMPLETION_AVAILABLE_BLOCKS.filter(type => type !== BlockEnum.End)
return nodes
},
getAvailableNextNodes(isChatMode: boolean) {
const nodes = isChatMode ? ALL_CHAT_AVAILABLE_BLOCKS : ALL_COMPLETION_AVAILABLE_BLOCKS
return nodes
},
checkValid(payload: AssignerNodeType, t: any) {
let errorMessages = ''
const {

View file

@ -1,27 +1,24 @@
import { BlockEnum } from '../../types'
import type { NodeDefault } from '../../types'
import { CodeLanguage, type CodeNodeType } from './types'
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/blocks'
import { genNodeMetaData } from '@/app/components/workflow/utils'
import { BlockEnum } from '@/app/components/workflow/types'
import { BlockClassificationEnum } from '@/app/components/workflow/block-selector/types'
const i18nPrefix = 'workflow.errorMsg'
const metaData = genNodeMetaData({
classification: BlockClassificationEnum.Transform,
sort: 1,
type: BlockEnum.Code,
})
const nodeDefault: NodeDefault<CodeNodeType> = {
metaData,
defaultValue: {
code: '',
code_language: CodeLanguage.python3,
variables: [],
outputs: {},
},
getAvailablePrevNodes(isChatMode: boolean) {
const nodes = isChatMode
? ALL_CHAT_AVAILABLE_BLOCKS
: ALL_COMPLETION_AVAILABLE_BLOCKS.filter(type => type !== BlockEnum.End)
return nodes
},
getAvailableNextNodes(isChatMode: boolean) {
const nodes = isChatMode ? ALL_CHAT_AVAILABLE_BLOCKS : ALL_COMPLETION_AVAILABLE_BLOCKS
return nodes
},
checkValid(payload: CodeNodeType, t: any) {
let errorMessages = ''
const { code, variables } = payload

View file

@ -8,8 +8,10 @@ import { useStore } from '../../store'
import type { CodeNodeType, OutputVar } from './types'
import { CodeLanguage } from './types'
import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud'
import { fetchNodeDefault } from '@/service/workflow'
import { useStore as useAppStore } from '@/app/components/app/store'
import {
fetchNodeDefault,
fetchPipelineNodeDefault,
} from '@/service/workflow'
import {
useNodesReadOnly,
} from '@/app/components/workflow/hooks'
@ -17,7 +19,8 @@ import {
const useConfig = (id: string, payload: CodeNodeType) => {
const { nodesReadOnly: readOnly } = useNodesReadOnly()
const appId = useAppStore.getState().appDetail?.id
const appId = useStore(s => s.appId)
const pipelineId = useStore(s => s.pipelineId)
const [allLanguageDefault, setAllLanguageDefault] = useState<Record<CodeLanguage, CodeNodeType> | null>(null)
useEffect(() => {
@ -33,7 +36,20 @@ const useConfig = (id: string, payload: CodeNodeType) => {
}
}, [appId])
const defaultConfig = useStore(s => s.nodesDefaultConfigs)[payload.type]
useEffect(() => {
if (pipelineId) {
(async () => {
const { config: javaScriptConfig } = await fetchPipelineNodeDefault(pipelineId, BlockEnum.Code, { code_language: CodeLanguage.javascript }) as any
const { config: pythonConfig } = await fetchPipelineNodeDefault(pipelineId, BlockEnum.Code, { code_language: CodeLanguage.python3 }) as any
setAllLanguageDefault({
[CodeLanguage.javascript]: javaScriptConfig as CodeNodeType,
[CodeLanguage.python3]: pythonConfig as CodeNodeType,
} as any)
})()
}
}, [pipelineId])
const defaultConfig = useStore(s => s.nodesDefaultConfigs)?.[payload.type]
const { inputs, setInputs } = useNodeCrud<CodeNodeType>(id, payload)
const { handleVarListChange, handleAddVariable } = useVarList<CodeNodeType>({
inputs,

View file

@ -38,6 +38,10 @@ import ListFilterNode from './list-operator/node'
import ListFilterPanel from './list-operator/panel'
import AgentNode from './agent/node'
import AgentPanel from './agent/panel'
import DataSourceNode from './data-source/node'
import DataSourcePanel from './data-source/panel'
import KnowledgeBaseNode from './knowledge-base/node'
import KnowledgeBasePanel from './knowledge-base/panel'
import { TransferMethod } from '@/types/app'
export const NodeComponentMap: Record<string, ComponentType<any>> = {
@ -61,6 +65,8 @@ export const NodeComponentMap: Record<string, ComponentType<any>> = {
[BlockEnum.DocExtractor]: DocExtractorNode,
[BlockEnum.ListFilter]: ListFilterNode,
[BlockEnum.Agent]: AgentNode,
[BlockEnum.DataSource]: DataSourceNode,
[BlockEnum.KnowledgeBase]: KnowledgeBaseNode,
}
export const PanelComponentMap: Record<string, ComponentType<any>> = {
@ -84,6 +90,8 @@ export const PanelComponentMap: Record<string, ComponentType<any>> = {
[BlockEnum.DocExtractor]: DocExtractorPanel,
[BlockEnum.ListFilter]: ListFilterPanel,
[BlockEnum.Agent]: AgentPanel,
[BlockEnum.DataSource]: DataSourcePanel,
[BlockEnum.KnowledgeBase]: KnowledgeBasePanel,
}
export const CUSTOM_NODE_TYPE = 'custom'

View file

@ -0,0 +1 @@
export const CUSTOM_DATA_SOURCE_EMPTY_NODE = 'custom-data-source-empty'

View file

@ -0,0 +1,22 @@
import type { NodeDefault } from '../../types'
import type { DataSourceEmptyNodeType } from './types'
import { genNodeMetaData } from '@/app/components/workflow/utils'
import { BlockEnum } from '@/app/components/workflow/types'
const metaData = genNodeMetaData({
sort: -1,
type: BlockEnum.DataSourceEmpty,
isUndeletable: true,
isSingleton: true,
})
const nodeDefault: NodeDefault<DataSourceEmptyNodeType> = {
metaData,
defaultValue: {},
checkValid() {
return {
isValid: true,
}
},
}
export default nodeDefault

View file

@ -0,0 +1,50 @@
import { useCallback } from 'react'
import { useStoreApi } from 'reactflow'
import { produce } from 'immer'
import type { OnSelectBlock } from '@/app/components/workflow/types'
import { generateNewNode } from '@/app/components/workflow/utils'
import { useNodesMetaData } from '@/app/components/workflow/hooks'
export const useReplaceDataSourceNode = (id: string) => {
const store = useStoreApi()
const { nodesMap: nodesMetaDataMap } = useNodesMetaData()
const handleReplaceNode = useCallback<OnSelectBlock>((
type,
toolDefaultValue,
) => {
const {
getNodes,
setNodes,
} = store.getState()
const nodes = getNodes()
const emptyNodeIndex = nodes.findIndex(node => node.id === id)
if (emptyNodeIndex < 0) return
const {
defaultValue,
} = nodesMetaDataMap![type]
const emptyNode = nodes[emptyNodeIndex]
const { newNode } = generateNewNode({
data: {
...(defaultValue as any),
...toolDefaultValue,
},
position: {
x: emptyNode.position.x,
y: emptyNode.position.y,
},
})
const newNodes = produce(nodes, (draft) => {
draft[emptyNodeIndex] = newNode
})
const newNodesWithoutTempNodes = produce(newNodes, (draft) => {
return draft.filter(node => !node.data._isTempNode)
})
setNodes(newNodesWithoutTempNodes)
}, [])
return {
handleReplaceNode,
}
}

View file

@ -0,0 +1,74 @@
import {
memo,
useCallback,
} from 'react'
import { useTranslation } from 'react-i18next'
import type { NodeProps } from 'reactflow'
import { RiAddLine } from '@remixicon/react'
import cn from '@/utils/classnames'
import Button from '@/app/components/base/button'
import BlockSelector from '@/app/components/workflow/block-selector'
import { useReplaceDataSourceNode } from './hooks'
const DataSourceEmptyNode = ({ id, data }: NodeProps) => {
const { t } = useTranslation()
const { handleReplaceNode } = useReplaceDataSourceNode(id)
const renderTrigger = useCallback(() => {
return (
<Button
variant='primary'
className='w-full'
>
<RiAddLine className='mr-1 h-4 w-4' />
{t('workflow.nodes.dataSource.add')}
</Button>
)
}, [])
return (
<div
className={cn(
'relative flex rounded-2xl border',
'border-transparent',
)}
style={{
width: data.width,
height: data.height,
}}
>
<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 shadow-xs',
'rounded-[15px] border border-transparent',
'w-[240px] bg-workflow-block-bg',
)}
>
<div className={cn(
'flex items-center rounded-t-2xl p-3',
)}>
<BlockSelector
asChild
onSelect={handleReplaceNode}
trigger={renderTrigger}
noBlocks
noTools
popupClassName='w-[320px]'
placement='bottom-start'
offset={{
mainAxis: 4,
crossAxis: 0,
}}
/>
</div>
</div>
</div>
)
}
export default memo(DataSourceEmptyNode)

View file

@ -0,0 +1,3 @@
import type { CommonNodeType } from '@/app/components/workflow/types'
export type DataSourceEmptyNodeType = CommonNodeType

View file

@ -0,0 +1,116 @@
'use client'
import type { FC } from 'react'
import React, { useCallback } from 'react'
import type { CustomRunFormProps } from './types'
import { DatasourceType } from '@/models/pipeline'
import LocalFile from '@/app/components/datasets/documents/create-from-pipeline/data-source/local-file'
import OnlineDocuments from '@/app/components/datasets/documents/create-from-pipeline/data-source/online-documents'
import WebsiteCrawl from '@/app/components/datasets/documents/create-from-pipeline/data-source/website-crawl'
import OnlineDrive from '@/app/components/datasets/documents/create-from-pipeline/data-source/online-drive'
import { useDataSourceStore } from '@/app/components/datasets/documents/create-from-pipeline/data-source/store'
import { useOnlineDocument, useOnlineDrive, useWebsiteCrawl } from '@/app/components/rag-pipeline/components/panel/test-run/preparation/hooks'
import Button from '@/app/components/base/button'
import { useTranslation } from 'react-i18next'
import DataSourceProvider from '@/app/components/datasets/documents/create-from-pipeline/data-source/store/provider'
import PanelWrap from '../_base/components/before-run-form/panel-wrap'
import useBeforeRunForm from './hooks/use-before-run-form'
const BeforeRunForm: FC<CustomRunFormProps> = (props) => {
const {
nodeId,
payload,
onCancel,
} = props
const { t } = useTranslation()
const dataSourceStore = useDataSourceStore()
const {
isPending,
handleRunWithSyncDraft,
datasourceType,
datasourceNodeData,
startRunBtnDisabled,
} = useBeforeRunForm(props)
const { clearOnlineDocumentData } = useOnlineDocument()
const { clearWebsiteCrawlData } = useWebsiteCrawl()
const { clearOnlineDriveData } = useOnlineDrive()
const clearDataSourceData = useCallback(() => {
if (datasourceType === DatasourceType.onlineDocument)
clearOnlineDocumentData()
else if (datasourceType === DatasourceType.websiteCrawl)
clearWebsiteCrawlData()
else if (datasourceType === DatasourceType.onlineDrive)
clearOnlineDriveData()
}, [datasourceType])
const handleCredentialChange = useCallback((credentialId: string) => {
const { setCurrentCredentialId } = dataSourceStore.getState()
clearDataSourceData()
setCurrentCredentialId(credentialId)
}, [dataSourceStore])
return (
<PanelWrap
nodeName={payload.title}
onHide={onCancel}
>
<div className='flex flex-col gap-y-5 px-4 pt-4'>
{datasourceType === DatasourceType.localFile && (
<LocalFile
allowedExtensions={datasourceNodeData.fileExtensions || []}
notSupportBatchUpload
/>
)}
{datasourceType === DatasourceType.onlineDocument && (
<OnlineDocuments
nodeId={nodeId}
nodeData={datasourceNodeData}
isInPipeline
onCredentialChange={handleCredentialChange}
/>
)}
{datasourceType === DatasourceType.websiteCrawl && (
<WebsiteCrawl
nodeId={nodeId}
nodeData={datasourceNodeData}
isInPipeline
onCredentialChange={handleCredentialChange}
/>
)}
{datasourceType === DatasourceType.onlineDrive && (
<OnlineDrive
nodeId={nodeId}
nodeData={datasourceNodeData}
isInPipeline
onCredentialChange={handleCredentialChange}
/>
)}
<div className='flex justify-end gap-x-2'>
<Button onClick={onCancel}>
{t('common.operation.cancel')}
</Button>
<Button
onClick={handleRunWithSyncDraft}
variant='primary'
loading={isPending}
disabled={isPending || startRunBtnDisabled}
>
{t('workflow.singleRun.startRun')}
</Button>
</div>
</div>
</PanelWrap>
)
}
const BeforeRunFormWrapper = (props: CustomRunFormProps) => {
return (
<DataSourceProvider>
<BeforeRunForm {...props} />
</DataSourceProvider>
)
}
export default React.memo(BeforeRunFormWrapper)

View file

@ -0,0 +1,59 @@
import { VarType } from '@/app/components/workflow/types'
export const COMMON_OUTPUT = [
{
name: 'datasource_type',
type: VarType.string,
description: 'local_file, online_document, website_crawl',
},
]
export const LOCAL_FILE_OUTPUT = [
{
name: 'file',
type: VarType.file,
description: 'file',
subItems: [
{
name: 'name',
type: VarType.string,
description: 'file name',
},
{
name: 'size',
type: VarType.number,
description: 'file size',
},
{
name: 'type',
type: VarType.string,
description: 'file type',
},
{
name: 'extension',
type: VarType.string,
description: 'file extension',
},
{
name: 'mime_type',
type: VarType.string,
description: 'file mime type',
},
{
name: 'transfer_method',
type: VarType.string,
description: 'file transfer method',
},
{
name: 'url',
type: VarType.string,
description: 'file url',
},
{
name: 'related_id',
type: VarType.string,
description: 'file related id',
},
],
},
]

View file

@ -0,0 +1,111 @@
import type { NodeDefault } from '../../types'
import type { DataSourceNodeType } from './types'
import { DataSourceClassification } from './types'
import { genNodeMetaData } from '@/app/components/workflow/utils'
import { BlockEnum } from '@/app/components/workflow/types'
import {
COMMON_OUTPUT,
LOCAL_FILE_OUTPUT,
} from './constants'
import { VarType as VarKindType } from '@/app/components/workflow/nodes/tool/types'
import { getMatchedSchemaType } from '../_base/components/variable/use-match-schema-type'
const i18nPrefix = 'workflow.errorMsg'
const metaData = genNodeMetaData({
sort: -1,
type: BlockEnum.DataSource,
isStart: true,
isRequired: true,
})
const nodeDefault: NodeDefault<DataSourceNodeType> = {
metaData,
defaultValue: {
datasource_parameters: {},
datasource_configurations: {},
},
checkValid(payload, t, moreDataForCheckValid) {
const { dataSourceInputsSchema, notAuthed } = moreDataForCheckValid
let errorMessage = ''
if (notAuthed)
errorMessage = t(`${i18nPrefix}.authRequired`)
if (!errorMessage) {
dataSourceInputsSchema.filter((field: any) => {
return field.required
}).forEach((field: any) => {
const targetVar = payload.datasource_parameters[field.variable]
if (!targetVar) {
errorMessage = t(`${i18nPrefix}.fieldRequired`, { field: field.label })
return
}
const { type: variable_type, value } = targetVar
if (variable_type === VarKindType.variable) {
if (!errorMessage && (!value || value.length === 0))
errorMessage = t(`${i18nPrefix}.fieldRequired`, { field: field.label })
}
else {
if (!errorMessage && (value === undefined || value === null || value === ''))
errorMessage = t(`${i18nPrefix}.fieldRequired`, { field: field.label })
}
})
}
return {
isValid: !errorMessage,
errorMessage,
}
},
getOutputVars(payload, allPluginInfoList, ragVars = [], { schemaTypeDefinitions } = { schemaTypeDefinitions: [] }) {
const {
plugin_id,
datasource_name,
provider_type,
} = payload
const isLocalFile = provider_type === DataSourceClassification.localFile
const currentDataSource = allPluginInfoList.dataSourceList?.find((ds: any) => ds.plugin_id === plugin_id)
const currentDataSourceItem = currentDataSource?.tools?.find((tool: any) => tool.name === datasource_name)
const output_schema = currentDataSourceItem?.output_schema
const dynamicOutputSchema: any[] = []
if (output_schema?.properties) {
Object.keys(output_schema.properties).forEach((outputKey) => {
const output = output_schema.properties[outputKey]
const dataType = output.type
let type = dataType === 'array'
? `array[${output.items?.type.slice(0, 1).toLocaleLowerCase()}${output.items?.type.slice(1)}]`
: `${dataType.slice(0, 1).toLocaleLowerCase()}${dataType.slice(1)}`
const schemaType = getMatchedSchemaType?.(output, schemaTypeDefinitions)
if (type === 'object' && schemaType === 'file')
type = 'file'
dynamicOutputSchema.push({
variable: outputKey,
type,
description: output.description,
schemaType,
children: output.type === 'object' ? {
schema: {
type: 'object',
properties: output.properties,
},
} : undefined,
})
})
}
return [
...COMMON_OUTPUT.map(item => ({ variable: item.name, type: item.type })),
...(
isLocalFile
? LOCAL_FILE_OUTPUT.map(item => ({ variable: item.name, type: item.type }))
: []
),
...ragVars,
...dynamicOutputSchema,
]
},
}
export default nodeDefault

View file

@ -0,0 +1,207 @@
import { useStoreApi } from 'reactflow'
import type { CustomRunFormProps, DataSourceNodeType } from '../types'
import { useEffect, useMemo, useRef } from 'react'
import { useNodeDataUpdate, useNodesSyncDraft } from '../../../hooks'
import { NodeRunningStatus } from '../../../types'
import { useInvalidLastRun } from '@/service/use-workflow'
import type { NodeRunResult } from '@/types/workflow'
import { fetchNodeInspectVars } from '@/service/workflow'
import { FlowType } from '@/types/common'
import { useDatasourceSingleRun } from '@/service/use-pipeline'
import { useDataSourceStore, useDataSourceStoreWithSelector } from '@/app/components/datasets/documents/create-from-pipeline/data-source/store'
import { DatasourceType } from '@/models/pipeline'
import { TransferMethod } from '@/types/app'
import { useShallow } from 'zustand/react/shallow'
const useBeforeRunForm = ({
nodeId,
flowId,
flowType,
payload,
setRunResult,
isPaused,
isRunAfterSingleRun,
setIsRunAfterSingleRun,
onSuccess,
appendNodeInspectVars,
}: CustomRunFormProps) => {
const store = useStoreApi()
const dataSourceStore = useDataSourceStore()
const isPausedRef = useRef(isPaused)
const { handleNodeDataUpdate } = useNodeDataUpdate()
const datasourceType = payload.provider_type as DatasourceType
const datasourceNodeData = payload as DataSourceNodeType
const {
localFileList,
onlineDocuments,
websitePages,
selectedFileIds,
} = useDataSourceStoreWithSelector(useShallow(state => ({
localFileList: state.localFileList,
onlineDocuments: state.onlineDocuments,
websitePages: state.websitePages,
selectedFileIds: state.selectedFileIds,
})))
const startRunBtnDisabled = useMemo(() => {
if (!datasourceNodeData) return false
if (datasourceType === DatasourceType.localFile)
return !localFileList.length || localFileList.some(file => !file.file.id)
if (datasourceType === DatasourceType.onlineDocument)
return !onlineDocuments.length
if (datasourceType === DatasourceType.websiteCrawl)
return !websitePages.length
if (datasourceType === DatasourceType.onlineDrive)
return !selectedFileIds.length
return false
}, [datasourceNodeData, datasourceType, localFileList, onlineDocuments.length, selectedFileIds.length, websitePages.length])
useEffect(() => {
isPausedRef.current = isPaused
}, [isPaused])
const runningStatus = payload._singleRunningStatus || NodeRunningStatus.NotStart
const setNodeRunning = () => {
handleNodeDataUpdate({
id: nodeId,
data: {
...payload,
_singleRunningStatus: NodeRunningStatus.Running,
},
})
}
const invalidLastRun = useInvalidLastRun(flowType, flowId, nodeId)
const updateRunResult = async (data: NodeRunResult) => {
const isPaused = isPausedRef.current
// The backend don't support pause the single run, so the frontend handle the pause state.
if (isPaused)
return
const canRunLastRun = !isRunAfterSingleRun || runningStatus === NodeRunningStatus.Succeeded
if (!canRunLastRun) {
setRunResult(data)
return
}
// run fail may also update the inspect vars when the node set the error default output.
const vars = await fetchNodeInspectVars(FlowType.ragPipeline, flowId, nodeId)
const { getNodes } = store.getState()
const nodes = getNodes()
appendNodeInspectVars(nodeId, vars, nodes)
if (data?.status === NodeRunningStatus.Succeeded)
onSuccess()
}
const { mutateAsync: handleDatasourceSingleRun, isPending } = useDatasourceSingleRun()
const handleRun = () => {
let datasourceInfo: Record<string, any> = {}
const { currentCredentialId: credentialId } = dataSourceStore.getState()
if (datasourceType === DatasourceType.localFile) {
const { localFileList } = dataSourceStore.getState()
const { id, name, type, size, extension, mime_type } = localFileList[0].file
const documentInfo = {
related_id: id,
name,
type,
size,
extension,
mime_type,
url: '',
transfer_method: TransferMethod.local_file,
}
datasourceInfo = documentInfo
}
if (datasourceType === DatasourceType.onlineDocument) {
const { onlineDocuments } = dataSourceStore.getState()
const { workspace_id, ...rest } = onlineDocuments[0]
const documentInfo = {
workspace_id,
page: rest,
credential_id: credentialId,
}
datasourceInfo = documentInfo
}
if (datasourceType === DatasourceType.websiteCrawl) {
const { websitePages } = dataSourceStore.getState()
datasourceInfo = {
...websitePages[0],
credential_id: credentialId,
}
}
if (datasourceType === DatasourceType.onlineDrive) {
const { bucket, onlineDriveFileList, selectedFileIds } = dataSourceStore.getState()
const file = onlineDriveFileList.find(file => file.id === selectedFileIds[0])
datasourceInfo = {
bucket,
id: file?.id,
type: file?.type,
credential_id: credentialId,
}
}
let hasError = false
handleDatasourceSingleRun({
pipeline_id: flowId,
start_node_id: nodeId,
start_node_title: datasourceNodeData.title,
datasource_type: datasourceType,
datasource_info: datasourceInfo,
}, {
onError: () => {
hasError = true
invalidLastRun()
if (isPausedRef.current)
return
handleNodeDataUpdate({
id: nodeId,
data: {
...payload,
_isSingleRun: false,
_singleRunningStatus: NodeRunningStatus.Failed,
},
})
},
onSettled: (data) => {
updateRunResult(data!)
if (!hasError && !isPausedRef.current) {
handleNodeDataUpdate({
id: nodeId,
data: {
...payload,
_isSingleRun: false,
_singleRunningStatus: NodeRunningStatus.Succeeded,
},
})
}
},
})
}
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
const handleRunWithSyncDraft = () => {
setNodeRunning()
setIsRunAfterSingleRun(true)
handleSyncWorkflowDraft(true, true, {
onSuccess() {
handleRun()
},
})
}
return {
isPending,
handleRunWithSyncDraft,
datasourceType,
datasourceNodeData,
startRunBtnDisabled,
}
}
export default useBeforeRunForm

View file

@ -0,0 +1,117 @@
import {
useCallback,
useEffect,
useMemo,
} from 'react'
import { useStoreApi } from 'reactflow'
import { useNodeDataUpdate } from '@/app/components/workflow/hooks'
import type {
DataSourceNodeType,
ToolVarInputs,
} from '../types'
export const useConfig = (id: string, dataSourceList?: any[]) => {
const store = useStoreApi()
const { handleNodeDataUpdateWithSyncDraft } = useNodeDataUpdate()
const getNodeData = useCallback(() => {
const { getNodes } = store.getState()
const nodes = getNodes()
return nodes.find(node => node.id === id)
}, [store, id])
const handleNodeDataUpdate = useCallback((data: Partial<DataSourceNodeType>) => {
handleNodeDataUpdateWithSyncDraft({
id,
data,
})
}, [id, handleNodeDataUpdateWithSyncDraft])
const handleLocalFileDataSourceInit = useCallback(() => {
const nodeData = getNodeData()
if (nodeData?.data._dataSourceStartToAdd && nodeData?.data.provider_type === 'local_file') {
handleNodeDataUpdate({
...nodeData.data,
_dataSourceStartToAdd: false,
})
}
}, [getNodeData, handleNodeDataUpdate])
useEffect(() => {
handleLocalFileDataSourceInit()
}, [handleLocalFileDataSourceInit])
const handleFileExtensionsChange = useCallback((fileExtensions: string[]) => {
const nodeData = getNodeData()
handleNodeDataUpdate({
...nodeData?.data,
fileExtensions,
})
}, [handleNodeDataUpdate, getNodeData])
const handleParametersChange = useCallback((datasource_parameters: ToolVarInputs) => {
const nodeData = getNodeData()
handleNodeDataUpdate({
...nodeData?.data,
datasource_parameters,
})
}, [handleNodeDataUpdate, getNodeData])
const outputSchema = useMemo(() => {
const nodeData = getNodeData()
if (!nodeData?.data || !dataSourceList) return []
const currentDataSource = dataSourceList.find((ds: any) => ds.plugin_id === nodeData.data.plugin_id)
const currentDataSourceItem = currentDataSource?.tools?.find((tool: any) => tool.name === nodeData.data.datasource_name)
const output_schema = currentDataSourceItem?.output_schema
const res: any[] = []
if (!output_schema || !output_schema.properties)
return res
Object.keys(output_schema.properties).forEach((outputKey) => {
const output = output_schema.properties[outputKey]
const type = output.type
if (type === 'object') {
res.push({
name: outputKey,
value: output,
})
}
else {
res.push({
name: outputKey,
type: output.type === 'array'
? `Array[${output.items?.type.slice(0, 1).toLocaleUpperCase()}${output.items?.type.slice(1)}]`
: `${output.type.slice(0, 1).toLocaleUpperCase()}${output.type.slice(1)}`,
description: output.description,
})
}
})
return res
}, [getNodeData, dataSourceList])
const hasObjectOutput = useMemo(() => {
const nodeData = getNodeData()
if (!nodeData?.data || !dataSourceList) return false
const currentDataSource = dataSourceList.find((ds: any) => ds.plugin_id === nodeData.data.plugin_id)
const currentDataSourceItem = currentDataSource?.tools?.find((tool: any) => tool.name === nodeData.data.datasource_name)
const output_schema = currentDataSourceItem?.output_schema
if (!output_schema || !output_schema.properties)
return false
const properties = output_schema.properties
return Object.keys(properties).some(key => properties[key].type === 'object')
}, [getNodeData, dataSourceList])
return {
handleFileExtensionsChange,
handleParametersChange,
outputSchema,
hasObjectOutput,
}
}

View file

@ -0,0 +1,12 @@
import type { FC } from 'react'
import { memo } from 'react'
import type { DataSourceNodeType } from './types'
import type { NodeProps } from '@/app/components/workflow/types'
const Node: FC<NodeProps<DataSourceNodeType>> = () => {
return (
<div>
</div>
)
}
export default memo(Node)

View file

@ -0,0 +1,164 @@
import type { FC } from 'react'
import {
useMemo,
} from 'react'
import { useTranslation } from 'react-i18next'
import { memo } from 'react'
import type { DataSourceNodeType } from './types'
import { DataSourceClassification } from './types'
import type { NodePanelProps } from '@/app/components/workflow/types'
import {
BoxGroupField,
} from '@/app/components/workflow/nodes/_base/components/layout'
import OutputVars, { VarItem } from '@/app/components/workflow/nodes/_base/components/output-vars'
import StructureOutputItem from '@/app/components/workflow/nodes/_base/components/variable/object-child-tree-panel/show'
import TagInput from '@/app/components/base/tag-input'
import { useNodesReadOnly } from '@/app/components/workflow/hooks'
import { useConfig } from './hooks/use-config'
import {
COMMON_OUTPUT,
LOCAL_FILE_OUTPUT,
} from './constants'
import { useStore } from '@/app/components/workflow/store'
import { toolParametersToFormSchemas } from '@/app/components/tools/utils/to-form-schema'
import ToolForm from '../tool/components/tool-form'
import { wrapStructuredVarItem } from '@/app/components/workflow/utils/tool'
import useMatchSchemaType, { getMatchedSchemaType } from '../_base/components/variable/use-match-schema-type'
const Panel: FC<NodePanelProps<DataSourceNodeType>> = ({ id, data }) => {
const { t } = useTranslation()
const { nodesReadOnly } = useNodesReadOnly()
const dataSourceList = useStore(s => s.dataSourceList)
const {
provider_type,
plugin_id,
fileExtensions = [],
datasource_parameters,
} = data
const {
handleFileExtensionsChange,
handleParametersChange,
outputSchema,
hasObjectOutput,
} = useConfig(id, dataSourceList)
const isLocalFile = provider_type === DataSourceClassification.localFile
const currentDataSource = dataSourceList?.find(ds => ds.plugin_id === plugin_id)
const currentDataSourceItem: any = currentDataSource?.tools?.find((tool: any) => tool.name === data.datasource_name)
const formSchemas = useMemo(() => {
return currentDataSourceItem ? toolParametersToFormSchemas(currentDataSourceItem.parameters) : []
}, [currentDataSourceItem])
const pipelineId = useStore(s => s.pipelineId)
const setShowInputFieldPanel = useStore(s => s.setShowInputFieldPanel)
const { schemaTypeDefinitions } = useMatchSchemaType()
return (
<div >
{
currentDataSource?.is_authorized && !isLocalFile && !!formSchemas?.length && (
<BoxGroupField
boxGroupProps={{
boxProps: { withBorderBottom: true },
}}
fieldProps={{
fieldTitleProps: {
title: t('workflow.nodes.tool.inputVars'),
},
supportCollapse: true,
}}
>
{formSchemas.length > 0 && (
<ToolForm
readOnly={nodesReadOnly}
nodeId={id}
schema={formSchemas as any}
value={datasource_parameters}
onChange={handleParametersChange}
currentProvider={currentDataSource}
currentTool={currentDataSourceItem}
showManageInputField={!!pipelineId}
onManageInputField={() => setShowInputFieldPanel?.(true)}
/>
)}
</BoxGroupField>
)
}
{
isLocalFile && (
<BoxGroupField
boxGroupProps={{
boxProps: { withBorderBottom: true },
}}
fieldProps={{
fieldTitleProps: {
title: t('workflow.nodes.dataSource.supportedFileFormats'),
},
}}
>
<div className='rounded-lg bg-components-input-bg-normal p-1 pt-0'>
<TagInput
items={fileExtensions}
onChange={handleFileExtensionsChange}
placeholder={t('workflow.nodes.dataSource.supportedFileFormatsPlaceholder')}
inputClassName='bg-transparent'
disableAdd={nodesReadOnly}
disableRemove={nodesReadOnly}
/>
</div>
</BoxGroupField>
)
}
<OutputVars>
{
COMMON_OUTPUT.map((item, index) => (
<VarItem
key={index}
name={item.name}
type={item.type}
description={item.description}
isIndent={hasObjectOutput}
/>
))
}
{
isLocalFile && LOCAL_FILE_OUTPUT.map((item, index) => (
<VarItem
key={index}
name={item.name}
type={item.type}
description={item.description}
subItems={item.subItems.map(item => ({
name: item.name,
type: item.type,
description: item.description,
}))}
/>
))
}
{outputSchema.map((outputItem) => {
const schemaType = getMatchedSchemaType(outputItem.value, schemaTypeDefinitions)
return (
<div key={outputItem.name}>
{outputItem.value?.type === 'object' ? (
<StructureOutputItem
rootClassName='code-sm-semibold text-text-secondary'
payload={wrapStructuredVarItem(outputItem, schemaType)}
/>
) : (
<VarItem
name={outputItem.name}
// eslint-disable-next-line sonarjs/no-nested-template-literals
type={`${outputItem.type.toLocaleLowerCase()}${schemaType ? ` (${schemaType})` : ''}`}
description={outputItem.description}
isIndent={hasObjectOutput}
/>
)}
</div>
)
})}
</OutputVars>
</div>
)
}
export default memo(Panel)

View file

@ -0,0 +1,47 @@
import type { CommonNodeType, Node, ValueSelector } from '@/app/components/workflow/types'
import type { FlowType } from '@/types/common'
import type { NodeRunResult, VarInInspect } from '@/types/workflow'
import type { Dispatch, SetStateAction } from 'react'
export enum VarType {
variable = 'variable',
constant = 'constant',
mixed = 'mixed',
}
export enum DataSourceClassification {
localFile = 'local_file',
websiteCrawl = 'website_crawl',
onlineDocument = 'online_document',
onlineDrive = 'online_drive',
}
export type ToolVarInputs = Record<string, {
type: VarType
value?: string | ValueSelector | any
}>
export type DataSourceNodeType = CommonNodeType & {
fileExtensions?: string[]
plugin_id: string
provider_type: string
provider_name: string
datasource_name: string
datasource_label: string
datasource_parameters: ToolVarInputs
datasource_configurations: Record<string, any>
}
export type CustomRunFormProps = {
nodeId: string
flowId: string
flowType: FlowType
payload: CommonNodeType
setRunResult: Dispatch<SetStateAction<NodeRunResult | null>>
setIsRunAfterSingleRun: Dispatch<SetStateAction<boolean>>
isPaused: boolean
isRunAfterSingleRun: boolean
onSuccess: () => void
onCancel: () => void
appendNodeInspectVars: (nodeId: string, vars: VarInInspect[], nodes: Node[]) => void
}

View file

@ -0,0 +1,11 @@
import { PipelineInputVarType } from '@/models/pipeline'
import { VarType } from '@/app/components/workflow/types'
export const inputVarTypeToVarType = (type: PipelineInputVarType): VarType => {
return ({
[PipelineInputVarType.number]: VarType.number,
[PipelineInputVarType.singleFile]: VarType.file,
[PipelineInputVarType.multiFiles]: VarType.arrayFile,
[PipelineInputVarType.checkbox]: VarType.boolean,
} as any)[type] || VarType.string
}

View file

@ -1,24 +1,22 @@
import { BlockEnum } from '../../types'
import type { NodeDefault } from '../../types'
import type { DocExtractorNodeType } from './types'
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/blocks'
import { genNodeMetaData } from '@/app/components/workflow/utils'
import { BlockEnum } from '@/app/components/workflow/types'
import { BlockClassificationEnum } from '@/app/components/workflow/block-selector/types'
const i18nPrefix = 'workflow.errorMsg'
const metaData = genNodeMetaData({
classification: BlockClassificationEnum.Transform,
sort: 4,
type: BlockEnum.DocExtractor,
helpLinkUri: 'doc-extractor',
})
const nodeDefault: NodeDefault<DocExtractorNodeType> = {
metaData,
defaultValue: {
variable_selector: [],
is_array_file: false,
},
getAvailablePrevNodes(isChatMode: boolean) {
const nodes = isChatMode
? ALL_CHAT_AVAILABLE_BLOCKS
: ALL_COMPLETION_AVAILABLE_BLOCKS.filter(type => type !== BlockEnum.End)
return nodes
},
getAvailableNextNodes(isChatMode: boolean) {
const nodes = isChatMode ? ALL_CHAT_AVAILABLE_BLOCKS : ALL_COMPLETION_AVAILABLE_BLOCKS
return nodes
},
checkValid(payload: DocExtractorNodeType, t: any) {
let errorMessages = ''
const { variable_selector: variable } = payload

View file

@ -1,21 +1,18 @@
import { BlockEnum } from '../../types'
import type { NodeDefault } from '../../types'
import type { EndNodeType } from './types'
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/blocks'
import { genNodeMetaData } from '@/app/components/workflow/utils'
import { BlockEnum } from '@/app/components/workflow/types'
const metaData = genNodeMetaData({
sort: 2.1,
type: BlockEnum.End,
isRequired: true,
})
const nodeDefault: NodeDefault<EndNodeType> = {
metaData,
defaultValue: {
outputs: [],
},
getAvailablePrevNodes(isChatMode: boolean) {
const nodes = isChatMode
? ALL_CHAT_AVAILABLE_BLOCKS
: ALL_COMPLETION_AVAILABLE_BLOCKS.filter(type => type !== BlockEnum.End)
return nodes
},
getAvailableNextNodes() {
return []
},
checkValid() {
return {
isValid: true,

View file

@ -1,13 +1,17 @@
import { BlockEnum } from '../../types'
import type { NodeDefault } from '../../types'
import { AuthorizationType, BodyType, Method } from './types'
import type { BodyPayload, HttpNodeType } from './types'
import {
ALL_CHAT_AVAILABLE_BLOCKS,
ALL_COMPLETION_AVAILABLE_BLOCKS,
} from '@/app/components/workflow/blocks'
import { genNodeMetaData } from '@/app/components/workflow/utils'
import { BlockEnum } from '@/app/components/workflow/types'
import { BlockClassificationEnum } from '@/app/components/workflow/block-selector/types'
const metaData = genNodeMetaData({
classification: BlockClassificationEnum.Utilities,
sort: 1,
type: BlockEnum.HttpRequest,
})
const nodeDefault: NodeDefault<HttpNodeType> = {
metaData,
defaultValue: {
variables: [],
method: Method.get,
@ -34,16 +38,6 @@ const nodeDefault: NodeDefault<HttpNodeType> = {
retry_interval: 100,
},
},
getAvailablePrevNodes(isChatMode: boolean) {
const nodes = isChatMode
? ALL_CHAT_AVAILABLE_BLOCKS
: ALL_COMPLETION_AVAILABLE_BLOCKS.filter(type => type !== BlockEnum.End)
return nodes
},
getAvailableNextNodes(isChatMode: boolean) {
const nodes = isChatMode ? ALL_CHAT_AVAILABLE_BLOCKS : ALL_COMPLETION_AVAILABLE_BLOCKS
return nodes
},
checkValid(payload: HttpNodeType, t: any) {
let errorMessages = ''

View file

@ -23,6 +23,8 @@ const ConditionInput = ({
}: ConditionInputProps) => {
const { t } = useTranslation()
const controlPromptEditorRerenderKey = useStore(s => s.controlPromptEditorRerenderKey)
const pipelineId = useStore(s => s.pipelineId)
const setShowInputFieldPanel = useStore(s => s.setShowInputFieldPanel)
return (
<PromptEditor
@ -49,6 +51,8 @@ const ConditionInput = ({
}
return acc
}, {} as any),
showManageInputField: !!pipelineId,
onManageInputField: () => setShowInputFieldPanel?.(true),
}}
onChange={onChange}
editable={!disabled}

View file

@ -41,6 +41,7 @@ import { Variable02 } from '@/app/components/base/icons/src/vender/solid/develop
import BoolValue from '@/app/components/workflow/panel/chat-variable-panel/components/bool-value'
import { getVarType } from '@/app/components/workflow/nodes/_base/components/variable/utils'
import { useIsChatMode } from '@/app/components/workflow/hooks/use-workflow'
import useMatchSchemaType from '../../../_base/components/variable/use-match-schema-type'
const optionNameI18NPrefix = 'workflow.nodes.ifElse.optionName'
type ConditionItemProps = {
@ -93,6 +94,11 @@ const ConditionItem = ({
const workflowStore = useWorkflowStore()
const {
setControlPromptEditorRerenderKey,
buildInTools,
customTools,
mcpTools,
workflowTools,
dataSourceList,
} = workflowStore.getState()
const doUpdateCondition = useCallback((newCondition: Condition) => {
@ -203,6 +209,7 @@ const ConditionItem = ({
onRemoveCondition?.(caseId, condition.id)
}, [caseId, condition, conditionId, isSubVariableKey, onRemoveCondition, onRemoveSubVariableCondition])
const { getMatchedSchemaType } = useMatchSchemaType()
const handleVarChange = useCallback((valueSelector: ValueSelector, _varItem: Var) => {
const {
conversationVariables,
@ -212,6 +219,14 @@ const ConditionItem = ({
conversationVariables,
availableNodes,
isChatMode,
allPluginInfoList: {
buildInTools,
customTools,
mcpTools,
workflowTools,
dataSourceList: dataSourceList ?? [],
},
getMatchedSchemaType,
})
const newCondition = produce(condition, (draft) => {

View file

@ -1,10 +1,19 @@
import { BlockEnum, type NodeDefault, VarType } from '../../types'
import { type NodeDefault, VarType } from '../../types'
import { type IfElseNodeType, LogicalOperator } from './types'
import { isEmptyRelatedOperator } from './utils'
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/blocks'
import { genNodeMetaData } from '@/app/components/workflow/utils'
import { BlockEnum } from '@/app/components/workflow/types'
import { BlockClassificationEnum } from '@/app/components/workflow/block-selector/types'
const i18nPrefix = 'workflow.errorMsg'
const metaData = genNodeMetaData({
classification: BlockClassificationEnum.Logic,
sort: 1,
type: BlockEnum.IfElse,
helpLinkUri: 'ifelse',
})
const nodeDefault: NodeDefault<IfElseNodeType> = {
metaData,
defaultValue: {
_targetBranches: [
{
@ -24,16 +33,6 @@ const nodeDefault: NodeDefault<IfElseNodeType> = {
},
],
},
getAvailablePrevNodes(isChatMode: boolean) {
const nodes = isChatMode
? ALL_CHAT_AVAILABLE_BLOCKS
: ALL_COMPLETION_AVAILABLE_BLOCKS.filter(type => type !== BlockEnum.End)
return nodes
},
getAvailableNextNodes(isChatMode: boolean) {
const nodes = isChatMode ? ALL_CHAT_AVAILABLE_BLOCKS : ALL_COMPLETION_AVAILABLE_BLOCKS
return nodes
},
checkValid(payload: IfElseNodeType, t: any) {
let errorMessages = ''
const { cases } = payload

View file

@ -1,16 +1,15 @@
import type { NodeDefault } from '../../types'
import type { IterationStartNodeType } from './types'
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/blocks'
import { genNodeMetaData } from '@/app/components/workflow/utils'
import { BlockEnum } from '@/app/components/workflow/types'
const metaData = genNodeMetaData({
sort: -1,
type: BlockEnum.IterationStart,
})
const nodeDefault: NodeDefault<IterationStartNodeType> = {
metaData,
defaultValue: {},
getAvailablePrevNodes() {
return []
},
getAvailableNextNodes(isChatMode: boolean) {
const nodes = isChatMode ? ALL_CHAT_AVAILABLE_BLOCKS : ALL_COMPLETION_AVAILABLE_BLOCKS
return nodes
},
checkValid() {
return {
isValid: true,

View file

@ -1,13 +1,18 @@
import { BlockEnum, ErrorHandleMode } from '../../types'
import type { NodeDefault } from '../../types'
import type { IterationNodeType } from './types'
import {
ALL_CHAT_AVAILABLE_BLOCKS,
ALL_COMPLETION_AVAILABLE_BLOCKS,
} from '@/app/components/workflow/blocks'
import { genNodeMetaData } from '@/app/components/workflow/utils'
import { BlockClassificationEnum } from '@/app/components/workflow/block-selector/types'
const i18nPrefix = 'workflow'
const metaData = genNodeMetaData({
classification: BlockClassificationEnum.Logic,
sort: 2,
type: BlockEnum.Iteration,
isTypeFixed: true,
})
const nodeDefault: NodeDefault<IterationNodeType> = {
metaData,
defaultValue: {
start_node_id: '',
iterator_selector: [],
@ -18,20 +23,6 @@ const nodeDefault: NodeDefault<IterationNodeType> = {
parallel_nums: 10,
error_handle_mode: ErrorHandleMode.Terminated,
},
getAvailablePrevNodes(isChatMode: boolean) {
const nodes = isChatMode
? ALL_CHAT_AVAILABLE_BLOCKS
: ALL_COMPLETION_AVAILABLE_BLOCKS.filter(
type => type !== BlockEnum.End,
)
return nodes
},
getAvailableNextNodes(isChatMode: boolean) {
const nodes = isChatMode
? ALL_CHAT_AVAILABLE_BLOCKS
: ALL_COMPLETION_AVAILABLE_BLOCKS
return nodes
},
checkValid(payload: IterationNodeType, t: any) {
let errorMessages = ''

View file

@ -14,6 +14,7 @@ import type { VarType as VarKindType } from '@/app/components/workflow/nodes/too
import type { Item } from '@/app/components/base/select'
import useInspectVarsCrud from '../../hooks/use-inspect-vars-crud'
import { isEqual } from 'lodash-es'
import { useStore } from '../../store'
const useConfig = (id: string, payload: IterationNodeType) => {
const {
@ -39,7 +40,19 @@ const useConfig = (id: string, payload: IterationNodeType) => {
// output
const { getIterationNodeChildren } = useWorkflow()
const iterationChildrenNodes = getIterationNodeChildren(id)
const childrenNodeVars = toNodeOutputVars(iterationChildrenNodes, isChatMode)
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 childrenNodeVars = toNodeOutputVars(iterationChildrenNodes, isChatMode, undefined, [], [], [], allPluginInfoList)
const handleOutputVarChange = useCallback((output: ValueSelector | string, _varKindType: VarKindType, varInfo?: Var) => {
if (isEqual(inputs.output_selector, output as ValueSelector))

View file

@ -13,13 +13,14 @@ import {
} from '../../utils'
import {
ITERATION_PADDING,
NODES_INITIAL_DATA,
} from '../../constants'
import { CUSTOM_ITERATION_START_NODE } from '../iteration-start/constants'
import { useNodesMetaData } from '@/app/components/workflow/hooks'
export const useNodeIterationInteractions = () => {
const { t } = useTranslation()
const store = useStoreApi()
const { nodesMap: nodesMetaDataMap } = useNodesMetaData()
const handleNodeIterationRerender = useCallback((nodeId: string) => {
const {
@ -128,7 +129,7 @@ export const useNodeIterationInteractions = () => {
const { newNode } = generateNewNode({
type: getNodeCustomTypeByNodeDataType(childNodeType),
data: {
...NODES_INITIAL_DATA[childNodeType],
...nodesMetaDataMap![childNodeType].defaultValue,
...child.data,
selected: false,
_isBundled: false,
@ -136,6 +137,7 @@ export const useNodeIterationInteractions = () => {
_connectedTargetHandleIds: [],
title: nodesWithSameType.length > 0 ? `${t(`workflow.blocks.${childNodeType}`)} ${childNodeTypeCount[childNodeType]}` : t(`workflow.blocks.${childNodeType}`),
iteration_id: newNodeId,
type: childNodeType,
},
position: child.position,
positionAbsolute: child.positionAbsolute,

View file

@ -0,0 +1,71 @@
import { useTranslation } from 'react-i18next'
import {
GeneralChunk,
ParentChildChunk,
QuestionAndAnswer,
} from '@/app/components/base/icons/src/vender/knowledge'
import cn from '@/utils/classnames'
import { ChunkStructureEnum } from '../../types'
import type { Option } from './type'
export const useChunkStructure = () => {
const { t } = useTranslation()
const GeneralOption: Option = {
id: ChunkStructureEnum.general,
icon: (isActive: boolean) => (
<GeneralChunk
className={cn(
'h-[18px] w-[18px] text-text-tertiary group-hover:text-util-colors-indigo-indigo-600',
isActive && 'text-util-colors-indigo-indigo-600',
)} />
),
title: t('datasetCreation.stepTwo.general'),
description: t('datasetCreation.stepTwo.generalTip'),
effectColor: 'blue',
}
const ParentChildOption: Option = {
id: ChunkStructureEnum.parent_child,
icon: (isActive: boolean) => (
<ParentChildChunk
className={cn(
'h-[18px] w-[18px] text-text-tertiary group-hover:text-util-colors-blue-light-blue-light-500',
isActive && 'text-util-colors-blue-light-blue-light-500',
)}
/>
),
title: t('datasetCreation.stepTwo.parentChild'),
description: t('datasetCreation.stepTwo.parentChildTip'),
effectColor: 'blue-light',
}
const QuestionAnswerOption: Option = {
id: ChunkStructureEnum.question_answer,
icon: (isActive: boolean) => (
<QuestionAndAnswer
className={cn(
'h-[18px] w-[18px] text-text-tertiary group-hover:text-util-colors-teal-teal-600',
isActive && 'text-util-colors-teal-teal-600',
)}
/>
),
title: 'Q&A',
description: t('datasetCreation.stepTwo.qaTip'),
effectColor: 'teal',
}
const optionMap: Record<ChunkStructureEnum, Option> = {
[ChunkStructureEnum.general]: GeneralOption,
[ChunkStructureEnum.parent_child]: ParentChildOption,
[ChunkStructureEnum.question_answer]: QuestionAnswerOption,
}
const options = [
GeneralOption,
ParentChildOption,
QuestionAnswerOption,
]
return {
options,
optionMap,
}
}

View file

@ -0,0 +1,78 @@
import { memo } from 'react'
import { useTranslation } from 'react-i18next'
import { RiAddLine } from '@remixicon/react'
import { Field } from '@/app/components/workflow/nodes/_base/components/layout'
import type { ChunkStructureEnum } from '../../types'
import OptionCard from '../option-card'
import Selector from './selector'
import { useChunkStructure } from './hooks'
import Button from '@/app/components/base/button'
import Instruction from './instruction'
type ChunkStructureProps = {
chunkStructure?: ChunkStructureEnum
onChunkStructureChange: (value: ChunkStructureEnum) => void
readonly?: boolean
}
const ChunkStructure = ({
chunkStructure,
onChunkStructureChange,
readonly = false,
}: ChunkStructureProps) => {
const { t } = useTranslation()
const {
options,
optionMap,
} = useChunkStructure()
return (
<Field
fieldTitleProps={{
title: t('workflow.nodes.knowledgeBase.chunkStructure'),
tooltip: t('workflow.nodes.knowledgeBase.chunkStructure'),
operation: chunkStructure && (
<Selector
options={options}
value={chunkStructure}
onChange={onChunkStructureChange}
readonly={readonly}
/>
),
}}
>
{
chunkStructure && (
<OptionCard
{...optionMap[chunkStructure]}
selectedId={chunkStructure}
enableSelect={false}
enableHighlightBorder={false}
/>
)
}
{
!chunkStructure && (
<>
<Selector
options={options}
onChange={onChunkStructureChange}
readonly={readonly}
trigger={(
<Button
className='w-full'
variant='secondary-accent'
>
<RiAddLine className='mr-1 h-4 w-4' />
{t('workflow.nodes.knowledgeBase.chooseChunkStructure')}
</Button>
)}
/>
<Instruction className='mt-2' />
</>
)
}
</Field>
)
}
export default memo(ChunkStructure)

View file

@ -0,0 +1,47 @@
import React from 'react'
import { AddChunks } from '@/app/components/base/icons/src/vender/knowledge'
import Line from './line'
import cn from '@/utils/classnames'
import { useTranslation } from 'react-i18next'
import { useDocLink } from '@/context/i18n'
type InstructionProps = {
className?: string
}
const Instruction = ({
className,
}: InstructionProps) => {
const { t } = useTranslation()
const docLink = useDocLink()
return (
<div className={cn('flex flex-col gap-y-2 overflow-hidden rounded-[10px] bg-workflow-process-bg p-4', className)}>
<div className='relative flex size-10 items-center justify-center rounded-[10px] border-[0.5px] border-components-card-border bg-components-card-bg shadow-lg backdrop-blur-[5px]'>
<AddChunks className='size-5 text-text-accent' />
<Line className='absolute -left-px bottom-[-76px]' type='vertical' />
<Line className='absolute -right-px bottom-[-76px]' type='vertical' />
<Line className='absolute -top-px right-[-184px]' type='horizontal' />
<Line className='absolute -bottom-px right-[-184px]' type='horizontal' />
</div>
<div className='flex flex-col gap-y-1'>
<div className='system-sm-medium text-text-secondary'>
{t('workflow.nodes.knowledgeBase.chunkStructureTip.title')}
</div>
<div className='system-xs-regular'>
<p className='text-text-tertiary'>{t('workflow.nodes.knowledgeBase.chunkStructureTip.message')}</p>
<a
href={docLink('/guides/knowledge-base/create-knowledge-and-upload-documents/chunking-and-cleaning-text')}
target='_blank'
rel='noopener noreferrer'
className='text-text-accent'
>
{t('workflow.nodes.knowledgeBase.chunkStructureTip.learnMore')}
</a>
</div>
</div>
</div>
)
}
export default React.memo(Instruction)

View file

@ -0,0 +1,41 @@
import React from 'react'
type LineProps = {
type?: 'vertical' | 'horizontal'
className?: string
}
const Line = ({
type = 'vertical',
className,
}: LineProps) => {
if (type === 'vertical') {
return (
<svg xmlns='http://www.w3.org/2000/svg' width='2' height='132' viewBox='0 0 2 132' fill='none' className={className}>
<path d='M1 0L1 132' stroke='url(#paint0_linear_10882_18766)' />
<defs>
<linearGradient id='paint0_linear_10882_18766' x1='-7.99584' y1='132' x2='-7.96108' y2='6.4974e-07' gradientUnits='userSpaceOnUse'>
<stop stopColor='var(--color-background-gradient-mask-transparent)' />
<stop offset='0.877606' stopColor='var(--color-divider-subtle)' />
<stop offset='1' stopColor='var(--color-background-gradient-mask-transparent)' />
</linearGradient>
</defs>
</svg>
)
}
return (
<svg xmlns='http://www.w3.org/2000/svg' width='240' height='2' viewBox='0 0 240 2' fill='none' className={className}>
<path d='M0 1H240' stroke='url(#paint0_linear_10882_18763)' />
<defs>
<linearGradient id='paint0_linear_10882_18763' x1='240' y1='9.99584' x2='3.95539e-05' y2='9.88094' gradientUnits='userSpaceOnUse'>
<stop stopColor='var(--color-background-gradient-mask-transparent)' />
<stop offset='0.9031' stopColor='var(--color-divider-subtle)' />
<stop offset='1' stopColor='var(--color-background-gradient-mask-transparent)' />
</linearGradient>
</defs>
</svg>
)
}
export default React.memo(Line)

View file

@ -0,0 +1,93 @@
import type { ReactNode } from 'react'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import Button from '@/app/components/base/button'
import type { ChunkStructureEnum } from '../../types'
import OptionCard from '../option-card'
import type { Option } from './type'
type SelectorProps = {
options: Option[]
value?: ChunkStructureEnum
onChange: (key: ChunkStructureEnum) => void
readonly?: boolean
trigger?: ReactNode
}
const Selector = ({
options,
value,
onChange,
readonly,
trigger,
}: SelectorProps) => {
const { t } = useTranslation()
const [open, setOpen] = useState(false)
const handleSelect = useCallback((optionId: ChunkStructureEnum) => {
onChange(optionId)
setOpen(false)
}, [onChange])
return (
<PortalToFollowElem
placement='bottom-end'
offset={{
mainAxis: 0,
crossAxis: -8,
}}
open={open}
onOpenChange={setOpen}
>
<PortalToFollowElemTrigger
asChild
onClick={() => {
if (readonly)
return
setOpen(!open)
}}
>
{
trigger || (
<Button
size='small'
variant='ghost-accent'
>
{t('workflow.panel.change')}
</Button>
)
}
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-10'>
<div className='w-[404px] rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-xl backdrop-blur-[5px]'>
<div className='system-sm-semibold px-3 pt-3.5 text-text-primary'>
{t('workflow.nodes.knowledgeBase.changeChunkStructure')}
</div>
<div className='space-y-1 p-3 pt-2'>
{
options.map(option => (
<OptionCard
key={option.id}
id={option.id}
selectedId={value}
icon={option.icon}
title={option.title}
description={option.description}
readonly={readonly}
onClick={handleSelect}
effectColor={option.effectColor}
></OptionCard>
))
}
</div>
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
)
}
export default Selector

View file

@ -0,0 +1,10 @@
import type { ReactNode } from 'react'
import type { ChunkStructureEnum } from '../../types'
export type Option = {
id: ChunkStructureEnum
icon: ReactNode | ((isActive: boolean) => ReactNode)
title: string
description: string
effectColor?: string
}

View file

@ -0,0 +1,64 @@
import {
memo,
useCallback,
useMemo,
} from 'react'
import { useTranslation } from 'react-i18next'
import { Field } from '@/app/components/workflow/nodes/_base/components/layout'
import ModelSelector from '@/app/components/header/account-setting/model-provider-page/model-selector'
import { useModelList } from '@/app/components/header/account-setting/model-provider-page/hooks'
import type { DefaultModel } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
type EmbeddingModelProps = {
embeddingModel?: string
embeddingModelProvider?: string
onEmbeddingModelChange?: (model: {
embeddingModel: string
embeddingModelProvider: string
}) => void
readonly?: boolean
}
const EmbeddingModel = ({
embeddingModel,
embeddingModelProvider,
onEmbeddingModelChange,
readonly = false,
}: EmbeddingModelProps) => {
const { t } = useTranslation()
const {
data: embeddingModelList,
} = useModelList(ModelTypeEnum.textEmbedding)
const embeddingModelConfig = useMemo(() => {
if (!embeddingModel || !embeddingModelProvider)
return undefined
return {
providerName: embeddingModelProvider,
modelName: embeddingModel,
}
}, [embeddingModel, embeddingModelProvider])
const handleEmbeddingModelChange = useCallback((model: DefaultModel) => {
onEmbeddingModelChange?.({
embeddingModelProvider: model.provider,
embeddingModel: model.model,
})
}, [onEmbeddingModelChange])
return (
<Field
fieldTitleProps={{
title: t('datasetSettings.form.embeddingModel'),
}}
>
<ModelSelector
defaultModel={embeddingModelConfig && { provider: embeddingModelConfig.providerName, model: embeddingModelConfig.modelName }}
modelList={embeddingModelList}
onSelect={handleEmbeddingModelChange}
readonly={readonly}
/>
</Field>
)
}
export default memo(EmbeddingModel)

View file

@ -0,0 +1,128 @@
import {
memo,
useCallback,
} from 'react'
import { useTranslation } from 'react-i18next'
import { RiQuestionLine } from '@remixicon/react'
import {
Economic,
HighQuality,
} from '@/app/components/base/icons/src/vender/knowledge'
import Tooltip from '@/app/components/base/tooltip'
import Slider from '@/app/components/base/slider'
import Input from '@/app/components/base/input'
import { Field } from '@/app/components/workflow/nodes/_base/components/layout'
import OptionCard from './option-card'
import cn from '@/utils/classnames'
import {
ChunkStructureEnum,
IndexMethodEnum,
} from '../types'
type IndexMethodProps = {
chunkStructure: ChunkStructureEnum
indexMethod?: IndexMethodEnum
onIndexMethodChange: (value: IndexMethodEnum) => void
keywordNumber: number
onKeywordNumberChange: (value: number) => void
readonly?: boolean
}
const IndexMethod = ({
chunkStructure,
indexMethod,
onIndexMethodChange,
keywordNumber,
onKeywordNumberChange,
readonly = false,
}: IndexMethodProps) => {
const { t } = useTranslation()
const isHighQuality = indexMethod === IndexMethodEnum.QUALIFIED
const isEconomy = indexMethod === IndexMethodEnum.ECONOMICAL
const handleIndexMethodChange = useCallback((newIndexMethod: IndexMethodEnum) => {
onIndexMethodChange(newIndexMethod)
}, [onIndexMethodChange])
const handleInputChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
const value = Number(e.target.value)
if (!Number.isNaN(value))
onKeywordNumberChange(value)
}, [onKeywordNumberChange])
return (
<Field
fieldTitleProps={{
title: t('datasetCreation.stepTwo.indexMode'),
}}
>
<div className='space-y-1'>
<OptionCard<IndexMethodEnum>
id={IndexMethodEnum.QUALIFIED}
selectedId={indexMethod}
icon={
<HighQuality
className={cn(
'h-[15px] w-[15px] text-text-tertiary group-hover:text-util-colors-orange-orange-500',
isHighQuality && 'text-util-colors-orange-orange-500',
)}
/>
}
title={t('datasetCreation.stepTwo.qualified')}
description={t('datasetSettings.form.indexMethodHighQualityTip')}
onClick={handleIndexMethodChange}
isRecommended
effectColor='orange'
></OptionCard>
{
chunkStructure === ChunkStructureEnum.general && (
<OptionCard
id={IndexMethodEnum.ECONOMICAL}
selectedId={indexMethod}
icon={
<Economic
className={cn(
'h-[15px] w-[15px] text-text-tertiary group-hover:text-util-colors-indigo-indigo-500',
isEconomy && 'text-util-colors-indigo-indigo-500',
)}
/>
}
title={t('datasetSettings.form.indexMethodEconomy')}
description={t('datasetSettings.form.indexMethodEconomyTip', { count: keywordNumber })}
onClick={handleIndexMethodChange}
effectColor='blue'
>
<div className='flex items-center'>
<div className='flex grow items-center'>
<div className='system-xs-medium truncate text-text-secondary'>
{t('datasetSettings.form.numberOfKeywords')}
</div>
<Tooltip
popupContent='number of keywords'
>
<RiQuestionLine className='ml-0.5 h-3.5 w-3.5 text-text-quaternary' />
</Tooltip>
</div>
<Slider
disabled={readonly}
className='mr-3 w-24 shrink-0'
value={keywordNumber}
onChange={onKeywordNumberChange}
/>
<Input
disabled={readonly}
className='shrink-0'
wrapperClassName='shrink-0 w-[72px]'
type='number'
value={keywordNumber}
onChange={handleInputChange}
/>
</div>
</OptionCard>
)
}
</div>
</Field>
)
}
export default memo(IndexMethod)

View file

@ -0,0 +1,146 @@
import type { ReactNode } from 'react'
import {
memo,
useMemo,
} from 'react'
import { useTranslation } from 'react-i18next'
import cn from '@/utils/classnames'
import Badge from '@/app/components/base/badge'
import {
OptionCardEffectBlue,
OptionCardEffectBlueLight,
OptionCardEffectOrange,
OptionCardEffectPurple,
OptionCardEffectTeal,
} from '@/app/components/base/icons/src/public/knowledge'
import { ArrowShape } from '@/app/components/base/icons/src/vender/knowledge'
const HEADER_EFFECT_MAP: Record<string, ReactNode> = {
'blue': <OptionCardEffectBlue />,
'blue-light': <OptionCardEffectBlueLight />,
'orange': <OptionCardEffectOrange />,
'purple': <OptionCardEffectPurple />,
'teal': <OptionCardEffectTeal />,
}
type OptionCardProps<T> = {
id?: T
selectedId?: T
enableSelect?: boolean
enableHighlightBorder?: boolean
enableRadio?: boolean
wrapperClassName?: string | ((isActive: boolean) => string)
className?: string | ((isActive: boolean) => string)
icon?: ReactNode | ((isActive: boolean) => ReactNode)
title: string
description?: string
isRecommended?: boolean
children?: ReactNode
effectColor?: string
onClick?: (id: T) => void
readonly?: boolean
}
const OptionCard = memo(({
id,
selectedId,
enableSelect = true,
enableHighlightBorder = true,
enableRadio,
wrapperClassName,
className,
icon,
title,
description,
isRecommended,
children,
effectColor,
onClick,
readonly,
}) => {
const { t } = useTranslation()
const isActive = useMemo(() => {
return id === selectedId
}, [id, selectedId])
const effectElement = useMemo(() => {
if (effectColor) {
return (
<div className={cn(
'absolute left-[-2px] top-[-2px] hidden h-14 w-14 rounded-full',
'group-hover:block',
isActive && 'block',
)}>
{HEADER_EFFECT_MAP[effectColor]}
</div>
)
}
return null
}, [effectColor, isActive])
return (
<div
className={cn(
'group overflow-hidden rounded-xl border border-components-option-card-option-border bg-components-option-card-option-bg',
isActive && enableHighlightBorder && 'border-[1.5px] border-components-option-card-option-selected-border',
enableSelect && 'cursor-pointer hover:shadow-xs',
readonly && 'cursor-not-allowed',
wrapperClassName && (typeof wrapperClassName === 'function' ? wrapperClassName(isActive) : wrapperClassName),
)}
onClick={() => !readonly && enableSelect && id && onClick?.(id)}
>
<div className={cn(
'relative flex rounded-t-xl p-2',
className && (typeof className === 'function' ? className(isActive) : className),
)}>
{effectElement}
{
icon && (
<div className='mr-1 flex h-[18px] w-[18px] shrink-0 items-center justify-center'>
{typeof icon === 'function' ? icon(isActive) : icon}
</div>
)
}
<div className='grow py-1 pt-[1px]'>
<div className='flex items-center'>
<div className='system-sm-medium flex grow items-center text-text-secondary'>
{title}
{
isRecommended && (
<Badge className='ml-1 h-4 border-text-accent-secondary text-text-accent-secondary'>
{t('datasetCreation.stepTwo.recommend')}
</Badge>
)
}
</div>
{
enableRadio && (
<div className={cn(
'ml-2 h-4 w-4 shrink-0 rounded-full border border-components-radio-border bg-components-radio-bg',
isActive && 'border-[5px] border-components-radio-border-checked',
)}>
</div>
)
}
</div>
{
description && (
<div className='system-xs-regular mt-1 text-text-tertiary'>
{description}
</div>
)
}
</div>
</div>
{
children && isActive && (
<div className='relative rounded-b-xl bg-components-panel-bg p-3'>
<ArrowShape className='absolute left-[14px] top-[-11px] h-4 w-4 text-components-panel-bg' />
{children}
</div>
)
}
</div>
)
}) as <T>(props: OptionCardProps<T>) => React.ReactElement
export default OptionCard

View file

@ -0,0 +1,93 @@
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import {
FullTextSearch,
HybridSearch,
VectorSearch,
} from '@/app/components/base/icons/src/vender/knowledge'
import {
HybridSearchModeEnum,
IndexMethodEnum,
RetrievalSearchMethodEnum,
} from '../../types'
import type {
HybridSearchModeOption,
Option,
} from './type'
export const useRetrievalSetting = (indexMethod?: IndexMethodEnum) => {
const { t } = useTranslation()
const VectorSearchOption: Option = useMemo(() => {
return {
id: RetrievalSearchMethodEnum.semantic,
icon: VectorSearch as any,
title: t('dataset.retrieval.semantic_search.title'),
description: t('dataset.retrieval.semantic_search.description'),
effectColor: 'purple',
}
}, [t])
const FullTextSearchOption: Option = useMemo(() => {
return {
id: RetrievalSearchMethodEnum.fullText,
icon: FullTextSearch as any,
title: t('dataset.retrieval.full_text_search.title'),
description: t('dataset.retrieval.full_text_search.description'),
effectColor: 'purple',
}
}, [t])
const HybridSearchOption: Option = useMemo(() => {
return {
id: RetrievalSearchMethodEnum.hybrid,
icon: HybridSearch as any,
title: t('dataset.retrieval.hybrid_search.title'),
description: t('dataset.retrieval.hybrid_search.description'),
effectColor: 'purple',
}
}, [t])
const InvertedIndexOption: Option = useMemo(() => {
return {
id: RetrievalSearchMethodEnum.keywordSearch,
icon: HybridSearch as any,
title: t('dataset.retrieval.keyword_search.title'),
description: t('dataset.retrieval.keyword_search.description'),
effectColor: 'purple',
}
}, [t])
const WeightedScoreModeOption: HybridSearchModeOption = useMemo(() => {
return {
id: HybridSearchModeEnum.WeightedScore,
title: t('dataset.weightedScore.title'),
description: t('dataset.weightedScore.description'),
}
}, [t])
const RerankModelModeOption: HybridSearchModeOption = useMemo(() => {
return {
id: HybridSearchModeEnum.RerankingModel,
title: t('common.modelProvider.rerankModel.key'),
description: t('common.modelProvider.rerankModel.tip'),
}
}, [t])
return useMemo(() => ({
options: indexMethod === IndexMethodEnum.ECONOMICAL ? [
InvertedIndexOption,
] : [
VectorSearchOption,
FullTextSearchOption,
HybridSearchOption,
],
hybridSearchModeOptions: [
WeightedScoreModeOption,
RerankModelModeOption,
],
}), [
VectorSearchOption,
FullTextSearchOption,
HybridSearchOption,
InvertedIndexOption,
indexMethod,
WeightedScoreModeOption,
RerankModelModeOption,
])
}

View file

@ -0,0 +1,102 @@
import {
memo,
} from 'react'
import { useTranslation } from 'react-i18next'
import { Field } from '@/app/components/workflow/nodes/_base/components/layout'
import type {
HybridSearchModeEnum,
RetrievalSearchMethodEnum,
} from '../../types'
import type {
IndexMethodEnum,
WeightedScore,
} from '../../types'
import { useRetrievalSetting } from './hooks'
import type { TopKAndScoreThresholdProps } from './top-k-and-score-threshold'
import type { RerankingModelSelectorProps } from './reranking-model-selector'
import SearchMethodOption from './search-method-option'
type RetrievalSettingProps = {
indexMethod?: IndexMethodEnum
readonly?: boolean
searchMethod?: RetrievalSearchMethodEnum
onRetrievalSearchMethodChange: (value: RetrievalSearchMethodEnum) => void
hybridSearchMode?: HybridSearchModeEnum
onHybridSearchModeChange: (value: HybridSearchModeEnum) => void
rerankingModelEnabled?: boolean
onRerankingModelEnabledChange?: (value: boolean) => void
weightedScore?: WeightedScore
onWeightedScoreChange: (value: { value: number[] }) => void
} & RerankingModelSelectorProps & TopKAndScoreThresholdProps
const RetrievalSetting = ({
indexMethod,
readonly,
searchMethod,
onRetrievalSearchMethodChange,
hybridSearchMode,
onHybridSearchModeChange,
weightedScore,
onWeightedScoreChange,
rerankingModelEnabled,
onRerankingModelEnabledChange,
rerankingModel,
onRerankingModelChange,
topK,
onTopKChange,
scoreThreshold,
onScoreThresholdChange,
isScoreThresholdEnabled,
onScoreThresholdEnabledChange,
}: RetrievalSettingProps) => {
const { t } = useTranslation()
const {
options,
hybridSearchModeOptions,
} = useRetrievalSetting(indexMethod)
return (
<Field
fieldTitleProps={{
title: t('datasetSettings.form.retrievalSetting.title'),
subTitle: (
<div className='body-xs-regular flex items-center text-text-tertiary'>
<a target='_blank' rel='noopener noreferrer' href='https://docs.dify.ai/guides/knowledge-base/create-knowledge-and-upload-documents#id-4-retrieval-settings' className='text-text-accent'>{t('datasetSettings.form.retrievalSetting.learnMore')}</a>
&nbsp;{t('workflow.nodes.knowledgeBase.aboutRetrieval')}
</div>
),
}}
>
<div className='space-y-1'>
{
options.map(option => (
<SearchMethodOption
key={option.id}
option={option}
hybridSearchModeOptions={hybridSearchModeOptions}
searchMethod={searchMethod}
onRetrievalSearchMethodChange={onRetrievalSearchMethodChange}
hybridSearchMode={hybridSearchMode}
onHybridSearchModeChange={onHybridSearchModeChange}
weightedScore={weightedScore}
onWeightedScoreChange={onWeightedScoreChange}
topK={topK}
onTopKChange={onTopKChange}
scoreThreshold={scoreThreshold}
onScoreThresholdChange={onScoreThresholdChange}
isScoreThresholdEnabled={isScoreThresholdEnabled}
onScoreThresholdEnabledChange={onScoreThresholdEnabledChange}
rerankingModelEnabled={rerankingModelEnabled}
onRerankingModelEnabledChange={onRerankingModelEnabledChange}
rerankingModel={rerankingModel}
onRerankingModelChange={onRerankingModelChange}
readonly={readonly}
/>
))
}
</div>
</Field>
)
}
export default memo(RetrievalSetting)

View file

@ -0,0 +1,51 @@
import {
memo,
useMemo,
} from 'react'
import ModelSelector from '@/app/components/header/account-setting/model-provider-page/model-selector'
import { useModelListAndDefaultModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
import type { DefaultModel } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import type { RerankingModel } from '../../types'
export type RerankingModelSelectorProps = {
rerankingModel?: RerankingModel
onRerankingModelChange?: (model: RerankingModel) => void
readonly?: boolean
}
const RerankingModelSelector = ({
rerankingModel,
onRerankingModelChange,
readonly = false,
}: RerankingModelSelectorProps) => {
const {
modelList: rerankModelList,
} = useModelListAndDefaultModel(ModelTypeEnum.rerank)
const rerankModel = useMemo(() => {
if (!rerankingModel)
return undefined
return {
providerName: rerankingModel.reranking_provider_name,
modelName: rerankingModel.reranking_model_name,
}
}, [rerankingModel])
const handleRerankingModelChange = (model: DefaultModel) => {
onRerankingModelChange?.({
reranking_provider_name: model.provider,
reranking_model_name: model.model,
})
}
return (
<ModelSelector
defaultModel={rerankModel && { provider: rerankModel.providerName, model: rerankModel.modelName }}
modelList={rerankModelList}
onSelect={handleRerankingModelChange}
readonly={readonly}
/>
)
}
export default memo(RerankingModelSelector)

View file

@ -0,0 +1,204 @@
import {
memo,
useCallback,
useMemo,
} from 'react'
import { useTranslation } from 'react-i18next'
import cn from '@/utils/classnames'
import WeightedScoreComponent from '@/app/components/app/configuration/dataset-config/params-config/weighted-score'
import { DEFAULT_WEIGHTED_SCORE } from '@/models/datasets'
import Switch from '@/app/components/base/switch'
import Tooltip from '@/app/components/base/tooltip'
import {
HybridSearchModeEnum,
RetrievalSearchMethodEnum,
} from '../../types'
import type {
WeightedScore,
} from '../../types'
import OptionCard from '../option-card'
import type {
HybridSearchModeOption,
Option,
} from './type'
import type { TopKAndScoreThresholdProps } from './top-k-and-score-threshold'
import TopKAndScoreThreshold from './top-k-and-score-threshold'
import type { RerankingModelSelectorProps } from './reranking-model-selector'
import RerankingModelSelector from './reranking-model-selector'
type SearchMethodOptionProps = {
readonly?: boolean
option: Option
hybridSearchModeOptions: HybridSearchModeOption[]
searchMethod?: RetrievalSearchMethodEnum
onRetrievalSearchMethodChange: (value: RetrievalSearchMethodEnum) => void
hybridSearchMode?: HybridSearchModeEnum
onHybridSearchModeChange: (value: HybridSearchModeEnum) => void
weightedScore?: WeightedScore
onWeightedScoreChange: (value: { value: number[] }) => void
rerankingModelEnabled?: boolean
onRerankingModelEnabledChange?: (value: boolean) => void
} & RerankingModelSelectorProps & TopKAndScoreThresholdProps
const SearchMethodOption = ({
readonly,
option,
hybridSearchModeOptions,
searchMethod,
onRetrievalSearchMethodChange,
hybridSearchMode,
onHybridSearchModeChange,
weightedScore,
onWeightedScoreChange,
rerankingModelEnabled,
onRerankingModelEnabledChange,
rerankingModel,
onRerankingModelChange,
topK,
onTopKChange,
scoreThreshold,
onScoreThresholdChange,
isScoreThresholdEnabled,
onScoreThresholdEnabledChange,
}: SearchMethodOptionProps) => {
const { t } = useTranslation()
const Icon = option.icon
const isHybridSearch = option.id === RetrievalSearchMethodEnum.hybrid
const isHybridSearchWeightedScoreMode = hybridSearchMode === HybridSearchModeEnum.WeightedScore
const weightedScoreValue = useMemo(() => {
const sematicWeightedScore = weightedScore?.vector_setting.vector_weight ?? DEFAULT_WEIGHTED_SCORE.other.semantic
const keywordWeightedScore = weightedScore?.keyword_setting.keyword_weight ?? DEFAULT_WEIGHTED_SCORE.other.keyword
const mergedValue = [sematicWeightedScore, keywordWeightedScore]
return {
value: mergedValue,
}
}, [weightedScore])
const icon = useCallback((isActive: boolean) => {
return (
<Icon
className={cn(
'h-[15px] w-[15px] text-text-tertiary group-hover:text-util-colors-purple-purple-600',
isActive && 'text-util-colors-purple-purple-600',
)}
/>
)
}, [Icon])
const hybridSearchModeWrapperClassName = useCallback((isActive: boolean) => {
return isActive ? 'border-[1.5px] bg-components-option-card-option-selected-bg' : ''
}, [])
const showRerankModelSelectorSwitch = useMemo(() => {
if (searchMethod === RetrievalSearchMethodEnum.semantic)
return true
if (searchMethod === RetrievalSearchMethodEnum.fullText)
return true
return false
}, [searchMethod])
const showRerankModelSelector = useMemo(() => {
if (searchMethod === RetrievalSearchMethodEnum.semantic)
return true
if (searchMethod === RetrievalSearchMethodEnum.fullText)
return true
if (searchMethod === RetrievalSearchMethodEnum.hybrid && hybridSearchMode !== HybridSearchModeEnum.WeightedScore)
return true
return false
}, [hybridSearchMode, searchMethod])
return (
<OptionCard
key={option.id}
id={option.id}
selectedId={searchMethod}
icon={icon}
title={option.title}
description={option.description}
effectColor={option.effectColor}
isRecommended={option.id === RetrievalSearchMethodEnum.hybrid}
onClick={onRetrievalSearchMethodChange}
readonly={readonly}
>
<div className='space-y-3'>
{
isHybridSearch && (
<div className='space-y-1'>
{
hybridSearchModeOptions.map(hybridOption => (
<OptionCard
key={hybridOption.id}
id={hybridOption.id}
selectedId={hybridSearchMode}
enableHighlightBorder={false}
enableRadio
wrapperClassName={hybridSearchModeWrapperClassName}
className='p-3'
title={hybridOption.title}
description={hybridOption.description}
onClick={onHybridSearchModeChange}
readonly={readonly}
/>
))
}
</div>
)
}
{
isHybridSearch && isHybridSearchWeightedScoreMode && (
<WeightedScoreComponent
value={weightedScoreValue}
onChange={onWeightedScoreChange}
readonly={readonly}
/>
)
}
{
showRerankModelSelector && (
<div>
{
showRerankModelSelectorSwitch && (
<div className='system-sm-semibold mb-1 flex items-center text-text-secondary'>
<Switch
className='mr-1'
defaultValue={rerankingModelEnabled}
onChange={onRerankingModelEnabledChange}
disabled={readonly}
/>
{t('common.modelProvider.rerankModel.key')}
<Tooltip
triggerClassName='ml-0.5 shrink-0 w-3.5 h-3.5'
popupContent={t('common.modelProvider.rerankModel.tip')}
/>
</div>
)
}
<RerankingModelSelector
rerankingModel={rerankingModel}
onRerankingModelChange={onRerankingModelChange}
readonly={readonly}
/>
</div>
)
}
<TopKAndScoreThreshold
topK={topK}
onTopKChange={onTopKChange}
scoreThreshold={scoreThreshold}
onScoreThresholdChange={onScoreThresholdChange}
isScoreThresholdEnabled={isScoreThresholdEnabled}
onScoreThresholdEnabledChange={onScoreThresholdEnabledChange}
readonly={readonly}
hiddenScoreThreshold={searchMethod === RetrievalSearchMethodEnum.keywordSearch}
/>
</div>
</OptionCard>
)
}
export default memo(SearchMethodOption)

View file

@ -0,0 +1,90 @@
import { memo } from 'react'
import { useTranslation } from 'react-i18next'
import Tooltip from '@/app/components/base/tooltip'
import Input from '@/app/components/base/input'
import Switch from '@/app/components/base/switch'
export type TopKAndScoreThresholdProps = {
topK: number
onTopKChange: (value: number) => void
scoreThreshold?: number
onScoreThresholdChange?: (value: number) => void
isScoreThresholdEnabled?: boolean
onScoreThresholdEnabledChange?: (value: boolean) => void
readonly?: boolean
hiddenScoreThreshold?: boolean
}
const TopKAndScoreThreshold = ({
topK,
onTopKChange,
scoreThreshold,
onScoreThresholdChange,
isScoreThresholdEnabled,
onScoreThresholdEnabledChange,
readonly,
hiddenScoreThreshold,
}: TopKAndScoreThresholdProps) => {
const { t } = useTranslation()
const handleTopKChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = Number(e.target.value)
if (Number.isNaN(value))
return
onTopKChange?.(value)
}
const handleScoreThresholdChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = Number(e.target.value)
if (Number.isNaN(value))
return
onScoreThresholdChange?.(value)
}
return (
<div className='grid grid-cols-2 gap-4'>
<div>
<div className='system-xs-medium mb-0.5 flex h-6 items-center text-text-secondary'>
{t('appDebug.datasetConfig.top_k')}
<Tooltip
triggerClassName='ml-0.5 shrink-0 w-3.5 h-3.5'
popupContent={t('appDebug.datasetConfig.top_kTip')}
/>
</div>
<Input
type='number'
value={topK}
onChange={handleTopKChange}
disabled={readonly}
/>
</div>
{
!hiddenScoreThreshold && (
<div>
<div className='mb-0.5 flex h-6 items-center'>
<Switch
className='mr-2'
defaultValue={isScoreThresholdEnabled}
onChange={onScoreThresholdEnabledChange}
disabled={readonly}
/>
<div className='system-sm-medium grow truncate text-text-secondary'>
{t('appDebug.datasetConfig.score_threshold')}
</div>
<Tooltip
triggerClassName='shrink-0 ml-0.5 w-3.5 h-3.5'
popupContent={t('appDebug.datasetConfig.score_thresholdTip')}
/>
</div>
<Input
type='number'
value={scoreThreshold}
onChange={handleScoreThresholdChange}
disabled={readonly || !isScoreThresholdEnabled}
/>
</div>
)
}
</div>
)
}
export default memo(TopKAndScoreThreshold)

View file

@ -0,0 +1,20 @@
import type { ComponentType } from 'react'
import type {
HybridSearchModeEnum,
RetrievalSearchMethodEnum,
} from '../../types'
export type Option = {
id: RetrievalSearchMethodEnum
icon: ComponentType<any>
title: any
description: string
effectColor?: string
showEffectColor?: boolean,
}
export type HybridSearchModeOption = {
id: HybridSearchModeEnum
title: string
description: string
}

View file

@ -0,0 +1,60 @@
import type { NodeDefault } from '../../types'
import type { KnowledgeBaseNodeType } from './types'
import { genNodeMetaData } from '@/app/components/workflow/utils'
import { BlockEnum } from '@/app/components/workflow/types'
const metaData = genNodeMetaData({
sort: 3.1,
type: BlockEnum.KnowledgeBase,
isRequired: true,
isUndeletable: true,
isSingleton: true,
isTypeFixed: true,
})
const nodeDefault: NodeDefault<KnowledgeBaseNodeType> = {
metaData,
defaultValue: {
index_chunk_variable_selector: [],
keyword_number: 10,
retrieval_model: {
top_k: 3,
score_threshold_enabled: false,
score_threshold: 0.5,
},
},
checkValid(payload, t) {
const {
chunk_structure,
indexing_technique,
retrieval_model,
} = payload
if (!chunk_structure) {
return {
isValid: false,
errorMessage: t('workflow.nodes.knowledgeBase.chunkIsRequired'),
}
}
if (!indexing_technique) {
return {
isValid: false,
errorMessage: t('workflow.nodes.knowledgeBase.indexMethodIsRequired'),
}
}
if (!retrieval_model || !retrieval_model.search_method) {
return {
isValid: false,
errorMessage: t('workflow.nodes.knowledgeBase.retrievalSettingIsRequired'),
}
}
return {
isValid: true,
errorMessage: '',
}
},
}
export default nodeDefault

View file

@ -0,0 +1,211 @@
import {
useCallback,
} from 'react'
import { produce } from 'immer'
import { useStoreApi } from 'reactflow'
import { useNodeDataUpdate } from '@/app/components/workflow/hooks'
import type { ValueSelector } from '@/app/components/workflow/types'
import {
ChunkStructureEnum,
IndexMethodEnum,
RetrievalSearchMethodEnum,
} from '../types'
import type {
HybridSearchModeEnum,
KnowledgeBaseNodeType,
RerankingModel,
} from '../types'
import { isHighQualitySearchMethod } from '../utils'
export const useConfig = (id: string) => {
const store = useStoreApi()
const { handleNodeDataUpdateWithSyncDraft } = useNodeDataUpdate()
const getNodeData = useCallback(() => {
const { getNodes } = store.getState()
const nodes = getNodes()
return nodes.find(node => node.id === id)
}, [store, id])
const handleNodeDataUpdate = useCallback((data: Partial<KnowledgeBaseNodeType>) => {
handleNodeDataUpdateWithSyncDraft({
id,
data,
})
}, [id, handleNodeDataUpdateWithSyncDraft])
const handleChunkStructureChange = useCallback((chunkStructure: ChunkStructureEnum) => {
const nodeData = getNodeData()
const {
indexing_technique,
retrieval_model,
chunk_structure,
index_chunk_variable_selector,
} = nodeData?.data || {}
const { search_method } = retrieval_model || {}
handleNodeDataUpdate({
chunk_structure: chunkStructure,
indexing_technique: (chunkStructure === ChunkStructureEnum.parent_child || chunkStructure === ChunkStructureEnum.question_answer) ? IndexMethodEnum.QUALIFIED : indexing_technique,
retrieval_model: {
...retrieval_model,
search_method: ((chunkStructure === ChunkStructureEnum.parent_child || chunkStructure === ChunkStructureEnum.question_answer) && !isHighQualitySearchMethod(search_method)) ? RetrievalSearchMethodEnum.keywordSearch : search_method,
},
index_chunk_variable_selector: chunkStructure === chunk_structure ? index_chunk_variable_selector : [],
})
}, [handleNodeDataUpdate, getNodeData])
const handleIndexMethodChange = useCallback((indexMethod: IndexMethodEnum) => {
const nodeData = getNodeData()
handleNodeDataUpdate(produce(nodeData?.data as KnowledgeBaseNodeType, (draft) => {
draft.indexing_technique = indexMethod
if (indexMethod === IndexMethodEnum.ECONOMICAL)
draft.retrieval_model.search_method = RetrievalSearchMethodEnum.keywordSearch
else if (indexMethod === IndexMethodEnum.QUALIFIED)
draft.retrieval_model.search_method = RetrievalSearchMethodEnum.semantic
}))
}, [handleNodeDataUpdate, getNodeData])
const handleKeywordNumberChange = useCallback((keywordNumber: number) => {
handleNodeDataUpdate({ keyword_number: keywordNumber })
}, [handleNodeDataUpdate])
const handleEmbeddingModelChange = useCallback(({
embeddingModel,
embeddingModelProvider,
}: {
embeddingModel: string
embeddingModelProvider: string
}) => {
const nodeData = getNodeData()
handleNodeDataUpdate({
embedding_model: embeddingModel,
embedding_model_provider: embeddingModelProvider,
retrieval_model: {
...nodeData?.data.retrieval_model,
vector_setting: {
...nodeData?.data.retrieval_model.vector_setting,
embedding_provider_name: embeddingModelProvider,
embedding_model_name: embeddingModel,
},
},
})
}, [getNodeData, handleNodeDataUpdate])
const handleRetrievalSearchMethodChange = useCallback((searchMethod: RetrievalSearchMethodEnum) => {
const nodeData = getNodeData()
handleNodeDataUpdate({
retrieval_model: {
...nodeData?.data.retrieval_model,
search_method: searchMethod,
},
})
}, [getNodeData, handleNodeDataUpdate])
const handleHybridSearchModeChange = useCallback((hybridSearchMode: HybridSearchModeEnum) => {
const nodeData = getNodeData()
handleNodeDataUpdate({
retrieval_model: {
...nodeData?.data.retrieval_model,
reranking_mode: hybridSearchMode,
},
})
}, [getNodeData, handleNodeDataUpdate])
const handleRerankingModelEnabledChange = useCallback((rerankingModelEnabled: boolean) => {
const nodeData = getNodeData()
handleNodeDataUpdate({
retrieval_model: {
...nodeData?.data.retrieval_model,
reranking_enable: rerankingModelEnabled,
},
})
}, [getNodeData, handleNodeDataUpdate])
const handleWeighedScoreChange = useCallback((weightedScore: { value: number[] }) => {
const nodeData = getNodeData()
handleNodeDataUpdate({
retrieval_model: {
...nodeData?.data.retrieval_model,
weights: {
weight_type: 'weighted_score',
vector_setting: {
vector_weight: weightedScore.value[0],
embedding_provider_name: '',
embedding_model_name: '',
},
keyword_setting: {
keyword_weight: weightedScore.value[1],
},
},
},
})
}, [getNodeData, handleNodeDataUpdate])
const handleRerankingModelChange = useCallback((rerankingModel: RerankingModel) => {
const nodeData = getNodeData()
handleNodeDataUpdate({
retrieval_model: {
...nodeData?.data.retrieval_model,
reranking_model: {
reranking_provider_name: rerankingModel.reranking_provider_name,
reranking_model_name: rerankingModel.reranking_model_name,
},
},
})
}, [getNodeData, handleNodeDataUpdate])
const handleTopKChange = useCallback((topK: number) => {
const nodeData = getNodeData()
handleNodeDataUpdate({
retrieval_model: {
...nodeData?.data.retrieval_model,
top_k: topK,
},
})
}, [getNodeData, handleNodeDataUpdate])
const handleScoreThresholdChange = useCallback((scoreThreshold: number) => {
const nodeData = getNodeData()
handleNodeDataUpdate({
retrieval_model: {
...nodeData?.data.retrieval_model,
score_threshold: scoreThreshold,
},
})
}, [getNodeData, handleNodeDataUpdate])
const handleScoreThresholdEnabledChange = useCallback((isEnabled: boolean) => {
const nodeData = getNodeData()
handleNodeDataUpdate({
retrieval_model: {
...nodeData?.data.retrieval_model,
score_threshold_enabled: isEnabled,
},
})
}, [getNodeData, handleNodeDataUpdate])
const handleInputVariableChange = useCallback((inputVariable: string | ValueSelector) => {
handleNodeDataUpdate({
index_chunk_variable_selector: Array.isArray(inputVariable) ? inputVariable : [],
})
}, [handleNodeDataUpdate])
return {
handleChunkStructureChange,
handleIndexMethodChange,
handleKeywordNumberChange,
handleEmbeddingModelChange,
handleRetrievalSearchMethodChange,
handleHybridSearchModeChange,
handleRerankingModelEnabledChange,
handleWeighedScoreChange,
handleRerankingModelChange,
handleTopKChange,
handleScoreThresholdChange,
handleScoreThresholdEnabledChange,
handleInputVariableChange,
}
}

View file

@ -0,0 +1,18 @@
import { useTranslation } from 'react-i18next'
import {
IndexMethodEnum,
RetrievalSearchMethodEnum,
} from '../types'
export const useSettingsDisplay = () => {
const { t } = useTranslation()
return {
[IndexMethodEnum.QUALIFIED]: t('datasetCreation.stepTwo.qualified'),
[IndexMethodEnum.ECONOMICAL]: t('datasetSettings.form.indexMethodEconomy'),
[RetrievalSearchMethodEnum.semantic]: t('dataset.retrieval.semantic_search.title'),
[RetrievalSearchMethodEnum.fullText]: t('dataset.retrieval.full_text_search.title'),
[RetrievalSearchMethodEnum.hybrid]: t('dataset.retrieval.hybrid_search.title'),
[RetrievalSearchMethodEnum.keywordSearch]: t('dataset.retrieval.keyword_search.title'),
}
}

View file

@ -0,0 +1,40 @@
import type { FC } from 'react'
import { memo } from 'react'
import { useTranslation } from 'react-i18next'
import type { KnowledgeBaseNodeType } from './types'
import { useSettingsDisplay } from './hooks/use-settings-display'
import type { NodeProps } from '@/app/components/workflow/types'
const Node: FC<NodeProps<KnowledgeBaseNodeType>> = ({ data }) => {
const { t } = useTranslation()
const settingsDisplay = useSettingsDisplay()
return (
<div className='mb-1 space-y-0.5 px-3 py-1'>
<div className='flex h-6 items-center rounded-md bg-workflow-block-parma-bg px-1.5'>
<div className='system-xs-medium-uppercase mr-2 shrink-0 text-text-tertiary'>
{t('datasetCreation.stepTwo.indexMode')}
</div>
<div
className='system-xs-medium grow truncate text-right text-text-secondary'
title={data.indexing_technique}
>
{settingsDisplay[data.indexing_technique as keyof typeof settingsDisplay]}
</div>
</div>
<div className='flex h-6 items-center rounded-md bg-workflow-block-parma-bg px-1.5'>
<div className='system-xs-medium-uppercase mr-2 shrink-0 text-text-tertiary'>
{t('datasetSettings.form.retrievalSetting.title')}
</div>
<div
className='system-xs-medium grow truncate text-right text-text-secondary'
title={data.retrieval_model.search_method}
>
{settingsDisplay[data.retrieval_model.search_method as keyof typeof settingsDisplay]}
</div>
</div>
</div>
)
}
export default memo(Node)

View file

@ -0,0 +1,175 @@
import type { FC } from 'react'
import {
memo,
useCallback,
useMemo,
} from 'react'
import { useTranslation } from 'react-i18next'
import type { KnowledgeBaseNodeType } from './types'
import {
ChunkStructureEnum,
IndexMethodEnum,
} from './types'
import ChunkStructure from './components/chunk-structure'
import IndexMethod from './components/index-method'
import RetrievalSetting from './components/retrieval-setting'
import EmbeddingModel from './components/embedding-model'
import { useConfig } from './hooks/use-config'
import type { NodePanelProps } from '@/app/components/workflow/types'
import {
BoxGroup,
BoxGroupField,
Group,
} from '@/app/components/workflow/nodes/_base/components/layout'
import Split from '../_base/components/split'
import { useNodesReadOnly } from '@/app/components/workflow/hooks'
import VarReferencePicker from '@/app/components/workflow/nodes/_base/components/variable/var-reference-picker'
import type { Var } from '@/app/components/workflow/types'
const Panel: FC<NodePanelProps<KnowledgeBaseNodeType>> = ({
id,
data,
}) => {
const { t } = useTranslation()
const { nodesReadOnly } = useNodesReadOnly()
const {
handleChunkStructureChange,
handleIndexMethodChange,
handleKeywordNumberChange,
handleEmbeddingModelChange,
handleRetrievalSearchMethodChange,
handleHybridSearchModeChange,
handleRerankingModelEnabledChange,
handleWeighedScoreChange,
handleRerankingModelChange,
handleTopKChange,
handleScoreThresholdChange,
handleScoreThresholdEnabledChange,
handleInputVariableChange,
} = useConfig(id)
const filterVar = useCallback((variable: Var) => {
if (!data.chunk_structure) return false
switch (data.chunk_structure) {
case ChunkStructureEnum.general:
return variable.schemaType === 'general_structure'
case ChunkStructureEnum.parent_child:
return variable.schemaType === 'parent_child_structure'
case ChunkStructureEnum.question_answer:
return variable.schemaType === 'qa_structure'
default:
return false
}
}, [data.chunk_structure])
const chunkTypePlaceHolder = useMemo(() => {
if (!data.chunk_structure) return ''
let placeholder = ''
switch (data.chunk_structure) {
case ChunkStructureEnum.general:
placeholder = 'general_structure'
break
case ChunkStructureEnum.parent_child:
placeholder = 'parent_child_structure'
break
case ChunkStructureEnum.question_answer:
placeholder = 'qa_structure'
break
default:
return ''
}
return placeholder.charAt(0).toUpperCase() + placeholder.slice(1)
}, [data.chunk_structure])
return (
<div>
<Group
className='py-3'
withBorderBottom={!!data.chunk_structure}
>
<ChunkStructure
chunkStructure={data.chunk_structure}
onChunkStructureChange={handleChunkStructureChange}
readonly={nodesReadOnly}
/>
</Group>
{
data.chunk_structure && (
<>
<BoxGroupField
boxGroupProps={{
boxProps: { withBorderBottom: true },
}}
fieldProps={{
fieldTitleProps: {
title: t('workflow.nodes.knowledgeBase.chunksInput'),
tooltip: t('workflow.nodes.knowledgeBase.chunksInputTip'),
},
}}
>
<VarReferencePicker
nodeId={id}
isShowNodeName
value={data.index_chunk_variable_selector}
onChange={handleInputVariableChange}
readonly={nodesReadOnly}
filterVar={filterVar}
isFilterFileVar
isSupportFileVar={false}
preferSchemaType
typePlaceHolder={chunkTypePlaceHolder}
/>
</BoxGroupField>
<BoxGroup>
<div className='space-y-3'>
<IndexMethod
chunkStructure={data.chunk_structure}
indexMethod={data.indexing_technique}
onIndexMethodChange={handleIndexMethodChange}
keywordNumber={data.keyword_number}
onKeywordNumberChange={handleKeywordNumberChange}
readonly={nodesReadOnly}
/>
{
data.indexing_technique === IndexMethodEnum.QUALIFIED && (
<EmbeddingModel
embeddingModel={data.embedding_model}
embeddingModelProvider={data.embedding_model_provider}
onEmbeddingModelChange={handleEmbeddingModelChange}
readonly={nodesReadOnly}
/>
)
}
<div className='pt-1'>
<Split className='h-[1px]' />
</div>
<RetrievalSetting
indexMethod={data.indexing_technique}
searchMethod={data.retrieval_model.search_method}
onRetrievalSearchMethodChange={handleRetrievalSearchMethodChange}
hybridSearchMode={data.retrieval_model.reranking_mode}
onHybridSearchModeChange={handleHybridSearchModeChange}
weightedScore={data.retrieval_model.weights}
onWeightedScoreChange={handleWeighedScoreChange}
rerankingModelEnabled={data.retrieval_model.reranking_enable}
onRerankingModelEnabledChange={handleRerankingModelEnabledChange}
rerankingModel={data.retrieval_model.reranking_model}
onRerankingModelChange={handleRerankingModelChange}
topK={data.retrieval_model.top_k}
onTopKChange={handleTopKChange}
scoreThreshold={data.retrieval_model.score_threshold}
onScoreThresholdChange={handleScoreThresholdChange}
isScoreThresholdEnabled={data.retrieval_model.score_threshold_enabled}
onScoreThresholdEnabledChange={handleScoreThresholdEnabledChange}
readonly={nodesReadOnly}
/>
</div>
</BoxGroup>
</>
)
}
</div>
)
}
export default memo(Panel)

View file

@ -0,0 +1,52 @@
import type { CommonNodeType } from '@/app/components/workflow/types'
import type { IndexingType } from '@/app/components/datasets/create/step-two'
import type { RETRIEVE_METHOD } from '@/types/app'
import type { WeightedScoreEnum } from '@/models/datasets'
import type { RerankingModeEnum } from '@/models/datasets'
export { WeightedScoreEnum } from '@/models/datasets'
export { IndexingType as IndexMethodEnum } from '@/app/components/datasets/create/step-two'
export { RETRIEVE_METHOD as RetrievalSearchMethodEnum } from '@/types/app'
export { RerankingModeEnum as HybridSearchModeEnum } from '@/models/datasets'
export enum ChunkStructureEnum {
general = 'text_model',
parent_child = 'hierarchical_model',
question_answer = 'qa_model',
}
export type RerankingModel = {
reranking_provider_name: string
reranking_model_name: string
}
export type WeightedScore = {
weight_type: WeightedScoreEnum
vector_setting: {
vector_weight: number
embedding_provider_name: string
embedding_model_name: string
}
keyword_setting: {
keyword_weight: number
}
}
export type RetrievalSetting = {
search_method?: RETRIEVE_METHOD
reranking_enable?: boolean
reranking_model?: RerankingModel
weights?: WeightedScore
top_k: number
score_threshold_enabled: boolean
score_threshold: number
reranking_mode?: RerankingModeEnum
}
export type KnowledgeBaseNodeType = CommonNodeType & {
index_chunk_variable_selector: string[]
chunk_structure?: ChunkStructureEnum
indexing_technique?: IndexingType
embedding_model?: string
embedding_model_provider?: string
keyword_number: number
retrieval_model: RetrievalSetting
}

View file

@ -0,0 +1,59 @@
import { useTranslation } from 'react-i18next'
import type { InputVar, Variable } from '@/app/components/workflow/types'
import { InputVarType } from '@/app/components/workflow/types'
import { useCallback, useMemo } from 'react'
import type { KnowledgeBaseNodeType } from './types'
type Params = {
id: string,
payload: KnowledgeBaseNodeType
runInputData: Record<string, any>
getInputVars: (textList: string[]) => InputVar[]
setRunInputData: (data: Record<string, any>) => void
toVarInputs: (variables: Variable[]) => InputVar[]
}
const useSingleRunFormParams = ({
payload,
runInputData,
setRunInputData,
}: Params) => {
const { t } = useTranslation()
const query = runInputData.query
const setQuery = useCallback((newQuery: string) => {
setRunInputData({
...runInputData,
query: newQuery,
})
}, [runInputData, setRunInputData])
const forms = useMemo(() => {
return [
{
inputs: [{
label: t('workflow.nodes.common.inputVars'),
variable: 'query',
type: InputVarType.paragraph,
required: true,
}],
values: { query },
onChange: (keyValue: Record<string, any>) => setQuery(keyValue.query),
},
]
}, [query, setQuery, t])
const getDependentVars = () => {
return [payload.index_chunk_variable_selector]
}
const getDependentVar = (variable: string) => {
if(variable === 'query')
return payload.index_chunk_variable_selector
}
return {
forms,
getDependentVars,
getDependentVar,
}
}
export default useSingleRunFormParams

View file

@ -0,0 +1,9 @@
import {
RetrievalSearchMethodEnum,
} from './types'
export const isHighQualitySearchMethod = (searchMethod: RetrievalSearchMethodEnum) => {
return searchMethod === RetrievalSearchMethodEnum.semantic
|| searchMethod === RetrievalSearchMethodEnum.hybrid
|| searchMethod === RetrievalSearchMethodEnum.fullText
}

View file

@ -8,15 +8,13 @@ import {
} from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import type { DataSet } from '@/models/datasets'
import { DataSourceType } from '@/models/datasets'
import ActionButton, { ActionButtonState } from '@/app/components/base/action-button'
import FileIcon from '@/app/components/base/file-icon'
import { Folder } from '@/app/components/base/icons/src/vender/solid/files'
import SettingsModal from '@/app/components/app/configuration/dataset-config/settings-modal'
import Drawer from '@/app/components/base/drawer'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import Badge from '@/app/components/base/badge'
import { useKnowledge } from '@/hooks/use-knowledge'
import AppIcon from '@/app/components/base/app-icon'
type Props = {
payload: DataSet
@ -54,6 +52,13 @@ const DatasetItem: FC<Props> = ({
onRemove()
}, [onRemove])
const iconInfo = payload.icon_info || {
icon: '📙',
icon_type: 'emoji',
icon_background: '#FFF4ED',
icon_url: '',
}
return (
<div className={`group/dataset-item flex h-10 cursor-pointer items-center justify-between rounded-lg
border-[0.5px] border-components-panel-border-subtle px-2
@ -62,17 +67,13 @@ const DatasetItem: FC<Props> = ({
: 'bg-components-panel-on-panel-item-bg hover:bg-components-panel-on-panel-item-bg-hover'
}`}>
<div className='flex w-0 grow items-center space-x-1.5'>
{
payload.data_source_type === DataSourceType.NOTION
? (
<div className='flex h-6 w-6 shrink-0 items-center justify-center rounded-md border-[0.5px] border-[#EAECF5]'>
<FileIcon type='notion' className='h-4 w-4' />
</div>
)
: <div className='flex h-6 w-6 shrink-0 items-center justify-center rounded-md border-[0.5px] border-[#E0EAFF] bg-[#F5F8FF]'>
<Folder className='h-4 w-4 text-[#444CE7]' />
</div>
}
<AppIcon
size='tiny'
iconType={iconInfo.icon_type}
icon={iconInfo.icon}
background={iconInfo.icon_type === 'image' ? undefined : iconInfo.icon_background}
imageUrl={iconInfo.icon_type === 'image' ? iconInfo.icon_url : undefined}
/>
<div className='system-sm-medium w-0 grow truncate text-text-secondary'>{payload.name}</div>
</div>
{!readonly && (

Some files were not shown because too many files have changed in this diff Show more