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
|
||||
}
|
||||
})
|
||||
|
Reference in New Issue
Block a user