From aca6cfda9a6d1d623857b6aaa19daa101a23f724 Mon Sep 17 00:00:00 2001 From: kvtodev Date: Tue, 7 Jul 2026 12:21:51 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E:=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E5=9C=A8=E5=AF=B9=E8=AF=9D=E6=A1=86=E7=B2=98?= =?UTF-8?q?=E8=B4=B4=E5=9B=BE=E7=89=87=E4=BD=9C=E4=B8=BA=E9=99=84=E4=BB=B6?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../EditorAgentConversationPanelView.tsx | 122 ++++++++++++++++++ 1 file changed, 122 insertions(+) diff --git a/src/components/image-editor/EditorAgentConversationPanelView.tsx b/src/components/image-editor/EditorAgentConversationPanelView.tsx index bbe08ea64..665162fc8 100644 --- a/src/components/image-editor/EditorAgentConversationPanelView.tsx +++ b/src/components/image-editor/EditorAgentConversationPanelView.tsx @@ -11,6 +11,7 @@ import { X, } from 'lucide-react'; import { + type ClipboardEvent as ReactClipboardEvent, type FormEvent, type WheelEvent as ReactWheelEvent, useEffect, @@ -30,7 +31,11 @@ import { PlatformActionButton } from '../common/PlatformActionButton'; import { PlatformDangerConfirmDialog } from '../common/PlatformDangerConfirmDialog'; import { UnifiedModal } from '../common/UnifiedModal'; import { ResolvedAssetImage } from '../ResolvedAssetImage'; +import { uploadEditorMediaAssetFile } from '../../services/image-editor/editorMediaAssetUploadClient'; +import { createEditorProjectResource } from '../../services/image-editor/editorProjectClient'; import type { CanvasLayer, EditorAsset } from './ImageCanvasEditorTypes'; +import { probeImageFileDimensions } from './ImageCanvasFileModel'; +import { useImageCanvasContextStore } from './useImageCanvasContextStore.ts'; import { type EditorAgentConversationClient, useEditorAgentConversation, @@ -449,6 +454,7 @@ export function EditorAgentConversationPanelView({ const [attachmentPickerTab, setAttachmentPickerTab] = useState('canvas'); const [attachmentError, setAttachmentError] = useState(null); + const [isPastingAttachment, setIsPastingAttachment] = useState(false); const [draftAttachmentKeys, setDraftAttachmentKeys] = useState>( () => new Set(), ); @@ -511,6 +517,9 @@ export function EditorAgentConversationPanelView({ stopCurrentTurn(); return; } + if (isPastingAttachment) { + return; + } const text = draftText.trim(); if (!text && !attachments.length) { return; @@ -525,6 +534,113 @@ export function EditorAgentConversationPanelView({ ); }); }; + const appendAttachments = (nextAttachments: EditorAgentAttachmentRef[]) => { + function mergeAttachments( + currentAttachments: EditorAgentAttachmentRef[], + nextAttachments: EditorAgentAttachmentRef[], + ) { + const merged = [...currentAttachments]; + const existingKeys = new Set(currentAttachments.map(attachmentKey)); + nextAttachments.forEach((attachment) => { + const key = attachmentKey(attachment); + if (!existingKeys.has(key)) { + existingKeys.add(key); + merged.push(attachment); + } + }); + return merged; + } + const mergedAttachments = mergeAttachments(attachments, nextAttachments); + if (mergedAttachments.length > EDITOR_AGENT_MAX_ATTACHMENTS) { + setAttachmentError(`最多 ${EDITOR_AGENT_MAX_ATTACHMENTS} 张`); + return false; + } + setAttachments(mergedAttachments); + setAttachmentError(null); + return true; + }; + + const createPastedAgentImageAttachment = async ( + file: File, + ): Promise => { + if (!projectId?.trim()) { + throw new Error('缺少画布项目'); + } + const [upload, dimensions] = await Promise.all([ + uploadEditorMediaAssetFile(file, 'image', { + pathSegments: ['editor', 'agent-paste', 'image', `${Date.now()}`], + entityId: projectId, + metadata: { + source: 'agent-input-paste', + }, + }), + probeImageFileDimensions(file), + ]); + const width = dimensions?.width ?? 1; + const height = dimensions?.height ?? 1; + const resource = await createEditorProjectResource(projectId, { + imageSrc: upload.src, + objectKey: upload.objectKey, + assetObjectId: upload.assetObjectId, + width, + height, + sourceType: 'uploaded', + }); + return { + source: 'canvas_resource', + referenceId: resource.resourceId, + objectKey: resource.objectKey ?? upload.objectKey, + imageSrc: resource.imageSrc, + thumbnailSrc: null, + label: resource.label ?? '粘贴图片', + width: resource.width, + height: resource.height, + }; + }; + function extractClipboardImageFiles( + clipboardData: DataTransfer | null, + ): File[] { + if (!clipboardData) { + return []; + } + const fileItems = Array.from(clipboardData.files ?? []).filter((file) => + file.type.startsWith('image/'), + ); + return [...fileItems]; + } + + const handleInputPaste = (event: ReactClipboardEvent) => { + const imageFiles = extractClipboardImageFiles(event.clipboardData); + if (!imageFiles.length) { + return; + } + + if (isPastingAttachment) { + return; + } + + event.preventDefault(); + if (attachments.length >= EDITOR_AGENT_MAX_ATTACHMENTS) { + setAttachmentError(`最多 ${EDITOR_AGENT_MAX_ATTACHMENTS} 张`); + return; + } + + // TODO: deduplicate those existing assets + setIsPastingAttachment(true); + setAttachmentError('图片上传中'); + void Promise.all( + imageFiles.map((file) => createPastedAgentImageAttachment(file)), + ) + .then((pastedAttachments) => { + appendAttachments(pastedAttachments); + }) + .catch(() => { + setAttachmentError('图片粘贴失败,请重试'); + }) + .finally(() => { + setIsPastingAttachment(false); + }); + }; const removeAttachment = (targetAttachment: EditorAgentAttachmentRef) => { const key = attachmentKey(targetAttachment); @@ -655,6 +771,11 @@ export function EditorAgentConversationPanelView({ {errorMessage} ) : null} + {attachmentError ? ( +
+ {attachmentError} +
+ ) : null}
setDraftText(event.currentTarget.value)} + onPaste={handleInputPaste} onKeyDown={(event) => { if (event.key === 'Enter' && !event.shiftKey) { event.preventDefault();