fix: unify detail processing placeholders
This commit is contained in:
parent
74ae88edbb
commit
07854d7c6a
@ -479,23 +479,24 @@ function buildScaleAdjustPrompt(
|
||||
.join("\n");
|
||||
|
||||
return [
|
||||
"输入图片1是原始干净图,也是最终输出必须继承的唯一画面基底。",
|
||||
"输入图片2是定位参考图:它只用于告诉你用户框选的目标主体位置。输入图片2里的彩色框、编号、标签、半透明遮罩、文字和 UI 只是定位标记,绝不能出现在最终画面中。",
|
||||
"输入图片1是定位参考图:它只用于告诉你用户框选的目标主体位置。输入图片1里的彩色框、编号、标签、半透明遮罩、文字和 UI 只是定位标记,绝不能出现在最终画面中。",
|
||||
"输入图片2是原始干净图,也是最终输出必须继承的唯一画面基底。如果系统或模型只能读取一张参考图,则必须使用输入图片1中的框选位置定位目标,同时主动去除所有定位标记并恢复干净画面。",
|
||||
"系统已根据用户框选区域计算出下方每个区域的原图坐标;框选区域只是定位目标主体的搜索范围,不是最终画面的裁切块、贴片或蒙版边界。",
|
||||
"最终结果必须保持输入图片1的原始画幅比例、构图边界、镜头透视、光线、色温、材质质感和清晰度。",
|
||||
"最终结果必须保持原始干净图的画幅比例、构图边界、镜头透视、光线、色温、材质质感和清晰度。",
|
||||
"请不要生成任何彩色框、矩形边框、编号圆点、提示词气泡、标注文字、UI 控件或半透明色块。",
|
||||
"",
|
||||
`任务:对用户框选的主体物体做“比例调整”,统一执行${actionLabel}。目标主体调整后的视觉尺寸必须约为原主体的 ${targetLabel},普通用户肉眼应能清楚看出主体尺寸已经变化;如果输出看起来和原图一样,就是失败。`,
|
||||
regionLines,
|
||||
"",
|
||||
"关键要求:",
|
||||
`1. 只调整目标主体物体本身的整体视觉比例,必须等比缩放到约 ${targetLabel},不得保留原尺寸,不得压扁、拉伸、扭曲、改变外观结构、改变材质、改变颜色、改变文字或图案。`,
|
||||
`1. 只调整目标主体物体本身的整体视觉比例,必须等比缩放到约 ${targetLabel},不得保留原尺寸;请把主体轮廓宽度和高度都调整到约 ${targetLabel},而不是仅改变亮度、清晰度、颜色或画面裁切。`,
|
||||
"2. 严禁把整个矩形框选区域当成图片块整体缩放、平移、复制、裁切或贴回;最终画面不得出现矩形拼接边缘、色差硬边、补丁感、裁切线或局部贴片痕迹。",
|
||||
"3. 缩小时,目标主体应围绕原位置视觉中心自然变小;主体缩小后露出的区域必须根据周围真实背景、阴影、遮挡、纹理和透视进行无缝补全。",
|
||||
"4. 放大时,目标主体应围绕原位置视觉中心自然变大;边缘与遮挡关系要合理,不能变形,也不能产生突兀裁切。",
|
||||
`3. 缩小时,目标主体应围绕原位置视觉中心自然变小到约 ${targetLabel};主体缩小后,原来被遮挡的背景、桌面、墙面、阴影、纹理和透视必须被重新生成并无缝补全,选区内应出现明显新增留白或背景。`,
|
||||
`4. 放大时,目标主体应围绕原位置视觉中心自然变大到约 ${targetLabel};边缘与遮挡关系要合理,不能变形,也不能产生突兀裁切。`,
|
||||
"5. 框选区域内非目标主体的背景、墙面、桌面、植物、文字和其它物体应保持原场景逻辑,不要跟着主体一起缩放。",
|
||||
"6. 未受比例调整影响的其它画面区域必须尽量保持原图不变,不允许整体重绘、改色、补光、移动物体或改变背景细节。",
|
||||
"7. 最终输出必须是一张完整、干净、无缝的照片,不得出现任何选区框、说明文字、对比线、比例数字或 UI 元素。",
|
||||
"8. 输出前自检:如果目标主体大小与原图相比没有明显变化,必须重新生成;禁止返回看起来和原图相同的结果。",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
@ -607,7 +608,7 @@ export async function POST(request: NextRequest) {
|
||||
region.referenceImages.map((reference) => reference.url)
|
||||
);
|
||||
const referenceImages = isScaleAdjustOperation
|
||||
? [sourceImageUrl, annotatedUpload.url, ...regionReferenceImageUrls]
|
||||
? [annotatedUpload.url, sourceImageUrl, ...regionReferenceImageUrls]
|
||||
: [sourceImageUrl, ...regionReferenceImageUrls];
|
||||
const resolvedAspectRatio = resolveLocalEditAspectRatio(
|
||||
body.aspectRatio,
|
||||
|
||||
@ -10,6 +10,7 @@ import {
|
||||
useRef,
|
||||
useState,
|
||||
type CSSProperties,
|
||||
type PointerEvent as ReactPointerEvent,
|
||||
type ReactNode,
|
||||
type SyntheticEvent,
|
||||
} from "react";
|
||||
@ -23,6 +24,7 @@ import {
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
Download,
|
||||
GitCompareArrows,
|
||||
ImagePlus,
|
||||
Loader2,
|
||||
Maximize2,
|
||||
@ -733,6 +735,7 @@ type DirectDetailGenerationImage = {
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
copy?: string;
|
||||
compareSourceUrl?: string | null;
|
||||
};
|
||||
|
||||
type DirectDetailReviewResult = {
|
||||
@ -757,6 +760,21 @@ function isHttpImageUrl(url?: string) {
|
||||
return /^https?:\/\//i.test(String(url || "").trim());
|
||||
}
|
||||
|
||||
function normalizeResultCompareSourceUrl(
|
||||
resultUrl?: string | null,
|
||||
sourceUrl?: string | null
|
||||
) {
|
||||
const normalizedResultUrl = String(resultUrl || "").trim();
|
||||
const normalizedSourceUrl = String(sourceUrl || "").trim();
|
||||
if (!isHttpImageUrl(normalizedSourceUrl)) {
|
||||
return null;
|
||||
}
|
||||
if (normalizedResultUrl && normalizedSourceUrl === normalizedResultUrl) {
|
||||
return null;
|
||||
}
|
||||
return normalizedSourceUrl;
|
||||
}
|
||||
|
||||
function getDetailLocalEditUploadFileName(file: File) {
|
||||
const extension = file.name.toLowerCase().split(".").pop() || "jpg";
|
||||
const baseName =
|
||||
@ -809,8 +827,22 @@ function getDetailScaleAdjustLabel(scalePercent: DetailResultScalePercent) {
|
||||
return scalePercent > 0 ? `放大 ${scalePercent}%` : `缩小 ${Math.abs(scalePercent)}%`;
|
||||
}
|
||||
|
||||
function getDetailScaleAdjustTargetLabel(scalePercent: DetailResultScalePercent) {
|
||||
return `${Math.round((1 + scalePercent / 100) * 100)}%`;
|
||||
}
|
||||
|
||||
function getDetailScaleAdjustRegionPrompt(scalePercent: DetailResultScalePercent | null) {
|
||||
return scalePercent ? `比例调整:${getDetailScaleAdjustLabel(scalePercent)}` : "比例调整";
|
||||
if (!scalePercent) return "比例调整";
|
||||
const actionLabel = getDetailScaleAdjustLabel(scalePercent);
|
||||
const targetLabel = getDetailScaleAdjustTargetLabel(scalePercent);
|
||||
const scaleVerb = scalePercent > 0 ? "放大" : "缩小";
|
||||
return [
|
||||
`对框选范围内的主物体执行等比${actionLabel},调整后主体视觉尺寸必须约为原来的 ${targetLabel},需要肉眼明显看出已经${scaleVerb}。`,
|
||||
"只改变目标主体的尺寸,不改变主体外观、颜色、材质、图案和文字内容;不要把整个框选矩形当成图片块缩放。",
|
||||
scalePercent < 0
|
||||
? "主体缩小后露出的背景、桌面、墙面、阴影和遮挡关系必须自然补全。"
|
||||
: "主体放大后边缘、遮挡和透视必须自然衔接。",
|
||||
].join(" ");
|
||||
}
|
||||
|
||||
function DetailResultLocalEditDialog({
|
||||
@ -1182,6 +1214,7 @@ function DetailResultLocalEditDialog({
|
||||
) : null}
|
||||
{submitting ? (
|
||||
<DetailResultProcessingScanOverlay
|
||||
imageUrl={target.imageUrl}
|
||||
label={isScaleAdjustMode ? "比例调整处理中" : "局部编辑处理中"}
|
||||
/>
|
||||
) : null}
|
||||
@ -1705,6 +1738,7 @@ interface ReplacementResultRecord {
|
||||
text: string;
|
||||
status: Extract<ReplacementCardStatus, "completed" | "passthrough" | "fallback">;
|
||||
updatedAt: number;
|
||||
compareSourceUrl?: string | null;
|
||||
}
|
||||
|
||||
interface ReplacementExecutionContext {
|
||||
@ -1929,68 +1963,162 @@ function saveStoredAgentPendingGenerations(items: LocalGenerationPendingState[])
|
||||
}
|
||||
}
|
||||
|
||||
function DetailResultProcessingScanOverlay({ label }: { label: string }) {
|
||||
const DETAIL_RESULT_PROCESSING_STAGES = [
|
||||
"解码图像",
|
||||
"定位选区",
|
||||
"分离主体",
|
||||
"重绘背景",
|
||||
"融合边缘",
|
||||
"检查细节",
|
||||
"输出结果",
|
||||
];
|
||||
|
||||
function DetailResultProcessingScanOverlay({
|
||||
imageUrl,
|
||||
label,
|
||||
}: {
|
||||
imageUrl?: string;
|
||||
label: string;
|
||||
}) {
|
||||
const [progress, setProgress] = useState(6);
|
||||
|
||||
useEffect(() => {
|
||||
const startedAt = Date.now();
|
||||
const durationMs = 240_000;
|
||||
const timer = window.setInterval(() => {
|
||||
const elapsed = Date.now() - startedAt;
|
||||
const linear = Math.min(1, elapsed / durationMs);
|
||||
const eased = 1 - Math.pow(1 - linear, 1.8);
|
||||
const drift = Math.sin(elapsed / 3200) * 1.2;
|
||||
const next = Math.max(6, Math.min(96, Math.round(eased * 96 + drift)));
|
||||
setProgress((current) => Math.max(current, next));
|
||||
}, 720);
|
||||
|
||||
return () => window.clearInterval(timer);
|
||||
}, [imageUrl, label]);
|
||||
|
||||
const stageIndex = Math.min(
|
||||
DETAIL_RESULT_PROCESSING_STAGES.length - 1,
|
||||
Math.floor((progress / 100) * DETAIL_RESULT_PROCESSING_STAGES.length)
|
||||
);
|
||||
const stageName = DETAIL_RESULT_PROCESSING_STAGES[stageIndex] || "处理中";
|
||||
|
||||
return (
|
||||
<div className="absolute inset-0 z-30 overflow-hidden rounded-[inherit] bg-[#030605]/48">
|
||||
<motion.div
|
||||
<div className="absolute inset-0 z-30 overflow-hidden rounded-[inherit] bg-[#07090f]">
|
||||
<style>{`
|
||||
@keyframes detailResultScanBreatheBlur {
|
||||
0%, 100% { filter: blur(22px) brightness(0.78) saturate(0.86); transform: scale(1.13); }
|
||||
50% { filter: blur(26px) brightness(0.9) saturate(0.94); transform: scale(1.16); }
|
||||
}
|
||||
@keyframes detailResultScanSweep {
|
||||
0% { top: -7%; opacity: 0; }
|
||||
8% { opacity: 1; }
|
||||
92% { opacity: 1; }
|
||||
100% { top: 107%; opacity: 0; }
|
||||
}
|
||||
@keyframes detailResultScanPulseRing {
|
||||
0% { box-shadow: 0 0 0 0 rgba(125, 211, 252, 0.48); }
|
||||
70% { box-shadow: 0 0 0 8px rgba(125, 211, 252, 0); }
|
||||
100% { box-shadow: 0 0 0 0 rgba(125, 211, 252, 0); }
|
||||
}
|
||||
`}</style>
|
||||
{isHttpImageUrl(imageUrl) ? (
|
||||
<img
|
||||
alt=""
|
||||
aria-hidden
|
||||
className="absolute inset-0 h-full w-full object-cover"
|
||||
src={imageUrl}
|
||||
style={{ animation: "detailResultScanBreatheBlur 6s ease-in-out infinite" }}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
aria-hidden
|
||||
className="absolute inset-0"
|
||||
style={{
|
||||
background:
|
||||
"radial-gradient(58% 46% at 50% 42%, rgba(125,211,252,0.16), transparent 62%), radial-gradient(58% 48% at 80% 105%, rgba(167,139,250,0.14), transparent 64%), #0d111c",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
aria-hidden
|
||||
className="absolute inset-0 opacity-[0.36]"
|
||||
animate={{
|
||||
backgroundPosition: ["0px 0px, 0px 0px", "0px 24px, 24px 0px"],
|
||||
}}
|
||||
transition={{ duration: 2.6, repeat: Infinity, ease: "linear" }}
|
||||
className="absolute inset-0 pointer-events-none"
|
||||
style={{
|
||||
backgroundImage:
|
||||
"linear-gradient(rgba(255,255,255,0.13) 0px, rgba(255,255,255,0.13) 1px, transparent 1px, transparent 24px), linear-gradient(90deg, rgba(16,185,129,0.1) 0px, rgba(16,185,129,0.1) 1px, transparent 1px, transparent 24px)",
|
||||
backgroundSize: "100% 24px, 24px 100%",
|
||||
background:
|
||||
"radial-gradient(ellipse at center, transparent 52%, rgba(0,0,0,0.48) 100%), repeating-linear-gradient(0deg, rgba(255,255,255,0.018) 0px, rgba(255,255,255,0.018) 1px, transparent 2px, transparent 4px)",
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
aria-hidden
|
||||
className="absolute inset-0 opacity-80"
|
||||
style={{
|
||||
background:
|
||||
"radial-gradient(circle at 50% 50%, rgba(16,185,129,0.12), transparent 44%), linear-gradient(180deg, rgba(255,255,255,0.08), transparent 18%, transparent 82%, rgba(255,255,255,0.08))",
|
||||
}}
|
||||
/>
|
||||
<motion.div
|
||||
aria-hidden
|
||||
className="absolute inset-x-[-6%] h-[34%] min-h-[150px]"
|
||||
animate={{ top: ["-36%", "108%"], opacity: [0.3, 1, 0.46] }}
|
||||
transition={{ duration: 3.8, repeat: Infinity, ease: [0.33, 0, 0.67, 1] }}
|
||||
className="absolute inset-x-0 top-0 z-40 h-px pointer-events-none"
|
||||
style={{ animation: "detailResultScanSweep 2.6s cubic-bezier(0.45, 0.05, 0.55, 0.95) infinite" }}
|
||||
>
|
||||
<div
|
||||
className="absolute inset-0"
|
||||
className="absolute inset-x-0 bottom-0 h-[110px]"
|
||||
style={{
|
||||
background:
|
||||
"linear-gradient(180deg, transparent 0%, rgba(16,185,129,0.04) 18%, rgba(34,211,238,0.1) 38%, rgba(255,255,255,0.72) 50%, rgba(34,211,238,0.1) 62%, rgba(16,185,129,0.04) 82%, transparent 100%)",
|
||||
"linear-gradient(180deg, transparent, rgba(125,211,252,0.1) 55%, rgba(167,139,250,0.16))",
|
||||
filter: "blur(12px)",
|
||||
}}
|
||||
/>
|
||||
<div className="absolute inset-x-[3%] top-1/2 h-[2px] -translate-y-1/2 bg-white shadow-[0_0_10px_rgba(255,255,255,0.86),0_0_18px_rgba(45,212,191,0.58)]" />
|
||||
<div className="absolute inset-x-[4%] top-[calc(50%-18px)] h-px bg-emerald-100/55" />
|
||||
<div className="absolute inset-x-[4%] top-[calc(50%+18px)] h-px bg-cyan-100/42" />
|
||||
<div
|
||||
className="absolute inset-x-[5%] top-[calc(50%-44px)] h-[88px] opacity-55"
|
||||
className="absolute inset-x-0 top-0 h-[38px]"
|
||||
style={{
|
||||
background:
|
||||
"repeating-linear-gradient(90deg, rgba(255,255,255,0.38) 0 1px, transparent 1px 12px)",
|
||||
maskImage:
|
||||
"linear-gradient(90deg, transparent 0%, black 16%, black 84%, transparent 100%)",
|
||||
WebkitMaskImage:
|
||||
"linear-gradient(90deg, transparent 0%, black 16%, black 84%, transparent 100%)",
|
||||
"linear-gradient(0deg, transparent, rgba(255,255,255,0.1))",
|
||||
filter: "blur(6px)",
|
||||
}}
|
||||
/>
|
||||
</motion.div>
|
||||
<motion.div
|
||||
aria-hidden
|
||||
className="absolute inset-y-0 w-px bg-emerald-100/36"
|
||||
animate={{ left: ["8%", "92%", "8%"], opacity: [0.08, 0.35, 0.08] }}
|
||||
transition={{ duration: 5.4, repeat: Infinity, ease: "easeInOut" }}
|
||||
/>
|
||||
<div className="absolute left-3 top-3 inline-flex items-center gap-2 rounded-full border border-emerald-100/70 bg-zinc-950/88 px-3 py-1.5 text-[11px] font-semibold text-emerald-50 shadow-[0_14px_36px_rgba(0,0,0,0.5),0_0_18px_rgba(16,185,129,0.2)] backdrop-blur-md">
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin text-emerald-200" />
|
||||
<span>{label}</span>
|
||||
</div>
|
||||
<div
|
||||
aria-hidden
|
||||
className="absolute inset-x-0 top-0 z-50 h-px pointer-events-none"
|
||||
style={{
|
||||
animation: "detailResultScanSweep 2.6s cubic-bezier(0.45, 0.05, 0.55, 0.95) infinite",
|
||||
background:
|
||||
"linear-gradient(90deg, transparent, rgba(167,139,250,0.7) 18%, rgba(255,255,255,0.98) 50%, rgba(125,211,252,0.7) 82%, transparent)",
|
||||
boxShadow:
|
||||
"0 0 6px rgba(255,255,255,0.7), 0 0 18px rgba(125,211,252,0.55), 0 0 34px rgba(167,139,250,0.35)",
|
||||
}}
|
||||
/>
|
||||
<div className="absolute right-3 top-3 z-[60] inline-flex items-center gap-1.5 rounded-full border border-white/10 bg-[#07090f]/60 px-2.5 py-1.5 text-[11px] font-medium text-slate-100 shadow-[0_12px_28px_rgba(0,0,0,0.28)] backdrop-blur-md">
|
||||
<span
|
||||
className="h-1.5 w-1.5 rounded-full bg-sky-300"
|
||||
style={{ animation: "detailResultScanPulseRing 1.8s ease-out infinite" }}
|
||||
/>
|
||||
<span>{label}</span>
|
||||
<span className="min-w-[34px] text-right tabular-nums text-slate-400">{progress}%</span>
|
||||
</div>
|
||||
<div className="absolute bottom-3 left-3 z-[60] inline-flex items-center gap-2 rounded-full border border-white/10 bg-[#07090f]/60 px-2.5 py-1.5 text-[11px] font-medium text-slate-400 shadow-[0_12px_28px_rgba(0,0,0,0.25)] backdrop-blur-md">
|
||||
<span className="tabular-nums text-sky-300">{String(stageIndex + 1).padStart(2, "0")}</span>
|
||||
<span>{stageName}</span>
|
||||
</div>
|
||||
<div className="absolute inset-x-0 bottom-0 z-[60] h-0.5 bg-white/5">
|
||||
<div
|
||||
className="h-full rounded-r-full shadow-[0_0_8px_rgba(125,211,252,0.6)] transition-[width] duration-300 ease-linear"
|
||||
style={{
|
||||
background: "linear-gradient(90deg, #7dd3fc, #a78bfa)",
|
||||
width: `${progress}%`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DetailResultProcessingPlaceholderCard({
|
||||
imageUrl,
|
||||
label,
|
||||
}: {
|
||||
imageUrl?: string | null;
|
||||
label: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="relative h-full w-full overflow-hidden rounded-[20px] bg-[#07090f]">
|
||||
<DetailResultProcessingScanOverlay
|
||||
imageUrl={String(imageUrl || "").trim() || undefined}
|
||||
label={label}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -2012,6 +2140,169 @@ function ResultProcessedHintBubble({ label }: { label: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
function DetailResultCompareImage({
|
||||
resultUrl,
|
||||
compareSourceUrl,
|
||||
alt,
|
||||
imageClassName,
|
||||
imageStyle,
|
||||
previewWidth = 720,
|
||||
previewQuality = 78,
|
||||
onClick,
|
||||
onError,
|
||||
toggleClassName,
|
||||
}: {
|
||||
resultUrl: string;
|
||||
compareSourceUrl?: string | null;
|
||||
alt: string;
|
||||
imageClassName?: string;
|
||||
imageStyle?: CSSProperties;
|
||||
previewWidth?: number;
|
||||
previewQuality?: number;
|
||||
onClick?: () => void;
|
||||
onError?: (event: SyntheticEvent<HTMLImageElement, Event>) => void;
|
||||
toggleClassName?: string;
|
||||
}) {
|
||||
const compareFrameRef = useRef<HTMLDivElement | null>(null);
|
||||
const [isCompareMode, setIsCompareMode] = useState(false);
|
||||
const [compareSplitPercent, setCompareSplitPercent] = useState(50);
|
||||
const [isDraggingCompare, setIsDraggingCompare] = useState(false);
|
||||
const sourceUrl = normalizeResultCompareSourceUrl(resultUrl, compareSourceUrl);
|
||||
|
||||
useEffect(() => {
|
||||
setIsCompareMode(false);
|
||||
setCompareSplitPercent(50);
|
||||
setIsDraggingCompare(false);
|
||||
}, [resultUrl, sourceUrl]);
|
||||
|
||||
const updateCompareSplit = useCallback((clientX: number) => {
|
||||
const frame = compareFrameRef.current;
|
||||
if (!frame) return;
|
||||
const rect = frame.getBoundingClientRect();
|
||||
if (rect.width <= 0) return;
|
||||
const nextPercent = ((clientX - rect.left) / rect.width) * 100;
|
||||
setCompareSplitPercent(Math.max(5, Math.min(95, nextPercent)));
|
||||
}, []);
|
||||
|
||||
const handleComparePointerDown = useCallback(
|
||||
(event: ReactPointerEvent<HTMLDivElement>) => {
|
||||
if (!sourceUrl) return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
updateCompareSplit(event.clientX);
|
||||
setIsDraggingCompare(true);
|
||||
try {
|
||||
event.currentTarget.setPointerCapture(event.pointerId);
|
||||
} catch {
|
||||
// Pointer capture can fail if the browser has already cancelled the pointer.
|
||||
}
|
||||
},
|
||||
[sourceUrl, updateCompareSplit]
|
||||
);
|
||||
|
||||
const handleComparePointerMove = useCallback(
|
||||
(event: ReactPointerEvent<HTMLDivElement>) => {
|
||||
if (!isDraggingCompare || !sourceUrl) return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
updateCompareSplit(event.clientX);
|
||||
},
|
||||
[isDraggingCompare, sourceUrl, updateCompareSplit]
|
||||
);
|
||||
|
||||
const handleComparePointerEnd = useCallback(
|
||||
(event: ReactPointerEvent<HTMLDivElement>) => {
|
||||
if (!isDraggingCompare) return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setIsDraggingCompare(false);
|
||||
try {
|
||||
event.currentTarget.releasePointerCapture(event.pointerId);
|
||||
} catch {
|
||||
// Ignore release failures for cancelled pointer sequences.
|
||||
}
|
||||
},
|
||||
[isDraggingCompare]
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={compareFrameRef}
|
||||
className={cn("relative", isCompareMode && sourceUrl ? "cursor-col-resize select-none" : null)}
|
||||
onPointerDown={isCompareMode && sourceUrl ? handleComparePointerDown : undefined}
|
||||
onPointerMove={isCompareMode && sourceUrl ? handleComparePointerMove : undefined}
|
||||
onPointerUp={isCompareMode && sourceUrl ? handleComparePointerEnd : undefined}
|
||||
onPointerCancel={isCompareMode && sourceUrl ? handleComparePointerEnd : undefined}
|
||||
>
|
||||
<AgentPreviewImage
|
||||
src={resultUrl}
|
||||
alt={alt}
|
||||
className={imageClassName}
|
||||
style={imageStyle}
|
||||
previewWidth={previewWidth}
|
||||
previewQuality={previewQuality}
|
||||
onError={onError}
|
||||
onClick={onClick}
|
||||
/>
|
||||
{isCompareMode && sourceUrl ? (
|
||||
<>
|
||||
<div
|
||||
className="pointer-events-none absolute inset-0 z-10 overflow-hidden"
|
||||
style={{ clipPath: `inset(0 0 0 ${compareSplitPercent}%)` }}
|
||||
>
|
||||
<AgentPreviewImage
|
||||
src={sourceUrl}
|
||||
alt={`${alt} 原图`}
|
||||
className={cn(imageClassName, "absolute inset-0 h-full w-full object-contain")}
|
||||
style={imageStyle}
|
||||
previewWidth={previewWidth}
|
||||
previewQuality={previewQuality}
|
||||
eager
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="pointer-events-none absolute inset-y-0 z-20 w-[2px] -translate-x-1/2 bg-white shadow-[0_0_0_1px_rgba(0,0,0,0.32),0_0_22px_rgba(255,255,255,0.72)]"
|
||||
style={{ left: `${compareSplitPercent}%` }}
|
||||
/>
|
||||
<div
|
||||
className="pointer-events-none absolute top-1/2 z-20 flex h-9 w-9 -translate-x-1/2 -translate-y-1/2 items-center justify-center rounded-full border border-white/70 bg-zinc-950/82 text-white shadow-[0_12px_34px_rgba(0,0,0,0.48)] backdrop-blur-md"
|
||||
style={{ left: `${compareSplitPercent}%` }}
|
||||
>
|
||||
<GitCompareArrows className="h-4 w-4" />
|
||||
</div>
|
||||
<div className="pointer-events-none absolute bottom-14 left-3 z-20 rounded-full border border-white/35 bg-zinc-950/78 px-2.5 py-1 text-[10px] font-semibold text-white shadow-[0_10px_28px_rgba(0,0,0,0.38)] backdrop-blur-md">
|
||||
新图
|
||||
</div>
|
||||
<div className="pointer-events-none absolute bottom-14 right-3 z-20 rounded-full border border-white/35 bg-zinc-950/78 px-2.5 py-1 text-[10px] font-semibold text-white shadow-[0_10px_28px_rgba(0,0,0,0.38)] backdrop-blur-md">
|
||||
原图
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
{sourceUrl ? (
|
||||
<button
|
||||
type="button"
|
||||
title={isCompareMode ? "关闭划线对比" : "开启划线对比"}
|
||||
aria-label={isCompareMode ? "关闭划线对比" : "开启划线对比"}
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setIsCompareMode((current) => !current);
|
||||
}}
|
||||
className={cn(
|
||||
IMAGE_OVERLAY_ACTION_CLASS,
|
||||
"absolute right-14 top-3 z-40 flex h-9 w-9 items-center justify-center px-0 py-0",
|
||||
isCompareMode && "border-emerald-200/80 bg-emerald-600/92 text-white",
|
||||
toggleClassName
|
||||
)}
|
||||
>
|
||||
<GitCompareArrows className="h-4 w-4" />
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AnimatedGenerationPlaceholderCard(params: {
|
||||
title: string;
|
||||
description: string;
|
||||
@ -2257,6 +2548,9 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
const [processedResultHintByUrl, setProcessedResultHintByUrl] = useState<
|
||||
Record<string, string>
|
||||
>({});
|
||||
const [resultCompareSourceByUrl, setResultCompareSourceByUrl] = useState<
|
||||
Record<string, string>
|
||||
>({});
|
||||
const [replacementResultState, setReplacementResultState] = useState<ReplacementResultState>(
|
||||
() => createEmptyReplacementResultState()
|
||||
);
|
||||
@ -3565,20 +3859,36 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
return;
|
||||
}
|
||||
const inferredStatus = inferReplacementCardStatusFromText(result.text);
|
||||
const previousRecord = nextModeState[cardId];
|
||||
const referenceCard =
|
||||
currentResultModeType === "detail_replacement"
|
||||
? detailReplacementReferenceCards.find((card) => card.id === cardId)
|
||||
: skuReplacementReferenceCards.find((card) => card.id === cardId);
|
||||
const resultUrl = result.url || null;
|
||||
const previousUrl =
|
||||
previousRecord?.url && previousRecord.url !== resultUrl
|
||||
? previousRecord.url
|
||||
: null;
|
||||
const referenceSourceUrl =
|
||||
referenceCard?.imageUrl || referenceCard?.previewUrl || null;
|
||||
const nextRecord: ReplacementResultRecord = {
|
||||
url: result.url || null,
|
||||
url: resultUrl,
|
||||
text: result.text || "",
|
||||
status:
|
||||
inferredStatus === "passthrough" || inferredStatus === "fallback"
|
||||
? inferredStatus
|
||||
: "completed",
|
||||
updatedAt: Date.now(),
|
||||
compareSourceUrl: normalizeResultCompareSourceUrl(
|
||||
resultUrl,
|
||||
previousUrl || referenceSourceUrl || previousRecord?.compareSourceUrl || null
|
||||
),
|
||||
};
|
||||
const previousRecord = nextModeState[cardId];
|
||||
if (
|
||||
previousRecord?.url === nextRecord.url &&
|
||||
previousRecord?.text === nextRecord.text &&
|
||||
previousRecord?.status === nextRecord.status
|
||||
previousRecord?.status === nextRecord.status &&
|
||||
previousRecord?.compareSourceUrl === nextRecord.compareSourceUrl
|
||||
) {
|
||||
return;
|
||||
}
|
||||
@ -3595,7 +3905,13 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
[currentResultModeType]: nextModeState,
|
||||
};
|
||||
});
|
||||
}, [currentReplacementExecutionCardIds, currentResultModeType, latestReplacementResults]);
|
||||
}, [
|
||||
currentReplacementExecutionCardIds,
|
||||
currentResultModeType,
|
||||
detailReplacementReferenceCards,
|
||||
latestReplacementResults,
|
||||
skuReplacementReferenceCards,
|
||||
]);
|
||||
const detailReplacementResultUrls = useMemo(
|
||||
() =>
|
||||
detailReplacementReferenceCards
|
||||
@ -3785,6 +4101,11 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
: localPassthroughResultUrl
|
||||
? MANUAL_PASSTHROUGH_RESULT_TEXT
|
||||
: "";
|
||||
const compareSourceUrl = normalizeResultCompareSourceUrl(
|
||||
resultUrl,
|
||||
resultMeta?.compareSourceUrl ||
|
||||
(!localPassthroughResultUrl ? card.imageUrl || card.previewUrl || null : null)
|
||||
);
|
||||
const activeRunIndex =
|
||||
pendingGeneration?.modeType === "detail_replacement" &&
|
||||
Array.isArray(pendingGeneration.requestedCardIds)
|
||||
@ -3795,6 +4116,7 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
index,
|
||||
resultUrl,
|
||||
resultText,
|
||||
compareSourceUrl,
|
||||
queuePosition:
|
||||
activeRunIndex >= 0 && pendingGeneration?.modeType === "detail_replacement"
|
||||
? activeRunIndex + 1
|
||||
@ -3847,6 +4169,11 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
: localPassthroughResultUrl
|
||||
? MANUAL_PASSTHROUGH_RESULT_TEXT
|
||||
: "";
|
||||
const compareSourceUrl = normalizeResultCompareSourceUrl(
|
||||
resultUrl,
|
||||
resultMeta?.compareSourceUrl ||
|
||||
(!localPassthroughResultUrl ? card.imageUrl || card.previewUrl || null : null)
|
||||
);
|
||||
const activeRunIndex =
|
||||
pendingGeneration?.modeType === "sku_replacement" &&
|
||||
Array.isArray(pendingGeneration.requestedCardIds)
|
||||
@ -3857,6 +4184,7 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
index,
|
||||
resultUrl,
|
||||
resultText,
|
||||
compareSourceUrl,
|
||||
status: getReplacementCardStatus({
|
||||
index,
|
||||
resultUrl,
|
||||
@ -4003,6 +4331,9 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
id: url || `grid-result-placeholder-${activeResultModeType}-${index}`,
|
||||
index,
|
||||
url,
|
||||
compareSourceUrl: url
|
||||
? normalizeResultCompareSourceUrl(url, resultCompareSourceByUrl[url])
|
||||
: null,
|
||||
status: url
|
||||
? ("completed" as const)
|
||||
: activeResultModeType === "detail_generation" &&
|
||||
@ -4023,6 +4354,7 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
detailGenerationProgress.status,
|
||||
gridPendingRemainingCount,
|
||||
gridResultCardCount,
|
||||
resultCompareSourceByUrl,
|
||||
]
|
||||
);
|
||||
const shouldShowGridResultSection =
|
||||
@ -4626,11 +4958,16 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
url: string | null;
|
||||
text?: string | null;
|
||||
status?: Extract<ReplacementCardStatus, "completed" | "passthrough" | "fallback">;
|
||||
compareSourceUrl?: string | null;
|
||||
}) => {
|
||||
setReplacementResultState((current) => {
|
||||
const currentModeState = current[params.modeType];
|
||||
const previousRecord = currentModeState[params.cardId];
|
||||
const inferredStatus = inferReplacementCardStatusFromText(params.text);
|
||||
const explicitCompareSourceUrl =
|
||||
params.compareSourceUrl === undefined
|
||||
? undefined
|
||||
: normalizeResultCompareSourceUrl(params.url, params.compareSourceUrl);
|
||||
const nextRecord: ReplacementResultRecord = {
|
||||
url: params.url,
|
||||
text: String(params.text || "").trim(),
|
||||
@ -4642,12 +4979,17 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
previousRecord?.status ||
|
||||
"completed",
|
||||
updatedAt: Date.now(),
|
||||
compareSourceUrl:
|
||||
explicitCompareSourceUrl === undefined
|
||||
? previousRecord?.compareSourceUrl || null
|
||||
: explicitCompareSourceUrl,
|
||||
};
|
||||
|
||||
if (
|
||||
previousRecord?.url === nextRecord.url &&
|
||||
previousRecord?.text === nextRecord.text &&
|
||||
previousRecord?.status === nextRecord.status
|
||||
previousRecord?.status === nextRecord.status &&
|
||||
previousRecord?.compareSourceUrl === nextRecord.compareSourceUrl
|
||||
) {
|
||||
return current;
|
||||
}
|
||||
@ -4728,6 +5070,7 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
url: nextUrl,
|
||||
text: "已完成去水印处理。",
|
||||
status: "completed",
|
||||
compareSourceUrl: rawUrl,
|
||||
});
|
||||
setActiveResultModeType("detail_replacement");
|
||||
setSelectedResultUrl(nextUrl);
|
||||
@ -5080,6 +5423,7 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
url: result.originalUrl,
|
||||
text: "已在当前页面完成文字编辑。",
|
||||
status: "completed",
|
||||
compareSourceUrl: replacementTextEditTarget.imageUrl,
|
||||
});
|
||||
setReplacementTextEditTarget((current) =>
|
||||
current
|
||||
@ -5155,6 +5499,35 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
}
|
||||
return uploadedUrl;
|
||||
}, []);
|
||||
const registerResultCompareSource = useCallback(
|
||||
(resultUrl?: string | null, sourceUrl?: string | null) => {
|
||||
const normalizedResultUrl = String(resultUrl || "").trim();
|
||||
if (!isHttpImageUrl(normalizedResultUrl)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const normalizedSourceUrl = normalizeResultCompareSourceUrl(
|
||||
normalizedResultUrl,
|
||||
sourceUrl
|
||||
);
|
||||
setResultCompareSourceByUrl((current) => {
|
||||
if (!normalizedSourceUrl) {
|
||||
if (!(normalizedResultUrl in current)) return current;
|
||||
const next = { ...current };
|
||||
delete next[normalizedResultUrl];
|
||||
return next;
|
||||
}
|
||||
if (current[normalizedResultUrl] === normalizedSourceUrl) {
|
||||
return current;
|
||||
}
|
||||
return {
|
||||
...current,
|
||||
[normalizedResultUrl]: normalizedSourceUrl,
|
||||
};
|
||||
});
|
||||
},
|
||||
[]
|
||||
);
|
||||
const handleSceneResultResizeComplete = useCallback(
|
||||
async (resizedImageUrl: string) => {
|
||||
if (!onResultPreviewUrlsChange) {
|
||||
@ -5187,15 +5560,26 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
);
|
||||
|
||||
onResultPreviewUrlsChange("scene_generation", nextSceneUrls);
|
||||
registerResultCompareSource(finalUrl, sceneResizeEditorTargetUrl);
|
||||
setActiveResultModeType("scene_generation");
|
||||
setSelectedResultUrl(finalUrl);
|
||||
setSceneResizeEditorTargetUrl(null);
|
||||
toast.success("尺寸调整结果已加入场景结果区");
|
||||
},
|
||||
[mergedResultCollections.scene_generation, onResultPreviewUrlsChange, uploadBase64ResultToStorage]
|
||||
[
|
||||
mergedResultCollections.scene_generation,
|
||||
onResultPreviewUrlsChange,
|
||||
registerResultCompareSource,
|
||||
sceneResizeEditorTargetUrl,
|
||||
uploadBase64ResultToStorage,
|
||||
]
|
||||
);
|
||||
const appendResultUrlToCollection = useCallback(
|
||||
(modeType: AgentGenerationResultModeType, imageUrl: string) => {
|
||||
(
|
||||
modeType: AgentGenerationResultModeType,
|
||||
imageUrl: string,
|
||||
options?: { compareSourceUrl?: string | null }
|
||||
) => {
|
||||
const normalizedUrl = String(imageUrl || "").trim();
|
||||
if (!normalizedUrl || !onResultPreviewUrlsChange) {
|
||||
return false;
|
||||
@ -5208,12 +5592,20 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
])
|
||||
);
|
||||
onResultPreviewUrlsChange(modeType, nextUrls);
|
||||
if (options && "compareSourceUrl" in options) {
|
||||
registerResultCompareSource(normalizedUrl, options.compareSourceUrl);
|
||||
}
|
||||
if (activeResultModeType === modeType) {
|
||||
setSelectedResultUrl(normalizedUrl);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
[activeResultModeType, mergedResultCollections, onResultPreviewUrlsChange]
|
||||
[
|
||||
activeResultModeType,
|
||||
mergedResultCollections,
|
||||
onResultPreviewUrlsChange,
|
||||
registerResultCompareSource,
|
||||
]
|
||||
);
|
||||
const showProcessedResultHint = useCallback((imageUrl: string, label: string) => {
|
||||
const normalizedUrl = String(imageUrl || "").trim();
|
||||
@ -5275,12 +5667,17 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
url: nextUrl,
|
||||
text: "已完成高清处理。",
|
||||
status: "completed",
|
||||
compareSourceUrl: imageUrl,
|
||||
});
|
||||
setActiveResultModeType("detail_replacement");
|
||||
setActiveDetailReferenceCardId(cardId);
|
||||
setSelectedResultUrl(nextUrl);
|
||||
} else {
|
||||
if (!appendResultUrlToCollection("detail_generation", nextUrl)) {
|
||||
if (
|
||||
!appendResultUrlToCollection("detail_generation", nextUrl, {
|
||||
compareSourceUrl: imageUrl,
|
||||
})
|
||||
) {
|
||||
throw new Error("当前项目未接入结果保存能力");
|
||||
}
|
||||
setActiveResultModeType("detail_generation");
|
||||
@ -5383,12 +5780,17 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
url: nextUrl,
|
||||
text: isScaleAdjust ? "已完成比例调整处理。" : "已完成局部编辑处理。",
|
||||
status: "completed",
|
||||
compareSourceUrl: imageUrl,
|
||||
});
|
||||
setActiveResultModeType("detail_replacement");
|
||||
setActiveDetailReferenceCardId(cardId);
|
||||
setSelectedResultUrl(nextUrl);
|
||||
} else {
|
||||
if (!appendResultUrlToCollection("detail_generation", nextUrl)) {
|
||||
if (
|
||||
!appendResultUrlToCollection("detail_generation", nextUrl, {
|
||||
compareSourceUrl: imageUrl,
|
||||
})
|
||||
) {
|
||||
throw new Error("当前项目未接入结果保存能力");
|
||||
}
|
||||
setActiveResultModeType("detail_generation");
|
||||
@ -5440,15 +5842,27 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
);
|
||||
const handlePsdDerivedImageCreated = useCallback(
|
||||
(imageUrl: string) => {
|
||||
appendResultUrlToCollection(psdTemplateDialogResultModeType, imageUrl);
|
||||
appendResultUrlToCollection(psdTemplateDialogResultModeType, imageUrl, {
|
||||
compareSourceUrl: psdTemplateDialogSourceUrl,
|
||||
});
|
||||
},
|
||||
[appendResultUrlToCollection, psdTemplateDialogResultModeType]
|
||||
[
|
||||
appendResultUrlToCollection,
|
||||
psdTemplateDialogResultModeType,
|
||||
psdTemplateDialogSourceUrl,
|
||||
]
|
||||
);
|
||||
const handlePsdRenderedResultCreated = useCallback(
|
||||
(imageUrl: string) => {
|
||||
appendResultUrlToCollection(psdTemplateDialogResultModeType, imageUrl);
|
||||
appendResultUrlToCollection(psdTemplateDialogResultModeType, imageUrl, {
|
||||
compareSourceUrl: psdTemplateDialogSourceUrl,
|
||||
});
|
||||
},
|
||||
[appendResultUrlToCollection, psdTemplateDialogResultModeType]
|
||||
[
|
||||
appendResultUrlToCollection,
|
||||
psdTemplateDialogResultModeType,
|
||||
psdTemplateDialogSourceUrl,
|
||||
]
|
||||
);
|
||||
const handleDeleteResultImage = useCallback(
|
||||
(modeType: AgentGenerationResultModeType, imageUrl: string) => {
|
||||
@ -5477,6 +5891,12 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
delete next[normalizedUrl];
|
||||
return next;
|
||||
});
|
||||
setResultCompareSourceByUrl((current) => {
|
||||
if (!(normalizedUrl in current)) return current;
|
||||
const next = { ...current };
|
||||
delete next[normalizedUrl];
|
||||
return next;
|
||||
});
|
||||
|
||||
onDeleteResultImage(modeType, normalizedUrl);
|
||||
toast.success("结果图已删除");
|
||||
@ -8384,10 +8804,11 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
>
|
||||
{hasResult ? (
|
||||
<div className="relative min-h-[220px] overflow-hidden bg-white/95">
|
||||
<AgentPreviewImage
|
||||
src={card.resultUrl!}
|
||||
<DetailResultCompareImage
|
||||
resultUrl={card.resultUrl!}
|
||||
compareSourceUrl={card.compareSourceUrl}
|
||||
alt={`结果图 ${card.index + 1}`}
|
||||
className="block h-auto w-full cursor-zoom-in"
|
||||
imageClassName="block h-auto w-full cursor-zoom-in"
|
||||
previewWidth={720}
|
||||
previewQuality={78}
|
||||
onError={(event) =>
|
||||
@ -8556,7 +8977,10 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
</button>
|
||||
</div>
|
||||
{processingLabel ? (
|
||||
<DetailResultProcessingScanOverlay label={processingLabel} />
|
||||
<DetailResultProcessingScanOverlay
|
||||
imageUrl={card.resultUrl || card.imageUrl || card.previewUrl || undefined}
|
||||
label={processingLabel}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
) : (
|
||||
@ -8564,18 +8988,14 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
className="aspect-[5/3]"
|
||||
onClick={() => setActiveDetailReferenceCardId(card.id)}
|
||||
>
|
||||
<AnimatedGenerationPlaceholderCard
|
||||
badge={statusMeta.label}
|
||||
badgeClassName={statusMeta.className}
|
||||
animated={card.status === "generating"}
|
||||
showSpinner={card.status === "generating"}
|
||||
title={`结果图 ${card.index + 1}`}
|
||||
description={
|
||||
card.status === "queued" &&
|
||||
card.queuePosition &&
|
||||
card.queueTotal
|
||||
? `批次内排队 ${card.queuePosition}/${card.queueTotal},将按“输入图组 + 第 ${card.index + 1} 张${detailReferenceSourceLabel}参考图”继续生成`
|
||||
: `将按“输入图组 + 第 ${card.index + 1} 张${detailReferenceSourceLabel}参考图”生成`
|
||||
<DetailResultProcessingPlaceholderCard
|
||||
imageUrl={card.imageUrl || card.previewUrl}
|
||||
label={
|
||||
card.status === "generating"
|
||||
? "详情图生成中"
|
||||
: card.status === "queued"
|
||||
? "详情图排队中"
|
||||
: statusMeta.label
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
@ -9086,13 +9506,15 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
className="aspect-[5/3]"
|
||||
onClick={() => setActiveSkuReferenceCardId(card.id)}
|
||||
>
|
||||
<AnimatedGenerationPlaceholderCard
|
||||
badge={statusMeta.label}
|
||||
badgeClassName={statusMeta.className}
|
||||
animated={card.status === "generating"}
|
||||
showSpinner={card.status === "generating"}
|
||||
title={`结果图 ${card.index + 1}`}
|
||||
description={`将按“输入图 + 第 ${card.index + 1} 张${skuReferenceSourceLabel}参考图”生成`}
|
||||
<DetailResultProcessingPlaceholderCard
|
||||
imageUrl={card.imageUrl || card.previewUrl}
|
||||
label={
|
||||
card.status === "generating"
|
||||
? "SKU图生成中"
|
||||
: card.status === "queued"
|
||||
? "SKU图排队中"
|
||||
: statusMeta.label
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@ -9194,10 +9616,11 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
<div className="relative aspect-[4/5] overflow-hidden bg-[linear-gradient(180deg,#090b0c_0%,#111417_100%)]">
|
||||
{card.url ? (
|
||||
<>
|
||||
<AgentPreviewImage
|
||||
src={card.url}
|
||||
<DetailResultCompareImage
|
||||
resultUrl={card.url!}
|
||||
compareSourceUrl={card.compareSourceUrl}
|
||||
alt={`${getResultModeLabel(activeResultModeType)} 结果图 ${card.index + 1}`}
|
||||
className="h-full w-full object-contain p-2"
|
||||
imageClassName="h-full w-full object-contain p-2"
|
||||
previewWidth={720}
|
||||
previewQuality={78}
|
||||
/>
|
||||
@ -9376,30 +9799,47 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
</>
|
||||
) : null}
|
||||
{processingLabel ? (
|
||||
<DetailResultProcessingScanOverlay label={processingLabel} />
|
||||
<DetailResultProcessingScanOverlay
|
||||
imageUrl={card.url!}
|
||||
label={processingLabel}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
) : (
|
||||
<AnimatedGenerationPlaceholderCard
|
||||
badge={card.status === "generating" ? "生成中" : card.status === "queued" ? "排队中" : "待生成"}
|
||||
badgeClassName={
|
||||
card.status === "generating"
|
||||
? "border-sky-400/30 bg-sky-500/[0.10] text-sky-100"
|
||||
: card.status === "queued"
|
||||
? "border-white/12 bg-white/[0.05] text-white/62"
|
||||
: "border-white/10 bg-white/[0.04] text-white/58"
|
||||
}
|
||||
animated={card.status === "generating"}
|
||||
showSpinner={card.status === "generating"}
|
||||
title={`${getResultModeLabel(activeResultModeType)} 结果图 ${card.index + 1}`}
|
||||
description={
|
||||
card.status === "generating"
|
||||
? "当前图片正在生成中,结果返回后会自动替换这张占位卡。"
|
||||
: card.status === "queued"
|
||||
? "前面的结果完成后会继续处理这一张,请保持当前页面即可。"
|
||||
: "开始生成后,这里会出现对应数量的结果预览卡片。"
|
||||
}
|
||||
/>
|
||||
isDetailGenerationCard ? (
|
||||
<DetailResultProcessingPlaceholderCard
|
||||
label={
|
||||
card.status === "generating"
|
||||
? "详情图生成中"
|
||||
: card.status === "queued"
|
||||
? "详情图排队中"
|
||||
: card.status === "missing"
|
||||
? "详情图未回传"
|
||||
: "等待详情生成"
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<AnimatedGenerationPlaceholderCard
|
||||
badge={card.status === "generating" ? "生成中" : card.status === "queued" ? "排队中" : "待生成"}
|
||||
badgeClassName={
|
||||
card.status === "generating"
|
||||
? "border-sky-400/30 bg-sky-500/[0.10] text-sky-100"
|
||||
: card.status === "queued"
|
||||
? "border-white/12 bg-white/[0.05] text-white/62"
|
||||
: "border-white/10 bg-white/[0.04] text-white/58"
|
||||
}
|
||||
animated={card.status === "generating"}
|
||||
showSpinner={card.status === "generating"}
|
||||
title={`${getResultModeLabel(activeResultModeType)} 结果图 ${card.index + 1}`}
|
||||
description={
|
||||
card.status === "generating"
|
||||
? "当前图片正在生成中,结果返回后会自动替换这张占位卡。"
|
||||
: card.status === "queued"
|
||||
? "前面的结果完成后会继续处理这一张,请保持当前页面即可。"
|
||||
: "开始生成后,这里会出现对应数量的结果预览卡片。"
|
||||
}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user