file uploader

This commit is contained in:
StyleZhang 2024-09-13 16:46:16 +08:00
parent 323a835de9
commit a4c6d0b94b
19 changed files with 260 additions and 148 deletions

View File

@ -15,7 +15,7 @@ import type { Theme } from '../../embedded-chatbot/theme/theme-context'
import { useTextAreaHeight } from './hooks'
import Operation from './operation'
import cn from '@/utils/classnames'
import { FileListFlexOperation } from '@/app/components/base/file-uploader'
import { FileListInChatInput } from '@/app/components/base/file-uploader'
import { FileContextProvider } from '@/app/components/base/file-uploader/store'
import VoiceInput from '@/app/components/base/voice-input'
import { useToastContext } from '@/app/components/base/toast'
@ -111,7 +111,7 @@ const ChatInputArea = ({
)}
>
<div className='relative px-[9px] max-h-[158px] overflow-x-hidden overflow-y-auto'>
<FileListFlexOperation />
<FileListInChatInput />
<div
ref={wrapperRef}
className='flex items-center justify-between'

View File

@ -1,51 +0,0 @@
import { memo } from 'react'
import { RiDownloadLine } from '@remixicon/react'
import FileTypeIcon from '../file-type-icon'
import cn from '@/utils/classnames'
import ActionButton from '@/app/components/base/action-button'
type FileListItemProps = {
isFile?: boolean
className?: string
}
const FileListFlexItem = ({
isFile,
className,
}: FileListItemProps) => {
if (isFile) {
return (
<div className={cn(
'w-[144px] h-[68px] rounded-lg border-[0.5px] border-components-panel-border bg-components-card-bg-alt shadow-xs',
className,
)}>
<div className='mb-1 h-8 line-clamp-2 system-xs-medium text-text-tertiary'></div>
<div className='flex items-center justify-between'>
<div className='flex items-center system-2xs-medium-uppercase text-text-tertiary'>
<FileTypeIcon
size='sm'
type='PDF'
className='mr-1'
/>
PDF
<div className='mx-1'>·</div>
3.9 MB
</div>
<ActionButton
size='xs'
>
<RiDownloadLine className='w-3.5 h-3.5 text-text-tertiary' />
</ActionButton>
</div>
</div>
)
}
return (
<div className={cn(
'w-[68px] h-[68px] border-[2px] border-effects-image-frame shadow-xs',
className,
)}></div>
)
}
export default memo(FileListFlexItem)

View File

@ -1,58 +0,0 @@
import {
forwardRef,
memo,
} from 'react'
import { RiCloseLine } from '@remixicon/react'
import { useStore } from '../store'
import { useFile } from '../hooks'
import FileListItem from './file-list-flex-item'
import Button from '@/app/components/base/button'
import ProgressCircle from '@/app/components/base/progress-bar/progress-circle'
const FileListFlexOperation = forwardRef<HTMLDivElement>((_, ref) => {
const files = useStore(s => s.files)
const { handleRemoveFile } = useFile()
return (
<div
ref={ref}
className='flex flex-wrap gap-2'
>
{
files.map(file => (
<div
key={file.id}
className='relative'
>
<Button
className='absolute -right-1.5 -top-1.5 p-0 w-5 h-5 rounded-full z-10'
onClick={() => handleRemoveFile(file.id)}
>
<RiCloseLine className='w-4 h-4 text-components-button-secondary-text' />
</Button>
{
file.progress !== 100 && (
<div
className='absolute inset-0 border-[2px] border-effects-image-frame shadow-md bg-black'
>
<ProgressCircle
className='absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2'
percentage={file.progress}
size={16}
circleStrokeColor='stroke-components-progress-white-border'
circleFillColor='fill-transparent'
sectorFillColor='fill-components-progress-white-progress'
/>
</div>
)
}
<FileListItem />
</div>
))
}
</div>
)
})
FileListFlexOperation.displayName = 'FileListFlexOperation'
export default memo(FileListFlexOperation)

View File

@ -1,22 +0,0 @@
import {
forwardRef,
memo,
} from 'react'
import FileListFlexItem from './file-list-flex-item'
const FileListFlexPreview = forwardRef<HTMLDivElement>((_, ref) => {
return (
<div
ref={ref}
className='flex flex-wrap gap-2'
>
<FileListFlexItem />
<FileListFlexItem />
<FileListFlexItem isFile />
<FileListFlexItem isFile />
</div>
)
})
FileListFlexPreview.displayName = 'FileListFlexPreview'
export default memo(FileListFlexPreview)

View File

