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:
parent
7dadb33003
commit
85cda47c70
1772 changed files with 102407 additions and 31710 deletions
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ const ErrorHandleTip = ({
|
|||
|
||||
if (type === ErrorHandleTypeEnum.defaultValue)
|
||||
return t('workflow.nodes.common.errorHandle.defaultValue.inLog')
|
||||
}, [])
|
||||
}, [t, type])
|
||||
|
||||
if (!type)
|
||||
return null
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
})
|
||||
|
|
@ -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>
|
||||
)
|
||||
})
|
||||
|
|
@ -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>
|
||||
)
|
||||
})
|
||||
|
|
@ -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>
|
||||
)
|
||||
})
|
||||
|
|
@ -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>
|
||||
)
|
||||
})
|
||||
|
|
@ -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>
|
||||
)
|
||||
})
|
||||
|
|
@ -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>
|
||||
)
|
||||
})
|
||||
|
|
@ -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'
|
||||
|
|
@ -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'>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -31,7 +31,6 @@ const PanelOperator = ({
|
|||
crossAxis: 53,
|
||||
},
|
||||
onOpenChange,
|
||||
inNode,
|
||||
showHelpLink = true,
|
||||
}: PanelOperatorProps) => {
|
||||
const [open, setOpen] = useState(false)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 && (
|
||||
|
|
|
|||
|
|
@ -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
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 >
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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] : []),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
export const CUSTOM_DATA_SOURCE_EMPTY_NODE = 'custom-data-source-empty'
|
||||
|
|
@ -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
|
||||
50
web/app/components/workflow/nodes/data-source-empty/hooks.ts
Normal file
50
web/app/components/workflow/nodes/data-source-empty/hooks.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
import type { CommonNodeType } from '@/app/components/workflow/types'
|
||||
|
||||
export type DataSourceEmptyNodeType = CommonNodeType
|
||||
|
|
@ -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)
|
||||
59
web/app/components/workflow/nodes/data-source/constants.ts
Normal file
59
web/app/components/workflow/nodes/data-source/constants.ts
Normal 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',
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
111
web/app/components/workflow/nodes/data-source/default.ts
Normal file
111
web/app/components/workflow/nodes/data-source/default.ts
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
12
web/app/components/workflow/nodes/data-source/node.tsx
Normal file
12
web/app/components/workflow/nodes/data-source/node.tsx
Normal 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)
|
||||
164
web/app/components/workflow/nodes/data-source/panel.tsx
Normal file
164
web/app/components/workflow/nodes/data-source/panel.tsx
Normal 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)
|
||||
47
web/app/components/workflow/nodes/data-source/types.ts
Normal file
47
web/app/components/workflow/nodes/data-source/types.ts
Normal 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
|
||||
}
|
||||
11
web/app/components/workflow/nodes/data-source/utils.ts
Normal file
11
web/app/components/workflow/nodes/data-source/utils.ts
Normal 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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 = ''
|
||||
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 = ''
|
||||
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
|
@ -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,
|
||||
])
|
||||
}
|
||||
|
|
@ -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>
|
||||
{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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
}
|
||||
60
web/app/components/workflow/nodes/knowledge-base/default.ts
Normal file
60
web/app/components/workflow/nodes/knowledge-base/default.ts
Normal 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
|
||||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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'),
|
||||
}
|
||||
}
|
||||
40
web/app/components/workflow/nodes/knowledge-base/node.tsx
Normal file
40
web/app/components/workflow/nodes/knowledge-base/node.tsx
Normal 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)
|
||||
175
web/app/components/workflow/nodes/knowledge-base/panel.tsx
Normal file
175
web/app/components/workflow/nodes/knowledge-base/panel.tsx
Normal 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)
|
||||
52
web/app/components/workflow/nodes/knowledge-base/types.ts
Normal file
52
web/app/components/workflow/nodes/knowledge-base/types.ts
Normal 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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
import {
|
||||
RetrievalSearchMethodEnum,
|
||||
} from './types'
|
||||
|
||||
export const isHighQualitySearchMethod = (searchMethod: RetrievalSearchMethodEnum) => {
|
||||
return searchMethod === RetrievalSearchMethodEnum.semantic
|
||||
|| searchMethod === RetrievalSearchMethodEnum.hybrid
|
||||
|| searchMethod === RetrievalSearchMethodEnum.fullText
|
||||
}
|
||||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue