mirror of
https://git.mirrors.martin98.com/https://github.com/langgenius/dify.git
synced 2025-08-18 22:15:58 +08:00
file uploader
This commit is contained in:
parent
97056dad30
commit
32b6c7063a
@ -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
|
||||||
|
@ -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>
|
||||||
)
|
)
|
||||||
|
@ -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>
|
||||||
))
|
))
|
||||||
|
@ -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]
|
||||||
|
@ -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 = () => {
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -34,3 +34,9 @@ export const fileUpload: FileUpload = ({
|
|||||||
onErrorCallback()
|
onErrorCallback()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const isFileType = (type: string) => {
|
||||||
|
return (file: File) => {
|
||||||
|
return file.type === type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user