From 3908080d21440d6d93a82054f4e5bafa30a64007 Mon Sep 17 00:00:00 2001 From: beyondkmp Date: Tue, 20 May 2025 13:09:37 +0800 Subject: [PATCH] refactor: centralize paste handling logic with PasteService (#6199) - Integrated PasteService for handling paste events in Inputbar and MessageEditor components. - Removed redundant paste handling code and improved maintainability. - Registered paste handlers and set last focused component for better user experience. - Ensured consistent behavior for text and file pasting across components. Co-authored-by: beyondkmp --- .../src/pages/home/Inputbar/Inputbar.tsx | 96 +++----- .../src/pages/home/Messages/MessageEditor.tsx | 101 ++++---- src/renderer/src/services/PasteService.ts | 215 ++++++++++++++++++ 3 files changed, 284 insertions(+), 128 deletions(-) create mode 100644 src/renderer/src/services/PasteService.ts diff --git a/src/renderer/src/pages/home/Inputbar/Inputbar.tsx b/src/renderer/src/pages/home/Inputbar/Inputbar.tsx index f68f5fbbbf..22d972806d 100644 --- a/src/renderer/src/pages/home/Inputbar/Inputbar.tsx +++ b/src/renderer/src/pages/home/Inputbar/Inputbar.tsx @@ -23,6 +23,7 @@ import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' import FileManager from '@renderer/services/FileManager' import { checkRateLimit, getUserMessage } from '@renderer/services/MessagesService' import { getModelUniqId } from '@renderer/services/ModelService' +import PasteService from '@renderer/services/PasteService' import { estimateTextTokens as estimateTxtTokens, estimateUserPromptUsage } from '@renderer/services/TokenService' import { translateText } from '@renderer/services/TranslateService' import WebSearchService from '@renderer/services/WebSearchService' @@ -581,72 +582,19 @@ const Inputbar: FC = ({ assistant: _assistant, setActiveTopic, topic }) = const onPaste = useCallback( async (event: ClipboardEvent) => { - // 优先处理文本粘贴 - const clipboardText = event.clipboardData?.getData('text') - if (clipboardText) { - // 1. 文本粘贴 - if (pasteLongTextAsFile && clipboardText.length > pasteLongTextThreshold) { - // 长文本直接转文件,阻止默认粘贴 - event.preventDefault() - - const tempFilePath = await window.api.file.create('pasted_text.txt') - await window.api.file.write(tempFilePath, clipboardText) - const selectedFile = await window.api.file.get(tempFilePath) - selectedFile && setFiles((prevFiles) => [...prevFiles, selectedFile]) - setText(text) // 保持输入框内容不变 - setTimeout(() => resizeTextArea(), 50) - return - } - // 短文本走默认粘贴行为,直接返回 - return - } - - // 2. 文件/图片粘贴(仅在无文本时处理) - if (event.clipboardData?.files && event.clipboardData.files.length > 0) { - event.preventDefault() - for (const file of event.clipboardData.files) { - try { - // 使用新的API获取文件路径 - const filePath = window.api.file.getPathForFile(file) - - // 如果没有路径,可能是剪贴板中的图像数据 - if (!filePath) { - // 图像生成也支持图像编辑 - if (file.type.startsWith('image/') && (isVisionModel(model) || isGenerateImageModel(model))) { - const tempFilePath = await window.api.file.create(file.name) - const arrayBuffer = await file.arrayBuffer() - const uint8Array = new Uint8Array(arrayBuffer) - await window.api.file.write(tempFilePath, uint8Array) - const selectedFile = await window.api.file.get(tempFilePath) - selectedFile && setFiles((prevFiles) => [...prevFiles, selectedFile]) - break - } else { - window.message.info({ - key: 'file_not_supported', - content: t('chat.input.file_not_supported') - }) - } - continue - } - - // 有路径的情况 - if (supportExts.includes(getFileExtension(filePath))) { - const selectedFile = await window.api.file.get(filePath) - selectedFile && setFiles((prevFiles) => [...prevFiles, selectedFile]) - } else { - window.message.info({ - key: 'file_not_supported', - content: t('chat.input.file_not_supported') - }) - } - } catch (error) { - Logger.error('[src/renderer/src/pages/home/Inputbar/Inputbar.tsx] onPaste:', error) - window.message.error(t('chat.input.file_error')) - } - } - return - } - // 其他情况默认粘贴 + return await PasteService.handlePaste( + event, + isVisionModel(model), + isGenerateImageModel(model), + supportExts, + setFiles, + setText, + pasteLongTextAsFile, + pasteLongTextThreshold, + text, + resizeTextArea, + t + ) }, [model, pasteLongTextAsFile, pasteLongTextThreshold, resizeTextArea, supportExts, t, text] ) @@ -749,6 +697,20 @@ const Inputbar: FC = ({ assistant: _assistant, setActiveTopic, topic }) = } }, [isDragging, handleDrag, handleDragEnd]) + // 注册粘贴处理函数并初始化全局监听 + useEffect(() => { + // 确保全局paste监听器仅初始化一次 + PasteService.init() + + // 注册当前组件的粘贴处理函数 + PasteService.registerHandler('inputbar', onPaste) + + // 卸载时取消注册 + return () => { + PasteService.unregisterHandler('inputbar') + } + }, [onPaste]) + useShortcut('new_topic', () => { addNewTopic() EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR) @@ -951,6 +913,8 @@ const Inputbar: FC = ({ assistant: _assistant, setActiveTopic, topic }) = styles={{ textarea: TextareaStyle }} onFocus={(e: React.FocusEvent) => { setInputFocus(true) + // 记录当前聚焦的组件 + PasteService.setLastFocusedComponent('inputbar') if (e.target.value.length === 0) { e.target.setSelectionRange(0, 0) } diff --git a/src/renderer/src/pages/home/Messages/MessageEditor.tsx b/src/renderer/src/pages/home/Messages/MessageEditor.tsx index 8b4cd32685..6b82ec625d 100644 --- a/src/renderer/src/pages/home/Messages/MessageEditor.tsx +++ b/src/renderer/src/pages/home/Messages/MessageEditor.tsx @@ -4,6 +4,7 @@ import { isGenerateImageModel, isVisionModel } from '@renderer/config/models' import { useAssistant } from '@renderer/hooks/useAssistant' import { useSettings } from '@renderer/hooks/useSettings' import FileManager from '@renderer/services/FileManager' +import PasteService from '@renderer/services/PasteService' import { FileType, FileTypes } from '@renderer/types' import { Message, MessageBlock, MessageBlockStatus, MessageBlockType } from '@renderer/types/newMessage' import { classNames, getFileExtension } from '@renderer/utils' @@ -62,6 +63,39 @@ const MessageBlockEditor: FC = ({ message, onSave, onResend, onCancel }) } }, []) + const onPaste = useCallback( + async (event: ClipboardEvent) => { + return await PasteService.handlePaste( + event, + isVisionModel(model), + isGenerateImageModel(model), + supportExts, + setFiles, + undefined, // 不需要setText + pasteLongTextAsFile, + pasteLongTextThreshold, + undefined, // 不需要text + resizeTextArea, + t + ) + }, + [model, pasteLongTextAsFile, pasteLongTextThreshold, resizeTextArea, supportExts, t] + ) + + // 添加全局粘贴事件处理 + useEffect(() => { + // 注册当前组件的粘贴处理函数 + PasteService.registerHandler('messageEditor', onPaste) + + // 在组件加载时将焦点设置为当前组件 + PasteService.setLastFocusedComponent('messageEditor') + + // 卸载时取消注册 + return () => { + PasteService.unregisterHandler('messageEditor') + } + }, [onPaste]) + const handleTextChange = (blockId: string, content: string) => { setEditedBlocks((prev) => prev.map((block) => (block.id === blockId ? { ...block, content } : block))) } @@ -131,67 +165,6 @@ const MessageBlockEditor: FC = ({ message, onSave, onResend, onCancel }) } } - const onPaste = useCallback( - async (event: ClipboardEvent) => { - // 1. 文本粘贴 - const clipboardText = event.clipboardData?.getData('text') - if (clipboardText) { - if (pasteLongTextAsFile && clipboardText.length > pasteLongTextThreshold) { - // 长文本直接转文件,阻止默认粘贴 - event.preventDefault() - - const tempFilePath = await window.api.file.create('pasted_text.txt') - await window.api.file.write(tempFilePath, clipboardText) - const selectedFile = await window.api.file.get(tempFilePath) - selectedFile && setFiles((prevFiles) => [...prevFiles, selectedFile]) - setTimeout(() => resizeTextArea(), 50) - return - } - // 短文本走默认粘贴行为,直接返回 - return - } - - // 2. 文件/图片粘贴 - if (event.clipboardData?.files && event.clipboardData.files.length > 0) { - event.preventDefault() - for (const file of event.clipboardData.files) { - const filePath = window.api.file.getPathForFile(file) - if (!filePath) { - // 图像生成也支持图像编辑 - if (file.type.startsWith('image/') && (isVisionModel(model) || isGenerateImageModel(model))) { - const tempFilePath = await window.api.file.create(file.name) - const arrayBuffer = await file.arrayBuffer() - const uint8Array = new Uint8Array(arrayBuffer) - await window.api.file.write(tempFilePath, uint8Array) - const selectedFile = await window.api.file.get(tempFilePath) - selectedFile && setFiles((prevFiles) => [...prevFiles, selectedFile]) - break - } else { - window.message.info({ - key: 'file_not_supported', - content: t('chat.input.file_not_supported') - }) - } - } - - if (supportExts.includes(getFileExtension(filePath))) { - const selectedFile = await window.api.file.get(filePath) - selectedFile && setFiles((prevFiles) => [...prevFiles, selectedFile]) - } else { - window.message.info({ - key: 'file_not_supported', - content: t('chat.input.file_not_supported') - }) - } - } - return - } - - // 短文本走默认粘贴行为 - }, - [model, pasteLongTextAsFile, pasteLongTextThreshold, resizeTextArea, supportExts, t] - ) - const autoResizeTextArea = (e: React.ChangeEvent) => { const textarea = e.target textarea.style.height = 'auto' @@ -205,7 +178,7 @@ const MessageBlockEditor: FC = ({ message, onSave, onResend, onCancel }) .filter((block) => block.type === MessageBlockType.MAIN_TEXT) .map((block) => (