Feat/attachments (#9526)
Co-authored-by: Joel <iamjoel007@gmail.com> Co-authored-by: JzoNg <jzongcode@gmail.com>
This commit is contained in:
@@ -5,6 +5,7 @@ import {
|
||||
RiAddLine,
|
||||
} from '@remixicon/react'
|
||||
import cn from '@/utils/classnames'
|
||||
import Button from '@/app/components/base/button'
|
||||
|
||||
type Props = {
|
||||
className?: string
|
||||
@@ -18,13 +19,15 @@ const AddButton: FC<Props> = ({
|
||||
onClick,
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(className, 'flex items-center h-7 justify-center bg-gray-100 hover:bg-gray-200 rounded-lg cursor-pointer text-xs font-medium text-gray-700 space-x-1')}
|
||||
<Button
|
||||
className={cn('w-full', className)}
|
||||
variant='tertiary'
|
||||
size='medium'
|
||||
onClick={onClick}
|
||||
>
|
||||
<RiAddLine className='w-3.5 h-3.5' />
|
||||
<RiAddLine className='mr-1 w-3.5 h-3.5' />
|
||||
<div>{text}</div>
|
||||
</div>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
export default React.memo(AddButton)
|
||||
|
@@ -7,13 +7,16 @@ import {
|
||||
RiDeleteBinLine,
|
||||
} from '@remixicon/react'
|
||||
import type { InputVar } from '../../../../types'
|
||||
import { BlockEnum, InputVarType } from '../../../../types'
|
||||
import { BlockEnum, InputVarType, SupportUploadFileTypes } from '../../../../types'
|
||||
import CodeEditor from '../editor/code-editor'
|
||||
import { CodeLanguage } from '../../../code/types'
|
||||
import TextEditor from '../editor/text-editor'
|
||||
import Select from '@/app/components/base/select'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Textarea from '@/app/components/base/textarea'
|
||||
import TextGenerationImageUploader from '@/app/components/base/image-uploader/text-generation-image-uploader'
|
||||
import { Resolution } from '@/types/app'
|
||||
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'
|
||||
@@ -27,6 +30,7 @@ type Props = {
|
||||
onChange: (value: any) => void
|
||||
className?: string
|
||||
autoFocus?: boolean
|
||||
inStepRun?: boolean
|
||||
}
|
||||
|
||||
const FormItem: FC<Props> = ({
|
||||
@@ -35,6 +39,7 @@ const FormItem: FC<Props> = ({
|
||||
onChange,
|
||||
className,
|
||||
autoFocus,
|
||||
inStepRun = false,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { type } = payload
|
||||
@@ -89,7 +94,7 @@ const FormItem: FC<Props> = ({
|
||||
const isContext = type === InputVarType.contexts
|
||||
const isIterator = type === InputVarType.iterator
|
||||
return (
|
||||
<div className={`${className}`}>
|
||||
<div className={cn(className)}>
|
||||
{!isArrayLikeType && (
|
||||
<div className='h-6 mb-1 flex items-center gap-1 text-text-secondary system-sm-semibold'>
|
||||
<div className='truncate'>{typeof payload.label === 'object' ? nodeKey : payload.label}</div>
|
||||
@@ -99,9 +104,7 @@ const FormItem: FC<Props> = ({
|
||||
<div className='grow'>
|
||||
{
|
||||
type === InputVarType.textInput && (
|
||||
<input
|
||||
className="w-full px-3 text-sm leading-8 text-gray-900 border-0 rounded-lg grow h-8 bg-gray-50 focus:outline-none focus:ring-1 focus:ring-inset focus:ring-gray-200"
|
||||
type="text"
|
||||
<Input
|
||||
value={value || ''}
|
||||
onChange={e => onChange(e.target.value)}
|
||||
placeholder={t('appDebug.variableConfig.inputPlaceholder')!}
|
||||
@@ -112,8 +115,7 @@ const FormItem: FC<Props> = ({
|
||||
|
||||
{
|
||||
type === InputVarType.number && (
|
||||
<input
|
||||
className="w-full px-3 text-sm leading-8 text-gray-900 border-0 rounded-lg grow h-8 bg-gray-50 focus:outline-none focus:ring-1 focus:ring-inset focus:ring-gray-200"
|
||||
<Input
|
||||
type="number"
|
||||
value={value || ''}
|
||||
onChange={e => onChange(e.target.value)}
|
||||
@@ -125,8 +127,7 @@ const FormItem: FC<Props> = ({
|
||||
|
||||
{
|
||||
type === InputVarType.paragraph && (
|
||||
<textarea
|
||||
className="w-full px-3 py-1 text-sm leading-[18px] text-gray-900 border-0 rounded-lg grow h-[120px] bg-gray-50 focus:outline-none focus:ring-1 focus:ring-inset focus:ring-gray-200"
|
||||
<Textarea
|
||||
value={value || ''}
|
||||
onChange={e => onChange(e.target.value)}
|
||||
placeholder={t('appDebug.variableConfig.inputPlaceholder')!}
|
||||
@@ -157,13 +158,42 @@ const FormItem: FC<Props> = ({
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
{(type === InputVarType.singleFile) && (
|
||||
<FileUploaderInAttachmentWrapper
|
||||
value={value ? [value] : []}
|
||||
onChange={(files) => {
|
||||
if (files.length)
|
||||
onChange(files[0])
|
||||
else
|
||||
onChange(null)
|
||||
}}
|
||||
fileConfig={{
|
||||
allowed_file_types: inStepRun ? [SupportUploadFileTypes.custom] : payload.allowed_file_types,
|
||||
allowed_file_extensions: inStepRun ? [] : payload.allowed_file_extensions,
|
||||
allowed_file_upload_methods: inStepRun ? [TransferMethod.local_file, TransferMethod.remote_url] : payload.allowed_file_upload_methods,
|
||||
number_limits: 1,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{(type === InputVarType.multiFiles) && (
|
||||
<FileUploaderInAttachmentWrapper
|
||||
value={value}
|
||||
onChange={files => onChange(files)}
|
||||
fileConfig={{
|
||||
allowed_file_types: inStepRun ? [SupportUploadFileTypes.custom] : payload.allowed_file_types,
|
||||
allowed_file_extensions: inStepRun ? [] : payload.allowed_file_extensions,
|
||||
allowed_file_upload_methods: inStepRun ? [TransferMethod.local_file, TransferMethod.remote_url] : payload.allowed_file_upload_methods,
|
||||
number_limits: inStepRun ? 5 : payload.max_length,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{
|
||||
type === InputVarType.files && (
|
||||
<TextGenerationImageUploader
|
||||
settings={{
|
||||
...fileSettings?.image,
|
||||
detail: Resolution.high,
|
||||
...fileSettings,
|
||||
detail: fileSettings?.image?.detail || Resolution.high,
|
||||
transfer_methods: fileSettings?.allowed_file_upload_methods || [],
|
||||
} as any}
|
||||
onFilesChange={files => onChange(files.filter(file => file.progress !== -1).map(fileItem => ({
|
||||
type: 'image',
|
||||
@@ -187,7 +217,7 @@ const FormItem: FC<Props> = ({
|
||||
(value as any).length > 1
|
||||
? (<RiDeleteBinLine
|
||||
onClick={handleArrayItemRemove(index)}
|
||||
className='mr-1 w-3.5 h-3.5 text-gray-500 cursor-pointer'
|
||||
className='mr-1 w-3.5 h-3.5 text-text-tertiary cursor-pointer'
|
||||
/>)
|
||||
: undefined
|
||||
}
|
||||
@@ -213,7 +243,7 @@ const FormItem: FC<Props> = ({
|
||||
(value as any).length > 1
|
||||
? (<RiDeleteBinLine
|
||||
onClick={handleArrayItemRemove(index)}
|
||||
className='mr-1 w-3.5 h-3.5 text-gray-500 cursor-pointer'
|
||||
className='mr-1 w-3.5 h-3.5 text-text-tertiary cursor-pointer'
|
||||
/>)
|
||||
: undefined
|
||||
}
|
||||
|
@@ -71,7 +71,7 @@ const Form: FC<Props> = ({
|
||||
<div className={cn(className, 'space-y-2')}>
|
||||
{label && (
|
||||
<div className='mb-1 flex items-center justify-between'>
|
||||
<div className='flex items-center h-6 text-xs font-medium text-gray-500 uppercase'>{label}</div>
|
||||
<div className='flex items-center h-6 system-xs-medium-uppercase text-text-tertiary'>{label}</div>
|
||||
{isArrayLikeType && (
|
||||
<AddButton onClick={handleAddContext} />
|
||||
)}
|
||||
@@ -80,6 +80,7 @@ const Form: FC<Props> = ({
|
||||
{inputs.map((input, index) => {
|
||||
return (
|
||||
<FormItem
|
||||
inStepRun
|
||||
key={index}
|
||||
payload={input}
|
||||
value={values[input.variable]}
|
||||
|
@@ -0,0 +1,91 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import produce from 'immer'
|
||||
import VarReferencePicker from './variable/var-reference-picker'
|
||||
import ResolutionPicker from '@/app/components/workflow/nodes/llm/components/resolution-picker'
|
||||
import Field from '@/app/components/workflow/nodes/_base/components/field'
|
||||
import Switch from '@/app/components/base/switch'
|
||||
import { type ValueSelector, type Var, VarType, type VisionSetting } from '@/app/components/workflow/types'
|
||||
import { Resolution } from '@/types/app'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
const i18nPrefix = 'workflow.nodes.llm'
|
||||
|
||||
type Props = {
|
||||
isVisionModel: boolean
|
||||
readOnly: boolean
|
||||
enabled: boolean
|
||||
onEnabledChange: (enabled: boolean) => void
|
||||
nodeId: string
|
||||
config?: VisionSetting
|
||||
onConfigChange: (config: VisionSetting) => void
|
||||
}
|
||||
|
||||
const ConfigVision: FC<Props> = ({
|
||||
isVisionModel,
|
||||
readOnly,
|
||||
enabled,
|
||||
onEnabledChange,
|
||||
nodeId,
|
||||
config = {
|
||||
detail: Resolution.high,
|
||||
variable_selector: [],
|
||||
},
|
||||
onConfigChange,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const filterVar = useCallback((payload: Var) => {
|
||||
return [VarType.file, VarType.arrayFile].includes(payload.type)
|
||||
}, [])
|
||||
const handleVisionResolutionChange = useCallback((resolution: Resolution) => {
|
||||
const newConfig = produce(config, (draft) => {
|
||||
draft.detail = resolution
|
||||
})
|
||||
onConfigChange(newConfig)
|
||||
}, [config, onConfigChange])
|
||||
|
||||
const handleVarSelectorChange = useCallback((valueSelector: ValueSelector | string) => {
|
||||
const newConfig = produce(config, (draft) => {
|
||||
draft.variable_selector = valueSelector as ValueSelector
|
||||
})
|
||||
onConfigChange(newConfig)
|
||||
}, [config, onConfigChange])
|
||||
|
||||
return (
|
||||
<Field
|
||||
title={t(`${i18nPrefix}.vision`)}
|
||||
tooltip={t('appDebug.vision.description')!}
|
||||
operations={
|
||||
<Tooltip
|
||||
popupContent={t('appDebug.vision.onlySupportVisionModelTip')!}
|
||||
disabled={isVisionModel}
|
||||
>
|
||||
<Switch disabled={readOnly || !isVisionModel} size='md' defaultValue={!isVisionModel ? false : enabled} onChange={onEnabledChange} />
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
{(enabled && isVisionModel)
|
||||
? (
|
||||
<div>
|
||||
<VarReferencePicker
|
||||
className='mb-4'
|
||||
filterVar={filterVar}
|
||||
nodeId={nodeId}
|
||||
value={config.variable_selector || []}
|
||||
onChange={handleVarSelectorChange}
|
||||
readonly={readOnly}
|
||||
/>
|
||||
<ResolutionPicker
|
||||
value={config.detail}
|
||||
onChange={handleVisionResolutionChange}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
: null}
|
||||
|
||||
</Field>
|
||||
)
|
||||
}
|
||||
export default React.memo(ConfigVision)
|
@@ -11,6 +11,8 @@ import {
|
||||
} from '@/app/components/base/icons/src/vender/line/files'
|
||||
import ToggleExpandBtn from '@/app/components/workflow/nodes/_base/components/toggle-expand-btn'
|
||||
import useToggleExpend from '@/app/components/workflow/nodes/_base/hooks/use-toggle-expend'
|
||||
import type { FileEntity } from '@/app/components/base/file-uploader/types'
|
||||
import FileListInLog from '@/app/components/base/file-uploader/file-list-in-log'
|
||||
|
||||
type Props = {
|
||||
className?: string
|
||||
@@ -21,6 +23,8 @@ type Props = {
|
||||
value: string
|
||||
isFocus: boolean
|
||||
isInNode?: boolean
|
||||
fileList?: FileEntity[]
|
||||
showFileList?: boolean
|
||||
}
|
||||
|
||||
const Base: FC<Props> = ({
|
||||
@@ -32,6 +36,8 @@ const Base: FC<Props> = ({
|
||||
value,
|
||||
isFocus,
|
||||
isInNode,
|
||||
fileList = [],
|
||||
showFileList,
|
||||
}) => {
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
const {
|
||||
@@ -87,6 +93,9 @@ const Base: FC<Props> = ({
|
||||
{children}
|
||||
</div>
|
||||
</PromptEditorHeightResizeWrap>
|
||||
{showFileList && fileList.length > 0 && (
|
||||
<FileListInLog fileList={fileList} />
|
||||
)}
|
||||
</div>
|
||||
</Wrap>
|
||||
)
|
||||
|
@@ -1,10 +1,13 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import Editor, { loader } from '@monaco-editor/react'
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import Base from '../base'
|
||||
import cn from '@/utils/classnames'
|
||||
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
|
||||
import {
|
||||
getFilesInLogs,
|
||||
} from '@/app/components/base/file-uploader/utils'
|
||||
|
||||
import './style.css'
|
||||
|
||||
@@ -27,6 +30,7 @@ export type Props = {
|
||||
onMount?: (editor: any, monaco: any) => void
|
||||
noWrapper?: boolean
|
||||
isExpand?: boolean
|
||||
showFileList?: boolean
|
||||
}
|
||||
|
||||
const languageMap = {
|
||||
@@ -58,6 +62,7 @@ const CodeEditor: FC<Props> = ({
|
||||
onMount,
|
||||
noWrapper,
|
||||
isExpand,
|
||||
showFileList,
|
||||
}) => {
|
||||
const [isFocus, setIsFocus] = React.useState(false)
|
||||
const [isMounted, setIsMounted] = React.useState(false)
|
||||
@@ -69,6 +74,12 @@ const CodeEditor: FC<Props> = ({
|
||||
valueRef.current = value
|
||||
}, [value])
|
||||
|
||||
const fileList = useMemo(() => {
|
||||
if (typeof value === 'object')
|
||||
return getFilesInLogs(value)
|
||||
return []
|
||||
}, [value])
|
||||
|
||||
const editorRef = useRef<any>(null)
|
||||
const resizeEditorToContent = () => {
|
||||
if (editorRef.current) {
|
||||
@@ -189,6 +200,8 @@ const CodeEditor: FC<Props> = ({
|
||||
isFocus={isFocus && !readOnly}
|
||||
minHeight={minHeight}
|
||||
isInNode={isInNode}
|
||||
fileList={fileList}
|
||||
showFileList={showFileList}
|
||||
>
|
||||
{main}
|
||||
</Base>
|
||||
|
@@ -12,6 +12,7 @@ import Tooltip from '@/app/components/base/tooltip'
|
||||
type Props = {
|
||||
className?: string
|
||||
title: JSX.Element | string | DefaultTFuncReturn
|
||||
isSubTitle?: boolean
|
||||
tooltip?: string
|
||||
supportFold?: boolean
|
||||
children?: JSX.Element | string | null
|
||||
@@ -22,6 +23,7 @@ type Props = {
|
||||
const Filed: FC<Props> = ({
|
||||
className,
|
||||
title,
|
||||
isSubTitle,
|
||||
tooltip,
|
||||
children,
|
||||
operations,
|
||||
@@ -37,7 +39,7 @@ const Filed: FC<Props> = ({
|
||||
onClick={() => supportFold && toggleFold()}
|
||||
className={cn('flex justify-between items-center', supportFold && 'cursor-pointer')}>
|
||||
<div className='flex items-center h-6'>
|
||||
<div className='system-sm-semibold-uppercase text-text-secondary'>{title}</div>
|
||||
<div className={cn(isSubTitle ? 'system-xs-medium-uppercase text-text-tertiary' : 'system-sm-semibold-uppercase text-text-secondary')}>{title}</div>
|
||||
{tooltip && (
|
||||
<Tooltip
|
||||
popupContent={tooltip}
|
||||
|
@@ -0,0 +1,77 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { SupportUploadFileTypes } from '../../../types'
|
||||
import cn from '@/utils/classnames'
|
||||
import { FILE_EXTS } from '@/app/components/base/prompt-editor/constants'
|
||||
import TagInput from '@/app/components/base/tag-input'
|
||||
import Checkbox from '@/app/components/base/checkbox'
|
||||
import { FileTypeIcon } from '@/app/components/base/file-uploader'
|
||||
|
||||
type Props = {
|
||||
type: SupportUploadFileTypes.image | SupportUploadFileTypes.document | SupportUploadFileTypes.audio | SupportUploadFileTypes.video | SupportUploadFileTypes.custom
|
||||
selected: boolean
|
||||
onToggle: (type: SupportUploadFileTypes) => void
|
||||
onCustomFileTypesChange?: (customFileTypes: string[]) => void
|
||||
customFileTypes?: string[]
|
||||
}
|
||||
|
||||
const FileTypeItem: FC<Props> = ({
|
||||
type,
|
||||
selected,
|
||||
onToggle,
|
||||
customFileTypes = [],
|
||||
onCustomFileTypesChange = () => { },
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const handleOnSelect = useCallback(() => {
|
||||
onToggle(type)
|
||||
}, [onToggle, type])
|
||||
|
||||
const isCustomSelected = type === SupportUploadFileTypes.custom && selected
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'rounded-lg bg-components-option-card-option-bg border border-components-option-card-option-border cursor-pointer select-none',
|
||||
!isCustomSelected && 'py-2 px-3',
|
||||
selected && 'border-[1.5px] bg-components-option-card-option-selected-bg border-components-option-card-option-selected-border',
|
||||
!selected && 'hover:bg-components-option-card-option-bg-hover hover:border-components-option-card-option-border-hover',
|
||||
)}
|
||||
onClick={handleOnSelect}
|
||||
>
|
||||
{isCustomSelected
|
||||
? (
|
||||
<div>
|
||||
<div className='flex items-center p-3 pb-2 border-b border-divider-subtle'>
|
||||
<FileTypeIcon className='shrink-0' type={type} size='md' />
|
||||
<div className='mx-2 grow text-text-primary system-sm-medium'>{t(`appDebug.variableConfig.file.${type}.name`)}</div>
|
||||
<Checkbox className='shrink-0' checked={selected} />
|
||||
</div>
|
||||
<div className='p-3' onClick={e => e.stopPropagation()}>
|
||||
<TagInput
|
||||
items={customFileTypes}
|
||||
onChange={onCustomFileTypesChange}
|
||||
placeholder={t('appDebug.variableConfig.file.custom.createPlaceholder')!}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<div className='flex items-center'>
|
||||
<FileTypeIcon className='shrink-0' type={type} size='md' />
|
||||
<div className='mx-2 grow'>
|
||||
<div className='text-text-primary system-sm-medium'>{t(`appDebug.variableConfig.file.${type}.name`)}</div>
|
||||
<div className='mt-1 text-text-tertiary system-2xs-regular-uppercase'>{type !== SupportUploadFileTypes.custom ? FILE_EXTS[type].join(', ') : t('appDebug.variableConfig.file.custom.description')}</div>
|
||||
</div>
|
||||
<Checkbox className='shrink-0' checked={selected} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(FileTypeItem)
|
@@ -0,0 +1,185 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import produce from 'immer'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { UploadFileSetting } from '../../../types'
|
||||
import { SupportUploadFileTypes } from '../../../types'
|
||||
import OptionCard from './option-card'
|
||||
import FileTypeItem from './file-type-item'
|
||||
import InputNumberWithSlider from './input-number-with-slider'
|
||||
import Field from '@/app/components/app/configuration/config-var/config-modal/field'
|
||||
import { TransferMethod } from '@/types/app'
|
||||
import { FILE_SIZE_LIMIT } from '@/app/components/base/file-uploader/constants'
|
||||
import { formatFileSize } from '@/utils/format'
|
||||
|
||||
type Props = {
|
||||
payload: UploadFileSetting
|
||||
isMultiple: boolean
|
||||
inFeaturePanel?: boolean
|
||||
hideSupportFileType?: boolean
|
||||
onChange: (payload: UploadFileSetting) => void
|
||||
}
|
||||
|
||||
const FileUploadSetting: FC<Props> = ({
|
||||
payload,
|
||||
isMultiple,
|
||||
inFeaturePanel = false,
|
||||
hideSupportFileType = false,
|
||||
onChange,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const {
|
||||
allowed_file_upload_methods,
|
||||
max_length,
|
||||
allowed_file_types,
|
||||
allowed_file_extensions,
|
||||
} = payload
|
||||
|
||||
const handleSupportFileTypeChange = useCallback((type: SupportUploadFileTypes) => {
|
||||
const newPayload = produce(payload, (draft) => {
|
||||
if (type === SupportUploadFileTypes.custom) {
|
||||
if (!draft.allowed_file_types.includes(SupportUploadFileTypes.custom))
|
||||
draft.allowed_file_types = [SupportUploadFileTypes.custom]
|
||||
|
||||
else
|
||||
draft.allowed_file_types = draft.allowed_file_types.filter(v => v !== type)
|
||||
}
|
||||
else {
|
||||
draft.allowed_file_types = draft.allowed_file_types.filter(v => v !== SupportUploadFileTypes.custom)
|
||||
if (draft.allowed_file_types.includes(type))
|
||||
draft.allowed_file_types = draft.allowed_file_types.filter(v => v !== type)
|
||||
else
|
||||
draft.allowed_file_types.push(type)
|
||||
}
|
||||
})
|
||||
onChange(newPayload)
|
||||
}, [onChange, payload])
|
||||
|
||||
const handleUploadMethodChange = useCallback((method: TransferMethod) => {
|
||||
return () => {
|
||||
const newPayload = produce(payload, (draft) => {
|
||||
if (method === TransferMethod.all)
|
||||
draft.allowed_file_upload_methods = [TransferMethod.local_file, TransferMethod.remote_url]
|
||||
else
|
||||
draft.allowed_file_upload_methods = [method]
|
||||
})
|
||||
onChange(newPayload)
|
||||
}
|
||||
}, [onChange, payload])
|
||||
|
||||
const handleCustomFileTypesChange = useCallback((customFileTypes: string[]) => {
|
||||
const newPayload = produce(payload, (draft) => {
|
||||
draft.allowed_file_extensions = customFileTypes.map((v) => {
|
||||
if (v.startsWith('.')) // Not start with dot
|
||||
return v.slice(1)
|
||||
return v
|
||||
})
|
||||
})
|
||||
onChange(newPayload)
|
||||
}, [onChange, payload])
|
||||
|
||||
const handleMaxUploadNumLimitChange = useCallback((value: number) => {
|
||||
const newPayload = produce(payload, (draft) => {
|
||||
draft.max_length = value
|
||||
})
|
||||
onChange(newPayload)
|
||||
}, [onChange, payload])
|
||||
|
||||
return (
|
||||
<div>
|
||||
{!inFeaturePanel && (
|
||||
<Field
|
||||
title={t('appDebug.variableConfig.file.supportFileTypes')}
|
||||
>
|
||||
<div className='space-y-1'>
|
||||
{
|
||||
[SupportUploadFileTypes.document, SupportUploadFileTypes.image, SupportUploadFileTypes.audio, SupportUploadFileTypes.video].map((type: SupportUploadFileTypes) => (
|
||||
<FileTypeItem
|
||||
key={type}
|
||||
type={type as SupportUploadFileTypes.image | SupportUploadFileTypes.document | SupportUploadFileTypes.audio | SupportUploadFileTypes.video}
|
||||
selected={allowed_file_types.includes(type)}
|
||||
onToggle={handleSupportFileTypeChange}
|
||||
/>
|
||||
))
|
||||
}
|
||||
<FileTypeItem
|
||||
type={SupportUploadFileTypes.custom}
|
||||
selected={allowed_file_types.includes(SupportUploadFileTypes.custom)}
|
||||
onToggle={handleSupportFileTypeChange}
|
||||
customFileTypes={allowed_file_extensions?.map(item => `.${item}`)}
|
||||
onCustomFileTypesChange={handleCustomFileTypesChange}
|
||||
/>
|
||||
</div>
|
||||
</Field>
|
||||
)}
|
||||
<Field
|
||||
title={t('appDebug.variableConfig.uploadFileTypes')}
|
||||
className='mt-4'
|
||||
>
|
||||
<div className='grid grid-cols-3 gap-2'>
|
||||
<OptionCard
|
||||
title={t('appDebug.variableConfig.localUpload')}
|
||||
selected={allowed_file_upload_methods.length === 1 && allowed_file_upload_methods.includes(TransferMethod.local_file)}
|
||||
onSelect={handleUploadMethodChange(TransferMethod.local_file)}
|
||||
/>
|
||||
<OptionCard
|
||||
title="URL"
|
||||
selected={allowed_file_upload_methods.length === 1 && allowed_file_upload_methods.includes(TransferMethod.remote_url)}
|
||||
onSelect={handleUploadMethodChange(TransferMethod.remote_url)}
|
||||
/>
|
||||
<OptionCard
|
||||
title={t('appDebug.variableConfig.both')}
|
||||
selected={allowed_file_upload_methods.includes(TransferMethod.local_file) && allowed_file_upload_methods.includes(TransferMethod.remote_url)}
|
||||
onSelect={handleUploadMethodChange(TransferMethod.all)}
|
||||
/>
|
||||
</div>
|
||||
</Field>
|
||||
{isMultiple && (
|
||||
<Field
|
||||
className='mt-4'
|
||||
title={t('appDebug.variableConfig.maxNumberOfUploads')!}
|
||||
>
|
||||
<div>
|
||||
<div className='mb-1.5 text-text-tertiary body-xs-regular'>{t('appDebug.variableConfig.maxNumberTip', { size: formatFileSize(FILE_SIZE_LIMIT) })}</div>
|
||||
<InputNumberWithSlider
|
||||
value={max_length}
|
||||
min={1}
|
||||
max={10}
|
||||
onChange={handleMaxUploadNumLimitChange}
|
||||
/>
|
||||
</div>
|
||||
</Field>
|
||||
)}
|
||||
{inFeaturePanel && !hideSupportFileType && (
|
||||
<Field
|
||||
title={t('appDebug.variableConfig.file.supportFileTypes')}
|
||||
className='mt-4'
|
||||
>
|
||||
<div className='space-y-1'>
|
||||
{
|
||||
[SupportUploadFileTypes.document, SupportUploadFileTypes.image, SupportUploadFileTypes.audio, SupportUploadFileTypes.video].map((type: SupportUploadFileTypes) => (
|
||||
<FileTypeItem
|
||||
key={type}
|
||||
type={type as SupportUploadFileTypes.image | SupportUploadFileTypes.document | SupportUploadFileTypes.audio | SupportUploadFileTypes.video}
|
||||
selected={allowed_file_types.includes(type)}
|
||||
onToggle={handleSupportFileTypeChange}
|
||||
/>
|
||||
))
|
||||
}
|
||||
<FileTypeItem
|
||||
type={SupportUploadFileTypes.custom}
|
||||
selected={allowed_file_types.includes(SupportUploadFileTypes.custom)}
|
||||
onToggle={handleSupportFileTypeChange}
|
||||
customFileTypes={allowed_file_extensions}
|
||||
onCustomFileTypesChange={handleCustomFileTypesChange}
|
||||
/>
|
||||
</div>
|
||||
</Field>
|
||||
)}
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(FileUploadSetting)
|
@@ -13,11 +13,11 @@ const InfoPanel: FC<Props> = ({
|
||||
}) => {
|
||||
return (
|
||||
<div>
|
||||
<div className='px-[5px] py-[3px] bg-gray-100 rounded-md'>
|
||||
<div className='leading-4 text-[10px] font-medium text-gray-500 uppercase'>
|
||||
<div className='px-[5px] py-[3px] bg-workflow-block-parma-bg rounded-md'>
|
||||
<div className='text-text-secondary system-2xs-semibold-uppercase uppercase'>
|
||||
{title}
|
||||
</div>
|
||||
<div className='leading-4 text-xs font-normal text-gray-700 break-words'>
|
||||
<div className='text-text-tertiary system-xs-regular break-words'>
|
||||
{content}
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -0,0 +1,65 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import Slider from '@/app/components/base/slider'
|
||||
|
||||
type Props = {
|
||||
value: number
|
||||
defaultValue?: number
|
||||
min?: number
|
||||
max?: number
|
||||
readonly?: boolean
|
||||
onChange: (value: number) => void
|
||||
}
|
||||
|
||||
const InputNumberWithSlider: FC<Props> = ({
|
||||
value,
|
||||
defaultValue = 0,
|
||||
min,
|
||||
max,
|
||||
readonly,
|
||||
onChange,
|
||||
}) => {
|
||||
const handleBlur = useCallback(() => {
|
||||
if (value === undefined || value === null) {
|
||||
onChange(defaultValue)
|
||||
return
|
||||
}
|
||||
if (max !== undefined && value > max) {
|
||||
onChange(max)
|
||||
return
|
||||
}
|
||||
if (min !== undefined && value < min)
|
||||
onChange(min)
|
||||
}, [defaultValue, max, min, onChange, value])
|
||||
|
||||
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
onChange(parseFloat(e.target.value))
|
||||
}, [onChange])
|
||||
|
||||
return (
|
||||
<div className='flex justify-between items-center h-8 space-x-2'>
|
||||
<input
|
||||
value={value}
|
||||
className='shrink-0 block pl-3 w-12 h-8 appearance-none outline-none rounded-lg bg-components-input-bg-normal text-[13px] text-components-input-text-filled'
|
||||
type='number'
|
||||
min={min}
|
||||
max={max}
|
||||
step={1}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
disabled={readonly}
|
||||
/>
|
||||
<Slider
|
||||
className='grow'
|
||||
value={value}
|
||||
min={min}
|
||||
max={max}
|
||||
step={1}
|
||||
onChange={onChange}
|
||||
disabled={readonly}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(InputNumberWithSlider)
|
@@ -1,9 +1,8 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { RiAlignLeft, RiCheckboxMultipleLine, RiFileCopy2Line, RiFileList2Line, RiHashtag, RiTextSnippet } from '@remixicon/react'
|
||||
import { InputVarType } from '../../../types'
|
||||
import { AlignLeft, LetterSpacing01 } from '@/app/components/base/icons/src/vender/line/editor'
|
||||
import { CheckDone01, Hash02 } from '@/app/components/base/icons/src/vender/line/general'
|
||||
|
||||
type Props = {
|
||||
className?: string
|
||||
@@ -12,11 +11,13 @@ type Props = {
|
||||
|
||||
const getIcon = (type: InputVarType) => {
|
||||
return ({
|
||||
[InputVarType.textInput]: LetterSpacing01,
|
||||
[InputVarType.paragraph]: AlignLeft,
|
||||
[InputVarType.select]: CheckDone01,
|
||||
[InputVarType.number]: Hash02,
|
||||
} as any)[type] || LetterSpacing01
|
||||
[InputVarType.textInput]: RiTextSnippet,
|
||||
[InputVarType.paragraph]: RiAlignLeft,
|
||||
[InputVarType.select]: RiCheckboxMultipleLine,
|
||||
[InputVarType.number]: RiHashtag,
|
||||
[InputVarType.singleFile]: RiFileList2Line,
|
||||
[InputVarType.multiFiles]: RiFileCopy2Line,
|
||||
} as any)[type] || RiTextSnippet
|
||||
}
|
||||
|
||||
const InputVarTypeIcon: FC<Props> = ({
|
||||
|
@@ -9,6 +9,7 @@ import cn from '@/utils/classnames'
|
||||
import Field from '@/app/components/workflow/nodes/_base/components/field'
|
||||
import Switch from '@/app/components/base/switch'
|
||||
import Slider from '@/app/components/base/slider'
|
||||
import Input from '@/app/components/base/input'
|
||||
|
||||
const i18nPrefix = 'workflow.nodes.common.memory'
|
||||
const WINDOW_SIZE_MIN = 1
|
||||
@@ -144,14 +145,14 @@ const MemoryConfig: FC<Props> = ({
|
||||
<>
|
||||
{/* window size */}
|
||||
<div className='flex justify-between'>
|
||||
<div className='flex items-center h-8 space-x-1'>
|
||||
<div className='flex items-center h-8 space-x-2'>
|
||||
<Switch
|
||||
defaultValue={payload?.window?.enabled}
|
||||
onChange={handleWindowEnabledChange}
|
||||
size='md'
|
||||
disabled={readonly}
|
||||
/>
|
||||
<div className='leading-[18px] text-xs font-medium text-gray-500 uppercase'>{t(`${i18nPrefix}.windowSize`)}</div>
|
||||
<div className='text-text-tertiary system-xs-medium-uppercase'>{t(`${i18nPrefix}.windowSize`)}</div>
|
||||
</div>
|
||||
<div className='flex items-center h-8 space-x-2'>
|
||||
<Slider
|
||||
@@ -163,16 +164,17 @@ const MemoryConfig: FC<Props> = ({
|
||||
onChange={handleWindowSizeChange}
|
||||
disabled={readonly || !payload.window?.enabled}
|
||||
/>
|
||||
<input
|
||||
<Input
|
||||
value={(payload.window?.size || WINDOW_SIZE_DEFAULT) as number}
|
||||
className='shrink-0 block ml-4 pl-3 w-12 h-8 appearance-none outline-none rounded-lg bg-gray-100 text-[13px] text-gra-900'
|
||||
wrapperClassName='w-12'
|
||||
className='pr-0 appearance-none'
|
||||
type='number'
|
||||
min={WINDOW_SIZE_MIN}
|
||||
max={WINDOW_SIZE_MAX}
|
||||
step={1}
|
||||
onChange={e => handleWindowSizeChange(e.target.value)}
|
||||
onBlur={handleBlur}
|
||||
disabled={readonly}
|
||||
disabled={readonly || !payload.window?.enabled}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -24,7 +24,6 @@ import {
|
||||
import {
|
||||
useStore,
|
||||
} from '../../../store'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
|
||||
type NodeHandleProps = {
|
||||
handleId: string
|
||||
@@ -154,10 +153,23 @@ export const NodeSourceHandle = memo(({
|
||||
}, [notInitialWorkflow, data.type, isChatMode])
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
popupContent={(
|
||||
<Handle
|
||||
id={handleId}
|
||||
type='source'
|
||||
position={Position.Right}
|
||||
className={`
|
||||
group/handle !w-4 !h-4 !bg-transparent !rounded-none !outline-none !border-none z-[1]
|
||||
after:absolute after:w-0.5 after:h-2 after:right-1.5 after:top-1 after:bg-primary-500
|
||||
hover:scale-125 transition-all
|
||||
${!connected && 'after:opacity-0'}
|
||||
${handleClassName}
|
||||
`}
|
||||
isConnectable={isConnectable}
|
||||
onClick={handleHandleClick}
|
||||
>
|
||||
<div className='hidden group-hover/handle:block absolute left-1/2 -top-1 -translate-y-full -translate-x-1/2 p-1.5 border-[0.5px] border-components-panel-border bg-components-tooltip-bg rounded-lg shadow-lg'>
|
||||
<div className='system-xs-regular text-text-tertiary'>
|
||||
<div>
|
||||
<div className=' whitespace-nowrap'>
|
||||
<span className='system-xs-medium text-text-secondary'>{t('workflow.common.parallelTip.click.title')}</span>
|
||||
{t('workflow.common.parallelTip.click.desc')}
|
||||
</div>
|
||||
@@ -166,42 +178,26 @@ export const NodeSourceHandle = memo(({
|
||||
{t('workflow.common.parallelTip.drag.desc')}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<Handle
|
||||
id={handleId}
|
||||
type='source'
|
||||
position={Position.Right}
|
||||
className={`
|
||||
!w-4 !h-4 !bg-transparent !rounded-none !outline-none !border-none z-[1]
|
||||
after:absolute after:w-0.5 after:h-2 after:right-1.5 after:top-1 after:bg-primary-500
|
||||
hover:scale-125 transition-all
|
||||
${!connected && 'after:opacity-0'}
|
||||
${handleClassName}
|
||||
`}
|
||||
isConnectable={isConnectable}
|
||||
onClick={handleHandleClick}
|
||||
>
|
||||
{
|
||||
isConnectable && !getNodesReadOnly() && (
|
||||
<BlockSelector
|
||||
open={open}
|
||||
onOpenChange={handleOpenChange}
|
||||
onSelect={handleSelect}
|
||||
asChild
|
||||
triggerClassName={open => `
|
||||
hidden absolute top-0 left-0 pointer-events-none
|
||||
${nodeSelectorClassName}
|
||||
group-hover:!flex
|
||||
${data.selected && '!flex'}
|
||||
${open && '!flex'}
|
||||
`}
|
||||
availableBlocksTypes={availableNextBlocks}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</Handle>
|
||||
</Tooltip>
|
||||
</div>
|
||||
{
|
||||
isConnectable && !getNodesReadOnly() && (
|
||||
<BlockSelector
|
||||
open={open}
|
||||
onOpenChange={handleOpenChange}
|
||||
onSelect={handleSelect}
|
||||
asChild
|
||||
triggerClassName={open => `
|
||||
hidden absolute top-0 left-0 pointer-events-none
|
||||
${nodeSelectorClassName}
|
||||
group-hover:!flex
|
||||
${data.selected && '!flex'}
|
||||
${open && '!flex'}
|
||||
`}
|
||||
availableBlocksTypes={availableNextBlocks}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</Handle>
|
||||
)
|
||||
})
|
||||
NodeSourceHandle.displayName = 'NodeSourceHandle'
|
||||
|
@@ -58,6 +58,7 @@ type Props = {
|
||||
}
|
||||
nodesOutputVars?: NodeOutPutVar[]
|
||||
availableNodes?: Node[]
|
||||
isSupportFileVar?: boolean
|
||||
isSupportPromptGenerator?: boolean
|
||||
onGenerated?: (prompt: string) => void
|
||||
modelConfig?: ModelConfig
|
||||
@@ -86,6 +87,7 @@ const Editor: FC<Props> = ({
|
||||
hasSetBlockStatus,
|
||||
nodesOutputVars,
|
||||
availableNodes = [],
|
||||
isSupportFileVar,
|
||||
isSupportPromptGenerator,
|
||||
isSupportJinja,
|
||||
editionType,
|
||||
@@ -245,6 +247,7 @@ const Editor: FC<Props> = ({
|
||||
onBlur={setBlur}
|
||||
onFocus={setFocus}
|
||||
editable={!readOnly}
|
||||
isSupportFileVar={isSupportFileVar}
|
||||
/>
|
||||
{/* to patch Editor not support dynamic change editable status */}
|
||||
{readOnly && <div className='absolute inset-0 z-10'></div>}
|
||||
|
@@ -11,7 +11,7 @@ const Split: FC<Props> = ({
|
||||
className,
|
||||
}) => {
|
||||
return (
|
||||
<div className={cn(className, 'h-[0.5px] bg-black/5')}>
|
||||
<div className={cn(className, 'h-[0.5px] bg-divider-subtle')}>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@@ -21,11 +21,13 @@ import cn from '@/utils/classnames'
|
||||
type VariableTagProps = {
|
||||
valueSelector: ValueSelector
|
||||
varType: VarType
|
||||
isShort?: boolean
|
||||
availableNodes?: Node[]
|
||||
}
|
||||
const VariableTag = ({
|
||||
valueSelector,
|
||||
varType,
|
||||
isShort,
|
||||
availableNodes,
|
||||
}: VariableTagProps) => {
|
||||
const nodes = useNodes<CommonNodeType>()
|
||||
@@ -76,7 +78,7 @@ const VariableTag = ({
|
||||
{variableName}
|
||||
</div>
|
||||
{
|
||||
varType && (
|
||||
!isShort && varType && (
|
||||
<div className='shrink-0 ml-0.5 text-text-tertiary'>{capitalize(varType)}</div>
|
||||
)
|
||||
}
|
||||
|
@@ -6,6 +6,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import type { OutputVar } from '../../../code/types'
|
||||
import RemoveButton from '../remove-button'
|
||||
import VarTypePicker from './var-type-picker'
|
||||
import Input from '@/app/components/base/input'
|
||||
import type { VarType } from '@/app/components/workflow/types'
|
||||
import { checkKeys } from '@/utils/var'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
@@ -85,12 +86,12 @@ const OutputVarList: FC<Props> = ({
|
||||
<div className='space-y-2'>
|
||||
{list.map((item, index) => (
|
||||
<div className='flex items-center space-x-1' key={index}>
|
||||
<input
|
||||
<Input
|
||||
readOnly={readonly}
|
||||
value={item.variable}
|
||||
onChange={handleVarNameChange(index)}
|
||||
className='w-0 grow h-8 leading-8 px-2.5 rounded-lg border-0 bg-gray-100 text-gray-900 text-[13px] placeholder:text-gray-400 focus:outline-none focus:ring-1 focus:ring-inset focus:ring-gray-200'
|
||||
type='text' />
|
||||
wrapperClassName='grow'
|
||||
/>
|
||||
<VarTypePicker
|
||||
readonly={readonly}
|
||||
value={item.variable_type}
|
||||
|
@@ -13,6 +13,9 @@ import { VarType as ToolVarType } from '../../../tool/types'
|
||||
import type { ToolNodeType } from '../../../tool/types'
|
||||
import type { ParameterExtractorNodeType } from '../../../parameter-extractor/types'
|
||||
import type { IterationNodeType } from '../../../iteration/types'
|
||||
import type { ListFilterNodeType } from '../../../list-operator/types'
|
||||
import { OUTPUT_FILE_SUB_VARIABLES } from '../../../if-else/default'
|
||||
import type { DocExtractorNodeType } from '../../../document-extractor/types'
|
||||
import { BlockEnum, InputVarType, VarType } from '@/app/components/workflow/types'
|
||||
import type { StartNodeType } from '@/app/components/workflow/nodes/start/types'
|
||||
import type { ConversationVariable, EnvironmentVariable, Node, NodeOutPutVar, ValueSelector, Var } from '@/app/components/workflow/types'
|
||||
@@ -43,24 +46,25 @@ export const isConversationVar = (valueSelector: ValueSelector) => {
|
||||
}
|
||||
|
||||
const inputVarTypeToVarType = (type: InputVarType): VarType => {
|
||||
if (type === InputVarType.number)
|
||||
return VarType.number
|
||||
|
||||
return VarType.string
|
||||
return ({
|
||||
[InputVarType.number]: VarType.number,
|
||||
[InputVarType.singleFile]: VarType.file,
|
||||
[InputVarType.multiFiles]: VarType.arrayFile,
|
||||
} as any)[type] || VarType.string
|
||||
}
|
||||
|
||||
const findExceptVarInObject = (obj: any, filterVar: (payload: Var, selector: ValueSelector) => boolean, value_selector: ValueSelector): Var => {
|
||||
const findExceptVarInObject = (obj: any, filterVar: (payload: Var, selector: ValueSelector) => boolean, value_selector: ValueSelector, isFile?: boolean): Var => {
|
||||
const { children } = obj
|
||||
const res: Var = {
|
||||
variable: obj.variable,
|
||||
type: VarType.object,
|
||||
type: isFile ? VarType.file : VarType.object,
|
||||
children: children.filter((item: Var) => {
|
||||
const { children } = item
|
||||
const currSelector = [...value_selector, item.variable]
|
||||
if (!children)
|
||||
return filterVar(item, currSelector)
|
||||
|
||||
const obj = findExceptVarInObject(item, filterVar, currSelector)
|
||||
const obj = findExceptVarInObject(item, filterVar, currSelector, false) // File doesn't contains file children
|
||||
return obj.children && obj.children?.length > 0
|
||||
}),
|
||||
}
|
||||
@@ -258,6 +262,37 @@ const formatItem = (
|
||||
break
|
||||
}
|
||||
|
||||
case BlockEnum.DocExtractor: {
|
||||
res.vars = [
|
||||
{
|
||||
variable: 'text',
|
||||
type: (data as DocExtractorNodeType).is_array_file ? VarType.arrayString : VarType.string,
|
||||
},
|
||||
]
|
||||
break
|
||||
}
|
||||
|
||||
case BlockEnum.ListFilter: {
|
||||
if (!(data as ListFilterNodeType).var_type)
|
||||
break
|
||||
|
||||
res.vars = [
|
||||
{
|
||||
variable: 'result',
|
||||
type: (data as ListFilterNodeType).var_type,
|
||||
},
|
||||
{
|
||||
variable: 'first_record',
|
||||
type: (data as ListFilterNodeType).item_var_type,
|
||||
},
|
||||
{
|
||||
variable: 'last_record',
|
||||
type: (data as ListFilterNodeType).item_var_type,
|
||||
},
|
||||
]
|
||||
break
|
||||
}
|
||||
|
||||
case 'env': {
|
||||
res.vars = data.envList.map((env: EnvironmentVariable) => {
|
||||
return {
|
||||
@@ -282,26 +317,55 @@ const formatItem = (
|
||||
|
||||
const selector = [id]
|
||||
res.vars = res.vars.filter((v) => {
|
||||
const { children } = v
|
||||
if (!children) {
|
||||
return filterVar(v, (() => {
|
||||
const variableArr = v.variable.split('.')
|
||||
const [first, ..._other] = variableArr
|
||||
if (first === 'sys' || first === 'env' || first === 'conversation')
|
||||
return variableArr
|
||||
const isCurrentMatched = filterVar(v, (() => {
|
||||
const variableArr = v.variable.split('.')
|
||||
const [first, ..._other] = variableArr
|
||||
if (first === 'sys' || first === 'env' || first === 'conversation')
|
||||
return variableArr
|
||||
|
||||
return [...selector, ...variableArr]
|
||||
})())
|
||||
}
|
||||
return [...selector, ...variableArr]
|
||||
})())
|
||||
if (isCurrentMatched)
|
||||
return true
|
||||
|
||||
const obj = findExceptVarInObject(v, filterVar, selector)
|
||||
const isFile = v.type === VarType.file
|
||||
const children = (() => {
|
||||
if (isFile) {
|
||||
return OUTPUT_FILE_SUB_VARIABLES.map((key) => {
|
||||
return {
|
||||
variable: key,
|
||||
type: key === 'size' ? VarType.number : VarType.string,
|
||||
}
|
||||
})
|
||||
}
|
||||
return v.children
|
||||
})()
|
||||
if (!children)
|
||||
return false
|
||||
|
||||
const obj = findExceptVarInObject(isFile ? { ...v, children } : v, filterVar, selector, isFile)
|
||||
return obj?.children && obj?.children.length > 0
|
||||
}).map((v) => {
|
||||
const { children } = v
|
||||
const isFile = v.type === VarType.file
|
||||
|
||||
const { children } = (() => {
|
||||
if (isFile) {
|
||||
return {
|
||||
children: OUTPUT_FILE_SUB_VARIABLES.map((key) => {
|
||||
return {
|
||||
variable: key,
|
||||
type: key === 'size' ? VarType.number : VarType.string,
|
||||
}
|
||||
}),
|
||||
}
|
||||
}
|
||||
return v
|
||||
})()
|
||||
|
||||
if (!children)
|
||||
return v
|
||||
|
||||
return findExceptVarInObject(v, filterVar, selector)
|
||||
return findExceptVarInObject(isFile ? { ...v, children } : v, filterVar, selector, isFile)
|
||||
})
|
||||
|
||||
return res
|
||||
@@ -334,7 +398,7 @@ export const toNodeOutputVars = (
|
||||
const res = [
|
||||
...nodes.filter(node => SUPPORT_OUTPUT_VARS_NODE.includes(node.data.type)),
|
||||
...(environmentVariables.length > 0 ? [ENV_NODE] : []),
|
||||
...((isChatMode && conversationVariables.length) > 0 ? [CHAT_VAR_NODE] : []),
|
||||
...((isChatMode && conversationVariables.length > 0) ? [CHAT_VAR_NODE] : []),
|
||||
].map((node) => {
|
||||
return {
|
||||
...formatItem(node, isChatMode, filterVar),
|
||||
@@ -352,29 +416,32 @@ const getIterationItemType = ({
|
||||
beforeNodesOutputVars: NodeOutPutVar[]
|
||||
}): VarType => {
|
||||
const outputVarNodeId = valueSelector[0]
|
||||
const targetVar = beforeNodesOutputVars.find(v => v.nodeId === outputVarNodeId)
|
||||
const isSystem = isSystemVar(valueSelector)
|
||||
|
||||
const targetVar = isSystem ? beforeNodesOutputVars.find(v => v.isStartNode) : beforeNodesOutputVars.find(v => v.nodeId === outputVarNodeId)
|
||||
if (!targetVar)
|
||||
return VarType.string
|
||||
|
||||
let arrayType: VarType = VarType.string
|
||||
|
||||
const isSystem = isSystemVar(valueSelector)
|
||||
let curr: any = targetVar.vars
|
||||
if (isSystem) {
|
||||
arrayType = curr.find((v: any) => v.variable === (valueSelector).join('.'))?.type
|
||||
}
|
||||
else {
|
||||
(valueSelector).slice(1).forEach((key, i) => {
|
||||
const isLast = i === valueSelector.length - 2
|
||||
curr = curr?.find((v: any) => v.variable === key)
|
||||
if (isLast) {
|
||||
arrayType = curr?.type
|
||||
}
|
||||
else {
|
||||
if (curr?.type === VarType.object || curr?.type === VarType.file)
|
||||
curr = curr.children
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (isSystem)
|
||||
return curr.find((v: any) => v.variable === (valueSelector).join('.'))?.type;
|
||||
|
||||
(valueSelector).slice(1).forEach((key, i) => {
|
||||
const isLast = i === valueSelector.length - 2
|
||||
curr = curr?.find((v: any) => v.variable === key)
|
||||
if (isLast) {
|
||||
arrayType = curr?.type
|
||||
}
|
||||
else {
|
||||
if (curr?.type === VarType.object)
|
||||
curr = curr.children
|
||||
}
|
||||
})
|
||||
switch (arrayType as VarType) {
|
||||
case VarType.arrayString:
|
||||
return VarType.string
|
||||
@@ -385,7 +452,7 @@ const getIterationItemType = ({
|
||||
case VarType.array:
|
||||
return VarType.any
|
||||
case VarType.arrayFile:
|
||||
return VarType.object
|
||||
return VarType.file
|
||||
default:
|
||||
return VarType.string
|
||||
}
|
||||
@@ -466,7 +533,7 @@ export const getVarType = ({
|
||||
type = curr?.type
|
||||
}
|
||||
else {
|
||||
if (curr?.type === VarType.object)
|
||||
if (curr?.type === VarType.object || curr?.type === VarType.file)
|
||||
curr = curr.children
|
||||
}
|
||||
})
|
||||
@@ -514,6 +581,16 @@ export const toNodeAvailableVars = ({
|
||||
environmentVariables,
|
||||
conversationVariables,
|
||||
})
|
||||
const itemChildren = itemType === VarType.file
|
||||
? {
|
||||
children: OUTPUT_FILE_SUB_VARIABLES.map((key) => {
|
||||
return {
|
||||
variable: key,
|
||||
type: key === 'size' ? VarType.number : VarType.string,
|
||||
}
|
||||
}),
|
||||
}
|
||||
: {}
|
||||
const iterationVar = {
|
||||
nodeId: iterationNode?.id,
|
||||
title: t('workflow.nodes.iteration.currentIteration'),
|
||||
@@ -521,6 +598,7 @@ export const toNodeAvailableVars = ({
|
||||
{
|
||||
variable: 'item',
|
||||
type: itemType,
|
||||
...itemChildren,
|
||||
},
|
||||
{
|
||||
variable: 'index',
|
||||
@@ -603,7 +681,7 @@ export const getNodeUsedVars = (node: Node): ValueSelector[] => {
|
||||
}
|
||||
case BlockEnum.IfElse: {
|
||||
res = (data as IfElseNodeType).conditions?.map((c) => {
|
||||
return c.variable_selector
|
||||
return c.variable_selector || []
|
||||
}) || []
|
||||
break
|
||||
}
|
||||
@@ -628,7 +706,7 @@ export const getNodeUsedVars = (node: Node): ValueSelector[] => {
|
||||
}
|
||||
case BlockEnum.HttpRequest: {
|
||||
const payload = (data as HttpNodeType)
|
||||
res = matchNotSystemVars([payload.url, payload.headers, payload.params, payload.body.data])
|
||||
res = matchNotSystemVars([payload.url, payload.headers, payload.params, typeof payload.body.data === 'string' ? payload.body.data : payload.body.data.map(d => d.value).join('')])
|
||||
break
|
||||
}
|
||||
case BlockEnum.Tool: {
|
||||
@@ -665,7 +743,7 @@ export const getNodeUsedVars = (node: Node): ValueSelector[] => {
|
||||
return res || []
|
||||
}
|
||||
|
||||
// used can be used in iteration node
|
||||
// can be used in iteration node
|
||||
export const getNodeUsedVarPassToServerKey = (node: Node, valueSelector: ValueSelector): string | string[] => {
|
||||
const { data } = node
|
||||
const { type } = data
|
||||
@@ -684,7 +762,7 @@ export const getNodeUsedVarPassToServerKey = (node: Node, valueSelector: ValueSe
|
||||
break
|
||||
}
|
||||
case BlockEnum.IfElse: {
|
||||
const targetVar = (data as IfElseNodeType).conditions?.find(c => c.variable_selector.join('.') === valueSelector.join('.'))
|
||||
const targetVar = (data as IfElseNodeType).conditions?.find(c => c.variable_selector?.join('.') === valueSelector.join('.'))
|
||||
if (targetVar)
|
||||
res = `#${valueSelector.join('.')}#`
|
||||
break
|
||||
@@ -805,7 +883,7 @@ export const updateNodeVars = (oldNode: Node, oldVarSelector: ValueSelector, new
|
||||
const payload = data as IfElseNodeType
|
||||
if (payload.conditions) {
|
||||
payload.conditions = payload.conditions.map((c) => {
|
||||
if (c.variable_selector.join('.') === oldVarSelector.join('.'))
|
||||
if (c.variable_selector?.join('.') === oldVarSelector.join('.'))
|
||||
c.variable_selector = newVarSelector
|
||||
return c
|
||||
})
|
||||
@@ -846,7 +924,17 @@ export const updateNodeVars = (oldNode: Node, oldVarSelector: ValueSelector, new
|
||||
payload.url = replaceOldVarInText(payload.url, oldVarSelector, newVarSelector)
|
||||
payload.headers = replaceOldVarInText(payload.headers, oldVarSelector, newVarSelector)
|
||||
payload.params = replaceOldVarInText(payload.params, oldVarSelector, newVarSelector)
|
||||
payload.body.data = replaceOldVarInText(payload.body.data, oldVarSelector, newVarSelector)
|
||||
if (typeof payload.body.data === 'string') {
|
||||
payload.body.data = replaceOldVarInText(payload.body.data, oldVarSelector, newVarSelector)
|
||||
}
|
||||
else {
|
||||
payload.body.data = payload.body.data.map((d) => {
|
||||
return {
|
||||
...d,
|
||||
value: replaceOldVarInText(d.value || '', oldVarSelector, newVarSelector),
|
||||
}
|
||||
})
|
||||
}
|
||||
break
|
||||
}
|
||||
case BlockEnum.Tool: {
|
||||
@@ -1023,6 +1111,18 @@ export const getNodeOutputVars = (node: Node, isChatMode: boolean): ValueSelecto
|
||||
res.push([id, 'output'])
|
||||
break
|
||||
}
|
||||
|
||||
case BlockEnum.DocExtractor: {
|
||||
res.push([id, 'text'])
|
||||
break
|
||||
}
|
||||
|
||||
case BlockEnum.ListFilter: {
|
||||
res.push([id, 'result'])
|
||||
res.push([id, 'first_record'])
|
||||
res.push([id, 'last_record'])
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return res
|
||||
|
@@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import produce from 'immer'
|
||||
import RemoveButton from '../remove-button'
|
||||
import VarReferencePicker from './var-reference-picker'
|
||||
import Input from '@/app/components/base/input'
|
||||
import type { ValueSelector, Var, Variable } from '@/app/components/workflow/types'
|
||||
import { VarType as VarKindType } from '@/app/components/workflow/nodes/tool/types'
|
||||
|
||||
@@ -75,13 +76,12 @@ const VarList: FC<Props> = ({
|
||||
<div className='space-y-2'>
|
||||
{list.map((item, index) => (
|
||||
<div className='flex items-center space-x-1' key={index}>
|
||||
<input
|
||||
readOnly={readonly}
|
||||
<Input
|
||||
wrapperClassName='w-[120px]'
|
||||
disabled={readonly}
|
||||
value={list[index].variable}
|
||||
onChange={handleVarNameChange(index)}
|
||||
placeholder={t('workflow.common.variableNamePlaceholder')!}
|
||||
className='w-[120px] h-8 leading-8 px-2.5 rounded-lg border-0 bg-gray-100 text-gray-900 text-[13px] placeholder:text-gray-400 focus:outline-none focus:ring-1 focus:ring-inset focus:ring-gray-200'
|
||||
type='text'
|
||||
/>
|
||||
<VarReferencePicker
|
||||
nodeId={nodeId}
|
||||
|
@@ -8,7 +8,8 @@ import {
|
||||
RiErrorWarningFill,
|
||||
} from '@remixicon/react'
|
||||
import produce from 'immer'
|
||||
import { useEdges, useStoreApi } from 'reactflow'
|
||||
import { 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 } from './utils'
|
||||
@@ -41,7 +42,7 @@ const TRIGGER_DEFAULT_WIDTH = 227
|
||||
type Props = {
|
||||
className?: string
|
||||
nodeId: string
|
||||
isShowNodeName: boolean
|
||||
isShowNodeName?: boolean
|
||||
readonly: boolean
|
||||
value: ValueSelector | string
|
||||
onChange: (value: ValueSelector | string, varKindType: VarKindType, varInfo?: Var) => void
|
||||
@@ -55,13 +56,16 @@ type Props = {
|
||||
isAddBtnTrigger?: boolean
|
||||
schema?: Partial<CredentialFormSchema>
|
||||
valueTypePlaceHolder?: string
|
||||
isInTable?: boolean
|
||||
onRemove?: () => void
|
||||
typePlaceHolder?: string
|
||||
}
|
||||
|
||||
const VarReferencePicker: FC<Props> = ({
|
||||
nodeId,
|
||||
readonly,
|
||||
className,
|
||||
isShowNodeName,
|
||||
isShowNodeName = true,
|
||||
value = [],
|
||||
onOpen = () => { },
|
||||
onChange,
|
||||
@@ -74,13 +78,15 @@ const VarReferencePicker: FC<Props> = ({
|
||||
isAddBtnTrigger,
|
||||
schema,
|
||||
valueTypePlaceHolder,
|
||||
isInTable,
|
||||
onRemove,
|
||||
typePlaceHolder,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const store = useStoreApi()
|
||||
const {
|
||||
getNodes,
|
||||
} = store.getState()
|
||||
const edges = useEdges()
|
||||
const isChatMode = useIsChatMode()
|
||||
|
||||
const { getCurrentVariableType } = useWorkflowVariables()
|
||||
@@ -219,7 +225,7 @@ const VarReferencePicker: FC<Props> = ({
|
||||
isChatVar,
|
||||
isValidVar,
|
||||
}
|
||||
}, [value, edges, outputVarNode])
|
||||
}, [value, outputVarNode])
|
||||
|
||||
// 8(left/right-padding) + 14(icon) + 4 + 14 + 2 = 42 + 17 buff
|
||||
const availableWidth = triggerWidth - 56
|
||||
@@ -245,114 +251,128 @@ const VarReferencePicker: FC<Props> = ({
|
||||
if (readonly)
|
||||
return
|
||||
!isConstant ? setOpen(!open) : setControlFocus(Date.now())
|
||||
}} className='!flex'>
|
||||
{isAddBtnTrigger
|
||||
? (
|
||||
<div>
|
||||
<AddButton onClick={() => { }}></AddButton>
|
||||
</div>
|
||||
)
|
||||
: (<div ref={!isSupportConstantValue ? triggerRef : null} className={cn((open || isFocus) ? 'border-gray-300' : 'border-gray-100', 'relative group/wrap flex items-center w-full h-8', !isSupportConstantValue && 'p-1 rounded-lg bg-gray-100 border')}>
|
||||
{isSupportConstantValue
|
||||
? <div onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
setOpen(false)
|
||||
setControlFocus(Date.now())
|
||||
}} className='h-full mr-1 flex items-center space-x-1'>
|
||||
<TypeSelector
|
||||
noLeft
|
||||
trigger={
|
||||
<div className='flex items-center h-8 px-2 radius-md bg-components-input-bg-normal'>
|
||||
<div className='mr-1 system-sm-regular text-components-input-text-filled'>{varKindTypes.find(item => item.value === varKindType)?.label}</div>
|
||||
<RiArrowDownSLine className='w-4 h-4 text-text-quaternary' />
|
||||
</div>
|
||||
}
|
||||
popupClassName='top-8'
|
||||
readonly={readonly}
|
||||
value={varKindType}
|
||||
options={varKindTypes}
|
||||
onChange={handleVarKindTypeChange}
|
||||
showChecked
|
||||
/>
|
||||
}} className='!flex group/picker-trigger-wrap relative'>
|
||||
<>
|
||||
{isAddBtnTrigger
|
||||
? (
|
||||
<div>
|
||||
<AddButton onClick={() => { }}></AddButton>
|
||||
</div>
|
||||
: (!hasValue && <div className='ml-1.5 mr-1'>
|
||||
<Variable02 className='w-3.5 h-3.5 text-gray-400' />
|
||||
</div>)}
|
||||
{isConstant
|
||||
? (
|
||||
<ConstantField
|
||||
value={value as string}
|
||||
onChange={onChange as ((value: string | number, varKindType: VarKindType, varInfo?: Var) => void)}
|
||||
schema={schema as CredentialFormSchema}
|
||||
readonly={readonly}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<VarPickerWrap
|
||||
onClick={() => {
|
||||
if (readonly)
|
||||
return
|
||||
!isConstant ? setOpen(!open) : setControlFocus(Date.now())
|
||||
}}
|
||||
className='grow h-full'
|
||||
>
|
||||
<div ref={isSupportConstantValue ? triggerRef : null} className={cn('h-full', isSupportConstantValue && 'flex items-center pl-1 py-1 rounded-lg bg-gray-100')}>
|
||||
<Tooltip popupContent={!isValidVar && hasValue && t('workflow.errorMsg.invalidVariable')}>
|
||||
<div className={cn('h-full items-center px-1.5 rounded-[5px] ', hasValue ? 'bg-white inline-flex' : 'flex',
|
||||
!isValidVar && hasValue && 'border border-red-400 !bg-[#FEF3F2]',
|
||||
)}>
|
||||
{hasValue
|
||||
? (
|
||||
<>
|
||||
{isShowNodeName && !isEnv && !isChatVar && (
|
||||
<div className='flex items-center'>
|
||||
<div className='px-[1px] h-3'>
|
||||
{outputVarNode?.type && <VarBlockIcon
|
||||
className='!text-gray-900'
|
||||
type={outputVarNode.type}
|
||||
/>}
|
||||
</div>
|
||||
<div className='mx-0.5 text-xs font-medium text-gray-700 truncate' title={outputVarNode?.title} style={{
|
||||
maxWidth: maxNodeNameWidth,
|
||||
}}>{outputVarNode?.title}</div>
|
||||
<Line3 className='mr-0.5'></Line3>
|
||||
</div>
|
||||
)}
|
||||
<div className='flex items-center text-primary-600'>
|
||||
{!hasValue && <Variable02 className='w-3.5 h-3.5' />}
|
||||
{isEnv && <Env className='w-3.5 h-3.5 text-util-colors-violet-violet-600' />}
|
||||
{isChatVar && <BubbleX className='w-3.5 h-3.5 text-util-colors-teal-teal-700' />}
|
||||
<div className={cn('ml-0.5 text-xs font-medium truncate', (isEnv || isChatVar) && '!text-text-secondary')} title={varName} style={{
|
||||
maxWidth: maxVarNameWidth,
|
||||
}}>{varName}</div>
|
||||
</div>
|
||||
<div className='ml-0.5 text-xs font-normal text-gray-500 capitalize truncate' title={type} style={{
|
||||
maxWidth: maxTypeWidth,
|
||||
}}>{type}</div>
|
||||
{!isValidVar && <RiErrorWarningFill className='ml-0.5 w-3 h-3 text-[#D92D20]' /> }
|
||||
</>
|
||||
)
|
||||
: <div className='text-[13px] font-normal text-gray-400'>{t('workflow.common.setVarValuePlaceholder')}</div>}
|
||||
)
|
||||
: (<div ref={!isSupportConstantValue ? triggerRef : null} className={cn((open || isFocus) ? 'border-gray-300' : 'border-gray-100', 'relative group/wrap flex items-center w-full h-8', !isSupportConstantValue && 'p-1 rounded-lg bg-gray-100 border', isInTable && 'bg-transparent border-none')}>
|
||||
{isSupportConstantValue
|
||||
? <div onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
setOpen(false)
|
||||
setControlFocus(Date.now())
|
||||
}} className='h-full mr-1 flex items-center space-x-1'>
|
||||
<TypeSelector
|
||||
noLeft
|
||||
trigger={
|
||||
<div className='flex items-center h-8 px-2 radius-md bg-components-input-bg-normal'>
|
||||
<div className='mr-1 system-sm-regular text-components-input-text-filled'>{varKindTypes.find(item => item.value === varKindType)?.label}</div>
|
||||
<RiArrowDownSLine className='w-4 h-4 text-text-quaternary' />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
}
|
||||
popupClassName='top-8'
|
||||
readonly={readonly}
|
||||
value={varKindType}
|
||||
options={varKindTypes}
|
||||
onChange={handleVarKindTypeChange}
|
||||
showChecked
|
||||
/>
|
||||
</div>
|
||||
: (!hasValue && <div className='ml-1.5 mr-1'>
|
||||
<Variable02 className='w-3.5 h-3.5 text-gray-400' />
|
||||
</div>)}
|
||||
{isConstant
|
||||
? (
|
||||
<ConstantField
|
||||
value={value as string}
|
||||
onChange={onChange as ((value: string | number, varKindType: VarKindType, varInfo?: Var) => void)}
|
||||
schema={schema as CredentialFormSchema}
|
||||
readonly={readonly}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<VarPickerWrap
|
||||
onClick={() => {
|
||||
if (readonly)
|
||||
return
|
||||
!isConstant ? setOpen(!open) : setControlFocus(Date.now())
|
||||
}}
|
||||
className='grow h-full'
|
||||
>
|
||||
<div ref={isSupportConstantValue ? triggerRef : null} className={cn('h-full', isSupportConstantValue && 'flex items-center pl-1 py-1 rounded-lg bg-gray-100')}>
|
||||
<Tooltip popupContent={!isValidVar && hasValue && t('workflow.errorMsg.invalidVariable')}>
|
||||
<div className={cn('h-full items-center px-1.5 rounded-[5px]', hasValue ? 'bg-white inline-flex' : 'flex')}>
|
||||
{hasValue
|
||||
? (
|
||||
<>
|
||||
{isShowNodeName && !isEnv && !isChatVar && (
|
||||
<div className='flex items-center'>
|
||||
<div className='px-[1px] h-3'>
|
||||
{outputVarNode?.type && <VarBlockIcon
|
||||
className='!text-gray-900'
|
||||
type={outputVarNode.type}
|
||||
/>}
|
||||
</div>
|
||||
<div className='mx-0.5 text-xs font-medium text-gray-700 truncate' title={outputVarNode?.title} style={{
|
||||
maxWidth: maxNodeNameWidth,
|
||||
}}>{outputVarNode?.title}</div>
|
||||
<Line3 className='mr-0.5'></Line3>
|
||||
</div>
|
||||
)}
|
||||
<div className='flex items-center text-primary-600'>
|
||||
{!hasValue && <Variable02 className='w-3.5 h-3.5' />}
|
||||
{isEnv && <Env className='w-3.5 h-3.5 text-util-colors-violet-violet-600' />}
|
||||
{isChatVar && <BubbleX className='w-3.5 h-3.5 text-util-colors-teal-teal-700' />}
|
||||
<div className={cn('ml-0.5 text-xs font-medium truncate', (isEnv || isChatVar) && '!text-text-secondary')} title={varName} style={{
|
||||
maxWidth: maxVarNameWidth,
|
||||
}}>{varName}</div>
|
||||
</div>
|
||||
<div className='ml-0.5 text-xs font-normal text-gray-500 capitalize truncate' title={type} style={{
|
||||
maxWidth: maxTypeWidth,
|
||||
}}>{type}</div>
|
||||
{!isValidVar && <RiErrorWarningFill className='ml-0.5 w-3 h-3 text-[#D92D20]' />}
|
||||
</>
|
||||
)
|
||||
: <div className='text-[13px] font-normal text-gray-400'>{t('workflow.common.setVarValuePlaceholder')}</div>}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
</VarPickerWrap>
|
||||
</VarPickerWrap>
|
||||
)}
|
||||
{(hasValue && !readonly && !isInTable) && (<div
|
||||
className='invisible group-hover/wrap:visible absolute h-5 right-1 top-[50%] translate-y-[-50%] group p-1 rounded-md hover:bg-black/5 cursor-pointer'
|
||||
onClick={handleClearVar}
|
||||
>
|
||||
<RiCloseLine className='w-3.5 h-3.5 text-gray-500 group-hover:text-gray-800' />
|
||||
</div>)}
|
||||
{!hasValue && valueTypePlaceHolder && (
|
||||
<Badge
|
||||
className=' absolute right-1 top-[50%] translate-y-[-50%] capitalize'
|
||||
text={valueTypePlaceHolder}
|
||||
uppercase={false}
|
||||
/>
|
||||
)}
|
||||
{(hasValue && !readonly) && (<div
|
||||
className='invisible group-hover/wrap:visible absolute h-5 right-1 top-[50%] translate-y-[-50%] group p-1 rounded-md hover:bg-black/5 cursor-pointer'
|
||||
onClick={handleClearVar}
|
||||
>
|
||||
<RiCloseLine className='w-3.5 h-3.5 text-gray-500 group-hover:text-gray-800' />
|
||||
</div>)}
|
||||
{!hasValue && valueTypePlaceHolder && (
|
||||
<Badge
|
||||
className=' absolute right-1 top-[50%] translate-y-[-50%] capitalize'
|
||||
text={valueTypePlaceHolder}
|
||||
uppercase={false}
|
||||
/>
|
||||
)}
|
||||
</div>)}
|
||||
{!readonly && isInTable && (
|
||||
<RemoveButton
|
||||
className='group-hover/picker-trigger-wrap:block hidden absolute right-1 top-0.5'
|
||||
onClick={() => onRemove?.()}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!hasValue && typePlaceHolder && (
|
||||
<Badge
|
||||
className='absolute right-2 top-1.5'
|
||||
text={typePlaceHolder}
|
||||
uppercase={false}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
</WrapElem>
|
||||
<PortalToFollowElemContent style={{
|
||||
zIndex: 100,
|
||||
|
@@ -23,7 +23,9 @@ const VarReferencePopup: FC<Props> = ({
|
||||
searchBoxClassName='mt-1'
|
||||
vars={vars}
|
||||
onChange={onChange}
|
||||
itemWidth={itemWidth} />
|
||||
itemWidth={itemWidth}
|
||||
isSupportFileVar
|
||||
/>
|
||||
</div >
|
||||
)
|
||||
}
|
||||
|
@@ -1,10 +1,7 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import { useBoolean, useHover } from 'ahooks'
|
||||
import {
|
||||
RiSearchLine,
|
||||
} from '@remixicon/react'
|
||||
import { useHover } from 'ahooks'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import cn from '@/utils/classnames'
|
||||
import { type NodeOutPutVar, type ValueSelector, type Var, VarType } from '@/app/components/workflow/types'
|
||||
@@ -15,9 +12,10 @@ import {
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import { XCircle } from '@/app/components/base/icons/src/vender/solid/general'
|
||||
import Input from '@/app/components/base/input'
|
||||
import { BubbleX, Env } from '@/app/components/base/icons/src/vender/line/others'
|
||||
import { checkKeys } from '@/utils/var'
|
||||
import { FILE_STRUCT } from '@/app/components/workflow/constants'
|
||||
|
||||
type ObjectChildrenProps = {
|
||||
nodeId: string
|
||||
@@ -27,6 +25,7 @@ type ObjectChildrenProps = {
|
||||
onChange: (value: ValueSelector, item: Var) => void
|
||||
onHovering?: (value: boolean) => void
|
||||
itemWidth?: number
|
||||
isSupportFileVar?: boolean
|
||||
}
|
||||
|
||||
type ItemProps = {
|
||||
@@ -37,6 +36,7 @@ type ItemProps = {
|
||||
onChange: (value: ValueSelector, item: Var) => void
|
||||
onHovering?: (value: boolean) => void
|
||||
itemWidth?: number
|
||||
isSupportFileVar?: boolean
|
||||
}
|
||||
|
||||
const Item: FC<ItemProps> = ({
|
||||
@@ -47,8 +47,10 @@ const Item: FC<ItemProps> = ({
|
||||
onChange,
|
||||
onHovering,
|
||||
itemWidth,
|
||||
isSupportFileVar,
|
||||
}) => {
|
||||
const isObj = itemData.type === VarType.object && itemData.children && itemData.children.length > 0
|
||||
const isFile = itemData.type === VarType.file
|
||||
const isObj = ([VarType.object, VarType.file].includes(itemData.type) && itemData.children && itemData.children.length > 0)
|
||||
const isSys = itemData.variable.startsWith('sys.')
|
||||
const isEnv = itemData.variable.startsWith('env.')
|
||||
const isChatVar = itemData.variable.startsWith('conversation.')
|
||||
@@ -80,6 +82,9 @@ const Item: FC<ItemProps> = ({
|
||||
}, [isHovering])
|
||||
const handleChosen = (e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
if (!isSupportFileVar && isFile)
|
||||
return
|
||||
|
||||
if (isSys || isEnv || isChatVar) { // system variable | environment variable | conversation variable
|
||||
onChange([...objPath, ...itemData.variable.split('.')], itemData)
|
||||
}
|
||||
@@ -98,35 +103,35 @@ const Item: FC<ItemProps> = ({
|
||||
ref={itemRef}
|
||||
className={cn(
|
||||
isObj ? ' pr-1' : 'pr-[18px]',
|
||||
isHovering && (isObj ? 'bg-primary-50' : 'bg-gray-50'),
|
||||
isHovering && (isObj ? 'bg-primary-50' : 'bg-state-base-hover'),
|
||||
'relative w-full flex items-center h-6 pl-3 rounded-md cursor-pointer')
|
||||
}
|
||||
onClick={handleChosen}
|
||||
>
|
||||
<div className='flex items-center w-0 grow'>
|
||||
{!isEnv && !isChatVar && <Variable02 className='shrink-0 w-3.5 h-3.5 text-primary-500' />}
|
||||
{!isEnv && !isChatVar && <Variable02 className='shrink-0 w-3.5 h-3.5 text-text-accent' />}
|
||||
{isEnv && <Env className='shrink-0 w-3.5 h-3.5 text-util-colors-violet-violet-600' />}
|
||||
{isChatVar && <BubbleX className='w-3.5 h-3.5 text-util-colors-teal-teal-700' />}
|
||||
{!isEnv && !isChatVar && (
|
||||
<div title={itemData.variable} className='ml-1 w-0 grow truncate text-[13px] font-normal text-gray-900'>{itemData.variable}</div>
|
||||
<div title={itemData.variable} className='ml-1 w-0 grow truncate text-text-secondary system-sm-medium'>{itemData.variable}</div>
|
||||
)}
|
||||
{isEnv && (
|
||||
<div title={itemData.variable} className='ml-1 w-0 grow truncate text-[13px] font-normal text-gray-900'>{itemData.variable.replace('env.', '')}</div>
|
||||
<div title={itemData.variable} className='ml-1 w-0 grow truncate text-text-secondary system-sm-medium'>{itemData.variable.replace('env.', '')}</div>
|
||||
)}
|
||||
{isChatVar && (
|
||||
<div title={itemData.des} className='ml-1 w-0 grow truncate text-[13px] font-normal text-gray-900'>{itemData.variable.replace('conversation.', '')}</div>
|
||||
<div title={itemData.des} className='ml-1 w-0 grow truncate text-text-secondary system-sm-medium'>{itemData.variable.replace('conversation.', '')}</div>
|
||||
)}
|
||||
</div>
|
||||
<div className='ml-1 shrink-0 text-xs font-normal text-gray-500 capitalize'>{itemData.type}</div>
|
||||
<div className='ml-1 shrink-0 text-xs font-normal text-text-tertiary capitalize'>{itemData.type}</div>
|
||||
{isObj && (
|
||||
<ChevronRight className='ml-0.5 w-3 h-3 text-gray-500' />
|
||||
<ChevronRight className={cn('ml-0.5 w-3 h-3 text-text-quaternary', isHovering && 'text-text-tertiary')} />
|
||||
)}
|
||||
</div>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent style={{
|
||||
zIndex: 100,
|
||||
}}>
|
||||
{isObj && (
|
||||
{(isObj && !isFile) && (
|
||||
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
||||
<ObjectChildren
|
||||
nodeId={nodeId}
|
||||
@@ -136,6 +141,20 @@ const Item: FC<ItemProps> = ({
|
||||
onChange={onChange}
|
||||
onHovering={setIsChildrenHovering}
|
||||
itemWidth={itemWidth}
|
||||
isSupportFileVar={isSupportFileVar}
|
||||
/>
|
||||
)}
|
||||
{isFile && (
|
||||
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
||||
<ObjectChildren
|
||||
nodeId={nodeId}
|
||||
title={title}
|
||||
objPath={[...objPath, itemData.variable]}
|
||||
data={FILE_STRUCT}
|
||||
onChange={onChange}
|
||||
onHovering={setIsChildrenHovering}
|
||||
itemWidth={itemWidth}
|
||||
isSupportFileVar={isSupportFileVar}
|
||||
/>
|
||||
)}
|
||||
</PortalToFollowElemContent>
|
||||
@@ -151,6 +170,7 @@ const ObjectChildren: FC<ObjectChildrenProps> = ({
|
||||
onChange,
|
||||
onHovering,
|
||||
itemWidth,
|
||||
isSupportFileVar,
|
||||
}) => {
|
||||
const currObjPath = objPath
|
||||
const itemRef = useRef(null)
|
||||
@@ -195,6 +215,7 @@ const ObjectChildren: FC<ObjectChildrenProps> = ({
|
||||
itemData={v}
|
||||
onChange={onChange}
|
||||
onHovering={setIsChildrenHovering}
|
||||
isSupportFileVar={isSupportFileVar}
|
||||
/>
|
||||
))
|
||||
}
|
||||
@@ -206,15 +227,19 @@ type Props = {
|
||||
hideSearch?: boolean
|
||||
searchBoxClassName?: string
|
||||
vars: NodeOutPutVar[]
|
||||
isSupportFileVar?: boolean
|
||||
onChange: (value: ValueSelector, item: Var) => void
|
||||
itemWidth?: number
|
||||
maxHeightClass?: string
|
||||
}
|
||||
const VarReferenceVars: FC<Props> = ({
|
||||
hideSearch,
|
||||
searchBoxClassName,
|
||||
vars,
|
||||
isSupportFileVar,
|
||||
onChange,
|
||||
itemWidth,
|
||||
maxHeightClass,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [searchText, setSearchText] = useState('')
|
||||
@@ -244,40 +269,21 @@ const VarReferenceVars: FC<Props> = ({
|
||||
}
|
||||
})
|
||||
|
||||
const [isFocus, {
|
||||
setFalse: setBlur,
|
||||
setTrue: setFocus,
|
||||
}] = useBoolean(false)
|
||||
return (
|
||||
<>
|
||||
{
|
||||
!hideSearch && (
|
||||
<>
|
||||
<div
|
||||
className={cn(searchBoxClassName, isFocus && 'shadow-sm bg-white', 'mb-2 mx-1 flex items-center px-2 rounded-lg bg-gray-100 ')}
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
|
||||
<RiSearchLine className='shrink-0 ml-[1px] mr-[5px] w-3.5 h-3.5 text-gray-400' />
|
||||
<input
|
||||
<div className={cn('mb-2 mx-1', searchBoxClassName)} onClick={e => e.stopPropagation()}>
|
||||
<Input
|
||||
showLeftIcon
|
||||
showClearIcon
|
||||
value={searchText}
|
||||
className='grow px-0.5 py-[7px] text-[13px] text-gray-700 bg-transparent appearance-none outline-none caret-primary-600 placeholder:text-gray-400'
|
||||
placeholder={t('workflow.common.searchVar') || ''}
|
||||
onChange={e => setSearchText(e.target.value)}
|
||||
onFocus={setFocus}
|
||||
onBlur={setBlur}
|
||||
onClear={() => setSearchText('')}
|
||||
autoFocus
|
||||
/>
|
||||
{
|
||||
searchText && (
|
||||
<div
|
||||
className='flex items-center justify-center ml-[5px] w-[18px] h-[18px] cursor-pointer'
|
||||
onClick={() => setSearchText('')}
|
||||
>
|
||||
<XCircle className='w-[14px] h-[14px] text-gray-400' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<div className='h-[0.5px] bg-black/5 relative left-[-4px]' style={{
|
||||
width: 'calc(100% + 8px)',
|
||||
@@ -287,13 +293,13 @@ const VarReferenceVars: FC<Props> = ({
|
||||
}
|
||||
|
||||
{filteredVars.length > 0
|
||||
? <div className='max-h-[85vh] overflow-y-auto'>
|
||||
? <div className={cn('max-h-[85vh] overflow-y-auto', maxHeightClass)}>
|
||||
|
||||
{
|
||||
filteredVars.map((item, i) => (
|
||||
<div key={i}>
|
||||
<div
|
||||
className='leading-[22px] px-3 text-xs font-medium text-gray-500 uppercase truncate'
|
||||
className='leading-[22px] px-3 text-text-tertiary system-xs-medium-uppercase truncate'
|
||||
title={item.title}
|
||||
>{item.title}</div>
|
||||
{item.vars.map((v, j) => (
|
||||
@@ -305,6 +311,7 @@ const VarReferenceVars: FC<Props> = ({
|
||||
itemData={v}
|
||||
onChange={onChange}
|
||||
itemWidth={itemWidth}
|
||||
isSupportFileVar={isSupportFileVar}
|
||||
/>
|
||||
))}
|
||||
</div>))
|
||||
|
@@ -17,18 +17,21 @@ export const useNodeHelpLink = (nodeType: BlockEnum) => {
|
||||
[BlockEnum.End]: 'end',
|
||||
[BlockEnum.Answer]: 'answer',
|
||||
[BlockEnum.LLM]: 'llm',
|
||||
[BlockEnum.KnowledgeRetrieval]: 'knowledge_retrieval',
|
||||
[BlockEnum.QuestionClassifier]: 'question_classifier',
|
||||
[BlockEnum.KnowledgeRetrieval]: 'knowledge-retrieval',
|
||||
[BlockEnum.QuestionClassifier]: 'question-classifier',
|
||||
[BlockEnum.IfElse]: 'ifelse',
|
||||
[BlockEnum.Code]: 'code',
|
||||
[BlockEnum.TemplateTransform]: 'template',
|
||||
[BlockEnum.VariableAssigner]: 'variable_assigner',
|
||||
[BlockEnum.VariableAggregator]: 'variable_assigner',
|
||||
[BlockEnum.Assigner]: 'variable_assignment',
|
||||
[BlockEnum.VariableAssigner]: 'variable-assigner',
|
||||
[BlockEnum.VariableAggregator]: 'variable-assigner',
|
||||
[BlockEnum.Assigner]: 'variable-assignment',
|
||||
[BlockEnum.Iteration]: 'iteration',
|
||||
[BlockEnum.ParameterExtractor]: 'parameter_extractor',
|
||||
[BlockEnum.HttpRequest]: 'http_request',
|
||||
[BlockEnum.IterationStart]: 'iteration',
|
||||
[BlockEnum.ParameterExtractor]: 'parameter-extractor',
|
||||
[BlockEnum.HttpRequest]: 'http-request',
|
||||
[BlockEnum.Tool]: 'tools',
|
||||
[BlockEnum.DocExtractor]: 'doc-extractor',
|
||||
[BlockEnum.ListFilter]: 'list-operator',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,16 +42,19 @@ export const useNodeHelpLink = (nodeType: BlockEnum) => {
|
||||
[BlockEnum.LLM]: 'llm',
|
||||
[BlockEnum.KnowledgeRetrieval]: 'knowledge-retrieval',
|
||||
[BlockEnum.QuestionClassifier]: 'question-classifier',
|
||||
[BlockEnum.IfElse]: 'if-else',
|
||||
[BlockEnum.IfElse]: 'ifelse',
|
||||
[BlockEnum.Code]: 'code',
|
||||
[BlockEnum.TemplateTransform]: 'template',
|
||||
[BlockEnum.VariableAssigner]: 'variable-assigner',
|
||||
[BlockEnum.VariableAggregator]: 'variable-assigner',
|
||||
[BlockEnum.Assigner]: 'variable-assignment',
|
||||
[BlockEnum.Iteration]: 'iteration',
|
||||
[BlockEnum.IterationStart]: 'iteration',
|
||||
[BlockEnum.ParameterExtractor]: 'parameter-extractor',
|
||||
[BlockEnum.HttpRequest]: 'http-request',
|
||||
[BlockEnum.Tool]: 'tools',
|
||||
[BlockEnum.DocExtractor]: 'doc-extractor',
|
||||
[BlockEnum.ListFilter]: 'list-operator',
|
||||
}
|
||||
}, [language])
|
||||
|
||||
|
@@ -80,8 +80,10 @@ const varTypeToInputVarType = (type: VarType, {
|
||||
return InputVarType.number
|
||||
if ([VarType.object, VarType.array, VarType.arrayNumber, VarType.arrayString, VarType.arrayObject].includes(type))
|
||||
return InputVarType.json
|
||||
if (type === VarType.file)
|
||||
return InputVarType.singleFile
|
||||
if (type === VarType.arrayFile)
|
||||
return InputVarType.files
|
||||
return InputVarType.multiFiles
|
||||
|
||||
return InputVarType.textInput
|
||||
}
|
||||
@@ -123,7 +125,7 @@ const useOneStepRun = <T>({
|
||||
res = curr
|
||||
}
|
||||
else {
|
||||
if (curr?.type === VarType.object)
|
||||
if (curr?.type === VarType.object || curr?.type === VarType.file)
|
||||
curr = curr.children
|
||||
}
|
||||
})
|
||||
|
@@ -38,6 +38,7 @@ const Panel: FC<NodePanelProps<AnswerNodeType>> = ({
|
||||
onChange={handleAnswerChange}
|
||||
nodesOutputVars={availableVars}
|
||||
availableNodes={availableNodesWithParent}
|
||||
isSupportFileVar
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
85
web/app/components/workflow/nodes/code/dependency-picker.tsx
Normal file
85
web/app/components/workflow/nodes/code/dependency-picker.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import { t } from 'i18next'
|
||||
import {
|
||||
RiArrowDownSLine,
|
||||
} from '@remixicon/react'
|
||||
import type { CodeDependency } from './types'
|
||||
import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '@/app/components/base/portal-to-follow-elem'
|
||||
import Input from '@/app/components/base/input'
|
||||
import { Check } from '@/app/components/base/icons/src/vender/line/general'
|
||||
|
||||
type Props = {
|
||||
value: CodeDependency
|
||||
available_dependencies: CodeDependency[]
|
||||
onChange: (dependency: CodeDependency) => void
|
||||
}
|
||||
|
||||
const DependencyPicker: FC<Props> = ({
|
||||
available_dependencies,
|
||||
value,
|
||||
onChange,
|
||||
}) => {
|
||||
const [open, setOpen] = useState(false)
|
||||
const [searchText, setSearchText] = useState('')
|
||||
|
||||
const handleChange = useCallback((dependency: CodeDependency) => {
|
||||
return () => {
|
||||
setOpen(false)
|
||||
onChange(dependency)
|
||||
}
|
||||
}, [onChange])
|
||||
|
||||
return (
|
||||
<PortalToFollowElem
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
placement='bottom-start'
|
||||
offset={4}
|
||||
>
|
||||
<PortalToFollowElemTrigger onClick={() => setOpen(!open)} className='flex-grow cursor-pointer'>
|
||||
<div className='flex items-center h-8 justify-between px-2.5 rounded-lg border-0 bg-gray-100 text-gray-900 text-[13px]'>
|
||||
<div className='grow w-0 truncate' title={value.name}>{value.name}</div>
|
||||
<RiArrowDownSLine className='shrink-0 w-3.5 h-3.5 text-gray-700' />
|
||||
</div>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent style={{
|
||||
zIndex: 100,
|
||||
}}>
|
||||
<div className='p-1 bg-white rounded-lg shadow-sm' style={{
|
||||
width: 350,
|
||||
}}>
|
||||
<div className='mb-2 mx-1'>
|
||||
<Input
|
||||
showLeftIcon
|
||||
showClearIcon
|
||||
value={searchText}
|
||||
placeholder={t('workflow.nodes.code.searchDependencies') || ''}
|
||||
onChange={e => setSearchText(e.target.value)}
|
||||
onClear={() => setSearchText('')}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
<div className='max-h-[30vh] overflow-y-auto'>
|
||||
{available_dependencies.filter((v) => {
|
||||
if (!searchText)
|
||||
return true
|
||||
return v.name.toLowerCase().includes(searchText.toLowerCase())
|
||||
}).map(dependency => (
|
||||
<div
|
||||
key={dependency.name}
|
||||
className='flex items-center h-[30px] justify-between pl-3 pr-2 rounded-lg hover:bg-gray-100 text-gray-900 text-[13px] cursor-pointer'
|
||||
onClick={handleChange(dependency)}
|
||||
>
|
||||
<div className='w-0 grow truncate'>{dependency.name}</div>
|
||||
{dependency.name === value.name && <Check className='shrink-0 w-4 h-4 text-primary-600' />}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(DependencyPicker)
|
@@ -30,6 +30,10 @@ import ParameterExtractorNode from './parameter-extractor/node'
|
||||
import ParameterExtractorPanel from './parameter-extractor/panel'
|
||||
import IterationNode from './iteration/node'
|
||||
import IterationPanel from './iteration/panel'
|
||||
import DocExtractorNode from './document-extractor/node'
|
||||
import DocExtractorPanel from './document-extractor/panel'
|
||||
import ListFilterNode from './list-operator/node'
|
||||
import ListFilterPanel from './list-operator/panel'
|
||||
|
||||
export const NodeComponentMap: Record<string, ComponentType<any>> = {
|
||||
[BlockEnum.Start]: StartNode,
|
||||
@@ -48,6 +52,8 @@ export const NodeComponentMap: Record<string, ComponentType<any>> = {
|
||||
[BlockEnum.VariableAggregator]: VariableAssignerNode,
|
||||
[BlockEnum.ParameterExtractor]: ParameterExtractorNode,
|
||||
[BlockEnum.Iteration]: IterationNode,
|
||||
[BlockEnum.DocExtractor]: DocExtractorNode,
|
||||
[BlockEnum.ListFilter]: ListFilterNode,
|
||||
}
|
||||
|
||||
export const PanelComponentMap: Record<string, ComponentType<any>> = {
|
||||
@@ -67,6 +73,8 @@ export const PanelComponentMap: Record<string, ComponentType<any>> = {
|
||||
[BlockEnum.Assigner]: AssignerPanel,
|
||||
[BlockEnum.ParameterExtractor]: ParameterExtractorPanel,
|
||||
[BlockEnum.Iteration]: IterationPanel,
|
||||
[BlockEnum.DocExtractor]: DocExtractorPanel,
|
||||
[BlockEnum.ListFilter]: ListFilterPanel,
|
||||
}
|
||||
|
||||
export const CUSTOM_NODE_TYPE = 'custom'
|
||||
|
@@ -0,0 +1,36 @@
|
||||
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/constants'
|
||||
const i18nPrefix = 'workflow.errorMsg'
|
||||
|
||||
const nodeDefault: NodeDefault<DocExtractorNodeType> = {
|
||||
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
|
||||
|
||||
if (!errorMessages && !variable?.length)
|
||||
errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t('workflow.nodes.assigner.assignedVariable') })
|
||||
|
||||
return {
|
||||
isValid: !errorMessages,
|
||||
errorMessage: errorMessages,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export default nodeDefault
|
@@ -0,0 +1,42 @@
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useNodes } from 'reactflow'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import NodeVariableItem from '../variable-assigner/components/node-variable-item'
|
||||
import { type DocExtractorNodeType } from './types'
|
||||
import { isConversationVar, isENV, isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils'
|
||||
import { BlockEnum, type Node, type NodeProps } from '@/app/components/workflow/types'
|
||||
|
||||
const i18nPrefix = 'workflow.nodes.docExtractor'
|
||||
|
||||
const NodeComponent: FC<NodeProps<DocExtractorNodeType>> = ({
|
||||
data,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const nodes: Node[] = useNodes()
|
||||
const { variable_selector: variable } = data
|
||||
|
||||
if (!variable || variable.length === 0)
|
||||
return null
|
||||
|
||||
const isSystem = isSystemVar(variable)
|
||||
const isEnv = isENV(variable)
|
||||
const isChatVar = isConversationVar(variable)
|
||||
const node = isSystem ? nodes.find(node => node.data.type === BlockEnum.Start) : nodes.find(node => node.id === variable[0])
|
||||
const varName = isSystem ? `sys.${variable[variable.length - 1]}` : variable.slice(1).join('.')
|
||||
return (
|
||||
<div className='relative px-3'>
|
||||
<div className='mb-1 system-2xs-medium-uppercase text-text-tertiary'>{t(`${i18nPrefix}.inputVar`)}</div>
|
||||
<NodeVariableItem
|
||||
node={node as Node}
|
||||
isEnv={isEnv}
|
||||
isChatVar={isChatVar}
|
||||
varName={varName}
|
||||
className='bg-workflow-block-parma-bg'
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(NodeComponent)
|
@@ -0,0 +1,88 @@
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import useSWR from 'swr'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import VarReferencePicker from '../_base/components/variable/var-reference-picker'
|
||||
import OutputVars, { VarItem } from '../_base/components/output-vars'
|
||||
import Split from '../_base/components/split'
|
||||
import { useNodeHelpLink } from '../_base/hooks/use-node-help-link'
|
||||
import useConfig from './use-config'
|
||||
import type { DocExtractorNodeType } from './types'
|
||||
import { fetchSupportFileTypes } from '@/service/datasets'
|
||||
import Field from '@/app/components/workflow/nodes/_base/components/field'
|
||||
import { BlockEnum, type NodePanelProps } from '@/app/components/workflow/types'
|
||||
import I18n from '@/context/i18n'
|
||||
import { LanguagesSupported } from '@/i18n/language'
|
||||
|
||||
const i18nPrefix = 'workflow.nodes.docExtractor'
|
||||
|
||||
const Panel: FC<NodePanelProps<DocExtractorNodeType>> = ({
|
||||
id,
|
||||
data,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { locale } = useContext(I18n)
|
||||
const link = useNodeHelpLink(BlockEnum.DocExtractor)
|
||||
const { data: supportFileTypesResponse } = useSWR({ url: '/files/support-type' }, fetchSupportFileTypes)
|
||||
const supportTypes = supportFileTypesResponse?.allowed_extensions || []
|
||||
const supportTypesShowNames = (() => {
|
||||
const extensionMap: { [key: string]: string } = {
|
||||
md: 'markdown',
|
||||
pptx: 'pptx',
|
||||
htm: 'html',
|
||||
xlsx: 'xlsx',
|
||||
docx: 'docx',
|
||||
}
|
||||
|
||||
return [...supportTypes]
|
||||
.map(item => extensionMap[item] || item) // map to standardized extension
|
||||
.map(item => item.toLowerCase()) // convert to lower case
|
||||
.filter((item, index, self) => self.indexOf(item) === index) // remove duplicates
|
||||
.join(locale !== LanguagesSupported[1] ? ', ' : '、 ')
|
||||
})()
|
||||
const {
|
||||
readOnly,
|
||||
inputs,
|
||||
handleVarChanges,
|
||||
filterVar,
|
||||
} = useConfig(id, data)
|
||||
|
||||
return (
|
||||
<div className='mt-2'>
|
||||
<div className='px-4 pb-4 space-y-4'>
|
||||
<Field
|
||||
title={t(`${i18nPrefix}.inputVar`)}
|
||||
>
|
||||
<>
|
||||
<VarReferencePicker
|
||||
readonly={readOnly}
|
||||
nodeId={id}
|
||||
isShowNodeName
|
||||
value={inputs.variable_selector || []}
|
||||
onChange={handleVarChanges}
|
||||
filterVar={filterVar}
|
||||
typePlaceHolder='File | Array[File]'
|
||||
/>
|
||||
<div className='mt-1 py-0.5 text-text-tertiary body-xs-regular'>
|
||||
{t(`${i18nPrefix}.supportFileTypes`, { types: supportTypesShowNames })}
|
||||
<a className='text-text-accent' href={link} target='_blank'>{t(`${i18nPrefix}.learnMore`)}</a>
|
||||
</div>
|
||||
</>
|
||||
</Field>
|
||||
</div>
|
||||
<Split />
|
||||
<div className='px-4 pt-4 pb-2'>
|
||||
<OutputVars>
|
||||
<VarItem
|
||||
name='text'
|
||||
type={inputs.is_array_file ? 'array[string]' : 'string'}
|
||||
description={t(`${i18nPrefix}.outputVars.text`)}
|
||||
/>
|
||||
</OutputVars>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(Panel)
|
@@ -0,0 +1,6 @@
|
||||
import type { CommonNodeType, ValueSelector } from '@/app/components/workflow/types'
|
||||
|
||||
export type DocExtractorNodeType = CommonNodeType & {
|
||||
variable_selector: ValueSelector
|
||||
is_array_file: boolean
|
||||
}
|
@@ -0,0 +1,66 @@
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import produce from 'immer'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
|
||||
import type { ValueSelector, Var } from '../../types'
|
||||
import { VarType } from '../../types'
|
||||
import { type DocExtractorNodeType } from './types'
|
||||
import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud'
|
||||
import {
|
||||
useIsChatMode,
|
||||
useNodesReadOnly,
|
||||
useWorkflow,
|
||||
useWorkflowVariables,
|
||||
} from '@/app/components/workflow/hooks'
|
||||
|
||||
const useConfig = (id: string, payload: DocExtractorNodeType) => {
|
||||
const { nodesReadOnly: readOnly } = useNodesReadOnly()
|
||||
const { inputs, setInputs } = useNodeCrud<DocExtractorNodeType>(id, payload)
|
||||
|
||||
const filterVar = useCallback((varPayload: Var) => {
|
||||
return varPayload.type === VarType.file || varPayload.type === VarType.arrayFile
|
||||
}, [])
|
||||
|
||||
const isChatMode = useIsChatMode()
|
||||
|
||||
const store = useStoreApi()
|
||||
const { getBeforeNodesInSameBranch } = useWorkflow()
|
||||
const {
|
||||
getNodes,
|
||||
} = store.getState()
|
||||
const currentNode = getNodes().find(n => n.id === id)
|
||||
const isInIteration = payload.isInIteration
|
||||
const iterationNode = isInIteration ? getNodes().find(n => n.id === currentNode!.parentId) : null
|
||||
const availableNodes = useMemo(() => {
|
||||
return getBeforeNodesInSameBranch(id)
|
||||
}, [getBeforeNodesInSameBranch, id])
|
||||
|
||||
const { getCurrentVariableType } = useWorkflowVariables()
|
||||
const getType = useCallback((variable?: ValueSelector) => {
|
||||
const varType = getCurrentVariableType({
|
||||
parentNode: iterationNode,
|
||||
valueSelector: variable || [],
|
||||
availableNodes,
|
||||
isChatMode,
|
||||
isConstant: false,
|
||||
})
|
||||
return varType
|
||||
}, [getCurrentVariableType, availableNodes, isChatMode, iterationNode])
|
||||
|
||||
const handleVarChanges = useCallback((variable: ValueSelector | string) => {
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
draft.variable_selector = variable as ValueSelector
|
||||
draft.is_array_file = getType(draft.variable_selector) === VarType.arrayFile
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}, [getType, inputs, setInputs])
|
||||
|
||||
return {
|
||||
readOnly,
|
||||
inputs,
|
||||
filterVar,
|
||||
handleVarChanges,
|
||||
}
|
||||
}
|
||||
|
||||
export default useConfig
|
@@ -12,6 +12,7 @@ import type { Var } from '@/app/components/workflow/types'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Input from '@/app/components/workflow/nodes/_base/components/input-support-select-var'
|
||||
import BaseInput from '@/app/components/base/input'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
const i18nPrefix = 'workflow.nodes.http.authorization'
|
||||
@@ -146,9 +147,7 @@ const Authorization: FC<Props> = ({
|
||||
</Field>
|
||||
{tempPayload.config?.type === APIType.custom && (
|
||||
<Field title={t(`${i18nPrefix}.header`)} isRequired>
|
||||
<input
|
||||
type='text'
|
||||
className='w-full h-8 leading-8 px-2.5 rounded-lg border-0 bg-gray-100 text-gray-900 text-[13px] placeholder:text-gray-400 focus:outline-none focus:ring-1 focus:ring-inset focus:ring-gray-200'
|
||||
<BaseInput
|
||||
value={tempPayload.config?.header || ''}
|
||||
onChange={handleAPIKeyOrHeaderChange('header')}
|
||||
/>
|
||||
|
@@ -1,17 +1,20 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback, useEffect } from 'react'
|
||||
import React, { useCallback, useMemo } from 'react'
|
||||
import produce from 'immer'
|
||||
import type { Body } from '../../types'
|
||||
import { BodyType } from '../../types'
|
||||
import useKeyValueList from '../../hooks/use-key-value-list'
|
||||
import { uniqueId } from 'lodash-es'
|
||||
import type { Body, BodyPayload, KeyValue as KeyValueType } from '../../types'
|
||||
import { BodyPayloadValueType, BodyType } from '../../types'
|
||||
import KeyValue from '../key-value'
|
||||
import useAvailableVarList from '../../../_base/hooks/use-available-var-list'
|
||||
import VarReferencePicker from '../../../_base/components/variable/var-reference-picker'
|
||||
import cn from '@/utils/classnames'
|
||||
import InputWithVar from '@/app/components/workflow/nodes/_base/components/prompt/editor'
|
||||
import type { Var } from '@/app/components/workflow/types'
|
||||
import type { ValueSelector, Var } from '@/app/components/workflow/types'
|
||||
import { VarType } from '@/app/components/workflow/types'
|
||||
|
||||
const UNIQUE_ID_PREFIX = 'key-value-'
|
||||
|
||||
type Props = {
|
||||
readonly: boolean
|
||||
nodeId: string
|
||||
@@ -23,15 +26,17 @@ const allTypes = [
|
||||
BodyType.none,
|
||||
BodyType.formData,
|
||||
BodyType.xWwwFormUrlencoded,
|
||||
BodyType.rawText,
|
||||
BodyType.json,
|
||||
BodyType.rawText,
|
||||
BodyType.binary,
|
||||
]
|
||||
const bodyTextMap = {
|
||||
[BodyType.none]: 'none',
|
||||
[BodyType.formData]: 'form-data',
|
||||
[BodyType.xWwwFormUrlencoded]: 'x-www-form-urlencoded',
|
||||
[BodyType.rawText]: 'raw text',
|
||||
[BodyType.rawText]: 'raw',
|
||||
[BodyType.json]: 'JSON',
|
||||
[BodyType.binary]: 'binary',
|
||||
}
|
||||
|
||||
const EditBody: FC<Props> = ({
|
||||
@@ -40,7 +45,15 @@ const EditBody: FC<Props> = ({
|
||||
payload,
|
||||
onChange,
|
||||
}) => {
|
||||
const { type } = payload
|
||||
const { type, data } = payload
|
||||
const bodyPayload = useMemo(() => {
|
||||
if (typeof data === 'string') { // old data
|
||||
return []
|
||||
}
|
||||
return data
|
||||
}, [data])
|
||||
const stringValue = [BodyType.formData, BodyType.xWwwFormUrlencoded].includes(type) ? '' : (bodyPayload[0]?.value || '')
|
||||
|
||||
const { availableVars, availableNodes } = useAvailableVarList(nodeId, {
|
||||
onlyLeafNodeVar: false,
|
||||
filterVar: (varPayload: Var) => {
|
||||
@@ -50,49 +63,69 @@ const EditBody: FC<Props> = ({
|
||||
|
||||
const handleTypeChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newType = e.target.value as BodyType
|
||||
const hasKeyValue = [BodyType.formData, BodyType.xWwwFormUrlencoded].includes(newType)
|
||||
onChange({
|
||||
type: newType,
|
||||
data: '',
|
||||
data: hasKeyValue
|
||||
? [
|
||||
{
|
||||
id: uniqueId(UNIQUE_ID_PREFIX),
|
||||
type: BodyPayloadValueType.text,
|
||||
key: '',
|
||||
value: '',
|
||||
},
|
||||
]
|
||||
: [],
|
||||
})
|
||||
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
||||
setBody([])
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [onChange])
|
||||
|
||||
const isCurrentKeyValue = type === BodyType.formData || type === BodyType.xWwwFormUrlencoded
|
||||
|
||||
const {
|
||||
list: body,
|
||||
setList: setBody,
|
||||
addItem: addBody,
|
||||
} = useKeyValueList(payload.data, (value) => {
|
||||
if (!isCurrentKeyValue)
|
||||
return
|
||||
|
||||
const newBody = produce(payload, (draft: Body) => {
|
||||
draft.data = value
|
||||
const handleAddBody = useCallback(() => {
|
||||
const newPayload = produce(payload, (draft) => {
|
||||
(draft.data as BodyPayload).push({
|
||||
id: uniqueId(UNIQUE_ID_PREFIX),
|
||||
type: BodyPayloadValueType.text,
|
||||
key: '',
|
||||
value: '',
|
||||
})
|
||||
})
|
||||
onChange(newBody)
|
||||
}, type === BodyType.json)
|
||||
onChange(newPayload)
|
||||
}, [onChange, payload])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isCurrentKeyValue)
|
||||
return
|
||||
|
||||
const newBody = produce(payload, (draft: Body) => {
|
||||
draft.data = body.map((item) => {
|
||||
if (!item.key && !item.value)
|
||||
return ''
|
||||
return `${item.key}:${item.value}`
|
||||
}).join('\n')
|
||||
const handleBodyPayloadChange = useCallback((newList: KeyValueType[]) => {
|
||||
const newPayload = produce(payload, (draft) => {
|
||||
draft.data = newList as BodyPayload
|
||||
})
|
||||
onChange(newBody)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isCurrentKeyValue])
|
||||
onChange(newPayload)
|
||||
}, [onChange, payload])
|
||||
|
||||
const filterOnlyFileVariable = (varPayload: Var) => {
|
||||
return [VarType.file, VarType.arrayFile].includes(varPayload.type)
|
||||
}
|
||||
|
||||
const handleBodyValueChange = useCallback((value: string) => {
|
||||
const newBody = produce(payload, (draft: Body) => {
|
||||
draft.data = value
|
||||
if ((draft.data as BodyPayload).length === 0) {
|
||||
(draft.data as BodyPayload).push({
|
||||
id: uniqueId(UNIQUE_ID_PREFIX),
|
||||
type: BodyPayloadValueType.text,
|
||||
key: '',
|
||||
value: '',
|
||||
})
|
||||
}
|
||||
(draft.data as BodyPayload)[0].value = value
|
||||
})
|
||||
onChange(newBody)
|
||||
}, [onChange, payload])
|
||||
|
||||
const handleFileChange = useCallback((value: ValueSelector | string) => {
|
||||
const newBody = produce(payload, (draft: Body) => {
|
||||
if ((draft.data as BodyPayload).length === 0) {
|
||||
(draft.data as BodyPayload).push({
|
||||
id: uniqueId(UNIQUE_ID_PREFIX),
|
||||
type: BodyPayloadValueType.file,
|
||||
})
|
||||
}
|
||||
(draft.data as BodyPayload)[0].file = value as ValueSelector
|
||||
})
|
||||
onChange(newBody)
|
||||
}, [onChange, payload])
|
||||
@@ -122,9 +155,10 @@ const EditBody: FC<Props> = ({
|
||||
<KeyValue
|
||||
readonly={readonly}
|
||||
nodeId={nodeId}
|
||||
list={body}
|
||||
onChange={setBody}
|
||||
onAdd={addBody}
|
||||
list={bodyPayload as KeyValueType[]}
|
||||
onChange={handleBodyPayloadChange}
|
||||
onAdd={handleAddBody}
|
||||
isSupportFile={type === BodyType.formData}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -133,7 +167,7 @@ const EditBody: FC<Props> = ({
|
||||
instanceId={'http-body-raw'}
|
||||
title={<div className='uppercase'>Raw text</div>}
|
||||
onChange={handleBodyValueChange}
|
||||
value={payload.data}
|
||||
value={stringValue}
|
||||
justVar
|
||||
nodesOutputVars={availableVars}
|
||||
availableNodes={availableNodes}
|
||||
@@ -145,7 +179,7 @@ const EditBody: FC<Props> = ({
|
||||
<InputWithVar
|
||||
instanceId={'http-body-json'}
|
||||
title='JSON'
|
||||
value={payload.data}
|
||||
value={stringValue}
|
||||
onChange={handleBodyValueChange}
|
||||
justVar
|
||||
nodesOutputVars={availableVars}
|
||||
@@ -153,6 +187,16 @@ const EditBody: FC<Props> = ({
|
||||
readOnly={readonly}
|
||||
/>
|
||||
)}
|
||||
|
||||
{type === BodyType.binary && (
|
||||
<VarReferencePicker
|
||||
nodeId={nodeId}
|
||||
readonly={readonly}
|
||||
value={bodyPayload[0]?.file || []}
|
||||
onChange={handleFileChange}
|
||||
filterVar={filterOnlyFileVariable}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
@@ -10,6 +10,7 @@ type Props = {
|
||||
list: KeyValue[]
|
||||
onChange: (newList: KeyValue[]) => void
|
||||
onAdd: () => void
|
||||
isSupportFile?: boolean
|
||||
// toggleKeyValueEdit: () => void
|
||||
}
|
||||
|
||||
@@ -19,6 +20,7 @@ const KeyValueList: FC<Props> = ({
|
||||
list,
|
||||
onChange,
|
||||
onAdd,
|
||||
isSupportFile,
|
||||
// toggleKeyValueEdit,
|
||||
}) => {
|
||||
// const handleBulkValueChange = useCallback((value: string) => {
|
||||
@@ -48,6 +50,7 @@ const KeyValueList: FC<Props> = ({
|
||||
list={list}
|
||||
onChange={onChange}
|
||||
onAdd={onAdd}
|
||||
isSupportFile={isSupportFile}
|
||||
// onSwitchToBulkEdit={toggleKeyValueEdit}
|
||||
/>
|
||||
// : <BulkEdit
|
||||
|
@@ -5,8 +5,7 @@ import produce from 'immer'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { KeyValue } from '../../../types'
|
||||
import KeyValueItem from './item'
|
||||
// import TooltipPlus from '@/app/components/base/tooltip-plus'
|
||||
// import { EditList } from '@/app/components/base/icons/src/vender/solid/communication'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
const i18nPrefix = 'workflow.nodes.http'
|
||||
|
||||
@@ -16,6 +15,7 @@ type Props = {
|
||||
list: KeyValue[]
|
||||
onChange: (newList: KeyValue[]) => void
|
||||
onAdd: () => void
|
||||
isSupportFile?: boolean
|
||||
// onSwitchToBulkEdit: () => void
|
||||
keyNotSupportVar?: boolean
|
||||
insertVarTipToLeft?: boolean
|
||||
@@ -27,6 +27,7 @@ const KeyValueList: FC<Props> = ({
|
||||
list,
|
||||
onChange,
|
||||
onAdd,
|
||||
isSupportFile,
|
||||
// onSwitchToBulkEdit,
|
||||
keyNotSupportVar,
|
||||
insertVarTipToLeft,
|
||||
@@ -55,23 +56,11 @@ const KeyValueList: FC<Props> = ({
|
||||
return null
|
||||
|
||||
return (
|
||||
<div className='border border-gray-200 rounded-lg overflow-hidden'>
|
||||
<div className='flex items-center h-7 leading-7 text-xs font-medium text-gray-500 uppercase'>
|
||||
<div className='w-1/2 h-full pl-3 border-r border-gray-200'>{t(`${i18nPrefix}.key`)}</div>
|
||||
<div className='flex w-1/2 h-full pl-3 pr-1 items-center justify-between'>
|
||||
<div>{t(`${i18nPrefix}.value`)}</div>
|
||||
{/* {!readonly && (
|
||||
<TooltipPlus
|
||||
popupContent={t(`${i18nPrefix}.bulkEdit`)}
|
||||
>
|
||||
<div
|
||||
className='p-1 cursor-pointer rounded-md hover:bg-black/5 text-gray-500 hover:text-gray-800'
|
||||
onClick={onSwitchToBulkEdit}
|
||||
>
|
||||
<EditList className='w-3 h-3' />
|
||||
</div>
|
||||
</TooltipPlus>)} */}
|
||||
</div>
|
||||
<div className='border border-divider-regular rounded-lg overflow-hidden'>
|
||||
<div className={cn('flex items-center h-7 leading-7 text-text-tertiary system-xs-medium-uppercase')}>
|
||||
<div className={cn('h-full pl-3 border-r border-divider-regular', isSupportFile ? 'w-[140px]' : 'w-1/2')}>{t(`${i18nPrefix}.key`)}</div>
|
||||
{isSupportFile && <div className='shrink-0 w-[70px] h-full pl-3 border-r border-divider-regular'>{t(`${i18nPrefix}.type`)}</div>}
|
||||
<div className={cn('h-full pl-3 pr-1 items-center justify-between', isSupportFile ? 'grow' : 'w-1/2')}>{t(`${i18nPrefix}.value`)}</div>
|
||||
</div>
|
||||
{
|
||||
list.map((item, index) => (
|
||||
@@ -86,6 +75,7 @@ const KeyValueList: FC<Props> = ({
|
||||
onAdd={onAdd}
|
||||
readonly={readonly}
|
||||
canRemove={list.length > 1}
|
||||
isSupportFile={isSupportFile}
|
||||
keyNotSupportVar={keyNotSupportVar}
|
||||
insertVarTipToLeft={insertVarTipToLeft}
|
||||
/>
|
||||
|
@@ -18,6 +18,7 @@ type Props = {
|
||||
onRemove?: () => void
|
||||
placeholder?: string
|
||||
readOnly?: boolean
|
||||
isSupportFile?: boolean
|
||||
insertVarTipToLeft?: boolean
|
||||
}
|
||||
|
||||
@@ -31,6 +32,7 @@ const InputItem: FC<Props> = ({
|
||||
onRemove,
|
||||
placeholder,
|
||||
readOnly,
|
||||
isSupportFile,
|
||||
insertVarTipToLeft,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
@@ -41,7 +43,11 @@ const InputItem: FC<Props> = ({
|
||||
const { availableVars, availableNodesWithParent } = useAvailableVarList(nodeId, {
|
||||
onlyLeafNodeVar: false,
|
||||
filterVar: (varPayload: Var) => {
|
||||
return [VarType.string, VarType.number, VarType.secret].includes(varPayload.type)
|
||||
const supportVarTypes = [VarType.string, VarType.number, VarType.secret]
|
||||
if (isSupportFile)
|
||||
supportVarTypes.push(...[VarType.file, VarType.arrayFile])
|
||||
|
||||
return supportVarTypes.includes(varPayload.type)
|
||||
},
|
||||
})
|
||||
|
||||
|
@@ -4,9 +4,13 @@ import React, { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import produce from 'immer'
|
||||
import type { KeyValue } from '../../../types'
|
||||
import VarReferencePicker from '../../../../_base/components/variable/var-reference-picker'
|
||||
import InputItem from './input-item'
|
||||
import cn from '@/utils/classnames'
|
||||
import Input from '@/app/components/base/input'
|
||||
import { PortalSelect } from '@/app/components/base/select'
|
||||
import type { ValueSelector, Var } from '@/app/components/workflow/types'
|
||||
import { VarType } from '@/app/components/workflow/types'
|
||||
// import Input from '@/app/components/base/input'
|
||||
|
||||
const i18nPrefix = 'workflow.nodes.http'
|
||||
|
||||
@@ -21,6 +25,7 @@ type Props = {
|
||||
onRemove: () => void
|
||||
isLastItem: boolean
|
||||
onAdd: () => void
|
||||
isSupportFile?: boolean
|
||||
keyNotSupportVar?: boolean
|
||||
insertVarTipToLeft?: boolean
|
||||
}
|
||||
@@ -36,26 +41,29 @@ const KeyValueItem: FC<Props> = ({
|
||||
onRemove,
|
||||
isLastItem,
|
||||
onAdd,
|
||||
isSupportFile,
|
||||
keyNotSupportVar,
|
||||
insertVarTipToLeft,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const handleChange = useCallback((key: string) => {
|
||||
return (value: string) => {
|
||||
return (value: string | ValueSelector) => {
|
||||
const newPayload = produce(payload, (draft: any) => {
|
||||
draft[key] = value
|
||||
})
|
||||
onChange(newPayload)
|
||||
if (key === 'value' && isLastItem)
|
||||
onAdd()
|
||||
}
|
||||
}, [onChange, onAdd, isLastItem, payload])
|
||||
}, [onChange, payload])
|
||||
|
||||
const filterOnlyFileVariable = (varPayload: Var) => {
|
||||
return [VarType.file, VarType.arrayFile].includes(varPayload.type)
|
||||
}
|
||||
|
||||
return (
|
||||
// group class name is for hover row show remove button
|
||||
<div className={cn(className, 'group flex h-min-7 border-t border-gray-200')}>
|
||||
<div className='w-1/2 border-r border-gray-200'>
|
||||
<div className={cn('shrink-0 border-r border-divider-regular', isSupportFile ? 'w-[140px]' : 'w-1/2')}>
|
||||
{!keyNotSupportVar
|
||||
? (
|
||||
<InputItem
|
||||
@@ -70,25 +78,56 @@ const KeyValueItem: FC<Props> = ({
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<Input
|
||||
className='rounded-none bg-white border-none system-sm-regular focus:ring-0 focus:bg-gray-100! hover:bg-gray-50'
|
||||
<input
|
||||
className='appearance-none outline-none rounded-none bg-white border-none system-sm-regular focus:ring-0 focus:bg-gray-100! hover:bg-gray-50'
|
||||
value={payload.key}
|
||||
onChange={e => handleChange('key')(e.target.value)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className='w-1/2'>
|
||||
<InputItem
|
||||
instanceId={`http-value-${instanceId}`}
|
||||
nodeId={nodeId}
|
||||
value={payload.value}
|
||||
onChange={handleChange('value')}
|
||||
hasRemove={!readonly && canRemove}
|
||||
onRemove={onRemove}
|
||||
placeholder={t(`${i18nPrefix}.value`)!}
|
||||
readOnly={readonly}
|
||||
insertVarTipToLeft={insertVarTipToLeft}
|
||||
/>
|
||||
{isSupportFile && (
|
||||
<div className='shrink-0 w-[70px] border-r border-divider-regular'>
|
||||
<PortalSelect
|
||||
value={payload.type!}
|
||||
onSelect={item => handleChange('type')(item.value as string)}
|
||||
items={[
|
||||
{ name: 'text', value: 'text' },
|
||||
{ name: 'file', value: 'file' },
|
||||
]}
|
||||
readonly={readonly}
|
||||
triggerClassName='rounded-none h-7'
|
||||
triggerClassNameFn={isOpen => isOpen ? 'bg-state-base-hover' : 'bg-transparent'}
|
||||
popupClassName='w-[80px] h-7'
|
||||
/>
|
||||
</div>)}
|
||||
<div className={cn(isSupportFile ? 'grow' : 'w-1/2')} onClick={() => isLastItem && onAdd()}>
|
||||
{(isSupportFile && payload.type === 'file')
|
||||
? (
|
||||
<VarReferencePicker
|
||||
nodeId={nodeId}
|
||||
readonly={readonly}
|
||||
value={payload.file || []}
|
||||
onChange={handleChange('file')}
|
||||
filterVar={filterOnlyFileVariable}
|
||||
isInTable
|
||||
onRemove={onRemove}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<InputItem
|
||||
instanceId={`http-value-${instanceId}`}
|
||||
nodeId={nodeId}
|
||||
value={payload.value}
|
||||
onChange={handleChange('value')}
|
||||
hasRemove={!readonly && canRemove}
|
||||
onRemove={onRemove}
|
||||
placeholder={t(`${i18nPrefix}.value`)!}
|
||||
readOnly={readonly}
|
||||
isSupportFile={isSupportFile}
|
||||
insertVarTipToLeft={insertVarTipToLeft}
|
||||
/>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
@@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import type { Timeout as TimeoutPayloadType } from '../../types'
|
||||
import cn from '@/utils/classnames'
|
||||
import Input from '@/app/components/base/input'
|
||||
import { ChevronRight } from '@/app/components/base/icons/src/vender/line/arrows'
|
||||
|
||||
type Props = {
|
||||
@@ -32,10 +33,18 @@ const InputField: FC<{
|
||||
<span className="text-[13px] font-medium text-gray-900">{title}</span>
|
||||
<span className="text-xs font-normal text-gray-500">{description}</span>
|
||||
</div>
|
||||
<input className="w-full px-3 text-sm leading-9 text-gray-900 border-0 rounded-lg grow h-9 bg-gray-100 focus:outline-none focus:ring-1 focus:ring-inset focus:ring-gray-200" value={value} onChange={(e) => {
|
||||
const value = Math.max(min, Math.min(max, parseInt(e.target.value, 10)))
|
||||
onChange(value)
|
||||
}} placeholder={placeholder} type='number' readOnly={readOnly} min={min} max={max} />
|
||||
<Input
|
||||
type='number'
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
const value = Math.max(min, Math.min(max, parseInt(e.target.value, 10)))
|
||||
onChange(value)
|
||||
}}
|
||||
placeholder={placeholder}
|
||||
readOnly={readOnly}
|
||||
min={min}
|
||||
max={max}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import { BlockEnum } from '../../types'
|
||||
import type { NodeDefault } from '../../types'
|
||||
import { AuthorizationType, BodyType, type HttpNodeType, Method } 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/constants'
|
||||
|
||||
const nodeDefault: NodeDefault<HttpNodeType> = {
|
||||
@@ -16,7 +17,7 @@ const nodeDefault: NodeDefault<HttpNodeType> = {
|
||||
params: '',
|
||||
body: {
|
||||
type: BodyType.none,
|
||||
data: '',
|
||||
data: [],
|
||||
},
|
||||
timeout: {
|
||||
max_connect_timeout: 0,
|
||||
@@ -40,6 +41,12 @@ const nodeDefault: NodeDefault<HttpNodeType> = {
|
||||
if (!errorMessages && !payload.url)
|
||||
errorMessages = t('workflow.errorMsg.fieldRequired', { field: t('workflow.nodes.http.api') })
|
||||
|
||||
if (!errorMessages
|
||||
&& payload.body.type === BodyType.binary
|
||||
&& ((!(payload.body.data as BodyPayload)[0]?.file) || (payload.body.data as BodyPayload)[0]?.file?.length === 0)
|
||||
)
|
||||
errorMessages = t('workflow.errorMsg.fieldRequired', { field: t('workflow.nodes.http.binaryFileVariable') })
|
||||
|
||||
return {
|
||||
isValid: !errorMessages,
|
||||
errorMessage: errorMessages,
|
||||
|
@@ -27,6 +27,7 @@ const Panel: FC<NodePanelProps<HttpNodeType>> = ({
|
||||
|
||||
const {
|
||||
readOnly,
|
||||
isDataReady,
|
||||
inputs,
|
||||
handleMethodChange,
|
||||
handleUrlChange,
|
||||
@@ -53,6 +54,9 @@ const Panel: FC<NodePanelProps<HttpNodeType>> = ({
|
||||
setInputVarValues,
|
||||
runResult,
|
||||
} = useConfig(id, data)
|
||||
// To prevent prompt editor in body not update data.
|
||||
if (!isDataReady)
|
||||
return null
|
||||
|
||||
return (
|
||||
<div className='mt-2'>
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import type { CommonNodeType, Variable } from '@/app/components/workflow/types'
|
||||
import type { CommonNodeType, ValueSelector, Variable } from '@/app/components/workflow/types'
|
||||
|
||||
export enum Method {
|
||||
get = 'get',
|
||||
@@ -15,17 +15,32 @@ export enum BodyType {
|
||||
xWwwFormUrlencoded = 'x-www-form-urlencoded',
|
||||
rawText = 'raw-text',
|
||||
json = 'json',
|
||||
binary = 'binary',
|
||||
}
|
||||
|
||||
export type KeyValue = {
|
||||
id?: string
|
||||
key: string
|
||||
value: string
|
||||
type?: string
|
||||
file?: ValueSelector
|
||||
}
|
||||
|
||||
export enum BodyPayloadValueType {
|
||||
text = 'text',
|
||||
file = 'file',
|
||||
}
|
||||
|
||||
export type BodyPayload = {
|
||||
id?: string
|
||||
key?: string
|
||||
type: BodyPayloadValueType
|
||||
file?: ValueSelector // when type is file
|
||||
value?: string // when type is text
|
||||
}[]
|
||||
export type Body = {
|
||||
type: BodyType
|
||||
data: string
|
||||
data: string | BodyPayload // string is deprecated, it would convert to BodyPayload after loaded
|
||||
}
|
||||
|
||||
export enum AuthorizationType {
|
||||
|
@@ -1,12 +1,13 @@
|
||||
import { useCallback, useEffect } from 'react'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import produce from 'immer'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import useVarList from '../_base/hooks/use-var-list'
|
||||
import { VarType } from '../../types'
|
||||
import type { Var } from '../../types'
|
||||
import { useStore } from '../../store'
|
||||
import type { Authorization, Body, HttpNodeType, Method, Timeout } from './types'
|
||||
import { type Authorization, type Body, BodyType, type HttpNodeType, type Method, type Timeout } from './types'
|
||||
import useKeyValueList from './hooks/use-key-value-list'
|
||||
import { transformToBodyPayload } from './utils'
|
||||
import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud'
|
||||
import useOneStepRun from '@/app/components/workflow/nodes/_base/hooks/use-one-step-run'
|
||||
import {
|
||||
@@ -25,15 +26,23 @@ const useConfig = (id: string, payload: HttpNodeType) => {
|
||||
setInputs,
|
||||
})
|
||||
|
||||
const [isDataReady, setIsDataReady] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const isReady = defaultConfig && Object.keys(defaultConfig).length > 0
|
||||
if (isReady) {
|
||||
setInputs({
|
||||
const newInputs = {
|
||||
...defaultConfig,
|
||||
...inputs,
|
||||
})
|
||||
}
|
||||
const bodyData = newInputs.body.data
|
||||
if (typeof bodyData === 'string')
|
||||
newInputs.body.data = transformToBodyPayload(bodyData, [BodyType.formData, BodyType.xWwwFormUrlencoded].includes(newInputs.body.type))
|
||||
|
||||
setInputs(newInputs)
|
||||
setIsDataReady(true)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [defaultConfig])
|
||||
|
||||
const handleMethodChange = useCallback((method: Method) => {
|
||||
@@ -123,11 +132,23 @@ const useConfig = (id: string, payload: HttpNodeType) => {
|
||||
defaultRunInputData: {},
|
||||
})
|
||||
|
||||
const fileVarInputs = useMemo(() => {
|
||||
if (!Array.isArray(inputs.body.data))
|
||||
return ''
|
||||
|
||||
const res = inputs.body.data
|
||||
.filter(item => item.file?.length)
|
||||
.map(item => item.file ? `{{#${item.file.join('.')}#}}` : '')
|
||||
.join(' ')
|
||||
return res
|
||||
}, [inputs.body.data])
|
||||
|
||||
const varInputs = getInputVars([
|
||||
inputs.url,
|
||||
inputs.headers,
|
||||
inputs.params,
|
||||
inputs.body.data,
|
||||
typeof inputs.body.data === 'string' ? inputs.body.data : inputs.body.data.map(item => item.value).join(''),
|
||||
fileVarInputs,
|
||||
])
|
||||
|
||||
const inputVarValues = (() => {
|
||||
@@ -145,6 +166,7 @@ const useConfig = (id: string, payload: HttpNodeType) => {
|
||||
|
||||
return {
|
||||
readOnly,
|
||||
isDataReady,
|
||||
inputs,
|
||||
handleVarListChange,
|
||||
handleAddVariable,
|
||||
|
@@ -1,5 +1,21 @@
|
||||
import type { HttpNodeType } from './types'
|
||||
import { type BodyPayload, BodyPayloadValueType } from './types'
|
||||
|
||||
export const checkNodeValid = (payload: HttpNodeType) => {
|
||||
return true
|
||||
export const transformToBodyPayload = (old: string, hasKey: boolean): BodyPayload => {
|
||||
if (!hasKey) {
|
||||
return [
|
||||
{
|
||||
type: BodyPayloadValueType.text,
|
||||
value: old,
|
||||
},
|
||||
]
|
||||
}
|
||||
const bodyPayload = old.split('\n').map((item) => {
|
||||
const [key, value] = item.split(':')
|
||||
return {
|
||||
key: key || '',
|
||||
type: BodyPayloadValueType.text,
|
||||
value: value || '',
|
||||
}
|
||||
})
|
||||
return bodyPayload
|
||||
}
|
||||
|
@@ -64,6 +64,7 @@ const ConditionAdd = ({
|
||||
<div className='w-[296px] bg-components-panel-bg-blur rounded-lg border-[0.5px] border-components-panel-border shadow-lg'>
|
||||
<VarReferenceVars
|
||||
vars={variables}
|
||||
isSupportFileVar
|
||||
onChange={handleSelectVariable}
|
||||
/>
|
||||
</div>
|
||||
|
@@ -0,0 +1,115 @@
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ComparisonOperator, type Condition } from '../types'
|
||||
import {
|
||||
comparisonOperatorNotRequireValue,
|
||||
isComparisonOperatorNeedTranslate,
|
||||
isEmptyRelatedOperator,
|
||||
} from '../utils'
|
||||
import { FILE_TYPE_OPTIONS, TRANSFER_METHOD } from '../default'
|
||||
import type { ValueSelector } from '../../../types'
|
||||
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
|
||||
import { BubbleX, Env } from '@/app/components/base/icons/src/vender/line/others'
|
||||
import cn from '@/utils/classnames'
|
||||
import { isConversationVar, isENV, isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils'
|
||||
const i18nPrefix = 'workflow.nodes.ifElse'
|
||||
|
||||
type ConditionValueProps = {
|
||||
condition: Condition
|
||||
}
|
||||
const ConditionValue = ({
|
||||
condition,
|
||||
}: ConditionValueProps) => {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
variable_selector,
|
||||
comparison_operator: operator,
|
||||
sub_variable_condition,
|
||||
} = condition
|
||||
|
||||
const variableSelector = variable_selector as ValueSelector
|
||||
|
||||
const variableName = (isSystemVar(variableSelector) ? variableSelector.slice(0).join('.') : variableSelector.slice(1).join('.'))
|
||||
const operatorName = isComparisonOperatorNeedTranslate(operator) ? t(`workflow.nodes.ifElse.comparisonOperator.${operator}`) : operator
|
||||
const notHasValue = comparisonOperatorNotRequireValue(operator)
|
||||
const isEnvVar = isENV(variableSelector)
|
||||
const isChatVar = isConversationVar(variableSelector)
|
||||
const formatValue = useCallback((c: Condition) => {
|
||||
const notHasValue = comparisonOperatorNotRequireValue(c.comparison_operator)
|
||||
if (notHasValue)
|
||||
return ''
|
||||
|
||||
const value = c.value as string
|
||||
return value.replace(/{{#([^#]*)#}}/g, (a, b) => {
|
||||
const arr: string[] = b.split('.')
|
||||
if (isSystemVar(arr))
|
||||
return `{{${b}}}`
|
||||
|
||||
return `{{${arr.slice(1).join('.')}}}`
|
||||
})
|
||||
}, [])
|
||||
|
||||
const isSelect = useCallback((c: Condition) => {
|
||||
return c.comparison_operator === ComparisonOperator.in || c.comparison_operator === ComparisonOperator.notIn
|
||||
}, [])
|
||||
|
||||
const selectName = useCallback((c: Condition) => {
|
||||
const isSelect = c.comparison_operator === ComparisonOperator.in || c.comparison_operator === ComparisonOperator.notIn
|
||||
if (isSelect) {
|
||||
const name = [...FILE_TYPE_OPTIONS, ...TRANSFER_METHOD].filter(item => item.value === (Array.isArray(c.value) ? c.value[0] : c.value))[0]
|
||||
return name
|
||||
? t(`workflow.nodes.ifElse.optionName.${name.i18nKey}`).replace(/{{#([^#]*)#}}/g, (a, b) => {
|
||||
const arr: string[] = b.split('.')
|
||||
if (isSystemVar(arr))
|
||||
return `{{${b}}}`
|
||||
|
||||
return `{{${arr.slice(1).join('.')}}}`
|
||||
})
|
||||
: ''
|
||||
}
|
||||
return ''
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className='rounded-md bg-workflow-block-parma-bg'>
|
||||
<div className='flex items-center px-1 h-6 '>
|
||||
{!isEnvVar && !isChatVar && <Variable02 className='shrink-0 mr-1 w-3.5 h-3.5 text-text-accent' />}
|
||||
{isEnvVar && <Env className='shrink-0 mr-1 w-3.5 h-3.5 text-util-colors-violet-violet-600' />}
|
||||
{isChatVar && <BubbleX className='w-3.5 h-3.5 text-util-colors-teal-teal-700' />}
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
'shrink-0 truncate text-xs font-medium text-text-accent',
|
||||
!notHasValue && 'max-w-[70px]',
|
||||
)}
|
||||
title={variableName}
|
||||
>
|
||||
{variableName}
|
||||
</div>
|
||||
<div
|
||||
className='shrink-0 mx-1 text-xs font-medium text-text-primary'
|
||||
title={operatorName}
|
||||
>
|
||||
{operatorName}
|
||||
</div>
|
||||
</div>
|
||||
<div className='ml-[10px] pl-[10px] border-l border-divider-regular'>
|
||||
{
|
||||
sub_variable_condition?.conditions.map((c: Condition, index) => (
|
||||
<div className='relative flex items-center h-6 space-x-1' key={c.id}>
|
||||
<div className='text-text-accent system-xs-medium'>{c.key}</div>
|
||||
<div className='text-text-primary system-xs-medium'>{isComparisonOperatorNeedTranslate(c.comparison_operator) ? t(`workflow.nodes.ifElse.comparisonOperator.${c.comparison_operator}`) : c.comparison_operator}</div>
|
||||
{c.comparison_operator && !isEmptyRelatedOperator(c.comparison_operator) && <div className='text-text-secondary system-xs-regular'>{isSelect(c) ? selectName(c) : formatValue(c)}</div>}
|
||||
{index !== sub_variable_condition.conditions.length - 1 && (<div className='absolute z-10 right-1 bottom-[-10px] leading-4 text-[10px] font-medium text-text-accent uppercase'>{t(`${i18nPrefix}.${sub_variable_condition.logical_operator}`)}</div>)}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(ConditionValue)
|
@@ -1,64 +1,111 @@
|
||||
import {
|
||||
useCallback,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiDeleteBinLine } from '@remixicon/react'
|
||||
import produce from 'immer'
|
||||
import type { VarType as NumberVarType } from '../../../tool/types'
|
||||
import type {
|
||||
ComparisonOperator,
|
||||
Condition,
|
||||
HandleAddSubVariableCondition,
|
||||
HandleRemoveCondition,
|
||||
HandleToggleSubVariableConditionLogicalOperator,
|
||||
HandleUpdateCondition,
|
||||
HandleUpdateSubVariableCondition,
|
||||
handleRemoveSubVariableCondition,
|
||||
} from '../../types'
|
||||
import { comparisonOperatorNotRequireValue } from '../../utils'
|
||||
import {
|
||||
ComparisonOperator,
|
||||
} from '../../types'
|
||||
import { comparisonOperatorNotRequireValue, getOperators } from '../../utils'
|
||||
import ConditionNumberInput from '../condition-number-input'
|
||||
import { FILE_TYPE_OPTIONS, SUB_VARIABLES, TRANSFER_METHOD } from '../../default'
|
||||
import ConditionWrap from '../condition-wrap'
|
||||
import ConditionOperator from './condition-operator'
|
||||
import ConditionInput from './condition-input'
|
||||
import VariableTag from '@/app/components/workflow/nodes/_base/components/variable-tag'
|
||||
import type {
|
||||
Node,
|
||||
NodeOutPutVar,
|
||||
Var,
|
||||
} from '@/app/components/workflow/types'
|
||||
import { VarType } from '@/app/components/workflow/types'
|
||||
import cn from '@/utils/classnames'
|
||||
import { SimpleSelect as Select } from '@/app/components/base/select'
|
||||
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
|
||||
const optionNameI18NPrefix = 'workflow.nodes.ifElse.optionName'
|
||||
|
||||
type ConditionItemProps = {
|
||||
className?: string
|
||||
disabled?: boolean
|
||||
caseId: string
|
||||
condition: Condition
|
||||
onRemoveCondition: HandleRemoveCondition
|
||||
onUpdateCondition: HandleUpdateCondition
|
||||
conditionId: string // in isSubVariableKey it's the value of the parent condition's id
|
||||
condition: Condition // condition may the condition of case or condition of sub variable
|
||||
file?: { key: string }
|
||||
isSubVariableKey?: boolean
|
||||
isValueFieldShort?: boolean
|
||||
onRemoveCondition?: HandleRemoveCondition
|
||||
onUpdateCondition?: HandleUpdateCondition
|
||||
onAddSubVariableCondition?: HandleAddSubVariableCondition
|
||||
onRemoveSubVariableCondition?: handleRemoveSubVariableCondition
|
||||
onUpdateSubVariableCondition?: HandleUpdateSubVariableCondition
|
||||
onToggleSubVariableConditionLogicalOperator?: HandleToggleSubVariableConditionLogicalOperator
|
||||
nodeId: string
|
||||
nodesOutputVars: NodeOutPutVar[]
|
||||
availableNodes: Node[]
|
||||
numberVariables: NodeOutPutVar[]
|
||||
filterVar: (varPayload: Var) => boolean
|
||||
}
|
||||
const ConditionItem = ({
|
||||
className,
|
||||
disabled,
|
||||
caseId,
|
||||
conditionId,
|
||||
condition,
|
||||
file,
|
||||
isSubVariableKey,
|
||||
isValueFieldShort,
|
||||
onRemoveCondition,
|
||||
onUpdateCondition,
|
||||
onAddSubVariableCondition,
|
||||
onRemoveSubVariableCondition,
|
||||
onUpdateSubVariableCondition,
|
||||
onToggleSubVariableConditionLogicalOperator,
|
||||
nodeId,
|
||||
nodesOutputVars,
|
||||
availableNodes,
|
||||
numberVariables,
|
||||
filterVar,
|
||||
}: ConditionItemProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [isHovered, setIsHovered] = useState(false)
|
||||
|
||||
const doUpdateCondition = useCallback((newCondition: Condition) => {
|
||||
if (isSubVariableKey)
|
||||
onUpdateSubVariableCondition?.(caseId, conditionId, condition.id, newCondition)
|
||||
else
|
||||
onUpdateCondition?.(caseId, condition.id, newCondition)
|
||||
}, [caseId, condition, conditionId, isSubVariableKey, onUpdateCondition, onUpdateSubVariableCondition])
|
||||
|
||||
const canChooseOperator = useMemo(() => {
|
||||
if (disabled)
|
||||
return false
|
||||
|
||||
if (isSubVariableKey)
|
||||
return !!condition.key
|
||||
|
||||
return true
|
||||
}, [condition.key, disabled, isSubVariableKey])
|
||||
const handleUpdateConditionOperator = useCallback((value: ComparisonOperator) => {
|
||||
const newCondition = {
|
||||
...condition,
|
||||
comparison_operator: value,
|
||||
}
|
||||
onUpdateCondition(caseId, condition.id, newCondition)
|
||||
}, [caseId, condition, onUpdateCondition])
|
||||
|
||||
const handleUpdateConditionValue = useCallback((value: string) => {
|
||||
const newCondition = {
|
||||
...condition,
|
||||
value,
|
||||
}
|
||||
onUpdateCondition(caseId, condition.id, newCondition)
|
||||
}, [caseId, condition, onUpdateCondition])
|
||||
doUpdateCondition(newCondition)
|
||||
}, [condition, doUpdateCondition])
|
||||
|
||||
const handleUpdateConditionNumberVarType = useCallback((numberVarType: NumberVarType) => {
|
||||
const newCondition = {
|
||||
@@ -66,37 +113,138 @@ const ConditionItem = ({
|
||||
numberVarType,
|
||||
value: '',
|
||||
}
|
||||
onUpdateCondition(caseId, condition.id, newCondition)
|
||||
}, [caseId, condition, onUpdateCondition])
|
||||
doUpdateCondition(newCondition)
|
||||
}, [condition, doUpdateCondition])
|
||||
|
||||
const isSubVariable = condition.varType === VarType.arrayFile && [ComparisonOperator.contains, ComparisonOperator.notContains, ComparisonOperator.allOf].includes(condition.comparison_operator!)
|
||||
const fileAttr = useMemo(() => {
|
||||
if (file)
|
||||
return file
|
||||
if (isSubVariableKey) {
|
||||
return {
|
||||
key: condition.key!,
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}, [condition.key, file, isSubVariableKey])
|
||||
|
||||
const isArrayValue = fileAttr?.key === 'transfer_method' || fileAttr?.key === 'type'
|
||||
|
||||
const handleUpdateConditionValue = useCallback((value: string) => {
|
||||
if (value === condition.value || (isArrayValue && value === condition.value?.[0]))
|
||||
return
|
||||
const newCondition = {
|
||||
...condition,
|
||||
value: isArrayValue ? [value] : value,
|
||||
}
|
||||
doUpdateCondition(newCondition)
|
||||
}, [condition, doUpdateCondition, fileAttr])
|
||||
|
||||
const isSelect = condition.comparison_operator && [ComparisonOperator.in, ComparisonOperator.notIn].includes(condition.comparison_operator)
|
||||
const selectOptions = useMemo(() => {
|
||||
if (isSelect) {
|
||||
if (fileAttr?.key === 'type' || condition.comparison_operator === ComparisonOperator.allOf) {
|
||||
return FILE_TYPE_OPTIONS.map(item => ({
|
||||
name: t(`${optionNameI18NPrefix}.${item.i18nKey}`),
|
||||
value: item.value,
|
||||
}))
|
||||
}
|
||||
if (fileAttr?.key === 'transfer_method') {
|
||||
return TRANSFER_METHOD.map(item => ({
|
||||
name: t(`${optionNameI18NPrefix}.${item.i18nKey}`),
|
||||
value: item.value,
|
||||
}))
|
||||
}
|
||||
return []
|
||||
}
|
||||
return []
|
||||
}, [condition.comparison_operator, fileAttr?.key, isSelect, t])
|
||||
|
||||
const isNotInput = isSelect || isSubVariable
|
||||
|
||||
const isSubVarSelect = isSubVariableKey
|
||||
const subVarOptions = SUB_VARIABLES.map(item => ({
|
||||
name: item,
|
||||
value: item,
|
||||
}))
|
||||
|
||||
const handleSubVarKeyChange = useCallback((key: string) => {
|
||||
const newCondition = produce(condition, (draft) => {
|
||||
draft.key = key
|
||||
if (key === 'size')
|
||||
draft.varType = VarType.number
|
||||
else
|
||||
draft.varType = VarType.string
|
||||
|
||||
draft.value = ''
|
||||
draft.comparison_operator = getOperators(undefined, { key })[0]
|
||||
})
|
||||
|
||||
onUpdateSubVariableCondition?.(caseId, conditionId, condition.id, newCondition)
|
||||
}, [caseId, condition, conditionId, onUpdateSubVariableCondition])
|
||||
|
||||
const doRemoveCondition = useCallback(() => {
|
||||
if (isSubVariableKey)
|
||||
onRemoveSubVariableCondition?.(caseId, conditionId, condition.id)
|
||||
else
|
||||
onRemoveCondition?.(caseId, condition.id)
|
||||
}, [caseId, condition, conditionId, isSubVariableKey, onRemoveCondition, onRemoveSubVariableCondition])
|
||||
|
||||
return (
|
||||
<div className='flex mb-1 last-of-type:mb-0'>
|
||||
<div className={cn('flex mb-1 last-of-type:mb-0', className)}>
|
||||
<div className={cn(
|
||||
'grow bg-components-input-bg-normal rounded-lg',
|
||||
isHovered && 'bg-state-destructive-hover',
|
||||
)}>
|
||||
<div className='flex items-center p-1'>
|
||||
<div className='grow w-0'>
|
||||
<VariableTag
|
||||
valueSelector={condition.variable_selector}
|
||||
varType={condition.varType}
|
||||
availableNodes={availableNodes}
|
||||
/>
|
||||
{isSubVarSelect
|
||||
? (
|
||||
<Select
|
||||
wrapperClassName='h-6'
|
||||
className='pl-0 text-xs'
|
||||
optionWrapClassName='w-[165px] max-h-none'
|
||||
defaultValue={condition.key}
|
||||
items={subVarOptions}
|
||||
onSelect={item => handleSubVarKeyChange(item.value as string)}
|
||||
renderTrigger={item => (
|
||||
item
|
||||
? <div className='flex justify-start cursor-pointer'>
|
||||
<div className='inline-flex max-w-full px-1.5 items-center h-6 rounded-md border-[0.5px] border-components-panel-border-subtle bg-components-badge-white-to-dark shadow-xs text-text-accent'>
|
||||
<Variable02 className='shrink-0 w-3.5 h-3.5 text-text-accent' />
|
||||
<div className='ml-0.5 truncate system-xs-medium'>{item?.name}</div>
|
||||
</div>
|
||||
</div>
|
||||
: <div className='text-left text-components-input-text-placeholder system-sm-regular'>{t('common.placeholder.select')}</div>
|
||||
)}
|
||||
hideChecked
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<VariableTag
|
||||
valueSelector={condition.variable_selector || []}
|
||||
varType={condition.varType}
|
||||
availableNodes={availableNodes}
|
||||
isShort
|
||||
/>
|
||||
)}
|
||||
|
||||
</div>
|
||||
<div className='mx-1 w-[1px] h-3 bg-divider-regular'></div>
|
||||
<ConditionOperator
|
||||
disabled={disabled}
|
||||
disabled={!canChooseOperator}
|
||||
varType={condition.varType}
|
||||
value={condition.comparison_operator}
|
||||
onSelect={handleUpdateConditionOperator}
|
||||
file={fileAttr}
|
||||
/>
|
||||
</div>
|
||||
{
|
||||
!comparisonOperatorNotRequireValue(condition.comparison_operator) && condition.varType !== VarType.number && (
|
||||
!comparisonOperatorNotRequireValue(condition.comparison_operator) && !isNotInput && condition.varType !== VarType.number && (
|
||||
<div className='px-2 py-1 max-h-[100px] border-t border-t-divider-subtle overflow-y-auto'>
|
||||
<ConditionInput
|
||||
disabled={disabled}
|
||||
value={condition.value}
|
||||
value={condition.value as string}
|
||||
onChange={handleUpdateConditionValue}
|
||||
nodesOutputVars={nodesOutputVars}
|
||||
availableNodes={availableNodes}
|
||||
@@ -105,14 +253,52 @@ const ConditionItem = ({
|
||||
)
|
||||
}
|
||||
{
|
||||
!comparisonOperatorNotRequireValue(condition.comparison_operator) && condition.varType === VarType.number && (
|
||||
!comparisonOperatorNotRequireValue(condition.comparison_operator) && !isNotInput && condition.varType === VarType.number && (
|
||||
<div className='px-2 py-1 pt-[3px] border-t border-t-divider-subtle'>
|
||||
<ConditionNumberInput
|
||||
numberVarType={condition.numberVarType}
|
||||
onNumberVarTypeChange={handleUpdateConditionNumberVarType}
|
||||
value={condition.value}
|
||||
value={condition.value as string}
|
||||
onValueChange={handleUpdateConditionValue}
|
||||
variables={numberVariables}
|
||||
isShort={isValueFieldShort}
|
||||
unit={fileAttr?.key === 'size' ? 'Byte' : undefined}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
!comparisonOperatorNotRequireValue(condition.comparison_operator) && isSelect && (
|
||||
<div className='border-t border-t-divider-subtle'>
|
||||
<Select
|
||||
wrapperClassName='h-8'
|
||||
className='px-2 text-xs rounded-t-none'
|
||||
defaultValue={isArrayValue ? (condition.value as string[])?.[0] : (condition.value as string)}
|
||||
items={selectOptions}
|
||||
onSelect={item => handleUpdateConditionValue(item.value as string)}
|
||||
hideChecked
|
||||
notClearable
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
!comparisonOperatorNotRequireValue(condition.comparison_operator) && isSubVariable && (
|
||||
<div className='p-1'>
|
||||
<ConditionWrap
|
||||
isSubVariable
|
||||
caseId={caseId}
|
||||
conditionId={conditionId}
|
||||
readOnly={!!disabled}
|
||||
cases={condition.sub_variable_condition ? [condition.sub_variable_condition] : []}
|
||||
handleAddSubVariableCondition={onAddSubVariableCondition}
|
||||
handleRemoveSubVariableCondition={onRemoveSubVariableCondition}
|
||||
handleUpdateSubVariableCondition={onUpdateSubVariableCondition}
|
||||
handleToggleSubVariableConditionLogicalOperator={onToggleSubVariableConditionLogicalOperator}
|
||||
nodeId={nodeId}
|
||||
nodesOutputVars={nodesOutputVars}
|
||||
availableNodes={availableNodes}
|
||||
filterVar={filterVar}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
@@ -122,7 +308,7 @@ const ConditionItem = ({
|
||||
className='shrink-0 flex items-center justify-center ml-1 mt-1 w-6 h-6 rounded-lg cursor-pointer hover:bg-state-destructive-hover text-text-tertiary hover:text-text-destructive'
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
onClick={() => onRemoveCondition(caseId, condition.id)}
|
||||
onClick={doRemoveCondition}
|
||||
>
|
||||
<RiDeleteBinLine className='w-4 h-4' />
|
||||
</div>
|
||||
|
@@ -17,14 +17,18 @@ import cn from '@/utils/classnames'
|
||||
const i18nPrefix = 'workflow.nodes.ifElse'
|
||||
|
||||
type ConditionOperatorProps = {
|
||||
className?: string
|
||||
disabled?: boolean
|
||||
varType: VarType
|
||||
file?: { key: string }
|
||||
value?: string
|
||||
onSelect: (value: ComparisonOperator) => void
|
||||
}
|
||||
const ConditionOperator = ({
|
||||
className,
|
||||
disabled,
|
||||
varType,
|
||||
file,
|
||||
value,
|
||||
onSelect,
|
||||
}: ConditionOperatorProps) => {
|
||||
@@ -32,15 +36,14 @@ const ConditionOperator = ({
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
const options = useMemo(() => {
|
||||
return getOperators(varType).map((o) => {
|
||||
return getOperators(varType, file).map((o) => {
|
||||
return {
|
||||
label: isComparisonOperatorNeedTranslate(o) ? t(`${i18nPrefix}.comparisonOperator.${o}`) : o,
|
||||
value: o,
|
||||
}
|
||||
})
|
||||
}, [t, varType])
|
||||
const selectedOption = options.find(o => o.value === value)
|
||||
|
||||
}, [t, varType, file])
|
||||
const selectedOption = options.find(o => Array.isArray(value) ? o.value === value[0] : o.value === value)
|
||||
return (
|
||||
<PortalToFollowElem
|
||||
open={open}
|
||||
@@ -53,7 +56,7 @@ const ConditionOperator = ({
|
||||
>
|
||||
<PortalToFollowElemTrigger onClick={() => setOpen(v => !v)}>
|
||||
<Button
|
||||
className={cn('shrink-0', !selectedOption && 'opacity-50')}
|
||||
className={cn('shrink-0', !selectedOption && 'opacity-50', className)}
|
||||
size='small'
|
||||
variant='ghost'
|
||||
disabled={disabled}
|
||||
@@ -61,7 +64,7 @@ const ConditionOperator = ({
|
||||
{
|
||||
selectedOption
|
||||
? selectedOption.label
|
||||
: 'select'
|
||||
: t(`${i18nPrefix}.select`)
|
||||
}
|
||||
<RiArrowDownSLine className='ml-1 w-3.5 h-3.5' />
|
||||
</Button>
|
||||
|
@@ -1,51 +1,101 @@
|
||||
import { RiLoopLeftLine } from '@remixicon/react'
|
||||
import { LogicalOperator } from '../../types'
|
||||
import type {
|
||||
CaseItem,
|
||||
HandleRemoveCondition,
|
||||
HandleUpdateCondition,
|
||||
HandleUpdateConditionLogicalOperator,
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import {
|
||||
type CaseItem,
|
||||
type HandleAddSubVariableCondition,
|
||||
type HandleRemoveCondition,
|
||||
type HandleToggleConditionLogicalOperator,
|
||||
type HandleToggleSubVariableConditionLogicalOperator,
|
||||
type HandleUpdateCondition,
|
||||
type HandleUpdateSubVariableCondition,
|
||||
LogicalOperator,
|
||||
type handleRemoveSubVariableCondition,
|
||||
} from '../../types'
|
||||
import ConditionItem from './condition-item'
|
||||
import type {
|
||||
Node,
|
||||
NodeOutPutVar,
|
||||
Var,
|
||||
} from '@/app/components/workflow/types'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type ConditionListProps = {
|
||||
isSubVariable?: boolean
|
||||
disabled?: boolean
|
||||
caseId: string
|
||||
conditionId?: string
|
||||
caseItem: CaseItem
|
||||
onUpdateCondition: HandleUpdateCondition
|
||||
onUpdateConditionLogicalOperator: HandleUpdateConditionLogicalOperator
|
||||
onRemoveCondition: HandleRemoveCondition
|
||||
onRemoveCondition?: HandleRemoveCondition
|
||||
onUpdateCondition?: HandleUpdateCondition
|
||||
onToggleConditionLogicalOperator?: HandleToggleConditionLogicalOperator
|
||||
nodeId: string
|
||||
nodesOutputVars: NodeOutPutVar[]
|
||||
availableNodes: Node[]
|
||||
numberVariables: NodeOutPutVar[]
|
||||
filterVar: (varPayload: Var) => boolean
|
||||
varsIsVarFileAttribute: Record<string, boolean>
|
||||
onAddSubVariableCondition?: HandleAddSubVariableCondition
|
||||
onRemoveSubVariableCondition?: handleRemoveSubVariableCondition
|
||||
onUpdateSubVariableCondition?: HandleUpdateSubVariableCondition
|
||||
onToggleSubVariableConditionLogicalOperator?: HandleToggleSubVariableConditionLogicalOperator
|
||||
}
|
||||
const ConditionList = ({
|
||||
isSubVariable,
|
||||
disabled,
|
||||
caseId,
|
||||
conditionId,
|
||||
caseItem,
|
||||
onUpdateCondition,
|
||||
onUpdateConditionLogicalOperator,
|
||||
onRemoveCondition,
|
||||
onToggleConditionLogicalOperator,
|
||||
onAddSubVariableCondition,
|
||||
onRemoveSubVariableCondition,
|
||||
onUpdateSubVariableCondition,
|
||||
onToggleSubVariableConditionLogicalOperator,
|
||||
nodeId,
|
||||
nodesOutputVars,
|
||||
availableNodes,
|
||||
numberVariables,
|
||||
varsIsVarFileAttribute,
|
||||
filterVar,
|
||||
}: ConditionListProps) => {
|
||||
const { conditions, logical_operator } = caseItem
|
||||
|
||||
const doToggleConditionLogicalOperator = useCallback(() => {
|
||||
if (isSubVariable)
|
||||
onToggleSubVariableConditionLogicalOperator?.(caseId!, conditionId!)
|
||||
else
|
||||
onToggleConditionLogicalOperator?.(caseId)
|
||||
}, [caseId, conditionId, isSubVariable, onToggleConditionLogicalOperator, onToggleSubVariableConditionLogicalOperator])
|
||||
|
||||
const isValueFieldShort = useMemo(() => {
|
||||
if (isSubVariable && conditions.length > 1)
|
||||
return true
|
||||
|
||||
return false
|
||||
}, [conditions.length, isSubVariable])
|
||||
const conditionItemClassName = useMemo(() => {
|
||||
if (!isSubVariable)
|
||||
return ''
|
||||
if (conditions.length < 2)
|
||||
return ''
|
||||
return logical_operator === LogicalOperator.and ? 'pl-[51px]' : 'pl-[42px]'
|
||||
}, [conditions.length, isSubVariable, logical_operator])
|
||||
|
||||
return (
|
||||
<div className='relative pl-[60px]'>
|
||||
<div className={cn('relative', !isSubVariable && 'pl-[60px]')}>
|
||||
{
|
||||
conditions.length > 1 && (
|
||||
<div className='absolute top-0 bottom-0 left-0 w-[60px]'>
|
||||
<div className={cn(
|
||||
'absolute top-0 bottom-0 left-0 w-[60px]',
|
||||
isSubVariable && logical_operator === LogicalOperator.and && 'left-[-10px]',
|
||||
isSubVariable && logical_operator === LogicalOperator.or && 'left-[-18px]',
|
||||
)}>
|
||||
<div className='absolute top-4 bottom-4 left-[46px] w-2.5 border border-divider-deep rounded-l-[8px] border-r-0'></div>
|
||||
<div className='absolute top-1/2 -translate-y-1/2 right-0 w-4 h-[29px] bg-components-panel-bg'></div>
|
||||
<div
|
||||
className='absolute top-1/2 right-1 -translate-y-1/2 flex items-center px-1 h-[21px] rounded-md border-[0.5px] border-components-button-secondary-border shadow-xs bg-components-button-secondary-bg text-text-accent-secondary text-[10px] font-semibold cursor-pointer'
|
||||
onClick={() => {
|
||||
onUpdateConditionLogicalOperator(caseItem.case_id, caseItem.logical_operator === LogicalOperator.and ? LogicalOperator.or : LogicalOperator.and)
|
||||
}}
|
||||
className='absolute top-1/2 right-1 -translate-y-1/2 flex items-center px-1 h-[21px] rounded-md border-[0.5px] border-components-button-secondary-border shadow-xs bg-components-button-secondary-bg text-text-accent-secondary text-[10px] font-semibold cursor-pointer select-none'
|
||||
onClick={doToggleConditionLogicalOperator}
|
||||
>
|
||||
{logical_operator.toUpperCase()}
|
||||
<RiLoopLeftLine className='ml-0.5 w-3 h-3' />
|
||||
@@ -57,14 +107,25 @@ const ConditionList = ({
|
||||
caseItem.conditions.map(condition => (
|
||||
<ConditionItem
|
||||
key={condition.id}
|
||||
className={conditionItemClassName}
|
||||
disabled={disabled}
|
||||
caseId={caseItem.case_id}
|
||||
caseId={caseId}
|
||||
conditionId={isSubVariable ? conditionId! : condition.id}
|
||||
condition={condition}
|
||||
isValueFieldShort={isValueFieldShort}
|
||||
onUpdateCondition={onUpdateCondition}
|
||||
onRemoveCondition={onRemoveCondition}
|
||||
onAddSubVariableCondition={onAddSubVariableCondition}
|
||||
onRemoveSubVariableCondition={onRemoveSubVariableCondition}
|
||||
onUpdateSubVariableCondition={onUpdateSubVariableCondition}
|
||||
onToggleSubVariableConditionLogicalOperator={onToggleSubVariableConditionLogicalOperator}
|
||||
nodeId={nodeId}
|
||||
nodesOutputVars={nodesOutputVars}
|
||||
availableNodes={availableNodes}
|
||||
filterVar={filterVar}
|
||||
numberVariables={numberVariables}
|
||||
file={varsIsVarFileAttribute[condition.id] ? { key: (condition.variable_selector || []).slice(-1)[0] } : undefined}
|
||||
isSubVariableKey={isSubVariable}
|
||||
/>
|
||||
))
|
||||
}
|
||||
|
@@ -6,6 +6,7 @@ import {
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiArrowDownSLine } from '@remixicon/react'
|
||||
import { capitalize } from 'lodash-es'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import { VarType as NumberVarType } from '../../tool/types'
|
||||
import VariableTag from '../../_base/components/variable-tag'
|
||||
import {
|
||||
@@ -35,6 +36,8 @@ type ConditionNumberInputProps = {
|
||||
value: string
|
||||
onValueChange: (v: string) => void
|
||||
variables: NodeOutPutVar[]
|
||||
isShort?: boolean
|
||||
unit?: string
|
||||
}
|
||||
const ConditionNumberInput = ({
|
||||
numberVarType = NumberVarType.constant,
|
||||
@@ -42,10 +45,16 @@ const ConditionNumberInput = ({
|
||||
value,
|
||||
onValueChange,
|
||||
variables,
|
||||
isShort,
|
||||
unit,
|
||||
}: ConditionNumberInputProps) => {
|
||||
const { t } = useTranslation()
|
||||
const [numberVarTypeVisible, setNumberVarTypeVisible] = useState(false)
|
||||
const [variableSelectorVisible, setVariableSelectorVisible] = useState(false)
|
||||
const [isFocus, {
|
||||
setTrue: setFocus,
|
||||
setFalse: setBlur,
|
||||
}] = useBoolean()
|
||||
|
||||
const handleSelectVariable = useCallback((valueSelector: ValueSelector) => {
|
||||
onValueChange(variableTransformer(valueSelector) as string)
|
||||
@@ -111,20 +120,21 @@ const ConditionNumberInput = ({
|
||||
<VariableTag
|
||||
valueSelector={variableTransformer(value) as string[]}
|
||||
varType={VarType.number}
|
||||
isShort={isShort}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
!value && (
|
||||
<div className='flex items-center p-1 h-6 text-components-input-text-placeholder text-[13px]'>
|
||||
<Variable02 className='mr-1 w-4 h-4' />
|
||||
{t('workflow.nodes.ifElse.selectVariable')}
|
||||
<Variable02 className='shrink-0 mr-1 w-4 h-4' />
|
||||
<div className='w-0 grow truncate'>{t('workflow.nodes.ifElse.selectVariable')}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className='z-[1000]'>
|
||||
<div className='w-[296px] bg-components-panel-bg-blur rounded-lg border-[0.5px] border-components-panel-border shadow-lg'>
|
||||
<div className={cn('w-[296px] pt-1 bg-components-panel-bg-blur rounded-lg border-[0.5px] border-components-panel-border shadow-lg', isShort && 'w-[200px]')}>
|
||||
<VarReferenceVars
|
||||
vars={variables}
|
||||
onChange={handleSelectVariable}
|
||||
@@ -136,13 +146,18 @@ const ConditionNumberInput = ({
|
||||
}
|
||||
{
|
||||
numberVarType === NumberVarType.constant && (
|
||||
<input
|
||||
className='block w-full px-2 text-[13px] text-components-input-text-filled placeholder:text-components-input-text-placeholder outline-none appearance-none bg-transparent'
|
||||
type='number'
|
||||
value={value}
|
||||
onChange={e => onValueChange(e.target.value)}
|
||||
placeholder={t('workflow.nodes.ifElse.enterValue') || ''}
|
||||
/>
|
||||
<div className=' relative'>
|
||||
<input
|
||||
className={cn('block w-full px-2 text-[13px] text-components-input-text-filled placeholder:text-components-input-text-placeholder outline-none appearance-none bg-transparent', unit && 'pr-6')}
|
||||
type='number'
|
||||
value={value}
|
||||
onChange={e => onValueChange(e.target.value)}
|
||||
placeholder={t('workflow.nodes.ifElse.enterValue') || ''}
|
||||
onFocus={setFocus}
|
||||
onBlur={setBlur}
|
||||
/>
|
||||
{!isFocus && unit && <div className='absolute right-2 top-[50%] translate-y-[-50%] text-text-tertiary system-sm-regular'>{unit}</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
|
@@ -3,11 +3,12 @@ import {
|
||||
useMemo,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { ComparisonOperator } from '../types'
|
||||
import { ComparisonOperator } from '../types'
|
||||
import {
|
||||
comparisonOperatorNotRequireValue,
|
||||
isComparisonOperatorNeedTranslate,
|
||||
} from '../utils'
|
||||
import { FILE_TYPE_OPTIONS, TRANSFER_METHOD } from '../default'
|
||||
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
|
||||
import { BubbleX, Env } from '@/app/components/base/icons/src/vender/line/others'
|
||||
import cn from '@/utils/classnames'
|
||||
@@ -15,16 +16,18 @@ import { isConversationVar, isENV, isSystemVar } from '@/app/components/workflow
|
||||
|
||||
type ConditionValueProps = {
|
||||
variableSelector: string[]
|
||||
labelName?: string
|
||||
operator: ComparisonOperator
|
||||
value: string
|
||||
value: string | string[]
|
||||
}
|
||||
const ConditionValue = ({
|
||||
variableSelector,
|
||||
labelName,
|
||||
operator,
|
||||
value,
|
||||
}: ConditionValueProps) => {
|
||||
const { t } = useTranslation()
|
||||
const variableName = isSystemVar(variableSelector) ? variableSelector.slice(0).join('.') : variableSelector.slice(1).join('.')
|
||||
const variableName = labelName || (isSystemVar(variableSelector) ? variableSelector.slice(0).join('.') : variableSelector.slice(1).join('.'))
|
||||
const operatorName = isComparisonOperatorNeedTranslate(operator) ? t(`workflow.nodes.ifElse.comparisonOperator.${operator}`) : operator
|
||||
const notHasValue = comparisonOperatorNotRequireValue(operator)
|
||||
const isEnvVar = isENV(variableSelector)
|
||||
@@ -33,6 +36,9 @@ const ConditionValue = ({
|
||||
if (notHasValue)
|
||||
return ''
|
||||
|
||||
if (Array.isArray(value)) // transfer method
|
||||
return value[0]
|
||||
|
||||
return value.replace(/{{#([^#]*)#}}/g, (a, b) => {
|
||||
const arr: string[] = b.split('.')
|
||||
if (isSystemVar(arr))
|
||||
@@ -42,6 +48,23 @@ const ConditionValue = ({
|
||||
})
|
||||
}, [notHasValue, value])
|
||||
|
||||
const isSelect = operator === ComparisonOperator.in || operator === ComparisonOperator.notIn
|
||||
const selectName = useMemo(() => {
|
||||
if (isSelect) {
|
||||
const name = [...FILE_TYPE_OPTIONS, ...TRANSFER_METHOD].filter(item => item.value === (Array.isArray(value) ? value[0] : value))[0]
|
||||
return name
|
||||
? t(`workflow.nodes.ifElse.optionName.${name.i18nKey}`).replace(/{{#([^#]*)#}}/g, (a, b) => {
|
||||
const arr: string[] = b.split('.')
|
||||
if (isSystemVar(arr))
|
||||
return `{{${b}}}`
|
||||
|
||||
return `{{${arr.slice(1).join('.')}}}`
|
||||
})
|
||||
: ''
|
||||
}
|
||||
return ''
|
||||
}, [isSelect, t, value])
|
||||
|
||||
return (
|
||||
<div className='flex items-center px-1 h-6 rounded-md bg-workflow-block-parma-bg'>
|
||||
{!isEnvVar && !isChatVar && <Variable02 className='shrink-0 mr-1 w-3.5 h-3.5 text-text-accent' />}
|
||||
@@ -65,7 +88,7 @@ const ConditionValue = ({
|
||||
</div>
|
||||
{
|
||||
!notHasValue && (
|
||||
<div className='truncate text-xs text-text-secondary' title={formatValue}>{formatValue}</div>
|
||||
<div className='truncate text-xs text-text-secondary' title={formatValue}>{isSelect ? selectName : formatValue}</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
|
@@ -0,0 +1,225 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ReactSortable } from 'react-sortablejs'
|
||||
import {
|
||||
RiAddLine,
|
||||
RiDeleteBinLine,
|
||||
RiDraggable,
|
||||
} from '@remixicon/react'
|
||||
import type { CaseItem, HandleAddCondition, HandleAddSubVariableCondition, HandleRemoveCondition, HandleToggleConditionLogicalOperator, HandleToggleSubVariableConditionLogicalOperator, HandleUpdateCondition, HandleUpdateSubVariableCondition, handleRemoveSubVariableCondition } from '../types'
|
||||
import type { Node, NodeOutPutVar, Var } from '../../../types'
|
||||
import { VarType } from '../../../types'
|
||||
import { useGetAvailableVars } from '../../variable-assigner/hooks'
|
||||
import { SUB_VARIABLES } from '../default'
|
||||
import ConditionList from './condition-list'
|
||||
import ConditionAdd from './condition-add'
|
||||
import cn from '@/utils/classnames'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { PortalSelect as Select } from '@/app/components/base/select'
|
||||
|
||||
type Props = {
|
||||
isSubVariable?: boolean
|
||||
caseId?: string
|
||||
conditionId?: string
|
||||
cases: CaseItem[]
|
||||
readOnly: boolean
|
||||
handleSortCase?: (sortedCases: (CaseItem & { id: string })[]) => void
|
||||
handleRemoveCase?: (caseId: string) => void
|
||||
handleAddCondition?: HandleAddCondition
|
||||
handleRemoveCondition?: HandleRemoveCondition
|
||||
handleUpdateCondition?: HandleUpdateCondition
|
||||
handleToggleConditionLogicalOperator?: HandleToggleConditionLogicalOperator
|
||||
handleAddSubVariableCondition?: HandleAddSubVariableCondition
|
||||
handleRemoveSubVariableCondition?: handleRemoveSubVariableCondition
|
||||
handleUpdateSubVariableCondition?: HandleUpdateSubVariableCondition
|
||||
handleToggleSubVariableConditionLogicalOperator?: HandleToggleSubVariableConditionLogicalOperator
|
||||
nodeId: string
|
||||
nodesOutputVars: NodeOutPutVar[]
|
||||
availableNodes: Node[]
|
||||
varsIsVarFileAttribute?: Record<string, boolean>
|
||||
filterVar: (varPayload: Var) => boolean
|
||||
}
|
||||
|
||||
const ConditionWrap: FC<Props> = ({
|
||||
isSubVariable,
|
||||
caseId,
|
||||
conditionId,
|
||||
nodeId: id = '',
|
||||
cases = [],
|
||||
readOnly,
|
||||
handleSortCase = () => { },
|
||||
handleRemoveCase,
|
||||
handleUpdateCondition,
|
||||
handleAddCondition,
|
||||
handleRemoveCondition,
|
||||
handleToggleConditionLogicalOperator,
|
||||
handleAddSubVariableCondition,
|
||||
handleRemoveSubVariableCondition,
|
||||
handleUpdateSubVariableCondition,
|
||||
handleToggleSubVariableConditionLogicalOperator,
|
||||
nodesOutputVars = [],
|
||||
availableNodes = [],
|
||||
varsIsVarFileAttribute = {},
|
||||
filterVar = () => true,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const getAvailableVars = useGetAvailableVars()
|
||||
|
||||
const [willDeleteCaseId, setWillDeleteCaseId] = useState('')
|
||||
const casesLength = cases.length
|
||||
|
||||
const filterNumberVar = useCallback((varPayload: Var) => {
|
||||
return varPayload.type === VarType.number
|
||||
}, [])
|
||||
|
||||
const subVarOptions = SUB_VARIABLES.map(item => ({
|
||||
name: item,
|
||||
value: item,
|
||||
}))
|
||||
|
||||
return (
|
||||
<>
|
||||
<ReactSortable
|
||||
list={cases.map(caseItem => ({ ...caseItem, id: caseItem.case_id }))}
|
||||
setList={handleSortCase}
|
||||
handle='.handle'
|
||||
ghostClass='bg-components-panel-bg'
|
||||
animation={150}
|
||||
disabled={readOnly || isSubVariable}
|
||||
>
|
||||
{
|
||||
cases.map((item, index) => (
|
||||
<div key={item.case_id}>
|
||||
<div
|
||||
className={cn(
|
||||
'group relative rounded-[10px] bg-components-panel-bg',
|
||||
willDeleteCaseId === item.case_id && 'bg-state-destructive-hover',
|
||||
!isSubVariable && 'py-1 px-3 min-h-[40px] ',
|
||||
isSubVariable && 'px-1 py-2',
|
||||
)}
|
||||
>
|
||||
{!isSubVariable && (
|
||||
<>
|
||||
<RiDraggable className={cn(
|
||||
'hidden handle absolute top-2 left-1 w-3 h-3 text-text-quaternary cursor-pointer',
|
||||
casesLength > 1 && 'group-hover:block',
|
||||
)} />
|
||||
<div className={cn(
|
||||
'absolute left-4 leading-4 text-[13px] font-semibold text-text-secondary',
|
||||
casesLength === 1 ? 'top-2.5' : 'top-1',
|
||||
)}>
|
||||
{
|
||||
index === 0 ? 'IF' : 'ELIF'
|
||||
}
|
||||
{
|
||||
casesLength > 1 && (
|
||||
<div className='text-[10px] text-text-tertiary font-medium'>CASE {index + 1}</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{
|
||||
!!item.conditions.length && (
|
||||
<div className='mb-2'>
|
||||
<ConditionList
|
||||
disabled={readOnly}
|
||||
caseItem={item}
|
||||
caseId={isSubVariable ? caseId! : item.case_id}
|
||||
conditionId={conditionId}
|
||||
onUpdateCondition={handleUpdateCondition}
|
||||
onRemoveCondition={handleRemoveCondition}
|
||||
onToggleConditionLogicalOperator={handleToggleConditionLogicalOperator}
|
||||
nodeId={id}
|
||||
nodesOutputVars={nodesOutputVars}
|
||||
availableNodes={availableNodes}
|
||||
filterVar={filterVar}
|
||||
numberVariables={getAvailableVars(id, '', filterNumberVar)}
|
||||
varsIsVarFileAttribute={varsIsVarFileAttribute}
|
||||
onAddSubVariableCondition={handleAddSubVariableCondition}
|
||||
onRemoveSubVariableCondition={handleRemoveSubVariableCondition}
|
||||
onUpdateSubVariableCondition={handleUpdateSubVariableCondition}
|
||||
onToggleSubVariableConditionLogicalOperator={handleToggleSubVariableConditionLogicalOperator}
|
||||
isSubVariable={isSubVariable}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<div className={cn(
|
||||
'flex items-center justify-between pr-[30px]',
|
||||
!item.conditions.length && !isSubVariable && 'mt-1',
|
||||
!item.conditions.length && isSubVariable && 'mt-2',
|
||||
!isSubVariable && ' pl-[60px]',
|
||||
)}>
|
||||
{isSubVariable
|
||||
? (
|
||||
<Select
|
||||
popupInnerClassName='w-[165px] max-h-none'
|
||||
onSelect={value => handleAddSubVariableCondition?.(caseId!, conditionId!, value.value as string)}
|
||||
items={subVarOptions}
|
||||
value=''
|
||||
renderTrigger={() => (
|
||||
<Button
|
||||
size='small'
|
||||
disabled={readOnly}
|
||||
>
|
||||
<RiAddLine className='mr-1 w-3.5 h-3.5' />
|
||||
{t('workflow.nodes.ifElse.addSubVariable')}
|
||||
</Button>
|
||||
)}
|
||||
hideChecked
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<ConditionAdd
|
||||
disabled={readOnly}
|
||||
caseId={item.case_id}
|
||||
variables={getAvailableVars(id, '', filterVar)}
|
||||
onSelectVariable={handleAddCondition!}
|
||||
/>
|
||||
)}
|
||||
|
||||
{
|
||||
((index === 0 && casesLength > 1) || (index > 0)) && (
|
||||
<Button
|
||||
className='hover:text-components-button-destructive-ghost-text hover:bg-components-button-destructive-ghost-bg-hover'
|
||||
size='small'
|
||||
variant='ghost'
|
||||
disabled={readOnly}
|
||||
onClick={() => handleRemoveCase?.(item.case_id)}
|
||||
onMouseEnter={() => setWillDeleteCaseId(item.case_id)}
|
||||
onMouseLeave={() => setWillDeleteCaseId('')}
|
||||
>
|
||||
<RiDeleteBinLine className='mr-1 w-3.5 h-3.5' />
|
||||
{t('common.operation.remove')}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
{!isSubVariable && (
|
||||
<div className='my-2 mx-3 h-[1px] bg-divider-subtle'></div>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</ReactSortable>
|
||||
{(cases.length === 0) && (
|
||||
<Button
|
||||
size='small'
|
||||
disabled={readOnly}
|
||||
onClick={() => handleAddSubVariableCondition?.(caseId!, conditionId!)}
|
||||
>
|
||||
<RiAddLine className='mr-1 w-3.5 h-3.5' />
|
||||
{t('workflow.nodes.ifElse.addSubVariable')}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default React.memo(ConditionWrap)
|
@@ -1,6 +1,7 @@
|
||||
import { BlockEnum, type NodeDefault } from '../../types'
|
||||
import { type IfElseNodeType, LogicalOperator } from './types'
|
||||
import { isEmptyRelatedOperator } from './utils'
|
||||
import { TransferMethod } from '@/types/app'
|
||||
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/constants'
|
||||
const i18nPrefix = 'workflow.errorMsg'
|
||||
|
||||
@@ -49,8 +50,25 @@ const nodeDefault: NodeDefault<IfElseNodeType> = {
|
||||
errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t(`${i18nPrefix}.fields.variable`) })
|
||||
if (!errorMessages && !condition.comparison_operator)
|
||||
errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t('workflow.nodes.ifElse.operator') })
|
||||
if (!errorMessages && !isEmptyRelatedOperator(condition.comparison_operator!) && !condition.value)
|
||||
errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t(`${i18nPrefix}.fields.variableValue`) })
|
||||
if (!errorMessages) {
|
||||
if (condition.sub_variable_condition) {
|
||||
const isSet = condition.sub_variable_condition.conditions.every((c) => {
|
||||
if (!c.comparison_operator)
|
||||
return false
|
||||
|
||||
if (isEmptyRelatedOperator(c.comparison_operator!))
|
||||
return true
|
||||
|
||||
return !!c.value
|
||||
})
|
||||
if (!isSet)
|
||||
errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t(`${i18nPrefix}.fields.variableValue`) })
|
||||
}
|
||||
else {
|
||||
if (!isEmptyRelatedOperator(condition.comparison_operator!) && !condition.value)
|
||||
errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t(`${i18nPrefix}.fields.variableValue`) })
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
return {
|
||||
@@ -61,3 +79,18 @@ const nodeDefault: NodeDefault<IfElseNodeType> = {
|
||||
}
|
||||
|
||||
export default nodeDefault
|
||||
|
||||
export const FILE_TYPE_OPTIONS = [
|
||||
{ value: 'image', i18nKey: 'image' },
|
||||
{ value: 'document', i18nKey: 'doc' },
|
||||
{ value: 'audio', i18nKey: 'audio' },
|
||||
{ value: 'video', i18nKey: 'video' },
|
||||
]
|
||||
|
||||
export const TRANSFER_METHOD = [
|
||||
{ value: TransferMethod.local_file, i18nKey: 'localUpload' },
|
||||
{ value: TransferMethod.remote_url, i18nKey: 'url' },
|
||||
]
|
||||
|
||||
export const SUB_VARIABLES = ['type', 'size', 'name', 'url', 'extension', 'mime_type', 'transfer_method']
|
||||
export const OUTPUT_FILE_SUB_VARIABLES = SUB_VARIABLES.filter(key => key !== 'transfer_method')
|
||||
|
@@ -1,11 +1,12 @@
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { NodeProps } from 'reactflow'
|
||||
import { NodeSourceHandle } from '../_base/components/node-handle'
|
||||
import { isEmptyRelatedOperator } from './utils'
|
||||
import type { IfElseNodeType } from './types'
|
||||
import type { Condition, IfElseNodeType } from './types'
|
||||
import ConditionValue from './components/condition-value'
|
||||
import ConditionFilesListValue from './components/condition-files-list-value'
|
||||
const i18nPrefix = 'workflow.nodes.ifElse'
|
||||
|
||||
const IfElseNode: FC<NodeProps<IfElseNodeType>> = (props) => {
|
||||
@@ -13,6 +14,32 @@ const IfElseNode: FC<NodeProps<IfElseNodeType>> = (props) => {
|
||||
const { t } = useTranslation()
|
||||
const { cases } = data
|
||||
const casesLength = cases.length
|
||||
const checkIsConditionSet = useCallback((condition: Condition) => {
|
||||
if (!condition.variable_selector || condition.variable_selector.length === 0)
|
||||
return false
|
||||
|
||||
if (condition.sub_variable_condition) {
|
||||
const isSet = condition.sub_variable_condition.conditions.every((c) => {
|
||||
if (!c.comparison_operator)
|
||||
return false
|
||||
|
||||
if (isEmptyRelatedOperator(c.comparison_operator!))
|
||||
return true
|
||||
|
||||
return !!c.value
|
||||
})
|
||||
return isSet
|
||||
}
|
||||
else {
|
||||
if (isEmptyRelatedOperator(condition.comparison_operator!))
|
||||
return true
|
||||
|
||||
return !!condition.value
|
||||
}
|
||||
}, [])
|
||||
const conditionNotSet = (<div className='flex items-center h-6 px-1 space-x-1 text-xs font-normal text-text-secondary bg-workflow-block-parma-bg rounded-md'>
|
||||
{t(`${i18nPrefix}.conditionNotSetup`)}
|
||||
</div>)
|
||||
|
||||
return (
|
||||
<div className='px-3'>
|
||||
@@ -35,21 +62,25 @@ const IfElseNode: FC<NodeProps<IfElseNodeType>> = (props) => {
|
||||
<div className='space-y-0.5'>
|
||||
{caseItem.conditions.map((condition, i) => (
|
||||
<div key={condition.id} className='relative'>
|
||||
{(condition.variable_selector?.length > 0 && condition.comparison_operator && (isEmptyRelatedOperator(condition.comparison_operator!) ? true : !!condition.value))
|
||||
? (
|
||||
<ConditionValue
|
||||
variableSelector={condition.variable_selector}
|
||||
operator={condition.comparison_operator}
|
||||
value={condition.value}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<div className='flex items-center h-6 px-1 space-x-1 text-xs font-normal text-text-secondary bg-workflow-block-parma-bg rounded-md'>
|
||||
{t(`${i18nPrefix}.conditionNotSetup`)}
|
||||
</div>
|
||||
)}
|
||||
{
|
||||
checkIsConditionSet(condition)
|
||||
? (
|
||||
(!isEmptyRelatedOperator(condition.comparison_operator!) && condition.sub_variable_condition)
|
||||
? (
|
||||
<ConditionFilesListValue condition={condition} />
|
||||
)
|
||||
: (
|
||||
<ConditionValue
|
||||
variableSelector={condition.variable_selector!}
|
||||
operator={condition.comparison_operator!}
|
||||
value={condition.value}
|
||||
/>
|
||||
)
|
||||
|
||||
)
|
||||
: conditionNotSet}
|
||||
{i !== caseItem.conditions.length - 1 && (
|
||||
<div className='absolute z-10 right-0 bottom-[-10px] leading-4 text-[10px] font-medium text-text-accent uppercase'>{t(`${i18nPrefix}.${caseItem.logical_operator}`)}</div>
|
||||
<div className='absolute z-10 right-1 bottom-[-10px] leading-4 text-[10px] font-medium text-text-accent uppercase'>{t(`${i18nPrefix}.${caseItem.logical_operator}`)}</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
@@ -1,24 +1,18 @@
|
||||
import type { FC } from 'react'
|
||||
import {
|
||||
memo,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ReactSortable } from 'react-sortablejs'
|
||||
import {
|
||||
RiAddLine,
|
||||
RiDeleteBinLine,
|
||||
RiDraggable,
|
||||
} from '@remixicon/react'
|
||||
import useConfig from './use-config'
|
||||
import ConditionAdd from './components/condition-add'
|
||||
import ConditionList from './components/condition-list'
|
||||
import type { IfElseNodeType } from './types'
|
||||
import ConditionWrap from './components/condition-wrap'
|
||||
import Button from '@/app/components/base/button'
|
||||
import type { NodePanelProps } from '@/app/components/workflow/types'
|
||||
import Field from '@/app/components/workflow/nodes/_base/components/field'
|
||||
import { useGetAvailableVars } from '@/app/components/workflow/nodes/variable-assigner/hooks'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
const i18nPrefix = 'workflow.nodes.ifElse'
|
||||
|
||||
const Panel: FC<NodePanelProps<IfElseNodeType>> = ({
|
||||
@@ -26,110 +20,48 @@ const Panel: FC<NodePanelProps<IfElseNodeType>> = ({
|
||||
data,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const getAvailableVars = useGetAvailableVars()
|
||||
const {
|
||||
readOnly,
|
||||
inputs,
|
||||
filterVar,
|
||||
filterNumberVar,
|
||||
handleAddCase,
|
||||
handleRemoveCase,
|
||||
handleSortCase,
|
||||
handleAddCondition,
|
||||
handleUpdateCondition,
|
||||
handleRemoveCondition,
|
||||
handleUpdateConditionLogicalOperator,
|
||||
handleToggleConditionLogicalOperator,
|
||||
handleAddSubVariableCondition,
|
||||
handleRemoveSubVariableCondition,
|
||||
handleUpdateSubVariableCondition,
|
||||
handleToggleSubVariableConditionLogicalOperator,
|
||||
nodesOutputVars,
|
||||
availableNodes,
|
||||
varsIsVarFileAttribute,
|
||||
} = useConfig(id, data)
|
||||
const [willDeleteCaseId, setWillDeleteCaseId] = useState('')
|
||||
const cases = inputs.cases || []
|
||||
const casesLength = cases.length
|
||||
|
||||
return (
|
||||
<div className='p-1'>
|
||||
<ReactSortable
|
||||
list={cases.map(caseItem => ({ ...caseItem, id: caseItem.case_id }))}
|
||||
setList={handleSortCase}
|
||||
handle='.handle'
|
||||
ghostClass='bg-components-panel-bg'
|
||||
animation={150}
|
||||
>
|
||||
{
|
||||
cases.map((item, index) => (
|
||||
<div key={item.case_id}>
|
||||
<div
|
||||
className={cn(
|
||||
'group relative py-1 px-3 min-h-[40px] rounded-[10px] bg-components-panel-bg',
|
||||
willDeleteCaseId === item.case_id && 'bg-state-destructive-hover',
|
||||
)}
|
||||
>
|
||||
<RiDraggable className={cn(
|
||||
'hidden handle absolute top-2 left-1 w-3 h-3 text-text-quaternary cursor-pointer',
|
||||
casesLength > 1 && 'group-hover:block',
|
||||
)} />
|
||||
<div className={cn(
|
||||
'absolute left-4 leading-4 text-[13px] font-semibold text-text-secondary',
|
||||
casesLength === 1 ? 'top-2.5' : 'top-1',
|
||||
)}>
|
||||
{
|
||||
index === 0 ? 'IF' : 'ELIF'
|
||||
}
|
||||
{
|
||||
casesLength > 1 && (
|
||||
<div className='text-[10px] text-text-tertiary font-medium'>CASE {index + 1}</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
{
|
||||
!!item.conditions.length && (
|
||||
<div className='mb-2'>
|
||||
<ConditionList
|
||||
disabled={readOnly}
|
||||
caseItem={item}
|
||||
onUpdateCondition={handleUpdateCondition}
|
||||
onRemoveCondition={handleRemoveCondition}
|
||||
onUpdateConditionLogicalOperator={handleUpdateConditionLogicalOperator}
|
||||
nodesOutputVars={nodesOutputVars}
|
||||
availableNodes={availableNodes}
|
||||
numberVariables={getAvailableVars(id, '', filterNumberVar)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<div className={cn(
|
||||
'flex items-center justify-between pl-[60px] pr-[30px]',
|
||||
!item.conditions.length && 'mt-1',
|
||||
)}>
|
||||
<ConditionAdd
|
||||
disabled={readOnly}
|
||||
caseId={item.case_id}
|
||||
variables={getAvailableVars(id, '', filterVar)}
|
||||
onSelectVariable={handleAddCondition}
|
||||
/>
|
||||
{
|
||||
((index === 0 && casesLength > 1) || (index > 0)) && (
|
||||
<Button
|
||||
className='hover:text-components-button-destructive-ghost-text hover:bg-components-button-destructive-ghost-bg-hover'
|
||||
size='small'
|
||||
variant='ghost'
|
||||
disabled={readOnly}
|
||||
onClick={() => handleRemoveCase(item.case_id)}
|
||||
onMouseEnter={() => setWillDeleteCaseId(item.case_id)}
|
||||
onMouseLeave={() => setWillDeleteCaseId('')}
|
||||
>
|
||||
<RiDeleteBinLine className='mr-1 w-3.5 h-3.5' />
|
||||
{t('common.operation.remove')}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div className='my-2 mx-3 h-[1px] bg-divider-subtle'></div>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</ReactSortable>
|
||||
<ConditionWrap
|
||||
nodeId={id}
|
||||
cases={cases}
|
||||
readOnly={readOnly}
|
||||
handleSortCase={handleSortCase}
|
||||
handleRemoveCase={handleRemoveCase}
|
||||
handleAddCondition={handleAddCondition}
|
||||
handleRemoveCondition={handleRemoveCondition}
|
||||
handleUpdateCondition={handleUpdateCondition}
|
||||
handleToggleConditionLogicalOperator={handleToggleConditionLogicalOperator}
|
||||
handleAddSubVariableCondition={handleAddSubVariableCondition}
|
||||
handleRemoveSubVariableCondition={handleRemoveSubVariableCondition}
|
||||
handleUpdateSubVariableCondition={handleUpdateSubVariableCondition}
|
||||
handleToggleSubVariableConditionLogicalOperator={handleToggleSubVariableConditionLogicalOperator}
|
||||
nodesOutputVars={nodesOutputVars}
|
||||
availableNodes={availableNodes}
|
||||
varsIsVarFileAttribute={varsIsVarFileAttribute}
|
||||
filterVar={filterVar}
|
||||
/>
|
||||
<div className='px-4 py-2'>
|
||||
<Button
|
||||
className='w-full'
|
||||
|
@@ -28,15 +28,22 @@ export enum ComparisonOperator {
|
||||
lessThanOrEqual = '≤',
|
||||
isNull = 'is null',
|
||||
isNotNull = 'is not null',
|
||||
in = 'in',
|
||||
notIn = 'not in',
|
||||
allOf = 'all of',
|
||||
exists = 'exists',
|
||||
notExists = 'not exists',
|
||||
}
|
||||
|
||||
export type Condition = {
|
||||
id: string
|
||||
varType: VarType
|
||||
variable_selector: ValueSelector
|
||||
variable_selector?: ValueSelector
|
||||
key?: string // sub variable key
|
||||
comparison_operator?: ComparisonOperator
|
||||
value: string
|
||||
value: string | string[]
|
||||
numberVarType?: NumberVarType
|
||||
sub_variable_condition?: CaseItem
|
||||
}
|
||||
|
||||
export type CaseItem = {
|
||||
@@ -49,9 +56,15 @@ export type IfElseNodeType = CommonNodeType & {
|
||||
logical_operator?: LogicalOperator
|
||||
conditions?: Condition[]
|
||||
cases: CaseItem[]
|
||||
isInIteration: boolean
|
||||
}
|
||||
|
||||
export type HandleAddCondition = (caseId: string, valueSelector: ValueSelector, varItem: Var) => void
|
||||
export type HandleRemoveCondition = (caseId: string, conditionId: string) => void
|
||||
export type HandleUpdateCondition = (caseId: string, conditionId: string, newCondition: Condition) => void
|
||||
export type HandleUpdateConditionLogicalOperator = (caseId: string, value: LogicalOperator) => void
|
||||
export type HandleToggleConditionLogicalOperator = (caseId: string) => void
|
||||
|
||||
export type HandleAddSubVariableCondition = (caseId: string, conditionId: string, key?: string) => void
|
||||
export type handleRemoveSubVariableCondition = (caseId: string, conditionId: string, subConditionId: string) => void
|
||||
export type HandleUpdateSubVariableCondition = (caseId: string, conditionId: string, subConditionId: string, newSubCondition: Condition) => void
|
||||
export type HandleToggleSubVariableConditionLogicalOperator = (caseId: string, conditionId: string) => void
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { useCallback } from 'react'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import produce from 'immer'
|
||||
import { v4 as uuid4 } from 'uuid'
|
||||
import { useUpdateNodeInternals } from 'reactflow'
|
||||
@@ -10,15 +10,19 @@ import { LogicalOperator } from './types'
|
||||
import type {
|
||||
CaseItem,
|
||||
HandleAddCondition,
|
||||
HandleAddSubVariableCondition,
|
||||
HandleRemoveCondition,
|
||||
HandleToggleConditionLogicalOperator,
|
||||
HandleToggleSubVariableConditionLogicalOperator,
|
||||
HandleUpdateCondition,
|
||||
HandleUpdateConditionLogicalOperator,
|
||||
HandleUpdateSubVariableCondition,
|
||||
IfElseNodeType,
|
||||
} from './types'
|
||||
import {
|
||||
branchNameCorrect,
|
||||
getOperators,
|
||||
} from './utils'
|
||||
import useIsVarFileAttribute from './use-is-var-file-attribute'
|
||||
import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud'
|
||||
import {
|
||||
useEdgesInteractions,
|
||||
@@ -32,8 +36,8 @@ const useConfig = (id: string, payload: IfElseNodeType) => {
|
||||
const { handleEdgeDeleteByDeleteBranch } = useEdgesInteractions()
|
||||
const { inputs, setInputs } = useNodeCrud<IfElseNodeType>(id, payload)
|
||||
|
||||
const filterVar = useCallback((varPayload: Var) => {
|
||||
return varPayload.type !== VarType.arrayFile
|
||||
const filterVar = useCallback(() => {
|
||||
return true
|
||||
}, [])
|
||||
|
||||
const {
|
||||
@@ -48,6 +52,23 @@ const useConfig = (id: string, payload: IfElseNodeType) => {
|
||||
return varPayload.type === VarType.number
|
||||
}, [])
|
||||
|
||||
const {
|
||||
getIsVarFileAttribute,
|
||||
} = useIsVarFileAttribute({
|
||||
nodeId: id,
|
||||
isInIteration: payload.isInIteration,
|
||||
})
|
||||
|
||||
const varsIsVarFileAttribute = useMemo(() => {
|
||||
const conditions: Record<string, boolean> = {}
|
||||
inputs.cases?.forEach((c) => {
|
||||
c.conditions.forEach((condition) => {
|
||||
conditions[condition.id] = getIsVarFileAttribute(condition.variable_selector!)
|
||||
})
|
||||
})
|
||||
return conditions
|
||||
}, [inputs.cases, getIsVarFileAttribute])
|
||||
|
||||
const {
|
||||
availableVars: availableNumberVars,
|
||||
availableNodesWithParent: availableNumberNodesWithParent,
|
||||
@@ -121,13 +142,13 @@ const useConfig = (id: string, payload: IfElseNodeType) => {
|
||||
id: uuid4(),
|
||||
varType: varItem.type,
|
||||
variable_selector: valueSelector,
|
||||
comparison_operator: getOperators(varItem.type)[0],
|
||||
comparison_operator: getOperators(varItem.type, getIsVarFileAttribute(valueSelector) ? { key: valueSelector.slice(-1)[0] } : undefined)[0],
|
||||
value: '',
|
||||
})
|
||||
}
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}, [inputs, setInputs])
|
||||
}, [getIsVarFileAttribute, inputs, setInputs])
|
||||
|
||||
const handleRemoveCondition = useCallback<HandleRemoveCondition>((caseId, conditionId) => {
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
@@ -150,11 +171,81 @@ const useConfig = (id: string, payload: IfElseNodeType) => {
|
||||
setInputs(newInputs)
|
||||
}, [inputs, setInputs])
|
||||
|
||||
const handleUpdateConditionLogicalOperator = useCallback<HandleUpdateConditionLogicalOperator>((caseId, value) => {
|
||||
const handleToggleConditionLogicalOperator = useCallback<HandleToggleConditionLogicalOperator>((caseId) => {
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
const targetCase = draft.cases?.find(item => item.case_id === caseId)
|
||||
if (targetCase)
|
||||
targetCase.logical_operator = value
|
||||
targetCase.logical_operator = targetCase.logical_operator === LogicalOperator.and ? LogicalOperator.or : LogicalOperator.and
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}, [inputs, setInputs])
|
||||
|
||||
const handleAddSubVariableCondition = useCallback<HandleAddSubVariableCondition>((caseId: string, conditionId: string, key?: string) => {
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
const condition = draft.cases?.find(item => item.case_id === caseId)?.conditions.find(item => item.id === conditionId)
|
||||
if (!condition)
|
||||
return
|
||||
if (!condition?.sub_variable_condition) {
|
||||
condition.sub_variable_condition = {
|
||||
case_id: uuid4(),
|
||||
logical_operator: LogicalOperator.and,
|
||||
conditions: [],
|
||||
}
|
||||
}
|
||||
const subVarCondition = condition.sub_variable_condition
|
||||
if (subVarCondition) {
|
||||
if (!subVarCondition.conditions)
|
||||
subVarCondition.conditions = []
|
||||
|
||||
subVarCondition.conditions.push({
|
||||
id: uuid4(),
|
||||
key: key || '',
|
||||
varType: VarType.string,
|
||||
comparison_operator: undefined,
|
||||
value: '',
|
||||
})
|
||||
}
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}, [inputs, setInputs])
|
||||
|
||||
const handleRemoveSubVariableCondition = useCallback((caseId: string, conditionId: string, subConditionId: string) => {
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
const condition = draft.cases?.find(item => item.case_id === caseId)?.conditions.find(item => item.id === conditionId)
|
||||
if (!condition)
|
||||
return
|
||||
if (!condition?.sub_variable_condition)
|
||||
return
|
||||
const subVarCondition = condition.sub_variable_condition
|
||||
if (subVarCondition)
|
||||
subVarCondition.conditions = subVarCondition.conditions.filter(item => item.id !== subConditionId)
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}, [inputs, setInputs])
|
||||
|
||||
const handleUpdateSubVariableCondition = useCallback<HandleUpdateSubVariableCondition>((caseId, conditionId, subConditionId, newSubCondition) => {
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
const targetCase = draft.cases?.find(item => item.case_id === caseId)
|
||||
if (targetCase) {
|
||||
const targetCondition = targetCase.conditions.find(item => item.id === conditionId)
|
||||
if (targetCondition && targetCondition.sub_variable_condition) {
|
||||
const targetSubCondition = targetCondition.sub_variable_condition.conditions.find(item => item.id === subConditionId)
|
||||
if (targetSubCondition)
|
||||
Object.assign(targetSubCondition, newSubCondition)
|
||||
}
|
||||
}
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}, [inputs, setInputs])
|
||||
|
||||
const handleToggleSubVariableConditionLogicalOperator = useCallback<HandleToggleSubVariableConditionLogicalOperator>((caseId, conditionId) => {
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
const targetCase = draft.cases?.find(item => item.case_id === caseId)
|
||||
if (targetCase) {
|
||||
const targetCondition = targetCase.conditions.find(item => item.id === conditionId)
|
||||
if (targetCondition && targetCondition.sub_variable_condition)
|
||||
targetCondition.sub_variable_condition.logical_operator = targetCondition.sub_variable_condition.logical_operator === LogicalOperator.and ? LogicalOperator.or : LogicalOperator.and
|
||||
}
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}, [inputs, setInputs])
|
||||
@@ -170,11 +261,16 @@ const useConfig = (id: string, payload: IfElseNodeType) => {
|
||||
handleAddCondition,
|
||||
handleRemoveCondition,
|
||||
handleUpdateCondition,
|
||||
handleUpdateConditionLogicalOperator,
|
||||
handleToggleConditionLogicalOperator,
|
||||
handleAddSubVariableCondition,
|
||||
handleUpdateSubVariableCondition,
|
||||
handleRemoveSubVariableCondition,
|
||||
handleToggleSubVariableConditionLogicalOperator,
|
||||
nodesOutputVars: availableVars,
|
||||
availableNodes: availableNodesWithParent,
|
||||
nodesOutputNumberVars: availableNumberVars,
|
||||
availableNumberNodes: availableNumberNodesWithParent,
|
||||
varsIsVarFileAttribute,
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -0,0 +1,45 @@
|
||||
import { useStoreApi } from 'reactflow'
|
||||
import { useMemo } from 'react'
|
||||
import { useIsChatMode, useWorkflow, useWorkflowVariables } from '../../hooks'
|
||||
import type { ValueSelector } from '../../types'
|
||||
import { VarType } from '../../types'
|
||||
|
||||
type Params = {
|
||||
nodeId: string
|
||||
isInIteration: boolean
|
||||
}
|
||||
const useIsVarFileAttribute = ({
|
||||
nodeId,
|
||||
isInIteration,
|
||||
}: Params) => {
|
||||
const isChatMode = useIsChatMode()
|
||||
const store = useStoreApi()
|
||||
const { getBeforeNodesInSameBranch } = useWorkflow()
|
||||
const {
|
||||
getNodes,
|
||||
} = store.getState()
|
||||
const currentNode = getNodes().find(n => n.id === nodeId)
|
||||
const iterationNode = isInIteration ? getNodes().find(n => n.id === currentNode!.parentId) : null
|
||||
const availableNodes = useMemo(() => {
|
||||
return getBeforeNodesInSameBranch(nodeId)
|
||||
}, [getBeforeNodesInSameBranch, nodeId])
|
||||
const { getCurrentVariableType } = useWorkflowVariables()
|
||||
const getIsVarFileAttribute = (variable: ValueSelector) => {
|
||||
if (variable.length !== 3)
|
||||
return false
|
||||
const parentVariable = variable.slice(0, 2)
|
||||
const varType = getCurrentVariableType({
|
||||
parentNode: iterationNode,
|
||||
valueSelector: parentVariable,
|
||||
availableNodes,
|
||||
isChatMode,
|
||||
isConstant: false,
|
||||
})
|
||||
return varType === VarType.file
|
||||
}
|
||||
return {
|
||||
getIsVarFileAttribute,
|
||||
}
|
||||
}
|
||||
|
||||
export default useIsVarFileAttribute
|
@@ -18,7 +18,72 @@ export const isComparisonOperatorNeedTranslate = (operator?: ComparisonOperator)
|
||||
return !notTranslateKey.includes(operator)
|
||||
}
|
||||
|
||||
export const getOperators = (type?: VarType) => {
|
||||
export const getOperators = (type?: VarType, file?: { key: string }) => {
|
||||
const isFile = !!file
|
||||
if (isFile) {
|
||||
const { key } = file
|
||||
|
||||
switch (key) {
|
||||
case 'name':
|
||||
return [
|
||||
ComparisonOperator.contains,
|
||||
ComparisonOperator.notContains,
|
||||
ComparisonOperator.startWith,
|
||||
ComparisonOperator.endWith,
|
||||
ComparisonOperator.is,
|
||||
ComparisonOperator.isNot,
|
||||
ComparisonOperator.empty,
|
||||
ComparisonOperator.notEmpty,
|
||||
]
|
||||
case 'type':
|
||||
return [
|
||||
ComparisonOperator.in,
|
||||
ComparisonOperator.notIn,
|
||||
]
|
||||
case 'size':
|
||||
return [
|
||||
ComparisonOperator.largerThan,
|
||||
ComparisonOperator.largerThanOrEqual,
|
||||
ComparisonOperator.lessThan,
|
||||
ComparisonOperator.lessThanOrEqual,
|
||||
]
|
||||
case 'extension':
|
||||
return [
|
||||
ComparisonOperator.is,
|
||||
ComparisonOperator.isNot,
|
||||
ComparisonOperator.contains,
|
||||
ComparisonOperator.notContains,
|
||||
]
|
||||
case 'mime_type':
|
||||
return [
|
||||
ComparisonOperator.contains,
|
||||
ComparisonOperator.notContains,
|
||||
ComparisonOperator.startWith,
|
||||
ComparisonOperator.endWith,
|
||||
ComparisonOperator.is,
|
||||
ComparisonOperator.isNot,
|
||||
ComparisonOperator.empty,
|
||||
ComparisonOperator.notEmpty,
|
||||
]
|
||||
case 'transfer_method':
|
||||
return [
|
||||
ComparisonOperator.in,
|
||||
ComparisonOperator.notIn,
|
||||
]
|
||||
case 'url':
|
||||
return [
|
||||
ComparisonOperator.contains,
|
||||
ComparisonOperator.notContains,
|
||||
ComparisonOperator.startWith,
|
||||
ComparisonOperator.endWith,
|
||||
ComparisonOperator.is,
|
||||
ComparisonOperator.isNot,
|
||||
ComparisonOperator.empty,
|
||||
ComparisonOperator.notEmpty,
|
||||
]
|
||||
}
|
||||
return []
|
||||
}
|
||||
switch (type) {
|
||||
case VarType.string:
|
||||
return [
|
||||
@@ -42,6 +107,11 @@ export const getOperators = (type?: VarType) => {
|
||||
ComparisonOperator.empty,
|
||||
ComparisonOperator.notEmpty,
|
||||
]
|
||||
case VarType.file:
|
||||
return [
|
||||
ComparisonOperator.exists,
|
||||
ComparisonOperator.notExists,
|
||||
]
|
||||
case VarType.arrayString:
|
||||
case VarType.arrayNumber:
|
||||
return [
|
||||
@@ -56,6 +126,14 @@ export const getOperators = (type?: VarType) => {
|
||||
ComparisonOperator.empty,
|
||||
ComparisonOperator.notEmpty,
|
||||
]
|
||||
case VarType.arrayFile:
|
||||
return [
|
||||
ComparisonOperator.contains,
|
||||
ComparisonOperator.notContains,
|
||||
ComparisonOperator.allOf,
|
||||
ComparisonOperator.empty,
|
||||
ComparisonOperator.notEmpty,
|
||||
]
|
||||
default:
|
||||
return [
|
||||
ComparisonOperator.is,
|
||||
@@ -70,7 +148,7 @@ export const comparisonOperatorNotRequireValue = (operator?: ComparisonOperator)
|
||||
if (!operator)
|
||||
return false
|
||||
|
||||
return [ComparisonOperator.empty, ComparisonOperator.notEmpty, ComparisonOperator.isNull, ComparisonOperator.isNotNull].includes(operator)
|
||||
return [ComparisonOperator.empty, ComparisonOperator.notEmpty, ComparisonOperator.isNull, ComparisonOperator.isNotNull, ComparisonOperator.exists, ComparisonOperator.notExists].includes(operator)
|
||||
}
|
||||
|
||||
export const branchNameCorrect = (branches: Branch[]) => {
|
||||
|
@@ -24,7 +24,7 @@ const useConfig = (id: string, payload: IterationNodeType) => {
|
||||
const { inputs, setInputs } = useNodeCrud<IterationNodeType>(id, payload)
|
||||
|
||||
const filterInputVar = useCallback((varPayload: Var) => {
|
||||
return [VarType.array, VarType.arrayString, VarType.arrayNumber, VarType.arrayObject].includes(varPayload.type)
|
||||
return [VarType.array, VarType.arrayString, VarType.arrayNumber, VarType.arrayObject, VarType.arrayFile].includes(varPayload.type)
|
||||
}, [])
|
||||
|
||||
const handleInputChange = useCallback((input: ValueSelector | string) => {
|
||||
@@ -50,6 +50,7 @@ const useConfig = (id: string, payload: IterationNodeType) => {
|
||||
[VarType.string]: VarType.arrayString,
|
||||
[VarType.number]: VarType.arrayNumber,
|
||||
[VarType.object]: VarType.arrayObject,
|
||||
[VarType.file]: VarType.arrayFile,
|
||||
} as Record<VarType, VarType>)[outputItemType] || VarType.arrayString
|
||||
})
|
||||
setInputs(newInputs)
|
||||
|
@@ -71,7 +71,7 @@ const DatasetItem: FC<Props> = ({
|
||||
<Folder className='w-4 h-4 text-[#444CE7]' />
|
||||
</div>
|
||||
}
|
||||
<div className='w-0 grow text-[13px] font-normal text-gray-800 truncate'>{payload.name}</div>
|
||||
<div className='w-0 grow text-text-secondary system-sm-medium truncate'>{payload.name}</div>
|
||||
</div>
|
||||
{!readonly && (
|
||||
<div className='hidden group-hover/dataset-item:flex shrink-0 ml-2 items-center space-x-1'>
|
||||
|
@@ -36,11 +36,11 @@ const Node: FC<NodeProps<KnowledgeRetrievalNodeType>> = ({
|
||||
<div className='mb-1 px-3 py-1'>
|
||||
<div className='space-y-0.5'>
|
||||
{selectedDatasets.map(({ id, name }) => (
|
||||
<div key={id} className='flex items-center h-[26px] bg-gray-100 rounded-md px-1 text-xs font-normal text-gray-700'>
|
||||
<div key={id} className='flex items-center h-[26px] bg-workflow-block-parma-bg rounded-md px-1 text-xs font-normal text-gray-700'>
|
||||
<div className='mr-1 shrink-0 p-1 bg-[#F5F8FF] rounded-md border-[0.5px] border-[#E0EAFF]'>
|
||||
<Folder className='w-3 h-3 text-[#444CE7]' />
|
||||
</div>
|
||||
<div className='grow w-0 text-xs font-normal text-gray-700 truncate'>
|
||||
<div className='grow w-0 text-text-secondary system-xs-regular truncate'>
|
||||
{name}
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -0,0 +1,113 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ConditionOperator from '../../if-else/components/condition-list/condition-operator'
|
||||
import { VarType } from '../../../types'
|
||||
import type { Condition } from '../types'
|
||||
import { ComparisonOperator } from '../../if-else/types'
|
||||
import { comparisonOperatorNotRequireValue, getOperators } from '../../if-else/utils'
|
||||
import SubVariablePicker from './sub-variable-picker'
|
||||
import Input from '@/app/components/base/input'
|
||||
import { FILE_TYPE_OPTIONS, TRANSFER_METHOD } from '@/app/components/workflow/nodes/if-else/default'
|
||||
import { SimpleSelect as Select } from '@/app/components/base/select'
|
||||
|
||||
const optionNameI18NPrefix = 'workflow.nodes.ifElse.optionName'
|
||||
type Props = {
|
||||
condition: Condition
|
||||
onChange: (condition: Condition) => void
|
||||
varType: VarType
|
||||
hasSubVariable: boolean
|
||||
readOnly: boolean
|
||||
}
|
||||
|
||||
const FilterCondition: FC<Props> = ({
|
||||
condition = { key: '', comparison_operator: ComparisonOperator.equal, value: '' },
|
||||
varType,
|
||||
onChange,
|
||||
hasSubVariable,
|
||||
readOnly,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const isSelect = [ComparisonOperator.in, ComparisonOperator.notIn, ComparisonOperator.allOf].includes(condition.comparison_operator)
|
||||
const isArrayValue = condition.key === 'transfer_method' || condition.key === 'type'
|
||||
const selectOptions = useMemo(() => {
|
||||
if (isSelect) {
|
||||
if (condition.key === 'type' || condition.comparison_operator === ComparisonOperator.allOf) {
|
||||
return FILE_TYPE_OPTIONS.map(item => ({
|
||||
name: t(`${optionNameI18NPrefix}.${item.i18nKey}`),
|
||||
value: item.value,
|
||||
}))
|
||||
}
|
||||
if (condition.key === 'transfer_method') {
|
||||
return TRANSFER_METHOD.map(item => ({
|
||||
name: t(`${optionNameI18NPrefix}.${item.i18nKey}`),
|
||||
value: item.value,
|
||||
}))
|
||||
}
|
||||
return []
|
||||
}
|
||||
return []
|
||||
}, [condition.comparison_operator, condition.key, isSelect, t])
|
||||
const handleChange = useCallback((key: string) => {
|
||||
return (value: any) => {
|
||||
onChange({
|
||||
...condition,
|
||||
[key]: (isArrayValue && key === 'value') ? [value] : value,
|
||||
})
|
||||
}
|
||||
}, [condition, onChange, isArrayValue])
|
||||
|
||||
const handleSubVariableChange = useCallback((value: string) => {
|
||||
onChange({
|
||||
key: value,
|
||||
comparison_operator: getOperators(varType, { key: value })[0],
|
||||
value: '',
|
||||
})
|
||||
}, [onChange, varType])
|
||||
|
||||
return (
|
||||
<div>
|
||||
{hasSubVariable && (
|
||||
<SubVariablePicker
|
||||
className="mb-2"
|
||||
value={condition.key}
|
||||
onChange={handleSubVariableChange}
|
||||
/>
|
||||
)}
|
||||
<div className='flex space-x-1'>
|
||||
<ConditionOperator
|
||||
className='h-8 bg-components-input-bg-normal'
|
||||
varType={varType}
|
||||
value={condition.comparison_operator}
|
||||
onSelect={handleChange('comparison_operator')}
|
||||
file={hasSubVariable ? { key: condition.key } : undefined}
|
||||
disabled={readOnly}
|
||||
/>
|
||||
{!comparisonOperatorNotRequireValue(condition.comparison_operator) && (
|
||||
<>
|
||||
{isSelect && (
|
||||
<Select
|
||||
items={selectOptions}
|
||||
defaultValue={isArrayValue ? (condition.value as string[])[0] : condition.value as string}
|
||||
onSelect={item => handleChange('value')(item.value)}
|
||||
className='!text-[13px]'
|
||||
wrapperClassName='grow h-8'
|
||||
placeholder='Select value'
|
||||
/>
|
||||
)}
|
||||
{!isSelect && (
|
||||
<Input
|
||||
type={((hasSubVariable && condition.key === 'size') || (!hasSubVariable && varType === VarType.number)) ? 'number' : 'text'}
|
||||
className='grow'
|
||||
value={condition.value}
|
||||
onChange={e => handleChange('value')(e.target.value)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(FilterCondition)
|
@@ -0,0 +1,80 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { Limit } from '../types'
|
||||
import InputNumberWithSlider from '../../_base/components/input-number-with-slider'
|
||||
import cn from '@/utils/classnames'
|
||||
import Field from '@/app/components/workflow/nodes/_base/components/field'
|
||||
import Switch from '@/app/components/base/switch'
|
||||
|
||||
const i18nPrefix = 'workflow.nodes.listFilter'
|
||||
const LIMIT_SIZE_MIN = 1
|
||||
const LIMIT_SIZE_MAX = 20
|
||||
const LIMIT_SIZE_DEFAULT = 10
|
||||
|
||||
type Props = {
|
||||
className?: string
|
||||
readonly: boolean
|
||||
config: Limit
|
||||
onChange: (limit: Limit) => void
|
||||
canSetRoleName?: boolean
|
||||
}
|
||||
|
||||
const LIMIT_DEFAULT: Limit = {
|
||||
enabled: false,
|
||||
size: LIMIT_SIZE_DEFAULT,
|
||||
}
|
||||
|
||||
const LimitConfig: FC<Props> = ({
|
||||
className,
|
||||
readonly,
|
||||
config = LIMIT_DEFAULT,
|
||||
onChange,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const payload = config
|
||||
|
||||
const handleLimitEnabledChange = useCallback((enabled: boolean) => {
|
||||
onChange({
|
||||
...config,
|
||||
enabled,
|
||||
})
|
||||
}, [config, onChange])
|
||||
|
||||
const handleLimitSizeChange = useCallback((size: number | string) => {
|
||||
onChange({
|
||||
...config,
|
||||
size: parseInt(size as string),
|
||||
})
|
||||
}, [onChange, config])
|
||||
|
||||
return (
|
||||
<div className={cn(className)}>
|
||||
<Field
|
||||
title={t(`${i18nPrefix}.limit`)}
|
||||
operations={
|
||||
<Switch
|
||||
defaultValue={payload.enabled}
|
||||
onChange={handleLimitEnabledChange}
|
||||
size='md'
|
||||
disabled={readonly}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{payload?.enabled
|
||||
? (
|
||||
<InputNumberWithSlider
|
||||
value={payload?.size || LIMIT_SIZE_DEFAULT}
|
||||
min={LIMIT_SIZE_MIN}
|
||||
max={LIMIT_SIZE_MAX}
|
||||
onChange={handleLimitSizeChange}
|
||||
readonly={readonly || !payload?.enabled}
|
||||
/>
|
||||
)
|
||||
: null}
|
||||
</Field>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(LimitConfig)
|
@@ -0,0 +1,73 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { SUB_VARIABLES } from '../../if-else/default'
|
||||
import type { Item } from '@/app/components/base/select'
|
||||
import { SimpleSelect as Select } from '@/app/components/base/select'
|
||||
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type Props = {
|
||||
value: string
|
||||
onChange: (value: string) => void
|
||||
className?: string
|
||||
}
|
||||
|
||||
const SubVariablePicker: FC<Props> = ({
|
||||
value,
|
||||
onChange,
|
||||
className,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const subVarOptions = SUB_VARIABLES.map(item => ({
|
||||
value: item,
|
||||
name: item,
|
||||
}))
|
||||
|
||||
const renderOption = ({ item }: { item: Record<string, any> }) => {
|
||||
return (
|
||||
<div className='flex items-center h-6 justify-between'>
|
||||
<div className='flex items-center h-full'>
|
||||
<Variable02 className='mr-[5px] w-3.5 h-3.5 text-text-accent' />
|
||||
<span className='text-text-secondary system-sm-medium'>{item.name}</span>
|
||||
</div>
|
||||
<span className='text-text-tertiary system-xs-regular'>{item.type}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const handleChange = useCallback(({ value }: Item) => {
|
||||
onChange(value as string)
|
||||
}, [onChange])
|
||||
|
||||
return (
|
||||
<div className={cn(className)}>
|
||||
<Select
|
||||
items={subVarOptions}
|
||||
defaultValue={value}
|
||||
onSelect={handleChange}
|
||||
className='!text-[13px]'
|
||||
placeholder={t('workflow.nodes.listFilter.selectVariableKeyPlaceholder')!}
|
||||
optionClassName='pl-1 pr-5 py-0'
|
||||
renderOption={renderOption}
|
||||
renderTrigger={item => (
|
||||
<div className='group/sub-variable-picker flex items-center h-8 pl-1 rounded-lg bg-components-input-bg-normal hover:bg-state-base-hover-alt'>
|
||||
{item
|
||||
? <div className='flex justify-start cursor-pointer'>
|
||||
<div className='inline-flex max-w-full px-1.5 items-center h-6 rounded-md border-[0.5px] border-components-panel-border-subtle bg-components-badge-white-to-dark shadow-xs text-text-accent'>
|
||||
<Variable02 className='shrink-0 w-3.5 h-3.5 text-text-accent' />
|
||||
<div className='ml-0.5 truncate system-xs-medium'>{item?.name}</div>
|
||||
</div>
|
||||
</div>
|
||||
: <div className='pl-1 flex text-components-input-text-placeholder system-sm-regular group-hover/sub-variable-picker:text-text-tertiary'>
|
||||
<Variable02 className='mr-1 shrink-0 w-4 h-4' />
|
||||
<span>{t('common.placeholder.select')}</span>
|
||||
</div>}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(SubVariablePicker)
|
61
web/app/components/workflow/nodes/list-operator/default.ts
Normal file
61
web/app/components/workflow/nodes/list-operator/default.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { BlockEnum, VarType } from '../../types'
|
||||
import type { NodeDefault } from '../../types'
|
||||
import { comparisonOperatorNotRequireValue } from '../if-else/utils'
|
||||
import { type ListFilterNodeType, OrderBy } from './types'
|
||||
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/constants'
|
||||
const i18nPrefix = 'workflow.errorMsg'
|
||||
|
||||
const nodeDefault: NodeDefault<ListFilterNodeType> = {
|
||||
defaultValue: {
|
||||
variable: [],
|
||||
filter_by: {
|
||||
enabled: false,
|
||||
conditions: [],
|
||||
},
|
||||
order_by: {
|
||||
enabled: false,
|
||||
key: '',
|
||||
value: OrderBy.ASC,
|
||||
},
|
||||
limit: {
|
||||
enabled: false,
|
||||
size: 10,
|
||||
},
|
||||
},
|
||||
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: ListFilterNodeType, t: any) {
|
||||
let errorMessages = ''
|
||||
const { variable, var_type, filter_by } = payload
|
||||
|
||||
if (!errorMessages && !variable?.length)
|
||||
errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t('workflow.nodes.listFilter.inputVar') })
|
||||
|
||||
// Check filter condition
|
||||
if (!errorMessages && filter_by?.enabled) {
|
||||
if (var_type === VarType.arrayFile && !filter_by.conditions[0]?.key)
|
||||
errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t('workflow.nodes.listFilter.filterConditionKey') })
|
||||
|
||||
if (!errorMessages && !filter_by.conditions[0]?.comparison_operator)
|
||||
errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t('workflow.nodes.listFilter.filterConditionComparisonOperator') })
|
||||
|
||||
if (!errorMessages && !comparisonOperatorNotRequireValue(filter_by.conditions[0]?.comparison_operator) && !filter_by.conditions[0]?.value)
|
||||
errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t('workflow.nodes.listFilter.filterConditionComparisonValue') })
|
||||
}
|
||||
|
||||
return {
|
||||
isValid: !errorMessages,
|
||||
errorMessage: errorMessages,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export default nodeDefault
|
42
web/app/components/workflow/nodes/list-operator/node.tsx
Normal file
42
web/app/components/workflow/nodes/list-operator/node.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useNodes } from 'reactflow'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import NodeVariableItem from '../variable-assigner/components/node-variable-item'
|
||||
import { type ListFilterNodeType } from './types'
|
||||
import { isConversationVar, isENV, isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils'
|
||||
import { BlockEnum, type Node, type NodeProps } from '@/app/components/workflow/types'
|
||||
|
||||
const i18nPrefix = 'workflow.nodes.listFilter'
|
||||
|
||||
const NodeComponent: FC<NodeProps<ListFilterNodeType>> = ({
|
||||
data,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const nodes: Node[] = useNodes()
|
||||
const { variable } = data
|
||||
|
||||
if (!variable || variable.length === 0)
|
||||
return null
|
||||
|
||||
const isSystem = isSystemVar(variable)
|
||||
const isEnv = isENV(variable)
|
||||
const isChatVar = isConversationVar(variable)
|
||||
const node = isSystem ? nodes.find(node => node.data.type === BlockEnum.Start) : nodes.find(node => node.id === variable[0])
|
||||
const varName = isSystem ? `sys.${variable[variable.length - 1]}` : variable.slice(1).join('.')
|
||||
return (
|
||||
<div className='relative px-3'>
|
||||
<div className='mb-1 system-2xs-medium-uppercase text-text-tertiary'>{t(`${i18nPrefix}.inputVar`)}</div>
|
||||
<NodeVariableItem
|
||||
node={node as Node}
|
||||
isEnv={isEnv}
|
||||
isChatVar={isChatVar}
|
||||
varName={varName}
|
||||
className='bg-workflow-block-parma-bg'
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(NodeComponent)
|
153
web/app/components/workflow/nodes/list-operator/panel.tsx
Normal file
153
web/app/components/workflow/nodes/list-operator/panel.tsx
Normal file
@@ -0,0 +1,153 @@
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import VarReferencePicker from '../_base/components/variable/var-reference-picker'
|
||||
import OutputVars, { VarItem } from '../_base/components/output-vars'
|
||||
import OptionCard from '../_base/components/option-card'
|
||||
import Split from '../_base/components/split'
|
||||
import useConfig from './use-config'
|
||||
import SubVariablePicker from './components/sub-variable-picker'
|
||||
import { type ListFilterNodeType, OrderBy } from './types'
|
||||
import LimitConfig from './components/limit-config'
|
||||
import FilterCondition from './components/filter-condition'
|
||||
import Field from '@/app/components/workflow/nodes/_base/components/field'
|
||||
import { type NodePanelProps } from '@/app/components/workflow/types'
|
||||
import Switch from '@/app/components/base/switch'
|
||||
|
||||
const i18nPrefix = 'workflow.nodes.listFilter'
|
||||
|
||||
const Panel: FC<NodePanelProps<ListFilterNodeType>> = ({
|
||||
id,
|
||||
data,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const {
|
||||
readOnly,
|
||||
inputs,
|
||||
itemVarType,
|
||||
itemVarTypeShowName,
|
||||
hasSubVariable,
|
||||
handleVarChanges,
|
||||
filterVar,
|
||||
handleFilterEnabledChange,
|
||||
handleFilterChange,
|
||||
handleLimitChange,
|
||||
handleOrderByEnabledChange,
|
||||
handleOrderByKeyChange,
|
||||
handleOrderByTypeChange,
|
||||
} = useConfig(id, data)
|
||||
|
||||
return (
|
||||
<div className='mt-2'>
|
||||
<div className='px-4 pb-4 space-y-4'>
|
||||
<Field
|
||||
title={t(`${i18nPrefix}.inputVar`)}
|
||||
>
|
||||
<VarReferencePicker
|
||||
readonly={readOnly}
|
||||
nodeId={id}
|
||||
isShowNodeName
|
||||
value={inputs.variable || []}
|
||||
onChange={handleVarChanges}
|
||||
filterVar={filterVar}
|
||||
typePlaceHolder='Array'
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field
|
||||
title={t(`${i18nPrefix}.filterCondition`)}
|
||||
operations={
|
||||
<Switch
|
||||
defaultValue={inputs.filter_by?.enabled}
|
||||
onChange={handleFilterEnabledChange}
|
||||
size='md'
|
||||
disabled={readOnly}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{inputs.filter_by?.enabled
|
||||
? (
|
||||
<FilterCondition
|
||||
condition={inputs.filter_by.conditions[0]}
|
||||
onChange={handleFilterChange}
|
||||
varType={itemVarType}
|
||||
hasSubVariable={hasSubVariable}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
)
|
||||
: null}
|
||||
</Field>
|
||||
<Split />
|
||||
<Field
|
||||
title={t(`${i18nPrefix}.orderBy`)}
|
||||
operations={
|
||||
<Switch
|
||||
defaultValue={inputs.order_by?.enabled}
|
||||
onChange={handleOrderByEnabledChange}
|
||||
size='md'
|
||||
disabled={readOnly}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{inputs.order_by?.enabled
|
||||
? (
|
||||
<div className='flex items-center justify-between'>
|
||||
{hasSubVariable && (
|
||||
<div className='grow mr-2'>
|
||||
<SubVariablePicker
|
||||
value={inputs.order_by.key as string}
|
||||
onChange={handleOrderByKeyChange}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className={!hasSubVariable ? 'w-full grid grid-cols-2 gap-1' : 'shrink-0 flex space-x-1'}>
|
||||
<OptionCard
|
||||
title={t(`${i18nPrefix}.asc`)}
|
||||
onSelect={handleOrderByTypeChange(OrderBy.ASC)}
|
||||
selected={inputs.order_by.value === OrderBy.ASC}
|
||||
/>
|
||||
<OptionCard
|
||||
title={t(`${i18nPrefix}.desc`)}
|
||||
onSelect={handleOrderByTypeChange(OrderBy.DESC)}
|
||||
selected={inputs.order_by.value === OrderBy.DESC}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
: null}
|
||||
</Field>
|
||||
<Split />
|
||||
<LimitConfig
|
||||
config={inputs.limit}
|
||||
onChange={handleLimitChange}
|
||||
readonly={readOnly}
|
||||
/>
|
||||
</div>
|
||||
<Split />
|
||||
<div className='px-4 pt-4 pb-2'>
|
||||
<OutputVars>
|
||||
<>
|
||||
<VarItem
|
||||
name='result'
|
||||
type={`Array[${itemVarTypeShowName}]`}
|
||||
description={t(`${i18nPrefix}.outputVars.result`)}
|
||||
/>
|
||||
<VarItem
|
||||
name='first_record'
|
||||
type={itemVarTypeShowName}
|
||||
description={t(`${i18nPrefix}.outputVars.first_record`)}
|
||||
/>
|
||||
<VarItem
|
||||
name='last_record'
|
||||
type={itemVarTypeShowName}
|
||||
description={t(`${i18nPrefix}.outputVars.last_record`)}
|
||||
/>
|
||||
</>
|
||||
</OutputVars>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(Panel)
|
34
web/app/components/workflow/nodes/list-operator/types.ts
Normal file
34
web/app/components/workflow/nodes/list-operator/types.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import type { ComparisonOperator } from '../if-else/types'
|
||||
import type { CommonNodeType, ValueSelector, VarType } from '@/app/components/workflow/types'
|
||||
|
||||
export enum OrderBy {
|
||||
ASC = 'asc',
|
||||
DESC = 'desc',
|
||||
}
|
||||
|
||||
export type Limit = {
|
||||
enabled: boolean
|
||||
size?: number
|
||||
}
|
||||
|
||||
export type Condition = {
|
||||
key: string
|
||||
comparison_operator: ComparisonOperator
|
||||
value: string | number | string[]
|
||||
}
|
||||
|
||||
export type ListFilterNodeType = CommonNodeType & {
|
||||
variable: ValueSelector
|
||||
var_type: VarType // Cache for the type of output variable
|
||||
item_var_type: VarType // Cache for the type of output variable
|
||||
filter_by: {
|
||||
enabled: boolean
|
||||
conditions: Condition[]
|
||||
}
|
||||
order_by: {
|
||||
enabled: boolean
|
||||
key: ValueSelector | string
|
||||
value: OrderBy
|
||||
}
|
||||
limit: Limit
|
||||
}
|
168
web/app/components/workflow/nodes/list-operator/use-config.ts
Normal file
168
web/app/components/workflow/nodes/list-operator/use-config.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import produce from 'immer'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
import type { ValueSelector, Var } from '../../types'
|
||||
import { VarType } from '../../types'
|
||||
import { getOperators } from '../if-else/utils'
|
||||
import { OrderBy } from './types'
|
||||
import type { Condition, Limit, ListFilterNodeType } from './types'
|
||||
import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud'
|
||||
import {
|
||||
useIsChatMode,
|
||||
useNodesReadOnly,
|
||||
useWorkflow,
|
||||
useWorkflowVariables,
|
||||
} from '@/app/components/workflow/hooks'
|
||||
|
||||
const useConfig = (id: string, payload: ListFilterNodeType) => {
|
||||
const { nodesReadOnly: readOnly } = useNodesReadOnly()
|
||||
const isChatMode = useIsChatMode()
|
||||
|
||||
const store = useStoreApi()
|
||||
const { getBeforeNodesInSameBranch } = useWorkflow()
|
||||
|
||||
const {
|
||||
getNodes,
|
||||
} = store.getState()
|
||||
const currentNode = getNodes().find(n => n.id === id)
|
||||
const isInIteration = payload.isInIteration
|
||||
const iterationNode = isInIteration ? getNodes().find(n => n.id === currentNode!.parentId) : null
|
||||
const availableNodes = useMemo(() => {
|
||||
return getBeforeNodesInSameBranch(id)
|
||||
}, [getBeforeNodesInSameBranch, id])
|
||||
|
||||
const { inputs, setInputs } = useNodeCrud<ListFilterNodeType>(id, payload)
|
||||
|
||||
const { getCurrentVariableType } = useWorkflowVariables()
|
||||
const getType = useCallback((variable?: ValueSelector) => {
|
||||
const varType = getCurrentVariableType({
|
||||
parentNode: iterationNode,
|
||||
valueSelector: variable || inputs.variable || [],
|
||||
availableNodes,
|
||||
isChatMode,
|
||||
isConstant: false,
|
||||
})
|
||||
let itemVarType = VarType.string
|
||||
switch (varType) {
|
||||
case VarType.arrayNumber:
|
||||
itemVarType = VarType.number
|
||||
break
|
||||
case VarType.arrayString:
|
||||
itemVarType = VarType.string
|
||||
break
|
||||
case VarType.arrayFile:
|
||||
itemVarType = VarType.file
|
||||
break
|
||||
case VarType.arrayObject:
|
||||
itemVarType = VarType.object
|
||||
break
|
||||
default:
|
||||
itemVarType = varType
|
||||
}
|
||||
return { varType, itemVarType }
|
||||
}, [availableNodes, getCurrentVariableType, inputs.variable, isChatMode, iterationNode])
|
||||
|
||||
const { varType, itemVarType } = getType()
|
||||
|
||||
const itemVarTypeShowName = useMemo(() => {
|
||||
if (!inputs.variable)
|
||||
return '?'
|
||||
return [itemVarType.substring(0, 1).toUpperCase(), itemVarType.substring(1)].join('')
|
||||
}, [inputs.variable, itemVarType])
|
||||
|
||||
const hasSubVariable = [VarType.arrayFile].includes(varType)
|
||||
|
||||
const handleVarChanges = useCallback((variable: ValueSelector | string) => {
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
draft.variable = variable as ValueSelector
|
||||
const { varType, itemVarType } = getType(draft.variable)
|
||||
const isFileArray = varType === VarType.arrayFile
|
||||
|
||||
draft.var_type = varType
|
||||
draft.item_var_type = itemVarType
|
||||
draft.filter_by.conditions = [{
|
||||
key: (isFileArray && !draft.filter_by.conditions[0]?.key) ? 'name' : '',
|
||||
comparison_operator: getOperators(itemVarType, isFileArray ? { key: 'name' } : undefined)[0],
|
||||
value: '',
|
||||
}]
|
||||
if (isFileArray && draft.order_by.enabled && !draft.order_by.key)
|
||||
draft.order_by.key = 'name'
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}, [getType, inputs, setInputs])
|
||||
|
||||
const filterVar = useCallback((varPayload: Var) => {
|
||||
// Don't know the item struct of VarType.arrayObject, so not support it
|
||||
return [VarType.arrayNumber, VarType.arrayString, VarType.arrayFile].includes(varPayload.type)
|
||||
}, [])
|
||||
|
||||
const handleFilterEnabledChange = useCallback((enabled: boolean) => {
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
draft.filter_by.enabled = enabled
|
||||
if (enabled && !draft.filter_by.conditions)
|
||||
draft.filter_by.conditions = []
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}, [hasSubVariable, inputs, setInputs])
|
||||
|
||||
const handleFilterChange = useCallback((condition: Condition) => {
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
draft.filter_by.conditions[0] = condition
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}, [inputs, setInputs])
|
||||
|
||||
const handleLimitChange = useCallback((limit: Limit) => {
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
draft.limit = limit
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}, [inputs, setInputs])
|
||||
|
||||
const handleOrderByEnabledChange = useCallback((enabled: boolean) => {
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
draft.order_by.enabled = enabled
|
||||
if (enabled) {
|
||||
draft.order_by.value = OrderBy.ASC
|
||||
if (hasSubVariable && !draft.order_by.key)
|
||||
draft.order_by.key = 'name'
|
||||
}
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}, [hasSubVariable, inputs, setInputs])
|
||||
|
||||
const handleOrderByKeyChange = useCallback((key: string) => {
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
draft.order_by.key = key
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}, [inputs, setInputs])
|
||||
|
||||
const handleOrderByTypeChange = useCallback((type: OrderBy) => {
|
||||
return () => {
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
draft.order_by.value = type
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}
|
||||
}, [inputs, setInputs])
|
||||
|
||||
return {
|
||||
readOnly,
|
||||
inputs,
|
||||
filterVar,
|
||||
varType,
|
||||
itemVarType,
|
||||
itemVarTypeShowName,
|
||||
hasSubVariable,
|
||||
handleVarChanges,
|
||||
handleFilterEnabledChange,
|
||||
handleFilterChange,
|
||||
handleLimitChange,
|
||||
handleOrderByEnabledChange,
|
||||
handleOrderByKeyChange,
|
||||
handleOrderByTypeChange,
|
||||
}
|
||||
}
|
||||
|
||||
export default useConfig
|
@@ -79,6 +79,8 @@ const nodeDefault: NodeDefault<LLMNodeType> = {
|
||||
})
|
||||
}
|
||||
}
|
||||
if (!errorMessages && payload.vision?.enabled && !payload.vision.configs?.variable_selector?.length)
|
||||
errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t(`${i18nPrefix}.fields.visionVariable`) })
|
||||
return {
|
||||
isValid: !errorMessages,
|
||||
errorMessage: errorMessages,
|
||||
|
@@ -3,8 +3,8 @@ import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import MemoryConfig from '../_base/components/memory-config'
|
||||
import VarReferencePicker from '../_base/components/variable/var-reference-picker'
|
||||
import ConfigVision from '../_base/components/config-vision'
|
||||
import useConfig from './use-config'
|
||||
import ResolutionPicker from './components/resolution-picker'
|
||||
import type { LLMNodeType } from './types'
|
||||
import ConfigPrompt from './components/config-prompt'
|
||||
import VarList from '@/app/components/workflow/nodes/_base/components/variable/var-list'
|
||||
@@ -13,14 +13,13 @@ import Field from '@/app/components/workflow/nodes/_base/components/field'
|
||||
import Split from '@/app/components/workflow/nodes/_base/components/split'
|
||||
import ModelParameterModal from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal'
|
||||
import OutputVars, { VarItem } from '@/app/components/workflow/nodes/_base/components/output-vars'
|
||||
import { Resolution } from '@/types/app'
|
||||
import { InputVarType, type NodePanelProps } from '@/app/components/workflow/types'
|
||||
import BeforeRunForm from '@/app/components/workflow/nodes/_base/components/before-run-form'
|
||||
import type { Props as FormProps } from '@/app/components/workflow/nodes/_base/components/before-run-form/form'
|
||||
import ResultPanel from '@/app/components/workflow/run/result-panel'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import Editor from '@/app/components/workflow/nodes/_base/components/prompt/editor'
|
||||
import Switch from '@/app/components/base/switch'
|
||||
|
||||
const i18nPrefix = 'workflow.nodes.llm'
|
||||
|
||||
const Panel: FC<NodePanelProps<LLMNodeType>> = ({
|
||||
@@ -36,7 +35,7 @@ const Panel: FC<NodePanelProps<LLMNodeType>> = ({
|
||||
isChatMode,
|
||||
isCompletionModel,
|
||||
shouldShowContextTip,
|
||||
isShowVisionConfig,
|
||||
isVisionModel,
|
||||
handleModelChanged,
|
||||
hasSetBlockStatus,
|
||||
handleCompletionParamsChange,
|
||||
@@ -102,12 +101,13 @@ const Panel: FC<NodePanelProps<LLMNodeType>> = ({
|
||||
)
|
||||
}
|
||||
|
||||
if (isShowVisionConfig) {
|
||||
if (isVisionModel) {
|
||||
const variableName = data.vision.configs?.variable_selector?.[1] || t(`${i18nPrefix}.files`)!
|
||||
forms.push(
|
||||
{
|
||||
label: t(`${i18nPrefix}.vision`)!,
|
||||
inputs: [{
|
||||
label: t(`${i18nPrefix}.files`)!,
|
||||
label: variableName!,
|
||||
variable: '#files#',
|
||||
type: InputVarType.files,
|
||||
required: false,
|
||||
@@ -256,28 +256,15 @@ const Panel: FC<NodePanelProps<LLMNodeType>> = ({
|
||||
)}
|
||||
|
||||
{/* Vision: GPT4-vision and so on */}
|
||||
{isShowVisionConfig && (
|
||||
<>
|
||||
<Split />
|
||||
<Field
|
||||
title={t(`${i18nPrefix}.vision`)}
|
||||
tooltip={t('appDebug.vision.description')!}
|
||||
operations={
|
||||
<Switch size='md' defaultValue={inputs.vision.enabled} onChange={handleVisionResolutionEnabledChange} />
|
||||
}
|
||||
>
|
||||
{inputs.vision.enabled
|
||||
? (
|
||||
<ResolutionPicker
|
||||
value={inputs.vision.configs?.detail || Resolution.high}
|
||||
onChange={handleVisionResolutionChange}
|
||||
/>
|
||||
)
|
||||
: null}
|
||||
|
||||
</Field>
|
||||
</>
|
||||
)}
|
||||
<ConfigVision
|
||||
nodeId={id}
|
||||
readOnly={readOnly}
|
||||
isVisionModel={isVisionModel}
|
||||
enabled={inputs.vision?.enabled}
|
||||
onEnabledChange={handleVisionResolutionEnabledChange}
|
||||
config={inputs.vision?.configs}
|
||||
onConfigChange={handleVisionResolutionChange}
|
||||
/>
|
||||
</div>
|
||||
<Split />
|
||||
<div className='px-4 pt-4 pb-2'>
|
||||
|
@@ -1,5 +1,4 @@
|
||||
import type { Resolution } from '@/types/app'
|
||||
import type { CommonNodeType, Memory, ModelConfig, PromptItem, ValueSelector, Variable } from '@/app/components/workflow/types'
|
||||
import type { CommonNodeType, Memory, ModelConfig, PromptItem, ValueSelector, Variable, VisionSetting } from '@/app/components/workflow/types'
|
||||
|
||||
export type LLMNodeType = CommonNodeType & {
|
||||
model: ModelConfig
|
||||
@@ -14,8 +13,6 @@ export type LLMNodeType = CommonNodeType & {
|
||||
}
|
||||
vision: {
|
||||
enabled: boolean
|
||||
configs?: {
|
||||
detail: Resolution
|
||||
}
|
||||
configs?: VisionSetting
|
||||
}
|
||||
}
|
||||
|
@@ -8,11 +8,10 @@ import {
|
||||
useNodesReadOnly,
|
||||
} from '../../hooks'
|
||||
import useAvailableVarList from '../_base/hooks/use-available-var-list'
|
||||
import useConfigVision from '../../hooks/use-config-vision'
|
||||
import type { LLMNodeType } from './types'
|
||||
import { Resolution } from '@/types/app'
|
||||
import { useModelListAndDefaultModelAndCurrentProviderAndModel, useTextGenerationCurrentProviderAndModelAndModelList } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
import { useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
import {
|
||||
ModelFeatureEnum,
|
||||
ModelTypeEnum,
|
||||
} from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud'
|
||||
@@ -100,7 +99,7 @@ const useConfig = (id: string, payload: LLMNodeType) => {
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [defaultConfig, isChatModel])
|
||||
|
||||
const [modelChanged, setModelChanged] = useState(false)
|
||||
@@ -109,6 +108,21 @@ const useConfig = (id: string, payload: LLMNodeType) => {
|
||||
currentModel,
|
||||
} = useModelListAndDefaultModelAndCurrentProviderAndModel(ModelTypeEnum.textGeneration)
|
||||
|
||||
const {
|
||||
isVisionModel,
|
||||
handleVisionResolutionEnabledChange,
|
||||
handleVisionResolutionChange,
|
||||
handleModelChanged: handleVisionConfigAfterModelChanged,
|
||||
} = useConfigVision(model, {
|
||||
payload: inputs.vision,
|
||||
onChange: (newPayload) => {
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
draft.vision = newPayload
|
||||
})
|
||||
setInputs(newInputs)
|
||||
},
|
||||
})
|
||||
|
||||
const handleModelChanged = useCallback((model: { provider: string; modelId: string; mode?: string }) => {
|
||||
const newInputs = produce(inputRef.current, (draft) => {
|
||||
draft.model.provider = model.provider
|
||||
@@ -139,44 +153,14 @@ const useConfig = (id: string, payload: LLMNodeType) => {
|
||||
setInputs(newInputs)
|
||||
}, [inputs, setInputs])
|
||||
|
||||
const {
|
||||
currentModel: currModel,
|
||||
} = useTextGenerationCurrentProviderAndModelAndModelList(
|
||||
{
|
||||
provider: model.provider,
|
||||
model: model.name,
|
||||
},
|
||||
)
|
||||
const isShowVisionConfig = !!currModel?.features?.includes(ModelFeatureEnum.vision)
|
||||
// change to vision model to set vision enabled, else disabled
|
||||
useEffect(() => {
|
||||
if (!modelChanged)
|
||||
return
|
||||
setModelChanged(false)
|
||||
if (!isShowVisionConfig) {
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
draft.vision = {
|
||||
enabled: false,
|
||||
}
|
||||
})
|
||||
setInputs(newInputs)
|
||||
return
|
||||
}
|
||||
if (!inputs.vision?.enabled) {
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
if (!draft.vision?.enabled) {
|
||||
draft.vision = {
|
||||
enabled: true,
|
||||
configs: {
|
||||
detail: Resolution.high,
|
||||
},
|
||||
}
|
||||
}
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isShowVisionConfig, modelChanged])
|
||||
handleVisionConfigAfterModelChanged()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isVisionModel, modelChanged])
|
||||
|
||||
// variables
|
||||
const isShowVars = (() => {
|
||||
@@ -293,46 +277,12 @@ const useConfig = (id: string, payload: LLMNodeType) => {
|
||||
setInputs(newInputs)
|
||||
}, [inputs, setInputs])
|
||||
|
||||
const handleVisionResolutionEnabledChange = useCallback((enabled: boolean) => {
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
if (!draft.vision) {
|
||||
draft.vision = {
|
||||
enabled,
|
||||
configs: {
|
||||
detail: Resolution.high,
|
||||
},
|
||||
}
|
||||
}
|
||||
else {
|
||||
draft.vision.enabled = enabled
|
||||
if (!draft.vision.configs) {
|
||||
draft.vision.configs = {
|
||||
detail: Resolution.high,
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}, [inputs, setInputs])
|
||||
|
||||
const handleVisionResolutionChange = useCallback((newResolution: Resolution) => {
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
if (!draft.vision.configs) {
|
||||
draft.vision.configs = {
|
||||
detail: Resolution.high,
|
||||
}
|
||||
}
|
||||
draft.vision.configs.detail = newResolution
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}, [inputs, setInputs])
|
||||
|
||||
const filterInputVar = useCallback((varPayload: Var) => {
|
||||
return [VarType.number, VarType.string, VarType.secret].includes(varPayload.type)
|
||||
return [VarType.number, VarType.string, VarType.secret, VarType.arrayString, VarType.arrayNumber].includes(varPayload.type)
|
||||
}, [])
|
||||
|
||||
const filterVar = useCallback((varPayload: Var) => {
|
||||
return [VarType.arrayObject, VarType.array, VarType.number, VarType.string, VarType.secret].includes(varPayload.type)
|
||||
const filterMemoryPromptVar = useCallback((varPayload: Var) => {
|
||||
return [VarType.arrayObject, VarType.array, VarType.number, VarType.string, VarType.secret, VarType.arrayString, VarType.arrayNumber].includes(varPayload.type)
|
||||
}, [])
|
||||
|
||||
const {
|
||||
@@ -340,7 +290,7 @@ const useConfig = (id: string, payload: LLMNodeType) => {
|
||||
availableNodesWithParent,
|
||||
} = useAvailableVarList(id, {
|
||||
onlyLeafNodeVar: false,
|
||||
filterVar,
|
||||
filterVar: filterMemoryPromptVar,
|
||||
})
|
||||
|
||||
// single run
|
||||
@@ -425,7 +375,7 @@ const useConfig = (id: string, payload: LLMNodeType) => {
|
||||
isCompletionModel,
|
||||
hasSetBlockStatus,
|
||||
shouldShowContextTip,
|
||||
isShowVisionConfig,
|
||||
isVisionModel,
|
||||
handleModelChanged,
|
||||
handleCompletionParamsChange,
|
||||
isShowVars,
|
||||
@@ -435,7 +385,7 @@ const useConfig = (id: string, payload: LLMNodeType) => {
|
||||
handleAddEmptyVariable,
|
||||
handleContextVarChange,
|
||||
filterInputVar,
|
||||
filterVar,
|
||||
filterVar: filterMemoryPromptVar,
|
||||
availableVars,
|
||||
availableNodesWithParent,
|
||||
handlePromptChange,
|
||||
|
@@ -5,11 +5,12 @@ import { useBoolean } from 'ahooks'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { Param } from '../../types'
|
||||
import { ParamType } from '../../types'
|
||||
import cn from '@/utils/classnames'
|
||||
import AddButton from '@/app/components/base/button/add-button'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Field from '@/app/components/app/configuration/config-var/config-modal/field'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Textarea from '@/app/components/base/textarea'
|
||||
import Select from '@/app/components/base/select'
|
||||
import Switch from '@/app/components/base/switch'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
@@ -19,7 +20,6 @@ import { checkKeys } from '@/utils/var'
|
||||
|
||||
const i18nPrefix = 'workflow.nodes.parameterExtractor'
|
||||
const errorI18nPrefix = 'workflow.errorMsg'
|
||||
const inputClassName = 'w-full px-3 text-sm leading-9 text-gray-900 border-0 rounded-lg grow h-9 bg-gray-100 focus:outline-none focus:ring-1 focus:ring-inset focus:ring-gray-200'
|
||||
|
||||
const DEFAULT_PARAM: Param = {
|
||||
name: '',
|
||||
@@ -136,9 +136,7 @@ const AddExtractParameter: FC<Props> = ({
|
||||
<div>
|
||||
<div className='space-y-2'>
|
||||
<Field title={t(`${i18nPrefix}.addExtractParameterContent.name`)}>
|
||||
<input
|
||||
type='text'
|
||||
className={inputClassName}
|
||||
<Input
|
||||
value={param.name}
|
||||
onChange={e => handleParamChange('name')(e.target.value)}
|
||||
placeholder={t(`${i18nPrefix}.addExtractParameterContent.namePlaceholder`)!}
|
||||
@@ -165,8 +163,7 @@ const AddExtractParameter: FC<Props> = ({
|
||||
</Field>
|
||||
)}
|
||||
<Field title={t(`${i18nPrefix}.addExtractParameterContent.description`)}>
|
||||
<textarea
|
||||
className={cn(inputClassName, '!h-[80px]')}
|
||||
<Textarea
|
||||
value={param.description}
|
||||
onChange={e => handleParamChange('description')(e.target.value)}
|
||||
placeholder={t(`${i18nPrefix}.addExtractParameterContent.descriptionPlaceholder`)!}
|
||||
|
@@ -4,31 +4,10 @@ import React, { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ReasoningModeType } from '../types'
|
||||
import Field from '../../_base/components/field'
|
||||
import cn from '@/utils/classnames'
|
||||
import OptionCard from '../../_base/components/option-card'
|
||||
|
||||
const i18nPrefix = 'workflow.nodes.parameterExtractor'
|
||||
|
||||
type ItemProps = {
|
||||
isChosen: boolean
|
||||
text: string
|
||||
onClick: () => void
|
||||
}
|
||||
|
||||
const Item: FC<ItemProps> = ({
|
||||
isChosen,
|
||||
text,
|
||||
onClick,
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(isChosen ? 'border-[1.5px] border-primary-400 bg-white' : 'border border-gray-100 bg-gray-25', 'grow w-0 shrink-0 flex items-center h-8 justify-center rounded-lg cursor-pointer text-[13px] font-normal text-gray-900')}
|
||||
onClick={() => !isChosen ? onClick() : () => { }}
|
||||
>
|
||||
{text}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
type Props = {
|
||||
type: ReasoningModeType
|
||||
onChange: (type: ReasoningModeType) => void
|
||||
@@ -51,16 +30,16 @@ const ReasoningModePicker: FC<Props> = ({
|
||||
title={t(`${i18nPrefix}.reasoningMode`)}
|
||||
tooltip={t(`${i18nPrefix}.reasoningModeTip`)!}
|
||||
>
|
||||
<div className='flex space-x-1'>
|
||||
<Item
|
||||
isChosen={type === ReasoningModeType.functionCall}
|
||||
text='Function/Tool Calling'
|
||||
onClick={handleChange(ReasoningModeType.functionCall)}
|
||||
<div className='grid grid-cols-2 gap-x-1'>
|
||||
<OptionCard
|
||||
title='Function/Tool Calling'
|
||||
onSelect={handleChange(ReasoningModeType.functionCall)}
|
||||
selected={type === ReasoningModeType.functionCall}
|
||||
/>
|
||||
<Item
|
||||
isChosen={type === ReasoningModeType.prompt}
|
||||
text='Prompt'
|
||||
onClick={handleChange(ReasoningModeType.prompt)}
|
||||
<OptionCard
|
||||
title='Prompt'
|
||||
selected={type === ReasoningModeType.prompt}
|
||||
onSelect={handleChange(ReasoningModeType.prompt)}
|
||||
/>
|
||||
</div>
|
||||
</Field>
|
||||
|
@@ -16,6 +16,9 @@ const nodeDefault: NodeDefault<ParameterExtractorNodeType> = {
|
||||
},
|
||||
},
|
||||
reasoning_mode: ReasoningModeType.prompt,
|
||||
vision: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
getAvailablePrevNodes(isChatMode: boolean) {
|
||||
const nodes = isChatMode
|
||||
@@ -54,6 +57,8 @@ const nodeDefault: NodeDefault<ParameterExtractorNodeType> = {
|
||||
errorMessages = t(`${i18nPrefix}.errorMsg.fieldRequired`, { field: t(`${i18nPrefix}.nodes.parameterExtractor.addExtractParameterContent.descriptionPlaceholder`) })
|
||||
})
|
||||
}
|
||||
if (!errorMessages && payload.vision?.enabled && !payload.vision.configs?.variable_selector?.length)
|
||||
errorMessages = t(`${i18nPrefix}.errorMsg.fieldRequired`, { field: t(`${i18nPrefix}.errorMsg.fields.visionVariable`) })
|
||||
return {
|
||||
isValid: !errorMessages,
|
||||
errorMessage: errorMessages,
|
||||
|
@@ -5,6 +5,7 @@ import MemoryConfig from '../_base/components/memory-config'
|
||||
import VarReferencePicker from '../_base/components/variable/var-reference-picker'
|
||||
import Editor from '../_base/components/prompt/editor'
|
||||
import ResultPanel from '../../run/result-panel'
|
||||
import ConfigVision from '../_base/components/config-vision'
|
||||
import useConfig from './use-config'
|
||||
import type { ParameterExtractorNodeType } from './types'
|
||||
import ExtractParameter from './components/extract-parameter/list'
|
||||
@@ -51,6 +52,9 @@ const Panel: FC<NodePanelProps<ParameterExtractorNodeType>> = ({
|
||||
availableNodesWithParent,
|
||||
inputVarValues,
|
||||
varInputs,
|
||||
isVisionModel,
|
||||
handleVisionResolutionChange,
|
||||
handleVisionResolutionEnabledChange,
|
||||
isShowSingleRun,
|
||||
hideSingleRun,
|
||||
runningStatus,
|
||||
@@ -65,20 +69,6 @@ const Panel: FC<NodePanelProps<ParameterExtractorNodeType>> = ({
|
||||
return (
|
||||
<div className='mt-2'>
|
||||
<div className='px-4 pb-4 space-y-4'>
|
||||
<Field
|
||||
title={t(`${i18nPrefix}.inputVar`)}
|
||||
>
|
||||
<>
|
||||
<VarReferencePicker
|
||||
readonly={readOnly}
|
||||
nodeId={id}
|
||||
isShowNodeName
|
||||
value={inputs.query || []}
|
||||
onChange={handleInputVarChange}
|
||||
filterVar={filterVar}
|
||||
/>
|
||||
</>
|
||||
</Field>
|
||||
<Field
|
||||
title={t(`${i18nCommonPrefix}.model`)}
|
||||
>
|
||||
@@ -97,6 +87,30 @@ const Panel: FC<NodePanelProps<ParameterExtractorNodeType>> = ({
|
||||
readonly={readOnly}
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
title={t(`${i18nPrefix}.inputVar`)}
|
||||
>
|
||||
<>
|
||||
<VarReferencePicker
|
||||
readonly={readOnly}
|
||||
nodeId={id}
|
||||
isShowNodeName
|
||||
value={inputs.query || []}
|
||||
onChange={handleInputVarChange}
|
||||
filterVar={filterVar}
|
||||
/>
|
||||
</>
|
||||
</Field>
|
||||
<Split />
|
||||
<ConfigVision
|
||||
nodeId={id}
|
||||
readOnly={readOnly}
|
||||
isVisionModel={isVisionModel}
|
||||
enabled={inputs.vision?.enabled}
|
||||
onEnabledChange={handleVisionResolutionEnabledChange}
|
||||
config={inputs.vision?.configs}
|
||||
onConfigChange={handleVisionResolutionChange}
|
||||
/>
|
||||
<Field
|
||||
title={t(`${i18nPrefix}.extractParameters`)}
|
||||
operations={
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import type { CommonNodeType, Memory, ModelConfig, ValueSelector } from '@/app/components/workflow/types'
|
||||
import type { CommonNodeType, Memory, ModelConfig, ValueSelector, VisionSetting } from '@/app/components/workflow/types'
|
||||
|
||||
export enum ParamType {
|
||||
string = 'string',
|
||||
@@ -30,4 +30,8 @@ export type ParameterExtractorNodeType = CommonNodeType & {
|
||||
parameters: Param[]
|
||||
instruction: string
|
||||
memory?: Memory
|
||||
vision: {
|
||||
enabled: boolean
|
||||
configs?: VisionSetting
|
||||
}
|
||||
}
|
||||
|
@@ -9,6 +9,7 @@ import {
|
||||
useWorkflow,
|
||||
} from '../../hooks'
|
||||
import useOneStepRun from '../_base/hooks/use-one-step-run'
|
||||
import useConfigVision from '../../hooks/use-config-vision'
|
||||
import type { Param, ParameterExtractorNodeType, ReasoningModeType } from './types'
|
||||
import { useModelListAndDefaultModelAndCurrentProviderAndModel, useTextGenerationCurrentProviderAndModelAndModelList } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
import {
|
||||
@@ -84,9 +85,23 @@ const useConfig = (id: string, payload: ParameterExtractorNodeType) => {
|
||||
}
|
||||
const modelMode = inputs.model?.mode
|
||||
const isChatModel = modelMode === 'chat'
|
||||
|
||||
const isCompletionModel = !isChatModel
|
||||
|
||||
const {
|
||||
isVisionModel,
|
||||
handleVisionResolutionEnabledChange,
|
||||
handleVisionResolutionChange,
|
||||
handleModelChanged: handleVisionConfigAfterModelChanged,
|
||||
} = useConfigVision(model, {
|
||||
payload: inputs.vision,
|
||||
onChange: (newPayload) => {
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
draft.vision = newPayload
|
||||
})
|
||||
setInputs(newInputs)
|
||||
},
|
||||
})
|
||||
|
||||
const appendDefaultPromptConfig = useCallback((draft: ParameterExtractorNodeType, defaultConfig: any, _passInIsChatMode?: boolean) => {
|
||||
const promptTemplates = defaultConfig.prompt_templates
|
||||
if (!isChatModel) {
|
||||
@@ -97,7 +112,7 @@ const useConfig = (id: string, payload: ParameterExtractorNodeType) => {
|
||||
}
|
||||
}, [isChatModel])
|
||||
|
||||
// const [modelChanged, setModelChanged] = useState(false)
|
||||
const [modelChanged, setModelChanged] = useState(false)
|
||||
const {
|
||||
currentProvider,
|
||||
currentModel,
|
||||
@@ -113,7 +128,7 @@ const useConfig = (id: string, payload: ParameterExtractorNodeType) => {
|
||||
appendDefaultPromptConfig(draft, defaultConfig, model.mode === 'chat')
|
||||
})
|
||||
setInputs(newInputs)
|
||||
// setModelChanged(true)
|
||||
setModelChanged(true)
|
||||
}, [setInputs, defaultConfig, appendDefaultPromptConfig])
|
||||
|
||||
useEffect(() => {
|
||||
@@ -126,6 +141,15 @@ const useConfig = (id: string, payload: ParameterExtractorNodeType) => {
|
||||
}
|
||||
}, [model?.provider, currentProvider, currentModel, handleModelChanged])
|
||||
|
||||
// change to vision model to set vision enabled, else disabled
|
||||
useEffect(() => {
|
||||
if (!modelChanged)
|
||||
return
|
||||
setModelChanged(false)
|
||||
handleVisionConfigAfterModelChanged()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isVisionModel, modelChanged])
|
||||
|
||||
const {
|
||||
currentModel: currModel,
|
||||
} = useTextGenerationCurrentProviderAndModelAndModelList(
|
||||
@@ -245,6 +269,9 @@ const useConfig = (id: string, payload: ParameterExtractorNodeType) => {
|
||||
handleMemoryChange,
|
||||
varInputs,
|
||||
inputVarValues,
|
||||
isVisionModel,
|
||||
handleVisionResolutionEnabledChange,
|
||||
handleVisionResolutionChange,
|
||||
isShowSingleRun,
|
||||
hideSingleRun,
|
||||
runningStatus,
|
||||
|
@@ -26,6 +26,9 @@ const nodeDefault: NodeDefault<QuestionClassifierNodeType> = {
|
||||
name: '',
|
||||
},
|
||||
],
|
||||
vision: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
getAvailablePrevNodes(isChatMode: boolean) {
|
||||
const nodes = isChatMode
|
||||
@@ -50,6 +53,9 @@ const nodeDefault: NodeDefault<QuestionClassifierNodeType> = {
|
||||
|
||||
if (!errorMessages && (payload.classes.some(item => !item.name)))
|
||||
errorMessages = t(`${i18nPrefix}.errorMsg.fieldRequired`, { field: t(`${i18nPrefix}.nodes.questionClassifiers.topicName`) })
|
||||
|
||||
if (!errorMessages && payload.vision?.enabled && !payload.vision.configs?.variable_selector?.length)
|
||||
errorMessages = t(`${i18nPrefix}.errorMsg.fieldRequired`, { field: t(`${i18nPrefix}.errorMsg.fields.visionVariable`) })
|
||||
return {
|
||||
isValid: !errorMessages,
|
||||
errorMessage: errorMessages,
|
||||
|
@@ -2,6 +2,7 @@ import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import VarReferencePicker from '../_base/components/variable/var-reference-picker'
|
||||
import ConfigVision from '../_base/components/config-vision'
|
||||
import useConfig from './use-config'
|
||||
import ClassList from './components/class-list'
|
||||
import AdvancedSetting from './components/advanced-setting'
|
||||
@@ -39,6 +40,9 @@ const Panel: FC<NodePanelProps<QuestionClassifierNodeType>> = ({
|
||||
varInputs,
|
||||
setInputVarValues,
|
||||
handleMemoryChange,
|
||||
isVisionModel,
|
||||
handleVisionResolutionChange,
|
||||
handleVisionResolutionEnabledChange,
|
||||
isShowSingleRun,
|
||||
hideSingleRun,
|
||||
runningStatus,
|
||||
@@ -53,18 +57,6 @@ const Panel: FC<NodePanelProps<QuestionClassifierNodeType>> = ({
|
||||
return (
|
||||
<div className='mt-2'>
|
||||
<div className='px-4 pb-4 space-y-4'>
|
||||
<Field
|
||||
title={t(`${i18nPrefix}.inputVars`)}
|
||||
>
|
||||
<VarReferencePicker
|
||||
readonly={readOnly}
|
||||
isShowNodeName
|
||||
nodeId={id}
|
||||
value={inputs.query_variable_selector}
|
||||
onChange={handleQueryVarChange}
|
||||
filterVar={filterVar}
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
title={t(`${i18nPrefix}.model`)}
|
||||
>
|
||||
@@ -83,6 +75,28 @@ const Panel: FC<NodePanelProps<QuestionClassifierNodeType>> = ({
|
||||
readonly={readOnly}
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
title={t(`${i18nPrefix}.inputVars`)}
|
||||
>
|
||||
<VarReferencePicker
|
||||
readonly={readOnly}
|
||||
isShowNodeName
|
||||
nodeId={id}
|
||||
value={inputs.query_variable_selector}
|
||||
onChange={handleQueryVarChange}
|
||||
filterVar={filterVar}
|
||||
/>
|
||||
</Field>
|
||||
<Split />
|
||||
<ConfigVision
|
||||
nodeId={id}
|
||||
readOnly={readOnly}
|
||||
isVisionModel={isVisionModel}
|
||||
enabled={inputs.vision?.enabled}
|
||||
onEnabledChange={handleVisionResolutionEnabledChange}
|
||||
config={inputs.vision?.configs}
|
||||
onConfigChange={handleVisionResolutionChange}
|
||||
/>
|
||||
<Field
|
||||
title={t(`${i18nPrefix}.class`)}
|
||||
>
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import type { CommonNodeType, Memory, ModelConfig, ValueSelector } from '@/app/components/workflow/types'
|
||||
import type { CommonNodeType, Memory, ModelConfig, ValueSelector, VisionSetting } from '@/app/components/workflow/types'
|
||||
|
||||
export type Topic = {
|
||||
id: string
|
||||
@@ -11,4 +11,8 @@ export type QuestionClassifierNodeType = CommonNodeType & {
|
||||
classes: Topic[]
|
||||
instruction: string
|
||||
memory?: Memory
|
||||
vision: {
|
||||
enabled: boolean
|
||||
configs?: VisionSetting
|
||||
}
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useRef } from 'react'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import produce from 'immer'
|
||||
import { BlockEnum, VarType } from '../../types'
|
||||
import type { Memory, ValueSelector, Var } from '../../types'
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from '../../hooks'
|
||||
import { useStore } from '../../store'
|
||||
import useAvailableVarList from '../_base/hooks/use-available-var-list'
|
||||
import useConfigVision from '../../hooks/use-config-vision'
|
||||
import type { QuestionClassifierNodeType } from './types'
|
||||
import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud'
|
||||
import useOneStepRun from '@/app/components/workflow/nodes/_base/hooks/use-one-step-run'
|
||||
@@ -28,7 +29,7 @@ const useConfig = (id: string, payload: QuestionClassifierNodeType) => {
|
||||
inputRef.current = inputs
|
||||
}, [inputs])
|
||||
|
||||
// model
|
||||
const [modelChanged, setModelChanged] = useState(false)
|
||||
const {
|
||||
currentProvider,
|
||||
currentModel,
|
||||
@@ -38,6 +39,21 @@ const useConfig = (id: string, payload: QuestionClassifierNodeType) => {
|
||||
const modelMode = inputs.model?.mode
|
||||
const isChatModel = modelMode === 'chat'
|
||||
|
||||
const {
|
||||
isVisionModel,
|
||||
handleVisionResolutionEnabledChange,
|
||||
handleVisionResolutionChange,
|
||||
handleModelChanged: handleVisionConfigAfterModelChanged,
|
||||
} = useConfigVision(model, {
|
||||
payload: inputs.vision,
|
||||
onChange: (newPayload) => {
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
draft.vision = newPayload
|
||||
})
|
||||
setInputs(newInputs)
|
||||
},
|
||||
})
|
||||
|
||||
const handleModelChanged = useCallback((model: { provider: string; modelId: string; mode?: string }) => {
|
||||
const newInputs = produce(inputRef.current, (draft) => {
|
||||
draft.model.provider = model.provider
|
||||
@@ -45,6 +61,7 @@ const useConfig = (id: string, payload: QuestionClassifierNodeType) => {
|
||||
draft.model.mode = model.mode!
|
||||
})
|
||||
setInputs(newInputs)
|
||||
setModelChanged(true)
|
||||
}, [setInputs])
|
||||
|
||||
useEffect(() => {
|
||||
@@ -64,6 +81,15 @@ const useConfig = (id: string, payload: QuestionClassifierNodeType) => {
|
||||
setInputs(newInputs)
|
||||
}, [inputs, setInputs])
|
||||
|
||||
// change to vision model to set vision enabled, else disabled
|
||||
useEffect(() => {
|
||||
if (!modelChanged)
|
||||
return
|
||||
setModelChanged(false)
|
||||
handleVisionConfigAfterModelChanged()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isVisionModel, modelChanged])
|
||||
|
||||
const handleQueryVarChange = useCallback((newVar: ValueSelector | string) => {
|
||||
const newInputs = produce(inputs, (draft) => {
|
||||
draft.query_variable_selector = newVar as ValueSelector
|
||||
@@ -191,6 +217,9 @@ const useConfig = (id: string, payload: QuestionClassifierNodeType) => {
|
||||
inputVarValues,
|
||||
setInputVarValues,
|
||||
handleMemoryChange,
|
||||
isVisionModel,
|
||||
handleVisionResolutionEnabledChange,
|
||||
handleVisionResolutionChange,
|
||||
isShowSingleRun,
|
||||
hideSingleRun,
|
||||
runningStatus,
|
||||
|
@@ -10,6 +10,7 @@ import InputVarTypeIcon from '../../_base/components/input-var-type-icon'
|
||||
import type { InputVar, MoreInfo } from '@/app/components/workflow/types'
|
||||
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
|
||||
import { Edit03 } from '@/app/components/base/icons/src/vender/solid/general'
|
||||
import Badge from '@/app/components/base/badge'
|
||||
import ConfigVarModal from '@/app/components/app/configuration/config-var/config-modal'
|
||||
|
||||
type Props = {
|
||||
@@ -19,6 +20,7 @@ type Props = {
|
||||
onRemove?: () => void
|
||||
rightContent?: JSX.Element
|
||||
varKeys?: string[]
|
||||
showLegacyBadge?: boolean
|
||||
}
|
||||
|
||||
const VarItem: FC<Props> = ({
|
||||
@@ -28,6 +30,7 @@ const VarItem: FC<Props> = ({
|
||||
onRemove = () => { },
|
||||
rightContent,
|
||||
varKeys = [],
|
||||
showLegacyBadge = false,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
@@ -50,6 +53,12 @@ const VarItem: FC<Props> = ({
|
||||
{payload.label && (<><div className='shrink-0 text-xs font-medium text-gray-400'>·</div>
|
||||
<div title={payload.label as string} className='max-w-[130px] truncate text-[13px] font-medium text-gray-500'>{payload.label as string}</div>
|
||||
</>)}
|
||||
{showLegacyBadge && (
|
||||
<Badge
|
||||
text='LEGACY'
|
||||
className='shrink-0 border-text-accent-secondary text-text-accent-secondary'
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className='shrink-0 ml-2 flex items-center'>
|
||||
{rightContent || (<>
|
||||
@@ -79,6 +88,7 @@ const VarItem: FC<Props> = ({
|
||||
isShowEditVarModal && (
|
||||
<ConfigVarModal
|
||||
isShow
|
||||
supportFile
|
||||
payload={payload}
|
||||
onClose={hideEditVarModal}
|
||||
onConfirm={handlePayloadChange}
|
||||
|
@@ -73,6 +73,7 @@ const Panel: FC<NodePanelProps<StartNodeType>> = ({
|
||||
|
||||
<VarItem
|
||||
readonly
|
||||
showLegacyBadge={!isChatMode}
|
||||
payload={{
|
||||
variable: 'sys.files',
|
||||
} as any}
|
||||
@@ -163,6 +164,7 @@ const Panel: FC<NodePanelProps<StartNodeType>> = ({
|
||||
{isShowAddVarModal && (
|
||||
<ConfigVarModal
|
||||
isCreate
|
||||
supportFile
|
||||
isShow={isShowAddVarModal}
|
||||
onClose={hideAddVarModal}
|
||||
onConfirm={handleAddVarConfirm}
|
||||
|
@@ -127,14 +127,15 @@ const InputVarList: FC<Props> = ({
|
||||
const varInput = value[variable]
|
||||
const isNumber = type === FormTypeEnum.textNumber
|
||||
const isSelect = type === FormTypeEnum.select
|
||||
const isFile = type === FormTypeEnum.files
|
||||
const isFile = type === FormTypeEnum.file
|
||||
const isFileArray = type === FormTypeEnum.files
|
||||
const isString = type !== FormTypeEnum.textNumber && type !== FormTypeEnum.files && type !== FormTypeEnum.select
|
||||
return (
|
||||
<div key={variable} className='space-y-1'>
|
||||
<div className='flex items-center h-[18px] space-x-2'>
|
||||
<span className='text-[13px] font-medium text-gray-900'>{label[language] || label.en_US}</span>
|
||||
<span className='text-xs font-normal text-gray-500'>{paramType(type)}</span>
|
||||
{required && <span className='leading-[18px] text-xs font-normal text-[#EC4A0A]'>Required</span>}
|
||||
<span className='text-text-secondary code-sm-semibold'>{label[language] || label.en_US}</span>
|
||||
<span className='text-text-tertiary system-xs-regular'>{paramType(type)}</span>
|
||||
{required && <span className='text-util-colors-orange-dark-orange-dark-600 system-xs-regular'>Required</span>}
|
||||
</div>
|
||||
{isString && (
|
||||
<Input
|
||||
@@ -165,6 +166,18 @@ const InputVarList: FC<Props> = ({
|
||||
/>
|
||||
)}
|
||||
{isFile && (
|
||||
<VarReferencePicker
|
||||
readonly={readOnly}
|
||||
isShowNodeName
|
||||
nodeId={nodeId}
|
||||
value={varInput?.type === VarKindType.constant ? (varInput?.value || '') : (varInput?.value || [])}
|
||||
onChange={handleNotMixedTypeChange(variable)}
|
||||
onOpen={handleOpen(index)}
|
||||
defaultVarKindType={VarKindType.variable}
|
||||
filterVar={(varPayload: Var) => varPayload.type === VarType.file}
|
||||
/>
|
||||
)}
|
||||
{isFileArray && (
|
||||
<VarReferencePicker
|
||||
readonly={readOnly}
|
||||
isShowNodeName
|
||||
@@ -176,7 +189,7 @@ const InputVarList: FC<Props> = ({
|
||||
filterVar={(varPayload: Var) => varPayload.type === VarType.arrayFile}
|
||||
/>
|
||||
)}
|
||||
{tooltip && <div className='leading-[18px] text-xs font-normal text-gray-600'>{tooltip[language] || tooltip.en_US}</div>}
|
||||
{tooltip && <div className='text-text-tertiary body-xs-regular'>{tooltip[language] || tooltip.en_US}</div>}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
@@ -110,7 +110,7 @@ const VarGroupItem: FC<Props> = ({
|
||||
<Folder className='mr-0.5 w-3.5 h-3.5' />
|
||||
{(!isEditGroupName)
|
||||
? (
|
||||
<div className='flex items-center h-6 px-1 rounded-lg cursor-text hover:bg-gray-100' onClick={setEditGroupName}>
|
||||
<div className='flex items-center h-6 px-1 rounded-lg cursor-text text-text-secondary system-sm-semibold hover:bg-gray-100' onClick={setEditGroupName}>
|
||||
{payload.group_name}
|
||||
</div>
|
||||
)
|
||||
@@ -145,7 +145,7 @@ const VarGroupItem: FC<Props> = ({
|
||||
operations={
|
||||
<div className='flex items-center h-6 space-x-2'>
|
||||
{payload.variables.length > 0 && (
|
||||
<div className='flex items-center h-[18px] px-1 border border-black/8 rounded-[5px] text-xs font-medium text-gray-500 capitalize'>{payload.output_type}</div>
|
||||
<div className='flex items-center h-[18px] px-1 border border-divider-deep rounded-[5px] text-text-tertiary system-2xs-medium-uppercase'>{payload.output_type}</div>
|
||||
)}
|
||||
{
|
||||
!readOnly
|
||||
|
Reference in New Issue
Block a user