mirror of
https://git.mirrors.martin98.com/https://github.com/langgenius/dify.git
synced 2025-08-18 02:45:57 +08:00
file uploader
This commit is contained in:
parent
97056dad30
commit
32b6c7063a
@ -38,7 +38,7 @@ const Operation = forwardRef<HTMLDivElement, OperationProps>(({
|
||||
ref={ref}
|
||||
>
|
||||
<div className='flex items-center space-x-1'>
|
||||
{visionConfig?.enabled && <FileUploaderInChatInput />}
|
||||
<FileUploaderInChatInput />
|
||||
{
|
||||
speechToTextConfig?.enabled && (
|
||||
<ActionButton
|
||||
|
@ -1,9 +1,11 @@
|
||||
import type { ChangeEvent } from 'react'
|
||||
import {
|
||||
memo,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiUploadCloud2Line } from '@remixicon/react'
|
||||
import { useFile } from '../hooks'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
@ -26,6 +28,16 @@ const FileFromLinkOrLocal = ({
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState(false)
|
||||
const [url, setUrl] = useState('')
|
||||
const { handleLocalFileUpload } = useFile()
|
||||
|
||||
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0]
|
||||
|
||||
if (!file)
|
||||
return
|
||||
|
||||
handleLocalFileUpload(file)
|
||||
}
|
||||
|
||||
return (
|
||||
<PortalToFollowElem
|
||||
@ -81,7 +93,7 @@ const FileFromLinkOrLocal = ({
|
||||
className='absolute block inset-0 opacity-0 text-[0] w-full disabled:cursor-not-allowed cursor-pointer'
|
||||
onClick={e => ((e.target as HTMLInputElement).value = '')}
|
||||
type='file'
|
||||
onChange={() => {}}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</Button>
|
||||
)
|
||||
|
@ -4,12 +4,14 @@ import {
|
||||
} 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
|
||||
@ -22,21 +24,28 @@ const FileListFlexOperation = forwardRef<HTMLDivElement>((_, ref) => {
|
||||
key={file._id}
|
||||
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' />
|
||||
</Button>
|
||||
<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={30}
|
||||
size={16}
|
||||
circleStrokeColor='stroke-components-progress-white-border'
|
||||
circleFillColor='fill-transparent'
|
||||
sectorFillColor='fill-components-progress-white-progress'
|
||||
/>
|
||||
</div>
|
||||
{
|
||||
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>
|
||||
))
|
||||
|
@ -6,7 +6,7 @@ import {
|
||||
import produce from 'immer'
|
||||
import { v4 as uuid4 } from 'uuid'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { TFile } from './types'
|
||||
import type { FileEntity } from './types'
|
||||
import { useFileStore } from './store'
|
||||
import { fileUpload } from './utils'
|
||||
import { useToastContext } from '@/app/components/base/toast'
|
||||
@ -15,15 +15,12 @@ type UseFileParams = {
|
||||
isPublicAPI?: boolean
|
||||
url?: string
|
||||
}
|
||||
export const useFile = ({
|
||||
isPublicAPI,
|
||||
url,
|
||||
}: UseFileParams) => {
|
||||
export const useFile = () => {
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useToastContext()
|
||||
const fileStore = useFileStore()
|
||||
|
||||
const handleAddOrUpdateFiles = useCallback((newFile: TFile) => {
|
||||
const handleAddOrUpdateFiles = useCallback((newFile: FileEntity) => {
|
||||
const {
|
||||
files,
|
||||
setFiles,
|
||||
@ -71,29 +68,37 @@ export const useFile = ({
|
||||
setFiles([])
|
||||
}, [fileStore])
|
||||
|
||||
const handleLocalFileUpload = useCallback((file: File) => {
|
||||
const handleLocalFileUpload = useCallback((
|
||||
file: File,
|
||||
{
|
||||
isPublicAPI,
|
||||
url,
|
||||
}: UseFileParams = { isPublicAPI: false },
|
||||
) => {
|
||||
const reader = new FileReader()
|
||||
const isImage = file.type.startsWith('image')
|
||||
reader.addEventListener(
|
||||
'load',
|
||||
() => {
|
||||
const imageFile = {
|
||||
const uploadingFile = {
|
||||
_id: uuid4(),
|
||||
file,
|
||||
_url: reader.result as string,
|
||||
_progress: 0,
|
||||
_base64Url: isImage ? reader.result as string : '',
|
||||
}
|
||||
handleAddOrUpdateFiles(imageFile)
|
||||
handleAddOrUpdateFiles(uploadingFile)
|
||||
fileUpload({
|
||||
file: imageFile.file,
|
||||
file: uploadingFile.file,
|
||||
onProgressCallback: (progress) => {
|
||||
handleAddOrUpdateFiles({ ...imageFile, _progress: progress })
|
||||
handleAddOrUpdateFiles({ ...uploadingFile, _progress: progress })
|
||||
},
|
||||
onSuccessCallback: (res) => {
|
||||
handleAddOrUpdateFiles({ ...imageFile, _fileId: res.id, _progress: 100 })
|
||||
handleAddOrUpdateFiles({ ...uploadingFile, _fileId: res.id, _progress: 100 })
|
||||
},
|
||||
onErrorCallback: () => {
|
||||
notify({ type: 'error', message: t('common.imageUploader.uploadFromComputerUploadError') })
|
||||
handleAddOrUpdateFiles({ ...imageFile, _progress: -1 })
|
||||
handleAddOrUpdateFiles({ ...uploadingFile, _progress: -1 })
|
||||
},
|
||||
}, isPublicAPI, url)
|
||||
},
|
||||
@ -107,7 +112,7 @@ export const useFile = ({
|
||||
false,
|
||||
)
|
||||
reader.readAsDataURL(file)
|
||||
}, [notify, t, handleAddOrUpdateFiles, isPublicAPI, url])
|
||||
}, [notify, t, handleAddOrUpdateFiles])
|
||||
|
||||
const handleClipboardPasteFile = useCallback((e: ClipboardEvent<HTMLTextAreaElement>) => {
|
||||
const file = e.clipboardData?.files[0]
|
||||
|
@ -7,11 +7,11 @@ import {
|
||||
useStore as useZustandStore,
|
||||
} from 'zustand'
|
||||
import { createStore } from 'zustand/vanilla'
|
||||
import type { TFile } from './types'
|
||||
import type { FileEntity } from './types'
|
||||
|
||||
type Shape = {
|
||||
files: TFile[]
|
||||
setFiles: (files: TFile[]) => void
|
||||
files: FileEntity[]
|
||||
setFiles: (files: FileEntity[]) => void
|
||||
}
|
||||
|
||||
export const createFileStore = () => {
|
||||
|
@ -1,3 +1,5 @@
|
||||
import type { TransferMethod } from '@/types/app'
|
||||
|
||||
export enum FileTypeEnum {
|
||||
IMAGE = 'IMAGE',
|
||||
VIDEO = 'VIDEO',
|
||||
@ -13,10 +15,12 @@ export enum FileTypeEnum {
|
||||
OTHER = 'OTHER',
|
||||
}
|
||||
|
||||
export type TFile = {
|
||||
export type FileEntity = {
|
||||
file: File
|
||||
_id: string
|
||||
_fileId?: string
|
||||
_progress?: number
|
||||
_url?: string
|
||||
_base64Url?: string
|
||||
_method?: TransferMethod
|
||||
}
|
||||
|
@ -34,3 +34,9 @@ export const fileUpload: FileUpload = ({
|
||||
onErrorCallback()
|
||||
})
|
||||
}
|
||||
|
||||
export const isFileType = (type: string) => {
|
||||
return (file: File) => {
|
||||
return file.type === type
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user