新增: 添加支持在对话框粘贴图片作为附件功能

This commit is contained in:
2026-07-07 12:21:51 +08:00
parent 981312520d
commit aca6cfda9a
@@ -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<AttachmentPickerTab>('canvas');
const [attachmentError, setAttachmentError] = useState<string | null>(null);
const [isPastingAttachment, setIsPastingAttachment] = useState(false);
const [draftAttachmentKeys, setDraftAttachmentKeys] = useState<Set<string>>(
() => 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<EditorAgentAttachmentRef> => {
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<HTMLTextAreaElement>) => {
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}
</div>
) : null}
{attachmentError ? (
<div className="mx-3 mb-2 rounded-2xl bg-amber-50 px-3 py-2 text-sm text-amber-700">
{attachmentError}
</div>
) : null}
<form
className="border-t border-slate-200 bg-white/95 p-3"
onSubmit={submitMessage}
@@ -685,6 +806,7 @@ export function EditorAgentConversationPanelView({
value={draftText}
rows={1}
onChange={(event) => setDraftText(event.currentTarget.value)}
onPaste={handleInputPaste}
onKeyDown={(event) => {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();