feat: multimodal support (image) (#27793)

Co-authored-by: zxhlyh <jasonapring2015@outlook.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Wu Tianwei
2025-12-09 11:44:50 +08:00
committed by GitHub
parent a44b800c85
commit 14d1b3f9b3
77 changed files with 2932 additions and 579 deletions

View File

@@ -0,0 +1,88 @@
import { useCallback, useMemo, useState } from 'react'
import type { FileEntity } from '@/app/components/base/file-thumb'
import FileThumb from '@/app/components/base/file-thumb'
import cn from '@/utils/classnames'
import More from './more'
import type { ImageInfo } from '../image-previewer'
import ImagePreviewer from '../image-previewer'
type Image = {
name: string
mimeType: string
sourceUrl: string
size: number
extension: string
}
type ImageListProps = {
images: Image[]
size: 'sm' | 'md'
limit?: number
className?: string
}
const ImageList = ({
images,
size,
limit = 9,
className,
}: ImageListProps) => {
const [showMore, setShowMore] = useState(false)
const [previewIndex, setPreviewIndex] = useState(0)
const [previewImages, setPreviewImages] = useState<ImageInfo[]>([])
const limitedImages = useMemo(() => {
return showMore ? images : images.slice(0, limit)
}, [images, limit, showMore])
const handleShowMore = useCallback(() => {
setShowMore(true)
}, [])
const handleImageClick = useCallback((file: FileEntity) => {
const index = limitedImages.findIndex(image => image.sourceUrl === file.sourceUrl)
if (index === -1) return
setPreviewIndex(index)
setPreviewImages(limitedImages.map(image => ({
url: image.sourceUrl,
name: image.name,
size: image.size,
})))
}, [limitedImages])
const handleClosePreview = useCallback(() => {
setPreviewImages([])
}, [])
return (
<>
<div className={cn('flex flex-wrap gap-1', className)}>
{
limitedImages.map(image => (
<FileThumb
key={image.sourceUrl}
file={image}
size={size}
onClick={handleImageClick}
/>
))
}
{images.length > limit && !showMore && (
<More
count={images.length - limitedImages.length}
onClick={handleShowMore}
/>
)}
</div>
{previewImages.length > 0 && (
<ImagePreviewer
images={previewImages}
initialIndex={previewIndex}
onClose={handleClosePreview}
/>
)}
</>
)
}
export default ImageList

View File

@@ -0,0 +1,39 @@
import React, { useCallback } from 'react'
type MoreProps = {
count: number
onClick?: () => void
}
const More = ({ count, onClick }: MoreProps) => {
const formatNumber = (num: number) => {
if (num === 0)
return '0'
if (num < 1000)
return num.toString()
if (num < 1000000)
return `${(num / 1000).toFixed(1)}k`
return `${(num / 1000000).toFixed(1)}M`
}
const handleClick = useCallback((e: React.MouseEvent<HTMLDivElement>) => {
e.stopPropagation()
e.preventDefault()
onClick?.()
}, [onClick])
return (
<div className='relative size-8 cursor-pointer p-[0.5px]' onClick={handleClick}>
<div className='relative z-10 size-full rounded-md border-[1.5px] border-components-panel-bg bg-divider-regular'>
<div className='flex size-full items-center justify-center'>
<span className='system-xs-regular text-text-tertiary'>
{`+${formatNumber(count)}`}
</span>
</div>
</div>
<div className='absolute -right-0.5 top-1 z-0 h-6 w-1 rounded-r-md bg-divider-regular' />
</div>
)
}
export default React.memo(More)