@ -1,8 +1,5 @@
import { memo } from 'react'
import {
RiDeleteBinLine,
RiEditCircleFill,
} from '@remixicon/react'
import { RiDeleteBinLine } from '@remixicon/react'
import FileTypeIcon from '../file-type-icon'
import type { FileEntity } from '../types'
import { useFile } from '../hooks'
@ -16,6 +13,7 @@ import ActionButton from '@/app/components/base/action-button'
import ProgressCircle from '@/app/components/base/progress-bar/progress-circle'
import { formatFileSize } from '@/utils/format'
import cn from '@/utils/classnames'
import { ReplayLine } from '@/app/components/base/icons/src/vender/other'
type FileInAttachmentItemProps = {
file: FileEntity
@ -80,8 +78,11 @@ const FileInAttachmentItem = ({
}
{
file.progress === -1 && (
<ActionButton onClick={() => handleReUploadFile(file.id)}>
<RiEditCircleFill className='mr-1 w-4 h-4 text-text-tertiary' />
<ActionButton
className='mr-1'
onClick={() => handleReUploadFile(file.id)}
>
<ReplayLine className='w-4 h-4 text-text-tertiary' />
</ActionButton>
)
}

View File

@ -0,0 +1,54 @@
import { RiCloseLine } from '@remixicon/react'
import FileImageRender from '../file-image-render'
import type { FileEntity } from '../types'
import { useFile } from '../hooks'
import Button from '@/app/components/base/button'
import ProgressCircle from '@/app/components/base/progress-bar/progress-circle'
import { ReplayLine } from '@/app/components/base/icons/src/vender/other'
type FileImageItemProps = {
file: FileEntity
className?: string
}
const FileImageItem = ({
file,
}: FileImageItemProps) => {
const { handleRemoveFile } = useFile()
return (
<div className='group relative'>
<Button
className='hidden group-hover:flex absolute -right-1.5 -top-1.5 p-0 w-5 h-5 rounded-full z-10'
onClick={() => handleRemoveFile(file.id)}
>
<RiCloseLine className='w-4 h-4 text-components-button-secondary-text' />
</Button>
<FileImageRender
className='w-[68px] h-[68px] shadow-md'
imageUrl={file.base64Url || ''}
/>
{
file.progress > 0 && file.progress < 100 && (
<div className='absolute inset-0 flex items-center justify-center border-[2px] border-effects-image-frame bg-background-overlay-alt'>
<ProgressCircle
percentage={file.progress}
size={12}
circleStrokeColor='stroke-components-progress-white-border'
circleFillColor='fill-transparent'
sectorFillColor='fill-components-progress-white-progress'
/>
</div>
)
}
{
file.progress === -1 && (
<div className='absolute inset-0 flex items-center justify-center border-[2px] border-state-destructive-border bg-background-overlay-destructive'>
<ReplayLine className='w-5 h-5' />
</div>
)
}
</div>
)
}
export default FileImageItem

View File

@ -0,0 +1,96 @@
import {
RiCloseLine,
RiDownloadLine,
} from '@remixicon/react'
import type { FileEntity } from '../types'
import {
getFileAppearanceType,
getFileExtension,
} from '../utils'
import { useFile } from '../hooks'
import FileTypeIcon from '../file-type-icon'
import cn from '@/utils/classnames'
import { formatFileSize } from '@/utils/format'
import ProgressCircle from '@/app/components/base/progress-bar/progress-circle'
import { ReplayLine } from '@/app/components/base/icons/src/vender/other'
import ActionButton from '@/app/components/base/action-button'
import Button from '@/app/components/base/button'
type FileItemProps = {
file: FileEntity
showDownload?: boolean
className?: string
}
const FileItem = ({
file,
showDownload,
}: FileItemProps) => {
const { handleRemoveFile } = useFile()
const ext = getFileExtension(file.file)
const uploadError = file.progress === -1
return (
<div
className={cn(
'group relative p-2 w-[144px] h-[68px] rounded-lg border-[0.5px] border-components-panel-border bg-components-card-bg shadow-xs',
!uploadError && 'hover:bg-components-card-bg-alt',
uploadError && 'border border-state-destructive-border bg-state-destructive-hover',
uploadError && 'hover:border-[0.5px] hover:border-state-destructive-border bg-state-destructive-hover-alt',
)}
>
<Button
className='hidden group-hover:flex absolute -right-1.5 -top-1.5 p-0 w-5 h-5 rounded-full z-10'
onClick={() => handleRemoveFile(file.id)}
>
<RiCloseLine className='w-4 h-4 text-components-button-secondary-text' />
</Button>
<div className='mb-1 h-8 line-clamp-2 system-xs-medium text-text-tertiary'>
{file.file?.name}
</div>
<div className='flex items-center justify-between'>
<div className='flex items-center system-2xs-medium-uppercase text-text-tertiary'>
<FileTypeIcon
size='sm'
type={getFileAppearanceType(file.file)}
className='mr-1'
/>
{
ext && (
<>
{ext}
<div className='mx-1'>·</div>
</>
)
}
{formatFileSize(file.file?.size || 0)}
</div>
{
showDownload && (
<ActionButton
size='xs'
>
<RiDownloadLine className='w-3.5 h-3.5 text-text-tertiary' />
</ActionButton>
)
}
{
file.progress > 0 && file.progress < 100 && (
<ProgressCircle
percentage={file.progress}
size={12}
/>
)
}
{
file.progress === -1 && (
<ReplayLine
className='w-4 h-4 text-text-tertiary'
/>
)
}
</div>
</div>
)
}
export default FileItem

View File

@ -0,0 +1,34 @@
import { isImage } from '../utils'
import { useStore } from '../store'
import FileImageItem from './file-image-item'
import FileItem from './file-item'
const FileList = () => {
const files = useStore(s => s.files)
return (
<div className='flex flex-wrap gap-2'>
{
files.map((file) => {
if (isImage(file.file)) {
return (
<FileImageItem
key={file.id}
file={file}
/>
)
}
return (
<FileItem
key={file.id}
file={file}
/>
)
})
}
</div>
)
}
export default FileList

View File

@ -1,4 +1,4 @@
export { default as FileUploaderInAttachmentWrapper } from './file-uploader-in-attachment'
export { default as FileUploaderInChatInput } from './file-uploader-in-chat-input'
export { default as FileTypeIcon } from './file-type-icon'
export { default as FileListFlexOperation } from './file-list-flex/file-list-flex-operation'
export { default as FileListInChatInput } from './file-uploader-in-chat-input/file-list'

View File

@ -0,0 +1,5 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Retry">
<path id="Vector" d="M9.99996 1.66669C14.6023 1.66669 18.3333 5.39765 18.3333 10C18.3333 14.6024 14.6023 18.3334 9.99996 18.3334C5.39758 18.3334 1.66663 14.6024 1.66663 10H3.33329C3.33329 13.6819 6.31806 16.6667 9.99996 16.6667C13.6819 16.6667 16.6666 13.6819 16.6666 10C16.6666 6.31812 13.6819 3.33335 9.99996 3.33335C7.70848 3.33335 5.68702 4.48947 4.48705 6.25022L6.66663 6.25002V7.91669H1.66663V2.91669H3.33329L3.3332 4.99934C4.85358 2.97565 7.2739 1.66669 9.99996 1.66669Z" fill="white"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 617 B

View File

@ -2,7 +2,7 @@
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './Unknown.json'
import data from './Unknow.json'
import IconBase from '@/app/components/base/icons/IconBase'
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
@ -11,6 +11,6 @@ const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseP
ref,
) => <IconBase {...props} ref={ref} data={data as IconData} />)
Icon.displayName = 'Unknown'
Icon.displayName = 'Unknow'
export default Icon

