dify/web/app/components/datasets/common/image-uploader/image-uploader-in-retrieval-testing/index.tsx
Wu Tianwei 14d1b3f9b3
feat: multimodal support (image) (#27793)
Co-authored-by: zxhlyh <jasonapring2015@outlook.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-09 11:44:50 +08:00

132 lines
3.6 KiB
TypeScript

import {
useCallback,
useState,
} from 'react'
import {
FileContextProvider,
} from '../store'
import type { FileEntity } from '../types'
import { useUpload } from '../hooks/use-upload'
import ImageInput from './image-input'
import cn from '@/utils/classnames'
import { useTranslation } from 'react-i18next'
import { useFileStoreWithSelector } from '../store'
import ImageItem from './image-item'
import type { ImageInfo } from '@/app/components/datasets/common/image-previewer'
import ImagePreviewer from '@/app/components/datasets/common/image-previewer'
type ImageUploaderInRetrievalTestingProps = {
textArea: React.ReactNode
actionButton: React.ReactNode
showUploader?: boolean
className?: string
actionAreaClassName?: string
}
const ImageUploaderInRetrievalTesting = ({
textArea,
actionButton,
showUploader = true,
className,
actionAreaClassName,
}: ImageUploaderInRetrievalTestingProps) => {
const { t } = useTranslation()
const files = useFileStoreWithSelector(s => s.files)
const [previewIndex, setPreviewIndex] = useState(0)
const [previewImages, setPreviewImages] = useState<ImageInfo[]>([])
const {
dragging,
dragRef,
dropRef,
handleRemoveFile,
handleReUploadFile,
} = useUpload()
const handleImagePreview = useCallback((fileId: string) => {
const index = files.findIndex(item => item.id === fileId)
if (index === -1) return
setPreviewIndex(index)
setPreviewImages(files.map(item => ({
url: item.base64Url || item.sourceUrl || '',
name: item.name,
size: item.size,
})))
}, [files])
const handleClosePreview = useCallback(() => {
setPreviewImages([])
}, [])
return (
<div
ref={dropRef}
className={cn('relative flex w-full flex-col', className)}
>
{dragging && (
<div
className='absolute inset-0.5 z-10 flex items-center justify-center rounded-lg border-[1.5px] border-dashed border-components-dropzone-border-accent bg-components-dropzone-bg-accent'
>
<div>{t('datasetHitTesting.imageUploader.dropZoneTip')}</div>
<div ref={dragRef} className='absolute inset-0' />
</div>
)}
{textArea}
{
showUploader && !!files.length && (
<div className='flex flex-wrap gap-1 bg-background-default px-4 py-2'>
{
files.map(file => (
<ImageItem
key={file.id}
file={file}
showDeleteAction
onRemove={handleRemoveFile}
onReUpload={handleReUploadFile}
onPreview={handleImagePreview}
/>
))
}
</div>
)
}
<div
className={cn(
'flex',
showUploader ? 'justify-between' : 'justify-end',
actionAreaClassName,
)}>
{showUploader && <ImageInput />}
{actionButton}
</div>
{previewImages.length > 0 && (
<ImagePreviewer
images={previewImages}
initialIndex={previewIndex}
onClose={handleClosePreview}
/>
)}
</div>
)
}
export type ImageUploaderInRetrievalTestingWrapperProps = {
value?: FileEntity[]
onChange: (files: FileEntity[]) => void
} & ImageUploaderInRetrievalTestingProps
const ImageUploaderInRetrievalTestingWrapper = ({
value,
onChange,
...props
}: ImageUploaderInRetrievalTestingWrapperProps) => {
return (
<FileContextProvider
value={value}
onChange={onChange}
>
<ImageUploaderInRetrievalTesting {...props} />
</FileContextProvider>
)
}
export default ImageUploaderInRetrievalTestingWrapper