file uploader

This commit is contained in:
StyleZhang 2024-09-18 16:36:31 +08:00
parent 1df41cef4c
commit fd9b71c4d7
6 changed files with 106 additions and 60 deletions

View File

@ -1,5 +1,4 @@
import {
memo,
useCallback,
useRef,
useState,
@ -16,11 +15,15 @@ import { useTextAreaHeight } from './hooks'
import Operation from './operation'
import cn from '@/utils/classnames'
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 { useToastContext } from '@/app/components/base/toast'
import FeatureBar from '@/app/components/base/features/new-feature-panel/feature-bar'
import type { FileUpload } from '@/app/components/base/features/types'
import { TransferMethod } from '@/types/app'
type ChatInputAreaProps = {
showFeatureBar?: boolean
@ -55,15 +58,27 @@ const ChatInputArea = ({
const [query, setQuery] = useState('')
const isUseInputMethod = useRef(false)
const [showVoiceInput, setShowVoiceInput] = useState(false)
const files = useStore(s => s.files)
const setFiles = useStore(s => s.setFiles)
const handleSend = () => {
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()) {
notify({ type: 'info', message: t('appAnnotation.errorMessage.queryRequired') })
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('')
setFiles([])
}
}
@ -103,64 +118,70 @@ const ChatInputArea = ({
)
return (
<FileContextProvider onChange={() => {}}>
<>
<div
className={cn(
'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'>
<FileListInChatInput />
<div
ref={wrapperRef}
className='flex items-center justify-between'
>
<div className='flex items-center relative grow w-full'>
<div
ref={textValueRef}
className='absolute w-auto h-auto p-1 leading-6 body-lg-regular pointer-events-none whitespace-pre invisible'
>
{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
className={cn(
'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'>
<FileListInChatInput />
<div
ref={wrapperRef}
className='flex items-center justify-between'
>
<div className='flex items-center relative grow w-full'>
<div
ref={textValueRef}
className='absolute w-auto h-auto p-1 leading-6 body-lg-regular pointer-events-none whitespace-pre invisible'
>
{query}
</div>
{
!isMultipleLine && operation
}
<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>
{
showVoiceInput && (
<VoiceInput
onCancel={() => setShowVoiceInput(false)}
onConverted={text => setQuery(text)}
/>
)
!isMultipleLine && operation
}
</div>
{
isMultipleLine && (
<div className='px-[9px]'>{operation}</div>
showVoiceInput && (
<VoiceInput
onCancel={() => setShowVoiceInput(false)}
onConverted={text => setQuery(text)}
/>
)
}
</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>
)
}
export default memo(ChatInputArea)
export default ChatInputAreaWrapper

View File

@ -0,0 +1 @@
export const FILE_LIMIT = 15 * 1024 * 1024

View File

@ -9,14 +9,21 @@ import { v4 as uuid4 } from 'uuid'
import { useTranslation } from 'react-i18next'
import type { FileEntity } from './types'
import { useFileStore } from './store'
import { fileUpload } from './utils'
import {
fileUpload,
getFileType,
} from './utils'
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 = () => {
const { t } = useTranslation()
const { notify } = useToastContext()
const fileStore = useFileStore()
const params = useParams()
const featuresStore = useFeaturesStore()
const handleAddOrUpdateFiles = useCallback((newFile: FileEntity) => {
const {
@ -90,6 +97,9 @@ export const useFile = () => {
const handleLocalFileUpload = useCallback((file: File) => {
const reader = new FileReader()
const isImage = file.type.startsWith('image')
const allowedFileTypes = featuresStore?.getState().features.file?.allowed_file_types
const isCustomFileType = allowedFileTypes?.includes(SupportUploadFileTypes.custom)
reader.addEventListener(
'load',
() => {
@ -99,6 +109,8 @@ export const useFile = () => {
url: '',
progress: 0,
base64Url: isImage ? reader.result as string : '',
fileType: isCustomFileType ? SupportUploadFileTypes.custom : getFileType(file),
type: TransferMethod.local_file,
}
handleAddOrUpdateFiles(uploadingFile)
fileUpload({

View File

@ -7,21 +7,20 @@ import {
create,
useStore as useZustandStore,
} from 'zustand'
import type { FileEntity } from './types'
import type {
FileEntity,
} from './types'
type Shape = {
files: FileEntity[]
setFiles: (files: FileEntity[]) => void
}
export const createFileStore = ({
onChange,
}: Pick<FileProviderProps, 'onChange'>) => {
export const createFileStore = () => {
return create<Shape>(set => ({
files: [],
setFiles: (files) => {
set({ files })
onChange(files)
},
}))
}
@ -43,18 +42,16 @@ export const useFileStore = () => {
type FileProviderProps = {
children: React.ReactNode
onChange: (files: FileEntity[]) => void
isPublicAPI?: boolean
url?: string
}
export const FileContextProvider = ({
children,
onChange,
}: FileProviderProps) => {
const storeRef = useRef<FileStore>()
if (!storeRef.current)
storeRef.current = createFileStore({ onChange })
storeRef.current = createFileStore()
return (
<FileContext.Provider value={storeRef.current}>

View File

@ -1,3 +1,5 @@
import type { TransferMethod } from '@/types/app'
export enum FileAppearanceTypeEnum {
IMAGE = 'IMAGE',
VIDEO = 'VIDEO',
@ -22,4 +24,6 @@ export type FileEntity = {
progress: number
url?: string
base64Url?: string
type: TransferMethod
fileType: string
}

View File

@ -1,5 +1,6 @@
import { FileAppearanceTypeEnum } from './types'
import { upload } from '@/service/base'
import { FILE_EXTS } from '@/app/components/base/prompt-editor/constants'
type FileUploadParams = {
file: File
@ -72,3 +73,13 @@ export const getFileExtension = (file?: File) => {
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 ''
}