mirror of
https://git.mirrors.martin98.com/https://github.com/langgenius/dify.git
synced 2025-08-18 12:55:56 +08:00
file uploader
This commit is contained in:
parent
1df41cef4c
commit
fd9b71c4d7
@ -1,5 +1,4 @@
|
|||||||
import {
|
import {
|
||||||
memo,
|
|
||||||
useCallback,
|
useCallback,
|
||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
@ -16,11 +15,15 @@ import { useTextAreaHeight } from './hooks'
|
|||||||
import Operation from './operation'
|
import Operation from './operation'
|
||||||
import cn from '@/utils/classnames'
|
import cn from '@/utils/classnames'
|
||||||
import { FileListInChatInput } from '@/app/components/base/file-uploader'
|
import { FileListInChatInput } from '@/app/components/base/file-uploader'
|
||||||
import { FileContextProvider } from '@/app/components/base/file-uploader/store'
|
import {
|
||||||
|
FileContextProvider,
|
||||||
|
useStore,
|
||||||
|
} from '@/app/components/base/file-uploader/store'
|
||||||
import VoiceInput from '@/app/components/base/voice-input'
|
import VoiceInput from '@/app/components/base/voice-input'
|
||||||
import { useToastContext } from '@/app/components/base/toast'
|
import { useToastContext } from '@/app/components/base/toast'
|
||||||
import FeatureBar from '@/app/components/base/features/new-feature-panel/feature-bar'
|
import FeatureBar from '@/app/components/base/features/new-feature-panel/feature-bar'
|
||||||
import type { FileUpload } from '@/app/components/base/features/types'
|
import type { FileUpload } from '@/app/components/base/features/types'
|
||||||
|
import { TransferMethod } from '@/types/app'
|
||||||
|
|
||||||
type ChatInputAreaProps = {
|
type ChatInputAreaProps = {
|
||||||
showFeatureBar?: boolean
|
showFeatureBar?: boolean
|
||||||
@ -55,15 +58,27 @@ const ChatInputArea = ({
|
|||||||
const [query, setQuery] = useState('')
|
const [query, setQuery] = useState('')
|
||||||
const isUseInputMethod = useRef(false)
|
const isUseInputMethod = useRef(false)
|
||||||
const [showVoiceInput, setShowVoiceInput] = useState(false)
|
const [showVoiceInput, setShowVoiceInput] = useState(false)
|
||||||
|
const files = useStore(s => s.files)
|
||||||
|
const setFiles = useStore(s => s.setFiles)
|
||||||
|
|
||||||
const handleSend = () => {
|
const handleSend = () => {
|
||||||
if (onSend) {
|
if (onSend) {
|
||||||
|
if (files.find(item => item.type === TransferMethod.local_file && !item.fileStorageId)) {
|
||||||
|
notify({ type: 'info', message: t('appDebug.errorMessage.waitForImgUpload') })
|
||||||
|
return
|
||||||
|
}
|
||||||
if (!query || !query.trim()) {
|
if (!query || !query.trim()) {
|
||||||
notify({ type: 'info', message: t('appAnnotation.errorMessage.queryRequired') })
|
notify({ type: 'info', message: t('appAnnotation.errorMessage.queryRequired') })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
onSend(query)
|
onSend(query, files.filter(file => file.progress !== -1).map(fileItem => ({
|
||||||
|
type: fileItem.fileType,
|
||||||
|
transfer_method: fileItem.type,
|
||||||
|
url: fileItem.url || '',
|
||||||
|
upload_file_id: fileItem.fileStorageId || '',
|
||||||
|
})))
|
||||||
setQuery('')
|
setQuery('')
|
||||||
|
setFiles([])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -103,64 +118,70 @@ const ChatInputArea = ({
|
|||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FileContextProvider onChange={() => {}}>
|
<>
|
||||||
<>
|
<div
|
||||||
<div
|
className={cn(
|
||||||
className={cn(
|
'relative py-[9px] bg-components-panel-bg-blur border border-components-chat-input-border rounded-xl shadow-md z-10',
|
||||||
'relative py-[9px] bg-components-panel-bg-blur border border-components-chat-input-border rounded-xl shadow-md z-10',
|
)}
|
||||||
)}
|
>
|
||||||
>
|
<div className='relative px-[9px] max-h-[158px] overflow-x-hidden overflow-y-auto'>
|
||||||
<div className='relative px-[9px] max-h-[158px] overflow-x-hidden overflow-y-auto'>
|
<FileListInChatInput />
|
||||||
<FileListInChatInput />
|
<div
|
||||||
<div
|
ref={wrapperRef}
|
||||||
ref={wrapperRef}
|
className='flex items-center justify-between'
|
||||||
className='flex items-center justify-between'
|
>
|
||||||
>
|
<div className='flex items-center relative grow w-full'>
|
||||||
<div className='flex items-center relative grow w-full'>
|
<div
|
||||||
<div
|
ref={textValueRef}
|
||||||
ref={textValueRef}
|
className='absolute w-auto h-auto p-1 leading-6 body-lg-regular pointer-events-none whitespace-pre invisible'
|
||||||
className='absolute w-auto h-auto p-1 leading-6 body-lg-regular pointer-events-none whitespace-pre invisible'
|
>
|
||||||
>
|
{query}
|
||||||
{query}
|
|
||||||
</div>
|
|
||||||
<Textarea
|
|
||||||
ref={textareaRef}
|
|
||||||
className='p-1 w-full leading-6 body-lg-regular text-text-tertiary outline-none'
|
|
||||||
placeholder='Enter message...'
|
|
||||||
autoSize={{ minRows: 1 }}
|
|
||||||
onResize={handleTextareaResize}
|
|
||||||
value={query}
|
|
||||||
onChange={(e) => {
|
|
||||||
setQuery(e.target.value)
|
|
||||||
handleTextareaResize()
|
|
||||||
}}
|
|
||||||
onKeyUp={handleKeyUp}
|
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
{
|
<Textarea
|
||||||
!isMultipleLine && operation
|
ref={textareaRef}
|
||||||
}
|
className='p-1 w-full leading-6 body-lg-regular text-text-tertiary outline-none'
|
||||||
|
placeholder='Enter message...'
|
||||||
|
autoSize={{ minRows: 1 }}
|
||||||
|
onResize={handleTextareaResize}
|
||||||
|
value={query}
|
||||||
|
onChange={(e) => {
|
||||||
|
setQuery(e.target.value)
|
||||||
|
handleTextareaResize()
|
||||||
|
}}
|
||||||
|
onKeyUp={handleKeyUp}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
{
|
{
|
||||||
showVoiceInput && (
|
!isMultipleLine && operation
|
||||||
<VoiceInput
|
|
||||||
onCancel={() => setShowVoiceInput(false)}
|
|
||||||
onConverted={text => setQuery(text)}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
{
|
{
|
||||||
isMultipleLine && (
|
showVoiceInput && (
|
||||||
<div className='px-[9px]'>{operation}</div>
|
<VoiceInput
|
||||||
|
onCancel={() => setShowVoiceInput(false)}
|
||||||
|
onConverted={text => setQuery(text)}
|
||||||
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
{showFeatureBar && <FeatureBar showFileUpload={showFileUpload} disabled={featureBarDisabled} onFeatureBarClick={onFeatureBarClick} />}
|
{
|
||||||
</>
|
isMultipleLine && (
|
||||||
|
<div className='px-[9px]'>{operation}</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
{showFeatureBar && <FeatureBar showFileUpload={showFileUpload} disabled={featureBarDisabled} onFeatureBarClick={onFeatureBarClick} />}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const ChatInputAreaWrapper = (props: ChatInputAreaProps) => {
|
||||||
|
return (
|
||||||
|
<FileContextProvider>
|
||||||
|
<ChatInputArea {...props} />
|
||||||
</FileContextProvider>
|
</FileContextProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default memo(ChatInputArea)
|
export default ChatInputAreaWrapper
|
||||||
|
1
web/app/components/base/file-uploader/constants.ts
Normal file
1
web/app/components/base/file-uploader/constants.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export const FILE_LIMIT = 15 * 1024 * 1024
|
@ -9,14 +9,21 @@ import { v4 as uuid4 } from 'uuid'
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import type { FileEntity } from './types'
|
import type { FileEntity } from './types'
|
||||||
import { useFileStore } from './store'
|
import { useFileStore } from './store'
|
||||||
import { fileUpload } from './utils'
|
import {
|
||||||
|
fileUpload,
|
||||||
|
getFileType,
|
||||||
|
} from './utils'
|
||||||
import { useToastContext } from '@/app/components/base/toast'
|
import { useToastContext } from '@/app/components/base/toast'
|
||||||
|
import { TransferMethod } from '@/types/app'
|
||||||
|
import { useFeaturesStore } from '@/app/components/base/features/hooks'
|
||||||
|
import { SupportUploadFileTypes } from '@/app/components/workflow/types'
|
||||||
|
|
||||||
export const useFile = () => {
|
export const useFile = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { notify } = useToastContext()
|
const { notify } = useToastContext()
|
||||||
const fileStore = useFileStore()
|
const fileStore = useFileStore()
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
|
const featuresStore = useFeaturesStore()
|
||||||
|
|
||||||
const handleAddOrUpdateFiles = useCallback((newFile: FileEntity) => {
|
const handleAddOrUpdateFiles = useCallback((newFile: FileEntity) => {
|
||||||
const {
|
const {
|
||||||
@ -90,6 +97,9 @@ export const useFile = () => {
|
|||||||
const handleLocalFileUpload = useCallback((file: File) => {
|
const handleLocalFileUpload = useCallback((file: File) => {
|
||||||
const reader = new FileReader()
|
const reader = new FileReader()
|
||||||
const isImage = file.type.startsWith('image')
|
const isImage = file.type.startsWith('image')
|
||||||
|
const allowedFileTypes = featuresStore?.getState().features.file?.allowed_file_types
|
||||||
|
const isCustomFileType = allowedFileTypes?.includes(SupportUploadFileTypes.custom)
|
||||||
|
|
||||||
reader.addEventListener(
|
reader.addEventListener(
|
||||||
'load',
|
'load',
|
||||||
() => {
|
() => {
|
||||||
@ -99,6 +109,8 @@ export const useFile = () => {
|
|||||||
url: '',
|
url: '',
|
||||||
progress: 0,
|
progress: 0,
|
||||||
base64Url: isImage ? reader.result as string : '',
|
base64Url: isImage ? reader.result as string : '',
|
||||||
|
fileType: isCustomFileType ? SupportUploadFileTypes.custom : getFileType(file),
|
||||||
|
type: TransferMethod.local_file,
|
||||||
}
|
}
|
||||||
handleAddOrUpdateFiles(uploadingFile)
|
handleAddOrUpdateFiles(uploadingFile)
|
||||||
fileUpload({
|
fileUpload({
|
||||||
|
@ -7,21 +7,20 @@ import {
|
|||||||
create,
|
create,
|
||||||
useStore as useZustandStore,
|
useStore as useZustandStore,
|
||||||
} from 'zustand'
|
} from 'zustand'
|
||||||
import type { FileEntity } from './types'
|
import type {
|
||||||
|
FileEntity,
|
||||||
|
} from './types'
|
||||||
|
|
||||||
type Shape = {
|
type Shape = {
|
||||||
files: FileEntity[]
|
files: FileEntity[]
|
||||||
setFiles: (files: FileEntity[]) => void
|
setFiles: (files: FileEntity[]) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createFileStore = ({
|
export const createFileStore = () => {
|
||||||
onChange,
|
|
||||||
}: Pick<FileProviderProps, 'onChange'>) => {
|
|
||||||
return create<Shape>(set => ({
|
return create<Shape>(set => ({
|
||||||
files: [],
|
files: [],
|
||||||
setFiles: (files) => {
|
setFiles: (files) => {
|
||||||
set({ files })
|
set({ files })
|
||||||
onChange(files)
|
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
@ -43,18 +42,16 @@ export const useFileStore = () => {
|
|||||||
|
|
||||||
type FileProviderProps = {
|
type FileProviderProps = {
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
onChange: (files: FileEntity[]) => void
|
|
||||||
isPublicAPI?: boolean
|
isPublicAPI?: boolean
|
||||||
url?: string
|
url?: string
|
||||||
}
|
}
|
||||||
export const FileContextProvider = ({
|
export const FileContextProvider = ({
|
||||||
children,
|
children,
|
||||||
onChange,
|
|
||||||
}: FileProviderProps) => {
|
}: FileProviderProps) => {
|
||||||
const storeRef = useRef<FileStore>()
|
const storeRef = useRef<FileStore>()
|
||||||
|
|
||||||
if (!storeRef.current)
|
if (!storeRef.current)
|
||||||
storeRef.current = createFileStore({ onChange })
|
storeRef.current = createFileStore()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FileContext.Provider value={storeRef.current}>
|
<FileContext.Provider value={storeRef.current}>
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import type { TransferMethod } from '@/types/app'
|
||||||
|
|
||||||
export enum FileAppearanceTypeEnum {
|
export enum FileAppearanceTypeEnum {
|
||||||
IMAGE = 'IMAGE',
|
IMAGE = 'IMAGE',
|
||||||
VIDEO = 'VIDEO',
|
VIDEO = 'VIDEO',
|
||||||
@ -22,4 +24,6 @@ export type FileEntity = {
|
|||||||
progress: number
|
progress: number
|
||||||
url?: string
|
url?: string
|
||||||
base64Url?: string
|
base64Url?: string
|
||||||
|
type: TransferMethod
|
||||||
|
fileType: string
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { FileAppearanceTypeEnum } from './types'
|
import { FileAppearanceTypeEnum } from './types'
|
||||||
import { upload } from '@/service/base'
|
import { upload } from '@/service/base'
|
||||||
|
import { FILE_EXTS } from '@/app/components/base/prompt-editor/constants'
|
||||||
|
|
||||||
type FileUploadParams = {
|
type FileUploadParams = {
|
||||||
file: File
|
file: File
|
||||||
@ -72,3 +73,13 @@ export const getFileExtension = (file?: File) => {
|
|||||||
|
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getFileType = (file?: File) => {
|
||||||
|
const extension = getFileExtension(file)
|
||||||
|
for (const key in FILE_EXTS) {
|
||||||
|
if ((FILE_EXTS[key]).includes(extension.toUpperCase()))
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user