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();