View File

@ -6,6 +6,6 @@ export { default as Json } from './Json'
export { default as Md } from './Md'
export { default as Pdf } from './Pdf'
export { default as Txt } from './Txt'
export { default as Unknown } from './Unknown'
export { default as Unknow } from './Unknow'
export { default as Xlsx } from './Xlsx'
export { default as Yaml } from './Yaml'

View File

@ -0,0 +1,36 @@
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "20",
"height": "20",
"viewBox": "0 0 20 20",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "g",
"attributes": {
"id": "Retry"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"id": "Vector",
"d": "M9.99996 1.66669C14.6023 1.66669 18.3333 5.39765 18.3333 10C18.3333 14.6024 14.6023 18.3334 9.99996 18.3334C5.39758 18.3334 1.66663 14.6024 1.66663 10H3.33329C3.33329 13.6819 6.31806 16.6667 9.99996 16.6667C13.6819 16.6667 16.6666 13.6819 16.6666 10C16.6666 6.31812 13.6819 3.33335 9.99996 3.33335C7.70848 3.33335 5.68702 4.48947 4.48705 6.25022L6.66663 6.25002V7.91669H1.66663V2.91669H3.33329L3.3332 4.99934C4.85358 2.97565 7.2739 1.66669 9.99996 1.66669Z",
"fill": "currentColor"
},
"children": []
}
]
}
]
},
"name": "ReplayLine"
}

View File

@ -2,7 +2,7 @@
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './CuteRobot.json'
import data from './ReplayLine.json'
import IconBase from '@/app/components/base/icons/IconBase'
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
@ -11,6 +11,6 @@ const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseP
ref,
) => <IconBase {...props} ref={ref} data={data as IconData} />)
Icon.displayName = 'CuteRobot'
Icon.displayName = 'ReplayLine'
export default Icon

View File

@ -1 +1,2 @@
export { default as Generator } from './Generator'
export { default as ReplayLine } from './ReplayLine'

View File

@ -0,0 +1,16 @@
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './CuteRobote.json'
import IconBase from '@/app/components/base/icons/IconBase'
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
props,
ref,
) => <IconBase {...props} ref={ref} data={data as IconData} />)
Icon.displayName = 'CuteRobote'
export default Icon

View File

@ -1,6 +1,6 @@
export { default as AiText } from './AiText'
export { default as ChatBot } from './ChatBot'
export { default as CuteRobot } from './CuteRobot'
export { default as CuteRobote } from './CuteRobote'
export { default as EditList } from './EditList'
export { default as MessageDotsCircle } from './MessageDotsCircle'
export { default as MessageFast } from './MessageFast'