mirror of
				https://github.com/langgenius/dify.git
				synced 2025-11-04 12:53:38 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			271 lines
		
	
	
		
			8.6 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			271 lines
		
	
	
		
			8.6 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
import { useCallback, useMemo, useRef, useState } from 'react'
 | 
						|
import type { ClipboardEvent } from 'react'
 | 
						|
import { useParams } from 'next/navigation'
 | 
						|
import { useTranslation } from 'react-i18next'
 | 
						|
import { imageUpload } from './utils'
 | 
						|
import { useToastContext } from '@/app/components/base/toast'
 | 
						|
import { ALLOW_FILE_EXTENSIONS, TransferMethod } from '@/types/app'
 | 
						|
import type { ImageFile, VisionSettings } from '@/types/app'
 | 
						|
 | 
						|
export const useImageFiles = () => {
 | 
						|
  const params = useParams()
 | 
						|
  const { t } = useTranslation()
 | 
						|
  const { notify } = useToastContext()
 | 
						|
  const [files, setFiles] = useState<ImageFile[]>([])
 | 
						|
  const filesRef = useRef<ImageFile[]>([])
 | 
						|
 | 
						|
  const handleUpload = (imageFile: ImageFile) => {
 | 
						|
    const files = filesRef.current
 | 
						|
    const index = files.findIndex(file => file._id === imageFile._id)
 | 
						|
 | 
						|
    if (index > -1) {
 | 
						|
      const currentFile = files[index]
 | 
						|
      const newFiles = [...files.slice(0, index), { ...currentFile, ...imageFile }, ...files.slice(index + 1)]
 | 
						|
      setFiles(newFiles)
 | 
						|
      filesRef.current = newFiles
 | 
						|
    }
 | 
						|
    else {
 | 
						|
      const newFiles = [...files, imageFile]
 | 
						|
      setFiles(newFiles)
 | 
						|
      filesRef.current = newFiles
 | 
						|
    }
 | 
						|
  }
 | 
						|
  const handleRemove = (imageFileId: string) => {
 | 
						|
    const files = filesRef.current
 | 
						|
    const index = files.findIndex(file => file._id === imageFileId)
 | 
						|
 | 
						|
    if (index > -1) {
 | 
						|
      const currentFile = files[index]
 | 
						|
      const newFiles = [...files.slice(0, index), { ...currentFile, deleted: true }, ...files.slice(index + 1)]
 | 
						|
      setFiles(newFiles)
 | 
						|
      filesRef.current = newFiles
 | 
						|
    }
 | 
						|
  }
 | 
						|
  const handleImageLinkLoadError = (imageFileId: string) => {
 | 
						|
    const files = filesRef.current
 | 
						|
    const index = files.findIndex(file => file._id === imageFileId)
 | 
						|
 | 
						|
    if (index > -1) {
 | 
						|
      const currentFile = files[index]
 | 
						|
      const newFiles = [...files.slice(0, index), { ...currentFile, progress: -1 }, ...files.slice(index + 1)]
 | 
						|
      filesRef.current = newFiles
 | 
						|
      setFiles(newFiles)
 | 
						|
    }
 | 
						|
  }
 | 
						|
  const handleImageLinkLoadSuccess = (imageFileId: string) => {
 | 
						|
    const files = filesRef.current
 | 
						|
    const index = files.findIndex(file => file._id === imageFileId)
 | 
						|
 | 
						|
    if (index > -1) {
 | 
						|
      const currentImageFile = files[index]
 | 
						|
      const newFiles = [...files.slice(0, index), { ...currentImageFile, progress: 100 }, ...files.slice(index + 1)]
 | 
						|
      filesRef.current = newFiles
 | 
						|
      setFiles(newFiles)
 | 
						|
    }
 | 
						|
  }
 | 
						|
  const handleReUpload = (imageFileId: string) => {
 | 
						|
    const files = filesRef.current
 | 
						|
    const index = files.findIndex(file => file._id === imageFileId)
 | 
						|
 | 
						|
    if (index > -1) {
 | 
						|
      const currentImageFile = files[index]
 | 
						|
      imageUpload({
 | 
						|
        file: currentImageFile.file!,
 | 
						|
        onProgressCallback: (progress) => {
 | 
						|
          const newFiles = [...files.slice(0, index), { ...currentImageFile, progress }, ...files.slice(index + 1)]
 | 
						|
          filesRef.current = newFiles
 | 
						|
          setFiles(newFiles)
 | 
						|
        },
 | 
						|
        onSuccessCallback: (res) => {
 | 
						|
          const newFiles = [...files.slice(0, index), { ...currentImageFile, fileId: res.id, progress: 100 }, ...files.slice(index + 1)]
 | 
						|
          filesRef.current = newFiles
 | 
						|
          setFiles(newFiles)
 | 
						|
        },
 | 
						|
        onErrorCallback: () => {
 | 
						|
          notify({ type: 'error', message: t('common.imageUploader.uploadFromComputerUploadError') })
 | 
						|
          const newFiles = [...files.slice(0, index), { ...currentImageFile, progress: -1 }, ...files.slice(index + 1)]
 | 
						|
          filesRef.current = newFiles
 | 
						|
          setFiles(newFiles)
 | 
						|
        },
 | 
						|
      }, !!params.token)
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  const handleClear = () => {
 | 
						|
    setFiles([])
 | 
						|
    filesRef.current = []
 | 
						|
  }
 | 
						|
 | 
						|
  const filteredFiles = useMemo(() => {
 | 
						|
    return files.filter(file => !file.deleted)
 | 
						|
  }, [files])
 | 
						|
 | 
						|
  return {
 | 
						|
    files: filteredFiles,
 | 
						|
    onUpload: handleUpload,
 | 
						|
    onRemove: handleRemove,
 | 
						|
    onImageLinkLoadError: handleImageLinkLoadError,
 | 
						|
    onImageLinkLoadSuccess: handleImageLinkLoadSuccess,
 | 
						|
    onReUpload: handleReUpload,
 | 
						|
    onClear: handleClear,
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
type useLocalUploaderProps = {
 | 
						|
  disabled?: boolean
 | 
						|
  limit?: number
 | 
						|
  onUpload: (imageFile: ImageFile) => void
 | 
						|
}
 | 
						|
 | 
						|
export const useLocalFileUploader = ({ limit, disabled = false, onUpload }: useLocalUploaderProps) => {
 | 
						|
  const { notify } = useToastContext()
 | 
						|
  const params = useParams()
 | 
						|
  const { t } = useTranslation()
 | 
						|
 | 
						|
  const handleLocalFileUpload = useCallback((file: File) => {
 | 
						|
    if (disabled) {
 | 
						|
      // TODO: leave some warnings?
 | 
						|
      return
 | 
						|
    }
 | 
						|
 | 
						|
    if (!ALLOW_FILE_EXTENSIONS.includes(file.type.split('/')[1]))
 | 
						|
      return
 | 
						|
 | 
						|
    if (limit && file.size > limit * 1024 * 1024) {
 | 
						|
      notify({ type: 'error', message: t('common.imageUploader.uploadFromComputerLimit', { size: limit }) })
 | 
						|
      return
 | 
						|
    }
 | 
						|
 | 
						|
    const reader = new FileReader()
 | 
						|
    reader.addEventListener(
 | 
						|
      'load',
 | 
						|
      () => {
 | 
						|
        const imageFile = {
 | 
						|
          type: TransferMethod.local_file,
 | 
						|
          _id: `${Date.now()}`,
 | 
						|
          fileId: '',
 | 
						|
          file,
 | 
						|
          url: reader.result as string,
 | 
						|
          base64Url: reader.result as string,
 | 
						|
          progress: 0,
 | 
						|
        }
 | 
						|
        onUpload(imageFile)
 | 
						|
        imageUpload({
 | 
						|
          file: imageFile.file,
 | 
						|
          onProgressCallback: (progress) => {
 | 
						|
            onUpload({ ...imageFile, progress })
 | 
						|
          },
 | 
						|
          onSuccessCallback: (res) => {
 | 
						|
            onUpload({ ...imageFile, fileId: res.id, progress: 100 })
 | 
						|
          },
 | 
						|
          onErrorCallback: () => {
 | 
						|
            notify({ type: 'error', message: t('common.imageUploader.uploadFromComputerUploadError') })
 | 
						|
            onUpload({ ...imageFile, progress: -1 })
 | 
						|
          },
 | 
						|
        }, !!params.token)
 | 
						|
      },
 | 
						|
      false,
 | 
						|
    )
 | 
						|
    reader.addEventListener(
 | 
						|
      'error',
 | 
						|
      () => {
 | 
						|
        notify({ type: 'error', message: t('common.imageUploader.uploadFromComputerReadError') })
 | 
						|
      },
 | 
						|
      false,
 | 
						|
    )
 | 
						|
    reader.readAsDataURL(file)
 | 
						|
  }, [disabled, limit, notify, t, onUpload, params.token])
 | 
						|
 | 
						|
  return { disabled, handleLocalFileUpload }
 | 
						|
}
 | 
						|
 | 
						|
type useClipboardUploaderProps = {
 | 
						|
  files: ImageFile[]
 | 
						|
  visionConfig?: VisionSettings
 | 
						|
  onUpload: (imageFile: ImageFile) => void
 | 
						|
}
 | 
						|
 | 
						|
export const useClipboardUploader = ({ visionConfig, onUpload, files }: useClipboardUploaderProps) => {
 | 
						|
  const allowLocalUpload = visionConfig?.transfer_methods?.includes(TransferMethod.local_file)
 | 
						|
  const disabled = useMemo(() =>
 | 
						|
    !visionConfig
 | 
						|
    || !visionConfig?.enabled
 | 
						|
    || !allowLocalUpload
 | 
						|
    || files.length >= visionConfig.number_limits!,
 | 
						|
  [allowLocalUpload, files.length, visionConfig])
 | 
						|
  const limit = useMemo(() => visionConfig ? +visionConfig.image_file_size_limit! : 0, [visionConfig])
 | 
						|
  const { handleLocalFileUpload } = useLocalFileUploader({ limit, onUpload, disabled })
 | 
						|
 | 
						|
  const handleClipboardPaste = useCallback((e: ClipboardEvent<HTMLTextAreaElement>) => {
 | 
						|
    // reserve native text copy behavior
 | 
						|
    const file = e.clipboardData?.files[0]
 | 
						|
    // when copied file, prevent default action
 | 
						|
    if (file) {
 | 
						|
      e.preventDefault()
 | 
						|
      handleLocalFileUpload(file)
 | 
						|
    }
 | 
						|
  }, [handleLocalFileUpload])
 | 
						|
 | 
						|
  return {
 | 
						|
    onPaste: handleClipboardPaste,
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
type useDraggableUploaderProps = {
 | 
						|
  files: ImageFile[]
 | 
						|
  visionConfig?: VisionSettings
 | 
						|
  onUpload: (imageFile: ImageFile) => void
 | 
						|
}
 | 
						|
 | 
						|
export const useDraggableUploader = <T extends HTMLElement>({ visionConfig, onUpload, files }: useDraggableUploaderProps) => {
 | 
						|
  const allowLocalUpload = visionConfig?.transfer_methods?.includes(TransferMethod.local_file)
 | 
						|
  const disabled = useMemo(() =>
 | 
						|
    !visionConfig
 | 
						|
    || !visionConfig?.enabled
 | 
						|
    || !allowLocalUpload
 | 
						|
    || files.length >= visionConfig.number_limits!,
 | 
						|
  [allowLocalUpload, files.length, visionConfig])
 | 
						|
  const limit = useMemo(() => visionConfig ? +visionConfig.image_file_size_limit! : 0, [visionConfig])
 | 
						|
  const { handleLocalFileUpload } = useLocalFileUploader({ disabled, onUpload, limit })
 | 
						|
  const [isDragActive, setIsDragActive] = useState(false)
 | 
						|
 | 
						|
  const handleDragEnter = useCallback((e: React.DragEvent<T>) => {
 | 
						|
    e.preventDefault()
 | 
						|
    e.stopPropagation()
 | 
						|
    if (!disabled)
 | 
						|
      setIsDragActive(true)
 | 
						|
  }, [disabled])
 | 
						|
 | 
						|
  const handleDragOver = useCallback((e: React.DragEvent<T>) => {
 | 
						|
    e.preventDefault()
 | 
						|
    e.stopPropagation()
 | 
						|
  }, [])
 | 
						|
 | 
						|
  const handleDragLeave = useCallback((e: React.DragEvent<T>) => {
 | 
						|
    e.preventDefault()
 | 
						|
    e.stopPropagation()
 | 
						|
    setIsDragActive(false)
 | 
						|
  }, [])
 | 
						|
 | 
						|
  const handleDrop = useCallback((e: React.DragEvent<T>) => {
 | 
						|
    e.preventDefault()
 | 
						|
    e.stopPropagation()
 | 
						|
    setIsDragActive(false)
 | 
						|
 | 
						|
    const file = e.dataTransfer.files[0]
 | 
						|
 | 
						|
    if (!file)
 | 
						|
      return
 | 
						|
 | 
						|
    handleLocalFileUpload(file)
 | 
						|
  }, [handleLocalFileUpload])
 | 
						|
 | 
						|
  return {
 | 
						|
    onDragEnter: handleDragEnter,
 | 
						|
    onDragOver: handleDragOver,
 | 
						|
    onDragLeave: handleDragLeave,
 | 
						|
    onDrop: handleDrop,
 | 
						|
    isDragActive,
 | 
						|
  }
 | 
						|
}
 |