mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-08 06:19:05 +08:00
feat(translate): 添加拖拽上传文件功能
实现文件拖拽上传功能,包括拖拽区域高亮显示和提示文本 添加多文件上传错误提示和未知错误处理
This commit is contained in:
parent
25827cdf8d
commit
a6c4d1a68a
@ -3767,7 +3767,9 @@
|
|||||||
"label": "交换源语言与目标语言"
|
"label": "交换源语言与目标语言"
|
||||||
},
|
},
|
||||||
"files": {
|
"files": {
|
||||||
|
"drag_text": "拖放到此处",
|
||||||
"error": {
|
"error": {
|
||||||
|
"multiple": "不允许上传多个文件",
|
||||||
"too_large": "文件过大",
|
"too_large": "文件过大",
|
||||||
"unknown": "读取文件内容失败"
|
"unknown": "读取文件内容失败"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import { LanguagesEnum, UNKNOWN } from '@renderer/config/translate'
|
|||||||
import { useCodeStyle } from '@renderer/context/CodeStyleProvider'
|
import { useCodeStyle } from '@renderer/context/CodeStyleProvider'
|
||||||
import db from '@renderer/databases'
|
import db from '@renderer/databases'
|
||||||
import { useDefaultModel } from '@renderer/hooks/useAssistant'
|
import { useDefaultModel } from '@renderer/hooks/useAssistant'
|
||||||
|
import { useDrag } from '@renderer/hooks/useDrag'
|
||||||
import { useFiles } from '@renderer/hooks/useFiles'
|
import { useFiles } from '@renderer/hooks/useFiles'
|
||||||
import { useOcr } from '@renderer/hooks/useOcr'
|
import { useOcr } from '@renderer/hooks/useOcr'
|
||||||
import { useTemporaryValue } from '@renderer/hooks/useTemporaryValue'
|
import { useTemporaryValue } from '@renderer/hooks/useTemporaryValue'
|
||||||
@ -20,6 +21,7 @@ import { setTranslating as setTranslatingAction } from '@renderer/store/runtime'
|
|||||||
import { setTranslatedContent as setTranslatedContentAction } from '@renderer/store/translate'
|
import { setTranslatedContent as setTranslatedContentAction } from '@renderer/store/translate'
|
||||||
import {
|
import {
|
||||||
type AutoDetectionMethod,
|
type AutoDetectionMethod,
|
||||||
|
FileMetadata,
|
||||||
isSupportedOcrFile,
|
isSupportedOcrFile,
|
||||||
type Model,
|
type Model,
|
||||||
type TranslateHistory,
|
type TranslateHistory,
|
||||||
@ -27,6 +29,7 @@ import {
|
|||||||
} from '@renderer/types'
|
} from '@renderer/types'
|
||||||
import { runAsyncFunction } from '@renderer/utils'
|
import { runAsyncFunction } from '@renderer/utils'
|
||||||
import { formatErrorMessage } from '@renderer/utils/error'
|
import { formatErrorMessage } from '@renderer/utils/error'
|
||||||
|
import { getFilesFromDropEvent, getTextFromDropEvent } from '@renderer/utils/input'
|
||||||
import {
|
import {
|
||||||
createInputScrollHandler,
|
createInputScrollHandler,
|
||||||
createOutputScrollHandler,
|
createOutputScrollHandler,
|
||||||
@ -37,7 +40,7 @@ import { imageExts, MB, textExts } from '@shared/config/constant'
|
|||||||
import { Button, Flex, FloatButton, Popover, Tooltip, Typography } from 'antd'
|
import { Button, Flex, FloatButton, Popover, Tooltip, Typography } from 'antd'
|
||||||
import TextArea, { TextAreaRef } from 'antd/es/input/TextArea'
|
import TextArea, { TextAreaRef } from 'antd/es/input/TextArea'
|
||||||
import { isEmpty, throttle } from 'lodash'
|
import { isEmpty, throttle } from 'lodash'
|
||||||
import { Check, FolderClock, Settings2 } from 'lucide-react'
|
import { Check, FolderClock, Settings2, UploadIcon } from 'lucide-react'
|
||||||
import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
@ -434,16 +437,9 @@ const TranslatePage: FC = () => {
|
|||||||
// 控制token估计
|
// 控制token估计
|
||||||
const tokenCount = useMemo(() => estimateTextTokens(text + prompt), [prompt, text])
|
const tokenCount = useMemo(() => estimateTextTokens(text + prompt), [prompt, text])
|
||||||
|
|
||||||
// 控制文件ocr
|
// 统一的文件处理
|
||||||
const handleSelectFile = useCallback(async () => {
|
const processFile = useCallback(
|
||||||
if (selecting) return
|
async (file: FileMetadata) => {
|
||||||
setIsProcessing(true)
|
|
||||||
try {
|
|
||||||
const [file] = await onSelectFile({ multipleSelections: false })
|
|
||||||
if (!file) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// extensible
|
// extensible
|
||||||
const shouldOCR = isSupportedOcrFile(file)
|
const shouldOCR = isSupportedOcrFile(file)
|
||||||
|
|
||||||
@ -471,6 +467,21 @@ const TranslatePage: FC = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
[ocr, t]
|
||||||
|
)
|
||||||
|
|
||||||
|
// 点击上传文件按钮
|
||||||
|
const handleSelectFile = useCallback(async () => {
|
||||||
|
if (selecting) return
|
||||||
|
setIsProcessing(true)
|
||||||
|
try {
|
||||||
|
const [file] = await onSelectFile({ multipleSelections: false })
|
||||||
|
if (!file) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return await processFile(file)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('Unknown error when selecting file.', e as Error)
|
logger.error('Unknown error when selecting file.', e as Error)
|
||||||
window.message.error(t('translate.files.error.unknown') + ': ' + formatErrorMessage(e))
|
window.message.error(t('translate.files.error.unknown') + ': ' + formatErrorMessage(e))
|
||||||
@ -478,10 +489,62 @@ const TranslatePage: FC = () => {
|
|||||||
clearFiles()
|
clearFiles()
|
||||||
setIsProcessing(false)
|
setIsProcessing(false)
|
||||||
}
|
}
|
||||||
}, [clearFiles, ocr, onSelectFile, selecting, t])
|
}, [clearFiles, onSelectFile, processFile, selecting, t])
|
||||||
|
|
||||||
|
// 拖动上传文件
|
||||||
|
const onDrop = useCallback(
|
||||||
|
async (e: React.DragEvent<HTMLDivElement>) => {
|
||||||
|
setIsProcessing(true)
|
||||||
|
// const supportedFiles = await filterSupportedFiles(_files, extensions)
|
||||||
|
const data = await getTextFromDropEvent(e).catch((err) => {
|
||||||
|
logger.error('getTextFromDropEvent', err)
|
||||||
|
window.message.error({
|
||||||
|
key: 'file_error',
|
||||||
|
content: t('translate.files.error.unknown')
|
||||||
|
})
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
if (data === null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setText(text + data)
|
||||||
|
|
||||||
|
const droppedFiles = await getFilesFromDropEvent(e).catch((err) => {
|
||||||
|
logger.error('handleDrop:', err)
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
|
||||||
|
if (droppedFiles) {
|
||||||
|
if (droppedFiles.length === 0) return
|
||||||
|
if (droppedFiles.length > 1) {
|
||||||
|
// 多文件上传时显示提示信息
|
||||||
|
window.message.error({
|
||||||
|
key: 'multiple_files',
|
||||||
|
content: t('translate.files.error.multiple')
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const file = droppedFiles[0]
|
||||||
|
processFile(file)
|
||||||
|
} else if (droppedFiles === null) {
|
||||||
|
window.message.error({
|
||||||
|
key: 'file_error',
|
||||||
|
content: t('translate.files.error.unknown')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
setIsProcessing(false)
|
||||||
|
},
|
||||||
|
[processFile, t, text]
|
||||||
|
)
|
||||||
|
|
||||||
|
const { isDragging, handleDragEnter, handleDragLeave, handleDragOver, handleDrop } = useDrag<HTMLDivElement>(onDrop)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container id="translate-page">
|
<Container
|
||||||
|
id="translate-page"
|
||||||
|
onDragEnter={handleDragEnter}
|
||||||
|
onDragLeave={handleDragLeave}
|
||||||
|
onDragOver={handleDragOver}>
|
||||||
<Navbar>
|
<Navbar>
|
||||||
<NavbarCenter style={{ borderRight: 'none', gap: 10 }}>{t('translate.title')}</NavbarCenter>
|
<NavbarCenter style={{ borderRight: 'none', gap: 10 }}>{t('translate.title')}</NavbarCenter>
|
||||||
</Navbar>
|
</Navbar>
|
||||||
@ -543,7 +606,15 @@ const TranslatePage: FC = () => {
|
|||||||
</InnerOperationBar>
|
</InnerOperationBar>
|
||||||
</OperationBar>
|
</OperationBar>
|
||||||
<AreaContainer>
|
<AreaContainer>
|
||||||
<InputContainer>
|
<InputContainer
|
||||||
|
style={isDragging ? { border: '2px dashed var(--color-primary)' } : undefined}
|
||||||
|
onDrop={handleDrop}>
|
||||||
|
{isDragging && (
|
||||||
|
<InputContainerDraggingHintContainer>
|
||||||
|
<UploadIcon color="var(--color-text-3)" />
|
||||||
|
{t('translate.files.drag_text')}
|
||||||
|
</InputContainerDraggingHintContainer>
|
||||||
|
)}
|
||||||
<FloatButton
|
<FloatButton
|
||||||
style={{ position: 'absolute', left: 8, bottom: 8 }}
|
style={{ position: 'absolute', left: 8, bottom: 8 }}
|
||||||
className="float-button"
|
className="float-button"
|
||||||
@ -663,6 +734,19 @@ const InputContainer = styled.div`
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
const InputContainerDraggingHintContainer = styled.div`
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: var(--color-text-3);
|
||||||
|
`
|
||||||
|
|
||||||
const Textarea = styled(TextArea)`
|
const Textarea = styled(TextArea)`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user