file uploader

This commit is contained in:
StyleZhang 2024-09-10 14:17:29 +08:00
parent 97056dad30
commit 32b6c7063a
7 changed files with 69 additions and 33 deletions

View File

@ -38,7 +38,7 @@ const Operation = forwardRef<HTMLDivElement, OperationProps>(({
ref={ref} ref={ref}
> >
<div className='flex items-center space-x-1'> <div className='flex items-center space-x-1'>
{visionConfig?.enabled && <FileUploaderInChatInput />} <FileUploaderInChatInput />
{ {
speechToTextConfig?.enabled && ( speechToTextConfig?.enabled && (
<ActionButton <ActionButton

View File

@ -1,9 +1,11 @@
import type { ChangeEvent } from 'react'
import { import {
memo, memo,
useState, useState,
} from 'react' } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { RiUploadCloud2Line } from '@remixicon/react' import { RiUploadCloud2Line } from '@remixicon/react'
import { useFile } from '../hooks'
import { import {
PortalToFollowElem, PortalToFollowElem,
PortalToFollowElemContent, PortalToFollowElemContent,
@ -26,6 +28,16 @@ const FileFromLinkOrLocal = ({
const { t } = useTranslation() const { t } = useTranslation()
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
const [url, setUrl] = useState('') const [url, setUrl] = useState('')
const { handleLocalFileUpload } = useFile()
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0]
if (!file)
return
handleLocalFileUpload(file)
}
return ( return (
<PortalToFollowElem <PortalToFollowElem
@ -81,7 +93,7 @@ const FileFromLinkOrLocal = ({
className='absolute block inset-0 opacity-0 text-[0] w-full disabled:cursor-not-allowed cursor-pointer' className='absolute block inset-0 opacity-0 text-[0] w-full disabled:cursor-not-allowed cursor-pointer'
onClick={e => ((e.target as HTMLInputElement).value = '')} onClick={e => ((e.target as HTMLInputElement).value = '')}
type='file' type='file'
onChange={() => {}} onChange={handleChange}
/> />
</Button> </Button>
) )

View File

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

View File

@ -6,7 +6,7 @@ import {
import produce from 'immer' import produce from 'immer'
import { v4 as uuid4 } from 'uuid' import { v4 as uuid4 } from 'uuid'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import type { TFile } from './types' import type { FileEntity } from './types'
import { useFileStore } from './store' import { useFileStore } from './store'
import { fileUpload } from './utils' import { fileUpload } from './utils'
import { useToastContext } from '@/app/components/base/toast' import { useToastContext } from '@/app/components/base/toast'
@ -15,15 +15,12 @@ type UseFileParams = {
isPublicAPI?: boolean isPublicAPI?: boolean
url?: string url?: string
} }
export const useFile = ({ export const useFile = () => {
isPublicAPI,
url,
}: UseFileParams) => {
const { t } = useTranslation() const { t } = useTranslation()
const { notify } = useToastContext() const { notify } = useToastContext()
const fileStore = useFileStore() const fileStore = useFileStore()
const handleAddOrUpdateFiles = useCallback((newFile: TFile) => { const handleAddOrUpdateFiles = useCallback((newFile: FileEntity) => {
const { const {
files, files,
setFiles, setFiles,
@ -71,29 +68,37 @@ export const useFile = ({
setFiles([]) setFiles([])
}, [fileStore]) }, [fileStore])
const handleLocalFileUpload = useCallback((file: File) => { const handleLocalFileUpload = useCallback((
file: File,
{
isPublicAPI,
url,
}: UseFileParams = { isPublicAPI: false },
) => {
const reader = new FileReader() const reader = new FileReader()
const isImage = file.type.startsWith('image')
reader.addEventListener( reader.addEventListener(
'load', 'load',
() => { () => {
const imageFile = { const uploadingFile = {
_id: uuid4(), _id: uuid4(),
file, file,
_url: reader.result as string, _url: reader.result as string,
_progress: 0, _progress: 0,
_base64Url: isImage ? reader.result as string : '',
} }
handleAddOrUpdateFiles(imageFile) handleAddOrUpdateFiles(uploadingFile)
fileUpload({ fileUpload({
file: imageFile.file, file: uploadingFile.file,
onProgressCallback: (progress) => { onProgressCallback: (progress) => {
handleAddOrUpdateFiles({ ...imageFile, _progress: progress }) handleAddOrUpdateFiles({ ...uploadingFile, _progress: progress })
}, },
onSuccessCallback: (res) => { onSuccessCallback: (res) => {
handleAddOrUpdateFiles({ ...imageFile, _fileId: res.id, _progress: 100 }) handleAddOrUpdateFiles({ ...uploadingFile, _fileId: res.id, _progress: 100 })
}, },
onErrorCallback: () => { onErrorCallback: () => {
notify({ type: 'error', message: t('common.imageUploader.uploadFromComputerUploadError') }) notify({ type: 'error', message: t('common.imageUploader.uploadFromComputerUploadError') })
handleAddOrUpdateFiles({ ...imageFile, _progress: -1 }) handleAddOrUpdateFiles({ ...uploadingFile, _progress: -1 })
}, },
}, isPublicAPI, url) }, isPublicAPI, url)
}, },
@ -107,7 +112,7 @@ export const useFile = ({
false, false,
) )
reader.readAsDataURL(file) reader.readAsDataURL(file)
}, [notify, t, handleAddOrUpdateFiles, isPublicAPI, url]) }, [notify, t, handleAddOrUpdateFiles])
const handleClipboardPasteFile = useCallback((e: ClipboardEvent<HTMLTextAreaElement>) => { const handleClipboardPasteFile = useCallback((e: ClipboardEvent<HTMLTextAreaElement>) => {
const file = e.clipboardData?.files[0] const file = e.clipboardData?.files[0]

View File

@ -7,11 +7,11 @@ import {
useStore as useZustandStore, useStore as useZustandStore,
} from 'zustand' } from 'zustand'
import { createStore } from 'zustand/vanilla' import { createStore } from 'zustand/vanilla'
import type { TFile } from './types' import type { FileEntity } from './types'
type Shape = { type Shape = {
files: TFile[] files: FileEntity[]
setFiles: (files: TFile[]) => void setFiles: (files: FileEntity[]) => void
} }
export const createFileStore = () => { export const createFileStore = () => {

View File

@ -1,3 +1,5 @@
import type { TransferMethod } from '@/types/app'
export enum FileTypeEnum { export enum FileTypeEnum {
IMAGE = 'IMAGE', IMAGE = 'IMAGE',
VIDEO = 'VIDEO', VIDEO = 'VIDEO',
@ -13,10 +15,12 @@ export enum FileTypeEnum {
OTHER = 'OTHER', OTHER = 'OTHER',
} }
export type TFile = { export type FileEntity = {
file: File file: File
_id: string _id: string
_fileId?: string _fileId?: string
_progress?: number _progress?: number
_url?: string _url?: string
_base64Url?: string
_method?: TransferMethod
} }

View File

@ -34,3 +34,9 @@ export const fileUpload: FileUpload = ({
onErrorCallback() onErrorCallback()
}) })
} }
export const isFileType = (type: string) => {
return (file: File) => {
return file.type === type
}
}