feat: add FileUploaderField and TextAreaField components; enhance BaseField to support file inputs

This commit is contained in:
twwu 2025-05-20 15:09:30 +08:00
parent 55f4177b01
commit cf73faf174
10 changed files with 184 additions and 19 deletions

View File

@ -110,7 +110,7 @@ const FileUploaderInAttachment = ({
)
}
type FileUploaderInAttachmentWrapperProps = {
export type FileUploaderInAttachmentWrapperProps = {
value?: FileEntity[]
onChange: (files: FileEntity[]) => void
fileConfig: FileUpload

View File

@ -0,0 +1,40 @@
import React from 'react'
import { useFieldContext } from '../..'
import type { LabelProps } from '../label'
import Label from '../label'
import cn from '@/utils/classnames'
import type { FileUploaderInAttachmentWrapperProps } from '../../../file-uploader/file-uploader-in-attachment'
import FileUploaderInAttachmentWrapper from '../../../file-uploader/file-uploader-in-attachment'
import type { FileEntity } from '../../../file-uploader/types'
type FileUploaderFieldProps = {
label: string
labelOptions?: Omit<LabelProps, 'htmlFor' | 'label'>
className?: string
} & Omit<FileUploaderInAttachmentWrapperProps, 'value' | 'onChange'>
const FileUploaderField = ({
label,
labelOptions,
className,
...inputProps
}: FileUploaderFieldProps) => {
const field = useFieldContext<FileEntity[]>()
return (
<div className={cn('flex flex-col gap-y-0.5', className)}>
<Label
htmlFor={field.name}
label={label}
{...(labelOptions ?? {})}
/>
<FileUploaderInAttachmentWrapper
value={field.state.value}
onChange={value => field.handleChange(value)}
{...inputProps}
/>
</div>
)
}
export default FileUploaderField

View File

@ -0,0 +1,41 @@
import React from 'react'
import { useFieldContext } from '../..'
import type { LabelProps } from '../label'
import Label from '../label'
import cn from '@/utils/classnames'
import type { TextareaProps } from '../../../textarea'
import Textarea from '../../../textarea'
type TextAreaFieldProps = {
label: string
labelOptions?: Omit<LabelProps, 'htmlFor' | 'label'>
className?: string
} & Omit<TextareaProps, 'className' | 'onChange' | 'onBlur' | 'value' | 'id'>
const TextAreaField = ({
label,
labelOptions,
className,
...inputProps
}: TextAreaFieldProps) => {
const field = useFieldContext<string>()
return (
<div className={cn('flex flex-col gap-y-0.5', className)}>
<Label
htmlFor={field.name}
label={label}
{...(labelOptions ?? {})}
/>
<Textarea
id={field.name}
value={field.state.value}
onChange={e => field.handleChange(e.target.value)}
onBlur={field.handleBlur}
{...inputProps}
/>
</div>
)
}
export default TextAreaField

View File

@ -29,6 +29,10 @@ const BaseField = ({
required,
showOptional,
popupProps,
allowedFileExtensions,
allowedFileTypes,
allowedFileUploadMethods,
maxLength,
} = config
const isAllConditionsMet = useStore(form.store, (state) => {
@ -63,6 +67,25 @@ const BaseField = ({
)
}
if (type === BaseFieldType.paragraph) {
return (
<form.AppField
name={variable}
children={field => (
<field.TextAreaField
label={label}
labelOptions={{
tooltip,
isRequired: required,
showOptional,
}}
placeholder={placeholder}
/>
)}
/>
)
}
if (type === BaseFieldType.numberInput) {
return (
<form.AppField
@ -117,6 +140,53 @@ const BaseField = ({
)
}
if (type === BaseFieldType.file) {
return (
<form.AppField
name={variable}
children={field => (
<field.FileUploaderField
label={label}
labelOptions={{
tooltip,
isRequired: required,
showOptional,
}}
fileConfig={{
allowed_file_extensions: allowedFileExtensions,
allowed_file_types: allowedFileTypes,
allowed_file_upload_methods: allowedFileUploadMethods,
}}
/>
)}
/>
)
}
if (type === BaseFieldType.fileList) {
return (
<form.AppField
name={variable}
children={field => (
<field.FileUploaderField
label={label}
labelOptions={{
tooltip,
isRequired: required,
showOptional,
}}
fileConfig={{
allowed_file_extensions: allowedFileExtensions,
allowed_file_types: allowedFileTypes,
allowed_file_upload_methods: allowedFileUploadMethods,
number_limits: maxLength,
}}
/>
)}
/>
)
}
return <></>
},
})

View File

@ -1,11 +1,15 @@
import type { TransferMethod } from '@/types/app'
import type { FormType } from '../..'
import type { Option } from '../../../select/pure'
export enum BaseFieldType {
textInput = 'textInput',
paragraph = 'paragraph',
numberInput = 'numberInput',
checkbox = 'checkbox',
select = 'select',
file = 'file',
fileList = 'fileList',
}
export type ShowCondition = {
@ -28,6 +32,12 @@ export type SelectConfiguration = {
}
}
export type FileConfiguration = {
allowedFileTypes: string[]
allowedFileExtensions: string[]
allowedFileUploadMethods: TransferMethod[]
}
export type BaseConfiguration = {
label: string
variable: string // Variable name
@ -38,7 +48,9 @@ export type BaseConfiguration = {
showConditions: ShowCondition[] // Show this field only when all conditions are met
type: BaseFieldType
tooltip?: string // Tooltip for this field
} & NumberConfiguration & Partial<SelectConfiguration>
} & NumberConfiguration
& Partial<SelectConfiguration>
& Partial<FileConfiguration>
export type BaseFormProps = {
initialData?: Record<string, any>

View File

@ -11,6 +11,8 @@ import FileTypesField from './components/field/file-types'
import UploadMethodField from './components/field/upload-method'
import NumberSliderField from './components/field/number-slider'
import VariableOrConstantInputField from './components/field/variable-selector'
import TextAreaField from './components/field/text-area'
import FileUploaderField from './components/field/file-uploader'
export const { fieldContext, useFieldContext, formContext, useFormContext }
= createFormHookContexts()
@ -18,6 +20,7 @@ export const { fieldContext, useFieldContext, formContext, useFormContext }
export const { useAppForm, withForm } = createFormHook({
fieldComponents: {
TextField,
TextAreaField,
NumberInputField,
CheckboxField,
SelectField,
@ -28,6 +31,7 @@ export const { useAppForm, withForm } = createFormHook({
UploadMethodField,
NumberSliderField,
VariableOrConstantInputField,
FileUploaderField,
},
formComponents: {
Actions,

View File

@ -108,8 +108,6 @@ const InputFieldDialog = ({
{
datasourceKeys.map((key) => {
const inputFields = inputFieldsMap[key] || []
if (!inputFields.length)
return null
return (
<FieldList
key={key}

View File

@ -1,5 +1,5 @@
import { useCallback, useEffect } from 'react'
import { useDataSourceOptions } from '../hooks'
import { useDatasourceOptions } from '../hooks'
import OptionCard from './option-card'
import { File, Watercrawl } from '@/app/components/base/icons/src/public/knowledge'
import { Notion } from '@/app/components/base/icons/src/public/common'
@ -9,7 +9,7 @@ import { DataSourceProvider } from '@/models/common'
import type { Datasource } from '../types'
type DataSourceOptionsProps = {
dataSourceNodeId: string
datasourceNodeId: string
onSelect: (option: Datasource) => void
}
@ -22,17 +22,17 @@ const DATA_SOURCE_ICONS = {
}
const DataSourceOptions = ({
dataSourceNodeId,
datasourceNodeId,
onSelect,
}: DataSourceOptionsProps) => {
const { dataSources, options } = useDataSourceOptions()
const { datasources, options } = useDatasourceOptions()
const handelSelect = useCallback((value: string) => {
const selectedOption = dataSources.find(option => option.nodeId === value)
const selectedOption = datasources.find(option => option.nodeId === value)
if (!selectedOption)
return
onSelect(selectedOption)
}, [dataSources, onSelect])
}, [datasources, onSelect])
useEffect(() => {
if (options.length > 0)
@ -46,7 +46,7 @@ const DataSourceOptions = ({
<OptionCard
key={option.value}
label={option.label}
selected={dataSourceNodeId === option.value}
selected={datasourceNodeId === option.value}
Icon={DATA_SOURCE_ICONS[option.type as keyof typeof DATA_SOURCE_ICONS]}
onClick={handelSelect.bind(null, option.value)}
/>

View File

@ -23,12 +23,12 @@ export const useTestRunSteps = () => {
return steps
}
export const useDataSourceOptions = () => {
export const useDatasourceOptions = () => {
const { t } = useTranslation()
const nodes = useNodes<DataSourceNodeType>()
const dataSources: Datasource[] = useMemo(() => {
const dataSourceNodes = nodes.filter(node => node.data.type === BlockEnum.DataSource)
return dataSourceNodes.map((node) => {
const datasources: Datasource[] = useMemo(() => {
const datasourceNodes = nodes.filter(node => node.data.type === BlockEnum.DataSource)
return datasourceNodes.map((node) => {
let type: DataSourceType | DataSourceProvider = DataSourceType.FILE
switch (node.data.tool_name) {
case 'file_upload':
@ -57,7 +57,7 @@ export const useDataSourceOptions = () => {
const options = useMemo(() => {
const options: DataSourceOption[] = []
dataSources.forEach((source) => {
datasources.forEach((source) => {
if (source.type === DataSourceType.FILE) {
options.push({
label: t('datasetPipeline.testRun.dataSource.localFiles'),
@ -95,6 +95,6 @@ export const useDataSourceOptions = () => {
}
})
return options
}, [dataSources, t])
return { dataSources, options }
}, [datasources, t])
return { datasources, options }
}

View File

@ -146,7 +146,7 @@ const TestRunPanel = () => {
<>
<div className='flex flex-col gap-y-4 px-4 py-2'>
<DataSourceOptions
dataSourceNodeId={datasource?.nodeId || ''}
datasourceNodeId={datasource?.nodeId || ''}
onSelect={setDatasource}
/>
{datasource?.type === DataSourceType.FILE && (