新增: 添加支持在对话框粘贴图片作为附件功能
This commit is contained in:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user