增加复制粘贴交互
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useRef, useState, type KeyboardEvent, type ReactNode, type RefObject } from "react";
|
||||
import { useEffect, useMemo, useRef, useState, type ClipboardEvent, type KeyboardEvent, type ReactNode, type RefObject } from "react";
|
||||
import { Check, CircleDollarSign, Download, Film, ImageIcon, ImagePlus, Info, Layers3, Loader2, Music, Pencil, Plus, RefreshCw, Save, ScanLine, Send, Upload, WandSparkles, X } from "lucide-react";
|
||||
import clsx from "clsx";
|
||||
import { clampPage, pageItems, Pagination } from "@/components/pagination";
|
||||
@@ -32,6 +32,7 @@ import { extractMaterialPlaceholders } from "@/lib/prompt/material-placeholders"
|
||||
import { formatBillingAmount } from "@/lib/billing";
|
||||
import { assetContentUrl, assetPreviewUrl, materialPreviewUrl, storedAssetPreviewUrl } from "@/lib/client/asset-urls";
|
||||
import { createSeedreamMarkedFile } from "@/lib/client/seedream-markup";
|
||||
import { clipboardImageFiles, clipboardImageURL, insertClipboardImageReferences } from "@/lib/client/clipboard-images";
|
||||
import {
|
||||
buildSeedreamInteractivePrompt,
|
||||
buildSeedreamLayerPrompt,
|
||||
@@ -50,6 +51,21 @@ type MaterialKind = PromptMaterial["type"];
|
||||
type ImageGenerateEngine = "jimeng" | "evolink" | "bailian" | "seedream";
|
||||
type VideoGenerateEngine = "seedance" | "bailian" | "minimax";
|
||||
|
||||
type MaterialUploadOptions = {
|
||||
targetLabel?: string;
|
||||
expectedType?: MaterialKind;
|
||||
seedreamSourceMode?: "single" | "append";
|
||||
maxFiles?: number;
|
||||
replaceAll?: boolean;
|
||||
importedURL?: boolean;
|
||||
paste?: {
|
||||
mode: GenerateMode;
|
||||
contextKey: string;
|
||||
insertReferences: boolean;
|
||||
selection?: { text: string; start: number; end: number };
|
||||
};
|
||||
};
|
||||
|
||||
type MentionState = {
|
||||
start: number;
|
||||
query: string;
|
||||
@@ -180,7 +196,9 @@ export function CreateStudio({ initialMode = "image" }: { initialMode?: Generate
|
||||
image: "",
|
||||
video: ""
|
||||
});
|
||||
const [materials, setMaterials] = useState<PromptMaterial[]>([]);
|
||||
const [materials, setMaterialState] = useState<PromptMaterial[]>([]);
|
||||
const materialsRef = useRef<PromptMaterial[]>([]);
|
||||
const uploadingRef = useRef(false);
|
||||
const [imageTemplates, setImageTemplates] = useState<ImageTemplate[]>([]);
|
||||
const [taskAssets, setTaskAssets] = useState<Asset[]>([]);
|
||||
const [recentJobs, setRecentJobs] = useState<GenerationJob[]>([]);
|
||||
@@ -242,6 +260,9 @@ export function CreateStudio({ initialMode = "image" }: { initialMode?: Generate
|
||||
const evolinkModelChosenRef = useRef(false);
|
||||
|
||||
const generateMode: GenerateMode = mode === "video" ? "video" : "image";
|
||||
const pasteContextKey = [generateMode, imageCreationMode, imageEngine, evolinkModel, videoEngine, videoModel, activeTemplateId, templateEditorOpen, previewTemplate?.id, taskDetailJobId].join("|");
|
||||
const pasteContextRef = useRef(pasteContextKey);
|
||||
pasteContextRef.current = pasteContextKey;
|
||||
const prompt = promptByMode[generateMode];
|
||||
const selectedJimengInfluence = jimengInfluenceOptions.find((option) => option.id === jimengInfluence) || jimengInfluenceOptions[1];
|
||||
const selectedEvolinkQuality = evolinkQualityOptions.find((option) => option.id === evolinkQuality) || evolinkQualityOptions[1];
|
||||
@@ -316,8 +337,9 @@ export function CreateStudio({ initialMode = "image" }: { initialMode?: Generate
|
||||
&& missingMaterialPlaceholders.length === 0
|
||||
&& missingAnnotationPlaceholders.length === 0
|
||||
&& minimaxMaterialsValid;
|
||||
const submitDisabled = busy || !generationInputReady;
|
||||
const submitTitle = !minimaxMaterialsValid
|
||||
const submitDisabled = busy || uploading || !generationInputReady;
|
||||
const submitTitle = uploading ? "素材上传或导入中,请稍候"
|
||||
: !minimaxMaterialsValid
|
||||
? "MiniMax H3 第一版仅支持 0 或 1 张图片素材"
|
||||
: unsupportedSpecializedPlaceholders.length
|
||||
? `交互编辑和图层拆分不支持 ${unsupportedSpecializedPlaceholders.map((placeholder) => placeholder.token).join("、")}`
|
||||
@@ -576,6 +598,14 @@ export function CreateStudio({ initialMode = "image" }: { initialMode?: Generate
|
||||
setPromptByMode((items) => ({ ...items, [generateMode]: value }));
|
||||
}
|
||||
|
||||
// Keep the latest list available to asynchronous uploads without putting
|
||||
// side effects inside a React state updater (which can run more than once).
|
||||
function setMaterials(value: PromptMaterial[] | ((items: PromptMaterial[]) => PromptMaterial[])) {
|
||||
const next = typeof value === "function" ? value(materialsRef.current) : value;
|
||||
materialsRef.current = next;
|
||||
setMaterialState(next);
|
||||
}
|
||||
|
||||
function selectImageCreationMode(nextMode: SeedreamCreationMode) {
|
||||
setImageCreationMode(nextMode);
|
||||
setMentionState(null);
|
||||
@@ -651,10 +681,87 @@ export function CreateStudio({ initialMode = "image" }: { initialMode?: Generate
|
||||
});
|
||||
const payload = await response.json();
|
||||
if (!response.ok) throw new Error(payload.error || "上传失败");
|
||||
if (!Array.isArray(payload.assets) || payload.assets.length !== files.length) {
|
||||
throw new Error("上传结果不完整,请刷新素材后重试。");
|
||||
}
|
||||
return payload.assets as Asset[];
|
||||
}
|
||||
|
||||
async function uploadFiles(files: FileList | null, options: { targetLabel?: string; expectedType?: MaterialKind; seedreamSourceMode?: "single" | "append"; maxFiles?: number; replaceAll?: boolean } = {}) {
|
||||
function handleImagePaste(event: ClipboardEvent<HTMLElement>) {
|
||||
if (templateEditorOpen || previewTemplate || taskDetailJobId) return;
|
||||
const files = clipboardImageFiles(event.clipboardData);
|
||||
const imageURL = files.length ? null : clipboardImageURL(event.clipboardData);
|
||||
if (!files.length && !imageURL) return; // Ordinary text and mixed text/URLs keep native paste behavior.
|
||||
// A URL is only a candidate: do not fetch it or intercept text paste without consent.
|
||||
// Show only the hostname, never a signed query string or credentials.
|
||||
if (imageURL && !window.confirm(`是否将来自 ${new URL(imageURL).hostname} 的链接导入为图片素材?\n确定后由服务器下载并保存图片;取消则按普通文字粘贴。`)) return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (uploadingRef.current || busy) {
|
||||
setError(uploadingRef.current ? "素材正在上传或导入,请稍后再粘贴。" : "任务正在提交,请稍后再粘贴。");
|
||||
return;
|
||||
}
|
||||
const options: MaterialUploadOptions = { expectedType: "image" };
|
||||
if (specializedImageMode) {
|
||||
options.seedreamSourceMode = imageCreationMode === "interactive" ? "append" : "single";
|
||||
if (options.seedreamSourceMode === "append" && seedreamInteractiveSourceKeys.length >= 10) {
|
||||
setError("Seedream 5.0 Pro 交互编辑最多支持 10 张输入图片。");
|
||||
return;
|
||||
}
|
||||
if (imageCreationMode === "layers") {
|
||||
if (files[0] && !isSeedreamLayerInputFile(files[0])) {
|
||||
setError("Seedream 5.0 Pro 图层拆分只支持 PNG 或 JPEG 输入图片。");
|
||||
return;
|
||||
}
|
||||
if (selectedSeedreamSource && !window.confirm("替换当前待拆分图片吗?当前标注会清空,原图片仍保留在素材中。")) return;
|
||||
}
|
||||
} else if (minimaxVideoMode) {
|
||||
options.maxFiles = 1;
|
||||
options.replaceAll = true;
|
||||
if (materials.length && !window.confirm("MiniMax H3 只支持一张首帧图,是否替换当前素材?")) return;
|
||||
} else if ((generateMode === "image" && imageEngine === "seedream") || (generateMode === "video" && videoEngine === "bailian")) {
|
||||
const limit = generateMode === "image" ? 10 : 2;
|
||||
const remaining = Math.max(0, limit - materials.filter((material) => material.type === "image").length);
|
||||
if (!remaining) {
|
||||
setError(generateMode === "image" ? "Seedream 5.0 Pro 最多支持 10 张参考图,请先移除图片。" : "百炼图生视频最多支持 2 张图片,请先移除图片。");
|
||||
return;
|
||||
}
|
||||
options.maxFiles = remaining;
|
||||
}
|
||||
const textarea = event.target === promptRef.current ? promptRef.current : null;
|
||||
options.paste = {
|
||||
mode: generateMode,
|
||||
contextKey: pasteContextKey,
|
||||
// Workspace inputs are selected directly and have their own sequential
|
||||
// image numbering; don't turn an empty automatic-split prompt into text.
|
||||
insertReferences: !specializedImageMode,
|
||||
selection: textarea ? { text: prompt, start: textarea.selectionStart, end: textarea.selectionEnd } : undefined
|
||||
};
|
||||
if (imageURL) {
|
||||
options.importedURL = true;
|
||||
void receiveMaterialAssets(() => importImageURL(imageURL), options, 1);
|
||||
} else {
|
||||
void uploadFiles(files, options);
|
||||
}
|
||||
}
|
||||
|
||||
async function importImageURL(url: string): Promise<Asset[]> {
|
||||
const response = await fetch("/api/assets/import-image", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ url })
|
||||
});
|
||||
const payload = await response.json().catch(() => null);
|
||||
if (!response.ok) throw new Error(payload?.error || "图片链接导入失败,请检查链接是否有效或稍后重试。");
|
||||
const asset = payload?.asset;
|
||||
if (!asset || asset.kind !== "image" || !asset.id || !asset.url || !asset.name) {
|
||||
throw new Error("图片导入结果不完整,请刷新素材后重试。");
|
||||
}
|
||||
return [asset as Asset];
|
||||
}
|
||||
|
||||
async function uploadFiles(files: FileList | File[] | null, options: MaterialUploadOptions = {}) {
|
||||
if (uploadingRef.current || busy) return;
|
||||
const allFiles = Array.from(files || []);
|
||||
const remainingInteractiveSlots = Math.max(0, 10 - seedreamInteractiveSourceKeys.length);
|
||||
const selectedFiles = options.seedreamSourceMode === "single"
|
||||
@@ -677,28 +784,69 @@ export function CreateStudio({ initialMode = "image" }: { initialMode?: Generate
|
||||
setError("Seedream 5.0 Pro 图层拆分只支持 PNG 或 JPEG 输入图片。");
|
||||
return;
|
||||
}
|
||||
await receiveMaterialAssets(() => uploadAssetFiles(selectedFiles), options, allFiles.length);
|
||||
}
|
||||
|
||||
// File paste and URL imports must share the same lock, context checks, source
|
||||
// selection and reference insertion; a remote URL never goes straight to a model.
|
||||
async function receiveMaterialAssets(loadAssets: () => Promise<Asset[]>, options: MaterialUploadOptions, requestedCount: number) {
|
||||
if (uploadingRef.current || busy) return;
|
||||
uploadingRef.current = true;
|
||||
setUploading(true);
|
||||
setError(null);
|
||||
setNotice(null);
|
||||
try {
|
||||
const uploaded = await uploadAssetFiles(selectedFiles);
|
||||
if (expectedType && uploaded.some((asset) => materialTypeForAsset(asset) !== expectedType)) {
|
||||
throw new Error(`请上传${shortTypeName(expectedType)}素材。`);
|
||||
const uploaded = await loadAssets();
|
||||
if (options.paste && options.paste.contextKey !== pasteContextRef.current) {
|
||||
setNotice("创作模式或编辑目标已切换,本次粘贴未加入当前任务,请重新粘贴。");
|
||||
return;
|
||||
}
|
||||
setMaterials((items) => {
|
||||
const next = options.replaceAll ? [] : [...items];
|
||||
uploaded.forEach((asset, index) => {
|
||||
const targetLabel = index === 0 ? normalizeMaterialLabel(options.targetLabel) : undefined;
|
||||
const material = materialFromAsset(asset, next, targetLabel);
|
||||
const existingIndex = targetLabel ? next.findIndex((item) => normalizeMaterialLabel(item.label) === targetLabel) : -1;
|
||||
if (existingIndex >= 0) {
|
||||
next[existingIndex] = material;
|
||||
return;
|
||||
}
|
||||
next.push(material);
|
||||
});
|
||||
return next;
|
||||
if (options.expectedType && uploaded.some((asset) => materialTypeForAsset(asset) !== options.expectedType)) {
|
||||
throw new Error(`请上传${shortTypeName(options.expectedType)}素材。`);
|
||||
}
|
||||
if (options.importedURL && options.seedreamSourceMode === "single"
|
||||
&& uploaded.some((asset) => !["image/png", "image/jpeg"].includes(String(asset.metadata?.contentType)))) {
|
||||
throw new Error("此图片已导入素材,但图层拆分只支持 PNG 或 JPEG,请使用对应格式的图片。");
|
||||
}
|
||||
const next = options.replaceAll ? [] : [...materialsRef.current];
|
||||
const addedMaterials: PromptMaterial[] = [];
|
||||
uploaded.forEach((asset, index) => {
|
||||
const targetLabel = index === 0 ? normalizeMaterialLabel(options.targetLabel) : undefined;
|
||||
const material = materialFromAsset(asset, next, targetLabel);
|
||||
addedMaterials.push(material);
|
||||
const existingIndex = targetLabel ? next.findIndex((item) => normalizeMaterialLabel(item.label) === targetLabel) : -1;
|
||||
if (existingIndex >= 0) {
|
||||
next[existingIndex] = material;
|
||||
return;
|
||||
}
|
||||
next.push(material);
|
||||
});
|
||||
setMaterials(next);
|
||||
if (options.paste?.insertReferences) {
|
||||
const pasted = options.paste;
|
||||
const labels = addedMaterials.map((material) => material.label || "");
|
||||
const textarea = promptRef.current;
|
||||
const selection = pasted.selection;
|
||||
const restoreCaret = textarea && selection && document.activeElement === textarea
|
||||
&& textarea.value === selection.text
|
||||
&& textarea.selectionStart === selection.start && textarea.selectionEnd === selection.end;
|
||||
const expectedPrompt = restoreCaret ? insertClipboardImageReferences(selection.text, labels, selection) : null;
|
||||
setPromptByMode((items) => ({
|
||||
...items,
|
||||
[pasted.mode]: insertClipboardImageReferences(items[pasted.mode], labels, selection)
|
||||
}));
|
||||
// Do not steal focus or move a cursor the user changed while uploading.
|
||||
if (restoreCaret && expectedPrompt !== null && expectedPrompt !== selection.text) {
|
||||
const cursor = expectedPrompt.length - (selection.text.length - selection.end);
|
||||
window.requestAnimationFrame(() => {
|
||||
if (document.activeElement === textarea && textarea.value === expectedPrompt) {
|
||||
textarea.setSelectionRange(cursor, cursor);
|
||||
}
|
||||
});
|
||||
}
|
||||
setMentionState(null);
|
||||
setMaterialDraft(null);
|
||||
}
|
||||
if (options.seedreamSourceMode === "single" && uploaded[0]) {
|
||||
setSeedreamSourceKey(uploaded[0].id || uploaded[0].url);
|
||||
setSeedreamLayerAnnotations([]);
|
||||
@@ -707,11 +855,17 @@ export function CreateStudio({ initialMode = "image" }: { initialMode?: Generate
|
||||
const uploadedKeys = uploaded.map((asset) => asset.id || asset.url);
|
||||
setSeedreamInteractiveSourceKeys((keys) => [...new Set([...keys, ...uploadedKeys])].slice(0, 10));
|
||||
setSeedreamSourceKey(uploadedKeys[0]);
|
||||
if (allFiles.length > selectedFiles.length) setNotice(`已添加 ${selectedFiles.length} 张图片,交互编辑最多支持 10 张输入图。`);
|
||||
if (requestedCount > uploaded.length) setNotice(`已添加 ${uploaded.length} 张图片,交互编辑最多支持 10 张输入图。`);
|
||||
}
|
||||
if (options.paste) {
|
||||
setNotice(requestedCount > uploaded.length
|
||||
? `当前模式仅接收本次前 ${uploaded.length} 张图片,其余图片未上传。`
|
||||
: `已${options.importedURL ? "导入" : "粘贴"} ${uploaded.length} 张图片${options.paste.insertReferences ? "并加入提示词引用" : "并选为输入图"}。`);
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : String(err));
|
||||
} finally {
|
||||
uploadingRef.current = false;
|
||||
setUploading(false);
|
||||
}
|
||||
}
|
||||
@@ -1141,6 +1295,7 @@ export function CreateStudio({ initialMode = "image" }: { initialMode?: Generate
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
if (uploadingRef.current || busy) return;
|
||||
if (unsupportedSpecializedPlaceholders.length) {
|
||||
setError(`${imageCreationMode === "layers" ? "图层拆分仅支持 1 张输入图片" : "交互编辑仅支持最多 10 张输入图片,且不接受视频或音频"},请删除 ${unsupportedSpecializedPlaceholders.map((placeholder) => placeholder.token).join("、")}。`);
|
||||
return;
|
||||
@@ -1351,6 +1506,7 @@ export function CreateStudio({ initialMode = "image" }: { initialMode?: Generate
|
||||
expectedType: "image",
|
||||
seedreamSourceMode: imageCreationMode === "interactive" ? "append" : "single"
|
||||
})}
|
||||
onPasteImage={handleImagePaste}
|
||||
onInsertAnnotation={insertAtCursor}
|
||||
/>
|
||||
) : null}
|
||||
@@ -1364,6 +1520,7 @@ export function CreateStudio({ initialMode = "image" }: { initialMode?: Generate
|
||||
type="file"
|
||||
multiple={minimaxVideoMode ? false : !specializedImageMode || imageCreationMode === "interactive"}
|
||||
accept={minimaxVideoMode ? "image/*" : specializedImageMode ? imageCreationMode === "layers" ? "image/png,image/jpeg" : "image/*" : materialUploadAccept}
|
||||
disabled={uploading || busy}
|
||||
onChange={(event) => {
|
||||
void uploadFiles(event.target.files, minimaxVideoMode
|
||||
? { expectedType: "image", maxFiles: 1, replaceAll: true }
|
||||
@@ -1386,6 +1543,7 @@ export function CreateStudio({ initialMode = "image" }: { initialMode?: Generate
|
||||
id="createPrompt"
|
||||
ref={promptRef}
|
||||
value={prompt}
|
||||
onPaste={handleImagePaste}
|
||||
onChange={(event) => {
|
||||
handlePromptInput(event.target.value, event.target.selectionStart);
|
||||
}}
|
||||
@@ -1426,6 +1584,8 @@ export function CreateStudio({ initialMode = "image" }: { initialMode?: Generate
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<small>{uploading ? "素材上传或导入中,请稍候…" : "支持 Ctrl+V / ⌘+V 粘贴截图、图片或图片下载地址;链接确认后导入,文字可照常粘贴。"}</small>
|
||||
|
||||
{unsupportedSpecializedPlaceholders.length ? (
|
||||
<div className="specialized-prompt-warning" role="alert">
|
||||
{imageCreationMode === "layers" ? "图层拆分仅接受 1 张输入图片" : "交互编辑仅接受最多 10 张输入图片,不接受视频或音频"},请从提示词中删除
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useRef, useState, type PointerEvent as ReactPointerEvent, type KeyboardEvent as ReactKeyboardEvent } from "react";
|
||||
import { useEffect, useMemo, useRef, useState, type ClipboardEvent as ReactClipboardEvent, type PointerEvent as ReactPointerEvent, type KeyboardEvent as ReactKeyboardEvent } from "react";
|
||||
import { BoxSelect, Brush, Check, Hand, ImageIcon, Loader2, MapPin, Maximize2, Minimize2, Redo2, ScanLine, Trash2, Undo2, Upload, X, ZoomIn, ZoomOut } from "lucide-react";
|
||||
import clsx from "clsx";
|
||||
import { materialPreviewUrl } from "@/lib/client/asset-urls";
|
||||
@@ -37,6 +37,7 @@ export function SeedreamWorkspace({
|
||||
onSelectSource,
|
||||
onToggleSource,
|
||||
onUpload,
|
||||
onPasteImage,
|
||||
onInsertAnnotation
|
||||
}: {
|
||||
mode: Exclude<SeedreamCreationMode, "basic">;
|
||||
@@ -49,6 +50,7 @@ export function SeedreamWorkspace({
|
||||
onSelectSource: (material: PromptMaterial) => void;
|
||||
onToggleSource: (material: PromptMaterial) => void;
|
||||
onUpload: (files: FileList | null) => void;
|
||||
onPasteImage?: (event: ReactClipboardEvent<HTMLElement>) => void;
|
||||
onInsertAnnotation: (token: string) => void;
|
||||
}) {
|
||||
const [tool, setTool] = useState<AnnotationTool>("box");
|
||||
@@ -324,7 +326,15 @@ export function SeedreamWorkspace({
|
||||
const currentImageIndex = sourceIndexByKey.get(sourceKey) || 1;
|
||||
|
||||
return (
|
||||
<section ref={workspaceRef} className={clsx("seedream-workspace", mode === "interactive" ? "interactive" : "layers")} aria-label={mode === "interactive" ? "Seedream 交互编辑" : "Seedream 图层拆分"}>
|
||||
<section
|
||||
ref={workspaceRef}
|
||||
className={clsx("seedream-workspace", mode === "interactive" ? "interactive" : "layers")}
|
||||
aria-label={mode === "interactive" ? "Seedream 交互编辑" : "Seedream 图层拆分"}
|
||||
onPaste={(event) => {
|
||||
if (event.target instanceof HTMLElement && event.target.closest("input, textarea, [contenteditable]")) return;
|
||||
onPasteImage?.(event);
|
||||
}}
|
||||
>
|
||||
<header className="seedream-workspace-head">
|
||||
<div>
|
||||
<strong>{mode === "interactive" ? "精准编辑画布" : "指定拆分区域"}</strong>
|
||||
@@ -334,10 +344,19 @@ export function SeedreamWorkspace({
|
||||
</header>
|
||||
|
||||
{!source ? (
|
||||
<div className="seedream-source-empty">
|
||||
<div
|
||||
className="seedream-source-empty"
|
||||
tabIndex={0}
|
||||
aria-label="图片画布:可粘贴截图或复制的图片"
|
||||
onPointerDown={(event) => {
|
||||
if (event.target instanceof Element && event.target.closest("label, button, input")) return;
|
||||
event.currentTarget.focus({ preventScroll: true });
|
||||
}}
|
||||
>
|
||||
<span className="seedream-source-empty-icon" aria-hidden="true"><ImageIcon size={24} /></span>
|
||||
<strong>{sources.length ? "选择参与生成的图片" : "先上传一张图片"}</strong>
|
||||
<span>{mode === "interactive" ? "可以选择多张图片进行跨图定位编辑" : "模板效果预览图不会自动参与拆分"}</span>
|
||||
<span>支持 Ctrl+V / ⌘+V 粘贴图片或图片下载地址,链接确认后导入</span>
|
||||
<label className="button primary">
|
||||
{uploading ? <Loader2 className="spin" size={16} /> : <Upload size={16} />}
|
||||
上传图片
|
||||
@@ -428,7 +447,7 @@ export function SeedreamWorkspace({
|
||||
</div>
|
||||
</div>
|
||||
<div className="seedream-canvas-note">
|
||||
<span>滚轮缩放 · 空格 / 中键拖动 · 放大后可继续标注 · 适应画布恢复全图</span>
|
||||
<span>滚轮缩放 · 空格 / 中键拖动 · 放大后可继续标注 · 适应画布恢复全图 · Ctrl+V / ⌘+V 粘贴图片或图片地址</span>
|
||||
{mode === "interactive"
|
||||
? "蓝色涂鸦会在提交时自动合成到对应输入图;框选和点选会转换成官方坐标标签。"
|
||||
: "蓝色涂鸦会合成到待拆图片;框选会转换为 <bbox> 坐标,不会修改原始素材。"}
|
||||
|
||||
Reference in New Issue
Block a user